damus

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

commit 9c8391b33b98ddfd87b3b289fb78a014518d9bb5
parent 89b2382ad765c4a2c522f08aba9b85c8536d75b9
Author: William Casarin <jb55@jb55.com>
Date:   Wed,  5 Apr 2023 10:23:07 -0700

Refactor settings into subsections

Diffstat:
Mdamus.xcodeproj/project.pbxproj | 20++++++++++++++++++++
Adamus/Components/IconLabel.swift | 44++++++++++++++++++++++++++++++++++++++++++++
Mdamus/Models/UserSettingsStore.swift | 7+++++++
Mdamus/Views/ConfigView.swift | 286+++++--------------------------------------------------------------------------
Adamus/Views/Settings/AppearanceSettingsView.swift | 65+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Adamus/Views/Settings/KeySettingsView.swift | 133+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mdamus/Views/Settings/NotificationSettingsView.swift | 6++++++
Adamus/Views/Settings/TranslationSettingsView.swift | 160+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Adamus/Views/Settings/ZapSettingsView.swift | 66++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
9 files changed, 516 insertions(+), 271 deletions(-)

diff --git a/damus.xcodeproj/project.pbxproj b/damus.xcodeproj/project.pbxproj @@ -39,6 +39,11 @@ 4C0A3F93280F66F5000448DE /* ReplyMap.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C0A3F92280F66F5000448DE /* ReplyMap.swift */; }; 4C1A9A1A29DCA17E00516EAC /* ReplyCounter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C1A9A1929DCA17E00516EAC /* ReplyCounter.swift */; }; 4C1A9A1D29DDCF9B00516EAC /* NotificationSettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C1A9A1C29DDCF9B00516EAC /* NotificationSettingsView.swift */; }; + 4C1A9A1F29DDD24B00516EAC /* AppearanceSettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C1A9A1E29DDD24B00516EAC /* AppearanceSettingsView.swift */; }; + 4C1A9A2129DDD3E100516EAC /* KeySettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C1A9A2029DDD3E100516EAC /* KeySettingsView.swift */; }; + 4C1A9A2329DDDB8100516EAC /* IconLabel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C1A9A2229DDDB8100516EAC /* IconLabel.swift */; }; + 4C1A9A2529DDDF2600516EAC /* ZapSettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C1A9A2429DDDF2600516EAC /* ZapSettingsView.swift */; }; + 4C1A9A2729DDE31900516EAC /* TranslationSettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C1A9A2629DDE31900516EAC /* TranslationSettingsView.swift */; }; 4C216F32286E388800040376 /* DMChatView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C216F31286E388800040376 /* DMChatView.swift */; }; 4C216F34286F5ACD00040376 /* DMView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C216F33286F5ACD00040376 /* DMView.swift */; }; 4C216F362870A9A700040376 /* InputDismissKeyboard.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C216F352870A9A700040376 /* InputDismissKeyboard.swift */; }; @@ -403,6 +408,11 @@ 4C0A3F92280F66F5000448DE /* ReplyMap.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReplyMap.swift; sourceTree = "<group>"; }; 4C1A9A1929DCA17E00516EAC /* ReplyCounter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReplyCounter.swift; sourceTree = "<group>"; }; 4C1A9A1C29DDCF9B00516EAC /* NotificationSettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationSettingsView.swift; sourceTree = "<group>"; }; + 4C1A9A1E29DDD24B00516EAC /* AppearanceSettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppearanceSettingsView.swift; sourceTree = "<group>"; }; + 4C1A9A2029DDD3E100516EAC /* KeySettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeySettingsView.swift; sourceTree = "<group>"; }; + 4C1A9A2229DDDB8100516EAC /* IconLabel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IconLabel.swift; sourceTree = "<group>"; }; + 4C1A9A2429DDDF2600516EAC /* ZapSettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ZapSettingsView.swift; sourceTree = "<group>"; }; + 4C1A9A2629DDE31900516EAC /* TranslationSettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TranslationSettingsView.swift; sourceTree = "<group>"; }; 4C216F31286E388800040376 /* DMChatView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DMChatView.swift; sourceTree = "<group>"; }; 4C216F33286F5ACD00040376 /* DMView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DMView.swift; sourceTree = "<group>"; }; 4C216F352870A9A700040376 /* InputDismissKeyboard.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InputDismissKeyboard.swift; sourceTree = "<group>"; }; @@ -819,6 +829,10 @@ isa = PBXGroup; children = ( 4C1A9A1C29DDCF9B00516EAC /* NotificationSettingsView.swift */, + 4C1A9A1E29DDD24B00516EAC /* AppearanceSettingsView.swift */, + 4C1A9A2029DDD3E100516EAC /* KeySettingsView.swift */, + 4C1A9A2429DDDF2600516EAC /* ZapSettingsView.swift */, + 4C1A9A2629DDE31900516EAC /* TranslationSettingsView.swift */, ); path = Settings; sourceTree = "<group>"; @@ -1087,6 +1101,7 @@ 7CFF6316299FEFE5005D382A /* SelectableText.swift */, 4C8EC52429D1FA6C0085D9A8 /* DamusColors.swift */, 4CE4F0F729DB7399005914DB /* ThiccDivider.swift */, + 4C1A9A2229DDDB8100516EAC /* IconLabel.swift */, ); path = Components; sourceTree = "<group>"; @@ -1547,6 +1562,7 @@ 4CC7AAF0297F11C700430951 /* SelectedEventView.swift in Sources */, 4CC7AAF8297F1CEE00430951 /* EventProfile.swift in Sources */, 64FBD06F296255C400D9D3B2 /* Theme.swift in Sources */, + 4C1A9A2329DDDB8100516EAC /* IconLabel.swift in Sources */, 4C3EA64928FF597700C48A62 /* bech32.c in Sources */, 4CE879522996B68900F758CC /* RelayType.swift in Sources */, 4C90BD162839DB54008EE7EF /* NostrMetadata.swift in Sources */, @@ -1596,6 +1612,7 @@ 4C8682872814DE470026224F /* ProfileView.swift in Sources */, 4C5F9114283D694D0052CD1C /* FollowTarget.swift in Sources */, 4CF0ABD629817F5B00D66079 /* ReportView.swift in Sources */, + 4C1A9A2729DDE31900516EAC /* TranslationSettingsView.swift in Sources */, 4CB8838629656C8B00DC99E7 /* NIP05.swift in Sources */, 4CF0ABD82981980C00D66079 /* Lists.swift in Sources */, 4C30AC8029A6A53F00E2BD5A /* ProfilePicturesView.swift in Sources */, @@ -1649,6 +1666,7 @@ 4CE879552996BAB900F758CC /* RelayPaidDetail.swift in Sources */, 4CF0ABD42980996B00D66079 /* Report.swift in Sources */, 4C06670B28FDE64700038D2A /* damus.c in Sources */, + 4C1A9A2529DDDF2600516EAC /* ZapSettingsView.swift in Sources */, 4C2CDDF7299D4A5E00879FD5 /* Debouncer.swift in Sources */, 3AAA95CC298E07E900F3D526 /* DeepLPlan.swift in Sources */, 4FE60CDD295E1C5E00105A1F /* Wallet.swift in Sources */, @@ -1659,6 +1677,7 @@ 4CC7AAF4297F18B400430951 /* ReplyDescription.swift in Sources */, 4C75EFA427FA577B0006080F /* PostView.swift in Sources */, 4C30AC7229A5677A00E2BD5A /* NotificationsView.swift in Sources */, + 4C1A9A2129DDD3E100516EAC /* KeySettingsView.swift in Sources */, 4C1A9A1D29DDCF9B00516EAC /* NotificationSettingsView.swift in Sources */, 4C75EFB528049D790006080F /* Relay.swift in Sources */, 4CEE2AF1280B216B00AB5EEF /* EventDetailView.swift in Sources */, @@ -1666,6 +1685,7 @@ 4C75EFBB2804A34C0006080F /* ProofOfWork.swift in Sources */, 4C3AC7A52836987600E1F516 /* MainTabView.swift in Sources */, 7C0F392F29B57CAF0039859C /* Binding+.swift in Sources */, + 4C1A9A1F29DDD24B00516EAC /* AppearanceSettingsView.swift in Sources */, 3AA59D1D2999B0400061C48E /* DraftsModel.swift in Sources */, 3169CAED294FCCFC00EE4006 /* Constants.swift in Sources */, 4CB9D4A72992D02B00A9A7E4 /* ProfileNameView.swift in Sources */, diff --git a/damus/Components/IconLabel.swift b/damus/Components/IconLabel.swift @@ -0,0 +1,44 @@ +// +// IconLabel.swift +// damus +// +// Created by William Casarin on 2023-04-05. +// + +import SwiftUI +import UIKit + +struct IconLabel: View { + let text: String + let img_name: String + let img_color: Color + + init(_ text: String, img_name: String, color: Color) { + self.text = text + self.img_name = img_name + self.img_color = color + } + + var body: some View { + HStack(spacing: 0) { + Image(systemName: img_name) + .foregroundColor(img_color) + .frame(width: 20) + .padding([.trailing], 20) + Text(text) + } + }} + +struct IconLabel_Previews: PreviewProvider { + static var previews: some View { + Form { + Section { + IconLabel(NSLocalizedString("Keys", comment: "Settings section for managing keys"), img_name: "key.fill", color: .orange) + + IconLabel(NSLocalizedString("Local Notifications", comment: "Section header for damus local notifications user configuration"), img_name: "bell.fill", color: .blue) + + IconLabel(NSLocalizedString("Appearance", comment: "Section header for text and appearance settings"), img_name: "textformat", color: .red) + } + } + } +} diff --git a/damus/Models/UserSettingsStore.swift b/damus/Models/UserSettingsStore.swift @@ -169,6 +169,12 @@ class UserSettingsStore: ObservableObject { UserDefaults.standard.set(truncate_timeline_text, forKey: "truncate_timeline_text") } } + + @Published var truncate_mention_text: Bool { + didSet { + UserDefaults.standard.set(truncate_mention_text, forKey: "truncate_mention_text") + } + } @Published var auto_translate: Bool { didSet { @@ -270,6 +276,7 @@ class UserSettingsStore: ObservableObject { dm_notification = UserDefaults.standard.object(forKey: "dm_notification") as? Bool ?? true notification_only_from_following = UserDefaults.standard.object(forKey: "notification_only_from_following") as? Bool ?? false truncate_timeline_text = UserDefaults.standard.object(forKey: "truncate_timeline_text") as? Bool ?? false + truncate_mention_text = UserDefaults.standard.object(forKey: "truncate_mention_text") as? Bool ?? true disable_animation = should_disable_image_animation() auto_translate = UserDefaults.standard.object(forKey: "auto_translate") as? Bool ?? true show_only_preferred_languages = UserDefaults.standard.object(forKey: "show_only_preferred_languages") as? Bool ?? false diff --git a/damus/Views/ConfigView.swift b/damus/Views/ConfigView.swift @@ -17,26 +17,14 @@ struct ConfigView: View { @State var confirm_logout: Bool = false @State var delete_account_warning: Bool = false @State var confirm_delete_account: Bool = false - @State var show_privkey: Bool = false - @State var has_authenticated_locally: Bool = false - @State var show_api_key: Bool = false - @State var privkey: String - @State var privkey_copied: Bool = false - @State var pubkey_copied: Bool = false @State var delete_text: String = "" - @State var default_zap_amount: String @ObservedObject var settings: UserSettingsStore - - let generator = UIImpactFeedbackGenerator(style: .light) private let DELETE_KEYWORD = "DELETE" init(state: DamusState) { self.state = state - let zap_amt = get_default_zap_amount(pubkey: state.pubkey).map({ "\($0)" }) ?? "1000" - _default_zap_amount = State(initialValue: zap_amt) - _privkey = State(initialValue: self.state.keypair.privkey_bech32 ?? "") _settings = ObservedObject(initialValue: state.settings) } @@ -44,202 +32,32 @@ struct ConfigView: View { colorScheme == .light ? DamusColors.black : DamusColors.white } - func authenticateLocally(completion: @escaping (Bool) -> Void) { - // Need to authenticate only once while ConfigView is presented - guard !has_authenticated_locally else { - completion(true) - return - } - let context = LAContext() - if context.canEvaluatePolicy(.deviceOwnerAuthentication, error: nil) { - context.evaluatePolicy(.deviceOwnerAuthentication, localizedReason: NSLocalizedString("Local authentication to access private key", comment: "Face ID usage description shown when trying to access private key")) { success, error in - DispatchQueue.main.async { - has_authenticated_locally = success - completion(success) - } - } - } else { - // If there's no authentication set up on the device, let the user copy the key without it - has_authenticated_locally = true - completion(true) - } - } - - // TODO: (jb55) could be more general but not gonna worry about it atm - func CopyButton(is_pk: Bool) -> some View { - return Button(action: { - let copyKey = { - UIPasteboard.general.string = is_pk ? self.state.keypair.pubkey_bech32 : self.privkey - self.privkey_copied = !is_pk - self.pubkey_copied = is_pk - generator.impactOccurred() - } - if is_pk { - // When trying to copy npub - copyKey() - } else { - // When trying to copy nsec - if has_authenticated_locally { - copyKey() - } else { - authenticateLocally { success in - if success { - copyKey() - } - } - } - } - }) { - let copied = is_pk ? self.pubkey_copied : self.privkey_copied - Image(systemName: copied ? "checkmark.circle" : "doc.on.doc") - } - } - var body: some View { ZStack(alignment: .leading) { Form { - Section(NSLocalizedString("Public Account ID", comment: "Section title for the user's public account ID.")) { - HStack { - Text(state.keypair.pubkey_bech32) - - CopyButton(is_pk: true) - } - .clipShape(RoundedRectangle(cornerRadius: 5)) - } - - if let sec = state.keypair.privkey_bech32 { - Section(NSLocalizedString("Secret Account Login Key", comment: "Section title for user's secret account login key.")) { - HStack { - if show_privkey == false || !has_authenticated_locally { - SecureField(NSLocalizedString("Private Key", comment: "Title of the secure field that holds the user's private key."), text: $privkey) - .disabled(true) - } else { - Text(sec) - .clipShape(RoundedRectangle(cornerRadius: 5)) - } - - CopyButton(is_pk: false) - } - - Toggle(NSLocalizedString("Show", comment: "Toggle to show or hide user's secret account login key."), isOn: $show_privkey) - .onChange(of: show_privkey) { newValue in - if newValue { - authenticateLocally { success in - show_privkey = success - } - } - } - } - } - - Section(NSLocalizedString("Wallet and others", comment: "Section header for miscellaneous user configuration")) { - Toggle(NSLocalizedString("Show wallet selector", comment: "Toggle to show or hide selection of wallet."), isOn: $settings.show_wallet_selector).toggleStyle(.switch) - Picker(NSLocalizedString("Select default wallet", comment: "Prompt selection of user's default wallet"), - selection: $settings.default_wallet) { - ForEach(Wallet.allCases, id: \.self) { wallet in - Text(wallet.model.displayName) - .tag(wallet.model.tag) - } - } - Toggle(NSLocalizedString("Left Handed", comment: "Moves the post button to the left side of the screen"), isOn: $settings.left_handed) - .toggleStyle(.switch) - Toggle(NSLocalizedString("Zap Vibration", comment: "Setting to enable vibration on zap"), isOn: $settings.zap_vibration) - .toggleStyle(.switch) - } - - NavigationLink(destination: NotificationSettingsView(settings: settings)) { - Section(NSLocalizedString("Local Notifications", comment: "Section header for damus local notifications user configuration")) { - } - } - - Section(NSLocalizedString("Default Zap Amount in sats", comment: "Section title for zap configuration")) { - TextField(String("1000"), text: $default_zap_amount) - .keyboardType(.numberPad) - .onReceive(Just(default_zap_amount)) { newValue in - if let parsed = handle_string_amount(new_value: newValue) { - self.default_zap_amount = String(parsed) - set_default_zap_amount(pubkey: self.state.pubkey, amount: parsed) - } - } - } - - Section(NSLocalizedString("Translations", comment: "Section title for selecting the translation service.")) { - Toggle(NSLocalizedString("Show only preferred languages on Universe feed", comment: "Toggle to show notes that are only in the device's preferred languages on the Universe feed and hide notes that are in other languages."), isOn: $settings.show_only_preferred_languages) - .toggleStyle(.switch) - - Picker(NSLocalizedString("Service", comment: "Prompt selection of translation service provider."), selection: $settings.translation_service) { - ForEach(TranslationService.allCases, id: \.self) { server in - Text(server.model.displayName) - .tag(server.model.tag) - } - } - - if settings.translation_service == .libretranslate { - Picker(NSLocalizedString("Server", comment: "Prompt selection of LibreTranslate server to perform machine translations on notes"), selection: $settings.libretranslate_server) { - ForEach(LibreTranslateServer.allCases, id: \.self) { server in - Text(server.model.displayName) - .tag(server.model.tag) - } - } - - if settings.libretranslate_server == .custom { - TextField(NSLocalizedString("URL", comment: "Example URL to LibreTranslate server"), text: $settings.libretranslate_url) - .disableAutocorrection(true) - .autocapitalization(UITextAutocapitalizationType.none) - } - - SecureField(NSLocalizedString("API Key (optional)", comment: "Prompt for optional entry of API Key to use translation server."), text: $settings.libretranslate_api_key) - .disableAutocorrection(true) - .disabled(settings.translation_service != .libretranslate) - .autocapitalization(UITextAutocapitalizationType.none) + Section { + NavigationLink(destination: KeySettingsView(keypair: state.keypair)) { + IconLabel(NSLocalizedString("Keys", comment: "Settings section for managing keys"), img_name: "key.fill", color: .purple) } - - if settings.translation_service == .deepl { - Picker(NSLocalizedString("Plan", comment: "Prompt selection of DeepL subscription plan to perform machine translations on notes"), selection: $settings.deepl_plan) { - ForEach(DeepLPlan.allCases, id: \.self) { server in - Text(server.model.displayName) - .tag(server.model.tag) - } - } - - SecureField(NSLocalizedString("API Key (required)", comment: "Prompt for required entry of API Key to use translation server."), text: $settings.deepl_api_key) - .disableAutocorrection(true) - .disabled(settings.translation_service != .deepl) - .autocapitalization(UITextAutocapitalizationType.none) - - if settings.deepl_api_key == "" { - Link(NSLocalizedString("Get API Key", comment: "Button to navigate to DeepL website to get a translation API key."), destination: URL(string: "https://www.deepl.com/pro-api")!) - } + + NavigationLink(destination: AppearanceSettingsView(settings: settings)) { + IconLabel(NSLocalizedString("Appearance", comment: "Section header for text and appearance settings"), img_name: "textformat", color: .red) } - - if settings.translation_service != .none { - Toggle(NSLocalizedString("Automatically translate notes", comment: "Toggle to automatically translate notes."), isOn: $settings.auto_translate) - .toggleStyle(.switch) + + NavigationLink(destination: NotificationSettingsView(settings: settings)) { + IconLabel(NSLocalizedString("Local Notifications", comment: "Section header for damus local notifications user configuration"), img_name: "bell.fill", color: .blue) } - } - - Section(NSLocalizedString("Images", comment: "Section title for images configuration.")) { - Toggle(NSLocalizedString("Disable animations", comment: "Button to disable image animation"), isOn: $settings.disable_animation) - .toggleStyle(.switch) - .onChange(of: settings.disable_animation) { _ in - clear_kingfisher_cache() - } - Toggle(NSLocalizedString("Always show images", comment: "Setting to always show and never blur images"), isOn: $settings.always_show_images) - .toggleStyle(.switch) - - Button(NSLocalizedString("Clear Cache", comment: "Button to clear image cache.")) { - clear_kingfisher_cache() + + NavigationLink(destination: ZapSettingsView(pubkey: state.pubkey, settings: settings)) { + IconLabel(NSLocalizedString("Zaps", comment: "Section header for zap settings"), img_name: "bolt.fill", color: .orange) } - Picker(NSLocalizedString("Select image uploader", comment: "Prompt selection of user's image uploader"), - selection: $settings.default_media_uploader) { - ForEach(MediaUploader.allCases, id: \.self) { uploader in - Text(uploader.model.displayName) - .tag(uploader.model.tag) - } + NavigationLink(destination: TranslationSettingsView(settings: settings)) { + IconLabel(NSLocalizedString("Translation", comment: "Section header for text and appearance settings"), img_name: "globe.americas.fill", color: .green) } } + Section(NSLocalizedString("Sign Out", comment: "Section title for signing out")) { Button(action: { if state.keypair.privkey == nil { @@ -314,80 +132,6 @@ struct ConfigView: View { } } - var libretranslate_view: some View { - VStack { - Picker(NSLocalizedString("Server", comment: "Prompt selection of LibreTranslate server to perform machine translations on notes"), selection: $settings.libretranslate_server) { - ForEach(LibreTranslateServer.allCases, id: \.self) { server in - Text(server.model.displayName) - .tag(server.model.tag) - } - } - - TextField(NSLocalizedString("URL", comment: "Example URL to LibreTranslate server"), text: $settings.libretranslate_url) - .disableAutocorrection(true) - .disabled(settings.libretranslate_server != .custom) - .autocapitalization(UITextAutocapitalizationType.none) - HStack { - let libretranslate_api_key_placeholder = NSLocalizedString("API Key (optional)", comment: "Prompt for optional entry of API Key to use translation server.") - if show_api_key { - TextField(libretranslate_api_key_placeholder, text: $settings.libretranslate_api_key) - .disableAutocorrection(true) - .autocapitalization(UITextAutocapitalizationType.none) - if settings.libretranslate_api_key != "" { - Button(NSLocalizedString("Hide API Key", comment: "Button to hide the LibreTranslate server API key.")) { - show_api_key = false - } - } - } else { - SecureField(libretranslate_api_key_placeholder, text: $settings.libretranslate_api_key) - .disableAutocorrection(true) - .autocapitalization(UITextAutocapitalizationType.none) - if settings.libretranslate_api_key != "" { - Button(NSLocalizedString("Show API Key", comment: "Button to show the LibreTranslate server API key.")) { - show_api_key = true - } - } - } - } - } - } - - var deepl_view: some View { - VStack { - Picker(NSLocalizedString("Plan", comment: "Prompt selection of DeepL subscription plan to perform machine translations on notes"), selection: $settings.deepl_plan) { - ForEach(DeepLPlan.allCases, id: \.self) { server in - Text(server.model.displayName) - .tag(server.model.tag) - } - } - - HStack { - let deepl_api_key_placeholder = NSLocalizedString("API Key (required)", comment: "Prompt for required entry of API Key to use translation server.") - if show_api_key { - TextField(deepl_api_key_placeholder, text: $settings.deepl_api_key) - .disableAutocorrection(true) - .autocapitalization(UITextAutocapitalizationType.none) - if settings.deepl_api_key != "" { - Button(NSLocalizedString("Hide API Key", comment: "Button to hide the DeepL translation API key.")) { - show_api_key = false - } - } - } else { - SecureField(deepl_api_key_placeholder, text: $settings.deepl_api_key) - .disableAutocorrection(true) - .autocapitalization(UITextAutocapitalizationType.none) - if settings.deepl_api_key != "" { - Button(NSLocalizedString("Show API Key", comment: "Button to show the DeepL translation API key.")) { - show_api_key = true - } - } - } - if settings.deepl_api_key == "" { - Link(NSLocalizedString("Get API Key", comment: "Button to navigate to DeepL website to get a translation API key."), destination: URL(string: "https://www.deepl.com/pro-api")!) - } - } - } - } } struct ConfigView_Previews: PreviewProvider { diff --git a/damus/Views/Settings/AppearanceSettingsView.swift b/damus/Views/Settings/AppearanceSettingsView.swift @@ -0,0 +1,65 @@ +// +// TextFormattingSettings.swift +// damus +// +// Created by William Casarin on 2023-04-05. +// + +import SwiftUI + + +struct AppearanceSettingsView: View { + @ObservedObject var settings: UserSettingsStore + @Environment(\.dismiss) var dismiss + + var body: some View { + Form { + Section(header: Text(NSLocalizedString("Text Truncation", comment: "Section header for damus text truncation user configuration"))) { + Toggle(NSLocalizedString("Truncate timeline text", comment: "Setting to truncate text in timeline"), isOn: $settings.truncate_timeline_text) + .toggleStyle(.switch) + Toggle(NSLocalizedString("Truncate notification mention text", comment: "Setting to truncate text in mention notifications"), isOn: $settings.truncate_mention_text) + .toggleStyle(.switch) + } + + Section(header: Text(NSLocalizedString("Accessibility", comment: "Section header for accessibility settings"))) { + Toggle(NSLocalizedString("Left Handed", comment: "Moves the post button to the left side of the screen"), isOn: $settings.left_handed) + .toggleStyle(.switch) + } + + Section(NSLocalizedString("Images", comment: "Section title for images configuration.")) { + Toggle(NSLocalizedString("Disable animations", comment: "Button to disable image animation"), isOn: $settings.disable_animation) + .toggleStyle(.switch) + .onChange(of: settings.disable_animation) { _ in + clear_kingfisher_cache() + } + Toggle(NSLocalizedString("Always show images", comment: "Setting to always show and never blur images"), isOn: $settings.always_show_images) + .toggleStyle(.switch) + + Picker(NSLocalizedString("Select image uploader", comment: "Prompt selection of user's image uploader"), + selection: $settings.default_media_uploader) { + ForEach(MediaUploader.allCases, id: \.self) { uploader in + Text(uploader.model.displayName) + .tag(uploader.model.tag) + } + } + + Button(NSLocalizedString("Clear Cache", comment: "Button to clear image cache.")) { + clear_kingfisher_cache() + } + } + + + } + .navigationTitle("Appearance") + .onReceive(handle_notify(.switched_timeline)) { _ in + dismiss() + } + } +} + + +struct TextFormattingSettings_Previews: PreviewProvider { + static var previews: some View { + AppearanceSettingsView(settings: UserSettingsStore()) + } +} diff --git a/damus/Views/Settings/KeySettingsView.swift b/damus/Views/Settings/KeySettingsView.swift @@ -0,0 +1,133 @@ +// +// KeySettingsView.swift +// damus +// +// Created by William Casarin on 2023-04-05. +// + +import SwiftUI +import LocalAuthentication + +struct KeySettingsView: View { + let keypair: Keypair + + @State var privkey: String + @State var privkey_copied: Bool = false + @State var pubkey_copied: Bool = false + @State var show_privkey: Bool = false + @State var has_authenticated_locally: Bool = false + + @Environment(\.dismiss) var dismiss + + init(keypair: Keypair) { + _privkey = State(initialValue: keypair.privkey_bech32 ?? "") + self.keypair = keypair + } + + var ShowSecToggle: some View { + Toggle(NSLocalizedString("Show", comment: "Toggle to show or hide user's secret account login key."), isOn: $show_privkey) + .onChange(of: show_privkey) { newValue in + if newValue { + authenticate_locally(has_authenticated_locally) { success in + show_privkey = success + } + } + } + } + + // TODO: (jb55) could be more general but not gonna worry about it atm + func CopyButton(is_pk: Bool) -> some View { + return Button(action: { + let copyKey = { + UIPasteboard.general.string = is_pk ? self.keypair.pubkey_bech32 : self.privkey + self.privkey_copied = !is_pk + self.pubkey_copied = is_pk + + let generator = UIImpactFeedbackGenerator(style: .light) + generator.impactOccurred() + } + + if is_pk { + copyKey() + return + } + + if has_authenticated_locally { + copyKey() + return + } + + authenticate_locally(has_authenticated_locally) { success in + if success { + copyKey() + } + } + }) { + let copied = is_pk ? self.pubkey_copied : self.privkey_copied + Image(systemName: copied ? "checkmark.circle" : "doc.on.doc") + } + } + + var body: some View { + Form { + Section(NSLocalizedString("Public Account ID", comment: "Section title for the user's public account ID.")) { + HStack { + Text(keypair.pubkey_bech32) + + CopyButton(is_pk: true) + } + .clipShape(RoundedRectangle(cornerRadius: 5)) + } + + if let sec = keypair.privkey_bech32 { + Section(NSLocalizedString("Secret Account Login Key", comment: "Section title for user's secret account login key.")) { + HStack { + if show_privkey == false || !has_authenticated_locally { + SecureField(NSLocalizedString("Private Key", comment: "Title of the secure field that holds the user's private key."), text: $privkey) + .disabled(true) + } else { + Text(sec) + .clipShape(RoundedRectangle(cornerRadius: 5)) + } + + CopyButton(is_pk: false) + } + + ShowSecToggle + } + } + + } + .navigationTitle("Keys") + .onReceive(handle_notify(.switched_timeline)) { _ in + dismiss() + } + } +} + +struct KeySettingsView_Previews: PreviewProvider { + static var previews: some View { + let kp = generate_new_keypair() + KeySettingsView(keypair: kp) + } +} + +func authenticate_locally(_ has_authenticated_locally: Bool, completion: @escaping (Bool) -> Void) { + // Need to authenticate only once while ConfigView is presented + guard !has_authenticated_locally else { + completion(true) + return + } + let context = LAContext() + if context.canEvaluatePolicy(.deviceOwnerAuthentication, error: nil) { + context.evaluatePolicy(.deviceOwnerAuthentication, localizedReason: NSLocalizedString("Local authentication to access private key", comment: "Face ID usage description shown when trying to access private key")) { success, error in + DispatchQueue.main.async { + completion(success) + } + } + } else { + // If there's no authentication set up on the device, let the user copy the key without it + completion(true) + } +} + diff --git a/damus/Views/Settings/NotificationSettingsView.swift b/damus/Views/Settings/NotificationSettingsView.swift @@ -9,6 +9,8 @@ import SwiftUI struct NotificationSettingsView: View { @ObservedObject var settings: UserSettingsStore + + @Environment(\.dismiss) var dismiss var body: some View { Form { @@ -30,6 +32,10 @@ struct NotificationSettingsView: View { .toggleStyle(.switch) } } + .navigationTitle("Notifications") + .onReceive(handle_notify(.switched_timeline)) { _ in + dismiss() + } } } diff --git a/damus/Views/Settings/TranslationSettingsView.swift b/damus/Views/Settings/TranslationSettingsView.swift @@ -0,0 +1,160 @@ +// +// TranslationSettingsView.swift +// damus +// +// Created by William Casarin on 2023-04-05. +// + +import SwiftUI + +struct TranslationSettingsView: View { + @ObservedObject var settings: UserSettingsStore + + @State var show_api_key: Bool = false + + @Environment(\.dismiss) var dismiss + + var body: some View { + Form { + Section(NSLocalizedString("Translations", comment: "Section title for selecting the translation service.")) { + Toggle(NSLocalizedString("Show only preferred languages on Universe feed", comment: "Toggle to show notes that are only in the device's preferred languages on the Universe feed and hide notes that are in other languages."), isOn: $settings.show_only_preferred_languages) + .toggleStyle(.switch) + + Picker(NSLocalizedString("Service", comment: "Prompt selection of translation service provider."), selection: $settings.translation_service) { + ForEach(TranslationService.allCases, id: \.self) { server in + Text(server.model.displayName) + .tag(server.model.tag) + } + } + + if settings.translation_service == .libretranslate { + Picker(NSLocalizedString("Server", comment: "Prompt selection of LibreTranslate server to perform machine translations on notes"), selection: $settings.libretranslate_server) { + ForEach(LibreTranslateServer.allCases, id: \.self) { server in + Text(server.model.displayName) + .tag(server.model.tag) + } + } + + if settings.libretranslate_server == .custom { + TextField(NSLocalizedString("URL", comment: "Example URL to LibreTranslate server"), text: $settings.libretranslate_url) + .disableAutocorrection(true) + .autocapitalization(UITextAutocapitalizationType.none) + } + + SecureField(NSLocalizedString("API Key (optional)", comment: "Prompt for optional entry of API Key to use translation server."), text: $settings.libretranslate_api_key) + .disableAutocorrection(true) + .disabled(settings.translation_service != .libretranslate) + .autocapitalization(UITextAutocapitalizationType.none) + } + + if settings.translation_service == .deepl { + Picker(NSLocalizedString("Plan", comment: "Prompt selection of DeepL subscription plan to perform machine translations on notes"), selection: $settings.deepl_plan) { + ForEach(DeepLPlan.allCases, id: \.self) { server in + Text(server.model.displayName) + .tag(server.model.tag) + } + } + + SecureField(NSLocalizedString("API Key (required)", comment: "Prompt for required entry of API Key to use translation server."), text: $settings.deepl_api_key) + .disableAutocorrection(true) + .disabled(settings.translation_service != .deepl) + .autocapitalization(UITextAutocapitalizationType.none) + + if settings.deepl_api_key == "" { + Link(NSLocalizedString("Get API Key", comment: "Button to navigate to DeepL website to get a translation API key."), destination: URL(string: "https://www.deepl.com/pro-api")!) + } + } + + if settings.translation_service != .none { + Toggle(NSLocalizedString("Automatically translate notes", comment: "Toggle to automatically translate notes."), isOn: $settings.auto_translate) + .toggleStyle(.switch) + } + } + } + .navigationTitle("Translation") + .onReceive(handle_notify(.switched_timeline)) { _ in + dismiss() + } + } + + var libretranslate_view: some View { + VStack { + Picker(NSLocalizedString("Server", comment: "Prompt selection of LibreTranslate server to perform machine translations on notes"), selection: $settings.libretranslate_server) { + ForEach(LibreTranslateServer.allCases, id: \.self) { server in + Text(server.model.displayName) + .tag(server.model.tag) + } + } + + TextField(NSLocalizedString("URL", comment: "Example URL to LibreTranslate server"), text: $settings.libretranslate_url) + .disableAutocorrection(true) + .disabled(settings.libretranslate_server != .custom) + .autocapitalization(UITextAutocapitalizationType.none) + HStack { + let libretranslate_api_key_placeholder = NSLocalizedString("API Key (optional)", comment: "Prompt for optional entry of API Key to use translation server.") + if show_api_key { + TextField(libretranslate_api_key_placeholder, text: $settings.libretranslate_api_key) + .disableAutocorrection(true) + .autocapitalization(UITextAutocapitalizationType.none) + if settings.libretranslate_api_key != "" { + Button(NSLocalizedString("Hide API Key", comment: "Button to hide the LibreTranslate server API key.")) { + show_api_key = false + } + } + } else { + SecureField(libretranslate_api_key_placeholder, text: $settings.libretranslate_api_key) + .disableAutocorrection(true) + .autocapitalization(UITextAutocapitalizationType.none) + if settings.libretranslate_api_key != "" { + Button(NSLocalizedString("Show API Key", comment: "Button to show the LibreTranslate server API key.")) { + show_api_key = true + } + } + } + } + } + } + + var deepl_view: some View { + VStack { + Picker(NSLocalizedString("Plan", comment: "Prompt selection of DeepL subscription plan to perform machine translations on notes"), selection: $settings.deepl_plan) { + ForEach(DeepLPlan.allCases, id: \.self) { server in + Text(server.model.displayName) + .tag(server.model.tag) + } + } + + HStack { + let deepl_api_key_placeholder = NSLocalizedString("API Key (required)", comment: "Prompt for required entry of API Key to use translation server.") + if show_api_key { + TextField(deepl_api_key_placeholder, text: $settings.deepl_api_key) + .disableAutocorrection(true) + .autocapitalization(UITextAutocapitalizationType.none) + if settings.deepl_api_key != "" { + Button(NSLocalizedString("Hide API Key", comment: "Button to hide the DeepL translation API key.")) { + show_api_key = false + } + } + } else { + SecureField(deepl_api_key_placeholder, text: $settings.deepl_api_key) + .disableAutocorrection(true) + .autocapitalization(UITextAutocapitalizationType.none) + if settings.deepl_api_key != "" { + Button(NSLocalizedString("Show API Key", comment: "Button to show the DeepL translation API key.")) { + show_api_key = true + } + } + } + if settings.deepl_api_key == "" { + Link(NSLocalizedString("Get API Key", comment: "Button to navigate to DeepL website to get a translation API key."), destination: URL(string: "https://www.deepl.com/pro-api")!) + } + } + } + } +} + +struct TranslationSettingsView_Previews: PreviewProvider { + static var previews: some View { + TranslationSettingsView(settings: UserSettingsStore()) + } +} diff --git a/damus/Views/Settings/ZapSettingsView.swift b/damus/Views/Settings/ZapSettingsView.swift @@ -0,0 +1,66 @@ +// +// WalletSettingsView.swift +// damus +// +// Created by William Casarin on 2023-04-05. +// + +import SwiftUI +import Combine + +struct ZapSettingsView: View { + let pubkey: String + @ObservedObject var settings: UserSettingsStore + + @State var default_zap_amount: String + @Environment(\.dismiss) var dismiss + + init(pubkey: String, settings: UserSettingsStore) { + self.pubkey = pubkey + let zap_amt = get_default_zap_amount(pubkey: pubkey).map({ "\($0)" }) ?? "1000" + _default_zap_amount = State(initialValue: zap_amt) + self._settings = ObservedObject(initialValue: settings) + } + + var body: some View { + Form { + Section("Wallet") { + + Toggle(NSLocalizedString("Show wallet selector", comment: "Toggle to show or hide selection of wallet."), isOn: $settings.show_wallet_selector).toggleStyle(.switch) + Picker(NSLocalizedString("Select default wallet", comment: "Prompt selection of user's default wallet"), + selection: $settings.default_wallet) { + ForEach(Wallet.allCases, id: \.self) { wallet in + Text(wallet.model.displayName) + .tag(wallet.model.tag) + } + } + } + + Section("Zaps") { + Toggle(NSLocalizedString("Zap Vibration", comment: "Setting to enable vibration on zap"), isOn: $settings.zap_vibration) + .toggleStyle(.switch) + } + + Section("Default Zap Amount in sats") { + TextField(String("1000"), text: $default_zap_amount) + .keyboardType(.numberPad) + .onReceive(Just(default_zap_amount)) { newValue in + if let parsed = handle_string_amount(new_value: newValue) { + self.default_zap_amount = String(parsed) + set_default_zap_amount(pubkey: self.pubkey, amount: parsed) + } + } + } + } + .navigationTitle("Zaps") + .onReceive(handle_notify(.switched_timeline)) { _ in + dismiss() + } + } +} + +struct WalletSettingsView_Previews: PreviewProvider { + static var previews: some View { + ZapSettingsView(pubkey: "pubkey", settings: UserSettingsStore()) + } +}