mirror of
https://github.com/Qortal/Qortal-Hub.git
synced 2025-07-23 04:36:52 +00:00
added admin space and qortalrequests
This commit is contained in:
66
src/components/Chat/AdminSpace.tsx
Normal file
66
src/components/Chat/AdminSpace.tsx
Normal 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>
|
||||
);
|
||||
};
|
150
src/components/Chat/AdminSpaceInner.tsx
Normal file
150
src/components/Chat/AdminSpaceInner.tsx
Normal 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>
|
||||
)
|
||||
}
|
@@ -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) {
|
||||
|
@@ -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>
|
||||
);
|
||||
|
@@ -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;
|
||||
`
|
||||
|
@@ -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)}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
|
||||
|
Reference in New Issue
Block a user