diff --git a/.changeset/good-clouds-brush.md b/.changeset/good-clouds-brush.md new file mode 100644 index 000000000000..62e0760254d2 --- /dev/null +++ b/.changeset/good-clouds-brush.md @@ -0,0 +1,5 @@ +--- +'@rocket.chat/meteor': patch +--- + +Fixes an issue that prevented room history from loading under certain conditions. diff --git a/apps/meteor/client/views/room/body/hooks/useGetMore.spec.ts b/apps/meteor/client/views/room/body/hooks/useGetMore.spec.ts new file mode 100644 index 000000000000..0d27c4886563 --- /dev/null +++ b/apps/meteor/client/views/room/body/hooks/useGetMore.spec.ts @@ -0,0 +1,80 @@ +import { renderHook } from '@testing-library/react'; +import React from 'react'; + +import { useGetMore } from './useGetMore'; +import { RoomHistoryManager } from '../../../../../app/ui-utils/client'; + +jest.mock('../../../../../app/ui-utils/client', () => ({ + RoomHistoryManager: { + isLoading: jest.fn(), + hasMore: jest.fn(), + hasMoreNext: jest.fn(), + getMore: jest.fn(), + getMoreNext: jest.fn(), + }, +})); + +const mockGetMore = jest.fn(); + +describe('useGetMore', () => { + it('should call getMore when scrolling near top and hasMore is true', () => { + (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).mockImplementation(mockGetMore); + const atBottomRef = { current: false }; + + const mockElement = { + addEventListener: jest.fn((event, handler) => { + if (event === 'scroll') { + handler({ + target: { + scrollTop: 10, + clientHeight: 300, + }, + }); + } + }), + removeEventListener: jest.fn(), + }; + + const useRefSpy = jest.spyOn(React, 'useRef').mockReturnValueOnce({ current: mockElement }); + + const { unmount } = renderHook(() => useGetMore('room-id', atBottomRef), { legacyRoot: true }); + + expect(useRefSpy).toHaveBeenCalledWith(null); + expect(RoomHistoryManager.getMore).toHaveBeenCalledWith('room-id'); + + unmount(); + expect(mockElement.removeEventListener).toHaveBeenCalledWith('scroll', expect.any(Function)); + }); + + it('should call getMoreNext when scrolling near bottom and hasMoreNext is true', () => { + (RoomHistoryManager.isLoading as jest.Mock).mockReturnValue(false); + (RoomHistoryManager.hasMore as jest.Mock).mockReturnValue(false); + (RoomHistoryManager.hasMoreNext as jest.Mock).mockReturnValue(true); + (RoomHistoryManager.getMoreNext as jest.Mock).mockImplementation(mockGetMore); + + const atBottomRef = { current: false }; + const mockElement = { + addEventListener: jest.fn((event, handler) => { + if (event === 'scroll') { + handler({ + target: { + scrollTop: 600, + clientHeight: 300, + scrollHeight: 800, + }, + }); + } + }), + removeEventListener: jest.fn(), + }; + const useRefSpy = jest.spyOn(React, 'useRef').mockReturnValueOnce({ current: mockElement }); + + renderHook(() => useGetMore('room-id', atBottomRef), { legacyRoot: true }); + + expect(useRefSpy).toHaveBeenCalledWith(null); + expect(RoomHistoryManager.getMoreNext).toHaveBeenCalledWith('room-id', atBottomRef); + }); +}); diff --git a/apps/meteor/client/views/room/body/hooks/useGetMore.ts b/apps/meteor/client/views/room/body/hooks/useGetMore.ts index c2f182e5131f..32a6b1fb78e7 100644 --- a/apps/meteor/client/views/room/body/hooks/useGetMore.ts +++ b/apps/meteor/client/views/room/body/hooks/useGetMore.ts @@ -1,40 +1,44 @@ import type { MutableRefObject } from 'react'; -import { useCallback } from 'react'; +import { useEffect, useRef } from 'react'; import { RoomHistoryManager } from '../../../../../app/ui-utils/client'; import { withThrottling } from '../../../../../lib/utils/highOrderFunctions'; export const useGetMore = (rid: string, atBottomRef: MutableRefObject) => { - return { - innerRef: useCallback( - (wrapper: HTMLElement | null) => { - if (!wrapper) { - return; + const ref = useRef(null); + + useEffect(() => { + if (!ref.current) { + return; + } + + const refValue = ref.current; + + const handleScroll = withThrottling({ wait: 100 })((event) => { + const lastScrollTopRef = event.target.scrollTop; + const height = event.target.clientHeight; + const isLoading = RoomHistoryManager.isLoading(rid); + const hasMore = RoomHistoryManager.hasMore(rid); + const hasMoreNext = RoomHistoryManager.hasMoreNext(rid); + + if ((isLoading === false && hasMore === true) || hasMoreNext === true) { + if (hasMore === true && lastScrollTopRef <= height / 3) { + RoomHistoryManager.getMore(rid); + } else if (hasMoreNext === true && Math.ceil(lastScrollTopRef) >= event.target.scrollHeight - height) { + RoomHistoryManager.getMoreNext(rid, atBottomRef); + atBottomRef.current = false; } + } + }); + + refValue.addEventListener('scroll', handleScroll); - let lastScrollTopRef = 0; - - wrapper.addEventListener( - 'scroll', - withThrottling({ wait: 100 })((event) => { - lastScrollTopRef = event.target.scrollTop; - const height = event.target.clientHeight; - const isLoading = RoomHistoryManager.isLoading(rid); - const hasMore = RoomHistoryManager.hasMore(rid); - const hasMoreNext = RoomHistoryManager.hasMoreNext(rid); - - if ((isLoading === false && hasMore === true) || hasMoreNext === true) { - if (hasMore === true && lastScrollTopRef <= height / 3) { - RoomHistoryManager.getMore(rid); - } else if (hasMoreNext === true && Math.ceil(lastScrollTopRef) >= event.target.scrollHeight - height) { - RoomHistoryManager.getMoreNext(rid, atBottomRef); - atBottomRef.current = false; - } - } - }), - ); - }, - [atBottomRef, rid], - ), + return () => { + refValue.removeEventListener('scroll', handleScroll); + }; + }, [rid, atBottomRef]); + + return { + innerRef: ref, }; };