Refactor the chat page

This commit is contained in:
Minecon724 2025-06-18 20:22:01 +02:00
commit 3a821889a9
Signed by: Minecon724
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.compose.setContent
import androidx.activity.enableEdgeToEdge
import androidx.activity.viewModels
import androidx.compose.animation.animateColorAsState
import androidx.compose.animation.core.animateDpAsState
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
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.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.Send
import androidx.compose.material3.CenterAlignedTopAppBar
import androidx.compose.material3.ElevatedCard
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.IconButtonDefaults
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.material3.TextField
import androidx.compose.material3.TextFieldDefaults
import androidx.compose.material3.TopAppBar
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
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.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.sp
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.lifecycle.viewmodel.compose.viewModel
import dagger.hilt.android.AndroidEntryPoint
import eu.m724.chatapp.activity.chat.compose.AnimatedChangingText
import eu.m724.chatapp.activity.chat.compose.LanguageModelMistakeWarning
import eu.m724.chatapp.activity.ui.theme.ChatAppTheme
@AndroidEntryPoint
class ChatActivity : ComponentActivity() {
private val viewModel: ChatActivityViewModel by viewModels()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
enableEdgeToEdge()
setContent {
Content()
Content(viewModel)
}
}
}
@Composable
fun Content(
viewModel: ChatActivityViewModel = viewModel()
viewModel: ChatActivityViewModel // = viewModel() doesn't work
) {
val uiState by viewModel.uiState.collectAsStateWithLifecycle()
val localSoftwareKeyboardController = LocalSoftwareKeyboardController.current
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 {
Scaffold(
modifier = Modifier.fillMaxSize(),
modifier = Modifier.fillMaxSize().imePadding(),
topBar = {
ChatTopAppBar(uiState.chatTitle ?: "Start a new conversation")
}
) { innerPadding ->
Column(
modifier = Modifier.fillMaxSize().padding(innerPadding).imePadding(),
modifier = Modifier.fillMaxSize().padding(innerPadding),
horizontalAlignment = Alignment.CenterHorizontally
) {
val lazyListState = rememberLazyListState()
LazyColumn(
modifier = Modifier.fillMaxSize().weight(1f),
state = lazyListState
@ -104,33 +120,34 @@ fun Content(
isComposing = !uiState.requestInProgress,
composerValue = composerValue,
onComposerValueChange = { composerValue = it },
composerFocusRequester = composerFocusRequester,
responseValue = uiState.currentMessageResponse // TODO animate this
)
}
}
ComposerToolBar(
modifier = Modifier.width(500.dp).padding(horizontal = 10.dp),
canSend = composerValue.isNotBlank() && !uiState.requestInProgress,
onSend = {
viewModel.sendMessage(composerValue)
}
)
Box(
modifier = Modifier.fillMaxWidth(),
contentAlignment = Alignment.CenterEnd
) {
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(
text = "AI can make mistakes, double-check.",
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)
}
LanguageModelMistakeWarning(
modifier = Modifier.padding(vertical = 10.dp)
)
}
}
}
@ -143,9 +160,16 @@ fun Content(
fun ChatTopAppBar(
title: String
) {
TopAppBar(
CenterAlignedTopAppBar(
title = {
AnimatedChangingText(title)
Box(
contentAlignment = Alignment.Center,
modifier = Modifier.fillMaxWidth(0.8f)
) {
AnimatedChangingText(
text = title,
)
}
}
)
}
@ -156,6 +180,7 @@ fun MessageExchange(
isComposing: Boolean,
composerValue: String,
onComposerValueChange: (String) -> Unit = {},
composerFocusRequester: FocusRequester = FocusRequester(),
responseValue: String = "",
) {
@ -165,7 +190,8 @@ fun MessageExchange(
MessageComposer(
value = composerValue,
onValueChange = onComposerValueChange,
enabled = isComposing
enabled = isComposing,
focusRequester = composerFocusRequester
)
if (!isComposing) {
@ -184,6 +210,7 @@ fun MessageComposer(
value: String,
onValueChange: (String) -> Unit,
enabled: Boolean,
focusRequester: FocusRequester,
modifier: Modifier = Modifier
) {
val textFieldPadding by animateDpAsState(
@ -202,14 +229,6 @@ fun MessageComposer(
}
)
val focusRequester = remember { FocusRequester() }
LaunchedEffect(enabled) {
if (enabled) {
focusRequester.requestFocus()
}
}
TextField(
value = value,
onValueChange = onValueChange,
@ -238,7 +257,8 @@ fun MessageComposer(
fun ComposerToolBar(
modifier: Modifier = Modifier,
canSend: Boolean,
onSend: () -> Unit
onSend: () -> Unit,
onEmptySpaceClick: () -> Unit
) {
val sendButtonColor by animateColorAsState(
targetValue = if (canSend) {
@ -249,7 +269,9 @@ fun ComposerToolBar(
)
ElevatedCard(
modifier = modifier,
modifier = modifier.clickable(
onClick = onEmptySpaceClick
),
shape = RoundedCornerShape(100)
) {
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) {
_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.shrinkHorizontally
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.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.text.style.TextOverflow
@Composable
fun AnimatedChangingText(
text: String,
modifier: Modifier = Modifier
) {
Row(
modifier = modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.Center
) {
AnimatedContent(
targetState = text,
transitionSpec = {
val enter = fadeIn() // + expandHorizontally()
val exit = fadeOut() + shrinkHorizontally(animationSpec = tween(800))
AnimatedContent(
targetState = text,
modifier = modifier,
transitionSpec = {
val enter = fadeIn() // + expandHorizontally()
val exit = fadeOut() + shrinkHorizontally(animationSpec = tween(800))
enter.togetherWith(exit)
},
label = "Animated horizontal text"
) { targetText ->
Text(
text = targetText,
softWrap = false
)
}
enter.togetherWith(exit)
},
label = "Animated horizontal text"
) { targetText ->
Text(
text = targetText,
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]
agp = "8.9.3"
agp = "8.10.1"
kotlin = "2.1.21"
coreKtx = "1.16.0"
junit = "4.13.2"
@ -9,7 +9,7 @@ appcompat = "1.7.1"
material = "1.12.0"
lifecycleRuntimeKtx = "2.9.1"
activityCompose = "1.10.1"
composeBom = "2025.06.00"
composeBom = "2025.06.01"
hilt = "2.56.2"
ksp = "2.1.21-2.0.2"
retrofit = "3.0.0"