diff --git a/apps/ios/Shared/Model/SimpleXAPI.swift b/apps/ios/Shared/Model/SimpleXAPI.swift index d5ad40b850..9c23b01cff 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 3f04253e5f..08170f825f 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 e17968b7e5..111643e6ab 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 5d25a489a8..34c3ecb4a8 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 0fa754ec20..fa8be9f065 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 cb58d2feaf..7d5f0115da 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 5eda201f24..3740ba464b 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 ededd5b26b..defc069a7e 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 3ea392c229..a39419b438 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 d5c692f1f0..bcd033f67f 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 7df65f2442..09cc0b9965 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) }