Remove NodeChip long-click (#3220)

This commit is contained in:
Phil Oliver 2025-09-29 10:31:55 -04:00 committed by GitHub
parent 98ef72d240
commit 48a27ba022
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
29 changed files with 248 additions and 588 deletions

View file

@ -45,8 +45,6 @@
<ID>ComposableParamOrder:NodeFilterTextField.kt$NodeFilterTextField</ID>
<ID>ComposableParamOrder:NodeItem.kt$NodeItem</ID>
<ID>ComposableParamOrder:NodeKeyStatusIcon.kt$NodeKeyStatusIcon</ID>
<ID>ComposableParamOrder:NodeMenu.kt$NodeMenu</ID>
<ID>ComposableParamOrder:NodeScreen.kt$NodeScreen</ID>
<ID>ComposableParamOrder:PaxMetrics.kt$PaxMetricsChart</ID>
<ID>ComposableParamOrder:PermissionScreenLayout.kt$PermissionScreenLayout</ID>
<ID>ComposableParamOrder:PowerMetrics.kt$PowerMetricsChart</ID>
@ -91,11 +89,10 @@
<ID>FinalNewline:UsbRepositoryModule.kt$com.geeksville.mesh.repository.usb.UsbRepositoryModule.kt</ID>
<ID>ForbiddenComment:SafeBluetooth.kt$SafeBluetooth$// TODO: display some kind of UI about restarting BLE</ID>
<ID>LambdaParameterEventTrailing:Channel.kt$onConfirm</ID>
<ID>LambdaParameterEventTrailing:MainAppBar.kt$onAction</ID>
<ID>LambdaParameterEventTrailing:MainAppBar.kt$onClickChip</ID>
<ID>LambdaParameterEventTrailing:Message.kt$onClick</ID>
<ID>LambdaParameterEventTrailing:Message.kt$onSendMessage</ID>
<ID>LambdaParameterEventTrailing:MessageList.kt$onReply</ID>
<ID>LambdaParameterEventTrailing:NodeChip.kt$onAction</ID>
<ID>LambdaParameterEventTrailing:NodeDetail.kt$onClick</ID>
<ID>LambdaParameterEventTrailing:NodeDetail.kt$onSaveNotes</ID>
<ID>LambdaParameterInRestartableEffect:Channel.kt$onConfirm</ID>
@ -119,7 +116,6 @@
<ID>LongMethod:StoreForwardConfigItemList.kt$@Composable fun StoreForwardConfigScreen(navController: NavController, viewModel: RadioConfigViewModel = hiltViewModel())</ID>
<ID>LongMethod:TelemetryConfigItemList.kt$@Composable fun TelemetryConfigScreen(navController: NavController, viewModel: RadioConfigViewModel = hiltViewModel())</ID>
<ID>LongMethod:UserConfigItemList.kt$@Composable fun UserConfigScreen(navController: NavController, viewModel: RadioConfigViewModel = hiltViewModel())</ID>
<ID>LongParameterList:MessageList.kt$( nodes: List&lt;Node&gt;, ourNode: Node?, isConnected: Boolean, modifier: Modifier = Modifier, listState: LazyListState = rememberLazyListState(), messages: List&lt;Message&gt;, selectedIds: MutableState&lt;Set&lt;Long&gt;&gt;, onUnreadChanged: (Long) -&gt; Unit, onSendReaction: (String, Int) -&gt; Unit, onNodeMenuAction: (NodeMenuAction) -&gt; Unit, onDeleteMessages: (List&lt;Long&gt;) -&gt; Unit, onSendMessage: (messageText: String, contactKey: String) -&gt; Unit, contactKey: String, onReply: (Message?) -&gt; Unit, )</ID>
<ID>LongParameterList:MessageViewModel.kt$MessageViewModel$( private val nodeRepository: NodeRepository, radioConfigRepository: RadioConfigRepository, quickChatActionRepository: QuickChatActionRepository, private val serviceRepository: ServiceRepository, private val packetRepository: PacketRepository, private val uiPrefs: UiPrefs, private val meshServiceNotifications: MeshServiceNotifications, )</ID>
<ID>MagicNumber:BluetoothInterface.kt$BluetoothInterface$1000</ID>
<ID>MagicNumber:BluetoothInterface.kt$BluetoothInterface$500</ID>
@ -202,7 +198,6 @@
<ID>ModifierMissing:MessageActions.kt$ReplyButton</ID>
<ID>ModifierMissing:NetworkConfigItemList.kt$NetworkConfigScreen</ID>
<ID>ModifierMissing:NetworkDevices.kt$NetworkDevices</ID>
<ID>ModifierMissing:NodeMenu.kt$NodeMenu</ID>
<ID>ModifierMissing:NodeScreen.kt$NodeScreen</ID>
<ID>ModifierMissing:NodeStatusIcons.kt$NodeStatusIcons</ID>
<ID>ModifierMissing:PaxMetrics.kt$PaxMetricsItem</ID>
@ -231,7 +226,6 @@
<ID>ModifierNotUsedAtRoot:EditDeviceProfileDialog.kt$modifier = modifier.weight(1f)</ID>
<ID>ModifierNotUsedAtRoot:EnvironmentCharts.kt$modifier = modifier.width(dp)</ID>
<ID>ModifierNotUsedAtRoot:EnvironmentCharts.kt$modifier.width(dp)</ID>
<ID>ModifierNotUsedAtRoot:NodeChip.kt$modifier = modifier.width(IntrinsicSize.Min).defaultMinSize(minWidth = 72.dp).semantics { contentDescription = node.user.shortName.ifEmpty { "Node" } }</ID>
<ID>ModifierNotUsedAtRoot:PaxMetrics.kt$modifier.width(dp)</ID>
<ID>ModifierNotUsedAtRoot:PowerMetrics.kt$modifier = modifier.weight(weight = Y_AXIS_WEIGHT)</ID>
<ID>ModifierNotUsedAtRoot:PowerMetrics.kt$modifier = modifier.width(dp)</ID>
@ -401,7 +395,7 @@
<ID>ViewModelForwarding:Main.kt$VersionChecks(uIViewModel)</ID>
<ID>ViewModelInjection:DebugSearch.kt$viewModel</ID>
<ID>WildcardImport:UsbRepository.kt$import kotlinx.coroutines.flow.*</ID>
<ID>Wrapping:Message.kt${ event -&gt; when (event) { is MessageScreenEvent.SendMessage -&gt; { viewModel.sendMessage(event.text, contactKey, event.replyingToPacketId) if (event.replyingToPacketId != null) replyingToPacketId = null messageInputState.clearText() } is MessageScreenEvent.SendReaction -&gt; viewModel.sendReaction(event.emoji, event.messageId, contactKey) is MessageScreenEvent.DeleteMessages -&gt; { viewModel.deleteMessages(event.ids) selectedMessageIds.value = emptySet() showDeleteDialog = false } is MessageScreenEvent.ClearUnreadCount -&gt; viewModel.clearUnreadCount(contactKey, event.lastReadMessageId) is MessageScreenEvent.HandleNodeMenuAction -&gt; { when (val action = event.action) { is NodeMenuAction.DirectMessage -&gt; { val hasPKC = ourNode?.hasPKC == true &amp;&amp; action.node.hasPKC val targetChannel = if (hasPKC) { DataPacket.PKC_CHANNEL_INDEX } else { action.node.channel } navigateToMessages("$targetChannel${action.node.user.id}") } is NodeMenuAction.MoreDetails -&gt; navigateToNodeDetails(action.node.num) is NodeMenuAction.Share -&gt; sharedContact = action.node else -&gt; viewModel.handleNodeMenuAction(action) } } is MessageScreenEvent.SetTitle -&gt; viewModel.setTitle(event.title) is MessageScreenEvent.NavigateToMessages -&gt; navigateToMessages(event.contactKey) is MessageScreenEvent.NavigateToNodeDetails -&gt; navigateToNodeDetails(event.nodeNum) MessageScreenEvent.NavigateBack -&gt; onNavigateBack() is MessageScreenEvent.CopyToClipboard -&gt; { clipboardManager.nativeClipboard.setPrimaryClip(ClipData.newPlainText(event.text, event.text)) selectedMessageIds.value = emptySet() } } }</ID>
<ID>Wrapping:Message.kt${ event -&gt; when (event) { is MessageScreenEvent.SendMessage -&gt; { viewModel.sendMessage(event.text, contactKey, event.replyingToPacketId) if (event.replyingToPacketId != null) replyingToPacketId = null messageInputState.clearText() } is MessageScreenEvent.SendReaction -&gt; viewModel.sendReaction(event.emoji, event.messageId, contactKey) is MessageScreenEvent.DeleteMessages -&gt; { viewModel.deleteMessages(event.ids) selectedMessageIds.value = emptySet() showDeleteDialog = false } is MessageScreenEvent.ClearUnreadCount -&gt; viewModel.clearUnreadCount(contactKey, event.lastReadMessageId) is MessageScreenEvent.NodeDetails -&gt; navigateToNodeDetails(event.node.num) is MessageScreenEvent.SetTitle -&gt; viewModel.setTitle(event.title) is MessageScreenEvent.NavigateToMessages -&gt; navigateToMessages(event.contactKey) is MessageScreenEvent.NavigateToNodeDetails -&gt; navigateToNodeDetails(event.nodeNum) MessageScreenEvent.NavigateBack -&gt; onNavigateBack() is MessageScreenEvent.CopyToClipboard -&gt; { clipboardManager.nativeClipboard.setPrimaryClip(ClipData.newPlainText(event.text, event.text)) selectedMessageIds.value = emptySet() } } }</ID>
<ID>Wrapping:SerialConnectionImpl.kt$SerialConnectionImpl$(</ID>
<ID>Wrapping:SerialConnectionImpl.kt$SerialConnectionImpl$(port, object : SerialInputOutputManager.Listener { override fun onNewData(data: ByteArray) { listener.onDataReceived(data) } override fun onRunError(e: Exception?) { closed.set(true) ignoreException { port.dtr = false port.rts = false port.close() } closedLatch.countDown() listener.onDisconnected(e) } })</ID>
<ID>Wrapping:SerialInterface.kt$SerialInterface$(</ID>

View file

@ -65,7 +65,6 @@ class MessageItemTest {
onClick = {},
onLongClick = {},
onStatusClick = {},
isConnected = true,
ourNode = testNode,
)
}
@ -105,7 +104,6 @@ class MessageItemTest {
onClick = {},
onLongClick = {},
onStatusClick = {},
isConnected = true,
ourNode = testNode,
)
}

View file

@ -406,14 +406,7 @@ fun MapView(
val alpha = (index.toFloat() / (sortedPositions.size.toFloat() - 1))
val color = Color(focusedNode!!.colors.second).copy(alpha = alpha)
if (index == sortedPositions.lastIndex) {
MarkerComposable(state = markerState, zIndex = 1f) {
NodeChip(
node = focusedNode,
isThisNode = false,
isConnected = false,
onAction = {},
)
}
MarkerComposable(state = markerState, zIndex = 1f) { NodeChip(node = focusedNode) }
} else {
MarkerInfoWindowComposable(
state = markerState,

View file

@ -59,7 +59,7 @@ fun ClusterItemsListDialog(
@Composable
private fun ClusterDialogListItem(item: NodeClusterItem, onClick: () -> Unit, modifier: Modifier = Modifier) {
ListItem(
leadingContent = { NodeChip(node = item.node, enabled = false, isThisNode = false, isConnected = false) {} },
leadingContent = { NodeChip(node = item.node) },
headlineContent = { Text(item.nodeTitle) },
supportingContent = {
if (item.nodeSnippet.isNotBlank()) {

View file

@ -63,8 +63,6 @@ fun NodeClusterMarkers(
navigateToNodeDetails(item.node.num)
false
},
clusterItemContent = { clusterItem ->
NodeChip(node = clusterItem.node, enabled = false, isThisNode = false, isConnected = false) {}
},
clusterItemContent = { clusterItem -> NodeChip(node = clusterItem.node) },
)
}

View file

@ -44,19 +44,17 @@ fun NodeMapScreen(
val positions = state.positionLogs
val destNum = state.node?.num
val ourNodeInfo by uiViewModel.ourNodeInfo.collectAsStateWithLifecycle()
val isConnected by uiViewModel.isConnectedStateFlow.collectAsStateWithLifecycle()
Scaffold(
topBar = {
MainAppBar(
title = state.node?.user?.longName ?: "",
ourNode = ourNodeInfo,
isConnected = isConnected,
showNodeChip = false,
canNavigateUp = true,
onNavigateUp = navController::navigateUp,
actions = {},
onAction = {},
onClickChip = {},
)
},
) { paddingValues ->

View file

@ -64,10 +64,7 @@ fun NavGraphBuilder.nodesGraph(navController: NavHostController, uiViewModel: UI
composable<NodesRoutes.Nodes>(
deepLinks = listOf(navDeepLink<NodesRoutes.Nodes>(basePath = "$DEEP_LINK_BASE_URI/nodes")),
) {
NodeScreen(
navigateToMessages = { navController.navigate(ContactsRoutes.Messages(it)) },
navigateToNodeDetails = { navController.navigate(NodesRoutes.NodeDetailGraph(it)) },
)
NodeScreen(navigateToNodeDetails = { navController.navigate(NodesRoutes.NodeDetailGraph(it)) })
}
nodeDetailGraph(navController, uiViewModel)
}

View file

@ -95,14 +95,11 @@ import com.geeksville.mesh.ui.common.components.ScannedQrCodeDialog
import com.geeksville.mesh.ui.connections.DeviceType
import com.geeksville.mesh.ui.connections.components.TopLevelNavIcon
import com.geeksville.mesh.ui.metrics.annotateTraceroute
import com.geeksville.mesh.ui.node.components.NodeMenuAction
import com.geeksville.mesh.ui.sharing.SharedContactDialog
import com.google.accompanist.permissions.ExperimentalPermissionsApi
import com.google.accompanist.permissions.isGranted
import com.google.accompanist.permissions.rememberPermissionState
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.launch
import org.meshtastic.core.database.model.Node
import org.meshtastic.core.model.DeviceVersion
import org.meshtastic.core.navigation.ConnectionsRoutes
import org.meshtastic.core.navigation.ContactsRoutes
@ -337,11 +334,6 @@ fun MainScreen(uIViewModel: UIViewModel = hiltViewModel(), scanModel: BTScanMode
) {
Scaffold(snackbarHost = { SnackbarHost(uIViewModel.snackBarHostState) }) { _ ->
Column(modifier = Modifier.fillMaxSize()) {
var sharedContact: Node? by remember { mutableStateOf(null) }
if (sharedContact != null) {
SharedContactDialog(contact = sharedContact, onDismiss = { sharedContact = null })
}
fun NavDestination.hasGlobalAppBar(): Boolean =
// List of screens to exclude from having the global app bar
listOf(
@ -384,22 +376,14 @@ fun MainScreen(uIViewModel: UIViewModel = hiltViewModel(), scanModel: BTScanMode
MainAppBar(
navController = navController,
ourNode = ourNodeInfo,
isConnected = connectionState.isConnected(),
onAction = { action ->
when (action) {
is NodeMenuAction.MoreDetails -> {
navController.navigate(
NodesRoutes.NodeDetailGraph(action.node.num),
{
launchSingleTop = true
restoreState = true
},
)
}
is NodeMenuAction.Share -> sharedContact = action.node
else -> {}
}
onClickChip = {
navController.navigate(
NodesRoutes.NodeDetailGraph(it.num),
{
launchSingleTop = true
restoreState = true
},
)
},
)
}

View file

@ -45,7 +45,6 @@ import androidx.navigation.NavHostController
import androidx.navigation.compose.currentBackStackEntryAsState
import com.geeksville.mesh.ui.debug.DebugMenuActions
import com.geeksville.mesh.ui.node.components.NodeChip
import com.geeksville.mesh.ui.node.components.NodeMenuAction
import org.meshtastic.core.database.model.Node
import org.meshtastic.core.navigation.ContactsRoutes
import org.meshtastic.core.navigation.SettingsRoutes
@ -58,8 +57,7 @@ fun MainAppBar(
modifier: Modifier = Modifier,
navController: NavHostController,
ourNode: Node?,
isConnected: Boolean,
onAction: (NodeMenuAction) -> Unit,
onClickChip: (Node) -> Unit,
) {
val backStackEntry by navController.currentBackStackEntryAsState()
val currentDestination = backStackEntry?.destination
@ -86,7 +84,6 @@ fun MainAppBar(
subtitle = null,
canNavigateUp = navController.previousBackStackEntry != null,
ourNode = ourNode,
isConnected = isConnected,
showNodeChip = false,
onNavigateUp = navController::navigateUp,
actions = {
@ -97,7 +94,7 @@ fun MainAppBar(
}
}
},
onAction = onAction,
onClickChip = onClickChip,
)
}
@ -108,12 +105,11 @@ fun MainAppBar(
title: String,
subtitle: String? = null,
ourNode: Node?,
isConnected: Boolean,
showNodeChip: Boolean,
canNavigateUp: Boolean,
onNavigateUp: () -> Unit,
actions: @Composable () -> Unit,
onAction: (NodeMenuAction) -> Unit,
onClickChip: (Node) -> Unit,
) {
TopAppBar(
title = {
@ -147,13 +143,7 @@ fun MainAppBar(
}
},
actions = {
TopBarActions(
ourNode = ourNode,
isConnected = isConnected,
showNodeChip = showNodeChip,
actions = actions,
onAction = onAction,
)
TopBarActions(ourNode = ourNode, showNodeChip = showNodeChip, actions = actions, onClickChip = onClickChip)
},
)
}
@ -161,20 +151,13 @@ fun MainAppBar(
@Composable
private fun TopBarActions(
ourNode: Node?,
isConnected: Boolean,
showNodeChip: Boolean,
actions: @Composable () -> Unit,
onAction: (NodeMenuAction) -> Unit,
onClickChip: (Node) -> Unit,
) {
AnimatedVisibility(visible = showNodeChip, enter = fadeIn(), exit = fadeOut()) {
ourNode?.let {
NodeChip(
modifier = Modifier.padding(horizontal = 16.dp),
node = it,
isThisNode = true,
isConnected = isConnected,
onAction = onAction,
)
ourNode?.let { node ->
NodeChip(modifier = Modifier.padding(horizontal = 16.dp), node = node, onClick = onClickChip)
}
}
@ -189,7 +172,6 @@ private fun MainAppBarPreview(@PreviewParameter(BooleanProvider::class) canNavig
title = "Title",
subtitle = "Subtitle",
ourNode = previewNode,
isConnected = false,
showNodeChip = true,
canNavigateUp = canNavigateUp,
onNavigateUp = {},

View file

@ -71,14 +71,11 @@ import com.geeksville.mesh.ui.connections.components.ConnectionsSegmentedBar
import com.geeksville.mesh.ui.connections.components.CurrentlyConnectedInfo
import com.geeksville.mesh.ui.connections.components.NetworkDevices
import com.geeksville.mesh.ui.connections.components.UsbDevices
import com.geeksville.mesh.ui.node.components.NodeMenuAction
import com.geeksville.mesh.ui.settings.components.SettingsItem
import com.geeksville.mesh.ui.settings.radio.RadioConfigViewModel
import com.geeksville.mesh.ui.settings.radio.components.PacketResponseStateDialog
import com.geeksville.mesh.ui.sharing.SharedContactDialog
import com.google.accompanist.permissions.ExperimentalPermissionsApi
import kotlinx.coroutines.delay
import org.meshtastic.core.database.model.Node
import org.meshtastic.core.navigation.Route
import org.meshtastic.core.navigation.SettingsRoutes
import org.meshtastic.core.strings.R
@ -177,27 +174,17 @@ fun ConnectionsScreen(
scanModel.setErrorText(context.getString(it, firmwareString))
}
}
var showSharedContact by remember { mutableStateOf<Node?>(null) }
if (showSharedContact != null) {
SharedContactDialog(contact = showSharedContact, onDismiss = { showSharedContact = null })
}
Scaffold(
topBar = {
MainAppBar(
title = stringResource(R.string.connections),
ourNode = ourNode,
isConnected = connectionState.isConnected(),
showNodeChip = ourNode != null && connectionState.isConnected(),
canNavigateUp = false,
onNavigateUp = {},
actions = {},
onAction = { action ->
when (action) {
is NodeMenuAction.MoreDetails -> onClickNodeChip(action.node.num)
else -> {}
}
},
onClickChip = { onClickNodeChip(it.num) },
)
},
) { paddingValues ->
@ -221,7 +208,6 @@ fun ConnectionsScreen(
CurrentlyConnectedInfo(
node = node,
onNavigateToNodeDetails = onNavigateToNodeDetails,
onSetShowSharedContact = { showSharedContact = it },
onClickDisconnect = { scanModel.disconnect() },
bluetoothRssi = bluetoothRssi,
)

View file

@ -26,7 +26,6 @@ import androidx.compose.foundation.layout.padding
import androidx.compose.material3.Button
import androidx.compose.material3.ButtonDefaults
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
@ -41,7 +40,6 @@ import com.geeksville.mesh.MeshProtos
import com.geeksville.mesh.PaxcountProtos
import com.geeksville.mesh.TelemetryProtos
import com.geeksville.mesh.ui.node.components.NodeChip
import com.geeksville.mesh.ui.node.components.NodeMenuAction
import org.meshtastic.core.database.model.Node
import org.meshtastic.core.strings.R
import org.meshtastic.core.ui.component.MaterialBatteryInfo
@ -54,7 +52,6 @@ import org.meshtastic.core.ui.theme.StatusColors.StatusRed
fun CurrentlyConnectedInfo(
node: Node,
onNavigateToNodeDetails: (Int) -> Unit,
onSetShowSharedContact: (Node) -> Unit,
onClickDisconnect: () -> Unit,
modifier: Modifier = Modifier,
bluetoothRssi: Int? = null,
@ -75,19 +72,7 @@ fun CurrentlyConnectedInfo(
modifier = Modifier.padding(horizontal = 16.dp, vertical = 8.dp),
horizontalAlignment = Alignment.CenterHorizontally,
) {
NodeChip(
node = node,
isThisNode = true,
isConnected = true,
onAction = { action ->
when (action) {
is NodeMenuAction.MoreDetails -> onNavigateToNodeDetails(node.num)
is NodeMenuAction.Share -> onSetShowSharedContact(node)
else -> {}
}
},
)
NodeChip(node = node, onClick = { onNavigateToNodeDetails(it.num) })
}
Column(modifier = Modifier.weight(1f, fill = true)) {
@ -124,26 +109,22 @@ fun CurrentlyConnectedInfo(
@Composable
private fun CurrentlyConnectedInfoPreview() {
AppTheme {
Surface {
CurrentlyConnectedInfo(
node =
Node(
num = 13444,
user =
MeshProtos.User.newBuilder().setShortName("\uD83E\uDEE0").setLongName("John Doe").build(),
isIgnored = false,
paxcounter = PaxcountProtos.Paxcount.newBuilder().setBle(10).setWifi(5).build(),
environmentMetrics =
TelemetryProtos.EnvironmentMetrics.newBuilder()
.setTemperature(25f)
.setRelativeHumidity(60f)
.build(),
),
bluetoothRssi = -75, // Example RSSI for signal preview
onNavigateToNodeDetails = {},
onSetShowSharedContact = {},
onClickDisconnect = {},
)
}
CurrentlyConnectedInfo(
node =
Node(
num = 13444,
user = MeshProtos.User.newBuilder().setShortName("\uD83E\uDEE0").setLongName("John Doe").build(),
isIgnored = false,
paxcounter = PaxcountProtos.Paxcount.newBuilder().setBle(10).setWifi(5).build(),
environmentMetrics =
TelemetryProtos.EnvironmentMetrics.newBuilder()
.setTemperature(25f)
.setRelativeHumidity(60f)
.build(),
),
bluetoothRssi = -75, // Example RSSI for signal preview
onNavigateToNodeDetails = {},
onClickDisconnect = {},
)
}
}

View file

@ -64,7 +64,6 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.geeksville.mesh.AppOnlyProtos
import com.geeksville.mesh.model.Contact
import com.geeksville.mesh.ui.common.components.MainAppBar
import com.geeksville.mesh.ui.node.components.NodeMenuAction
import org.meshtastic.core.strings.R
import java.util.concurrent.TimeUnit
@ -144,17 +143,11 @@ fun ContactsScreen(
MainAppBar(
title = stringResource(R.string.conversations),
ourNode = ourNode,
isConnected = connectionState.isConnected(),
showNodeChip = ourNode != null && connectionState.isConnected(),
canNavigateUp = false,
onNavigateUp = {},
actions = {},
onAction = { action ->
when (action) {
is NodeMenuAction.MoreDetails -> onClickNodeChip(action.node.num)
else -> {}
}
},
onClickChip = { onClickNodeChip(it.num) },
)
},
floatingActionButton = {

View file

@ -27,7 +27,6 @@ import androidx.compose.ui.res.stringResource
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.geeksville.mesh.ui.common.components.MainAppBar
import com.geeksville.mesh.ui.node.components.NodeMenuAction
import org.meshtastic.core.strings.R
@Composable
@ -47,17 +46,11 @@ fun MapScreen(
MainAppBar(
title = stringResource(R.string.map),
ourNode = ourNodeInfo,
isConnected = isConnected,
showNodeChip = ourNodeInfo != null && isConnected,
canNavigateUp = false,
onNavigateUp = {},
actions = {},
onAction = { action ->
when (action) {
is NodeMenuAction.MoreDetails -> onClickNodeChip(action.node.num)
else -> {}
}
},
onClickChip = { onClickNodeChip(it.num) },
)
},
) { paddingValues ->

View file

@ -97,7 +97,6 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.geeksville.mesh.AppOnlyProtos
import com.geeksville.mesh.ui.common.components.SecurityIcon
import com.geeksville.mesh.ui.node.components.NodeKeyStatusIcon
import com.geeksville.mesh.ui.node.components.NodeMenuAction
import com.geeksville.mesh.ui.sharing.SharedContactDialog
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch
@ -207,24 +206,7 @@ internal fun MessageScreen(
is MessageScreenEvent.ClearUnreadCount ->
viewModel.clearUnreadCount(contactKey, event.lastReadMessageId)
is MessageScreenEvent.HandleNodeMenuAction -> {
when (val action = event.action) {
is NodeMenuAction.DirectMessage -> {
val hasPKC = ourNode?.hasPKC == true && action.node.hasPKC
val targetChannel =
if (hasPKC) {
DataPacket.PKC_CHANNEL_INDEX
} else {
action.node.channel
}
navigateToMessages("$targetChannel${action.node.user.id}")
}
is NodeMenuAction.MoreDetails -> navigateToNodeDetails(action.node.num)
is NodeMenuAction.Share -> sharedContact = action.node
else -> viewModel.handleNodeMenuAction(action)
}
}
is MessageScreenEvent.NodeDetails -> navigateToNodeDetails(event.node.num)
is MessageScreenEvent.SetTitle -> viewModel.setTitle(event.title)
is MessageScreenEvent.NavigateToMessages -> navigateToMessages(event.contactKey)
@ -297,7 +279,6 @@ internal fun MessageScreen(
MessageList(
nodes = nodes,
ourNode = ourNode,
isConnected = connectionState.isConnected(),
modifier = Modifier.fillMaxSize(),
listState = listState,
messages = messages,
@ -308,7 +289,7 @@ internal fun MessageScreen(
onSendMessage = { text, contactKey -> viewModel.sendMessage(text, contactKey) },
contactKey = contactKey,
onReply = { message -> replyingToPacketId = message?.packetId },
onNodeMenuAction = { action -> onEvent(MessageScreenEvent.HandleNodeMenuAction(action)) },
onClickChip = { onEvent(MessageScreenEvent.NodeDetails(it)) },
)
// Show FAB if we can scroll towards the newest messages (index 0).
if (listState.canScrollBackward) {

View file

@ -48,7 +48,6 @@ import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import com.geeksville.mesh.ui.message.components.MessageItem
import com.geeksville.mesh.ui.message.components.ReactionDialog
import com.geeksville.mesh.ui.node.components.NodeMenuAction
import kotlinx.coroutines.FlowPreview
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.flow.debounce
@ -107,14 +106,13 @@ fun DeliveryInfo(
internal fun MessageList(
nodes: List<Node>,
ourNode: Node?,
isConnected: Boolean,
modifier: Modifier = Modifier,
listState: LazyListState = rememberLazyListState(),
messages: List<Message>,
selectedIds: MutableState<Set<Long>>,
onUnreadChanged: (Long) -> Unit,
onSendReaction: (String, Int) -> Unit,
onNodeMenuAction: (NodeMenuAction) -> Unit,
onClickChip: (Node) -> Unit,
onDeleteMessages: (List<Long>) -> Unit,
onSendMessage: (messageText: String, contactKey: String) -> Unit,
contactKey: String,
@ -173,13 +171,12 @@ internal fun MessageList(
selectedIds.toggle(msg.uuid)
haptics.performHapticFeedback(HapticFeedbackType.LongPress)
},
onAction = onNodeMenuAction,
onClickChip = onClickChip,
onStatusClick = { showStatusDialog = msg },
onReply = { onReply(msg) },
emojis = msg.emojis,
sendReaction = { onSendReaction(it, msg.packetId) },
onShowReactions = { showReactionDialog = msg.emojis },
isConnected = isConnected,
onNavigateToOriginalMessage = {
coroutineScope.launch {
val targetIndex = messages.indexOfFirst { it.packetId == msg.replyId }

View file

@ -17,16 +17,15 @@
package com.geeksville.mesh.ui.message
import com.geeksville.mesh.ui.node.components.NodeMenuAction
import org.meshtastic.core.database.model.Node
/**
* Defines the various user interactions that can occur on the [MessageScreen].
* These events are typically handled by the [com.geeksville.mesh.model.UIViewModel].
* Defines the various user interactions that can occur on the [MessageScreen]. These events are typically handled by
* the [com.geeksville.mesh.model.UIViewModel].
*/
internal sealed interface MessageScreenEvent {
/** Send a new text message. */
data class SendMessage(val text: String, val replyingToPacketId: Int? = null) :
MessageScreenEvent
data class SendMessage(val text: String, val replyingToPacketId: Int? = null) : MessageScreenEvent
/** Send an emoji reaction to a specific message. */
data class SendReaction(val emoji: String, val messageId: Int) : MessageScreenEvent
@ -38,7 +37,7 @@ internal sealed interface MessageScreenEvent {
data class ClearUnreadCount(val lastReadMessageId: Long) : MessageScreenEvent
/** Handle an action from a node's context menu. */
data class HandleNodeMenuAction(val action: NodeMenuAction) : MessageScreenEvent
data class NodeDetails(val node: Node) : MessageScreenEvent
/** Set the title of the screen (typically the contact or channel name). */
data class SetTitle(val title: String) : MessageScreenEvent

View file

@ -24,7 +24,6 @@ import com.geeksville.mesh.channelSet
import com.geeksville.mesh.service.MeshServiceNotifications
import com.geeksville.mesh.service.ServiceAction
import com.geeksville.mesh.service.ServiceRepository
import com.geeksville.mesh.ui.node.components.NodeMenuAction
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.MutableStateFlow
@ -43,7 +42,6 @@ import org.meshtastic.core.data.repository.RadioConfigRepository
import org.meshtastic.core.database.model.Message
import org.meshtastic.core.database.model.Node
import org.meshtastic.core.model.DataPacket
import org.meshtastic.core.model.Position
import org.meshtastic.core.prefs.ui.UiPrefs
import timber.log.Timber
import javax.inject.Inject
@ -102,10 +100,6 @@ constructor(
initialValue = emptyList(),
)
// TODO this should be moved to a repository class
private val _lastTraceRouteTime = MutableStateFlow<Long?>(null)
val lastTraceRouteTime: StateFlow<Long?> = _lastTraceRouteTime.asStateFlow()
fun setTitle(title: String) {
viewModelScope.launch { _title.value = title }
}
@ -157,22 +151,6 @@ constructor(
if (unreadCount == 0) meshServiceNotifications.cancelMessageNotification(contact)
}
fun handleNodeMenuAction(action: NodeMenuAction) {
when (action) {
is NodeMenuAction.Remove -> removeNode(action.node.num)
is NodeMenuAction.Ignore -> ignoreNode(action.node)
is NodeMenuAction.Favorite -> favoriteNode(action.node)
is NodeMenuAction.RequestUserInfo -> requestUserInfo(action.node.num)
is NodeMenuAction.RequestPosition -> requestPosition(action.node.num)
is NodeMenuAction.TraceRoute -> {
requestTraceroute(action.node.num)
_lastTraceRouteTime.value = System.currentTimeMillis()
}
else -> {}
}
}
private fun favoriteNode(node: Node) = viewModelScope.launch {
try {
serviceRepository.onServiceAction(ServiceAction.Favorite(node))
@ -188,51 +166,4 @@ constructor(
Timber.e("Send DataPacket error: ${ex.message}")
}
}
private fun removeNode(nodeNum: Int) = viewModelScope.launch(Dispatchers.IO) {
Timber.i("Removing node '$nodeNum'")
try {
val packetId = serviceRepository.meshService?.packetId ?: return@launch
serviceRepository.meshService?.removeByNodenum(packetId, nodeNum)
nodeRepository.deleteNode(nodeNum)
} catch (ex: RemoteException) {
Timber.e("Remove node error: ${ex.message}")
}
}
private fun ignoreNode(node: Node) = viewModelScope.launch {
try {
serviceRepository.onServiceAction(ServiceAction.Ignore(node))
} catch (ex: RemoteException) {
Timber.e(ex, "Ignore node error:")
}
}
private fun requestUserInfo(destNum: Int) {
Timber.i("Requesting UserInfo for '$destNum'")
try {
serviceRepository.meshService?.requestUserInfo(destNum)
} catch (ex: RemoteException) {
Timber.e("Request NodeInfo error: ${ex.message}")
}
}
fun requestPosition(destNum: Int, position: Position = Position(0.0, 0.0, 0)) {
Timber.i("Requesting position for '$destNum'")
try {
serviceRepository.meshService?.requestPosition(destNum, position)
} catch (ex: RemoteException) {
Timber.e("Request position error: ${ex.message}")
}
}
fun requestTraceroute(destNum: Int) {
Timber.i("Requesting traceroute for '$destNum'")
try {
val packetId = serviceRepository.meshService?.packetId ?: return
serviceRepository.meshService?.requestTraceroute(packetId, destNum)
} catch (ex: RemoteException) {
Timber.e("Request traceroute error: ${ex.message}")
}
}
}

View file

@ -51,7 +51,6 @@ import androidx.compose.ui.tooling.preview.PreviewLightDark
import androidx.compose.ui.unit.dp
import com.geeksville.mesh.ui.common.preview.NodePreviewParameterProvider
import com.geeksville.mesh.ui.node.components.NodeChip
import com.geeksville.mesh.ui.node.components.NodeMenuAction
import org.meshtastic.core.database.entity.Reaction
import org.meshtastic.core.database.model.Message
import org.meshtastic.core.database.model.Node
@ -77,9 +76,8 @@ internal fun MessageItem(
emojis: List<Reaction> = emptyList(),
onClick: () -> Unit = {},
onLongClick: () -> Unit = {},
onAction: (NodeMenuAction) -> Unit = {},
onClickChip: (Node) -> Unit = {},
onStatusClick: () -> Unit = {},
isConnected: Boolean,
onNavigateToOriginalMessage: (Int) -> Unit = {},
) = Column(
modifier =
@ -134,12 +132,8 @@ internal fun MessageItem(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(4.dp),
) {
NodeChip(
node = if (message.fromLocal) ourNode else node,
onAction = onAction,
isConnected = isConnected,
isThisNode = message.fromLocal,
)
val chipNode = if (message.fromLocal) ourNode else node
NodeChip(node = chipNode, onClick = onClickChip)
Text(
text = with(if (message.fromLocal) ourNode.user else node.user) { "$longName ($id)" },
overflow = TextOverflow.Ellipsis,
@ -325,7 +319,6 @@ private fun MessageItemPreview() {
onClick = {},
onLongClick = {},
onStatusClick = {},
isConnected = true,
ourNode = sent.node,
)
@ -336,7 +329,6 @@ private fun MessageItemPreview() {
onClick = {},
onLongClick = {},
onStatusClick = {},
isConnected = true,
ourNode = sent.node,
)
@ -347,7 +339,6 @@ private fun MessageItemPreview() {
onClick = {},
onLongClick = {},
onStatusClick = {},
isConnected = true,
ourNode = sent.node,
)
}

View file

@ -196,7 +196,6 @@ fun NodeDetailScreen(
val environmentState by viewModel.environmentState.collectAsStateWithLifecycle()
val lastTracerouteTime by nodeDetailViewModel.lastTraceRouteTime.collectAsStateWithLifecycle()
val ourNode by nodeDetailViewModel.ourNodeInfo.collectAsStateWithLifecycle()
val connectionState by nodeDetailViewModel.connectionState.collectAsStateWithLifecycle()
val availableLogs by
remember(state, environmentState) {
@ -225,12 +224,11 @@ fun NodeDetailScreen(
MainAppBar(
title = node?.user?.longName ?: "",
ourNode = ourNode,
isConnected = connectionState.isConnected(),
showNodeChip = false,
canNavigateUp = true,
onNavigateUp = onNavigateUp,
actions = {},
onAction = {},
onClickChip = {},
)
},
) { paddingValues ->
@ -642,7 +640,9 @@ private fun DeviceActions(
displayIgnoreDialog = false
displayRemoveDialog = false
},
onAction = { onAction(NodeDetailAction.HandleNodeMenuAction(it)) },
onConfirmFavorite = { onAction(NodeDetailAction.HandleNodeMenuAction(NodeMenuAction.Favorite(it))) },
onConfirmIgnore = { onAction(NodeDetailAction.HandleNodeMenuAction(NodeMenuAction.Ignore(it))) },
onConfirmRemove = { onAction(NodeDetailAction.HandleNodeMenuAction(NodeMenuAction.Remove(it))) },
)
TitledCard(title = stringResource(R.string.actions)) {
SettingsItem(

View file

@ -45,8 +45,6 @@ constructor(
val ourNodeInfo: StateFlow<Node?> = nodeRepository.ourNodeInfo
val connectionState = serviceRepository.connectionState
private val _lastTraceRouteTime = MutableStateFlow<Long?>(null)
val lastTraceRouteTime: StateFlow<Long?> = _lastTraceRouteTime.asStateFlow()

View file

@ -29,9 +29,20 @@ import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.DoDisturbOn
import androidx.compose.material.icons.outlined.DoDisturbOn
import androidx.compose.material.icons.rounded.DeleteOutline
import androidx.compose.material.icons.rounded.Star
import androidx.compose.material.icons.rounded.StarBorder
import androidx.compose.material3.DropdownMenu
import androidx.compose.material3.DropdownMenuItem
import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
import androidx.compose.material3.Icon
import androidx.compose.material3.LocalContentColor
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.material3.animateFloatingActionButton
import androidx.compose.runtime.Composable
import androidx.compose.runtime.derivedStateOf
@ -41,34 +52,31 @@ import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.graphicsLayer
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.DpOffset
import androidx.compose.ui.unit.dp
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.geeksville.mesh.AdminProtos
import com.geeksville.mesh.service.ConnectionState
import com.geeksville.mesh.ui.common.components.MainAppBar
import com.geeksville.mesh.ui.node.components.NodeActionDialogs
import com.geeksville.mesh.ui.node.components.NodeFilterTextField
import com.geeksville.mesh.ui.node.components.NodeItem
import com.geeksville.mesh.ui.node.components.NodeMenuAction
import com.geeksville.mesh.ui.sharing.AddContactFAB
import com.geeksville.mesh.ui.sharing.SharedContactDialog
import com.geeksville.mesh.ui.sharing.supportsQrCodeSharing
import org.meshtastic.core.database.model.Node
import org.meshtastic.core.model.DataPacket
import org.meshtastic.core.model.DeviceVersion
import org.meshtastic.core.strings.R
import org.meshtastic.core.ui.component.rememberTimeTickWithLifecycle
import org.meshtastic.core.ui.theme.StatusColors.StatusRed
@OptIn(ExperimentalFoundationApi::class, ExperimentalMaterial3ExpressiveApi::class)
@Suppress("LongMethod", "CyclomaticComplexMethod")
@Composable
fun NodeScreen(
nodesViewModel: NodesViewModel = hiltViewModel(),
navigateToMessages: (String) -> Unit,
navigateToNodeDetails: (Int) -> Unit,
) {
fun NodeScreen(nodesViewModel: NodesViewModel = hiltViewModel(), navigateToNodeDetails: (Int) -> Unit) {
val state by nodesViewModel.nodesUiState.collectAsStateWithLifecycle()
val nodes by nodesViewModel.nodeList.collectAsStateWithLifecycle()
@ -83,11 +91,6 @@ fun NodeScreen(
val currentTimeMillis = rememberTimeTickWithLifecycle()
val connectionState by nodesViewModel.connectionState.collectAsStateWithLifecycle()
var showSharedContact: Node? by remember { mutableStateOf(null) }
if (showSharedContact != null) {
SharedContactDialog(contact = showSharedContact, onDismiss = { showSharedContact = null })
}
val isScrollInProgress by remember { derivedStateOf { listState.isScrollInProgress } }
Scaffold(
topBar = {
@ -95,12 +98,11 @@ fun NodeScreen(
title = stringResource(R.string.nodes),
subtitle = stringResource(R.string.node_count_template, onlineNodeCount, totalNodeCount),
ourNode = ourNode,
isConnected = connectionState.isConnected(),
showNodeChip = false,
canNavigateUp = false,
onNavigateUp = {},
actions = {},
onAction = {},
onClickChip = {},
)
},
floatingActionButton = {
@ -151,37 +153,124 @@ fun NodeScreen(
}
items(nodes, key = { it.num }) { node ->
NodeItem(
modifier = Modifier.animateItem(),
thisNode = ourNode,
thatNode = node,
distanceUnits = state.distanceUnits,
tempInFahrenheit = state.tempInFahrenheit,
onAction = { menuItem ->
when (menuItem) {
is NodeMenuAction.Remove -> nodesViewModel.removeNode(node.num)
is NodeMenuAction.Ignore -> nodesViewModel.ignoreNode(node)
is NodeMenuAction.Favorite -> nodesViewModel.favoriteNode(node)
is NodeMenuAction.DirectMessage -> {
val hasPKC = nodesViewModel.ourNodeInfo.value?.hasPKC == true && node.hasPKC
val channel = if (hasPKC) DataPacket.PKC_CHANNEL_INDEX else node.channel
navigateToMessages("$channel${node.user.id}")
}
var displayFavoriteDialog by remember { mutableStateOf(false) }
var displayIgnoreDialog by remember { mutableStateOf(false) }
var displayRemoveDialog by remember { mutableStateOf(false) }
is NodeMenuAction.RequestUserInfo -> nodesViewModel.requestUserInfo(node.num)
is NodeMenuAction.RequestPosition -> nodesViewModel.requestPosition(node.num)
is NodeMenuAction.TraceRoute -> nodesViewModel.requestTraceroute(node.num)
is NodeMenuAction.MoreDetails -> navigateToNodeDetails(node.num)
is NodeMenuAction.Share -> showSharedContact = node
}
NodeActionDialogs(
node = node,
displayFavoriteDialog = displayFavoriteDialog,
displayIgnoreDialog = displayIgnoreDialog,
displayRemoveDialog = displayRemoveDialog,
onDismissMenuRequest = {
displayFavoriteDialog = false
displayIgnoreDialog = false
displayRemoveDialog = false
},
expanded = state.showDetails,
currentTimeMillis = currentTimeMillis,
isConnected = connectionState.isConnected(),
onConfirmFavorite = nodesViewModel::favoriteNode,
onConfirmIgnore = nodesViewModel::ignoreNode,
onConfirmRemove = { nodesViewModel.removeNode(it.num) },
)
Box {
var showContextMenu by remember { mutableStateOf(false) }
NodeItem(
modifier = Modifier.animateItem(),
thisNode = ourNode,
thatNode = node,
distanceUnits = state.distanceUnits,
tempInFahrenheit = state.tempInFahrenheit,
onClickChip = { navigateToNodeDetails(it.num) },
onLongClick = { showContextMenu = true },
expanded = state.showDetails,
currentTimeMillis = currentTimeMillis,
isConnected = connectionState.isConnected(),
)
val isThisNode = remember(node) { ourNode?.num == node.num }
ContextMenu(
expanded = !isThisNode && showContextMenu,
node = node,
onClickFavorite = { displayFavoriteDialog = true },
onClickIgnore = { displayIgnoreDialog = true },
onClickRemove = { displayRemoveDialog = true },
onDismiss = { showContextMenu = false },
)
}
}
item { Spacer(modifier = Modifier.height(88.dp)) }
}
}
}
}
@Composable
private fun ContextMenu(
expanded: Boolean,
node: Node,
onClickFavorite: (Node) -> Unit,
onClickIgnore: (Node) -> Unit,
onClickRemove: (Node) -> Unit,
onDismiss: () -> Unit,
) {
DropdownMenu(expanded = expanded, onDismissRequest = onDismiss, offset = DpOffset(16.dp, 0.dp)) {
val isFavorite = node.isFavorite
val isIgnored = node.isIgnored
DropdownMenuItem(
onClick = {
onClickFavorite(node)
onDismiss()
},
enabled = !isIgnored,
leadingIcon = {
Icon(
imageVector = if (isFavorite) Icons.Rounded.Star else Icons.Rounded.StarBorder,
contentDescription = null,
)
},
text = { Text(stringResource(if (isFavorite) R.string.remove_favorite else R.string.add_favorite)) },
)
DropdownMenuItem(
onClick = {
onClickIgnore(node)
onDismiss()
},
leadingIcon = {
Icon(
imageVector = if (isIgnored) Icons.Filled.DoDisturbOn else Icons.Outlined.DoDisturbOn,
contentDescription = null,
tint = MaterialTheme.colorScheme.StatusRed,
)
},
text = {
Text(
text = stringResource(if (isIgnored) R.string.remove_ignored else R.string.ignore),
color = MaterialTheme.colorScheme.StatusRed,
)
},
)
DropdownMenuItem(
onClick = {
onClickRemove(node)
onDismiss()
},
enabled = !isIgnored,
leadingIcon = {
Icon(
imageVector = Icons.Rounded.DeleteOutline,
contentDescription = null,
tint = if (isIgnored) LocalContentColor.current else MaterialTheme.colorScheme.StatusRed,
)
},
text = {
Text(
text = stringResource(R.string.remove),
color = if (isIgnored) Color.Unspecified else MaterialTheme.colorScheme.StatusRed,
)
},
)
}
}

View file

@ -40,7 +40,6 @@ import org.meshtastic.core.data.repository.NodeRepository
import org.meshtastic.core.data.repository.RadioConfigRepository
import org.meshtastic.core.database.model.Node
import org.meshtastic.core.database.model.NodeSortOption
import org.meshtastic.core.model.Position
import org.meshtastic.core.prefs.ui.UiPrefs
import timber.log.Timber
import javax.inject.Inject
@ -170,22 +169,14 @@ constructor(
fun addSharedContact(sharedContact: AdminProtos.SharedContact) =
viewModelScope.launch { serviceRepository.onServiceAction(ServiceAction.AddSharedContact(sharedContact)) }
fun removeNode(nodeNum: Int) = viewModelScope.launch(Dispatchers.IO) {
Timber.i("Removing node '$nodeNum'")
try {
val packetId = serviceRepository.meshService?.packetId ?: return@launch
serviceRepository.meshService?.removeByNodenum(packetId, nodeNum)
nodeRepository.deleteNode(nodeNum)
} catch (ex: RemoteException) {
Timber.e("Remove node error: ${ex.message}")
}
fun setSharedContactRequested(sharedContact: AdminProtos.SharedContact?) {
_sharedContactRequested.value = sharedContact
}
fun ignoreNode(node: Node) = viewModelScope.launch {
try {
serviceRepository.onServiceAction(ServiceAction.Ignore(node))
} catch (ex: RemoteException) {
Timber.e(ex, "Ignore node error")
private fun toggle(state: MutableStateFlow<Boolean>, onChanged: (newValue: Boolean) -> Unit) {
(!state.value).let { toggled ->
state.update { toggled }
onChanged(toggled)
}
}
@ -197,42 +188,22 @@ constructor(
}
}
fun requestUserInfo(destNum: Int) {
Timber.i("Requesting UserInfo for '$destNum'")
fun ignoreNode(node: Node) = viewModelScope.launch {
try {
serviceRepository.meshService?.requestUserInfo(destNum)
serviceRepository.onServiceAction(ServiceAction.Ignore(node))
} catch (ex: RemoteException) {
Timber.e("Request NodeInfo error: ${ex.message}")
Timber.e(ex, "Ignore node error")
}
}
fun requestPosition(destNum: Int, position: Position = Position(0.0, 0.0, 0)) {
Timber.i("Requesting position for '$destNum'")
fun removeNode(nodeNum: Int) = viewModelScope.launch(Dispatchers.IO) {
Timber.i("Removing node '$nodeNum'")
try {
serviceRepository.meshService?.requestPosition(destNum, position)
val packetId = serviceRepository.meshService?.packetId ?: return@launch
serviceRepository.meshService?.removeByNodenum(packetId, nodeNum)
nodeRepository.deleteNode(nodeNum)
} catch (ex: RemoteException) {
Timber.e("Request position error: ${ex.message}")
}
}
fun requestTraceroute(destNum: Int) {
Timber.i("Requesting traceroute for '$destNum'")
try {
val packetId = serviceRepository.meshService?.packetId ?: return
serviceRepository.meshService?.requestTraceroute(packetId, destNum)
} catch (ex: RemoteException) {
Timber.e("Request traceroute error: ${ex.message}")
}
}
fun setSharedContactRequested(sharedContact: AdminProtos.SharedContact?) {
_sharedContactRequested.value = sharedContact
}
private fun toggle(state: MutableStateFlow<Boolean>, onChanged: (newValue: Boolean) -> Unit) {
(!state.value).let { toggled ->
state.update { toggled }
onChanged(toggled)
Timber.e("Remove node error: ${ex.message}")
}
}
}

View file

@ -17,9 +17,7 @@
package com.geeksville.mesh.ui.node.components
import androidx.compose.foundation.combinedClickable
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.IntrinsicSize
import androidx.compose.foundation.layout.defaultMinSize
import androidx.compose.foundation.layout.fillMaxWidth
@ -29,10 +27,7 @@ import androidx.compose.material3.ElevatedAssistChip
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.semantics.contentDescription
@ -47,65 +42,33 @@ import com.geeksville.mesh.TelemetryProtos
import org.meshtastic.core.database.model.Node
@Composable
fun NodeChip(
modifier: Modifier = Modifier,
enabled: Boolean = true,
node: Node,
isThisNode: Boolean,
isConnected: Boolean,
onAction: (NodeMenuAction) -> Unit,
) {
fun NodeChip(modifier: Modifier = Modifier, node: Node, onClick: (Node) -> Unit = {}) {
val isIgnored = node.isIgnored
val (textColor, nodeColor) = node.colors
var menuExpanded by remember { mutableStateOf(false) }
val inputChipInteractionSource = remember { MutableInteractionSource() }
Box {
ElevatedAssistChip(
modifier =
modifier.width(IntrinsicSize.Min).defaultMinSize(minWidth = 72.dp).semantics {
contentDescription = node.user.shortName.ifEmpty { "Node" }
},
elevation = AssistChipDefaults.elevatedAssistChipElevation(),
colors =
AssistChipDefaults.elevatedAssistChipColors(
containerColor = Color(nodeColor),
labelColor = Color(textColor),
),
label = {
Text(
modifier = Modifier.fillMaxWidth(),
text = node.user.shortName.ifEmpty { "???" },
fontSize = MaterialTheme.typography.labelLarge.fontSize,
textDecoration = TextDecoration.LineThrough.takeIf { isIgnored },
textAlign = TextAlign.Center,
maxLines = 1,
)
},
onClick = {},
interactionSource = inputChipInteractionSource,
)
Box(
modifier =
Modifier.matchParentSize()
.combinedClickable(
enabled = enabled,
onClick = { onAction(NodeMenuAction.MoreDetails(node)) },
onLongClick = { menuExpanded = true },
interactionSource = inputChipInteractionSource,
indication = null,
)
.semantics { contentDescription = node.user.shortName.ifEmpty { "Node" } },
)
}
NodeMenu(
expanded = menuExpanded,
node = node,
showFullMenu = !isThisNode && isConnected,
onDismissMenuRequest = { menuExpanded = false },
onAction = {
menuExpanded = false
onAction(it)
ElevatedAssistChip(
modifier =
modifier.width(IntrinsicSize.Min).defaultMinSize(minWidth = 72.dp).semantics {
contentDescription = node.user.shortName.ifEmpty { "Node" }
},
elevation = AssistChipDefaults.elevatedAssistChipElevation(),
colors =
AssistChipDefaults.elevatedAssistChipColors(
containerColor = Color(nodeColor),
labelColor = Color(textColor),
),
label = {
Text(
modifier = Modifier.fillMaxWidth(),
text = node.user.shortName.ifEmpty { "???" },
fontSize = MaterialTheme.typography.labelLarge.fontSize,
textDecoration = TextDecoration.LineThrough.takeIf { isIgnored },
textAlign = TextAlign.Center,
maxLines = 1,
)
},
onClick = { onClick(node) },
interactionSource = inputChipInteractionSource,
)
}
@ -123,5 +86,5 @@ fun NodeChipPreview() {
environmentMetrics =
TelemetryProtos.EnvironmentMetrics.newBuilder().setTemperature(25f).setRelativeHumidity(60f).build(),
)
NodeChip(node = node, isThisNode = false, isConnected = true, onAction = {})
NodeChip(node = node)
}

View file

@ -18,6 +18,7 @@
package com.geeksville.mesh.ui.node.components
import android.content.res.Configuration
import androidx.compose.foundation.combinedClickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
@ -68,7 +69,8 @@ fun NodeItem(
distanceUnits: Int,
tempInFahrenheit: Boolean,
modifier: Modifier = Modifier,
onAction: (NodeMenuAction) -> Unit = {},
onClickChip: (Node) -> Unit = {},
onLongClick: () -> Unit = {},
expanded: Boolean = false,
currentTimeMillis: Long,
isConnected: Boolean = false,
@ -123,13 +125,16 @@ fun NodeItem(
Card(
modifier =
modifier.fillMaxWidth().padding(horizontal = 8.dp, vertical = 4.dp).defaultMinSize(minHeight = 80.dp),
onClick = { showDetails(!detailsShown) },
modifier
.combinedClickable(onClick = { showDetails(!detailsShown) }, onLongClick = onLongClick)
.fillMaxWidth()
.padding(horizontal = 8.dp, vertical = 4.dp)
.defaultMinSize(minHeight = 80.dp),
colors = cardColors,
) {
Column(modifier = Modifier.fillMaxWidth().padding(8.dp)) {
Row(modifier = Modifier.fillMaxWidth(), verticalAlignment = Alignment.CenterVertically) {
NodeChip(node = thatNode, isThisNode = isThisNode, isConnected = isConnected, onAction = onAction)
NodeChip(node = thatNode, onClick = onClickChip)
NodeKeyStatusIcon(
hasPKC = thatNode.hasPKC,

View file

@ -17,166 +17,12 @@
package com.geeksville.mesh.ui.node.components
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Star
import androidx.compose.material.icons.twotone.StarBorder
import androidx.compose.material3.Checkbox
import androidx.compose.material3.DropdownMenu
import androidx.compose.material3.DropdownMenuItem
import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import org.meshtastic.core.database.model.Node
import org.meshtastic.core.database.model.isUnmessageableRole
import org.meshtastic.core.strings.R
import org.meshtastic.core.ui.component.SimpleAlertDialog
@Suppress("LongMethod")
@Composable
fun NodeMenu(
expanded: Boolean,
node: Node,
showFullMenu: Boolean = false,
onDismissMenuRequest: () -> Unit,
onAction: (NodeMenuAction) -> Unit,
) {
val isUnmessageable =
if (node.user.hasIsUnmessagable()) {
node.user.isUnmessagable
} else {
// for older firmwares
node.user.role?.isUnmessageableRole() == true
}
var displayFavoriteDialog by remember { mutableStateOf(false) }
var displayIgnoreDialog by remember { mutableStateOf(false) }
var displayRemoveDialog by remember { mutableStateOf(false) }
val dialogDismissRequest = {
displayFavoriteDialog = false
displayIgnoreDialog = false
displayRemoveDialog = false
onDismissMenuRequest()
}
val onMenuAction: (NodeMenuAction) -> Unit = {
dialogDismissRequest()
onDismissMenuRequest()
onAction(it)
}
NodeActionDialogs(
node = node,
displayFavoriteDialog = displayFavoriteDialog,
displayIgnoreDialog = displayIgnoreDialog,
displayRemoveDialog = displayRemoveDialog,
onDismissMenuRequest = dialogDismissRequest,
onAction = onMenuAction,
)
DropdownMenu(
modifier = Modifier.background(MaterialTheme.colorScheme.background.copy(alpha = 1f)),
expanded = expanded,
onDismissRequest = onDismissMenuRequest,
) {
if (showFullMenu) {
if (!isUnmessageable) {
DropdownMenuItem(
onClick = {
dialogDismissRequest()
onMenuAction(NodeMenuAction.DirectMessage(node))
},
text = { Text(stringResource(R.string.direct_message)) },
)
}
DropdownMenuItem(
onClick = {
dialogDismissRequest()
onMenuAction(NodeMenuAction.RequestUserInfo(node))
},
text = { Text(stringResource(R.string.exchange_userinfo)) },
)
DropdownMenuItem(
onClick = {
dialogDismissRequest()
onMenuAction(NodeMenuAction.RequestPosition(node))
},
text = { Text(stringResource(R.string.exchange_position)) },
)
DropdownMenuItem(
onClick = {
dialogDismissRequest()
onMenuAction(NodeMenuAction.TraceRoute(node))
},
text = { Text(stringResource(R.string.traceroute)) },
)
DropdownMenuItem(
onClick = {
dialogDismissRequest()
displayFavoriteDialog = true
},
enabled = !node.isIgnored,
text = { Text(stringResource(R.string.favorite)) },
trailingIcon = {
Icon(
imageVector = if (node.isFavorite) Icons.Filled.Star else Icons.TwoTone.StarBorder,
contentDescription = stringResource(R.string.favorite),
)
},
)
DropdownMenuItem(
onClick = {
dialogDismissRequest()
displayIgnoreDialog = true
},
text = { Text(stringResource(R.string.ignore)) },
trailingIcon = {
Checkbox(
checked = node.isIgnored,
onCheckedChange = {
dialogDismissRequest()
displayIgnoreDialog = true
},
modifier = Modifier.size(24.dp),
)
},
)
DropdownMenuItem(
onClick = {
dialogDismissRequest()
displayRemoveDialog = true
},
enabled = !node.isIgnored,
text = { Text(stringResource(R.string.remove)) },
)
HorizontalDivider(Modifier.padding(vertical = 8.dp))
}
DropdownMenuItem(
onClick = {
dialogDismissRequest()
onMenuAction(NodeMenuAction.Share(node))
},
text = { Text(stringResource(R.string.share_contact)) },
)
DropdownMenuItem(
onClick = {
dialogDismissRequest()
onMenuAction(NodeMenuAction.MoreDetails(node))
},
text = { Text(stringResource(R.string.more_details)) },
)
}
}
@Composable
fun NodeActionDialogs(
node: Node,
@ -184,7 +30,9 @@ fun NodeActionDialogs(
displayIgnoreDialog: Boolean,
displayRemoveDialog: Boolean,
onDismissMenuRequest: () -> Unit,
onAction: (NodeMenuAction) -> Unit,
onConfirmFavorite: (Node) -> Unit,
onConfirmIgnore: (Node) -> Unit,
onConfirmRemove: (Node) -> Unit,
) {
if (displayFavoriteDialog) {
SimpleAlertDialog(
@ -196,7 +44,7 @@ fun NodeActionDialogs(
),
onConfirm = {
onDismissMenuRequest()
onAction(NodeMenuAction.Favorite(node))
onConfirmFavorite(node)
},
onDismiss = onDismissMenuRequest,
)
@ -211,7 +59,7 @@ fun NodeActionDialogs(
),
onConfirm = {
onDismissMenuRequest()
onAction(NodeMenuAction.Ignore(node))
onConfirmIgnore(node)
},
onDismiss = onDismissMenuRequest,
)
@ -222,7 +70,7 @@ fun NodeActionDialogs(
text = R.string.remove_node_text,
onConfirm = {
onDismissMenuRequest()
onAction(NodeMenuAction.Remove(node))
onConfirmRemove(node)
},
onDismiss = onDismissMenuRequest,
)

View file

@ -60,7 +60,6 @@ import com.geeksville.mesh.ClientOnlyProtos.DeviceProfile
import com.geeksville.mesh.android.gpsDisabled
import com.geeksville.mesh.navigation.getNavRouteFrom
import com.geeksville.mesh.ui.common.components.MainAppBar
import com.geeksville.mesh.ui.node.components.NodeMenuAction
import com.geeksville.mesh.ui.settings.components.SettingsItem
import com.geeksville.mesh.ui.settings.components.SettingsItemDetail
import com.geeksville.mesh.ui.settings.components.SettingsItemSwitch
@ -194,17 +193,11 @@ fun SettingsScreen(
stringResource(R.string.remotely_administrating, remoteName)
},
ourNode = ourNode,
isConnected = isConnected,
showNodeChip = ourNode != null && isConnected && state.isLocal,
canNavigateUp = false,
onNavigateUp = {},
actions = {},
onAction = { action ->
when (action) {
is NodeMenuAction.MoreDetails -> onClickNodeChip(action.node.num)
else -> {}
}
},
onClickChip = { onClickNodeChip(it.num) },
)
},
) { paddingValues ->

View file

@ -173,12 +173,7 @@ private fun NodesDeletionPreview(nodesToDelete: List<NodeEntity>) {
verticalArrangement = Arrangement.Center,
) {
nodesToDelete.forEach { node ->
NodeChip(
node = node.toModel(),
modifier = Modifier.padding(end = 8.dp, bottom = 8.dp),
isThisNode = false,
isConnected = false,
) {}
NodeChip(node = node.toModel(), modifier = Modifier.padding(end = 8.dp, bottom = 8.dp))
}
}
}

View file

@ -59,10 +59,9 @@ fun <T : MessageLite> RadioConfigScreenList(
canNavigateUp = true,
onNavigateUp = onBack,
ourNode = null,
isConnected = false,
showNodeChip = false,
actions = {},
onAction = {},
onClickChip = {},
)
},
) { innerPadding ->

View file

@ -297,6 +297,7 @@
<string name="delivery_confirmed">Delivery confirmed</string>
<string name="error">Error</string>
<string name="ignore">Ignore</string>
<string name="remove_ignored">Remove from ignored</string>
<string name="ignore_add">Add \'%s\' to ignore list?</string>
<string name="ignore_remove">Remove \'%s\' from ignore list?</string>
<string name="map_select_download_region">Select download region</string>
@ -397,6 +398,8 @@
<string name="alert_bell_text">Alert Bell Character!</string>
<string name="critical_alert">Critical Alert!</string>
<string name="favorite">Favorite</string>
<string name="add_favorite">Add to favorites</string>
<string name="remove_favorite">Remove from favorites</string>
<string name="favorite_add">Add \'%s\' as a favorite node?</string>
<string name="favorite_remove">Remove \'%s\' as a favorite node?</string>
<string name="power_metrics_log">Power Metrics Log</string>