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 }