Compare commits

...

2 Commits

Author SHA1 Message Date
Levitating Pineapple 469ff6b754 Merge master 2024-09-19 11:18:50 +03:00
Levitating Pineapple 17f99edb6f ios: add on on-device translation sheet to chat 2024-09-19 11:04:46 +03:00
4 changed files with 148 additions and 2 deletions
+17
View File
@@ -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 */,
+54 -2
View File
@@ -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)")
}