diff --git a/README.md b/README.md
index 561a322c62..d4103fd104 100644
--- a/README.md
+++ b/README.md
@@ -2,7 +2,7 @@
[](https://github.com/simplex-chat/simplex-chat/releases)
[](https://github.com/simplex-chat/simplex-chat/releases)
[](https://www.reddit.com/r/SimpleXChat)
-[](https://mastodon.social/@simplex)
+
| 30/03/2023 | EN, [FR](/docs/lang/fr/README.md), [CZ](/docs/lang/cs/README.md) |
@@ -15,7 +15,7 @@
## Welcome to SimpleX Chat!
1. 📲 [Install the app](#install-the-app).
-2. ↔️ [Connect to the team](#connect-to-the-team-via-the-app) and [join user groups](#join-user-groups).
+2. ↔️ [Connect to the team](#connect-to-the-team), [join user groups](#join-user-groups) and [follow our updates](#follow-our-updates).
3. 🤝 [Make a private connection](#make-a-private-connection) with a friend.
4. 🔤 [Help translating SimpleX Chat](#help-translating-simplex-chat).
5. ⚡️ [Contribute](#contribute) and [help us with donations](#help-us-with-donations).
@@ -40,14 +40,22 @@
- 🚀 [TestFlight preview for iOS](https://testflight.apple.com/join/DWuT2LQu) with the new features 1-2 weeks earlier - **limited to 10,000 users**!
- 🖥 Available as a terminal (console) [app / CLI](#zap-quick-installation-of-a-terminal-app) on Linux, MacOS, Windows.
-## Connect to the team via the app
+## Connect to the team
+
+You can connect to the team via the app using "chat with the developers button" available when you have no conversations in the profile, "Send questions and ideas" in the app settings or via our [SimpleX address](https://simplex.chat/contact#/?v=1&smp=smp%3A%2F%2FPQUV2eL0t7OStZOoAsPEV2QYWt4-xilbakvGUGOItUo%3D%40smp6.simplex.im%2FK1rslx-m5bpXVIdMZg9NLUZ_8JBm8xTt%23%2F%3Fv%3D1%26dh%3DMCowBQYDK2VuAyEALDeVe-sG8mRY22LsXlPgiwTNs9dbiLrNuA7f3ZMAJ2w%253D%26srv%3Dbylepyau3ty4czmn77q4fglvperknl4bi2eb2fdy2bh4jxtf32kf73yd.onion). Please connect to:
- to ask any questions
- to suggest any improvements
- to share anything relevant
+We are replying the questions manually, so it is not instant – it can take up to 24 hours.
+
+If you are interested in helping us to integrate open-source language models, and in [joining our team](./docs/JOIN_TEAM.md), please get in touch.
+
## Join user groups
+You can join the groups created by other users via the new [directory service](https://simplex.chat/contact#/?v=1-4&smp=smp%3A%2F%2Fu2dS9sG8nMNURyZwqASV4yROM28Er0luVTx5X1CsMrU%3D%40smp4.simplex.im%2FeXSPwqTkKyDO3px4fLf1wx3MvPdjdLW3%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEAaiv6MkMH44L2TcYrt_CsX3ZvM11WgbMEUn0hkIKTOho%253D%26srv%3Do5vmywmrnaxalvz6wi3zicyftgio6psuvyniis6gco6bp6ekl4cqj4id.onion). We are not responsible for the content shared in these groups.
+
**Please note**: The groups below are created for the users to be able to ask questions, make suggestions and ask questions about SimpleX Chat only.
You also can:
@@ -79,7 +87,14 @@ There are groups in other languages, that we have the apps interface translated
You can join either by opening these links in the app or by opening them in a desktop browser and scanning the QR code.
-You can also join the group created by other users by searching for them via the [directory service](https://simplex.chat/contact#/?v=1-4&smp=smp%3A%2F%2Fu2dS9sG8nMNURyZwqASV4yROM28Er0luVTx5X1CsMrU%3D%40smp4.simplex.im%2FeXSPwqTkKyDO3px4fLf1wx3MvPdjdLW3%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEAaiv6MkMH44L2TcYrt_CsX3ZvM11WgbMEUn0hkIKTOho%253D%26srv%3Do5vmywmrnaxalvz6wi3zicyftgio6psuvyniis6gco6bp6ekl4cqj4id.onion). We are not responsible for the content shared in these groups.
+## Follow our updates
+
+We publish our updates and releases via:
+
+- [Reddit](https://www.reddit.com/r/SimpleXChat/), [Twitter](https://twitter.com/SimpleXChat), [Lemmy](https://lemmy.ml/c/simplex), [Mastodon](https://mastodon.social/@simplex) and [Nostr](https://snort.social/p/npub1exv22uulqnmlluszc4yk92jhs2e5ajcs6mu3t00a6avzjcalj9csm7d828).
+- SimpleX Chat [team profile](#connect-to-the-team).
+- [blog](https://simplex.chat/blog/) and [RSS feed](https://simplex.chat/feed.rss).
+- [mailing list](https://simplex.chat/#join-simplex), very rarely.
## Make a private connection
diff --git a/apps/ios/Shared/Model/AudioRecPlay.swift b/apps/ios/Shared/Model/AudioRecPlay.swift
index 698799457e..973d79ab3c 100644
--- a/apps/ios/Shared/Model/AudioRecPlay.swift
+++ b/apps/ios/Shared/Model/AudioRecPlay.swift
@@ -103,9 +103,15 @@ class AudioPlayer: NSObject, AVAudioPlayerDelegate {
self.onFinishPlayback = onFinishPlayback
}
- func start(fileName: String, at: TimeInterval?) {
- let url = getAppFilePath(fileName)
- audioPlayer = try? AVAudioPlayer(contentsOf: url)
+ func start(fileSource: CryptoFile, at: TimeInterval?) {
+ let url = getAppFilePath(fileSource.filePath)
+ if let cfArgs = fileSource.cryptoArgs {
+ if let data = try? readCryptoFile(path: url.path, cryptoArgs: cfArgs) {
+ audioPlayer = try? AVAudioPlayer(data: data)
+ }
+ } else {
+ audioPlayer = try? AVAudioPlayer(contentsOf: url)
+ }
audioPlayer?.delegate = self
audioPlayer?.prepareToPlay()
if let at = at {
diff --git a/apps/ios/Shared/Model/ImageUtils.swift b/apps/ios/Shared/Model/ImageUtils.swift
index 4987f5a6f7..90070e74d3 100644
--- a/apps/ios/Shared/Model/ImageUtils.swift
+++ b/apps/ios/Shared/Model/ImageUtils.swift
@@ -11,42 +11,43 @@ import SimpleXChat
import SwiftUI
import AVKit
-func getLoadedFilePath(_ file: CIFile?) -> String? {
- if let fileName = getLoadedFileName(file) {
- return getAppFilePath(fileName).path
- }
- return nil
-}
-
-func getLoadedFileName(_ file: CIFile?) -> String? {
- if let file = file,
- file.loaded,
- let fileName = file.filePath {
- return fileName
+func getLoadedFileSource(_ file: CIFile?) -> CryptoFile? {
+ if let file = file, file.loaded {
+ return file.fileSource
}
return nil
}
func getLoadedImage(_ file: CIFile?) -> UIImage? {
- let loadedFilePath = getLoadedFilePath(file)
- if let loadedFilePath = loadedFilePath, let fileName = file?.filePath {
- let filePath = getAppFilePath(fileName)
+ if let fileSource = getLoadedFileSource(file) {
+ let filePath = getAppFilePath(fileSource.filePath)
do {
- let data = try Data(contentsOf: filePath)
+ let data = try getFileData(filePath, fileSource.cryptoArgs)
let img = UIImage(data: data)
- try img?.setGifFromData(data, levelOfIntegrity: 1.0)
- return img
+ do {
+ try img?.setGifFromData(data, levelOfIntegrity: 1.0)
+ return img
+ } catch {
+ return UIImage(data: data)
+ }
} catch {
- return UIImage(contentsOfFile: loadedFilePath)
+ return nil
}
}
return nil
}
+func getFileData(_ path: URL, _ cfArgs: CryptoFileArgs?) throws -> Data {
+ if let cfArgs = cfArgs {
+ return try readCryptoFile(path: path.path, cryptoArgs: cfArgs)
+ } else {
+ return try Data(contentsOf: path)
+ }
+}
+
func getLoadedVideo(_ file: CIFile?) -> URL? {
- let loadedFilePath = getLoadedFilePath(file)
- if loadedFilePath != nil, let fileName = file?.filePath {
- let filePath = getAppFilePath(fileName)
+ if let fileSource = getLoadedFileSource(file) {
+ let filePath = getAppFilePath(fileSource.filePath)
if FileManager.default.fileExists(atPath: filePath.path) {
return filePath
}
@@ -54,18 +55,18 @@ func getLoadedVideo(_ file: CIFile?) -> URL? {
return nil
}
-func saveAnimImage(_ image: UIImage) -> String? {
+func saveAnimImage(_ image: UIImage) -> CryptoFile? {
let fileName = generateNewFileName("IMG", "gif")
guard let imageData = image.imageData else { return nil }
- return saveFile(imageData, fileName)
+ return saveFile(imageData, fileName, encrypted: privacyEncryptLocalFilesGroupDefault.get())
}
-func saveImage(_ uiImage: UIImage) -> String? {
+func saveImage(_ uiImage: UIImage) -> CryptoFile? {
let hasAlpha = imageHasAlpha(uiImage)
let ext = hasAlpha ? "png" : "jpg"
if let imageDataResized = resizeImageToDataSize(uiImage, maxDataSize: MAX_IMAGE_SIZE, hasAlpha: hasAlpha) {
let fileName = generateNewFileName("IMG", ext)
- return saveFile(imageDataResized, fileName)
+ return saveFile(imageDataResized, fileName, encrypted: privacyEncryptLocalFilesGroupDefault.get())
}
return nil
}
@@ -157,13 +158,19 @@ func imageHasAlpha(_ img: UIImage) -> Bool {
return false
}
-func saveFileFromURL(_ url: URL) -> String? {
- let savedFile: String?
+func saveFileFromURL(_ url: URL, encrypted: Bool) -> CryptoFile? {
+ let savedFile: CryptoFile?
if url.startAccessingSecurityScopedResource() {
do {
- let fileData = try Data(contentsOf: url)
let fileName = uniqueCombine(url.lastPathComponent)
- savedFile = saveFile(fileData, fileName)
+ let toPath = getAppFilePath(fileName).path
+ if encrypted {
+ let cfArgs = try encryptCryptoFile(fromPath: url.path, toPath: toPath)
+ savedFile = CryptoFile(filePath: fileName, cryptoArgs: cfArgs)
+ } else {
+ try FileManager.default.copyItem(atPath: url.path, toPath: toPath)
+ savedFile = CryptoFile.plain(fileName)
+ }
} catch {
logger.error("FileUtils.saveFileFromURL error: \(error.localizedDescription)")
savedFile = nil
@@ -176,18 +183,16 @@ func saveFileFromURL(_ url: URL) -> String? {
return savedFile
}
-func saveFileFromURLWithoutLoad(_ url: URL) -> String? {
- let savedFile: String?
+func moveTempFileFromURL(_ url: URL) -> CryptoFile? {
do {
let fileName = uniqueCombine(url.lastPathComponent)
try FileManager.default.moveItem(at: url, to: getAppFilePath(fileName))
ChatModel.shared.filesToDelete.remove(url)
- savedFile = fileName
+ return CryptoFile.plain(fileName)
} catch {
- logger.error("FileUtils.saveFileFromURLWithoutLoad error: \(error.localizedDescription)")
- savedFile = nil
+ logger.error("ImageUtils.moveTempFileFromURL error: \(error.localizedDescription)")
+ return nil
}
- return savedFile
}
func generateNewFileName(_ prefix: String, _ ext: String) -> String {
@@ -288,4 +293,4 @@ extension UIImage {
}
return self
}
-}
\ No newline at end of file
+}
diff --git a/apps/ios/Shared/Model/SimpleXAPI.swift b/apps/ios/Shared/Model/SimpleXAPI.swift
index 59524c2c39..7a625bae63 100644
--- a/apps/ios/Shared/Model/SimpleXAPI.swift
+++ b/apps/ios/Shared/Model/SimpleXAPI.swift
@@ -315,7 +315,7 @@ func apiGetChatItemInfo(type: ChatType, id: Int64, itemId: Int64) async throws -
throw r
}
-func apiSendMessage(type: ChatType, id: Int64, file: String?, quotedItemId: Int64?, msg: MsgContent, live: Bool = false, ttl: Int? = nil) async -> ChatItem? {
+func apiSendMessage(type: ChatType, id: Int64, file: CryptoFile?, quotedItemId: Int64?, msg: MsgContent, live: Bool = false, ttl: Int? = nil) async -> ChatItem? {
let chatModel = ChatModel.shared
let cmd: ChatCommand = .apiSendMessage(type: type, id: id, file: file, quotedItemId: quotedItemId, msg: msg, live: live, ttl: ttl)
let r: ChatResponse
@@ -807,14 +807,14 @@ func apiChatUnread(type: ChatType, id: Int64, unreadChat: Bool) async throws {
try await sendCommandOkResp(.apiChatUnread(type: type, id: id, unreadChat: unreadChat))
}
-func receiveFile(user: any UserLike, fileId: Int64, auto: Bool = false) async {
- if let chatItem = await apiReceiveFile(fileId: fileId, auto: auto) {
+func receiveFile(user: any UserLike, fileId: Int64, encrypted: Bool, auto: Bool = false) async {
+ if let chatItem = await apiReceiveFile(fileId: fileId, encrypted: encrypted, auto: auto) {
await chatItemSimpleUpdate(user, chatItem)
}
}
-func apiReceiveFile(fileId: Int64, inline: Bool? = nil, auto: Bool = false) async -> AChatItem? {
- let r = await chatSendCmd(.receiveFile(fileId: fileId, inline: inline))
+func apiReceiveFile(fileId: Int64, encrypted: Bool, inline: Bool? = nil, auto: Bool = false) async -> AChatItem? {
+ let r = await chatSendCmd(.receiveFile(fileId: fileId, encrypted: encrypted, inline: inline))
let am = AlertManager.shared
if case let .rcvFileAccepted(_, chatItem) = r { return chatItem }
if case .rcvFileAcceptedSndCancelled = r {
@@ -1357,7 +1357,7 @@ func processReceivedMsg(_ res: ChatResponse) async {
}
if let file = cItem.autoReceiveFile() {
Task {
- await receiveFile(user: user, fileId: file.fileId, auto: true)
+ await receiveFile(user: user, fileId: file.fileId, encrypted: cItem.encryptLocalFile, auto: true)
}
}
if cItem.showNotification {
@@ -1660,15 +1660,3 @@ private struct UserResponse: Decodable {
var user: User?
var error: String?
}
-
-struct RuntimeError: Error {
- let message: String
-
- init(_ message: String) {
- self.message = message
- }
-
- public var localizedDescription: String {
- return message
- }
-}
diff --git a/apps/ios/Shared/Views/Chat/ChatItem/CIFileView.swift b/apps/ios/Shared/Views/Chat/ChatItem/CIFileView.swift
index 0c43ebe41a..1c32f36c9c 100644
--- a/apps/ios/Shared/Views/Chat/ChatItem/CIFileView.swift
+++ b/apps/ios/Shared/Views/Chat/ChatItem/CIFileView.swift
@@ -16,8 +16,8 @@ struct CIFileView: View {
var body: some View {
let metaReserve = edited
- ? " "
- : " "
+ ? " "
+ : " "
Button(action: fileAction) {
HStack(alignment: .bottom, spacing: 6) {
fileIndicator()
@@ -84,7 +84,8 @@ struct CIFileView: View {
Task {
logger.debug("CIFileView fileAction - in .rcvInvitation, in Task")
if let user = ChatModel.shared.currentUser {
- await receiveFile(user: user, fileId: file.fileId)
+ let encrypted = file.fileProtocol == .xftp && privacyEncryptLocalFilesGroupDefault.get()
+ await receiveFile(user: user, fileId: file.fileId, encrypted: encrypted)
}
}
} else {
@@ -109,9 +110,8 @@ struct CIFileView: View {
}
case .rcvComplete:
logger.debug("CIFileView fileAction - in .rcvComplete")
- if let filePath = getLoadedFilePath(file) {
- let url = URL(fileURLWithPath: filePath)
- showShareSheet(items: [url])
+ if let fileSource = getLoadedFileSource(file) {
+ saveCryptoFile(fileSource)
}
default: break
}
@@ -193,6 +193,30 @@ struct CIFileView: View {
}
}
+func saveCryptoFile(_ fileSource: CryptoFile) {
+ if let cfArgs = fileSource.cryptoArgs {
+ let url = getAppFilePath(fileSource.filePath)
+ let tempUrl = getTempFilesDirectory().appendingPathComponent(fileSource.filePath)
+ Task {
+ do {
+ try decryptCryptoFile(fromPath: url.path, cryptoArgs: cfArgs, toPath: tempUrl.path)
+ await MainActor.run {
+ showShareSheet(items: [tempUrl]) {
+ removeFile(tempUrl)
+ }
+ }
+ } catch {
+ await MainActor.run {
+ AlertManager.shared.showAlertMsg(title: "Error decrypting file", message: "Error: \(error.localizedDescription)")
+ }
+ }
+ }
+ } else {
+ let url = getAppFilePath(fileSource.filePath)
+ showShareSheet(items: [url])
+ }
+}
+
struct CIFileView_Previews: PreviewProvider {
static var previews: some View {
let sentFile: ChatItem = ChatItem(
diff --git a/apps/ios/Shared/Views/Chat/ChatItem/CIImageView.swift b/apps/ios/Shared/Views/Chat/ChatItem/CIImageView.swift
index b13ee52829..bb43179577 100644
--- a/apps/ios/Shared/Views/Chat/ChatItem/CIImageView.swift
+++ b/apps/ios/Shared/Views/Chat/ChatItem/CIImageView.swift
@@ -16,6 +16,7 @@ struct CIImageView: View {
let maxWidth: CGFloat
@Binding var imgWidth: CGFloat?
@State var scrollProxy: ScrollViewProxy?
+ @State var metaColor: Color
@State private var showFullScreenImage = false
var body: some View {
@@ -36,9 +37,8 @@ struct CIImageView: View {
case .rcvInvitation:
Task {
if let user = ChatModel.shared.currentUser {
- await receiveFile(user: user, fileId: file.fileId)
+ await receiveFile(user: user, fileId: file.fileId, encrypted: chatItem.encryptLocalFile)
}
- // TODO image accepted alert?
}
case .rcvAccepted:
switch file.fileProtocol {
@@ -110,7 +110,7 @@ struct CIImageView: View {
.resizable()
.aspectRatio(contentMode: .fit)
.frame(width: size, height: size)
- .foregroundColor(.white)
+ .foregroundColor(metaColor)
.padding(padding)
}
diff --git a/apps/ios/Shared/Views/Chat/ChatItem/CIMetaView.swift b/apps/ios/Shared/Views/Chat/ChatItem/CIMetaView.swift
index 996afd0485..30430dc19a 100644
--- a/apps/ios/Shared/Views/Chat/ChatItem/CIMetaView.swift
+++ b/apps/ios/Shared/Views/Chat/ChatItem/CIMetaView.swift
@@ -21,27 +21,28 @@ struct CIMetaView: View {
} else {
let meta = chatItem.meta
let ttl = chat.chatInfo.timedMessagesTTL
+ let encrypted = chatItem.encryptedFile
switch meta.itemStatus {
case let .sndSent(sndProgress):
switch sndProgress {
- case .complete: ciMetaText(meta, chatTTL: ttl, color: metaColor, sent: .sent)
- case .partial: ciMetaText(meta, chatTTL: ttl, color: paleMetaColor, sent: .sent)
+ case .complete: ciMetaText(meta, chatTTL: ttl, encrypted: encrypted, color: metaColor, sent: .sent)
+ case .partial: ciMetaText(meta, chatTTL: ttl, encrypted: encrypted, color: paleMetaColor, sent: .sent)
}
case let .sndRcvd(_, sndProgress):
switch sndProgress {
case .complete:
ZStack {
- ciMetaText(meta, chatTTL: ttl, color: metaColor, sent: .rcvd1)
- ciMetaText(meta, chatTTL: ttl, color: metaColor, sent: .rcvd2)
+ ciMetaText(meta, chatTTL: ttl, encrypted: encrypted, color: metaColor, sent: .rcvd1)
+ ciMetaText(meta, chatTTL: ttl, encrypted: encrypted, color: metaColor, sent: .rcvd2)
}
case .partial:
ZStack {
- ciMetaText(meta, chatTTL: ttl, color: paleMetaColor, sent: .rcvd1)
- ciMetaText(meta, chatTTL: ttl, color: paleMetaColor, sent: .rcvd2)
+ ciMetaText(meta, chatTTL: ttl, encrypted: encrypted, color: paleMetaColor, sent: .rcvd1)
+ ciMetaText(meta, chatTTL: ttl, encrypted: encrypted, color: paleMetaColor, sent: .rcvd2)
}
}
default:
- ciMetaText(meta, chatTTL: ttl, color: metaColor)
+ ciMetaText(meta, chatTTL: ttl, encrypted: encrypted, color: metaColor)
}
}
}
@@ -53,7 +54,7 @@ enum SentCheckmark {
case rcvd2
}
-func ciMetaText(_ meta: CIMeta, chatTTL: Int?, color: Color = .clear, transparent: Bool = false, sent: SentCheckmark? = nil) -> Text {
+func ciMetaText(_ meta: CIMeta, chatTTL: Int?, encrypted: Bool?, color: Color = .clear, transparent: Bool = false, sent: SentCheckmark? = nil) -> Text {
var r = Text("")
if meta.itemEdited {
r = r + statusIconText("pencil", color)
@@ -80,7 +81,11 @@ func ciMetaText(_ meta: CIMeta, chatTTL: Int?, color: Color = .clear, transparen
} else if !meta.disappearing {
r = r + statusIconText("circlebadge.fill", .clear) + Text(" ")
}
- return (r + meta.timestampText.foregroundColor(color)).font(.caption)
+ if let enc = encrypted {
+ r = r + statusIconText(enc ? "lock" : "lock.open", color) + Text(" ")
+ }
+ r = r + meta.timestampText.foregroundColor(color)
+ return r.font(.caption)
}
private func statusIconText(_ icon: String, _ color: Color) -> Text {
diff --git a/apps/ios/Shared/Views/Chat/ChatItem/CIRcvDecryptionError.swift b/apps/ios/Shared/Views/Chat/ChatItem/CIRcvDecryptionError.swift
index 2e0a19eada..e1a5c252ec 100644
--- a/apps/ios/Shared/Views/Chat/ChatItem/CIRcvDecryptionError.swift
+++ b/apps/ios/Shared/Views/Chat/ChatItem/CIRcvDecryptionError.swift
@@ -118,7 +118,7 @@ struct CIRcvDecryptionError: View {
.foregroundColor(syncSupported ? .accentColor : .secondary)
.font(.callout)
+ Text(" ")
- + ciMetaText(chatItem.meta, chatTTL: nil, transparent: true)
+ + ciMetaText(chatItem.meta, chatTTL: nil, encrypted: nil, transparent: true)
)
}
.padding(.horizontal, 12)
@@ -139,7 +139,7 @@ struct CIRcvDecryptionError: View {
.foregroundColor(.red)
.italic()
+ Text(" ")
- + ciMetaText(chatItem.meta, chatTTL: nil, transparent: true)
+ + ciMetaText(chatItem.meta, chatTTL: nil, encrypted: nil, transparent: true)
}
.padding(.horizontal, 12)
CIMetaView(chatItem: chatItem)
diff --git a/apps/ios/Shared/Views/Chat/ChatItem/CIVideoView.swift b/apps/ios/Shared/Views/Chat/ChatItem/CIVideoView.swift
index 6de2e44b77..3807a11b4e 100644
--- a/apps/ios/Shared/Views/Chat/ChatItem/CIVideoView.swift
+++ b/apps/ios/Shared/Views/Chat/ChatItem/CIVideoView.swift
@@ -59,7 +59,7 @@ struct CIVideoView: View {
if let file = file {
switch file.fileStatus {
case .rcvInvitation:
- receiveFileIfValidSize(file: file, receiveFile: receiveFile)
+ receiveFileIfValidSize(file: file, encrypted: false, receiveFile: receiveFile)
case .rcvAccepted:
switch file.fileProtocol {
case .xftp:
@@ -85,7 +85,7 @@ struct CIVideoView: View {
}
if let file = file, case .rcvInvitation = file.fileStatus {
Button {
- receiveFileIfValidSize(file: file, receiveFile: receiveFile)
+ receiveFileIfValidSize(file: file, encrypted: false, receiveFile: receiveFile)
} label: {
playPauseIcon("play.fill")
}
@@ -253,10 +253,11 @@ struct CIVideoView: View {
.padding([.trailing, .top], 11)
}
- private func receiveFileIfValidSize(file: CIFile, receiveFile: @escaping (User, Int64, Bool) async -> Void) {
+ // TODO encrypt: where file size is checked?
+ private func receiveFileIfValidSize(file: CIFile, encrypted: Bool, receiveFile: @escaping (User, Int64, Bool, Bool) async -> Void) {
Task {
if let user = ChatModel.shared.currentUser {
- await receiveFile(user, file.fileId, false)
+ await receiveFile(user, file.fileId, encrypted, false)
}
}
}
diff --git a/apps/ios/Shared/Views/Chat/ChatItem/CIVoiceView.swift b/apps/ios/Shared/Views/Chat/ChatItem/CIVoiceView.swift
index 167823934e..b0875abe8d 100644
--- a/apps/ios/Shared/Views/Chat/ChatItem/CIVoiceView.swift
+++ b/apps/ios/Shared/Views/Chat/ChatItem/CIVoiceView.swift
@@ -159,7 +159,8 @@ struct VoiceMessagePlayer: View {
}
}
.onChange(of: chatModel.stopPreviousRecPlay) { it in
- if let recordingFileName = getLoadedFileName(recordingFile), chatModel.stopPreviousRecPlay != getAppFilePath(recordingFileName) {
+ if let recordingFileName = getLoadedFileSource(recordingFile)?.filePath,
+ chatModel.stopPreviousRecPlay != getAppFilePath(recordingFileName) {
audioPlayer?.stop()
playbackState = .noPlayback
playbackTime = TimeInterval(0)
@@ -174,8 +175,8 @@ struct VoiceMessagePlayer: View {
switch playbackState {
case .noPlayback:
Button {
- if let recordingFileName = getLoadedFileName(recordingFile) {
- startPlayback(recordingFileName)
+ if let recordingSource = getLoadedFileSource(recordingFile) {
+ startPlayback(recordingSource)
}
} label: {
playPauseIcon("play.fill")
@@ -219,7 +220,7 @@ struct VoiceMessagePlayer: View {
Button {
Task {
if let user = ChatModel.shared.currentUser {
- await receiveFile(user: user, fileId: recordingFile.fileId)
+ await receiveFile(user: user, fileId: recordingFile.fileId, encrypted: privacyEncryptLocalFilesGroupDefault.get())
}
}
} label: {
@@ -251,8 +252,8 @@ struct VoiceMessagePlayer: View {
.clipShape(Circle())
}
- private func startPlayback(_ recordingFileName: String) {
- chatModel.stopPreviousRecPlay = getAppFilePath(recordingFileName)
+ private func startPlayback(_ recordingSource: CryptoFile) {
+ chatModel.stopPreviousRecPlay = getAppFilePath(recordingSource.filePath)
audioPlayer = AudioPlayer(
onTimer: { playbackTime = $0 },
onFinishPlayback: {
@@ -260,7 +261,7 @@ struct VoiceMessagePlayer: View {
playbackTime = TimeInterval(0)
}
)
- audioPlayer?.start(fileName: recordingFileName, at: playbackTime)
+ audioPlayer?.start(fileSource: recordingSource, at: playbackTime)
playbackState = .playing
}
}
diff --git a/apps/ios/Shared/Views/Chat/ChatItem/FramedItemView.swift b/apps/ios/Shared/Views/Chat/ChatItem/FramedItemView.swift
index ceaf175f9b..aab0cd5f55 100644
--- a/apps/ios/Shared/Views/Chat/ChatItem/FramedItemView.swift
+++ b/apps/ios/Shared/Views/Chat/ChatItem/FramedItemView.swift
@@ -97,7 +97,7 @@ struct FramedItemView: View {
} else {
switch (chatItem.content.msgContent) {
case let .image(text, image):
- CIImageView(chatItem: chatItem, image: image, maxWidth: maxWidth, imgWidth: $imgWidth, scrollProxy: scrollProxy)
+ CIImageView(chatItem: chatItem, image: image, maxWidth: maxWidth, imgWidth: $imgWidth, scrollProxy: scrollProxy, metaColor: metaColor)
.overlay(DetermineWidth())
if text == "" && !chatItem.meta.isLive {
Color.clear
diff --git a/apps/ios/Shared/Views/Chat/ChatItem/MsgContentView.swift b/apps/ios/Shared/Views/Chat/ChatItem/MsgContentView.swift
index 3ac908bb78..498b3cb2e0 100644
--- a/apps/ios/Shared/Views/Chat/ChatItem/MsgContentView.swift
+++ b/apps/ios/Shared/Views/Chat/ChatItem/MsgContentView.swift
@@ -80,7 +80,7 @@ struct MsgContentView: View {
}
private func reserveSpaceForMeta(_ mt: CIMeta) -> Text {
- (rightToLeft ? Text("\n") : Text(" ")) + ciMetaText(mt, chatTTL: chat.chatInfo.timedMessagesTTL, transparent: true)
+ (rightToLeft ? Text("\n") : Text(" ")) + ciMetaText(mt, chatTTL: chat.chatInfo.timedMessagesTTL, encrypted: nil, transparent: true)
}
}
diff --git a/apps/ios/Shared/Views/Chat/ChatView.swift b/apps/ios/Shared/Views/Chat/ChatView.swift
index 4f1b4fe728..2a0cd4f2c2 100644
--- a/apps/ios/Shared/Views/Chat/ChatView.swift
+++ b/apps/ios/Shared/Views/Chat/ChatView.swift
@@ -601,15 +601,15 @@ struct ChatView: View {
}
menu.append(shareUIAction())
menu.append(copyUIAction())
- if let filePath = getLoadedFilePath(ci.file) {
+ if let fileSource = getLoadedFileSource(ci.file) {
if case .image = ci.content.msgContent, let image = getLoadedImage(ci.file) {
if image.imageData != nil {
- menu.append(saveFileAction(filePath))
+ menu.append(saveFileAction(fileSource))
} else {
menu.append(saveImageAction(image))
}
} else {
- menu.append(saveFileAction(filePath))
+ menu.append(saveFileAction(fileSource))
}
}
if ci.meta.editable && !mc.isVoice && !live {
@@ -747,13 +747,12 @@ struct ChatView: View {
}
}
- private func saveFileAction(_ filePath: String) -> UIAction {
+ private func saveFileAction(_ fileSource: CryptoFile) -> UIAction {
UIAction(
title: NSLocalizedString("Save", comment: "chat item action"),
- image: UIImage(systemName: "square.and.arrow.down")
+ image: UIImage(systemName: fileSource.cryptoArgs == nil ? "square.and.arrow.down" : "lock.open")
) { _ in
- let fileURL = URL(fileURLWithPath: filePath)
- showShareSheet(items: [fileURL])
+ saveCryptoFile(fileSource)
}
}
diff --git a/apps/ios/Shared/Views/Chat/ComposeMessage/ComposeView.swift b/apps/ios/Shared/Views/Chat/ComposeMessage/ComposeView.swift
index 674f31bf71..c999c9dca0 100644
--- a/apps/ios/Shared/Views/Chat/ComposeMessage/ComposeView.swift
+++ b/apps/ios/Shared/Views/Chat/ComposeMessage/ComposeView.swift
@@ -167,25 +167,23 @@ struct ComposeState {
}
func chatItemPreview(chatItem: ChatItem) -> ComposePreview {
- let chatItemPreview: ComposePreview
switch chatItem.content.msgContent {
case .text:
- chatItemPreview = .noPreview
+ return .noPreview
case let .link(_, preview: preview):
- chatItemPreview = .linkPreview(linkPreview: preview)
+ return .linkPreview(linkPreview: preview)
case let .image(_, image):
- chatItemPreview = .mediaPreviews(mediaPreviews: [(image, nil)])
+ return .mediaPreviews(mediaPreviews: [(image, nil)])
case let .video(_, image, _):
- chatItemPreview = .mediaPreviews(mediaPreviews: [(image, nil)])
+ return .mediaPreviews(mediaPreviews: [(image, nil)])
case let .voice(_, duration):
- chatItemPreview = .voicePreview(recordingFileName: chatItem.file?.fileName ?? "", duration: duration)
+ return .voicePreview(recordingFileName: chatItem.file?.fileName ?? "", duration: duration)
case .file:
let fileName = chatItem.file?.fileName ?? ""
- chatItemPreview = .filePreview(fileName: fileName, file: getAppFilePath(fileName))
+ return .filePreview(fileName: fileName, file: getAppFilePath(fileName))
default:
- chatItemPreview = .noPreview
+ return .noPreview
}
- return chatItemPreview
}
enum UploadContent: Equatable {
@@ -656,10 +654,10 @@ struct ComposeView: View {
}
case let .voicePreview(recordingFileName, duration):
stopPlayback.toggle()
- chatModel.filesToDelete.remove(getAppFilePath(recordingFileName))
- sent = await send(.voice(text: msgText, duration: duration), quoted: quoted, file: recordingFileName, ttl: ttl)
+ let file = voiceCryptoFile(recordingFileName)
+ sent = await send(.voice(text: msgText, duration: duration), quoted: quoted, file: file, ttl: ttl)
case let .filePreview(_, file):
- if let savedFile = saveFileFromURL(file) {
+ if let savedFile = saveFileFromURL(file, encrypted: privacyEncryptLocalFilesGroupDefault.get()) {
sent = await send(.file(msgText), quoted: quoted, file: savedFile, live: live, ttl: ttl)
}
}
@@ -727,13 +725,28 @@ struct ComposeView: View {
func sendVideo(_ imageData: (String, UploadContent?), text: String = "", quoted: Int64? = nil, live: Bool = false, ttl: Int?) async -> ChatItem? {
let (image, data) = imageData
- if case let .video(_, url, duration) = data, let savedFile = saveFileFromURLWithoutLoad(url) {
+ if case let .video(_, url, duration) = data, let savedFile = moveTempFileFromURL(url) {
return await send(.video(text: text, image: image, duration: duration), quoted: quoted, file: savedFile, live: live, ttl: ttl)
}
return nil
}
- func send(_ mc: MsgContent, quoted: Int64?, file: String? = nil, live: Bool = false, ttl: Int?) async -> ChatItem? {
+ func voiceCryptoFile(_ fileName: String) -> CryptoFile? {
+ if !privacyEncryptLocalFilesGroupDefault.get() {
+ return CryptoFile.plain(fileName)
+ }
+ let url = getAppFilePath(fileName)
+ let toFile = generateNewFileName("voice", "m4a")
+ let toUrl = getAppFilePath(toFile)
+ if let cfArgs = try? encryptCryptoFile(fromPath: url.path, toPath: toUrl.path) {
+ removeFile(url)
+ return CryptoFile(filePath: toFile, cryptoArgs: cfArgs)
+ } else {
+ return nil
+ }
+ }
+
+ func send(_ mc: MsgContent, quoted: Int64?, file: CryptoFile? = nil, live: Bool = false, ttl: Int?) async -> ChatItem? {
if let chatItem = await apiSendMessage(
type: chat.chatInfo.chatType,
id: chat.chatInfo.apiId,
@@ -750,7 +763,7 @@ struct ComposeView: View {
return chatItem
}
if let file = file {
- removeFile(file)
+ removeFile(file.filePath)
}
return nil
}
@@ -770,7 +783,7 @@ struct ComposeView: View {
}
}
- func saveAnyImage(_ img: UploadContent) -> String? {
+ func saveAnyImage(_ img: UploadContent) -> CryptoFile? {
switch img {
case let .simpleImage(image): return saveImage(image)
case let .animatedImage(image): return saveAnimImage(image)
diff --git a/apps/ios/Shared/Views/Chat/ComposeMessage/ComposeVoiceView.swift b/apps/ios/Shared/Views/Chat/ComposeMessage/ComposeVoiceView.swift
index 2bd23f8ae7..2617bc77bc 100644
--- a/apps/ios/Shared/Views/Chat/ComposeMessage/ComposeVoiceView.swift
+++ b/apps/ios/Shared/Views/Chat/ComposeMessage/ComposeVoiceView.swift
@@ -188,7 +188,7 @@ struct ComposeVoiceView: View {
playbackTime = recordingTime // animate progress bar to the end
}
)
- audioPlayer?.start(fileName: recordingFileName, at: playbackTime)
+ audioPlayer?.start(fileSource: CryptoFile.plain(recordingFileName), at: playbackTime)
playbackState = .playing
}
}
diff --git a/apps/ios/Shared/Views/Helpers/ShareSheet.swift b/apps/ios/Shared/Views/Helpers/ShareSheet.swift
index 15883f8340..936c6cb3ab 100644
--- a/apps/ios/Shared/Views/Helpers/ShareSheet.swift
+++ b/apps/ios/Shared/Views/Helpers/ShareSheet.swift
@@ -8,11 +8,15 @@
import SwiftUI
-func showShareSheet(items: [Any]) {
+func showShareSheet(items: [Any], completed: (() -> Void)? = nil) {
let keyWindowScene = UIApplication.shared.connectedScenes.first { $0.activationState == .foregroundActive } as? UIWindowScene
if let keyWindow = keyWindowScene?.windows.filter(\.isKeyWindow).first,
let presentedViewController = keyWindow.rootViewController?.presentedViewController ?? keyWindow.rootViewController {
let activityViewController = UIActivityViewController(activityItems: items, applicationActivities: nil)
+ if let completed = completed {
+ let handler: UIActivityViewController.CompletionWithItemsHandler = { _,_,_,_ in completed() }
+ activityViewController.completionWithItemsHandler = handler
+ }
presentedViewController.present(activityViewController, animated: true)
}
}
diff --git a/apps/ios/Shared/Views/UserSettings/PrivacySettings.swift b/apps/ios/Shared/Views/UserSettings/PrivacySettings.swift
index 4b583caba9..34b6f147bd 100644
--- a/apps/ios/Shared/Views/UserSettings/PrivacySettings.swift
+++ b/apps/ios/Shared/Views/UserSettings/PrivacySettings.swift
@@ -15,6 +15,7 @@ struct PrivacySettings: View {
@AppStorage(DEFAULT_PRIVACY_LINK_PREVIEWS) private var useLinkPreviews = true
@AppStorage(DEFAULT_PRIVACY_SHOW_CHAT_PREVIEWS) private var showChatPreviews = true
@AppStorage(DEFAULT_PRIVACY_SAVE_LAST_DRAFT) private var saveLastDraft = true
+ @AppStorage(GROUP_DEFAULT_PRIVACY_ENCRYPT_LOCAL_FILES, store: groupDefaults) private var encryptLocalFiles = true
@State private var simplexLinkMode = privacySimplexLinkModeDefault.get()
@AppStorage(DEFAULT_PRIVACY_PROTECT_SCREEN) private var protectScreen = false
@AppStorage(DEFAULT_PERFORM_LA) private var prefPerformLA = false
@@ -63,6 +64,9 @@ struct PrivacySettings: View {
}
Section {
+ settingsRow("lock.doc") {
+ Toggle("Encrypt local files", isOn: $encryptLocalFiles)
+ }
settingsRow("photo") {
Toggle("Auto-accept images", isOn: $autoAcceptImages)
.onChange(of: autoAcceptImages) {
diff --git a/apps/ios/SimpleX Localizations/ar.xcloc/Localized Contents/ar.xliff b/apps/ios/SimpleX Localizations/ar.xcloc/Localized Contents/ar.xliff
index 980e511428..e0477899be 100644
--- a/apps/ios/SimpleX Localizations/ar.xcloc/Localized Contents/ar.xliff
+++ b/apps/ios/SimpleX Localizations/ar.xcloc/Localized Contents/ar.xliff
@@ -3655,6 +3655,26 @@ SimpleX servers cannot see your profile.
%1$@ في %2$@:
copied message info, <sender> at <time>
+
+ # %@
+ # %@
+ copied message info title, # <title>
+
+
+ ## History
+ ## السجل
+ copied message info
+
+
+ ## In reply to
+ ## ردًا على
+ copied message info
+
+
+ %@ and %@ connected
+ %@ و %@ متصل
+ No comment provided by engineer.
+