diff --git a/.changeset/message-list-history-loop-while-hidden.md b/.changeset/message-list-history-loop-while-hidden.md new file mode 100644 index 0000000000000..c2c72007aacf7 --- /dev/null +++ b/.changeset/message-list-history-loop-while-hidden.md @@ -0,0 +1,5 @@ +--- +'@rocket.chat/meteor': patch +--- + +Fixed the message list loading the entire room history while it is hidden behind a full-width contextual bar. On a narrow window the room layout hides the message body, and a hidden element reports its height and scroll offset as `0`, which made the load-older-messages check always true - so every page that arrived triggered the next one until the whole room was in memory, hammering the server until it rate-limited the client. On returning to the list the scroll position was far in the past, because the content had grown underneath it. diff --git a/apps/meteor/client/views/room/body/hooks/useGetMore.spec.tsx b/apps/meteor/client/views/room/body/hooks/useGetMore.spec.tsx index 1a388da54b55b..82279a342927c 100644 --- a/apps/meteor/client/views/room/body/hooks/useGetMore.spec.tsx +++ b/apps/meteor/client/views/room/body/hooks/useGetMore.spec.tsx @@ -66,6 +66,40 @@ describe('useGetMore', () => { }); }); + it('should not call getMore while the list is hidden, even though it reports being at the top', async () => { + const root = mockAppRoot(); + (RoomHistoryManager.isLoading as jest.Mock).mockReturnValue(false); + (RoomHistoryManager.hasMore as jest.Mock).mockReturnValue(true); + (RoomHistoryManager.hasMoreNext as jest.Mock).mockReturnValue(false); + (RoomHistoryManager.getMore as jest.Mock).mockClear(); + + const Test = () => { + const [atBottom] = useState(false); + const { innerRef } = useGetMore('room-id', atBottom); + return
; + }; + + // What a display: none element reports. Without a guard `scrollTop <= clientHeight / 3` is + // 0 <= 0, so every observer callback would load another page of history. + (getBoundingClientRect as jest.Mock).mockReturnValue({ + scrollTop: 0, + clientHeight: 0, + scrollHeight: 0, + }); + + render(, { + wrapper: root.build(), + }); + + const scrollableElement = screen.getByTestId('scrollable-element'); + scrollableElement.dispatchEvent(new Event('wheel')); + scrollableElement.dispatchEvent(new Event('scroll')); + + await waitFor(() => { + expect(RoomHistoryManager.getMore).not.toHaveBeenCalled(); + }); + }); + it('should call getMoreNext when scrolling near bottom and hasMoreNext is true', () => { const root = mockAppRoot(); (RoomHistoryManager.isLoading as jest.Mock).mockReturnValue(false); diff --git a/apps/meteor/client/views/room/body/hooks/useGetMore.ts b/apps/meteor/client/views/room/body/hooks/useGetMore.ts index a32eb95d8972f..a9ee083a2f6b0 100644 --- a/apps/meteor/client/views/room/body/hooks/useGetMore.ts +++ b/apps/meteor/client/views/room/body/hooks/useGetMore.ts @@ -41,6 +41,13 @@ export const useGetMore = (rid: string, isJumpingToMessage: boolean) => { const { scrollTop, clientHeight, scrollHeight } = getBoundingClientRect(element); + // RoomLayout hides the message body while a contextual bar takes the full room width, + // and a hidden element reports clientHeight 0. Without this guard the position check + // below is always true and pulls in the entire room history, one observer call at a time. + if (clientHeight === 0) { + return; + } + const lastScrollTopRef = scrollTop; const height = clientHeight; const hasMore = RoomHistoryManager.hasMore(rid);