commit aa559b2916e01c7721fce4ced2f4b6f41e3df3ba
parent 9bf8349db60a9f247aa02e29a1205c8bb7ec9716
Author: William Casarin <jb55@jb55.com>
Date:   Fri, 21 Apr 2023 16:21:01 -0700
Refactor and Scope user settings to pubkey
Diffstat:
10 files changed, 269 insertions(+), 286 deletions(-)
diff --git a/damus.xcodeproj/project.pbxproj b/damus.xcodeproj/project.pbxproj
@@ -155,6 +155,7 @@
 		4C9F18E229AA9B6C008C55EC /* CustomizeZapView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C9F18E129AA9B6C008C55EC /* CustomizeZapView.swift */; };
 		4C9F18E429ABDE6D008C55EC /* MaybeAnonPfpView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C9F18E329ABDE6D008C55EC /* MaybeAnonPfpView.swift */; };
 		4CA2EFA0280E37AC0044ACD8 /* TimelineView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CA2EF9F280E37AC0044ACD8 /* TimelineView.swift */; };
+		4CA5588329F33F5B00DC6A45 /* StringCodable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CA5588229F33F5B00DC6A45 /* StringCodable.swift */; };
 		4CAAD8AD298851D000060CEA /* AccountDeletion.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CAAD8AC298851D000060CEA /* AccountDeletion.swift */; };
 		4CAAD8B029888AD200060CEA /* RelayConfigView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CAAD8AF29888AD200060CEA /* RelayConfigView.swift */; };
 		4CACA9D5280C31E100D9BBE8 /* ReplyView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CACA9D4280C31E100D9BBE8 /* ReplyView.swift */; };
@@ -563,6 +564,7 @@
 		4C9F18E129AA9B6C008C55EC /* CustomizeZapView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomizeZapView.swift; sourceTree = "<group>"; };
 		4C9F18E329ABDE6D008C55EC /* MaybeAnonPfpView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MaybeAnonPfpView.swift; sourceTree = "<group>"; };
 		4CA2EF9F280E37AC0044ACD8 /* TimelineView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineView.swift; sourceTree = "<group>"; };
+		4CA5588229F33F5B00DC6A45 /* StringCodable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StringCodable.swift; sourceTree = "<group>"; };
 		4CAAD8AC298851D000060CEA /* AccountDeletion.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountDeletion.swift; sourceTree = "<group>"; };
 		4CAAD8AF29888AD200060CEA /* RelayConfigView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RelayConfigView.swift; sourceTree = "<group>"; };
 		4CACA9D4280C31E100D9BBE8 /* ReplyView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReplyView.swift; sourceTree = "<group>"; };
@@ -1008,6 +1010,7 @@
 				4C8D00C729DF791C0036AF10 /* CompatibleAttribute.swift */,
 				4C8D00CB29DF92DF0036AF10 /* Hashtags.swift */,
 				4CDA128B29EB19C40006FA5A /* LocalNotification.swift */,
+				4CA5588229F33F5B00DC6A45 /* StringCodable.swift */,
 			);
 			path = Util;
 			sourceTree = "<group>";
@@ -1524,6 +1527,7 @@
 				4CC6193A29DC777C006A86D1 /* RelayBootstrap.swift in Sources */,
 				4C285C8A2838B985008A31F1 /* ProfilePictureSelector.swift in Sources */,
 				4C9F18E429ABDE6D008C55EC /* MaybeAnonPfpView.swift in Sources */,
+				4CA5588329F33F5B00DC6A45 /* StringCodable.swift in Sources */,
 				4C75EFB92804A2740006080F /* EventView.swift in Sources */,
 				4C8D00C829DF791C0036AF10 /* CompatibleAttribute.swift in Sources */,
 				3AA247FD297E3CFF0090C62D /* RepostsModel.swift in Sources */,
diff --git a/damus/ContentView.swift b/damus/ContentView.swift
@@ -678,7 +678,12 @@ struct ContentView: View {
         }
         
         pool.register_handler(sub_id: sub_id, handler: home.handle_event)
