diff --git a/src/components/common/UnreadCounter.tsx b/src/components/common/UnreadCounter.tsx new file mode 100644 index 000000000..6dfed89a8 --- /dev/null +++ b/src/components/common/UnreadCounter.tsx @@ -0,0 +1,31 @@ +import React, { FC, memo, useEffect } from '../../lib/teact/teact'; + +import { formatIntegerCompact } from '../../util/textFormat'; +import { useFolderManagerForUnreadCounters } from '../../hooks/useFolderManager'; +import { getAllNotificationsCount } from '../../util/folderManager'; +import { updateAppBadge } from '../../util/appBadge'; + +interface OwnProps { + isForAppBadge?: boolean; +} + +const UnreadCounter: FC = ({ isForAppBadge }) => { + useFolderManagerForUnreadCounters(); + const unreadNotificationsCount = getAllNotificationsCount(); + + useEffect(() => { + if (isForAppBadge) { + updateAppBadge(unreadNotificationsCount); + } + }, [isForAppBadge, unreadNotificationsCount]); + + if (isForAppBadge || !unreadNotificationsCount) { + return undefined; + } + + return ( +
{formatIntegerCompact(unreadNotificationsCount)}
+ ); +}; + +export default memo(UnreadCounter); diff --git a/src/components/left/main/ChatFolders.tsx b/src/components/left/main/ChatFolders.tsx index 57bd8e06d..264cd3096 100644 --- a/src/components/left/main/ChatFolders.tsx +++ b/src/components/left/main/ChatFolders.tsx @@ -3,27 +3,23 @@ import React, { } from '../../../lib/teact/teact'; import { getDispatch, withGlobal } from '../../../lib/teact/teactn'; -import { ApiChat, ApiChatFolder, ApiUser } from '../../../api/types'; -import { GlobalState } from '../../../global/types'; -import { NotifyException, NotifySettings, SettingsScreens } from '../../../types'; +import { ApiChatFolder } from '../../../api/types'; +import { SettingsScreens } from '../../../types'; import { FolderEditDispatch } from '../../../hooks/reducers/useFoldersReducer'; -import { IS_TOUCH_ENV } from '../../../util/environment'; import { ALL_FOLDER_ID } from '../../../config'; -import { buildCollectionByKey } from '../../../util/iteratees'; +import { IS_TOUCH_ENV } from '../../../util/environment'; import { captureEvents, SwipeDirection } from '../../../util/captureEvents'; -import { getFolderUnreadDialogs } from '../../../modules/helpers'; -import { selectNotifyExceptions, selectNotifySettings } from '../../../modules/selectors'; -import useShowTransition from '../../../hooks/useShowTransition'; import buildClassName from '../../../util/buildClassName'; -import useThrottledMemo from '../../../hooks/useThrottledMemo'; +import captureEscKeyListener from '../../../util/captureEscKeyListener'; +import useShowTransition from '../../../hooks/useShowTransition'; import useLang from '../../../hooks/useLang'; import useHistoryBack from '../../../hooks/useHistoryBack'; -import captureEscKeyListener from '../../../util/captureEscKeyListener'; import Transition from '../../ui/Transition'; import TabList from '../../ui/TabList'; import ChatList from './ChatList'; +import { useFolderManagerForUnreadCounters } from '../../../hooks/useFolderManager'; type OwnProps = { onScreenSelect: (screen: SettingsScreens) => void; @@ -31,12 +27,7 @@ type OwnProps = { }; type StateProps = { - allListIds: GlobalState['chats']['listIds']; - chatsById: Record; - usersById: Record; chatFoldersById: Record; - notifySettings: NotifySettings; - notifyExceptions?: Record; orderedFolderIds?: number[]; activeChatFolder: number; currentUserId?: string; @@ -44,23 +35,17 @@ type StateProps = { shouldSkipHistoryAnimations?: boolean; }; -const INFO_THROTTLE = 3000; const SAVED_MESSAGES_HOTKEY = '0'; const ChatFolders: FC = ({ - allListIds, - chatsById, - usersById, + foldersDispatch, + onScreenSelect, chatFoldersById, - notifySettings, - notifyExceptions, orderedFolderIds, activeChatFolder, currentUserId, lastSyncTime, shouldSkipHistoryAnimations, - foldersDispatch, - onScreenSelect, }) => { const { loadChatFolders, @@ -85,36 +70,22 @@ const ChatFolders: FC = ({ : undefined; }, [chatFoldersById, orderedFolderIds]); - const folderCountersById = useThrottledMemo(() => { - if (!displayedFolders || !displayedFolders.length) { - return undefined; - } - - const counters = displayedFolders.map((folder) => { - const { - unreadDialogsCount, hasActiveDialogs, - } = getFolderUnreadDialogs(allListIds, chatsById, usersById, folder, notifySettings, notifyExceptions) || {}; - - return { - id: folder.id, - badgeCount: unreadDialogsCount, - isBadgeActive: hasActiveDialogs, - }; - }); - - return buildCollectionByKey(counters, 'id'); - }, INFO_THROTTLE, [displayedFolders, allListIds, chatsById, usersById, notifySettings, notifyExceptions]); - + const folderCountersById = useFolderManagerForUnreadCounters(); const folderTabs = useMemo(() => { if (!displayedFolders || !displayedFolders.length) { return undefined; } return [ - { title: lang.code === 'en' ? 'All' : lang('FilterAllChats'), id: ALL_FOLDER_ID }, - ...displayedFolders.map((folder) => ({ - title: folder.title, - ...(folderCountersById?.[folder.id]), + { + id: ALL_FOLDER_ID, + title: lang.code === 'en' ? 'All' : lang('FilterAllChats'), + }, + ...displayedFolders.map(({ id, title }) => ({ + id, + title, + badgeCount: folderCountersById[id]?.chatsCount, + isBadgeActive: Boolean(folderCountersById[id]?.notificationsCount), })), ]; }, [displayedFolders, folderCountersById, lang]); @@ -204,6 +175,7 @@ const ChatFolders: FC = ({ @@ -215,6 +187,7 @@ const ChatFolders: FC = ({ folderType="folder" folderId={activeFolder.id} isActive={isActive} + lastSyncTime={lastSyncTime} onScreenSelect={onScreenSelect} foldersDispatch={foldersDispatch} /> @@ -243,8 +216,6 @@ const ChatFolders: FC = ({ export default memo(withGlobal( (global): StateProps => { const { - chats: { listIds: allListIds, byId: chatsById }, - users: { byId: usersById }, chatFolders: { byId: chatFoldersById, orderedIds: orderedFolderIds, @@ -256,16 +227,11 @@ export default memo(withGlobal( } = global; return { - allListIds, - chatsById, - usersById, chatFoldersById, orderedFolderIds, - lastSyncTime, - notifySettings: selectNotifySettings(global), - notifyExceptions: selectNotifyExceptions(global), activeChatFolder, currentUserId, + lastSyncTime, shouldSkipHistoryAnimations, }; }, diff --git a/src/components/left/main/ChatList.tsx b/src/components/left/main/ChatList.tsx index 42576f502..2cdaf77c7 100644 --- a/src/components/left/main/ChatList.tsx +++ b/src/components/left/main/ChatList.tsx @@ -1,26 +1,24 @@ import React, { FC, memo, useMemo, useCallback, useEffect, } from '../../../lib/teact/teact'; -import { getDispatch, withGlobal } from '../../../lib/teact/teactn'; +import { getDispatch } from '../../../lib/teact/teactn'; -import { GlobalState } from '../../../global/types'; -import { - ApiChat, ApiChatFolder, ApiUser, -} from '../../../api/types'; -import { NotifyException, NotifySettings, SettingsScreens } from '../../../types'; +import { SettingsScreens } from '../../../types'; import { FolderEditDispatch } from '../../../hooks/reducers/useFoldersReducer'; -import { ALL_CHATS_PRELOAD_DISABLED, CHAT_HEIGHT_PX, CHAT_LIST_SLICE } from '../../../config'; +import { + ALL_CHATS_PRELOAD_DISABLED, + ALL_FOLDER_ID, + ARCHIVED_FOLDER_ID, + CHAT_HEIGHT_PX, + CHAT_LIST_SLICE, +} from '../../../config'; import { IS_ANDROID, IS_MAC_OS, IS_PWA } from '../../../util/environment'; -import usePrevious from '../../../hooks/usePrevious'; import { mapValues } from '../../../util/iteratees'; -import { - getChatOrder, prepareChatList, prepareFolderListIds, reduceChatList, -} from '../../../modules/helpers'; -import { - selectChatFolder, selectNotifyExceptions, selectNotifySettings, -} from '../../../modules/selectors'; +import { getPinnedChatsCount } from '../../../util/folderManager'; +import usePrevious from '../../../hooks/usePrevious'; import useInfiniteScroll from '../../../hooks/useInfiniteScroll'; +import { useFolderManagerForOrderedIds } from '../../../hooks/useFolderManager'; import { useChatAnimationType } from './hooks'; import InfiniteScroll from '../../ui/InfiniteScroll'; @@ -32,40 +30,16 @@ type OwnProps = { folderType: 'all' | 'archived' | 'folder'; folderId?: number; isActive: boolean; - onScreenSelect?: (screen: SettingsScreens) => void; - foldersDispatch?: FolderEditDispatch; -}; - -type StateProps = { - allListIds: GlobalState['chats']['listIds']; - chatsById: Record; - usersById: Record; - listIds?: string[]; - orderedPinnedIds?: string[]; - chatFolder?: ApiChatFolder; lastSyncTime?: number; - notifySettings: NotifySettings; - notifyExceptions?: Record; + foldersDispatch?: FolderEditDispatch; + onScreenSelect?: (screen: SettingsScreens) => void; }; -enum FolderTypeToListType { - 'all' = 'active', - 'archived' = 'archived', -} - -const ChatList: FC = ({ +const ChatList: FC = ({ folderType, folderId, isActive, - allListIds, - chatsById, - usersById, - listIds, - orderedPinnedIds, - chatFolder, lastSyncTime, - notifySettings, - notifyExceptions, foldersDispatch, onScreenSelect, }) => { @@ -77,30 +51,22 @@ const ChatList: FC = ({ openNextChat, } = getDispatch(); - const [currentListIds, currentPinnedIds] = useMemo(() => { - return folderType === 'folder' && chatFolder - ? prepareFolderListIds(allListIds, chatsById, usersById, chatFolder, notifySettings, notifyExceptions) - : [listIds, orderedPinnedIds]; - }, [ - folderType, chatFolder, allListIds, chatsById, usersById, - notifySettings, notifyExceptions, listIds, orderedPinnedIds, - ]); + const virtualFolderId = ( + folderType === 'all' ? ALL_FOLDER_ID : folderType === 'archived' ? ARCHIVED_FOLDER_ID : folderId! + ); - const [orderById, orderedIds, chatArrays] = useMemo(() => { - if (!currentListIds || (folderType === 'folder' && !chatFolder)) { - return []; + const orderedIds = useFolderManagerForOrderedIds(virtualFolderId); + + const orderById = useMemo(() => { + if (!orderedIds) { + return undefined; } - const newChatArrays = prepareChatList(chatsById, currentListIds, currentPinnedIds, folderType); - const singleList = ([] as ApiChat[]).concat(newChatArrays.pinnedChats, newChatArrays.otherChats); - const newOrderedIds = singleList.map(({ id }) => id); - const newOrderById = singleList.reduce((acc, chat, i) => { - acc[chat.id] = i; + return orderedIds.reduce((acc, id, i) => { + acc[id] = i; return acc; }, {} as Record); - - return [newOrderById, newOrderedIds, newChatArrays]; - }, [currentListIds, currentPinnedIds, folderType, chatFolder, chatsById]); + }, [orderedIds]); const prevOrderById = usePrevious(orderById); @@ -126,14 +92,6 @@ const ChatList: FC = ({ folderType === 'all' && !ALL_CHATS_PRELOAD_DISABLED, ); - const viewportChatArrays = useMemo(() => { - if (!viewportIds || !chatArrays) { - return undefined; - } - - return reduceChatList(chatArrays, viewportIds); - }, [chatArrays, viewportIds]); - useEffect(() => { if (lastSyncTime && folderType === 'all') { preloadTopChatMessages(); @@ -178,7 +136,7 @@ const ChatList: FC = ({ function renderChats() { const viewportOffset = orderedIds!.indexOf(viewportIds![0]); - const pinnedOffset = viewportOffset + viewportChatArrays!.pinnedChats.length; + const pinnedCount = getPinnedChatsCount(virtualFolderId) || 0; return (
= ({ style={IS_ANDROID ? `height: ${orderedIds!.length * CHAT_HEIGHT_PX}px` : undefined} teactFastList > - {viewportChatArrays!.pinnedChats.map(({ id }, i) => ( + {viewportIds!.map((id, i) => ( = ({ style={`top: ${(viewportOffset + i) * CHAT_HEIGHT_PX}px;`} /> ))} - {viewportChatArrays!.otherChats.map((chat, i) => ( - - ))}
); } @@ -225,7 +171,7 @@ const ChatList: FC = ({ noFastList noScrollRestore > - {viewportIds?.length && viewportChatArrays ? ( + {viewportIds?.length ? ( renderChats() ) : viewportIds && !viewportIds.length ? ( ( @@ -243,33 +189,4 @@ const ChatList: FC = ({ ); }; -export default memo(withGlobal( - (global, { folderType, folderId }): StateProps => { - const { - chats: { - listIds, - byId: chatsById, - orderedPinnedIds, - }, - users: { byId: usersById }, - lastSyncTime, - } = global; - const listType = folderType !== 'folder' ? FolderTypeToListType[folderType] : undefined; - const chatFolder = folderId ? selectChatFolder(global, folderId) : undefined; - - return { - allListIds: listIds, - chatsById, - usersById, - lastSyncTime, - notifySettings: selectNotifySettings(global), - notifyExceptions: selectNotifyExceptions(global), - ...(listType ? { - listIds: listIds[listType], - orderedPinnedIds: orderedPinnedIds[listType], - } : { - chatFolder, - }), - }; - }, -)(ChatList)); +export default memo(ChatList); diff --git a/src/components/left/settings/SettingsPrivacyVisibilityExceptionList.tsx b/src/components/left/settings/SettingsPrivacyVisibilityExceptionList.tsx index 974ed4a15..746585fe7 100644 --- a/src/components/left/settings/SettingsPrivacyVisibilityExceptionList.tsx +++ b/src/components/left/settings/SettingsPrivacyVisibilityExceptionList.tsx @@ -1,19 +1,18 @@ import React, { FC, memo, useCallback, useMemo, useState, } from '../../../lib/teact/teact'; -import { getDispatch, withGlobal } from '../../../lib/teact/teactn'; +import { getDispatch, getGlobal, withGlobal } from '../../../lib/teact/teactn'; import { GlobalState } from '../../../global/types'; -import { ApiChat } from '../../../api/types'; import { ApiPrivacySettings, SettingsScreens } from '../../../types'; +import { ALL_FOLDER_ID, ARCHIVED_FOLDER_ID } from '../../../config'; +import { unique } from '../../../util/iteratees'; +import { filterChatsByName, isChatGroup, isUserId } from '../../../modules/helpers'; import useLang from '../../../hooks/useLang'; -import searchWords from '../../../util/searchWords'; -import { getPrivacyKey } from './helper/privacy'; -import { - getChatTitle, isChatGroup, isUserId, prepareChatList, -} from '../../../modules/helpers'; import useHistoryBack from '../../../hooks/useHistoryBack'; +import { useFolderManagerForOrderedIds } from '../../../hooks/useFolderManager'; +import { getPrivacyKey } from './helper/privacy'; import Picker from '../../common/Picker'; import FloatingActionButton from '../../ui/FloatingActionButton'; @@ -28,27 +27,17 @@ export type OwnProps = { type StateProps = { currentUserId?: string; - chatsById: Record; - listIds?: string[]; - orderedPinnedIds?: string[]; - archivedListIds?: string[]; - archivedPinnedIds?: string[]; settings?: ApiPrivacySettings; }; const SettingsPrivacyVisibilityExceptionList: FC = ({ - currentUserId, isAllowList, screen, - settings, - chatsById, - listIds, - orderedPinnedIds, - archivedListIds, - archivedPinnedIds, isActive, onScreenSelect, onReset, + currentUserId, + settings, }) => { const { setPrivacySettings } = getDispatch(); @@ -69,46 +58,23 @@ const SettingsPrivacyVisibilityExceptionList: FC = ({ const [isSubmitShown, setIsSubmitShown] = useState(false); const [newSelectedContactIds, setNewSelectedContactIds] = useState(selectedContactIds); - const chats = useMemo(() => { - const activeChatArrays = listIds - ? prepareChatList(chatsById, listIds, orderedPinnedIds, 'all') - : undefined; - const archivedChatArrays = archivedListIds - ? prepareChatList(chatsById, archivedListIds, archivedPinnedIds, 'archived') - : undefined; - - if (!activeChatArrays && !archivedChatArrays) { - return undefined; - } - - return [ - ...(activeChatArrays - ? [ - ...activeChatArrays.pinnedChats, - ...activeChatArrays.otherChats, - ] - : [] - ), - ...(archivedChatArrays ? archivedChatArrays.otherChats : []), - ]; - }, [chatsById, listIds, orderedPinnedIds, archivedListIds, archivedPinnedIds]); - + const folderAllOrderedIds = useFolderManagerForOrderedIds(ALL_FOLDER_ID); + const folderArchivedOrderedIds = useFolderManagerForOrderedIds(ARCHIVED_FOLDER_ID); const displayedIds = useMemo(() => { - if (!chats) { - return undefined; - } + // No need for expensive global updates on chats, so we avoid them + const chatsById = getGlobal().chats.byId; - return chats - .filter((chat) => ( - ((isUserId(chat.id) && chat.id !== currentUserId) || isChatGroup(chat)) - && ( - !searchQuery - || searchWords(getChatTitle(lang, chat), searchQuery) - || selectedContactIds.includes(chat.id) - ) - )) - .map(({ id }) => id); - }, [chats, currentUserId, lang, searchQuery, selectedContactIds]); + const chatIds = unique([...folderAllOrderedIds, ...folderArchivedOrderedIds]) + .filter((chatId) => { + const chat = chatsById[chatId]; + return chat && ((isUserId(chat.id) && chat.id !== currentUserId) || isChatGroup(chat)); + }); + + return unique([ + ...selectedContactIds, + ...filterChatsByName(lang, chatIds, chatsById, searchQuery), + ]); + }, [folderAllOrderedIds, folderArchivedOrderedIds, selectedContactIds, lang, searchQuery, currentUserId]); const handleSelectedContactIdsChange = useCallback((value: string[]) => { setNewSelectedContactIds(value); @@ -175,22 +141,8 @@ function getCurrentPrivacySettings(global: GlobalState, screen: SettingsScreens) export default memo(withGlobal( (global, { screen }): StateProps => { - const { - chats: { - byId: chatsById, - listIds, - orderedPinnedIds, - }, - currentUserId, - } = global; - return { - currentUserId, - chatsById, - listIds: listIds.active, - orderedPinnedIds: orderedPinnedIds.active, - archivedPinnedIds: orderedPinnedIds.archived, - archivedListIds: listIds.archived, + currentUserId: global.currentUserId, settings: getCurrentPrivacySettings(global, screen), }; }, diff --git a/src/components/left/settings/folders/SettingsFoldersChatFilters.tsx b/src/components/left/settings/folders/SettingsFoldersChatFilters.tsx index 3585614d0..b454532a8 100644 --- a/src/components/left/settings/folders/SettingsFoldersChatFilters.tsx +++ b/src/components/left/settings/folders/SettingsFoldersChatFilters.tsx @@ -1,23 +1,24 @@ import React, { FC, memo, useMemo, useCallback, } from '../../../../lib/teact/teact'; -import { getDispatch, withGlobal } from '../../../../lib/teact/teactn'; +import { getDispatch, getGlobal } from '../../../../lib/teact/teactn'; -import { ApiChat } from '../../../../api/types'; import { SettingsScreens } from '../../../../types'; +import { unique } from '../../../../util/iteratees'; + +import { ALL_FOLDER_ID, ARCHIVED_FOLDER_ID } from '../../../../config'; +import { filterChatsByName } from '../../../../modules/helpers'; import useLang from '../../../../hooks/useLang'; -import searchWords from '../../../../util/searchWords'; -import { prepareChatList, getChatTitle } from '../../../../modules/helpers'; +import useHistoryBack from '../../../../hooks/useHistoryBack'; +import { useFolderManagerForOrderedIds } from '../../../../hooks/useFolderManager'; import { FoldersState, FolderEditDispatch, selectChatFilters, } from '../../../../hooks/reducers/useFoldersReducer'; -import useHistoryBack from '../../../../hooks/useHistoryBack'; import SettingsFoldersChatsPicker from './SettingsFoldersChatsPicker'; - import Loading from '../../../ui/Loading'; type OwnProps = { @@ -29,26 +30,13 @@ type OwnProps = { onReset: () => void; }; -type StateProps = { - chatsById: Record; - listIds?: string[]; - orderedPinnedIds?: string[]; - archivedListIds?: string[]; - archivedPinnedIds?: string[]; -}; - -const SettingsFoldersChatFilters: FC = ({ - isActive, - onScreenSelect, - onReset, +const SettingsFoldersChatFilters: FC = ({ mode, state, dispatch, - chatsById, - listIds, - orderedPinnedIds, - archivedListIds, - archivedPinnedIds, + isActive, + onScreenSelect, + onReset, }) => { const { loadMoreChats } = getDispatch(); @@ -56,38 +44,20 @@ const SettingsFoldersChatFilters: FC = ({ const { selectedChatIds, selectedChatTypes } = selectChatFilters(state, mode, true); const lang = useLang(); - const chats = useMemo(() => { - const activeChatArrays = listIds - ? prepareChatList(chatsById, listIds, orderedPinnedIds, 'all') - : undefined; - const archivedChatArrays = archivedListIds - ? prepareChatList(chatsById, archivedListIds, archivedPinnedIds, 'archived') - : undefined; - if (!activeChatArrays && !archivedChatArrays) { - return undefined; - } - - return [ - ...(activeChatArrays?.pinnedChats || []), - ...(activeChatArrays?.otherChats || []), - ...(archivedChatArrays?.otherChats || []), - ]; - }, [chatsById, listIds, orderedPinnedIds, archivedListIds, archivedPinnedIds]); + const folderAllOrderedIds = useFolderManagerForOrderedIds(ALL_FOLDER_ID); + const folderArchivedOrderedIds = useFolderManagerForOrderedIds(ARCHIVED_FOLDER_ID); const displayedIds = useMemo(() => { - if (!chats) { - return undefined; - } + // No need for expensive global updates on chats, so we avoid them + const chatsById = getGlobal().chats.byId; - return chats - .filter((chat) => ( - !chatFilter - || searchWords(getChatTitle(lang, chat), chatFilter) - || selectedChatIds.includes(chat.id) - )) - .map(({ id }) => id); - }, [chats, chatFilter, lang, selectedChatIds]); + const chatIds = [...folderAllOrderedIds, ...folderArchivedOrderedIds]; + return unique([ + ...selectedChatIds, + ...filterChatsByName(lang, chatIds, chatsById, chatFilter), + ]); + }, [folderAllOrderedIds, folderArchivedOrderedIds, selectedChatIds, lang, chatFilter]); const handleFilterChange = useCallback((newFilter: string) => { dispatch({ @@ -159,22 +129,4 @@ const SettingsFoldersChatFilters: FC = ({ ); }; -export default memo(withGlobal( - (global): StateProps => { - const { - chats: { - byId: chatsById, - listIds, - orderedPinnedIds, - }, - } = global; - - return { - chatsById, - listIds: listIds.active, - orderedPinnedIds: orderedPinnedIds.active, - archivedPinnedIds: orderedPinnedIds.archived, - archivedListIds: listIds.archived, - }; - }, -)(SettingsFoldersChatFilters)); +export default memo(SettingsFoldersChatFilters); diff --git a/src/components/left/settings/folders/SettingsFoldersMain.tsx b/src/components/left/settings/folders/SettingsFoldersMain.tsx index 39c835a38..ae0a49118 100644 --- a/src/components/left/settings/folders/SettingsFoldersMain.tsx +++ b/src/components/left/settings/folders/SettingsFoldersMain.tsx @@ -3,17 +3,16 @@ import React, { } from '../../../../lib/teact/teact'; import { getDispatch, withGlobal } from '../../../../lib/teact/teactn'; -import { GlobalState } from '../../../../global/types'; -import { ApiChatFolder, ApiChat, ApiUser } from '../../../../api/types'; -import { NotifyException, NotifySettings, SettingsScreens } from '../../../../types'; +import { ApiChatFolder } from '../../../../api/types'; +import { SettingsScreens } from '../../../../types'; import { STICKER_SIZE_FOLDER_SETTINGS } from '../../../../config'; -import { selectNotifyExceptions, selectNotifySettings } from '../../../../modules/selectors'; import { throttle } from '../../../../util/schedulers'; import getAnimationData from '../../../common/helpers/animatedAssets'; import { getFolderDescriptionText } from '../../../../modules/helpers'; import useLang from '../../../../hooks/useLang'; import useHistoryBack from '../../../../hooks/useHistoryBack'; +import { useFolderManagerForChatsCount } from '../../../../hooks/useFolderManager'; import ListItem from '../../../ui/ListItem'; import Button from '../../../ui/Button'; @@ -29,14 +28,9 @@ type OwnProps = { }; type StateProps = { - allListIds: GlobalState['chats']['listIds']; - chatsById: Record; - usersById: Record; orderedFolderIds?: number[]; foldersById: Record; recommendedChatFolders?: ApiChatFolder[]; - notifySettings: NotifySettings; - notifyExceptions?: Record; }; const runThrottledForLoadRecommended = throttle((cb) => cb(), 60000, true); @@ -45,18 +39,13 @@ const MAX_ALLOWED_FOLDERS = 10; const SettingsFoldersMain: FC = ({ isActive, - allListIds, - chatsById, - usersById, - orderedFolderIds, - foldersById, - recommendedChatFolders, - notifySettings, - notifyExceptions, onCreateFolder, onEditFolder, onScreenSelect, onReset, + orderedFolderIds, + foldersById, + recommendedChatFolders, }) => { const { loadRecommendedChatFolders, @@ -101,6 +90,7 @@ const SettingsFoldersMain: FC = ({ useHistoryBack(isActive, onReset, onScreenSelect, SettingsScreens.Folders); + const chatsCountByFolderId = useFolderManagerForChatsCount(); const userFolders = useMemo(() => { if (!orderedFolderIds) { return undefined; @@ -112,12 +102,10 @@ const SettingsFoldersMain: FC = ({ return { id: folder.id, title: folder.title, - subtitle: getFolderDescriptionText( - lang, allListIds, chatsById, usersById, folder, notifySettings, notifyExceptions, - ), + subtitle: getFolderDescriptionText(lang, folder, chatsCountByFolderId[folder.id]), }; }); - }, [lang, allListIds, foldersById, chatsById, usersById, orderedFolderIds, notifySettings, notifyExceptions]); + }, [orderedFolderIds, foldersById, lang, chatsCountByFolderId]); const handleCreateFolderFromRecommended = useCallback((folder: ApiChatFolder) => { if (Object.keys(foldersById).length >= MAX_ALLOWED_FOLDERS) { @@ -228,11 +216,6 @@ const SettingsFoldersMain: FC = ({ export default memo(withGlobal( (global): StateProps => { - const { - chats: { listIds: allListIds, byId: chatsById }, - users: { byId: usersById }, - } = global; - const { orderedIds: orderedFolderIds, byId: foldersById, @@ -240,14 +223,9 @@ export default memo(withGlobal( } = global.chatFolders; return { - allListIds, - chatsById, - usersById, orderedFolderIds, foldersById, recommendedChatFolders, - notifySettings: selectNotifySettings(global), - notifyExceptions: selectNotifyExceptions(global), }; }, )(SettingsFoldersMain)); diff --git a/src/components/main/Main.tsx b/src/components/main/Main.tsx index ef73d96e0..d6db19493 100644 --- a/src/components/main/Main.tsx +++ b/src/components/main/Main.tsx @@ -1,7 +1,7 @@ import React, { FC, useEffect, memo, useCallback, } from '../../lib/teact/teact'; -import { getDispatch, getGlobal, withGlobal } from '../../lib/teact/teactn'; +import { getDispatch, withGlobal } from '../../lib/teact/teactn'; import { LangCode } from '../../types'; import { ApiMessage } from '../../api/types'; @@ -12,7 +12,6 @@ import { } from '../../config'; import { selectChatMessage, - selectCountNotMutedUnreadOptimized, selectIsForwardModalOpen, selectIsMediaViewerOpen, selectIsRightColumnShown, @@ -25,6 +24,7 @@ import { waitForTransitionEnd } from '../../util/cssAnimationEndListeners'; import { processDeepLink } from '../../util/deeplink'; import stopEvent from '../../util/stopEvent'; import windowSize from '../../util/windowSize'; +import { getAllNotificationsCount } from '../../util/folderManager'; import useShowTransition from '../../hooks/useShowTransition'; import useBackgroundMode from '../../hooks/useBackgroundMode'; import useBeforeUnload from '../../hooks/useBeforeUnload'; @@ -32,6 +32,8 @@ import useOnChange from '../../hooks/useOnChange'; import usePreventPinchZoomGesture from '../../hooks/usePreventPinchZoomGesture'; import { LOCATION_HASH } from '../../hooks/useHistoryBack'; +import StickerSetModal from '../common/StickerSetModal.async'; +import UnreadCount from '../common/UnreadCounter'; import LeftColumn from '../left/LeftColumn'; import MiddleColumn from '../middle/MiddleColumn'; import RightColumn from '../right/RightColumn'; @@ -43,7 +45,6 @@ import Dialogs from './Dialogs.async'; import ForwardPicker from './ForwardPicker.async'; import SafeLinkModal from './SafeLinkModal.async'; import HistoryCalendar from './HistoryCalendar.async'; -import StickerSetModal from '../common/StickerSetModal.async'; import GroupCall from '../calls/group/GroupCall.async'; import ActiveCallHeader from '../calls/ActiveCallHeader.async'; import CallFallbackConfirm from '../calls/CallFallbackConfirm.async'; @@ -248,7 +249,7 @@ const Main: FC = ({ const handleBlur = useCallback(() => { updateIsOnline(false); - const initialUnread = selectCountNotMutedUnreadOptimized(getGlobal()); + const initialUnread = getAllNotificationsCount(); let index = 0; clearInterval(notificationInterval); @@ -259,7 +260,7 @@ const Main: FC = ({ } if (index % 2 === 0) { - const newUnread = selectCountNotMutedUnreadOptimized(getGlobal()) - initialUnread; + const newUnread = getAllNotificationsCount() - initialUnread; if (newUnread > 0) { updatePageTitle(`${newUnread} notification${newUnread > 1 ? 's' : ''}`); updateIcon(true); @@ -321,6 +322,7 @@ const Main: FC = ({ )} + ); }; diff --git a/src/components/middle/MiddleHeader.tsx b/src/components/middle/MiddleHeader.tsx index 9945f330c..6bcd1d2d3 100644 --- a/src/components/middle/MiddleHeader.tsx +++ b/src/components/middle/MiddleHeader.tsx @@ -46,13 +46,13 @@ import useConnectionStatus from '../../hooks/useConnectionStatus'; import PrivateChatInfo from '../common/PrivateChatInfo'; import GroupChatInfo from '../common/GroupChatInfo'; +import UnreadCounter from '../common/UnreadCounter'; import Transition from '../ui/Transition'; import Button from '../ui/Button'; import HeaderActions from './HeaderActions'; import HeaderPinnedMessage from './HeaderPinnedMessage'; import AudioPlayer from './AudioPlayer'; import GroupCallTopPane from '../calls/group/GroupCallTopPane'; -import UnreadCount from './UnreadCount'; import './MiddleHeader.scss'; @@ -346,7 +346,7 @@ const MiddleHeader: FC = ({ ); } - function renderBackButton(asClose = false, withUnreadCount = false) { + function renderBackButton(asClose = false, withUnreadCounter = false) { return (
- {withUnreadCount && } + {withUnreadCounter && }
); } diff --git a/src/components/middle/UnreadCount.tsx b/src/components/middle/UnreadCount.tsx deleted file mode 100644 index af3cf3982..000000000 --- a/src/components/middle/UnreadCount.tsx +++ /dev/null @@ -1,31 +0,0 @@ -import React, { FC, memo } from '../../lib/teact/teact'; -import { withGlobal } from '../../lib/teact/teactn'; - -import { GlobalState } from '../../global/types'; - -import { selectCountNotMutedUnreadOptimized } from '../../modules/selectors'; -import { formatIntegerCompact } from '../../util/textFormat'; - -type StateProps = { - unreadCount: number; -}; - -const UnreadCount: FC = ({ - unreadCount, -}) => { - if (!unreadCount) { - return undefined; - } - - return ( -
{formatIntegerCompact(unreadCount)}
- ); -}; - -export default memo(withGlobal( - (global: GlobalState): StateProps => { - return { - unreadCount: selectCountNotMutedUnreadOptimized(global), - }; - }, -)(UnreadCount)); diff --git a/src/hooks/useFolderManager.ts b/src/hooks/useFolderManager.ts new file mode 100644 index 000000000..3d6b000a7 --- /dev/null +++ b/src/hooks/useFolderManager.ts @@ -0,0 +1,33 @@ +import { useEffect, useState } from '../lib/teact/teact'; +import { + getOrderedIds, + getUnreadCounters, + getChatsCount, + addOrderedIdsCallback, + addUnreadCountersCallback, + addChatsCountCallback, +} from '../util/folderManager'; + +export function useFolderManagerForOrderedIds(folderId: number) { + const [orderedIds, setOrderedIds] = useState(getOrderedIds(folderId)); + + useEffect(() => addOrderedIdsCallback(folderId, setOrderedIds), [folderId]); + + return orderedIds; +} + +export function useFolderManagerForUnreadCounters() { + const [unreadCounters, setUnreadCounters] = useState(getUnreadCounters()); + + useEffect(() => addUnreadCountersCallback(setUnreadCounters), []); + + return unreadCounters; +} + +export function useFolderManagerForChatsCount() { + const [chatsCount, setChatsCount] = useState(getChatsCount()); + + useEffect(() => addChatsCountCallback(setChatsCount), []); + + return chatsCount; +} diff --git a/src/modules/actions/api/chats.ts b/src/modules/actions/api/chats.ts index 0c315ad84..409d82866 100644 --- a/src/modules/actions/api/chats.ts +++ b/src/modules/actions/api/chats.ts @@ -16,7 +16,7 @@ import { LOCALIZED_TIPS, RE_TG_LINK, SERVICE_NOTIFICATIONS_USER_ID, - TMP_CHAT_ID, + TMP_CHAT_ID, ALL_FOLDER_ID, } from '../../../config'; import { callApi } from '../../../api/gramjs'; import { @@ -46,11 +46,12 @@ import { import { buildCollectionByKey, omit } from '../../../util/iteratees'; import { debounce, pause, throttle } from '../../../util/schedulers'; import { - isChatSummaryOnly, isChatArchived, prepareChatList, isChatBasicGroup, + isChatSummaryOnly, isChatArchived, isChatBasicGroup, } from '../../helpers'; import { processDeepLink } from '../../../util/deeplink'; import { updateGroupCall } from '../../reducers/calls'; import { selectGroupCall } from '../../selectors/calls'; +import { getOrderedIds } from '../../../util/folderManager'; const TOP_CHAT_MESSAGES_PRELOAD_INTERVAL = 100; const CHATS_PRELOAD_INTERVAL = 300; @@ -61,31 +62,21 @@ const runDebouncedForLoadFullChat = debounce((cb) => cb(), 500, false, true); addReducer('preloadTopChatMessages', (global, actions) => { (async () => { - const preloadedChatIds: string[] = []; + const preloadedChatIds = new Set(); for (let i = 0; i < TOP_CHAT_MESSAGES_PRELOAD_LIMIT; i++) { await pause(TOP_CHAT_MESSAGES_PRELOAD_INTERVAL); - const { - byId, - listIds: { active: listIds }, - orderedPinnedIds: { active: orderedPinnedIds }, - } = getGlobal().chats; - if (!listIds) { - return; - } - const { chatId: currentChatId } = selectCurrentMessageList(global) || {}; - 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) { + const folderAllOrderedIds = getOrderedIds(ALL_FOLDER_ID); + const nextChatId = folderAllOrderedIds?.find((id) => id !== currentChatId && !preloadedChatIds.has(id)); + if (!nextChatId) { return; } - preloadedChatIds.push(chatToPreload.id); + preloadedChatIds.add(nextChatId); - actions.loadViewportMessages({ chatId: chatToPreload.id, threadId: MAIN_THREAD_ID }); + actions.loadViewportMessages({ chatId: nextChatId, threadId: MAIN_THREAD_ID }); } })(); }); diff --git a/src/modules/actions/api/sync.ts b/src/modules/actions/api/sync.ts index 0a5c26a3c..3f6562cae 100644 --- a/src/modules/actions/api/sync.ts +++ b/src/modules/actions/api/sync.ts @@ -11,7 +11,6 @@ import { } from '../../../config'; import { callApi } from '../../../api/gramjs'; import { buildCollectionByKey } from '../../../util/iteratees'; -import { updateAppBadge } from '../../../util/appBadge'; import { replaceChatListIds, replaceChats, @@ -34,7 +33,6 @@ import { selectDraft, selectChatMessage, selectThreadInfo, - selectCountNotMutedUnreadOptimized, selectLastServiceNotification, } from '../../selectors'; import { isUserId } from '../../helpers'; @@ -102,8 +100,6 @@ async function afterSync() { await callApi('fetchCurrentUser'); - updateAppBadge(selectCountNotMutedUnreadOptimized(getGlobal())); - if (DEBUG) { // eslint-disable-next-line no-console console.log('>>> FINISH AFTER-SYNC'); diff --git a/src/modules/actions/apiUpdaters/chats.ts b/src/modules/actions/apiUpdaters/chats.ts index 875a6971b..2ea8a2057 100644 --- a/src/modules/actions/apiUpdaters/chats.ts +++ b/src/modules/actions/apiUpdaters/chats.ts @@ -5,7 +5,6 @@ import { ApiUpdate, MAIN_THREAD_ID } from '../../../api/types'; import { ARCHIVED_FOLDER_ID, MAX_ACTIVE_PINNED_CHATS } from '../../../config'; import { pick } from '../../../util/iteratees'; import { closeMessageNotifications, notifyAboutNewMessage } from '../../../util/notifications'; -import { updateAppBadge } from '../../../util/appBadge'; import { updateChat, updateChatListIds, @@ -19,16 +18,12 @@ import { selectIsChatListed, selectChatListType, selectCurrentMessageList, - selectCountNotMutedUnreadOptimized, } from '../../selectors'; -import { throttle } from '../../../util/schedulers'; const TYPING_STATUS_CLEAR_DELAY = 6000; // 6 seconds // Enough to animate and mark as read in Message List const CURRENT_CHAT_UNREAD_DELAY = 1500; -const runThrottledForUpdateAppBadge = throttle((cb) => cb(), 500, true); - addReducer('apiUpdate', (global, actions, update: ApiUpdate) => { switch (update['@type']) { case 'updateChat': { @@ -40,8 +35,6 @@ addReducer('apiUpdate', (global, actions, update: ApiUpdate) => { const newGlobal = updateChat(global, update.id, update.chat, update.newProfilePhoto); setGlobal(newGlobal); - runThrottledForUpdateAppBadge(() => updateAppBadge(selectCountNotMutedUnreadOptimized(getGlobal()))); - if (update.chat.id) { closeMessageNotifications({ chatId: update.chat.id, @@ -77,8 +70,6 @@ addReducer('apiUpdate', (global, actions, update: ApiUpdate) => { case 'updateChatInbox': { setGlobal(updateChat(global, update.id, update.chat)); - runThrottledForUpdateAppBadge(() => updateAppBadge(selectCountNotMutedUnreadOptimized(getGlobal()))); - break; } @@ -129,7 +120,6 @@ addReducer('apiUpdate', (global, actions, update: ApiUpdate) => { })); } - updateAppBadge(selectCountNotMutedUnreadOptimized(getGlobal())); notifyAboutNewMessage({ chat, message, diff --git a/src/modules/helpers/chats.ts b/src/modules/helpers/chats.ts index df776b324..bc124f81a 100644 --- a/src/modules/helpers/chats.ts +++ b/src/modules/helpers/chats.ts @@ -7,7 +7,6 @@ import { MAIN_THREAD_ID, } from '../../api/types'; -import { GlobalState } from '../../global/types'; import { NotifyException, NotifySettings } from '../../types'; import { LangFn } from '../../hooks/useLang'; @@ -270,207 +269,7 @@ export function getCanDeleteChat(chat: ApiChat) { return isChatBasicGroup(chat) || ((isChatSuperGroup(chat) || isChatChannel(chat)) && chat.isCreator); } -export function prepareFolderListIds( - allListIds: GlobalState['chats']['listIds'], - chatsById: Record, - usersById: Record, - folder: ApiChatFolder, - notifySettings: NotifySettings, - notifyExceptions?: Record, -) { - 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 = ([] as string[]).concat(allListIds.active || [], allListIds.archived || []) - .filter((id) => { - const chat = chatsById[id]; - return chat && filterChatFolder( - chat, - folder, - usersById, - notifySettings, - notifyExceptions, - excludedChatIds, - includedChatIds, - pinnedChatIds, - ); - }); - - 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, - usersById: Record, - notifySettings: NotifySettings, - notifyExceptions?: Record, - excludedChatIds?: Set, - includedChatIds?: Set, - pinnedChatIds?: Set, -) { - if (!chat.isListed) { - return false; - } - - const { id: chatId, type, unreadMentionsCount } = chat; - - if (excludedChatIds?.has(chatId)) { - return false; - } - - if (includedChatIds?.has(chatId)) { - return true; - } - - if (pinnedChatIds?.has(chatId)) { - return true; - } - - if (folder.excludeArchived && chat.folderId === ARCHIVED_FOLDER_ID) { - return false; - } - - if (folder.excludeRead && !chat.unreadCount && !unreadMentionsCount && !chat.hasUnreadMark) { - return false; - } - - if (folder.excludeMuted && !unreadMentionsCount && selectIsChatMuted(chat, notifySettings, notifyExceptions)) { - return false; - } - - if (type === 'chatTypePrivate') { - const user = usersById[chatId]; - if (user) { - const { type: userType, isContact } = user; - - if (userType === 'userTypeBot') { - if (folder.bots) { - return true; - } - } else { - if (folder.contacts && isContact) { - return true; - } - - if (folder.nonContacts && !isContact) { - return true; - } - } - } - } else if (type === 'chatTypeChannel') { - return Boolean(folder.channels); - } else if (type === 'chatTypeBasicGroup' || type === 'chatTypeSuperGroup') { - return Boolean(folder.groups); - } - - return false; -} - -export function prepareChatList( - chatsById: Record, - listIds: string[], - orderedPinnedIds?: string[], - folderType: 'all' | 'archived' | 'folder' = 'all', - noOrder = false, -) { - const listIdsSet = new Set(listIds); - const orderedPinnedIdsSet = orderedPinnedIds ? new Set(orderedPinnedIds) : undefined; - - const pinnedChats = orderedPinnedIds?.reduce((acc, id) => { - const chat = chatsById[id]; - - if (chat && listIdsSet.has(chat.id) && checkChat(chat, folderType)) { - acc.push(chat); - } - - return acc; - }, [] as ApiChat[]) || []; - - const otherChats = listIds.reduce((acc, id) => { - const chat = chatsById[id]; - - if (chat && (!orderedPinnedIdsSet || !orderedPinnedIdsSet.has(chat.id)) && checkChat(chat, folderType)) { - acc.push(chat); - } - - return acc; - }, [] as ApiChat[]); - - return { - pinnedChats, - otherChats: noOrder ? otherChats : orderBy(otherChats, getChatOrder, 'desc'), - }; -} - -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( - chatArrays: { pinnedChats: ApiChat[]; otherChats: ApiChat[] }, - filteredIds: string[], -) { - const filteredIdsSet = new Set(filteredIds); - - return { - pinnedChats: chatArrays.pinnedChats.filter(({ id }) => filteredIdsSet.has(id)), - otherChats: chatArrays.otherChats.filter(({ id }) => filteredIdsSet.has(id)), - }; -} - -export function getFolderUnreadDialogs( - allListIds: GlobalState['chats']['listIds'], - chatsById: Record, - usersById: Record, - folder: ApiChatFolder, - notifySettings: NotifySettings, - notifyExceptions?: Record, -) { - const [listIds] = prepareFolderListIds(allListIds, chatsById, usersById, folder, notifySettings, notifyExceptions); - - let hasActiveDialogs = false; - const unreadDialogsCount = listIds.reduce((acc, id) => { - const chat = chatsById[id]; - if (!chat?.lastMessage || chat?.isRestricted || chat?.isNotJoined) { - return acc; - } - - const isUnread = chat.unreadCount || chat.hasUnreadMark; - - if (isUnread) { - acc++; - } - - if (!hasActiveDialogs && ( - chat.unreadMentionsCount || (isUnread && !selectIsChatMuted(chat, notifySettings, notifyExceptions)) - )) { - hasActiveDialogs = true; - } - - return acc; - }, 0); - - return { - unreadDialogsCount, - hasActiveDialogs, - }; -} - -export function getFolderDescriptionText( - lang: LangFn, - allListIds: GlobalState['chats']['listIds'], - chatsById: Record, - usersById: Record, - folder: ApiChatFolder, - notifySettings: NotifySettings, - notifyExceptions?: Record, -) { +export function getFolderDescriptionText(lang: LangFn, folder: ApiChatFolder, chatsCount?: number) { const { id, title, emoticon, description, pinnedChatIds, excludedChatIds, includedChatIds, @@ -481,12 +280,12 @@ export function getFolderDescriptionText( // If folder has multiple additive filters or uses include/exclude lists, // we display folder chats count if ( - Object.values(filters).filter(Boolean).length > 1 - || (excludedChatIds?.length) - || (includedChatIds?.length) - ) { - const length = getFolderChatsCount(allListIds, chatsById, usersById, folder, notifySettings, notifyExceptions); - return lang('Chats', length); + chatsCount !== undefined && ( + Object.values(filters).filter(Boolean).length > 1 + || (excludedChatIds?.length) + || (includedChatIds?.length) + )) { + return lang('Chats', chatsCount); } // Otherwise, we return a short description of a single filter @@ -505,21 +304,6 @@ export function getFolderDescriptionText( } } -function getFolderChatsCount( - allListIds: GlobalState['chats']['listIds'], - chatsById: Record, - usersById: Record, - folder: ApiChatFolder, - notifySettings: NotifySettings, - notifyExceptions?: Record, -) { - const [listIds, pinnedIds] = prepareFolderListIds( - allListIds, chatsById, usersById, folder, notifySettings, notifyExceptions, - ); - const { pinnedChats, otherChats } = prepareChatList(chatsById, listIds, pinnedIds, 'folder', true); - return pinnedChats.length + otherChats.length; -} - export function getMessageSenderName(lang: LangFn, chatId: string, sender?: ApiUser) { if (!sender || isUserId(chatId)) { return undefined; diff --git a/src/modules/selectors/chats.ts b/src/modules/selectors/chats.ts index e1be64a0f..aecff17d6 100644 --- a/src/modules/selectors/chats.ts +++ b/src/modules/selectors/chats.ts @@ -2,14 +2,12 @@ import { ApiChat, MAIN_THREAD_ID } from '../../api/types'; import { GlobalState } from '../../global/types'; import { - getPrivateChatUserId, isChatChannel, isUserId, isHistoryClearMessage, isUserBot, isUserOnline, selectIsChatMuted, + getPrivateChatUserId, isChatChannel, isUserId, isHistoryClearMessage, isUserBot, isUserOnline, } from '../helpers'; import { selectUser } from './users'; import { ALL_FOLDER_ID, ARCHIVED_FOLDER_ID, MEMBERS_LOAD_SLICE, SERVICE_NOTIFICATIONS_USER_ID, } from '../../config'; -import { selectNotifyExceptions, selectNotifySettings } from './settings'; -import memoized from '../../util/memoized'; export function selectChat(global: GlobalState, chatId: string): ApiChat | undefined { return global.chats.byId[chatId]; @@ -154,40 +152,6 @@ export function selectChatByUsername(global: GlobalState, username: string) { ); } -const selectCountNotMutedUnreadMemo = memoized(( - activeChatIds: GlobalState['chats']['listIds']['active'], - chatsById: GlobalState['chats']['byId'], - notifySettings: GlobalState['settings']['byKey'], - notifyExceptions: GlobalState['settings']['notifyExceptions'], -) => { - return activeChatIds?.reduce((acc, chatId) => { - const chat = chatsById[chatId]; - - if ( - chat - && chat.unreadCount - && chat.isListed - && !chat.isNotJoined - && !chat.isRestricted - && (chat.unreadMentionsCount || !selectIsChatMuted(chat, notifySettings, notifyExceptions)) - ) { - return acc + chat.unreadCount; - } - - return acc; - }, 0) || 0; -}); - -// Still slow but at least memoized -export function selectCountNotMutedUnreadOptimized(global: GlobalState) { - return selectCountNotMutedUnreadMemo( - global.chats.listIds.active, - global.chats.byId, - selectNotifySettings(global), - selectNotifyExceptions(global), - ); -} - export function selectIsServiceChatReady(global: GlobalState) { return Boolean(selectChat(global, SERVICE_NOTIFICATIONS_USER_ID)); } diff --git a/src/util/callbacks.ts b/src/util/callbacks.ts index a923141d4..b242dc6d2 100644 --- a/src/util/callbacks.ts +++ b/src/util/callbacks.ts @@ -22,9 +22,16 @@ export function createCallbackManager() { }); } + function hasCallbacks() { + return Boolean(callbacks.length); + } + return { runCallbacks, addCallback, removeCallback, + hasCallbacks, }; } + +export type CallbackManager = ReturnType; diff --git a/src/util/folderManager.ts b/src/util/folderManager.ts new file mode 100644 index 000000000..1710ac428 --- /dev/null +++ b/src/util/folderManager.ts @@ -0,0 +1,626 @@ +import { addCallback, getGlobal } from '../lib/teact/teactn'; + +import { GlobalState } from '../global/types'; +import { NotifyException, NotifySettings } from '../types'; +import { ApiChat, ApiChatFolder, ApiUser } from '../api/types'; + +import { ALL_FOLDER_ID, ARCHIVED_FOLDER_ID, DEBUG } from '../config'; +import { selectNotifySettings, selectNotifyExceptions } from '../modules/selectors'; +import { selectIsChatMuted } from '../modules/helpers'; +import { onIdle, throttle } from './schedulers'; +import { areSortedArraysEqual, unique } from './iteratees'; +import arePropsShallowEqual from './arePropsShallowEqual'; +import { CallbackManager, createCallbackManager } from './callbacks'; + +interface FolderSummary { + id: number; + listIds?: Set; + orderedPinnedIds?: string[]; + contacts?: true; + nonContacts?: true; + groups?: true; + channels?: true; + bots?: true; + excludeMuted?: true; + excludeRead?: true; + excludeArchived?: true; + excludedChatIds?: Set; + includedChatIds?: Set; + pinnedChatIds?: Set; +} + +interface ChatSummary { + id: string; + type: ApiChat['type']; + isListed: boolean; + isArchived: boolean; + isMuted: boolean; + isUnread: boolean; + unreadCount?: number; + unreadMentionsCount?: number; + order: number; + isUserBot?: boolean; + isUserContact?: boolean; +} + +const UPDATE_THROTTLE = 500; +const DEBUG_DURATION_LIMIT = 6; + +const prevGlobal: { + allFolderListIds?: GlobalState['chats']['listIds']['active']; + allFolderPinnedIds?: GlobalState['chats']['orderedPinnedIds']['active']; + archivedFolderListIds?: GlobalState['chats']['listIds']['archived']; + archivedFolderPinnedIds?: GlobalState['chats']['orderedPinnedIds']['archived']; + chatsById: Record; + foldersById: Record; + usersById: Record; + notifySettings: NotifySettings; + notifyExceptions?: Record; +} = { + foldersById: {}, + chatsById: {}, + usersById: {}, + notifySettings: {} as NotifySettings, + notifyExceptions: {}, +}; + +const prepared: { + folderSummariesById: Record; + chatSummariesById: Map; + folderIdsByChatId: Record; + chatIdsByFolderId: Record>; + isOrderedListJustPatched: Record; +} = { + folderSummariesById: {}, + chatSummariesById: new Map(), + folderIdsByChatId: {}, + chatIdsByFolderId: {}, + isOrderedListJustPatched: {}, +}; + +const results: { + orderedIdsByFolderId: Record; + chatsCountByFolderId: Record; + unreadCountersByFolderId: Record; +} = { + orderedIdsByFolderId: {}, + chatsCountByFolderId: {}, + unreadCountersByFolderId: {}, +}; + +const callbacks: { + orderedIdsByFolderId: Record; + chatsCountByFolderId: CallbackManager; + unreadCountersByFolderId: CallbackManager; +} = { + orderedIdsByFolderId: {}, + chatsCountByFolderId: createCallbackManager(), + unreadCountersByFolderId: createCallbackManager(), +}; + +const updateFolderManagerThrottled = throttle(() => { + onIdle(() => { + updateFolderManager(getGlobal()); + }); +}, UPDATE_THROTTLE); + +let inited = false; + +function init() { + addCallback(updateFolderManagerThrottled); + updateFolderManager(getGlobal()); +} + +/* Getters */ + +export function getOrderedIds(folderId: number) { + if (!inited) init(); + + return results.orderedIdsByFolderId[folderId]; +} + +export function getChatsCount() { + if (!inited) init(); + + return results.chatsCountByFolderId; +} + +export function getUnreadCounters() { + if (!inited) init(); + + return results.unreadCountersByFolderId; +} + +export function getAllNotificationsCount() { + return getUnreadCounters()[ALL_FOLDER_ID]?.notificationsCount || 0; +} + +export function getPinnedChatsCount(folderId: number) { + return prepared.folderSummariesById[folderId]?.pinnedChatIds?.size; +} + +/* Callback managers */ + +export function addOrderedIdsCallback(folderId: number, callback: (orderedIds: string[]) => void) { + if (!callbacks.orderedIdsByFolderId[folderId]) { + callbacks.orderedIdsByFolderId[folderId] = createCallbackManager(); + } + + return callbacks.orderedIdsByFolderId[folderId].addCallback(callback); +} + +export function addChatsCountCallback(callback: (chatsCount: typeof results.chatsCountByFolderId) => void) { + return callbacks.chatsCountByFolderId.addCallback(callback); +} + +export function addUnreadCountersCallback(callback: (unreadCounters: typeof results.unreadCountersByFolderId) => void) { + return callbacks.unreadCountersByFolderId.addCallback(callback); +} + +/* Global update handlers */ + +function updateFolderManager(global: GlobalState) { + // eslint-disable-next-line @typescript-eslint/naming-convention + let DEBUG_startedAt: number; + if (DEBUG) { + DEBUG_startedAt = performance.now(); + } + + const isAllFolderChanged = Boolean( + global.chats.listIds.active + && isMainFolderChanged(ALL_FOLDER_ID, global.chats.listIds.active, global.chats.orderedPinnedIds.active), + ); + const isArchivedFolderChanged = Boolean( + global.chats.listIds.archived + && isMainFolderChanged(ARCHIVED_FOLDER_ID, global.chats.listIds.archived, global.chats.orderedPinnedIds.archived), + ); + + const areFoldersChanged = global.chatFolders.byId !== prevGlobal.foldersById; + const areChatsChanged = global.chats.byId !== prevGlobal.chatsById; + const areUsersChanged = global.users.byId !== prevGlobal.usersById; + const areNotifySettingsChanged = selectNotifySettings(global) !== prevGlobal.notifySettings; + const areNotifyExceptionsChanged = selectNotifyExceptions(global) !== prevGlobal.notifyExceptions; + + if (!( + isAllFolderChanged || isArchivedFolderChanged || areFoldersChanged + || areChatsChanged || areUsersChanged || areNotifySettingsChanged || areNotifyExceptionsChanged + ) + ) { + return; + } + + const prevAllFolderListIds = prevGlobal.allFolderListIds; + const prevArchivedFolderListIds = prevGlobal.archivedFolderListIds; + + updateFolders(global, isAllFolderChanged, isArchivedFolderChanged, areFoldersChanged); + + const affectedFolderIds = updateChats( + global, areFoldersChanged, areNotifySettingsChanged, areNotifyExceptionsChanged, + prevAllFolderListIds, prevArchivedFolderListIds, + ); + + updateResults(affectedFolderIds); + + if (DEBUG) { + const duration = performance.now() - DEBUG_startedAt!; + if (duration > DEBUG_DURATION_LIMIT) { + // eslint-disable-next-line no-console + console.warn(`Slow \`updateFolderManager\`: ${Math.round(duration)} ms`); + } + } +} + +function isMainFolderChanged(folderId: number, newListIds?: string[], newPinnedIds?: string[]) { + const currentListIds = folderId === ALL_FOLDER_ID + ? prevGlobal.allFolderListIds + : prevGlobal.archivedFolderListIds; + const currentPinnedIds = folderId === ALL_FOLDER_ID + ? prevGlobal.allFolderPinnedIds + : prevGlobal.archivedFolderPinnedIds; + + return currentListIds !== newListIds || currentPinnedIds !== newPinnedIds; +} + +function updateFolders( + global: GlobalState, isAllFolderChanged: boolean, isArchivedFolderChanged: boolean, areFoldersChanged: boolean, +) { + const changedFolders = []; + + if (isAllFolderChanged) { + const newListIds = global.chats.listIds.active!; + const newPinnedIds = global.chats.orderedPinnedIds.active; + + prepared.folderSummariesById[ALL_FOLDER_ID] = buildFolderSummaryFromMainList( + ALL_FOLDER_ID, newListIds, newPinnedIds, + ); + + prevGlobal.allFolderListIds = newListIds; + prevGlobal.allFolderPinnedIds = newPinnedIds; + + changedFolders.push(ALL_FOLDER_ID); + } + + if (isArchivedFolderChanged) { + const newListIds = global.chats.listIds.archived!; + const newPinnedIds = global.chats.orderedPinnedIds.archived; + + prepared.folderSummariesById[ARCHIVED_FOLDER_ID] = buildFolderSummaryFromMainList( + ARCHIVED_FOLDER_ID, newListIds, newPinnedIds, + ); + + prevGlobal.archivedFolderListIds = newListIds; + prevGlobal.archivedFolderPinnedIds = newPinnedIds; + + changedFolders.push(ARCHIVED_FOLDER_ID); + } + + if (areFoldersChanged) { + const newFoldersById = global.chatFolders.byId; + + Object.values(newFoldersById).forEach((folder) => { + if (folder === prevGlobal.foldersById[folder.id]) { + return; + } + + prepared.folderSummariesById[folder.id] = buildFolderSummary(folder); + + changedFolders.push(folder.id); + }); + + prevGlobal.foldersById = newFoldersById; + } + + return changedFolders; +} + +function buildFolderSummaryFromMainList( + folderId: number, listIds: string[], orderedPinnedIds?: string[], +): FolderSummary { + return { + id: folderId, + listIds: new Set(listIds), + orderedPinnedIds: orderedPinnedIds, + pinnedChatIds: new Set(orderedPinnedIds), + }; +} + +function buildFolderSummary(folder: ApiChatFolder): FolderSummary { + return { + ...folder, + orderedPinnedIds: folder.pinnedChatIds, + excludedChatIds: folder.excludedChatIds ? new Set(folder.excludedChatIds) : undefined, + includedChatIds: folder.excludedChatIds ? new Set(folder.includedChatIds) : undefined, + pinnedChatIds: folder.excludedChatIds ? new Set(folder.pinnedChatIds) : undefined, + }; +} + +function updateChats( + global: GlobalState, + areFoldersChanged: boolean, + areNotifySettingsChanged: boolean, + areNotifyExceptionsChanged: boolean, + prevAllFolderListIds?: string[], + prevArchivedFolderListIds?: string[], +) { + const newChatsById = global.chats.byId; + const newUsersById = global.users.byId; + const newNotifySettings = selectNotifySettings(global); + const newNotifyExceptions = selectNotifyExceptions(global); + const folderSummaries = Object.values(prepared.folderSummariesById); + const affectedFolderIds = new Set(); + + const newAllFolderListIds = global.chats.listIds.active; + const newArchivedFolderListIds = global.chats.listIds.archived; + let allIds = [...newAllFolderListIds || [], ...newArchivedFolderListIds || []]; + if (newAllFolderListIds !== prevAllFolderListIds || newArchivedFolderListIds !== prevArchivedFolderListIds) { + allIds = unique(allIds.concat(prevAllFolderListIds || [], prevArchivedFolderListIds || [])); + } + + allIds.forEach((chatId) => { + const chat = newChatsById[chatId]; + + if ( + !areFoldersChanged + && !areNotifySettingsChanged + && !areNotifyExceptionsChanged + && chat === prevGlobal.chatsById[chatId] + && newUsersById[chatId] === prevGlobal.usersById[chatId] + ) { + return; + } + + let newFolderIds: number[]; + if (chat) { + const currentSummary = prepared.chatSummariesById.get(chatId); + const newSummary = buildChatSummary(chat, newNotifySettings, newNotifyExceptions, newUsersById[chatId]); + if (!areFoldersChanged && currentSummary && arePropsShallowEqual(newSummary, currentSummary)) { + return; + } + + prepared.chatSummariesById.set(chatId, newSummary); + + newFolderIds = buildChatFolderIds(newSummary, folderSummaries); + newFolderIds.forEach((folderId) => { + affectedFolderIds.add(folderId); + }); + } else { + prepared.chatSummariesById.delete(chatId); + newFolderIds = []; + } + + const currentFolderIds = prepared.folderIdsByChatId[chatId] || []; + if (areSortedArraysEqual(newFolderIds, currentFolderIds)) { + return; + } + + const deletedFolderIds = updateListsForChat(chatId, currentFolderIds, newFolderIds); + deletedFolderIds.forEach((folderId) => { + affectedFolderIds.add(folderId); + }); + }); + + prevGlobal.chatsById = newChatsById; + prevGlobal.usersById = newUsersById; + prevGlobal.notifySettings = newNotifySettings; + prevGlobal.notifyExceptions = newNotifyExceptions; + + return Array.from(affectedFolderIds); +} + +function buildChatSummary( + chat: ApiChat, + notifySettings: NotifySettings, + notifyExceptions?: Record, + user?: ApiUser, +): ChatSummary { + const { + id, type, lastMessage, isRestricted, isNotJoined, folderId, + unreadCount, unreadMentionsCount, hasUnreadMark, + joinDate, draftDate, + } = chat; + + const userInfo = type === 'chatTypePrivate' && user; + + return { + id, + type, + isListed: Boolean(lastMessage && !isRestricted && !isNotJoined), + isArchived: folderId === ARCHIVED_FOLDER_ID, + isMuted: selectIsChatMuted(chat, notifySettings, notifyExceptions), + isUnread: Boolean(unreadCount || unreadMentionsCount || hasUnreadMark), + unreadCount, + unreadMentionsCount, + order: Math.max(joinDate || 0, draftDate || 0, lastMessage?.date || 0), + isUserBot: userInfo ? userInfo.type === 'userTypeBot' : undefined, + isUserContact: userInfo ? userInfo.isContact : undefined, + }; +} + +function buildChatFolderIds(chatSummary: ChatSummary, folderSummaries: FolderSummary[]) { + return folderSummaries.reduce((acc, folderSummary) => { + if (isChatInFolder(chatSummary, folderSummary)) { + acc.push(folderSummary.id); + } + + return acc; + }, []).sort(); +} + +function isChatInFolder( + chatSummary: ChatSummary, + folderSummary: FolderSummary, +) { + if (!chatSummary.isListed) { + return false; + } + + const { id: chatId, type } = chatSummary; + + if (folderSummary.listIds) { + if ( + (chatSummary.isArchived && folderSummary.id === ALL_FOLDER_ID) + || (!chatSummary.isArchived && folderSummary.id === ARCHIVED_FOLDER_ID) + ) { + return false; + } + + return folderSummary.listIds.has(chatId); + } + + if (folderSummary.excludedChatIds?.has(chatId)) { + return false; + } + + if (folderSummary.includedChatIds?.has(chatId)) { + return true; + } + + if (folderSummary.pinnedChatIds?.has(chatId)) { + return true; + } + + if (folderSummary.excludeArchived && chatSummary.isArchived) { + return false; + } + + if (folderSummary.excludeRead && !chatSummary.isUnread) { + return false; + } + + if (folderSummary.excludeMuted && chatSummary.isMuted && !chatSummary.unreadMentionsCount) { + return false; + } + + if (type === 'chatTypePrivate') { + if (chatSummary.isUserBot) { + if (folderSummary.bots) { + return true; + } + } else { + if (folderSummary.contacts && chatSummary.isUserContact) { + return true; + } + + if (folderSummary.nonContacts && !chatSummary.isUserContact) { + return true; + } + } + } else if (type === 'chatTypeChannel') { + return Boolean(folderSummary.channels); + } else if (type === 'chatTypeBasicGroup' || type === 'chatTypeSuperGroup') { + return Boolean(folderSummary.groups); + } + + return false; +} + +function updateListsForChat(chatId: string, currentFolderIds: number[], newFolderIds: number[]) { + const currentFolderIdsSet = new Set(currentFolderIds); + const newFolderIdsSet = new Set(newFolderIds); + const deletedFolderIds: number[] = []; + + unique([...currentFolderIds, ...newFolderIds]).forEach((folderId) => { + let currentFolderOrderedIds = results.orderedIdsByFolderId[folderId]; + + if (currentFolderIdsSet.has(folderId) && !newFolderIdsSet.has(folderId)) { + prepared.chatIdsByFolderId[folderId].delete(chatId); + + deletedFolderIds.push(folderId); + + if (currentFolderOrderedIds) { + currentFolderOrderedIds = currentFolderOrderedIds.filter((id) => id !== chatId); + prepared.isOrderedListJustPatched[folderId] = true; + } + } else if (!currentFolderIdsSet.has(folderId) && newFolderIdsSet.has(folderId)) { + if (!prepared.chatIdsByFolderId[folderId]) { + prepared.chatIdsByFolderId[folderId] = new Set(); + } + + prepared.chatIdsByFolderId[folderId].add(chatId); + + if (currentFolderOrderedIds) { + currentFolderOrderedIds.push(chatId); + prepared.isOrderedListJustPatched[folderId] = true; + } + } + + results.orderedIdsByFolderId[folderId] = currentFolderOrderedIds; + }); + + prepared.folderIdsByChatId[chatId] = newFolderIds; + + return deletedFolderIds; +} + +function updateResults(affectedFolderIds: number[]) { + let wasUnreadCountersChanged = false; + let wasChatsCountChanged = false; + + Array.from(affectedFolderIds).forEach((folderId) => { + const newOrderedIds = buildFolderOrderedIds(folderId); + + const currentOrderedIds = results.orderedIdsByFolderId[folderId]; + const areOrderedIdsChanged = ( + !currentOrderedIds + || prepared.isOrderedListJustPatched[folderId] + || !areSortedArraysEqual(newOrderedIds, currentOrderedIds) + ); + if (areOrderedIdsChanged) { + prepared.isOrderedListJustPatched[folderId] = false; + results.orderedIdsByFolderId[folderId] = newOrderedIds; + callbacks.orderedIdsByFolderId[folderId]?.runCallbacks(newOrderedIds); + } + + const currentChatsCount = results.chatsCountByFolderId[folderId]; + const newChatsCount = newOrderedIds.length; + if (!wasChatsCountChanged) { + wasChatsCountChanged = currentChatsCount !== newChatsCount; + } + results.chatsCountByFolderId[folderId] = newChatsCount; + + const currentUnreadCounters = results.unreadCountersByFolderId[folderId]; + const newUnreadCounters = buildFolderUnreadCounters(folderId); + if (!wasUnreadCountersChanged) { + wasUnreadCountersChanged = ( + !currentUnreadCounters || !arePropsShallowEqual(newUnreadCounters, currentUnreadCounters) + ); + } + results.unreadCountersByFolderId[folderId] = newUnreadCounters; + }); + + if (wasChatsCountChanged) { + // We need to update the entire object as it will be returned from a hook + const newValue = { ...results.chatsCountByFolderId }; + results.chatsCountByFolderId = newValue; + callbacks.chatsCountByFolderId.runCallbacks(newValue); + } + + if (wasUnreadCountersChanged) { + // We need to update the entire object as it will be returned from a hook + const newValue = { ...results.unreadCountersByFolderId }; + results.unreadCountersByFolderId = newValue; + callbacks.unreadCountersByFolderId.runCallbacks(newValue); + } +} + +function buildFolderOrderedIds(folderId: number) { + const { + folderSummariesById: { [folderId]: { orderedPinnedIds, pinnedChatIds } }, + chatSummariesById, + chatIdsByFolderId: { [folderId]: chatIds }, + } = prepared; + const { + orderedIdsByFolderId: { [folderId]: prevOrderedIds }, + } = results; + + const allListIds = prevOrderedIds || Array.from(chatIds); + const notPinnedIds = pinnedChatIds ? allListIds.filter((id) => !pinnedChatIds.has(id)) : allListIds; + const sortedNotPinnedIds = notPinnedIds.sort((chatId1: string, chatId2: string) => { + return chatSummariesById.get(chatId2)!.order - chatSummariesById.get(chatId1)!.order; + }); + + return [ + ...(orderedPinnedIds || []), + ...sortedNotPinnedIds, + ]; +} + +function buildFolderUnreadCounters(folderId: number) { + const { + chatSummariesById, + } = prepared; + const { + orderedIdsByFolderId: { [folderId]: orderedIds }, + } = results; + + return orderedIds.reduce((newUnreadCounters, chatId) => { + const chatSummary = chatSummariesById.get(chatId); + if (!chatSummary) { + return newUnreadCounters; + } + + if (chatSummary.isUnread) { + newUnreadCounters.chatsCount++; + + if (chatSummary.unreadMentionsCount) { + newUnreadCounters.notificationsCount += chatSummary.unreadMentionsCount; + } + + if (!chatSummary.isMuted) { + if (chatSummary.unreadCount) { + newUnreadCounters.notificationsCount += chatSummary.unreadCount; + } else if (!chatSummary.unreadMentionsCount) { + newUnreadCounters.notificationsCount += 1; // Manually marked unread + } + } + } + + return newUnreadCounters; + }, { + chatsCount: 0, + notificationsCount: 0, + }); +}