Skip to content

Commit 62660fc

Browse files
authored
fix: Restore room scroll position (#34908)
1 parent e6eff85 commit 62660fc

File tree

3 files changed

+126
-43
lines changed

3 files changed

+126
-43
lines changed

.changeset/gentle-moons-unite.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@rocket.chat/meteor': patch
3+
---
4+
5+
Fixes an issue where room scroll position wasn't being restored when moving between rooms.
Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
import { renderHook } from '@testing-library/react';
2+
import React from 'react';
3+
4+
import { useRestoreScrollPosition } from './useRestoreScrollPosition';
5+
import { RoomManager } from '../../../../lib/RoomManager';
6+
7+
jest.mock('../../../../lib/RoomManager', () => ({
8+
RoomManager: {
9+
getStore: jest.fn(),
10+
},
11+
}));
12+
13+
describe('useRestoreScrollPosition', () => {
14+
it('should restore room scroll position based on store', () => {
15+
(RoomManager.getStore as jest.Mock).mockReturnValue({ scroll: 100, atBottom: false });
16+
17+
const mockElement = {
18+
addEventListener: jest.fn(),
19+
removeEventListener: jest.fn(),
20+
};
21+
22+
const useRefSpy = jest.spyOn(React, 'useRef').mockReturnValueOnce({ current: mockElement });
23+
24+
const { unmount } = renderHook(() => useRestoreScrollPosition('room-id'), { legacyRoot: true });
25+
26+
expect(useRefSpy).toHaveBeenCalledWith(null);
27+
expect(mockElement).toHaveProperty('scrollTop', 100);
28+
expect(mockElement).toHaveProperty('scrollLeft', 30);
29+
30+
unmount();
31+
expect(mockElement.removeEventListener).toHaveBeenCalledWith('scroll', expect.any(Function));
32+
});
33+
34+
it('should not restore scroll position if already at bottom', () => {
35+
(RoomManager.getStore as jest.Mock).mockReturnValue({ scroll: 100, atBottom: true });
36+
37+
const mockElement = {
38+
addEventListener: jest.fn(),
39+
removeEventListener: jest.fn(),
40+
scrollHeight: 800,
41+
};
42+
43+
const useRefSpy = jest.spyOn(React, 'useRef').mockReturnValueOnce({ current: mockElement });
44+
45+
const { unmount } = renderHook(() => useRestoreScrollPosition('room-id'), { legacyRoot: true });
46+
47+
expect(useRefSpy).toHaveBeenCalledWith(null);
48+
expect(mockElement).toHaveProperty('scrollTop', 800);
49+
expect(mockElement).not.toHaveProperty('scrollLeft');
50+
51+
unmount();
52+
expect(mockElement.removeEventListener).toHaveBeenCalledWith('scroll', expect.any(Function));
53+
});
54+
55+
it('should update store based on scroll position', () => {
56+
const update = jest.fn();
57+
(RoomManager.getStore as jest.Mock).mockReturnValue({ update });
58+
59+
const mockElement = {
60+
addEventListener: jest.fn((event, handler) => {
61+
if (event === 'scroll') {
62+
handler({
63+
target: {
64+
scrollTop: 500,
65+
},
66+
});
67+
}
68+
}),
69+
removeEventListener: jest.fn(),
70+
};
71+
72+
const useRefSpy = jest.spyOn(React, 'useRef').mockReturnValueOnce({ current: mockElement });
73+
74+
const { unmount } = renderHook(() => useRestoreScrollPosition('room-id'), { legacyRoot: true });
75+
76+
expect(useRefSpy).toHaveBeenCalledWith(null);
77+
expect(update).toHaveBeenCalledWith({ scroll: 500, atBottom: false });
78+
79+
unmount();
80+
expect(mockElement.removeEventListener).toHaveBeenCalledWith('scroll', expect.any(Function));
81+
});
82+
});
Lines changed: 39 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -1,54 +1,50 @@
11
import type { IRoom } from '@rocket.chat/core-typings';
2-
import { useMergedRefs } from '@rocket.chat/fuselage-hooks';
3-
import type { RefObject } from 'react';
4-
import { useCallback } from 'react';
2+
import { useCallback, useEffect, useRef } from 'react';
53

64
import { isAtBottom } from '../../../../../app/ui/client/views/app/lib/scrolling';
75
import { withThrottling } from '../../../../../lib/utils/highOrderFunctions';
86
import { RoomManager } from '../../../../lib/RoomManager';
97

108
export function useRestoreScrollPosition(roomId: IRoom['_id']) {
11-
const ref = useCallback(
12-
(node: HTMLElement | null) => {
13-
if (!node) {
14-
return;
15-
}
16-
const store = RoomManager.getStore(roomId);
17-
18-
if (store?.scroll && !store.atBottom) {
19-
node.scrollTo({
20-
left: 30,
21-
top: store.scroll,
22-
});
23-
} else {
24-
node.scrollTo({
25-
top: node.scrollHeight,
26-
});
27-
}
28-
},
29-
[roomId],
30-
);
31-
32-
const refCallback = useCallback(
33-
(node: HTMLElement | null) => {
34-
if (!node) {
35-
return;
36-
}
37-
38-
const store = RoomManager.getStore(roomId);
39-
40-
const handleWrapperScroll = withThrottling({ wait: 100 })(() => {
41-
store?.update({ scroll: node.scrollTop, atBottom: isAtBottom(node, 50) });
42-
});
43-
44-
node.addEventListener('scroll', handleWrapperScroll, {
45-
passive: true,
46-
});
47-
},
48-
[roomId],
49-
);
9+
const ref = useRef<HTMLElement>(null);
10+
11+
const handleRestoreScroll = useCallback(() => {
12+
if (!ref.current) {
13+
return;
14+
}
15+
16+
const store = RoomManager.getStore(roomId);
17+
18+
if (store?.scroll && !store.atBottom) {
19+
ref.current.scrollTop = store.scroll;
20+
ref.current.scrollLeft = 30;
21+
} else {
22+
ref.current.scrollTop = ref.current.scrollHeight;
23+
}
24+
}, [roomId]);
25+
26+
useEffect(() => {
27+
if (!ref.current) {
28+
return;
29+
}
30+
31+
handleRestoreScroll();
32+
33+
const refValue = ref.current;
34+
const store = RoomManager.getStore(roomId);
35+
36+
const handleWrapperScroll = withThrottling({ wait: 100 })((event) => {
37+
store?.update({ scroll: event.target.scrollTop, atBottom: isAtBottom(event.target, 50) });
38+
});
39+
40+
refValue.addEventListener('scroll', handleWrapperScroll, { passive: true });
41+
42+
return () => {
43+
refValue.removeEventListener('scroll', handleWrapperScroll);
44+
};
45+
}, [roomId, handleRestoreScroll]);
5046

5147
return {
52-
innerRef: useMergedRefs(refCallback, ref) as unknown as RefObject<HTMLElement>,
48+
innerRef: ref,
5349
};
5450
}

0 commit comments

Comments
 (0)