Media Gallery / Activity Log make the room timeline drop loaded messages, and it continues after the panel is closed (regression of #163) #259

Closed
opened 2026-10-04 17:41:25 -04:00 by jared · 0 comments
Owner

What happens
Opening the Media Gallery (unencrypted rooms) or Room Settings → Activity (all rooms) makes the room timeline lose messages from the top of its rendered window:

  • On opening the panel, every matching event that is already loaded is replayed as a new arrival.
  • From then on, every new matching event is counted twice, so each one shifts the window by one extra message.

This continues for the rest of the session, even after the panel is closed. Messages you had loaded disappear from view, and scrolling back has to paginate them again.

Repro. Measured with Playwright on a local Synapse. The room had 40 messages (every 5th an m.file); I sat at the bottom while Bob sent 3 files and then a text. The lines show which message heads the rendered list:

control (no gallery):  start …file0 text1…  → after 3 new files + 1 text: still …file0 text1…
gallery opened:        start …file0 text1…  → opening drops 5 → file0
                       +NEWFILE1 → text 2   +NEWFILE2 → text 7   +NEWFILE3 → text 9   +text → file10
gallery opened+closed: same drift (text 2 → file5 → text 9 → file10)

React also logs: Cannot update a component (RoomTimeline) while rendering a different component (MediaGallery).

Why

  • Registered set created during render. useRoomMediaTimeline (src/app/hooks/useRoomMediaTimeline.ts:56) calls createDetachedTimelineSet, which calls room.getOrCreateFilteredTimelineSet(filter, { prepopulateTimeline: true, useSyncEvents: true }) (src/app/utils/detachedTimeline.ts:121), inside a useMemo during render.
  • The room re-emits it as live. The SDK registers that set on the room, caches it in filteredTimelineSets for the whole session, and re-emits its RoomEvent.Timeline through the Room with liveEvent: true. That happens:
    • for every prepopulated event (addLiveEvent), and
    • once per set for every new matching sync event.
  • Each emit moves the window. RoomTimeline's useLiveEventArrive (src/app/features/room/RoomTimeline.tsx:401) treats each emit as an appended event and does range.start/end += 1 (RoomTimeline.tsx:728-734).
  • Activity Log affects every room. It uses the same helper with filterSafeWhenEncrypted: true (src/app/features/room-settings/RoomActivityLog.tsx:342-348). So membership, name, topic, power-level and ACL events double-count in every room once it has been opened.

Fix ideas

  • In useLiveEventArrive (and other RoomEvent.Timeline listeners that assume the live timeline: roomToUnread, the notification handler, CallEmbedProvider, RoomInsights, RoomWidgetView), ignore events where data.timeline.getTimelineSet() !== room.getUnfilteredTimelineSet().
  • And/or don't register anything on the room: page a private EventTimelineSet with /messages plus the filter. Build it in an effect, not useMemo.
  • Regression test: open the gallery, have another user send media, then assert the first rendered message is unchanged. Do the same for the Activity Log with a membership change.

Related: #163 (original fix d929143f).

More context

Introduced by d929143f (2026-09-18, the #163 fix). Before it, the gallery paginated the live timeline itself (the #163 symptom); the fix moved it onto a registered filtered set, which is what causes this.

Who / where

  • Media Gallery: every room that is not encrypted (the server-side contains_url filter is only used there; encrypted rooms get a private set, which is unaffected).
  • Activity Log: every room, encrypted or not (it passes filterSafeWhenEncrypted: true).
  • Web and desktop alike; the effect lasts for the rest of the session (until reload).

Manual repro

  1. Open a busy unencrypted room with a few images; stay scrolled to the bottom.
  2. Open the Media Gallery, then close it.
  3. Have someone post 3–4 images.
  4. Actual: each new image knocks several older messages out of the rendered window; scroll up and they have to load again. Expected: the window only grows by the new messages.

Acceptance criteria

  • Opening or closing the gallery or Activity Log leaves the first rendered message unchanged.
  • A new media or membership event grows the window by exactly one item.
  • No "Cannot update a component while rendering" warning.
  • Playwright regression (both panels) added to e2e/local-homeserver.spec.ts.

Found in the October 2026 client audit (Playwright against a local Synapse + LiveKit stack, plus code reading). Code refs are at cinny 30e2dbcd, cinny-desktop 8f67ec7, element-call 3b0050e8.

**What happens** Opening the **Media Gallery** (unencrypted rooms) or **Room Settings → Activity** (all rooms) makes the room timeline lose messages from the top of its rendered window: - On opening the panel, every matching event that is already loaded is replayed as a new arrival. - From then on, **every new matching event is counted twice**, so each one shifts the window by one extra message. This continues for the rest of the session, **even after the panel is closed**. Messages you had loaded disappear from view, and scrolling back has to paginate them again. **Repro.** Measured with Playwright on a local Synapse. The room had 40 messages (every 5th an `m.file`); I sat at the bottom while Bob sent 3 files and then a text. The lines show which message heads the rendered list: ``` control (no gallery): start …file0 text1… → after 3 new files + 1 text: still …file0 text1… gallery opened: start …file0 text1… → opening drops 5 → file0 +NEWFILE1 → text 2 +NEWFILE2 → text 7 +NEWFILE3 → text 9 +text → file10 gallery opened+closed: same drift (text 2 → file5 → text 9 → file10) ``` React also logs: `Cannot update a component (RoomTimeline) while rendering a different component (MediaGallery)`. **Why** - **Registered set created during render.** `useRoomMediaTimeline` (src/app/hooks/useRoomMediaTimeline.ts:56) calls `createDetachedTimelineSet`, which calls `room.getOrCreateFilteredTimelineSet(filter, { prepopulateTimeline: true, useSyncEvents: true })` (src/app/utils/detachedTimeline.ts:121), inside a `useMemo` during render. - **The room re-emits it as live.** The SDK registers that set on the room, caches it in `filteredTimelineSets` for the whole session, and re-emits its `RoomEvent.Timeline` through the `Room` with `liveEvent: true`. That happens: - for every prepopulated event (`addLiveEvent`), and - once per set for every new matching sync event. - **Each emit moves the window.** `RoomTimeline`'s `useLiveEventArrive` (src/app/features/room/RoomTimeline.tsx:401) treats each emit as an appended event and does `range.start/end += 1` (RoomTimeline.tsx:728-734). - **Activity Log affects every room.** It uses the same helper with `filterSafeWhenEncrypted: true` (src/app/features/room-settings/RoomActivityLog.tsx:342-348). So membership, name, topic, power-level and ACL events double-count in **every** room once it has been opened. **Fix ideas** - In `useLiveEventArrive` (and other `RoomEvent.Timeline` listeners that assume the live timeline: `roomToUnread`, the notification handler, `CallEmbedProvider`, `RoomInsights`, `RoomWidgetView`), ignore events where `data.timeline.getTimelineSet() !== room.getUnfilteredTimelineSet()`. - And/or don't register anything on the room: page a private `EventTimelineSet` with `/messages` plus the filter. Build it in an effect, not `useMemo`. - **Regression test:** open the gallery, have another user send media, then assert the first rendered message is unchanged. Do the same for the Activity Log with a membership change. Related: #163 (original fix d929143f). ### More context **Introduced by** d929143f (2026-09-18, the #163 fix). Before it, the gallery paginated the live timeline itself (the #163 symptom); the fix moved it onto a *registered* filtered set, which is what causes this. **Who / where** - **Media Gallery:** every room that is **not encrypted** (the server-side `contains_url` filter is only used there; encrypted rooms get a private set, which is unaffected). - **Activity Log:** **every** room, encrypted or not (it passes `filterSafeWhenEncrypted: true`). - Web and desktop alike; the effect lasts for the rest of the session (until reload). **Manual repro** 1. Open a busy unencrypted room with a few images; stay scrolled to the bottom. 2. Open the Media Gallery, then close it. 3. Have someone post 3–4 images. 4. **Actual:** each new image knocks several older messages out of the rendered window; scroll up and they have to load again. **Expected:** the window only grows by the new messages. **Acceptance criteria** - Opening or closing the gallery or Activity Log leaves the first rendered message unchanged. - A new media or membership event grows the window by exactly one item. - No "Cannot update a component while rendering" warning. - Playwright regression (both panels) added to `e2e/local-homeserver.spec.ts`. --- _Found in the October 2026 client audit (Playwright against a local Synapse + LiveKit stack, plus code reading). Code refs are at cinny `30e2dbcd`, cinny-desktop `8f67ec7`, element-call `3b0050e8`._
jared added the bugpriority: higharea: messaging labels 2026-10-04 17:41:25 -04:00
jared closed this issue 2026-10-17 21:37:09 -04:00
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: LotusGuild/cinny#259