diff --git a/src/App.tsx b/src/App.tsx index eaca680..77f32c5 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -103,6 +103,7 @@ import { useQortalGetSaveSettings } from "./useQortalGetSaveSettings"; import { useRecoilState, useResetRecoilState, useSetRecoilState } from "recoil"; import { canSaveSettingToQdnAtom, + enabledDevModeAtom, fullScreenAtom, hasSettingsChangedAtom, oldPinnedAppsAtom, @@ -370,9 +371,17 @@ function App() { useRetrieveDataLocalStorage(); useQortalGetSaveSettings(userInfo?.name); const [fullScreen, setFullScreen] = useRecoilState(fullScreenAtom); + const [isEnabledDevMode, setIsEnabledDevMode] = useRecoilState(enabledDevModeAtom) const { toggleFullScreen } = useAppFullScreen(setFullScreen); + useEffect(()=> { + const isDevModeFromStorage = localStorage.getItem('isEnabledDevMode'); + if(isDevModeFromStorage){ + setIsEnabledDevMode(JSON.parse(isDevModeFromStorage)) + } + }, []) + useEffect(() => { // Attach a global event listener for double-click const handleDoubleClick = () => { @@ -1519,7 +1528,7 @@ function App() { desktopViewMode={desktopViewMode} setDesktopViewMode={setDesktopViewMode} /> - {!isMobile && desktopViewMode !== "apps" && renderProfile()} + {!isMobile && desktopViewMode !== "apps" && desktopViewMode !== "dev" && renderProfile()} { +export const AppViewer = React.forwardRef(({ app , hide, isDevMode}, iframeRef) => { const { rootHeight } = useContext(MyContext); // const iframeRef = useRef(null); const { document, window: frameWindow } = useFrame(); - const {path, history, changeCurrentIndex} = useQortalMessageListener(frameWindow, iframeRef, app?.tabId) + const {path, history, changeCurrentIndex} = useQortalMessageListener(frameWindow, iframeRef, app?.tabId, isDevMode) const [url, setUrl] = useState('') useEffect(()=> { + if(isDevMode){ + setUrl(app?.url) + return + } + setUrl(`${getBaseApiReact()}/render/${app?.service}/${app?.name}${app?.path != null ? `/${app?.path}` : ''}?theme=dark&identifier=${(app?.identifier != null && app?.identifier != 'null') ? app?.identifier : ''}`) }, [app?.service, app?.name, app?.identifier, app?.path]) const defaultUrl = useMemo(()=> { return url - }, [url]) + }, [url, isDevMode]) const refreshAppFunc = (e) => { const {tabId} = e.detail if(tabId === app?.tabId){ + if(isDevMode){ + setUrl(app?.url + `?time=${Date.now()}`) + return + + } const constructUrl = `${getBaseApiReact()}/render/${app?.service}/${app?.name}${path != null ? path : ''}?theme=dark&identifier=${app?.identifier != null ? app?.identifier : ''}&time=${new Date().getMilliseconds()}` setUrl(constructUrl) } @@ -41,7 +51,7 @@ export const AppViewer = React.forwardRef(({ app , hide}, iframeRef) => { return () => { unsubscribeFromEvent("refreshApp", refreshAppFunc); }; - }, [app, path]); + }, [app, path, isDevMode]); // Function to navigate back in iframe const navigateBackInIframe = async () => { diff --git a/src/components/Apps/AppViewerContainer.tsx b/src/components/Apps/AppViewerContainer.tsx index 51bc0ff..27be01f 100644 --- a/src/components/Apps/AppViewerContainer.tsx +++ b/src/components/Apps/AppViewerContainer.tsx @@ -3,7 +3,7 @@ import { AppViewer } from './AppViewer'; import Frame from 'react-frame-component'; import { MyContext, isMobile } from '../../App'; -const AppViewerContainer = React.forwardRef(({ app, isSelected, hide }, ref) => { +const AppViewerContainer = React.forwardRef(({ app, isSelected, hide, isDevMode }, ref) => { const { rootHeight } = useContext(MyContext); @@ -42,7 +42,7 @@ const AppViewerContainer = React.forwardRef(({ app, isSelected, hide }, ref) => overflow: 'hidden', }} > - + ); }); diff --git a/src/components/Apps/AppsDevMode.tsx b/src/components/Apps/AppsDevMode.tsx new file mode 100644 index 0000000..4930d50 --- /dev/null +++ b/src/components/Apps/AppsDevMode.tsx @@ -0,0 +1,310 @@ +import React, { useCallback, useContext, useEffect, useMemo, useRef, useState } from "react"; +import { AppsDevModeHome } from "./AppsDevModeHome"; +import { Spacer } from "../../common/Spacer"; +import { MyContext, getBaseApiReact } from "../../App"; +import { AppInfo } from "./AppInfo"; +import { + executeEvent, + subscribeToEvent, + unsubscribeFromEvent, +} from "../../utils/events"; +import { AppsParent } from "./Apps-styles"; +import AppViewerContainer from "./AppViewerContainer"; +import ShortUniqueId from "short-unique-id"; +import { AppPublish } from "./AppPublish"; +import { AppsLibraryDesktop } from "./AppsLibraryDesktop"; +import { AppsCategoryDesktop } from "./AppsCategoryDesktop"; +import { AppsNavBarDesktop } from "./AppsNavBarDesktop"; +import { Box, ButtonBase } from "@mui/material"; +import { HomeIcon } from "../../assets/Icons/HomeIcon"; +import { MessagingIcon } from "../../assets/Icons/MessagingIcon"; +import { Save } from "../Save/Save"; +import { HubsIcon } from "../../assets/Icons/HubsIcon"; +import { AppsDevModeNavBar } from "./AppsDevModeNavBar"; + +const uid = new ShortUniqueId({ length: 8 }); + +export const AppsDevMode = ({ mode, setMode, show , myName, goToHome, setDesktopSideView, hasUnreadDirects, isDirects, isGroups, hasUnreadGroups, toggleSideViewGroups, toggleSideViewDirects}) => { + const [availableQapps, setAvailableQapps] = useState([]); + const [selectedAppInfo, setSelectedAppInfo] = useState(null); + const [selectedCategory, setSelectedCategory] = useState(null) + const [tabs, setTabs] = useState([]); + const [selectedTab, setSelectedTab] = useState(null); + const [isNewTabWindow, setIsNewTabWindow] = useState(false); + const [categories, setCategories] = useState([]) + const iframeRefs = useRef({}); + + useEffect(() => { + setTimeout(() => { + executeEvent("appsDevModeSetTabsToNav", { + data: { + tabs: tabs, + selectedTab: selectedTab, + isNewTabWindow: isNewTabWindow, + }, + }); + }, 100); + }, [show, tabs, selectedTab, isNewTabWindow]); + + + + + + + + + + + const navigateBackFunc = (e) => { + if (['category', 'appInfo-from-category', 'appInfo', 'library', 'publish'].includes(mode)) { + // Handle the various modes as needed + if (mode === 'category') { + setMode('library'); + setSelectedCategory(null); + } else if (mode === 'appInfo-from-category') { + setMode('category'); + } else if (mode === 'appInfo') { + setMode('library'); + } else if (mode === 'library') { + if (isNewTabWindow) { + setMode('viewer'); + } else { + setMode('home'); + } + } else if (mode === 'publish') { + setMode('library'); + } + } else if(selectedTab?.tabId) { + executeEvent(`navigateBackApp-${selectedTab?.tabId}`, {}) + } + }; + + + useEffect(() => { + subscribeToEvent("devModeNavigateBack", navigateBackFunc); + + return () => { + unsubscribeFromEvent("devModeNavigateBack", navigateBackFunc); + }; + }, [mode, selectedTab]); + + const addTabFunc = (e) => { + const data = e.detail?.data; + const newTab = { + ...data, + tabId: uid.rnd(), + }; + setTabs((prev) => [...prev, newTab]); + setSelectedTab(newTab); + setMode("viewer"); + + setIsNewTabWindow(false); + }; + + + + useEffect(() => { + subscribeToEvent("appsDevModeAddTab", addTabFunc); + + return () => { + unsubscribeFromEvent("appsDevModeAddTab", addTabFunc); + }; + }, [tabs]); + const setSelectedTabFunc = (e) => { + const data = e.detail?.data; + + setSelectedTab(data); + setTimeout(() => { + executeEvent("appsDevModeSetTabsToNav", { + data: { + tabs: tabs, + selectedTab: data, + isNewTabWindow: isNewTabWindow, + }, + }); + }, 100); + setIsNewTabWindow(false); + }; + + + useEffect(() => { + subscribeToEvent("setSelectedTab", setSelectedTabFunc); + + return () => { + unsubscribeFromEvent("setSelectedTab", setSelectedTabFunc); + }; + }, [tabs, isNewTabWindow]); + + const removeTabFunc = (e) => { + const data = e.detail?.data; + const copyTabs = [...tabs].filter((tab) => tab?.tabId !== data?.tabId); + if (copyTabs?.length === 0) { + setMode("home"); + } else { + setSelectedTab(copyTabs[0]); + } + setTabs(copyTabs); + setSelectedTab(copyTabs[0]); + setTimeout(() => { + executeEvent("setTabsToNav", { + data: { + tabs: copyTabs, + selectedTab: copyTabs[0], + }, + }); + }, 400); + }; + + useEffect(() => { + subscribeToEvent("removeTab", removeTabFunc); + + return () => { + unsubscribeFromEvent("removeTab", removeTabFunc); + }; + }, [tabs]); + + const setNewTabWindowFunc = (e) => { + setIsNewTabWindow(true); + setSelectedTab(null) + }; + + useEffect(() => { + subscribeToEvent("devModeNewTabWindow", setNewTabWindowFunc); + + return () => { + unsubscribeFromEvent("devModeNewTabWindow", setNewTabWindowFunc); + }; + }, [tabs]); + + + return ( + + + + { + goToHome(); + + }} + > + + + + + { + setDesktopSideView("directs"); + toggleSideViewDirects() + }} + > + + + + + { + setDesktopSideView("groups"); + toggleSideViewGroups() + }} + > + + + + + {mode !== 'home' && ( + + + )} + + + + + {mode === "home" && ( + + + + + + )} + + + + + {tabs.map((tab) => { + if (!iframeRefs.current[tab.tabId]) { + iframeRefs.current[tab.tabId] = React.createRef(); + } + return ( + + ); + })} + + {isNewTabWindow && mode === "viewer" && ( + <> + + + + + + + )} + + ); +}; diff --git a/src/components/Apps/AppsDevModeHome.tsx b/src/components/Apps/AppsDevModeHome.tsx new file mode 100644 index 0000000..3ffeccf --- /dev/null +++ b/src/components/Apps/AppsDevModeHome.tsx @@ -0,0 +1,183 @@ +import React, { useContext, useMemo, useState } from "react"; +import { + AppCircle, + AppCircleContainer, + AppCircleLabel, + AppLibrarySubTitle, + AppsContainer, + AppsParent, +} from "./Apps-styles"; +import { + Avatar, + Box, + Button, + ButtonBase, + Dialog, + DialogActions, + DialogContent, + DialogContentText, + DialogTitle, + Input, +} from "@mui/material"; +import { Add } from "@mui/icons-material"; +import { MyContext, getBaseApiReact, isMobile } from "../../App"; +import LogoSelected from "../../assets/svgs/LogoSelected.svg"; +import { executeEvent } from "../../utils/events"; +import { Spacer } from "../../common/Spacer"; +import { AppsDevModeSortablePinnedApps } from "./AppsDevModeSortablePinnedApps"; +import { useModal } from "../../common/useModal"; +import { isUsingLocal } from "../../background"; +import { Label } from "../Group/AddGroup"; + +export const AppsDevModeHome = ({ + setMode, + myApp, + myWebsite, + availableQapps, +}) => { + + const [domain, setDomain] = useState(""); + const [port, setPort] = useState(""); + const { isShow, onCancel, onOk, show, message } = useModal(); + const { + openSnackGlobal, + setOpenSnackGlobal, + infoSnackCustom, + setInfoSnackCustom, + } = useContext(MyContext); + + const addDevModeApp = async () => { + try { + const usingLocal = await isUsingLocal(); + if (!usingLocal) { + setOpenSnackGlobal(true); + + setInfoSnackCustom({ + type: "error", + message: + "Please use your local node for dev mode! Logout and use Local node.", + }); + return; + } + await show({ + message: "", + publishFee: "", + }); + const framework = domain + ":" + port; + const response = await fetch( + `${getBaseApiReact()}/developer/proxy/start`, + { + method: "POST", + headers: { + "Content-Type": "text/plain", + }, + body: framework, + } + ); + const responseData = await response.text(); + executeEvent("appsDevModeAddTab", { + data: { + url: "http://127.0.0.1:" + responseData, + }, + }); + } catch (error) {} + }; + + return ( + <> + + + Dev Mode Apps + + + + + { + addDevModeApp(); + }} + > + + + + + + App + + + + {isShow && ( + + + {"Add custom framework"} + + + + + setDomain(e.target.value)} + /> + + + + + setPort(e.target.value)} + /> + + + + + + + + )} + + ); +}; diff --git a/src/components/Apps/AppsDevModeNavBar.tsx b/src/components/Apps/AppsDevModeNavBar.tsx new file mode 100644 index 0000000..11b626c --- /dev/null +++ b/src/components/Apps/AppsDevModeNavBar.tsx @@ -0,0 +1,272 @@ +import React, { useEffect, useMemo, useRef, useState } from "react"; +import { + AppsNavBarLeft, + AppsNavBarParent, + AppsNavBarRight, +} from "./Apps-styles"; +import NavBack from "../../assets/svgs/NavBack.svg"; +import NavAdd from "../../assets/svgs/NavAdd.svg"; +import NavMoreMenu from "../../assets/svgs/NavMoreMenu.svg"; +import { + ButtonBase, + ListItemIcon, + ListItemText, + Menu, + MenuItem, + Tab, + Tabs, +} from "@mui/material"; +import { + executeEvent, + subscribeToEvent, + unsubscribeFromEvent, +} from "../../utils/events"; +import TabComponent from "./TabComponent"; +import PushPinIcon from "@mui/icons-material/PushPin"; +import RefreshIcon from "@mui/icons-material/Refresh"; +import { useRecoilState, useSetRecoilState } from "recoil"; +import { + navigationControllerAtom, + settingsLocalLastUpdatedAtom, + sortablePinnedAppsAtom, +} from "../../atoms/global"; +import { AppsDevModeTabComponent } from "./AppsDevModeTabComponent"; + + + +export const AppsDevModeNavBar = () => { + const [tabs, setTabs] = useState([]); + const [selectedTab, setSelectedTab] = useState(null); + const [navigationController, setNavigationController] = useRecoilState(navigationControllerAtom) + + const [isNewTabWindow, setIsNewTabWindow] = useState(false); + const tabsRef = useRef(null); + const [anchorEl, setAnchorEl] = useState(null); + const open = Boolean(anchorEl); + + + + const handleClick = (event) => { + setAnchorEl(event.currentTarget); + }; + + const handleClose = () => { + setAnchorEl(null); + }; + + useEffect(() => { + // Scroll to the last tab whenever the tabs array changes (e.g., when a new tab is added) + if (tabsRef.current) { + const tabElements = tabsRef.current.querySelectorAll(".MuiTab-root"); + if (tabElements.length > 0) { + const lastTab = tabElements[tabElements.length - 1]; + lastTab.scrollIntoView({ + behavior: "smooth", + block: "nearest", + inline: "end", + }); + } + } + }, [tabs.length]); // Dependency on the number of tabs + + + const isDisableBackButton = useMemo(()=> { + if(selectedTab && navigationController[selectedTab?.tabId]?.hasBack) return false + if(selectedTab && !navigationController[selectedTab?.tabId]?.hasBack) return true + return false + }, [navigationController, selectedTab]) + + + + + const setTabsToNav = (e) => { + const { tabs, selectedTab, isNewTabWindow } = e.detail?.data; + + setTabs([...tabs]); + setSelectedTab(!selectedTab ? null : { ...selectedTab }); + setIsNewTabWindow(isNewTabWindow); + }; + + useEffect(() => { + subscribeToEvent("appsDevModeSetTabsToNav", setTabsToNav); + + return () => { + unsubscribeFromEvent("appsDevModeSetTabsToNav", setTabsToNav); + }; + }, []); + + + + + return ( + + + { + executeEvent("devModeNavigateBack", selectedTab?.tabId); + }} + disabled={isDisableBackButton} + sx={{ + opacity: !isDisableBackButton ? 1 : 0.1, + cursor: !isDisableBackButton ? 'pointer': 'default' + }} + > + + + + {tabs?.map((tab) => ( + + } // Pass custom component + sx={{ + "&.Mui-selected": { + color: "white", + }, + padding: "0px", + margin: "0px", + minWidth: "0px", + width: "50px", + }} + /> + ))} + + + {selectedTab && ( + + { + setSelectedTab(null); + executeEvent("devModeNewTabWindow", {}); + }} + > + + + { + if (!selectedTab) return; + handleClick(e); + }} + > + + + + )} + + + + { + executeEvent("refreshApp", { + tabId: selectedTab?.tabId, + }); + handleClose(); + }} + > + + + + + + + + ); +}; diff --git a/src/components/Apps/AppsDevModeTabComponent.tsx b/src/components/Apps/AppsDevModeTabComponent.tsx new file mode 100644 index 0000000..174ef73 --- /dev/null +++ b/src/components/Apps/AppsDevModeTabComponent.tsx @@ -0,0 +1,58 @@ +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 LogoSelected from "../../assets/svgs/LogoSelected.svg"; +import { executeEvent } from '../../utils/events'; + +export const AppsDevModeTabComponent = ({isSelected, app}) => { + return ( + { + if(isSelected){ + executeEvent('removeTab', { + data: app + }) + return + } + executeEvent('setSelectedTab', { + data: app + }) + }}> + + {isSelected && ( + + + + ) } + + center-icon + + + + ) +} + diff --git a/src/components/Apps/AppsHomeDesktop.tsx b/src/components/Apps/AppsHomeDesktop.tsx index e7346ff..6481871 100644 --- a/src/components/Apps/AppsHomeDesktop.tsx +++ b/src/components/Apps/AppsHomeDesktop.tsx @@ -12,8 +12,8 @@ import { Add } from "@mui/icons-material"; import { getBaseApiReact, isMobile } from "../../App"; import LogoSelected from "../../assets/svgs/LogoSelected.svg"; import { executeEvent } from "../../utils/events"; -import { SortablePinnedApps } from "./SortablePinnedApps"; import { Spacer } from "../../common/Spacer"; +import { SortablePinnedApps } from "./SortablePinnedApps"; export const AppsHomeDesktop = ({ setMode, diff --git a/src/components/Apps/useQortalMessageListener.tsx b/src/components/Apps/useQortalMessageListener.tsx index d5abebf..210ee91 100644 --- a/src/components/Apps/useQortalMessageListener.tsx +++ b/src/components/Apps/useQortalMessageListener.tsx @@ -383,7 +383,7 @@ const UIQortalRequests = [ return obj; // Updated object with references to stored files } -export const useQortalMessageListener = (frameWindow, iframeRef, tabId) => { +export const useQortalMessageListener = (frameWindow, iframeRef, tabId, isDevMode) => { const [path, setPath] = useState('') const [history, setHistory] = useState({ customQDNHistoryPaths: [], @@ -530,7 +530,7 @@ isDOMContentLoaded: false setHistory(event?.data?.payload) } - } else if(event?.data?.action === 'SET_TAB'){ + } else if(event?.data?.action === 'SET_TAB' && !isDevMode){ executeEvent("addTab", { data: event?.data?.payload }) @@ -553,7 +553,7 @@ isDOMContentLoaded: false }; - }, []); // Empty dependency array to run once when the component mounts + }, [isDevMode]); // Empty dependency array to run once when the component mounts diff --git a/src/components/Desktop/DesktopFooter.tsx b/src/components/Desktop/DesktopFooter.tsx index f1cf754..70b7e5b 100644 --- a/src/components/Desktop/DesktopFooter.tsx +++ b/src/components/Desktop/DesktopFooter.tsx @@ -17,6 +17,8 @@ import AppIcon from "../../assets/svgs/AppIcon.svg"; import { HomeIcon } from "../../assets/Icons/HomeIcon"; import { Save } from "../Save/Save"; +import { useRecoilState } from "recoil"; +import { enabledDevModeAtom } from "../../atoms/global"; export const IconWrapper = ({ children, label, color, selected }) => { return ( @@ -81,7 +83,8 @@ export const DesktopFooter = ({ setIsOpenSideViewGroups }) => { - + const [isEnabledDevMode, setIsEnabledDevMode] = useRecoilState(enabledDevModeAtom) + if(hide) return return ( + {isEnabledDevMode && ( + { + setDesktopViewMode('dev') + setIsOpenSideViewDirects(false) + setIsOpenSideViewGroups(false) + }} + > + + + + + )} + ); diff --git a/src/components/Group/Group.tsx b/src/components/Group/Group.tsx index 05f88b1..82f4359 100644 --- a/src/components/Group/Group.tsx +++ b/src/components/Group/Group.tsx @@ -91,6 +91,7 @@ import { DesktopHeader } from "../Desktop/DesktopHeader"; import { Apps } from "../Apps/Apps"; import { AppsNavBar } from "../Apps/AppsNavBar"; import { AppsDesktop } from "../Apps/AppsDesktop"; +import { AppsDevMode } from "../Apps/AppsDevMode"; // let touchStartY = 0; // let disablePullToRefresh = false; @@ -437,6 +438,7 @@ export const Group = ({ const initiatedGetMembers = useRef(false); const [groupChatTimestamps, setGroupChatTimestamps] = React.useState({}); const [appsMode, setAppsMode] = useState('home') + const [appsModeDev, setAppsModeDev] = useState('home') const [isOpenSideViewDirects, setIsOpenSideViewDirects] = useState(false) const [isOpenSideViewGroups, setIsOpenSideViewGroups] = useState(false) const toggleSideViewDirects = ()=> { @@ -1553,6 +1555,8 @@ export const Group = ({ } }; + console.log('desktopViewMode', desktopViewMode) + const renderDirects = () => { return (
- {!isMobile && ((desktopSideView === 'groups' && desktopViewMode !== 'apps') || isOpenSideViewGroups) && renderGroups()} - {!isMobile && ((desktopSideView === 'directs' && desktopViewMode !== 'apps') || isOpenSideViewDirects) && renderDirects()} + {!isMobile && ((desktopSideView === 'groups' && desktopViewMode !== 'apps' && desktopViewMode !== 'dev') || isOpenSideViewGroups) && renderGroups()} + {!isMobile && ((desktopSideView === 'directs' && desktopViewMode !== 'apps' && desktopViewMode !== 'dev') || isOpenSideViewDirects) && renderDirects()} @@ -2515,10 +2519,15 @@ export const Group = ({ isDirects={isOpenSideViewDirects} hasUnreadGroups={groupChatHasUnread || groupsAnnHasUnread} /> )} + {!isMobile && ( + + )} {!isMobile && !selectedGroup && - groupSection === "home" && desktopViewMode !== "apps" && ( + groupSection === "home" && desktopViewMode !== "apps" && desktopViewMode !== "dev" && ( diff --git a/src/components/Group/Settings.tsx b/src/components/Group/Settings.tsx index 945ec70..f14483c 100644 --- a/src/components/Group/Settings.tsx +++ b/src/components/Group/Settings.tsx @@ -25,6 +25,8 @@ import { LoadingSnackbar } from "../Snackbar/LoadingSnackbar"; import { getFee } from "../../background"; import { LoadingButton } from "@mui/lab"; import { subscribeToEvent, unsubscribeFromEvent } from "../../utils/events"; +import { enabledDevModeAtom } from "../../atoms/global"; +import { useRecoilState } from "recoil"; function a11yProps(index: number) { return { @@ -81,6 +83,9 @@ export const Settings = ({ setOpen, }) => { const [checked, setChecked] = React.useState(false); + const [isEnabledDevMode, setIsEnabledDevMode] = useRecoilState(enabledDevModeAtom) + + const handleChange = (event: React.ChangeEvent) => { setChecked(event.target.checked); @@ -148,7 +153,7 @@ export const Settings = ({ - General Settings + General Settings - - } - label="Disable all push notifications" - /> - - - - - - + + } + label="Disable all push notifications" + /> + { + setIsEnabledDevMode(e.target.checked) + localStorage.setItem('isEnabledDevMode', JSON.stringify(e.target.checked)) + }} /> + } + label="Enable dev mode" + /> - - - ); };