From 193e17f7afd7540e0d0b3cbaa1f9b9ffc2b7f86f Mon Sep 17 00:00:00 2001 From: Stanislav Dmitrenko <7953703+avently@users.noreply.github.com> Date: Tue, 8 Oct 2024 14:08:22 +0700 Subject: [PATCH] android, desktop: thread-safe terminal items and floating terminal improvements (#4992) * android, desktop: thread-safe terminal items and floating terminal improvements * optimization --- .../chat/simplex/common/model/ChatModel.kt | 13 +++++ .../chat/simplex/common/views/TerminalView.kt | 54 ++++++++++++++++--- .../simplex/common/views/helpers/ModalView.kt | 5 +- .../views/usersettings/DeveloperView.kt | 2 +- .../kotlin/chat/simplex/common/DesktopApp.kt | 8 ++- 5 files changed, 71 insertions(+), 11 deletions(-) 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 7234209577..682a472060 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 @@ -71,6 +71,9 @@ object ChatModel { val groupMembers = mutableStateListOf() val groupMembersIndexes = mutableStateMapOf() + // false: default placement, true: floating window. + // Used for deciding to add terminal items on main thread or not. Floating means appPrefs.terminalAlwaysVisible + var terminalsVisible = setOf() val terminalItems = mutableStateOf>(listOf()) val userAddress = mutableStateOf(null) val chatItemTTL = mutableStateOf(ChatItemTTL.None) @@ -772,6 +775,16 @@ object ChatModel { fun addTerminalItem(item: TerminalItem) { val maxItems = if (appPreferences.developerTools.get()) 500 else 200 + if (terminalsVisible.isNotEmpty()) { + withApi { + addTerminalItem(item, maxItems) + } + } else { + addTerminalItem(item, maxItems) + } + } + + private fun addTerminalItem(item: TerminalItem, maxItems: Int) { if (terminalItems.value.size >= maxItems) { terminalItems.value = terminalItems.value.subList(1, terminalItems.value.size) } diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/TerminalView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/TerminalView.kt index e44a174b53..d89803f8e4 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/TerminalView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/TerminalView.kt @@ -20,11 +20,12 @@ import chat.simplex.common.views.chat.* import chat.simplex.common.views.helpers.* import chat.simplex.common.model.ChatModel import chat.simplex.common.platform.* -import chat.simplex.res.MR -import dev.icerock.moko.resources.compose.stringResource +import kotlinx.coroutines.flow.collect +import kotlinx.coroutines.flow.filter +import kotlinx.coroutines.launch @Composable -fun TerminalView(chatModel: ChatModel, close: () -> Unit) { +fun TerminalView(floating: Boolean = false, close: () -> Unit) { val composeState = remember { mutableStateOf(ComposeState(useLinkPreviews = false)) } val close = { close() @@ -37,6 +38,7 @@ fun TerminalView(chatModel: ChatModel, close: () -> Unit) { }) TerminalLayout( composeState, + floating, sendCommand = { sendCommand(chatModel, composeState) }, close ) @@ -65,6 +67,7 @@ private fun sendCommand(chatModel: ChatModel, composeState: MutableState, + floating: Boolean, sendCommand: () -> Unit, close: () -> Unit ) { @@ -118,19 +121,40 @@ fun TerminalLayout( color = MaterialTheme.colors.background, contentColor = LocalContentColor.current ) { - TerminalLog() + TerminalLog(floating) } } } } @Composable -fun TerminalLog() { +fun TerminalLog(floating: Boolean) { val reversedTerminalItems by remember { derivedStateOf { chatModel.terminalItems.value.asReversed() } } val clipboard = LocalClipboardManager.current - LazyColumnWithScrollBar(reverseLayout = true) { + val listState = LocalAppBarHandler.current?.listState ?: rememberLazyListState() + LaunchedEffect(Unit) { + var autoScrollToBottom = true + launch { + snapshotFlow { listState.layoutInfo.totalItemsCount } + .filter { autoScrollToBottom } + .collect { + try { + listState.scrollToItem(0) + } catch (e: Exception) { + Log.e(TAG, e.stackTraceToString()) + } + } + } + launch { + snapshotFlow { listState.firstVisibleItemIndex } + .collect { + autoScrollToBottom = listState.firstVisibleItemIndex == 0 + } + } + } + LazyColumnWithScrollBar(reverseLayout = true, state = listState) { items(reversedTerminalItems, key = { item -> item.id to item.createdAtNanos }) { item -> val rhId = item.remoteHostId val rhIdStr = if (rhId == null) "" else "$rhId " @@ -142,7 +166,12 @@ fun TerminalLog() { modifier = Modifier .fillMaxWidth() .clickable { - ModalManager.start.showModal(endButtons = { ShareButton { clipboard.shareText(item.details) } }) { + val modalPlace = if (floating) { + ModalManager.floatingTerminal + } else { + ModalManager.start + } + modalPlace.showModal(endButtons = { ShareButton { clipboard.shareText(item.details) } }) { SelectionContainer(modifier = Modifier.verticalScroll(rememberScrollState())) { val details = item.details .let { @@ -156,6 +185,16 @@ fun TerminalLog() { ) } } + DisposableEffect(Unit) { + val terminals = chatModel.terminalsVisible.toMutableSet() + terminals += floating + chatModel.terminalsVisible = terminals + onDispose { + val terminals = chatModel.terminalsVisible.toMutableSet() + terminals -= floating + chatModel.terminalsVisible = terminals + } + } } @Preview/*( @@ -169,6 +208,7 @@ fun PreviewTerminalLayout() { TerminalLayout( composeState = remember { mutableStateOf(ComposeState(useLinkPreviews = false)) }, sendCommand = {}, + floating = false, close = {} ) } diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/ModalView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/ModalView.kt index 8da73ab3ca..4c35e72701 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/ModalView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/ModalView.kt @@ -2,10 +2,8 @@ package chat.simplex.common.views.helpers import androidx.compose.animation.* import androidx.compose.animation.core.* -import androidx.compose.foundation.ScrollState import androidx.compose.foundation.background import androidx.compose.foundation.layout.* -import androidx.compose.foundation.lazy.LazyListState import androidx.compose.material.* import androidx.compose.runtime.* import androidx.compose.ui.Modifier @@ -209,11 +207,14 @@ class ModalManager(private val placement: ModalPlacement? = null) { val end = if (appPlatform.isAndroid) shared else ModalManager(ModalPlacement.END) val fullscreen = if (appPlatform.isAndroid) shared else ModalManager(ModalPlacement.FULLSCREEN) + val floatingTerminal = if (appPlatform.isAndroid) shared else ModalManager(ModalPlacement.START) + fun closeAllModalsEverywhere() { start.closeModals() center.closeModals() end.closeModals() fullscreen.closeModals() + floatingTerminal.closeModals() } @OptIn(ExperimentalAnimationApi::class) diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/DeveloperView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/DeveloperView.kt index 9dfdf23085..2123d98f41 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/DeveloperView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/DeveloperView.kt @@ -35,7 +35,7 @@ fun DeveloperView( val unchangedHints = mutableStateOf(unchangedHintPreferences()) SectionView { InstallTerminalAppItem(uriHandler) - ChatConsoleItem { withAuth(generalGetString(MR.strings.auth_open_chat_console), generalGetString(MR.strings.auth_log_in_using_credential), showCustomModal { it, close -> TerminalView(it, close) }) } + ChatConsoleItem { withAuth(generalGetString(MR.strings.auth_open_chat_console), generalGetString(MR.strings.auth_log_in_using_credential), showCustomModal { it, close -> TerminalView(false, close) }) } ResetHintsItem(unchangedHints) SettingsPreferenceItem(painterResource(MR.images.ic_code), stringResource(MR.strings.show_developer_options), developerTools) SectionTextFooter( diff --git a/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/DesktopApp.kt b/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/DesktopApp.kt index fc0e97b417..1fb739946c 100644 --- a/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/DesktopApp.kt +++ b/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/DesktopApp.kt @@ -199,7 +199,13 @@ private fun ApplicationScope.AppWindow(closedByError: MutableState) { 768.dp) Window(state = cWindowState, onCloseRequest = { hiddenUntilRestart = true }, title = stringResource(MR.strings.chat_console)) { SimpleXTheme { - TerminalView(ChatModel) { hiddenUntilRestart = true } + TerminalView(true) { hiddenUntilRestart = true } + ModalManager.floatingTerminal.showInView() + DisposableEffect(Unit) { + onDispose { + ModalManager.floatingTerminal.closeModals() + } + } } } }