@ -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 ] = [ ]
@ -64,17 +72,48 @@ 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 ) {
Text ( artifacts . content )
Text ( artifacts . content )
. 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 {
// I f f o r w h a t e v e r r e a s o n w e ' r e n o t a b l e t o f i g u r e o u t t h e l a n g u a g e o f t h e n o t e , o r t r a n s l a t e t h e n o t e , f a i l g r a c e f u l l y a n d d o n o t r e t r y . I t ' s n o t t h e e n d o f t h e w o r l d . D o n ' t w a n t t o t a k e d o w n s o m e o n e ' s t r a n s l a t i o n s e r v e r w i t h a n a c c i d e n t a l d e n i a l o f s e r v i c e a t t a c k .
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 ( )