Add "Exclude MQTT" filter to Nodes view. (#4825)

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: James Rich <2199651+jamesarich@users.noreply.github.com>
Co-authored-by: James Rich <james.a.rich@gmail.com>
This commit is contained in:
Victorio Berra
2026-03-18 10:39:59 -05:00
committed by GitHub
parent 1e9e838025
commit eae5a6bdac
9 changed files with 73 additions and 2 deletions

View File

@@ -44,6 +44,7 @@ const val KEY_EXCLUDE_INFRASTRUCTURE = "exclude-infrastructure"
const val KEY_ONLY_ONLINE = "only-online"
const val KEY_ONLY_DIRECT = "only-direct"
const val KEY_SHOW_IGNORED = "show-ignored"
const val KEY_EXCLUDE_MQTT = "exclude-mqtt"
@Single
@Suppress("TooManyFunctions") // One setter per preference field — inherently grows with preferences.
@@ -73,6 +74,7 @@ class UiPreferencesDataSource(@Named("CorePreferencesDataStore") private val dat
val onlyOnline: StateFlow<Boolean> = dataStore.prefStateFlow(key = ONLY_ONLINE, default = false)
val onlyDirect: StateFlow<Boolean> = dataStore.prefStateFlow(key = ONLY_DIRECT, default = false)
val showIgnored: StateFlow<Boolean> = dataStore.prefStateFlow(key = SHOW_IGNORED, default = false)
val excludeMqtt: StateFlow<Boolean> = dataStore.prefStateFlow(key = EXCLUDE_MQTT, default = false)
fun setAppIntroCompleted(completed: Boolean) {
dataStore.setPref(key = APP_INTRO_COMPLETED, value = completed)
@@ -106,6 +108,10 @@ class UiPreferencesDataSource(@Named("CorePreferencesDataStore") private val dat
dataStore.setPref(key = SHOW_IGNORED, value = value)
}
fun setExcludeMqtt(value: Boolean) {
dataStore.setPref(key = EXCLUDE_MQTT, value = value)
}
private fun <T : Any> DataStore<Preferences>.prefStateFlow(
key: Preferences.Key<T>,
default: T,
@@ -126,5 +132,6 @@ class UiPreferencesDataSource(@Named("CorePreferencesDataStore") private val dat
val ONLY_ONLINE = booleanPreferencesKey(KEY_ONLY_ONLINE)
val ONLY_DIRECT = booleanPreferencesKey(KEY_ONLY_DIRECT)
val SHOW_IGNORED = booleanPreferencesKey(KEY_SHOW_IGNORED)
val EXCLUDE_MQTT = booleanPreferencesKey(KEY_EXCLUDE_MQTT)
}
}

View File

@@ -53,6 +53,7 @@
<string name="internal">Internal</string>
<string name="node_sort_via_favorite">via Favorite</string>
<string name="node_filter_show_ignored">Only show ignored Nodes</string>
<string name="node_filter_exclude_mqtt">Exclude MQTT</string>
<string name="unrecognized">Unrecognized</string>
<string name="message_status_enroute">Waiting to be acknowledged</string>
<string name="message_status_queued">Queued for sending</string>

View File

@@ -150,6 +150,8 @@ fun DesktopAdaptiveNodeListScreen(
showIgnored = state.filter.showIgnored,
onToggleShowIgnored = { viewModel.nodeFilterPreferences.toggleShowIgnored() },
ignoredNodeCount = ignoredNodeCount,
excludeMqtt = state.filter.excludeMqtt,
onToggleExcludeMqtt = { viewModel.nodeFilterPreferences.toggleExcludeMqtt() },
)
}

View File

@@ -163,6 +163,8 @@ fun NodeListScreen(
showIgnored = state.filter.showIgnored,
onToggleShowIgnored = { viewModel.nodeFilterPreferences.toggleShowIgnored() },
ignoredNodeCount = ignoredNodeCount,
excludeMqtt = state.filter.excludeMqtt,
onToggleExcludeMqtt = { viewModel.nodeFilterPreferences.toggleExcludeMqtt() },
)
}

View File

@@ -62,6 +62,7 @@ import org.meshtastic.core.model.NodeSortOption
import org.meshtastic.core.resources.Res
import org.meshtastic.core.resources.desc_node_filter_clear
import org.meshtastic.core.resources.node_filter_exclude_infrastructure
import org.meshtastic.core.resources.node_filter_exclude_mqtt
import org.meshtastic.core.resources.node_filter_ignored
import org.meshtastic.core.resources.node_filter_include_unknown
import org.meshtastic.core.resources.node_filter_only_direct
@@ -91,6 +92,8 @@ fun NodeFilterTextField(
showIgnored: Boolean,
onToggleShowIgnored: () -> Unit,
ignoredNodeCount: Int,
excludeMqtt: Boolean,
onToggleExcludeMqtt: () -> Unit,
) {
Column(modifier = modifier.background(MaterialTheme.colorScheme.background)) {
Row {
@@ -113,6 +116,8 @@ fun NodeFilterTextField(
showIgnored = showIgnored,
onToggleShowIgnored = onToggleShowIgnored,
ignoredNodeCount = ignoredNodeCount,
excludeMqtt = excludeMqtt,
onToggleExcludeMqtt = onToggleExcludeMqtt,
),
)
}
@@ -148,6 +153,8 @@ data class NodeFilterToggles(
val showIgnored: Boolean,
val onToggleShowIgnored: () -> Unit,
val ignoredNodeCount: Int,
val excludeMqtt: Boolean,
val onToggleExcludeMqtt: () -> Unit,
)
@Composable
@@ -268,6 +275,12 @@ private fun NodeSortButton(
null
},
)
DropdownMenuCheck(
text = stringResource(Res.string.node_filter_exclude_mqtt),
checked = toggles.excludeMqtt,
onClick = toggles.onToggleExcludeMqtt,
)
}
}

