diff --git a/app/src/main/java/com/geeksville/mesh/model/UIState.kt b/app/src/main/java/com/geeksville/mesh/model/UIState.kt index bbd86f6db..33c4eccdb 100644 --- a/app/src/main/java/com/geeksville/mesh/model/UIState.kt +++ b/app/src/main/java/com/geeksville/mesh/model/UIState.kt @@ -260,7 +260,7 @@ class UIViewModel @Inject constructor( .stateIn(viewModelScope, SharingStarted.WhileSubscribed(5_000), emptyList()) private val nodeFilterText = MutableStateFlow("") - private val nodeSortOption = MutableStateFlow(NodeSortOption.LAST_HEARD) + private val nodeSortOption = MutableStateFlow(NodeSortOption.VIA_FAVORITE) private val includeUnknown = MutableStateFlow(preferences.getBoolean("include-unknown", false)) private val showDetails = MutableStateFlow(preferences.getBoolean("show-details", false)) diff --git a/app/src/main/java/com/geeksville/mesh/ui/NodeItem.kt b/app/src/main/java/com/geeksville/mesh/ui/NodeItem.kt index 6d43119d7..512b51aa9 100644 --- a/app/src/main/java/com/geeksville/mesh/ui/NodeItem.kt +++ b/app/src/main/java/com/geeksville/mesh/ui/NodeItem.kt @@ -17,6 +17,7 @@ package com.geeksville.mesh.ui +import android.content.res.Configuration import androidx.compose.foundation.combinedClickable import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.layout.Arrangement @@ -57,13 +58,14 @@ import com.geeksville.mesh.ConfigProtos.Config.DeviceConfig import com.geeksville.mesh.ConfigProtos.Config.DisplayConfig import com.geeksville.mesh.MeshProtos import com.geeksville.mesh.R +import com.geeksville.mesh.model.DeviceVersion import com.geeksville.mesh.model.Node import com.geeksville.mesh.model.isUnmessageableRole import com.geeksville.mesh.ui.components.NodeKeyStatusIcon import com.geeksville.mesh.ui.components.NodeMenu import com.geeksville.mesh.ui.components.NodeMenuAction +import com.geeksville.mesh.ui.components.NodeStatusIcons import com.geeksville.mesh.ui.components.SignalInfo -import com.geeksville.mesh.ui.components.StatusIcons import com.geeksville.mesh.ui.compose.ElevationInfo import com.geeksville.mesh.ui.compose.SatelliteCountInfo import com.geeksville.mesh.ui.preview.NodePreviewParameterProvider @@ -84,11 +86,11 @@ fun NodeItem( currentTimeMillis: Long, isConnected: Boolean = false, ) { - val isFavorite = thatNode.isFavorite + val isFavorite = remember(thatNode) { thatNode.isFavorite } val isIgnored = thatNode.isIgnored val longName = thatNode.user.longName.ifEmpty { stringResource(id = R.string.unknown_username) } - val isThisNode = thisNode?.num == thatNode.num + val isThisNode = remember(thatNode) { thisNode?.num == thatNode.num } val system = remember(distanceUnits) { DisplayConfig.DisplayUnits.forNumber(distanceUnits) } val distance = remember(thisNode, thatNode) { thisNode?.distance(thatNode)?.takeIf { it > 0 }?.toDistanceString(system) @@ -112,10 +114,13 @@ fun NodeItem( } val (detailsShown, showDetails) = remember { mutableStateOf(expanded) } - val unmessageable = if (thatNode.user.hasIsUnmessagable()) { - thatNode.user.isUnmessagable - } else { - thatNode.user.role?.isUnmessageableRole() == true + val unmessageable = remember(thatNode) { + val firmwareVersion = DeviceVersion(thatNode.metadata?.firmwareVersion ?: "") + if (firmwareVersion >= DeviceVersion("2.6.8")) { + thatNode.user.isUnmessagable + } else { + thatNode.user.role?.isUnmessageableRole() == true + } } var menuExpanded by remember { mutableStateOf(false) } @@ -193,7 +198,11 @@ fun NodeItem( lastHeard = thatNode.lastHeard, currentTimeMillis = currentTimeMillis ) - StatusIcons(isFavorite = isFavorite, unmessageable = unmessageable) + NodeStatusIcons( + isThisNode = isThisNode, + isFavorite = isFavorite, + isUnmessageable = unmessageable + ) } Row( modifier = Modifier @@ -320,7 +329,7 @@ fun NodeInfoSimplePreview() { @Composable @Preview( showBackground = true, - uiMode = android.content.res.Configuration.UI_MODE_NIGHT_YES, + uiMode = Configuration.UI_MODE_NIGHT_YES, ) fun NodeInfoPreview( @PreviewParameter(NodePreviewParameterProvider::class) diff --git a/app/src/main/java/com/geeksville/mesh/ui/components/NodeStatusIcons.kt b/app/src/main/java/com/geeksville/mesh/ui/components/NodeStatusIcons.kt new file mode 100644 index 000000000..c8002c3a3 --- /dev/null +++ b/app/src/main/java/com/geeksville/mesh/ui/components/NodeStatusIcons.kt @@ -0,0 +1,114 @@ +/* + * 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.components + +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.rounded.NoCell +import androidx.compose.material.icons.rounded.Star +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.PlainTooltip +import androidx.compose.material3.Text +import androidx.compose.material3.TooltipBox +import androidx.compose.material3.TooltipDefaults +import androidx.compose.material3.rememberTooltipState +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import com.geeksville.mesh.R + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun NodeStatusIcons( + isThisNode: Boolean, + isUnmessageable: Boolean, + isFavorite: Boolean, +) { + Row( + modifier = Modifier.padding(4.dp) + ) { + if (isUnmessageable) { + TooltipBox( + positionProvider = TooltipDefaults.rememberPlainTooltipPositionProvider(), + tooltip = { + PlainTooltip { + Text(stringResource(R.string.unmonitored_or_infrastructure)) + } + }, + state = rememberTooltipState() + ) { + IconButton( + onClick = {}, + modifier = Modifier + .size(24.dp), + ) { + Icon( + imageVector = Icons.Rounded.NoCell, + contentDescription = stringResource(R.string.unmessageable), + modifier = Modifier + .size(24.dp), // Smaller size for badge + tint = MaterialTheme.colorScheme.error, + ) + } + } + } + if (isFavorite && !isThisNode) { + TooltipBox( + positionProvider = TooltipDefaults.rememberPlainTooltipPositionProvider(), + tooltip = { + PlainTooltip { + Text(stringResource(R.string.favorite)) + } + }, + state = rememberTooltipState() + ) { + IconButton( + onClick = {}, + modifier = Modifier + .size(24.dp), + ) { + Icon( + imageVector = Icons.Rounded.Star, + contentDescription = stringResource(R.string.favorite), + modifier = Modifier + .size(24.dp), // Smaller size for badge + tint = Color(color = 0xFFFEC30A) + ) + } + } + } + } +} + +@Preview +@Composable +fun StatusIconsPreview() { + NodeStatusIcons( + isThisNode = false, + isUnmessageable = true, + isFavorite = true, + ) +} diff --git a/app/src/main/java/com/geeksville/mesh/ui/components/StatusIcons.kt b/app/src/main/java/com/geeksville/mesh/ui/components/StatusIcons.kt deleted file mode 100644 index 565049f65..000000000 --- a/app/src/main/java/com/geeksville/mesh/ui/components/StatusIcons.kt +++ /dev/null @@ -1,69 +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.components - -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.size -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.filled.Star -import androidx.compose.material.icons.outlined.NoCell -import androidx.compose.material3.Icon -import androidx.compose.material3.MaterialTheme -import androidx.compose.runtime.Composable -import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.tooling.preview.Preview -import androidx.compose.ui.unit.dp -import com.geeksville.mesh.R - -@Composable -fun StatusIcons( - isFavorite: Boolean, - unmessageable: Boolean -) { - Row( - modifier = Modifier.padding(5.dp) - ) { - if (isFavorite) { - Icon( - imageVector = Icons.Filled.Star, - contentDescription = stringResource(R.string.favorite), - modifier = Modifier - .size(25.dp), // Smaller size for badge - tint = Color(color = 0xFFFEC30A) - ) - } - if (unmessageable) { - Icon( - imageVector = Icons.Outlined.NoCell, - contentDescription = stringResource(R.string.unmessageable), - modifier = Modifier - .size(25.dp), // Smaller size for badge - tint = MaterialTheme.colorScheme.error, - ) - } - } -} - -@Preview -@Composable -fun StatusIconsPreview() { - StatusIcons(isFavorite = true, unmessageable = true) -} diff --git a/app/src/main/java/com/geeksville/mesh/ui/radioconfig/components/UserConfigItemList.kt b/app/src/main/java/com/geeksville/mesh/ui/radioconfig/components/UserConfigItemList.kt index bbd70fb2d..74cd458d3 100644 --- a/app/src/main/java/com/geeksville/mesh/ui/radioconfig/components/UserConfigItemList.kt +++ b/app/src/main/java/com/geeksville/mesh/ui/radioconfig/components/UserConfigItemList.kt @@ -38,6 +38,8 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.geeksville.mesh.MeshProtos import com.geeksville.mesh.R import com.geeksville.mesh.copy +import com.geeksville.mesh.deviceMetadata +import com.geeksville.mesh.model.DeviceVersion import com.geeksville.mesh.ui.components.EditTextPreference import com.geeksville.mesh.ui.components.PreferenceCategory import com.geeksville.mesh.ui.components.PreferenceFooter @@ -63,17 +65,21 @@ fun UserConfigScreen( userConfig = state.userConfig, enabled = true, onSaveClicked = viewModel::setOwner, + metadata = state.metadata, ) } +@Suppress("LongMethod") @Composable fun UserConfigItemList( + metadata: MeshProtos.DeviceMetadata?, userConfig: MeshProtos.User, enabled: Boolean, onSaveClicked: (MeshProtos.User) -> Unit, ) { val focusManager = LocalFocusManager.current var userInput by rememberSaveable { mutableStateOf(userConfig) } + val firmwareVersion = DeviceVersion(metadata?.firmwareVersion ?: "") LazyColumn( modifier = Modifier.fillMaxSize() @@ -135,11 +141,13 @@ fun UserConfigItemList( title = stringResource(R.string.unmessageable), summary = stringResource(R.string.unmonitored_or_infrastructure), checked = userInput.isUnmessagable, - enabled = userInput.hasIsUnmessagable(), + enabled = firmwareVersion >= DeviceVersion("2.6.8"), onCheckedChange = { userInput = userInput.copy { isUnmessagable = it } } ) } + item { HorizontalDivider() } + item { SwitchPreference( title = stringResource(R.string.licensed_amateur_radio), @@ -179,5 +187,8 @@ private fun UserConfigPreview() { }, enabled = true, onSaveClicked = { }, + metadata = deviceMetadata { + firmwareVersion = "2.8.0" + } ) }