diff --git a/core/navigation/src/commonMain/kotlin/org/meshtastic/core/navigation/NavBackStackExt.kt b/core/navigation/src/commonMain/kotlin/org/meshtastic/core/navigation/NavBackStackExt.kt index 5638814f8..03580fe2b 100644 --- a/core/navigation/src/commonMain/kotlin/org/meshtastic/core/navigation/NavBackStackExt.kt +++ b/core/navigation/src/commonMain/kotlin/org/meshtastic/core/navigation/NavBackStackExt.kt @@ -24,7 +24,9 @@ import androidx.navigation3.runtime.NavKey */ fun MutableList.navigateTopLevel(route: NavKey) { if (isNotEmpty()) { - this[0] = route + if (this[0] != route) { + this[0] = route + } while (size > 1) { removeAt(lastIndex) } @@ -32,3 +34,41 @@ fun MutableList.navigateTopLevel(route: NavKey) { add(route) } } + +/** + * Replaces the last entry in the back stack with the given route. + * If the back stack is empty, it simply adds the route. + */ +fun MutableList.replaceLast(route: NavKey) { + if (isNotEmpty()) { + if (this[lastIndex] != route) { + this[lastIndex] = route + } + } else { + add(route) + } +} + +/** + * Replaces the entire back stack with the given routes in a way that minimizes structural changes + * and prevents the back stack from temporarily becoming empty. + */ +fun MutableList.replaceAll(routes: List) { + if (routes.isEmpty()) { + clear() + return + } + for (i in routes.indices) { + if (i < size) { + // Only mutate if the route actually changed, protecting Nav3's internal state matching. + if (this[i] != routes[i]) { + this[i] = routes[i] + } + } else { + add(routes[i]) + } + } + while (size > routes.size) { + removeAt(lastIndex) + } +} diff --git a/core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/component/MeshtasticAppShell.kt b/core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/component/MeshtasticAppShell.kt index 164305b05..b4fd4c8d4 100644 --- a/core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/component/MeshtasticAppShell.kt +++ b/core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/component/MeshtasticAppShell.kt @@ -22,6 +22,7 @@ import androidx.compose.ui.Modifier import androidx.navigation3.runtime.NavBackStack import androidx.navigation3.runtime.NavKey import org.meshtastic.core.navigation.NodeDetailRoutes +import org.meshtastic.core.navigation.replaceAll import org.meshtastic.core.ui.viewmodel.UIViewModel /** @@ -39,8 +40,7 @@ fun MeshtasticAppShell( ) { LaunchedEffect(uiViewModel) { uiViewModel.navigationDeepLink.collect { navKeys -> - backStack.clear() - backStack.addAll(navKeys) + backStack.replaceAll(navKeys) } } diff --git a/feature/messaging/src/commonMain/kotlin/org/meshtastic/feature/messaging/navigation/ContactsNavigation.kt b/feature/messaging/src/commonMain/kotlin/org/meshtastic/feature/messaging/navigation/ContactsNavigation.kt index 75bd59b8c..4db201db7 100644 --- a/feature/messaging/src/commonMain/kotlin/org/meshtastic/feature/messaging/navigation/ContactsNavigation.kt +++ b/feature/messaging/src/commonMain/kotlin/org/meshtastic/feature/messaging/navigation/ContactsNavigation.kt @@ -28,6 +28,7 @@ import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableSharedFlow import org.koin.compose.viewmodel.koinViewModel import org.meshtastic.core.navigation.ContactsRoutes +import org.meshtastic.core.navigation.replaceLast import org.meshtastic.core.ui.component.ScrollToTopEvent import org.meshtastic.feature.messaging.QuickChatScreen import org.meshtastic.feature.messaging.QuickChatViewModel @@ -50,11 +51,20 @@ fun EntryProviderScope.contactsGraph( } entry(metadata = { ListDetailSceneStrategy.detailPane() }) { args -> - ContactsEntryContent( - backStack = backStack, - scrollToTopEvents = scrollToTopEvents, - initialContactKey = args.contactKey, - initialMessage = args.message, + val contactKey = args.contactKey + val messageViewModel: org.meshtastic.feature.messaging.MessageViewModel = + koinViewModel(key = "messages-$contactKey") + messageViewModel.setContactKey(contactKey) + + org.meshtastic.feature.messaging.MessageScreen( + contactKey = contactKey, + message = args.message, + viewModel = messageViewModel, + navigateToNodeDetails = { + backStack.add(org.meshtastic.core.navigation.NodesRoutes.NodeDetailGraph(it)) + }, + navigateToQuickChatOptions = { backStack.add(org.meshtastic.core.navigation.ContactsRoutes.QuickChat) }, + onNavigateBack = { backStack.removeLastOrNull() }, ) } @@ -63,10 +73,8 @@ fun EntryProviderScope.contactsGraph( val viewModel = koinViewModel() ShareScreen( viewModel = viewModel, - onConfirm = { - // Navigation 3 - replace Top with Messages manually, but for now we just pop and add - backStack.removeLastOrNull() - backStack.add(ContactsRoutes.Messages(it, message)) + onConfirm = { contactKey -> + backStack.replaceLast(ContactsRoutes.Messages(contactKey, message)) }, onNavigateUp = { backStack.removeLastOrNull() }, ) @@ -82,8 +90,6 @@ fun EntryProviderScope.contactsGraph( fun ContactsEntryContent( backStack: NavBackStack, scrollToTopEvents: Flow, - initialContactKey: String? = null, - initialMessage: String = "", ) { val uiViewModel: org.meshtastic.core.ui.viewmodel.UIViewModel = koinViewModel() val sharedContactRequested by uiViewModel.sharedContactRequested.collectAsStateWithLifecycle() @@ -93,30 +99,11 @@ fun ContactsEntryContent( AdaptiveContactsScreen( backStack = backStack, contactsViewModel = contactsViewModel, - messageViewModel = koinViewModel(), // Ignored by custom detail pane below scrollToTopEvents = scrollToTopEvents, sharedContactRequested = sharedContactRequested, requestChannelSet = requestChannelSet, onHandleDeepLink = uiViewModel::handleDeepLink, onClearSharedContactRequested = uiViewModel::clearSharedContactRequested, onClearRequestChannelUrl = uiViewModel::clearRequestChannelUrl, - initialContactKey = initialContactKey, - initialMessage = initialMessage, - detailPaneCustom = { contactKey -> - val messageViewModel: org.meshtastic.feature.messaging.MessageViewModel = - koinViewModel(key = "messages-$contactKey") - messageViewModel.setContactKey(contactKey) - - org.meshtastic.feature.messaging.MessageScreen( - contactKey = contactKey, - message = if (contactKey == initialContactKey) initialMessage else "", - viewModel = messageViewModel, - navigateToNodeDetails = { - backStack.add(org.meshtastic.core.navigation.NodesRoutes.NodeDetailGraph(it)) - }, - navigateToQuickChatOptions = { backStack.add(org.meshtastic.core.navigation.ContactsRoutes.QuickChat) }, - onNavigateBack = { backStack.removeLastOrNull() }, - ) - }, ) } diff --git a/feature/messaging/src/commonMain/kotlin/org/meshtastic/feature/messaging/ui/contact/AdaptiveContactsScreen.kt b/feature/messaging/src/commonMain/kotlin/org/meshtastic/feature/messaging/ui/contact/AdaptiveContactsScreen.kt index cddf92498..441042e66 100644 --- a/feature/messaging/src/commonMain/kotlin/org/meshtastic/feature/messaging/ui/contact/AdaptiveContactsScreen.kt +++ b/feature/messaging/src/commonMain/kotlin/org/meshtastic/feature/messaging/ui/contact/AdaptiveContactsScreen.kt @@ -25,7 +25,6 @@ import org.meshtastic.core.navigation.ChannelsRoutes import org.meshtastic.core.navigation.ContactsRoutes import org.meshtastic.core.navigation.NodesRoutes import org.meshtastic.core.ui.component.ScrollToTopEvent -import org.meshtastic.feature.messaging.MessageViewModel import org.meshtastic.proto.ChannelSet import org.meshtastic.proto.SharedContact @@ -33,16 +32,12 @@ import org.meshtastic.proto.SharedContact fun AdaptiveContactsScreen( backStack: NavBackStack, contactsViewModel: ContactsViewModel, - messageViewModel: MessageViewModel, scrollToTopEvents: Flow, sharedContactRequested: SharedContact?, requestChannelSet: ChannelSet?, onHandleDeepLink: (MeshtasticUri, onInvalid: () -> Unit) -> Unit, onClearSharedContactRequested: () -> Unit, onClearRequestChannelUrl: () -> Unit, - initialContactKey: String? = null, - initialMessage: String = "", - detailPaneCustom: @Composable ((contactKey: String) -> Unit)? = null, ) { ContactsScreen( onNavigateToShare = { backStack.add(ChannelsRoutes.ChannelsGraph) }, diff --git a/feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/navigation/AdaptiveNodeListScreen.kt b/feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/navigation/AdaptiveNodeListScreen.kt index 09ec48b63..ce8bd665e 100644 --- a/feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/navigation/AdaptiveNodeListScreen.kt +++ b/feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/navigation/AdaptiveNodeListScreen.kt @@ -23,7 +23,6 @@ import kotlinx.coroutines.flow.Flow import org.koin.compose.viewmodel.koinViewModel import org.meshtastic.core.navigation.ChannelsRoutes import org.meshtastic.core.navigation.NodesRoutes -import org.meshtastic.core.navigation.Route import org.meshtastic.core.ui.component.ScrollToTopEvent import org.meshtastic.feature.node.list.NodeListScreen import org.meshtastic.feature.node.list.NodeListViewModel @@ -32,9 +31,6 @@ import org.meshtastic.feature.node.list.NodeListViewModel fun AdaptiveNodeListScreen( backStack: NavBackStack, scrollToTopEvents: Flow, - initialNodeId: Int? = null, - onNavigate: (Route) -> Unit = {}, - onNavigateToMessages: (String) -> Unit = {}, onHandleDeepLink: (org.meshtastic.core.common.util.MeshtasticUri, onInvalid: () -> Unit) -> Unit = { _, _ -> }, ) { val nodeListViewModel: NodeListViewModel = koinViewModel() diff --git a/feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/navigation/NodesNavigation.kt b/feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/navigation/NodesNavigation.kt index 8cfb72881..d68f064a8 100644 --- a/feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/navigation/NodesNavigation.kt +++ b/feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/navigation/NodesNavigation.kt @@ -53,6 +53,9 @@ import org.meshtastic.core.resources.power import org.meshtastic.core.resources.signal import org.meshtastic.core.resources.traceroute import org.meshtastic.core.ui.component.ScrollToTopEvent +import org.meshtastic.feature.node.compass.CompassViewModel +import org.meshtastic.feature.node.detail.NodeDetailScreen +import org.meshtastic.feature.node.detail.NodeDetailViewModel import org.meshtastic.feature.node.metrics.DeviceMetricsScreen import org.meshtastic.feature.node.metrics.EnvironmentMetricsScreen import org.meshtastic.feature.node.metrics.HostMetricsLogScreen @@ -76,8 +79,6 @@ fun EntryProviderScope.nodesGraph( AdaptiveNodeListScreen( backStack = backStack, scrollToTopEvents = scrollToTopEvents, - onNavigate = { backStack.add(it) }, - onNavigateToMessages = { backStack.add(ContactsRoutes.Messages(it)) }, onHandleDeepLink = onHandleDeepLink, ) } @@ -86,8 +87,6 @@ fun EntryProviderScope.nodesGraph( AdaptiveNodeListScreen( backStack = backStack, scrollToTopEvents = scrollToTopEvents, - onNavigate = { backStack.add(it) }, - onNavigateToMessages = { backStack.add(ContactsRoutes.Messages(it)) }, onHandleDeepLink = onHandleDeepLink, ) } @@ -106,21 +105,21 @@ fun EntryProviderScope.nodeDetailGraph( AdaptiveNodeListScreen( backStack = backStack, scrollToTopEvents = scrollToTopEvents, - initialNodeId = args.destNum, - onNavigate = { backStack.add(it) }, - onNavigateToMessages = { backStack.add(ContactsRoutes.Messages(it)) }, onHandleDeepLink = onHandleDeepLink, ) } entry(metadata = { ListDetailSceneStrategy.detailPane() }) { args -> - AdaptiveNodeListScreen( - backStack = backStack, - scrollToTopEvents = scrollToTopEvents, - initialNodeId = args.destNum, + val nodeDetailViewModel: NodeDetailViewModel = koinViewModel() + val compassViewModel: CompassViewModel = koinViewModel() + val destNum = args.destNum ?: 0 // Handle nullable destNum if needed + NodeDetailScreen( + nodeId = destNum, + viewModel = nodeDetailViewModel, + compassViewModel = compassViewModel, + navigateToMessages = { backStack.add(ContactsRoutes.Messages(it)) }, onNavigate = { backStack.add(it) }, - onNavigateToMessages = { backStack.add(ContactsRoutes.Messages(it)) }, - onHandleDeepLink = onHandleDeepLink, + onNavigateUp = { backStack.removeLastOrNull() }, ) }