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() } } }