refactor(ui): Icon audit and node list item refactor (#4313)

Signed-off-by: James Rich <2199651+jamesarich@users.noreply.github.com>
This commit is contained in:
James Rich 2026-01-25 16:43:23 -06:00 committed by GitHub
parent 5db2c9d69c
commit a28aa4d52e
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
91 changed files with 2178 additions and 702 deletions

View file

@ -48,19 +48,19 @@ import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.ArrowBack
import androidx.compose.material.icons.automirrored.filled.Reply
import androidx.compose.material.icons.automirrored.filled.Send
import androidx.compose.material.icons.filled.ArrowDownward
import androidx.compose.material.icons.filled.ChatBubbleOutline
import androidx.compose.material.icons.filled.Close
import androidx.compose.material.icons.filled.ContentCopy
import androidx.compose.material.icons.filled.Delete
import androidx.compose.material.icons.filled.MoreVert
import androidx.compose.material.icons.filled.SelectAll
import androidx.compose.material.icons.filled.SpeakerNotes
import androidx.compose.material.icons.filled.SpeakerNotesOff
import androidx.compose.material.icons.filled.Visibility
import androidx.compose.material.icons.filled.VisibilityOff
import androidx.compose.material.icons.rounded.ArrowDownward
import androidx.compose.material.icons.rounded.ChatBubbleOutline
import androidx.compose.material.icons.rounded.ContentCopy
import androidx.compose.material.icons.rounded.Delete
import androidx.compose.material.icons.rounded.FilterList
import androidx.compose.material.icons.rounded.FilterListOff
import androidx.compose.material.icons.rounded.MoreVert
import androidx.compose.material.icons.rounded.SelectAll
import androidx.compose.material.icons.rounded.SpeakerNotes
import androidx.compose.material.icons.rounded.SpeakerNotesOff
import androidx.compose.material.icons.rounded.Visibility
import androidx.compose.material.icons.rounded.VisibilityOff
import androidx.compose.material3.AlertDialog
import androidx.compose.material3.Button
import androidx.compose.material3.DropdownMenu
@ -499,7 +499,7 @@ private fun BoxScope.ScrollToBottomFab(coroutineScope: CoroutineScope, listState
},
) {
Icon(
imageVector = Icons.Default.ArrowDownward,
imageVector = Icons.Rounded.ArrowDownward,
contentDescription = stringResource(Res.string.scroll_to_bottom),
)
}
@ -683,13 +683,13 @@ private fun ActionModeTopBar(selectedCount: Int, onAction: (MessageMenuAction) -
},
actions = {
IconButton(onClick = { onAction(MessageMenuAction.ClipboardCopy) }) {
Icon(imageVector = Icons.Default.ContentCopy, contentDescription = stringResource(Res.string.copy))
Icon(imageVector = Icons.Rounded.ContentCopy, contentDescription = stringResource(Res.string.copy))
}
IconButton(onClick = { onAction(MessageMenuAction.Delete) }) {
Icon(imageVector = Icons.Default.Delete, contentDescription = stringResource(Res.string.delete))
Icon(imageVector = Icons.Rounded.Delete, contentDescription = stringResource(Res.string.delete))
}
IconButton(onClick = { onAction(MessageMenuAction.SelectAll) }) {
Icon(imageVector = Icons.Default.SelectAll, contentDescription = stringResource(Res.string.select_all))
Icon(imageVector = Icons.Rounded.SelectAll, contentDescription = stringResource(Res.string.select_all))
}
},
)
@ -775,7 +775,7 @@ private fun MessageTopBarActions(
var expanded by remember { mutableStateOf(false) }
Box {
IconButton(onClick = { expanded = true }, enabled = true) {
Icon(imageVector = Icons.Default.MoreVert, contentDescription = stringResource(Res.string.overflow_menu))
Icon(imageVector = Icons.Rounded.MoreVert, contentDescription = stringResource(Res.string.overflow_menu))
}
OverFlowMenu(
expanded = expanded,
@ -828,7 +828,7 @@ private fun QuickChatToggleMenuItem(showQuickChat: Boolean, onDismiss: () -> Uni
},
leadingIcon = {
Icon(
imageVector = if (showQuickChat) Icons.Default.SpeakerNotesOff else Icons.Default.SpeakerNotes,
imageVector = if (showQuickChat) Icons.Rounded.SpeakerNotesOff else Icons.Rounded.SpeakerNotes,
contentDescription = title,
)
},
@ -844,7 +844,7 @@ private fun QuickChatOptionsMenuItem(onDismiss: () -> Unit, onNavigate: () -> Un
onDismiss()
onNavigate()
},
leadingIcon = { Icon(imageVector = Icons.Default.ChatBubbleOutline, contentDescription = title) },
leadingIcon = { Icon(imageVector = Icons.Rounded.ChatBubbleOutline, contentDescription = title) },
)
}
@ -859,7 +859,7 @@ private fun FilteredMessagesMenuItem(showFiltered: Boolean, count: Int, onDismis
},
leadingIcon = {
Icon(
imageVector = if (showFiltered) Icons.Default.VisibilityOff else Icons.Default.Visibility,
imageVector = if (showFiltered) Icons.Rounded.VisibilityOff else Icons.Rounded.Visibility,
contentDescription = title,
)
},

View file

@ -1,5 +1,5 @@
/*
* Copyright (c) 2025 Meshtastic LLC
* Copyright (c) 2025-2026 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
@ -14,7 +14,6 @@
* 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
import androidx.compose.foundation.layout.Arrangement
@ -35,10 +34,10 @@ import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Add
import androidx.compose.material.icons.filled.DragHandle
import androidx.compose.material.icons.filled.Edit
import androidx.compose.material.icons.filled.FastForward
import androidx.compose.material.icons.rounded.Add
import androidx.compose.material.icons.rounded.DragHandle
import androidx.compose.material.icons.rounded.Edit
import androidx.compose.material.icons.rounded.FastForward
import androidx.compose.material3.AlertDialog
import androidx.compose.material3.Button
import androidx.compose.material3.Card
@ -148,7 +147,7 @@ fun QuickChatScreen(
onClick = { showActionDialog = QuickChatAction(position = actions.size) },
modifier = Modifier.align(Alignment.BottomEnd).padding(16.dp),
) {
Icon(imageVector = Icons.Default.Add, contentDescription = stringResource(Res.string.add))
Icon(imageVector = Icons.Rounded.Add, contentDescription = stringResource(Res.string.add))
}
}
}
@ -231,9 +230,9 @@ private fun EditQuickChatDialog(
val (text, icon) =
if (isInstant) {
Res.string.quick_chat_instant to Icons.Default.FastForward
Res.string.quick_chat_instant to Icons.Rounded.FastForward
} else {
Res.string.quick_chat_append to Icons.Default.Add
Res.string.quick_chat_append to Icons.Rounded.Add
}
Row(verticalAlignment = Alignment.CenterVertically) {
@ -338,7 +337,7 @@ private fun QuickChatItem(
leadingContent = {
if (action.mode == QuickChatAction.Mode.Instant) {
Icon(
imageVector = Icons.Default.FastForward,
imageVector = Icons.Rounded.FastForward,
contentDescription = stringResource(Res.string.quick_chat_instant),
)
}
@ -349,12 +348,12 @@ private fun QuickChatItem(
Row(verticalAlignment = Alignment.CenterVertically) {
IconButton(onClick = { onEdit(action) }, modifier = Modifier.size(48.dp)) {
Icon(
imageVector = Icons.Default.Edit,
imageVector = Icons.Rounded.Edit,
contentDescription = stringResource(Res.string.quick_chat_edit),
)
}
Icon(
imageVector = Icons.Default.DragHandle,
imageVector = Icons.Rounded.DragHandle,
contentDescription = stringResource(Res.string.quick_chat),
)
}

View file

@ -22,7 +22,7 @@ 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.AddReaction
import androidx.compose.material.icons.rounded.AddReaction
import androidx.compose.material.icons.twotone.AddLink
import androidx.compose.material.icons.twotone.Cloud
import androidx.compose.material.icons.twotone.CloudDone
@ -61,7 +61,7 @@ internal fun ReactionButton(onSendReaction: (String) -> Unit = {}) {
)
}
IconButton(onClick = { showEmojiPickerDialog = true }) {
Icon(imageVector = Icons.Default.AddReaction, contentDescription = stringResource(Res.string.react))
Icon(imageVector = Icons.Rounded.AddReaction, contentDescription = stringResource(Res.string.react))
}
}

View file

@ -27,11 +27,11 @@ import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.AddReaction
import androidx.compose.material.icons.filled.ContentCopy
import androidx.compose.material.icons.filled.Delete
import androidx.compose.material.icons.filled.Reply
import androidx.compose.material.icons.filled.SelectAll
import androidx.compose.material.icons.rounded.AddReaction
import androidx.compose.material.icons.rounded.ContentCopy
import androidx.compose.material.icons.rounded.Delete
import androidx.compose.material.icons.rounded.Reply
import androidx.compose.material.icons.rounded.SelectAll
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.Icon
@ -95,25 +95,25 @@ fun MessageActionsContent(
ListItem(
headlineContent = { Text(stringResource(Res.string.reply)) },
leadingContent = { Icon(Icons.Default.Reply, contentDescription = stringResource(Res.string.reply)) },
leadingContent = { Icon(Icons.Rounded.Reply, contentDescription = stringResource(Res.string.reply)) },
modifier = Modifier.clickable(onClick = onReply),
)
ListItem(
headlineContent = { Text(stringResource(Res.string.copy)) },
leadingContent = { Icon(Icons.Default.ContentCopy, contentDescription = stringResource(Res.string.copy)) },
leadingContent = { Icon(Icons.Rounded.ContentCopy, contentDescription = stringResource(Res.string.copy)) },
modifier = Modifier.clickable(onClick = onCopy),
)
ListItem(
headlineContent = { Text(stringResource(Res.string.select)) },
leadingContent = { Icon(Icons.Default.SelectAll, contentDescription = stringResource(Res.string.select)) },
leadingContent = { Icon(Icons.Rounded.SelectAll, contentDescription = stringResource(Res.string.select)) },
modifier = Modifier.clickable(onClick = onSelect),
)
ListItem(
headlineContent = { Text(stringResource(Res.string.delete)) },
leadingContent = { Icon(Icons.Default.Delete, contentDescription = stringResource(Res.string.delete)) },
leadingContent = { Icon(Icons.Rounded.Delete, contentDescription = stringResource(Res.string.delete)) },
modifier = Modifier.clickable(onClick = onDelete),
)
}
@ -146,7 +146,7 @@ private fun QuickEmojiRow(quickEmojis: List<String>, onReact: (String) -> Unit,
modifier = Modifier.size(40.dp).background(MaterialTheme.colorScheme.surfaceVariant, CircleShape),
) {
Icon(
Icons.Default.AddReaction,
Icons.Rounded.AddReaction,
contentDescription = "More reactions",
modifier = Modifier.size(20.dp),
tint = MaterialTheme.colorScheme.onSurfaceVariant,

View file

@ -16,6 +16,7 @@
*/
package org.meshtastic.feature.messaging.component
import android.content.ClipData
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.foundation.background
import androidx.compose.foundation.border
@ -29,16 +30,7 @@ import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Cloud
import androidx.compose.material.icons.filled.FormatQuote
import androidx.compose.material.icons.twotone.AddLink
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.Link
import androidx.compose.material.icons.twotone.Warning
import androidx.compose.material.icons.rounded.FormatQuote
import androidx.compose.material3.CardDefaults
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon
@ -52,18 +44,20 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalClipboardManager
import androidx.compose.ui.platform.ClipEntry
import androidx.compose.ui.platform.LocalClipboard
import androidx.compose.ui.semantics.contentDescription
import androidx.compose.ui.semantics.semantics
import androidx.compose.ui.text.AnnotatedString
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.tooling.preview.PreviewLightDark
import androidx.compose.ui.unit.dp
import kotlinx.coroutines.launch
import org.jetbrains.compose.resources.stringResource
import org.meshtastic.core.database.entity.Reaction
import org.meshtastic.core.database.model.Message
@ -71,7 +65,6 @@ import org.meshtastic.core.database.model.Node
import org.meshtastic.core.model.MessageStatus
import org.meshtastic.core.strings.Res
import org.meshtastic.core.strings.filter_message_label
import org.meshtastic.core.strings.hops_away_template
import org.meshtastic.core.strings.message_delivery_status
import org.meshtastic.core.strings.reply
import org.meshtastic.core.strings.sample_message
@ -82,6 +75,14 @@ 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.EmojiPicker
import org.meshtastic.core.ui.icon.Cloud
import org.meshtastic.core.ui.icon.CloudDone
import org.meshtastic.core.ui.icon.CloudOffTwoTone
import org.meshtastic.core.ui.icon.CloudSync
import org.meshtastic.core.ui.icon.CloudTwoTone
import org.meshtastic.core.ui.icon.Hops
import org.meshtastic.core.ui.icon.MeshtasticIcons
import org.meshtastic.core.ui.icon.Warning
import org.meshtastic.core.ui.theme.AppTheme
import org.meshtastic.core.ui.theme.MessageItemColors
@ -123,7 +124,8 @@ internal fun MessageItem(
),
) {
var activeSheet by remember { mutableStateOf<ActiveSheet?>(null) }
val clipboardManager = LocalClipboardManager.current
val clipboardManager = LocalClipboard.current
val coroutineScope = rememberCoroutineScope()
val sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true)
if (activeSheet != null) {
@ -143,7 +145,11 @@ internal fun MessageItem(
onMoreReactions = { activeSheet = ActiveSheet.Emoji },
onCopy = {
activeSheet = null
clipboardManager.setText(AnnotatedString(message.text))
coroutineScope.launch {
clipboardManager.setClipEntry(
ClipEntry(ClipData.newPlainText("message", message.text)),
)
}
},
onSelect = {
activeSheet = null
@ -222,7 +228,7 @@ internal fun MessageItem(
)
if (message.viaMqtt) {
Icon(
Icons.Default.Cloud,
MeshtasticIcons.Cloud,
contentDescription = stringResource(Res.string.via_mqtt),
modifier = Modifier.size(16.dp),
)
@ -278,10 +284,21 @@ internal fun MessageItem(
Rssi(message.rssi)
}
} else {
Text(
text = stringResource(Res.string.hops_away_template, message.hopsAway),
style = MaterialTheme.typography.labelSmall,
)
Row(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(2.dp),
) {
Icon(
imageVector = MeshtasticIcons.Hops,
contentDescription = null,
modifier = Modifier.size(14.dp),
tint = cardColors.contentColor.copy(alpha = 0.7f),
)
Text(
text = message.hopsAway.toString(),
style = MaterialTheme.typography.labelSmall,
)
}
}
}
if (containsBel) {
@ -341,14 +358,14 @@ private enum class ActiveSheet {
private fun MessageStatusIcon(status: MessageStatus, onClick: () -> Unit, modifier: Modifier = Modifier) {
val icon =
when (status) {
MessageStatus.RECEIVED -> Icons.TwoTone.HowToReg
MessageStatus.QUEUED -> Icons.TwoTone.CloudUpload
MessageStatus.DELIVERED -> Icons.TwoTone.CloudDone
MessageStatus.SFPP_ROUTING -> Icons.TwoTone.AddLink
MessageStatus.SFPP_CONFIRMED -> Icons.TwoTone.Link
MessageStatus.ENROUTE -> Icons.TwoTone.Cloud
MessageStatus.ERROR -> Icons.TwoTone.CloudOff
else -> Icons.TwoTone.Warning
MessageStatus.RECEIVED -> MeshtasticIcons.CloudDone
MessageStatus.QUEUED -> MeshtasticIcons.CloudSync
MessageStatus.DELIVERED -> MeshtasticIcons.CloudDone
MessageStatus.SFPP_ROUTING -> MeshtasticIcons.CloudSync
MessageStatus.SFPP_CONFIRMED -> MeshtasticIcons.CloudDone
MessageStatus.ENROUTE -> MeshtasticIcons.CloudTwoTone
MessageStatus.ERROR -> MeshtasticIcons.CloudOffTwoTone
else -> MeshtasticIcons.Warning
}
Icon(
imageVector = icon,
@ -392,7 +409,7 @@ private fun OriginalMessageSnippet(
horizontalArrangement = Arrangement.spacedBy(4.dp),
) {
Icon(
Icons.Default.FormatQuote,
Icons.Rounded.FormatQuote,
contentDescription = stringResource(Res.string.reply),
modifier = Modifier.size(16.dp),
)

View file

@ -35,7 +35,7 @@ import androidx.compose.foundation.lazy.LazyRow
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.AddReaction
import androidx.compose.material.icons.rounded.AddReaction
import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
@ -67,7 +67,6 @@ import org.meshtastic.core.model.util.getShortDateTime
import org.meshtastic.core.strings.Res
import org.meshtastic.core.strings.delivery_confirmed
import org.meshtastic.core.strings.error
import org.meshtastic.core.strings.hops_away_template
import org.meshtastic.core.strings.message_delivery_status
import org.meshtastic.core.strings.message_status_enroute
import org.meshtastic.core.strings.message_status_queued
@ -77,6 +76,8 @@ import org.meshtastic.core.ui.component.BottomSheetDialog
import org.meshtastic.core.ui.component.Rssi
import org.meshtastic.core.ui.component.Snr
import org.meshtastic.core.ui.emoji.EmojiPickerDialog
import org.meshtastic.core.ui.icon.Hops
import org.meshtastic.core.ui.icon.MeshtasticIcons
import org.meshtastic.core.ui.theme.AppTheme
import org.meshtastic.feature.messaging.DeliveryInfo
import org.meshtastic.proto.MeshProtos
@ -186,7 +187,7 @@ private fun AddReactionButton(modifier: Modifier = Modifier, onSendReaction: (St
border = BorderStroke(1.dp, MaterialTheme.colorScheme.outlineVariant.copy(alpha = 0.2f)),
) {
Icon(
imageVector = Icons.Default.AddReaction,
imageVector = Icons.Rounded.AddReaction,
contentDescription = stringResource(Res.string.react),
modifier = Modifier.padding(6.dp),
tint = MaterialTheme.colorScheme.onSurfaceVariant,
@ -300,10 +301,21 @@ internal fun ReactionDialog(
Rssi(reaction.rssi)
}
} else {
Text(
text = stringResource(Res.string.hops_away_template, reaction.hopsAway),
style = MaterialTheme.typography.labelSmall,
)
Row(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(2.dp),
) {
Icon(
imageVector = MeshtasticIcons.Hops,
contentDescription = null,
modifier = Modifier.size(14.dp),
tint = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.7f),
)
Text(
text = reaction.hopsAway.toString(),
style = MaterialTheme.typography.labelSmall,
)
}
}
}
Spacer(modifier = Modifier.weight(1f))