import clsx from 'clsx'; import throttle from 'lodash.throttle'; import React from 'react'; import type { ItemProps, ListItem } from 'react-virtuoso'; import { EmptyStateIndicator as DefaultEmptyStateIndicator } from '../EmptyStateIndicator'; import { LoadingIndicator as DefaultLoadingIndicator } from '../Loading'; import { Message } from '../Message'; import { useComponentContext } from '../../context'; import type { GroupStyle, RenderedMessage } from './utils'; import { getIsFirstUnreadMessage, isDateSeparatorMessage, isIntroMessage } from './utils'; import type { VirtuosoContext } from './VirtualizedMessageList'; import type { UnknownType } from '../../types/types'; import { useThreadContext } from '../Threads'; const PREPEND_OFFSET = 10 ** 7; export function calculateItemIndex(virtuosoIndex: number, numItemsPrepended: number) { return virtuosoIndex + numItemsPrepended - PREPEND_OFFSET; } export function calculateFirstItemIndex(numItemsPrepended: number) { return PREPEND_OFFSET - numItemsPrepended; } export const makeItemsRenderedHandler = ( renderedItemsActions: Array<(msg: RenderedMessage[]) => void>, processedMessages: RenderedMessage[], ) => throttle((items: ListItem[]) => { const renderedMessages = items .map((item) => { if (!item.originalIndex) return undefined; return processedMessages[calculateItemIndex(item.originalIndex, PREPEND_OFFSET)]; }) .filter((msg) => !!msg); renderedItemsActions.forEach((action) => action(renderedMessages as RenderedMessage[]), ); }, 200); type CommonVirtuosoComponentProps = { context?: VirtuosoContext; }; // `display: inline-block` traps the items' CSS margins, preventing incorrect item measurements. export const Item = ({ context, ...props }: ItemProps & CommonVirtuosoComponentProps) => { if (!context) return <>; const message = context.processedMessages[ calculateItemIndex(props['data-item-index'], context.numItemsPrepended) ]; const groupStyles: GroupStyle = context.messageGroupStyles[message.id]; return (
); }; export const Header = ({ context }: CommonVirtuosoComponentProps) => { const { LoadingIndicator = DefaultLoadingIndicator } = useComponentContext(); return ( <> {context?.head} {context?.loadingMore && LoadingIndicator && (
)} ); }; export const EmptyPlaceholder = ({ context }: CommonVirtuosoComponentProps) => { const thread = useThreadContext(); const isThreadList = !!thread; const { EmptyStateIndicator = DefaultEmptyStateIndicator } = useComponentContext(); // Virtuoso can render the empty placeholder first even when `totalCount` > 0, so hide it while // there are messages. if ( typeof context?.processedMessages !== 'undefined' && context.processedMessages.length > 0 ) return null; return ( <> {EmptyStateIndicator && ( )} ); }; export const messageRenderer = ( virtuosoIndex: number, _data: UnknownType, virtuosoContext: VirtuosoContext, ) => { const { additionalMessageComposerProps, channel, closeReactionSelectorOnClick, customMessageRenderer, DateSeparator, firstUnreadMessageId, focusedMessageId, formatDate, lastOwnMessage, lastReadDate, lastReadMessageId, lastReceivedMessageId, messageGroupStyles, MessageSystem, numItemsPrepended, ownMessagesDeliveredToOthers, ownMessagesReadByOthers, processedMessages: messageList, reactionDetailsSort, renderText, returnAllReadData, showAvatar, sortReactions, unreadMessageCount = 0, UnreadMessagesSeparator, virtuosoRef, } = virtuosoContext; const streamMessageIndex = calculateItemIndex(virtuosoIndex, numItemsPrepended); if (customMessageRenderer) { return customMessageRenderer(messageList, streamMessageIndex); } const message = messageList[streamMessageIndex]; // Virtuoso breaks on null or zero height. if (!message || isIntroMessage(message)) return
; if (isDateSeparatorMessage(message)) { return DateSeparator ? ( ) : null; } if (message.type === 'system') { return MessageSystem ? : null; } const isFirstUnreadMessage = getIsFirstUnreadMessage({ channel, firstUnreadMessageId, isFirstMessage: streamMessageIndex === 0, lastReadAt: lastReadDate, lastReadMessageId, message, previousMessage: streamMessageIndex ? messageList[streamMessageIndex - 1] : undefined, unreadCount: unreadMessageCount, }); return ( <> {isFirstUnreadMessage && (
)} ); };