mirror of
https://github.com/Qortal/Qortal-Hub.git
synced 2025-05-31 05:36:59 +00:00
763 lines
22 KiB
TypeScript
763 lines
22 KiB
TypeScript
import React, {
|
|
useCallback,
|
|
useContext,
|
|
useEffect,
|
|
useMemo,
|
|
useState,
|
|
} from 'react';
|
|
import {
|
|
Box,
|
|
Button,
|
|
ButtonBase,
|
|
Dialog,
|
|
DialogActions,
|
|
DialogContent,
|
|
Input,
|
|
MenuItem,
|
|
Select,
|
|
Tab,
|
|
Tabs,
|
|
useTheme,
|
|
} from '@mui/material';
|
|
import { useDropzone } from 'react-dropzone';
|
|
import { useHandlePrivateApps } from '../../hooks/useHandlePrivateApps';
|
|
import {
|
|
groupsPropertiesAtom,
|
|
memberGroupsAtom,
|
|
myGroupsWhereIAmAdminAtom,
|
|
} from '../../atoms/global';
|
|
import { Label } from '../Group/AddGroup';
|
|
import { Spacer } from '../../common/Spacer';
|
|
import {
|
|
AppCircle,
|
|
AppCircleContainer,
|
|
AppCircleLabel,
|
|
PublishQAppChoseFile,
|
|
PublishQAppInfo,
|
|
} from './Apps-styles';
|
|
import AddIcon from '@mui/icons-material/Add';
|
|
import ImageUploader from '../../common/ImageUploader';
|
|
import { QORTAL_APP_CONTEXT } from '../../App';
|
|
import { fileToBase64 } from '../../utils/fileReading';
|
|
import { objectToBase64 } from '../../qdn/encryption/group-encryption';
|
|
import { getFee } from '../../background/background.ts';
|
|
import { useAtom } from 'jotai';
|
|
import { useTranslation } from 'react-i18next';
|
|
import { useSortedMyNames } from '../../hooks/useSortedMyNames';
|
|
|
|
const maxFileSize = 50 * 1024 * 1024; // 50MB
|
|
|
|
export const AppsPrivate = ({ myName, myAddress }) => {
|
|
const [names, setNames] = useState([]);
|
|
const [name, setName] = useState(0);
|
|
|
|
const { openApp } = useHandlePrivateApps();
|
|
const [file, setFile] = useState(null);
|
|
const [logo, setLogo] = useState(null);
|
|
const [qortalUrl, setQortalUrl] = useState('');
|
|
const [selectedGroup, setSelectedGroup] = useState(0);
|
|
|
|
const [valueTabPrivateApp, setValueTabPrivateApp] = useState(0);
|
|
const [groupsProperties] = useAtom(groupsPropertiesAtom);
|
|
const [myGroupsWhereIAmAdminFromGlobal] = useAtom(myGroupsWhereIAmAdminAtom);
|
|
|
|
const myGroupsWhereIAmAdmin = useMemo(() => {
|
|
return myGroupsWhereIAmAdminFromGlobal?.filter(
|
|
(group) => groupsProperties[group?.groupId]?.isOpen === false
|
|
);
|
|
}, [myGroupsWhereIAmAdminFromGlobal, groupsProperties]);
|
|
|
|
const [isOpenPrivateModal, setIsOpenPrivateModal] = useState(false);
|
|
const { show, setInfoSnackCustom, setOpenSnackGlobal } =
|
|
useContext(QORTAL_APP_CONTEXT);
|
|
const [memberGroups] = useAtom(memberGroupsAtom);
|
|
|
|
const theme = useTheme();
|
|
const { t } = useTranslation(['auth', 'core', 'group']);
|
|
|
|
const myGroupsPrivate = useMemo(() => {
|
|
return memberGroups?.filter(
|
|
(group) => groupsProperties[group?.groupId]?.isOpen === false
|
|
);
|
|
}, [memberGroups, groupsProperties]);
|
|
|
|
const [privateAppValues, setPrivateAppValues] = useState({
|
|
name: '',
|
|
service: 'DOCUMENT',
|
|
identifier: '',
|
|
groupId: 0,
|
|
});
|
|
|
|
const [newPrivateAppValues, setNewPrivateAppValues] = useState({
|
|
service: 'DOCUMENT',
|
|
identifier: '',
|
|
name: '',
|
|
});
|
|
|
|
const mySortedNames = useSortedMyNames(names, myName);
|
|
|
|
const { getRootProps, getInputProps } = useDropzone({
|
|
accept: {
|
|
'application/zip': ['.zip'], // Only accept zip files
|
|
},
|
|
maxSize: maxFileSize,
|
|
multiple: false, // Disable multiple file uploads
|
|
onDrop: (acceptedFiles) => {
|
|
if (acceptedFiles.length > 0) {
|
|
setFile(acceptedFiles[0]); // Set the file name
|
|
}
|
|
},
|
|
onDropRejected: (fileRejections) => {
|
|
fileRejections.forEach(({ file, errors }) => {
|
|
errors.forEach((error) => {
|
|
if (error.code === 'file-too-large') {
|
|
console.error(
|
|
t('core:message.error.file_too_large', {
|
|
filename: file.name,
|
|
size: maxFileSize / (1024 * 1024),
|
|
postProcess: 'capitalizeFirstChar',
|
|
})
|
|
);
|
|
}
|
|
});
|
|
});
|
|
},
|
|
});
|
|
|
|
const addPrivateApp = async () => {
|
|
try {
|
|
if (privateAppValues?.groupId === 0) return;
|
|
await openApp(privateAppValues, true);
|
|
} catch (error) {
|
|
console.error(error);
|
|
}
|
|
};
|
|
|
|
const clearFields = () => {
|
|
setPrivateAppValues({
|
|
name: '',
|
|
service: 'DOCUMENT',
|
|
identifier: '',
|
|
groupId: 0,
|
|
});
|
|
setNewPrivateAppValues({
|
|
service: 'DOCUMENT',
|
|
identifier: '',
|
|
name: '',
|
|
});
|
|
setFile(null);
|
|
setValueTabPrivateApp(0);
|
|
setSelectedGroup(0);
|
|
setLogo(null);
|
|
};
|
|
|
|
const publishPrivateApp = async () => {
|
|
try {
|
|
if (selectedGroup === 0) return;
|
|
|
|
if (!logo)
|
|
throw new Error(
|
|
t('core:message.generic.select_image', {
|
|
postProcess: 'capitalizeFirstChar',
|
|
})
|
|
);
|
|
|
|
if (!myName)
|
|
throw new Error(
|
|
t('core:message.generic.name_publish', {
|
|
postProcess: 'capitalizeFirstChar',
|
|
})
|
|
);
|
|
|
|
if (!newPrivateAppValues?.name)
|
|
throw new Error(
|
|
t('core:message.error.app_need_name', {
|
|
postProcess: 'capitalizeFirstChar',
|
|
})
|
|
);
|
|
|
|
const base64Logo = await fileToBase64(logo);
|
|
const base64App = await fileToBase64(file);
|
|
const objectToSave = {
|
|
app: base64App,
|
|
logo: base64Logo,
|
|
name: newPrivateAppValues.name,
|
|
};
|
|
const object64 = await objectToBase64(objectToSave);
|
|
const decryptedData = await window.sendMessage(
|
|
'ENCRYPT_QORTAL_GROUP_DATA',
|
|
{
|
|
base64: object64,
|
|
groupId: selectedGroup,
|
|
}
|
|
);
|
|
|
|
if (decryptedData?.error) {
|
|
throw new Error(
|
|
decryptedData?.error ||
|
|
t('core:message.error.encrypt_app', {
|
|
postProcess: 'capitalizeFirstChar',
|
|
})
|
|
);
|
|
}
|
|
|
|
const fee = await getFee('ARBITRARY');
|
|
|
|
await show({
|
|
message: t('core:message.question.publish_app', {
|
|
postProcess: 'capitalizeFirstChar',
|
|
}),
|
|
publishFee: fee.fee + ' QORT',
|
|
});
|
|
|
|
await new Promise((res, rej) => {
|
|
window
|
|
.sendMessage('publishOnQDN', {
|
|
data: decryptedData,
|
|
identifier: newPrivateAppValues?.identifier,
|
|
service: newPrivateAppValues?.service,
|
|
uploadType: 'base64',
|
|
name,
|
|
})
|
|
.then((response) => {
|
|
if (!response?.error) {
|
|
res(response);
|
|
return;
|
|
}
|
|
rej(response.error);
|
|
})
|
|
.catch((error) => {
|
|
rej(
|
|
error.message ||
|
|
t('core:message.error.generic', {
|
|
postProcess: 'capitalizeFirstChar',
|
|
})
|
|
);
|
|
});
|
|
});
|
|
|
|
openApp(
|
|
{
|
|
identifier: newPrivateAppValues?.identifier,
|
|
service: newPrivateAppValues?.service,
|
|
name,
|
|
groupId: selectedGroup,
|
|
},
|
|
true
|
|
);
|
|
clearFields();
|
|
} catch (error) {
|
|
setOpenSnackGlobal(true);
|
|
setInfoSnackCustom({
|
|
type: 'error',
|
|
message:
|
|
error?.message ||
|
|
t('core:message.error.publish_app', {
|
|
postProcess: 'capitalizeFirstChar',
|
|
}),
|
|
});
|
|
}
|
|
};
|
|
|
|
const handleChange = (event: React.SyntheticEvent, newValue: number) => {
|
|
setValueTabPrivateApp(newValue);
|
|
};
|
|
|
|
function a11yProps(index: number) {
|
|
return {
|
|
id: `simple-tab-${index}`,
|
|
'aria-controls': `simple-tabpanel-${index}`,
|
|
};
|
|
}
|
|
|
|
const getNames = useCallback(async () => {
|
|
if (!myAddress) return;
|
|
try {
|
|
const res = await fetch(
|
|
`${getBaseApiReact()}/names/address/${myAddress}?limit=0`
|
|
);
|
|
const data = await res.json();
|
|
setNames(data?.map((item) => item.name));
|
|
} catch (error) {
|
|
console.error(error);
|
|
}
|
|
}, [myAddress]);
|
|
useEffect(() => {
|
|
getNames();
|
|
}, [getNames]);
|
|
|
|
return (
|
|
<>
|
|
<ButtonBase
|
|
onClick={() => {
|
|
setIsOpenPrivateModal(true);
|
|
}}
|
|
sx={{
|
|
width: '80px',
|
|
}}
|
|
>
|
|
<AppCircleContainer
|
|
sx={{
|
|
gap: '10px',
|
|
}}
|
|
>
|
|
<AppCircle>
|
|
<AddIcon />
|
|
</AppCircle>
|
|
|
|
<AppCircleLabel>Private</AppCircleLabel>
|
|
</AppCircleContainer>
|
|
</ButtonBase>
|
|
|
|
{isOpenPrivateModal && (
|
|
<Dialog
|
|
open={isOpenPrivateModal}
|
|
aria-labelledby="alert-dialog-title"
|
|
aria-describedby="alert-dialog-description"
|
|
onKeyDown={(e) => {
|
|
if (e.key === 'Enter') {
|
|
if (valueTabPrivateApp === 0) {
|
|
if (
|
|
!privateAppValues.name ||
|
|
!privateAppValues.service ||
|
|
!privateAppValues.identifier ||
|
|
!privateAppValues?.groupId
|
|
)
|
|
return;
|
|
addPrivateApp();
|
|
}
|
|
}
|
|
}}
|
|
maxWidth="md"
|
|
fullWidth={true}
|
|
PaperProps={{
|
|
style: {
|
|
backgroundColor: theme.palette.background.paper,
|
|
boxShadow: 'none',
|
|
},
|
|
}}
|
|
>
|
|
<Box>
|
|
<Tabs
|
|
value={valueTabPrivateApp}
|
|
onChange={handleChange}
|
|
aria-label={t('core:basic_tabs_example', {
|
|
postProcess: 'capitalizeFirstChar',
|
|
})}
|
|
variant={'fullWidth'}
|
|
scrollButtons="auto"
|
|
sx={{
|
|
'& .MuiTabs-indicator': {
|
|
backgroundColor: theme.palette.background.default,
|
|
},
|
|
}}
|
|
>
|
|
<Tab
|
|
label={t('core:action.access_app', {
|
|
postProcess: 'capitalizeFirstChar',
|
|
})}
|
|
{...a11yProps(0)}
|
|
sx={{
|
|
'&.Mui-selected': {
|
|
color: theme.palette.text.primary,
|
|
},
|
|
fontSize: '1rem',
|
|
}}
|
|
/>
|
|
<Tab
|
|
label={t('core:action.publish_app', {
|
|
postProcess: 'capitalizeFirstChar',
|
|
})}
|
|
{...a11yProps(1)}
|
|
sx={{
|
|
'&.Mui-selected': {
|
|
color: theme.palette.text.primary,
|
|
},
|
|
fontSize: '1rem',
|
|
}}
|
|
/>
|
|
</Tabs>
|
|
</Box>
|
|
{valueTabPrivateApp === 0 && (
|
|
<>
|
|
<DialogContent>
|
|
<Box
|
|
sx={{
|
|
display: 'flex',
|
|
flexDirection: 'column',
|
|
gap: '5px',
|
|
}}
|
|
>
|
|
<Label>
|
|
{t('group:action.select_group', {
|
|
postProcess: 'capitalizeFirstChar',
|
|
})}
|
|
</Label>
|
|
<Label>
|
|
{t('group:message.generic.only_private_groups', {
|
|
postProcess: 'capitalizeFirstChar',
|
|
})}
|
|
</Label>
|
|
|
|
<Select
|
|
labelId="demo-simple-select-label"
|
|
id="demo-simple-select"
|
|
value={privateAppValues?.groupId}
|
|
label={t('group:group.group_other', {
|
|
postProcess: 'capitalizeFirstChar',
|
|
})}
|
|
onChange={(e) => {
|
|
setPrivateAppValues((prev) => {
|
|
return {
|
|
...prev,
|
|
groupId: e.target.value,
|
|
};
|
|
});
|
|
}}
|
|
>
|
|
<MenuItem value={0}>
|
|
{t('group:message.generic.no_selection', {
|
|
postProcess: 'capitalizeFirstChar',
|
|
})}
|
|
</MenuItem>
|
|
|
|
{myGroupsPrivate
|
|
?.filter((item) => !item?.isOpen)
|
|
.map((group) => {
|
|
return (
|
|
<MenuItem key={group?.groupId} value={group?.groupId}>
|
|
{group?.groupName}
|
|
</MenuItem>
|
|
);
|
|
})}
|
|
</Select>
|
|
</Box>
|
|
|
|
<Spacer height="10px" />
|
|
|
|
<Box
|
|
sx={{
|
|
display: 'flex',
|
|
flexDirection: 'column',
|
|
gap: '5px',
|
|
marginTop: '15px',
|
|
}}
|
|
>
|
|
<Label>
|
|
{t('core:name', { postProcess: 'capitalizeFirstChar' })}
|
|
</Label>
|
|
<Input
|
|
placeholder={t('core:name', {
|
|
postProcess: 'capitalizeFirstChar',
|
|
})}
|
|
value={privateAppValues?.name}
|
|
onChange={(e) =>
|
|
setPrivateAppValues((prev) => {
|
|
return {
|
|
...prev,
|
|
name: e.target.value,
|
|
};
|
|
})
|
|
}
|
|
/>
|
|
</Box>
|
|
|
|
<Box
|
|
sx={{
|
|
display: 'flex',
|
|
flexDirection: 'column',
|
|
gap: '5px',
|
|
marginTop: '15px',
|
|
}}
|
|
>
|
|
<Label>
|
|
{t('core:identifier', {
|
|
postProcess: 'capitalizeFirstChar',
|
|
})}
|
|
</Label>
|
|
|
|
<Input
|
|
placeholder={t('core:identifier', {
|
|
postProcess: 'capitalizeFirstChar',
|
|
})}
|
|
value={privateAppValues?.identifier}
|
|
onChange={(e) =>
|
|
setPrivateAppValues((prev) => {
|
|
return {
|
|
...prev,
|
|
identifier: e.target.value,
|
|
};
|
|
})
|
|
}
|
|
/>
|
|
</Box>
|
|
</DialogContent>
|
|
|
|
<DialogActions>
|
|
<Button
|
|
variant="contained"
|
|
onClick={() => {
|
|
setIsOpenPrivateModal(false);
|
|
}}
|
|
>
|
|
{t('core:action.close', {
|
|
postProcess: 'capitalizeFirstChar',
|
|
})}
|
|
</Button>
|
|
<Button
|
|
disabled={
|
|
!privateAppValues.name ||
|
|
!privateAppValues.service ||
|
|
!privateAppValues.identifier ||
|
|
!privateAppValues?.groupId
|
|
}
|
|
variant="contained"
|
|
onClick={() => addPrivateApp()}
|
|
autoFocus
|
|
>
|
|
{t('core:action.access', {
|
|
postProcess: 'capitalizeFirstChar',
|
|
})}
|
|
</Button>
|
|
</DialogActions>
|
|
</>
|
|
)}
|
|
{valueTabPrivateApp === 1 && (
|
|
<>
|
|
<DialogContent>
|
|
<PublishQAppInfo
|
|
sx={{
|
|
backgroundColor: theme.palette.background.paper,
|
|
fontSize: '14px',
|
|
}}
|
|
>
|
|
{t('core:message.generic.select_zip', {
|
|
postProcess: 'capitalizeFirstChar',
|
|
})}
|
|
</PublishQAppInfo>
|
|
|
|
<Spacer height="10px" />
|
|
|
|
<PublishQAppInfo
|
|
sx={{
|
|
backgroundColor: theme.palette.background.paper,
|
|
fontSize: '14px',
|
|
}}
|
|
>{`
|
|
50mb MB max`}</PublishQAppInfo>
|
|
{file && (
|
|
<>
|
|
<Spacer height="5px" />
|
|
|
|
<PublishQAppInfo>{`Selected: (${file?.name})`}</PublishQAppInfo>
|
|
</>
|
|
)}
|
|
|
|
<Spacer height="18px" />
|
|
|
|
<PublishQAppChoseFile
|
|
sx={{
|
|
backgroundColor: theme.palette.background.default,
|
|
fontSize: '14px',
|
|
}}
|
|
{...getRootProps()}
|
|
>
|
|
{' '}
|
|
<input {...getInputProps()} />
|
|
{file
|
|
? t('core:action.change_file', {
|
|
postProcess: 'capitalizeFirstChar',
|
|
})
|
|
: t('core:action.choose_file', {
|
|
postProcess: 'capitalizeFirstChar',
|
|
})}
|
|
</PublishQAppChoseFile>
|
|
<Spacer height="20px" />
|
|
|
|
<Box
|
|
sx={{
|
|
display: 'flex',
|
|
flexDirection: 'column',
|
|
gap: '5px',
|
|
}}
|
|
>
|
|
<Label>Select a Qortal name</Label>
|
|
|
|
<Select
|
|
labelId="demo-simple-select-label"
|
|
id="demo-simple-select"
|
|
value={name}
|
|
label="Groups where you are an admin"
|
|
onChange={(e) => setName(e.target.value)}
|
|
>
|
|
<MenuItem value={0}>No name selected</MenuItem>
|
|
{mySortedNames.map((name) => {
|
|
return (
|
|
<MenuItem key={name} value={name}>
|
|
{name}
|
|
</MenuItem>
|
|
);
|
|
})}
|
|
</Select>
|
|
</Box>
|
|
<Spacer height="20px" />
|
|
|
|
<Box
|
|
sx={{
|
|
display: 'flex',
|
|
flexDirection: 'column',
|
|
gap: '5px',
|
|
}}
|
|
>
|
|
<Label>
|
|
{t('group:action.select_group', {
|
|
postProcess: 'capitalizeFirstChar',
|
|
})}
|
|
</Label>
|
|
|
|
<Label>
|
|
{t('group:amessage.generic.admin_only', {
|
|
postProcess: 'capitalizeFirstChar',
|
|
})}
|
|
</Label>
|
|
|
|
<Select
|
|
labelId="demo-simple-select-label"
|
|
id="demo-simple-select"
|
|
value={selectedGroup}
|
|
label={t('group:group.groups_admin', {
|
|
postProcess: 'capitalizeFirstChar',
|
|
})}
|
|
onChange={(e) => setSelectedGroup(e.target.value)}
|
|
>
|
|
<MenuItem value={0}>
|
|
{t('group:message.generic.no_selection', {
|
|
postProcess: 'capitalizeFirstChar',
|
|
})}
|
|
</MenuItem>
|
|
|
|
{myGroupsWhereIAmAdmin
|
|
?.filter((item) => !item?.isOpen)
|
|
.map((group) => {
|
|
return (
|
|
<MenuItem key={group?.groupId} value={group?.groupId}>
|
|
{group?.groupName}
|
|
</MenuItem>
|
|
);
|
|
})}
|
|
</Select>
|
|
</Box>
|
|
|
|
<Spacer height="20px" />
|
|
|
|
<Box
|
|
sx={{
|
|
display: 'flex',
|
|
flexDirection: 'column',
|
|
gap: '5px',
|
|
marginTop: '15px',
|
|
}}
|
|
>
|
|
<Label>
|
|
{t('core:identifier', {
|
|
postProcess: 'capitalizeFirstChar',
|
|
})}
|
|
</Label>
|
|
<Input
|
|
placeholder={t('core:identifier', {
|
|
postProcess: 'capitalizeFirstChar',
|
|
})}
|
|
value={newPrivateAppValues?.identifier}
|
|
onChange={(e) =>
|
|
setNewPrivateAppValues((prev) => {
|
|
return {
|
|
...prev,
|
|
identifier: e.target.value,
|
|
};
|
|
})
|
|
}
|
|
/>
|
|
</Box>
|
|
|
|
<Spacer height="10px" />
|
|
|
|
<Box
|
|
sx={{
|
|
display: 'flex',
|
|
flexDirection: 'column',
|
|
gap: '5px',
|
|
marginTop: '15px',
|
|
}}
|
|
>
|
|
<Label>
|
|
{t('core:app_name', { postProcess: 'capitalizeFirstChar' })}
|
|
</Label>
|
|
|
|
<Input
|
|
placeholder={t('core:app_name', {
|
|
postProcess: 'capitalizeFirstChar',
|
|
})}
|
|
value={newPrivateAppValues?.name}
|
|
onChange={(e) =>
|
|
setNewPrivateAppValues((prev) => {
|
|
return {
|
|
...prev,
|
|
name: e.target.value,
|
|
};
|
|
})
|
|
}
|
|
/>
|
|
</Box>
|
|
|
|
<Spacer height="10px" />
|
|
|
|
<ImageUploader onPick={(file) => setLogo(file)}>
|
|
<Button variant="contained">
|
|
{t('core:action.choose_logo', {
|
|
postProcess: 'capitalizeFirstChar',
|
|
})}
|
|
</Button>
|
|
</ImageUploader>
|
|
|
|
{logo?.name}
|
|
|
|
<Spacer height="25px" />
|
|
</DialogContent>
|
|
|
|
<DialogActions>
|
|
<Button
|
|
variant="contained"
|
|
onClick={() => {
|
|
setIsOpenPrivateModal(false);
|
|
clearFields();
|
|
}}
|
|
>
|
|
{t('core:action.close', {
|
|
postProcess: 'capitalizeFirstChar',
|
|
})}
|
|
</Button>
|
|
|
|
<Button
|
|
disabled={
|
|
!newPrivateAppValues.name ||
|
|
!newPrivateAppValues.service ||
|
|
!newPrivateAppValues.identifier ||
|
|
!selectedGroup
|
|
}
|
|
variant="contained"
|
|
onClick={() => publishPrivateApp()}
|
|
autoFocus
|
|
>
|
|
{t('core:action.publish', {
|
|
postProcess: 'capitalizeFirstChar',
|
|
})}
|
|
</Button>
|
|
</DialogActions>
|
|
</>
|
|
)}
|
|
</Dialog>
|
|
)}
|
|
</>
|
|
);
|
|
};
|