[Perf] Various optimizations for calculating chat lists

This commit is contained in:
Alexander Zinchuk 2021-12-04 13:44:10 +01:00
parent 865ed08d82
commit 8015a7360e
6 changed files with 153 additions and 140 deletions

View File

@ -4,7 +4,7 @@ import React, {
import { withGlobal } from '../../../lib/teact/teactn';
import { ApiChat, ApiChatFolder, ApiUser } from '../../../api/types';
import { GlobalActions } from '../../../global/types';
import { GlobalActions, GlobalState } from '../../../global/types';
import { NotifyException, NotifySettings, SettingsScreens } from '../../../types';
import { FolderEditDispatch } from '../../../hooks/reducers/useFoldersReducer';
@ -30,6 +30,7 @@ type OwnProps = {
};
type StateProps = {
allListIds: GlobalState['chats']['listIds'];
chatsById: Record<string, ApiChat>;
usersById: Record<string, ApiUser>;
chatFoldersById: Record<number, ApiChatFolder>;
@ -48,6 +49,7 @@ const INFO_THROTTLE = 3000;
const SAVED_MESSAGES_HOTKEY = '0';
const ChatFolders: FC<OwnProps & StateProps & DispatchProps> = ({
allListIds,
chatsById,
usersById,
chatFoldersById,
@ -86,11 +88,10 @@ const ChatFolders: FC<OwnProps & StateProps & DispatchProps> = ({
return undefined;
}
const chatIds = Object.keys(chatsById);
const counters = displayedFolders.map((folder) => {
const {
unreadDialogsCount, hasActiveDialogs,
} = getFolderUnreadDialogs(chatsById, usersById, folder, chatIds, notifySettings, notifyExceptions) || {};
} = getFolderUnreadDialogs(allListIds, chatsById, usersById, folder, notifySettings, notifyExceptions) || {};
return {
id: folder.id,
@ -100,7 +101,7 @@ const ChatFolders: FC<OwnProps & StateProps & DispatchProps> = ({
});
return buildCollectionByKey(counters, 'id');
}, INFO_THROTTLE, [displayedFolders, chatsById, usersById, notifySettings, notifyExceptions]);
}, INFO_THROTTLE, [displayedFolders, allListIds, chatsById, usersById, notifySettings, notifyExceptions]);
const folderTabs = useMemo(() => {
if (!displayedFolders || !displayedFolders.length) {
@ -240,7 +241,7 @@ const ChatFolders: FC<OwnProps & StateProps & DispatchProps> = ({
export default memo(withGlobal<OwnProps>(
(global): StateProps => {
const {
chats: { byId: chatsById },
chats: { listIds: allListIds, byId: chatsById },
users: { byId: usersById },
chatFolders: {
byId: chatFoldersById,
@ -253,6 +254,7 @@ export default memo(withGlobal<OwnProps>(
} = global;
return {
allListIds,
chatsById,
usersById,
chatFoldersById,

View File

@ -3,7 +3,7 @@ import React, {
} from '../../../lib/teact/teact';
import { withGlobal } from '../../../lib/teact/teactn';
import { GlobalActions } from '../../../global/types';
import { GlobalActions, GlobalState } from '../../../global/types';
import {
ApiChat, ApiChatFolder, ApiUser,
} from '../../../api/types';
@ -37,11 +37,12 @@ type OwnProps = {
};
type StateProps = {
allListIds: GlobalState['chats']['listIds'];
chatsById: Record<string, ApiChat>;
usersById: Record<string, ApiUser>;
chatFolder?: ApiChatFolder;
listIds?: string[];
orderedPinnedIds?: string[];
chatFolder?: ApiChatFolder;
lastSyncTime?: number;
notifySettings: NotifySettings;
notifyExceptions?: Record<number, NotifyException>;
@ -60,15 +61,16 @@ const ChatList: FC<OwnProps & StateProps & DispatchProps> = ({
folderType,
folderId,
isActive,
chatFolder,
allListIds,
chatsById,
usersById,
listIds,
orderedPinnedIds,
chatFolder,
lastSyncTime,
foldersDispatch,
notifySettings,
notifyExceptions,
foldersDispatch,
onScreenSelect,
loadMoreChats,
preloadTopChatMessages,
@ -78,9 +80,12 @@ const ChatList: FC<OwnProps & StateProps & DispatchProps> = ({
}) => {
const [currentListIds, currentPinnedIds] = useMemo(() => {
return folderType === 'folder' && chatFolder
? prepareFolderListIds(chatsById, usersById, chatFolder, notifySettings, notifyExceptions)
? prepareFolderListIds(allListIds, chatsById, usersById, chatFolder, notifySettings, notifyExceptions)
: [listIds, orderedPinnedIds];
}, [folderType, chatFolder, chatsById, usersById, notifySettings, notifyExceptions, listIds, orderedPinnedIds]);
}, [
folderType, chatFolder, allListIds, chatsById, usersById,
notifySettings, notifyExceptions, listIds, orderedPinnedIds,
]);
const [orderById, orderedIds, chatArrays] = useMemo(() => {
if (!currentListIds || (folderType === 'folder' && !chatFolder)) {
@ -106,7 +111,7 @@ const ChatList: FC<OwnProps & StateProps & DispatchProps> = ({
}
return mapValues(orderById, (order, id) => {
return order - (prevOrderById[id] !== undefined ? prevOrderById[id] : Infinity);
return prevOrderById[id] !== undefined ? order - prevOrderById[id] : -Infinity;
});
}, [orderById, prevOrderById]);
@ -137,6 +142,39 @@ const ChatList: FC<OwnProps & StateProps & DispatchProps> = ({
}
}, [lastSyncTime, folderType, preloadTopChatMessages, preloadArchivedChats]);
// Support <Cmd>+<Digit> and <Alt>+<Up/Down> to navigate between chats
useEffect(() => {
if (!isActive || !orderedIds) {
return undefined;
}
function handleKeyDown(e: KeyboardEvent) {
if (IS_PWA && ((IS_MAC_OS && e.metaKey) || (!IS_MAC_OS && e.ctrlKey)) && e.code.startsWith('Digit')) {
const [, digit] = e.code.match(/Digit(\d)/) || [];
if (!digit) return;
const position = Number(digit) - 1;
if (position > orderedIds!.length - 1) return;
openChat({ id: orderedIds![position], shouldReplaceHistory: true });
}
if (e.altKey) {
const targetIndexDelta = e.key === 'ArrowDown' ? 1 : e.key === 'ArrowUp' ? -1 : undefined;
if (!targetIndexDelta) return;
e.preventDefault();
openNextChat({ targetIndexDelta, orderedIds });
}
}
document.addEventListener('keydown', handleKeyDown);
return () => {
document.removeEventListener('keydown', handleKeyDown);
};
}, [isActive, openChat, openNextChat, orderedIds]);
const getAnimationType = useChatAnimationType(orderDiffById);
function renderChats() {
@ -179,36 +217,6 @@ const ChatList: FC<OwnProps & StateProps & DispatchProps> = ({
);
}
useEffect(() => {
const handleKeyDown = (e: KeyboardEvent) => {
if (isActive && orderedIds) {
if (IS_PWA && ((IS_MAC_OS && e.metaKey) || (!IS_MAC_OS && e.ctrlKey)) && e.code.startsWith('Digit')) {
const [, digit] = e.code.match(/Digit(\d)/) || [];
if (!digit) return;
const position = Number(digit) - 1;
if (position > orderedIds.length - 1) return;
openChat({ id: orderedIds[position], shouldReplaceHistory: true });
}
if (e.altKey) {
const targetIndexDelta = e.key === 'ArrowDown' ? 1 : e.key === 'ArrowUp' ? -1 : undefined;
if (!targetIndexDelta) return;
e.preventDefault();
openNextChat({ targetIndexDelta, orderedIds });
}
}
};
document.addEventListener('keydown', handleKeyDown, false);
return () => {
document.removeEventListener('keydown', handleKeyDown, false);
};
});
return (
<InfiniteScroll
className="chat-list custom-scroll"
@ -251,6 +259,7 @@ export default memo(withGlobal<OwnProps>(
const chatFolder = folderId ? selectChatFolder(global, folderId) : undefined;
return {
allListIds: listIds,
chatsById,
usersById,
lastSyncTime,

View File

@ -1,4 +1,4 @@
import { useCallback } from '../../../../lib/teact/teact';
import { useMemo } from '../../../../lib/teact/teact';
export enum ChatAnimationTypes {
Move,
@ -7,29 +7,27 @@ export enum ChatAnimationTypes {
}
export function useChatAnimationType(orderDiffById: Record<string, number>) {
const movesUp = useCallback((id: string) => orderDiffById[id] < 0, [orderDiffById]);
const movesDown = useCallback((id: string) => orderDiffById[id] > 0, [orderDiffById]);
return useMemo(() => {
const orderDiffs = Object.values(orderDiffById);
const numberOfUp = orderDiffs.filter((diff) => diff < 0).length;
const numberOfDown = orderDiffs.filter((diff) => diff > 0).length;
const orderDiffIds = Object.keys(orderDiffById);
const numberOfUp = orderDiffIds.filter(movesUp).length;
const numberOfDown = orderDiffIds.filter(movesDown).length;
return (chatId: string): ChatAnimationTypes => {
const orderDiff = orderDiffById[chatId];
if (orderDiff === 0) {
return ChatAnimationTypes.None;
}
return useCallback((chatId: string): ChatAnimationTypes => {
const orderDiff = orderDiffById[chatId];
if (
orderDiff === Infinity
|| orderDiff === -Infinity
|| (numberOfUp <= numberOfDown && orderDiff < 0)
|| (numberOfDown < numberOfUp && orderDiff > 0)
) {
return ChatAnimationTypes.Opacity;
}
if (orderDiff === 0) {
return ChatAnimationTypes.None;
}
if (
orderDiff === Infinity
|| orderDiff === -Infinity
|| (numberOfUp <= numberOfDown && movesUp(chatId))
|| (numberOfDown < numberOfUp && movesDown(chatId))
) {
return ChatAnimationTypes.Opacity;
}
return ChatAnimationTypes.Move;
}, [movesDown, movesUp, numberOfDown, numberOfUp, orderDiffById]);
return ChatAnimationTypes.Move;
};
}, [orderDiffById]);
}

View File

@ -3,7 +3,7 @@ import React, {
} from '../../../../lib/teact/teact';
import { withGlobal } from '../../../../lib/teact/teactn';
import { GlobalActions } from '../../../../global/types';
import { GlobalActions, GlobalState } from '../../../../global/types';
import { ApiChatFolder, ApiChat, ApiUser } from '../../../../api/types';
import { NotifyException, NotifySettings, SettingsScreens } from '../../../../types';
@ -22,14 +22,15 @@ import Loading from '../../../ui/Loading';
import AnimatedSticker from '../../../common/AnimatedSticker';
type OwnProps = {
isActive?: boolean;
onCreateFolder: () => void;
onEditFolder: (folder: ApiChatFolder) => void;
isActive?: boolean;
onScreenSelect: (screen: SettingsScreens) => void;
onReset: () => void;
};
type StateProps = {
allListIds: GlobalState['chats']['listIds'];
chatsById: Record<string, ApiChat>;
usersById: Record<string, ApiUser>;
orderedFolderIds?: number[];
@ -46,11 +47,8 @@ const runThrottledForLoadRecommended = throttle((cb) => cb(), 60000, true);
const MAX_ALLOWED_FOLDERS = 10;
const SettingsFoldersMain: FC<OwnProps & StateProps & DispatchProps> = ({
onCreateFolder,
onEditFolder,
isActive,
onScreenSelect,
onReset,
allListIds,
chatsById,
usersById,
orderedFolderIds,
@ -58,6 +56,10 @@ const SettingsFoldersMain: FC<OwnProps & StateProps & DispatchProps> = ({
recommendedChatFolders,
notifySettings,
notifyExceptions,
onCreateFolder,
onEditFolder,
onScreenSelect,
onReset,
loadRecommendedChatFolders,
addChatFolder,
showDialog,
@ -104,8 +106,6 @@ const SettingsFoldersMain: FC<OwnProps & StateProps & DispatchProps> = ({
return undefined;
}
const chatIds = Object.keys(chatsById);
return orderedFolderIds.map((id) => {
const folder = foldersById[id];
@ -113,11 +113,11 @@ const SettingsFoldersMain: FC<OwnProps & StateProps & DispatchProps> = ({
id: folder.id,
title: folder.title,
subtitle: getFolderDescriptionText(
lang, chatsById, usersById, folder, chatIds, notifySettings, notifyExceptions,
lang, allListIds, chatsById, usersById, folder, notifySettings, notifyExceptions,
),
};
});
}, [orderedFolderIds, chatsById, foldersById, usersById, notifySettings, notifyExceptions, lang]);
}, [lang, allListIds, foldersById, chatsById, usersById, orderedFolderIds, notifySettings, notifyExceptions]);
const handleCreateFolderFromRecommended = useCallback((folder: ApiChatFolder) => {
if (Object.keys(foldersById).length >= MAX_ALLOWED_FOLDERS) {
@ -229,7 +229,7 @@ const SettingsFoldersMain: FC<OwnProps & StateProps & DispatchProps> = ({
export default memo(withGlobal<OwnProps>(
(global): StateProps => {
const {
chats: { byId: chatsById },
chats: { listIds: allListIds, byId: chatsById },
users: { byId: usersById },
} = global;
@ -240,6 +240,7 @@ export default memo(withGlobal<OwnProps>(
} = global.chatFolders;
return {
allListIds,
chatsById,
usersById,
orderedFolderIds,

View File

@ -76,7 +76,7 @@ addReducer('preloadTopChatMessages', (global, actions) => {
}
const { chatId: currentChatId } = selectCurrentMessageList(global) || {};
const { pinnedChats, otherChats } = prepareChatList(byId, listIds, orderedPinnedIds);
const { pinnedChats, otherChats } = prepareChatList(byId, listIds, orderedPinnedIds, 'all', true);
const topChats = [...pinnedChats, ...otherChats];
const chatToPreload = topChats.find(({ id }) => id !== currentChatId && !preloadedChatIds.includes(id));
if (!chatToPreload) {

View File

@ -7,6 +7,7 @@ import {
MAIN_THREAD_ID,
} from '../../api/types';
import { GlobalState } from '../../global/types';
import { NotifyException, NotifySettings } from '../../types';
import { LangFn } from '../../hooks/useLang';
@ -269,17 +270,17 @@ export function getCanDeleteChat(chat: ApiChat) {
}
export function prepareFolderListIds(
allListIds: GlobalState['chats']['listIds'],
chatsById: Record<string, ApiChat>,
usersById: Record<string, ApiUser>,
folder: ApiChatFolder,
notifySettings: NotifySettings,
notifyExceptions?: Record<number, NotifyException>,
chatIdsCache?: string[],
) {
const excludedChatIds = folder.excludedChatIds ? new Set(folder.excludedChatIds) : undefined;
const includedChatIds = folder.excludedChatIds ? new Set(folder.includedChatIds) : undefined;
const pinnedChatIds = folder.excludedChatIds ? new Set(folder.pinnedChatIds) : undefined;
const listIds = (chatIdsCache || Object.keys(chatsById))
const listIds = ([] as string[]).concat(allListIds.active || [], allListIds.archived || [])
.filter((id) => {
return filterChatFolder(
chatsById[id],
@ -296,6 +297,7 @@ export function prepareFolderListIds(
return [listIds, folder.pinnedChatIds] as const;
}
// This function is the most expensive in the project, so any possible optimizations are welcome
function filterChatFolder(
chat: ApiChat,
folder: ApiChatFolder,
@ -310,51 +312,55 @@ function filterChatFolder(
return false;
}
if (excludedChatIds && excludedChatIds.has(chat.id)) {
const { id: chatId, type, unreadMentionsCount } = chat;
if (excludedChatIds?.has(chatId)) {
return false;
}
if (includedChatIds && includedChatIds.has(chat.id)) {
if (includedChatIds?.has(chatId)) {
return true;
}
if (pinnedChatIds && pinnedChatIds.has(chat.id)) {
if (pinnedChatIds?.has(chatId)) {
return true;
}
if (isChatArchived(chat) && folder.excludeArchived) {
if (folder.excludeArchived && chat.folderId === ARCHIVED_FOLDER_ID) {
return false;
}
if (folder.excludeMuted && !chat.unreadMentionsCount && selectIsChatMuted(chat, notifySettings, notifyExceptions)) {
if (folder.excludeRead && !chat.unreadCount && !unreadMentionsCount && !chat.hasUnreadMark) {
return false;
}
if (!chat.unreadCount && !chat.unreadMentionsCount && !chat.hasUnreadMark && folder.excludeRead) {
if (folder.excludeMuted && !unreadMentionsCount && selectIsChatMuted(chat, notifySettings, notifyExceptions)) {
return false;
}
if (isUserId(chat.id)) {
const privateChatUser = usersById[chat.id];
if (type === 'chatTypePrivate') {
const user = usersById[chatId];
if (user) {
const { type: userType, isContact } = user;
const isChatWithBot = privateChatUser && privateChatUser.type === 'userTypeBot';
if (isChatWithBot) {
if (folder.bots) {
return true;
}
} else {
if (folder.contacts && privateChatUser && privateChatUser.isContact) {
return true;
}
if (userType === 'userTypeBot') {
if (folder.bots) {
return true;
}
} else {
if (folder.contacts && isContact) {
return true;
}
if (folder.nonContacts && privateChatUser && !privateChatUser.isContact) {
return true;
if (folder.nonContacts && !isContact) {
return true;
}
}
}
} else if (isChatGroup(chat)) {
return !!folder.groups;
} else if (isChatChannel(chat)) {
} else if (type === 'chatTypeChannel') {
return !!folder.channels;
} else if (type === 'chatTypeBasicGroup' || type === 'chatTypeSuperGroup') {
return !!folder.groups;
}
return false;
@ -365,6 +371,7 @@ export function prepareChatList(
listIds: string[],
orderedPinnedIds?: string[],
folderType: 'all' | 'archived' | 'folder' = 'all',
noOrder = false,
) {
const listIdsSet = new Set(listIds);
const orderedPinnedIdsSet = orderedPinnedIds ? new Set(orderedPinnedIds) : undefined;
@ -372,7 +379,7 @@ export function prepareChatList(
const pinnedChats = orderedPinnedIds?.reduce((acc, id) => {
const chat = chatsById[id];
if (chat && listIdsSet.has(chat.id) && chatFilter(chat, folderType)) {
if (chat && listIdsSet.has(chat.id) && checkChat(chat, folderType)) {
acc.push(chat);
}
@ -382,39 +389,25 @@ export function prepareChatList(
const otherChats = listIds.reduce((acc, id) => {
const chat = chatsById[id];
if (chat && (!orderedPinnedIdsSet || !orderedPinnedIdsSet.has(chat.id)) && chatFilter(chat, folderType)) {
if (chat && (!orderedPinnedIdsSet || !orderedPinnedIdsSet.has(chat.id)) && checkChat(chat, folderType)) {
acc.push(chat);
}
return acc;
}, [] as ApiChat[]);
const otherChatsOrdered = orderBy(otherChats, getChatOrder, 'desc');
return {
pinnedChats,
otherChats: otherChatsOrdered,
otherChats: noOrder ? otherChats : orderBy(otherChats, getChatOrder, 'desc'),
};
}
function chatFilter(chat: ApiChat, folderType: 'all' | 'archived' | 'folder') {
if (!chat.lastMessage || chat.migratedTo) {
return false;
}
switch (folderType) {
case 'all':
if (isChatArchived(chat)) {
return false;
}
break;
case 'archived':
if (!isChatArchived(chat)) {
return false;
}
break;
}
return !chat.isRestricted && !chat.isNotJoined;
function checkChat(chat: ApiChat, folderType: 'all' | 'archived' | 'folder') {
return (
chat.lastMessage && !chat.migratedTo && !chat.isRestricted && !chat.isNotJoined
&& !(folderType === 'all' && chat.folderId === ARCHIVED_FOLDER_ID)
&& !(folderType === 'archived' && chat.folderId !== ARCHIVED_FOLDER_ID)
);
}
export function reduceChatList(
@ -430,26 +423,36 @@ export function reduceChatList(
}
export function getFolderUnreadDialogs(
allListIds: GlobalState['chats']['listIds'],
chatsById: Record<string, ApiChat>,
usersById: Record<string, ApiUser>,
folder: ApiChatFolder,
chatIdsCache: string[],
notifySettings: NotifySettings,
notifyExceptions?: Record<number, NotifyException>,
) {
const [listIds] = prepareFolderListIds(chatsById, usersById, folder, notifySettings, notifyExceptions, chatIdsCache);
const [listIds] = prepareFolderListIds(allListIds, chatsById, usersById, folder, notifySettings, notifyExceptions);
const listedChats = listIds
.map((id) => chatsById[id])
.filter((chat) => (chat?.lastMessage && !chat.isRestricted && !chat.isNotJoined));
let hasActiveDialogs = false;
const unreadDialogsCount = listIds.reduce((acc, id) => {
const chat = chatsById[id];
if (!chat?.lastMessage || chat?.isRestricted || chat?.isNotJoined) {
return acc;
}
const unreadDialogsCount = listedChats
.reduce((total, chat) => (chat.unreadCount || chat.hasUnreadMark ? total + 1 : total), 0);
const isUnread = chat.unreadCount || chat.hasUnreadMark;
const hasActiveDialogs = listedChats.some((chat) => (
chat.unreadMentionsCount
|| (!selectIsChatMuted(chat, notifySettings, notifyExceptions) && (chat.unreadCount || chat.hasUnreadMark))
));
if (isUnread) {
acc++;
}
if (!hasActiveDialogs && (
chat.unreadMentionsCount || (isUnread && !selectIsChatMuted(chat, notifySettings, notifyExceptions))
)) {
hasActiveDialogs = true;
}
return acc;
}, 0);
return {
unreadDialogsCount,
@ -459,10 +462,10 @@ export function getFolderUnreadDialogs(
export function getFolderDescriptionText(
lang: LangFn,
allListIds: GlobalState['chats']['listIds'],
chatsById: Record<string, ApiChat>,
usersById: Record<string, ApiUser>,
folder: ApiChatFolder,
chatIdsCache: string[],
notifySettings: NotifySettings,
notifyExceptions?: Record<number, NotifyException>,
) {
@ -480,7 +483,7 @@ export function getFolderDescriptionText(
|| (excludedChatIds?.length)
|| (includedChatIds?.length)
) {
const length = getFolderChatsCount(chatsById, usersById, folder, chatIdsCache, notifySettings, notifyExceptions);
const length = getFolderChatsCount(allListIds, chatsById, usersById, folder, notifySettings, notifyExceptions);
return lang('Chats', length);
}
@ -501,17 +504,17 @@ export function getFolderDescriptionText(
}
function getFolderChatsCount(
allListIds: GlobalState['chats']['listIds'],
chatsById: Record<string, ApiChat>,
usersById: Record<string, ApiUser>,
folder: ApiChatFolder,
chatIdsCache: string[],
notifySettings: NotifySettings,
notifyExceptions?: Record<string, NotifyException>,
) {
const [listIds, pinnedIds] = prepareFolderListIds(
chatsById, usersById, folder, notifySettings, notifyExceptions, chatIdsCache,
allListIds, chatsById, usersById, folder, notifySettings, notifyExceptions,
);
const { pinnedChats, otherChats } = prepareChatList(chatsById, listIds, pinnedIds, 'folder');
const { pinnedChats, otherChats } = prepareChatList(chatsById, listIds, pinnedIds, 'folder', true);
return pinnedChats.length + otherChats.length;
}