diff --git a/damus/Components/TranslateView.swift b/damus/Components/TranslateView.swift index c44c0f89..38085580 100644 --- a/damus/Components/TranslateView.swift +++ b/damus/Components/TranslateView.swift @@ -64,7 +64,7 @@ struct TranslateView: View { guard let note_language = translations_model.note_language else { return } - let res = await translate_note(profiles: damus_state.profiles, keypair: damus_state.keypair, event: event, settings: damus_state.settings, note_lang: note_language) + let res = await translate_note(profiles: damus_state.profiles, keypair: damus_state.keypair, event: event, settings: damus_state.settings, note_lang: note_language, purple: damus_state.purple) DispatchQueue.main.async { self.translations_model.state = res } @@ -125,10 +125,10 @@ struct TranslateView_Previews: PreviewProvider { } } -func translate_note(profiles: Profiles, keypair: Keypair, event: NostrEvent, settings: UserSettingsStore, note_lang: String) async -> TranslateStatus { +func translate_note(profiles: Profiles, keypair: Keypair, event: NostrEvent, settings: UserSettingsStore, note_lang: String, purple: DamusPurple) async -> TranslateStatus { // If the note language is different from our preferred languages, send a translation request. - let translator = Translator(settings) + let translator = Translator(settings, purple: purple) let originalContent = event.get_content(keypair) let translated_note = try? await translator.translate(originalContent, from: note_lang, to: current_language()) diff --git a/damus/Models/Purple/DamusPurple.swift b/damus/Models/Purple/DamusPurple.swift index ce84996c..93a25cea 100644 --- a/damus/Models/Purple/DamusPurple.swift +++ b/damus/Models/Purple/DamusPurple.swift @@ -126,6 +126,36 @@ class DamusPurple: StoreObserverDelegate { } } } + + func translate(text: String, source source_language: String, target target_language: String) async throws -> String { + var url = environment.get_base_url() + url.append(path: "/translate") + url.append(queryItems: [ + .init(name: "source", value: source_language), + .init(name: "target", value: target_language), + .init(name: "q", value: text) + ]) + let (data, response) = try await make_nip98_authenticated_request( + method: .get, + url: url, + payload: nil, + payload_type: nil, + auth_keypair: self.keypair + ) + + if let httpResponse = response as? HTTPURLResponse { + switch httpResponse.statusCode { + case 200: + return try JSONDecoder().decode(TranslationResult.self, from: data).text + default: + Log.error("Translation error with Damus Purple. HTTP status code: %d; Response: %s", for: .damus_purple, httpResponse.statusCode, String(data: data, encoding: .utf8) ?? "Unknown") + throw PurpleError.translation_error(status_code: httpResponse.statusCode, response: data) + } + } + else { + throw PurpleError.translation_no_response + } + } } // MARK: API types @@ -155,4 +185,13 @@ extension DamusPurple { } } } + + enum PurpleError: Error { + case translation_error(status_code: Int, response: Data) + case translation_no_response + } + + struct TranslationResult: Codable { + let text: String + } } diff --git a/damus/Models/TranslationService.swift b/damus/Models/TranslationService.swift index 896540d2..f2aab409 100644 --- a/damus/Models/TranslationService.swift +++ b/damus/Models/TranslationService.swift @@ -29,6 +29,7 @@ enum TranslationService: String, CaseIterable, Identifiable, StringCodable { } case none + case purple case libretranslate case deepl case nokyctranslate @@ -38,6 +39,8 @@ enum TranslationService: String, CaseIterable, Identifiable, StringCodable { switch self { case .none: return .init(tag: self.rawValue, displayName: NSLocalizedString("none_translation_service", value: "None", comment: "Dropdown option for selecting no translation service.")) + case .purple: + return .init(tag: self.rawValue, displayName: NSLocalizedString("Damus Purple", comment: "Dropdown option for selecting Damus Purple as a translation service.")) case .libretranslate: return .init(tag: self.rawValue, displayName: NSLocalizedString("LibreTranslate (Open Source)", comment: "Dropdown option for selecting LibreTranslate as the translation service.")) case .deepl: diff --git a/damus/Models/UserSettingsStore.swift b/damus/Models/UserSettingsStore.swift index 74a00e14..f4d1b259 100644 --- a/damus/Models/UserSettingsStore.swift +++ b/damus/Models/UserSettingsStore.swift @@ -296,6 +296,8 @@ class UserSettingsStore: ObservableObject { switch translation_service { case .none: return false + case .purple: + return true case .libretranslate: return URLComponents(string: libretranslate_url) != nil case .deepl: diff --git a/damus/Util/EventCache.swift b/damus/Util/EventCache.swift index 82bb6466..bc502ce5 100644 --- a/damus/Util/EventCache.swift +++ b/damus/Util/EventCache.swift @@ -306,7 +306,6 @@ func should_translate(event: NostrEvent, our_keypair: Keypair, settings: UserSet } func should_preload_translation(event: NostrEvent, our_keypair: Keypair, current_status: TranslateStatus, settings: UserSettingsStore, note_lang: String?) -> Bool { - switch current_status { case .havent_tried: return should_translate(event: event, our_keypair: our_keypair, settings: settings, note_lang: note_lang) && settings.auto_translate @@ -445,7 +444,7 @@ func preload_event(plan: PreloadPlan, state: DamusState) async { // We have to recheck should_translate here now that we have note_language if plan.load_translations && should_translate(event: plan.event, our_keypair: our_keypair, settings: settings, note_lang: note_language) && settings.auto_translate { - translations = await translate_note(profiles: profiles, keypair: our_keypair, event: plan.event, settings: settings, note_lang: note_language) + translations = await translate_note(profiles: profiles, keypair: our_keypair, event: plan.event, settings: settings, note_lang: note_language, purple: state.purple) } let ts = translations diff --git a/damus/Util/Router.swift b/damus/Util/Router.swift index ac6731e7..292f90bd 100644 --- a/damus/Util/Router.swift +++ b/damus/Util/Router.swift @@ -81,7 +81,7 @@ enum Route: Hashable { case .ZapSettings(let settings): ZapSettingsView(settings: settings) case .TranslationSettings(let settings): - TranslationSettingsView(settings: settings) + TranslationSettingsView(settings: settings, damus_state: damusState) case .ReactionsSettings(let settings): ReactionsSettingsView(settings: settings) case .SearchSettings(let settings): diff --git a/damus/Util/Translator.swift b/damus/Util/Translator.swift index 3b734f48..1681e665 100644 --- a/damus/Util/Translator.swift +++ b/damus/Util/Translator.swift @@ -12,16 +12,20 @@ import FoundationNetworking public struct Translator { private let userSettingsStore: UserSettingsStore + private let purple: DamusPurple private let session = URLSession.shared private let encoder = JSONEncoder() private let decoder = JSONDecoder() - init(_ userSettingsStore: UserSettingsStore) { + init(_ userSettingsStore: UserSettingsStore, purple: DamusPurple) { self.userSettingsStore = userSettingsStore + self.purple = purple } public func translate(_ text: String, from sourceLanguage: String, to targetLanguage: String) async throws -> String? { switch userSettingsStore.translation_service { + case .purple: + return try await translateWithPurple(text, from: sourceLanguage, to: targetLanguage) case .libretranslate: return try await translateWithLibreTranslate(text, from: sourceLanguage, to: targetLanguage) case .nokyctranslate: @@ -90,6 +94,10 @@ public struct Translator { return response.translations.map { $0.text }.joined(separator: " ") } + private func translateWithPurple(_ text: String, from sourceLanguage: String, to targetLanguage: String) async throws -> String? { + return try await self.purple.translate(text: text, source: sourceLanguage, target: targetLanguage) + } + private func translateWithNoKYCTranslate(_ text: String, from sourceLanguage: String, to targetLanguage: String) async throws -> String? { let url = try makeURL("https://translate.nokyctranslate.com", path: "/translate") diff --git a/damus/Views/Purple/DamusPurpleView.swift b/damus/Views/Purple/DamusPurpleView.swift index c524aaff..bd101c38 100644 --- a/damus/Views/Purple/DamusPurpleView.swift +++ b/damus/Views/Purple/DamusPurpleView.swift @@ -42,7 +42,7 @@ struct PurchasedProduct { } struct DamusPurpleView: View { - let purple_api: DamusPurple + let damus_state: DamusState let keypair: Keypair @State var products: ProductState @@ -50,13 +50,14 @@ struct DamusPurpleView: View { @State var selection: DamusPurpleType = .yearly @State var show_welcome_sheet: Bool = false @State var show_manage_subscriptions = false + @State var show_settings_change_confirmation_dialog = false @Environment(\.dismiss) var dismiss - init(purple: DamusPurple, keypair: Keypair) { + init(damus_state: DamusState) { self._products = State(wrappedValue: .loading) - self.purple_api = purple - self.keypair = keypair + self.damus_state = damus_state + self.keypair = damus_state.keypair } var body: some View { @@ -94,12 +95,38 @@ struct DamusPurpleView: View { await load_products() } .ignoresSafeArea(.all) - .sheet(isPresented: $show_welcome_sheet, content: { + .sheet(isPresented: $show_welcome_sheet, onDismiss: { + update_user_settings_to_purple() + }, content: { DamusPurpleWelcomeView() }) + .confirmationDialog( + NSLocalizedString("It seems that you already have a translation service configured. Would you like to switch to Damus Purple as your translator?", comment: "Confirmation dialog question asking users if they want their translation settings to be automatically switched to the Damus Purple translation service"), + isPresented: $show_settings_change_confirmation_dialog, + titleVisibility: .visible + ) { + Button("Yes") { + set_translation_settings_to_purple() + }.keyboardShortcut(.defaultAction) + Button("No", role: .cancel) {} + } .manageSubscriptionsSheet(isPresented: $show_manage_subscriptions) } + func update_user_settings_to_purple() { + if damus_state.settings.translation_service == .none { + set_translation_settings_to_purple() + } + else { + show_settings_change_confirmation_dialog = true + } + } + + func set_translation_settings_to_purple() { + damus_state.settings.translation_service = .purple + damus_state.settings.auto_translate = true + } + func handle_transactions(products: [Product]) async { for await update in StoreKit.Transaction.updates { switch update { @@ -203,9 +230,9 @@ struct DamusPurpleView: View { switch result { case .success: - self.purple_api.starred_profiles_cache[keypair.pubkey] = nil + self.damus_state.purple.starred_profiles_cache[keypair.pubkey] = nil Task { - await self.purple_api.send_receipt() + await self.damus_state.purple.send_receipt() } default: break @@ -423,6 +450,6 @@ struct DamusPurpleView_Previews: PreviewProvider { ]) */ - DamusPurpleView(purple: test_damus_state.purple, keypair: test_damus_state.keypair) + DamusPurpleView(damus_state: test_damus_state) } } diff --git a/damus/Views/Settings/TranslationSettingsView.swift b/damus/Views/Settings/TranslationSettingsView.swift index be4699f4..b2733294 100644 --- a/damus/Views/Settings/TranslationSettingsView.swift +++ b/damus/Views/Settings/TranslationSettingsView.swift @@ -9,6 +9,7 @@ import SwiftUI struct TranslationSettingsView: View { @ObservedObject var settings: UserSettingsStore + var damus_state: DamusState @Environment(\.dismiss) var dismiss @@ -19,11 +20,17 @@ struct TranslationSettingsView: View { .toggleStyle(.switch) Picker(NSLocalizedString("Service", comment: "Prompt selection of translation service provider."), selection: $settings.translation_service) { - ForEach(TranslationService.allCases, id: \.self) { server in + ForEach(TranslationService.allCases.filter({ settings.enable_experimental_purple_api ? true : $0 != .purple }), id: \.self) { server in Text(server.model.displayName) .tag(server.model.tag) } } + + if settings.translation_service == .purple && settings.enable_experimental_purple_api { + NavigationLink(destination: DamusPurpleView(damus_state: damus_state)) { + Text(NSLocalizedString("Configure Damus Purple", comment: "Button to allow Damus Purple to be configured")) + } + } if settings.translation_service == .libretranslate { Picker(NSLocalizedString("Server", comment: "Prompt selection of LibreTranslate server to perform machine translations on notes"), selection: $settings.libretranslate_server) { @@ -103,6 +110,6 @@ struct TranslationSettingsView: View { struct TranslationSettingsView_Previews: PreviewProvider { static var previews: some View { - TranslationSettingsView(settings: UserSettingsStore()) + TranslationSettingsView(settings: UserSettingsStore(), damus_state: test_damus_state) } } diff --git a/damus/Views/SideMenuView.swift b/damus/Views/SideMenuView.swift index b47b0c83..4b2bbfc0 100644 --- a/damus/Views/SideMenuView.swift +++ b/damus/Views/SideMenuView.swift @@ -55,7 +55,7 @@ struct SideMenuView: View { } if damus_state.settings.enable_experimental_purple_api { - NavigationLink(destination: DamusPurpleView(purple: damus_state.purple, keypair: damus_state.keypair)) { + NavigationLink(destination: DamusPurpleView(damus_state: damus_state)) { HStack(spacing: 13) { Image("nostr-hashtag") Text("Purple")