From ea021c8fd552d1567cea0250315ef32987f9e327 Mon Sep 17 00:00:00 2001
From: Phil Oliver <3497406+poliver@users.noreply.github.com>
Date: Mon, 3 Nov 2025 22:14:18 -0500
Subject: [PATCH] Move message actions to dialog
---
.../feature/messaging/MessageList.kt | 4 +-
.../messaging/component/MessageActions.kt | 109 ---------------
.../component/MessageActionsDialog.kt | 126 ++++++++++++++++++
.../messaging/component/MessageItem.kt | 46 +++++--
.../feature/messaging/component/Reaction.kt | 1 -
5 files changed, 162 insertions(+), 124 deletions(-)
delete mode 100644 feature/messaging/src/main/kotlin/org/meshtastic/feature/messaging/component/MessageActions.kt
create mode 100644 feature/messaging/src/main/kotlin/org/meshtastic/feature/messaging/component/MessageActionsDialog.kt
diff --git a/feature/messaging/src/main/kotlin/org/meshtastic/feature/messaging/MessageList.kt b/feature/messaging/src/main/kotlin/org/meshtastic/feature/messaging/MessageList.kt
index dbff72503..d87a794f0 100644
--- a/feature/messaging/src/main/kotlin/org/meshtastic/feature/messaging/MessageList.kt
+++ b/feature/messaging/src/main/kotlin/org/meshtastic/feature/messaging/MessageList.kt
@@ -187,11 +187,11 @@ internal fun MessageList(
message = msg,
selected = selected,
onClick = { if (inSelectionMode) selectedIds.toggle(msg.uuid) },
- onLongClick = {
+ onClickChip = onClickChip,
+ onClickSelect = {
selectedIds.toggle(msg.uuid)
haptics.performHapticFeedback(HapticFeedbackType.LongPress)
},
- onClickChip = onClickChip,
onStatusClick = { showStatusDialog = msg },
onReply = { onReply(msg) },
emojis = msg.emojis,
diff --git a/feature/messaging/src/main/kotlin/org/meshtastic/feature/messaging/component/MessageActions.kt b/feature/messaging/src/main/kotlin/org/meshtastic/feature/messaging/component/MessageActions.kt
deleted file mode 100644
index 69e59484a..000000000
--- a/feature/messaging/src/main/kotlin/org/meshtastic/feature/messaging/component/MessageActions.kt
+++ /dev/null
@@ -1,109 +0,0 @@
-/*
- * Copyright (c) 2025 Meshtastic LLC
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program. If not, see .
- */
-
-package org.meshtastic.feature.messaging.component
-
-import androidx.compose.animation.AnimatedVisibility
-import androidx.compose.foundation.layout.Row
-import androidx.compose.foundation.layout.wrapContentSize
-import androidx.compose.material.icons.Icons
-import androidx.compose.material.icons.automirrored.filled.Reply
-import androidx.compose.material.icons.filled.EmojiEmotions
-import androidx.compose.material.icons.twotone.Cloud
-import androidx.compose.material.icons.twotone.CloudDone
-import androidx.compose.material.icons.twotone.CloudOff
-import androidx.compose.material.icons.twotone.CloudUpload
-import androidx.compose.material.icons.twotone.HowToReg
-import androidx.compose.material.icons.twotone.Warning
-import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
-import androidx.compose.material3.Icon
-import androidx.compose.material3.IconButton
-import androidx.compose.runtime.Composable
-import androidx.compose.runtime.getValue
-import androidx.compose.runtime.mutableStateOf
-import androidx.compose.runtime.remember
-import androidx.compose.runtime.setValue
-import androidx.compose.ui.Modifier
-import androidx.compose.ui.res.stringResource
-import org.meshtastic.core.model.MessageStatus
-import org.meshtastic.core.strings.R
-import org.meshtastic.core.ui.emoji.EmojiPickerDialog
-
-@Composable
-internal fun ReactionButton(onSendReaction: (String) -> Unit = {}) {
- var showEmojiPickerDialog by remember { mutableStateOf(false) }
- if (showEmojiPickerDialog) {
- EmojiPickerDialog(
- onConfirm = { selectedEmoji ->
- showEmojiPickerDialog = false
- onSendReaction(selectedEmoji)
- },
- onDismiss = { showEmojiPickerDialog = false },
- )
- }
- IconButton(onClick = { showEmojiPickerDialog = true }) {
- Icon(imageVector = Icons.Default.EmojiEmotions, contentDescription = stringResource(R.string.react))
- }
-}
-
-@Composable
-private fun ReplyButton(onClick: () -> Unit = {}) = IconButton(
- onClick = onClick,
- content = {
- Icon(imageVector = Icons.AutoMirrored.Filled.Reply, contentDescription = stringResource(R.string.reply))
- },
-)
-
-@Composable
-private fun MessageStatusButton(onStatusClick: () -> Unit = {}, status: MessageStatus, fromLocal: Boolean) =
- AnimatedVisibility(visible = fromLocal) {
- IconButton(onClick = onStatusClick) {
- Icon(
- imageVector =
- when (status) {
- MessageStatus.RECEIVED -> Icons.TwoTone.HowToReg
- MessageStatus.QUEUED -> Icons.TwoTone.CloudUpload
- MessageStatus.DELIVERED -> Icons.TwoTone.CloudDone
- MessageStatus.ENROUTE -> Icons.TwoTone.Cloud
- MessageStatus.ERROR -> Icons.TwoTone.CloudOff
- else -> Icons.TwoTone.Warning
- },
- contentDescription = stringResource(R.string.message_delivery_status),
- )
- }
- }
-
-@OptIn(ExperimentalMaterial3ExpressiveApi::class)
-@Composable
-internal fun MessageActions(
- modifier: Modifier = Modifier,
- isLocal: Boolean = false,
- status: MessageStatus?,
- onSendReaction: (String) -> Unit = {},
- onSendReply: () -> Unit = {},
- onStatusClick: () -> Unit = {},
-) {
- Row(modifier = modifier.wrapContentSize()) {
- ReactionButton { onSendReaction(it) }
- ReplyButton { onSendReply() }
- MessageStatusButton(
- onStatusClick = onStatusClick,
- status = status ?: MessageStatus.UNKNOWN,
- fromLocal = isLocal,
- )
- }
-}
diff --git a/feature/messaging/src/main/kotlin/org/meshtastic/feature/messaging/component/MessageActionsDialog.kt b/feature/messaging/src/main/kotlin/org/meshtastic/feature/messaging/component/MessageActionsDialog.kt
new file mode 100644
index 000000000..f2b8a0644
--- /dev/null
+++ b/feature/messaging/src/main/kotlin/org/meshtastic/feature/messaging/component/MessageActionsDialog.kt
@@ -0,0 +1,126 @@
+/*
+ * Copyright (c) 2025 Meshtastic LLC
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+
+package org.meshtastic.feature.messaging.component
+
+/*
+ * Copyright (c) 2025 Meshtastic LLC
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.automirrored.rounded.Reply
+import androidx.compose.material.icons.rounded.CheckCircleOutline
+import androidx.compose.material.icons.rounded.EmojiEmotions
+import androidx.compose.material.icons.twotone.Cloud
+import androidx.compose.material.icons.twotone.CloudDone
+import androidx.compose.material.icons.twotone.CloudOff
+import androidx.compose.material.icons.twotone.CloudUpload
+import androidx.compose.material.icons.twotone.HowToReg
+import androidx.compose.material.icons.twotone.Warning
+import androidx.compose.material3.Card
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.tooling.preview.Preview
+import androidx.compose.ui.window.Dialog
+import org.meshtastic.core.model.MessageStatus
+import org.meshtastic.core.strings.R
+import org.meshtastic.core.ui.component.ListItem
+import org.meshtastic.core.ui.theme.AppTheme
+
+@Composable
+fun MessageActionsDialog(
+ status: MessageStatus?,
+ onDismiss: () -> Unit = {},
+ onClickReact: () -> Unit = {},
+ onClickReply: () -> Unit,
+ onClickSelect: () -> Unit,
+ onClickStatus: () -> Unit,
+) {
+ Dialog(onDismissRequest = onDismiss) {
+ Card {
+ ListItem(
+ leadingIcon = Icons.Rounded.EmojiEmotions,
+ text = stringResource(R.string.react),
+ trailingIcon = null,
+ ) {
+ onClickReact()
+ onDismiss()
+ }
+
+ ListItem(leadingIcon = Icons.Rounded.CheckCircleOutline, text = "Select", trailingIcon = null) {
+ onClickSelect()
+ onDismiss()
+ }
+
+ ListItem(
+ leadingIcon = Icons.AutoMirrored.Rounded.Reply,
+ text = stringResource(R.string.reply),
+ trailingIcon = null,
+ ) {
+ onClickReply()
+ onDismiss()
+ }
+
+ status?.let {
+ ListItem(
+ leadingIcon =
+ when (it) {
+ MessageStatus.RECEIVED -> Icons.TwoTone.HowToReg
+ MessageStatus.QUEUED -> Icons.TwoTone.CloudUpload
+ MessageStatus.DELIVERED -> Icons.TwoTone.CloudDone
+ MessageStatus.ENROUTE -> Icons.TwoTone.Cloud
+ MessageStatus.ERROR -> Icons.TwoTone.CloudOff
+ else -> Icons.TwoTone.Warning
+ },
+ text = stringResource(R.string.message_delivery_status),
+ trailingIcon = null,
+ ) {
+ onClickStatus()
+ onDismiss()
+ }
+ }
+ }
+ }
+}
+
+@Preview
+@Composable
+private fun MessageActionsDialogPreview() {
+ AppTheme {
+ MessageActionsDialog(
+ status = MessageStatus.DELIVERED,
+ onDismiss = {},
+ onClickReact = {},
+ onClickReply = {},
+ onClickSelect = {},
+ onClickStatus = {},
+ )
+ }
+}
diff --git a/feature/messaging/src/main/kotlin/org/meshtastic/feature/messaging/component/MessageItem.kt b/feature/messaging/src/main/kotlin/org/meshtastic/feature/messaging/component/MessageItem.kt
index 65bc8a2e4..1f4deec94 100644
--- a/feature/messaging/src/main/kotlin/org/meshtastic/feature/messaging/component/MessageItem.kt
+++ b/feature/messaging/src/main/kotlin/org/meshtastic/feature/messaging/component/MessageItem.kt
@@ -41,6 +41,10 @@ import androidx.compose.material3.OutlinedCard
import androidx.compose.material3.Text
import androidx.compose.material3.contentColorFor
import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
@@ -59,6 +63,7 @@ import org.meshtastic.core.ui.component.NodeChip
import org.meshtastic.core.ui.component.Rssi
import org.meshtastic.core.ui.component.Snr
import org.meshtastic.core.ui.component.preview.NodePreviewParameterProvider
+import org.meshtastic.core.ui.emoji.EmojiPickerDialog
import org.meshtastic.core.ui.theme.AppTheme
import org.meshtastic.core.ui.theme.MessageItemColors
@@ -75,8 +80,8 @@ internal fun MessageItem(
onShowReactions: () -> Unit = {},
emojis: List = emptyList(),
onClick: () -> Unit = {},
- onLongClick: () -> Unit = {},
onClickChip: (Node) -> Unit = {},
+ onClickSelect: () -> Unit = {},
onStatusClick: () -> Unit = {},
onNavigateToOriginalMessage: (Int) -> Unit = {},
) = Column(
@@ -85,6 +90,30 @@ internal fun MessageItem(
.fillMaxWidth()
.background(color = if (selected) Color.Gray else MaterialTheme.colorScheme.background),
) {
+ var showMessageActionsDialog by remember { mutableStateOf(false) }
+ var showEmojiPickerDialog by remember { mutableStateOf(false) }
+
+ if (showMessageActionsDialog) {
+ MessageActionsDialog(
+ status = if (message.fromLocal) message.status else null,
+ onDismiss = { showMessageActionsDialog = false },
+ onClickReact = { showEmojiPickerDialog = true },
+ onClickReply = onReply,
+ onClickSelect = onClickSelect,
+ onClickStatus = onStatusClick,
+ )
+ }
+
+ if (showEmojiPickerDialog) {
+ EmojiPickerDialog(
+ onConfirm = { selectedEmoji ->
+ showEmojiPickerDialog = false
+ sendReaction(selectedEmoji)
+ },
+ onDismiss = { showEmojiPickerDialog = false },
+ )
+ }
+
val containsBel = message.text.contains('\u0007')
val containerColor =
Color(
@@ -116,7 +145,7 @@ internal fun MessageItem(
start = if (!message.fromLocal) 0.dp else 16.dp,
end = if (message.fromLocal) 0.dp else 16.dp,
)
- .combinedClickable(onClick = onClick, onLongClick = onLongClick)
+ .combinedClickable(onClick = onClick, onLongClick = { showMessageActionsDialog = true })
.then(messageModifier),
colors = cardColors,
) {
@@ -148,13 +177,6 @@ internal fun MessageItem(
modifier = Modifier.size(16.dp),
)
}
- MessageActions(
- isLocal = message.fromLocal,
- status = message.status,
- onSendReaction = sendReaction,
- onSendReply = onReply,
- onStatusClick = onStatusClick,
- )
}
Column(modifier = Modifier.padding(horizontal = 8.dp)) {
@@ -317,7 +339,7 @@ private fun MessageItemPreview() {
node = sent.node,
selected = false,
onClick = {},
- onLongClick = {},
+ onClickSelect = {},
onStatusClick = {},
ourNode = sent.node,
)
@@ -327,7 +349,7 @@ private fun MessageItemPreview() {
node = received.node,
selected = false,
onClick = {},
- onLongClick = {},
+ onClickSelect = {},
onStatusClick = {},
ourNode = sent.node,
)
@@ -337,7 +359,7 @@ private fun MessageItemPreview() {
node = receivedWithOriginalMessage.node,
selected = false,
onClick = {},
- onLongClick = {},
+ onClickSelect = {},
onStatusClick = {},
ourNode = sent.node,
)
diff --git a/feature/messaging/src/main/kotlin/org/meshtastic/feature/messaging/component/Reaction.kt b/feature/messaging/src/main/kotlin/org/meshtastic/feature/messaging/component/Reaction.kt
index b5caa23bb..4b20fdf1c 100644
--- a/feature/messaging/src/main/kotlin/org/meshtastic/feature/messaging/component/Reaction.kt
+++ b/feature/messaging/src/main/kotlin/org/meshtastic/feature/messaging/component/Reaction.kt
@@ -150,7 +150,6 @@ private fun ReactionItemPreview() {
Column(modifier = Modifier.background(MaterialTheme.colorScheme.background)) {
ReactionItem(emoji = "\uD83D\uDE42")
ReactionItem(emoji = "\uD83D\uDE42", emojiCount = 2)
- ReactionButton()
}
}
}