damus

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

UserSettingsStore.swift (11113B)


      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     @StringSetting(key: "default_wallet", default_value: .system_default_wallet)
    100     var default_wallet: Wallet
    101     
    102     @StringSetting(key: "default_media_uploader", default_value: .nostrBuild)
    103     var default_media_uploader: MediaUploader
    104     
    105     @Setting(key: "show_wallet_selector", default_value: false)
    106     var show_wallet_selector: Bool
    107     
    108     @Setting(key: "left_handed", default_value: false)
    109     var left_handed: Bool
    110     
    111     @Setting(key: "blur_images", default_value: true)
    112     var blur_images: Bool
    113     
    114     @Setting(key: "media_previews", default_value: true)
    115     var media_previews: Bool
    116     
    117     @Setting(key: "hide_nsfw_tagged_content", default_value: false)
    118     var hide_nsfw_tagged_content: Bool
    119     
    120     @Setting(key: "show_profile_action_sheet_on_pfp_click", default_value: true)
    121     var show_profile_action_sheet_on_pfp_click: Bool
    122 
    123     @Setting(key: "zap_vibration", default_value: true)
    124     var zap_vibration: Bool
    125     
    126     @Setting(key: "zap_notification", default_value: true)
    127     var zap_notification: Bool
    128     
    129     @Setting(key: "default_zap_amount", default_value: fallback_zap_amount)
    130     var default_zap_amount: Int
    131     
    132     @Setting(key: "mention_notification", default_value: true)
    133     var mention_notification: Bool
    134     
    135     @StringSetting(key: "zap_type", default_value: ZapType.pub)
    136     var default_zap_type: ZapType
    137 
    138     @Setting(key: "repost_notification", default_value: true)
    139     var repost_notification: Bool
    140 
    141     @Setting(key: "font_size", default_value: 1.0)
    142     var font_size: Double
    143 
    144     @Setting(key: "dm_notification", default_value: true)
    145     var dm_notification: Bool
    146     
    147     @Setting(key: "like_notification", default_value: true)
    148     var like_notification: Bool
    149     
    150     @Setting(key: "notification_only_from_following", default_value: false)
    151     var notification_only_from_following: Bool
    152     
    153     @Setting(key: "translate_dms", default_value: false)
    154     var translate_dms: Bool
    155     
    156     @Setting(key: "truncate_timeline_text", default_value: false)
    157     var truncate_timeline_text: Bool
    158     
    159     /// 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
    160     @Setting(key: "nozaps", default_value: true)
    161     var nozaps: Bool
    162     
    163     @Setting(key: "truncate_mention_text", default_value: true)
    164     var truncate_mention_text: Bool
    165     
    166     @Setting(key: "notification_indicators", default_value: NewEventsBits.all.rawValue)
    167     var notification_indicators: Int
    168     
    169     @Setting(key: "auto_translate", default_value: true)
    170     var auto_translate: Bool
    171 
    172     @Setting(key: "show_general_statuses", default_value: true)
    173     var show_general_statuses: Bool
    174 
    175     @Setting(key: "show_music_statuses", default_value: true)
    176     var show_music_statuses: Bool
    177 
    178     @Setting(key: "show_only_preferred_languages", default_value: false)
    179     var show_only_preferred_languages: Bool
    180     
    181     @Setting(key: "multiple_events_per_pubkey", default_value: false)
    182     var multiple_events_per_pubkey: Bool
    183 
    184     @Setting(key: "onlyzaps_mode", default_value: false)
    185     var onlyzaps_mode: Bool
    186     
    187     @Setting(key: "disable_animation", default_value: UIAccessibility.isReduceMotionEnabled)
    188     var disable_animation: Bool
    189     
    190     @Setting(key: "donation_percent", default_value: 0)
    191     var donation_percent: Int
    192     
    193     @Setting(key: "developer_mode", default_value: false)
    194     var developer_mode: Bool
    195     
    196     @Setting(key: "always_show_onboarding_suggestions", default_value: false)
    197     var always_show_onboarding_suggestions: Bool
    198 
    199     @Setting(key: "enable_experimental_push_notifications", default_value: false)
    200     var enable_experimental_push_notifications: Bool
    201     
    202     @Setting(key: "send_device_token_to_localhost", default_value: false)
    203     var send_device_token_to_localhost: Bool
    204     
    205     @Setting(key: "enable_experimental_purple_api", default_value: false)
    206     var enable_experimental_purple_api: Bool
    207     
    208     @StringSetting(key: "purple_environment", default_value: .production)
    209     var purple_enviroment: DamusPurpleEnvironment
    210 
    211     @Setting(key: "enable_experimental_purple_iap_support", default_value: false)
    212     var enable_experimental_purple_iap_support: Bool
    213     
    214     @Setting(key: "emoji_reactions", default_value: default_emoji_reactions)
    215     var emoji_reactions: [String]
    216     
    217     @Setting(key: "default_emoji_reaction", default_value: "🤙")
    218     var default_emoji_reaction: String
    219 
    220     // Helper for inverse of disable_animation.
    221     // disable_animation was introduced as a setting first, but it's more natural for the settings UI to show the inverse.
    222     var enable_animation: Bool {
    223         get {
    224             !disable_animation
    225         }
    226         set {
    227             disable_animation = !newValue
    228         }
    229     }
    230     
    231     @StringSetting(key: "friend_filter", default_value: .all)
    232     var friend_filter: FriendFilter
    233 
    234     @StringSetting(key: "translation_service", default_value: .none)
    235     var translation_service: TranslationService
    236 
    237     @StringSetting(key: "deepl_plan", default_value: .free)
    238     var deepl_plan: DeepLPlan
    239     
    240     var deepl_api_key: String {
    241         get {
    242             return internal_deepl_api_key ?? ""
    243         }
    244         set {
    245             internal_deepl_api_key = newValue == "" ? nil : newValue
    246         }
    247     }
    248 
    249     @StringSetting(key: "libretranslate_server", default_value: .custom)
    250     var libretranslate_server: LibreTranslateServer
    251     
    252     @Setting(key: "libretranslate_url", default_value: "")
    253     var libretranslate_url: String
    254 
    255     var libretranslate_api_key: String {
    256         get {
    257             return internal_libretranslate_api_key ?? ""
    258         }
    259         set {
    260             internal_libretranslate_api_key = newValue == "" ? nil : newValue
    261         }
    262     }
    263     
    264     var nokyctranslate_api_key: String {
    265         get {
    266             return internal_nokyctranslate_api_key ?? ""
    267         }
    268         set {
    269             internal_nokyctranslate_api_key = newValue == "" ? nil : newValue
    270         }
    271     }
    272 
    273     var winetranslate_api_key: String {
    274         get {
    275             return internal_winetranslate_api_key ?? ""
    276         }
    277         set {
    278             internal_winetranslate_api_key = newValue == "" ? nil : newValue
    279         }
    280     }
    281     
    282     // These internal keys are necessary because entries in the keychain need to be Optional,
    283     // but the translation view needs non-Optional String in order to use them as Bindings.
    284     @KeychainStorage(account: "deepl_apikey")
    285     var internal_deepl_api_key: String?
    286     
    287     @KeychainStorage(account: "nokyctranslate_apikey")
    288     var internal_nokyctranslate_api_key: String?
    289 
    290     @KeychainStorage(account: "winetranslate_apikey")
    291     var internal_winetranslate_api_key: String?
    292     
    293     @KeychainStorage(account: "libretranslate_apikey")
    294     var internal_libretranslate_api_key: String?
    295     
    296     @KeychainStorage(account: "nostr_wallet_connect")
    297     var nostr_wallet_connect: String? // TODO: strongly type this to WalletConnectURL
    298 
    299     var can_translate: Bool {
    300         switch translation_service {
    301         case .none:
    302             return false
    303         case .purple:
    304             return true
    305         case .libretranslate:
    306             return URLComponents(string: libretranslate_url) != nil
    307         case .deepl:
    308             return internal_deepl_api_key != nil
    309         case .nokyctranslate:
    310             return internal_nokyctranslate_api_key != nil
    311         case .winetranslate:
    312             return internal_winetranslate_api_key != nil
    313         }
    314     }
    315 }
    316 
    317 func pk_setting_key(_ pubkey: Pubkey, key: String) -> String {
    318     return "\(pubkey.hex())_\(key)"
    319 }