Repository navigation
fix(VirtualizedMessageList): keep the last message fully visible in short lists - #3317
Conversation
β¦hort lists Virtuoso reads a bare index as align: 'start', so both the initial anchor and scrollToBottom parked the last message's top at the viewport top. Where that message is taller than the list -- a floating panel, a sidebar, a compact mobile view -- its bottom was clipped by the difference, and the jump-to-latest button landed in the same place rather than at the bottom. The hardcoded atBottomThreshold of 100px compounded it: on a ~200px list that is half the viewport, so the list called itself at the bottom while part of a message was still below the fold. That flag hides the jump-to-latest button, suppresses the new-message notification, and marks the channel read, so a message could be marked read that had never been fully shown, with no control left to reach it. Both thresholds now scale with the measured scroller height, and marking read additionally requires the list to be at its true bottom rather than merely inside the threshold. MessageList keeps its existing behaviour: the stricter mark-read gate is opt-in and only VirtualizedMessageList passes it. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
Important Review skippedAuto reviews are disabled on base/target branches other than the default branch. Please check the settings in the CodeRabbit UI or the βοΈ Run configuration
You can disable this status message by setting the Use the checkbox below for a quick retry:
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
|
Size Change: +2.41 kB (+0.21%) Total Size: 1.13 MB π¦ View Changed
βΉοΈ View Unchanged
|
Codecov Reportβ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## release-v14 #3317 +/- ##
===============================================
+ Coverage 85.63% 85.87% +0.23%
===============================================
Files 521 522 +1
Lines 16555 16632 +77
Branches 5208 5221 +13
===============================================
+ Hits 14177 14282 +105
+ Misses 2378 2350 -28 β View full report in Codecov by Harness. π New features to boost your workflow:
|
β¦verrides Two gaps in the previous commit, from review on the v15 port (#3318). An integrator passing additionalVirtuosoProps.scrollerRef replaced the callback the viewport measurement depends on, leaving the thresholds at their full-height defaults and isAtTrueBottom permanently true. scrollerRef is now applied after the spread and composed with the caller's. Content can also grow without the scroller resizing or scrolling -- a bubble whose avatar or image lands late -- which moves the true bottom without firing either event the hook observes. totalListHeightChanged now triggers a fresh measurement, likewise composed with the caller's. The third review finding does not apply here: this branch already carries isAtTrueBottom in the mark-read effect's dependencies. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
β¦ls the window With `useWindowScroll` Virtuoso hands back the window rather than an element, and the viewport hook discarded it. The list was then never measured: the thresholds stayed at their full-height defaults and the true-bottom check answered "yes" forever, so a channel could still be marked read with the newest message cut off. Scroll events still come from the window, but the measurements now come from the document's scrolling element. Also covers the two callback-composition paths that had no tests: an integrator's `scrollerRef` and `totalListHeightChanged` still run, and the list still measures itself rather than silently falling back to the unmeasured defaults. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
β¦modes The previous commit handled a Window arriving through `scrollerRef`, but that never happens: Virtuoso's window scroller passes its own no-op in the scrollerRef slot (List.tsx, buildWindowScroller), so an integrator's callback is never invoked under `useWindowScroll` or `customScrollParent`. Nothing was measured, the thresholds stayed at their full-height defaults and the true-bottom check answered "yes" for the life of the list -- so a channel could still be marked read with the newest message clipped. The list now tells the hook what to measure for those two modes rather than waiting for a callback that will not come. Covered by a test for each mode, which needs a ResizeObserver stub because Virtuoso's own window-viewport hook requires one. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
β¦s teardown Virtuoso passes null when the scroller goes away, and a caller relying on that signal has to receive it through the composed callback too. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
β¦ UI shows The bottom check used a flat 4px while the jump-to-latest control used the viewport-scaled threshold, so between the two there was a band -- 4 to ~20px on a short panel, 4 to ~67px on a full-height one -- where the control was hidden because the list called itself at the bottom, and mark-read was blocked because the stricter check disagreed. The user saw no way to scroll further and the channel never went read. Both now derive from the same measured height, so they cannot disagree: if the UI says the list is at the bottom, the channel marks read. Found by cross-browser testing against a real channel; reproduced in Chrome, Firefox and Safari, and gone in all three after this change. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
## [14.13.2](v14.13.1...v14.13.2) (2026-10-13) ### Bug Fixes * **ai-components:** chart and markdown fixes ([#3320](#3320)) ([8c1b9bf](8c1b9bf)), closes [#3314](#3314) [#3315](#3315) * **VirtualizedMessageList:** keep the last message fully visible in short lists ([#3317](#3317)) ([1a6021e](1a6021e))
|
π This PR is included in version 14.13.2 π The release is available on: Your semantic-release bot π¦π |
π― Goal
If a chat panel is shorter than a single message, the newest message gets cut off at the bottom β and there is no way to scroll down to the rest of it. Clicking "jump to latest" does nothing. This shows up in floating chat panels, sidebars and compact mobile views; the reporter's panels are about 200px tall with messages up to 336px.
A full-height message list never shows this, which is why it has gone unnoticed for so long.
Closes REACT-1221
π Implementation details
Two separate problems stack up here.
1. We asked Virtuoso to scroll to the wrong place.
When you hand
scrollToIndexa plain number, Virtuoso aligns that row's top with the top of the viewport. That is fine while the message is shorter than the list β Virtuoso clamps and you land at the bottom anyway. Once the message is taller than the list, you land with its bottom hanging below the fold, cut off by exactly the difference.Both places that scroll to the newest message passed a plain number: the initial anchor (
calculateInitialTopMostItemIndex) andscrollToBottom. Both now ask for the end of the row instead:The index was already correct β
'LAST'resolves to the same value β so only the alignment changes.2. The list thought it was at the bottom when it wasn't.
atBottomThresholdwas hardcoded to 100px. That is a sensible slack for a full-height list, but on a 200px panel it is half the viewport, so the list reports "at bottom" while a good chunk of a message is still hidden.That one flag drives three things: it hides the jump-to-latest button, it suppresses the new-message notification, and it feeds the mark-read decision. So a message could be marked read on the server that the user had provably never seen, with the only control that could have reached it hidden by the same flag.
Both thresholds now scale with the actual height of the scroller (
clamp(viewportHeight * 0.1, 4, 100)), measured through a newuseScrollerViewporthook. For a full-height list this lands close to the old 100px; for a 200px panel it is about 20px. Marking read additionally requires the list to be at its true bottom rather than merely inside the threshold.MessageListis unaffected: the stricter mark-read gate is optional and onlyVirtualizedMessageListpasses it.π¨ UI Changes
Measured in a 200px panel with a 250px last message:
Tests. Three added, each confirmed to fail without its fix: the initial anchor and
scrollToBottomboth target the end of the last row, and the channel is not marked read while the last message is still clipped. jsdom has no layout engine, so these assert the scroll target; the clipping itself was verified in a browser.The v15 port is #3318.