From f31054de4ffce6fed8cd5c3e088a363de02a2d0d Mon Sep 17 00:00:00 2001 From: Stanislav Dmitrenko <7953703+avently@users.noreply.github.com> Date: Tue, 28 Nov 2023 06:11:53 +0800 Subject: [PATCH 01/13] desktop (windows): fix action (#3479) --- .github/workflows/build.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 600b934bd..afdb9bea1 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -262,7 +262,7 @@ jobs: # rm -rf dist-newstyle/src/direct-sq* is here because of the bug in cabal's dependency which prevents second build from finishing - name: 'Setup MSYS2' - if: startsWith(github.ref, 'refs/tags/v') && matrix.os == 'windows-latest' + if: matrix.os == 'windows-latest' uses: msys2/setup-msys2@v2 with: msystem: ucrt64 From 0e18b13beaaf46c65374ec6d5f82a2f1bcf6238c Mon Sep 17 00:00:00 2001 From: Stanislav Dmitrenko <7953703+avently@users.noreply.github.com> Date: Fri, 1 Dec 2023 03:38:21 +0800 Subject: [PATCH 02/13] desktop: adapting onboarding process to linking devices (#3490) * desktop: adapting onboarding process to linking devices * show progress on long operations * changes * clearing chat cache logic * lines --------- Co-authored-by: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com> --- .../views/onboarding/SimpleXInfo.android.kt | 15 ++ .../kotlin/chat/simplex/common/App.kt | 56 ++++-- .../chat/simplex/common/model/ChatModel.kt | 9 +- .../chat/simplex/common/model/SimpleXAPI.kt | 55 +++++- .../chat/simplex/common/platform/Core.kt | 14 +- .../simplex/common/platform/NtfManager.kt | 8 +- .../chat/simplex/common/views/WelcomeView.kt | 23 ++- .../common/views/chatlist/ChatListView.kt | 29 +-- .../common/views/chatlist/ShareListView.kt | 4 +- .../common/views/chatlist/UserPicker.kt | 53 ++++-- .../simplex/common/views/helpers/Section.kt | 6 +- .../views/onboarding/CreateSimpleXAddress.kt | 4 + .../views/onboarding/LinkAMobileView.kt | 91 +++++++++ .../common/views/onboarding/OnboardingView.kt | 1 + .../onboarding/SetupDatabasePassphrase.kt | 11 +- .../common/views/onboarding/SimpleXInfo.kt | 16 +- .../common/views/remote/ConnectMobileView.kt | 144 +++++++++------ .../views/usersettings/NetworkAndServers.kt | 23 ++- .../views/usersettings/PrivacySettings.kt | 64 +++---- .../common/views/usersettings/SettingsView.kt | 173 ++++++++++-------- .../views/usersettings/UserProfilesView.kt | 5 +- .../commonMain/resources/MR/base/strings.xml | 3 + .../resources/MR/images/ic_refresh.svg | 1 + .../views/onboarding/SimpleXInfo.desktop.kt | 23 +++ 24 files changed, 565 insertions(+), 266 deletions(-) create mode 100644 apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/views/onboarding/SimpleXInfo.android.kt create mode 100644 apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/onboarding/LinkAMobileView.kt create mode 100644 apps/multiplatform/common/src/commonMain/resources/MR/images/ic_refresh.svg create mode 100644 apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/views/onboarding/SimpleXInfo.desktop.kt diff --git a/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/views/onboarding/SimpleXInfo.android.kt b/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/views/onboarding/SimpleXInfo.android.kt new file mode 100644 index 000000000..bfe87b17d --- /dev/null +++ b/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/views/onboarding/SimpleXInfo.android.kt @@ -0,0 +1,15 @@ +package chat.simplex.common.views.onboarding + +import androidx.compose.runtime.Composable +import chat.simplex.common.model.SharedPreference +import chat.simplex.common.model.User +import chat.simplex.res.MR + +@Composable +actual fun OnboardingActionButton(user: User?, onboardingStage: SharedPreference, onclick: (() -> Unit)?) { + if (user == null) { + OnboardingActionButton(MR.strings.create_your_profile, onboarding = OnboardingStage.Step2_CreateProfile, true, onclick = onclick) + } else { + OnboardingActionButton(MR.strings.make_private_connection, onboarding = OnboardingStage.OnboardingComplete, true, onclick = onclick) + } +} diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/App.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/App.kt index cfbcb7aa4..d4062a1aa 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/App.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/App.kt @@ -37,8 +37,7 @@ import kotlinx.coroutines.flow.* data class SettingsViewState( val userPickerState: MutableStateFlow, - val scaffoldState: ScaffoldState, - val switchingUsersAndHosts: MutableState + val scaffoldState: ScaffoldState ) @Composable @@ -102,11 +101,8 @@ fun MainScreen() { } Box { - var onboarding by remember { mutableStateOf(chatModel.controller.appPrefs.onboardingStage.get()) } - LaunchedEffect(Unit) { - snapshotFlow { chatModel.controller.appPrefs.onboardingStage.state.value }.distinctUntilChanged().collect { onboarding = it } - } - val userCreated = chatModel.userCreated.value + val onboarding by remember { chatModel.controller.appPrefs.onboardingStage.state } + val localUserCreated = chatModel.localUserCreated.value var showInitializationView by remember { mutableStateOf(false) } when { chatModel.chatDbStatus.value == null && showInitializationView -> InitializationView() @@ -115,14 +111,18 @@ fun MainScreen() { DatabaseErrorView(chatModel.chatDbStatus, chatModel.controller.appPrefs) } } - remember { chatModel.chatDbEncrypted }.value == null || userCreated == null -> SplashView() - onboarding == OnboardingStage.OnboardingComplete && userCreated -> { + remember { chatModel.chatDbEncrypted }.value == null || localUserCreated == null -> SplashView() + onboarding == OnboardingStage.OnboardingComplete -> { Box { showAdvertiseLAAlert = true - val userPickerState by rememberSaveable(stateSaver = AnimatedViewState.saver()) { mutableStateOf(MutableStateFlow(AnimatedViewState.GONE)) } + val userPickerState by rememberSaveable(stateSaver = AnimatedViewState.saver()) { mutableStateOf(MutableStateFlow(if (chatModel.desktopNoUserNoRemote()) AnimatedViewState.VISIBLE else AnimatedViewState.GONE)) } + KeyChangeEffect(chatModel.desktopNoUserNoRemote) { + if (chatModel.desktopNoUserNoRemote() && !ModalManager.start.hasModalsOpen()) { + userPickerState.value = AnimatedViewState.VISIBLE + } + } val scaffoldState = rememberScaffoldState() - val switchingUsersAndHosts = rememberSaveable { mutableStateOf(false) } - val settingsState = remember { SettingsViewState(userPickerState, scaffoldState, switchingUsersAndHosts) } + val settingsState = remember { SettingsViewState(userPickerState, scaffoldState) } if (appPlatform.isAndroid) { AndroidScreen(settingsState) } else { @@ -137,12 +137,14 @@ fun MainScreen() { } } onboarding == OnboardingStage.Step2_CreateProfile -> CreateFirstProfile(chatModel) {} + onboarding == OnboardingStage.LinkAMobile -> LinkAMobile() onboarding == OnboardingStage.Step2_5_SetupDatabasePassphrase -> SetupDatabasePassphrase(chatModel) onboarding == OnboardingStage.Step3_CreateSimpleXAddress -> CreateSimpleXAddress(chatModel, null) onboarding == OnboardingStage.Step4_SetNotificationsMode -> SetNotificationsMode(chatModel) } if (appPlatform.isAndroid) { ModalManager.fullscreen.showInView() + SwitchingUsersView() } val unauthorized = remember { derivedStateOf { AppLock.userAuthorized.value != true } } @@ -262,7 +264,7 @@ fun CenterPartOfScreen() { .background(MaterialTheme.colors.background), contentAlignment = Alignment.Center ) { - Text(stringResource(MR.strings.no_selected_chat)) + Text(stringResource(if (chatModel.desktopNoUserNoRemote) MR.strings.no_connected_mobile else MR.strings.no_selected_chat)) } } else { ModalManager.center.showInView() @@ -286,6 +288,7 @@ fun DesktopScreen(settingsState: SettingsViewState) { } Box(Modifier.widthIn(max = DEFAULT_START_MODAL_WIDTH)) { ModalManager.start.showInView() + SwitchingUsersView() } Row(Modifier.padding(start = DEFAULT_START_MODAL_WIDTH).clipToBounds()) { Box(Modifier.widthIn(min = DEFAULT_MIN_CENTER_MODAL_WIDTH).weight(1f)) { @@ -298,7 +301,7 @@ fun DesktopScreen(settingsState: SettingsViewState) { EndPartOfScreen() } } - val (userPickerState, scaffoldState, switchingUsersAndHosts ) = settingsState + val (userPickerState, scaffoldState ) = settingsState val scope = rememberCoroutineScope() if (scaffoldState.drawerState.isOpen) { Box( @@ -312,7 +315,7 @@ fun DesktopScreen(settingsState: SettingsViewState) { ) } VerticalDivider(Modifier.padding(start = DEFAULT_START_MODAL_WIDTH)) - UserPicker(chatModel, userPickerState, switchingUsersAndHosts) { + UserPicker(chatModel, userPickerState) { scope.launch { if (scaffoldState.drawerState.isOpen) scaffoldState.drawerState.close() else scaffoldState.drawerState.open() } userPickerState.value = AnimatedViewState.GONE } @@ -335,3 +338,26 @@ fun InitializationView() { } } } + +@Composable +private fun SwitchingUsersView() { + if (remember { chatModel.switchingUsersAndHosts }.value) { + Box( + Modifier.fillMaxSize().clickable(enabled = false, onClick = {}), + contentAlignment = Alignment.Center + ) { + ProgressIndicator() + } + } +} + +@Composable +private fun ProgressIndicator() { + CircularProgressIndicator( + Modifier + .padding(horizontal = 2.dp) + .size(30.dp), + color = MaterialTheme.colors.secondary, + strokeWidth = 2.5.dp + ) +} diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/ChatModel.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/ChatModel.kt index 60b70c0ae..808b1938e 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/ChatModel.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/ChatModel.kt @@ -2,7 +2,7 @@ package chat.simplex.common.model import androidx.compose.material.* import androidx.compose.runtime.* -import androidx.compose.runtime.snapshots.SnapshotStateList +import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.snapshots.SnapshotStateMap import androidx.compose.ui.graphics.Color import androidx.compose.ui.text.SpanStyle @@ -43,7 +43,7 @@ object ChatModel { val setDeliveryReceipts = mutableStateOf(false) val currentUser = mutableStateOf(null) val users = mutableStateListOf() - val userCreated = mutableStateOf(null) + val localUserCreated = mutableStateOf(null) val chatRunning = mutableStateOf(null) val chatDbChanged = mutableStateOf(false) val chatDbEncrypted = mutableStateOf(false) @@ -51,6 +51,7 @@ object ChatModel { val chats = mutableStateListOf() // map of connections network statuses, key is agent connection id val networkStatuses = mutableStateMapOf() + val switchingUsersAndHosts = mutableStateOf(false) // current chat val chatId = mutableStateOf(null) @@ -108,6 +109,9 @@ object ChatModel { var updatingChatsMutex: Mutex = Mutex() + val desktopNoUserNoRemote: Boolean @Composable get() = appPlatform.isDesktop && currentUser.value == null && currentRemoteHost.value == null + fun desktopNoUserNoRemote(): Boolean = appPlatform.isDesktop && currentUser.value == null && currentRemoteHost.value == null + // remote controller val remoteHosts = mutableStateListOf() val currentRemoteHost = mutableStateOf(null) @@ -620,6 +624,7 @@ object ChatModel { terminalItems.add(item) } + val connectedToRemote: Boolean @Composable get() = currentRemoteHost.value != null || remoteCtrlSession.value?.active == true fun connectedToRemote(): Boolean = currentRemoteHost.value != null || remoteCtrlSession.value?.active == true } diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt index 34b6fd99f..c61772578 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt @@ -362,7 +362,7 @@ object ChatController { chatModel.users.addAll(users) if (justStarted) { chatModel.currentUser.value = user - chatModel.userCreated.value = true + chatModel.localUserCreated.value = true getUserChatData(null) appPrefs.chatLastStart.set(Clock.System.now()) chatModel.chatRunning.value = true @@ -382,6 +382,31 @@ object ChatController { } } + suspend fun startChatWithoutUser() { + Log.d(TAG, "user: null") + try { + if (chatModel.chatRunning.value == true) return + apiSetTempFolder(coreTmpDir.absolutePath) + apiSetFilesFolder(appFilesDir.absolutePath) + if (appPlatform.isDesktop) { + apiSetRemoteHostsFolder(remoteHostsDir.absolutePath) + } + apiSetXFTPConfig(getXFTPCfg()) + apiSetEncryptLocalFiles(appPrefs.privacyEncryptLocalFiles.get()) + chatModel.users.clear() + chatModel.currentUser.value = null + chatModel.localUserCreated.value = false + appPrefs.chatLastStart.set(Clock.System.now()) + chatModel.chatRunning.value = true + startReceiver() + setLocalDeviceName(appPrefs.deviceNameForRemoteAccess.get()!!) + Log.d(TAG, "startChat: started without user") + } catch (e: Error) { + Log.e(TAG, "failed starting chat without user $e") + throw e + } + } + suspend fun changeActiveUser(rhId: Long?, toUserId: Long, viewPwd: String?) { try { changeActiveUser_(rhId, toUserId, viewPwd) @@ -475,7 +500,9 @@ object ChatController { val r = sendCmd(rh, CC.ShowActiveUser()) if (r is CR.ActiveUser) return r.user.updateRemoteHostId(rh) Log.d(TAG, "apiGetActiveUser: ${r.responseType} ${r.details}") - chatModel.userCreated.value = false + if (rh == null) { + chatModel.localUserCreated.value = false + } return null } @@ -1990,7 +2017,7 @@ object ChatController { chatModel.setContactNetworkStatus(contact, NetworkStatus.Error(err)) } - suspend fun switchUIRemoteHost(rhId: Long?) { + suspend fun switchUIRemoteHost(rhId: Long?) = showProgressIfNeeded { // TODO lock the switch so that two switches can't run concurrently? chatModel.chatId.value = null ModalManager.center.closeModals() @@ -2003,7 +2030,10 @@ object ChatController { chatModel.users.clear() chatModel.users.addAll(users) chatModel.currentUser.value = user - chatModel.userCreated.value = true + if (user == null) { + chatModel.chatItems.clear() + chatModel.chats.clear() + } val statuses = apiGetNetworkStatuses(rhId) if (statuses != null) { chatModel.networkStatuses.clear() @@ -2013,6 +2043,23 @@ object ChatController { getUserChatData(rhId) } + suspend fun showProgressIfNeeded(block: suspend () -> Unit) { + val job = withBGApi { + try { + delay(500) + chatModel.switchingUsersAndHosts.value = true + } catch (e: Throwable) { + chatModel.switchingUsersAndHosts.value = false + } + } + try { + block() + } finally { + job.cancel() + chatModel.switchingUsersAndHosts.value = false + } + } + fun getXFTPCfg(): XFTPFileConfig { return XFTPFileConfig(minFileSize = 0) } diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/platform/Core.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/platform/Core.kt index 3d3a91cb3..a4c1c333e 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/platform/Core.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/platform/Core.kt @@ -55,10 +55,22 @@ suspend fun initChatController(useKey: String? = null, confirmMigrations: Migrat if (appPreferences.encryptionStartedAt.get() != null) appPreferences.encryptionStartedAt.set(null) val user = chatController.apiGetActiveUser(null) if (user == null) { - chatModel.controller.appPrefs.onboardingStage.set(OnboardingStage.Step1_SimpleXInfo) chatModel.controller.appPrefs.privacyDeliveryReceiptsSet.set(true) chatModel.currentUser.value = null chatModel.users.clear() + if (appPlatform.isDesktop) { + /** + * Setting it here to null because otherwise the screen will flash in [MainScreen] after the first start + * because of default value of [OnboardingStage.OnboardingComplete] + * */ + chatModel.localUserCreated.value = null + if (chatController.listRemoteHosts()?.isEmpty() == true) { + chatController.appPrefs.onboardingStage.set(OnboardingStage.Step1_SimpleXInfo) + } + chatController.startChatWithoutUser() + } else { + chatController.appPrefs.onboardingStage.set(OnboardingStage.Step1_SimpleXInfo) + } } else { val savedOnboardingStage = appPreferences.onboardingStage.get() appPreferences.onboardingStage.set(if (listOf(OnboardingStage.Step1_SimpleXInfo, OnboardingStage.Step2_CreateProfile).contains(savedOnboardingStage) && chatModel.users.size == 1) { diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/platform/NtfManager.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/platform/NtfManager.kt index 06925e28a..6ca065086 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/platform/NtfManager.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/platform/NtfManager.kt @@ -59,7 +59,9 @@ abstract class NtfManager { awaitChatStartedIfNeeded(chatModel) if (userId != null && userId != chatModel.currentUser.value?.userId && chatModel.currentUser.value != null) { // TODO include remote host ID in desktop notifications? - chatModel.controller.changeActiveUser(null, userId, null) + chatModel.controller.showProgressIfNeeded { + chatModel.controller.changeActiveUser(null, userId, null) + } } val cInfo = chatModel.getChat(chatId)?.chatInfo chatModel.clearOverlays.value = true @@ -72,7 +74,9 @@ abstract class NtfManager { awaitChatStartedIfNeeded(chatModel) if (userId != null && userId != chatModel.currentUser.value?.userId && chatModel.currentUser.value != null) { // TODO include remote host ID in desktop notifications? - chatModel.controller.changeActiveUser(null, userId, null) + chatModel.controller.showProgressIfNeeded { + chatModel.controller.changeActiveUser(null, userId, null) + } } chatModel.chatId.value = null chatModel.clearOverlays.value = true diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/WelcomeView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/WelcomeView.kt index fa86a6291..a63c4c528 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/WelcomeView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/WelcomeView.kt @@ -21,8 +21,8 @@ import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.* import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp -import chat.simplex.common.model.ChatModel -import chat.simplex.common.model.Profile +import chat.simplex.common.model.* +import chat.simplex.common.model.ChatModel.controller import chat.simplex.common.platform.* import chat.simplex.common.ui.theme.* import chat.simplex.common.views.helpers.* @@ -76,7 +76,13 @@ fun CreateProfile(chatModel: ChatModel, close: () -> Unit) { disabled = !canCreateProfile(displayName.value), textColor = MaterialTheme.colors.primary, iconColor = MaterialTheme.colors.primary, - click = { createProfileInProfiles(chatModel, displayName.value, close) }, + click = { + if (chatModel.localUserCreated.value == true) { + createProfileInProfiles(chatModel, displayName.value, close) + } else { + createProfileInNoProfileSetup(displayName.value, close) + } + }, ) SectionTextFooter(generalGetString(MR.strings.your_profile_is_stored_on_your_device)) SectionTextFooter(generalGetString(MR.strings.profile_is_only_shared_with_your_contacts)) @@ -168,6 +174,17 @@ fun CreateFirstProfile(chatModel: ChatModel, close: () -> Unit) { } } +fun createProfileInNoProfileSetup(displayName: String, close: () -> Unit) { + withApi { + val user = controller.apiCreateActiveUser(null, Profile(displayName.trim(), "", null)) ?: return@withApi + controller.appPrefs.onboardingStage.set(OnboardingStage.Step3_CreateSimpleXAddress) + chatModel.chatRunning.value = false + controller.startChat(user) + controller.switchUIRemoteHost(null) + close() + } +} + fun createProfileInProfiles(chatModel: ChatModel, displayName: String, close: () -> Unit) { withApi { val rhId = chatModel.remoteHostId() diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ChatListView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ChatListView.kt index 301f09033..a91e5e7b3 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ChatListView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ChatListView.kt @@ -68,14 +68,14 @@ fun ChatListView(chatModel: ChatModel, settingsState: SettingsViewState, setPerf val endPadding = if (appPlatform.isDesktop) 56.dp else 0.dp var searchInList by rememberSaveable { mutableStateOf("") } val scope = rememberCoroutineScope() - val (userPickerState, scaffoldState, switchingUsersAndHosts ) = settingsState + val (userPickerState, scaffoldState ) = settingsState Scaffold(topBar = { Box(Modifier.padding(end = endPadding)) { ChatListToolbar(chatModel, scaffoldState.drawerState, userPickerState, stopped) { searchInList = it.trim() } } }, scaffoldState = scaffoldState, drawerContent = { SettingsView(chatModel, setPerformLA, scaffoldState.drawerState) }, drawerScrimColor = MaterialTheme.colors.onSurface.copy(alpha = if (isInDarkTheme()) 0.16f else 0.32f), drawerGesturesEnabled = appPlatform.isAndroid, floatingActionButton = { - if (searchInList.isEmpty()) { + if (searchInList.isEmpty() && !chatModel.desktopNoUserNoRemote) { FloatingActionButton( onClick = { if (!stopped) { @@ -104,7 +104,7 @@ fun ChatListView(chatModel: ChatModel, settingsState: SettingsViewState, setPerf ) { if (chatModel.chats.isNotEmpty()) { ChatList(chatModel, search = searchInList) - } else if (!switchingUsersAndHosts.value) { + } else if (!chatModel.switchingUsersAndHosts.value && !chatModel.desktopNoUserNoRemote) { Box(Modifier.fillMaxSize()) { if (!stopped && !newChatSheetState.collectAsState().value.isVisible()) { OnboardingButtons(showNewChatSheet) @@ -121,19 +121,11 @@ fun ChatListView(chatModel: ChatModel, settingsState: SettingsViewState, setPerf NewChatSheet(chatModel, newChatSheetState, stopped, hideNewChatSheet) } if (appPlatform.isAndroid) { - UserPicker(chatModel, userPickerState, switchingUsersAndHosts) { + UserPicker(chatModel, userPickerState) { scope.launch { if (scaffoldState.drawerState.isOpen) scaffoldState.drawerState.close() else scaffoldState.drawerState.open() } userPickerState.value = AnimatedViewState.GONE } } - if (switchingUsersAndHosts.value) { - Box( - Modifier.fillMaxSize().clickable(enabled = false, onClick = {}), - contentAlignment = Alignment.Center - ) { - ProgressIndicator() - } - } } @Composable @@ -209,7 +201,7 @@ private fun ChatListToolbar(chatModel: ChatModel, drawerState: DrawerState, user navigationButton = { if (showSearch) { NavigationButtonBack(hideSearchOnBack) - } else if (chatModel.users.isEmpty()) { + } else if (chatModel.users.isEmpty() && !chatModel.desktopNoUserNoRemote) { NavigationButtonMenu { scope.launch { if (drawerState.isOpen) drawerState.close() else drawerState.open() } } } else { val users by remember { derivedStateOf { chatModel.users.filter { u -> u.user.activeUser || !u.user.hidden } } } @@ -304,17 +296,6 @@ private fun ToggleFilterButton() { } } -@Composable -private fun ProgressIndicator() { - CircularProgressIndicator( - Modifier - .padding(horizontal = 2.dp) - .size(30.dp), - color = MaterialTheme.colors.secondary, - strokeWidth = 2.5.dp - ) -} - @Composable expect fun DesktopActiveCallOverlayLayout(newChatSheetState: MutableStateFlow) diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ShareListView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ShareListView.kt index ecd47c937..8338d2960 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ShareListView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ShareListView.kt @@ -26,7 +26,7 @@ import kotlinx.coroutines.flow.MutableStateFlow @Composable fun ShareListView(chatModel: ChatModel, settingsState: SettingsViewState, stopped: Boolean) { var searchInList by rememberSaveable { mutableStateOf("") } - val (userPickerState, scaffoldState, switchingUsersAndHosts) = settingsState + val (userPickerState, scaffoldState) = settingsState val endPadding = if (appPlatform.isDesktop) 56.dp else 0.dp Scaffold( Modifier.padding(end = endPadding), @@ -47,7 +47,7 @@ fun ShareListView(chatModel: ChatModel, settingsState: SettingsViewState, stoppe } } if (appPlatform.isAndroid) { - UserPicker(chatModel, userPickerState, switchingUsersAndHosts, showSettings = false, showCancel = true, cancelClicked = { + UserPicker(chatModel, userPickerState, showSettings = false, showCancel = true, cancelClicked = { chatModel.sharedContent.value = null userPickerState.value = AnimatedViewState.GONE }) diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/UserPicker.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/UserPicker.kt index 46a333f61..d87c05a91 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/UserPicker.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/UserPicker.kt @@ -26,7 +26,9 @@ 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.CreateProfile import chat.simplex.common.views.remote.* +import chat.simplex.common.views.usersettings.doWithAuth import chat.simplex.res.MR import dev.icerock.moko.resources.compose.stringResource import kotlinx.coroutines.delay @@ -38,7 +40,6 @@ import kotlin.math.roundToInt fun UserPicker( chatModel: ChatModel, userPickerState: MutableStateFlow, - switchingUsersAndHosts: MutableState, showSettings: Boolean = true, showCancel: Boolean = false, cancelClicked: () -> Unit = {}, @@ -123,14 +124,10 @@ fun UserPicker( userPickerState.value = AnimatedViewState.HIDING if (!u.user.activeUser) { scope.launch { - val job = launch { - delay(500) - switchingUsersAndHosts.value = true + controller.showProgressIfNeeded { + ModalManager.closeAllModalsEverywhere() + chatModel.controller.changeActiveUser(u.user.remoteHostId, u.user.userId, null) } - ModalManager.closeAllModalsEverywhere() - chatModel.controller.changeActiveUser(u.user.remoteHostId, u.user.userId, null) - job.cancel() - switchingUsersAndHosts.value = false } } } @@ -162,13 +159,13 @@ fun UserPicker( val currentRemoteHost = remember { chatModel.currentRemoteHost }.value Column(Modifier.weight(1f).verticalScroll(rememberScrollState())) { if (remoteHosts.isNotEmpty()) { - if (currentRemoteHost == null) { + if (currentRemoteHost == null && chatModel.localUserCreated.value == true) { LocalDevicePickerItem(true) { userPickerState.value = AnimatedViewState.HIDING switchToLocalDevice() } Divider(Modifier.requiredHeight(1.dp)) - } else { + } else if (currentRemoteHost != null) { val connecting = rememberSaveable { mutableStateOf(false) } RemoteHostPickerItem(currentRemoteHost, actionButtonClick = { @@ -176,7 +173,7 @@ fun UserPicker( stopRemoteHostAndReloadHosts(currentRemoteHost, true) }) { userPickerState.value = AnimatedViewState.HIDING - switchToRemoteHost(currentRemoteHost, switchingUsersAndHosts, connecting) + switchToRemoteHost(currentRemoteHost, connecting) } Divider(Modifier.requiredHeight(1.dp)) } @@ -184,7 +181,7 @@ fun UserPicker( UsersView() - if (remoteHosts.isNotEmpty() && currentRemoteHost != null) { + if (remoteHosts.isNotEmpty() && currentRemoteHost != null && chatModel.localUserCreated.value == true) { LocalDevicePickerItem(false) { userPickerState.value = AnimatedViewState.HIDING switchToLocalDevice() @@ -199,7 +196,7 @@ fun UserPicker( stopRemoteHostAndReloadHosts(h, false) }) { userPickerState.value = AnimatedViewState.HIDING - switchToRemoteHost(h, switchingUsersAndHosts, connecting) + switchToRemoteHost(h, connecting) } Divider(Modifier.requiredHeight(1.dp)) } @@ -220,6 +217,18 @@ fun UserPicker( userPickerState.value = AnimatedViewState.GONE } Divider(Modifier.requiredHeight(1.dp)) + } else if (chatModel.desktopNoUserNoRemote) { + CreateInitialProfile { + doWithAuth(generalGetString(MR.strings.auth_open_chat_profiles), generalGetString(MR.strings.auth_log_in_using_credential)) { + ModalManager.center.showModalCloseable { close -> + LaunchedEffect(Unit) { + userPickerState.value = AnimatedViewState.HIDING + } + CreateProfile(chat.simplex.common.platform.chatModel, close) + } + } + } + Divider(Modifier.requiredHeight(1.dp)) } if (showSettings) { SettingsPickerItem(settingsClicked) @@ -401,6 +410,16 @@ private fun LinkAMobilePickerItem(onClick: () -> Unit) { } } +@Composable +private fun CreateInitialProfile(onClick: () -> Unit) { + SectionItemView(onClick, padding = PaddingValues(start = DEFAULT_PADDING + 7.dp, end = DEFAULT_PADDING), minHeight = 68.dp) { + val text = generalGetString(MR.strings.create_chat_profile) + Icon(painterResource(MR.images.ic_manage_accounts), 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) { @@ -441,21 +460,15 @@ private fun switchToLocalDevice() { } } -private fun switchToRemoteHost(h: RemoteHostInfo, switchingUsersAndHosts: MutableState, connecting: MutableState) { +private fun switchToRemoteHost(h: RemoteHostInfo, connecting: MutableState) { if (!h.activeHost()) { withBGApi { - val job = launch { - delay(500) - switchingUsersAndHosts.value = true - } ModalManager.closeAllModalsEverywhere() if (h.sessionState != null) { chatModel.controller.switchUIRemoteHost(h.remoteHostId) } else { connectMobileDevice(h, connecting) } - job.cancel() - switchingUsersAndHosts.value = false } } else { connectMobileDevice(h, connecting) diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/Section.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/Section.kt index 16d7e88d6..ba0edb98d 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/Section.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/Section.kt @@ -10,6 +10,7 @@ import androidx.compose.ui.graphics.painter.Painter import androidx.compose.ui.platform.LocalDensity import dev.icerock.moko.resources.compose.painterResource import androidx.compose.ui.text.AnnotatedString +import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.* import chat.simplex.common.platform.onRightClick @@ -202,13 +203,14 @@ fun SectionTextFooter(text: String) { } @Composable -fun SectionTextFooter(text: AnnotatedString) { +fun SectionTextFooter(text: AnnotatedString, textAlign: TextAlign = TextAlign.Start) { Text( text, Modifier.padding(start = DEFAULT_PADDING, end = DEFAULT_PADDING, top = DEFAULT_PADDING_HALF).fillMaxWidth(0.9F), color = MaterialTheme.colors.secondary, lineHeight = 18.sp, - fontSize = 14.sp + fontSize = 14.sp, + textAlign = textAlign ) } diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/onboarding/CreateSimpleXAddress.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/onboarding/CreateSimpleXAddress.kt index 49e62fb06..092fb8bf6 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/onboarding/CreateSimpleXAddress.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/onboarding/CreateSimpleXAddress.kt @@ -182,6 +182,10 @@ private fun prepareChatBeforeAddressCreation(rhId: Long?) { val user = chatModel.controller.apiGetActiveUser(rhId) ?: return@withApi chatModel.currentUser.value = user if (chatModel.users.isEmpty()) { + if (appPlatform.isDesktop) { + // Make possible to use chat after going to remote device linking and returning back to local profile creation + chatModel.chatRunning.value = false + } chatModel.controller.startChat(user) } else { val users = chatModel.controller.listUsers(rhId) diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/onboarding/LinkAMobileView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/onboarding/LinkAMobileView.kt new file mode 100644 index 000000000..03b3d6dbd --- /dev/null +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/onboarding/LinkAMobileView.kt @@ -0,0 +1,91 @@ +package chat.simplex.common.views.onboarding + +import SectionTextFooter +import SectionView +import androidx.compose.foundation.layout.* +import androidx.compose.runtime.* +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextDecoration +import androidx.compose.ui.unit.dp +import chat.simplex.common.model.ChatModel +import chat.simplex.common.platform.chatModel +import chat.simplex.common.ui.theme.DEFAULT_PADDING +import chat.simplex.common.views.helpers.* +import chat.simplex.common.views.remote.AddingMobileDevice +import chat.simplex.common.views.remote.DeviceNameField +import chat.simplex.common.views.usersettings.PreferenceToggle +import chat.simplex.res.MR +import dev.icerock.moko.resources.compose.painterResource +import dev.icerock.moko.resources.compose.stringResource + +@Composable +fun LinkAMobile() { + val connecting = rememberSaveable { mutableStateOf(false) } + val deviceName = chatModel.controller.appPrefs.deviceNameForRemoteAccess + var deviceNameInQrCode by remember { mutableStateOf(chatModel.controller.appPrefs.deviceNameForRemoteAccess.get()) } + val staleQrCode = remember { mutableStateOf(false) } + + LinkAMobileLayout( + deviceName = remember { deviceName.state }, + connecting, + staleQrCode, + updateDeviceName = { + withBGApi { + if (it != "" && it != deviceName.get()) { + chatModel.controller.setLocalDeviceName(it) + deviceName.set(it) + staleQrCode.value = deviceName.get() != deviceNameInQrCode + } + } + } + ) + KeyChangeEffect(staleQrCode.value) { + if (!staleQrCode.value) { + deviceNameInQrCode = deviceName.get() + } + } +} + +@Composable +private fun LinkAMobileLayout( + deviceName: State, + connecting: MutableState, + staleQrCode: MutableState, + updateDeviceName: (String) -> Unit, +) { + Column(Modifier.padding(top = 20.dp)) { + AppBarTitle(stringResource(if (remember { chatModel.remoteHosts }.isEmpty()) MR.strings.link_a_mobile else MR.strings.linked_mobiles)) + Row(Modifier.weight(1f).padding(horizontal = DEFAULT_PADDING * 2), verticalAlignment = Alignment.CenterVertically) { + Column( + Modifier.weight(0.3f), + verticalArrangement = Arrangement.spacedBy(8.dp) + ) { + SectionView(generalGetString(MR.strings.this_device_name).uppercase()) { + DeviceNameField(deviceName.value ?: "") { updateDeviceName(it) } + SectionTextFooter(generalGetString(MR.strings.this_device_name_shared_with_mobile)) + PreferenceToggle(stringResource(MR.strings.multicast_discoverable_via_local_network), remember { ChatModel.controller.appPrefs.offerRemoteMulticast.state }.value) { + ChatModel.controller.appPrefs.offerRemoteMulticast.set(it) + } + } + } + Box(Modifier.weight(0.7f)) { + AddingMobileDevice(false, staleQrCode, connecting) { + if (chatModel.remoteHosts.isEmpty()) { + chatModel.controller.appPrefs.onboardingStage.set(OnboardingStage.Step1_SimpleXInfo) + } else { + chatModel.controller.appPrefs.onboardingStage.set(OnboardingStage.OnboardingComplete) + } + } + } + } + SimpleButtonDecorated( + text = stringResource(MR.strings.about_simplex), + icon = painterResource(MR.images.ic_arrow_back_ios_new), + textDecoration = TextDecoration.None, + fontWeight = FontWeight.Medium + ) { chatModel.controller.appPrefs.onboardingStage.set(OnboardingStage.Step1_SimpleXInfo) } + } +} diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/onboarding/OnboardingView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/onboarding/OnboardingView.kt index 0a48bbd1e..d4c63248e 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/onboarding/OnboardingView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/onboarding/OnboardingView.kt @@ -3,6 +3,7 @@ package chat.simplex.common.views.onboarding enum class OnboardingStage { Step1_SimpleXInfo, Step2_CreateProfile, + LinkAMobile, Step2_5_SetupDatabasePassphrase, Step3_CreateSimpleXAddress, Step4_SetNotificationsMode, diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/onboarding/SetupDatabasePassphrase.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/onboarding/SetupDatabasePassphrase.kt index a4ebd23de..d9d012a50 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/onboarding/SetupDatabasePassphrase.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/onboarding/SetupDatabasePassphrase.kt @@ -43,7 +43,11 @@ fun SetupDatabasePassphrase(m: ChatModel) { val newKey = rememberSaveable { mutableStateOf("") } val confirmNewKey = rememberSaveable { mutableStateOf("") } fun nextStep() { - m.controller.appPrefs.onboardingStage.set(OnboardingStage.Step3_CreateSimpleXAddress) + if (appPlatform.isAndroid || chatModel.currentUser.value != null) { + m.controller.appPrefs.onboardingStage.set(OnboardingStage.Step3_CreateSimpleXAddress) + } else { + m.controller.appPrefs.onboardingStage.set(OnboardingStage.LinkAMobile) + } } SetupDatabasePassphraseLayout( currentKey, @@ -159,10 +163,7 @@ private fun SetupDatabasePassphraseLayout( } }, isValid = { confirmNewKey.value == "" || newKey.value == confirmNewKey.value }, - keyboardActions = KeyboardActions(onDone = { - if (!disabled) onClickUpdate() - defaultKeyboardAction(ImeAction.Done) - }), + keyboardActions = KeyboardActions(onDone = { defaultKeyboardAction(ImeAction.Done) }), ) Box(Modifier.align(Alignment.CenterHorizontally).padding(vertical = DEFAULT_PADDING)) { diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/onboarding/SimpleXInfo.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/onboarding/SimpleXInfo.kt index f20c4508b..2aad2556a 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/onboarding/SimpleXInfo.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/onboarding/SimpleXInfo.kt @@ -8,6 +8,7 @@ import androidx.compose.material.* import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.painter.Painter import dev.icerock.moko.resources.compose.painterResource import dev.icerock.moko.resources.compose.stringResource @@ -99,26 +100,22 @@ private fun InfoRow(icon: Painter, titleId: StringResource, textId: StringResour } @Composable -fun OnboardingActionButton(user: User?, onboardingStage: SharedPreference, onclick: (() -> Unit)? = null) { - if (user == null) { - OnboardingActionButton(MR.strings.create_your_profile, onboarding = OnboardingStage.Step2_CreateProfile, true, onclick) - } else { - OnboardingActionButton(MR.strings.make_private_connection, onboarding = OnboardingStage.OnboardingComplete, true, onclick) - } -} +expect fun OnboardingActionButton(user: User?, onboardingStage: SharedPreference, onclick: (() -> Unit)? = null) @Composable fun OnboardingActionButton( labelId: StringResource, onboarding: OnboardingStage?, border: Boolean, + icon: Painter? = null, + iconColor: Color = MaterialTheme.colors.primary, onclick: (() -> Unit)? ) { val modifier = if (border) { Modifier .border(border = BorderStroke(1.dp, MaterialTheme.colors.primary), shape = RoundedCornerShape(50)) .padding( - horizontal = DEFAULT_PADDING * 2, + horizontal = if (icon == null) DEFAULT_PADDING * 2 else DEFAULT_PADDING_HALF, vertical = 4.dp ) } else { @@ -131,6 +128,9 @@ fun OnboardingActionButton( ChatController.appPrefs.onboardingStage.set(onboarding) } }, modifier) { + if (icon != null) { + Icon(icon, stringResource(labelId), Modifier.padding(end = DEFAULT_PADDING_HALF), tint = iconColor) + } Text(stringResource(labelId), style = MaterialTheme.typography.h2, color = MaterialTheme.colors.primary, fontSize = 20.sp) Icon( painterResource(MR.images.ic_arrow_forward_ios), "next stage", tint = MaterialTheme.colors.primary, diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/remote/ConnectMobileView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/remote/ConnectMobileView.kt index 53f0339be..b6ed14150 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/remote/ConnectMobileView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/remote/ConnectMobileView.kt @@ -13,12 +13,14 @@ import androidx.compose.foundation.text.selection.SelectionContainer import androidx.compose.material.* import androidx.compose.runtime.* import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.platform.LocalClipboardManager import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.font.* import androidx.compose.ui.text.input.* +import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import chat.simplex.common.model.* @@ -97,9 +99,11 @@ fun ConnectMobileLayout( SectionDividerSpaced(maxBottomPadding = false) } SectionView(stringResource(MR.strings.devices).uppercase()) { - SettingsActionItemWithContent(text = stringResource(MR.strings.this_device), icon = painterResource(MR.images.ic_desktop), click = connectDesktop) { - if (connectedHost.value == null) { - Icon(painterResource(MR.images.ic_done_filled), null, Modifier.size(20.dp), tint = MaterialTheme.colors.onBackground) + if (chatModel.localUserCreated.value == true) { + SettingsActionItemWithContent(text = stringResource(MR.strings.this_device), icon = painterResource(MR.images.ic_desktop), click = connectDesktop) { + if (connectedHost.value == null) { + Icon(painterResource(MR.images.ic_done_filled), null, Modifier.size(20.dp), tint = MaterialTheme.colors.onBackground) + } } } @@ -162,26 +166,37 @@ fun DeviceNameField( @Composable private fun ConnectMobileViewLayout( - title: String, + title: String?, invitation: String?, deviceName: String?, sessionCode: String?, - port: String? + port: String?, + staleQrCode: Boolean = false, + refreshQrCode: () -> Unit = {} ) { Column( Modifier.fillMaxWidth().verticalScroll(rememberScrollState()), verticalArrangement = Arrangement.spacedBy(8.dp) ) { - AppBarTitle(title) + if (title != null) { + AppBarTitle(title) + } SectionView { 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)) + Box { + QRCode( + invitation, Modifier + .padding(start = DEFAULT_PADDING, top = DEFAULT_PADDING_HALF, end = DEFAULT_PADDING, bottom = DEFAULT_PADDING_HALF) + .aspectRatio(1f) + ) + if (staleQrCode) { + Box(Modifier.matchParentSize().background(MaterialTheme.colors.background.copy(alpha = 0.9f)), contentAlignment = Alignment.Center) { + SimpleButtonDecorated(stringResource(MR.strings.refresh_qr_code), painterResource(MR.images.ic_refresh), click = refreshQrCode) + } + } + } + SectionTextFooter(annotatedStringResource(MR.strings.open_on_mobile_and_scan_qr_code), textAlign = TextAlign.Center) + SectionTextFooter(annotatedStringResource(MR.strings.waiting_for_mobile_to_connect_on_port, port), textAlign = TextAlign.Center) if (remember { controller.appPrefs.developerTools.state }.value) { val clipboard = LocalClipboardManager.current @@ -237,55 +252,72 @@ fun connectMobileDevice(rh: RemoteHostInfo, connecting: MutableState) { private fun showAddingMobileDevice(connecting: MutableState) { ModalManager.start.showModalCloseable { close -> - val invitation = rememberSaveable { mutableStateOf(null) } - val port = rememberSaveable { mutableStateOf(null) } - val pairing = remember { chatModel.remoteHostPairing } - val sessionCode = when (val state = pairing.value?.second) { - is RemoteHostSessionState.PendingConfirmation -> state.sessionCode - else -> null + AddingMobileDevice(true, remember { mutableStateOf(false) }, connecting, close) + } +} + +@Composable +fun AddingMobileDevice(showTitle: Boolean, staleQrCode: MutableState, connecting: MutableState, close: () -> Unit) { + val invitation = rememberSaveable { mutableStateOf(null) } + val port = rememberSaveable { mutableStateOf(null) } + val startRemoteHost = suspend { + val r = chatModel.controller.startRemoteHost(null, controller.appPrefs.offerRemoteMulticast.get()) + if (r != null) { + connecting.value = true + invitation.value = r.second + port.value = r.third + chatModel.remoteHostPairing.value = null to RemoteHostSessionState.Starting } - /** It's needed to prevent screen flashes when [chatModel.newRemoteHostPairing] sets to null in background */ - var cachedSessionCode by remember { mutableStateOf(null) } - if (cachedSessionCode == null && sessionCode != null) { - cachedSessionCode = sessionCode - } - val remoteDeviceName = pairing.value?.first?.hostDeviceName - ConnectMobileViewLayout( - title = if (cachedSessionCode == null) stringResource(MR.strings.link_a_mobile) else stringResource(MR.strings.verify_connection), - invitation = invitation.value, - deviceName = remoteDeviceName, - sessionCode = cachedSessionCode, - port = port.value - ) - val oldRemoteHostId by remember { mutableStateOf(chatModel.currentRemoteHost.value?.remoteHostId) } - LaunchedEffect(remember { chatModel.currentRemoteHost }.value) { - if (chatModel.currentRemoteHost.value?.remoteHostId != null && chatModel.currentRemoteHost.value?.remoteHostId != oldRemoteHostId) { - close() - } - } - KeyChangeEffect(pairing.value) { - if (pairing.value == null) { - close() - } - } - DisposableEffect(Unit) { + } + val pairing = remember { chatModel.remoteHostPairing } + val sessionCode = when (val state = pairing.value?.second) { + is RemoteHostSessionState.PendingConfirmation -> state.sessionCode + else -> null + } + /** It's needed to prevent screen flashes when [chatModel.newRemoteHostPairing] sets to null in background */ + var cachedSessionCode by remember { mutableStateOf(null) } + if (cachedSessionCode == null && sessionCode != null) { + cachedSessionCode = sessionCode + } + val remoteDeviceName = pairing.value?.first?.hostDeviceName + ConnectMobileViewLayout( + title = if (!showTitle) null else if (cachedSessionCode == null) stringResource(MR.strings.link_a_mobile) else stringResource(MR.strings.verify_connection), + invitation = invitation.value, + deviceName = remoteDeviceName, + sessionCode = cachedSessionCode, + port = port.value, + staleQrCode = staleQrCode.value, + refreshQrCode = { withBGApi { - val r = chatModel.controller.startRemoteHost(null, controller.appPrefs.offerRemoteMulticast.get()) - if (r != null) { - connecting.value = true - invitation.value = r.second - port.value = r.third - chatModel.remoteHostPairing.value = null to RemoteHostSessionState.Starting + if (chatController.stopRemoteHost(null)) { + startRemoteHost() + staleQrCode.value = false } } - onDispose { - if (chatModel.currentRemoteHost.value?.remoteHostId == oldRemoteHostId) { - withBGApi { - chatController.stopRemoteHost(null) - } + }, + ) + val oldRemoteHostId by remember { mutableStateOf(chatModel.currentRemoteHost.value?.remoteHostId) } + LaunchedEffect(remember { chatModel.currentRemoteHost }.value) { + if (chatModel.currentRemoteHost.value?.remoteHostId != null && chatModel.currentRemoteHost.value?.remoteHostId != oldRemoteHostId) { + close() + } + } + KeyChangeEffect(pairing.value) { + if (pairing.value == null) { + close() + } + } + DisposableEffect(Unit) { + withBGApi { + startRemoteHost() + } + onDispose { + if (chatModel.currentRemoteHost.value?.remoteHostId == oldRemoteHostId) { + withBGApi { + chatController.stopRemoteHost(null) } - chatModel.remoteHostPairing.value = null } + chatModel.remoteHostPairing.value = null } } } diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/NetworkAndServers.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/NetworkAndServers.kt index ad30b57c6..a96de71b9 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/NetworkAndServers.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/NetworkAndServers.kt @@ -25,6 +25,7 @@ import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import chat.simplex.common.model.* +import chat.simplex.common.platform.chatModel import chat.simplex.common.ui.theme.* import chat.simplex.common.views.chat.item.ClickableText import chat.simplex.common.views.helpers.* @@ -169,18 +170,20 @@ fun NetworkAndServersView( verticalArrangement = Arrangement.spacedBy(8.dp) ) { AppBarTitle(stringResource(MR.strings.network_and_servers)) - SectionView(generalGetString(MR.strings.settings_section_title_messages)) { - SettingsActionItem(painterResource(MR.images.ic_dns), stringResource(MR.strings.smp_servers), showCustomModal { m, close -> ProtocolServersView(m, m.remoteHostId, ServerProtocol.SMP, close) }) + if (!chatModel.desktopNoUserNoRemote) { + SectionView(generalGetString(MR.strings.settings_section_title_messages)) { + SettingsActionItem(painterResource(MR.images.ic_dns), stringResource(MR.strings.smp_servers), showCustomModal { m, close -> ProtocolServersView(m, m.remoteHostId, ServerProtocol.SMP, close) }) - SettingsActionItem(painterResource(MR.images.ic_dns), stringResource(MR.strings.xftp_servers), showCustomModal { m, close -> ProtocolServersView(m, m.remoteHostId, ServerProtocol.XFTP, close) }) + SettingsActionItem(painterResource(MR.images.ic_dns), stringResource(MR.strings.xftp_servers), showCustomModal { m, close -> ProtocolServersView(m, m.remoteHostId, ServerProtocol.XFTP, close) }) - if (currentRemoteHost == null) { - UseSocksProxySwitch(networkUseSocksProxy, proxyPort, toggleSocksProxy, showSettingsModal) - UseOnionHosts(onionHosts, networkUseSocksProxy, showSettingsModal, useOnion) - if (developerTools) { - SessionModePicker(sessionMode, showSettingsModal, updateSessionMode) + if (currentRemoteHost == null) { + UseSocksProxySwitch(networkUseSocksProxy, proxyPort, toggleSocksProxy, showSettingsModal) + UseOnionHosts(onionHosts, networkUseSocksProxy, showSettingsModal, useOnion) + if (developerTools) { + SessionModePicker(sessionMode, showSettingsModal, updateSessionMode) + } + SettingsActionItem(painterResource(MR.images.ic_cable), stringResource(MR.strings.network_settings), showSettingsModal { AdvancedNetworkSettingsView(it) }) } - SettingsActionItem(painterResource(MR.images.ic_cable), stringResource(MR.strings.network_settings), showSettingsModal { AdvancedNetworkSettingsView(it) }) } } if (currentRemoteHost == null && networkUseSocksProxy.value) { @@ -192,7 +195,7 @@ fun NetworkAndServersView( } } Divider(Modifier.padding(start = DEFAULT_PADDING_HALF, top = 32.dp, end = DEFAULT_PADDING_HALF, bottom = 30.dp)) - } else { + } else if (!chatModel.desktopNoUserNoRemote) { Divider(Modifier.padding(start = DEFAULT_PADDING_HALF, top = 24.dp, end = DEFAULT_PADDING_HALF, bottom = 30.dp)) } diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/PrivacySettings.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/PrivacySettings.kt index 1a5aa49eb..3d2b7b7fa 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/PrivacySettings.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/PrivacySettings.kt @@ -92,7 +92,6 @@ fun PrivacySettingsView( chatModel.simplexLinkMode.value = it }) } - SectionDividerSpaced() val currentUser = chatModel.currentUser.value if (currentUser != null) { @@ -142,39 +141,42 @@ fun PrivacySettingsView( } } - DeliveryReceiptsSection( - currentUser = currentUser, - setOrAskSendReceiptsContacts = { enable -> - val contactReceiptsOverrides = chatModel.chats.fold(0) { count, chat -> - if (chat.chatInfo is ChatInfo.Direct) { - val sendRcpts = chat.chatInfo.contact.chatSettings.sendRcpts - count + (if (sendRcpts == null || sendRcpts == enable) 0 else 1) + if (!chatModel.desktopNoUserNoRemote) { + SectionDividerSpaced() + DeliveryReceiptsSection( + currentUser = currentUser, + setOrAskSendReceiptsContacts = { enable -> + val contactReceiptsOverrides = chatModel.chats.fold(0) { count, chat -> + if (chat.chatInfo is ChatInfo.Direct) { + val sendRcpts = chat.chatInfo.contact.chatSettings.sendRcpts + count + (if (sendRcpts == null || sendRcpts == enable) 0 else 1) + } else { + count + } + } + if (contactReceiptsOverrides == 0) { + setSendReceiptsContacts(enable, clearOverrides = false) } else { - count + showUserContactsReceiptsAlert(enable, contactReceiptsOverrides, ::setSendReceiptsContacts) + } + }, + setOrAskSendReceiptsGroups = { enable -> + val groupReceiptsOverrides = chatModel.chats.fold(0) { count, chat -> + if (chat.chatInfo is ChatInfo.Group) { + val sendRcpts = chat.chatInfo.groupInfo.chatSettings.sendRcpts + count + (if (sendRcpts == null || sendRcpts == enable) 0 else 1) + } else { + count + } + } + if (groupReceiptsOverrides == 0) { + setSendReceiptsGroups(enable, clearOverrides = false) + } else { + showUserGroupsReceiptsAlert(enable, groupReceiptsOverrides, ::setSendReceiptsGroups) } } - if (contactReceiptsOverrides == 0) { - setSendReceiptsContacts(enable, clearOverrides = false) - } else { - showUserContactsReceiptsAlert(enable, contactReceiptsOverrides, ::setSendReceiptsContacts) - } - }, - setOrAskSendReceiptsGroups = { enable -> - val groupReceiptsOverrides = chatModel.chats.fold(0) { count, chat -> - if (chat.chatInfo is ChatInfo.Group) { - val sendRcpts = chat.chatInfo.groupInfo.chatSettings.sendRcpts - count + (if (sendRcpts == null || sendRcpts == enable) 0 else 1) - } else { - count - } - } - if (groupReceiptsOverrides == 0) { - setSendReceiptsGroups(enable, clearOverrides = false) - } else { - showUserGroupsReceiptsAlert(enable, groupReceiptsOverrides, ::setSendReceiptsGroups) - } - } - ) + ) + } } SectionBottomSpacer() } diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/SettingsView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/SettingsView.kt index dfe358152..c73cb0142 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/SettingsView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/SettingsView.kt @@ -25,6 +25,7 @@ import androidx.compose.ui.unit.* import chat.simplex.common.model.* import chat.simplex.common.platform.* import chat.simplex.common.ui.theme.* +import chat.simplex.common.views.CreateProfile import chat.simplex.common.views.database.DatabaseView import chat.simplex.common.views.helpers.* import chat.simplex.common.views.onboarding.SimpleXInfo @@ -38,76 +39,39 @@ import kotlinx.coroutines.launch fun SettingsView(chatModel: ChatModel, setPerformLA: (Boolean) -> Unit, drawerState: DrawerState) { val user = chatModel.currentUser.value val stopped = chatModel.chatRunning.value == false - - if (user != null) { - val requireAuth = remember { chatModel.controller.appPrefs.performLA.state } - SettingsLayout( - profile = user.profile, - stopped, - chatModel.chatDbEncrypted.value == true, - remember { chatModel.controller.appPrefs.storeDBPassphrase.state }.value, - remember { chatModel.controller.appPrefs.notificationsMode.state }, - user.displayName, - setPerformLA = setPerformLA, - showModal = { modalView -> { ModalManager.start.showModal { modalView(chatModel) } } }, - showSettingsModal = { modalView -> { ModalManager.start.showModal(true) { modalView(chatModel) } } }, - showSettingsModalWithSearch = { modalView -> - ModalManager.start.showCustomModal { close -> - val search = rememberSaveable { mutableStateOf("") } - ModalView( - { close() }, - endButtons = { - SearchTextField(Modifier.fillMaxWidth(), placeholder = stringResource(MR.strings.search_verb), alwaysVisible = true) { search.value = it } - }, - content = { modalView(chatModel, search) }) + SettingsLayout( + profile = user?.profile, + stopped, + chatModel.chatDbEncrypted.value == true, + remember { chatModel.controller.appPrefs.storeDBPassphrase.state }.value, + remember { chatModel.controller.appPrefs.notificationsMode.state }, + user?.displayName, + setPerformLA = setPerformLA, + showModal = { modalView -> { ModalManager.start.showModal { modalView(chatModel) } } }, + showSettingsModal = { modalView -> { ModalManager.start.showModal(true) { modalView(chatModel) } } }, + showSettingsModalWithSearch = { modalView -> + ModalManager.start.showCustomModal { close -> + val search = rememberSaveable { mutableStateOf("") } + ModalView( + { close() }, + endButtons = { + SearchTextField(Modifier.fillMaxWidth(), placeholder = stringResource(MR.strings.search_verb), alwaysVisible = true) { search.value = it } + }, + content = { modalView(chatModel, search) }) + } + }, + showCustomModal = { modalView -> { ModalManager.start.showCustomModal { close -> modalView(chatModel, close) } } }, + showVersion = { + withApi { + val info = chatModel.controller.apiGetVersion() + if (info != null) { + ModalManager.start.showModal { VersionInfoView(info) } } - }, - showCustomModal = { modalView -> { ModalManager.start.showCustomModal { close -> modalView(chatModel, close) } } }, - showVersion = { - withApi { - val info = chatModel.controller.apiGetVersion() - if (info != null) { - ModalManager.start.showModal { VersionInfoView(info) } - } - } - }, - withAuth = { title, desc, block -> - if (!requireAuth.value) { - block() - } else { - var autoShow = true - ModalManager.fullscreen.showModalCloseable { close -> - val onFinishAuth = { success: Boolean -> - if (success) { - close() - block() - } - } - - LaunchedEffect(Unit) { - if (autoShow) { - autoShow = false - runAuth(title, desc, onFinishAuth) - } - } - Box( - Modifier.fillMaxSize().background(MaterialTheme.colors.background), - contentAlignment = Alignment.Center - ) { - SimpleButton( - stringResource(MR.strings.auth_unlock), - icon = painterResource(MR.images.ic_lock), - click = { - runAuth(title, desc, onFinishAuth) - } - ) - } - } - } - }, - drawerState = drawerState, - ) - } + } + }, + withAuth = ::doWithAuth, + drawerState = drawerState, + ) } val simplexTeamUri = @@ -115,12 +79,12 @@ val simplexTeamUri = @Composable fun SettingsLayout( - profile: LocalProfile, + profile: LocalProfile?, stopped: Boolean, encrypted: Boolean, passphraseSaved: Boolean, notificationsMode: State, - userDisplayName: String, + userDisplayName: String?, setPerformLA: (Boolean) -> Unit, showModal: (@Composable (ChatModel) -> Unit) -> (() -> Unit), showSettingsModal: (@Composable (ChatModel) -> Unit) -> (() -> Unit), @@ -150,13 +114,22 @@ fun SettingsLayout( AppBarTitle(stringResource(MR.strings.your_settings)) SectionView(stringResource(MR.strings.settings_section_title_you)) { - SectionItemView(showCustomModal { chatModel, close -> UserProfileView(chatModel, close) }, 80.dp, padding = PaddingValues(start = 16.dp, end = DEFAULT_PADDING), disabled = stopped) { - ProfilePreview(profile, stopped = stopped) - } val profileHidden = rememberSaveable { mutableStateOf(false) } - SettingsActionItem(painterResource(MR.images.ic_manage_accounts), stringResource(MR.strings.your_chat_profiles), { withAuth(generalGetString(MR.strings.auth_open_chat_profiles), generalGetString(MR.strings.auth_log_in_using_credential)) { showSettingsModalWithSearch { it, search -> UserProfilesView(it, search, profileHidden) } } }, disabled = stopped, extraPadding = true) - SettingsActionItem(painterResource(MR.images.ic_qr_code), stringResource(MR.strings.your_simplex_contact_address), showCustomModal { it, close -> UserAddressView(it, shareViaProfile = it.currentUser.value!!.addressShared, close = close) }, disabled = stopped, extraPadding = true) - ChatPreferencesItem(showCustomModal, stopped = stopped) + if (profile != null) { + SectionItemView(showCustomModal { chatModel, close -> UserProfileView(chatModel, close) }, 80.dp, padding = PaddingValues(start = 16.dp, end = DEFAULT_PADDING), disabled = stopped) { + ProfilePreview(profile, stopped = stopped) + } + SettingsActionItem(painterResource(MR.images.ic_manage_accounts), stringResource(MR.strings.your_chat_profiles), { withAuth(generalGetString(MR.strings.auth_open_chat_profiles), generalGetString(MR.strings.auth_log_in_using_credential)) { showSettingsModalWithSearch { it, search -> UserProfilesView(it, search, profileHidden) } } }, disabled = stopped, extraPadding = true) + SettingsActionItem(painterResource(MR.images.ic_qr_code), stringResource(MR.strings.your_simplex_contact_address), showCustomModal { it, close -> UserAddressView(it, shareViaProfile = it.currentUser.value!!.addressShared, close = close) }, disabled = stopped, extraPadding = true) + ChatPreferencesItem(showCustomModal, stopped = stopped) + } else if (chatModel.localUserCreated.value == false) { + SettingsActionItem(painterResource(MR.images.ic_manage_accounts), stringResource(MR.strings.create_chat_profile), { withAuth(generalGetString(MR.strings.auth_open_chat_profiles), generalGetString(MR.strings.auth_log_in_using_credential)) { ModalManager.center.showModalCloseable { close -> + LaunchedEffect(Unit) { + closeSettings() + } + CreateProfile(chatModel, close) + } } }, disabled = stopped, extraPadding = true) + } 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() }, disabled = stopped, extraPadding = true) } else { @@ -171,15 +144,19 @@ fun SettingsLayout( SettingsActionItem(painterResource(MR.images.ic_videocam), stringResource(MR.strings.settings_audio_video_calls), showSettingsModal { CallSettingsView(it, showModal) }, disabled = stopped, extraPadding = true) SettingsActionItem(painterResource(MR.images.ic_lock), stringResource(MR.strings.privacy_and_security), showSettingsModal { PrivacySettingsView(it, showSettingsModal, setPerformLA) }, disabled = stopped, extraPadding = true) SettingsActionItem(painterResource(MR.images.ic_light_mode), stringResource(MR.strings.appearance_settings), showSettingsModal { AppearanceView(it, showSettingsModal) }, extraPadding = true) - DatabaseItem(encrypted, passphraseSaved, showSettingsModal { DatabaseView(it, showSettingsModal) }, stopped) + if (!chatModel.desktopNoUserNoRemote) { + DatabaseItem(encrypted, passphraseSaved, showSettingsModal { DatabaseView(it, showSettingsModal) }, stopped) + } } SectionDividerSpaced() SectionView(stringResource(MR.strings.settings_section_title_help)) { - SettingsActionItem(painterResource(MR.images.ic_help), stringResource(MR.strings.how_to_use_simplex_chat), showModal { HelpView(userDisplayName) }, disabled = stopped, extraPadding = true) + SettingsActionItem(painterResource(MR.images.ic_help), stringResource(MR.strings.how_to_use_simplex_chat), showModal { HelpView(userDisplayName ?: "") }, disabled = stopped, extraPadding = true) SettingsActionItem(painterResource(MR.images.ic_add), stringResource(MR.strings.whats_new), showCustomModal { _, close -> WhatsNewView(viaSettings = true, close) }, disabled = stopped, extraPadding = true) SettingsActionItem(painterResource(MR.images.ic_info), stringResource(MR.strings.about_simplex_chat), showModal { SimpleXInfo(it, onboarding = false) }, extraPadding = true) - SettingsActionItem(painterResource(MR.images.ic_tag), stringResource(MR.strings.chat_with_the_founder), { uriHandler.openVerifiedSimplexUri(simplexTeamUri) }, textColor = MaterialTheme.colors.primary, disabled = stopped, extraPadding = true) + if (!chatModel.desktopNoUserNoRemote) { + SettingsActionItem(painterResource(MR.images.ic_tag), stringResource(MR.strings.chat_with_the_founder), { uriHandler.openVerifiedSimplexUri(simplexTeamUri) }, textColor = MaterialTheme.colors.primary, disabled = stopped, extraPadding = true) + } SettingsActionItem(painterResource(MR.images.ic_mail), stringResource(MR.strings.send_us_an_email), { uriHandler.openUriCatching("mailto:chat@simplex.chat") }, textColor = MaterialTheme.colors.primary, extraPadding = true) } SectionDividerSpaced() @@ -469,6 +446,42 @@ fun PreferenceToggleWithIcon( } } +fun doWithAuth(title: String, desc: String, block: () -> Unit) { + val requireAuth = chatModel.controller.appPrefs.performLA.get() + if (!requireAuth) { + block() + } else { + var autoShow = true + ModalManager.fullscreen.showModalCloseable { close -> + val onFinishAuth = { success: Boolean -> + if (success) { + close() + block() + } + } + + LaunchedEffect(Unit) { + if (autoShow) { + autoShow = false + runAuth(title, desc, onFinishAuth) + } + } + Box( + Modifier.fillMaxSize().background(MaterialTheme.colors.background), + contentAlignment = Alignment.Center + ) { + SimpleButton( + stringResource(MR.strings.auth_unlock), + icon = painterResource(MR.images.ic_lock), + click = { + runAuth(title, desc, onFinishAuth) + } + ) + } + } + } +} + private fun runAuth(title: String, desc: String, onFinish: (success: Boolean) -> Unit) { authenticate( title, diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/UserProfilesView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/UserProfilesView.kt index 401c3afed..390f1cac9 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/UserProfilesView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/UserProfilesView.kt @@ -21,6 +21,7 @@ import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp import chat.simplex.common.model.* +import chat.simplex.common.model.ChatModel.controller import chat.simplex.common.platform.* import chat.simplex.common.ui.theme.* import chat.simplex.common.views.chat.item.ItemAction @@ -56,7 +57,9 @@ fun UserProfilesView(m: ChatModel, search: MutableState, profileHidden: ModalManager.end.closeModals() } withBGApi { - m.controller.changeActiveUser(user.remoteHostId, user.userId, userViewPassword(user, searchTextOrPassword.value.trim())) + controller.showProgressIfNeeded { + m.controller.changeActiveUser(user.remoteHostId, user.userId, userViewPassword(user, searchTextOrPassword.value.trim())) + } } }, removeUser = { user -> diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/base/strings.xml b/apps/multiplatform/common/src/commonMain/resources/MR/base/strings.xml index 22f5d2fbe..82d571d4a 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/base/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/base/strings.xml @@ -565,6 +565,7 @@ Your settings Your SimpleX address Your chat profiles + Create chat profile Database passphrase & export About SimpleX Chat How to use it @@ -1689,6 +1690,8 @@ Paste desktop address Desktop Not compatible! + Refresh + No connected mobile Coming soon! diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_refresh.svg b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_refresh.svg new file mode 100644 index 000000000..a1f27d579 --- /dev/null +++ b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_refresh.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/views/onboarding/SimpleXInfo.desktop.kt b/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/views/onboarding/SimpleXInfo.desktop.kt new file mode 100644 index 000000000..83c438e4a --- /dev/null +++ b/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/views/onboarding/SimpleXInfo.desktop.kt @@ -0,0 +1,23 @@ +package chat.simplex.common.views.onboarding + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Row +import androidx.compose.runtime.Composable +import chat.simplex.common.model.ChatModel.controller +import chat.simplex.common.model.SharedPreference +import chat.simplex.common.model.User +import chat.simplex.common.ui.theme.DEFAULT_PADDING +import chat.simplex.res.MR +import dev.icerock.moko.resources.compose.painterResource + +@Composable +actual fun OnboardingActionButton(user: User?, onboardingStage: SharedPreference, onclick: (() -> Unit)?) { + if (user == null) { + Row(horizontalArrangement = Arrangement.spacedBy(DEFAULT_PADDING * 2.5f)) { + OnboardingActionButton(MR.strings.link_a_mobile, onboarding = if (controller.appPrefs.initialRandomDBPassphrase.get()) OnboardingStage.Step2_5_SetupDatabasePassphrase else OnboardingStage.LinkAMobile, true, icon = painterResource(MR.images.ic_smartphone_300), onclick = onclick) + OnboardingActionButton(MR.strings.create_your_profile, onboarding = OnboardingStage.Step2_CreateProfile, true, icon = painterResource(MR.images.ic_desktop), onclick = onclick) + } + } else { + OnboardingActionButton(MR.strings.make_private_connection, onboarding = OnboardingStage.OnboardingComplete, true, onclick = onclick) + } +} From 540c8883a0eb57c000555dad1c1a0491ac0a235c Mon Sep 17 00:00:00 2001 From: Stanislav Dmitrenko <7953703+avently@users.noreply.github.com> Date: Fri, 1 Dec 2023 03:39:16 +0800 Subject: [PATCH 03/13] android: do not show alert too early in obboarding (#3493) --- .../android/src/main/java/chat/simplex/app/SimplexApp.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/multiplatform/android/src/main/java/chat/simplex/app/SimplexApp.kt b/apps/multiplatform/android/src/main/java/chat/simplex/app/SimplexApp.kt index b6afab4ea..90696b79a 100644 --- a/apps/multiplatform/android/src/main/java/chat/simplex/app/SimplexApp.kt +++ b/apps/multiplatform/android/src/main/java/chat/simplex/app/SimplexApp.kt @@ -75,7 +75,7 @@ class SimplexApp: Application(), LifecycleEventObserver { } Lifecycle.Event.ON_RESUME -> { isAppOnForeground = true - if (chatModel.controller.appPrefs.onboardingStage.get() == OnboardingStage.OnboardingComplete) { + if (chatModel.controller.appPrefs.onboardingStage.get() == OnboardingStage.OnboardingComplete && chatModel.currentUser.value != null) { SimplexService.showBackgroundServiceNoticeIfNeeded() } /** From b74e33b9582afb3f385580e928af0452de896b32 Mon Sep 17 00:00:00 2001 From: spaced4ndy <8711996+spaced4ndy@users.noreply.github.com> Date: Fri, 1 Dec 2023 19:02:50 +0400 Subject: [PATCH 04/13] docs: inactive group members rfc (#3419) --- .../rfcs/2023-11-21-inactive-group-members.md | 110 ++++++++++++++++++ 1 file changed, 110 insertions(+) create mode 100644 docs/rfcs/2023-11-21-inactive-group-members.md diff --git a/docs/rfcs/2023-11-21-inactive-group-members.md b/docs/rfcs/2023-11-21-inactive-group-members.md new file mode 100644 index 000000000..66e58848a --- /dev/null +++ b/docs/rfcs/2023-11-21-inactive-group-members.md @@ -0,0 +1,110 @@ +# Inactive group members + +## Problem + +Group traffic is higher than necessary due to lack of diagnosis of inactive group members. By inactive we understand group members who went offline for indefinitely long time, uninstalled application without leaving group, failed to send x.grp.leave message before deleting connection, or in any other way failed to explicitly communicate further inactivity. + +Currently other group members continue to identify such members as active and to send messages to their connections until exceeding receiving SMP queues quotas, with pending messages being slowly retried even after that. + +## Solution + +Identify inactive members and don't send messages to their connections. Silent periodically online members should continue to receive messages, so decision to mark member as inactive should be made conservatively. + +Agent: +- on SMP.QUOTA error notify client with ERR CONN QUOTA (new ConnectionErrorType QUOTA). +- on receiving QCONT notify client (new event). + +Chat, on sending side, per member: +- unanswered_snd_msg_count - number of messages that were sent consecutively without receiving a message from member. +- last_rcv_ts - timestamp of last received message. +- inactive flag. +- set inactive if: + - agent reports QUOTA error. + - on sending message: (unanswered_snd_msg_count > K) && (last_rcv_ts earlier than Ddiff days ago), Ddiff = 1/2/3 days? +- reset inactive: + - on receiving QCONT. + - on receiving message or receipt. Also reset unanswered_snd_msg_count, last_rcv_ts. +- don't send to member if inactive. + - don't send only content messages (x.msg.new, etc.) and always send messages altering group state? +- unanswered_snd_msg_count, last_rcv_ts to be tracked, checked, reset only for members with compatible version. + +Chat, on receiving side, per member: +- unanswered_rcv_msg_count - number of messages that were received consecutively without sending a message to member. +- send non-optional receipt / another (new) protocol message if: + - on receiving message: unanswered_rcv_msg_count > M, M < K. +- on sending a message or receipt to member reset unanswered_rcv_msg_count. +- unanswered_rcv_msg_count to be tracked, checked, reset only for members with compatible version. + +\*** + +Consider above condition: + +> (unanswered_snd_msg_count > K) && (last_rcv_ts earlier than Ddiff days ago) + +It still doesn't account for following situation: + +1. Sending member sends a few (N1, N1 < M) messages to silent member on day D1. +2. Sending member doesn't send messages for several days. +3. Sending member sends more messages (N2, N1 + N2 > K) to silent member on day DI (DI - D1 > diff in days in above condition), while silent member is offline. + - Sending member checks above condition and evaluates it to be true, marks silent member as inactive. + - Simply remembering last_snd_ts on sending side and adding check for it not being from several days ago to above condition is not enough, as it will be overwritten by current day sends and will only evaluate false for the first send. What could work is remembering prev_session_last_snd_ts or prev_day_last_snd_ts, but it further complicates logic, and still probably wouldn't account for some time zone differences. +4. Sending member sends yet more messages, which will not be queued for silent member marked inactive. +5. Silent member comes online, sends receipt upon receiving message fulfilling above condition: `unanswered_rcv_msg_count > M`, and will lose following messages. + - If sending member created messages from 4 as pending, and sent them upon receiving receipt from silent member, silent member would only receive them after sending member coming online. If they are in different time zones it may happen on next day. + +Same situation can occur even without step 1, simply by sending many messages while other member is offline. + +The problem is less acute the greater the difference between K and M, but making K >> M renders this whole mechanism obsolete, as we could then simply rely on QUOTA errors to mark group members inactive (and don't slow retry in agent?). + +Perhaps an acceptable way to solve this problem is to add a task to cleanup manager that would send receipts to all members on condition: (unanswered_rcv_msg_count > 0) && (last_reply_ts earlier than 1 day ago). (Adds last_reply_ts to tracking on receiving side). Perhaps it should be a task separate from cleanup manager that only occurs once per start, or with longer interval. + +\*** + +Additionally we could consider group member connection as disabled with smaller AUTH error count. Currently it's 10 messages, could be 1. + +### Delivery suspension notice + +When receiving side comes back online, replies and continues to receive messages, it has no way of knowing there was a gap in messages from sending member. To notify receiving member about delivery suspension, sending member should send notice containing shared message id of the last sent message (new protocol event) to them: + +```haskell +XGrpMemSuspended :: SharedMsgId -> ChatMsgEvent 'Json +``` + +Sending side additionally tracks: +- xgrpmemsuspended_sent flag - to only send it once. + +When processing it, receiving member creates a "gap" chat item (e.g. event saying "member x suspended delivery to you due to your inactivity, there may be a gap in messages"). + +After receiving member signals activity by sending any reply, sending member may send message history before continuing normal delivery. + +Starting point for message history: either receiving member could request history starting from specific shared message id (received in XGrpMemSuspended) with another new protocol event, or sending member can remember it instead of just flag. + +### Sending message history + +New protocol event: + +```haskell +XGrpMsgHistory :: [ChatMessage 'Json] -> ChatMsgEvent 'Json +``` + +Sending member builds messages history starting starting from requested/remembered shared message id: +- `messages` table is periodically cleaned up, so messages would be retrieved from `chat_items`. +- if chat item for starting shared message id is not found (it may have been deleted manually or as a disappearing message), abort? + - sending member could track number of skipped messages per member, but again if any chat items were deleted, older (previously successfully sent) chat items would be retrieved, resulting in duplicate messages. If receiving member has also cleaned up records in `messages` table, they wouldn't be deduplicated. + - sending member could track timestamp of first unsent message instead of shared msg id. +- sending member should probably limit maximum number of messages sent as history (100?). +- only XMsgNew events should be sent in XGrpMsgHistory (chat items to be transformed back into text messages). + - updates, deletions would be reflected in chat item list. + - reactions would be omitted. + - files would be likely expired by the time of sending history, so only file name and size may be sent in FileInvitation, with invitation being practically not acceptable. + - add new flag to CIFile "expired" for receiving member to mark chat items created based on such invitations. + - FileInvitation in MsgContainer could also contain this flag as optional to explicitly communicate that only file metadata is sent. + - alternatively sending member could re-upload files, but this seems excessive. + - XMsgNew events don't include message timestamps (instead usually broker ts is retrieved from agent message meta), so receiving member wouldn't be able to restore them from history. Perhaps history should include XGrpMsgForward events containing XMsgNew events instead. +- XGrpMsgHistory is likely to exceed message block limit. + - either multiple messages comprising a history can be batched as a single message on chat level until the block size is exceeded. + - or large history messages could be batched on agent level. + +\*** + +Same XGrpMsgHistory protocol event could be sent by host to new members, after sending introductions. From 40e69ae71377e85ae6f42da235bb88e64eb357e1 Mon Sep 17 00:00:00 2001 From: Stanislav Dmitrenko <7953703+avently@users.noreply.github.com> Date: Fri, 1 Dec 2023 23:04:00 +0800 Subject: [PATCH 05/13] desktop: enable database operations (#3495) * desktop: enable database operations * disconnect hosts button * not relaying on dev tools * different logic * different logic 2 * toggle placement --- .../chat/simplex/common/model/SimpleXAPI.kt | 5 +- .../views/database/DatabaseErrorView.kt | 3 +- .../common/views/database/DatabaseView.kt | 249 ++++++++++-------- .../common/views/remote/ConnectMobileView.kt | 1 + .../common/views/usersettings/SettingsView.kt | 4 +- .../commonMain/resources/MR/base/strings.xml | 1 + 6 files changed, 146 insertions(+), 117 deletions(-) diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt index c61772578..724e6bfcb 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt @@ -430,8 +430,9 @@ object ChatController { } suspend fun getUserChatData(rhId: Long?) { - chatModel.userAddress.value = apiGetUserAddress(rhId) - chatModel.chatItemTTL.value = getChatItemTTL(rhId) + val hasUser = chatModel.currentUser.value != null + chatModel.userAddress.value = if (hasUser) apiGetUserAddress(rhId) else null + chatModel.chatItemTTL.value = if (hasUser) getChatItemTTL(rhId) else ChatItemTTL.None updatingChatsMutex.withLock { val chats = apiGetChats(rhId) chatModel.updateChats(chats) diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/database/DatabaseErrorView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/database/DatabaseErrorView.kt index bce8fdf4f..4e5424215 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/database/DatabaseErrorView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/database/DatabaseErrorView.kt @@ -264,7 +264,8 @@ private fun DatabaseKeyField(text: MutableState, enabled: Boolean, onCli text, generalGetString(MR.strings.enter_passphrase), isValid = ::validKey, - keyboardActions = KeyboardActions(onDone = if (enabled) { + // Don't enable this on desktop since it interfere with key event listener + keyboardActions = KeyboardActions(onDone = if (enabled && appPlatform.isAndroid) { { onClick?.invoke() } } else null ), diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/database/DatabaseView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/database/DatabaseView.kt index a4c8dc981..224317f94 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/database/DatabaseView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/database/DatabaseView.kt @@ -4,6 +4,7 @@ import SectionBottomSpacer import SectionDividerSpaced import SectionTextFooter import SectionItemView +import SectionSpacer import SectionView import androidx.compose.desktop.ui.tooling.preview.Preview import androidx.compose.foundation.layout.* @@ -20,6 +21,7 @@ import androidx.compose.ui.text.* import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp import chat.simplex.common.model.* +import chat.simplex.common.model.ChatModel.controller import chat.simplex.common.model.ChatModel.updatingChatsMutex import chat.simplex.common.ui.theme.* import chat.simplex.common.views.helpers.* @@ -59,7 +61,9 @@ fun DatabaseView( val appFilesCountAndSize = remember { mutableStateOf(directoryFileCountAndSize(appFilesDir.absolutePath)) } val importArchiveLauncher = rememberFileChooserLauncher(true) { to: URI? -> if (to != null) { - importArchiveAlert(m, to, appFilesCountAndSize, progressIndicator) + importArchiveAlert(m, to, appFilesCountAndSize, progressIndicator) { + startChat(m, chatLastStart, m.chatDbChanged) + } } } val chatItemTTL = remember { mutableStateOf(m.chatItemTTL.value) } @@ -77,7 +81,6 @@ fun DatabaseView( m.chatDbEncrypted.value, m.controller.appPrefs.storeDBPassphrase.state.value, m.controller.appPrefs.initialRandomDBPassphrase, - m.controller.appPrefs.developerTools.state.value, importArchiveLauncher, chatArchiveName, chatArchiveTime, @@ -100,7 +103,13 @@ fun DatabaseView( setCiTTL(m, rhId, chatItemTTL, progressIndicator, appFilesCountAndSize) } }, - showSettingsModal + showSettingsModal, + disconnectAllHosts = { + val connected = chatModel.remoteHosts.filter { it.sessionState is RemoteHostSessionState.Connected } + connected.forEachIndexed { index, h -> + controller.stopRemoteHostAndReloadHosts(h, index == connected.lastIndex && chatModel.connectedToRemote()) + } + } ) if (progressIndicator.value) { Box( @@ -129,7 +138,6 @@ fun DatabaseLayout( chatDbEncrypted: Boolean?, passphraseSaved: Boolean, initialRandomDBPassphrase: SharedPreference, - developerTools: Boolean, importArchiveLauncher: FileChooserLauncher, chatArchiveName: MutableState, chatArchiveTime: MutableState, @@ -144,36 +152,43 @@ fun DatabaseLayout( deleteChatAlert: () -> Unit, deleteAppFilesAndMedia: () -> Unit, onChatItemTTLSelected: (ChatItemTTL) -> Unit, - showSettingsModal: (@Composable (ChatModel) -> Unit) -> (() -> Unit) + showSettingsModal: (@Composable (ChatModel) -> Unit) -> (() -> Unit), + disconnectAllHosts: () -> Unit, ) { val stopped = !runChat - val operationsDisabled = !stopped || progressIndicator + val operationsDisabled = (!stopped || progressIndicator) && !chatModel.desktopNoUserNoRemote Column( Modifier.fillMaxWidth().verticalScroll(rememberScrollState()), ) { AppBarTitle(stringResource(MR.strings.your_chat_database)) - SectionView(stringResource(MR.strings.messages_section_title).uppercase()) { - TtlOptions(chatItemTTL, enabled = rememberUpdatedState(!stopped && !progressIndicator), onChatItemTTLSelected) - } - SectionTextFooter( - remember(currentUser?.displayName) { - buildAnnotatedString { - append(generalGetString(MR.strings.messages_section_description) + " ") - withStyle(SpanStyle(fontWeight = FontWeight.Bold)) { - append(currentUser?.displayName ?: "") - } - append(".") - } + if (!chatModel.desktopNoUserNoRemote) { + SectionView(stringResource(MR.strings.messages_section_title).uppercase()) { + TtlOptions(chatItemTTL, enabled = rememberUpdatedState(!stopped && !progressIndicator), onChatItemTTLSelected) } - ) - - if (currentRemoteHost == null) { + SectionTextFooter( + remember(currentUser?.displayName) { + buildAnnotatedString { + append(generalGetString(MR.strings.messages_section_description) + " ") + withStyle(SpanStyle(fontWeight = FontWeight.Bold)) { + append(currentUser?.displayName ?: "") + } + append(".") + } + } + ) SectionDividerSpaced(maxTopPadding = true) - + } + val toggleEnabled = remember { chatModel.remoteHosts }.none { it.sessionState is RemoteHostSessionState.Connected } + if (chatModel.localUserCreated.value == true) { SectionView(stringResource(MR.strings.run_chat_section)) { - RunChatSetting(runChat, stopped, startChat, stopChatAlert) + if (!toggleEnabled) { + SectionItemView(disconnectAllHosts) { + Text(generalGetString(MR.strings.disconnect_remote_hosts), Modifier.fillMaxWidth(), color = WarningOrange) + } + } + RunChatSetting(runChat, stopped, toggleEnabled, startChat, stopChatAlert) } SectionTextFooter( if (stopped) { @@ -183,92 +198,96 @@ fun DatabaseLayout( } ) SectionDividerSpaced() - - SectionView(stringResource(MR.strings.chat_database_section)) { - val unencrypted = chatDbEncrypted == false - SettingsActionItem( - if (unencrypted) painterResource(MR.images.ic_lock_open_right) else if (useKeyChain) painterResource(MR.images.ic_vpn_key_filled) - else painterResource(MR.images.ic_lock), - stringResource(MR.strings.database_passphrase), - click = showSettingsModal() { DatabaseEncryptionView(it) }, - iconColor = if (unencrypted || (appPlatform.isDesktop && passphraseSaved)) WarningOrange else MaterialTheme.colors.secondary, - disabled = operationsDisabled - ) - if (appPlatform.isDesktop && developerTools) { - SettingsActionItem( - painterResource(MR.images.ic_folder_open), - stringResource(MR.strings.open_database_folder), - ::desktopOpenDatabaseDir, - disabled = operationsDisabled - ) - } - SettingsActionItem( - painterResource(MR.images.ic_ios_share), - stringResource(MR.strings.export_database), - click = { - if (initialRandomDBPassphrase.get()) { - exportProhibitedAlert() - } else { - exportArchive() - } - }, - textColor = MaterialTheme.colors.primary, - iconColor = MaterialTheme.colors.primary, - disabled = operationsDisabled - ) - SettingsActionItem( - painterResource(MR.images.ic_download), - stringResource(MR.strings.import_database), - { withApi { importArchiveLauncher.launch("application/zip") } }, - textColor = Color.Red, - iconColor = Color.Red, - disabled = operationsDisabled - ) - val chatArchiveNameVal = chatArchiveName.value - val chatArchiveTimeVal = chatArchiveTime.value - val chatLastStartVal = chatLastStart.value - if (chatArchiveNameVal != null && chatArchiveTimeVal != null && chatLastStartVal != null) { - val title = chatArchiveTitle(chatArchiveTimeVal, chatLastStartVal) - SettingsActionItem( - painterResource(MR.images.ic_inventory_2), - title, - click = showSettingsModal { ChatArchiveView(it, title, chatArchiveNameVal, chatArchiveTimeVal) }, - disabled = operationsDisabled - ) - } - SettingsActionItem( - painterResource(MR.images.ic_delete_forever), - stringResource(MR.strings.delete_database), - deleteChatAlert, - textColor = Color.Red, - iconColor = Color.Red, - disabled = operationsDisabled - ) - } - SectionDividerSpaced(maxTopPadding = true) - - SectionView(stringResource(MR.strings.files_and_media_section).uppercase()) { - val deleteFilesDisabled = operationsDisabled || appFilesCountAndSize.value.first == 0 - SectionItemView( - deleteAppFilesAndMedia, - disabled = deleteFilesDisabled - ) { - Text( - stringResource(if (users.size > 1) MR.strings.delete_files_and_media_for_all_users else MR.strings.delete_files_and_media_all), - color = if (deleteFilesDisabled) MaterialTheme.colors.secondary else Color.Red - ) - } - } - val (count, size) = appFilesCountAndSize.value - SectionTextFooter( - if (count == 0) { - stringResource(MR.strings.no_received_app_files) - } else { - String.format(stringResource(MR.strings.total_files_count_and_size), count, formatBytes(size)) - } - ) } + SectionView(stringResource(MR.strings.chat_database_section)) { + if (chatModel.localUserCreated.value != true && !toggleEnabled) { + SectionItemView(disconnectAllHosts) { + Text(generalGetString(MR.strings.disconnect_remote_hosts), Modifier.fillMaxWidth(), color = WarningOrange) + } + } + val unencrypted = chatDbEncrypted == false + SettingsActionItem( + if (unencrypted) painterResource(MR.images.ic_lock_open_right) else if (useKeyChain) painterResource(MR.images.ic_vpn_key_filled) + else painterResource(MR.images.ic_lock), + stringResource(MR.strings.database_passphrase), + click = showSettingsModal() { DatabaseEncryptionView(it) }, + iconColor = if (unencrypted || (appPlatform.isDesktop && passphraseSaved)) WarningOrange else MaterialTheme.colors.secondary, + disabled = operationsDisabled + ) + if (appPlatform.isDesktop) { + SettingsActionItem( + painterResource(MR.images.ic_folder_open), + stringResource(MR.strings.open_database_folder), + ::desktopOpenDatabaseDir, + disabled = operationsDisabled + ) + } + SettingsActionItem( + painterResource(MR.images.ic_ios_share), + stringResource(MR.strings.export_database), + click = { + if (initialRandomDBPassphrase.get()) { + exportProhibitedAlert() + } else { + exportArchive() + } + }, + textColor = MaterialTheme.colors.primary, + iconColor = MaterialTheme.colors.primary, + disabled = operationsDisabled + ) + SettingsActionItem( + painterResource(MR.images.ic_download), + stringResource(MR.strings.import_database), + { withApi { importArchiveLauncher.launch("application/zip") } }, + textColor = Color.Red, + iconColor = Color.Red, + disabled = operationsDisabled + ) + val chatArchiveNameVal = chatArchiveName.value + val chatArchiveTimeVal = chatArchiveTime.value + val chatLastStartVal = chatLastStart.value + if (chatArchiveNameVal != null && chatArchiveTimeVal != null && chatLastStartVal != null) { + val title = chatArchiveTitle(chatArchiveTimeVal, chatLastStartVal) + SettingsActionItem( + painterResource(MR.images.ic_inventory_2), + title, + click = showSettingsModal { ChatArchiveView(it, title, chatArchiveNameVal, chatArchiveTimeVal) }, + disabled = operationsDisabled + ) + } + SettingsActionItem( + painterResource(MR.images.ic_delete_forever), + stringResource(MR.strings.delete_database), + deleteChatAlert, + textColor = Color.Red, + iconColor = Color.Red, + disabled = operationsDisabled + ) + } + SectionDividerSpaced(maxTopPadding = true) + + SectionView(stringResource(MR.strings.files_and_media_section).uppercase()) { + val deleteFilesDisabled = operationsDisabled || appFilesCountAndSize.value.first == 0 + SectionItemView( + deleteAppFilesAndMedia, + disabled = deleteFilesDisabled + ) { + Text( + stringResource(if (users.size > 1) MR.strings.delete_files_and_media_for_all_users else MR.strings.delete_files_and_media_all), + color = if (deleteFilesDisabled) MaterialTheme.colors.secondary else Color.Red + ) + } + } + val (count, size) = appFilesCountAndSize.value + SectionTextFooter( + if (count == 0) { + stringResource(MR.strings.no_received_app_files) + } else { + String.format(stringResource(MR.strings.total_files_count_and_size), count, formatBytes(size)) + } + ) SectionBottomSpacer() } } @@ -319,6 +338,7 @@ private fun TtlOptions(current: State, enabled: State, onS fun RunChatSetting( runChat: Boolean, stopped: Boolean, + enabled: Boolean, startChat: () -> Unit, stopChatAlert: () -> Unit ) { @@ -337,6 +357,7 @@ fun RunChatSetting( stopChatAlert() } }, + enabled = enabled, ) } } @@ -501,13 +522,14 @@ private fun importArchiveAlert( m: ChatModel, importedArchiveURI: URI, appFilesCountAndSize: MutableState>, - progressIndicator: MutableState + progressIndicator: MutableState, + startChat: () -> Unit, ) { AlertManager.shared.showAlertDialog( title = generalGetString(MR.strings.import_database_question), text = generalGetString(MR.strings.your_current_chat_database_will_be_deleted_and_replaced_with_the_imported_one), confirmText = generalGetString(MR.strings.import_database_confirmation), - onConfirm = { importArchive(m, importedArchiveURI, appFilesCountAndSize, progressIndicator) }, + onConfirm = { importArchive(m, importedArchiveURI, appFilesCountAndSize, progressIndicator, startChat) }, destructive = true, ) } @@ -516,7 +538,8 @@ private fun importArchive( m: ChatModel, importedArchiveURI: URI, appFilesCountAndSize: MutableState>, - progressIndicator: MutableState + progressIndicator: MutableState, + startChat: () -> Unit, ) { progressIndicator.value = true val archivePath = saveArchiveFromURI(importedArchiveURI) @@ -533,6 +556,10 @@ private fun importArchive( operationEnded(m, progressIndicator) { AlertManager.shared.showAlertMsg(generalGetString(MR.strings.chat_database_imported), text = generalGetString(MR.strings.restart_the_app_to_use_imported_chat_database)) } + if (chatModel.localUserCreated.value == false) { + chatModel.chatRunning.value = false + startChat() + } } else { operationEnded(m, progressIndicator) { AlertManager.shared.showAlertMsg(generalGetString(MR.strings.chat_database_imported), text = generalGetString(MR.strings.restart_the_app_to_use_imported_chat_database) + "\n" + generalGetString(MR.strings.non_fatal_errors_occured_during_import)) @@ -681,7 +708,6 @@ fun PreviewDatabaseLayout() { chatDbEncrypted = false, passphraseSaved = false, initialRandomDBPassphrase = SharedPreference({ true }, {}), - developerTools = true, importArchiveLauncher = rememberFileChooserLauncher(true) {}, chatArchiveName = remember { mutableStateOf("dummy_archive") }, chatArchiveTime = remember { mutableStateOf(Clock.System.now()) }, @@ -697,6 +723,7 @@ fun PreviewDatabaseLayout() { deleteAppFilesAndMedia = {}, showSettingsModal = { {} }, onChatItemTTLSelected = {}, + disconnectAllHosts = {}, ) } } diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/remote/ConnectMobileView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/remote/ConnectMobileView.kt index b6ed14150..a7e76b4ab 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/remote/ConnectMobileView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/remote/ConnectMobileView.kt @@ -233,6 +233,7 @@ private fun ConnectMobileViewLayout( } } } + SectionBottomSpacer() } } diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/SettingsView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/SettingsView.kt index c73cb0142..0f37fb197 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/SettingsView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/SettingsView.kt @@ -144,9 +144,7 @@ fun SettingsLayout( SettingsActionItem(painterResource(MR.images.ic_videocam), stringResource(MR.strings.settings_audio_video_calls), showSettingsModal { CallSettingsView(it, showModal) }, disabled = stopped, extraPadding = true) SettingsActionItem(painterResource(MR.images.ic_lock), stringResource(MR.strings.privacy_and_security), showSettingsModal { PrivacySettingsView(it, showSettingsModal, setPerformLA) }, disabled = stopped, extraPadding = true) SettingsActionItem(painterResource(MR.images.ic_light_mode), stringResource(MR.strings.appearance_settings), showSettingsModal { AppearanceView(it, showSettingsModal) }, extraPadding = true) - if (!chatModel.desktopNoUserNoRemote) { - DatabaseItem(encrypted, passphraseSaved, showSettingsModal { DatabaseView(it, showSettingsModal) }, stopped) - } + DatabaseItem(encrypted, passphraseSaved, showSettingsModal { DatabaseView(it, showSettingsModal) }, stopped) } SectionDividerSpaced() diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/base/strings.xml b/apps/multiplatform/common/src/commonMain/resources/MR/base/strings.xml index 82d571d4a..bfd61aa9e 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/base/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/base/strings.xml @@ -1660,6 +1660,7 @@ Unlink desktop? Unlink Disconnect + Disconnect mobiles %s was disconnected]]> Disconnect desktop? Only one device can work at the same time From 9a1c7f41f770df72891c4fd6a3f459a9062bfd42 Mon Sep 17 00:00:00 2001 From: Alexander Bondarenko <486682+dpwiz@users.noreply.github.com> Date: Fri, 1 Dec 2023 18:52:47 +0200 Subject: [PATCH 06/13] core: expand ranges to fit ghc 8.10 & 9.6 (#3496) * expand ranges to fit ghc 8.10 & 9.6 * update nix * use hashes from mq master * fix more deps * use network-transport from hackage --- cabal.project | 7 +--- package.yaml | 12 +++--- scripts/nix/sha256map.nix | 3 +- simplex-chat.cabal | 84 +++++++++++++++++++-------------------- stack.yaml | 4 +- 5 files changed, 51 insertions(+), 59 deletions(-) diff --git a/cabal.project b/cabal.project index 7190c6183..026f963f7 100644 --- a/cabal.project +++ b/cabal.project @@ -9,7 +9,7 @@ constraints: zip +disable-bzip2 +disable-zstd source-repository-package type: git location: https://github.com/simplex-chat/simplexmq.git - tag: 6bffcc8503e5193d57e543ac0100712a8e27d454 + tag: 90a8fc91d35c578c3b52ad296a6f1df715da2278 source-repository-package type: git @@ -45,8 +45,3 @@ source-repository-package type: git location: https://github.com/simplex-chat/android-support.git tag: 9aa09f148089d6752ce563b14c2df1895718d806 - -source-repository-package - type: git - location: https://github.com/simplex-chat/network-transport.git - tag: 0013798272a683e35ca38d2fdaf480942311fba8 diff --git a/package.yaml b/package.yaml index 145b178c4..7c828ed7d 100644 --- a/package.yaml +++ b/package.yaml @@ -32,10 +32,10 @@ dependencies: - filepath == 1.4.* - http-types == 0.12.* - http2 >= 4.2.2 && < 4.3 - - memory == 0.18.* - - mtl == 2.3.* + - memory >= 0.15 && < 0.19 + - mtl >= 2.2 && < 3 - network >= 3.1.2.7 && < 3.2 - - network-transport == 0.5.6 + - network-transport >= 0.5.6 && < 0.6 - optparse-applicative >= 0.15 && < 0.17 - process == 1.6.* - random >= 1.1 && < 1.3 @@ -45,14 +45,14 @@ dependencies: - socks == 0.6.* - sqlcipher-simple == 0.4.* - stm == 2.5.* - - template-haskell == 2.20.* + - template-haskell >= 2.16 && < 2.21 - terminal == 0.2.* - - text == 2.0.* + - text >= 2.0 && < 3 - time == 1.9.* - tls >= 1.6.0 && < 1.7 - unliftio == 0.2.* - unliftio-core == 0.2.* - - zip == 2.0.* + - zip >= 1.7 && < 2.1 flags: swift: diff --git a/scripts/nix/sha256map.nix b/scripts/nix/sha256map.nix index 004187e88..15a4b04ec 100644 --- a/scripts/nix/sha256map.nix +++ b/scripts/nix/sha256map.nix @@ -1,5 +1,5 @@ { - "https://github.com/simplex-chat/simplexmq.git"."6bffcc8503e5193d57e543ac0100712a8e27d454" = "131kdcvh01985lnf4azss4rg7swpjjh647c29m95b33hd1f7mf17"; + "https://github.com/simplex-chat/simplexmq.git"."90a8fc91d35c578c3b52ad296a6f1df715da2278" = "1yjixh6b2s1law3kh885fsbr1inv1r7iy4g9g2bn6j4ygdn8vlzy"; "https://github.com/simplex-chat/hs-socks.git"."a30cc7a79a08d8108316094f8f2f82a0c5e1ac51" = "0yasvnr7g91k76mjkamvzab2kvlb1g5pspjyjn2fr6v83swjhj38"; "https://github.com/kazu-yamamoto/http2.git"."f5525b755ff2418e6e6ecc69e877363b0d0bcaeb" = "0fyx0047gvhm99ilp212mmz37j84cwrfnpmssib5dw363fyb88b6"; "https://github.com/simplex-chat/direct-sqlcipher.git"."f814ee68b16a9447fbb467ccc8f29bdd3546bfd9" = "1ql13f4kfwkbaq7nygkxgw84213i0zm7c1a8hwvramayxl38dq5d"; @@ -7,5 +7,4 @@ "https://github.com/simplex-chat/aeson.git"."aab7b5a14d6c5ea64c64dcaee418de1bb00dcc2b" = "0jz7kda8gai893vyvj96fy962ncv8dcsx71fbddyy8zrvc88jfrr"; "https://github.com/simplex-chat/haskell-terminal.git"."f708b00009b54890172068f168bf98508ffcd495" = "0zmq7lmfsk8m340g47g5963yba7i88n4afa6z93sg9px5jv1mijj"; "https://github.com/simplex-chat/android-support.git"."9aa09f148089d6752ce563b14c2df1895718d806" = "0pbf2pf13v2kjzi397nr13f1h3jv0imvsq8rpiyy2qyx5vd50pqn"; - "https://github.com/simplex-chat/network-transport.git"."0013798272a683e35ca38d2fdaf480942311fba8" = "0dnn62apgvc248df0m8ib7phrzn63wm0xs71xvlypv52j6cgwzkb"; } diff --git a/simplex-chat.cabal b/simplex-chat.cabal index 18b5da39a..6f7e61eda 100644 --- a/simplex-chat.cabal +++ b/simplex-chat.cabal @@ -184,10 +184,10 @@ library , filepath ==1.4.* , http-types ==0.12.* , http2 >=4.2.2 && <4.3 - , memory ==0.18.* - , mtl ==2.3.* + , memory >=0.15 && <0.19 + , mtl >=2.2 && <3 , network >=3.1.2.7 && <3.2 - , network-transport ==0.5.6 + , network-transport >=0.5.6 && <0.6 , optparse-applicative >=0.15 && <0.17 , process ==1.6.* , random >=1.1 && <1.3 @@ -197,14 +197,14 @@ library , socks ==0.6.* , sqlcipher-simple ==0.4.* , stm ==2.5.* - , template-haskell ==2.20.* + , template-haskell >=2.16 && <2.21 , terminal ==0.2.* - , text ==2.0.* + , text >=2.0 && <3 , time ==1.9.* , tls >=1.6.0 && <1.7 , unliftio ==0.2.* , unliftio-core ==0.2.* - , zip ==2.0.* + , zip >=1.7 && <2.1 default-language: Haskell2010 if flag(swift) cpp-options: -DswiftJSON @@ -236,10 +236,10 @@ executable simplex-bot , filepath ==1.4.* , http-types ==0.12.* , http2 >=4.2.2 && <4.3 - , memory ==0.18.* - , mtl ==2.3.* + , memory >=0.15 && <0.19 + , mtl >=2.2 && <3 , network >=3.1.2.7 && <3.2 - , network-transport ==0.5.6 + , network-transport >=0.5.6 && <0.6 , optparse-applicative >=0.15 && <0.17 , process ==1.6.* , random >=1.1 && <1.3 @@ -250,14 +250,14 @@ executable simplex-bot , socks ==0.6.* , sqlcipher-simple ==0.4.* , stm ==2.5.* - , template-haskell ==2.20.* + , template-haskell >=2.16 && <2.21 , terminal ==0.2.* - , text ==2.0.* + , text >=2.0 && <3 , time ==1.9.* , tls >=1.6.0 && <1.7 , unliftio ==0.2.* , unliftio-core ==0.2.* - , zip ==2.0.* + , zip >=1.7 && <2.1 default-language: Haskell2010 if flag(swift) cpp-options: -DswiftJSON @@ -289,10 +289,10 @@ executable simplex-bot-advanced , filepath ==1.4.* , http-types ==0.12.* , http2 >=4.2.2 && <4.3 - , memory ==0.18.* - , mtl ==2.3.* + , memory >=0.15 && <0.19 + , mtl >=2.2 && <3 , network >=3.1.2.7 && <3.2 - , network-transport ==0.5.6 + , network-transport >=0.5.6 && <0.6 , optparse-applicative >=0.15 && <0.17 , process ==1.6.* , random >=1.1 && <1.3 @@ -303,14 +303,14 @@ executable simplex-bot-advanced , socks ==0.6.* , sqlcipher-simple ==0.4.* , stm ==2.5.* - , template-haskell ==2.20.* + , template-haskell >=2.16 && <2.21 , terminal ==0.2.* - , text ==2.0.* + , text >=2.0 && <3 , time ==1.9.* , tls >=1.6.0 && <1.7 , unliftio ==0.2.* , unliftio-core ==0.2.* - , zip ==2.0.* + , zip >=1.7 && <2.1 default-language: Haskell2010 if flag(swift) cpp-options: -DswiftJSON @@ -344,10 +344,10 @@ executable simplex-broadcast-bot , filepath ==1.4.* , http-types ==0.12.* , http2 >=4.2.2 && <4.3 - , memory ==0.18.* - , mtl ==2.3.* + , memory >=0.15 && <0.19 + , mtl >=2.2 && <3 , network >=3.1.2.7 && <3.2 - , network-transport ==0.5.6 + , network-transport >=0.5.6 && <0.6 , optparse-applicative >=0.15 && <0.17 , process ==1.6.* , random >=1.1 && <1.3 @@ -358,14 +358,14 @@ executable simplex-broadcast-bot , socks ==0.6.* , sqlcipher-simple ==0.4.* , stm ==2.5.* - , template-haskell ==2.20.* + , template-haskell >=2.16 && <2.21 , terminal ==0.2.* - , text ==2.0.* + , text >=2.0 && <3 , time ==1.9.* , tls >=1.6.0 && <1.7 , unliftio ==0.2.* , unliftio-core ==0.2.* - , zip ==2.0.* + , zip >=1.7 && <2.1 default-language: Haskell2010 if flag(swift) cpp-options: -DswiftJSON @@ -398,10 +398,10 @@ executable simplex-chat , filepath ==1.4.* , http-types ==0.12.* , http2 >=4.2.2 && <4.3 - , memory ==0.18.* - , mtl ==2.3.* + , memory >=0.15 && <0.19 + , mtl >=2.2 && <3 , network ==3.1.* - , network-transport ==0.5.6 + , network-transport >=0.5.6 && <0.6 , optparse-applicative >=0.15 && <0.17 , process ==1.6.* , random >=1.1 && <1.3 @@ -412,15 +412,15 @@ executable simplex-chat , socks ==0.6.* , sqlcipher-simple ==0.4.* , stm ==2.5.* - , template-haskell ==2.20.* + , template-haskell >=2.16 && <2.21 , terminal ==0.2.* - , text ==2.0.* + , text >=2.0 && <3 , time ==1.9.* , tls >=1.6.0 && <1.7 , unliftio ==0.2.* , unliftio-core ==0.2.* , websockets ==0.12.* - , zip ==2.0.* + , zip >=1.7 && <2.1 default-language: Haskell2010 if flag(swift) cpp-options: -DswiftJSON @@ -456,10 +456,10 @@ executable simplex-directory-service , filepath ==1.4.* , http-types ==0.12.* , http2 >=4.2.2 && <4.3 - , memory ==0.18.* - , mtl ==2.3.* + , memory >=0.15 && <0.19 + , mtl >=2.2 && <3 , network >=3.1.2.7 && <3.2 - , network-transport ==0.5.6 + , network-transport >=0.5.6 && <0.6 , optparse-applicative >=0.15 && <0.17 , process ==1.6.* , random >=1.1 && <1.3 @@ -470,14 +470,14 @@ executable simplex-directory-service , socks ==0.6.* , sqlcipher-simple ==0.4.* , stm ==2.5.* - , template-haskell ==2.20.* + , template-haskell >=2.16 && <2.21 , terminal ==0.2.* - , text ==2.0.* + , text >=2.0 && <3 , time ==1.9.* , tls >=1.6.0 && <1.7 , unliftio ==0.2.* , unliftio-core ==0.2.* - , zip ==2.0.* + , zip >=1.7 && <2.1 default-language: Haskell2010 if flag(swift) cpp-options: -DswiftJSON @@ -540,10 +540,10 @@ test-suite simplex-chat-test , hspec ==2.11.* , http-types ==0.12.* , http2 >=4.2.2 && <4.3 - , memory ==0.18.* - , mtl ==2.3.* + , memory >=0.15 && <0.19 + , mtl >=2.2 && <3 , network ==3.1.* - , network-transport ==0.5.6 + , network-transport >=0.5.6 && <0.6 , optparse-applicative >=0.15 && <0.17 , process ==1.6.* , random >=1.1 && <1.3 @@ -555,14 +555,14 @@ test-suite simplex-chat-test , socks ==0.6.* , sqlcipher-simple ==0.4.* , stm ==2.5.* - , template-haskell ==2.20.* + , template-haskell >=2.16 && <2.21 , terminal ==0.2.* - , text ==2.0.* + , text >=2.0 && <3 , time ==1.9.* , tls >=1.6.0 && <1.7 , unliftio ==0.2.* , unliftio-core ==0.2.* - , zip ==2.0.* + , zip >=1.7 && <2.1 default-language: Haskell2010 if flag(swift) cpp-options: -DswiftJSON diff --git a/stack.yaml b/stack.yaml index 66e89de6f..2866c9bfa 100644 --- a/stack.yaml +++ b/stack.yaml @@ -49,7 +49,7 @@ extra-deps: # - simplexmq-1.0.0@sha256:34b2004728ae396e3ae449cd090ba7410781e2b3cefc59259915f4ca5daa9ea8,8561 # - ../simplexmq - github: simplex-chat/simplexmq - commit: 6bffcc8503e5193d57e543ac0100712a8e27d454 + commit: 90a8fc91d35c578c3b52ad296a6f1df715da2278 - github: kazu-yamamoto/http2 commit: f5525b755ff2418e6e6ecc69e877363b0d0bcaeb # - ../direct-sqlcipher @@ -65,8 +65,6 @@ extra-deps: commit: f708b00009b54890172068f168bf98508ffcd495 - github: simplex-chat/android-support commit: 9aa09f148089d6752ce563b14c2df1895718d806 - - github: simplex-chat/network-transport - commit: 0013798272a683e35ca38d2fdaf480942311fba8 # # extra-deps: [] From e1ff7c88d70fdd67ca0e47d3ad3a672ce9c8aa05 Mon Sep 17 00:00:00 2001 From: Stanislav Dmitrenko <7953703+avently@users.noreply.github.com> Date: Sat, 2 Dec 2023 04:41:08 +0800 Subject: [PATCH 07/13] desktop: allow changing listening ip and port of remote (#3498) * desktop: allow changing listening ip and port of remote * remove empty lines --------- Co-authored-by: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com> --- .../chat/simplex/common/model/SimpleXAPI.kt | 20 ++- .../views/helpers/DefaultBasicTextField.kt | 1 - .../helpers/ExposedDropDownSettingRow.kt | 117 +++++++------ .../simplex/common/views/helpers/Utils.kt | 2 +- .../common/views/remote/ConnectMobileView.kt | 159 ++++++++++++++---- .../views/usersettings/NetworkAndServers.kt | 6 +- 6 files changed, 211 insertions(+), 94 deletions(-) diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt index 724e6bfcb..6c01aff5d 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt @@ -1426,9 +1426,9 @@ object ChatController { chatModel.remoteHosts.addAll(hosts) } - suspend fun startRemoteHost(rhId: Long?, multicast: Boolean = true): Triple? { - val r = sendCmd(null, CC.StartRemoteHost(rhId, multicast)) - if (r is CR.RemoteHostStarted) return Triple(r.remoteHost_, r.invitation, r.ctrlPort) + suspend fun startRemoteHost(rhId: Long?, multicast: Boolean = true, address: RemoteCtrlAddress?, port: Int?): CR.RemoteHostStarted? { + val r = sendCmd(null, CC.StartRemoteHost(rhId, multicast, address, port)) + if (r is CR.RemoteHostStarted) return r apiErrorAlert("startRemoteHost", generalGetString(MR.strings.error_alert_title), r) return null } @@ -2248,7 +2248,7 @@ sealed class CC { // Remote control class SetLocalDeviceName(val displayName: String): CC() class ListRemoteHosts(): CC() - class StartRemoteHost(val remoteHostId: Long?, val multicast: Boolean): CC() + class StartRemoteHost(val remoteHostId: Long?, val multicast: Boolean, val address: RemoteCtrlAddress?, val port: Int?): CC() class SwitchRemoteHost (val remoteHostId: Long?): CC() class StopRemoteHost(val remoteHostKey: Long?): CC() class DeleteRemoteHost(val remoteHostId: Long): CC() @@ -2384,7 +2384,7 @@ sealed class CC { is CancelFile -> "/fcancel $fileId" is SetLocalDeviceName -> "/set device name $displayName" is ListRemoteHosts -> "/list remote hosts" - is StartRemoteHost -> "/start remote host " + if (remoteHostId == null) "new" else "$remoteHostId multicast=${onOff(multicast)}" + is StartRemoteHost -> "/start remote host " + (if (remoteHostId == null) "new" else "$remoteHostId multicast=${onOff(multicast)}") + (if (address != null) " addr=${address.address} iface=${address.`interface`}" else "") + (if (port != null) " port=$port" else "") is SwitchRemoteHost -> "/switch remote host " + if (remoteHostId == null) "local" else "$remoteHostId" is StopRemoteHost -> "/stop remote host " + if (remoteHostKey == null) "new" else "$remoteHostKey" is DeleteRemoteHost -> "/delete remote host $remoteHostId" @@ -3606,6 +3606,8 @@ data class RemoteHostInfo( val remoteHostId: Long, val hostDeviceName: String, val storePath: String, + val bindAddress_: RemoteCtrlAddress?, + val bindPort_: Int?, val sessionState: RemoteHostSessionState? ) { val activeHost: Boolean @@ -3614,6 +3616,12 @@ data class RemoteHostInfo( fun activeHost(): Boolean = chatModel.currentRemoteHost.value?.remoteHostId == remoteHostId } +@Serializable +data class RemoteCtrlAddress( + val address: String, + val `interface`: String +) + @Serializable sealed class RemoteHostSessionState { @Serializable @SerialName("starting") object Starting: RemoteHostSessionState() @@ -3848,7 +3856,7 @@ sealed class CR { // remote events (desktop) @Serializable @SerialName("remoteHostList") class RemoteHostList(val remoteHosts: List): CR() @Serializable @SerialName("currentRemoteHost") class CurrentRemoteHost(val remoteHost_: RemoteHostInfo?): CR() - @Serializable @SerialName("remoteHostStarted") class RemoteHostStarted(val remoteHost_: RemoteHostInfo?, val invitation: String, val ctrlPort: String): CR() + @Serializable @SerialName("remoteHostStarted") class RemoteHostStarted(val remoteHost_: RemoteHostInfo?, val invitation: String, val localAddrs: List, val ctrlPort: String): CR() @Serializable @SerialName("remoteHostSessionCode") class RemoteHostSessionCode(val remoteHost_: RemoteHostInfo?, val sessionCode: String): CR() @Serializable @SerialName("newRemoteHost") class NewRemoteHost(val remoteHost: RemoteHostInfo): CR() @Serializable @SerialName("remoteHostConnected") class RemoteHostConnected(val remoteHost: RemoteHostInfo): CR() diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/DefaultBasicTextField.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/DefaultBasicTextField.kt index 08dfaa0df..65098cea2 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/DefaultBasicTextField.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/DefaultBasicTextField.kt @@ -152,7 +152,6 @@ fun DefaultConfigurableTextField( BasicTextField( value = state.value, modifier = modifier - .fillMaxWidth() .background(colors.backgroundColor(enabled).value, shape) .indicatorLine(enabled, false, interactionSource, colors) .defaultMinSize( diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/ExposedDropDownSettingRow.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/ExposedDropDownSettingRow.kt index 72a8aaf10..904b2fc34 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/ExposedDropDownSettingRow.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/ExposedDropDownSettingRow.kt @@ -10,72 +10,87 @@ import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.painter.Painter import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.text.style.TextOverflow -import androidx.compose.ui.unit.dp -import androidx.compose.ui.unit.sp +import androidx.compose.ui.unit.* import chat.simplex.res.MR import chat.simplex.common.ui.theme.* import chat.simplex.common.views.usersettings.SettingsActionItemWithContent +@Composable +fun ExposedDropDownSetting( + values: List>, + selection: State, + textColor: Color = MaterialTheme.colors.secondary, + label: String? = null, + enabled: State = mutableStateOf(true), + minWidth: Dp = 200.dp, + maxWidth: Dp = with(LocalDensity.current) { 180.sp.toDp() }, + onSelected: (T) -> Unit +) { + val expanded = remember { mutableStateOf(false) } + ExposedDropdownMenuBox( + expanded = expanded.value, + onExpandedChange = { + expanded.value = !expanded.value && enabled.value + } + ) { + Row( + Modifier.padding(start = 10.dp), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.End + ) { + Text( + values.first { it.first == selection.value }.second + (if (label != null) " $label" else ""), + Modifier.widthIn(max = maxWidth), + maxLines = 1, + overflow = TextOverflow.Ellipsis, + color = textColor + ) + Spacer(Modifier.size(12.dp)) + Icon( + if (!expanded.value) painterResource(MR.images.ic_expand_more) else painterResource(MR.images.ic_expand_less), + generalGetString(MR.strings.icon_descr_more_button), + tint = MaterialTheme.colors.secondary + ) + } + DefaultExposedDropdownMenu( + modifier = Modifier.widthIn(min = minWidth), + expanded = expanded, + ) { + values.forEach { selectionOption -> + DropdownMenuItem( + onClick = { + onSelected(selectionOption.first) + expanded.value = false + }, + contentPadding = PaddingValues(horizontal = DEFAULT_PADDING * 1.5f) + ) { + Text( + selectionOption.second + (if (label != null) " $label" else ""), + maxLines = 1, + overflow = TextOverflow.Ellipsis, + color = if (isInDarkTheme()) MenuTextColorDark else Color.Black, + ) + } + } + } + } +} + @Composable fun ExposedDropDownSettingRow( title: String, values: List>, selection: State, + textColor: Color = MaterialTheme.colors.secondary, label: String? = null, icon: Painter? = null, iconTint: Color = MaterialTheme.colors.secondary, enabled: State = mutableStateOf(true), + minWidth: Dp = 200.dp, + maxWidth: Dp = with(LocalDensity.current) { 180.sp.toDp() }, onSelected: (T) -> Unit ) { SettingsActionItemWithContent(icon, title, iconColor = iconTint, disabled = !enabled.value) { - val expanded = remember { mutableStateOf(false) } - ExposedDropdownMenuBox( - expanded = expanded.value, - onExpandedChange = { - expanded.value = !expanded.value && enabled.value - } - ) { - Row( - Modifier.padding(start = 10.dp), - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.End - ) { - val maxWidth = with(LocalDensity.current) { 180.sp.toDp() } - Text( - values.first { it.first == selection.value }.second + (if (label != null) " $label" else ""), - Modifier.widthIn(max = maxWidth), - maxLines = 1, - overflow = TextOverflow.Ellipsis, - color = MaterialTheme.colors.secondary - ) - Spacer(Modifier.size(12.dp)) - Icon( - if (!expanded.value) painterResource(MR.images.ic_expand_more) else painterResource(MR.images.ic_expand_less), - generalGetString(MR.strings.icon_descr_more_button), - tint = MaterialTheme.colors.secondary - ) - } - DefaultExposedDropdownMenu( - modifier = Modifier.widthIn(min = 200.dp), - expanded = expanded, - ) { - values.forEach { selectionOption -> - DropdownMenuItem( - onClick = { - onSelected(selectionOption.first) - expanded.value = false - }, - contentPadding = PaddingValues(horizontal = DEFAULT_PADDING * 1.5f) - ) { - Text( - selectionOption.second + (if (label != null) " $label" else ""), - maxLines = 1, - overflow = TextOverflow.Ellipsis, - color = if (isInDarkTheme()) MenuTextColorDark else Color.Black, - ) - } - } - } - } + ExposedDropDownSetting(values, selection ,textColor, label, enabled, minWidth, maxWidth, onSelected) } } diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/Utils.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/Utils.kt index 6e12681dd..0a0ef17c4 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/Utils.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/Utils.kt @@ -55,7 +55,7 @@ fun annotatedStringResource(id: StringResource): AnnotatedString { @Composable fun annotatedStringResource(id: StringResource, vararg args: Any?): AnnotatedString { val density = LocalDensity.current - return remember(id) { + return remember(id, args) { escapedHtmlToAnnotatedString(id.localized().format(args = args), density) } } diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/remote/ConnectMobileView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/remote/ConnectMobileView.kt index a7e76b4ab..c1c5d978c 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/remote/ConnectMobileView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/remote/ConnectMobileView.kt @@ -9,6 +9,7 @@ import SectionView import TextIconSpaced import androidx.compose.foundation.* import androidx.compose.foundation.layout.* +import androidx.compose.foundation.text.KeyboardActions import androidx.compose.foundation.text.selection.SelectionContainer import androidx.compose.material.* import androidx.compose.runtime.* @@ -17,6 +18,7 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.platform.LocalClipboardManager +import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.font.* import androidx.compose.ui.text.input.* @@ -32,11 +34,11 @@ import chat.simplex.common.views.chat.item.ItemAction import chat.simplex.common.views.chatlist.* import chat.simplex.common.views.helpers.* import chat.simplex.common.views.newchat.QRCode -import chat.simplex.common.views.usersettings.PreferenceToggle -import chat.simplex.common.views.usersettings.SettingsActionItemWithContent +import chat.simplex.common.views.usersettings.* import chat.simplex.res.MR import dev.icerock.moko.resources.compose.painterResource import dev.icerock.moko.resources.compose.stringResource +import kotlinx.coroutines.flow.distinctUntilChanged @Composable fun ConnectMobileView() { @@ -156,7 +158,7 @@ fun DeviceNameField( DefaultConfigurableTextField( state = state, placeholder = generalGetString(MR.strings.enter_this_device_name), - modifier = Modifier.padding(start = DEFAULT_PADDING), + modifier = Modifier.fillMaxWidth().padding(start = DEFAULT_PADDING), isValid = { true }, ) KeyChangeEffect(state.value) { @@ -172,7 +174,10 @@ private fun ConnectMobileViewLayout( sessionCode: String?, port: String?, staleQrCode: Boolean = false, - refreshQrCode: () -> Unit = {} + editEnabled: Boolean = false, + editClicked: () -> Unit = {}, + refreshQrCode: () -> Unit = {}, + UnderQrLayout: @Composable () -> Unit = {}, ) { Column( Modifier.fillMaxWidth().verticalScroll(rememberScrollState()), @@ -196,7 +201,18 @@ private fun ConnectMobileViewLayout( } } SectionTextFooter(annotatedStringResource(MR.strings.open_on_mobile_and_scan_qr_code), textAlign = TextAlign.Center) - SectionTextFooter(annotatedStringResource(MR.strings.waiting_for_mobile_to_connect_on_port, port), textAlign = TextAlign.Center) + Row(verticalAlignment = Alignment.CenterVertically) { + SectionTextFooter(annotatedStringResource(MR.strings.waiting_for_mobile_to_connect_on_port, port), textAlign = TextAlign.Center) + if (editEnabled) { + Spacer(Modifier.width(4.dp)) + IconButton(editClicked, Modifier.size(16.dp)) { + Icon(painterResource(MR.images.ic_edit), stringResource(MR.strings.edit_verb), Modifier.size(16.dp), tint = MaterialTheme.colors.primary) + } + Spacer(Modifier.width(DEFAULT_PADDING)) + } + } + + UnderQrLayout() if (remember { controller.appPrefs.developerTools.state }.value) { val clipboard = LocalClipboardManager.current @@ -259,14 +275,22 @@ private fun showAddingMobileDevice(connecting: MutableState) { @Composable fun AddingMobileDevice(showTitle: Boolean, staleQrCode: MutableState, connecting: MutableState, close: () -> Unit) { - val invitation = rememberSaveable { mutableStateOf(null) } - val port = rememberSaveable { mutableStateOf(null) } + val cachedR = remember { mutableStateOf(null) } + val customAddress = rememberSaveable { mutableStateOf(null) } + val customPort = rememberSaveable { mutableStateOf(null) } + var editing by rememberSaveable { mutableStateOf(false) } val startRemoteHost = suspend { - val r = chatModel.controller.startRemoteHost(null, controller.appPrefs.offerRemoteMulticast.get()) + val r = chatModel.controller.startRemoteHost( + rhId = null, + multicast = controller.appPrefs.offerRemoteMulticast.get(), + address = if (customAddress.value?.address != cachedR.address?.address) customAddress.value else cachedR.rh?.bindAddress_, + port = if (customPort.value != cachedR.port) customPort.value else cachedR.rh?.bindPort_ + ) if (r != null) { + cachedR.value = r connecting.value = true - invitation.value = r.second - port.value = r.third + customAddress.value = cachedR.address + customPort.value = cachedR.port chatModel.remoteHostPairing.value = null to RemoteHostSessionState.Starting } } @@ -283,19 +307,23 @@ fun AddingMobileDevice(showTitle: Boolean, staleQrCode: MutableState, c val remoteDeviceName = pairing.value?.first?.hostDeviceName ConnectMobileViewLayout( title = if (!showTitle) null else if (cachedSessionCode == null) stringResource(MR.strings.link_a_mobile) else stringResource(MR.strings.verify_connection), - invitation = invitation.value, + invitation = cachedR.invitation, deviceName = remoteDeviceName, sessionCode = cachedSessionCode, - port = port.value, - staleQrCode = staleQrCode.value, + port = cachedR.value?.ctrlPort, + staleQrCode = staleQrCode.value || (cachedR.address != customAddress.value && customAddress.value != null) || (cachedR.port != customPort.value && customPort.value != null), + editEnabled = !editing && cachedR.addresses.isNotEmpty(), + editClicked = { editing = true }, refreshQrCode = { withBGApi { if (chatController.stopRemoteHost(null)) { startRemoteHost() staleQrCode.value = false + editing = false } } }, + UnderQrLayout = { UnderQrLayout(editing, cachedR, customAddress, customPort) } ) val oldRemoteHostId by remember { mutableStateOf(chatModel.currentRemoteHost.value?.remoteHostId) } LaunchedEffect(remember { chatModel.currentRemoteHost }.value) { @@ -325,9 +353,26 @@ fun AddingMobileDevice(showTitle: Boolean, staleQrCode: MutableState, c private fun showConnectMobileDevice(rh: RemoteHostInfo, connecting: MutableState) { ModalManager.start.showModalCloseable { close -> + val cachedR = remember { mutableStateOf(null) } + val customAddress = rememberSaveable { mutableStateOf(null) } + val customPort = rememberSaveable { mutableStateOf(null) } + var editing by rememberSaveable { mutableStateOf(false) } + val startRemoteHost = suspend { + val r = chatModel.controller.startRemoteHost( + rhId = rh.remoteHostId, + multicast = controller.appPrefs.offerRemoteMulticast.get(), + address = if (customAddress.value?.address != cachedR.address?.address) customAddress.value else cachedR.rh?.bindAddress_ ?: rh.bindAddress_, + port = if (customPort.value != cachedR.port) customPort.value else cachedR.rh?.bindPort_ ?: rh.bindPort_ + ) + if (r != null) { + cachedR.value = r + connecting.value = true + customAddress.value = cachedR.address + customPort.value = cachedR.port + chatModel.remoteHostPairing.value = null to RemoteHostSessionState.Starting + } + } val pairing = remember { chatModel.remoteHostPairing } - val invitation = rememberSaveable { mutableStateOf(null) } - val port = rememberSaveable { mutableStateOf(null) } val sessionCode = when (val state = pairing.value?.second) { is RemoteHostSessionState.PendingConfirmation -> state.sessionCode else -> null @@ -339,25 +384,25 @@ private fun showConnectMobileDevice(rh: RemoteHostInfo, connecting: MutableState } ConnectMobileViewLayout( title = if (cachedSessionCode == null) stringResource(MR.strings.scan_from_mobile) else stringResource(MR.strings.verify_connection), - invitation = invitation.value, + invitation = cachedR.invitation, deviceName = pairing.value?.first?.hostDeviceName ?: rh.hostDeviceName, sessionCode = cachedSessionCode, - port = port.value + port = cachedR.value?.ctrlPort, + staleQrCode = (cachedR.address != customAddress.value && customAddress.value != null) || (cachedR.port != customPort.value && customPort.value != null), + editEnabled = !editing && cachedR.addresses.isNotEmpty(), + editClicked = { editing = true }, + refreshQrCode = { + withBGApi { + if (chatController.stopRemoteHost(rh.remoteHostId)) { + startRemoteHost() + editing = false + } + } + }, + UnderQrLayout = { UnderQrLayout(editing, cachedR, customAddress, customPort) } ) - var remoteHostId by rememberSaveable { mutableStateOf(null) } - LaunchedEffect(Unit) { - val r = chatModel.controller.startRemoteHost(rh.remoteHostId, controller.appPrefs.offerRemoteMulticast.get()) - if (r != null) { - val (rh_, inv) = r - connecting.value = true - remoteHostId = rh_?.remoteHostId - invitation.value = inv - port.value = r.third - chatModel.remoteHostPairing.value = null to RemoteHostSessionState.Starting - } - } LaunchedEffect(remember { chatModel.currentRemoteHost }.value) { - if (remoteHostId != null && chatModel.currentRemoteHost.value?.remoteHostId == remoteHostId) { + if (cachedR.remoteHostId != null && chatModel.currentRemoteHost.value?.remoteHostId == cachedR.remoteHostId) { close() } } @@ -367,10 +412,13 @@ private fun showConnectMobileDevice(rh: RemoteHostInfo, connecting: MutableState } } DisposableEffect(Unit) { + withBGApi { + startRemoteHost() + } onDispose { - if (remoteHostId != null && chatModel.currentRemoteHost.value?.remoteHostId != remoteHostId) { + if (cachedR.remoteHostId != null && chatModel.currentRemoteHost.value?.remoteHostId != cachedR.remoteHostId) { withBGApi { - chatController.stopRemoteHost(remoteHostId) + chatController.stopRemoteHost(cachedR.remoteHostId) } } chatModel.remoteHostPairing.value = null @@ -403,3 +451,50 @@ private fun showConnectedMobileDevice(rh: RemoteHostInfo, disconnectHost: () -> } } } + +@Composable +private fun UnderQrLayout(editing: Boolean, cachedR: State, customAddress: MutableState, customPort: MutableState) { + if (editing) { + Row(Modifier.fillMaxWidth(), verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.Center) { + ExposedDropDownSetting( + cachedR.addresses.map { it to it.address + " (${it.`interface`})" }, + customAddress, + textColor = MaterialTheme.colors.onBackground, + minWidth = 250.dp, + maxWidth = with(LocalDensity.current) { 250.sp.toDp() }, + onSelected = { + customAddress.value = it + } + ) + val portUnsaved = rememberSaveable(stateSaver = TextFieldValue.Saver) { + mutableStateOf(TextFieldValue((customPort.value ?: cachedR.port!!).toString())) + } + Spacer(Modifier.width(DEFAULT_PADDING)) + DefaultConfigurableTextField( + portUnsaved, + stringResource(MR.strings.port_verb), + modifier = Modifier.widthIn(max = 100.dp), + isValid = { validPort(it) && it.toInt() > 1023 }, + keyboardActions = KeyboardActions(onDone = { defaultKeyboardAction(ImeAction.Done) }), + keyboardType = KeyboardType.Number, + ) + LaunchedEffect(Unit) { + snapshotFlow { portUnsaved.value.text } + .distinctUntilChanged() + .collect { + if (validPort(it) && it.toInt() > 1023) { + customPort.value = it.toInt() + } + } + } + } + } +} + +private val State.rh: RemoteHostInfo? get() = value?.remoteHost_ +private val State.remoteHostId: Long? get() = value?.remoteHost_?.remoteHostId +private val State.invitation: String? get() = value?.invitation +private val State.address: RemoteCtrlAddress? get() = value?.localAddrs?.firstOrNull() +private val State.addresses: List get() = + (if (controller.appPrefs.developerTools.get()) value?.localAddrs else value?.localAddrs?.filterNot { it.address == "127.0.0.1" }) ?: emptyList() +private val State.port: Int? get() = value?.ctrlPort?.toIntOrNull() diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/NetworkAndServers.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/NetworkAndServers.kt index a96de71b9..fcd602ee2 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/NetworkAndServers.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/NetworkAndServers.kt @@ -305,7 +305,7 @@ fun SockProxySettings(m: ChatModel) { DefaultConfigurableTextField( hostUnsaved, stringResource(MR.strings.host_verb), - modifier = Modifier, + modifier = Modifier.fillMaxWidth(), isValid = ::validHost, keyboardActions = KeyboardActions(onNext = { defaultKeyboardAction(ImeAction.Next) }), keyboardType = KeyboardType.Text, @@ -315,7 +315,7 @@ fun SockProxySettings(m: ChatModel) { DefaultConfigurableTextField( portUnsaved, stringResource(MR.strings.port_verb), - modifier = Modifier, + modifier = Modifier.fillMaxWidth(), isValid = ::validPort, keyboardActions = KeyboardActions(onDone = { defaultKeyboardAction(ImeAction.Done); save() }), keyboardType = KeyboardType.Number, @@ -428,7 +428,7 @@ private fun validHost(s: String): Boolean { } // https://ihateregex.io/expr/port/ -private fun validPort(s: String): Boolean { +fun validPort(s: String): Boolean { val validPort = Regex("^(6553[0-5])|(655[0-2][0-9])|(65[0-4][0-9]{2})|(6[0-4][0-9]{3})|([1-5][0-9]{4})|([0-5]{0,5})|([0-9]{1,4})$") return s.isNotBlank() && s.matches(validPort) } From f94c0311c115ec2127acc3f22ef928cb9ee0ad2f Mon Sep 17 00:00:00 2001 From: Alexander Bondarenko <486682+dpwiz@users.noreply.github.com> Date: Sat, 2 Dec 2023 14:24:29 +0200 Subject: [PATCH 08/13] raise lower bound on mtl to a real version (#3499) * raise lower bound on mtl to a real version * simplexmq --------- Co-authored-by: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com> --- cabal.project | 2 +- package.yaml | 2 +- scripts/nix/sha256map.nix | 2 +- simplex-chat.cabal | 16 ++++++++-------- stack.yaml | 2 +- 5 files changed, 12 insertions(+), 12 deletions(-) diff --git a/cabal.project b/cabal.project index 026f963f7..a8bb4c2f8 100644 --- a/cabal.project +++ b/cabal.project @@ -9,7 +9,7 @@ constraints: zip +disable-bzip2 +disable-zstd source-repository-package type: git location: https://github.com/simplex-chat/simplexmq.git - tag: 90a8fc91d35c578c3b52ad296a6f1df715da2278 + tag: 117168ccce93ef5ac478c02e3f02a018fa8a2200 source-repository-package type: git diff --git a/package.yaml b/package.yaml index 7c828ed7d..e77d43546 100644 --- a/package.yaml +++ b/package.yaml @@ -33,7 +33,7 @@ dependencies: - http-types == 0.12.* - http2 >= 4.2.2 && < 4.3 - memory >= 0.15 && < 0.19 - - mtl >= 2.2 && < 3 + - mtl >= 2.3.1 && < 3 - network >= 3.1.2.7 && < 3.2 - network-transport >= 0.5.6 && < 0.6 - optparse-applicative >= 0.15 && < 0.17 diff --git a/scripts/nix/sha256map.nix b/scripts/nix/sha256map.nix index 15a4b04ec..7e39845fe 100644 --- a/scripts/nix/sha256map.nix +++ b/scripts/nix/sha256map.nix @@ -1,5 +1,5 @@ { - "https://github.com/simplex-chat/simplexmq.git"."90a8fc91d35c578c3b52ad296a6f1df715da2278" = "1yjixh6b2s1law3kh885fsbr1inv1r7iy4g9g2bn6j4ygdn8vlzy"; + "https://github.com/simplex-chat/simplexmq.git"."117168ccce93ef5ac478c02e3f02a018fa8a2200" = "1091c4b8qjp9v29z862b7clda90w3kb4v2aqp5b2jh6yprlga98w"; "https://github.com/simplex-chat/hs-socks.git"."a30cc7a79a08d8108316094f8f2f82a0c5e1ac51" = "0yasvnr7g91k76mjkamvzab2kvlb1g5pspjyjn2fr6v83swjhj38"; "https://github.com/kazu-yamamoto/http2.git"."f5525b755ff2418e6e6ecc69e877363b0d0bcaeb" = "0fyx0047gvhm99ilp212mmz37j84cwrfnpmssib5dw363fyb88b6"; "https://github.com/simplex-chat/direct-sqlcipher.git"."f814ee68b16a9447fbb467ccc8f29bdd3546bfd9" = "1ql13f4kfwkbaq7nygkxgw84213i0zm7c1a8hwvramayxl38dq5d"; diff --git a/simplex-chat.cabal b/simplex-chat.cabal index 6f7e61eda..3c080e4b5 100644 --- a/simplex-chat.cabal +++ b/simplex-chat.cabal @@ -1,6 +1,6 @@ cabal-version: 1.12 --- This file has been generated from package.yaml by hpack version 0.36.0. +-- This file has been generated from package.yaml by hpack version 0.35.5. -- -- see: https://github.com/sol/hpack @@ -185,7 +185,7 @@ library , http-types ==0.12.* , http2 >=4.2.2 && <4.3 , memory >=0.15 && <0.19 - , mtl >=2.2 && <3 + , mtl >=2.3.1 && <3 , network >=3.1.2.7 && <3.2 , network-transport >=0.5.6 && <0.6 , optparse-applicative >=0.15 && <0.17 @@ -237,7 +237,7 @@ executable simplex-bot , http-types ==0.12.* , http2 >=4.2.2 && <4.3 , memory >=0.15 && <0.19 - , mtl >=2.2 && <3 + , mtl >=2.3.1 && <3 , network >=3.1.2.7 && <3.2 , network-transport >=0.5.6 && <0.6 , optparse-applicative >=0.15 && <0.17 @@ -290,7 +290,7 @@ executable simplex-bot-advanced , http-types ==0.12.* , http2 >=4.2.2 && <4.3 , memory >=0.15 && <0.19 - , mtl >=2.2 && <3 + , mtl >=2.3.1 && <3 , network >=3.1.2.7 && <3.2 , network-transport >=0.5.6 && <0.6 , optparse-applicative >=0.15 && <0.17 @@ -345,7 +345,7 @@ executable simplex-broadcast-bot , http-types ==0.12.* , http2 >=4.2.2 && <4.3 , memory >=0.15 && <0.19 - , mtl >=2.2 && <3 + , mtl >=2.3.1 && <3 , network >=3.1.2.7 && <3.2 , network-transport >=0.5.6 && <0.6 , optparse-applicative >=0.15 && <0.17 @@ -399,7 +399,7 @@ executable simplex-chat , http-types ==0.12.* , http2 >=4.2.2 && <4.3 , memory >=0.15 && <0.19 - , mtl >=2.2 && <3 + , mtl >=2.3.1 && <3 , network ==3.1.* , network-transport >=0.5.6 && <0.6 , optparse-applicative >=0.15 && <0.17 @@ -457,7 +457,7 @@ executable simplex-directory-service , http-types ==0.12.* , http2 >=4.2.2 && <4.3 , memory >=0.15 && <0.19 - , mtl >=2.2 && <3 + , mtl >=2.3.1 && <3 , network >=3.1.2.7 && <3.2 , network-transport >=0.5.6 && <0.6 , optparse-applicative >=0.15 && <0.17 @@ -541,7 +541,7 @@ test-suite simplex-chat-test , http-types ==0.12.* , http2 >=4.2.2 && <4.3 , memory >=0.15 && <0.19 - , mtl >=2.2 && <3 + , mtl >=2.3.1 && <3 , network ==3.1.* , network-transport >=0.5.6 && <0.6 , optparse-applicative >=0.15 && <0.17 diff --git a/stack.yaml b/stack.yaml index 2866c9bfa..69f8e2121 100644 --- a/stack.yaml +++ b/stack.yaml @@ -49,7 +49,7 @@ extra-deps: # - simplexmq-1.0.0@sha256:34b2004728ae396e3ae449cd090ba7410781e2b3cefc59259915f4ca5daa9ea8,8561 # - ../simplexmq - github: simplex-chat/simplexmq - commit: 90a8fc91d35c578c3b52ad296a6f1df715da2278 + commit: 117168ccce93ef5ac478c02e3f02a018fa8a2200 - github: kazu-yamamoto/http2 commit: f5525b755ff2418e6e6ecc69e877363b0d0bcaeb # - ../direct-sqlcipher From 6a9a67db14a3dcf4fd70f42e98081645091d8c7d Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com> Date: Sun, 3 Dec 2023 15:42:26 +0000 Subject: [PATCH 09/13] cli: option to mark shown messages as read (off by default) (#3506) * cli: option to mark shown messages as read (off by default) * fix tests * fix tests --- apps/simplex-broadcast-bot/src/Broadcast/Options.hs | 1 + .../src/Directory/Options.hs | 13 +++++++------ src/Simplex/Chat/Options.hs | 8 ++++++++ src/Simplex/Chat/Terminal.hs | 6 +++--- src/Simplex/Chat/Terminal/Output.hs | 8 ++++---- tests/ChatClient.hs | 3 ++- tests/RemoteTests.hs | 6 +++--- 7 files changed, 28 insertions(+), 17 deletions(-) diff --git a/apps/simplex-broadcast-bot/src/Broadcast/Options.hs b/apps/simplex-broadcast-bot/src/Broadcast/Options.hs index 3758af2fc..9a79af4b4 100644 --- a/apps/simplex-broadcast-bot/src/Broadcast/Options.hs +++ b/apps/simplex-broadcast-bot/src/Broadcast/Options.hs @@ -83,5 +83,6 @@ mkChatOpts BroadcastBotOpts {coreOptions} = allowInstantFiles = True, autoAcceptFileSize = 0, muteNotifications = True, + markRead = False, maintenance = False } diff --git a/apps/simplex-directory-service/src/Directory/Options.hs b/apps/simplex-directory-service/src/Directory/Options.hs index 8f28c9013..0ca8cee78 100644 --- a/apps/simplex-directory-service/src/Directory/Options.hs +++ b/apps/simplex-directory-service/src/Directory/Options.hs @@ -5,10 +5,10 @@ {-# LANGUAGE ScopedTypeVariables #-} module Directory.Options - ( DirectoryOpts (..), - getDirectoryOpts, - mkChatOpts, - ) + ( DirectoryOpts (..), + getDirectoryOpts, + mkChatOpts, + ) where import Options.Applicative @@ -35,8 +35,8 @@ directoryOpts appDir defaultDbFileName = do <> help "Comma-separated list of super-users in the format CONTACT_ID:DISPLAY_NAME who will be allowed to manage the directory" ) directoryLog <- - Just <$> - strOption + Just + <$> strOption ( long "directory-file" <> metavar "DIRECTORY_FILE" <> help "Append only log for directory state" @@ -81,5 +81,6 @@ mkChatOpts DirectoryOpts {coreOptions} = allowInstantFiles = True, autoAcceptFileSize = 0, muteNotifications = True, + markRead = False, maintenance = False } diff --git a/src/Simplex/Chat/Options.hs b/src/Simplex/Chat/Options.hs index a6f2b759e..f8cab1e35 100644 --- a/src/Simplex/Chat/Options.hs +++ b/src/Simplex/Chat/Options.hs @@ -42,6 +42,7 @@ data ChatOpts = ChatOpts allowInstantFiles :: Bool, autoAcceptFileSize :: Integer, muteNotifications :: Bool, + markRead :: Bool, maintenance :: Bool } @@ -268,6 +269,12 @@ chatOptsP appDir defaultDbFileName = do ( long "mute" <> help "Mute notifications" ) + markRead <- + switch + ( long "mark-read" + <> short 'r' + <> help "Mark shown messages as read" + ) maintenance <- switch ( long "maintenance" @@ -286,6 +293,7 @@ chatOptsP appDir defaultDbFileName = do allowInstantFiles, autoAcceptFileSize, muteNotifications, + markRead, maintenance } diff --git a/src/Simplex/Chat/Terminal.hs b/src/Simplex/Chat/Terminal.hs index 89d234f94..c27675678 100644 --- a/src/Simplex/Chat/Terminal.hs +++ b/src/Simplex/Chat/Terminal.hs @@ -44,7 +44,7 @@ simplexChatTerminal cfg opts t = handle checkDBKeyError . simplexChatCore cfg opts $ \u cc -> do ct <- newChatTerminal t opts when (firstTime cc) . printToTerminal ct $ chatWelcome u - runChatTerminal ct cc + runChatTerminal ct cc opts checkDBKeyError :: SQLError -> IO () checkDBKeyError e = case sqlError e of @@ -53,5 +53,5 @@ checkDBKeyError e = case sqlError e of exitFailure _ -> throwIO e -runChatTerminal :: ChatTerminal -> ChatController -> IO () -runChatTerminal ct cc = raceAny_ [runTerminalInput ct cc, runTerminalOutput ct cc, runInputLoop ct cc] +runChatTerminal :: ChatTerminal -> ChatController -> ChatOpts -> IO () +runChatTerminal ct cc opts = raceAny_ [runTerminalInput ct cc, runTerminalOutput ct cc opts, runInputLoop ct cc] diff --git a/src/Simplex/Chat/Terminal/Output.hs b/src/Simplex/Chat/Terminal/Output.hs index 4fa6931f5..be8aa12cf 100644 --- a/src/Simplex/Chat/Terminal/Output.hs +++ b/src/Simplex/Chat/Terminal/Output.hs @@ -142,13 +142,13 @@ withTermLock ChatTerminal {termLock} action = do action atomically $ putTMVar termLock () -runTerminalOutput :: ChatTerminal -> ChatController -> IO () -runTerminalOutput ct cc@ChatController {outputQ, showLiveItems, logFilePath} = do +runTerminalOutput :: ChatTerminal -> ChatController -> ChatOpts -> IO () +runTerminalOutput ct cc@ChatController {outputQ, showLiveItems, logFilePath} ChatOpts {markRead} = do forever $ do (_, outputRH, r) <- atomically $ readTBQueue outputQ case r of - CRNewChatItem u ci -> markChatItemRead u ci - CRChatItemUpdated u ci -> markChatItemRead u ci + CRNewChatItem u ci -> when markRead $ markChatItemRead u ci + CRChatItemUpdated u ci -> when markRead $ markChatItemRead u ci CRRemoteHostConnected {remoteHost = RemoteHostInfo {remoteHostId}} -> getRemoteUser remoteHostId CRRemoteHostStopped {remoteHostId_} -> mapM_ removeRemoteUser remoteHostId_ _ -> pure () diff --git a/tests/ChatClient.hs b/tests/ChatClient.hs index 53101cd07..824e6be0a 100644 --- a/tests/ChatClient.hs +++ b/tests/ChatClient.hs @@ -82,6 +82,7 @@ testOpts = allowInstantFiles = True, autoAcceptFileSize = 0, muteNotifications = True, + markRead = True, maintenance = False } @@ -174,7 +175,7 @@ startTestChat_ db cfg opts user = do t <- withVirtualTerminal termSettings pure ct <- newChatTerminal t opts cc <- newChatController db (Just user) cfg opts - chatAsync <- async . runSimplexChat opts user cc . const $ runChatTerminal ct + chatAsync <- async . runSimplexChat opts user cc $ \_u cc' -> runChatTerminal ct cc' opts atomically . unless (maintenance opts) $ readTVar (agentAsync cc) >>= \a -> when (isNothing a) retry termQ <- newTQueueIO termAsync <- async $ readTerminalOutput t termQ diff --git a/tests/RemoteTests.hs b/tests/RemoteTests.hs index f03e19149..13bc2942f 100644 --- a/tests/RemoteTests.hs +++ b/tests/RemoteTests.hs @@ -162,13 +162,13 @@ storedBindingsTest = testChat2 aliceProfile aliceDesktopProfile $ \mobile deskto desktop ##> "/list remote hosts" desktop <## "Remote hosts:" - desktop <## "1. Mobile (connected) [lo 127.0.0.1:52230]" + desktop <##. "1. Mobile (connected) [" stopDesktop mobile desktop desktop ##> "/list remote hosts" desktop <## "Remote hosts:" - desktop <## "1. Mobile [lo 127.0.0.1:52230]" + desktop <##. "1. Mobile [" - -- TODO: more parser tests +-- TODO: more parser tests remoteMessageTest :: HasCallStack => FilePath -> IO () remoteMessageTest = testChat3 aliceProfile aliceDesktopProfile bobProfile $ \mobile desktop bob -> do From acaa597c908488efd9a2fb5c3b6474bd6d1f9fea Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com> Date: Sun, 3 Dec 2023 15:42:43 +0000 Subject: [PATCH 10/13] desktop, android: fix image not appearing in view when received (#3504) * desktop, android: fix image not appearing in view when received * change to KeyChangeEffect --- .../chat/simplex/common/views/chat/item/CIImageView.kt | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/CIImageView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/CIImageView.kt index 8b0b2debc..1e5919c0b 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/CIImageView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/CIImageView.kt @@ -164,6 +164,12 @@ fun CIImageView( } } } + } else { + KeyChangeEffect(file) { + if (res.value == null) { + res.value = imageAndFilePath(file) + } + } } val loaded = res.value if (loaded != null) { From fec5ff3f15ebea7b51b56f4ac612f487cea84328 Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com> Date: Sun, 3 Dec 2023 22:21:13 +0000 Subject: [PATCH 11/13] docs: SimpleX address (#3508) * docs: SimpleX address * table * header --- docs/guide/app-settings.md | 2 + docs/guide/making-connections.md | 65 ++++++++++++++++++++++++++++++++ 2 files changed, 67 insertions(+) diff --git a/docs/guide/app-settings.md b/docs/guide/app-settings.md index 5482a1297..3a966e18e 100644 --- a/docs/guide/app-settings.md +++ b/docs/guide/app-settings.md @@ -45,6 +45,8 @@ When people connect to you via this address, you will receive a connection reque If you start receiving too many requests via this address it is always safe to remove it – all the connections you created via this address will remain active, as this address is not used to deliver the messages. +See the comparison with [1-time invitation links](./making-connections.md#comparison-of-1-time-invitation-links-and-simplex-contact-addresses). + Read more in [this post](../../blog/20221108-simplex-chat-v4.2-security-audit-new-website.md#auto-accept-contact-requests). ### Chat preferences diff --git a/docs/guide/making-connections.md b/docs/guide/making-connections.md index 0cf46aecd..cf14883ae 100644 --- a/docs/guide/making-connections.md +++ b/docs/guide/making-connections.md @@ -13,6 +13,71 @@ Private Connection — connect using an invitation link or QR code via video or Group Chat — Users have the option to create a secret group, share their contact link [which can be deleted later on], or generate a one-time invitation link. +## Your SimpleX contact address + +You can [create an optional long term address](./app-settings.md#your-simplex-contact-address) for other people to connect with you. Unlike 1-time invitation links, these addresses can be used many times, that makes them good to share online, e.g. on social media platforms, or in email signatures. That helps more people discover SimpleX Chat, so please do it! + +When people connect to you via this address, you will receive a connection request that you can accept or reject. You can configure an automatic acceptance of connection request and an automatic welcome message that will be sent to the new contacts. You can also share this address as part of your SimpleX profile, so group members can connect to you, and you contacts can share it with others - if this is something that you want. + +If you start receiving too many requests via this address it is always safe to remove it – all the connections you created via this address will remain active, as this address is not used to deliver the messages. + +### Comparison of 1-time invitation links and SimpleX Contact addresses + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
1-time invitation linkSimpleX contact address
Can be used many times?NoYes
Can be included in user profile?No, as it can only be used once.Yes, to allow group members to connect directly, and your contacts to pass it on to their contacts.
When to use it?With somebody you know, via another communication channel or QR code (in person or during a video call)Where many people can see and connect via it, e.g. in email signature, website, social media or group chat.
SecurityMore secure, as can only be used once, and the initial connection request (including profile) is encrypted with double ratchet.Initial connection request is also e2e encrypted, but without double ratchet (it is initialized when request is accepted).
IdentificationBoth sides know who they connect to, as they know with whom and by who the link was shared. You can attach alias to this invitation as soon as you share it or use it, to identify the other person when connection is established.Only the person using the address knows who they connect to, via the channel where they found the address (email, social media, etc.). The address owner can only see the user profile of the request, and has no proof of identity from the person sending the request*.
Advantages over other platformsThere is no direct analogy, other platforms don’t offer one-time invitations without any fixed part identifying the user.Unlike addresses in other platforms, SimpleX addresses are not used to deliver the messages — only the initial connection requests.
It means that removing this address will not break the contacts made via it (like changing an email address would), it would only prevent new connections, which makes it a good solution against spam and abuse.
Vulnerability to attacksUntil the connection is established, anybody who intercepts this link can connect to it, so it has to be verified with the original contact that the connection succeeded.These addresses are vulnerable to connection request spam. Unlike other platforms, you can delete or change the address, without losing any contacts (see above).
Passive attacks on connection linksBoth types of links are not vulnerable if simply observed — they only contain public keys. So they can be safely shared via insecure or public channels, as long as you can confirm that you connected to the intended person.
Active attacks on connection linksIf the link is substituted via the attack on the channel used to share it, the connection security can be compromised, and the original messages monitored (man-in-the-middle attack). If it is a real risk then security code should be verified to mitigate it - doing so proves** that the link and keys were not substituted, and that the end-to-end encryption is secure.
+ +* Adding optional verified identities that we plan in the future will change it — the address owner will have an option to request identity verification before accepting the connection. + +** Connection security code is the cryptographic hash (SHA256) of combined public keys of both sides — there are 2256 possible security codes (1 with 77 zeros – about [1000 times smaller](https://www.wolframalpha.com/input?i=2%5E256) than the estimated number of atoms in the visible universe). + ## Conversation preferences Tap on one of your conversations to open conversation preferences. From 3481d379c69b0a7e750165a52b2c77de6b27a259 Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com> Date: Mon, 4 Dec 2023 10:01:37 +0000 Subject: [PATCH 12/13] core: compatibility with GHC 8.10.7, narrow dependency ranges (#3503) * Revert "raise lower bound on mtl to a real version (#3499)" This reverts commit f94c0311c115ec2127acc3f22ef928cb9ee0ad2f. * Revert "core: expand ranges to fit ghc 8.10 & 9.6 (#3496)" This reverts commit 9a1c7f41f770df72891c4fd6a3f459a9062bfd42. * update simplexmq * remove netword-transport fork * simplexmq * fix test * fix index-state in cabal.project * simplexmq * simplexmq * bytestring,simplexmq * template-haskell, simplexmq * simplexmq * simplexmq * mtl * simplexmq --- cabal.project | 10 ++- package.yaml | 21 ++++-- scripts/nix/sha256map.nix | 3 +- simplex-chat.cabal | 149 +++++++++++++++++++++++++------------- stack.yaml | 96 ------------------------ 5 files changed, 124 insertions(+), 155 deletions(-) delete mode 100644 stack.yaml diff --git a/cabal.project b/cabal.project index a8bb4c2f8..ecfff99fb 100644 --- a/cabal.project +++ b/cabal.project @@ -4,12 +4,14 @@ packages: . with-compiler: ghc-9.6.3 +index-state: 2023-10-20T00:00:00Z + constraints: zip +disable-bzip2 +disable-zstd source-repository-package type: git location: https://github.com/simplex-chat/simplexmq.git - tag: 117168ccce93ef5ac478c02e3f02a018fa8a2200 + tag: eaf5317834b069144b5f4897f9c79831983e54dd source-repository-package type: git @@ -45,3 +47,9 @@ source-repository-package type: git location: https://github.com/simplex-chat/android-support.git tag: 9aa09f148089d6752ce563b14c2df1895718d806 + +-- TODO this fork is only needed to compile with GHC 8.10.7 - it allows previous base version +source-repository-package + type: git + location: https://github.com/simplex-chat/zip.git + tag: bd421c6b19cc4c465cd7af1f6f26169fb8ee1ebc diff --git a/package.yaml b/package.yaml index e77d43546..6734df9fb 100644 --- a/package.yaml +++ b/package.yaml @@ -19,7 +19,6 @@ dependencies: - attoparsec == 0.14.* - base >= 4.7 && < 5 - base64-bytestring >= 1.0 && < 1.3 - - bytestring == 0.11.* - composition == 1.0.* - constraints >= 0.12 && < 0.14 - containers == 0.6.* @@ -32,10 +31,10 @@ dependencies: - filepath == 1.4.* - http-types == 0.12.* - http2 >= 4.2.2 && < 4.3 - - memory >= 0.15 && < 0.19 - - mtl >= 2.3.1 && < 3 + - memory == 0.18.* + - mtl >= 2.3.1 && < 3.0 - network >= 3.1.2.7 && < 3.2 - - network-transport >= 0.5.6 && < 0.6 + - network-transport == 0.5.6 - optparse-applicative >= 0.15 && < 0.17 - process == 1.6.* - random >= 1.1 && < 1.3 @@ -45,14 +44,12 @@ dependencies: - socks == 0.6.* - sqlcipher-simple == 0.4.* - stm == 2.5.* - - template-haskell >= 2.16 && < 2.21 - terminal == 0.2.* - - text >= 2.0 && < 3 - time == 1.9.* - tls >= 1.6.0 && < 1.7 - unliftio == 0.2.* - unliftio-core == 0.2.* - - zip >= 1.7 && < 2.1 + - zip == 2.0.* flags: swift: @@ -64,6 +61,16 @@ when: - condition: flag(swift) cpp-options: - -DswiftJSON + - condition: impl(ghc >= 9.6.2) + dependencies: + - bytestring == 0.11.* + - template-haskell == 2.20.* + - text >= 2.0.1 && < 2.2 + - condition: impl(ghc < 9.6.2) + dependencies: + - bytestring == 0.10.* + - template-haskell == 2.16.* + - text >= 1.2.3.0 && < 1.3 library: source-dirs: src diff --git a/scripts/nix/sha256map.nix b/scripts/nix/sha256map.nix index 7e39845fe..f1c383a96 100644 --- a/scripts/nix/sha256map.nix +++ b/scripts/nix/sha256map.nix @@ -1,5 +1,5 @@ { - "https://github.com/simplex-chat/simplexmq.git"."117168ccce93ef5ac478c02e3f02a018fa8a2200" = "1091c4b8qjp9v29z862b7clda90w3kb4v2aqp5b2jh6yprlga98w"; + "https://github.com/simplex-chat/simplexmq.git"."eaf5317834b069144b5f4897f9c79831983e54dd" = "0jlic1q08mq9p9sgvigmc59r6x1r5fa1zsfqvvrwd97pwain36mj"; "https://github.com/simplex-chat/hs-socks.git"."a30cc7a79a08d8108316094f8f2f82a0c5e1ac51" = "0yasvnr7g91k76mjkamvzab2kvlb1g5pspjyjn2fr6v83swjhj38"; "https://github.com/kazu-yamamoto/http2.git"."f5525b755ff2418e6e6ecc69e877363b0d0bcaeb" = "0fyx0047gvhm99ilp212mmz37j84cwrfnpmssib5dw363fyb88b6"; "https://github.com/simplex-chat/direct-sqlcipher.git"."f814ee68b16a9447fbb467ccc8f29bdd3546bfd9" = "1ql13f4kfwkbaq7nygkxgw84213i0zm7c1a8hwvramayxl38dq5d"; @@ -7,4 +7,5 @@ "https://github.com/simplex-chat/aeson.git"."aab7b5a14d6c5ea64c64dcaee418de1bb00dcc2b" = "0jz7kda8gai893vyvj96fy962ncv8dcsx71fbddyy8zrvc88jfrr"; "https://github.com/simplex-chat/haskell-terminal.git"."f708b00009b54890172068f168bf98508ffcd495" = "0zmq7lmfsk8m340g47g5963yba7i88n4afa6z93sg9px5jv1mijj"; "https://github.com/simplex-chat/android-support.git"."9aa09f148089d6752ce563b14c2df1895718d806" = "0pbf2pf13v2kjzi397nr13f1h3jv0imvsq8rpiyy2qyx5vd50pqn"; + "https://github.com/simplex-chat/zip.git"."bd421c6b19cc4c465cd7af1f6f26169fb8ee1ebc" = "1csqfjhvc8wb5h4kxxndmb6iw7b4ib9ff2n81hrizsmnf45a6gg0"; } diff --git a/simplex-chat.cabal b/simplex-chat.cabal index 3c080e4b5..a53c15c78 100644 --- a/simplex-chat.cabal +++ b/simplex-chat.cabal @@ -1,6 +1,6 @@ cabal-version: 1.12 --- This file has been generated from package.yaml by hpack version 0.35.5. +-- This file has been generated from package.yaml by hpack version 0.35.0. -- -- see: https://github.com/sol/hpack @@ -171,7 +171,6 @@ library , attoparsec ==0.14.* , base >=4.7 && <5 , base64-bytestring >=1.0 && <1.3 - , bytestring ==0.11.* , composition ==1.0.* , constraints >=0.12 && <0.14 , containers ==0.6.* @@ -184,10 +183,10 @@ library , filepath ==1.4.* , http-types ==0.12.* , http2 >=4.2.2 && <4.3 - , memory >=0.15 && <0.19 - , mtl >=2.3.1 && <3 + , memory ==0.18.* + , mtl >=2.3.1 && <3.0 , network >=3.1.2.7 && <3.2 - , network-transport >=0.5.6 && <0.6 + , network-transport ==0.5.6 , optparse-applicative >=0.15 && <0.17 , process ==1.6.* , random >=1.1 && <1.3 @@ -197,17 +196,25 @@ library , socks ==0.6.* , sqlcipher-simple ==0.4.* , stm ==2.5.* - , template-haskell >=2.16 && <2.21 , terminal ==0.2.* - , text >=2.0 && <3 , time ==1.9.* , tls >=1.6.0 && <1.7 , unliftio ==0.2.* , unliftio-core ==0.2.* - , zip >=1.7 && <2.1 + , zip ==2.0.* default-language: Haskell2010 if flag(swift) cpp-options: -DswiftJSON + if impl(ghc >= 9.6.2) + build-depends: + bytestring ==0.11.* + , template-haskell ==2.20.* + , text >=2.0.1 && <2.2 + if impl(ghc < 9.6.2) + build-depends: + bytestring ==0.10.* + , template-haskell ==2.16.* + , text >=1.2.3.0 && <1.3 executable simplex-bot main-is: Main.hs @@ -223,7 +230,6 @@ executable simplex-bot , attoparsec ==0.14.* , base >=4.7 && <5 , base64-bytestring >=1.0 && <1.3 - , bytestring ==0.11.* , composition ==1.0.* , constraints >=0.12 && <0.14 , containers ==0.6.* @@ -236,10 +242,10 @@ executable simplex-bot , filepath ==1.4.* , http-types ==0.12.* , http2 >=4.2.2 && <4.3 - , memory >=0.15 && <0.19 - , mtl >=2.3.1 && <3 + , memory ==0.18.* + , mtl >=2.3.1 && <3.0 , network >=3.1.2.7 && <3.2 - , network-transport >=0.5.6 && <0.6 + , network-transport ==0.5.6 , optparse-applicative >=0.15 && <0.17 , process ==1.6.* , random >=1.1 && <1.3 @@ -250,17 +256,25 @@ executable simplex-bot , socks ==0.6.* , sqlcipher-simple ==0.4.* , stm ==2.5.* - , template-haskell >=2.16 && <2.21 , terminal ==0.2.* - , text >=2.0 && <3 , time ==1.9.* , tls >=1.6.0 && <1.7 , unliftio ==0.2.* , unliftio-core ==0.2.* - , zip >=1.7 && <2.1 + , zip ==2.0.* default-language: Haskell2010 if flag(swift) cpp-options: -DswiftJSON + if impl(ghc >= 9.6.2) + build-depends: + bytestring ==0.11.* + , template-haskell ==2.20.* + , text >=2.0.1 && <2.2 + if impl(ghc < 9.6.2) + build-depends: + bytestring ==0.10.* + , template-haskell ==2.16.* + , text >=1.2.3.0 && <1.3 executable simplex-bot-advanced main-is: Main.hs @@ -276,7 +290,6 @@ executable simplex-bot-advanced , attoparsec ==0.14.* , base >=4.7 && <5 , base64-bytestring >=1.0 && <1.3 - , bytestring ==0.11.* , composition ==1.0.* , constraints >=0.12 && <0.14 , containers ==0.6.* @@ -289,10 +302,10 @@ executable simplex-bot-advanced , filepath ==1.4.* , http-types ==0.12.* , http2 >=4.2.2 && <4.3 - , memory >=0.15 && <0.19 - , mtl >=2.3.1 && <3 + , memory ==0.18.* + , mtl >=2.3.1 && <3.0 , network >=3.1.2.7 && <3.2 - , network-transport >=0.5.6 && <0.6 + , network-transport ==0.5.6 , optparse-applicative >=0.15 && <0.17 , process ==1.6.* , random >=1.1 && <1.3 @@ -303,17 +316,25 @@ executable simplex-bot-advanced , socks ==0.6.* , sqlcipher-simple ==0.4.* , stm ==2.5.* - , template-haskell >=2.16 && <2.21 , terminal ==0.2.* - , text >=2.0 && <3 , time ==1.9.* , tls >=1.6.0 && <1.7 , unliftio ==0.2.* , unliftio-core ==0.2.* - , zip >=1.7 && <2.1 + , zip ==2.0.* default-language: Haskell2010 if flag(swift) cpp-options: -DswiftJSON + if impl(ghc >= 9.6.2) + build-depends: + bytestring ==0.11.* + , template-haskell ==2.20.* + , text >=2.0.1 && <2.2 + if impl(ghc < 9.6.2) + build-depends: + bytestring ==0.10.* + , template-haskell ==2.16.* + , text >=1.2.3.0 && <1.3 executable simplex-broadcast-bot main-is: ../Main.hs @@ -331,7 +352,6 @@ executable simplex-broadcast-bot , attoparsec ==0.14.* , base >=4.7 && <5 , base64-bytestring >=1.0 && <1.3 - , bytestring ==0.11.* , composition ==1.0.* , constraints >=0.12 && <0.14 , containers ==0.6.* @@ -344,10 +364,10 @@ executable simplex-broadcast-bot , filepath ==1.4.* , http-types ==0.12.* , http2 >=4.2.2 && <4.3 - , memory >=0.15 && <0.19 - , mtl >=2.3.1 && <3 + , memory ==0.18.* + , mtl >=2.3.1 && <3.0 , network >=3.1.2.7 && <3.2 - , network-transport >=0.5.6 && <0.6 + , network-transport ==0.5.6 , optparse-applicative >=0.15 && <0.17 , process ==1.6.* , random >=1.1 && <1.3 @@ -358,17 +378,25 @@ executable simplex-broadcast-bot , socks ==0.6.* , sqlcipher-simple ==0.4.* , stm ==2.5.* - , template-haskell >=2.16 && <2.21 , terminal ==0.2.* - , text >=2.0 && <3 , time ==1.9.* , tls >=1.6.0 && <1.7 , unliftio ==0.2.* , unliftio-core ==0.2.* - , zip >=1.7 && <2.1 + , zip ==2.0.* default-language: Haskell2010 if flag(swift) cpp-options: -DswiftJSON + if impl(ghc >= 9.6.2) + build-depends: + bytestring ==0.11.* + , template-haskell ==2.20.* + , text >=2.0.1 && <2.2 + if impl(ghc < 9.6.2) + build-depends: + bytestring ==0.10.* + , template-haskell ==2.16.* + , text >=1.2.3.0 && <1.3 executable simplex-chat main-is: Main.hs @@ -385,7 +413,6 @@ executable simplex-chat , attoparsec ==0.14.* , base >=4.7 && <5 , base64-bytestring >=1.0 && <1.3 - , bytestring ==0.11.* , composition ==1.0.* , constraints >=0.12 && <0.14 , containers ==0.6.* @@ -398,10 +425,10 @@ executable simplex-chat , filepath ==1.4.* , http-types ==0.12.* , http2 >=4.2.2 && <4.3 - , memory >=0.15 && <0.19 - , mtl >=2.3.1 && <3 + , memory ==0.18.* + , mtl >=2.3.1 && <3.0 , network ==3.1.* - , network-transport >=0.5.6 && <0.6 + , network-transport ==0.5.6 , optparse-applicative >=0.15 && <0.17 , process ==1.6.* , random >=1.1 && <1.3 @@ -412,18 +439,26 @@ executable simplex-chat , socks ==0.6.* , sqlcipher-simple ==0.4.* , stm ==2.5.* - , template-haskell >=2.16 && <2.21 , terminal ==0.2.* - , text >=2.0 && <3 , time ==1.9.* , tls >=1.6.0 && <1.7 , unliftio ==0.2.* , unliftio-core ==0.2.* , websockets ==0.12.* - , zip >=1.7 && <2.1 + , zip ==2.0.* default-language: Haskell2010 if flag(swift) cpp-options: -DswiftJSON + if impl(ghc >= 9.6.2) + build-depends: + bytestring ==0.11.* + , template-haskell ==2.20.* + , text >=2.0.1 && <2.2 + if impl(ghc < 9.6.2) + build-depends: + bytestring ==0.10.* + , template-haskell ==2.16.* + , text >=1.2.3.0 && <1.3 executable simplex-directory-service main-is: ../Main.hs @@ -443,7 +478,6 @@ executable simplex-directory-service , attoparsec ==0.14.* , base >=4.7 && <5 , base64-bytestring >=1.0 && <1.3 - , bytestring ==0.11.* , composition ==1.0.* , constraints >=0.12 && <0.14 , containers ==0.6.* @@ -456,10 +490,10 @@ executable simplex-directory-service , filepath ==1.4.* , http-types ==0.12.* , http2 >=4.2.2 && <4.3 - , memory >=0.15 && <0.19 - , mtl >=2.3.1 && <3 + , memory ==0.18.* + , mtl >=2.3.1 && <3.0 , network >=3.1.2.7 && <3.2 - , network-transport >=0.5.6 && <0.6 + , network-transport ==0.5.6 , optparse-applicative >=0.15 && <0.17 , process ==1.6.* , random >=1.1 && <1.3 @@ -470,17 +504,25 @@ executable simplex-directory-service , socks ==0.6.* , sqlcipher-simple ==0.4.* , stm ==2.5.* - , template-haskell >=2.16 && <2.21 , terminal ==0.2.* - , text >=2.0 && <3 , time ==1.9.* , tls >=1.6.0 && <1.7 , unliftio ==0.2.* , unliftio-core ==0.2.* - , zip >=1.7 && <2.1 + , zip ==2.0.* default-language: Haskell2010 if flag(swift) cpp-options: -DswiftJSON + if impl(ghc >= 9.6.2) + build-depends: + bytestring ==0.11.* + , template-haskell ==2.20.* + , text >=2.0.1 && <2.2 + if impl(ghc < 9.6.2) + build-depends: + bytestring ==0.10.* + , template-haskell ==2.16.* + , text >=1.2.3.0 && <1.3 test-suite simplex-chat-test type: exitcode-stdio-1.0 @@ -524,7 +566,6 @@ test-suite simplex-chat-test , attoparsec ==0.14.* , base >=4.7 && <5 , base64-bytestring >=1.0 && <1.3 - , bytestring ==0.11.* , composition ==1.0.* , constraints >=0.12 && <0.14 , containers ==0.6.* @@ -540,10 +581,10 @@ test-suite simplex-chat-test , hspec ==2.11.* , http-types ==0.12.* , http2 >=4.2.2 && <4.3 - , memory >=0.15 && <0.19 - , mtl >=2.3.1 && <3 + , memory ==0.18.* + , mtl >=2.3.1 && <3.0 , network ==3.1.* - , network-transport >=0.5.6 && <0.6 + , network-transport ==0.5.6 , optparse-applicative >=0.15 && <0.17 , process ==1.6.* , random >=1.1 && <1.3 @@ -555,14 +596,22 @@ test-suite simplex-chat-test , socks ==0.6.* , sqlcipher-simple ==0.4.* , stm ==2.5.* - , template-haskell >=2.16 && <2.21 , terminal ==0.2.* - , text >=2.0 && <3 , time ==1.9.* , tls >=1.6.0 && <1.7 , unliftio ==0.2.* , unliftio-core ==0.2.* - , zip >=1.7 && <2.1 + , zip ==2.0.* default-language: Haskell2010 if flag(swift) cpp-options: -DswiftJSON + if impl(ghc >= 9.6.2) + build-depends: + bytestring ==0.11.* + , template-haskell ==2.20.* + , text >=2.0.1 && <2.2 + if impl(ghc < 9.6.2) + build-depends: + bytestring ==0.10.* + , template-haskell ==2.16.* + , text >=1.2.3.0 && <1.3 diff --git a/stack.yaml b/stack.yaml deleted file mode 100644 index 69f8e2121..000000000 --- a/stack.yaml +++ /dev/null @@ -1,96 +0,0 @@ -# This file was automatically generated by 'stack init' -# -# Some commonly used options have been documented as comments in this file. -# For advanced use and comprehensive documentation of the format, please see: -# https://docs.haskellstack.org/en/stable/yaml_configuration/ - -# Resolver to choose a 'specific' stackage snapshot or a compiler version. -# A snapshot resolver dictates the compiler version and the set of packages -# to be used for project dependencies. For example: -# -# resolver: lts-3.5 -# resolver: nightly-2015-09-21 -# resolver: ghc-7.10.2 -# -# The location of a snapshot can be provided as a file or url. Stack assumes -# a snapshot provided as a file might change, whereas a url resource does not. -# -# resolver: ./custom-snapshot.yaml -# resolver: https://example.com/snapshots/2018-01-01.yaml -resolver: lts-18.21 - -# User packages to be built. -# Various formats can be used as shown in the example below. -# -# packages: -# - some-directory -# - https://example.com/foo/bar/baz-0.0.2.tar.gz -# subdirs: -# - auto-update -# - wai -packages: - - . -# Dependency packages to be pulled from upstream that are not in the resolver. -# These entries can reference officially published versions as well as -# forks / in-progress versions pinned to a git hash. For example: -# -extra-deps: - - cryptostore-0.2.1.0@sha256:9896e2984f36a1c8790f057fd5ce3da4cbcaf8aa73eb2d9277916886978c5b19,3881 - - network-3.1.2.7@sha256:e3d78b13db9512aeb106e44a334ab42b7aa48d26c097299084084cb8be5c5568,4888 - - simple-logger-0.1.0@sha256:be8ede4bd251a9cac776533bae7fb643369ebd826eb948a9a18df1a8dd252ff8,1079 - - tls-1.6.0@sha256:7ae39373fd2de27fb80e90f76d22aeeb9a074a0ddd120cbd02c9c52f516a9e55,6987 - # below hackage dependencies are to update Aeson to 2.0.3 - - OneTuple-0.3.1@sha256:a848c096c9d29e82ffdd30a9998aa2931cbccb3a1bc137539d80f6174d31603e,2262 - - attoparsec-0.14.4@sha256:79584bdada8b730cb5138fca8c35c76fbef75fc1d1e01e6b1d815a5ee9843191,5810 - - hashable-1.4.0.2@sha256:0cddd0229d1aac305ea0404409c0bbfab81f075817bd74b8b2929eff58333e55,5005 - - semialign-1.2.0.1@sha256:0e179b4d3a8eff79001d374d6c91917c6221696b9620f0a4d86852fc6a9b9501,2836 - - text-short-0.1.5@sha256:962c6228555debdc46f758d0317dea16e5240d01419b42966674b08a5c3d8fa6,3498 - - time-compat-1.9.6.1@sha256:42d8f2e08e965e1718917d54ad69e1d06bd4b87d66c41dc7410f59313dba4ed1,5033 - # - simplexmq-1.0.0@sha256:34b2004728ae396e3ae449cd090ba7410781e2b3cefc59259915f4ca5daa9ea8,8561 - # - ../simplexmq - - github: simplex-chat/simplexmq - commit: 117168ccce93ef5ac478c02e3f02a018fa8a2200 - - github: kazu-yamamoto/http2 - commit: f5525b755ff2418e6e6ecc69e877363b0d0bcaeb - # - ../direct-sqlcipher - - github: simplex-chat/direct-sqlcipher - commit: f814ee68b16a9447fbb467ccc8f29bdd3546bfd9 - # - ../sqlcipher-simple - - github: simplex-chat/sqlcipher-simple - commit: a46bd361a19376c5211f1058908fc0ae6bf42446 - # - terminal-0.2.0.0@sha256:de6770ecaae3197c66ac1f0db5a80cf5a5b1d3b64a66a05b50f442de5ad39570,2977 - - github: simplex-chat/aeson - commit: aab7b5a14d6c5ea64c64dcaee418de1bb00dcc2b - - github: simplex-chat/haskell-terminal - commit: f708b00009b54890172068f168bf98508ffcd495 - - github: simplex-chat/android-support - commit: 9aa09f148089d6752ce563b14c2df1895718d806 -# -# extra-deps: [] - -# Override default flag values for local packages and extra-deps -flags: - zip: - disable-bzip2: true - disable-zstd: true - direct-sqlcipher: - openssl: true -# Extra package databases containing global packages -# extra-package-dbs: [] - -# Control whether we use the GHC we find on the path -# system-ghc: true -# -# Require a specific version of stack, using version ranges -# require-stack-version: -any # Default -# require-stack-version: ">=2.1" -# -# Override the architecture used by stack, especially useful on Windows -# arch: i386 -# arch: x86_64 -# -# Extra directories used by stack for building -# extra-lib-dirs: [/path/to/dir] -# -# Allow a newer minor version of GHC than the snapshot specifies -# compiler-check: newer-minor From 087acd91805d070876943867a8837afa66617ca4 Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com> Date: Mon, 4 Dec 2023 12:29:49 +0000 Subject: [PATCH 13/13] changes to support GHC 8.10.7 (#3512) * Revert "raise lower bound on mtl to a real version (#3499)" This reverts commit f94c0311c115ec2127acc3f22ef928cb9ee0ad2f. * Revert "core: expand ranges to fit ghc 8.10 & 9.6 (#3496)" This reverts commit 9a1c7f41f770df72891c4fd6a3f459a9062bfd42. * update simplexmq * remove netword-transport fork * compatibility with GHC 8.10.7 * simplexmq * fix test * simplexmq, deps * update sqlcipher deps in sha256nix * fix index-state in cabal.project * index-state * remove import * add cabal.project.freeze * simplexmq * remove freeze * simplexmq * bytestring,simplexmq * template-haskell, simplexmq * simplexmq * simplexmq * simplexmq * mtl * simplexmq * remove duplicate index-state --- apps/simplex-bot-advanced/Main.hs | 2 +- apps/simplex-broadcast-bot/src/Broadcast/Bot.hs | 2 +- apps/simplex-chat/Server.hs | 1 + .../src/Directory/Service.hs | 2 +- cabal.project | 6 +++--- package.yaml | 2 +- scripts/nix/sha256map.nix | 4 ++-- simplex-chat.cabal | 14 +++++++------- src/Simplex/Chat.hs | 1 + src/Simplex/Chat/Archive.hs | 1 + src/Simplex/Chat/Bot.hs | 1 + src/Simplex/Chat/Mobile/File.hs | 1 + src/Simplex/Chat/Mobile/WebRTC.hs | 2 ++ src/Simplex/Chat/Store/Connections.hs | 1 + src/Simplex/Chat/Store/Direct.hs | 2 ++ src/Simplex/Chat/Store/Files.hs | 2 ++ src/Simplex/Chat/Store/Groups.hs | 2 ++ src/Simplex/Chat/Store/Messages.hs | 2 ++ src/Simplex/Chat/Store/Profiles.hs | 1 + src/Simplex/Chat/Store/Shared.hs | 2 ++ src/Simplex/Chat/Terminal.hs | 2 +- src/Simplex/Chat/Terminal/Input.hs | 1 + src/Simplex/Chat/Util.hs | 1 + tests/ChatClient.hs | 1 + 24 files changed, 39 insertions(+), 17 deletions(-) diff --git a/apps/simplex-bot-advanced/Main.hs b/apps/simplex-bot-advanced/Main.hs index 510c9c30b..50d7005e3 100644 --- a/apps/simplex-bot-advanced/Main.hs +++ b/apps/simplex-bot-advanced/Main.hs @@ -8,7 +8,7 @@ module Main where import Control.Concurrent.Async import Control.Concurrent.STM -import Control.Monad.Reader +import Control.Monad import qualified Data.Text as T import Simplex.Chat.Bot import Simplex.Chat.Controller diff --git a/apps/simplex-broadcast-bot/src/Broadcast/Bot.hs b/apps/simplex-broadcast-bot/src/Broadcast/Bot.hs index 326a8728a..5fa3fff0a 100644 --- a/apps/simplex-broadcast-bot/src/Broadcast/Bot.hs +++ b/apps/simplex-broadcast-bot/src/Broadcast/Bot.hs @@ -9,7 +9,7 @@ module Broadcast.Bot where import Control.Concurrent (forkIO) import Control.Concurrent.Async import Control.Concurrent.STM -import Control.Monad.Reader +import Control.Monad import qualified Data.Text as T import Broadcast.Options import Simplex.Chat.Bot diff --git a/apps/simplex-chat/Server.hs b/apps/simplex-chat/Server.hs index 46c71a796..3f4484eac 100644 --- a/apps/simplex-chat/Server.hs +++ b/apps/simplex-chat/Server.hs @@ -8,6 +8,7 @@ module Server where +import Control.Monad import Control.Monad.Except import Control.Monad.Reader import Data.Aeson (FromJSON, ToJSON) diff --git a/apps/simplex-directory-service/src/Directory/Service.hs b/apps/simplex-directory-service/src/Directory/Service.hs index 291457591..fb187bbeb 100644 --- a/apps/simplex-directory-service/src/Directory/Service.hs +++ b/apps/simplex-directory-service/src/Directory/Service.hs @@ -15,7 +15,7 @@ where import Control.Concurrent (forkIO) import Control.Concurrent.Async import Control.Concurrent.STM -import Control.Monad.Reader +import Control.Monad import qualified Data.ByteString.Char8 as B import Data.List (sortOn) import Data.Maybe (fromMaybe, maybeToList) diff --git a/cabal.project b/cabal.project index 9205c0a4a..777f094bc 100644 --- a/cabal.project +++ b/cabal.project @@ -4,7 +4,7 @@ packages: . with-compiler: ghc-8.10.7 -index-state: 2023-10-20T00:00:00Z +index-state: 2023-10-06T00:00:00Z constraints: zip +disable-bzip2 +disable-zstd @@ -26,12 +26,12 @@ source-repository-package source-repository-package type: git location: https://github.com/simplex-chat/direct-sqlcipher.git - tag: 34309410eb2069b029b8fc1872deb1e0db123294 + tag: f814ee68b16a9447fbb467ccc8f29bdd3546bfd9 source-repository-package type: git location: https://github.com/simplex-chat/sqlcipher-simple.git - tag: 5e154a2aeccc33ead6c243ec07195ab673137221 + tag: a46bd361a19376c5211f1058908fc0ae6bf42446 source-repository-package type: git diff --git a/package.yaml b/package.yaml index 2d1177354..29b1be957 100644 --- a/package.yaml +++ b/package.yaml @@ -22,7 +22,7 @@ dependencies: - composition == 1.0.* - constraints >= 0.12 && < 0.14 - containers == 0.6.* - - cryptonite >= 0.27 && < 0.30 + - cryptonite == 0.30.* - data-default >= 0.7 && < 0.8 - directory == 1.3.* - direct-sqlcipher == 2.3.* diff --git a/scripts/nix/sha256map.nix b/scripts/nix/sha256map.nix index 86eccf886..b99146142 100644 --- a/scripts/nix/sha256map.nix +++ b/scripts/nix/sha256map.nix @@ -2,8 +2,8 @@ "https://github.com/simplex-chat/simplexmq.git"."eaf5317834b069144b5f4897f9c79831983e54dd" = "0jlic1q08mq9p9sgvigmc59r6x1r5fa1zsfqvvrwd97pwain36mj"; "https://github.com/simplex-chat/hs-socks.git"."a30cc7a79a08d8108316094f8f2f82a0c5e1ac51" = "0yasvnr7g91k76mjkamvzab2kvlb1g5pspjyjn2fr6v83swjhj38"; "https://github.com/kazu-yamamoto/http2.git"."f5525b755ff2418e6e6ecc69e877363b0d0bcaeb" = "0fyx0047gvhm99ilp212mmz37j84cwrfnpmssib5dw363fyb88b6"; - "https://github.com/simplex-chat/direct-sqlcipher.git"."34309410eb2069b029b8fc1872deb1e0db123294" = "0kwkmhyfsn2lixdlgl15smgr1h5gjk7fky6abzh8rng2h5ymnffd"; - "https://github.com/simplex-chat/sqlcipher-simple.git"."5e154a2aeccc33ead6c243ec07195ab673137221" = "1d1gc5wax4vqg0801ajsmx1sbwvd9y7p7b8mmskvqsmpbwgbh0m0"; + "https://github.com/simplex-chat/direct-sqlcipher.git"."f814ee68b16a9447fbb467ccc8f29bdd3546bfd9" = "0kiwhvml42g9anw4d2v0zd1fpc790pj9syg5x3ik4l97fnkbbwpp"; + "https://github.com/simplex-chat/sqlcipher-simple.git"."a46bd361a19376c5211f1058908fc0ae6bf42446" = "1z0r78d8f0812kxbgsm735qf6xx8lvaz27k1a0b4a2m0sshpd5gl"; "https://github.com/simplex-chat/aeson.git"."aab7b5a14d6c5ea64c64dcaee418de1bb00dcc2b" = "0jz7kda8gai893vyvj96fy962ncv8dcsx71fbddyy8zrvc88jfrr"; "https://github.com/simplex-chat/haskell-terminal.git"."f708b00009b54890172068f168bf98508ffcd495" = "0zmq7lmfsk8m340g47g5963yba7i88n4afa6z93sg9px5jv1mijj"; "https://github.com/simplex-chat/android-support.git"."9aa09f148089d6752ce563b14c2df1895718d806" = "0pbf2pf13v2kjzi397nr13f1h3jv0imvsq8rpiyy2qyx5vd50pqn"; diff --git a/simplex-chat.cabal b/simplex-chat.cabal index bd067baee..05e1f106d 100644 --- a/simplex-chat.cabal +++ b/simplex-chat.cabal @@ -174,7 +174,7 @@ library , composition ==1.0.* , constraints >=0.12 && <0.14 , containers ==0.6.* - , cryptonite >=0.27 && <0.30 + , cryptonite ==0.30.* , data-default ==0.7.* , direct-sqlcipher ==2.3.* , directory ==1.3.* @@ -233,7 +233,7 @@ executable simplex-bot , composition ==1.0.* , constraints >=0.12 && <0.14 , containers ==0.6.* - , cryptonite >=0.27 && <0.30 + , cryptonite ==0.30.* , data-default ==0.7.* , direct-sqlcipher ==2.3.* , directory ==1.3.* @@ -293,7 +293,7 @@ executable simplex-bot-advanced , composition ==1.0.* , constraints >=0.12 && <0.14 , containers ==0.6.* - , cryptonite >=0.27 && <0.30 + , cryptonite ==0.30.* , data-default ==0.7.* , direct-sqlcipher ==2.3.* , directory ==1.3.* @@ -355,7 +355,7 @@ executable simplex-broadcast-bot , composition ==1.0.* , constraints >=0.12 && <0.14 , containers ==0.6.* - , cryptonite >=0.27 && <0.30 + , cryptonite ==0.30.* , data-default ==0.7.* , direct-sqlcipher ==2.3.* , directory ==1.3.* @@ -416,7 +416,7 @@ executable simplex-chat , composition ==1.0.* , constraints >=0.12 && <0.14 , containers ==0.6.* - , cryptonite >=0.27 && <0.30 + , cryptonite ==0.30.* , data-default ==0.7.* , direct-sqlcipher ==2.3.* , directory ==1.3.* @@ -481,7 +481,7 @@ executable simplex-directory-service , composition ==1.0.* , constraints >=0.12 && <0.14 , containers ==0.6.* - , cryptonite >=0.27 && <0.30 + , cryptonite ==0.30.* , data-default ==0.7.* , direct-sqlcipher ==2.3.* , directory ==1.3.* @@ -569,7 +569,7 @@ test-suite simplex-chat-test , composition ==1.0.* , constraints >=0.12 && <0.14 , containers ==0.6.* - , cryptonite >=0.27 && <0.30 + , cryptonite ==0.30.* , data-default ==0.7.* , deepseq ==1.4.* , direct-sqlcipher ==2.3.* diff --git a/src/Simplex/Chat.hs b/src/Simplex/Chat.hs index 1b46a642b..bcd533a9a 100644 --- a/src/Simplex/Chat.hs +++ b/src/Simplex/Chat.hs @@ -18,6 +18,7 @@ module Simplex.Chat where import Control.Applicative (optional, (<|>)) import Control.Concurrent.STM (retry) import Control.Logger.Simple +import Control.Monad import Control.Monad.Except import Control.Monad.IO.Unlift import Control.Monad.Reader diff --git a/src/Simplex/Chat/Archive.hs b/src/Simplex/Chat/Archive.hs index be7b05ea9..22e5f1ee2 100644 --- a/src/Simplex/Chat/Archive.hs +++ b/src/Simplex/Chat/Archive.hs @@ -14,6 +14,7 @@ module Simplex.Chat.Archive where import qualified Codec.Archive.Zip as Z +import Control.Monad import Control.Monad.Except import Control.Monad.Reader import Data.Functor (($>)) diff --git a/src/Simplex/Chat/Bot.hs b/src/Simplex/Chat/Bot.hs index 4c0d37605..3f7e2c2f0 100644 --- a/src/Simplex/Chat/Bot.hs +++ b/src/Simplex/Chat/Bot.hs @@ -8,6 +8,7 @@ module Simplex.Chat.Bot where import Control.Concurrent.Async import Control.Concurrent.STM +import Control.Monad import Control.Monad.Reader import qualified Data.ByteString.Char8 as B import qualified Data.Text as T diff --git a/src/Simplex/Chat/Mobile/File.hs b/src/Simplex/Chat/Mobile/File.hs index 284f56902..1da64a304 100644 --- a/src/Simplex/Chat/Mobile/File.hs +++ b/src/Simplex/Chat/Mobile/File.hs @@ -16,6 +16,7 @@ module Simplex.Chat.Mobile.File ) where +import Control.Monad import Control.Monad.Except import Control.Monad.IO.Class import qualified Data.Aeson as J diff --git a/src/Simplex/Chat/Mobile/WebRTC.hs b/src/Simplex/Chat/Mobile/WebRTC.hs index 588e28545..422cfd5a8 100644 --- a/src/Simplex/Chat/Mobile/WebRTC.hs +++ b/src/Simplex/Chat/Mobile/WebRTC.hs @@ -8,7 +8,9 @@ module Simplex.Chat.Mobile.WebRTC reservedSize, ) where +import Control.Monad import Control.Monad.Except +import Control.Monad.IO.Class import qualified Crypto.Cipher.Types as AES import Data.Bifunctor (bimap) import qualified Data.ByteArray as BA diff --git a/src/Simplex/Chat/Store/Connections.hs b/src/Simplex/Chat/Store/Connections.hs index 019f2bae0..a72b23886 100644 --- a/src/Simplex/Chat/Store/Connections.hs +++ b/src/Simplex/Chat/Store/Connections.hs @@ -16,6 +16,7 @@ module Simplex.Chat.Store.Connections where import Control.Applicative ((<|>)) +import Control.Monad import Control.Monad.Except import Data.Int (Int64) import Data.Maybe (catMaybes, fromMaybe) diff --git a/src/Simplex/Chat/Store/Direct.hs b/src/Simplex/Chat/Store/Direct.hs index 427a5842f..1b3a5285d 100644 --- a/src/Simplex/Chat/Store/Direct.hs +++ b/src/Simplex/Chat/Store/Direct.hs @@ -66,7 +66,9 @@ module Simplex.Chat.Store.Direct ) where +import Control.Monad import Control.Monad.Except +import Control.Monad.IO.Class import Data.Either (rights) import Data.Functor (($>)) import Data.Int (Int64) diff --git a/src/Simplex/Chat/Store/Files.hs b/src/Simplex/Chat/Store/Files.hs index 927f4b947..41d0733db 100644 --- a/src/Simplex/Chat/Store/Files.hs +++ b/src/Simplex/Chat/Store/Files.hs @@ -77,7 +77,9 @@ module Simplex.Chat.Store.Files where import Control.Applicative ((<|>)) +import Control.Monad import Control.Monad.Except +import Control.Monad.IO.Class import Data.Either (rights) import Data.Int (Int64) import Data.Maybe (fromMaybe, isJust, listToMaybe) diff --git a/src/Simplex/Chat/Store/Groups.hs b/src/Simplex/Chat/Store/Groups.hs index 09aef8b91..a3dc5d815 100644 --- a/src/Simplex/Chat/Store/Groups.hs +++ b/src/Simplex/Chat/Store/Groups.hs @@ -113,7 +113,9 @@ module Simplex.Chat.Store.Groups ) where +import Control.Monad import Control.Monad.Except +import Control.Monad.IO.Class import Crypto.Random (ChaChaDRG) import Data.Either (rights) import Data.Int (Int64) diff --git a/src/Simplex/Chat/Store/Messages.hs b/src/Simplex/Chat/Store/Messages.hs index 892f35fe5..102612b4e 100644 --- a/src/Simplex/Chat/Store/Messages.hs +++ b/src/Simplex/Chat/Store/Messages.hs @@ -101,7 +101,9 @@ module Simplex.Chat.Store.Messages ) where +import Control.Monad import Control.Monad.Except +import Control.Monad.IO.Class import Crypto.Random (ChaChaDRG) import Data.Bifunctor (first) import Data.ByteString.Char8 (ByteString) diff --git a/src/Simplex/Chat/Store/Profiles.hs b/src/Simplex/Chat/Store/Profiles.hs index 35a990056..7ce48db4a 100644 --- a/src/Simplex/Chat/Store/Profiles.hs +++ b/src/Simplex/Chat/Store/Profiles.hs @@ -59,6 +59,7 @@ module Simplex.Chat.Store.Profiles ) where +import Control.Monad import Control.Monad.Except import Control.Monad.IO.Class import qualified Data.Aeson.TH as J diff --git a/src/Simplex/Chat/Store/Shared.hs b/src/Simplex/Chat/Store/Shared.hs index f07a89d33..93c3ab197 100644 --- a/src/Simplex/Chat/Store/Shared.hs +++ b/src/Simplex/Chat/Store/Shared.hs @@ -12,7 +12,9 @@ module Simplex.Chat.Store.Shared where import Control.Exception (Exception) import qualified Control.Exception as E +import Control.Monad import Control.Monad.Except +import Control.Monad.IO.Class import Crypto.Random (ChaChaDRG, randomBytesGenerate) import qualified Data.Aeson.TH as J import qualified Data.ByteString.Base64 as B64 diff --git a/src/Simplex/Chat/Terminal.hs b/src/Simplex/Chat/Terminal.hs index 34f51989b..c27675678 100644 --- a/src/Simplex/Chat/Terminal.hs +++ b/src/Simplex/Chat/Terminal.hs @@ -5,7 +5,7 @@ module Simplex.Chat.Terminal where import Control.Exception (handle, throwIO) -import Control.Monad.Except +import Control.Monad import qualified Data.List.NonEmpty as L import Database.SQLite.Simple (SQLError (..)) import qualified Database.SQLite.Simple as DB diff --git a/src/Simplex/Chat/Terminal/Input.hs b/src/Simplex/Chat/Terminal/Input.hs index 31d75f52d..7b96abc1c 100644 --- a/src/Simplex/Chat/Terminal/Input.hs +++ b/src/Simplex/Chat/Terminal/Input.hs @@ -12,6 +12,7 @@ module Simplex.Chat.Terminal.Input where import Control.Applicative (optional, (<|>)) import Control.Concurrent (forkFinally, forkIO, killThread, mkWeakThreadId, threadDelay) +import Control.Monad import Control.Monad.Except import Control.Monad.Reader import qualified Data.Attoparsec.ByteString.Char8 as A diff --git a/src/Simplex/Chat/Util.hs b/src/Simplex/Chat/Util.hs index 3ce663e69..46b5be28b 100644 --- a/src/Simplex/Chat/Util.hs +++ b/src/Simplex/Chat/Util.hs @@ -2,6 +2,7 @@ module Simplex.Chat.Util (week, encryptFile, chunkSize) where import Control.Monad import Control.Monad.Except +import Control.Monad.IO.Class import qualified Data.ByteString.Lazy as LB import Data.Time (NominalDiffTime) import Simplex.Messaging.Crypto.File (CryptoFile (..), CryptoFileArgs (..)) diff --git a/tests/ChatClient.hs b/tests/ChatClient.hs index aba75ec84..824e6be0a 100644 --- a/tests/ChatClient.hs +++ b/tests/ChatClient.hs @@ -13,6 +13,7 @@ import Control.Concurrent (forkIOWithUnmask, killThread, threadDelay) import Control.Concurrent.Async import Control.Concurrent.STM import Control.Exception (bracket, bracket_) +import Control.Monad import Control.Monad.Except import Data.Functor (($>)) import Data.List (dropWhileEnd, find)