From 86a35603d576214cb6c1b668f894165f76a5deb3 Mon Sep 17 00:00:00 2001 From: James Rich <2199651+jamesarich@users.noreply.github.com> Date: Sat, 21 Jun 2025 12:14:33 +0000 Subject: [PATCH] refactor: Add remote node indicator and node chip to app bar (#2195) --- .../java/com/geeksville/mesh/model/UIState.kt | 5 +- .../main/java/com/geeksville/mesh/ui/Main.kt | 87 ++++++++++++------- .../mesh/ui/message/components/MessageItem.kt | 6 +- .../mesh/ui/radioconfig/RadioConfig.kt | 11 ++- app/src/main/res/values/strings.xml | 1 + 5 files changed, 75 insertions(+), 35 deletions(-) diff --git a/app/src/main/java/com/geeksville/mesh/model/UIState.kt b/app/src/main/java/com/geeksville/mesh/model/UIState.kt index 2ca3576d9..4066d808e 100644 --- a/app/src/main/java/com/geeksville/mesh/model/UIState.kt +++ b/app/src/main/java/com/geeksville/mesh/model/UIState.kt @@ -266,7 +266,10 @@ class UIViewModel @Inject constructor( private val _title = MutableStateFlow("") val title: StateFlow = _title.asStateFlow() fun setTitle(title: String) { - _title.value = title + viewModelScope.launch { + + _title.value = title + } } val receivingLocationUpdates: StateFlow get() = locationRepository.receivingLocationUpdates diff --git a/app/src/main/java/com/geeksville/mesh/ui/Main.kt b/app/src/main/java/com/geeksville/mesh/ui/Main.kt index b8a7c2681..38f3fec8b 100644 --- a/app/src/main/java/com/geeksville/mesh/ui/Main.kt +++ b/app/src/main/java/com/geeksville/mesh/ui/Main.kt @@ -18,6 +18,7 @@ package com.geeksville.mesh.ui import androidx.annotation.StringRes +import androidx.compose.animation.AnimatedVisibility import androidx.compose.foundation.background import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.fillMaxSize @@ -62,6 +63,7 @@ import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.vectorResource +import androidx.compose.ui.text.style.TextOverflow import androidx.hilt.navigation.compose.hiltViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.navigation.NavDestination @@ -74,6 +76,7 @@ import androidx.navigation.compose.rememberNavController import com.geeksville.mesh.BuildConfig import com.geeksville.mesh.R import com.geeksville.mesh.model.DeviceVersion +import com.geeksville.mesh.model.Node import com.geeksville.mesh.model.UIViewModel import com.geeksville.mesh.navigation.ChannelsRoutes import com.geeksville.mesh.navigation.ConnectionsRoutes @@ -90,7 +93,10 @@ import com.geeksville.mesh.ui.common.components.MultipleChoiceAlertDialog import com.geeksville.mesh.ui.common.components.ScannedQrCodeDialog import com.geeksville.mesh.ui.common.components.SimpleAlertDialog import com.geeksville.mesh.ui.debug.DebugMenuActions +import com.geeksville.mesh.ui.node.components.NodeChip +import com.geeksville.mesh.ui.node.components.NodeMenuAction import com.geeksville.mesh.ui.radioconfig.RadioConfigMenuActions +import com.geeksville.mesh.ui.sharing.SharedContactDialog enum class TopLevelDestination(@StringRes val label: Int, val icon: ImageVector, val route: Route) { Contacts(R.string.contacts, Icons.AutoMirrored.TwoTone.Chat, ContactsRoutes.Contacts), @@ -243,16 +249,36 @@ fun MainScreen( modifier = Modifier .fillMaxSize() ) { + var sharedContact: Node? by remember { mutableStateOf(null) } + if (sharedContact != null) { + SharedContactDialog( + contact = sharedContact, + onDismiss = { sharedContact = null } + ) + } MainAppBar( viewModel = viewModel, isManaged = localConfig.security.isManaged, navController = navController, onAction = { action -> - when (action) { - MainMenuAction.DEBUG -> navController.navigate(Route.DebugPanel) - MainMenuAction.RADIO_CONFIG -> navController.navigate(RadioConfigRoutes.RadioConfig()) - MainMenuAction.QUICK_CHAT -> navController.navigate(ContactsRoutes.QuickChat) - else -> onAction(action) + if (action is MainMenuAction) { + when (action) { + MainMenuAction.DEBUG -> navController.navigate(Route.DebugPanel) + MainMenuAction.RADIO_CONFIG -> navController.navigate(RadioConfigRoutes.RadioConfig()) + MainMenuAction.QUICK_CHAT -> navController.navigate(ContactsRoutes.QuickChat) + else -> onAction(action) + } + } else if (action is NodeMenuAction) { + when (action) { + is NodeMenuAction.MoreDetails -> navController.navigate( + NodesRoutes.NodeDetail( + action.node.num + ) + ) + + is NodeMenuAction.Share -> sharedContact = action.node + else -> {} + } } }, ) @@ -339,7 +365,7 @@ private fun MainAppBar( isManaged: Boolean, navController: NavHostController, modifier: Modifier = Modifier, - onAction: (MainMenuAction) -> Unit + onAction: (Any?) -> Unit ) { val backStackEntry by navController.currentBackStackEntryAsState() val currentDestination = backStackEntry?.destination @@ -352,34 +378,25 @@ private fun MainAppBar( val title by viewModel.title.collectAsStateWithLifecycle("") TopAppBar( title = { - when { - currentDestination == null || isTopLevelRoute -> { - Text( - text = stringResource(id = R.string.app_name), - ) - } + val title = when { + currentDestination == null || isTopLevelRoute -> stringResource(id = R.string.app_name) - currentDestination.hasRoute() -> - Text( - stringResource(id = R.string.debug_panel), - ) + currentDestination.hasRoute() -> stringResource(id = R.string.debug_panel) - currentDestination.hasRoute() -> - Text( - stringResource(id = R.string.quick_chat), - ) + currentDestination.hasRoute() -> stringResource(id = R.string.quick_chat) - currentDestination.hasRoute() -> - Text( - stringResource(id = R.string.share_to), - ) + currentDestination.hasRoute() -> stringResource(id = R.string.share_to) - currentDestination.showLongNameTitle() -> { - Text( - title, - ) - } + currentDestination.showLongNameTitle() -> title + + else -> stringResource(id = R.string.app_name) } + Text( + text = title, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + style = MaterialTheme.typography.titleLarge, + ) }, modifier = modifier, navigationIcon = if (canNavigateBack && !isTopLevelRoute) { @@ -422,8 +439,18 @@ private fun TopBarActions( currentDestination: NavDestination?, isManaged: Boolean, isTopLevelRoute: Boolean, - onAction: (MainMenuAction) -> Unit + onAction: (Any?) -> Unit ) { + val ourNode by viewModel.ourNodeInfo.collectAsStateWithLifecycle() + val isConnected by viewModel.isConnected.collectAsStateWithLifecycle(false) + AnimatedVisibility(ourNode != null) { + NodeChip( + node = ourNode!!, + isThisNode = true, + isConnected = isConnected, + onAction = onAction + ) + } when { currentDestination == null || isTopLevelRoute -> MainMenuActions(isManaged, onAction) diff --git a/app/src/main/java/com/geeksville/mesh/ui/message/components/MessageItem.kt b/app/src/main/java/com/geeksville/mesh/ui/message/components/MessageItem.kt index 7968d3b64..ca9b43f80 100644 --- a/app/src/main/java/com/geeksville/mesh/ui/message/components/MessageItem.kt +++ b/app/src/main/java/com/geeksville/mesh/ui/message/components/MessageItem.kt @@ -176,6 +176,7 @@ internal fun MessageItem( .fillMaxWidth() .padding(horizontal = 4.dp), verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(4.dp) ) { NodeChip( node = if (fromLocal) ourNode else node, @@ -183,14 +184,13 @@ internal fun MessageItem( isConnected = isConnected, isThisNode = fromLocal, ) - Spacer(Modifier.width(4.dp)) Text( text = with(if (fromLocal) ourNode.user else node.user) { "$longName ($id)" }, overflow = TextOverflow.Ellipsis, maxLines = 1, - style = MaterialTheme.typography.labelLarge + style = MaterialTheme.typography.labelMedium, + modifier = Modifier.weight(1f, fill = false) ) - Spacer(Modifier.weight(1f)) MessageActions( onSendReaction = sendReaction, onSendReply = onReply, diff --git a/app/src/main/java/com/geeksville/mesh/ui/radioconfig/RadioConfig.kt b/app/src/main/java/com/geeksville/mesh/ui/radioconfig/RadioConfig.kt index e27f05f67..7407c942e 100644 --- a/app/src/main/java/com/geeksville/mesh/ui/radioconfig/RadioConfig.kt +++ b/app/src/main/java/com/geeksville/mesh/ui/radioconfig/RadioConfig.kt @@ -90,7 +90,16 @@ fun RadioConfigScreen( onNavigate: (Route) -> Unit = {} ) { val node by viewModel.destNode.collectAsStateWithLifecycle() - val nodeName: String? = node?.user?.longName + val ourNode by uiViewModel.ourNodeInfo.collectAsStateWithLifecycle() + val isLocal = node?.num == ourNode?.num + val nodeName: String? = node?.user?.longName?.let { + if (!isLocal) { + "$it (" + stringResource(R.string.remote) + ")" + } else { + it + } + } + nodeName?.let { uiViewModel.setTitle(it) } diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index fd852c283..8fef33c12 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -693,4 +693,5 @@ Export Keys Exports public and private keys to a file. Please store somewhere securely. Modules unlocked + Remote