mirror of
https://github.com/Novaloop-AG/element-x-ios.git
synced 2026-11-02 12:07:55 +00:00
c41a38159b
* Initial plan * Offer only joined rooms in destination pickers Co-authored-by: Velin92 <34335419+Velin92@users.noreply.github.com> * Clean whitespace in share picker test Co-authored-by: Velin92 <34335419+Velin92@users.noreply.github.com> * Restrict the shared picker room list to joined rooms The alternate room summary provider, which backs the forward, share and Add rooms pickers and global search, now always includes the SDK's joined filter in its base filter. This replaces the per-call joinedOnly flags, so Add rooms no longer offers invited rooms or spaces and no screen can forget the flag. A filter set before the provider has its room list is now applied once the list is set, instead of being reset to the default. The share picker hit this on a cold launch from the share extension. The forward and share pickers also hide upgraded rooms, matching the Suggestions section. The SDK has no filter for these. Also registers the share picker's test file in the project, fixes the blank-line indentation SwiftFormat flagged, and re-records the picker snapshots without the upgraded room. * Check room membership for forwarding suggestions and search breadcrumbs RoomSummary now has isJoined, built from the room's membership. The forwarding suggestions and the search breadcrumbs used a missing joinRequestType to mean joined, which is also the case for rooms that have been left or banned from. It only worked because the room list's base filter already drops those. --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: Velin92 <34335419+Velin92@users.noreply.github.com> Co-authored-by: Mauro Romito <mauro.romito@element.io>
504 lines
23 KiB
Swift
504 lines
23 KiB
Swift
//
|
|
// Copyright 2025 Element Creations Ltd.
|
|
// Copyright 2022-2025 New Vector Ltd.
|
|
//
|
|
// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
|
|
// Please see LICENSE files in the repository root for full details.
|
|
//
|
|
|
|
import Combine
|
|
import Foundation
|
|
import MatrixRustSDK
|
|
|
|
class RoomSummaryProvider: RoomSummaryProviderProtocol {
|
|
private let roomListService: RoomListServiceProtocol
|
|
private let eventStringBuilder: RoomEventStringBuilder
|
|
private let name: String
|
|
private let shouldUpdateVisibleRange: Bool
|
|
private let notificationSettings: NotificationSettingsProxyProtocol
|
|
private let userSettings: UserSettings
|
|
private let baseFilter: [RoomListEntriesDynamicFilterKind]
|
|
|
|
private let roomListPageSize: UInt32
|
|
/// Remember how many rooms we had on the previous requests so we can deduplicate
|
|
private var roomCountOnLastPageAddRequest = -1
|
|
|
|
private let visibleItemRangePublisher = CurrentValueSubject<Range<Int>, Never>(0..<0)
|
|
|
|
// periphery:ignore - retaining purpose
|
|
private var roomList: RoomListProtocol?
|
|
|
|
private var cancellables = Set<AnyCancellable>()
|
|
private var listUpdatesSubscriptionResult: RoomListEntriesWithDynamicAdaptersResult?
|
|
private var currentFilter: RoomSummaryProviderFilter?
|
|
private var stateUpdatesTaskHandle: TaskHandle?
|
|
|
|
private let roomListSubject = CurrentValueSubject<[RoomSummary], Never>([])
|
|
private let stateSubject = CurrentValueSubject<RoomSummaryProviderState, Never>(.notLoaded)
|
|
|
|
/// Bridge from the SDK's synchronous callback into Swift Concurrency. Yielding is safe from any
|
|
/// thread; a single long-lived `for await` consumer (set up in `init`) applies the diffs on the
|
|
/// main actor in FIFO order, guaranteeing one in-flight update at a time.
|
|
private let diffsContinuation: AsyncStream<[RoomListEntriesUpdate]>.Continuation
|
|
|
|
/// Same bridging pattern as `diffsContinuation`, for the room list loading state.
|
|
private let loadingStateContinuation: AsyncStream<RoomListLoadingState>.Continuation
|
|
|
|
var roomListPublisher: CurrentValuePublisher<[RoomSummary], Never> {
|
|
roomListSubject.asCurrentValuePublisher()
|
|
}
|
|
|
|
var statePublisher: CurrentValuePublisher<RoomSummaryProviderState, Never> {
|
|
stateSubject.asCurrentValuePublisher()
|
|
}
|
|
|
|
private var rooms: [RoomSummary] = [] {
|
|
didSet {
|
|
roomListSubject.send(rooms)
|
|
}
|
|
}
|
|
|
|
/// Build a new summary provider with the given parameters
|
|
/// - Parameters:
|
|
/// - shouldUpdateVisibleRange: whether this summary provider should forward visible ranges
|
|
/// to the room list service through the `applyInput(input: .viewport(ranges` api. Only useful for
|
|
/// lists that need to update the visible range on Sliding Sync
|
|
/// - includesOnlyJoinedRooms: whether to leave out invites and other rooms the user hasn't joined, whatever the filter
|
|
init(roomListService: RoomListServiceProtocol,
|
|
eventStringBuilder: RoomEventStringBuilder,
|
|
name: String,
|
|
shouldUpdateVisibleRange: Bool = false,
|
|
includesOnlyJoinedRooms: Bool = false,
|
|
roomListPageSize: UInt32 = 100,
|
|
notificationSettings: NotificationSettingsProxyProtocol,
|
|
userSettings: UserSettings) {
|
|
self.roomListService = roomListService
|
|
self.eventStringBuilder = eventStringBuilder
|
|
self.name = name
|
|
self.shouldUpdateVisibleRange = shouldUpdateVisibleRange
|
|
self.notificationSettings = notificationSettings
|
|
self.userSettings = userSettings
|
|
self.roomListPageSize = roomListPageSize
|
|
|
|
baseFilter = [.any(filters: [.all(filters: [.nonSpace, .nonLeft]),
|
|
.all(filters: [.space, .invite])]),
|
|
.deduplicateVersions] + (includesOnlyJoinedRooms ? [.joined] : [])
|
|
|
|
let (diffsStream, diffsContinuation) = AsyncStream<[RoomListEntriesUpdate]>.makeStream()
|
|
self.diffsContinuation = diffsContinuation
|
|
|
|
let (loadingStateStream, loadingStateContinuation) = AsyncStream<RoomListLoadingState>.makeStream()
|
|
self.loadingStateContinuation = loadingStateContinuation
|
|
|
|
Task { [weak self] in
|
|
for await diffs in diffsStream {
|
|
await self?.updateRoomsWithDiffs(diffs)
|
|
}
|
|
}
|
|
|
|
Task { [weak self, name] in
|
|
for await state in loadingStateStream {
|
|
MXLog.info("\(name): Received state update: \(state)")
|
|
self?.stateSubject.send(RoomSummaryProviderState(roomListState: state))
|
|
}
|
|
}
|
|
|
|
setupVisibleRangeObservers()
|
|
|
|
setupNotificationSettingsSubscription()
|
|
}
|
|
|
|
deinit {
|
|
diffsContinuation.finish()
|
|
loadingStateContinuation.finish()
|
|
}
|
|
|
|
func setRoomList(_ roomList: RoomList) {
|
|
guard stateUpdatesTaskHandle == nil else {
|
|
return
|
|
}
|
|
|
|
self.roomList = roomList
|
|
|
|
do {
|
|
listUpdatesSubscriptionResult = roomList.entriesWithDynamicAdapters(pageSize: UInt32(roomListPageSize),
|
|
listener: SDKListener { [diffsContinuation] updates in
|
|
diffsContinuation.yield(updates)
|
|
})
|
|
|
|
// Forces the listener above to be called with the current state
|
|
let filter = currentFilter ?? .all(filters: [])
|
|
currentFilter = nil
|
|
setFilter(filter)
|
|
|
|
let stateUpdatesSubscriptionResult = try roomList.loadingState(listener: SDKListener { [loadingStateContinuation] state in
|
|
loadingStateContinuation.yield(state)
|
|
})
|
|
|
|
stateUpdatesTaskHandle = stateUpdatesSubscriptionResult.stateStream
|
|
|
|
stateSubject.send(RoomSummaryProviderState(roomListState: stateUpdatesSubscriptionResult.state))
|
|
} catch {
|
|
MXLog.error("Failed setting up room list entry listener with error: \(error)")
|
|
}
|
|
}
|
|
|
|
func updateVisibleRange(_ range: Range<Int>) {
|
|
visibleItemRangePublisher.send(range)
|
|
}
|
|
|
|
func setFilter(_ filter: RoomSummaryProviderFilter) {
|
|
guard filter != currentFilter else {
|
|
return
|
|
}
|
|
|
|
currentFilter = filter
|
|
|
|
switch filter {
|
|
case .excludeAll:
|
|
_ = listUpdatesSubscriptionResult?.controller().setFilter(kind: .none)
|
|
case let .search(query):
|
|
let filters = nameFilter(for: query) + baseFilter
|
|
_ = listUpdatesSubscriptionResult?.controller().setFilter(kind: .all(filters: filters))
|
|
case .rooms(let roomIDs, let filters):
|
|
var rustFilters = filters.map(\.rustFilter) + baseFilter
|
|
|
|
rustFilters.append(.identifiers(identifiers: Array(roomIDs)))
|
|
|
|
if !filters.contains(.lowPriority), userSettings.lowPriorityFilterEnabled {
|
|
rustFilters.append(.nonLowPriority)
|
|
}
|
|
|
|
_ = listUpdatesSubscriptionResult?.controller().setFilter(kind: .all(filters: rustFilters))
|
|
case let .all(filters):
|
|
var rustFilters = filters.map(\.rustFilter) + baseFilter
|
|
|
|
if !filters.contains(.lowPriority), userSettings.lowPriorityFilterEnabled {
|
|
rustFilters.append(.nonLowPriority)
|
|
}
|
|
|
|
_ = listUpdatesSubscriptionResult?.controller().setFilter(kind: .all(filters: rustFilters))
|
|
}
|
|
}
|
|
|
|
// MARK: - Private
|
|
|
|
private func nameFilter(for query: String) -> [RoomListEntriesDynamicFilterKind] {
|
|
userSettings.fuzzyRoomListSearchEnabled ? [.fuzzyMatchRoomName(pattern: query)] : [.normalizedMatchRoomName(pattern: query)]
|
|
}
|
|
|
|
private func setupVisibleRangeObservers() {
|
|
// Unthrottled to add another page half way through the last one
|
|
visibleItemRangePublisher
|
|
.sink { [weak self] range in
|
|
guard let self,
|
|
!range.isEmpty,
|
|
range.upperBound >= rooms.count - Int(roomListPageSize) / 2,
|
|
rooms.count != roomCountOnLastPageAddRequest else {
|
|
return
|
|
}
|
|
|
|
roomCountOnLastPageAddRequest = rooms.count
|
|
|
|
MXLog.info("\(self.name): Adding a page at \(rooms.count) rooms, visible range: \(range)")
|
|
|
|
listUpdatesSubscriptionResult?.controller().addOnePage()
|
|
}
|
|
.store(in: &cancellables)
|
|
|
|
visibleItemRangePublisher
|
|
.throttle(for: 0.5, scheduler: DispatchQueue.main, latest: true)
|
|
.removeDuplicates()
|
|
.sink { [weak self] range in
|
|
guard let self else { return }
|
|
|
|
MXLog.info("\(self.name): Updating visible range: \(range)")
|
|
|
|
if range.lowerBound == 0, range.upperBound < rooms.count {
|
|
roomCountOnLastPageAddRequest = -1
|
|
listUpdatesSubscriptionResult?.controller().resetToOnePage()
|
|
}
|
|
}
|
|
.store(in: &cancellables)
|
|
|
|
visibleItemRangePublisher
|
|
.throttle(for: 0.5, scheduler: DispatchQueue.main, latest: true)
|
|
.filter { [weak self] range in
|
|
guard let self else { return false }
|
|
return !range.isEmpty && shouldUpdateVisibleRange
|
|
}
|
|
.compactMap { [weak self] (range: Range) -> [String]? in
|
|
guard let self else { return nil }
|
|
|
|
// 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 range = range
|
|
if range.upperBound - range.lowerBound > SlidingSyncConstants.maximumVisibleRangeSize {
|
|
let upperBound = range.lowerBound + SlidingSyncConstants.maximumVisibleRangeSize
|
|
range = range.lowerBound..<upperBound
|
|
}
|
|
|
|
MXLog.info("\(self.name): Subscribing to rooms in range: \(range)")
|
|
|
|
return range
|
|
.filter { $0 < self.rooms.count }
|
|
.map { self.rooms[$0].id }
|
|
}
|
|
.removeDuplicates()
|
|
.sink { [weak self] roomIDs in
|
|
guard let self else { return }
|
|
|
|
Task { [weak self] in
|
|
do {
|
|
try await self?.roomListService.setRoomSubscriptions(roomIds: roomIDs)
|
|
} catch {
|
|
MXLog.error("Failed subscribing to rooms with error: \(error)")
|
|
}
|
|
}
|
|
}
|
|
.store(in: &cancellables)
|
|
}
|
|
|
|
fileprivate func updateRoomsWithDiffs(_ diffs: [RoomListEntriesUpdate]) async {
|
|
// Building the room summaries (SDK fetches + string building) and applying the
|
|
// CollectionDifference can be expensive, so compute off the main actor and only
|
|
// hop back to publish the result.
|
|
rooms = await Self.updatedRooms(from: diffs, on: rooms, eventStringBuilder: eventStringBuilder, name: name)
|
|
}
|
|
|
|
@concurrent
|
|
private static func updatedRooms(from diffs: [RoomListEntriesUpdate],
|
|
on currentRooms: [RoomSummary],
|
|
eventStringBuilder: RoomEventStringBuilder,
|
|
name: String) async -> [RoomSummary] {
|
|
var updatedRooms = currentRooms
|
|
for diff in diffs {
|
|
updatedRooms = await processDiff(diff, on: updatedRooms, eventStringBuilder: eventStringBuilder, name: name)
|
|
}
|
|
return updatedRooms
|
|
}
|
|
|
|
private nonisolated static func processDiff(_ diff: RoomListEntriesUpdate, on currentItems: [RoomSummary], eventStringBuilder: RoomEventStringBuilder, name: String) async -> [RoomSummary] {
|
|
guard let collectionDiff = await buildDiff(from: diff, on: currentItems, eventStringBuilder: eventStringBuilder) else {
|
|
MXLog.error("\(name): Failed building CollectionDifference from \(diff)")
|
|
return currentItems
|
|
}
|
|
|
|
guard let updatedItems = currentItems.applying(collectionDiff) else {
|
|
MXLog.error("\(name): Failed applying diff: \(collectionDiff)")
|
|
return currentItems
|
|
}
|
|
|
|
return updatedItems
|
|
}
|
|
|
|
private nonisolated static func fetchRoomDetails(from room: Room) async -> (roomInfo: RoomInfo?, latestEvent: LatestEventValue?) {
|
|
do {
|
|
let latestEvent = await room.latestEvent()
|
|
let roomInfo = try await room.roomInfo()
|
|
return (roomInfo, latestEvent)
|
|
} catch {
|
|
MXLog.error("Failed fetching room info with error: \(error)")
|
|
return (nil, nil)
|
|
}
|
|
}
|
|
|
|
private nonisolated static func buildRoomSummary(from room: Room, eventStringBuilder: RoomEventStringBuilder) async -> RoomSummary {
|
|
let roomDetails = await fetchRoomDetails(from: room)
|
|
|
|
guard let roomInfo = roomDetails.roomInfo else {
|
|
// The room info can be momentarily unavailable while the client tears down (e.g. on logout),
|
|
// where the SDK still delivers a diff but `room.roomInfo()` throws. Return a placeholder to
|
|
// keep the diff indices aligned with the SDK instead of crashing.
|
|
MXLog.error("Missing room info for \(room.id())")
|
|
return .placeholder(room: room)
|
|
}
|
|
|
|
var attributedLastMessage: AttributedString?
|
|
var lastMessageDate: Date?
|
|
var lastMessageState: RoomSummary.LastMessageState?
|
|
|
|
if let latestRoomMessage = roomDetails.latestEvent {
|
|
switch latestRoomMessage {
|
|
case .local(let timestamp, let senderID, let profile, let content, let state):
|
|
let sender = TimelineItemSender(senderID: senderID, senderProfile: profile)
|
|
attributedLastMessage = eventStringBuilder.buildAttributedString(for: content, sender: sender, isOutgoing: true)
|
|
lastMessageDate = Date(timeIntervalSince1970: TimeInterval(timestamp / 1000))
|
|
|
|
switch state {
|
|
case .isSending:
|
|
lastMessageState = .sending
|
|
case .cannotBeSent:
|
|
lastMessageState = .failed
|
|
case .hasBeenSent:
|
|
lastMessageState = nil
|
|
}
|
|
case .remote(let timestamp, let senderID, let isOwn, let profile, let content):
|
|
let sender = TimelineItemSender(senderID: senderID, senderProfile: profile)
|
|
attributedLastMessage = eventStringBuilder.buildAttributedString(for: content, sender: sender, isOutgoing: isOwn)
|
|
lastMessageDate = Date(timeIntervalSince1970: TimeInterval(timestamp / 1000))
|
|
case .remoteInvite(let timestamp, let senderID, let profile):
|
|
lastMessageDate = Date(timeIntervalSince1970: TimeInterval(timestamp / 1000))
|
|
|
|
if let senderID {
|
|
let sender = TimelineItemSender(senderID: senderID, senderProfile: profile)
|
|
let senderDisplayName = sender.displayName ?? sender.id
|
|
let invitedYouString = eventStringBuilder.stateEventStringBuilder.buildInvitedYouString(senderDisplayName)
|
|
attributedLastMessage = AttributedString(invitedYouString)
|
|
}
|
|
case .none:
|
|
break
|
|
}
|
|
}
|
|
|
|
var inviterProxy: RoomMemberProxyProtocol?
|
|
if let inviter = roomInfo.inviter {
|
|
inviterProxy = RoomMemberProxy(member: inviter)
|
|
}
|
|
|
|
let notificationMode = roomInfo.cachedUserDefinedNotificationMode.flatMap { RoomNotificationModeProxy.from(roomNotificationMode: $0) }
|
|
|
|
let joinRequestType: RoomSummary.JoinRequestType? = switch roomInfo.membership {
|
|
case .invited: .invite(inviter: inviterProxy)
|
|
case .knocked: .knock
|
|
default: nil
|
|
}
|
|
|
|
let activeCallIntent: RtcCallIntent? = switch roomInfo.activeRoomCallConsensusIntent {
|
|
case .full(let intent):
|
|
intent
|
|
case .partial(intent: let intent, _, _):
|
|
intent
|
|
case .none:
|
|
nil
|
|
}
|
|
|
|
return RoomSummary(room: room,
|
|
id: roomInfo.id,
|
|
joinRequestType: joinRequestType,
|
|
isJoined: roomInfo.membership == .joined,
|
|
name: roomInfo.displayName ?? roomInfo.id,
|
|
isDirect: roomInfo.isDirect,
|
|
isSpace: roomInfo.isSpace,
|
|
avatarURL: roomInfo.avatarUrl.flatMap(URL.init(string:)),
|
|
heroes: roomInfo.heroes.map(UserProfile.init),
|
|
activeMembersCount: UInt(roomInfo.activeMembersCount),
|
|
lastMessage: attributedLastMessage,
|
|
lastMessageDate: lastMessageDate,
|
|
lastMessageState: lastMessageState,
|
|
unreadMessagesCount: UInt(roomInfo.numUnreadMessages),
|
|
unreadMentionsCount: UInt(roomInfo.numUnreadMentions),
|
|
unreadNotificationsCount: UInt(roomInfo.numUnreadNotifications),
|
|
notificationMode: notificationMode,
|
|
canonicalAlias: roomInfo.canonicalAlias,
|
|
alternativeAliases: .init(roomInfo.alternativeAliases),
|
|
hasOngoingCall: roomInfo.hasRoomCall,
|
|
activeCallIntent: activeCallIntent.map { .init(rustCallIntent: $0) },
|
|
isMarkedUnread: roomInfo.isMarkedUnread,
|
|
isFavourite: roomInfo.isFavourite,
|
|
isTombstoned: roomInfo.successorRoom != nil)
|
|
}
|
|
|
|
private nonisolated static func buildDiff(from diff: RoomListEntriesUpdate, on rooms: [RoomSummary], eventStringBuilder: RoomEventStringBuilder) async -> CollectionDifference<RoomSummary>? {
|
|
var changes = [CollectionDifference<RoomSummary>.Change]()
|
|
|
|
switch diff {
|
|
case .append(let values):
|
|
for (index, value) in values.enumerated() {
|
|
let summary = await buildRoomSummary(from: value, eventStringBuilder: eventStringBuilder)
|
|
changes.append(.insert(offset: rooms.count + index, element: summary, associatedWith: nil))
|
|
}
|
|
case .clear:
|
|
for (index, value) in rooms.enumerated() {
|
|
changes.append(.remove(offset: index, element: value, associatedWith: nil))
|
|
}
|
|
case .insert(let index, let value):
|
|
let summary = await buildRoomSummary(from: value, eventStringBuilder: eventStringBuilder)
|
|
changes.append(.insert(offset: Int(index), element: summary, associatedWith: nil))
|
|
case .popBack:
|
|
guard let value = rooms.last else {
|
|
fatalError()
|
|
}
|
|
|
|
changes.append(.remove(offset: rooms.count - 1, element: value, associatedWith: nil))
|
|
case .popFront:
|
|
let summary = rooms[0]
|
|
changes.append(.remove(offset: 0, element: summary, associatedWith: nil))
|
|
case .pushBack(let value):
|
|
let summary = await buildRoomSummary(from: value, eventStringBuilder: eventStringBuilder)
|
|
changes.append(.insert(offset: rooms.count, element: summary, associatedWith: nil))
|
|
case .pushFront(let value):
|
|
let summary = await buildRoomSummary(from: value, eventStringBuilder: eventStringBuilder)
|
|
changes.append(.insert(offset: 0, element: summary, associatedWith: nil))
|
|
case .remove(let index):
|
|
let summary = rooms[Int(index)]
|
|
changes.append(.remove(offset: Int(index), element: summary, associatedWith: nil))
|
|
case .reset(let values):
|
|
for (index, summary) in rooms.enumerated() {
|
|
changes.append(.remove(offset: index, element: summary, associatedWith: nil))
|
|
}
|
|
|
|
for (index, value) in values.enumerated() {
|
|
await changes.append(.insert(offset: index, element: buildRoomSummary(from: value, eventStringBuilder: eventStringBuilder), associatedWith: nil))
|
|
}
|
|
case .set(let index, let value):
|
|
let summary = await buildRoomSummary(from: value, eventStringBuilder: eventStringBuilder)
|
|
changes.append(.remove(offset: Int(index), element: summary, associatedWith: nil))
|
|
changes.append(.insert(offset: Int(index), element: summary, associatedWith: nil))
|
|
case .truncate(let length):
|
|
for (index, value) in rooms.enumerated() {
|
|
if index < length {
|
|
continue
|
|
}
|
|
|
|
changes.append(.remove(offset: index, element: value, associatedWith: nil))
|
|
}
|
|
}
|
|
|
|
return CollectionDifference(changes)
|
|
}
|
|
|
|
private func setupNotificationSettingsSubscription() {
|
|
notificationSettings.callbacks
|
|
.receive(on: DispatchQueue.main)
|
|
.dropFirst() // drop the first one to avoid rebuilding the summaries during the first synchronization
|
|
.sink { [weak self] callback in
|
|
guard let self else { return }
|
|
switch callback {
|
|
case .settingsDidChange:
|
|
Task { await self.rebuildRoomSummaries() }
|
|
}
|
|
}
|
|
.store(in: &cancellables)
|
|
}
|
|
|
|
private func rebuildRoomSummaries() async {
|
|
MXLog.info("\(name): Rebuilding room summaries for \(rooms.count) rooms")
|
|
|
|
rooms = await Self.rebuiltRoomSummaries(from: rooms, eventStringBuilder: eventStringBuilder)
|
|
|
|
MXLog.info("\(name): Finished rebuilding room summaries (\(rooms.count) rooms)")
|
|
}
|
|
|
|
@concurrent
|
|
private static func rebuiltRoomSummaries(from rooms: [RoomSummary], eventStringBuilder: RoomEventStringBuilder) async -> [RoomSummary] {
|
|
var rebuiltRooms = [RoomSummary]()
|
|
rebuiltRooms.reserveCapacity(rooms.count)
|
|
for room in rooms {
|
|
await rebuiltRooms.append(buildRoomSummary(from: room.room, eventStringBuilder: eventStringBuilder))
|
|
}
|
|
return rebuiltRooms
|
|
}
|
|
}
|
|
|
|
extension RoomSummaryProviderState {
|
|
init(roomListState: RoomListLoadingState) {
|
|
switch roomListState {
|
|
case .notLoaded:
|
|
self = .notLoaded
|
|
case .loaded(let maximumNumberOfRooms):
|
|
self = .loaded(totalNumberOfRooms: UInt(maximumNumberOfRooms ?? 0))
|
|
}
|
|
}
|
|
}
|