diff --git a/ElementX/Sources/Application/Settings/UserSettings.swift b/ElementX/Sources/Application/Settings/UserSettings.swift new file mode 100644 index 000000000..04394b2ab --- /dev/null +++ b/ElementX/Sources/Application/Settings/UserSettings.swift @@ -0,0 +1,49 @@ +// +// Copyright 2026 Element Creations 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 + +/// The settings for a signed in account, composed of the app wide settings and the settings +/// specific to that account. +@dynamicMemberLookup +final nonisolated class UserSettings: Sendable { + let app: AppSettings + let account: AccountSettings + + init(appSettings: AppSettings, accountSettings: AccountSettings) { + self.app = appSettings + self.account = accountSettings + } + + static func volatile() -> UserSettings { + let appSettings = AppSettings.volatile() + return UserSettings(appSettings: appSettings, accountSettings: .init()) + } + + subscript(dynamicMember keyPath: ReferenceWritableKeyPath) -> Value { + get { app[keyPath: keyPath] } + set { app[keyPath: keyPath] = newValue } + } + + subscript(dynamicMember keyPath: KeyPath) -> Value { + app[keyPath: keyPath] + } + + subscript(dynamicMember keyPath: ReferenceWritableKeyPath) -> Value { + get { account[keyPath: keyPath] } + set { account[keyPath: keyPath] = newValue } + } + + subscript(dynamicMember keyPath: KeyPath) -> Value { + account[keyPath: keyPath] + } +} + +/// Settings specific to an account. +struct AccountSettings { + // Placeholder for now, implementation to follow with a dedicated user defaults store scoped to a specific user ID. +} diff --git a/ElementX/Sources/Services/Session/UserSession.swift b/ElementX/Sources/Services/Session/UserSession.swift index c03604811..58ce52364 100644 --- a/ElementX/Sources/Services/Session/UserSession.swift +++ b/ElementX/Sources/Services/Session/UserSession.swift @@ -15,11 +15,11 @@ class UserSession: UserSessionProtocol { private var authErrorCancellable: AnyCancellable? let clientProxy: ClientProxyProtocol - let mediaProvider: MediaProviderProtocol + let userSettings: UserSettings + let mediaProvider: MediaProviderProtocol let voiceMessageMediaManager: VoiceMessageMediaManagerProtocol let liveLocationManager: LiveLocationManagerProtocol - /// Scans media content, `nil` when no content scanner is configured for the server. let contentScannerService: ContentScannerServiceProtocol? @@ -30,8 +30,14 @@ class UserSession: UserSessionProtocol { sessionSecurityStateSubject.asCurrentValuePublisher() } - init(clientProxy: ClientProxyProtocol, mediaProvider: MediaProviderProtocol, voiceMessageMediaManager: VoiceMessageMediaManagerProtocol, liveLocationManager: LiveLocationManagerProtocol) { + init(clientProxy: ClientProxyProtocol, + userSettings: UserSettings, + mediaProvider: MediaProviderProtocol, + voiceMessageMediaManager: VoiceMessageMediaManagerProtocol, + liveLocationManager: LiveLocationManagerProtocol) { self.clientProxy = clientProxy + self.userSettings = userSettings + self.mediaProvider = mediaProvider self.voiceMessageMediaManager = voiceMessageMediaManager self.liveLocationManager = liveLocationManager diff --git a/ElementX/Sources/Services/Session/UserSessionProtocol.swift b/ElementX/Sources/Services/Session/UserSessionProtocol.swift index 243163a13..7ea8bf32c 100644 --- a/ElementX/Sources/Services/Session/UserSessionProtocol.swift +++ b/ElementX/Sources/Services/Session/UserSessionProtocol.swift @@ -21,10 +21,11 @@ struct SessionSecurityState: Equatable { // sourcery: AutoMockable protocol UserSessionProtocol: Sendable { var clientProxy: ClientProxyProtocol { get } + var userSettings: UserSettings { get } + var mediaProvider: MediaProviderProtocol { get } var voiceMessageMediaManager: VoiceMessageMediaManagerProtocol { get } var liveLocationManager: LiveLocationManagerProtocol { get } - /// Scans media content, `nil` when no content scanner is configured for the server. var contentScannerService: ContentScannerServiceProtocol? { get } diff --git a/ElementX/Sources/Services/UserSession/UserSessionStore.swift b/ElementX/Sources/Services/UserSession/UserSessionStore.swift index 2c75656ad..99ab6e7e5 100644 --- a/ElementX/Sources/Services/UserSession/UserSessionStore.swift +++ b/ElementX/Sources/Services/UserSession/UserSessionStore.swift @@ -59,9 +59,11 @@ class UserSessionStore: UserSessionStoreProtocol { return .failure(.missingCredentials) } - switch await restorePreviousLogin(credentials) { + let userSettings = UserSettings(appSettings: appSettings, accountSettings: .init()) + + switch await restorePreviousLogin(credentials, userSettings: userSettings) { case .success(let clientProxy): - return await .success(buildUserSessionWithClient(clientProxy)) + return await .success(buildUserSessionWithClient(clientProxy, userSettings: userSettings)) case .failure(let error): MXLog.error("Failed restoring login with error: \(error)") @@ -77,7 +79,8 @@ class UserSessionStore: UserSessionStoreProtocol { do { let session = try client.session() let userID = try client.userId() - let clientProxy = try await setupProxyForClient(client) + let userSettings = UserSettings(appSettings: appSettings, accountSettings: .init()) + let clientProxy = try await setupProxyForClient(client, userSettings: userSettings) keychainController.setRestorationToken(RestorationToken(session: session, sessionDirectories: sessionDirectories, @@ -87,7 +90,7 @@ class UserSessionStore: UserSessionStoreProtocol { MXLog.info("Set up session for user \(userID) at: \(sessionDirectories)") - return await .success(buildUserSessionWithClient(clientProxy)) + return await .success(buildUserSessionWithClient(clientProxy, userSettings: userSettings)) } catch { MXLog.error("Failed creating user session with error: \(error)") return .failure(.failedSettingUpSession) @@ -106,7 +109,7 @@ class UserSessionStore: UserSessionStoreProtocol { // MARK: - Private - private func buildUserSessionWithClient(_ clientProxy: ClientProxyProtocol) async -> UserSessionProtocol { + private func buildUserSessionWithClient(_ clientProxy: ClientProxyProtocol, userSettings: UserSettings) async -> UserSessionProtocol { let mediaProvider = MediaProvider(mediaLoader: clientProxy.mediaLoader, imageCache: .onlyInMemory, homeserverReachabilityPublisher: clientProxy.homeserverReachabilityPublisher) @@ -115,16 +118,17 @@ class UserSessionStore: UserSessionStoreProtocol { let liveLocationManager = await MainActor.run { LiveLocationManager(clientProxy: clientProxy, - appSettings: appSettings) + userSettings: userSettings) } return UserSession(clientProxy: clientProxy, + userSettings: userSettings, mediaProvider: mediaProvider, voiceMessageMediaManager: voiceMessageMediaManager, liveLocationManager: liveLocationManager) } - private func restorePreviousLogin(_ credentials: KeychainCredentials) async -> Result { + private func restorePreviousLogin(_ credentials: KeychainCredentials, userSettings: UserSettings) async -> Result { guard credentials.restorationToken.sessionDirectories.isNonTransientUserDataValid() else { MXLog.error("Failed restoring login, missing non-transient user data") return .failure(.failedRestoringLogin) @@ -142,7 +146,7 @@ class UserSessionStore: UserSessionStoreProtocol { Task(priority: .low) { await appHooks.remoteSettingsHook.updateCache(using: client) } - return try await .success(setupProxyForClient(client)) + return try await .success(setupProxyForClient(client, userSettings: userSettings)) } catch UserSessionStoreError.failedSettingUpClientProxy(let error) { // If this has failed, there is likely something wrong with the creation of the sync service // There is nothing we can do, but at the same time we don't want the user to the get logged out @@ -154,11 +158,11 @@ class UserSessionStore: UserSessionStoreProtocol { } } - private func setupProxyForClient(_ client: ClientProtocol) async throws -> ClientProxyProtocol { + private func setupProxyForClient(_ client: ClientProtocol, userSettings: UserSettings) async throws -> ClientProxyProtocol { do { return try await ClientProxy(client: client, networkMonitor: networkMonitor, - appSettings: appSettings, + userSettings: userSettings, analyticsService: analyticsService) } catch { throw UserSessionStoreError.failedSettingUpClientProxy(error)