Skip to content
Closed
Changes from all 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
36 changes: 35 additions & 1 deletion src/app/features/room/RoomTimeline.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -226,6 +226,10 @@ export function RoomTimeline({
const topSpacerHeightRef = useRef(0);
const mountScrollWindowRef = useRef<number>(Date.now() + 3000);
const hasInitialScrolledRef = useRef(false);
// Short-lived guard set for ~350 ms after a jump scrollToIndex so that
// intermediate scroll events from the animation don't flip atBottom prematurely.
const jumpScrollBlockRef = useRef(false);
const jumpScrollBlockTimerRef = useRef<ReturnType<typeof setTimeout> | undefined>(undefined);
// Stored in a ref so eventsLength fluctuations (e.g. onLifecycle timeline reset
// firing within the window) cannot cancel it via useLayoutEffect cleanup.
const initialScrollTimerRef = useRef<ReturnType<typeof setTimeout> | undefined>(undefined);
Expand Down Expand Up @@ -260,6 +264,23 @@ export function RoomTimeline({
vListRef.current.scrollTo(vListRef.current.scrollSize);
}, []);

// Start a short scroll-settle block after a programmatic jump scrollToIndex.
// After 350 ms the block lifts and atBottom is recomputed from the actual
// VList position so "Jump to Latest" appears correctly.
const startJumpScrollBlock = useCallback(() => {
jumpScrollBlockRef.current = true;
if (jumpScrollBlockTimerRef.current !== undefined) clearTimeout(jumpScrollBlockTimerRef.current);
jumpScrollBlockTimerRef.current = setTimeout(() => {
jumpScrollBlockRef.current = false;
jumpScrollBlockTimerRef.current = undefined;
const v = vListRef.current;
if (v) {
const dist = v.scrollSize - v.scrollOffset - v.viewportSize;
setAtBottom(dist < 100);
}
}, 350);
}, [setAtBottom]);

const timelineSync = useTimelineSync({
room,
mx,
Expand Down Expand Up @@ -399,6 +420,7 @@ export function RoomTimeline({
const processedIndex = getRawIndexToProcessedIndex(timelineSync.focusItem.index);
if (processedIndex !== undefined) {
vListRef.current.scrollToIndex(processedIndex, { align: 'center' });
startJumpScrollBlock();
timelineSync.setFocusItem((prev) => (prev ? { ...prev, scrollTo: false } : undefined));
}
}
Expand All @@ -409,7 +431,7 @@ export function RoomTimeline({
return () => {
if (timeoutId !== undefined) clearTimeout(timeoutId);
};
}, [timelineSync.focusItem, timelineSync, reducedMotion, getRawIndexToProcessedIndex]);
}, [timelineSync.focusItem, timelineSync, reducedMotion, getRawIndexToProcessedIndex, startJumpScrollBlock]);

useEffect(() => {
if (timelineSync.focusItem) {
Expand All @@ -420,6 +442,10 @@ export function RoomTimeline({
useEffect(() => {
if (!eventId) return;
setIsReady(false);
// Re-arm the initial-scroll guard so that if the jump fails and
// useTimelineSync falls back to the live timeline, the useLayoutEffect
// can fire and call setIsReady(true) via the normal initial-scroll path.
hasInitialScrolledRef.current = false;
timelineSyncRef.current.loadEventTimeline(eventId);
}, [eventId, room.roomId]);

Expand Down Expand Up @@ -533,6 +559,7 @@ export function RoomTimeline({
}
if (vListRef.current && processedIndex !== undefined) {
vListRef.current.scrollToIndex(processedIndex, { align: 'center' });
startJumpScrollBlock();
}
timelineSync.setFocusItem({ index: focusRawIndex, scrollTo: false, highlight: true });
} else {
Expand Down Expand Up @@ -675,6 +702,13 @@ export function RoomTimeline({

const distanceFromBottom = v.scrollSize - offset - v.viewportSize;
const isNowAtBottom = distanceFromBottom < 100;

// While a jump scroll is settling (briefly after scrollToIndex), VList
// fires intermediate scroll events that can incorrectly flip atBottom.
// Use a short-lived block instead of the full focusItem lifetime so that
// normal scrolling resumes quickly and atBottom is recomputed correctly.
if (jumpScrollBlockRef.current) return;

if (isNowAtBottom !== atBottomRef.current) {
setAtBottom(isNowAtBottom);
}
Expand Down
Loading