From bdce66b62fbb41fc5c47e442c8b0a917c243660f Mon Sep 17 00:00:00 2001 From: MartinSchoeler Date: Thu, 21 May 2026 14:24:35 -0300 Subject: [PATCH 1/9] regression: unfollowed thread message marked as unread --- apps/meteor/client/lib/chats/readStateManager.ts | 3 ++- .../client/views/room/body/hooks/useUnreadMessages.ts | 7 +++++-- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/apps/meteor/client/lib/chats/readStateManager.ts b/apps/meteor/client/lib/chats/readStateManager.ts index 3e824a1da76db..e29c76cf63296 100644 --- a/apps/meteor/client/lib/chats/readStateManager.ts +++ b/apps/meteor/client/lib/chats/readStateManager.ts @@ -72,7 +72,8 @@ export class ReadStateManager extends Emitter { (record) => record.rid === this.subscription?.rid && record.ts.getTime() > (this.subscription.ls?.getTime() ?? 0) && - record.u._id !== getUserId(), + record.u._id !== getUserId() && + (!record.tmid || record.tshow === true), (a, b) => a.ts.getTime() - b.ts.getTime(), ); diff --git a/apps/meteor/client/views/room/body/hooks/useUnreadMessages.ts b/apps/meteor/client/views/room/body/hooks/useUnreadMessages.ts index 80a4105a57fd2..4d52d8e31c596 100644 --- a/apps/meteor/client/views/room/body/hooks/useUnreadMessages.ts +++ b/apps/meteor/client/views/room/body/hooks/useUnreadMessages.ts @@ -64,8 +64,11 @@ export const useHandleUnread = ( let message = firstUnread; if (!message) { message = findFirstMessage( - (record) => record.rid === rid && record.ts.getTime() > (unread?.since.getTime() ?? -Infinity), - (a, b) => a.ts.getTime() - b.ts.getTime(), + (record) => + record.rid === rid && + record.ts.getTime() > (unread?.since.getTime() ?? -Infinity) && + (!record.tmid || record.tshow === true), + (a, b) => a.ts.getTime() - b.ts.getTime(), ); } if (!message) { From 507600393241141efc4be0cbbf263aa3bef0c11d Mon Sep 17 00:00:00 2001 From: MartinSchoeler Date: Thu, 21 May 2026 15:13:26 -0300 Subject: [PATCH 2/9] lint --- .../client/views/room/body/hooks/useUnreadMessages.ts | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/apps/meteor/client/views/room/body/hooks/useUnreadMessages.ts b/apps/meteor/client/views/room/body/hooks/useUnreadMessages.ts index 4d52d8e31c596..84e6058c9141a 100644 --- a/apps/meteor/client/views/room/body/hooks/useUnreadMessages.ts +++ b/apps/meteor/client/views/room/body/hooks/useUnreadMessages.ts @@ -65,10 +65,8 @@ export const useHandleUnread = ( if (!message) { message = findFirstMessage( (record) => - record.rid === rid && - record.ts.getTime() > (unread?.since.getTime() ?? -Infinity) && - (!record.tmid || record.tshow === true), - (a, b) => a.ts.getTime() - b.ts.getTime(), + record.rid === rid && record.ts.getTime() > (unread?.since.getTime() ?? -Infinity) && (!record.tmid || record.tshow === true), + (a, b) => a.ts.getTime() - b.ts.getTime(), ); } if (!message) { From 71f462299c63d4fc89b4b380bbe0dc63a071a89b Mon Sep 17 00:00:00 2001 From: MartinSchoeler Date: Mon, 25 May 2026 15:50:55 -0300 Subject: [PATCH 3/9] review: Implement jumpContext url param --- .../client/lib/utils/setMessageJumpQueryStringParameter.ts | 6 +++--- .../room/MessageList/hooks/useTryToJumpToThreadMessage.ts | 5 +++-- .../client/views/room/body/hooks/useUnreadMessages.ts | 2 +- 3 files changed, 7 insertions(+), 6 deletions(-) diff --git a/apps/meteor/client/lib/utils/setMessageJumpQueryStringParameter.ts b/apps/meteor/client/lib/utils/setMessageJumpQueryStringParameter.ts index 6e94fe7902351..d30b12790e836 100644 --- a/apps/meteor/client/lib/utils/setMessageJumpQueryStringParameter.ts +++ b/apps/meteor/client/lib/utils/setMessageJumpQueryStringParameter.ts @@ -3,14 +3,14 @@ import type { LocationPathname } from '@rocket.chat/ui-contexts'; import { router } from '../../providers/RouterProvider'; -export const setMessageJumpQueryStringParameter = async (msg: IMessage['_id'] | null) => { - const { msg: _, ...search } = router.getSearchParameters(); +export const setMessageJumpQueryStringParameter = async (msg: IMessage['_id'] | null, context?: 'jumpToUnread') => { + const { msg: _msg, jumpContext: _jumpContext, ...search } = router.getSearchParameters(); const locationPathname = new URL(window.location.href).pathname as LocationPathname; router.navigate( { pathname: locationPathname, - search: msg ? { ...search, msg } : search, + search: msg ? { ...search, msg, ...(context && { jumpContext: context }) } : search, }, { replace: true }, ); diff --git a/apps/meteor/client/views/room/MessageList/hooks/useTryToJumpToThreadMessage.ts b/apps/meteor/client/views/room/MessageList/hooks/useTryToJumpToThreadMessage.ts index 604aa1e72a9f2..d68f05ad62ac5 100644 --- a/apps/meteor/client/views/room/MessageList/hooks/useTryToJumpToThreadMessage.ts +++ b/apps/meteor/client/views/room/MessageList/hooks/useTryToJumpToThreadMessage.ts @@ -11,6 +11,7 @@ import { useGoToRoom } from '../../hooks/useGoToRoom'; const useTryToJumpToThreadMessage = (): void => { const messageJumpParam = useSearchParameter('msg'); + const messageJumpContext = useSearchParameter('jumpContext'); const goToRoom = useGoToRoom(); const tab = useRouteParameter('tab'); const context = useRouteParameter('context'); @@ -28,7 +29,7 @@ const useTryToJumpToThreadMessage = (): void => { }); useEffect(() => { - if (!messageJumpParam) { + if (!messageJumpParam || messageJumpContext === 'jumpToUnread') { return; } @@ -56,7 +57,7 @@ const useTryToJumpToThreadMessage = (): void => { await RoomHistoryManager.getMore(message.rid); } })(); - }, [messageJumpParam, message, goToRoom, tab, context]); + }, [messageJumpParam, message, goToRoom, tab, context, messageJumpContext]); }; export default useTryToJumpToThreadMessage; diff --git a/apps/meteor/client/views/room/body/hooks/useUnreadMessages.ts b/apps/meteor/client/views/room/body/hooks/useUnreadMessages.ts index 84e6058c9141a..cd552056ed214 100644 --- a/apps/meteor/client/views/room/body/hooks/useUnreadMessages.ts +++ b/apps/meteor/client/views/room/body/hooks/useUnreadMessages.ts @@ -72,7 +72,7 @@ export const useHandleUnread = ( if (!message) { return; } - setMessageJumpQueryStringParameter(message?._id); + setMessageJumpQueryStringParameter(message?._id, 'jumpToUnread'); chat.readStateManager.markAsRead(); setUnreadCount(0); }, [room._id, setUnreadCount, findFirstMessage, unread?.since, chat.readStateManager]); From acc4ce658d233673fe997a5fdb0a46714a340e0f Mon Sep 17 00:00:00 2001 From: MartinSchoeler Date: Mon, 25 May 2026 16:25:46 -0300 Subject: [PATCH 4/9] test: add thread jump test --- .../hooks/useTryToJumpToThreadMessage.spec.ts | 90 +++++++++++++++++++ 1 file changed, 90 insertions(+) create mode 100644 apps/meteor/client/views/room/MessageList/hooks/useTryToJumpToThreadMessage.spec.ts diff --git a/apps/meteor/client/views/room/MessageList/hooks/useTryToJumpToThreadMessage.spec.ts b/apps/meteor/client/views/room/MessageList/hooks/useTryToJumpToThreadMessage.spec.ts new file mode 100644 index 0000000000000..c68fdbc627237 --- /dev/null +++ b/apps/meteor/client/views/room/MessageList/hooks/useTryToJumpToThreadMessage.spec.ts @@ -0,0 +1,90 @@ +import { mockAppRoot } from '@rocket.chat/mock-providers'; +import { renderHook, waitFor } from '@testing-library/react'; + +import useTryToJumpToThreadMessage from './useTryToJumpToThreadMessage'; +import { RoomHistoryManager } from '../../../../../app/ui-utils/client'; + +jest.mock('../../../../../app/ui-utils/client', () => ({ + RoomHistoryManager: { + getSurroundingMessages: jest.fn().mockResolvedValue(undefined), + isLoaded: jest.fn().mockReturnValue(false), + getMore: jest.fn().mockResolvedValue(undefined), + }, +})); + +jest.mock('../../../../lib/RoomManager', () => ({ + RoomManager: { + opened: undefined as string | undefined, + }, +})); + +jest.mock('../../../../lib/rooms/roomCoordinator', () => ({ + roomCoordinator: { + openRouteLink: jest.fn(), + }, +})); + +jest.mock('../../../../providers/RouterProvider', () => ({ + router: { + getSearchParameters: jest.fn().mockReturnValue({}), + getRouteParameters: jest.fn().mockReturnValue({}), + }, +})); + +jest.mock('../../../../stores', () => ({ + Subscriptions: { + state: { + find: jest.fn().mockReturnValue(undefined), + }, + }, +})); + +const mockedRoomHistoryManager = jest.mocked(RoomHistoryManager); + +afterEach(() => { + jest.clearAllMocks(); +}); + +describe('useTryToJumpToThreadMessage', () => { + describe('early return when msg param is absent or jumpContext is jumpToUnread', () => { + it('should not navigate or load messages when msg search parameter is absent', () => { + const endpointSpy = jest.fn(); + + renderHook(() => useTryToJumpToThreadMessage(), { + wrapper: mockAppRoot().withEndpoint('GET', '/v1/chat.getMessage', endpointSpy).build(), + }); + + expect(endpointSpy).not.toHaveBeenCalled(); + expect(mockedRoomHistoryManager.getSurroundingMessages).not.toHaveBeenCalled(); + expect(mockedRoomHistoryManager.getMore).not.toHaveBeenCalled(); + }); + + it('should not navigate or load messages when jumpContext is jumpToUnread', async () => { + const threadMessage = { + _id: 'msg-1', + rid: 'room-1', + tmid: 'parent-msg-1', + ts: new Date('2024-01-01T00:00:00Z').toISOString(), + u: { _id: 'user-1', username: 'john' }, + msg: 'Thread reply', + _updatedAt: new Date('2024-01-01T00:00:00Z').toISOString(), + }; + + const endpointSpy = jest.fn().mockResolvedValue({ message: threadMessage }); + + renderHook(() => useTryToJumpToThreadMessage(), { + wrapper: mockAppRoot() + .withRouter({ getSearchParameters: () => ({ msg: 'msg-1', jumpContext: 'jumpToUnread' }) }) + .withEndpoint('GET', '/v1/chat.getMessage', endpointSpy) + .build(), + }); + + await waitFor(() => { + expect(endpointSpy).toHaveBeenCalledWith({ msgId: 'msg-1' }); + }); + + expect(mockedRoomHistoryManager.getSurroundingMessages).not.toHaveBeenCalled(); + expect(mockedRoomHistoryManager.getMore).not.toHaveBeenCalled(); + }); + }); +}); From 68815ccd13788da39fe5e3721b176ec2a185292d Mon Sep 17 00:00:00 2001 From: Martin Schoeler Date: Tue, 26 May 2026 16:00:04 -0300 Subject: [PATCH 5/9] chore: fix jump to thread message e2e test (#40690) Co-authored-by: Douglas Gubert Co-authored-by: Julio Araujo --- .../hooks/useTryToJumpToMessage.ts | 5 +++- .../room/body/hooks/useHasNewMessages.spec.ts | 1 + .../room/body/hooks/useHasNewMessages.ts | 4 +++ .../Threads/components/ThreadMessageList.tsx | 26 ++++++++++++++++++- 4 files changed, 34 insertions(+), 2 deletions(-) diff --git a/apps/meteor/client/views/room/MessageList/hooks/useTryToJumpToMessage.ts b/apps/meteor/client/views/room/MessageList/hooks/useTryToJumpToMessage.ts index 63909b96249e0..779ef48794284 100644 --- a/apps/meteor/client/views/room/MessageList/hooks/useTryToJumpToMessage.ts +++ b/apps/meteor/client/views/room/MessageList/hooks/useTryToJumpToMessage.ts @@ -38,6 +38,9 @@ const useTryToJumpToMessage = ({ rid, virtualizerRef, setIsJumpingToMessage, mes setIsJumpingToMessage(false); return; } + if (!message) { + return; + } // Thread deep links are handled by useTryToJumpToThreadMessage; do not use the main list virtualizer if (message && isThreadMessage(message) && !isThreadMainMessage(message)) { setIsJumpingToMessage(false); @@ -78,7 +81,7 @@ const useTryToJumpToMessage = ({ rid, virtualizerRef, setIsJumpingToMessage, mes setTimeout(() => { setIsJumpingToMessage(false); setMessageJumpQueryStringParameter(null); - }, 1000); + }, 500); }, [messageJumpParam, virtualizerRef, setIsJumpingToMessage, rid, messages, message]); }; diff --git a/apps/meteor/client/views/room/body/hooks/useHasNewMessages.spec.ts b/apps/meteor/client/views/room/body/hooks/useHasNewMessages.spec.ts index d13c3a8f001f9..fa62da76d4268 100644 --- a/apps/meteor/client/views/room/body/hooks/useHasNewMessages.spec.ts +++ b/apps/meteor/client/views/room/body/hooks/useHasNewMessages.spec.ts @@ -15,6 +15,7 @@ jest.mock('../../../../../app/ui-utils/client', () => ({ RoomHistoryManager: { clear: jest.fn(), getMoreIfIsEmpty: jest.fn(), + hasMoreNext: jest.fn(), }, })); diff --git a/apps/meteor/client/views/room/body/hooks/useHasNewMessages.ts b/apps/meteor/client/views/room/body/hooks/useHasNewMessages.ts index ab3afcb05c9fd..acd68261dfc3f 100644 --- a/apps/meteor/client/views/room/body/hooks/useHasNewMessages.ts +++ b/apps/meteor/client/views/room/body/hooks/useHasNewMessages.ts @@ -66,6 +66,10 @@ export const useHasNewMessages = ( return; } if (msg.u._id === uid) { + // If there are more next messages, jump to the recent messages + if (RoomHistoryManager.hasMoreNext(rid)) { + return handleJumpToRecentButtonClick(); + } setShouldJumpToBottom(true); setHasNewMessages(false); } diff --git a/apps/meteor/client/views/room/contextualBar/Threads/components/ThreadMessageList.tsx b/apps/meteor/client/views/room/contextualBar/Threads/components/ThreadMessageList.tsx index dc535603af0ca..2487b3fced269 100644 --- a/apps/meteor/client/views/room/contextualBar/Threads/components/ThreadMessageList.tsx +++ b/apps/meteor/client/views/room/contextualBar/Threads/components/ThreadMessageList.tsx @@ -12,6 +12,7 @@ import type { VirtualizerHandle } from 'virtua'; import { VList } from 'virtua'; import { ThreadMessageItem } from './ThreadMessageItem'; +import { setMessageJumpQueryStringParameter } from '../../../../../lib/utils/setMessageJumpQueryStringParameter'; import { BubbleDate } from '../../../BubbleDate'; import { useKeepMountedMessages } from '../../../MessageList/hooks/useKeepMountedMessages'; import { isMessageNewDay } from '../../../MessageList/lib/isMessageNewDay'; @@ -72,7 +73,8 @@ const ThreadMessageList = ({ mainMessage, shouldJumpToBottom, setShouldJumpToBot const { messageListRef } = useMessageListNavigation(); const virtualizerRef = useRef(null); - const isAtBottom = useRef(true); + const isAtBottom = useRef(null); + const lastScrollSizeRef = useRef(0); const items = loading ? [] : [mainMessage, ...messages]; @@ -102,6 +104,10 @@ const ThreadMessageList = ({ mainMessage, shouldJumpToBottom, setShouldJumpToBot setShouldJumpToBottom(false); return; } + if (isAtBottom.current === true && lastScrollSizeRef.current !== handle?.scrollSize) { + lastScrollSizeRef.current = handle?.scrollSize ?? 0; + setShouldJumpToBottom(true); + } if (shouldJumpToBottom) { handle.scrollToIndex(items.length, { align: 'end' }); setShouldJumpToBottom(false); @@ -129,6 +135,24 @@ const ThreadMessageList = ({ mainMessage, shouldJumpToBottom, setShouldJumpToBot }, 2000); }, [threadMsgTargetIndex, msgJumpParam, mainMessage._id, setShouldJumpToBottom]); + useEffect(() => { + if (!msgJumpParam) { + return; + } + const clearMsgJumpParam = () => { + if (messages.find((m) => m._id === msgJumpParam) && mainMessage._id !== msgJumpParam) { + setMessageJumpQueryStringParameter(null); + } + }; + const timeoutId = setTimeout(() => { + clearMsgJumpParam(); + }, 500); + return () => { + clearMsgJumpParam(); + clearTimeout(timeoutId); + }; + }, [msgJumpParam, messages, mainMessage._id]); + useEffect(() => { const handlerId = `thread-scroll-${mainMessage._id}`; clientCallbacks.add( From a5f7cb56a9acee51f54a3d36a3622e7679ffae77 Mon Sep 17 00:00:00 2001 From: MartinSchoeler Date: Tue, 26 May 2026 16:24:23 -0300 Subject: [PATCH 6/9] fix: clear issue from conflict --- .../room/contextualBar/Threads/components/ThreadMessageList.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/apps/meteor/client/views/room/contextualBar/Threads/components/ThreadMessageList.tsx b/apps/meteor/client/views/room/contextualBar/Threads/components/ThreadMessageList.tsx index 88a8c83be0839..a3161fdae4e9d 100644 --- a/apps/meteor/client/views/room/contextualBar/Threads/components/ThreadMessageList.tsx +++ b/apps/meteor/client/views/room/contextualBar/Threads/components/ThreadMessageList.tsx @@ -131,7 +131,6 @@ const ThreadMessageList = ({ mainMessage, shouldJumpToBottom, setShouldJumpToBot setShouldJumpToBottom(false); handle.scrollToIndex(threadMsgTargetIndex, { align: 'center' }); setHighlightMessage(msgJumpParam); - setMessageJumpQueryStringParameter(null); setTimeout(() => { clearHighlightMessage(); }, 2000); From 267b2da95d40cda5817eeca9d72276468c581e57 Mon Sep 17 00:00:00 2001 From: MartinSchoeler Date: Tue, 26 May 2026 17:23:39 -0300 Subject: [PATCH 7/9] fix review --- .../views/room/MessageList/hooks/useTryToJumpToMessage.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/apps/meteor/client/views/room/MessageList/hooks/useTryToJumpToMessage.ts b/apps/meteor/client/views/room/MessageList/hooks/useTryToJumpToMessage.ts index 779ef48794284..d15ef63a8765b 100644 --- a/apps/meteor/client/views/room/MessageList/hooks/useTryToJumpToMessage.ts +++ b/apps/meteor/client/views/room/MessageList/hooks/useTryToJumpToMessage.ts @@ -42,7 +42,8 @@ const useTryToJumpToMessage = ({ rid, virtualizerRef, setIsJumpingToMessage, mes return; } // Thread deep links are handled by useTryToJumpToThreadMessage; do not use the main list virtualizer - if (message && isThreadMessage(message) && !isThreadMainMessage(message)) { + // If tshow is true, there is a preview on the main list, in this case we scroll to it + if (message && isThreadMessage(message) && !isThreadMainMessage(message) && message.tshow !== true) { setIsJumpingToMessage(false); return; } From dfc4054c66f48ecf36e5c500f6518d9e56dfacb3 Mon Sep 17 00:00:00 2001 From: MartinSchoeler Date: Tue, 26 May 2026 18:03:16 -0300 Subject: [PATCH 8/9] add more test case --- .../hooks/useTryToJumpToThreadMessage.spec.ts | 36 +++++++++++++++++++ 1 file changed, 36 insertions(+) diff --git a/apps/meteor/client/views/room/MessageList/hooks/useTryToJumpToThreadMessage.spec.ts b/apps/meteor/client/views/room/MessageList/hooks/useTryToJumpToThreadMessage.spec.ts index c68fdbc627237..24bd84fa65bad 100644 --- a/apps/meteor/client/views/room/MessageList/hooks/useTryToJumpToThreadMessage.spec.ts +++ b/apps/meteor/client/views/room/MessageList/hooks/useTryToJumpToThreadMessage.spec.ts @@ -87,4 +87,40 @@ describe('useTryToJumpToThreadMessage', () => { expect(mockedRoomHistoryManager.getMore).not.toHaveBeenCalled(); }); }); + + describe('when jumpContext is absent', () => { + it('should load more messages when jumpContext is null', async () => { + const threadMessage = { + _id: 'msg-1', + rid: 'room-1', + tmid: 'parent-msg-1', + ts: new Date('2024-01-01T00:00:00Z').toISOString(), + u: { _id: 'user-1', username: 'john' }, + msg: 'Thread reply', + _updatedAt: new Date('2024-01-01T00:00:00Z').toISOString(), + }; + + mockedRoomHistoryManager.isLoaded.mockReturnValue(false); + + const endpointSpy = jest.fn().mockResolvedValue({ message: threadMessage }); + + renderHook(() => useTryToJumpToThreadMessage(), { + wrapper: mockAppRoot() + .withRouter({ getSearchParameters: () => ({ msg: 'msg-1' }) }) + .withEndpoint('GET', '/v1/chat.getMessage', endpointSpy) + .withMethod('getRoomById', () => ({ _id: 'room-1', t: 'c', name: 'general' }) as any) + .build(), + }); + + await waitFor(() => { + expect(endpointSpy).toHaveBeenCalledWith({ msgId: 'msg-1' }); + }); + + await waitFor(() => { + expect(mockedRoomHistoryManager.getMore).toHaveBeenCalledWith('room-1'); + }); + + expect(mockedRoomHistoryManager.getSurroundingMessages).not.toHaveBeenCalled(); + }); + }); }); From 7abc0d7affed86209beb1de595ab1d7a2d84f83f Mon Sep 17 00:00:00 2001 From: MartinSchoeler Date: Tue, 26 May 2026 18:10:07 -0300 Subject: [PATCH 9/9] update existing test --- .../MessageList/hooks/useTryToJumpToThreadMessage.spec.ts | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/apps/meteor/client/views/room/MessageList/hooks/useTryToJumpToThreadMessage.spec.ts b/apps/meteor/client/views/room/MessageList/hooks/useTryToJumpToThreadMessage.spec.ts index 24bd84fa65bad..2d99ff0eef2f9 100644 --- a/apps/meteor/client/views/room/MessageList/hooks/useTryToJumpToThreadMessage.spec.ts +++ b/apps/meteor/client/views/room/MessageList/hooks/useTryToJumpToThreadMessage.spec.ts @@ -76,6 +76,7 @@ describe('useTryToJumpToThreadMessage', () => { wrapper: mockAppRoot() .withRouter({ getSearchParameters: () => ({ msg: 'msg-1', jumpContext: 'jumpToUnread' }) }) .withEndpoint('GET', '/v1/chat.getMessage', endpointSpy) + .withMethod('getRoomById', () => ({ _id: 'room-1', t: 'c', name: 'general' }) as any) .build(), }); @@ -83,6 +84,11 @@ describe('useTryToJumpToThreadMessage', () => { expect(endpointSpy).toHaveBeenCalledWith({ msgId: 'msg-1' }); }); + await waitFor(async () => { + await new Promise((resolve) => setTimeout(resolve, 300)); + expect(mockedRoomHistoryManager.getMore).not.toHaveBeenCalled(); + }); + expect(mockedRoomHistoryManager.getSurroundingMessages).not.toHaveBeenCalled(); expect(mockedRoomHistoryManager.getMore).not.toHaveBeenCalled(); });