damus

nostr ios client
git clone git://jb55.com/damus
Log | Files | Refs | README | LICENSE

UserSettingsStore.swift (13086B)


      1 //
      2 //  UserSettingsStore.swift
      3 //  damus
      4 //
      5 //  Created by Suhail Saqan on 12/29/22.
      6 //
      7 
      8 import Foundation
      9 import UIKit
     10 
     11 let fallback_zap_amount = 1000
     12 let default_emoji_reactions = ["🤣", "🤙", "⚡", "💜", "🔥", "😀", "😃", "😄", "🥶"]
     13 
     14 func setting_property_key(key: String) -> String {
     15     return pk_setting_key(UserSettingsStore.pubkey ?? .empty, key: key)
     16 }
     17 
     18 func setting_get_property_value<T>(key: String, scoped_key: String, default_value: T) -> T {
     19     if let loaded = DamusUserDefaults.standard.object(forKey: scoped_key) as? T {
     20         return loaded
     21     } else if let loaded = DamusUserDefaults.standard.object(forKey: key) as? T {
     22         // If pubkey-scoped setting does not exist but the deprecated non-pubkey-scoped setting does,
     23         // migrate the deprecated setting into the pubkey-scoped one and delete the deprecated one.
     24         DamusUserDefaults.standard.set(loaded, forKey: scoped_key)
     25         DamusUserDefaults.standard.removeObject(forKey: key)
     26         return loaded
     27     } else {
     28         return default_value
     29     }
     30 }
     31 
     32 func setting_set_property_value<T: Equatable>(scoped_key: String, old_value: T, new_value: T) -> T? {
     33     guard old_value != new_value else { return nil }
     34     DamusUserDefaults.standard.set(new_value, forKey: scoped_key)
     35     UserSettingsStore.shared?.objectWillChange.send()
     36     return new_value
     37 }
     38 
     39 @propertyWrapper struct Setting<T: Equatable> {
     40     private let key: String
     41     private var value: T
     42     
     43     init(key: String, default_value: T) {
     44         if T.self == Bool.self {
     45             UserSettingsStore.bool_options.insert(key)
     46         }
     47         let scoped_key = setting_property_key(key: key)
     48 
     49         self.value = setting_get_property_value(key: key, scoped_key: scoped_key, default_value: default_value)
     50         self.key = scoped_key
     51     }
     52 
     53     var wrappedValue: T {
     54         get { return value }
     55         set {
     56             guard let new_val = setting_set_property_value(scoped_key: key, old_value: value, new_value: newValue) else { return }
     57             self.value = new_val
     58         }
     59     }
     60 }
     61 
     62 @propertyWrapper class StringSetting<T: StringCodable & Equatable> {
     63     private let key: String
     64     private var value: T
     65     
     66     init(key: String, default_value: T) {
     67         self.key = pk_setting_key(UserSettingsStore.pubkey ?? .empty, key: key)
     68         if let loaded = DamusUserDefaults.standard.string(forKey: self.key), let val = T.init(from: loaded) {
     69             self.value = val
     70         } else if let loaded = DamusUserDefaults.standard.string(forKey: key), let val = T.init(from: loaded) {
     71             // If pubkey-scoped setting does not exist but the deprecated non-pubkey-scoped setting does,
     72             // migrate the deprecated setting into the pubkey-scoped one and delete the deprecated one.
     73             self.value = val
     74             DamusUserDefaults.standard.set(val.to_string(), forKey: self.key)
     75             DamusUserDefaults.standard.removeObject(forKey: key)
     76         } else {
     77             self.value = default_value
     78         }
     79     }
     80     
     81     var wrappedValue: T {
     82         get { return value }
     83         set {
     84             guard self.value != newValue else {
     85                 return
     86             }
     87             self.value = newValue
     88             DamusUserDefaults.standard.set(newValue.to_string(), forKey: key)
     89             UserSettingsStore.shared!.objectWillChange.send()
     90         }
     91     }
     92 }
     93 
     94 class UserSettingsStore: ObservableObject {
     95     static var pubkey: Pubkey? = nil
     96     static var shared: UserSettingsStore? = nil
     97     static var bool_options = Set<String>()
     98     
     99     static func globally_load_for(pubkey: Pubkey) -> UserSettingsStore {
    100         // dumb stuff needed for property wrappers
    101         UserSettingsStore.pubkey = pubkey
    102         let settings = UserSettingsStore()
    103         UserSettingsStore.shared = settings
    104         return settings
    105     }
    106     
    107     @StringSetting(key: "default_wallet", default_value: .system_default_wallet)
    108     var default_wallet: Wallet
    109     
    110     @StringSetting(key: "default_media_uploader", default_value: .nostrBuild)
    111     var default_media_uploader: MediaUploader
    112     
    113     @Setting(key: "show_wallet_selector", default_value: false)
    114     var show_wallet_selector: Bool
    115     
    116     @Setting(key: "left_handed", default_value: false)
    117     var left_handed: Bool
    118     
    119     @Setting(key: "blur_images", default_value: true)
    120     var blur_images: Bool
    121     
    122     @Setting(key: "media_previews", default_value: true)
    123     var media_previews: Bool
    124     
    125     @Setting(key: "hide_nsfw_tagged_content", default_value: false)
    126     var hide_nsfw_tagged_content: Bool
    127     
    128     @Setting(key: "show_profile_action_sheet_on_pfp_click", default_value: true)
    129     var show_profile_action_sheet_on_pfp_click: Bool
    130 
    131     @Setting(key: "zap_vibration", default_value: true)
    132     var zap_vibration: Bool
    133     
    134     @Setting(key: "zap_notification", default_value: true)
    135     var zap_notification: Bool
    136     
    137     @Setting(key: "default_zap_amount", default_value: fallback_zap_amount)
    138     var default_zap_amount: Int
    139     
    140     @Setting(key: "mention_notification", default_value: true)
    141     var mention_notification: Bool
    142     
    143     @StringSetting(key: "zap_type", default_value: ZapType.pub)
    144     var default_zap_type: ZapType
    145 
    146     @Setting(key: "repost_notification", default_value: true)
    147     var repost_notification: Bool
    148 
    149     @Setting(key: "font_size", default_value: 1.0)
    150     var font_size: Double
    151 
    152     @Setting(key: "dm_notification", default_value: true)
    153     var dm_notification: Bool
    154     
    155     @Setting(key: "like_notification", default_value: true)
    156     var like_notification: Bool
    157     
    158     @StringSetting(key: "notification_mode", default_value: .push)
    159     var notification_mode: NotificationsMode
    160     
    161     @Setting(key: "notification_only_from_following", default_value: false)
    162     var notification_only_from_following: Bool
    163     
    164     @Setting(key: "translate_dms", default_value: false)
    165     var translate_dms: Bool
    166     
    167     @Setting(key: "truncate_timeline_text", default_value: false)
    168     var truncate_timeline_text: Bool
    169     
    170     /// Nozaps mode gimps note zapping to fit into apple's content-tipping guidelines. It can not be configurable to end-users on the app store
    171     @Setting(key: "nozaps", default_value: true)
    172     var nozaps: Bool
    173     
    174     @Setting(key: "truncate_mention_text", default_value: true)
    175     var truncate_mention_text: Bool
    176     
    177     @Setting(key: "notification_indicators", default_value: NewEventsBits.all.rawValue)
    178     var notification_indicators: Int
    179     
    180     @Setting(key: "auto_translate", default_value: true)
    181     var auto_translate: Bool
    182 
    183     @Setting(key: "show_general_statuses", default_value: true)
    184     var show_general_statuses: Bool
    185 
    186     @Setting(key: "show_music_statuses", default_value: true)
    187     var show_music_statuses: Bool
    188     
    189     @Setting(key: "multiple_events_per_pubkey", default_value: false)
    190     var multiple_events_per_pubkey: Bool
    191 
    192     @Setting(key: "onlyzaps_mode", default_value: false)
    193     var onlyzaps_mode: Bool
    194     
    195     @Setting(key: "disable_animation", default_value: UIAccessibility.isReduceMotionEnabled)
    196     var disable_animation: Bool
    197     
    198     @Setting(key: "donation_percent", default_value: 0)
    199     var donation_percent: Int
    200     
    201     @Setting(key: "developer_mode", default_value: false)
    202     var developer_mode: Bool
    203     
    204     /// Makes all post content gibberish and blurhashes images, to avoid distractions when developers are working.
    205     @Setting(key: "undistract_mode", default_value: false)
    206     var undistractMode: Bool
    207     
    208     @Setting(key: "always_show_onboarding_suggestions", default_value: false)
    209     var always_show_onboarding_suggestions: Bool
    210 
    211     // @Setting(key: "enable_experimental_push_notifications", default_value: false)
    212     // This was a feature flag setting during early development, but now this is enabled for everyone.
    213     var enable_push_notifications: Bool = true
    214     
    215     @StringSetting(key: "push_notification_environment", default_value: .production)
    216     var push_notification_environment: PushNotificationClient.Environment
    217     
    218     @Setting(key: "enable_experimental_purple_api", default_value: false)
    219     var enable_experimental_purple_api: Bool
    220     
    221     @StringSetting(key: "purple_environment", default_value: .production)
    222     var purple_enviroment: DamusPurpleEnvironment
    223 
    224     @Setting(key: "enable_experimental_purple_iap_support", default_value: false)
    225     var enable_experimental_purple_iap_support: Bool
    226     
    227     @Setting(key: "emoji_reactions", default_value: default_emoji_reactions)
    228     var emoji_reactions: [String]
    229     
    230     @Setting(key: "default_emoji_reaction", default_value: "🤙")
    231     var default_emoji_reaction: String
    232 
    233     // Helper for inverse of disable_animation.
    234     // disable_animation was introduced as a setting first, but it's more natural for the settings UI to show the inverse.
    235     var enable_animation: Bool {
    236         get {
    237             !disable_animation
    238         }
    239         set {
    240             disable_animation = !newValue
    241         }
    242     }
    243     
    244     @StringSetting(key: "friend_filter", default_value: .all)
    245     var friend_filter: FriendFilter
    246 
    247     @StringSetting(key: "translation_service", default_value: .none)
    248     var translation_service: TranslationService
    249 
    250     @StringSetting(key: "deepl_plan", default_value: .free)
    251     var deepl_plan: DeepLPlan
    252     
    253     var deepl_api_key: String {
    254         get {
    255             return internal_deepl_api_key ?? ""
    256         }
    257         set {
    258             internal_deepl_api_key = newValue == "" ? nil : newValue
    259         }
    260     }
    261 
    262     @StringSetting(key: "libretranslate_server", default_value: .custom)
    263     var libretranslate_server: LibreTranslateServer
    264     
    265     @Setting(key: "libretranslate_url", default_value: "")
    266     var libretranslate_url: String
    267 
    268     var libretranslate_api_key: String {
    269         get {
    270             return internal_libretranslate_api_key ?? ""
    271         }
    272         set {
    273             internal_libretranslate_api_key = newValue == "" ? nil : newValue
    274         }
    275     }
    276     
    277     var nokyctranslate_api_key: String {
    278         get {
    279             return internal_nokyctranslate_api_key ?? ""
    280         }
    281         set {
    282             internal_nokyctranslate_api_key = newValue == "" ? nil : newValue
    283         }
    284     }
    285 
    286     var winetranslate_api_key: String {
    287         get {
    288             return internal_winetranslate_api_key ?? ""
    289         }
    290         set {
    291             internal_winetranslate_api_key = newValue == "" ? nil : newValue
    292         }
    293     }
    294     
    295     // These internal keys are necessary because entries in the keychain need to be Optional,
    296     // but the translation view needs non-Optional String in order to use them as Bindings.
    297     @KeychainStorage(account: "deepl_apikey")
    298     var internal_deepl_api_key: String?
    299     
    300     @KeychainStorage(account: "nokyctranslate_apikey")
    301     var internal_nokyctranslate_api_key: String?
    302 
    303     @KeychainStorage(account: "winetranslate_apikey")
    304     var internal_winetranslate_api_key: String?
    305     
    306     @KeychainStorage(account: "libretranslate_apikey")
    307     var internal_libretranslate_api_key: String?
    308     
    309     @KeychainStorage(account: "nostr_wallet_connect")
    310     var nostr_wallet_connect: String? // TODO: strongly type this to WalletConnectURL
    311 
    312     var can_translate: Bool {
    313         switch translation_service {
    314         case .none:
    315             return false
    316         case .purple:
    317             return true
    318         case .libretranslate:
    319             return URLComponents(string: libretranslate_url) != nil
    320         case .deepl:
    321             return internal_deepl_api_key != nil
    322         case .nokyctranslate:
    323             return internal_nokyctranslate_api_key != nil
    324         case .winetranslate:
    325             return internal_winetranslate_api_key != nil
    326         }
    327     }
    328     
    329     // MARK: Internal, hidden settings
    330     
    331     // TODO: Get rid of this once we have NostrDB query capabilities integrated
    332     @Setting(key: "latest_contact_event_id", default_value: nil)
    333     var latest_contact_event_id_hex: String?
    334     
    335     // TODO: Get rid of this once we have NostrDB query capabilities integrated
    336     @Setting(key: "draft_event_ids", default_value: nil)
    337     var draft_event_ids: [String]?
    338     
    339     // MARK: Helper types
    340     
    341     enum NotificationsMode: String, CaseIterable, Identifiable, StringCodable, Equatable {
    342         var id: String { self.rawValue }
    343 
    344         func to_string() -> String {
    345             return rawValue
    346         }
    347         
    348         init?(from string: String) {
    349             guard let notifications_mode = NotificationsMode(rawValue: string) else {
    350                 return nil
    351             }
    352             self = notifications_mode
    353         }
    354         
    355         func text_description() -> String {
    356             switch self {
    357                 case .local:
    358                     NSLocalizedString("Local", comment: "Option for notification mode setting: Local notification mode")
    359                 case .push:
    360                     NSLocalizedString("Push", comment: "Option for notification mode setting: Push notification mode")
    361             }
    362         }
    363         
    364         case local
    365         case push
    366     }
    367     
    368 }
    369 
    370 func pk_setting_key(_ pubkey: Pubkey, key: String) -> String {
    371     return "\(pubkey.hex())_\(key)"
    372 }