group list optimizations

This commit is contained in:
PhilReact 2025-04-29 17:27:14 +03:00
parent 8bc414356a
commit 6ee01a4ec3
9 changed files with 937 additions and 740 deletions

View File

@ -1,15 +1,15 @@
import type { CapacitorConfig } from '@capacitor/cli';
import type { CapacitorConfig } from "@capacitor/cli";
const config: CapacitorConfig = {
appId: 'org.Qortal.Qortal-Hub',
appName: 'Qortal-Hub',
webDir: 'dist',
"plugins": {
"LocalNotifications": {
"smallIcon": "qort",
"iconColor": "#09b6e8"
}
}
appId: "org.Qortal.Qortal-Hub",
appName: "Qortal-Hub",
webDir: "dist",
plugins: {
LocalNotifications: {
smallIcon: "qort",
iconColor: "#09b6e8",
},
},
};
export default config;

View File

@ -100,6 +100,8 @@ import { useRecoilState, useResetRecoilState, useSetRecoilState } from 'recoil';
import {
canSaveSettingToQdnAtom,
enabledDevModeAtom,
groupAnnouncementsAtom,
groupChatTimestampsAtom,
groupsOwnerNamesAtom,
groupsPropertiesAtom,
hasSettingsChangedAtom,
@ -107,11 +109,13 @@ import {
isUsingImportExportSettingsAtom,
lastPaymentSeenTimestampAtom,
mailsAtom,
mutedGroupsAtom,
oldPinnedAppsAtom,
qMailLastEnteredTimestampAtom,
settingsLocalLastUpdatedAtom,
settingsQDNLastUpdatedAtom,
sortablePinnedAppsAtom,
timestampEnterDataAtom,
} from './atoms/global';
import { NotAuthenticated } from './ExtStates/NotAuthenticated';
import { handleGetFileFromIndexedDB } from './utils/indexedDB';
@ -479,6 +483,15 @@ function App() {
lastPaymentSeenTimestampAtom
);
const resetGroupsOwnerNamesAtom = useResetRecoilState(groupsOwnerNamesAtom);
const resetGroupAnnouncementsAtom = useResetRecoilState(
groupAnnouncementsAtom
);
const resetMutedGroupsAtom = useResetRecoilState(mutedGroupsAtom);
const resetGroupChatTimestampsAtom = useResetRecoilState(
groupChatTimestampsAtom
);
const resetTimestampEnterAtom = useResetRecoilState(timestampEnterDataAtom);
const resetAllRecoil = () => {
resetAtomSortablePinnedAppsAtom();
@ -492,6 +505,10 @@ function App() {
resetGroupPropertiesAtom();
resetLastPaymentSeenTimestampAtom();
resetGroupsOwnerNamesAtom();
resetGroupAnnouncementsAtom();
resetMutedGroupsAtom();
resetGroupChatTimestampsAtom();
resetTimestampEnterAtom();
};
const handleSetGlobalApikey = (key) => {

View File

@ -201,3 +201,73 @@ export const isOpenBlockedModalAtom = atom({
key: 'isOpenBlockedModalAtom',
default: false,
});
export const groupsOwnerNamesSelector = selectorFamily({
key: 'groupsOwnerNamesSelector',
get:
(key) =>
({ get }) => {
const data = get(groupsOwnerNamesAtom);
return data[key] || null; // Return the value for the key or null if not found
},
});
export const groupAnnouncementsAtom = atom({
key: 'groupAnnouncementsAtom',
default: {},
});
export const groupAnnouncementSelector = selectorFamily({
key: 'groupAnnouncementSelector',
get:
(key) =>
({ get }) => {
const data = get(groupAnnouncementsAtom);
return data[key] || null; // Return the value for the key or null if not found
},
});
export const groupPropertySelector = selectorFamily({
key: 'groupPropertySelector',
get:
(key) =>
({ get }) => {
const data = get(groupsPropertiesAtom);
return data[key] || null; // Return the value for the key or null if not found
},
});
export const mutedGroupsAtom = atom({
key: 'mutedGroupsAtom',
default: [],
});
export const groupChatTimestampsAtom = atom({
key: 'groupChatTimestampsAtom',
default: {},
});
export const groupChatTimestampSelector = selectorFamily({
key: 'groupChatTimestampSelector',
get:
(key) =>
({ get }) => {
const data = get(groupChatTimestampsAtom);
return data[key] || null; // Return the value for the key or null if not found
},
});
export const timestampEnterDataAtom = atom({
key: 'timestampEnterDataAtom',
default: {},
});
export const timestampEnterDataSelector = selectorFamily({
key: 'timestampEnterDataSelector',
get:
(key) =>
({ get }) => {
const data = get(timestampEnterDataAtom);
return data[key] || null; // Return the value for the key or null if not found
},
});

View File

@ -75,6 +75,21 @@ const getBadgeImg = (level) => {
}
};
const UserBadge = React.memo(({ userInfo }) => {
return (
<Tooltip disableFocusListener title={`level ${userInfo ?? 0}`}>
<img
style={{
visibility: userInfo !== undefined ? 'visible' : 'hidden',
width: '30px',
height: 'auto',
}}
src={getBadgeImg(userInfo)}
/>
</Tooltip>
);
});
export const MessageItem = React.memo(
({
message,
@ -210,16 +225,7 @@ export const MessageItem = React.memo(
{message?.senderName?.charAt(0)}
</Avatar>
</WrapperUserAction>
<Tooltip disableFocusListener title={`level ${userInfo ?? 0}`}>
<img
style={{
visibility: userInfo !== undefined ? 'visible' : 'hidden',
width: '30px',
height: 'auto',
}}
src={getBadgeImg(userInfo)}
/>
</Tooltip>
<UserBadge userInfo={userInfo} />
</Box>
)}

View File

@ -1,4 +1,4 @@
import { useCallback, useEffect, useMemo, useRef } from 'react';
import React, { useCallback, useEffect, useMemo, useRef } from 'react';
import { EditorProvider, useCurrentEditor } from '@tiptap/react';
import StarterKit from '@tiptap/starter-kit';
import { Color } from '@tiptap/extension-color';
@ -41,7 +41,8 @@ function textMatcher(doc, from) {
return { start, query };
}
const MenuBar = ({
const MenuBar = React.memo(
({
setEditorRef,
isChat,
isDisabledEditorEnter,
@ -51,16 +52,16 @@ const MenuBar = ({
const fileInputRef = useRef(null);
const theme = useTheme();
if (!editor) {
return null;
}
useEffect(() => {
if (editor && setEditorRef) {
setEditorRef(editor);
}
}, [editor, setEditorRef]);
if (!editor) {
return null;
}
const handleImageUpload = async (file) => {
let compressedFile;
await new Promise<void>((resolve) => {
@ -329,7 +330,8 @@ const MenuBar = ({
</div>
</div>
);
};
}
);
const extensions = [
Color.configure({ types: [TextStyle.name, ListItem.name] }),
@ -373,10 +375,10 @@ export default ({
? extensions.filter((item) => item?.name !== 'image')
: extensions;
const editorRef = useRef(null);
const setEditorRefFunc = (editorInstance) => {
const setEditorRefFunc = useCallback((editorInstance) => {
editorRef.current = editorInstance;
setEditorRef(editorInstance);
};
}, []);
// const users = [
// { id: 1, label: 'Alice' },

View File

@ -10,6 +10,8 @@ import {
import MailOutlineIcon from '@mui/icons-material/MailOutline';
import NotificationsOffIcon from '@mui/icons-material/NotificationsOff';
import { executeEvent } from '../utils/events';
import { useRecoilState } from 'recoil';
import { mutedGroupsAtom } from '../atoms/global';
const CustomStyledMenu = styled(Menu)(({ theme }) => ({
'& .MuiPaper-root': {
@ -28,16 +30,12 @@ const CustomStyledMenu = styled(Menu)(({ theme }) => ({
},
}));
export const ContextMenu = ({
children,
groupId,
getUserSettings,
mutedGroups,
}) => {
export const ContextMenu = ({ children, groupId, getUserSettings }) => {
const [menuPosition, setMenuPosition] = useState(null);
const longPressTimeout = useRef(null);
const preventClick = useRef(false); // Flag to prevent click after long-press or right-click
const theme = useTheme();
const [mutedGroups] = useRecoilState(mutedGroupsAtom);
const isMuted = useMemo(() => {
return mutedGroups.includes(groupId);
}, [mutedGroups, groupId]);

View File

@ -199,6 +199,8 @@ export const AddGroup = ({ address, open, setOpen }) => {
};
}, []);
if (!open) return null;
return (
<Fragment>
<Dialog

View File

@ -68,10 +68,14 @@ import { AdminSpace } from '../Chat/AdminSpace';
import { useRecoilState, useSetRecoilState } from 'recoil';
import {
addressInfoControllerAtom,
groupAnnouncementsAtom,
groupChatTimestampsAtom,
groupsOwnerNamesAtom,
groupsPropertiesAtom,
isOpenBlockedModalAtom,
mutedGroupsAtom,
selectedGroupIdAtom,
timestampEnterDataAtom,
} from '../../atoms/global';
import { sortArrayByTimestampAndGroupName } from '../../utils/time';
import PersonOffIcon from '@mui/icons-material/PersonOff';
@ -80,6 +84,7 @@ import NoEncryptionGmailerrorredIcon from '@mui/icons-material/NoEncryptionGmail
import { BlockedUsersModal } from './BlockedUsersModal';
import { WalletsAppWrapper } from './WalletsAppWrapper';
import { useTranslation } from 'react-i18next';
import { GroupList } from './GroupList';
export const getPublishesFromAdmins = async (admins: string[], groupId) => {
const queryString = admins.map((name) => `name=${name}`).join('&');
@ -117,7 +122,7 @@ interface GroupProps {
balance: number;
}
const timeDifferenceForNotificationChats = 900000;
export const timeDifferenceForNotificationChats = 900000;
export const requestQueueMemberNames = new RequestQueueWithPromise(5);
export const requestQueueAdminMemberNames = new RequestQueueWithPromise(5);
@ -410,7 +415,9 @@ export const Group = ({
const { setMemberGroups, rootHeight, isRunningPublicNode } =
useContext(MyContext);
const lastGroupNotification = useRef<null | number>(null);
const [timestampEnterData, setTimestampEnterData] = useState({});
const [timestampEnterData, setTimestampEnterData] = useRecoilState(
timestampEnterDataAtom
);
const [chatMode, setChatMode] = useState('groups');
const [newChat, setNewChat] = useState(false);
const [openSnack, setOpenSnack] = React.useState(false);
@ -421,7 +428,10 @@ export const Group = ({
const [firstSecretKeyInCreation, setFirstSecretKeyInCreation] =
React.useState(false);
const [groupSection, setGroupSection] = React.useState('home');
const [groupAnnouncements, setGroupAnnouncements] = React.useState({});
const [groupAnnouncements, setGroupAnnouncements] = useRecoilState(
groupAnnouncementsAtom
);
const [defaultThread, setDefaultThread] = React.useState(null);
const [isOpenDrawer, setIsOpenDrawer] = React.useState(false);
const setIsOpenBlockedUserModal = useSetRecoilState(isOpenBlockedModalAtom);
@ -429,7 +439,7 @@ export const Group = ({
const [hideCommonKeyPopup, setHideCommonKeyPopup] = React.useState(false);
const [isLoadingGroupMessage, setIsLoadingGroupMessage] = React.useState('');
const [drawerMode, setDrawerMode] = React.useState('groups');
const [mutedGroups, setMutedGroups] = useState([]);
const setMutedGroups = useSetRecoilState(mutedGroupsAtom);
const [mobileViewMode, setMobileViewMode] = useState('home');
const [mobileViewModeKeepOpen, setMobileViewModeKeepOpen] = useState('');
const isFocusedRef = useRef(true);
@ -443,7 +453,9 @@ export const Group = ({
const settimeoutForRefetchSecretKey = useRef(null);
const { clearStatesMessageQueueProvider } = useMessageQueue();
const initiatedGetMembers = useRef(false);
const [groupChatTimestamps, setGroupChatTimestamps] = React.useState({});
const [groupChatTimestamps, setGroupChatTimestamps] = useRecoilState(
groupChatTimestampsAtom
);
const [appsMode, setAppsMode] = useState('home');
const [appsModeDev, setAppsModeDev] = useState('home');
const [isOpenSideViewDirects, setIsOpenSideViewDirects] = useState(false);
@ -500,7 +512,7 @@ export const Group = ({
selectedDirectRef.current = selectedDirect;
}, [selectedDirect]);
const getUserSettings = async () => {
const getUserSettings = useCallback(async () => {
try {
return new Promise((res, rej) => {
window
@ -522,13 +534,13 @@ export const Group = ({
} catch (error) {
console.log('error', error);
}
};
}, [setMutedGroups]);
useEffect(() => {
getUserSettings();
}, []);
}, [getUserSettings]);
const getTimestampEnterChat = async () => {
const getTimestampEnterChat = useCallback(async () => {
try {
return new Promise((res, rej) => {
window
@ -548,7 +560,7 @@ export const Group = ({
} catch (error) {
console.log(error);
}
};
}, []);
const refreshHomeDataFunc = () => {
setGroupSection('default');
@ -650,41 +662,45 @@ export const Group = ({
return hasUnread;
}, [groupAnnouncements, groups]);
const getSecretKey = async (
loadingGroupParam?: boolean,
secretKeyToPublish?: boolean
) => {
const getSecretKey = useCallback(
async (loadingGroupParam?: boolean, secretKeyToPublish?: boolean) => {
try {
setIsLoadingGroupMessage('Locating encryption keys');
pauseAllQueues();
let dataFromStorage;
let publishFromStorage;
let adminsFromStorage;
if (
secretKeyToPublish &&
secretKey &&
lastFetchedSecretKey.current &&
Date.now() - lastFetchedSecretKey.current < 600000
)
) {
return secretKey;
}
if (loadingGroupParam) {
setIsLoadingGroup(true);
}
if (selectedGroup?.groupId !== selectedGroupRef.current.groupId) {
if (settimeoutForRefetchSecretKey.current) {
clearTimeout(settimeoutForRefetchSecretKey.current);
}
return;
}
const prevGroupId = selectedGroupRef.current.groupId;
// const validApi = await findUsableApi();
const { names, addresses, both } =
adminsFromStorage || (await getGroupAdmins(selectedGroup?.groupId));
setAdmins(addresses);
setAdminsWithNames(both);
if (!names.length) {
throw new Error('Network error');
}
if (!names.length) throw new Error('Network error');
const publish =
publishFromStorage ||
(await getPublishesFromAdmins(names, selectedGroup?.groupId));
@ -695,6 +711,7 @@ export const Group = ({
}
return;
}
if (publish === false) {
setTriedToFetchSecretKey(true);
settimeoutForRefetchSecretKey.current = setTimeout(() => {
@ -702,29 +719,33 @@ export const Group = ({
}, 120000);
return false;
}
setSecretKeyPublishDate(publish?.updated || publish?.created);
let data;
if (dataFromStorage) {
data = dataFromStorage;
} else {
// const shouldRebuild = !secretKeyPublishDate || (publish?.update && publish?.updated > secretKeyPublishDate)
setIsLoadingGroupMessage('Downloading encryption keys');
const res = await fetch(
`${getBaseApiReact()}/arbitrary/DOCUMENT_PRIVATE/${publish.name}/${
publish.identifier
}?encoding=base64&rebuild=true`
`${getBaseApiReact()}/arbitrary/DOCUMENT_PRIVATE/${publish.name}/${publish.identifier}?encoding=base64&rebuild=true`
);
data = await res.text();
}
const decryptedKey: any = await decryptResource(data);
const dataint8Array = base64ToUint8Array(decryptedKey.data);
const decryptedKeyToObject = uint8ArrayToObject(dataint8Array);
if (!validateSecretKey(decryptedKeyToObject))
if (!validateSecretKey(decryptedKeyToObject)) {
throw new Error('SecretKey is not valid');
}
setSecretKeyDetails(publish);
setSecretKey(decryptedKeyToObject);
lastFetchedSecretKey.current = Date.now();
setMemberCountFromSecretKeyData(decryptedKey.count);
window
.sendMessage('setGroupData', {
groupId: selectedGroup?.groupId,
@ -758,7 +779,22 @@ export const Group = ({
setIsLoadingGroupMessage('');
resumeAllQueues();
}
};
},
[
secretKey,
selectedGroup?.groupId,
setIsLoadingGroup,
setIsLoadingGroupMessage,
setSecretKey,
setSecretKeyDetails,
setTriedToFetchSecretKey,
setFirstSecretKeyInCreation,
setMemberCountFromSecretKeyData,
setAdmins,
setAdminsWithNames,
setSecretKeyPublishDate,
]
);
const getAdminsForPublic = async (selectedGroup) => {
try {
@ -1050,8 +1086,6 @@ export const Group = ({
triedToFetchSecretKey,
]);
console.log('groupOwner?.owner', groupOwner);
const notifyAdmin = async (admin) => {
try {
setIsLoadingNotifyAdmin(true);
@ -1327,8 +1361,6 @@ export const Group = ({
};
}, []);
console.log('selectedGroup', selectedGroup);
const openGroupChatFromNotification = (e) => {
if (isLoadingOpenSectionFromNotification.current) return;
@ -1498,9 +1530,9 @@ export const Group = ({
};
}, [groups, selectedGroup]);
const handleSecretKeyCreationInProgress = () => {
const handleSecretKeyCreationInProgress = useCallback(() => {
setFirstSecretKeyInCreation(true);
};
}, []);
const goToHome = async () => {
setDesktopViewMode('home');
@ -1811,112 +1843,7 @@ export const Group = ({
);
};
const renderGroups = () => {
return (
<div
style={{
display: 'flex',
width: '380px',
flexDirection: 'column',
alignItems: 'flex-start',
height: '100%',
background: theme.palette.background.surface,
borderRadius: '0px 15px 15px 0px',
padding: '0px 2px',
}}
>
<Box
sx={{
width: '100%',
alignItems: 'center',
justifyContent: 'center',
display: 'flex',
gap: '10px',
}}
>
<ButtonBase
onClick={() => {
setDesktopSideView('groups');
}}
>
<IconWrapper
color={
groupChatHasUnread || groupsAnnHasUnread
? theme.palette.other.unread
: desktopSideView === 'groups'
? theme.palette.text.primary
: theme.palette.text.secondary
}
label="Groups"
selected={desktopSideView === 'groups'}
customWidth="75px"
>
<HubsIcon
height={24}
color={
groupChatHasUnread || groupsAnnHasUnread
? theme.palette.other.unread
: desktopSideView === 'groups'
? theme.palette.text.primary
: theme.palette.text.secondary
}
/>
</IconWrapper>
</ButtonBase>
<ButtonBase
onClick={() => {
setDesktopSideView('directs');
}}
>
<IconWrapper
customWidth="75px"
color={
directChatHasUnread
? theme.palette.other.unread
: desktopSideView === 'directs'
? theme.palette.text.primary
: theme.palette.text.secondary
}
label="Messaging"
selected={desktopSideView === 'directs'}
>
<MessagingIcon
height={24}
color={
directChatHasUnread
? theme.palette.other.unread
: desktopSideView === 'directs'
? theme.palette.text.primary
: theme.palette.text.secondary
}
/>
</IconWrapper>
</ButtonBase>
</Box>
<div
style={{
alignItems: 'flex-start',
display: 'flex',
flexDirection: 'column',
flexGrow: 1,
left: chatMode === 'directs' && '-1000px',
overflowY: 'auto',
position: chatMode === 'directs' && 'fixed',
visibility: chatMode === 'directs' && 'hidden',
width: '100%',
}}
>
{groups.map((group: any) => (
<List
sx={{
width: '100%',
}}
className="group-list"
dense={true}
>
<ListItem
onClick={() => {
const selectGroupFunc = useCallback((group) => {
setMobileViewMode('group');
setDesktopSideView('groups');
initiatedGetMembers.current = false;
@ -1943,195 +1870,7 @@ export const Group = ({
setTimeout(() => {
setSelectedGroup(group);
}, 200);
}}
sx={{
display: 'flex',
background:
group?.groupId === selectedGroup?.groupId &&
theme.palette.action.selected,
borderRadius: '2px',
cursor: 'pointer',
flexDirection: 'column',
padding: '2px',
width: '100%',
'&:hover': {
backgroundColor: 'action.hover', // background on hover
},
}}
>
<ContextMenu
mutedGroups={mutedGroups}
getUserSettings={getUserSettings}
groupId={group.groupId}
>
<Box
sx={{
alignItems: 'center',
display: 'flex',
width: '100%',
}}
>
<ListItemAvatar>
{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 (
<>
@ -2176,9 +1915,24 @@ export const Group = ({
/>
)}
{desktopViewMode === 'chat' &&
desktopSideView !== 'directs' &&
renderGroups()}
{desktopViewMode === 'chat' && desktopSideView !== 'directs' && (
<GroupList
selectGroupFunc={selectGroupFunc}
setDesktopSideView={setDesktopSideView}
groupChatHasUnread={groupChatHasUnread}
groupsAnnHasUnread={groupsAnnHasUnread}
desktopSideView={desktopSideView}
directChatHasUnread={directChatHasUnread}
chatMode={chatMode}
groups={groups}
selectedGroup={selectedGroup}
getUserSettings={getUserSettings}
setOpenAddGroup={setOpenAddGroup}
isRunningPublicNode={isRunningPublicNode}
setIsOpenBlockedUserModal={setIsOpenBlockedUserModal}
myAddress={myAddress}
/>
)}
{desktopViewMode === 'chat' &&
desktopSideView === 'directs' &&
@ -2318,7 +2072,7 @@ export const Group = ({
isPrivate={isPrivate}
setSecretKey={setSecretKey}
handleNewEncryptionNotification={setNewEncryptionNotification}
hide={groupSection !== 'chat' || selectedDirect || newChat}
hide={groupSection !== 'chat' || !!selectedDirect || newChat}
hideView={!(desktopViewMode === 'chat' && selectedGroup)}
handleSecretKeyCreationInProgress={
handleSecretKeyCreationInProgress

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