From 85a680fe988206f2bade3c98fc7241e585cac555 Mon Sep 17 00:00:00 2001 From: Nikola Naumovski Date: Wed, 14 Oct 2026 10:55:12 +0200 Subject: [PATCH] refactor(MessageList): render thread messages from the thread unread snapshot useMessageListElements read the unread snapshot from channel.messagePaginator even inside a thread, while the rest of MessageList reads it from the rendered list via useMessagePaginator(). A thread list re-rendered on every change to the channel snapshot, and a custom renderMessages received the channel state. --- .../__tests__/MessageList.test.tsx | 30 +++++++++++++++++++ .../MessageList/useMessageListElements.tsx | 5 +++- 2 files changed, 34 insertions(+), 1 deletion(-) diff --git a/src/components/MessageList/__tests__/MessageList.test.tsx b/src/components/MessageList/__tests__/MessageList.test.tsx index e80de0e448..3949b4b774 100644 --- a/src/components/MessageList/__tests__/MessageList.test.tsx +++ b/src/components/MessageList/__tests__/MessageList.test.tsx @@ -809,6 +809,36 @@ describe('MessageList', () => { ).not.toBeInTheDocument(); }); + it('should render thread messages from the thread unread snapshot, not the channel one', async () => { + const thread = chatClient.threads.ensure({ channel, parentMessage: message1 }); + const renderMessages = vi.fn(() => []); + + await act(() => { + renderComponent({ + channelProps: { channel }, + chatClient, + msgListProps: fromPartial({ renderMessages }), + thread, + }); + }); + + act(() => { + thread.messagePaginator.unreadStateSnapshot.partialNext({ unreadCount: 3 }); + }); + expect(renderMessages).toHaveBeenLastCalledWith( + expect.objectContaining({ + channelUnreadUiState: expect.objectContaining({ unreadCount: 3 }), + }), + ); + + // the channel's snapshot belongs to the channel list: changing it must not re-render the thread + const renderCountBeforeChannelChange = renderMessages.mock.calls.length; + act(() => { + channel.messagePaginator.unreadStateSnapshot.partialNext({ unreadCount: 7 }); + }); + expect(renderMessages).toHaveBeenCalledTimes(renderCountBeforeChannelChange); + }); + it('should display custom unread messages separator when channel is marked unread', async () => { const customUnreadMessagesSeparatorText = 'CustomUnreadMessagesSeparator'; const UnreadMessagesSeparator = () => ( diff --git a/src/components/MessageList/hooks/MessageList/useMessageListElements.tsx b/src/components/MessageList/hooks/MessageList/useMessageListElements.tsx index 07b9814782..8332ff57c7 100644 --- a/src/components/MessageList/hooks/MessageList/useMessageListElements.tsx +++ b/src/components/MessageList/hooks/MessageList/useMessageListElements.tsx @@ -11,6 +11,7 @@ import { useComponentContext } from '../../../../context/ComponentContext'; import type { LocalMessage, UnreadSnapshotState } from 'stream-chat'; import type { MessageRenderer, SharedMessageProps } from '../../renderMessages'; import { useChannel } from '../../../../context'; +import { useMessagePaginator } from '../../../../hooks'; import { useLastDeliveredData } from '../useLastDeliveredData'; import { useStateStore } from '../../../../store'; @@ -41,9 +42,11 @@ export const useMessageListElements = (props: UseMessageListElementsProps) => { const { customClasses } = useChatContext(); const channel = useChannel(); + const messagePaginator = useMessagePaginator(); const components = useComponentContext(); + // The rendered list's own snapshot: in a thread that is the thread's, as for the rest of the list. const channelUnreadUiState = useStateStore( - channel.messagePaginator.unreadStateSnapshot, + messagePaginator.unreadStateSnapshot, unreadStateSnapshotSelector, ); // get the readData, but only for messages submitted by the user themselves