Media Gallery: opening it scrolls the room timeline into the past while it loads older media #163
Closed
opened 2026-09-17 23:06:34 -04:00 by jared
·
2 comments
No Branch/Tag Specified
lotus
update-packages
sw-fix
read-me-update
image-path-changes
dm-calls
fix-2469
renovate/element-hq-element-call-embedded-0.x
renovate/npm-i18next-http-backend-vulnerability
renovate/npm-vite-vulnerability
dev
docs-update
more-theme
fix-257
imporve-thread-reply
revert-2402-improve-menu-congestion
mxidColor-toggle
update-sw-main-msg
v4.11.1
v4.10.5
v4.10.4
v4.10.3
v4.10.2
v4.10.1
v4.10.0
v4.9.1
v4.9.0
v4.8.1
v4.8.0
v4.7.1
v4.7.0
v4.6.0
v4.5.1
v4.5.0
v4.4.0
v4.3.2
v4.3.0
v4.2.3
v4.2.2
v4.2.1
v4.2.0
v4.1.0
v4.0.3
v4.0.0
v3.2.0
v3.1.0
v3.0.0
v2.2.6
v2.2.5
v2.2.4
v2.2.3
v2.2.2
v2.2.1
v2.2.0
v2.1.3
v2.1.2
v2.1.1
v2.1.0
v2.0.4
v2.0.3
v2.0.2
v2.0.1
v2.0.0
v1.8.2
v1.8.1
v1.8.0
v1.7.0
v1.6.1
v1.6.0
v1.5.1
v1.5.0
v1.4.0
v1.3.2
v1.3.1
v1.3.0
v1.2.1
v1.2.0
v1.1.0
v1.0.0
Labels
Clear labels
a11y
area: appearance
area: auth-session
area: build-ci
area: calls
area: desktop
area: media
area: messaging
area: mobile
area: moderation
area: navigation
area: notifications
area: settings
area: threads
bug
dependencies
docs
duplicate
enhancement
help wanted
invalid
needs-human-review
performance
planning
priority: critical
priority: high
priority: low
priority: medium
qa
question
research
security
tech-debt
ux
wontfix
Accessibility: keyboard, screen reader, contrast, motion
Client area: appearance
Client area: auth-session
Client area: build-ci
Client area: calls
Client area: desktop
Client area: media
Client area: messaging
Client area: mobile
Client area: moderation
Client area: navigation
Client area: notifications
Client area: settings
Client area: threads
Something is not working
Third-party package versions and advisories
README / LOTUS_* docs wrong or missing
This issue or pull request already exists
New feature
Need some help
Something is wrong
Re-render storms, leaks, heavy work on hot paths
Data loss, security hole, or crash on a main path
Broken feature or serious usability problem
Minor issue or polish
Wrong behaviour in an edge case or notable degradation
Manual QA: shipped, needs a human in a real environment
More information is needed
XSS, unsafe URLs, data leaks, auth/session
Code health, dead code, fragile patterns
Usability or visual inconsistency
This won't be fixed
Milestone
No items
No Milestone
Features 2026-Q4
Projects
Clear projects
No projects
Notifications
Due Date
No due date set.
Dependencies
No dependencies set.
Reference: LotusGuild/cinny#163
Reference in New Issue
Block a user
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
Jared: "When opening the Media Gallery the message timeline zooms to the past as it loads the old images when this should not happen!"
The gallery paginates the room to find media and that pagination is landing in the live timeline the user is looking at (shared
Roomtimeline /paginateEventTimelineon the live timeline, or the gallery's fetch driving the same virtualiser). Fix: fetch media via a separateEventTimelineSet(unfiltered) or/messageswith a filter, never the live timeline; the room view must not move. Regression test: open the gallery in a long room and assert the timeline scroll position is unchanged (Playwright, E2E tier).Root cause (confirmed in code, 2026-09-17)
The gallery paginates the room's live timeline — the very object the message list is rendering:
RoomTimelinedoes not hold events; it holds a numeric windowrange: { start, end }of absolute indices into the concatenatedlinkedTimelinesevent arrays (RoomTimeline.tsx:588-604,timelineSegments). Backwards pagination in matrix-js-sdk prepends:EventTimeline.addEvent(..., toStartOfTimeline)doesthis.events.splice(0, 0, event); this.baseIndex++(node_modules/matrix-js-sdk/src/models/event-timeline.ts:382-391). So after the gallery pulls one page, indexNin the live timeline now points at an event 100 places older than before.RoomTimelinenever learns about that shift:recalibratePagination(lTimelines, timelinesEventsCount, backwards)(RoomTimeline.tsx:334-380) — it snapshots the per-timeline event counts before callingpaginateEventTimelineand shiftsrangeby the delta afterwards. The gallery's call bypasses that entirely.useLiveEventArriveignores the prepended events (data.liveEventis false for back-paginated events,RoomTimeline.tsx:388-397), so no range correction happens on the event either.On the next render of
RoomTimeline(anything — a receipt, typing, a settings atom, scroll),getItems()maps the unchangedrangethrough the shifted arrays and the viewport shows older messages: the "zoom into the past". Each gallery page (100 events, auto-fired by theIntersectionObserversentinel atMediaGallery.tsx:783) moves it another 100 events back. Two secondary symptoms fall out of the same shift:rangeAtEnd = range.end === eventsLengthbecomes false (RoomTimeline.tsx:605), so the timeline thinks it is no longer at the live end — new messages stop auto-scrolling and "Jump to latest" appears even though the user never scrolled.tabCountsandgetFilteredEvents(MediaGallery.tsx:733-745, 818-830) also read the live timeline, so the two panels are coupled in both directions.Same bug elsewhere (found by grepping
paginateEventTimeline(room.getLiveTimeline())room-settings/RoomActivityLog.tsx:366(50/page)room-settings/ExportRoomHistory.tsx:76,180MAX_EXPORT_PAGESinto the live timeline. Export a month of a busy room and the timeline behind it is thousands of events in the past (and the SDK keeps every one of those events in memory for the session).message-search/MessageSearch.tsx:96("load more history" for client-side search of encrypted rooms)RoomTimelineis normally unmounted. Leave as is; verify it's not reachable while a room is open in a split layout.room/thread/ThreadTimeline.tsx:217Fix options
A. Give the gallery its own timeline set (recommended — the Element Web FilePanel approach). Never touch the live timeline.
room.getOrCreateFilteredTimelineSet(filter)with a server-side filter{ room: { timeline: { types: ['m.room.message'], contains_url: true } } }(mx.getOrCreateFilter('FILTER_FILES_' + userId, filter)once, cache the id).paginateEventTimelineon that set's live timeline hits/messages?filter=…— the server returns only media events, so a page of 100 is 100 media items instead of 100 events of which 3 are images. Faster gallery and no coupling. The SDK API exists in 41.7 (room.ts:2004) andpaginateEventTimelinedispatches oneventTimeline.getTimelineSet(), so nothing else changes.contains_urlcan't match ciphertext, so use a private unfilterednew EventTimelineSet(room, { timelineSupport: true })(not registered with the room → doesn't receive sync events, doesn't affect anything), seed its backward token from the live timeline's earliest neighbour, paginate that, decrypt (decryptAllTimelineEvent, already used byRoomTimeline) and filter bymsgtypeclient-side — exactly what the gallery does today, just on a private array. Element does this branch too (FilePanel.tsx,fetchFileEventsServer).RoomEvent.Timelinewithdata.liveEventand prepend matching new media to the gallery's list (today it "works" only because it re-reads the live timeline on every load).tabCounts/getFilteredEventsonto the gallery's set. Redactions: keep the existingisRedacted()filter; the private set getsRoomEvent.Redactionvia the room, or re-check on render.RoomActivityLog(state events only — a filter withtypes: ['m.room.member','m.room.power_levels',…]is server-side too) andExportRoomHistory(unfiltered private set; also fixes the memory retention).Estimated size: ~150 lines in a new
src/app/hooks/useRoomMediaTimeline.ts(orutils/timelineSet.ts) + the three call-site swaps. Risk is low becauseRoomTimelineis untouched.B. Make
RoomTimelinetolerate external prepends. Subscribe toRoomEvent.TimelinewithtoStartOfTimeline === trueon the live timeline set andsetTimeline(cs => ({...cs, range: {start: cs.range.start + 1, end: cs.range.end + 1}}))per event, while suppressing it during its own pagination (which already recalibrates). Smaller diff but it changes the most sensitive component in the app, double-counts if the suppression flag is wrong, and does nothing for the memory/perf side. Not recommended.C. Band-aid: gate the sentinel so the gallery only auto-paginates while the timeline is scrolled to the bottom, or paginate in smaller pages. Doesn't fix the jump, only makes it rarer. Not recommended.
Recommendation: A, gallery first (the reported symptom), then the export and activity log as a follow-up commit using the same helper. No behaviour change is needed in
RoomTimeline.Reproduced and fixed —
d929143fReproduction (Playwright against a local Synapse,
scripts/dev-homeserver.sh+scripts/dev-seed.py, 400-message room with 40 images):Exactly the predicted mechanism: each 100-event page prepended to the live timeline shifts
RoomTimeline's index window 100 events into the past; the shift becomes visible on the next render (here a live message) and at-bottom tracking is lost.Fix = option A.
utils/detachedTimeline.ts+hooks/useRoomMediaTimeline.ts: the gallery pages through its own timeline set — a room-registered filtered set with a server-sidecontains_urlfilter in plain rooms (a page is 100 media events, so the 40 images arrived in fewer requests than before), a privateEventTimelineSetseeded from the live timeline in encrypted rooms (raw history, decrypt, filter by msgtype, live events fed in after decryption, redactions removed).RoomActivityLoguses the same helper with a type-only filter (safe when encrypted);ExportRoomHistorypages a private set so a full export no longer parks thousands of events in the live timeline.RoomTimelineis untouched.After the fix, same script: plain room — timeline stays at the bottom through gallery pages, live messages keep auto-scrolling, no "Jump to latest"; all 40 images shown. Encrypted room (200 encrypted events, 20 encrypted images sent by a separate crypto-capable client): same result, 20/20 images, 0 undecryptable, "Beginning of history" reached. Activity log (4× load more) and a full history export (plain: 408 messages incl.
img0.png; encrypted: 206, all decrypted) also leave the timeline where it was.Unit tests:
utils/detachedTimeline.test.ts(seeding, back-token copy, live timeline untouched by prepends, filter selection plain vs encrypted, per-filter caching).Known limitation (pre-existing, unchanged): media posted inside threads is not in the gallery — thread replies are partitioned out of the room timeline by the SDK. Noting it on #165.