From bc6951e7a6ea25b8d60e2ac0e61a7785bfa79577 Mon Sep 17 00:00:00 2001 From: Phil Oliver <3497406+poliver@users.noreply.github.com> Date: Thu, 11 Sep 2025 16:16:19 -0400 Subject: [PATCH] Message input tweaks --- .../com/geeksville/mesh/ui/message/Message.kt | 97 ++++++++++--------- app/src/main/res/values/strings.xml | 1 - 2 files changed, 53 insertions(+), 45 deletions(-) diff --git a/app/src/main/java/com/geeksville/mesh/ui/message/Message.kt b/app/src/main/java/com/geeksville/mesh/ui/message/Message.kt index 622b68dac..f48b991d4 100644 --- a/app/src/main/java/com/geeksville/mesh/ui/message/Message.kt +++ b/app/src/main/java/com/geeksville/mesh/ui/message/Message.kt @@ -39,7 +39,6 @@ import androidx.compose.foundation.lazy.items import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.foundation.shape.RoundedCornerShape 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.clearText 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.automirrored.filled.ArrowBack 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.ChatBubbleOutline 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.material3.AlertDialog import androidx.compose.material3.Button +import androidx.compose.material3.ButtonDefaults import androidx.compose.material3.DropdownMenu import androidx.compose.material3.DropdownMenuItem import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi +import androidx.compose.material3.FilledIconButton import androidx.compose.material3.FloatingActionButton import androidx.compose.material3.Icon import androidx.compose.material3.IconButton @@ -739,6 +741,7 @@ private fun QuickChatRow( * @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. */ +@OptIn(ExperimentalMaterial3ExpressiveApi::class) @Suppress("LongMethod") // Due to multiple parts of the OutlinedTextField @Composable private fun MessageInput( @@ -758,51 +761,57 @@ private fun MessageInput( val isOverLimit = currentByteLength > maxByteSize val canSend = !isOverLimit && currentText.isNotEmpty() && isEnabled - OutlinedTextField( - modifier = modifier.fillMaxWidth().padding(horizontal = 8.dp, vertical = 4.dp), - state = textFieldState, - lineLimits = TextFieldLineLimits.SingleLine, - label = { Text(stringResource(R.string.message_input_label)) }, - enabled = isEnabled, - shape = RoundedCornerShape(ROUNDED_CORNER_PERCENT.toFloat()), - isError = isOverLimit, - placeholder = { Text(stringResource(R.string.type_a_message)) }, - keyboardOptions = - KeyboardOptions(capitalization = KeyboardCapitalization.Sentences, imeAction = ImeAction.Send), - onKeyboardAction = { - if (canSend) { - onSendMessage() - } - }, - supportingText = { - if (isEnabled) { // Only show supporting text if input is enabled - Text( - text = "$currentByteLength/$maxByteSize", - style = MaterialTheme.typography.bodySmall, - color = - if (isOverLimit) { - MaterialTheme.colorScheme.error - } else { - MaterialTheme.colorScheme.onSurfaceVariant - }, - 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) { + Column(modifier = modifier.padding(8.dp)) { + Row(horizontalArrangement = Arrangement.spacedBy(8.dp), verticalAlignment = Alignment.Bottom) { + OutlinedTextField( + modifier = Modifier.fillMaxWidth().weight(1f), + state = textFieldState, + enabled = isEnabled, + shape = RoundedCornerShape(ROUNDED_CORNER_PERCENT.toFloat()), + isError = isOverLimit, + placeholder = { Text(stringResource(R.string.message_input_label)) }, + keyboardOptions = + KeyboardOptions(capitalization = KeyboardCapitalization.Sentences, imeAction = ImeAction.Send), + onKeyboardAction = { + if (canSend) { + onSendMessage() + } + }, + // 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. + ) + + FilledIconButton( + onClick = { if (canSend) onSendMessage() }, + enabled = canSend, + modifier = Modifier.size(ButtonDefaults.MediumContainerHeight), + ) { Icon( - imageVector = Icons.AutoMirrored.Default.Send, + imageVector = Icons.AutoMirrored.Rounded.Send, + modifier = Modifier.size(ButtonDefaults.MediumIconSize), 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 @@ -810,7 +819,7 @@ private fun MessageInput( private fun MessageInputPreview() { AppTheme { Surface { - Column(modifier = Modifier.padding(8.dp)) { + Column { MessageInput(isEnabled = true, textFieldState = rememberTextFieldState("Hello"), onSendMessage = {}) Spacer(Modifier.size(16.dp)) MessageInput(isEnabled = false, textFieldState = rememberTextFieldState("Disabled"), onSendMessage = {}) diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index d849a9106..099016dd5 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -718,7 +718,6 @@ Delete Messages? Clear selection Message - Type a message PAX Metrics Log PAX No PAX metrics logs available.