From f62200ecd1c9bcc11e22c09c265390b73de24d35 Mon Sep 17 00:00:00 2001 From: litruv Date: Fri, 24 Jul 2026 16:00:54 +1000 Subject: [PATCH] Fix Jump to Latest sticking visible while scrolled to the live bottom. --- src/app/features/room/RoomTimeline.tsx | 35 +++++++++++++++++++++++--- 1 file changed, 32 insertions(+), 3 deletions(-) diff --git a/src/app/features/room/RoomTimeline.tsx b/src/app/features/room/RoomTimeline.tsx index 8dd194e..996169b 100644 --- a/src/app/features/room/RoomTimeline.tsx +++ b/src/app/features/room/RoomTimeline.tsx @@ -785,9 +785,16 @@ export function RoomTimeline({ room, eventId, roomInputRef, editor }: RoomTimeli atLiveEndRef.current = liveTimelineLinked && rangeAtEnd; // 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(() => { if (!liveTimelineLinked || !rangeAtEnd) { setAtBottom(false); + return; + } + const scrollEl = scrollRef.current; + if (scrollEl && isScrolledToBottom(scrollEl, 100)) { + setAtBottom(true); } }, [liveTimelineLinked, rangeAtEnd]); @@ -1034,11 +1041,16 @@ export function RoomTimeline({ room, eventId, roomInputRef, editor }: RoomTimeli if (!targetEntry) return; // 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); return; } + if (!atLiveEndRef.current) return; + setAtBottom(true); setLatestUnreadMessage(null); if (document.hasFocus()) { @@ -1145,7 +1157,16 @@ export function RoomTimeline({ room, eventId, roomInputRef, editor }: RoomTimeli const onScroll = () => { if (restoringScrollRef.current) return; 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 }); @@ -1250,6 +1271,9 @@ export function RoomTimeline({ room, eventId, roomInputRef, editor }: RoomTimeli scrollToBottomRef.current.smooth, scrollToBottomRef.current.force ); + if (atLiveEndRef.current) { + setAtBottom(true); + } } } }, [scrollToBottomCount]); @@ -2588,7 +2612,12 @@ export function RoomTimeline({ room, eventId, roomInputRef, editor }: RoomTimeli ))} - + {/* 1×1 so IntersectionObserver reliably detects the live bottom */} + {(showUnreadTop || returnToEventId) && (