Skip to content

New timeline panel in the room view (labs) - #34859

Draft
langleyd wants to merge 17 commits into
developfrom
langleyd/timeline-mvvm-3-panel
Draft

New timeline panel in the room view (labs)#34859
langleyd wants to merge 17 commits into
developfrom
langleyd/timeline-mvvm-3-panel

Conversation

@langleyd

@langleyd langleyd commented Aug 28, 2026

Copy link
Copy Markdown
Member

Timeline MVVM 3 — new timeline panel in the room view (labs)

Puts the new virtualised timeline into the room view behind the
"Improved timeline scrolling and navigation" labs flag. With the flag off,
nothing changes.

The integration

  • A new NewTimelinePanel renders the shared TimelineView +
    RoomTimelineViewModel (from the previous PRs in this series) in place of
    TimelinePanel, switched at a single point in RoomView.
  • Events render through the existing EventTile via a small
    LegacyEventTileAdapter, so message appearance is unchanged — tiles will be
    migrated to shared views one by one later.
  • The panel manages its own scrolling, read receipts and read marker; the read
    marker uses the new "New" line style from the design system.
  • Known gaps, to be addressed in follow-ups: keyboard navigation, scroll-to-bottom-on-send, read receipts aren't displayed yet, and IRC layout falls back to Modern. Also I might try setting last read position and fully read marker on close of the tab.

Keeping the timeline still

The bulk of the fixes here deal with one theme: rows changing height after
they render, shoving the timeline around
. Each cause needed its own fix:

  • Images — the box reserved for a photo before it downloads was silently
    collapsing to zero (a CSS sizing conflict), so the message grew when the
    photo arrived. The box now genuinely holds the photo's space from first
    paint, and failed images keep the same height instead of collapsing.

  • Polls — a poll's question and options are already in the event; only the
    votes need fetching. They now render immediately at full size, and the votes
    fill in without changing the height (previously the tile grew ~180px).

  • Reply previews — a new compact mode shows the quoted message immediately
    when it's already loaded, and stands a fixed-height skeleton in while
    fetching one that isn't, so the preview never resizes. Opt-in via an
    EventTile prop; the legacy timeline is untouched.

  • Everything else (link previews, late-loading content) — fixes to our
    @tanstack/virtual-core patch so the virtualizer's scroll compensation
    actually holds the view still when a row off-screen changes size:

    • decisions now use the up-to-date scroll position rather than one that can
      be a frame stale during a programmatic scroll;
    • the stay-at-bottom correction retries if the browser clamped the first
      write (the cause of "the timeline stays still while previews
      load and push content off the bottom, so the timline is no longer flush to the bottom");
    • corrections now also apply while scrolling upwards — previously every
      preview or image that finished loading above the viewport shoved the view
      mid-scroll.

    All three are candidates for upstreaming to TanStack Virtual.

Content that grows while it is on screen is deliberately left alone (a
message being edited shouldn't drag the view) — that residual movement is the
same as the legacy timeline and shrinks further as fixed-size designs land
(e.g. the new URL preview style).

Other fixes along the way

  • Loading more history could silently stall at the top of the timeline until
    the user jiggled the scroll — requests arriving mid-fetch are now remembered
    and re-run, matching the old ScrollPanel's behaviour.
  • Full-width rows (read marker line, date separators) overflowed the
    right-hand edge by the browser's default list indent.

Patch fixes so the timeline holds its place when rows change size while a
scroll is still settling: decisions use the up-to-date scroll position, the
stay-at-bottom correction retries if the browser clamped it, and corrections
now also apply while scrolling upwards. All candidates for upstreaming.
The list kept the browser's default 40px indent, which pushed every row 40px
too wide. Only full-width rows (the read marker line, date separators) showed
it.
The new design: a green line with a "New" label at its right-hand end. Opt-in
via a new label prop, so the legacy timeline keeps its plain line. Also adds an
as="div" option for callers that already provide the list item.
The box meant to hold a photo's space before it downloads was being sized to
zero by the browser, so the message grew when the photo arrived and shoved the
timeline. Failed images also keep the reserved height now instead of collapsing
to one line.
A request to load more history that arrived while a fetch was already running
was silently dropped, and the view would sometimes never re-ask — leaving the
timeline stuck at the top until the user jiggled the scroll. Such requests are
now remembered and run when the current fetch finishes, matching what the old
ScrollPanel did.
The question and options are in the event we already have; only votes need
fetching. Rendering them immediately stops a freshly loaded poll growing ~180px
when its votes arrive and shoving the timeline.
For the new timeline: the quoted message shows immediately when it is already
loaded, a fixed-height skeleton stands in while one is fetched, and the rows are
pinned to one height so the preview never resizes and shoves the messages around
it. Opt-in via a new EventTile prop; the legacy timeline is unchanged.
The new virtualised timeline panel, rendering events through the legacy
EventTile for now. Enabled by the "Improved timeline scrolling and navigation"
labs setting; the existing timeline is untouched when the flag is off.
Mark component props read-only, hoist the timeline row renderer to module
level, pass the quoted event into the reply header fetch instead of re-reading
state after the await, and drop a TODO tag.
The clamp stopping a reserved image box from overflowing its container lived in
new-timeline CSS, but the box is drawn by a shared component the legacy timeline
and thread panel use too, so images could overflow there. Moved it onto the
component's own link wrapper, beside the width that makes it necessary: a 757px
image in a 350px pane rendered at 757px before and 350px after, and still takes
its natural size when there is room.

Also, for the new timeline's rows: skip a row whose event has gone from the room
rather than letting the tile crash and lose the whole timeline, drop a leftover
placeholder that drew the word "Gap", and give the pagination spinner a label
screen readers can announce.
…mvvm-3-panel

# Conflicts:
#	apps/web/src/components/views/messages/MPollBody.tsx
#	apps/web/src/components/views/rooms/EventTile.tsx
#	pnpm-lock.yaml
The story had no left/right inset, so the label rendered flush against the edge
of the frame and was clipped. Pads it like the timeline does, and adds the
baseline image the visual test needs.
Covers how the panel draws each kind of row — messages, the read marker, date
separators, the loading spinner, gaps — along with the layout fallback, edit
state scoping, and skipping a row whose event has gone. Also covers the tile
adapter's pass-through and the reply preview's three states (already loaded,
skeleton while fetching, and error).

The panel's tests stand in a stub for the virtualised TimelineView, which needs
real layout the test environment doesn't have and has its own tests.
The stories dropped the marker into a plain list with no room around it, so the
browser's list styling came along for the ride: every baseline had a bullet in
it, and the rule was pressed against the top edge of the frame, hard to see and
liable to be clipped by the marker's own 1px offset. The hidden-marker story was
a picture of a bullet and nothing else.

Resetting the list and insetting the stories draws the marker the way a timeline
does — a hairline, evenly inset, with room above it.
…mvvm-3-panel

# Conflicts:
#	apps/web/src/components/views/elements/ReplyChain.test.tsx
The second media box in that story was being shrunk to fit its content (255px
wide) rather than taking the size the event declares; with the reserved box now
holding, it renders at 297px and its spinner and timestamp sit inside it instead
of overflowing. Every other story in the timeline suite is unchanged.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant