* Replace GrantLoginWithQrCodeHandlerSDKMock with LinkNewDeviceServiceMock. Add tests for all initial states on the QRCodeLoginScreen. * Add tests for linking both mobile and desktop devices. * Add UI tests for linking a new device. * Don't show the Link Desktop Computer button when running on macOS. This mirrors the decision to hide the Sign In With QR Code button on the start screen.
1303 lines
52 KiB
Swift
1303 lines
52 KiB
Swift
//
|
|
// Copyright 2025 Element Creations Ltd.
|
|
// Copyright 2022-2025 New Vector Ltd.
|
|
//
|
|
// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
|
|
// Please see LICENSE files in the repository root for full details.
|
|
//
|
|
|
|
@preconcurrency import Combine
|
|
import CryptoKit
|
|
import Foundation
|
|
import OrderedCollections
|
|
|
|
import MatrixRustSDK
|
|
|
|
class ClientProxy: ClientProxyProtocol {
|
|
private let client: ClientProtocol
|
|
private let networkMonitor: NetworkMonitorProtocol
|
|
private let appSettings: AppSettings
|
|
|
|
let mediaLoader: MediaLoaderProtocol
|
|
private let clientQueue: DispatchQueue
|
|
|
|
private var roomListService: RoomListService
|
|
// periphery: ignore - only for retain
|
|
private var roomListStateUpdateTaskHandle: TaskHandle?
|
|
// periphery: ignore - only for retain
|
|
private var roomListStateLoadingStateUpdateTaskHandle: TaskHandle?
|
|
|
|
private var syncService: SyncService
|
|
// periphery: ignore - only for retain
|
|
private var syncServiceStateUpdateTaskHandle: TaskHandle?
|
|
|
|
// periphery:ignore - required for instance retention in the rust codebase
|
|
private var ignoredUsersListenerTaskHandle: TaskHandle?
|
|
|
|
// periphery:ignore - required for instance retention in the rust codebase
|
|
private var verificationStateListenerTaskHandle: TaskHandle?
|
|
|
|
// periphery:ignore - required for instance retention in the rust codebase
|
|
private var sendQueueListenerTaskHandle: TaskHandle?
|
|
|
|
// periphery:ignore - required for instance retention in the rust codebase
|
|
private var mediaPreviewConfigListenerTaskHandle: TaskHandle?
|
|
|
|
private var delegateHandle: TaskHandle?
|
|
|
|
// These following summary providers both operate on the same allRooms() list but
|
|
// can apply their own filtering and pagination
|
|
private(set) var roomSummaryProvider: RoomSummaryProviderProtocol
|
|
private(set) var alternateRoomSummaryProvider: RoomSummaryProviderProtocol
|
|
|
|
private(set) var staticRoomSummaryProvider: StaticRoomSummaryProviderProtocol
|
|
|
|
let notificationSettings: NotificationSettingsProxyProtocol
|
|
|
|
let secureBackupController: SecureBackupControllerProtocol
|
|
|
|
private(set) var sessionVerificationController: SessionVerificationControllerProxyProtocol?
|
|
|
|
let spaceService: SpaceServiceProxyProtocol
|
|
|
|
private static var roomCreationPowerLevelOverrides: PowerLevels {
|
|
.init(usersDefault: nil,
|
|
eventsDefault: nil,
|
|
stateDefault: nil,
|
|
ban: nil,
|
|
kick: nil,
|
|
redact: nil,
|
|
invite: nil,
|
|
notifications: nil,
|
|
users: [:],
|
|
events: [
|
|
"m.call.member": Int32(0),
|
|
"org.matrix.msc3401.call.member": Int32(0)
|
|
])
|
|
}
|
|
|
|
private static var knockingRoomCreationPowerLevelOverrides: PowerLevels {
|
|
.init(usersDefault: nil,
|
|
eventsDefault: nil,
|
|
stateDefault: nil,
|
|
ban: nil,
|
|
kick: nil,
|
|
redact: nil,
|
|
invite: Int32(50),
|
|
notifications: nil,
|
|
users: [:],
|
|
events: [
|
|
"m.call.member": Int32(0),
|
|
"org.matrix.msc3401.call.member": Int32(0)
|
|
])
|
|
}
|
|
|
|
private var loadCachedAvatarURLTask: Task<Void, Never>?
|
|
private let userAvatarURLSubject = CurrentValueSubject<URL?, Never>(nil)
|
|
var userAvatarURLPublisher: CurrentValuePublisher<URL?, Never> {
|
|
userAvatarURLSubject.asCurrentValuePublisher()
|
|
}
|
|
|
|
private let userDisplayNameSubject = CurrentValueSubject<String?, Never>(nil)
|
|
var userDisplayNamePublisher: CurrentValuePublisher<String?, Never> {
|
|
userDisplayNameSubject.asCurrentValuePublisher()
|
|
}
|
|
|
|
private let ignoredUsersSubject = CurrentValueSubject<[String]?, Never>(nil)
|
|
var ignoredUsersPublisher: CurrentValuePublisher<[String]?, Never> {
|
|
ignoredUsersSubject
|
|
.asCurrentValuePublisher()
|
|
}
|
|
|
|
private var cancellables = Set<AnyCancellable>()
|
|
|
|
/// Will be `true` whilst the app cleans up and forces a logout. Prevents the sync service from restarting
|
|
/// before the client is released which ends up running in a loop. This is a workaround until the sync service
|
|
/// can tell us *what* error occurred so we can handle restarts more gracefully.
|
|
private var hasEncounteredAuthError = false
|
|
|
|
deinit {
|
|
stopSync { [delegateHandle] in
|
|
// The delegate handle needs to be cancelled always after the sync stops
|
|
delegateHandle?.cancel()
|
|
}
|
|
}
|
|
|
|
private let actionsSubject = PassthroughSubject<ClientProxyAction, Never>()
|
|
var actionsPublisher: AnyPublisher<ClientProxyAction, Never> {
|
|
actionsSubject.eraseToAnyPublisher()
|
|
}
|
|
|
|
private let loadingStateSubject = CurrentValueSubject<ClientProxyLoadingState, Never>(.notLoading)
|
|
var loadingStatePublisher: CurrentValuePublisher<ClientProxyLoadingState, Never> {
|
|
loadingStateSubject.asCurrentValuePublisher()
|
|
}
|
|
|
|
private let verificationStateSubject = CurrentValueSubject<SessionVerificationState, Never>(.unknown)
|
|
var verificationStatePublisher: CurrentValuePublisher<SessionVerificationState, Never> {
|
|
verificationStateSubject.asCurrentValuePublisher()
|
|
}
|
|
|
|
private let homeserverReachabilitySubject = CurrentValueSubject<NetworkMonitorReachability, Never>(.reachable)
|
|
var homeserverReachabilityPublisher: CurrentValuePublisher<NetworkMonitorReachability, Never> {
|
|
homeserverReachabilitySubject.asCurrentValuePublisher()
|
|
}
|
|
|
|
private let timelineMediaVisibilitySubject = CurrentValueSubject<TimelineMediaVisibility, Never>(.always)
|
|
var timelineMediaVisibilityPublisher: CurrentValuePublisher<TimelineMediaVisibility, Never> {
|
|
timelineMediaVisibilitySubject.asCurrentValuePublisher()
|
|
}
|
|
|
|
private let hideInviteAvatarsSubject = CurrentValueSubject<Bool, Never>(false)
|
|
var hideInviteAvatarsPublisher: CurrentValuePublisher<Bool, Never> {
|
|
hideInviteAvatarsSubject.asCurrentValuePublisher()
|
|
}
|
|
|
|
var roomsToAwait: Set<String> = []
|
|
|
|
private let sendQueueStatusSubject = CurrentValueSubject<Bool, Never>(false)
|
|
|
|
init(client: ClientProtocol,
|
|
networkMonitor: NetworkMonitorProtocol,
|
|
appSettings: AppSettings) async throws {
|
|
self.client = client
|
|
self.networkMonitor = networkMonitor
|
|
self.appSettings = appSettings
|
|
|
|
clientQueue = .init(label: "ClientProxyQueue", attributes: .concurrent)
|
|
|
|
mediaLoader = MediaLoader(client: client)
|
|
|
|
notificationSettings = await NotificationSettingsProxy(notificationSettings: client.getNotificationSettings())
|
|
|
|
secureBackupController = SecureBackupController(encryption: client.encryption())
|
|
|
|
spaceService = await SpaceServiceProxy(spaceService: client.spaceService())
|
|
|
|
let configuredAppService = try await ClientProxyServices(client: client,
|
|
actionsSubject: actionsSubject,
|
|
notificationSettings: notificationSettings,
|
|
appSettings: appSettings)
|
|
|
|
syncService = configuredAppService.syncService
|
|
roomListService = configuredAppService.roomListService
|
|
roomSummaryProvider = configuredAppService.roomSummaryProvider
|
|
alternateRoomSummaryProvider = configuredAppService.alternateRoomSummaryProvider
|
|
staticRoomSummaryProvider = configuredAppService.staticRoomSummaryProvider
|
|
|
|
syncServiceStateUpdateTaskHandle = createSyncServiceStateObserver(syncService)
|
|
roomListStateUpdateTaskHandle = createRoomListServiceObserver(roomListService)
|
|
roomListStateLoadingStateUpdateTaskHandle = createRoomListLoadingStateUpdateObserver(roomListService)
|
|
|
|
delegateHandle = try client.setDelegate(delegate: ClientDelegateWrapper { [weak self] isSoftLogout in
|
|
self?.hasEncounteredAuthError = true
|
|
self?.actionsSubject.send(.receivedAuthError(isSoftLogout: isSoftLogout))
|
|
})
|
|
|
|
try await client.setUtdDelegate(utdDelegate: ClientDecryptionErrorDelegate(actionsSubject: actionsSubject))
|
|
|
|
networkMonitor.reachabilityPublisher
|
|
.removeDuplicates()
|
|
.receive(on: DispatchQueue.main)
|
|
.sink { [weak self] reachability in
|
|
if reachability == .reachable {
|
|
self?.startSync()
|
|
}
|
|
}
|
|
.store(in: &cancellables)
|
|
|
|
loadUserAvatarURLFromCache()
|
|
|
|
ignoredUsersListenerTaskHandle = client.subscribeToIgnoredUsers(listener: SDKListener { [weak self] ignoredUsers in
|
|
self?.ignoredUsersSubject.send(ignoredUsers)
|
|
})
|
|
|
|
await updateVerificationState(client.encryption().verificationState())
|
|
|
|
verificationStateListenerTaskHandle = client.encryption().verificationStateListener(listener: SDKListener { [weak self] verificationState in
|
|
Task { await self?.updateVerificationState(verificationState) }
|
|
})
|
|
|
|
sendQueueListenerTaskHandle = client.subscribeToSendQueueStatus(listener: SDKListener { [weak self] roomID, error in
|
|
MXLog.error("Send queue failed in room: \(roomID) with error: \(error)")
|
|
self?.sendQueueStatusSubject.send(false)
|
|
})
|
|
|
|
sendQueueStatusSubject
|
|
.combineLatest(homeserverReachabilityPublisher)
|
|
.debounce(for: 1.0, scheduler: DispatchQueue.main)
|
|
.sink { enabled, reachability in
|
|
MXLog.info("Send queue status changed to enabled: \(enabled), homeserver reachability: \(reachability)")
|
|
|
|
if enabled == false, reachability == .reachable {
|
|
MXLog.info("Enabling all send queues")
|
|
Task {
|
|
await client.enableAllSendQueues(enable: true)
|
|
}
|
|
}
|
|
}
|
|
.store(in: &cancellables)
|
|
|
|
Task {
|
|
do {
|
|
try await client.setMediaRetentionPolicy(policy: .init(maxCacheSize: nil,
|
|
maxFileSize: nil,
|
|
// 30 days in seconds
|
|
lastAccessExpiry: 30 * 24 * 60 * 60,
|
|
// 1 day in seconds
|
|
cleanupFrequency: 24 * 60 * 60))
|
|
} catch {
|
|
MXLog.error("Failed setting media retention policy with error: \(error)")
|
|
}
|
|
}
|
|
|
|
Task {
|
|
mediaPreviewConfigListenerTaskHandle = await createMediaPreviewConfigObserver()
|
|
}
|
|
}
|
|
|
|
var userID: String {
|
|
do {
|
|
return try client.userId()
|
|
} catch {
|
|
MXLog.error("Failed retrieving room info with error: \(error)")
|
|
return "Unknown user identifier"
|
|
}
|
|
}
|
|
|
|
var deviceID: String? {
|
|
do {
|
|
return try client.deviceId()
|
|
} catch {
|
|
MXLog.error("Failed retrieving deviceID with error: \(error)")
|
|
return nil
|
|
}
|
|
}
|
|
|
|
var homeserver: String {
|
|
client.homeserver()
|
|
}
|
|
|
|
var canDeactivateAccount: Bool {
|
|
client.canDeactivateAccount()
|
|
}
|
|
|
|
var userIDServerName: String? {
|
|
do {
|
|
return try client.userIdServerName()
|
|
} catch {
|
|
MXLog.error("Failed retrieving userID server name with error: \(error)")
|
|
return nil
|
|
}
|
|
}
|
|
|
|
var isReportRoomSupported: Bool {
|
|
get async {
|
|
do {
|
|
return try await client.isReportRoomApiSupported()
|
|
} catch {
|
|
MXLog.error("Failed checking report room support with error: \(error)")
|
|
return false
|
|
}
|
|
}
|
|
}
|
|
|
|
var isLiveKitRTCSupported: Bool {
|
|
get async {
|
|
do {
|
|
return try await client.isLivekitRtcSupported()
|
|
} catch {
|
|
MXLog.error("Failed checking LiveKit RTC support with error: \(error)")
|
|
return false
|
|
}
|
|
}
|
|
}
|
|
|
|
var isLoginWithQRCodeSupported: Bool {
|
|
get async {
|
|
do {
|
|
return try await client.isLoginWithQrCodeSupported()
|
|
} catch {
|
|
MXLog.error("Failed checking QR code support with error: \(error)")
|
|
return false
|
|
}
|
|
}
|
|
}
|
|
|
|
var maxMediaUploadSize: Result<UInt, ClientProxyError> {
|
|
get async {
|
|
do {
|
|
return try await .success(UInt(client.getMaxMediaUploadSize()))
|
|
} catch {
|
|
MXLog.error("Failed checking the max media upload size with error: \(error)")
|
|
return .failure(.sdkError(error))
|
|
}
|
|
}
|
|
}
|
|
|
|
private(set) lazy var pusherNotificationClientIdentifier: String? = {
|
|
// NOTE: The result is stored as part of the restoration token. Any changes
|
|
// here would require a migration to correctly match incoming notifications.
|
|
guard let data = userID.data(using: .utf8) else { return nil }
|
|
let digest = SHA256.hash(data: data)
|
|
return digest.compactMap { String(format: "%02x", $0) }.joined()
|
|
}()
|
|
|
|
func isOnlyDeviceLeft() async -> Result<Bool, ClientProxyError> {
|
|
do {
|
|
let result = try await client.encryption().isLastDevice()
|
|
return .success(result)
|
|
} catch {
|
|
MXLog.error("Failed checking isLastDevice with error: \(error)")
|
|
return .failure(.sdkError(error))
|
|
}
|
|
}
|
|
|
|
func hasDevicesToVerifyAgainst() async -> Result<Bool, ClientProxyError> {
|
|
do {
|
|
let result = try await client.encryption().hasDevicesToVerifyAgainst()
|
|
return .success(result)
|
|
} catch {
|
|
MXLog.error("Failed checking hasDevicesToVerifyAgainst with error: \(error)")
|
|
return .failure(.sdkError(error))
|
|
}
|
|
}
|
|
|
|
func startSync() {
|
|
guard !hasEncounteredAuthError else {
|
|
MXLog.warning("Ignoring request, this client has an unknown token.")
|
|
return
|
|
}
|
|
|
|
guard networkMonitor.reachabilityPublisher.value == .reachable else {
|
|
MXLog.warning("Ignoring request, network unreachable.")
|
|
return
|
|
}
|
|
|
|
MXLog.info("Starting sync")
|
|
|
|
Task {
|
|
await syncService.start()
|
|
|
|
// If we are using OIDC we want to cache the account management URL in volatile memory on the SDK side.
|
|
// To avoid the cache being invalidated while the app is backgrounded, we cache at every sync start.
|
|
await cacheAccountURL()
|
|
}
|
|
}
|
|
|
|
/// A stored task for restarting the sync after a failure. This is stored so that we can cancel
|
|
/// it when `stopSync` is called (e.g. when signing out) to prevent an otherwise infinite
|
|
/// loop that was triggered by trying to sync a signed out session.
|
|
@CancellableTask private var restartTask: Task<Void, Never>?
|
|
|
|
func restartSync() {
|
|
guard restartTask == nil else { return }
|
|
|
|
restartTask = Task { [weak self] in
|
|
do {
|
|
// Until the SDK can tell us the failure, we add a small
|
|
// delay to avoid generating multi-gigabyte log files.
|
|
try await Task.sleep(for: .milliseconds(250))
|
|
self?.startSync()
|
|
} catch {
|
|
MXLog.error("Restart cancelled.")
|
|
}
|
|
self?.restartTask = nil
|
|
}
|
|
}
|
|
|
|
func stopSync() {
|
|
stopSync(completion: nil)
|
|
}
|
|
|
|
func stopSync(completion: (() -> Void)?) {
|
|
MXLog.info("Stopping sync")
|
|
|
|
if restartTask != nil {
|
|
MXLog.warning("Removing the sync service restart task.")
|
|
restartTask = nil
|
|
}
|
|
|
|
// Capture the sync service strongly as this method is called on deinit and so the
|
|
// existence of self when the Task executes is questionable and would sometimes crash.
|
|
// Note: This isn't strictly necessary now given the unwrap above, but leaving the code as
|
|
// documentation. SE-0371 will allow us to fix this by using an async deinit.
|
|
Task { [syncService] in
|
|
defer {
|
|
completion?()
|
|
}
|
|
|
|
await syncService.stop()
|
|
MXLog.info("Sync stopped")
|
|
}
|
|
}
|
|
|
|
func expireSyncSessions() async {
|
|
await syncService.expireSessions()
|
|
}
|
|
|
|
func accountURL(action: AccountManagementAction) async -> URL? {
|
|
try? await client.accountUrl(action: action).flatMap(URL.init(string:))
|
|
}
|
|
|
|
func directRoomForUserID(_ userID: String) -> Result<String?, ClientProxyError> {
|
|
do {
|
|
let roomID = try client.getDmRoom(userId: userID)?.id()
|
|
return .success(roomID)
|
|
} catch {
|
|
MXLog.error("Failed retrieving direct room for userID: \(userID) with error: \(error)")
|
|
return .failure(.sdkError(error))
|
|
}
|
|
}
|
|
|
|
func createDirectRoom(with userID: String, expectedRoomName: String?) async -> Result<String, ClientProxyError> {
|
|
do {
|
|
let parameters = CreateRoomParameters(name: nil,
|
|
topic: nil,
|
|
isEncrypted: true,
|
|
isDirect: true,
|
|
visibility: .private,
|
|
preset: .trustedPrivateChat,
|
|
invite: [userID],
|
|
avatar: nil,
|
|
powerLevelContentOverride: Self.roomCreationPowerLevelOverrides)
|
|
let roomID = try await client.createRoom(request: parameters)
|
|
|
|
await waitForRoomToSync(roomID: roomID)
|
|
|
|
return .success(roomID)
|
|
} catch {
|
|
MXLog.error("Failed creating direct room for userID: \(userID) with error: \(error)")
|
|
return .failure(.sdkError(error))
|
|
}
|
|
}
|
|
|
|
func createRoom(name: String,
|
|
topic: String?,
|
|
isRoomPrivate: Bool,
|
|
isKnockingOnly: Bool,
|
|
userIDs: [String],
|
|
avatarURL: URL?,
|
|
aliasLocalPart: String?) async -> Result<String, ClientProxyError> {
|
|
do {
|
|
let parameters = CreateRoomParameters(name: name,
|
|
topic: topic,
|
|
isEncrypted: isRoomPrivate,
|
|
isDirect: false,
|
|
visibility: isRoomPrivate ? .private : .public,
|
|
preset: isRoomPrivate ? .privateChat : .publicChat,
|
|
invite: userIDs,
|
|
avatar: avatarURL?.absoluteString,
|
|
powerLevelContentOverride: isKnockingOnly ? Self.knockingRoomCreationPowerLevelOverrides : Self.roomCreationPowerLevelOverrides,
|
|
joinRuleOverride: isKnockingOnly ? .knock : nil,
|
|
historyVisibilityOverride: isRoomPrivate ? .invited : nil,
|
|
// This is an FFI naming mistake, what is required is the `aliasLocalPart` not the whole alias
|
|
canonicalAlias: aliasLocalPart)
|
|
let roomID = try await client.createRoom(request: parameters)
|
|
|
|
await waitForRoomToSync(roomID: roomID)
|
|
|
|
return .success(roomID)
|
|
} catch {
|
|
MXLog.error("Failed creating room with error: \(error)")
|
|
return .failure(.sdkError(error))
|
|
}
|
|
}
|
|
|
|
func joinRoom(_ roomID: String, via: [String]) async -> Result<Void, ClientProxyError> {
|
|
do {
|
|
let _ = try await client.joinRoomByIdOrAlias(roomIdOrAlias: roomID, serverNames: via)
|
|
|
|
await waitForRoomToSync(roomID: roomID, timeout: .seconds(30))
|
|
|
|
return .success(())
|
|
} catch ClientError.MatrixApi(.unknown, _, _, _) {
|
|
MXLog.error("Failed joining roomID: \(roomID) invalid invite")
|
|
return .failure(.invalidInvite)
|
|
} catch ClientError.MatrixApi(.forbidden, _, _, _) {
|
|
MXLog.error("Failed joining roomID: \(roomID) forbidden")
|
|
return .failure(.forbiddenAccess)
|
|
} catch {
|
|
MXLog.error("Failed joining roomID: \(roomID) with error: \(error)")
|
|
return .failure(.sdkError(error))
|
|
}
|
|
}
|
|
|
|
func joinRoomAlias(_ roomAlias: String) async -> Result<Void, ClientProxyError> {
|
|
do {
|
|
let room = try await client.joinRoomByIdOrAlias(roomIdOrAlias: roomAlias, serverNames: [])
|
|
|
|
await waitForRoomToSync(roomID: room.id(), timeout: .seconds(30))
|
|
|
|
return .success(())
|
|
} catch ClientError.MatrixApi(.unknown, _, _, _) {
|
|
MXLog.error("Failed joining roomAlias: \(roomAlias) invalid invite")
|
|
return .failure(.invalidInvite)
|
|
} catch ClientError.MatrixApi(.forbidden, _, _, _) {
|
|
MXLog.error("Failed joining roomAlias: \(roomAlias) forbidden")
|
|
return .failure(.forbiddenAccess)
|
|
} catch {
|
|
MXLog.error("Failed joining roomAlias: \(roomAlias) with error: \(error)")
|
|
return .failure(.sdkError(error))
|
|
}
|
|
}
|
|
|
|
func knockRoom(_ roomID: String, via: [String], message: String?) async -> Result<Void, ClientProxyError> {
|
|
do {
|
|
let _ = try await client.knock(roomIdOrAlias: roomID, reason: message, serverNames: via)
|
|
await waitForRoomToSync(roomID: roomID, timeout: .seconds(30))
|
|
return .success(())
|
|
} catch {
|
|
MXLog.error("Failed knocking roomID: \(roomID) with error: \(error)")
|
|
return .failure(.sdkError(error))
|
|
}
|
|
}
|
|
|
|
func knockRoomAlias(_ roomAlias: String, message: String?) async -> Result<Void, ClientProxyError> {
|
|
do {
|
|
let room = try await client.knock(roomIdOrAlias: roomAlias, reason: message, serverNames: [])
|
|
await waitForRoomToSync(roomID: room.id(), timeout: .seconds(30))
|
|
return .success(())
|
|
} catch {
|
|
MXLog.error("Failed knocking roomAlias: \(roomAlias) with error: \(error)")
|
|
return .failure(.sdkError(error))
|
|
}
|
|
}
|
|
|
|
func canJoinRoom(with rules: [AllowRule]) -> Bool {
|
|
for rule in rules {
|
|
if case let .roomMembership(roomID) = rule,
|
|
let room = try? client.getRoom(roomId: roomID),
|
|
room.membership() == .joined {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
func uploadMedia(_ media: MediaInfo) async -> Result<String, ClientProxyError> {
|
|
guard let mimeType = media.mimeType else {
|
|
MXLog.error("Failed uploading media, invalid mime type: \(media)")
|
|
return .failure(ClientProxyError.invalidMedia)
|
|
}
|
|
|
|
do {
|
|
let data = try Data(contentsOf: media.url)
|
|
let matrixUrl = try await client.uploadMedia(mimeType: mimeType, data: data, progressWatcher: nil)
|
|
return .success(matrixUrl)
|
|
} catch let ClientError.MatrixApi(errorKind, _, _, _) {
|
|
MXLog.error("Failed uploading media with error kind: \(errorKind)")
|
|
return .failure(ClientProxyError.failedUploadingMedia(errorKind))
|
|
} catch {
|
|
MXLog.error("Failed uploading media with error: \(error)")
|
|
return .failure(ClientProxyError.sdkError(error))
|
|
}
|
|
}
|
|
|
|
func roomForIdentifier(_ identifier: String) async -> RoomProxyType? {
|
|
let shouldAwait = roomsToAwait.remove(identifier) != nil
|
|
|
|
// Try fetching the room from the cold cache (if available) first
|
|
if let room = await buildRoomForIdentifier(identifier) {
|
|
return room
|
|
}
|
|
|
|
if !staticRoomSummaryProvider.statePublisher.value.isLoaded {
|
|
_ = await staticRoomSummaryProvider.statePublisher.values.first { $0.isLoaded }
|
|
}
|
|
|
|
if shouldAwait {
|
|
await waitForRoomToSync(roomID: identifier)
|
|
}
|
|
|
|
return await buildRoomForIdentifier(identifier)
|
|
}
|
|
|
|
func roomPreviewForIdentifier(_ identifier: String, via: [String]) async -> Result<RoomPreviewProxyProtocol, ClientProxyError> {
|
|
do {
|
|
let roomPreview = try await client.getRoomPreviewFromRoomId(roomId: identifier, viaServers: via)
|
|
return .success(RoomPreviewProxy(roomPreview: roomPreview))
|
|
} catch ClientError.MatrixApi(.forbidden, _, _, _) {
|
|
MXLog.error("Failed retrieving preview for room: \(identifier) is private")
|
|
return .failure(.roomPreviewIsPrivate)
|
|
} catch {
|
|
MXLog.error("Failed retrieving preview for room: \(identifier) with error: \(error)")
|
|
return .failure(.sdkError(error))
|
|
}
|
|
}
|
|
|
|
func roomSummaryForIdentifier(_ identifier: String) -> RoomSummary? {
|
|
staticRoomSummaryProvider.roomListPublisher.value.first { $0.id == identifier }
|
|
}
|
|
|
|
func roomSummaryForAlias(_ alias: String) -> RoomSummary? {
|
|
staticRoomSummaryProvider.roomListPublisher.value.first { $0.canonicalAlias == alias || $0.alternativeAliases.contains(alias) }
|
|
}
|
|
|
|
func reportRoomForIdentifier(_ identifier: String, reason: String) async -> Result<Void, ClientProxyError> {
|
|
do {
|
|
guard let room = try client.getRoom(roomId: identifier) else {
|
|
MXLog.error("Failed reporting room with identifier: \(identifier), room not in local store")
|
|
return .failure(.roomNotInLocalStore)
|
|
}
|
|
try await room.reportRoom(reason: reason)
|
|
return .success(())
|
|
} catch {
|
|
MXLog.error("Failed reporting room with identifier: \(identifier), with error: \(error)")
|
|
return .failure(.sdkError(error))
|
|
}
|
|
}
|
|
|
|
func loadUserDisplayName() async -> Result<Void, ClientProxyError> {
|
|
do {
|
|
let displayName = try await client.displayName()
|
|
userDisplayNameSubject.send(displayName)
|
|
return .success(())
|
|
} catch {
|
|
MXLog.error("Failed loading user display name with error: \(error)")
|
|
return .failure(.sdkError(error))
|
|
}
|
|
}
|
|
|
|
func setUserDisplayName(_ name: String) async -> Result<Void, ClientProxyError> {
|
|
do {
|
|
try await client.setDisplayName(name: name)
|
|
Task {
|
|
await self.loadUserDisplayName()
|
|
}
|
|
return .success(())
|
|
} catch {
|
|
MXLog.error("Failed setting user display name with error: \(error)")
|
|
return .failure(.sdkError(error))
|
|
}
|
|
}
|
|
|
|
func loadUserAvatarURL() async -> Result<Void, ClientProxyError> {
|
|
do {
|
|
let urlString = try await client.avatarUrl()
|
|
loadCachedAvatarURLTask?.cancel()
|
|
userAvatarURLSubject.send(urlString.flatMap(URL.init))
|
|
return .success(())
|
|
} catch {
|
|
MXLog.error("Failed loading user avatar URL with error: \(error)")
|
|
return .failure(.sdkError(error))
|
|
}
|
|
}
|
|
|
|
func setUserAvatar(media: MediaInfo) async -> Result<Void, ClientProxyError> {
|
|
guard case let .image(imageURL, _, _) = media, let mimeType = media.mimeType else {
|
|
MXLog.error("Failed uploading, invalid media: \(media)")
|
|
return .failure(.invalidMedia)
|
|
}
|
|
|
|
do {
|
|
let data = try Data(contentsOf: imageURL)
|
|
try await client.uploadAvatar(mimeType: mimeType, data: data)
|
|
Task {
|
|
await self.loadUserAvatarURL()
|
|
}
|
|
return .success(())
|
|
} catch {
|
|
MXLog.error("Failed setting user avatar with error: \(error)")
|
|
return .failure(.sdkError(error))
|
|
}
|
|
}
|
|
|
|
func removeUserAvatar() async -> Result<Void, ClientProxyError> {
|
|
do {
|
|
try await client.removeAvatar()
|
|
Task {
|
|
await self.loadUserAvatarURL()
|
|
}
|
|
return .success(())
|
|
} catch {
|
|
MXLog.error("Failed removing user avatar with error: \(error)")
|
|
return .failure(.sdkError(error))
|
|
}
|
|
}
|
|
|
|
func linkNewDeviceService() -> LinkNewDeviceServiceProtocol {
|
|
LinkNewDeviceService(handler: client.newGrantLoginWithQrCodeHandler())
|
|
}
|
|
|
|
func deactivateAccount(password: String?, eraseData: Bool) async -> Result<Void, ClientProxyError> {
|
|
do {
|
|
try await client.deactivateAccount(authData: password.map { .password(passwordDetails: .init(identifier: userID, password: $0)) },
|
|
eraseData: eraseData)
|
|
return .success(())
|
|
} catch {
|
|
return .failure(.sdkError(error))
|
|
}
|
|
}
|
|
|
|
func logout() async {
|
|
do {
|
|
try await client.logout()
|
|
} catch {
|
|
MXLog.error("Failed logging out with error: \(error)")
|
|
}
|
|
}
|
|
|
|
func setPusher(with configuration: PusherConfiguration) async throws {
|
|
try await client.setPusher(identifiers: configuration.identifiers,
|
|
kind: configuration.kind,
|
|
appDisplayName: configuration.appDisplayName,
|
|
deviceDisplayName: configuration.deviceDisplayName,
|
|
profileTag: configuration.profileTag,
|
|
lang: configuration.lang)
|
|
}
|
|
|
|
func searchUsers(searchTerm: String, limit: UInt) async -> Result<SearchUsersResultsProxy, ClientProxyError> {
|
|
do {
|
|
return try await .success(.init(sdkResults: client.searchUsers(searchTerm: searchTerm, limit: UInt64(limit))))
|
|
} catch {
|
|
MXLog.error("Failed searching users with error: \(error)")
|
|
return .failure(.sdkError(error))
|
|
}
|
|
}
|
|
|
|
func profile(for userID: String) async -> Result<UserProfileProxy, ClientProxyError> {
|
|
do {
|
|
return try await .success(.init(sdkUserProfile: client.getProfile(userId: userID)))
|
|
} catch {
|
|
MXLog.error("Failed retrieving profile for userID: \(userID) with error: \(error)")
|
|
return .failure(.sdkError(error))
|
|
}
|
|
}
|
|
|
|
func roomDirectorySearchProxy() -> RoomDirectorySearchProxyProtocol {
|
|
RoomDirectorySearchProxy(roomDirectorySearch: client.roomDirectorySearch(), appSettings: appSettings)
|
|
}
|
|
|
|
func resolveRoomAlias(_ alias: String) async -> Result<ResolvedRoomAlias, ClientProxyError> {
|
|
do {
|
|
guard let resolvedAlias = try await client.resolveRoomAlias(roomAlias: alias) else {
|
|
MXLog.error("Failed resolving room alias, is nil")
|
|
return .failure(.failedResolvingRoomAlias)
|
|
}
|
|
|
|
// Resolving aliases is done through the directory/room API which returns too many / all known
|
|
// vias, which in turn results in invalid join requests. Trim them to something manageable
|
|
// https://github.com/element-hq/synapse/issues/17298
|
|
let limitedAlias = ResolvedRoomAlias(roomId: resolvedAlias.roomId, servers: Array(resolvedAlias.servers.prefix(50)))
|
|
|
|
return .success(limitedAlias)
|
|
} catch {
|
|
MXLog.error("Failed resolving room alias: \(alias) with error: \(error)")
|
|
return .failure(.sdkError(error))
|
|
}
|
|
}
|
|
|
|
func isAliasAvailable(_ alias: String) async -> Result<Bool, ClientProxyError> {
|
|
do {
|
|
let result = try await client.isRoomAliasAvailable(alias: alias)
|
|
return .success(result)
|
|
} catch {
|
|
MXLog.error("Failed checking if alias: \(alias) is available with error: \(error)")
|
|
return .failure(.sdkError(error))
|
|
}
|
|
}
|
|
|
|
func clearCaches() async -> Result<Void, ClientProxyError> {
|
|
do {
|
|
return try await .success(client.clearCaches(syncService: syncService))
|
|
} catch {
|
|
MXLog.error("Failed clearing client caches with error: \(error)")
|
|
return .failure(.sdkError(error))
|
|
}
|
|
}
|
|
|
|
func optimizeStores() async -> Result<Void, ClientProxyError> {
|
|
do {
|
|
return try await .success(client.optimizeStores())
|
|
} catch {
|
|
MXLog.error("Failed optimizing client stores with error: \(error)")
|
|
return .failure(.sdkError(error))
|
|
}
|
|
}
|
|
|
|
func storeSizes() async -> Result<StoreSizes, ClientProxyError> {
|
|
do {
|
|
return try await .success(client.getStoreSizes())
|
|
} catch {
|
|
MXLog.error("Failed optimizing client stores with error: \(error)")
|
|
return .failure(.sdkError(error))
|
|
}
|
|
}
|
|
|
|
func fetchMediaPreviewConfiguration() async -> Result<MediaPreviewConfig?, ClientProxyError> {
|
|
do {
|
|
let config = try await client.fetchMediaPreviewConfig()
|
|
return .success(config)
|
|
} catch {
|
|
MXLog.error("Failed fetching media preview config with error: \(error)")
|
|
return .failure(.sdkError(error))
|
|
}
|
|
}
|
|
|
|
// MARK: Ignored users
|
|
|
|
func ignoreUser(_ userID: String) async -> Result<Void, ClientProxyError> {
|
|
do {
|
|
try await client.ignoreUser(userId: userID)
|
|
return .success(())
|
|
} catch {
|
|
MXLog.error("Failed ignoring user with error: \(error)")
|
|
return .failure(.sdkError(error))
|
|
}
|
|
}
|
|
|
|
func unignoreUser(_ userID: String) async -> Result<Void, ClientProxyError> {
|
|
do {
|
|
try await client.unignoreUser(userId: userID)
|
|
return .success(())
|
|
} catch {
|
|
MXLog.error("Failed unignoring user with error: \(error)")
|
|
return .failure(.sdkError(error))
|
|
}
|
|
}
|
|
|
|
// MARK: Recently visited rooms
|
|
|
|
func trackRecentlyVisitedRoom(_ roomID: String) async -> Result<Void, ClientProxyError> {
|
|
do {
|
|
try await client.trackRecentlyVisitedRoom(room: roomID)
|
|
return .success(())
|
|
} catch {
|
|
MXLog.error("Failed tracking recently visited room: \(roomID) with error: \(error)")
|
|
return .failure(.sdkError(error))
|
|
}
|
|
}
|
|
|
|
func recentlyVisitedRooms() async -> Result<[String], ClientProxyError> {
|
|
do {
|
|
let result = try await client.getRecentlyVisitedRooms()
|
|
return .success(result)
|
|
} catch {
|
|
MXLog.error("Failed retrieving recently visited rooms with error: \(error)")
|
|
return .failure(.sdkError(error))
|
|
}
|
|
}
|
|
|
|
func recentConversationCounterparts() async -> [UserProfileProxy] {
|
|
let maxResultsToReturn = 5
|
|
|
|
guard case let .success(roomIdentifiers) = await recentlyVisitedRooms() else {
|
|
return []
|
|
}
|
|
|
|
var users: OrderedSet<UserProfileProxy> = []
|
|
|
|
for roomID in roomIdentifiers {
|
|
guard case let .joined(roomProxy) = await roomForIdentifier(roomID),
|
|
roomProxy.infoPublisher.value.isDirect,
|
|
let members = await roomProxy.members() else {
|
|
continue
|
|
}
|
|
|
|
for member in members where member.isActive && member.userID != userID {
|
|
users.append(.init(userID: member.userID, displayName: member.displayName, avatarURL: member.avatarURL))
|
|
|
|
// Return early to avoid unnecessary work
|
|
if users.count >= maxResultsToReturn {
|
|
return users.elements
|
|
}
|
|
}
|
|
}
|
|
|
|
return users.elements
|
|
}
|
|
|
|
// MARK: Moderation & Safety
|
|
|
|
func setTimelineMediaVisibility(_ value: TimelineMediaVisibility) async -> Result<Void, ClientProxyError> {
|
|
do {
|
|
try await client.setMediaPreviewDisplayPolicy(policy: value.rustValue)
|
|
return .success(())
|
|
} catch {
|
|
MXLog.error("Failed to set timeline media visibility: \(error)")
|
|
return .failure(.sdkError(error))
|
|
}
|
|
}
|
|
|
|
func setHideInviteAvatars(_ value: Bool) async -> Result<Void, ClientProxyError> {
|
|
do {
|
|
try await client.setInviteAvatarsDisplayPolicy(policy: value ? .off : .on)
|
|
return .success(())
|
|
} catch {
|
|
MXLog.error("Failed to set hide invite avatars: \(error)")
|
|
return .failure(.sdkError(error))
|
|
}
|
|
}
|
|
|
|
// MARK: - Private
|
|
|
|
private func cacheAccountURL() async {
|
|
// Calling this function for the first time will cache the account URL in volatile memory for 24 hrs on the SDK.
|
|
_ = try? await client.accountUrl(action: nil)
|
|
}
|
|
|
|
private func updateVerificationState(_ verificationState: VerificationState) async {
|
|
let verificationState: SessionVerificationState = switch verificationState {
|
|
case .unknown:
|
|
.unknown
|
|
case .unverified:
|
|
.unverified
|
|
case .verified:
|
|
.verified
|
|
}
|
|
|
|
// The session verification controller requires the user's identity which
|
|
// isn't available before a keys query response. Use the verification
|
|
// state updates as an aproximation for when that happens.
|
|
await buildSessionVerificationControllerProxyIfPossible(verificationState: verificationState)
|
|
|
|
// Only update the session verification state after creating a session
|
|
// verification proxy to avoid race conditions
|
|
verificationStateSubject.send(verificationState)
|
|
}
|
|
|
|
private func buildSessionVerificationControllerProxyIfPossible(verificationState: SessionVerificationState) async {
|
|
guard sessionVerificationController == nil, verificationState != .unknown else {
|
|
return
|
|
}
|
|
|
|
do {
|
|
let sessionVerificationController = try await client.getSessionVerificationController()
|
|
self.sessionVerificationController = SessionVerificationControllerProxy(sessionVerificationController: sessionVerificationController)
|
|
} catch {
|
|
MXLog.error("Failed retrieving session verification controller proxy with error: \(error)")
|
|
}
|
|
}
|
|
|
|
private func loadUserAvatarURLFromCache() {
|
|
loadCachedAvatarURLTask = Task {
|
|
do {
|
|
let urlString = try await self.client.cachedAvatarUrl()
|
|
guard !Task.isCancelled else { return }
|
|
self.userAvatarURLSubject.value = urlString.flatMap(URL.init)
|
|
} catch {
|
|
MXLog.error("Failed to look for the avatar url in the cache: \(error)")
|
|
}
|
|
}
|
|
}
|
|
|
|
private func createSyncServiceStateObserver(_ syncService: SyncService) -> TaskHandle {
|
|
syncService.state(listener: SDKListener { [weak self] state in
|
|
guard let self else { return }
|
|
|
|
MXLog.info("Received sync service update: \(state)")
|
|
|
|
switch state {
|
|
case .running, .terminated, .idle:
|
|
homeserverReachabilitySubject.send(.reachable)
|
|
case .offline:
|
|
homeserverReachabilitySubject.send(.unreachable)
|
|
case .error:
|
|
restartSync()
|
|
}
|
|
})
|
|
}
|
|
|
|
private func createMediaPreviewConfigObserver() async -> TaskHandle? {
|
|
do {
|
|
return try await client.subscribeToMediaPreviewConfig(listener: SDKListener { [weak self] config in
|
|
guard let self else { return }
|
|
|
|
if let config {
|
|
timelineMediaVisibilitySubject.send(config.mediaPreviewVisibility)
|
|
hideInviteAvatarsSubject.send(config.hideInviteAvatars)
|
|
} else {
|
|
// return default values
|
|
timelineMediaVisibilitySubject.send(.always)
|
|
hideInviteAvatarsSubject.send(false)
|
|
}
|
|
})
|
|
} catch {
|
|
MXLog.error("Failed creating media preview config observer: \(error)")
|
|
return nil
|
|
}
|
|
}
|
|
|
|
private func createRoomListServiceObserver(_ roomListService: RoomListService) -> TaskHandle {
|
|
roomListService.state(listener: SDKListener { [weak self] state in
|
|
guard let self else { return }
|
|
|
|
MXLog.info("Received room list update: \(state)")
|
|
|
|
guard state != .error,
|
|
state != .terminated else {
|
|
// The sync service is responsible of handling error and termination
|
|
return
|
|
}
|
|
|
|
// Hide the sync spinner as soon as we get any update back
|
|
actionsSubject.send(.receivedSyncUpdate)
|
|
|
|
if ignoredUsersSubject.value == nil {
|
|
updateIgnoredUsers()
|
|
}
|
|
})
|
|
}
|
|
|
|
private func createRoomListLoadingStateUpdateObserver(_ roomListService: RoomListService) -> TaskHandle {
|
|
roomListService.syncIndicator(delayBeforeShowingInMs: 1000, delayBeforeHidingInMs: 0, listener: SDKListener { [weak self] state in
|
|
guard let self else { return }
|
|
|
|
switch state {
|
|
case .show:
|
|
loadingStateSubject.send(.loading)
|
|
case .hide:
|
|
loadingStateSubject.send(.notLoading)
|
|
}
|
|
})
|
|
}
|
|
|
|
private func buildRoomForIdentifier(_ roomID: String) async -> RoomProxyType? {
|
|
do {
|
|
guard let room = try client.getRoom(roomId: roomID) else {
|
|
return nil
|
|
}
|
|
|
|
switch room.membership() {
|
|
case .invited:
|
|
return try await .invited(InvitedRoomProxy(room: room))
|
|
case .knocked:
|
|
guard appSettings.knockingEnabled else {
|
|
return nil
|
|
}
|
|
|
|
return try await .knocked(KnockedRoomProxy(room: room))
|
|
case .joined:
|
|
let roomProxy = try await JoinedRoomProxy(roomListService: roomListService,
|
|
room: room,
|
|
appSettings: appSettings)
|
|
|
|
return .joined(roomProxy)
|
|
case .left:
|
|
return .left
|
|
case .banned:
|
|
return try await .banned(BannedRoomProxy(room: room))
|
|
}
|
|
} catch {
|
|
MXLog.error("Failed retrieving room: \(roomID), with error: \(error)")
|
|
return nil
|
|
}
|
|
}
|
|
|
|
private func waitForRoomToSync(roomID: String, timeout: Duration = .seconds(10)) async {
|
|
MXLog.info("Wait for \(roomID)")
|
|
let runner = ExpiringTaskRunner { [weak self] in
|
|
guard let self else { return }
|
|
|
|
do {
|
|
_ = try await client.awaitRoomRemoteEcho(roomId: roomID)
|
|
MXLog.info("Wait for \(roomID) got remote echo.")
|
|
} catch {
|
|
MXLog.info("Failed waiting for remote echo in \(roomID): \(error)")
|
|
}
|
|
}
|
|
|
|
do {
|
|
try await runner.run(timeout: timeout)
|
|
} catch {
|
|
MXLog.info("Wait for \(roomID) failed: \(error)")
|
|
}
|
|
}
|
|
|
|
private func updateIgnoredUsers() {
|
|
Task {
|
|
do {
|
|
let ignoredUsers = try await client.ignoredUsers()
|
|
ignoredUsersSubject.send(ignoredUsers)
|
|
} catch {
|
|
MXLog.error("Failed fetching ignored users with error: \(error)")
|
|
}
|
|
}
|
|
}
|
|
|
|
// MARK: - Crypto
|
|
|
|
func ed25519Base64() async -> String? {
|
|
await client.encryption().ed25519Key()
|
|
}
|
|
|
|
func curve25519Base64() async -> String? {
|
|
await client.encryption().curve25519Key()
|
|
}
|
|
|
|
func pinUserIdentity(_ userID: String) async -> Result<Void, ClientProxyError> {
|
|
MXLog.info("Pinning current identity for user: \(userID)")
|
|
|
|
do {
|
|
guard let userIdentity = try await client.encryption().userIdentity(userId: userID, fallbackToServer: true) else {
|
|
MXLog.error("Failed retrieving identity for user: \(userID)")
|
|
return .failure(.failedRetrievingUserIdentity)
|
|
}
|
|
|
|
return try await .success(userIdentity.pin())
|
|
} catch {
|
|
MXLog.error("Failed pinning current identity for user: \(error)")
|
|
return .failure(.sdkError(error))
|
|
}
|
|
}
|
|
|
|
func withdrawUserIdentityVerification(_ userID: String) async -> Result<Void, ClientProxyError> {
|
|
MXLog.info("Withdrawing current identity verification for user: \(userID)")
|
|
|
|
do {
|
|
guard let userIdentity = try await client.encryption().userIdentity(userId: userID, fallbackToServer: true) else {
|
|
MXLog.error("Failed retrieving identity for user: \(userID)")
|
|
return .failure(.failedRetrievingUserIdentity)
|
|
}
|
|
|
|
return try await .success(userIdentity.withdrawVerification())
|
|
} catch {
|
|
MXLog.error("Failed withdrawing current identity verification for user: \(error)")
|
|
return .failure(.sdkError(error))
|
|
}
|
|
}
|
|
|
|
func resetIdentity() async -> Result<IdentityResetHandle?, ClientProxyError> {
|
|
do {
|
|
return try await .success(client.encryption().resetIdentity())
|
|
} catch {
|
|
return .failure(.sdkError(error))
|
|
}
|
|
}
|
|
|
|
func userIdentity(for userID: String, fallBackToServer: Bool) async -> Result<UserIdentityProxyProtocol?, ClientProxyError> {
|
|
do {
|
|
return try await .success(client.encryption().userIdentity(userId: userID, fallbackToServer: fallBackToServer).map(UserIdentityProxy.init))
|
|
} catch {
|
|
MXLog.error("Failed retrieving user identity: \(error)")
|
|
return .failure(.sdkError(error))
|
|
}
|
|
}
|
|
}
|
|
|
|
private final class ClientDelegateWrapper: ClientDelegate {
|
|
private let authErrorCallback: @Sendable (Bool) -> Void
|
|
|
|
init(authErrorCallback: @escaping @Sendable (Bool) -> Void) {
|
|
self.authErrorCallback = authErrorCallback
|
|
}
|
|
|
|
// MARK: - ClientDelegate
|
|
|
|
func didReceiveAuthError(isSoftLogout: Bool) {
|
|
MXLog.error("Received authentication error, softlogout=\(isSoftLogout)")
|
|
authErrorCallback(isSoftLogout)
|
|
}
|
|
|
|
func didRefreshTokens() {
|
|
MXLog.info("Delegating session updates to the ClientSessionDelegate.")
|
|
}
|
|
}
|
|
|
|
private final class ClientDecryptionErrorDelegate: UnableToDecryptDelegate {
|
|
private let actionsSubject: PassthroughSubject<ClientProxyAction, Never>
|
|
|
|
init(actionsSubject: PassthroughSubject<ClientProxyAction, Never>) {
|
|
self.actionsSubject = actionsSubject
|
|
}
|
|
|
|
func onUtd(info: UnableToDecryptInfo) {
|
|
actionsSubject.send(.receivedDecryptionError(info))
|
|
}
|
|
}
|
|
|
|
private struct ClientProxyServices {
|
|
let syncService: SyncService
|
|
let roomListService: RoomListService
|
|
let roomSummaryProvider: RoomSummaryProviderProtocol
|
|
let alternateRoomSummaryProvider: RoomSummaryProviderProtocol
|
|
let staticRoomSummaryProvider: StaticRoomSummaryProviderProtocol
|
|
|
|
init(client: ClientProtocol,
|
|
actionsSubject: PassthroughSubject<ClientProxyAction, Never>,
|
|
notificationSettings: NotificationSettingsProxyProtocol,
|
|
appSettings: AppSettings) async throws {
|
|
let syncService = try await client
|
|
.syncService()
|
|
.withCrossProcessLock()
|
|
.withOfflineMode()
|
|
.withSharePos(enable: true)
|
|
.finish()
|
|
|
|
let roomListService = syncService.roomListService()
|
|
|
|
let roomMessageEventStringBuilder = RoomMessageEventStringBuilder(attributedStringBuilder: AttributedStringBuilder(cacheKey: "roomList",
|
|
mentionBuilder: PlainMentionBuilder()), destination: .roomList)
|
|
let eventStringBuilder = try RoomEventStringBuilder(stateEventStringBuilder: RoomStateEventStringBuilder(userID: client.userId(), shouldDisambiguateDisplayNames: false),
|
|
messageEventStringBuilder: roomMessageEventStringBuilder,
|
|
shouldDisambiguateDisplayNames: false,
|
|
shouldPrefixSenderName: true)
|
|
|
|
roomSummaryProvider = RoomSummaryProvider(roomListService: roomListService,
|
|
eventStringBuilder: eventStringBuilder,
|
|
name: "AllRooms",
|
|
shouldUpdateVisibleRange: true,
|
|
notificationSettings: notificationSettings,
|
|
appSettings: appSettings)
|
|
try await roomSummaryProvider.setRoomList(roomListService.allRooms())
|
|
|
|
alternateRoomSummaryProvider = RoomSummaryProvider(roomListService: roomListService,
|
|
eventStringBuilder: eventStringBuilder,
|
|
name: "AlternateAllRooms",
|
|
notificationSettings: notificationSettings,
|
|
appSettings: appSettings)
|
|
try await alternateRoomSummaryProvider.setRoomList(roomListService.allRooms())
|
|
|
|
staticRoomSummaryProvider = RoomSummaryProvider(roomListService: roomListService,
|
|
eventStringBuilder: eventStringBuilder,
|
|
name: "StaticAllRooms",
|
|
roomListPageSize: .max,
|
|
notificationSettings: notificationSettings,
|
|
appSettings: appSettings)
|
|
try await staticRoomSummaryProvider.setRoomList(roomListService.allRooms())
|
|
|
|
self.syncService = syncService
|
|
self.roomListService = roomListService
|
|
}
|
|
}
|
|
|
|
private extension MediaPreviewConfig {
|
|
var mediaPreviewVisibility: TimelineMediaVisibility {
|
|
switch mediaPreviews {
|
|
case .on:
|
|
.always
|
|
case .private:
|
|
.privateOnly
|
|
case .off:
|
|
.never
|
|
case .none:
|
|
.always
|
|
}
|
|
}
|
|
|
|
var hideInviteAvatars: Bool {
|
|
switch inviteAvatars {
|
|
case .off:
|
|
true
|
|
case .on:
|
|
false
|
|
case .none:
|
|
true
|
|
}
|
|
}
|
|
}
|
|
|
|
private extension TimelineMediaVisibility {
|
|
var rustValue: MediaPreviews {
|
|
switch self {
|
|
case .always:
|
|
.on
|
|
case .never:
|
|
.off
|
|
case .privateOnly:
|
|
.private
|
|
}
|
|
}
|
|
}
|