Skip to content

fix(messages): release Virtua's held range when scrolling stops - #2032

Merged
loganj merged 2 commits into
mainfrom
larry/virtua-idle-releases-jump-hold
Oct 31, 2026
Merged

loganj merged 2 commits into
mainfrom
larry/virtua-idle-releases-jump-hold

Conversation

@loganj

@loganj loganj commented Oct 30, 2026

Copy link
Copy Markdown
Contributor

Summary

  • While you scroll the channel timeline, the list renders extra rows ahead of where you are heading, so a fast scroll doesn't show blank space. Once scrolling stops, it renders that extra on both sides.
  • Rows above the viewport sometimes change height, for example as older history measures in. The list then corrects the scroll position so the rows you are reading stay put. The list holds its rendered rows in place until that correction's own scroll event arrives.
  • Some corrections never get a scroll event of their own: the correction moved nothing, or another write undid it in the same task. Before this change, the held rows then stayed in place even after scrolling stopped. The timeline sat idle with extra rows on one side only, and a row just past the viewport on the other side stayed unmounted until the next scroll.
  • That broke main CI in Linux WebKit: history-loading.spec.mjs "a held pointer keeps its place after the page's reader row unmounts" (run https://github.com/block/buzz-app/actions/runs/41120162311). After a 91px correction, the list went idle with no rows rendered below the viewport. The held message row did not remount before the test failed.
  • Now, when scrolling stops, the list releases held rows and re-renders. So once idle it again renders extra rows on both sides, as patches/README.md describes.

Details

  • The change is in our Virtua patch (patches/[email protected]; Virtua is the list virtualization library):
    • The store's scroll-end step now clears a pending correction and re-renders, even when the scroll direction was already idle.
    • Before that, the driver's scroll-end reads the native scroll offset, as its viewport-resize path already does. A correction whose scroll event arrives after the 150 ms scroll-end timer is then handled as its own event. Without this, the release would re-render from the pre-correction offset: a 40-row history page would mount rows 0–24 while the viewport sat at 2300–2800px.
  • What the CI trace shows, and what it doesn't: it shows the list idle with extra rows on one side only, right after the correction. It does not show which write left the correction without a scroll event.
  • Known limit, stated in the README: a correction made after scrolling has already stopped, with no later scroll event, still holds its rows until the next scroll. This is unchanged from before.
  • Tests (src/features/messages/virtua-compensation.test.mjs) run the installed, patched store and driver:
    • A correction undone before its scroll event, then the driver's real scroll-end timer: the list renders both sides once idle.
    • A held range with the direction already idle: scroll-end re-renders.
    • Scroll-end fires before a history page's correction scroll event: the held rows stay correct.
    • The first two fail on main's patch. The third fails without the driver change. All 43 pass with both changes.
  • Browser cases added or removed: none. The browser journey that failed in CI is unchanged.
  • Validation, local macOS: typecheck, all src/features/messages Vitest tests, and the history-loading, image-scroll and initial-position browser files in Chromium and WebKit (2 workers).
    • One WebKit case, "input after the page lands starts from the reader's row", failed intermittently by about 40px: 2 of 31 runs with this change, 2 of 20 with main's patch. It is an existing local flake, not caused by this change.
    • The CI failure itself did not reproduce on macOS, so hosted Linux WebKit is the real check.

🤖 Generated with Claude Code

loganj and others added 2 commits October 30, 2026 16:59
A flushed scroll correction holds the mounted range until its own scroll
event. A correction that moves nothing, or that another write undoes in
the same task, raises no event, so the held range outlived idle. On Linux
WebKit the timeline sat idle with no buffer below the viewport, and a
held reader row just below it did not remount before the test failed (history-loading.spec.mjs
"a held pointer keeps its place after the page's reader row unmounts",
CI run 41120162311).

Scroll-end now releases the held range and re-renders, so idle buffers
both sides as patches/README.md describes.

Co-authored-by: Larry <627498bd4bd1f281a16431e3c6cce3b5c25b6692798c78672298aefbf2f8f8b5@buzz.block.builderlab.xyz>
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Signed-off-by: Logan Johnson <[email protected]>
The scroll-end timer can fire between a flushed jump's write and that
jump's scroll event. Releasing the held range then re-rendered from the
pre-jump offset against post-jump row offsets: a 40-row prepend mounted
rows 0-24 while the viewport sat at 2300-2800px.

Scroll-end now reads the native offset first, as the viewport resize
path does, so a trailing jump is consumed as its own event and the
release applies only to a jump that left the offset unmoved.

Co-authored-by: Larry <627498bd4bd1f281a16431e3c6cce3b5c25b6692798c78672298aefbf2f8f8b5@buzz.block.builderlab.xyz>
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Signed-off-by: Logan Johnson <[email protected]>
@loganj
loganj marked this pull request as ready for review October 30, 2026 21:35
@loganj
loganj requested a review from a team as a code owner October 30, 2026 21:35

@kalvinnchau kalvinnchau left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖 Reviewed at 15b4ef3. No defects found. The scroll-end release is confined to the single e.h(2) caller, the driver reads the native offset before the release, and both changes are covered by tests that fail when each is reverted. Local macOS: src/features/messages Vitest 2266/2266 passed; CI green.

@loganj
loganj merged commit 177d375 into main Oct 31, 2026
39 checks passed
@loganj
loganj deleted the larry/virtua-idle-releases-jump-hold branch October 31, 2026 00:39
shellz-n-stuff added a commit that referenced this pull request Oct 31, 2026
Picks up the month-boundary StatusEditor test fix (#2064).

Co-Authored-By: Claude Opus 5.5 <[email protected]>

* origin/main: (56 commits)
  perf(sidebar): subscribe to selected live and list facts (#1966)
  feat(desktop): align the sign-in callback page with web branding (#2038)
  Show linked PRs in Solo headers and move idle huddles to Chat actions (#1968)
  feat(messages): mark edited messages with "(edited)" (#2020)
  feat(desktop): bundle the buzz CLI in every package (#2027)
  feat(plugins): open a plugin page from buzz://plugin links (#2053)
  feat(plugins): managed plugin sources (#2052)
  test(browser): prevent sampled clock pause races (#1955)
  test: fix month-boundary and Sonner mount races in StatusEditor and RelayStaff tests (#2064)
  test: stop tests leaving temp dirs and a flake-check fixture behind (#1977)
  fix(messages): release Virtua's held range when scrolling stops (#2032)
  test(mcp): allow protection checks in bundled help fixture (#2061)
  fix(emoji): recover transient image failures without reopening chats (#2000)
  test: wait for dialog focus, reach a reading position, re-sign pair() (#1975)
  feat(plugins): sign builderlab in through the native fi session in fi builds (#1461)
  feat(agents): offer bundled Buzz help (#2033)
  fix(github): show pull request status for anchors and subpages (#2055)
  fix(dialogs): focus safe controls and task inputs (#2021)
  fix(composer): truncate workspace names at narrow widths (#2043)
  AIDA-2287: Refresh public channel search incrementally (#2023)
  ...

Signed-off-by: Alex Rosenzweig <[email protected]>
morgmart added a commit that referenced this pull request Oct 31, 2026
…isibility

* origin/main: (23 commits)
  feat(profiles): show NIP-FI verified names (BUZZ-433) (#2005)
  feat(identity): sign in to the organization when Buzz opens (#2086)
  perf(identity-names): stop rerunning name policy for live member churn (#2092)
  fix(sidebar): keep the channel drag preview under the pointer (#2019)
  fix(identity): use xyz.block.buzz.desktop federated-session keychain service (#2094)
  perf(relay): read each channel's remote access once per accepted batch (#2091)
  fix(styles): keep page status :has() on each rule's subject (#2081)
  fix(navigation): show a notice for invalid deep links (#2054)
  fix(messages): preserve held readers through equal-height resizes (#1963)
  perf(sidebar): subscribe to selected live and list facts (#1966)
  feat(desktop): align the sign-in callback page with web branding (#2038)
  Show linked PRs in Solo headers and move idle huddles to Chat actions (#1968)
  feat(messages): mark edited messages with "(edited)" (#2020)
  feat(desktop): bundle the buzz CLI in every package (#2027)
  feat(plugins): open a plugin page from buzz://plugin links (#2053)
  feat(plugins): managed plugin sources (#2052)
  test(browser): prevent sampled clock pause races (#1955)
  test: fix month-boundary and Sonner mount races in StatusEditor and RelayStaff tests (#2064)
  test: stop tests leaving temp dirs and a flake-check fixture behind (#1977)
  fix(messages): release Virtua's held range when scrolling stops (#2032)
  ...

# Conflicts:
#	src/bundled/channels/ChannelMemberAvatars.tsx
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants