mirror of
https://github.com/meshtastic/Meshtastic-Android.git
synced 2026-04-20 22:23:37 +00:00
feat: implement right-click as mirror for long-click across shared UI
This commit introduces a new `Modifier.onRightClick` extension to improve desktop usability by mapping secondary mouse clicks to existing long-click actions. It also removes the legacy desktop `MenuBar` implementation in favor of this more consistent cross-platform UX approach.
Specific changes include:
- **Core UI**: Added `Modifier.onRightClick` in `ModifierExtensions.kt` using `PointerButton.Secondary` to detect right-clicks on desktop platforms.
- **Desktop**: Removed the `MenuBar` and associated keyboard shortcuts from the main entry point.
- **Messaging Feature**: Added right-click support to `MessageItem`, `Reaction`, and `ContactItem`.
- **Node Feature**:
- Added right-click support to `NodeItem`, `NodeDetailsSection`, `NodeDetailComponents`, and `InfoCard` to trigger copy actions or context menus.
- Updated `TracerouteLog`, `NeighborInfoLog`, and `NodeItem` to handle right-click events.
- **Connections Feature**: Updated `DeviceListItem` to support right-click for deletion/context actions.
- **Documentation**: Updated `roadmap.md` to reflect the completion of right-click UX integration.
Signed-off-by: James Rich <2199651+jamesarich@users.noreply.github.com>
This commit is contained in:
parent
5a287f7133
commit
0bc907ec32
13 changed files with 117 additions and 112 deletions
|
|
@ -16,7 +16,12 @@
|
|||
*/
|
||||
package org.meshtastic.core.ui.util
|
||||
|
||||
import androidx.compose.foundation.gestures.awaitEachGesture
|
||||
import androidx.compose.ui.ExperimentalComposeUiApi
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.input.pointer.PointerButton
|
||||
import androidx.compose.ui.input.pointer.PointerEventType
|
||||
import androidx.compose.ui.input.pointer.pointerInput
|
||||
|
||||
/**
|
||||
* Conditionally applies the [action] to the receiver [Modifier] if [precondition] is true. Otherwise, returns the
|
||||
|
|
@ -24,3 +29,18 @@ import androidx.compose.ui.Modifier
|
|||
*/
|
||||
inline fun Modifier.thenIf(precondition: Boolean, action: Modifier.() -> Modifier): Modifier =
|
||||
if (precondition) action() else this
|
||||
|
||||
/**
|
||||
* Adds a secondary (right) mouse-button click handler. On touch-only platforms the secondary button event never fires,
|
||||
* so this is a safe no-op. Intended to mirror `onLongClick` behavior for desktop users who expect right-click context
|
||||
* actions.
|
||||
*/
|
||||
@OptIn(ExperimentalComposeUiApi::class)
|
||||
fun Modifier.onRightClick(action: () -> Unit): Modifier = pointerInput(action) {
|
||||
awaitEachGesture {
|
||||
val event = awaitPointerEvent()
|
||||
if (event.type == PointerEventType.Press && event.button == PointerButton.Secondary) {
|
||||
action()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -28,12 +28,9 @@ import androidx.compose.runtime.setValue
|
|||
import androidx.compose.runtime.snapshotFlow
|
||||
import androidx.compose.runtime.staticCompositionLocalOf
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.input.key.Key
|
||||
import androidx.compose.ui.input.key.KeyShortcut
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.unit.DpSize
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.window.MenuBar
|
||||
import androidx.compose.ui.window.Notification
|
||||
import androidx.compose.ui.window.Tray
|
||||
import androidx.compose.ui.window.Window
|
||||
|
|
@ -49,7 +46,6 @@ import org.koin.core.context.startKoin
|
|||
import org.meshtastic.core.common.util.MeshtasticUri
|
||||
import org.meshtastic.core.datastore.UiPreferencesDataSource
|
||||
import org.meshtastic.core.navigation.MeshtasticNavSavedStateConfig
|
||||
import org.meshtastic.core.navigation.SettingsRoutes
|
||||
import org.meshtastic.core.navigation.TopLevelDestination
|
||||
import org.meshtastic.core.service.MeshServiceOrchestrator
|
||||
import org.meshtastic.core.ui.theme.AppTheme
|
||||
|
|
@ -201,56 +197,6 @@ fun main(args: Array<String>) = application(exitProcessOnExit = false) {
|
|||
val backStack =
|
||||
rememberNavBackStack(MeshtasticNavSavedStateConfig, TopLevelDestination.Connections.route as NavKey)
|
||||
|
||||
MenuBar {
|
||||
Menu("File") {
|
||||
Item("Settings", shortcut = KeyShortcut(Key.Comma, meta = true)) {
|
||||
if (
|
||||
TopLevelDestination.Settings != TopLevelDestination.fromNavKey(backStack.lastOrNull())
|
||||
) {
|
||||
backStack.add(TopLevelDestination.Settings.route)
|
||||
while (backStack.size > 1) {
|
||||
backStack.removeAt(0)
|
||||
}
|
||||
}
|
||||
}
|
||||
Separator()
|
||||
Item("Quit", shortcut = KeyShortcut(Key.Q, meta = true)) { exitApplication() }
|
||||
}
|
||||
Menu("View") {
|
||||
Item("Toggle Theme", shortcut = KeyShortcut(Key.T, meta = true, shift = true)) {
|
||||
val newTheme = if (isDarkTheme) 1 else 2 // 1 = Light, 2 = Dark
|
||||
uiPrefs.setTheme(newTheme)
|
||||
}
|
||||
}
|
||||
Menu("Navigate") {
|
||||
Item("Conversations", shortcut = KeyShortcut(Key.One, meta = true)) {
|
||||
backStack.add(TopLevelDestination.Conversations.route)
|
||||
while (backStack.size > 1) {
|
||||
backStack.removeAt(0)
|
||||
}
|
||||
}
|
||||
Item("Nodes", shortcut = KeyShortcut(Key.Two, meta = true)) {
|
||||
backStack.add(TopLevelDestination.Nodes.route)
|
||||
while (backStack.size > 1) {
|
||||
backStack.removeAt(0)
|
||||
}
|
||||
}
|
||||
Item("Map", shortcut = KeyShortcut(Key.Three, meta = true)) {
|
||||
backStack.add(TopLevelDestination.Map.route)
|
||||
while (backStack.size > 1) {
|
||||
backStack.removeAt(0)
|
||||
}
|
||||
}
|
||||
Item("Connections", shortcut = KeyShortcut(Key.Four, meta = true)) {
|
||||
backStack.add(TopLevelDestination.Connections.route)
|
||||
while (backStack.size > 1) {
|
||||
backStack.removeAt(0)
|
||||
}
|
||||
}
|
||||
}
|
||||
Menu("Help") { Item("About") { backStack.add(SettingsRoutes.About) } }
|
||||
}
|
||||
|
||||
// Providing localePref via a staticCompositionLocalOf forces the entire subtree to
|
||||
// recompose when the locale changes — CMP Resources' rememberResourceEnvironment then
|
||||
// re-reads Locale.current and all stringResource() calls update. Unlike key(), this
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
# Roadmap
|
||||
|
||||
> Last updated: 2026-03-17
|
||||
> Last updated: 2026-03-22
|
||||
|
||||
Forward-looking priorities for the Meshtastic KMP multi-target effort. For current state, see [`kmp-status.md`](./kmp-status.md). For the full gap analysis, see [`decisions/architecture-review-2026-03.md`](./decisions/architecture-review-2026-03.md).
|
||||
|
||||
|
|
@ -42,7 +42,7 @@ These items address structural gaps identified in the March 2026 architecture re
|
|||
- Test navigation flows end-to-end
|
||||
2. **Tier 2: Polish (High Priority)**
|
||||
- Additional desktop-specific settings polish
|
||||
- ✅ **MenuBar integration** and Keyboard shortcuts
|
||||
- ✅ **Right-click mirrors long-click** across all shared UI components (`Modifier.onRightClick` in `core:ui`)
|
||||
- Window management
|
||||
- State persistence
|
||||
3. **Tier 3: Advanced (Nice-to-have)**
|
||||
|
|
@ -74,7 +74,7 @@ These items address structural gaps identified in the March 2026 architecture re
|
|||
| Charts | ✅ Vico KMP charts wired in commonMain (Device, Environment, Signal, Power, Pax) |
|
||||
| Debug Panel | ✅ Real screen (mesh log viewer via shared `DebugViewModel`) |
|
||||
| Notifications | ✅ Desktop native notifications with system tray icon support |
|
||||
| MenuBar | ✅ Done — Native application menu bar with File/View menus |
|
||||
| Right-click UX | ✅ Done — `Modifier.onRightClick` mirrors long-click across all shared components |
|
||||
| About | ✅ Shared `commonMain` screen (AboutLibraries KMP `produceLibraries` + per-platform JSON) |
|
||||
| Packaging | ✅ Done — Native distribution pipeline in CI (DMG, MSI, DEB) |
|
||||
|
||||
|
|
|
|||
|
|
@ -58,6 +58,7 @@ import org.meshtastic.core.resources.network
|
|||
import org.meshtastic.core.resources.serial
|
||||
import org.meshtastic.core.ui.component.NodeChip
|
||||
import org.meshtastic.core.ui.component.Rssi
|
||||
import org.meshtastic.core.ui.util.onRightClick
|
||||
import org.meshtastic.feature.connections.model.DeviceListEntry
|
||||
|
||||
private const val RSSI_UPDATE_RATE_MS = 2000L
|
||||
|
|
@ -113,7 +114,7 @@ fun DeviceListItem(
|
|||
|
||||
val clickableModifier =
|
||||
if (onDelete != null) {
|
||||
Modifier.combinedClickable(onClick = onSelect, onLongClick = onDelete)
|
||||
Modifier.combinedClickable(onClick = onSelect, onLongClick = onDelete).onRightClick { onDelete() }
|
||||
} else {
|
||||
Modifier.clickable(onClick = onSelect)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -74,6 +74,7 @@ import org.meshtastic.core.ui.icon.Hops
|
|||
import org.meshtastic.core.ui.icon.MeshtasticIcons
|
||||
import org.meshtastic.core.ui.theme.MessageItemColors
|
||||
import org.meshtastic.core.ui.util.createClipEntry
|
||||
import org.meshtastic.core.ui.util.onRightClick
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Suppress("LongMethod", "CyclomaticComplexMethod")
|
||||
|
|
@ -241,6 +242,12 @@ fun MessageItem(
|
|||
},
|
||||
onDoubleClick = onDoubleClick,
|
||||
)
|
||||
.onRightClick {
|
||||
onLongClick()
|
||||
if (!inSelectionMode) {
|
||||
activeSheet = ActiveSheet.Actions
|
||||
}
|
||||
}
|
||||
.then(messageModifier)
|
||||
.semantics(mergeDescendants = true) {
|
||||
val senderName = if (message.fromLocal) ourNode.user.long_name else node.user.long_name
|
||||
|
|
|
|||
|
|
@ -75,6 +75,7 @@ import org.meshtastic.core.ui.component.Snr
|
|||
import org.meshtastic.core.ui.emoji.EmojiPickerDialog
|
||||
import org.meshtastic.core.ui.icon.Hops
|
||||
import org.meshtastic.core.ui.icon.MeshtasticIcons
|
||||
import org.meshtastic.core.ui.util.onRightClick
|
||||
import org.meshtastic.feature.messaging.DeliveryInfo
|
||||
|
||||
@Composable
|
||||
|
|
@ -93,6 +94,7 @@ internal fun ReactionItem(
|
|||
modifier =
|
||||
modifier
|
||||
.combinedClickable(onClick = onClick, onLongClick = onLongClick)
|
||||
.onRightClick(onLongClick)
|
||||
.then(if (isSending) Modifier.graphicsLayer(alpha = 0.5f) else Modifier),
|
||||
color =
|
||||
when {
|
||||
|
|
|
|||
|
|
@ -53,6 +53,7 @@ import androidx.compose.ui.unit.dp
|
|||
import org.meshtastic.core.common.util.DateFormatter
|
||||
import org.meshtastic.core.model.Contact
|
||||
import org.meshtastic.core.ui.component.SecurityIcon
|
||||
import org.meshtastic.core.ui.util.onRightClick
|
||||
import org.meshtastic.proto.ChannelSet
|
||||
|
||||
@Suppress("LongMethod")
|
||||
|
|
@ -88,6 +89,7 @@ fun ContactItem(
|
|||
modifier =
|
||||
modifier
|
||||
.combinedClickable(onClick = onClick, onLongClick = onLongClick)
|
||||
.onRightClick(onLongClick)
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 8.dp, vertical = 4.dp)
|
||||
.semantics { contentDescription = shortName },
|
||||
|
|
|
|||
|
|
@ -49,6 +49,7 @@ import org.jetbrains.compose.resources.stringResource
|
|||
import org.meshtastic.core.resources.Res
|
||||
import org.meshtastic.core.resources.copy
|
||||
import org.meshtastic.core.ui.util.createClipEntry
|
||||
import org.meshtastic.core.ui.util.onRightClick
|
||||
import org.meshtastic.core.ui.util.thenIf
|
||||
|
||||
@OptIn(ExperimentalFoundationApi::class)
|
||||
|
|
@ -66,17 +67,20 @@ fun InfoCard(
|
|||
val shape = MaterialTheme.shapes.medium
|
||||
val copyLabel = stringResource(Res.string.copy)
|
||||
|
||||
val copyAction = { coroutineScope.launch { clipboard.setClipEntry(createClipEntry(value, text)) } }
|
||||
|
||||
Card(
|
||||
modifier =
|
||||
modifier
|
||||
.defaultMinSize(minHeight = 48.dp)
|
||||
.clip(shape)
|
||||
.combinedClickable(
|
||||
onLongClick = { coroutineScope.launch { clipboard.setClipEntry(createClipEntry(value, text)) } },
|
||||
onLongClick = { copyAction() },
|
||||
onLongClickLabel = copyLabel,
|
||||
onClick = {},
|
||||
role = Role.Button,
|
||||
)
|
||||
.onRightClick { copyAction() }
|
||||
.semantics(mergeDescendants = true) { contentDescription = "$text: $value" },
|
||||
shape = shape,
|
||||
colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.surfaceContainerLow),
|
||||
|
|
|
|||
|
|
@ -54,6 +54,7 @@ import org.jetbrains.compose.resources.stringResource
|
|||
import org.meshtastic.core.resources.Res
|
||||
import org.meshtastic.core.resources.copy
|
||||
import org.meshtastic.core.ui.util.createClipEntry
|
||||
import org.meshtastic.core.ui.util.onRightClick
|
||||
|
||||
@Composable
|
||||
internal fun SectionCard(
|
||||
|
|
@ -95,17 +96,20 @@ internal fun InfoItem(
|
|||
val coroutineScope = rememberCoroutineScope()
|
||||
val copyLabel = stringResource(Res.string.copy)
|
||||
|
||||
val copyAction = { coroutineScope.launch { clipboard.setClipEntry(createClipEntry(value, label)) } }
|
||||
|
||||
Column(
|
||||
modifier =
|
||||
modifier
|
||||
.fillMaxWidth()
|
||||
.defaultMinSize(minHeight = 48.dp) // Minimum touch target height
|
||||
.combinedClickable(
|
||||
onLongClick = { coroutineScope.launch { clipboard.setClipEntry(createClipEntry(value, label)) } },
|
||||
onLongClick = { copyAction() },
|
||||
onLongClickLabel = copyLabel, // Clear intent for accessibility
|
||||
onClick = {},
|
||||
role = Role.Button,
|
||||
)
|
||||
.onRightClick { copyAction() }
|
||||
.padding(horizontal = 20.dp, vertical = 8.dp)
|
||||
.semantics(mergeDescendants = true) {
|
||||
// Screen readers read as a unified data unit
|
||||
|
|
|
|||
|
|
@ -89,6 +89,7 @@ import org.meshtastic.core.ui.icon.Verified
|
|||
import org.meshtastic.core.ui.icon.role
|
||||
import org.meshtastic.core.ui.util.createClipEntry
|
||||
import org.meshtastic.core.ui.util.formatAgo
|
||||
import org.meshtastic.core.ui.util.onRightClick
|
||||
|
||||
@Composable
|
||||
fun NodeDetailsSection(node: Node, modifier: Modifier = Modifier) {
|
||||
|
|
@ -324,20 +325,23 @@ private fun PublicKeyItem(publicKeyBytes: ByteArray) {
|
|||
val label = stringResource(Res.string.public_key)
|
||||
val copyLabel = stringResource(Res.string.copy)
|
||||
|
||||
val copyAction = {
|
||||
if (!isMismatch) {
|
||||
coroutineScope.launch { clipboard.setClipEntry(createClipEntry(publicKeyBase64, label)) }
|
||||
}
|
||||
}
|
||||
|
||||
Column(
|
||||
modifier =
|
||||
Modifier.fillMaxWidth()
|
||||
.defaultMinSize(minHeight = 48.dp)
|
||||
.combinedClickable(
|
||||
onLongClick = {
|
||||
if (!isMismatch) {
|
||||
coroutineScope.launch { clipboard.setClipEntry(createClipEntry(publicKeyBase64, label)) }
|
||||
}
|
||||
},
|
||||
onLongClick = { copyAction() },
|
||||
onLongClickLabel = copyLabel,
|
||||
onClick = {},
|
||||
role = Role.Button,
|
||||
)
|
||||
.onRightClick { copyAction() }
|
||||
.padding(horizontal = 20.dp, vertical = 8.dp)
|
||||
.semantics(mergeDescendants = true) { contentDescription = "$label: $publicKeyBase64" },
|
||||
) {
|
||||
|
|
|
|||
|
|
@ -89,6 +89,8 @@ import org.meshtastic.core.ui.component.determineSignalQuality
|
|||
import org.meshtastic.core.ui.icon.AirUtilization
|
||||
import org.meshtastic.core.ui.icon.ChannelUtilization
|
||||
import org.meshtastic.core.ui.icon.MeshtasticIcons
|
||||
import org.meshtastic.core.ui.util.onRightClick
|
||||
import org.meshtastic.core.ui.util.thenIf
|
||||
import org.meshtastic.proto.Config
|
||||
|
||||
private const val ACTIVE_ALPHA = 0.5f
|
||||
|
|
@ -153,7 +155,10 @@ fun NodeItem(
|
|||
Card(modifier = modifier.fillMaxWidth(), colors = cardColors) {
|
||||
Column(
|
||||
modifier =
|
||||
Modifier.combinedClickable(onClick = onClick, onLongClick = onLongClick).fillMaxWidth().padding(12.dp),
|
||||
Modifier.combinedClickable(onClick = onClick, onLongClick = onLongClick)
|
||||
.thenIf(onLongClick != null) { onRightClick { onLongClick?.invoke() } }
|
||||
.fillMaxWidth()
|
||||
.padding(12.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(10.dp),
|
||||
) {
|
||||
NodeItemHeader(
|
||||
|
|
|
|||
|
|
@ -54,6 +54,7 @@ import org.meshtastic.core.ui.theme.StatusColors.StatusGreen
|
|||
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.core.ui.util.onRightClick
|
||||
import org.meshtastic.feature.node.component.CooldownIconButton
|
||||
import org.meshtastic.feature.node.detail.NodeRequestEffect
|
||||
|
||||
|
|
@ -130,22 +131,26 @@ fun NeighborInfoLogScreen(modifier: Modifier = Modifier, viewModel: MetricsViewM
|
|||
text = "$time - $text",
|
||||
contentDescription = stringResource(Res.string.neighbor_info),
|
||||
modifier =
|
||||
Modifier.combinedClickable(onLongClick = { expanded = true }) {
|
||||
result
|
||||
?.fromRadio
|
||||
?.packet
|
||||
?.getNeighborInfoResponse(::getUsername, header = header)
|
||||
?.let {
|
||||
val message =
|
||||
annotateNeighborInfo(
|
||||
it,
|
||||
statusGreen = statusGreen,
|
||||
statusYellow = statusYellow,
|
||||
statusOrange = statusOrange,
|
||||
)
|
||||
viewModel.showLogDetail(Res.string.neighbor_info, message)
|
||||
}
|
||||
},
|
||||
Modifier.combinedClickable(
|
||||
onLongClick = { expanded = true },
|
||||
onClick = {
|
||||
result
|
||||
?.fromRadio
|
||||
?.packet
|
||||
?.getNeighborInfoResponse(::getUsername, header = header)
|
||||
?.let {
|
||||
val message =
|
||||
annotateNeighborInfo(
|
||||
it,
|
||||
statusGreen = statusGreen,
|
||||
statusYellow = statusYellow,
|
||||
statusOrange = statusOrange,
|
||||
)
|
||||
viewModel.showLogDetail(Res.string.neighbor_info, message)
|
||||
}
|
||||
},
|
||||
)
|
||||
.onRightClick { expanded = true },
|
||||
)
|
||||
DropdownMenu(expanded = expanded, onDismissRequest = { expanded = false }) {
|
||||
DeleteItem {
|
||||
|
|
|
|||
|
|
@ -70,6 +70,7 @@ import org.meshtastic.core.ui.theme.StatusColors.StatusGreen
|
|||
import org.meshtastic.core.ui.theme.StatusColors.StatusOrange
|
||||
import org.meshtastic.core.ui.theme.StatusColors.StatusYellow
|
||||
import org.meshtastic.core.ui.util.annotateTraceroute
|
||||
import org.meshtastic.core.ui.util.onRightClick
|
||||
import org.meshtastic.feature.map.model.TracerouteOverlay
|
||||
import org.meshtastic.feature.node.component.CooldownIconButton
|
||||
import org.meshtastic.feature.node.detail.NodeRequestEffect
|
||||
|
|
@ -196,37 +197,41 @@ fun TracerouteLogScreen(
|
|||
text = stringResource(Res.string.traceroute_time_and_text, time, text),
|
||||
contentDescription = stringResource(Res.string.traceroute),
|
||||
modifier =
|
||||
Modifier.combinedClickable(onLongClick = { expanded = true }) {
|
||||
val dialogMessage =
|
||||
tracerouteDetailsAnnotated
|
||||
?: result
|
||||
?.fromRadio
|
||||
?.packet
|
||||
?.getTracerouteResponse(
|
||||
::getUsername,
|
||||
headerTowards = headerTowardsStr,
|
||||
headerBack = headerBackStr,
|
||||
)
|
||||
?.let {
|
||||
annotateTraceroute(
|
||||
it,
|
||||
statusGreen = statusGreen,
|
||||
statusYellow = statusYellow,
|
||||
statusOrange = statusOrange,
|
||||
Modifier.combinedClickable(
|
||||
onLongClick = { expanded = true },
|
||||
onClick = {
|
||||
val dialogMessage =
|
||||
tracerouteDetailsAnnotated
|
||||
?: result
|
||||
?.fromRadio
|
||||
?.packet
|
||||
?.getTracerouteResponse(
|
||||
::getUsername,
|
||||
headerTowards = headerTowardsStr,
|
||||
headerBack = headerBackStr,
|
||||
)
|
||||
}
|
||||
dialogMessage?.let {
|
||||
val responseLogUuid = result?.uuid ?: return@combinedClickable
|
||||
viewModel.showTracerouteDetail(
|
||||
annotatedMessage = it,
|
||||
requestId = log.fromRadio.packet?.id ?: 0,
|
||||
responseLogUuid = responseLogUuid,
|
||||
overlay = overlay,
|
||||
onViewOnMap = onViewOnMap,
|
||||
onShowError = { /* Handle error */ },
|
||||
)
|
||||
}
|
||||
},
|
||||
?.let {
|
||||
annotateTraceroute(
|
||||
it,
|
||||
statusGreen = statusGreen,
|
||||
statusYellow = statusYellow,
|
||||
statusOrange = statusOrange,
|
||||
)
|
||||
}
|
||||
dialogMessage?.let {
|
||||
val responseLogUuid = result?.uuid ?: return@combinedClickable
|
||||
viewModel.showTracerouteDetail(
|
||||
annotatedMessage = it,
|
||||
requestId = log.fromRadio.packet?.id ?: 0,
|
||||
responseLogUuid = responseLogUuid,
|
||||
overlay = overlay,
|
||||
onViewOnMap = onViewOnMap,
|
||||
onShowError = { /* Handle error */ },
|
||||
)
|
||||
}
|
||||
},
|
||||
)
|
||||
.onRightClick { expanded = true },
|
||||
)
|
||||
DropdownMenu(expanded = expanded, onDismissRequest = { expanded = false }) {
|
||||
DeleteItem {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue