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
@@ -8,7 +8,7 @@
import Foundation
/// Represents a notification alert tone and its backing audio file location.
struct NotificationTone: Hashable, Comparable, Codable {
nonisolated struct NotificationTone: Hashable, Comparable, Codable {
/// Display name for the tone, falling back to the filename stem.
var label: String {
labelOverride ?? URL(filePath: "/\(relativePath.last, default: "")").deletingPathExtension().lastPathComponent
@@ -10,7 +10,7 @@ import Foundation
import UniformTypeIdentifiers
/// Manages notification tone selection, import, conversion, and deletion.
struct NotificationToneManager: NotificationToneManagerProtocol {
nonisolated struct NotificationToneManager: NotificationToneManagerProtocol {
@globalActor
actor ConversionActor {
static let shared = ConversionActor()
@@ -44,7 +44,7 @@ struct NotificationToneManager: NotificationToneManagerProtocol {
static let selectedToneFilename = "currentAlert.caf"
/// Directory where user-imported custom tones are stored.
static let libraryLocation = URL.libraryDirectory.appending(components: "Sounds", "AvailableSounds", directoryHint: .isDirectory)
nonisolated static let libraryLocation = URL.libraryDirectory.appending(components: "Sounds", "AvailableSounds", directoryHint: .isDirectory)
/// Creates the manager and ensures required library directories exist.
init(appSettings: AppSettings) {
@@ -8,7 +8,7 @@
import Foundation
// sourcery: AutoMockable
protocol NotificationToneManagerProtocol {
nonisolated protocol NotificationToneManagerProtocol: Sendable {
@discardableResult
func setSelectedTone(_ alertTone: NotificationTone) throws -> URL
func customTones() -> [NotificationTone]
@@ -34,7 +34,6 @@ enum AppLockServiceBiometricResult {
case interrupted
}
@MainActor
protocol AppLockServiceProtocol: AnyObject {
/// The use of a PIN code is mandatory for this device.
var isMandatory: Bool { get }
@@ -9,7 +9,7 @@
import Foundation
/// A timer that adds a grace-period to the app before locking it.
class AppLockTimer {
final nonisolated class AppLockTimer {
/// The amount of time the app should remain unlocked for whilst backgrounded.
let gracePeriod: TimeInterval
@@ -12,7 +12,7 @@ import LocalAuthentication
/// It works as the actual context does and won't update the return values of `biometryType` and
/// `evaluatedPolicyDomainStateValue` until either `canEvaluatePolicy` or
/// `evaluatePolicy` have been called.
class LAContextMock: LAContext {
nonisolated class LAContextMock: LAContext {
var biometryTypeValue: LABiometryType!
private var internalBiometryTypeValue: LABiometryType!
override var biometryType: LABiometryType {
@@ -19,7 +19,7 @@ enum AudioConverterPreferredFileExtension: String {
case ogg
}
struct AudioConverter: AudioConverterProtocol {
nonisolated struct AudioConverter: AudioConverterProtocol {
func convertToOpusOgg(sourceURL: URL, destinationURL: URL) throws {
do {
try OGGConverter.convertM4aFileToOpusOGG(src: sourceURL, dest: destinationURL)
@@ -8,7 +8,7 @@
import Foundation
protocol AudioConverterProtocol: Sendable {
nonisolated protocol AudioConverterProtocol: Sendable {
func convertToOpusOgg(sourceURL: URL, destinationURL: URL) throws
func convertToMPEG4AAC(sourceURL: URL, destinationURL: URL) throws
}
@@ -8,14 +8,14 @@
import AVFoundation
protocol AudioSessionProtocol: AnyObject {
nonisolated protocol AudioSessionProtocol: AnyObject, Sendable {
func requestRecordPermission(_ response: @escaping (Bool) -> Void)
func setAllowHapticsAndSystemSoundsDuringRecording(_ inValue: Bool) throws
func setCategory(_ category: AVAudioSession.Category, mode: AVAudioSession.Mode, options: AVAudioSession.CategoryOptions) throws
func setActive(_ active: Bool, options: AVAudioSession.SetActiveOptions) throws
}
extension AudioSessionProtocol {
nonisolated extension AudioSessionProtocol {
func setActive(_ active: Bool) throws {
try setActive(active, options: [])
}
@@ -24,4 +24,4 @@ extension AudioSessionProtocol {
// sourcery: AutoMockable
extension AudioSessionProtocol { }
extension AVAudioSession: AudioSessionProtocol { }
nonisolated extension AVAudioSession: AudioSessionProtocol { }
@@ -82,7 +82,7 @@ class AudioPlayer: NSObject, AudioPlayerProtocol {
private var isStopped = true
deinit {
isolated deinit {
deinitInProgress = true
stop()
unloadContent()
@@ -184,31 +184,40 @@ class AudioPlayer: NSObject, AudioPlayerProtocol {
return
}
statusObserver = playerItem.observe(\.status, options: [.old, .new]) { [weak self] _, _ in
guard let self else { return }
switch playerItem.status {
case .failed:
setInternalState(.error(playerItem.error ?? AudioPlayerError.genericError))
case .readyToPlay:
guard state == .loading else { return }
setInternalState(.readyToPlay)
default:
break
statusObserver = playerItem.observe(\.status, options: [.old, .new]) { [weak self] item, _ in
// KVO fires on an arbitrary thread, hop to the main actor with the values we need.
let status = item.status
let error = item.error
Task { @MainActor [weak self] in
guard let self else { return }
switch status {
case .failed:
setInternalState(.error(error ?? AudioPlayerError.genericError))
case .readyToPlay:
guard state == .loading else { return }
setInternalState(.readyToPlay)
default:
break
}
}
}
rateObserver = internalAudioPlayer.observe(\.rate, options: [.old, .new]) { [weak self] _, _ in
guard let self else { return }
if internalAudioPlayer.rate == 0 {
if isStopped {
setInternalState(.stopped)
rateObserver = internalAudioPlayer.observe(\.rate, options: [.old, .new]) { [weak self] player, _ in
// KVO fires on an arbitrary thread, hop to the main actor with the values we need.
let rate = player.rate
Task { @MainActor [weak self] in
guard let self else { return }
if rate == 0 {
if isStopped {
setInternalState(.stopped)
} else {
setInternalState(.paused)
}
} else {
setInternalState(.paused)
setInternalState(.playing)
}
} else {
setInternalState(.playing)
}
}
@@ -16,7 +16,7 @@ enum AudioPlayerError: Error {
/// There used to be a MediaPlayerProtocol that AudioPlayerProtocol inherited from.
/// This should be called something else but we already have an AudioPlayerState,
/// AudioPlayerPlaybackState and InternalAudioPlayerState so who knows what to call this.
enum MediaPlayerState {
nonisolated enum MediaPlayerState {
case loading
case playing
case paused
@@ -24,7 +24,6 @@ enum AudioPlayerStateIdentifier {
case recorderPreview
}
@MainActor
class AudioPlayerState: ObservableObject, Identifiable {
let id: AudioPlayerStateIdentifier
let title: String
@@ -12,14 +12,16 @@ import Combine
import Foundation
import UIKit
private enum InternalAudioRecorderState: Equatable {
private nonisolated enum InternalAudioRecorderState: Equatable {
case recording
case suspended
case stopped
case error(AudioRecorderError)
}
class AudioRecorder: AudioRecorderProtocol {
/// All mutable state is confined to the serial `dispatchQueue` (with the realtime tap
/// reading converter/file that are set up before the engine starts), hence `@unchecked`.
nonisolated class AudioRecorder: AudioRecorderProtocol, @unchecked Sendable {
private let audioSession: AudioSessionProtocol
private var audioEngine: AVAudioEngine?
private var mixer: AVAudioMixerNode?
@@ -143,7 +145,7 @@ class AudioRecorder: AudioRecorderProtocol {
return try AVAudioFile(forWriting: recordingURL, settings: settings)
}
private func startRecording(audioFileURL: URL, completion: @escaping (Result<Void, AudioRecorderError>) -> Void) {
private func startRecording(audioFileURL: URL, completion: @escaping @Sendable (Result<Void, AudioRecorderError>) -> Void) {
dispatchQueue.async { [weak self] in
guard let self, !self.stopped else {
completion(.failure(.recordingCancelled))
@@ -220,7 +222,7 @@ class AudioRecorder: AudioRecorderProtocol {
}
}
private func stopRecording(completion: @escaping () -> Void) {
private func stopRecording(completion: @escaping @Sendable () -> Void) {
dispatchQueue.async { [weak self] in
defer {
completion()
@@ -247,7 +249,7 @@ class AudioRecorder: AudioRecorderProtocol {
releaseAudioSession()
}
private func deleteRecording(completion: @escaping () -> Void) {
private func deleteRecording(completion: @escaping @Sendable () -> Void) {
dispatchQueue.async { [weak self] in
defer {
completion()
@@ -287,9 +289,12 @@ class AudioRecorder: AudioRecorderProtocol {
}
// Convert the buffer
// The block type is `@Sendable` but `convert(to:error:withInputFrom:)`
// invokes it synchronously on this same thread, the buffer never crosses.
nonisolated(unsafe) let inputBufferForConversion = buffer
let inputBlock: AVAudioConverterInputBlock = { _, outStatus in
outStatus.pointee = AVAudioConverterInputStatus.haveData
return buffer
return inputBufferForConversion
}
var conversionError: NSError?
@@ -9,7 +9,7 @@
import Combine
import Foundation
enum AudioRecorderError: Error, Equatable {
nonisolated enum AudioRecorderError: Error, Equatable {
case unsupportedAudioFormat
case audioSessionFailure
case audioEngineFailure
@@ -19,13 +19,13 @@ enum AudioRecorderError: Error, Equatable {
case recordPermissionNotGranted
}
enum AudioRecorderAction {
nonisolated enum AudioRecorderAction {
case didStartRecording
case didStopRecording
case didFailWithError(error: AudioRecorderError)
}
protocol AudioRecorderProtocol: AnyObject {
nonisolated protocol AudioRecorderProtocol: AnyObject, Sendable {
var actions: AnyPublisher<AudioRecorderAction, Never> { get }
var currentTime: TimeInterval { get }
var isRecording: Bool { get }
@@ -16,7 +16,6 @@ enum AudioRecorderRecordingState {
case error
}
@MainActor
class AudioRecorderState: ObservableObject, Identifiable {
let id = UUID()
@@ -198,8 +198,10 @@ class AuthenticationService: AuthenticationServiceProtocol {
// n.b. We deliberatley don't check whether the received server is in our appSettings.accountProviders
let listener = SDKListener { progress in
guard let progress = QRLoginProgress(rustProgress: progress) else { return }
// The SDK calls the listener from arbitrary threads; onMainActor forwards the progress
// updates on the main actor in FIFO order.
let listener = SDKListener.onMainActor { rustProgress in
guard let progress = QRLoginProgress(rustProgress: rustProgress) else { return }
progressSubject.send(progress)
}
@@ -52,11 +52,11 @@ struct ClassicAppAccount: Equatable, CustomStringConvertible {
final class ClassicAppMXAccount: NSObject, NSCoding {
/// The obtained user ID.
var userID: String
let userID: String
/// The access token to create a MXRestClient.
var accessToken: String
let accessToken: String
/// The homeserver url (ex: "https://matrix.org").
var homeserverURL: URL
let homeserverURL: URL
/// Disable the account without logging out (NO by default).
///
@@ -72,7 +72,7 @@ final class ClassicAppMXAccount: NSObject, NSCoding {
}
/// Override the existing `CustomStringConvertible` conformance.
override var description: String {
override nonisolated var description: String {
"ClassicAppMXAccount(userID: \(userID), homeserverURL: \(homeserverURL), isDisabled: \(isDisabled), isSoftLogout: \(isSoftLogout))"
}
@@ -8,7 +8,7 @@
import SwiftUI
/// A media provider that can download a `ClassicAppAccount`'s avatar image.
class ClassicAppMediaLoader: MediaLoaderProtocol {
final class ClassicAppMediaLoader: MediaLoaderProtocol {
let classicAppAccount: ClassicAppAccount
let urlSession: URLSession
@@ -52,16 +52,29 @@ class LinkNewDeviceService: LinkNewDeviceServiceProtocol {
/// Links a new device by showing it a QR code.
func linkMobileDevice() -> LinkMobileProgressPublisher {
let progressSubject = CurrentValueSubject<LinkMobileProgress, QRCodeLoginError>(.starting)
// Bridge from the SDK's synchronous callback into Swift Concurrency. Yielding is safe from
// any thread; the single long-lived `for await` consumer forwards the progress updates on
// the main actor in FIFO order.
let (progressStream, progressContinuation) = AsyncStream<GrantGeneratedQrLoginProgress>.makeStream()
let listener = SDKListener {
do {
try progressSubject.send(.init(rustProgress: $0))
} catch {
MXLog.error("Invalid GenerateProgress")
progressSubject.send(completion: .failure(.unknown))
progressContinuation.yield($0)
}
Task {
for await rustProgress in progressStream {
do {
try progressSubject.send(.init(rustProgress: rustProgress))
} catch {
MXLog.error("Invalid GenerateProgress")
progressSubject.send(completion: .failure(.unknown))
}
}
}
Task {
defer { progressContinuation.finish() }
do {
// Note: The SDK doesn't provide us with a way to cancel the grant if the user hit the cancel button 🤷‍♂️
try await grantLoginHandler.generate(progressListener: listener) // The success state is handled by the listener.
@@ -82,12 +95,22 @@ class LinkNewDeviceService: LinkNewDeviceServiceProtocol {
/// Links a new device using a QR code generated by said device.
func linkDesktopDevice(with scannedQRData: Data) -> LinkDesktopProgressPublisher {
let progressSubject = CurrentValueSubject<LinkDesktopProgress, QRCodeLoginError>(.starting)
// Same callback bridging as `linkMobileDevice()` above.
let (progressStream, progressContinuation) = AsyncStream<GrantQrLoginProgress>.makeStream()
let listener = SDKListener {
do {
try progressSubject.send(.init(rustProgress: $0))
} catch {
MXLog.error("Invalid ScanProgress")
progressSubject.send(completion: .failure(.unknown))
progressContinuation.yield($0)
}
Task {
for await rustProgress in progressStream {
do {
try progressSubject.send(.init(rustProgress: rustProgress))
} catch {
MXLog.error("Invalid ScanProgress")
progressSubject.send(completion: .failure(.unknown))
}
}
}
@@ -97,6 +120,7 @@ class LinkNewDeviceService: LinkNewDeviceServiceProtocol {
} catch {
MXLog.error("QR code decode error: \(error)")
progressSubject.send(completion: .failure(.invalidQRCode))
progressContinuation.finish()
return progressSubject.asCurrentValuePublisher()
}
@@ -104,6 +128,7 @@ class LinkNewDeviceService: LinkNewDeviceServiceProtocol {
// Note the equivalent check will also happen for sign in with QR code in the AuthenticationService.
Task {
defer { progressContinuation.finish() }
do {
// Note: The SDK doesn't provide us with a way to cancel the grant if the user hit the cancel button 🤷‍♂️
try await grantLoginHandler.scan(qrCodeData: qrCodeData, progressListener: listener) // The success state is handled by the listener.
@@ -215,7 +240,7 @@ private extension QRCodeLoginError {
}
}
class CheckCodeSenderProxy: Equatable {
nonisolated class CheckCodeSenderProxy: Equatable {
static func == (lhs: CheckCodeSenderProxy, rhs: CheckCodeSenderProxy) -> Bool {
lhs.underlyingSender === rhs.underlyingSender
}
@@ -289,12 +289,14 @@ private enum MultipartFormDataType {
case file(url: URL)
}
extension BugReportService: URLSessionTaskDelegate {
nonisolated extension BugReportService: URLSessionTaskDelegate {
/// URLSession calls its delegate on a background queue, hop to the main actor
/// where the service lives.
func urlSession(_ session: URLSession, didCreateTask task: URLSessionTask) {
task.progress.publisher(for: \.fractionCompleted)
.sink { [weak self] value in
Task { @MainActor [weak self] in
for await value in task.progress.publisher(for: \.fractionCompleted).values {
self?.progressSubject.send(value)
}
.store(in: &cancellables)
}
}
}
@@ -10,7 +10,7 @@ import Combine
import Foundation
import UIKit
struct BugReport: Equatable {
nonisolated struct BugReport: Equatable {
let userID: String?
let deviceID: String?
let ed25519: String?
@@ -247,8 +247,11 @@ class ClientProxy: ClientProxyProtocol {
roomListStateLoadingStateUpdateTaskHandle = createRoomListLoadingStateUpdateObserver(roomListService)
delegateHandle = try client.setDelegate(delegate: ClientDelegateWrapper { [weak self] isSoftLogout in
self?.hasEncounteredAuthError = true
self?.actionsSubject.send(.receivedAuthError(isSoftLogout: isSoftLogout))
// Called by the SDK from arbitrary threads, hop to the main actor where the proxy lives.
Task { @MainActor in
self?.hasEncounteredAuthError = true
self?.actionsSubject.send(.receivedAuthError(isSoftLogout: isSoftLogout))
}
} backgroundTaskErrorCallback: { error in
switch error {
case .panic(let message, let backtrace):
@@ -628,7 +631,8 @@ class ClientProxy: ClientProxyProtocol {
}
if !staticRoomSummaryProvider.statePublisher.value.isLoaded {
_ = await staticRoomSummaryProvider.statePublisher.values.first { $0.isLoaded }
var iterator = staticRoomSummaryProvider.statePublisher.values.makeAsyncIterator()
while let state = await iterator.next(isolation: #isolation), !state.isLoaded { }
}
if shouldAwait {
@@ -904,7 +908,7 @@ class ClientProxy: ClientProxyProtocol {
}
}
func recentlyVisitedRooms(filter: (JoinedRoomProxyProtocol) -> Bool) async -> [JoinedRoomProxyProtocol] {
func recentlyVisitedRooms(filter: @Sendable (JoinedRoomProxyProtocol) -> Bool) async -> [JoinedRoomProxyProtocol] {
let maxResultsToReturn = 5
guard case let .success(roomIdentifiers) = await recentlyVisitedRoomIDs() else {
@@ -1005,7 +1009,7 @@ class ClientProxy: ClientProxyProtocol {
}
.store(in: &cancellables)
ignoredUsersListenerTaskHandle = client.subscribeToIgnoredUsers(listener: SDKListener { [weak self] ignoredUsers in
ignoredUsersListenerTaskHandle = client.subscribeToIgnoredUsers(listener: SDKListener.onMainActor { [weak self] ignoredUsers in
self?.ignoredUsersSubject.send(ignoredUsers)
})
@@ -1014,12 +1018,12 @@ class ClientProxy: ClientProxyProtocol {
Task { await self?.updateVerificationState(verificationState) }
})
sendQueueStatusListenerTaskHandle = client.subscribeToSendQueueStatus(listener: SDKListener { [weak self] roomID, error in
sendQueueStatusListenerTaskHandle = client.subscribeToSendQueueStatus(listener: SDKListener.onMainActor { [weak self] roomID, error in
MXLog.error("Send queue failed in room: \(roomID) with error: \(error)")
self?.sendQueueStatusSubject.send(false)
})
sendQueueUpdatesListenerTaskHandle = try? await client.subscribeToSendQueueUpdates(listener: SDKListener { [analyticsService] _, update in
sendQueueUpdatesListenerTaskHandle = try? await client.subscribeToSendQueueUpdates(listener: SDKListener.onMainActor { [analyticsService] _, update in
switch update {
case .newLocalEvent(let transactionID):
analyticsService.signpost.startTransaction(.sendMessage(uuid: transactionID))
@@ -1172,7 +1176,7 @@ class ClientProxy: ClientProxyProtocol {
}
private func createSyncServiceStateObserver(_ syncService: SyncService) -> TaskHandle {
syncService.state(listener: SDKListener { [weak self] state in
syncService.state(listener: SDKListener.onMainActor { [weak self] state in
guard let self else { return }
MXLog.info("Received sync service update: \(state)")
@@ -1190,7 +1194,7 @@ class ClientProxy: ClientProxyProtocol {
private func createMediaPreviewConfigObserver() async -> TaskHandle? {
do {
return try await client.subscribeToMediaPreviewConfig(listener: SDKListener { [weak self] config in
return try await client.subscribeToMediaPreviewConfig(listener: SDKListener.onMainActor { [weak self] config in
guard let self else { return }
if let config {
@@ -1210,7 +1214,7 @@ class ClientProxy: ClientProxyProtocol {
private func createLiveLocationOwnInfoUpdatesObserver() -> TaskHandle? {
do {
return try client.subscribeToOwnBeaconInfoUpdates(listener: SDKListener { [weak self] update in
return try client.subscribeToOwnBeaconInfoUpdates(listener: SDKListener.onMainActor { [weak self] update in
guard let self else { return }
let appUpdate = LiveLocationOwnInfoUpdate(roomID: update.roomId,
eventID: update.eventId,
@@ -1224,7 +1228,7 @@ class ClientProxy: ClientProxyProtocol {
}
private func createRoomListServiceObserver(_ roomListService: RoomListService) -> TaskHandle {
roomListService.state(listener: SDKListener { [weak self] state in
roomListService.state(listener: SDKListener.onMainActor { [weak self] state in
guard let self else { return }
MXLog.info("Received room list update: \(state)")
@@ -1246,7 +1250,7 @@ class ClientProxy: ClientProxyProtocol {
}
private func createRoomListLoadingStateUpdateObserver(_ roomListService: RoomListService) -> TaskHandle {
roomListService.syncIndicator(delayBeforeShowingInMs: 1000, delayBeforeHidingInMs: 0, listener: SDKListener { [weak self] state in
roomListService.syncIndicator(delayBeforeShowingInMs: 1000, delayBeforeHidingInMs: 0, listener: SDKListener.onMainActor { [weak self] state in
guard let self else { return }
switch state {
@@ -260,7 +260,7 @@ protocol ClientProxyProtocol: AnyObject {
func trackRecentlyVisitedRoom(_ roomID: String) async -> Result<Void, ClientProxyError>
func recentlyVisitedRooms(filter: (JoinedRoomProxyProtocol) -> Bool) async -> [JoinedRoomProxyProtocol]
func recentlyVisitedRooms(filter: @Sendable (JoinedRoomProxyProtocol) -> Bool) async -> [JoinedRoomProxyProtocol]
func recentConversationCounterparts() async -> [UserProfileProxy]
// MARK: - Crypto
@@ -220,14 +220,19 @@ class ElementCallService: NSObject, ElementCallServiceProtocol, PKPushRegistryDe
// https://stackoverflow.com/a/41230020/730924
update.remoteHandle = .init(type: .generic, value: roomID)
// The push registry uses the main queue so the completion is main actor bound,
// whereas CallKit invokes its completion on a background queue.
let mainActorCompletion = { @MainActor @Sendable in completion() }
callProvider.reportNewIncomingCall(with: callID.callKitID, update: update) { [weak self] error in
if let error {
MXLog.error("Failed reporting new incoming call with error: \(error)")
}
self?.actionsSubject.send(.receivedIncomingCallRequest)
completion()
Task { @MainActor in
self?.actionsSubject.send(.receivedIncomingCallRequest)
mainActorCompletion()
}
}
endUnansweredCallTask = Task { [weak self] in
@@ -351,8 +356,6 @@ class ElementCallService: NSObject, ElementCallServiceProtocol, PKPushRegistryDe
private func reportImmediatelyEndedCall(reason: CXCallEndedReason,
callerInfo: (roomID: String, roomDisplayName: String?)? = nil,
completion: @escaping () -> Void) {
let provider = callProvider
let callID = UUID()
let update = CXCallUpdate()
// Never answered through CallKit, so the hasVideo workaround for #5335 isn't needed.
@@ -363,14 +366,18 @@ class ElementCallService: NSObject, ElementCallServiceProtocol, PKPushRegistryDe
update.remoteHandle = .init(type: .generic, value: callerInfo.roomID)
}
provider.reportNewIncomingCall(with: callID, update: update) { error in
// CallKit invokes the completion on a background queue, so hop back to the main actor
// to report the ended call and to run the (main actor bound) completion.
let mainActorCompletion = { @MainActor @Sendable in completion() }
callProvider.reportNewIncomingCall(with: callID, update: update) { [weak self] error in
if let error {
MXLog.error("Failed reporting immediately ended call with error: \(error)")
}
provider.reportCall(with: callID, endedAt: nil, reason: reason)
completion()
Task { @MainActor in
self?.callProvider.reportCall(with: callID, endedAt: nil, reason: reason)
mainActorCompletion()
}
}
}
@@ -443,7 +450,7 @@ class ElementCallService: NSObject, ElementCallServiceProtocol, PKPushRegistryDe
MXLog.info("Observe decline events for notification \(rtcNotificationID)")
let listener: CallDeclineListener = SDKListener { [weak self] senderID in
let listener: CallDeclineListener = SDKListener.onMainActor { [weak self] senderID in
guard let self else { return }
MXLog.debug("Call declined event received from \(senderID)")
@@ -8,7 +8,7 @@
import Foundation
enum ElementCallServiceNotificationKey: String {
nonisolated enum ElementCallServiceNotificationKey: String {
case roomID
case roomDisplayName
/// When an incoming call is set to ring, there will be a `m.rtc.notification`event (MSC4075).
@@ -19,4 +19,4 @@ enum ElementCallServiceNotificationKey: String {
case isVoiceCall
}
let ElementCallServiceNotificationDiscardDelta = 15.0
nonisolated let ElementCallServiceNotificationDiscardDelta = 15.0
@@ -129,7 +129,7 @@ final class ElementCallWidgetDriver: WidgetCapabilitiesProvider, ElementCallWidg
self.widgetDriver = widgetDriver
Task.detached { [weak self, widgetDriver, messagePublisher] in
Task.detached { [weak self, widgetDriver] in
MXLog.debug("Started message receiving loop")
defer {
@@ -141,10 +141,9 @@ final class ElementCallWidgetDriver: WidgetCapabilitiesProvider, ElementCallWidg
return
}
messagePublisher.send(receivedMessage)
MXLog.debug("Received message: \(receivedMessage)")
self?.handleMessageIfNeeded(receivedMessage)
await self?.receiveMessage(receivedMessage)
}
}
@@ -177,12 +176,18 @@ final class ElementCallWidgetDriver: WidgetCapabilitiesProvider, ElementCallWidg
// MARK: - WidgetCapabilitiesProvider
func acquireCapabilities(capabilities: WidgetCapabilities) -> WidgetCapabilities {
/// Called by the SDK from arbitrary threads, only touches Sendable state.
nonisolated func acquireCapabilities(capabilities: WidgetCapabilities) -> WidgetCapabilities {
getElementCallRequiredPermissions(ownUserId: room.ownUserId(), ownDeviceId: deviceID)
}
// MARK: - Private
private func receiveMessage(_ message: String) {
messagePublisher.send(message)
handleMessageIfNeeded(message)
}
func handleMessageIfNeeded(_ message: String) {
guard let data = message.data(using: .utf8) else {
return
@@ -6,7 +6,7 @@
// Please see LICENSE files in the repository root for full details.
//
import Emojibase
@preconcurrency import Emojibase
import Foundation
class EmojiProvider: EmojiProviderProtocol {
@@ -114,7 +114,7 @@ class EmojiProvider: EmojiProviderProtocol {
}
}
extension EmojibaseDatasource: EmojiLoaderProtocol {
nonisolated extension EmojibaseDatasource: EmojiLoaderProtocol {
func load() async -> [EmojiCategory] {
do {
let store: EmojibaseStore = try await load()
@@ -32,7 +32,6 @@ enum EmojiProviderState {
case loaded([EmojiCategory])
}
@MainActor
protocol EmojiProviderProtocol {
var state: EmojiProviderState { get }
@@ -10,7 +10,7 @@ import Foundation
@preconcurrency import KeychainAccess
import MatrixRustSDK
enum KeychainControllerService: String {
nonisolated enum KeychainControllerService: String {
case sessions
case tests
@@ -23,7 +23,9 @@ enum KeychainControllerService: String {
}
}
final class KeychainController: KeychainControllerProtocol {
/// The SDK calls the `ClientSessionDelegate` methods from arbitrary threads and the
/// underlying `Keychain` is thread-safe, so the controller doesn't need an actor.
final nonisolated class KeychainController: KeychainControllerProtocol {
/// The keychain responsible for storing account restoration tokens (keyed by userID).
private let restorationTokenKeychain: Keychain
/// The keychain responsible for storing all other secrets in the app (keyed by `Key`s).
@@ -15,7 +15,7 @@ struct KeychainCredentials {
}
// sourcery: AutoMockable
protocol KeychainControllerProtocol: ClientSessionDelegate {
nonisolated protocol KeychainControllerProtocol: ClientSessionDelegate {
// MARK: Restoration Tokens
func setRestorationToken(_ restorationToken: RestorationToken, forUsername: String)
@@ -41,7 +41,6 @@ class LiveLocationManager: NSObject, LiveLocationManagerProtocol, CLLocationMana
private var lastLocation: CLLocationCoordinate2D?
@MainActor
init(clientProxy: ClientProxyProtocol,
appSettings: AppSettings,
locationManager: @autoclosure @MainActor () -> CLLocationManagerProtocol = CLLocationManager()) {
@@ -20,15 +20,34 @@ final class RoomLiveLocationService: RoomLiveLocationServiceProtocol {
liveLocationsSubject.asCurrentValuePublisher()
}
/// 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 updates on the
/// main actor in FIFO order, guaranteeing one in-flight update at a time.
private let updatesContinuation: AsyncStream<[LiveLocationShareUpdate]>.Continuation
deinit {
updatesContinuation.finish()
}
init(liveLocationsObserver: LiveLocationsObserver) {
self.liveLocationsObserver = liveLocationsObserver
observationToken = liveLocationsObserver
.subscribe(listener: SDKListener { [weak self] updates in
let (updatesStream, updatesContinuation) = AsyncStream<[LiveLocationShareUpdate]>.makeStream()
self.updatesContinuation = updatesContinuation
Task { [weak self] in
for await updates in updatesStream {
guard let self else { return }
MXLog.info("Received live location shares update")
let updatedShares = handleLiveLocationShareUpdates(updates)
liveLocationsSubject.send(updatedShares)
}
}
observationToken = liveLocationsObserver
.subscribe(listener: SDKListener { updates in
updatesContinuation.yield(updates)
})
}
@@ -9,6 +9,6 @@ import Combine
import Foundation
// sourcery: AutoMockable
protocol RoomLiveLocationServiceProtocol {
protocol RoomLiveLocationServiceProtocol: Sendable {
var liveLocationsPublisher: CurrentValuePublisher<[LiveLocationShare], Never> { get }
}
@@ -12,7 +12,7 @@ import Foundation
/// A wrapper around Rust's `MediaFileHandle` type that provides us with a
/// media file that is stored unencrypted in a temporary location for previewing.
final class MediaFileHandleProxy: Sendable {
final nonisolated class MediaFileHandleProxy: Sendable {
/// The underlying handle for the file.
private let handle: MediaFileHandleProtocol
@@ -12,7 +12,7 @@ import UIKit
@preconcurrency import MatrixRustSDK
private final class MediaRequest {
private final nonisolated class MediaRequest {
var continuations: [CheckedContinuation<Data, Error>] = []
}
@@ -16,7 +16,7 @@ enum MediaLoaderError: Error {
}
// sourcery: AutoMockable
protocol MediaLoaderProtocol: Sendable {
nonisolated protocol MediaLoaderProtocol: Sendable {
func loadMediaContentForSource(_ source: MediaSourceProxy) async throws -> Data
func loadMediaThumbnailForSource(_ source: MediaSourceProxy, width: UInt, height: UInt) async throws -> Data
@@ -10,7 +10,7 @@ import Combine
import Kingfisher
import UIKit
struct MediaProvider: MediaProviderProtocol {
nonisolated struct MediaProvider: MediaProviderProtocol {
private let mediaLoader: MediaLoaderProtocol
private let imageCache: Kingfisher.ImageCache
private let homeserverReachabilityPublisher: CurrentValuePublisher<NetworkMonitorReachability, Never>?
@@ -18,7 +18,7 @@ enum MediaProviderError: Error {
}
// sourcery: AutoMockable
protocol MediaProviderProtocol {
nonisolated protocol MediaProviderProtocol: Sendable {
func imageFromSource(_ source: MediaSourceProxy?, size: CGSize?) -> UIImage?
func loadImageFromSource(_ source: MediaSourceProxy, size: CGSize?) async -> Result<UIImage, MediaProviderError>
func loadImageDataFromSource(_ source: MediaSourceProxy) async -> Result<Data, MediaProviderError>
@@ -9,7 +9,7 @@
import Foundation
@preconcurrency import MatrixRustSDK
struct MediaSourceProxy: Hashable {
nonisolated struct MediaSourceProxy: Hashable {
/// The media source provided by Rust.
let underlyingSource: MediaSource
/// The media's mime type, used when loading the media's file.
@@ -12,7 +12,6 @@ enum MediaPlayerProviderError: Error {
case unsupportedMediaType
}
@MainActor
protocol MediaPlayerProviderProtocol {
var player: AudioPlayerProtocol { get }
@@ -169,15 +169,15 @@ final class NotificationManager: NSObject, NotificationManagerProtocol {
locArgs: [])),
pusherNotificationClientIdentifier: clientProxy.pusherNotificationClientIdentifier)
let configuration = try await PusherConfiguration(identifiers: .init(pushkey: deviceToken.base64EncodedString(),
appId: appSettings.pusherAppID),
kind: .http(data: .init(url: appSettings.pushGatewayNotifyEndpoint.absoluteString,
format: .eventIdOnly,
defaultPayload: defaultPayload.toJsonString())),
appDisplayName: "\(InfoPlistReader.main.bundleDisplayName) (iOS)",
deviceDisplayName: UIDevice.current.name,
profileTag: pusherProfileTag(),
lang: Bundle.app.preferredLocalizations.first ?? "en")
let configuration = try PusherConfiguration(identifiers: .init(pushkey: deviceToken.base64EncodedString(),
appId: appSettings.pusherAppID),
kind: .http(data: .init(url: appSettings.pushGatewayNotifyEndpoint.absoluteString,
format: .eventIdOnly,
defaultPayload: defaultPayload.toJsonString())),
appDisplayName: "\(InfoPlistReader.main.bundleDisplayName) (iOS)",
deviceDisplayName: UIDevice.current.name,
profileTag: pusherProfileTag(),
lang: Bundle.app.preferredLocalizations.first ?? "en")
try await clientProxy.setPusher(with: configuration)
MXLog.info("Set pusher succeeded")
return true
@@ -232,7 +232,6 @@ extension NotificationManager: UNUserNotificationCenterDelegate {
return [.badge, .sound, .list, .banner]
}
@MainActor
func userNotificationCenter(_ center: UNUserNotificationCenter,
didReceive response: UNNotificationResponse) async {
switch response.actionIdentifier {
@@ -8,7 +8,7 @@
import Foundation
enum NotificationConstants {
nonisolated enum NotificationConstants {
enum UserInfoKey {
static let roomIdentifier = "room_id"
static let eventIdentifier = "event_id"
@@ -11,17 +11,18 @@ import Foundation
import MatrixRustSDK
private final class WeakNotificationSettingsProxy: NotificationSettingsDelegate {
private weak var proxy: NotificationSettingsProxy?
@MainActor private weak var proxy: NotificationSettingsProxy?
init(proxy: NotificationSettingsProxy) {
@MainActor init(proxy: NotificationSettingsProxy) {
self.proxy = proxy
}
// MARK: - NotificationSettingsDelegate
/// Called by the SDK from arbitrary threads, hop to the main actor where the proxy lives.
func settingsDidChange() {
Task {
await proxy?.settingsDidChange()
Task { @MainActor in
self.proxy?.settingsDidChange()
}
}
}
@@ -119,12 +120,18 @@ final class NotificationSettingsProxy: NotificationSettingsProxyProtocol {
func updatedSettings() async {
// The timeout avoids having to wait indefinitely. This can happen when setting a mode that is already the current mode,
// as in this case no API call is made by the RustSDK and the push rules are therefore not updated.
_ = await callbacks
var iterator = callbacks
.timeout(.seconds(2.0), scheduler: DispatchQueue.main, options: nil, customError: nil)
.values.first { $0 == .settingsDidChange }
.values
.makeAsyncIterator()
while let callback = await iterator.next(isolation: #isolation) {
if callback == .settingsDidChange {
break
}
}
}
@MainActor
func settingsDidChange() {
callbacks.send(.settingsDidChange)
}
@@ -9,13 +9,13 @@
import Foundation
import MatrixRustSDK
enum RoomNotificationModeProxy: String, CaseIterable, Equatable {
nonisolated enum RoomNotificationModeProxy: String, CaseIterable, Equatable {
case allMessages
case mentionsAndKeywordsOnly
case mute
}
extension RoomNotificationModeProxy {
nonisolated extension RoomNotificationModeProxy {
static func from(roomNotificationMode: RoomNotificationMode) -> Self {
switch roomNotificationMode {
case .allMessages:
@@ -10,7 +10,7 @@ import Foundation
import MatrixRustSDK
// sourcery: AutoMockable
protocol RoomNotificationSettingsProxyProtocol {
protocol RoomNotificationSettingsProxyProtocol: Sendable {
var mode: RoomNotificationModeProxy { get }
var isDefault: Bool { get }
}
@@ -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()
}
})
}
@@ -12,7 +12,6 @@ import MatrixRustSDK
final class RoomDirectorySearchProxy: RoomDirectorySearchProxyProtocol {
private let roomDirectorySearch: RoomDirectorySearchProtocol
private let serialDispatchQueue = DispatchQueue(label: "io.element.elementx.room_directory_search_proxy", qos: .default)
private let resultsSubject = CurrentValueSubject<[RoomDirectorySearchResult], Never>([])
@@ -25,22 +24,32 @@ final class RoomDirectorySearchProxy: RoomDirectorySearchProxyProtocol {
set { resultsSubject.send(newValue) }
}
private let diffsPublisher = PassthroughSubject<[RoomDirectorySearchEntryUpdate], 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 updates on the
/// main actor in FIFO order, guaranteeing one in-flight update at a time.
private let updatesContinuation: AsyncStream<[RoomDirectorySearchEntryUpdate]>.Continuation
private var searchEntriesSubscription: TaskHandle?
private var cancellables = Set<AnyCancellable>()
deinit {
updatesContinuation.finish()
}
init(roomDirectorySearch: RoomDirectorySearchProtocol) {
self.roomDirectorySearch = roomDirectorySearch
diffsPublisher
.receive(on: serialDispatchQueue)
.sink { [weak self] in self?.updateResultsWithDiffs($0) }
.store(in: &cancellables)
let (updatesStream, updatesContinuation) = AsyncStream<[RoomDirectorySearchEntryUpdate]>.makeStream()
self.updatesContinuation = updatesContinuation
Task { [weak self] in
for await updates in updatesStream {
await self?.updateResultsWithDiffs(updates)
}
}
Task {
searchEntriesSubscription = await roomDirectorySearch.results(listener: SDKListener { [weak self] updates in
self?.diffsPublisher.send(updates)
searchEntriesSubscription = await roomDirectorySearch.results(listener: SDKListener { updates in
updatesContinuation.yield(updates)
})
}
}
@@ -63,13 +72,21 @@ final class RoomDirectorySearchProxy: RoomDirectorySearchProxyProtocol {
}
}
private func updateResultsWithDiffs(_ updates: [RoomDirectorySearchEntryUpdate]) {
results = updates.reduce(results) { currentItems, diff in
private func updateResultsWithDiffs(_ updates: [RoomDirectorySearchEntryUpdate]) async {
// Building the results and applying the CollectionDifference can be expensive for large
// search batches, so compute off the main actor and only hop back to publish.
results = await Self.updatedResults(from: updates, on: results)
}
@concurrent
private static func updatedResults(from updates: [RoomDirectorySearchEntryUpdate],
on currentResults: [RoomDirectorySearchResult]) async -> [RoomDirectorySearchResult] {
updates.reduce(currentResults) { currentItems, diff in
processDiff(diff, on: currentItems)
}
}
private func processDiff(_ diff: RoomDirectorySearchEntryUpdate, on currentItems: [RoomDirectorySearchResult]) -> [RoomDirectorySearchResult] {
private nonisolated static func processDiff(_ diff: RoomDirectorySearchEntryUpdate, on currentItems: [RoomDirectorySearchResult]) -> [RoomDirectorySearchResult] {
guard let collectionDiff = buildDiff(from: diff, on: currentItems) else {
return currentItems
}
@@ -81,42 +98,42 @@ final class RoomDirectorySearchProxy: RoomDirectorySearchProxyProtocol {
return updatedItems
}
private func buildDiff(from diff: RoomDirectorySearchEntryUpdate, on currentItems: [RoomDirectorySearchResult]) -> CollectionDifference<RoomDirectorySearchResult>? {
private nonisolated static func buildDiff(from diff: RoomDirectorySearchEntryUpdate, on currentItems: [RoomDirectorySearchResult]) -> CollectionDifference<RoomDirectorySearchResult>? {
var changes = [CollectionDifference<RoomDirectorySearchResult>.Change]()
switch diff {
case .append(let values):
for (index, value) in values.enumerated() {
let result = buildResultForRoomDescription(value)
changes.append(.insert(offset: results.count + index, element: result, associatedWith: nil))
changes.append(.insert(offset: currentItems.count + index, element: result, associatedWith: nil))
}
case .clear:
for (index, value) in results.enumerated() {
for (index, value) in currentItems.enumerated() {
changes.append(.remove(offset: index, element: value, associatedWith: nil))
}
case .insert(let index, let value):
let result = buildResultForRoomDescription(value)
changes.append(.insert(offset: Int(index), element: result, associatedWith: nil))
case .popBack:
guard let value = results.last else {
guard let value = currentItems.last else {
fatalError()
}
changes.append(.remove(offset: results.count - 1, element: value, associatedWith: nil))
changes.append(.remove(offset: currentItems.count - 1, element: value, associatedWith: nil))
case .popFront:
let result = results[0]
let result = currentItems[0]
changes.append(.remove(offset: 0, element: result, associatedWith: nil))
case .pushBack(let value):
let result = buildResultForRoomDescription(value)
changes.append(.insert(offset: results.count, element: result, associatedWith: nil))
changes.append(.insert(offset: currentItems.count, element: result, associatedWith: nil))
case .pushFront(let value):
let result = buildResultForRoomDescription(value)
changes.append(.insert(offset: 0, element: result, associatedWith: nil))
case .remove(let index):
let result = results[Int(index)]
let result = currentItems[Int(index)]
changes.append(.remove(offset: Int(index), element: result, associatedWith: nil))
case .reset(let values):
for (index, result) in results.enumerated() {
for (index, result) in currentItems.enumerated() {
changes.append(.remove(offset: index, element: result, associatedWith: nil))
}
@@ -128,7 +145,7 @@ final class RoomDirectorySearchProxy: RoomDirectorySearchProxyProtocol {
changes.append(.remove(offset: Int(index), element: result, associatedWith: nil))
changes.append(.insert(offset: Int(index), element: result, associatedWith: nil))
case .truncate(let length):
for (index, value) in results.enumerated() {
for (index, value) in currentItems.enumerated() {
if index < length {
continue
}
@@ -140,7 +157,7 @@ final class RoomDirectorySearchProxy: RoomDirectorySearchProxyProtocol {
return CollectionDifference(changes)
}
private func buildResultForRoomDescription(_ value: RoomDescription) -> RoomDirectorySearchResult {
private nonisolated static func buildResultForRoomDescription(_ value: RoomDescription) -> RoomDirectorySearchResult {
RoomDirectorySearchResult(id: value.roomId,
alias: value.alias,
name: value.name,
@@ -21,7 +21,7 @@ enum RoomDirectorySearchError: Error {
case nextPageQueryFailed
}
struct RoomDirectorySearchResult: Identifiable {
nonisolated struct RoomDirectorySearchResult: Identifiable {
let id: String
let alias: String?
let name: String?
@@ -9,6 +9,7 @@
import Combine
import Foundation
import MatrixRustSDK
import Synchronization
class SecureBackupController: SecureBackupControllerProtocol {
private let encryption: Encryption
@@ -36,7 +37,7 @@ class SecureBackupController: SecureBackupControllerProtocol {
init(encryption: Encryption) {
self.encryption = encryption
backupStateListenerTaskHandle = encryption.backupStateListener(listener: SDKListener { [weak self] state in
backupStateListenerTaskHandle = encryption.backupStateListener(listener: SDKListener.onMainActor { [weak self] state in
guard let self else { return }
switch state {
@@ -63,7 +64,7 @@ class SecureBackupController: SecureBackupControllerProtocol {
}
})
recoveryStateListenerTaskHandle = encryption.recoveryStateListener(listener: SDKListener { [weak self] state in
recoveryStateListenerTaskHandle = encryption.recoveryStateListener(listener: SDKListener.onMainActor { [weak self] state in
guard let self else { return }
switch state {
@@ -118,10 +119,10 @@ class SecureBackupController: SecureBackupControllerProtocol {
MXLog.info("Resetting recovery key")
}
var keyUploadErrored = false
let keyUploadErrored = Mutex(false)
// Note: `enableRecovery` also handles the reset case (equivalent to `resetRecoveryKey`).
// Despite the name, it is the correct call for both initial setup and key rotation.
let recoveryKey = try await encryption.enableRecovery(waitForBackupsToUpload: false, passphrase: passphrase, progressListener: SDKListener { [weak self] state in
let recoveryKey = try await encryption.enableRecovery(waitForBackupsToUpload: false, passphrase: passphrase, progressListener: SDKListener.onMainActor { [weak self] state in
guard let self else { return }
switch state {
@@ -131,11 +132,11 @@ class SecureBackupController: SecureBackupControllerProtocol {
recoveryStateSubject.send(.enabled)
case .roomKeyUploadError:
MXLog.error("Failed enabling recovery: room key upload error")
keyUploadErrored = true
keyUploadErrored.withLock { $0 = true }
}
})
return keyUploadErrored ? .failure(.failedGeneratingRecoveryKey) : .success(recoveryKey)
return keyUploadErrored.withLock { $0 } ? .failure(.failedGeneratingRecoveryKey) : .success(recoveryKey)
} catch {
MXLog.error("Failed generating recovery key with error: \(error)")
@@ -157,7 +158,7 @@ class SecureBackupController: SecureBackupControllerProtocol {
func waitForKeyBackupUpload(uploadStateSubject: CurrentValueSubject<SecureBackupSteadyState, Never>) async -> Result<Void, SecureBackupControllerError> {
do {
MXLog.info("Waiting for backup upload steady state")
try await encryption.waitForBackupUploadSteadyState(progressListener: SDKListener { state in
try await encryption.waitForBackupUploadSteadyState(progressListener: SDKListener.onMainActor { state in
let uploadState: SecureBackupSteadyState = switch state {
case .waiting: .waiting
case .uploading(let backedUpCount, let totalCount): .uploading(uploadedKeyCount: Int(backedUpCount), totalKeyCount: Int(totalCount))
@@ -9,7 +9,7 @@
import Combine
import Foundation
enum SecureBackupRecoveryState {
nonisolated enum SecureBackupRecoveryState {
case unknown
case disabled
case enabled
@@ -19,7 +19,7 @@ enum SecureBackupRecoveryState {
case settingUp
}
enum SecureBackupKeyBackupState {
nonisolated enum SecureBackupKeyBackupState {
/// Any state where backups couldn't have been enabled but we didn't explicitly disable them on this client.
/// For all intents and purposes, within the client, this can be treated as `disabled`.
case unknown
@@ -19,7 +19,7 @@ struct SessionSecurityState: Equatable {
}
// sourcery: AutoMockable
protocol UserSessionProtocol {
protocol UserSessionProtocol: Sendable {
var clientProxy: ClientProxyProtocol { get }
var mediaProvider: MediaProviderProtocol { get }
var voiceMessageMediaManager: VoiceMessageMediaManagerProtocol { get }
@@ -11,46 +11,63 @@ import Foundation
import MatrixRustSDK
private final class WeakSessionVerificationControllerProxy: SessionVerificationControllerDelegate {
private weak var proxy: SessionVerificationControllerProxy?
@MainActor private weak var proxy: SessionVerificationControllerProxy?
init(proxy: SessionVerificationControllerProxy) {
@MainActor init(proxy: SessionVerificationControllerProxy) {
self.proxy = proxy
}
// MARK: - SessionVerificationControllerDelegate
// The delegate methods are called by the SDK from arbitrary threads,
// hop to the main actor where the proxy lives.
func didReceiveVerificationRequest(details: MatrixRustSDK.SessionVerificationRequestDetails) {
proxy?.didReceiveVerificationRequest(details: details)
Task { @MainActor in
self.proxy?.didReceiveVerificationRequest(details: details)
}
}
func didReceiveVerificationData(data: MatrixRustSDK.SessionVerificationData) {
switch data {
// We can handle only emojis for now
case .emojis(let emojis, _):
proxy?.didReceiveData(emojis)
Task { @MainActor in
self.proxy?.didReceiveData(emojis)
}
default:
break
}
}
func didAcceptVerificationRequest() {
proxy?.didAcceptVerificationRequest()
Task { @MainActor in
self.proxy?.didAcceptVerificationRequest()
}
}
func didStartSasVerification() {
proxy?.didStartSasVerification()
Task { @MainActor in
self.proxy?.didStartSasVerification()
}
}
func didFail() {
proxy?.didFail()
Task { @MainActor in
self.proxy?.didFail()
}
}
func didCancel() {
proxy?.didCancel()
Task { @MainActor in
self.proxy?.didCancel()
}
}
func didFinish() {
proxy?.didFinish()
Task { @MainActor in
self.proxy?.didFinish()
}
}
}
@@ -28,27 +28,31 @@ class SpaceRoomListProxy: SpaceRoomListProxyProtocol {
spaceRoomsSubject.asCurrentValuePublisher()
}
private let paginationStateHandle: TaskHandle
let paginationStatePublisher: CurrentValuePublisher<SpaceRoomListPaginationState, Never>
private var paginationStateHandle: TaskHandle?
private let paginationStateSubject: CurrentValueSubject<SpaceRoomListPaginationState, Never>
var paginationStatePublisher: CurrentValuePublisher<SpaceRoomListPaginationState, Never> {
paginationStateSubject.asCurrentValuePublisher()
}
init(_ spaceRoomList: SpaceRoomListProtocol) async throws {
guard let spaceRoom = spaceRoomList.space() else { throw SpaceRoomListProxyError.missingSpace }
self.spaceRoomList = spaceRoomList
spaceServiceRoomSubject = .init(SpaceServiceRoom(spaceRoom: spaceRoom))
paginationStateSubject = .init(spaceRoomList.paginationState())
let paginationStateSubject = CurrentValueSubject<SpaceRoomListPaginationState, Never>(spaceRoomList.paginationState())
paginationStatePublisher = paginationStateSubject.asCurrentValuePublisher()
paginationStateHandle = spaceRoomList.subscribeToPaginationStateUpdates(listener: SDKListener { paginationState in
paginationStateSubject.send(paginationState)
// The SDK calls listeners from arbitrary threads; onMainActor applies the updates on the
// main actor in FIFO order. The subscriptions (and so the listeners) live as long as their
// handles, which are released when this proxy is deallocated.
paginationStateHandle = spaceRoomList.subscribeToPaginationStateUpdates(listener: SDKListener.onMainActor { [weak self] paginationState in
self?.paginationStateSubject.send(paginationState)
})
spaceRoomsHandle = await spaceRoomList.subscribeToRoomUpdate(listener: SDKListener { [weak self] updates in
spaceRoomsHandle = await spaceRoomList.subscribeToRoomUpdate(listener: SDKListener.onMainActor { [weak self] updates in
self?.handleUpdates(updates)
})
spaceServiceRoomHandle = spaceRoomList.subscribeToSpaceUpdates(listener: SDKListener { [weak self] spaceRoom in
spaceServiceRoomHandle = spaceRoomList.subscribeToSpaceUpdates(listener: SDKListener.onMainActor { [weak self] spaceRoom in
guard let spaceRoom else { return }
self?.spaceServiceRoomSubject.send(SpaceServiceRoom(spaceRoom: spaceRoom))
})
@@ -14,7 +14,7 @@ enum SpaceRoomListProxyError: Error {
}
// sourcery: AutoMockable
protocol SpaceRoomListProxyProtocol {
protocol SpaceRoomListProxyProtocol: Sendable {
var id: String { get }
var spaceServiceRoomPublisher: CurrentValuePublisher<SpaceServiceRoom, Never> { get }
@@ -25,19 +25,48 @@ class SpaceServiceProxy: SpaceServiceProxyProtocol {
spaceFilterSubject.asCurrentValuePublisher()
}
// Bridges from the SDK's synchronous callbacks into Swift Concurrency. Yielding is safe from
// any thread; single long-lived `for await` consumers (set up in `init`) apply the updates on
// the main actor in FIFO order, guaranteeing one in-flight update at a time.
private let spaceListUpdatesContinuation: AsyncStream<[SpaceListUpdate]>.Continuation
private let spaceFilterUpdatesContinuation: AsyncStream<[SpaceFilterUpdate]>.Continuation
deinit {
spaceListUpdatesContinuation.finish()
spaceFilterUpdatesContinuation.finish()
}
init(spaceService: SpaceServiceProtocol) {
self.spaceService = spaceService
let (spaceListUpdates, spaceListUpdatesContinuation) = AsyncStream<[SpaceListUpdate]>.makeStream()
self.spaceListUpdatesContinuation = spaceListUpdatesContinuation
let (spaceFilterUpdates, spaceFilterUpdatesContinuation) = AsyncStream<[SpaceFilterUpdate]>.makeStream()
self.spaceFilterUpdatesContinuation = spaceFilterUpdatesContinuation
Task { [weak self] in
for await updates in spaceListUpdates {
self?.handleSpaceListUpdates(updates)
}
}
Task { [weak self] in
for await updates in spaceFilterUpdates {
self?.handleSpaceFilterUpdates(updates)
}
}
Task { await setupSubscriptions() }
}
private func setupSubscriptions() async {
topLevelSpacesHandle = await spaceService.subscribeToTopLevelJoinedSpaces(listener: SDKListener { [weak self] updates in
self?.handleSpaceListUpdates(updates)
topLevelSpacesHandle = await spaceService.subscribeToTopLevelJoinedSpaces(listener: SDKListener { [spaceListUpdatesContinuation] updates in
spaceListUpdatesContinuation.yield(updates)
})
spaceFilterHandle = await spaceService.subscribeToSpaceFilters(listener: SDKListener { [weak self] updates in
self?.handleSpaceFilterUpdates(updates)
spaceFilterHandle = await spaceService.subscribeToSpaceFilters(listener: SDKListener { [spaceFilterUpdatesContinuation] updates in
spaceFilterUpdatesContinuation.yield(updates)
})
}
@@ -10,7 +10,7 @@ import CoreLocation
import Foundation
/// A structure that parses a geo URI (i.e. geo:53.99803101552848,-8.25347900390625;u=10) and constructs their constituent parts.
struct GeoURI: Hashable {
nonisolated struct GeoURI: Hashable {
// MARK: - Properties
let latitude: Double
@@ -69,8 +69,11 @@ struct GeoURI: Hashable {
// swiftlint:disable:next large_tuple
private typealias RegexGeoURI = Regex<(Substring, latitude: Substring, longitude: Substring, uncertainty: Substring?)>
private extension RegexGeoURI {
static let standard: Self = /geo:(?<latitude>-?\d+(?:\.\d+)?),(?<longitude>-?\d+(?:\.\d+)?)(?:,-?\d+(?:\.\d+)?)?(?:;u=(?<uncertainty>\d+(?:\.\d+)?))?/
private nonisolated extension RegexGeoURI {
/// A computed property as `Regex` isn't Sendable.
static var standard: Self {
/geo:(?<latitude>-?\d+(?:\.\d+)?),(?<longitude>-?\d+(?:\.\d+)?)(?:,-?\d+(?:\.\d+)?)?(?:;u=(?<uncertainty>\d+(?:\.\d+)?))?/
}
}
extension GeoURI {
@@ -79,7 +82,7 @@ extension GeoURI {
}
}
private extension NumberFormatter {
private nonisolated extension NumberFormatter {
static let decimal: NumberFormatter = {
let numberFormatter = NumberFormatter()
numberFormatter.locale = Locale(identifier: "en_US_POSIX")
@@ -574,10 +574,14 @@ class TimelineController: TimelineControllerProtocol {
switch timelineItem.properties.replyDetails {
case .notLoaded:
activeTimeline.fetchDetails(for: eventID)
Task { @MainActor in
activeTimeline.fetchDetails(for: eventID)
}
case .error:
if refetchOnError {
activeTimeline.fetchDetails(for: eventID)
Task { @MainActor in
activeTimeline.fetchDetails(for: eventID)
}
}
default:
break
@@ -653,7 +657,7 @@ class TimelineController: TimelineControllerProtocol {
}
}
private extension TimelineItemProxy {
private nonisolated extension TimelineItemProxy {
var isItemCollapsible: Bool {
if case let .event(eventItem) = self {
switch eventItem.content {
@@ -13,7 +13,6 @@ enum TimelineFactoryControllerError: Error {
case roomProxyError(RoomProxyError)
}
@MainActor
protocol TimelineControllerFactoryProtocol {
func buildTimelineController(roomProxy: JoinedRoomProxyProtocol,
initialFocussedEventID: String?,
@@ -9,7 +9,7 @@
import Foundation
/// Represents all reactions of the same type for a single event.
struct AggregatedReaction: Hashable, Identifiable {
nonisolated struct AggregatedReaction: Hashable, Identifiable {
/// Length at which we ellipsize a reaction key for display
/// Reactions can be free text, so we need to limit the length
/// displayed on screen.
@@ -28,14 +28,14 @@ struct AggregatedReaction: Hashable, Identifiable {
}
/// Details of who sent the reaction
struct ReactionSender: Hashable, Identifiable {
nonisolated struct ReactionSender: Hashable, Identifiable {
/// The id of the user who sent the reaction
let id: String
/// The time that the reaction was received on the original homeserver
let timestamp: Date
}
extension AggregatedReaction {
nonisolated extension AggregatedReaction {
/// The number of times this reactions was sent.
var count: Int {
senders.count
@@ -13,7 +13,7 @@ import SwiftUI
/// Represents an issue with a timeline item's authenticity such as coming from an
/// unsigned session or being sent unencrypted in an encrypted room. See Rust's
/// `ShieldStateCode` for more information about the meaning of the cases.
enum EncryptionAuthenticity: Hashable {
nonisolated enum EncryptionAuthenticity: Hashable {
enum Color { case red, gray }
case notGuaranteed(color: Color)
@@ -65,7 +65,7 @@ enum EncryptionAuthenticity: Hashable {
}
}
extension EncryptionAuthenticity {
nonisolated extension EncryptionAuthenticity {
init?(shieldState: ShieldState) {
switch shieldState {
case .red(let code):
@@ -8,12 +8,12 @@
import Foundation
struct ReadReceipt: Hashable {
nonisolated struct ReadReceipt: Hashable {
let userID: String
let formattedTimestamp: String?
}
extension ReadReceipt: Identifiable {
nonisolated extension ReadReceipt: Identifiable {
var id: String {
userID
}
@@ -9,7 +9,7 @@
import Foundation
/// Properties of a matrix event that are common between all timeline items.
struct RoomTimelineItemProperties: Hashable {
nonisolated struct RoomTimelineItemProperties: Hashable {
/// Information on the item this one replying to
var replyDetails: TimelineItemReplyDetails?
/// Whether it's part of a thread or not
@@ -8,7 +8,7 @@
import Foundation
enum TimelineEventContent: Hashable {
nonisolated enum TimelineEventContent: Hashable {
case message(EventBasedMessageTimelineItemContentType)
case poll(question: String)
case liveLocation
@@ -8,7 +8,7 @@
import MatrixRustSDK
import SwiftUI
struct TimelineItemKeyForwarder: Identifiable, Hashable {
nonisolated struct TimelineItemKeyForwarder: Identifiable, Hashable {
let id: String
let displayName: String?
@@ -8,12 +8,12 @@
import Foundation
struct TimelineItemReply {
nonisolated struct TimelineItemReply {
let details: TimelineItemReplyDetails
let isThreaded: Bool
}
enum TimelineItemReplyDetails: Hashable {
nonisolated enum TimelineItemReplyDetails: Hashable {
case notLoaded(eventID: String)
case loading(eventID: String)
case loaded(sender: TimelineItemSender, eventID: String, eventContent: TimelineEventContent)
@@ -6,7 +6,7 @@
// Please see LICENSE files in the repository root for full details.
//
enum TimelineItemThreadSummary: Hashable {
nonisolated enum TimelineItemThreadSummary: Hashable {
case notLoaded
case loading
case loaded(senderID: String, sender: TimelineItemSender, latestEventContent: TimelineEventContent, numberOfReplies: Int)
@@ -14,7 +14,7 @@ import Foundation
/// Its value is consistent only per timeline instance, it should **not** be used to identify an item across timeline instances.
/// - eventOrTransactionID: Contains the 2 possible identifiers of an event, either it has a remote event id or
/// a local transaction id, never both or none.
enum TimelineItemIdentifier: Hashable {
nonisolated enum TimelineItemIdentifier: Hashable {
struct UniqueID: Hashable {
let value: String
@@ -77,7 +77,7 @@ enum TimelineItemIdentifier: Hashable {
// MARK: - Mocks
extension TimelineItemIdentifier {
nonisolated extension TimelineItemIdentifier {
static var randomEvent: Self {
.event(uniqueID: .init(UUID().uuidString), eventOrTransactionID: .eventID(UUID().uuidString))
}
@@ -68,7 +68,7 @@ class TimelineItemProvider: TimelineItemProviderProtocol {
// The stream also allows to handle the sendable items in the listener.
Task { [weak self] in
for await diffs in diffStream {
self?.updateItemsWithDiffs(diffs)
await self?.applyDiffs(diffs)
}
}
@@ -81,17 +81,34 @@ class TimelineItemProvider: TimelineItemProviderProtocol {
// MARK: - Private
private func updateItemsWithDiffs(_ diffs: [TimelineDiff]) {
let span = MXLog.createSpan("process_timeline_list_diffs:\(kind)")
private func applyDiffs(_ diffs: [TimelineDiff]) async {
MXLog.verbose("Received diffs: \(diffs)")
// Building the item proxies and computing/applying the CollectionDifference can be
// expensive, so run it off the main actor and only hop back to publish the result.
let result = await Self.processDiffs(diffs, on: itemProxies, spanName: "process_timeline_list_diffs:\(kind)")
itemProxies = result.itemProxies
if result.hasMembershipChange {
membershipChangeSubject.send(())
}
}
@concurrent
private static func processDiffs(_ diffs: [TimelineDiff],
on currentItems: [TimelineItemProxy],
spanName: String) async -> (itemProxies: [TimelineItemProxy], hasMembershipChange: Bool) {
let span = MXLog.createSpan(spanName)
span.enter()
defer {
span.exit()
}
MXLog.verbose("Received diffs: \(diffs)")
var hasMembershipChange = false
itemProxies = diffs.reduce(itemProxies) { currentItems, diff in
guard let collectionDiff = buildDiff(from: diff, on: currentItems) else {
let itemProxies = diffs.reduce(currentItems) { currentItems, diff in
guard let collectionDiff = buildDiff(from: diff, on: currentItems, hasMembershipChange: &hasMembershipChange) else {
MXLog.error("Failed building CollectionDifference from \(diff)")
return currentItems
}
@@ -103,9 +120,11 @@ class TimelineItemProvider: TimelineItemProviderProtocol {
return updatedItems
}
return (itemProxies, hasMembershipChange)
}
private func buildDiff(from diff: TimelineDiff, on itemProxies: [TimelineItemProxy]) -> CollectionDifference<TimelineItemProxy>? {
private nonisolated static func buildDiff(from diff: TimelineDiff, on itemProxies: [TimelineItemProxy], hasMembershipChange: inout Bool) -> CollectionDifference<TimelineItemProxy>? {
var changes = [CollectionDifference<TimelineItemProxy>.Change]()
switch diff {
@@ -114,7 +133,7 @@ class TimelineItemProvider: TimelineItemProviderProtocol {
let itemProxy = TimelineItemProxy(item: item)
if itemProxy.isMembershipChange {
membershipChangeSubject.send(())
hasMembershipChange = true
}
changes.append(.insert(offset: Int(itemProxies.count) + index, element: itemProxy, associatedWith: nil))
@@ -138,7 +157,7 @@ class TimelineItemProvider: TimelineItemProviderProtocol {
let itemProxy = TimelineItemProxy(item: item)
if itemProxy.isMembershipChange {
membershipChangeSubject.send(())
hasMembershipChange = true
}
changes.append(.insert(offset: Int(itemProxies.count), element: itemProxy, associatedWith: nil))
@@ -170,7 +189,7 @@ class TimelineItemProvider: TimelineItemProviderProtocol {
}
}
private extension TimelineItemProxy {
private nonisolated extension TimelineItemProxy {
var isMembershipChange: Bool {
switch self {
case .event(let eventTimelineItemProxy):
@@ -10,10 +10,10 @@ import Combine
import Foundation
import MatrixRustSDK
struct TimelinePaginationState: Equatable {
nonisolated struct TimelinePaginationState: Equatable {
/// An initial state that is used to prevent pagination whilst loading the timeline.
/// Once the initial items are loaded the TimelineProxy will publish the correct value.
static var initial = TimelinePaginationState(backward: .endReached, forward: .endReached)
static let initial = TimelinePaginationState(backward: .endReached, forward: .endReached)
let backward: PaginationState
let forward: PaginationState
}
@@ -21,7 +21,6 @@ struct TimelinePaginationState: Equatable {
/// Entities implementing this protocol are responsible for processings diffs coming from the rust timeline
/// and converting them into an array of Element X specific ``TimelineItemProxy``s that will be
/// published as an array together with the pagination state through the ``updatePublisher``.
@MainActor
protocol TimelineItemProviderProtocol {
/// A publisher that signals when ``itemProxies`` or ``paginationState`` are changed.
var updatePublisher: AnyPublisher<([TimelineItemProxy], TimelinePaginationState), Never> { get }
@@ -11,7 +11,7 @@ import MatrixRustSDK
import MatrixRustSDKMocks
/// A light wrapper around timeline items returned from Rust.
enum TimelineItemProxy {
nonisolated enum TimelineItemProxy {
case event(EventTimelineItemProxy)
case virtual(MatrixRustSDK.VirtualTimelineItem, uniqueID: TimelineItemIdentifier.UniqueID)
case unknown(MatrixRustSDK.TimelineItem)
@@ -37,7 +37,7 @@ enum TimelineItemProxy {
}
/// The delivery status for the item.
enum TimelineItemDeliveryStatus: Hashable {
nonisolated enum TimelineItemDeliveryStatus: Hashable {
case sending
case sent
case sendingFailed(TimelineItemSendFailure)
@@ -51,7 +51,7 @@ enum TimelineItemDeliveryStatus: Hashable {
}
/// The reason a timeline item failed to send.
enum TimelineItemSendFailure: Hashable {
nonisolated enum TimelineItemSendFailure: Hashable {
enum VerifiedUser: Hashable {
case hasUnsignedDevice(devices: [String: [String]])
case changedIdentity(users: [String])
@@ -69,7 +69,11 @@ enum TimelineItemSendFailure: Hashable {
}
/// A light wrapper around event timeline items returned from Rust.
class EventTimelineItemProxy {
///
/// The proxy is shared between the main actor and the background timeline item
/// building, so it's stateless and forwards everything to the underlying
/// (internally synchronised) Rust object.
final nonisolated class EventTimelineItemProxy: Sendable {
let item: MatrixRustSDK.EventTimelineItem
let id: TimelineItemIdentifier
@@ -79,13 +83,13 @@ class EventTimelineItemProxy {
id = .event(uniqueID: uniqueID, eventOrTransactionID: .init(rustValue: item.eventOrTransactionId))
}
lazy var deliveryStatus: TimelineItemDeliveryStatus? = {
var deliveryStatus: TimelineItemDeliveryStatus? {
guard let localSendState = item.localSendState else {
return nil
}
switch localSendState {
case .sendingFailed(let error, let isRecoverable):
case .sendingFailed(let error, _):
switch error {
case .identityViolations(let users):
return .sendingFailed(.verifiedUser(.changedIdentity(users: users)))
@@ -99,42 +103,62 @@ class EventTimelineItemProxy {
case .sent:
return .sent
}
}()
}
lazy var canBeRepliedTo = item.canBeRepliedTo
var canBeRepliedTo: Bool {
item.canBeRepliedTo
}
lazy var content = item.content
var content: TimelineItemContent {
item.content
}
lazy var isOwn = item.isOwn
var isOwn: Bool {
item.isOwn
}
lazy var isEditable = item.isEditable
var isEditable: Bool {
item.isEditable
}
lazy var sender = TimelineItemSender(senderID: item.sender, senderProfile: item.senderProfile)
var sender: TimelineItemSender {
TimelineItemSender(senderID: item.sender, senderProfile: item.senderProfile)
}
lazy var forwarder: TimelineItemKeyForwarder? = {
var forwarder: TimelineItemKeyForwarder? {
guard let forwarderID = item.forwarder, let forwarderProfile = item.forwarderProfile else {
return nil
}
return TimelineItemKeyForwarder(forwarderID: forwarderID, forwarderProfile: forwarderProfile)
}()
}
lazy var timestamp = Date(timeIntervalSince1970: TimeInterval(item.timestamp / 1000))
var timestamp: Date {
Date(timeIntervalSince1970: TimeInterval(item.timestamp / 1000))
}
lazy var debugInfo: TimelineItemDebugInfo = {
var debugInfo: TimelineItemDebugInfo {
let debugInfo = item.lazyProvider.debugInfo()
return TimelineItemDebugInfo(model: debugInfo.model, originalJSON: debugInfo.originalJson, latestEditJSON: debugInfo.latestEditJson)
}()
}
lazy var shieldState = item.lazyProvider.getShields(strict: false)
var shieldState: ShieldState? {
item.lazyProvider.getShields(strict: false)
}
lazy var sendHandle = item.lazyProvider.getSendHandle()
var sendHandle: SendHandle? {
item.lazyProvider.getSendHandle()
}
lazy var shouldBoost = item.lazyProvider.containsOnlyEmojis()
var shouldBoost: Bool {
item.lazyProvider.containsOnlyEmojis()
}
lazy var readReceipts = item.readReceipts
var readReceipts: [String: Receipt] {
item.readReceipts
}
}
struct TimelineItemDebugInfo: Identifiable, CustomStringConvertible {
nonisolated struct TimelineItemDebugInfo: Identifiable, CustomStringConvertible {
let id = UUID()
let model: String
let originalJSON: String?
@@ -175,7 +199,7 @@ struct TimelineItemDebugInfo: Identifiable, CustomStringConvertible {
}
}
struct SendHandleProxy: Hashable {
nonisolated struct SendHandleProxy: Hashable {
enum Error: Swift.Error {
case sdkError(Swift.Error)
}
@@ -209,7 +233,7 @@ struct SendHandleProxy: Hashable {
}
}
struct VideoInfoProxy: Hashable {
nonisolated struct VideoInfoProxy: Hashable {
let source: MediaSourceProxy
private(set) var duration: TimeInterval
private(set) var size: CGSize?
@@ -253,7 +277,7 @@ struct VideoInfoProxy: Hashable {
}
}
struct ImageInfoProxy: Hashable {
nonisolated struct ImageInfoProxy: Hashable {
let source: MediaSourceProxy
private(set) var size: CGSize?
private(set) var aspectRatio: CGFloat?
@@ -317,7 +341,7 @@ struct ImageInfoProxy: Hashable {
}
}
private struct MediaInfoProxy: Hashable {
private nonisolated struct MediaInfoProxy: Hashable {
private(set) var size: CGSize?
private(set) var mimeType: String?
private(set) var aspectRatio: CGFloat?
@@ -9,7 +9,7 @@
import MatrixRustSDK
import SwiftUI
struct TimelineItemSender: Identifiable, Hashable {
nonisolated struct TimelineItemSender: Identifiable, Hashable {
static let test = TimelineItemSender(id: "@test.matrix.org")
let id: String
@@ -9,4 +9,4 @@
import Foundation
// periphery: ignore - markdown protocol
protocol DecorationTimelineItemProtocol: RoomTimelineItemProtocol { }
nonisolated protocol DecorationTimelineItemProtocol: RoomTimelineItemProtocol { }
@@ -8,7 +8,7 @@
import Foundation
enum EventBasedMessageTimelineItemContentType: Hashable {
nonisolated enum EventBasedMessageTimelineItemContentType: Hashable {
case audio(AudioRoomTimelineItemContent)
case emote(EmoteRoomTimelineItemContent)
case file(FileRoomTimelineItemContent)
@@ -20,11 +20,11 @@ enum EventBasedMessageTimelineItemContentType: Hashable {
case voice(AudioRoomTimelineItemContent)
}
protocol EventBasedMessageTimelineItemProtocol: EventBasedTimelineItemProtocol {
nonisolated protocol EventBasedMessageTimelineItemProtocol: EventBasedTimelineItemProtocol {
var contentType: EventBasedMessageTimelineItemContentType { get }
}
extension EventBasedMessageTimelineItemProtocol {
nonisolated extension EventBasedMessageTimelineItemProtocol {
var supportsMediaCaption: Bool {
switch contentType {
case .audio, .file, .image, .video:
@@ -9,7 +9,7 @@
import Foundation
import UIKit
protocol EventBasedTimelineItemProtocol: RoomTimelineItemProtocol, CustomStringConvertible {
nonisolated protocol EventBasedTimelineItemProtocol: RoomTimelineItemProtocol, CustomStringConvertible {
var timestamp: Date { get }
var isOutgoing: Bool { get }
var isEditable: Bool { get }
@@ -22,7 +22,7 @@ protocol EventBasedTimelineItemProtocol: RoomTimelineItemProtocol, CustomStringC
var properties: RoomTimelineItemProperties { get }
}
extension EventBasedTimelineItemProtocol {
nonisolated extension EventBasedTimelineItemProtocol {
var description: String {
"\(String(describing: Self.self)): id: \(id), timestamp: \(timestamp), isOutgoing: \(isOutgoing), properties: \(properties)"
}
@@ -8,7 +8,7 @@
import Foundation
struct AudioRoomTimelineItem: EventBasedMessageTimelineItemProtocol, Equatable {
nonisolated struct AudioRoomTimelineItem: EventBasedMessageTimelineItemProtocol, Equatable {
let id: TimelineItemIdentifier
let timestamp: Date
let isOutgoing: Bool
@@ -9,7 +9,7 @@
import UIKit
import UniformTypeIdentifiers
struct AudioRoomTimelineItemContent: Hashable {
nonisolated struct AudioRoomTimelineItemContent: Hashable {
let filename: String
var caption: String?
var formattedCaption: AttributedString?
@@ -8,7 +8,7 @@
import UIKit
struct EmoteRoomTimelineItem: TextBasedRoomTimelineItem, Equatable {
nonisolated struct EmoteRoomTimelineItem: TextBasedRoomTimelineItem, Equatable {
let id: TimelineItemIdentifier
let timestamp: Date
let isOutgoing: Bool
@@ -8,7 +8,7 @@
import UIKit
struct EmoteRoomTimelineItemContent: Hashable {
nonisolated struct EmoteRoomTimelineItemContent: Hashable {
let body: String
var formattedBody: AttributedString?
/// The original textual representation of the formatted body directly from the event (usually HTML code)
@@ -9,7 +9,7 @@
import UIKit
import UniformTypeIdentifiers
struct FileRoomTimelineItem: EventBasedMessageTimelineItemProtocol, Equatable {
nonisolated struct FileRoomTimelineItem: EventBasedMessageTimelineItemProtocol, Equatable {
let id: TimelineItemIdentifier
let timestamp: Date
let isOutgoing: Bool
@@ -9,7 +9,7 @@
import Foundation
import UniformTypeIdentifiers
struct FileRoomTimelineItemContent: Hashable {
nonisolated struct FileRoomTimelineItemContent: Hashable {
let filename: String
var caption: String?
var formattedCaption: AttributedString?

Some files were not shown because too many files have changed in this diff Show More