From ccb52e0acd32e7b339b1230dd12fcaab0ebcde04 Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com> Date: Thu, 6 Apr 2023 23:25:40 +0100 Subject: [PATCH 1/4] ios: validate server protocol --- .../UserSettings/ProtocolServersView.swift | 2 +- apps/ios/SimpleX.xcodeproj/project.pbxproj | 40 +++++++++---------- apps/ios/SimpleXChat/APITypes.swift | 6 +-- 3 files changed, 24 insertions(+), 24 deletions(-) diff --git a/apps/ios/Shared/Views/UserSettings/ProtocolServersView.swift b/apps/ios/Shared/Views/UserSettings/ProtocolServersView.swift index 6b4e4bc927..2d8eba9014 100644 --- a/apps/ios/Shared/Views/UserSettings/ProtocolServersView.swift +++ b/apps/ios/Shared/Views/UserSettings/ProtocolServersView.swift @@ -177,7 +177,7 @@ struct ProtocolServersView: View { HStack { Group { if let address = address { - if !address.valid { + if !address.valid || address.serverProtocol != serverProtocol { invalidServer() } else if !uniqueAddress(srv, address) { Image(systemName: "exclamationmark.circle").foregroundColor(.red) diff --git a/apps/ios/SimpleX.xcodeproj/project.pbxproj b/apps/ios/SimpleX.xcodeproj/project.pbxproj index 537994d5de..da61b2b641 100644 --- a/apps/ios/SimpleX.xcodeproj/project.pbxproj +++ b/apps/ios/SimpleX.xcodeproj/project.pbxproj @@ -99,11 +99,11 @@ 5CB346E52868AA7F001FD2EF /* SuspendChat.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CB346E42868AA7F001FD2EF /* SuspendChat.swift */; }; 5CB346E72868D76D001FD2EF /* NotificationsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CB346E62868D76D001FD2EF /* NotificationsView.swift */; }; 5CB346E92869E8BA001FD2EF /* PushEnvironment.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CB346E82869E8BA001FD2EF /* PushEnvironment.swift */; }; - 5CB6348E29DE261C0066AD6B /* libgmpxx.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5CB6348929DE261C0066AD6B /* libgmpxx.a */; }; - 5CB6348F29DE261C0066AD6B /* libffi.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5CB6348A29DE261C0066AD6B /* libffi.a */; }; - 5CB6349029DE261C0066AD6B /* libHSsimplex-chat-4.6.1.1-44qOXuEu6Xh4AokU7t538i.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5CB6348B29DE261C0066AD6B /* libHSsimplex-chat-4.6.1.1-44qOXuEu6Xh4AokU7t538i.a */; }; - 5CB6349129DE261C0066AD6B /* libgmp.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5CB6348C29DE261C0066AD6B /* libgmp.a */; }; - 5CB6349229DE261C0066AD6B /* libHSsimplex-chat-4.6.1.1-44qOXuEu6Xh4AokU7t538i-ghc8.10.7.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5CB6348D29DE261C0066AD6B /* libHSsimplex-chat-4.6.1.1-44qOXuEu6Xh4AokU7t538i-ghc8.10.7.a */; }; + 5CB6349829DF7CF00066AD6B /* libgmpxx.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5CB6349329DF7CF00066AD6B /* libgmpxx.a */; }; + 5CB6349929DF7CF00066AD6B /* libffi.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5CB6349429DF7CF00066AD6B /* libffi.a */; }; + 5CB6349A29DF7CF00066AD6B /* libgmp.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5CB6349529DF7CF00066AD6B /* libgmp.a */; }; + 5CB6349B29DF7CF00066AD6B /* libHSsimplex-chat-4.6.1.1-KdibHfABD5wCx2UMHpStjD-ghc8.10.7.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5CB6349629DF7CF00066AD6B /* libHSsimplex-chat-4.6.1.1-KdibHfABD5wCx2UMHpStjD-ghc8.10.7.a */; }; + 5CB6349C29DF7CF00066AD6B /* libHSsimplex-chat-4.6.1.1-KdibHfABD5wCx2UMHpStjD.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5CB6349729DF7CF00066AD6B /* libHSsimplex-chat-4.6.1.1-KdibHfABD5wCx2UMHpStjD.a */; }; 5CB924D727A8563F00ACCCDD /* SettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CB924D627A8563F00ACCCDD /* SettingsView.swift */; }; 5CB924E127A867BA00ACCCDD /* UserProfile.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CB924E027A867BA00ACCCDD /* UserProfile.swift */; }; 5CB924E427A8683A00ACCCDD /* UserAddress.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CB924E327A8683A00ACCCDD /* UserAddress.swift */; }; @@ -355,11 +355,11 @@ 5CB346E42868AA7F001FD2EF /* SuspendChat.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SuspendChat.swift; sourceTree = ""; }; 5CB346E62868D76D001FD2EF /* NotificationsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationsView.swift; sourceTree = ""; }; 5CB346E82869E8BA001FD2EF /* PushEnvironment.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PushEnvironment.swift; sourceTree = ""; }; - 5CB6348929DE261C0066AD6B /* libgmpxx.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libgmpxx.a; sourceTree = ""; }; - 5CB6348A29DE261C0066AD6B /* libffi.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libffi.a; sourceTree = ""; }; - 5CB6348B29DE261C0066AD6B /* libHSsimplex-chat-4.6.1.1-44qOXuEu6Xh4AokU7t538i.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-4.6.1.1-44qOXuEu6Xh4AokU7t538i.a"; sourceTree = ""; }; - 5CB6348C29DE261C0066AD6B /* libgmp.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libgmp.a; sourceTree = ""; }; - 5CB6348D29DE261C0066AD6B /* libHSsimplex-chat-4.6.1.1-44qOXuEu6Xh4AokU7t538i-ghc8.10.7.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-4.6.1.1-44qOXuEu6Xh4AokU7t538i-ghc8.10.7.a"; sourceTree = ""; }; + 5CB6349329DF7CF00066AD6B /* libgmpxx.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libgmpxx.a; sourceTree = ""; }; + 5CB6349429DF7CF00066AD6B /* libffi.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libffi.a; sourceTree = ""; }; + 5CB6349529DF7CF00066AD6B /* libgmp.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libgmp.a; sourceTree = ""; }; + 5CB6349629DF7CF00066AD6B /* libHSsimplex-chat-4.6.1.1-KdibHfABD5wCx2UMHpStjD-ghc8.10.7.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-4.6.1.1-KdibHfABD5wCx2UMHpStjD-ghc8.10.7.a"; sourceTree = ""; }; + 5CB6349729DF7CF00066AD6B /* libHSsimplex-chat-4.6.1.1-KdibHfABD5wCx2UMHpStjD.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-4.6.1.1-KdibHfABD5wCx2UMHpStjD.a"; sourceTree = ""; }; 5CB924D627A8563F00ACCCDD /* SettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsView.swift; sourceTree = ""; }; 5CB924E027A867BA00ACCCDD /* UserProfile.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserProfile.swift; sourceTree = ""; }; 5CB924E327A8683A00ACCCDD /* UserAddress.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserAddress.swift; sourceTree = ""; }; @@ -471,13 +471,13 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( - 5CB6348E29DE261C0066AD6B /* libgmpxx.a in Frameworks */, + 5CB6349C29DF7CF00066AD6B /* libHSsimplex-chat-4.6.1.1-KdibHfABD5wCx2UMHpStjD.a in Frameworks */, + 5CB6349B29DF7CF00066AD6B /* libHSsimplex-chat-4.6.1.1-KdibHfABD5wCx2UMHpStjD-ghc8.10.7.a in Frameworks */, + 5CB6349829DF7CF00066AD6B /* libgmpxx.a in Frameworks */, + 5CB6349A29DF7CF00066AD6B /* libgmp.a in Frameworks */, 5CE2BA93284534B000EC33A6 /* libiconv.tbd in Frameworks */, - 5CB6349029DE261C0066AD6B /* libHSsimplex-chat-4.6.1.1-44qOXuEu6Xh4AokU7t538i.a in Frameworks */, 5CE2BA94284534BB00EC33A6 /* libz.tbd in Frameworks */, - 5CB6349129DE261C0066AD6B /* libgmp.a in Frameworks */, - 5CB6349229DE261C0066AD6B /* libHSsimplex-chat-4.6.1.1-44qOXuEu6Xh4AokU7t538i-ghc8.10.7.a in Frameworks */, - 5CB6348F29DE261C0066AD6B /* libffi.a in Frameworks */, + 5CB6349929DF7CF00066AD6B /* libffi.a in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -536,11 +536,11 @@ 5C764E5C279C70B7000C6508 /* Libraries */ = { isa = PBXGroup; children = ( - 5CB6348A29DE261C0066AD6B /* libffi.a */, - 5CB6348C29DE261C0066AD6B /* libgmp.a */, - 5CB6348929DE261C0066AD6B /* libgmpxx.a */, - 5CB6348D29DE261C0066AD6B /* libHSsimplex-chat-4.6.1.1-44qOXuEu6Xh4AokU7t538i-ghc8.10.7.a */, - 5CB6348B29DE261C0066AD6B /* libHSsimplex-chat-4.6.1.1-44qOXuEu6Xh4AokU7t538i.a */, + 5CB6349429DF7CF00066AD6B /* libffi.a */, + 5CB6349529DF7CF00066AD6B /* libgmp.a */, + 5CB6349329DF7CF00066AD6B /* libgmpxx.a */, + 5CB6349629DF7CF00066AD6B /* libHSsimplex-chat-4.6.1.1-KdibHfABD5wCx2UMHpStjD-ghc8.10.7.a */, + 5CB6349729DF7CF00066AD6B /* libHSsimplex-chat-4.6.1.1-KdibHfABD5wCx2UMHpStjD.a */, ); path = Libraries; sourceTree = ""; diff --git a/apps/ios/SimpleXChat/APITypes.swift b/apps/ios/SimpleXChat/APITypes.swift index 8193f07cf8..2bac9953e0 100644 --- a/apps/ios/SimpleXChat/APITypes.swift +++ b/apps/ios/SimpleXChat/APITypes.swift @@ -891,13 +891,13 @@ public struct ProtocolTestFailure: Decodable, Error, Equatable { } public struct ServerAddress: Decodable { - public var serverProtocol: ServerProtocol? + public var serverProtocol: ServerProtocol public var hostnames: [String] public var port: String public var keyHash: String public var basicAuth: String - public init(serverProtocol: ServerProtocol?, hostnames: [String], port: String, keyHash: String, basicAuth: String = "") { + public init(serverProtocol: ServerProtocol, hostnames: [String], port: String, keyHash: String, basicAuth: String = "") { self.serverProtocol = serverProtocol self.hostnames = hostnames self.port = port @@ -906,7 +906,7 @@ public struct ServerAddress: Decodable { } public var uri: String { - "\(serverProtocol?.rawValue ?? "smp")://\(keyHash)\(basicAuth == "" ? "" : ":" + basicAuth)@\(hostnames.joined(separator: ","))" + "\(serverProtocol)://\(keyHash)\(basicAuth == "" ? "" : ":" + basicAuth)@\(hostnames.joined(separator: ","))" } public var valid: Bool { From 717c90c58bf3da35c6905268f0aea990ea1fcb32 Mon Sep 17 00:00:00 2001 From: spaced4ndy <8711996+spaced4ndy@users.noreply.github.com> Date: Fri, 7 Apr 2023 17:41:07 +0400 Subject: [PATCH 2/4] mobile: remove cancelled files (#2154) --- .../java/chat/simplex/app/model/SimpleXAPI.kt | 48 +++++++++++-------- apps/ios/Shared/Model/SimpleXAPI.swift | 18 ++----- .../ios/SimpleX NSE/NotificationService.swift | 12 +++++ apps/ios/SimpleXChat/FileUtils.swift | 15 ++++++ 4 files changed, 59 insertions(+), 34 deletions(-) diff --git a/apps/android/app/src/main/java/chat/simplex/app/model/SimpleXAPI.kt b/apps/android/app/src/main/java/chat/simplex/app/model/SimpleXAPI.kt index 90ff215dd3..38927e4ea3 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/model/SimpleXAPI.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/model/SimpleXAPI.kt @@ -1002,6 +1002,7 @@ open class ChatController(var ctrl: ChatCtrl?, val ntfManager: NtfManager, val a val chatItem = apiCancelFile(fileId) if (chatItem != null) { chatItemSimpleUpdate(user, chatItem) + cleanupFile(chatItem) } } @@ -1417,40 +1418,27 @@ open class ChatController(var ctrl: ChatCtrl?, val ntfManager: NtfManager, val a chatItemSimpleUpdate(r.user, r.chatItem) is CR.RcvFileComplete -> chatItemSimpleUpdate(r.user, r.chatItem) - is CR.RcvFileSndCancelled -> + is CR.RcvFileSndCancelled -> { chatItemSimpleUpdate(r.user, r.chatItem) + cleanupFile(r.chatItem) + } is CR.RcvFileProgressXFTP -> chatItemSimpleUpdate(r.user, r.chatItem) is CR.SndFileStart -> chatItemSimpleUpdate(r.user, r.chatItem) is CR.SndFileComplete -> { chatItemSimpleUpdate(r.user, r.chatItem) - val cItem = r.chatItem.chatItem - val mc = cItem.content.msgContent - val fileName = cItem.file?.fileName - if ( - r.chatItem.chatInfo.chatType == ChatType.Direct - && mc is MsgContent.MCFile - && fileName != null - ) { - removeFile(appContext, fileName) - } + cleanupDirectFile(r.chatItem) } - is CR.SndFileRcvCancelled -> + is CR.SndFileRcvCancelled -> { chatItemSimpleUpdate(r.user, r.chatItem) + cleanupDirectFile(r.chatItem) + } is CR.SndFileProgressXFTP -> chatItemSimpleUpdate(r.user, r.chatItem) is CR.SndFileCompleteXFTP -> { chatItemSimpleUpdate(r.user, r.chatItem) - val cItem = r.chatItem.chatItem - val mc = cItem.content.msgContent - val fileName = cItem.file?.fileName - if ( - mc is MsgContent.MCFile - && fileName != null - ) { - removeFile(appContext, fileName) - } + cleanupFile(r.chatItem) } is CR.CallInvitation -> { chatModel.callManager.reportNewIncomingCall(r.callInvitation) @@ -1502,6 +1490,24 @@ open class ChatController(var ctrl: ChatCtrl?, val ntfManager: NtfManager, val a } } + private fun cleanupDirectFile(aChatItem: AChatItem) { + if (aChatItem.chatInfo.chatType == ChatType.Direct) { + cleanupFile(aChatItem) + } + } + + private fun cleanupFile(aChatItem: AChatItem) { + val cItem = aChatItem.chatItem + val mc = cItem.content.msgContent + val fileName = cItem.file?.fileName + if ( + mc is MsgContent.MCFile + && fileName != null + ) { + removeFile(appContext, fileName) + } + } + private fun active(user: User): Boolean = user.userId == chatModel.currentUser.value?.userId private fun withCall(r: CR, contact: Contact, perform: (Call) -> Unit) { diff --git a/apps/ios/Shared/Model/SimpleXAPI.swift b/apps/ios/Shared/Model/SimpleXAPI.swift index ffaed725b6..f4e8fe8c02 100644 --- a/apps/ios/Shared/Model/SimpleXAPI.swift +++ b/apps/ios/Shared/Model/SimpleXAPI.swift @@ -745,6 +745,7 @@ func apiReceiveFile(fileId: Int64, inline: Bool? = nil) async -> AChatItem? { func cancelFile(user: User, fileId: Int64) async { if let chatItem = await apiCancelFile(fileId: fileId) { DispatchQueue.main.async { chatItemSimpleUpdate(user, chatItem) } + cleanupFile(chatItem) } } @@ -1329,31 +1330,22 @@ func processReceivedMsg(_ res: ChatResponse) async { chatItemSimpleUpdate(user, aChatItem) case let .rcvFileSndCancelled(user, aChatItem, _): chatItemSimpleUpdate(user, aChatItem) + cleanupFile(aChatItem) case let .rcvFileProgressXFTP(user, aChatItem, _, _): chatItemSimpleUpdate(user, aChatItem) case let .sndFileStart(user, aChatItem, _): chatItemSimpleUpdate(user, aChatItem) case let .sndFileComplete(user, aChatItem, _): chatItemSimpleUpdate(user, aChatItem) - let cItem = aChatItem.chatItem - let mc = cItem.content.msgContent - if aChatItem.chatInfo.chatType == .direct, - case .file = mc, - let fileName = cItem.file?.filePath { - removeFile(fileName) - } + cleanupDirectFile(aChatItem) case let .sndFileRcvCancelled(user, aChatItem, _): chatItemSimpleUpdate(user, aChatItem) + cleanupDirectFile(aChatItem) case let .sndFileProgressXFTP(user, aChatItem, _, _, _): chatItemSimpleUpdate(user, aChatItem) case let .sndFileCompleteXFTP(user, aChatItem, _): chatItemSimpleUpdate(user, aChatItem) - let cItem = aChatItem.chatItem - let mc = cItem.content.msgContent - if case .file = mc, - let fileName = cItem.file?.filePath { - removeFile(fileName) - } + cleanupFile(aChatItem) case let .callInvitation(invitation): m.callInvitations[invitation.contact.id] = invitation activateCall(invitation) diff --git a/apps/ios/SimpleX NSE/NotificationService.swift b/apps/ios/SimpleX NSE/NotificationService.swift index 52a44ef25d..10c9a5945d 100644 --- a/apps/ios/SimpleX NSE/NotificationService.swift +++ b/apps/ios/SimpleX NSE/NotificationService.swift @@ -292,6 +292,18 @@ func receivedMsgNtf(_ res: ChatResponse) async -> (String, NSENotification)? { } } return cItem.showMutableNotification ? (aChatItem.chatId, .nse(notification: createMessageReceivedNtf(user, cInfo, cItem))) : nil + case let .rcvFileSndCancelled(_, aChatItem, _): + cleanupFile(aChatItem) + return nil + case let .sndFileComplete(_, aChatItem, _): + cleanupDirectFile(aChatItem) + return nil + case let .sndFileRcvCancelled(_, aChatItem, _): + cleanupDirectFile(aChatItem) + return nil + case let .sndFileCompleteXFTP(_, aChatItem, _): + cleanupFile(aChatItem) + return nil case let .callInvitation(invitation): // Do not post it without CallKit support, iOS will stop launching the app without showing CallKit return ( diff --git a/apps/ios/SimpleXChat/FileUtils.swift b/apps/ios/SimpleXChat/FileUtils.swift index dc5e628428..dd6cdbf178 100644 --- a/apps/ios/SimpleXChat/FileUtils.swift +++ b/apps/ios/SimpleXChat/FileUtils.swift @@ -200,6 +200,21 @@ public func removeFile(_ fileName: String) { } } +public func cleanupDirectFile(_ aChatItem: AChatItem) { + if aChatItem.chatInfo.chatType == .direct { + cleanupFile(aChatItem) + } +} + +public func cleanupFile(_ aChatItem: AChatItem) { + let cItem = aChatItem.chatItem + let mc = cItem.content.msgContent + if case .file = mc, + let fileName = cItem.file?.filePath { + removeFile(fileName) + } +} + public func getMaxFileSize(_ fileProtocol: FileProtocol) -> Int64 { switch fileProtocol { case .xftp: return MAX_FILE_SIZE_XFTP From 85537a99e81a0a68a9256d2e123b7792e47ee3f8 Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com> Date: Fri, 7 Apr 2023 18:18:21 +0100 Subject: [PATCH 3/4] core: 4.6.1.2 --- package.yaml | 2 +- simplex-chat.cabal | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/package.yaml b/package.yaml index bcf6150b68..13016253e7 100644 --- a/package.yaml +++ b/package.yaml @@ -1,5 +1,5 @@ name: simplex-chat -version: 4.6.1.1 +version: 4.6.1.2 #synopsis: #description: homepage: https://github.com/simplex-chat/simplex-chat#readme diff --git a/simplex-chat.cabal b/simplex-chat.cabal index a974126c6e..a9ddadfe5c 100644 --- a/simplex-chat.cabal +++ b/simplex-chat.cabal @@ -5,7 +5,7 @@ cabal-version: 1.12 -- see: https://github.com/sol/hpack name: simplex-chat -version: 4.6.1.1 +version: 4.6.1.2 category: Web, System, Services, Cryptography homepage: https://github.com/simplex-chat/simplex-chat#readme author: simplex.chat From 991332a809194cff93cf17e42815ffdbc1e77509 Mon Sep 17 00:00:00 2001 From: Stanislav Dmitrenko <7953703+avently@users.noreply.github.com> Date: Fri, 7 Apr 2023 20:19:44 +0300 Subject: [PATCH 4/4] android: servers UI/API (#2155) * android: servers UI/API * non-optional server protocol in parsed address * make another enum for ServerProtocol --------- Co-authored-by: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com> --- .../java/chat/simplex/app/model/ChatModel.kt | 2 - .../java/chat/simplex/app/model/SimpleXAPI.kt | 171 ++++++++++++------ .../views/usersettings/NetworkAndServers.kt | 11 +- ...SMPServerView.kt => ProtocolServerView.kt} | 23 ++- ...PServersView.kt => ProtocolServersView.kt} | 70 ++++--- ...ScanSMPServer.kt => ScanProtocolServer.kt} | 7 +- .../app/src/main/res/values/strings.xml | 16 +- apps/ios/SimpleXChat/APITypes.swift | 2 +- 8 files changed, 204 insertions(+), 98 deletions(-) rename apps/android/app/src/main/java/chat/simplex/app/views/usersettings/{SMPServerView.kt => ProtocolServerView.kt} (89%) rename apps/android/app/src/main/java/chat/simplex/app/views/usersettings/{SMPServersView.kt => ProtocolServersView.kt} (79%) rename apps/android/app/src/main/java/chat/simplex/app/views/usersettings/{ScanSMPServer.kt => ScanProtocolServer.kt} (89%) diff --git a/apps/android/app/src/main/java/chat/simplex/app/model/ChatModel.kt b/apps/android/app/src/main/java/chat/simplex/app/model/ChatModel.kt index 7646abe8a2..a25d56a337 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/model/ChatModel.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/model/ChatModel.kt @@ -54,10 +54,8 @@ class ChatModel(val controller: ChatController) { val terminalItems = mutableStateListOf() val userAddress = mutableStateOf(null) - val userSMPServers = mutableStateOf<(List)?>(null) // Allows to temporary save servers that are being edited on multiple screens val userSMPServersUnsaved = mutableStateOf<(List)?>(null) - val presetSMPServers = mutableStateOf<(List)?>(null) val chatItemTTL = mutableStateOf(ChatItemTTL.None) // set when app opened from external intent diff --git a/apps/android/app/src/main/java/chat/simplex/app/model/SimpleXAPI.kt b/apps/android/app/src/main/java/chat/simplex/app/model/SimpleXAPI.kt index 38927e4ea3..c14eff4481 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/model/SimpleXAPI.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/model/SimpleXAPI.kt @@ -337,9 +337,6 @@ open class ChatController(var ctrl: ChatCtrl?, val ntfManager: NtfManager, val a suspend fun getUserChatData() { chatModel.userAddress.value = apiGetUserAddress() - val smpServers = getUserSMPServers() - chatModel.userSMPServers.value = smpServers?.first - chatModel.presetSMPServers.value = smpServers?.second chatModel.chatItemTTL.value = getChatItemTTL() val chats = apiGetChats() chatModel.updateChats(chats) @@ -579,38 +576,44 @@ open class ChatController(var ctrl: ChatCtrl?, val ntfManager: NtfManager, val a return null } - private suspend fun getUserSMPServers(): Pair, List>? { - val userId = kotlin.runCatching { currentUserId("getUserSMPServers") }.getOrElse { return null } - val r = sendCmd(CC.APIGetUserSMPServers(userId)) - if (r is CR.UserSMPServers) return r.smpServers to r.presetSMPServers - Log.e(TAG, "getUserSMPServers bad response: ${r.responseType} ${r.details}") - return null + suspend fun getUserProtoServers(serverProtocol: ServerProtocol): UserProtocolServers? { + val userId = kotlin.runCatching { currentUserId("getUserProtoServers") }.getOrElse { return null } + val r = sendCmd(CC.APIGetUserProtoServers(userId, serverProtocol)) + return if (r is CR.UserProtoServers) r.servers + else { + Log.e(TAG, "getUserProtoServers bad response: ${r.responseType} ${r.details}") + AlertManager.shared.showAlertMsg( + generalGetString(if (serverProtocol == ServerProtocol.SMP) R.string.error_loading_smp_servers else R.string.error_loading_xftp_servers), + "${r.responseType}: ${r.details}" + ) + null + } } - suspend fun setUserSMPServers(smpServers: List): Boolean { - val userId = kotlin.runCatching { currentUserId("setUserSMPServers") }.getOrElse { return false } - val r = sendCmd(CC.APISetUserSMPServers(userId, smpServers)) + suspend fun setUserProtoServers(serverProtocol: ServerProtocol, servers: List): Boolean { + val userId = kotlin.runCatching { currentUserId("setUserProtoServers") }.getOrElse { return false } + val r = sendCmd(CC.APISetUserProtoServers(userId, serverProtocol, servers)) return when (r) { is CR.CmdOk -> true else -> { - Log.e(TAG, "setUserSMPServers bad response: ${r.responseType} ${r.details}") + Log.e(TAG, "setUserProtoServers bad response: ${r.responseType} ${r.details}") AlertManager.shared.showAlertMsg( - generalGetString(R.string.error_saving_smp_servers), - generalGetString(R.string.ensure_smp_server_address_are_correct_format_and_unique) + generalGetString(if (serverProtocol == ServerProtocol.SMP) R.string.error_saving_smp_servers else R.string.error_saving_xftp_servers), + generalGetString(if (serverProtocol == ServerProtocol.SMP) R.string.ensure_smp_server_address_are_correct_format_and_unique else R.string.ensure_xftp_server_address_are_correct_format_and_unique) ) false } } } - suspend fun testSMPServer(smpServer: String): SMPTestFailure? { - val userId = currentUserId("testSMPServer") - val r = sendCmd(CC.APITestSMPServer(userId, smpServer)) + suspend fun testProtoServer(server: String): ProtocolTestFailure? { + val userId = currentUserId("testProtoServer") + val r = sendCmd(CC.APITestProtoServer(userId, server)) return when (r) { - is CR.SmpTestResult -> r.smpTestFailure + is CR.ServerTestResult -> r.testFailure else -> { - Log.e(TAG, "testSMPServer bad response: ${r.responseType} ${r.details}") - throw Exception("testSMPServer bad response: ${r.responseType} ${r.details}") + Log.e(TAG, "testProtoServer bad response: ${r.responseType} ${r.details}") + throw Exception("testProtoServer bad response: ${r.responseType} ${r.details}") } } } @@ -1865,9 +1868,9 @@ sealed class CC { class APIGroupLinkMemberRole(val groupId: Long, val memberRole: GroupMemberRole): CC() class APIDeleteGroupLink(val groupId: Long): CC() class APIGetGroupLink(val groupId: Long): CC() - class APIGetUserSMPServers(val userId: Long): CC() - class APISetUserSMPServers(val userId: Long, val smpServers: List): CC() - class APITestSMPServer(val userId: Long, val smpServer: String): CC() + class APIGetUserProtoServers(val userId: Long, val serverProtocol: ServerProtocol): CC() + class APISetUserProtoServers(val userId: Long, val serverProtocol: ServerProtocol, val servers: List): CC() + class APITestProtoServer(val userId: Long, val server: String): CC() class APISetChatItemTTL(val userId: Long, val seconds: Long?): CC() class APIGetChatItemTTL(val userId: Long): CC() class APISetNetworkConfig(val networkConfig: NetCfg): CC() @@ -1949,9 +1952,9 @@ sealed class CC { is APIGroupLinkMemberRole -> "/_set link role #$groupId ${memberRole.name.lowercase()}" is APIDeleteGroupLink -> "/_delete link #$groupId" is APIGetGroupLink -> "/_get link #$groupId" - is APIGetUserSMPServers -> "/_smp $userId" - is APISetUserSMPServers -> "/_smp $userId ${smpServersStr(smpServers)}" - is APITestSMPServer -> "/_smp test $userId $smpServer" + is APIGetUserProtoServers -> "/_servers $userId ${serverProtocol.name.lowercase()}" + is APISetUserProtoServers -> "/_servers $userId ${serverProtocol.name.lowercase()} ${protoServersStr(servers)}" + is APITestProtoServer -> "/_server test $userId $server" is APISetChatItemTTL -> "/_ttl $userId ${chatItemTTLStr(seconds)}" is APIGetChatItemTTL -> "/_ttl $userId" is APISetNetworkConfig -> "/_network ${json.encodeToString(networkConfig)}" @@ -2034,9 +2037,9 @@ sealed class CC { is APIGroupLinkMemberRole -> "apiGroupLinkMemberRole" is APIDeleteGroupLink -> "apiDeleteGroupLink" is APIGetGroupLink -> "apiGetGroupLink" - is APIGetUserSMPServers -> "apiGetUserSMPServers" - is APISetUserSMPServers -> "apiSetUserSMPServers" - is APITestSMPServer -> "testSMPServer" + is APIGetUserProtoServers -> "apiGetUserProtoServers" + is APISetUserProtoServers -> "apiSetUserProtoServers" + is APITestProtoServer -> "testProtoServer" is APISetChatItemTTL -> "apiSetChatItemTTL" is APIGetChatItemTTL -> "apiGetChatItemTTL" is APISetNetworkConfig -> "/apiSetNetworkConfig" @@ -2113,7 +2116,7 @@ sealed class CC { companion object { fun chatRef(chatType: ChatType, id: Long) = "${chatType.type}${id}" - fun smpServersStr(smpServers: List) = if (smpServers.isEmpty()) "default" else json.encodeToString(SMPServersConfig(smpServers)) + fun protoServersStr(servers: List) = json.encodeToString(ProtoServersConfig(servers)) } } @@ -2148,8 +2151,21 @@ class ArchiveConfig(val archivePath: String, val disableCompression: Boolean? = class DBEncryptionConfig(val currentKey: String, val newKey: String) @Serializable -data class SMPServersConfig( - val smpServers: List +enum class ServerProtocol { + @SerialName("smp") SMP, + @SerialName("xftp") XFTP; +} + +@Serializable +data class ProtoServersConfig( + val servers: List +) + +@Serializable +data class UserProtocolServers( + val serverProtocol: ServerProtocol, + val protoServers: List, + val presetServers: List, ) @Serializable @@ -2203,29 +2219,39 @@ data class ServerCfg( } @Serializable -enum class SMPTestStep { +enum class ProtocolTestStep { @SerialName("connect") Connect, + @SerialName("disconnect") Disconnect, @SerialName("createQueue") CreateQueue, @SerialName("secureQueue") SecureQueue, @SerialName("deleteQueue") DeleteQueue, - @SerialName("disconnect") Disconnect; + @SerialName("createFile") CreateFile, + @SerialName("uploadFile") UploadFile, + @SerialName("downloadFile") DownloadFile, + @SerialName("compareFile") CompareFile, + @SerialName("deleteFile") DeleteFile; val text: String get() = when (this) { Connect -> generalGetString(R.string.smp_server_test_connect) + Disconnect -> generalGetString(R.string.smp_server_test_disconnect) CreateQueue -> generalGetString(R.string.smp_server_test_create_queue) SecureQueue -> generalGetString(R.string.smp_server_test_secure_queue) DeleteQueue -> generalGetString(R.string.smp_server_test_delete_queue) - Disconnect -> generalGetString(R.string.smp_server_test_disconnect) + CreateFile -> generalGetString(R.string.smp_server_test_create_file) + UploadFile -> generalGetString(R.string.smp_server_test_upload_file) + DownloadFile -> generalGetString(R.string.smp_server_test_download_file) + CompareFile -> generalGetString(R.string.smp_server_test_compare_file) + DeleteFile -> generalGetString(R.string.smp_server_test_delete_file) } } @Serializable -data class SMPTestFailure( - val testStep: SMPTestStep, +data class ProtocolTestFailure( + val testStep: ProtocolTestStep, val testError: AgentErrorType ) { override fun equals(other: Any?): Boolean { - if (other !is SMPTestFailure) return false + if (other !is ProtocolTestFailure) return false return other.testStep == this.testStep } @@ -2238,6 +2264,8 @@ data class SMPTestFailure( return when { testError is AgentErrorType.SMP && testError.smpErr is SMPErrorType.AUTH -> err + " " + generalGetString(R.string.error_smp_test_server_auth) + testError is AgentErrorType.XFTP && testError.xftpErr is XFTPErrorType.AUTH -> + err + " " + generalGetString(R.string.error_xftp_test_server_auth) testError is AgentErrorType.BROKER && testError.brokerErr is BrokerErrorType.NETWORK -> err + " " + generalGetString(R.string.error_smp_test_certificate) else -> err @@ -2247,6 +2275,7 @@ data class SMPTestFailure( @Serializable data class ServerAddress( + val serverProtocol: ServerProtocol, val hostnames: List, val port: String, val keyHash: String, @@ -2254,19 +2283,21 @@ data class ServerAddress( ) { val uri: String get() = - "smp://${keyHash}${if (basicAuth.isEmpty()) "" else ":$basicAuth"}@${hostnames.joinToString(",")}" + "${serverProtocol}://${keyHash}${if (basicAuth.isEmpty()) "" else ":$basicAuth"}@${hostnames.joinToString(",")}" val valid: Boolean get() = hostnames.isNotEmpty() && hostnames.toSet().size == hostnames.size companion object { - val empty = ServerAddress( + fun empty(serverProtocol: ServerProtocol) = ServerAddress( + serverProtocol = serverProtocol, hostnames = emptyList(), port = "", keyHash = "", basicAuth = "" ) val sampleData = ServerAddress( + serverProtocol = ServerProtocol.SMP, hostnames = listOf("smp.simplex.im", "1234.onion"), port = "", keyHash = "LcJUMfVhwD8yxjAiSaDzzGF3-kLG4Uh0Fl_ZIjrRwjI=", @@ -3011,8 +3042,8 @@ sealed class CR { @Serializable @SerialName("chatStopped") class ChatStopped: CR() @Serializable @SerialName("apiChats") class ApiChats(val user: User, val chats: List): CR() @Serializable @SerialName("apiChat") class ApiChat(val user: User, val chat: Chat): CR() - @Serializable @SerialName("userSMPServers") class UserSMPServers(val user: User, val smpServers: List, val presetSMPServers: List): CR() - @Serializable @SerialName("smpTestResult") class SmpTestResult(val user: User, val smpTestFailure: SMPTestFailure? = null): CR() + @Serializable @SerialName("userProtoServers") class UserProtoServers(val user: User, val servers: UserProtocolServers): CR() + @Serializable @SerialName("serverTestResult") class ServerTestResult(val user: User, val testServer: String, val testFailure: ProtocolTestFailure? = null): CR() @Serializable @SerialName("chatItemTTL") class ChatItemTTL(val user: User, val chatItemTTL: Long? = null): CR() @Serializable @SerialName("networkConfig") class NetworkConfig(val networkConfig: NetCfg): CR() @Serializable @SerialName("contactInfo") class ContactInfo(val user: User, val contact: Contact, val connectionStats: ConnectionStats, val customUserProfile: Profile? = null): CR() @@ -3119,8 +3150,8 @@ sealed class CR { is ChatStopped -> "chatStopped" is ApiChats -> "apiChats" is ApiChat -> "apiChat" - is UserSMPServers -> "userSMPServers" - is SmpTestResult -> "smpTestResult" + is UserProtoServers -> "userProtoServers" + is ServerTestResult -> "serverTestResult" is ChatItemTTL -> "chatItemTTL" is NetworkConfig -> "networkConfig" is ContactInfo -> "contactInfo" @@ -3225,8 +3256,8 @@ sealed class CR { is ChatStopped -> noDetails() is ApiChats -> withUser(user, json.encodeToString(chats)) is ApiChat -> withUser(user, json.encodeToString(chat)) - is UserSMPServers -> withUser(user, "$smpServers: ${json.encodeToString(smpServers)}\n$presetSMPServers: ${json.encodeToString(presetSMPServers)}") - is SmpTestResult -> withUser(user, json.encodeToString(smpTestFailure)) + is UserProtoServers -> withUser(user, "servers: ${json.encodeToString(servers)}") + is ServerTestResult -> withUser(user, "server: $testServer\nresult: ${json.encodeToString(testFailure)}") is ChatItemTTL -> withUser(user, json.encodeToString(chatItemTTL)) is NetworkConfig -> json.encodeToString(networkConfig) is ContactInfo -> withUser(user, "contact: ${json.encodeToString(contact)}\nconnectionStats: ${json.encodeToString(connectionStats)}") @@ -3459,6 +3490,7 @@ sealed class AgentErrorType { is CMD -> "CMD ${cmdErr.string}" is CONN -> "CONN ${connErr.string}" is SMP -> "SMP ${smpErr.string}" + is XFTP -> "XFTP ${xftpErr.string}" is BROKER -> "BROKER ${brokerErr.string}" is AGENT -> "AGENT ${agentErr.string}" is INTERNAL -> "INTERNAL $internalErr" @@ -3466,6 +3498,7 @@ sealed class AgentErrorType { @Serializable @SerialName("CMD") class CMD(val cmdErr: CommandErrorType): AgentErrorType() @Serializable @SerialName("CONN") class CONN(val connErr: ConnectionErrorType): AgentErrorType() @Serializable @SerialName("SMP") class SMP(val smpErr: SMPErrorType): AgentErrorType() + @Serializable @SerialName("XFTP") class XFTP(val xftpErr: XFTPErrorType): AgentErrorType() @Serializable @SerialName("BROKER") class BROKER(val brokerAddress: String, val brokerErr: BrokerErrorType): AgentErrorType() @Serializable @SerialName("AGENT") class AGENT(val agentErr: SMPAgentError): AgentErrorType() @Serializable @SerialName("INTERNAL") class INTERNAL(val internalErr: String): AgentErrorType() @@ -3533,7 +3566,7 @@ sealed class SMPErrorType { } @Serializable @SerialName("BLOCK") class BLOCK: SMPErrorType() @Serializable @SerialName("SESSION") class SESSION: SMPErrorType() - @Serializable @SerialName("CMD") class CMD(val cmdErr: SMPCommandError): SMPErrorType() + @Serializable @SerialName("CMD") class CMD(val cmdErr: ProtocolCommandError): SMPErrorType() @Serializable @SerialName("AUTH") class AUTH: SMPErrorType() @Serializable @SerialName("QUOTA") class QUOTA: SMPErrorType() @Serializable @SerialName("NO_MSG") class NO_MSG: SMPErrorType() @@ -3542,7 +3575,7 @@ sealed class SMPErrorType { } @Serializable -sealed class SMPCommandError { +sealed class ProtocolCommandError { val string: String get() = when (this) { is UNKNOWN -> "UNKNOWN" is SYNTAX -> "SYNTAX" @@ -3550,11 +3583,11 @@ sealed class SMPCommandError { is HAS_AUTH -> "HAS_AUTH" is NO_QUEUE -> "NO_QUEUE" } - @Serializable @SerialName("UNKNOWN") class UNKNOWN: SMPCommandError() - @Serializable @SerialName("SYNTAX") class SYNTAX: SMPCommandError() - @Serializable @SerialName("NO_AUTH") class NO_AUTH: SMPCommandError() - @Serializable @SerialName("HAS_AUTH") class HAS_AUTH: SMPCommandError() - @Serializable @SerialName("NO_QUEUE") class NO_QUEUE: SMPCommandError() + @Serializable @SerialName("UNKNOWN") class UNKNOWN: ProtocolCommandError() + @Serializable @SerialName("SYNTAX") class SYNTAX: ProtocolCommandError() + @Serializable @SerialName("NO_AUTH") class NO_AUTH: ProtocolCommandError() + @Serializable @SerialName("HAS_AUTH") class HAS_AUTH: ProtocolCommandError() + @Serializable @SerialName("NO_QUEUE") class NO_QUEUE: ProtocolCommandError() } @Serializable @@ -3596,3 +3629,33 @@ sealed class SMPAgentError { @Serializable @SerialName("A_VERSION") class A_VERSION: SMPAgentError() @Serializable @SerialName("A_ENCRYPTION") class A_ENCRYPTION: SMPAgentError() } + +@Serializable +sealed class XFTPErrorType { + val string: String get() = when (this) { + is BLOCK -> "BLOCK" + is SESSION -> "SESSION" + is CMD -> "CMD ${cmdErr.string}" + is AUTH -> "AUTH" + is SIZE -> "SIZE" + is QUOTA -> "QUOTA" + is DIGEST -> "DIGEST" + is CRYPTO -> "CRYPTO" + is NO_FILE -> "NO_FILE" + is HAS_FILE -> "HAS_FILE" + is FILE_IO -> "FILE_IO" + is INTERNAL -> "INTERNAL" + } + @Serializable @SerialName("BLOCK") object BLOCK: XFTPErrorType() + @Serializable @SerialName("SESSION") object SESSION: XFTPErrorType() + @Serializable @SerialName("CMD") class CMD(val cmdErr: ProtocolCommandError): XFTPErrorType() + @Serializable @SerialName("AUTH") object AUTH: XFTPErrorType() + @Serializable @SerialName("SIZE") object SIZE: XFTPErrorType() + @Serializable @SerialName("QUOTA") object QUOTA: XFTPErrorType() + @Serializable @SerialName("DIGEST") object DIGEST: XFTPErrorType() + @Serializable @SerialName("CRYPTO") object CRYPTO: XFTPErrorType() + @Serializable @SerialName("NO_FILE") object NO_FILE: XFTPErrorType() + @Serializable @SerialName("HAS_FILE") object HAS_FILE: XFTPErrorType() + @Serializable @SerialName("FILE_IO") object FILE_IO: XFTPErrorType() + @Serializable @SerialName("INTERNAL") object INTERNAL: XFTPErrorType() +} \ No newline at end of file diff --git a/apps/android/app/src/main/java/chat/simplex/app/views/usersettings/NetworkAndServers.kt b/apps/android/app/src/main/java/chat/simplex/app/views/usersettings/NetworkAndServers.kt index 36cffb693f..1ed408f150 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/views/usersettings/NetworkAndServers.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/views/usersettings/NetworkAndServers.kt @@ -40,6 +40,7 @@ fun NetworkAndServersView( NetworkAndServersLayout( developerTools = developerTools, + xftpSendEnabled = remember { chatModel.controller.appPrefs.xftpSendEnabled.state }, networkUseSocksProxy = networkUseSocksProxy, onionHosts = onionHosts, sessionMode = sessionMode, @@ -135,6 +136,7 @@ fun NetworkAndServersView( @Composable fun NetworkAndServersLayout( developerTools: Boolean, + xftpSendEnabled: State, networkUseSocksProxy: MutableState, onionHosts: MutableState, sessionMode: MutableState, @@ -152,8 +154,14 @@ fun NetworkAndServersView( ) { AppBarTitle(stringResource(R.string.network_and_servers)) SectionView(generalGetString(R.string.settings_section_title_messages)) { - SettingsActionItem(Icons.Outlined.Dns, stringResource(R.string.smp_servers), showCustomModal { m, close -> SMPServersView(m, close) }) + SettingsActionItem(Icons.Outlined.Dns, stringResource(R.string.smp_servers), showCustomModal { m, close -> ProtocolServersView(m, ServerProtocol.SMP, close) }) SectionDivider() + + if (xftpSendEnabled.value) { + SettingsActionItem(Icons.Outlined.Dns, stringResource(R.string.xftp_servers), showCustomModal { m, close -> ProtocolServersView(m, ServerProtocol.XFTP, close) }) + SectionDivider() + } + SectionItemView { UseSocksProxySwitch(networkUseSocksProxy, toggleSocksProxy) } @@ -297,6 +305,7 @@ fun PreviewNetworkAndServersLayout() { SimpleXTheme { NetworkAndServersLayout( developerTools = true, + xftpSendEnabled = remember { mutableStateOf(true) }, networkUseSocksProxy = remember { mutableStateOf(true) }, showModal = { {} }, showSettingsModal = { {} }, diff --git a/apps/android/app/src/main/java/chat/simplex/app/views/usersettings/SMPServerView.kt b/apps/android/app/src/main/java/chat/simplex/app/views/usersettings/ProtocolServerView.kt similarity index 89% rename from apps/android/app/src/main/java/chat/simplex/app/views/usersettings/SMPServerView.kt rename to apps/android/app/src/main/java/chat/simplex/app/views/usersettings/ProtocolServerView.kt index e6d61332ce..923693790b 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/views/usersettings/SMPServerView.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/views/usersettings/ProtocolServerView.kt @@ -32,12 +32,13 @@ import kotlinx.coroutines.isActive import kotlinx.coroutines.launch @Composable -fun SMPServerView(m: ChatModel, server: ServerCfg, onUpdate: (ServerCfg) -> Unit, onDelete: () -> Unit) { +fun ProtocolServerView(m: ChatModel, server: ServerCfg, serverProtocol: ServerProtocol, onUpdate: (ServerCfg) -> Unit, onDelete: () -> Unit) { var testing by remember { mutableStateOf(false) } val scope = rememberCoroutineScope() - SMPServerLayout( + ProtocolServerLayout( testing, server, + serverProtocol, testServer = { testing = true scope.launch { @@ -68,9 +69,10 @@ fun SMPServerView(m: ChatModel, server: ServerCfg, onUpdate: (ServerCfg) -> Unit } @Composable -private fun SMPServerLayout( +private fun ProtocolServerLayout( testing: Boolean, server: ServerCfg, + serverProtocol: ServerProtocol, testServer: () -> Unit, onUpdate: (ServerCfg) -> Unit, onDelete: () -> Unit, @@ -86,7 +88,7 @@ private fun SMPServerLayout( if (server.preset) { PresetServer(testing, server, testServer, onUpdate, onDelete) } else { - CustomServer(testing, server, testServer, onUpdate, onDelete) + CustomServer(testing, server, serverProtocol, testServer, onUpdate, onDelete) } } } @@ -119,12 +121,19 @@ private fun PresetServer( private fun CustomServer( testing: Boolean, server: ServerCfg, + serverProtocol: ServerProtocol, testServer: () -> Unit, onUpdate: (ServerCfg) -> Unit, onDelete: () -> Unit, ) { val serverAddress = remember { mutableStateOf(server.server) } - val valid = remember { derivedStateOf { parseServerAddress(serverAddress.value)?.valid == true } } + val valid = remember { + derivedStateOf { + with(parseServerAddress(serverAddress.value)) { + this?.valid == true && this.serverProtocol == serverProtocol + } + } + } SectionView( stringResource(R.string.smp_servers_your_server_address).uppercase(), icon = Icons.Outlined.ErrorOutline, @@ -187,9 +196,9 @@ fun ShowTestStatus(server: ServerCfg, modifier: Modifier = Modifier) = else -> Icon(Icons.Outlined.Check, null, modifier, tint = Color.Transparent) } -suspend fun testServerConnection(server: ServerCfg, m: ChatModel): Pair = +suspend fun testServerConnection(server: ServerCfg, m: ChatModel): Pair = try { - val r = m.controller.testSMPServer(server.server) + val r = m.controller.testProtoServer(server.server) server.copy(tested = r == null) to r } catch (e: Exception) { Log.e(TAG, "testServerConnection ${e.stackTraceToString()}") diff --git a/apps/android/app/src/main/java/chat/simplex/app/views/usersettings/SMPServersView.kt b/apps/android/app/src/main/java/chat/simplex/app/views/usersettings/ProtocolServersView.kt similarity index 79% rename from apps/android/app/src/main/java/chat/simplex/app/views/usersettings/SMPServersView.kt rename to apps/android/app/src/main/java/chat/simplex/app/views/usersettings/ProtocolServersView.kt index 8286121c33..210db9f61d 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/views/usersettings/SMPServersView.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/views/usersettings/ProtocolServersView.kt @@ -27,17 +27,19 @@ import chat.simplex.app.views.helpers.* import kotlinx.coroutines.launch @Composable -fun SMPServersView(m: ChatModel, close: () -> Unit) { +fun ProtocolServersView(m: ChatModel, serverProtocol: ServerProtocol, close: () -> Unit) { + var presetServers by remember { mutableStateOf(emptyList()) } var servers by remember { - mutableStateOf(m.userSMPServersUnsaved.value ?: m.userSMPServers.value ?: emptyList()) + mutableStateOf(m.userSMPServersUnsaved.value ?: emptyList()) } + val currServers = remember { mutableStateOf(servers) } val testing = rememberSaveable { mutableStateOf(false) } - val serversUnchanged = remember { derivedStateOf { servers == m.userSMPServers.value || testing.value } } + val serversUnchanged = remember { derivedStateOf { servers == currServers.value || testing.value } } val allServersDisabled = remember { derivedStateOf { servers.all { !it.enabled } } } val saveDisabled = remember { derivedStateOf { servers.isEmpty() || - servers == m.userSMPServers.value || + servers == currServers.value || testing.value || !servers.all { srv -> val address = parseServerAddress(srv.server) @@ -47,13 +49,25 @@ fun SMPServersView(m: ChatModel, close: () -> Unit) { } } + LaunchedEffect(Unit) { + val res = m.controller.getUserProtoServers(serverProtocol) + if (res != null) { + currServers.value = res.protoServers + presetServers = res.presetServers + if (servers.isEmpty()) { + servers = currServers.value + } + } + } + fun showServer(server: ServerCfg) { ModalManager.shared.showModalCloseable(true) { close -> var old by remember { mutableStateOf(server) } val index = servers.indexOf(old) - SMPServerView( + ProtocolServerView( m, old, + serverProtocol, onUpdate = { updated -> val newServers = ArrayList(servers) newServers.removeAt(index) @@ -75,11 +89,12 @@ fun SMPServersView(m: ChatModel, close: () -> Unit) { ModalView( close = { if (saveDisabled.value) close() - else showUnsavedChangesAlert({ saveSMPServers(servers, m, close) }, close) + else showUnsavedChangesAlert({ saveServers(serverProtocol, currServers, servers, m, close) }, close) }, background = if (isInDarkTheme()) MaterialTheme.colors.background else SettingsBackgroundLight ) { - SMPServersLayout( + ProtocolServersLayout( + serverProtocol, testing = testing.value, servers = servers, serversUnchanged = serversUnchanged.value, @@ -102,7 +117,7 @@ fun SMPServersView(m: ChatModel, close: () -> Unit) { SectionItemView({ AlertManager.shared.hideAlert() ModalManager.shared.showModalCloseable { close -> - ScanSMPServer { + ScanProtocolServer { close() servers = servers + it m.userSMPServersUnsaved.value = servers @@ -112,11 +127,11 @@ fun SMPServersView(m: ChatModel, close: () -> Unit) { ) { Text(stringResource(R.string.smp_servers_scan_qr)) } - val hasAllPresets = hasAllPresets(servers, m) + val hasAllPresets = hasAllPresets(presetServers, servers, m) if (!hasAllPresets) { SectionItemView({ AlertManager.shared.hideAlert() - servers = (servers + addAllPresets(servers, m)).sortedByDescending { it.preset } + servers = (servers + addAllPresets(presetServers, servers, m)).sortedByDescending { it.preset } }) { Text(stringResource(R.string.smp_servers_preset_add), color = MaterialTheme.colors.onBackground) } @@ -134,11 +149,11 @@ fun SMPServersView(m: ChatModel, close: () -> Unit) { } }, resetServers = { - servers = m.userSMPServers.value ?: emptyList() + servers = currServers.value ?: emptyList() m.userSMPServersUnsaved.value = null }, saveSMPServers = { - saveSMPServers(servers, m) + saveServers(serverProtocol, currServers, servers, m) }, showServer = ::showServer, ) @@ -161,7 +176,8 @@ fun SMPServersView(m: ChatModel, close: () -> Unit) { } @Composable -private fun SMPServersLayout( +private fun ProtocolServersLayout( + serverProtocol: ServerProtocol, testing: Boolean, servers: List, serversUnchanged: Boolean, @@ -180,12 +196,12 @@ private fun SMPServersLayout( .verticalScroll(rememberScrollState()) .padding(bottom = DEFAULT_PADDING), ) { - AppBarTitle(stringResource(R.string.your_SMP_servers)) + AppBarTitle(stringResource(if (serverProtocol == ServerProtocol.SMP) R.string.your_SMP_servers else R.string.your_XFTP_servers)) - SectionView(stringResource(R.string.smp_servers).uppercase()) { + SectionView(stringResource(if (serverProtocol == ServerProtocol.SMP) R.string.smp_servers else R.string.xftp_servers).uppercase()) { for (srv in servers) { SectionItemView({ showServer(srv) }, disabled = testing) { - SmpServerView(srv, servers, testing) + ProtocolServerView(serverProtocol, srv, servers, testing) } SectionDivider() } @@ -232,10 +248,10 @@ private fun SMPServersLayout( } @Composable -private fun SmpServerView(srv: ServerCfg, servers: List, disabled: Boolean) { +private fun ProtocolServerView(serverProtocol: ServerProtocol, srv: ServerCfg, servers: List, disabled: Boolean) { val address = parseServerAddress(srv.server) when { - address == null || !address.valid || !uniqueAddress(srv, address, servers) -> InvalidServer() + address == null || !address.valid || address.serverProtocol != serverProtocol || !uniqueAddress(srv, address, servers) -> InvalidServer() !srv.enabled -> Icon(Icons.Outlined.DoNotDisturb, null, tint = HighOrLowlight) else -> ShowTestStatus(srv) } @@ -271,12 +287,12 @@ private fun uniqueAddress(s: ServerCfg, address: ServerAddress, servers: List, m: ChatModel): Boolean = - m.presetSMPServers.value?.all { hasPreset(it, servers) } ?: true +private fun hasAllPresets(presetServers: List, servers: List, m: ChatModel): Boolean = + presetServers.all { hasPreset(it, servers) } ?: true -private fun addAllPresets(servers: List, m: ChatModel): List { +private fun addAllPresets(presetServers: List, servers: List, m: ChatModel): List { val toAdd = ArrayList() - for (srv in m.presetSMPServers.value ?: emptyList()) { + for (srv in presetServers) { if (!hasPreset(srv, servers)) { toAdd.add(ServerCfg(srv, preset = true, tested = null, enabled = true)) } @@ -313,8 +329,8 @@ private fun resetTestStatus(servers: List): List { return copy } -private suspend fun runServersTest(servers: List, m: ChatModel, onUpdated: (List) -> Unit): Map { - val fs: MutableMap = mutableMapOf() +private suspend fun runServersTest(servers: List, m: ChatModel, onUpdated: (List) -> Unit): Map { + val fs: MutableMap = mutableMapOf() val updatedServers = ArrayList(servers) for ((index, server) in servers.withIndex()) { if (server.enabled) { @@ -331,10 +347,10 @@ private suspend fun runServersTest(servers: List, m: ChatModel, onUpd return fs } -private fun saveSMPServers(servers: List, m: ChatModel, afterSave: () -> Unit = {}) { +private fun saveServers(protocol: ServerProtocol, currServers: MutableState>, servers: List, m: ChatModel, afterSave: () -> Unit = {}) { withApi { - if (m.controller.setUserSMPServers(servers)) { - m.userSMPServers.value = servers + if (m.controller.setUserProtoServers(protocol, servers)) { + currServers.value = servers m.userSMPServersUnsaved.value = null } afterSave() diff --git a/apps/android/app/src/main/java/chat/simplex/app/views/usersettings/ScanSMPServer.kt b/apps/android/app/src/main/java/chat/simplex/app/views/usersettings/ScanProtocolServer.kt similarity index 89% rename from apps/android/app/src/main/java/chat/simplex/app/views/usersettings/ScanSMPServer.kt rename to apps/android/app/src/main/java/chat/simplex/app/views/usersettings/ScanProtocolServer.kt index 3844a712d7..525263a27b 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/views/usersettings/ScanSMPServer.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/views/usersettings/ScanProtocolServer.kt @@ -2,7 +2,6 @@ package chat.simplex.app.views.usersettings import android.Manifest import androidx.compose.foundation.layout.* -import androidx.compose.foundation.verticalScroll import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.ui.Modifier @@ -17,16 +16,16 @@ import chat.simplex.app.views.newchat.QRCodeScanner import com.google.accompanist.permissions.rememberPermissionState @Composable -fun ScanSMPServer(onNext: (ServerCfg) -> Unit) { +fun ScanProtocolServer(onNext: (ServerCfg) -> Unit) { val cameraPermissionState = rememberPermissionState(permission = Manifest.permission.CAMERA) LaunchedEffect(Unit) { cameraPermissionState.launchPermissionRequest() } - ScanSMPServerLayout(onNext) + ScanProtocolServerLayout(onNext) } @Composable -private fun ScanSMPServerLayout(onNext: (ServerCfg) -> Unit) { +private fun ScanProtocolServerLayout(onNext: (ServerCfg) -> Unit) { Column( Modifier .fillMaxSize() diff --git a/apps/android/app/src/main/res/values/strings.xml b/apps/android/app/src/main/res/values/strings.xml index 80eb68bdfe..b59775407c 100644 --- a/apps/android/app/src/main/res/values/strings.xml +++ b/apps/android/app/src/main/res/values/strings.xml @@ -60,7 +60,11 @@ Error saving SMP servers + Error saving XFTP servers Make sure SMP server addresses are in correct format, line separated and are not duplicated. + Make sure XFTP server addresses are in correct format, line separated and are not duplicated. + Error loading SMP servers + Error loading XFTP servers Error updating network configuration Failed to load chat Failed to load chats @@ -96,12 +100,18 @@ Error changing address Test failed at step %s. Server requires authorization to create queues, check password + Server requires authorization to upload, check password Possibly, certificate fingerprint in server address is incorrect Connect + Disconnect Create queue Secure queue Delete queue - Disconnect + Create file + Upload file + Download file + Compare file + Delete file Error deleting user profile Error updating user privacy @@ -476,12 +486,14 @@ Delete server The servers for new connections of your current chat profile Save servers? + XFTP servers Install SimpleX Chat for terminal Star on GitHub Contribute Rate the app Use SimpleX Chat servers? Your SMP servers + Your XFTP servers Using SimpleX Chat servers. How to How to use your servers @@ -732,7 +744,7 @@ LANGUAGE APP ICON THEMES - MESSAGES + MESSAGES AND FILES CALLS Incognito mode EXPERIMENTAL diff --git a/apps/ios/SimpleXChat/APITypes.swift b/apps/ios/SimpleXChat/APITypes.swift index 2bac9953e0..75c2ed540b 100644 --- a/apps/ios/SimpleXChat/APITypes.swift +++ b/apps/ios/SimpleXChat/APITypes.swift @@ -602,7 +602,7 @@ public enum ChatResponse: Decodable, Error { case let .apiChats(u, chats): return withUser(u, String(describing: chats)) case let .apiChat(u, chat): return withUser(u, String(describing: chat)) case let .userProtoServers(u, servers): return withUser(u, "servers: \(String(describing: servers))") - case let .serverTestResult(u, server, testFailure): return withUser(u, "server: \(server)\result: \(String(describing: testFailure))") + case let .serverTestResult(u, server, testFailure): return withUser(u, "server: \(server)\nresult: \(String(describing: testFailure))") case let .chatItemTTL(u, chatItemTTL): return withUser(u, String(describing: chatItemTTL)) case let .networkConfig(networkConfig): return String(describing: networkConfig) case let .contactInfo(u, contact, connectionStats, customUserProfile): return withUser(u, "contact: \(String(describing: contact))\nconnectionStats: \(String(describing: connectionStats))\ncustomUserProfile: \(String(describing: customUserProfile))")