Use local room list sorting from Rust. (#2978)

* Switch to the new local room list sorting API

* Get rid of the RoomSummary enum, replace it by the inner RoomSummaryDetails

* And finally, rename RoomSummaryDetails to RoomSummary

* Fix a bunch of warnings

* Small tidy up post rebase/review.

- Remove unused invalidated property.
- Rename some RoomSummary instances to summary instead of details.
- Fix tests with missing roomListItem's.

* Update snapshots

---------

Co-authored-by: Doug <douglase@element.io>
This commit is contained in:
Stefan Ceriu
2024-07-05 12:10:53 +03:00
committed by GitHub
parent c024702d50
commit d71c8c4e83
26 changed files with 556 additions and 608 deletions
@@ -17,7 +17,9 @@
import Foundation
import MatrixRustSDK
struct RoomSummaryDetails {
struct RoomSummary {
let roomListItem: RoomListItem
let id: String
let isInvite: Bool
@@ -46,9 +48,9 @@ struct RoomSummaryDetails {
var isMuted: Bool { notificationMode == .mute }
}
extension RoomSummaryDetails: CustomStringConvertible {
extension RoomSummary: CustomStringConvertible {
var description: String { """
RoomSummaryDetails: - id: \(id) \
RoomSummary: - id: \(id) \
- isDirect: \(isDirect) \
- unreadMessagesCount: \(unreadMessagesCount) \
- unreadMentionsCount: \(unreadMentionsCount) \
@@ -58,8 +60,9 @@ extension RoomSummaryDetails: CustomStringConvertible {
}
}
extension RoomSummaryDetails {
init(id: String, settingsMode: RoomNotificationModeProxy, hasUnreadMessages: Bool, hasUnreadMentions: Bool, hasUnreadNotifications: Bool) {
extension RoomSummary {
init(roomListItem: RoomListItem, id: String, settingsMode: RoomNotificationModeProxy, hasUnreadMessages: Bool, hasUnreadMentions: Bool, hasUnreadNotifications: Bool) {
self.roomListItem = roomListItem
self.id = id
let string = "\(settingsMode) - messages: \(hasUnreadMessages) - mentions: \(hasUnreadMentions) - notifications: \(hasUnreadNotifications)"
name = string
@@ -35,7 +35,6 @@ class RoomSummaryProvider: RoomSummaryProviderProtocol {
private var cancellables = Set<AnyCancellable>()
private var listUpdatesSubscriptionResult: RoomListEntriesWithDynamicAdaptersResult?
private var listUpdatesTaskHandle: TaskHandle?
private var stateUpdatesTaskHandle: TaskHandle?
private let roomListSubject = CurrentValueSubject<[RoomSummary], Never>([])
@@ -85,7 +84,7 @@ class RoomSummaryProvider: RoomSummaryProviderProtocol {
}
func setRoomList(_ roomList: RoomList) {
guard listUpdatesTaskHandle == nil, stateUpdatesTaskHandle == nil else {
guard stateUpdatesTaskHandle == nil else {
return
}
@@ -101,8 +100,6 @@ class RoomSummaryProvider: RoomSummaryProviderProtocol {
// Forces the listener above to be called with the current state
setFilter(.all(filters: []))
listUpdatesTaskHandle = listUpdatesSubscriptionResult?.entriesStream
let stateUpdatesSubscriptionResult = try roomList.loadingState(listener: RoomListStateObserver { [weak self] state in
guard let self else { return }
MXLog.info("\(name): Received state update: \(state)")
@@ -119,48 +116,31 @@ class RoomSummaryProvider: RoomSummaryProviderProtocol {
func updateVisibleRange(_ range: Range<Int>) {
if range.upperBound >= rooms.count {
listUpdatesSubscriptionResult?.controller.addOnePage()
listUpdatesSubscriptionResult?.controller().addOnePage()
} else if range.lowerBound == 0 {
listUpdatesSubscriptionResult?.controller.resetToOnePage()
listUpdatesSubscriptionResult?.controller().resetToOnePage()
}
guard shouldUpdateVisibleRange else {
return
}
Task {
do {
// The scroll view content size based visible range calculations might create large ranges
// This is just a safety check to not overload the backend
var upperBound = range.upperBound
if range.upperBound - range.lowerBound > SlidingSyncConstants.maximumVisibleRangeSize {
upperBound = range.lowerBound + SlidingSyncConstants.maximumVisibleRangeSize
}
MXLog.info("\(name): Setting visible range to \(range.lowerBound)...\(upperBound)")
try await roomListService.applyInput(input: .viewport(ranges: [.init(start: UInt32(range.lowerBound), endInclusive: UInt32(upperBound))]))
} catch {
MXLog.error("Failed updating visible range with error: \(error)")
}
}
}
func setFilter(_ filter: RoomSummaryProviderFilter) {
switch filter {
case .excludeAll:
_ = listUpdatesSubscriptionResult?.controller.setFilter(kind: .none)
_ = listUpdatesSubscriptionResult?.controller().setFilter(kind: .none)
case let .search(query):
let filters: [RoomListEntriesDynamicFilterKind] = if appSettings.fuzzyRoomListSearchEnabled {
[.fuzzyMatchRoomName(pattern: query), .nonLeft]
} else {
[.normalizedMatchRoomName(pattern: query), .nonLeft]
}
_ = listUpdatesSubscriptionResult?.controller.setFilter(kind: .all(filters: filters))
_ = listUpdatesSubscriptionResult?.controller().setFilter(kind: .all(filters: filters))
case let .all(filters):
var filters = filters.map(\.rustFilter)
filters.append(.nonLeft)
_ = listUpdatesSubscriptionResult?.controller.setFilter(kind: .all(filters: filters))
_ = listUpdatesSubscriptionResult?.controller().setFilter(kind: .all(filters: filters))
}
}
@@ -175,13 +155,13 @@ class RoomSummaryProvider: RoomSummaryProviderProtocol {
MXLog.info("Started processing room list diffs")
MXLog.verbose("\(name): Received \(diffs.count) diffs, current room list \(rooms.compactMap { $0.id ?? "Empty" })")
MXLog.verbose("\(name): Received \(diffs.count) diffs, current room list \(rooms.map(\.id))")
rooms = diffs.reduce(rooms) { currentItems, diff in
processDiff(diff, on: currentItems)
}
MXLog.verbose("\(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.map(\.id))")
MXLog.info("Finished processing room list diffs")
}
@@ -200,18 +180,17 @@ class RoomSummaryProvider: RoomSummaryProviderProtocol {
return updatedItems
}
private func fetchRoomDetails(roomID: String) -> (roomInfo: RoomInfo?, latestEvent: EventTimelineItem?) {
private func fetchRoomDetails(from roomListItem: RoomListItem) -> (roomInfo: RoomInfo?, latestEvent: EventTimelineItem?) {
class FetchResult {
var roomInfo: RoomInfo?
var latestEvent: EventTimelineItem?
}
let semaphore = DispatchSemaphore(value: 0)
let result = FetchResult()
Task {
do {
let roomListItem = try roomListService.room(roomId: roomID)
result.latestEvent = await roomListItem.latestEvent()
result.roomInfo = try await roomListItem.roomInfo()
} catch {
@@ -222,12 +201,12 @@ class RoomSummaryProvider: RoomSummaryProviderProtocol {
semaphore.wait()
return (result.roomInfo, result.latestEvent)
}
private func buildRoomSummaryForIdentifier(_ identifier: String, invalidated: Bool) -> RoomSummary {
let roomDetails = fetchRoomDetails(roomID: identifier)
private func buildRoomSummary(from roomListItem: RoomListItem) -> RoomSummary {
let roomDetails = fetchRoomDetails(from: roomListItem)
guard let roomInfo = roomDetails.roomInfo else {
return .empty
fatalError("Missing room info for \(roomListItem.id())")
}
var attributedLastMessage: AttributedString?
@@ -246,47 +225,24 @@ class RoomSummaryProvider: RoomSummaryProviderProtocol {
let notificationMode = roomInfo.userDefinedNotificationMode.flatMap { RoomNotificationModeProxy.from(roomNotificationMode: $0) }
let details = RoomSummaryDetails(id: roomInfo.id,
isInvite: roomInfo.membership == .invited,
inviter: inviterProxy,
name: roomInfo.displayName ?? roomInfo.id,
isDirect: roomInfo.isDirect,
avatarURL: roomInfo.avatarUrl.flatMap(URL.init(string:)),
heroes: roomInfo.heroes.map(UserProfileProxy.init),
lastMessage: attributedLastMessage,
lastMessageFormattedTimestamp: lastMessageFormattedTimestamp,
unreadMessagesCount: UInt(roomInfo.numUnreadMessages),
unreadMentionsCount: UInt(roomInfo.numUnreadMentions),
unreadNotificationsCount: UInt(roomInfo.numUnreadNotifications),
notificationMode: notificationMode,
canonicalAlias: roomInfo.canonicalAlias,
hasOngoingCall: roomInfo.hasRoomCall,
isMarkedUnread: roomInfo.isMarkedUnread,
isFavourite: roomInfo.isFavourite)
return invalidated ? .invalidated(details: details) : .filled(details: details)
}
private func buildSummaryForRoomListEntry(_ entry: RoomListEntry) -> RoomSummary {
switch entry {
case .empty:
return .empty
case .filled(let roomId):
return buildRoomSummaryForIdentifier(roomId, invalidated: false)
case .invalidated(let roomId):
guard let cachedRoom = rooms.first(where: { $0.id == roomId }) else {
return buildRoomSummaryForIdentifier(roomId, invalidated: true)
}
switch cachedRoom {
case .empty:
return .empty
case .filled(let details):
return .invalidated(details: details)
case .invalidated:
return cachedRoom
}
}
return RoomSummary(roomListItem: roomListItem,
id: roomInfo.id,
isInvite: roomInfo.membership == .invited,
inviter: inviterProxy,
name: roomInfo.displayName ?? roomInfo.id,
isDirect: roomInfo.isDirect,
avatarURL: roomInfo.avatarUrl.flatMap(URL.init(string:)),
heroes: roomInfo.heroes.map(UserProfileProxy.init),
lastMessage: attributedLastMessage,
lastMessageFormattedTimestamp: lastMessageFormattedTimestamp,
unreadMessagesCount: UInt(roomInfo.numUnreadMessages),
unreadMentionsCount: UInt(roomInfo.numUnreadMentions),
unreadNotificationsCount: UInt(roomInfo.numUnreadNotifications),
notificationMode: notificationMode,
canonicalAlias: roomInfo.canonicalAlias,
hasOngoingCall: roomInfo.hasRoomCall,
isMarkedUnread: roomInfo.isMarkedUnread,
isFavourite: roomInfo.isFavourite)
}
private func buildDiff(from diff: RoomListEntriesUpdate, on rooms: [RoomSummary]) -> CollectionDifference<RoomSummary>? {
@@ -294,10 +250,10 @@ class RoomSummaryProvider: RoomSummaryProviderProtocol {
switch diff {
case .append(let values):
let debugIdentifiers = values.map(\.debugIdentifier)
let debugIdentifiers = values.map { $0.id() }
MXLog.verbose("\(name): Append \(debugIdentifiers)")
for (index, value) in values.enumerated() {
let summary = buildSummaryForRoomListEntry(value)
let summary = buildRoomSummary(from: value)
changes.append(.insert(offset: rooms.count + index, element: summary, associatedWith: nil))
}
case .clear:
@@ -306,8 +262,8 @@ class RoomSummaryProvider: RoomSummaryProviderProtocol {
changes.append(.remove(offset: index, element: value, associatedWith: nil))
}
case .insert(let index, let value):
MXLog.verbose("\(name): Insert at \(value.debugIdentifier) at \(index)")
let summary = buildSummaryForRoomListEntry(value)
MXLog.verbose("\(name): Insert at \(value.id()) at \(index)")
let summary = buildRoomSummary(from: value)
changes.append(.insert(offset: Int(index), element: summary, associatedWith: nil))
case .popBack:
MXLog.verbose("\(name): Pop Back")
@@ -321,30 +277,30 @@ class RoomSummaryProvider: RoomSummaryProviderProtocol {
let summary = rooms[0]
changes.append(.remove(offset: 0, element: summary, associatedWith: nil))
case .pushBack(let value):
MXLog.verbose("\(name): Push Back \(value.debugIdentifier)")
let summary = buildSummaryForRoomListEntry(value)
MXLog.verbose("\(name): Push Back \(value.id())")
let summary = buildRoomSummary(from: value)
changes.append(.insert(offset: rooms.count, element: summary, associatedWith: nil))
case .pushFront(let value):
MXLog.verbose("\(name): Push Front \(value.debugIdentifier)")
let summary = buildSummaryForRoomListEntry(value)
MXLog.verbose("\(name): Push Front \(value.id())")
let summary = buildRoomSummary(from: value)
changes.append(.insert(offset: 0, element: summary, associatedWith: nil))
case .remove(let index):
let summary = rooms[Int(index)]
MXLog.verbose("\(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)
let debugIdentifiers = values.map { $0.id() }
MXLog.verbose("\(name): Replace all items with \(debugIdentifiers)")
for (index, summary) in rooms.enumerated() {
changes.append(.remove(offset: index, element: summary, associatedWith: nil))
}
for (index, value) in values.enumerated() {
changes.append(.insert(offset: index, element: buildSummaryForRoomListEntry(value), associatedWith: nil))
changes.append(.insert(offset: index, element: buildRoomSummary(from: value), associatedWith: nil))
}
case .set(let index, let value):
MXLog.verbose("\(name): Update \(value.debugIdentifier) at \(index)")
let summary = buildSummaryForRoomListEntry(value)
MXLog.verbose("\(name): Update \(value.id()) at \(index)")
let summary = buildRoomSummary(from: value)
changes.append(.remove(offset: Int(index), element: summary, associatedWith: nil))
changes.append(.insert(offset: Int(index), element: summary, associatedWith: nil))
case .truncate(let length):
@@ -384,14 +340,7 @@ class RoomSummaryProvider: RoomSummaryProviderProtocol {
MXLog.info("\(name): Rebuilding room summaries for \(rooms.count) rooms")
rooms = rooms.map {
switch $0 {
case .empty:
return $0
case .filled(let details):
return self.buildRoomSummaryForIdentifier(details.id, invalidated: false)
case .invalidated(let details):
return self.buildRoomSummaryForIdentifier(details.id, invalidated: true)
}
self.buildRoomSummary(from: $0.roomListItem)
}
MXLog.info("\(name): Finished rebuilding room summaries (\(rooms.count) rooms)")
@@ -409,19 +358,6 @@ extension RoomSummaryProviderState {
}
}
extension MatrixRustSDK.RoomListEntry {
var debugIdentifier: String {
switch self {
case .empty:
return "Empty"
case .invalidated(let roomId):
return "Invalidated(\(roomId))"
case .filled(let roomId):
return "Filled(\(roomId))"
}
}
}
private class RoomListEntriesListenerProxy: RoomListEntriesListener {
private let onUpdateClosure: ([RoomListEntriesUpdate]) -> Void
@@ -41,61 +41,6 @@ enum RoomSummaryProviderState {
}
}
enum RoomSummary: CustomStringConvertible, Equatable {
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):
return details.id
}
}
var name: String? {
switch self {
case .empty:
return nil
case .invalidated(let details), .filled(let details):
return details.name
}
}
var isDirect: Bool {
switch self {
case .empty:
return false
case .invalidated(let details), .filled(let details):
return details.isDirect
}
}
var description: String {
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))"
}
}
static func == (lhs: RoomSummary, rhs: RoomSummary) -> Bool {
switch (lhs, rhs) {
case (.empty, .empty):
return true
case (.filled(let lhsDetails), .filled(let rhsDetails)), (.invalidated(let lhsDetails), .invalidated(let rhsDetails)):
return lhsDetails.id == rhsDetails.id
default:
return false
}
}
}
enum RoomSummaryProviderFilter: Equatable {
/// Filters out everything
case excludeAll