feat(ui): relay health indicator in NavigationRail
Add RelayHealthIndicator component — shows elapsed time since last relay event. Hidden when <30s (healthy), shows "45s ago" / "3m ago" when stale. Placed above BunkerHeartbeatIndicator in NavigationRail. Reads from Coordinator's subscriptionHealth StateFlow. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -736,6 +736,7 @@ fun MainContent(
|
|||||||
Row(Modifier.fillMaxSize().weight(1f)) {
|
Row(Modifier.fillMaxSize().weight(1f)) {
|
||||||
when (layoutMode) {
|
when (layoutMode) {
|
||||||
LayoutMode.SINGLE_PANE -> {
|
LayoutMode.SINGLE_PANE -> {
|
||||||
|
val healthMap by subscriptionsCoordinator.subscriptionHealth.collectAsState()
|
||||||
SinglePaneLayout(
|
SinglePaneLayout(
|
||||||
relayManager = relayManager,
|
relayManager = relayManager,
|
||||||
localCache = localCache,
|
localCache = localCache,
|
||||||
@@ -752,6 +753,7 @@ fun MainContent(
|
|||||||
onZapFeedback = onZapFeedback,
|
onZapFeedback = onZapFeedback,
|
||||||
signerConnectionState = signerConnectionState,
|
signerConnectionState = signerConnectionState,
|
||||||
lastPingTimeSec = lastPingTimeSec,
|
lastPingTimeSec = lastPingTimeSec,
|
||||||
|
subscriptionHealth = healthMap,
|
||||||
modifier = Modifier.weight(1f),
|
modifier = Modifier.weight(1f),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
+76
@@ -0,0 +1,76 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (c) 2025 Vitor Pamplona
|
||||||
|
*
|
||||||
|
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||||
|
* this software and associated documentation files (the "Software"), to deal in
|
||||||
|
* the Software without restriction, including without limitation the rights to use,
|
||||||
|
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
|
||||||
|
* Software, and to permit persons to whom the Software is furnished to do so,
|
||||||
|
* subject to the following conditions:
|
||||||
|
*
|
||||||
|
* The above copyright notice and this permission notice shall be included in all
|
||||||
|
* copies or substantial portions of the Software.
|
||||||
|
*
|
||||||
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||||
|
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||||
|
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
|
||||||
|
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||||
|
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
|
*/
|
||||||
|
package com.vitorpamplona.amethyst.desktop.ui.components
|
||||||
|
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.mutableLongStateOf
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.runtime.setValue
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import kotlinx.coroutines.delay
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Displays relay health as elapsed time since last event.
|
||||||
|
* Hidden when < 30s (healthy). Shows "45s" or "3m" when stale.
|
||||||
|
*/
|
||||||
|
@Composable
|
||||||
|
fun RelayHealthIndicator(
|
||||||
|
lastEventReceivedAt: Long?,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
) {
|
||||||
|
if (lastEventReceivedAt == null) return
|
||||||
|
|
||||||
|
// Tick every 5s to update elapsed display
|
||||||
|
var now by remember { mutableLongStateOf(System.currentTimeMillis()) }
|
||||||
|
LaunchedEffect(Unit) {
|
||||||
|
while (true) {
|
||||||
|
delay(5_000)
|
||||||
|
now = System.currentTimeMillis()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val elapsedMs = now - lastEventReceivedAt
|
||||||
|
if (elapsedMs < 30_000) return // healthy, don't show
|
||||||
|
|
||||||
|
val text = formatElapsed(elapsedMs)
|
||||||
|
|
||||||
|
Text(
|
||||||
|
text = text,
|
||||||
|
style = MaterialTheme.typography.labelSmall,
|
||||||
|
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||||
|
modifier = modifier.padding(horizontal = 8.dp),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun formatElapsed(elapsedMs: Long): String {
|
||||||
|
val seconds = elapsedMs / 1000
|
||||||
|
return when {
|
||||||
|
seconds < 60 -> "${seconds}s ago"
|
||||||
|
seconds < 3600 -> "${seconds / 60}m ago"
|
||||||
|
else -> "${seconds / 3600}h ago"
|
||||||
|
}
|
||||||
|
}
|
||||||
+13
@@ -65,7 +65,9 @@ import com.vitorpamplona.amethyst.desktop.cache.DesktopLocalCache
|
|||||||
import com.vitorpamplona.amethyst.desktop.network.DesktopRelayConnectionManager
|
import com.vitorpamplona.amethyst.desktop.network.DesktopRelayConnectionManager
|
||||||
import com.vitorpamplona.amethyst.desktop.service.highlights.DesktopHighlightStore
|
import com.vitorpamplona.amethyst.desktop.service.highlights.DesktopHighlightStore
|
||||||
import com.vitorpamplona.amethyst.desktop.subscriptions.DesktopRelaySubscriptionsCoordinator
|
import com.vitorpamplona.amethyst.desktop.subscriptions.DesktopRelaySubscriptionsCoordinator
|
||||||
|
import com.vitorpamplona.amethyst.desktop.subscriptions.SubscriptionHealth
|
||||||
import com.vitorpamplona.amethyst.desktop.ui.ZapFeedback
|
import com.vitorpamplona.amethyst.desktop.ui.ZapFeedback
|
||||||
|
import com.vitorpamplona.amethyst.desktop.ui.components.RelayHealthIndicator
|
||||||
import com.vitorpamplona.amethyst.desktop.ui.media.LocalIsImmersiveFullscreen
|
import com.vitorpamplona.amethyst.desktop.ui.media.LocalIsImmersiveFullscreen
|
||||||
import com.vitorpamplona.quartz.nip47WalletConnect.Nip47WalletConnect.Nip47URINorm
|
import com.vitorpamplona.quartz.nip47WalletConnect.Nip47WalletConnect.Nip47URINorm
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
@@ -108,6 +110,7 @@ fun SinglePaneLayout(
|
|||||||
onZapFeedback: (ZapFeedback) -> Unit,
|
onZapFeedback: (ZapFeedback) -> Unit,
|
||||||
signerConnectionState: SignerConnectionState,
|
signerConnectionState: SignerConnectionState,
|
||||||
lastPingTimeSec: Long?,
|
lastPingTimeSec: Long?,
|
||||||
|
subscriptionHealth: Map<String, SubscriptionHealth> = emptyMap(),
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
) {
|
) {
|
||||||
var currentColumnType by remember { mutableStateOf<DeckColumnType>(DeckColumnType.HomeFeed) }
|
var currentColumnType by remember { mutableStateOf<DeckColumnType>(DeckColumnType.HomeFeed) }
|
||||||
@@ -150,6 +153,16 @@ fun SinglePaneLayout(
|
|||||||
|
|
||||||
Spacer(Modifier.weight(1f))
|
Spacer(Modifier.weight(1f))
|
||||||
|
|
||||||
|
// Relay health — shows elapsed time since last event (hidden when <30s)
|
||||||
|
val latestEvent =
|
||||||
|
subscriptionHealth.values
|
||||||
|
.mapNotNull { it.lastEventReceivedAt }
|
||||||
|
.maxOrNull()
|
||||||
|
RelayHealthIndicator(
|
||||||
|
lastEventReceivedAt = latestEvent,
|
||||||
|
modifier = Modifier.padding(bottom = 4.dp),
|
||||||
|
)
|
||||||
|
|
||||||
BunkerHeartbeatIndicator(
|
BunkerHeartbeatIndicator(
|
||||||
signerConnectionState = signerConnectionState,
|
signerConnectionState = signerConnectionState,
|
||||||
lastPingTimeSec = lastPingTimeSec,
|
lastPingTimeSec = lastPingTimeSec,
|
||||||
|
|||||||
Reference in New Issue
Block a user