mirror of
https://github.com/Novaloop-AG/element-x-ios.git
synced 2026-11-02 21:33:26 +00:00
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:
@@ -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()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user