import React, { FC, memo, useMemo, useCallback, useEffect, } from '../../../lib/teact/teact'; import { withGlobal } from '../../../lib/teact/teactn'; import { GlobalActions } from '../../../global/types'; import { ApiChat, ApiChatFolder, ApiUser, } from '../../../api/types'; import { NotifyException, NotifySettings } from '../../../types'; import { ALL_CHATS_PRELOAD_DISABLED, 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, pick } from '../../../util/iteratees'; import { getChatOrder, prepareChatList, prepareFolderListIds } from '../../../modules/helpers'; import { selectChatFolder, selectNotifyExceptions, selectNotifySettings, } from '../../../modules/selectors'; import useInfiniteScroll from '../../../hooks/useInfiniteScroll'; import { useChatAnimationType } from './hooks'; import InfiniteScroll from '../../ui/InfiniteScroll'; import Loading from '../../ui/Loading'; import Chat from './Chat'; type OwnProps = { folderType: 'all' | 'archived' | 'folder'; folderId?: number; noChatsText?: string; isActive: boolean; }; type StateProps = { chatsById: Record; usersById: Record; chatFolder?: ApiChatFolder; listIds?: number[]; orderedPinnedIds?: number[]; lastSyncTime?: number; notifySettings: NotifySettings; notifyExceptions?: Record; }; type DispatchProps = Pick; enum FolderTypeToListType { 'all' = 'active', 'archived' = 'archived' } const ChatList: FC = ({ folderType, folderId, noChatsText = 'Chat list is empty.', isActive, chatFolder, chatsById, usersById, listIds, orderedPinnedIds, lastSyncTime, notifySettings, notifyExceptions, loadMoreChats, preloadTopChatMessages, openChat, openNextChat, }) => { const [currentListIds, currentPinnedIds] = useMemo(() => { return folderType === 'folder' && chatFolder ? prepareFolderListIds(chatsById, usersById, chatFolder, notifySettings, notifyExceptions) : [listIds, orderedPinnedIds]; }, [folderType, chatFolder, chatsById, usersById, notifySettings, notifyExceptions, listIds, orderedPinnedIds]); const [orderById, orderedIds] = useMemo(() => { if (!currentListIds || (folderType === 'folder' && !chatFolder)) { return []; } const newChatArrays = prepareChatList(chatsById, currentListIds, currentPinnedIds, folderType); const singleList = [...newChatArrays.pinnedChats, ...newChatArrays.otherChats]; const newOrderedIds = singleList.map(({ id }) => id); const newOrderById = singleList.reduce((acc, chat, i) => { acc[chat.id] = i; return acc; }, {} as Record); return [newOrderById, newOrderedIds]; }, [currentListIds, currentPinnedIds, folderType, chatFolder, chatsById]); const prevOrderById = usePrevious(orderById); const orderDiffById = useMemo(() => { if (!orderById || !prevOrderById) { return {}; } return mapValues(orderById, (order, id) => { return order - (prevOrderById[id] !== undefined ? prevOrderById[id] : Infinity); }); }, [orderById, prevOrderById]); const loadMoreOfType = useCallback(() => { loadMoreChats({ listType: folderType === 'archived' ? 'archived' : 'active' }); }, [loadMoreChats, folderType]); const [viewportIds, getMore] = useInfiniteScroll( lastSyncTime ? loadMoreOfType : undefined, orderedIds, undefined, CHAT_LIST_SLICE, folderType === 'all' && !ALL_CHATS_PRELOAD_DISABLED, ); // TODO Refactor to not call `prepareChatList` twice const chatArrays = viewportIds && prepareChatList(chatsById, viewportIds, currentPinnedIds, folderType); useEffect(() => { if (lastSyncTime && folderType === 'all') { preloadTopChatMessages(); } }, [lastSyncTime, folderType, preloadTopChatMessages]); const getAnimationType = useChatAnimationType(orderDiffById); function renderChats() { const viewportOffset = orderedIds!.indexOf(viewportIds![0]); const pinnedOffset = viewportOffset + chatArrays!.pinnedChats.length; return (
{chatArrays!.pinnedChats.map(({ id }, i) => ( ))} {chatArrays!.otherChats.map((chat, i) => ( ))}
); } 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 ( {viewportIds && viewportIds.length && chatArrays ? ( renderChats() ) : viewportIds && !viewportIds.length ? (
{noChatsText}
) : ( )}
); }; export default memo(withGlobal( (global, { folderType, folderId }): StateProps => { const { chats: { listIds, byId: chatsById, orderedPinnedIds, }, users: { byId: usersById }, lastSyncTime, } = global; const listType = folderType !== 'folder' ? FolderTypeToListType[folderType] : undefined; const chatFolder = folderId ? selectChatFolder(global, folderId) : undefined; return { chatsById, usersById, lastSyncTime, notifySettings: selectNotifySettings(global), notifyExceptions: selectNotifyExceptions(global), ...(listType ? { listIds: listIds[listType], orderedPinnedIds: orderedPinnedIds[listType], } : { chatFolder, }), }; }, (setGlobal, actions): DispatchProps => pick(actions, [ 'loadMoreChats', 'preloadTopChatMessages', 'openChat', 'openNextChat', ]), )(ChatList));