mirror of
https://github.com/Novaloop-AG/element-x-ios.git
synced 2026-11-04 01:20:12 +00:00
Replace the UserPreference property wrapper with the new macro version.
This commit is contained in:
@@ -170,7 +170,7 @@ class AppCoordinator: AppCoordinatorProtocol, AuthenticationFlowCoordinatorDeleg
|
|||||||
|
|
||||||
registerBackgroundAppRefresh()
|
registerBackgroundAppRefresh()
|
||||||
|
|
||||||
appSettings.$analyticsConsentState
|
appSettings.analyticsConsentStatePublisher
|
||||||
.dropFirst() // Sentry is configured during init; only reconfigure when consent state actually changes
|
.dropFirst() // Sentry is configured during init; only reconfigure when consent state actually changes
|
||||||
.sink { [bugReportService, analyticsService, appSettings] _ in
|
.sink { [bugReportService, analyticsService, appSettings] _ in
|
||||||
Self.setupSentry(bugReportService: bugReportService, appSettings: appSettings, analytics: analyticsService)
|
Self.setupSentry(bugReportService: bugReportService, appSettings: appSettings, analytics: analyticsService)
|
||||||
@@ -221,7 +221,7 @@ class AppCoordinator: AppCoordinatorProtocol, AuthenticationFlowCoordinatorDeleg
|
|||||||
func toPresentable() -> AnyView {
|
func toPresentable() -> AnyView {
|
||||||
AnyView(navigationRootCoordinator.toPresentable()
|
AnyView(navigationRootCoordinator.toPresentable()
|
||||||
.environment(\.analyticsService, analyticsService)
|
.environment(\.analyticsService, analyticsService)
|
||||||
.onReceive(appSettings.$appAppearance) { [weak self] appAppearance in
|
.onReceive(appSettings.appAppearancePublisher) { [weak self] appAppearance in
|
||||||
guard let self else { return }
|
guard let self else { return }
|
||||||
|
|
||||||
windowManager.windows.forEach { window in
|
windowManager.windows.forEach { window in
|
||||||
|
|||||||
@@ -10,7 +10,9 @@
|
|||||||
import EmbeddedElementCall
|
import EmbeddedElementCall
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
import Combine
|
||||||
import Foundation
|
import Foundation
|
||||||
|
import Macros
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
|
|
||||||
/// Common settings between app and NSE
|
/// Common settings between app and NSE
|
||||||
@@ -32,76 +34,8 @@ nonisolated enum AppBuildType {
|
|||||||
case debug
|
case debug
|
||||||
case nightly
|
case nightly
|
||||||
case release
|
case release
|
||||||
}
|
|
||||||
|
|
||||||
/// Store Element specific app settings.
|
|
||||||
///
|
|
||||||
/// State is persisted in `UserDefaults`, which is thread-safe per Apple's documentation, hence `@unchecked`.
|
|
||||||
final nonisolated class AppSettings: @unchecked Sendable {
|
|
||||||
fileprivate enum UserDefaultsKeys: String, PreferenceKeyable {
|
|
||||||
case lastVersionLaunched
|
|
||||||
case seenInvites
|
|
||||||
case hasSeenNewSoundBanner
|
|
||||||
case appLockNumberOfPINAttempts
|
|
||||||
case appLockNumberOfBiometricAttempts
|
|
||||||
case timelineStyle
|
|
||||||
|
|
||||||
case analyticsConsentState
|
|
||||||
case hasRunNotificationPermissionsOnboarding
|
|
||||||
case hasRunIdentityConfirmationOnboarding
|
|
||||||
case hasRequestedLocationAlwaysLocationAuthorization
|
|
||||||
|
|
||||||
case frequentlyUsedSystemEmojis
|
|
||||||
|
|
||||||
case enableNotifications
|
|
||||||
case enableInAppNotifications
|
|
||||||
case pusherProfileTag
|
|
||||||
case lastNotificationBootTime
|
|
||||||
case selectedNotificationTone
|
|
||||||
case logLevel
|
|
||||||
case traceLogPacks
|
|
||||||
case viewSourceEnabled
|
|
||||||
case optimizeMediaUploads
|
|
||||||
case appAppearance
|
|
||||||
case sharePresence
|
|
||||||
|
|
||||||
case elementCallBaseURLOverride
|
|
||||||
|
|
||||||
case voiceMessagePlaybackSpeed
|
|
||||||
|
|
||||||
// Live Location
|
|
||||||
case liveLocationSharingTimeoutDatesByRoomID
|
|
||||||
case liveLocationMinimumDistanceUpdate
|
|
||||||
case liveLocationDisclaimerDisplayed
|
|
||||||
|
|
||||||
// Feature flags
|
|
||||||
case fuzzyRoomListSearchEnabled
|
|
||||||
case lowPriorityFilterEnabled
|
|
||||||
case enableOnlySignedDeviceIsolationMode
|
|
||||||
case knockingEnabled
|
|
||||||
case threadsEnabled
|
|
||||||
case globalSearchEnabled
|
|
||||||
case roomThreadListEnabled
|
|
||||||
case linkPreviewsEnabled
|
|
||||||
case jumpToReadMarkerEnabled
|
|
||||||
case focusEventOnNotificationTap
|
|
||||||
case linkNewDeviceEnabled
|
|
||||||
case automaticBackPaginationEnabled
|
|
||||||
case clientPausingAndResumingEnabled
|
|
||||||
|
|
||||||
// Doug's tweaks 🔧
|
|
||||||
case roomListActivityVisibility
|
|
||||||
case hideQuietNotificationAlerts
|
|
||||||
|
|
||||||
case developerOptionsEnabled
|
|
||||||
}
|
|
||||||
|
|
||||||
static let suiteName: String = InfoPlistReader.main.appGroupIdentifier
|
static var current: AppBuildType {
|
||||||
|
|
||||||
/// UserDefaults to be used on reads and writes.
|
|
||||||
private let store: UserDefaultsProtocol
|
|
||||||
|
|
||||||
static var appBuildType: AppBuildType {
|
|
||||||
#if DEBUG
|
#if DEBUG
|
||||||
return .debug
|
return .debug
|
||||||
#else
|
#else
|
||||||
@@ -113,6 +47,20 @@ final nonisolated class AppSettings: @unchecked Sendable {
|
|||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Store Element specific app settings.
|
||||||
|
///
|
||||||
|
/// State is persisted in `UserDefaults`, which is thread-safe per Apple's documentation, hence `@unchecked`.
|
||||||
|
final nonisolated class AppSettings: @unchecked Sendable {
|
||||||
|
static let suiteName: String = InfoPlistReader.main.appGroupIdentifier
|
||||||
|
|
||||||
|
/// UserDefaults to be used on reads and writes.
|
||||||
|
private let store: UserDefaultsProtocol
|
||||||
|
|
||||||
|
static var appBuildType: AppBuildType {
|
||||||
|
AppBuildType.current
|
||||||
|
}
|
||||||
|
|
||||||
func resetAllSettings() {
|
func resetAllSettings() {
|
||||||
MXLog.warning("Resetting the AppSettings.")
|
MXLog.warning("Resetting the AppSettings.")
|
||||||
@@ -121,7 +69,7 @@ final nonisolated class AppSettings: @unchecked Sendable {
|
|||||||
|
|
||||||
func resetSessionSpecificSettings() {
|
func resetSessionSpecificSettings() {
|
||||||
MXLog.warning("Resetting the user session specific AppSettings.")
|
MXLog.warning("Resetting the user session specific AppSettings.")
|
||||||
store.removeObject(forKey: UserDefaultsKeys.hasRunIdentityConfirmationOnboarding.rawValue)
|
resetHasRunIdentityConfirmationOnboarding()
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - Hooks
|
// MARK: - Hooks
|
||||||
@@ -175,16 +123,16 @@ final nonisolated class AppSettings: @unchecked Sendable {
|
|||||||
/// used to detect when migrations should be run. When `nil` the app may have been
|
/// used to detect when migrations should be run. When `nil` the app may have been
|
||||||
/// deleted between runs so should clear data in the shared container and keychain.
|
/// deleted between runs so should clear data in the shared container and keychain.
|
||||||
@UserPreference
|
@UserPreference
|
||||||
nonisolated(unsafe) var lastVersionLaunched: String?
|
var lastVersionLaunched: String?
|
||||||
|
|
||||||
/// The Set of room identifiers of invites that the user already saw in the invites list.
|
/// The Set of room identifiers of invites that the user already saw in the invites list.
|
||||||
/// This Set is being used to implement badges for unread invites.
|
/// This Set is being used to implement badges for unread invites.
|
||||||
@UserPreference
|
@UserPreference(defaultValue: Set<String>())
|
||||||
nonisolated(unsafe) var seenInvites: Set<String>
|
var seenInvites: Set<String>
|
||||||
|
|
||||||
/// Defaults to `true` for new users, and we use a migration to set it to `false` for existing users.
|
/// Defaults to `true` for new users, and we use a migration to set it to `false` for existing users.
|
||||||
@UserPreference
|
@UserPreference(defaultValue: true)
|
||||||
nonisolated(unsafe) var hasSeenNewSoundBanner: Bool
|
var hasSeenNewSoundBanner: Bool
|
||||||
|
|
||||||
/// The initial set of account providers shown to the user in the authentication flow.
|
/// The initial set of account providers shown to the user in the authentication flow.
|
||||||
///
|
///
|
||||||
@@ -228,8 +176,8 @@ final nonisolated class AppSettings: @unchecked Sendable {
|
|||||||
/// **Note:** This property isn't overridable as it in unexpected for forks to come across the error (or to even have a "Pro" app).
|
/// **Note:** This property isn't overridable as it in unexpected for forks to come across the error (or to even have a "Pro" app).
|
||||||
let elementProAppStoreURL: URL = "https://apps.apple.com/app/element-pro-for-work/id6502951615"
|
let elementProAppStoreURL: URL = "https://apps.apple.com/app/element-pro-for-work/id6502951615"
|
||||||
|
|
||||||
@UserPreference
|
@UserPreference(defaultValue: AppAppearance.system)
|
||||||
nonisolated(unsafe) var appAppearance: AppAppearance
|
var appAppearance: AppAppearance
|
||||||
|
|
||||||
// MARK: - Security
|
// MARK: - Security
|
||||||
|
|
||||||
@@ -240,8 +188,8 @@ final nonisolated class AppSettings: @unchecked Sendable {
|
|||||||
/// Any codes that the user isn't allowed to use for their PIN.
|
/// Any codes that the user isn't allowed to use for their PIN.
|
||||||
let appLockPINCodeBlockList = ["0000", "1234"]
|
let appLockPINCodeBlockList = ["0000", "1234"]
|
||||||
/// The number of attempts the user has made to unlock the app with a PIN code (resets when unlocked).
|
/// The number of attempts the user has made to unlock the app with a PIN code (resets when unlocked).
|
||||||
@UserPreference
|
@UserPreference(defaultValue: 0)
|
||||||
nonisolated(unsafe) var appLockNumberOfPINAttempts: Int
|
var appLockNumberOfPINAttempts: Int
|
||||||
|
|
||||||
// MARK: - Authentication
|
// MARK: - Authentication
|
||||||
|
|
||||||
@@ -281,34 +229,34 @@ final nonisolated class AppSettings: @unchecked Sendable {
|
|||||||
pushGatewayBaseURL.appending(path: "_matrix/push/v1/notify")
|
pushGatewayBaseURL.appending(path: "_matrix/push/v1/notify")
|
||||||
}
|
}
|
||||||
|
|
||||||
@UserPreference
|
@UserPreference(defaultValue: true)
|
||||||
nonisolated(unsafe) var enableNotifications: Bool
|
var enableNotifications: Bool
|
||||||
|
|
||||||
@UserPreference
|
@UserPreference(defaultValue: true)
|
||||||
nonisolated(unsafe) var enableInAppNotifications: Bool
|
var enableInAppNotifications: Bool
|
||||||
|
|
||||||
@UserPreference
|
@UserPreference(defaultValue: false)
|
||||||
nonisolated(unsafe) var hideQuietNotificationAlerts: Bool
|
var hideQuietNotificationAlerts: Bool
|
||||||
|
|
||||||
/// Tag describing which set of device specific rules a pusher executes.
|
/// Tag describing which set of device specific rules a pusher executes.
|
||||||
@UserPreference
|
@UserPreference
|
||||||
nonisolated(unsafe) var pusherProfileTag: String?
|
var pusherProfileTag: String?
|
||||||
|
|
||||||
/// The device's last boot time as recorded by the NSE.
|
/// The device's last boot time as recorded by the NSE.
|
||||||
@UserPreference
|
@UserPreference
|
||||||
nonisolated(unsafe) var lastNotificationBootTime: TimeInterval?
|
var lastNotificationBootTime: TimeInterval?
|
||||||
|
|
||||||
/// The sound played when delivering noisy notifications. If nil, use the ElementX default
|
/// The sound played when delivering noisy notifications. If nil, use the ElementX default
|
||||||
@UserPreference
|
@UserPreference
|
||||||
nonisolated(unsafe) var selectedNotificationTone: NotificationTone?
|
var selectedNotificationTone: NotificationTone?
|
||||||
|
|
||||||
// MARK: - Logging
|
// MARK: - Logging
|
||||||
|
|
||||||
@UserPreference
|
@UserPreference(defaultValue: LogLevel.info)
|
||||||
nonisolated(unsafe) var logLevel: LogLevel
|
var logLevel: LogLevel
|
||||||
|
|
||||||
@UserPreference
|
@UserPreference(defaultValue: Set<TraceLogPack>())
|
||||||
nonisolated(unsafe) var traceLogPacks: Set<TraceLogPack>
|
var traceLogPacks: Set<TraceLogPack>
|
||||||
|
|
||||||
// MARK: - Bug report
|
// MARK: - Bug report
|
||||||
|
|
||||||
@@ -335,47 +283,47 @@ final nonisolated class AppSettings: @unchecked Sendable {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Whether the user has opted in to send analytics.
|
/// Whether the user has opted in to send analytics.
|
||||||
@UserPreference
|
@UserPreference(defaultValue: AnalyticsConsentState.unknown)
|
||||||
nonisolated(unsafe) var analyticsConsentState: AnalyticsConsentState
|
var analyticsConsentState: AnalyticsConsentState
|
||||||
|
|
||||||
@UserPreference
|
@UserPreference(defaultValue: false)
|
||||||
nonisolated(unsafe) var hasRunNotificationPermissionsOnboarding: Bool
|
var hasRunNotificationPermissionsOnboarding: Bool
|
||||||
|
|
||||||
@UserPreference
|
@UserPreference(defaultValue: false)
|
||||||
nonisolated(unsafe) var hasRunIdentityConfirmationOnboarding: Bool
|
var hasRunIdentityConfirmationOnboarding: Bool
|
||||||
|
|
||||||
@UserPreference
|
@UserPreference(defaultValue: false)
|
||||||
nonisolated(unsafe) var hasRequestedLocationAlwaysLocationAuthorization: Bool
|
var hasRequestedLocationAlwaysLocationAuthorization: Bool
|
||||||
|
|
||||||
@UserPreference
|
@UserPreference(defaultValue: [FrequentlyUsedEmoji]())
|
||||||
nonisolated(unsafe) var frequentlyUsedSystemEmojis: [FrequentlyUsedEmoji]
|
var frequentlyUsedSystemEmojis: [FrequentlyUsedEmoji]
|
||||||
|
|
||||||
// MARK: - Live Location
|
// MARK: - Live Location
|
||||||
|
|
||||||
@UserPreference
|
@UserPreference(key: "liveLocationSharingTimeoutDatesByRoomID", defaultValue: [String: LiveLocationSession]())
|
||||||
nonisolated(unsafe) var liveLocationSharingSessionsByRoomID: [String: LiveLocationSession]
|
var liveLocationSharingSessionsByRoomID: [String: LiveLocationSession]
|
||||||
|
|
||||||
@UserPreference
|
@UserPreference(defaultValue: 10)
|
||||||
nonisolated(unsafe) var liveLocationMinimumDistanceUpdate: Int
|
var liveLocationMinimumDistanceUpdate: Int
|
||||||
|
|
||||||
@UserPreference
|
@UserPreference(defaultValue: false)
|
||||||
nonisolated(unsafe) var liveLocationDisclaimerDisplayed: Bool
|
var liveLocationDisclaimerDisplayed: Bool
|
||||||
|
|
||||||
// MARK: - Home Screen
|
// MARK: - Home Screen
|
||||||
|
|
||||||
@UserPreference
|
@UserPreference(defaultValue: RoomListActivityVisibility.current)
|
||||||
nonisolated(unsafe) var roomListActivityVisibility: RoomListActivityVisibility
|
var roomListActivityVisibility: RoomListActivityVisibility
|
||||||
|
|
||||||
// MARK: - Room Screen
|
// MARK: - Room Screen
|
||||||
|
|
||||||
@UserPreference
|
@UserPreference(defaultValue: AppBuildType.current == .debug)
|
||||||
nonisolated(unsafe) var viewSourceEnabled: Bool
|
var viewSourceEnabled: Bool
|
||||||
|
|
||||||
@UserPreference
|
@UserPreference(defaultValue: true)
|
||||||
nonisolated(unsafe) var optimizeMediaUploads: Bool
|
var optimizeMediaUploads: Bool
|
||||||
|
|
||||||
@UserPreference
|
@UserPreference(defaultValue: AudioPlaybackSpeed.default)
|
||||||
nonisolated(unsafe) var voiceMessagePlaybackSpeed: AudioPlaybackSpeed
|
var voiceMessagePlaybackSpeed: AudioPlaybackSpeed
|
||||||
|
|
||||||
/// Whether or not to show a warning on the media caption composer so the user knows
|
/// Whether or not to show a warning on the media caption composer so the user knows
|
||||||
/// that captions might not be visible to users who are using other Matrix clients.
|
/// that captions might not be visible to users who are using other Matrix clients.
|
||||||
@@ -394,7 +342,7 @@ final nonisolated class AppSettings: @unchecked Sendable {
|
|||||||
let elementCallPosthogSentryDSN = "https://3bd2f95ba5554d4497da7153b552ffb5@sentry.tools.element.io/41"
|
let elementCallPosthogSentryDSN = "https://3bd2f95ba5554d4497da7153b552ffb5@sentry.tools.element.io/41"
|
||||||
|
|
||||||
@UserPreference
|
@UserPreference
|
||||||
nonisolated(unsafe) var elementCallBaseURLOverride: URL?
|
var elementCallBaseURLOverride: URL?
|
||||||
|
|
||||||
// MARK: - Users
|
// MARK: - Users
|
||||||
|
|
||||||
@@ -416,100 +364,57 @@ final nonisolated class AppSettings: @unchecked Sendable {
|
|||||||
|
|
||||||
// MARK: - Presence
|
// MARK: - Presence
|
||||||
|
|
||||||
@UserPreference
|
@UserPreference(defaultValue: true)
|
||||||
nonisolated(unsafe) var sharePresence: Bool
|
var sharePresence: Bool
|
||||||
|
|
||||||
// MARK: - Feature Flags
|
// MARK: - Feature Flags
|
||||||
|
|
||||||
/// Others
|
/// Others
|
||||||
@UserPreference
|
@UserPreference(defaultValue: false)
|
||||||
nonisolated(unsafe) var fuzzyRoomListSearchEnabled: Bool
|
var fuzzyRoomListSearchEnabled: Bool
|
||||||
|
|
||||||
@UserPreference
|
@UserPreference(defaultValue: false)
|
||||||
nonisolated(unsafe) var lowPriorityFilterEnabled: Bool
|
var lowPriorityFilterEnabled: Bool
|
||||||
|
|
||||||
/// Configuration to enable only signed device isolation mode for crypto. In this mode only devices signed by their owner will be considered in e2ee rooms.
|
/// Configuration to enable only signed device isolation mode for crypto. In this mode only devices signed by their owner will be considered in e2ee rooms.
|
||||||
@UserPreference
|
@UserPreference(defaultValue: false)
|
||||||
nonisolated(unsafe) var enableOnlySignedDeviceIsolationMode: Bool
|
var enableOnlySignedDeviceIsolationMode: Bool
|
||||||
|
|
||||||
@UserPreference
|
@UserPreference(defaultValue: false)
|
||||||
nonisolated(unsafe) var knockingEnabled: Bool
|
var knockingEnabled: Bool
|
||||||
|
|
||||||
@UserPreference
|
@UserPreference(defaultValue: false)
|
||||||
nonisolated(unsafe) var threadsEnabled: Bool
|
var threadsEnabled: Bool
|
||||||
|
|
||||||
@UserPreference
|
@UserPreference(defaultValue: false)
|
||||||
nonisolated(unsafe) var roomThreadListEnabled: Bool
|
var roomThreadListEnabled: Bool
|
||||||
|
|
||||||
@UserPreference
|
@UserPreference(defaultValue: ProcessInfo().isiOSAppOnMac)
|
||||||
nonisolated(unsafe) var globalSearchEnabled: Bool
|
var globalSearchEnabled: Bool
|
||||||
|
|
||||||
@UserPreference
|
@UserPreference(defaultValue: false)
|
||||||
nonisolated(unsafe) var focusEventOnNotificationTap: Bool
|
var focusEventOnNotificationTap: Bool
|
||||||
|
|
||||||
@UserPreference
|
@UserPreference(defaultValue: false)
|
||||||
nonisolated(unsafe) var linkPreviewsEnabled: Bool
|
var linkPreviewsEnabled: Bool
|
||||||
|
|
||||||
@UserPreference
|
@UserPreference(defaultValue: false)
|
||||||
nonisolated(unsafe) var jumpToReadMarkerEnabled: Bool
|
var jumpToReadMarkerEnabled: Bool
|
||||||
|
|
||||||
@UserPreference
|
@UserPreference(defaultValue: false)
|
||||||
nonisolated(unsafe) var linkNewDeviceEnabled: Bool
|
var linkNewDeviceEnabled: Bool
|
||||||
|
|
||||||
@UserPreference
|
@UserPreference(defaultValue: false)
|
||||||
nonisolated(unsafe) var automaticBackPaginationEnabled: Bool
|
var automaticBackPaginationEnabled: Bool
|
||||||
|
|
||||||
@UserPreference
|
@UserPreference(defaultValue: AppBuildType.current != .release, volatile: true)
|
||||||
nonisolated(unsafe) var clientPausingAndResumingEnabled: Bool
|
var clientPausingAndResumingEnabled: Bool
|
||||||
|
|
||||||
@UserPreference
|
@UserPreference(defaultValue: AppBuildType.current != .release)
|
||||||
nonisolated(unsafe) var developerOptionsEnabled: Bool
|
var developerOptionsEnabled: Bool
|
||||||
|
|
||||||
init(store: UserDefaultsProtocol) {
|
init(store: UserDefaultsProtocol) {
|
||||||
// UserDefaults to be used on reads and writes.
|
|
||||||
self.store = store
|
self.store = store
|
||||||
|
|
||||||
_lastVersionLaunched = UserPreference(key: .lastVersionLaunched, storage: store)
|
|
||||||
_seenInvites = UserPreference(key: .seenInvites, defaultValue: [], storage: store)
|
|
||||||
_hasSeenNewSoundBanner = UserPreference(key: .hasSeenNewSoundBanner, defaultValue: true, storage: store)
|
|
||||||
_appAppearance = UserPreference(key: .appAppearance, defaultValue: .system, storage: store)
|
|
||||||
_appLockNumberOfPINAttempts = UserPreference(key: .appLockNumberOfPINAttempts, defaultValue: 0, storage: store)
|
|
||||||
_enableNotifications = UserPreference(key: .enableNotifications, defaultValue: true, storage: store)
|
|
||||||
_enableInAppNotifications = UserPreference(key: .enableInAppNotifications, defaultValue: true, storage: store)
|
|
||||||
_hideQuietNotificationAlerts = UserPreference(key: .hideQuietNotificationAlerts, defaultValue: false, storage: store)
|
|
||||||
_pusherProfileTag = UserPreference(key: .pusherProfileTag, storage: store)
|
|
||||||
_lastNotificationBootTime = UserPreference(key: .lastNotificationBootTime, storage: store)
|
|
||||||
_selectedNotificationTone = UserPreference(key: .selectedNotificationTone, storage: store)
|
|
||||||
_logLevel = UserPreference(key: .logLevel, defaultValue: LogLevel.info, storage: store)
|
|
||||||
_traceLogPacks = UserPreference(key: .traceLogPacks, defaultValue: [], storage: store)
|
|
||||||
_analyticsConsentState = UserPreference(key: .analyticsConsentState, defaultValue: AnalyticsConsentState.unknown, storage: store)
|
|
||||||
_hasRunNotificationPermissionsOnboarding = UserPreference(key: .hasRunNotificationPermissionsOnboarding, defaultValue: false, storage: store)
|
|
||||||
_hasRunIdentityConfirmationOnboarding = UserPreference(key: .hasRunIdentityConfirmationOnboarding, defaultValue: false, storage: store)
|
|
||||||
_hasRequestedLocationAlwaysLocationAuthorization = UserPreference(key: .hasRequestedLocationAlwaysLocationAuthorization, defaultValue: false, storage: store)
|
|
||||||
_frequentlyUsedSystemEmojis = UserPreference(key: .frequentlyUsedSystemEmojis, defaultValue: [FrequentlyUsedEmoji](), storage: store)
|
|
||||||
_liveLocationSharingSessionsByRoomID = UserPreference(key: .liveLocationSharingTimeoutDatesByRoomID, defaultValue: [String: LiveLocationSession](), storage: store)
|
|
||||||
_liveLocationMinimumDistanceUpdate = UserPreference(key: .liveLocationMinimumDistanceUpdate, defaultValue: 10, storage: store)
|
|
||||||
_liveLocationDisclaimerDisplayed = UserPreference(key: .liveLocationDisclaimerDisplayed, defaultValue: false, storage: store)
|
|
||||||
_roomListActivityVisibility = UserPreference(key: .roomListActivityVisibility, defaultValue: .current, storage: store)
|
|
||||||
_viewSourceEnabled = UserPreference(key: .viewSourceEnabled, defaultValue: Self.appBuildType == .debug, storage: store)
|
|
||||||
_optimizeMediaUploads = UserPreference(key: .optimizeMediaUploads, defaultValue: true, storage: store)
|
|
||||||
_voiceMessagePlaybackSpeed = UserPreference(key: .voiceMessagePlaybackSpeed, defaultValue: AudioPlaybackSpeed.default, storage: store)
|
|
||||||
_elementCallBaseURLOverride = UserPreference(key: .elementCallBaseURLOverride, defaultValue: nil, storage: store)
|
|
||||||
_sharePresence = UserPreference(key: .sharePresence, defaultValue: true, storage: store)
|
|
||||||
_fuzzyRoomListSearchEnabled = UserPreference(key: .fuzzyRoomListSearchEnabled, defaultValue: false, storage: store)
|
|
||||||
_lowPriorityFilterEnabled = UserPreference(key: .lowPriorityFilterEnabled, defaultValue: false, storage: store)
|
|
||||||
_enableOnlySignedDeviceIsolationMode = UserPreference(key: .enableOnlySignedDeviceIsolationMode, defaultValue: false, storage: store)
|
|
||||||
_knockingEnabled = UserPreference(key: .knockingEnabled, defaultValue: false, storage: store)
|
|
||||||
_threadsEnabled = UserPreference(key: .threadsEnabled, defaultValue: false, storage: store)
|
|
||||||
_globalSearchEnabled = UserPreference(key: .globalSearchEnabled, defaultValue: ProcessInfo().isiOSAppOnMac, storage: store)
|
|
||||||
_roomThreadListEnabled = UserPreference(key: .roomThreadListEnabled, defaultValue: false, storage: store)
|
|
||||||
_focusEventOnNotificationTap = UserPreference(key: .focusEventOnNotificationTap, defaultValue: false, storage: store)
|
|
||||||
_linkPreviewsEnabled = UserPreference(key: .linkPreviewsEnabled, defaultValue: false, storage: store)
|
|
||||||
_jumpToReadMarkerEnabled = UserPreference(key: .jumpToReadMarkerEnabled, defaultValue: false, storage: store)
|
|
||||||
_linkNewDeviceEnabled = UserPreference(key: .linkNewDeviceEnabled, defaultValue: false, storage: store)
|
|
||||||
_automaticBackPaginationEnabled = UserPreference(key: .automaticBackPaginationEnabled, defaultValue: false, storage: store)
|
|
||||||
_clientPausingAndResumingEnabled = UserPreference(key: .clientPausingAndResumingEnabled, defaultValue: Self.appBuildType != .release, storage: VolatileUserDefaults())
|
|
||||||
_developerOptionsEnabled = UserPreference(key: .developerOptionsEnabled, defaultValue: Self.appBuildType != .release, storage: store)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
static func volatile() -> AppSettings {
|
static func volatile() -> AppSettings {
|
||||||
@@ -518,13 +423,3 @@ final nonisolated class AppSettings: @unchecked Sendable {
|
|||||||
}
|
}
|
||||||
|
|
||||||
nonisolated extension AppSettings: CommonSettingsProtocol { }
|
nonisolated extension AppSettings: CommonSettingsProtocol { }
|
||||||
|
|
||||||
private nonisolated extension UserPreference {
|
|
||||||
convenience init(key: AppSettings.UserDefaultsKeys, defaultValue: T, storage backingStorage: UserDefaultsProtocol, mode: Mode = .localOverRemote) {
|
|
||||||
self.init(key: key as any PreferenceKeyable, defaultValue: defaultValue, storage: backingStorage, mode: mode)
|
|
||||||
}
|
|
||||||
|
|
||||||
convenience init(key: AppSettings.UserDefaultsKeys, storage: UserDefaultsProtocol, mode: Mode = .localOverRemote) where T: ExpressibleByNilLiteral {
|
|
||||||
self.init(key: key as any PreferenceKeyable, defaultValue: nil, storage: storage, mode: mode)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -6,123 +6,15 @@
|
|||||||
// Please see LICENSE files in the repository root for full details.
|
// Please see LICENSE files in the repository root for full details.
|
||||||
//
|
//
|
||||||
|
|
||||||
import Combine
|
|
||||||
import Foundation
|
import Foundation
|
||||||
|
|
||||||
/// A property wrapper that allows storing data in a keyed storage while also exposing a Combine publisher
|
|
||||||
/// to listen for value changes. The publisher does not skip consecutive duplicates, as there is no
|
|
||||||
/// `Equatable` enforcement at this level.
|
|
||||||
@propertyWrapper
|
|
||||||
final nonisolated class UserPreference<T: Codable> {
|
|
||||||
static var remotePrefix: String {
|
|
||||||
"remote-"
|
|
||||||
}
|
|
||||||
|
|
||||||
enum Mode {
|
|
||||||
case localOverRemote
|
|
||||||
case remoteOverLocal
|
|
||||||
}
|
|
||||||
|
|
||||||
private let key: String
|
|
||||||
private var remoteKey: String {
|
|
||||||
"\(Self.remotePrefix)\(key)"
|
|
||||||
}
|
|
||||||
|
|
||||||
private var keyedStorage: any KeyedStorage<T>
|
|
||||||
private let defaultValue: T
|
|
||||||
private let subject: PassthroughSubject<T, Never> = .init()
|
|
||||||
private let mode: Mode
|
|
||||||
|
|
||||||
/// This can be used to check if is still possible for the user to change the value or not
|
|
||||||
/// Can only be accessed by using `_preferenceName.isLockedToRemote`
|
|
||||||
var isLockedToRemote: Bool {
|
|
||||||
mode == .remoteOverLocal && remoteValue != nil
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Initializes the property wrapper with a static default value.
|
|
||||||
///
|
|
||||||
/// - Parameters:
|
|
||||||
/// - key: The key used to store and retrieve the value.
|
|
||||||
/// - defaultValue: The default value to use if no stored value exists or if `forceDefault` is `true`.
|
|
||||||
/// - keyedStorage: The storage instance where the value is saved.
|
|
||||||
/// - forceDefault: A publisher that determines whether the default value should always be used. Defaults to publish `false`. Useful in the context of remote settings that need to override the local value.
|
|
||||||
init(key: any PreferenceKeyable,
|
|
||||||
defaultValue: T,
|
|
||||||
keyedStorage: any KeyedStorage<T>,
|
|
||||||
mode: Mode) {
|
|
||||||
self.key = key.rawValue
|
|
||||||
self.defaultValue = defaultValue
|
|
||||||
self.keyedStorage = keyedStorage
|
|
||||||
self.mode = mode
|
|
||||||
}
|
|
||||||
|
|
||||||
/// The wrapped value is supposed to be the one updated by the user so it can only control the local value
|
|
||||||
var wrappedValue: T {
|
|
||||||
get {
|
|
||||||
switch mode {
|
|
||||||
case .localOverRemote:
|
|
||||||
return keyedStorage[key] ?? keyedStorage[remoteKey] ?? defaultValue
|
|
||||||
case .remoteOverLocal:
|
|
||||||
return keyedStorage[remoteKey] ?? keyedStorage[key] ?? defaultValue
|
|
||||||
}
|
|
||||||
}
|
|
||||||
set {
|
|
||||||
keyedStorage[key] = newValue
|
|
||||||
subject.send(wrappedValue)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// This is supposed to be the value that is set by the remote settings
|
|
||||||
/// So it can only be accessed by doing `AppSettings._preferenceName.remoteValue`
|
|
||||||
var remoteValue: T? {
|
|
||||||
get {
|
|
||||||
keyedStorage[remoteKey]
|
|
||||||
} set {
|
|
||||||
keyedStorage[remoteKey] = newValue
|
|
||||||
if mode == .remoteOverLocal || keyedStorage[key] == nil {
|
|
||||||
subject.send(wrappedValue)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var projectedValue: AnyPublisher<T, Never> {
|
|
||||||
subject
|
|
||||||
.prepend(wrappedValue)
|
|
||||||
.eraseToAnyPublisher()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// MARK: - UserPreference convenience initializers
|
|
||||||
|
|
||||||
nonisolated extension UserPreference {
|
|
||||||
convenience init(key: any PreferenceKeyable, defaultValue: T, storage backingStorage: UserDefaultsProtocol, mode: Mode = .localOverRemote) {
|
|
||||||
let storage: any KeyedStorage<T> = UserDefaultsStorage(userDefaults: backingStorage)
|
|
||||||
|
|
||||||
self.init(key: key, defaultValue: defaultValue, keyedStorage: storage, mode: mode)
|
|
||||||
}
|
|
||||||
|
|
||||||
convenience init(key: any PreferenceKeyable, storage: UserDefaultsProtocol, mode: Mode = .localOverRemote) where T: ExpressibleByNilLiteral {
|
|
||||||
self.init(key: key, defaultValue: nil, storage: storage, mode: mode)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// MARK: - Keys
|
|
||||||
|
|
||||||
nonisolated protocol PreferenceKeyable: RawRepresentable where RawValue == String { }
|
|
||||||
|
|
||||||
// MARK: - Storage
|
// MARK: - Storage
|
||||||
|
|
||||||
nonisolated protocol KeyedStorage<Value> {
|
/// Typed storage of a `Codable` value in `UserDefaults`, used by the `@UserPreference` macro.
|
||||||
associatedtype Value: Codable
|
|
||||||
|
|
||||||
subscript(key: String) -> Value? { get set }
|
|
||||||
}
|
|
||||||
|
|
||||||
/// An implementation of KeyedStorage on the UserDefaults.
|
|
||||||
///
|
///
|
||||||
/// When used with a `Value` that conforms to `PlistRepresentable` the Codable encode/decode
|
/// When used with a `Value` that conforms to `PlistRepresentable` the Codable encode/decode
|
||||||
/// phase is skipped, and values are stored natively in the plist.
|
/// phase is skipped, and values are stored natively in the plist.
|
||||||
final nonisolated class UserDefaultsStorage<Value: Codable>: KeyedStorage {
|
nonisolated struct UserDefaultsStorage<Value: Codable> {
|
||||||
private let userDefaults: UserDefaultsProtocol
|
private let userDefaults: UserDefaultsProtocol
|
||||||
|
|
||||||
init(userDefaults: UserDefaultsProtocol) {
|
init(userDefaults: UserDefaultsProtocol) {
|
||||||
@@ -140,7 +32,7 @@ final nonisolated class UserDefaultsStorage<Value: Codable>: KeyedStorage {
|
|||||||
|
|
||||||
return value
|
return value
|
||||||
}
|
}
|
||||||
set {
|
nonmutating set {
|
||||||
if Value.self is PlistRepresentable.Type {
|
if Value.self is PlistRepresentable.Type {
|
||||||
encodePlistRepresentable(value: newValue, for: key)
|
encodePlistRepresentable(value: newValue, for: key)
|
||||||
} else {
|
} else {
|
||||||
@@ -198,8 +90,6 @@ nonisolated extension Optional: Nullable {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
nonisolated extension Dictionary: KeyedStorage where Key == String, Value: Codable { }
|
|
||||||
|
|
||||||
// MARK: - PlistRepresentable
|
// MARK: - PlistRepresentable
|
||||||
|
|
||||||
/// A protocol to mark types as being plist compliant.
|
/// A protocol to mark types as being plist compliant.
|
||||||
|
|||||||
@@ -118,21 +118,21 @@ class HomeScreenViewModel: HomeScreenViewModelType, HomeScreenViewModelProtocol
|
|||||||
.weakAssign(to: \.state.selectedRoomID, on: self)
|
.weakAssign(to: \.state.selectedRoomID, on: self)
|
||||||
.store(in: &cancellables)
|
.store(in: &cancellables)
|
||||||
|
|
||||||
appSettings.$roomListActivityVisibility
|
appSettings.roomListActivityVisibilityPublisher
|
||||||
.sink { [weak self] value in
|
.sink { [weak self] value in
|
||||||
self?.state.roomListActivityVisibility = value
|
self?.state.roomListActivityVisibility = value
|
||||||
self?.updateRooms()
|
self?.updateRooms()
|
||||||
}
|
}
|
||||||
.store(in: &cancellables)
|
.store(in: &cancellables)
|
||||||
|
|
||||||
appSettings.$seenInvites
|
appSettings.seenInvitesPublisher
|
||||||
.removeDuplicates()
|
.removeDuplicates()
|
||||||
.sink { [weak self] _ in
|
.sink { [weak self] _ in
|
||||||
self?.updateRooms()
|
self?.updateRooms()
|
||||||
}
|
}
|
||||||
.store(in: &cancellables)
|
.store(in: &cancellables)
|
||||||
|
|
||||||
appSettings.$hasSeenNewSoundBanner
|
appSettings.hasSeenNewSoundBannerPublisher
|
||||||
.sink { [weak self] hasSeenNewSoundBanner in
|
.sink { [weak self] hasSeenNewSoundBanner in
|
||||||
self?.state.shouldShowNewSoundBanner = !hasSeenNewSoundBanner
|
self?.state.shouldShowNewSoundBanner = !hasSeenNewSoundBanner
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -173,7 +173,7 @@ class LocationSharingScreenViewModel: LocationSharingScreenViewModelType, Locati
|
|||||||
}
|
}
|
||||||
.store(in: &cancellables)
|
.store(in: &cancellables)
|
||||||
|
|
||||||
appSettings.$liveLocationSharingSessionsByRoomID
|
appSettings.liveLocationSharingSessionsByRoomIDPublisher
|
||||||
.map { [roomID = roomProxy.id] sessions in sessions[roomID] != nil }
|
.map { [roomID = roomProxy.id] sessions in sessions[roomID] != nil }
|
||||||
.removeDuplicates()
|
.removeDuplicates()
|
||||||
.sink { [weak self] isSharingLiveLocationOnThisDevice in
|
.sink { [weak self] isSharingLiveLocationOnThisDevice in
|
||||||
|
|||||||
@@ -163,11 +163,11 @@ class RoomScreenViewModel: RoomScreenViewModelType, RoomScreenViewModelProtocol
|
|||||||
// MARK: - Private
|
// MARK: - Private
|
||||||
|
|
||||||
private func setupSubscriptions(ongoingCallRoomIDPublisher: CurrentValuePublisher<String?, Never>) {
|
private func setupSubscriptions(ongoingCallRoomIDPublisher: CurrentValuePublisher<String?, Never>) {
|
||||||
appSettings.$roomThreadListEnabled
|
appSettings.roomThreadListEnabledPublisher
|
||||||
.weakAssign(to: \.state.roomThreadListEnabled, on: self)
|
.weakAssign(to: \.state.roomThreadListEnabled, on: self)
|
||||||
.store(in: &cancellables)
|
.store(in: &cancellables)
|
||||||
|
|
||||||
appSettings.$liveLocationSharingSessionsByRoomID
|
appSettings.liveLocationSharingSessionsByRoomIDPublisher
|
||||||
.receive(on: DispatchQueue.main)
|
.receive(on: DispatchQueue.main)
|
||||||
.sink { [weak self] sessionsByRoomID in
|
.sink { [weak self] sessionsByRoomID in
|
||||||
guard let self else { return }
|
guard let self else { return }
|
||||||
|
|||||||
+1
-1
@@ -152,7 +152,7 @@ class SecurityAndPrivacyScreenViewModel: SecurityAndPrivacyScreenViewModelType,
|
|||||||
.weakAssign(to: \.state.isSpace, on: self)
|
.weakAssign(to: \.state.isSpace, on: self)
|
||||||
.store(in: &cancellables)
|
.store(in: &cancellables)
|
||||||
|
|
||||||
appSettings.$knockingEnabled
|
appSettings.knockingEnabledPublisher
|
||||||
.weakAssign(to: \.state.isKnockingEnabled, on: self)
|
.weakAssign(to: \.state.isKnockingEnabled, on: self)
|
||||||
.store(in: &cancellables)
|
.store(in: &cancellables)
|
||||||
}
|
}
|
||||||
|
|||||||
+1
-1
@@ -23,7 +23,7 @@ class AnalyticsSettingsScreenViewModel: AnalyticsSettingsScreenViewModelType, An
|
|||||||
|
|
||||||
super.init(initialViewState: state)
|
super.init(initialViewState: state)
|
||||||
|
|
||||||
appSettings.$analyticsConsentState
|
appSettings.analyticsConsentStatePublisher
|
||||||
.map { $0 == .optedIn }
|
.map { $0 == .optedIn }
|
||||||
.weakAssign(to: \.state.bindings.enableAnalytics, on: self)
|
.weakAssign(to: \.state.bindings.enableAnalytics, on: self)
|
||||||
.store(in: &cancellables)
|
.store(in: &cancellables)
|
||||||
|
|||||||
+2
-2
@@ -48,11 +48,11 @@ class NotificationSettingsScreenViewModel: NotificationSettingsScreenViewModelTy
|
|||||||
customToneSelectionEnabled: !ProcessInfo.processInfo.isiOSAppOnMac))
|
customToneSelectionEnabled: !ProcessInfo.processInfo.isiOSAppOnMac))
|
||||||
|
|
||||||
// Listen for changes to AppSettings.
|
// Listen for changes to AppSettings.
|
||||||
appSettings.$enableNotifications
|
appSettings.enableNotificationsPublisher
|
||||||
.weakAssign(to: \.state.bindings.enableNotifications, on: self)
|
.weakAssign(to: \.state.bindings.enableNotifications, on: self)
|
||||||
.store(in: &cancellables)
|
.store(in: &cancellables)
|
||||||
|
|
||||||
appSettings.$selectedNotificationTone
|
appSettings.selectedNotificationTonePublisher
|
||||||
.map { $0 ?? NotificationToneManager.defaultElementXMessageTone }
|
.map { $0 ?? NotificationToneManager.defaultElementXMessageTone }
|
||||||
.weakAssign(to: \.state.selectedAlertTone, on: self)
|
.weakAssign(to: \.state.selectedAlertTone, on: self)
|
||||||
.store(in: &cancellables)
|
.store(in: &cancellables)
|
||||||
|
|||||||
@@ -33,11 +33,11 @@ class SettingsScreenViewModel: SettingsScreenViewModelType, SettingsScreenViewMo
|
|||||||
navigationBarVisibility: isInSecondaryWindow ? .hidden : .automatic),
|
navigationBarVisibility: isInSecondaryWindow ? .hidden : .automatic),
|
||||||
mediaProvider: userSession.mediaProvider)
|
mediaProvider: userSession.mediaProvider)
|
||||||
|
|
||||||
appSettings.$developerOptionsEnabled
|
appSettings.developerOptionsEnabledPublisher
|
||||||
.weakAssign(to: \.state.showDeveloperOptions, on: self)
|
.weakAssign(to: \.state.showDeveloperOptions, on: self)
|
||||||
.store(in: &cancellables)
|
.store(in: &cancellables)
|
||||||
|
|
||||||
appSettings.$linkNewDeviceEnabled
|
appSettings.linkNewDeviceEnabledPublisher
|
||||||
.weakAssign(to: \.state.showLinkNewDeviceButton, on: self)
|
.weakAssign(to: \.state.showLinkNewDeviceButton, on: self)
|
||||||
.store(in: &cancellables)
|
.store(in: &cancellables)
|
||||||
|
|
||||||
|
|||||||
@@ -508,7 +508,7 @@ class TimelineInteractionHandler {
|
|||||||
duration: voiceMessageRoomTimelineItem.content.duration,
|
duration: voiceMessageRoomTimelineItem.content.duration,
|
||||||
waveform: voiceMessageRoomTimelineItem.content.waveform,
|
waveform: voiceMessageRoomTimelineItem.content.waveform,
|
||||||
playbackSpeed: appSettings.voiceMessagePlaybackSpeed,
|
playbackSpeed: appSettings.voiceMessagePlaybackSpeed,
|
||||||
playbackSpeedPublisher: appSettings.$voiceMessagePlaybackSpeed)
|
playbackSpeedPublisher: appSettings.voiceMessagePlaybackSpeedPublisher)
|
||||||
mediaPlayerProvider.register(audioPlayerState: playerState)
|
mediaPlayerProvider.register(audioPlayerState: playerState)
|
||||||
return playerState
|
return playerState
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -556,19 +556,19 @@ class TimelineViewModel: TimelineViewModelType, TimelineViewModelProtocol {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private func setupAppSettingsSubscriptions() {
|
private func setupAppSettingsSubscriptions() {
|
||||||
appSettings.$sharePresence
|
appSettings.sharePresencePublisher
|
||||||
.weakAssign(to: \.state.showReadReceipts, on: self)
|
.weakAssign(to: \.state.showReadReceipts, on: self)
|
||||||
.store(in: &cancellables)
|
.store(in: &cancellables)
|
||||||
|
|
||||||
appSettings.$viewSourceEnabled
|
appSettings.viewSourceEnabledPublisher
|
||||||
.weakAssign(to: \.state.isViewSourceEnabled, on: self)
|
.weakAssign(to: \.state.isViewSourceEnabled, on: self)
|
||||||
.store(in: &cancellables)
|
.store(in: &cancellables)
|
||||||
|
|
||||||
appSettings.$threadsEnabled
|
appSettings.threadsEnabledPublisher
|
||||||
.weakAssign(to: \.state.areThreadsEnabled, on: self)
|
.weakAssign(to: \.state.areThreadsEnabled, on: self)
|
||||||
.store(in: &cancellables)
|
.store(in: &cancellables)
|
||||||
|
|
||||||
appSettings.$jumpToReadMarkerEnabled
|
appSettings.jumpToReadMarkerEnabledPublisher
|
||||||
.weakAssign(to: \.state.jumpToReadMarkerEnabled, on: self)
|
.weakAssign(to: \.state.jumpToReadMarkerEnabled, on: self)
|
||||||
.store(in: &cancellables)
|
.store(in: &cancellables)
|
||||||
|
|
||||||
|
|||||||
@@ -55,7 +55,7 @@ class AppLockService: AppLockServiceProtocol {
|
|||||||
}
|
}
|
||||||
|
|
||||||
var numberOfPINAttempts: AnyPublisher<Int, Never> {
|
var numberOfPINAttempts: AnyPublisher<Int, Never> {
|
||||||
appSettings.$appLockNumberOfPINAttempts
|
appSettings.appLockNumberOfPINAttemptsPublisher
|
||||||
}
|
}
|
||||||
|
|
||||||
init(keychainController: KeychainControllerProtocol, appSettings: AppSettings, context: LAContext = .init()) {
|
init(keychainController: KeychainControllerProtocol, appSettings: AppSettings, context: LAContext = .init()) {
|
||||||
|
|||||||
@@ -183,7 +183,7 @@ class LiveLocationManager: NSObject, LiveLocationManagerProtocol, CLLocationMana
|
|||||||
}
|
}
|
||||||
.store(in: &cancellables)
|
.store(in: &cancellables)
|
||||||
|
|
||||||
appSettings.$liveLocationSharingSessionsByRoomID
|
appSettings.liveLocationSharingSessionsByRoomIDPublisher
|
||||||
.removeDuplicates()
|
.removeDuplicates()
|
||||||
.sink { [weak self] sessions in
|
.sink { [weak self] sessions in
|
||||||
guard let self else { return }
|
guard let self else { return }
|
||||||
@@ -197,7 +197,7 @@ class LiveLocationManager: NSObject, LiveLocationManagerProtocol, CLLocationMana
|
|||||||
}
|
}
|
||||||
.store(in: &cancellables)
|
.store(in: &cancellables)
|
||||||
|
|
||||||
appSettings.$liveLocationMinimumDistanceUpdate
|
appSettings.liveLocationMinimumDistanceUpdatePublisher
|
||||||
.removeDuplicates()
|
.removeDuplicates()
|
||||||
.debounce(for: .seconds(1), scheduler: DispatchQueue.main)
|
.debounce(for: .seconds(1), scheduler: DispatchQueue.main)
|
||||||
.sink { [weak self] newValue in
|
.sink { [weak self] newValue in
|
||||||
|
|||||||
@@ -51,7 +51,7 @@ final class NotificationManager: NSObject, NotificationManagerProtocol {
|
|||||||
MXLog.info("App setting 'enableNotifications' is '\(notificationsEnabled)'")
|
MXLog.info("App setting 'enableNotifications' is '\(notificationsEnabled)'")
|
||||||
|
|
||||||
// Listen for changes to AppSettings.enableNotifications
|
// Listen for changes to AppSettings.enableNotifications
|
||||||
appSettings.$enableNotifications
|
appSettings.enableNotificationsPublisher
|
||||||
.sink { [weak self] newValue in
|
.sink { [weak self] newValue in
|
||||||
self?.enableNotifications(newValue)
|
self?.enableNotifications(newValue)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -190,7 +190,7 @@ final class LiveLocationManagerTests {
|
|||||||
try await simulateBeaconEcho(roomID: "!room:matrix.org", eventID: "$event:matrix.org")
|
try await simulateBeaconEcho(roomID: "!room:matrix.org", eventID: "$event:matrix.org")
|
||||||
#expect(appSettings.liveLocationSharingSessionsByRoomID["!room:matrix.org"] != nil)
|
#expect(appSettings.liveLocationSharingSessionsByRoomID["!room:matrix.org"] != nil)
|
||||||
|
|
||||||
let deferred = deferFulfillment(appSettings.$liveLocationSharingSessionsByRoomID) { $0["!room:matrix.org"] == nil }
|
let deferred = deferFulfillment(appSettings.liveLocationSharingSessionsByRoomIDPublisher) { $0["!room:matrix.org"] == nil }
|
||||||
beaconInfoSubject.send(LiveLocationOwnInfoUpdate(roomID: "!room:matrix.org", eventID: "$external_event:matrix.org", isLive: true))
|
beaconInfoSubject.send(LiveLocationOwnInfoUpdate(roomID: "!room:matrix.org", eventID: "$external_event:matrix.org", isLive: true))
|
||||||
try await deferred.fulfill()
|
try await deferred.fulfill()
|
||||||
|
|
||||||
@@ -237,7 +237,7 @@ final class LiveLocationManagerTests {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private func simulateBeaconEcho(roomID: String, eventID: String) async throws {
|
private func simulateBeaconEcho(roomID: String, eventID: String) async throws {
|
||||||
let deferred = deferFulfillment(appSettings.$liveLocationSharingSessionsByRoomID) { $0[roomID] != nil }
|
let deferred = deferFulfillment(appSettings.liveLocationSharingSessionsByRoomIDPublisher) { $0[roomID] != nil }
|
||||||
beaconInfoSubject.send(LiveLocationOwnInfoUpdate(roomID: roomID, eventID: eventID, isLive: true))
|
beaconInfoSubject.send(LiveLocationOwnInfoUpdate(roomID: roomID, eventID: eventID, isLive: true))
|
||||||
try await deferred.fulfill()
|
try await deferred.fulfill()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,8 +6,10 @@
|
|||||||
// Please see LICENSE files in the repository root for full details.
|
// Please see LICENSE files in the repository root for full details.
|
||||||
//
|
//
|
||||||
|
|
||||||
|
import Combine
|
||||||
@testable import ElementX
|
@testable import ElementX
|
||||||
import Foundation
|
import Foundation
|
||||||
|
import Macros
|
||||||
import Testing
|
import Testing
|
||||||
|
|
||||||
struct UserPreferenceTests {
|
struct UserPreferenceTests {
|
||||||
@@ -131,58 +133,29 @@ struct UserPreferenceTests {
|
|||||||
#expect(value.codable == nil)
|
#expect(value.codable == nil)
|
||||||
#expect(testDefaults.data(forKey: TestsKey.key3.rawValue) == nil)
|
#expect(testDefaults.data(forKey: TestsKey.key3.rawValue) == nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
|
||||||
func localOverRemoteValue() {
|
|
||||||
let storage = VolatileUserDefaults()
|
|
||||||
let preference = UserPreference(key: TestsKey.testKey, defaultValue: "", storage: storage)
|
|
||||||
#expect(preference.wrappedValue == "")
|
|
||||||
|
|
||||||
preference.remoteValue = "remote"
|
|
||||||
#expect(preference.wrappedValue == "remote")
|
|
||||||
|
|
||||||
preference.wrappedValue = "local"
|
|
||||||
#expect(preference.wrappedValue == "local")
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
func remoteOverLocalValue() {
|
|
||||||
let storage = VolatileUserDefaults()
|
|
||||||
let preference = UserPreference(key: TestsKey.testKey, defaultValue: "", storage: storage, mode: .remoteOverLocal)
|
|
||||||
#expect(preference.wrappedValue == "")
|
|
||||||
|
|
||||||
preference.remoteValue = "remote"
|
|
||||||
#expect(preference.wrappedValue == "remote")
|
|
||||||
|
|
||||||
preference.wrappedValue = "local"
|
|
||||||
#expect(preference.wrappedValue == "remote")
|
|
||||||
#expect(preference.isLockedToRemote)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private nonisolated struct TestPreferences {
|
private final class TestPreferences {
|
||||||
@UserPreference
|
let store: UserDefaultsProtocol
|
||||||
|
|
||||||
|
init(_ store: UserDefaultsProtocol) {
|
||||||
|
self.store = store
|
||||||
|
}
|
||||||
|
|
||||||
|
@UserPreference(key: TestsKey.key1.rawValue, volatile: true)
|
||||||
var volatileVar: String?
|
var volatileVar: String?
|
||||||
|
|
||||||
@UserPreference
|
@UserPreference(key: TestsKey.key2.rawValue)
|
||||||
var plist: String?
|
var plist: String?
|
||||||
|
|
||||||
@UserPreference
|
@UserPreference(key: TestsKey.key3.rawValue)
|
||||||
var codable: CodableTestType?
|
var codable: CodableTestType?
|
||||||
|
|
||||||
@UserPreference
|
@UserPreference(key: TestsKey.key4.rawValue, volatile: true)
|
||||||
var volatileCodable: CodableTestType?
|
var volatileCodable: CodableTestType?
|
||||||
|
|
||||||
@UserPreference
|
@UserPreference(key: TestsKey.key5.rawValue)
|
||||||
var plistArray: [Int]?
|
var plistArray: [Int]?
|
||||||
|
|
||||||
init(_ storage: UserDefaultsProtocol) {
|
|
||||||
_volatileVar = UserPreference(key: TestsKey.key1, storage: VolatileUserDefaults())
|
|
||||||
_plist = UserPreference(key: TestsKey.key2, storage: storage)
|
|
||||||
_codable = UserPreference(key: TestsKey.key3, storage: storage)
|
|
||||||
_volatileCodable = UserPreference(key: TestsKey.key4, storage: VolatileUserDefaults())
|
|
||||||
_plistArray = UserPreference(key: TestsKey.key5, storage: storage)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private struct CodableTestType: Equatable, Codable {
|
private struct CodableTestType: Equatable, Codable {
|
||||||
@@ -190,13 +163,10 @@ private struct CodableTestType: Equatable, Codable {
|
|||||||
let b: [Int]
|
let b: [Int]
|
||||||
}
|
}
|
||||||
|
|
||||||
private nonisolated struct TestsKey: PreferenceKeyable, RawRepresentable {
|
private enum TestsKey: String {
|
||||||
static let testKey = Self(rawValue: "testKey")
|
case key1 = "foo.volatile"
|
||||||
static let key1 = Self(rawValue: "foo.volatile")
|
case key2 = "foo.plist"
|
||||||
static let key2 = Self(rawValue: "foo.plist")
|
case key3 = "foo.codable"
|
||||||
static let key3 = Self(rawValue: "foo.codable")
|
case key4 = "foo.volatile.codable"
|
||||||
static let key4 = Self(rawValue: "foo.volatile.codable")
|
case key5 = "foo.plist.array"
|
||||||
static let key5 = Self(rawValue: "foo.plist.array")
|
|
||||||
|
|
||||||
let rawValue: String
|
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user