feat(messaging): add IME Send action to message input (#5047)

This commit is contained in:
James Rich 2026-04-10 11:02:55 -05:00 committed by GitHub
parent 6f5fa49b94
commit 3d51a48da2
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 4 additions and 99 deletions

View file

@ -54,6 +54,7 @@ import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalClipboard
import androidx.compose.ui.platform.LocalFocusManager
import androidx.compose.ui.text.input.ImeAction
import androidx.compose.ui.text.input.KeyboardCapitalization
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.tooling.preview.PreviewLightDark
@ -460,7 +461,9 @@ private fun MessageInput(
shape = RoundedCornerShape(ROUNDED_CORNER_PERCENT.toFloat()),
isError = isOverLimit,
placeholder = { Text(stringResource(Res.string.type_a_message)) },
keyboardOptions = KeyboardOptions(capitalization = KeyboardCapitalization.Sentences),
keyboardOptions =
KeyboardOptions(capitalization = KeyboardCapitalization.Sentences, imeAction = ImeAction.Send),
onKeyboardAction = { if (canSend) onSendMessage() },
supportingText = {
if (isEnabled) { // Only show supporting text if input is enabled
Text(

View file

@ -68,7 +68,6 @@ import org.meshtastic.core.resources.Res
import org.meshtastic.core.resources.alert_bell_text
import org.meshtastic.core.resources.cancel_reply
import org.meshtastic.core.resources.clear_selection
import org.meshtastic.core.resources.conversations
import org.meshtastic.core.resources.copy
import org.meshtastic.core.resources.delete
import org.meshtastic.core.resources.delete_messages
@ -77,7 +76,6 @@ import org.meshtastic.core.resources.filter_disable_for_contact
import org.meshtastic.core.resources.filter_enable_for_contact
import org.meshtastic.core.resources.filter_hide_count
import org.meshtastic.core.resources.filter_show_count
import org.meshtastic.core.resources.message_input_label
import org.meshtastic.core.resources.navigate_back
import org.meshtastic.core.resources.new_messages_below
import org.meshtastic.core.resources.overflow_menu
@ -88,10 +86,7 @@ import org.meshtastic.core.resources.reply
import org.meshtastic.core.resources.replying_to
import org.meshtastic.core.resources.scroll_to_bottom
import org.meshtastic.core.resources.select_all
import org.meshtastic.core.resources.send
import org.meshtastic.core.resources.type_a_message
import org.meshtastic.core.resources.unknown
import org.meshtastic.core.ui.component.EmptyDetailPlaceholder
import org.meshtastic.core.ui.component.MeshtasticTextDialog
import org.meshtastic.core.ui.component.NodeKeyStatusIcon
import org.meshtastic.core.ui.component.SecurityIcon
@ -99,7 +94,6 @@ import org.meshtastic.core.ui.icon.ArrowBack
import org.meshtastic.core.ui.icon.ArrowDownward
import org.meshtastic.core.ui.icon.ChatBubbleOutline
import org.meshtastic.core.ui.icon.Close
import org.meshtastic.core.ui.icon.Conversations
import org.meshtastic.core.ui.icon.Copy
import org.meshtastic.core.ui.icon.Delete
import org.meshtastic.core.ui.icon.FilterList
@ -109,7 +103,6 @@ import org.meshtastic.core.ui.icon.More
import org.meshtastic.core.ui.icon.Muted
import org.meshtastic.core.ui.icon.Reply
import org.meshtastic.core.ui.icon.SelectAll
import org.meshtastic.core.ui.icon.Send
import org.meshtastic.core.ui.icon.Unmuted
import org.meshtastic.core.ui.icon.Visibility
import org.meshtastic.core.ui.icon.VisibilityOff
@ -600,99 +593,8 @@ fun MessageStatusDialog(
// endregion
// region ── EmptyConversationsPlaceholder ──
@Composable
fun EmptyConversationsPlaceholder(modifier: Modifier = Modifier) {
EmptyDetailPlaceholder(
icon = MeshtasticIcons.Conversations,
title = stringResource(Res.string.conversations),
modifier = modifier,
)
}
// endregion
// region ── MessageInput ──
/**
* Shared message input field with send button, byte counter, and homoglyph encoding support.
*
* @param messageText The current message text.
* @param onMessageChange Callback when the text changes.
* @param onSendMessage Callback when the send button is pressed.
* @param isEnabled Whether the input field should be enabled.
* @param isHomoglyphEncodingEnabled Whether to optimize text using homoglyph encoding.
* @param maxByteSize The maximum allowed size of the message in bytes.
*/
@Composable
fun MessageInput(
messageText: String,
onMessageChange: (String) -> Unit,
onSendMessage: () -> Unit,
isEnabled: Boolean,
modifier: Modifier = Modifier,
isHomoglyphEncodingEnabled: Boolean = false,
maxByteSize: Int = MESSAGE_CHARACTER_LIMIT_BYTES,
) {
val currentText =
if (isHomoglyphEncodingEnabled) {
org.meshtastic.core.common.util.HomoglyphCharacterStringTransformer.optimizeUtf8StringWithHomoglyphs(
messageText,
)
} else {
messageText
}
val currentByteLength = remember(currentText) { currentText.encodeToByteArray().size }
val isOverLimit = currentByteLength > maxByteSize
val canSend = !isOverLimit && currentText.isNotEmpty() && isEnabled
androidx.compose.material3.OutlinedTextField(
modifier = modifier.fillMaxWidth().padding(horizontal = 8.dp, vertical = 4.dp),
value = messageText,
onValueChange = onMessageChange,
maxLines = MAX_INPUT_LINES,
label = { Text(stringResource(Res.string.message_input_label)) },
enabled = isEnabled,
shape = RoundedCornerShape(ROUNDED_CORNER_PERCENT.toFloat()),
isError = isOverLimit,
placeholder = { Text(stringResource(Res.string.type_a_message)) },
supportingText = {
if (isEnabled) {
Text(
text = "$currentByteLength/$maxByteSize",
style = MaterialTheme.typography.bodySmall,
color =
if (isOverLimit) {
MaterialTheme.colorScheme.error
} else {
MaterialTheme.colorScheme.onSurfaceVariant
},
modifier = Modifier.fillMaxWidth(),
textAlign = androidx.compose.ui.text.style.TextAlign.End,
)
}
},
trailingIcon = {
IconButton(onClick = { if (canSend) onSendMessage() }, enabled = canSend) {
Icon(imageVector = MeshtasticIcons.Send, contentDescription = stringResource(Res.string.send))
}
},
)
}
// endregion
// region ── Utility Functions ──
/** Maximum number of lines for the message input field. */
private const val MAX_INPUT_LINES = 3
/** Corner radius percentage for the message input field. */
private const val ROUNDED_CORNER_PERCENT = 100
/** The maximum number of characters to display in the reply snippet. */
internal const val SNIPPET_CHARACTER_LIMIT = 50