Flash-free scroll restore on open (editor + chat) + chat batch-load fixes#2276
Merged
Conversation
…undation + editor)
On re-open the previous messages are still cached and rendered by the time a useLayoutEffect runs, so scroll straight to the target (last-read message, else bottom) BEFORE the first paint — the first frame is already positioned, with no stale 'past' frame and no jump, matching the editor's pre-paint restore. No hide or delay. init() refreshes asynchronously and re-confirms the same target; Chromium's overflow-anchor keeps the viewport stable. Keyed to the chat subId so in-place chat->chat switches re-position and same-chat dependency churn doesn't. Cold opens (no cache) fall through to init()'s normal async positioning.
…ndow) loadDeps destroys and re-subscribes the whole per-chat deps subscription to only the ids it is given. The batch-load paths passed only the NEW page, so already-loaded messages' image/file attachments were dropped from S.Detail and their images vanished on every batch load. Subscribe deps for the full window (existing + new) in loadMessages and subscribeMessages(non-clear), matching the live-message path (onMessageAdd) that was already correct.
The 'load older' prefetch is scroll-event driven (fires when scrollTop is within ~1 viewport of the top), and the chat relies on the browser's overflow-anchor to hold position on prepend. But overflow-anchor is suppressed at scrollTop 0, so a fast scroll that reaches the hard top pins the view at 0: the prepend doesn't shift it down and no further scroll event fires, so pagination stalls. Fix: only at scrollTop 0, capture the top message before the prepend and restore its position pre-paint (useLayoutEffect on dummy), landing the view just below the newly-loaded batch — off 0 — so the prefetch keeps firing as you scroll. For scrollTop > 0 nothing changes (overflow-anchor still handles it). The head is never evicted by a prepend, so the anchor always survives.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to subscribe to this conversation on GitHub.
Already have an account?
Sign in.
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Why
Opening an object or chat restored the saved scroll after the content had already painted at the top → a visible flash + jump. This makes the restore happen before the first paint, so the first frame is already in the right place. While testing the chat side, three related batch-loading bugs surfaced and are fixed here too.
Editor
useScrollRestorehook): save the topmost-visible block + offset on scroll; on open, position synchronously inuseLayoutEffect(before paint), anchored to the element so it stays exact even as async content settles. BoundedResizeObserverre-pins through the settle window; legacy pixelgetScrollkept as fallback.#blockLastfiller measure so a near-bottom target is reachable on frame one; skipfocus.scrollon open when a saved scroll exists.edit.tsx) — unnecessary latency once restore is pre-paint.Chat
overflow-anchorholds it acrossinit()'s async refresh.loadDepsdestroys + re-subscribes the whole per-chat deps subscription; the batch paths passed only the new page, dropping already-loaded messages' attachments fromS.Detail. Now subscribe deps for the full window.overflow-anchoris suppressed atscrollTop 0, so a fast scroll pinned the view there and the scroll-driven prefetch couldn't re-fire. AtscrollTop 0only, anchor the top message and restore its position pre-paint so the view lands off 0 and loads keep flowing.Tests / verification
src/ts/lib/util/scrollAnchor.test.ts(restore math + settle tracker).bun run typecheck+bun run lintclean.Notes
docs/superpowers/.