diff --git a/src/components/Apps/AppViewer.tsx b/src/components/Apps/AppViewer.tsx index cef2d8d..ad1380f 100644 --- a/src/components/Apps/AppViewer.tsx +++ b/src/components/Apps/AppViewer.tsx @@ -40,15 +40,17 @@ export const AppViewer = React.forwardRef(({ app , hide, isDevMode}, iframeRef) }, [url, isDevMode]) + const refreshAppFunc = (e) => { const {tabId} = e.detail if(tabId === app?.tabId){ if(isDevMode){ resetHistory() - if(!app?.isPreview){ + if(!app?.isPreview || app?.isPrivate){ setUrl(app?.url + `?time=${Date.now()}`) } + return } diff --git a/src/components/Apps/AppsDesktop.tsx b/src/components/Apps/AppsDesktop.tsx index b29f9d5..4acbb19 100644 --- a/src/components/Apps/AppsDesktop.tsx +++ b/src/components/Apps/AppsDesktop.tsx @@ -310,6 +310,7 @@ export const AppsDesktop = ({ mode, setMode, show , myName, goToHome, setDesktop }; }, [tabs]); + return ( - + )} @@ -479,6 +480,7 @@ export const AppsDesktop = ({ mode, setMode, show , myName, goToHome, setDesktop isSelected={tab?.tabId === selectedTab?.tabId} app={tab} ref={iframeRefs.current[tab.tabId]} + isDevMode={tab?.service ? false : true} /> ); })} @@ -494,7 +496,7 @@ export const AppsDesktop = ({ mode, setMode, show , myName, goToHome, setDesktop }}> - + )} diff --git a/src/components/Apps/AppsHomeDesktop.tsx b/src/components/Apps/AppsHomeDesktop.tsx index 7c548d1..2b2ea00 100644 --- a/src/components/Apps/AppsHomeDesktop.tsx +++ b/src/components/Apps/AppsHomeDesktop.tsx @@ -1,4 +1,4 @@ -import React, { useMemo, useState } from "react"; +import React, { useContext, useMemo, useState } from "react"; import { AppCircle, AppCircleContainer, @@ -6,24 +6,175 @@ import { AppLibrarySubTitle, AppsContainer, AppsParent, + PublishQAppChoseFile, + PublishQAppInfo, } from "./Apps-styles"; -import { Avatar, Box, ButtonBase, Input } from "@mui/material"; +import { Avatar, Box, Button, ButtonBase, Dialog, DialogActions, DialogContent, DialogTitle, Input, MenuItem, Select, Tab, Tabs } from "@mui/material"; import { Add } from "@mui/icons-material"; -import { getBaseApiReact, isMobile } from "../../App"; +import { getBaseApiReact, isMobile, MyContext } from "../../App"; import LogoSelected from "../../assets/svgs/LogoSelected.svg"; import { executeEvent } from "../../utils/events"; import { Spacer } from "../../common/Spacer"; import { SortablePinnedApps } from "./SortablePinnedApps"; import { extractComponents } from "../Chat/MessageDisplay"; import ArrowOutwardIcon from '@mui/icons-material/ArrowOutward'; +import { createEndpoint, getFee } from "../../background"; +import { useRecoilState, useSetRecoilState } from "recoil"; +import { myGroupsWhereIAmAdminAtom, settingsLocalLastUpdatedAtom, sortablePinnedAppsAtom } from "../../atoms/global"; +import { saveToLocalStorage } from "./AppsNavBarDesktop"; +import { Label } from "../Group/AddGroup"; +import { useHandlePrivateApps } from "./useHandlePrivateApps"; +import { useDropzone } from "react-dropzone"; + +const maxFileSize = 50 * 1024 * 1024 ; // 50MB or 400MB + export const AppsHomeDesktop = ({ setMode, myApp, myWebsite, availableQapps, + myName }) => { - const [qortalUrl, setQortalUrl] = useState('') + const {openApp} = useHandlePrivateApps() + const [file, setFile] = useState(null) + 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 { + show, + setInfoSnackCustom, + memberGroups + } = useContext(MyContext); + const [qortalUrl, setQortalUrl] = useState('') + const [selectedGroup, setSelectedGroup] = useState(0); + + const [valueTabPrivateApp, setValueTabPrivateApp] = useState(0) +const [myGroupsWhereIAmAdmin, setMyGroupsWhereIAmAdmin] = useRecoilState( + myGroupsWhereIAmAdminAtom + ); + const [isOpenPrivateModal, setIsOpenPrivateModal] = useState(false) + const [sortablePinnedApps, setSortablePinnedApps] = useRecoilState( + sortablePinnedAppsAtom + ); + const setSettingsLocalLastUpdated = useSetRecoilState( + settingsLocalLastUpdatedAtom + ); + const [privateAppValues, setPrivateAppValues] = useState({ + name: 'a-test', + service: 'DOCUMENT', + identifier: 'qortal_test_private', + groupId: 0 + }) + + const [newPrivateAppValues, setNewPrivateAppValues] = useState({ + service: 'DOCUMENT', + identifier: '' + }) + + const addPrivateApp = async ()=> { + try { + if(privateAppValues?.groupId === 0) return + openApp(privateAppValues, true) + + + } catch (error) { + + } + } + + const clearFields = ()=> { + setPrivateAppValues({ + name: '', + service: 'DOCUMENT', + identifier: '', + groupId: 0 + }) + setNewPrivateAppValues({ +service: 'DOCUMENT', + identifier: '' + }) + setFile(null) + setValueTabPrivateApp(0) + setSelectedGroup(null) + + } + + const publishPrivateApp = async ()=> { + try { + if(selectedGroup === 0) return + if(!myName) throw new Error('You need a Qortal name to publish') + const decryptedData = await window.sendMessage( + "ENCRYPT_QORTAL_GROUP_DATA", + + { + file: file, + 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) { + setInfoSnackCustom({ + type: "error", + message: error?.message || "Unable to publish app", + }); + } + } const openQortalUrl = ()=> { try { if(!qortalUrl) return @@ -38,6 +189,18 @@ export const AppsHomeDesktop = ({ } } + const handleChange = (event: React.SyntheticEvent, newValue: number) => { + setValueTabPrivateApp(newValue); + }; + + function a11yProps(index: number) { + return { + id: `simple-tab-${index}`, + "aria-controls": `simple-tabpanel-${index}`, + }; + } + + return ( <> { setMode("library"); }} + sx={{ + width: "80px", + }} > Library - + { + setIsOpenPrivateModal(true); + }} + sx={{ + width: "80px", + }} + > + + + + + + Private + + + + {isOpenPrivateModal && ( + { + if (e.key === "Enter") { + if(valueTabPrivateApp === 0){ + if(!privateAppValues.name || !privateAppValues.service || !privateAppValues.identifier || !privateAppValues?.groupId) return + addPrivateApp(); + } + + } + }} + maxWidth="md" + fullWidth={true} + > + + {valueTabPrivateApp === 0 ? "Access private app" : "Publish private app"} + + + + + + + + + {valueTabPrivateApp === 0 && ( + <> + + {/* + + setPrivateAppValues((prev)=> { + return { + ...prev, + service: e.target.value + } + })} + /> + */} + + + + + + + + + setPrivateAppValues((prev)=> { + return { + ...prev, + name: e.target.value + } + })} + /> + + + + setPrivateAppValues((prev)=> { + return { + ...prev, + identifier: e.target.value + } + })} + /> + + + + + + + + )} + {valueTabPrivateApp === 1 && ( + <> + + + Select .zip file containing static content:{" "} + + + {` + 50mb MB maximum`} + {file && ( + <> + + {`Selected: (${file?.name})`} + + )} + + + + {" "} + + {file ? 'Change' : 'Choose'} File + + + + + + + + + {/* + + setPrivateAppValues((prev)=> { + return { + ...prev, + service: e.target.value + } + })} + /> + */} + + + setNewPrivateAppValues((prev)=> { + return { + ...prev, + identifier: e.target.value + } + })} + /> + + + + + + + + )} + + + )} ); }; diff --git a/src/components/Apps/AppsNavBarDesktop.tsx b/src/components/Apps/AppsNavBarDesktop.tsx index 3184af6..f5599b7 100644 --- a/src/components/Apps/AppsNavBarDesktop.tsx +++ b/src/components/Apps/AppsNavBarDesktop.tsx @@ -31,6 +31,7 @@ import { settingsLocalLastUpdatedAtom, sortablePinnedAppsAtom, } from "../../atoms/global"; +import { useHandlePrivateApps } from "./useHandlePrivateApps"; export function saveToLocalStorage(key, subKey, newValue) { try { @@ -133,10 +134,24 @@ export const AppsNavBarDesktop = ({disableBack}) => { - const isSelectedAppPinned = !!sortablePinnedApps?.find( - (item) => - item?.name === selectedTab?.name && item?.service === selectedTab?.service - ); + // const isSelectedAppPinned = !!sortablePinnedApps?.find( + // (item) => + // item?.name === selectedTab?.name && item?.service === selectedTab?.service + // ); + + const isSelectedAppPinned = useMemo(()=> { + if(selectedTab?.isPrivate){ + return !!sortablePinnedApps?.find( + (item) => + item?.privateAppProperties?.name === selectedTab?.privateAppProperties?.name && item?.privateAppProperties?.service === selectedTab?.privateAppProperties?.service && item?.privateAppProperties?.identifier === selectedTab?.privateAppProperties?.identifier + ); + } else { + return !!sortablePinnedApps?.find( + (item) => + item?.name === selectedTab?.name && item?.service === selectedTab?.service + ); + } + }, [selectedTab,sortablePinnedApps]) return ( { if (isSelectedAppPinned) { // Remove the selected app if it is pinned - updatedApps = prev.filter( - (item) => - !( - item?.name === selectedTab?.name && - item?.service === selectedTab?.service - ) - ); + if(selectedTab?.isPrivate){ + updatedApps = prev.filter( + (item) => + !( + item?.privateAppProperties?.name === selectedTab?.privateAppProperties?.name && + item?.privateAppProperties?.service === selectedTab?.privateAppProperties?.service && + item?.privateAppProperties?.identifier === selectedTab?.privateAppProperties?.identifier + ) + ); + } else { + updatedApps = prev.filter( + (item) => + !( + item?.name === selectedTab?.name && + item?.service === selectedTab?.service + ) + ); + } + } else { // Add the selected app if it is not pinned - updatedApps = [ + if(selectedTab?.isPrivate){ + updatedApps = [ ...prev, { - name: selectedTab?.name, - service: selectedTab?.service, + isPreview: true, + isPrivate: true, + privateAppProperties: { + name: selectedTab?.privateAppProperties?.name, + service: selectedTab?.privateAppProperties?.service, + identifier: selectedTab?.privateAppProperties?.identifier, + } + }, ]; + } else { + updatedApps = [ + ...prev, + { + name: selectedTab?.name, + service: selectedTab?.service, + }, + ]; + } + } saveToLocalStorage( @@ -338,6 +382,10 @@ export const AppsNavBarDesktop = ({disableBack}) => { { + if(selectedTab?.refreshFunc){ + selectedTab.refreshFunc(selectedTab?.tabId) + return + } executeEvent("refreshApp", { tabId: selectedTab?.tabId, }); @@ -368,38 +416,41 @@ export const AppsNavBarDesktop = ({disableBack}) => { primary="Refresh" /> - { - executeEvent("copyLink", { - tabId: selectedTab?.tabId, - }); - handleClose(); - }} - > - { + executeEvent("copyLink", { + tabId: selectedTab?.tabId, + }); + handleClose(); }} > - + + + - - - + + )} + ); diff --git a/src/components/Apps/SortablePinnedApps.tsx b/src/components/Apps/SortablePinnedApps.tsx index 15f54b9..3adb470 100644 --- a/src/components/Apps/SortablePinnedApps.tsx +++ b/src/components/Apps/SortablePinnedApps.tsx @@ -11,8 +11,11 @@ import { settingsLocalLastUpdatedAtom, sortablePinnedAppsAtom } from '../../atom import { useRecoilState, useSetRecoilState } from 'recoil'; import { saveToLocalStorage } from './AppsNavBar'; import { ContextMenuPinnedApps } from '../ContextMenuPinnedApps'; +import LockIcon from "@mui/icons-material/Lock"; +import { useHandlePrivateApps } from './useHandlePrivateApps'; const SortableItem = ({ id, name, app, isDesktop }) => { + const {openApp} = useHandlePrivateApps() const { attributes, listeners, setNodeRef, transform, transition } = useSortable({ id }); const style = { transform: CSS.Transform.toString(transform), @@ -36,9 +39,14 @@ const SortableItem = ({ id, name, app, isDesktop }) => { transition, }} onClick={()=> { - executeEvent("addTab", { - data: app - }) + if(app?.isPrivate){ + openApp(app?.privateAppProperties) + } else { + executeEvent("addTab", { + data: app + }) + } + }} > { border: "none", }} > - + ) : ( + { alt="center-icon" /> + )} + - + {app?.isPrivate ? ( + + Private + + ) : ( + {app?.metadata?.title || app?.name} + )} + @@ -85,7 +110,6 @@ const SortableItem = ({ id, name, app, isDesktop }) => { export const SortablePinnedApps = ({ isDesktop, myWebsite, myApp, availableQapps = [] }) => { const [pinnedApps, setPinnedApps] = useRecoilState(sortablePinnedAppsAtom); const setSettingsLocalLastUpdated = useSetRecoilState(settingsLocalLastUpdatedAtom); - const transformPinnedApps = useMemo(() => { // Clone the existing pinned apps list diff --git a/src/components/Apps/TabComponent.tsx b/src/components/Apps/TabComponent.tsx index aca6b55..303fcab 100644 --- a/src/components/Apps/TabComponent.tsx +++ b/src/components/Apps/TabComponent.tsx @@ -1,61 +1,73 @@ -import React from 'react' -import { TabParent } from './Apps-styles' +import React from "react"; +import { TabParent } from "./Apps-styles"; import NavCloseTab from "../../assets/svgs/NavCloseTab.svg"; -import { getBaseApiReact } from '../../App'; -import { Avatar, ButtonBase } from '@mui/material'; +import { getBaseApiReact } from "../../App"; +import { Avatar, ButtonBase } from "@mui/material"; import LogoSelected from "../../assets/svgs/LogoSelected.svg"; -import { executeEvent } from '../../utils/events'; - -const TabComponent = ({isSelected, app}) => { +import { executeEvent } from "../../utils/events"; +import LockIcon from "@mui/icons-material/Lock"; +const TabComponent = ({ isSelected, app }) => { return ( - { - if(isSelected){ - executeEvent('removeTab', { - data: app - }) - return + { + if (isSelected) { + executeEvent("removeTab", { + data: app, + }); + return; } - executeEvent('setSelectedTab', { - data: app - }) - }}> - + executeEvent("setSelectedTab", { + data: app, + }); + }} + > + {isSelected && ( - - - - ) } - - center-icon - - + + )} + {app?.isPrivate ? ( + + ) : ( + + center-icon + + )} + - ) -} + ); +}; -export default TabComponent \ No newline at end of file +export default TabComponent; diff --git a/src/components/Apps/useHandlePrivateApps.tsx b/src/components/Apps/useHandlePrivateApps.tsx new file mode 100644 index 0000000..8fce921 --- /dev/null +++ b/src/components/Apps/useHandlePrivateApps.tsx @@ -0,0 +1,129 @@ +import React, { useContext, useState } from "react"; +import { executeEvent } from "../../utils/events"; +import { getBaseApiReact, MyContext } from "../../App"; +import { createEndpoint } from "../../background"; +import { useRecoilState, useSetRecoilState } from "recoil"; +import { + settingsLocalLastUpdatedAtom, + sortablePinnedAppsAtom, +} from "../../atoms/global"; +import { saveToLocalStorage } from "./AppsNavBarDesktop"; + +export const useHandlePrivateApps = () => { + const [status, setStatus] = useState(""); + const { + openSnackGlobal, + setOpenSnackGlobal, + infoSnackCustom, + setInfoSnackCustom, + } = useContext(MyContext); + const [sortablePinnedApps, setSortablePinnedApps] = useRecoilState( + sortablePinnedAppsAtom + ); + const setSettingsLocalLastUpdated = useSetRecoilState( + settingsLocalLastUpdatedAtom + ); + const openApp = async (privateAppProperties, addToPinnedApps) => { + try { + setOpenSnackGlobal(true); + + setInfoSnackCustom({ + type: "info", + message: "Fetching app data", + }); + const urlData = `${getBaseApiReact()}/arbitrary/${ + privateAppProperties?.service + }/${privateAppProperties?.name}/${ + privateAppProperties?.identifier + }?encoding=base64`; + + const responseData = await fetch(urlData, { + method: "GET", + headers: { + "Content-Type": "application/json", + }, + }); + const data = await responseData.text(); + + setInfoSnackCustom({ + type: "info", + message: "Decrypting app", + }); + const decryptedData = await window.sendMessage( + "DECRYPT_QORTAL_GROUP_DATA", + + { + base64: data, + groupId: privateAppProperties?.groupId, + } + ); + if(decryptedData?.error) throw new Error(decryptedData?.error) + if (decryptedData) { + setInfoSnackCustom({ + type: "info", + message: "Building app", + }); + const endpoint = await createEndpoint( + `/arbitrary/APP/${privateAppProperties?.name}/zip?preview=true` + ); + const response = await fetch(endpoint, { + method: "POST", + headers: { + "Content-Type": "text/plain", + }, + body: decryptedData, + }); + const previewPath = await response.text(); + setOpenSnackGlobal(false); + const refreshfunc = async (tabId) => { + executeEvent("refreshApp", { + tabId: tabId, + }); + }; + + executeEvent("addTab", { + data: { + url: await createEndpoint(previewPath), + isPreview: true, + isPrivate: true, + privateAppProperties: { ...privateAppProperties }, + filePath: "", + refreshFunc: (tabId) => { + refreshfunc(tabId); + }, + }, + }); + + if (addToPinnedApps) { + setSortablePinnedApps((prev) => { + const updatedApps = [ + ...prev, + { + isPrivate: true, + isPreview: true, + privateAppProperties: { ...privateAppProperties }, + }, + ]; + + saveToLocalStorage( + "ext_saved_settings", + "sortablePinnedApps", + updatedApps + ); + return updatedApps; + }); + setSettingsLocalLastUpdated(Date.now()); + } + } + } catch (error) { + setInfoSnackCustom({ + type: "error", + message: error?.message || "Unable to access app", + }); + } + }; + return { + openApp, + status, + }; +}; diff --git a/src/components/ContextMenuPinnedApps.tsx b/src/components/ContextMenuPinnedApps.tsx index be0ae46..f8ca7ac 100644 --- a/src/components/ContextMenuPinnedApps.tsx +++ b/src/components/ContextMenuPinnedApps.tsx @@ -124,11 +124,20 @@ export const ContextMenuPinnedApps = ({ children, app, isMine }) => { { handleClose(e); setSortablePinnedApps((prev) => { - const updatedApps = prev.filter( - (item) => !(item?.name === app?.name && item?.service === app?.service) - ); - saveToLocalStorage('ext_saved_settings', 'sortablePinnedApps', updatedApps); - return updatedApps; + if(app?.isPrivate){ + const updatedApps = prev.filter( + (item) => !(item?.privateAppProperties?.name === app?.privateAppProperties?.name && item?.privateAppProperties?.service === app?.privateAppProperties?.service && item?.privateAppProperties?.identifier === app?.privateAppProperties?.identifier) + ); + saveToLocalStorage('ext_saved_settings', 'sortablePinnedApps', updatedApps); + return updatedApps; + } else { + const updatedApps = prev.filter( + (item) => !(item?.name === app?.name && item?.service === app?.service) + ); + saveToLocalStorage('ext_saved_settings', 'sortablePinnedApps', updatedApps); + return updatedApps; + } + }); }}>