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