diff --git a/app/src/main/java/com/geeksville/mesh/ui/message/MessageList.kt b/app/src/main/java/com/geeksville/mesh/ui/message/MessageList.kt
index 2e6b3d083..1889f451f 100644
--- a/app/src/main/java/com/geeksville/mesh/ui/message/MessageList.kt
+++ b/app/src/main/java/com/geeksville/mesh/ui/message/MessageList.kt
@@ -41,8 +41,6 @@ import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
import androidx.compose.runtime.snapshotFlow
import androidx.compose.ui.Modifier
-import androidx.compose.ui.hapticfeedback.HapticFeedbackType
-import androidx.compose.ui.platform.LocalHapticFeedback
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
@@ -118,7 +116,6 @@ internal fun MessageList(
contactKey: String,
onReply: (Message?) -> Unit,
) {
- val haptics = LocalHapticFeedback.current
val inSelectionMode by remember { derivedStateOf { selectedIds.value.isNotEmpty() } }
AutoScrollToBottom(listState, messages)
UpdateUnreadCount(listState, messages, onUnreadChanged)
@@ -169,11 +166,6 @@ internal fun MessageList(
ourNode = ourNode!!,
message = msg,
selected = selected,
- onClick = { if (inSelectionMode) selectedIds.toggle(msg.uuid) },
- onLongClick = {
- selectedIds.toggle(msg.uuid)
- haptics.performHapticFeedback(HapticFeedbackType.LongPress)
- },
onAction = onNodeMenuAction,
onStatusClick = { showStatusDialog = msg },
onReply = { onReply(msg) },
diff --git a/app/src/main/java/com/geeksville/mesh/ui/message/components/MessageActions.kt b/app/src/main/java/com/geeksville/mesh/ui/message/components/MessageActions.kt
deleted file mode 100644
index 65350205b..000000000
--- a/app/src/main/java/com/geeksville/mesh/ui/message/components/MessageActions.kt
+++ /dev/null
@@ -1,128 +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 com.geeksville.mesh.ui.message.components
-
-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 com.geeksville.mesh.MessageStatus
-import com.geeksville.mesh.R
-import com.geeksville.mesh.ui.common.components.EmojiPickerDialog
-
-@Composable
-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
-fun ReplyButton(
- onClick: () -> Unit = {},
-) = IconButton(
- onClick = onClick,
- content = {
- Icon(
- imageVector = Icons.AutoMirrored.Filled.Reply,
- contentDescription = stringResource(R.string.reply),
- )
- }
-)
-
-@Composable
-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
-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/app/src/main/java/com/geeksville/mesh/ui/message/components/MessageActionsDialog.kt b/app/src/main/java/com/geeksville/mesh/ui/message/components/MessageActionsDialog.kt
new file mode 100644
index 000000000..e4b938fb3
--- /dev/null
+++ b/app/src/main/java/com/geeksville/mesh/ui/message/components/MessageActionsDialog.kt
@@ -0,0 +1,101 @@
+/*
+ * 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 com.geeksville.mesh.ui.message.components
+
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.automirrored.rounded.Reply
+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 com.geeksville.mesh.MessageStatus
+import com.geeksville.mesh.R
+import com.geeksville.mesh.ui.common.theme.AppTheme
+import com.geeksville.mesh.ui.settings.components.SettingsItem
+
+@Composable
+fun MessageActionsDialog(
+ status: MessageStatus?,
+ onDismiss: () -> Unit = {},
+ onShowEmojiDialog: () -> Unit = {},
+ onClickReply: () -> Unit,
+ onClickStatus: () -> Unit,
+) {
+ Dialog(onDismissRequest = onDismiss) {
+ Card {
+ SettingsItem(
+ leadingIcon = Icons.Rounded.EmojiEmotions,
+ text = stringResource(R.string.react),
+ trailingIcon = null,
+ ) {
+ onShowEmojiDialog()
+ onDismiss()
+ }
+
+ SettingsItem(
+ leadingIcon = Icons.AutoMirrored.Rounded.Reply,
+ text = stringResource(R.string.reply),
+ trailingIcon = null,
+ ) {
+ onClickReply()
+ onDismiss()
+ }
+
+ status?.let {
+ SettingsItem(
+ 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 = null,
+ onDismiss = {},
+ onShowEmojiDialog = {},
+ onClickReply = {},
+ onClickStatus = {},
+ )
+ }
+}
diff --git a/app/src/main/java/com/geeksville/mesh/ui/message/components/MessageItem.kt b/app/src/main/java/com/geeksville/mesh/ui/message/components/MessageItem.kt
index f7ab4f738..ed5d49624 100644
--- a/app/src/main/java/com/geeksville/mesh/ui/message/components/MessageItem.kt
+++ b/app/src/main/java/com/geeksville/mesh/ui/message/components/MessageItem.kt
@@ -44,6 +44,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
@@ -57,6 +61,7 @@ import com.geeksville.mesh.R
import com.geeksville.mesh.database.entity.Reaction
import com.geeksville.mesh.model.Message
import com.geeksville.mesh.model.Node
+import com.geeksville.mesh.ui.common.components.EmojiPickerDialog
import com.geeksville.mesh.ui.common.components.MDText
import com.geeksville.mesh.ui.common.preview.NodePreviewParameterProvider
import com.geeksville.mesh.ui.common.theme.AppTheme
@@ -76,19 +81,35 @@ internal fun MessageItem(
sendReaction: (String) -> Unit = {},
onShowReactions: () -> Unit = {},
emojis: List = emptyList(),
- onClick: () -> Unit = {},
- onLongClick: () -> Unit = {},
onAction: (NodeMenuAction) -> Unit = {},
onStatusClick: () -> Unit = {},
isConnected: Boolean,
onNavigateToOriginalMessage: (Int) -> Unit = {},
showNodeInfo: Boolean,
-) = Column(
- modifier =
- modifier
- .fillMaxWidth()
- .background(color = if (selected) Color.Gray else MaterialTheme.colorScheme.background),
-) {
+) = Column(modifier = modifier.fillMaxWidth()) {
+ 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 },
+ onShowEmojiDialog = { showEmojiPickerDialog = true },
+ onClickReply = { onReply() },
+ onClickStatus = onStatusClick,
+ )
+ }
+
+ if (showEmojiPickerDialog) {
+ EmojiPickerDialog(
+ onConfirm = { selectedEmoji ->
+ showEmojiPickerDialog = false
+ sendReaction(selectedEmoji)
+ },
+ onDismiss = { showEmojiPickerDialog = false },
+ )
+ }
+
val containsBel = message.text.contains('\u0007')
val containerColor =
Color(
@@ -132,7 +153,12 @@ internal fun MessageItem(
Box(
modifier =
Modifier.fillMaxWidth()
- .combinedClickable(onClick = onClick, onLongClick = onLongClick)
+ .combinedClickable(
+ interactionSource = null,
+ indication = null,
+ onLongClick = { showMessageActionsDialog = true },
+ onClick = {},
+ )
.padding(horizontal = 8.dp, vertical = 4.dp),
) {
@Suppress("MagicNumber")
@@ -183,13 +209,6 @@ internal fun MessageItem(
modifier = Modifier.size(12.dp),
)
}
- /* MessageActions(
- isLocal = message.fromLocal,
- status = message.status,
- onSendReaction = sendReaction,
- onSendReply = onReply,
- onStatusClick = onStatusClick,
- )*/
}
}
}
@@ -328,8 +347,6 @@ private fun MessageItemPreview() {
message = sent,
node = sent.node,
selected = false,
- onClick = {},
- onLongClick = {},
onStatusClick = {},
isConnected = true,
ourNode = sent.node,
@@ -340,8 +357,6 @@ private fun MessageItemPreview() {
message = received,
node = received.node,
selected = false,
- onClick = {},
- onLongClick = {},
onStatusClick = {},
isConnected = true,
ourNode = sent.node,
@@ -352,8 +367,6 @@ private fun MessageItemPreview() {
message = receivedWithOriginalMessage,
node = receivedWithOriginalMessage.node,
selected = false,
- onClick = {},
- onLongClick = {},
onStatusClick = {},
isConnected = true,
ourNode = sent.node,
diff --git a/app/src/main/java/com/geeksville/mesh/ui/message/components/Reaction.kt b/app/src/main/java/com/geeksville/mesh/ui/message/components/Reaction.kt
index 4742a4461..61bf5004e 100644
--- a/app/src/main/java/com/geeksville/mesh/ui/message/components/Reaction.kt
+++ b/app/src/main/java/com/geeksville/mesh/ui/message/components/Reaction.kt
@@ -57,39 +57,20 @@ import com.geeksville.mesh.ui.common.components.BottomSheetDialog
import com.geeksville.mesh.ui.common.theme.AppTheme
@Composable
-private fun ReactionItem(
- emoji: String,
- emojiCount: Int = 1,
- onClick: () -> Unit = {},
- onLongClick: () -> Unit = {},
-) {
+private fun ReactionItem(emoji: String, emojiCount: Int = 1, onClick: () -> Unit = {}, onLongClick: () -> Unit = {}) {
BadgedBox(
badge = {
if (emojiCount > 1) {
- Badge {
- Text(
- fontWeight = FontWeight.Bold,
- text = emojiCount.toString()
- )
- }
+ Badge { Text(fontWeight = FontWeight.Bold, text = emojiCount.toString()) }
}
- }
+ },
) {
Surface(
- modifier = Modifier
- .combinedClickable(
- onClick = onClick,
- onLongClick = onLongClick
- ),
+ modifier = Modifier.combinedClickable(onClick = onClick, onLongClick = onLongClick),
color = MaterialTheme.colorScheme.primaryContainer,
shape = CircleShape,
) {
- Text(
- text = emoji,
- modifier = Modifier
- .padding(4.dp)
- .clip(CircleShape),
- )
+ Text(text = emoji, modifier = Modifier.padding(4.dp).clip(CircleShape))
}
}
}
@@ -102,10 +83,7 @@ fun ReactionRow(
onSendReaction: (String) -> Unit = {},
onShowReactions: () -> Unit = {},
) {
- val emojiList =
- reduceEmojis(
- reactions.reversed().map { it.emoji }
- ).entries
+ val emojiList = reduceEmojis(reactions.reversed().map { it.emoji }).entries
AnimatedVisibility(emojiList.isNotEmpty()) {
LazyRow(
@@ -113,16 +91,12 @@ fun ReactionRow(
horizontalArrangement = Arrangement.Start,
verticalAlignment = Alignment.CenterVertically,
) {
- items(
- emojiList.size
- ) { index ->
+ items(emojiList.size) { index ->
val entry = emojiList.elementAt(index)
ReactionItem(
emoji = entry.key,
emojiCount = entry.value,
- onClick = {
- onSendReaction(entry.key)
- },
+ onClick = { onSendReaction(entry.key) },
onLongClick = onShowReactions,
)
}
@@ -133,71 +107,49 @@ fun ReactionRow(
fun reduceEmojis(emojis: List): Map = emojis.groupingBy { it }.eachCount()
@Composable
-fun ReactionDialog(
- reactions: List,
- onDismiss: () -> Unit = {}
-) = BottomSheetDialog(
- onDismiss = onDismiss,
- modifier = Modifier.fillMaxHeight(fraction = .3f),
-) {
- val groupedEmojis = reactions.groupBy { it.emoji }
- var selectedEmoji by remember { mutableStateOf(null) }
- val filteredReactions = selectedEmoji?.let { groupedEmojis[it] ?: emptyList() } ?: reactions
+fun ReactionDialog(reactions: List, onDismiss: () -> Unit = {}) =
+ BottomSheetDialog(onDismiss = onDismiss, modifier = Modifier.fillMaxHeight(fraction = .3f)) {
+ val groupedEmojis = reactions.groupBy { it.emoji }
+ var selectedEmoji by remember { mutableStateOf(null) }
+ val filteredReactions = selectedEmoji?.let { groupedEmojis[it] ?: emptyList() } ?: reactions
- LazyRow(
- horizontalArrangement = Arrangement.spacedBy(8.dp),
- modifier = Modifier.fillMaxWidth()
- ) {
- items(groupedEmojis.entries.toList()) { (emoji, reactions) ->
- Text(
- text = "$emoji${reactions.size}",
- modifier = Modifier
- .clip(CircleShape)
- .background(if (selectedEmoji == emoji) Color.Gray else Color.Transparent)
- .padding(8.dp)
- .clickable {
- selectedEmoji = if (selectedEmoji == emoji) null else emoji
- },
- style = MaterialTheme.typography.bodyMedium
- )
- }
- }
-
- HorizontalDivider(Modifier.padding(vertical = 8.dp))
-
- LazyColumn(
- modifier = Modifier.fillMaxWidth(),
- verticalArrangement = Arrangement.spacedBy(12.dp)
- ) {
- items(filteredReactions) { reaction ->
- Row(
- modifier = Modifier.fillMaxWidth(),
- horizontalArrangement = Arrangement.SpaceBetween,
- verticalAlignment = Alignment.CenterVertically,
- ) {
+ LazyRow(horizontalArrangement = Arrangement.spacedBy(8.dp), modifier = Modifier.fillMaxWidth()) {
+ items(groupedEmojis.entries.toList()) { (emoji, reactions) ->
Text(
- text = reaction.user.longName,
- style = MaterialTheme.typography.titleMedium
- )
- Text(
- text = reaction.emoji,
- style = MaterialTheme.typography.titleLarge
+ text = "$emoji${reactions.size}",
+ modifier =
+ Modifier.clip(CircleShape)
+ .background(if (selectedEmoji == emoji) Color.Gray else Color.Transparent)
+ .padding(8.dp)
+ .clickable { selectedEmoji = if (selectedEmoji == emoji) null else emoji },
+ style = MaterialTheme.typography.bodyMedium,
)
}
}
+
+ HorizontalDivider(Modifier.padding(vertical = 8.dp))
+
+ LazyColumn(modifier = Modifier.fillMaxWidth(), verticalArrangement = Arrangement.spacedBy(12.dp)) {
+ items(filteredReactions) { reaction ->
+ Row(
+ modifier = Modifier.fillMaxWidth(),
+ horizontalArrangement = Arrangement.SpaceBetween,
+ verticalAlignment = Alignment.CenterVertically,
+ ) {
+ Text(text = reaction.user.longName, style = MaterialTheme.typography.titleMedium)
+ Text(text = reaction.emoji, style = MaterialTheme.typography.titleLarge)
+ }
+ }
+ }
}
-}
@PreviewLightDark
@Composable
fun ReactionItemPreview() {
AppTheme {
- Column(
- modifier = Modifier.background(MaterialTheme.colorScheme.background)
- ) {
+ Column(modifier = Modifier.background(MaterialTheme.colorScheme.background)) {
ReactionItem(emoji = "\uD83D\uDE42")
ReactionItem(emoji = "\uD83D\uDE42", emojiCount = 2)
- ReactionButton()
}
}
}
@@ -207,20 +159,21 @@ fun ReactionItemPreview() {
fun ReactionRowPreview() {
AppTheme {
ReactionRow(
- reactions = listOf(
+ reactions =
+ listOf(
Reaction(
replyId = 1,
user = MeshProtos.User.getDefaultInstance(),
emoji = "\uD83D\uDE42",
- timestamp = 1L
+ timestamp = 1L,
),
Reaction(
replyId = 1,
user = MeshProtos.User.getDefaultInstance(),
emoji = "\uD83D\uDE42",
- timestamp = 1L
+ timestamp = 1L,
),
- )
+ ),
)
}
}