From 6ee01a4ec321bdc4ae41bb14da67c03468ee4b48 Mon Sep 17 00:00:00 2001 From: PhilReact Date: Tue, 29 Apr 2025 17:27:14 +0300 Subject: [PATCH] group list optimizations --- electron/capacitor.config.ts | 20 +- src/App.tsx | 17 + src/atoms/global.ts | 70 +++ src/components/Chat/MessageItem.tsx | 26 +- src/components/Chat/TipTap.tsx | 540 +++++++++++------------ src/components/ContextMenu.tsx | 10 +- src/components/Group/AddGroup.tsx | 2 + src/components/Group/Group.tsx | 644 +++++++++------------------- src/components/Group/GroupList.tsx | 348 +++++++++++++++ 9 files changed, 937 insertions(+), 740 deletions(-) create mode 100644 src/components/Group/GroupList.tsx diff --git a/electron/capacitor.config.ts b/electron/capacitor.config.ts index c7f641f..87d927b 100644 --- a/electron/capacitor.config.ts +++ b/electron/capacitor.config.ts @@ -1,15 +1,15 @@ -import type { CapacitorConfig } from '@capacitor/cli'; +import type { CapacitorConfig } from "@capacitor/cli"; const config: CapacitorConfig = { - appId: 'org.Qortal.Qortal-Hub', - appName: 'Qortal-Hub', - webDir: 'dist', - "plugins": { - "LocalNotifications": { - "smallIcon": "qort", - "iconColor": "#09b6e8" - } - } + appId: "org.Qortal.Qortal-Hub", + appName: "Qortal-Hub", + webDir: "dist", + plugins: { + LocalNotifications: { + smallIcon: "qort", + iconColor: "#09b6e8", + }, + }, }; export default config; diff --git a/src/App.tsx b/src/App.tsx index dd3c814..b711dc2 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -100,6 +100,8 @@ import { useRecoilState, useResetRecoilState, useSetRecoilState } from 'recoil'; import { canSaveSettingToQdnAtom, enabledDevModeAtom, + groupAnnouncementsAtom, + groupChatTimestampsAtom, groupsOwnerNamesAtom, groupsPropertiesAtom, hasSettingsChangedAtom, @@ -107,11 +109,13 @@ import { isUsingImportExportSettingsAtom, lastPaymentSeenTimestampAtom, mailsAtom, + mutedGroupsAtom, oldPinnedAppsAtom, qMailLastEnteredTimestampAtom, settingsLocalLastUpdatedAtom, settingsQDNLastUpdatedAtom, sortablePinnedAppsAtom, + timestampEnterDataAtom, } from './atoms/global'; import { NotAuthenticated } from './ExtStates/NotAuthenticated'; import { handleGetFileFromIndexedDB } from './utils/indexedDB'; @@ -479,6 +483,15 @@ function App() { lastPaymentSeenTimestampAtom ); const resetGroupsOwnerNamesAtom = useResetRecoilState(groupsOwnerNamesAtom); + const resetGroupAnnouncementsAtom = useResetRecoilState( + groupAnnouncementsAtom + ); + const resetMutedGroupsAtom = useResetRecoilState(mutedGroupsAtom); + + const resetGroupChatTimestampsAtom = useResetRecoilState( + groupChatTimestampsAtom + ); + const resetTimestampEnterAtom = useResetRecoilState(timestampEnterDataAtom); const resetAllRecoil = () => { resetAtomSortablePinnedAppsAtom(); @@ -492,6 +505,10 @@ function App() { resetGroupPropertiesAtom(); resetLastPaymentSeenTimestampAtom(); resetGroupsOwnerNamesAtom(); + resetGroupAnnouncementsAtom(); + resetMutedGroupsAtom(); + resetGroupChatTimestampsAtom(); + resetTimestampEnterAtom(); }; const handleSetGlobalApikey = (key) => { diff --git a/src/atoms/global.ts b/src/atoms/global.ts index 2ff3c21..d20b56c 100644 --- a/src/atoms/global.ts +++ b/src/atoms/global.ts @@ -201,3 +201,73 @@ export const isOpenBlockedModalAtom = atom({ key: 'isOpenBlockedModalAtom', default: false, }); + +export const groupsOwnerNamesSelector = selectorFamily({ + key: 'groupsOwnerNamesSelector', + get: + (key) => + ({ get }) => { + const data = get(groupsOwnerNamesAtom); + return data[key] || null; // Return the value for the key or null if not found + }, +}); + +export const groupAnnouncementsAtom = atom({ + key: 'groupAnnouncementsAtom', + default: {}, +}); + +export const groupAnnouncementSelector = selectorFamily({ + key: 'groupAnnouncementSelector', + get: + (key) => + ({ get }) => { + const data = get(groupAnnouncementsAtom); + return data[key] || null; // Return the value for the key or null if not found + }, +}); + +export const groupPropertySelector = selectorFamily({ + key: 'groupPropertySelector', + get: + (key) => + ({ get }) => { + const data = get(groupsPropertiesAtom); + return data[key] || null; // Return the value for the key or null if not found + }, +}); + +export const mutedGroupsAtom = atom({ + key: 'mutedGroupsAtom', + default: [], +}); + +export const groupChatTimestampsAtom = atom({ + key: 'groupChatTimestampsAtom', + default: {}, +}); + +export const groupChatTimestampSelector = selectorFamily({ + key: 'groupChatTimestampSelector', + get: + (key) => + ({ get }) => { + const data = get(groupChatTimestampsAtom); + return data[key] || null; // Return the value for the key or null if not found + }, +}); + +export const timestampEnterDataAtom = atom({ + key: 'timestampEnterDataAtom', + default: {}, +}); + +export const timestampEnterDataSelector = selectorFamily({ + key: 'timestampEnterDataSelector', + get: + (key) => + ({ get }) => { + const data = get(timestampEnterDataAtom); + return data[key] || null; // Return the value for the key or null if not found + }, +}); diff --git a/src/components/Chat/MessageItem.tsx b/src/components/Chat/MessageItem.tsx index 0b23e3f..d861dac 100644 --- a/src/components/Chat/MessageItem.tsx +++ b/src/components/Chat/MessageItem.tsx @@ -75,6 +75,21 @@ const getBadgeImg = (level) => { } }; +const UserBadge = React.memo(({ userInfo }) => { + return ( + + + + ); +}); + export const MessageItem = React.memo( ({ message, @@ -210,16 +225,7 @@ export const MessageItem = React.memo( {message?.senderName?.charAt(0)} - - - + )} diff --git a/src/components/Chat/TipTap.tsx b/src/components/Chat/TipTap.tsx index 01155b5..0697798 100644 --- a/src/components/Chat/TipTap.tsx +++ b/src/components/Chat/TipTap.tsx @@ -1,4 +1,4 @@ -import { useCallback, useEffect, useMemo, useRef } from 'react'; +import React, { useCallback, useEffect, useMemo, useRef } from 'react'; import { EditorProvider, useCurrentEditor } from '@tiptap/react'; import StarterKit from '@tiptap/starter-kit'; import { Color } from '@tiptap/extension-color'; @@ -41,295 +41,297 @@ function textMatcher(doc, from) { return { start, query }; } -const MenuBar = ({ - setEditorRef, - isChat, - isDisabledEditorEnter, - setIsDisabledEditorEnter, -}) => { - const { editor } = useCurrentEditor(); - const fileInputRef = useRef(null); - const theme = useTheme(); +const MenuBar = React.memo( + ({ + setEditorRef, + isChat, + isDisabledEditorEnter, + setIsDisabledEditorEnter, + }) => { + const { editor } = useCurrentEditor(); + const fileInputRef = useRef(null); + const theme = useTheme(); - if (!editor) { - return null; - } + useEffect(() => { + if (editor && setEditorRef) { + setEditorRef(editor); + } + }, [editor, setEditorRef]); - useEffect(() => { - if (editor && setEditorRef) { - setEditorRef(editor); + if (!editor) { + return null; } - }, [editor, setEditorRef]); - const handleImageUpload = async (file) => { - let compressedFile; - await new Promise((resolve) => { - new Compressor(file, { - quality: 0.6, - maxWidth: 1200, - mimeType: 'image/webp', - success(result) { - compressedFile = new File([result], 'image.webp', { - type: 'image/webp', - }); - resolve(); - }, - error(err) { - console.error('Image compression error:', err); - }, + const handleImageUpload = async (file) => { + let compressedFile; + await new Promise((resolve) => { + new Compressor(file, { + quality: 0.6, + maxWidth: 1200, + mimeType: 'image/webp', + success(result) { + compressedFile = new File([result], 'image.webp', { + type: 'image/webp', + }); + resolve(); + }, + error(err) { + console.error('Image compression error:', err); + }, + }); }); - }); - if (compressedFile) { - const reader = new FileReader(); - reader.onload = () => { - const url = reader.result; - editor - .chain() - .focus() - .setImage({ src: url, style: 'width: auto' }) - .run(); - fileInputRef.current.value = ''; - }; - reader.readAsDataURL(compressedFile); - } - }; + if (compressedFile) { + const reader = new FileReader(); + reader.onload = () => { + const url = reader.result; + editor + .chain() + .focus() + .setImage({ src: url, style: 'width: auto' }) + .run(); + fileInputRef.current.value = ''; + }; + reader.readAsDataURL(compressedFile); + } + }; - const triggerImageUpload = () => { - fileInputRef.current.click(); // Trigger the file input click - }; + const triggerImageUpload = () => { + fileInputRef.current.click(); // Trigger the file input click + }; - const handlePaste = (event) => { - const items = event.clipboardData.items; - for (const item of items) { - if (item.type.startsWith('image/')) { - const file = item.getAsFile(); - if (file) { - event.preventDefault(); // Prevent the default paste behavior - handleImageUpload(file); // Call the image upload function + const handlePaste = (event) => { + const items = event.clipboardData.items; + for (const item of items) { + if (item.type.startsWith('image/')) { + const file = item.getAsFile(); + if (file) { + event.preventDefault(); // Prevent the default paste behavior + handleImageUpload(file); // Call the image upload function + } } } - } - }; + }; - useEffect(() => { - if (editor) { - editor.view.dom.addEventListener('paste', handlePaste); - return () => { - editor.view.dom.removeEventListener('paste', handlePaste); - }; - } - }, [editor]); + useEffect(() => { + if (editor) { + editor.view.dom.addEventListener('paste', handlePaste); + return () => { + editor.view.dom.removeEventListener('paste', handlePaste); + }; + } + }, [editor]); - return ( -
-
- editor.chain().focus().toggleBold().run()} - disabled={!editor.can().chain().focus().toggleBold().run()} - sx={{ - color: editor.isActive('bold') - ? theme.palette.text.primary - : theme.palette.text.secondary, - padding: 'revert', + return ( +
+
- - - editor.chain().focus().toggleItalic().run()} - disabled={!editor.can().chain().focus().toggleItalic().run()} - sx={{ - color: editor.isActive('italic') - ? theme.palette.text.primary - : theme.palette.text.secondary, - padding: 'revert', - }} - > - - - editor.chain().focus().toggleStrike().run()} - disabled={!editor.can().chain().focus().toggleStrike().run()} - sx={{ - color: editor.isActive('strike') - ? theme.palette.text.primary - : theme.palette.text.secondary, - padding: 'revert', - }} - > - - - editor.chain().focus().toggleCode().run()} - disabled={!editor.can().chain().focus().toggleCode().run()} - sx={{ - color: editor.isActive('code') - ? theme.palette.text.primary - : theme.palette.text.secondary, - padding: 'revert', - }} - > - - - editor.chain().focus().unsetAllMarks().run()} - sx={{ - color: - editor.isActive('bold') || - editor.isActive('italic') || - editor.isActive('strike') || - editor.isActive('code') + editor.chain().focus().toggleBold().run()} + disabled={!editor.can().chain().focus().toggleBold().run()} + sx={{ + color: editor.isActive('bold') ? theme.palette.text.primary : theme.palette.text.secondary, - padding: 'revert', - }} - > - - - editor.chain().focus().toggleBulletList().run()} - sx={{ - color: editor.isActive('bulletList') - ? theme.palette.text.primary - : theme.palette.text.secondary, - padding: 'revert', - }} - > - - - editor.chain().focus().toggleOrderedList().run()} - sx={{ - color: editor.isActive('orderedList') - ? theme.palette.text.primary - : theme.palette.text.secondary, - padding: 'revert', - }} - > - - - editor.chain().focus().toggleCodeBlock().run()} - sx={{ - color: editor.isActive('codeBlock') - ? theme.palette.text.primary - : theme.palette.text.secondary, - padding: 'revert', - }} - > - - - editor.chain().focus().toggleBlockquote().run()} - sx={{ - color: editor.isActive('blockquote') - ? theme.palette.text.primary - : theme.palette.text.secondary, - padding: 'revert', - }} - > - - - editor.chain().focus().setHorizontalRule().run()} - disabled={!editor.can().chain().focus().setHorizontalRule().run()} - sx={{ color: 'gray', padding: 'revert' }} - > - - - - editor.chain().focus().toggleHeading({ level: 1 }).run() - } - sx={{ - color: editor.isActive('heading', { level: 1 }) - ? theme.palette.text.primary - : theme.palette.text.secondary, - padding: 'revert', - }} - > - - - editor.chain().focus().undo().run()} - disabled={!editor.can().chain().focus().undo().run()} - sx={{ color: 'gray', padding: 'revert' }} - > - - - editor.chain().focus().redo().run()} - disabled={!editor.can().chain().focus().redo().run()} - sx={{ color: 'gray' }} - > - - - {isChat && ( - { - setIsDisabledEditorEnter(!isDisabledEditorEnter); + padding: 'revert', }} > - + + editor.chain().focus().toggleItalic().run()} + disabled={!editor.can().chain().focus().toggleItalic().run()} + sx={{ + color: editor.isActive('italic') + ? theme.palette.text.primary + : theme.palette.text.secondary, + padding: 'revert', + }} + > + + + editor.chain().focus().toggleStrike().run()} + disabled={!editor.can().chain().focus().toggleStrike().run()} + sx={{ + color: editor.isActive('strike') + ? theme.palette.text.primary + : theme.palette.text.secondary, + padding: 'revert', + }} + > + + + editor.chain().focus().toggleCode().run()} + disabled={!editor.can().chain().focus().toggleCode().run()} + sx={{ + color: editor.isActive('code') + ? theme.palette.text.primary + : theme.palette.text.secondary, + padding: 'revert', + }} + > + + + editor.chain().focus().unsetAllMarks().run()} + sx={{ + color: + editor.isActive('bold') || + editor.isActive('italic') || + editor.isActive('strike') || + editor.isActive('code') + ? theme.palette.text.primary + : theme.palette.text.secondary, + padding: 'revert', + }} + > + + + editor.chain().focus().toggleBulletList().run()} + sx={{ + color: editor.isActive('bulletList') + ? theme.palette.text.primary + : theme.palette.text.secondary, + padding: 'revert', + }} + > + + + editor.chain().focus().toggleOrderedList().run()} + sx={{ + color: editor.isActive('orderedList') + ? theme.palette.text.primary + : theme.palette.text.secondary, + padding: 'revert', + }} + > + + + editor.chain().focus().toggleCodeBlock().run()} + sx={{ + color: editor.isActive('codeBlock') + ? theme.palette.text.primary + : theme.palette.text.secondary, + padding: 'revert', + }} + > + + + editor.chain().focus().toggleBlockquote().run()} + sx={{ + color: editor.isActive('blockquote') + ? theme.palette.text.primary + : theme.palette.text.secondary, + padding: 'revert', + }} + > + + + editor.chain().focus().setHorizontalRule().run()} + disabled={!editor.can().chain().focus().setHorizontalRule().run()} + sx={{ color: 'gray', padding: 'revert' }} + > + + + + editor.chain().focus().toggleHeading({ level: 1 }).run() + } + sx={{ + color: editor.isActive('heading', { level: 1 }) + ? theme.palette.text.primary + : theme.palette.text.secondary, + padding: 'revert', + }} + > + + + editor.chain().focus().undo().run()} + disabled={!editor.can().chain().focus().undo().run()} + sx={{ color: 'gray', padding: 'revert' }} + > + + + editor.chain().focus().redo().run()} + disabled={!editor.can().chain().focus().redo().run()} + sx={{ color: 'gray' }} + > + + + {isChat && ( + - { + setIsDisabledEditorEnter(!isDisabledEditorEnter); }} > - disable enter - - - )} - {!isChat && ( - <> - - - - handleImageUpload(event.target.files[0])} - accept="image/*" - /> - - )} + + + disable enter + + + )} + {!isChat && ( + <> + + + + handleImageUpload(event.target.files[0])} + accept="image/*" + /> + + )} +
-
- ); -}; + ); + } +); const extensions = [ Color.configure({ types: [TextStyle.name, ListItem.name] }), @@ -373,10 +375,10 @@ export default ({ ? extensions.filter((item) => item?.name !== 'image') : extensions; const editorRef = useRef(null); - const setEditorRefFunc = (editorInstance) => { + const setEditorRefFunc = useCallback((editorInstance) => { editorRef.current = editorInstance; setEditorRef(editorInstance); - }; + }, []); // const users = [ // { id: 1, label: 'Alice' }, diff --git a/src/components/ContextMenu.tsx b/src/components/ContextMenu.tsx index 84df599..0306e57 100644 --- a/src/components/ContextMenu.tsx +++ b/src/components/ContextMenu.tsx @@ -10,6 +10,8 @@ import { import MailOutlineIcon from '@mui/icons-material/MailOutline'; import NotificationsOffIcon from '@mui/icons-material/NotificationsOff'; import { executeEvent } from '../utils/events'; +import { useRecoilState } from 'recoil'; +import { mutedGroupsAtom } from '../atoms/global'; const CustomStyledMenu = styled(Menu)(({ theme }) => ({ '& .MuiPaper-root': { @@ -28,16 +30,12 @@ const CustomStyledMenu = styled(Menu)(({ theme }) => ({ }, })); -export const ContextMenu = ({ - children, - groupId, - getUserSettings, - mutedGroups, -}) => { +export const ContextMenu = ({ children, groupId, getUserSettings }) => { const [menuPosition, setMenuPosition] = useState(null); const longPressTimeout = useRef(null); const preventClick = useRef(false); // Flag to prevent click after long-press or right-click const theme = useTheme(); + const [mutedGroups] = useRecoilState(mutedGroupsAtom); const isMuted = useMemo(() => { return mutedGroups.includes(groupId); }, [mutedGroups, groupId]); diff --git a/src/components/Group/AddGroup.tsx b/src/components/Group/AddGroup.tsx index 66e3880..278d6a7 100644 --- a/src/components/Group/AddGroup.tsx +++ b/src/components/Group/AddGroup.tsx @@ -199,6 +199,8 @@ export const AddGroup = ({ address, open, setOpen }) => { }; }, []); + if (!open) return null; + return ( { const queryString = admins.map((name) => `name=${name}`).join('&'); @@ -117,7 +122,7 @@ interface GroupProps { balance: number; } -const timeDifferenceForNotificationChats = 900000; +export const timeDifferenceForNotificationChats = 900000; export const requestQueueMemberNames = new RequestQueueWithPromise(5); export const requestQueueAdminMemberNames = new RequestQueueWithPromise(5); @@ -410,7 +415,9 @@ export const Group = ({ const { setMemberGroups, rootHeight, isRunningPublicNode } = useContext(MyContext); const lastGroupNotification = useRef(null); - const [timestampEnterData, setTimestampEnterData] = useState({}); + const [timestampEnterData, setTimestampEnterData] = useRecoilState( + timestampEnterDataAtom + ); const [chatMode, setChatMode] = useState('groups'); const [newChat, setNewChat] = useState(false); const [openSnack, setOpenSnack] = React.useState(false); @@ -421,7 +428,10 @@ export const Group = ({ const [firstSecretKeyInCreation, setFirstSecretKeyInCreation] = React.useState(false); const [groupSection, setGroupSection] = React.useState('home'); - const [groupAnnouncements, setGroupAnnouncements] = React.useState({}); + const [groupAnnouncements, setGroupAnnouncements] = useRecoilState( + groupAnnouncementsAtom + ); + const [defaultThread, setDefaultThread] = React.useState(null); const [isOpenDrawer, setIsOpenDrawer] = React.useState(false); const setIsOpenBlockedUserModal = useSetRecoilState(isOpenBlockedModalAtom); @@ -429,7 +439,7 @@ export const Group = ({ const [hideCommonKeyPopup, setHideCommonKeyPopup] = React.useState(false); const [isLoadingGroupMessage, setIsLoadingGroupMessage] = React.useState(''); const [drawerMode, setDrawerMode] = React.useState('groups'); - const [mutedGroups, setMutedGroups] = useState([]); + const setMutedGroups = useSetRecoilState(mutedGroupsAtom); const [mobileViewMode, setMobileViewMode] = useState('home'); const [mobileViewModeKeepOpen, setMobileViewModeKeepOpen] = useState(''); const isFocusedRef = useRef(true); @@ -443,7 +453,9 @@ export const Group = ({ const settimeoutForRefetchSecretKey = useRef(null); const { clearStatesMessageQueueProvider } = useMessageQueue(); const initiatedGetMembers = useRef(false); - const [groupChatTimestamps, setGroupChatTimestamps] = React.useState({}); + const [groupChatTimestamps, setGroupChatTimestamps] = useRecoilState( + groupChatTimestampsAtom + ); const [appsMode, setAppsMode] = useState('home'); const [appsModeDev, setAppsModeDev] = useState('home'); const [isOpenSideViewDirects, setIsOpenSideViewDirects] = useState(false); @@ -500,7 +512,7 @@ export const Group = ({ selectedDirectRef.current = selectedDirect; }, [selectedDirect]); - const getUserSettings = async () => { + const getUserSettings = useCallback(async () => { try { return new Promise((res, rej) => { window @@ -522,13 +534,13 @@ export const Group = ({ } catch (error) { console.log('error', error); } - }; + }, [setMutedGroups]); useEffect(() => { getUserSettings(); - }, []); + }, [getUserSettings]); - const getTimestampEnterChat = async () => { + const getTimestampEnterChat = useCallback(async () => { try { return new Promise((res, rej) => { window @@ -548,7 +560,7 @@ export const Group = ({ } catch (error) { console.log(error); } - }; + }, []); const refreshHomeDataFunc = () => { setGroupSection('default'); @@ -650,115 +662,139 @@ export const Group = ({ return hasUnread; }, [groupAnnouncements, groups]); - const getSecretKey = async ( - loadingGroupParam?: boolean, - secretKeyToPublish?: boolean - ) => { - try { - setIsLoadingGroupMessage('Locating encryption keys'); - pauseAllQueues(); - let dataFromStorage; - let publishFromStorage; - let adminsFromStorage; - if ( - secretKeyToPublish && - secretKey && - lastFetchedSecretKey.current && - Date.now() - lastFetchedSecretKey.current < 600000 - ) - return secretKey; - if (loadingGroupParam) { - setIsLoadingGroup(true); - } - if (selectedGroup?.groupId !== selectedGroupRef.current.groupId) { - if (settimeoutForRefetchSecretKey.current) { - clearTimeout(settimeoutForRefetchSecretKey.current); - } - return; - } - const prevGroupId = selectedGroupRef.current.groupId; - // const validApi = await findUsableApi(); - const { names, addresses, both } = - adminsFromStorage || (await getGroupAdmins(selectedGroup?.groupId)); - setAdmins(addresses); - setAdminsWithNames(both); - if (!names.length) { - throw new Error('Network error'); - } - const publish = - publishFromStorage || - (await getPublishesFromAdmins(names, selectedGroup?.groupId)); + const getSecretKey = useCallback( + async (loadingGroupParam?: boolean, secretKeyToPublish?: boolean) => { + try { + setIsLoadingGroupMessage('Locating encryption keys'); + pauseAllQueues(); - if (prevGroupId !== selectedGroupRef.current.groupId) { - if (settimeoutForRefetchSecretKey.current) { - clearTimeout(settimeoutForRefetchSecretKey.current); + let dataFromStorage; + let publishFromStorage; + let adminsFromStorage; + + if ( + secretKeyToPublish && + secretKey && + lastFetchedSecretKey.current && + Date.now() - lastFetchedSecretKey.current < 600000 + ) { + return secretKey; } - return; - } - if (publish === false) { - setTriedToFetchSecretKey(true); - settimeoutForRefetchSecretKey.current = setTimeout(() => { - getSecretKey(); - }, 120000); - return false; - } - setSecretKeyPublishDate(publish?.updated || publish?.created); - let data; - if (dataFromStorage) { - data = dataFromStorage; - } else { - // const shouldRebuild = !secretKeyPublishDate || (publish?.update && publish?.updated > secretKeyPublishDate) - setIsLoadingGroupMessage('Downloading encryption keys'); - const res = await fetch( - `${getBaseApiReact()}/arbitrary/DOCUMENT_PRIVATE/${publish.name}/${ - publish.identifier - }?encoding=base64&rebuild=true` - ); - data = await res.text(); - } - const decryptedKey: any = await decryptResource(data); - const dataint8Array = base64ToUint8Array(decryptedKey.data); - const decryptedKeyToObject = uint8ArrayToObject(dataint8Array); - if (!validateSecretKey(decryptedKeyToObject)) - throw new Error('SecretKey is not valid'); - setSecretKeyDetails(publish); - setSecretKey(decryptedKeyToObject); - lastFetchedSecretKey.current = Date.now(); - setMemberCountFromSecretKeyData(decryptedKey.count); - window - .sendMessage('setGroupData', { - groupId: selectedGroup?.groupId, - secretKeyData: data, - secretKeyResource: publish, - admins: { names, addresses, both }, - }) - .catch((error) => { - console.error( - 'Failed to set group data:', - error.message || 'An error occurred' + + if (loadingGroupParam) { + setIsLoadingGroup(true); + } + + if (selectedGroup?.groupId !== selectedGroupRef.current.groupId) { + if (settimeoutForRefetchSecretKey.current) { + clearTimeout(settimeoutForRefetchSecretKey.current); + } + return; + } + + const prevGroupId = selectedGroupRef.current.groupId; + + const { names, addresses, both } = + adminsFromStorage || (await getGroupAdmins(selectedGroup?.groupId)); + setAdmins(addresses); + setAdminsWithNames(both); + + if (!names.length) throw new Error('Network error'); + + const publish = + publishFromStorage || + (await getPublishesFromAdmins(names, selectedGroup?.groupId)); + + if (prevGroupId !== selectedGroupRef.current.groupId) { + if (settimeoutForRefetchSecretKey.current) { + clearTimeout(settimeoutForRefetchSecretKey.current); + } + return; + } + + if (publish === false) { + setTriedToFetchSecretKey(true); + settimeoutForRefetchSecretKey.current = setTimeout(() => { + getSecretKey(); + }, 120000); + return false; + } + + setSecretKeyPublishDate(publish?.updated || publish?.created); + + let data; + if (dataFromStorage) { + data = dataFromStorage; + } else { + setIsLoadingGroupMessage('Downloading encryption keys'); + const res = await fetch( + `${getBaseApiReact()}/arbitrary/DOCUMENT_PRIVATE/${publish.name}/${publish.identifier}?encoding=base64&rebuild=true` ); - }); + data = await res.text(); + } - if (decryptedKeyToObject) { - setTriedToFetchSecretKey(true); - setFirstSecretKeyInCreation(false); - return decryptedKeyToObject; - } else { - setTriedToFetchSecretKey(true); + const decryptedKey: any = await decryptResource(data); + const dataint8Array = base64ToUint8Array(decryptedKey.data); + const decryptedKeyToObject = uint8ArrayToObject(dataint8Array); + + if (!validateSecretKey(decryptedKeyToObject)) { + throw new Error('SecretKey is not valid'); + } + + setSecretKeyDetails(publish); + setSecretKey(decryptedKeyToObject); + lastFetchedSecretKey.current = Date.now(); + setMemberCountFromSecretKeyData(decryptedKey.count); + + window + .sendMessage('setGroupData', { + groupId: selectedGroup?.groupId, + secretKeyData: data, + secretKeyResource: publish, + admins: { names, addresses, both }, + }) + .catch((error) => { + console.error( + 'Failed to set group data:', + error.message || 'An error occurred' + ); + }); + + if (decryptedKeyToObject) { + setTriedToFetchSecretKey(true); + setFirstSecretKeyInCreation(false); + return decryptedKeyToObject; + } else { + setTriedToFetchSecretKey(true); + } + } catch (error) { + if (error === 'Unable to decrypt data') { + setTriedToFetchSecretKey(true); + settimeoutForRefetchSecretKey.current = setTimeout(() => { + getSecretKey(); + }, 120000); + } + } finally { + setIsLoadingGroup(false); + setIsLoadingGroupMessage(''); + resumeAllQueues(); } - } catch (error) { - if (error === 'Unable to decrypt data') { - setTriedToFetchSecretKey(true); - settimeoutForRefetchSecretKey.current = setTimeout(() => { - getSecretKey(); - }, 120000); - } - } finally { - setIsLoadingGroup(false); - setIsLoadingGroupMessage(''); - resumeAllQueues(); - } - }; + }, + [ + secretKey, + selectedGroup?.groupId, + setIsLoadingGroup, + setIsLoadingGroupMessage, + setSecretKey, + setSecretKeyDetails, + setTriedToFetchSecretKey, + setFirstSecretKeyInCreation, + setMemberCountFromSecretKeyData, + setAdmins, + setAdminsWithNames, + setSecretKeyPublishDate, + ] + ); const getAdminsForPublic = async (selectedGroup) => { try { @@ -1050,8 +1086,6 @@ export const Group = ({ triedToFetchSecretKey, ]); - console.log('groupOwner?.owner', groupOwner); - const notifyAdmin = async (admin) => { try { setIsLoadingNotifyAdmin(true); @@ -1327,8 +1361,6 @@ export const Group = ({ }; }, []); - console.log('selectedGroup', selectedGroup); - const openGroupChatFromNotification = (e) => { if (isLoadingOpenSectionFromNotification.current) return; @@ -1498,9 +1530,9 @@ export const Group = ({ }; }, [groups, selectedGroup]); - const handleSecretKeyCreationInProgress = () => { + const handleSecretKeyCreationInProgress = useCallback(() => { setFirstSecretKeyInCreation(true); - }; + }, []); const goToHome = async () => { setDesktopViewMode('home'); @@ -1811,327 +1843,34 @@ export const Group = ({ ); }; - const renderGroups = () => { - return ( -
- - { - setDesktopSideView('groups'); - }} - > - - - - - { - setDesktopSideView('directs'); - }} - > - - - - - - -
- {groups.map((group: any) => ( - - { - setMobileViewMode('group'); - setDesktopSideView('groups'); - initiatedGetMembers.current = false; - clearAllQueues(); - setSelectedDirect(null); - setTriedToFetchSecretKey(false); - setNewChat(false); - setSelectedGroup(null); - setUserInfoForLevels({}); - setSecretKey(null); - lastFetchedSecretKey.current = null; - setSecretKeyPublishDate(null); - setAdmins([]); - setSecretKeyDetails(null); - setAdminsWithNames([]); - setGroupOwner(null); - setMembers([]); - setMemberCountFromSecretKeyData(null); - setHideCommonKeyPopup(false); - setFirstSecretKeyInCreation(false); - setGroupSection('chat'); - setIsOpenDrawer(false); - setIsForceShowCreationKeyPopup(false); - setTimeout(() => { - setSelectedGroup(group); - }, 200); - }} - sx={{ - display: 'flex', - background: - group?.groupId === selectedGroup?.groupId && - theme.palette.action.selected, - borderRadius: '2px', - cursor: 'pointer', - flexDirection: 'column', - padding: '2px', - width: '100%', - '&:hover': { - backgroundColor: 'action.hover', // background on hover - }, - }} - > - - - - {groupsOwnerNames[group?.groupId] ? ( - - {group?.groupName?.charAt(0).toUpperCase()} - - ) : ( - - {' '} - {group?.groupName?.charAt(0).toUpperCase() || 'G'} - - )} - - - {groupAnnouncements[group?.groupId] && - !groupAnnouncements[group?.groupId]?.seentimestamp && ( - - )} - - {group?.data && - groupChatTimestamps[group?.groupId] && - group?.sender !== myAddress && - group?.timestamp && - ((!timestampEnterData[group?.groupId] && - Date.now() - group?.timestamp < - timeDifferenceForNotificationChats) || - timestampEnterData[group?.groupId] < - group?.timestamp) && ( - - )} - {groupsProperties[group?.groupId]?.isOpen === false && ( - - )} - - - - - - ))} -
-
- {chatMode === 'groups' && ( - <> - { - setOpenAddGroup(true); - }} - > - - Group - - - {!isRunningPublicNode && ( - { - setIsOpenBlockedUserModal(true); - }} - sx={{ - minWidth: 'unset', - padding: '10px', - }} - > - - - )} - - )} - {chatMode === 'directs' && ( - { - setNewChat(true); - setSelectedDirect(null); - setIsOpenDrawer(false); - }} - > - - New Chat - - )} -
-
- ); - }; + const selectGroupFunc = useCallback((group) => { + setMobileViewMode('group'); + setDesktopSideView('groups'); + initiatedGetMembers.current = false; + clearAllQueues(); + setSelectedDirect(null); + setTriedToFetchSecretKey(false); + setNewChat(false); + setSelectedGroup(null); + setUserInfoForLevels({}); + setSecretKey(null); + lastFetchedSecretKey.current = null; + setSecretKeyPublishDate(null); + setAdmins([]); + setSecretKeyDetails(null); + setAdminsWithNames([]); + setGroupOwner(null); + setMembers([]); + setMemberCountFromSecretKeyData(null); + setHideCommonKeyPopup(false); + setFirstSecretKeyInCreation(false); + setGroupSection('chat'); + setIsOpenDrawer(false); + setIsForceShowCreationKeyPopup(false); + setTimeout(() => { + setSelectedGroup(group); + }, 200); + }, []); return ( <> @@ -2176,9 +1915,24 @@ export const Group = ({ /> )} - {desktopViewMode === 'chat' && - desktopSideView !== 'directs' && - renderGroups()} + {desktopViewMode === 'chat' && desktopSideView !== 'directs' && ( + + )} {desktopViewMode === 'chat' && desktopSideView === 'directs' && @@ -2318,7 +2072,7 @@ export const Group = ({ isPrivate={isPrivate} setSecretKey={setSecretKey} handleNewEncryptionNotification={setNewEncryptionNotification} - hide={groupSection !== 'chat' || selectedDirect || newChat} + hide={groupSection !== 'chat' || !!selectedDirect || newChat} hideView={!(desktopViewMode === 'chat' && selectedGroup)} handleSecretKeyCreationInProgress={ handleSecretKeyCreationInProgress diff --git a/src/components/Group/GroupList.tsx b/src/components/Group/GroupList.tsx new file mode 100644 index 0000000..896ed4d --- /dev/null +++ b/src/components/Group/GroupList.tsx @@ -0,0 +1,348 @@ +import { + Avatar, + Box, + ButtonBase, + List, + ListItem, + ListItemAvatar, + ListItemText, + useTheme, +} from '@mui/material'; +import React, { useCallback } from 'react'; +import { IconWrapper } from '../Desktop/DesktopFooter'; +import { HubsIcon } from '../../assets/Icons/HubsIcon'; +import { MessagingIcon } from '../../assets/Icons/MessagingIcon'; +import { ContextMenu } from '../ContextMenu'; +import { getBaseApiReact } from '../../App'; +import { formatEmailDate } from './QMailMessages'; +import CampaignIcon from '@mui/icons-material/Campaign'; +import MarkChatUnreadIcon from '@mui/icons-material/MarkChatUnread'; +import LockIcon from '@mui/icons-material/Lock'; +import { CustomButton } from '../../styles/App-styles'; +import AddCircleOutlineIcon from '@mui/icons-material/AddCircleOutline'; +import PersonOffIcon from '@mui/icons-material/PersonOff'; +import { + groupAnnouncementSelector, + groupChatTimestampSelector, + groupPropertySelector, + groupsOwnerNamesSelector, + timestampEnterDataSelector, +} from '../../atoms/global'; +import { useRecoilValue } from 'recoil'; +import { timeDifferenceForNotificationChats } from './Group'; + +export const GroupList = ({ + selectGroupFunc, + setDesktopSideView, + groupChatHasUnread, + groupsAnnHasUnread, + desktopSideView, + directChatHasUnread, + chatMode, + groups, + selectedGroup, + getUserSettings, + setOpenAddGroup, + isRunningPublicNode, + setIsOpenBlockedUserModal, + myAddress, +}) => { + const theme = useTheme(); + return ( +
+ + { + setDesktopSideView('groups'); + }} + > + + + + + { + setDesktopSideView('directs'); + }} + > + + + + + + +
+ + {groups.map((group: any) => ( + + ))} + +
+
+ <> + { + setOpenAddGroup(true); + }} + > + + Group + + + {!isRunningPublicNode && ( + { + setIsOpenBlockedUserModal(true); + }} + sx={{ + minWidth: 'unset', + padding: '10px', + }} + > + + + )} + +
+
+ ); +}; + +const GroupItem = React.memo( + ({ selectGroupFunc, group, selectedGroup, getUserSettings, myAddress }) => { + const theme = useTheme(); + const ownerName = useRecoilValue(groupsOwnerNamesSelector(group?.groupId)); + const announcement = useRecoilValue( + groupAnnouncementSelector(group?.groupId) + ); + const groupProperty = useRecoilValue(groupPropertySelector(group?.groupId)); + const groupChatTimestamp = useRecoilValue( + groupChatTimestampSelector(group?.groupId) + ); + const timestampEnterData = useRecoilValue( + timestampEnterDataSelector(group?.groupId) + ); + const selectGroupHandler = useCallback(() => { + selectGroupFunc(group); + }, [group, selectGroupFunc]); + + return ( + + + + + {ownerName ? ( + + {group?.groupName?.charAt(0).toUpperCase()} + + ) : ( + + {' '} + {group?.groupName?.charAt(0).toUpperCase() || 'G'} + + )} + + + {announcement && !announcement?.seentimestamp && ( + + )} + + {group?.data && + groupChatTimestamp && + group?.sender !== myAddress && + group?.timestamp && + ((!timestampEnterData && + Date.now() - group?.timestamp < + timeDifferenceForNotificationChats) || + timestampEnterData < group?.timestamp) && ( + + )} + {groupProperty?.isOpen === false && ( + + )} + + + + + ); + } +);