mirror of
https://github.com/Qortal/Qortal-Hub.git
synced 2025-05-15 22:26:58 +00:00
added group avatar
This commit is contained in:
parent
ed7b36791a
commit
6c03c16be8
@ -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) => {
|
||||
|
@ -192,6 +192,10 @@ export const groupsPropertiesAtom = atom({
|
||||
key: 'groupsPropertiesAtom',
|
||||
default: {},
|
||||
});
|
||||
export const groupsOwnerNamesAtom = atom({
|
||||
key: 'groupsOwnerNamesAtom',
|
||||
default: {},
|
||||
});
|
||||
|
||||
export const isOpenBlockedModalAtom = atom({
|
||||
key: 'isOpenBlockedModalAtom',
|
||||
|
@ -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}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
@ -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.
|
||||
</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>Group Avatar</Typography>
|
||||
|
||||
<GroupAvatar
|
||||
setOpenSnack={setOpenSnackGlobal}
|
||||
setInfoSnack={setInfoSnackCustom}
|
||||
myName={userInfo?.name}
|
||||
balance={balance}
|
||||
groupId={selectedGroup}
|
||||
/>
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
@ -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 = ({
|
||||
}}
|
||||
>
|
||||
<ListItemAvatar>
|
||||
{groupsProperties[group?.groupId]?.isOpen === false ? (
|
||||
<Box
|
||||
sx={{
|
||||
alignItems: 'center',
|
||||
background: theme.palette.background.default,
|
||||
borderRadius: '50%',
|
||||
display: 'flex',
|
||||
height: '40px',
|
||||
justifyContent: 'center',
|
||||
width: '40px',
|
||||
}}
|
||||
{groupsOwnerNames[group?.groupId] ? (
|
||||
<Avatar
|
||||
alt={group?.groupName?.charAt(0)}
|
||||
src={`${getBaseApiReact()}/arbitrary/THUMBNAIL/${
|
||||
groupsOwnerNames[group?.groupId]
|
||||
}/qortal_group_avatar_${group?.groupId}?async=true`}
|
||||
>
|
||||
{/* <Avatar src={`${getBaseApiReact()}/arbitrary/THUMBNAIL/${
|
||||
app?.name
|
||||
}/qortal_avatar?async=true`} /> */}
|
||||
<LockIcon
|
||||
sx={{
|
||||
color: theme.palette.other.positive,
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
{group?.groupName?.charAt(0).toUpperCase()}
|
||||
</Avatar>
|
||||
) : (
|
||||
<Box
|
||||
sx={{
|
||||
alignItems: 'center',
|
||||
background: theme.palette.background.default,
|
||||
borderRadius: '50%',
|
||||
display: 'flex',
|
||||
height: '40px',
|
||||
justifyContent: 'center',
|
||||
width: '40px',
|
||||
}}
|
||||
>
|
||||
<NoEncryptionGmailerrorredIcon
|
||||
sx={{
|
||||
color: theme.palette.other.danger,
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
<Avatar alt={group?.groupName?.charAt(0)}>
|
||||
{' '}
|
||||
{group?.groupName?.charAt(0).toUpperCase() || 'G'}
|
||||
</Avatar>
|
||||
)}
|
||||
</ListItemAvatar>
|
||||
<ListItemText
|
||||
@ -2020,24 +2025,44 @@ export const Group = ({
|
||||
sx={{
|
||||
color: theme.palette.other.unread,
|
||||
marginRight: '5px',
|
||||
marginBottom: 'auto',
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
{group?.data &&
|
||||
groupChatTimestamps[group?.groupId] &&
|
||||
group?.sender !== myAddress &&
|
||||
group?.timestamp &&
|
||||
((!timestampEnterData[group?.groupId] &&
|
||||
Date.now() - group?.timestamp <
|
||||
timeDifferenceForNotificationChats) ||
|
||||
timestampEnterData[group?.groupId] <
|
||||
group?.timestamp) && (
|
||||
<MarkChatUnreadIcon
|
||||
<Box
|
||||
sx={{
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
gap: '5px',
|
||||
justifyContent: 'flex-start',
|
||||
height: '100%',
|
||||
marginBottom: 'auto',
|
||||
}}
|
||||
>
|
||||
{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={{
|
||||
color: theme.palette.other.unread,
|
||||
color: theme.palette.other.positive,
|
||||
marginBottom: 'auto',
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</Box>
|
||||
</Box>
|
||||
</ContextMenu>
|
||||
</ListItem>
|
||||
@ -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}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
|
@ -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
|
||||
|
293
src/components/GroupAvatar.tsx
Normal file
293
src/components/GroupAvatar.tsx
Normal 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>
|
||||
);
|
||||
};
|
Loading…
x
Reference in New Issue
Block a user