fix chat focus

This commit is contained in:
2026-08-15 12:55:57 +03:00
parent 4f7d3dc6bc
commit f7a7c645c1
@@ -45,6 +45,7 @@ import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import androidx.compose.runtime.snapshotFlow
import androidx.compose.runtime.withFrameNanos import androidx.compose.runtime.withFrameNanos
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
@@ -68,6 +69,7 @@ import androidx.compose.ui.text.style.TextDecoration
import androidx.compose.ui.text.withLink import androidx.compose.ui.text.withLink
import androidx.compose.ui.unit.LayoutDirection import androidx.compose.ui.unit.LayoutDirection
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import org.jetbrains.compose.resources.painterResource import org.jetbrains.compose.resources.painterResource
import org.jetbrains.compose.resources.stringResource import org.jetbrains.compose.resources.stringResource
@@ -83,11 +85,7 @@ import ru.shadowsparky.ui.components.isTv
import ru.shadowsparky.ui.theme.focusBorder import ru.shadowsparky.ui.theme.focusBorder
@Composable @Composable
fun ChatContent( fun ChatContent(component: ChatComponent, paddingValues: PaddingValues, state: ChatState) {
component: ChatComponent,
paddingValues: PaddingValues,
state: ChatState
) {
Box( Box(
modifier = Modifier.fillMaxSize() modifier = Modifier.fillMaxSize()
.padding( .padding(
@@ -144,19 +142,23 @@ private fun ChatContent(
val listState = rememberLazyListState() val listState = rememberLazyListState()
var textInput by remember { mutableStateOf("") } var textInput by remember { mutableStateOf("") }
val textFieldFocusRequester = remember { FocusRequester() } val textFieldFocusRequester = remember { FocusRequester() }
LaunchedEffect(state.messages.size) { LaunchedEffect(state.messages.size) {
if (state.messages.isNotEmpty()) { if (state.messages.isNotEmpty()) {
listState.animateScrollToItem(0) listState.animateScrollToItem(0)
} }
} }
LaunchedEffect(state.messages) { textInput = "" } LaunchedEffect(state.messages) { textInput = "" }
Column(modifier = Modifier.fillMaxSize()) { Column(modifier = Modifier.fillMaxSize()) {
val scope = rememberCoroutineScope()
val focusRequesters = remember { mutableMapOf<Any, FocusRequester>() }
fun requesterFor(message: Message): FocusRequester {
val key = message.id ?: message.hashCode()
return focusRequesters.getOrPut(key) { FocusRequester() }
}
LazyColumn( LazyColumn(
state = listState, state = listState,
modifier = Modifier.weight(1f) modifier = Modifier
.weight(1f)
.fillMaxWidth(), .fillMaxWidth(),
contentPadding = contentPadding, contentPadding = contentPadding,
verticalArrangement = Arrangement.spacedBy(8.dp), verticalArrangement = Arrangement.spacedBy(8.dp),
@@ -164,15 +166,48 @@ private fun ChatContent(
) { ) {
itemsIndexed( itemsIndexed(
items = state.messages, items = state.messages,
key = { _, item -> item.id ?: item.hashCode() } key = { _, item -> item.id ?: item.hashCode() },
) { index, message -> ) { index, message ->
val requester = requesterFor(message)
MessageBubble( MessageBubble(
itemFocusRequester = requester,
message = message, message = message,
onParseMessage = onParseMessage, onParseMessage = onParseMessage,
onClickLink = onClickLink, onClickLink = onClickLink,
onKeyEvent = { onKeyEvent = { event ->
if (index == 0) { when (event.key) {
if (it.key == Key.DirectionDown) textFieldFocusRequester.requestFocus() Key.DirectionUp -> {
val targetIndex = index + 1
if (targetIndex < state.messages.size) {
val target = state.messages[targetIndex]
val targetRequester = requesterFor(target)
scope.launch {
listState.scrollToItem(targetIndex)
snapshotFlow {
listState.layoutInfo.visibleItemsInfo
.any { it.index == targetIndex }
}.first { it }
targetRequester.requestFocus()
}
}
}
Key.DirectionDown -> {
val targetIndex = index - 1
if (targetIndex >= 0) {
val target = state.messages[targetIndex]
val targetRequester = requesterFor(target)
scope.launch {
listState.scrollToItem(targetIndex)
snapshotFlow {
listState.layoutInfo.visibleItemsInfo
.any { it.index == targetIndex }
}.first { it }
targetRequester.requestFocus()
}
} else {
textFieldFocusRequester.requestFocus()
}
}
} }
} }
) )
@@ -223,6 +258,7 @@ private fun ChatContent(
@Composable @Composable
private fun MessageBubble( private fun MessageBubble(
itemFocusRequester: FocusRequester,
message: Message, message: Message,
onParseMessage: (String) -> List<TextToken>, onParseMessage: (String) -> List<TextToken>,
onClickLink: (String) -> Unit, onClickLink: (String) -> Unit,
@@ -237,7 +273,6 @@ private fun MessageBubble(
val annotatedString = remember(tokens) { val annotatedString = remember(tokens) {
buildText(isTv, tokens, primaryColor, focusedColor, onClickLink) buildText(isTv, tokens, primaryColor, focusedColor, onClickLink)
} }
val itemFocusRequester = remember { FocusRequester() }
var dialog by remember { mutableStateOf(false) } var dialog by remember { mutableStateOf(false) }
var boxModifier = Modifier.widthIn(max = 280.dp) var boxModifier = Modifier.widthIn(max = 280.dp)
if (isTv) { if (isTv) {
@@ -359,17 +394,14 @@ private fun ScrollableMessage(
coroutineScope.launch { scrollState.animateScrollBy(scrollStep) } coroutineScope.launch { scrollState.animateScrollBy(scrollStep) }
true true
} }
Key.DirectionUp -> { Key.DirectionUp -> {
coroutineScope.launch { scrollState.animateScrollBy(-scrollStep) } coroutineScope.launch { scrollState.animateScrollBy(-scrollStep) }
true true
} }
Key.Enter, Key.DirectionCenter -> { Key.Enter, Key.DirectionCenter -> {
onEnter() onEnter()
true true
} }
else -> false else -> false
} }
} else { } else {