Add LibreTranslate integration for machine translating notes from other languages
This commit is contained in:
@@ -18,6 +18,7 @@
|
|||||||
3ACB685C297633BC00C46468 /* InfoPlist.strings in Resources */ = {isa = PBXBuildFile; fileRef = 3ACB685A297633BC00C46468 /* InfoPlist.strings */; };
|
3ACB685C297633BC00C46468 /* InfoPlist.strings in Resources */ = {isa = PBXBuildFile; fileRef = 3ACB685A297633BC00C46468 /* InfoPlist.strings */; };
|
||||||
3ACB685F297633BC00C46468 /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = 3ACB685D297633BC00C46468 /* Localizable.strings */; };
|
3ACB685F297633BC00C46468 /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = 3ACB685D297633BC00C46468 /* Localizable.strings */; };
|
||||||
3ACBCB78295FE5C70037388A /* TimeAgoTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3ACBCB77295FE5C70037388A /* TimeAgoTests.swift */; };
|
3ACBCB78295FE5C70037388A /* TimeAgoTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3ACBCB77295FE5C70037388A /* TimeAgoTests.swift */; };
|
||||||
|
3AE45AF6297BB2E700C1D842 /* LibreTranslateServer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3AE45AF5297BB2E700C1D842 /* LibreTranslateServer.swift */; };
|
||||||
4C06670128FC7C5900038D2A /* RelayView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C06670028FC7C5900038D2A /* RelayView.swift */; };
|
4C06670128FC7C5900038D2A /* RelayView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C06670028FC7C5900038D2A /* RelayView.swift */; };
|
||||||
4C06670428FC7EC500038D2A /* Kingfisher in Frameworks */ = {isa = PBXBuildFile; productRef = 4C06670328FC7EC500038D2A /* Kingfisher */; };
|
4C06670428FC7EC500038D2A /* Kingfisher in Frameworks */ = {isa = PBXBuildFile; productRef = 4C06670328FC7EC500038D2A /* Kingfisher */; };
|
||||||
4C06670628FCB08600038D2A /* ImageCarousel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C06670528FCB08600038D2A /* ImageCarousel.swift */; };
|
4C06670628FCB08600038D2A /* ImageCarousel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C06670528FCB08600038D2A /* ImageCarousel.swift */; };
|
||||||
@@ -236,6 +237,7 @@
|
|||||||
3ACB685B297633BC00C46468 /* es-419 */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "es-419"; path = "es-419.lproj/InfoPlist.strings"; sourceTree = "<group>"; };
|
3ACB685B297633BC00C46468 /* es-419 */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "es-419"; path = "es-419.lproj/InfoPlist.strings"; sourceTree = "<group>"; };
|
||||||
3ACB685E297633BC00C46468 /* es-419 */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "es-419"; path = "es-419.lproj/Localizable.strings"; sourceTree = "<group>"; };
|
3ACB685E297633BC00C46468 /* es-419 */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "es-419"; path = "es-419.lproj/Localizable.strings"; sourceTree = "<group>"; };
|
||||||
3ACBCB77295FE5C70037388A /* TimeAgoTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimeAgoTests.swift; sourceTree = "<group>"; };
|
3ACBCB77295FE5C70037388A /* TimeAgoTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimeAgoTests.swift; sourceTree = "<group>"; };
|
||||||
|
3AE45AF5297BB2E700C1D842 /* LibreTranslateServer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LibreTranslateServer.swift; sourceTree = "<group>"; };
|
||||||
3AEB8003297CCEA800713A25 /* tr-TR */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "tr-TR"; path = "tr-TR.lproj/InfoPlist.strings"; sourceTree = "<group>"; };
|
3AEB8003297CCEA800713A25 /* tr-TR */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "tr-TR"; path = "tr-TR.lproj/InfoPlist.strings"; sourceTree = "<group>"; };
|
||||||
3AEB8004297CCEA800713A25 /* tr-TR */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "tr-TR"; path = "tr-TR.lproj/Localizable.strings"; sourceTree = "<group>"; };
|
3AEB8004297CCEA800713A25 /* tr-TR */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "tr-TR"; path = "tr-TR.lproj/Localizable.strings"; sourceTree = "<group>"; };
|
||||||
3AEB8005297CCEA900713A25 /* tr-TR */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = "tr-TR"; path = "tr-TR.lproj/Localizable.stringsdict"; sourceTree = "<group>"; };
|
3AEB8005297CCEA900713A25 /* tr-TR */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = "tr-TR"; path = "tr-TR.lproj/Localizable.stringsdict"; sourceTree = "<group>"; };
|
||||||
@@ -582,6 +584,7 @@
|
|||||||
7C45AE70297353390031D7BC /* KFImageModel.swift */,
|
7C45AE70297353390031D7BC /* KFImageModel.swift */,
|
||||||
4CF0ABD32980996B00D66079 /* Report.swift */,
|
4CF0ABD32980996B00D66079 /* Report.swift */,
|
||||||
4CF0ABDD2981A69500D66079 /* MutelistModel.swift */,
|
4CF0ABDD2981A69500D66079 /* MutelistModel.swift */,
|
||||||
|
3AE45AF5297BB2E700C1D842 /* LibreTranslateServer.swift */,
|
||||||
);
|
);
|
||||||
path = Models;
|
path = Models;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
@@ -1171,6 +1174,7 @@
|
|||||||
4C06670B28FDE64700038D2A /* damus.c in Sources */,
|
4C06670B28FDE64700038D2A /* damus.c in Sources */,
|
||||||
4FE60CDD295E1C5E00105A1F /* Wallet.swift in Sources */,
|
4FE60CDD295E1C5E00105A1F /* Wallet.swift in Sources */,
|
||||||
3AA247FF297E3D900090C62D /* RepostsView.swift in Sources */,
|
3AA247FF297E3D900090C62D /* RepostsView.swift in Sources */,
|
||||||
|
3AE45AF6297BB2E700C1D842 /* LibreTranslateServer.swift in Sources */,
|
||||||
4C99737B28C92A9200E53835 /* ChatroomMetadata.swift in Sources */,
|
4C99737B28C92A9200E53835 /* ChatroomMetadata.swift in Sources */,
|
||||||
4CC7AAF4297F18B400430951 /* ReplyDescription.swift in Sources */,
|
4CC7AAF4297F18B400430951 /* ReplyDescription.swift in Sources */,
|
||||||
4C75EFA427FA577B0006080F /* PostView.swift in Sources */,
|
4C75EFA427FA577B0006080F /* PostView.swift in Sources */,
|
||||||
|
|||||||
@@ -34,7 +34,7 @@ struct InvoiceView: View {
|
|||||||
|
|
||||||
let invoice: Invoice
|
let invoice: Invoice
|
||||||
@State var showing_select_wallet: Bool = false
|
@State var showing_select_wallet: Bool = false
|
||||||
@ObservedObject var user_settings = UserSettingsStore()
|
@EnvironmentObject var user_settings: UserSettingsStore
|
||||||
|
|
||||||
var PayButton: some View {
|
var PayButton: some View {
|
||||||
Button {
|
Button {
|
||||||
|
|||||||
@@ -292,6 +292,7 @@ struct ContentView: View {
|
|||||||
.padding([.bottom], 8)
|
.padding([.bottom], 8)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
.environmentObject(user_settings)
|
||||||
.onAppear() {
|
.onAppear() {
|
||||||
self.connect()
|
self.connect()
|
||||||
//KingfisherManager.shared.cache.clearDiskCache()
|
//KingfisherManager.shared.cache.clearDiskCache()
|
||||||
|
|||||||
44
damus/Models/LibreTranslateServer.swift
Normal file
44
damus/Models/LibreTranslateServer.swift
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
//
|
||||||
|
// LibreTranslateServer.swift
|
||||||
|
// damus
|
||||||
|
//
|
||||||
|
// Created by Terry Yiu on 1/21/23.
|
||||||
|
//
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
|
||||||
|
enum LibreTranslateServer: String, CaseIterable, Identifiable {
|
||||||
|
var id: String { self.rawValue }
|
||||||
|
|
||||||
|
struct Model: Identifiable, Hashable {
|
||||||
|
var id: String { self.tag }
|
||||||
|
var tag: String
|
||||||
|
var displayName: String
|
||||||
|
var url: String?
|
||||||
|
}
|
||||||
|
|
||||||
|
case none
|
||||||
|
case argosopentech
|
||||||
|
case terraprint
|
||||||
|
case vern
|
||||||
|
case custom
|
||||||
|
|
||||||
|
var model: Model {
|
||||||
|
switch self {
|
||||||
|
case .none:
|
||||||
|
return .init(tag: self.rawValue, displayName: NSLocalizedString("None", comment: "Dropdown option for selecting no translation server."), url: nil)
|
||||||
|
case .argosopentech:
|
||||||
|
return .init(tag: self.rawValue, displayName: "translate.argosopentech.com", url: "https://translate.argosopentech.com")
|
||||||
|
case .terraprint:
|
||||||
|
return .init(tag: self.rawValue, displayName: "translate.terraprint.co", url: "https://translate.terraprint.co")
|
||||||
|
case .vern:
|
||||||
|
return .init(tag: self.rawValue, displayName: "lt.vern.cc", url: "https://lt.vern.cc")
|
||||||
|
case .custom:
|
||||||
|
return .init(tag: self.rawValue, displayName: NSLocalizedString("Custom", comment: "Dropdown option for selecting a custom translation server."), url: nil)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static var allModels: [Model] {
|
||||||
|
return Self.allCases.map { $0.model }
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -6,6 +6,7 @@
|
|||||||
//
|
//
|
||||||
|
|
||||||
import Foundation
|
import Foundation
|
||||||
|
import Vault
|
||||||
|
|
||||||
class UserSettingsStore: ObservableObject {
|
class UserSettingsStore: ObservableObject {
|
||||||
@Published var default_wallet: Wallet {
|
@Published var default_wallet: Wallet {
|
||||||
@@ -26,6 +27,44 @@ class UserSettingsStore: ObservableObject {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Published var libretranslate_server: LibreTranslateServer {
|
||||||
|
didSet {
|
||||||
|
if oldValue == libretranslate_server {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
UserDefaults.standard.set(libretranslate_server.rawValue, forKey: "libretranslate_server")
|
||||||
|
|
||||||
|
libretranslate_api_key = ""
|
||||||
|
|
||||||
|
if libretranslate_server == .custom || libretranslate_server == .none {
|
||||||
|
libretranslate_url = ""
|
||||||
|
} else {
|
||||||
|
libretranslate_url = libretranslate_server.model.url!
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Published var libretranslate_url: String {
|
||||||
|
didSet {
|
||||||
|
UserDefaults.standard.set(libretranslate_url, forKey: "libretranslate_url")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Published var libretranslate_api_key: String {
|
||||||
|
didSet {
|
||||||
|
do {
|
||||||
|
if libretranslate_api_key == "" {
|
||||||
|
try clearLibreTranslateApiKey()
|
||||||
|
} else {
|
||||||
|
try saveLibreTranslateApiKey(libretranslate_api_key)
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// No-op.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
init() {
|
init() {
|
||||||
if let defaultWalletName = UserDefaults.standard.string(forKey: "default_wallet"),
|
if let defaultWalletName = UserDefaults.standard.string(forKey: "default_wallet"),
|
||||||
let default_wallet = Wallet(rawValue: defaultWalletName)
|
let default_wallet = Wallet(rawValue: defaultWalletName)
|
||||||
@@ -37,5 +76,40 @@ class UserSettingsStore: ObservableObject {
|
|||||||
show_wallet_selector = UserDefaults.standard.object(forKey: "show_wallet_selector") as? Bool ?? true
|
show_wallet_selector = UserDefaults.standard.object(forKey: "show_wallet_selector") as? Bool ?? true
|
||||||
|
|
||||||
left_handed = UserDefaults.standard.object(forKey: "left_handed") as? Bool ?? false
|
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 ?? ""
|
||||||
|
} else {
|
||||||
|
// Note from @tyiu:
|
||||||
|
// Default server is disabled by default for now until we gain some confidence that it is working well in production.
|
||||||
|
// Instead of throwing all Damus users onto feature immediately, allow for discovery of feature organically.
|
||||||
|
// Also, we are connecting to servers listed as mirrors on the official LibreTranslate GitHub README that do not require API keys.
|
||||||
|
// However, we have not asked them for permission to use, so we're trying to be good neighbors for now.
|
||||||
|
// Opportunity: spin up dedicated trusted LibreTranslate server that requires an API key for any access (or higher rate limit access).
|
||||||
|
libretranslate_server = .none
|
||||||
|
libretranslate_url = ""
|
||||||
|
}
|
||||||
|
|
||||||
|
do {
|
||||||
|
libretranslate_api_key = try Vault.getPrivateKey(keychainConfiguration: DamusLibreTranslateKeychainConfiguration())
|
||||||
|
} catch {
|
||||||
|
libretranslate_api_key = ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func saveLibreTranslateApiKey(_ apiKey: String) throws {
|
||||||
|
try Vault.savePrivateKey(apiKey, keychainConfiguration: DamusLibreTranslateKeychainConfiguration())
|
||||||
|
}
|
||||||
|
|
||||||
|
func clearLibreTranslateApiKey() throws {
|
||||||
|
try Vault.deletePrivateKey(keychainConfiguration: DamusLibreTranslateKeychainConfiguration())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
struct DamusLibreTranslateKeychainConfiguration: KeychainConfiguration {
|
||||||
|
var serviceName = "damus"
|
||||||
|
var accessGroup: String? = nil
|
||||||
|
var accountName = "libretranslate_apikey"
|
||||||
|
}
|
||||||
|
|||||||
@@ -103,11 +103,15 @@ class NostrEvent: Codable, Identifiable, CustomStringConvertible, Equatable, Has
|
|||||||
if let bs = _blocks {
|
if let bs = _blocks {
|
||||||
return bs
|
return bs
|
||||||
}
|
}
|
||||||
let blocks = parse_mentions(content: self.get_content(privkey), tags: self.tags)
|
let blocks = get_blocks(content: self.get_content(privkey))
|
||||||
self._blocks = blocks
|
self._blocks = blocks
|
||||||
return blocks
|
return blocks
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func get_blocks(content: String) -> [Block] {
|
||||||
|
return parse_mentions(content: content, tags: self.tags)
|
||||||
|
}
|
||||||
|
|
||||||
lazy var inner_event: NostrEvent? = {
|
lazy var inner_event: NostrEvent? = {
|
||||||
// don't try to deserialize an inner event if we know there won't be one
|
// don't try to deserialize an inner event if we know there won't be one
|
||||||
if self.known_kind == .boost {
|
if self.known_kind == .boost {
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ struct ConfigView: View {
|
|||||||
@State var confirm_logout: Bool = false
|
@State var confirm_logout: Bool = false
|
||||||
@State var new_relay: String = ""
|
@State var new_relay: String = ""
|
||||||
@State var show_privkey: Bool = false
|
@State var show_privkey: Bool = false
|
||||||
|
@State var show_libretranslate_api_key: Bool = false
|
||||||
@State var privkey: String
|
@State var privkey: String
|
||||||
@State var privkey_copied: Bool = false
|
@State var privkey_copied: Bool = false
|
||||||
@State var pubkey_copied: Bool = false
|
@State var pubkey_copied: Bool = false
|
||||||
@@ -116,6 +117,39 @@ 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) {
|
||||||
|
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)
|
||||||
|
.disableAutocorrection(true)
|
||||||
|
.disabled(user_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)
|
||||||
|
.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)
|
||||||
|
.disableAutocorrection(true)
|
||||||
|
.autocapitalization(UITextAutocapitalizationType.none)
|
||||||
|
Button(NSLocalizedString("Show API Key", comment: "Button to hide the LibreTranslate server API key.")) {
|
||||||
|
show_libretranslate_api_key = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Section(NSLocalizedString("Left Handed", comment: "Moves the post button to the left side of the screen")) {
|
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: $user_settings.left_handed)
|
||||||
.toggleStyle(.switch)
|
.toggleStyle(.switch)
|
||||||
|
|||||||
@@ -8,6 +8,10 @@
|
|||||||
import SwiftUI
|
import SwiftUI
|
||||||
import LinkPresentation
|
import LinkPresentation
|
||||||
|
|
||||||
|
#if canImport(FoundationNetworking)
|
||||||
|
import FoundationNetworking
|
||||||
|
#endif
|
||||||
|
|
||||||
struct NoteArtifacts {
|
struct NoteArtifacts {
|
||||||
let content: AttributedString
|
let content: AttributedString
|
||||||
let images: [URL]
|
let images: [URL]
|
||||||
@@ -21,6 +25,10 @@ struct NoteArtifacts {
|
|||||||
|
|
||||||
func render_note_content(ev: NostrEvent, profiles: Profiles, privkey: String?) -> NoteArtifacts {
|
func render_note_content(ev: NostrEvent, profiles: Profiles, privkey: String?) -> NoteArtifacts {
|
||||||
let blocks = ev.blocks(privkey)
|
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 invoices: [Invoice] = []
|
||||||
var img_urls: [URL] = []
|
var img_urls: [URL] = []
|
||||||
var link_urls: [URL] = []
|
var link_urls: [URL] = []
|
||||||
@@ -47,7 +55,7 @@ func render_note_content(ev: NostrEvent, profiles: Profiles, privkey: String?) -
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return NoteArtifacts(content: txt, images: img_urls, invoices: invoices, links: link_urls)
|
return NoteArtifacts(content: txt, images: img_urls, invoices: invoices, links: link_urls)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -64,10 +72,18 @@ struct NoteContentView: View {
|
|||||||
|
|
||||||
let show_images: Bool
|
let show_images: Bool
|
||||||
|
|
||||||
|
@State var checkingTranslationStatus: Bool = false
|
||||||
|
@State var language: 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 artifacts: NoteArtifacts
|
||||||
|
|
||||||
@State var preview: LinkViewRepresentable? = nil
|
@State var preview: LinkViewRepresentable? = nil
|
||||||
let size: EventViewKind
|
let size: EventViewKind
|
||||||
|
|
||||||
|
@EnvironmentObject var user_settings: UserSettingsStore
|
||||||
|
|
||||||
func MainContent() -> some View {
|
func MainContent() -> some View {
|
||||||
return VStack(alignment: .leading) {
|
return VStack(alignment: .leading) {
|
||||||
@@ -75,6 +91,29 @@ struct NoteContentView: View {
|
|||||||
.font(eventviewsize_to_font(size))
|
.font(eventviewsize_to_font(size))
|
||||||
.fixedSize(horizontal: false, vertical: true)
|
.fixedSize(horizontal: false, vertical: true)
|
||||||
|
|
||||||
|
if size == .selected && language != nil && translated_artifacts != nil {
|
||||||
|
let languageName = Locale.current.localizedString(forLanguageCode: language!)
|
||||||
|
if show_translated_note {
|
||||||
|
Button(NSLocalizedString("Translated from \(languageName!)", comment: "Button to indicate that the note has been translated from a different language.")) {
|
||||||
|
show_translated_note = false
|
||||||
|
}
|
||||||
|
.font(.footnote)
|
||||||
|
.contentShape(Rectangle())
|
||||||
|
.padding(.top, 10)
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
.font(.footnote)
|
||||||
|
.contentShape(Rectangle())
|
||||||
|
.padding(.top, 10)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if show_images && artifacts.images.count > 0 {
|
if show_images && artifacts.images.count > 0 {
|
||||||
ImageCarousel(urls: artifacts.images)
|
ImageCarousel(urls: artifacts.images)
|
||||||
} else if !show_images && artifacts.images.count > 0 {
|
} else if !show_images && artifacts.images.count > 0 {
|
||||||
@@ -142,6 +181,35 @@ struct NoteContentView: View {
|
|||||||
previews.store(evid: self.event.id, preview: view)
|
previews.store(evid: self.event.id, preview: view)
|
||||||
self.preview = view
|
self.preview = view
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if size == .selected && language == nil && !checkingTranslationStatus && user_settings.libretranslate_url != "" {
|
||||||
|
checkingTranslationStatus = true
|
||||||
|
|
||||||
|
let currentLanguage = Locale.current.languageCode ?? "en"
|
||||||
|
let translator = Translator(user_settings.libretranslate_url, apiKey: user_settings.libretranslate_api_key)
|
||||||
|
|
||||||
|
do {
|
||||||
|
language = try await translator.detect(event.content)
|
||||||
|
|
||||||
|
if language == nil {
|
||||||
|
language = currentLanguage
|
||||||
|
translated_note = nil
|
||||||
|
} else if language != currentLanguage {
|
||||||
|
translated_note = try await translator.translate(event.content, from: language!, to: currentLanguage)
|
||||||
|
|
||||||
|
if translated_note != nil {
|
||||||
|
let blocks = event.get_blocks(content: translated_note!)
|
||||||
|
translated_artifacts = render_blocks(blocks: blocks, profiles: profiles, privkey: privkey)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} 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.
|
||||||
|
language = currentLanguage
|
||||||
|
translated_note = nil
|
||||||
|
}
|
||||||
|
|
||||||
|
checkingTranslationStatus = false
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -196,6 +264,112 @@ func mention_str(_ m: Mention, profiles: Profiles) -> AttributedString {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public struct Translator {
|
||||||
|
private let url: String
|
||||||
|
private let apiKey: String?
|
||||||
|
private let session = URLSession.shared
|
||||||
|
private let encoder = JSONEncoder()
|
||||||
|
private let decoder = JSONDecoder()
|
||||||
|
|
||||||
|
public init(_ url: String, apiKey: String? = nil) {
|
||||||
|
self.url = url
|
||||||
|
self.apiKey = apiKey
|
||||||
|
}
|
||||||
|
|
||||||
|
public func detect(_ text: String) async throws -> String? {
|
||||||
|
let url = try makeURL(path: "/detect")
|
||||||
|
|
||||||
|
var request = URLRequest(url: url)
|
||||||
|
request.httpMethod = "POST"
|
||||||
|
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
|
||||||
|
|
||||||
|
struct RequestBody: Encodable {
|
||||||
|
let q: String
|
||||||
|
let api_key: String?
|
||||||
|
}
|
||||||
|
let body = RequestBody(q: text, api_key: apiKey)
|
||||||
|
request.httpBody = try encoder.encode(body)
|
||||||
|
|
||||||
|
struct Response: Decodable {
|
||||||
|
let confidence: Double
|
||||||
|
let language: String
|
||||||
|
}
|
||||||
|
|
||||||
|
let data = try await session.data(for: request)
|
||||||
|
let response = try decoder.decode([Response].self, from: data)
|
||||||
|
let language = response.first!
|
||||||
|
|
||||||
|
if language.confidence >= 80 {
|
||||||
|
return language.language
|
||||||
|
} else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public func translate(_ text: String, from sourceLanguage: String, to targetLanguage: String) async throws -> String {
|
||||||
|
let url = try makeURL(path: "/translate")
|
||||||
|
|
||||||
|
var request = URLRequest(url: url)
|
||||||
|
request.httpMethod = "POST"
|
||||||
|
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
|
||||||
|
|
||||||
|
struct RequestBody: Encodable {
|
||||||
|
let q: String
|
||||||
|
let source: String
|
||||||
|
let target: String
|
||||||
|
let api_key: String?
|
||||||
|
}
|
||||||
|
let body = RequestBody(q: text, source: sourceLanguage, target: targetLanguage, api_key: apiKey)
|
||||||
|
request.httpBody = try encoder.encode(body)
|
||||||
|
|
||||||
|
struct Response: Decodable {
|
||||||
|
let translatedText: String
|
||||||
|
}
|
||||||
|
let response: Response = try await decodedData(for: request)
|
||||||
|
return response.translatedText
|
||||||
|
}
|
||||||
|
|
||||||
|
private func makeURL(path: String) throws -> URL {
|
||||||
|
guard var components = URLComponents(string: url) else {
|
||||||
|
throw URLError(.badURL)
|
||||||
|
}
|
||||||
|
components.path = path
|
||||||
|
guard let url = components.url else {
|
||||||
|
throw URLError(.badURL)
|
||||||
|
}
|
||||||
|
return url
|
||||||
|
}
|
||||||
|
|
||||||
|
private func decodedData<Output: Decodable>(for request: URLRequest) async throws -> Output {
|
||||||
|
let data = try await session.data(for: request)
|
||||||
|
let result = try decoder.decode(Output.self, from: data)
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private extension URLSession {
|
||||||
|
func data(for request: URLRequest) async throws -> Data {
|
||||||
|
var task: URLSessionDataTask?
|
||||||
|
let onCancel = { task?.cancel() }
|
||||||
|
return try await withTaskCancellationHandler(
|
||||||
|
operation: {
|
||||||
|
try await withCheckedThrowingContinuation { continuation in
|
||||||
|
task = dataTask(with: request) { data, _, error in
|
||||||
|
guard let data = data else {
|
||||||
|
let error = error ?? URLError(.badServerResponse)
|
||||||
|
return continuation.resume(throwing: error)
|
||||||
|
}
|
||||||
|
continuation.resume(returning: data)
|
||||||
|
}
|
||||||
|
task?.resume()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onCancel: { onCancel() }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
struct NoteContentView_Previews: PreviewProvider {
|
struct NoteContentView_Previews: PreviewProvider {
|
||||||
static var previews: some View {
|
static var previews: some View {
|
||||||
let state = test_damus_state()
|
let state = test_damus_state()
|
||||||
|
|||||||
@@ -115,7 +115,7 @@ struct ProfileView: View {
|
|||||||
@State var is_zoomed: Bool = false
|
@State var is_zoomed: Bool = false
|
||||||
@State var show_share_sheet: Bool = false
|
@State var show_share_sheet: Bool = false
|
||||||
@State var action_sheet_presented: Bool = false
|
@State var action_sheet_presented: Bool = false
|
||||||
@StateObject var user_settings = UserSettingsStore()
|
@EnvironmentObject var user_settings: UserSettingsStore
|
||||||
|
|
||||||
@Environment(\.dismiss) var dismiss
|
@Environment(\.dismiss) var dismiss
|
||||||
@Environment(\.colorScheme) var colorScheme
|
@Environment(\.colorScheme) var colorScheme
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ struct SideMenuView: View {
|
|||||||
@Binding var isSidebarVisible: Bool
|
@Binding var isSidebarVisible: Bool
|
||||||
|
|
||||||
@State var confirm_logout: Bool = false
|
@State var confirm_logout: Bool = false
|
||||||
@StateObject var user_settings = UserSettingsStore()
|
@EnvironmentObject var user_settings: UserSettingsStore
|
||||||
|
|
||||||
@State private var showQRCode = false
|
@State private var showQRCode = false
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user