fix(icons): audit and correct icon migration regressions from #5030 #5040 #5056 (#5136)

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
James Rich 2026-04-14 20:14:31 -05:00 committed by GitHub
parent fa63a4ac50
commit bf0deef708
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 34 additions and 60 deletions

View file

@ -25,6 +25,7 @@ import org.meshtastic.core.resources.ic_fingerprint
import org.meshtastic.core.resources.ic_fork_left
import org.meshtastic.core.resources.ic_home
import org.meshtastic.core.resources.ic_icecream
import org.meshtastic.core.resources.ic_memory
import org.meshtastic.core.resources.ic_military_tech
import org.meshtastic.core.resources.ic_mountain_flag
import org.meshtastic.core.resources.ic_my_location
@ -75,4 +76,4 @@ val MeshtasticIcons.DeviceNumbers: ImageVector
val MeshtasticIcons.Android: ImageVector
@Composable get() = vectorResource(Res.drawable.ic_android)
val MeshtasticIcons.HardwareModel: ImageVector
@Composable get() = vectorResource(Res.drawable.ic_router)
@Composable get() = vectorResource(Res.drawable.ic_memory)

View file

@ -24,11 +24,13 @@ import org.meshtastic.core.model.MessageStatus
import org.meshtastic.core.resources.Res
import org.meshtastic.core.resources.message_delivery_status
import org.meshtastic.core.ui.icon.Acknowledged
import org.meshtastic.core.ui.icon.AddLink
import org.meshtastic.core.ui.icon.CloudUpload
import org.meshtastic.core.ui.icon.LinkIcon
import org.meshtastic.core.ui.icon.MeshtasticIcons
import org.meshtastic.core.ui.icon.MessageEnroute
import org.meshtastic.core.ui.icon.MessageError
import org.meshtastic.core.ui.icon.MqttDelivered
import org.meshtastic.core.ui.icon.MqttSyncing
import org.meshtastic.core.ui.icon.Warning
@Composable
@ -36,10 +38,10 @@ fun MessageStatusIcon(status: MessageStatus, modifier: Modifier = Modifier) {
val icon =
when (status) {
MessageStatus.RECEIVED -> MeshtasticIcons.Acknowledged
MessageStatus.QUEUED -> MeshtasticIcons.MqttSyncing
MessageStatus.QUEUED -> MeshtasticIcons.CloudUpload
MessageStatus.DELIVERED -> MeshtasticIcons.MqttDelivered
MessageStatus.SFPP_ROUTING -> MeshtasticIcons.MqttSyncing
MessageStatus.SFPP_CONFIRMED -> MeshtasticIcons.MqttDelivered
MessageStatus.SFPP_ROUTING -> MeshtasticIcons.AddLink
MessageStatus.SFPP_CONFIRMED -> MeshtasticIcons.LinkIcon
MessageStatus.ENROUTE -> MeshtasticIcons.MessageEnroute
MessageStatus.ERROR -> MeshtasticIcons.MessageError
else -> MeshtasticIcons.Warning

View file

@ -48,6 +48,7 @@ import org.jetbrains.compose.resources.stringResource
import org.jetbrains.compose.resources.vectorResource
import org.meshtastic.core.common.util.formatString
import org.meshtastic.core.model.ConnectionState
import org.meshtastic.core.model.DeviceType
import org.meshtastic.core.model.Node
import org.meshtastic.core.model.isUnmessageableRole
import org.meshtastic.core.model.util.UnitConversions.celsiusToFahrenheit
@ -106,6 +107,7 @@ fun NodeItem(
onClick: () -> Unit = {},
onLongClick: (() -> Unit)? = null,
connectionState: ConnectionState,
deviceType: DeviceType? = null,
isActive: Boolean = false,
) {
val originalLongName = thatNode.user.long_name.ifEmpty { stringResource(Res.string.unknown_username) }
@ -166,6 +168,7 @@ fun NodeItem(
isMuted = isMuted,
isUnmessageable = unmessageable,
connectionState = connectionState,
deviceType = deviceType,
contentColor = contentColor,
)
@ -400,6 +403,7 @@ private fun NodeItemHeader(
isMuted: Boolean,
isUnmessageable: Boolean,
connectionState: ConnectionState,
deviceType: DeviceType?,
contentColor: Color,
) {
Row(
@ -445,6 +449,7 @@ private fun NodeItemHeader(
isMuted = isMuted,
isUnmessageable = isUnmessageable,
connectionState = connectionState,
deviceType = deviceType,
contentColor = contentColor,
)
}

View file

@ -37,6 +37,7 @@ import androidx.compose.ui.unit.dp
import org.jetbrains.compose.resources.StringResource
import org.jetbrains.compose.resources.stringResource
import org.meshtastic.core.model.ConnectionState
import org.meshtastic.core.model.DeviceType
import org.meshtastic.core.resources.Res
import org.meshtastic.core.resources.connected
import org.meshtastic.core.resources.connecting
@ -46,17 +47,11 @@ import org.meshtastic.core.resources.favorite
import org.meshtastic.core.resources.mute_always
import org.meshtastic.core.resources.unmessageable
import org.meshtastic.core.resources.unmonitored_or_infrastructure
import org.meshtastic.core.ui.icon.DeviceSleep
import org.meshtastic.core.ui.icon.Disconnected
import org.meshtastic.core.ui.component.ConnectionsNavIcon
import org.meshtastic.core.ui.icon.Favorite
import org.meshtastic.core.ui.icon.MeshtasticIcons
import org.meshtastic.core.ui.icon.MqttDelivered
import org.meshtastic.core.ui.icon.MqttSyncing
import org.meshtastic.core.ui.icon.Unmessageable
import org.meshtastic.core.ui.icon.VolumeOff
import org.meshtastic.core.ui.theme.StatusColors.StatusGreen
import org.meshtastic.core.ui.theme.StatusColors.StatusOrange
import org.meshtastic.core.ui.theme.StatusColors.StatusRed
import org.meshtastic.core.ui.theme.StatusColors.StatusYellow
@OptIn(ExperimentalMaterial3Api::class)
@ -68,11 +63,12 @@ fun NodeStatusIcons(
isMuted: Boolean,
connectionState: ConnectionState,
modifier: Modifier = Modifier,
deviceType: DeviceType? = null,
contentColor: Color = LocalContentColor.current,
) {
Row(modifier = modifier.padding(4.dp)) {
if (isThisNode) {
ThisNodeStatusBadge(connectionState)
ThisNodeStatusBadge(connectionState = connectionState, deviceType = deviceType)
}
if (isUnmessageable) {
@ -104,7 +100,7 @@ fun NodeStatusIcons(
@OptIn(ExperimentalMaterial3Api::class)
@Composable
private fun ThisNodeStatusBadge(connectionState: ConnectionState) {
private fun ThisNodeStatusBadge(connectionState: ConnectionState, deviceType: DeviceType?) {
TooltipBox(
positionProvider = TooltipDefaults.rememberTooltipPositionProvider(TooltipAnchorPosition.Above),
tooltip = {
@ -123,55 +119,10 @@ private fun ThisNodeStatusBadge(connectionState: ConnectionState) {
},
state = rememberTooltipState(),
) {
when (connectionState) {
ConnectionState.Connected -> ConnectedStatusIcon()
ConnectionState.Connecting -> ConnectingStatusIcon()
ConnectionState.Disconnected -> DisconnectedStatusIcon()
ConnectionState.DeviceSleep -> DeviceSleepStatusIcon()
}
ConnectionsNavIcon(connectionState = connectionState, deviceType = deviceType, modifier = Modifier.size(24.dp))
}
}
@Composable
private fun ConnectedStatusIcon() {
Icon(
imageVector = MeshtasticIcons.MqttDelivered,
contentDescription = stringResource(Res.string.connected),
modifier = Modifier.size(24.dp),
tint = MaterialTheme.colorScheme.StatusGreen,
)
}
@Composable
private fun ConnectingStatusIcon() {
Icon(
imageVector = MeshtasticIcons.MqttSyncing,
contentDescription = stringResource(Res.string.connecting),
modifier = Modifier.size(24.dp),
tint = MaterialTheme.colorScheme.StatusOrange,
)
}
@Composable
private fun DisconnectedStatusIcon() {
Icon(
imageVector = MeshtasticIcons.Disconnected,
contentDescription = stringResource(Res.string.disconnected),
modifier = Modifier.size(24.dp),
tint = MaterialTheme.colorScheme.StatusRed,
)
}
@Composable
private fun DeviceSleepStatusIcon() {
Icon(
imageVector = MeshtasticIcons.DeviceSleep,
contentDescription = stringResource(Res.string.device_sleeping),
modifier = Modifier.size(24.dp),
tint = MaterialTheme.colorScheme.StatusYellow,
)
}
@OptIn(ExperimentalMaterial3Api::class)
@Composable
private fun StatusBadge(

View file

@ -97,6 +97,7 @@ fun NodeListScreen(
}
val connectionState by viewModel.connectionState.collectAsStateWithLifecycle()
val deviceType by viewModel.deviceType.collectAsStateWithLifecycle()
val isScrollInProgress by remember {
derivedStateOf { listState.isScrollInProgress && (listState.canScrollForward || listState.canScrollBackward) }
@ -187,6 +188,7 @@ fun NodeListScreen(
onClick = { navigateToNodeDetails(node.num) },
onLongClick = longClick,
connectionState = connectionState,
deviceType = deviceType,
isActive = isActive,
)
val isThisNode = remember(node) { ourNode?.num == node.num }

View file

@ -23,13 +23,16 @@ import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.launch
import org.koin.core.annotation.KoinViewModel
import org.meshtastic.core.model.DeviceType
import org.meshtastic.core.model.Node
import org.meshtastic.core.model.NodeSortOption
import org.meshtastic.core.model.RadioController
import org.meshtastic.core.repository.NodeRepository
import org.meshtastic.core.repository.RadioConfigRepository
import org.meshtastic.core.repository.RadioInterfaceService
import org.meshtastic.core.repository.ServiceRepository
import org.meshtastic.core.ui.viewmodel.stateInWhileSubscribed
import org.meshtastic.feature.node.detail.NodeManagementActions
@ -45,6 +48,7 @@ class NodeListViewModel(
private val radioConfigRepository: RadioConfigRepository,
private val serviceRepository: ServiceRepository,
private val radioController: RadioController,
private val radioInterfaceService: RadioInterfaceService,
val nodeManagementActions: NodeManagementActions,
private val getFilteredNodesUseCase: GetFilteredNodesUseCase,
val nodeFilterPreferences: NodeFilterPreferences,
@ -58,6 +62,11 @@ class NodeListViewModel(
val connectionState = serviceRepository.connectionState
val deviceType: StateFlow<DeviceType?> =
radioInterfaceService.currentDeviceAddressFlow
.map { address -> address?.let { DeviceType.fromAddress(it) } }
.stateInWhileSubscribed(initialValue = null)
private val nodeSortOption = nodeFilterPreferences.nodeSortOption
private val _nodeFilterText = savedStateHandle.getStateFlow(KEY_FILTER_TEXT, "")

View file

@ -32,6 +32,7 @@ import org.meshtastic.core.repository.RadioConfigRepository
import org.meshtastic.core.repository.ServiceRepository
import org.meshtastic.core.testing.FakeNodeRepository
import org.meshtastic.core.testing.FakeRadioController
import org.meshtastic.core.testing.FakeRadioInterfaceService
import org.meshtastic.core.testing.TestDataFactory
import org.meshtastic.feature.node.detail.NodeManagementActions
import org.meshtastic.feature.node.domain.usecase.GetFilteredNodesUseCase
@ -45,6 +46,7 @@ class NodeListViewModelTest {
private lateinit var viewModel: NodeListViewModel
private lateinit var nodeRepository: FakeNodeRepository
private lateinit var radioController: FakeRadioController
private lateinit var radioInterfaceService: FakeRadioInterfaceService
private val radioConfigRepository: RadioConfigRepository = mock(MockMode.autofill)
private val serviceRepository: ServiceRepository = mock(MockMode.autofill)
private val nodeFilterPreferences: NodeFilterPreferences = mock(MockMode.autofill)
@ -55,6 +57,7 @@ class NodeListViewModelTest {
fun setUp() {
nodeRepository = FakeNodeRepository()
radioController = FakeRadioController()
radioInterfaceService = FakeRadioInterfaceService()
every { radioConfigRepository.localConfigFlow } returns MutableStateFlow(org.meshtastic.proto.LocalConfig())
every { radioConfigRepository.deviceProfileFlow } returns MutableStateFlow(org.meshtastic.proto.DeviceProfile())
@ -79,6 +82,7 @@ class NodeListViewModelTest {
radioConfigRepository = radioConfigRepository,
serviceRepository = serviceRepository,
radioController = radioController,
radioInterfaceService = radioInterfaceService,
nodeManagementActions = nodeManagementActions,
getFilteredNodesUseCase = getFilteredNodesUseCase,
nodeFilterPreferences = nodeFilterPreferences,