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
init() {
hs_init(0, nil)
DispatchQueue.global(qos: .background).sync {
haskell_init()
// hs_init(0, nil)
}
UserDefaults.standard.register(defaults: appDefaults)
setGroupDefaults()
registerGroupDefaults()

View File

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

View File

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

View File

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

View File

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

View File

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

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) {
"android.intent.action.VIEW" -> {
val uri = intent.data
if (uri != null) connectIfOpenedViaUri(chatModel.remoteHostId, uri.toURI(), ChatModel)
if (uri != null) connectIfOpenedViaUri(chatModel.remoteHostId(), uri.toURI(), ChatModel)
}
}
}

View File

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

View File

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

View File

@ -5,7 +5,7 @@
//#include <android/log.h>
// from the RTS
void hs_init(int * argc, char **argv[]);
void hs_init_with_rtsopts(int * argc, char **argv[]);
// from android-support
void setLineBuffering(void);
@ -32,7 +32,17 @@ Java_chat_simplex_common_platform_CoreKt_pipeStdOutToSocket(JNIEnv *env, __unuse
JNIEXPORT void JNICALL
Java_chat_simplex_common_platform_CoreKt_initHS(__unused JNIEnv *env, __unused jclass clazz) {
hs_init(NULL, NULL);
int argc = 5;
char *argv[] = {
"simplex",
"+RTS", // requires `hs_init_with_rtsopts`
"-A16m", // chunk size for new allocations
"-H64m", // initial heap size
"-xn", // non-moving GC
NULL
};
char **pargv = argv;
hs_init_with_rtsopts(&argc, &pargv);
setLineBuffering();
}

View File

@ -4,11 +4,14 @@
#include <stdint.h>
// from the RTS
void hs_init(int * argc, char **argv[]);
void hs_init_with_rtsopts(int * argc, char **argv[]);
JNIEXPORT void JNICALL
Java_chat_simplex_common_platform_CoreKt_initHS(JNIEnv *env, jclass clazz) {
hs_init(NULL, NULL);
int argc = 5;
char *argv[] = {"simplex", "+RTS", "-A16m", "-H64m", "-xn", NULL}; // see android/simplex-api.c for details
char **pargv = argv;
hs_init_with_rtsopts(&argc, &pargv);
}
// from simplex-chat

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -205,7 +205,9 @@ private fun RoleSelectionRow(groupInfo: GroupInfo, selectedRole: MutableState<Gr
verticalAlignment = Alignment.CenterVertically,
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(
generalGetString(MR.strings.new_member_role),
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) {
when (groupInfo.membership.memberStatus) {
GroupMemberStatus.MemInvited -> acceptGroupInvitationAlertDialog(rhId, groupInfo, chatModel, inProgress)
GroupMemberStatus.MemAccepted -> groupInvitationAcceptedAlert()
GroupMemberStatus.MemAccepted -> groupInvitationAcceptedAlert(rhId)
else -> withBGApi { openChat(rhId, ChatInfo.Group(groupInfo), chatModel) }
}
}
@ -538,7 +538,8 @@ fun contactRequestAlertDialog(rhId: Long?, contactRequest: ChatInfo.ContactReque
Text(generalGetString(MR.strings.reject_contact_button), Modifier.fillMaxWidth(), textAlign = TextAlign.Center, color = Color.Red)
}
}
}
},
hostDevice = hostDevice(rhId),
)
}
@ -644,7 +645,8 @@ fun askCurrentOrIncognitoProfileConnectContactViaAddress(
Text(stringResource(MR.strings.cancel_verb), Modifier.fillMaxWidth(), textAlign = TextAlign.Center, color = MaterialTheme.colors.primary)
}
}
}
},
hostDevice = hostDevice(rhId),
)
}
@ -654,7 +656,8 @@ suspend fun connectContactViaAddress(chatModel: ChatModel, rhId: Long?, contactI
chatModel.updateContact(rhId, contact)
AlertManager.shared.showAlertMsg(
title = generalGetString(MR.strings.connection_request_sent),
text = generalGetString(MR.strings.you_will_be_connected_when_your_connection_request_is_accepted)
text = generalGetString(MR.strings.you_will_be_connected_when_your_connection_request_is_accepted),
hostDevice = hostDevice(rhId),
)
return true
}
@ -674,7 +677,8 @@ fun acceptGroupInvitationAlertDialog(rhId: Long?, groupInfo: GroupInfo, chatMode
}
},
dismissText = generalGetString(MR.strings.delete_verb),
onDismiss = { deleteGroup(rhId, groupInfo, chatModel) }
onDismiss = { deleteGroup(rhId, groupInfo, chatModel) },
hostDevice = hostDevice(rhId),
)
}
@ -700,10 +704,11 @@ fun deleteGroup(rhId: Long?, groupInfo: GroupInfo, chatModel: ChatModel) {
}
}
fun groupInvitationAcceptedAlert() {
fun groupInvitationAcceptedAlert(rhId: Long?) {
AlertManager.shared.showAlertMsg(
generalGetString(MR.strings.joining_group),
generalGetString(MR.strings.youve_accepted_group_invitation_connecting_to_inviting_group_member)
generalGetString(MR.strings.youve_accepted_group_invitation_connecting_to_inviting_group_member),
hostDevice = hostDevice(rhId),
)
}

View File

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

View File

@ -26,8 +26,7 @@ import chat.simplex.common.model.ChatModel.controller
import chat.simplex.common.ui.theme.*
import chat.simplex.common.views.helpers.*
import chat.simplex.common.platform.*
import chat.simplex.common.views.remote.ConnectDesktopView
import chat.simplex.common.views.remote.connectMobileDevice
import chat.simplex.common.views.remote.*
import chat.simplex.res.MR
import dev.icerock.moko.resources.compose.stringResource
import kotlinx.coroutines.delay
@ -84,7 +83,7 @@ fun UserPicker(
.filter { it }
.collect {
try {
val updatedUsers = chatModel.controller.listUsers(chatModel.remoteHostId).sortedByDescending { it.user.activeUser }
val updatedUsers = chatModel.controller.listUsers(chatModel.remoteHostId()).sortedByDescending { it.user.activeUser }
var same = users.size == updatedUsers.size
if (same) {
for (i in 0 until minOf(users.size, updatedUsers.size)) {
@ -213,6 +212,14 @@ fun UserPicker(
userPickerState.value = AnimatedViewState.GONE
}
Divider(Modifier.requiredHeight(1.dp))
} else if (remoteHosts.isEmpty()) {
LinkAMobilePickerItem {
ModalManager.start.showModal {
ConnectMobileView()
}
userPickerState.value = AnimatedViewState.GONE
}
Divider(Modifier.requiredHeight(1.dp))
}
if (showSettings) {
SettingsPickerItem(settingsClicked)
@ -384,6 +391,16 @@ private fun UseFromDesktopPickerItem(onClick: () -> Unit) {
}
}
@Composable
private fun LinkAMobilePickerItem(onClick: () -> Unit) {
SectionItemView(onClick, padding = PaddingValues(start = DEFAULT_PADDING + 7.dp, end = DEFAULT_PADDING), minHeight = 68.dp) {
val text = generalGetString(MR.strings.link_a_mobile)
Icon(painterResource(MR.images.ic_smartphone_300), text, Modifier.size(20.dp), tint = MaterialTheme.colors.onBackground)
Spacer(Modifier.width(DEFAULT_PADDING + 6.dp))
Text(text, color = if (isInDarkTheme()) MenuTextColorDark else Color.Black)
}
}
@Composable
private fun SettingsPickerItem(onClick: () -> Unit) {
SectionItemView(onClick, padding = PaddingValues(start = DEFAULT_PADDING + 7.dp, end = DEFAULT_PADDING), minHeight = 68.dp) {

View File

@ -627,7 +627,7 @@ private fun afterSetCiTTL(
try {
updatingChatsMutex.withLock {
// this is using current remote host on purpose - if it changes during update, it will load correct chats
val chats = m.controller.apiGetChats(m.remoteHostId)
val chats = m.controller.apiGetChats(m.remoteHostId())
m.updateChats(chats)
}
} catch (e: Exception) {

View File

@ -1,6 +1,5 @@
package chat.simplex.common.views.helpers
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.CornerSize
import androidx.compose.foundation.shape.RoundedCornerShape
@ -14,10 +13,12 @@ import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.text.AnnotatedString
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.*
import chat.simplex.common.model.ChatModel
import chat.simplex.common.platform.*
import chat.simplex.common.ui.theme.*
import chat.simplex.res.MR
import dev.icerock.moko.resources.StringResource
import dev.icerock.moko.resources.compose.painterResource
class AlertManager {
var alertViews = mutableStateListOf<(@Composable () -> Unit)>()
@ -40,8 +41,11 @@ class AlertManager {
AlertDialog(
onDismissRequest = this::hideAlert,
title = alertTitle(title),
text = alertText(text),
buttons = buttons,
buttons = {
AlertContent(text, null, extraPadding = true) {
buttons()
}
},
shape = RoundedCornerShape(corner = CornerSize(25.dp))
)
}
@ -51,31 +55,17 @@ class AlertManager {
title: String,
text: AnnotatedString? = null,
onDismissRequest: (() -> Unit)? = null,
hostDevice: Pair<Long?, String>? = null,
buttons: @Composable () -> Unit,
) {
showAlert {
AlertDialog(
onDismissRequest = { onDismissRequest?.invoke(); hideAlert() },
title = {
Text(
title,
Modifier.fillMaxWidth().padding(vertical = DEFAULT_PADDING),
textAlign = TextAlign.Center,
fontSize = 20.sp
)
},
title = alertTitle(title),
buttons = {
Column(
Modifier
.padding(bottom = DEFAULT_PADDING)
) {
CompositionLocalProvider(LocalContentAlpha provides ContentAlpha.high) {
if (text != null) {
Text(text, Modifier.fillMaxWidth().padding(start = DEFAULT_PADDING, end = DEFAULT_PADDING, bottom = DEFAULT_PADDING * 1.5f), fontSize = 16.sp, textAlign = TextAlign.Center, color = MaterialTheme.colors.secondary)
}
AlertContent(text, hostDevice, extraPadding = true) {
buttons()
}
}
},
shape = RoundedCornerShape(corner = CornerSize(25.dp))
)
@ -90,16 +80,17 @@ class AlertManager {
dismissText: String = generalGetString(MR.strings.cancel_verb),
onDismiss: (() -> Unit)? = null,
onDismissRequest: (() -> Unit)? = null,
destructive: Boolean = false
destructive: Boolean = false,
hostDevice: Pair<Long?, String>? = null,
) {
showAlert {
AlertDialog(
onDismissRequest = { onDismissRequest?.invoke(); hideAlert() },
title = alertTitle(title),
text = alertText(text),
buttons = {
AlertContent(text, hostDevice, true) {
Row(
Modifier.fillMaxWidth().padding(horizontal = DEFAULT_PADDING).padding(bottom = DEFAULT_PADDING_HALF),
Modifier.fillMaxWidth().padding(horizontal = DEFAULT_PADDING),
horizontalArrangement = Arrangement.SpaceBetween
) {
val focusRequester = remember { FocusRequester() }
@ -115,6 +106,7 @@ class AlertManager {
hideAlert()
}, Modifier.focusRequester(focusRequester)) { Text(confirmText, color = if (destructive) MaterialTheme.colors.error else Color.Unspecified) }
}
}
},
shape = RoundedCornerShape(corner = CornerSize(25.dp))
)
@ -135,8 +127,8 @@ class AlertManager {
AlertDialog(
onDismissRequest = { onDismissRequest?.invoke(); hideAlert() },
title = alertTitle(title),
text = alertText(text),
buttons = {
AlertContent(text, null) {
Column(
Modifier.fillMaxWidth().padding(horizontal = DEFAULT_PADDING_HALF).padding(top = DEFAULT_PADDING, bottom = 2.dp),
horizontalAlignment = Alignment.CenterHorizontally
@ -150,6 +142,7 @@ class AlertManager {
hideAlert()
}) { Text(confirmText, color = if (destructive) Color.Red else Color.Unspecified, textAlign = TextAlign.End) }
}
}
},
shape = RoundedCornerShape(corner = CornerSize(25.dp))
)
@ -158,20 +151,21 @@ class AlertManager {
fun showAlertMsg(
title: String, text: String? = null,
confirmText: String = generalGetString(MR.strings.ok)
confirmText: String = generalGetString(MR.strings.ok),
hostDevice: Pair<Long?, String>? = null,
) {
showAlert {
AlertDialog(
onDismissRequest = this::hideAlert,
title = alertTitle(title),
text = alertText(text),
buttons = {
AlertContent(text, hostDevice, extraPadding = true) {
val focusRequester = remember { FocusRequester() }
LaunchedEffect(Unit) {
focusRequester.requestFocus()
}
Row(
Modifier.fillMaxWidth().padding(horizontal = DEFAULT_PADDING).padding(bottom = DEFAULT_PADDING_HALF),
Modifier.fillMaxWidth().padding(horizontal = DEFAULT_PADDING),
horizontalArrangement = Arrangement.Center
) {
TextButton(
@ -183,6 +177,7 @@ class AlertManager {
Text(confirmText, color = Color.Unspecified)
}
}
}
},
shape = RoundedCornerShape(corner = CornerSize(25.dp))
)
@ -191,18 +186,19 @@ class AlertManager {
fun showAlertMsgWithProgress(
title: String,
text: String? = null
text: String? = null,
) {
showAlert {
AlertDialog(
onDismissRequest = this::hideAlert,
title = alertTitle(title),
text = alertText(text),
buttons = {
AlertContent(text, null) {
Box(Modifier.fillMaxWidth().height(72.dp).padding(bottom = DEFAULT_PADDING * 2), contentAlignment = Alignment.Center) {
CircularProgressIndicator(Modifier.size(36.dp).padding(4.dp), color = MaterialTheme.colors.secondary, strokeWidth = 3.dp)
}
}
}
)
}
}
@ -211,7 +207,8 @@ class AlertManager {
title: StringResource,
text: StringResource? = null,
confirmText: StringResource = MR.strings.ok,
) = showAlertMsg(generalGetString(title), if (text != null) generalGetString(text) else null, generalGetString(confirmText))
hostDevice: Pair<Long?, String>? = null,
) = showAlertMsg(generalGetString(title), if (text != null) generalGetString(text) else null, generalGetString(confirmText), hostDevice)
@Composable
fun showInView() {
@ -234,18 +231,75 @@ private fun alertTitle(title: String): (@Composable () -> Unit)? {
}
}
private fun alertText(text: String?): (@Composable () -> Unit)? {
return if (text == null) {
null
@Composable
private fun AlertContent(text: String?, 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(
escapedHtmlToAnnotatedString(text, LocalDensity.current),
Modifier.fillMaxWidth(),
textAlign = TextAlign.Center,
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 chat.simplex.common.ui.theme.*
import chat.simplex.res.MR
import dev.icerock.moko.resources.compose.painterResource
@Composable
fun CloseSheetBar(close: (() -> Unit)?, showClose: Boolean = true, endButtons: @Composable RowScope.() -> Unit = {}) {
@ -47,23 +49,38 @@ fun CloseSheetBar(close: (() -> Unit)?, showClose: Boolean = true, endButtons: @
}
@Composable
fun AppBarTitle(title: String, withPadding: Boolean = true, bottomPadding: Dp = DEFAULT_PADDING * 1.5f) {
fun AppBarTitle(title: String, hostDevice: Pair<Long?, String>? = null, withPadding: Boolean = true, bottomPadding: Dp = DEFAULT_PADDING * 1.5f) {
val theme = CurrentColors.collectAsState()
val titleColor = CurrentColors.collectAsState().value.appColors.title
val brush = if (theme.value.base == DefaultTheme.SIMPLEX)
Brush.linearGradient(listOf(titleColor.darker(0.2f), titleColor.lighter(0.35f)), Offset(0f, Float.POSITIVE_INFINITY), Offset(Float.POSITIVE_INFINITY, 0f))
else // color is not updated when changing themes if I pass null here
Brush.linearGradient(listOf(titleColor, titleColor), Offset(0f, Float.POSITIVE_INFINITY), Offset(Float.POSITIVE_INFINITY, 0f))
Column {
Text(
title,
Modifier
.fillMaxWidth()
.padding(bottom = bottomPadding, start = if (withPadding) DEFAULT_PADDING else 0.dp, end = if (withPadding) DEFAULT_PADDING else 0.dp,),
.padding(start = if (withPadding) DEFAULT_PADDING else 0.dp, end = if (withPadding) DEFAULT_PADDING else 0.dp,),
overflow = TextOverflow.Ellipsis,
style = MaterialTheme.typography.h1.copy(brush = brush),
color = MaterialTheme.colors.primaryVariant,
textAlign = TextAlign.Center
)
if (hostDevice != null) {
HostDeviceTitle(hostDevice)
}
Spacer(Modifier.height(bottomPadding))
}
}
@Composable
private fun HostDeviceTitle(hostDevice: Pair<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/*(

View File

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

View File

@ -79,7 +79,7 @@ fun AddContactLayout(
.verticalScroll(rememberScrollState()),
verticalArrangement = Arrangement.SpaceBetween,
) {
AppBarTitle(stringResource(MR.strings.add_contact))
AppBarTitle(stringResource(MR.strings.add_contact), hostDevice(rh?.remoteHostId))
SectionView(stringResource(MR.strings.one_time_link_short).uppercase()) {
if (connReq.isNotEmpty()) {

View File

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

View File

@ -56,6 +56,7 @@ fun ContactConnectionInfoView(
connReq = connReqInvitation,
contactConnection = contactConnection,
focusAlias = focusAlias,
rhId = rhId,
deleteConnection = { deleteContactConnectionAlert(rhId, contactConnection, chatModel, close) },
onLocalAliasChanged = { setContactAlias(rhId, contactConnection, it, chatModel) },
share = { if (connReqInvitation != null) clipboard.shareText(connReqInvitation) },
@ -80,6 +81,7 @@ private fun ContactConnectionInfoLayout(
connReq: String?,
contactConnection: PendingContactConnection,
focusAlias: Boolean,
rhId: Long?,
deleteConnection: () -> Unit,
onLocalAliasChanged: (String) -> Unit,
share: () -> Unit,
@ -114,7 +116,8 @@ private fun ContactConnectionInfoLayout(
stringResource(
if (contactConnection.initiated) MR.strings.you_invited_a_contact
else MR.strings.you_accepted_connection
)
),
hostDevice(rhId)
)
Text(
stringResource(
@ -185,6 +188,7 @@ private fun PreviewContactConnectionInfoView() {
connReq = "https://simplex.chat/contact#/?v=1&smp=smp%3A%2F%2FPQUV2eL0t7OStZOoAsPEV2QYWt4-xilbakvGUGOItUo%3D%40smp6.simplex.im%2FK1rslx-m5bpXVIdMZg9NLUZ_8JBm8xTt%23MCowBQYDK2VuAyEALDeVe-sG8mRY22LsXlPgiwTNs9dbiLrNuA7f3ZMAJ2w%3D",
contactConnection = PendingContactConnection.getSampleData(),
focusAlias = false,
rhId = null,
deleteConnection = {},
onLocalAliasChanged = {},
share = {},

View File

@ -67,7 +67,7 @@ fun PasteToConnectLayout(
Modifier.verticalScroll(rememberScrollState()).padding(horizontal = DEFAULT_PADDING),
verticalArrangement = Arrangement.SpaceBetween,
) {
AppBarTitle(stringResource(MR.strings.connect_via_link), false)
AppBarTitle(stringResource(MR.strings.connect_via_link), hostDevice(rhId), withPadding = false)
Box(Modifier.padding(top = DEFAULT_PADDING, bottom = 6.dp)) {
TextEditor(

View File

@ -4,7 +4,6 @@ import SectionBottomSpacer
import SectionItemView
import SectionTextFooter
import androidx.compose.desktop.ui.tooling.preview.Preview
import chat.simplex.common.platform.Log
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
@ -17,7 +16,7 @@ import androidx.compose.ui.text.style.TextAlign
import dev.icerock.moko.resources.compose.stringResource
import androidx.compose.ui.unit.dp
import chat.simplex.common.model.*
import chat.simplex.common.platform.TAG
import chat.simplex.common.platform.*
import chat.simplex.common.ui.theme.*
import chat.simplex.common.views.chatlist.*
import chat.simplex.common.views.helpers.*
@ -65,6 +64,7 @@ suspend fun planAndConnect(
confirmText = if (incognito) generalGetString(MR.strings.connect_via_link_incognito) else generalGetString(MR.strings.connect_via_link_verb),
onConfirm = { withApi { connectViaUri(chatModel, rhId, uri, incognito, connectionPlan, close) } },
destructive = true,
hostDevice = hostDevice(rhId),
)
} else {
askCurrentOrIncognitoProfileAlert(
@ -82,12 +82,14 @@ suspend fun planAndConnect(
openKnownContact(chatModel, rhId, close, contact)
AlertManager.shared.showAlertMsg(
generalGetString(MR.strings.contact_already_exists),
String.format(generalGetString(MR.strings.connect_plan_you_are_already_connecting_to_vName), contact.displayName)
String.format(generalGetString(MR.strings.connect_plan_you_are_already_connecting_to_vName), contact.displayName),
hostDevice = hostDevice(rhId),
)
} else {
AlertManager.shared.showAlertMsg(
generalGetString(MR.strings.connect_plan_already_connecting),
generalGetString(MR.strings.connect_plan_you_are_already_connecting_via_this_one_time_link)
generalGetString(MR.strings.connect_plan_you_are_already_connecting_via_this_one_time_link),
hostDevice = hostDevice(rhId),
)
}
}
@ -97,7 +99,8 @@ suspend fun planAndConnect(
openKnownContact(chatModel, rhId, close, contact)
AlertManager.shared.showAlertMsg(
generalGetString(MR.strings.contact_already_exists),
String.format(generalGetString(MR.strings.you_are_already_connected_to_vName_via_this_link), contact.displayName)
String.format(generalGetString(MR.strings.you_are_already_connected_to_vName_via_this_link), contact.displayName),
hostDevice = hostDevice(rhId),
)
}
}
@ -124,6 +127,7 @@ suspend fun planAndConnect(
confirmText = if (incognito) generalGetString(MR.strings.connect_via_link_incognito) else generalGetString(MR.strings.connect_via_link_verb),
onConfirm = { withApi { connectViaUri(chatModel, rhId, uri, incognito, connectionPlan, close) } },
destructive = true,
hostDevice = hostDevice(rhId),
)
} else {
askCurrentOrIncognitoProfileAlert(
@ -143,6 +147,7 @@ suspend fun planAndConnect(
confirmText = if (incognito) generalGetString(MR.strings.connect_via_link_incognito) else generalGetString(MR.strings.connect_via_link_verb),
onConfirm = { withApi { connectViaUri(chatModel, rhId, uri, incognito, connectionPlan, close) } },
destructive = true,
hostDevice = hostDevice(rhId),
)
} else {
askCurrentOrIncognitoProfileAlert(
@ -159,7 +164,8 @@ suspend fun planAndConnect(
openKnownContact(chatModel, rhId, close, contact)
AlertManager.shared.showAlertMsg(
generalGetString(MR.strings.contact_already_exists),
String.format(generalGetString(MR.strings.connect_plan_you_are_already_connecting_to_vName), contact.displayName)
String.format(generalGetString(MR.strings.connect_plan_you_are_already_connecting_to_vName), contact.displayName),
hostDevice = hostDevice(rhId),
)
}
is ContactAddressPlan.Known -> {
@ -168,7 +174,8 @@ suspend fun planAndConnect(
openKnownContact(chatModel, rhId, close, contact)
AlertManager.shared.showAlertMsg(
generalGetString(MR.strings.contact_already_exists),
String.format(generalGetString(MR.strings.you_are_already_connected_to_vName_via_this_link), contact.displayName)
String.format(generalGetString(MR.strings.you_are_already_connected_to_vName_via_this_link), contact.displayName),
hostDevice = hostDevice(rhId),
)
}
is ContactAddressPlan.ContactViaAddress -> {
@ -190,7 +197,8 @@ suspend fun planAndConnect(
title = generalGetString(MR.strings.connect_via_group_link),
text = generalGetString(MR.strings.you_will_join_group),
confirmText = if (incognito) generalGetString(MR.strings.join_group_incognito_button) else generalGetString(MR.strings.join_group_button),
onConfirm = { withApi { connectViaUri(chatModel, rhId, uri, incognito, connectionPlan, close) } }
onConfirm = { withApi { connectViaUri(chatModel, rhId, uri, incognito, connectionPlan, close) } },
hostDevice = hostDevice(rhId),
)
} else {
askCurrentOrIncognitoProfileAlert(
@ -215,6 +223,7 @@ suspend fun planAndConnect(
confirmText = if (incognito) generalGetString(MR.strings.join_group_incognito_button) else generalGetString(MR.strings.join_group_button),
onConfirm = { withApi { connectViaUri(chatModel, rhId, uri, incognito, connectionPlan, close) } },
destructive = true,
hostDevice = hostDevice(rhId),
)
} else {
askCurrentOrIncognitoProfileAlert(
@ -236,7 +245,8 @@ suspend fun planAndConnect(
} else {
AlertManager.shared.showAlertMsg(
generalGetString(MR.strings.connect_plan_already_joining_the_group),
generalGetString(MR.strings.connect_plan_you_are_already_joining_the_group_via_this_link)
generalGetString(MR.strings.connect_plan_you_are_already_joining_the_group_via_this_link),
hostDevice = hostDevice(rhId),
)
}
}
@ -246,7 +256,8 @@ suspend fun planAndConnect(
openKnownGroup(chatModel, rhId, close, groupInfo)
AlertManager.shared.showAlertMsg(
generalGetString(MR.strings.connect_plan_group_already_exists),
String.format(generalGetString(MR.strings.connect_plan_you_are_already_in_group_vName), groupInfo.displayName)
String.format(generalGetString(MR.strings.connect_plan_you_are_already_in_group_vName), groupInfo.displayName),
hostDevice = hostDevice(rhId),
)
}
}
@ -284,7 +295,8 @@ suspend fun connectViaUri(
ConnectionLinkType.CONTACT -> generalGetString(MR.strings.you_will_be_connected_when_your_connection_request_is_accepted)
ConnectionLinkType.INVITATION -> generalGetString(MR.strings.you_will_be_connected_when_your_contacts_device_is_online)
ConnectionLinkType.GROUP -> generalGetString(MR.strings.you_will_be_connected_when_group_host_device_is_online)
}
},
hostDevice = hostDevice(rhId),
)
}
return r
@ -336,7 +348,8 @@ fun askCurrentOrIncognitoProfileAlert(
Text(stringResource(MR.strings.cancel_verb), Modifier.fillMaxWidth(), textAlign = TextAlign.Center, color = MaterialTheme.colors.primary)
}
}
}
},
hostDevice = hostDevice(rhId),
)
}
@ -411,7 +424,8 @@ fun ownGroupLinkConfirmConnect(
Text(stringResource(MR.strings.cancel_verb), Modifier.fillMaxWidth(), textAlign = TextAlign.Center, color = MaterialTheme.colors.primary)
}
}
}
},
hostDevice = hostDevice(rhId),
)
}
@ -455,7 +469,7 @@ fun ConnectContactLayout(
Modifier.verticalScroll(rememberScrollState()).padding(horizontal = DEFAULT_PADDING),
verticalArrangement = Arrangement.SpaceBetween
) {
AppBarTitle(stringResource(MR.strings.scan_QR_code), false)
AppBarTitle(stringResource(MR.strings.scan_QR_code), hostDevice(rh?.remoteHostId), withPadding = false)
Box(
Modifier
.fillMaxWidth()

View File

@ -26,7 +26,7 @@ fun HowItWorks(user: User?, onboardingStage: SharedPreference<OnboardingStage>?
.fillMaxWidth()
.padding(horizontal = DEFAULT_PADDING),
) {
AppBarTitle(stringResource(MR.strings.how_simplex_works), false)
AppBarTitle(stringResource(MR.strings.how_simplex_works), withPadding = false)
ReadableText(MR.strings.many_people_asked_how_can_it_deliver)
ReadableText(MR.strings.to_protect_privacy_simplex_has_ids_for_queues)
ReadableText(MR.strings.you_control_servers_to_receive_your_contacts_to_send)

View File

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

View File

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

View File

@ -26,7 +26,7 @@ fun HelpLayout(userDisplayName: String) {
.verticalScroll(rememberScrollState())
.padding(horizontal = DEFAULT_PADDING),
){
AppBarTitle(String.format(stringResource(MR.strings.personal_welcome), userDisplayName), false)
AppBarTitle(String.format(stringResource(MR.strings.personal_welcome), userDisplayName), withPadding = false)
ChatHelpView()
}
}

View File

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

View File

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

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)
ChatPreferencesItem(showCustomModal, stopped = stopped)
if (appPlatform.isDesktop) {
SettingsActionItem(painterResource(MR.images.ic_smartphone), stringResource(if (remember { chatModel.remoteHosts }.isEmpty()) MR.strings.link_a_mobile else MR.strings.linked_mobiles), showModal { ConnectMobileView(it) }, disabled = stopped, extraPadding = true)
SettingsActionItem(painterResource(MR.images.ic_smartphone), stringResource(if (remember { chatModel.remoteHosts }.isEmpty()) MR.strings.link_a_mobile else MR.strings.linked_mobiles), showModal { ConnectMobileView() }, disabled = stopped, extraPadding = true)
} else {
SettingsActionItem(painterResource(MR.images.ic_desktop), stringResource(MR.strings.settings_section_title_use_from_desktop), showCustomModal{ it, close -> ConnectDesktopView(close) }, disabled = stopped, extraPadding = true)
}

View File

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

View File

@ -18,7 +18,7 @@ fun VersionInfoView(info: CoreVersionInfo) {
Column(
Modifier.padding(horizontal = DEFAULT_PADDING),
) {
AppBarTitle(stringResource(MR.strings.app_version_title), false)
AppBarTitle(stringResource(MR.strings.app_version_title), withPadding = false)
if (appPlatform.isAndroid) {
Text(String.format(stringResource(MR.strings.app_version_name), BuildConfigCommon.ANDROID_VERSION_NAME))
Text(String.format(stringResource(MR.strings.app_version_code), BuildConfigCommon.ANDROID_VERSION_CODE))

View File

@ -1178,6 +1178,7 @@
<!-- GroupMemberRole -->
<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_admin">admin</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_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_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_descr">Via secure quantum resistant protocol.</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">Unlink</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="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="desktop_incompatible_version">Incompatible version</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="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="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="new_desktop"><![CDATA[<i>(nuovo)</i>]]></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="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="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="new_desktop"><![CDATA[<i>(nieuw)</i>]]></string>
<string name="unlink_desktop_question">Desktop ontkoppelen?</string>

View File

@ -1465,7 +1465,7 @@
<string name="new_mobile_device">新移动设备</string>
<string name="desktop_address">桌面地址</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="new_desktop"><![CDATA[<i>(新)</i>]]></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.Modifier
import androidx.compose.ui.input.key.*
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.unit.dp
import androidx.compose.ui.window.*
import chat.simplex.common.model.ChatController
@ -19,6 +20,7 @@ import chat.simplex.common.ui.theme.DEFAULT_START_MODAL_WIDTH
import chat.simplex.common.ui.theme.SimpleXTheme
import chat.simplex.common.views.TerminalView
import chat.simplex.common.views.helpers.FileDialogChooser
import chat.simplex.common.views.helpers.escapedHtmlToAnnotatedString
import chat.simplex.res.MR
import dev.icerock.moko.resources.compose.stringResource
import kotlinx.coroutines.*
@ -80,7 +82,7 @@ fun showApp() = application {
if (toast != null) {
Box(Modifier.fillMaxSize().padding(bottom = 20.dp), contentAlignment = Alignment.BottomCenter) {
Text(
toast.first,
escapedHtmlToAnnotatedString(toast.first, LocalDensity.current),
Modifier.background(MaterialTheme.colors.primary, RoundedCornerShape(100)).padding(vertical = 5.dp, horizontal = 10.dp),
color = MaterialTheme.colors.onPrimary,
style = MaterialTheme.typography.body1

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -19,6 +19,7 @@ where
import Control.Logger.Simple (LogLevel (..))
import qualified Data.Attoparsec.ByteString.Char8 as A
import qualified Data.ByteString.Char8 as B
import Data.Text (Text)
import Numeric.Natural (Natural)
import Options.Applicative
import Simplex.Chat.Controller (ChatLogLevel (..), updateStr, versionNumber, versionString)
@ -32,6 +33,7 @@ import System.FilePath (combine)
data ChatOpts = ChatOpts
{ coreOptions :: CoreChatOpts,
deviceName :: Maybe Text,
chatCmd :: String,
chatCmdDelay :: Int,
chatServerPort :: Maybe String,
@ -200,6 +202,14 @@ coreChatOptsP appDir defaultDbFileName = do
chatOptsP :: FilePath -> FilePath -> Parser ChatOpts
chatOptsP appDir defaultDbFileName = do
coreOptions <- coreChatOptsP appDir defaultDbFileName
deviceName <-
optional $
strOption
( long "device-name"
<> short 'e'
<> metavar "DEVICE"
<> help "Device name to use in connections with remote hosts and controller"
)
chatCmd <-
strOption
( long "execute"
@ -268,6 +278,7 @@ chatOptsP appDir defaultDbFileName = do
pure
ChatOpts
{ coreOptions,
deviceName,
chatCmd,
chatCmdDelay,
chatServerPort,

View File

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

View File

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

View File

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

View File

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

View File

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