Move message actions to dialog

This commit is contained in:
Phil Oliver
2025-11-03 22:14:18 -05:00
parent 78a10118a0
commit ea021c8fd5
5 changed files with 162 additions and 124 deletions

View File

@@ -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,

View File

@@ -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 <https://www.gnu.org/licenses/>.
*/
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,
)
}
}

View File

@@ -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 <https://www.gnu.org/licenses/>.
*/
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 <https://www.gnu.org/licenses/>.
*/
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 = {},
)
}
}

View File

@@ -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<Reaction> = 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,
)

View File

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