Phase 2: render only the viewport (+overscan) of the main timeline instead of
all ~200+ rows, lifting the ~2,000-row ceiling. Main timeline ONLY — the thread
pane (and its useTimelineScrollManager) is untouched (Phase 3).
What changed:
- MessageTimeline now drives @tanstack/react-virtual off the same deferred
snapshot the rows render from, flattening day-grouped entries into
index-addressable rows via buildVirtualTimelineRows. Rows are built from the
FILTERED main-timeline entries (buildMainTimelineEntries), so dropped thread
replies never desync the row->entry mapping.
- useVirtualTimelineScroll REPLACES the bespoke 427-line useTimelineScrollManager
for the main timeline. The virtualizer owns the scroll container and all
measurement/anchoring; this hook is a thin wrapper keeping sticky-bottom
autoscroll, accent smooth-scroll, the newMessageCount pill, isAtBottom, and
deep-link + find-in-page jumps (via findVirtualRowIndexForMessage ->
scrollToIndex). No scrollTop locking, no ResizeObserver re-pinning.
- useLoadOlderOnScroll is gutted to a trigger: the double-requestAnimationFrame
scrollTop correction and restoreScrollPosition plumbing are DELETED. Stable
per-row keys (getItemKey) let the virtualizer hold scroll position on prepend
natively — the band-aid is gone at the root.
- cmd+F find drives scroll-to-row through the same scrollToIndex bridge; a
render-all-while-searching escape hatch exists as an off-by-default fallback
(the in-app path is the default, keeping the perf win).
- The per-entry render (3 row variants + video-review context) is extracted to
renderTimelineEntry + useVideoReviewContextById, a single source of truth
injected into VirtualizedTimelineList. The old nested TimelineMessageList is
deleted (orphaned by the rewire).
Must-keeps preserved: sticky-bottom autoscroll, day dividers (first-class
variable-height flat rows), jump-to-message deep links, no-tearing (same
deferredMessages snapshot drives both rows and scroll logic).
Coverage: 8 jsdom/testing-library DOM tests cover row dispatch (divider vs
message, flat-index->entry mapping), init autoscroll, accent smooth-scroll, and
deep-link scrollToIndex. jsdom has no layout engine, so react-virtual's pixel
measurement/scroll math stays on a manual/visual verification pass.
Co-authored-by: Taylor Ho <taylorkmho@gmail.com>
Signed-off-by: Taylor Ho <taylorkmho@gmail.com>