diff --git a/damus.xcodeproj/project.pbxproj b/damus.xcodeproj/project.pbxproj index 3ef08435..c3347395 100644 --- a/damus.xcodeproj/project.pbxproj +++ b/damus.xcodeproj/project.pbxproj @@ -533,6 +533,7 @@ D7CCFC152B05891000323D86 /* Referenced.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CC14FF82A741939007AEB17 /* Referenced.swift */; }; D7CCFC162B05894300323D86 /* Pubkey.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CC14FF02A73FCDB007AEB17 /* Pubkey.swift */; }; D7CCFC192B058A3F00323D86 /* Block.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7527271D2A93FF0100214108 /* Block.swift */; }; + D7CD35132B1A72B800D63139 /* Constants.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3169CAEC294FCCFC00EE4006 /* Constants.swift */; }; D7CE1B182B0BDFDD002EDAD4 /* mdb.c in Sources */ = {isa = PBXBuildFile; fileRef = 4C4793002A993B9A00489948 /* mdb.c */; }; D7CE1B192B0BE132002EDAD4 /* builder.c in Sources */ = {isa = PBXBuildFile; fileRef = 4C4792942A9939BD00489948 /* builder.c */; }; D7CE1B1A2B0BE135002EDAD4 /* json_parser.c in Sources */ = {isa = PBXBuildFile; fileRef = 4C4792C82A9939BD00489948 /* json_parser.c */; }; @@ -3437,6 +3438,7 @@ D7CCFC072B05833200323D86 /* NdbNote.swift in Sources */, D7CE1B3F2B0BE719002EDAD4 /* Enum.swift in Sources */, D74AAFD22B155E78006CF0F4 /* WalletConnect.swift in Sources */, + D7CD35132B1A72B800D63139 /* Constants.swift in Sources */, D7EDED222B117DCA0018B19C /* SequenceUtils.swift in Sources */, D7CE1B422B0BE719002EDAD4 /* Offset.swift in Sources */, D7FB10A72B0C371A00FA8D42 /* Log.swift in Sources */, diff --git a/damus/Models/DamusUserDefaults.swift b/damus/Models/DamusUserDefaults.swift index 851dd00b..b9c44ed0 100644 --- a/damus/Models/DamusUserDefaults.swift +++ b/damus/Models/DamusUserDefaults.swift @@ -7,61 +7,127 @@ import Foundation -/// DamusUserDefaults -/// This struct acts like a drop-in replacement for `UserDefaults.standard` -/// for cases where we want to store such items in a UserDefaults that is shared among the Damus app group -/// so that they can be accessed from other target (e.g. The notification extension target). +/// # DamusUserDefaults /// -/// This struct handles migration automatically to the new shared UserDefaults +/// This struct acts like a UserDefaults object, but is also capable of automatically mirroring values to a separate store. +/// +/// It works by using a specific store container as the main source of truth, and by optionally mirroring values to a different container if needed. +/// +/// This is useful when the data of a UserDefaults object needs to be accessible from another store container, +/// as it offers ways to automatically mirror information over a different container (e.g. When using app extensions) +/// +/// Since it mirrors items instead of migrating them, this object can be used in a backwards compatible manner. +/// +/// The easiest way to use this is to use `DamusUserDefaults.standard` as a drop-in replacement for `UserDefaults.standard` +/// Or, you can initialize a custom object with customizable stores. struct DamusUserDefaults { - static let shared: DamusUserDefaults = DamusUserDefaults() - private static let default_suite_name: String = "group.com.damus" // Shared defaults for this app group - private let suite_name: String - private let defaults: UserDefaults + // MARK: - Helper data structures + + enum Store: Equatable { + case standard + case shared + case custom(UserDefaults) + + func get_user_defaults() -> UserDefaults? { + switch self { + case .standard: + return UserDefaults.standard + case .shared: + return UserDefaults(suiteName: Constants.DAMUS_APP_GROUP_IDENTIFIER) + case .custom(let user_defaults): + return user_defaults + } + } + } + + enum DamusUserDefaultsError: Error { + case cannot_initialize_user_defaults + case cannot_mirror_main_user_defaults + } + + // MARK: - Stored properties + + private let main: UserDefaults + private let mirrors: [UserDefaults] // MARK: - Initializers - init() { - self.init(suite_name: Self.default_suite_name)! // Pretty low risk to force-unwrap given that the default suite name is a constant. + init?(main: Store, mirror mirrors: [Store] = []) throws { + guard let main_user_defaults = main.get_user_defaults() else { throw DamusUserDefaultsError.cannot_initialize_user_defaults } + let mirror_user_defaults: [UserDefaults] = try mirrors.compactMap({ mirror_store in + guard let mirror_user_default = mirror_store.get_user_defaults() else { + throw DamusUserDefaultsError.cannot_initialize_user_defaults + } + guard mirror_store != main else { + throw DamusUserDefaultsError.cannot_mirror_main_user_defaults + } + return mirror_user_default + }) + + self.main = main_user_defaults + self.mirrors = mirror_user_defaults } - init?(suite_name: String = Self.default_suite_name) { - self.suite_name = suite_name - guard let defaults = UserDefaults(suiteName: suite_name) else { - return nil - } - self.defaults = defaults - } - - // MARK: - Functions for feature parity with UserDefaults.standard + // MARK: - Functions for feature parity with UserDefaults func string(forKey defaultName: String) -> String? { - if let value = self.defaults.string(forKey: defaultName) { - return value - } - let fallback_value = UserDefaults.standard.string(forKey: defaultName) - self.defaults.set(fallback_value, forKey: defaultName) // Migrate - return fallback_value + let value = self.main.string(forKey: defaultName) + self.mirror(value, forKey: defaultName) + return value } func set(_ value: Any?, forKey defaultName: String) { - self.defaults.set(value, forKey: defaultName) + self.main.set(value, forKey: defaultName) + self.mirror(value, forKey: defaultName) } func removeObject(forKey defaultName: String) { - self.defaults.removeObject(forKey: defaultName) - // Remove from standard UserDefaults to avoid it coming back as a fallback_value when we fetch it next time - UserDefaults.standard.removeObject(forKey: defaultName) + self.main.removeObject(forKey: defaultName) + self.mirror_object_removal(forKey: defaultName) } func object(forKey defaultName: String) -> Any? { - if let value = self.defaults.object(forKey: defaultName) { - return value - } - let fallback_value = UserDefaults.standard.string(forKey: defaultName) - self.defaults.set(fallback_value, forKey: defaultName) // Migrate - return fallback_value + let value = self.main.object(forKey: defaultName) + self.mirror(value, forKey: defaultName) + return value } + // MARK: - Mirroring utilities + + private func mirror(_ value: Any?, forKey defaultName: String) { + for mirror in self.mirrors { + mirror.set(value, forKey: defaultName) + } + } + + private func mirror_object_removal(forKey defaultName: String) { + for mirror in self.mirrors { + mirror.removeObject(forKey: defaultName) + } + } +} + +// MARK: - Default convenience objects + +/// # Convenience objects +/// +/// - `DamusUserDefaults.standard`: will detect the bundle identifier and pick an appropriate object. You should generally use this one. +/// - `DamusUserDefaults.app`: stores things on its own container, and mirrors them to the shared container. +/// - `DamusUserDefaults.shared`: stores things on the shared container and does no mirroring +extension DamusUserDefaults { + static let app: DamusUserDefaults = try! DamusUserDefaults(main: .standard, mirror: [.shared])! // Since the underlying behavior is very static, the risk of crashing on force unwrap is low + static let shared: DamusUserDefaults = try! DamusUserDefaults(main: .shared)! // Since the underlying behavior is very static, the risk of crashing on force unwrap is low + static var standard: DamusUserDefaults { + get { + switch Bundle.main.bundleIdentifier { + case Constants.MAIN_APP_BUNDLE_IDENTIFIER: + return Self.app + case Constants.NOTIFICATION_EXTENSION_BUNDLE_IDENTIFIER: + return Self.shared + default: + return Self.shared + } + } + } } diff --git a/damus/Models/UserSettingsStore.swift b/damus/Models/UserSettingsStore.swift index 68c72546..5e48ec7f 100644 --- a/damus/Models/UserSettingsStore.swift +++ b/damus/Models/UserSettingsStore.swift @@ -16,13 +16,13 @@ func setting_property_key(key: String) -> String { } func setting_get_property_value(key: String, scoped_key: String, default_value: T) -> T { - if let loaded = DamusUserDefaults.shared.object(forKey: scoped_key) as? T { + if let loaded = DamusUserDefaults.standard.object(forKey: scoped_key) as? T { return loaded - } else if let loaded = DamusUserDefaults.shared.object(forKey: key) as? T { + } else if let loaded = DamusUserDefaults.standard.object(forKey: key) as? T { // If pubkey-scoped setting does not exist but the deprecated non-pubkey-scoped setting does, // migrate the deprecated setting into the pubkey-scoped one and delete the deprecated one. - DamusUserDefaults.shared.set(loaded, forKey: scoped_key) - DamusUserDefaults.shared.removeObject(forKey: key) + DamusUserDefaults.standard.set(loaded, forKey: scoped_key) + DamusUserDefaults.standard.removeObject(forKey: key) return loaded } else { return default_value @@ -31,7 +31,7 @@ func setting_get_property_value(key: String, scoped_key: String, default_valu func setting_set_property_value(scoped_key: String, old_value: T, new_value: T) -> T? { guard old_value != new_value else { return nil } - DamusUserDefaults.shared.set(new_value, forKey: scoped_key) + DamusUserDefaults.standard.set(new_value, forKey: scoped_key) UserSettingsStore.shared?.objectWillChange.send() return new_value } @@ -65,14 +65,14 @@ func setting_set_property_value(scoped_key: String, old_value: T, init(key: String, default_value: T) { self.key = pk_setting_key(UserSettingsStore.pubkey ?? .empty, key: key) - if let loaded = DamusUserDefaults.shared.string(forKey: self.key), let val = T.init(from: loaded) { + if let loaded = DamusUserDefaults.standard.string(forKey: self.key), let val = T.init(from: loaded) { self.value = val - } else if let loaded = DamusUserDefaults.shared.string(forKey: key), let val = T.init(from: loaded) { + } else if let loaded = DamusUserDefaults.standard.string(forKey: key), let val = T.init(from: loaded) { // If pubkey-scoped setting does not exist but the deprecated non-pubkey-scoped setting does, // migrate the deprecated setting into the pubkey-scoped one and delete the deprecated one. self.value = val - DamusUserDefaults.shared.set(val.to_string(), forKey: self.key) - DamusUserDefaults.shared.removeObject(forKey: key) + DamusUserDefaults.standard.set(val.to_string(), forKey: self.key) + DamusUserDefaults.standard.removeObject(forKey: key) } else { self.value = default_value } @@ -85,7 +85,7 @@ func setting_set_property_value(scoped_key: String, old_value: T, return } self.value = newValue - DamusUserDefaults.shared.set(newValue.to_string(), forKey: key) + DamusUserDefaults.standard.set(newValue.to_string(), forKey: key) UserSettingsStore.shared!.objectWillChange.send() } } diff --git a/damus/Util/Constants.swift b/damus/Util/Constants.swift index 1a9c819f..48d82cea 100644 --- a/damus/Util/Constants.swift +++ b/damus/Util/Constants.swift @@ -9,6 +9,7 @@ import Foundation class Constants { //static let EXAMPLE_DEMOS: DamusState = .empty + static let DAMUS_APP_GROUP_IDENTIFIER: String = "group.com.damus" static let DEVICE_TOKEN_RECEIVER_PRODUCTION_URL: URL = URL(string: "https://notify.damus.io:8000/user-info")! static let DEVICE_TOKEN_RECEIVER_TEST_URL: URL = URL(string: "http://localhost:8000/user-info")! static let MAIN_APP_BUNDLE_IDENTIFIER: String = "com.jb55.damus2" diff --git a/damus/Util/Keys.swift b/damus/Util/Keys.swift index 75daee68..71453463 100644 --- a/damus/Util/Keys.swift +++ b/damus/Util/Keys.swift @@ -124,7 +124,7 @@ func privkey_to_pubkey(privkey: Privkey) -> Pubkey? { } func save_pubkey(pubkey: Pubkey) { - DamusUserDefaults.shared.set(pubkey.hex(), forKey: "pubkey") + DamusUserDefaults.standard.set(pubkey.hex(), forKey: "pubkey") } enum Keys { @@ -141,7 +141,7 @@ func clear_saved_privkey() throws { } func clear_saved_pubkey() { - DamusUserDefaults.shared.removeObject(forKey: "pubkey") + DamusUserDefaults.standard.removeObject(forKey: "pubkey") } func save_keypair(pubkey: Pubkey, privkey: Privkey) throws { @@ -175,7 +175,7 @@ func get_saved_keypair() -> Keypair? { } func get_saved_pubkey() -> String? { - return DamusUserDefaults.shared.string(forKey: "pubkey") + return DamusUserDefaults.standard.string(forKey: "pubkey") } func get_saved_privkey() -> String? { @@ -198,10 +198,10 @@ func contentContainsPrivateKey(_ content: String) -> Bool { } fileprivate func removePrivateKeyFromUserDefaults() throws { - guard let privkey_str = DamusUserDefaults.shared.string(forKey: "privkey"), + guard let privkey_str = DamusUserDefaults.standard.string(forKey: "privkey"), let privkey = hex_decode_privkey(privkey_str) else { return } try save_privkey(privkey: privkey) - DamusUserDefaults.shared.removeObject(forKey: "privkey") + DamusUserDefaults.standard.removeObject(forKey: "privkey") } diff --git a/damus/Views/TimelineView.swift b/damus/Views/TimelineView.swift index ec663ec4..760b5f2c 100644 --- a/damus/Views/TimelineView.swift +++ b/damus/Views/TimelineView.swift @@ -66,7 +66,7 @@ struct TimelineView: View { struct TimelineView_Previews: PreviewProvider { @StateObject static var events = test_event_holder static var previews: some View { - TimelineView(events: events, loading: .constant(true), damus: Constants.EXAMPLE_DEMOS, show_friend_icon: true, filter: { _ in true }) + TimelineView(events: events, loading: .constant(true), damus: test_damus_state, show_friend_icon: true, filter: { _ in true }) } }