mirror of
https://github.com/simplex-chat/simplex-chat.git
synced 2024-12-17 17:20:21 +01:00
Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 469ff6b754 | |||
| 17f99edb6f |
@@ -891,6 +891,7 @@ struct ChatView: View {
|
||||
@State private var showDeleteMessages = false
|
||||
@State private var showChatItemInfoSheet: Bool = false
|
||||
@State private var chatItemInfo: ChatItemInfo?
|
||||
@State private var showTranslationSheet: Bool = false
|
||||
@State private var msgWidth: CGFloat = 0
|
||||
|
||||
@Binding var selectedChatItems: Set<Int64>?
|
||||
@@ -1208,6 +1209,11 @@ struct ChatView: View {
|
||||
}) {
|
||||
ChatItemInfoView(ci: ci, chatItemInfo: $chatItemInfo)
|
||||
}
|
||||
.sheet(isPresented: $showTranslationSheet) {
|
||||
if #available(iOS 18.0, *) {
|
||||
TranslateView(source: ci.text).presentationDetents([.medium, .large])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func showMemberImage(_ member: GroupMember, _ prevItem: ChatItem?) -> Bool {
|
||||
@@ -1264,6 +1270,9 @@ struct ChatView: View {
|
||||
shareButton(ci)
|
||||
copyButton(ci)
|
||||
}
|
||||
if #available(iOS 18.0, *) {
|
||||
if !ci.text.isEmpty { translateButton(ci) }
|
||||
}
|
||||
if let fileSource = fileSource, fileExists {
|
||||
if case .image = ci.content.msgContent, let image = getLoadedImage(ci.file) {
|
||||
if image.imageData != nil {
|
||||
@@ -1448,6 +1457,14 @@ struct ChatView: View {
|
||||
}
|
||||
}
|
||||
|
||||
private func translateButton(_ ci: ChatItem) -> Button<some View> {
|
||||
Button {
|
||||
showTranslationSheet = true
|
||||
} label: {
|
||||
Label("Translate", systemImage: "globe")
|
||||
}
|
||||
}
|
||||
|
||||
func saveButton(image: UIImage) -> Button<some View> {
|
||||
Button {
|
||||
UIImageWriteToSavedPhotosAlbum(image, nil, nil, nil)
|
||||
|
||||
@@ -0,0 +1,73 @@
|
||||
//
|
||||
// TranslateView.swift
|
||||
// SimpleX
|
||||
//
|
||||
// Created by user on 19/09/2024.
|
||||
// Copyright © 2024 SimpleX Chat. All rights reserved.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
import Translation
|
||||
|
||||
@available(iOS 18.0, *)
|
||||
struct TranslateView: View {
|
||||
let source: String
|
||||
@State private var supprtedLanguages = [Locale.Language]()
|
||||
@State private var target: String?
|
||||
@State private var configuration = TranslationSession.Configuration()
|
||||
|
||||
var body: some View {
|
||||
NavigationStack {
|
||||
List {
|
||||
Section {
|
||||
languagePicker("From", selection: $configuration.source)
|
||||
Text(source).foregroundStyle(.secondary).lineLimit(1)
|
||||
}
|
||||
Section {
|
||||
languagePicker("To ", selection: $configuration.target)
|
||||
if let target {
|
||||
Text(target)
|
||||
} else {
|
||||
ProgressView()
|
||||
}
|
||||
} footer: {
|
||||
Text("\(Image(systemName: "info.circle")) Uses on device translation")
|
||||
}
|
||||
}
|
||||
.navigationTitle("Translate")
|
||||
}
|
||||
.task { supprtedLanguages = await LanguageAvailability().supportedLanguages }
|
||||
.translationTask(configuration) { session in
|
||||
do {
|
||||
target = nil
|
||||
let response = try await session.translate(source)
|
||||
await MainActor.run {
|
||||
configuration.source = response.sourceLanguage
|
||||
configuration.target = response.targetLanguage
|
||||
target = response.targetText
|
||||
}
|
||||
} catch {
|
||||
print(error)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
private func languagePicker(_ label: String, selection: Binding<Locale.Language?>) -> some View {
|
||||
Picker(label, selection: selection) {
|
||||
Text("Detect language").tag(Optional<Locale.Language>.none)
|
||||
ForEach(supprtedLanguages, id: \.self) { language in
|
||||
Text(title(for: language)).tag(language)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func title(for language: Locale.Language) -> String {
|
||||
if let code = language.languageCode,
|
||||
let string = Locale.current.localizedString(forLanguageCode: code.identifier) {
|
||||
string
|
||||
} else {
|
||||
language.minimalIdentifier
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -204,6 +204,7 @@
|
||||
CE38A29C2C3FCD72005ED185 /* SwiftyGif in Frameworks */ = {isa = PBXBuildFile; productRef = CE38A29B2C3FCD72005ED185 /* SwiftyGif */; };
|
||||
CE75480A2C622630009579B7 /* SwipeLabel.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE7548092C622630009579B7 /* SwipeLabel.swift */; };
|
||||
CE984D4B2C36C5D500E3AEFF /* ChatItemClipShape.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE984D4A2C36C5D500E3AEFF /* ChatItemClipShape.swift */; };
|
||||
CEA034032C9C0F1800B587E7 /* TranslateView.swift in Sources */ = {isa = PBXBuildFile; fileRef = CEA034022C9C0F1800B587E7 /* TranslateView.swift */; };
|
||||
CEDE70222C48FD9500233B1F /* SEChatState.swift in Sources */ = {isa = PBXBuildFile; fileRef = CEDE70212C48FD9500233B1F /* SEChatState.swift */; };
|
||||
CEE723AA2C3BD3D70009AE93 /* ShareViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = CEE723A92C3BD3D70009AE93 /* ShareViewController.swift */; };
|
||||
CEE723B12C3BD3D70009AE93 /* SimpleX SE.appex in Embed App Extensions */ = {isa = PBXBuildFile; fileRef = CEE723A72C3BD3D70009AE93 /* SimpleX SE.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; };
|
||||
@@ -543,6 +544,7 @@
|
||||
CE3097FA2C4C0C9F00180898 /* ErrorAlert.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ErrorAlert.swift; sourceTree = "<group>"; };
|
||||
CE7548092C622630009579B7 /* SwipeLabel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SwipeLabel.swift; sourceTree = "<group>"; };
|
||||
CE984D4A2C36C5D500E3AEFF /* ChatItemClipShape.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatItemClipShape.swift; sourceTree = "<group>"; };
|
||||
CEA034022C9C0F1800B587E7 /* TranslateView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TranslateView.swift; sourceTree = "<group>"; };
|
||||
CEDE70212C48FD9500233B1F /* SEChatState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SEChatState.swift; sourceTree = "<group>"; };
|
||||
CEE723A72C3BD3D70009AE93 /* SimpleX SE.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = "SimpleX SE.appex"; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
CEE723A92C3BD3D70009AE93 /* ShareViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShareViewController.swift; sourceTree = "<group>"; };
|
||||
@@ -727,6 +729,7 @@
|
||||
5CBE6C132944CC12002D9531 /* ScanCodeView.swift */,
|
||||
64C06EB42A0A4A7C00792D4D /* ChatItemInfoView.swift */,
|
||||
648679AA2BC96A74006456E7 /* ChatItemForwardingView.swift */,
|
||||
CEA034022C9C0F1800B587E7 /* TranslateView.swift */,
|
||||
8CE848A22C5A0FA000D5C7C8 /* SelectableChatItemToolbars.swift */,
|
||||
);
|
||||
path = Chat;
|
||||
@@ -1393,6 +1396,7 @@
|
||||
5CB634A829E437960066AD6B /* PasscodeEntry.swift in Sources */,
|
||||
5CFA59C42860BC6200863A68 /* MigrateToAppGroupView.swift in Sources */,
|
||||
648010AB281ADD15009009B9 /* CIFileView.swift in Sources */,
|
||||
CEA034032C9C0F1800B587E7 /* TranslateView.swift in Sources */,
|
||||
644EFFE2292D089800525D5B /* FramedCIVoiceView.swift in Sources */,
|
||||
5C4B3B0A285FB130003915F2 /* DatabaseView.swift in Sources */,
|
||||
5CB2084F28DA4B4800D024EC /* RTCServers.swift in Sources */,
|
||||
|
||||
@@ -177,14 +177,66 @@ public func fromCString(_ c: UnsafeMutablePointer<CChar>) -> String {
|
||||
return s
|
||||
}
|
||||
|
||||
// TODO: Remove after XCode 16 crash has been resolved/merged
|
||||
class ThreadExecutor<ResultType> {
|
||||
private var stackSize: Int
|
||||
private var qos: QualityOfService
|
||||
|
||||
/// Initialize by specifying the stack size
|
||||
init(stackSize: Int, qos: QualityOfService) {
|
||||
self.stackSize = stackSize
|
||||
self.qos = qos
|
||||
}
|
||||
|
||||
/// Execute a closure synchronously on a separate thread and return the result
|
||||
func executeSync(_ task: @escaping () throws -> ResultType) throws -> ResultType {
|
||||
// Initialize the semaphore (initially locked)
|
||||
let semaphore = DispatchSemaphore(value: 0)
|
||||
var result: Result<ResultType, Error>?
|
||||
|
||||
// Initialize the thread
|
||||
let thread = Thread {
|
||||
do {
|
||||
let taskResult = try task()
|
||||
result = .success(taskResult)
|
||||
} catch {
|
||||
result = .failure(error)
|
||||
}
|
||||
// Release the semaphore when the task is completed
|
||||
semaphore.signal()
|
||||
}
|
||||
|
||||
thread.stackSize = stackSize
|
||||
thread.qualityOfService = qos
|
||||
|
||||
thread.start()
|
||||
|
||||
// Wait until the thread completes
|
||||
semaphore.wait()
|
||||
|
||||
// Return the result
|
||||
switch result! {
|
||||
case .success(let result):
|
||||
return result
|
||||
case .failure(let error):
|
||||
throw error
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public func chatResponse(_ s: String) -> ChatResponse {
|
||||
let d = s.data(using: .utf8)!
|
||||
// TODO is there a way to do it without copying the data? e.g:
|
||||
// let p = UnsafeMutableRawPointer.init(mutating: UnsafeRawPointer(cjson))
|
||||
// let d = Data.init(bytesNoCopy: p, count: strlen(cjson), deallocator: .free)
|
||||
do {
|
||||
let r = try jsonDecoder.decode(APIResponse.self, from: d)
|
||||
return r.resp
|
||||
let executor = ThreadExecutor<APIResponse>(
|
||||
stackSize: 2*1024*1024, // 2MiB
|
||||
qos: Thread.current.qualityOfService // inherit priority
|
||||
)
|
||||
return try executor.executeSync {
|
||||
try jsonDecoder.decode(APIResponse.self, from: d)
|
||||
}.resp
|
||||
} catch {
|
||||
logger.error("chatResponse jsonDecoder.decode error: \(error.localizedDescription)")
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user