diff --git a/src/assets/Icons/AdminsIcon.tsx b/src/assets/Icons/AdminsIcon.tsx new file mode 100644 index 0000000..d2c89c6 --- /dev/null +++ b/src/assets/Icons/AdminsIcon.tsx @@ -0,0 +1,15 @@ +import React from 'react'; + +export const AdminsIcon= ({ color = 'white', height = 48, width = 50 }) => { + return ( + + + + + + + + + ); + }; + \ No newline at end of file diff --git a/src/background-cases.ts b/src/background-cases.ts index 7f336fd..5f48c92 100644 --- a/src/background-cases.ts +++ b/src/background-cases.ts @@ -54,7 +54,7 @@ import { updateThreadActivity, walletVersion, } from "./background"; -import { decryptGroupEncryption, encryptAndPublishSymmetricKeyGroupChat, publishGroupEncryptedResource, publishOnQDN } from "./backgroundFunctions/encryption"; +import { decryptGroupEncryption, encryptAndPublishSymmetricKeyGroupChat, encryptAndPublishSymmetricKeyGroupChatForAdmins, publishGroupEncryptedResource, publishOnQDN } from "./backgroundFunctions/encryption"; import { PUBLIC_NOTIFICATION_CODE_FIRST_SECRET_KEY } from "./constants/codes"; import { encryptSingle } from "./qdn/encryption/group-encryption"; import { _createPoll, _voteOnPoll } from "./qortalRequests/get"; @@ -1250,6 +1250,41 @@ export async function encryptAndPublishSymmetricKeyGroupChatCase( } } +export async function encryptAndPublishSymmetricKeyGroupChatForAdminsCase( + request, + event +) { + try { + const { groupId, previousData, admins } = request.payload; + const { data, numberOfMembers } = + await encryptAndPublishSymmetricKeyGroupChatForAdmins({ + groupId, + previousData, + admins + }); + + event.source.postMessage( + { + requestId: request.requestId, + action: "encryptAndPublishSymmetricKeyGroupChatForAdmins", + payload: data, + type: "backgroundMessageResponse", + }, + event.origin + ); + } catch (error) { + event.source.postMessage( + { + requestId: request.requestId, + action: "encryptAndPublishSymmetricKeyGroupChat", + error: error?.message, + type: "backgroundMessageResponse", + }, + event.origin + ); + } +} + export async function publishGroupEncryptedResourceCase(request, event) { try { const {encryptedData, identifier} = request.payload; diff --git a/src/background.ts b/src/background.ts index 9b266eb..6efc048 100644 --- a/src/background.ts +++ b/src/background.ts @@ -98,6 +98,7 @@ import { versionCase, createPollCase, voteOnPollCase, + encryptAndPublishSymmetricKeyGroupChatForAdminsCase, } from "./background-cases"; import { getData, removeKeysAndLogout, storeData } from "./utils/chromeStorage"; import {BackgroundFetch} from '@transistorsoft/capacitor-background-fetch'; @@ -2822,6 +2823,9 @@ function setupMessageListener() { case "encryptAndPublishSymmetricKeyGroupChat": encryptAndPublishSymmetricKeyGroupChatCase(request, event); break; + case "encryptAndPublishSymmetricKeyGroupChatForAdmins": + encryptAndPublishSymmetricKeyGroupChatForAdminsCase(request, event); + break; case "publishGroupEncryptedResource": publishGroupEncryptedResourceCase(request, event); break; diff --git a/src/backgroundFunctions/encryption.ts b/src/backgroundFunctions/encryption.ts index 3511597..8260d9d 100644 --- a/src/backgroundFunctions/encryption.ts +++ b/src/backgroundFunctions/encryption.ts @@ -85,6 +85,77 @@ const getPublicKeys = async (groupNumber: number) => { return members } + export const getPublicKeysByAddress = async (admins) => { + const validApi = await getBaseApi() + + + let members: any = []; + if (Array.isArray(admins)) { + for (const address of admins) { + if (address) { + const resAddress = await fetch(`${validApi}/addresses/${address}`); + const resData = await resAddress.json(); + const publicKey = resData.publicKey; + members.push(publicKey) + } + } + } + + return members + } + + + export const encryptAndPublishSymmetricKeyGroupChatForAdmins = async ({groupId, previousData, admins}: { + groupId: number, + previousData: Object, + }) => { + try { + + let highestKey = 0 + if(previousData){ + highestKey = Math.max(...Object.keys((previousData || {})).filter(item=> !isNaN(+item)).map(Number)); + + } + + const resKeyPair = await getKeyPair() + const parsedData = resKeyPair + const privateKey = parsedData.privateKey + const userPublicKey = parsedData.publicKey + const groupmemberPublicKeys = await getPublicKeysByAddress(admins.map((admin)=> admin.address)) + + + const symmetricKey = createSymmetricKeyAndNonce() + const nextNumber = highestKey + 1 + const objectToSave = { + ...previousData, + [nextNumber]: symmetricKey + } + + const symmetricKeyAndNonceBase64 = await objectToBase64(objectToSave) + + const encryptedData = encryptDataGroup({ + data64: symmetricKeyAndNonceBase64, + publicKeys: groupmemberPublicKeys, + privateKey, + userPublicKey + }) + if(encryptedData){ + const registeredName = await getNameInfo() + const data = await publishData({ + registeredName, file: encryptedData, service: 'DOCUMENT_PRIVATE', identifier: `admins-symmetric-qchat-group-${groupId}`, uploadType: 'file', isBase64: true, withFee: true + }) + return { + data, + numberOfMembers: groupmemberPublicKeys.length + } + + } else { + throw new Error('Cannot encrypt content') + } + } catch (error: any) { + throw new Error(error.message); + } + } export const encryptAndPublishSymmetricKeyGroupChat = async ({groupId, previousData}: { diff --git a/src/components/Chat/AdminSpace.tsx b/src/components/Chat/AdminSpace.tsx new file mode 100644 index 0000000..f340e45 --- /dev/null +++ b/src/components/Chat/AdminSpace.tsx @@ -0,0 +1,66 @@ +import React, { + useCallback, + useContext, + useEffect, + useMemo, + useRef, + useState, +} from "react"; +import { GroupMail } from "../Group/Forum/GroupMail"; +import { MyContext, isMobile } from "../../App"; +import { getRootHeight } from "../../utils/mobile/mobileUtils"; +import { Box, Typography } from "@mui/material"; +import { AdminSpaceInner } from "./AdminSpaceInner"; + + + + + + +export const AdminSpace = ({ + selectedGroup, + adminsWithNames, + userInfo, + secretKey, + getSecretKey, + isAdmin, + myAddress, + hide, + defaultThread, + setDefaultThread +}) => { + const { rootHeight } = useContext(MyContext); + const [isMoved, setIsMoved] = useState(false); + useEffect(() => { + if (hide) { + setTimeout(() => setIsMoved(true), 300); // Wait for the fade-out to complete before moving + } else { + setIsMoved(false); // Reset the position immediately when showing + } + }, [hide]); + + return ( +
+ {!isAdmin && Sorry, this space is only for Admins.} + {isAdmin && } + +
+ ); +}; diff --git a/src/components/Chat/AdminSpaceInner.tsx b/src/components/Chat/AdminSpaceInner.tsx new file mode 100644 index 0000000..1307ad1 --- /dev/null +++ b/src/components/Chat/AdminSpaceInner.tsx @@ -0,0 +1,150 @@ +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'; + + +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(); + + 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}) => { + 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]); + return ( + + + + {isFetchingAdminGroupSecretKey && Fetching Admins secret keys} + {!isFetchingAdminGroupSecretKey && !adminGroupSecretKey && No secret key published yet} + {adminGroupSecretKeyPublishDetails && ( + Last encryption date: {formatTimestampForum(adminGroupSecretKeyPublishDetails?.updated || adminGroupSecretKeyPublishDetails?.created)} + )} + + + + ) +} diff --git a/src/components/Chat/ChatGroup.tsx b/src/components/Chat/ChatGroup.tsx index dd33b75..7dadd28 100644 --- a/src/components/Chat/ChatGroup.tsx +++ b/src/components/Chat/ChatGroup.tsx @@ -27,6 +27,7 @@ import { isFocusedParentGroupAtom } from '../../atoms/global' import { useRecoilState } from 'recoil' import AppViewerContainer from '../Apps/AppViewerContainer' import CloseIcon from "@mui/icons-material/Close"; +import { throttle } from 'lodash' const uid = new ShortUniqueId({ length: 5 }); @@ -53,6 +54,7 @@ export const ChatGroup = ({selectedGroup, secretKey, setSecretKey, getSecretKey, const groupSocketTimeoutRef = useRef(null); // Group Socket Timeout reference const editorRef = useRef(null); const { queueChats, addToQueue, processWithNewMessages } = useMessageQueue(); + const handleUpdateRef = useRef(null); const lastReadTimestamp = useRef(null) @@ -476,23 +478,24 @@ export const ChatGroup = ({selectedGroup, secretKey, setSecretKey, getSecretKey, hasInitializedWebsocket.current = true }, [secretKey]) + useEffect(() => { if (!editorRef?.current) return; - const handleUpdate = () => { - const htmlContent = editorRef?.current.getHTML(); - const stringified = JSON.stringify(htmlContent); - const size = new Blob([stringified]).size; + + handleUpdateRef.current = throttle(() => { + const htmlContent = editorRef.current.getHTML(); + const size = new TextEncoder().encode(htmlContent).length; setMessageSize(size + 100); - }; + }, 1200); - // Add a listener for the editorRef?.current's content updates - editorRef?.current.on('update', handleUpdate); + const currentEditor = editorRef.current; + + currentEditor.on("update", handleUpdateRef.current); - // Cleanup the listener on unmount return () => { - editorRef?.current.off('update', handleUpdate); + currentEditor.off("update", handleUpdateRef.current); }; - }, [editorRef?.current]); + }, [editorRef, setMessageSize]); useEffect(()=> { @@ -579,6 +582,9 @@ const clearEditorContent = () => { }; + + + const sendMessage = async ()=> { try { if(isSending) return diff --git a/src/components/ContextMenu.tsx b/src/components/ContextMenu.tsx index 491e1f7..5469404 100644 --- a/src/components/ContextMenu.tsx +++ b/src/components/ContextMenu.tsx @@ -1,42 +1,39 @@ -import React, { useState, useRef, useMemo, useEffect } from 'react'; +import React, { useState, useRef, useMemo } from 'react'; import { ListItemIcon, Menu, MenuItem, Typography, styled } from '@mui/material'; import MailOutlineIcon from '@mui/icons-material/MailOutline'; import NotificationsOffIcon from '@mui/icons-material/NotificationsOff'; import { executeEvent } from '../utils/events'; const CustomStyledMenu = styled(Menu)(({ theme }) => ({ - '& .MuiPaper-root': { - backgroundColor: '#f9f9f9', - borderRadius: '12px', - padding: theme.spacing(1), - boxShadow: '0 5px 15px rgba(0, 0, 0, 0.2)', + '& .MuiPaper-root': { + backgroundColor: '#f9f9f9', + borderRadius: '12px', + padding: theme.spacing(1), + boxShadow: '0 5px 15px rgba(0, 0, 0, 0.2)', + }, + '& .MuiMenuItem-root': { + fontSize: '14px', + color: '#444', + transition: '0.3s background-color', + '&:hover': { + backgroundColor: '#f0f0f0', }, - '& .MuiMenuItem-root': { - fontSize: '14px', // Smaller font size for the menu item text - color: '#444', - transition: '0.3s background-color', - '&:hover': { - backgroundColor: '#f0f0f0', // Explicit hover state - }, - - }, - })); + }, +})); export const ContextMenu = ({ children, groupId, getUserSettings, mutedGroups }) => { const [menuPosition, setMenuPosition] = useState(null); const longPressTimeout = useRef(null); const preventClick = useRef(false); // Flag to prevent click after long-press or right-click + const touchStartPosition = useRef({ x: 0, y: 0 }); + const touchMoved = useRef(false); - const isMuted = useMemo(()=> { - return mutedGroups.includes(groupId) - }, [mutedGroups, groupId]) + const isMuted = useMemo(() => mutedGroups.includes(groupId), [mutedGroups, groupId]); // Handle right-click (context menu) for desktop const handleContextMenu = (event) => { event.preventDefault(); - event.stopPropagation(); // Prevent parent click - - // Set flag to prevent any click event after right-click + event.stopPropagation(); preventClick.current = true; setMenuPosition({ @@ -47,76 +44,81 @@ export const ContextMenu = ({ children, groupId, getUserSettings, mutedGroups }) // Handle long-press for mobile const handleTouchStart = (event) => { + touchMoved.current = false; // Reset moved state + touchStartPosition.current = { + x: event.touches[0].clientX, + y: event.touches[0].clientY, + }; + longPressTimeout.current = setTimeout(() => { - preventClick.current = true; // Prevent the next click after long-press - event.stopPropagation(); // Prevent parent click - setMenuPosition({ - mouseX: event.touches[0].clientX, - mouseY: event.touches[0].clientY, - }); + if (!touchMoved.current) { + preventClick.current = true; + event.stopPropagation(); + setMenuPosition({ + mouseX: event.touches[0].clientX, + mouseY: event.touches[0].clientY, + }); + } }, 500); // Long press duration }; + const handleTouchMove = (event) => { + const currentPosition = { + x: event.touches[0].clientX, + y: event.touches[0].clientY, + }; + + const distanceMoved = Math.sqrt( + Math.pow(currentPosition.x - touchStartPosition.current.x, 2) + + Math.pow(currentPosition.y - touchStartPosition.current.y, 2) + ); + + if (distanceMoved > 10) { + touchMoved.current = true; // Mark as moved + clearTimeout(longPressTimeout.current); // Cancel the long press + } + }; + const handleTouchEnd = (event) => { clearTimeout(longPressTimeout.current); if (preventClick.current) { event.preventDefault(); - event.stopPropagation(); // Prevent synthetic click after long-press - preventClick.current = false; // Reset the flag + event.stopPropagation(); + preventClick.current = false; } }; - - - const handleSetGroupMute = ()=> { - try { - let value = [...mutedGroups] - if(isMuted){ - value = value.filter((group)=> group !== groupId) - } else { - value.push(groupId) - } - window.sendMessage("addUserSettings", { - keyValue: { - key: 'mutedGroups', - value, - }, - }) - .then((response) => { - if (response?.error) { - console.error("Error adding user settings:", response.error); - } else { - console.log("User settings added successfully"); - } - }) - .catch((error) => { - console.error("Failed to add user settings:", error.message || "An error occurred"); - }); - - setTimeout(() => { - getUserSettings() - }, 400); - - } catch (error) { - - } - } - - - const handleClose = (e) => { e.preventDefault(); - e.stopPropagation(); + e.stopPropagation(); setMenuPosition(null); }; + const handleSetGroupMute = () => { + const value = isMuted + ? mutedGroups.filter((group) => group !== groupId) + : [...mutedGroups, groupId]; + + window + .sendMessage("addUserSettings", { + keyValue: { key: 'mutedGroups', value }, + }) + .then((response) => { + if (response?.error) console.error("Error adding user settings:", response.error); + else console.log("User settings added successfully"); + }) + .catch((error) => console.error("Failed to add user settings:", error.message)); + + setTimeout(() => getUserSettings(), 400); + }; + return (
{children} @@ -131,16 +133,14 @@ export const ContextMenu = ({ children, groupId, getUserSettings, mutedGroups }) ? { top: menuPosition.mouseY, left: menuPosition.mouseX } : undefined } - onClick={(e)=> { - e.stopPropagation(); - }} + onClick={(e) => e.stopPropagation()} > - { - handleClose(e) - executeEvent("markAsRead", { - groupId - }); - }}> + { + handleClose(e); + executeEvent("markAsRead", { groupId }); + }} + > @@ -148,18 +148,22 @@ export const ContextMenu = ({ children, groupId, getUserSettings, mutedGroups }) Mark As Read - { - - handleClose(e) - handleSetGroupMute() - - }}> + { + handleClose(e); + handleSetGroupMute(); + }} + > - + - + {isMuted ? 'Unmute ' : 'Mute '}Push Notifications @@ -167,5 +171,3 @@ export const ContextMenu = ({ children, groupId, getUserSettings, mutedGroups })
); }; - - diff --git a/src/components/Group/Forum/Mail-styles.ts b/src/components/Group/Forum/Mail-styles.ts index 5308bf0..534304d 100644 --- a/src/components/Group/Forum/Mail-styles.ts +++ b/src/components/Group/Forum/Mail-styles.ts @@ -729,7 +729,7 @@ font-size: 23px; font-style: normal; font-weight: 700; line-height: normal; -white-space: nowrap; +white-space: wrap; text-overflow: ellipsis; overflow: hidden; ` diff --git a/src/components/Group/Group.tsx b/src/components/Group/Group.tsx index cebb71d..7b8b26b 100644 --- a/src/components/Group/Group.tsx +++ b/src/components/Group/Group.tsx @@ -93,6 +93,7 @@ import { AppsNavBar } from "../Apps/AppsNavBar"; import { AppsDesktop } from "../Apps/AppsDesktop"; import { formatEmailDate } from "./QMailMessages"; import { useHandleMobileNativeBack } from "../../hooks/useHandleMobileNativeBack"; +import { AdminSpace } from "../Chat/AdminSpace"; // let touchStartY = 0; // let disablePullToRefresh = false; @@ -2308,7 +2309,7 @@ export const Group = ({ handleNewEncryptionNotification={ setNewEncryptionNotification } - hide={groupSection !== "chat" || !secretKey} + hide={groupSection !== "chat" || !secretKey || selectedDirect || newChat} handleSecretKeyCreationInProgress={ handleSecretKeyCreationInProgress @@ -2422,6 +2423,7 @@ export const Group = ({ defaultThread={defaultThread} setDefaultThread={setDefaultThread} /> + )} diff --git a/src/components/Group/GroupMenu.tsx b/src/components/Group/GroupMenu.tsx index a02dbff..23cd90d 100644 --- a/src/components/Group/GroupMenu.tsx +++ b/src/components/Group/GroupMenu.tsx @@ -15,6 +15,7 @@ import { NotificationIcon2 } from "../../assets/Icons/NotificationIcon2"; import { ChatIcon } from "../../assets/Icons/ChatIcon"; import { ThreadsIcon } from "../../assets/Icons/ThreadsIcon"; import { MembersIcon } from "../../assets/Icons/MembersIcon"; +import { AdminsIcon } from "../../assets/Icons/AdminsIcon"; export const GroupMenu = ({ setGroupSection, groupSection, setOpenManageMembers, goToAnnouncements, goToChat, hasUnreadChat, hasUnreadAnnouncements }) => { const [anchorEl, setAnchorEl] = useState(null); @@ -80,6 +81,9 @@ export const GroupMenu = ({ setGroupSection, groupSection, setOpenManageMembers, )} {groupSection === "forum" &&( <> {" Threads"} + )} + {groupSection === "adminSpace" &&( + <> {" Admins"} )} @@ -196,6 +200,25 @@ export const GroupMenu = ({ setGroupSection, groupSection, setOpenManageMembers, }, }} primary="Members" /> + { + setGroupSection("adminSpace"); + handleClose(); + }} + > + + + + + + ); diff --git a/src/qortalRequests/get.ts b/src/qortalRequests/get.ts index bc969fa..f2acbd9 100644 --- a/src/qortalRequests/get.ts +++ b/src/qortalRequests/get.ts @@ -44,6 +44,7 @@ import signTradeBotTransaction from "../transactions/signTradeBotTransaction"; import { executeEvent } from "../utils/events"; import { extractComponents } from "../components/Chat/MessageDisplay"; import { decryptResource, getGroupAdmins, getPublishesFromAdmins, validateSecretKey } from "../components/Group/Group"; +import { getPublishesFromAdminsAdminSpace } from "../components/Chat/AdminSpaceInner"; const btcFeePerByte = 0.00000100 const ltcFeePerByte = 0.00000030 @@ -382,10 +383,11 @@ export const encryptData = async (data, sender) => { } }; + export const encryptQortalGroupData = async (data, sender) => { let data64 = data.data64; let groupId = data?.groupId - + let isAdmins = data?.isAdmins if(!groupId){ throw new Error('Please provide a groupId') } @@ -395,7 +397,10 @@ export const encryptQortalGroupData = async (data, sender) => { if (!data64) { throw new Error("Please include data to encrypt"); } + + let secretKeyObject + if(!isAdmins){ if(groupSecretkeys[groupId] && groupSecretkeys[groupId].secretKeyObject && groupSecretkeys[groupId]?.timestamp && (Date.now() - groupSecretkeys[groupId]?.timestamp) < 1200000){ secretKeyObject = groupSecretkeys[groupId].secretKeyObject } @@ -428,7 +433,44 @@ url timestamp: Date.now() } } +} else { + if(groupSecretkeys[`admins-${groupId}`] && groupSecretkeys[`admins-${groupId}`].secretKeyObject && groupSecretkeys[`admins-${groupId}`]?.timestamp && (Date.now() - groupSecretkeys[`admins-${groupId}`]?.timestamp) < 1200000){ + secretKeyObject = groupSecretkeys[`admins-${groupId}`].secretKeyObject + } + + if(!secretKeyObject){ + const { names } = + await getGroupAdmins(groupId) + + const publish = + await getPublishesFromAdminsAdminSpace(names, groupId); + if(publish === false) throw new Error('No group key found.') + const url = await createEndpoint(`/arbitrary/DOCUMENT_PRIVATE/${publish.name}/${ + publish.identifier + }?encoding=base64`); + + const res = await fetch( +url + ); + const resData = await res.text(); + const decryptedKey: any = await decryptResource(resData); + + const dataint8Array = base64ToUint8Array(decryptedKey.data); + const decryptedKeyToObject = uint8ArrayToObject(dataint8Array); + + if (!validateSecretKey(decryptedKeyToObject)) + throw new Error("SecretKey is not valid"); + secretKeyObject = decryptedKeyToObject + groupSecretkeys[`admins-${groupId}`] = { + secretKeyObject, + timestamp: Date.now() + } + } + + + +} const resGroupEncryptedResource = encryptSingle({ data64, secretKeyObject: secretKeyObject, @@ -444,17 +486,17 @@ url export const decryptQortalGroupData = async (data, sender) => { let data64 = data.data64; let groupId = data?.groupId + let isAdmins = data?.isAdmins if(!groupId){ throw new Error('Please provide a groupId') } - if (data.fileId) { - data64 = await getFileFromContentScript(data.fileId); - } + if (!data64) { throw new Error("Please include data to encrypt"); } let secretKeyObject + if(!isAdmins){ if(groupSecretkeys[groupId] && groupSecretkeys[groupId].secretKeyObject && groupSecretkeys[groupId]?.timestamp && (Date.now() - groupSecretkeys[groupId]?.timestamp) < 1200000){ secretKeyObject = groupSecretkeys[groupId].secretKeyObject } @@ -485,6 +527,40 @@ url timestamp: Date.now() } } +} else { + if(groupSecretkeys[`admins-${groupId}`] && groupSecretkeys[`admins-${groupId}`].secretKeyObject && groupSecretkeys[`admins-${groupId}`]?.timestamp && (Date.now() - groupSecretkeys[`admins-${groupId}`]?.timestamp) < 1200000){ + secretKeyObject = groupSecretkeys[`admins-${groupId}`].secretKeyObject + } + if(!secretKeyObject){ + const { names } = + await getGroupAdmins(groupId) + + const publish = + await getPublishesFromAdminsAdminSpace(names, groupId); + if(publish === false) throw new Error('No group key found.') + const url = await createEndpoint(`/arbitrary/DOCUMENT_PRIVATE/${publish.name}/${ + publish.identifier + }?encoding=base64`); + + const res = await fetch( +url + ); + const resData = await res.text(); + const decryptedKey: any = await decryptResource(resData); + + const dataint8Array = base64ToUint8Array(decryptedKey.data); + const decryptedKeyToObject = uint8ArrayToObject(dataint8Array); + if (!validateSecretKey(decryptedKeyToObject)) + throw new Error("SecretKey is not valid"); + secretKeyObject = decryptedKeyToObject + groupSecretkeys[`admins-${groupId}`] = { + secretKeyObject, + timestamp: Date.now() + } + } + + +} const resGroupDecryptResource = decryptSingle({ data64, secretKeyObject: secretKeyObject, skipDecodeBase64: true