mirror of
https://github.com/Qortal/Qortal-Hub.git
synced 2025-06-30 19:01:21 +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 = {
|
const config: CapacitorConfig = {
|
||||||
appId: 'org.Qortal.Qortal-Hub',
|
appId: "org.Qortal.Qortal-Hub",
|
||||||
appName: 'Qortal-Hub',
|
appName: "Qortal-Hub",
|
||||||
webDir: 'dist',
|
webDir: "dist",
|
||||||
"plugins": {
|
plugins: {
|
||||||
"LocalNotifications": {
|
LocalNotifications: {
|
||||||
"smallIcon": "qort",
|
smallIcon: "qort",
|
||||||
"iconColor": "#09b6e8"
|
iconColor: "#09b6e8",
|
||||||
}
|
},
|
||||||
}
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
export default config;
|
export default config;
|
||||||
|
20
src/App.tsx
20
src/App.tsx
@ -100,17 +100,22 @@ import { useRecoilState, useResetRecoilState, useSetRecoilState } from 'recoil';
|
|||||||
import {
|
import {
|
||||||
canSaveSettingToQdnAtom,
|
canSaveSettingToQdnAtom,
|
||||||
enabledDevModeAtom,
|
enabledDevModeAtom,
|
||||||
|
groupAnnouncementsAtom,
|
||||||
|
groupChatTimestampsAtom,
|
||||||
|
groupsOwnerNamesAtom,
|
||||||
groupsPropertiesAtom,
|
groupsPropertiesAtom,
|
||||||
hasSettingsChangedAtom,
|
hasSettingsChangedAtom,
|
||||||
isDisabledEditorEnterAtom,
|
isDisabledEditorEnterAtom,
|
||||||
isUsingImportExportSettingsAtom,
|
isUsingImportExportSettingsAtom,
|
||||||
lastPaymentSeenTimestampAtom,
|
lastPaymentSeenTimestampAtom,
|
||||||
mailsAtom,
|
mailsAtom,
|
||||||
|
mutedGroupsAtom,
|
||||||
oldPinnedAppsAtom,
|
oldPinnedAppsAtom,
|
||||||
qMailLastEnteredTimestampAtom,
|
qMailLastEnteredTimestampAtom,
|
||||||
settingsLocalLastUpdatedAtom,
|
settingsLocalLastUpdatedAtom,
|
||||||
settingsQDNLastUpdatedAtom,
|
settingsQDNLastUpdatedAtom,
|
||||||
sortablePinnedAppsAtom,
|
sortablePinnedAppsAtom,
|
||||||
|
timestampEnterDataAtom,
|
||||||
} from './atoms/global';
|
} from './atoms/global';
|
||||||
import { NotAuthenticated } from './ExtStates/NotAuthenticated';
|
import { NotAuthenticated } from './ExtStates/NotAuthenticated';
|
||||||
import { handleGetFileFromIndexedDB } from './utils/indexedDB';
|
import { handleGetFileFromIndexedDB } from './utils/indexedDB';
|
||||||
@ -477,6 +482,16 @@ function App() {
|
|||||||
const resetLastPaymentSeenTimestampAtom = useResetRecoilState(
|
const resetLastPaymentSeenTimestampAtom = useResetRecoilState(
|
||||||
lastPaymentSeenTimestampAtom
|
lastPaymentSeenTimestampAtom
|
||||||
);
|
);
|
||||||
|
const resetGroupsOwnerNamesAtom = useResetRecoilState(groupsOwnerNamesAtom);
|
||||||
|
const resetGroupAnnouncementsAtom = useResetRecoilState(
|
||||||
|
groupAnnouncementsAtom
|
||||||
|
);
|
||||||
|
const resetMutedGroupsAtom = useResetRecoilState(mutedGroupsAtom);
|
||||||
|
|
||||||
|
const resetGroupChatTimestampsAtom = useResetRecoilState(
|
||||||
|
groupChatTimestampsAtom
|
||||||
|
);
|
||||||
|
const resetTimestampEnterAtom = useResetRecoilState(timestampEnterDataAtom);
|
||||||
|
|
||||||
const resetAllRecoil = () => {
|
const resetAllRecoil = () => {
|
||||||
resetAtomSortablePinnedAppsAtom();
|
resetAtomSortablePinnedAppsAtom();
|
||||||
@ -489,6 +504,11 @@ function App() {
|
|||||||
resetAtomMailsAtom();
|
resetAtomMailsAtom();
|
||||||
resetGroupPropertiesAtom();
|
resetGroupPropertiesAtom();
|
||||||
resetLastPaymentSeenTimestampAtom();
|
resetLastPaymentSeenTimestampAtom();
|
||||||
|
resetGroupsOwnerNamesAtom();
|
||||||
|
resetGroupAnnouncementsAtom();
|
||||||
|
resetMutedGroupsAtom();
|
||||||
|
resetGroupChatTimestampsAtom();
|
||||||
|
resetTimestampEnterAtom();
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleSetGlobalApikey = (key) => {
|
const handleSetGlobalApikey = (key) => {
|
||||||
|
@ -192,8 +192,82 @@ 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',
|
||||||
default: false,
|
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,
|
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>
|
||||||
|
@ -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>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -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(
|
export const MessageItem = React.memo(
|
||||||
({
|
({
|
||||||
message,
|
message,
|
||||||
@ -210,16 +225,7 @@ export const MessageItem = React.memo(
|
|||||||
{message?.senderName?.charAt(0)}
|
{message?.senderName?.charAt(0)}
|
||||||
</Avatar>
|
</Avatar>
|
||||||
</WrapperUserAction>
|
</WrapperUserAction>
|
||||||
<Tooltip disableFocusListener title={`level ${userInfo ?? 0}`}>
|
<UserBadge userInfo={userInfo} />
|
||||||
<img
|
|
||||||
style={{
|
|
||||||
visibility: userInfo !== undefined ? 'visible' : 'hidden',
|
|
||||||
width: '30px',
|
|
||||||
height: 'auto',
|
|
||||||
}}
|
|
||||||
src={getBadgeImg(userInfo)}
|
|
||||||
/>
|
|
||||||
</Tooltip>
|
|
||||||
</Box>
|
</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 { EditorProvider, useCurrentEditor } from '@tiptap/react';
|
||||||
import StarterKit from '@tiptap/starter-kit';
|
import StarterKit from '@tiptap/starter-kit';
|
||||||
import { Color } from '@tiptap/extension-color';
|
import { Color } from '@tiptap/extension-color';
|
||||||
@ -41,295 +41,297 @@ function textMatcher(doc, from) {
|
|||||||
return { start, query };
|
return { start, query };
|
||||||
}
|
}
|
||||||
|
|
||||||
const MenuBar = ({
|
const MenuBar = React.memo(
|
||||||
setEditorRef,
|
({
|
||||||
isChat,
|
setEditorRef,
|
||||||
isDisabledEditorEnter,
|
isChat,
|
||||||
setIsDisabledEditorEnter,
|
isDisabledEditorEnter,
|
||||||
}) => {
|
setIsDisabledEditorEnter,
|
||||||
const { editor } = useCurrentEditor();
|
}) => {
|
||||||
const fileInputRef = useRef(null);
|
const { editor } = useCurrentEditor();
|
||||||
const theme = useTheme();
|
const fileInputRef = useRef(null);
|
||||||
|
const theme = useTheme();
|
||||||
|
|
||||||
if (!editor) {
|
useEffect(() => {
|
||||||
return null;
|
if (editor && setEditorRef) {
|
||||||
}
|
setEditorRef(editor);
|
||||||
|
}
|
||||||
|
}, [editor, setEditorRef]);
|
||||||
|
|
||||||
useEffect(() => {
|
if (!editor) {
|
||||||
if (editor && setEditorRef) {
|
return null;
|
||||||
setEditorRef(editor);
|
|
||||||
}
|
}
|
||||||
}, [editor, setEditorRef]);
|
|
||||||
|
|
||||||
const handleImageUpload = async (file) => {
|
const handleImageUpload = async (file) => {
|
||||||
let compressedFile;
|
let compressedFile;
|
||||||
await new Promise<void>((resolve) => {
|
await new Promise<void>((resolve) => {
|
||||||
new Compressor(file, {
|
new Compressor(file, {
|
||||||
quality: 0.6,
|
quality: 0.6,
|
||||||
maxWidth: 1200,
|
maxWidth: 1200,
|
||||||
mimeType: 'image/webp',
|
mimeType: 'image/webp',
|
||||||
success(result) {
|
success(result) {
|
||||||
compressedFile = new File([result], 'image.webp', {
|
compressedFile = new File([result], 'image.webp', {
|
||||||
type: 'image/webp',
|
type: 'image/webp',
|
||||||
});
|
});
|
||||||
resolve();
|
resolve();
|
||||||
},
|
},
|
||||||
error(err) {
|
error(err) {
|
||||||
console.error('Image compression error:', err);
|
console.error('Image compression error:', err);
|
||||||
},
|
},
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
|
||||||
|
|
||||||
if (compressedFile) {
|
if (compressedFile) {
|
||||||
const reader = new FileReader();
|
const reader = new FileReader();
|
||||||
reader.onload = () => {
|
reader.onload = () => {
|
||||||
const url = reader.result;
|
const url = reader.result;
|
||||||
editor
|
editor
|
||||||
.chain()
|
.chain()
|
||||||
.focus()
|
.focus()
|
||||||
.setImage({ src: url, style: 'width: auto' })
|
.setImage({ src: url, style: 'width: auto' })
|
||||||
.run();
|
.run();
|
||||||
fileInputRef.current.value = '';
|
fileInputRef.current.value = '';
|
||||||
};
|
};
|
||||||
reader.readAsDataURL(compressedFile);
|
reader.readAsDataURL(compressedFile);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const triggerImageUpload = () => {
|
const triggerImageUpload = () => {
|
||||||
fileInputRef.current.click(); // Trigger the file input click
|
fileInputRef.current.click(); // Trigger the file input click
|
||||||
};
|
};
|
||||||
|
|
||||||
const handlePaste = (event) => {
|
const handlePaste = (event) => {
|
||||||
const items = event.clipboardData.items;
|
const items = event.clipboardData.items;
|
||||||
for (const item of items) {
|
for (const item of items) {
|
||||||
if (item.type.startsWith('image/')) {
|
if (item.type.startsWith('image/')) {
|
||||||
const file = item.getAsFile();
|
const file = item.getAsFile();
|
||||||
if (file) {
|
if (file) {
|
||||||
event.preventDefault(); // Prevent the default paste behavior
|
event.preventDefault(); // Prevent the default paste behavior
|
||||||
handleImageUpload(file); // Call the image upload function
|
handleImageUpload(file); // Call the image upload function
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
};
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (editor) {
|
if (editor) {
|
||||||
editor.view.dom.addEventListener('paste', handlePaste);
|
editor.view.dom.addEventListener('paste', handlePaste);
|
||||||
return () => {
|
return () => {
|
||||||
editor.view.dom.removeEventListener('paste', handlePaste);
|
editor.view.dom.removeEventListener('paste', handlePaste);
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}, [editor]);
|
}, [editor]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="control-group">
|
<div className="control-group">
|
||||||
<div
|
<div
|
||||||
className="button-group"
|
className="button-group"
|
||||||
style={{
|
style={{
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
}}
|
|
||||||
>
|
|
||||||
<IconButton
|
|
||||||
onClick={() => editor.chain().focus().toggleBold().run()}
|
|
||||||
disabled={!editor.can().chain().focus().toggleBold().run()}
|
|
||||||
sx={{
|
|
||||||
color: editor.isActive('bold')
|
|
||||||
? theme.palette.text.primary
|
|
||||||
: theme.palette.text.secondary,
|
|
||||||
padding: 'revert',
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<FormatBoldIcon />
|
<IconButton
|
||||||
</IconButton>
|
onClick={() => editor.chain().focus().toggleBold().run()}
|
||||||
<IconButton
|
disabled={!editor.can().chain().focus().toggleBold().run()}
|
||||||
onClick={() => editor.chain().focus().toggleItalic().run()}
|
sx={{
|
||||||
disabled={!editor.can().chain().focus().toggleItalic().run()}
|
color: editor.isActive('bold')
|
||||||
sx={{
|
|
||||||
color: editor.isActive('italic')
|
|
||||||
? theme.palette.text.primary
|
|
||||||
: theme.palette.text.secondary,
|
|
||||||
padding: 'revert',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<FormatItalicIcon />
|
|
||||||
</IconButton>
|
|
||||||
<IconButton
|
|
||||||
onClick={() => editor.chain().focus().toggleStrike().run()}
|
|
||||||
disabled={!editor.can().chain().focus().toggleStrike().run()}
|
|
||||||
sx={{
|
|
||||||
color: editor.isActive('strike')
|
|
||||||
? theme.palette.text.primary
|
|
||||||
: theme.palette.text.secondary,
|
|
||||||
padding: 'revert',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<StrikethroughSIcon />
|
|
||||||
</IconButton>
|
|
||||||
<IconButton
|
|
||||||
onClick={() => editor.chain().focus().toggleCode().run()}
|
|
||||||
disabled={!editor.can().chain().focus().toggleCode().run()}
|
|
||||||
sx={{
|
|
||||||
color: editor.isActive('code')
|
|
||||||
? theme.palette.text.primary
|
|
||||||
: theme.palette.text.secondary,
|
|
||||||
padding: 'revert',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<CodeIcon />
|
|
||||||
</IconButton>
|
|
||||||
<IconButton
|
|
||||||
onClick={() => editor.chain().focus().unsetAllMarks().run()}
|
|
||||||
sx={{
|
|
||||||
color:
|
|
||||||
editor.isActive('bold') ||
|
|
||||||
editor.isActive('italic') ||
|
|
||||||
editor.isActive('strike') ||
|
|
||||||
editor.isActive('code')
|
|
||||||
? theme.palette.text.primary
|
? theme.palette.text.primary
|
||||||
: theme.palette.text.secondary,
|
: theme.palette.text.secondary,
|
||||||
padding: 'revert',
|
padding: 'revert',
|
||||||
}}
|
|
||||||
>
|
|
||||||
<FormatClearIcon />
|
|
||||||
</IconButton>
|
|
||||||
<IconButton
|
|
||||||
onClick={() => editor.chain().focus().toggleBulletList().run()}
|
|
||||||
sx={{
|
|
||||||
color: editor.isActive('bulletList')
|
|
||||||
? theme.palette.text.primary
|
|
||||||
: theme.palette.text.secondary,
|
|
||||||
padding: 'revert',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<FormatListBulletedIcon />
|
|
||||||
</IconButton>
|
|
||||||
<IconButton
|
|
||||||
onClick={() => editor.chain().focus().toggleOrderedList().run()}
|
|
||||||
sx={{
|
|
||||||
color: editor.isActive('orderedList')
|
|
||||||
? theme.palette.text.primary
|
|
||||||
: theme.palette.text.secondary,
|
|
||||||
padding: 'revert',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<FormatListNumberedIcon />
|
|
||||||
</IconButton>
|
|
||||||
<IconButton
|
|
||||||
onClick={() => editor.chain().focus().toggleCodeBlock().run()}
|
|
||||||
sx={{
|
|
||||||
color: editor.isActive('codeBlock')
|
|
||||||
? theme.palette.text.primary
|
|
||||||
: theme.palette.text.secondary,
|
|
||||||
padding: 'revert',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<DeveloperModeIcon />
|
|
||||||
</IconButton>
|
|
||||||
<IconButton
|
|
||||||
onClick={() => editor.chain().focus().toggleBlockquote().run()}
|
|
||||||
sx={{
|
|
||||||
color: editor.isActive('blockquote')
|
|
||||||
? theme.palette.text.primary
|
|
||||||
: theme.palette.text.secondary,
|
|
||||||
padding: 'revert',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<FormatQuoteIcon />
|
|
||||||
</IconButton>
|
|
||||||
<IconButton
|
|
||||||
onClick={() => editor.chain().focus().setHorizontalRule().run()}
|
|
||||||
disabled={!editor.can().chain().focus().setHorizontalRule().run()}
|
|
||||||
sx={{ color: 'gray', padding: 'revert' }}
|
|
||||||
>
|
|
||||||
<HorizontalRuleIcon />
|
|
||||||
</IconButton>
|
|
||||||
<IconButton
|
|
||||||
onClick={() =>
|
|
||||||
editor.chain().focus().toggleHeading({ level: 1 }).run()
|
|
||||||
}
|
|
||||||
sx={{
|
|
||||||
color: editor.isActive('heading', { level: 1 })
|
|
||||||
? theme.palette.text.primary
|
|
||||||
: theme.palette.text.secondary,
|
|
||||||
padding: 'revert',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<FormatHeadingIcon fontSize="small" />
|
|
||||||
</IconButton>
|
|
||||||
<IconButton
|
|
||||||
onClick={() => editor.chain().focus().undo().run()}
|
|
||||||
disabled={!editor.can().chain().focus().undo().run()}
|
|
||||||
sx={{ color: 'gray', padding: 'revert' }}
|
|
||||||
>
|
|
||||||
<UndoIcon />
|
|
||||||
</IconButton>
|
|
||||||
<IconButton
|
|
||||||
onClick={() => editor.chain().focus().redo().run()}
|
|
||||||
disabled={!editor.can().chain().focus().redo().run()}
|
|
||||||
sx={{ color: 'gray' }}
|
|
||||||
>
|
|
||||||
<RedoIcon />
|
|
||||||
</IconButton>
|
|
||||||
{isChat && (
|
|
||||||
<Box
|
|
||||||
sx={{
|
|
||||||
display: 'flex',
|
|
||||||
alignItems: 'center',
|
|
||||||
marginLeft: '5px',
|
|
||||||
cursor: 'pointer',
|
|
||||||
}}
|
|
||||||
onClick={() => {
|
|
||||||
setIsDisabledEditorEnter(!isDisabledEditorEnter);
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Checkbox
|
<FormatBoldIcon />
|
||||||
edge="start"
|
</IconButton>
|
||||||
tabIndex={-1}
|
<IconButton
|
||||||
disableRipple
|
onClick={() => editor.chain().focus().toggleItalic().run()}
|
||||||
checked={isDisabledEditorEnter}
|
disabled={!editor.can().chain().focus().toggleItalic().run()}
|
||||||
|
sx={{
|
||||||
|
color: editor.isActive('italic')
|
||||||
|
? theme.palette.text.primary
|
||||||
|
: theme.palette.text.secondary,
|
||||||
|
padding: 'revert',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<FormatItalicIcon />
|
||||||
|
</IconButton>
|
||||||
|
<IconButton
|
||||||
|
onClick={() => editor.chain().focus().toggleStrike().run()}
|
||||||
|
disabled={!editor.can().chain().focus().toggleStrike().run()}
|
||||||
|
sx={{
|
||||||
|
color: editor.isActive('strike')
|
||||||
|
? theme.palette.text.primary
|
||||||
|
: theme.palette.text.secondary,
|
||||||
|
padding: 'revert',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<StrikethroughSIcon />
|
||||||
|
</IconButton>
|
||||||
|
<IconButton
|
||||||
|
onClick={() => editor.chain().focus().toggleCode().run()}
|
||||||
|
disabled={!editor.can().chain().focus().toggleCode().run()}
|
||||||
|
sx={{
|
||||||
|
color: editor.isActive('code')
|
||||||
|
? theme.palette.text.primary
|
||||||
|
: theme.palette.text.secondary,
|
||||||
|
padding: 'revert',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<CodeIcon />
|
||||||
|
</IconButton>
|
||||||
|
<IconButton
|
||||||
|
onClick={() => editor.chain().focus().unsetAllMarks().run()}
|
||||||
|
sx={{
|
||||||
|
color:
|
||||||
|
editor.isActive('bold') ||
|
||||||
|
editor.isActive('italic') ||
|
||||||
|
editor.isActive('strike') ||
|
||||||
|
editor.isActive('code')
|
||||||
|
? theme.palette.text.primary
|
||||||
|
: theme.palette.text.secondary,
|
||||||
|
padding: 'revert',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<FormatClearIcon />
|
||||||
|
</IconButton>
|
||||||
|
<IconButton
|
||||||
|
onClick={() => editor.chain().focus().toggleBulletList().run()}
|
||||||
|
sx={{
|
||||||
|
color: editor.isActive('bulletList')
|
||||||
|
? theme.palette.text.primary
|
||||||
|
: theme.palette.text.secondary,
|
||||||
|
padding: 'revert',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<FormatListBulletedIcon />
|
||||||
|
</IconButton>
|
||||||
|
<IconButton
|
||||||
|
onClick={() => editor.chain().focus().toggleOrderedList().run()}
|
||||||
|
sx={{
|
||||||
|
color: editor.isActive('orderedList')
|
||||||
|
? theme.palette.text.primary
|
||||||
|
: theme.palette.text.secondary,
|
||||||
|
padding: 'revert',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<FormatListNumberedIcon />
|
||||||
|
</IconButton>
|
||||||
|
<IconButton
|
||||||
|
onClick={() => editor.chain().focus().toggleCodeBlock().run()}
|
||||||
|
sx={{
|
||||||
|
color: editor.isActive('codeBlock')
|
||||||
|
? theme.palette.text.primary
|
||||||
|
: theme.palette.text.secondary,
|
||||||
|
padding: 'revert',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<DeveloperModeIcon />
|
||||||
|
</IconButton>
|
||||||
|
<IconButton
|
||||||
|
onClick={() => editor.chain().focus().toggleBlockquote().run()}
|
||||||
|
sx={{
|
||||||
|
color: editor.isActive('blockquote')
|
||||||
|
? theme.palette.text.primary
|
||||||
|
: theme.palette.text.secondary,
|
||||||
|
padding: 'revert',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<FormatQuoteIcon />
|
||||||
|
</IconButton>
|
||||||
|
<IconButton
|
||||||
|
onClick={() => editor.chain().focus().setHorizontalRule().run()}
|
||||||
|
disabled={!editor.can().chain().focus().setHorizontalRule().run()}
|
||||||
|
sx={{ color: 'gray', padding: 'revert' }}
|
||||||
|
>
|
||||||
|
<HorizontalRuleIcon />
|
||||||
|
</IconButton>
|
||||||
|
<IconButton
|
||||||
|
onClick={() =>
|
||||||
|
editor.chain().focus().toggleHeading({ level: 1 }).run()
|
||||||
|
}
|
||||||
|
sx={{
|
||||||
|
color: editor.isActive('heading', { level: 1 })
|
||||||
|
? theme.palette.text.primary
|
||||||
|
: theme.palette.text.secondary,
|
||||||
|
padding: 'revert',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<FormatHeadingIcon fontSize="small" />
|
||||||
|
</IconButton>
|
||||||
|
<IconButton
|
||||||
|
onClick={() => editor.chain().focus().undo().run()}
|
||||||
|
disabled={!editor.can().chain().focus().undo().run()}
|
||||||
|
sx={{ color: 'gray', padding: 'revert' }}
|
||||||
|
>
|
||||||
|
<UndoIcon />
|
||||||
|
</IconButton>
|
||||||
|
<IconButton
|
||||||
|
onClick={() => editor.chain().focus().redo().run()}
|
||||||
|
disabled={!editor.can().chain().focus().redo().run()}
|
||||||
|
sx={{ color: 'gray' }}
|
||||||
|
>
|
||||||
|
<RedoIcon />
|
||||||
|
</IconButton>
|
||||||
|
{isChat && (
|
||||||
|
<Box
|
||||||
sx={{
|
sx={{
|
||||||
'&.Mui-checked': {
|
display: 'flex',
|
||||||
color: theme.palette.text.secondary,
|
alignItems: 'center',
|
||||||
},
|
marginLeft: '5px',
|
||||||
'& .MuiSvgIcon-root': {
|
cursor: 'pointer',
|
||||||
color: theme.palette.text.secondary,
|
|
||||||
},
|
|
||||||
}}
|
}}
|
||||||
/>
|
onClick={() => {
|
||||||
<Typography
|
setIsDisabledEditorEnter(!isDisabledEditorEnter);
|
||||||
sx={{
|
|
||||||
fontSize: '14px',
|
|
||||||
color: theme.palette.text.primary,
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
disable enter
|
<Checkbox
|
||||||
</Typography>
|
edge="start"
|
||||||
</Box>
|
tabIndex={-1}
|
||||||
)}
|
disableRipple
|
||||||
{!isChat && (
|
checked={isDisabledEditorEnter}
|
||||||
<>
|
sx={{
|
||||||
<IconButton
|
'&.Mui-checked': {
|
||||||
onClick={triggerImageUpload}
|
color: theme.palette.text.secondary,
|
||||||
sx={{
|
},
|
||||||
color: theme.palette.text.secondary,
|
'& .MuiSvgIcon-root': {
|
||||||
padding: 'revert',
|
color: theme.palette.text.secondary,
|
||||||
}}
|
},
|
||||||
>
|
}}
|
||||||
<ImageIcon />
|
/>
|
||||||
</IconButton>
|
<Typography
|
||||||
<input
|
sx={{
|
||||||
type="file"
|
fontSize: '14px',
|
||||||
ref={fileInputRef}
|
color: theme.palette.text.primary,
|
||||||
style={{ display: 'none' }}
|
}}
|
||||||
onChange={(event) => handleImageUpload(event.target.files[0])}
|
>
|
||||||
accept="image/*"
|
disable enter
|
||||||
/>
|
</Typography>
|
||||||
</>
|
</Box>
|
||||||
)}
|
)}
|
||||||
|
{!isChat && (
|
||||||
|
<>
|
||||||
|
<IconButton
|
||||||
|
onClick={triggerImageUpload}
|
||||||
|
sx={{
|
||||||
|
color: theme.palette.text.secondary,
|
||||||
|
padding: 'revert',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<ImageIcon />
|
||||||
|
</IconButton>
|
||||||
|
<input
|
||||||
|
type="file"
|
||||||
|
ref={fileInputRef}
|
||||||
|
style={{ display: 'none' }}
|
||||||
|
onChange={(event) => handleImageUpload(event.target.files[0])}
|
||||||
|
accept="image/*"
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
);
|
||||||
);
|
}
|
||||||
};
|
);
|
||||||
|
|
||||||
const extensions = [
|
const extensions = [
|
||||||
Color.configure({ types: [TextStyle.name, ListItem.name] }),
|
Color.configure({ types: [TextStyle.name, ListItem.name] }),
|
||||||
@ -373,10 +375,10 @@ export default ({
|
|||||||
? extensions.filter((item) => item?.name !== 'image')
|
? extensions.filter((item) => item?.name !== 'image')
|
||||||
: extensions;
|
: extensions;
|
||||||
const editorRef = useRef(null);
|
const editorRef = useRef(null);
|
||||||
const setEditorRefFunc = (editorInstance) => {
|
const setEditorRefFunc = useCallback((editorInstance) => {
|
||||||
editorRef.current = editorInstance;
|
editorRef.current = editorInstance;
|
||||||
setEditorRef(editorInstance);
|
setEditorRef(editorInstance);
|
||||||
};
|
}, []);
|
||||||
|
|
||||||
// const users = [
|
// const users = [
|
||||||
// { id: 1, label: 'Alice' },
|
// { id: 1, label: 'Alice' },
|
||||||
|
@ -10,6 +10,8 @@ import {
|
|||||||
import MailOutlineIcon from '@mui/icons-material/MailOutline';
|
import MailOutlineIcon from '@mui/icons-material/MailOutline';
|
||||||
import NotificationsOffIcon from '@mui/icons-material/NotificationsOff';
|
import NotificationsOffIcon from '@mui/icons-material/NotificationsOff';
|
||||||
import { executeEvent } from '../utils/events';
|
import { executeEvent } from '../utils/events';
|
||||||
|
import { useRecoilState } from 'recoil';
|
||||||
|
import { mutedGroupsAtom } from '../atoms/global';
|
||||||
|
|
||||||
const CustomStyledMenu = styled(Menu)(({ theme }) => ({
|
const CustomStyledMenu = styled(Menu)(({ theme }) => ({
|
||||||
'& .MuiPaper-root': {
|
'& .MuiPaper-root': {
|
||||||
@ -28,16 +30,12 @@ const CustomStyledMenu = styled(Menu)(({ theme }) => ({
|
|||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
|
|
||||||
export const ContextMenu = ({
|
export const ContextMenu = ({ children, groupId, getUserSettings }) => {
|
||||||
children,
|
|
||||||
groupId,
|
|
||||||
getUserSettings,
|
|
||||||
mutedGroups,
|
|
||||||
}) => {
|
|
||||||
const [menuPosition, setMenuPosition] = useState(null);
|
const [menuPosition, setMenuPosition] = useState(null);
|
||||||
const longPressTimeout = useRef(null);
|
const longPressTimeout = useRef(null);
|
||||||
const preventClick = useRef(false); // Flag to prevent click after long-press or right-click
|
const preventClick = useRef(false); // Flag to prevent click after long-press or right-click
|
||||||
const theme = useTheme();
|
const theme = useTheme();
|
||||||
|
const [mutedGroups] = useRecoilState(mutedGroupsAtom);
|
||||||
const isMuted = useMemo(() => {
|
const isMuted = useMemo(() => {
|
||||||
return mutedGroups.includes(groupId);
|
return mutedGroups.includes(groupId);
|
||||||
}, [mutedGroups, groupId]);
|
}, [mutedGroups, groupId]);
|
||||||
|
@ -199,6 +199,8 @@ export const AddGroup = ({ address, open, setOpen }) => {
|
|||||||
};
|
};
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
if (!open) return null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Fragment>
|
<Fragment>
|
||||||
<Dialog
|
<Dialog
|
||||||
|
@ -68,9 +68,14 @@ import { AdminSpace } from '../Chat/AdminSpace';
|
|||||||
import { useRecoilState, useSetRecoilState } from 'recoil';
|
import { useRecoilState, useSetRecoilState } from 'recoil';
|
||||||
import {
|
import {
|
||||||
addressInfoControllerAtom,
|
addressInfoControllerAtom,
|
||||||
|
groupAnnouncementsAtom,
|
||||||
|
groupChatTimestampsAtom,
|
||||||
|
groupsOwnerNamesAtom,
|
||||||
groupsPropertiesAtom,
|
groupsPropertiesAtom,
|
||||||
isOpenBlockedModalAtom,
|
isOpenBlockedModalAtom,
|
||||||
|
mutedGroupsAtom,
|
||||||
selectedGroupIdAtom,
|
selectedGroupIdAtom,
|
||||||
|
timestampEnterDataAtom,
|
||||||
} from '../../atoms/global';
|
} from '../../atoms/global';
|
||||||
import { sortArrayByTimestampAndGroupName } from '../../utils/time';
|
import { sortArrayByTimestampAndGroupName } from '../../utils/time';
|
||||||
import PersonOffIcon from '@mui/icons-material/PersonOff';
|
import PersonOffIcon from '@mui/icons-material/PersonOff';
|
||||||
@ -79,6 +84,7 @@ import NoEncryptionGmailerrorredIcon from '@mui/icons-material/NoEncryptionGmail
|
|||||||
import { BlockedUsersModal } from './BlockedUsersModal';
|
import { BlockedUsersModal } from './BlockedUsersModal';
|
||||||
import { WalletsAppWrapper } from './WalletsAppWrapper';
|
import { WalletsAppWrapper } from './WalletsAppWrapper';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import { GroupList } from './GroupList';
|
||||||
|
|
||||||
export const getPublishesFromAdmins = async (admins: string[], groupId) => {
|
export const getPublishesFromAdmins = async (admins: string[], groupId) => {
|
||||||
const queryString = admins.map((name) => `name=${name}`).join('&');
|
const queryString = admins.map((name) => `name=${name}`).join('&');
|
||||||
@ -116,7 +122,7 @@ interface GroupProps {
|
|||||||
balance: number;
|
balance: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
const timeDifferenceForNotificationChats = 900000;
|
export const timeDifferenceForNotificationChats = 900000;
|
||||||
|
|
||||||
export const requestQueueMemberNames = new RequestQueueWithPromise(5);
|
export const requestQueueMemberNames = new RequestQueueWithPromise(5);
|
||||||
export const requestQueueAdminMemberNames = new RequestQueueWithPromise(5);
|
export const requestQueueAdminMemberNames = new RequestQueueWithPromise(5);
|
||||||
@ -409,7 +415,9 @@ export const Group = ({
|
|||||||
const { setMemberGroups, rootHeight, isRunningPublicNode } =
|
const { setMemberGroups, rootHeight, isRunningPublicNode } =
|
||||||
useContext(MyContext);
|
useContext(MyContext);
|
||||||
const lastGroupNotification = useRef<null | number>(null);
|
const lastGroupNotification = useRef<null | number>(null);
|
||||||
const [timestampEnterData, setTimestampEnterData] = useState({});
|
const [timestampEnterData, setTimestampEnterData] = useRecoilState(
|
||||||
|
timestampEnterDataAtom
|
||||||
|
);
|
||||||
const [chatMode, setChatMode] = useState('groups');
|
const [chatMode, setChatMode] = useState('groups');
|
||||||
const [newChat, setNewChat] = useState(false);
|
const [newChat, setNewChat] = useState(false);
|
||||||
const [openSnack, setOpenSnack] = React.useState(false);
|
const [openSnack, setOpenSnack] = React.useState(false);
|
||||||
@ -420,7 +428,10 @@ export const Group = ({
|
|||||||
const [firstSecretKeyInCreation, setFirstSecretKeyInCreation] =
|
const [firstSecretKeyInCreation, setFirstSecretKeyInCreation] =
|
||||||
React.useState(false);
|
React.useState(false);
|
||||||
const [groupSection, setGroupSection] = React.useState('home');
|
const [groupSection, setGroupSection] = React.useState('home');
|
||||||
const [groupAnnouncements, setGroupAnnouncements] = React.useState({});
|
const [groupAnnouncements, setGroupAnnouncements] = useRecoilState(
|
||||||
|
groupAnnouncementsAtom
|
||||||
|
);
|
||||||
|
|
||||||
const [defaultThread, setDefaultThread] = React.useState(null);
|
const [defaultThread, setDefaultThread] = React.useState(null);
|
||||||
const [isOpenDrawer, setIsOpenDrawer] = React.useState(false);
|
const [isOpenDrawer, setIsOpenDrawer] = React.useState(false);
|
||||||
const setIsOpenBlockedUserModal = useSetRecoilState(isOpenBlockedModalAtom);
|
const setIsOpenBlockedUserModal = useSetRecoilState(isOpenBlockedModalAtom);
|
||||||
@ -428,7 +439,7 @@ export const Group = ({
|
|||||||
const [hideCommonKeyPopup, setHideCommonKeyPopup] = React.useState(false);
|
const [hideCommonKeyPopup, setHideCommonKeyPopup] = React.useState(false);
|
||||||
const [isLoadingGroupMessage, setIsLoadingGroupMessage] = React.useState('');
|
const [isLoadingGroupMessage, setIsLoadingGroupMessage] = React.useState('');
|
||||||
const [drawerMode, setDrawerMode] = React.useState('groups');
|
const [drawerMode, setDrawerMode] = React.useState('groups');
|
||||||
const [mutedGroups, setMutedGroups] = useState([]);
|
const setMutedGroups = useSetRecoilState(mutedGroupsAtom);
|
||||||
const [mobileViewMode, setMobileViewMode] = useState('home');
|
const [mobileViewMode, setMobileViewMode] = useState('home');
|
||||||
const [mobileViewModeKeepOpen, setMobileViewModeKeepOpen] = useState('');
|
const [mobileViewModeKeepOpen, setMobileViewModeKeepOpen] = useState('');
|
||||||
const isFocusedRef = useRef(true);
|
const isFocusedRef = useRef(true);
|
||||||
@ -442,17 +453,23 @@ export const Group = ({
|
|||||||
const settimeoutForRefetchSecretKey = useRef(null);
|
const settimeoutForRefetchSecretKey = useRef(null);
|
||||||
const { clearStatesMessageQueueProvider } = useMessageQueue();
|
const { clearStatesMessageQueueProvider } = useMessageQueue();
|
||||||
const initiatedGetMembers = useRef(false);
|
const initiatedGetMembers = useRef(false);
|
||||||
const [groupChatTimestamps, setGroupChatTimestamps] = React.useState({});
|
const [groupChatTimestamps, setGroupChatTimestamps] = useRecoilState(
|
||||||
|
groupChatTimestampsAtom
|
||||||
|
);
|
||||||
const [appsMode, setAppsMode] = useState('home');
|
const [appsMode, setAppsMode] = useState('home');
|
||||||
const [appsModeDev, setAppsModeDev] = useState('home');
|
const [appsModeDev, setAppsModeDev] = useState('home');
|
||||||
const [isOpenSideViewDirects, setIsOpenSideViewDirects] = useState(false);
|
const [isOpenSideViewDirects, setIsOpenSideViewDirects] = useState(false);
|
||||||
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(() => {
|
||||||
@ -495,7 +512,7 @@ export const Group = ({
|
|||||||
selectedDirectRef.current = selectedDirect;
|
selectedDirectRef.current = selectedDirect;
|
||||||
}, [selectedDirect]);
|
}, [selectedDirect]);
|
||||||
|
|
||||||
const getUserSettings = async () => {
|
const getUserSettings = useCallback(async () => {
|
||||||
try {
|
try {
|
||||||
return new Promise((res, rej) => {
|
return new Promise((res, rej) => {
|
||||||
window
|
window
|
||||||
@ -517,13 +534,13 @@ export const Group = ({
|
|||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.log('error', error);
|
console.log('error', error);
|
||||||
}
|
}
|
||||||
};
|
}, [setMutedGroups]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
getUserSettings();
|
getUserSettings();
|
||||||
}, []);
|
}, [getUserSettings]);
|
||||||
|
|
||||||
const getTimestampEnterChat = async () => {
|
const getTimestampEnterChat = useCallback(async () => {
|
||||||
try {
|
try {
|
||||||
return new Promise((res, rej) => {
|
return new Promise((res, rej) => {
|
||||||
window
|
window
|
||||||
@ -543,7 +560,7 @@ export const Group = ({
|
|||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.log(error);
|
console.log(error);
|
||||||
}
|
}
|
||||||
};
|
}, []);
|
||||||
|
|
||||||
const refreshHomeDataFunc = () => {
|
const refreshHomeDataFunc = () => {
|
||||||
setGroupSection('default');
|
setGroupSection('default');
|
||||||
@ -645,115 +662,139 @@ export const Group = ({
|
|||||||
return hasUnread;
|
return hasUnread;
|
||||||
}, [groupAnnouncements, groups]);
|
}, [groupAnnouncements, groups]);
|
||||||
|
|
||||||
const getSecretKey = async (
|
const getSecretKey = useCallback(
|
||||||
loadingGroupParam?: boolean,
|
async (loadingGroupParam?: boolean, secretKeyToPublish?: boolean) => {
|
||||||
secretKeyToPublish?: boolean
|
try {
|
||||||
) => {
|
setIsLoadingGroupMessage('Locating encryption keys');
|
||||||
try {
|
pauseAllQueues();
|
||||||
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');
|
|
||||||
}
|
|
||||||
const publish =
|
|
||||||
publishFromStorage ||
|
|
||||||
(await getPublishesFromAdmins(names, selectedGroup?.groupId));
|
|
||||||
|
|
||||||
if (prevGroupId !== selectedGroupRef.current.groupId) {
|
let dataFromStorage;
|
||||||
if (settimeoutForRefetchSecretKey.current) {
|
let publishFromStorage;
|
||||||
clearTimeout(settimeoutForRefetchSecretKey.current);
|
let adminsFromStorage;
|
||||||
|
|
||||||
|
if (
|
||||||
|
secretKeyToPublish &&
|
||||||
|
secretKey &&
|
||||||
|
lastFetchedSecretKey.current &&
|
||||||
|
Date.now() - lastFetchedSecretKey.current < 600000
|
||||||
|
) {
|
||||||
|
return secretKey;
|
||||||
}
|
}
|
||||||
return;
|
|
||||||
}
|
if (loadingGroupParam) {
|
||||||
if (publish === false) {
|
setIsLoadingGroup(true);
|
||||||
setTriedToFetchSecretKey(true);
|
}
|
||||||
settimeoutForRefetchSecretKey.current = setTimeout(() => {
|
|
||||||
getSecretKey();
|
if (selectedGroup?.groupId !== selectedGroupRef.current.groupId) {
|
||||||
}, 120000);
|
if (settimeoutForRefetchSecretKey.current) {
|
||||||
return false;
|
clearTimeout(settimeoutForRefetchSecretKey.current);
|
||||||
}
|
}
|
||||||
setSecretKeyPublishDate(publish?.updated || publish?.created);
|
return;
|
||||||
let data;
|
}
|
||||||
if (dataFromStorage) {
|
|
||||||
data = dataFromStorage;
|
const prevGroupId = selectedGroupRef.current.groupId;
|
||||||
} else {
|
|
||||||
// const shouldRebuild = !secretKeyPublishDate || (publish?.update && publish?.updated > secretKeyPublishDate)
|
const { names, addresses, both } =
|
||||||
setIsLoadingGroupMessage('Downloading encryption keys');
|
adminsFromStorage || (await getGroupAdmins(selectedGroup?.groupId));
|
||||||
const res = await fetch(
|
setAdmins(addresses);
|
||||||
`${getBaseApiReact()}/arbitrary/DOCUMENT_PRIVATE/${publish.name}/${
|
setAdminsWithNames(both);
|
||||||
publish.identifier
|
|
||||||
}?encoding=base64&rebuild=true`
|
if (!names.length) throw new Error('Network error');
|
||||||
);
|
|
||||||
data = await res.text();
|
const publish =
|
||||||
}
|
publishFromStorage ||
|
||||||
const decryptedKey: any = await decryptResource(data);
|
(await getPublishesFromAdmins(names, selectedGroup?.groupId));
|
||||||
const dataint8Array = base64ToUint8Array(decryptedKey.data);
|
|
||||||
const decryptedKeyToObject = uint8ArrayToObject(dataint8Array);
|
if (prevGroupId !== selectedGroupRef.current.groupId) {
|
||||||
if (!validateSecretKey(decryptedKeyToObject))
|
if (settimeoutForRefetchSecretKey.current) {
|
||||||
throw new Error('SecretKey is not valid');
|
clearTimeout(settimeoutForRefetchSecretKey.current);
|
||||||
setSecretKeyDetails(publish);
|
}
|
||||||
setSecretKey(decryptedKeyToObject);
|
return;
|
||||||
lastFetchedSecretKey.current = Date.now();
|
}
|
||||||
setMemberCountFromSecretKeyData(decryptedKey.count);
|
|
||||||
window
|
if (publish === false) {
|
||||||
.sendMessage('setGroupData', {
|
setTriedToFetchSecretKey(true);
|
||||||
groupId: selectedGroup?.groupId,
|
settimeoutForRefetchSecretKey.current = setTimeout(() => {
|
||||||
secretKeyData: data,
|
getSecretKey();
|
||||||
secretKeyResource: publish,
|
}, 120000);
|
||||||
admins: { names, addresses, both },
|
return false;
|
||||||
})
|
}
|
||||||
.catch((error) => {
|
|
||||||
console.error(
|
setSecretKeyPublishDate(publish?.updated || publish?.created);
|
||||||
'Failed to set group data:',
|
|
||||||
error.message || 'An error occurred'
|
let data;
|
||||||
|
if (dataFromStorage) {
|
||||||
|
data = dataFromStorage;
|
||||||
|
} else {
|
||||||
|
setIsLoadingGroupMessage('Downloading encryption keys');
|
||||||
|
const res = await fetch(
|
||||||
|
`${getBaseApiReact()}/arbitrary/DOCUMENT_PRIVATE/${publish.name}/${publish.identifier}?encoding=base64&rebuild=true`
|
||||||
);
|
);
|
||||||
});
|
data = await res.text();
|
||||||
|
}
|
||||||
|
|
||||||
if (decryptedKeyToObject) {
|
const decryptedKey: any = await decryptResource(data);
|
||||||
setTriedToFetchSecretKey(true);
|
const dataint8Array = base64ToUint8Array(decryptedKey.data);
|
||||||
setFirstSecretKeyInCreation(false);
|
const decryptedKeyToObject = uint8ArrayToObject(dataint8Array);
|
||||||
return decryptedKeyToObject;
|
|
||||||
} else {
|
if (!validateSecretKey(decryptedKeyToObject)) {
|
||||||
setTriedToFetchSecretKey(true);
|
throw new Error('SecretKey is not valid');
|
||||||
|
}
|
||||||
|
|
||||||
|
setSecretKeyDetails(publish);
|
||||||
|
setSecretKey(decryptedKeyToObject);
|
||||||
|
lastFetchedSecretKey.current = Date.now();
|
||||||
|
setMemberCountFromSecretKeyData(decryptedKey.count);
|
||||||
|
|
||||||
|
window
|
||||||
|
.sendMessage('setGroupData', {
|
||||||
|
groupId: selectedGroup?.groupId,
|
||||||
|
secretKeyData: data,
|
||||||
|
secretKeyResource: publish,
|
||||||
|
admins: { names, addresses, both },
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
console.error(
|
||||||
|
'Failed to set group data:',
|
||||||
|
error.message || 'An error occurred'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
if (decryptedKeyToObject) {
|
||||||
|
setTriedToFetchSecretKey(true);
|
||||||
|
setFirstSecretKeyInCreation(false);
|
||||||
|
return decryptedKeyToObject;
|
||||||
|
} else {
|
||||||
|
setTriedToFetchSecretKey(true);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
if (error === 'Unable to decrypt data') {
|
||||||
|
setTriedToFetchSecretKey(true);
|
||||||
|
settimeoutForRefetchSecretKey.current = setTimeout(() => {
|
||||||
|
getSecretKey();
|
||||||
|
}, 120000);
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
setIsLoadingGroup(false);
|
||||||
|
setIsLoadingGroupMessage('');
|
||||||
|
resumeAllQueues();
|
||||||
}
|
}
|
||||||
} catch (error) {
|
},
|
||||||
if (error === 'Unable to decrypt data') {
|
[
|
||||||
setTriedToFetchSecretKey(true);
|
secretKey,
|
||||||
settimeoutForRefetchSecretKey.current = setTimeout(() => {
|
selectedGroup?.groupId,
|
||||||
getSecretKey();
|
setIsLoadingGroup,
|
||||||
}, 120000);
|
setIsLoadingGroupMessage,
|
||||||
}
|
setSecretKey,
|
||||||
} finally {
|
setSecretKeyDetails,
|
||||||
setIsLoadingGroup(false);
|
setTriedToFetchSecretKey,
|
||||||
setIsLoadingGroupMessage('');
|
setFirstSecretKeyInCreation,
|
||||||
resumeAllQueues();
|
setMemberCountFromSecretKeyData,
|
||||||
}
|
setAdmins,
|
||||||
};
|
setAdminsWithNames,
|
||||||
|
setSecretKeyPublishDate,
|
||||||
|
]
|
||||||
|
);
|
||||||
|
|
||||||
const getAdminsForPublic = async (selectedGroup) => {
|
const getAdminsForPublic = async (selectedGroup) => {
|
||||||
try {
|
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) => {
|
const getGroupsProperties = useCallback(async (address) => {
|
||||||
try {
|
try {
|
||||||
const url = `${getBaseApiReact()}/groups/member/${address}`;
|
const url = `${getBaseApiReact()}/groups/member/${address}`;
|
||||||
@ -837,6 +896,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);
|
||||||
}
|
}
|
||||||
@ -1468,9 +1530,9 @@ export const Group = ({
|
|||||||
};
|
};
|
||||||
}, [groups, selectedGroup]);
|
}, [groups, selectedGroup]);
|
||||||
|
|
||||||
const handleSecretKeyCreationInProgress = () => {
|
const handleSecretKeyCreationInProgress = useCallback(() => {
|
||||||
setFirstSecretKeyInCreation(true);
|
setFirstSecretKeyInCreation(true);
|
||||||
};
|
}, []);
|
||||||
|
|
||||||
const goToHome = async () => {
|
const goToHome = async () => {
|
||||||
setDesktopViewMode('home');
|
setDesktopViewMode('home');
|
||||||
@ -1781,332 +1843,34 @@ export const Group = ({
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const renderGroups = () => {
|
const selectGroupFunc = useCallback((group) => {
|
||||||
return (
|
setMobileViewMode('group');
|
||||||
<div
|
setDesktopSideView('groups');
|
||||||
style={{
|
initiatedGetMembers.current = false;
|
||||||
display: 'flex',
|
clearAllQueues();
|
||||||
width: '380px',
|
setSelectedDirect(null);
|
||||||
flexDirection: 'column',
|
setTriedToFetchSecretKey(false);
|
||||||
alignItems: 'flex-start',
|
setNewChat(false);
|
||||||
height: '100%',
|
setSelectedGroup(null);
|
||||||
background: theme.palette.background.surface,
|
setUserInfoForLevels({});
|
||||||
borderRadius: '0px 15px 15px 0px',
|
setSecretKey(null);
|
||||||
padding: '0px 2px',
|
lastFetchedSecretKey.current = null;
|
||||||
}}
|
setSecretKeyPublishDate(null);
|
||||||
>
|
setAdmins([]);
|
||||||
<Box
|
setSecretKeyDetails(null);
|
||||||
sx={{
|
setAdminsWithNames([]);
|
||||||
width: '100%',
|
setGroupOwner(null);
|
||||||
alignItems: 'center',
|
setMembers([]);
|
||||||
justifyContent: 'center',
|
setMemberCountFromSecretKeyData(null);
|
||||||
display: 'flex',
|
setHideCommonKeyPopup(false);
|
||||||
gap: '10px',
|
setFirstSecretKeyInCreation(false);
|
||||||
}}
|
setGroupSection('chat');
|
||||||
>
|
setIsOpenDrawer(false);
|
||||||
<ButtonBase
|
setIsForceShowCreationKeyPopup(false);
|
||||||
onClick={() => {
|
setTimeout(() => {
|
||||||
setDesktopSideView('groups');
|
setSelectedGroup(group);
|
||||||
}}
|
}, 200);
|
||||||
>
|
}, []);
|
||||||
<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={() => {
|
|
||||||
setMobileViewMode('group');
|
|
||||||
setDesktopSideView('groups');
|
|
||||||
initiatedGetMembers.current = false;
|
|
||||||
clearAllQueues();
|
|
||||||
setSelectedDirect(null);
|
|
||||||
setTriedToFetchSecretKey(false);
|
|
||||||
setNewChat(false);
|
|
||||||
setSelectedGroup(null);
|
|
||||||
setUserInfoForLevels({});
|
|
||||||
setSecretKey(null);
|
|
||||||
lastFetchedSecretKey.current = null;
|
|
||||||
setSecretKeyPublishDate(null);
|
|
||||||
setAdmins([]);
|
|
||||||
setSecretKeyDetails(null);
|
|
||||||
setAdminsWithNames([]);
|
|
||||||
setGroupOwner(null);
|
|
||||||
setMembers([]);
|
|
||||||
setMemberCountFromSecretKeyData(null);
|
|
||||||
setHideCommonKeyPopup(false);
|
|
||||||
setFirstSecretKeyInCreation(false);
|
|
||||||
setGroupSection('chat');
|
|
||||||
setIsOpenDrawer(false);
|
|
||||||
setIsForceShowCreationKeyPopup(false);
|
|
||||||
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 (
|
return (
|
||||||
<>
|
<>
|
||||||
@ -2151,9 +1915,24 @@ export const Group = ({
|
|||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{desktopViewMode === 'chat' &&
|
{desktopViewMode === 'chat' && desktopSideView !== 'directs' && (
|
||||||
desktopSideView !== 'directs' &&
|
<GroupList
|
||||||
renderGroups()}
|
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' &&
|
{desktopViewMode === 'chat' &&
|
||||||
desktopSideView === 'directs' &&
|
desktopSideView === 'directs' &&
|
||||||
@ -2293,7 +2072,7 @@ export const Group = ({
|
|||||||
isPrivate={isPrivate}
|
isPrivate={isPrivate}
|
||||||
setSecretKey={setSecretKey}
|
setSecretKey={setSecretKey}
|
||||||
handleNewEncryptionNotification={setNewEncryptionNotification}
|
handleNewEncryptionNotification={setNewEncryptionNotification}
|
||||||
hide={groupSection !== 'chat' || selectedDirect || newChat}
|
hide={groupSection !== 'chat' || !!selectedDirect || newChat}
|
||||||
hideView={!(desktopViewMode === 'chat' && selectedGroup)}
|
hideView={!(desktopViewMode === 'chat' && selectedGroup)}
|
||||||
handleSecretKeyCreationInProgress={
|
handleSecretKeyCreationInProgress={
|
||||||
handleSecretKeyCreationInProgress
|
handleSecretKeyCreationInProgress
|
||||||
@ -2431,10 +2210,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}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
|
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 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
|
||||||
|
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