[Perf] Introduce Folder Manager optimization

This commit is contained in:
Alexander Zinchuk 2022-02-02 22:48:15 +01:00
parent 1f1c30fd0f
commit 30121c903c
17 changed files with 831 additions and 673 deletions

View File

@ -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<OwnProps> = ({ isForAppBadge }) => {
useFolderManagerForUnreadCounters();
const unreadNotificationsCount = getAllNotificationsCount();
useEffect(() => {
if (isForAppBadge) {
updateAppBadge(unreadNotificationsCount);
}
}, [isForAppBadge, unreadNotificationsCount]);
if (isForAppBadge || !unreadNotificationsCount) {
return undefined;
}
return (
<div className="unread-count active">{formatIntegerCompact(unreadNotificationsCount)}</div>
);
};
export default memo(UnreadCounter);

View File

@ -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<string, ApiChat>;
usersById: Record<string, ApiUser>;
chatFoldersById: Record<number, ApiChatFolder>;
notifySettings: NotifySettings;
notifyExceptions?: Record<number, NotifyException>;
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<OwnProps & StateProps> = ({
allListIds,
chatsById,
usersById,
foldersDispatch,
onScreenSelect,
chatFoldersById,
notifySettings,
notifyExceptions,
orderedFolderIds,
activeChatFolder,
currentUserId,
lastSyncTime,
shouldSkipHistoryAnimations,
foldersDispatch,
onScreenSelect,
}) => {
const {
loadChatFolders,
@ -85,36 +70,22 @@ const ChatFolders: FC<OwnProps & StateProps> = ({
: 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<OwnProps & StateProps> = ({
<ChatList
folderType="all"
isActive={isActive}
lastSyncTime={lastSyncTime}
foldersDispatch={foldersDispatch}
onScreenSelect={onScreenSelect}
/>
@ -215,6 +187,7 @@ const ChatFolders: FC<OwnProps & StateProps> = ({
folderType="folder"
folderId={activeFolder.id}
isActive={isActive}
lastSyncTime={lastSyncTime}
onScreenSelect={onScreenSelect}
foldersDispatch={foldersDispatch}
/>
@ -243,8 +216,6 @@ const ChatFolders: FC<OwnProps & StateProps> = ({
export default memo(withGlobal<OwnProps>(
(global): StateProps => {
const {
chats: { listIds: allListIds, byId: chatsById },
users: { byId: usersById },
chatFolders: {
byId: chatFoldersById,
orderedIds: orderedFolderIds,
@ -256,16 +227,11 @@ export default memo(withGlobal<OwnProps>(
} = global;
return {
allListIds,
chatsById,
usersById,
chatFoldersById,
orderedFolderIds,
lastSyncTime,
notifySettings: selectNotifySettings(global),
notifyExceptions: selectNotifyExceptions(global),
activeChatFolder,
currentUserId,
lastSyncTime,
shouldSkipHistoryAnimations,
};
},

View File

@ -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<string, ApiChat>;
usersById: Record<string, ApiUser>;
listIds?: string[];
orderedPinnedIds?: string[];
chatFolder?: ApiChatFolder;
lastSyncTime?: number;
notifySettings: NotifySettings;
notifyExceptions?: Record<number, NotifyException>;
foldersDispatch?: FolderEditDispatch;
onScreenSelect?: (screen: SettingsScreens) => void;
};
enum FolderTypeToListType {
'all' = 'active',
'archived' = 'archived',
}
const ChatList: FC<OwnProps & StateProps> = ({
const ChatList: FC<OwnProps> = ({
folderType,
folderId,
isActive,
allListIds,
chatsById,
usersById,
listIds,
orderedPinnedIds,
chatFolder,
lastSyncTime,
notifySettings,
notifyExceptions,
foldersDispatch,
onScreenSelect,
}) => {
@ -77,30 +51,22 @@ const ChatList: FC<OwnProps & StateProps> = ({
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<string, number>);
return [newOrderById, newOrderedIds, newChatArrays];
}, [currentListIds, currentPinnedIds, folderType, chatFolder, chatsById]);
}, [orderedIds]);
const prevOrderById = usePrevious(orderById);
@ -126,14 +92,6 @@ const ChatList: FC<OwnProps & StateProps> = ({
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<OwnProps & StateProps> = ({
function renderChats() {
const viewportOffset = orderedIds!.indexOf(viewportIds![0]);
const pinnedOffset = viewportOffset + viewportChatArrays!.pinnedChats.length;
const pinnedCount = getPinnedChatsCount(virtualFolderId) || 0;
return (
<div
@ -187,12 +145,12 @@ const ChatList: FC<OwnProps & StateProps> = ({
style={IS_ANDROID ? `height: ${orderedIds!.length * CHAT_HEIGHT_PX}px` : undefined}
teactFastList
>
{viewportChatArrays!.pinnedChats.map(({ id }, i) => (
{viewportIds!.map((id, i) => (
<Chat
key={id}
teactOrderKey={i}
chatId={id}
isPinned
isPinned={viewportOffset + i < pinnedCount}
folderId={folderId}
animationType={getAnimationType(id)}
orderDiff={orderDiffById[id]}
@ -200,18 +158,6 @@ const ChatList: FC<OwnProps & StateProps> = ({
style={`top: ${(viewportOffset + i) * CHAT_HEIGHT_PX}px;`}
/>
))}
{viewportChatArrays!.otherChats.map((chat, i) => (
<Chat
key={chat.id}
teactOrderKey={getChatOrder(chat)}
chatId={chat.id}
folderId={folderId}
animationType={getAnimationType(chat.id)}
orderDiff={orderDiffById[chat.id]}
// @ts-ignore
style={`top: ${(pinnedOffset + i) * CHAT_HEIGHT_PX}px;`}
/>
))}
</div>
);
}
@ -225,7 +171,7 @@ const ChatList: FC<OwnProps & StateProps> = ({
noFastList
noScrollRestore
>
{viewportIds?.length && viewportChatArrays ? (
{viewportIds?.length ? (
renderChats()
) : viewportIds && !viewportIds.length ? (
(
@ -243,33 +189,4 @@ const ChatList: FC<OwnProps & StateProps> = ({
);
};
export default memo(withGlobal<OwnProps>(
(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);

View File

@ -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<string, ApiChat>;
listIds?: string[];
orderedPinnedIds?: string[];
archivedListIds?: string[];
archivedPinnedIds?: string[];
settings?: ApiPrivacySettings;
};
const SettingsPrivacyVisibilityExceptionList: FC<OwnProps & StateProps> = ({
currentUserId,
isAllowList,
screen,
settings,
chatsById,
listIds,
orderedPinnedIds,
archivedListIds,
archivedPinnedIds,
isActive,
onScreenSelect,
onReset,
currentUserId,
settings,
}) => {
const { setPrivacySettings } = getDispatch();
@ -69,46 +58,23 @@ const SettingsPrivacyVisibilityExceptionList: FC<OwnProps & StateProps> = ({
const [isSubmitShown, setIsSubmitShown] = useState<boolean>(false);
const [newSelectedContactIds, setNewSelectedContactIds] = useState<string[]>(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<OwnProps>(
(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),
};
},

View File

@ -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<string, ApiChat>;
listIds?: string[];
orderedPinnedIds?: string[];
archivedListIds?: string[];
archivedPinnedIds?: string[];
};
const SettingsFoldersChatFilters: FC<OwnProps & StateProps> = ({
isActive,
onScreenSelect,
onReset,
const SettingsFoldersChatFilters: FC<OwnProps> = ({
mode,
state,
dispatch,
chatsById,
listIds,
orderedPinnedIds,
archivedListIds,
archivedPinnedIds,
isActive,
onScreenSelect,
onReset,
}) => {
const { loadMoreChats } = getDispatch();
@ -56,38 +44,20 @@ const SettingsFoldersChatFilters: FC<OwnProps & StateProps> = ({
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<OwnProps & StateProps> = ({
);
};
export default memo(withGlobal<OwnProps>(
(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);

View File

@ -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<string, ApiChat>;
usersById: Record<string, ApiUser>;
orderedFolderIds?: number[];
foldersById: Record<number, ApiChatFolder>;
recommendedChatFolders?: ApiChatFolder[];
notifySettings: NotifySettings;
notifyExceptions?: Record<number, NotifyException>;
};
const runThrottledForLoadRecommended = throttle((cb) => cb(), 60000, true);
@ -45,18 +39,13 @@ const MAX_ALLOWED_FOLDERS = 10;
const SettingsFoldersMain: FC<OwnProps & StateProps> = ({
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<OwnProps & StateProps> = ({
useHistoryBack(isActive, onReset, onScreenSelect, SettingsScreens.Folders);
const chatsCountByFolderId = useFolderManagerForChatsCount();
const userFolders = useMemo(() => {
if (!orderedFolderIds) {
return undefined;
@ -112,12 +102,10 @@ const SettingsFoldersMain: FC<OwnProps & StateProps> = ({
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<OwnProps & StateProps> = ({
export default memo(withGlobal<OwnProps>(
(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<OwnProps>(
} = global.chatFolders;
return {
allListIds,
chatsById,
usersById,
orderedFolderIds,
foldersById,
recommendedChatFolders,
notifySettings: selectNotifySettings(global),
notifyExceptions: selectNotifyExceptions(global),
};
},
)(SettingsFoldersMain));

View File

@ -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<StateProps> = ({
const handleBlur = useCallback(() => {
updateIsOnline(false);
const initialUnread = selectCountNotMutedUnreadOptimized(getGlobal());
const initialUnread = getAllNotificationsCount();
let index = 0;
clearInterval(notificationInterval);
@ -259,7 +260,7 @@ const Main: FC<StateProps> = ({
}
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<StateProps> = ({
)}
<DownloadManager />
<CallFallbackConfirm isOpen={isCallFallbackConfirmOpen} />
<UnreadCount isForAppBadge />
</div>
);
};

View File

@ -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<OwnProps & StateProps> = ({
);
}
function renderBackButton(asClose = false, withUnreadCount = false) {
function renderBackButton(asClose = false, withUnreadCounter = false) {
return (
<div className="back-button">
<Button
@ -358,7 +358,7 @@ const MiddleHeader: FC<OwnProps & StateProps> = ({
>
<div className={buildClassName('animated-close-icon', !asClose && 'state-back')} />
</Button>
{withUnreadCount && <UnreadCount />}
{withUnreadCounter && <UnreadCounter />}
</div>
);
}

View File

@ -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<StateProps> = ({
unreadCount,
}) => {
if (!unreadCount) {
return undefined;
}
return (
<div className="unread-count active">{formatIntegerCompact(unreadCount)}</div>
);
};
export default memo(withGlobal(
(global: GlobalState): StateProps => {
return {
unreadCount: selectCountNotMutedUnreadOptimized(global),
};
},
)(UnreadCount));

View File

@ -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;
}

View File

@ -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<string>();
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 });
}
})();
});

View File

@ -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');

View File

@ -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,

View File

@ -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<string, ApiChat>,
usersById: Record<string, ApiUser>,
folder: ApiChatFolder,
notifySettings: NotifySettings,
notifyExceptions?: Record<number, NotifyException>,
) {
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<string, ApiUser>,
notifySettings: NotifySettings,
notifyExceptions?: Record<number, NotifyException>,
excludedChatIds?: Set<string>,
includedChatIds?: Set<string>,
pinnedChatIds?: Set<string>,
) {
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<string, ApiChat>,
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<string, ApiChat>,
usersById: Record<string, ApiUser>,
folder: ApiChatFolder,
notifySettings: NotifySettings,
notifyExceptions?: Record<number, NotifyException>,
) {
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<string, ApiChat>,
usersById: Record<string, ApiUser>,
folder: ApiChatFolder,
notifySettings: NotifySettings,
notifyExceptions?: Record<number, NotifyException>,
) {
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<string, ApiChat>,
usersById: Record<string, ApiUser>,
folder: ApiChatFolder,
notifySettings: NotifySettings,
notifyExceptions?: Record<string, NotifyException>,
) {
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;

View File

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

View File

@ -22,9 +22,16 @@ export function createCallbackManager() {
});
}
function hasCallbacks() {
return Boolean(callbacks.length);
}
return {
runCallbacks,
addCallback,
removeCallback,
hasCallbacks,
};
}
export type CallbackManager = ReturnType<typeof createCallbackManager>;

626
src/util/folderManager.ts Normal file
View File

@ -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<string>;
orderedPinnedIds?: string[];
contacts?: true;
nonContacts?: true;
groups?: true;
channels?: true;
bots?: true;
excludeMuted?: true;
excludeRead?: true;
excludeArchived?: true;
excludedChatIds?: Set<string>;
includedChatIds?: Set<string>;
pinnedChatIds?: Set<string>;
}
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<string, ApiChat>;
foldersById: Record<string, ApiChatFolder>;
usersById: Record<string, ApiUser>;
notifySettings: NotifySettings;
notifyExceptions?: Record<number, NotifyException>;
} = {
foldersById: {},
chatsById: {},
usersById: {},
notifySettings: {} as NotifySettings,
notifyExceptions: {},
};
const prepared: {
folderSummariesById: Record<string, FolderSummary>;
chatSummariesById: Map<string, ChatSummary>;
folderIdsByChatId: Record<string, number[]>;
chatIdsByFolderId: Record<number, Set<string>>;
isOrderedListJustPatched: Record<number, boolean | undefined>;
} = {
folderSummariesById: {},
chatSummariesById: new Map(),
folderIdsByChatId: {},
chatIdsByFolderId: {},
isOrderedListJustPatched: {},
};
const results: {
orderedIdsByFolderId: Record<string, string[]>;
chatsCountByFolderId: Record<string, number>;
unreadCountersByFolderId: Record<string, {
chatsCount: number;
notificationsCount: number;
}>;
} = {
orderedIdsByFolderId: {},
chatsCountByFolderId: {},
unreadCountersByFolderId: {},
};
const callbacks: {
orderedIdsByFolderId: Record<number, CallbackManager>;
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<number>();
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<number, NotifyException>,
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<number[]>((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,
});
}