From 6ea9ff0ca01782ec1b4ceffffb74d9fa9a2fdc1c Mon Sep 17 00:00:00 2001 From: MartinCupela <32706194+MartinCupela@users.noreply.github.com> Date: Wed, 25 Oct 2023 14:21:28 +0200 Subject: [PATCH] feat(VirtualizedMessageList): allow to merge custom virtuoso components with the SDK defaults (#2140) Allow integrators to add only some custom virtuoso components via `VirtualizedMessageListProps['additionalVirtuosoProps']['components']` and the rest to be filled with the defaults provided by the SDK. --- .../DateSeparator/DateSeparator.tsx | 2 +- .../__tests__/DateSeparator.test.js | 4 + .../EventComponent/EventComponent.tsx | 2 +- .../__tests__/EventComponent.test.js | 1 + src/components/MessageList/MessageList.tsx | 8 +- .../MessageList/VirtualizedMessageList.tsx | 342 ++++++---------- .../VirtualizedMessageListComponents.tsx | 163 ++++++++ .../__tests__/VirtualizedMessageList.test.js | 2 +- .../VirtualizedMessageListComponents.test.js | 377 ++++++++++++++++++ ...tualizedMessageListComponents.test.js.snap | 183 +++++++++ .../MessageList/hooks/MessageList/index.ts | 4 + .../{ => MessageList}/useEnrichedMessages.ts | 10 +- .../useMessageListElements.tsx | 24 +- .../useMessageListScrollManager.ts | 6 +- .../useScrollLocationLogic.tsx | 4 +- .../hooks/VirtualizedMessageList/index.ts | 6 + .../useGiphyPreview.ts | 6 +- .../useMessageSetKey.ts | 33 ++ .../useNewMessageNotification.ts | 4 +- .../usePrependMessagesCount.ts | 4 +- .../useScrollToBottomOnNewMessage.ts | 58 +++ .../useShouldForceScrollToBottom.ts | 4 +- .../useMessageListScrollManager.test.js | 2 +- src/components/MessageList/hooks/index.ts | 14 +- 24 files changed, 997 insertions(+), 266 deletions(-) create mode 100644 src/components/MessageList/VirtualizedMessageListComponents.tsx create mode 100644 src/components/MessageList/__tests__/VirtualizedMessageListComponents.test.js create mode 100644 src/components/MessageList/__tests__/__snapshots__/VirtualizedMessageListComponents.test.js.snap create mode 100644 src/components/MessageList/hooks/MessageList/index.ts rename src/components/MessageList/hooks/{ => MessageList}/useEnrichedMessages.ts (87%) rename src/components/MessageList/hooks/{ => MessageList}/useMessageListElements.tsx (81%) rename src/components/MessageList/hooks/{ => MessageList}/useMessageListScrollManager.ts (93%) rename src/components/MessageList/hooks/{ => MessageList}/useScrollLocationLogic.tsx (95%) create mode 100644 src/components/MessageList/hooks/VirtualizedMessageList/index.ts rename src/components/MessageList/hooks/{ => VirtualizedMessageList}/useGiphyPreview.ts (80%) create mode 100644 src/components/MessageList/hooks/VirtualizedMessageList/useMessageSetKey.ts rename src/components/MessageList/hooks/{ => VirtualizedMessageList}/useNewMessageNotification.ts (92%) rename src/components/MessageList/hooks/{ => VirtualizedMessageList}/usePrependMessagesCount.ts (95%) create mode 100644 src/components/MessageList/hooks/VirtualizedMessageList/useScrollToBottomOnNewMessage.ts rename src/components/MessageList/hooks/{ => VirtualizedMessageList}/useShouldForceScrollToBottom.ts (86%) diff --git a/src/components/DateSeparator/DateSeparator.tsx b/src/components/DateSeparator/DateSeparator.tsx index 6eae90c24..a01d8c09d 100644 --- a/src/components/DateSeparator/DateSeparator.tsx +++ b/src/components/DateSeparator/DateSeparator.tsx @@ -27,7 +27,7 @@ const UnMemoizedDateSeparator = (props: DateSeparatorProps) => { }); return ( -
+
{(position === 'right' || position === 'center') && (
)} diff --git a/src/components/DateSeparator/__tests__/DateSeparator.test.js b/src/components/DateSeparator/__tests__/DateSeparator.test.js index b6a30e26f..cd750d484 100644 --- a/src/components/DateSeparator/__tests__/DateSeparator.test.js +++ b/src/components/DateSeparator/__tests__/DateSeparator.test.js @@ -47,6 +47,7 @@ describe('DateSeparator', () => { expect(tree).toMatchInlineSnapshot(`

{ expect(tree).toMatchInlineSnapshot(`

{ expect(tree).toMatchInlineSnapshot(`
{ expect(tree).toMatchInlineSnapshot(`

+

{text}

diff --git a/src/components/EventComponent/__tests__/EventComponent.test.js b/src/components/EventComponent/__tests__/EventComponent.test.js index 7dd5a8853..5ddf99a0e 100644 --- a/src/components/EventComponent/__tests__/EventComponent.test.js +++ b/src/components/EventComponent/__tests__/EventComponent.test.js @@ -30,6 +30,7 @@ describe('EventComponent', () => { expect(tree).toMatchInlineSnapshot(`
= { - customClasses: ChatProps['customClasses']; - /** Latest received message id in the current channel */ - lastReceivedMessageId: string | null | undefined; - messageGroupStyles: Record; - numItemsPrepended: number; - /** Mapping of message ID of own messages to the array of users, who read the given message */ - ownMessagesReadByOthers: Record[]>; - processedMessages: StreamMessage[]; -}; +> = Required, 'DateSeparator' | 'MessageSystem'>> & + Pick, VirtualizedMessageListPropsForContext> & + Pick, 'customClasses'> & { + /** Latest received message id in the current channel */ + lastReceivedMessageId: string | null | undefined; + /** Object mapping between the message ID and a string representing the position in the group of a sequence of messages posted by the same user. */ + messageGroupStyles: Record; + /** Number of messages prepended before the first page of messages. This is needed to calculate the virtual position in the virtual list. */ + numItemsPrepended: number; + /** Mapping of message ID of own messages to the array of users, who read the given message */ + ownMessagesReadByOthers: Record[]>; + /** The original message list enriched with date separators, omitted deleted messages or giphy previews. */ + processedMessages: StreamMessage[]; + /** Instance of VirtuosoHandle object providing the API to navigate in the virtualized list by various scroll actions. */ + virtuosoRef: RefObject; + }; type VirtualizedMessageListWithContextProps< StreamChatGenerics extends DefaultStreamChatGenerics = DefaultStreamChatGenerics @@ -120,9 +142,11 @@ const VirtualizedMessageListWithContext = < props: VirtualizedMessageListWithContextProps, ) => { const { - additionalVirtuosoProps, + additionalMessageInputProps, + additionalVirtuosoProps = {}, channel, closeReactionSelectorOnClick, + customMessageActions, customMessageRenderer, defaultItemHeight, disableDateSeparator = true, @@ -137,7 +161,8 @@ const VirtualizedMessageListWithContext = < loadingMore, loadMore, loadMoreNewer, - Message: propMessage, + Message: MessageUIComponentFromProps, + messageActions, messageLimit = 100, messages, notifications, @@ -154,27 +179,30 @@ const VirtualizedMessageListWithContext = < threadList, } = props; + const { + components: virtuosoComponentsFromProps, + ...overridingVirtuosoProps + } = additionalVirtuosoProps; + // Stops errors generated from react-virtuoso to bubble up // to Sentry or other tracking tools. useCaptureResizeObserverExceededError(); const { DateSeparator = DefaultDateSeparator, - EmptyStateIndicator = DefaultEmptyStateIndicator, GiphyPreviewMessage = DefaultGiphyPreviewMessage, - LoadingIndicator = DefaultLoadingIndicator, MessageListNotifications = DefaultMessageListNotifications, MessageNotification = DefaultMessageNotification, MessageSystem = EventComponent, - TypingIndicator = null, - VirtualMessage: contextMessage = MessageSimple, + VirtualMessage: MessageUIComponentFromContext = MessageSimple, } = useComponentContext('VirtualizedMessageList'); + const MessageUIComponent = MessageUIComponentFromProps || MessageUIComponentFromContext; const { client, customClasses } = useChatContext('VirtualizedMessageList'); - const lastRead = useMemo(() => channel.lastRead?.(), [channel]); + const virtuoso = useRef(null); - const MessageUIComponent = propMessage || contextMessage; + const lastRead = useMemo(() => channel.lastRead?.(), [channel]); const { giphyPreviewMessage, setGiphyPreviewMessage } = useGiphyPreview( separateGiphyPreview, @@ -239,11 +267,9 @@ const VirtualizedMessageListWithContext = < return acc; }, {}), // processedMessages were incorrectly rebuilt with a new object identity at some point, hence the .length usage - [processedMessages.length, shouldGroupByUser], + [processedMessages.length, shouldGroupByUser, groupStylesFn], ); - const virtuoso = useRef(null); - const { atBottom, isMessageListScrolledToBottom, @@ -273,56 +299,11 @@ const VirtualizedMessageListWithContext = < jumpToLatestMessage, ]); - const [newMessagesReceivedInBackground, setNewMessagesReceivedInBackground] = React.useState( - false, - ); - - const resetNewMessagesReceivedInBackground = useCallback(() => { - setNewMessagesReceivedInBackground(false); - }, []); - - useEffect(() => { - setNewMessagesReceivedInBackground(true); - }, [messages]); - - const scrollToBottomIfConfigured = useCallback( - (event: Event) => { - if (scrollToLatestMessageOnFocus && event.target === window) { - if (newMessagesReceivedInBackground) { - setTimeout(scrollToBottom, 100); - } - } - }, - [scrollToLatestMessageOnFocus, scrollToBottom, newMessagesReceivedInBackground], - ); - - useEffect(() => { - if (typeof window !== 'undefined') { - window.addEventListener('focus', scrollToBottomIfConfigured); - window.addEventListener('blur', resetNewMessagesReceivedInBackground); - } - - return () => { - window.removeEventListener('focus', scrollToBottomIfConfigured); - window.removeEventListener('blur', resetNewMessagesReceivedInBackground); - }; - }, [scrollToBottomIfConfigured]); + useScrollToBottomOnNewMessage({ messages, scrollToBottom, scrollToLatestMessageOnFocus }); const numItemsPrepended = usePrependedMessagesCount(processedMessages, !disableDateSeparator); - /** - * Logic to update the key of the virtuoso component when the list jumps to a new location. - */ - const [messageSetKey, setMessageSetKey] = useState(+new Date()); - const firstMessageId = useRef(); - - useEffect(() => { - const continuousSet = messages?.find((message) => message.id === firstMessageId.current); - if (!continuousSet) { - setMessageSetKey(+new Date()); - } - firstMessageId.current = messages?.[0]?.id; - }, [messages]); + const { messageSetKey } = useMessageSetKey({ messages }); const shouldForceScrollToBottom = useShouldForceScrollToBottom(processedMessages, client.userID); @@ -338,117 +319,14 @@ const VirtualizedMessageListWithContext = < return isAtBottom ? stickToBottomScrollBehavior : false; }; - const messageRenderer = useCallback( - ( - messageList: StreamMessage[], - virtuosoIndex: number, - virtuosoContext: VirtuosoContext, - ) => { - const { lastReceivedMessageId, ownMessagesReadByOthers } = virtuosoContext; - const streamMessageIndex = virtuosoIndex + numItemsPrepended - PREPEND_OFFSET; - // use custom renderer supplied by client if present and skip the rest - if (customMessageRenderer) { - return customMessageRenderer(messageList, streamMessageIndex); - } - - const message = messageList[streamMessageIndex]; - - if (message.customType === CUSTOM_MESSAGE_TYPE.date && message.date && isDate(message.date)) { - return ; - } - - if (!message) return
; // returning null or zero height breaks the virtuoso - - if (message.type === 'system') { - return ; - } - - const groupedByUser = - shouldGroupByUser && - streamMessageIndex > 0 && - message.user?.id === messageList[streamMessageIndex - 1].user?.id; - - const firstOfGroup = - shouldGroupByUser && message.user?.id !== messageList[streamMessageIndex - 1]?.user?.id; - - const endOfGroup = - shouldGroupByUser && message.user?.id !== messageList[streamMessageIndex + 1]?.user?.id; - - return ( - - ); - }, - [customMessageRenderer, shouldGroupByUser, numItemsPrepended], + const computeItemKey = useCallback< + ComputeItemKey> + >( + (index, _, { numItemsPrepended, processedMessages }) => + processedMessages[calculateItemIndex(index, numItemsPrepended)].id, + [], ); - const Item = useMemo(() => { - // using 'display: inline-block' - // traps CSS margins of the item elements, preventing incorrect item measurements - const Item: Components['Item'] = (props) => { - const context = props.context as VirtuosoContext; - - const streamMessageIndex = - props['data-item-index'] + context.numItemsPrepended - PREPEND_OFFSET; - const message = context.processedMessages[streamMessageIndex]; - const groupStyles: GroupStyle = context.messageGroupStyles[message.id] || ''; - - return ( -
- ); - }; - return Item; - }, []); - - const virtuosoComponents: Partial = useMemo(() => { - const EmptyPlaceholder: Components['EmptyPlaceholder'] = () => ( - <> - {EmptyStateIndicator && ( - - )} - - ); - - const Header: Components['Header'] = () => - loadingMore ? ( -
- -
- ) : ( - head || null - ); - - const Footer: Components['Footer'] = () => - TypingIndicator ? : <>; - - return { - EmptyPlaceholder, - Footer, - Header, - Item, - }; - }, [loadingMore, head, Item]); - const atBottomStateChange = (isAtBottom: boolean) => { atBottom.current = isAtBottom; setIsMessageListScrolledToBottom(isAtBottom); @@ -484,35 +362,48 @@ const VirtualizedMessageListWithContext = < <>
- > atBottomStateChange={atBottomStateChange} atBottomThreshold={200} className='str-chat__message-list-scroll' - components={virtuosoComponents} - computeItemKey={(index) => - processedMessages[numItemsPrepended + index - PREPEND_OFFSET].id - } - context={ - { - customClasses, - lastReceivedMessageId, - messageGroupStyles, - numItemsPrepended, - ownMessagesReadByOthers, - processedMessages, - } as VirtuosoContext - } + components={{ + EmptyPlaceholder, + Footer, + Header, + Item, + ...virtuosoComponentsFromProps, + }} + computeItemKey={computeItemKey} + context={{ + additionalMessageInputProps, + closeReactionSelectorOnClick, + customClasses, + customMessageActions, + customMessageRenderer, + DateSeparator, + head, + lastReceivedMessageId, + loadingMore, + Message: MessageUIComponent, + messageActions, + messageGroupStyles, + MessageSystem, + numItemsPrepended, + ownMessagesReadByOthers, + processedMessages, + shouldGroupByUser, + threadList, + virtuosoRef: virtuoso, + }} endReached={endReached} - firstItemIndex={PREPEND_OFFSET - numItemsPrepended} + firstItemIndex={calculateFirstItemIndex(numItemsPrepended)} followOutput={followOutput} increaseViewportBy={{ bottom: 200, top: 0 }} initialTopMostItemIndex={calculateInitialTopMostItemIndex( processedMessages, highlightedMessageId, )} - itemContent={(i, _, context) => - messageRenderer(processedMessages, i, context as VirtuosoContext) - } + itemContent={messageRenderer} itemSize={fractionalItemSize} key={messageSetKey} overscan={overscan} @@ -520,7 +411,7 @@ const VirtualizedMessageListWithContext = < startReached={startReached} style={{ overflowX: 'hidden' }} totalCount={processedMessages.length} - {...additionalVirtuosoProps} + {...overridingVirtuosoProps} {...(scrollSeekPlaceHolder ? { scrollSeek: scrollSeekPlaceHolder } : {})} {...(defaultItemHeight ? { defaultItemHeight } : {})} /> @@ -549,7 +440,7 @@ export type VirtualizedMessageListProps< StreamChatGenerics extends DefaultStreamChatGenerics = DefaultStreamChatGenerics > = Partial, PropsDrilledToMessage>> & { /** Additional props to be passed the underlying [`react-virtuoso` virtualized list dependency](https://virtuoso.dev/virtuoso-api-reference/) */ - additionalVirtuosoProps?: VirtuosoProps; + additionalVirtuosoProps?: VirtuosoProps>; /** If true, picking a reaction from the `ReactionSelector` component will close the selector */ closeReactionSelectorOnClick?: boolean; /** Custom render function, if passed, certain UI props are ignored */ @@ -557,7 +448,9 @@ export type VirtualizedMessageListProps< messageList: StreamMessage[], index: number, ) => React.ReactElement; - /** If set, the default item height is used for the calculation of the total list height. Use if you expect messages with a lot of height variance */ + /** @deprecated Use additionalVirtuosoProps.defaultItemHeight instead. Will be removed with next major release - `v11.0.0`. + * If set, the default item height is used for the calculation of the total list height. Use if you expect messages with a lot of height variance + * */ defaultItemHeight?: number; /** Disables the injection of date separator components in MessageList, defaults to `true` */ disableDateSeparator?: boolean; @@ -572,7 +465,10 @@ export type VirtualizedMessageListProps< hasMore?: boolean; /** Whether or not the list has newer items to load */ hasMoreNewer?: boolean; - /** Element to be rendered at the top of the thread message list. By default, these are the Message and ThreadStart components */ + /** + * @deprecated Use additionalVirtuosoProps.components.Header to override default component rendered above the list ove messages. + * Element to be rendered at the top of the thread message list. By default, these are the Message and ThreadStart components + */ head?: React.ReactElement; /** Hides the `MessageDeleted` components from the list, defaults to `false` */ hideDeletedMessages?: boolean; @@ -594,11 +490,15 @@ export type VirtualizedMessageListProps< messageLimit?: number; /** Optional prop to override the messages available from [ChannelStateContext](https://getstream.io/chat/docs/sdk/react/contexts/channel_state_context/) */ messages?: StreamMessage[]; - /** The amount of extra content the list should render in addition to what's necessary to fill in the viewport */ + /** + * @deprecated Use additionalVirtuosoProps.overscan instead. Will be removed with next major release - `v11.0.0`. + * The amount of extra content the list should render in addition to what's necessary to fill in the viewport + */ overscan?: number; /** Keep track of read receipts for each message sent by the user. When disabled, only the last own message delivery / read status is rendered. */ returnAllReadData?: boolean; /** + * @deprecated Pass additionalVirtuosoProps.scrollSeekConfiguration and specify the placeholder in additionalVirtuosoProps.components.ScrollSeekPlaceholder instead. Will be removed with next major release - `v11.0.0`. * Performance improvement by showing placeholders if user scrolls fast through list. * it can be used like this: * ``` diff --git a/src/components/MessageList/VirtualizedMessageListComponents.tsx b/src/components/MessageList/VirtualizedMessageListComponents.tsx new file mode 100644 index 000000000..4fd94cf36 --- /dev/null +++ b/src/components/MessageList/VirtualizedMessageListComponents.tsx @@ -0,0 +1,163 @@ +import { DefaultStreamChatGenerics, UnknownType } from '../../types/types'; +import { EmptyStateIndicator as DefaultEmptyStateIndicator } from '../EmptyStateIndicator'; +import { isDate, useComponentContext } from '../../context'; +import React from 'react'; +import { LoadingIndicator as DefaultLoadingIndicator } from '../Loading'; +import { CUSTOM_MESSAGE_TYPE } from '../../constants/messageTypes'; +import { Message } from '../Message'; +import { ItemProps } from 'react-virtuoso'; +import { GroupStyle } from './utils'; +import clsx from 'clsx'; +import { VirtuosoContext } from './VirtualizedMessageList'; + +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; +} + +type CommonVirtuosoComponentProps< + StreamChatGenerics extends DefaultStreamChatGenerics = DefaultStreamChatGenerics +> = { + context?: VirtuosoContext; +}; +// using 'display: inline-block' +// traps CSS margins of the item elements, preventing incorrect item measurements +export const Item = < + StreamChatGenerics extends DefaultStreamChatGenerics = DefaultStreamChatGenerics +>({ + 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 = < + StreamChatGenerics extends DefaultStreamChatGenerics = DefaultStreamChatGenerics +>({ + context, +}: CommonVirtuosoComponentProps) => { + const { LoadingIndicator = DefaultLoadingIndicator } = useComponentContext( + 'VirtualizedMessageListHeader', + ); + if (!context?.loadingMore) return null; + return LoadingIndicator ? ( +
+ +
+ ) : ( + context?.head || null + ); +}; +export const EmptyPlaceholder = < + StreamChatGenerics extends DefaultStreamChatGenerics = DefaultStreamChatGenerics +>({ + context, +}: CommonVirtuosoComponentProps) => { + const { + EmptyStateIndicator = DefaultEmptyStateIndicator, + } = useComponentContext('VirtualizedMessageList'); + return ( + <> + {EmptyStateIndicator && ( + + )} + + ); +}; +export const Footer = < + StreamChatGenerics extends DefaultStreamChatGenerics = DefaultStreamChatGenerics +>() => { + const { TypingIndicator } = useComponentContext('VirtualizedMessageList'); + return TypingIndicator ? : null; +}; +export const messageRenderer = < + StreamChatGenerics extends DefaultStreamChatGenerics = DefaultStreamChatGenerics +>( + virtuosoIndex: number, + _data: UnknownType, + virtuosoContext: VirtuosoContext, +) => { + const { + additionalMessageInputProps, + closeReactionSelectorOnClick, + customMessageActions, + customMessageRenderer, + DateSeparator, + lastReceivedMessageId, + Message: MessageUIComponent, + messageActions, + MessageSystem, + numItemsPrepended, + ownMessagesReadByOthers, + processedMessages: messageList, + shouldGroupByUser, + virtuosoRef, + } = virtuosoContext; + + const streamMessageIndex = calculateItemIndex(virtuosoIndex, numItemsPrepended); + + if (customMessageRenderer) { + return customMessageRenderer(messageList, streamMessageIndex); + } + + const message = messageList[streamMessageIndex]; + + if (!message) return
; // returning null or zero height breaks the virtuoso + + if (message.customType === CUSTOM_MESSAGE_TYPE.date && message.date && isDate(message.date)) { + return DateSeparator ? : null; + } + + if (message.type === 'system') { + return MessageSystem ? : null; + } + + const groupedByUser = + shouldGroupByUser && + streamMessageIndex > 0 && + message.user?.id === messageList[streamMessageIndex - 1].user?.id; + const firstOfGroup = + shouldGroupByUser && message.user?.id !== messageList[streamMessageIndex - 1]?.user?.id; + + const endOfGroup = + shouldGroupByUser && message.user?.id !== messageList[streamMessageIndex + 1]?.user?.id; + + return ( + + ); +}; diff --git a/src/components/MessageList/__tests__/VirtualizedMessageList.test.js b/src/components/MessageList/__tests__/VirtualizedMessageList.test.js index 3ce12004a..a928db091 100644 --- a/src/components/MessageList/__tests__/VirtualizedMessageList.test.js +++ b/src/components/MessageList/__tests__/VirtualizedMessageList.test.js @@ -13,7 +13,7 @@ import { useMockedApis, } from '../../../mock-builders'; -import { usePrependedMessagesCount } from '../hooks/usePrependMessagesCount'; +import { usePrependedMessagesCount } from '../hooks'; import { VirtualizedMessageList } from '../VirtualizedMessageList'; import { Chat } from '../../Chat'; diff --git a/src/components/MessageList/__tests__/VirtualizedMessageListComponents.test.js b/src/components/MessageList/__tests__/VirtualizedMessageListComponents.test.js new file mode 100644 index 000000000..449898900 --- /dev/null +++ b/src/components/MessageList/__tests__/VirtualizedMessageListComponents.test.js @@ -0,0 +1,377 @@ +import React from 'react'; +import '@testing-library/jest-dom'; +import { render, screen } from '@testing-library/react'; +import { + EmptyPlaceholder, + Footer, + Header, + Item, + messageRenderer, +} from '../VirtualizedMessageListComponents'; +import { + generateChannel, + generateMessage, + generateUser, + getTestClientWithUser, +} from '../../../mock-builders'; +import { + ChannelActionProvider, + ChannelStateProvider, + ChatProvider, + ComponentProvider, +} from '../../../context'; +import { MessageSimple } from '../../Message'; + +const prependOffset = 0; +const user1 = generateUser(); +const user2 = generateUser(); +let client; +let channel; + +const PREPEND_OFFSET = 10 ** 7; + +const Wrapper = ({ children, componentContext = {} }) => ( + + + + {children} + + + +); + +const renderElements = (children, componentContext) => + render({children}); + +describe('VirtualizedMessageComponents', () => { + describe('Item', function () { + const processedMessages = [generateMessage()]; + const withVirtualMessageClasses = { virtualMessage: 'XXX' }; + const withMessageGroupStyles = { [processedMessages[0].id]: 'single' }; + const withoutVirtualMessageClasses = undefined; + const withoutMessageGroupStyles = {}; + + it.each([ + ['with', 'without', withVirtualMessageClasses, withoutMessageGroupStyles], + ['without', 'without', withoutVirtualMessageClasses, withoutMessageGroupStyles], + ['without', 'with', withoutVirtualMessageClasses, withMessageGroupStyles], + ['with', 'with', withVirtualMessageClasses, withMessageGroupStyles], + ])( + 'should render wrapper %s custom classes %s group styles', + (_, __, customClasses, messageGroupStyles) => { + const props = { + 'data-item-index': PREPEND_OFFSET, + }; + const virtuosoContext = { + customClasses, + messageGroupStyles, + numItemsPrepended: 0, + processedMessages, + }; + + const { container } = renderElements(); + expect(container).toMatchSnapshot(); + }, + ); + }); + + describe('Header', () => { + const head =
Custom head
; + const CustomLoadingIndicator = () =>
Custom Loading Indicator
; + it('should render empty div in Header when not loading more messages', () => { + const { container } = renderElements(
); + expect(container).toMatchInlineSnapshot(`
`); + }); + + it('should render LoadingIndicator in Header when loading more messages', () => { + const context = { loadingMore: true }; + const { container } = renderElements(
); + expect(container).toMatchSnapshot(); + }); + + it('should render custom LoadingIndicator in Header when loading more messages', () => { + const componentContext = { LoadingIndicator: CustomLoadingIndicator }; + const context = { loadingMore: true }; + const { container } = renderElements(
, componentContext); + expect(container).toMatchInlineSnapshot(` +
+
+
+ Custom Loading Indicator +
+
+
+ `); + }); + + it('should not render custom LoadingIndicator in Header when not loading more messages', () => { + const componentContext = { LoadingIndicator: CustomLoadingIndicator }; + const { container } = renderElements(
, componentContext); + expect(container).toMatchInlineSnapshot(`
`); + }); + + // FIXME: this is a crazy pattern of having to set LoadingIndicator to null so that additionalVirtuosoProps.head can be rendered. + it('should not render custom head in Header when loading more messages, but the LoadingIndicator', () => { + const context = { head, loadingMore: true }; + const { container } = renderElements(
); + expect(container).toMatchSnapshot(); + }); + + // FIXME: this is a crazy pattern of having to set LoadingIndicator to null so that additionalVirtuosoProps.head can be rendered. + it('should render custom head in Header when LoadingIndicator in component context is set to null', () => { + const componentContext = { + LoadingIndicator: null, + }; + const context = { head, loadingMore: true }; + const { container } = renderElements(
, componentContext); + expect(container).toMatchInlineSnapshot(` +
+
+ Custom head +
+
+ `); + }); + + it('should not render custom head in Header when not loading more messages', () => { + const context = { head }; + const { container } = renderElements(
); + expect(container).toMatchInlineSnapshot(`
`); + }); + + it('should render custom LoadingIndicator instead of head when loading more', () => { + const componentContext = { LoadingIndicator: CustomLoadingIndicator }; + const context = { head, loadingMore: true }; + const { container } = renderElements(
, componentContext); + expect(container).toMatchInlineSnapshot(` +
+
+
+ Custom Loading Indicator +
+
+
+ `); + }); + }); + + describe('EmptyPlaceholder', () => { + const EmptyStateIndicator = ({ listType }) => ( +
Custom EmptyStateIndicator
+ ); + const NullEmptyStateIndicator = null; + const componentContext = { EmptyStateIndicator }; + it('should render for main message list by default', () => { + const { container } = renderElements(); + expect(container).toMatchSnapshot(); + }); + + it('should render empty for thread by default', () => { + const { container } = renderElements(); + expect(container).toMatchInlineSnapshot(`
`); + }); + it('should render custom EmptyStateIndicator for main message list', () => { + const { container } = renderElements(, componentContext); + expect(container).toMatchSnapshot(); + }); + + it('should render custom EmptyStateIndicator for thread', () => { + const { container } = renderElements( + , + componentContext, + ); + expect(container).toMatchSnapshot(); + }); + + it('should render empty if EmptyStateIndicator nullified', () => { + const componentContext = { EmptyStateIndicator: NullEmptyStateIndicator }; + const { container } = renderElements(, componentContext); + expect(container).toMatchInlineSnapshot(`
`); + }); + + it('should render empty in thread if EmptyStateIndicator nullified', () => { + const componentContext = { EmptyStateIndicator: NullEmptyStateIndicator }; + const { container } = renderElements( + , + componentContext, + ); + expect(container).toMatchInlineSnapshot(`
`); + }); + }); + + describe('Footer', () => { + it('should render nothing in Footer by default', () => { + const { container } = renderElements(