mirror of
https://github.com/Qortal/Qortal-Hub.git
synced 2025-06-26 01:11:20 +00:00
551 lines
17 KiB
TypeScript
551 lines
17 KiB
TypeScript
import React, { useContext, useMemo, useState } from "react";
|
|
import {
|
|
Avatar,
|
|
Box,
|
|
Button,
|
|
ButtonBase,
|
|
Dialog,
|
|
DialogActions,
|
|
DialogContent,
|
|
DialogTitle,
|
|
Input,
|
|
MenuItem,
|
|
Select,
|
|
Tab,
|
|
Tabs,
|
|
Typography,
|
|
} from "@mui/material";
|
|
import { useDropzone } from "react-dropzone";
|
|
import { useHandlePrivateApps } from "./useHandlePrivateApps";
|
|
import { useRecoilState, useSetRecoilState } from "recoil";
|
|
import { groupsPropertiesAtom, myGroupsWhereIAmAdminAtom } from "../../atoms/global";
|
|
import { Label } from "../Group/AddGroup";
|
|
import { Spacer } from "../../common/Spacer";
|
|
import {
|
|
Add,
|
|
AppCircle,
|
|
AppCircleContainer,
|
|
AppCircleLabel,
|
|
PublishQAppChoseFile,
|
|
PublishQAppInfo,
|
|
} from "./Apps-styles";
|
|
import ImageUploader from "../../common/ImageUploader";
|
|
import { isMobile, MyContext } from "../../App";
|
|
import { fileToBase64 } from "../../utils/fileReading";
|
|
import { objectToBase64 } from "../../qdn/encryption/group-encryption";
|
|
import { getFee } from "../../background";
|
|
|
|
const maxFileSize = 50 * 1024 * 1024; // 50MB
|
|
|
|
export const AppsPrivate = ({myName}) => {
|
|
const { openApp } = useHandlePrivateApps();
|
|
const [file, setFile] = useState(null);
|
|
const [logo, setLogo] = useState(null);
|
|
const [qortalUrl, setQortalUrl] = useState("");
|
|
const [selectedGroup, setSelectedGroup] = useState(0);
|
|
const [groupsProperties] = useRecoilState(groupsPropertiesAtom)
|
|
const [valueTabPrivateApp, setValueTabPrivateApp] = useState(0);
|
|
const [myGroupsWhereIAmAdminFromGlobal] = useRecoilState(
|
|
myGroupsWhereIAmAdminAtom
|
|
);
|
|
|
|
const myGroupsWhereIAmAdmin = useMemo(()=> {
|
|
return myGroupsWhereIAmAdminFromGlobal?.filter((group)=> groupsProperties[group?.groupId]?.isOpen === false)
|
|
}, [myGroupsWhereIAmAdminFromGlobal, groupsProperties])
|
|
const [isOpenPrivateModal, setIsOpenPrivateModal] = useState(false);
|
|
const { show, setInfoSnackCustom, setOpenSnackGlobal, memberGroups } = useContext(MyContext);
|
|
|
|
|
|
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 { 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(
|
|
`File ${file.name} is too large. Max size allowed is ${
|
|
maxFileSize / (1024 * 1024)
|
|
} MB.`
|
|
);
|
|
}
|
|
});
|
|
});
|
|
},
|
|
});
|
|
|
|
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("Please select an image for a logo");
|
|
if (!myName) throw new Error("You need a Qortal name to publish");
|
|
if (!newPrivateAppValues?.name) throw new Error("Your app needs a name");
|
|
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 || "Unable to encrypt app. App not published"
|
|
);
|
|
}
|
|
const fee = await getFee("ARBITRARY");
|
|
|
|
await show({
|
|
message: "Would you like to publish this app?",
|
|
publishFee: fee.fee + " QORT",
|
|
});
|
|
await new Promise((res, rej) => {
|
|
window
|
|
.sendMessage("publishOnQDN", {
|
|
data: decryptedData,
|
|
identifier: newPrivateAppValues?.identifier,
|
|
service: newPrivateAppValues?.service,
|
|
})
|
|
.then((response) => {
|
|
if (!response?.error) {
|
|
res(response);
|
|
return;
|
|
}
|
|
rej(response.error);
|
|
})
|
|
.catch((error) => {
|
|
rej(error.message || "An error occurred");
|
|
});
|
|
});
|
|
openApp(
|
|
{
|
|
identifier: newPrivateAppValues?.identifier,
|
|
service: newPrivateAppValues?.service,
|
|
name: myName,
|
|
groupId: selectedGroup,
|
|
},
|
|
true
|
|
);
|
|
clearFields();
|
|
} catch (error) {
|
|
setOpenSnackGlobal(true)
|
|
setInfoSnackCustom({
|
|
type: "error",
|
|
message: error?.message || "Unable to publish app",
|
|
});
|
|
}
|
|
};
|
|
|
|
const handleChange = (event: React.SyntheticEvent, newValue: number) => {
|
|
setValueTabPrivateApp(newValue);
|
|
};
|
|
|
|
function a11yProps(index: number) {
|
|
return {
|
|
id: `simple-tab-${index}`,
|
|
"aria-controls": `simple-tabpanel-${index}`,
|
|
};
|
|
}
|
|
return (
|
|
<>
|
|
<ButtonBase
|
|
onClick={() => {
|
|
setIsOpenPrivateModal(true);
|
|
}}
|
|
sx={{
|
|
width: "80px",
|
|
}}
|
|
>
|
|
<AppCircleContainer
|
|
sx={{
|
|
gap: !isMobile ? "10px" : "5px",
|
|
}}
|
|
>
|
|
<AppCircle>
|
|
<Add>+</Add>
|
|
</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}
|
|
>
|
|
<DialogTitle id="alert-dialog-title">
|
|
{valueTabPrivateApp === 0
|
|
? "Access private app"
|
|
: "Publish private app"}
|
|
</DialogTitle>
|
|
|
|
<Box>
|
|
<Tabs
|
|
value={valueTabPrivateApp}
|
|
onChange={handleChange}
|
|
aria-label="basic tabs example"
|
|
variant={isMobile ? "scrollable" : "fullWidth"} // Scrollable on mobile, full width on desktop
|
|
scrollButtons="auto"
|
|
allowScrollButtonsMobile
|
|
sx={{
|
|
"& .MuiTabs-indicator": {
|
|
backgroundColor: "white",
|
|
},
|
|
}}
|
|
>
|
|
<Tab
|
|
label="Access app"
|
|
{...a11yProps(0)}
|
|
sx={{
|
|
"&.Mui-selected": {
|
|
color: "white",
|
|
},
|
|
fontSize: isMobile ? "0.75rem" : "1rem", // Adjust font size for mobile
|
|
}}
|
|
/>
|
|
<Tab
|
|
label="Publish app"
|
|
{...a11yProps(1)}
|
|
sx={{
|
|
"&.Mui-selected": {
|
|
color: "white",
|
|
},
|
|
fontSize: isMobile ? "0.75rem" : "1rem", // Adjust font size for mobile
|
|
}}
|
|
/>
|
|
</Tabs>
|
|
</Box>
|
|
{valueTabPrivateApp === 0 && (
|
|
<>
|
|
<DialogContent>
|
|
<Box
|
|
sx={{
|
|
display: "flex",
|
|
flexDirection: "column",
|
|
gap: "5px",
|
|
}}
|
|
>
|
|
<Label>Select a group</Label>
|
|
<Label>Only private groups will be shown</Label>
|
|
<Select
|
|
labelId="demo-simple-select-label"
|
|
id="demo-simple-select"
|
|
value={privateAppValues?.groupId}
|
|
label="Groups"
|
|
onChange={(e) => {
|
|
setPrivateAppValues((prev) => {
|
|
return {
|
|
...prev,
|
|
groupId: e.target.value,
|
|
};
|
|
});
|
|
}}
|
|
>
|
|
<MenuItem value={0}>No group selected</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>name</Label>
|
|
<Input
|
|
placeholder="name"
|
|
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>identifier</Label>
|
|
<Input
|
|
placeholder="identifier"
|
|
value={privateAppValues?.identifier}
|
|
onChange={(e) =>
|
|
setPrivateAppValues((prev) => {
|
|
return {
|
|
...prev,
|
|
identifier: e.target.value,
|
|
};
|
|
})
|
|
}
|
|
/>
|
|
</Box>
|
|
</DialogContent>
|
|
<DialogActions>
|
|
<Button
|
|
variant="contained"
|
|
onClick={() => {
|
|
setIsOpenPrivateModal(false);
|
|
}}
|
|
>
|
|
Close
|
|
</Button>
|
|
<Button
|
|
disabled={
|
|
!privateAppValues.name ||
|
|
!privateAppValues.service ||
|
|
!privateAppValues.identifier ||
|
|
!privateAppValues?.groupId
|
|
}
|
|
variant="contained"
|
|
onClick={() => addPrivateApp()}
|
|
autoFocus
|
|
>
|
|
Access
|
|
</Button>
|
|
</DialogActions>
|
|
</>
|
|
)}
|
|
{valueTabPrivateApp === 1 && (
|
|
<>
|
|
<DialogContent>
|
|
<PublishQAppInfo
|
|
sx={{
|
|
fontSize: "14px",
|
|
}}
|
|
>
|
|
Select .zip file containing static content:{" "}
|
|
</PublishQAppInfo>
|
|
<Spacer height="10px" />
|
|
<PublishQAppInfo
|
|
sx={{
|
|
fontSize: "14px",
|
|
}}
|
|
>{`
|
|
50mb MB maximum`}</PublishQAppInfo>
|
|
{file && (
|
|
<>
|
|
<Spacer height="5px" />
|
|
<PublishQAppInfo>{`Selected: (${file?.name})`}</PublishQAppInfo>
|
|
</>
|
|
)}
|
|
|
|
<Spacer height="18px" />
|
|
<PublishQAppChoseFile {...getRootProps()}>
|
|
{" "}
|
|
<input {...getInputProps()} />
|
|
{file ? "Change" : "Choose"} File
|
|
</PublishQAppChoseFile>
|
|
<Spacer height="20px" />
|
|
<Box
|
|
sx={{
|
|
display: "flex",
|
|
flexDirection: "column",
|
|
gap: "5px",
|
|
}}
|
|
>
|
|
<Label>Select a group</Label>
|
|
<Label>
|
|
Only groups where you are an admin will be shown
|
|
</Label>
|
|
<Select
|
|
labelId="demo-simple-select-label"
|
|
id="demo-simple-select"
|
|
value={selectedGroup}
|
|
label="Groups where you are an admin"
|
|
onChange={(e) => setSelectedGroup(e.target.value)}
|
|
>
|
|
<MenuItem value={0}>No group selected</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>identifier</Label>
|
|
<Input
|
|
placeholder="identifier"
|
|
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>App name</Label>
|
|
<Input
|
|
placeholder="App name"
|
|
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">Choose logo</Button>
|
|
</ImageUploader>
|
|
{logo?.name}
|
|
<Spacer height="25px" />
|
|
</DialogContent>
|
|
<DialogActions>
|
|
<Button
|
|
variant="contained"
|
|
onClick={() => {
|
|
setIsOpenPrivateModal(false);
|
|
clearFields();
|
|
}}
|
|
>
|
|
Close
|
|
</Button>
|
|
<Button
|
|
disabled={
|
|
!newPrivateAppValues.name ||
|
|
!newPrivateAppValues.service ||
|
|
!newPrivateAppValues.identifier ||
|
|
!selectedGroup
|
|
}
|
|
variant="contained"
|
|
onClick={() => publishPrivateApp()}
|
|
autoFocus
|
|
>
|
|
Publish
|
|
</Button>
|
|
</DialogActions>
|
|
</>
|
|
)}
|
|
</Dialog>
|
|
)}
|
|
</>
|
|
);
|
|
};
|