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 }