From 3167a36d847d1c719847e78f72d46805afc69fc8 Mon Sep 17 00:00:00 2001 From: Alexander Zinchuk Date: Wed, 23 Apr 2025 18:59:04 +0200 Subject: [PATCH] Chat Folders: Add "Read all messages" and "Edit" for the "All chats" folder (#5793) Co-authored-by: zubiden <19638254+zubiden@users.noreply.github.com> --- src/assets/localization/fallback.strings | 25 +++++--- src/components/common/gift/GiftMenuItems.tsx | 2 +- src/components/left/main/Archive.tsx | 10 ++-- src/components/left/main/ChatFolders.tsx | 62 ++++++++++++++++++-- src/components/ui/Tab.scss | 2 +- src/hooks/useChatContextActions.ts | 33 ++++++----- src/hooks/useFolderManager.ts | 12 +++- src/types/language.d.ts | 26 +++++--- src/util/folderManager.ts | 40 +++++++++++++ 9 files changed, 164 insertions(+), 48 deletions(-) diff --git a/src/assets/localization/fallback.strings b/src/assets/localization/fallback.strings index 6ac2d9207..f44afd5ed 100644 --- a/src/assets/localization/fallback.strings +++ b/src/assets/localization/fallback.strings @@ -11,7 +11,7 @@ "Month11" = "November"; "Month12" = "December"; "GroupLeaveGroup" = "Leave Group"; -"DeleteChatUser" = "Delete chat"; +"DeleteChat" = "Delete Chat"; "AccDescrGroup" = "Group"; "AccDescrChannel" = "Channel"; "Nothing" = "Nothing"; @@ -85,6 +85,9 @@ "ProfileCopyPhone" = "Copy Phone Number"; "ContextCopySelected" = "Copy Selected Text"; "ContextCopyText" = "Copy Text"; +"ContextArchiveCollapse" = "Collapse"; +"ContextArchiveExpand" = "Collapse"; +"ContextArchiveToMenu" = "Move to Main Menu"; "CallMessageVideoIncomingDeclined" = "Declined Video Call"; "CallMessageVideoOutgoingMissed" = "Canceled Video Call"; "CallMessageVideoIncomingMissed" = "Missed Video Call"; @@ -478,8 +481,9 @@ "PasscodeControllerDisableTitle" = "Disable Passcode"; "PasscodeControllerChangeTitle" = "Change Passcode"; "FilterNew" = "New Folder"; -"FilterEdit" = "Edit folder"; -"FilterDelete" = "Delete folder"; +"EditFolder" = "Edit Folder"; +"FilterEditFolders" = "Edit Folders"; +"FilterMenuDelete" = "Delete Folder"; "FilterShare" = "Share"; "AutoDeleteConfirm" = "Confirm"; "LogOutTitle" = "Log Out"; @@ -827,7 +831,7 @@ "EmptyGroupInfoLine3" = "• Public links such as t.me/title"; "EmptyGroupInfoLine4" = "• Admins with different rights"; "Reactions" = "Reactions"; -"MarkAllAsRead" = "Mark all as read"; +"ChatListMarkAllAsRead" = "Mark All as Read"; "PaymentCardNumber" = "Card Number"; "PaymentCheckoutAcceptRecurrent" = "I accept the *Terms of Service* of {bot}."; "CheckoutTotalAmount" = "Total"; @@ -881,7 +885,7 @@ "ChannelSubscribers" = "Subscribers"; "ChannelBlockedUsers" = "Removed users"; "ChannelDelete" = "Delete Channel"; -"LeaveChannel" = "Leave channel"; +"ChannelLeave" = "Leave Channel"; "ChannelDeleteAlert" = "Deleting this channel will remove all subscribers and all posts will be lost. Delete the channel anyway?"; "ChannelLeaveAlert" = "Are you sure you want to leave this channel?"; "ChannelCreator" = "Owner"; @@ -1178,10 +1182,13 @@ "ConversationViewChannel" = "VIEW CHANNEL"; "Telegram" = "Telegram"; "ChatListFilterAddToFolder" = "Add to folder..."; -"UnpinFromTop" = "Unpin from top"; -"PinToTop" = "Pin to top"; -"MarkAsRead" = "Mark as read"; -"MarkAsUnread" = "Mark as unread"; +"ChatListUnpinFromTop" = "Unpin from Top"; +"ChatListPinToTop" = "Pin to Top"; +"ChatListOpenInNewWindow" = "Open in New Window"; +"ChatListOpenInNewTab" = "Open in New Tab"; +"ChatListContextMaskAsRead" = "Mark as Read"; +"ChatListContextMaskAsUnread" = "Mark as Unread"; +"ChatListContextAddToFolder" = "Add to Folder"; "Unarchive" = "Unarchive"; "Archive" = "Archive"; "WaitingForNetwork" = "Waiting for network..."; diff --git a/src/components/common/gift/GiftMenuItems.tsx b/src/components/common/gift/GiftMenuItems.tsx index 7ba02a964..b47b5730d 100644 --- a/src/components/common/gift/GiftMenuItems.tsx +++ b/src/components/common/gift/GiftMenuItems.tsx @@ -109,7 +109,7 @@ const GiftMenuItems = ({ <> {hasPinOptions && ( - {lang(savedGift.isPinned ? 'UnpinFromTop' : 'PinToTop')} + {lang(savedGift.isPinned ? 'ChatListUnpinFromTop' : 'ChatListPinToTop')} )} diff --git a/src/components/left/main/Archive.tsx b/src/components/left/main/Archive.tsx index 4c198a2f5..5e94aebaf 100644 --- a/src/components/left/main/Archive.tsx +++ b/src/components/left/main/Archive.tsx @@ -13,7 +13,7 @@ import { formatIntegerCompact } from '../../../util/textFormat'; import renderText from '../../common/helpers/renderText'; import { useFolderManagerForOrderedIds, useFolderManagerForUnreadCounters } from '../../../hooks/useFolderManager'; -import useOldLang from '../../../hooks/useOldLang'; +import useLang from '../../../hooks/useLang'; import Avatar from '../../common/Avatar'; import Icon from '../../common/icons/Icon'; @@ -42,7 +42,7 @@ const Archive: FC = ({ onClick, }) => { const { updateArchiveSettings } = getActions(); - const lang = useOldLang(); + const lang = useLang(); const orderedChatIds = useFolderManagerForOrderedIds(ARCHIVED_FOLDER_ID); const unreadCounters = useFolderManagerForUnreadCounters(); @@ -75,7 +75,7 @@ const Archive: FC = ({ const contextActions = useMemo(() => { const actionMinimize = !archiveSettings.isMinimized && { - title: lang('lng_context_archive_collapse'), + title: lang('ContextArchiveCollapse'), icon: 'collapse', handler: () => { updateArchiveSettings({ isMinimized: true }); @@ -83,7 +83,7 @@ const Archive: FC = ({ } satisfies MenuItemContextAction; const actionExpand = archiveSettings.isMinimized && { - title: lang('lng_context_archive_expand'), + title: lang('ContextArchiveExpand'), icon: 'expand', handler: () => { updateArchiveSettings({ isMinimized: false }); @@ -91,7 +91,7 @@ const Archive: FC = ({ } satisfies MenuItemContextAction; const actionHide = { - title: lang('lng_context_archive_to_menu'), + title: lang('ContextArchiveToMenu'), icon: 'archive-to-main', handler: () => { updateArchiveSettings({ isHidden: true }); diff --git a/src/components/left/main/ChatFolders.tsx b/src/components/left/main/ChatFolders.tsx index dc78cf311..b0ed9951c 100644 --- a/src/components/left/main/ChatFolders.tsx +++ b/src/components/left/main/ChatFolders.tsx @@ -7,9 +7,10 @@ import { getActions, getGlobal, withGlobal } from '../../../global'; import type { ApiChatFolder, ApiChatlistExportedInvite, ApiSession } from '../../../api/types'; import type { GlobalState } from '../../../global/types'; import type { FolderEditDispatch } from '../../../hooks/reducers/useFoldersReducer'; -import type { LeftColumnContent, SettingsScreens } from '../../../types'; +import type { LeftColumnContent } from '../../../types'; import type { MenuItemContextAction } from '../../ui/ListItem'; import type { TabWithProperties } from '../../ui/TabList'; +import { SettingsScreens } from '../../../types'; import { ALL_FOLDER_ID } from '../../../config'; import { selectCanShareFolder, selectTabState } from '../../../global/selectors'; @@ -22,7 +23,10 @@ import { MEMO_EMPTY_ARRAY } from '../../../util/memo'; import { renderTextWithEntities } from '../../common/helpers/renderTextWithEntities'; import useDerivedState from '../../../hooks/useDerivedState'; -import { useFolderManagerForUnreadCounters } from '../../../hooks/useFolderManager'; +import { + useFolderManagerForUnreadChatsByFolder, + useFolderManagerForUnreadCounters, +} from '../../../hooks/useFolderManager'; import useHistoryBack from '../../../hooks/useHistoryBack'; import useLang from '../../../hooks/useLang'; import useLastCallback from '../../../hooks/useLastCallback'; @@ -90,6 +94,7 @@ const ChatFolders: FC = ({ openDeleteChatFolderModal, openEditChatFolder, openLimitReachedModal, + markChatMessagesRead, } = getActions(); // eslint-disable-next-line no-null/no-null @@ -112,6 +117,14 @@ const ChatFolders: FC = ({ }); const isStoryRibbonClosing = useDerivedState(getIsStoryRibbonClosing); + const scrollToTop = useLastCallback(() => { + const activeList = ref.current?.querySelector('.chat-list.Transition_slide-active'); + activeList?.scrollTo({ + top: 0, + behavior: 'smooth', + }); + }); + const allChatsFolder: ApiChatFolder = useMemo(() => { return { id: ALL_FOLDER_ID, @@ -137,6 +150,16 @@ const ChatFolders: FC = ({ const isInAllChatsFolder = allChatsFolderIndex === activeChatFolder; const isInFirstFolder = FIRST_FOLDER_INDEX === activeChatFolder; + const folderUnreadChatsCountersById = useFolderManagerForUnreadChatsByFolder(); + const handleReadAllChats = useLastCallback((folderId: number) => { + const unreadChatIds = folderUnreadChatsCountersById[folderId]; + if (!unreadChatIds?.length) return; + + unreadChatIds.forEach((chatId) => { + markChatMessagesRead({ id: chatId }); + }); + }); + const folderCountersById = useFolderManagerForUnreadCounters(); const folderTabs = useMemo(() => { if (!displayedFolders || !displayedFolders.length) { @@ -177,17 +200,41 @@ const ChatFolders: FC = ({ }); } - if (id !== ALL_FOLDER_ID) { + if (id === ALL_FOLDER_ID) { contextActions.push({ - title: lang('FilterEdit'), + title: lang('FilterEditFolders'), + icon: 'edit', + handler: () => { + onSettingsScreenSelect(SettingsScreens.Folders); + }, + }); + + if (folderUnreadChatsCountersById[id]?.length) { + contextActions.push({ + title: lang('ChatListMarkAllAsRead'), + icon: 'readchats', + handler: () => handleReadAllChats(folder.id), + }); + } + } else { + contextActions.push({ + title: lang('EditFolder'), icon: 'edit', handler: () => { openEditChatFolder({ folderId: id }); }, }); + if (folderUnreadChatsCountersById[id]?.length) { + contextActions.push({ + title: lang('ChatListMarkAllAsRead'), + icon: 'readchats', + handler: () => handleReadAllChats(folder.id), + }); + } + contextActions.push({ - title: lang('FilterDelete'), + title: lang('FilterMenuDelete'), icon: 'delete', destructive: true, handler: () => { @@ -211,11 +258,14 @@ const ChatFolders: FC = ({ }); }, [ displayedFolders, maxFolders, folderCountersById, lang, chatFoldersById, maxChatLists, folderInvitesById, - maxFolderInvites, + maxFolderInvites, folderUnreadChatsCountersById, onSettingsScreenSelect, ]); const handleSwitchTab = useLastCallback((index: number) => { setActiveChatFolder({ activeChatFolder: index }, { forceOnHeavyAnimation: true }); + if (activeChatFolder === index) { + scrollToTop(); + } }); // Prevent `activeTab` pointing at non-existing folder after update diff --git a/src/components/ui/Tab.scss b/src/components/ui/Tab.scss index c1124efc5..a33992721 100644 --- a/src/components/ui/Tab.scss +++ b/src/components/ui/Tab.scss @@ -55,7 +55,7 @@ .badge { min-width: 1.25rem; height: 1.25rem; - margin-inline-start: 0.5rem; + margin-inline-start: 0.3125rem; background: var(--color-gray); border-radius: 0.75rem; padding: 0 0.3125rem; diff --git a/src/hooks/useChatContextActions.ts b/src/hooks/useChatContextActions.ts index cf8cf7197..a48d64932 100644 --- a/src/hooks/useChatContextActions.ts +++ b/src/hooks/useChatContextActions.ts @@ -11,7 +11,7 @@ import { } from '../global/helpers'; import { IS_ELECTRON, IS_OPEN_IN_NEW_TAB_SUPPORTED } from '../util/browser/windowEnvironment'; import { compact } from '../util/iteratees'; -import useOldLang from './useOldLang'; +import useLang from './useLang'; const useChatContextActions = ({ chat, @@ -44,7 +44,7 @@ const useChatContextActions = ({ handleChatFolderChange: NoneToVoidFunction; handleReport?: NoneToVoidFunction; }, isInSearch = false) => { - const lang = useOldLang(); + const lang = useLang(); const { isSelf } = user || {}; const isServiceNotifications = user?.id === SERVICE_NOTIFICATIONS_USER_ID; @@ -57,7 +57,7 @@ const useChatContextActions = ({ } if (isUserId(chat.id)) { - return lang('DeleteChatUser'); + return lang('DeleteChat'); } if (getCanDeleteChat(chat)) { @@ -65,10 +65,10 @@ const useChatContextActions = ({ } if (isChatChannel(chat)) { - return lang('LeaveChannel'); + return lang('ChannelLeave'); } - return lang('Group.LeaveGroup'); + return lang('GroupLeaveGroup'); }, [chat, isSavedDialog, lang]); return useMemo(() => { @@ -87,7 +87,7 @@ const useChatContextActions = ({ } = getActions(); const actionOpenInNewTab = IS_OPEN_IN_NEW_TAB_SUPPORTED && { - title: IS_ELECTRON ? 'Open in new window' : 'Open in new tab', + title: IS_ELECTRON ? lang('ChatListOpenInNewWindow') : lang('ChatListOpenInNewTab'), icon: 'open-in-new-tab', handler: () => { if (isSavedDialog) { @@ -108,12 +108,12 @@ const useChatContextActions = ({ const actionPin = isPinned ? { - title: lang('UnpinFromTop'), + title: lang('ChatListUnpinFromTop'), icon: 'unpin', handler: togglePinned, } : { - title: lang('PinToTop'), + title: lang('ChatListPinToTop'), icon: 'pin', handler: togglePinned, }; @@ -130,19 +130,19 @@ const useChatContextActions = ({ } const actionAddToFolder = canChangeFolder ? { - title: lang('ChatList.Filter.AddToFolder'), + title: lang('ChatListContextAddToFolder'), icon: 'folder', handler: handleChatFolderChange, } : undefined; const actionMute = isMuted ? { - title: lang('ChatList.Unmute'), + title: lang('ChatsUnmute'), icon: 'unmute', handler: () => updateChatMutedState({ chatId: chat.id, isMuted: false }), } : { - title: `${lang('ChatList.Mute')}...`, + title: `${lang('ChatsMute')}...`, icon: 'mute', handler: handleMute, }; @@ -153,10 +153,13 @@ const useChatContextActions = ({ const actionMaskAsRead = ( chat.unreadCount || chat.hasUnreadMark || Object.values(topics || {}).some(({ unreadCount }) => unreadCount) - ) ? { title: lang('MarkAsRead'), icon: 'readchats', handler: () => markChatMessagesRead({ id: chat.id }) } - : undefined; + ) ? { + title: lang('ChatListContextMaskAsRead'), + icon: 'readchats', + handler: () => markChatMessagesRead({ id: chat.id }), + } : undefined; const actionMarkAsUnread = !(chat.unreadCount || chat.hasUnreadMark) && !chat.isForum - ? { title: lang('MarkAsUnread'), icon: 'unread', handler: () => markChatUnread({ id: chat.id }) } + ? { title: lang('ChatListContextMaskAsUnread'), icon: 'unread', handler: () => markChatUnread({ id: chat.id }) } : undefined; const actionArchive = isChatArchived(chat) @@ -165,7 +168,7 @@ const useChatContextActions = ({ const canReport = handleReport && !user && (isChatChannel(chat) || isChatGroup(chat)); const actionReport = canReport - ? { title: lang('ReportPeer.Report'), icon: 'flag', handler: handleReport } + ? { title: lang('ReportPeerReport'), icon: 'flag', handler: handleReport } : undefined; const isInFolder = folderId !== undefined; diff --git a/src/hooks/useFolderManager.ts b/src/hooks/useFolderManager.ts index 8f7a9ebf3..e3c5a7c69 100644 --- a/src/hooks/useFolderManager.ts +++ b/src/hooks/useFolderManager.ts @@ -2,10 +2,10 @@ import { useEffect } from '../lib/teact/teact'; import { addChatsCountCallback, - addOrderedIdsCallback, + addOrderedIdsCallback, addUnreadChatsByFolderIdCallback, addUnreadCountersCallback, getChatsCount, - getOrderedIds, + getOrderedIds, getUnreadChatsByFolderId, getUnreadCounters, } from '../util/folderManager'; import useForceUpdate from './useForceUpdate'; @@ -33,3 +33,11 @@ export function useFolderManagerForChatsCount() { return getChatsCount(); } + +export function useFolderManagerForUnreadChatsByFolder() { + const forceUpdate = useForceUpdate(); + + useEffect(() => addUnreadChatsByFolderIdCallback(forceUpdate), [forceUpdate]); + + return getUnreadChatsByFolderId(); +} diff --git a/src/types/language.d.ts b/src/types/language.d.ts index 4ab713b64..ff9e763d1 100644 --- a/src/types/language.d.ts +++ b/src/types/language.d.ts @@ -14,7 +14,7 @@ export interface LangPair { 'Month11': undefined; 'Month12': undefined; 'GroupLeaveGroup': undefined; - 'DeleteChatUser': undefined; + 'DeleteChat': undefined; 'AccDescrGroup': undefined; 'AccDescrChannel': undefined; 'Nothing': undefined; @@ -67,6 +67,9 @@ export interface LangPair { 'ProfileCopyPhone': undefined; 'ContextCopySelected': undefined; 'ContextCopyText': undefined; + 'ContextArchiveCollapse': undefined; + 'ContextArchiveExpand': undefined; + 'ContextArchiveToMenu': undefined; 'CallMessageVideoIncomingDeclined': undefined; 'CallMessageVideoOutgoingMissed': undefined; 'CallMessageVideoIncomingMissed': undefined; @@ -428,8 +431,9 @@ export interface LangPair { 'PasscodeControllerDisableTitle': undefined; 'PasscodeControllerChangeTitle': undefined; 'FilterNew': undefined; - 'FilterEdit': undefined; - 'FilterDelete': undefined; + 'EditFolder': undefined; + 'FilterEditFolders': undefined; + 'FilterMenuDelete': undefined; 'FilterShare': undefined; 'AutoDeleteConfirm': undefined; 'LogOutTitle': undefined; @@ -710,7 +714,7 @@ export interface LangPair { 'EmptyGroupInfoLine3': undefined; 'EmptyGroupInfoLine4': undefined; 'Reactions': undefined; - 'MarkAllAsRead': undefined; + 'ChatListMarkAllAsRead': undefined; 'PaymentCardNumber': undefined; 'CheckoutTotalAmount': undefined; 'PaymentCheckoutMethod': undefined; @@ -761,7 +765,7 @@ export interface LangPair { 'ChannelSubscribers': undefined; 'ChannelBlockedUsers': undefined; 'ChannelDelete': undefined; - 'LeaveChannel': undefined; + 'ChannelLeave': undefined; 'ChannelDeleteAlert': undefined; 'ChannelLeaveAlert': undefined; 'ChannelCreator': undefined; @@ -976,6 +980,7 @@ export interface LangPair { 'ChatListFilterErrorEmpty': undefined; 'ChatListFilterErrorTitleEmpty': undefined; 'FilterMuted': undefined; + 'ReadFolder': undefined; 'FilterRead': undefined; 'FilterArchived': undefined; 'GroupsAndChannelsLimitTitle': undefined; @@ -1008,10 +1013,13 @@ export interface LangPair { 'ConversationViewChannel': undefined; 'Telegram': undefined; 'ChatListFilterAddToFolder': undefined; - 'UnpinFromTop': undefined; - 'PinToTop': undefined; - 'MarkAsRead': undefined; - 'MarkAsUnread': undefined; + 'ChatListUnpinFromTop': undefined; + 'ChatListPinToTop': undefined; + 'ChatListOpenInNewWindow': undefined; + 'ChatListOpenInNewTab': undefined; + 'ChatListContextMaskAsRead': undefined; + 'ChatListContextMaskAsUnread': undefined; + 'ChatListContextAddToFolder': undefined; 'Unarchive': undefined; 'Archive': undefined; 'WaitingForNetwork': undefined; diff --git a/src/util/folderManager.ts b/src/util/folderManager.ts index 6951d9340..563ca5020 100644 --- a/src/util/folderManager.ts +++ b/src/util/folderManager.ts @@ -97,12 +97,14 @@ let results: { chatsCount: number; notificationsCount: number; } | undefined>; + unreadChatIdsByFolderId: Record; } = initials.results; let callbacks: { orderedIdsByFolderId: Record; chatsCountByFolderId: CallbackManager; unreadCountersByFolderId: CallbackManager; + unreadChatIdsByFolderId: CallbackManager; } = initials.callbacks; if (DEBUG) { @@ -159,6 +161,12 @@ export function getUnreadCounters() { return results.unreadCountersByFolderId; } +export function getUnreadChatsByFolderId() { + if (!inited) init(); + + return results.unreadChatIdsByFolderId; +} + export function getAllNotificationsCount() { return getUnreadCounters()[ALL_FOLDER_ID]?.notificationsCount || 0; } @@ -182,6 +190,12 @@ export function addChatsCountCallback(callback: (chatsCount: typeof results.chat return callbacks.chatsCountByFolderId.addCallback(callback); } +export function addUnreadChatsByFolderIdCallback( + callback: (unreadChats: typeof results.unreadChatIdsByFolderId) => void, +) { + return callbacks.unreadChatIdsByFolderId.addCallback(callback); +} + export function addUnreadCountersCallback(callback: (unreadCounters: typeof results.unreadCountersByFolderId) => void) { return callbacks.unreadCountersByFolderId.addCallback(callback); } @@ -677,6 +691,7 @@ function updateListsForChat(chatId: string, currentFolderIds: number[], newFolde function updateResults(affectedFolderIds: number[]) { let wasUnreadCountersChanged = false; let wasChatsCountChanged = false; + let wasUnreadChatsChanged = false; Array.from(affectedFolderIds).forEach((folderId) => { const { pinnedCount: newPinnedCount, orderedIds: newOrderedIds } = buildFolderOrderedIds(folderId); @@ -715,6 +730,15 @@ function updateResults(affectedFolderIds: number[]) { ); } results.unreadCountersByFolderId[folderId] = newUnreadCounters; + + const currentUnreadChats = results.unreadChatIdsByFolderId[folderId]; + const newUnreadChats = buildFolderByUnreadChats(folderId); + if (!wasUnreadChatsChanged) { + wasUnreadChatsChanged = ( + !currentUnreadChats || !areSortedArraysEqual(newUnreadChats, currentUnreadChats) + ); + } + results.unreadChatIdsByFolderId[folderId] = newUnreadChats; }); if (wasChatsCountChanged) { @@ -730,6 +754,10 @@ function updateResults(affectedFolderIds: number[]) { results.unreadCountersByFolderId = newValue; callbacks.unreadCountersByFolderId.runCallbacks(newValue); } + + if (wasUnreadChatsChanged) { + callbacks.unreadChatIdsByFolderId.runCallbacks(results.unreadChatIdsByFolderId); + } } function buildFolderOrderedIds(folderId: number) { @@ -801,6 +829,16 @@ function buildFolderUnreadCounters(folderId: number) { }); } +function buildFolderByUnreadChats(folderId: number) { + const { chatSummariesById } = prepared; + const { orderedIdsByFolderId: { [folderId]: orderedIds } } = results; + + return orderedIds!.filter((chatId) => { + const chatSummary = chatSummariesById.get(chatId); + return chatSummary?.isUnread; + }); +} + function buildInitials() { return { prevGlobal: { @@ -823,12 +861,14 @@ function buildInitials() { pinnedCountByFolderId: {}, chatsCountByFolderId: {}, unreadCountersByFolderId: {}, + unreadChatIdsByFolderId: {}, }, callbacks: { orderedIdsByFolderId: {}, chatsCountByFolderId: createCallbackManager(), unreadCountersByFolderId: createCallbackManager(), + unreadChatIdsByFolderId: createCallbackManager(), }, }; }