diff --git a/src/atoms/global.ts b/src/atoms/global.ts index 2ad90d4..5e212c2 100644 --- a/src/atoms/global.ts +++ b/src/atoms/global.ts @@ -9,4 +9,14 @@ export const sortablePinnedAppsAtom = atom({ export const canSaveSettingToQdnAtom = atom({ key: 'canSaveSettingToQdnAtom', default: false, +}); + +export const settingsQDNLastUpdatedAtom = atom({ + key: 'settingsQDNLastUpdatedAtom', + default: -100, +}); + +export const settingsLocalLastUpdatedAtom = atom({ + key: 'settingsLocalLastUpdatedAtom', + default: 0, }); \ No newline at end of file diff --git a/src/components/Apps/AppsNavBar.tsx b/src/components/Apps/AppsNavBar.tsx index a069c8a..8d87fca 100644 --- a/src/components/Apps/AppsNavBar.tsx +++ b/src/components/Apps/AppsNavBar.tsx @@ -12,28 +12,52 @@ import { executeEvent, subscribeToEvent, unsubscribeFromEvent } from "../../util import TabComponent from "./TabComponent"; import PushPinIcon from '@mui/icons-material/PushPin'; import RefreshIcon from '@mui/icons-material/Refresh'; -import { useRecoilState } from "recoil"; -import { sortablePinnedAppsAtom } from "../../atoms/global"; +import { useRecoilState, useSetRecoilState } from "recoil"; +import { settingsLocalLastUpdatedAtom, sortablePinnedAppsAtom } from "../../atoms/global"; -export function saveToLocalStorage(key, value) { +export function saveToLocalStorage(key, subKey, newValue) { try { - const serializedValue = JSON.stringify(value); - localStorage.setItem(key, serializedValue); - console.log(`Data saved to localStorage with key: ${key}`); + // Fetch existing data + const existingData = localStorage.getItem(key); + let combinedData = {}; + + if (existingData) { + // Parse the existing data + const parsedData = JSON.parse(existingData); + // Merge with the new data under the subKey + combinedData = { + ...parsedData, + timestamp: Date.now(), // Update the root timestamp + [subKey]: newValue // Assuming the data is an array + }; + } else { + // If no existing data, just use the new data under the subKey + combinedData = { + timestamp: Date.now(), // Set the initial root timestamp + [subKey]: newValue + }; + } + + // Save combined data back to localStorage + const serializedValue = JSON.stringify(combinedData); + localStorage.setItem(key, serializedValue); + console.log(`Data saved to localStorage with key: ${key} and subKey: ${subKey}`); } catch (error) { - console.error('Error saving to localStorage:', error); + console.error('Error saving to localStorage:', error); } } + export const AppsNavBar = () => { const [tabs, setTabs] = useState([]) - const [selectedTab, setSelectedTab] = useState([]) + const [selectedTab, setSelectedTab] = useState(null) const [isNewTabWindow, setIsNewTabWindow] = useState(false) const tabsRef = useRef(null); const [anchorEl, setAnchorEl] = useState(null); const open = Boolean(anchorEl); const [sortablePinnedApps, setSortablePinnedApps] = useRecoilState(sortablePinnedAppsAtom); + const setSettingsLocalLastUpdated = useSetRecoilState(settingsLocalLastUpdatedAtom); const handleClick = (event) => { setAnchorEl(event.currentTarget); @@ -59,7 +83,7 @@ export const AppsNavBar = () => { const {tabs, selectedTab, isNewTabWindow} = e.detail?.data; setTabs([...tabs]) - setSelectedTab({...selectedTab}) + setSelectedTab(!selectedTab ? nulll : {...selectedTab}) setIsNewTabWindow(isNewTabWindow) }; @@ -71,6 +95,8 @@ export const AppsNavBar = () => { }; }, []); + console.log('selectedTab', selectedTab) + const isSelectedAppPinned = !!sortablePinnedApps?.find((item)=> item?.name === selectedTab?.name && item?.service === selectedTab?.service) return ( @@ -115,6 +141,7 @@ export const AppsNavBar = () => { gap: '10px' }}> { + setSelectedTab(null) executeEvent("newTabWindow", { }); }}> @@ -186,10 +213,11 @@ export const AppsNavBar = () => { }]; } - saveToLocalStorage('sortablePinnedApps', updatedApps) + saveToLocalStorage('ext_saved_settings', 'sortablePinnedApps', updatedApps) return updatedApps; }); - + setSettingsLocalLastUpdated(Date.now()) + handleClose(); }} > diff --git a/src/components/Apps/SortablePinnedApps.tsx b/src/components/Apps/SortablePinnedApps.tsx index 374cb1d..d758997 100644 --- a/src/components/Apps/SortablePinnedApps.tsx +++ b/src/components/Apps/SortablePinnedApps.tsx @@ -7,9 +7,10 @@ import { Avatar, ButtonBase } from '@mui/material'; import { AppCircle, AppCircleContainer, AppCircleLabel } from './Apps-styles'; import { getBaseApiReact } from '../../App'; import { executeEvent } from '../../utils/events'; -import { sortablePinnedAppsAtom } from '../../atoms/global'; -import { useRecoilState } from 'recoil'; +import { settingsLocalLastUpdatedAtom, sortablePinnedAppsAtom } from '../../atoms/global'; +import { useRecoilState, useSetRecoilState } from 'recoil'; import { saveToLocalStorage } from './AppsNavBar'; +import { ContextMenuPinnedApps } from '../ContextMenuPinnedApps'; const SortableItem = ({ id, name, app }) => { const { attributes, listeners, setNodeRef, transform, transition } = useSortable({ id }); @@ -27,6 +28,7 @@ const SortableItem = ({ id, name, app }) => { }; return ( + { + ); }; export const SortablePinnedApps = ({ myWebsite, myApp, availableQapps = [] }) => { const [pinnedApps, setPinnedApps] = useRecoilState(sortablePinnedAppsAtom); + const setSettingsLocalLastUpdated = useSetRecoilState(settingsLocalLastUpdatedAtom); const transformPinnedApps = useMemo(()=> { console.log({myWebsite, myApp, availableQapps, pinnedApps}) @@ -149,8 +153,8 @@ export const SortablePinnedApps = ({ myWebsite, myApp, availableQapps = [] }) = const newOrder = arrayMove(transformPinnedApps, oldIndex, newIndex); setPinnedApps(newOrder); - saveToLocalStorage('sortablePinnedApps', newOrder) - + saveToLocalStorage('ext_saved_settings','sortablePinnedApps', newOrder) + setSettingsLocalLastUpdated(Date.now()) } }; return ( diff --git a/src/components/ContextMenuPinnedApps.tsx b/src/components/ContextMenuPinnedApps.tsx new file mode 100644 index 0000000..7075f4c --- /dev/null +++ b/src/components/ContextMenuPinnedApps.tsx @@ -0,0 +1,136 @@ +import React, { useState, useRef } from 'react'; +import { ListItemIcon, Menu, MenuItem, Typography, styled } from '@mui/material'; +import PushPinIcon from '@mui/icons-material/PushPin'; +import { saveToLocalStorage } from './Apps/AppsNavBar'; +import { useRecoilState } from 'recoil'; +import { sortablePinnedAppsAtom } from '../atoms/global'; + +const CustomStyledMenu = styled(Menu)(({ theme }) => ({ + '& .MuiPaper-root': { + backgroundColor: '#f9f9f9', + borderRadius: '12px', + padding: theme.spacing(1), + boxShadow: '0 5px 15px rgba(0, 0, 0, 0.2)', + }, + '& .MuiMenuItem-root': { + fontSize: '14px', + color: '#444', + transition: '0.3s background-color', + '&:hover': { + backgroundColor: '#f0f0f0', + }, + }, +})); + +export const ContextMenuPinnedApps = ({ children, app, setEnableDrag }) => { + const [menuPosition, setMenuPosition] = useState(null); + const longPressTimeout = useRef(null); + const maxHoldTimeout = useRef(null); + const preventClick = useRef(false); + const startTouchPosition = useRef({ x: 0, y: 0 }); // Track initial touch position + const [sortablePinnedApps, setSortablePinnedApps] = useRecoilState(sortablePinnedAppsAtom); + + const handleContextMenu = (event) => { + event.preventDefault(); + event.stopPropagation(); + preventClick.current = true; + setMenuPosition({ + mouseX: event.clientX, + mouseY: event.clientY, + }); + }; + + const handleTouchStart = (event) => { + const { clientX, clientY } = event.touches[0]; + startTouchPosition.current = { x: clientX, y: clientY }; + + longPressTimeout.current = setTimeout(() => { + preventClick.current = true; + setEnableDrag(false); + event.stopPropagation(); + setMenuPosition({ + mouseX: clientX, + mouseY: clientY, + }); + }, 500); + + // Set a maximum hold duration (e.g., 1.5 seconds) + maxHoldTimeout.current = setTimeout(() => { + clearTimeout(longPressTimeout.current); + }, 1500); + }; + + const handleTouchMove = (event) => { + const { clientX, clientY } = event.touches[0]; + const { x, y } = startTouchPosition.current; + + // Determine if the touch has moved beyond a small threshold (e.g., 10px) + const movedEnough = Math.abs(clientX - x) > 10 || Math.abs(clientY - y) > 10; + + if (movedEnough) { + clearTimeout(longPressTimeout.current); + clearTimeout(maxHoldTimeout.current); + } + }; + + const handleTouchEnd = (event) => { + clearTimeout(longPressTimeout.current); + clearTimeout(maxHoldTimeout.current); + setEnableDrag(true); + if (preventClick.current) { + event.preventDefault(); + event.stopPropagation(); + preventClick.current = false; + } + }; + + const handleClose = (e) => { + e.preventDefault(); + e.stopPropagation(); + setMenuPosition(null); + }; + + return ( +
+ {children} + { + e.stopPropagation(); + }} + > + { + 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; + }); + }}> + + + + + Unpin app + + + +
+ ); +}; diff --git a/src/components/Mobile/MobileHeader.tsx b/src/components/Mobile/MobileHeader.tsx index 8b95391..0feeb7b 100644 --- a/src/components/Mobile/MobileHeader.tsx +++ b/src/components/Mobile/MobileHeader.tsx @@ -239,16 +239,22 @@ const Header = ({ }} > {/* Left Home Icon */} - + - - +
+ {/* Center Title */} QORTAL - + {/* Right Logout Icon */} - + - + + diff --git a/src/components/Save/Save.tsx b/src/components/Save/Save.tsx index 9781ded..8a11f52 100644 --- a/src/components/Save/Save.tsx +++ b/src/components/Save/Save.tsx @@ -1,7 +1,7 @@ import React, { useContext, useMemo, useState } from 'react' import { useRecoilState } from 'recoil'; import isEqual from 'lodash/isEqual'; // Import deep comparison utility -import { canSaveSettingToQdnAtom, sortablePinnedAppsAtom } from '../../atoms/global'; +import { canSaveSettingToQdnAtom, settingsLocalLastUpdatedAtom, settingsQDNLastUpdatedAtom, sortablePinnedAppsAtom } from '../../atoms/global'; import { ButtonBase } from '@mui/material'; import { objectToBase64 } from '../../qdn/encryption/group-encryption'; import { MyContext } from '../../App'; @@ -10,12 +10,15 @@ import { CustomizedSnackbars } from '../Snackbar/Snackbar'; import { SaveIcon } from '../../assets/svgs/SaveIcon'; export const Save = () => { const [pinnedApps, setPinnedApps] = useRecoilState(sortablePinnedAppsAtom); - const [canSave, _] = useRecoilState(canSaveSettingToQdnAtom); + const [settingsQdnLastUpdated, setSettingsQdnLastUpdated] = useRecoilState(settingsQDNLastUpdatedAtom); + const [settingsLocalLastUpdated] = useRecoilState(settingsLocalLastUpdatedAtom); + + const [canSave] = useRecoilState(canSaveSettingToQdnAtom); const [openSnack, setOpenSnack] = useState(false); const [isLoading, setIsLoading] = useState(false) const [infoSnack, setInfoSnack] = useState(null); const [oldPinnedApps, setOldPinnedApps] = useState(pinnedApps) - console.log('oldpin', {oldPinnedApps, pinnedApps}) + console.log('oldpin', {oldPinnedApps, pinnedApps}, settingsQdnLastUpdated, settingsLocalLastUpdated, settingsQdnLastUpdated < settingsLocalLastUpdated,) const { show } = useContext(MyContext); const hasChanged = useMemo(()=> { @@ -35,14 +38,21 @@ export const Save = () => { } }) } - return !isEqual(oldChanges, newChanges) - }, [oldPinnedApps, pinnedApps]) + console.log('!isEqual(oldChanges, newChanges)', !isEqual(oldChanges, newChanges)) + if(settingsQdnLastUpdated === -100) return false + return !isEqual(oldChanges, newChanges) || settingsQdnLastUpdated < settingsLocalLastUpdated + }, [oldPinnedApps, pinnedApps, settingsQdnLastUpdated, settingsLocalLastUpdated]) const saveToQdn = async ()=> { try { setIsLoading(true) const data64 = await objectToBase64({ - sortablePinnedApps: pinnedApps + sortablePinnedApps: pinnedApps.map((item)=> { + return { + name: item?.name, + service: item?.service + } + }) }) const encryptData = await new Promise((res, rej) => { chrome?.runtime?.sendMessage( @@ -95,6 +105,7 @@ export const Save = () => { console.log('saved', response) if(response?.identifier){ setOldPinnedApps(pinnedApps) + setSettingsQdnLastUpdated(Date.now()) setInfoSnack({ type: "success", message: @@ -115,11 +126,12 @@ export const Save = () => { setIsLoading(false) } } + console.log('settingsQdnLastUpdated', settingsQdnLastUpdated) return ( <> - + { } const publishData = await response.json(); - if(publishData?.length > 0) return true + if(publishData?.length > 0) return {hasPublishRecord: false, timestamp: publishData[0]?.updated || publishData[0].created} - return false + return {hasPublishRecord: false} }; const getPublish = async (myName) => { - let data + try { + let data const res = await fetch( `${getBaseApiReact()}/arbitrary/DOCUMENT_PRIVATE/${myName}/ext_saved_settings?encoding=base64` ); @@ -47,22 +48,32 @@ const getPublishRecord = async (myName) => { const dataint8Array = base64ToUint8Array(decryptedKey.data); const decryptedKeyToObject = uint8ArrayToObject(dataint8Array); return decryptedKeyToObject + } catch (error) { + return null + } }; export const useQortalGetSaveSettings = (myName) => { const setSortablePinnedApps = useSetRecoilState(sortablePinnedAppsAtom); const setCanSave = useSetRecoilState(canSaveSettingToQdnAtom); - - - const getSavedSettings = useCallback(async (myName)=> { + const setSettingsQDNLastUpdated = useSetRecoilState(settingsQDNLastUpdatedAtom); + const [settingsLocalLastUpdated] = useRecoilState(settingsLocalLastUpdatedAtom); + const getSavedSettings = useCallback(async (myName, settingsLocalLastUpdated)=> { try { - const hasPublishRecord = await getPublishRecord(myName) + const {hasPublishRecord, timestamp} = await getPublishRecord(myName) if(hasPublishRecord){ const settings = await getPublish(myName) - if(settings?.sortablePinnedApps){ - fetchFromLocalStorage('sortablePinnedApps', settings.sortablePinnedApps) + if(settings?.sortablePinnedApps && timestamp > settingsLocalLastUpdated){ setSortablePinnedApps(settings.sortablePinnedApps) + setSettingsQDNLastUpdated(timestamp || 0) } + if(!settings){ + // set -100 to indicate that it couldn't fetch the publish + setSettingsQDNLastUpdated(-100) + + } + } else { + setSettingsQDNLastUpdated( 0) } setCanSave(true) } catch (error) { @@ -70,8 +81,8 @@ export const useQortalGetSaveSettings = (myName) => { } }, []) useEffect(()=> { - if(!myName) return - getSavedSettings(myName) - }, [getSavedSettings, myName]) + if(!myName || !settingsLocalLastUpdated) return + getSavedSettings(myName, settingsLocalLastUpdated) + }, [getSavedSettings, myName, settingsLocalLastUpdated]) } diff --git a/src/useRetrieveDataLocalStorage.tsx b/src/useRetrieveDataLocalStorage.tsx index 93831e0..6d7b05e 100644 --- a/src/useRetrieveDataLocalStorage.tsx +++ b/src/useRetrieveDataLocalStorage.tsx @@ -1,6 +1,6 @@ import React, { useCallback, useEffect } from 'react' import { useSetRecoilState } from 'recoil'; -import { sortablePinnedAppsAtom } from './atoms/global'; +import { settingsLocalLastUpdatedAtom, sortablePinnedAppsAtom } from './atoms/global'; function fetchFromLocalStorage(key) { try { @@ -18,13 +18,14 @@ function fetchFromLocalStorage(key) { export const useRetrieveDataLocalStorage = () => { const setSortablePinnedApps = useSetRecoilState(sortablePinnedAppsAtom); - + const setSettingsLocalLastUpdated = useSetRecoilState(settingsLocalLastUpdatedAtom); const getSortablePinnedApps = useCallback(()=> { - const pinnedAppsLocal = fetchFromLocalStorage('sortablePinnedApps') - if(pinnedAppsLocal){ - setSortablePinnedApps(pinnedAppsLocal) + const pinnedAppsLocal = fetchFromLocalStorage('ext_saved_settings') + if(pinnedAppsLocal?.sortablePinnedApps){ + setSortablePinnedApps(pinnedAppsLocal?.sortablePinnedApps) } + setSettingsLocalLastUpdated(pinnedAppsLocal?.timestamp || -1) }, []) useEffect(()=> {