mirror of
https://github.com/simplex-chat/simplex-chat.git
synced 2025-06-29 12:49:53 +00:00
ui: types and stubs to encrypt local files (#3003)
* ui: types and stubs to encrypt local files * ios: encrypt automatically received images in local storage * encrypt sent images, marked to be received via NSE * ios: encrypt sent and received local voice files * encrypt sent and received local files * fix NSE * remove comment * decrypt files in background thread
This commit is contained in:
parent
a27f30ce12
commit
748572ace9
36 changed files with 407 additions and 197 deletions
|
@ -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 {
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -271,7 +271,7 @@ func receivedMsgNtf(_ res: ChatResponse) async -> (String, NSENotification)? {
|
|||
ntfBadgeCountGroupDefault.set(max(0, ntfBadgeCountGroupDefault.get() - 1))
|
||||
}
|
||||
if let file = cItem.autoReceiveFile() {
|
||||
cItem = autoReceiveFile(file) ?? cItem
|
||||
cItem = autoReceiveFile(file, encrypted: cItem.encryptLocalFile) ?? cItem
|
||||
}
|
||||
let ntf: NSENotification = cInfo.ntfsEnabled ? .nse(notification: createMessageReceivedNtf(user, cInfo, cItem)) : .empty
|
||||
return cItem.showNotification ? (aChatItem.chatId, ntf) : nil
|
||||
|
@ -367,25 +367,25 @@ func apiGetNtfMessage(nonce: String, encNtfInfo: String) -> NtfMessages? {
|
|||
return nil
|
||||
}
|
||||
|
||||
func apiReceiveFile(fileId: Int64, inline: Bool? = nil) -> AChatItem? {
|
||||
let r = sendSimpleXCmd(.receiveFile(fileId: fileId, inline: inline))
|
||||
func apiReceiveFile(fileId: Int64, encrypted: Bool, inline: Bool? = nil) -> AChatItem? {
|
||||
let r = sendSimpleXCmd(.receiveFile(fileId: fileId, encrypted: encrypted, inline: inline))
|
||||
if case let .rcvFileAccepted(_, chatItem) = r { return chatItem }
|
||||
logger.error("receiveFile error: \(responseError(r))")
|
||||
return nil
|
||||
}
|
||||
|
||||
func apiSetFileToReceive(fileId: Int64) {
|
||||
let r = sendSimpleXCmd(.setFileToReceive(fileId: fileId))
|
||||
func apiSetFileToReceive(fileId: Int64, encrypted: Bool) {
|
||||
let r = sendSimpleXCmd(.setFileToReceive(fileId: fileId, encrypted: encrypted))
|
||||
if case .cmdOk = r { return }
|
||||
logger.error("setFileToReceive error: \(responseError(r))")
|
||||
}
|
||||
|
||||
func autoReceiveFile(_ file: CIFile) -> ChatItem? {
|
||||
func autoReceiveFile(_ file: CIFile, encrypted: Bool) -> ChatItem? {
|
||||
switch file.fileProtocol {
|
||||
case .smp:
|
||||
return apiReceiveFile(fileId: file.fileId)?.chatItem
|
||||
return apiReceiveFile(fileId: file.fileId, encrypted: false)?.chatItem
|
||||
case .xftp:
|
||||
apiSetFileToReceive(fileId: file.fileId)
|
||||
apiSetFileToReceive(fileId: file.fileId, encrypted: encrypted)
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
|
|
@ -77,6 +77,7 @@
|
|||
5C9CC7A928C532AB00BEF955 /* DatabaseErrorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C9CC7A828C532AB00BEF955 /* DatabaseErrorView.swift */; };
|
||||
5C9CC7AD28C55D7800BEF955 /* DatabaseEncryptionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C9CC7AC28C55D7800BEF955 /* DatabaseEncryptionView.swift */; };
|
||||
5C9D13A3282187BB00AB8B43 /* WebRTC.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C9D13A2282187BB00AB8B43 /* WebRTC.swift */; };
|
||||
5C9D811A2AA8727A001D49FD /* CryptoFile.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C9D81182AA7A4F1001D49FD /* CryptoFile.swift */; };
|
||||
5C9F83F42A9A7D98009AD0AA /* libffi.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5C9F83EF2A9A7D98009AD0AA /* libffi.a */; };
|
||||
5C9F83F52A9A7D98009AD0AA /* libgmp.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5C9F83F02A9A7D98009AD0AA /* libgmp.a */; };
|
||||
5C9F83F62A9A7D98009AD0AA /* libgmpxx.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5C9F83F12A9A7D98009AD0AA /* libgmpxx.a */; };
|
||||
|
@ -331,6 +332,7 @@
|
|||
5C9CC7A828C532AB00BEF955 /* DatabaseErrorView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DatabaseErrorView.swift; sourceTree = "<group>"; };
|
||||
5C9CC7AC28C55D7800BEF955 /* DatabaseEncryptionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DatabaseEncryptionView.swift; sourceTree = "<group>"; };
|
||||
5C9D13A2282187BB00AB8B43 /* WebRTC.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WebRTC.swift; sourceTree = "<group>"; };
|
||||
5C9D81182AA7A4F1001D49FD /* CryptoFile.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CryptoFile.swift; sourceTree = "<group>"; };
|
||||
5C9F83EF2A9A7D98009AD0AA /* libffi.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libffi.a; sourceTree = "<group>"; };
|
||||
5C9F83F02A9A7D98009AD0AA /* libgmp.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libgmp.a; sourceTree = "<group>"; };
|
||||
5C9F83F12A9A7D98009AD0AA /* libgmpxx.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libgmpxx.a; sourceTree = "<group>"; };
|
||||
|
@ -723,10 +725,10 @@
|
|||
5CADE79929211BB900072E13 /* PreferencesView.swift */,
|
||||
5C5DB70D289ABDD200730FFF /* AppearanceSettings.swift */,
|
||||
5C05DF522840AA1D00C683F9 /* CallSettings.swift */,
|
||||
5C3F1D57284363C400EC8A82 /* PrivacySettings.swift */,
|
||||
5CB924E027A867BA00ACCCDD /* UserProfile.swift */,
|
||||
5CC036DF29C488D500C0EF20 /* HiddenProfileView.swift */,
|
||||
5C577F7C27C83AA10006112D /* MarkdownHelp.swift */,
|
||||
5C3F1D57284363C400EC8A82 /* PrivacySettings.swift */,
|
||||
5C93292E29239A170090FFF9 /* ProtocolServersView.swift */,
|
||||
5C93293029239BED0090FFF9 /* ProtocolServerView.swift */,
|
||||
5C9329402929248A0090FFF9 /* ScanProtocolServer.swift */,
|
||||
|
@ -779,6 +781,7 @@
|
|||
5CDCAD7D2818941F00503DA2 /* API.swift */,
|
||||
5CDCAD80281A7E2700503DA2 /* Notifications.swift */,
|
||||
64DAE1502809D9F5000DA960 /* FileUtils.swift */,
|
||||
5C9D81182AA7A4F1001D49FD /* CryptoFile.swift */,
|
||||
5C00168028C4FE760094D739 /* KeyChain.swift */,
|
||||
5CE2BA76284530BF00EC33A6 /* SimpleXChat.h */,
|
||||
5CE2BA8A2845332200EC33A6 /* SimpleX.h */,
|
||||
|
@ -1236,6 +1239,7 @@
|
|||
5CE2BA90284533A300EC33A6 /* JSON.swift in Sources */,
|
||||
5CE2BA8B284533A300EC33A6 /* ChatTypes.swift in Sources */,
|
||||
5CE2BA8F284533A300EC33A6 /* APITypes.swift in Sources */,
|
||||
5C9D811A2AA8727A001D49FD /* CryptoFile.swift in Sources */,
|
||||
5CE2BA8C284533A300EC33A6 /* AppGroup.swift in Sources */,
|
||||
5CE2BA8D284533A300EC33A6 /* CallTypes.swift in Sources */,
|
||||
5CE2BA8E284533A300EC33A6 /* API.swift in Sources */,
|
||||
|
|
|
@ -9,7 +9,7 @@
|
|||
import Foundation
|
||||
import SwiftUI
|
||||
|
||||
let jsonDecoder = getJSONDecoder()
|
||||
public let jsonDecoder = getJSONDecoder()
|
||||
let jsonEncoder = getJSONEncoder()
|
||||
|
||||
public enum ChatCommand {
|
||||
|
@ -39,7 +39,7 @@ public enum ChatCommand {
|
|||
case apiGetChats(userId: Int64)
|
||||
case apiGetChat(type: ChatType, id: Int64, pagination: ChatPagination, search: String)
|
||||
case apiGetChatItemInfo(type: ChatType, id: Int64, itemId: Int64)
|
||||
case apiSendMessage(type: ChatType, id: Int64, file: String?, quotedItemId: Int64?, msg: MsgContent, live: Bool, ttl: Int?)
|
||||
case apiSendMessage(type: ChatType, id: Int64, file: CryptoFile?, quotedItemId: Int64?, msg: MsgContent, live: Bool, ttl: Int?)
|
||||
case apiUpdateChatItem(type: ChatType, id: Int64, itemId: Int64, msg: MsgContent, live: Bool)
|
||||
case apiDeleteChatItem(type: ChatType, id: Int64, itemId: Int64, mode: CIDeleteMode)
|
||||
case apiDeleteMemberChatItem(groupId: Int64, groupMemberId: Int64, itemId: Int64)
|
||||
|
@ -110,8 +110,8 @@ public enum ChatCommand {
|
|||
case apiCallStatus(contact: Contact, callStatus: WebRTCCallStatus)
|
||||
case apiChatRead(type: ChatType, id: Int64, itemRange: (Int64, Int64))
|
||||
case apiChatUnread(type: ChatType, id: Int64, unreadChat: Bool)
|
||||
case receiveFile(fileId: Int64, inline: Bool?)
|
||||
case setFileToReceive(fileId: Int64)
|
||||
case receiveFile(fileId: Int64, encrypted: Bool, inline: Bool?)
|
||||
case setFileToReceive(fileId: Int64, encrypted: Bool)
|
||||
case cancelFile(fileId: Int64)
|
||||
case showVersion
|
||||
case string(String)
|
||||
|
@ -157,7 +157,7 @@ public enum ChatCommand {
|
|||
(search == "" ? "" : " search=\(search)")
|
||||
case let .apiGetChatItemInfo(type, id, itemId): return "/_get item info \(ref(type, id)) \(itemId)"
|
||||
case let .apiSendMessage(type, id, file, quotedItemId, mc, live, ttl):
|
||||
let msg = encodeJSON(ComposedMessage(filePath: file, quotedItemId: quotedItemId, msgContent: mc))
|
||||
let msg = encodeJSON(ComposedMessage(fileSource: file, quotedItemId: quotedItemId, msgContent: mc))
|
||||
let ttlStr = ttl != nil ? "\(ttl!)" : "default"
|
||||
return "/_send \(ref(type, id)) live=\(onOff(live)) ttl=\(ttlStr) json \(msg)"
|
||||
case let .apiUpdateChatItem(type, id, itemId, mc, live): return "/_update item \(ref(type, id)) \(itemId) live=\(onOff(live)) \(mc.cmdString)"
|
||||
|
@ -239,12 +239,13 @@ public enum ChatCommand {
|
|||
case let .apiCallStatus(contact, callStatus): return "/_call status @\(contact.apiId) \(callStatus.rawValue)"
|
||||
case let .apiChatRead(type, id, itemRange: (from, to)): return "/_read chat \(ref(type, id)) from=\(from) to=\(to)"
|
||||
case let .apiChatUnread(type, id, unreadChat): return "/_unread chat \(ref(type, id)) \(onOff(unreadChat))"
|
||||
case let .receiveFile(fileId, inline):
|
||||
case let .receiveFile(fileId, encrypted, inline):
|
||||
let s = "/freceive \(fileId) encrypt=\(onOff(encrypted))"
|
||||
if let inline = inline {
|
||||
return "/freceive \(fileId) inline=\(onOff(inline))"
|
||||
return s + " inline=\(onOff(inline))"
|
||||
}
|
||||
return "/freceive \(fileId)"
|
||||
case let .setFileToReceive(fileId): return "/_set_file_to_receive \(fileId)"
|
||||
return s
|
||||
case let .setFileToReceive(fileId, encrypted): return "/_set_file_to_receive \(fileId) encrypt=\(onOff(encrypted))"
|
||||
case let .cancelFile(fileId): return "/fcancel \(fileId)"
|
||||
case .showVersion: return "/version"
|
||||
case let .string(str): return str
|
||||
|
@ -853,7 +854,7 @@ public enum ChatPagination {
|
|||
}
|
||||
|
||||
struct ComposedMessage: Encodable {
|
||||
var filePath: String?
|
||||
var fileSource: CryptoFile?
|
||||
var quotedItemId: Int64?
|
||||
var msgContent: MsgContent
|
||||
}
|
||||
|
|
|
@ -17,6 +17,7 @@ public let GROUP_DEFAULT_NTF_ENABLE_LOCAL = "ntfEnableLocal"
|
|||
public let GROUP_DEFAULT_NTF_ENABLE_PERIODIC = "ntfEnablePeriodic"
|
||||
let GROUP_DEFAULT_PRIVACY_ACCEPT_IMAGES = "privacyAcceptImages"
|
||||
public let GROUP_DEFAULT_PRIVACY_TRANSFER_IMAGES_INLINE = "privacyTransferImagesInline" // no longer used
|
||||
public let GROUP_DEFAULT_PRIVACY_ENCRYPT_LOCAL_FILES = "privacyEncryptLocalFiles"
|
||||
let GROUP_DEFAULT_NTF_BADGE_COUNT = "ntgBadgeCount"
|
||||
let GROUP_DEFAULT_NETWORK_USE_ONION_HOSTS = "networkUseOnionHosts"
|
||||
let GROUP_DEFAULT_NETWORK_SESSION_MODE = "networkSessionMode"
|
||||
|
@ -59,6 +60,7 @@ public func registerGroupDefaults() {
|
|||
GROUP_DEFAULT_INITIAL_RANDOM_DB_PASSPHRASE: false,
|
||||
GROUP_DEFAULT_PRIVACY_ACCEPT_IMAGES: true,
|
||||
GROUP_DEFAULT_PRIVACY_TRANSFER_IMAGES_INLINE: false,
|
||||
GROUP_DEFAULT_PRIVACY_ENCRYPT_LOCAL_FILES: true,
|
||||
GROUP_DEFAULT_CONFIRM_DB_UPGRADES: false,
|
||||
GROUP_DEFAULT_CALL_KIT_ENABLED: true,
|
||||
])
|
||||
|
@ -113,7 +115,7 @@ public let ntfEnablePeriodicGroupDefault = BoolDefault(defaults: groupDefaults,
|
|||
|
||||
public let privacyAcceptImagesGroupDefault = BoolDefault(defaults: groupDefaults, forKey: GROUP_DEFAULT_PRIVACY_ACCEPT_IMAGES)
|
||||
|
||||
public let privacyTransferImagesInlineGroupDefault = BoolDefault(defaults: groupDefaults, forKey: GROUP_DEFAULT_PRIVACY_TRANSFER_IMAGES_INLINE)
|
||||
public let privacyEncryptLocalFilesGroupDefault = BoolDefault(defaults: groupDefaults, forKey: GROUP_DEFAULT_PRIVACY_ENCRYPT_LOCAL_FILES)
|
||||
|
||||
public let ntfBadgeCountGroupDefault = IntDefault(defaults: groupDefaults, forKey: GROUP_DEFAULT_NTF_BADGE_COUNT)
|
||||
|
||||
|
|
|
@ -2112,6 +2112,17 @@ public struct ChatItem: Identifiable, Decodable {
|
|||
return nil
|
||||
}
|
||||
|
||||
public var encryptedFile: Bool? {
|
||||
guard let fileSource = file?.fileSource else { return nil }
|
||||
return fileSource.cryptoArgs != nil
|
||||
}
|
||||
|
||||
public var encryptLocalFile: Bool {
|
||||
file?.fileProtocol == .xftp &&
|
||||
content.msgContent?.isVideo == false &&
|
||||
privacyEncryptLocalFilesGroupDefault.get()
|
||||
}
|
||||
|
||||
public var memberDisplayName: String? {
|
||||
get {
|
||||
if case let .groupRcv(groupMember) = chatDir {
|
||||
|
@ -2690,12 +2701,18 @@ public struct CIFile: Decodable {
|
|||
public var fileId: Int64
|
||||
public var fileName: String
|
||||
public var fileSize: Int64
|
||||
public var filePath: String?
|
||||
public var fileSource: CryptoFile?
|
||||
public var fileStatus: CIFileStatus
|
||||
public var fileProtocol: FileProtocol
|
||||
|
||||
public static func getSample(fileId: Int64 = 1, fileName: String = "test.txt", fileSize: Int64 = 100, filePath: String? = "test.txt", fileStatus: CIFileStatus = .rcvComplete) -> CIFile {
|
||||
CIFile(fileId: fileId, fileName: fileName, fileSize: fileSize, filePath: filePath, fileStatus: fileStatus, fileProtocol: .xftp)
|
||||
let f: CryptoFile?
|
||||
if let filePath = filePath {
|
||||
f = CryptoFile.plain(filePath)
|
||||
} else {
|
||||
f = nil
|
||||
}
|
||||
return CIFile(fileId: fileId, fileName: fileName, fileSize: fileSize, fileSource: f, fileStatus: fileStatus, fileProtocol: .xftp)
|
||||
}
|
||||
|
||||
public var loaded: Bool {
|
||||
|
@ -2742,6 +2759,25 @@ public struct CIFile: Decodable {
|
|||
}
|
||||
}
|
||||
|
||||
public struct CryptoFile: Codable {
|
||||
public var filePath: String // the name of the file, not a full path
|
||||
public var cryptoArgs: CryptoFileArgs?
|
||||
|
||||
public init(filePath: String, cryptoArgs: CryptoFileArgs?) {
|
||||
self.filePath = filePath
|
||||
self.cryptoArgs = cryptoArgs
|
||||
}
|
||||
|
||||
public static func plain(_ f: String) -> CryptoFile {
|
||||
CryptoFile(filePath: f, cryptoArgs: nil)
|
||||
}
|
||||
}
|
||||
|
||||
public struct CryptoFileArgs: Codable {
|
||||
public var fileKey: String
|
||||
public var fileNonce: String
|
||||
}
|
||||
|
||||
public struct CancelAction {
|
||||
public var uiAction: String
|
||||
public var alert: AlertInfo
|
||||
|
|
64
apps/ios/SimpleXChat/CryptoFile.swift
Normal file
64
apps/ios/SimpleXChat/CryptoFile.swift
Normal file
|
@ -0,0 +1,64 @@
|
|||
//
|
||||
// CryptoFile.swift
|
||||
// SimpleX (iOS)
|
||||
//
|
||||
// Created by Evgeny on 05/09/2023.
|
||||
// Copyright © 2023 SimpleX Chat. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
enum WriteFileResult: Decodable {
|
||||
case result(cryptoArgs: CryptoFileArgs)
|
||||
case error(writeError: String)
|
||||
}
|
||||
|
||||
public func writeCryptoFile(path: String, data: Data) throws -> CryptoFileArgs {
|
||||
let ptr: UnsafeMutableRawPointer = malloc(data.count)
|
||||
memcpy(ptr, (data as NSData).bytes, data.count)
|
||||
var cPath = path.cString(using: .utf8)!
|
||||
let cjson = chat_write_file(&cPath, ptr, Int32(data.count))!
|
||||
let d = fromCString(cjson).data(using: .utf8)!
|
||||
switch try jsonDecoder.decode(WriteFileResult.self, from: d) {
|
||||
case let .result(cfArgs): return cfArgs
|
||||
case let .error(err): throw RuntimeError(err)
|
||||
}
|
||||
}
|
||||
|
||||
enum ReadFileResult: Decodable {
|
||||
case result(fileSize: Int)
|
||||
case error(readError: String)
|
||||
}
|
||||
|
||||
public func readCryptoFile(path: String, cryptoArgs: CryptoFileArgs) throws -> Data {
|
||||
var cPath = path.cString(using: .utf8)!
|
||||
var cKey = cryptoArgs.fileKey.cString(using: .utf8)!
|
||||
var cNonce = cryptoArgs.fileNonce.cString(using: .utf8)!
|
||||
let r = chat_read_file(&cPath, &cKey, &cNonce)!
|
||||
let d = String.init(cString: r).data(using: .utf8)!
|
||||
switch try jsonDecoder.decode(ReadFileResult.self, from: d) {
|
||||
case let .error(err): throw RuntimeError(err)
|
||||
case let .result(size): return Data(bytes: r.advanced(by: d.count + 1), count: size)
|
||||
}
|
||||
}
|
||||
|
||||
public func encryptCryptoFile(fromPath: String, toPath: String) throws -> CryptoFileArgs {
|
||||
var cFromPath = fromPath.cString(using: .utf8)!
|
||||
var cToPath = toPath.cString(using: .utf8)!
|
||||
let cjson = chat_encrypt_file(&cFromPath, &cToPath)!
|
||||
let d = fromCString(cjson).data(using: .utf8)!
|
||||
switch try jsonDecoder.decode(WriteFileResult.self, from: d) {
|
||||
case let .result(cfArgs): return cfArgs
|
||||
case let .error(err): throw RuntimeError(err)
|
||||
}
|
||||
}
|
||||
|
||||
public func decryptCryptoFile(fromPath: String, cryptoArgs: CryptoFileArgs, toPath: String) throws {
|
||||
var cFromPath = fromPath.cString(using: .utf8)!
|
||||
var cKey = cryptoArgs.fileKey.cString(using: .utf8)!
|
||||
var cNonce = cryptoArgs.fileNonce.cString(using: .utf8)!
|
||||
var cToPath = toPath.cString(using: .utf8)!
|
||||
let cErr = chat_decrypt_file(&cFromPath, &cKey, &cNonce, &cToPath)!
|
||||
let err = fromCString(cErr)
|
||||
if err != "" { throw RuntimeError(err) }
|
||||
}
|
|
@ -173,11 +173,16 @@ public func getAppFilePath(_ fileName: String) -> URL {
|
|||
getAppFilesDirectory().appendingPathComponent(fileName)
|
||||
}
|
||||
|
||||
public func saveFile(_ data: Data, _ fileName: String) -> String? {
|
||||
public func saveFile(_ data: Data, _ fileName: String, encrypted: Bool) -> CryptoFile? {
|
||||
let filePath = getAppFilePath(fileName)
|
||||
do {
|
||||
try data.write(to: filePath)
|
||||
return fileName
|
||||
if encrypted {
|
||||
let cfArgs = try writeCryptoFile(path: filePath.path, data: data)
|
||||
return CryptoFile(filePath: fileName, cryptoArgs: cfArgs)
|
||||
} else {
|
||||
try data.write(to: filePath)
|
||||
return CryptoFile.plain(fileName)
|
||||
}
|
||||
} catch {
|
||||
logger.error("FileUtils.saveFile error: \(error.localizedDescription)")
|
||||
return nil
|
||||
|
@ -210,7 +215,7 @@ public func cleanupFile(_ aChatItem: AChatItem) {
|
|||
let cItem = aChatItem.chatItem
|
||||
let mc = cItem.content.msgContent
|
||||
if case .file = mc,
|
||||
let fileName = cItem.file?.filePath {
|
||||
let fileName = cItem.file?.fileSource?.filePath {
|
||||
removeFile(fileName)
|
||||
}
|
||||
}
|
||||
|
@ -221,3 +226,15 @@ public func getMaxFileSize(_ fileProtocol: FileProtocol) -> Int64 {
|
|||
case .smp: return MAX_FILE_SIZE_SMP
|
||||
}
|
||||
}
|
||||
|
||||
public struct RuntimeError: Error {
|
||||
let message: String
|
||||
|
||||
public init(_ message: String) {
|
||||
self.message = message
|
||||
}
|
||||
|
||||
public var localizedDescription: String {
|
||||
return message
|
||||
}
|
||||
}
|
||||
|
|
|
@ -25,3 +25,17 @@ extern char *chat_parse_server(char *str);
|
|||
extern char *chat_password_hash(char *pwd, char *salt);
|
||||
extern char *chat_encrypt_media(char *key, char *frame, int len);
|
||||
extern char *chat_decrypt_media(char *key, char *frame, int len);
|
||||
|
||||
// chat_write_file returns NUL-terminated string with JSON of WriteFileResult
|
||||
extern char *chat_write_file(char *path, char *data, int len);
|
||||
|
||||
// chat_read_file returns a buffer with:
|
||||
// 1. NUL-terminated C string with JSON of ReadFileResult, followed by
|
||||
// 2. file data, the length is defined in ReadFileResult
|
||||
extern char *chat_read_file(char *path, char *key, char *nonce);
|
||||
|
||||
// chat_encrypt_file returns NUL-terminated string with JSON of WriteFileResult
|
||||
extern char *chat_encrypt_file(char *fromPath, char *toPath);
|
||||
|
||||
// chat_decrypt_file returns NUL-terminated string with the error message
|
||||
extern char *chat_decrypt_file(char *fromPath, char *key, char *nonce, char *toPath);
|
||||
|
|
|
@ -2024,7 +2024,7 @@ class CIFile(
|
|||
val fileId: Long,
|
||||
val fileName: String,
|
||||
val fileSize: Long,
|
||||
val filePath: String? = null,
|
||||
val fileSource: CryptoFile? = null,
|
||||
val fileStatus: CIFileStatus,
|
||||
val fileProtocol: FileProtocol
|
||||
) {
|
||||
|
@ -2072,10 +2072,23 @@ class CIFile(
|
|||
filePath: String? = "test.txt",
|
||||
fileStatus: CIFileStatus = CIFileStatus.RcvComplete
|
||||
): CIFile =
|
||||
CIFile(fileId = fileId, fileName = fileName, fileSize = fileSize, filePath = filePath, fileStatus = fileStatus, fileProtocol = FileProtocol.XFTP)
|
||||
CIFile(fileId = fileId, fileName = fileName, fileSize = fileSize, fileSource = if (filePath == null) null else CryptoFile.plain(filePath), fileStatus = fileStatus, fileProtocol = FileProtocol.XFTP)
|
||||
}
|
||||
}
|
||||
|
||||
@Serializable
|
||||
class CryptoFile(
|
||||
val filePath: String,
|
||||
val cryptoArgs: CryptoFileArgs?
|
||||
) {
|
||||
companion object {
|
||||
fun plain(f: String): CryptoFile = CryptoFile(f, null)
|
||||
}
|
||||
}
|
||||
|
||||
@Serializable
|
||||
class CryptoFileArgs(val fileKey: String, val fileNonce: String)
|
||||
|
||||
class CancelAction(
|
||||
val uiActionId: StringResource,
|
||||
val alert: AlertInfo
|
||||
|
|
|
@ -586,7 +586,7 @@ object ChatController {
|
|||
return null
|
||||
}
|
||||
|
||||
suspend fun apiSendMessage(type: ChatType, id: Long, file: String? = null, quotedItemId: Long? = null, mc: MsgContent, live: Boolean = false, ttl: Int? = null): AChatItem? {
|
||||
suspend fun apiSendMessage(type: ChatType, id: Long, file: CryptoFile? = null, quotedItemId: Long? = null, mc: MsgContent, live: Boolean = false, ttl: Int? = null): AChatItem? {
|
||||
val cmd = CC.ApiSendMessage(type, id, file, quotedItemId, mc, live, ttl)
|
||||
val r = sendCmd(cmd)
|
||||
return when (r) {
|
||||
|
@ -1079,8 +1079,8 @@ object ChatController {
|
|||
return false
|
||||
}
|
||||
|
||||
suspend fun apiReceiveFile(fileId: Long, inline: Boolean? = null, auto: Boolean = false): AChatItem? {
|
||||
val r = sendCmd(CC.ReceiveFile(fileId, inline))
|
||||
suspend fun apiReceiveFile(fileId: Long, encrypted: Boolean, inline: Boolean? = null, auto: Boolean = false): AChatItem? {
|
||||
val r = sendCmd(CC.ReceiveFile(fileId, encrypted, inline))
|
||||
return when (r) {
|
||||
is CR.RcvFileAccepted -> r.chatItem
|
||||
is CR.RcvFileAcceptedSndCancelled -> {
|
||||
|
@ -1413,7 +1413,8 @@ object ChatController {
|
|||
((mc is MsgContent.MCImage && file.fileSize <= MAX_IMAGE_SIZE_AUTO_RCV)
|
||||
|| (mc is MsgContent.MCVideo && file.fileSize <= MAX_VIDEO_SIZE_AUTO_RCV)
|
||||
|| (mc is MsgContent.MCVoice && file.fileSize <= MAX_VOICE_SIZE_AUTO_RCV && file.fileStatus !is CIFileStatus.RcvAccepted))) {
|
||||
withApi { receiveFile(r.user, file.fileId, auto = true) }
|
||||
// TODO encrypt images and voice
|
||||
withApi { receiveFile(r.user, file.fileId, encrypted = false, auto = true) }
|
||||
}
|
||||
if (cItem.showNotification && (allowedToShowNotification() || chatModel.chatId.value != cInfo.id)) {
|
||||
ntfManager.notifyMessageReceived(r.user, cInfo, cItem)
|
||||
|
@ -1647,8 +1648,8 @@ object ChatController {
|
|||
}
|
||||
}
|
||||
|
||||
suspend fun receiveFile(user: UserLike, fileId: Long, auto: Boolean = false) {
|
||||
val chatItem = apiReceiveFile(fileId, auto = auto)
|
||||
suspend fun receiveFile(user: UserLike, fileId: Long, encrypted: Boolean, auto: Boolean = false) {
|
||||
val chatItem = apiReceiveFile(fileId, encrypted = encrypted, auto = auto)
|
||||
if (chatItem != null) {
|
||||
chatItemSimpleUpdate(user, chatItem)
|
||||
}
|
||||
|
@ -1804,7 +1805,7 @@ sealed class CC {
|
|||
class ApiGetChats(val userId: Long): CC()
|
||||
class ApiGetChat(val type: ChatType, val id: Long, val pagination: ChatPagination, val search: String = ""): CC()
|
||||
class ApiGetChatItemInfo(val type: ChatType, val id: Long, val itemId: Long): CC()
|
||||
class ApiSendMessage(val type: ChatType, val id: Long, val file: String?, val quotedItemId: Long?, val mc: MsgContent, val live: Boolean, val ttl: Int?): CC()
|
||||
class ApiSendMessage(val type: ChatType, val id: Long, val file: CryptoFile?, val quotedItemId: Long?, val mc: MsgContent, val live: Boolean, val ttl: Int?): CC()
|
||||
class ApiUpdateChatItem(val type: ChatType, val id: Long, val itemId: Long, val mc: MsgContent, val live: Boolean): CC()
|
||||
class ApiDeleteChatItem(val type: ChatType, val id: Long, val itemId: Long, val mode: CIDeleteMode): CC()
|
||||
class ApiDeleteMemberChatItem(val groupId: Long, val groupMemberId: Long, val itemId: Long): CC()
|
||||
|
@ -1867,7 +1868,7 @@ sealed class CC {
|
|||
class ApiRejectContact(val contactReqId: Long): CC()
|
||||
class ApiChatRead(val type: ChatType, val id: Long, val range: ItemRange): CC()
|
||||
class ApiChatUnread(val type: ChatType, val id: Long, val unreadChat: Boolean): CC()
|
||||
class ReceiveFile(val fileId: Long, val inline: Boolean?): CC()
|
||||
class ReceiveFile(val fileId: Long, val encrypted: Boolean, val inline: Boolean?): CC()
|
||||
class CancelFile(val fileId: Long): CC()
|
||||
class ShowVersion(): CC()
|
||||
|
||||
|
@ -1972,7 +1973,7 @@ sealed class CC {
|
|||
is ApiCallStatus -> "/_call status @${contact.apiId} ${callStatus.value}"
|
||||
is ApiChatRead -> "/_read chat ${chatRef(type, id)} from=${range.from} to=${range.to}"
|
||||
is ApiChatUnread -> "/_unread chat ${chatRef(type, id)} ${onOff(unreadChat)}"
|
||||
is ReceiveFile -> if (inline == null) "/freceive $fileId" else "/freceive $fileId inline=${onOff(inline)}"
|
||||
is ReceiveFile -> "/freceive $fileId encrypt=${onOff(encrypted)}" + (if (inline == null) "" else " inline=${onOff(inline)}")
|
||||
is CancelFile -> "/fcancel $fileId"
|
||||
is ShowVersion -> "/version"
|
||||
}
|
||||
|
@ -2134,7 +2135,7 @@ sealed class ChatPagination {
|
|||
}
|
||||
|
||||
@Serializable
|
||||
class ComposedMessage(val filePath: String?, val quotedItemId: Long?, val msgContent: MsgContent)
|
||||
class ComposedMessage(val fileSource: CryptoFile?, val quotedItemId: Long?, val msgContent: MsgContent)
|
||||
|
||||
@Serializable
|
||||
class XFTPFileConfig(val minFileSize: Long)
|
||||
|
|
|
@ -62,8 +62,9 @@ fun getAppFilePath(fileName: String): String {
|
|||
}
|
||||
|
||||
fun getLoadedFilePath(file: CIFile?): String? {
|
||||
return if (file?.filePath != null && file.loaded) {
|
||||
val filePath = getAppFilePath(file.filePath)
|
||||
val f = file?.fileSource?.filePath
|
||||
return if (f != null && file.loaded) {
|
||||
val filePath = getAppFilePath(f)
|
||||
if (File(filePath).exists()) filePath else null
|
||||
} else {
|
||||
null
|
||||
|
|
|
@ -244,8 +244,8 @@ fun ChatView(chatId: String, chatModel: ChatModel, onComposed: suspend (chatId:
|
|||
}
|
||||
}
|
||||
},
|
||||
receiveFile = { fileId ->
|
||||
withApi { chatModel.controller.receiveFile(user, fileId) }
|
||||
receiveFile = { fileId, encrypted ->
|
||||
withApi { chatModel.controller.receiveFile(user, fileId, encrypted) }
|
||||
},
|
||||
cancelFile = { fileId ->
|
||||
withApi { chatModel.controller.cancelFile(user, fileId) }
|
||||
|
@ -403,7 +403,7 @@ fun ChatLayout(
|
|||
showMemberInfo: (GroupInfo, GroupMember) -> Unit,
|
||||
loadPrevMessages: (ChatInfo) -> Unit,
|
||||
deleteMessage: (Long, CIDeleteMode) -> Unit,
|
||||
receiveFile: (Long) -> Unit,
|
||||
receiveFile: (Long, Boolean) -> Unit,
|
||||
cancelFile: (Long) -> Unit,
|
||||
joinGroup: (Long) -> Unit,
|
||||
startCall: (CallMediaType) -> Unit,
|
||||
|
@ -656,7 +656,7 @@ fun BoxWithConstraintsScope.ChatItemsList(
|
|||
showMemberInfo: (GroupInfo, GroupMember) -> Unit,
|
||||
loadPrevMessages: (ChatInfo) -> Unit,
|
||||
deleteMessage: (Long, CIDeleteMode) -> Unit,
|
||||
receiveFile: (Long) -> Unit,
|
||||
receiveFile: (Long, Boolean) -> Unit,
|
||||
cancelFile: (Long) -> Unit,
|
||||
joinGroup: (Long) -> Unit,
|
||||
acceptCall: (Contact) -> Unit,
|
||||
|
@ -1257,7 +1257,7 @@ fun PreviewChatLayout() {
|
|||
showMemberInfo = { _, _ -> },
|
||||
loadPrevMessages = { _ -> },
|
||||
deleteMessage = { _, _ -> },
|
||||
receiveFile = {},
|
||||
receiveFile = { _, _ -> },
|
||||
cancelFile = {},
|
||||
joinGroup = {},
|
||||
startCall = {},
|
||||
|
@ -1324,7 +1324,7 @@ fun PreviewGroupChatLayout() {
|
|||
showMemberInfo = { _, _ -> },
|
||||
loadPrevMessages = { _ -> },
|
||||
deleteMessage = { _, _ -> },
|
||||
receiveFile = {},
|
||||
receiveFile = { _, _ -> },
|
||||
cancelFile = {},
|
||||
joinGroup = {},
|
||||
startCall = {},
|
||||
|
|
|
@ -317,7 +317,7 @@ fun ComposeView(
|
|||
chatModel.filesToDelete.clear()
|
||||
}
|
||||
|
||||
suspend fun send(cInfo: ChatInfo, mc: MsgContent, quoted: Long?, file: String? = null, live: Boolean = false, ttl: Int?): ChatItem? {
|
||||
suspend fun send(cInfo: ChatInfo, mc: MsgContent, quoted: Long?, file: CryptoFile? = null, live: Boolean = false, ttl: Int?): ChatItem? {
|
||||
val aChatItem = chatModel.controller.apiSendMessage(
|
||||
type = cInfo.chatType,
|
||||
id = cInfo.apiId,
|
||||
|
@ -331,7 +331,7 @@ fun ComposeView(
|
|||
chatModel.addChatItem(cInfo, aChatItem.chatItem)
|
||||
return aChatItem.chatItem
|
||||
}
|
||||
if (file != null) removeFile(file)
|
||||
if (file != null) removeFile(file.filePath)
|
||||
return null
|
||||
}
|
||||
|
||||
|
@ -404,7 +404,7 @@ fun ComposeView(
|
|||
sent = updateMessage(liveMessage.chatItem, cInfo, live)
|
||||
} else {
|
||||
val msgs: ArrayList<MsgContent> = ArrayList()
|
||||
val files: ArrayList<String> = ArrayList()
|
||||
val files: ArrayList<CryptoFile> = ArrayList()
|
||||
when (val preview = cs.preview) {
|
||||
ComposePreview.NoPreview -> msgs.add(MsgContent.MCText(msgText))
|
||||
is ComposePreview.CLinkPreview -> msgs.add(checkLinkPreview())
|
||||
|
@ -413,7 +413,7 @@ fun ComposeView(
|
|||
val file = when (it) {
|
||||
is UploadContent.SimpleImage -> saveImage(it.uri)
|
||||
is UploadContent.AnimatedImage -> saveAnimImage(it.uri)
|
||||
is UploadContent.Video -> saveFileFromUri(it.uri)
|
||||
is UploadContent.Video -> saveFileFromUri(it.uri, encrypted = false)
|
||||
}
|
||||
if (file != null) {
|
||||
files.add(file)
|
||||
|
@ -432,12 +432,13 @@ fun ComposeView(
|
|||
withContext(Dispatchers.IO) {
|
||||
Files.move(tmpFile.toPath(), actualFile.toPath())
|
||||
}
|
||||
files.add(actualFile.name)
|
||||
// TODO encrypt voice files
|
||||
files.add(CryptoFile.plain(actualFile.name))
|
||||
deleteUnusedFiles()
|
||||
msgs.add(MsgContent.MCVoice(if (msgs.isEmpty()) msgText else "", preview.durationMs / 1000))
|
||||
}
|
||||
is ComposePreview.FilePreview -> {
|
||||
val file = saveFileFromUri(preview.uri)
|
||||
val file = saveFileFromUri(preview.uri, encrypted = false)
|
||||
if (file != null) {
|
||||
files.add((file))
|
||||
msgs.add(MsgContent.MCFile(if (msgs.isEmpty()) msgText else ""))
|
||||
|
|
|
@ -28,7 +28,7 @@ import java.net.URI
|
|||
fun CIFileView(
|
||||
file: CIFile?,
|
||||
edited: Boolean,
|
||||
receiveFile: (Long) -> Unit
|
||||
receiveFile: (Long, Boolean) -> Unit
|
||||
) {
|
||||
val saveFileLauncher = rememberSaveFileLauncher(ciFile = file)
|
||||
|
||||
|
@ -71,7 +71,7 @@ fun CIFileView(
|
|||
when (file.fileStatus) {
|
||||
is CIFileStatus.RcvInvitation -> {
|
||||
if (fileSizeValid()) {
|
||||
receiveFile(file.fileId)
|
||||
receiveFile(file.fileId, false)
|
||||
} else {
|
||||
AlertManager.shared.showAlertMsg(
|
||||
generalGetString(MR.strings.large_file),
|
||||
|
|
|
@ -31,7 +31,7 @@ fun CIImageView(
|
|||
file: CIFile?,
|
||||
imageProvider: () -> ImageGalleryProvider,
|
||||
showMenu: MutableState<Boolean>,
|
||||
receiveFile: (Long) -> Unit
|
||||
receiveFile: (Long, Boolean) -> Unit
|
||||
) {
|
||||
@Composable
|
||||
fun progressIndicator() {
|
||||
|
@ -152,7 +152,8 @@ fun CIImageView(
|
|||
when (file.fileStatus) {
|
||||
CIFileStatus.RcvInvitation ->
|
||||
if (fileSizeValid()) {
|
||||
receiveFile(file.fileId)
|
||||
// TODO encrypt image
|
||||
receiveFile(file.fileId, false)
|
||||
} else {
|
||||
AlertManager.shared.showAlertMsg(
|
||||
generalGetString(MR.strings.large_file),
|
||||
|
|
|
@ -31,7 +31,7 @@ fun CIVideoView(
|
|||
file: CIFile?,
|
||||
imageProvider: () -> ImageGalleryProvider,
|
||||
showMenu: MutableState<Boolean>,
|
||||
receiveFile: (Long) -> Unit
|
||||
receiveFile: (Long, Boolean) -> Unit
|
||||
) {
|
||||
Box(
|
||||
Modifier.layoutId(CHAT_IMAGE_LAYOUT_ID),
|
||||
|
@ -54,7 +54,7 @@ fun CIVideoView(
|
|||
if (file != null) {
|
||||
when (file.fileStatus) {
|
||||
CIFileStatus.RcvInvitation ->
|
||||
receiveFileIfValidSize(file, receiveFile)
|
||||
receiveFileIfValidSize(file, encrypted = false, receiveFile)
|
||||
CIFileStatus.RcvAccepted ->
|
||||
when (file.fileProtocol) {
|
||||
FileProtocol.XFTP ->
|
||||
|
@ -80,7 +80,7 @@ fun CIVideoView(
|
|||
DurationProgress(file, remember { mutableStateOf(false) }, remember { mutableStateOf(duration * 1000L) }, remember { mutableStateOf(0L) }/*, soundEnabled*/)
|
||||
}
|
||||
if (file?.fileStatus is CIFileStatus.RcvInvitation) {
|
||||
PlayButton(error = false, { showMenu.value = true }) { receiveFileIfValidSize(file, receiveFile) }
|
||||
PlayButton(error = false, { showMenu.value = true }) { receiveFileIfValidSize(file, encrypted = false, receiveFile) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -301,9 +301,9 @@ private fun fileSizeValid(file: CIFile?): Boolean {
|
|||
return false
|
||||
}
|
||||
|
||||
private fun receiveFileIfValidSize(file: CIFile, receiveFile: (Long) -> Unit) {
|
||||
private fun receiveFileIfValidSize(file: CIFile, encrypted: Boolean, receiveFile: (Long, Boolean) -> Unit) {
|
||||
if (fileSizeValid(file)) {
|
||||
receiveFile(file.fileId)
|
||||
receiveFile(file.fileId, encrypted)
|
||||
} else {
|
||||
AlertManager.shared.showAlertMsg(
|
||||
generalGetString(MR.strings.large_file),
|
||||
|
|
|
@ -37,18 +37,19 @@ fun CIVoiceView(
|
|||
ci: ChatItem,
|
||||
timedMessagesTTL: Int?,
|
||||
longClick: () -> Unit,
|
||||
receiveFile: (Long) -> Unit,
|
||||
receiveFile: (Long, Boolean) -> Unit,
|
||||
) {
|
||||
Row(
|
||||
Modifier.padding(top = if (hasText) 14.dp else 4.dp, bottom = if (hasText) 14.dp else 6.dp, start = if (hasText) 6.dp else 0.dp, end = if (hasText) 6.dp else 0.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
if (file != null) {
|
||||
val filePath = remember(file.filePath, file.fileStatus) { getLoadedFilePath(file) }
|
||||
var brokenAudio by rememberSaveable(file.filePath) { mutableStateOf(false) }
|
||||
val audioPlaying = rememberSaveable(file.filePath) { mutableStateOf(false) }
|
||||
val progress = rememberSaveable(file.filePath) { mutableStateOf(0) }
|
||||
val duration = rememberSaveable(file.filePath) { mutableStateOf(providedDurationSec * 1000) }
|
||||
val f = file.fileSource?.filePath
|
||||
val filePath = remember(f, file.fileStatus) { getLoadedFilePath(file) }
|
||||
var brokenAudio by rememberSaveable(f) { mutableStateOf(false) }
|
||||
val audioPlaying = rememberSaveable(f) { mutableStateOf(false) }
|
||||
val progress = rememberSaveable(f) { mutableStateOf(0) }
|
||||
val duration = rememberSaveable(f) { mutableStateOf(providedDurationSec * 1000) }
|
||||
val play = {
|
||||
AudioPlayer.play(filePath, audioPlaying, progress, duration, true)
|
||||
brokenAudio = !audioPlaying.value
|
||||
|
@ -94,7 +95,7 @@ private fun VoiceLayout(
|
|||
play: () -> Unit,
|
||||
pause: () -> Unit,
|
||||
longClick: () -> Unit,
|
||||
receiveFile: (Long) -> Unit,
|
||||
receiveFile: (Long, Boolean) -> Unit,
|
||||
onProgressChanged: (Int) -> Unit,
|
||||
) {
|
||||
@Composable
|
||||
|
@ -248,7 +249,7 @@ private fun VoiceMsgIndicator(
|
|||
play: () -> Unit,
|
||||
pause: () -> Unit,
|
||||
longClick: () -> Unit,
|
||||
receiveFile: (Long) -> Unit,
|
||||
receiveFile: (Long, Boolean) -> Unit,
|
||||
) {
|
||||
val strokeWidth = with(LocalDensity.current) { 3.dp.toPx() }
|
||||
val strokeColor = MaterialTheme.colors.primary
|
||||
|
@ -268,7 +269,8 @@ private fun VoiceMsgIndicator(
|
|||
}
|
||||
} else {
|
||||
if (file?.fileStatus is CIFileStatus.RcvInvitation) {
|
||||
PlayPauseButton(audioPlaying, sent, 0f, strokeWidth, strokeColor, true, error, { receiveFile(file.fileId) }, {}, longClick = longClick)
|
||||
// TODO encrypt voice
|
||||
PlayPauseButton(audioPlaying, sent, 0f, strokeWidth, strokeColor, true, error, { receiveFile(file.fileId, false) }, {}, longClick = longClick)
|
||||
} else if (file?.fileStatus is CIFileStatus.RcvTransfer
|
||||
|| file?.fileStatus is CIFileStatus.RcvAccepted
|
||||
) {
|
||||
|
|
|
@ -48,7 +48,7 @@ fun ChatItemView(
|
|||
useLinkPreviews: Boolean,
|
||||
linkMode: SimplexLinkMode,
|
||||
deleteMessage: (Long, CIDeleteMode) -> Unit,
|
||||
receiveFile: (Long) -> Unit,
|
||||
receiveFile: (Long, Boolean) -> Unit,
|
||||
cancelFile: (Long) -> Unit,
|
||||
joinGroup: (Long) -> Unit,
|
||||
acceptCall: (Contact) -> Unit,
|
||||
|
@ -566,7 +566,7 @@ fun PreviewChatItemView() {
|
|||
linkMode = SimplexLinkMode.DESCRIPTION,
|
||||
composeState = remember { mutableStateOf(ComposeState(useLinkPreviews = true)) },
|
||||
deleteMessage = { _, _ -> },
|
||||
receiveFile = {},
|
||||
receiveFile = { _, _ -> },
|
||||
cancelFile = {},
|
||||
joinGroup = {},
|
||||
acceptCall = { _ -> },
|
||||
|
@ -595,7 +595,7 @@ fun PreviewChatItemViewDeletedContent() {
|
|||
linkMode = SimplexLinkMode.DESCRIPTION,
|
||||
composeState = remember { mutableStateOf(ComposeState(useLinkPreviews = true)) },
|
||||
deleteMessage = { _, _ -> },
|
||||
receiveFile = {},
|
||||
receiveFile = { _, _ -> },
|
||||
cancelFile = {},
|
||||
joinGroup = {},
|
||||
acceptCall = { _ -> },
|
||||
|
|
|
@ -36,7 +36,7 @@ fun FramedItemView(
|
|||
imageProvider: (() -> ImageGalleryProvider)? = null,
|
||||
linkMode: SimplexLinkMode,
|
||||
showMenu: MutableState<Boolean>,
|
||||
receiveFile: (Long) -> Unit,
|
||||
receiveFile: (Long, Boolean) -> Unit,
|
||||
onLinkLongClick: (link: String) -> Unit = {},
|
||||
scrollToItem: (Long) -> Unit = {},
|
||||
) {
|
||||
|
|
|
@ -95,12 +95,13 @@ fun getThemeFromUri(uri: URI, withAlertOnException: Boolean = true): ThemeOverri
|
|||
return null
|
||||
}
|
||||
|
||||
fun saveImage(uri: URI): String? {
|
||||
fun saveImage(uri: URI): CryptoFile? {
|
||||
val bitmap = getBitmapFromUri(uri) ?: return null
|
||||
return saveImage(bitmap)
|
||||
}
|
||||
|
||||
fun saveImage(image: ImageBitmap): String? {
|
||||
fun saveImage(image: ImageBitmap): CryptoFile? {
|
||||
// TODO encrypt image
|
||||
return try {
|
||||
val ext = if (image.hasAlpha()) "png" else "jpg"
|
||||
val dataResized = resizeImageToDataSize(image, ext == "png", maxDataSize = MAX_IMAGE_SIZE)
|
||||
|
@ -110,14 +111,15 @@ fun saveImage(image: ImageBitmap): String? {
|
|||
dataResized.writeTo(output)
|
||||
output.flush()
|
||||
output.close()
|
||||
fileToSave
|
||||
CryptoFile.plain(fileToSave)
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Util.kt saveImage error: ${e.stackTraceToString()}")
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
fun saveAnimImage(uri: URI): String? {
|
||||
fun saveAnimImage(uri: URI): CryptoFile? {
|
||||
// TODO encrypt image
|
||||
return try {
|
||||
val filename = getFileName(uri)?.lowercase()
|
||||
var ext = when {
|
||||
|
@ -135,7 +137,7 @@ fun saveAnimImage(uri: URI): String? {
|
|||
input?.copyTo(output)
|
||||
}
|
||||
}
|
||||
fileToSave
|
||||
CryptoFile.plain(fileToSave)
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Util.kt saveAnimImage error: ${e.message}")
|
||||
null
|
||||
|
@ -144,15 +146,16 @@ fun saveAnimImage(uri: URI): String? {
|
|||
|
||||
expect suspend fun saveTempImageUncompressed(image: ImageBitmap, asPng: Boolean): File?
|
||||
|
||||
fun saveFileFromUri(uri: URI): String? {
|
||||
fun saveFileFromUri(uri: URI, encrypted: Boolean): CryptoFile? {
|
||||
return try {
|
||||
val inputStream = uri.inputStream()
|
||||
val fileToSave = getFileName(uri)
|
||||
// TODO encrypt file if "encrypted" is true
|
||||
if (inputStream != null && fileToSave != null) {
|
||||
val destFileName = uniqueCombine(fileToSave)
|
||||
val destFile = File(getAppFilePath(destFileName))
|
||||
Files.copy(inputStream, destFile.toPath())
|
||||
destFileName
|
||||
CryptoFile.plain(destFileName)
|
||||
} else {
|
||||
Log.e(TAG, "Util.kt saveFileFromUri null inputStream")
|
||||
null
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue