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.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(
|
||||||
|
|
@ -279,9 +301,3 @@ fun ComposerToolBar(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Preview(showBackground = true)
|
|
||||||
@Composable
|
|
||||||
fun ContentPreview() {
|
|
||||||
Content()
|
|
||||||
}
|
|
||||||
|
|
@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
}
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -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]
|
[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"
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue