mirror of
https://github.com/Qortal/Qortal-Hub.git
synced 2025-05-09 19:27:54 +00:00
group list optimizations
This commit is contained in:
parent
8bc414356a
commit
6ee01a4ec3
@ -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;
|
||||||
|
17
src/App.tsx
17
src/App.tsx
@ -100,6 +100,8 @@ import { useRecoilState, useResetRecoilState, useSetRecoilState } from 'recoil';
|
|||||||
import {
|
import {
|
||||||
canSaveSettingToQdnAtom,
|
canSaveSettingToQdnAtom,
|
||||||
enabledDevModeAtom,
|
enabledDevModeAtom,
|
||||||
|
groupAnnouncementsAtom,
|
||||||
|
groupChatTimestampsAtom,
|
||||||
groupsOwnerNamesAtom,
|
groupsOwnerNamesAtom,
|
||||||
groupsPropertiesAtom,
|
groupsPropertiesAtom,
|
||||||
hasSettingsChangedAtom,
|
hasSettingsChangedAtom,
|
||||||
@ -107,11 +109,13 @@ import {
|
|||||||
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';
|
||||||
@ -479,6 +483,15 @@ function App() {
|
|||||||
lastPaymentSeenTimestampAtom
|
lastPaymentSeenTimestampAtom
|
||||||
);
|
);
|
||||||
const resetGroupsOwnerNamesAtom = useResetRecoilState(groupsOwnerNamesAtom);
|
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();
|
||||||
@ -492,6 +505,10 @@ function App() {
|
|||||||
resetGroupPropertiesAtom();
|
resetGroupPropertiesAtom();
|
||||||
resetLastPaymentSeenTimestampAtom();
|
resetLastPaymentSeenTimestampAtom();
|
||||||
resetGroupsOwnerNamesAtom();
|
resetGroupsOwnerNamesAtom();
|
||||||
|
resetGroupAnnouncementsAtom();
|
||||||
|
resetMutedGroupsAtom();
|
||||||
|
resetGroupChatTimestampsAtom();
|
||||||
|
resetTimestampEnterAtom();
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleSetGlobalApikey = (key) => {
|
const handleSetGlobalApikey = (key) => {
|
||||||
|
@ -201,3 +201,73 @@ 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
|
||||||
|
},
|
||||||
|
});
|
||||||
|
@ -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,10 +68,14 @@ import { AdminSpace } from '../Chat/AdminSpace';
|
|||||||
import { useRecoilState, useSetRecoilState } from 'recoil';
|
import { useRecoilState, useSetRecoilState } from 'recoil';
|
||||||
import {
|
import {
|
||||||
addressInfoControllerAtom,
|
addressInfoControllerAtom,
|
||||||
|
groupAnnouncementsAtom,
|
||||||
|
groupChatTimestampsAtom,
|
||||||
groupsOwnerNamesAtom,
|
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';
|
||||||
@ -80,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('&');
|
||||||
@ -117,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);
|
||||||
@ -410,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);
|
||||||
@ -421,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);
|
||||||
@ -429,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);
|
||||||
@ -443,7 +453,9 @@ 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);
|
||||||
@ -500,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
|
||||||
@ -522,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
|
||||||
@ -548,7 +560,7 @@ export const Group = ({
|
|||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.log(error);
|
console.log(error);
|
||||||
}
|
}
|
||||||
};
|
}, []);
|
||||||
|
|
||||||
const refreshHomeDataFunc = () => {
|
const refreshHomeDataFunc = () => {
|
||||||
setGroupSection('default');
|
setGroupSection('default');
|
||||||
@ -650,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 {
|
||||||
@ -1050,8 +1086,6 @@ export const Group = ({
|
|||||||
triedToFetchSecretKey,
|
triedToFetchSecretKey,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
console.log('groupOwner?.owner', groupOwner);
|
|
||||||
|
|
||||||
const notifyAdmin = async (admin) => {
|
const notifyAdmin = async (admin) => {
|
||||||
try {
|
try {
|
||||||
setIsLoadingNotifyAdmin(true);
|
setIsLoadingNotifyAdmin(true);
|
||||||
@ -1327,8 +1361,6 @@ export const Group = ({
|
|||||||
};
|
};
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
console.log('selectedGroup', selectedGroup);
|
|
||||||
|
|
||||||
const openGroupChatFromNotification = (e) => {
|
const openGroupChatFromNotification = (e) => {
|
||||||
if (isLoadingOpenSectionFromNotification.current) return;
|
if (isLoadingOpenSectionFromNotification.current) return;
|
||||||
|
|
||||||
@ -1498,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');
|
||||||
@ -1811,327 +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>
|
|
||||||
{groupsOwnerNames[group?.groupId] ? (
|
|
||||||
<Avatar
|
|
||||||
alt={group?.groupName?.charAt(0)}
|
|
||||||
src={`${getBaseApiReact()}/arbitrary/THUMBNAIL/${
|
|
||||||
groupsOwnerNames[group?.groupId]
|
|
||||||
}/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',
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
{groupAnnouncements[group?.groupId] &&
|
|
||||||
!groupAnnouncements[group?.groupId]?.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 &&
|
|
||||||
groupChatTimestamps[group?.groupId] &&
|
|
||||||
group?.sender !== myAddress &&
|
|
||||||
group?.timestamp &&
|
|
||||||
((!timestampEnterData[group?.groupId] &&
|
|
||||||
Date.now() - group?.timestamp <
|
|
||||||
timeDifferenceForNotificationChats) ||
|
|
||||||
timestampEnterData[group?.groupId] <
|
|
||||||
group?.timestamp) && (
|
|
||||||
<MarkChatUnreadIcon
|
|
||||||
sx={{
|
|
||||||
color: theme.palette.other.unread,
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
{groupsProperties[group?.groupId]?.isOpen === false && (
|
|
||||||
<LockIcon
|
|
||||||
sx={{
|
|
||||||
color: theme.palette.other.positive,
|
|
||||||
marginBottom: 'auto',
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</Box>
|
|
||||||
</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 (
|
||||||
<>
|
<>
|
||||||
@ -2176,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' &&
|
||||||
@ -2318,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
|
||||||
|
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>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
);
|
Loading…
x
Reference in New Issue
Block a user