diff --git a/src/components/Chat/AdminSpace.tsx b/src/components/Chat/AdminSpace.tsx index f340e45..47e7b0c 100644 --- a/src/components/Chat/AdminSpace.tsx +++ b/src/components/Chat/AdminSpace.tsx @@ -27,7 +27,8 @@ export const AdminSpace = ({ myAddress, hide, defaultThread, - setDefaultThread + setDefaultThread, + setIsForceShowCreationKeyPopup }) => { const { rootHeight } = useContext(MyContext); const [isMoved, setIsMoved] = useState(false); @@ -50,7 +51,8 @@ export const AdminSpace = ({ opacity: hide ? 0 : 1, visibility: hide && 'hidden', position: hide ? 'fixed' : 'relative', - left: hide && '-1000px' + left: hide && '-1000px', + overflow: 'auto' }} > {!isAdmin && Sorry, this space is only for Admins.} - {isAdmin && } + {isAdmin && } ); diff --git a/src/components/Chat/AdminSpaceInner.tsx b/src/components/Chat/AdminSpaceInner.tsx index 1307ad1..65852b6 100644 --- a/src/components/Chat/AdminSpaceInner.tsx +++ b/src/components/Chat/AdminSpaceInner.tsx @@ -1,150 +1,248 @@ -import React, { useCallback, useContext, useEffect, useState } from 'react' -import { MyContext, getArbitraryEndpointReact, getBaseApiReact } from '../../App'; -import { Box, Button, Typography } from '@mui/material'; -import { decryptResource, validateSecretKey } from '../Group/Group'; -import { getFee } from '../../background'; -import { base64ToUint8Array } from '../../qdn/encryption/group-encryption'; -import { uint8ArrayToObject } from '../../backgroundFunctions/encryption'; -import { formatTimestampForum } from '../../utils/time'; -import { Spacer } from '../../common/Spacer'; +import React, { useCallback, useContext, useEffect, useState } from "react"; +import { + MyContext, + getArbitraryEndpointReact, + getBaseApiReact, +} from "../../App"; +import { Box, Button, Typography } from "@mui/material"; +import { + decryptResource, + getPublishesFromAdmins, + validateSecretKey, +} from "../Group/Group"; +import { getFee } from "../../background"; +import { base64ToUint8Array } from "../../qdn/encryption/group-encryption"; +import { uint8ArrayToObject } from "../../backgroundFunctions/encryption"; +import { formatTimestampForum } from "../../utils/time"; +import { Spacer } from "../../common/Spacer"; +export const getPublishesFromAdminsAdminSpace = async ( + admins: string[], + groupId +) => { + const queryString = admins.map((name) => `name=${name}`).join("&"); + const url = `${getBaseApiReact()}${getArbitraryEndpointReact()}?mode=ALL&service=DOCUMENT_PRIVATE&identifier=admins-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(); -export const getPublishesFromAdminsAdminSpace = async (admins: string[], groupId) => { - const queryString = admins.map((name) => `name=${name}`).join("&"); - const url = `${getBaseApiReact()}${getArbitraryEndpointReact()}?mode=ALL&service=DOCUMENT_PRIVATE&identifier=admins-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 filterId = adminData.filter( + (data: any) => data.identifier === `admins-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]; +}; + +export const AdminSpaceInner = ({ + selectedGroup, + adminsWithNames, + setIsForceShowCreationKeyPopup, +}) => { + const [adminGroupSecretKey, setAdminGroupSecretKey] = useState(null); + const [isFetchingAdminGroupSecretKey, setIsFetchingAdminGroupSecretKey] = + useState(true); + const [isFetchingGroupSecretKey, setIsFetchingGroupSecretKey] = + useState(true); + const [ + adminGroupSecretKeyPublishDetails, + setAdminGroupSecretKeyPublishDetails, + ] = useState(null); + const [groupSecretKeyPublishDetails, setGroupSecretKeyPublishDetails] = + useState(null); + const [isLoadingPublishKey, setIsLoadingPublishKey] = useState(false); + const { show, setTxList, setInfoSnackCustom, setOpenSnackGlobal } = + useContext(MyContext); + + const getAdminGroupSecretKey = useCallback(async () => { + try { + if (!selectedGroup) return; + const getLatestPublish = await getPublishesFromAdminsAdminSpace( + adminsWithNames.map((admin) => admin?.name), + selectedGroup + ); + if (getLatestPublish === false) return; + let data; + + const res = await fetch( + `${getBaseApiReact()}/arbitrary/DOCUMENT_PRIVATE/${ + getLatestPublish.name + }/${getLatestPublish.identifier}?encoding=base64` + ); + 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"); + setAdminGroupSecretKey(decryptedKeyToObject); + setAdminGroupSecretKeyPublishDetails(getLatestPublish); + } catch (error) { + } finally { + setIsFetchingAdminGroupSecretKey(false); } - const adminData = await response.json(); - - const filterId = adminData.filter( - (data: any) => - data.identifier === `admins-symmetric-qchat-group-${groupId}` - ); - if (filterId?.length === 0) { - return false; + }, [adminsWithNames, selectedGroup]); + + const getGroupSecretKey = useCallback(async () => { + try { + if (!selectedGroup) return; + const getLatestPublish = await getPublishesFromAdmins( + adminsWithNames.map((admin) => admin?.name), + selectedGroup + ); + if (getLatestPublish === false) setGroupSecretKeyPublishDetails(false); + setGroupSecretKeyPublishDetails(getLatestPublish); + } catch (error) { + } finally { + setIsFetchingGroupSecretKey(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]; + }, [adminsWithNames, selectedGroup]); + + const createCommonSecretForAdmins = async () => { + try { + const fee = await getFee("ARBITRARY"); + await show({ + message: "Would you like to perform an ARBITRARY transaction?", + publishFee: fee.fee + " QORT", + }); + setIsLoadingPublishKey(true); + + window + .sendMessage("encryptAndPublishSymmetricKeyGroupChatForAdmins", { + groupId: selectedGroup, + previousData: null, + admins: adminsWithNames, + }) + .then((response) => { + if (!response?.error) { + setInfoSnackCustom({ + type: "success", + message: + "Successfully re-encrypted secret key. It may take a couple of minutes for the changes to propagate. Refresh the group in 5 mins.", + }); + setOpenSnackGlobal(true); + return; + } + setInfoSnackCustom({ + type: "error", + message: response?.error || "unable to re-encrypt secret key", + }); + setOpenSnackGlobal(true); + }) + .catch((error) => { + setInfoSnackCustom({ + type: "error", + message: error?.message || "unable to re-encrypt secret key", + }); + setOpenSnackGlobal(true); + }); + } catch (error) {} }; -export const AdminSpaceInner = ({selectedGroup, adminsWithNames}) => { - const [adminGroupSecretKey, setAdminGroupSecretKey] = useState(null) - const [isFetchingAdminGroupSecretKey, setIsFetchingAdminGroupSecretKey] = useState(true) - const [adminGroupSecretKeyPublishDetails, setAdminGroupSecretKeyPublishDetails] = useState(null) - - const [isLoadingPublishKey, setIsLoadingPublishKey] = useState(false) - const { show, setTxList, setInfoSnackCustom, - setOpenSnackGlobal } = useContext(MyContext); - - - const getAdminGroupSecretKey = useCallback(async ()=> { - try { - if(!selectedGroup) return - const getLatestPublish = await getPublishesFromAdminsAdminSpace(adminsWithNames.map((admin)=> admin?.name), selectedGroup) - if(getLatestPublish === false) return - let data; - - const res = await fetch( - `${getBaseApiReact()}/arbitrary/DOCUMENT_PRIVATE/${getLatestPublish.name}/${ - getLatestPublish.identifier - }?encoding=base64` - ); - 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"); - setAdminGroupSecretKey(decryptedKeyToObject) - setAdminGroupSecretKeyPublishDetails(getLatestPublish) - } catch (error) { - - } finally { - setIsFetchingAdminGroupSecretKey(false) - } - }, [adminsWithNames, selectedGroup]) - - const createCommonSecretForAdmins = async ()=> { - try { - const fee = await getFee('ARBITRARY') - await show({ - message: "Would you like to perform an ARBITRARY transaction?" , - publishFee: fee.fee + ' QORT' - }) - setIsLoadingPublishKey(true) - - - window.sendMessage("encryptAndPublishSymmetricKeyGroupChatForAdmins", { - groupId: selectedGroup, - previousData: null, - admins: adminsWithNames - }) - .then((response) => { - - if (!response?.error) { - setInfoSnackCustom({ - type: "success", - message: "Successfully re-encrypted secret key. It may take a couple of minutes for the changes to propagate. Refresh the group in 5 mins.", - }); - setOpenSnackGlobal(true); - return - } - setInfoSnackCustom({ - type: "error", - message: response?.error || "unable to re-encrypt secret key", - }); - setOpenSnackGlobal(true); - }) - .catch((error) => { - setInfoSnackCustom({ - type: "error", - message: error?.message || "unable to re-encrypt secret key", - }); - setOpenSnackGlobal(true); - }); - - } catch (error) { - - } - } - useEffect(() => { - getAdminGroupSecretKey() - }, [getAdminGroupSecretKey]); + useEffect(() => { + getAdminGroupSecretKey(); + getGroupSecretKey(); + }, [getAdminGroupSecretKey, getGroupSecretKey]); return ( - + + Reminder: After publishing the key, it will take a couple of minutes for it to appear. Please just wait. - - {isFetchingAdminGroupSecretKey && Fetching Admins secret keys} - {!isFetchingAdminGroupSecretKey && !adminGroupSecretKey && No secret key published yet} - {adminGroupSecretKeyPublishDetails && ( - Last encryption date: {formatTimestampForum(adminGroupSecretKeyPublishDetails?.updated || adminGroupSecretKeyPublishDetails?.created)} + + {isFetchingGroupSecretKey && ( + Fetching Group secret key publishes )} - + {!isFetchingGroupSecretKey && + groupSecretKeyPublishDetails === false && ( + No secret key published yet + )} + {groupSecretKeyPublishDetails && ( + + Last encryption date:{" "} + {formatTimestampForum( + groupSecretKeyPublishDetails?.updated || + groupSecretKeyPublishDetails?.created + )}{" "} + {` by ${groupSecretKeyPublishDetails?.name}`} + + )} + + + This key is to encrypt GROUP related content. This is the only one used in this UI as of now. All group members will be able to see content encrypted with this key. + + + + {isFetchingAdminGroupSecretKey && ( + Fetching Admins secret key + )} + {!isFetchingAdminGroupSecretKey && !adminGroupSecretKey && ( + No secret key published yet + )} + {adminGroupSecretKeyPublishDetails && ( + + Last encryption date:{" "} + {formatTimestampForum( + adminGroupSecretKeyPublishDetails?.updated || + adminGroupSecretKeyPublishDetails?.created + )} + + )} + + + This key is to encrypt ADMIN related content. Only admins would see content encrypted with it. - ) -} + ); +}; diff --git a/src/components/Chat/ChatGroup.tsx b/src/components/Chat/ChatGroup.tsx index 87598b8..22b7ea0 100644 --- a/src/components/Chat/ChatGroup.tsx +++ b/src/components/Chat/ChatGroup.tsx @@ -63,14 +63,14 @@ export const ChatGroup = ({selectedGroup, secretKey, setSecretKey, getSecretKey, const openQManager = useCallback(()=> { setIsOpenQManager(true) }, []) - const getTimestampEnterChat = async () => { + const getTimestampEnterChat = async (selectedGroup) => { try { return new Promise((res, rej) => { window.sendMessage("getTimestampEnterChat") .then((response) => { if (!response?.error) { - if(response && selectedGroup && response[selectedGroup]){ - lastReadTimestamp.current = response[selectedGroup] + if(response && selectedGroup){ + lastReadTimestamp.current = response[selectedGroup] || undefined window.sendMessage("addTimestampEnterChat", { timestamp: Date.now(), groupId: selectedGroup @@ -98,8 +98,9 @@ export const ChatGroup = ({selectedGroup, secretKey, setSecretKey, getSecretKey, }; useEffect(()=> { - getTimestampEnterChat() - }, []) + if(!selectedGroup) return + getTimestampEnterChat(selectedGroup) + }, [selectedGroup]) @@ -217,7 +218,9 @@ export const ChatGroup = ({selectedGroup, secretKey, setSecretKey, getSecretKey, const formatted = combineUIAndExtensionMsgs .filter((rawItem) => !rawItem?.chatReference) .map((item) => { - + const additionalFields = item?.data === 'NDAwMQ==' ? { + text: "

First group key created.

" + } : {} return { ...item, id: item.signature, @@ -225,6 +228,7 @@ export const ChatGroup = ({selectedGroup, secretKey, setSecretKey, getSecretKey, repliedTo: item?.repliedTo || item?.decryptedData?.repliedTo, unread: item?.sender === myAddress ? false : !!item?.chatReference ? false : true, isNotEncrypted: !!item?.messageText, + ...additionalFields } }); setMessages((prev) => [...prev, ...formatted]); @@ -299,6 +303,9 @@ export const ChatGroup = ({selectedGroup, secretKey, setSecretKey, getSecretKey, const formatted = combineUIAndExtensionMsgs .filter((rawItem) => !rawItem?.chatReference) .map((item) => { + const additionalFields = item?.data === 'NDAwMQ==' ? { + text: "

First group key created.

" + } : {} const divide = lastReadTimestamp.current && !firstUnreadFound && item.timestamp > lastReadTimestamp.current && myAddress !== item?.sender; if(divide){ @@ -311,7 +318,8 @@ export const ChatGroup = ({selectedGroup, secretKey, setSecretKey, getSecretKey, repliedTo: item?.repliedTo || item?.decryptedData?.repliedTo, isNotEncrypted: !!item?.messageText, unread: false, - divide + divide, + ...additionalFields } }); setMessages(formatted); diff --git a/src/components/Chat/CreateCommonSecret.tsx b/src/components/Chat/CreateCommonSecret.tsx index c371c55..468b35f 100644 --- a/src/components/Chat/CreateCommonSecret.tsx +++ b/src/components/Chat/CreateCommonSecret.tsx @@ -8,7 +8,7 @@ import { decryptResource, getGroupAdmins, validateSecretKey } from '../Group/Gro import { base64ToUint8Array } from '../../qdn/encryption/group-encryption'; import { uint8ArrayToObject } from '../../backgroundFunctions/encryption'; -export const CreateCommonSecret = ({groupId, secretKey, isOwner, myAddress, secretKeyDetails, userInfo, noSecretKey, setHideCommonKeyPopup}) => { +export const CreateCommonSecret = ({groupId, secretKey, isOwner, myAddress, secretKeyDetails, userInfo, noSecretKey, setHideCommonKeyPopup, setIsForceShowCreationKeyPopup}) => { const { show, setTxList } = useContext(MyContext); const [openSnack, setOpenSnack] = React.useState(false); @@ -131,6 +131,9 @@ export const CreateCommonSecret = ({groupId, secretKey, isOwner, myAddress, sec ]); } setIsLoading(false); + setTimeout(() => { + setIsForceShowCreationKeyPopup(false) + }, 1000); }) .catch((error) => { console.error("Failed to encrypt and publish symmetric key for group chat:", error.message || "An error occurred"); @@ -173,6 +176,7 @@ export const CreateCommonSecret = ({groupId, secretKey, isOwner, myAddress, sec }}>
diff --git a/src/components/Group/Group.tsx b/src/components/Group/Group.tsx index 800a6d9..b4a3de3 100644 --- a/src/components/Group/Group.tsx +++ b/src/components/Group/Group.tsx @@ -492,7 +492,7 @@ export const Group = ({ const [appsMode, setAppsMode] = useState('home') const [isOpenSideViewDirects, setIsOpenSideViewDirects] = useState(false) const [isOpenSideViewGroups, setIsOpenSideViewGroups] = useState(false) - + const [isForceShowCreationKeyPopup, setIsForceShowCreationKeyPopup] = useState(false) const [groupsProperties, setGroupsProperties] = useState({}) const isPrivate = useMemo(()=> { @@ -2535,7 +2535,10 @@ export const Group = ({ setDefaultThread={setDefaultThread} isPrivate={isPrivate} /> - + {groupSection === "adminSpace" && ( + + )} )} @@ -2548,11 +2551,11 @@ export const Group = ({ zIndex: 100, }} > - {isPrivate && admins.includes(myAddress) && + {(isPrivate && admins.includes(myAddress) && shouldReEncrypt && triedToFetchSecretKey && !firstSecretKeyInCreation && - !hideCommonKeyPopup && ( + !hideCommonKeyPopup) || isForceShowCreationKeyPopup && (