added group avatar

This commit is contained in:
PhilReact 2025-04-29 02:00:26 +03:00
parent ed7b36791a
commit 6c03c16be8
7 changed files with 412 additions and 49 deletions

View File

@ -100,6 +100,7 @@ import { useRecoilState, useResetRecoilState, useSetRecoilState } from 'recoil';
import { import {
canSaveSettingToQdnAtom, canSaveSettingToQdnAtom,
enabledDevModeAtom, enabledDevModeAtom,
groupsOwnerNamesAtom,
groupsPropertiesAtom, groupsPropertiesAtom,
hasSettingsChangedAtom, hasSettingsChangedAtom,
isDisabledEditorEnterAtom, isDisabledEditorEnterAtom,
@ -477,6 +478,7 @@ function App() {
const resetLastPaymentSeenTimestampAtom = useResetRecoilState( const resetLastPaymentSeenTimestampAtom = useResetRecoilState(
lastPaymentSeenTimestampAtom lastPaymentSeenTimestampAtom
); );
const resetGroupsOwnerNamesAtom = useResetRecoilState(groupsOwnerNamesAtom);
const resetAllRecoil = () => { const resetAllRecoil = () => {
resetAtomSortablePinnedAppsAtom(); resetAtomSortablePinnedAppsAtom();
@ -489,6 +491,7 @@ function App() {
resetAtomMailsAtom(); resetAtomMailsAtom();
resetGroupPropertiesAtom(); resetGroupPropertiesAtom();
resetLastPaymentSeenTimestampAtom(); resetLastPaymentSeenTimestampAtom();
resetGroupsOwnerNamesAtom();
}; };
const handleSetGlobalApikey = (key) => { const handleSetGlobalApikey = (key) => {

View File

@ -192,6 +192,10 @@ export const groupsPropertiesAtom = atom({
key: 'groupsPropertiesAtom', key: 'groupsPropertiesAtom',
default: {}, default: {},
}); });
export const groupsOwnerNamesAtom = atom({
key: 'groupsOwnerNamesAtom',
default: {},
});
export const isOpenBlockedModalAtom = atom({ export const isOpenBlockedModalAtom = atom({
key: 'isOpenBlockedModalAtom', key: 'isOpenBlockedModalAtom',

View File

@ -15,6 +15,8 @@ export const AdminSpace = ({
defaultThread, defaultThread,
setDefaultThread, setDefaultThread,
setIsForceShowCreationKeyPopup, setIsForceShowCreationKeyPopup,
balance,
isOwner,
}) => { }) => {
const { rootHeight } = useContext(MyContext); const { rootHeight } = useContext(MyContext);
const [isMoved, setIsMoved] = useState(false); const [isMoved, setIsMoved] = useState(false);
@ -37,6 +39,7 @@ export const AdminSpace = ({
position: hide ? 'fixed' : 'relative', position: hide ? 'fixed' : 'relative',
visibility: hide && 'hidden', visibility: hide && 'hidden',
width: '100%', width: '100%',
overflow: 'auto',
}} }}
> >
{!isAdmin && ( {!isAdmin && (
@ -56,6 +59,9 @@ export const AdminSpace = ({
setIsForceShowCreationKeyPopup={setIsForceShowCreationKeyPopup} setIsForceShowCreationKeyPopup={setIsForceShowCreationKeyPopup}
adminsWithNames={adminsWithNames} adminsWithNames={adminsWithNames}
selectedGroup={selectedGroup} selectedGroup={selectedGroup}
balance={balance}
userInfo={userInfo}
isOwner={isOwner}
/> />
)} )}
</div> </div>

View File

@ -15,6 +15,7 @@ import { base64ToUint8Array } from '../../qdn/encryption/group-encryption';
import { uint8ArrayToObject } from '../../backgroundFunctions/encryption'; import { uint8ArrayToObject } from '../../backgroundFunctions/encryption';
import { formatTimestampForum } from '../../utils/time'; import { formatTimestampForum } from '../../utils/time';
import { Spacer } from '../../common/Spacer'; import { Spacer } from '../../common/Spacer';
import { GroupAvatar } from '../GroupAvatar';
export const getPublishesFromAdminsAdminSpace = async ( export const getPublishesFromAdminsAdminSpace = async (
admins: string[], admins: string[],
@ -53,6 +54,9 @@ export const AdminSpaceInner = ({
selectedGroup, selectedGroup,
adminsWithNames, adminsWithNames,
setIsForceShowCreationKeyPopup, setIsForceShowCreationKeyPopup,
balance,
userInfo,
isOwner,
}) => { }) => {
const [adminGroupSecretKey, setAdminGroupSecretKey] = useState(null); const [adminGroupSecretKey, setAdminGroupSecretKey] = useState(null);
const [isFetchingAdminGroupSecretKey, setIsFetchingAdminGroupSecretKey] = const [isFetchingAdminGroupSecretKey, setIsFetchingAdminGroupSecretKey] =
@ -282,6 +286,32 @@ export const AdminSpaceInner = ({
content encrypted with it. content encrypted with it.
</Typography> </Typography>
</Box> </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>Group Avatar</Typography>
<GroupAvatar
setOpenSnack={setOpenSnackGlobal}
setInfoSnack={setInfoSnackCustom}
myName={userInfo?.name}
balance={balance}
groupId={selectedGroup}
/>
</Box>
)}
</Box> </Box>
); );
}; };

View File

@ -68,6 +68,7 @@ import { AdminSpace } from '../Chat/AdminSpace';
import { useRecoilState, useSetRecoilState } from 'recoil'; import { useRecoilState, useSetRecoilState } from 'recoil';
import { import {
addressInfoControllerAtom, addressInfoControllerAtom,
groupsOwnerNamesAtom,
groupsPropertiesAtom, groupsPropertiesAtom,
isOpenBlockedModalAtom, isOpenBlockedModalAtom,
selectedGroupIdAtom, selectedGroupIdAtom,
@ -449,10 +450,14 @@ export const Group = ({
const [isOpenSideViewGroups, setIsOpenSideViewGroups] = useState(false); const [isOpenSideViewGroups, setIsOpenSideViewGroups] = useState(false);
const [isForceShowCreationKeyPopup, setIsForceShowCreationKeyPopup] = const [isForceShowCreationKeyPopup, setIsForceShowCreationKeyPopup] =
useState(false); useState(false);
const groupsOwnerNamesRef = useRef({});
const { t } = useTranslation(['core', 'group']); const { t } = useTranslation(['core', 'group']);
const [groupsProperties, setGroupsProperties] = const [groupsProperties, setGroupsProperties] =
useRecoilState(groupsPropertiesAtom); useRecoilState(groupsPropertiesAtom);
const [groupsOwnerNames, setGroupsOwnerNames] =
useRecoilState(groupsOwnerNamesAtom);
const setUserInfoForLevels = useSetRecoilState(addressInfoControllerAtom); const setUserInfoForLevels = useSetRecoilState(addressInfoControllerAtom);
const isPrivate = useMemo(() => { const isPrivate = useMemo(() => {
@ -826,6 +831,24 @@ export const Group = ({
} }
}; };
const getOwnerNameForGroup = async (owner: string, groupId: string) => {
try {
if (!owner) return;
if (groupsOwnerNamesRef.current[groupId]) return;
const name = await requestQueueMemberNames.enqueue(() => {
return getNameInfo(owner);
});
if (name) {
groupsOwnerNamesRef.current[groupId] = name;
setGroupsOwnerNames((prev) => {
return { ...prev, [groupId]: name };
});
}
} catch (error) {
console.error(error);
}
};
const getGroupsProperties = useCallback(async (address) => { const getGroupsProperties = useCallback(async (address) => {
try { try {
const url = `${getBaseApiReact()}/groups/member/${address}`; const url = `${getBaseApiReact()}/groups/member/${address}`;
@ -837,6 +860,9 @@ export const Group = ({
return result; return result;
}, {}); }, {});
setGroupsProperties(transformToObject); setGroupsProperties(transformToObject);
Object.keys(transformToObject).forEach((key) => {
getOwnerNameForGroup(transformToObject[key]?.owner || '', key);
});
} catch (error) { } catch (error) {
console.log(error); console.log(error);
} }
@ -1024,6 +1050,8 @@ export const Group = ({
triedToFetchSecretKey, triedToFetchSecretKey,
]); ]);
console.log('groupOwner?.owner', groupOwner);
const notifyAdmin = async (admin) => { const notifyAdmin = async (admin) => {
try { try {
setIsLoadingNotifyAdmin(true); setIsLoadingNotifyAdmin(true);
@ -1299,6 +1327,8 @@ export const Group = ({
}; };
}, []); }, []);
console.log('selectedGroup', selectedGroup);
const openGroupChatFromNotification = (e) => { const openGroupChatFromNotification = (e) => {
if (isLoadingOpenSectionFromNotification.current) return; if (isLoadingOpenSectionFromNotification.current) return;
@ -1942,45 +1972,20 @@ export const Group = ({
}} }}
> >
<ListItemAvatar> <ListItemAvatar>
{groupsProperties[group?.groupId]?.isOpen === false ? ( {groupsOwnerNames[group?.groupId] ? (
<Box <Avatar
sx={{ alt={group?.groupName?.charAt(0)}
alignItems: 'center', src={`${getBaseApiReact()}/arbitrary/THUMBNAIL/${
background: theme.palette.background.default, groupsOwnerNames[group?.groupId]
borderRadius: '50%', }/qortal_group_avatar_${group?.groupId}?async=true`}
display: 'flex',
height: '40px',
justifyContent: 'center',
width: '40px',
}}
> >
{/* <Avatar src={`${getBaseApiReact()}/arbitrary/THUMBNAIL/${ {group?.groupName?.charAt(0).toUpperCase()}
app?.name </Avatar>
}/qortal_avatar?async=true`} /> */}
<LockIcon
sx={{
color: theme.palette.other.positive,
}}
/>
</Box>
) : ( ) : (
<Box <Avatar alt={group?.groupName?.charAt(0)}>
sx={{ {' '}
alignItems: 'center', {group?.groupName?.charAt(0).toUpperCase() || 'G'}
background: theme.palette.background.default, </Avatar>
borderRadius: '50%',
display: 'flex',
height: '40px',
justifyContent: 'center',
width: '40px',
}}
>
<NoEncryptionGmailerrorredIcon
sx={{
color: theme.palette.other.danger,
}}
/>
</Box>
)} )}
</ListItemAvatar> </ListItemAvatar>
<ListItemText <ListItemText
@ -2020,24 +2025,44 @@ export const Group = ({
sx={{ sx={{
color: theme.palette.other.unread, color: theme.palette.other.unread,
marginRight: '5px', marginRight: '5px',
marginBottom: 'auto',
}} }}
/> />
)} )}
{group?.data && <Box
groupChatTimestamps[group?.groupId] && sx={{
group?.sender !== myAddress && display: 'flex',
group?.timestamp && flexDirection: 'column',
((!timestampEnterData[group?.groupId] && gap: '5px',
Date.now() - group?.timestamp < justifyContent: 'flex-start',
timeDifferenceForNotificationChats) || height: '100%',
timestampEnterData[group?.groupId] < marginBottom: 'auto',
group?.timestamp) && ( }}
<MarkChatUnreadIcon >
{group?.data &&
groupChatTimestamps[group?.groupId] &&
group?.sender !== myAddress &&
group?.timestamp &&
((!timestampEnterData[group?.groupId] &&
Date.now() - group?.timestamp <
timeDifferenceForNotificationChats) ||
timestampEnterData[group?.groupId] <
group?.timestamp) && (
<MarkChatUnreadIcon
sx={{
color: theme.palette.other.unread,
}}
/>
)}
{groupsProperties[group?.groupId]?.isOpen === false && (
<LockIcon
sx={{ sx={{
color: theme.palette.other.unread, color: theme.palette.other.positive,
marginBottom: 'auto',
}} }}
/> />
)} )}
</Box>
</Box> </Box>
</ContextMenu> </ContextMenu>
</ListItem> </ListItem>
@ -2431,10 +2456,12 @@ export const Group = ({
} }
adminsWithNames={adminsWithNames} adminsWithNames={adminsWithNames}
selectedGroup={selectedGroup?.groupId} selectedGroup={selectedGroup?.groupId}
isOwner={groupOwner?.owner === myAddress}
myAddress={myAddress} myAddress={myAddress}
userInfo={userInfo} userInfo={userInfo}
hide={groupSection !== 'adminSpace'} hide={groupSection !== 'adminSpace'}
isAdmin={admins.includes(myAddress)} isAdmin={admins.includes(myAddress)}
balance={balance}
/> />
)} )}
</> </>

View File

@ -49,7 +49,7 @@ import ErrorBoundary from '../../common/ErrorBoundary';
import ExpandMoreIcon from '@mui/icons-material/ExpandMore'; import ExpandMoreIcon from '@mui/icons-material/ExpandMore';
import ExpandLessIcon from '@mui/icons-material/ExpandLess'; import ExpandLessIcon from '@mui/icons-material/ExpandLess';
import { getFee } from '../../background'; import { getFee } from '../../background';
export const requestQueuePromos = new RequestQueueWithPromise(20); export const requestQueuePromos = new RequestQueueWithPromise(3);
export function utf8ToBase64(inputString: string): string { export function utf8ToBase64(inputString: string): string {
// Encode the string as UTF-8 // Encode the string as UTF-8

View File

@ -0,0 +1,293 @@
import React, { useCallback, useContext, useEffect, useState } from 'react';
import Logo2 from '../assets/svgs/Logo2.svg';
import { MyContext, getArbitraryEndpointReact, getBaseApiReact } from '../App';
import {
Avatar,
Box,
Button,
ButtonBase,
Popover,
Typography,
useTheme,
} from '@mui/material';
import { Spacer } from '../common/Spacer';
import ImageUploader from '../common/ImageUploader';
import { getFee } from '../background';
import { fileToBase64 } from '../utils/fileReading';
import { LoadingButton } from '@mui/lab';
import ErrorIcon from '@mui/icons-material/Error';
export const GroupAvatar = ({
myName,
balance,
setOpenSnack,
setInfoSnack,
groupId,
}) => {
const [hasAvatar, setHasAvatar] = useState(false);
const [avatarFile, setAvatarFile] = useState(null);
const [tempAvatar, setTempAvatar] = useState(null);
const { show } = useContext(MyContext);
const [anchorEl, setAnchorEl] = useState(null);
const [isLoading, setIsLoading] = useState(false);
// Handle child element click to open Popover
const handleChildClick = (event) => {
event.stopPropagation(); // Prevent parent onClick from firing
setAnchorEl(event.currentTarget);
};
// Handle closing the Popover
const handleClose = () => {
setAnchorEl(null);
};
// Determine if the popover is open
const open = Boolean(anchorEl);
const id = open ? 'avatar-img' : undefined;
const checkIfAvatarExists = useCallback(async (name, groupId) => {
try {
const identifier = `qortal_group_avatar_${groupId}`;
const url = `${getBaseApiReact()}${getArbitraryEndpointReact()}?mode=ALL&service=THUMBNAIL&identifier=${identifier}&limit=1&name=${name}&includemetadata=false&prefix=true`;
const response = await fetch(url, {
method: 'GET',
headers: {
'Content-Type': 'application/json',
},
});
const responseData = await response.json();
if (responseData?.length > 0) {
setHasAvatar(true);
}
} catch (error) {
console.log(error);
}
}, []);
useEffect(() => {
if (!myName || !groupId) return;
checkIfAvatarExists(myName, groupId);
}, [myName, groupId, checkIfAvatarExists]);
const publishAvatar = async () => {
try {
if (!groupId) return;
const fee = await getFee('ARBITRARY');
if (+balance < +fee.fee)
throw new Error(`Publishing an Avatar requires ${fee.fee}`);
await show({
message: 'Would you like to publish an avatar?',
publishFee: fee.fee + ' QORT',
});
setIsLoading(true);
const avatarBase64 = await fileToBase64(avatarFile);
await new Promise((res, rej) => {
window
.sendMessage('publishOnQDN', {
data: avatarBase64,
identifier: `qortal_group_avatar_${groupId}`,
service: 'THUMBNAIL',
})
.then((response) => {
if (!response?.error) {
res(response);
return;
}
rej(response.error);
})
.catch((error) => {
rej(error.message || 'An error occurred');
});
});
setAvatarFile(null);
setTempAvatar(`data:image/webp;base64,${avatarBase64}`);
handleClose();
} catch (error) {
if (error?.message) {
setOpenSnack(true);
setInfoSnack({
type: 'error',
message: error?.message,
});
}
} finally {
setIsLoading(false);
}
};
if (tempAvatar) {
return (
<>
<Avatar
sx={{
height: '138px',
width: '138px',
}}
src={tempAvatar}
alt={myName}
>
{myName?.charAt(0)}
</Avatar>
<ButtonBase onClick={handleChildClick}>
<Typography
sx={{
fontSize: '12px',
opacity: 0.5,
}}
>
change avatar
</Typography>
</ButtonBase>
<PopoverComp
myName={myName}
avatarFile={avatarFile}
setAvatarFile={setAvatarFile}
id={id}
open={open}
anchorEl={anchorEl}
handleClose={handleClose}
publishAvatar={publishAvatar}
isLoading={isLoading}
/>
</>
);
}
if (hasAvatar) {
return (
<>
<Avatar
sx={{
height: '138px',
width: '138px',
}}
src={`${getBaseApiReact()}/arbitrary/THUMBNAIL/${myName}/qortal_group_avatar_${groupId}?async=true`}
alt={myName}
>
{myName?.charAt(0)}
</Avatar>
<ButtonBase onClick={handleChildClick}>
<Typography
sx={{
fontSize: '12px',
opacity: 0.5,
}}
>
change avatar
</Typography>
</ButtonBase>
<PopoverComp
myName={myName}
avatarFile={avatarFile}
setAvatarFile={setAvatarFile}
id={id}
open={open}
anchorEl={anchorEl}
handleClose={handleClose}
publishAvatar={publishAvatar}
isLoading={isLoading}
/>
</>
);
}
return (
<>
<img src={Logo2} />
<ButtonBase onClick={handleChildClick}>
<Typography
sx={{
fontSize: '12px',
opacity: 0.5,
}}
>
set avatar
</Typography>
</ButtonBase>
<PopoverComp
myName={myName}
avatarFile={avatarFile}
setAvatarFile={setAvatarFile}
id={id}
open={open}
anchorEl={anchorEl}
handleClose={handleClose}
publishAvatar={publishAvatar}
isLoading={isLoading}
/>
</>
);
};
const PopoverComp = ({
avatarFile,
setAvatarFile,
id,
open,
anchorEl,
handleClose,
publishAvatar,
isLoading,
myName,
}) => {
const theme = useTheme();
return (
<Popover
id={id}
open={open}
anchorEl={anchorEl}
onClose={handleClose} // Close popover on click outside
anchorOrigin={{
vertical: 'bottom',
horizontal: 'center',
}}
transformOrigin={{
vertical: 'top',
horizontal: 'center',
}}
>
<Box sx={{ p: 2, display: 'flex', flexDirection: 'column', gap: 1 }}>
<Typography
sx={{
fontSize: '12px',
}}
>
(500 KB max. for GIFS){' '}
</Typography>
<ImageUploader onPick={(file) => setAvatarFile(file)}>
<Button variant="contained">Choose Image</Button>
</ImageUploader>
{avatarFile?.name}
<Spacer height="25px" />
{!myName && (
<Box
sx={{
display: 'flex',
gap: '5px',
alignItems: 'center',
}}
>
<ErrorIcon
sx={{
color: theme.palette.text.primary,
}}
/>
<Typography>
A registered name is required to set an avatar
</Typography>
</Box>
)}
<Spacer height="25px" />
<LoadingButton
loading={isLoading}
disabled={!avatarFile || !myName}
onClick={publishAvatar}
variant="contained"
>
Publish avatar
</LoadingButton>
</Box>
</Popover>
);
};