From a878373d14243789e0f103a91faed0dd455d9353 Mon Sep 17 00:00:00 2001 From: Phil Oliver <3497406+poliver@users.noreply.github.com> Date: Mon, 29 Sep 2025 16:54:28 -0400 Subject: [PATCH] Remove node card expansion (#3243) --- .../com/geeksville/mesh/ui/node/NodeScreen.kt | 21 +++-- .../geeksville/mesh/ui/node/NodesViewModel.kt | 23 +---- .../ui/node/components/NodeFilterTextField.kt | 27 ------ .../mesh/ui/node/components/NodeItem.kt | 87 ++----------------- .../org/meshtastic/core/prefs/ui/UiPrefs.kt | 2 - 5 files changed, 22 insertions(+), 138 deletions(-) diff --git a/app/src/main/java/com/geeksville/mesh/ui/node/NodeScreen.kt b/app/src/main/java/com/geeksville/mesh/ui/node/NodeScreen.kt index a17293996..31278f486 100644 --- a/app/src/main/java/com/geeksville/mesh/ui/node/NodeScreen.kt +++ b/app/src/main/java/com/geeksville/mesh/ui/node/NodeScreen.kt @@ -91,7 +91,9 @@ fun NodeScreen(nodesViewModel: NodesViewModel = hiltViewModel(), navigateToNodeD val currentTimeMillis = rememberTimeTickWithLifecycle() val connectionState by nodesViewModel.connectionState.collectAsStateWithLifecycle() - val isScrollInProgress by remember { derivedStateOf { listState.isScrollInProgress } } + val isScrollInProgress by remember { + derivedStateOf { listState.isScrollInProgress && (listState.canScrollForward || listState.canScrollBackward) } + } Scaffold( topBar = { MainAppBar( @@ -144,8 +146,6 @@ fun NodeScreen(nodesViewModel: NodesViewModel = hiltViewModel(), navigateToNodeD onToggleOnlyOnline = nodesViewModel::toggleOnlyOnline, onlyDirect = state.filter.onlyDirect, onToggleOnlyDirect = nodesViewModel::toggleOnlyDirect, - showDetails = state.showDetails, - onToggleShowDetails = nodesViewModel::toggleShowDetails, showIgnored = state.filter.showIgnored, onToggleShowIgnored = nodesViewModel::toggleShowIgnored, ignoredNodeCount = ignoredNodeCount, @@ -172,8 +172,14 @@ fun NodeScreen(nodesViewModel: NodesViewModel = hiltViewModel(), navigateToNodeD onConfirmRemove = { nodesViewModel.removeNode(it.num) }, ) - Box { + Box(modifier = Modifier.padding(horizontal = 8.dp, vertical = 4.dp)) { var showContextMenu by remember { mutableStateOf(false) } + val longClick = + if (node.num != ourNode?.num) { + { showContextMenu = true } + } else { + null + } NodeItem( modifier = Modifier.animateItem(), @@ -181,9 +187,8 @@ fun NodeScreen(nodesViewModel: NodesViewModel = hiltViewModel(), navigateToNodeD thatNode = node, distanceUnits = state.distanceUnits, tempInFahrenheit = state.tempInFahrenheit, - onClickChip = { navigateToNodeDetails(it.num) }, - onLongClick = { showContextMenu = true }, - expanded = state.showDetails, + onClick = { navigateToNodeDetails(node.num) }, + onLongClick = longClick, currentTimeMillis = currentTimeMillis, isConnected = connectionState.isConnected(), ) @@ -213,7 +218,7 @@ private fun ContextMenu( onClickRemove: (Node) -> Unit, onDismiss: () -> Unit, ) { - DropdownMenu(expanded = expanded, onDismissRequest = onDismiss, offset = DpOffset(16.dp, 0.dp)) { + DropdownMenu(expanded = expanded, onDismissRequest = onDismiss, offset = DpOffset(x = 0.dp, y = 8.dp)) { val isFavorite = node.isFavorite val isIgnored = node.isIgnored diff --git a/app/src/main/java/com/geeksville/mesh/ui/node/NodesViewModel.kt b/app/src/main/java/com/geeksville/mesh/ui/node/NodesViewModel.kt index 6ba62efd2..ff0b77fee 100644 --- a/app/src/main/java/com/geeksville/mesh/ui/node/NodesViewModel.kt +++ b/app/src/main/java/com/geeksville/mesh/ui/node/NodesViewModel.kt @@ -34,14 +34,12 @@ import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.flatMapLatest import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.stateIn -import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch import org.meshtastic.core.data.repository.NodeRepository import org.meshtastic.core.data.repository.RadioConfigRepository import org.meshtastic.core.database.model.Node import org.meshtastic.core.database.model.NodeSortOption import org.meshtastic.core.datastore.UiPreferencesDataSource -import org.meshtastic.core.prefs.ui.UiPrefs import timber.log.Timber import javax.inject.Inject @@ -52,7 +50,6 @@ constructor( private val nodeRepository: NodeRepository, radioConfigRepository: RadioConfigRepository, private val serviceRepository: ServiceRepository, - private val uiPrefs: UiPrefs, private val uiPreferencesDataSource: UiPreferencesDataSource, ) : ViewModel() { @@ -97,21 +94,13 @@ constructor( NodeFilterState(filterText, includeUnknown, onlyOnline, onlyDirect, showIgnored) } - private val showDetails = MutableStateFlow(uiPrefs.showDetails) - val nodesUiState: StateFlow = - combine(nodeSortOption, nodeFilter, showDetails, radioConfigRepository.deviceProfileFlow) { - sort, - nodeFilter, - showDetails, - profile, - -> + combine(nodeSortOption, nodeFilter, radioConfigRepository.deviceProfileFlow) { sort, nodeFilter, profile -> NodesUiState( sort = sort, filter = nodeFilter, distanceUnits = profile.config.display.units.number, tempInFahrenheit = profile.moduleConfig.telemetry.environmentDisplayFahrenheit, - showDetails = showDetails, ) } .stateIn( @@ -172,8 +161,6 @@ constructor( uiPreferencesDataSource.setNodeSort(sort.ordinal) } - fun toggleShowDetails() = toggle(showDetails) { uiPrefs.showDetails = it } - fun addSharedContact(sharedContact: AdminProtos.SharedContact) = viewModelScope.launch { serviceRepository.onServiceAction(ServiceAction.AddSharedContact(sharedContact)) } @@ -181,13 +168,6 @@ constructor( _sharedContactRequested.value = sharedContact } - private fun toggle(state: MutableStateFlow, onChanged: (newValue: Boolean) -> Unit) { - (!state.value).let { toggled -> - state.update { toggled } - onChanged(toggled) - } - } - fun favoriteNode(node: Node) = viewModelScope.launch { try { serviceRepository.onServiceAction(ServiceAction.Favorite(node)) @@ -221,7 +201,6 @@ data class NodesUiState( val filter: NodeFilterState = NodeFilterState(), val distanceUnits: Int = 0, val tempInFahrenheit: Boolean = false, - val showDetails: Boolean = false, ) data class NodeFilterState( diff --git a/app/src/main/java/com/geeksville/mesh/ui/node/components/NodeFilterTextField.kt b/app/src/main/java/com/geeksville/mesh/ui/node/components/NodeFilterTextField.kt index 9385719d0..17a0ae966 100644 --- a/app/src/main/java/com/geeksville/mesh/ui/node/components/NodeFilterTextField.kt +++ b/app/src/main/java/com/geeksville/mesh/ui/node/components/NodeFilterTextField.kt @@ -75,8 +75,6 @@ fun NodeFilterTextField( onToggleOnlyOnline: () -> Unit, onlyDirect: Boolean, onToggleOnlyDirect: () -> Unit, - showDetails: Boolean, - onToggleShowDetails: () -> Unit, showIgnored: Boolean, onToggleShowIgnored: () -> Unit, ignoredNodeCount: Int, @@ -97,8 +95,6 @@ fun NodeFilterTextField( onToggleOnlyOnline = onToggleOnlyOnline, onlyDirect = onlyDirect, onToggleOnlyDirect = onToggleOnlyDirect, - showDetails = showDetails, - onToggleShowDetails = onToggleShowDetails, showIgnored = showIgnored, onToggleShowIgnored = onToggleShowIgnored, ignoredNodeCount = ignoredNodeCount, @@ -258,25 +254,6 @@ private fun NodeSortButton( }, ) HorizontalDivider() - DropdownMenuItem( - onClick = { - toggles.onToggleShowDetails() - expanded = false - }, - text = { - Row { - AnimatedVisibility(visible = toggles.showDetails) { - Icon( - imageVector = Icons.Default.Done, - contentDescription = null, - modifier = Modifier.padding(end = 4.dp), - ) - } - Text(text = stringResource(id = R.string.node_filter_show_details)) - } - }, - ) - HorizontalDivider() DropdownMenuItem( onClick = { toggles.onToggleShowIgnored() @@ -321,8 +298,6 @@ private fun NodeFilterTextFieldPreview() { onToggleOnlyOnline = {}, onlyDirect = false, onToggleOnlyDirect = {}, - showDetails = false, - onToggleShowDetails = {}, showIgnored = false, onToggleShowIgnored = {}, ignoredNodeCount = 0, @@ -337,8 +312,6 @@ data class NodeFilterToggles( val onToggleOnlyOnline: () -> Unit, val onlyDirect: Boolean, val onToggleOnlyDirect: () -> Unit, - val showDetails: Boolean, - val onToggleShowDetails: () -> Unit, val showIgnored: Boolean, val onToggleShowIgnored: () -> Unit, val ignoredNodeCount: Int, diff --git a/app/src/main/java/com/geeksville/mesh/ui/node/components/NodeItem.kt b/app/src/main/java/com/geeksville/mesh/ui/node/components/NodeItem.kt index 46d0be8c3..b9444d9fc 100644 --- a/app/src/main/java/com/geeksville/mesh/ui/node/components/NodeItem.kt +++ b/app/src/main/java/com/geeksville/mesh/ui/node/components/NodeItem.kt @@ -31,27 +31,22 @@ import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.width import androidx.compose.material3.Card import androidx.compose.material3.CardDefaults -import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.LocalTextStyle import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.material3.contentColorFor import androidx.compose.runtime.Composable -import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.font.FontStyle -import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextDecoration import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.unit.dp -import com.geeksville.mesh.ConfigProtos.Config.DeviceConfig import com.geeksville.mesh.ConfigProtos.Config.DisplayConfig -import com.geeksville.mesh.MeshProtos import com.geeksville.mesh.ui.common.components.SignalInfo import com.geeksville.mesh.ui.common.preview.NodePreviewParameterProvider import org.meshtastic.core.database.model.Node @@ -69,9 +64,8 @@ fun NodeItem( distanceUnits: Int, tempInFahrenheit: Boolean, modifier: Modifier = Modifier, - onClickChip: (Node) -> Unit = {}, - onLongClick: () -> Unit = {}, - expanded: Boolean = false, + onClick: () -> Unit = {}, + onLongClick: (() -> Unit)? = null, currentTimeMillis: Long, isConnected: Boolean = false, ) { @@ -83,18 +77,6 @@ fun NodeItem( val distance = remember(thisNode, thatNode) { thisNode?.distance(thatNode)?.takeIf { it > 0 }?.toDistanceString(system) } - val hwInfoString = - when (val hwModel = thatNode.user.hwModel) { - MeshProtos.HardwareModel.UNSET -> MeshProtos.HardwareModel.UNSET.name - else -> hwModel.name.replace('_', '-').replace('p', '.').lowercase() - } - val roleName = - if (thatNode.isUnknownUser) { - DeviceConfig.Role.UNRECOGNIZED.name - } else { - thatNode.user.role.name - } - val style = if (thatNode.isUnknownUser) { LocalTextStyle.current.copy(fontStyle = FontStyle.Italic) @@ -114,7 +96,6 @@ fun NodeItem( .copy(containerColor = containerColor, contentColor = contentColorFor(containerColor)) } ?: (CardDefaults.cardColors()) - val (detailsShown, showDetails) = remember { mutableStateOf(expanded) } val unmessageable = remember(thatNode) { when { @@ -123,18 +104,13 @@ fun NodeItem( } } - Card( - modifier = - modifier - .combinedClickable(onClick = { showDetails(!detailsShown) }, onLongClick = onLongClick) - .fillMaxWidth() - .padding(horizontal = 8.dp, vertical = 4.dp) - .defaultMinSize(minHeight = 80.dp), - colors = cardColors, - ) { - Column(modifier = Modifier.fillMaxWidth().padding(8.dp)) { + Card(modifier = modifier.fillMaxWidth().defaultMinSize(minHeight = 80.dp), colors = cardColors) { + Column( + modifier = + Modifier.combinedClickable(onClick = onClick, onLongClick = onLongClick).fillMaxWidth().padding(8.dp), + ) { Row(modifier = Modifier.fillMaxWidth(), verticalAlignment = Alignment.CenterVertically) { - NodeChip(node = thatNode, onClick = onClickChip) + NodeChip(node = thatNode) NodeKeyStatusIcon( hasPKC = thatNode.hasPKC, @@ -190,51 +166,6 @@ fun NodeItem( ) } } - - if (detailsShown || expanded) { - Spacer(modifier = Modifier.height(8.dp)) - HorizontalDivider() - Spacer(modifier = Modifier.height(8.dp)) - Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween) { - thatNode.validPosition?.let { - LinkedCoordinates( - latitude = thatNode.latitude, - longitude = thatNode.longitude, - nodeName = longName, - ) - } - thatNode.validPosition?.let { position -> - ElevationInfo( - altitude = position.altitude, - system = system, - suffix = stringResource(id = R.string.elevation_suffix), - ) - } - } - Spacer(modifier = Modifier.height(4.dp)) - Row(modifier = Modifier.fillMaxWidth()) { - Text( - modifier = Modifier.weight(1f), - text = hwInfoString, - fontSize = MaterialTheme.typography.labelLarge.fontSize, - style = style, - ) - Text( - modifier = Modifier.weight(1f), - text = roleName, - textAlign = TextAlign.Center, - fontSize = MaterialTheme.typography.labelLarge.fontSize, - style = style, - ) - Text( - modifier = Modifier.weight(1f), - text = thatNode.user.id.ifEmpty { "???" }, - textAlign = TextAlign.End, - fontSize = MaterialTheme.typography.labelLarge.fontSize, - style = style, - ) - } - } } } } @@ -261,7 +192,6 @@ fun NodeInfoPreview(@PreviewParameter(NodePreviewParameterProvider::class) thatN thatNode = thatNode, distanceUnits = 1, tempInFahrenheit = true, - expanded = false, currentTimeMillis = System.currentTimeMillis(), ) Text(text = "Details Shown", color = MaterialTheme.colorScheme.onBackground) @@ -270,7 +200,6 @@ fun NodeInfoPreview(@PreviewParameter(NodePreviewParameterProvider::class) thatN thatNode = thatNode, distanceUnits = 1, tempInFahrenheit = true, - expanded = true, currentTimeMillis = System.currentTimeMillis(), ) } diff --git a/core/prefs/src/main/kotlin/org/meshtastic/core/prefs/ui/UiPrefs.kt b/core/prefs/src/main/kotlin/org/meshtastic/core/prefs/ui/UiPrefs.kt index 1da38bcea..138a4afa5 100644 --- a/core/prefs/src/main/kotlin/org/meshtastic/core/prefs/ui/UiPrefs.kt +++ b/core/prefs/src/main/kotlin/org/meshtastic/core/prefs/ui/UiPrefs.kt @@ -30,7 +30,6 @@ import javax.inject.Singleton interface UiPrefs { var hasShownNotPairedWarning: Boolean - var showDetails: Boolean var showQuickChat: Boolean fun shouldProvideNodeLocation(nodeNum: Int): StateFlow @@ -63,7 +62,6 @@ class UiPrefsImpl @Inject constructor(@UiSharedPreferences private val prefs: Sh } override var hasShownNotPairedWarning: Boolean by PrefDelegate(prefs, "has_shown_not_paired_warning", false) - override var showDetails: Boolean by PrefDelegate(prefs, "show-details", false) override var showQuickChat: Boolean by PrefDelegate(prefs, "show-quick-chat", false) override fun shouldProvideNodeLocation(nodeNum: Int): StateFlow = provideNodeLocationFlows