import { Avatar, Box, ButtonBase, List, ListItem, ListItemAvatar, ListItemText, Typography, useTheme, } from '@mui/material'; import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; import { ChatGroup } from '../Chat/ChatGroup'; import { CreateCommonSecret } from '../Chat/CreateCommonSecret'; import { base64ToUint8Array } from '../../qdn/encryption/group-encryption'; import { uint8ArrayToObject } from '../../backgroundFunctions/encryption'; import { AddGroup } from './AddGroup'; import CreateIcon from '@mui/icons-material/Create'; import { AuthenticatedContainerInnerRight, CustomButton, } from '../../styles/App-styles'; import { Spacer } from '../../common/Spacer'; import { ManageMembers } from './ManageMembers'; import MarkChatUnreadIcon from '@mui/icons-material/MarkChatUnread'; import { clearAllQueues, getArbitraryEndpointReact, getBaseApiReact, pauseAllQueues, resumeAllQueues, } from '../../App'; import { ChatDirect } from '../Chat/ChatDirect'; import { CustomizedSnackbars } from '../Snackbar/Snackbar'; import { LoadingButton } from '@mui/lab'; import { LoadingSnackbar } from '../Snackbar/LoadingSnackbar'; import { GroupAnnouncements } from '../Chat/GroupAnnouncements'; import { GroupForum } from '../Chat/GroupForum'; import { executeEvent, subscribeToEvent, unsubscribeFromEvent, } from '../../utils/events'; import { RequestQueueWithPromise } from '../../utils/queue/queue'; import { WebSocketActive } from './WebsocketActive'; import { useMessageQueue } from '../../MessageQueueContext'; import { HomeDesktop } from './HomeDesktop'; import { IconWrapper } from '../Desktop/DesktopFooter'; import { DesktopHeader } from '../Desktop/DesktopHeader'; import { AppsDesktop } from '../Apps/AppsDesktop'; import { AppsDevMode } from '../Apps/AppsDevMode'; import { DesktopSideBar } from '../DesktopSideBar'; import { HubsIcon } from '../../assets/Icons/HubsIcon'; import { MessagingIcon } from '../../assets/Icons/MessagingIcon'; import { formatEmailDate } from './QMailMessages'; import { AdminSpace } from '../Chat/AdminSpace'; import { addressInfoControllerAtom, groupAnnouncementsAtom, groupChatTimestampsAtom, groupsOwnerNamesAtom, groupsPropertiesAtom, isOpenBlockedModalAtom, memberGroupsAtom, mutedGroupsAtom, selectedGroupIdAtom, timestampEnterDataAtom, } from '../../atoms/global'; import { sortArrayByTimestampAndGroupName } from '../../utils/time'; import { BlockedUsersModal } from './BlockedUsersModal'; import { WalletsAppWrapper } from './WalletsAppWrapper'; import { useTranslation } from 'react-i18next'; import { GroupList } from './GroupList'; import { useAtom, useSetAtom } from 'jotai'; export const getPublishesFromAdmins = async (admins: string[], groupId) => { const queryString = admins.map((name) => `name=${name}`).join('&'); const url = `${getBaseApiReact()}${getArbitraryEndpointReact()}?mode=ALL&service=DOCUMENT_PRIVATE&identifier=symmetric-qchat-group-${ groupId }&exactmatchnames=true&limit=0&reverse=true&${queryString}&prefix=true`; const response = await fetch(url); if (!response.ok) { throw new Error('network error'); } const adminData = await response.json(); const filterId = adminData.filter( (data: any) => data.identifier === `symmetric-qchat-group-${groupId}` ); if (filterId?.length === 0) { return false; } const sortedData = filterId.sort((a: any, b: any) => { // Get the most recent date for both a and b const dateA = a.updated ? new Date(a.updated) : new Date(a.created); const dateB = b.updated ? new Date(b.updated) : new Date(b.created); // Sort by most recent return dateB.getTime() - dateA.getTime(); }); return sortedData[0]; }; interface GroupProps { balance: number; isFocused: boolean; myAddress: string; userInfo: any; } export const timeDifferenceForNotificationChats = 900000; export const requestQueueMemberNames = new RequestQueueWithPromise(5); export const requestQueueAdminMemberNames = new RequestQueueWithPromise(5); export const getGroupAdminsAddress = async (groupNumber: number) => { const response = await fetch( `${getBaseApiReact()}/groups/members/${groupNumber}?limit=0&onlyAdmins=true` ); const groupData = await response.json(); const members: any = []; if (groupData && Array.isArray(groupData?.members)) { for (const member of groupData.members) { if (member.member) { members.push(member?.member); } } return members; } }; export function validateSecretKey(obj) { // Check if the input is an object if (typeof obj !== 'object' || obj === null) { return false; } // Iterate over each key in the object for (const key in obj) { // Ensure the key is a string representation of a positive integer if (!/^\d+$/.test(key)) { return false; } // Get the corresponding value for the key const value = obj[key]; // Check that value is an object and not null if (typeof value !== 'object' || value === null) { return false; } // Check for messageKey if (!value.hasOwnProperty('messageKey')) { return false; } // Ensure messageKey and nonce are non-empty strings if ( typeof value.messageKey !== 'string' || value.messageKey.trim() === '' ) { return false; } } // If all checks passed, return true return true; } export const getGroupMembers = async (groupNumber: number) => { // const validApi = await findUsableApi(); const response = await fetch( `${getBaseApiReact()}/groups/members/${groupNumber}?limit=0` ); const groupData = await response.json(); return groupData; }; export const decryptResource = async (data: string, fromQortalRequest) => { try { return new Promise((res, rej) => { window .sendMessage('decryptGroupEncryption', { data, }) .then((response) => { if (!response?.error) { res(response); return; } if (fromQortalRequest) { rej({ error: response.error, message: response?.error }); } else { rej(response.error); } }) .catch((error) => { if (fromQortalRequest) { rej({ message: error.message || 'An error occurred', error: error.message || 'An error occurred', }); } else { rej(error.message || 'An error occurred'); } }); }); } catch (error) { console.log(error); } }; export const addDataPublishesFunc = async (data: string, groupId, type) => { try { return new Promise((res, rej) => { window .sendMessage('addDataPublishes', { data, groupId, type, }) .then((response) => { if (!response?.error) { res(response); return; } rej(response.error); }) .catch((error) => { rej(error.message || 'An error occurred'); }); }); } catch (error) { console.log(error); } }; export const getDataPublishesFunc = async (groupId, type) => { try { return new Promise((res, rej) => { window .sendMessage('getDataPublishes', { groupId, type, }) .then((response) => { if (!response?.error) { res(response); return; } rej(response.error); }) .catch((error) => { rej(error.message || 'An error occurred'); }); }); } catch (error) { console.log(error); } }; export async function getNameInfo(address: string) { const response = await fetch(`${getBaseApiReact()}/names/address/` + address); const nameData = await response.json(); if (nameData?.length > 0) { return nameData[0]?.name; } else { return ''; } } export const getGroupAdmins = async (groupNumber: number) => { // const validApi = await findUsableApi(); const response = await fetch( `${getBaseApiReact()}/groups/members/${groupNumber}?limit=0&onlyAdmins=true` ); const groupData = await response.json(); let members: any = []; let membersAddresses = []; let both = []; const getMemNames = groupData?.members?.map(async (member) => { if (member?.member) { const name = await requestQueueAdminMemberNames.enqueue(() => { return getNameInfo(member.member); }); if (name) { members.push(name); both.push({ name, address: member.member }); } membersAddresses.push(member.member); } return true; }); await Promise.all(getMemNames); return { names: members, addresses: membersAddresses, both }; }; export const getNames = async (listOfMembers) => { // const validApi = await findUsableApi(); let members: any = []; const getMemNames = listOfMembers.map(async (member) => { if (member.member) { const name = await requestQueueMemberNames.enqueue(() => { return getNameInfo(member.member); }); if (name) { members.push({ ...member, name }); } else { members.push({ ...member, name: '' }); } } return true; }); await Promise.all(getMemNames); return members; }; export const getNamesForAdmins = async (admins) => { let members: any = []; const getMemNames = admins?.map(async (admin) => { if (admin) { const name = await requestQueueAdminMemberNames.enqueue(() => { return getNameInfo(admin); }); if (name) { members.push({ address: admin, name }); } } return true; }); await Promise.all(getMemNames); return members; }; function areKeysEqual(array1, array2) { // If lengths differ, the arrays cannot be equal if (array1?.length !== array2?.length) { return false; } // Sort both arrays and compare their elements const sortedArray1 = [...array1].sort(); const sortedArray2 = [...array2].sort(); return sortedArray1.every((key, index) => key === sortedArray2[index]); } export const Group = ({ myAddress, isFocused, userInfo, balance, setIsOpenDrawerProfile, setDesktopViewMode, desktopViewMode, }: GroupProps) => { const [desktopSideView, setDesktopSideView] = useState('groups'); const [secretKey, setSecretKey] = useState(null); const [secretKeyPublishDate, setSecretKeyPublishDate] = useState(null); const lastFetchedSecretKey = useRef(null); const [secretKeyDetails, setSecretKeyDetails] = useState(null); const [newEncryptionNotification, setNewEncryptionNotification] = useState(null); const [memberCountFromSecretKeyData, setMemberCountFromSecretKeyData] = useState(null); const [selectedGroup, setSelectedGroup] = useState(null); const [selectedDirect, setSelectedDirect] = useState(null); const hasInitialized = useRef(false); const hasInitializedWebsocket = useRef(false); const [groups, setGroups] = useState([]); const [directs, setDirects] = useState([]); const [admins, setAdmins] = useState([]); const [adminsWithNames, setAdminsWithNames] = useState([]); const [members, setMembers] = useState([]); const [groupOwner, setGroupOwner] = useState(null); const [triedToFetchSecretKey, setTriedToFetchSecretKey] = useState(false); const [openAddGroup, setOpenAddGroup] = useState(false); const [isInitialGroups, setIsInitialGroups] = useState(false); const [openManageMembers, setOpenManageMembers] = useState(false); const setMemberGroups = useSetAtom(memberGroupsAtom); const lastGroupNotification = useRef(null); const [timestampEnterData, setTimestampEnterData] = useAtom( timestampEnterDataAtom ); const [chatMode, setChatMode] = useState('groups'); const [newChat, setNewChat] = useState(false); const [openSnack, setOpenSnack] = useState(false); const [infoSnack, setInfoSnack] = useState(null); const [isLoadingNotifyAdmin, setIsLoadingNotifyAdmin] = useState(false); const [isLoadingGroups, setIsLoadingGroups] = useState(true); const [isLoadingGroup, setIsLoadingGroup] = useState(false); const [firstSecretKeyInCreation, setFirstSecretKeyInCreation] = useState(false); const [groupSection, setGroupSection] = useState('home'); const [groupAnnouncements, setGroupAnnouncements] = useAtom( groupAnnouncementsAtom ); const [defaultThread, setDefaultThread] = useState(null); const [isOpenDrawer, setIsOpenDrawer] = useState(false); const setIsOpenBlockedUserModal = useSetAtom(isOpenBlockedModalAtom); const [hideCommonKeyPopup, setHideCommonKeyPopup] = useState(false); const [isLoadingGroupMessage, setIsLoadingGroupMessage] = useState(''); const [drawerMode, setDrawerMode] = useState('groups'); const setMutedGroups = useSetAtom(mutedGroupsAtom); const [mobileViewMode, setMobileViewMode] = useState('home'); const [mobileViewModeKeepOpen, setMobileViewModeKeepOpen] = useState(''); const isFocusedRef = useRef(true); const timestampEnterDataRef = useRef({}); const selectedGroupRef = useRef(null); const selectedDirectRef = useRef(null); const groupSectionRef = useRef(null); const checkGroupInterval = useRef(null); const isLoadingOpenSectionFromNotification = useRef(false); const setupGroupWebsocketInterval = useRef(null); const settimeoutForRefetchSecretKey = useRef(null); const { clearStatesMessageQueueProvider } = useMessageQueue(); const initiatedGetMembers = useRef(false); const [groupChatTimestamps, setGroupChatTimestamps] = useAtom( groupChatTimestampsAtom ); const [appsMode, setAppsMode] = useState('home'); const [appsModeDev, setAppsModeDev] = useState('home'); const [isOpenSideViewDirects, setIsOpenSideViewDirects] = useState(false); const [isOpenSideViewGroups, setIsOpenSideViewGroups] = useState(false); const [isForceShowCreationKeyPopup, setIsForceShowCreationKeyPopup] = useState(false); const groupsOwnerNamesRef = useRef({}); const { t } = useTranslation(['core', 'group']); const [groupsProperties, setGroupsProperties] = useAtom(groupsPropertiesAtom); const setGroupsOwnerNames = useSetAtom(groupsOwnerNamesAtom); const setUserInfoForLevels = useSetAtom(addressInfoControllerAtom); const isPrivate = useMemo(() => { if (selectedGroup?.groupId === '0') return false; if (!selectedGroup?.groupId || !groupsProperties[selectedGroup?.groupId]) return null; if (groupsProperties[selectedGroup?.groupId]?.isOpen === true) return false; if (groupsProperties[selectedGroup?.groupId]?.isOpen === false) return true; return null; }, [selectedGroup]); const setSelectedGroupId = useSetAtom(selectedGroupIdAtom); const toggleSideViewDirects = () => { if (isOpenSideViewGroups) { setIsOpenSideViewGroups(false); } setIsOpenSideViewDirects((prev) => !prev); }; const toggleSideViewGroups = () => { if (isOpenSideViewDirects) { setIsOpenSideViewDirects(false); } setIsOpenSideViewGroups((prev) => !prev); }; useEffect(() => { timestampEnterDataRef.current = timestampEnterData; }, [timestampEnterData]); useEffect(() => { isFocusedRef.current = isFocused; }, [isFocused]); useEffect(() => { groupSectionRef.current = groupSection; }, [groupSection]); useEffect(() => { selectedGroupRef.current = selectedGroup; setSelectedGroupId(selectedGroup?.groupId); }, [selectedGroup]); useEffect(() => { selectedDirectRef.current = selectedDirect; }, [selectedDirect]); const getUserSettings = useCallback(async () => { try { return new Promise((res, rej) => { window .sendMessage('getUserSettings', { key: 'mutedGroups', }) .then((response) => { if (!response?.error) { setMutedGroups(response || []); res(response); return; } rej(response.error); }) .catch((error) => { rej( error.message || t('core:message.error.generic', { postProcess: 'capitalizeFirst', }) ); }); }); } catch (error) { console.log('error', error); } }, [setMutedGroups]); useEffect(() => { getUserSettings(); }, [getUserSettings]); const getTimestampEnterChat = useCallback(async () => { try { return new Promise((res, rej) => { window .sendMessage('getTimestampEnterChat') .then((response) => { if (!response?.error) { setTimestampEnterData(response); res(response); return; } rej(response.error); }) .catch((error) => { rej( error.message || t('core:message.error.generic', { postProcess: 'capitalizeFirst', }) ); }); }); } catch (error) { console.log(error); } }, []); const refreshHomeDataFunc = () => { setGroupSection('default'); setTimeout(() => { setGroupSection('home'); }, 300); }; const getGroupAnnouncements = async () => { try { return new Promise((res, rej) => { window .sendMessage('getGroupNotificationTimestamp') .then((response) => { if (!response?.error) { setGroupAnnouncements(response); res(response); return; } rej(response.error); }) .catch((error) => { rej( error.message || t('core:message.error.generic', { postProcess: 'capitalizeFirst', }) ); }); }); } catch (error) { console.log(error); } }; useEffect(() => { if (myAddress) { getGroupAnnouncements(); getTimestampEnterChat(); } }, [myAddress]); const getGroupOwner = async (groupId) => { try { const url = `${getBaseApiReact()}/groups/${groupId}`; const response = await fetch(url); let data = await response.json(); const name = await getNameInfo(data?.owner); if (name) { data.name = name; } setGroupOwner(data); } catch (error) { console.log(error); } }; const directChatHasUnread = useMemo(() => { let hasUnread = false; directs.forEach((direct) => { if ( direct?.sender !== myAddress && direct?.timestamp && ((!timestampEnterData[direct?.address] && Date.now() - direct?.timestamp < timeDifferenceForNotificationChats) || timestampEnterData[direct?.address] < direct?.timestamp) ) { hasUnread = true; } }); return hasUnread; }, [timestampEnterData, directs, myAddress]); const groupChatHasUnread = useMemo(() => { let hasUnread = false; groups.forEach((group) => { if ( group?.data && group?.sender !== myAddress && group?.timestamp && groupChatTimestamps[group?.groupId] && ((!timestampEnterData[group?.groupId] && Date.now() - group?.timestamp < timeDifferenceForNotificationChats) || timestampEnterData[group?.groupId] < group?.timestamp) ) { hasUnread = true; } }); return hasUnread; }, [timestampEnterData, groups, myAddress, groupChatTimestamps]); const groupsAnnHasUnread = useMemo(() => { let hasUnread = false; groups.forEach((group) => { if ( groupAnnouncements[group?.groupId] && !groupAnnouncements[group?.groupId]?.seentimestamp ) { hasUnread = true; } }); return hasUnread; }, [groupAnnouncements, groups]); const getSecretKey = useCallback( 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 { 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(); } 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(); } }, [ secretKey, selectedGroup?.groupId, setIsLoadingGroup, setIsLoadingGroupMessage, setSecretKey, setSecretKeyDetails, setTriedToFetchSecretKey, setFirstSecretKeyInCreation, setMemberCountFromSecretKeyData, setAdmins, setAdminsWithNames, setSecretKeyPublishDate, ] ); const getAdminsForPublic = async (selectedGroup) => { try { const { names, addresses, both } = await getGroupAdmins( selectedGroup?.groupId ); setAdmins(addresses); setAdminsWithNames(both); } catch (error) { console.log(error); } }; useEffect(() => { if (selectedGroup && isPrivate !== null) { if (isPrivate) { setTriedToFetchSecretKey(false); getSecretKey(true); } getGroupOwner(selectedGroup?.groupId); } if (isPrivate === false) { setTriedToFetchSecretKey(true); if (selectedGroup?.groupId !== '0') { getAdminsForPublic(selectedGroup); } } }, [selectedGroup, isPrivate]); const getCountNewMesg = async (groupId, after) => { try { const response = await fetch( `${getBaseApiReact()}/chat/messages?after=${after}&txGroupId=${groupId}&haschatreference=false&encoding=BASE64&limit=1` ); const data = await response.json(); if (data && data[0]) return data[0].timestamp; } catch (error) { console.log(error); } }; const getLatestRegularChat = async (groups) => { try { const groupData = {}; const getGroupData = groups.map(async (group) => { if (!group.groupId || !group?.timestamp) return null; if ( !groupData[group.groupId] || groupData[group.groupId] < group.timestamp ) { const hasMoreRecentMsg = await getCountNewMesg( group.groupId, timestampEnterDataRef.current[group?.groupId] || Date.now() - 24 * 60 * 60 * 1000 ); if (hasMoreRecentMsg) { groupData[group.groupId] = hasMoreRecentMsg; } } else { return null; } }); await Promise.all(getGroupData); setGroupChatTimestamps(groupData); } catch (error) { console.log(error); } }; const getOwnerNameForGroup = async (owner: string, groupId: string) => { try { if (!owner) return; if (groupsOwnerNamesRef.current[groupId]) return; const name = await requestQueueMemberNames.enqueue(() => { return getNameInfo(owner); }); if (name) { groupsOwnerNamesRef.current[groupId] = name; setGroupsOwnerNames((prev) => { return { ...prev, [groupId]: name }; }); } } catch (error) { console.error(error); } }; const getGroupsProperties = useCallback(async (address) => { try { const url = `${getBaseApiReact()}/groups/member/${address}`; const response = await fetch(url); if (!response.ok) throw new Error('Cannot get group properties'); let data = await response.json(); const transformToObject = data.reduce((result, item) => { result[item.groupId] = item; return result; }, {}); setGroupsProperties(transformToObject); Object.keys(transformToObject).forEach((key) => { getOwnerNameForGroup(transformToObject[key]?.owner || '', key); }); } catch (error) { console.log(error); } }, []); useEffect(() => { if (!myAddress) return; if ( areKeysEqual( groups?.map((grp) => grp?.groupId), Object.keys(groupsProperties) ) ) { // TODO: empty block. Check it! } else { getGroupsProperties(myAddress); } }, [groups, myAddress]); useEffect(() => { // Handler function for incoming messages const messageHandler = (event) => { if (event.origin !== window.location.origin) { return; } const message = event.data; if (message?.action === 'SET_GROUPS') { // Update the component state with the received 'sendqort' state setGroups(sortArrayByTimestampAndGroupName(message.payload)); getLatestRegularChat(message.payload); setMemberGroups( message.payload?.filter((item) => item?.groupId !== '0') ); if (selectedGroupRef.current && groupSectionRef.current === 'chat') { window .sendMessage('addTimestampEnterChat', { timestamp: Date.now(), groupId: selectedGroupRef.current.groupId, }) .catch((error) => { console.error( 'Failed to add timestamp:', error.message || 'An error occurred' ); }); } if (selectedDirectRef.current) { window .sendMessage('addTimestampEnterChat', { timestamp: Date.now(), groupId: selectedDirectRef.current.address, }) .catch((error) => { console.error( 'Failed to add timestamp:', error.message || 'An error occurred' ); }); } setTimeout(() => { getTimestampEnterChat(); }, 600); } if (message?.action === 'SET_GROUP_ANNOUNCEMENTS') { // Update the component state with the received 'sendqort' state setGroupAnnouncements(message.payload); if ( selectedGroupRef.current && groupSectionRef.current === 'announcement' ) { window .sendMessage('addGroupNotificationTimestamp', { timestamp: Date.now(), groupId: selectedGroupRef.current.groupId, }) .catch((error) => { console.error( 'Failed to add group notification timestamp:', error.message || 'An error occurred' ); }); setTimeout(() => { getGroupAnnouncements(); }, 200); } } if (message?.action === 'SET_DIRECTS') { // Update the component state with the received 'sendqort' state setDirects(message.payload); } else if (message?.action === 'PLAY_NOTIFICATION_SOUND') { // audio.play(); } }; // Attach the event listener window.addEventListener('message', messageHandler); // Clean up the event listener on component unmount return () => { window.removeEventListener('message', messageHandler); }; }, []); useEffect(() => { if ( !myAddress || hasInitializedWebsocket.current || !groups || groups?.length === 0 ) return; window.sendMessage('setupGroupWebsocket', {}).catch((error) => { console.error( 'Failed to setup group websocket:', error.message || 'An error occurred' ); }); hasInitializedWebsocket.current = true; }, [myAddress, groups]); const getMembers = async (groupId) => { try { const res = await getGroupMembers(groupId); if (groupId !== selectedGroupRef.current?.groupId) return; setMembers(res); } catch (error) { console.log(error); } }; useEffect(() => { if ( !initiatedGetMembers.current && selectedGroup?.groupId && secretKey && admins.includes(myAddress) && selectedGroup?.groupId !== '0' ) { // getAdmins(selectedGroup?.groupId); getMembers(selectedGroup?.groupId); initiatedGetMembers.current = true; } }, [selectedGroup?.groupId, secretKey, myAddress, admins]); const shouldReEncrypt = useMemo(() => { if (triedToFetchSecretKey && !secretKeyPublishDate) return true; if ( !secretKeyPublishDate || !memberCountFromSecretKeyData || members?.length === 0 ) return false; const isDiffMemberNumber = memberCountFromSecretKeyData !== members?.memberCount && newEncryptionNotification?.decryptedData?.data?.numberOfMembers !== members?.memberCount; if (isDiffMemberNumber) return true; const latestJoined = members?.members.reduce((maxJoined, current) => { return current.joined > maxJoined ? current.joined : maxJoined; }, members?.members[0].joined); if ( secretKeyPublishDate < latestJoined && newEncryptionNotification?.data?.timestamp < latestJoined ) { return true; } return false; }, [ memberCountFromSecretKeyData, members, secretKeyPublishDate, newEncryptionNotification, triedToFetchSecretKey, ]); const notifyAdmin = async (admin) => { try { setIsLoadingNotifyAdmin(true); await new Promise((res, rej) => { window .sendMessage('notifyAdminRegenerateSecretKey', { adminAddress: admin.address, groupName: selectedGroup?.groupName, }) .then((response) => { if (!response?.error) { res(response); return; } rej(response.error); }) .catch((error) => { rej( error.message || t('core:message.error.generic', { postProcess: 'capitalizeFirst', }) ); }); }); setInfoSnack({ type: 'success', message: 'Successfully sent notification.', }); setOpenSnack(true); } catch (error) { setInfoSnack({ type: 'error', message: 'Unable to send notification', }); } finally { setIsLoadingNotifyAdmin(false); } }; const isUnreadChat = useMemo(() => { const findGroup = groups .filter((group) => group?.sender !== myAddress) .find((gr) => gr?.groupId === selectedGroup?.groupId); if (!findGroup) return false; if (!findGroup?.data) return false; return ( findGroup?.timestamp && groupChatTimestamps[findGroup?.groupId] && ((!timestampEnterData[selectedGroup?.groupId] && Date.now() - findGroup?.timestamp < timeDifferenceForNotificationChats) || timestampEnterData?.[selectedGroup?.groupId] < findGroup?.timestamp) ); }, [timestampEnterData, selectedGroup, groupChatTimestamps]); const isUnread = useMemo(() => { if (!selectedGroup) return false; return ( groupAnnouncements?.[selectedGroup?.groupId]?.seentimestamp === false ); }, [groupAnnouncements, selectedGroup, myAddress]); const openDirectChatFromNotification = (e) => { if (isLoadingOpenSectionFromNotification.current) return; isLoadingOpenSectionFromNotification.current = true; const directAddress = e.detail?.from; const findDirect = directs?.find( (direct) => direct?.address === directAddress ); if (findDirect?.address === selectedDirect?.address) { isLoadingOpenSectionFromNotification.current = false; return; } if (findDirect) { setDesktopSideView('directs'); setDesktopViewMode('home'); setSelectedDirect(null); setNewChat(false); window .sendMessage('addTimestampEnterChat', { timestamp: Date.now(), groupId: findDirect.address, }) .catch((error) => { console.error( 'Failed to add timestamp:', error.message || 'An error occurred' ); }); setTimeout(() => { setSelectedDirect(findDirect); getTimestampEnterChat(); isLoadingOpenSectionFromNotification.current = false; }, 200); } else { isLoadingOpenSectionFromNotification.current = false; } }; const openDirectChatFromInternal = (e) => { const directAddress = e.detail?.address; const name = e.detail?.name; const findDirect = directs?.find( (direct) => direct?.address === directAddress || direct?.name === name ); if (findDirect) { setDesktopSideView('directs'); setSelectedDirect(null); setNewChat(false); window .sendMessage('addTimestampEnterChat', { timestamp: Date.now(), groupId: findDirect.address, }) .catch((error) => { console.error( 'Failed to add timestamp:', error.message || 'An error occurred' ); }); setTimeout(() => { setSelectedDirect(findDirect); getTimestampEnterChat(); }, 200); } else { setDesktopSideView('directs'); setNewChat(true); setTimeout(() => { executeEvent('setDirectToValueNewChat', { directToValue: name || directAddress, }); }, 500); } }; useEffect(() => { subscribeToEvent('openDirectMessageInternal', openDirectChatFromInternal); return () => { unsubscribeFromEvent( 'openDirectMessageInternal', openDirectChatFromInternal ); }; }, [directs, selectedDirect]); useEffect(() => { subscribeToEvent('openDirectMessage', openDirectChatFromNotification); return () => { unsubscribeFromEvent('openDirectMessage', openDirectChatFromNotification); }; }, [directs, selectedDirect]); const handleMarkAsRead = (e) => { const { groupId } = e.detail; window .sendMessage('addTimestampEnterChat', { timestamp: Date.now(), groupId, }) .catch((error) => { console.error( 'Failed to add timestamp:', error.message || 'An error occurred' ); }); window .sendMessage('addGroupNotificationTimestamp', { timestamp: Date.now(), groupId, }) .catch((error) => { console.error( 'Failed to add group notification timestamp:', error.message || 'An error occurred' ); }); setTimeout(() => { getGroupAnnouncements(); getTimestampEnterChat(); }, 200); }; useEffect(() => { subscribeToEvent('markAsRead', handleMarkAsRead); return () => { unsubscribeFromEvent('markAsRead', handleMarkAsRead); }; }, []); const resetAllStatesAndRefs = () => { // Reset all useState values to their initial states setSecretKey(null); lastFetchedSecretKey.current = null; setSecretKeyPublishDate(null); setSecretKeyDetails(null); setNewEncryptionNotification(null); setMemberCountFromSecretKeyData(null); setIsForceShowCreationKeyPopup(false); setSelectedGroup(null); setSelectedDirect(null); setGroups([]); setDirects([]); setAdmins([]); setAdminsWithNames([]); setMembers([]); setGroupOwner(null); setTriedToFetchSecretKey(false); setHideCommonKeyPopup(false); setOpenAddGroup(false); setIsInitialGroups(false); setOpenManageMembers(false); setTimestampEnterData({}); setChatMode('groups'); setNewChat(false); setOpenSnack(false); setInfoSnack(null); setIsLoadingNotifyAdmin(false); setIsLoadingGroups(false); setIsLoadingGroup(false); setFirstSecretKeyInCreation(false); setGroupSection('home'); setGroupAnnouncements({}); setDefaultThread(null); setMobileViewMode('home'); // Reset all useRef values to their initial states hasInitialized.current = false; hasInitializedWebsocket.current = false; lastGroupNotification.current = null; isFocusedRef.current = true; selectedGroupRef.current = null; selectedDirectRef.current = null; groupSectionRef.current = null; checkGroupInterval.current = null; isLoadingOpenSectionFromNotification.current = false; setupGroupWebsocketInterval.current = null; settimeoutForRefetchSecretKey.current = null; initiatedGetMembers.current = false; setDesktopViewMode('home'); }; const logoutEventFunc = () => { resetAllStatesAndRefs(); clearStatesMessageQueueProvider(); }; useEffect(() => { subscribeToEvent('logout-event', logoutEventFunc); return () => { unsubscribeFromEvent('logout-event', logoutEventFunc); }; }, []); const openAppsMode = () => { setDesktopViewMode('apps'); }; useEffect(() => { subscribeToEvent('open-apps-mode', openAppsMode); return () => { unsubscribeFromEvent('open-apps-mode', openAppsMode); }; }, []); const openGroupChatFromNotification = (e) => { if (isLoadingOpenSectionFromNotification.current) return; const groupId = e.detail?.from; const findGroup = groups?.find((group) => +group?.groupId === +groupId); if (findGroup?.groupId === selectedGroup?.groupId) { isLoadingOpenSectionFromNotification.current = false; setChatMode('groups'); setDesktopViewMode('chat'); return; } if (findGroup) { setChatMode('groups'); setSelectedGroup(null); setSelectedDirect(null); setNewChat(false); setSecretKey(null); setGroupOwner(null); lastFetchedSecretKey.current = null; initiatedGetMembers.current = false; setSecretKeyPublishDate(null); setAdmins([]); setSecretKeyDetails(null); setAdminsWithNames([]); setMembers([]); setMemberCountFromSecretKeyData(null); setIsForceShowCreationKeyPopup(false); setTriedToFetchSecretKey(false); setFirstSecretKeyInCreation(false); setGroupSection('chat'); setDesktopViewMode('chat'); window .sendMessage('addTimestampEnterChat', { timestamp: Date.now(), groupId: findGroup.groupId, }) .catch((error) => { console.error( 'Failed to add timestamp:', error.message || 'An error occurred' ); }); setTimeout(() => { setSelectedGroup(findGroup); setMobileViewMode('group'); setDesktopSideView('groups'); getTimestampEnterChat(); isLoadingOpenSectionFromNotification.current = false; }, 350); } else { isLoadingOpenSectionFromNotification.current = false; } }; useEffect(() => { subscribeToEvent('openGroupMessage', openGroupChatFromNotification); return () => { unsubscribeFromEvent('openGroupMessage', openGroupChatFromNotification); }; }, [groups, selectedGroup]); const openGroupAnnouncementFromNotification = (e) => { const groupId = e.detail?.from; const findGroup = groups?.find((group) => +group?.groupId === +groupId); if (findGroup?.groupId === selectedGroup?.groupId) return; if (findGroup) { setChatMode('groups'); setSelectedGroup(null); setSecretKey(null); setGroupOwner(null); lastFetchedSecretKey.current = null; initiatedGetMembers.current = false; setSecretKeyPublishDate(null); setAdmins([]); setSecretKeyDetails(null); setAdminsWithNames([]); setMembers([]); setMemberCountFromSecretKeyData(null); setIsForceShowCreationKeyPopup(false); setTriedToFetchSecretKey(false); setFirstSecretKeyInCreation(false); setGroupSection('announcement'); setDesktopViewMode('chat'); window .sendMessage('addGroupNotificationTimestamp', { timestamp: Date.now(), groupId: findGroup.groupId, }) .catch((error) => { console.error( 'Failed to add group notification timestamp:', error.message || 'An error occurred' ); }); setTimeout(() => { setSelectedGroup(findGroup); setMobileViewMode('group'); setDesktopSideView('groups'); getGroupAnnouncements(); }, 350); } }; useEffect(() => { subscribeToEvent( 'openGroupAnnouncement', openGroupAnnouncementFromNotification ); return () => { unsubscribeFromEvent( 'openGroupAnnouncement', openGroupAnnouncementFromNotification ); }; }, [groups, selectedGroup]); const openThreadNewPostFunc = (e) => { const data = e.detail?.data; const { groupId } = data; const findGroup = groups?.find((group) => +group?.groupId === +groupId); if (findGroup?.groupId === selectedGroup?.groupId) { setGroupSection('forum'); setDefaultThread(data); return; } if (findGroup) { setChatMode('groups'); setSelectedGroup(null); setSecretKey(null); setGroupOwner(null); lastFetchedSecretKey.current = null; initiatedGetMembers.current = false; setSecretKeyPublishDate(null); setAdmins([]); setSecretKeyDetails(null); setAdminsWithNames([]); setMembers([]); setMemberCountFromSecretKeyData(null); setIsForceShowCreationKeyPopup(false); setTriedToFetchSecretKey(false); setFirstSecretKeyInCreation(false); setGroupSection('forum'); setDefaultThread(data); setDesktopViewMode('chat'); setTimeout(() => { setSelectedGroup(findGroup); setMobileViewMode('group'); setDesktopSideView('groups'); getGroupAnnouncements(); }, 350); } }; useEffect(() => { subscribeToEvent('openThreadNewPost', openThreadNewPostFunc); return () => { unsubscribeFromEvent('openThreadNewPost', openThreadNewPostFunc); }; }, [groups, selectedGroup]); const handleSecretKeyCreationInProgress = useCallback(() => { setFirstSecretKeyInCreation(true); }, []); const goToHome = async () => { setDesktopViewMode('home'); await new Promise((res) => { setTimeout(() => { res(null); }, 200); }); }; const goToAnnouncements = async () => { setGroupSection('default'); await new Promise((res) => { setTimeout(() => { res(null); }, 200); }); setSelectedDirect(null); setNewChat(false); setGroupSection('announcement'); window .sendMessage('addGroupNotificationTimestamp', { timestamp: Date.now(), groupId: selectedGroupRef.current.groupId, }) .catch((error) => { console.error( 'Failed to add group notification timestamp:', error.message || 'An error occurred' ); }); setTimeout(() => { getGroupAnnouncements(); }, 200); }; const openDrawerGroups = () => { setIsOpenDrawer(true); setDrawerMode('groups'); }; const goToThreads = () => { setSelectedDirect(null); setNewChat(false); setGroupSection('forum'); }; const goToChat = async () => { setGroupSection('default'); await new Promise((res) => { setTimeout(() => { res(null); }, 200); }); setGroupSection('chat'); setNewChat(false); setSelectedDirect(null); if (selectedGroupRef.current) { window .sendMessage('addTimestampEnterChat', { timestamp: Date.now(), groupId: selectedGroupRef.current.groupId, }) .catch((error) => { console.error( 'Failed to add timestamp:', error.message || 'An error occurred' ); }); setTimeout(() => { getTimestampEnterChat(); }, 200); } }; const theme = useTheme(); const renderDirects = () => { return (
{ setDesktopSideView('groups'); }} > { setDesktopSideView('directs'); }} >
{directs.map((direct: any) => ( { setSelectedDirect(null); setNewChat(false); setIsOpenDrawer(false); window .sendMessage('addTimestampEnterChat', { timestamp: Date.now(), groupId: direct.address, }) .catch((error) => { console.error( 'Failed to add timestamp:', error.message || 'An error occurred' ); }); setTimeout(() => { setSelectedDirect(direct); getTimestampEnterChat(); }, 200); }} sx={{ display: 'flex', width: '100%', flexDirection: 'column', cursor: 'pointer', padding: '2px', borderRadius: '2px', background: direct?.address === selectedDirect?.address && theme.palette.background.default, }} > {(direct?.name || direct?.address)?.charAt(0)} {direct?.sender !== myAddress && direct?.timestamp && ((!timestampEnterData[direct?.address] && Date.now() - direct?.timestamp < timeDifferenceForNotificationChats) || timestampEnterData[direct?.address] < direct?.timestamp) && ( )} ))}
{ 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 ( <>
{((desktopViewMode !== 'apps' && desktopViewMode !== 'dev') || isOpenSideViewGroups) && ( )} {desktopViewMode === 'chat' && desktopSideView !== 'directs' && ( )} {desktopViewMode === 'chat' && desktopSideView === 'directs' && renderDirects()} {newChat && ( <> { setSelectedDirect(null); setNewChat(false); }} setMobileViewModeKeepOpen={setMobileViewModeKeepOpen} /> )} {desktopViewMode === 'chat' && !selectedGroup && ( {t('group:message.generic.no_selection', { postProcess: 'capitalizeFirst', })} )}
{triedToFetchSecretKey && ( )} {isPrivate && firstSecretKeyInCreation && triedToFetchSecretKey && !secretKeyPublishDate && (
{' '} {t('group:message.generic.encryption_key', { postProcess: 'capitalizeFirst', })}
)} {isPrivate && !admins.includes(myAddress) && !secretKey && triedToFetchSecretKey ? ( <> {secretKeyPublishDate || (!secretKeyPublishDate && !firstSecretKeyInCreation) ? (
{' '} {t('group:message.generic.not_part_group', { postProcess: 'capitalizeFirst', })} {t('group:message.generic.only_encrypted', { postProcess: 'capitalizeFirst', })} {t('group:message.generic.notify_admins', { postProcess: 'capitalizeFirst', })} {adminsWithNames.map((admin) => { return ( {admin?.name} notifyAdmin(admin)} > {t('core:action.notify', { postProcess: 'capitalizeFirst', })} ); })}
) : null} ) : admins.includes(myAddress) && !secretKey && isPrivate && triedToFetchSecretKey ? null : !triedToFetchSecretKey ? null : ( <> {groupSection === 'adminSpace' && ( )} )} {((isPrivate && admins.includes(myAddress) && shouldReEncrypt && triedToFetchSecretKey && !firstSecretKeyInCreation && !hideCommonKeyPopup) || isForceShowCreationKeyPopup) && ( )}
{openManageMembers && ( )}
{selectedDirect && !newChat && ( <> { setSelectedDirect(null); setNewChat(false); }} setMobileViewModeKeepOpen={setMobileViewModeKeepOpen} /> )}
); };