Merge branch 'master' into master-ghc8107

This commit is contained in:
Evgeny Poberezkin 2023-11-23 16:22:46 +00:00
commit 1781495ee3
60 changed files with 534 additions and 290 deletions

View File

@ -26,7 +26,10 @@ struct SimpleXApp: App {
@State private var showInitializationView = false @State private var showInitializationView = false
init() { init() {
hs_init(0, nil) DispatchQueue.global(qos: .background).sync {
haskell_init()
// hs_init(0, nil)
}
UserDefaults.standard.register(defaults: appDefaults) UserDefaults.standard.register(defaults: appDefaults)
setGroupDefaults() setGroupDefaults()
registerGroupDefaults() registerGroupDefaults()

View File

@ -157,7 +157,7 @@ struct AddGroupMembersViewCommon: View {
private func rolePicker() -> some View { private func rolePicker() -> some View {
Picker("New member role", selection: $selectedRole) { Picker("New member role", selection: $selectedRole) {
ForEach(GroupMemberRole.allCases) { role in ForEach(GroupMemberRole.allCases) { role in
if role <= groupInfo.membership.memberRole { if role <= groupInfo.membership.memberRole && role != .author {
Text(role.text) Text(role.text)
} }
} }

View File

@ -382,7 +382,7 @@ struct ConnectDesktopView: View {
private func disconnectButton() -> some View { private func disconnectButton() -> some View {
Button { Button {
disconnectDesktop() disconnectDesktop(.dismiss)
} label: { } label: {
Label("Disconnect", systemImage: "multiply") Label("Disconnect", systemImage: "multiply")
} }

View File

@ -118,11 +118,8 @@
5CCB939C297EFCB100399E78 /* NavStackCompat.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CCB939B297EFCB100399E78 /* NavStackCompat.swift */; }; 5CCB939C297EFCB100399E78 /* NavStackCompat.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CCB939B297EFCB100399E78 /* NavStackCompat.swift */; };
5CCD403427A5F6DF00368C90 /* AddContactView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CCD403327A5F6DF00368C90 /* AddContactView.swift */; }; 5CCD403427A5F6DF00368C90 /* AddContactView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CCD403327A5F6DF00368C90 /* AddContactView.swift */; };
5CCD403727A5F9A200368C90 /* ScanToConnectView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CCD403627A5F9A200368C90 /* ScanToConnectView.swift */; }; 5CCD403727A5F9A200368C90 /* ScanToConnectView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CCD403627A5F9A200368C90 /* ScanToConnectView.swift */; };
5CDA5A2D2B04FE2D00A71D61 /* libgmp.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5CDA5A282B04FE2D00A71D61 /* libgmp.a */; }; 5CD67B8F2B0E858A00C510B1 /* hs_init.h in Headers */ = {isa = PBXBuildFile; fileRef = 5CD67B8D2B0E858A00C510B1 /* hs_init.h */; settings = {ATTRIBUTES = (Public, ); }; };
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 */; }; 5CD67B902B0E858A00C510B1 /* hs_init.c in Sources */ = {isa = PBXBuildFile; fileRef = 5CD67B8E2B0E858A00C510B1 /* hs_init.c */; };
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 */; };
5CDCAD482818589900503DA2 /* NotificationService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CDCAD472818589900503DA2 /* NotificationService.swift */; }; 5CDCAD482818589900503DA2 /* NotificationService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CDCAD472818589900503DA2 /* NotificationService.swift */; };
5CE2BA702845308900EC33A6 /* SimpleXChat.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 5CE2BA682845308900EC33A6 /* SimpleXChat.framework */; }; 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, ); }; }; 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 */; }; 5CEACCED27DEA495000BD591 /* MsgContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CEACCEC27DEA495000BD591 /* MsgContentView.swift */; };
5CEBD7462A5C0A8F00665FE2 /* KeyboardPadding.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CEBD7452A5C0A8F00665FE2 /* KeyboardPadding.swift */; }; 5CEBD7462A5C0A8F00665FE2 /* KeyboardPadding.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CEBD7452A5C0A8F00665FE2 /* KeyboardPadding.swift */; };
5CEBD7482A5F115D00665FE2 /* SetDeliveryReceiptsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CEBD7472A5F115D00665FE2 /* SetDeliveryReceiptsView.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 */; }; 5CFA59C42860BC6200863A68 /* MigrateToAppGroupView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CFA59C32860BC6200863A68 /* MigrateToAppGroupView.swift */; };
5CFA59D12864782E00863A68 /* ChatArchiveView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CFA59CF286477B400863A68 /* ChatArchiveView.swift */; }; 5CFA59D12864782E00863A68 /* ChatArchiveView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CFA59CF286477B400863A68 /* ChatArchiveView.swift */; };
5CFE0921282EEAF60002594B /* ZoomableScrollView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CFE0920282EEAF60002594B /* ZoomableScrollView.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 = "<group>"; }; 5CCB939B297EFCB100399E78 /* NavStackCompat.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NavStackCompat.swift; sourceTree = "<group>"; };
5CCD403327A5F6DF00368C90 /* AddContactView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddContactView.swift; sourceTree = "<group>"; }; 5CCD403327A5F6DF00368C90 /* AddContactView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddContactView.swift; sourceTree = "<group>"; };
5CCD403627A5F9A200368C90 /* ScanToConnectView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ScanToConnectView.swift; sourceTree = "<group>"; }; 5CCD403627A5F9A200368C90 /* ScanToConnectView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ScanToConnectView.swift; sourceTree = "<group>"; };
5CDA5A282B04FE2D00A71D61 /* libgmp.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libgmp.a; sourceTree = "<group>"; }; 5CD67B8D2B0E858A00C510B1 /* hs_init.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = hs_init.h; sourceTree = "<group>"; };
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 = "<group>"; }; 5CD67B8E2B0E858A00C510B1 /* hs_init.c */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.c; path = hs_init.c; sourceTree = "<group>"; };
5CDA5A2A2B04FE2D00A71D61 /* libffi.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libffi.a; sourceTree = "<group>"; };
5CDA5A2B2B04FE2D00A71D61 /* libgmpxx.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libgmpxx.a; sourceTree = "<group>"; };
5CDA5A2C2B04FE2D00A71D61 /* libHSsimplex-chat-5.4.0.3-rODxCBVsb2BkD1fnTAqXL.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-5.4.0.3-rODxCBVsb2BkD1fnTAqXL.a"; sourceTree = "<group>"; };
5CDCAD452818589900503DA2 /* SimpleX NSE.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = "SimpleX NSE.appex"; sourceTree = BUILT_PRODUCTS_DIR; }; 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 = "<group>"; }; 5CDCAD472818589900503DA2 /* NotificationService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationService.swift; sourceTree = "<group>"; };
5CDCAD492818589900503DA2 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; }; 5CDCAD492818589900503DA2 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
@ -435,6 +434,11 @@
5CEACCEC27DEA495000BD591 /* MsgContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MsgContentView.swift; sourceTree = "<group>"; }; 5CEACCEC27DEA495000BD591 /* MsgContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MsgContentView.swift; sourceTree = "<group>"; };
5CEBD7452A5C0A8F00665FE2 /* KeyboardPadding.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeyboardPadding.swift; sourceTree = "<group>"; }; 5CEBD7452A5C0A8F00665FE2 /* KeyboardPadding.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeyboardPadding.swift; sourceTree = "<group>"; };
5CEBD7472A5F115D00665FE2 /* SetDeliveryReceiptsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SetDeliveryReceiptsView.swift; sourceTree = "<group>"; }; 5CEBD7472A5F115D00665FE2 /* SetDeliveryReceiptsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SetDeliveryReceiptsView.swift; sourceTree = "<group>"; };
5CF077F62B0D60C000105111 /* libgmpxx.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libgmpxx.a; sourceTree = "<group>"; };
5CF077F72B0D60C000105111 /* libHSsimplex-chat-5.4.0.5-AEaxUB19STC3bOtqr9BLL2.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-5.4.0.5-AEaxUB19STC3bOtqr9BLL2.a"; sourceTree = "<group>"; };
5CF077F82B0D60C000105111 /* libgmp.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libgmp.a; sourceTree = "<group>"; };
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 = "<group>"; };
5CF077FA2B0D60C100105111 /* libffi.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libffi.a; sourceTree = "<group>"; };
5CFA59C32860BC6200863A68 /* MigrateToAppGroupView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MigrateToAppGroupView.swift; sourceTree = "<group>"; }; 5CFA59C32860BC6200863A68 /* MigrateToAppGroupView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MigrateToAppGroupView.swift; sourceTree = "<group>"; };
5CFA59CF286477B400863A68 /* ChatArchiveView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatArchiveView.swift; sourceTree = "<group>"; }; 5CFA59CF286477B400863A68 /* ChatArchiveView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatArchiveView.swift; sourceTree = "<group>"; };
5CFE0920282EEAF60002594B /* ZoomableScrollView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = ZoomableScrollView.swift; path = Shared/Views/ZoomableScrollView.swift; sourceTree = SOURCE_ROOT; }; 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; isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647; buildActionMask = 2147483647;
files = ( 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 */, 5CE2BA93284534B000EC33A6 /* libiconv.tbd in Frameworks */,
5CDA5A2D2B04FE2D00A71D61 /* libgmp.a in Frameworks */, 5CF077FF2B0D60C100105111 /* libffi.a in Frameworks */,
5CDA5A2E2B04FE2D00A71D61 /* libHSsimplex-chat-5.4.0.3-rODxCBVsb2BkD1fnTAqXL-ghc9.6.3.a in Frameworks */, 5CF077FC2B0D60C100105111 /* libHSsimplex-chat-5.4.0.5-AEaxUB19STC3bOtqr9BLL2.a in Frameworks */,
5CDA5A2F2B04FE2D00A71D61 /* libffi.a in Frameworks */,
5CE2BA94284534BB00EC33A6 /* libz.tbd in Frameworks */, 5CE2BA94284534BB00EC33A6 /* libz.tbd in Frameworks */,
5CDA5A312B04FE2D00A71D61 /* libHSsimplex-chat-5.4.0.3-rODxCBVsb2BkD1fnTAqXL.a in Frameworks */,
); );
runOnlyForDeploymentPostprocessing = 0; runOnlyForDeploymentPostprocessing = 0;
}; };
@ -585,11 +589,11 @@
5C764E5C279C70B7000C6508 /* Libraries */ = { 5C764E5C279C70B7000C6508 /* Libraries */ = {
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
5CDA5A2A2B04FE2D00A71D61 /* libffi.a */, 5CF077FA2B0D60C100105111 /* libffi.a */,
5CDA5A282B04FE2D00A71D61 /* libgmp.a */, 5CF077F82B0D60C000105111 /* libgmp.a */,
5CDA5A2B2B04FE2D00A71D61 /* libgmpxx.a */, 5CF077F62B0D60C000105111 /* libgmpxx.a */,
5CDA5A292B04FE2D00A71D61 /* libHSsimplex-chat-5.4.0.3-rODxCBVsb2BkD1fnTAqXL-ghc9.6.3.a */, 5CF077F92B0D60C100105111 /* libHSsimplex-chat-5.4.0.5-AEaxUB19STC3bOtqr9BLL2-ghc9.6.3.a */,
5CDA5A2C2B04FE2D00A71D61 /* libHSsimplex-chat-5.4.0.3-rODxCBVsb2BkD1fnTAqXL.a */, 5CF077F72B0D60C000105111 /* libHSsimplex-chat-5.4.0.5-AEaxUB19STC3bOtqr9BLL2.a */,
); );
path = Libraries; path = Libraries;
sourceTree = "<group>"; sourceTree = "<group>";
@ -818,6 +822,8 @@
5CE2BA8A2845332200EC33A6 /* SimpleX.h */, 5CE2BA8A2845332200EC33A6 /* SimpleX.h */,
5CE2BA78284530CC00EC33A6 /* SimpleXChat.docc */, 5CE2BA78284530CC00EC33A6 /* SimpleXChat.docc */,
5CE2BA96284537A800EC33A6 /* dummy.m */, 5CE2BA96284537A800EC33A6 /* dummy.m */,
5CD67B8D2B0E858A00C510B1 /* hs_init.h */,
5CD67B8E2B0E858A00C510B1 /* hs_init.c */,
); );
path = SimpleXChat; path = SimpleXChat;
sourceTree = "<group>"; sourceTree = "<group>";
@ -902,6 +908,7 @@
buildActionMask = 2147483647; buildActionMask = 2147483647;
files = ( files = (
5CE2BA77284530BF00EC33A6 /* SimpleXChat.h in Headers */, 5CE2BA77284530BF00EC33A6 /* SimpleXChat.h in Headers */,
5CD67B8F2B0E858A00C510B1 /* hs_init.h in Headers */,
5CE2BA952845354B00EC33A6 /* SimpleX.h in Headers */, 5CE2BA952845354B00EC33A6 /* SimpleX.h in Headers */,
); );
runOnlyForDeploymentPostprocessing = 0; runOnlyForDeploymentPostprocessing = 0;
@ -1272,6 +1279,7 @@
5C00168128C4FE760094D739 /* KeyChain.swift in Sources */, 5C00168128C4FE760094D739 /* KeyChain.swift in Sources */,
5CE2BA97284537A800EC33A6 /* dummy.m in Sources */, 5CE2BA97284537A800EC33A6 /* dummy.m in Sources */,
5CE2BA922845340900EC33A6 /* FileUtils.swift in Sources */, 5CE2BA922845340900EC33A6 /* FileUtils.swift in Sources */,
5CD67B902B0E858A00C510B1 /* hs_init.c in Sources */,
5CE2BA91284533A300EC33A6 /* Notifications.swift in Sources */, 5CE2BA91284533A300EC33A6 /* Notifications.swift in Sources */,
5CE2BA79284530CC00EC33A6 /* SimpleXChat.docc in Sources */, 5CE2BA79284530CC00EC33A6 /* SimpleXChat.docc in Sources */,
5CE2BA90284533A300EC33A6 /* JSON.swift in Sources */, 5CE2BA90284533A300EC33A6 /* JSON.swift in Sources */,
@ -1504,7 +1512,7 @@
CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_MODULES = YES;
CODE_SIGN_ENTITLEMENTS = "SimpleX (iOS).entitlements"; CODE_SIGN_ENTITLEMENTS = "SimpleX (iOS).entitlements";
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 181; CURRENT_PROJECT_VERSION = 182;
DEVELOPMENT_TEAM = 5NN7GUYB6T; DEVELOPMENT_TEAM = 5NN7GUYB6T;
ENABLE_BITCODE = NO; ENABLE_BITCODE = NO;
ENABLE_PREVIEWS = YES; ENABLE_PREVIEWS = YES;
@ -1547,7 +1555,7 @@
CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_MODULES = YES;
CODE_SIGN_ENTITLEMENTS = "SimpleX (iOS).entitlements"; CODE_SIGN_ENTITLEMENTS = "SimpleX (iOS).entitlements";
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 181; CURRENT_PROJECT_VERSION = 182;
DEVELOPMENT_TEAM = 5NN7GUYB6T; DEVELOPMENT_TEAM = 5NN7GUYB6T;
ENABLE_BITCODE = NO; ENABLE_BITCODE = NO;
ENABLE_PREVIEWS = YES; ENABLE_PREVIEWS = YES;
@ -1628,7 +1636,7 @@
CODE_SIGN_ENTITLEMENTS = "SimpleX NSE/SimpleX NSE.entitlements"; CODE_SIGN_ENTITLEMENTS = "SimpleX NSE/SimpleX NSE.entitlements";
CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 181; CURRENT_PROJECT_VERSION = 182;
DEVELOPMENT_TEAM = 5NN7GUYB6T; DEVELOPMENT_TEAM = 5NN7GUYB6T;
ENABLE_BITCODE = NO; ENABLE_BITCODE = NO;
GENERATE_INFOPLIST_FILE = YES; GENERATE_INFOPLIST_FILE = YES;
@ -1660,7 +1668,7 @@
CODE_SIGN_ENTITLEMENTS = "SimpleX NSE/SimpleX NSE.entitlements"; CODE_SIGN_ENTITLEMENTS = "SimpleX NSE/SimpleX NSE.entitlements";
CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 181; CURRENT_PROJECT_VERSION = 182;
DEVELOPMENT_TEAM = 5NN7GUYB6T; DEVELOPMENT_TEAM = 5NN7GUYB6T;
ENABLE_BITCODE = NO; ENABLE_BITCODE = NO;
GENERATE_INFOPLIST_FILE = YES; GENERATE_INFOPLIST_FILE = YES;
@ -1692,7 +1700,7 @@
APPLICATION_EXTENSION_API_ONLY = YES; APPLICATION_EXTENSION_API_ONLY = YES;
CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_MODULES = YES;
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 181; CURRENT_PROJECT_VERSION = 182;
DEFINES_MODULE = YES; DEFINES_MODULE = YES;
DEVELOPMENT_TEAM = 5NN7GUYB6T; DEVELOPMENT_TEAM = 5NN7GUYB6T;
DYLIB_COMPATIBILITY_VERSION = 1; DYLIB_COMPATIBILITY_VERSION = 1;
@ -1738,7 +1746,7 @@
APPLICATION_EXTENSION_API_ONLY = YES; APPLICATION_EXTENSION_API_ONLY = YES;
CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_MODULES = YES;
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 181; CURRENT_PROJECT_VERSION = 182;
DEFINES_MODULE = YES; DEFINES_MODULE = YES;
DEVELOPMENT_TEAM = 5NN7GUYB6T; DEVELOPMENT_TEAM = 5NN7GUYB6T;
DYLIB_COMPATIBILITY_VERSION = 1; DYLIB_COMPATIBILITY_VERSION = 1;

