Qortal-Hub/src/components/Chat/AdminSpaceInner.tsx
2025-05-17 14:15:34 +02:00

377 lines
11 KiB
TypeScript

import { 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';
import { GroupAvatar } from './GroupAvatar';
import { useTranslation } from 'react-i18next';
import i18next from 'i18next';
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(i18next.t('core:message.error.network_generic'));
}
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,
balance,
userInfo,
isOwner,
}) => {
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, setInfoSnackCustom, setOpenSnackGlobal } =
useContext(MyContext);
const { t } = useTranslation(['auth', 'core', 'group']);
const getAdminGroupSecretKey = useCallback(async () => {
try {
if (!selectedGroup) return;
const getLatestPublish = await getPublishesFromAdminsAdminSpace(
adminsWithNames.map((admin) => admin?.name),
selectedGroup
);
if (getLatestPublish === false) return;
const res = await fetch(
`${getBaseApiReact()}/arbitrary/DOCUMENT_PRIVATE/${
getLatestPublish.name
}/${getLatestPublish.identifier}?encoding=base64&rebuild=true`
);
const 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(
t('auth:message.error.invalid_secret_key', {
postProcess: 'capitalizeFirst',
})
);
setAdminGroupSecretKey(decryptedKeyToObject);
setAdminGroupSecretKeyPublishDetails(getLatestPublish);
} catch (error) {
console.log(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) {
console.log(error);
} finally {
setIsFetchingGroupSecretKey(false);
}
}, [adminsWithNames, selectedGroup]);
const createCommonSecretForAdmins = async () => {
try {
const fee = await getFee('ARBITRARY');
await show({
message: t('core:message.question.perform_transaction', {
action: 'ARBITRARY',
postProcess: 'capitalizeFirst',
}),
publishFee: fee.fee + ' QORT',
});
setIsLoadingPublishKey(true);
window
.sendMessage('encryptAndPublishSymmetricKeyGroupChatForAdmins', {
groupId: selectedGroup,
previousData: adminGroupSecretKey,
admins: adminsWithNames,
})
.then((response) => {
if (!response?.error) {
setInfoSnackCustom({
type: 'success',
message: t('auth:message.success.reencrypted_secret_key', {
postProcess: 'capitalizeFirst',
}),
});
setOpenSnackGlobal(true);
return;
}
setInfoSnackCustom({
type: 'error',
message:
response?.error ||
t('auth:message.error.unable_reencrypt_secret_key', {
postProcess: 'capitalizeFirst',
}),
});
setOpenSnackGlobal(true);
})
.catch((error) => {
setInfoSnackCustom({
type: 'error',
message:
error?.message ||
t('auth:message.error.unable_reencrypt_secret_key', {
postProcess: 'capitalizeFirst',
}),
});
setOpenSnackGlobal(true);
});
} catch (error) {
console.log(error);
}
};
useEffect(() => {
getAdminGroupSecretKey();
getGroupSecretKey();
}, [getAdminGroupSecretKey, getGroupSecretKey]);
return (
<Box
sx={{
alignItems: 'center',
display: 'flex',
flexDirection: 'column',
padding: '10px',
width: '100%',
}}
>
<Typography
sx={{
fontSize: '14px',
}}
>
{t('auth:message.error.publishing_key', {
postProcess: 'capitalizeFirst',
})}
</Typography>
<Spacer height="25px" />
<Box
sx={{
border: '1px solid gray',
borderRadius: '6px',
display: 'flex',
flexDirection: 'column',
gap: '20px',
maxWidth: '90%',
padding: '10px',
width: '300px',
}}
>
{isFetchingGroupSecretKey && (
<Typography>
{t('auth:message.generic.fetching_group_secret_key', {
postProcess: 'capitalizeFirst',
})}
</Typography>
)}
{!isFetchingGroupSecretKey &&
groupSecretKeyPublishDetails === false && (
<Typography>
{t('auth:message.generic.no_secret_key_published', {
postProcess: 'capitalizeFirst',
})}
</Typography>
)}
{groupSecretKeyPublishDetails && (
<Typography>
{t('auth:message.generic.last_encryption_date', {
date: formatTimestampForum(
groupSecretKeyPublishDetails?.updated ||
groupSecretKeyPublishDetails?.created
),
name: groupSecretKeyPublishDetails?.name,
postProcess: 'capitalizeFirst',
})}
</Typography>
)}
<Button
disabled={isFetchingGroupSecretKey}
onClick={() => setIsForceShowCreationKeyPopup(true)}
variant="contained"
>
{t('auth:action.publish_group_secret_key', {
postProcess: 'capitalizeFirst',
})}
</Button>
<Spacer height="20px" />
<Typography
sx={{
fontSize: '14px',
}}
>
{t('auth:tips.key_encrypt_group', {
postProcess: 'capitalizeFirst',
})}
</Typography>
</Box>
<Spacer height="25px" />
<Box
sx={{
border: '1px solid gray',
borderRadius: '6px',
display: 'flex',
flexDirection: 'column',
gap: '20px',
maxWidth: '90%',
padding: '10px',
width: '300px',
}}
>
{isFetchingAdminGroupSecretKey && (
<Typography>
{t('auth:message.generic.fetching_admin_secret_key', {
postProcess: 'capitalizeFirst',
})}
</Typography>
)}
{!isFetchingAdminGroupSecretKey && !adminGroupSecretKey && (
<Typography>
{t('auth:message.generic.no_secret_key_published', {
postProcess: 'capitalizeFirst',
})}
</Typography>
)}
{adminGroupSecretKeyPublishDetails && (
<Typography>
{t('auth:message.generic.last_encryption_date', {
date: formatTimestampForum(
adminGroupSecretKeyPublishDetails?.updated ||
adminGroupSecretKeyPublishDetails?.created
),
postProcess: 'capitalizeFirst',
})}
</Typography>
)}
<Button
disabled={isFetchingAdminGroupSecretKey}
onClick={createCommonSecretForAdmins}
variant="contained"
>
{t('auth:action.publish_admin_secret_key', {
postProcess: 'capitalizeFirst',
})}
</Button>
<Spacer height="20px" />
<Typography
sx={{
fontSize: '14px',
}}
>
{t('auth:tips.key_encrypt_admin', {
postProcess: 'capitalizeFirst',
})}
</Typography>
</Box>
<Spacer height="25px" />
{isOwner && (
<Box
sx={{
border: '1px solid gray',
borderRadius: '6px',
display: 'flex',
flexDirection: 'column',
gap: '20px',
maxWidth: '90%',
padding: '10px',
width: '300px',
alignItems: 'center',
}}
>
<Typography>
{t('group:group.avatar', {
postProcess: 'capitalizeFirst',
})}
</Typography>
<GroupAvatar
setOpenSnack={setOpenSnackGlobal}
setInfoSnack={setInfoSnackCustom}
myName={userInfo?.name}
balance={balance}
groupId={selectedGroup}
/>
</Box>
)}
</Box>
);
};