refactor: Adding structure
Huge refactor to add better structure to the project. Separating features with their associated view and model structure. This should be better organization and will allow us to improve the overall architecture in the future. I forsee many more improvements that can follow this change. e.g. MVVM Arch As well as cleaning up duplicate, unused, functionality. Many files have global functions that can also be moved or be renamed. damus/ ├── Features/ │ ├── <Feature>/ │ │ ├── Views/ │ │ └── Models/ ├── Shared/ │ ├── Components/ │ ├── Media/ │ ├── Buttons/ │ ├── Extensions/ │ ├── Empty Views/ │ ├── ErrorHandling/ │ ├── Modifiers/ │ └── Utilities/ ├── Core/ │ ├── Nostr/ │ ├── NIPs/ │ ├── DIPs/ │ ├── Types/ │ ├── Networking/ │ └── Storage/ Signed-off-by: ericholguin <ericholguin@apache.org>
This commit is contained in:
committed by
Daniel D’Aquino
parent
fdbf271432
commit
65a22813a3
401
damus/Features/Settings/Models/UserSettingsStore.swift
Normal file
401
damus/Features/Settings/Models/UserSettingsStore.swift
Normal file
@@ -0,0 +1,401 @@
|
||||
//
|
||||
// UserSettingsStore.swift
|
||||
// damus
|
||||
//
|
||||
// Created by Suhail Saqan on 12/29/22.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import UIKit
|
||||
|
||||
let fallback_zap_amount = 1000
|
||||
let default_emoji_reactions = ["🤣", "🤙", "⚡", "💜", "🔥", "😀", "😃", "😄", "🥶"]
|
||||
|
||||
func setting_property_key(key: String) -> String {
|
||||
return pk_setting_key(UserSettingsStore.pubkey ?? .empty, key: key)
|
||||
}
|
||||
|
||||
func setting_get_property_value<T>(key: String, scoped_key: String, default_value: T) -> T {
|
||||
if let loaded = DamusUserDefaults.standard.object(forKey: scoped_key) as? T {
|
||||
return loaded
|
||||
} else if let loaded = DamusUserDefaults.standard.object(forKey: key) as? T {
|
||||
// If pubkey-scoped setting does not exist but the deprecated non-pubkey-scoped setting does,
|
||||
// migrate the deprecated setting into the pubkey-scoped one and delete the deprecated one.
|
||||
DamusUserDefaults.standard.set(loaded, forKey: scoped_key)
|
||||
DamusUserDefaults.standard.removeObject(forKey: key)
|
||||
return loaded
|
||||
} else {
|
||||
return default_value
|
||||
}
|
||||
}
|
||||
|
||||
func setting_set_property_value<T: Equatable>(scoped_key: String, old_value: T, new_value: T) -> T? {
|
||||
guard old_value != new_value else { return nil }
|
||||
DamusUserDefaults.standard.set(new_value, forKey: scoped_key)
|
||||
UserSettingsStore.shared?.objectWillChange.send()
|
||||
return new_value
|
||||
}
|
||||
|
||||
@propertyWrapper struct Setting<T: Equatable> {
|
||||
private let key: String
|
||||
private var value: T
|
||||
|
||||
init(key: String, default_value: T) {
|
||||
if T.self == Bool.self {
|
||||
UserSettingsStore.bool_options.insert(key)
|
||||
}
|
||||
let scoped_key = setting_property_key(key: key)
|
||||
|
||||
self.value = setting_get_property_value(key: key, scoped_key: scoped_key, default_value: default_value)
|
||||
self.key = scoped_key
|
||||
}
|
||||
|
||||
var wrappedValue: T {
|
||||
get { return value }
|
||||
set {
|
||||
guard let new_val = setting_set_property_value(scoped_key: key, old_value: value, new_value: newValue) else { return }
|
||||
self.value = new_val
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@propertyWrapper class StringSetting<T: StringCodable & Equatable> {
|
||||
private let key: String
|
||||
private var value: T
|
||||
|
||||
init(key: String, default_value: T) {
|
||||
self.key = pk_setting_key(UserSettingsStore.pubkey ?? .empty, key: key)
|
||||
if let loaded = DamusUserDefaults.standard.string(forKey: self.key), let val = T.init(from: loaded) {
|
||||
self.value = val
|
||||
} else if let loaded = DamusUserDefaults.standard.string(forKey: key), let val = T.init(from: loaded) {
|
||||
// If pubkey-scoped setting does not exist but the deprecated non-pubkey-scoped setting does,
|
||||
// migrate the deprecated setting into the pubkey-scoped one and delete the deprecated one.
|
||||
self.value = val
|
||||
DamusUserDefaults.standard.set(val.to_string(), forKey: self.key)
|
||||
DamusUserDefaults.standard.removeObject(forKey: key)
|
||||
} else {
|
||||
self.value = default_value
|
||||
}
|
||||
}
|
||||
|
||||
var wrappedValue: T {
|
||||
get { return value }
|
||||
set {
|
||||
guard self.value != newValue else {
|
||||
return
|
||||
}
|
||||
self.value = newValue
|
||||
DamusUserDefaults.standard.set(newValue.to_string(), forKey: key)
|
||||
UserSettingsStore.shared!.objectWillChange.send()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class UserSettingsStore: ObservableObject {
|
||||
static var pubkey: Pubkey? = nil
|
||||
static var shared: UserSettingsStore? = nil
|
||||
static var bool_options = Set<String>()
|
||||
|
||||
static func globally_load_for(pubkey: Pubkey) -> UserSettingsStore {
|
||||
// dumb stuff needed for property wrappers
|
||||
UserSettingsStore.pubkey = pubkey
|
||||
let settings = UserSettingsStore()
|
||||
UserSettingsStore.shared = settings
|
||||
return settings
|
||||
}
|
||||
|
||||
@StringSetting(key: "default_wallet", default_value: .system_default_wallet)
|
||||
var default_wallet: Wallet
|
||||
|
||||
@StringSetting(key: "default_media_uploader", default_value: .nostrBuild)
|
||||
var default_media_uploader: MediaUploader
|
||||
|
||||
@Setting(key: "show_wallet_selector", default_value: false)
|
||||
var show_wallet_selector: Bool
|
||||
|
||||
@Setting(key: "dismiss_wallet_high_balance_warning", default_value: false)
|
||||
var dismiss_wallet_high_balance_warning: Bool
|
||||
|
||||
@Setting(key: "hide_wallet_balance", default_value: false)
|
||||
var hide_wallet_balance: Bool
|
||||
|
||||
@Setting(key: "left_handed", default_value: false)
|
||||
var left_handed: Bool
|
||||
|
||||
@Setting(key: "blur_images", default_value: true)
|
||||
var blur_images: Bool
|
||||
|
||||
@Setting(key: "media_previews", default_value: true)
|
||||
var media_previews: Bool
|
||||
|
||||
@Setting(key: "show_trusted_replies_first", default_value: true)
|
||||
var show_trusted_replies_first: Bool
|
||||
|
||||
@Setting(key: "reset_tips_on_launch", default_value: false)
|
||||
var reset_tips_on_launch: Bool
|
||||
|
||||
@Setting(key: "hide_nsfw_tagged_content", default_value: false)
|
||||
var hide_nsfw_tagged_content: Bool
|
||||
|
||||
@Setting(key: "reduce_bitcoin_content", default_value: false)
|
||||
var reduce_bitcoin_content: Bool
|
||||
|
||||
@Setting(key: "show_profile_action_sheet_on_pfp_click", default_value: true)
|
||||
var show_profile_action_sheet_on_pfp_click: Bool
|
||||
|
||||
@Setting(key: "zap_vibration", default_value: true)
|
||||
var zap_vibration: Bool
|
||||
|
||||
@Setting(key: "zap_notification", default_value: true)
|
||||
var zap_notification: Bool
|
||||
|
||||
@Setting(key: "default_zap_amount", default_value: fallback_zap_amount)
|
||||
var default_zap_amount: Int
|
||||
|
||||
@Setting(key: "mention_notification", default_value: true)
|
||||
var mention_notification: Bool
|
||||
|
||||
@StringSetting(key: "zap_type", default_value: ZapType.pub)
|
||||
var default_zap_type: ZapType
|
||||
|
||||
@Setting(key: "repost_notification", default_value: true)
|
||||
var repost_notification: Bool
|
||||
|
||||
@Setting(key: "font_size", default_value: 1.0)
|
||||
var font_size: Double
|
||||
|
||||
@Setting(key: "dm_notification", default_value: true)
|
||||
var dm_notification: Bool
|
||||
|
||||
@Setting(key: "like_notification", default_value: true)
|
||||
var like_notification: Bool
|
||||
|
||||
@StringSetting(key: "notification_mode", default_value: .push)
|
||||
var notification_mode: NotificationsMode
|
||||
|
||||
@Setting(key: "notification_only_from_following", default_value: false)
|
||||
var notification_only_from_following: Bool
|
||||
|
||||
@Setting(key: "hellthread_notifications_disabled", default_value: false)
|
||||
var hellthread_notifications_disabled: Bool
|
||||
|
||||
@Setting(key: "hellthread_notification_max_pubkeys", default_value: DEFAULT_HELLTHREAD_MAX_PUBKEYS)
|
||||
var hellthread_notification_max_pubkeys: Int
|
||||
|
||||
@Setting(key: "translate_dms", default_value: false)
|
||||
var translate_dms: Bool
|
||||
|
||||
@Setting(key: "truncate_timeline_text", default_value: false)
|
||||
var truncate_timeline_text: Bool
|
||||
|
||||
/// 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
|
||||
///
|
||||
/// Update 2025-05-12: This can be re-enabled 🥳. See https://github.com/damus-io/damus/issues/3016
|
||||
// @Setting(key: "nozaps", default_value: true)
|
||||
var nozaps: Bool {
|
||||
return false
|
||||
}
|
||||
|
||||
@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
|
||||
|
||||
@Setting(key: "show_general_statuses", default_value: true)
|
||||
var show_general_statuses: Bool
|
||||
|
||||
@Setting(key: "show_music_statuses", default_value: true)
|
||||
var show_music_statuses: Bool
|
||||
|
||||
@Setting(key: "multiple_events_per_pubkey", default_value: false)
|
||||
var multiple_events_per_pubkey: Bool
|
||||
|
||||
@Setting(key: "onlyzaps_mode", default_value: false)
|
||||
var onlyzaps_mode: Bool
|
||||
|
||||
@Setting(key: "disable_animation", default_value: UIAccessibility.isReduceMotionEnabled)
|
||||
var disable_animation: Bool
|
||||
|
||||
@Setting(key: "donation_percent", default_value: 0)
|
||||
var donation_percent: Int
|
||||
|
||||
@Setting(key: "developer_mode", default_value: false)
|
||||
var developer_mode: Bool
|
||||
|
||||
/// Makes all post content gibberish and blurhashes images, to avoid distractions when developers are working.
|
||||
@Setting(key: "undistract_mode", default_value: false)
|
||||
var undistractMode: Bool
|
||||
|
||||
@Setting(key: "always_show_onboarding_suggestions", default_value: false)
|
||||
var always_show_onboarding_suggestions: Bool
|
||||
|
||||
// @Setting(key: "enable_experimental_push_notifications", default_value: false)
|
||||
// This was a feature flag setting during early development, but now this is enabled for everyone.
|
||||
var enable_push_notifications: Bool = true
|
||||
|
||||
@StringSetting(key: "push_notification_environment", default_value: .production)
|
||||
var push_notification_environment: PushNotificationClient.Environment
|
||||
|
||||
@Setting(key: "enable_experimental_purple_api", default_value: false)
|
||||
var enable_experimental_purple_api: Bool
|
||||
|
||||
@StringSetting(key: "purple_environment", default_value: .production)
|
||||
var purple_enviroment: DamusPurpleEnvironment
|
||||
|
||||
@Setting(key: "enable_experimental_purple_iap_support", default_value: false)
|
||||
var enable_experimental_purple_iap_support: Bool
|
||||
|
||||
@Setting(key: "emoji_reactions", default_value: default_emoji_reactions)
|
||||
var emoji_reactions: [String]
|
||||
|
||||
@Setting(key: "default_emoji_reaction", default_value: "🤙")
|
||||
var default_emoji_reaction: String
|
||||
|
||||
// Helper for inverse of disable_animation.
|
||||
// disable_animation was introduced as a setting first, but it's more natural for the settings UI to show the inverse.
|
||||
var enable_animation: Bool {
|
||||
get {
|
||||
!disable_animation
|
||||
}
|
||||
set {
|
||||
disable_animation = !newValue
|
||||
}
|
||||
}
|
||||
|
||||
@StringSetting(key: "friend_filter", default_value: .all)
|
||||
var friend_filter: FriendFilter
|
||||
|
||||
@StringSetting(key: "translation_service", default_value: .none)
|
||||
var translation_service: TranslationService
|
||||
|
||||
@StringSetting(key: "deepl_plan", default_value: .free)
|
||||
var deepl_plan: DeepLPlan
|
||||
|
||||
var deepl_api_key: String {
|
||||
get {
|
||||
return internal_deepl_api_key ?? ""
|
||||
}
|
||||
set {
|
||||
internal_deepl_api_key = newValue == "" ? nil : newValue
|
||||
}
|
||||
}
|
||||
|
||||
@StringSetting(key: "libretranslate_server", default_value: .custom)
|
||||
var libretranslate_server: LibreTranslateServer
|
||||
|
||||
@Setting(key: "libretranslate_url", default_value: "")
|
||||
var libretranslate_url: String
|
||||
|
||||
var libretranslate_api_key: String {
|
||||
get {
|
||||
return internal_libretranslate_api_key ?? ""
|
||||
}
|
||||
set {
|
||||
internal_libretranslate_api_key = newValue == "" ? nil : newValue
|
||||
}
|
||||
}
|
||||
|
||||
var nokyctranslate_api_key: String {
|
||||
get {
|
||||
return internal_nokyctranslate_api_key ?? ""
|
||||
}
|
||||
set {
|
||||
internal_nokyctranslate_api_key = newValue == "" ? nil : newValue
|
||||
}
|
||||
}
|
||||
|
||||
var winetranslate_api_key: String {
|
||||
get {
|
||||
return internal_winetranslate_api_key ?? ""
|
||||
}
|
||||
set {
|
||||
internal_winetranslate_api_key = newValue == "" ? nil : newValue
|
||||
}
|
||||
}
|
||||
|
||||
// These internal keys are necessary because entries in the keychain need to be Optional,
|
||||
// but the translation view needs non-Optional String in order to use them as Bindings.
|
||||
@KeychainStorage(account: "deepl_apikey")
|
||||
var internal_deepl_api_key: String?
|
||||
|
||||
@KeychainStorage(account: "nokyctranslate_apikey")
|
||||
var internal_nokyctranslate_api_key: String?
|
||||
|
||||
@KeychainStorage(account: "winetranslate_apikey")
|
||||
var internal_winetranslate_api_key: String?
|
||||
|
||||
@KeychainStorage(account: "libretranslate_apikey")
|
||||
var internal_libretranslate_api_key: String?
|
||||
|
||||
@KeychainStorage(account: "nostr_wallet_connect")
|
||||
var nostr_wallet_connect: String? // TODO: strongly type this to WalletConnectURL
|
||||
|
||||
var can_translate: Bool {
|
||||
switch translation_service {
|
||||
case .none:
|
||||
return false
|
||||
case .purple:
|
||||
return true
|
||||
case .libretranslate:
|
||||
return URLComponents(string: libretranslate_url) != nil
|
||||
case .deepl:
|
||||
return internal_deepl_api_key != nil
|
||||
case .nokyctranslate:
|
||||
return internal_nokyctranslate_api_key != nil
|
||||
case .winetranslate:
|
||||
return internal_winetranslate_api_key != nil
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: Internal, hidden settings
|
||||
|
||||
// TODO: Get rid of this once we have NostrDB query capabilities integrated
|
||||
@Setting(key: "latest_contact_event_id", default_value: nil)
|
||||
var latest_contact_event_id_hex: String?
|
||||
|
||||
// TODO: Get rid of this once we have NostrDB query capabilities integrated
|
||||
@Setting(key: "draft_event_ids", default_value: nil)
|
||||
var draft_event_ids: [String]?
|
||||
|
||||
// TODO: Get rid of this once we have NostrDB query capabilities integrated
|
||||
@Setting(key: "latest_relay_list_event_id", default_value: nil)
|
||||
var latestRelayListEventIdHex: String?
|
||||
|
||||
// MARK: Helper types
|
||||
|
||||
enum NotificationsMode: String, CaseIterable, Identifiable, StringCodable, Equatable {
|
||||
var id: String { self.rawValue }
|
||||
|
||||
func to_string() -> String {
|
||||
return rawValue
|
||||
}
|
||||
|
||||
init?(from string: String) {
|
||||
guard let notifications_mode = NotificationsMode(rawValue: string) else {
|
||||
return nil
|
||||
}
|
||||
self = notifications_mode
|
||||
}
|
||||
|
||||
func text_description() -> String {
|
||||
switch self {
|
||||
case .local:
|
||||
NSLocalizedString("Local", comment: "Option for notification mode setting: Local notification mode")
|
||||
case .push:
|
||||
NSLocalizedString("Push", comment: "Option for notification mode setting: Push notification mode")
|
||||
}
|
||||
}
|
||||
|
||||
case local
|
||||
case push
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func pk_setting_key(_ pubkey: Pubkey, key: String) -> String {
|
||||
return "\(pubkey.hex())_\(key)"
|
||||
}
|
||||
Reference in New Issue
Block a user