diff --git a/src/components/left/main/ChatFolders.tsx b/src/components/left/main/ChatFolders.tsx index 250595ac1..5641ddd54 100644 --- a/src/components/left/main/ChatFolders.tsx +++ b/src/components/left/main/ChatFolders.tsx @@ -4,7 +4,7 @@ import React, { import { withGlobal } from '../../../lib/teact/teactn'; import { ApiChat, ApiChatFolder, ApiUser } from '../../../api/types'; -import { GlobalActions } from '../../../global/types'; +import { GlobalActions, GlobalState } from '../../../global/types'; import { NotifyException, NotifySettings, SettingsScreens } from '../../../types'; import { FolderEditDispatch } from '../../../hooks/reducers/useFoldersReducer'; @@ -30,6 +30,7 @@ type OwnProps = { }; type StateProps = { + allListIds: GlobalState['chats']['listIds']; chatsById: Record; usersById: Record; chatFoldersById: Record; @@ -48,6 +49,7 @@ const INFO_THROTTLE = 3000; const SAVED_MESSAGES_HOTKEY = '0'; const ChatFolders: FC = ({ + allListIds, chatsById, usersById, chatFoldersById, @@ -86,11 +88,10 @@ const ChatFolders: FC = ({ return undefined; } - const chatIds = Object.keys(chatsById); const counters = displayedFolders.map((folder) => { const { unreadDialogsCount, hasActiveDialogs, - } = getFolderUnreadDialogs(chatsById, usersById, folder, chatIds, notifySettings, notifyExceptions) || {}; + } = getFolderUnreadDialogs(allListIds, chatsById, usersById, folder, notifySettings, notifyExceptions) || {}; return { id: folder.id, @@ -100,7 +101,7 @@ const ChatFolders: FC = ({ }); return buildCollectionByKey(counters, 'id'); - }, INFO_THROTTLE, [displayedFolders, chatsById, usersById, notifySettings, notifyExceptions]); + }, INFO_THROTTLE, [displayedFolders, allListIds, chatsById, usersById, notifySettings, notifyExceptions]); const folderTabs = useMemo(() => { if (!displayedFolders || !displayedFolders.length) { @@ -240,7 +241,7 @@ const ChatFolders: FC = ({ export default memo(withGlobal( (global): StateProps => { const { - chats: { byId: chatsById }, + chats: { listIds: allListIds, byId: chatsById }, users: { byId: usersById }, chatFolders: { byId: chatFoldersById, @@ -253,6 +254,7 @@ export default memo(withGlobal( } = global; return { + allListIds, chatsById, usersById, chatFoldersById, diff --git a/src/components/left/main/ChatList.tsx b/src/components/left/main/ChatList.tsx index 8f3febb7e..f4fac0027 100644 --- a/src/components/left/main/ChatList.tsx +++ b/src/components/left/main/ChatList.tsx @@ -3,7 +3,7 @@ import React, { } from '../../../lib/teact/teact'; import { withGlobal } from '../../../lib/teact/teactn'; -import { GlobalActions } from '../../../global/types'; +import { GlobalActions, GlobalState } from '../../../global/types'; import { ApiChat, ApiChatFolder, ApiUser, } from '../../../api/types'; @@ -37,11 +37,12 @@ type OwnProps = { }; type StateProps = { + allListIds: GlobalState['chats']['listIds']; chatsById: Record; usersById: Record; - chatFolder?: ApiChatFolder; listIds?: string[]; orderedPinnedIds?: string[]; + chatFolder?: ApiChatFolder; lastSyncTime?: number; notifySettings: NotifySettings; notifyExceptions?: Record; @@ -60,15 +61,16 @@ const ChatList: FC = ({ folderType, folderId, isActive, - chatFolder, + allListIds, chatsById, usersById, listIds, orderedPinnedIds, + chatFolder, lastSyncTime, - foldersDispatch, notifySettings, notifyExceptions, + foldersDispatch, onScreenSelect, loadMoreChats, preloadTopChatMessages, @@ -78,9 +80,12 @@ const ChatList: FC = ({ }) => { const [currentListIds, currentPinnedIds] = useMemo(() => { return folderType === 'folder' && chatFolder - ? prepareFolderListIds(chatsById, usersById, chatFolder, notifySettings, notifyExceptions) + ? prepareFolderListIds(allListIds, chatsById, usersById, chatFolder, notifySettings, notifyExceptions) : [listIds, orderedPinnedIds]; - }, [folderType, chatFolder, chatsById, usersById, notifySettings, notifyExceptions, listIds, orderedPinnedIds]); + }, [ + folderType, chatFolder, allListIds, chatsById, usersById, + notifySettings, notifyExceptions, listIds, orderedPinnedIds, + ]); const [orderById, orderedIds, chatArrays] = useMemo(() => { if (!currentListIds || (folderType === 'folder' && !chatFolder)) { @@ -106,7 +111,7 @@ const ChatList: FC = ({ } return mapValues(orderById, (order, id) => { - return order - (prevOrderById[id] !== undefined ? prevOrderById[id] : Infinity); + return prevOrderById[id] !== undefined ? order - prevOrderById[id] : -Infinity; }); }, [orderById, prevOrderById]); @@ -137,6 +142,39 @@ const ChatList: FC = ({ } }, [lastSyncTime, folderType, preloadTopChatMessages, preloadArchivedChats]); + // Support + and + to navigate between chats + useEffect(() => { + if (!isActive || !orderedIds) { + return undefined; + } + + function handleKeyDown(e: KeyboardEvent) { + if (IS_PWA && ((IS_MAC_OS && e.metaKey) || (!IS_MAC_OS && e.ctrlKey)) && e.code.startsWith('Digit')) { + const [, digit] = e.code.match(/Digit(\d)/) || []; + if (!digit) return; + + const position = Number(digit) - 1; + if (position > orderedIds!.length - 1) return; + + openChat({ id: orderedIds![position], shouldReplaceHistory: true }); + } + + if (e.altKey) { + const targetIndexDelta = e.key === 'ArrowDown' ? 1 : e.key === 'ArrowUp' ? -1 : undefined; + if (!targetIndexDelta) return; + + e.preventDefault(); + openNextChat({ targetIndexDelta, orderedIds }); + } + } + + document.addEventListener('keydown', handleKeyDown); + + return () => { + document.removeEventListener('keydown', handleKeyDown); + }; + }, [isActive, openChat, openNextChat, orderedIds]); + const getAnimationType = useChatAnimationType(orderDiffById); function renderChats() { @@ -179,36 +217,6 @@ const ChatList: FC = ({ ); } - useEffect(() => { - const handleKeyDown = (e: KeyboardEvent) => { - if (isActive && orderedIds) { - if (IS_PWA && ((IS_MAC_OS && e.metaKey) || (!IS_MAC_OS && e.ctrlKey)) && e.code.startsWith('Digit')) { - const [, digit] = e.code.match(/Digit(\d)/) || []; - if (!digit) return; - - const position = Number(digit) - 1; - if (position > orderedIds.length - 1) return; - - openChat({ id: orderedIds[position], shouldReplaceHistory: true }); - } - - if (e.altKey) { - const targetIndexDelta = e.key === 'ArrowDown' ? 1 : e.key === 'ArrowUp' ? -1 : undefined; - if (!targetIndexDelta) return; - - e.preventDefault(); - openNextChat({ targetIndexDelta, orderedIds }); - } - } - }; - - document.addEventListener('keydown', handleKeyDown, false); - - return () => { - document.removeEventListener('keydown', handleKeyDown, false); - }; - }); - return ( ( const chatFolder = folderId ? selectChatFolder(global, folderId) : undefined; return { + allListIds: listIds, chatsById, usersById, lastSyncTime, diff --git a/src/components/left/main/hooks/useChatAnimationType.ts b/src/components/left/main/hooks/useChatAnimationType.ts index 4f89be4a3..8bb1b463d 100644 --- a/src/components/left/main/hooks/useChatAnimationType.ts +++ b/src/components/left/main/hooks/useChatAnimationType.ts @@ -1,4 +1,4 @@ -import { useCallback } from '../../../../lib/teact/teact'; +import { useMemo } from '../../../../lib/teact/teact'; export enum ChatAnimationTypes { Move, @@ -7,29 +7,27 @@ export enum ChatAnimationTypes { } export function useChatAnimationType(orderDiffById: Record) { - const movesUp = useCallback((id: string) => orderDiffById[id] < 0, [orderDiffById]); - const movesDown = useCallback((id: string) => orderDiffById[id] > 0, [orderDiffById]); + return useMemo(() => { + const orderDiffs = Object.values(orderDiffById); + const numberOfUp = orderDiffs.filter((diff) => diff < 0).length; + const numberOfDown = orderDiffs.filter((diff) => diff > 0).length; - const orderDiffIds = Object.keys(orderDiffById); - const numberOfUp = orderDiffIds.filter(movesUp).length; - const numberOfDown = orderDiffIds.filter(movesDown).length; + return (chatId: string): ChatAnimationTypes => { + const orderDiff = orderDiffById[chatId]; + if (orderDiff === 0) { + return ChatAnimationTypes.None; + } - return useCallback((chatId: string): ChatAnimationTypes => { - const orderDiff = orderDiffById[chatId]; + if ( + orderDiff === Infinity + || orderDiff === -Infinity + || (numberOfUp <= numberOfDown && orderDiff < 0) + || (numberOfDown < numberOfUp && orderDiff > 0) + ) { + return ChatAnimationTypes.Opacity; + } - if (orderDiff === 0) { - return ChatAnimationTypes.None; - } - - if ( - orderDiff === Infinity - || orderDiff === -Infinity - || (numberOfUp <= numberOfDown && movesUp(chatId)) - || (numberOfDown < numberOfUp && movesDown(chatId)) - ) { - return ChatAnimationTypes.Opacity; - } - - return ChatAnimationTypes.Move; - }, [movesDown, movesUp, numberOfDown, numberOfUp, orderDiffById]); + return ChatAnimationTypes.Move; + }; + }, [orderDiffById]); } diff --git a/src/components/left/settings/folders/SettingsFoldersMain.tsx b/src/components/left/settings/folders/SettingsFoldersMain.tsx index 487fef58c..5562d26f6 100644 --- a/src/components/left/settings/folders/SettingsFoldersMain.tsx +++ b/src/components/left/settings/folders/SettingsFoldersMain.tsx @@ -3,7 +3,7 @@ import React, { } from '../../../../lib/teact/teact'; import { withGlobal } from '../../../../lib/teact/teactn'; -import { GlobalActions } from '../../../../global/types'; +import { GlobalActions, GlobalState } from '../../../../global/types'; import { ApiChatFolder, ApiChat, ApiUser } from '../../../../api/types'; import { NotifyException, NotifySettings, SettingsScreens } from '../../../../types'; @@ -22,14 +22,15 @@ import Loading from '../../../ui/Loading'; import AnimatedSticker from '../../../common/AnimatedSticker'; type OwnProps = { + isActive?: boolean; onCreateFolder: () => void; onEditFolder: (folder: ApiChatFolder) => void; - isActive?: boolean; onScreenSelect: (screen: SettingsScreens) => void; onReset: () => void; }; type StateProps = { + allListIds: GlobalState['chats']['listIds']; chatsById: Record; usersById: Record; orderedFolderIds?: number[]; @@ -46,11 +47,8 @@ const runThrottledForLoadRecommended = throttle((cb) => cb(), 60000, true); const MAX_ALLOWED_FOLDERS = 10; const SettingsFoldersMain: FC = ({ - onCreateFolder, - onEditFolder, isActive, - onScreenSelect, - onReset, + allListIds, chatsById, usersById, orderedFolderIds, @@ -58,6 +56,10 @@ const SettingsFoldersMain: FC = ({ recommendedChatFolders, notifySettings, notifyExceptions, + onCreateFolder, + onEditFolder, + onScreenSelect, + onReset, loadRecommendedChatFolders, addChatFolder, showDialog, @@ -104,8 +106,6 @@ const SettingsFoldersMain: FC = ({ return undefined; } - const chatIds = Object.keys(chatsById); - return orderedFolderIds.map((id) => { const folder = foldersById[id]; @@ -113,11 +113,11 @@ const SettingsFoldersMain: FC = ({ id: folder.id, title: folder.title, subtitle: getFolderDescriptionText( - lang, chatsById, usersById, folder, chatIds, notifySettings, notifyExceptions, + lang, allListIds, chatsById, usersById, folder, notifySettings, notifyExceptions, ), }; }); - }, [orderedFolderIds, chatsById, foldersById, usersById, notifySettings, notifyExceptions, lang]); + }, [lang, allListIds, foldersById, chatsById, usersById, orderedFolderIds, notifySettings, notifyExceptions]); const handleCreateFolderFromRecommended = useCallback((folder: ApiChatFolder) => { if (Object.keys(foldersById).length >= MAX_ALLOWED_FOLDERS) { @@ -229,7 +229,7 @@ const SettingsFoldersMain: FC = ({ export default memo(withGlobal( (global): StateProps => { const { - chats: { byId: chatsById }, + chats: { listIds: allListIds, byId: chatsById }, users: { byId: usersById }, } = global; @@ -240,6 +240,7 @@ export default memo(withGlobal( } = global.chatFolders; return { + allListIds, chatsById, usersById, orderedFolderIds, diff --git a/src/modules/actions/api/chats.ts b/src/modules/actions/api/chats.ts index e7c1e06f2..a21d8d43a 100644 --- a/src/modules/actions/api/chats.ts +++ b/src/modules/actions/api/chats.ts @@ -76,7 +76,7 @@ addReducer('preloadTopChatMessages', (global, actions) => { } const { chatId: currentChatId } = selectCurrentMessageList(global) || {}; - const { pinnedChats, otherChats } = prepareChatList(byId, listIds, orderedPinnedIds); + const { pinnedChats, otherChats } = prepareChatList(byId, listIds, orderedPinnedIds, 'all', true); const topChats = [...pinnedChats, ...otherChats]; const chatToPreload = topChats.find(({ id }) => id !== currentChatId && !preloadedChatIds.includes(id)); if (!chatToPreload) { diff --git a/src/modules/helpers/chats.ts b/src/modules/helpers/chats.ts index de126723d..e659c5b3e 100644 --- a/src/modules/helpers/chats.ts +++ b/src/modules/helpers/chats.ts @@ -7,6 +7,7 @@ import { MAIN_THREAD_ID, } from '../../api/types'; +import { GlobalState } from '../../global/types'; import { NotifyException, NotifySettings } from '../../types'; import { LangFn } from '../../hooks/useLang'; @@ -269,17 +270,17 @@ export function getCanDeleteChat(chat: ApiChat) { } export function prepareFolderListIds( + allListIds: GlobalState['chats']['listIds'], chatsById: Record, usersById: Record, folder: ApiChatFolder, notifySettings: NotifySettings, notifyExceptions?: Record, - chatIdsCache?: string[], ) { const excludedChatIds = folder.excludedChatIds ? new Set(folder.excludedChatIds) : undefined; const includedChatIds = folder.excludedChatIds ? new Set(folder.includedChatIds) : undefined; const pinnedChatIds = folder.excludedChatIds ? new Set(folder.pinnedChatIds) : undefined; - const listIds = (chatIdsCache || Object.keys(chatsById)) + const listIds = ([] as string[]).concat(allListIds.active || [], allListIds.archived || []) .filter((id) => { return filterChatFolder( chatsById[id], @@ -296,6 +297,7 @@ export function prepareFolderListIds( return [listIds, folder.pinnedChatIds] as const; } +// This function is the most expensive in the project, so any possible optimizations are welcome function filterChatFolder( chat: ApiChat, folder: ApiChatFolder, @@ -310,51 +312,55 @@ function filterChatFolder( return false; } - if (excludedChatIds && excludedChatIds.has(chat.id)) { + const { id: chatId, type, unreadMentionsCount } = chat; + + if (excludedChatIds?.has(chatId)) { return false; } - if (includedChatIds && includedChatIds.has(chat.id)) { + if (includedChatIds?.has(chatId)) { return true; } - if (pinnedChatIds && pinnedChatIds.has(chat.id)) { + if (pinnedChatIds?.has(chatId)) { return true; } - if (isChatArchived(chat) && folder.excludeArchived) { + if (folder.excludeArchived && chat.folderId === ARCHIVED_FOLDER_ID) { return false; } - if (folder.excludeMuted && !chat.unreadMentionsCount && selectIsChatMuted(chat, notifySettings, notifyExceptions)) { + if (folder.excludeRead && !chat.unreadCount && !unreadMentionsCount && !chat.hasUnreadMark) { return false; } - if (!chat.unreadCount && !chat.unreadMentionsCount && !chat.hasUnreadMark && folder.excludeRead) { + if (folder.excludeMuted && !unreadMentionsCount && selectIsChatMuted(chat, notifySettings, notifyExceptions)) { return false; } - if (isUserId(chat.id)) { - const privateChatUser = usersById[chat.id]; + if (type === 'chatTypePrivate') { + const user = usersById[chatId]; + if (user) { + const { type: userType, isContact } = user; - const isChatWithBot = privateChatUser && privateChatUser.type === 'userTypeBot'; - if (isChatWithBot) { - if (folder.bots) { - return true; - } - } else { - if (folder.contacts && privateChatUser && privateChatUser.isContact) { - return true; - } + if (userType === 'userTypeBot') { + if (folder.bots) { + return true; + } + } else { + if (folder.contacts && isContact) { + return true; + } - if (folder.nonContacts && privateChatUser && !privateChatUser.isContact) { - return true; + if (folder.nonContacts && !isContact) { + return true; + } } } - } else if (isChatGroup(chat)) { - return !!folder.groups; - } else if (isChatChannel(chat)) { + } else if (type === 'chatTypeChannel') { return !!folder.channels; + } else if (type === 'chatTypeBasicGroup' || type === 'chatTypeSuperGroup') { + return !!folder.groups; } return false; @@ -365,6 +371,7 @@ export function prepareChatList( listIds: string[], orderedPinnedIds?: string[], folderType: 'all' | 'archived' | 'folder' = 'all', + noOrder = false, ) { const listIdsSet = new Set(listIds); const orderedPinnedIdsSet = orderedPinnedIds ? new Set(orderedPinnedIds) : undefined; @@ -372,7 +379,7 @@ export function prepareChatList( const pinnedChats = orderedPinnedIds?.reduce((acc, id) => { const chat = chatsById[id]; - if (chat && listIdsSet.has(chat.id) && chatFilter(chat, folderType)) { + if (chat && listIdsSet.has(chat.id) && checkChat(chat, folderType)) { acc.push(chat); } @@ -382,39 +389,25 @@ export function prepareChatList( const otherChats = listIds.reduce((acc, id) => { const chat = chatsById[id]; - if (chat && (!orderedPinnedIdsSet || !orderedPinnedIdsSet.has(chat.id)) && chatFilter(chat, folderType)) { + if (chat && (!orderedPinnedIdsSet || !orderedPinnedIdsSet.has(chat.id)) && checkChat(chat, folderType)) { acc.push(chat); } return acc; }, [] as ApiChat[]); - const otherChatsOrdered = orderBy(otherChats, getChatOrder, 'desc'); return { pinnedChats, - otherChats: otherChatsOrdered, + otherChats: noOrder ? otherChats : orderBy(otherChats, getChatOrder, 'desc'), }; } -function chatFilter(chat: ApiChat, folderType: 'all' | 'archived' | 'folder') { - if (!chat.lastMessage || chat.migratedTo) { - return false; - } - - switch (folderType) { - case 'all': - if (isChatArchived(chat)) { - return false; - } - break; - case 'archived': - if (!isChatArchived(chat)) { - return false; - } - break; - } - - return !chat.isRestricted && !chat.isNotJoined; +function checkChat(chat: ApiChat, folderType: 'all' | 'archived' | 'folder') { + return ( + chat.lastMessage && !chat.migratedTo && !chat.isRestricted && !chat.isNotJoined + && !(folderType === 'all' && chat.folderId === ARCHIVED_FOLDER_ID) + && !(folderType === 'archived' && chat.folderId !== ARCHIVED_FOLDER_ID) + ); } export function reduceChatList( @@ -430,26 +423,36 @@ export function reduceChatList( } export function getFolderUnreadDialogs( + allListIds: GlobalState['chats']['listIds'], chatsById: Record, usersById: Record, folder: ApiChatFolder, - chatIdsCache: string[], notifySettings: NotifySettings, notifyExceptions?: Record, ) { - const [listIds] = prepareFolderListIds(chatsById, usersById, folder, notifySettings, notifyExceptions, chatIdsCache); + const [listIds] = prepareFolderListIds(allListIds, chatsById, usersById, folder, notifySettings, notifyExceptions); - const listedChats = listIds - .map((id) => chatsById[id]) - .filter((chat) => (chat?.lastMessage && !chat.isRestricted && !chat.isNotJoined)); + let hasActiveDialogs = false; + const unreadDialogsCount = listIds.reduce((acc, id) => { + const chat = chatsById[id]; + if (!chat?.lastMessage || chat?.isRestricted || chat?.isNotJoined) { + return acc; + } - const unreadDialogsCount = listedChats - .reduce((total, chat) => (chat.unreadCount || chat.hasUnreadMark ? total + 1 : total), 0); + const isUnread = chat.unreadCount || chat.hasUnreadMark; - const hasActiveDialogs = listedChats.some((chat) => ( - chat.unreadMentionsCount - || (!selectIsChatMuted(chat, notifySettings, notifyExceptions) && (chat.unreadCount || chat.hasUnreadMark)) - )); + if (isUnread) { + acc++; + } + + if (!hasActiveDialogs && ( + chat.unreadMentionsCount || (isUnread && !selectIsChatMuted(chat, notifySettings, notifyExceptions)) + )) { + hasActiveDialogs = true; + } + + return acc; + }, 0); return { unreadDialogsCount, @@ -459,10 +462,10 @@ export function getFolderUnreadDialogs( export function getFolderDescriptionText( lang: LangFn, + allListIds: GlobalState['chats']['listIds'], chatsById: Record, usersById: Record, folder: ApiChatFolder, - chatIdsCache: string[], notifySettings: NotifySettings, notifyExceptions?: Record, ) { @@ -480,7 +483,7 @@ export function getFolderDescriptionText( || (excludedChatIds?.length) || (includedChatIds?.length) ) { - const length = getFolderChatsCount(chatsById, usersById, folder, chatIdsCache, notifySettings, notifyExceptions); + const length = getFolderChatsCount(allListIds, chatsById, usersById, folder, notifySettings, notifyExceptions); return lang('Chats', length); } @@ -501,17 +504,17 @@ export function getFolderDescriptionText( } function getFolderChatsCount( + allListIds: GlobalState['chats']['listIds'], chatsById: Record, usersById: Record, folder: ApiChatFolder, - chatIdsCache: string[], notifySettings: NotifySettings, notifyExceptions?: Record, ) { const [listIds, pinnedIds] = prepareFolderListIds( - chatsById, usersById, folder, notifySettings, notifyExceptions, chatIdsCache, + allListIds, chatsById, usersById, folder, notifySettings, notifyExceptions, ); - const { pinnedChats, otherChats } = prepareChatList(chatsById, listIds, pinnedIds, 'folder'); + const { pinnedChats, otherChats } = prepareChatList(chatsById, listIds, pinnedIds, 'folder', true); return pinnedChats.length + otherChats.length; }