Refactor the chat page
This commit is contained in:
parent
758442d44b
commit
3a821889a9
5 changed files with 106 additions and 75 deletions
|
@ -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()
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
||||
)
|
||||
}
|
||||
}
|
|
@ -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
|
||||
)
|
||||
}
|
|
@ -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"
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue