From 3a821889a9e6646ba6220537ee4de2f823500bfb Mon Sep 17 00:00:00 2001 From: Minecon724 Date: Wed, 18 Jun 2025 20:22:01 +0200 Subject: [PATCH] Refactor the chat page --- .../chatapp/activity/chat/ChatActivity.kt | 118 ++++++++++-------- .../activity/chat/ChatActivityViewModel.kt | 2 +- .../chat/compose/AnimatedChangingText.kt | 37 +++--- .../compose/LanguageModelMistakeWarning.kt | 20 +++ gradle/libs.versions.toml | 4 +- 5 files changed, 106 insertions(+), 75 deletions(-) create mode 100644 app/src/main/java/eu/m724/chatapp/activity/chat/compose/LanguageModelMistakeWarning.kt diff --git a/app/src/main/java/eu/m724/chatapp/activity/chat/ChatActivity.kt b/app/src/main/java/eu/m724/chatapp/activity/chat/ChatActivity.kt index a9e02ea..e3aa4c8 100644 --- a/app/src/main/java/eu/m724/chatapp/activity/chat/ChatActivity.kt +++ b/app/src/main/java/eu/m724/chatapp/activity/chat/ChatActivity.kt @@ -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() } \ No newline at end of file diff --git a/app/src/main/java/eu/m724/chatapp/activity/chat/ChatActivityViewModel.kt b/app/src/main/java/eu/m724/chatapp/activity/chat/ChatActivityViewModel.kt index 16f0694..8288219 100644 --- a/app/src/main/java/eu/m724/chatapp/activity/chat/ChatActivityViewModel.kt +++ b/app/src/main/java/eu/m724/chatapp/activity/chat/ChatActivityViewModel.kt @@ -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 } } diff --git a/app/src/main/java/eu/m724/chatapp/activity/chat/compose/AnimatedChangingText.kt b/app/src/main/java/eu/m724/chatapp/activity/chat/compose/AnimatedChangingText.kt index 41d421b..84808da 100644 --- a/app/src/main/java/eu/m724/chatapp/activity/chat/compose/AnimatedChangingText.kt +++ b/app/src/main/java/eu/m724/chatapp/activity/chat/compose/AnimatedChangingText.kt @@ -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 + ) } } \ No newline at end of file diff --git a/app/src/main/java/eu/m724/chatapp/activity/chat/compose/LanguageModelMistakeWarning.kt b/app/src/main/java/eu/m724/chatapp/activity/chat/compose/LanguageModelMistakeWarning.kt new file mode 100644 index 0000000..167540d --- /dev/null +++ b/app/src/main/java/eu/m724/chatapp/activity/chat/compose/LanguageModelMistakeWarning.kt @@ -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 + ) +} \ No newline at end of file diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index e7b4461..24f0691 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -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"