Change DamusUserDefaults to mirror settings from app container

... to shared container instead of migrating

This commit is a reimplementation of DamusUserDefaults that mirrors
settings from the app to the shared container (instead of migrating
values over).

This new implementation brings the benefit of being backwards compatible
with the user's settings. That is, even if the user upgrades or
downgrades between various versions and changes settings along the way,
the main settings in the app will stay consistent between Damus versions
— that is, changes to the settings would not be lost between
downgrades/upgrades

General settings test
----------------------

PASS

Device: iPhone 15 Pro simulator
iOS: 17.0.1
Damus: This commit
Setup: A device with non-standard settings
Steps:
1. Flash Damus on the device
2. Check any non-default settings that were there before. Ensure that settings remained the same. PASS
3. Change one setting (any setting) to a non-default value
4. Restart Damus
5. Ensure settings change in step 3 persisted on the device

Notification settings test
--------------------------

PASS

Device: iPhone 15 Pro simulator
iOS: 17.0.1
Damus: This commit
Setup:
- Two phones running Damus on different accounts
- Local relay with strfry-push-notify test setup
- Apple push notification test tool

Coverage:
1. Mention notifications
2. DM notifications
3. Reaction notifications
4. Repost notifications

Steps for each notification type:
1. Use the secondary phone to generate a push notification
2. Trigger the push notification (Send push notification from test tool)
3. Ensure that the notification is received on the other device
4. Turn off notifications for that notification type on settings
5. Trigger the same push notification (Resend push notification from test tool)
6. Ensure that the notification is not received on the other device
7. Turn on notifications for that notification type on settings
8. Trigger the same push notification (Resend from test tool)
9. Ensure that notification appears on the device

Result: PASS (notifications are received when enabled and not received when disabled)
Signed-off-by: Daniel D’Aquino <daniel@daquino.me>
Signed-off-by: William Casarin <jb55@jb55.com>
This commit is contained in:
Daniel D’Aquino
2023-12-01 21:26:54 +00:00
committed by William Casarin
parent aab9e97a25
commit 54674104ea
6 changed files with 121 additions and 52 deletions

View File

@@ -533,6 +533,7 @@
D7CCFC152B05891000323D86 /* Referenced.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CC14FF82A741939007AEB17 /* Referenced.swift */; }; D7CCFC152B05891000323D86 /* Referenced.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CC14FF82A741939007AEB17 /* Referenced.swift */; };
D7CCFC162B05894300323D86 /* Pubkey.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CC14FF02A73FCDB007AEB17 /* Pubkey.swift */; }; D7CCFC162B05894300323D86 /* Pubkey.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CC14FF02A73FCDB007AEB17 /* Pubkey.swift */; };
D7CCFC192B058A3F00323D86 /* Block.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7527271D2A93FF0100214108 /* Block.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 */; }; D7CE1B182B0BDFDD002EDAD4 /* mdb.c in Sources */ = {isa = PBXBuildFile; fileRef = 4C4793002A993B9A00489948 /* mdb.c */; };
D7CE1B192B0BE132002EDAD4 /* builder.c in Sources */ = {isa = PBXBuildFile; fileRef = 4C4792942A9939BD00489948 /* builder.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 */; }; D7CE1B1A2B0BE135002EDAD4 /* json_parser.c in Sources */ = {isa = PBXBuildFile; fileRef = 4C4792C82A9939BD00489948 /* json_parser.c */; };
@@ -3437,6 +3438,7 @@
D7CCFC072B05833200323D86 /* NdbNote.swift in Sources */, D7CCFC072B05833200323D86 /* NdbNote.swift in Sources */,
D7CE1B3F2B0BE719002EDAD4 /* Enum.swift in Sources */, D7CE1B3F2B0BE719002EDAD4 /* Enum.swift in Sources */,
D74AAFD22B155E78006CF0F4 /* WalletConnect.swift in Sources */, D74AAFD22B155E78006CF0F4 /* WalletConnect.swift in Sources */,
D7CD35132B1A72B800D63139 /* Constants.swift in Sources */,
D7EDED222B117DCA0018B19C /* SequenceUtils.swift in Sources */, D7EDED222B117DCA0018B19C /* SequenceUtils.swift in Sources */,
D7CE1B422B0BE719002EDAD4 /* Offset.swift in Sources */, D7CE1B422B0BE719002EDAD4 /* Offset.swift in Sources */,
D7FB10A72B0C371A00FA8D42 /* Log.swift in Sources */, D7FB10A72B0C371A00FA8D42 /* Log.swift in Sources */,

