damus

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

commit c5341ba3378ca6b17589cfdd16fd369863f109d5
parent 95fb7bccf8c8dff5f9f4b517bdb45a2cc80561f0
Author: William Casarin <jb55@jb55.com>
Date:   Thu,  6 Apr 2023 10:15:42 -0700

Cache translations, fix translation popping

Completely refactor Translate View. Simplify bool logic into a state enum.

Changelog-Fixed: Fix translation text popping
Changelog-Added: Cache translations

Diffstat:
Mdamus/Components/TranslateView.swift | 190++++++++++++++++++++++++++++++++++++++++++++++++-------------------------------
Mdamus/Util/EventCache.swift | 20++++++++++++++++++++
Mdamus/Views/NoteContentView.swift | 6+++++-
3 files changed, 141 insertions(+), 75 deletions(-)

diff --git a/damus/Components/TranslateView.swift b/damus/Components/TranslateView.swift @@ -8,109 +8,151 @@ import SwiftUI import NaturalLanguage + +struct Translated: Equatable { + let artifacts: NoteArtifacts + let language: String +} + +enum TranslateStatus: Equatable { + case havent_tried + case trying + case translated(Translated) + case not_needed +} + struct TranslateView: View { let damus_state: DamusState let event: NostrEvent let size: EventViewKind + let currentLanguage: String + + @State var translated: TranslateStatus + + init(damus_state: DamusState, event: NostrEvent, size: EventViewKind) { + self.damus_state = damus_state + self.event = event + self.size = size + + if #available(iOS 16, *) { + self.currentLanguage = Locale.current.language.languageCode?.identifier ?? "en" + } else { + self.currentLanguage = Locale.current.languageCode ?? "en" + } + + if let cached = damus_state.events.lookup_translated_artifacts(evid: event.id) { + self._translated = State(initialValue: cached) + } else { + let initval: TranslateStatus = self.damus_state.settings.auto_translate ? .trying : .havent_tried + self._translated = State(initialValue: initval) + } + } - @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 - let preferredLanguages = Set(Locale.preferredLanguages.map { localeToLanguage($0) }) var TranslateButton: some View { Button(NSLocalizedString("Translate Note", comment: "Button to translate note from different language.")) { - show_translated_note = true + self.translated = .trying } .translate_button_style() } - func Translated(lang: String, artifacts: NoteArtifacts) -> some View { - return Group { - Button(String(format: NSLocalizedString("Translated from %@", comment: "Button to indicate that the note has been translated from a different language."), lang)) { - show_translated_note = false - } - .translate_button_style() + func TranslatedView(lang: String?, artifacts: NoteArtifacts) -> some View { + return VStack(alignment: .leading) { + Text(String(format: NSLocalizedString("Translated from %@", comment: "Button to indicate that the note has been translated from a different language."), lang ?? "ja")) + .foregroundColor(.gray) + .font(.footnote) + .padding([.top, .bottom], 10) - SelectableText(attributedString: artifacts.content, size: self.size) + if self.size == .selected { + SelectableText(attributedString: artifacts.content, size: self.size) + } else { + Text(artifacts.content) + .font(eventviewsize_to_font(self.size)) + } } } - func MainContent(note_lang: String) -> some View { - return Group { - let languageName = Locale.current.localizedString(forLanguageCode: note_lang) - if let languageName, let translated_artifacts, show_translated_note { - Translated(lang: languageName, artifacts: translated_artifacts) - } else if !damus_state.settings.auto_translate { - TranslateButton - } else { - Text("") - } + func failed_attempt() { + self.translated = .not_needed + damus_state.events.store_translation_artifacts(evid: event.id, translated: .not_needed) + } + + func attempt_translation() async { + guard case .trying = translated else { + return + } + + guard damus_state.settings.can_translate(damus_state.pubkey) else { + return + } + + let note_lang = event.note_language(damus_state.keypair.privkey) ?? currentLanguage + + // Don't translate if its in our preferred languages + guard !preferredLanguages.contains(note_lang) else { + failed_attempt() + return } + + // If the note language is different from our preferred languages, send a translation request. + let translator = Translator(damus_state.settings) + let originalContent = event.get_content(damus_state.keypair.privkey) + let translated_note = try? await translator.translate(originalContent, from: note_lang, to: currentLanguage) + + guard let translated_note else { + // if its the same, give up and don't retry + failed_attempt() + return + } + + guard originalContent != translated_note else { + // if its the same, give up and don't retry + failed_attempt() + return + } + + // Render translated note + let translated_blocks = event.get_blocks(content: translated_note) + let artifacts = render_blocks(blocks: translated_blocks, profiles: damus_state.profiles, privkey: damus_state.keypair.privkey) + + // and cache it + self.translated = .translated(Translated(artifacts: artifacts, language: note_lang)) + damus_state.events.store_translation_artifacts(evid: event.id, translated: self.translated) } var body: some View { Group { - if let note_lang = noteLanguage, noteLanguage != currentLanguage { - MainContent(note_lang: note_lang) - } else { + switch translated { + case .havent_tried: + if damus_state.settings.auto_translate { + Text("") + } else { + TranslateButton + } + case .trying: + Text("Translating...") + .foregroundColor(.gray) + .font(.footnote) + .padding([.top, .bottom], 10) + case .translated(let translated): + let languageName = Locale.current.localizedString(forLanguageCode: translated.language) + TranslatedView(lang: languageName, artifacts: translated.artifacts) + case .not_needed: Text("") } } - .task { - guard noteLanguage == nil && !checkingTranslationStatus && damus_state.settings.can_translate(damus_state.pubkey) else { - return - } - - checkingTranslationStatus = true - - if #available(iOS 16, *) { - currentLanguage = Locale.current.language.languageCode?.identifier ?? "en" - } else { - currentLanguage = Locale.current.languageCode ?? "en" - } - - noteLanguage = event.note_language(damus_state.keypair.privkey) ?? currentLanguage - - guard let note_lang = noteLanguage else { - noteLanguage = currentLanguage - translated_note = nil - checkingTranslationStatus = false + .onChange(of: translated) { val in + guard case .trying = translated else { return } - if !preferredLanguages.contains(note_lang) { - do { - // If the note language is different from our preferred languages, send a translation request. - let translator = Translator(damus_state.settings) - let originalContent = event.get_content(damus_state.keypair.privkey) - translated_note = try await translator.translate(originalContent, from: note_lang, to: currentLanguage) - - if originalContent == translated_note { - // If the translation is the same as the original, don't bother showing it. - noteLanguage = currentLanguage - translated_note = nil - } - } 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 - } + Task { + await attempt_translation() } - - if let translated_note { - // Render translated note. - let translated_blocks = event.get_blocks(content: translated_note) - translated_artifacts = render_blocks(blocks: translated_blocks, profiles: damus_state.profiles, privkey: damus_state.keypair.privkey) - } - - checkingTranslationStatus = false - - show_translated_note = damus_state.settings.auto_translate + } + .task { + await attempt_translation() } } } diff --git a/damus/Util/EventCache.swift b/damus/Util/EventCache.swift @@ -13,6 +13,8 @@ class EventCache { private var events: [String: NostrEvent] = [:] private var replies = ReplyMap() private var cancellable: AnyCancellable? + private var translations: [String: TranslateStatus] = [:] + private var artifacts: [String: NoteArtifacts] = [:] //private var thread_latest: [String: Int64] @@ -24,6 +26,22 @@ class EventCache { } } + func store_translation_artifacts(evid: String, translated: TranslateStatus) { + self.translations[evid] = translated + } + + func store_artifacts(evid: String, artifacts: NoteArtifacts) { + self.artifacts[evid] = artifacts + } + + func lookup_artifacts(evid: String) -> NoteArtifacts? { + return self.artifacts[evid] + } + + func lookup_translated_artifacts(evid: String) -> TranslateStatus? { + return self.translations[evid] + } + func parent_events(event: NostrEvent) -> [NostrEvent] { var parents: [NostrEvent] = [] @@ -87,6 +105,8 @@ class EventCache { private func prune() { events = [:] + translations = [:] + artifacts = [:] replies.replies = [:] } } diff --git a/damus/Views/NoteContentView.swift b/damus/Views/NoteContentView.swift @@ -234,7 +234,11 @@ struct NoteContentView_Previews: PreviewProvider { } } -struct NoteArtifacts { +struct NoteArtifacts: Equatable { + static func == (lhs: NoteArtifacts, rhs: NoteArtifacts) -> Bool { + return lhs.content == rhs.content + } + let content: AttributedString let images: [URL] let invoices: [Invoice]