mirror of
https://github.com/Novaloop-AG/element-x-ios.git
synced 2026-11-03 16:10:20 +00:00
Add support for the new RoomList API (#1092)
* Add support for the new RoomList API Squashed commits: [8c2625a] Re-enable invites [339e59b5] Add automatic syncing restarts after entering the terminated state for whatever reason [b5498e96] Reimplemented room list state management and usage [8154b0cf] Fix visible rooms range setting after scrolling stops [5155c44d] Hook up initial loading idicator and session verification banner to room list service states [700a5a2e] Enable back room subscriptions, timeline listeners and unread notifications / counts [9df383bc] Enable last messages and timestamps, they don't crash on release builds [7c4da9da] Add back certain fields now that the "full room" is available [9f6534a7] Adopt interfaces to new RoomList API, untested as requests don't go to the right URL * Tweaks following code review * Bump the RustSDK to v1.0.76-alpha
This commit is contained in:
@@ -16,6 +16,7 @@
|
||||
|
||||
import Combine
|
||||
import Foundation
|
||||
import MatrixRustSDK
|
||||
|
||||
enum MockRoomSummaryProviderState {
|
||||
case loading
|
||||
@@ -25,7 +26,6 @@ enum MockRoomSummaryProviderState {
|
||||
class MockRoomSummaryProvider: RoomSummaryProviderProtocol {
|
||||
let roomListPublisher: CurrentValuePublisher<[RoomSummary], Never>
|
||||
let statePublisher: CurrentValuePublisher<RoomSummaryProviderState, Never>
|
||||
let countPublisher: CurrentValuePublisher<UInt, Never>
|
||||
|
||||
convenience init() {
|
||||
self.init(state: .loading)
|
||||
@@ -36,15 +36,16 @@ class MockRoomSummaryProvider: RoomSummaryProviderProtocol {
|
||||
case .loading:
|
||||
roomListPublisher = .init([])
|
||||
statePublisher = .init(.notLoaded)
|
||||
countPublisher = .init(0)
|
||||
case .loaded(let rooms):
|
||||
roomListPublisher = .init(rooms)
|
||||
statePublisher = .init(.fullyLoaded)
|
||||
countPublisher = .init(UInt(rooms.count))
|
||||
}
|
||||
}
|
||||
|
||||
func updateVisibleRange(_ range: Range<Int>, timelineLimit: UInt) { }
|
||||
func subscribeIfNecessary(entriesFunction: (RoomListEntriesListener) async throws -> RoomListEntriesResult,
|
||||
entriesLoadingStateFunction: (SlidingSyncListStateObserver) async throws -> RoomListEntriesLoadingStateResult) { }
|
||||
|
||||
func updateVisibleRange(_ range: Range<Int>) { }
|
||||
}
|
||||
|
||||
extension Array where Element == RoomSummary {
|
||||
|
||||
@@ -19,17 +19,22 @@ import Foundation
|
||||
import MatrixRustSDK
|
||||
|
||||
class RoomSummaryProvider: RoomSummaryProviderProtocol {
|
||||
private let slidingSyncListProxy: SlidingSyncListProxy
|
||||
private let serialDispatchQueue: DispatchQueue
|
||||
private let roomListService: RoomListProtocol
|
||||
private let eventStringBuilder: RoomEventStringBuilder
|
||||
private let name: String
|
||||
|
||||
private let serialDispatchQueue: DispatchQueue
|
||||
|
||||
private var cancellables = Set<AnyCancellable>()
|
||||
private var listUpdatesTaskHandle: TaskHandle?
|
||||
private var stateUpdatesTaskHandle: TaskHandle?
|
||||
|
||||
private let roomListSubject = CurrentValueSubject<[RoomSummary], Never>([])
|
||||
private let stateSubject = CurrentValueSubject<RoomSummaryProviderState, Never>(.notLoaded)
|
||||
private let countSubject = CurrentValueSubject<UInt, Never>(0)
|
||||
|
||||
private let diffPublisher = PassthroughSubject<RoomListEntriesUpdate, Never>()
|
||||
|
||||
var roomListPublisher: CurrentValuePublisher<[RoomSummary], Never> {
|
||||
roomListSubject.asCurrentValuePublisher()
|
||||
}
|
||||
@@ -37,10 +42,6 @@ class RoomSummaryProvider: RoomSummaryProviderProtocol {
|
||||
var statePublisher: CurrentValuePublisher<RoomSummaryProviderState, Never> {
|
||||
stateSubject.asCurrentValuePublisher()
|
||||
}
|
||||
|
||||
var countPublisher: CurrentValuePublisher<UInt, Never> {
|
||||
countSubject.asCurrentValuePublisher()
|
||||
}
|
||||
|
||||
private var rooms: [RoomSummary] = [] {
|
||||
didSet {
|
||||
@@ -48,47 +49,75 @@ class RoomSummaryProvider: RoomSummaryProviderProtocol {
|
||||
}
|
||||
}
|
||||
|
||||
init(slidingSyncListProxy: SlidingSyncListProxy, eventStringBuilder: RoomEventStringBuilder, name: String) {
|
||||
self.slidingSyncListProxy = slidingSyncListProxy
|
||||
init(roomListService: RoomListProtocol,
|
||||
eventStringBuilder: RoomEventStringBuilder,
|
||||
name: String) {
|
||||
self.roomListService = roomListService
|
||||
serialDispatchQueue = DispatchQueue(label: "io.element.elementx.roomsummaryprovider", qos: .utility)
|
||||
self.eventStringBuilder = eventStringBuilder
|
||||
self.name = name
|
||||
|
||||
rooms = slidingSyncListProxy.currentRoomsList().map { roomListEntry in
|
||||
buildSummaryForRoomListEntry(roomListEntry)
|
||||
}
|
||||
|
||||
roomListSubject.send(rooms) // didSet not called from initialisers
|
||||
|
||||
slidingSyncListProxy.statePublisher
|
||||
.map(RoomSummaryProviderState.init)
|
||||
.subscribe(stateSubject)
|
||||
.store(in: &cancellables)
|
||||
|
||||
slidingSyncListProxy.countPublisher
|
||||
.subscribe(countSubject)
|
||||
.store(in: &cancellables)
|
||||
|
||||
slidingSyncListProxy.diffPublisher
|
||||
diffPublisher
|
||||
.collect(.byTime(serialDispatchQueue, 0.025))
|
||||
.sink { [weak self] in self?.updateRoomsWithDiffs($0) }
|
||||
.store(in: &cancellables)
|
||||
}
|
||||
|
||||
func updateVisibleRange(_ range: Range<Int>, timelineLimit: UInt) {
|
||||
slidingSyncListProxy.updateVisibleRange(range, timelineLimit: timelineLimit)
|
||||
func subscribeIfNecessary(entriesFunction: (RoomListEntriesListener) async throws -> RoomListEntriesResult,
|
||||
entriesLoadingStateFunction: (SlidingSyncListStateObserver) async throws -> RoomListEntriesLoadingStateResult) async {
|
||||
guard listUpdatesTaskHandle == nil, stateUpdatesTaskHandle == nil else {
|
||||
return
|
||||
}
|
||||
|
||||
do {
|
||||
let listUpdatesSubscriptionResult = try await entriesFunction(RoomListEntriesListenerProxy { [weak self] update in
|
||||
guard let self else { return }
|
||||
MXLog.verbose("\(name): Received list update")
|
||||
diffPublisher.send(update)
|
||||
})
|
||||
|
||||
listUpdatesTaskHandle = listUpdatesSubscriptionResult.entriesStream
|
||||
|
||||
rooms = listUpdatesSubscriptionResult.entries.map { roomListEntry in
|
||||
buildSummaryForRoomListEntry(roomListEntry)
|
||||
}
|
||||
|
||||
let stateUpdatesSubscriptionResult = try await entriesLoadingStateFunction(RoomListStateObserver { [weak self] state in
|
||||
guard let self else { return }
|
||||
MXLog.info("\(name): Received state update: \(state)")
|
||||
stateSubject.send(RoomSummaryProviderState(slidingSyncState: state))
|
||||
})
|
||||
|
||||
stateSubject.send(RoomSummaryProviderState(slidingSyncState: stateUpdatesSubscriptionResult.entriesLoadingState))
|
||||
|
||||
stateUpdatesTaskHandle = stateUpdatesSubscriptionResult.entriesLoadingStateStream
|
||||
|
||||
} catch {
|
||||
MXLog.error("Failed setting up room list entry listener with error: \(error)")
|
||||
}
|
||||
}
|
||||
|
||||
func updateVisibleRange(_ range: Range<Int>) {
|
||||
Task {
|
||||
do {
|
||||
MXLog.info("\(name): Setting visible range to \(range)")
|
||||
try await roomListService.applyInput(input: .viewport(ranges: [.init(start: UInt32(range.lowerBound), endInclusive: UInt32(range.upperBound))]))
|
||||
} catch {
|
||||
MXLog.error("Failed updating visible range with error: \(error)")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Private
|
||||
|
||||
fileprivate func updateRoomsWithDiffs(_ diffs: [SlidingSyncListRoomsListDiff]) {
|
||||
fileprivate func updateRoomsWithDiffs(_ diffs: [RoomListEntriesUpdate]) {
|
||||
let span = MXLog.createSpan("\(name).process_room_list_diffs")
|
||||
span.enter()
|
||||
defer {
|
||||
span.exit()
|
||||
}
|
||||
|
||||
MXLog.info("\(name): Received \(diffs.count) diffs, current room list \(rooms.compactMap { $0.id ?? "Empty" })")
|
||||
MXLog.verbose("\(name): Received \(diffs.count) diffs, current room list \(rooms.compactMap { $0.id ?? "Empty" })")
|
||||
|
||||
rooms = diffs
|
||||
.reduce(rooms) { currentItems, diff in
|
||||
@@ -107,11 +136,11 @@ class RoomSummaryProvider: RoomSummaryProviderProtocol {
|
||||
|
||||
detectDuplicatesInRoomList(rooms)
|
||||
|
||||
MXLog.info("\(name): Finished applying \(diffs.count) diffs, new room list \(rooms.compactMap { $0.id ?? "Empty" })")
|
||||
MXLog.verbose("\(name): Finished applying \(diffs.count) diffs, new room list \(rooms.compactMap { $0.id ?? "Empty" })")
|
||||
}
|
||||
|
||||
private func buildRoomSummaryForIdentifier(_ identifier: String, invalidated: Bool) -> RoomSummary {
|
||||
guard let room = try? slidingSyncListProxy.roomForIdentifier(identifier) else {
|
||||
private func buildRoomSummaryForIdentifier(_ identifier: String) -> RoomSummary {
|
||||
guard let roomListItem = try? roomListService.room(roomId: identifier) else {
|
||||
MXLog.error("\(name): Failed finding room with id: \(identifier)")
|
||||
return .empty
|
||||
}
|
||||
@@ -119,30 +148,24 @@ class RoomSummaryProvider: RoomSummaryProviderProtocol {
|
||||
var attributedLastMessage: AttributedString?
|
||||
var lastMessageFormattedTimestamp: String?
|
||||
|
||||
// Dispatch onto another queue otherwise the rust method latestRoomMessage crashes.
|
||||
// This will be fixed when we get async uniffi support.
|
||||
DispatchQueue.global(qos: .userInitiated).sync {
|
||||
if let latestRoomMessage = room.latestRoomMessage() {
|
||||
let lastMessage = EventTimelineItemProxy(item: latestRoomMessage)
|
||||
lastMessageFormattedTimestamp = lastMessage.timestamp.formattedMinimal()
|
||||
attributedLastMessage = eventStringBuilder.buildAttributedString(for: lastMessage)
|
||||
}
|
||||
if let latestRoomMessage = roomListItem.latestEvent() {
|
||||
let lastMessage = EventTimelineItemProxy(item: latestRoomMessage)
|
||||
lastMessageFormattedTimestamp = lastMessage.timestamp.formattedMinimal()
|
||||
attributedLastMessage = eventStringBuilder.buildAttributedString(for: lastMessage)
|
||||
}
|
||||
|
||||
let fullRoom = room.fullRoom()
|
||||
let avatarURL = fullRoom?.avatarUrl().flatMap(URL.init(string:))
|
||||
let canonicalAlias = fullRoom?.canonicalAlias()
|
||||
|
||||
let details = RoomSummaryDetails(id: room.roomId(),
|
||||
name: room.name() ?? room.roomId(),
|
||||
isDirect: fullRoom?.isDirect() ?? room.isDm() ?? false,
|
||||
avatarURL: avatarURL,
|
||||
let room = roomListItem.fullRoom()
|
||||
|
||||
let details = RoomSummaryDetails(id: roomListItem.id(),
|
||||
name: roomListItem.name() ?? room.id(),
|
||||
isDirect: room.isDirect(),
|
||||
avatarURL: room.avatarUrl().flatMap(URL.init(string:)),
|
||||
lastMessage: attributedLastMessage,
|
||||
lastMessageFormattedTimestamp: lastMessageFormattedTimestamp,
|
||||
unreadNotificationCount: UInt(room.unreadNotifications().notificationCount()),
|
||||
canonicalAlias: canonicalAlias)
|
||||
|
||||
return invalidated ? .invalidated(details: details) : .filled(details: details)
|
||||
unreadNotificationCount: UInt(roomListItem.unreadNotifications().notificationCount()),
|
||||
canonicalAlias: room.canonicalAlias())
|
||||
|
||||
return .filled(details: details)
|
||||
}
|
||||
|
||||
private func buildSummaryForRoomListEntry(_ entry: RoomListEntry) -> RoomSummary {
|
||||
@@ -150,48 +173,48 @@ class RoomSummaryProvider: RoomSummaryProviderProtocol {
|
||||
case .empty:
|
||||
return .empty
|
||||
case .filled(let roomId):
|
||||
return buildRoomSummaryForIdentifier(roomId, invalidated: false)
|
||||
return buildRoomSummaryForIdentifier(roomId)
|
||||
case .invalidated(let roomId):
|
||||
return buildRoomSummaryForIdentifier(roomId, invalidated: true)
|
||||
return buildRoomSummaryForIdentifier(roomId)
|
||||
}
|
||||
}
|
||||
|
||||
// swiftlint:disable:next cyclomatic_complexity function_body_length
|
||||
private func buildDiff(from diff: SlidingSyncListRoomsListDiff, on rooms: [RoomSummary]) -> CollectionDifference<RoomSummary>? {
|
||||
private func buildDiff(from diff: RoomListEntriesUpdate, on rooms: [RoomSummary]) -> CollectionDifference<RoomSummary>? {
|
||||
var changes = [CollectionDifference<RoomSummary>.Change]()
|
||||
|
||||
switch diff {
|
||||
case .pushFront(let value):
|
||||
MXLog.info("\(name): Push Front \(value.debugIdentifier)")
|
||||
MXLog.verbose("\(name): Push Front \(value.debugIdentifier)")
|
||||
let summary = buildSummaryForRoomListEntry(value)
|
||||
changes.append(.insert(offset: 0, element: summary, associatedWith: nil))
|
||||
case .pushBack(let value):
|
||||
MXLog.info("\(name): Push Back \(value.debugIdentifier)")
|
||||
MXLog.verbose("\(name): Push Back \(value.debugIdentifier)")
|
||||
let summary = buildSummaryForRoomListEntry(value)
|
||||
changes.append(.insert(offset: rooms.count, element: summary, associatedWith: nil))
|
||||
case .append(values: let values):
|
||||
let debugIdentifiers = values.map(\.debugIdentifier)
|
||||
MXLog.info("\(name): Append \(debugIdentifiers)")
|
||||
MXLog.verbose("\(name): Append \(debugIdentifiers)")
|
||||
for (index, value) in values.enumerated() {
|
||||
let summary = buildSummaryForRoomListEntry(value)
|
||||
changes.append(.insert(offset: rooms.count + index, element: summary, associatedWith: nil))
|
||||
}
|
||||
case .set(let index, let value):
|
||||
MXLog.info("\(name): Update \(value.debugIdentifier) at \(index)")
|
||||
MXLog.verbose("\(name): Update \(value.debugIdentifier) at \(index)")
|
||||
let summary = buildSummaryForRoomListEntry(value)
|
||||
changes.append(.remove(offset: Int(index), element: summary, associatedWith: nil))
|
||||
changes.append(.insert(offset: Int(index), element: summary, associatedWith: nil))
|
||||
case .insert(let index, let value):
|
||||
MXLog.info("\(name): Insert at \(value.debugIdentifier) at \(index)")
|
||||
MXLog.verbose("\(name): Insert at \(value.debugIdentifier) at \(index)")
|
||||
let summary = buildSummaryForRoomListEntry(value)
|
||||
changes.append(.insert(offset: Int(index), element: summary, associatedWith: nil))
|
||||
case .remove(let index):
|
||||
let summary = rooms[Int(index)]
|
||||
MXLog.info("\(name): Remove \(summary.id ?? "") from \(index)")
|
||||
MXLog.verbose("\(name): Remove \(summary.id ?? "") from \(index)")
|
||||
changes.append(.remove(offset: Int(index), element: summary, associatedWith: nil))
|
||||
case .reset(let values):
|
||||
let debugIdentifiers = values.map(\.debugIdentifier)
|
||||
MXLog.info("\(name): Replace all items with \(debugIdentifiers)")
|
||||
MXLog.verbose("\(name): Replace all items with \(debugIdentifiers)")
|
||||
for (index, summary) in rooms.enumerated() {
|
||||
changes.append(.remove(offset: index, element: summary, associatedWith: nil))
|
||||
}
|
||||
@@ -200,16 +223,16 @@ class RoomSummaryProvider: RoomSummaryProviderProtocol {
|
||||
changes.append(.insert(offset: index, element: buildSummaryForRoomListEntry(value), associatedWith: nil))
|
||||
}
|
||||
case .clear:
|
||||
MXLog.info("\(name): Clear all items")
|
||||
MXLog.verbose("\(name): Clear all items")
|
||||
for (index, value) in rooms.enumerated() {
|
||||
changes.append(.remove(offset: index, element: value, associatedWith: nil))
|
||||
}
|
||||
case .popFront:
|
||||
MXLog.info("\(name): Pop Front")
|
||||
MXLog.verbose("\(name): Pop Front")
|
||||
let summary = rooms[0]
|
||||
changes.append(.remove(offset: 0, element: summary, associatedWith: nil))
|
||||
case .popBack:
|
||||
MXLog.info("\(name): Pop Back")
|
||||
MXLog.verbose("\(name): Pop Back")
|
||||
guard let value = rooms.last else {
|
||||
fatalError()
|
||||
}
|
||||
@@ -241,7 +264,7 @@ class RoomSummaryProvider: RoomSummaryProviderProtocol {
|
||||
}
|
||||
|
||||
extension RoomSummaryProviderState {
|
||||
init(slidingSyncState: SlidingSyncState) {
|
||||
init(slidingSyncState: SlidingSyncListLoadingState) {
|
||||
switch slidingSyncState {
|
||||
case .notLoaded:
|
||||
self = .notLoaded
|
||||
@@ -276,3 +299,27 @@ extension MatrixRustSDK.RoomListEntry {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private class RoomListEntriesListenerProxy: RoomListEntriesListener {
|
||||
private let onUpdateClosure: (RoomListEntriesUpdate) -> Void
|
||||
|
||||
init(_ onUpdateClosure: @escaping (RoomListEntriesUpdate) -> Void) {
|
||||
self.onUpdateClosure = onUpdateClosure
|
||||
}
|
||||
|
||||
func onUpdate(roomEntriesUpdate: RoomListEntriesUpdate) {
|
||||
onUpdateClosure(roomEntriesUpdate)
|
||||
}
|
||||
}
|
||||
|
||||
private class RoomListStateObserver: SlidingSyncListStateObserver {
|
||||
private let onUpdateClosure: (SlidingSyncListLoadingState) -> Void
|
||||
|
||||
init(_ onUpdateClosure: @escaping (SlidingSyncListLoadingState) -> Void) {
|
||||
self.onUpdateClosure = onUpdateClosure
|
||||
}
|
||||
|
||||
func didReceiveUpdate(newState: SlidingSyncListLoadingState) {
|
||||
onUpdateClosure(newState)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -28,13 +28,12 @@ enum RoomSummaryProviderState {
|
||||
enum RoomSummary: CustomStringConvertible {
|
||||
case empty
|
||||
case filled(details: RoomSummaryDetails)
|
||||
case invalidated(details: RoomSummaryDetails)
|
||||
|
||||
var id: String? {
|
||||
switch self {
|
||||
case .empty:
|
||||
return nil
|
||||
case .invalidated(let details), .filled(let details):
|
||||
case .filled(let details):
|
||||
return details.id
|
||||
}
|
||||
}
|
||||
@@ -43,7 +42,7 @@ enum RoomSummary: CustomStringConvertible {
|
||||
switch self {
|
||||
case .empty:
|
||||
return nil
|
||||
case .invalidated(let details), .filled(let details):
|
||||
case .filled(let details):
|
||||
return details.name
|
||||
}
|
||||
}
|
||||
@@ -52,8 +51,6 @@ enum RoomSummary: CustomStringConvertible {
|
||||
switch self {
|
||||
case .empty:
|
||||
return "\(String(describing: Self.self)): Empty"
|
||||
case .invalidated(let details):
|
||||
return "\(String(describing: Self.self)): Invalidated(\(details.id))"
|
||||
case .filled(let details):
|
||||
return "\(String(describing: Self.self)): Filled(\(details.id))"
|
||||
}
|
||||
@@ -67,8 +64,10 @@ protocol RoomSummaryProviderProtocol {
|
||||
/// Publishes the current state the summary provider is finding itself in
|
||||
var statePublisher: CurrentValuePublisher<RoomSummaryProviderState, Never> { get }
|
||||
|
||||
/// Publishes the total number of rooms
|
||||
var countPublisher: CurrentValuePublisher<UInt, Never> { get }
|
||||
|
||||
func updateVisibleRange(_ range: Range<Int>, timelineLimit: UInt)
|
||||
/// A separate subscription method is needed instead of running this in the constructor because the invites list is added later on the Rust side.
|
||||
/// Wanted to be able to build the InvitesSummaryProvider directly instead of having to inform the HomeScreenViewModel about it later
|
||||
func subscribeIfNecessary(entriesFunction: (RoomListEntriesListener) async throws -> RoomListEntriesResult,
|
||||
entriesLoadingStateFunction: (SlidingSyncListStateObserver) async throws -> RoomListEntriesLoadingStateResult) async
|
||||
|
||||
func updateVisibleRange(_ range: Range<Int>)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user