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

211 lines
6.1 KiB
TypeScript

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

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<VirtualizedMessageListProps> =
React.memo((props) => {
const listRef = useRef<DynamicSizeList>(null);
const postListRef = useRef<HTMLDivElement>(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 (
<div key={itemId} className="text-center text-gray-400">
{t('加载中...')}
</div>
);
} else if (itemId === messageReverseItemId.TEXT_CHANNEL_INTRO) {
return (
<div key={itemId} className="text-center text-gray-400">
{t('到顶了')}
</div>
);
}
const messages = props.messages;
const index = findMessageIndexWithId(messages, itemId); // TODO: 这里是因为mattermost的动态列表传的id因此只能这边再用id找回可以看看是否可以优化
if (index === -1) {
return <div />;
}
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 (
<div key={message._id} data-debug={JSON.stringify(index)}>
{showDate && (
<Divider className="text-sm opacity-40 px-6 font-normal select-none">
{getMessageTimeDiff(messageCreatedAt)}
</Divider>
)}
<ChatMessageItem showAvatar={showAvatar} payload={message} />
</div>
);
};
// 初始渲染范围
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 (
<AutoSizer>
{({ height, width }) => (
<DynamicSizeList
ref={listRef}
height={height}
width={width}
itemData={itemData}
overscanCountForward={OVERSCAN_COUNT_FORWARD}
overscanCountBackward={OVERSCAN_COUNT_BACKWARD}
onScroll={handleScroll}
initScrollToIndex={initScrollToIndex}
canLoadMorePosts={() => {}}
innerRef={postListRef}
style={{ ...virtListStyles, ...dynamicListStyle }}
innerListStyle={postListStyle}
initRangeToRender={initRangeToRender}
loaderId={messageReverseItemId.OLDER_MESSAGES_LOADER}
correctScrollToBottom={isBottom}
>
{renderRow}
</DynamicSizeList>
)}
</AutoSizer>
);
});
VirtualizedMessageList.displayName = 'VirtualizedMessageList';