mirror of
https://github.com/Qortal/Qortal-Hub.git
synced 2025-06-30 10:51:22 +00:00
Merge pull request #34 from Qortal/feature/add-group-avatar
Feature/add group avatar
This commit is contained in:
commit
2d8bf8fb97
@ -1,15 +1,15 @@
|
||||
import type { CapacitorConfig } from '@capacitor/cli';
|
||||
import type { CapacitorConfig } from "@capacitor/cli";
|
||||
|
||||
const config: CapacitorConfig = {
|
||||
appId: 'org.Qortal.Qortal-Hub',
|
||||
appName: 'Qortal-Hub',
|
||||
webDir: 'dist',
|
||||
"plugins": {
|
||||
"LocalNotifications": {
|
||||
"smallIcon": "qort",
|
||||
"iconColor": "#09b6e8"
|
||||
}
|
||||
}
|
||||
appId: "org.Qortal.Qortal-Hub",
|
||||
appName: "Qortal-Hub",
|
||||
webDir: "dist",
|
||||
plugins: {
|
||||
LocalNotifications: {
|
||||
smallIcon: "qort",
|
||||
iconColor: "#09b6e8",
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export default config;
|
||||
|
20
src/App.tsx
20
src/App.tsx
@ -100,17 +100,22 @@ import { useRecoilState, useResetRecoilState, useSetRecoilState } from 'recoil';
|
||||
import {
|
||||
canSaveSettingToQdnAtom,
|
||||
enabledDevModeAtom,
|
||||
groupAnnouncementsAtom,
|
||||
groupChatTimestampsAtom,
|
||||
groupsOwnerNamesAtom,
|
||||
groupsPropertiesAtom,
|
||||
hasSettingsChangedAtom,
|
||||
isDisabledEditorEnterAtom,
|
||||
isUsingImportExportSettingsAtom,
|
||||
lastPaymentSeenTimestampAtom,
|
||||
mailsAtom,
|
||||
mutedGroupsAtom,
|
||||
oldPinnedAppsAtom,
|
||||
qMailLastEnteredTimestampAtom,
|
||||
settingsLocalLastUpdatedAtom,
|
||||
settingsQDNLastUpdatedAtom,
|
||||
sortablePinnedAppsAtom,
|
||||
timestampEnterDataAtom,
|
||||
} from './atoms/global';
|
||||
import { NotAuthenticated } from './ExtStates/NotAuthenticated';
|
||||
import { handleGetFileFromIndexedDB } from './utils/indexedDB';
|
||||
@ -477,6 +482,16 @@ function App() {
|
||||
const resetLastPaymentSeenTimestampAtom = useResetRecoilState(
|
||||
lastPaymentSeenTimestampAtom
|
||||
);
|
||||
const resetGroupsOwnerNamesAtom = useResetRecoilState(groupsOwnerNamesAtom);
|
||||
const resetGroupAnnouncementsAtom = useResetRecoilState(
|
||||
groupAnnouncementsAtom
|
||||
);
|
||||
const resetMutedGroupsAtom = useResetRecoilState(mutedGroupsAtom);
|
||||
|
||||
const resetGroupChatTimestampsAtom = useResetRecoilState(
|
||||
groupChatTimestampsAtom
|
||||
);
|
||||
const resetTimestampEnterAtom = useResetRecoilState(timestampEnterDataAtom);
|
||||
|
||||
const resetAllRecoil = () => {
|
||||
resetAtomSortablePinnedAppsAtom();
|
||||
@ -489,6 +504,11 @@ function App() {
|
||||
resetAtomMailsAtom();
|
||||
resetGroupPropertiesAtom();
|
||||
resetLastPaymentSeenTimestampAtom();
|
||||
resetGroupsOwnerNamesAtom();
|
||||
resetGroupAnnouncementsAtom();
|
||||
resetMutedGroupsAtom();
|
||||
resetGroupChatTimestampsAtom();
|
||||
resetTimestampEnterAtom();
|
||||
};
|
||||
|
||||
const handleSetGlobalApikey = (key) => {
|
||||
|
@ -192,8 +192,82 @@ export const groupsPropertiesAtom = atom({
|
||||
key: 'groupsPropertiesAtom',
|
||||
default: {},
|
||||
});
|
||||
export const groupsOwnerNamesAtom = atom({
|
||||
key: 'groupsOwnerNamesAtom',
|
||||
default: {},
|
||||
});
|
||||
|
||||
export const isOpenBlockedModalAtom = atom({
|
||||
key: 'isOpenBlockedModalAtom',
|
||||
default: false,
|
||||
});
|
||||
|
||||
export const groupsOwnerNamesSelector = selectorFamily({
|
||||
key: 'groupsOwnerNamesSelector',
|
||||
get:
|
||||
(key) =>
|
||||
({ get }) => {
|
||||
const data = get(groupsOwnerNamesAtom);
|
||||
return data[key] || null; // Return the value for the key or null if not found
|
||||
},
|
||||
});
|
||||
|
||||
export const groupAnnouncementsAtom = atom({
|
||||
key: 'groupAnnouncementsAtom',
|
||||
default: {},
|
||||
});
|
||||
|
||||
export const groupAnnouncementSelector = selectorFamily({
|
||||
key: 'groupAnnouncementSelector',
|
||||
get:
|
||||
(key) =>
|
||||
({ get }) => {
|
||||
const data = get(groupAnnouncementsAtom);
|
||||
return data[key] || null; // Return the value for the key or null if not found
|
||||
},
|
||||
});
|
||||
|
||||
export const groupPropertySelector = selectorFamily({
|
||||
key: 'groupPropertySelector',
|
||||
get:
|
||||
(key) =>
|
||||
({ get }) => {
|
||||
const data = get(groupsPropertiesAtom);
|
||||
return data[key] || null; // Return the value for the key or null if not found
|
||||
},
|
||||
});
|
||||
|
||||
export const mutedGroupsAtom = atom({
|
||||
key: 'mutedGroupsAtom',
|
||||
default: [],
|
||||
});
|
||||
|
||||
export const groupChatTimestampsAtom = atom({
|
||||
key: 'groupChatTimestampsAtom',
|
||||
default: {},
|
||||
});
|
||||
|
||||
export const groupChatTimestampSelector = selectorFamily({
|
||||
key: 'groupChatTimestampSelector',
|
||||
get:
|
||||
(key) =>
|
||||
({ get }) => {
|
||||
const data = get(groupChatTimestampsAtom);
|
||||
return data[key] || null; // Return the value for the key or null if not found
|
||||
},
|
||||
});
|
||||
|
||||
export const timestampEnterDataAtom = atom({
|
||||
key: 'timestampEnterDataAtom',
|
||||
default: {},
|
||||
});
|
||||
|
||||
export const timestampEnterDataSelector = selectorFamily({
|
||||
key: 'timestampEnterDataSelector',
|
||||
get:
|
||||
(key) =>
|
||||
({ get }) => {
|
||||
const data = get(timestampEnterDataAtom);
|
||||
return data[key] || null; // Return the value for the key or null if not found
|
||||
},
|
||||
});
|
||||
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
@ -75,6 +75,21 @@ const getBadgeImg = (level) => {
|
||||
}
|
||||
};
|
||||
|
||||
const UserBadge = React.memo(({ userInfo }) => {
|
||||
return (
|
||||
<Tooltip disableFocusListener title={`level ${userInfo ?? 0}`}>
|
||||
<img
|
||||
style={{
|
||||
visibility: userInfo !== undefined ? 'visible' : 'hidden',
|
||||
width: '30px',
|
||||
height: 'auto',
|
||||
}}
|
||||
src={getBadgeImg(userInfo)}
|
||||
/>
|
||||
</Tooltip>
|
||||
);
|
||||
});
|
||||
|
||||
export const MessageItem = React.memo(
|
||||
({
|
||||
message,
|
||||
@ -210,16 +225,7 @@ export const MessageItem = React.memo(
|
||||
{message?.senderName?.charAt(0)}
|
||||
</Avatar>
|
||||
</WrapperUserAction>
|
||||
<Tooltip disableFocusListener title={`level ${userInfo ?? 0}`}>
|
||||
<img
|
||||
style={{
|
||||
visibility: userInfo !== undefined ? 'visible' : 'hidden',
|
||||
width: '30px',
|
||||
height: 'auto',
|
||||
}}
|
||||
src={getBadgeImg(userInfo)}
|
||||
/>
|
||||
</Tooltip>
|
||||
<UserBadge userInfo={userInfo} />
|
||||
</Box>
|
||||
)}
|
||||
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { useCallback, useEffect, useMemo, useRef } from 'react';
|
||||
import React, { useCallback, useEffect, useMemo, useRef } from 'react';
|
||||
import { EditorProvider, useCurrentEditor } from '@tiptap/react';
|
||||
import StarterKit from '@tiptap/starter-kit';
|
||||
import { Color } from '@tiptap/extension-color';
|
||||
@ -41,26 +41,27 @@ function textMatcher(doc, from) {
|
||||
return { start, query };
|
||||
}
|
||||
|
||||
const MenuBar = ({
|
||||
const MenuBar = React.memo(
|
||||
({
|
||||
setEditorRef,
|
||||
isChat,
|
||||
isDisabledEditorEnter,
|
||||
setIsDisabledEditorEnter,
|
||||
}) => {
|
||||
}) => {
|
||||
const { editor } = useCurrentEditor();
|
||||
const fileInputRef = useRef(null);
|
||||
const theme = useTheme();
|
||||
|
||||
if (!editor) {
|
||||
return null;
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (editor && setEditorRef) {
|
||||
setEditorRef(editor);
|
||||
}
|
||||
}, [editor, setEditorRef]);
|
||||
|
||||
if (!editor) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const handleImageUpload = async (file) => {
|
||||
let compressedFile;
|
||||
await new Promise<void>((resolve) => {
|
||||
@ -329,7 +330,8 @@ const MenuBar = ({
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
}
|
||||
);
|
||||
|
||||
const extensions = [
|
||||
Color.configure({ types: [TextStyle.name, ListItem.name] }),
|
||||
@ -373,10 +375,10 @@ export default ({
|
||||
? extensions.filter((item) => item?.name !== 'image')
|
||||
: extensions;
|
||||
const editorRef = useRef(null);
|
||||
const setEditorRefFunc = (editorInstance) => {
|
||||
const setEditorRefFunc = useCallback((editorInstance) => {
|
||||
editorRef.current = editorInstance;
|
||||
setEditorRef(editorInstance);
|
||||
};
|
||||
}, []);
|
||||
|
||||
// const users = [
|
||||
// { id: 1, label: 'Alice' },
|
||||
|
@ -10,6 +10,8 @@ import {
|
||||
import MailOutlineIcon from '@mui/icons-material/MailOutline';
|
||||
import NotificationsOffIcon from '@mui/icons-material/NotificationsOff';
|
||||
import { executeEvent } from '../utils/events';
|
||||
import { useRecoilState } from 'recoil';
|
||||
import { mutedGroupsAtom } from '../atoms/global';
|
||||
|
||||
const CustomStyledMenu = styled(Menu)(({ theme }) => ({
|
||||
'& .MuiPaper-root': {
|
||||
@ -28,16 +30,12 @@ const CustomStyledMenu = styled(Menu)(({ theme }) => ({
|
||||
},
|
||||
}));
|
||||
|
||||
export const ContextMenu = ({
|
||||
children,
|
||||
groupId,
|
||||
getUserSettings,
|
||||
mutedGroups,
|
||||
}) => {
|
||||
export const ContextMenu = ({ children, groupId, getUserSettings }) => {
|
||||
const [menuPosition, setMenuPosition] = useState(null);
|
||||
const longPressTimeout = useRef(null);
|
||||
const preventClick = useRef(false); // Flag to prevent click after long-press or right-click
|
||||
const theme = useTheme();
|
||||
const [mutedGroups] = useRecoilState(mutedGroupsAtom);
|
||||
const isMuted = useMemo(() => {
|
||||
return mutedGroups.includes(groupId);
|
||||
}, [mutedGroups, groupId]);
|
||||
|
@ -199,6 +199,8 @@ export const AddGroup = ({ address, open, setOpen }) => {
|
||||
};
|
||||
}, []);
|
||||
|
||||
if (!open) return null;
|
||||
|
||||
return (
|
||||
<Fragment>
|
||||
<Dialog
|
||||
|
@ -68,9 +68,14 @@ import { AdminSpace } from '../Chat/AdminSpace';
|
||||
import { useRecoilState, useSetRecoilState } from 'recoil';
|
||||
import {
|
||||
addressInfoControllerAtom,
|
||||
groupAnnouncementsAtom,
|
||||
groupChatTimestampsAtom,
|
||||
groupsOwnerNamesAtom,
|
||||
groupsPropertiesAtom,
|
||||
isOpenBlockedModalAtom,
|
||||
mutedGroupsAtom,
|
||||
selectedGroupIdAtom,
|
||||
timestampEnterDataAtom,
|
||||
} from '../../atoms/global';
|
||||
import { sortArrayByTimestampAndGroupName } from '../../utils/time';
|
||||
import PersonOffIcon from '@mui/icons-material/PersonOff';
|
||||
@ -79,6 +84,7 @@ import NoEncryptionGmailerrorredIcon from '@mui/icons-material/NoEncryptionGmail
|
||||
import { BlockedUsersModal } from './BlockedUsersModal';
|
||||
import { WalletsAppWrapper } from './WalletsAppWrapper';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { GroupList } from './GroupList';
|
||||
|
||||
export const getPublishesFromAdmins = async (admins: string[], groupId) => {
|
||||
const queryString = admins.map((name) => `name=${name}`).join('&');
|
||||
@ -116,7 +122,7 @@ interface GroupProps {
|
||||
balance: number;
|
||||
}
|
||||
|
||||
const timeDifferenceForNotificationChats = 900000;
|
||||
export const timeDifferenceForNotificationChats = 900000;
|
||||
|
||||
export const requestQueueMemberNames = new RequestQueueWithPromise(5);
|
||||
export const requestQueueAdminMemberNames = new RequestQueueWithPromise(5);
|
||||
@ -409,7 +415,9 @@ export const Group = ({
|
||||
const { setMemberGroups, rootHeight, isRunningPublicNode } =
|
||||
useContext(MyContext);
|
||||
const lastGroupNotification = useRef<null | number>(null);
|
||||
const [timestampEnterData, setTimestampEnterData] = useState({});
|
||||
const [timestampEnterData, setTimestampEnterData] = useRecoilState(
|
||||
timestampEnterDataAtom
|
||||
);
|
||||
const [chatMode, setChatMode] = useState('groups');
|
||||
const [newChat, setNewChat] = useState(false);
|
||||
const [openSnack, setOpenSnack] = React.useState(false);
|
||||
@ -420,7 +428,10 @@ export const Group = ({
|
||||
const [firstSecretKeyInCreation, setFirstSecretKeyInCreation] =
|
||||
React.useState(false);
|
||||
const [groupSection, setGroupSection] = React.useState('home');
|
||||
const [groupAnnouncements, setGroupAnnouncements] = React.useState({});
|
||||
const [groupAnnouncements, setGroupAnnouncements] = useRecoilState(
|
||||
groupAnnouncementsAtom
|
||||
);
|
||||
|
||||
const [defaultThread, setDefaultThread] = React.useState(null);
|
||||
const [isOpenDrawer, setIsOpenDrawer] = React.useState(false);
|
||||
const setIsOpenBlockedUserModal = useSetRecoilState(isOpenBlockedModalAtom);
|
||||
@ -428,7 +439,7 @@ export const Group = ({
|
||||
const [hideCommonKeyPopup, setHideCommonKeyPopup] = React.useState(false);
|
||||
const [isLoadingGroupMessage, setIsLoadingGroupMessage] = React.useState('');
|
||||
const [drawerMode, setDrawerMode] = React.useState('groups');
|
||||
const [mutedGroups, setMutedGroups] = useState([]);
|
||||
const setMutedGroups = useSetRecoilState(mutedGroupsAtom);
|
||||
const [mobileViewMode, setMobileViewMode] = useState('home');
|
||||
const [mobileViewModeKeepOpen, setMobileViewModeKeepOpen] = useState('');
|
||||
const isFocusedRef = useRef(true);
|
||||
@ -442,17 +453,23 @@ export const Group = ({
|
||||
const settimeoutForRefetchSecretKey = useRef(null);
|
||||
const { clearStatesMessageQueueProvider } = useMessageQueue();
|
||||
const initiatedGetMembers = useRef(false);
|
||||
const [groupChatTimestamps, setGroupChatTimestamps] = React.useState({});
|
||||
const [groupChatTimestamps, setGroupChatTimestamps] = useRecoilState(
|
||||
groupChatTimestampsAtom
|
||||
);
|
||||
const [appsMode, setAppsMode] = useState('home');
|
||||
const [appsModeDev, setAppsModeDev] = useState('home');
|
||||
const [isOpenSideViewDirects, setIsOpenSideViewDirects] = useState(false);
|
||||
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(() => {
|
||||
@ -495,7 +512,7 @@ export const Group = ({
|
||||
selectedDirectRef.current = selectedDirect;
|
||||
}, [selectedDirect]);
|
||||
|
||||
const getUserSettings = async () => {
|
||||
const getUserSettings = useCallback(async () => {
|
||||
try {
|
||||
return new Promise((res, rej) => {
|
||||
window
|
||||
@ -517,13 +534,13 @@ export const Group = ({
|
||||
} catch (error) {
|
||||
console.log('error', error);
|
||||
}
|
||||
};
|
||||
}, [setMutedGroups]);
|
||||
|
||||
useEffect(() => {
|
||||
getUserSettings();
|
||||
}, []);
|
||||
}, [getUserSettings]);
|
||||
|
||||
const getTimestampEnterChat = async () => {
|
||||
const getTimestampEnterChat = useCallback(async () => {
|
||||
try {
|
||||
return new Promise((res, rej) => {
|
||||
window
|
||||
@ -543,7 +560,7 @@ export const Group = ({
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
}
|
||||
};
|
||||
}, []);
|
||||
|
||||
const refreshHomeDataFunc = () => {
|
||||
setGroupSection('default');
|
||||
@ -645,41 +662,45 @@ export const Group = ({
|
||||
return hasUnread;
|
||||
}, [groupAnnouncements, groups]);
|
||||
|
||||
const getSecretKey = async (
|
||||
loadingGroupParam?: boolean,
|
||||
secretKeyToPublish?: boolean
|
||||
) => {
|
||||
const getSecretKey = useCallback(
|
||||
async (loadingGroupParam?: boolean, secretKeyToPublish?: boolean) => {
|
||||
try {
|
||||
setIsLoadingGroupMessage('Locating encryption keys');
|
||||
pauseAllQueues();
|
||||
|
||||
let dataFromStorage;
|
||||
let publishFromStorage;
|
||||
let adminsFromStorage;
|
||||
|
||||
if (
|
||||
secretKeyToPublish &&
|
||||
secretKey &&
|
||||
lastFetchedSecretKey.current &&
|
||||
Date.now() - lastFetchedSecretKey.current < 600000
|
||||
)
|
||||
) {
|
||||
return secretKey;
|
||||
}
|
||||
|
||||
if (loadingGroupParam) {
|
||||
setIsLoadingGroup(true);
|
||||
}
|
||||
|
||||
if (selectedGroup?.groupId !== selectedGroupRef.current.groupId) {
|
||||
if (settimeoutForRefetchSecretKey.current) {
|
||||
clearTimeout(settimeoutForRefetchSecretKey.current);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
const prevGroupId = selectedGroupRef.current.groupId;
|
||||
// const validApi = await findUsableApi();
|
||||
|
||||
const { names, addresses, both } =
|
||||
adminsFromStorage || (await getGroupAdmins(selectedGroup?.groupId));
|
||||
setAdmins(addresses);
|
||||
setAdminsWithNames(both);
|
||||
if (!names.length) {
|
||||
throw new Error('Network error');
|
||||
}
|
||||
|
||||
if (!names.length) throw new Error('Network error');
|
||||
|
||||
const publish =
|
||||
publishFromStorage ||
|
||||
(await getPublishesFromAdmins(names, selectedGroup?.groupId));
|
||||
@ -690,6 +711,7 @@ export const Group = ({
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (publish === false) {
|
||||
setTriedToFetchSecretKey(true);
|
||||
settimeoutForRefetchSecretKey.current = setTimeout(() => {
|
||||
@ -697,29 +719,33 @@ export const Group = ({
|
||||
}, 120000);
|
||||
return false;
|
||||
}
|
||||
|
||||
setSecretKeyPublishDate(publish?.updated || publish?.created);
|
||||
|
||||
let data;
|
||||
if (dataFromStorage) {
|
||||
data = dataFromStorage;
|
||||
} else {
|
||||
// const shouldRebuild = !secretKeyPublishDate || (publish?.update && publish?.updated > secretKeyPublishDate)
|
||||
setIsLoadingGroupMessage('Downloading encryption keys');
|
||||
const res = await fetch(
|
||||
`${getBaseApiReact()}/arbitrary/DOCUMENT_PRIVATE/${publish.name}/${
|
||||
publish.identifier
|
||||
}?encoding=base64&rebuild=true`
|
||||
`${getBaseApiReact()}/arbitrary/DOCUMENT_PRIVATE/${publish.name}/${publish.identifier}?encoding=base64&rebuild=true`
|
||||
);
|
||||
data = await res.text();
|
||||
}
|
||||
|
||||
const decryptedKey: any = await decryptResource(data);
|
||||
const dataint8Array = base64ToUint8Array(decryptedKey.data);
|
||||
const decryptedKeyToObject = uint8ArrayToObject(dataint8Array);
|
||||
if (!validateSecretKey(decryptedKeyToObject))
|
||||
|
||||
if (!validateSecretKey(decryptedKeyToObject)) {
|
||||
throw new Error('SecretKey is not valid');
|
||||
}
|
||||
|
||||
setSecretKeyDetails(publish);
|
||||
setSecretKey(decryptedKeyToObject);
|
||||
lastFetchedSecretKey.current = Date.now();
|
||||
setMemberCountFromSecretKeyData(decryptedKey.count);
|
||||
|
||||
window
|
||||
.sendMessage('setGroupData', {
|
||||
groupId: selectedGroup?.groupId,
|
||||
@ -753,7 +779,22 @@ export const Group = ({
|
||||
setIsLoadingGroupMessage('');
|
||||
resumeAllQueues();
|
||||
}
|
||||
};
|
||||
},
|
||||
[
|
||||
secretKey,
|
||||
selectedGroup?.groupId,
|
||||
setIsLoadingGroup,
|
||||
setIsLoadingGroupMessage,
|
||||
setSecretKey,
|
||||
setSecretKeyDetails,
|
||||
setTriedToFetchSecretKey,
|
||||
setFirstSecretKeyInCreation,
|
||||
setMemberCountFromSecretKeyData,
|
||||
setAdmins,
|
||||
setAdminsWithNames,
|
||||
setSecretKeyPublishDate,
|
||||
]
|
||||
);
|
||||
|
||||
const getAdminsForPublic = async (selectedGroup) => {
|
||||
try {
|
||||
@ -826,6 +867,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 +896,9 @@ export const Group = ({
|
||||
return result;
|
||||
}, {});
|
||||
setGroupsProperties(transformToObject);
|
||||
Object.keys(transformToObject).forEach((key) => {
|
||||
getOwnerNameForGroup(transformToObject[key]?.owner || '', key);
|
||||
});
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
}
|
||||
@ -1468,9 +1530,9 @@ export const Group = ({
|
||||
};
|
||||
}, [groups, selectedGroup]);
|
||||
|
||||
const handleSecretKeyCreationInProgress = () => {
|
||||
const handleSecretKeyCreationInProgress = useCallback(() => {
|
||||
setFirstSecretKeyInCreation(true);
|
||||
};
|
||||
}, []);
|
||||
|
||||
const goToHome = async () => {
|
||||
setDesktopViewMode('home');
|
||||
@ -1781,112 +1843,7 @@ export const Group = ({
|
||||
);
|
||||
};
|
||||
|
||||
const renderGroups = () => {
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
width: '380px',
|
||||
flexDirection: 'column',
|
||||
alignItems: 'flex-start',
|
||||
height: '100%',
|
||||
background: theme.palette.background.surface,
|
||||
borderRadius: '0px 15px 15px 0px',
|
||||
padding: '0px 2px',
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
sx={{
|
||||
width: '100%',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
display: 'flex',
|
||||
gap: '10px',
|
||||
}}
|
||||
>
|
||||
<ButtonBase
|
||||
onClick={() => {
|
||||
setDesktopSideView('groups');
|
||||
}}
|
||||
>
|
||||
<IconWrapper
|
||||
color={
|
||||
groupChatHasUnread || groupsAnnHasUnread
|
||||
? theme.palette.other.unread
|
||||
: desktopSideView === 'groups'
|
||||
? theme.palette.text.primary
|
||||
: theme.palette.text.secondary
|
||||
}
|
||||
label="Groups"
|
||||
selected={desktopSideView === 'groups'}
|
||||
customWidth="75px"
|
||||
>
|
||||
<HubsIcon
|
||||
height={24}
|
||||
color={
|
||||
groupChatHasUnread || groupsAnnHasUnread
|
||||
? theme.palette.other.unread
|
||||
: desktopSideView === 'groups'
|
||||
? theme.palette.text.primary
|
||||
: theme.palette.text.secondary
|
||||
}
|
||||
/>
|
||||
</IconWrapper>
|
||||
</ButtonBase>
|
||||
<ButtonBase
|
||||
onClick={() => {
|
||||
setDesktopSideView('directs');
|
||||
}}
|
||||
>
|
||||
<IconWrapper
|
||||
customWidth="75px"
|
||||
color={
|
||||
directChatHasUnread
|
||||
? theme.palette.other.unread
|
||||
: desktopSideView === 'directs'
|
||||
? theme.palette.text.primary
|
||||
: theme.palette.text.secondary
|
||||
}
|
||||
label="Messaging"
|
||||
selected={desktopSideView === 'directs'}
|
||||
>
|
||||
<MessagingIcon
|
||||
height={24}
|
||||
color={
|
||||
directChatHasUnread
|
||||
? theme.palette.other.unread
|
||||
: desktopSideView === 'directs'
|
||||
? theme.palette.text.primary
|
||||
: theme.palette.text.secondary
|
||||
}
|
||||
/>
|
||||
</IconWrapper>
|
||||
</ButtonBase>
|
||||
</Box>
|
||||
|
||||
<div
|
||||
style={{
|
||||
alignItems: 'flex-start',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
flexGrow: 1,
|
||||
left: chatMode === 'directs' && '-1000px',
|
||||
overflowY: 'auto',
|
||||
position: chatMode === 'directs' && 'fixed',
|
||||
visibility: chatMode === 'directs' && 'hidden',
|
||||
width: '100%',
|
||||
}}
|
||||
>
|
||||
{groups.map((group: any) => (
|
||||
<List
|
||||
sx={{
|
||||
width: '100%',
|
||||
}}
|
||||
className="group-list"
|
||||
dense={true}
|
||||
>
|
||||
<ListItem
|
||||
onClick={() => {
|
||||
const selectGroupFunc = useCallback((group) => {
|
||||
setMobileViewMode('group');
|
||||
setDesktopSideView('groups');
|
||||
initiatedGetMembers.current = false;
|
||||
@ -1913,200 +1870,7 @@ export const Group = ({
|
||||
setTimeout(() => {
|
||||
setSelectedGroup(group);
|
||||
}, 200);
|
||||
}}
|
||||
sx={{
|
||||
display: 'flex',
|
||||
background:
|
||||
group?.groupId === selectedGroup?.groupId &&
|
||||
theme.palette.action.selected,
|
||||
borderRadius: '2px',
|
||||
cursor: 'pointer',
|
||||
flexDirection: 'column',
|
||||
padding: '2px',
|
||||
width: '100%',
|
||||
'&:hover': {
|
||||
backgroundColor: 'action.hover', // background on hover
|
||||
},
|
||||
}}
|
||||
>
|
||||
<ContextMenu
|
||||
mutedGroups={mutedGroups}
|
||||
getUserSettings={getUserSettings}
|
||||
groupId={group.groupId}
|
||||
>
|
||||
<Box
|
||||
sx={{
|
||||
alignItems: 'center',
|
||||
display: 'flex',
|
||||
width: '100%',
|
||||
}}
|
||||
>
|
||||
<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',
|
||||
}}
|
||||
>
|
||||
{/* <Avatar src={`${getBaseApiReact()}/arbitrary/THUMBNAIL/${
|
||||
app?.name
|
||||
}/qortal_avatar?async=true`} /> */}
|
||||
<LockIcon
|
||||
sx={{
|
||||
color: theme.palette.other.positive,
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
) : (
|
||||
<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>
|
||||
)}
|
||||
</ListItemAvatar>
|
||||
<ListItemText
|
||||
primary={
|
||||
group.groupId === '0' ? 'General' : group.groupName
|
||||
}
|
||||
secondary={
|
||||
!group?.timestamp
|
||||
? 'no messages'
|
||||
: `last message: ${formatEmailDate(group?.timestamp)}`
|
||||
}
|
||||
primaryTypographyProps={{
|
||||
style: {
|
||||
color:
|
||||
group?.groupId === selectedGroup?.groupId &&
|
||||
theme.palette.text.primary,
|
||||
fontSize: '16px',
|
||||
},
|
||||
}} // Change the color of the primary text
|
||||
secondaryTypographyProps={{
|
||||
style: {
|
||||
color:
|
||||
group?.groupId === selectedGroup?.groupId &&
|
||||
theme.palette.text.primary,
|
||||
fontSize: '12px',
|
||||
},
|
||||
}}
|
||||
sx={{
|
||||
width: '150px',
|
||||
fontFamily: 'Inter',
|
||||
fontSize: '16px',
|
||||
}}
|
||||
/>
|
||||
{groupAnnouncements[group?.groupId] &&
|
||||
!groupAnnouncements[group?.groupId]?.seentimestamp && (
|
||||
<CampaignIcon
|
||||
sx={{
|
||||
color: theme.palette.other.unread,
|
||||
marginRight: '5px',
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
{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,
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</Box>
|
||||
</ContextMenu>
|
||||
</ListItem>
|
||||
</List>
|
||||
))}
|
||||
</div>
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
gap: '10px',
|
||||
justifyContent: 'center',
|
||||
padding: '10px',
|
||||
width: '100%',
|
||||
}}
|
||||
>
|
||||
{chatMode === 'groups' && (
|
||||
<>
|
||||
<CustomButton
|
||||
onClick={() => {
|
||||
setOpenAddGroup(true);
|
||||
}}
|
||||
>
|
||||
<AddCircleOutlineIcon
|
||||
sx={{
|
||||
color: theme.palette.text.primary,
|
||||
}}
|
||||
/>
|
||||
Group
|
||||
</CustomButton>
|
||||
|
||||
{!isRunningPublicNode && (
|
||||
<CustomButton
|
||||
onClick={() => {
|
||||
setIsOpenBlockedUserModal(true);
|
||||
}}
|
||||
sx={{
|
||||
minWidth: 'unset',
|
||||
padding: '10px',
|
||||
}}
|
||||
>
|
||||
<PersonOffIcon
|
||||
sx={{
|
||||
color: theme.palette.text.primary,
|
||||
}}
|
||||
/>
|
||||
</CustomButton>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
{chatMode === 'directs' && (
|
||||
<CustomButton
|
||||
onClick={() => {
|
||||
setNewChat(true);
|
||||
setSelectedDirect(null);
|
||||
setIsOpenDrawer(false);
|
||||
}}
|
||||
>
|
||||
<CreateIcon
|
||||
sx={{
|
||||
color: theme.palette.text.primary,
|
||||
}}
|
||||
/>
|
||||
New Chat
|
||||
</CustomButton>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<>
|
||||
@ -2151,9 +1915,24 @@ export const Group = ({
|
||||
/>
|
||||
)}
|
||||
|
||||
{desktopViewMode === 'chat' &&
|
||||
desktopSideView !== 'directs' &&
|
||||
renderGroups()}
|
||||
{desktopViewMode === 'chat' && desktopSideView !== 'directs' && (
|
||||
<GroupList
|
||||
selectGroupFunc={selectGroupFunc}
|
||||
setDesktopSideView={setDesktopSideView}
|
||||
groupChatHasUnread={groupChatHasUnread}
|
||||
groupsAnnHasUnread={groupsAnnHasUnread}
|
||||
desktopSideView={desktopSideView}
|
||||
directChatHasUnread={directChatHasUnread}
|
||||
chatMode={chatMode}
|
||||
groups={groups}
|
||||
selectedGroup={selectedGroup}
|
||||
getUserSettings={getUserSettings}
|
||||
setOpenAddGroup={setOpenAddGroup}
|
||||
isRunningPublicNode={isRunningPublicNode}
|
||||
setIsOpenBlockedUserModal={setIsOpenBlockedUserModal}
|
||||
myAddress={myAddress}
|
||||
/>
|
||||
)}
|
||||
|
||||
{desktopViewMode === 'chat' &&
|
||||
desktopSideView === 'directs' &&
|
||||
@ -2293,7 +2072,7 @@ export const Group = ({
|
||||
isPrivate={isPrivate}
|
||||
setSecretKey={setSecretKey}
|
||||
handleNewEncryptionNotification={setNewEncryptionNotification}
|
||||
hide={groupSection !== 'chat' || selectedDirect || newChat}
|
||||
hide={groupSection !== 'chat' || !!selectedDirect || newChat}
|
||||
hideView={!(desktopViewMode === 'chat' && selectedGroup)}
|
||||
handleSecretKeyCreationInProgress={
|
||||
handleSecretKeyCreationInProgress
|
||||
@ -2431,10 +2210,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}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
|
348
src/components/Group/GroupList.tsx
Normal file
348
src/components/Group/GroupList.tsx
Normal file
@ -0,0 +1,348 @@
|
||||
import {
|
||||
Avatar,
|
||||
Box,
|
||||
ButtonBase,
|
||||
List,
|
||||
ListItem,
|
||||
ListItemAvatar,
|
||||
ListItemText,
|
||||
useTheme,
|
||||
} from '@mui/material';
|
||||
import React, { useCallback } from 'react';
|
||||
import { IconWrapper } from '../Desktop/DesktopFooter';
|
||||
import { HubsIcon } from '../../assets/Icons/HubsIcon';
|
||||
import { MessagingIcon } from '../../assets/Icons/MessagingIcon';
|
||||
import { ContextMenu } from '../ContextMenu';
|
||||
import { getBaseApiReact } from '../../App';
|
||||
import { formatEmailDate } from './QMailMessages';
|
||||
import CampaignIcon from '@mui/icons-material/Campaign';
|
||||
import MarkChatUnreadIcon from '@mui/icons-material/MarkChatUnread';
|
||||
import LockIcon from '@mui/icons-material/Lock';
|
||||
import { CustomButton } from '../../styles/App-styles';
|
||||
import AddCircleOutlineIcon from '@mui/icons-material/AddCircleOutline';
|
||||
import PersonOffIcon from '@mui/icons-material/PersonOff';
|
||||
import {
|
||||
groupAnnouncementSelector,
|
||||
groupChatTimestampSelector,
|
||||
groupPropertySelector,
|
||||
groupsOwnerNamesSelector,
|
||||
timestampEnterDataSelector,
|
||||
} from '../../atoms/global';
|
||||
import { useRecoilValue } from 'recoil';
|
||||
import { timeDifferenceForNotificationChats } from './Group';
|
||||
|
||||
export const GroupList = ({
|
||||
selectGroupFunc,
|
||||
setDesktopSideView,
|
||||
groupChatHasUnread,
|
||||
groupsAnnHasUnread,
|
||||
desktopSideView,
|
||||
directChatHasUnread,
|
||||
chatMode,
|
||||
groups,
|
||||
selectedGroup,
|
||||
getUserSettings,
|
||||
setOpenAddGroup,
|
||||
isRunningPublicNode,
|
||||
setIsOpenBlockedUserModal,
|
||||
myAddress,
|
||||
}) => {
|
||||
const theme = useTheme();
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
width: '380px',
|
||||
flexDirection: 'column',
|
||||
alignItems: 'flex-start',
|
||||
height: '100%',
|
||||
background: theme.palette.background.surface,
|
||||
borderRadius: '0px 15px 15px 0px',
|
||||
padding: '0px 2px',
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
sx={{
|
||||
width: '100%',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
display: 'flex',
|
||||
gap: '10px',
|
||||
}}
|
||||
>
|
||||
<ButtonBase
|
||||
onClick={() => {
|
||||
setDesktopSideView('groups');
|
||||
}}
|
||||
>
|
||||
<IconWrapper
|
||||
color={
|
||||
groupChatHasUnread || groupsAnnHasUnread
|
||||
? theme.palette.other.unread
|
||||
: desktopSideView === 'groups'
|
||||
? theme.palette.text.primary
|
||||
: theme.palette.text.secondary
|
||||
}
|
||||
label="Groups"
|
||||
selected={desktopSideView === 'groups'}
|
||||
customWidth="75px"
|
||||
>
|
||||
<HubsIcon
|
||||
height={24}
|
||||
color={
|
||||
groupChatHasUnread || groupsAnnHasUnread
|
||||
? theme.palette.other.unread
|
||||
: desktopSideView === 'groups'
|
||||
? theme.palette.text.primary
|
||||
: theme.palette.text.secondary
|
||||
}
|
||||
/>
|
||||
</IconWrapper>
|
||||
</ButtonBase>
|
||||
<ButtonBase
|
||||
onClick={() => {
|
||||
setDesktopSideView('directs');
|
||||
}}
|
||||
>
|
||||
<IconWrapper
|
||||
customWidth="75px"
|
||||
color={
|
||||
directChatHasUnread
|
||||
? theme.palette.other.unread
|
||||
: desktopSideView === 'directs'
|
||||
? theme.palette.text.primary
|
||||
: theme.palette.text.secondary
|
||||
}
|
||||
label="Messaging"
|
||||
selected={desktopSideView === 'directs'}
|
||||
>
|
||||
<MessagingIcon
|
||||
height={24}
|
||||
color={
|
||||
directChatHasUnread
|
||||
? theme.palette.other.unread
|
||||
: desktopSideView === 'directs'
|
||||
? theme.palette.text.primary
|
||||
: theme.palette.text.secondary
|
||||
}
|
||||
/>
|
||||
</IconWrapper>
|
||||
</ButtonBase>
|
||||
</Box>
|
||||
|
||||
<div
|
||||
style={{
|
||||
alignItems: 'flex-start',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
flexGrow: 1,
|
||||
left: chatMode === 'directs' && '-1000px',
|
||||
overflowY: 'auto',
|
||||
position: chatMode === 'directs' && 'fixed',
|
||||
visibility: chatMode === 'directs' && 'hidden',
|
||||
width: '100%',
|
||||
}}
|
||||
>
|
||||
<List
|
||||
sx={{
|
||||
width: '100%',
|
||||
}}
|
||||
className="group-list"
|
||||
dense={false}
|
||||
>
|
||||
{groups.map((group: any) => (
|
||||
<GroupItem
|
||||
selectGroupFunc={selectGroupFunc}
|
||||
key={group.groupId}
|
||||
group={group}
|
||||
selectedGroup={selectedGroup}
|
||||
getUserSettings={getUserSettings}
|
||||
myAddress={myAddress}
|
||||
/>
|
||||
))}
|
||||
</List>
|
||||
</div>
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
gap: '10px',
|
||||
justifyContent: 'center',
|
||||
padding: '10px',
|
||||
width: '100%',
|
||||
}}
|
||||
>
|
||||
<>
|
||||
<CustomButton
|
||||
onClick={() => {
|
||||
setOpenAddGroup(true);
|
||||
}}
|
||||
>
|
||||
<AddCircleOutlineIcon
|
||||
sx={{
|
||||
color: theme.palette.text.primary,
|
||||
}}
|
||||
/>
|
||||
Group
|
||||
</CustomButton>
|
||||
|
||||
{!isRunningPublicNode && (
|
||||
<CustomButton
|
||||
onClick={() => {
|
||||
setIsOpenBlockedUserModal(true);
|
||||
}}
|
||||
sx={{
|
||||
minWidth: 'unset',
|
||||
padding: '10px',
|
||||
}}
|
||||
>
|
||||
<PersonOffIcon
|
||||
sx={{
|
||||
color: theme.palette.text.primary,
|
||||
}}
|
||||
/>
|
||||
</CustomButton>
|
||||
)}
|
||||
</>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const GroupItem = React.memo(
|
||||
({ selectGroupFunc, group, selectedGroup, getUserSettings, myAddress }) => {
|
||||
const theme = useTheme();
|
||||
const ownerName = useRecoilValue(groupsOwnerNamesSelector(group?.groupId));
|
||||
const announcement = useRecoilValue(
|
||||
groupAnnouncementSelector(group?.groupId)
|
||||
);
|
||||
const groupProperty = useRecoilValue(groupPropertySelector(group?.groupId));
|
||||
const groupChatTimestamp = useRecoilValue(
|
||||
groupChatTimestampSelector(group?.groupId)
|
||||
);
|
||||
const timestampEnterData = useRecoilValue(
|
||||
timestampEnterDataSelector(group?.groupId)
|
||||
);
|
||||
const selectGroupHandler = useCallback(() => {
|
||||
selectGroupFunc(group);
|
||||
}, [group, selectGroupFunc]);
|
||||
|
||||
return (
|
||||
<ListItem
|
||||
onClick={selectGroupHandler}
|
||||
sx={{
|
||||
display: 'flex',
|
||||
background:
|
||||
group?.groupId === selectedGroup?.groupId &&
|
||||
theme.palette.action.selected,
|
||||
borderRadius: '2px',
|
||||
cursor: 'pointer',
|
||||
flexDirection: 'column',
|
||||
padding: '10px',
|
||||
width: '100%',
|
||||
'&:hover': {
|
||||
backgroundColor: 'action.hover', // background on hover
|
||||
},
|
||||
}}
|
||||
>
|
||||
<ContextMenu getUserSettings={getUserSettings} groupId={group.groupId}>
|
||||
<Box
|
||||
sx={{
|
||||
alignItems: 'center',
|
||||
display: 'flex',
|
||||
width: '100%',
|
||||
}}
|
||||
>
|
||||
<ListItemAvatar>
|
||||
{ownerName ? (
|
||||
<Avatar
|
||||
alt={group?.groupName?.charAt(0)}
|
||||
src={`${getBaseApiReact()}/arbitrary/THUMBNAIL/${
|
||||
ownerName
|
||||
}/qortal_group_avatar_${group?.groupId}?async=true`}
|
||||
>
|
||||
{group?.groupName?.charAt(0).toUpperCase()}
|
||||
</Avatar>
|
||||
) : (
|
||||
<Avatar alt={group?.groupName?.charAt(0)}>
|
||||
{' '}
|
||||
{group?.groupName?.charAt(0).toUpperCase() || 'G'}
|
||||
</Avatar>
|
||||
)}
|
||||
</ListItemAvatar>
|
||||
<ListItemText
|
||||
primary={group.groupId === '0' ? 'General' : group.groupName}
|
||||
secondary={
|
||||
!group?.timestamp
|
||||
? 'no messages'
|
||||
: `last message: ${formatEmailDate(group?.timestamp)}`
|
||||
}
|
||||
primaryTypographyProps={{
|
||||
style: {
|
||||
color:
|
||||
group?.groupId === selectedGroup?.groupId &&
|
||||
theme.palette.text.primary,
|
||||
fontSize: '16px',
|
||||
},
|
||||
}} // Change the color of the primary text
|
||||
secondaryTypographyProps={{
|
||||
style: {
|
||||
color:
|
||||
group?.groupId === selectedGroup?.groupId &&
|
||||
theme.palette.text.primary,
|
||||
fontSize: '12px',
|
||||
},
|
||||
}}
|
||||
sx={{
|
||||
width: '150px',
|
||||
fontFamily: 'Inter',
|
||||
fontSize: '16px',
|
||||
}}
|
||||
/>
|
||||
{announcement && !announcement?.seentimestamp && (
|
||||
<CampaignIcon
|
||||
sx={{
|
||||
color: theme.palette.other.unread,
|
||||
marginRight: '5px',
|
||||
marginBottom: 'auto',
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
<Box
|
||||
sx={{
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
gap: '5px',
|
||||
justifyContent: 'flex-start',
|
||||
height: '100%',
|
||||
marginBottom: 'auto',
|
||||
}}
|
||||
>
|
||||
{group?.data &&
|
||||
groupChatTimestamp &&
|
||||
group?.sender !== myAddress &&
|
||||
group?.timestamp &&
|
||||
((!timestampEnterData &&
|
||||
Date.now() - group?.timestamp <
|
||||
timeDifferenceForNotificationChats) ||
|
||||
timestampEnterData < group?.timestamp) && (
|
||||
<MarkChatUnreadIcon
|
||||
sx={{
|
||||
color: theme.palette.other.unread,
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
{groupProperty?.isOpen === false && (
|
||||
<LockIcon
|
||||
sx={{
|
||||
color: theme.palette.other.positive,
|
||||
marginBottom: 'auto',
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</Box>
|
||||
</Box>
|
||||
</ContextMenu>
|
||||
</ListItem>
|
||||
);
|
||||
}
|
||||
);
|
@ -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: '16px',
|
||||
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: '16px',
|
||||
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: '16px',
|
||||
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