Message input tweaks

This commit is contained in:
Phil Oliver 2025-09-11 16:16:19 -04:00
parent d634194d31
commit bc6951e7a6
2 changed files with 53 additions and 45 deletions

View file

@ -39,7 +39,6 @@ import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.foundation.text.input.TextFieldLineLimits
import androidx.compose.foundation.text.input.TextFieldState import androidx.compose.foundation.text.input.TextFieldState
import androidx.compose.foundation.text.input.clearText import androidx.compose.foundation.text.input.clearText
import androidx.compose.foundation.text.input.rememberTextFieldState import androidx.compose.foundation.text.input.rememberTextFieldState
@ -47,7 +46,7 @@ import androidx.compose.foundation.text.input.setTextAndPlaceCursorAtEnd
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.ArrowBack import androidx.compose.material.icons.automirrored.filled.ArrowBack
import androidx.compose.material.icons.automirrored.filled.Reply import androidx.compose.material.icons.automirrored.filled.Reply
import androidx.compose.material.icons.automirrored.filled.Send import androidx.compose.material.icons.automirrored.rounded.Send
import androidx.compose.material.icons.filled.ArrowDownward import androidx.compose.material.icons.filled.ArrowDownward
import androidx.compose.material.icons.filled.ChatBubbleOutline import androidx.compose.material.icons.filled.ChatBubbleOutline
import androidx.compose.material.icons.filled.Close import androidx.compose.material.icons.filled.Close
@ -59,9 +58,12 @@ import androidx.compose.material.icons.filled.SpeakerNotes
import androidx.compose.material.icons.filled.SpeakerNotesOff import androidx.compose.material.icons.filled.SpeakerNotesOff
import androidx.compose.material3.AlertDialog import androidx.compose.material3.AlertDialog
import androidx.compose.material3.Button import androidx.compose.material3.Button
import androidx.compose.material3.ButtonDefaults
import androidx.compose.material3.DropdownMenu import androidx.compose.material3.DropdownMenu
import androidx.compose.material3.DropdownMenuItem import androidx.compose.material3.DropdownMenuItem
import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
import androidx.compose.material3.FilledIconButton
import androidx.compose.material3.FloatingActionButton import androidx.compose.material3.FloatingActionButton
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton import androidx.compose.material3.IconButton
@ -739,6 +741,7 @@ private fun QuickChatRow(
* @param maxByteSize The maximum allowed size of the message in bytes. * @param maxByteSize The maximum allowed size of the message in bytes.
* @param onSendMessage Callback invoked when the send button is pressed or send IME action is triggered. * @param onSendMessage Callback invoked when the send button is pressed or send IME action is triggered.
*/ */
@OptIn(ExperimentalMaterial3ExpressiveApi::class)
@Suppress("LongMethod") // Due to multiple parts of the OutlinedTextField @Suppress("LongMethod") // Due to multiple parts of the OutlinedTextField
@Composable @Composable
private fun MessageInput( private fun MessageInput(
@ -758,51 +761,57 @@ private fun MessageInput(
val isOverLimit = currentByteLength > maxByteSize val isOverLimit = currentByteLength > maxByteSize
val canSend = !isOverLimit && currentText.isNotEmpty() && isEnabled val canSend = !isOverLimit && currentText.isNotEmpty() && isEnabled
OutlinedTextField( Column(modifier = modifier.padding(8.dp)) {
modifier = modifier.fillMaxWidth().padding(horizontal = 8.dp, vertical = 4.dp), Row(horizontalArrangement = Arrangement.spacedBy(8.dp), verticalAlignment = Alignment.Bottom) {
state = textFieldState, OutlinedTextField(
lineLimits = TextFieldLineLimits.SingleLine, modifier = Modifier.fillMaxWidth().weight(1f),
label = { Text(stringResource(R.string.message_input_label)) }, state = textFieldState,
enabled = isEnabled, enabled = isEnabled,
shape = RoundedCornerShape(ROUNDED_CORNER_PERCENT.toFloat()), shape = RoundedCornerShape(ROUNDED_CORNER_PERCENT.toFloat()),
isError = isOverLimit, isError = isOverLimit,
placeholder = { Text(stringResource(R.string.type_a_message)) }, placeholder = { Text(stringResource(R.string.message_input_label)) },
keyboardOptions = keyboardOptions =
KeyboardOptions(capitalization = KeyboardCapitalization.Sentences, imeAction = ImeAction.Send), KeyboardOptions(capitalization = KeyboardCapitalization.Sentences, imeAction = ImeAction.Send),
onKeyboardAction = { onKeyboardAction = {
if (canSend) { if (canSend) {
onSendMessage() onSendMessage()
} }
}, },
supportingText = { // Direct byte limiting via inputTransformation in TextFieldState is complex.
if (isEnabled) { // Only show supporting text if input is enabled // The current approach (show error, disable send) is generally preferred for UX.
Text( // If strict real-time byte trimming is required, it needs careful handling of
text = "$currentByteLength/$maxByteSize", // cursor position and multi-byte characters, likely outside simple inputTransformation.
style = MaterialTheme.typography.bodySmall, )
color =
if (isOverLimit) { FilledIconButton(
MaterialTheme.colorScheme.error onClick = { if (canSend) onSendMessage() },
} else { enabled = canSend,
MaterialTheme.colorScheme.onSurfaceVariant modifier = Modifier.size(ButtonDefaults.MediumContainerHeight),
}, ) {
modifier = Modifier.fillMaxWidth(),
textAlign = TextAlign.End,
)
}
},
// Direct byte limiting via inputTransformation in TextFieldState is complex.
// The current approach (show error, disable send) is generally preferred for UX.
// If strict real-time byte trimming is required, it needs careful handling of
// cursor position and multi-byte characters, likely outside simple inputTransformation.
trailingIcon = {
IconButton(onClick = { if (canSend) onSendMessage() }, enabled = canSend) {
Icon( Icon(
imageVector = Icons.AutoMirrored.Default.Send, imageVector = Icons.AutoMirrored.Rounded.Send,
modifier = Modifier.size(ButtonDefaults.MediumIconSize),
contentDescription = stringResource(id = R.string.send), contentDescription = stringResource(id = R.string.send),
) )
} }
}, }
)
if (isEnabled) { // Only show supporting text if input is enabled
@Suppress("MagicNumber")
Text(
text = "$currentByteLength/$maxByteSize",
style = MaterialTheme.typography.bodySmall,
color =
if (isOverLimit) {
MaterialTheme.colorScheme.error
} else {
MaterialTheme.colorScheme.onSurfaceVariant
},
modifier = Modifier.fillMaxWidth().padding(top = 2.dp, end = 84.dp),
textAlign = TextAlign.End,
)
}
}
} }
@PreviewLightDark @PreviewLightDark
@ -810,7 +819,7 @@ private fun MessageInput(
private fun MessageInputPreview() { private fun MessageInputPreview() {
AppTheme { AppTheme {
Surface { Surface {
Column(modifier = Modifier.padding(8.dp)) { Column {
MessageInput(isEnabled = true, textFieldState = rememberTextFieldState("Hello"), onSendMessage = {}) MessageInput(isEnabled = true, textFieldState = rememberTextFieldState("Hello"), onSendMessage = {})
Spacer(Modifier.size(16.dp)) Spacer(Modifier.size(16.dp))
MessageInput(isEnabled = false, textFieldState = rememberTextFieldState("Disabled"), onSendMessage = {}) MessageInput(isEnabled = false, textFieldState = rememberTextFieldState("Disabled"), onSendMessage = {})

View file

@ -718,7 +718,6 @@
<string name="delete_messages_title">Delete Messages?</string> <string name="delete_messages_title">Delete Messages?</string>
<string name="clear_selection">Clear selection</string> <string name="clear_selection">Clear selection</string>
<string name="message_input_label">Message</string> <string name="message_input_label">Message</string>
<string name="type_a_message">Type a message</string>
<string name="pax_metrics_log">PAX Metrics Log</string> <string name="pax_metrics_log">PAX Metrics Log</string>
<string name="pax">PAX</string> <string name="pax">PAX</string>
<string name="no_pax_metrics_logs">No PAX metrics logs available.</string> <string name="no_pax_metrics_logs">No PAX metrics logs available.</string>