diff --git a/apps/ios/Shared/SimpleXApp.swift b/apps/ios/Shared/SimpleXApp.swift index 13e681ae2..fd1ec9511 100644 --- a/apps/ios/Shared/SimpleXApp.swift +++ b/apps/ios/Shared/SimpleXApp.swift @@ -26,7 +26,10 @@ struct SimpleXApp: App { @State private var showInitializationView = false init() { - hs_init(0, nil) + DispatchQueue.global(qos: .background).sync { + haskell_init() +// hs_init(0, nil) + } UserDefaults.standard.register(defaults: appDefaults) setGroupDefaults() registerGroupDefaults() diff --git a/apps/ios/Shared/Views/Chat/Group/AddGroupMembersView.swift b/apps/ios/Shared/Views/Chat/Group/AddGroupMembersView.swift index d206b9b41..b89c006c6 100644 --- a/apps/ios/Shared/Views/Chat/Group/AddGroupMembersView.swift +++ b/apps/ios/Shared/Views/Chat/Group/AddGroupMembersView.swift @@ -157,7 +157,7 @@ struct AddGroupMembersViewCommon: View { private func rolePicker() -> some View { Picker("New member role", selection: $selectedRole) { ForEach(GroupMemberRole.allCases) { role in - if role <= groupInfo.membership.memberRole { + if role <= groupInfo.membership.memberRole && role != .author { Text(role.text) } } diff --git a/apps/ios/Shared/Views/RemoteAccess/ConnectDesktopView.swift b/apps/ios/Shared/Views/RemoteAccess/ConnectDesktopView.swift index 0f6ef7be0..0ee231288 100644 --- a/apps/ios/Shared/Views/RemoteAccess/ConnectDesktopView.swift +++ b/apps/ios/Shared/Views/RemoteAccess/ConnectDesktopView.swift @@ -382,7 +382,7 @@ struct ConnectDesktopView: View { private func disconnectButton() -> some View { Button { - disconnectDesktop() + disconnectDesktop(.dismiss) } label: { Label("Disconnect", systemImage: "multiply") } diff --git a/apps/ios/SimpleX.xcodeproj/project.pbxproj b/apps/ios/SimpleX.xcodeproj/project.pbxproj index d8fb52ca3..47769ac2d 100644 --- a/apps/ios/SimpleX.xcodeproj/project.pbxproj +++ b/apps/ios/SimpleX.xcodeproj/project.pbxproj @@ -118,11 +118,8 @@ 5CCB939C297EFCB100399E78 /* NavStackCompat.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CCB939B297EFCB100399E78 /* NavStackCompat.swift */; }; 5CCD403427A5F6DF00368C90 /* AddContactView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CCD403327A5F6DF00368C90 /* AddContactView.swift */; }; 5CCD403727A5F9A200368C90 /* ScanToConnectView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CCD403627A5F9A200368C90 /* ScanToConnectView.swift */; }; - 5CDA5A2D2B04FE2D00A71D61 /* libgmp.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5CDA5A282B04FE2D00A71D61 /* libgmp.a */; }; - 5CDA5A2E2B04FE2D00A71D61 /* libHSsimplex-chat-5.4.0.3-rODxCBVsb2BkD1fnTAqXL-ghc9.6.3.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5CDA5A292B04FE2D00A71D61 /* libHSsimplex-chat-5.4.0.3-rODxCBVsb2BkD1fnTAqXL-ghc9.6.3.a */; }; - 5CDA5A2F2B04FE2D00A71D61 /* libffi.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5CDA5A2A2B04FE2D00A71D61 /* libffi.a */; }; - 5CDA5A302B04FE2D00A71D61 /* libgmpxx.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5CDA5A2B2B04FE2D00A71D61 /* libgmpxx.a */; }; - 5CDA5A312B04FE2D00A71D61 /* libHSsimplex-chat-5.4.0.3-rODxCBVsb2BkD1fnTAqXL.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5CDA5A2C2B04FE2D00A71D61 /* libHSsimplex-chat-5.4.0.3-rODxCBVsb2BkD1fnTAqXL.a */; }; + 5CD67B8F2B0E858A00C510B1 /* hs_init.h in Headers */ = {isa = PBXBuildFile; fileRef = 5CD67B8D2B0E858A00C510B1 /* hs_init.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 5CD67B902B0E858A00C510B1 /* hs_init.c in Sources */ = {isa = PBXBuildFile; fileRef = 5CD67B8E2B0E858A00C510B1 /* hs_init.c */; }; 5CDCAD482818589900503DA2 /* NotificationService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CDCAD472818589900503DA2 /* NotificationService.swift */; }; 5CE2BA702845308900EC33A6 /* SimpleXChat.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 5CE2BA682845308900EC33A6 /* SimpleXChat.framework */; }; 5CE2BA712845308900EC33A6 /* SimpleXChat.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 5CE2BA682845308900EC33A6 /* SimpleXChat.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; @@ -148,6 +145,11 @@ 5CEACCED27DEA495000BD591 /* MsgContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CEACCEC27DEA495000BD591 /* MsgContentView.swift */; }; 5CEBD7462A5C0A8F00665FE2 /* KeyboardPadding.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CEBD7452A5C0A8F00665FE2 /* KeyboardPadding.swift */; }; 5CEBD7482A5F115D00665FE2 /* SetDeliveryReceiptsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CEBD7472A5F115D00665FE2 /* SetDeliveryReceiptsView.swift */; }; + 5CF077FB2B0D60C100105111 /* libgmpxx.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5CF077F62B0D60C000105111 /* libgmpxx.a */; }; + 5CF077FC2B0D60C100105111 /* libHSsimplex-chat-5.4.0.5-AEaxUB19STC3bOtqr9BLL2.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5CF077F72B0D60C000105111 /* libHSsimplex-chat-5.4.0.5-AEaxUB19STC3bOtqr9BLL2.a */; }; + 5CF077FD2B0D60C100105111 /* libgmp.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5CF077F82B0D60C000105111 /* libgmp.a */; }; + 5CF077FE2B0D60C100105111 /* libHSsimplex-chat-5.4.0.5-AEaxUB19STC3bOtqr9BLL2-ghc9.6.3.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5CF077F92B0D60C100105111 /* libHSsimplex-chat-5.4.0.5-AEaxUB19STC3bOtqr9BLL2-ghc9.6.3.a */; }; + 5CF077FF2B0D60C100105111 /* libffi.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5CF077FA2B0D60C100105111 /* libffi.a */; }; 5CFA59C42860BC6200863A68 /* MigrateToAppGroupView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CFA59C32860BC6200863A68 /* MigrateToAppGroupView.swift */; }; 5CFA59D12864782E00863A68 /* ChatArchiveView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CFA59CF286477B400863A68 /* ChatArchiveView.swift */; }; 5CFE0921282EEAF60002594B /* ZoomableScrollView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CFE0920282EEAF60002594B /* ZoomableScrollView.swift */; }; @@ -404,11 +406,8 @@ 5CCB939B297EFCB100399E78 /* NavStackCompat.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NavStackCompat.swift; sourceTree = ""; }; 5CCD403327A5F6DF00368C90 /* AddContactView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddContactView.swift; sourceTree = ""; }; 5CCD403627A5F9A200368C90 /* ScanToConnectView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ScanToConnectView.swift; sourceTree = ""; }; - 5CDA5A282B04FE2D00A71D61 /* libgmp.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libgmp.a; sourceTree = ""; }; - 5CDA5A292B04FE2D00A71D61 /* libHSsimplex-chat-5.4.0.3-rODxCBVsb2BkD1fnTAqXL-ghc9.6.3.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-5.4.0.3-rODxCBVsb2BkD1fnTAqXL-ghc9.6.3.a"; sourceTree = ""; }; - 5CDA5A2A2B04FE2D00A71D61 /* libffi.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libffi.a; sourceTree = ""; }; - 5CDA5A2B2B04FE2D00A71D61 /* libgmpxx.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libgmpxx.a; sourceTree = ""; }; - 5CDA5A2C2B04FE2D00A71D61 /* libHSsimplex-chat-5.4.0.3-rODxCBVsb2BkD1fnTAqXL.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-5.4.0.3-rODxCBVsb2BkD1fnTAqXL.a"; sourceTree = ""; }; + 5CD67B8D2B0E858A00C510B1 /* hs_init.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = hs_init.h; sourceTree = ""; }; + 5CD67B8E2B0E858A00C510B1 /* hs_init.c */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.c; path = hs_init.c; sourceTree = ""; }; 5CDCAD452818589900503DA2 /* SimpleX NSE.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = "SimpleX NSE.appex"; sourceTree = BUILT_PRODUCTS_DIR; }; 5CDCAD472818589900503DA2 /* NotificationService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationService.swift; sourceTree = ""; }; 5CDCAD492818589900503DA2 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; @@ -435,6 +434,11 @@ 5CEACCEC27DEA495000BD591 /* MsgContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MsgContentView.swift; sourceTree = ""; }; 5CEBD7452A5C0A8F00665FE2 /* KeyboardPadding.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeyboardPadding.swift; sourceTree = ""; }; 5CEBD7472A5F115D00665FE2 /* SetDeliveryReceiptsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SetDeliveryReceiptsView.swift; sourceTree = ""; }; + 5CF077F62B0D60C000105111 /* libgmpxx.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libgmpxx.a; sourceTree = ""; }; + 5CF077F72B0D60C000105111 /* libHSsimplex-chat-5.4.0.5-AEaxUB19STC3bOtqr9BLL2.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-5.4.0.5-AEaxUB19STC3bOtqr9BLL2.a"; sourceTree = ""; }; + 5CF077F82B0D60C000105111 /* libgmp.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libgmp.a; sourceTree = ""; }; + 5CF077F92B0D60C100105111 /* libHSsimplex-chat-5.4.0.5-AEaxUB19STC3bOtqr9BLL2-ghc9.6.3.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-5.4.0.5-AEaxUB19STC3bOtqr9BLL2-ghc9.6.3.a"; sourceTree = ""; }; + 5CF077FA2B0D60C100105111 /* libffi.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libffi.a; sourceTree = ""; }; 5CFA59C32860BC6200863A68 /* MigrateToAppGroupView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MigrateToAppGroupView.swift; sourceTree = ""; }; 5CFA59CF286477B400863A68 /* ChatArchiveView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatArchiveView.swift; sourceTree = ""; }; 5CFE0920282EEAF60002594B /* ZoomableScrollView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = ZoomableScrollView.swift; path = Shared/Views/ZoomableScrollView.swift; sourceTree = SOURCE_ROOT; }; @@ -517,13 +521,13 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( - 5CDA5A302B04FE2D00A71D61 /* libgmpxx.a in Frameworks */, + 5CF077FB2B0D60C100105111 /* libgmpxx.a in Frameworks */, + 5CF077FD2B0D60C100105111 /* libgmp.a in Frameworks */, + 5CF077FE2B0D60C100105111 /* libHSsimplex-chat-5.4.0.5-AEaxUB19STC3bOtqr9BLL2-ghc9.6.3.a in Frameworks */, 5CE2BA93284534B000EC33A6 /* libiconv.tbd in Frameworks */, - 5CDA5A2D2B04FE2D00A71D61 /* libgmp.a in Frameworks */, - 5CDA5A2E2B04FE2D00A71D61 /* libHSsimplex-chat-5.4.0.3-rODxCBVsb2BkD1fnTAqXL-ghc9.6.3.a in Frameworks */, - 5CDA5A2F2B04FE2D00A71D61 /* libffi.a in Frameworks */, + 5CF077FF2B0D60C100105111 /* libffi.a in Frameworks */, + 5CF077FC2B0D60C100105111 /* libHSsimplex-chat-5.4.0.5-AEaxUB19STC3bOtqr9BLL2.a in Frameworks */, 5CE2BA94284534BB00EC33A6 /* libz.tbd in Frameworks */, - 5CDA5A312B04FE2D00A71D61 /* libHSsimplex-chat-5.4.0.3-rODxCBVsb2BkD1fnTAqXL.a in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -585,11 +589,11 @@ 5C764E5C279C70B7000C6508 /* Libraries */ = { isa = PBXGroup; children = ( - 5CDA5A2A2B04FE2D00A71D61 /* libffi.a */, - 5CDA5A282B04FE2D00A71D61 /* libgmp.a */, - 5CDA5A2B2B04FE2D00A71D61 /* libgmpxx.a */, - 5CDA5A292B04FE2D00A71D61 /* libHSsimplex-chat-5.4.0.3-rODxCBVsb2BkD1fnTAqXL-ghc9.6.3.a */, - 5CDA5A2C2B04FE2D00A71D61 /* libHSsimplex-chat-5.4.0.3-rODxCBVsb2BkD1fnTAqXL.a */, + 5CF077FA2B0D60C100105111 /* libffi.a */, + 5CF077F82B0D60C000105111 /* libgmp.a */, + 5CF077F62B0D60C000105111 /* libgmpxx.a */, + 5CF077F92B0D60C100105111 /* libHSsimplex-chat-5.4.0.5-AEaxUB19STC3bOtqr9BLL2-ghc9.6.3.a */, + 5CF077F72B0D60C000105111 /* libHSsimplex-chat-5.4.0.5-AEaxUB19STC3bOtqr9BLL2.a */, ); path = Libraries; sourceTree = ""; @@ -818,6 +822,8 @@ 5CE2BA8A2845332200EC33A6 /* SimpleX.h */, 5CE2BA78284530CC00EC33A6 /* SimpleXChat.docc */, 5CE2BA96284537A800EC33A6 /* dummy.m */, + 5CD67B8D2B0E858A00C510B1 /* hs_init.h */, + 5CD67B8E2B0E858A00C510B1 /* hs_init.c */, ); path = SimpleXChat; sourceTree = ""; @@ -902,6 +908,7 @@ buildActionMask = 2147483647; files = ( 5CE2BA77284530BF00EC33A6 /* SimpleXChat.h in Headers */, + 5CD67B8F2B0E858A00C510B1 /* hs_init.h in Headers */, 5CE2BA952845354B00EC33A6 /* SimpleX.h in Headers */, ); runOnlyForDeploymentPostprocessing = 0; @@ -1272,6 +1279,7 @@ 5C00168128C4FE760094D739 /* KeyChain.swift in Sources */, 5CE2BA97284537A800EC33A6 /* dummy.m in Sources */, 5CE2BA922845340900EC33A6 /* FileUtils.swift in Sources */, + 5CD67B902B0E858A00C510B1 /* hs_init.c in Sources */, 5CE2BA91284533A300EC33A6 /* Notifications.swift in Sources */, 5CE2BA79284530CC00EC33A6 /* SimpleXChat.docc in Sources */, 5CE2BA90284533A300EC33A6 /* JSON.swift in Sources */, @@ -1504,7 +1512,7 @@ CLANG_ENABLE_MODULES = YES; CODE_SIGN_ENTITLEMENTS = "SimpleX (iOS).entitlements"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 181; + CURRENT_PROJECT_VERSION = 182; DEVELOPMENT_TEAM = 5NN7GUYB6T; ENABLE_BITCODE = NO; ENABLE_PREVIEWS = YES; @@ -1547,7 +1555,7 @@ CLANG_ENABLE_MODULES = YES; CODE_SIGN_ENTITLEMENTS = "SimpleX (iOS).entitlements"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 181; + CURRENT_PROJECT_VERSION = 182; DEVELOPMENT_TEAM = 5NN7GUYB6T; ENABLE_BITCODE = NO; ENABLE_PREVIEWS = YES; @@ -1628,7 +1636,7 @@ CODE_SIGN_ENTITLEMENTS = "SimpleX NSE/SimpleX NSE.entitlements"; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 181; + CURRENT_PROJECT_VERSION = 182; DEVELOPMENT_TEAM = 5NN7GUYB6T; ENABLE_BITCODE = NO; GENERATE_INFOPLIST_FILE = YES; @@ -1660,7 +1668,7 @@ CODE_SIGN_ENTITLEMENTS = "SimpleX NSE/SimpleX NSE.entitlements"; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 181; + CURRENT_PROJECT_VERSION = 182; DEVELOPMENT_TEAM = 5NN7GUYB6T; ENABLE_BITCODE = NO; GENERATE_INFOPLIST_FILE = YES; @@ -1692,7 +1700,7 @@ APPLICATION_EXTENSION_API_ONLY = YES; CLANG_ENABLE_MODULES = YES; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 181; + CURRENT_PROJECT_VERSION = 182; DEFINES_MODULE = YES; DEVELOPMENT_TEAM = 5NN7GUYB6T; DYLIB_COMPATIBILITY_VERSION = 1; @@ -1738,7 +1746,7 @@ APPLICATION_EXTENSION_API_ONLY = YES; CLANG_ENABLE_MODULES = YES; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 181; + CURRENT_PROJECT_VERSION = 182; DEFINES_MODULE = YES; DEVELOPMENT_TEAM = 5NN7GUYB6T; DYLIB_COMPATIBILITY_VERSION = 1; diff --git a/apps/ios/SimpleXChat/ChatTypes.swift b/apps/ios/SimpleXChat/ChatTypes.swift index 551ed2794..dc4cdda46 100644 --- a/apps/ios/SimpleXChat/ChatTypes.swift +++ b/apps/ios/SimpleXChat/ChatTypes.swift @@ -1847,7 +1847,7 @@ public struct GroupMember: Identifiable, Decodable { public func canChangeRoleTo(groupInfo: GroupInfo) -> [GroupMemberRole]? { if !canBeRemoved(groupInfo: groupInfo) { return nil } let userRole = groupInfo.membership.memberRole - return GroupMemberRole.allCases.filter { $0 <= userRole } + return GroupMemberRole.allCases.filter { $0 <= userRole && $0 != .author } } public var memberIncognito: Bool { @@ -1887,6 +1887,7 @@ public struct GroupMemberIds: Decodable { public enum GroupMemberRole: String, Identifiable, CaseIterable, Comparable, Decodable { case observer = "observer" + case author = "author" case member = "member" case admin = "admin" case owner = "owner" @@ -1896,6 +1897,7 @@ public enum GroupMemberRole: String, Identifiable, CaseIterable, Comparable, Dec public var text: String { switch self { case .observer: return NSLocalizedString("observer", comment: "member role") + case .author: return NSLocalizedString("author", comment: "member role") case .member: return NSLocalizedString("member", comment: "member role") case .admin: return NSLocalizedString("admin", comment: "member role") case .owner: return NSLocalizedString("owner", comment: "member role") @@ -1905,9 +1907,10 @@ public enum GroupMemberRole: String, Identifiable, CaseIterable, Comparable, Dec private var comparisonValue: Int { switch self { case .observer: return 0 - case .member: return 1 - case .admin: return 2 - case .owner: return 3 + case .author: return 1 + case .member: return 2 + case .admin: return 3 + case .owner: return 4 } } diff --git a/apps/ios/SimpleXChat/SimpleX.h b/apps/ios/SimpleXChat/SimpleX.h index 250f1cb73..2872922a9 100644 --- a/apps/ios/SimpleXChat/SimpleX.h +++ b/apps/ios/SimpleXChat/SimpleX.h @@ -9,7 +9,7 @@ #ifndef SimpleX_h #define SimpleX_h -#endif /* SimpleX_h */ +#include "hs_init.h" extern void hs_init(int argc, char **argv[]); @@ -42,3 +42,5 @@ extern char *chat_encrypt_file(char *fromPath, char *toPath); // chat_decrypt_file returns null-terminated string with the error message extern char *chat_decrypt_file(char *fromPath, char *key, char *nonce, char *toPath); + +#endif /* SimpleX_h */ diff --git a/apps/ios/SimpleXChat/hs_init.c b/apps/ios/SimpleXChat/hs_init.c new file mode 100644 index 000000000..7a5ea2456 --- /dev/null +++ b/apps/ios/SimpleXChat/hs_init.c @@ -0,0 +1,25 @@ +// +// hs_init.c +// SimpleXChat +// +// Created by Evgeny on 22/11/2023. +// Copyright © 2023 SimpleX Chat. All rights reserved. +// + +#include "hs_init.h" + +extern void hs_init_with_rtsopts(int * argc, char **argv[]); + +void haskell_init(void) { + int argc = 5; + char *argv[] = { + "simplex", + "+RTS", // requires `hs_init_with_rtsopts` + "-A16m", // chunk size for new allocations + "-H64m", // initial heap size + "-xn", // non-moving GC + 0 + }; + char **pargv = argv; + hs_init_with_rtsopts(&argc, &pargv); +} diff --git a/apps/ios/SimpleXChat/hs_init.h b/apps/ios/SimpleXChat/hs_init.h new file mode 100644 index 000000000..48850e819 --- /dev/null +++ b/apps/ios/SimpleXChat/hs_init.h @@ -0,0 +1,14 @@ +// +// hs_init.h +// SimpleXChat +// +// Created by Evgeny on 22/11/2023. +// Copyright © 2023 SimpleX Chat. All rights reserved. +// + +#ifndef hs_init_h +#define hs_init_h + +void haskell_init(void); + +#endif /* hs_init_h */ diff --git a/apps/multiplatform/android/src/main/java/chat/simplex/app/MainActivity.kt b/apps/multiplatform/android/src/main/java/chat/simplex/app/MainActivity.kt index 4c5d595a8..a84590fb8 100644 --- a/apps/multiplatform/android/src/main/java/chat/simplex/app/MainActivity.kt +++ b/apps/multiplatform/android/src/main/java/chat/simplex/app/MainActivity.kt @@ -126,7 +126,7 @@ fun processIntent(intent: Intent?) { when (intent?.action) { "android.intent.action.VIEW" -> { val uri = intent.data - if (uri != null) connectIfOpenedViaUri(chatModel.remoteHostId, uri.toURI(), ChatModel) + if (uri != null) connectIfOpenedViaUri(chatModel.remoteHostId(), uri.toURI(), ChatModel) } } } diff --git a/apps/multiplatform/android/src/main/java/chat/simplex/app/SimplexApp.kt b/apps/multiplatform/android/src/main/java/chat/simplex/app/SimplexApp.kt index 13908f69b..b6afab4ea 100644 --- a/apps/multiplatform/android/src/main/java/chat/simplex/app/SimplexApp.kt +++ b/apps/multiplatform/android/src/main/java/chat/simplex/app/SimplexApp.kt @@ -57,7 +57,7 @@ class SimplexApp: Application(), LifecycleEventObserver { updatingChatsMutex.withLock { kotlin.runCatching { val currentUserId = chatModel.currentUser.value?.userId - val chats = ArrayList(chatController.apiGetChats(chatModel.remoteHostId)) + val chats = ArrayList(chatController.apiGetChats(chatModel.remoteHostId())) /** Active user can be changed in background while [ChatController.apiGetChats] is executing */ if (chatModel.currentUser.value?.userId == currentUserId) { val currentChatId = chatModel.chatId.value diff --git a/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/platform/Share.android.kt b/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/platform/Share.android.kt index eb6ed0bbf..f0c5ea694 100644 --- a/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/platform/Share.android.kt +++ b/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/platform/Share.android.kt @@ -8,15 +8,12 @@ import android.provider.MediaStore import android.webkit.MimeTypeMap import androidx.compose.ui.platform.ClipboardManager import androidx.compose.ui.platform.UriHandler -import androidx.core.content.FileProvider -import androidx.core.net.toUri import chat.simplex.common.helpers.* import chat.simplex.common.model.* import chat.simplex.common.views.helpers.* import java.io.BufferedOutputStream import java.io.File import chat.simplex.res.MR -import java.io.ByteArrayOutputStream actual fun ClipboardManager.shareText(text: String) { val sendIntent: Intent = Intent().apply { diff --git a/apps/multiplatform/common/src/commonMain/cpp/android/simplex-api.c b/apps/multiplatform/common/src/commonMain/cpp/android/simplex-api.c index b729e3b7f..54478425f 100644 --- a/apps/multiplatform/common/src/commonMain/cpp/android/simplex-api.c +++ b/apps/multiplatform/common/src/commonMain/cpp/android/simplex-api.c @@ -5,7 +5,7 @@ //#include // from the RTS -void hs_init(int * argc, char **argv[]); +void hs_init_with_rtsopts(int * argc, char **argv[]); // from android-support void setLineBuffering(void); @@ -32,7 +32,17 @@ Java_chat_simplex_common_platform_CoreKt_pipeStdOutToSocket(JNIEnv *env, __unuse JNIEXPORT void JNICALL Java_chat_simplex_common_platform_CoreKt_initHS(__unused JNIEnv *env, __unused jclass clazz) { - hs_init(NULL, NULL); + int argc = 5; + char *argv[] = { + "simplex", + "+RTS", // requires `hs_init_with_rtsopts` + "-A16m", // chunk size for new allocations + "-H64m", // initial heap size + "-xn", // non-moving GC + NULL + }; + char **pargv = argv; + hs_init_with_rtsopts(&argc, &pargv); setLineBuffering(); } diff --git a/apps/multiplatform/common/src/commonMain/cpp/desktop/simplex-api.c b/apps/multiplatform/common/src/commonMain/cpp/desktop/simplex-api.c index 1b0d11a2b..e2cd7ed55 100644 --- a/apps/multiplatform/common/src/commonMain/cpp/desktop/simplex-api.c +++ b/apps/multiplatform/common/src/commonMain/cpp/desktop/simplex-api.c @@ -4,11 +4,14 @@ #include // from the RTS -void hs_init(int * argc, char **argv[]); +void hs_init_with_rtsopts(int * argc, char **argv[]); JNIEXPORT void JNICALL Java_chat_simplex_common_platform_CoreKt_initHS(JNIEnv *env, jclass clazz) { - hs_init(NULL, NULL); + int argc = 5; + char *argv[] = {"simplex", "+RTS", "-A16m", "-H64m", "-xn", NULL}; // see android/simplex-api.c for details + char **pargv = argv; + hs_init_with_rtsopts(&argc, &pargv); } // from simplex-chat diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/ChatModel.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/ChatModel.kt index 660e7de15..7541b7f34 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/ChatModel.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/ChatModel.kt @@ -111,7 +111,8 @@ object ChatModel { // remote controller val remoteHosts = mutableStateListOf() val currentRemoteHost = mutableStateOf(null) - val remoteHostId: Long? get() = currentRemoteHost?.value?.remoteHostId + val remoteHostId: Long? @Composable get() = remember { currentRemoteHost }.value?.remoteHostId + fun remoteHostId(): Long? = currentRemoteHost.value?.remoteHostId val newRemoteHostPairing = mutableStateOf?>(null) val remoteCtrlSession = mutableStateOf(null) @@ -1252,7 +1253,7 @@ data class GroupMember ( fun canChangeRoleTo(groupInfo: GroupInfo): List? = if (!canBeRemoved(groupInfo)) null else groupInfo.membership.memberRole.let { userRole -> - GroupMemberRole.values().filter { it <= userRole } + GroupMemberRole.values().filter { it <= userRole && it != GroupMemberRole.Author } } val memberIncognito = memberProfile.profileId != memberContactProfileId @@ -1294,12 +1295,14 @@ data class GroupMemberIds( @Serializable enum class GroupMemberRole(val memberRole: String) { @SerialName("observer") Observer("observer"), // order matters in comparisons + @SerialName("author") Author("author"), @SerialName("member") Member("member"), @SerialName("admin") Admin("admin"), @SerialName("owner") Owner("owner"); val text: String get() = when (this) { Observer -> generalGetString(MR.strings.group_member_role_observer) + Author -> generalGetString(MR.strings.group_member_role_author) Member -> generalGetString(MR.strings.group_member_role_member) Admin -> generalGetString(MR.strings.group_member_role_admin) Owner -> generalGetString(MR.strings.group_member_role_owner) diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt index 9298388df..bfcec69da 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt @@ -4,7 +4,6 @@ import chat.simplex.common.views.helpers.* import androidx.compose.runtime.* import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.painter.Painter -import chat.simplex.common.model.ChatModel.remoteHostId import chat.simplex.common.model.ChatModel.updatingChatsMutex import dev.icerock.moko.resources.compose.painterResource import chat.simplex.common.platform.* @@ -1393,10 +1392,10 @@ object ChatController { chatModel.remoteHosts.addAll(hosts) } - suspend fun startRemoteHost(rhId: Long?, multicast: Boolean = false): Pair? { + suspend fun startRemoteHost(rhId: Long?, multicast: Boolean = false): Triple? { val r = sendCmd(null, CC.StartRemoteHost(rhId, multicast)) - if (r is CR.RemoteHostStarted) return r.remoteHost_ to r.invitation - apiErrorAlert("listRemoteHosts", generalGetString(MR.strings.error_alert_title), r) + if (r is CR.RemoteHostStarted) return Triple(r.remoteHost_, r.invitation, r.ctrlPort) + apiErrorAlert("startRemoteHost", generalGetString(MR.strings.error_alert_title), r) return null } @@ -1626,7 +1625,7 @@ object ChatController { || (mc is MsgContent.MCVoice && file.fileSize <= MAX_VOICE_SIZE_AUTO_RCV && file.fileStatus !is CIFileStatus.RcvAccepted))) { withApi { receiveFile(rhId, r.user, file.fileId, encrypted = cItem.encryptLocalFile && chatController.appPrefs.privacyEncryptLocalFiles.get(), auto = true) } } - if (cItem.showNotification && (allowedToShowNotification() || chatModel.chatId.value != cInfo.id || chatModel.remoteHostId != rhId)) { + if (cItem.showNotification && (allowedToShowNotification() || chatModel.chatId.value != cInfo.id || chatModel.remoteHostId() != rhId)) { ntfManager.notifyMessageReceived(r.user, cInfo, cItem) } } @@ -1845,8 +1844,14 @@ object ChatController { switchUIRemoteHost(r.remoteHost.remoteHostId) } is CR.RemoteHostStopped -> { + val disconnectedHost = chatModel.remoteHosts.firstOrNull { it.remoteHostId == r.remoteHostId_ } chatModel.newRemoteHostPairing.value = null - if (chatModel.currentRemoteHost.value != null) { + if (disconnectedHost != null) { + showToast( + generalGetString(MR.strings.remote_host_was_disconnected_toast).format(disconnectedHost.hostDeviceName.ifEmpty { disconnectedHost.remoteHostId.toString() }) + ) + } + if (chatModel.remoteHostId() == r.remoteHostId_) { chatModel.currentRemoteHost.value = null switchUIRemoteHost(null) } @@ -1908,7 +1913,7 @@ object ChatController { } private fun activeUser(rhId: Long?, user: UserLike): Boolean = - rhId == chatModel.remoteHostId && user.userId == chatModel.currentUser.value?.userId + rhId == chatModel.remoteHostId() && user.userId == chatModel.currentUser.value?.userId private fun withCall(r: CR, contact: Contact, perform: (Call) -> Unit) { val call = chatModel.activeCall.value @@ -1968,6 +1973,9 @@ object ChatController { suspend fun switchUIRemoteHost(rhId: Long?) { // TODO lock the switch so that two switches can't run concurrently? chatModel.chatId.value = null + ModalManager.center.closeModals() + ModalManager.end.closeModals() + AlertManager.shared.alertViews.clear() chatModel.currentRemoteHost.value = switchRemoteHost(rhId) reloadRemoteHosts() val user = apiGetActiveUser(rhId) @@ -3766,7 +3774,7 @@ sealed class CR { // remote events (desktop) @Serializable @SerialName("remoteHostList") class RemoteHostList(val remoteHosts: List): CR() @Serializable @SerialName("currentRemoteHost") class CurrentRemoteHost(val remoteHost_: RemoteHostInfo?): CR() - @Serializable @SerialName("remoteHostStarted") class RemoteHostStarted(val remoteHost_: RemoteHostInfo?, val invitation: String): CR() + @Serializable @SerialName("remoteHostStarted") class RemoteHostStarted(val remoteHost_: RemoteHostInfo?, val invitation: String, val ctrlPort: String): CR() @Serializable @SerialName("remoteHostSessionCode") class RemoteHostSessionCode(val remoteHost_: RemoteHostInfo?, val sessionCode: String): CR() @Serializable @SerialName("newRemoteHost") class NewRemoteHost(val remoteHost: RemoteHostInfo): CR() @Serializable @SerialName("remoteHostConnected") class RemoteHostConnected(val remoteHost: RemoteHostInfo): CR() diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/TerminalView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/TerminalView.kt index ec2082557..59f62f9c9 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/TerminalView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/TerminalView.kt @@ -54,7 +54,7 @@ private fun sendCommand(chatModel: ChatModel, composeState: MutableState Unit) { fun createProfileInProfiles(chatModel: ChatModel, displayName: String, close: () -> Unit) { withApi { - val rhId = chatModel.remoteHostId + val rhId = chatModel.remoteHostId() val user = chatModel.controller.apiCreateActiveUser( rhId, Profile(displayName.trim(), "", null) ) ?: return@withApi diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ScanCodeView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ScanCodeView.kt index 8ce39eea3..bb479d8eb 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ScanCodeView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ScanCodeView.kt @@ -17,7 +17,7 @@ fun ScanCodeView(verifyCode: (String?, cb: (Boolean) -> Unit) -> Unit, close: () .fillMaxSize() .padding(horizontal = DEFAULT_PADDING) ) { - AppBarTitle(stringResource(MR.strings.scan_code), false) + AppBarTitle(stringResource(MR.strings.scan_code), withPadding = false) Box( Modifier .fillMaxWidth() diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/VerifyCodeView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/VerifyCodeView.kt index 1b25c52cf..e1840dd88 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/VerifyCodeView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/VerifyCodeView.kt @@ -63,7 +63,7 @@ private fun VerifyCodeLayout( .verticalScroll(rememberScrollState()) .padding(horizontal = DEFAULT_PADDING) ) { - AppBarTitle(stringResource(MR.strings.security_code), false) + AppBarTitle(stringResource(MR.strings.security_code), withPadding = false) val splitCode = splitToParts(connectionCode, 24) Row(Modifier.fillMaxWidth().padding(bottom = DEFAULT_PADDING_HALF), horizontalArrangement = Arrangement.Center) { if (connectionVerified) { diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/AddGroupMembersView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/AddGroupMembersView.kt index ff23d40b8..37ee9729f 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/AddGroupMembersView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/AddGroupMembersView.kt @@ -205,7 +205,9 @@ private fun RoleSelectionRow(groupInfo: GroupInfo, selectedRole: MutableState? = null) { when (groupInfo.membership.memberStatus) { GroupMemberStatus.MemInvited -> acceptGroupInvitationAlertDialog(rhId, groupInfo, chatModel, inProgress) - GroupMemberStatus.MemAccepted -> groupInvitationAcceptedAlert() + GroupMemberStatus.MemAccepted -> groupInvitationAcceptedAlert(rhId) else -> withBGApi { openChat(rhId, ChatInfo.Group(groupInfo), chatModel) } } } @@ -538,7 +538,8 @@ fun contactRequestAlertDialog(rhId: Long?, contactRequest: ChatInfo.ContactReque Text(generalGetString(MR.strings.reject_contact_button), Modifier.fillMaxWidth(), textAlign = TextAlign.Center, color = Color.Red) } } - } + }, + hostDevice = hostDevice(rhId), ) } @@ -644,7 +645,8 @@ fun askCurrentOrIncognitoProfileConnectContactViaAddress( Text(stringResource(MR.strings.cancel_verb), Modifier.fillMaxWidth(), textAlign = TextAlign.Center, color = MaterialTheme.colors.primary) } } - } + }, + hostDevice = hostDevice(rhId), ) } @@ -654,7 +656,8 @@ suspend fun connectContactViaAddress(chatModel: ChatModel, rhId: Long?, contactI chatModel.updateContact(rhId, contact) AlertManager.shared.showAlertMsg( title = generalGetString(MR.strings.connection_request_sent), - text = generalGetString(MR.strings.you_will_be_connected_when_your_connection_request_is_accepted) + text = generalGetString(MR.strings.you_will_be_connected_when_your_connection_request_is_accepted), + hostDevice = hostDevice(rhId), ) return true } @@ -674,7 +677,8 @@ fun acceptGroupInvitationAlertDialog(rhId: Long?, groupInfo: GroupInfo, chatMode } }, dismissText = generalGetString(MR.strings.delete_verb), - onDismiss = { deleteGroup(rhId, groupInfo, chatModel) } + onDismiss = { deleteGroup(rhId, groupInfo, chatModel) }, + hostDevice = hostDevice(rhId), ) } @@ -700,10 +704,11 @@ fun deleteGroup(rhId: Long?, groupInfo: GroupInfo, chatModel: ChatModel) { } } -fun groupInvitationAcceptedAlert() { +fun groupInvitationAcceptedAlert(rhId: Long?) { AlertManager.shared.showAlertMsg( generalGetString(MR.strings.joining_group), - generalGetString(MR.strings.youve_accepted_group_invitation_connecting_to_inviting_group_member) + generalGetString(MR.strings.youve_accepted_group_invitation_connecting_to_inviting_group_member), + hostDevice = hostDevice(rhId), ) } diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ChatListView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ChatListView.kt index f9502bf90..301f09033 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ChatListView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ChatListView.kt @@ -53,7 +53,7 @@ fun ChatListView(chatModel: ChatModel, settingsState: SettingsViewState, setPerf val url = chatModel.appOpenUrl.value if (url != null) { chatModel.appOpenUrl.value = null - connectIfOpenedViaUri(chatModel.remoteHostId, url, chatModel) + connectIfOpenedViaUri(chatModel.remoteHostId(), url, chatModel) } } if (appPlatform.isDesktop) { diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/UserPicker.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/UserPicker.kt index 20e856df6..46a333f61 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/UserPicker.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/UserPicker.kt @@ -26,8 +26,7 @@ import chat.simplex.common.model.ChatModel.controller import chat.simplex.common.ui.theme.* import chat.simplex.common.views.helpers.* import chat.simplex.common.platform.* -import chat.simplex.common.views.remote.ConnectDesktopView -import chat.simplex.common.views.remote.connectMobileDevice +import chat.simplex.common.views.remote.* import chat.simplex.res.MR import dev.icerock.moko.resources.compose.stringResource import kotlinx.coroutines.delay @@ -84,7 +83,7 @@ fun UserPicker( .filter { it } .collect { try { - val updatedUsers = chatModel.controller.listUsers(chatModel.remoteHostId).sortedByDescending { it.user.activeUser } + val updatedUsers = chatModel.controller.listUsers(chatModel.remoteHostId()).sortedByDescending { it.user.activeUser } var same = users.size == updatedUsers.size if (same) { for (i in 0 until minOf(users.size, updatedUsers.size)) { @@ -213,6 +212,14 @@ fun UserPicker( userPickerState.value = AnimatedViewState.GONE } Divider(Modifier.requiredHeight(1.dp)) + } else if (remoteHosts.isEmpty()) { + LinkAMobilePickerItem { + ModalManager.start.showModal { + ConnectMobileView() + } + userPickerState.value = AnimatedViewState.GONE + } + Divider(Modifier.requiredHeight(1.dp)) } if (showSettings) { SettingsPickerItem(settingsClicked) @@ -384,6 +391,16 @@ private fun UseFromDesktopPickerItem(onClick: () -> Unit) { } } +@Composable +private fun LinkAMobilePickerItem(onClick: () -> Unit) { + SectionItemView(onClick, padding = PaddingValues(start = DEFAULT_PADDING + 7.dp, end = DEFAULT_PADDING), minHeight = 68.dp) { + val text = generalGetString(MR.strings.link_a_mobile) + Icon(painterResource(MR.images.ic_smartphone_300), text, Modifier.size(20.dp), tint = MaterialTheme.colors.onBackground) + Spacer(Modifier.width(DEFAULT_PADDING + 6.dp)) + Text(text, color = if (isInDarkTheme()) MenuTextColorDark else Color.Black) + } +} + @Composable private fun SettingsPickerItem(onClick: () -> Unit) { SectionItemView(onClick, padding = PaddingValues(start = DEFAULT_PADDING + 7.dp, end = DEFAULT_PADDING), minHeight = 68.dp) { diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/database/DatabaseView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/database/DatabaseView.kt index 1f4be2966..bc43310b8 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/database/DatabaseView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/database/DatabaseView.kt @@ -627,7 +627,7 @@ private fun afterSetCiTTL( try { updatingChatsMutex.withLock { // this is using current remote host on purpose - if it changes during update, it will load correct chats - val chats = m.controller.apiGetChats(m.remoteHostId) + val chats = m.controller.apiGetChats(m.remoteHostId()) m.updateChats(chats) } } catch (e: Exception) { diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/AlertManager.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/AlertManager.kt index fa9c89384..10bbae6bf 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/AlertManager.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/AlertManager.kt @@ -1,6 +1,5 @@ package chat.simplex.common.views.helpers -import androidx.compose.foundation.background import androidx.compose.foundation.layout.* import androidx.compose.foundation.shape.CornerSize import androidx.compose.foundation.shape.RoundedCornerShape @@ -14,10 +13,12 @@ import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.text.AnnotatedString import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.* +import chat.simplex.common.model.ChatModel import chat.simplex.common.platform.* import chat.simplex.common.ui.theme.* import chat.simplex.res.MR import dev.icerock.moko.resources.StringResource +import dev.icerock.moko.resources.compose.painterResource class AlertManager { var alertViews = mutableStateListOf<(@Composable () -> Unit)>() @@ -40,8 +41,11 @@ class AlertManager { AlertDialog( onDismissRequest = this::hideAlert, title = alertTitle(title), - text = alertText(text), - buttons = buttons, + buttons = { + AlertContent(text, null, extraPadding = true) { + buttons() + } + }, shape = RoundedCornerShape(corner = CornerSize(25.dp)) ) } @@ -51,30 +55,16 @@ class AlertManager { title: String, text: AnnotatedString? = null, onDismissRequest: (() -> Unit)? = null, + hostDevice: Pair? = null, buttons: @Composable () -> Unit, ) { showAlert { AlertDialog( onDismissRequest = { onDismissRequest?.invoke(); hideAlert() }, - title = { - Text( - title, - Modifier.fillMaxWidth().padding(vertical = DEFAULT_PADDING), - textAlign = TextAlign.Center, - fontSize = 20.sp - ) - }, + title = alertTitle(title), buttons = { - Column( - Modifier - .padding(bottom = DEFAULT_PADDING) - ) { - CompositionLocalProvider(LocalContentAlpha provides ContentAlpha.high) { - if (text != null) { - Text(text, Modifier.fillMaxWidth().padding(start = DEFAULT_PADDING, end = DEFAULT_PADDING, bottom = DEFAULT_PADDING * 1.5f), fontSize = 16.sp, textAlign = TextAlign.Center, color = MaterialTheme.colors.secondary) - } - buttons() - } + AlertContent(text, hostDevice, extraPadding = true) { + buttons() } }, shape = RoundedCornerShape(corner = CornerSize(25.dp)) @@ -90,30 +80,32 @@ class AlertManager { dismissText: String = generalGetString(MR.strings.cancel_verb), onDismiss: (() -> Unit)? = null, onDismissRequest: (() -> Unit)? = null, - destructive: Boolean = false + destructive: Boolean = false, + hostDevice: Pair? = null, ) { showAlert { AlertDialog( onDismissRequest = { onDismissRequest?.invoke(); hideAlert() }, title = alertTitle(title), - text = alertText(text), buttons = { - Row ( - Modifier.fillMaxWidth().padding(horizontal = DEFAULT_PADDING).padding(bottom = DEFAULT_PADDING_HALF), - horizontalArrangement = Arrangement.SpaceBetween - ) { - val focusRequester = remember { FocusRequester() } - LaunchedEffect(Unit) { - focusRequester.requestFocus() + AlertContent(text, hostDevice, true) { + Row( + Modifier.fillMaxWidth().padding(horizontal = DEFAULT_PADDING), + horizontalArrangement = Arrangement.SpaceBetween + ) { + val focusRequester = remember { FocusRequester() } + LaunchedEffect(Unit) { + focusRequester.requestFocus() + } + TextButton(onClick = { + onDismiss?.invoke() + hideAlert() + }) { Text(dismissText) } + TextButton(onClick = { + onConfirm?.invoke() + hideAlert() + }, Modifier.focusRequester(focusRequester)) { Text(confirmText, color = if (destructive) MaterialTheme.colors.error else Color.Unspecified) } } - TextButton(onClick = { - onDismiss?.invoke() - hideAlert() - }) { Text(dismissText) } - TextButton(onClick = { - onConfirm?.invoke() - hideAlert() - }, Modifier.focusRequester(focusRequester)) { Text(confirmText, color = if (destructive) MaterialTheme.colors.error else Color.Unspecified) } } }, shape = RoundedCornerShape(corner = CornerSize(25.dp)) @@ -135,20 +127,21 @@ class AlertManager { AlertDialog( onDismissRequest = { onDismissRequest?.invoke(); hideAlert() }, title = alertTitle(title), - text = alertText(text), buttons = { - Column( - Modifier.fillMaxWidth().padding(horizontal = DEFAULT_PADDING_HALF).padding(top = DEFAULT_PADDING, bottom = 2.dp), - horizontalAlignment = Alignment.CenterHorizontally - ) { - TextButton(onClick = { - onDismiss?.invoke() - hideAlert() - }) { Text(dismissText) } - TextButton(onClick = { - onConfirm?.invoke() - hideAlert() - }) { Text(confirmText, color = if (destructive) Color.Red else Color.Unspecified, textAlign = TextAlign.End) } + AlertContent(text, null) { + Column( + Modifier.fillMaxWidth().padding(horizontal = DEFAULT_PADDING_HALF).padding(top = DEFAULT_PADDING, bottom = 2.dp), + horizontalAlignment = Alignment.CenterHorizontally + ) { + TextButton(onClick = { + onDismiss?.invoke() + hideAlert() + }) { Text(dismissText) } + TextButton(onClick = { + onConfirm?.invoke() + hideAlert() + }) { Text(confirmText, color = if (destructive) Color.Red else Color.Unspecified, textAlign = TextAlign.End) } + } } }, shape = RoundedCornerShape(corner = CornerSize(25.dp)) @@ -158,29 +151,31 @@ class AlertManager { fun showAlertMsg( title: String, text: String? = null, - confirmText: String = generalGetString(MR.strings.ok) + confirmText: String = generalGetString(MR.strings.ok), + hostDevice: Pair? = null, ) { showAlert { AlertDialog( onDismissRequest = this::hideAlert, title = alertTitle(title), - text = alertText(text), buttons = { - val focusRequester = remember { FocusRequester() } - LaunchedEffect(Unit) { - focusRequester.requestFocus() - } - Row( - Modifier.fillMaxWidth().padding(horizontal = DEFAULT_PADDING).padding(bottom = DEFAULT_PADDING_HALF), - horizontalArrangement = Arrangement.Center - ) { - TextButton( - onClick = { - hideAlert() - }, - Modifier.focusRequester(focusRequester) + AlertContent(text, hostDevice, extraPadding = true) { + val focusRequester = remember { FocusRequester() } + LaunchedEffect(Unit) { + focusRequester.requestFocus() + } + Row( + Modifier.fillMaxWidth().padding(horizontal = DEFAULT_PADDING), + horizontalArrangement = Arrangement.Center ) { - Text(confirmText, color = Color.Unspecified) + TextButton( + onClick = { + hideAlert() + }, + Modifier.focusRequester(focusRequester) + ) { + Text(confirmText, color = Color.Unspecified) + } } } }, @@ -191,16 +186,17 @@ class AlertManager { fun showAlertMsgWithProgress( title: String, - text: String? = null + text: String? = null, ) { showAlert { AlertDialog( onDismissRequest = this::hideAlert, title = alertTitle(title), - text = alertText(text), buttons = { - Box(Modifier.fillMaxWidth().height(72.dp).padding(bottom = DEFAULT_PADDING * 2), contentAlignment = Alignment.Center) { - CircularProgressIndicator(Modifier.size(36.dp).padding(4.dp), color = MaterialTheme.colors.secondary, strokeWidth = 3.dp) + AlertContent(text, null) { + Box(Modifier.fillMaxWidth().height(72.dp).padding(bottom = DEFAULT_PADDING * 2), contentAlignment = Alignment.Center) { + CircularProgressIndicator(Modifier.size(36.dp).padding(4.dp), color = MaterialTheme.colors.secondary, strokeWidth = 3.dp) + } } } ) @@ -211,7 +207,8 @@ class AlertManager { title: StringResource, text: StringResource? = null, confirmText: StringResource = MR.strings.ok, - ) = showAlertMsg(generalGetString(title), if (text != null) generalGetString(text) else null, generalGetString(confirmText)) + hostDevice: Pair? = null, + ) = showAlertMsg(generalGetString(title), if (text != null) generalGetString(text) else null, generalGetString(confirmText), hostDevice) @Composable fun showInView() { @@ -234,18 +231,75 @@ private fun alertTitle(title: String): (@Composable () -> Unit)? { } } -private fun alertText(text: String?): (@Composable () -> Unit)? { - return if (text == null) { - null - } else { - ({ - Text( - escapedHtmlToAnnotatedString(text, LocalDensity.current), - Modifier.fillMaxWidth(), - textAlign = TextAlign.Center, - fontSize = 16.sp, - color = MaterialTheme.colors.secondary - ) - }) +@Composable +private fun AlertContent(text: String?, hostDevice: Pair?, extraPadding: Boolean = false, content: @Composable (() -> Unit)) { + Column( + Modifier + .padding(bottom = if (appPlatform.isDesktop) DEFAULT_PADDING else DEFAULT_PADDING_HALF) + ) { + if (appPlatform.isDesktop) { + HostDeviceTitle(hostDevice, extraPadding = extraPadding) + } else { + Spacer(Modifier.size(DEFAULT_PADDING_HALF)) + } + CompositionLocalProvider(LocalContentAlpha provides ContentAlpha.high) { + if (text != null) { + Text( + escapedHtmlToAnnotatedString(text, LocalDensity.current), + Modifier.fillMaxWidth().padding(start = DEFAULT_PADDING, end = DEFAULT_PADDING, bottom = DEFAULT_PADDING * 1.5f), + fontSize = 16.sp, + textAlign = TextAlign.Center, + color = MaterialTheme.colors.secondary + ) + } + } + content() + } +} + +@Composable +private fun AlertContent(text: AnnotatedString?, hostDevice: Pair?, extraPadding: Boolean = false, content: @Composable (() -> Unit)) { + Column( + Modifier + .padding(bottom = if (appPlatform.isDesktop) DEFAULT_PADDING else DEFAULT_PADDING_HALF) + ) { + if (appPlatform.isDesktop) { + HostDeviceTitle(hostDevice, extraPadding = extraPadding) + } else { + Spacer(Modifier.size(DEFAULT_PADDING_HALF)) + } + CompositionLocalProvider(LocalContentAlpha provides ContentAlpha.high) { + if (text != null) { + Text( + text, + Modifier.fillMaxWidth().padding(start = DEFAULT_PADDING, end = DEFAULT_PADDING, bottom = DEFAULT_PADDING * 1.5f), + fontSize = 16.sp, + textAlign = TextAlign.Center, + color = MaterialTheme.colors.secondary + ) + } + } + content() + } +} + +fun hostDevice(rhId: Long?): Pair? = if (rhId == null && chatModel.remoteHosts.isNotEmpty()) { + null to ChatModel.controller.appPrefs.deviceNameForRemoteAccess.get()!! +} else if (rhId == null) { + null +} else { + rhId to (chatModel.remoteHosts.firstOrNull { it.remoteHostId == rhId }?.hostDeviceName?.ifEmpty { rhId.toString() } ?: rhId.toString()) +} + +@Composable +private fun HostDeviceTitle(hostDevice: Pair?, extraPadding: Boolean = false) { + if (hostDevice != null) { + Row(Modifier.fillMaxWidth().padding(top = 5.dp, bottom = if (extraPadding) DEFAULT_PADDING * 2 else DEFAULT_PADDING_HALF), verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.Center) { + Icon(painterResource(if (hostDevice.first == null) MR.images.ic_desktop else MR.images.ic_smartphone_300), null, Modifier.size(15.dp), tint = MaterialTheme.colors.secondary) + Spacer(Modifier.width(10.dp)) + Text(hostDevice.second, color = MaterialTheme.colors.secondary) + } + } else { + Spacer(Modifier.height(if (extraPadding) DEFAULT_PADDING * 2 else 0.dp)) } } diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/CloseSheetBar.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/CloseSheetBar.kt index 848b21eb4..1a29a334a 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/CloseSheetBar.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/CloseSheetBar.kt @@ -14,6 +14,8 @@ import androidx.compose.desktop.ui.tooling.preview.Preview import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp import chat.simplex.common.ui.theme.* +import chat.simplex.res.MR +import dev.icerock.moko.resources.compose.painterResource @Composable fun CloseSheetBar(close: (() -> Unit)?, showClose: Boolean = true, endButtons: @Composable RowScope.() -> Unit = {}) { @@ -47,23 +49,38 @@ fun CloseSheetBar(close: (() -> Unit)?, showClose: Boolean = true, endButtons: @ } @Composable -fun AppBarTitle(title: String, withPadding: Boolean = true, bottomPadding: Dp = DEFAULT_PADDING * 1.5f) { +fun AppBarTitle(title: String, hostDevice: Pair? = null, withPadding: Boolean = true, bottomPadding: Dp = DEFAULT_PADDING * 1.5f) { val theme = CurrentColors.collectAsState() val titleColor = CurrentColors.collectAsState().value.appColors.title val brush = if (theme.value.base == DefaultTheme.SIMPLEX) Brush.linearGradient(listOf(titleColor.darker(0.2f), titleColor.lighter(0.35f)), Offset(0f, Float.POSITIVE_INFINITY), Offset(Float.POSITIVE_INFINITY, 0f)) else // color is not updated when changing themes if I pass null here Brush.linearGradient(listOf(titleColor, titleColor), Offset(0f, Float.POSITIVE_INFINITY), Offset(Float.POSITIVE_INFINITY, 0f)) - Text( - title, - Modifier - .fillMaxWidth() - .padding(bottom = bottomPadding, start = if (withPadding) DEFAULT_PADDING else 0.dp, end = if (withPadding) DEFAULT_PADDING else 0.dp,), - overflow = TextOverflow.Ellipsis, - style = MaterialTheme.typography.h1.copy(brush = brush), - color = MaterialTheme.colors.primaryVariant, - textAlign = TextAlign.Center - ) + Column { + Text( + title, + Modifier + .fillMaxWidth() + .padding(start = if (withPadding) DEFAULT_PADDING else 0.dp, end = if (withPadding) DEFAULT_PADDING else 0.dp,), + overflow = TextOverflow.Ellipsis, + style = MaterialTheme.typography.h1.copy(brush = brush), + color = MaterialTheme.colors.primaryVariant, + textAlign = TextAlign.Center + ) + if (hostDevice != null) { + HostDeviceTitle(hostDevice) + } + Spacer(Modifier.height(bottomPadding)) + } +} + +@Composable +private fun HostDeviceTitle(hostDevice: Pair, extraPadding: Boolean = false) { + Row(Modifier.fillMaxWidth().padding(top = 5.dp, bottom = if (extraPadding) DEFAULT_PADDING * 2 else DEFAULT_PADDING_HALF), verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.Center) { + Icon(painterResource(if (hostDevice.first == null) MR.images.ic_desktop else MR.images.ic_smartphone_300), null, Modifier.size(15.dp), tint = MaterialTheme.colors.secondary) + Spacer(Modifier.width(10.dp)) + Text(hostDevice.second, color = MaterialTheme.colors.secondary) + } } @Preview/*( diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/Utils.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/Utils.kt index dbadec32f..6e12681dd 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/Utils.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/Utils.kt @@ -56,7 +56,7 @@ fun annotatedStringResource(id: StringResource): AnnotatedString { fun annotatedStringResource(id: StringResource, vararg args: Any?): AnnotatedString { val density = LocalDensity.current return remember(id) { - escapedHtmlToAnnotatedString(id.localized().format(args), density) + escapedHtmlToAnnotatedString(id.localized().format(args = args), density) } } @@ -373,7 +373,7 @@ inline fun serializableSaver(): Saver = Saver( fun UriHandler.openVerifiedSimplexUri(uri: String) { val URI = try { URI.create(uri) } catch (e: Exception) { null } if (URI != null) { - connectIfOpenedViaUri(chatModel.remoteHostId, URI, ChatModel) + connectIfOpenedViaUri(chatModel.remoteHostId(), URI, ChatModel) } } diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/newchat/AddContactView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/newchat/AddContactView.kt index a539bf498..84080d5b9 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/newchat/AddContactView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/newchat/AddContactView.kt @@ -79,7 +79,7 @@ fun AddContactLayout( .verticalScroll(rememberScrollState()), verticalArrangement = Arrangement.SpaceBetween, ) { - AppBarTitle(stringResource(MR.strings.add_contact)) + AppBarTitle(stringResource(MR.strings.add_contact), hostDevice(rh?.remoteHostId)) SectionView(stringResource(MR.strings.one_time_link_short).uppercase()) { if (connReq.isNotEmpty()) { diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/newchat/AddGroupView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/newchat/AddGroupView.kt index aa5494e5a..4f71e81b0 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/newchat/AddGroupView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/newchat/AddGroupView.kt @@ -58,6 +58,7 @@ fun AddGroupView(chatModel: ChatModel, rh: RemoteHostInfo?, close: () -> Unit) { } }, incognitoPref = chatModel.controller.appPrefs.incognito, + rhId, close ) } @@ -66,6 +67,7 @@ fun AddGroupView(chatModel: ChatModel, rh: RemoteHostInfo?, close: () -> Unit) { fun AddGroupLayout( createGroup: (Boolean, GroupProfile) -> Unit, incognitoPref: SharedPreference, + rhId: Long?, close: () -> Unit ) { val bottomSheetModalState = rememberModalBottomSheetState(initialValue = ModalBottomSheetValue.Hidden) @@ -98,7 +100,7 @@ fun AddGroupLayout( .verticalScroll(rememberScrollState()) .padding(horizontal = DEFAULT_PADDING) ) { - AppBarTitle(stringResource(MR.strings.create_secret_group_title)) + AppBarTitle(stringResource(MR.strings.create_secret_group_title), hostDevice(rhId)) Box( Modifier .fillMaxWidth() @@ -174,7 +176,8 @@ fun PreviewAddGroupLayout() { AddGroupLayout( createGroup = { _, _ -> }, incognitoPref = SharedPreference({ false }, {}), - close = {} + close = {}, + rhId = null, ) } } diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/newchat/ContactConnectionInfoView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/newchat/ContactConnectionInfoView.kt index 50419cdaf..5e9495e86 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/newchat/ContactConnectionInfoView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/newchat/ContactConnectionInfoView.kt @@ -56,6 +56,7 @@ fun ContactConnectionInfoView( connReq = connReqInvitation, contactConnection = contactConnection, focusAlias = focusAlias, + rhId = rhId, deleteConnection = { deleteContactConnectionAlert(rhId, contactConnection, chatModel, close) }, onLocalAliasChanged = { setContactAlias(rhId, contactConnection, it, chatModel) }, share = { if (connReqInvitation != null) clipboard.shareText(connReqInvitation) }, @@ -80,6 +81,7 @@ private fun ContactConnectionInfoLayout( connReq: String?, contactConnection: PendingContactConnection, focusAlias: Boolean, + rhId: Long?, deleteConnection: () -> Unit, onLocalAliasChanged: (String) -> Unit, share: () -> Unit, @@ -114,7 +116,8 @@ private fun ContactConnectionInfoLayout( stringResource( if (contactConnection.initiated) MR.strings.you_invited_a_contact else MR.strings.you_accepted_connection - ) + ), + hostDevice(rhId) ) Text( stringResource( @@ -185,6 +188,7 @@ private fun PreviewContactConnectionInfoView() { connReq = "https://simplex.chat/contact#/?v=1&smp=smp%3A%2F%2FPQUV2eL0t7OStZOoAsPEV2QYWt4-xilbakvGUGOItUo%3D%40smp6.simplex.im%2FK1rslx-m5bpXVIdMZg9NLUZ_8JBm8xTt%23MCowBQYDK2VuAyEALDeVe-sG8mRY22LsXlPgiwTNs9dbiLrNuA7f3ZMAJ2w%3D", contactConnection = PendingContactConnection.getSampleData(), focusAlias = false, + rhId = null, deleteConnection = {}, onLocalAliasChanged = {}, share = {}, diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/newchat/PasteToConnect.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/newchat/PasteToConnect.kt index 3e4447d35..dacf93757 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/newchat/PasteToConnect.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/newchat/PasteToConnect.kt @@ -67,7 +67,7 @@ fun PasteToConnectLayout( Modifier.verticalScroll(rememberScrollState()).padding(horizontal = DEFAULT_PADDING), verticalArrangement = Arrangement.SpaceBetween, ) { - AppBarTitle(stringResource(MR.strings.connect_via_link), false) + AppBarTitle(stringResource(MR.strings.connect_via_link), hostDevice(rhId), withPadding = false) Box(Modifier.padding(top = DEFAULT_PADDING, bottom = 6.dp)) { TextEditor( diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/newchat/ScanToConnectView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/newchat/ScanToConnectView.kt index bd111c9c3..4deeda3e2 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/newchat/ScanToConnectView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/newchat/ScanToConnectView.kt @@ -4,7 +4,6 @@ import SectionBottomSpacer import SectionItemView import SectionTextFooter import androidx.compose.desktop.ui.tooling.preview.Preview -import chat.simplex.common.platform.Log import androidx.compose.foundation.layout.* import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.verticalScroll @@ -17,7 +16,7 @@ import androidx.compose.ui.text.style.TextAlign import dev.icerock.moko.resources.compose.stringResource import androidx.compose.ui.unit.dp import chat.simplex.common.model.* -import chat.simplex.common.platform.TAG +import chat.simplex.common.platform.* import chat.simplex.common.ui.theme.* import chat.simplex.common.views.chatlist.* import chat.simplex.common.views.helpers.* @@ -65,6 +64,7 @@ suspend fun planAndConnect( confirmText = if (incognito) generalGetString(MR.strings.connect_via_link_incognito) else generalGetString(MR.strings.connect_via_link_verb), onConfirm = { withApi { connectViaUri(chatModel, rhId, uri, incognito, connectionPlan, close) } }, destructive = true, + hostDevice = hostDevice(rhId), ) } else { askCurrentOrIncognitoProfileAlert( @@ -82,12 +82,14 @@ suspend fun planAndConnect( openKnownContact(chatModel, rhId, close, contact) AlertManager.shared.showAlertMsg( generalGetString(MR.strings.contact_already_exists), - String.format(generalGetString(MR.strings.connect_plan_you_are_already_connecting_to_vName), contact.displayName) + String.format(generalGetString(MR.strings.connect_plan_you_are_already_connecting_to_vName), contact.displayName), + hostDevice = hostDevice(rhId), ) } else { AlertManager.shared.showAlertMsg( generalGetString(MR.strings.connect_plan_already_connecting), - generalGetString(MR.strings.connect_plan_you_are_already_connecting_via_this_one_time_link) + generalGetString(MR.strings.connect_plan_you_are_already_connecting_via_this_one_time_link), + hostDevice = hostDevice(rhId), ) } } @@ -97,7 +99,8 @@ suspend fun planAndConnect( openKnownContact(chatModel, rhId, close, contact) AlertManager.shared.showAlertMsg( generalGetString(MR.strings.contact_already_exists), - String.format(generalGetString(MR.strings.you_are_already_connected_to_vName_via_this_link), contact.displayName) + String.format(generalGetString(MR.strings.you_are_already_connected_to_vName_via_this_link), contact.displayName), + hostDevice = hostDevice(rhId), ) } } @@ -124,6 +127,7 @@ suspend fun planAndConnect( confirmText = if (incognito) generalGetString(MR.strings.connect_via_link_incognito) else generalGetString(MR.strings.connect_via_link_verb), onConfirm = { withApi { connectViaUri(chatModel, rhId, uri, incognito, connectionPlan, close) } }, destructive = true, + hostDevice = hostDevice(rhId), ) } else { askCurrentOrIncognitoProfileAlert( @@ -143,6 +147,7 @@ suspend fun planAndConnect( confirmText = if (incognito) generalGetString(MR.strings.connect_via_link_incognito) else generalGetString(MR.strings.connect_via_link_verb), onConfirm = { withApi { connectViaUri(chatModel, rhId, uri, incognito, connectionPlan, close) } }, destructive = true, + hostDevice = hostDevice(rhId), ) } else { askCurrentOrIncognitoProfileAlert( @@ -159,7 +164,8 @@ suspend fun planAndConnect( openKnownContact(chatModel, rhId, close, contact) AlertManager.shared.showAlertMsg( generalGetString(MR.strings.contact_already_exists), - String.format(generalGetString(MR.strings.connect_plan_you_are_already_connecting_to_vName), contact.displayName) + String.format(generalGetString(MR.strings.connect_plan_you_are_already_connecting_to_vName), contact.displayName), + hostDevice = hostDevice(rhId), ) } is ContactAddressPlan.Known -> { @@ -168,7 +174,8 @@ suspend fun planAndConnect( openKnownContact(chatModel, rhId, close, contact) AlertManager.shared.showAlertMsg( generalGetString(MR.strings.contact_already_exists), - String.format(generalGetString(MR.strings.you_are_already_connected_to_vName_via_this_link), contact.displayName) + String.format(generalGetString(MR.strings.you_are_already_connected_to_vName_via_this_link), contact.displayName), + hostDevice = hostDevice(rhId), ) } is ContactAddressPlan.ContactViaAddress -> { @@ -190,7 +197,8 @@ suspend fun planAndConnect( title = generalGetString(MR.strings.connect_via_group_link), text = generalGetString(MR.strings.you_will_join_group), confirmText = if (incognito) generalGetString(MR.strings.join_group_incognito_button) else generalGetString(MR.strings.join_group_button), - onConfirm = { withApi { connectViaUri(chatModel, rhId, uri, incognito, connectionPlan, close) } } + onConfirm = { withApi { connectViaUri(chatModel, rhId, uri, incognito, connectionPlan, close) } }, + hostDevice = hostDevice(rhId), ) } else { askCurrentOrIncognitoProfileAlert( @@ -215,6 +223,7 @@ suspend fun planAndConnect( confirmText = if (incognito) generalGetString(MR.strings.join_group_incognito_button) else generalGetString(MR.strings.join_group_button), onConfirm = { withApi { connectViaUri(chatModel, rhId, uri, incognito, connectionPlan, close) } }, destructive = true, + hostDevice = hostDevice(rhId), ) } else { askCurrentOrIncognitoProfileAlert( @@ -236,7 +245,8 @@ suspend fun planAndConnect( } else { AlertManager.shared.showAlertMsg( generalGetString(MR.strings.connect_plan_already_joining_the_group), - generalGetString(MR.strings.connect_plan_you_are_already_joining_the_group_via_this_link) + generalGetString(MR.strings.connect_plan_you_are_already_joining_the_group_via_this_link), + hostDevice = hostDevice(rhId), ) } } @@ -246,7 +256,8 @@ suspend fun planAndConnect( openKnownGroup(chatModel, rhId, close, groupInfo) AlertManager.shared.showAlertMsg( generalGetString(MR.strings.connect_plan_group_already_exists), - String.format(generalGetString(MR.strings.connect_plan_you_are_already_in_group_vName), groupInfo.displayName) + String.format(generalGetString(MR.strings.connect_plan_you_are_already_in_group_vName), groupInfo.displayName), + hostDevice = hostDevice(rhId), ) } } @@ -284,7 +295,8 @@ suspend fun connectViaUri( ConnectionLinkType.CONTACT -> generalGetString(MR.strings.you_will_be_connected_when_your_connection_request_is_accepted) ConnectionLinkType.INVITATION -> generalGetString(MR.strings.you_will_be_connected_when_your_contacts_device_is_online) ConnectionLinkType.GROUP -> generalGetString(MR.strings.you_will_be_connected_when_group_host_device_is_online) - } + }, + hostDevice = hostDevice(rhId), ) } return r @@ -336,7 +348,8 @@ fun askCurrentOrIncognitoProfileAlert( Text(stringResource(MR.strings.cancel_verb), Modifier.fillMaxWidth(), textAlign = TextAlign.Center, color = MaterialTheme.colors.primary) } } - } + }, + hostDevice = hostDevice(rhId), ) } @@ -411,7 +424,8 @@ fun ownGroupLinkConfirmConnect( Text(stringResource(MR.strings.cancel_verb), Modifier.fillMaxWidth(), textAlign = TextAlign.Center, color = MaterialTheme.colors.primary) } } - } + }, + hostDevice = hostDevice(rhId), ) } @@ -455,7 +469,7 @@ fun ConnectContactLayout( Modifier.verticalScroll(rememberScrollState()).padding(horizontal = DEFAULT_PADDING), verticalArrangement = Arrangement.SpaceBetween ) { - AppBarTitle(stringResource(MR.strings.scan_QR_code), false) + AppBarTitle(stringResource(MR.strings.scan_QR_code), hostDevice(rh?.remoteHostId), withPadding = false) Box( Modifier .fillMaxWidth() diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/onboarding/HowItWorks.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/onboarding/HowItWorks.kt index e3dfb2b73..6c76acc3e 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/onboarding/HowItWorks.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/onboarding/HowItWorks.kt @@ -26,7 +26,7 @@ fun HowItWorks(user: User?, onboardingStage: SharedPreference? .fillMaxWidth() .padding(horizontal = DEFAULT_PADDING), ) { - AppBarTitle(stringResource(MR.strings.how_simplex_works), false) + AppBarTitle(stringResource(MR.strings.how_simplex_works), withPadding = false) ReadableText(MR.strings.many_people_asked_how_can_it_deliver) ReadableText(MR.strings.to_protect_privacy_simplex_has_ids_for_queues) ReadableText(MR.strings.you_control_servers_to_receive_your_contacts_to_send) diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/remote/ConnectDesktopView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/remote/ConnectDesktopView.kt index d631836dd..8d3cfddce 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/remote/ConnectDesktopView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/remote/ConnectDesktopView.kt @@ -53,6 +53,7 @@ fun ConnectDesktopView(close: () -> Unit) { ModalView(close = closeWithAlert) { ConnectDesktopLayout( deviceName = deviceName.value!!, + close ) } val ntfModeService = remember { chatModel.controller.appPrefs.notificationsMode.get() == NotificationsMode.SERVICE } @@ -67,7 +68,7 @@ fun ConnectDesktopView(close: () -> Unit) { } @Composable -private fun ConnectDesktopLayout(deviceName: String) { +private fun ConnectDesktopLayout(deviceName: String, close: () -> Unit) { val sessionAddress = remember { mutableStateOf("") } val remoteCtrls = remember { mutableStateListOf() } val session = remember { chatModel.remoteCtrlSession }.value @@ -89,7 +90,7 @@ private fun ConnectDesktopLayout(deviceName: String) { } } - is UIRemoteCtrlSessionState.Connected -> ActiveSession(session, session.sessionState.remoteCtrl) + is UIRemoteCtrlSessionState.Connected -> ActiveSession(session, session.sessionState.remoteCtrl, close) } } else { ConnectDesktop(deviceName, remoteCtrls, sessionAddress) @@ -205,7 +206,7 @@ private fun CtrlDeviceVersionText(session: RemoteCtrlSession) { } @Composable -private fun ActiveSession(session: RemoteCtrlSession, rc: RemoteCtrlInfo) { +private fun ActiveSession(session: RemoteCtrlSession, rc: RemoteCtrlInfo, close: () -> Unit) { AppBarTitle(stringResource(MR.strings.connected_to_desktop)) SectionView(stringResource(MR.strings.connected_desktop).uppercase(), padding = PaddingValues(horizontal = DEFAULT_PADDING)) { Text(rc.deviceViewName) @@ -223,7 +224,7 @@ private fun ActiveSession(session: RemoteCtrlSession, rc: RemoteCtrlInfo) { SectionSpacer() SectionView { - DisconnectButton(::disconnectDesktop) + DisconnectButton { disconnectDesktop(close) } } } diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/remote/ConnectMobileView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/remote/ConnectMobileView.kt index 0d90e5945..163d15ce6 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/remote/ConnectMobileView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/remote/ConnectMobileView.kt @@ -36,12 +36,10 @@ import dev.icerock.moko.resources.compose.painterResource import dev.icerock.moko.resources.compose.stringResource @Composable -fun ConnectMobileView( - m: ChatModel -) { +fun ConnectMobileView() { val connecting = rememberSaveable() { mutableStateOf(false) } val remoteHosts = remember { chatModel.remoteHosts } - val deviceName = m.controller.appPrefs.deviceNameForRemoteAccess + val deviceName = chatModel.controller.appPrefs.deviceNameForRemoteAccess LaunchedEffect(Unit) { controller.reloadRemoteHosts() } @@ -49,11 +47,11 @@ fun ConnectMobileView( deviceName = remember { deviceName.state }, remoteHosts = remoteHosts, connecting, - connectedHost = remember { m.currentRemoteHost }, + connectedHost = remember { chatModel.currentRemoteHost }, updateDeviceName = { withBGApi { if (it != "") { - m.controller.setLocalDeviceName(it) + chatModel.controller.setLocalDeviceName(it) deviceName.set(it) } } @@ -163,7 +161,8 @@ private fun ConnectMobileViewLayout( title: String, invitation: String?, deviceName: String?, - sessionCode: String? + sessionCode: String?, + port: String? ) { Column( Modifier.fillMaxWidth().verticalScroll(rememberScrollState()), @@ -171,13 +170,14 @@ private fun ConnectMobileViewLayout( ) { AppBarTitle(title) SectionView { - if (invitation != null && sessionCode == null) { + if (invitation != null && sessionCode == null && port != null) { QRCode( invitation, Modifier .padding(start = DEFAULT_PADDING, top = DEFAULT_PADDING_HALF, end = DEFAULT_PADDING, bottom = DEFAULT_PADDING_HALF) .aspectRatio(1f) ) SectionTextFooter(annotatedStringResource(MR.strings.open_on_mobile_and_scan_qr_code)) + SectionTextFooter(annotatedStringResource(MR.strings.waiting_for_mobile_to_connect_on_port, port)) if (remember { controller.appPrefs.developerTools.state }.value) { val clipboard = LocalClipboardManager.current @@ -234,6 +234,7 @@ fun connectMobileDevice(rh: RemoteHostInfo, connecting: MutableState) { private fun showAddingMobileDevice(connecting: MutableState) { ModalManager.start.showModalCloseable { close -> val invitation = rememberSaveable { mutableStateOf(null) } + val port = rememberSaveable { mutableStateOf(null) } val pairing = remember { chatModel.newRemoteHostPairing } val sessionCode = when (val state = pairing.value?.second) { is RemoteHostSessionState.PendingConfirmation -> state.sessionCode @@ -249,7 +250,8 @@ private fun showAddingMobileDevice(connecting: MutableState) { title = if (cachedSessionCode == null) stringResource(MR.strings.link_a_mobile) else stringResource(MR.strings.verify_connection), invitation = invitation.value, deviceName = remoteDeviceName, - sessionCode = cachedSessionCode + sessionCode = cachedSessionCode, + port = port.value ) val oldRemoteHostId by remember { mutableStateOf(chatModel.currentRemoteHost.value?.remoteHostId) } LaunchedEffect(remember { chatModel.currentRemoteHost }.value) { @@ -268,6 +270,7 @@ private fun showAddingMobileDevice(connecting: MutableState) { if (r != null) { connecting.value = true invitation.value = r.second + port.value = r.third } } onDispose { @@ -286,6 +289,7 @@ private fun showConnectMobileDevice(rh: RemoteHostInfo, connecting: MutableState ModalManager.start.showModalCloseable { close -> val pairing = remember { chatModel.newRemoteHostPairing } val invitation = rememberSaveable { mutableStateOf(null) } + val port = rememberSaveable { mutableStateOf(null) } val sessionCode = when (val state = pairing.value?.second) { is RemoteHostSessionState.PendingConfirmation -> state.sessionCode else -> null @@ -300,6 +304,7 @@ private fun showConnectMobileDevice(rh: RemoteHostInfo, connecting: MutableState invitation = invitation.value, deviceName = pairing.value?.first?.hostDeviceName ?: rh.hostDeviceName, sessionCode = cachedSessionCode, + port = port.value ) var remoteHostId by rememberSaveable { mutableStateOf(null) } LaunchedEffect(Unit) { @@ -309,6 +314,7 @@ private fun showConnectMobileDevice(rh: RemoteHostInfo, connecting: MutableState connecting.value = true remoteHostId = rh_?.remoteHostId invitation.value = inv + port.value = r.third } } LaunchedEffect(remember { chatModel.currentRemoteHost }.value) { @@ -345,7 +351,8 @@ private fun showConnectedMobileDevice(rh: RemoteHostInfo, disconnectHost: () -> title = stringResource(MR.strings.connected_to_mobile), invitation = null, deviceName = rh.hostDeviceName, - sessionCode = sessionCode + sessionCode = sessionCode, + port = null, ) Spacer(Modifier.height(DEFAULT_PADDING_HALF)) SectionItemView(disconnectHost) { diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/HelpView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/HelpView.kt index c5bde4494..33e183aaa 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/HelpView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/HelpView.kt @@ -26,7 +26,7 @@ fun HelpLayout(userDisplayName: String) { .verticalScroll(rememberScrollState()) .padding(horizontal = DEFAULT_PADDING), ){ - AppBarTitle(String.format(stringResource(MR.strings.personal_welcome), userDisplayName), false) + AppBarTitle(String.format(stringResource(MR.strings.personal_welcome), userDisplayName), withPadding = false) ChatHelpView() } } diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/ProtocolServersView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/ProtocolServersView.kt index cbcb7344f..66dde9f96 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/ProtocolServersView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/ProtocolServersView.kt @@ -29,13 +29,12 @@ import kotlinx.coroutines.launch @Composable fun ProtocolServersView(m: ChatModel, rhId: Long?, serverProtocol: ServerProtocol, close: () -> Unit) { - // TODO close if remote host changes - var presetServers by remember { mutableStateOf(emptyList()) } - var servers by remember { + var presetServers by remember(rhId) { mutableStateOf(emptyList()) } + var servers by remember(rhId) { mutableStateOf(m.userSMPServersUnsaved.value ?: emptyList()) } - val currServers = remember { mutableStateOf(servers) } - val testing = rememberSaveable { mutableStateOf(false) } + val currServers = remember(rhId) { mutableStateOf(servers) } + val testing = rememberSaveable(rhId) { mutableStateOf(false) } val serversUnchanged = remember { derivedStateOf { servers == currServers.value || testing.value } } val allServersDisabled = remember { derivedStateOf { servers.all { !it.enabled } } } val saveDisabled = remember { @@ -51,7 +50,12 @@ fun ProtocolServersView(m: ChatModel, rhId: Long?, serverProtocol: ServerProtoco } } - LaunchedEffect(Unit) { + KeyChangeEffect(rhId) { + m.userSMPServersUnsaved.value = null + servers = emptyList() + } + + LaunchedEffect(rhId) { val res = m.controller.getUserProtoServers(rhId, serverProtocol) if (res != null) { currServers.value = res.protoServers diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/ScanProtocolServer.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/ScanProtocolServer.kt index ac74bd04d..77cb0ead1 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/ScanProtocolServer.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/ScanProtocolServer.kt @@ -22,7 +22,7 @@ fun ScanProtocolServerLayout(rhId: Long?, onNext: (ServerCfg) -> Unit) { .fillMaxSize() .padding(horizontal = DEFAULT_PADDING) ) { - AppBarTitle(stringResource(MR.strings.smp_servers_scan_qr), false) + AppBarTitle(stringResource(MR.strings.smp_servers_scan_qr), withPadding = false) Box( Modifier .fillMaxWidth() diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/SettingsView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/SettingsView.kt index 7bd060e8d..4e86d33a4 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/SettingsView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/SettingsView.kt @@ -158,7 +158,7 @@ fun SettingsLayout( SettingsActionItem(painterResource(MR.images.ic_qr_code), stringResource(MR.strings.your_simplex_contact_address), showCustomModal { it, close -> UserAddressView(it, it.currentUser.value?.remoteHostId, shareViaProfile = it.currentUser.value!!.addressShared, close = close) }, disabled = stopped, extraPadding = true) ChatPreferencesItem(showCustomModal, stopped = stopped) if (appPlatform.isDesktop) { - SettingsActionItem(painterResource(MR.images.ic_smartphone), stringResource(if (remember { chatModel.remoteHosts }.isEmpty()) MR.strings.link_a_mobile else MR.strings.linked_mobiles), showModal { ConnectMobileView(it) }, disabled = stopped, extraPadding = true) + SettingsActionItem(painterResource(MR.images.ic_smartphone), stringResource(if (remember { chatModel.remoteHosts }.isEmpty()) MR.strings.link_a_mobile else MR.strings.linked_mobiles), showModal { ConnectMobileView() }, disabled = stopped, extraPadding = true) } else { SettingsActionItem(painterResource(MR.images.ic_desktop), stringResource(MR.strings.settings_section_title_use_from_desktop), showCustomModal{ it, close -> ConnectDesktopView(close) }, disabled = stopped, extraPadding = true) } diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/UserAddressView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/UserAddressView.kt index 98989a775..d24824108 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/UserAddressView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/UserAddressView.kt @@ -65,6 +65,7 @@ fun UserAddressView( UserAddressLayout( userAddress = userAddress.value, shareViaProfile, + rhId, onCloseHandler, createAddress = { withApi { @@ -169,6 +170,7 @@ fun UserAddressView( private fun UserAddressLayout( userAddress: UserContactLinkRec?, shareViaProfile: MutableState, + rhId: Long?, onCloseHandler: MutableState<(close: () -> Unit) -> Unit>, createAddress: () -> Unit, learnMore: () -> Unit, @@ -181,7 +183,7 @@ private fun UserAddressLayout( Column( Modifier.verticalScroll(rememberScrollState()), ) { - AppBarTitle(stringResource(MR.strings.simplex_address), false) + AppBarTitle(stringResource(MR.strings.simplex_address), hostDevice(rhId), withPadding = false) Column( Modifier.fillMaxWidth().padding(bottom = DEFAULT_PADDING_HALF), horizontalAlignment = Alignment.CenterHorizontally, @@ -438,6 +440,7 @@ fun PreviewUserAddressLayoutNoAddress() { setProfileAddress = { _ -> }, learnMore = {}, shareViaProfile = remember { mutableStateOf(false) }, + rhId = null, onCloseHandler = remember { mutableStateOf({}) }, sendEmail = {}, ) @@ -471,6 +474,7 @@ fun PreviewUserAddressLayoutAddressCreated() { setProfileAddress = { _ -> }, learnMore = {}, shareViaProfile = remember { mutableStateOf(false) }, + rhId = null, onCloseHandler = remember { mutableStateOf({}) }, sendEmail = {}, ) diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/VersionInfoView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/VersionInfoView.kt index 010b94e03..06a476221 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/VersionInfoView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/VersionInfoView.kt @@ -18,7 +18,7 @@ fun VersionInfoView(info: CoreVersionInfo) { Column( Modifier.padding(horizontal = DEFAULT_PADDING), ) { - AppBarTitle(stringResource(MR.strings.app_version_title), false) + AppBarTitle(stringResource(MR.strings.app_version_title), withPadding = false) if (appPlatform.isAndroid) { Text(String.format(stringResource(MR.strings.app_version_name), BuildConfigCommon.ANDROID_VERSION_NAME)) Text(String.format(stringResource(MR.strings.app_version_code), BuildConfigCommon.ANDROID_VERSION_CODE)) diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/base/strings.xml b/apps/multiplatform/common/src/commonMain/resources/MR/base/strings.xml index 177dcb55f..44f950c32 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/base/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/base/strings.xml @@ -1178,6 +1178,7 @@ observer + author member admin owner @@ -1608,16 +1609,6 @@ Toggle incognito when connecting. 6 new interface languages Arabic, Bulgarian, Finnish, Hebrew, Thai and Ukrainian - thanks to the users and Weblate. - Connect desktop and mobile! - Use your mobile app chat profile via desktop app. - Group improvements - Create groups incognito, block group members, faster join via link, and more. - Notify about contact deletion - You can optionally notify contacts when deleting them. - Checking SimpleX links - Detection of previously used and your own SimpleX links. - Spaces in profile names - You can now add spaces to your profile name. Link mobile and desktop apps! 🔗 Via secure quantum resistant protocol. Better groups @@ -1668,9 +1659,11 @@ Unlink desktop? Unlink Disconnect + %s was disconnected]]> Disconnect desktop? Only one device can work at the same time - Use from desktop in mobile app and scan QR code]]> + Use from desktop in mobile app and scan QR code.]]> + %s]]> Bad desktop address Incompatible version Desktop app version %s is not compatible with this app. diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/it/strings.xml b/apps/multiplatform/common/src/commonMain/resources/MR/it/strings.xml index 093ca0564..8aa847c78 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/it/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/it/strings.xml @@ -1465,7 +1465,7 @@ Nuovo dispositivo mobile Indirizzo desktop Solo un dispositivo può funzionare nello stesso momento - Usa dal desktop nell\'app mobile e scansiona il codice QR]]> + Usa dal desktop nell\'app mobile e scansiona il codice QR.]]> Versione incompatibile (nuovo)]]> Scollegare il desktop? diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/nl/strings.xml b/apps/multiplatform/common/src/commonMain/resources/MR/nl/strings.xml index c11284439..801df9d60 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/nl/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/nl/strings.xml @@ -1463,7 +1463,7 @@ Nieuw mobiel apparaat Desktop adres Er kan slechts één apparaat tegelijkertijd werken - Gebruik vanaf desktop in de mobiele app en scan de QR-code]]> + Gebruik vanaf desktop in de mobiele app en scan de QR-code.]]> Incompatibele versie (nieuw)]]> Desktop ontkoppelen? diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/zh-rCN/strings.xml b/apps/multiplatform/common/src/commonMain/resources/MR/zh-rCN/strings.xml index e672c189a..90f475135 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/zh-rCN/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/zh-rCN/strings.xml @@ -1465,7 +1465,7 @@ 新移动设备 桌面地址 同一时刻只有一台设备可以工作 - 从桌面使用并扫描二维码]]> + 从桌面使用并扫描二维码.]]> 不兼容的版本 (新)]]> 取消链接桌面端? diff --git a/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/DesktopApp.kt b/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/DesktopApp.kt index 2931e0e01..1a95317a6 100644 --- a/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/DesktopApp.kt +++ b/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/DesktopApp.kt @@ -10,6 +10,7 @@ import androidx.compose.runtime.* import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.input.key.* +import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.unit.dp import androidx.compose.ui.window.* import chat.simplex.common.model.ChatController @@ -19,6 +20,7 @@ import chat.simplex.common.ui.theme.DEFAULT_START_MODAL_WIDTH import chat.simplex.common.ui.theme.SimpleXTheme import chat.simplex.common.views.TerminalView import chat.simplex.common.views.helpers.FileDialogChooser +import chat.simplex.common.views.helpers.escapedHtmlToAnnotatedString import chat.simplex.res.MR import dev.icerock.moko.resources.compose.stringResource import kotlinx.coroutines.* @@ -80,7 +82,7 @@ fun showApp() = application { if (toast != null) { Box(Modifier.fillMaxSize().padding(bottom = 20.dp), contentAlignment = Alignment.BottomCenter) { Text( - toast.first, + escapedHtmlToAnnotatedString(toast.first, LocalDensity.current), Modifier.background(MaterialTheme.colors.primary, RoundedCornerShape(100)).padding(vertical = 5.dp, horizontal = 10.dp), color = MaterialTheme.colors.onPrimary, style = MaterialTheme.typography.body1 diff --git a/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/platform/Share.desktop.kt b/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/platform/Share.desktop.kt index a91bc5a76..f7728f9c6 100644 --- a/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/platform/Share.desktop.kt +++ b/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/platform/Share.desktop.kt @@ -4,7 +4,6 @@ import androidx.compose.ui.platform.ClipboardManager import androidx.compose.ui.platform.UriHandler import androidx.compose.ui.text.AnnotatedString import chat.simplex.common.model.* -import chat.simplex.common.views.helpers.getAppFileUri import chat.simplex.common.views.helpers.withApi import java.io.File import java.net.URI diff --git a/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/views/chat/item/ChatItemView.desktop.kt b/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/views/chat/item/ChatItemView.desktop.kt index 91efdf790..905a6e352 100644 --- a/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/views/chat/item/ChatItemView.desktop.kt +++ b/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/views/chat/item/ChatItemView.desktop.kt @@ -19,7 +19,6 @@ import chat.simplex.res.MR import dev.icerock.moko.resources.compose.painterResource import dev.icerock.moko.resources.compose.stringResource import java.io.File -import java.util.* @Composable actual fun ReactionIcon(text: String, fontSize: TextUnit) { diff --git a/apps/multiplatform/gradle.properties b/apps/multiplatform/gradle.properties index b16636d0d..9c817f08f 100644 --- a/apps/multiplatform/gradle.properties +++ b/apps/multiplatform/gradle.properties @@ -25,11 +25,11 @@ android.nonTransitiveRClass=true android.enableJetifier=true kotlin.mpp.androidSourceSetLayoutVersion=2 -android.version_name=5.4-beta.3 -android.version_code=160 +android.version_name=5.4-beta.4 +android.version_code=161 -desktop.version_name=5.4-beta.3 -desktop.version_code=16 +desktop.version_name=5.4-beta.4 +desktop.version_code=17 kotlin.version=1.8.20 gradle.plugin.version=7.4.2 diff --git a/apps/simplex-broadcast-bot/src/Broadcast/Options.hs b/apps/simplex-broadcast-bot/src/Broadcast/Options.hs index 76b349a49..3758af2fc 100644 --- a/apps/simplex-broadcast-bot/src/Broadcast/Options.hs +++ b/apps/simplex-broadcast-bot/src/Broadcast/Options.hs @@ -74,6 +74,7 @@ mkChatOpts :: BroadcastBotOpts -> ChatOpts mkChatOpts BroadcastBotOpts {coreOptions} = ChatOpts { coreOptions, + deviceName = Nothing, chatCmd = "", chatCmdDelay = 3, chatServerPort = Nothing, diff --git a/apps/simplex-directory-service/src/Directory/Options.hs b/apps/simplex-directory-service/src/Directory/Options.hs index 1f06afe11..8f28c9013 100644 --- a/apps/simplex-directory-service/src/Directory/Options.hs +++ b/apps/simplex-directory-service/src/Directory/Options.hs @@ -72,6 +72,7 @@ mkChatOpts :: DirectoryOpts -> ChatOpts mkChatOpts DirectoryOpts {coreOptions} = ChatOpts { coreOptions, + deviceName = Nothing, chatCmd = "", chatCmdDelay = 3, chatServerPort = Nothing, diff --git a/src/Simplex/Chat.hs b/src/Simplex/Chat.hs index bac065f7a..a6612f475 100644 --- a/src/Simplex/Chat.hs +++ b/src/Simplex/Chat.hs @@ -35,19 +35,20 @@ import Data.Either (fromRight, rights) import Data.Fixed (div') import Data.Functor (($>)) import Data.Int (Int64) -import Data.List (find, foldl', isSuffixOf, partition, sortOn) +import Data.List (find, foldl', isSuffixOf, partition, sortBy, sortOn) import Data.List.NonEmpty (NonEmpty, nonEmpty) import qualified Data.List.NonEmpty as L import Data.Map.Strict (Map) import qualified Data.Map.Strict as M import Data.Maybe (catMaybes, fromMaybe, isJust, isNothing, listToMaybe, mapMaybe, maybeToList) +import Data.Ord (comparing) import Data.Text (Text) import qualified Data.Text as T import Data.Text.Encoding (decodeLatin1, encodeUtf8) import Data.Time (NominalDiffTime, addUTCTime, defaultTimeLocale, formatTime) import Data.Time.Clock (UTCTime, diffUTCTime, getCurrentTime, nominalDay, nominalDiffTimeToSeconds) import Data.Time.Clock.System (SystemTime, systemToUTCTime) -import Data.Word (Word32) +import Data.Word (Word16, Word32) import qualified Database.SQLite.Simple as SQL import Simplex.Chat.Archive import Simplex.Chat.Call @@ -100,6 +101,7 @@ import qualified Simplex.Messaging.TMap as TM import Simplex.Messaging.Transport.Client (defaultSocksProxy) import Simplex.Messaging.Util import Simplex.Messaging.Version +import Simplex.RemoteControl.Invitation (RCSignedInvitation (..), RCInvitation (..)) import System.Exit (ExitCode, exitFailure, exitSuccess) import System.FilePath (takeFileName, ()) import System.IO (Handle, IOMode (..), SeekMode (..), hFlush, stdout) @@ -147,7 +149,8 @@ defaultChatConfig = cleanupManagerStepDelay = 3 * 1000000, -- 3 seconds ciExpirationInterval = 30 * 60 * 1000000, -- 30 minutes coreApi = False, - highlyAvailable = False + highlyAvailable = False, + deviceNameForRemote = "" } _defaultSMPServers :: NonEmpty SMPServerWithAuth @@ -191,7 +194,7 @@ createChatDatabase filePrefix key confirmMigrations = runExceptT $ do pure ChatDatabase {chatStore, agentStore} newChatController :: ChatDatabase -> Maybe User -> ChatConfig -> ChatOpts -> IO ChatController -newChatController ChatDatabase {chatStore, agentStore} user cfg@ChatConfig {agentConfig = aCfg, defaultServers, inlineFiles, tempDir} ChatOpts {coreOptions = CoreChatOpts {smpServers, xftpServers, networkConfig, logLevel, logConnections, logServerHosts, logFile, tbqSize, highlyAvailable}, optFilesFolder, showReactions, allowInstantFiles, autoAcceptFileSize} = do +newChatController ChatDatabase {chatStore, agentStore} user cfg@ChatConfig {agentConfig = aCfg, defaultServers, inlineFiles, tempDir, deviceNameForRemote} ChatOpts {coreOptions = CoreChatOpts {smpServers, xftpServers, networkConfig, logLevel, logConnections, logServerHosts, logFile, tbqSize, highlyAvailable}, deviceName, optFilesFolder, showReactions, allowInstantFiles, autoAcceptFileSize} = do let inlineFiles' = if allowInstantFiles || autoAcceptFileSize > 0 then inlineFiles else inlineFiles {sendChunks = 0, receiveInstant = False} config = cfg {logLevel, showReactions, tbqSize, subscriptionEvents = logConnections, hostEvents = logServerHosts, defaultServers = configServers, inlineFiles = inlineFiles', autoAcceptFileSize, highlyAvailable} firstTime = dbNew chatStore @@ -209,7 +212,7 @@ newChatController ChatDatabase {chatStore, agentStore} user cfg@ChatConfig {agen sndFiles <- newTVarIO M.empty rcvFiles <- newTVarIO M.empty currentCalls <- atomically TM.empty - localDeviceName <- newTVarIO "" -- TODO set in config + localDeviceName <- newTVarIO $ fromMaybe deviceNameForRemote deviceName multicastSubscribers <- newTMVarIO 0 remoteSessionSeq <- newTVarIO 0 remoteHostSessions <- atomically TM.empty @@ -1958,8 +1961,8 @@ processChatCommand = \case ListRemoteHosts -> withUser_ $ CRRemoteHostList <$> listRemoteHosts SwitchRemoteHost rh_ -> withUser_ $ CRCurrentRemoteHost <$> switchRemoteHost rh_ StartRemoteHost rh_ -> withUser_ $ do - (remoteHost_, inv) <- startRemoteHost rh_ - pure CRRemoteHostStarted {remoteHost_, invitation = decodeLatin1 $ strEncode inv} + (remoteHost_, inv@RCSignedInvitation {invitation = RCInvitation {port}}) <- startRemoteHost rh_ + pure CRRemoteHostStarted {remoteHost_, invitation = decodeLatin1 $ strEncode inv, ctrlPort = show port} StopRemoteHost rh_ -> withUser_ $ closeRemoteHost rh_ >> ok_ DeleteRemoteHost rh -> withUser_ $ deleteRemoteHost rh >> ok_ StoreRemoteFile rh encrypted_ localPath -> withUser_ $ CRRemoteFileStored rh <$> storeRemoteFile rh encrypted_ localPath @@ -2845,17 +2848,17 @@ subscribeUserConnections onlyNeeded agentBatchSubscribe user@User {userId} = do let connIds = map aConnId' pcs pure (connIds, M.fromList $ zip connIds pcs) contactSubsToView :: Map ConnId (Either AgentErrorType ()) -> Map ConnId Contact -> Bool -> m () - contactSubsToView rs cts ce = ifM (asks $ coreApi . config) notifyAPI notifyCLI + contactSubsToView rs cts ce = do + chatModifyVar connNetworkStatuses $ M.union (M.fromList statuses) + ifM (asks $ coreApi . config) (notifyAPI statuses) notifyCLI where notifyCLI = do let cRs = resultsFor rs cts cErrors = sortOn (\(Contact {localDisplayName = n}, _) -> n) $ filterErrors cRs toView . CRContactSubSummary user $ map (uncurry ContactSubStatus) cRs when ce $ mapM_ (toView . uncurry (CRContactSubError user)) cErrors - notifyAPI = do - let statuses = M.foldrWithKey' addStatus [] cts - chatModifyVar connNetworkStatuses $ M.union (M.fromList statuses) - toView $ CRNetworkStatuses (Just user) $ map (uncurry ConnNetworkStatus) statuses + notifyAPI = toView . CRNetworkStatuses (Just user) . map (uncurry ConnNetworkStatus) + statuses = M.foldrWithKey' addStatus [] cts where addStatus :: ConnId -> Contact -> [(AgentConnId, NetworkStatus)] -> [(AgentConnId, NetworkStatus)] addStatus _ Contact {activeConn = Nothing} nss = nss @@ -3076,12 +3079,12 @@ processAgentMessageNoConn = \case where hostEvent :: ChatResponse -> m () hostEvent = whenM (asks $ hostEvents . config) . toView - serverEvent srv conns nsStatus event = ifM (asks $ coreApi . config) notifyAPI notifyCLI + serverEvent srv conns nsStatus event = do + chatModifyVar connNetworkStatuses $ \m -> foldl' (\m' cId -> M.insert cId nsStatus m') m connIds + ifM (asks $ coreApi . config) (notifyAPI connIds) notifyCLI where - notifyAPI = do - let connIds = map AgentConnId conns - chatModifyVar connNetworkStatuses $ \m -> foldl' (\m' cId -> M.insert cId nsStatus m') m connIds - toView $ CRNetworkStatus nsStatus connIds + connIds = map AgentConnId conns + notifyAPI = toView . CRNetworkStatus nsStatus notifyCLI = do cs <- withStore' (`getConnectionsContacts` conns) toView $ event srv cs @@ -3544,7 +3547,8 @@ processAgentMessageConn user@User {userId} corrId agentConnId agentMessage = do members <- withStore' $ \db -> getGroupMembers db user gInfo intros <- withStore' $ \db -> createIntroductions db members m void . sendGroupMessage user gInfo members . XGrpMemNew $ memberInfo m - forM_ intros $ \intro -> + shuffledIntros <- liftIO $ shuffleMembers intros $ \GroupMemberIntro {reMember = GroupMember {memberRole}} -> memberRole + forM_ shuffledIntros $ \intro -> processIntro intro `catchChatError` (toView . CRChatError (Just user)) where sendXGrpLinkMem = do @@ -5517,7 +5521,8 @@ sendGroupMessage' :: forall e m. (MsgEncodingI e, ChatMonad m) => User -> [Group sendGroupMessage' user members chatMsgEvent groupId introId_ postDeliver = do msg <- createSndMessage chatMsgEvent (GroupId groupId) -- TODO collect failed deliveries into a single error - rs <- forM (filter memberCurrent members) $ \m -> + recipientMembers <- liftIO $ shuffleMembers (filter memberCurrent members) $ \GroupMember {memberRole} -> memberRole + rs <- forM recipientMembers $ \m -> messageMember m msg `catchChatError` (\e -> toView (CRChatError (Just user) e) $> Nothing) let sentToMembers = catMaybes rs pure (msg, sentToMembers) @@ -5555,6 +5560,15 @@ sendGroupMessage' user members chatMsgEvent groupId introId_ postDeliver = do XGrpMsgForward {} -> True _ -> False +shuffleMembers :: [a] -> (a -> GroupMemberRole) -> IO [a] +shuffleMembers ms role = do + let (adminMs, otherMs) = partition ((GRAdmin <=) . role) ms + liftM2 (<>) (shuffle adminMs) (shuffle otherMs) + where + random :: IO Word16 + random = randomRIO (0, 65535) + shuffle xs = map snd . sortBy (comparing fst) <$> mapM (\x -> (,x) <$> random) xs + sendPendingGroupMessages :: ChatMonad m => User -> GroupMember -> Connection -> m () sendPendingGroupMessages user GroupMember {groupMemberId, localDisplayName} conn = do pendingMessages <- withStore' $ \db -> getPendingGroupMessages db groupMemberId diff --git a/src/Simplex/Chat/Controller.hs b/src/Simplex/Chat/Controller.hs index 7533649c6..d4f9c9331 100644 --- a/src/Simplex/Chat/Controller.hs +++ b/src/Simplex/Chat/Controller.hs @@ -135,7 +135,8 @@ data ChatConfig = ChatConfig cleanupManagerStepDelay :: Int64, ciExpirationInterval :: Int64, -- microseconds coreApi :: Bool, - highlyAvailable :: Bool + highlyAvailable :: Bool, + deviceNameForRemote :: Text } data DefaultAgentServers = DefaultAgentServers @@ -657,14 +658,14 @@ data ChatResponse | CRContactConnectionDeleted {user :: User, connection :: PendingContactConnection} | CRRemoteHostList {remoteHosts :: [RemoteHostInfo]} | CRCurrentRemoteHost {remoteHost_ :: Maybe RemoteHostInfo} - | CRRemoteHostStarted {remoteHost_ :: Maybe RemoteHostInfo, invitation :: Text} + | CRRemoteHostStarted {remoteHost_ :: Maybe RemoteHostInfo, invitation :: Text, ctrlPort :: String} | CRRemoteHostSessionCode {remoteHost_ :: Maybe RemoteHostInfo, sessionCode :: Text} | CRNewRemoteHost {remoteHost :: RemoteHostInfo} | CRRemoteHostConnected {remoteHost :: RemoteHostInfo} | CRRemoteHostStopped {remoteHostId_ :: Maybe RemoteHostId} | CRRemoteFileStored {remoteHostId :: RemoteHostId, remoteFileSource :: CryptoFile} | CRRemoteCtrlList {remoteCtrls :: [RemoteCtrlInfo]} - | CRRemoteCtrlFound {remoteCtrl :: RemoteCtrlInfo} -- registered fingerprint, may connect + | CRRemoteCtrlFound {remoteCtrl :: RemoteCtrlInfo, ctrlAppInfo_ :: Maybe CtrlAppInfo, appVersion :: AppVersion, compatible :: Bool} | CRRemoteCtrlConnecting {remoteCtrl_ :: Maybe RemoteCtrlInfo, ctrlAppInfo :: CtrlAppInfo, appVersion :: AppVersion} | CRRemoteCtrlSessionCode {remoteCtrl_ :: Maybe RemoteCtrlInfo, sessionCode :: Text} | CRRemoteCtrlConnected {remoteCtrl :: RemoteCtrlInfo} @@ -702,7 +703,7 @@ allowRemoteEvent = \case CRRemoteHostStopped _ -> False CRRemoteFileStored {} -> False CRRemoteCtrlList _ -> False - CRRemoteCtrlFound _ -> False + CRRemoteCtrlFound {} -> False CRRemoteCtrlConnecting {} -> False CRRemoteCtrlSessionCode {} -> False CRRemoteCtrlConnected _ -> False diff --git a/src/Simplex/Chat/Mobile.hs b/src/Simplex/Chat/Mobile.hs index ffcf5a0ce..912710254 100644 --- a/src/Simplex/Chat/Mobile.hs +++ b/src/Simplex/Chat/Mobile.hs @@ -181,6 +181,7 @@ mobileChatOpts dbFilePrefix dbKey = tbqSize = 1024, highlyAvailable = False }, + deviceName = Nothing, chatCmd = "", chatCmdDelay = 3, chatServerPort = Nothing, @@ -197,7 +198,8 @@ defaultMobileConfig = defaultChatConfig { confirmMigrations = MCYesUp, logLevel = CLLError, - coreApi = True + coreApi = True, + deviceNameForRemote = "Mobile" } getActiveUser_ :: SQLiteStore -> IO (Maybe User) diff --git a/src/Simplex/Chat/Options.hs b/src/Simplex/Chat/Options.hs index 04aef29df..7ce6305d2 100644 --- a/src/Simplex/Chat/Options.hs +++ b/src/Simplex/Chat/Options.hs @@ -19,6 +19,7 @@ where import Control.Logger.Simple (LogLevel (..)) import qualified Data.Attoparsec.ByteString.Char8 as A import qualified Data.ByteString.Char8 as B +import Data.Text (Text) import Numeric.Natural (Natural) import Options.Applicative import Simplex.Chat.Controller (ChatLogLevel (..), updateStr, versionNumber, versionString) @@ -32,6 +33,7 @@ import System.FilePath (combine) data ChatOpts = ChatOpts { coreOptions :: CoreChatOpts, + deviceName :: Maybe Text, chatCmd :: String, chatCmdDelay :: Int, chatServerPort :: Maybe String, @@ -200,6 +202,14 @@ coreChatOptsP appDir defaultDbFileName = do chatOptsP :: FilePath -> FilePath -> Parser ChatOpts chatOptsP appDir defaultDbFileName = do coreOptions <- coreChatOptsP appDir defaultDbFileName + deviceName <- + optional $ + strOption + ( long "device-name" + <> short 'e' + <> metavar "DEVICE" + <> help "Device name to use in connections with remote hosts and controller" + ) chatCmd <- strOption ( long "execute" @@ -268,6 +278,7 @@ chatOptsP appDir defaultDbFileName = do pure ChatOpts { coreOptions, + deviceName, chatCmd, chatCmdDelay, chatServerPort, diff --git a/src/Simplex/Chat/Remote.hs b/src/Simplex/Chat/Remote.hs index d8d53aa0c..ff271ba1c 100644 --- a/src/Simplex/Chat/Remote.hs +++ b/src/Simplex/Chat/Remote.hs @@ -397,12 +397,15 @@ findKnownRemoteCtrl = do cmdOk <- newEmptyTMVarIO action <- async $ handleCtrlError sseq "findKnownRemoteCtrl.discover" $ do atomically $ takeTMVar cmdOk - (RCCtrlPairing {ctrlFingerprint}, inv) <- timeoutThrow (ChatErrorRemoteCtrl RCETimeout) discoveryTimeout . withAgent $ \a -> rcDiscoverCtrl a pairings + (RCCtrlPairing {ctrlFingerprint}, inv@(RCVerifiedInvitation RCInvitation {app})) <- + timeoutThrow (ChatErrorRemoteCtrl RCETimeout) discoveryTimeout . withAgent $ \a -> rcDiscoverCtrl a pairings + ctrlAppInfo_ <- (Just <$> parseCtrlAppInfo app) `catchChatError` const (pure Nothing) rc <- withStore' (`getRemoteCtrlByFingerprint` ctrlFingerprint) >>= \case Nothing -> throwChatError $ CEInternalError "connecting with a stored ctrl" Just rc -> pure rc atomically $ putTMVar foundCtrl (rc, inv) - toView CRRemoteCtrlFound {remoteCtrl = remoteCtrlInfo rc (Just RCSSearching)} + let compatible = isJust $ compatibleAppVersion hostAppVersionRange . appVersionRange =<< ctrlAppInfo_ + toView CRRemoteCtrlFound {remoteCtrl = remoteCtrlInfo rc (Just RCSSearching), ctrlAppInfo_, appVersion = currentAppVersion, compatible} updateRemoteCtrlSession sseq $ \case RCSessionStarting -> Right RCSessionSearching {action, foundCtrl} _ -> Left $ ChatErrorRemoteCtrl RCEBadState @@ -439,7 +442,8 @@ startRemoteCtrlSession = do connectRemoteCtrl :: ChatMonad m => RCVerifiedInvitation -> SessionSeq -> m (Maybe RemoteCtrlInfo, CtrlAppInfo) connectRemoteCtrl verifiedInv@(RCVerifiedInvitation inv@RCInvitation {ca, app}) sseq = handleCtrlError sseq "connectRemoteCtrl" $ do - (ctrlInfo@CtrlAppInfo {deviceName = ctrlDeviceName}, v) <- parseCtrlAppInfo app + ctrlInfo@CtrlAppInfo {deviceName = ctrlDeviceName} <- parseCtrlAppInfo app + v <- checkAppVersion ctrlInfo rc_ <- withStore' $ \db -> getRemoteCtrlByFingerprint db ca mapM_ (validateRemoteCtrl inv) rc_ hostAppInfo <- getHostAppInfo v @@ -467,18 +471,19 @@ connectRemoteCtrl verifiedInv@(RCVerifiedInvitation inv@RCInvitation {ca, app}) in Right RCSessionPendingConfirmation {remoteCtrlId_, ctrlDeviceName = ctrlName, rcsClient, tls, sessionCode, rcsWaitSession, rcsWaitConfirmation} _ -> Left $ ChatErrorRemoteCtrl RCEBadState toView CRRemoteCtrlSessionCode {remoteCtrl_ = (`remoteCtrlInfo` Just RCSPendingConfirmation {sessionCode}) <$> rc_, sessionCode} - parseCtrlAppInfo ctrlAppInfo = do - ctrlInfo@CtrlAppInfo {appVersionRange} <- - liftEitherWith (const $ ChatErrorRemoteCtrl RCEBadInvitation) $ JT.parseEither J.parseJSON ctrlAppInfo - v <- case compatibleAppVersion hostAppVersionRange appVersionRange of + checkAppVersion CtrlAppInfo {appVersionRange} = + case compatibleAppVersion hostAppVersionRange appVersionRange of Just (AppCompatible v) -> pure v Nothing -> throwError $ ChatErrorRemoteCtrl $ RCEBadVersion $ maxVersion appVersionRange - pure (ctrlInfo, v) getHostAppInfo appVersion = do hostDeviceName <- chatReadVar localDeviceName encryptFiles <- chatReadVar encryptLocalFiles pure HostAppInfo {appVersion, deviceName = hostDeviceName, encoding = localEncoding, encryptFiles} +parseCtrlAppInfo :: ChatMonad m => JT.Value -> m CtrlAppInfo +parseCtrlAppInfo ctrlAppInfo = do + liftEitherWith (const $ ChatErrorRemoteCtrl RCEBadInvitation) $ JT.parseEither J.parseJSON ctrlAppInfo + handleRemoteCommand :: forall m. ChatMonad m => (ByteString -> m ChatResponse) -> RemoteCrypto -> TBQueue ChatResponse -> HTTP2Request -> m () handleRemoteCommand execChatCommand encryption remoteOutputQ HTTP2Request {request, reqBody, sendResponse} = do logDebug "handleRemoteCommand" @@ -654,7 +659,8 @@ cancelActiveRemoteCtrl sseq_ = handleAny (logError . tshow) $ do cancelRemoteCtrl :: Bool -> RemoteCtrlSession -> IO () cancelRemoteCtrl handlingError = \case RCSessionStarting -> pure () - RCSessionSearching {action} -> uninterruptibleCancel action + RCSessionSearching {action} -> + unless handlingError $ uninterruptibleCancel action RCSessionConnecting {rcsClient, rcsWaitSession} -> do unless handlingError $ uninterruptibleCancel rcsWaitSession cancelCtrlClient rcsClient diff --git a/src/Simplex/Chat/Terminal.hs b/src/Simplex/Chat/Terminal.hs index 221ad87a9..afc0ee4c3 100644 --- a/src/Simplex/Chat/Terminal.hs +++ b/src/Simplex/Chat/Terminal.hs @@ -35,7 +35,8 @@ terminalChatConfig = ntf = ["ntf://FB-Uop7RTaZZEG0ZLD2CIaTjsPh-Fw0zFAnb7QyA8Ks=@ntf2.simplex.im,ntg7jdjy2i3qbib3sykiho3enekwiaqg3icctliqhtqcg6jmoh6cxiad.onion"], xftp = defaultXFTPServers, netCfg = defaultNetworkConfig - } + }, + deviceNameForRemote = "SimpleX CLI" } simplexChatTerminal :: WithTerminal t => ChatConfig -> ChatOpts -> t -> IO () diff --git a/src/Simplex/Chat/View.hs b/src/Simplex/Chat/View.hs index 17851614f..5e3093332 100644 --- a/src/Simplex/Chat/View.hs +++ b/src/Simplex/Chat/View.hs @@ -284,11 +284,13 @@ responseToView hu@(currentRH, user_) ChatConfig {logLevel, showReactions, showRe rhi_ ] CRRemoteHostList hs -> viewRemoteHosts hs - CRRemoteHostStarted {remoteHost_, invitation} -> - [ maybe "new remote host started" (\RemoteHostInfo {remoteHostId = rhId} -> "remote host " <> sShow rhId <> " started") remoteHost_, + CRRemoteHostStarted {remoteHost_, invitation, ctrlPort} -> + [ plain $ maybe ("new remote host" <> started) (\RemoteHostInfo {remoteHostId = rhId} -> "remote host " <> show rhId <> started) remoteHost_, "Remote session invitation:", plain invitation ] + where + started = " started on port " <> ctrlPort CRRemoteHostSessionCode {remoteHost_, sessionCode} -> [ maybe "new remote host connecting" (\RemoteHostInfo {remoteHostId = rhId} -> "remote host " <> sShow rhId <> " connecting") remoteHost_, "Compare session code with host:", @@ -303,18 +305,16 @@ responseToView hu@(currentRH, user_) ChatConfig {logLevel, showReactions, showRe [plain $ "file " <> filePath <> " stored on remote host " <> show rhId] <> maybe [] ((: []) . plain . cryptoFileArgsStr testView) cfArgs_ CRRemoteCtrlList cs -> viewRemoteCtrls cs - CRRemoteCtrlFound rc -> - ["remote controller found:", viewRemoteCtrl rc] - CRRemoteCtrlConnecting {remoteCtrl_, ctrlAppInfo = CtrlAppInfo {deviceName, appVersionRange = AppVersionRange _ (AppVersion ctrlVersion)}, appVersion = AppVersion v} -> - [ (maybe "connecting new remote controller" (\RemoteCtrlInfo {remoteCtrlId} -> "connecting remote controller " <> sShow remoteCtrlId) remoteCtrl_ <> ": ") - <> (if T.null deviceName then "" else plain deviceName <> ", ") - <> ("v" <> plain (V.showVersion ctrlVersion) <> ctrlVersionInfo) + CRRemoteCtrlFound {remoteCtrl = RemoteCtrlInfo {remoteCtrlId, ctrlDeviceName}, ctrlAppInfo_, appVersion, compatible} -> + [ "remote controller " <> sShow remoteCtrlId <> " found: " + <> maybe (deviceName <> "not compatible") (\info -> viewRemoteCtrl info appVersion compatible) ctrlAppInfo_ ] where - ctrlVersionInfo - | ctrlVersion < v = " (older than this app - upgrade controller)" - | ctrlVersion > v = " (newer than this app - upgrade it)" - | otherwise = "" + deviceName = if T.null ctrlDeviceName then "" else plain ctrlDeviceName <> ", " + CRRemoteCtrlConnecting {remoteCtrl_, ctrlAppInfo, appVersion} -> + [ (maybe "connecting new remote controller" (\RemoteCtrlInfo {remoteCtrlId} -> "connecting remote controller " <> sShow remoteCtrlId) remoteCtrl_ <> ": ") + <> viewRemoteCtrl ctrlAppInfo appVersion True + ] CRRemoteCtrlSessionCode {remoteCtrl_, sessionCode} -> [ maybe "new remote controller connected" (\RemoteCtrlInfo {remoteCtrlId} -> "remote controller " <> sShow remoteCtrlId <> " connected") remoteCtrl_, "Compare session code with controller and use:", @@ -1728,10 +1728,16 @@ viewRemoteCtrls = \case RCSPendingConfirmation {sessionCode} -> " (pending confirmation, code: " <> sessionCode <> ")" RCSConnected _ -> " (connected)" --- TODO fingerprint, accepted? -viewRemoteCtrl :: RemoteCtrlInfo -> StyledString -viewRemoteCtrl RemoteCtrlInfo {remoteCtrlId, ctrlDeviceName} = - plain $ tshow remoteCtrlId <> ". " <> ctrlDeviceName +viewRemoteCtrl :: CtrlAppInfo -> AppVersion -> Bool -> StyledString +viewRemoteCtrl CtrlAppInfo {deviceName, appVersionRange = AppVersionRange _ (AppVersion ctrlVersion)} (AppVersion v) compatible = + (if T.null deviceName then "" else plain deviceName <> ", ") + <> ("v" <> plain (V.showVersion ctrlVersion) <> ctrlVersionInfo) + where + ctrlVersionInfo + | ctrlVersion < v = " (older than this app - upgrade controller" <> showCompatible <> ")" + | ctrlVersion > v = " (newer than this app - upgrade it" <> showCompatible <> ")" + | otherwise = "" + showCompatible = if compatible then "" else ", " <> bold' "not compatible" viewChatError :: ChatLogLevel -> Bool -> ChatError -> [StyledString] viewChatError logLevel testView = \case diff --git a/tests/ChatClient.hs b/tests/ChatClient.hs index bdad4a03e..8376a2f56 100644 --- a/tests/ChatClient.hs +++ b/tests/ChatClient.hs @@ -71,6 +71,7 @@ testOpts = tbqSize = 16, highlyAvailable = False }, + deviceName = Nothing, chatCmd = "", chatCmdDelay = 3, chatServerPort = Nothing, diff --git a/tests/RemoteTests.hs b/tests/RemoteTests.hs index b7b34999b..9f77245d9 100644 --- a/tests/RemoteTests.hs +++ b/tests/RemoteTests.hs @@ -116,7 +116,7 @@ remoteHandshakeRejectTest = testChat3 aliceProfile aliceDesktopProfile bobProfil mobileBob ##> "/set device name MobileBob" mobileBob <## "ok" desktop ##> "/start remote host 1" - desktop <## "remote host 1 started" + desktop <##. "remote host 1 started on port " desktop <## "Remote session invitation:" inv <- getTermLine desktop mobileBob ##> ("/connect remote ctrl " <> inv) @@ -425,7 +425,7 @@ startRemote mobile desktop = do mobile ##> "/set device name Mobile" mobile <## "ok" desktop ##> "/start remote host new" - desktop <## "new remote host started" + desktop <##. "new remote host started on port " desktop <## "Remote session invitation:" inv <- getTermLine desktop mobile ##> ("/connect remote ctrl " <> inv) @@ -440,7 +440,7 @@ startRemote mobile desktop = do startRemoteStored :: TestCC -> TestCC -> IO () startRemoteStored mobile desktop = do desktop ##> "/start remote host 1" - desktop <## "remote host 1 started" + desktop <##. "remote host 1 started on port " desktop <## "Remote session invitation:" inv <- getTermLine desktop mobile ##> ("/connect remote ctrl " <> inv) @@ -454,13 +454,12 @@ startRemoteStored mobile desktop = do startRemoteDiscover :: TestCC -> TestCC -> IO () startRemoteDiscover mobile desktop = do desktop ##> "/start remote host 1 multicast=on" - desktop <## "remote host 1 started" + desktop <##. "remote host 1 started on port " desktop <## "Remote session invitation:" _inv <- getTermLine desktop -- will use multicast instead mobile ##> "/find remote ctrl" mobile <## "ok" - mobile <## "remote controller found:" - mobile <## "1. My desktop" + mobile <## ("remote controller 1 found: My desktop, v" <> versionNumber) mobile ##> "/confirm remote ctrl 1" mobile <## ("connecting remote controller 1: My desktop, v" <> versionNumber)