From c7488c96c6add12874047e02f66939c6bd5df4a0 Mon Sep 17 00:00:00 2001 From: martincupela Date: Tue, 30 Jun 2026 13:18:22 +0200 Subject: [PATCH] fix: prevent changing unread UI state for thread messages --- .../hooks/__tests__/useMarkRead.test.tsx | 81 +++++++++++++++++++ .../MessageList/hooks/useMarkRead.ts | 6 +- 2 files changed, 86 insertions(+), 1 deletion(-) diff --git a/src/components/MessageList/hooks/__tests__/useMarkRead.test.tsx b/src/components/MessageList/hooks/__tests__/useMarkRead.test.tsx index b377cc6af7..ac289b1183 100644 --- a/src/components/MessageList/hooks/__tests__/useMarkRead.test.tsx +++ b/src/components/MessageList/hooks/__tests__/useMarkRead.test.tsx @@ -504,6 +504,87 @@ describe('useMarkRead', () => { expect(setChannelUnreadUiState).not.toHaveBeenCalled(); }); + it.each([ + [ + 'message list is not scrolled to the bottom', + { isMessageListScrolledToBottom: false }, + ], + ['channel was marked unread', { wasMarkedUnread: true }], + ])( + 'should not increase channel unread UI count for thread messages when %s', + async (_, paramsOverride) => { + const { + channels: [channel], + client, + } = await initClientWithChannels(); + + await render({ + channel, + client, + params: { ...shouldMarkReadParams, ...paramsOverride }, + }); + + await act(() => { + dispatchMessageNewEvent(client, generateMessage({ parent_id: 'X' }), channel); + }); + + expect(setChannelUnreadUiState).not.toHaveBeenCalled(); + expect(markRead).not.toHaveBeenCalled(); + }, + ); + + it('should not increase channel unread UI count for thread messages when document is hidden', async () => { + const { + channels: [channel], + client, + } = await initClientWithChannels(); + + await render({ + channel, + client, + params: shouldMarkReadParams, + }); + + const docHiddenSpy = vi.spyOn(document, 'hidden', 'get').mockReturnValueOnce(true); + await act(() => { + dispatchMessageNewEvent(client, generateMessage({ parent_id: 'X' }), channel); + }); + + expect(setChannelUnreadUiState).not.toHaveBeenCalled(); + expect(markRead).not.toHaveBeenCalled(); + docHiddenSpy.mockRestore(); + }); + + it('should increase channel unread UI count for thread messages with show_in_channel enabled when not scrolled to the bottom', async () => { + let channelUnreadUiStateCb: (prev?: ChannelUnreadUiState) => ChannelUnreadUiState; + setChannelUnreadUiState.mockImplementationOnce( + (cb) => (channelUnreadUiStateCb = cb), + ); + const { + channels: [channel], + client, + } = await initClientWithChannels(); + + await render({ + channel, + client, + params: { ...shouldMarkReadParams, isMessageListScrolledToBottom: false }, + }); + + await act(() => { + dispatchMessageNewEvent( + client, + generateMessage({ parent_id: 'X', show_in_channel: true }), + channel, + ); + }); + + expect(setChannelUnreadUiState).toHaveBeenCalledTimes(1); + const channelUnreadUiState = channelUnreadUiStateCb(); + expect(channelUnreadUiState.unread_messages).toBe(1); + expect(markRead).not.toHaveBeenCalled(); + }); + it('should mark channel read for thread messages with event.show_in_channel enabled', async () => { const { channels: [channel], diff --git a/src/components/MessageList/hooks/useMarkRead.ts b/src/components/MessageList/hooks/useMarkRead.ts index 9de52ae2af..5307f562ac 100644 --- a/src/components/MessageList/hooks/useMarkRead.ts +++ b/src/components/MessageList/hooks/useMarkRead.ts @@ -54,6 +54,10 @@ export const useMarkRead = ({ const mainChannelUpdated = !event.message?.parent_id || event.message?.show_in_channel; + // Thread replies that are not shown in the channel must not affect the + // main channel's unread UI state (count / notification). + if (!mainChannelUpdated) return; + if (!isMessageListScrolledToBottom || wasMarkedUnread || document.hidden) { setChannelUnreadUiState((prev) => { const previousUnreadCount = prev?.unread_messages ?? 0; @@ -71,7 +75,7 @@ export const useMarkRead = ({ unread_messages: previousUnreadCount + 1, }; }); - } else if (mainChannelUpdated && shouldMarkRead()) { + } else if (shouldMarkRead()) { markRead(); } };