Swift 6 Migration (#5733)

* Enable Swift 6.2 with default main actor isolation for the app target

Turn on SWIFT_VERSION 6.2, SWIFT_APPROACHABLE_CONCURRENCY and SWIFT_DEFAULT_ACTOR_ISOLATION = MainActor for the ElementX target, matching the NSE and Share Extension (which already use 6.2 without the main actor default).

# Conflicts:
#	ElementX.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved

* Update the code generation templates for Swift 6

Emit nonisolated/@unchecked Sendable mocks and nonisolated string enums, and mark async mock methods @concurrent so they keep suspending like the real implementations (otherwise intermediate states are unobservable in tests). Includes the regenerated mocks and strings.

# Conflicts:
#	ElementX/Sources/Mocks/Generated/GeneratedMocks.swift

* Make the data layer and services nonisolated and Sendable

Annotate the value-type models, Rust SDK proxy wrappers, timeline item graph, settings, logging and NSE-shared types as nonisolated, and conform the service protocols to Sendable so their existentials can cross isolation boundaries. These types run off the main actor and no longer inherit the target's default main actor isolation.

* Adopt the new isolation across call sites, mocks and signatures

Update call sites, closure signatures and mock configuration helpers to match the nonisolated/Sendable data layer, including the main actor isolated mock inits and the @Sendable filter on recentlyVisitedRooms.

^ Conflicts:
^	ElementX/Sources/Services/SecureBackup/SecureBackupController.swift

* Remove redundant @MainActor annotations

With the app target defaulting to main actor isolation, the explicit @MainActor on view models, coordinators, views and their protocols is no longer required.

* Fix concurrency hotspots surfaced by the migration

Resolve the room summary main-thread deadlock by moving diff processing onto AsyncStreams, add the SDKListener.onMainActor FIFO bridge for SDK callbacks, hop KVO/timer/UIKit callbacks to the main actor via assumeIsolated, add isolated deinits, and apply the few justified nonisolated(unsafe) escapes for queue-confined Apple APIs.

* Fix the unit tests for Swift 6 concurrency

Hop the waitForConfirmation body onto the caller's actor (Swift Testing runs confirmation closures off-actor), and adjust the affected suites and async signatures.

* offload diffs on concurrent threads instead of main actor

* make room info protocols sendable

* use a task group for fetching data so that everything runs concurrently in the TextRoomTimelineView when getting the preview.

* use the generic onMainActor implementation, which already makes a stream and send the continuation on the main actor.

* small pr comments issues being addressed

* update enterprise

* Update StaticCode to 1.1.0 and specify the required Swift isolation.

* fix ElementCallServiceTests

* update enterprise

* re-generate mocks

* update enterprise

* enterprise new code conformed to swift 6

---------

Co-authored-by: Doug <douglase@element.io>
This commit is contained in:
Mauro
2026-06-18 17:55:31 +02:00
committed by GitHub
parent 7edca8b5a1
commit a520d94760
444 changed files with 3677 additions and 3361 deletions
@@ -9,7 +9,7 @@ import Foundation
import MatrixRustSDK
/// The rule used for users wishing to join a room.
public enum JoinRule: Equatable, Hashable, Sendable {
public nonisolated enum JoinRule: Equatable, Hashable, Sendable {
/// Anyone can join the room without any prior action.
case `public`
/// A user who wishes to join the room must first receive an invite.
@@ -52,7 +52,7 @@ public enum JoinRule: Equatable, Hashable, Sendable {
}
/// An allow rule which defines a condition that allows joining a room.
public enum AllowRule: Equatable, Hashable, Sendable {
public nonisolated enum AllowRule: Equatable, Hashable, Sendable {
/// Only a member of the `room_id` Room can join the one this rule is used in.
case roomMembership(roomID: String)
/// A custom allow rule implementation, containing its JSON representation as a `String`.
@@ -146,7 +146,7 @@ class JoinedRoomProxy: JoinedRoomProxyProtocol {
return
}
roomInfoObservationToken = room.subscribeToRoomInfoUpdates(listener: SDKListener { [weak self] roomInfo in
roomInfoObservationToken = room.subscribeToRoomInfoUpdates(listener: SDKListener.onMainActor { [weak self] roomInfo in
MXLog.info("Received room info update")
self?.infoSubject.send(RoomInfoProxy(roomInfo: roomInfo))
})
@@ -794,7 +794,7 @@ class JoinedRoomProxy: JoinedRoomProxyProtocol {
// MARK: - Private
private func subscribeToTypingNotifications() {
typingNotificationObservationToken = room.subscribeToTypingNotifications(listener: SDKListener { [weak self] typingUserIDs in
typingNotificationObservationToken = room.subscribeToTypingNotifications(listener: SDKListener.onMainActor { [weak self] typingUserIDs in
guard let self else { return }
MXLog.info("Received typing notification update, typingUsers: \(typingUserIDs)")
@@ -813,7 +813,7 @@ class JoinedRoomProxy: JoinedRoomProxyProtocol {
private func subscribeToIdentityStatusChanges() async {
do {
identityStatusChangesObservationToken = try await room.subscribeToIdentityStatusChanges(listener: SDKListener { [weak self] changes in
identityStatusChangesObservationToken = try await room.subscribeToIdentityStatusChanges(listener: SDKListener.onMainActor { [weak self] changes in
guard let self else { return }
MXLog.info("Received identity status changes: \(changes)")
@@ -827,7 +827,7 @@ class JoinedRoomProxy: JoinedRoomProxyProtocol {
private func subscribeToKnockRequests() async {
do {
knockRequestsChangesObservationToken = try await room.subscribeToKnockRequests(listener: SDKListener { [weak self] requests in
knockRequestsChangesObservationToken = try await room.subscribeToKnockRequests(listener: SDKListener.onMainActor { [weak self] requests in
guard let self else { return }
MXLog.info("Received requests to join update, requests id: \(requests.map(\.eventId))")
@@ -9,7 +9,7 @@
import Foundation
import MatrixRustSDK
struct KnockRequestProxy: KnockRequestProxyProtocol {
nonisolated struct KnockRequestProxy: KnockRequestProxyProtocol {
private let knockRequest: KnockRequest
init(knockRequest: KnockRequest) {
@@ -8,12 +8,12 @@
import Foundation
enum KnockRequestProxyError: Error {
nonisolated enum KnockRequestProxyError: Error {
case sdkError(Error)
}
// sourcery: AutoMockable
protocol KnockRequestProxyProtocol {
nonisolated protocol KnockRequestProxyProtocol: Sendable {
var eventID: String { get }
var userID: String { get }
var displayName: String? { get }
@@ -9,7 +9,7 @@
import Foundation
import MatrixRustSDK
struct RoomInfoProxy: RoomInfoProxyProtocol {
nonisolated struct RoomInfoProxy: RoomInfoProxyProtocol {
let roomInfo: RoomInfo
var id: String {
@@ -166,7 +166,7 @@ struct RoomInfoProxy: RoomInfoProxyProtocol {
}
}
struct RoomPreviewInfoProxy: BaseRoomInfoProxyProtocol {
nonisolated struct RoomPreviewInfoProxy: BaseRoomInfoProxyProtocol {
let roomPreviewInfo: RoomPreviewInfo
let successor: SuccessorRoom? = nil
@@ -9,7 +9,7 @@
import Foundation
import MatrixRustSDK
protocol BaseRoomInfoProxyProtocol {
nonisolated protocol BaseRoomInfoProxyProtocol: Sendable {
var id: String { get }
var displayName: String? { get }
var topic: String? { get }
@@ -25,7 +25,7 @@ protocol BaseRoomInfoProxyProtocol {
}
// sourcery: AutoMockable
protocol RoomInfoProxyProtocol: BaseRoomInfoProxyProtocol {
nonisolated protocol RoomInfoProxyProtocol: BaseRoomInfoProxyProtocol {
var id: String { get }
var creators: [String] { get }
var displayName: String? { get }
@@ -9,7 +9,7 @@
import Foundation
import MatrixRustSDK
struct RoomMemberDetails: Identifiable, Hashable {
nonisolated struct RoomMemberDetails: Identifiable, Hashable {
let id: String
let name: String?
let avatarURL: URL?
@@ -29,7 +29,7 @@ struct RoomMemberDetails: Identifiable, Hashable {
}
}
extension RoomMemberDetails {
nonisolated extension RoomMemberDetails {
init(withProxy proxy: RoomMemberProxyProtocol) {
id = proxy.userID
name = proxy.displayName
@@ -9,7 +9,7 @@
import Foundation
import MatrixRustSDK
final class RoomMemberProxy: RoomMemberProxyProtocol {
final nonisolated class RoomMemberProxy: RoomMemberProxyProtocol {
private let member: RoomMember
init(member: RoomMember) {
@@ -10,7 +10,7 @@ import Foundation
import MatrixRustSDK
// sourcery: AutoMockable
protocol RoomMemberProxyProtocol: AnyObject {
nonisolated protocol RoomMemberProxyProtocol: AnyObject, Sendable {
var userID: String { get }
var displayName: String? { get }
@@ -29,7 +29,7 @@ protocol RoomMemberProxyProtocol: AnyObject {
var isServiceMember: Bool { get }
}
extension RoomMemberProxyProtocol {
nonisolated extension RoomMemberProxyProtocol {
/// The member is active in the room (joined or invited).
var isActive: Bool {
membership == .join || membership == .invite || membership == .knock
@@ -47,7 +47,7 @@ extension RoomMemberProxyProtocol {
}
}
extension [RoomMemberProxyProtocol] {
nonisolated extension [RoomMemberProxyProtocol] {
/// The members, sorted first by power-level, and then alphabetically within each power-level.
func sorted() -> Self {
sorted { lhs, rhs in
@@ -60,7 +60,7 @@ extension [RoomMemberProxyProtocol] {
}
}
extension RoomMemberProxyProtocol {
nonisolated extension RoomMemberProxyProtocol {
var role: RoomRole {
.init(powerLevel: powerLevel)
}
@@ -74,7 +74,7 @@ struct RoomPermissionsSetting: Identifiable {
}
}
struct RoomPermissions {
nonisolated struct RoomPermissions {
/// The level required to ban a user.
var ban: Int64
/// The level required to invite a user.
@@ -8,7 +8,7 @@
import MatrixRustSDK
enum RoomPowerLevel: Hashable, Comparable {
nonisolated enum RoomPowerLevel: Hashable, Comparable {
case value(Int)
case infinite
@@ -36,7 +36,7 @@ enum RoomPowerLevel: Hashable, Comparable {
}
}
extension PowerLevel {
nonisolated extension PowerLevel {
var toRoomPowerLevel: RoomPowerLevel {
switch self {
case .infinite: .infinite
@@ -9,7 +9,7 @@
import MatrixRustSDK
// sourcery: AutoMockable
protocol RoomPowerLevelsProxyProtocol {
protocol RoomPowerLevelsProxyProtocol: Sendable {
var values: RoomPowerLevelsValues { get }
var userPowerLevels: [String: Int64] { get }
@@ -8,7 +8,7 @@
import MatrixRustSDK
struct RoomPowerLevelsProxy: RoomPowerLevelsProxyProtocol {
nonisolated struct RoomPowerLevelsProxy: RoomPowerLevelsProxyProtocol {
private let powerLevels: RoomPowerLevels
init?(_ powerLevels: RoomPowerLevels?) {
@@ -9,7 +9,7 @@
import Foundation
/// A preview object for the Room. useful to get all the possible info for rooms to which the user is not invited to
protocol RoomPreviewProxyProtocol {
protocol RoomPreviewProxyProtocol: Sendable {
var info: RoomPreviewInfoProxy { get }
var ownMembershipDetails: RoomMembershipDetailsProxyProtocol? { get async }
}
@@ -33,7 +33,7 @@ enum RoomProxyType {
}
// sourcery: AutoMockable
protocol RoomProxyProtocol {
protocol RoomProxyProtocol: Sendable {
var id: String { get }
var ownUserID: String { get }
}
@@ -8,7 +8,7 @@
import MatrixRustSDK
enum RoomRole: Comparable {
nonisolated enum RoomRole: Comparable {
/// Default role PL 0...49
case user
/// Able to perform room moderation actions PL 50...99
@@ -21,7 +21,7 @@ enum RoomRole: Comparable {
case creator
}
extension RoomRole {
nonisolated extension RoomRole {
init(powerLevel: RoomPowerLevel) {
do {
let role = try suggestedRoleForPowerLevel(powerLevel: powerLevel.rustPowerLevel)
@@ -69,7 +69,7 @@ extension RoomRole {
}
}
extension RoomRole {
nonisolated extension RoomRole {
init(powerLevelValue: Int64) {
// Also this is not great, and should be handled by a `suggestedRoleForPowerLevelValue` function from the SDK
guard powerLevelValue < 150 else {
@@ -9,7 +9,7 @@
import Foundation
import MatrixRustSDK
struct RoomEventStringBuilder {
nonisolated struct RoomEventStringBuilder {
let stateEventStringBuilder: RoomStateEventStringBuilder
let messageEventStringBuilder: RoomMessageEventStringBuilder
let shouldPrefixSenderName: Bool
@@ -9,7 +9,7 @@
import Foundation
import MatrixRustSDK
struct RoomMessageEventStringBuilder {
nonisolated struct RoomMessageEventStringBuilder {
enum Style {
/// Plain: no prefix, no special text treatment
/// Shown in push notifications and thread lists
@@ -10,7 +10,7 @@ import Foundation
import MatrixRustSDK
/// A quick summary of a Room, useful to describe and give quick informations for the room list
struct RoomSummary {
nonisolated struct RoomSummary {
enum JoinRequestType {
case invite(inviter: RoomMemberProxyProtocol?)
case knock
@@ -20,8 +20,6 @@ class RoomSummaryProvider: RoomSummaryProviderProtocol {
private let roomListPageSize: UInt32
private let serialDispatchQueue: DispatchQueue
private let visibleItemRangePublisher = CurrentValueSubject<Range<Int>, Never>(0..<0)
// periphery:ignore - retaining purpose
@@ -35,7 +33,13 @@ class RoomSummaryProvider: RoomSummaryProviderProtocol {
private let roomListSubject = CurrentValueSubject<[RoomSummary], Never>([])
private let stateSubject = CurrentValueSubject<RoomSummaryProviderState, Never>(.notLoaded)
private let diffsPublisher = PassthroughSubject<[RoomListEntriesUpdate], Never>()
/// 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()
@@ -64,7 +68,6 @@ class RoomSummaryProvider: RoomSummaryProviderProtocol {
notificationSettings: NotificationSettingsProxyProtocol,
appSettings: AppSettings) {
self.roomListService = roomListService
serialDispatchQueue = DispatchQueue(label: "io.element.elementx.room_summary_provider", qos: .default)
self.eventStringBuilder = eventStringBuilder
self.name = name
self.shouldUpdateVisibleRange = shouldUpdateVisibleRange
@@ -72,16 +75,35 @@ class RoomSummaryProvider: RoomSummaryProviderProtocol {
self.appSettings = appSettings
self.roomListPageSize = roomListPageSize
diffsPublisher
.receive(on: serialDispatchQueue)
.sink { [weak self] in self?.updateRoomsWithDiffs($0) }
.store(in: &cancellables)
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
@@ -91,18 +113,15 @@ class RoomSummaryProvider: RoomSummaryProviderProtocol {
do {
listUpdatesSubscriptionResult = roomList.entriesWithDynamicAdapters(pageSize: UInt32(roomListPageSize),
listener: SDKListener { [weak self] updates in
guard let self else { return }
diffsPublisher.send(updates)
listener: SDKListener { [diffsContinuation] updates in
diffsContinuation.yield(updates)
})
// Forces the listener above to be called with the current state
setFilter(.all(filters: []))
let stateUpdatesSubscriptionResult = try roomList.loadingState(listener: SDKListener { [weak self] state in
guard let self else { return }
MXLog.info("\(name): Received state update: \(state)")
stateSubject.send(RoomSummaryProviderState(roomListState: state))
let stateUpdatesSubscriptionResult = try roomList.loadingState(listener: SDKListener { [loadingStateContinuation] state in
loadingStateContinuation.yield(state)
})
stateUpdatesTaskHandle = stateUpdatesSubscriptionResult.stateStream
@@ -208,20 +227,33 @@ class RoomSummaryProvider: RoomSummaryProviderProtocol {
.store(in: &cancellables)
}
fileprivate func updateRoomsWithDiffs(_ diffs: [RoomListEntriesUpdate]) {
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] {
let span = MXLog.createSpan("\(name).process_room_list_diffs")
span.enter()
defer {
span.exit()
}
rooms = diffs.reduce(rooms) { currentItems, diff in
processDiff(diff, on: currentItems)
var updatedRooms = currentRooms
for diff in diffs {
updatedRooms = await processDiff(diff, on: updatedRooms, eventStringBuilder: eventStringBuilder, name: name)
}
return updatedRooms
}
private func processDiff(_ diff: RoomListEntriesUpdate, on currentItems: [RoomSummary]) -> [RoomSummary] {
guard let collectionDiff = buildDiff(from: diff, on: currentItems) else {
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
}
@@ -234,30 +266,19 @@ class RoomSummaryProvider: RoomSummaryProviderProtocol {
return updatedItems
}
private func fetchRoomDetails(from room: Room) -> (roomInfo: RoomInfo?, latestEvent: LatestEventValue?) {
class FetchResult {
var roomInfo: RoomInfo?
var latestEvent: LatestEventValue?
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)
}
let semaphore = DispatchSemaphore(value: 0)
let result = FetchResult()
Task {
do {
result.latestEvent = await room.latestEvent()
result.roomInfo = try await room.roomInfo()
} catch {
MXLog.error("Failed fetching room info with error: \(error)")
}
semaphore.signal()
}
semaphore.wait()
return (result.roomInfo, result.latestEvent)
}
private func buildRoomSummary(from room: Room) -> RoomSummary {
let roomDetails = fetchRoomDetails(from: room)
private nonisolated static func buildRoomSummary(from room: Room, eventStringBuilder: RoomEventStringBuilder) async -> RoomSummary {
let roomDetails = await fetchRoomDetails(from: room)
guard let roomInfo = roomDetails.roomInfo else {
fatalError("Missing room info for \(room.id())")
@@ -347,13 +368,13 @@ class RoomSummaryProvider: RoomSummaryProviderProtocol {
isTombstoned: roomInfo.successorRoom != nil)
}
private func buildDiff(from diff: RoomListEntriesUpdate, on rooms: [RoomSummary]) -> CollectionDifference<RoomSummary>? {
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 = buildRoomSummary(from: value)
let summary = await buildRoomSummary(from: value, eventStringBuilder: eventStringBuilder)
changes.append(.insert(offset: rooms.count + index, element: summary, associatedWith: nil))
}
case .clear:
@@ -361,7 +382,7 @@ class RoomSummaryProvider: RoomSummaryProviderProtocol {
changes.append(.remove(offset: index, element: value, associatedWith: nil))
}
case .insert(let index, let value):
let summary = buildRoomSummary(from: 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 {
@@ -373,10 +394,10 @@ class RoomSummaryProvider: RoomSummaryProviderProtocol {
let summary = rooms[0]
changes.append(.remove(offset: 0, element: summary, associatedWith: nil))
case .pushBack(let value):
let summary = buildRoomSummary(from: 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 = buildRoomSummary(from: 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)]
@@ -387,10 +408,10 @@ class RoomSummaryProvider: RoomSummaryProviderProtocol {
}
for (index, value) in values.enumerated() {
changes.append(.insert(offset: index, element: buildRoomSummary(from: value), associatedWith: nil))
await changes.append(.insert(offset: index, element: buildRoomSummary(from: value, eventStringBuilder: eventStringBuilder), associatedWith: nil))
}
case .set(let index, let value):
let summary = buildRoomSummary(from: 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):
@@ -408,32 +429,40 @@ class RoomSummaryProvider: RoomSummaryProviderProtocol {
private func setupNotificationSettingsSubscription() {
notificationSettings.callbacks
.receive(on: serialDispatchQueue)
.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:
self.rebuildRoomSummaries()
Task { await self.rebuildRoomSummaries() }
}
}
.store(in: &cancellables)
}
private func rebuildRoomSummaries() {
private func rebuildRoomSummaries() async {
MXLog.info("\(name): Rebuilding room summaries for \(rooms.count) rooms")
rooms = await Self.rebuiltRoomSummaries(from: rooms, eventStringBuilder: eventStringBuilder, name: name)
MXLog.info("\(name): Finished rebuilding room summaries (\(rooms.count) rooms)")
}
@concurrent
private static func rebuiltRoomSummaries(from rooms: [RoomSummary], eventStringBuilder: RoomEventStringBuilder, name: String) async -> [RoomSummary] {
let span = MXLog.createSpan("\(name).rebuild_room_summaries")
span.enter()
defer {
span.exit()
}
MXLog.info("\(name): Rebuilding room summaries for \(rooms.count) rooms")
rooms = rooms.map {
self.buildRoomSummary(from: $0.room)
var rebuiltRooms = [RoomSummary]()
rebuiltRooms.reserveCapacity(rooms.count)
for room in rooms {
await rebuiltRooms.append(buildRoomSummary(from: room.room, eventStringBuilder: eventStringBuilder))
}
MXLog.info("\(name): Finished rebuilding room summaries (\(rooms.count) rooms)")
return rebuiltRooms
}
}
@@ -20,23 +20,32 @@ class RoomThreadListProxy: RoomThreadListServiceProxyProtocol {
}
private var paginationStateHandle: TaskHandle?
let paginationStatePublisher: CurrentValuePublisher<RoomThreadListPaginationState, Never>
private let paginationStateSubject: CurrentValueSubject<RoomThreadListPaginationState, Never>
var paginationStatePublisher: CurrentValuePublisher<RoomThreadListPaginationState, Never> {
paginationStateSubject.asCurrentValuePublisher()
}
init(threadListService: ThreadListServiceProtocol, eventStringBuilder: RoomEventStringBuilder, paginationToken: String? = nil) {
self.threadListService = threadListService
self.eventStringBuilder = eventStringBuilder
let paginationStateSubject = CurrentValueSubject<RoomThreadListPaginationState, Never>(.init(sdkState: threadListService.paginationState()))
paginationStatePublisher = paginationStateSubject.asCurrentValuePublisher()
paginationStateSubject = CurrentValueSubject<RoomThreadListPaginationState, Never>(.init(sdkState: threadListService.paginationState()))
paginationStateHandle = threadListService.subscribeToPaginationStateUpdates(listener: SDKListener { state in
paginationStateSubject.send(.init(sdkState: state))
// The listeners are called by the SDK from arbitrary threads, hop to the
// main actor where this proxy lives.
paginationStateHandle = threadListService.subscribeToPaginationStateUpdates(listener: SDKListener { [weak self] state in
Task { @MainActor in
self?.paginationStateSubject.send(.init(sdkState: state))
}
})
updateItems()
itemUpdatesHandle = self.threadListService.subscribeToItemsUpdates(listener: SDKListener { [weak self] _ in
self?.updateItems()
Task { @MainActor in
self?.updateItems()
}
})
}