damus

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

commit 1696e0365e228359d105f4bb75b179981c3dc724
parent 006f8d79e02f43b2bba555c397335ead274da546
Author: William Casarin <jb55@jb55.com>
Date:   Fri,  3 Feb 2023 09:25:07 -0800

refactor: settings and translation view

Diffstat:
Mdamus.xcodeproj/project.pbxproj | 12++++++------
Adamus/Components/TranslateView.swift | 138+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mdamus/ContentView.swift | 7+++----
Mdamus/Models/DamusState.swift | 3++-
Mdamus/Models/Mentions.swift | 2+-
Mdamus/Models/UserSettingsStore.swift | 31+++++++++++++++++++++++--------
Mdamus/Views/ChatView.swift | 9++++++++-
Mdamus/Views/ConfigView.swift | 26++++++++++++++------------
Mdamus/Views/DMView.swift | 2+-
Mdamus/Views/Events/EventBody.swift | 2+-
Mdamus/Views/NoteContentView.swift | 211++++++++++++++++++++++++-------------------------------------------------------
Mdamus/Views/PostButton.swift | 3+--
Mdamus/Views/ProfileView.swift | 8+++-----
Ddamus/Views/ReplyQuoteView.swift | 65-----------------------------------------------------------------
Mdamus/Views/SideMenuView.swift | 4+---
15 files changed, 266 insertions(+), 257 deletions(-)

diff --git 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 = "<group>"; }; 4C0A3F90280F6528000448DE /* ChatView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatView.swift; sourceTree = "<group>"; }; 4C0A3F92280F66F5000448DE /* ReplyMap.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReplyMap.swift; sourceTree = "<group>"; }; - 4C0A3F94280F6C78000448DE /* ReplyQuoteView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReplyQuoteView.swift; sourceTree = "<group>"; }; 4C0A3F96280F8E02000448DE /* ThreadView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ThreadView.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>"; }; @@ -361,6 +360,7 @@ 4C3EA67A28FF7B3900C48A62 /* InvoiceTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InvoiceTests.swift; sourceTree = "<group>"; }; 4C3EA67C28FFBBA200C48A62 /* InvoicesView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InvoicesView.swift; sourceTree = "<group>"; }; 4C3EA67E28FFC01D00C48A62 /* InvoiceView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InvoiceView.swift; sourceTree = "<group>"; }; + 4C42812B298C848200DBF26F /* TranslateView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TranslateView.swift; sourceTree = "<group>"; }; 4C477C9D282C3A4800033AA3 /* TipCounter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TipCounter.swift; sourceTree = "<group>"; }; 4C4A3A5A288A1B2200453788 /* damus.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = damus.entitlements; sourceTree = "<group>"; }; 4C5C7E67284ED36500A22DF5 /* SearchHomeModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchHomeModel.swift; sourceTree = "<group>"; }; @@ -414,6 +414,7 @@ 4CB883AF297705DD00DC99E7 /* ZapButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ZapButton.swift; sourceTree = "<group>"; }; 4CB883B5297730E400DC99E7 /* LNUrls.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LNUrls.swift; sourceTree = "<group>"; }; 4CBCA92F297DB57F00EC6B2F /* WebsiteLink.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WebsiteLink.swift; sourceTree = "<group>"; }; + 4CC7AAE6297EFA7B00430951 /* Zap.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Zap.swift; sourceTree = "<group>"; }; 4CC7AAEA297F0AEC00430951 /* BuilderEventView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BuilderEventView.swift; sourceTree = "<group>"; }; 4CC7AAEC297F0B9E00430951 /* Highlight.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Highlight.swift; sourceTree = "<group>"; }; 4CC7AAEF297F11C700430951 /* SelectedEventView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SelectedEventView.swift; sourceTree = "<group>"; }; @@ -422,7 +423,6 @@ 4CC7AAF5297F1A6A00430951 /* EventBody.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EventBody.swift; sourceTree = "<group>"; }; 4CC7AAF7297F1CEE00430951 /* EventProfile.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EventProfile.swift; sourceTree = "<group>"; }; 4CC7AAF9297F64AC00430951 /* EventMenu.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EventMenu.swift; sourceTree = "<group>"; }; - 4CC7AAE6297EFA7B00430951 /* Zap.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Zap.swift; sourceTree = "<group>"; }; 4CD7641A28A1641400B6928F /* EndBlock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EndBlock.swift; sourceTree = "<group>"; }; 4CE4F8CC281352B30009DFBB /* Notifications.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Notifications.swift; sourceTree = "<group>"; }; 4CE4F9DD2852768D00C00DD9 /* ConfigView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConfigView.swift; sourceTree = "<group>"; }; @@ -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 = "<group>"; @@ -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 @@ -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 @@ -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 @@ -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 @@ -52,7 +52,7 @@ struct LightningInvoice<T> { 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 @@ -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 @@ -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 @@ -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 @@ -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 @@ -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 @@ -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 @@ -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 @@ -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 @@ -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 @@ -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())