View File

@@ -57,5 +57,6 @@ class GetFilteredNodesUseCase constructor(private val nodeRepository: NodeReposi
true
}
}
.filter { node -> if (filter.excludeMqtt) !node.viaMqtt else true }
}
}

View File

@@ -28,6 +28,7 @@ class NodeFilterPreferences constructor(private val uiPreferencesDataSource: UiP
val onlyOnline = uiPreferencesDataSource.onlyOnline
val onlyDirect = uiPreferencesDataSource.onlyDirect
val showIgnored = uiPreferencesDataSource.showIgnored
val excludeMqtt = uiPreferencesDataSource.excludeMqtt
val nodeSortOption =
uiPreferencesDataSource.nodeSort.map { NodeSortOption.entries.getOrElse(it) { NodeSortOption.VIA_FAVORITE } }
@@ -55,4 +56,8 @@ class NodeFilterPreferences constructor(private val uiPreferencesDataSource: UiP
fun toggleShowIgnored() {
uiPreferencesDataSource.setShowIgnored(!showIgnored.value)
}
fun toggleExcludeMqtt() {
uiPreferencesDataSource.setExcludeMqtt(!excludeMqtt.value)
}
}

View File

@@ -91,7 +91,11 @@ class NodeListViewModel(
}
private val nodeFilter: Flow<NodeFilterState> =
combine(_nodeFilterText, filterToggles) { filterText, filterToggles ->
combine(_nodeFilterText, filterToggles, nodeFilterPreferences.excludeMqtt) {
filterText,
filterToggles,
excludeMqtt,
->
NodeFilterState(
filterText = filterText,
includeUnknown = filterToggles.includeUnknown,
@@ -99,6 +103,7 @@ class NodeListViewModel(
onlyOnline = filterToggles.onlyOnline,
onlyDirect = filterToggles.onlyDirect,
showIgnored = filterToggles.showIgnored,
excludeMqtt = excludeMqtt,
)
}
val nodesUiState: StateFlow<NodesUiState> =
@@ -183,6 +188,7 @@ data class NodeFilterState(
val onlyOnline: Boolean = false,
val onlyDirect: Boolean = false,
val showIgnored: Boolean = false,
val excludeMqtt: Boolean = false,
)
data class NodeFilterToggles(

View File

@@ -47,9 +47,10 @@ class GetFilteredNodesUseCaseTest {
role: Config.DeviceConfig.Role = Config.DeviceConfig.Role.CLIENT,
ignored: Boolean = false,
name: String = "Node$num",
viaMqtt: Boolean = false,
): Node {
val user = User(id = "!$num", long_name = name, short_name = "N$num", role = role)
return Node(num = num, user = user, isIgnored = ignored)
return Node(num = num, user = user, isIgnored = ignored, viaMqtt = viaMqtt)
}
@Test
@@ -116,4 +117,37 @@ class GetFilteredNodesUseCaseTest {
assertEquals(1, result.size)
assertEquals(1, result.first().num)
}
@Test
fun `invoke filters out MQTT nodes if excludeMqtt is true`() = runTest {
// Arrange
val loraNode = createNode(1, viaMqtt = false)
val mqttNode = createNode(2, viaMqtt = true)
val filter = NodeFilterState(excludeMqtt = true)
every { nodeRepository.getNodes(any(), any(), any(), any(), any()) } returns flowOf(listOf(loraNode, mqttNode))
// Act
val result = useCase(filter, NodeSortOption.LAST_HEARD).first()
// Assert
assertEquals(1, result.size)
assertEquals(1, result.first().num)
}
@Test
fun `invoke keeps MQTT nodes if excludeMqtt is false`() = runTest {
// Arrange
val loraNode = createNode(1, viaMqtt = false)
val mqttNode = createNode(2, viaMqtt = true)
val filter = NodeFilterState(excludeMqtt = false)
every { nodeRepository.getNodes(any(), any(), any(), any(), any()) } returns flowOf(listOf(loraNode, mqttNode))
// Act
val result = useCase(filter, NodeSortOption.LAST_HEARD).first()
// Assert
assertEquals(2, result.size)
}
}