From 3d51a48da2e78c440d06ad48eb3e9a07acd00e2b Mon Sep 17 00:00:00 2001 From: James Rich <2199651+jamesarich@users.noreply.github.com> Date: Fri, 10 Apr 2026 11:02:55 -0500 Subject: [PATCH] feat(messaging): add IME Send action to message input (#5047) --- .../meshtastic/feature/messaging/Message.kt | 5 +- .../component/MessageScreenComponents.kt | 98 ------------------- 2 files changed, 4 insertions(+), 99 deletions(-) diff --git a/feature/messaging/src/commonMain/kotlin/org/meshtastic/feature/messaging/Message.kt b/feature/messaging/src/commonMain/kotlin/org/meshtastic/feature/messaging/Message.kt index d598f056b..8d9236a8a 100644 --- a/feature/messaging/src/commonMain/kotlin/org/meshtastic/feature/messaging/Message.kt +++ b/feature/messaging/src/commonMain/kotlin/org/meshtastic/feature/messaging/Message.kt @@ -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( diff --git a/feature/messaging/src/commonMain/kotlin/org/meshtastic/feature/messaging/component/MessageScreenComponents.kt b/feature/messaging/src/commonMain/kotlin/org/meshtastic/feature/messaging/component/MessageScreenComponents.kt index dc502ef4f..6416337df 100644 --- a/feature/messaging/src/commonMain/kotlin/org/meshtastic/feature/messaging/component/MessageScreenComponents.kt +++ b/feature/messaging/src/commonMain/kotlin/org/meshtastic/feature/messaging/component/MessageScreenComponents.kt @@ -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