From 6c03c16be8cd2ec979315664ad6eeac49ff005e4 Mon Sep 17 00:00:00 2001 From: PhilReact Date: Tue, 29 Apr 2025 02:00:26 +0300 Subject: [PATCH] added group avatar --- src/App.tsx | 3 + src/atoms/global.ts | 4 + src/components/Chat/AdminSpace.tsx | 6 + src/components/Chat/AdminSpaceInner.tsx | 30 ++ src/components/Group/Group.tsx | 123 +++++--- .../Group/ListOfGroupPromotions.tsx | 2 +- src/components/GroupAvatar.tsx | 293 ++++++++++++++++++ 7 files changed, 412 insertions(+), 49 deletions(-) create mode 100644 src/components/GroupAvatar.tsx diff --git a/src/App.tsx b/src/App.tsx index d31f4d7..dd3c814 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -100,6 +100,7 @@ import { useRecoilState, useResetRecoilState, useSetRecoilState } from 'recoil'; import { canSaveSettingToQdnAtom, enabledDevModeAtom, + groupsOwnerNamesAtom, groupsPropertiesAtom, hasSettingsChangedAtom, isDisabledEditorEnterAtom, @@ -477,6 +478,7 @@ function App() { const resetLastPaymentSeenTimestampAtom = useResetRecoilState( lastPaymentSeenTimestampAtom ); + const resetGroupsOwnerNamesAtom = useResetRecoilState(groupsOwnerNamesAtom); const resetAllRecoil = () => { resetAtomSortablePinnedAppsAtom(); @@ -489,6 +491,7 @@ function App() { resetAtomMailsAtom(); resetGroupPropertiesAtom(); resetLastPaymentSeenTimestampAtom(); + resetGroupsOwnerNamesAtom(); }; const handleSetGlobalApikey = (key) => { diff --git a/src/atoms/global.ts b/src/atoms/global.ts index 9b16fe4..2ff3c21 100644 --- a/src/atoms/global.ts +++ b/src/atoms/global.ts @@ -192,6 +192,10 @@ export const groupsPropertiesAtom = atom({ key: 'groupsPropertiesAtom', default: {}, }); +export const groupsOwnerNamesAtom = atom({ + key: 'groupsOwnerNamesAtom', + default: {}, +}); export const isOpenBlockedModalAtom = atom({ key: 'isOpenBlockedModalAtom', diff --git a/src/components/Chat/AdminSpace.tsx b/src/components/Chat/AdminSpace.tsx index 2421ef2..a57b6e4 100644 --- a/src/components/Chat/AdminSpace.tsx +++ b/src/components/Chat/AdminSpace.tsx @@ -15,6 +15,8 @@ export const AdminSpace = ({ defaultThread, setDefaultThread, setIsForceShowCreationKeyPopup, + balance, + isOwner, }) => { const { rootHeight } = useContext(MyContext); const [isMoved, setIsMoved] = useState(false); @@ -37,6 +39,7 @@ export const AdminSpace = ({ position: hide ? 'fixed' : 'relative', visibility: hide && 'hidden', width: '100%', + overflow: 'auto', }} > {!isAdmin && ( @@ -56,6 +59,9 @@ export const AdminSpace = ({ setIsForceShowCreationKeyPopup={setIsForceShowCreationKeyPopup} adminsWithNames={adminsWithNames} selectedGroup={selectedGroup} + balance={balance} + userInfo={userInfo} + isOwner={isOwner} /> )} diff --git a/src/components/Chat/AdminSpaceInner.tsx b/src/components/Chat/AdminSpaceInner.tsx index bd6af61..de68ed6 100644 --- a/src/components/Chat/AdminSpaceInner.tsx +++ b/src/components/Chat/AdminSpaceInner.tsx @@ -15,6 +15,7 @@ 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'; export const getPublishesFromAdminsAdminSpace = async ( admins: string[], @@ -53,6 +54,9 @@ export const AdminSpaceInner = ({ selectedGroup, adminsWithNames, setIsForceShowCreationKeyPopup, + balance, + userInfo, + isOwner, }) => { const [adminGroupSecretKey, setAdminGroupSecretKey] = useState(null); const [isFetchingAdminGroupSecretKey, setIsFetchingAdminGroupSecretKey] = @@ -282,6 +286,32 @@ export const AdminSpaceInner = ({ content encrypted with it. + + {isOwner && ( + + Group Avatar + + + + )} ); }; diff --git a/src/components/Group/Group.tsx b/src/components/Group/Group.tsx index 5009a01..ae7c000 100644 --- a/src/components/Group/Group.tsx +++ b/src/components/Group/Group.tsx @@ -68,6 +68,7 @@ import { AdminSpace } from '../Chat/AdminSpace'; import { useRecoilState, useSetRecoilState } from 'recoil'; import { addressInfoControllerAtom, + groupsOwnerNamesAtom, groupsPropertiesAtom, isOpenBlockedModalAtom, selectedGroupIdAtom, @@ -449,10 +450,14 @@ export const Group = ({ const [isOpenSideViewGroups, setIsOpenSideViewGroups] = useState(false); const [isForceShowCreationKeyPopup, setIsForceShowCreationKeyPopup] = useState(false); + const groupsOwnerNamesRef = useRef({}); const { t } = useTranslation(['core', 'group']); const [groupsProperties, setGroupsProperties] = useRecoilState(groupsPropertiesAtom); + const [groupsOwnerNames, setGroupsOwnerNames] = + useRecoilState(groupsOwnerNamesAtom); + const setUserInfoForLevels = useSetRecoilState(addressInfoControllerAtom); 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) => { try { const url = `${getBaseApiReact()}/groups/member/${address}`; @@ -837,6 +860,9 @@ export const Group = ({ return result; }, {}); setGroupsProperties(transformToObject); + Object.keys(transformToObject).forEach((key) => { + getOwnerNameForGroup(transformToObject[key]?.owner || '', key); + }); } catch (error) { console.log(error); } @@ -1024,6 +1050,8 @@ export const Group = ({ triedToFetchSecretKey, ]); + console.log('groupOwner?.owner', groupOwner); + const notifyAdmin = async (admin) => { try { setIsLoadingNotifyAdmin(true); @@ -1299,6 +1327,8 @@ export const Group = ({ }; }, []); + console.log('selectedGroup', selectedGroup); + const openGroupChatFromNotification = (e) => { if (isLoadingOpenSectionFromNotification.current) return; @@ -1942,45 +1972,20 @@ export const Group = ({ }} > - {groupsProperties[group?.groupId]?.isOpen === false ? ( - - {/* */} - - + {group?.groupName?.charAt(0).toUpperCase()} + ) : ( - - - + + {' '} + {group?.groupName?.charAt(0).toUpperCase() || 'G'} + )} )} - {group?.data && - groupChatTimestamps[group?.groupId] && - group?.sender !== myAddress && - group?.timestamp && - ((!timestampEnterData[group?.groupId] && - Date.now() - group?.timestamp < - timeDifferenceForNotificationChats) || - timestampEnterData[group?.groupId] < - group?.timestamp) && ( - + {group?.data && + groupChatTimestamps[group?.groupId] && + group?.sender !== myAddress && + group?.timestamp && + ((!timestampEnterData[group?.groupId] && + Date.now() - group?.timestamp < + timeDifferenceForNotificationChats) || + timestampEnterData[group?.groupId] < + group?.timestamp) && ( + + )} + {groupsProperties[group?.groupId]?.isOpen === false && ( + )} + @@ -2431,10 +2456,12 @@ export const Group = ({ } adminsWithNames={adminsWithNames} selectedGroup={selectedGroup?.groupId} + isOwner={groupOwner?.owner === myAddress} myAddress={myAddress} userInfo={userInfo} hide={groupSection !== 'adminSpace'} isAdmin={admins.includes(myAddress)} + balance={balance} /> )} diff --git a/src/components/Group/ListOfGroupPromotions.tsx b/src/components/Group/ListOfGroupPromotions.tsx index 598f828..4c0ef85 100644 --- a/src/components/Group/ListOfGroupPromotions.tsx +++ b/src/components/Group/ListOfGroupPromotions.tsx @@ -49,7 +49,7 @@ import ErrorBoundary from '../../common/ErrorBoundary'; import ExpandMoreIcon from '@mui/icons-material/ExpandMore'; import ExpandLessIcon from '@mui/icons-material/ExpandLess'; import { getFee } from '../../background'; -export const requestQueuePromos = new RequestQueueWithPromise(20); +export const requestQueuePromos = new RequestQueueWithPromise(3); export function utf8ToBase64(inputString: string): string { // Encode the string as UTF-8 diff --git a/src/components/GroupAvatar.tsx b/src/components/GroupAvatar.tsx new file mode 100644 index 0000000..4d74030 --- /dev/null +++ b/src/components/GroupAvatar.tsx @@ -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 ( + <> + + {myName?.charAt(0)} + + + + change avatar + + + + + ); + } + + if (hasAvatar) { + return ( + <> + + {myName?.charAt(0)} + + + + change avatar + + + + + ); + } + + return ( + <> + + + + set avatar + + + + + ); +}; + +const PopoverComp = ({ + avatarFile, + setAvatarFile, + id, + open, + anchorEl, + handleClose, + publishAvatar, + isLoading, + myName, +}) => { + const theme = useTheme(); + return ( + + + + (500 KB max. for GIFS){' '} + + setAvatarFile(file)}> + + + {avatarFile?.name} + + {!myName && ( + + + + A registered name is required to set an avatar + + + )} + + + + Publish avatar + + + + ); +};