android, desktop: thread-safe terminal items and floating terminal improvements (#4992)

* android, desktop: thread-safe terminal items and floating terminal improvements

* optimization
This commit is contained in:
Stanislav Dmitrenko
2024-10-08 14:08:22 +07:00
committed by GitHub
parent 87fd642951
commit 193e17f7af
5 changed files with 71 additions and 11 deletions
@@ -71,6 +71,9 @@ object ChatModel {
val groupMembers = mutableStateListOf<GroupMember>()
val groupMembersIndexes = mutableStateMapOf<Long, Int>()
// 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<Boolean>()
val terminalItems = mutableStateOf<List<TerminalItem>>(listOf())
val userAddress = mutableStateOf<UserContactLinkRec?>(null)
val chatItemTTL = mutableStateOf<ChatItemTTL>(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)
}
@@ -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<Compose
@Composable
fun TerminalLayout(
composeState: MutableState<ComposeState>,
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 = {}
)
}
@@ -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)
@@ -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(
@@ -199,7 +199,13 @@ private fun ApplicationScope.AppWindow(closedByError: MutableState<Boolean>) {
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()
}
}
}
}
}