From 1696e0365e228359d105f4bb75b179981c3dc724 Mon Sep 17 00:00:00 2001 From: William Casarin Date: Fri, 3 Feb 2023 09:25:07 -0800 Subject: [PATCH] refactor: settings and translation view --- damus.xcodeproj/project.pbxproj | 12 +- damus/Components/TranslateView.swift | 138 ++++++++++++++++++ damus/ContentView.swift | 7 +- damus/Models/DamusState.swift | 3 +- damus/Models/Mentions.swift | 2 +- damus/Models/UserSettingsStore.swift | 31 +++- damus/Views/ChatView.swift | 9 +- damus/Views/ConfigView.swift | 26 ++-- damus/Views/DMView.swift | 2 +- damus/Views/Events/EventBody.swift | 2 +- damus/Views/NoteContentView.swift | 211 ++++++++------------------- damus/Views/PostButton.swift | 3 +- damus/Views/ProfileView.swift | 8 +- damus/Views/ReplyQuoteView.swift | 65 --------- damus/Views/SideMenuView.swift | 4 +- 15 files changed, 266 insertions(+), 257 deletions(-) create mode 100644 damus/Components/TranslateView.swift delete mode 100644 damus/Views/ReplyQuoteView.swift diff --git a/damus.xcodeproj/project.pbxproj b/damus.xcodeproj/project.pbxproj index 967a6041..ffca4ce8 100644 --- a/damus.xcodeproj/project.pbxproj +++ b/damus.xcodeproj/project.pbxproj @@ -28,7 +28,6 @@ 4C0A3F8F280F640A000448DE /* ThreadModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C0A3F8E280F640A000448DE /* ThreadModel.swift */; }; 4C0A3F91280F6528000448DE /* ChatView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C0A3F90280F6528000448DE /* ChatView.swift */; }; 4C0A3F93280F66F5000448DE /* ReplyMap.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C0A3F92280F66F5000448DE /* ReplyMap.swift */; }; - 4C0A3F95280F6C78000448DE /* ReplyQuoteView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C0A3F94280F6C78000448DE /* ReplyQuoteView.swift */; }; 4C0A3F97280F8E02000448DE /* ThreadView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C0A3F96280F8E02000448DE /* ThreadView.swift */; }; 4C216F32286E388800040376 /* DMChatView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C216F31286E388800040376 /* DMChatView.swift */; }; 4C216F34286F5ACD00040376 /* DMView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C216F33286F5ACD00040376 /* DMView.swift */; }; @@ -86,6 +85,7 @@ 4C3EA67B28FF7B3900C48A62 /* InvoiceTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C3EA67A28FF7B3900C48A62 /* InvoiceTests.swift */; }; 4C3EA67D28FFBBA300C48A62 /* InvoicesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C3EA67C28FFBBA200C48A62 /* InvoicesView.swift */; }; 4C3EA67F28FFC01D00C48A62 /* InvoiceView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C3EA67E28FFC01D00C48A62 /* InvoiceView.swift */; }; + 4C42812C298C848200DBF26F /* TranslateView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C42812B298C848200DBF26F /* TranslateView.swift */; }; 4C477C9E282C3A4800033AA3 /* TipCounter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C477C9D282C3A4800033AA3 /* TipCounter.swift */; }; 4C5C7E68284ED36500A22DF5 /* SearchHomeModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C5C7E67284ED36500A22DF5 /* SearchHomeModel.swift */; }; 4C5C7E6A284EDE2E00A22DF5 /* SearchResultsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C5C7E69284EDE2E00A22DF5 /* SearchResultsView.swift */; }; @@ -138,6 +138,7 @@ 4CB883B0297705DD00DC99E7 /* ZapButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CB883AF297705DD00DC99E7 /* ZapButton.swift */; }; 4CB883B6297730E400DC99E7 /* LNUrls.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CB883B5297730E400DC99E7 /* LNUrls.swift */; }; 4CBCA930297DB57F00EC6B2F /* WebsiteLink.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CBCA92F297DB57F00EC6B2F /* WebsiteLink.swift */; }; + 4CC7AAE7297EFA7B00430951 /* Zap.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CC7AAE6297EFA7B00430951 /* Zap.swift */; }; 4CC7AAEB297F0AEC00430951 /* BuilderEventView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CC7AAEA297F0AEC00430951 /* BuilderEventView.swift */; }; 4CC7AAED297F0B9E00430951 /* Highlight.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CC7AAEC297F0B9E00430951 /* Highlight.swift */; }; 4CC7AAF0297F11C700430951 /* SelectedEventView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CC7AAEF297F11C700430951 /* SelectedEventView.swift */; }; @@ -146,7 +147,6 @@ 4CC7AAF6297F1A6A00430951 /* EventBody.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CC7AAF5297F1A6A00430951 /* EventBody.swift */; }; 4CC7AAF8297F1CEE00430951 /* EventProfile.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CC7AAF7297F1CEE00430951 /* EventProfile.swift */; }; 4CC7AAFA297F64AC00430951 /* EventMenu.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CC7AAF9297F64AC00430951 /* EventMenu.swift */; }; - 4CC7AAE7297EFA7B00430951 /* Zap.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CC7AAE6297EFA7B00430951 /* Zap.swift */; }; 4CD7641B28A1641400B6928F /* EndBlock.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CD7641A28A1641400B6928F /* EndBlock.swift */; }; 4CE4F8CD281352B30009DFBB /* Notifications.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CE4F8CC281352B30009DFBB /* Notifications.swift */; }; 4CE4F9DE2852768D00C00DD9 /* ConfigView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CE4F9DD2852768D00C00DD9 /* ConfigView.swift */; }; @@ -274,7 +274,6 @@ 4C0A3F8E280F640A000448DE /* ThreadModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ThreadModel.swift; sourceTree = ""; }; 4C0A3F90280F6528000448DE /* ChatView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatView.swift; sourceTree = ""; }; 4C0A3F92280F66F5000448DE /* ReplyMap.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReplyMap.swift; sourceTree = ""; }; - 4C0A3F94280F6C78000448DE /* ReplyQuoteView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReplyQuoteView.swift; sourceTree = ""; }; 4C0A3F96280F8E02000448DE /* ThreadView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ThreadView.swift; sourceTree = ""; }; 4C216F31286E388800040376 /* DMChatView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DMChatView.swift; sourceTree = ""; }; 4C216F33286F5ACD00040376 /* DMView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DMView.swift; sourceTree = ""; }; @@ -361,6 +360,7 @@ 4C3EA67A28FF7B3900C48A62 /* InvoiceTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InvoiceTests.swift; sourceTree = ""; }; 4C3EA67C28FFBBA200C48A62 /* InvoicesView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InvoicesView.swift; sourceTree = ""; }; 4C3EA67E28FFC01D00C48A62 /* InvoiceView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InvoiceView.swift; sourceTree = ""; }; + 4C42812B298C848200DBF26F /* TranslateView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TranslateView.swift; sourceTree = ""; }; 4C477C9D282C3A4800033AA3 /* TipCounter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TipCounter.swift; sourceTree = ""; }; 4C4A3A5A288A1B2200453788 /* damus.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = damus.entitlements; sourceTree = ""; }; 4C5C7E67284ED36500A22DF5 /* SearchHomeModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchHomeModel.swift; sourceTree = ""; }; @@ -414,6 +414,7 @@ 4CB883AF297705DD00DC99E7 /* ZapButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ZapButton.swift; sourceTree = ""; }; 4CB883B5297730E400DC99E7 /* LNUrls.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LNUrls.swift; sourceTree = ""; }; 4CBCA92F297DB57F00EC6B2F /* WebsiteLink.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WebsiteLink.swift; sourceTree = ""; }; + 4CC7AAE6297EFA7B00430951 /* Zap.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Zap.swift; sourceTree = ""; }; 4CC7AAEA297F0AEC00430951 /* BuilderEventView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BuilderEventView.swift; sourceTree = ""; }; 4CC7AAEC297F0B9E00430951 /* Highlight.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Highlight.swift; sourceTree = ""; }; 4CC7AAEF297F11C700430951 /* SelectedEventView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SelectedEventView.swift; sourceTree = ""; }; @@ -422,7 +423,6 @@ 4CC7AAF5297F1A6A00430951 /* EventBody.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EventBody.swift; sourceTree = ""; }; 4CC7AAF7297F1CEE00430951 /* EventProfile.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EventProfile.swift; sourceTree = ""; }; 4CC7AAF9297F64AC00430951 /* EventMenu.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EventMenu.swift; sourceTree = ""; }; - 4CC7AAE6297EFA7B00430951 /* Zap.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Zap.swift; sourceTree = ""; }; 4CD7641A28A1641400B6928F /* EndBlock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EndBlock.swift; sourceTree = ""; }; 4CE4F8CC281352B30009DFBB /* Notifications.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Notifications.swift; sourceTree = ""; }; 4CE4F9DD2852768D00C00DD9 /* ConfigView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConfigView.swift; sourceTree = ""; }; @@ -659,7 +659,6 @@ 4C8682862814DE470026224F /* ProfileView.swift */, 4C3AC7A42836987600E1F516 /* MainTabView.swift */, 4C363A8B28236B92006E126D /* PubkeyView.swift */, - 4C0A3F94280F6C78000448DE /* ReplyQuoteView.swift */, 4CACA9D4280C31E100D9BBE8 /* ReplyView.swift */, F7F0BA262978E54D009531F3 /* ParicipantsView.swift */, 4C285C8D28399BFD008A31F1 /* SaveKeysView.swift */, @@ -793,6 +792,7 @@ 4CF0ABE22981BC7D00D66079 /* UserView.swift */, 7C902AE22981D55B002AB16E /* ZoomableScrollView.swift */, 4CB883AF297705DD00DC99E7 /* ZapButton.swift */, + 4C42812B298C848200DBF26F /* TranslateView.swift */, ); path = Components; sourceTree = ""; @@ -1177,6 +1177,7 @@ 4C06670628FCB08600038D2A /* ImageCarousel.swift in Sources */, 4C75EFAF28049D350006080F /* NostrFilter.swift in Sources */, 4C3EA64C28FF59AC00C48A62 /* bech32_util.c in Sources */, + 4C42812C298C848200DBF26F /* TranslateView.swift in Sources */, 4C363A9C282838B9006E126D /* EventRef.swift in Sources */, 3AA24802297E3DC20090C62D /* RepostView.swift in Sources */, 4CD7641B28A1641400B6928F /* EndBlock.swift in Sources */, @@ -1215,7 +1216,6 @@ 4C3AC7A728369BA200E1F516 /* SearchHomeView.swift in Sources */, 4CB883B0297705DD00DC99E7 /* ZapButton.swift in Sources */, 4C363A922825FCF2006E126D /* ProfileUpdate.swift in Sources */, - 4C0A3F95280F6C78000448DE /* ReplyQuoteView.swift in Sources */, 4C3BEFDA281DCA1400B3DE84 /* LikeCounter.swift in Sources */, 4CB88389296AF99A00DC99E7 /* EventDetailBar.swift in Sources */, 4CF0ABDE2981A69500D66079 /* MutelistModel.swift in Sources */, diff --git a/damus/Components/TranslateView.swift b/damus/Components/TranslateView.swift new file mode 100644 index 00000000..9a104868 --- /dev/null +++ b/damus/Components/TranslateView.swift @@ -0,0 +1,138 @@ +// +// TranslateButton.swift +// damus +// +// Created by William Casarin on 2023-02-02. +// + +import SwiftUI +import NaturalLanguage + +struct TranslateView: View { + let damus_state: DamusState + let event: NostrEvent + let size: EventViewKind + + @State var checkingTranslationStatus: Bool = false + @State var currentLanguage: String = "en" + @State var noteLanguage: String? = nil + @State var translated_note: String? = nil + @State var show_translated_note: Bool = false + @State var translated_artifacts: NoteArtifacts? = nil + + var TranslateButton: some View { + Button(NSLocalizedString("Translate Note", comment: "Button to translate note from different language.")) { + show_translated_note = true + } + .translate_button_style() + } + + func Translated(lang: String, artifacts: NoteArtifacts) -> some View { + return Group { + Button(NSLocalizedString("Translated from \(lang)", comment: "Button to indicate that the note has been translated from a different language.")) { + show_translated_note = false + } + .translate_button_style() + + Text(artifacts.content) + .font(eventviewsize_to_font(size)) + .fixedSize(horizontal: false, vertical: true) + } + } + + func CheckingStatus(lang: String) -> some View { + return Button(NSLocalizedString("Translating from \(lang)...", comment: "Button to indicate that the note is in the process of being translated from a different language.")) { + show_translated_note = false + } + .translate_button_style() + } + + func MainContent(note_lang: String) -> some View { + return Group { + let languageName = Locale.current.localizedString(forLanguageCode: note_lang) + if let lang = languageName, show_translated_note { + if checkingTranslationStatus { + CheckingStatus(lang: lang) + } else if let artifacts = translated_artifacts { + Translated(lang: lang, artifacts: artifacts) + } + } else { + TranslateButton + } + } + } + + var body: some View { + Group { + if let note_lang = noteLanguage, noteLanguage != currentLanguage { + MainContent(note_lang: note_lang) + } else { + Text("") + } + } + .task { + let translate_url = damus_state.settings.libretranslate_url + let api_key = damus_state.settings.libretranslate_api_key + + guard noteLanguage == nil && !checkingTranslationStatus && translate_url != "" else { + return + } + + checkingTranslationStatus = true + + if #available(iOS 16, *) { + currentLanguage = Locale.current.language.languageCode?.identifier ?? "en" + } else { + currentLanguage = Locale.current.languageCode ?? "en" + } + + // Rely on Apple's NLLanguageRecognizer to tell us which language it thinks the note is in. + let content = event.get_content(damus_state.keypair.privkey) + noteLanguage = NLLanguageRecognizer.dominantLanguage(for: content)?.rawValue ?? currentLanguage + + if let lang = noteLanguage, noteLanguage != currentLanguage { + // If the detected dominant language is a variant, remove the variant component and just take the language part as LibreTranslate typically only supports the variant-less language. + if #available(iOS 16, *) { + noteLanguage = Locale.LanguageCode(stringLiteral: lang).identifier(.alpha2) + } else { + noteLanguage = Locale.canonicalLanguageIdentifier(from: lang) + } + } + + guard let note_lang = noteLanguage else { + noteLanguage = currentLanguage + translated_note = nil + checkingTranslationStatus = false + return + } + + if note_lang != currentLanguage { + do { + // If the note language is different from our language, send a translation request. + let translator = Translator(translate_url, apiKey: api_key) + translated_note = try await translator.translate(content, from: note_lang, to: currentLanguage) + } catch { + // If for whatever reason we're not able to figure out the language of the note, or translate the note, fail gracefully and do not retry. It's not the end of the world. Don't want to take down someone's translation server with an accidental denial of service attack. + noteLanguage = currentLanguage + translated_note = nil + } + } + + if let translated = translated_note { + // Render translated note. + let blocks = event.get_blocks(content: translated) + translated_artifacts = render_blocks(blocks: blocks, profiles: damus_state.profiles, privkey: damus_state.keypair.privkey) + } + + checkingTranslationStatus = false + + } + } +} + +struct TranslateView_Previews: PreviewProvider { + static var previews: some View { + let ds = test_damus_state() + TranslateView(damus_state: ds, event: test_event, size: .selected) + } +} diff --git a/damus/ContentView.swift b/damus/ContentView.swift index 19fc7fc9..8681cb7e 100644 --- a/damus/ContentView.swift +++ b/damus/ContentView.swift @@ -89,7 +89,6 @@ struct ContentView: View { @State var filter_state : FilterState = .posts_and_replies @State private var isSideBarOpened = false @StateObject var home: HomeModel = HomeModel() - @StateObject var user_settings = UserSettingsStore() // connect retry timer let timer = Timer.publish(every: 4, on: .main, in: .common).autoconnect() @@ -112,7 +111,7 @@ struct ContentView: View { .tabViewStyle(.page(indexDisplayMode: .never)) if privkey != nil { - PostButtonContainer(userSettings: user_settings) { + PostButtonContainer(is_left_handed: damus_state?.settings.left_handed ?? false) { self.active_sheet = .post } } @@ -286,7 +285,6 @@ struct ContentView: View { .padding([.bottom], 8) } } - .environmentObject(user_settings) .onAppear() { self.connect() //KingfisherManager.shared.cache.clearDiskCache() @@ -563,7 +561,8 @@ struct ContentView: View { dms: home.dms, previews: PreviewCache(), zaps: Zaps(our_pubkey: pubkey), - lnurls: LNUrls() + lnurls: LNUrls(), + settings: UserSettingsStore() ) home.damus_state = self.damus_state! diff --git a/damus/Models/DamusState.swift b/damus/Models/DamusState.swift index a848ef77..37f39e63 100644 --- a/damus/Models/DamusState.swift +++ b/damus/Models/DamusState.swift @@ -20,6 +20,7 @@ struct DamusState { let previews: PreviewCache let zaps: Zaps let lnurls: LNUrls + let settings: UserSettingsStore var pubkey: String { return keypair.pubkey @@ -31,6 +32,6 @@ struct DamusState { static var empty: DamusState { - return DamusState.init(pool: RelayPool(), keypair: Keypair(pubkey: "", privkey: ""), likes: EventCounter(our_pubkey: ""), boosts: EventCounter(our_pubkey: ""), contacts: Contacts(our_pubkey: ""), tips: TipCounter(our_pubkey: ""), profiles: Profiles(), dms: DirectMessagesModel(our_pubkey: ""), previews: PreviewCache(), zaps: Zaps(our_pubkey: ""), lnurls: LNUrls()) + return DamusState.init(pool: RelayPool(), keypair: Keypair(pubkey: "", privkey: ""), likes: EventCounter(our_pubkey: ""), boosts: EventCounter(our_pubkey: ""), contacts: Contacts(our_pubkey: ""), tips: TipCounter(our_pubkey: ""), profiles: Profiles(), dms: DirectMessagesModel(our_pubkey: ""), previews: PreviewCache(), zaps: Zaps(our_pubkey: ""), lnurls: LNUrls(), settings: UserSettingsStore()) } } diff --git a/damus/Models/Mentions.swift b/damus/Models/Mentions.swift index df76f315..d70eb78d 100644 --- a/damus/Models/Mentions.swift +++ b/damus/Models/Mentions.swift @@ -52,7 +52,7 @@ struct LightningInvoice { switch description { case .description(let string): return string - case .description_hash(let data): + case .description_hash: return "" } } diff --git a/damus/Models/UserSettingsStore.swift b/damus/Models/UserSettingsStore.swift index 50fd8c0f..97dc4b47 100644 --- a/damus/Models/UserSettingsStore.swift +++ b/damus/Models/UserSettingsStore.swift @@ -22,6 +22,22 @@ func get_default_wallet(_ pubkey: String) -> Wallet { } } +func get_libretranslate_server(_ pubkey: String) -> LibreTranslateServer? { + guard let server_name = UserDefaults.standard.string(forKey: "libretranslate_server") else { + return nil + } + + return LibreTranslateServer(rawValue: server_name) +} + +func get_libretranslate_url(_ pubkey: String, server: LibreTranslateServer) -> String? { + if let url = server.model.url { + return url + } + + return UserDefaults.standard.object(forKey: "libretranslate_url") as? String +} + class UserSettingsStore: ObservableObject { @Published var default_wallet: Wallet { didSet { @@ -82,15 +98,14 @@ class UserSettingsStore: ObservableObject { init() { // TODO: pubkey-scoped settings let pubkey = "" - self.default_wallet = get_default_wallet("") - show_wallet_selector = should_show_wallet_selector("") + self.default_wallet = get_default_wallet(pubkey) + show_wallet_selector = should_show_wallet_selector(pubkey) left_handed = UserDefaults.standard.object(forKey: "left_handed") as? Bool ?? false - - if let translationServerName = UserDefaults.standard.string(forKey: "libretranslate_server"), - let translationServer = LibreTranslateServer(rawValue: translationServerName) { - self.libretranslate_server = translationServer - libretranslate_url = translationServer.model.url ?? UserDefaults.standard.object(forKey: "libretranslate_url") as? String ?? "" + + if let server = get_libretranslate_server(pubkey) { + self.libretranslate_server = server + self.libretranslate_url = get_libretranslate_url(pubkey, server: server) ?? "" } else { // Note from @tyiu: // Default server is disabled by default for now until we gain some confidence that it is working well in production. @@ -101,7 +116,7 @@ class UserSettingsStore: ObservableObject { libretranslate_server = .none libretranslate_url = "" } - + do { libretranslate_api_key = try Vault.getPrivateKey(keychainConfiguration: DamusLibreTranslateKeychainConfiguration()) } catch { diff --git a/damus/Views/ChatView.swift b/damus/Views/ChatView.swift index 72b3f9b1..4654f350 100644 --- a/damus/Views/ChatView.swift +++ b/damus/Views/ChatView.swift @@ -96,17 +96,24 @@ struct ChatView: View { if let ref_id = thread.replies.lookup(event.id) { if !is_reply_to_prev() { + /* ReplyQuoteView(keypair: damus_state.keypair, quoter: event, event_id: ref_id, profiles: damus_state.profiles, previews: damus_state.previews) .frame(maxHeight: expand_reply ? nil : 100) .environmentObject(thread) .onTapGesture { expand_reply = !expand_reply } + */ ReplyDescription } } - NoteContentView(keypair: damus_state.keypair, event: event, profiles: damus_state.profiles, previews: damus_state.previews, show_images: should_show_images(contacts: damus_state.contacts, ev: event, our_pubkey: damus_state.pubkey), artifacts: .just_content(event.content), size: .normal) + let show_images = should_show_images(contacts: damus_state.contacts, ev: event, our_pubkey: damus_state.pubkey) + NoteContentView(damus_state: damus_state, + event: event, + show_images: show_images, + artifacts: .just_content(event.content), + size: .normal) if is_active || next_ev == nil || next_ev!.pubkey != event.pubkey { let bar = make_actionbar_model(ev: event, damus: damus_state) diff --git a/damus/Views/ConfigView.swift b/damus/Views/ConfigView.swift index cb85ba88..217469e4 100644 --- a/damus/Views/ConfigView.swift +++ b/damus/Views/ConfigView.swift @@ -19,13 +19,15 @@ struct ConfigView: View { @State var privkey_copied: Bool = false @State var pubkey_copied: Bool = false @State var delete_text: String = "" - @EnvironmentObject var user_settings: UserSettingsStore - + + @ObservedObject var settings: UserSettingsStore + let generator = UIImpactFeedbackGenerator(style: .light) - + init(state: DamusState) { self.state = state _privkey = State(initialValue: self.state.keypair.privkey_bech32 ?? "") + _settings = ObservedObject(initialValue: state.settings) } // TODO: (jb55) could be more general but not gonna worry about it atm @@ -72,9 +74,9 @@ struct ConfigView: View { } Section(NSLocalizedString("Wallet Selector", comment: "Section title for selection of wallet.")) { - Toggle(NSLocalizedString("Show wallet selector", comment: "Toggle to show or hide selection of wallet."), isOn: $user_settings.show_wallet_selector).toggleStyle(.switch) + 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: $user_settings.default_wallet) { + selection: $settings.default_wallet) { ForEach(Wallet.allCases, id: \.self) { wallet in Text(wallet.model.displayName) .tag(wallet.model.tag) @@ -83,28 +85,28 @@ struct ConfigView: View { } Section(NSLocalizedString("LibreTranslate Translations", comment: "Section title for selecting the server that hosts the LibreTranslate machine translation API.")) { - Picker(NSLocalizedString("Server", comment: "Prompt selection of LibreTranslate server to perform machine translations on notes"), selection: $user_settings.libretranslate_server) { + 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 user_settings.libretranslate_server != .none { - TextField(NSLocalizedString("URL", comment: "Example URL to LibreTranslate server"), text: $user_settings.libretranslate_url) + if settings.libretranslate_server != .none { + TextField(NSLocalizedString("URL", comment: "Example URL to LibreTranslate server"), text: $settings.libretranslate_url) .disableAutocorrection(true) - .disabled(user_settings.libretranslate_server != .custom) + .disabled(settings.libretranslate_server != .custom) .autocapitalization(UITextAutocapitalizationType.none) HStack { if show_libretranslate_api_key { - TextField(NSLocalizedString("API Key (optional)", comment: "Example URL to LibreTranslate server"), text: $user_settings.libretranslate_api_key) + TextField(NSLocalizedString("API Key (optional)", comment: "Example URL to LibreTranslate server"), text: $settings.libretranslate_api_key) .disableAutocorrection(true) .autocapitalization(UITextAutocapitalizationType.none) Button(NSLocalizedString("Hide API Key", comment: "Button to hide the LibreTranslate server API key.")) { show_libretranslate_api_key = false } } else { - SecureField(NSLocalizedString("API Key (optional)", comment: "Example URL to LibreTranslate server"), text: $user_settings.libretranslate_api_key) + SecureField(NSLocalizedString("API Key (optional)", comment: "Example URL to LibreTranslate server"), text: $settings.libretranslate_api_key) .disableAutocorrection(true) .autocapitalization(UITextAutocapitalizationType.none) Button(NSLocalizedString("Show API Key", comment: "Button to hide the LibreTranslate server API key.")) { @@ -116,7 +118,7 @@ struct ConfigView: View { } Section(NSLocalizedString("Left Handed", comment: "Moves the post button to the left side of the screen")) { - Toggle(NSLocalizedString("Left Handed", comment: "Moves the post button to the left side of the screen"), isOn: $user_settings.left_handed) + Toggle(NSLocalizedString("Left Handed", comment: "Moves the post button to the left side of the screen"), isOn: $settings.left_handed) .toggleStyle(.switch) } diff --git a/damus/Views/DMView.swift b/damus/Views/DMView.swift index c6b88618..54ee72e5 100644 --- a/damus/Views/DMView.swift +++ b/damus/Views/DMView.swift @@ -23,7 +23,7 @@ struct DMView: View { let should_show_img = should_show_images(contacts: damus_state.contacts, ev: event, our_pubkey: damus_state.pubkey) - NoteContentView(keypair: damus_state.keypair, event: event, profiles: damus_state.profiles, previews: damus_state.previews, show_images: should_show_img, artifacts: .just_content(event.get_content(damus_state.keypair.privkey)), size: .normal) + NoteContentView(damus_state: damus_state, event: event, show_images: should_show_img, artifacts: .just_content(event.get_content(damus_state.keypair.privkey)), size: .normal) .foregroundColor(is_ours ? Color.white : Color.primary) .padding(10) .background(is_ours ? Color.accentColor : Color.secondary.opacity(0.15)) diff --git a/damus/Views/Events/EventBody.swift b/damus/Views/Events/EventBody.swift index 4477f385..40a62b1c 100644 --- a/damus/Views/Events/EventBody.swift +++ b/damus/Views/Events/EventBody.swift @@ -23,7 +23,7 @@ struct EventBody: View { let should_show_img = should_show_images(contacts: damus_state.contacts, ev: event, our_pubkey: damus_state.pubkey, booster_pubkey: nil) - NoteContentView(privkey: damus_state.keypair.privkey, event: event, profiles: damus_state.profiles, previews: damus_state.previews, show_images: should_show_img, artifacts: .just_content(content), size: size) + NoteContentView(damus_state: damus_state, event: event, show_images: should_show_img, artifacts: .just_content(content), size: size) .frame(maxWidth: .infinity, alignment: .leading) } } diff --git a/damus/Views/NoteContentView.swift b/damus/Views/NoteContentView.swift index 0bc80f0e..bb492c91 100644 --- a/damus/Views/NoteContentView.swift +++ b/damus/Views/NoteContentView.swift @@ -13,108 +13,16 @@ import NaturalLanguage import FoundationNetworking #endif -struct NoteArtifacts { - let content: AttributedString - let images: [URL] - let invoices: [Invoice] - let links: [URL] - - static func just_content(_ content: String) -> NoteArtifacts { - NoteArtifacts(content: AttributedString(stringLiteral: content), images: [], invoices: [], links: []) - } -} - -func render_note_content(ev: NostrEvent, profiles: Profiles, privkey: String?) -> NoteArtifacts { - let blocks = ev.blocks(privkey) - return render_blocks(blocks: blocks, profiles: profiles, privkey: privkey) -} - -func render_blocks(blocks: [Block], profiles: Profiles, privkey: String?) -> NoteArtifacts { - var invoices: [Invoice] = [] - var img_urls: [URL] = [] - var link_urls: [URL] = [] - let txt: AttributedString = blocks.reduce("") { str, block in - switch block { - case .mention(let m): - return str + mention_str(m, profiles: profiles) - case .text(let txt): - return str + AttributedString(stringLiteral: txt) - case .hashtag(let htag): - return str + hashtag_str(htag) - case .invoice(let invoice): - invoices.append(invoice) - return str - case .url(let url): - // Handle Image URLs - if is_image_url(url) { - // Append Image - img_urls.append(url) - return str - } else { - link_urls.append(url) - return str + url_str(url) - } - } - } - - return NoteArtifacts(content: txt, images: img_urls, invoices: invoices, links: link_urls) -} - -func is_image_url(_ url: URL) -> Bool { - let str = url.lastPathComponent.lowercased() - return str.hasSuffix("png") || str.hasSuffix("jpg") || str.hasSuffix("jpeg") || str.hasSuffix("gif") -} - struct NoteContentView: View { - let keypair: Keypair + let damus_state: DamusState let event: NostrEvent - let profiles: Profiles - let previews: PreviewCache - let show_images: Bool - - @State var checkingTranslationStatus: Bool = false - @State var currentLanguage: String = "en" - @State var noteLanguage: String? = nil - @State var translated_note: String? = nil - @State var show_translated_note: Bool = false - @State var translated_artifacts: NoteArtifacts? = nil @State var artifacts: NoteArtifacts - @State var preview: LinkViewRepresentable? = nil let size: EventViewKind - - @EnvironmentObject var user_settings: UserSettingsStore - var TranslateButton: some View { - Group { - let languageName = Locale.current.localizedString(forLanguageCode: noteLanguage!) - if show_translated_note { - if checkingTranslationStatus { - Button(NSLocalizedString("Translating from \(languageName!)...", comment: "Button to indicate that the note is in the process of being translated from a different language.")) { - show_translated_note = false - } - .translate_button_style() - - } else if translated_artifacts != nil { - Button(NSLocalizedString("Translated from \(languageName!)", comment: "Button to indicate that the note has been translated from a different language.")) { - show_translated_note = false - } - .translate_button_style() - - Text(translated_artifacts!.content) - .font(eventviewsize_to_font(size)) - .fixedSize(horizontal: false, vertical: true) - } - } else { - Button(NSLocalizedString("Translate Note", comment: "Button to translate note from different language.")) { - show_translated_note = true - } - .translate_button_style() - } - } - } + @State var preview: LinkViewRepresentable? = nil func MainContent() -> some View { return VStack(alignment: .leading) { @@ -122,8 +30,8 @@ struct NoteContentView: View { .font(eventviewsize_to_font(size)) .fixedSize(horizontal: false, vertical: true) - if size == .selected && noteLanguage != nil && noteLanguage != currentLanguage { - TranslateButton + if size == .selected { + TranslateView(damus_state: damus_state, event: event, size: size) } if show_images && artifacts.images.count > 0 { @@ -138,7 +46,7 @@ struct NoteContentView: View { .cornerRadius(10) } if artifacts.invoices.count > 0 { - InvoicesView(our_pubkey: keypair.pubkey, invoices: artifacts.invoices) + InvoicesView(our_pubkey: damus_state.keypair.pubkey, invoices: artifacts.invoices) } if let preview = self.preview, show_images { @@ -157,16 +65,16 @@ struct NoteContentView: View { var body: some View { MainContent() .onAppear() { - self.artifacts = render_note_content(ev: event, profiles: profiles, privkey: keypair.privkey) + self.artifacts = render_note_content(ev: event, profiles: damus_state.profiles, privkey: damus_state.keypair.privkey) } .onReceive(handle_notify(.profile_updated)) { notif in let profile = notif.object as! ProfileUpdate - let blocks = event.blocks(keypair.privkey) + let blocks = event.blocks(damus_state.keypair.privkey) for block in blocks { switch block { case .mention(let m): if m.type == .pubkey && m.ref.ref_id == profile.pubkey { - self.artifacts = render_note_content(ev: event, profiles: profiles, privkey: keypair.privkey) + self.artifacts = render_note_content(ev: event, profiles: damus_state.profiles, privkey: damus_state.keypair.privkey) } case .text: return case .hashtag: return @@ -176,7 +84,7 @@ struct NoteContentView: View { } } .task { - if let preview = previews.lookup(self.event.id) { + if let preview = damus_state.previews.lookup(self.event.id) { switch preview { case .value(let view): self.preview = view @@ -190,54 +98,10 @@ struct NoteContentView: View { let meta = await getMetaData(for: artifacts.links.first!) let view = meta.map { LinkViewRepresentable(meta: .linkmeta($0)) } - previews.store(evid: self.event.id, preview: view) + damus_state.previews.store(evid: self.event.id, preview: view) self.preview = view } - if size == .selected && noteLanguage == nil && !checkingTranslationStatus && user_settings.libretranslate_url != "" { - checkingTranslationStatus = true - - if #available(iOS 16, *) { - currentLanguage = Locale.current.language.languageCode?.identifier ?? "en" - } else { - currentLanguage = Locale.current.languageCode ?? "en" - } - - // Rely on Apple's NLLanguageRecognizer to tell us which language it thinks the note is in. - noteLanguage = NLLanguageRecognizer.dominantLanguage(for: event.content)?.rawValue ?? currentLanguage - - if noteLanguage != currentLanguage { - // If the detected dominant language is a variant, remove the variant component and just take the language part as LibreTranslate typically only supports the variant-less language. - if #available(iOS 16, *) { - noteLanguage = Locale.LanguageCode(stringLiteral: noteLanguage!).identifier(.alpha2) - } else { - noteLanguage = Locale.canonicalLanguageIdentifier(from: noteLanguage!) - } - } - - if noteLanguage == nil { - noteLanguage = currentLanguage - translated_note = nil - } else if noteLanguage != currentLanguage { - do { - // If the note language is different from our language, send a translation request. - let translator = Translator(user_settings.libretranslate_url, apiKey: user_settings.libretranslate_api_key) - translated_note = try await translator.translate(event.content, from: noteLanguage!, to: currentLanguage) - } catch { - // If for whatever reason we're not able to figure out the language of the note, or translate the note, fail gracefully and do not retry. It's not the end of the world. Don't want to take down someone's translation server with an accidental denial of service attack. - noteLanguage = currentLanguage - translated_note = nil - } - } - - if translated_note != nil { - // Render translated note. - let blocks = event.get_blocks(content: translated_note!) - translated_artifacts = render_blocks(blocks: blocks, profiles: profiles, privkey: privkey) - } - - checkingTranslationStatus = false - } } } @@ -372,7 +236,7 @@ struct NoteContentView_Previews: PreviewProvider { let state = test_damus_state() let content = "hi there ¯\\_(ツ)_/¯ https://jb55.com/s/Oct12-150217.png 5739a762ef6124dd.jpg" let artifacts = NoteArtifacts(content: AttributedString(stringLiteral: content), images: [], invoices: [], links: []) - NoteContentView(privkey: "", event: NostrEvent(content: content, pubkey: "pk"), profiles: state.profiles, previews: PreviewCache(), show_images: true, artifacts: artifacts, size: .normal) + NoteContentView(damus_state: state, event: NostrEvent(content: content, pubkey: "pk"), show_images: true, artifacts: artifacts, size: .normal) } } @@ -385,3 +249,56 @@ extension View { .padding([.top, .bottom], 10) } } + +struct NoteArtifacts { + let content: AttributedString + let images: [URL] + let invoices: [Invoice] + let links: [URL] + + static func just_content(_ content: String) -> NoteArtifacts { + NoteArtifacts(content: AttributedString(stringLiteral: content), images: [], invoices: [], links: []) + } +} + +func render_note_content(ev: NostrEvent, profiles: Profiles, privkey: String?) -> NoteArtifacts { + let blocks = ev.blocks(privkey) + return render_blocks(blocks: blocks, profiles: profiles, privkey: privkey) +} + +func render_blocks(blocks: [Block], profiles: Profiles, privkey: String?) -> NoteArtifacts { + var invoices: [Invoice] = [] + var img_urls: [URL] = [] + var link_urls: [URL] = [] + let txt: AttributedString = blocks.reduce("") { str, block in + switch block { + case .mention(let m): + return str + mention_str(m, profiles: profiles) + case .text(let txt): + return str + AttributedString(stringLiteral: txt) + case .hashtag(let htag): + return str + hashtag_str(htag) + case .invoice(let invoice): + invoices.append(invoice) + return str + case .url(let url): + // Handle Image URLs + if is_image_url(url) { + // Append Image + img_urls.append(url) + return str + } else { + link_urls.append(url) + return str + url_str(url) + } + } + } + + return NoteArtifacts(content: txt, images: img_urls, invoices: invoices, links: link_urls) +} + +func is_image_url(_ url: URL) -> Bool { + let str = url.lastPathComponent.lowercased() + return str.hasSuffix("png") || str.hasSuffix("jpg") || str.hasSuffix("jpeg") || str.hasSuffix("gif") +} + diff --git a/damus/Views/PostButton.swift b/damus/Views/PostButton.swift index 4e220815..17e5155a 100644 --- a/damus/Views/PostButton.swift +++ b/damus/Views/PostButton.swift @@ -34,8 +34,7 @@ func PostButton(action: @escaping () -> ()) -> some View { .keyboardShortcut("n", modifiers: [.command, .shift]) } -func PostButtonContainer(userSettings: UserSettingsStore, action: @escaping () -> Void) -> some View { - let is_left_handed = userSettings.left_handed.self +func PostButtonContainer(is_left_handed: Bool, action: @escaping () -> Void) -> some View { return VStack { Spacer() diff --git a/damus/Views/ProfileView.swift b/damus/Views/ProfileView.swift index 4f40b01d..48082a92 100644 --- a/damus/Views/ProfileView.swift +++ b/damus/Views/ProfileView.swift @@ -116,7 +116,6 @@ struct ProfileView: View { @State var is_zoomed: Bool = false @State var show_share_sheet: Bool = false @State var action_sheet_presented: Bool = false - @EnvironmentObject var user_settings: UserSettingsStore @Environment(\.dismiss) var dismiss @Environment(\.colorScheme) var colorScheme @@ -142,10 +141,10 @@ struct ProfileView: View { func LNButton(lnurl: String, profile: Profile) -> some View { Button(action: { - if user_settings.show_wallet_selector { + if damus_state.settings.show_wallet_selector { showing_select_wallet = true } else { - open_with_wallet(wallet: user_settings.default_wallet.model, invoice: lnurl) + open_with_wallet(wallet: damus_state.settings.default_wallet.model, invoice: lnurl) } }) { Image(systemName: "bolt.circle") @@ -162,7 +161,6 @@ struct ProfileView: View { .cornerRadius(24) .sheet(isPresented: $showing_select_wallet, onDismiss: {showing_select_wallet = false}) { SelectWalletView(showingSelectWallet: $showing_select_wallet, our_pubkey: damus_state.pubkey, invoice: lnurl) - .environmentObject(user_settings) } } @@ -409,7 +407,7 @@ struct ProfileView_Previews: PreviewProvider { func test_damus_state() -> DamusState { let pubkey = "3efdaebb1d8923ebd99c9e7ace3b4194ab45512e2be79c1b7d68d9243e0d2681" - let damus = DamusState(pool: RelayPool(), keypair: Keypair(pubkey: pubkey, privkey: "privkey"), likes: EventCounter(our_pubkey: pubkey), boosts: EventCounter(our_pubkey: pubkey), contacts: Contacts(our_pubkey: pubkey), tips: TipCounter(our_pubkey: pubkey), profiles: Profiles(), dms: DirectMessagesModel(our_pubkey: pubkey), previews: PreviewCache(), zaps: Zaps(our_pubkey: pubkey), lnurls: LNUrls()) + let damus = DamusState.empty let prof = Profile(name: "damus", display_name: "damus", about: "iOS app!", picture: "https://damus.io/img/logo.png", banner: "", website: "https://damus.io", lud06: nil, lud16: "jb55@sendsats.lol", nip05: "damus.io") let tsprof = TimestampedProfile(profile: prof, timestamp: 0) diff --git a/damus/Views/ReplyQuoteView.swift b/damus/Views/ReplyQuoteView.swift deleted file mode 100644 index 766d942d..00000000 --- a/damus/Views/ReplyQuoteView.swift +++ /dev/null @@ -1,65 +0,0 @@ -// -// SwiftUIView.swift -// damus -// -// Created by William Casarin on 2022-04-19. -// - -import SwiftUI - -struct ReplyQuoteView: View { - let keypair: Keypair - let quoter: NostrEvent - let event_id: String - let profiles: Profiles - let previews: PreviewCache - - @EnvironmentObject var thread: ThreadModel - - func MainContent(event: NostrEvent) -> some View { - HStack(alignment: .top) { - Rectangle() - .frame(width: 2) - .padding([.leading], 4) - .foregroundColor(.accentColor) - - VStack(alignment: .leading) { - HStack(alignment: .top) { - ProfilePicView(pubkey: event.pubkey, size: 16, highlight: .reply, profiles: profiles) - Text(Profile.displayName(profile: profiles.lookup(id: event.pubkey), pubkey: event.pubkey)) - .foregroundColor(.accentColor) - Text("\(format_relative_time(event.created_at))", comment: "Amount of time that has passed since reply quote event occurred.") - .foregroundColor(.gray) - } - - NoteContentView(keypair: keypair, event: event, profiles: profiles, previews: previews, show_images: false, artifacts: .just_content(event.content), size: .normal) - .font(.callout) - .foregroundColor(.accentColor) - - //Spacer() - } - //.border(Color.red) - } - //.border(Color.green) - } - - var body: some View { - Group { - if let event = thread.lookup(event_id) { - MainContent(event: event) - .padding(4) - .frame(maxWidth: .infinity, alignment: .leading) - .contentShape(Rectangle()) - } - } - } -} - -struct ReplyQuoteView_Previews: PreviewProvider { - static var previews: some View { - let s = test_damus_state() - let quoter = NostrEvent(content: "a\nb\nc", pubkey: "pubkey") - ReplyQuoteView(keypair: s.keypair, quoter: quoter, event_id: "pubkey2", profiles: s.profiles, previews: PreviewCache()) - .environmentObject(ThreadModel(event: quoter, damus_state: s)) - } -} diff --git a/damus/Views/SideMenuView.swift b/damus/Views/SideMenuView.swift index 9d8c7dc9..5e3c48ce 100644 --- a/damus/Views/SideMenuView.swift +++ b/damus/Views/SideMenuView.swift @@ -10,9 +10,7 @@ import SwiftUI struct SideMenuView: View { let damus_state: DamusState @Binding var isSidebarVisible: Bool - @State var confirm_logout: Bool = false - @EnvironmentObject var user_settings: UserSettingsStore @State private var showQRCode = false @@ -121,7 +119,7 @@ struct SideMenuView: View { .foregroundColor(textColor()) } - NavigationLink(destination: ConfigView(state: damus_state).environmentObject(user_settings)) { + NavigationLink(destination: ConfigView(state: damus_state)) { Label(NSLocalizedString("Settings", comment: "Sidebar menu label for accessing the app settings"), systemImage: "gear") .font(.title2) .foregroundColor(textColor())