stream-chat-react
Version:
React components to create chat conversations or livestream style chat
93 lines (92 loc) • 6.39 kB
JavaScript
import clsx from 'clsx';
import throttle from 'lodash.throttle';
import React from 'react';
import { EmptyStateIndicator as DefaultEmptyStateIndicator } from '../EmptyStateIndicator';
import { LoadingIndicator as DefaultLoadingIndicator } from '../Loading';
import { isMessageEdited, Message } from '../Message';
import { useComponentContext } from '../../context';
import { getIsFirstUnreadMessage, isDateSeparatorMessage, isIntroMessage } from './utils';
const PREPEND_OFFSET = 10 ** 7;
export function calculateItemIndex(virtuosoIndex, numItemsPrepended) {
return virtuosoIndex + numItemsPrepended - PREPEND_OFFSET;
}
export function calculateFirstItemIndex(numItemsPrepended) {
return PREPEND_OFFSET - numItemsPrepended;
}
export const makeItemsRenderedHandler = (renderedItemsActions, processedMessages) => throttle((items) => {
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));
}, 200);
// using 'display: inline-block'
// traps CSS margins of the item elements, preventing incorrect item measurements
export const Item = ({ context, ...props }) => {
if (!context)
return React.createElement(React.Fragment, null);
const message = context.processedMessages[calculateItemIndex(props['data-item-index'], context.numItemsPrepended)];
const groupStyles = context.messageGroupStyles[message.id];
return (React.createElement("div", { ...props, className: context?.customClasses?.virtualMessage ||
clsx('str-chat__virtual-list-message-wrapper str-chat__li', {
[`str-chat__li--${groupStyles}`]: groupStyles,
}) }));
};
export const Header = ({ context }) => {
const { LoadingIndicator = DefaultLoadingIndicator } = useComponentContext('VirtualizedMessageListHeader');
return (React.createElement(React.Fragment, null,
context?.head,
context?.loadingMore && LoadingIndicator && (React.createElement("div", { className: 'str-chat__virtual-list__loading' },
React.createElement(LoadingIndicator, { size: 20 })))));
};
export const EmptyPlaceholder = ({ context }) => {
const { EmptyStateIndicator = DefaultEmptyStateIndicator } = useComponentContext('VirtualizedMessageList');
// prevent showing that there are no messages if there actually are messages (for some reason virtuoso decides to render empty placeholder first, even though it has the totalCount prop > 0)
if (typeof context?.processedMessages !== 'undefined' &&
context.processedMessages.length > 0)
return null;
return (React.createElement(React.Fragment, null, EmptyStateIndicator && (React.createElement(EmptyStateIndicator, { listType: context?.threadList ? 'thread' : 'message' }))));
};
export const messageRenderer = (virtuosoIndex, _data, virtuosoContext) => {
const { additionalMessageInputProps, closeReactionSelectorOnClick, customMessageActions, customMessageRenderer, DateSeparator, firstUnreadMessageId, formatDate, lastReadDate, lastReadMessageId, lastReceivedMessageId, Message: MessageUIComponent, messageActions, messageGroupStyles, MessageSystem, numItemsPrepended, openThread, ownMessagesReadByOthers, processedMessages: messageList, reactionDetailsSort, shouldGroupByUser, sortReactionDetails, sortReactions, threadList, unreadMessageCount = 0, UnreadMessagesSeparator, virtuosoRef, } = virtuosoContext;
const streamMessageIndex = calculateItemIndex(virtuosoIndex, numItemsPrepended);
if (customMessageRenderer) {
return customMessageRenderer(messageList, streamMessageIndex);
}
const message = messageList[streamMessageIndex];
if (!message || isIntroMessage(message))
return React.createElement("div", { style: { height: '1px' } }); // returning null or zero height breaks the virtuoso
if (isDateSeparatorMessage(message)) {
return DateSeparator ? (React.createElement(DateSeparator, { date: message.date, unread: message.unread })) : null;
}
if (message.type === 'system') {
return MessageSystem ? React.createElement(MessageSystem, { message: message }) : null;
}
const maybePrevMessage = messageList[streamMessageIndex - 1];
const maybeNextMessage = messageList[streamMessageIndex + 1];
const groupedByUser = shouldGroupByUser &&
streamMessageIndex > 0 &&
message.user?.id === maybePrevMessage?.user?.id;
// FIXME: firstOfGroup & endOfGroup should be derived from groupStyles which apply a more complex logic
const firstOfGroup = shouldGroupByUser &&
(message.user?.id !== maybePrevMessage?.user?.id ||
(maybePrevMessage && isMessageEdited(maybePrevMessage)));
const endOfGroup = shouldGroupByUser &&
(message.user?.id !== maybeNextMessage?.user?.id || isMessageEdited(message));
const isFirstUnreadMessage = getIsFirstUnreadMessage({
firstUnreadMessageId,
isFirstMessage: streamMessageIndex === 0,
lastReadDate,
lastReadMessageId,
message,
previousMessage: streamMessageIndex ? messageList[streamMessageIndex - 1] : undefined,
unreadMessageCount,
});
return (React.createElement(React.Fragment, null,
isFirstUnreadMessage && (React.createElement("div", { className: 'str-chat__unread-messages-separator-wrapper' },
React.createElement(UnreadMessagesSeparator, { unreadCount: unreadMessageCount }))),
React.createElement(Message, { additionalMessageInputProps: additionalMessageInputProps, autoscrollToBottom: virtuosoRef.current?.autoscrollToBottom, closeReactionSelectorOnClick: closeReactionSelectorOnClick, customMessageActions: customMessageActions, endOfGroup: endOfGroup, firstOfGroup: firstOfGroup, formatDate: formatDate, groupedByUser: groupedByUser, groupStyles: [messageGroupStyles[message.id] ?? ''], lastReceivedId: lastReceivedMessageId, message: message, Message: MessageUIComponent, messageActions: messageActions, openThread: openThread, reactionDetailsSort: reactionDetailsSort, readBy: ownMessagesReadByOthers[message.id] || [], sortReactionDetails: sortReactionDetails, sortReactions: sortReactions, threadList: threadList })));
};