import DynamicSizeList, { OnScrollInfo, } from '@/components/DynamicVirtualizedList/DynamicSizeList'; import { Divider } from 'antd'; import React, { useEffect, useMemo, useRef, useState } from 'react'; import AutoSizer from 'react-virtualized-auto-sizer'; import { ChatMessage, getMessageTimeDiff, shouldShowMessageTime, t, useUpdateRef, } from 'tailchat-shared'; import { messageReverseItemId } from './const'; import { ChatMessageItem } from './Item'; // Reference: https://github.com/mattermost/mattermost-webapp/blob/master/components/post_view/post_list_virtualized/post_list_virtualized.jsx const OVERSCAN_COUNT_BACKWARD = 80; const OVERSCAN_COUNT_FORWARD = 80; const HEIGHT_TRIGGER_FOR_MORE_POSTS = 200; // 触发加载更多的方法 const postListStyle = { padding: '14px 0px 7px', }; const virtListStyles: React.CSSProperties = { position: 'absolute', bottom: 0, maxHeight: '100%', }; const dynamicListStyle: React.CSSProperties = {}; // TODO function findMessageIndexWithId( messages: ChatMessage[], messageId: string ): number { return messages.findIndex((m) => m._id === messageId); } export interface VirtualizedMessageListProps { messages: ChatMessage[]; isLoadingMore: boolean; hasMoreMessage: boolean; onUpdateReadedMessage: (lastMessageId: string) => void; onLoadMore: () => void; } export const VirtualizedMessageList: React.FC = React.memo((props) => { const listRef = useRef(null); const postListRef = useRef(null); const [isBottom, setIsBottom] = useState(true); const handleScroll = (info: OnScrollInfo) => { const { clientHeight, scrollOffset, scrollHeight, scrollDirection, scrollUpdateWasRequested, } = info; if (scrollHeight <= 0) { return; } const didUserScrollBackwards = scrollDirection === 'backward' && !scrollUpdateWasRequested; const isOffsetWithInRange = scrollOffset < HEIGHT_TRIGGER_FOR_MORE_POSTS; if ( didUserScrollBackwards && isOffsetWithInRange && !props.isLoadingMore ) { // 加载更多历史信息 props.onLoadMore(); } if (clientHeight + scrollOffset === scrollHeight) { // 当前滚动条位于底部 setIsBottom(true); props.onUpdateReadedMessage( props.messages[props.messages.length - 1]._id ); } }; const onUpdateReadedMessageRef = useUpdateRef(props.onUpdateReadedMessage); useEffect(() => { if (props.messages.length === 0) { return; } if (postListRef.current?.scrollTop === 0) { // 当前列表在最低 onUpdateReadedMessageRef.current( props.messages[props.messages.length - 1]._id ); } }, [props.messages.length]); const initScrollToIndex = () => { return { index: 0, position: 'end' as const, }; }; /** * 渲染列表元素 */ const renderRow = ({ data, itemId }: any) => { if (itemId === messageReverseItemId.OLDER_MESSAGES_LOADER) { return (
{t('加载中...')}
); } else if (itemId === messageReverseItemId.TEXT_CHANNEL_INTRO) { return (
{t('到顶了')}
); } const messages = props.messages; const index = findMessageIndexWithId(messages, itemId); // TODO: 这里是因为mattermost的动态列表传的id因此只能这边再用id找回,可以看看是否可以优化 if (index === -1) { return
; } const message = messages[index]; let showDate = true; let showAvatar = true; const messageCreatedAt = new Date(message.createdAt ?? ''); if (index > 0) { // 当不是第一条数据时 // 进行时间合并 const prevMessage = messages[index - 1]; if ( !shouldShowMessageTime( new Date(prevMessage.createdAt ?? ''), messageCreatedAt ) ) { showDate = false; } // 进行头像合并(在同一时间块下 且发送者为同一人) if (showDate === false) { showAvatar = prevMessage.author !== message.author; } } return (
{showDate && ( {getMessageTimeDiff(messageCreatedAt)} )}
); }; // 初始渲染范围 const initRangeToRender = useMemo(() => [0, props.messages.length], []); const itemData = useMemo( () => [ ...props.messages.map((m) => m._id).reverse(), props.hasMoreMessage ? messageReverseItemId.OLDER_MESSAGES_LOADER : messageReverseItemId.TEXT_CHANNEL_INTRO, ], [props.messages, props.hasMoreMessage] ); return ( {({ height, width }) => ( {}} innerRef={postListRef} style={{ ...virtListStyles, ...dynamicListStyle }} innerListStyle={postListStyle} initRangeToRender={initRangeToRender} loaderId={messageReverseItemId.OLDER_MESSAGES_LOADER} correctScrollToBottom={isBottom} > {renderRow} )} ); }); VirtualizedMessageList.displayName = 'VirtualizedMessageList';