From 69888c271ab33e39fbf7f94b6e472615cd3998bf Mon Sep 17 00:00:00 2001 From: PhilReact Date: Sun, 29 Dec 2024 09:44:38 +0200 Subject: [PATCH] batch of updates 4 --- src/App-styles.ts | 41 +++ src/App.tsx | 105 ++++++-- src/assets/Icons/AdminsIcon.tsx | 15 ++ src/background.ts | 20 +- src/backgroundFunctions/encryption.ts | 72 ++++++ src/components/Apps/AppInfoSnippet.tsx | 6 +- src/components/Apps/AppsCategoryDesktop.tsx | 21 +- src/components/Apps/AppsHomeDesktop.tsx | 79 +++++- src/components/Apps/AppsLibraryDesktop.tsx | 51 +++- src/components/Chat/AdminSpace.tsx | 64 +++++ src/components/Chat/AdminSpaceInner.tsx | 244 ++++++++++++++++++ src/components/Chat/ChatDirect.tsx | 15 +- src/components/Chat/ChatGroup.tsx | 11 +- src/components/Chat/CreateCommonSecret.tsx | 11 +- src/components/Desktop/DesktopHeader.tsx | 51 +++- src/components/Group/AddGroup.tsx | 14 +- src/components/Group/AddGroupList.tsx | 26 +- src/components/Group/Group.tsx | 91 ++++--- .../Group/ListOfGroupPromotions.tsx | 47 +++- src/components/Group/UserListOfInvites.tsx | 34 ++- .../PasswordField/PasswordField.tsx | 8 +- src/qortalRequests/get.ts | 12 +- 22 files changed, 907 insertions(+), 131 deletions(-) create mode 100644 src/assets/Icons/AdminsIcon.tsx create mode 100644 src/components/Chat/AdminSpace.tsx create mode 100644 src/components/Chat/AdminSpaceInner.tsx diff --git a/src/App-styles.ts b/src/App-styles.ts index c2a036f..13eba7a 100644 --- a/src/App-styles.ts +++ b/src/App-styles.ts @@ -137,6 +137,47 @@ border-radius: 5px; } `; +interface CustomButtonProps { + bgColor?: string; + color?: string; +} +export const CustomButtonAccept = styled(Box)( + ({ bgColor, color }) => ({ + boxSizing: "border-box", + padding: "15px 20px", + gap: "10px", + border: "0.5px solid rgba(255, 255, 255, 0.5)", + filter: "drop-shadow(1px 4px 10.5px rgba(0,0,0,0.3))", + borderRadius: 5, + display: "inline-flex", + justifyContent: "center", + alignItems: "center", + width: "fit-content", + transition: "all 0.2s", + minWidth: 160, + cursor: "pointer", + fontWeight: 600, + fontFamily: "Inter", + textAlign: "center", + opacity: 0.7, + // Use the passed-in props or fallback defaults + backgroundColor: bgColor || "transparent", + color: color || "white", + + "&:hover": { + opacity: 1, + backgroundColor: bgColor + ? bgColor + : "rgba(41, 41, 43, 1)", // fallback hover bg + color: color || "white", + svg: { + path: { + fill: color || "white", + }, + }, + }, + }) +); export const CustomInput = styled(TextField)({ width: "183px", // Adjust the width as needed diff --git a/src/App.tsx b/src/App.tsx index 1b80e97..94a4333 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -61,6 +61,7 @@ import { AuthenticatedContainerInnerLeft, AuthenticatedContainerInnerRight, CustomButton, + CustomButtonAccept, CustomInput, CustomLabel, TextItalic, @@ -319,6 +320,14 @@ function App() { const { isShow: isShowUnsavedChanges, onCancel: onCancelUnsavedChanges, onOk: onOkUnsavedChanges, show: showUnsavedChanges, message: messageUnsavedChanges } = useModal(); const {downloadResource} = useFetchResources() + const { + isShow: isShowInfo, + onCancel: onCancelInfo, + onOk: onOkInfo, + show: showInfo, + message: messageInfo, + } = useModal(); + const { onCancel: onCancelQortalRequest, onOk: onOkQortalRequest, @@ -355,6 +364,13 @@ function App() { const { toggleFullScreen } = useAppFullScreen(setFullScreen); const {showTutorial, openTutorialModal, shownTutorialsInitiated, setOpenTutorialModal} = useHandleTutorials() + const passwordRef = useRef(null); + useEffect(() => { + if (extState === "wallet-dropped" && passwordRef.current) { + passwordRef.current.focus(); + } + }, [extState]); + useEffect(() => { // Attach a global event listener for double-click const handleDoubleClick = () => { @@ -1618,7 +1634,12 @@ function App() { show, message, rootHeight, - downloadResource + downloadResource, + showInfo, + openSnackGlobal: openSnack, + setOpenSnackGlobal: setOpenSnack, + infoSnackCustom: infoSnack, + setInfoSnackCustom: setInfoSnack, }} > setPaymentPassword(e.target.value)} autoComplete="off" + ref={passwordRef} /> @@ -2636,11 +2658,48 @@ function App() { - - + + + )} + {isShowInfo && ( + + {"Important Info"} + + + {messageInfo.message} + + + + @@ -2892,22 +2951,26 @@ function App() { gap: "14px", }} > - onOkQortalRequestExtension("accepted")} - > - accept - - onCancelQortalRequestExtension()} - > - decline - + onOkQortalRequestExtension("accepted")} + > + accept + + onCancelQortalRequestExtension()} + > + decline + {sendPaymentError} 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.ts b/src/background.ts index f570a7c..d194e7e 100644 --- a/src/background.ts +++ b/src/background.ts @@ -6,6 +6,7 @@ import { constant, isArray } from "lodash"; import { decryptGroupEncryption, encryptAndPublishSymmetricKeyGroupChat, + encryptAndPublishSymmetricKeyGroupChatForAdmins, publishGroupEncryptedResource, publishOnQDN, uint8ArrayToObject, @@ -2549,8 +2550,7 @@ async function createGroup({ const signedBytes = Base58.encode(tx.signedBytes); const res = await processTransactionVersion2(signedBytes); - if (!res?.signature) - throw new Error("Transaction was not able to be processed"); + if (!res?.signature) throw new Error(res?.message || "Transaction was not able to be processed"); return res; } async function inviteToGroup({ groupId, qortalAddress, inviteTime }) { @@ -4289,6 +4289,22 @@ chrome?.runtime?.onMessage.addListener((request, sender, sendResponse) => { break; } + case "encryptAndPublishSymmetricKeyGroupChatForAdmins": { + const { groupId, previousData, admins } = request.payload; + + encryptAndPublishSymmetricKeyGroupChatForAdmins({ + groupId, previousData, admins + }) + .then((data) => { + sendResponse(data); + }) + .catch((error) => { + console.error(error.message); + sendResponse({ error: error.message }); + }); + return true; + break; + } case "publishGroupEncryptedResource": { const { encryptedData, identifier } = request.payload; diff --git a/src/backgroundFunctions/encryption.ts b/src/backgroundFunctions/encryption.ts index a78f479..5c1feb0 100644 --- a/src/backgroundFunctions/encryption.ts +++ b/src/backgroundFunctions/encryption.ts @@ -83,8 +83,80 @@ 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 { + console.log({groupId, previousData, admins}) + let highestKey = 0 + if(previousData){ + highestKey = Math.max(...Object.keys((previousData || {})).filter(item=> !isNaN(+item)).map(Number)); + + } + + const resKeyPair = await getKeyPair() + const parsedData = JSON.parse(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) + console.log({ data64: symmetricKeyAndNonceBase64, + publicKeys: groupmemberPublicKeys, + privateKey, + userPublicKey}) + 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}: { groupId: number, previousData: Object, diff --git a/src/components/Apps/AppInfoSnippet.tsx b/src/components/Apps/AppInfoSnippet.tsx index 01a7083..f141222 100644 --- a/src/components/Apps/AppInfoSnippet.tsx +++ b/src/components/Apps/AppInfoSnippet.tsx @@ -22,7 +22,7 @@ import { useRecoilState, useSetRecoilState } from "recoil"; import { settingsLocalLastUpdatedAtom, sortablePinnedAppsAtom } from "../../atoms/global"; import { saveToLocalStorage } from "./AppsNavBar"; -export const AppInfoSnippet = ({ app, myName, isFromCategory }) => { +export const AppInfoSnippet = ({ app, myName, isFromCategory, parentStyles = {} }) => { const isInstalled = app?.status?.status === 'READY' const [sortablePinnedApps, setSortablePinnedApps] = useRecoilState(sortablePinnedAppsAtom); @@ -30,7 +30,9 @@ export const AppInfoSnippet = ({ app, myName, isFromCategory }) => { const isSelectedAppPinned = !!sortablePinnedApps?.find((item)=> item?.name === app?.name && item?.service === app?.service) const setSettingsLocalLastUpdated = useSetRecoilState(settingsLocalLastUpdatedAtom); return ( - + { + if(category?.id === 'all') return availableQapps + return availableQapps.filter( (app) => app?.metadata?.category === category?.id ); @@ -96,7 +98,11 @@ export const AppsCategoryDesktop = ({ const handler = setTimeout(() => { setDebouncedValue(searchValue); }, 350); - + setTimeout(() => { + virtuosoRef.current.scrollToIndex({ + index: 0 + }); + }, 500); // Cleanup timeout if searchValue changes before the timeout completes return () => { clearTimeout(handler); @@ -108,7 +114,7 @@ export const AppsCategoryDesktop = ({ const searchedList = useMemo(() => { if (!debouncedValue) return categoryList; return categoryList.filter((app) => - app.name.toLowerCase().includes(debouncedValue.toLowerCase()) + app.name.toLowerCase().includes(debouncedValue.toLowerCase()) || (app?.metadata?.title && app?.metadata?.title?.toLowerCase().includes(debouncedValue.toLowerCase())) ); }, [debouncedValue, categoryList]); @@ -120,6 +126,9 @@ export const AppsCategoryDesktop = ({ app={app} myName={myName} isFromCategory={true} + parentStyles={{ + padding: '0px 10px' + }} /> ); }; @@ -193,7 +202,7 @@ export const AppsCategoryDesktop = ({ diff --git a/src/components/Apps/AppsHomeDesktop.tsx b/src/components/Apps/AppsHomeDesktop.tsx index e7346ff..d3fc20d 100644 --- a/src/components/Apps/AppsHomeDesktop.tsx +++ b/src/components/Apps/AppsHomeDesktop.tsx @@ -7,13 +7,15 @@ import { AppsContainer, AppsParent, } from "./Apps-styles"; -import { Avatar, ButtonBase } from "@mui/material"; +import { Avatar, Box, ButtonBase, Input } from "@mui/material"; import { Add } from "@mui/icons-material"; import { getBaseApiReact, isMobile } from "../../App"; import LogoSelected from "../../assets/svgs/LogoSelected.svg"; import { executeEvent } from "../../utils/events"; import { SortablePinnedApps } from "./SortablePinnedApps"; import { Spacer } from "../../common/Spacer"; +import { extractComponents } from "../Chat/MessageDisplay"; +import ArrowOutwardIcon from '@mui/icons-material/ArrowOutward'; export const AppsHomeDesktop = ({ setMode, @@ -21,6 +23,22 @@ export const AppsHomeDesktop = ({ myWebsite, availableQapps, }) => { + const [qortalUrl, setQortalUrl] = useState('') + + const openQortalUrl = ()=> { + try { + if(!qortalUrl) return + const res = extractComponents(qortalUrl); + if (res) { + const { service, name, identifier, path } = res; + executeEvent("addTab", { data: { service, name, identifier, path } }); + executeEvent("open-apps-mode", { }); + setQortalUrl('qortal://') + } + } catch (error) { + + } + } return ( <> + + + + { + setQortalUrl(e.target.value) + }} + disableUnderline + autoComplete='off' + autoCorrect='off' + placeholder="qortal://" + sx={{ + width: '100%', + color: 'white', + '& .MuiInput-input::placeholder': { + color: 'rgba(84, 84, 84, 0.70) !important', + fontSize: '20px', + fontStyle: 'normal', + fontWeight: 400, + lineHeight: '120%', // 24px + letterSpacing: '0.15px', + opacity: 1 + }, + '&:focus': { + outline: 'none', + }, + // Add any additional styles for the input here + }} + onKeyDown={(e) => { + if (e.key === 'Enter' && qortalUrl) { + console.log('hello') + openQortalUrl(); + } + }} + /> + openQortalUrl()}> + + + + { setDebouncedValue(searchValue); }, 350); - + setTimeout(() => { + virtuosoRef.current.scrollToIndex({ + index: 0 + }); + }, 500); // Cleanup timeout if searchValue changes before the timeout completes return () => { clearTimeout(handler); @@ -133,7 +137,7 @@ export const AppsLibraryDesktop = ({ const searchedList = useMemo(() => { if (!debouncedValue) return []; return availableQapps.filter((app) => - app.name.toLowerCase().includes(debouncedValue.toLowerCase()) + app.name.toLowerCase().includes(debouncedValue.toLowerCase()) || (app?.metadata?.title && app?.metadata?.title?.toLowerCase().includes(debouncedValue.toLowerCase())) ); }, [debouncedValue]); @@ -144,6 +148,9 @@ export const AppsLibraryDesktop = ({ key={`${app?.service}-${app?.name}`} app={app} myName={myName} + parentStyles={{ + padding: '0px 10px' + }} /> ); }; @@ -259,7 +266,7 @@ export const AppsLibraryDesktop = ({ + ) : searchedList?.length === 0 && debouncedValue ? ( + + No results + ) : ( <> + { + executeEvent("selectedCategory", { + data: { + id: 'all', + name: 'All' + }, + }); + }} + > + + All + + {categories?.map((category) => { return ( { + 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..1dda46b --- /dev/null +++ b/src/components/Chat/AdminSpaceInner.tsx @@ -0,0 +1,244 @@ +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(); + + 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); + } + }, [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); + } + }, [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); + + + + chrome?.runtime?.sendMessage({ action: "encryptAndPublishSymmetricKeyGroupChatForAdmins", payload: { + groupId: selectedGroup, + previousData: adminGroupSecretKey, + admins: adminsWithNames, + } }, (response) => { + console.log('response', 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; + } else { + setInfoSnackCustom({ + type: "error", + message: response?.error || "unable to re-encrypt secret key", + }); + setOpenSnackGlobal(true); + } + + }); + } catch (error) {} + }; + + 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. + + + {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/ChatDirect.tsx b/src/components/Chat/ChatDirect.tsx index 1df0c15..660ae52 100644 --- a/src/components/Chat/ChatDirect.tsx +++ b/src/components/Chat/ChatDirect.tsx @@ -572,7 +572,7 @@ const sendMessage = async ()=> { minHeight: isMobile ? '0px' : '150px', maxHeight: isMobile ? 'auto' : '400px', display: 'flex', - flexDirection: 'column', + flexDirection: 'row', overflow: 'hidden', width: '100%', boxSizing: 'border-box', @@ -588,14 +588,17 @@ const sendMessage = async ()=> { flexDirection: 'column', flexGrow: isMobile && 1, overflow: !isMobile && "auto", - flexShrink: 0 + flexShrink: 0, + width: 'calc(100% - 100px)', + justifyContent: 'flex-end' }}> {replyMessage && ( @@ -651,7 +654,7 @@ const sendMessage = async ()=> { { cursor: isSending ? 'default' : 'pointer', background: isSending && 'rgba(0, 0, 0, 0.8)', flexShrink: 0, - padding: isMobile && '5px' + padding: isMobile && '5px', + width: '100px', + minWidth: 'auto' }} > {isSending && ( diff --git a/src/components/Chat/ChatGroup.tsx b/src/components/Chat/ChatGroup.tsx index 98b73e5..a0ccd72 100644 --- a/src/components/Chat/ChatGroup.tsx +++ b/src/components/Chat/ChatGroup.tsx @@ -58,7 +58,7 @@ export const ChatGroup = ({selectedGroup, secretKey, setSecretKey, getSecretKey, - const getTimestampEnterChat = async () => { + const getTimestampEnterChat = async (selectedGroup) => { try { return new Promise((res, rej) => { chrome?.runtime?.sendMessage( @@ -66,8 +66,8 @@ export const ChatGroup = ({selectedGroup, secretKey, setSecretKey, getSecretKey, action: "getTimestampEnterChat", }, (response) => { - if (!response?.error && selectedGroup && response[selectedGroup]) { - lastReadTimestamp.current = response[selectedGroup] + if(response && selectedGroup){ + lastReadTimestamp.current = response[selectedGroup] || undefined chrome?.runtime?.sendMessage({ action: "addTimestampEnterChat", payload: { @@ -89,8 +89,9 @@ export const ChatGroup = ({selectedGroup, secretKey, setSecretKey, getSecretKey, }; useEffect(()=> { - getTimestampEnterChat() - }, []) + if(!selectedGroup) return + getTimestampEnterChat(selectedGroup) + }, [selectedGroup]) const openQManager = useCallback(()=> { setIsOpenQManager(true) diff --git a/src/components/Chat/CreateCommonSecret.tsx b/src/components/Chat/CreateCommonSecret.tsx index e691464..387803d 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, isForceShowCreationKeyPopup}) => { const { show, setTxList } = useContext(MyContext); const [openSnack, setOpenSnack] = React.useState(false); @@ -128,6 +128,9 @@ export const CreateCommonSecret = ({groupId, secretKey, isOwner, myAddress, sec }, ...prev]) } setIsLoading(false) + setTimeout(() => { + setIsForceShowCreationKeyPopup(false) + }, 1000); }); } catch (error) { @@ -144,7 +147,7 @@ export const CreateCommonSecret = ({groupId, secretKey, isOwner, myAddress, sec maxWidth: '350px', background: '#444444' }}> - Re-encyrpt key + Re-encrypt key {noSecretKey ? ( There is no group secret key. Be the first admin to publish one! @@ -153,7 +156,7 @@ export const CreateCommonSecret = ({groupId, secretKey, isOwner, myAddress, sec The latest group secret key was published by a non-owner. As the owner of the group please re-encrypt the key as a safeguard - ): ( + ): isForceShowCreationKeyPopup ? null : ( The group member list has changed. Please re-encrypt the secret key. @@ -165,6 +168,8 @@ export const CreateCommonSecret = ({groupId, secretKey, isOwner, myAddress, sec }}> diff --git a/src/components/Desktop/DesktopHeader.tsx b/src/components/Desktop/DesktopHeader.tsx index 85e1094..788b236 100644 --- a/src/components/Desktop/DesktopHeader.tsx +++ b/src/components/Desktop/DesktopHeader.tsx @@ -18,8 +18,11 @@ 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 LockIcon from '@mui/icons-material/Lock'; +import NoEncryptionGmailerrorredIcon from '@mui/icons-material/NoEncryptionGmailerrorred'; +import { AdminsIcon } from "../../assets/Icons/AdminsIcon"; -const IconWrapper = ({ children, label, color, selected, selectColor }) => { +const IconWrapper = ({ children, label, color, selected, selectColor, customHeight }) => { return ( { alignItems: "center", gap: "5px", flexDirection: "column", - height: "65px", - width: "65px", + height: customHeight ? customHeight : "65px", + width: customHeight ? customHeight : "65px", borderRadius: "50%", backgroundColor: selected ? selectColor || "rgba(28, 29, 32, 1)" : "transparent", }} @@ -79,7 +82,8 @@ export const DesktopHeader = ({ hasUnreadChat, isChat, isForum, - setGroupSection + setGroupSection, + isPrivate }) => { const [value, setValue] = React.useState(0); return ( @@ -94,7 +98,20 @@ export const DesktopHeader = ({ padding: "10px", }} > - + + {isPrivate && ( + + )} + {isPrivate === false && ( + + )}
+ { + setGroupSection("adminSpace"); + + }} + > + + + + ); diff --git a/src/components/Group/AddGroup.tsx b/src/components/Group/AddGroup.tsx index 5b89238..ce9260e 100644 --- a/src/components/Group/AddGroup.tsx +++ b/src/components/Group/AddGroup.tsx @@ -194,7 +194,7 @@ export const AddGroup = ({ address, open, setOpen }) => { - Add Group + Group Mgmt { flexGrow: 1, overflowY: "auto", color: "white", + flexDirection: 'column', + display: 'flex' }} > @@ -454,7 +456,10 @@ export const AddGroup = ({ address, open, setOpen }) => { {value === 1 && ( @@ -465,7 +470,10 @@ export const AddGroup = ({ address, open, setOpen }) => { {value === 2 && ( diff --git a/src/components/Group/AddGroupList.tsx b/src/components/Group/AddGroupList.tsx index 4f459e0..18dd67f 100644 --- a/src/components/Group/AddGroupList.tsx +++ b/src/components/Group/AddGroupList.tsx @@ -26,6 +26,9 @@ import _ from "lodash"; import { MyContext, getBaseApiReact } from "../../App"; import { LoadingButton } from "@mui/lab"; import { getBaseApi, getFee } from "../../background"; +import LockIcon from '@mui/icons-material/Lock'; +import NoEncryptionGmailerrorredIcon from '@mui/icons-material/NoEncryptionGmailerrorred'; +import { Spacer } from "../../common/Spacer"; const cache = new CellMeasurerCache({ fixedWidth: true, @@ -220,7 +223,17 @@ export const AddGroupList = ({ setInfoSnack, setOpenSnack }) => { handlePopoverOpen(event, index)} > - + {group?.isOpen === false && ( + + )} + {group?.isOpen === true && ( + + )} + { }; return ( -
+

Groups list

{
@@ -267,6 +283,6 @@ export const AddGroupList = ({ setInfoSnack, setOpenSnack }) => { )}
-
+
); }; diff --git a/src/components/Group/Group.tsx b/src/components/Group/Group.tsx index 595f2c8..e3dc8f4 100644 --- a/src/components/Group/Group.tsx +++ b/src/components/Group/Group.tsx @@ -97,6 +97,7 @@ import NoEncryptionGmailerrorredIcon from '@mui/icons-material/NoEncryptionGmail import { useSetRecoilState } from "recoil"; import { selectedGroupIdAtom } from "../../atoms/global"; import { sortArrayByTimestampAndGroupName } from "../../utils/time"; +import { AdminSpace } from "../Chat/AdminSpace"; // let touchStartY = 0; // let disablePullToRefresh = false; @@ -153,6 +154,37 @@ export const getGroupAdminsAddress = async (groupNumber: number) => { } }; +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]; +}; + export function validateSecretKey(obj) { // Check if the input is an object if (typeof obj !== "object" || obj === null) { @@ -676,9 +708,8 @@ export const Group = ({ if ( group?.data && - isExtMsg(group?.data) && group?.sender !== myAddress && - group?.timestamp && (!isUpdateMsg(group?.data) || groupChatTimestamps[group?.groupId]) && + group?.timestamp && groupChatTimestamps[group?.groupId] && ((!timestampEnterData[group?.groupId] && Date.now() - group?.timestamp < timeDifferenceForNotificationChats) || timestampEnterData[group?.groupId] < group?.timestamp) @@ -712,36 +743,7 @@ export const Group = ({ // }; // }, [checkGroupListFunc, myAddress]); - const getPublishesFromAdmins = async (admins: string[]) => { - // const validApi = await findUsableApi(); - const queryString = admins.map((name) => `name=${name}`).join("&"); - const url = `${getBaseApiReact()}${getArbitraryEndpointReact()}?mode=ALL&service=DOCUMENT_PRIVATE&identifier=symmetric-qchat-group-${ - selectedGroup?.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-${selectedGroup?.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]; - }; + const getSecretKey = async ( loadingGroupParam?: boolean, secretKeyToPublish?: boolean @@ -768,7 +770,7 @@ export const Group = ({ secretKeyToPublish && secretKey && lastFetchedSecretKey.current && - Date.now() - lastFetchedSecretKey.current < 1800000 + Date.now() - lastFetchedSecretKey.current < 600000 ) return secretKey; if (loadingGroupParam) { @@ -790,7 +792,7 @@ export const Group = ({ throw new Error("Network error"); } const publish = - publishFromStorage || (await getPublishesFromAdmins(names)); + publishFromStorage || (await getPublishesFromAdmins(names, selectedGroup?.groupId)); if (prevGroupId !== selectedGroupRef.current.groupId) { if (settimeoutForRefetchSecretKey.current) { @@ -944,16 +946,14 @@ export const Group = ({ } } - const getLatestRegularChat = async (groups)=> { try { const groupData = {} const getGroupData = groups.map(async(group)=> { - const isUpdate = isUpdateMsg(group?.data) if(!group.groupId || !group?.timestamp) return null - if(isUpdate && (!groupData[group.groupId] || groupData[group.groupId] < group.timestamp)){ + 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 @@ -969,7 +969,6 @@ export const Group = ({ } } - useEffect(() => { @@ -1154,9 +1153,9 @@ export const Group = ({ .filter((group) => group?.sender !== myAddress) .find((gr) => gr?.groupId === selectedGroup?.groupId); if (!findGroup) return false; - if (!findGroup?.data || !isExtMsg(findGroup?.data)) return false; + if (!findGroup?.data) return false; return ( - findGroup?.timestamp && (!isUpdateMsg(findGroup?.data) || groupChatTimestamps[findGroup?.groupId]) && + findGroup?.timestamp && groupChatTimestamps[findGroup?.groupId] && ((!timestampEnterData[selectedGroup?.groupId] && Date.now() - findGroup?.timestamp < timeDifferenceForNotificationChats) || @@ -2233,7 +2232,7 @@ export const Group = ({ /> )} {group?.data && - isExtMsg(group?.data) && (!isUpdateMsg(group?.data) || groupChatTimestamps[group?.groupId]) && + groupChatTimestamps[group?.groupId] && group?.sender !== myAddress && group?.timestamp && ((!timestampEnterData[group?.groupId] && @@ -2272,7 +2271,7 @@ export const Group = ({ color: "white", }} /> - Add Group + Group Mgmt )} {chatMode === "directs" && ( @@ -2596,7 +2595,7 @@ export const Group = ({ style={{ display: "flex", width: "100%", - height: "100$", + height: "100%", flexDirection: "column", alignItems: "flex-start", padding: "20px", @@ -2697,6 +2696,10 @@ export const Group = ({ defaultThread={defaultThread} setDefaultThread={setDefaultThread} /> + {groupSection === "adminSpace" && ( + + )} )} @@ -2727,6 +2730,8 @@ export const Group = ({ !secretKey && triedToFetchSecretKey } + isForceShowCreationKeyPopup={isForceShowCreationKeyPopup} + setIsForceShowCreationKeyPopup={setIsForceShowCreationKeyPopup} /> )}
diff --git a/src/components/Group/ListOfGroupPromotions.tsx b/src/components/Group/ListOfGroupPromotions.tsx index 892bb8a..9674730 100644 --- a/src/components/Group/ListOfGroupPromotions.tsx +++ b/src/components/Group/ListOfGroupPromotions.tsx @@ -33,6 +33,8 @@ import { import { getNameInfo } from "./Group"; import { getBaseApi, getFee } from "../../background"; import { LoadingButton } from "@mui/lab"; +import LockIcon from '@mui/icons-material/Lock'; +import NoEncryptionGmailerrorredIcon from '@mui/icons-material/NoEncryptionGmailerrorred'; import { MyContext, getArbitraryEndpointReact, @@ -206,23 +208,25 @@ export const ListOfGroupPromotions = () => { const data = utf8ToBase64(text); const identifier = `group-promotions-ui24-group-${selectedGroup}-${uid.rnd()}`; + await new Promise((res, rej) => { - window - .sendMessage("publishOnQDN", { - data: data, + chrome?.runtime?.sendMessage( + { + action: "publishOnQDN", + payload: { + data: data, identifier: identifier, service: "DOCUMENT", - }) - .then((response) => { + }, + }, + (response) => { if (!response?.error) { res(response); return; } rej(response.error); - }) - .catch((error) => { - rej(error.message || "An error occurred"); - }); + } + ); }); setInfoSnack({ type: "success", @@ -482,6 +486,31 @@ export const ListOfGroupPromotions = () => { {promotion?.groupName} + + + {promotion?.isOpen === false && ( + + )} + {promotion?.isOpen === true && ( + + )} + + {promotion?.isOpen ? 'Public group' : 'Private group' } + + { handlePopoverOpen(event, index)}> - + {invite?.isOpen === false && ( + + )} + {invite?.isOpen === true && ( + + )} + @@ -184,9 +196,21 @@ export const UserListOfInvites = ({myAddress, setInfoSnack, setOpenSnack}) => { }; return ( -
+

Invite list

-
+
{({ height, width }) => ( { )}
-
+
); } diff --git a/src/components/PasswordField/PasswordField.tsx b/src/components/PasswordField/PasswordField.tsx index 12002d6..18f9114 100644 --- a/src/components/PasswordField/PasswordField.tsx +++ b/src/components/PasswordField/PasswordField.tsx @@ -1,5 +1,5 @@ import { Button, InputAdornment, TextField, TextFieldProps, styled } from "@mui/material"; -import { useState } from 'react' +import { forwardRef, useState } from 'react' export const CustomInput = styled(TextField)({ width: "183px", // Adjust the width as needed @@ -41,7 +41,7 @@ export const CustomInput = styled(TextField)({ }); -export const PasswordField: React.FunctionComponent = ({ ...props }) => { +export const PasswordField = forwardRef( ({ ...props }, ref) => { const [canViewPassword, setCanViewPassword] = useState(false); return ( = ({ ...prop ) }} + inputRef={ref} + {...props} /> ) -} \ No newline at end of file +}); \ No newline at end of file diff --git a/src/qortalRequests/get.ts b/src/qortalRequests/get.ts index c824cfe..b361ee2 100644 --- a/src/qortalRequests/get.ts +++ b/src/qortalRequests/get.ts @@ -255,7 +255,6 @@ const getPublishesFromAdminsAdminSpace = async ( return true; }); await Promise.all(getMemNames); -console.log('members', members) return { names: members, addresses: membersAddresses, both }; }; @@ -1778,7 +1777,8 @@ export const getUserWalletFunc = async (coin) => { const wallet = await getSaveWallet(); const address = wallet.address0; const resKeyPair = await getKeyPair(); - const parsedData = resKeyPair; + const parsedData = JSON.parse(resKeyPair); + switch (coin) { case "QORT": userWallet["address"] = address; @@ -3179,7 +3179,8 @@ export const signTransaction = async (data, isFromExtension) => { body: data.unsignedBytes, }); const resKeyPair = await getKeyPair(); - const parsedData = resKeyPair; + const parsedData = JSON.parse(resKeyPair); + const uint8PrivateKey = Base58.decode(parsedData.privateKey); const uint8PublicKey = Base58.decode(parsedData.publicKey); const keyPair = { @@ -3213,7 +3214,6 @@ export const signTransaction = async (data, isFromExtension) => { export const decryptQortalGroupData = async (data, sender) => { - console.log('data', data) let data64 = data.data64; let groupId = data?.groupId let isAdmins = data?.isAdmins @@ -3291,7 +3291,6 @@ url } -console.log('secretKeyObject', secretKeyObject) const resGroupDecryptResource = decryptSingle({ data64, secretKeyObject: secretKeyObject, skipDecodeBase64: true @@ -3320,7 +3319,8 @@ export const encryptDataWithSharingKey = async (data, sender) => { const dataObjectBase64 = await objectToBase64(dataObject) const resKeyPair = await getKeyPair(); - const parsedData = resKeyPair; + const parsedData = JSON.parse(resKeyPair); + const privateKey = parsedData.privateKey; const userPublicKey = parsedData.publicKey;