docs(message-list): note smooth-scroll exclusion; point sentinel comment at subsystem doc
Reviewer caught two small gaps after Task 3: - Effect A's smooth-scroll path on new messages is intentionally NOT instrumented with the sentinel (the animation lands asynchronously across frames; no intermediate scrollTop is worth pinning to). The doc now records this so the reader's intuition matches the code. - The sentinel-branch comment in MessageList.tsx pointed at "spec §2", which is the planning doc rather than the durable subsystem spec. Pointed at docs/systems/message-list.md instead.
This commit is contained in:
@@ -15,7 +15,7 @@ The chat message list (`packages/web/src/components/chat/MessageList.tsx`) is re
|
|||||||
|
|
||||||
Three effects cooperate. Their ordering is established by the 2026-03-25 race-fix and the 2026-04-25 sentinel addendum.
|
Three effects cooperate. Their ordering is established by the 2026-03-25 race-fix and the 2026-04-25 sentinel addendum.
|
||||||
|
|
||||||
**Effect A — initial snap / restore.** Runs once when `messages.length` transitions from 0 to N for a channel. Reads `chatStore.scrollPositions.get(channelId)`. If a saved anchor exists, scrolls that message into view and computes the resulting `isAtBottomRef` from actual distance. Otherwise, sets `container.scrollTop = container.scrollHeight`, captures the post-clamp value into `lastProgrammaticBottomScrollRef`, and sets `isAtBottomRef.current = true`. On subsequent message arrivals (`messages.length > prev`), if `isAtBottomRef.current`, smooth-scrolls via `bottomRef.scrollIntoView({ behavior: 'smooth' })`.
|
**Effect A — initial snap / restore.** Runs once when `messages.length` transitions from 0 to N for a channel. Reads `chatStore.scrollPositions.get(channelId)`. If a saved anchor exists, scrolls that message into view and computes the resulting `isAtBottomRef` from actual distance. Otherwise, sets `container.scrollTop = container.scrollHeight`, captures the post-clamp value into `lastProgrammaticBottomScrollRef`, and sets `isAtBottomRef.current = true`. On subsequent message arrivals (`messages.length > prev`), if `isAtBottomRef.current`, smooth-scrolls via `bottomRef.scrollIntoView({ behavior: 'smooth' })` — this path deliberately does *not* update the sentinel, because the smooth animation lands asynchronously across many frames and no single intermediate `scrollTop` is worth pinning to. The path is already gated on `isAtBottomRef.current`, so it cannot fire while the user is scrolled away; the rare image-load growth during a smooth scroll is tolerated.
|
||||||
|
|
||||||
**Effect B — ResizeObserver.** Observes the message-list content container. When height grows and `isAtBottomRef.current === true`, re-pins to bottom and updates the sentinel. Gated on `isAtBottomRef.current` so it cannot interfere when the user has scrolled away.
|
**Effect B — ResizeObserver.** Observes the message-list content container. When height grows and `isAtBottomRef.current === true`, re-pins to bottom and updates the sentinel. Gated on `isAtBottomRef.current` so it cannot interfere when the user has scrolled away.
|
||||||
|
|
||||||
|
|||||||
@@ -240,7 +240,7 @@ export function MessageList({ channelId, jumpToMessageId, onJumpComplete }: Mess
|
|||||||
// was queued by our own command. Layout may have grown between the command and the
|
// was queued by our own command. Layout may have grown between the command and the
|
||||||
// event firing, but our intent is "stay at bottom" — do not let a post-growth distance
|
// event firing, but our intent is "stay at bottom" — do not let a post-growth distance
|
||||||
// measurement flip the at-bottom flags. Re-pin defensively (content may have grown
|
// measurement flip the at-bottom flags. Re-pin defensively (content may have grown
|
||||||
// again) and update the sentinel; convergence is described in the spec §2.
|
// again) and update the sentinel. See docs/systems/message-list.md (Auto-scroll model).
|
||||||
if (container.scrollTop === lastProgrammaticBottomScrollRef.current) {
|
if (container.scrollTop === lastProgrammaticBottomScrollRef.current) {
|
||||||
isAtBottomRef.current = true;
|
isAtBottomRef.current = true;
|
||||||
setIsAtBottom(true);
|
setIsAtBottom(true);
|
||||||
|
|||||||
Reference in New Issue
Block a user