From eae5a6bdac497c226c2f8abbf31799fd5e3edd24 Mon Sep 17 00:00:00 2001 From: Victorio Berra <2934507+VictorioBerra@users.noreply.github.com> Date: Wed, 18 Mar 2026 10:39:59 -0500 Subject: [PATCH] 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 --- .../core/datastore/UiPreferencesDataSource.kt | 7 ++++ .../composeResources/values/strings.xml | 1 + .../ui/nodes/DesktopAdaptiveNodeListScreen.kt | 2 ++ .../feature/node/list/NodeListScreen.kt | 2 ++ .../node/component/NodeFilterTextField.kt | 13 +++++++ .../domain/usecase/GetFilteredNodesUseCase.kt | 1 + .../node/list/NodeFilterPreferences.kt | 5 +++ .../feature/node/list/NodeListViewModel.kt | 8 ++++- .../usecase/GetFilteredNodesUseCaseTest.kt | 36 ++++++++++++++++++- 9 files changed, 73 insertions(+), 2 deletions(-) diff --git a/core/datastore/src/commonMain/kotlin/org/meshtastic/core/datastore/UiPreferencesDataSource.kt b/core/datastore/src/commonMain/kotlin/org/meshtastic/core/datastore/UiPreferencesDataSource.kt index 64dfc8abf..6801cb340 100644 --- a/core/datastore/src/commonMain/kotlin/org/meshtastic/core/datastore/UiPreferencesDataSource.kt +++ b/core/datastore/src/commonMain/kotlin/org/meshtastic/core/datastore/UiPreferencesDataSource.kt @@ -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 = dataStore.prefStateFlow(key = ONLY_ONLINE, default = false) val onlyDirect: StateFlow = dataStore.prefStateFlow(key = ONLY_DIRECT, default = false) val showIgnored: StateFlow = dataStore.prefStateFlow(key = SHOW_IGNORED, default = false) + val excludeMqtt: StateFlow = 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 DataStore.prefStateFlow( key: Preferences.Key, 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) } } diff --git a/core/resources/src/commonMain/composeResources/values/strings.xml b/core/resources/src/commonMain/composeResources/values/strings.xml index 82a361465..fed685b53 100644 --- a/core/resources/src/commonMain/composeResources/values/strings.xml +++ b/core/resources/src/commonMain/composeResources/values/strings.xml @@ -53,6 +53,7 @@ Internal via Favorite Only show ignored Nodes + Exclude MQTT Unrecognized Waiting to be acknowledged Queued for sending diff --git a/desktop/src/main/kotlin/org/meshtastic/desktop/ui/nodes/DesktopAdaptiveNodeListScreen.kt b/desktop/src/main/kotlin/org/meshtastic/desktop/ui/nodes/DesktopAdaptiveNodeListScreen.kt index 8f2999e96..9ea892ae0 100644 --- a/desktop/src/main/kotlin/org/meshtastic/desktop/ui/nodes/DesktopAdaptiveNodeListScreen.kt +++ b/desktop/src/main/kotlin/org/meshtastic/desktop/ui/nodes/DesktopAdaptiveNodeListScreen.kt @@ -150,6 +150,8 @@ fun DesktopAdaptiveNodeListScreen( showIgnored = state.filter.showIgnored, onToggleShowIgnored = { viewModel.nodeFilterPreferences.toggleShowIgnored() }, ignoredNodeCount = ignoredNodeCount, + excludeMqtt = state.filter.excludeMqtt, + onToggleExcludeMqtt = { viewModel.nodeFilterPreferences.toggleExcludeMqtt() }, ) } diff --git a/feature/node/src/androidMain/kotlin/org/meshtastic/feature/node/list/NodeListScreen.kt b/feature/node/src/androidMain/kotlin/org/meshtastic/feature/node/list/NodeListScreen.kt index fb6d9710f..205d56f48 100644 --- a/feature/node/src/androidMain/kotlin/org/meshtastic/feature/node/list/NodeListScreen.kt +++ b/feature/node/src/androidMain/kotlin/org/meshtastic/feature/node/list/NodeListScreen.kt @@ -163,6 +163,8 @@ fun NodeListScreen( showIgnored = state.filter.showIgnored, onToggleShowIgnored = { viewModel.nodeFilterPreferences.toggleShowIgnored() }, ignoredNodeCount = ignoredNodeCount, + excludeMqtt = state.filter.excludeMqtt, + onToggleExcludeMqtt = { viewModel.nodeFilterPreferences.toggleExcludeMqtt() }, ) } diff --git a/feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/component/NodeFilterTextField.kt b/feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/component/NodeFilterTextField.kt index 6cf1340bf..f40acd33b 100644 --- a/feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/component/NodeFilterTextField.kt +++ b/feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/component/NodeFilterTextField.kt @@ -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, + ) } } diff --git a/feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/domain/usecase/GetFilteredNodesUseCase.kt b/feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/domain/usecase/GetFilteredNodesUseCase.kt index 039939871..6df461c8e 100644 --- a/feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/domain/usecase/GetFilteredNodesUseCase.kt +++ b/feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/domain/usecase/GetFilteredNodesUseCase.kt @@ -57,5 +57,6 @@ class GetFilteredNodesUseCase constructor(private val nodeRepository: NodeReposi true } } + .filter { node -> if (filter.excludeMqtt) !node.viaMqtt else true } } } diff --git a/feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/list/NodeFilterPreferences.kt b/feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/list/NodeFilterPreferences.kt index e11721371..7e7b5867f 100644 --- a/feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/list/NodeFilterPreferences.kt +++ b/feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/list/NodeFilterPreferences.kt @@ -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) + } } diff --git a/feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/list/NodeListViewModel.kt b/feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/list/NodeListViewModel.kt index 83dfeea9a..c486b3ca6 100644 --- a/feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/list/NodeListViewModel.kt +++ b/feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/list/NodeListViewModel.kt @@ -91,7 +91,11 @@ class NodeListViewModel( } private val nodeFilter: Flow = - 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 = @@ -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( diff --git a/feature/node/src/test/kotlin/org/meshtastic/feature/node/domain/usecase/GetFilteredNodesUseCaseTest.kt b/feature/node/src/test/kotlin/org/meshtastic/feature/node/domain/usecase/GetFilteredNodesUseCaseTest.kt index 8ab7cbf06..246d4c9fd 100644 --- a/feature/node/src/test/kotlin/org/meshtastic/feature/node/domain/usecase/GetFilteredNodesUseCaseTest.kt +++ b/feature/node/src/test/kotlin/org/meshtastic/feature/node/domain/usecase/GetFilteredNodesUseCaseTest.kt @@ -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) + } }