-
+        
+        // dumb stuff needed for property wrappers
+        UserSettingsStore.pubkey = pubkey
+        let settings = UserSettingsStore()
+        UserSettingsStore.shared = settings
+        
         self.damus_state = DamusState(pool: pool,
                                       keypair: keypair,
                                       likes: EventCounter(our_pubkey: pubkey),
@@ -690,7 +695,7 @@ struct ContentView: View {
                                       previews: PreviewCache(),
                                       zaps: Zaps(our_pubkey: pubkey),
                                       lnurls: LNUrls(),
-                                      settings: UserSettingsStore(),
+                                      settings: settings,
                                       relay_filters: relay_filters,
                                       relay_metadata: metadatas,
                                       drafts: Drafts(),
diff --git a/damus/Models/DamusState.swift b/damus/Models/DamusState.swift
@@ -39,6 +39,8 @@ struct DamusState {
         keypair.privkey != nil
     }
     
+    static var settings_pubkey: String? = nil
+    
     static var empty: DamusState {
         return DamusState.init(pool: RelayPool(), keypair: Keypair(pubkey: "", privkey: ""), likes: EventCounter(our_pubkey: ""), boosts: EventCounter(our_pubkey: ""), contacts: Contacts(our_pubkey: ""), tips: TipCounter(our_pubkey: ""), profiles: Profiles(), dms: DirectMessagesModel(our_pubkey: ""), previews: PreviewCache(), zaps: Zaps(our_pubkey: ""), lnurls: LNUrls(), settings: UserSettingsStore(), relay_filters: RelayFilters(our_pubkey: ""), relay_metadata: RelayMetadatas(), drafts: Drafts(), events: EventCache(), bookmarks: BookmarksManager(pubkey: ""), postbox: PostBox(pool: RelayPool()), bootstrap_relays: [], replies: ReplyCounter(our_pubkey: ""), muted_threads: MutedThreadsManager(keypair: Keypair(pubkey: "", privkey: nil))) }
 }
diff --git a/damus/Models/DeepLPlan.swift b/damus/Models/DeepLPlan.swift
@@ -7,7 +7,19 @@
 
 import Foundation
 
-enum DeepLPlan: String, CaseIterable, Identifiable {
+enum DeepLPlan: String, CaseIterable, Identifiable, StringCodable {
+    init?(from string: String) {
+        guard let dl = DeepLPlan(rawValue: string) else {
+            return nil
+        }
+        
+        self = dl
+    }
+    
+    func to_string() -> String {
+        return self.rawValue
+    }
+    
     var id: String { self.rawValue }
 
     struct Model: Identifiable, Hashable {
diff --git a/damus/Models/TranslationService.swift b/damus/Models/TranslationService.swift
@@ -7,7 +7,19 @@
 
 import Foundation
 
-enum TranslationService: String, CaseIterable, Identifiable {
+enum TranslationService: String, CaseIterable, Identifiable, StringCodable {
+    init?(from string: String) {
+        guard let ts = TranslationService(rawValue: string) else {
+            return nil
+        }
+        
+        self = ts
+    }
+    
+    func to_string() -> String {
+        return self.rawValue
+    }
+    
     var id: String { self.rawValue }
 
     struct Model: Identifiable, Hashable {
diff --git a/damus/Models/UserSettingsStore.swift b/damus/Models/UserSettingsStore.swift
@@ -9,218 +9,135 @@ import Foundation
 import Vault
 import UIKit
 
-func should_show_wallet_selector(_ pubkey: String) -> Bool {
-    return UserDefaults.standard.object(forKey: "show_wallet_selector") as? Bool ?? true
-}
-
-func pk_setting_key(_ pubkey: String, key: String) -> String {
-    return "\(pubkey)_\(key)"
-}
-
-func default_zap_setting_key(pubkey: String) -> String {
-    return pk_setting_key(pubkey, key: "default_zap_amount")
-}
-
-func set_default_zap_amount(pubkey: String, amount: Int) {
-    let key = default_zap_setting_key(pubkey: pubkey)
-    UserDefaults.standard.setValue(amount, forKey: key)
-}
-
-let fallback_zap_amount = 1000
-
-func get_default_zap_amount(pubkey: String) -> Int {
-    let key = default_zap_setting_key(pubkey: pubkey)
-    let amt = UserDefaults.standard.integer(forKey: key)
-    if amt == 0 {
-        return fallback_zap_amount
-    }
-    return amt
-}
-
-func should_disable_image_animation() -> Bool {
-    return (UserDefaults.standard.object(forKey: "disable_animation") as? Bool)
-            ?? UIAccessibility.isReduceMotionEnabled
- }
-
-func get_default_wallet(_ pubkey: String) -> Wallet {
-    if let defaultWalletName = UserDefaults.standard.string(forKey: "default_wallet"),
-       let default_wallet = Wallet(rawValue: defaultWalletName)
-    {
-        return default_wallet
-    } else {
-        return .system_default_wallet
-    }
-}
-
-func get_media_uploader(_ pubkey: String) -> MediaUploader {
-    if let defaultMediaUploader = UserDefaults.standard.string(forKey: "default_media_uploader"),
-       let defaultMediaUploader = MediaUploader(rawValue: defaultMediaUploader) {
-        return defaultMediaUploader
-    } else {
-        return .nostrBuild
-    }
-}
-
-private func get_translation_service(_ pubkey: String) -> TranslationService? {
-    guard let translation_service = UserDefaults.standard.string(forKey: "translation_service") else {
-        return nil
-    }
-
-    return TranslationService(rawValue: translation_service)
-}
-
-private func get_deepl_plan(_ pubkey: String) -> DeepLPlan? {
-    guard let server_name = UserDefaults.standard.string(forKey: "deepl_plan") else {
-        return nil
-    }
-
-    return DeepLPlan(rawValue: server_name)
-}
-
-private func get_libretranslate_server(_ pubkey: String) -> LibreTranslateServer? {
-    guard let server_name = UserDefaults.standard.string(forKey: "libretranslate_server") else {
-        return nil
-    }
+@propertyWrapper struct Setting<T: Equatable> {
+    private let key: String
+    private var value: T
     
-    return LibreTranslateServer(rawValue: server_name)
-}
-
-private func get_libretranslate_url(_ pubkey: String, server: LibreTranslateServer) -> String? {
-    if let url = server.model.url {
-        return url
-    }
-    
-    return UserDefaults.standard.object(forKey: "libretranslate_url") as? String
-}
-
-class UserSettingsStore: ObservableObject {
-    @Published var default_wallet: Wallet {
-        didSet {
-            UserDefaults.standard.set(default_wallet.rawValue, forKey: "default_wallet")
-        }
-    }
-
-    @Published var default_media_uploader: MediaUploader {
-        didSet {
-            UserDefaults.standard.set(default_media_uploader.rawValue, forKey: "default_media_uploader")
+    init(key: String, default_value: T) {
+        self.key = pk_setting_key(UserSettingsStore.pubkey ?? "", key: key)
+        if let loaded = UserDefaults.standard.object(forKey: self.key) as? T {
+            self.value = loaded
+        } else if let loaded = UserDefaults.standard.object(forKey: key) as? T {
+            // try to load from deprecated non-pubkey-keyed setting
+            self.value = loaded
+        } else {
+            self.value = default_value
         }
     }
     
-    @Published var show_wallet_selector: Bool {
-        didSet {
-            UserDefaults.standard.set(show_wallet_selector, forKey: "show_wallet_selector")
+    var wrappedValue: T {
+        get { return value }
+        set {
+            guard self.value != newValue else {
+                return
+            }
+            self.value = newValue
+            UserDefaults.standard.set(newValue, forKey: key)
+            UserSettingsStore.shared!.objectWillChange.send()
         }
     }
+}
 
-    @Published var left_handed: Bool {
-        didSet {
-            UserDefaults.standard.set(left_handed, forKey: "left_handed")
-        }
-    }
+@propertyWrapper class StringSetting<T: StringCodable & Equatable> {
+    private let key: String
+    private var value: T
     
-    @Published var always_show_images: Bool {
-        didSet {
-            UserDefaults.standard.set(always_show_images, forKey: "always_show_images")
-        }
-    }
-
-    @Published var zap_vibration: Bool {
-        didSet {
-            UserDefaults.standard.set(zap_vibration, forKey: "zap_vibration")
-        }
-    }
-
-    @Published var zap_notification: Bool {
-        didSet {
-            UserDefaults.standard.set(zap_notification, forKey: "zap_notification")
-        }
-    }
-
-    @Published var mention_notification: Bool {
-        didSet {
-            UserDefaults.standard.set(mention_notification, forKey: "mention_notification")
-        }
-    }
-
-    @Published var repost_notification: Bool {
-        didSet {
-            UserDefaults.standard.set(repost_notification, forKey: "repost_notification")
-        }
-    }
-
-    @Published var dm_notification: Bool {
-        didSet {
-            UserDefaults.standard.set(dm_notification, forKey: "dm_notification")
-        }
-    }
-
-    @Published var like_notification: Bool {
-        didSet {
-            UserDefaults.standard.set(like_notification, forKey: "like_notification")
-        }
-    }
-
-    @Published var notification_only_from_following: Bool {
-        didSet {
-            UserDefaults.standard.set(notification_only_from_following, forKey: "notification_only_from_following")
+    init(key: String, default_value: T) {
+        self.key = pk_setting_key(UserSettingsStore.pubkey ?? "", key: key)
+        if let loaded = UserDefaults.standard.string(forKey: self.key), let val = T.init(from: loaded) {
+            self.value = val
+        } else if let loaded = UserDefaults.standard.string(forKey: key), let val = T.init(from: loaded) {
+            // try to load from deprecated non-pubkey-keyed setting
+            self.value = val
+        } else {
+            self.value = default_value
         }
     }
     
-    @Published var translate_dms: Bool {
-        didSet {
-            UserDefaults.standard.set(translate_dms, forKey: "translate_dms")
+    var wrappedValue: T {
+        get { return value }
+        set {
+            guard self.value != newValue else {
+                return
+            }
+            self.value = newValue
+            UserDefaults.standard.set(newValue.to_string(), forKey: key)
+            UserSettingsStore.shared!.objectWillChange.send()
         }
     }
+}
 
-    @Published var truncate_timeline_text: Bool {
-        didSet {
-            UserDefaults.standard.set(truncate_timeline_text, forKey: "truncate_timeline_text")
-        }
-    }
+class UserSettingsStore: ObservableObject {
+    static var pubkey: String? = nil
+    static var shared: UserSettingsStore? = nil
     
-    @Published var notification_indicators: Int {
-        didSet {
-            UserDefaults.standard.set(notification_indicators, forKey: "notification_indicators")
-        }
-    }
+    @StringSetting(key: "default_wallet", default_value: .system_default_wallet)
+    var default_wallet: Wallet
     
-    @Published var truncate_mention_text: Bool {
-        didSet {
-            UserDefaults.standard.set(truncate_mention_text, forKey: "truncate_mention_text")
-        }
-    }
+    @StringSetting(key: "default_media_uploader", default_value: .nostrBuild)
+    var default_media_uploader: MediaUploader
+    
+    @Setting(key: "show_wallet_selector", default_value: true)
+    var show_wallet_selector: Bool
+    
+    @Setting(key: "left_handed", default_value: false)
+    var left_handed: Bool
+    
+    @Setting(key: "always_show_images", default_value: false)
+    var always_show_images: Bool
 
-    @Published var auto_translate: Bool {
-        didSet {
-            UserDefaults.standard.set(auto_translate, forKey: "auto_translate")
-        }
-    }
+    @Setting(key: "zap_vibration", default_value: true)
+    var zap_vibration: Bool
+    
+    @Setting(key: "zap_notification", default_value: true)
+    var zap_notification: Bool
+    
+    @Setting(key: "mention_notification", default_value: true)
+    var mention_notification: Bool
 
-    @Published var show_only_preferred_languages: Bool {
-        didSet {
-            UserDefaults.standard.set(show_only_preferred_languages, forKey: "show_only_preferred_languages")
-        }
-    }
+    @Setting(key: "repost_notification", default_value: true)
+    var repost_notification: Bool
+    
+    @Setting(key: "dm_notification", default_value: true)
+    var dm_notification: Bool
+    
+    @Setting(key: "like_notification", default_value: true)
+    var like_notification: Bool
+    
+    @Setting(key: "notification_only_from_following", default_value: false)
+    var notification_only_from_following: Bool
+    
+    @Setting(key: "translate_dms", default_value: false)
+    var translate_dms: Bool
+    
+    @Setting(key: "truncate_timeline_text", default_value: false)
+    var truncate_timeline_text: Bool
+    
+    @Setting(key: "truncate_mention_text", default_value: true)
+    var truncate_mention_text: Bool
+    
+    @Setting(key: "notification_indicators", default_value: NewEventsBits.all.rawValue)
+    var notification_indicators: Int
+    
+    @Setting(key: "auto_translate", default_value: true)
+    var auto_translate: Bool
 
-    @Published var onlyzaps_mode: Bool {
-        didSet {
-            UserDefaults.standard.set(onlyzaps_mode, forKey: "onlyzaps_mode")
-        }
-    }
+    @Setting(key: "show_only_preferred_languages", default_value: false)
+    var show_only_preferred_languages: Bool
 
-    @Published var translation_service: TranslationService {
-        didSet {
-            UserDefaults.standard.set(translation_service.rawValue, forKey: "translation_service")
-        }
-    }
+    @Setting(key: "onlyzaps_mode", default_value: false)
+    var onlyzaps_mode: Bool
+    
+    @Setting(key: "disable_animation", default_value: UIAccessibility.isReduceMotionEnabled)
+    var disable_animation: Bool
 
-    @Published var deepl_plan: DeepLPlan {
-        didSet {
-            UserDefaults.standard.set(deepl_plan.rawValue, forKey: "deepl_plan")
-        }
-    }
+    @StringSetting(key: "translation_service", default_value: .none)
+    var translation_service: TranslationService
 
-    @Published var deepl_api_key: String {
+    @StringSetting(key: "deepl_plan", default_value: .free)
+    var deepl_plan: DeepLPlan
+    
+    var deepl_api_key: String {
         didSet {
             do {
                 if deepl_api_key == "" {
@@ -234,31 +151,14 @@ class UserSettingsStore: ObservableObject {
         }
     }
 
-    @Published var libretranslate_server: LibreTranslateServer {
-        didSet {
-            if oldValue == libretranslate_server {
-                return
-            }
-
-            UserDefaults.standard.set(libretranslate_server.rawValue, forKey: "libretranslate_server")
-
-            libretranslate_api_key = ""
-
-            if libretranslate_server == .custom {
-                libretranslate_url = ""
-            } else {
-                libretranslate_url = libretranslate_server.model.url!
-            }
-        }
-    }
-
-    @Published var libretranslate_url: String {
-        didSet {
-            UserDefaults.standard.set(libretranslate_url, forKey: "libretranslate_url")
-        }
-    }
+    @Setting(key: "libretranslate_server", default_value: .vern)
+    var libretranslate_server: LibreTranslateServer
+    
+    @Setting(key: "libretranslate_url", default_value: "")
+    var libretranslate_url: String
 
-    @Published var libretranslate_api_key: String {
+    @Setting(key: "libretranslate_api_key", default_value: "")
+    var libretranslate_api_key: String {
         didSet {
             do {
                 if libretranslate_api_key == "" {
@@ -271,72 +171,8 @@ class UserSettingsStore: ObservableObject {
             }
         }
     }
-    
-    @Published var disable_animation: Bool {
-        didSet {
-            UserDefaults.standard.set(disable_animation, forKey: "disable_animation")
-        }
-     }
 
     init() {
-        // TODO: pubkey-scoped settings
-        let pubkey = ""
-        self.default_wallet = get_default_wallet(pubkey)
-        show_wallet_selector = should_show_wallet_selector(pubkey)
-        always_show_images = UserDefaults.standard.object(forKey: "always_show_images") as? Bool ?? false
-
-        default_media_uploader = get_media_uploader(pubkey)
-
-        left_handed = UserDefaults.standard.object(forKey: "left_handed") as? Bool ?? false
-        zap_vibration = UserDefaults.standard.object(forKey: "zap_vibration") as? Bool ?? false
-        zap_notification = UserDefaults.standard.object(forKey: "zap_notification") as? Bool ?? true
-        mention_notification = UserDefaults.standard.object(forKey: "mention_notification") as? Bool ?? true
-        repost_notification = UserDefaults.standard.object(forKey: "repost_notification") as? Bool ?? true
-        like_notification = UserDefaults.standard.object(forKey: "like_notification") as? Bool ?? true
-        dm_notification = UserDefaults.standard.object(forKey: "dm_notification") as? Bool ?? true
-        notification_indicators = UserDefaults.standard.object(forKey: "notification_indicators") as? Int ?? NewEventsBits.all.rawValue
-        notification_only_from_following = UserDefaults.standard.object(forKey: "notification_only_from_following") as? Bool ?? false
-        translate_dms = UserDefaults.standard.object(forKey: "translate_dms") as? Bool ?? false
-        truncate_timeline_text = UserDefaults.standard.object(forKey: "truncate_timeline_text") as? Bool ?? false
-        truncate_mention_text = UserDefaults.standard.object(forKey: "truncate_mention_text") as? Bool ?? false
-        disable_animation = should_disable_image_animation()
-        auto_translate = UserDefaults.standard.object(forKey: "auto_translate") as? Bool ?? true
-        show_only_preferred_languages = UserDefaults.standard.object(forKey: "show_only_preferred_languages") as? Bool ?? false
-        onlyzaps_mode = UserDefaults.standard.object(forKey: "onlyzaps_mode") as? Bool ?? false
-
-        // Note from @tyiu:
-        // Default translation service is disabled by default for now until we gain some confidence that it is working well in production.
-        // Instead of throwing all Damus users onto feature immediately, allow for discovery of feature organically.
-        // Also, we are connecting to servers listed as mirrors on the official LibreTranslate GitHub README that do not require API keys.
-        // However, we have not asked them for permission to use, so we're trying to be good neighbors for now.
-        // Opportunity: spin up dedicated trusted LibreTranslate server that requires an API key for any access (or higher rate limit access).
-        if let translation_service = get_translation_service(pubkey) {
-            self.translation_service = translation_service
-        } else {
-            self.translation_service = .none
-        }
-
-        if let libretranslate_server = get_libretranslate_server(pubkey) {
-            self.libretranslate_server = libretranslate_server
-            self.libretranslate_url = get_libretranslate_url(pubkey, server: libretranslate_server) ?? ""
-        } else {
-            // Choose a random server to distribute load.
-            libretranslate_server = .allCases.filter { $0 != .custom }.randomElement()!
-            libretranslate_url = ""
-        }
-            
-        do {
-            libretranslate_api_key = try Vault.getPrivateKey(keychainConfiguration: DamusLibreTranslateKeychainConfiguration())
-        } catch {
-            libretranslate_api_key = ""
-        }
-
-        if let deepl_plan = get_deepl_plan(pubkey) {
-            self.deepl_plan = deepl_plan
-        } else {
-            self.deepl_plan = .free
-        }
-
         do {
             deepl_api_key = try Vault.getPrivateKey(keychainConfiguration: DamusDeepLKeychainConfiguration())
         } catch {
@@ -383,3 +219,79 @@ struct DamusDeepLKeychainConfiguration: KeychainConfiguration {
     var accessGroup: String? = nil
     var accountName = "deepl_apikey"
 }
+
+func should_show_wallet_selector(_ pubkey: String) -> Bool {
+    return UserDefaults.standard.object(forKey: "show_wallet_selector") as? Bool ?? true
+}
+
+func pk_setting_key(_ pubkey: String, key: String) -> String {
+    return "\(pubkey)_\(key)"
+}
+
+func default_zap_setting_key(pubkey: String) -> String {
+    return pk_setting_key(pubkey, key: "default_zap_amount")
+}
+
+func set_default_zap_amount(pubkey: String, amount: Int) {
+    let key = default_zap_setting_key(pubkey: pubkey)
+    UserDefaults.standard.setValue(amount, forKey: key)
+}
+
+let fallback_zap_amount = 1000
+
+func get_default_zap_amount(pubkey: String) -> Int {
+    let key = default_zap_setting_key(pubkey: pubkey)
+    let amt = UserDefaults.standard.integer(forKey: key)
+    if amt == 0 {
+        return fallback_zap_amount
+    }
+    return amt
+}
+
+func should_disable_image_animation() -> Bool {
+    return (UserDefaults.standard.object(forKey: "disable_animation") as? Bool)
+            ?? UIAccessibility.isReduceMotionEnabled
+ }
+
+func get_default_wallet(_ pubkey: String) -> Wallet {
+    if let defaultWalletName = UserDefaults.standard.string(forKey: "default_wallet"),
+       let default_wallet = Wallet(rawValue: defaultWalletName)
+    {
+        return default_wallet
+    } else {
+        return .system_default_wallet
+    }
+}
+
+func get_media_uploader(_ pubkey: String) -> MediaUploader {
+    if let defaultMediaUploader = UserDefaults.standard.string(forKey: "default_media_uploader"),
+       let defaultMediaUploader = MediaUploader(rawValue: defaultMediaUploader) {
+        return defaultMediaUploader
+    } else {
+        return .nostrBuild
+    }
+}
+
+private func get_translation_service(_ pubkey: String) -> TranslationService? {
+    guard let translation_service = UserDefaults.standard.string(forKey: "translation_service") else {
+        return nil
+    }
+
+    return TranslationService(rawValue: translation_service)
+}
+
+private func get_libretranslate_url(_ pubkey: String, server: LibreTranslateServer) -> String? {
+    if let url = server.model.url {
+        return url
+    }
+    
+    return UserDefaults.standard.object(forKey: "libretranslate_url") as? String
+}
+
+private func get_libretranslate_server(_ pubkey: String) -> LibreTranslateServer? {
+    guard let server_name = UserDefaults.standard.string(forKey: "libretranslate_server") else {
+        return nil
+    }
+    
+    return LibreTranslateServer(rawValue: server_name)
+}
diff --git a/damus/Models/Wallet.swift b/damus/Models/Wallet.swift
@@ -7,7 +7,7 @@
 
 import Foundation
 
-enum Wallet: String, CaseIterable, Identifiable {
+enum Wallet: String, CaseIterable, Identifiable, StringCodable {
     var id: String { self.rawValue }
     
     struct Model: Identifiable, Hashable {
@@ -20,6 +20,17 @@ enum Wallet: String, CaseIterable, Identifiable {
         var image: String
     }
     
+    func to_string() -> String {
+        return rawValue
+    }
+    
+    init?(from string: String) {
+        guard let w = Wallet(rawValue: string) else {
+            return nil
+        }
+        self = w
+    }
+    
     // New url prefixes needed to be added to LSApplicationQueriesSchemes
     case system_default_wallet
     case strike
diff --git a/damus/Util/StringCodable.swift b/damus/Util/StringCodable.swift
@@ -0,0 +1,13 @@
+//
+//  StringCodable.swift
+//  damus
+//
+//  Created by William Casarin on 2023-04-21.
+//
+
+import Foundation
+
+protocol StringCodable {
+    init?(from string: String)
+    func to_string() -> String
+}
diff --git a/damus/Views/AttachMediaUtility.swift b/damus/Views/AttachMediaUtility.swift
@@ -89,10 +89,22 @@ extension NSMutableData {
     }
 }
 
-enum MediaUploader: String, CaseIterable, Identifiable {
+enum MediaUploader: String, CaseIterable, Identifiable, StringCodable {
     var id: String { self.rawValue }
     case nostrBuild
     case nostrImg
+    
+    init?(from string: String) {
+        guard let mu = MediaUploader(rawValue: string) else {
+            return nil
+        }
+        
+        self = mu
+    }
+    
+    func to_string() -> String {
+        return rawValue
+    }
 
     var nameParam: String {
         switch self {
diff --git a/damus/Views/Profile/ProfileView.swift b/damus/Views/Profile/ProfileView.swift
@@ -266,9 +266,9 @@ struct ProfileView: View {
                         } label: {
                             Label(addr, systemImage: "doc.on.doc")
                         }
-                    } else if let lnurl = profile.lud06 {
+                    } else if let lnurl = profile.lnurl {
                         Button {
-                            UIPasteboard.general.string = profile.lnurl ?? ""
+                            UIPasteboard.general.string = lnurl
                         } label: {
                             Label(NSLocalizedString("Copy LNURL", comment: "Context menu option for copying a user's Lightning URL."), systemImage: "doc.on.doc")
                         }