View File

@ -1847,7 +1847,7 @@ public struct GroupMember: Identifiable, Decodable {
public func canChangeRoleTo(groupInfo: GroupInfo) -> [GroupMemberRole]? { public func canChangeRoleTo(groupInfo: GroupInfo) -> [GroupMemberRole]? {
if !canBeRemoved(groupInfo: groupInfo) { return nil } if !canBeRemoved(groupInfo: groupInfo) { return nil }
let userRole = groupInfo.membership.memberRole let userRole = groupInfo.membership.memberRole
return GroupMemberRole.allCases.filter { $0 <= userRole } return GroupMemberRole.allCases.filter { $0 <= userRole && $0 != .author }
} }
public var memberIncognito: Bool { public var memberIncognito: Bool {
@ -1887,6 +1887,7 @@ public struct GroupMemberIds: Decodable {
public enum GroupMemberRole: String, Identifiable, CaseIterable, Comparable, Decodable { public enum GroupMemberRole: String, Identifiable, CaseIterable, Comparable, Decodable {
case observer = "observer" case observer = "observer"
case author = "author"
case member = "member" case member = "member"
case admin = "admin" case admin = "admin"
case owner = "owner" case owner = "owner"
@ -1896,6 +1897,7 @@ public enum GroupMemberRole: String, Identifiable, CaseIterable, Comparable, Dec
public var text: String { public var text: String {
switch self { switch self {
case .observer: return NSLocalizedString("observer", comment: "member role") 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 .member: return NSLocalizedString("member", comment: "member role")
case .admin: return NSLocalizedString("admin", comment: "member role") case .admin: return NSLocalizedString("admin", comment: "member role")
case .owner: return NSLocalizedString("owner", 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 { private var comparisonValue: Int {
switch self { switch self {
case .observer: return 0 case .observer: return 0
case .member: return 1 case .author: return 1
case .admin: return 2 case .member: return 2
case .owner: return 3 case .admin: return 3
case .owner: return 4
} }
} }

View File

@ -9,7 +9,7 @@
#ifndef SimpleX_h #ifndef SimpleX_h
#define SimpleX_h #define SimpleX_h
#endif /* SimpleX_h */ #include "hs_init.h"
extern void hs_init(int argc, char **argv[]); 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 // chat_decrypt_file returns null-terminated string with the error message
extern char *chat_decrypt_file(char *fromPath, char *key, char *nonce, char *toPath); extern char *chat_decrypt_file(char *fromPath, char *key, char *nonce, char *toPath);
#endif /* SimpleX_h */

View File

@ -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);
}

View File

@ -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 */

View File

@ -126,7 +126,7 @@ fun processIntent(intent: Intent?) {
when (intent?.action) { when (intent?.action) {
"android.intent.action.VIEW" -> { "android.intent.action.VIEW" -> {
val uri = intent.data val uri = intent.data
if (uri != null) connectIfOpenedViaUri(chatModel.remoteHostId, uri.toURI(), ChatModel) if (uri != null) connectIfOpenedViaUri(chatModel.remoteHostId(), uri.toURI(), ChatModel)
} }
} }
} }

View File

@ -57,7 +57,7 @@ class SimplexApp: Application(), LifecycleEventObserver {
updatingChatsMutex.withLock { updatingChatsMutex.withLock {
kotlin.runCatching { kotlin.runCatching {
val currentUserId = chatModel.currentUser.value?.userId 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 */ /** Active user can be changed in background while [ChatController.apiGetChats] is executing */
if (chatModel.currentUser.value?.userId == currentUserId) { if (chatModel.currentUser.value?.userId == currentUserId) {
val currentChatId = chatModel.chatId.value val currentChatId = chatModel.chatId.value

View File

@ -8,15 +8,12 @@ import android.provider.MediaStore
import android.webkit.MimeTypeMap import android.webkit.MimeTypeMap
import androidx.compose.ui.platform.ClipboardManager import androidx.compose.ui.platform.ClipboardManager
import androidx.compose.ui.platform.UriHandler 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.helpers.*
import chat.simplex.common.model.* import chat.simplex.common.model.*
import chat.simplex.common.views.helpers.* import chat.simplex.common.views.helpers.*
import java.io.BufferedOutputStream import java.io.BufferedOutputStream
import java.io.File import java.io.File
import chat.simplex.res.MR import chat.simplex.res.MR
import java.io.ByteArrayOutputStream
actual fun ClipboardManager.shareText(text: String) { actual fun ClipboardManager.shareText(text: String) {
val sendIntent: Intent = Intent().apply { val sendIntent: Intent = Intent().apply {

View File

@ -5,7 +5,7 @@
//#include <android/log.h> //#include <android/log.h>
// from the RTS // from the RTS
void hs_init(int * argc, char **argv[]); void hs_init_with_rtsopts(int * argc, char **argv[]);
// from android-support // from android-support
void setLineBuffering(void); void setLineBuffering(void);
@ -32,7 +32,17 @@ Java_chat_simplex_common_platform_CoreKt_pipeStdOutToSocket(JNIEnv *env, __unuse
JNIEXPORT void JNICALL JNIEXPORT void JNICALL
Java_chat_simplex_common_platform_CoreKt_initHS(__unused JNIEnv *env, __unused jclass clazz) { 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(); setLineBuffering();
} }

View File

@ -4,11 +4,14 @@
#include <stdint.h> #include <stdint.h>
// from the RTS // from the RTS
void hs_init(int * argc, char **argv[]); void hs_init_with_rtsopts(int * argc, char **argv[]);
JNIEXPORT void JNICALL JNIEXPORT void JNICALL
Java_chat_simplex_common_platform_CoreKt_initHS(JNIEnv *env, jclass clazz) { 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 // from simplex-chat

View File

@ -111,7 +111,8 @@ object ChatModel {
// remote controller // remote controller
val remoteHosts = mutableStateListOf<RemoteHostInfo>() val remoteHosts = mutableStateListOf<RemoteHostInfo>()
val currentRemoteHost = mutableStateOf<RemoteHostInfo?>(null) val currentRemoteHost = mutableStateOf<RemoteHostInfo?>(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<Pair<RemoteHostInfo?, RemoteHostSessionState>?>(null) val newRemoteHostPairing = mutableStateOf<Pair<RemoteHostInfo?, RemoteHostSessionState>?>(null)
val remoteCtrlSession = mutableStateOf<RemoteCtrlSession?>(null) val remoteCtrlSession = mutableStateOf<RemoteCtrlSession?>(null)
@ -1252,7 +1253,7 @@ data class GroupMember (
fun canChangeRoleTo(groupInfo: GroupInfo): List<GroupMemberRole>? = fun canChangeRoleTo(groupInfo: GroupInfo): List<GroupMemberRole>? =
if (!canBeRemoved(groupInfo)) null if (!canBeRemoved(groupInfo)) null
else groupInfo.membership.memberRole.let { userRole -> else groupInfo.membership.memberRole.let { userRole ->
GroupMemberRole.values().filter { it <= userRole } GroupMemberRole.values().filter { it <= userRole && it != GroupMemberRole.Author }
} }
val memberIncognito = memberProfile.profileId != memberContactProfileId val memberIncognito = memberProfile.profileId != memberContactProfileId
@ -1294,12 +1295,14 @@ data class GroupMemberIds(
@Serializable @Serializable
enum class GroupMemberRole(val memberRole: String) { enum class GroupMemberRole(val memberRole: String) {
@SerialName("observer") Observer("observer"), // order matters in comparisons @SerialName("observer") Observer("observer"), // order matters in comparisons
@SerialName("author") Author("author"),
@SerialName("member") Member("member"), @SerialName("member") Member("member"),
@SerialName("admin") Admin("admin"), @SerialName("admin") Admin("admin"),
@SerialName("owner") Owner("owner"); @SerialName("owner") Owner("owner");
val text: String get() = when (this) { val text: String get() = when (this) {
Observer -> generalGetString(MR.strings.group_member_role_observer) Observer -> generalGetString(MR.strings.group_member_role_observer)
Author -> generalGetString(MR.strings.group_member_role_author)
Member -> generalGetString(MR.strings.group_member_role_member) Member -> generalGetString(MR.strings.group_member_role_member)
Admin -> generalGetString(MR.strings.group_member_role_admin) Admin -> generalGetString(MR.strings.group_member_role_admin)
Owner -> generalGetString(MR.strings.group_member_role_owner) Owner -> generalGetString(MR.strings.group_member_role_owner)

View File

@ -4,7 +4,6 @@ import chat.simplex.common.views.helpers.*
import androidx.compose.runtime.* import androidx.compose.runtime.*
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.painter.Painter import androidx.compose.ui.graphics.painter.Painter
import chat.simplex.common.model.ChatModel.remoteHostId
import chat.simplex.common.model.ChatModel.updatingChatsMutex import chat.simplex.common.model.ChatModel.updatingChatsMutex
import dev.icerock.moko.resources.compose.painterResource import dev.icerock.moko.resources.compose.painterResource
import chat.simplex.common.platform.* import chat.simplex.common.platform.*
@ -1393,10 +1392,10 @@ object ChatController {
chatModel.remoteHosts.addAll(hosts) chatModel.remoteHosts.addAll(hosts)
} }
suspend fun startRemoteHost(rhId: Long?, multicast: Boolean = false): Pair<RemoteHostInfo?, String>? { suspend fun startRemoteHost(rhId: Long?, multicast: Boolean = false): Triple<RemoteHostInfo?, String, String>? {
val r = sendCmd(null, CC.StartRemoteHost(rhId, multicast)) val r = sendCmd(null, CC.StartRemoteHost(rhId, multicast))
if (r is CR.RemoteHostStarted) return r.remoteHost_ to r.invitation if (r is CR.RemoteHostStarted) return Triple(r.remoteHost_, r.invitation, r.ctrlPort)
apiErrorAlert("listRemoteHosts", generalGetString(MR.strings.error_alert_title), r) apiErrorAlert("startRemoteHost", generalGetString(MR.strings.error_alert_title), r)
return null return null
} }
@ -1626,7 +1625,7 @@ object ChatController {
|| (mc is MsgContent.MCVoice && file.fileSize <= MAX_VOICE_SIZE_AUTO_RCV && file.fileStatus !is CIFileStatus.RcvAccepted))) { || (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) } 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) ntfManager.notifyMessageReceived(r.user, cInfo, cItem)
} }
} }
@ -1845,8 +1844,14 @@ object ChatController {
switchUIRemoteHost(r.remoteHost.remoteHostId) switchUIRemoteHost(r.remoteHost.remoteHostId)
} }
is CR.RemoteHostStopped -> { is CR.RemoteHostStopped -> {
val disconnectedHost = chatModel.remoteHosts.firstOrNull { it.remoteHostId == r.remoteHostId_ }
chatModel.newRemoteHostPairing.value = null 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 chatModel.currentRemoteHost.value = null
switchUIRemoteHost(null) switchUIRemoteHost(null)
} }
@ -1908,7 +1913,7 @@ object ChatController {
} }
private fun activeUser(rhId: Long?, user: UserLike): Boolean = 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) { private fun withCall(r: CR, contact: Contact, perform: (Call) -> Unit) {
val call = chatModel.activeCall.value val call = chatModel.activeCall.value
@ -1968,6 +1973,9 @@ object ChatController {
suspend fun switchUIRemoteHost(rhId: Long?) { suspend fun switchUIRemoteHost(rhId: Long?) {
// TODO lock the switch so that two switches can't run concurrently? // TODO lock the switch so that two switches can't run concurrently?
chatModel.chatId.value = null chatModel.chatId.value = null
ModalManager.center.closeModals()
ModalManager.end.closeModals()
AlertManager.shared.alertViews.clear()
chatModel.currentRemoteHost.value = switchRemoteHost(rhId) chatModel.currentRemoteHost.value = switchRemoteHost(rhId)
reloadRemoteHosts() reloadRemoteHosts()
val user = apiGetActiveUser(rhId) val user = apiGetActiveUser(rhId)
@ -3766,7 +3774,7 @@ sealed class CR {
// remote events (desktop) // remote events (desktop)
@Serializable @SerialName("remoteHostList") class RemoteHostList(val remoteHosts: List<RemoteHostInfo>): CR() @Serializable @SerialName("remoteHostList") class RemoteHostList(val remoteHosts: List<RemoteHostInfo>): CR()
@Serializable @SerialName("currentRemoteHost") class CurrentRemoteHost(val remoteHost_: RemoteHostInfo?): 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("remoteHostSessionCode") class RemoteHostSessionCode(val remoteHost_: RemoteHostInfo?, val sessionCode: String): CR()
@Serializable @SerialName("newRemoteHost") class NewRemoteHost(val remoteHost: RemoteHostInfo): CR() @Serializable @SerialName("newRemoteHost") class NewRemoteHost(val remoteHost: RemoteHostInfo): CR()
@Serializable @SerialName("remoteHostConnected") class RemoteHostConnected(val remoteHost: RemoteHostInfo): CR() @Serializable @SerialName("remoteHostConnected") class RemoteHostConnected(val remoteHost: RemoteHostInfo): CR()

View File

@ -54,7 +54,7 @@ private fun sendCommand(chatModel: ChatModel, composeState: MutableState<Compose
withApi { withApi {
// show "in progress" // show "in progress"
// TODO show active remote host in chat console? // TODO show active remote host in chat console?
chatModel.controller.sendCmd(chatModel.remoteHostId, CC.Console(s)) chatModel.controller.sendCmd(chatModel.remoteHostId(), CC.Console(s))
composeState.value = ComposeState(useLinkPreviews = false) composeState.value = ComposeState(useLinkPreviews = false)
// hide "in progress" // hide "in progress"
} }

View File

@ -170,7 +170,7 @@ fun CreateFirstProfile(chatModel: ChatModel, close: () -> Unit) {
fun createProfileInProfiles(chatModel: ChatModel, displayName: String, close: () -> Unit) { fun createProfileInProfiles(chatModel: ChatModel, displayName: String, close: () -> Unit) {
withApi { withApi {
val rhId = chatModel.remoteHostId val rhId = chatModel.remoteHostId()
val user = chatModel.controller.apiCreateActiveUser( val user = chatModel.controller.apiCreateActiveUser(
rhId, Profile(displayName.trim(), "", null) rhId, Profile(displayName.trim(), "", null)
) ?: return@withApi ) ?: return@withApi

View File

@ -17,7 +17,7 @@ fun ScanCodeView(verifyCode: (String?, cb: (Boolean) -> Unit) -> Unit, close: ()
.fillMaxSize() .fillMaxSize()
.padding(horizontal = DEFAULT_PADDING) .padding(horizontal = DEFAULT_PADDING)
) { ) {
AppBarTitle(stringResource(MR.strings.scan_code), false) AppBarTitle(stringResource(MR.strings.scan_code), withPadding = false)
Box( Box(
Modifier Modifier
.fillMaxWidth() .fillMaxWidth()

View File

@ -63,7 +63,7 @@ private fun VerifyCodeLayout(
.verticalScroll(rememberScrollState()) .verticalScroll(rememberScrollState())
.padding(horizontal = DEFAULT_PADDING) .padding(horizontal = DEFAULT_PADDING)
) { ) {
AppBarTitle(stringResource(MR.strings.security_code), false) AppBarTitle(stringResource(MR.strings.security_code), withPadding = false)
val splitCode = splitToParts(connectionCode, 24) val splitCode = splitToParts(connectionCode, 24)
Row(Modifier.fillMaxWidth().padding(bottom = DEFAULT_PADDING_HALF), horizontalArrangement = Arrangement.Center) { Row(Modifier.fillMaxWidth().padding(bottom = DEFAULT_PADDING_HALF), horizontalArrangement = Arrangement.Center) {
if (connectionVerified) { if (connectionVerified) {

View File

@ -205,7 +205,9 @@ private fun RoleSelectionRow(groupInfo: GroupInfo, selectedRole: MutableState<Gr
verticalAlignment = Alignment.CenterVertically, verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.SpaceBetween horizontalArrangement = Arrangement.SpaceBetween
) { ) {
val values = GroupMemberRole.values().filter { it <= groupInfo.membership.memberRole }.map { it to it.text } val values = GroupMemberRole.values()
.filter { it <= groupInfo.membership.memberRole && it != GroupMemberRole.Author }
.map { it to it.text }
ExposedDropDownSettingRow( ExposedDropDownSettingRow(
generalGetString(MR.strings.new_member_role), generalGetString(MR.strings.new_member_role),
values, values,

View File

@ -129,7 +129,7 @@ fun directChatAction(rhId: Long?, contact: Contact, chatModel: ChatModel) {
fun groupChatAction(rhId: Long?, groupInfo: GroupInfo, chatModel: ChatModel, inProgress: MutableState<Boolean>? = null) { fun groupChatAction(rhId: Long?, groupInfo: GroupInfo, chatModel: ChatModel, inProgress: MutableState<Boolean>? = null) {
when (groupInfo.membership.memberStatus) { when (groupInfo.membership.memberStatus) {
GroupMemberStatus.MemInvited -> acceptGroupInvitationAlertDialog(rhId, groupInfo, chatModel, inProgress) GroupMemberStatus.MemInvited -> acceptGroupInvitationAlertDialog(rhId, groupInfo, chatModel, inProgress)
GroupMemberStatus.MemAccepted -> groupInvitationAcceptedAlert() GroupMemberStatus.MemAccepted -> groupInvitationAcceptedAlert(rhId)
else -> withBGApi { openChat(rhId, ChatInfo.Group(groupInfo), chatModel) } 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) 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) 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) chatModel.updateContact(rhId, contact)
AlertManager.shared.showAlertMsg( AlertManager.shared.showAlertMsg(
title = generalGetString(MR.strings.connection_request_sent), 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 return true
} }
@ -674,7 +677,8 @@ fun acceptGroupInvitationAlertDialog(rhId: Long?, groupInfo: GroupInfo, chatMode
} }
}, },
dismissText = generalGetString(MR.strings.delete_verb), 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( AlertManager.shared.showAlertMsg(
generalGetString(MR.strings.joining_group), 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),
) )
} }

View File

@ -53,7 +53,7 @@ fun ChatListView(chatModel: ChatModel, settingsState: SettingsViewState, setPerf
val url = chatModel.appOpenUrl.value val url = chatModel.appOpenUrl.value
if (url != null) { if (url != null) {
chatModel.appOpenUrl.value = null chatModel.appOpenUrl.value = null
connectIfOpenedViaUri(chatModel.remoteHostId, url, chatModel) connectIfOpenedViaUri(chatModel.remoteHostId(), url, chatModel)
} }
} }
if (appPlatform.isDesktop) { if (appPlatform.isDesktop) {

View File

@ -26,8 +26,7 @@ import chat.simplex.common.model.ChatModel.controller
import chat.simplex.common.ui.theme.* import chat.simplex.common.ui.theme.*
import chat.simplex.common.views.helpers.* import chat.simplex.common.views.helpers.*
import chat.simplex.common.platform.* import chat.simplex.common.platform.*
import chat.simplex.common.views.remote.ConnectDesktopView import chat.simplex.common.views.remote.*
import chat.simplex.common.views.remote.connectMobileDevice
import chat.simplex.res.MR import chat.simplex.res.MR
import dev.icerock.moko.resources.compose.stringResource import dev.icerock.moko.resources.compose.stringResource
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
@ -84,7 +83,7 @@ fun UserPicker(
.filter { it } .filter { it }
.collect { .collect {
try { 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 var same = users.size == updatedUsers.size
if (same) { if (same) {
for (i in 0 until minOf(users.size, updatedUsers.size)) { for (i in 0 until minOf(users.size, updatedUsers.size)) {
@ -213,6 +212,14 @@ fun UserPicker(
userPickerState.value = AnimatedViewState.GONE userPickerState.value = AnimatedViewState.GONE
} }
Divider(Modifier.requiredHeight(1.dp)) 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) { if (showSettings) {
SettingsPickerItem(settingsClicked) 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 @Composable
private fun SettingsPickerItem(onClick: () -> Unit) { private fun SettingsPickerItem(onClick: () -> Unit) {
SectionItemView(onClick, padding = PaddingValues(start = DEFAULT_PADDING + 7.dp, end = DEFAULT_PADDING), minHeight = 68.dp) { SectionItemView(onClick, padding = PaddingValues(start = DEFAULT_PADDING + 7.dp, end = DEFAULT_PADDING), minHeight = 68.dp) {

View File

@ -627,7 +627,7 @@ private fun afterSetCiTTL(
try { try {
updatingChatsMutex.withLock { updatingChatsMutex.withLock {
// this is using current remote host on purpose - if it changes during update, it will load correct chats // 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) m.updateChats(chats)
} }
} catch (e: Exception) { } catch (e: Exception) {

View File

@ -1,6 +1,5 @@
package chat.simplex.common.views.helpers package chat.simplex.common.views.helpers
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.* import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.CornerSize import androidx.compose.foundation.shape.CornerSize
import androidx.compose.foundation.shape.RoundedCornerShape 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.AnnotatedString
import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.* import androidx.compose.ui.unit.*
import chat.simplex.common.model.ChatModel
import chat.simplex.common.platform.* import chat.simplex.common.platform.*
import chat.simplex.common.ui.theme.* import chat.simplex.common.ui.theme.*
import chat.simplex.res.MR import chat.simplex.res.MR
import dev.icerock.moko.resources.StringResource import dev.icerock.moko.resources.StringResource
import dev.icerock.moko.resources.compose.painterResource
class AlertManager { class AlertManager {
var alertViews = mutableStateListOf<(@Composable () -> Unit)>() var alertViews = mutableStateListOf<(@Composable () -> Unit)>()
@ -40,8 +41,11 @@ class AlertManager {
AlertDialog( AlertDialog(
onDismissRequest = this::hideAlert, onDismissRequest = this::hideAlert,
title = alertTitle(title), title = alertTitle(title),
text = alertText(text), buttons = {
buttons = buttons, AlertContent(text, null, extraPadding = true) {
buttons()
}
},
shape = RoundedCornerShape(corner = CornerSize(25.dp)) shape = RoundedCornerShape(corner = CornerSize(25.dp))
) )
} }
@ -51,30 +55,16 @@ class AlertManager {
title: String, title: String,
text: AnnotatedString? = null, text: AnnotatedString? = null,
onDismissRequest: (() -> Unit)? = null, onDismissRequest: (() -> Unit)? = null,
hostDevice: Pair<Long?, String>? = null,
buttons: @Composable () -> Unit, buttons: @Composable () -> Unit,
) { ) {
showAlert { showAlert {
AlertDialog( AlertDialog(
onDismissRequest = { onDismissRequest?.invoke(); hideAlert() }, onDismissRequest = { onDismissRequest?.invoke(); hideAlert() },
title = { title = alertTitle(title),
Text(
title,
Modifier.fillMaxWidth().padding(vertical = DEFAULT_PADDING),
textAlign = TextAlign.Center,
fontSize = 20.sp
)
},
buttons = { buttons = {
Column( AlertContent(text, hostDevice, extraPadding = true) {
Modifier buttons()
.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()
}
} }
}, },
shape = RoundedCornerShape(corner = CornerSize(25.dp)) shape = RoundedCornerShape(corner = CornerSize(25.dp))
@ -90,30 +80,32 @@ class AlertManager {
dismissText: String = generalGetString(MR.strings.cancel_verb), dismissText: String = generalGetString(MR.strings.cancel_verb),
onDismiss: (() -> Unit)? = null, onDismiss: (() -> Unit)? = null,
onDismissRequest: (() -> Unit)? = null, onDismissRequest: (() -> Unit)? = null,
destructive: Boolean = false destructive: Boolean = false,
hostDevice: Pair<Long?, String>? = null,
) { ) {
showAlert { showAlert {
AlertDialog( AlertDialog(
onDismissRequest = { onDismissRequest?.invoke(); hideAlert() }, onDismissRequest = { onDismissRequest?.invoke(); hideAlert() },
title = alertTitle(title), title = alertTitle(title),
text = alertText(text),
buttons = { buttons = {
Row ( AlertContent(text, hostDevice, true) {
Modifier.fillMaxWidth().padding(horizontal = DEFAULT_PADDING).padding(bottom = DEFAULT_PADDING_HALF), Row(
horizontalArrangement = Arrangement.SpaceBetween Modifier.fillMaxWidth().padding(horizontal = DEFAULT_PADDING),
) { horizontalArrangement = Arrangement.SpaceBetween
val focusRequester = remember { FocusRequester() } ) {
LaunchedEffect(Unit) { val focusRequester = remember { FocusRequester() }
focusRequester.requestFocus() 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)) shape = RoundedCornerShape(corner = CornerSize(25.dp))
@ -135,20 +127,21 @@ class AlertManager {
AlertDialog( AlertDialog(
onDismissRequest = { onDismissRequest?.invoke(); hideAlert() }, onDismissRequest = { onDismissRequest?.invoke(); hideAlert() },
title = alertTitle(title), title = alertTitle(title),
text = alertText(text),
buttons = { buttons = {
Column( AlertContent(text, null) {
Modifier.fillMaxWidth().padding(horizontal = DEFAULT_PADDING_HALF).padding(top = DEFAULT_PADDING, bottom = 2.dp), Column(
horizontalAlignment = Alignment.CenterHorizontally Modifier.fillMaxWidth().padding(horizontal = DEFAULT_PADDING_HALF).padding(top = DEFAULT_PADDING, bottom = 2.dp),
) { horizontalAlignment = Alignment.CenterHorizontally
TextButton(onClick = { ) {
onDismiss?.invoke() TextButton(onClick = {
hideAlert() onDismiss?.invoke()
}) { Text(dismissText) } hideAlert()
TextButton(onClick = { }) { Text(dismissText) }
onConfirm?.invoke() TextButton(onClick = {
hideAlert() onConfirm?.invoke()
}) { Text(confirmText, color = if (destructive) Color.Red else Color.Unspecified, textAlign = TextAlign.End) } hideAlert()
}) { Text(confirmText, color = if (destructive) Color.Red else Color.Unspecified, textAlign = TextAlign.End) }
}
} }
}, },
shape = RoundedCornerShape(corner = CornerSize(25.dp)) shape = RoundedCornerShape(corner = CornerSize(25.dp))
@ -158,29 +151,31 @@ class AlertManager {
fun showAlertMsg( fun showAlertMsg(
title: String, text: String? = null, title: String, text: String? = null,
confirmText: String = generalGetString(MR.strings.ok) confirmText: String = generalGetString(MR.strings.ok),
hostDevice: Pair<Long?, String>? = null,
) { ) {
showAlert { showAlert {
AlertDialog( AlertDialog(
onDismissRequest = this::hideAlert, onDismissRequest = this::hideAlert,
title = alertTitle(title), title = alertTitle(title),
text = alertText(text),
buttons = { buttons = {
val focusRequester = remember { FocusRequester() } AlertContent(text, hostDevice, extraPadding = true) {
LaunchedEffect(Unit) { val focusRequester = remember { FocusRequester() }
focusRequester.requestFocus() LaunchedEffect(Unit) {
} focusRequester.requestFocus()
Row( }
Modifier.fillMaxWidth().padding(horizontal = DEFAULT_PADDING).padding(bottom = DEFAULT_PADDING_HALF), Row(
horizontalArrangement = Arrangement.Center Modifier.fillMaxWidth().padding(horizontal = DEFAULT_PADDING),
) { horizontalArrangement = Arrangement.Center
TextButton(
onClick = {
hideAlert()
},
Modifier.focusRequester(focusRequester)
) { ) {
Text(confirmText, color = Color.Unspecified) TextButton(
onClick = {
hideAlert()
},
Modifier.focusRequester(focusRequester)
) {
Text(confirmText, color = Color.Unspecified)
}
} }
} }
}, },
@ -191,16 +186,17 @@ class AlertManager {
fun showAlertMsgWithProgress( fun showAlertMsgWithProgress(
title: String, title: String,
text: String? = null text: String? = null,
) { ) {
showAlert { showAlert {
AlertDialog( AlertDialog(
onDismissRequest = this::hideAlert, onDismissRequest = this::hideAlert,
title = alertTitle(title), title = alertTitle(title),
text = alertText(text),
buttons = { buttons = {
Box(Modifier.fillMaxWidth().height(72.dp).padding(bottom = DEFAULT_PADDING * 2), contentAlignment = Alignment.Center) { AlertContent(text, null) {
CircularProgressIndicator(Modifier.size(36.dp).padding(4.dp), color = MaterialTheme.colors.secondary, strokeWidth = 3.dp) 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, title: StringResource,
text: StringResource? = null, text: StringResource? = null,
confirmText: StringResource = MR.strings.ok, confirmText: StringResource = MR.strings.ok,
) = showAlertMsg(generalGetString(title), if (text != null) generalGetString(text) else null, generalGetString(confirmText)) hostDevice: Pair<Long?, String>? = null,
) = showAlertMsg(generalGetString(title), if (text != null) generalGetString(text) else null, generalGetString(confirmText), hostDevice)
@Composable @Composable
fun showInView() { fun showInView() {
@ -234,18 +231,75 @@ private fun alertTitle(title: String): (@Composable () -> Unit)? {
} }
} }
private fun alertText(text: String?): (@Composable () -> Unit)? { @Composable
return if (text == null) { private fun AlertContent(text: String?, hostDevice: Pair<Long?, String>?, extraPadding: Boolean = false, content: @Composable (() -> Unit)) {
null Column(
} else { Modifier
({ .padding(bottom = if (appPlatform.isDesktop) DEFAULT_PADDING else DEFAULT_PADDING_HALF)
Text( ) {
escapedHtmlToAnnotatedString(text, LocalDensity.current), if (appPlatform.isDesktop) {
Modifier.fillMaxWidth(), HostDeviceTitle(hostDevice, extraPadding = extraPadding)
textAlign = TextAlign.Center, } else {
fontSize = 16.sp, Spacer(Modifier.size(DEFAULT_PADDING_HALF))
color = MaterialTheme.colors.secondary }
) 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<Long?, String>?, 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<Long?, String>? = 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<Long?, String>?, 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))
} }
} }

View File

@ -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 androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import chat.simplex.common.ui.theme.* import chat.simplex.common.ui.theme.*
import chat.simplex.res.MR
import dev.icerock.moko.resources.compose.painterResource
@Composable @Composable
fun CloseSheetBar(close: (() -> Unit)?, showClose: Boolean = true, endButtons: @Composable RowScope.() -> Unit = {}) { fun CloseSheetBar(close: (() -> Unit)?, showClose: Boolean = true, endButtons: @Composable RowScope.() -> Unit = {}) {
@ -47,23 +49,38 @@ fun CloseSheetBar(close: (() -> Unit)?, showClose: Boolean = true, endButtons: @
} }
@Composable @Composable
fun AppBarTitle(title: String, withPadding: Boolean = true, bottomPadding: Dp = DEFAULT_PADDING * 1.5f) { fun AppBarTitle(title: String, hostDevice: Pair<Long?, String>? = null, withPadding: Boolean = true, bottomPadding: Dp = DEFAULT_PADDING * 1.5f) {
val theme = CurrentColors.collectAsState() val theme = CurrentColors.collectAsState()
val titleColor = CurrentColors.collectAsState().value.appColors.title val titleColor = CurrentColors.collectAsState().value.appColors.title
val brush = if (theme.value.base == DefaultTheme.SIMPLEX) 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)) 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 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)) Brush.linearGradient(listOf(titleColor, titleColor), Offset(0f, Float.POSITIVE_INFINITY), Offset(Float.POSITIVE_INFINITY, 0f))
Text( Column {
title, Text(
Modifier title,
.fillMaxWidth() Modifier
.padding(bottom = bottomPadding, start = if (withPadding) DEFAULT_PADDING else 0.dp, end = if (withPadding) DEFAULT_PADDING else 0.dp,), .fillMaxWidth()
overflow = TextOverflow.Ellipsis, .padding(start = if (withPadding) DEFAULT_PADDING else 0.dp, end = if (withPadding) DEFAULT_PADDING else 0.dp,),
style = MaterialTheme.typography.h1.copy(brush = brush), overflow = TextOverflow.Ellipsis,
color = MaterialTheme.colors.primaryVariant, style = MaterialTheme.typography.h1.copy(brush = brush),
textAlign = TextAlign.Center color = MaterialTheme.colors.primaryVariant,
) textAlign = TextAlign.Center
)
if (hostDevice != null) {
HostDeviceTitle(hostDevice)
}
Spacer(Modifier.height(bottomPadding))
}
}
@Composable
private fun HostDeviceTitle(hostDevice: Pair<Long?, String>, 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/*( @Preview/*(

View File

@ -56,7 +56,7 @@ fun annotatedStringResource(id: StringResource): AnnotatedString {
fun annotatedStringResource(id: StringResource, vararg args: Any?): AnnotatedString { fun annotatedStringResource(id: StringResource, vararg args: Any?): AnnotatedString {
val density = LocalDensity.current val density = LocalDensity.current
return remember(id) { return remember(id) {
escapedHtmlToAnnotatedString(id.localized().format(args), density) escapedHtmlToAnnotatedString(id.localized().format(args = args), density)
} }
} }
@ -373,7 +373,7 @@ inline fun <reified T> serializableSaver(): Saver<T, *> = Saver(
fun UriHandler.openVerifiedSimplexUri(uri: String) { fun UriHandler.openVerifiedSimplexUri(uri: String) {
val URI = try { URI.create(uri) } catch (e: Exception) { null } val URI = try { URI.create(uri) } catch (e: Exception) { null }
if (URI != null) { if (URI != null) {
connectIfOpenedViaUri(chatModel.remoteHostId, URI, ChatModel) connectIfOpenedViaUri(chatModel.remoteHostId(), URI, ChatModel)
} }
} }

View File

@ -79,7 +79,7 @@ fun AddContactLayout(
.verticalScroll(rememberScrollState()), .verticalScroll(rememberScrollState()),
verticalArrangement = Arrangement.SpaceBetween, 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()) { SectionView(stringResource(MR.strings.one_time_link_short).uppercase()) {
if (connReq.isNotEmpty()) { if (connReq.isNotEmpty()) {

View File

@ -58,6 +58,7 @@ fun AddGroupView(chatModel: ChatModel, rh: RemoteHostInfo?, close: () -> Unit) {
} }
}, },
incognitoPref = chatModel.controller.appPrefs.incognito, incognitoPref = chatModel.controller.appPrefs.incognito,
rhId,
close close
) )
} }
@ -66,6 +67,7 @@ fun AddGroupView(chatModel: ChatModel, rh: RemoteHostInfo?, close: () -> Unit) {
fun AddGroupLayout( fun AddGroupLayout(
createGroup: (Boolean, GroupProfile) -> Unit, createGroup: (Boolean, GroupProfile) -> Unit,
incognitoPref: SharedPreference<Boolean>, incognitoPref: SharedPreference<Boolean>,
rhId: Long?,
close: () -> Unit close: () -> Unit
) { ) {
val bottomSheetModalState = rememberModalBottomSheetState(initialValue = ModalBottomSheetValue.Hidden) val bottomSheetModalState = rememberModalBottomSheetState(initialValue = ModalBottomSheetValue.Hidden)
@ -98,7 +100,7 @@ fun AddGroupLayout(
.verticalScroll(rememberScrollState()) .verticalScroll(rememberScrollState())
.padding(horizontal = DEFAULT_PADDING) .padding(horizontal = DEFAULT_PADDING)
) { ) {
AppBarTitle(stringResource(MR.strings.create_secret_group_title)) AppBarTitle(stringResource(MR.strings.create_secret_group_title), hostDevice(rhId))
Box( Box(
Modifier Modifier
.fillMaxWidth() .fillMaxWidth()
@ -174,7 +176,8 @@ fun PreviewAddGroupLayout() {
AddGroupLayout( AddGroupLayout(
createGroup = { _, _ -> }, createGroup = { _, _ -> },
incognitoPref = SharedPreference({ false }, {}), incognitoPref = SharedPreference({ false }, {}),
close = {} close = {},
rhId = null,
) )
} }
} }

View File

@ -56,6 +56,7 @@ fun ContactConnectionInfoView(
connReq = connReqInvitation, connReq = connReqInvitation,
contactConnection = contactConnection, contactConnection = contactConnection,
focusAlias = focusAlias, focusAlias = focusAlias,
rhId = rhId,
deleteConnection = { deleteContactConnectionAlert(rhId, contactConnection, chatModel, close) }, deleteConnection = { deleteContactConnectionAlert(rhId, contactConnection, chatModel, close) },
onLocalAliasChanged = { setContactAlias(rhId, contactConnection, it, chatModel) }, onLocalAliasChanged = { setContactAlias(rhId, contactConnection, it, chatModel) },
share = { if (connReqInvitation != null) clipboard.shareText(connReqInvitation) }, share = { if (connReqInvitation != null) clipboard.shareText(connReqInvitation) },
@ -80,6 +81,7 @@ private fun ContactConnectionInfoLayout(
connReq: String?, connReq: String?,
contactConnection: PendingContactConnection, contactConnection: PendingContactConnection,
focusAlias: Boolean, focusAlias: Boolean,
rhId: Long?,
deleteConnection: () -> Unit, deleteConnection: () -> Unit,
onLocalAliasChanged: (String) -> Unit, onLocalAliasChanged: (String) -> Unit,
share: () -> Unit, share: () -> Unit,
@ -114,7 +116,8 @@ private fun ContactConnectionInfoLayout(
stringResource( stringResource(
if (contactConnection.initiated) MR.strings.you_invited_a_contact if (contactConnection.initiated) MR.strings.you_invited_a_contact
else MR.strings.you_accepted_connection else MR.strings.you_accepted_connection
) ),
hostDevice(rhId)
) )
Text( Text(
stringResource( 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", 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(), contactConnection = PendingContactConnection.getSampleData(),
focusAlias = false, focusAlias = false,
rhId = null,
deleteConnection = {}, deleteConnection = {},
onLocalAliasChanged = {}, onLocalAliasChanged = {},
share = {}, share = {},

View File

@ -67,7 +67,7 @@ fun PasteToConnectLayout(
Modifier.verticalScroll(rememberScrollState()).padding(horizontal = DEFAULT_PADDING), Modifier.verticalScroll(rememberScrollState()).padding(horizontal = DEFAULT_PADDING),
verticalArrangement = Arrangement.SpaceBetween, 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)) { Box(Modifier.padding(top = DEFAULT_PADDING, bottom = 6.dp)) {
TextEditor( TextEditor(

View File

@ -4,7 +4,6 @@ import SectionBottomSpacer
import SectionItemView import SectionItemView
import SectionTextFooter import SectionTextFooter
import androidx.compose.desktop.ui.tooling.preview.Preview import androidx.compose.desktop.ui.tooling.preview.Preview
import chat.simplex.common.platform.Log
import androidx.compose.foundation.layout.* import androidx.compose.foundation.layout.*
import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll import androidx.compose.foundation.verticalScroll
@ -17,7 +16,7 @@ import androidx.compose.ui.text.style.TextAlign
import dev.icerock.moko.resources.compose.stringResource import dev.icerock.moko.resources.compose.stringResource
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import chat.simplex.common.model.* 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.ui.theme.*
import chat.simplex.common.views.chatlist.* import chat.simplex.common.views.chatlist.*
import chat.simplex.common.views.helpers.* 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), 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) } }, onConfirm = { withApi { connectViaUri(chatModel, rhId, uri, incognito, connectionPlan, close) } },
destructive = true, destructive = true,
hostDevice = hostDevice(rhId),
) )
} else { } else {
askCurrentOrIncognitoProfileAlert( askCurrentOrIncognitoProfileAlert(
@ -82,12 +82,14 @@ suspend fun planAndConnect(
openKnownContact(chatModel, rhId, close, contact) openKnownContact(chatModel, rhId, close, contact)
AlertManager.shared.showAlertMsg( AlertManager.shared.showAlertMsg(
generalGetString(MR.strings.contact_already_exists), 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 { } else {
AlertManager.shared.showAlertMsg( AlertManager.shared.showAlertMsg(
generalGetString(MR.strings.connect_plan_already_connecting), 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) openKnownContact(chatModel, rhId, close, contact)
AlertManager.shared.showAlertMsg( AlertManager.shared.showAlertMsg(
generalGetString(MR.strings.contact_already_exists), 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), 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) } }, onConfirm = { withApi { connectViaUri(chatModel, rhId, uri, incognito, connectionPlan, close) } },
destructive = true, destructive = true,
hostDevice = hostDevice(rhId),
) )
} else { } else {
askCurrentOrIncognitoProfileAlert( 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), 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) } }, onConfirm = { withApi { connectViaUri(chatModel, rhId, uri, incognito, connectionPlan, close) } },
destructive = true, destructive = true,
hostDevice = hostDevice(rhId),
) )
} else { } else {
askCurrentOrIncognitoProfileAlert( askCurrentOrIncognitoProfileAlert(
@ -159,7 +164,8 @@ suspend fun planAndConnect(
openKnownContact(chatModel, rhId, close, contact) openKnownContact(chatModel, rhId, close, contact)
AlertManager.shared.showAlertMsg( AlertManager.shared.showAlertMsg(
generalGetString(MR.strings.contact_already_exists), 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 -> { is ContactAddressPlan.Known -> {
@ -168,7 +174,8 @@ suspend fun planAndConnect(
openKnownContact(chatModel, rhId, close, contact) openKnownContact(chatModel, rhId, close, contact)
AlertManager.shared.showAlertMsg( AlertManager.shared.showAlertMsg(
generalGetString(MR.strings.contact_already_exists), 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 -> { is ContactAddressPlan.ContactViaAddress -> {
@ -190,7 +197,8 @@ suspend fun planAndConnect(
title = generalGetString(MR.strings.connect_via_group_link), title = generalGetString(MR.strings.connect_via_group_link),
text = generalGetString(MR.strings.you_will_join_group), 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), 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 { } else {
askCurrentOrIncognitoProfileAlert( 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), 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) } },
destructive = true, destructive = true,
hostDevice = hostDevice(rhId),
) )
} else { } else {
askCurrentOrIncognitoProfileAlert( askCurrentOrIncognitoProfileAlert(
@ -236,7 +245,8 @@ suspend fun planAndConnect(
} else { } else {
AlertManager.shared.showAlertMsg( AlertManager.shared.showAlertMsg(
generalGetString(MR.strings.connect_plan_already_joining_the_group), 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) openKnownGroup(chatModel, rhId, close, groupInfo)
AlertManager.shared.showAlertMsg( AlertManager.shared.showAlertMsg(
generalGetString(MR.strings.connect_plan_group_already_exists), 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.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.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) ConnectionLinkType.GROUP -> generalGetString(MR.strings.you_will_be_connected_when_group_host_device_is_online)
} },
hostDevice = hostDevice(rhId),
) )
} }
return r return r
@ -336,7 +348,8 @@ fun askCurrentOrIncognitoProfileAlert(
Text(stringResource(MR.strings.cancel_verb), Modifier.fillMaxWidth(), textAlign = TextAlign.Center, color = MaterialTheme.colors.primary) 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) 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), Modifier.verticalScroll(rememberScrollState()).padding(horizontal = DEFAULT_PADDING),
verticalArrangement = Arrangement.SpaceBetween verticalArrangement = Arrangement.SpaceBetween
) { ) {
AppBarTitle(stringResource(MR.strings.scan_QR_code), false) AppBarTitle(stringResource(MR.strings.scan_QR_code), hostDevice(rh?.remoteHostId), withPadding = false)
Box( Box(
Modifier Modifier
.fillMaxWidth() .fillMaxWidth()

View File

@ -26,7 +26,7 @@ fun HowItWorks(user: User?, onboardingStage: SharedPreference<OnboardingStage>?
.fillMaxWidth() .fillMaxWidth()
.padding(horizontal = DEFAULT_PADDING), .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.many_people_asked_how_can_it_deliver)
ReadableText(MR.strings.to_protect_privacy_simplex_has_ids_for_queues) ReadableText(MR.strings.to_protect_privacy_simplex_has_ids_for_queues)
ReadableText(MR.strings.you_control_servers_to_receive_your_contacts_to_send) ReadableText(MR.strings.you_control_servers_to_receive_your_contacts_to_send)

View File

@ -53,6 +53,7 @@ fun ConnectDesktopView(close: () -> Unit) {
ModalView(close = closeWithAlert) { ModalView(close = closeWithAlert) {
ConnectDesktopLayout( ConnectDesktopLayout(
deviceName = deviceName.value!!, deviceName = deviceName.value!!,
close
) )
} }
val ntfModeService = remember { chatModel.controller.appPrefs.notificationsMode.get() == NotificationsMode.SERVICE } val ntfModeService = remember { chatModel.controller.appPrefs.notificationsMode.get() == NotificationsMode.SERVICE }
@ -67,7 +68,7 @@ fun ConnectDesktopView(close: () -> Unit) {
} }
@Composable @Composable
private fun ConnectDesktopLayout(deviceName: String) { private fun ConnectDesktopLayout(deviceName: String, close: () -> Unit) {
val sessionAddress = remember { mutableStateOf("") } val sessionAddress = remember { mutableStateOf("") }
val remoteCtrls = remember { mutableStateListOf<RemoteCtrlInfo>() } val remoteCtrls = remember { mutableStateListOf<RemoteCtrlInfo>() }
val session = remember { chatModel.remoteCtrlSession }.value 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 { } else {
ConnectDesktop(deviceName, remoteCtrls, sessionAddress) ConnectDesktop(deviceName, remoteCtrls, sessionAddress)
@ -205,7 +206,7 @@ private fun CtrlDeviceVersionText(session: RemoteCtrlSession) {
} }
@Composable @Composable
private fun ActiveSession(session: RemoteCtrlSession, rc: RemoteCtrlInfo) { private fun ActiveSession(session: RemoteCtrlSession, rc: RemoteCtrlInfo, close: () -> Unit) {
AppBarTitle(stringResource(MR.strings.connected_to_desktop)) AppBarTitle(stringResource(MR.strings.connected_to_desktop))
SectionView(stringResource(MR.strings.connected_desktop).uppercase(), padding = PaddingValues(horizontal = DEFAULT_PADDING)) { SectionView(stringResource(MR.strings.connected_desktop).uppercase(), padding = PaddingValues(horizontal = DEFAULT_PADDING)) {
Text(rc.deviceViewName) Text(rc.deviceViewName)
@ -223,7 +224,7 @@ private fun ActiveSession(session: RemoteCtrlSession, rc: RemoteCtrlInfo) {
SectionSpacer() SectionSpacer()
SectionView { SectionView {
DisconnectButton(::disconnectDesktop) DisconnectButton { disconnectDesktop(close) }
} }
} }

View File

@ -36,12 +36,10 @@ import dev.icerock.moko.resources.compose.painterResource
import dev.icerock.moko.resources.compose.stringResource import dev.icerock.moko.resources.compose.stringResource
@Composable @Composable
fun ConnectMobileView( fun ConnectMobileView() {
m: ChatModel
) {
val connecting = rememberSaveable() { mutableStateOf(false) } val connecting = rememberSaveable() { mutableStateOf(false) }
val remoteHosts = remember { chatModel.remoteHosts } val remoteHosts = remember { chatModel.remoteHosts }
val deviceName = m.controller.appPrefs.deviceNameForRemoteAccess val deviceName = chatModel.controller.appPrefs.deviceNameForRemoteAccess
LaunchedEffect(Unit) { LaunchedEffect(Unit) {
controller.reloadRemoteHosts() controller.reloadRemoteHosts()
} }
@ -49,11 +47,11 @@ fun ConnectMobileView(
deviceName = remember { deviceName.state }, deviceName = remember { deviceName.state },
remoteHosts = remoteHosts, remoteHosts = remoteHosts,
connecting, connecting,
connectedHost = remember { m.currentRemoteHost }, connectedHost = remember { chatModel.currentRemoteHost },
updateDeviceName = { updateDeviceName = {
withBGApi { withBGApi {
if (it != "") { if (it != "") {
m.controller.setLocalDeviceName(it) chatModel.controller.setLocalDeviceName(it)
deviceName.set(it) deviceName.set(it)
} }
} }
@ -163,7 +161,8 @@ private fun ConnectMobileViewLayout(
title: String, title: String,
invitation: String?, invitation: String?,
deviceName: String?, deviceName: String?,
sessionCode: String? sessionCode: String?,
port: String?
) { ) {
Column( Column(
Modifier.fillMaxWidth().verticalScroll(rememberScrollState()), Modifier.fillMaxWidth().verticalScroll(rememberScrollState()),
@ -171,13 +170,14 @@ private fun ConnectMobileViewLayout(
) { ) {
AppBarTitle(title) AppBarTitle(title)
SectionView { SectionView {
if (invitation != null && sessionCode == null) { if (invitation != null && sessionCode == null && port != null) {
QRCode( QRCode(
invitation, Modifier invitation, Modifier
.padding(start = DEFAULT_PADDING, top = DEFAULT_PADDING_HALF, end = DEFAULT_PADDING, bottom = DEFAULT_PADDING_HALF) .padding(start = DEFAULT_PADDING, top = DEFAULT_PADDING_HALF, end = DEFAULT_PADDING, bottom = DEFAULT_PADDING_HALF)
.aspectRatio(1f) .aspectRatio(1f)
) )
SectionTextFooter(annotatedStringResource(MR.strings.open_on_mobile_and_scan_qr_code)) 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) { if (remember { controller.appPrefs.developerTools.state }.value) {
val clipboard = LocalClipboardManager.current val clipboard = LocalClipboardManager.current
@ -234,6 +234,7 @@ fun connectMobileDevice(rh: RemoteHostInfo, connecting: MutableState<Boolean>) {
private fun showAddingMobileDevice(connecting: MutableState<Boolean>) { private fun showAddingMobileDevice(connecting: MutableState<Boolean>) {
ModalManager.start.showModalCloseable { close -> ModalManager.start.showModalCloseable { close ->
val invitation = rememberSaveable { mutableStateOf<String?>(null) } val invitation = rememberSaveable { mutableStateOf<String?>(null) }
val port = rememberSaveable { mutableStateOf<String?>(null) }
val pairing = remember { chatModel.newRemoteHostPairing } val pairing = remember { chatModel.newRemoteHostPairing }
val sessionCode = when (val state = pairing.value?.second) { val sessionCode = when (val state = pairing.value?.second) {
is RemoteHostSessionState.PendingConfirmation -> state.sessionCode is RemoteHostSessionState.PendingConfirmation -> state.sessionCode
@ -249,7 +250,8 @@ private fun showAddingMobileDevice(connecting: MutableState<Boolean>) {
title = if (cachedSessionCode == null) stringResource(MR.strings.link_a_mobile) else stringResource(MR.strings.verify_connection), title = if (cachedSessionCode == null) stringResource(MR.strings.link_a_mobile) else stringResource(MR.strings.verify_connection),
invitation = invitation.value, invitation = invitation.value,
deviceName = remoteDeviceName, deviceName = remoteDeviceName,
sessionCode = cachedSessionCode sessionCode = cachedSessionCode,
port = port.value
) )
val oldRemoteHostId by remember { mutableStateOf(chatModel.currentRemoteHost.value?.remoteHostId) } val oldRemoteHostId by remember { mutableStateOf(chatModel.currentRemoteHost.value?.remoteHostId) }
LaunchedEffect(remember { chatModel.currentRemoteHost }.value) { LaunchedEffect(remember { chatModel.currentRemoteHost }.value) {
@ -268,6 +270,7 @@ private fun showAddingMobileDevice(connecting: MutableState<Boolean>) {
if (r != null) { if (r != null) {
connecting.value = true connecting.value = true
invitation.value = r.second invitation.value = r.second
port.value = r.third
} }
} }
onDispose { onDispose {
@ -286,6 +289,7 @@ private fun showConnectMobileDevice(rh: RemoteHostInfo, connecting: MutableState
ModalManager.start.showModalCloseable { close -> ModalManager.start.showModalCloseable { close ->
val pairing = remember { chatModel.newRemoteHostPairing } val pairing = remember { chatModel.newRemoteHostPairing }
val invitation = rememberSaveable { mutableStateOf<String?>(null) } val invitation = rememberSaveable { mutableStateOf<String?>(null) }
val port = rememberSaveable { mutableStateOf<String?>(null) }
val sessionCode = when (val state = pairing.value?.second) { val sessionCode = when (val state = pairing.value?.second) {
is RemoteHostSessionState.PendingConfirmation -> state.sessionCode is RemoteHostSessionState.PendingConfirmation -> state.sessionCode
else -> null else -> null
@ -300,6 +304,7 @@ private fun showConnectMobileDevice(rh: RemoteHostInfo, connecting: MutableState
invitation = invitation.value, invitation = invitation.value,
deviceName = pairing.value?.first?.hostDeviceName ?: rh.hostDeviceName, deviceName = pairing.value?.first?.hostDeviceName ?: rh.hostDeviceName,
sessionCode = cachedSessionCode, sessionCode = cachedSessionCode,
port = port.value
) )
var remoteHostId by rememberSaveable { mutableStateOf<Long?>(null) } var remoteHostId by rememberSaveable { mutableStateOf<Long?>(null) }
LaunchedEffect(Unit) { LaunchedEffect(Unit) {
@ -309,6 +314,7 @@ private fun showConnectMobileDevice(rh: RemoteHostInfo, connecting: MutableState
connecting.value = true connecting.value = true
remoteHostId = rh_?.remoteHostId remoteHostId = rh_?.remoteHostId
invitation.value = inv invitation.value = inv
port.value = r.third
} }
} }
LaunchedEffect(remember { chatModel.currentRemoteHost }.value) { LaunchedEffect(remember { chatModel.currentRemoteHost }.value) {
@ -345,7 +351,8 @@ private fun showConnectedMobileDevice(rh: RemoteHostInfo, disconnectHost: () ->
title = stringResource(MR.strings.connected_to_mobile), title = stringResource(MR.strings.connected_to_mobile),
invitation = null, invitation = null,
deviceName = rh.hostDeviceName, deviceName = rh.hostDeviceName,
sessionCode = sessionCode sessionCode = sessionCode,
port = null,
) )
Spacer(Modifier.height(DEFAULT_PADDING_HALF)) Spacer(Modifier.height(DEFAULT_PADDING_HALF))
SectionItemView(disconnectHost) { SectionItemView(disconnectHost) {

View File

@ -26,7 +26,7 @@ fun HelpLayout(userDisplayName: String) {
.verticalScroll(rememberScrollState()) .verticalScroll(rememberScrollState())
.padding(horizontal = DEFAULT_PADDING), .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() ChatHelpView()
} }
} }

View File

@ -29,13 +29,12 @@ import kotlinx.coroutines.launch
@Composable @Composable
fun ProtocolServersView(m: ChatModel, rhId: Long?, serverProtocol: ServerProtocol, close: () -> Unit) { fun ProtocolServersView(m: ChatModel, rhId: Long?, serverProtocol: ServerProtocol, close: () -> Unit) {
// TODO close if remote host changes var presetServers by remember(rhId) { mutableStateOf(emptyList<String>()) }
var presetServers by remember { mutableStateOf(emptyList<String>()) } var servers by remember(rhId) {
var servers by remember {
mutableStateOf(m.userSMPServersUnsaved.value ?: emptyList()) mutableStateOf(m.userSMPServersUnsaved.value ?: emptyList())
} }
val currServers = remember { mutableStateOf(servers) } val currServers = remember(rhId) { mutableStateOf(servers) }
val testing = rememberSaveable { mutableStateOf(false) } val testing = rememberSaveable(rhId) { mutableStateOf(false) }
val serversUnchanged = remember { derivedStateOf { servers == currServers.value || testing.value } } val serversUnchanged = remember { derivedStateOf { servers == currServers.value || testing.value } }
val allServersDisabled = remember { derivedStateOf { servers.all { !it.enabled } } } val allServersDisabled = remember { derivedStateOf { servers.all { !it.enabled } } }
val saveDisabled = remember { 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) val res = m.controller.getUserProtoServers(rhId, serverProtocol)
if (res != null) { if (res != null) {
currServers.value = res.protoServers currServers.value = res.protoServers

View File

@ -22,7 +22,7 @@ fun ScanProtocolServerLayout(rhId: Long?, onNext: (ServerCfg) -> Unit) {
.fillMaxSize() .fillMaxSize()
.padding(horizontal = DEFAULT_PADDING) .padding(horizontal = DEFAULT_PADDING)
) { ) {
AppBarTitle(stringResource(MR.strings.smp_servers_scan_qr), false) AppBarTitle(stringResource(MR.strings.smp_servers_scan_qr), withPadding = false)
Box( Box(
Modifier Modifier
.fillMaxWidth() .fillMaxWidth()

View File

@ -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) 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) ChatPreferencesItem(showCustomModal, stopped = stopped)
if (appPlatform.isDesktop) { 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 { } 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) SettingsActionItem(painterResource(MR.images.ic_desktop), stringResource(MR.strings.settings_section_title_use_from_desktop), showCustomModal{ it, close -> ConnectDesktopView(close) }, disabled = stopped, extraPadding = true)
} }

View File

@ -65,6 +65,7 @@ fun UserAddressView(
UserAddressLayout( UserAddressLayout(
userAddress = userAddress.value, userAddress = userAddress.value,
shareViaProfile, shareViaProfile,
rhId,
onCloseHandler, onCloseHandler,
createAddress = { createAddress = {
withApi { withApi {
@ -169,6 +170,7 @@ fun UserAddressView(
private fun UserAddressLayout( private fun UserAddressLayout(
userAddress: UserContactLinkRec?, userAddress: UserContactLinkRec?,
shareViaProfile: MutableState<Boolean>, shareViaProfile: MutableState<Boolean>,
rhId: Long?,
onCloseHandler: MutableState<(close: () -> Unit) -> Unit>, onCloseHandler: MutableState<(close: () -> Unit) -> Unit>,
createAddress: () -> Unit, createAddress: () -> Unit,
learnMore: () -> Unit, learnMore: () -> Unit,
@ -181,7 +183,7 @@ private fun UserAddressLayout(
Column( Column(
Modifier.verticalScroll(rememberScrollState()), Modifier.verticalScroll(rememberScrollState()),
) { ) {
AppBarTitle(stringResource(MR.strings.simplex_address), false) AppBarTitle(stringResource(MR.strings.simplex_address), hostDevice(rhId), withPadding = false)
Column( Column(
Modifier.fillMaxWidth().padding(bottom = DEFAULT_PADDING_HALF), Modifier.fillMaxWidth().padding(bottom = DEFAULT_PADDING_HALF),
horizontalAlignment = Alignment.CenterHorizontally, horizontalAlignment = Alignment.CenterHorizontally,
@ -438,6 +440,7 @@ fun PreviewUserAddressLayoutNoAddress() {
setProfileAddress = { _ -> }, setProfileAddress = { _ -> },
learnMore = {}, learnMore = {},
shareViaProfile = remember { mutableStateOf(false) }, shareViaProfile = remember { mutableStateOf(false) },
rhId = null,
onCloseHandler = remember { mutableStateOf({}) }, onCloseHandler = remember { mutableStateOf({}) },
sendEmail = {}, sendEmail = {},
) )
@ -471,6 +474,7 @@ fun PreviewUserAddressLayoutAddressCreated() {
setProfileAddress = { _ -> }, setProfileAddress = { _ -> },
learnMore = {}, learnMore = {},
shareViaProfile = remember { mutableStateOf(false) }, shareViaProfile = remember { mutableStateOf(false) },
rhId = null,
onCloseHandler = remember { mutableStateOf({}) }, onCloseHandler = remember { mutableStateOf({}) },
sendEmail = {}, sendEmail = {},
) )

View File

@ -18,7 +18,7 @@ fun VersionInfoView(info: CoreVersionInfo) {
Column( Column(
Modifier.padding(horizontal = DEFAULT_PADDING), 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) { if (appPlatform.isAndroid) {
Text(String.format(stringResource(MR.strings.app_version_name), BuildConfigCommon.ANDROID_VERSION_NAME)) 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)) Text(String.format(stringResource(MR.strings.app_version_code), BuildConfigCommon.ANDROID_VERSION_CODE))

View File

@ -1178,6 +1178,7 @@
<!-- GroupMemberRole --> <!-- GroupMemberRole -->
<string name="group_member_role_observer">observer</string> <string name="group_member_role_observer">observer</string>
<string name="group_member_role_author">author</string>
<string name="group_member_role_member">member</string> <string name="group_member_role_member">member</string>
<string name="group_member_role_admin">admin</string> <string name="group_member_role_admin">admin</string>
<string name="group_member_role_owner">owner</string> <string name="group_member_role_owner">owner</string>
@ -1608,16 +1609,6 @@
<string name="v5_3_simpler_incognito_mode_descr">Toggle incognito when connecting.</string> <string name="v5_3_simpler_incognito_mode_descr">Toggle incognito when connecting.</string>
<string name="v5_3_new_interface_languages">6 new interface languages</string> <string name="v5_3_new_interface_languages">6 new interface languages</string>
<string name="v5_3_new_interface_languages_descr">Arabic, Bulgarian, Finnish, Hebrew, Thai and Ukrainian - thanks to the users and Weblate.</string> <string name="v5_3_new_interface_languages_descr">Arabic, Bulgarian, Finnish, Hebrew, Thai and Ukrainian - thanks to the users and Weblate.</string>
<string name="v5_4_connect_desktop_mobile">Connect desktop and mobile!</string>
<string name="v5_4_connect_desktop_mobile_descr">Use your mobile app chat profile via desktop app.</string>
<string name="v5_4_group_improvements">Group improvements</string>
<string name="v5_4_group_improvements_descr">Create groups incognito, block group members, faster join via link, and more.</string>
<string name="v5_4_notify_contact_deletion">Notify about contact deletion</string>
<string name="v5_4_notify_contact_deletion_descr">You can optionally notify contacts when deleting them.</string>
<string name="v5_4_checking_simplex_links">Checking SimpleX links</string>
<string name="v5_4_checking_simplex_links_descr">Detection of previously used and your own SimpleX links.</string>
<string name="v5_4_spaces_in_profile_names">Spaces in profile names</string>
<string name="v5_4_spaces_in_profile_names_descr">You can now add spaces to your profile name.</string>
<string name="v5_4_link_mobile_desktop">Link mobile and desktop apps! 🔗</string> <string name="v5_4_link_mobile_desktop">Link mobile and desktop apps! 🔗</string>
<string name="v5_4_link_mobile_desktop_descr">Via secure quantum resistant protocol.</string> <string name="v5_4_link_mobile_desktop_descr">Via secure quantum resistant protocol.</string>
<string name="v5_4_better_groups">Better groups</string> <string name="v5_4_better_groups">Better groups</string>
@ -1668,9 +1659,11 @@
<string name="unlink_desktop_question">Unlink desktop?</string> <string name="unlink_desktop_question">Unlink desktop?</string>
<string name="unlink_desktop">Unlink</string> <string name="unlink_desktop">Unlink</string>
<string name="disconnect_remote_host">Disconnect</string> <string name="disconnect_remote_host">Disconnect</string>
<string name="remote_host_was_disconnected_toast"><![CDATA[Mobile <b>%s</b> was disconnected]]></string>
<string name="disconnect_desktop_question">Disconnect desktop?</string> <string name="disconnect_desktop_question">Disconnect desktop?</string>
<string name="only_one_device_can_work_at_the_same_time">Only one device can work at the same time</string> <string name="only_one_device_can_work_at_the_same_time">Only one device can work at the same time</string>
<string name="open_on_mobile_and_scan_qr_code"><![CDATA[Open <i>Use from desktop</i> in mobile app and scan QR code]]></string> <string name="open_on_mobile_and_scan_qr_code"><![CDATA[Open <i>Use from desktop</i> in mobile app and scan QR code.]]></string>
<string name="waiting_for_mobile_to_connect_on_port"><![CDATA[Waiting for mobile to connect on port <i>%s</i>]]></string>
<string name="bad_desktop_address">Bad desktop address</string> <string name="bad_desktop_address">Bad desktop address</string>
<string name="desktop_incompatible_version">Incompatible version</string> <string name="desktop_incompatible_version">Incompatible version</string>
<string name="desktop_app_version_is_incompatible">Desktop app version %s is not compatible with this app.</string> <string name="desktop_app_version_is_incompatible">Desktop app version %s is not compatible with this app.</string>

View File

@ -1465,7 +1465,7 @@
<string name="new_mobile_device">Nuovo dispositivo mobile</string> <string name="new_mobile_device">Nuovo dispositivo mobile</string>
<string name="desktop_address">Indirizzo desktop</string> <string name="desktop_address">Indirizzo desktop</string>
<string name="only_one_device_can_work_at_the_same_time">Solo un dispositivo può funzionare nello stesso momento</string> <string name="only_one_device_can_work_at_the_same_time">Solo un dispositivo può funzionare nello stesso momento</string>
<string name="open_on_mobile_and_scan_qr_code"><![CDATA[Apri <i>Usa dal desktop</i> nell\'app mobile e scansiona il codice QR]]></string> <string name="open_on_mobile_and_scan_qr_code"><![CDATA[Apri <i>Usa dal desktop</i> nell\'app mobile e scansiona il codice QR.]]></string>
<string name="desktop_incompatible_version">Versione incompatibile</string> <string name="desktop_incompatible_version">Versione incompatibile</string>
<string name="new_desktop"><![CDATA[<i>(nuovo)</i>]]></string> <string name="new_desktop"><![CDATA[<i>(nuovo)</i>]]></string>
<string name="unlink_desktop_question">Scollegare il desktop?</string> <string name="unlink_desktop_question">Scollegare il desktop?</string>

View File

@ -1463,7 +1463,7 @@
<string name="new_mobile_device">Nieuw mobiel apparaat</string> <string name="new_mobile_device">Nieuw mobiel apparaat</string>
<string name="desktop_address">Desktop adres</string> <string name="desktop_address">Desktop adres</string>
<string name="only_one_device_can_work_at_the_same_time">Er kan slechts één apparaat tegelijkertijd werken</string> <string name="only_one_device_can_work_at_the_same_time">Er kan slechts één apparaat tegelijkertijd werken</string>
<string name="open_on_mobile_and_scan_qr_code"><![CDATA[Open <i>Gebruik vanaf desktop</i> in de mobiele app en scan de QR-code]]></string> <string name="open_on_mobile_and_scan_qr_code"><![CDATA[Open <i>Gebruik vanaf desktop</i> in de mobiele app en scan de QR-code.]]></string>
<string name="desktop_incompatible_version">Incompatibele versie</string> <string name="desktop_incompatible_version">Incompatibele versie</string>
<string name="new_desktop"><![CDATA[<i>(nieuw)</i>]]></string> <string name="new_desktop"><![CDATA[<i>(nieuw)</i>]]></string>
<string name="unlink_desktop_question">Desktop ontkoppelen?</string> <string name="unlink_desktop_question">Desktop ontkoppelen?</string>

View File

@ -1465,7 +1465,7 @@
<string name="new_mobile_device">新移动设备</string> <string name="new_mobile_device">新移动设备</string>
<string name="desktop_address">桌面地址</string> <string name="desktop_address">桌面地址</string>
<string name="only_one_device_can_work_at_the_same_time">同一时刻只有一台设备可以工作</string> <string name="only_one_device_can_work_at_the_same_time">同一时刻只有一台设备可以工作</string>
<string name="open_on_mobile_and_scan_qr_code"><![CDATA[在移动应用中打开<i>从桌面使用</i>并扫描二维码]]></string> <string name="open_on_mobile_and_scan_qr_code"><![CDATA[在移动应用中打开<i>从桌面使用</i>并扫描二维码.]]></string>
<string name="desktop_incompatible_version">不兼容的版本</string> <string name="desktop_incompatible_version">不兼容的版本</string>
<string name="new_desktop"><![CDATA[<i>(新)</i>]]></string> <string name="new_desktop"><![CDATA[<i>(新)</i>]]></string>
<string name="unlink_desktop_question">取消链接桌面端?</string> <string name="unlink_desktop_question">取消链接桌面端?</string>

View File

@ -10,6 +10,7 @@ import androidx.compose.runtime.*
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.input.key.* import androidx.compose.ui.input.key.*
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.window.* import androidx.compose.ui.window.*
import chat.simplex.common.model.ChatController 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.ui.theme.SimpleXTheme
import chat.simplex.common.views.TerminalView import chat.simplex.common.views.TerminalView
import chat.simplex.common.views.helpers.FileDialogChooser import chat.simplex.common.views.helpers.FileDialogChooser
import chat.simplex.common.views.helpers.escapedHtmlToAnnotatedString
import chat.simplex.res.MR import chat.simplex.res.MR
import dev.icerock.moko.resources.compose.stringResource import dev.icerock.moko.resources.compose.stringResource
import kotlinx.coroutines.* import kotlinx.coroutines.*
@ -80,7 +82,7 @@ fun showApp() = application {
if (toast != null) { if (toast != null) {
Box(Modifier.fillMaxSize().padding(bottom = 20.dp), contentAlignment = Alignment.BottomCenter) { Box(Modifier.fillMaxSize().padding(bottom = 20.dp), contentAlignment = Alignment.BottomCenter) {
Text( Text(
toast.first, escapedHtmlToAnnotatedString(toast.first, LocalDensity.current),
Modifier.background(MaterialTheme.colors.primary, RoundedCornerShape(100)).padding(vertical = 5.dp, horizontal = 10.dp), Modifier.background(MaterialTheme.colors.primary, RoundedCornerShape(100)).padding(vertical = 5.dp, horizontal = 10.dp),
color = MaterialTheme.colors.onPrimary, color = MaterialTheme.colors.onPrimary,
style = MaterialTheme.typography.body1 style = MaterialTheme.typography.body1

View File

@ -4,7 +4,6 @@ import androidx.compose.ui.platform.ClipboardManager
import androidx.compose.ui.platform.UriHandler import androidx.compose.ui.platform.UriHandler
import androidx.compose.ui.text.AnnotatedString import androidx.compose.ui.text.AnnotatedString
import chat.simplex.common.model.* import chat.simplex.common.model.*
import chat.simplex.common.views.helpers.getAppFileUri
import chat.simplex.common.views.helpers.withApi import chat.simplex.common.views.helpers.withApi
import java.io.File import java.io.File
import java.net.URI import java.net.URI

View File

@ -19,7 +19,6 @@ import chat.simplex.res.MR
import dev.icerock.moko.resources.compose.painterResource import dev.icerock.moko.resources.compose.painterResource
import dev.icerock.moko.resources.compose.stringResource import dev.icerock.moko.resources.compose.stringResource
import java.io.File import java.io.File
import java.util.*
@Composable @Composable
actual fun ReactionIcon(text: String, fontSize: TextUnit) { actual fun ReactionIcon(text: String, fontSize: TextUnit) {

View File

@ -25,11 +25,11 @@ android.nonTransitiveRClass=true
android.enableJetifier=true android.enableJetifier=true
kotlin.mpp.androidSourceSetLayoutVersion=2 kotlin.mpp.androidSourceSetLayoutVersion=2
android.version_name=5.4-beta.3 android.version_name=5.4-beta.4
android.version_code=160 android.version_code=161
desktop.version_name=5.4-beta.3 desktop.version_name=5.4-beta.4
desktop.version_code=16 desktop.version_code=17
kotlin.version=1.8.20 kotlin.version=1.8.20
gradle.plugin.version=7.4.2 gradle.plugin.version=7.4.2

View File

@ -74,6 +74,7 @@ mkChatOpts :: BroadcastBotOpts -> ChatOpts
mkChatOpts BroadcastBotOpts {coreOptions} = mkChatOpts BroadcastBotOpts {coreOptions} =
ChatOpts ChatOpts
{ coreOptions, { coreOptions,
deviceName = Nothing,
chatCmd = "", chatCmd = "",
chatCmdDelay = 3, chatCmdDelay = 3,
chatServerPort = Nothing, chatServerPort = Nothing,

View File

@ -72,6 +72,7 @@ mkChatOpts :: DirectoryOpts -> ChatOpts
mkChatOpts DirectoryOpts {coreOptions} = mkChatOpts DirectoryOpts {coreOptions} =
ChatOpts ChatOpts
{ coreOptions, { coreOptions,
deviceName = Nothing,
chatCmd = "", chatCmd = "",
chatCmdDelay = 3, chatCmdDelay = 3,
chatServerPort = Nothing, chatServerPort = Nothing,

View File

@ -35,19 +35,20 @@ import Data.Either (fromRight, rights)
import Data.Fixed (div') import Data.Fixed (div')
import Data.Functor (($>)) import Data.Functor (($>))
import Data.Int (Int64) 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 Data.List.NonEmpty (NonEmpty, nonEmpty)
import qualified Data.List.NonEmpty as L import qualified Data.List.NonEmpty as L
import Data.Map.Strict (Map) import Data.Map.Strict (Map)
import qualified Data.Map.Strict as M import qualified Data.Map.Strict as M
import Data.Maybe (catMaybes, fromMaybe, isJust, isNothing, listToMaybe, mapMaybe, maybeToList) import Data.Maybe (catMaybes, fromMaybe, isJust, isNothing, listToMaybe, mapMaybe, maybeToList)
import Data.Ord (comparing)
import Data.Text (Text) import Data.Text (Text)
import qualified Data.Text as T import qualified Data.Text as T
import Data.Text.Encoding (decodeLatin1, encodeUtf8) import Data.Text.Encoding (decodeLatin1, encodeUtf8)
import Data.Time (NominalDiffTime, addUTCTime, defaultTimeLocale, formatTime) import Data.Time (NominalDiffTime, addUTCTime, defaultTimeLocale, formatTime)
import Data.Time.Clock (UTCTime, diffUTCTime, getCurrentTime, nominalDay, nominalDiffTimeToSeconds) import Data.Time.Clock (UTCTime, diffUTCTime, getCurrentTime, nominalDay, nominalDiffTimeToSeconds)
import Data.Time.Clock.System (SystemTime, systemToUTCTime) import Data.Time.Clock.System (SystemTime, systemToUTCTime)
import Data.Word (Word32) import Data.Word (Word16, Word32)
import qualified Database.SQLite.Simple as SQL import qualified Database.SQLite.Simple as SQL
import Simplex.Chat.Archive import Simplex.Chat.Archive
import Simplex.Chat.Call import Simplex.Chat.Call
@ -100,6 +101,7 @@ import qualified Simplex.Messaging.TMap as TM
import Simplex.Messaging.Transport.Client (defaultSocksProxy) import Simplex.Messaging.Transport.Client (defaultSocksProxy)
import Simplex.Messaging.Util import Simplex.Messaging.Util
import Simplex.Messaging.Version import Simplex.Messaging.Version
import Simplex.RemoteControl.Invitation (RCSignedInvitation (..), RCInvitation (..))
import System.Exit (ExitCode, exitFailure, exitSuccess) import System.Exit (ExitCode, exitFailure, exitSuccess)
import System.FilePath (takeFileName, (</>)) import System.FilePath (takeFileName, (</>))
import System.IO (Handle, IOMode (..), SeekMode (..), hFlush, stdout) import System.IO (Handle, IOMode (..), SeekMode (..), hFlush, stdout)
@ -147,7 +149,8 @@ defaultChatConfig =
cleanupManagerStepDelay = 3 * 1000000, -- 3 seconds cleanupManagerStepDelay = 3 * 1000000, -- 3 seconds
ciExpirationInterval = 30 * 60 * 1000000, -- 30 minutes ciExpirationInterval = 30 * 60 * 1000000, -- 30 minutes
coreApi = False, coreApi = False,
highlyAvailable = False highlyAvailable = False,
deviceNameForRemote = ""
} }
_defaultSMPServers :: NonEmpty SMPServerWithAuth _defaultSMPServers :: NonEmpty SMPServerWithAuth
@ -191,7 +194,7 @@ createChatDatabase filePrefix key confirmMigrations = runExceptT $ do
pure ChatDatabase {chatStore, agentStore} pure ChatDatabase {chatStore, agentStore}
newChatController :: ChatDatabase -> Maybe User -> ChatConfig -> ChatOpts -> IO ChatController 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} 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} config = cfg {logLevel, showReactions, tbqSize, subscriptionEvents = logConnections, hostEvents = logServerHosts, defaultServers = configServers, inlineFiles = inlineFiles', autoAcceptFileSize, highlyAvailable}
firstTime = dbNew chatStore firstTime = dbNew chatStore
@ -209,7 +212,7 @@ newChatController ChatDatabase {chatStore, agentStore} user cfg@ChatConfig {agen
sndFiles <- newTVarIO M.empty sndFiles <- newTVarIO M.empty
rcvFiles <- newTVarIO M.empty rcvFiles <- newTVarIO M.empty
currentCalls <- atomically TM.empty currentCalls <- atomically TM.empty
localDeviceName <- newTVarIO "" -- TODO set in config localDeviceName <- newTVarIO $ fromMaybe deviceNameForRemote deviceName
multicastSubscribers <- newTMVarIO 0 multicastSubscribers <- newTMVarIO 0
remoteSessionSeq <- newTVarIO 0 remoteSessionSeq <- newTVarIO 0
remoteHostSessions <- atomically TM.empty remoteHostSessions <- atomically TM.empty
@ -1958,8 +1961,8 @@ processChatCommand = \case
ListRemoteHosts -> withUser_ $ CRRemoteHostList <$> listRemoteHosts ListRemoteHosts -> withUser_ $ CRRemoteHostList <$> listRemoteHosts
SwitchRemoteHost rh_ -> withUser_ $ CRCurrentRemoteHost <$> switchRemoteHost rh_ SwitchRemoteHost rh_ -> withUser_ $ CRCurrentRemoteHost <$> switchRemoteHost rh_
StartRemoteHost rh_ -> withUser_ $ do StartRemoteHost rh_ -> withUser_ $ do
(remoteHost_, inv) <- startRemoteHost rh_ (remoteHost_, inv@RCSignedInvitation {invitation = RCInvitation {port}}) <- startRemoteHost rh_
pure CRRemoteHostStarted {remoteHost_, invitation = decodeLatin1 $ strEncode inv} pure CRRemoteHostStarted {remoteHost_, invitation = decodeLatin1 $ strEncode inv, ctrlPort = show port}
StopRemoteHost rh_ -> withUser_ $ closeRemoteHost rh_ >> ok_ StopRemoteHost rh_ -> withUser_ $ closeRemoteHost rh_ >> ok_
DeleteRemoteHost rh -> withUser_ $ deleteRemoteHost rh >> ok_ DeleteRemoteHost rh -> withUser_ $ deleteRemoteHost rh >> ok_
StoreRemoteFile rh encrypted_ localPath -> withUser_ $ CRRemoteFileStored rh <$> storeRemoteFile rh encrypted_ localPath 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 let connIds = map aConnId' pcs
pure (connIds, M.fromList $ zip connIds pcs) pure (connIds, M.fromList $ zip connIds pcs)
contactSubsToView :: Map ConnId (Either AgentErrorType ()) -> Map ConnId Contact -> Bool -> m () 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 where
notifyCLI = do notifyCLI = do
let cRs = resultsFor rs cts let cRs = resultsFor rs cts
cErrors = sortOn (\(Contact {localDisplayName = n}, _) -> n) $ filterErrors cRs cErrors = sortOn (\(Contact {localDisplayName = n}, _) -> n) $ filterErrors cRs
toView . CRContactSubSummary user $ map (uncurry ContactSubStatus) cRs toView . CRContactSubSummary user $ map (uncurry ContactSubStatus) cRs
when ce $ mapM_ (toView . uncurry (CRContactSubError user)) cErrors when ce $ mapM_ (toView . uncurry (CRContactSubError user)) cErrors
notifyAPI = do notifyAPI = toView . CRNetworkStatuses (Just user) . map (uncurry ConnNetworkStatus)
let statuses = M.foldrWithKey' addStatus [] cts statuses = M.foldrWithKey' addStatus [] cts
chatModifyVar connNetworkStatuses $ M.union (M.fromList statuses)
toView $ CRNetworkStatuses (Just user) $ map (uncurry ConnNetworkStatus) statuses
where where
addStatus :: ConnId -> Contact -> [(AgentConnId, NetworkStatus)] -> [(AgentConnId, NetworkStatus)] addStatus :: ConnId -> Contact -> [(AgentConnId, NetworkStatus)] -> [(AgentConnId, NetworkStatus)]
addStatus _ Contact {activeConn = Nothing} nss = nss addStatus _ Contact {activeConn = Nothing} nss = nss
@ -3076,12 +3079,12 @@ processAgentMessageNoConn = \case
where where
hostEvent :: ChatResponse -> m () hostEvent :: ChatResponse -> m ()
hostEvent = whenM (asks $ hostEvents . config) . toView 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 where
notifyAPI = do connIds = map AgentConnId conns
let connIds = map AgentConnId conns notifyAPI = toView . CRNetworkStatus nsStatus
chatModifyVar connNetworkStatuses $ \m -> foldl' (\m' cId -> M.insert cId nsStatus m') m connIds
toView $ CRNetworkStatus nsStatus connIds
notifyCLI = do notifyCLI = do
cs <- withStore' (`getConnectionsContacts` conns) cs <- withStore' (`getConnectionsContacts` conns)
toView $ event srv cs toView $ event srv cs
@ -3544,7 +3547,8 @@ processAgentMessageConn user@User {userId} corrId agentConnId agentMessage = do
members <- withStore' $ \db -> getGroupMembers db user gInfo members <- withStore' $ \db -> getGroupMembers db user gInfo
intros <- withStore' $ \db -> createIntroductions db members m intros <- withStore' $ \db -> createIntroductions db members m
void . sendGroupMessage user gInfo members . XGrpMemNew $ memberInfo 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)) processIntro intro `catchChatError` (toView . CRChatError (Just user))
where where
sendXGrpLinkMem = do sendXGrpLinkMem = do
@ -5517,7 +5521,8 @@ sendGroupMessage' :: forall e m. (MsgEncodingI e, ChatMonad m) => User -> [Group
sendGroupMessage' user members chatMsgEvent groupId introId_ postDeliver = do sendGroupMessage' user members chatMsgEvent groupId introId_ postDeliver = do
msg <- createSndMessage chatMsgEvent (GroupId groupId) msg <- createSndMessage chatMsgEvent (GroupId groupId)
-- TODO collect failed deliveries into a single error -- 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) messageMember m msg `catchChatError` (\e -> toView (CRChatError (Just user) e) $> Nothing)
let sentToMembers = catMaybes rs let sentToMembers = catMaybes rs
pure (msg, sentToMembers) pure (msg, sentToMembers)
@ -5555,6 +5560,15 @@ sendGroupMessage' user members chatMsgEvent groupId introId_ postDeliver = do
XGrpMsgForward {} -> True XGrpMsgForward {} -> True
_ -> False _ -> 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 :: ChatMonad m => User -> GroupMember -> Connection -> m ()
sendPendingGroupMessages user GroupMember {groupMemberId, localDisplayName} conn = do sendPendingGroupMessages user GroupMember {groupMemberId, localDisplayName} conn = do
pendingMessages <- withStore' $ \db -> getPendingGroupMessages db groupMemberId pendingMessages <- withStore' $ \db -> getPendingGroupMessages db groupMemberId

View File

@ -135,7 +135,8 @@ data ChatConfig = ChatConfig
cleanupManagerStepDelay :: Int64, cleanupManagerStepDelay :: Int64,
ciExpirationInterval :: Int64, -- microseconds ciExpirationInterval :: Int64, -- microseconds
coreApi :: Bool, coreApi :: Bool,
highlyAvailable :: Bool highlyAvailable :: Bool,
deviceNameForRemote :: Text
} }
data DefaultAgentServers = DefaultAgentServers data DefaultAgentServers = DefaultAgentServers
@ -657,14 +658,14 @@ data ChatResponse
| CRContactConnectionDeleted {user :: User, connection :: PendingContactConnection} | CRContactConnectionDeleted {user :: User, connection :: PendingContactConnection}
| CRRemoteHostList {remoteHosts :: [RemoteHostInfo]} | CRRemoteHostList {remoteHosts :: [RemoteHostInfo]}
| CRCurrentRemoteHost {remoteHost_ :: Maybe 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} | CRRemoteHostSessionCode {remoteHost_ :: Maybe RemoteHostInfo, sessionCode :: Text}
| CRNewRemoteHost {remoteHost :: RemoteHostInfo} | CRNewRemoteHost {remoteHost :: RemoteHostInfo}
| CRRemoteHostConnected {remoteHost :: RemoteHostInfo} | CRRemoteHostConnected {remoteHost :: RemoteHostInfo}
| CRRemoteHostStopped {remoteHostId_ :: Maybe RemoteHostId} | CRRemoteHostStopped {remoteHostId_ :: Maybe RemoteHostId}
| CRRemoteFileStored {remoteHostId :: RemoteHostId, remoteFileSource :: CryptoFile} | CRRemoteFileStored {remoteHostId :: RemoteHostId, remoteFileSource :: CryptoFile}
| CRRemoteCtrlList {remoteCtrls :: [RemoteCtrlInfo]} | 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} | CRRemoteCtrlConnecting {remoteCtrl_ :: Maybe RemoteCtrlInfo, ctrlAppInfo :: CtrlAppInfo, appVersion :: AppVersion}
| CRRemoteCtrlSessionCode {remoteCtrl_ :: Maybe RemoteCtrlInfo, sessionCode :: Text} | CRRemoteCtrlSessionCode {remoteCtrl_ :: Maybe RemoteCtrlInfo, sessionCode :: Text}
| CRRemoteCtrlConnected {remoteCtrl :: RemoteCtrlInfo} | CRRemoteCtrlConnected {remoteCtrl :: RemoteCtrlInfo}
@ -702,7 +703,7 @@ allowRemoteEvent = \case
CRRemoteHostStopped _ -> False CRRemoteHostStopped _ -> False
CRRemoteFileStored {} -> False CRRemoteFileStored {} -> False
CRRemoteCtrlList _ -> False CRRemoteCtrlList _ -> False
CRRemoteCtrlFound _ -> False CRRemoteCtrlFound {} -> False
CRRemoteCtrlConnecting {} -> False CRRemoteCtrlConnecting {} -> False
CRRemoteCtrlSessionCode {} -> False CRRemoteCtrlSessionCode {} -> False
CRRemoteCtrlConnected _ -> False CRRemoteCtrlConnected _ -> False

View File

@ -181,6 +181,7 @@ mobileChatOpts dbFilePrefix dbKey =
tbqSize = 1024, tbqSize = 1024,
highlyAvailable = False highlyAvailable = False
}, },
deviceName = Nothing,
chatCmd = "", chatCmd = "",
chatCmdDelay = 3, chatCmdDelay = 3,
chatServerPort = Nothing, chatServerPort = Nothing,
@ -197,7 +198,8 @@ defaultMobileConfig =
defaultChatConfig defaultChatConfig
{ confirmMigrations = MCYesUp, { confirmMigrations = MCYesUp,
logLevel = CLLError, logLevel = CLLError,
coreApi = True coreApi = True,
deviceNameForRemote = "Mobile"
} }
getActiveUser_ :: SQLiteStore -> IO (Maybe User) getActiveUser_ :: SQLiteStore -> IO (Maybe User)

View File

@ -19,6 +19,7 @@ where
import Control.Logger.Simple (LogLevel (..)) import Control.Logger.Simple (LogLevel (..))
import qualified Data.Attoparsec.ByteString.Char8 as A import qualified Data.Attoparsec.ByteString.Char8 as A
import qualified Data.ByteString.Char8 as B import qualified Data.ByteString.Char8 as B
import Data.Text (Text)
import Numeric.Natural (Natural) import Numeric.Natural (Natural)
import Options.Applicative import Options.Applicative
import Simplex.Chat.Controller (ChatLogLevel (..), updateStr, versionNumber, versionString) import Simplex.Chat.Controller (ChatLogLevel (..), updateStr, versionNumber, versionString)
@ -32,6 +33,7 @@ import System.FilePath (combine)
data ChatOpts = ChatOpts data ChatOpts = ChatOpts
{ coreOptions :: CoreChatOpts, { coreOptions :: CoreChatOpts,
deviceName :: Maybe Text,
chatCmd :: String, chatCmd :: String,
chatCmdDelay :: Int, chatCmdDelay :: Int,
chatServerPort :: Maybe String, chatServerPort :: Maybe String,
@ -200,6 +202,14 @@ coreChatOptsP appDir defaultDbFileName = do
chatOptsP :: FilePath -> FilePath -> Parser ChatOpts chatOptsP :: FilePath -> FilePath -> Parser ChatOpts
chatOptsP appDir defaultDbFileName = do chatOptsP appDir defaultDbFileName = do
coreOptions <- coreChatOptsP appDir defaultDbFileName 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 <- chatCmd <-
strOption strOption
( long "execute" ( long "execute"
@ -268,6 +278,7 @@ chatOptsP appDir defaultDbFileName = do
pure pure
ChatOpts ChatOpts
{ coreOptions, { coreOptions,
deviceName,
chatCmd, chatCmd,
chatCmdDelay, chatCmdDelay,
chatServerPort, chatServerPort,

View File

@ -397,12 +397,15 @@ findKnownRemoteCtrl = do
cmdOk <- newEmptyTMVarIO cmdOk <- newEmptyTMVarIO
action <- async $ handleCtrlError sseq "findKnownRemoteCtrl.discover" $ do action <- async $ handleCtrlError sseq "findKnownRemoteCtrl.discover" $ do
atomically $ takeTMVar cmdOk 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 rc <- withStore' (`getRemoteCtrlByFingerprint` ctrlFingerprint) >>= \case
Nothing -> throwChatError $ CEInternalError "connecting with a stored ctrl" Nothing -> throwChatError $ CEInternalError "connecting with a stored ctrl"
Just rc -> pure rc Just rc -> pure rc
atomically $ putTMVar foundCtrl (rc, inv) 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 updateRemoteCtrlSession sseq $ \case
RCSessionStarting -> Right RCSessionSearching {action, foundCtrl} RCSessionStarting -> Right RCSessionSearching {action, foundCtrl}
_ -> Left $ ChatErrorRemoteCtrl RCEBadState _ -> Left $ ChatErrorRemoteCtrl RCEBadState
@ -439,7 +442,8 @@ startRemoteCtrlSession = do
connectRemoteCtrl :: ChatMonad m => RCVerifiedInvitation -> SessionSeq -> m (Maybe RemoteCtrlInfo, CtrlAppInfo) connectRemoteCtrl :: ChatMonad m => RCVerifiedInvitation -> SessionSeq -> m (Maybe RemoteCtrlInfo, CtrlAppInfo)
connectRemoteCtrl verifiedInv@(RCVerifiedInvitation inv@RCInvitation {ca, app}) sseq = handleCtrlError sseq "connectRemoteCtrl" $ do 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 rc_ <- withStore' $ \db -> getRemoteCtrlByFingerprint db ca
mapM_ (validateRemoteCtrl inv) rc_ mapM_ (validateRemoteCtrl inv) rc_
hostAppInfo <- getHostAppInfo v 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} in Right RCSessionPendingConfirmation {remoteCtrlId_, ctrlDeviceName = ctrlName, rcsClient, tls, sessionCode, rcsWaitSession, rcsWaitConfirmation}
_ -> Left $ ChatErrorRemoteCtrl RCEBadState _ -> Left $ ChatErrorRemoteCtrl RCEBadState
toView CRRemoteCtrlSessionCode {remoteCtrl_ = (`remoteCtrlInfo` Just RCSPendingConfirmation {sessionCode}) <$> rc_, sessionCode} toView CRRemoteCtrlSessionCode {remoteCtrl_ = (`remoteCtrlInfo` Just RCSPendingConfirmation {sessionCode}) <$> rc_, sessionCode}
parseCtrlAppInfo ctrlAppInfo = do checkAppVersion CtrlAppInfo {appVersionRange} =
ctrlInfo@CtrlAppInfo {appVersionRange} <- case compatibleAppVersion hostAppVersionRange appVersionRange of
liftEitherWith (const $ ChatErrorRemoteCtrl RCEBadInvitation) $ JT.parseEither J.parseJSON ctrlAppInfo
v <- case compatibleAppVersion hostAppVersionRange appVersionRange of
Just (AppCompatible v) -> pure v Just (AppCompatible v) -> pure v
Nothing -> throwError $ ChatErrorRemoteCtrl $ RCEBadVersion $ maxVersion appVersionRange Nothing -> throwError $ ChatErrorRemoteCtrl $ RCEBadVersion $ maxVersion appVersionRange
pure (ctrlInfo, v)
getHostAppInfo appVersion = do getHostAppInfo appVersion = do
hostDeviceName <- chatReadVar localDeviceName hostDeviceName <- chatReadVar localDeviceName
encryptFiles <- chatReadVar encryptLocalFiles encryptFiles <- chatReadVar encryptLocalFiles
pure HostAppInfo {appVersion, deviceName = hostDeviceName, encoding = localEncoding, encryptFiles} 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 :: forall m. ChatMonad m => (ByteString -> m ChatResponse) -> RemoteCrypto -> TBQueue ChatResponse -> HTTP2Request -> m ()
handleRemoteCommand execChatCommand encryption remoteOutputQ HTTP2Request {request, reqBody, sendResponse} = do handleRemoteCommand execChatCommand encryption remoteOutputQ HTTP2Request {request, reqBody, sendResponse} = do
logDebug "handleRemoteCommand" logDebug "handleRemoteCommand"
@ -654,7 +659,8 @@ cancelActiveRemoteCtrl sseq_ = handleAny (logError . tshow) $ do
cancelRemoteCtrl :: Bool -> RemoteCtrlSession -> IO () cancelRemoteCtrl :: Bool -> RemoteCtrlSession -> IO ()
cancelRemoteCtrl handlingError = \case cancelRemoteCtrl handlingError = \case
RCSessionStarting -> pure () RCSessionStarting -> pure ()
RCSessionSearching {action} -> uninterruptibleCancel action RCSessionSearching {action} ->
unless handlingError $ uninterruptibleCancel action
RCSessionConnecting {rcsClient, rcsWaitSession} -> do RCSessionConnecting {rcsClient, rcsWaitSession} -> do
unless handlingError $ uninterruptibleCancel rcsWaitSession unless handlingError $ uninterruptibleCancel rcsWaitSession
cancelCtrlClient rcsClient cancelCtrlClient rcsClient

View File

@ -35,7 +35,8 @@ terminalChatConfig =
ntf = ["ntf://FB-Uop7RTaZZEG0ZLD2CIaTjsPh-Fw0zFAnb7QyA8Ks=@ntf2.simplex.im,ntg7jdjy2i3qbib3sykiho3enekwiaqg3icctliqhtqcg6jmoh6cxiad.onion"], ntf = ["ntf://FB-Uop7RTaZZEG0ZLD2CIaTjsPh-Fw0zFAnb7QyA8Ks=@ntf2.simplex.im,ntg7jdjy2i3qbib3sykiho3enekwiaqg3icctliqhtqcg6jmoh6cxiad.onion"],
xftp = defaultXFTPServers, xftp = defaultXFTPServers,
netCfg = defaultNetworkConfig netCfg = defaultNetworkConfig
} },
deviceNameForRemote = "SimpleX CLI"
} }
simplexChatTerminal :: WithTerminal t => ChatConfig -> ChatOpts -> t -> IO () simplexChatTerminal :: WithTerminal t => ChatConfig -> ChatOpts -> t -> IO ()

View File

@ -284,11 +284,13 @@ responseToView hu@(currentRH, user_) ChatConfig {logLevel, showReactions, showRe
rhi_ rhi_
] ]
CRRemoteHostList hs -> viewRemoteHosts hs CRRemoteHostList hs -> viewRemoteHosts hs
CRRemoteHostStarted {remoteHost_, invitation} -> CRRemoteHostStarted {remoteHost_, invitation, ctrlPort} ->
[ maybe "new remote host started" (\RemoteHostInfo {remoteHostId = rhId} -> "remote host " <> sShow rhId <> " started") remoteHost_, [ plain $ maybe ("new remote host" <> started) (\RemoteHostInfo {remoteHostId = rhId} -> "remote host " <> show rhId <> started) remoteHost_,
"Remote session invitation:", "Remote session invitation:",
plain invitation plain invitation
] ]
where
started = " started on port " <> ctrlPort
CRRemoteHostSessionCode {remoteHost_, sessionCode} -> CRRemoteHostSessionCode {remoteHost_, sessionCode} ->
[ maybe "new remote host connecting" (\RemoteHostInfo {remoteHostId = rhId} -> "remote host " <> sShow rhId <> " connecting") remoteHost_, [ maybe "new remote host connecting" (\RemoteHostInfo {remoteHostId = rhId} -> "remote host " <> sShow rhId <> " connecting") remoteHost_,
"Compare session code with host:", "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] [plain $ "file " <> filePath <> " stored on remote host " <> show rhId]
<> maybe [] ((: []) . plain . cryptoFileArgsStr testView) cfArgs_ <> maybe [] ((: []) . plain . cryptoFileArgsStr testView) cfArgs_
CRRemoteCtrlList cs -> viewRemoteCtrls cs CRRemoteCtrlList cs -> viewRemoteCtrls cs
CRRemoteCtrlFound rc -> CRRemoteCtrlFound {remoteCtrl = RemoteCtrlInfo {remoteCtrlId, ctrlDeviceName}, ctrlAppInfo_, appVersion, compatible} ->
["remote controller found:", viewRemoteCtrl rc] [ "remote controller " <> sShow remoteCtrlId <> " found: "
CRRemoteCtrlConnecting {remoteCtrl_, ctrlAppInfo = CtrlAppInfo {deviceName, appVersionRange = AppVersionRange _ (AppVersion ctrlVersion)}, appVersion = AppVersion v} -> <> maybe (deviceName <> "not compatible") (\info -> viewRemoteCtrl info appVersion compatible) ctrlAppInfo_
[ (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)
] ]
where where
ctrlVersionInfo deviceName = if T.null ctrlDeviceName then "" else plain ctrlDeviceName <> ", "
| ctrlVersion < v = " (older than this app - upgrade controller)" CRRemoteCtrlConnecting {remoteCtrl_, ctrlAppInfo, appVersion} ->
| ctrlVersion > v = " (newer than this app - upgrade it)" [ (maybe "connecting new remote controller" (\RemoteCtrlInfo {remoteCtrlId} -> "connecting remote controller " <> sShow remoteCtrlId) remoteCtrl_ <> ": ")
| otherwise = "" <> viewRemoteCtrl ctrlAppInfo appVersion True
]
CRRemoteCtrlSessionCode {remoteCtrl_, sessionCode} -> CRRemoteCtrlSessionCode {remoteCtrl_, sessionCode} ->
[ maybe "new remote controller connected" (\RemoteCtrlInfo {remoteCtrlId} -> "remote controller " <> sShow remoteCtrlId <> " connected") remoteCtrl_, [ maybe "new remote controller connected" (\RemoteCtrlInfo {remoteCtrlId} -> "remote controller " <> sShow remoteCtrlId <> " connected") remoteCtrl_,
"Compare session code with controller and use:", "Compare session code with controller and use:",
@ -1728,10 +1728,16 @@ viewRemoteCtrls = \case
RCSPendingConfirmation {sessionCode} -> " (pending confirmation, code: " <> sessionCode <> ")" RCSPendingConfirmation {sessionCode} -> " (pending confirmation, code: " <> sessionCode <> ")"
RCSConnected _ -> " (connected)" RCSConnected _ -> " (connected)"
-- TODO fingerprint, accepted? viewRemoteCtrl :: CtrlAppInfo -> AppVersion -> Bool -> StyledString
viewRemoteCtrl :: RemoteCtrlInfo -> StyledString viewRemoteCtrl CtrlAppInfo {deviceName, appVersionRange = AppVersionRange _ (AppVersion ctrlVersion)} (AppVersion v) compatible =
viewRemoteCtrl RemoteCtrlInfo {remoteCtrlId, ctrlDeviceName} = (if T.null deviceName then "" else plain deviceName <> ", ")
plain $ tshow remoteCtrlId <> ". " <> ctrlDeviceName <> ("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 :: ChatLogLevel -> Bool -> ChatError -> [StyledString]
viewChatError logLevel testView = \case viewChatError logLevel testView = \case

View File

@ -71,6 +71,7 @@ testOpts =
tbqSize = 16, tbqSize = 16,
highlyAvailable = False highlyAvailable = False
}, },
deviceName = Nothing,
chatCmd = "", chatCmd = "",
chatCmdDelay = 3, chatCmdDelay = 3,
chatServerPort = Nothing, chatServerPort = Nothing,

View File

@ -116,7 +116,7 @@ remoteHandshakeRejectTest = testChat3 aliceProfile aliceDesktopProfile bobProfil
mobileBob ##> "/set device name MobileBob" mobileBob ##> "/set device name MobileBob"
mobileBob <## "ok" mobileBob <## "ok"
desktop ##> "/start remote host 1" desktop ##> "/start remote host 1"
desktop <## "remote host 1 started" desktop <##. "remote host 1 started on port "
desktop <## "Remote session invitation:" desktop <## "Remote session invitation:"
inv <- getTermLine desktop inv <- getTermLine desktop
mobileBob ##> ("/connect remote ctrl " <> inv) mobileBob ##> ("/connect remote ctrl " <> inv)
@ -425,7 +425,7 @@ startRemote mobile desktop = do
mobile ##> "/set device name Mobile" mobile ##> "/set device name Mobile"
mobile <## "ok" mobile <## "ok"
desktop ##> "/start remote host new" desktop ##> "/start remote host new"
desktop <## "new remote host started" desktop <##. "new remote host started on port "
desktop <## "Remote session invitation:" desktop <## "Remote session invitation:"
inv <- getTermLine desktop inv <- getTermLine desktop
mobile ##> ("/connect remote ctrl " <> inv) mobile ##> ("/connect remote ctrl " <> inv)
@ -440,7 +440,7 @@ startRemote mobile desktop = do
startRemoteStored :: TestCC -> TestCC -> IO () startRemoteStored :: TestCC -> TestCC -> IO ()
startRemoteStored mobile desktop = do startRemoteStored mobile desktop = do
desktop ##> "/start remote host 1" desktop ##> "/start remote host 1"
desktop <## "remote host 1 started" desktop <##. "remote host 1 started on port "
desktop <## "Remote session invitation:" desktop <## "Remote session invitation:"
inv <- getTermLine desktop inv <- getTermLine desktop
mobile ##> ("/connect remote ctrl " <> inv) mobile ##> ("/connect remote ctrl " <> inv)
@ -454,13 +454,12 @@ startRemoteStored mobile desktop = do
startRemoteDiscover :: TestCC -> TestCC -> IO () startRemoteDiscover :: TestCC -> TestCC -> IO ()
startRemoteDiscover mobile desktop = do startRemoteDiscover mobile desktop = do
desktop ##> "/start remote host 1 multicast=on" desktop ##> "/start remote host 1 multicast=on"
desktop <## "remote host 1 started" desktop <##. "remote host 1 started on port "
desktop <## "Remote session invitation:" desktop <## "Remote session invitation:"
_inv <- getTermLine desktop -- will use multicast instead _inv <- getTermLine desktop -- will use multicast instead
mobile ##> "/find remote ctrl" mobile ##> "/find remote ctrl"
mobile <## "ok" mobile <## "ok"
mobile <## "remote controller found:" mobile <## ("remote controller 1 found: My desktop, v" <> versionNumber)
mobile <## "1. My desktop"
mobile ##> "/confirm remote ctrl 1" mobile ##> "/confirm remote ctrl 1"
mobile <## ("connecting remote controller 1: My desktop, v" <> versionNumber) mobile <## ("connecting remote controller 1: My desktop, v" <> versionNumber)