properly update states in chat screen

This commit is contained in:
2026-08-17 19:34:50 +03:00
parent 4eeda5e2ee
commit db728f8ab5
3 changed files with 45 additions and 15 deletions
@@ -6,7 +6,7 @@ import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import com.arkivanov.decompose.extensions.compose.subscribeAsState
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import org.jetbrains.compose.resources.stringResource
import ru.shadowsparky.chat.presentation.ChatComponent
import ru.shadowsparky.chat.presentation.ChatContent
@@ -17,7 +17,7 @@ import ru.shadowsparky.vbox.shared.presentation.nav.Route
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun ChatScreen(component: ChatComponent, route: Route, onNavigate: (Route) -> Unit) {
val state by component.state.subscribeAsState()
val state by component.state.collectAsStateWithLifecycle()
MainScreenScaffold(
appBar = {
TopAppBar(title = { Text(stringResource(Res.string.chat_title_app_bar)) })
@@ -1,8 +1,6 @@
package ru.shadowsparky.chat.presentation
import com.arkivanov.decompose.ComponentContext
import com.arkivanov.decompose.value.MutableValue
import com.arkivanov.decompose.value.Value
import com.arkivanov.essenty.lifecycle.coroutines.repeatOnLifecycle
import com.arkivanov.essenty.lifecycle.doOnDestroy
import kotlinx.coroutines.CoroutineExceptionHandler
@@ -11,6 +9,9 @@ import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.cancel
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.launch
import ru.shadowsparky.chat.domain.ChatRepository
import ru.shadowsparky.chat.domain.ChatRoles
@@ -26,7 +27,9 @@ sealed interface ChatState {
data class Data(
val messages: List<Message>,
val isSending: Boolean = false,
val sendError: String? = null
val sendError: String? = null,
val scrollToFirstMessage: Boolean = false,
val clearInput: Boolean = false
) : ChatState
}
@@ -37,6 +40,7 @@ class ChatComponent(
val onLinkClicked: (String) -> Unit,
val refreshFlow: Flow<Unit>
) : ComponentContext by componentContext {
private var needClearInput = false
private val errorHandler = CoroutineExceptionHandler { _, throwable ->
_state.value = ChatState.Error(throwable.message ?: throwable.toString())
}
@@ -45,13 +49,16 @@ class ChatComponent(
lifecycle.doOnDestroy { ctx.cancel() }
}
private val _state = MutableValue<ChatState>(ChatState.Loading)
val state: Value<ChatState> = _state
private val _state = MutableStateFlow<ChatState>(ChatState.Loading)
val state: StateFlow<ChatState> = _state.asStateFlow()
init {
loadInitialMessages()
scope.launch {
repeatOnLifecycle { refreshFlow.collect { refreshList() } }
repeatOnLifecycle {
refreshFlow.collect { refreshList() }
}
}
}
@@ -72,6 +79,7 @@ class ChatComponent(
if (content.isBlank() || currentState.isSending) return
_state.value = currentState.copy(isSending = true, sendError = null)
scope.launch {
needClearInput = true
repository.send(Message(role = ChatRoles.USER, content = content, timestamp = 0L))
}
}
@@ -84,7 +92,9 @@ class ChatComponent(
val freshMessages = repository.query(afterId = latestMessageId)
_state.value = currentState.copy(
messages = freshMessages + currentState.messages,
isSending = false
isSending = false,
scrollToFirstMessage = freshMessages.isNotEmpty(),
clearInput = needClearInput
)
}
}
@@ -93,4 +103,13 @@ class ChatComponent(
val currentState = _state.value as? ChatState.Data ?: return
_state.value = currentState.copy(sendError = null)
}
fun resetState(needScroll: Boolean, needClearInput: Boolean) {
val currentState = _state.value as? ChatState.Data ?: return
this.needClearInput = needClearInput
_state.value = currentState.copy(
scrollToFirstMessage = needScroll,
clearInput = needClearInput
)
}
}
@@ -45,6 +45,7 @@ import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.runtime.snapshotFlow
import androidx.compose.runtime.withFrameNanos
@@ -124,7 +125,10 @@ fun ChatContent(component: ChatComponent, paddingValues: PaddingValues, state: C
onParseMessage = { component.parseMessage(it) },
onClickLink = { component.onLinkClicked(it) },
onSendMessage = { component.sendMessage(it) },
onDismissError = { component.clearSendError() }
onDismissError = { component.clearSendError() },
resetState = { needScroll, needClearInput ->
component.resetState(needScroll, needClearInput)
}
)
}
}
@@ -139,17 +143,24 @@ private fun ChatContent(
onParseMessage: (String) -> List<TextToken>,
onClickLink: (String) -> Unit,
onSendMessage: (String) -> Unit,
onDismissError: () -> Unit
onDismissError: () -> Unit,
resetState: (needScroll: Boolean, needClearInput: Boolean) -> Unit
) {
val listState = rememberLazyListState()
var textInput by remember { mutableStateOf("") }
var textInput by rememberSaveable { mutableStateOf("") }
val textFieldFocusRequester = remember { FocusRequester() }
LaunchedEffect(state.messages.size) {
if (state.messages.isNotEmpty()) {
LaunchedEffect(state.scrollToFirstMessage) {
if (state.scrollToFirstMessage) {
listState.animateScrollToItem(0)
resetState(false, state.clearInput)
}
}
LaunchedEffect(state.clearInput) {
if (state.clearInput) {
textInput = ""
resetState(state.scrollToFirstMessage, false)
}
}
LaunchedEffect(state.messages) { textInput = "" }
Column(modifier = Modifier.fillMaxSize()) {
val scope = rememberCoroutineScope()
val focusRequesters = remember { mutableMapOf<Any, FocusRequester>() }