Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .changeset/jolly-poets-tan.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
'@rocket.chat/meteor': patch
---

Fixes the issue where the message list keep jumping to the latest messages instead of restoring the previous
Comment thread
dougfabris marked this conversation as resolved.
Resolved
position when leaving a channel scrolled mid-history and coming back.
Comment thread
dougfabris marked this conversation as resolved.
Resolved
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import { renderHook } from '@testing-library/react';

import { useKeepAtBottom } from './useKeepAtBottom';

let resizeCallbacks: ResizeObserverCallback[] = [];

class MockResizeObserver {
constructor(cb: ResizeObserverCallback) {
resizeCallbacks.push(cb);
}

observe = jest.fn();

unobserve = jest.fn();

disconnect = jest.fn();
}

// Rows are measured asynchronously after the list remounts (avatars, images, reactions),
// so the observer fires repeatedly while the restored position is being applied.
const settleList = (times: number) => {
for (let i = 0; i < times; i++) {
resizeCallbacks.forEach((cb) => cb([], {} as ResizeObserver));
}
};

const mountList = (isAtBottom: { current: boolean }) => {
const { result } = renderHook(() => useKeepAtBottom(isAtBottom));

const node = document.createElement('div');
node.appendChild(document.createElement('div'));
result.current.keepAtBottomRef(node);

const scrollToEnd = jest.fn();
result.current.setKeepAtBottom(scrollToEnd);

return scrollToEnd;
};

describe('useKeepAtBottom', () => {
beforeEach(() => {
resizeCallbacks = [];
(global as any).ResizeObserver = MockResizeObserver;
});

it('does not pull the list to the latest messages when the room was left mid-history', () => {
const scrollToEnd = mountList({ current: false });

settleList(3);

expect(scrollToEnd).not.toHaveBeenCalled();
});

it('keeps the list at the bottom when the room was left at the bottom', () => {
const scrollToEnd = mountList({ current: true });

settleList(3);

expect(scrollToEnd).toHaveBeenCalledTimes(3);
});
});
5 changes: 3 additions & 2 deletions apps/meteor/client/views/room/body/RoomBody.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { isTruthy } from '@rocket.chat/tools';
import { CustomVirtuaScrollbars, useEmbeddedLayout } from '@rocket.chat/ui-client';
import { usePermission, useRole, useSetting, useTranslation, useUser, useUserPreference, useRoomToolbox } from '@rocket.chat/ui-contexts';
import type { MouseEvent } from 'react';
import { memo, useCallback, useMemo, useRef, useState } from 'react';
import { memo, useCallback, useMemo, useState } from 'react';

import { useMergedRefsV2 } from '../../../hooks/useMergedRefsV2';
import { BubbleDate } from '../BubbleDate';
Expand All @@ -17,6 +17,7 @@ import UploadProgressIndicator from './UploadProgress';
import ComposerContainer from '../composer/ComposerContainer';
import { useFileUpload } from './hooks/useFileUpload';
import { useGoToHomeOnRemoved } from './hooks/useGoToHomeOnRemoved';
import { useIsAtBottomRef } from './hooks/useIsAtBottomRef';
import { useQuoteMessageByUrl } from './hooks/useQuoteMessageByUrl';
import { useReadMessageWindowEvents } from './hooks/useReadMessageWindowEvents';
import RoomComposer from '../composer/RoomComposer/RoomComposer';
Expand Down Expand Up @@ -48,7 +49,7 @@ const RoomBody = () => {
const subscription = useRoomSubscription();

const [shouldJumpToBottom, setShouldJumpToBottom] = useState<boolean>(false);
const isAtBottom = useRef<boolean>(true);
const isAtBottom = useIsAtBottomRef(room._id);
Comment thread
gabriellsh marked this conversation as resolved.
Resolved
const [isJumpingToMessage, setIsJumpingToMessage] = useState<boolean>(false);

const retentionPolicy = useRetentionPolicy(room);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import { renderHook } from '@testing-library/react';

import { useIsAtBottomRef } from './useIsAtBottomRef';
import { RoomManager } from '../../../../lib/RoomManager';

jest.mock('../../../../lib/RoomManager', () => ({
RoomManager: { getStore: jest.fn() },
}));

const mockStore = (store: { atBottom: boolean } | undefined) => (RoomManager.getStore as jest.Mock).mockReturnValue(store);

describe('useIsAtBottomRef', () => {
beforeEach(() => {
jest.clearAllMocks();
});

it('starts at the bottom when the room has no stored position yet', () => {
mockStore(undefined);

const { result } = renderHook(() => useIsAtBottomRef('rid'));

expect(result.current.current).toBe(true);
});

it('starts away from the bottom when the room was left scrolled mid-history', () => {
mockStore({ atBottom: false });

const { result } = renderHook(() => useIsAtBottomRef('rid'));

expect(result.current.current).toBe(false);
});

it('starts at the bottom when the room was left at the bottom', () => {
mockStore({ atBottom: true });

const { result } = renderHook(() => useIsAtBottomRef('rid'));

expect(result.current.current).toBe(true);
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import type { RefObject } from 'react';
import { useRef } from 'react';

import { RoomManager } from '../../../../lib/RoomManager';

/** Starting from `true` makes `useKeepAtBottom` pull a restored mid-history position down to the latest messages, so seed it from the position persisted for the room being opened. */
export const useIsAtBottomRef = (rid: string): RefObject<boolean> => {
return useRef<boolean>(RoomManager.getStore(rid)?.atBottom ?? true);
Comment thread
gabriellsh marked this conversation as resolved.
Resolved
Comment thread
gabriellsh marked this conversation as resolved.
Resolved
};
Loading