// // Copyright 2025 Element Creations Ltd. // Copyright 2023-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. // import Foundation import MatrixRustSDK // sourcery: AutoMockable nonisolated protocol NSEUserSessionProtocol { var inviteAvatarsVisibility: InviteAvatars { get async } var mediaPreviewVisibility: MediaPreviews { get async } var threadsEnabled: Bool { get } } final nonisolated class NSEUserSession: NSEUserSessionProtocol { let userSettings: UserSettings private let baseClient: ClientProtocol private let notificationClient: NotificationClient private let userID: String private(set) lazy var mediaProvider: MediaProviderProtocol = MediaProvider(mediaLoader: MediaLoader(client: baseClient), imageCache: .onlyOnDisk, homeserverReachabilityPublisher: nil) private let delegateHandle: TaskHandle? var mediaPreviewVisibility: MediaPreviews { get async { do { return try await baseClient.getMediaPreviewDisplayPolicy() ?? .on } catch { MXLog.error("Failed to get media preview visibility, defaulting to on. Error: \(error)") return .on } } } var inviteAvatarsVisibility: InviteAvatars { get async { do { return try await baseClient.getInviteAvatarsDisplayPolicy() ?? .on } catch { MXLog.error("Failed to get invite avatars visibility, defaulting to on. Error: \(error)") return .on } } } var threadsEnabled: Bool { userSettings.app.threadsEnabled } init(credentials: KeychainCredentials, roomID: String, clientSessionDelegate: ClientSessionDelegate, clientFactory: ClientFactoryProtocol = ClientFactory(), appSettings: AppSettings, appHooks: AppHooks) async throws { userID = credentials.userID userSettings = appSettings.userSettings(for: credentials.userID) baseClient = try await clientFactory.makeNSEClient(credentials: credentials, roomID: roomID, clientSessionDelegate: clientSessionDelegate, userSettings: userSettings, appHooks: appHooks) do { try await baseClient.setPresence(presence: .offline, immediate: false) } catch { MXLog.error("Failed configuring offline presence before notification processing with error: \(error)") } delegateHandle = try baseClient.setDelegate(delegate: ClientDelegateWrapper()) // Inject the content scanner so the SDK gates the media it downloads whilst building the notification. if let contentScannerURL = userSettings.app.contentScannerURL.publisher.value { let contentScanner = ContentScanner(scannerUrl: contentScannerURL.absoluteString) await baseClient.setContentScanner(contentScanner: contentScanner) } notificationClient = try await baseClient.notificationClient(processSetup: .multipleProcesses) } func notificationItemProxy(roomID: String, eventID: String) async -> NotificationItemProxyProtocol? { do { let notificationStatus = try await notificationClient.getNotification(roomId: roomID, eventId: eventID) switch notificationStatus { case .event(let notification): return NotificationItemProxy(notificationItem: notification, receiverID: userID, roomID: roomID) case .eventNotFound: MXLog.error("Notification event not found - roomID: \(roomID) eventID: \(eventID)") return nil case .eventFilteredOut: MXLog.warning("Notification event filtered out - roomID: \(roomID) eventID: \(eventID)") return nil case .eventRedacted: MXLog.warning("Notification event redacted - roomID: \(roomID) eventID: \(eventID)") return nil } } catch { MXLog.error("Could not get notification's content creating an empty notification instead, error: \(error)") return EmptyNotificationItemProxy(roomID: roomID, receiverID: userID) } } func roomForIdentifier(_ roomID: String) -> Room? { do { return try notificationClient.getRoom(roomId: roomID) } catch { MXLog.error("Failed retrieving room with error: \(error)") return nil } } deinit { delegateHandle?.cancel() } } private final nonisolated class ClientDelegateWrapper: ClientDelegate { // MARK: - ClientDelegate func didReceiveAuthError(isSoftLogout: Bool) { MXLog.error("Received authentication error, the NSE can't handle this.") } // periphery:ignore - required by the SDK's delegate protocol func didRefreshTokens() { MXLog.info("Delegating session updates to the ClientSessionDelegate.") } func onBackgroundTaskErrorReport(taskName: String, error: MatrixRustSDK.BackgroundTaskFailureReason) { switch error { case .panic(let message, let backtrace): MXLog.error("Received background task panic: \(message ?? "Missing message")\nBacktrace:\n\(backtrace ?? "Missing backtrace")") case .error(let error): MXLog.error("Received background task error: \(error)") case .earlyTermination: MXLog.error("Received background task early termination") } } }