View File

@@ -7,61 +7,127 @@
import Foundation import Foundation
/// DamusUserDefaults /// # 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).
/// ///
/// 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 { 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 // MARK: - Helper data structures
private let defaults: UserDefaults
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 // MARK: - Initializers
init() { init?(main: Store, mirror mirrors: [Store] = []) throws {
self.init(suite_name: Self.default_suite_name)! // Pretty low risk to force-unwrap given that the default suite name is a constant. 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) { // MARK: - Functions for feature parity with UserDefaults
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
func string(forKey defaultName: String) -> String? { func string(forKey defaultName: String) -> String? {
if let value = self.defaults.string(forKey: defaultName) { let value = self.main.string(forKey: defaultName)
return value self.mirror(value, forKey: defaultName)
} return value
let fallback_value = UserDefaults.standard.string(forKey: defaultName)
self.defaults.set(fallback_value, forKey: defaultName) // Migrate
return fallback_value
} }
func set(_ value: Any?, forKey defaultName: String) { 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) { func removeObject(forKey defaultName: String) {
self.defaults.removeObject(forKey: defaultName) self.main.removeObject(forKey: defaultName)
// Remove from standard UserDefaults to avoid it coming back as a fallback_value when we fetch it next time self.mirror_object_removal(forKey: defaultName)
UserDefaults.standard.removeObject(forKey: defaultName)
} }
func object(forKey defaultName: String) -> Any? { func object(forKey defaultName: String) -> Any? {
if let value = self.defaults.object(forKey: defaultName) { let value = self.main.object(forKey: defaultName)
return value self.mirror(value, forKey: defaultName)
} return value
let fallback_value = UserDefaults.standard.string(forKey: defaultName)
self.defaults.set(fallback_value, forKey: defaultName) // Migrate
return fallback_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
}
}
}
} }

View File

@@ -16,13 +16,13 @@ func setting_property_key(key: String) -> String {
} }
func setting_get_property_value<T>(key: String, scoped_key: String, default_value: T) -> T { func setting_get_property_value<T>(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 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, // 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. // migrate the deprecated setting into the pubkey-scoped one and delete the deprecated one.
DamusUserDefaults.shared.set(loaded, forKey: scoped_key) DamusUserDefaults.standard.set(loaded, forKey: scoped_key)
DamusUserDefaults.shared.removeObject(forKey: key) DamusUserDefaults.standard.removeObject(forKey: key)
return loaded return loaded
} else { } else {
return default_value return default_value
@@ -31,7 +31,7 @@ func setting_get_property_value<T>(key: String, scoped_key: String, default_valu
func setting_set_property_value<T: Equatable>(scoped_key: String, old_value: T, new_value: T) -> T? { func setting_set_property_value<T: Equatable>(scoped_key: String, old_value: T, new_value: T) -> T? {
guard old_value != new_value else { return nil } 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() UserSettingsStore.shared?.objectWillChange.send()
return new_value return new_value
} }
@@ -65,14 +65,14 @@ func setting_set_property_value<T: Equatable>(scoped_key: String, old_value: T,
init(key: String, default_value: T) { init(key: String, default_value: T) {
self.key = pk_setting_key(UserSettingsStore.pubkey ?? .empty, key: key) 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 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, // 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. // migrate the deprecated setting into the pubkey-scoped one and delete the deprecated one.
self.value = val self.value = val
DamusUserDefaults.shared.set(val.to_string(), forKey: self.key) DamusUserDefaults.standard.set(val.to_string(), forKey: self.key)
DamusUserDefaults.shared.removeObject(forKey: key) DamusUserDefaults.standard.removeObject(forKey: key)
} else { } else {
self.value = default_value self.value = default_value
} }
@@ -85,7 +85,7 @@ func setting_set_property_value<T: Equatable>(scoped_key: String, old_value: T,
return return
} }
self.value = newValue self.value = newValue
DamusUserDefaults.shared.set(newValue.to_string(), forKey: key) DamusUserDefaults.standard.set(newValue.to_string(), forKey: key)
UserSettingsStore.shared!.objectWillChange.send() UserSettingsStore.shared!.objectWillChange.send()
} }
} }

