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