From 967fcadf77ce7170ccdbd51c00b7959f748bff1a Mon Sep 17 00:00:00 2001 From: ericek111 Date: Fri, 25 Sep 2026 15:12:50 +0000 Subject: [PATCH] Keep chat drafts per conversation and open each at its newest line Switching conversations dropped the message being written, and showed the next one at the scroll position of the last. Co-Authored-By: Claude Opus 5.5 --- .../java/com/ts3client/android/ui/ChatPane.kt | 25 +++++++++++++------ 1 file changed, 18 insertions(+), 7 deletions(-) diff --git a/android/app/src/main/java/com/ts3client/android/ui/ChatPane.kt b/android/app/src/main/java/com/ts3client/android/ui/ChatPane.kt index add201b..54299ce 100644 --- a/android/app/src/main/java/com/ts3client/android/ui/ChatPane.kt +++ b/android/app/src/main/java/com/ts3client/android/ui/ChatPane.kt @@ -27,11 +27,13 @@ import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.key import androidx.compose.runtime.remember import androidx.compose.runtime.rememberUpdatedState +import androidx.compose.runtime.mutableStateMapOf +import androidx.compose.runtime.saveable.Saver import androidx.compose.runtime.saveable.rememberSaveable -import androidx.compose.runtime.setValue +import androidx.compose.runtime.snapshots.SnapshotStateMap import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.input.key.Key @@ -68,17 +70,21 @@ fun ChatPane( ConversationChip(conversation, conversation.key == selected.key, onSelect, onClose) } } - Lines(selected.lines, onLink, Modifier.weight(1f)) - var draft by rememberSaveable(selected.key.toString()) { mutableStateOf("") } + // Each conversation opens at its newest line. + key(selected.key) { Lines(selected.lines, onLink, Modifier.weight(1f)) } + // One unsent message per conversation, kept while another one is on screen. + val drafts = rememberSaveable(saver = DraftsSaver) { mutableStateMapOf() } + val draftKey = selected.key.toString() + val draft = drafts[draftKey].orEmpty() val send = { if (draft.isNotBlank()) { onSend(draft.trim()) - draft = "" + drafts.remove(draftKey) } } Row(Modifier.padding(8.dp), verticalAlignment = Alignment.CenterVertically) { OutlinedTextField( - draft, { draft = it }, + draft, { drafts[draftKey] = it }, Modifier.weight(1f).onPreviewKeyEvent { e -> // A hardware keyboard's Enter sends, as on the desktop; Shift+Enter starts a new line. val enter = e.key == Key.Enter && !e.isShiftPressed @@ -98,6 +104,11 @@ fun ChatPane( } } +private val DraftsSaver = Saver, HashMap>( + save = { HashMap(it) }, + restore = { mutableStateMapOf().apply { putAll(it) } }, +) + @Composable private fun ConversationChip( conversation: Conversation, @@ -120,7 +131,7 @@ private fun ConversationChip( /** The conversation's lines, following new ones while the newest is in view. */ @Composable private fun Lines(lines: List, onLink: (String) -> Unit, modifier: Modifier) { - val listState = rememberLazyListState() + val listState = rememberLazyListState(initialFirstVisibleItemIndex = (lines.size - 1).coerceAtLeast(0)) val linkColor = MaterialTheme.colorScheme.primary val link by rememberUpdatedState(onLink) LaunchedEffect(lines.lastOrNull()?.id) {