Skip to content

Fix room list flickering when selecting rooms#64

Open
beezly wants to merge 1 commit intoviktorstrate:mainfrom
beezly:fix/room-list-flicker
Open

Fix room list flickering when selecting rooms#64
beezly wants to merge 1 commit intoviktorstrate:mainfrom
beezly:fix/room-list-flicker

Conversation

@beezly
Copy link
Contributor

@beezly beezly commented Mar 9, 2026

Closes #27

Problem

Clicking a room in the sidebar causes all visible room rows to briefly flicker. This only happens the first time a room is selected — once loaded, subsequent clicks to that room are flicker-free.

Root Cause

When a room is clicked, the read marker moves and the SDK emits .set or .reset room list updates. updateRoomEntries handled these by creating new SidebarRoom objects:

case let .set(index, room):
    self.rooms[Int(index)] = SidebarRoom(room: room)

SidebarRoom.init always starts with roomInfo = nil and fires an async task to fetch it. This means every affected row briefly renders without roomInfo — losing unread badges and counts — before the fetch completes. That blank frame is the visible flicker.

This also explains the behaviour in #27 where favourites appear to temporarily drop to the Rooms section — the new SidebarRoom starts with roomInfo = nil, so isFavourite is momentarily false, causing the room to be filtered into the wrong section until roomInfo is fetched.

Fix

Reuse existing SidebarRoom objects by room ID rather than replacing them. Since SidebarRoom is already subscribed to live roomInfo updates via subscribeToRoomInfoUpdates, there's no need to replace the object when the SDK emits a .set for the same room ID.

  • .set: skip replacement if the room ID at that index hasn't changed
  • .reset: build a lookup of existing objects by ID and reuse them, only creating new SidebarRoom instances for genuinely new rooms

@viktorstrate
Copy link
Owner

Awesome fix, this is great both for performance and for fixing the flickerings. However, I am worried that just ignoring updates might be too optimistic, say that a .set update changes any properties on the internal room attribute, such as the display name of the room, then this fix will simply ignore it (correct me if I'm wrong).

I was thinking that we can fix this by adding an updateRoom method to the SidebarRoom class.

public func updateRoom(_ room: MatrixRustSDK.Room) {
      assert(id == room.id())
      self.room = room
  }

When sketching this, I see that we need to make id its own attribute since it needs to be thread safe and changing room from let to var breaks that.

And then calling that instead of doing nothing, if the room already exists.

@beezly beezly force-pushed the fix/room-list-flicker branch 2 times, most recently from 5799b23 to f71e58c Compare March 10, 2026 15:10
When .set or .reset diffs arrive, reuse existing SidebarRoom instances
and call updateRoom() to refresh the underlying room reference and
re-subscribe. This preserves object identity so SwiftUI doesn't treat
the update as a remove+insert, and retains already-loaded roomInfo.

LiveRoom now stores its own let room reference so that SidebarRoom.room
can be var without needing nonisolated(unsafe).

Also cancel the old listener Task on updateRoom to prevent stale writes,
and use uniquingKeysWith for defensive handling of duplicate room IDs.
@beezly beezly force-pushed the fix/room-list-flicker branch from f71e58c to 488b9a1 Compare March 10, 2026 15:23
@beezly
Copy link
Contributor Author

beezly commented Mar 10, 2026

Thanks for the suggestion! I've reworked the fix to use updateRoom() as you described.

The implementation:

  • id is stored as its own let property
  • room is private(set) var, with updateRoom() to refresh the reference and re-subscribe
  • For .set, existing instances are reused when the ID matches; for .reset, a lookup dictionary maps existing instances by ID

To allow room to be var without concurrency issues, LiveRoom now stores its own let room reference captured at init - it doesn't need to follow sidebar updates since it represents the actively-viewed room with its own timeline and state. This avoids needing nonisolated(unsafe) on SidebarRoom.room (which would otherwise be required because LiveRoom has nonisolated computed properties that access it for the Models.Room protocol conformance).

A couple of additional defensive changes:

  • The old listener Task is explicitly cancelled in updateRoom() to prevent stale writes from an in-flight subscription racing with the new one
  • The .reset dictionary uses uniquingKeysWith rather than uniqueKeysWithValues to handle any unexpected duplicate room IDs - it shouldn't happen, but if it ever does it won't cause problems.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Room list with favourites flickers

2 participants