Refactor the chat page

This commit is contained in:
Minecon724 2025-06-18 20:22:01 +02:00
commit 3a821889a9
Signed by untrusted user who does not match committer: m724
GPG key ID: A02E6E67AB961189
5 changed files with 106 additions and 75 deletions

View file

@ -4,9 +4,12 @@ import android.os.Bundle
import androidx.activity.ComponentActivity import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge import androidx.activity.enableEdgeToEdge
import androidx.activity.viewModels
import androidx.compose.animation.animateColorAsState import androidx.compose.animation.animateColorAsState
import androidx.compose.animation.core.animateDpAsState import androidx.compose.animation.core.animateDpAsState
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
@ -21,17 +24,16 @@ import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.Send import androidx.compose.material.icons.automirrored.filled.Send
import androidx.compose.material3.CenterAlignedTopAppBar
import androidx.compose.material3.ElevatedCard import androidx.compose.material3.ElevatedCard
import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton import androidx.compose.material3.IconButton
import androidx.compose.material3.IconButtonDefaults import androidx.compose.material3.IconButtonDefaults
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Scaffold import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.material3.TextField import androidx.compose.material3.TextField
import androidx.compose.material3.TextFieldDefaults import androidx.compose.material3.TextFieldDefaults
import androidx.compose.material3.TopAppBar
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
@ -43,48 +45,62 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.focus.FocusRequester import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.focus.focusRequester import androidx.compose.ui.focus.focusRequester
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.platform.LocalSoftwareKeyboardController
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.lifecycle.viewmodel.compose.viewModel
import dagger.hilt.android.AndroidEntryPoint import dagger.hilt.android.AndroidEntryPoint
import eu.m724.chatapp.activity.chat.compose.AnimatedChangingText import eu.m724.chatapp.activity.chat.compose.AnimatedChangingText
import eu.m724.chatapp.activity.chat.compose.LanguageModelMistakeWarning
import eu.m724.chatapp.activity.ui.theme.ChatAppTheme import eu.m724.chatapp.activity.ui.theme.ChatAppTheme
@AndroidEntryPoint @AndroidEntryPoint
class ChatActivity : ComponentActivity() { class ChatActivity : ComponentActivity() {
private val viewModel: ChatActivityViewModel by viewModels()
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
enableEdgeToEdge() enableEdgeToEdge()
setContent { setContent {
Content() Content(viewModel)
} }
} }
} }
@Composable @Composable
fun Content( fun Content(
viewModel: ChatActivityViewModel = viewModel() viewModel: ChatActivityViewModel // = viewModel() doesn't work
) { ) {
val uiState by viewModel.uiState.collectAsStateWithLifecycle() val uiState by viewModel.uiState.collectAsStateWithLifecycle()
val localSoftwareKeyboardController = LocalSoftwareKeyboardController.current
var composerValue by remember { mutableStateOf("") } var composerValue by remember { mutableStateOf("") }
val composerFocusRequester = remember { FocusRequester() }
val lazyListState = rememberLazyListState()
LaunchedEffect(uiState.requestInProgress) { // TODO probably not the best way
if (!uiState.requestInProgress) {
composerValue = ""
composerFocusRequester.requestFocus() // TODO maybe make toggleable? or smart?
} else {
if (!uiState.messageHistory.isEmpty()) {
lazyListState.animateScrollToItem(uiState.messageHistory.size)
}
}
}
ChatAppTheme { ChatAppTheme {
Scaffold( Scaffold(
modifier = Modifier.fillMaxSize(), modifier = Modifier.fillMaxSize().imePadding(),
topBar = { topBar = {
ChatTopAppBar(uiState.chatTitle ?: "Start a new conversation") ChatTopAppBar(uiState.chatTitle ?: "Start a new conversation")
} }
) { innerPadding -> ) { innerPadding ->
Column( Column(
modifier = Modifier.fillMaxSize().padding(innerPadding).imePadding(), modifier = Modifier.fillMaxSize().padding(innerPadding),
horizontalAlignment = Alignment.CenterHorizontally horizontalAlignment = Alignment.CenterHorizontally
) { ) {
val lazyListState = rememberLazyListState()
LazyColumn( LazyColumn(
modifier = Modifier.fillMaxSize().weight(1f), modifier = Modifier.fillMaxSize().weight(1f),
state = lazyListState state = lazyListState
@ -104,33 +120,34 @@ fun Content(
isComposing = !uiState.requestInProgress, isComposing = !uiState.requestInProgress,
composerValue = composerValue, composerValue = composerValue,
onComposerValueChange = { composerValue = it }, onComposerValueChange = { composerValue = it },
composerFocusRequester = composerFocusRequester,
responseValue = uiState.currentMessageResponse // TODO animate this responseValue = uiState.currentMessageResponse // TODO animate this
) )
} }
} }
ComposerToolBar( Box(
modifier = Modifier.width(500.dp).padding(horizontal = 10.dp), modifier = Modifier.fillMaxWidth(),
canSend = composerValue.isNotBlank() && !uiState.requestInProgress, contentAlignment = Alignment.CenterEnd
onSend = { ) {
viewModel.sendMessage(composerValue) Column(
} horizontalAlignment = Alignment.CenterHorizontally
) ) {
ComposerToolBar(
modifier = Modifier.width(500.dp).padding(horizontal = 10.dp),
canSend = composerValue.isNotBlank() && !uiState.requestInProgress,
onSend = {
viewModel.sendMessage(composerValue)
},
onEmptySpaceClick = {
composerFocusRequester.requestFocus()
localSoftwareKeyboardController?.show()
}
)
Text( LanguageModelMistakeWarning(
text = "AI can make mistakes, double-check.", modifier = Modifier.padding(vertical = 10.dp)
modifier = Modifier.padding(vertical = 10.dp), )
color = MaterialTheme.colorScheme.secondary.copy(alpha = 0.6f),
fontSize = 12.sp,
lineHeight = 14.sp
)
LaunchedEffect(uiState.requestInProgress) { // TODO probably not the best way
if (!uiState.requestInProgress) {
composerValue = ""
if (!uiState.messageHistory.isEmpty()) {
lazyListState.animateScrollToItem(uiState.messageHistory.size - 1)
}
} }
} }
} }
@ -143,9 +160,16 @@ fun Content(
fun ChatTopAppBar( fun ChatTopAppBar(
title: String title: String
) { ) {
TopAppBar( CenterAlignedTopAppBar(
title = { title = {
AnimatedChangingText(title) Box(
contentAlignment = Alignment.Center,
modifier = Modifier.fillMaxWidth(0.8f)
) {
AnimatedChangingText(
text = title,
)
}
} }
) )
} }
@ -156,6 +180,7 @@ fun MessageExchange(
isComposing: Boolean, isComposing: Boolean,
composerValue: String, composerValue: String,
onComposerValueChange: (String) -> Unit = {}, onComposerValueChange: (String) -> Unit = {},
composerFocusRequester: FocusRequester = FocusRequester(),
responseValue: String = "", responseValue: String = "",
) { ) {
@ -165,7 +190,8 @@ fun MessageExchange(
MessageComposer( MessageComposer(
value = composerValue, value = composerValue,
onValueChange = onComposerValueChange, onValueChange = onComposerValueChange,
enabled = isComposing enabled = isComposing,
focusRequester = composerFocusRequester
) )
if (!isComposing) { if (!isComposing) {
@ -184,6 +210,7 @@ fun MessageComposer(
value: String, value: String,
onValueChange: (String) -> Unit, onValueChange: (String) -> Unit,
enabled: Boolean, enabled: Boolean,
focusRequester: FocusRequester,
modifier: Modifier = Modifier modifier: Modifier = Modifier
) { ) {
val textFieldPadding by animateDpAsState( val textFieldPadding by animateDpAsState(
@ -202,14 +229,6 @@ fun MessageComposer(
} }
) )
val focusRequester = remember { FocusRequester() }
LaunchedEffect(enabled) {
if (enabled) {
focusRequester.requestFocus()
}
}
TextField( TextField(
value = value, value = value,
onValueChange = onValueChange, onValueChange = onValueChange,
@ -238,7 +257,8 @@ fun MessageComposer(
fun ComposerToolBar( fun ComposerToolBar(
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
canSend: Boolean, canSend: Boolean,
onSend: () -> Unit onSend: () -> Unit,
onEmptySpaceClick: () -> Unit
) { ) {
val sendButtonColor by animateColorAsState( val sendButtonColor by animateColorAsState(
targetValue = if (canSend) { targetValue = if (canSend) {
@ -249,7 +269,9 @@ fun ComposerToolBar(
) )
ElevatedCard( ElevatedCard(
modifier = modifier, modifier = modifier.clickable(
onClick = onEmptySpaceClick
),
shape = RoundedCornerShape(100) shape = RoundedCornerShape(100)
) { ) {
Row( Row(
@ -278,10 +300,4 @@ fun ComposerToolBar(
} }
} }
} }
}
@Preview(showBackground = true)
@Composable
fun ContentPreview() {
Content()
} }

View file

@ -35,7 +35,7 @@ class ChatActivityViewModel @Inject constructor(
if (_uiState.value.chatTitle == null) { if (_uiState.value.chatTitle == null) {
_uiState.update { _uiState.update {
it.copy(chatTitle = message) // TODO it.copy(chatTitle = "User says \"$message\"") // TODO
} }
} }

View file

@ -6,36 +6,31 @@ import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut import androidx.compose.animation.fadeOut
import androidx.compose.animation.shrinkHorizontally import androidx.compose.animation.shrinkHorizontally
import androidx.compose.animation.togetherWith import androidx.compose.animation.togetherWith
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.text.style.TextOverflow
@Composable @Composable
fun AnimatedChangingText( fun AnimatedChangingText(
text: String, text: String,
modifier: Modifier = Modifier modifier: Modifier = Modifier
) { ) {
Row( AnimatedContent(
modifier = modifier.fillMaxWidth(), targetState = text,
horizontalArrangement = Arrangement.Center modifier = modifier,
) { transitionSpec = {
AnimatedContent( val enter = fadeIn() // + expandHorizontally()
targetState = text, val exit = fadeOut() + shrinkHorizontally(animationSpec = tween(800))
transitionSpec = {
val enter = fadeIn() // + expandHorizontally()
val exit = fadeOut() + shrinkHorizontally(animationSpec = tween(800))
enter.togetherWith(exit) enter.togetherWith(exit)
}, },
label = "Animated horizontal text" label = "Animated horizontal text"
) { targetText -> ) { targetText ->
Text( Text(
text = targetText, text = targetText,
softWrap = false softWrap = false,
) overflow = TextOverflow.Ellipsis
} )
} }
} }

View file

@ -0,0 +1,20 @@
package eu.m724.chatapp.activity.chat.compose
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.sp
@Composable
fun LanguageModelMistakeWarning(
modifier: Modifier = Modifier
) {
Text(
text = "AI can make mistakes, double-check.",
modifier = modifier,
color = MaterialTheme.colorScheme.secondary.copy(alpha = 0.6f),
fontSize = 12.sp,
lineHeight = 14.sp
)
}

View file

@ -1,5 +1,5 @@
[versions] [versions]
agp = "8.9.3" agp = "8.10.1"
kotlin = "2.1.21" kotlin = "2.1.21"
coreKtx = "1.16.0" coreKtx = "1.16.0"
junit = "4.13.2" junit = "4.13.2"
@ -9,7 +9,7 @@ appcompat = "1.7.1"
material = "1.12.0" material = "1.12.0"
lifecycleRuntimeKtx = "2.9.1" lifecycleRuntimeKtx = "2.9.1"
activityCompose = "1.10.1" activityCompose = "1.10.1"
composeBom = "2025.06.00" composeBom = "2025.06.01"
hilt = "2.56.2" hilt = "2.56.2"
ksp = "2.1.21-2.0.2" ksp = "2.1.21-2.0.2"
retrofit = "3.0.0" retrofit = "3.0.0"