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
+
+
+
+ );
+};