From 8a52212bcabd16cc80d78e746dc0eb5a971e64c9 Mon Sep 17 00:00:00 2001 From: James Rich Date: Tue, 24 Mar 2026 12:43:38 -0500 Subject: [PATCH] refactor: Centralize snackbar management and decouple UI feedback from feature modules - Introduce `SnackbarManager` and `MeshtasticSnackbarHost` to provide a unified, global mechanism for displaying transient messages across Android and Desktop platforms. - Integrate `SnackbarManager` into `UIViewModel` and inject it into `CommonNodeRequestActions` to allow business logic to trigger feedback without direct UI dependencies. - Remove localized `SnackbarHostState` and `LaunchedEffect` collectors from various screens in the `feature:node` module, including `PositionLog`, `PaxMetrics`, `DeviceMetrics`, and `TracerouteLog`. - Delete `NodeRequestEffect` and refactor `NodeRequestActions` to utilize the global snackbar manager for request feedback. - Update the top-level UI entry points in `Main.kt` (Android) and `DesktopMainScreen.kt` to host the shared `MeshtasticSnackbarHost`. - Add comprehensive unit tests for `SnackbarManager` in `core:ui` to verify event buffering, action callbacks, and duration logic. - Clean up related ViewModel tests to reflect the removal of the `effects` flow. --- .../main/kotlin/org/meshtastic/app/ui/Main.kt | 57 +++++---- .../ui/component/MeshtasticSnackbarHost.kt | 57 +++++++++ .../core/ui/util/SnackbarManager.kt | 65 +++++++++++ .../core/ui/viewmodel/UIViewModel.kt | 10 ++ .../core/ui/util/SnackbarManagerTest.kt | 108 ++++++++++++++++++ .../desktop/ui/DesktopMainScreen.kt | 83 ++++++++------ .../feature/node/detail/NodeDetailScreen.kt | 14 --- .../feature/node/metrics/PositionLog.kt | 18 --- .../node/detail/CommonNodeRequestActions.kt | 44 +++---- .../node/detail/NodeDetailViewModel.kt | 2 - .../feature/node/detail/NodeRequestActions.kt | 7 -- .../feature/node/metrics/BaseMetricChart.kt | 4 - .../feature/node/metrics/DeviceMetrics.kt | 14 --- .../node/metrics/EnvironmentMetrics.kt | 17 --- .../feature/node/metrics/HostMetricsLog.kt | 18 --- .../feature/node/metrics/MetricsViewModel.kt | 3 - .../feature/node/metrics/NeighborInfoLog.kt | 17 --- .../feature/node/metrics/PaxMetrics.kt | 15 --- .../feature/node/metrics/PowerMetrics.kt | 15 --- .../feature/node/metrics/SignalMetrics.kt | 15 --- .../feature/node/metrics/TracerouteLog.kt | 17 --- .../node/detail/NodeDetailViewModelTest.kt | 1 - .../node/metrics/MetricsViewModelTest.kt | 1 - 23 files changed, 333 insertions(+), 269 deletions(-) create mode 100644 core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/component/MeshtasticSnackbarHost.kt create mode 100644 core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/util/SnackbarManager.kt create mode 100644 core/ui/src/commonTest/kotlin/org/meshtastic/core/ui/util/SnackbarManagerTest.kt diff --git a/app/src/main/kotlin/org/meshtastic/app/ui/Main.kt b/app/src/main/kotlin/org/meshtastic/app/ui/Main.kt index 69eefcd30..47b4b59a5 100644 --- a/app/src/main/kotlin/org/meshtastic/app/ui/Main.kt +++ b/app/src/main/kotlin/org/meshtastic/app/ui/Main.kt @@ -24,6 +24,7 @@ import androidx.compose.animation.fadeIn import androidx.compose.animation.fadeOut import androidx.compose.animation.scaleIn import androidx.compose.animation.scaleOut +import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.height @@ -55,6 +56,7 @@ import androidx.compose.runtime.mutableIntStateOf import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp import androidx.lifecycle.compose.collectAsStateWithLifecycle @@ -91,6 +93,7 @@ import org.meshtastic.core.resources.traceroute import org.meshtastic.core.resources.view_on_map import org.meshtastic.core.service.MeshService import org.meshtastic.core.ui.component.AlertHost +import org.meshtastic.core.ui.component.MeshtasticSnackbarHost import org.meshtastic.core.ui.component.ScrollToTopEvent import org.meshtastic.core.ui.component.SharedDialogs import org.meshtastic.core.ui.navigation.icon @@ -316,30 +319,36 @@ fun MainScreen(uIViewModel: UIViewModel = koinViewModel(), scanModel: ScannerVie } }, ) { - val provider = - entryProvider { - contactsGraph(backStack, uIViewModel.scrollToTopEventFlow) - nodesGraph( - backStack = backStack, - scrollToTopEvents = uIViewModel.scrollToTopEventFlow, - nodeMapScreen = { destNum, onNavigateUp -> - val vm = - org.koin.compose.viewmodel.koinViewModel() - vm.setDestNum(destNum) - org.meshtastic.app.map.node.NodeMapScreen(vm, onNavigateUp = onNavigateUp) - }, - ) - mapGraph(backStack) - channelsGraph(backStack) - connectionsGraph(backStack) - settingsGraph(backStack) - firmwareGraph(backStack) - } - NavDisplay( - backStack = backStack, - entryProvider = provider, - modifier = Modifier.fillMaxSize().recalculateWindowInsets().safeDrawingPadding(), - ) + Box(modifier = Modifier.fillMaxSize()) { + val provider = + entryProvider { + contactsGraph(backStack, uIViewModel.scrollToTopEventFlow) + nodesGraph( + backStack = backStack, + scrollToTopEvents = uIViewModel.scrollToTopEventFlow, + nodeMapScreen = { destNum, onNavigateUp -> + val vm = + org.koin.compose.viewmodel.koinViewModel() + vm.setDestNum(destNum) + org.meshtastic.app.map.node.NodeMapScreen(vm, onNavigateUp = onNavigateUp) + }, + ) + mapGraph(backStack) + channelsGraph(backStack) + connectionsGraph(backStack) + settingsGraph(backStack) + firmwareGraph(backStack) + } + NavDisplay( + backStack = backStack, + entryProvider = provider, + modifier = Modifier.fillMaxSize().recalculateWindowInsets().safeDrawingPadding(), + ) + MeshtasticSnackbarHost( + snackbarManager = uIViewModel.snackbarManager, + modifier = Modifier.align(Alignment.BottomCenter), + ) + } } } diff --git a/core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/component/MeshtasticSnackbarHost.kt b/core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/component/MeshtasticSnackbarHost.kt new file mode 100644 index 000000000..368ce8e7c --- /dev/null +++ b/core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/component/MeshtasticSnackbarHost.kt @@ -0,0 +1,57 @@ +/* + * Copyright (c) 2025-2026 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 org.meshtastic.core.ui.component + +import androidx.compose.material3.SnackbarHost +import androidx.compose.material3.SnackbarHostState +import androidx.compose.material3.SnackbarResult +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.remember +import androidx.compose.ui.Modifier +import org.meshtastic.core.ui.util.SnackbarManager + +/** + * Shared composable that observes [SnackbarManager.events] and renders a Material 3 [SnackbarHost]. + * + * Each event is consumed exactly once. If the snackbar had an action label and the user tapped it, + * [SnackbarManager.SnackbarEvent.onAction] is invoked. + * + * Usage: Place `MeshtasticSnackbarHost(snackbarManager)` once in the top-level composable of each platform host, + * typically inside a `Box` with `Alignment.BottomCenter`. + */ +@Composable +fun MeshtasticSnackbarHost(snackbarManager: SnackbarManager, modifier: Modifier = Modifier) { + val snackbarHostState = remember { SnackbarHostState() } + + LaunchedEffect(snackbarManager) { + snackbarManager.events.collect { event -> + val result = snackbarHostState.showSnackbar( + message = event.message, + actionLabel = event.actionLabel, + withDismissAction = event.withDismissAction, + duration = event.duration, + ) + if (result == SnackbarResult.ActionPerformed) { + event.onAction?.invoke() + } + } + } + + SnackbarHost(hostState = snackbarHostState, modifier = modifier) +} + diff --git a/core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/util/SnackbarManager.kt b/core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/util/SnackbarManager.kt new file mode 100644 index 000000000..d9302481b --- /dev/null +++ b/core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/util/SnackbarManager.kt @@ -0,0 +1,65 @@ +/* + * Copyright (c) 2025-2026 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 org.meshtastic.core.ui.util + +import androidx.compose.material3.SnackbarDuration +import kotlinx.coroutines.channels.Channel +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.receiveAsFlow +import org.koin.core.annotation.Single + +/** + * A global manager for displaying snackbars across the application. This allows ViewModels to trigger transient + * feedback messages without direct dependencies on UI components or `SnackbarHostState`. + * + * Events are buffered in a [Channel] and consumed exactly once by the host composable via `MeshtasticSnackbarHost`. + * + * @see AlertManager for the modal dialog equivalent. + */ +@Single +open class SnackbarManager { + data class SnackbarEvent( + val message: String, + val actionLabel: String? = null, + val withDismissAction: Boolean = false, + val duration: SnackbarDuration = SnackbarDuration.Short, + val onAction: (() -> Unit)? = null, + ) + + private val _events = Channel(Channel.BUFFERED) + open val events: Flow = _events.receiveAsFlow() + + open fun showSnackbar( + message: String, + actionLabel: String? = null, + withDismissAction: Boolean = false, + duration: SnackbarDuration = if (actionLabel != null) SnackbarDuration.Indefinite else SnackbarDuration.Short, + onAction: (() -> Unit)? = null, + ) { + _events.trySend( + SnackbarEvent( + message = message, + actionLabel = actionLabel, + withDismissAction = withDismissAction, + duration = duration, + onAction = onAction, + ), + ) + } +} + + diff --git a/core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/viewmodel/UIViewModel.kt b/core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/viewmodel/UIViewModel.kt index 9ff6239c8..e1f13485a 100644 --- a/core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/viewmodel/UIViewModel.kt +++ b/core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/viewmodel/UIViewModel.kt @@ -57,6 +57,7 @@ import org.meshtastic.core.resources.compromised_keys import org.meshtastic.core.ui.component.ScrollToTopEvent import org.meshtastic.core.ui.util.AlertManager import org.meshtastic.core.ui.util.ComposableContent +import org.meshtastic.core.ui.util.SnackbarManager import org.meshtastic.proto.ChannelSet import org.meshtastic.proto.ClientNotification import org.meshtastic.proto.SharedContact @@ -80,6 +81,7 @@ class UIViewModel( private val notificationManager: NotificationManager, packetRepository: PacketRepository, val alertManager: AlertManager, + val snackbarManager: SnackbarManager, ) : ViewModel() { private val _navigationDeepLink = MutableSharedFlow(replay = 1) @@ -165,6 +167,14 @@ class UIViewModel( alertManager.dismissAlert() } + fun showSnackbar( + message: String, + actionLabel: String? = null, + onAction: (() -> Unit)? = null, + ) { + snackbarManager.showSnackbar(message = message, actionLabel = actionLabel, onAction = onAction) + } + fun setDeviceAddress(address: String) { radioController.setDeviceAddress(address) } diff --git a/core/ui/src/commonTest/kotlin/org/meshtastic/core/ui/util/SnackbarManagerTest.kt b/core/ui/src/commonTest/kotlin/org/meshtastic/core/ui/util/SnackbarManagerTest.kt new file mode 100644 index 000000000..8584fa83f --- /dev/null +++ b/core/ui/src/commonTest/kotlin/org/meshtastic/core/ui/util/SnackbarManagerTest.kt @@ -0,0 +1,108 @@ +/* + * Copyright (c) 2025-2026 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 org.meshtastic.core.ui.util + +import androidx.compose.material3.SnackbarDuration +import app.cash.turbine.test +import kotlinx.coroutines.test.runTest +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertNull +import kotlin.test.assertTrue + +class SnackbarManagerTest { + + private val snackbarManager = SnackbarManager() + + @Test + fun showSnackbar_emits_event_with_message() = runTest { + snackbarManager.events.test { + snackbarManager.showSnackbar(message = "Hello") + + val event = awaitItem() + assertEquals("Hello", event.message) + assertNull(event.actionLabel) + assertEquals(SnackbarDuration.Short, event.duration) + } + } + + @Test + fun showSnackbar_with_action_defaults_to_indefinite_duration() = runTest { + snackbarManager.events.test { + snackbarManager.showSnackbar(message = "Deleted", actionLabel = "Undo") + + val event = awaitItem() + assertEquals("Deleted", event.message) + assertEquals("Undo", event.actionLabel) + assertEquals(SnackbarDuration.Indefinite, event.duration) + } + } + + @Test + fun showSnackbar_with_explicit_duration_overrides_default() = runTest { + snackbarManager.events.test { + snackbarManager.showSnackbar( + message = "Saved", + actionLabel = "View", + duration = SnackbarDuration.Long, + ) + + val event = awaitItem() + assertEquals(SnackbarDuration.Long, event.duration) + } + } + + @Test + fun multiple_events_are_queued_and_consumed_in_order() = runTest { + snackbarManager.events.test { + snackbarManager.showSnackbar(message = "First") + snackbarManager.showSnackbar(message = "Second") + snackbarManager.showSnackbar(message = "Third") + + assertEquals("First", awaitItem().message) + assertEquals("Second", awaitItem().message) + assertEquals("Third", awaitItem().message) + } + } + + @Test + fun onAction_callback_is_preserved_in_event() = runTest { + var actionTriggered = false + snackbarManager.events.test { + snackbarManager.showSnackbar( + message = "Item removed", + actionLabel = "Undo", + onAction = { actionTriggered = true }, + ) + + val event = awaitItem() + event.onAction?.invoke() + assertTrue(actionTriggered) + } + } + + @Test + fun withDismissAction_is_passed_through() = runTest { + snackbarManager.events.test { + snackbarManager.showSnackbar(message = "Notice", withDismissAction = true) + + val event = awaitItem() + assertTrue(event.withDismissAction) + } + } +} + diff --git a/desktop/src/main/kotlin/org/meshtastic/desktop/ui/DesktopMainScreen.kt b/desktop/src/main/kotlin/org/meshtastic/desktop/ui/DesktopMainScreen.kt index fff4df006..5ecc1a61b 100644 --- a/desktop/src/main/kotlin/org/meshtastic/desktop/ui/DesktopMainScreen.kt +++ b/desktop/src/main/kotlin/org/meshtastic/desktop/ui/DesktopMainScreen.kt @@ -16,6 +16,7 @@ */ package org.meshtastic.desktop.ui +import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.material3.Icon @@ -26,6 +27,7 @@ import androidx.compose.material3.Surface import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue +import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.navigation3.runtime.NavBackStack @@ -39,6 +41,7 @@ import org.meshtastic.core.model.DeviceType import org.meshtastic.core.navigation.TopLevelDestination import org.meshtastic.core.repository.RadioInterfaceService import org.meshtastic.core.ui.component.AlertHost +import org.meshtastic.core.ui.component.MeshtasticSnackbarHost import org.meshtastic.core.ui.component.SharedDialogs import org.meshtastic.core.ui.navigation.icon import org.meshtastic.core.ui.viewmodel.UIViewModel @@ -77,46 +80,52 @@ fun DesktopMainScreen( AlertHost(uiViewModel.alertManager) Surface(modifier = Modifier.fillMaxSize(), color = MaterialTheme.colorScheme.background) { - Row(modifier = Modifier.fillMaxSize()) { - NavigationRail { - TopLevelDestination.entries.forEach { destination -> - NavigationRailItem( - selected = destination == selected, - onClick = { - if (destination != selected) { - backStack.add(destination.route) - while (backStack.size > 1) { - backStack.removeAt(0) + Box(modifier = Modifier.fillMaxSize()) { + Row(modifier = Modifier.fillMaxSize()) { + NavigationRail { + TopLevelDestination.entries.forEach { destination -> + NavigationRailItem( + selected = destination == selected, + onClick = { + if (destination != selected) { + backStack.add(destination.route) + while (backStack.size > 1) { + backStack.removeAt(0) + } } - } - }, - icon = { - if (destination == TopLevelDestination.Connections) { - org.meshtastic.feature.connections.ui.components.AnimatedConnectionsNavIcon( - connectionState = connectionState, - deviceType = DeviceType.fromAddress(selectedDevice ?: "NoDevice"), - meshActivityFlow = radioService.meshActivity, - colorScheme = colorScheme, - ) - } else { - Icon( - imageVector = destination.icon, - contentDescription = stringResource(destination.label), - ) - } - }, - label = { Text(stringResource(destination.label)) }, - ) + }, + icon = { + if (destination == TopLevelDestination.Connections) { + org.meshtastic.feature.connections.ui.components.AnimatedConnectionsNavIcon( + connectionState = connectionState, + deviceType = DeviceType.fromAddress(selectedDevice ?: "NoDevice"), + meshActivityFlow = radioService.meshActivity, + colorScheme = colorScheme, + ) + } else { + Icon( + imageVector = destination.icon, + contentDescription = stringResource(destination.label), + ) + } + }, + label = { Text(stringResource(destination.label)) }, + ) + } } + + val provider = entryProvider { desktopNavGraph(backStack) } + + NavDisplay( + backStack = backStack, + onBack = { backStack.removeLastOrNull() }, + entryProvider = provider, + modifier = Modifier.weight(1f).fillMaxSize(), + ) } - - val provider = entryProvider { desktopNavGraph(backStack) } - - NavDisplay( - backStack = backStack, - onBack = { backStack.removeLastOrNull() }, - entryProvider = provider, - modifier = Modifier.weight(1f).fillMaxSize(), + MeshtasticSnackbarHost( + snackbarManager = uiViewModel.snackbarManager, + modifier = Modifier.align(Alignment.BottomCenter), ) } } diff --git a/feature/node/src/androidMain/kotlin/org/meshtastic/feature/node/detail/NodeDetailScreen.kt b/feature/node/src/androidMain/kotlin/org/meshtastic/feature/node/detail/NodeDetailScreen.kt index 0d673afd9..853017d94 100644 --- a/feature/node/src/androidMain/kotlin/org/meshtastic/feature/node/detail/NodeDetailScreen.kt +++ b/feature/node/src/androidMain/kotlin/org/meshtastic/feature/node/detail/NodeDetailScreen.kt @@ -26,8 +26,6 @@ import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ModalBottomSheet import androidx.compose.material3.Scaffold -import androidx.compose.material3.SnackbarHost -import androidx.compose.material3.SnackbarHostState import androidx.compose.material3.rememberModalBottomSheetState import androidx.compose.runtime.Composable import androidx.compose.runtime.DisposableEffect @@ -81,21 +79,11 @@ actual fun NodeDetailScreen( ) { LaunchedEffect(nodeId) { viewModel.start(nodeId) } - val snackbarHostState = remember { SnackbarHostState() } val uiState by viewModel.uiState.collectAsStateWithLifecycle() - LaunchedEffect(Unit) { - viewModel.effects.collect { effect -> - if (effect is NodeRequestEffect.ShowFeedback) { - snackbarHostState.showSnackbar(effect.text.resolve()) - } - } - } - NodeDetailScaffold( modifier = modifier, uiState = uiState, - snackbarHostState = snackbarHostState, viewModel = viewModel, navigateToMessages = navigateToMessages, onNavigate = onNavigate, @@ -109,7 +97,6 @@ actual fun NodeDetailScreen( private fun NodeDetailScaffold( modifier: Modifier, uiState: NodeDetailUiState, - snackbarHostState: SnackbarHostState, viewModel: NodeDetailViewModel, navigateToMessages: (String) -> Unit, onNavigate: (Route) -> Unit, @@ -139,7 +126,6 @@ private fun NodeDetailScaffold( onClickChip = {}, ) }, - snackbarHost = { SnackbarHost(snackbarHostState) }, ) { paddingValues -> NodeDetailContent( uiState = uiState, diff --git a/feature/node/src/androidMain/kotlin/org/meshtastic/feature/node/metrics/PositionLog.kt b/feature/node/src/androidMain/kotlin/org/meshtastic/feature/node/metrics/PositionLog.kt index 2ed2fa7cd..5862a0ed9 100644 --- a/feature/node/src/androidMain/kotlin/org/meshtastic/feature/node/metrics/PositionLog.kt +++ b/feature/node/src/androidMain/kotlin/org/meshtastic/feature/node/metrics/PositionLog.kt @@ -36,15 +36,11 @@ import androidx.compose.material3.LocalTextStyle import androidx.compose.material3.MaterialTheme import androidx.compose.material3.OutlinedButton import androidx.compose.material3.Scaffold -import androidx.compose.material3.SnackbarHost -import androidx.compose.material3.SnackbarHostState import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.CompositionLocalProvider -import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier @@ -64,7 +60,6 @@ import org.meshtastic.core.ui.icon.MeshtasticIcons import org.meshtastic.core.ui.icon.Refresh import org.meshtastic.core.ui.icon.Save import org.meshtastic.core.ui.theme.AppTheme -import org.meshtastic.feature.node.detail.NodeRequestEffect import org.meshtastic.proto.Config import org.meshtastic.proto.Position @@ -104,18 +99,6 @@ private fun ActionButtons( @Composable actual fun PositionLogScreen(viewModel: MetricsViewModel, onNavigateUp: () -> Unit) { val state by viewModel.state.collectAsStateWithLifecycle() - val snackbarHostState = remember { SnackbarHostState() } - - LaunchedEffect(Unit) { - viewModel.effects.collect { effect -> - when (effect) { - is NodeRequestEffect.ShowFeedback -> { - @Suppress("SpreadOperator") - snackbarHostState.showSnackbar(effect.text.resolve()) - } - } - } - } val exportPositionLauncher = rememberLauncherForActivityResult(ActivityResultContracts.StartActivityForResult()) { @@ -144,7 +127,6 @@ actual fun PositionLogScreen(viewModel: MetricsViewModel, onNavigateUp: () -> Un onClickChip = {}, ) }, - snackbarHost = { SnackbarHost(snackbarHostState) }, ) { innerPadding -> BoxWithConstraints(modifier = Modifier.padding(innerPadding)) { val compactWidth = maxWidth < 600.dp diff --git a/feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/detail/CommonNodeRequestActions.kt b/feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/detail/CommonNodeRequestActions.kt index 2ec8c9d50..3d2704df4 100644 --- a/feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/detail/CommonNodeRequestActions.kt +++ b/feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/detail/CommonNodeRequestActions.kt @@ -18,11 +18,8 @@ package org.meshtastic.feature.node.detail import co.touchlab.kermit.Logger import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.SharedFlow import kotlinx.coroutines.flow.StateFlow -import kotlinx.coroutines.flow.asSharedFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch @@ -46,12 +43,13 @@ import org.meshtastic.core.resources.requesting_from import org.meshtastic.core.resources.signal_quality import org.meshtastic.core.resources.traceroute import org.meshtastic.core.resources.user_info +import org.meshtastic.core.ui.util.SnackbarManager @Single(binds = [NodeRequestActions::class]) -class CommonNodeRequestActions constructor(private val radioController: RadioController) : NodeRequestActions { - - private val _effects = MutableSharedFlow() - override val effects: SharedFlow = _effects.asSharedFlow() +class CommonNodeRequestActions constructor( + private val radioController: RadioController, + private val snackbarManager: SnackbarManager, +) : NodeRequestActions { private val _lastTracerouteTime = MutableStateFlow(null) override val lastTracerouteTime: StateFlow = _lastTracerouteTime.asStateFlow() @@ -59,15 +57,15 @@ class CommonNodeRequestActions constructor(private val radioController: RadioCon private val _lastRequestNeighborTimes = MutableStateFlow>(emptyMap()) override val lastRequestNeighborTimes: StateFlow> = _lastRequestNeighborTimes.asStateFlow() + private suspend fun showFeedback(text: UiText) { + snackbarManager.showSnackbar(message = text.resolve()) + } + override fun requestUserInfo(scope: CoroutineScope, destNum: Int, longName: String) { scope.launch(ioDispatcher) { Logger.i { "Requesting UserInfo for '$destNum'" } radioController.requestUserInfo(destNum) - _effects.emit( - NodeRequestEffect.ShowFeedback( - UiText.Resource(Res.string.requesting_from, Res.string.user_info, longName), - ), - ) + showFeedback(UiText.Resource(Res.string.requesting_from, Res.string.user_info, longName)) } } @@ -77,11 +75,7 @@ class CommonNodeRequestActions constructor(private val radioController: RadioCon val packetId = radioController.getPacketId() radioController.requestNeighborInfo(packetId, destNum) _lastRequestNeighborTimes.update { it + (destNum to nowMillis) } - _effects.emit( - NodeRequestEffect.ShowFeedback( - UiText.Resource(Res.string.requesting_from, Res.string.neighbor_info, longName), - ), - ) + showFeedback(UiText.Resource(Res.string.requesting_from, Res.string.neighbor_info, longName)) } } @@ -89,11 +83,7 @@ class CommonNodeRequestActions constructor(private val radioController: RadioCon scope.launch(ioDispatcher) { Logger.i { "Requesting position for '$destNum'" } radioController.requestPosition(destNum, position) - _effects.emit( - NodeRequestEffect.ShowFeedback( - UiText.Resource(Res.string.requesting_from, Res.string.position, longName), - ), - ) + showFeedback(UiText.Resource(Res.string.requesting_from, Res.string.position, longName)) } } @@ -114,9 +104,7 @@ class CommonNodeRequestActions constructor(private val radioController: RadioCon TelemetryType.PAX -> Res.string.request_pax_metrics } - _effects.emit( - NodeRequestEffect.ShowFeedback(UiText.Resource(Res.string.requesting_from, typeRes, longName)), - ) + showFeedback(UiText.Resource(Res.string.requesting_from, typeRes, longName)) } } @@ -126,11 +114,7 @@ class CommonNodeRequestActions constructor(private val radioController: RadioCon val packetId = radioController.getPacketId() radioController.requestTraceroute(packetId, destNum) _lastTracerouteTime.value = nowMillis - _effects.emit( - NodeRequestEffect.ShowFeedback( - UiText.Resource(Res.string.requesting_from, Res.string.traceroute, longName), - ), - ) + showFeedback(UiText.Resource(Res.string.requesting_from, Res.string.traceroute, longName)) } } } diff --git a/feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/detail/NodeDetailViewModel.kt b/feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/detail/NodeDetailViewModel.kt index 553607a9a..75a55c6ba 100644 --- a/feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/detail/NodeDetailViewModel.kt +++ b/feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/detail/NodeDetailViewModel.kt @@ -21,7 +21,6 @@ import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.SharedFlow import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.combine @@ -84,7 +83,6 @@ class NodeDetailViewModel( } .stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), NodeDetailUiState()) - val effects: SharedFlow = nodeRequestActions.effects fun start(nodeId: Int) { if (manualNodeId.value != nodeId) { diff --git a/feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/detail/NodeRequestActions.kt b/feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/detail/NodeRequestActions.kt index 1908cbbe3..3c396d8a9 100644 --- a/feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/detail/NodeRequestActions.kt +++ b/feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/detail/NodeRequestActions.kt @@ -17,19 +17,12 @@ package org.meshtastic.feature.node.detail import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.flow.SharedFlow import kotlinx.coroutines.flow.StateFlow import org.meshtastic.core.model.Position import org.meshtastic.core.model.TelemetryType -import org.meshtastic.core.resources.UiText - -sealed class NodeRequestEffect { - data class ShowFeedback(val text: UiText) : NodeRequestEffect() -} /** Interface for high-level node request actions (e.g., requesting user info, position, telemetry). */ interface NodeRequestActions { - val effects: SharedFlow val lastTracerouteTime: StateFlow val lastRequestNeighborTimes: StateFlow> diff --git a/feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/metrics/BaseMetricChart.kt b/feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/metrics/BaseMetricChart.kt index ffc8b698a..b31061ded 100644 --- a/feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/metrics/BaseMetricChart.kt +++ b/feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/metrics/BaseMetricChart.kt @@ -30,8 +30,6 @@ import androidx.compose.material.icons.rounded.Info import androidx.compose.material3.Icon import androidx.compose.material3.IconButton import androidx.compose.material3.Scaffold -import androidx.compose.material3.SnackbarHost -import androidx.compose.material3.SnackbarHostState import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf @@ -152,7 +150,6 @@ fun BaseMetricScreen( data: List, timeProvider: (T) -> Double, infoData: List = emptyList(), - snackbarHostState: SnackbarHostState = remember { SnackbarHostState() }, onRequestTelemetry: (() -> Unit)? = null, chartPart: @Composable (Modifier, Double?, VicoScrollState, (Double) -> Unit) -> Unit, listPart: @Composable (Modifier, Double?, LazyListState, (Double) -> Unit) -> Unit, @@ -192,7 +189,6 @@ fun BaseMetricScreen( onClickChip = {}, ) }, - snackbarHost = { SnackbarHost(snackbarHostState) }, ) { innerPadding -> Column(modifier = Modifier.padding(innerPadding)) { if (displayInfoDialog) { diff --git a/feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/metrics/DeviceMetrics.kt b/feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/metrics/DeviceMetrics.kt index 03407da05..afc9f227b 100644 --- a/feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/metrics/DeviceMetrics.kt +++ b/feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/metrics/DeviceMetrics.kt @@ -36,7 +36,6 @@ import androidx.compose.foundation.text.selection.SelectionContainer import androidx.compose.material3.Card import androidx.compose.material3.CardDefaults import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.SnackbarHostState import androidx.compose.material3.Surface import androidx.compose.material3.Text import androidx.compose.runtime.Composable @@ -84,7 +83,6 @@ import org.meshtastic.core.ui.theme.GraphColors.Cyan import org.meshtastic.core.ui.theme.GraphColors.Gold import org.meshtastic.core.ui.theme.GraphColors.Green import org.meshtastic.core.ui.theme.GraphColors.Purple -import org.meshtastic.feature.node.detail.NodeRequestEffect import org.meshtastic.feature.node.metrics.CommonCharts.MS_PER_SEC import org.meshtastic.proto.Telemetry @@ -130,23 +128,12 @@ fun DeviceMetricsScreen(viewModel: MetricsViewModel, onNavigateUp: () -> Unit) { val timeFrame by viewModel.timeFrame.collectAsStateWithLifecycle() val availableTimeFrames by viewModel.availableTimeFrames.collectAsStateWithLifecycle() val data = state.deviceMetrics.filter { it.time.toLong() >= timeFrame.timeThreshold() } - val snackbarHostState = remember { SnackbarHostState() } val hasBattery = remember(data) { data.any { it.device_metrics?.battery_level != null } } val hasVoltage = remember(data) { data.any { it.device_metrics?.voltage != null } } val hasChUtil = remember(data) { data.any { it.device_metrics?.channel_utilization != null } } val hasAirUtil = remember(data) { data.any { it.device_metrics?.air_util_tx != null } } - LaunchedEffect(Unit) { - viewModel.effects.collect { effect -> - when (effect) { - is NodeRequestEffect.ShowFeedback -> { - @Suppress("SpreadOperator") - snackbarHostState.showSnackbar(effect.text.resolve()) - } - } - } - } val filteredLegendData = remember(hasBattery, hasVoltage, hasChUtil, hasAirUtil) { @@ -193,7 +180,6 @@ fun DeviceMetricsScreen(viewModel: MetricsViewModel, onNavigateUp: () -> Unit) { data = data, timeProvider = { it.time.toDouble() }, infoData = infoItems, - snackbarHostState = snackbarHostState, onRequestTelemetry = { viewModel.requestTelemetry(TelemetryType.DEVICE) }, controlPart = { TimeFrameSelector( diff --git a/feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/metrics/EnvironmentMetrics.kt b/feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/metrics/EnvironmentMetrics.kt index 1447a2f59..400a014ce 100644 --- a/feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/metrics/EnvironmentMetrics.kt +++ b/feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/metrics/EnvironmentMetrics.kt @@ -35,13 +35,10 @@ import androidx.compose.foundation.text.selection.SelectionContainer import androidx.compose.material3.Card import androidx.compose.material3.CardDefaults import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.SnackbarHostState import androidx.compose.material3.Surface import androidx.compose.material3.Text import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue -import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color @@ -68,7 +65,6 @@ import org.meshtastic.core.resources.uv_lux import org.meshtastic.core.resources.voltage import org.meshtastic.core.ui.component.IaqDisplayMode import org.meshtastic.core.ui.component.IndoorAirQuality -import org.meshtastic.feature.node.detail.NodeRequestEffect import org.meshtastic.feature.node.metrics.CommonCharts.MS_PER_SEC import org.meshtastic.proto.Telemetry @@ -79,18 +75,6 @@ fun EnvironmentMetricsScreen(viewModel: MetricsViewModel, onNavigateUp: () -> Un val filteredTelemetries by viewModel.filteredEnvironmentMetrics.collectAsStateWithLifecycle() val timeFrame by viewModel.timeFrame.collectAsStateWithLifecycle() val availableTimeFrames by viewModel.availableTimeFrames.collectAsStateWithLifecycle() - val snackbarHostState = remember { SnackbarHostState() } - - LaunchedEffect(Unit) { - viewModel.effects.collect { effect -> - when (effect) { - is NodeRequestEffect.ShowFeedback -> { - @Suppress("SpreadOperator") - snackbarHostState.showSnackbar(effect.text.resolve()) - } - } - } - } BaseMetricScreen( onNavigateUp = onNavigateUp, @@ -100,7 +84,6 @@ fun EnvironmentMetricsScreen(viewModel: MetricsViewModel, onNavigateUp: () -> Un data = filteredTelemetries, timeProvider = { it.time.toDouble() }, infoData = listOf(InfoDialogData(Res.string.iaq, Res.string.iaq_definition, Environment.IAQ.color)), - snackbarHostState = snackbarHostState, onRequestTelemetry = { viewModel.requestTelemetry(TelemetryType.ENVIRONMENT) }, controlPart = { TimeFrameSelector( diff --git a/feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/metrics/HostMetricsLog.kt b/feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/metrics/HostMetricsLog.kt index 4aad82977..ad43346f3 100644 --- a/feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/metrics/HostMetricsLog.kt +++ b/feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/metrics/HostMetricsLog.kt @@ -38,13 +38,9 @@ import androidx.compose.material3.LinearProgressIndicator import androidx.compose.material3.MaterialTheme import androidx.compose.material3.ProgressIndicatorDefaults import androidx.compose.material3.Scaffold -import androidx.compose.material3.SnackbarHost -import androidx.compose.material3.SnackbarHostState import androidx.compose.material3.Text import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue -import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.text.TextStyle @@ -68,25 +64,12 @@ import org.meshtastic.core.ui.component.MainAppBar import org.meshtastic.core.ui.icon.DataArray import org.meshtastic.core.ui.icon.MeshtasticIcons import org.meshtastic.core.ui.icon.Refresh -import org.meshtastic.feature.node.detail.NodeRequestEffect import org.meshtastic.proto.Telemetry @OptIn(ExperimentalFoundationApi::class) @Composable fun HostMetricsLogScreen(metricsViewModel: MetricsViewModel, onNavigateUp: () -> Unit) { val state by metricsViewModel.state.collectAsStateWithLifecycle() - val snackbarHostState = remember { SnackbarHostState() } - - LaunchedEffect(Unit) { - metricsViewModel.effects.collect { effect -> - when (effect) { - is NodeRequestEffect.ShowFeedback -> { - @Suppress("SpreadOperator") - snackbarHostState.showSnackbar(effect.text.resolve()) - } - } - } - } val hostMetrics = state.hostMetrics @@ -108,7 +91,6 @@ fun HostMetricsLogScreen(metricsViewModel: MetricsViewModel, onNavigateUp: () -> onClickChip = {}, ) }, - snackbarHost = { SnackbarHost(snackbarHostState) }, ) { innerPadding -> LazyColumn( modifier = Modifier.fillMaxSize().padding(innerPadding), diff --git a/feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/metrics/MetricsViewModel.kt b/feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/metrics/MetricsViewModel.kt index bafa7a2b0..7b8f655d7 100644 --- a/feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/metrics/MetricsViewModel.kt +++ b/feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/metrics/MetricsViewModel.kt @@ -23,7 +23,6 @@ import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import co.touchlab.kermit.Logger import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.SharedFlow import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.combine @@ -64,7 +63,6 @@ import org.meshtastic.core.ui.util.toMessageRes import org.meshtastic.core.ui.viewmodel.stateInWhileSubscribed import org.meshtastic.feature.map.model.TracerouteOverlay import org.meshtastic.feature.node.detail.NodeRequestActions -import org.meshtastic.feature.node.detail.NodeRequestEffect import org.meshtastic.feature.node.domain.usecase.GetNodeDetailsUseCase import org.meshtastic.feature.node.model.MetricsState import org.meshtastic.feature.node.model.TimeFrame @@ -175,7 +173,6 @@ open class MetricsViewModel( } .stateInWhileSubscribed(emptyList()) - val effects: SharedFlow = nodeRequestActions.effects val lastTraceRouteTime: StateFlow = nodeRequestActions.lastTracerouteTime diff --git a/feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/metrics/NeighborInfoLog.kt b/feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/metrics/NeighborInfoLog.kt index 218b271bc..21e8b65bb 100644 --- a/feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/metrics/NeighborInfoLog.kt +++ b/feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/metrics/NeighborInfoLog.kt @@ -28,10 +28,7 @@ import androidx.compose.material3.DropdownMenu import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Scaffold -import androidx.compose.material3.SnackbarHost -import androidx.compose.material3.SnackbarHostState import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember @@ -55,25 +52,12 @@ import org.meshtastic.core.ui.theme.StatusColors.StatusOrange import org.meshtastic.core.ui.theme.StatusColors.StatusYellow import org.meshtastic.core.ui.util.annotateNeighborInfo import org.meshtastic.feature.node.component.CooldownIconButton -import org.meshtastic.feature.node.detail.NodeRequestEffect @OptIn(ExperimentalFoundationApi::class) @Suppress("LongMethod", "CyclomaticComplexMethod") @Composable fun NeighborInfoLogScreen(modifier: Modifier = Modifier, viewModel: MetricsViewModel, onNavigateUp: () -> Unit) { val state by viewModel.state.collectAsStateWithLifecycle() - val snackbarHostState = remember { SnackbarHostState() } - - LaunchedEffect(Unit) { - viewModel.effects.collect { effect -> - when (effect) { - is NodeRequestEffect.ShowFeedback -> { - @Suppress("SpreadOperator") - snackbarHostState.showSnackbar(effect.text.resolve()) - } - } - } - } fun getUsername(nodeNum: Int): String = with(viewModel.getUser(nodeNum)) { "$long_name ($short_name)" } @@ -104,7 +88,6 @@ fun NeighborInfoLogScreen(modifier: Modifier = Modifier, viewModel: MetricsViewM onClickChip = {}, ) }, - snackbarHost = { SnackbarHost(snackbarHostState) }, ) { innerPadding -> LazyColumn( modifier = modifier.fillMaxSize().padding(innerPadding), diff --git a/feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/metrics/PaxMetrics.kt b/feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/metrics/PaxMetrics.kt index 35f7c15ef..ec810308a 100644 --- a/feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/metrics/PaxMetrics.kt +++ b/feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/metrics/PaxMetrics.kt @@ -31,7 +31,6 @@ import androidx.compose.foundation.lazy.itemsIndexed import androidx.compose.material3.Card import androidx.compose.material3.CardDefaults import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.SnackbarHostState import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect @@ -70,7 +69,6 @@ import org.meshtastic.core.ui.icon.MeshtasticIcons import org.meshtastic.core.ui.icon.Paxcount import org.meshtastic.core.ui.theme.GraphColors.Orange import org.meshtastic.core.ui.theme.GraphColors.Purple -import org.meshtastic.feature.node.detail.NodeRequestEffect import org.meshtastic.proto.Paxcount as ProtoPaxcount private enum class PaxSeries(val color: Color, val legendRes: StringResource) { @@ -177,18 +175,6 @@ fun PaxMetricsScreen(metricsViewModel: MetricsViewModel, onNavigateUp: () -> Uni val paxMetrics by metricsViewModel.filteredPaxMetrics.collectAsStateWithLifecycle() val timeFrame by metricsViewModel.timeFrame.collectAsStateWithLifecycle() val availableTimeFrames by metricsViewModel.availableTimeFrames.collectAsStateWithLifecycle() - val snackbarHostState = remember { SnackbarHostState() } - - LaunchedEffect(Unit) { - metricsViewModel.effects.collect { effect -> - when (effect) { - is NodeRequestEffect.ShowFeedback -> { - @Suppress("SpreadOperator") - snackbarHostState.showSnackbar(effect.text.resolve()) - } - } - } - } // Prepare data for graph val graphData = @@ -211,7 +197,6 @@ fun PaxMetricsScreen(metricsViewModel: MetricsViewModel, onNavigateUp: () -> Uni nodeName = state.node?.user?.long_name ?: "", data = paxMetrics, timeProvider = { (it.first.received_date / CommonCharts.MS_PER_SEC).toDouble() }, - snackbarHostState = snackbarHostState, onRequestTelemetry = { metricsViewModel.requestTelemetry(TelemetryType.PAX) }, controlPart = { TimeFrameSelector( diff --git a/feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/metrics/PowerMetrics.kt b/feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/metrics/PowerMetrics.kt index 9c1c9ba0e..8743efeb1 100644 --- a/feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/metrics/PowerMetrics.kt +++ b/feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/metrics/PowerMetrics.kt @@ -36,7 +36,6 @@ import androidx.compose.material3.Card import androidx.compose.material3.CardDefaults import androidx.compose.material3.FilterChip import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.SnackbarHostState import androidx.compose.material3.Surface import androidx.compose.material3.Text import androidx.compose.runtime.Composable @@ -73,7 +72,6 @@ import org.meshtastic.core.resources.power_metrics_log import org.meshtastic.core.resources.voltage import org.meshtastic.core.ui.theme.GraphColors.Gold import org.meshtastic.core.ui.theme.GraphColors.InfantryBlue -import org.meshtastic.feature.node.detail.NodeRequestEffect import org.meshtastic.feature.node.metrics.CommonCharts.MS_PER_SEC import org.meshtastic.proto.Telemetry @@ -112,18 +110,6 @@ fun PowerMetricsScreen(viewModel: MetricsViewModel, onNavigateUp: () -> Unit) { val availableTimeFrames by viewModel.availableTimeFrames.collectAsStateWithLifecycle() val data = state.powerMetrics.filter { it.time.toLong() >= timeFrame.timeThreshold() } var selectedChannel by remember { mutableStateOf(PowerChannel.ONE) } - val snackbarHostState = remember { SnackbarHostState() } - - LaunchedEffect(Unit) { - viewModel.effects.collect { effect -> - when (effect) { - is NodeRequestEffect.ShowFeedback -> { - @Suppress("SpreadOperator") - snackbarHostState.showSnackbar(effect.text.resolve()) - } - } - } - } BaseMetricScreen( onNavigateUp = onNavigateUp, @@ -132,7 +118,6 @@ fun PowerMetricsScreen(viewModel: MetricsViewModel, onNavigateUp: () -> Unit) { nodeName = state.node?.user?.long_name ?: "", data = data, timeProvider = { it.time.toDouble() }, - snackbarHostState = snackbarHostState, onRequestTelemetry = { viewModel.requestTelemetry(TelemetryType.POWER) }, controlPart = { Column { diff --git a/feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/metrics/SignalMetrics.kt b/feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/metrics/SignalMetrics.kt index c170d6d16..d53776354 100644 --- a/feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/metrics/SignalMetrics.kt +++ b/feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/metrics/SignalMetrics.kt @@ -35,7 +35,6 @@ import androidx.compose.foundation.text.selection.SelectionContainer import androidx.compose.material3.Card import androidx.compose.material3.CardDefaults import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.SnackbarHostState import androidx.compose.material3.Surface import androidx.compose.material3.Text import androidx.compose.runtime.Composable @@ -67,7 +66,6 @@ import org.meshtastic.core.resources.snr_definition import org.meshtastic.core.ui.component.LoraSignalIndicator import org.meshtastic.core.ui.theme.GraphColors.Blue import org.meshtastic.core.ui.theme.GraphColors.Green -import org.meshtastic.feature.node.detail.NodeRequestEffect import org.meshtastic.feature.node.metrics.CommonCharts.MS_PER_SEC import org.meshtastic.proto.MeshPacket @@ -89,18 +87,6 @@ fun SignalMetricsScreen(viewModel: MetricsViewModel, onNavigateUp: () -> Unit) { val timeFrame by viewModel.timeFrame.collectAsStateWithLifecycle() val availableTimeFrames by viewModel.availableTimeFrames.collectAsStateWithLifecycle() val data = state.signalMetrics.filter { it.rx_time.toLong() >= timeFrame.timeThreshold() } - val snackbarHostState = remember { SnackbarHostState() } - - LaunchedEffect(Unit) { - viewModel.effects.collect { effect -> - when (effect) { - is NodeRequestEffect.ShowFeedback -> { - @Suppress("SpreadOperator") - snackbarHostState.showSnackbar(effect.text.resolve()) - } - } - } - } BaseMetricScreen( onNavigateUp = onNavigateUp, @@ -109,7 +95,6 @@ fun SignalMetricsScreen(viewModel: MetricsViewModel, onNavigateUp: () -> Unit) { nodeName = state.node?.user?.long_name ?: "", data = data, timeProvider = { it.rx_time.toDouble() }, - snackbarHostState = snackbarHostState, onRequestTelemetry = { viewModel.requestTelemetry(TelemetryType.LOCAL_STATS) }, infoData = listOf( diff --git a/feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/metrics/TracerouteLog.kt b/feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/metrics/TracerouteLog.kt index 49d870da2..4d00c684a 100644 --- a/feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/metrics/TracerouteLog.kt +++ b/feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/metrics/TracerouteLog.kt @@ -28,10 +28,7 @@ import androidx.compose.material3.DropdownMenu import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Scaffold -import androidx.compose.material3.SnackbarHost -import androidx.compose.material3.SnackbarHostState import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember @@ -72,7 +69,6 @@ import org.meshtastic.core.ui.theme.StatusColors.StatusYellow import org.meshtastic.core.ui.util.annotateTraceroute import org.meshtastic.feature.map.model.TracerouteOverlay import org.meshtastic.feature.node.component.CooldownIconButton -import org.meshtastic.feature.node.detail.NodeRequestEffect import org.meshtastic.proto.RouteDiscovery @OptIn(ExperimentalFoundationApi::class) @@ -85,18 +81,6 @@ fun TracerouteLogScreen( onViewOnMap: (requestId: Int, responseLogUuid: String) -> Unit = { _, _ -> }, ) { val state by viewModel.state.collectAsStateWithLifecycle() - val snackbarHostState = remember { SnackbarHostState() } - - LaunchedEffect(Unit) { - viewModel.effects.collect { effect -> - when (effect) { - is NodeRequestEffect.ShowFeedback -> { - @Suppress("SpreadOperator") - snackbarHostState.showSnackbar(effect.text.resolve()) - } - } - } - } fun getUsername(nodeNum: Int): String = with(viewModel.getUser(nodeNum)) { "$long_name ($short_name)" } @@ -127,7 +111,6 @@ fun TracerouteLogScreen( onClickChip = {}, ) }, - snackbarHost = { SnackbarHost(snackbarHostState) }, ) { innerPadding -> LazyColumn( modifier = modifier.fillMaxSize().padding(innerPadding), diff --git a/feature/node/src/commonTest/kotlin/org/meshtastic/feature/node/detail/NodeDetailViewModelTest.kt b/feature/node/src/commonTest/kotlin/org/meshtastic/feature/node/detail/NodeDetailViewModelTest.kt index d56d6c635..c3ed67b5b 100644 --- a/feature/node/src/commonTest/kotlin/org/meshtastic/feature/node/detail/NodeDetailViewModelTest.kt +++ b/feature/node/src/commonTest/kotlin/org/meshtastic/feature/node/detail/NodeDetailViewModelTest.kt @@ -57,7 +57,6 @@ class NodeDetailViewModelTest { Dispatchers.setMain(testDispatcher) every { getNodeDetailsUseCase(any()) } returns emptyFlow() - every { nodeRequestActions.effects } returns kotlinx.coroutines.flow.MutableSharedFlow() viewModel = createViewModel(1234) } diff --git a/feature/node/src/commonTest/kotlin/org/meshtastic/feature/node/metrics/MetricsViewModelTest.kt b/feature/node/src/commonTest/kotlin/org/meshtastic/feature/node/metrics/MetricsViewModelTest.kt index 689d4b214..34e411af0 100644 --- a/feature/node/src/commonTest/kotlin/org/meshtastic/feature/node/metrics/MetricsViewModelTest.kt +++ b/feature/node/src/commonTest/kotlin/org/meshtastic/feature/node/metrics/MetricsViewModelTest.kt @@ -78,7 +78,6 @@ class MetricsViewModelTest { // Default setup for flows every { serviceRepository.tracerouteResponse } returns MutableStateFlow(null) - every { nodeRequestActions.effects } returns mock() every { nodeRequestActions.lastTracerouteTime } returns MutableStateFlow(null) every { nodeRequestActions.lastRequestNeighborTimes } returns MutableStateFlow(emptyMap()) every { nodeRepository.nodeDBbyNum } returns MutableStateFlow(emptyMap())