mirror of
https://github.com/vitorpamplona/amethyst.git
synced 2026-08-09 08:04:45 +00:00
feat(notifications): auto-retry faulty relays + actionable relay detail
Improve the notifications history UX around slow/unreachable relays, keeping the per-relay markers (they let users notice their own bad relays) but making recovery automatic and the tap-through actionable. - Auto-retry stalled relays with backoff (~3s→30s): once the buffer driver stops (every relay done-or-stalled) but some are merely stalled, keep re-advancing them so recovery no longer depends on the user scrolling to the marker or reopening the screen. One non-restarting effect so the backoff survives the transient in-flight blips each retry causes; cancels on leave. - Add a "Try Again" action to RelayReachDetailDialog (shared): when a caller passes onRetry and a relay is stalled, the tapped marker's detail popup offers an active retry and drops the now-inaccurate "retries on reopen" hint. Notifications wire it to advanceAll; DM callers pass nothing (unchanged). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QZ7uGCKZZWzXXpHyVmXw8f
This commit is contained in:
+28
-1
@@ -92,6 +92,7 @@ import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.combine
|
||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||
import kotlinx.coroutines.flow.filter
|
||||
import kotlinx.coroutines.flow.first
|
||||
|
||||
@Composable
|
||||
fun RenderCardFeed(
|
||||
@@ -204,6 +205,26 @@ private fun FeedLoaded(
|
||||
if (shouldLoadMore && !loadingMore && !exhausted) history.advanceAll()
|
||||
}
|
||||
|
||||
// Auto-retry faulty relays with backoff. The buffer driver above stops once every relay is done-or-
|
||||
// stalled (exhausted); when some are merely stalled (a slow/unreachable relay, not a real end) this
|
||||
// keeps re-advancing them so recovery doesn't depend on the user scrolling to the marker or reopening.
|
||||
// A single non-restarting effect so the backoff survives the transient in-flight blips each retry causes.
|
||||
LaunchedEffect(history) {
|
||||
var backoffMs = STALLED_RETRY_MIN_MS
|
||||
while (true) {
|
||||
history.status.first { it.exhausted && it.stalledCount > 0 } // park until stuck on a stalled relay
|
||||
while (true) {
|
||||
delay(backoffMs)
|
||||
val s = history.status.value
|
||||
if (!(s.exhausted && s.stalledCount > 0)) break // recovered (a relay answered, or scroll retried)
|
||||
history.advanceAll()
|
||||
history.loadingMore.first { !it } // let the retry settle before escalating
|
||||
backoffMs = (backoffMs * 2).coerceAtMost(STALLED_RETRY_MAX_MS)
|
||||
}
|
||||
backoffMs = STALLED_RETRY_MIN_MS // reset for the next stall
|
||||
}
|
||||
}
|
||||
|
||||
// One cursor per relay: its reached depth, state (reaching / stalled / done) and the advance() that pulls
|
||||
// its next page. A done relay's marker sinks to the oldest end reading "fully loaded".
|
||||
val limits =
|
||||
@@ -228,7 +249,8 @@ private fun FeedLoaded(
|
||||
// The relays behind a tapped in-stream marker; non-null shows the per-relay breakdown popup.
|
||||
var syncDetail by remember { mutableStateOf<List<RelayReachCursor>?>(null) }
|
||||
syncDetail?.let { detail ->
|
||||
RelayReachDetailDialog(detail, ::formatHistoryReachDate) { syncDetail = null }
|
||||
// Tap-through offers a Try Again on stalled relays, so a user who sees a bad relay can act on it.
|
||||
RelayReachDetailDialog(detail, ::formatHistoryReachDate, onRetry = { history.advanceAll() }) { syncDetail = null }
|
||||
}
|
||||
|
||||
StickToTopOnPrepend(listState, items.list.firstOrNull()?.id())
|
||||
@@ -383,6 +405,11 @@ private const val BOOTSTRAP_DEBOUNCE_MS = 1200L
|
||||
// Large on purpose: the feed reads as infinite scroll, the user practically never reaches the bottom.
|
||||
private const val NOTIFICATION_LOOKAHEAD_BUFFER = 100
|
||||
|
||||
// Backoff bounds for auto-retrying stalled (slow/unreachable) relays: first retry ~3s after a stall,
|
||||
// doubling up to ~30s, so a faulty relay is retried gently but keeps a chance to recover on its own.
|
||||
private const val STALLED_RETRY_MIN_MS = 3_000L
|
||||
private const val STALLED_RETRY_MAX_MS = 30_000L
|
||||
|
||||
private fun reachState(p: RelayPagingProgress): RelayReachState =
|
||||
when {
|
||||
p.done -> RelayReachState.DONE
|
||||
|
||||
+23
-2
@@ -59,6 +59,7 @@ import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.vitorpamplona.amethyst.commons.resources.Res
|
||||
import com.vitorpamplona.amethyst.commons.resources.action_dismiss
|
||||
import com.vitorpamplona.amethyst.commons.resources.action_try_again
|
||||
import com.vitorpamplona.amethyst.commons.resources.chats_history_all_caught_up
|
||||
import com.vitorpamplona.amethyst.commons.resources.chats_history_by_relay
|
||||
import com.vitorpamplona.amethyst.commons.resources.chats_history_incomplete
|
||||
@@ -377,14 +378,32 @@ private fun relayShortName(relay: NormalizedRelayUrl): String =
|
||||
fun RelayReachDetailDialog(
|
||||
cursors: List<RelayReachCursor>,
|
||||
formatReachDate: (epochSeconds: Long) -> String,
|
||||
// When provided and at least one relay is stalled, the dialog offers a "Try Again" action that
|
||||
// re-advances the stalled relays on demand (and suppresses the "retries on reopen" hint, since the
|
||||
// caller retries actively). Null keeps the passive, dismiss-only dialog (the DM callers).
|
||||
onRetry: (() -> Unit)? = null,
|
||||
onDismiss: () -> Unit,
|
||||
) {
|
||||
val rows = remember(cursors) { cursors.sortedBy { it.reachedUntil } }
|
||||
val showRetry = onRetry != null && rows.any { it.state == RelayReachState.STALLED }
|
||||
AlertDialog(
|
||||
onDismissRequest = onDismiss,
|
||||
confirmButton = {
|
||||
TextButton(onClick = onDismiss) { Text(stringResource(Res.string.action_dismiss)) }
|
||||
if (showRetry) {
|
||||
TextButton(onClick = {
|
||||
onRetry?.invoke()
|
||||
onDismiss()
|
||||
}) { Text(stringResource(Res.string.action_try_again)) }
|
||||
} else {
|
||||
TextButton(onClick = onDismiss) { Text(stringResource(Res.string.action_dismiss)) }
|
||||
}
|
||||
},
|
||||
dismissButton =
|
||||
if (showRetry) {
|
||||
{ TextButton(onClick = onDismiss) { Text(stringResource(Res.string.action_dismiss)) } }
|
||||
} else {
|
||||
null
|
||||
},
|
||||
title = { Text(stringResource(Res.string.chats_history_by_relay)) },
|
||||
text = {
|
||||
Column(
|
||||
@@ -425,7 +444,9 @@ fun RelayReachDetailDialog(
|
||||
maxLines = 1,
|
||||
)
|
||||
}
|
||||
if (c.state == RelayReachState.STALLED) StalledRetryHint()
|
||||
// The passive "retries on reopen" caption only applies without an active Try Again
|
||||
// action; with one, the button (and the caller's auto-retry) covers it.
|
||||
if (c.state == RelayReachState.STALLED && onRetry == null) StalledRetryHint()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user