From 488b9a144505f1e417a4e0159485e3bd9114df7c Mon Sep 17 00:00:00 2001 From: Andrew Beresford Date: Tue, 10 Mar 2026 15:22:21 +0000 Subject: [PATCH] Reuse SidebarRoom instances on .set/.reset to prevent room list flicker 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. --- Mactrix/Models/LiveRoom.swift | 5 ++--- Mactrix/Models/MatrixClient+Listeners.swift | 16 ++++++++++++-- Mactrix/Models/SidebarRoom.swift | 24 +++++++++++++++------ 3 files changed, 33 insertions(+), 12 deletions(-) diff --git a/Mactrix/Models/LiveRoom.swift b/Mactrix/Models/LiveRoom.swift index f28ca66..4ddceb7 100644 --- a/Mactrix/Models/LiveRoom.swift +++ b/Mactrix/Models/LiveRoom.swift @@ -12,9 +12,7 @@ public final class LiveRoom: Identifiable { @ObservationIgnored private var typingHandle: TaskHandle? - public nonisolated var room: MatrixRustSDK.Room { - sidebarRoom.room - } + public let room: MatrixRustSDK.Room public var roomInfo: MatrixRustSDK.RoomInfo? { sidebarRoom.roomInfo @@ -26,6 +24,7 @@ public final class LiveRoom: Identifiable { public init(sidebarRoom: SidebarRoom) { self.sidebarRoom = sidebarRoom + self.room = sidebarRoom.room startListening() diff --git a/Mactrix/Models/MatrixClient+Listeners.swift b/Mactrix/Models/MatrixClient+Listeners.swift index 53bc71d..204ddb9 100644 --- a/Mactrix/Models/MatrixClient+Listeners.swift +++ b/Mactrix/Models/MatrixClient+Listeners.swift @@ -21,13 +21,25 @@ extension MatrixClient { case let .insert(index, room): self.rooms.insert(SidebarRoom(room: room), at: Int(index)) case let .set(index, room): - self.rooms[Int(index)] = SidebarRoom(room: room) + let existing = self.rooms[Int(index)] + if existing.id == room.id() { + existing.updateRoom(room) + } else { + self.rooms[Int(index)] = SidebarRoom(room: room) + } case let .remove(index): self.rooms.remove(at: Int(index)) case let .truncate(length): self.rooms.removeSubrange(Int(length) ..< self.rooms.count) case let .reset(values: values): - self.rooms = values.map(SidebarRoom.init(room:)) + let existingById = Dictionary(self.rooms.map { ($0.id, $0) }, uniquingKeysWith: { first, _ in first }) + self.rooms = values.map { room in + if let existing = existingById[room.id()] { + existing.updateRoom(room) + return existing + } + return SidebarRoom(room: room) + } } } } diff --git a/Mactrix/Models/SidebarRoom.swift b/Mactrix/Models/SidebarRoom.swift index 2456f31..c3b4b49 100644 --- a/Mactrix/Models/SidebarRoom.swift +++ b/Mactrix/Models/SidebarRoom.swift @@ -5,16 +5,15 @@ import OSLog @MainActor @Observable public final class SidebarRoom: Identifiable { - public let room: MatrixRustSDK.Room + public let id: String + public private(set) var room: MatrixRustSDK.Room public var roomInfo: RoomInfo? @ObservationIgnored private var roomInfoHandle: TaskHandle? - - public nonisolated var id: String { - room.id() - } + @ObservationIgnored private var listenerTask: Task? public init(room: MatrixRustSDK.Room) { + self.id = room.id() self.room = room Task { @@ -28,13 +27,24 @@ public final class SidebarRoom: Identifiable { } } + /// Updates the underlying room reference without replacing this instance. + /// Preserves object identity and loaded roomInfo while ensuring the room + /// object stays current. Re-subscribes to room info updates on the new reference. + public func updateRoom(_ newRoom: MatrixRustSDK.Room) { + assert(id == newRoom.id()) + room = newRoom + listenerTask?.cancel() + roomInfoHandle = nil + listenToRoomInfo() + } + private func listenToRoomInfo() { let listener = AsyncSDKListener() roomInfoHandle = room.subscribeToRoomInfoUpdates(listener: listener) - Task { [weak self] in + listenerTask = Task { [weak self] in for await roomInfo in listener._throttle(for: .milliseconds(500)) { - guard let self else { break } + guard let self, !Task.isCancelled else { break } self.roomInfo = roomInfo } }