added admin space and qortalrequests

This commit is contained in:
2024-12-01 09:34:53 +02:00
parent 1f900dd72b
commit a85e451d2f
11 changed files with 464 additions and 18 deletions

View File

@@ -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 (
<div
style={{
// reference to change height
height: isMobile ? `calc(${rootHeight} - 127px` : "calc(100vh - 70px)",
display: "flex",
flexDirection: "column",
width: "100%",
opacity: hide ? 0 : 1,
visibility: hide && 'hidden',
position: hide ? 'fixed' : 'relative',
left: hide && '-1000px'
}}
>
{!isAdmin && <Box sx={{
width: '100%',
display: 'flex',
justifyContent: 'center',
paddingTop: '25px'
}}><Typography>Sorry, this space is only for Admins.</Typography></Box>}
{isAdmin && <AdminSpaceInner adminsWithNames={adminsWithNames} selectedGroup={selectedGroup} />}
</div>
);
};

View File

@@ -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 (
<Box sx={{
width: '100%',
display: 'flex',
flexDirection: 'column',
padding: '10px'
}}>
<Spacer height="25px" />
<Box sx={{
display: 'flex',
flexDirection: 'column',
gap: '20px',
width: '300px',
maxWidth: '90%'
}}>
{isFetchingAdminGroupSecretKey && <Typography>Fetching Admins secret keys</Typography>}
{!isFetchingAdminGroupSecretKey && !adminGroupSecretKey && <Typography>No secret key published yet</Typography>}
{adminGroupSecretKeyPublishDetails && (
<Typography>Last encryption date: {formatTimestampForum(adminGroupSecretKeyPublishDetails?.updated || adminGroupSecretKeyPublishDetails?.created)}</Typography>
)}
<Button onClick={createCommonSecretForAdmins} variant="contained">Publish admin secret key</Button>
</Box>
</Box>
)
}

View File

@@ -23,6 +23,7 @@ import { RESOURCE_TYPE_NUMBER_GROUP_CHAT_REACTIONS } from '../../constants/resou
import { isExtMsg } from '../../background'
import AppViewerContainer from '../Apps/AppViewerContainer'
import CloseIcon from "@mui/icons-material/Close";
import { throttle } from 'lodash'
const uid = new ShortUniqueId({ length: 5 });
@@ -50,7 +51,8 @@ const [messageSize, setMessageSize] = useState(0)
const [, forceUpdate] = useReducer((x) => x + 1, 0);
const lastReadTimestamp = useRef(null)
const handleUpdateRef = useRef(null);
const getTimestampEnterChat = async () => {
try {
@@ -624,21 +626,21 @@ const clearEditorContent = () => {
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(() => {
if (hide) {

View File

@@ -18,6 +18,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";
const IconWrapper = ({ children, label, color, selected, selectColor, customHeight }) => {
return (
@@ -278,6 +279,30 @@ export const DesktopHeader = ({
/>
</IconWrapper>
</ButtonBase>
<ButtonBase
onClick={() => {
setGroupSection("adminSpace");
}}
>
<IconWrapper
color={groupSection === 'adminSpace' ? 'black' : "rgba(250, 250, 250, 0.5)"}
label="Admins"
selected={groupSection === 'adminSpace'}
customHeight="55px"
selectColor="#09b6e8"
>
<AdminsIcon
height={25}
width={20}
color={
groupSection === 'adminSpace'
? "black"
: "rgba(250, 250, 250, 0.5)"
}
/>
</IconWrapper>
</ButtonBase>
</Box>
</Box>
);

View File

@@ -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;
`

View File

@@ -96,6 +96,7 @@ 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";
// let touchStartY = 0;
// let disablePullToRefresh = false;
@@ -2475,7 +2476,7 @@ export const Group = ({
handleNewEncryptionNotification={
setNewEncryptionNotification
}
hide={groupSection !== "chat" || !secretKey}
hide={groupSection !== "chat" || !secretKey || selectedDirect || newChat}
hideView={!(desktopViewMode === 'chat' && selectedGroup)}
handleSecretKeyCreationInProgress={
handleSecretKeyCreationInProgress
@@ -2588,6 +2589,8 @@ export const Group = ({
defaultThread={defaultThread}
setDefaultThread={setDefaultThread}
/>
<AdminSpace adminsWithNames={adminsWithNames} selectedGroup={selectedGroup?.groupId} myAddress={myAddress} userInfo={userInfo} hide={groupSection !== "adminSpace"} isAdmin={admins.includes(myAddress)}
/>
</>
)}