251 lines
7.6 KiB
TypeScript
251 lines
7.6 KiB
TypeScript
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<number, ApiChat>;
|
|
usersById: Record<number, ApiUser>;
|
|
chatFolder?: ApiChatFolder;
|
|
listIds?: number[];
|
|
orderedPinnedIds?: number[];
|
|
lastSyncTime?: number;
|
|
notifySettings: NotifySettings;
|
|
notifyExceptions?: Record<number, NotifyException>;
|
|
};
|
|
|
|
type DispatchProps = Pick<GlobalActions, 'loadMoreChats' | 'preloadTopChatMessages' | 'openChat' | 'openNextChat'>;
|
|
|
|
enum FolderTypeToListType {
|
|
'all' = 'active',
|
|
'archived' = 'archived'
|
|
}
|
|
|
|
const ChatList: FC<OwnProps & StateProps & DispatchProps> = ({
|
|
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<string, number>);
|
|
|
|
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 (
|
|
<div
|
|
className="scroll-container"
|
|
// @ts-ignore
|
|
style={IS_ANDROID ? `height: ${orderedIds!.length * CHAT_HEIGHT_PX}px` : undefined}
|
|
teactFastList
|
|
>
|
|
{chatArrays!.pinnedChats.map(({ id }, i) => (
|
|
<Chat
|
|
key={id}
|
|
teactOrderKey={i}
|
|
chatId={id}
|
|
isPinned
|
|
folderId={folderId}
|
|
animationType={getAnimationType(id)}
|
|
orderDiff={orderDiffById[id]}
|
|
// @ts-ignore
|
|
style={`top: ${(viewportOffset + i) * CHAT_HEIGHT_PX}px;`}
|
|
/>
|
|
))}
|
|
{chatArrays!.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>
|
|
);
|
|
}
|
|
|
|
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"
|
|
items={viewportIds}
|
|
onLoadMore={getMore}
|
|
preloadBackwards={CHAT_LIST_SLICE}
|
|
noFastList
|
|
noScrollRestore
|
|
>
|
|
{viewportIds && viewportIds.length && chatArrays ? (
|
|
renderChats()
|
|
) : viewportIds && !viewportIds.length ? (
|
|
<div className="no-results">{noChatsText}</div>
|
|
) : (
|
|
<Loading key="loading" />
|
|
)}
|
|
</InfiniteScroll>
|
|
);
|
|
};
|
|
|
|
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 {
|
|
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));
|