Fix Jump to Latest sticking visible while scrolled to the live bottom.

This commit is contained in:
2026-07-24 16:00:54 +10:00
parent b52926f7d8
commit f62200ecd1

View File

@@ -785,9 +785,16 @@ export function RoomTimeline({ room, eventId, roomInputRef, editor }: RoomTimeli
atLiveEndRef.current = liveTimelineLinked && rangeAtEnd; atLiveEndRef.current = liveTimelineLinked && rangeAtEnd;
// Historical / non-live windows are never "at bottom" of the room. // Historical / non-live windows are never "at bottom" of the room.
// When we return to the live end, re-check scroll — IntersectionObserver may
// not re-fire if the anchor stayed intersecting across the transition.
useEffect(() => { useEffect(() => {
if (!liveTimelineLinked || !rangeAtEnd) { if (!liveTimelineLinked || !rangeAtEnd) {
setAtBottom(false); setAtBottom(false);
return;
}
const scrollEl = scrollRef.current;
if (scrollEl && isScrolledToBottom(scrollEl, 100)) {
setAtBottom(true);
} }
}, [liveTimelineLinked, rangeAtEnd]); }, [liveTimelineLinked, rangeAtEnd]);
@@ -1034,11 +1041,16 @@ export function RoomTimeline({ room, eventId, roomInputRef, editor }: RoomTimeli
if (!targetEntry) return; if (!targetEntry) return;
// Leave the live bottom immediately so Jump to Latest stays available. // Leave the live bottom immediately so Jump to Latest stays available.
if (!targetEntry.isIntersecting || !atLiveEndRef.current) { // Only intersection clears atBottom here; !atLiveEnd is handled by the
// liveTimelineLinked/rangeAtEnd effect so we don't stick false when the
// live end reconnects without a new intersection event.
if (!targetEntry.isIntersecting) {
setAtBottom(false); setAtBottom(false);
return; return;
} }
if (!atLiveEndRef.current) return;
setAtBottom(true); setAtBottom(true);
setLatestUnreadMessage(null); setLatestUnreadMessage(null);
if (document.hasFocus()) { if (document.hasFocus()) {
@@ -1145,7 +1157,16 @@ export function RoomTimeline({ room, eventId, roomInputRef, editor }: RoomTimeli
const onScroll = () => { const onScroll = () => {
if (restoringScrollRef.current) return; if (restoringScrollRef.current) return;
window.cancelAnimationFrame(raf); window.cancelAnimationFrame(raf);
raf = window.requestAnimationFrame(() => persistTimelineScroll(roomId)); raf = window.requestAnimationFrame(() => {
persistTimelineScroll(roomId);
// Keep Jump to Latest in sync with real scroll position when on the live end.
// IntersectionObserver alone can miss transitions and leave atBottom stuck.
if (atLiveEndRef.current) {
const at = isScrolledToBottom(scrollEl, 100);
setAtBottom(at);
if (at) setLatestUnreadMessage(null);
}
});
}; };
scrollEl.addEventListener('scroll', onScroll, { passive: true }); scrollEl.addEventListener('scroll', onScroll, { passive: true });
@@ -1250,6 +1271,9 @@ export function RoomTimeline({ room, eventId, roomInputRef, editor }: RoomTimeli
scrollToBottomRef.current.smooth, scrollToBottomRef.current.smooth,
scrollToBottomRef.current.force scrollToBottomRef.current.force
); );
if (atLiveEndRef.current) {
setAtBottom(true);
}
} }
} }
}, [scrollToBottomCount]); }, [scrollToBottomCount]);
@@ -2588,7 +2612,12 @@ export function RoomTimeline({ room, eventId, roomInputRef, editor }: RoomTimeli
</MessageBase> </MessageBase>
</> </>
))} ))}
<span ref={atBottomAnchorRef} /> {/* 1×1 so IntersectionObserver reliably detects the live bottom */}
<span
ref={atBottomAnchorRef}
aria-hidden
style={{ display: 'block', width: 1, height: 1 }}
/>
</Box> </Box>
</Scroll> </Scroll>
{(showUnreadTop || returnToEventId) && ( {(showUnreadTop || returnToEventId) && (