mirror of
https://github.com/Novaloop-AG/element-x-ios.git
synced 2026-11-05 11:58:32 +00:00
Swift 6 Migration (#5733)
* Enable Swift 6.2 with default main actor isolation for the app target Turn on SWIFT_VERSION 6.2, SWIFT_APPROACHABLE_CONCURRENCY and SWIFT_DEFAULT_ACTOR_ISOLATION = MainActor for the ElementX target, matching the NSE and Share Extension (which already use 6.2 without the main actor default). # Conflicts: # ElementX.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved * Update the code generation templates for Swift 6 Emit nonisolated/@unchecked Sendable mocks and nonisolated string enums, and mark async mock methods @concurrent so they keep suspending like the real implementations (otherwise intermediate states are unobservable in tests). Includes the regenerated mocks and strings. # Conflicts: # ElementX/Sources/Mocks/Generated/GeneratedMocks.swift * Make the data layer and services nonisolated and Sendable Annotate the value-type models, Rust SDK proxy wrappers, timeline item graph, settings, logging and NSE-shared types as nonisolated, and conform the service protocols to Sendable so their existentials can cross isolation boundaries. These types run off the main actor and no longer inherit the target's default main actor isolation. * Adopt the new isolation across call sites, mocks and signatures Update call sites, closure signatures and mock configuration helpers to match the nonisolated/Sendable data layer, including the main actor isolated mock inits and the @Sendable filter on recentlyVisitedRooms. ^ Conflicts: ^ ElementX/Sources/Services/SecureBackup/SecureBackupController.swift * Remove redundant @MainActor annotations With the app target defaulting to main actor isolation, the explicit @MainActor on view models, coordinators, views and their protocols is no longer required. * Fix concurrency hotspots surfaced by the migration Resolve the room summary main-thread deadlock by moving diff processing onto AsyncStreams, add the SDKListener.onMainActor FIFO bridge for SDK callbacks, hop KVO/timer/UIKit callbacks to the main actor via assumeIsolated, add isolated deinits, and apply the few justified nonisolated(unsafe) escapes for queue-confined Apple APIs. * Fix the unit tests for Swift 6 concurrency Hop the waitForConfirmation body onto the caller's actor (Swift Testing runs confirmation closures off-actor), and adjust the affected suites and async signatures. * offload diffs on concurrent threads instead of main actor * make room info protocols sendable * use a task group for fetching data so that everything runs concurrently in the TextRoomTimelineView when getting the preview. * use the generic onMainActor implementation, which already makes a stream and send the continuation on the main actor. * small pr comments issues being addressed * update enterprise * Update StaticCode to 1.1.0 and specify the required Swift isolation. * fix ElementCallServiceTests * update enterprise * re-generate mocks * update enterprise * enterprise new code conformed to swift 6 --------- Co-authored-by: Doug <douglase@element.io>
This commit is contained in:
@@ -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:
|
||||
|
||||
+1
-1
@@ -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,
|
||||
|
||||
+1
-1
@@ -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 }
|
||||
|
||||
+26
-9
@@ -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 {
|
||||
|
||||
-1
@@ -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
|
||||
}
|
||||
|
||||
+1
-1
@@ -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
|
||||
|
||||
+1
-1
@@ -8,7 +8,7 @@
|
||||
import MatrixRustSDK
|
||||
import SwiftUI
|
||||
|
||||
struct TimelineItemKeyForwarder: Identifiable, Hashable {
|
||||
nonisolated struct TimelineItemKeyForwarder: Identifiable, Hashable {
|
||||
let id: String
|
||||
let displayName: String?
|
||||
|
||||
|
||||
+2
-2
@@ -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)
|
||||
|
||||
+1
-1
@@ -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
|
||||
|
||||
+1
-1
@@ -9,4 +9,4 @@
|
||||
import Foundation
|
||||
|
||||
// periphery: ignore - markdown protocol
|
||||
protocol DecorationTimelineItemProtocol: RoomTimelineItemProtocol { }
|
||||
nonisolated protocol DecorationTimelineItemProtocol: RoomTimelineItemProtocol { }
|
||||
|
||||
+3
-3
@@ -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:
|
||||
|
||||
+2
-2
@@ -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)"
|
||||
}
|
||||
|
||||
+1
-1
@@ -8,7 +8,7 @@
|
||||
|
||||
import Foundation
|
||||
|
||||
struct AudioRoomTimelineItem: EventBasedMessageTimelineItemProtocol, Equatable {
|
||||
nonisolated struct AudioRoomTimelineItem: EventBasedMessageTimelineItemProtocol, Equatable {
|
||||
let id: TimelineItemIdentifier
|
||||
let timestamp: Date
|
||||
let isOutgoing: Bool
|
||||
|
||||
+1
-1
@@ -9,7 +9,7 @@
|
||||
import UIKit
|
||||
import UniformTypeIdentifiers
|
||||
|
||||
struct AudioRoomTimelineItemContent: Hashable {
|
||||
nonisolated struct AudioRoomTimelineItemContent: Hashable {
|
||||
let filename: String
|
||||
var caption: String?
|
||||
var formattedCaption: AttributedString?
|
||||
|
||||
+1
-1
@@ -8,7 +8,7 @@
|
||||
|
||||
import UIKit
|
||||
|
||||
struct EmoteRoomTimelineItem: TextBasedRoomTimelineItem, Equatable {
|
||||
nonisolated struct EmoteRoomTimelineItem: TextBasedRoomTimelineItem, Equatable {
|
||||
let id: TimelineItemIdentifier
|
||||
let timestamp: Date
|
||||
let isOutgoing: Bool
|
||||
|
||||
+1
-1
@@ -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)
|
||||
|
||||
+1
-1
@@ -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
|
||||
|
||||
+1
-1
@@ -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
Reference in New Issue
Block a user