You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
tailchat/web/src/components/ChatBox/ChatMessageList/VirtualizedList.tsx

125 lines
3.8 KiB
TypeScript

import React, { useMemo, useRef } from 'react';
import { buildMessageItemRow } from './Item';
import type { MessageListProps } from './types';
import {
FollowOutputScalarType,
Virtuoso,
VirtuosoGridHandle,
} from 'react-virtuoso';
import type { ChatMessage } from 'tailchat-shared';
import _last from 'lodash/last';
const PREPEND_OFFSET = 10 ** 7;
const virtuosoStyle: React.CSSProperties = {
height: '100%',
};
/**
* 新版的虚拟列表
* 参考: https://github.com/GetStream/stream-chat-react/blob/master/src/components/MessageList/VirtualizedMessageList.tsx
*/
export const VirtualizedMessageList: React.FC<MessageListProps> = React.memo(
(props) => {
const listRef = useRef<VirtuosoGridHandle>();
const numItemsPrepended = usePrependedMessagesCount(props.messages);
const handleLoadMore = () => {
if (props.isLoadingMore) {
return;
}
if (props.hasMoreMessage) {
props.onLoadMore();
}
};
const followOutput = (isAtBottom: boolean): FollowOutputScalarType => {
if (isAtBottom) {
// 更新最新查看的消息id
const lastMessage = _last(props.messages);
if (lastMessage) {
props.onUpdateReadedMessage(lastMessage._id);
}
setTimeout(() => {
// 这里 Virtuoso 有个动态渲染高度的bug, 因此需要异步再次滚动到底部以确保代码功能work
listRef.current?.scrollToIndex({
index:
PREPEND_OFFSET - numItemsPrepended + props.messages.length - 1,
align: 'end',
});
}, 20);
}
/**
* 如果有新的内容,且当前处于最底部时, 保持在最底部
*/
return isAtBottom ? 'smooth' : false;
};
const itemContent = (virtuosoIndex: number) => {
const index = virtuosoIndex + numItemsPrepended - PREPEND_OFFSET;
return buildMessageItemRow(props.messages, props.messages[index]._id);
};
return (
<Virtuoso
style={virtuosoStyle}
ref={listRef as any}
firstItemIndex={PREPEND_OFFSET - numItemsPrepended}
initialTopMostItemIndex={Math.max(props.messages.length - 1, 0)}
totalCount={props.messages.length}
overscan={20}
itemContent={itemContent}
alignToBottom={true}
startReached={handleLoadMore}
followOutput={followOutput}
defaultItemHeight={25}
atBottomThreshold={40}
/>
);
}
);
VirtualizedMessageList.displayName = 'VirtualizedMessageList';
function usePrependedMessagesCount(messages: ChatMessage[]) {
const currentFirstMessageId = messages?.[0]?._id;
const firstMessageId = useRef(currentFirstMessageId);
const earliestMessageId = useRef(currentFirstMessageId);
const previousNumItemsPrepended = useRef(0);
const numItemsPrepended = useMemo(() => {
if (!messages || !messages.length) {
return 0;
}
// if no new messages were prepended, return early (same amount as before)
if (currentFirstMessageId === earliestMessageId.current) {
return previousNumItemsPrepended.current;
}
if (!firstMessageId.current) {
firstMessageId.current = currentFirstMessageId;
}
earliestMessageId.current = currentFirstMessageId;
// if new messages were prepended, find out how many
// start with this number because there cannot be fewer prepended items than before
for (
let i = previousNumItemsPrepended.current;
i < messages.length;
i += 1
) {
if (messages[i]._id === firstMessageId.current) {
previousNumItemsPrepended.current = i;
return i;
}
}
return 0;
// TODO: there's a bug here, the messages prop is the same array instance (something mutates it)
// that's why the second dependency is necessary
}, [messages, messages?.length]);
return numItemsPrepended;
}