The memo-boundary re-render fix needs visual regression evidence that
scroll-back through history tracks correctly rather than freezing on the
newest rows. The screenshot spec captures the post-fix tracking and
top-of-history states under the same mock-relay window semantics the
timeline uses in production.
Also re-points emitMockHistory's since-filter comment at the timeline's
actual live subscription (subscribeToChannel -> buildChannelFilter,
limit:50, no since/until). The prior comment misattributed it to the
huddle-TTS sub, leaving a reader to believe the timeline path exercises
the since branch when it does not.
Co-authored-by: Will Pfleger <pfleger.will@gmail.com>
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
Wheeling to the top of a channel left the rendered window frozen on the
newest ~11 rows — users could not scroll back through history. MessageTimeline
re-renders on every scroll via useVirtualizer's onChange, but TimelineMessageList
is React.memo and read getVirtualItems() internally through the virtualizer's
stable mutable ref. Shallow compare cannot see the range walk through that ref,
so the memo skipped re-render while the virtual range advanced underneath.
Thread virtualItems as a fresh-array prop from the hook-owner into the memoized
renderer so memo sees a new reference each render and re-renders in lockstep —
the documented react-virtual pattern. The e2e fixture now honors since/until/limit
to model a true older-page gap, and a new anchor test asserts the tracked row's
getBoundingClientRect().top holds within 8px across a scroll-back.
Co-authored-by: Will Pfleger <pfleger.will@gmail.com>
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
The short-channel topPad bottom-align was doubly broken. First, the pad was
set as the spacer's paddingTop, but an absolutely-positioned virtual row
resolves top:0 against the padding box inner edge — padding inflated the box
without moving the rows, so they pinned to the top with dead space below
(the inverse of bottom-align). Fold topPad into each row's translateY instead.
Second, the pad ignored the non-row scroll chrome (the channel-header padding
above the spacer and composer padding below), so the spacer over-padded by
exactly that chrome and a short channel became scrollable. Subtract the
measured chrome (scrollHeight minus spacer height, which is pad-invariant).
Adds a fail-first e2e asserting a short channel bottom-aligns against the
content-box floor and is not scrollable.
Co-authored-by: Will Pfleger <pfleger.will@gmail.com>
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
Virtualizing the timeline made the legacy sticky DayDivider impossible:
position:sticky cannot work on a position:absolute virtual row. Restore
the pinned-date UX with the standard virtualized sticky-group-header
pattern instead — selectActiveDayHeading derives the day group owning the
topmost rendered row (index-based, measurement-free), and the timeline
paints ONE header in a sibling layer pinned at the measured channel-chrome
top. The matching inline divider is hidden with invisible so its label
never doubles while keeping its measured height (no scroll jump).
The expanded-diff viewer is a Radix Dialog portal, so its open state must
live above the virtualized rows — otherwise scrolling the triggering row
out of the window unmounts it and the open modal vanishes. Lift ownership
to both surfaces via an onExpandDiff callback. A single ExpandedDiff value
(not a Set) is the faithful model: the modal backdrop makes more than one
open structurally impossible.
Co-authored-by: Will Pfleger <pfleger.will@gmail.com>
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
The two chat-message surfaces (MessageTimeline, MessageThreadPanel) ran
on a hand-rolled scroll manager with two ResizeObservers and a double-rAF
scrollTop band-aid that raced during prepend, causing the channel-jump
bug. Replace it with @tanstack/react-virtual as the single scroll owner.
virtual-core@3.17.0 ships the three hard behaviors natively, so they are
configuration not hand-built code: anchorTo:"end" holds the viewport on
load-older prepend, followOnAppend pins the bottom during a burst, and the
internal reconcile loop settles deep-link scrollToIndex once the target
row measures. The hook owns only what the library does not: the
short-channel bottom-align pad, the at-bottom/new-message pill state, and
the deep-link highlight.
Channel/thread switch resets via key-remount (fresh virtualizer = zero
stale measurement cache) rather than porting the old reset logic.
useTimelineScrollManager + useLoadOlderOnScroll deleted, zero callers.
Co-authored-by: Will Pfleger <pfleger.will@gmail.com>
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
Phase 1 of the timeline virtualization migration. Adds the two foundations
the surface rewrites consume in Phase 2, with no behavior swap yet.
buildTimelineVirtualItems flattens the main-timeline entries + day
boundaries + firstUnreadMessageId into one ordered, keyed item per measured
row (dividers become their own items; the thread summary rides on its message
row so each message stays a single measured unit). Keys are byte-identical to
the legacy render keys so deep-link lookups and test selectors keep resolving.
useChatScrollVirtualizer wraps useVirtualizer directly (not the shared
VirtualizedList). It leans on virtual-core 3.17's library-native anchorTo:end
(anchored prepend) and followOnAppend (bottom-stick), and owns only the
short-channel bottom-align pad, recomputed off the live getTotalSize() on each
size-changing measurement and on container resize.
Co-authored-by: Will Pfleger <pfleger.will@gmail.com>
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
Adds a build-time-configured transport reconnect hook so internal builds can recover the underlying transport (e.g. WARP VPN) before the relay reconnect, while OSS builds stay a pure no-op.
The generic "Reconnect to relay" button is transport-agnostic; internal users behind a VPN need the transport re-established first. A build-time env var `BUZZ_BUILD_RELAY_RECONNECT_CMD` (set only in `squareup/buzz-releases`) carries a JSON config validated into a typed Rust struct at compile time, so the OSS binary ships with zero VPN knowledge.
- New Tauri command `relay_reconnect_hook` runs structured fixed-argv steps plus a readiness probe, wrapped in `tokio::task::spawn_blocking`; non-fatal end-to-end so any failure falls through to `preconnect()`.
- `ReconnectHookConfig` lives in one dep-free source file `include!`'d by both `build.rs` and the runtime command, so the compile-time validation and runtime parse cannot drift.
- `useReconnectRelay.ts` invokes the hook before relay preconnect, guarded so a hook rejection cannot abort the reconnect.
- When `BUZZ_BUILD_RELAY_RECONNECT_CMD` is unset (OSS), the command compiles to an early-return no-op.
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
Co-authored-by: npub1mn7jgtj4w2pd0g0zeuhxsa6jy6p0rewxz4kujt98my82ahfmp72sxjexk7 <dcfd242e557282d7a1e2cf2e6877522682f1e5c6156dc92ca7d90eaedd3b0f95@sprout-oss.stage.blox.sqprod.co>