From aeb732c2f6feb2c8336db851fbed5735f5832872 Mon Sep 17 00:00:00 2001 From: spaced4ndy <8711996+spaced4ndy@users.noreply.github.com> Date: Fri, 24 Mar 2023 15:20:15 +0400 Subject: [PATCH] ios: support XFTP files (#2064) --- apps/ios/Shared/Model/SimpleXAPI.swift | 14 ++++++++ .../Views/Chat/ChatItem/CIFileView.swift | 25 +++++++++++--- .../Views/Chat/ChatItem/CIVoiceView.swift | 2 +- .../Chat/ChatItem/FramedCIVoiceView.swift | 2 +- .../ExperimentalFeaturesView.swift | 14 ++++++-- .../Views/UserSettings/SettingsView.swift | 12 +++---- .../ios/SimpleX NSE/NotificationService.swift | 17 +++++++++- apps/ios/SimpleXChat/APITypes.swift | 14 ++++++++ apps/ios/SimpleXChat/AppGroup.swift | 11 +++++- apps/ios/SimpleXChat/ChatTypes.swift | 34 +++++++++++++------ apps/ios/SimpleXChat/FileUtils.swift | 7 +++- 11 files changed, 124 insertions(+), 28 deletions(-) diff --git a/apps/ios/Shared/Model/SimpleXAPI.swift b/apps/ios/Shared/Model/SimpleXAPI.swift index d5ad40b85..9c23b01cf 100644 --- a/apps/ios/Shared/Model/SimpleXAPI.swift +++ b/apps/ios/Shared/Model/SimpleXAPI.swift @@ -215,12 +215,24 @@ func apiSuspendChat(timeoutMicroseconds: Int) { logger.error("apiSuspendChat error: \(String(describing: r))") } +func apiSetTempFolder(tempFolder: String) throws { + let r = chatSendCmdSync(.setTempFolder(tempFolder: tempFolder)) + if case .cmdOk = r { return } + throw r +} + func apiSetFilesFolder(filesFolder: String) throws { let r = chatSendCmdSync(.setFilesFolder(filesFolder: filesFolder)) if case .cmdOk = r { return } throw r } +func setXFTPConfig(_ cfg: XFTPFileConfig?) throws { + let r = chatSendCmdSync(.apiSetXFTPConfig(config: cfg)) + if case .cmdOk = r { return } + throw r +} + func apiSetIncognito(incognito: Bool) throws { let r = chatSendCmdSync(.setIncognito(incognito: incognito)) if case .cmdOk = r { return } @@ -992,7 +1004,9 @@ func initializeChat(start: Bool, dbKey: String? = nil, refreshInvitations: Bool if encryptionStartedDefault.get() { encryptionStartedDefault.set(false) } + try apiSetTempFolder(tempFolder: getTempFilesDirectory().path) try apiSetFilesFolder(filesFolder: getAppFilesDirectory().path) + try setXFTPConfig(getXFTPCfg()) try apiSetIncognito(incognito: incognitoGroupDefault.get()) m.chatInitialized = true m.currentUser = try apiGetActiveUser() diff --git a/apps/ios/Shared/Views/Chat/ChatItem/CIFileView.swift b/apps/ios/Shared/Views/Chat/ChatItem/CIFileView.swift index 3f04253e5..08170f825 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() @@ -45,7 +45,24 @@ struct CIFileView: View { .padding(.leading, 10) .padding(.trailing, 12) } - .disabled(file == nil || (file?.fileStatus != .rcvInvitation && file?.fileStatus != .rcvAccepted && file?.fileStatus != .rcvComplete)) + .disabled(!itemInteractive) + } + + var itemInteractive: Bool { + if let file = file { + switch (file.fileStatus) { + case .sndStored: return false + case .sndTransfer: return false + case .sndComplete: return false + case .sndCancelled: return false + case .rcvInvitation: return true + case .rcvAccepted: return true + case .rcvTransfer: return false + case .rcvComplete: return true + case .rcvCancelled: return false + } + } + return false } func fileSizeValid() -> Bool { @@ -155,7 +172,7 @@ struct CIFileView_Previews: PreviewProvider { ChatItemView(chatInfo: ChatInfo.sampleData.direct, chatItem: ChatItem.getFileMsgContentSample(), revealed: Binding.constant(false)) ChatItemView(chatInfo: ChatInfo.sampleData.direct, chatItem: ChatItem.getFileMsgContentSample(fileName: "some_long_file_name_here", fileStatus: .rcvInvitation), revealed: Binding.constant(false)) ChatItemView(chatInfo: ChatInfo.sampleData.direct, chatItem: ChatItem.getFileMsgContentSample(fileStatus: .rcvAccepted), revealed: Binding.constant(false)) - ChatItemView(chatInfo: ChatInfo.sampleData.direct, chatItem: ChatItem.getFileMsgContentSample(fileStatus: .rcvTransfer), revealed: Binding.constant(false)) + ChatItemView(chatInfo: ChatInfo.sampleData.direct, chatItem: ChatItem.getFileMsgContentSample(fileStatus: .rcvTransfer(rcvProgress: 7, rcvTotal: 10)), revealed: Binding.constant(false)) ChatItemView(chatInfo: ChatInfo.sampleData.direct, chatItem: ChatItem.getFileMsgContentSample(fileStatus: .rcvCancelled), revealed: Binding.constant(false)) ChatItemView(chatInfo: ChatInfo.sampleData.direct, chatItem: ChatItem.getFileMsgContentSample(fileSize: 1_000_000_000, fileStatus: .rcvInvitation), revealed: Binding.constant(false)) ChatItemView(chatInfo: ChatInfo.sampleData.direct, chatItem: ChatItem.getFileMsgContentSample(text: "Hello there", fileStatus: .rcvInvitation), revealed: Binding.constant(false)) diff --git a/apps/ios/Shared/Views/Chat/ChatItem/CIVoiceView.swift b/apps/ios/Shared/Views/Chat/ChatItem/CIVoiceView.swift index e17968b7e..111643e6a 100644 --- a/apps/ios/Shared/Views/Chat/ChatItem/CIVoiceView.swift +++ b/apps/ios/Shared/Views/Chat/ChatItem/CIVoiceView.swift @@ -243,7 +243,7 @@ struct CIVoiceView_Previews: PreviewProvider { ) ChatItemView(chatInfo: ChatInfo.sampleData.direct, chatItem: sentVoiceMessage, revealed: Binding.constant(false)) ChatItemView(chatInfo: ChatInfo.sampleData.direct, chatItem: ChatItem.getVoiceMsgContentSample(), revealed: Binding.constant(false)) - ChatItemView(chatInfo: ChatInfo.sampleData.direct, chatItem: ChatItem.getVoiceMsgContentSample(fileStatus: .rcvTransfer), revealed: Binding.constant(false)) + ChatItemView(chatInfo: ChatInfo.sampleData.direct, chatItem: ChatItem.getVoiceMsgContentSample(fileStatus: .rcvTransfer(rcvProgress: 7, rcvTotal: 10)), revealed: Binding.constant(false)) ChatItemView(chatInfo: ChatInfo.sampleData.direct, chatItem: voiceMessageWtFile, revealed: Binding.constant(false)) } .previewLayout(.fixed(width: 360, height: 360)) diff --git a/apps/ios/Shared/Views/Chat/ChatItem/FramedCIVoiceView.swift b/apps/ios/Shared/Views/Chat/ChatItem/FramedCIVoiceView.swift index 5d25a489a..34c3ecb4a 100644 --- a/apps/ios/Shared/Views/Chat/ChatItem/FramedCIVoiceView.swift +++ b/apps/ios/Shared/Views/Chat/ChatItem/FramedCIVoiceView.swift @@ -62,7 +62,7 @@ struct FramedCIVoiceView_Previews: PreviewProvider { Group { ChatItemView(chatInfo: ChatInfo.sampleData.direct, chatItem: sentVoiceMessage, revealed: Binding.constant(false)) ChatItemView(chatInfo: ChatInfo.sampleData.direct, chatItem: ChatItem.getVoiceMsgContentSample(text: "Hello there"), revealed: Binding.constant(false)) - ChatItemView(chatInfo: ChatInfo.sampleData.direct, chatItem: ChatItem.getVoiceMsgContentSample(text: "Hello there", fileStatus: .rcvTransfer), revealed: Binding.constant(false)) + ChatItemView(chatInfo: ChatInfo.sampleData.direct, chatItem: ChatItem.getVoiceMsgContentSample(text: "Hello there", fileStatus: .rcvTransfer(rcvProgress: 7, rcvTotal: 10)), revealed: Binding.constant(false)) ChatItemView(chatInfo: ChatInfo.sampleData.direct, chatItem: ChatItem.getVoiceMsgContentSample(text: "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum."), revealed: Binding.constant(false)) ChatItemView(chatInfo: ChatInfo.sampleData.direct, chatItem: voiceMessageWithQuote, revealed: Binding.constant(false)) } diff --git a/apps/ios/Shared/Views/UserSettings/ExperimentalFeaturesView.swift b/apps/ios/Shared/Views/UserSettings/ExperimentalFeaturesView.swift index 0fa754ec2..fa8be9f06 100644 --- a/apps/ios/Shared/Views/UserSettings/ExperimentalFeaturesView.swift +++ b/apps/ios/Shared/Views/UserSettings/ExperimentalFeaturesView.swift @@ -7,15 +7,23 @@ // import SwiftUI +import SimpleXChat struct ExperimentalFeaturesView: View { - @AppStorage(DEFAULT_EXPERIMENTAL_CALLS) private var enableCalls = false + @AppStorage(GROUP_DEFAULT_XFTP_SEND_ENABLED, store: UserDefaults(suiteName: APP_GROUP_NAME)!) private var xftpSendEnabled = false var body: some View { List { Section("") { - settingsRow("video") { - Toggle("Audio & video calls", isOn: $enableCalls) + settingsRow("arrow.up.doc") { + Toggle("Send files via XFTP", isOn: $xftpSendEnabled) + .onChange(of: xftpSendEnabled) { _ in + do { + try setXFTPConfig(getXFTPCfg()) + } catch { + logger.error("setXFTPConfig: cannot set XFTP config \(responseError(error))") + } + } } } } diff --git a/apps/ios/Shared/Views/UserSettings/SettingsView.swift b/apps/ios/Shared/Views/UserSettings/SettingsView.swift index cb58d2fea..7d5f0115d 100644 --- a/apps/ios/Shared/Views/UserSettings/SettingsView.swift +++ b/apps/ios/Shared/Views/UserSettings/SettingsView.swift @@ -277,12 +277,12 @@ struct SettingsView: View { .padding(.leading, indent) } } -// NavigationLink { -// ExperimentalFeaturesView() -// .navigationTitle("Experimental features") -// } label: { -// settingsRow("gauge") { Text("Experimental features") } -// } + NavigationLink { + ExperimentalFeaturesView() + .navigationTitle("Experimental features") + } label: { + settingsRow("gauge") { Text("Experimental features") } + } NavigationLink { VersionView() .navigationBarTitle("App version") diff --git a/apps/ios/SimpleX NSE/NotificationService.swift b/apps/ios/SimpleX NSE/NotificationService.swift index 5eda201f2..3740ba464 100644 --- a/apps/ios/SimpleX NSE/NotificationService.swift +++ b/apps/ios/SimpleX NSE/NotificationService.swift @@ -199,6 +199,7 @@ class NotificationService: UNNotificationServiceExtension { var chatStarted = false var networkConfig: NetCfg = getNetCfg() +var xftpConfig: XFTPFileConfig? = getXFTPCfg() func startChat() -> DBMigrationResult? { hs_init(0, nil) @@ -212,10 +213,12 @@ func startChat() -> DBMigrationResult? { logger.debug("active user \(String(describing: user))") do { try setNetworkConfig(networkConfig) + try apiSetTempFolder(tempFolder: getTempFilesDirectory().path) + try apiSetFilesFolder(filesFolder: getAppFilesDirectory().path) + try setXFTPConfig(xftpConfig) let justStarted = try apiStartChat() chatStarted = true if justStarted { - try apiSetFilesFolder(filesFolder: getAppFilesDirectory().path) try apiSetIncognito(incognito: incognitoGroupDefault.get()) chatLastStartGroupDefault.set(Date.now) Task { await receiveMessages() } @@ -329,12 +332,24 @@ func apiStartChat() throws -> Bool { } } +func apiSetTempFolder(tempFolder: String) throws { + let r = sendSimpleXCmd(.setTempFolder(tempFolder: tempFolder)) + if case .cmdOk = r { return } + throw r +} + func apiSetFilesFolder(filesFolder: String) throws { let r = sendSimpleXCmd(.setFilesFolder(filesFolder: filesFolder)) if case .cmdOk = r { return } throw r } +func setXFTPConfig(_ cfg: XFTPFileConfig?) throws { + let r = sendSimpleXCmd(.apiSetXFTPConfig(config: cfg)) + if case .cmdOk = r { return } + throw r +} + func apiSetIncognito(incognito: Bool) throws { let r = sendSimpleXCmd(.setIncognito(incognito: incognito)) if case .cmdOk = r { return } diff --git a/apps/ios/SimpleXChat/APITypes.swift b/apps/ios/SimpleXChat/APITypes.swift index ededd5b26..defc069a7 100644 --- a/apps/ios/SimpleXChat/APITypes.swift +++ b/apps/ios/SimpleXChat/APITypes.swift @@ -26,7 +26,9 @@ public enum ChatCommand { case apiStopChat case apiActivateChat case apiSuspendChat(timeoutMicroseconds: Int) + case setTempFolder(tempFolder: String) case setFilesFolder(filesFolder: String) + case apiSetXFTPConfig(config: XFTPFileConfig?) case setIncognito(incognito: Bool) case apiExportArchive(config: ArchiveConfig) case apiImportArchive(config: ArchiveConfig) @@ -117,7 +119,13 @@ public enum ChatCommand { case .apiStopChat: return "/_stop" case .apiActivateChat: return "/_app activate" case let .apiSuspendChat(timeoutMicroseconds): return "/_app suspend \(timeoutMicroseconds)" + case let .setTempFolder(tempFolder): return "/_temp_folder \(tempFolder)" case let .setFilesFolder(filesFolder): return "/_files_folder \(filesFolder)" + case let .apiSetXFTPConfig(cfg): if let cfg = cfg { + return "/_xftp on \(encodeJSON(cfg))" + } else { + return "/_xftp off" + } case let .setIncognito(incognito): return "/incognito \(onOff(incognito))" case let .apiExportArchive(cfg): return "/_db export \(encodeJSON(cfg))" case let .apiImportArchive(cfg): return "/_db import \(encodeJSON(cfg))" @@ -219,7 +227,9 @@ public enum ChatCommand { case .apiStopChat: return "apiStopChat" case .apiActivateChat: return "apiActivateChat" case .apiSuspendChat: return "apiSuspendChat" + case .setTempFolder: return "setTempFolder" case .setFilesFolder: return "setFilesFolder" + case .apiSetXFTPConfig: return "apiSetXFTPConfig" case .setIncognito: return "setIncognito" case .apiExportArchive: return "apiExportArchive" case .apiImportArchive: return "apiImportArchive" @@ -712,6 +722,10 @@ struct ComposedMessage: Encodable { var msgContent: MsgContent } +public struct XFTPFileConfig: Encodable { + var minFileSize: Int64 +} + public struct ArchiveConfig: Encodable { var archivePath: String var disableCompression: Bool? diff --git a/apps/ios/SimpleXChat/AppGroup.swift b/apps/ios/SimpleXChat/AppGroup.swift index 3ea392c22..a39419b43 100644 --- a/apps/ios/SimpleXChat/AppGroup.swift +++ b/apps/ios/SimpleXChat/AppGroup.swift @@ -30,6 +30,7 @@ let GROUP_DEFAULT_INCOGNITO = "incognito" let GROUP_DEFAULT_STORE_DB_PASSPHRASE = "storeDBPassphrase" let GROUP_DEFAULT_INITIAL_RANDOM_DB_PASSPHRASE = "initialRandomDBPassphrase" public let GROUP_DEFAULT_CALL_KIT_ENABLED = "callKitEnabled" +public let GROUP_DEFAULT_XFTP_SEND_ENABLED = "xftpSendEnabled" public let APP_GROUP_NAME = "group.chat.simplex.app" @@ -52,7 +53,8 @@ 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_CALL_KIT_ENABLED: true + GROUP_DEFAULT_CALL_KIT_ENABLED: true, + GROUP_DEFAULT_XFTP_SEND_ENABLED: false ]) } @@ -123,6 +125,8 @@ public let initialRandomDBPassphraseGroupDefault = BoolDefault(defaults: groupDe public let callKitEnabledGroupDefault = BoolDefault(defaults: groupDefaults, forKey: GROUP_DEFAULT_CALL_KIT_ENABLED) +public let xftpSendEnabledGroupDefault = BoolDefault(defaults: groupDefaults, forKey: GROUP_DEFAULT_XFTP_SEND_ENABLED) + public class DateDefault { var defaults: UserDefaults var key: String @@ -195,6 +199,11 @@ public class Default { } } +public func getXFTPCfg() -> XFTPFileConfig? { + let xftpSendEnabled = xftpSendEnabledGroupDefault.get() + return xftpSendEnabled ? XFTPFileConfig(minFileSize: 0) : nil +} + public func getNetCfg() -> NetCfg { let onionHosts = networkUseOnionHostsGroupDefault.get() let (hostMode, requiredHostMode) = onionHosts.hostMode diff --git a/apps/ios/SimpleXChat/ChatTypes.swift b/apps/ios/SimpleXChat/ChatTypes.swift index d5c692f1f..bcd033f67 100644 --- a/apps/ios/SimpleXChat/ChatTypes.swift +++ b/apps/ios/SimpleXChat/ChatTypes.swift @@ -2237,16 +2237,30 @@ public struct CIFile: Decodable { } } -public enum CIFileStatus: String, Decodable { - case sndStored = "snd_stored" - case sndTransfer = "snd_transfer" - case sndComplete = "snd_complete" - case sndCancelled = "snd_cancelled" - case rcvInvitation = "rcv_invitation" - case rcvAccepted = "rcv_accepted" - case rcvTransfer = "rcv_transfer" - case rcvComplete = "rcv_complete" - case rcvCancelled = "rcv_cancelled" +public enum CIFileStatus: Decodable { + case sndStored + case sndTransfer(sndProgress: Int, sndTotal: Int) + case sndComplete + case sndCancelled + case rcvInvitation + case rcvAccepted + case rcvTransfer(rcvProgress: Int, rcvTotal: Int) + case rcvComplete + case rcvCancelled + + var id: String { + switch self { + case .sndStored: return "sndStored" + case let .sndTransfer(sndProgress, sndTotal): return "sndTransfer \(sndProgress) \(sndTotal)" + case .sndComplete: return "sndComplete" + case .sndCancelled: return "sndCancelled" + case .rcvInvitation: return "rcvInvitation" + case .rcvAccepted: return "rcvAccepted" + case let .rcvTransfer(rcvProgress, rcvTotal): return "rcvTransfer \(rcvProgress) \(rcvTotal)" + case .rcvComplete: return "rcvComplete" + case .rcvCancelled: return "rcvCancelled" + } + } } public enum MsgContent { diff --git a/apps/ios/SimpleXChat/FileUtils.swift b/apps/ios/SimpleXChat/FileUtils.swift index 7df65f244..09cc0b996 100644 --- a/apps/ios/SimpleXChat/FileUtils.swift +++ b/apps/ios/SimpleXChat/FileUtils.swift @@ -16,7 +16,8 @@ public let MAX_IMAGE_SIZE: Int64 = 236700 public let MAX_IMAGE_SIZE_AUTO_RCV: Int64 = MAX_IMAGE_SIZE * 2 -public let MAX_FILE_SIZE: Int64 = 8000000 +//public let MAX_FILE_SIZE_SMP: Int64 = 8000000 // TODO distinguish between XFTP and SMP files +public let MAX_FILE_SIZE: Int64 = 1_073_741_824 public let MAX_VOICE_MESSAGE_LENGTH = TimeInterval(30) @@ -158,6 +159,10 @@ public func removeLegacyDatabaseAndFiles() -> Bool { return r1 && r2 } +public func getTempFilesDirectory() -> URL { + getAppDirectory().appendingPathComponent("temp_files", isDirectory: true) +} + public func getAppFilesDirectory() -> URL { getAppDirectory().appendingPathComponent("app_files", isDirectory: true) }