View File

@@ -9,6 +9,7 @@ import Foundation
class Constants { class Constants {
//static let EXAMPLE_DEMOS: DamusState = .empty //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_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 DEVICE_TOKEN_RECEIVER_TEST_URL: URL = URL(string: "http://localhost:8000/user-info")!
static let MAIN_APP_BUNDLE_IDENTIFIER: String = "com.jb55.damus2" static let MAIN_APP_BUNDLE_IDENTIFIER: String = "com.jb55.damus2"

View File

@@ -124,7 +124,7 @@ func privkey_to_pubkey(privkey: Privkey) -> Pubkey? {
} }
func save_pubkey(pubkey: Pubkey) { func save_pubkey(pubkey: Pubkey) {
DamusUserDefaults.shared.set(pubkey.hex(), forKey: "pubkey") DamusUserDefaults.standard.set(pubkey.hex(), forKey: "pubkey")
} }
enum Keys { enum Keys {
@@ -141,7 +141,7 @@ func clear_saved_privkey() throws {
} }
func clear_saved_pubkey() { func clear_saved_pubkey() {
DamusUserDefaults.shared.removeObject(forKey: "pubkey") DamusUserDefaults.standard.removeObject(forKey: "pubkey")
} }
func save_keypair(pubkey: Pubkey, privkey: Privkey) throws { func save_keypair(pubkey: Pubkey, privkey: Privkey) throws {
@@ -175,7 +175,7 @@ func get_saved_keypair() -> Keypair? {
} }
func get_saved_pubkey() -> String? { func get_saved_pubkey() -> String? {
return DamusUserDefaults.shared.string(forKey: "pubkey") return DamusUserDefaults.standard.string(forKey: "pubkey")
} }
func get_saved_privkey() -> String? { func get_saved_privkey() -> String? {
@@ -198,10 +198,10 @@ func contentContainsPrivateKey(_ content: String) -> Bool {
} }
fileprivate func removePrivateKeyFromUserDefaults() throws { 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) let privkey = hex_decode_privkey(privkey_str)
else { return } else { return }
try save_privkey(privkey: privkey) try save_privkey(privkey: privkey)
DamusUserDefaults.shared.removeObject(forKey: "privkey") DamusUserDefaults.standard.removeObject(forKey: "privkey")
} }

View File

@@ -66,7 +66,7 @@ struct TimelineView<Content: View>: View {
struct TimelineView_Previews: PreviewProvider { struct TimelineView_Previews: PreviewProvider {
@StateObject static var events = test_event_holder @StateObject static var events = test_event_holder
static var previews: some View { static var previews: some View {
TimelineView<AnyView>(events: events, loading: .constant(true), damus: Constants.EXAMPLE_DEMOS, show_friend_icon: true, filter: { _ in true }) TimelineView<AnyView>(events: events, loading: .constant(true), damus: test_damus_state, show_friend_icon: true, filter: { _ in true })
} }
} }