Skip to content

Commit

Permalink
Hook up Damus Purple translation service
Browse files Browse the repository at this point in the history
This commit integrates the Damus Purple translation service:
- Automatically handles translation settings change after purchase
- Asks for permission to override translation settings if the user already has translation setup
- Translation settings can be changed with Damus Purple, if desired
- Translation requests working with the Damus API server

Testing
--------

PASS

Device: iPhone 15 simulator
iOS: 17.2
Damus: This commit
Damus Purple API server: `9397201d7d55ddcec4c18fcd337f759b61dce697` running on Ubuntu 22.04 LTS VM (npm run dev)
iOS setting: English set as the only preferred language.
Steps:
1. Enable Damus Purple feature flag on developer settings, set purple localhost mode, and restart app
2. Set translation setting to something other than none (e.g. DeepL)
3. Simulate Damus Purple purchase
4. Check that when dismissing welcome view, a confirmation prompt will ask the user whether they want to switch translator to Damus Purple. PASS
5. Click "Yes".
6. Go to translation settings. Check that translation settings are set to "Purple". PASS
7. Go to a non-English profile. Check that translations appear with "Mock translation" (Which is the translation text provided by the mock translation server). PASS
8. Reinstall app
9. Repeat the test, but this time starting with no translation settings. Make sure that translation settings will automatically switch to Damus Purple. PASS

Feature flag testing
--------------------

PASS

Preconditions: Same as above
Steps:
1. Turn off translation
2. Turn off Damus Purple feature flag
3. Go to translation settings. Make sure that Damus Purple is not an option. PASS

Closes: #1836
Signed-off-by: Daniel D’Aquino <[email protected]>
Reviewed-by: William Casarin <[email protected]>
Signed-off-by: William Casarin <[email protected]>
  • Loading branch information
danieldaquino authored and jb55 committed Jan 1, 2024
1 parent 39b6dfb commit 9a54707
Show file tree
Hide file tree
Showing 10 changed files with 103 additions and 18 deletions.
6 changes: 3 additions & 3 deletions damus/Components/TranslateView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand Down Expand Up @@ -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())

Expand Down
39 changes: 39 additions & 0 deletions damus/Models/Purple/DamusPurple.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
}
}
3 changes: 3 additions & 0 deletions damus/Models/TranslationService.swift
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ enum TranslationService: String, CaseIterable, Identifiable, StringCodable {
}

case none
case purple
case libretranslate
case deepl
case nokyctranslate
Expand All @@ -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:
Expand Down
2 changes: 2 additions & 0 deletions damus/Models/UserSettingsStore.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
3 changes: 1 addition & 2 deletions damus/Util/EventCache.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion damus/Util/Router.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down
10 changes: 9 additions & 1 deletion damus/Util/Translator.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -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")

Expand Down
43 changes: 35 additions & 8 deletions damus/Views/Purple/DamusPurpleView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -42,21 +42,22 @@ struct PurchasedProduct {
}

struct DamusPurpleView: View {
let purple_api: DamusPurple
let damus_state: DamusState
let keypair: Keypair

@State var products: ProductState
@State var purchased: PurchasedProduct? = nil
@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 {
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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)
}
}
11 changes: 9 additions & 2 deletions damus/Views/Settings/TranslationSettingsView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import SwiftUI

struct TranslationSettingsView: View {
@ObservedObject var settings: UserSettingsStore
var damus_state: DamusState

@Environment(\.dismiss) var dismiss

Expand All @@ -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) {
Expand Down Expand Up @@ -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)
}
}
2 changes: 1 addition & 1 deletion damus/Views/SideMenuView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down

0 comments on commit 9a54707

Please sign in to comment.