From cc94ef91036290fcc18e5b2bfe809c3ba8b86d35 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 20 Jul 2026 20:14:30 +0000 Subject: [PATCH] fix(notifications): retry stalled relays via per-relay sentinels MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The buffer-only driver stopped paging once every relay was done-or-stalled (exhausted), so a transient all-relays blip halted history until re-navigation — bad for faulty relays with different datasets, exactly when we'd miss data. Restore the per-relay RelayReachSentinels alongside the look-ahead buffer: - the buffer driver (advanceAll) keeps the runway full from healthy relays; - the sentinels retry an individual relay when its frontier marker scrolls into view — the recovery path once the buffer can't keep the frontier ahead (relays stalled/exhausted), naturally rate-limited by scrolling. The buffer keeps the frontier ~a screen below the fold, so the sentinels stay quiet during normal scrolling and only fire on stall/end. This also makes the kept per-relay markers functional again (they drive the retry) instead of purely decorative. Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_01QZ7uGCKZZWzXXpHyVmXw8f --- .../loggedIn/notifications/CardFeedView.kt | 38 +++++++++++++------ 1 file changed, 27 insertions(+), 11 deletions(-) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/notifications/CardFeedView.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/notifications/CardFeedView.kt index afafdc267b..6f43b4624a 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/notifications/CardFeedView.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/notifications/CardFeedView.kt @@ -58,6 +58,7 @@ import com.vitorpamplona.amethyst.commons.relayClient.paging.PagingStatus import com.vitorpamplona.amethyst.commons.ui.feeds.RelayReachCursor import com.vitorpamplona.amethyst.commons.ui.feeds.RelayReachDetailDialog import com.vitorpamplona.amethyst.commons.ui.feeds.RelayReachMarkers +import com.vitorpamplona.amethyst.commons.ui.feeds.RelayReachSentinels import com.vitorpamplona.amethyst.commons.ui.feeds.RelayReachState import com.vitorpamplona.amethyst.commons.ui.layouts.rememberFeedContentPadding import com.vitorpamplona.amethyst.commons.ui.notifications.Card @@ -172,12 +173,15 @@ private fun FeedLoaded( val items by loaded.feed.collectAsStateWithLifecycle() val openPolls by polls.flow.collectAsStateWithLifecycle() - // Infinite-scroll backward pagination: the notifications feed keeps a fat look-ahead buffer of older - // notifications loaded below the viewport. When fewer than [NOTIFICATION_LOOKAHEAD_BUFFER] rows remain - // ahead of the last visible one, every not-done relay is stepped one older page (until+limit, gap-proof) - // — refilling eagerly and repeating until the buffer is full again or all relays run dry. The per-relay - // [BackwardRelayPager] engine is the same one the DM history uses; only the trigger differs (buffer depth - // here vs. marker visibility there). + // Infinite-scroll backward pagination over the per-relay [BackwardRelayPager] (the same engine the DM + // history uses — each relay keeps its own until+limit cursor so faulty relays with different datasets + // page independently and can't gap each other). Two drivers cooperate: + // 1. the look-ahead BUFFER below keeps a fat runway of older notifications loaded ahead of the viewport + // (advanceAll), so healthy relays fill the feed and the user practically never reaches the end; + // 2. the per-relay MARKERS/SENTINELS below retry an INDIVIDUAL relay when its frontier marker scrolls + // into view — the recovery path for a stalled/faulty relay, naturally rate-limited by scrolling. + // The buffer keeps the frontier ~a screen-full below the fold, so the sentinels stay quiet during normal + // scrolling and only fire when the buffer can't keep up (relays stalled/exhausted) — exactly a retry. val history = remember(accountViewModel) { accountViewModel.dataSources().account.notificationsHistory } val historyStatus by history.status.collectAsStateWithLifecycle() @@ -201,8 +205,7 @@ private fun FeedLoaded( } // One cursor per relay: its reached depth, state (reaching / stalled / done) and the advance() that pulls - // its next page (also usable to retry a stalled relay by tapping). A done relay's marker sinks to the - // oldest end reading "fully loaded". + // its next page. A done relay's marker sinks to the oldest end reading "fully loaded". val limits = remember(historyStatus) { historyStatus.relayProgress.map { (relay, p) -> @@ -210,6 +213,18 @@ private fun FeedLoaded( } } + // Count of items above the notification cards in the LazyColumn (scaffold header + donation card + open + // polls), so the hoisted sentinel can map a visible LazyColumn index back to a card. + val leadingItemCount = (if (headerContent != null) 1 else 0) + 1 + openPolls.size + + // Per-relay retry driver: when a relay's frontier marker is on screen (the buffer couldn't keep the + // frontier ahead, i.e. that relay stalled or the feed is genuinely at its end), step that one relay. + // A done relay drives nothing. This is the recovery path the buffer driver above can't cover once every + // relay is stalled (exhausted) — scrolling to the stalled marker retries it, no hammering. + if (limits.isNotEmpty()) { + RelayReachSentinels(limits, listState) { index -> items.list.getOrNull(index - leadingItemCount)?.createdAt() } + } + // The relays behind a tapped in-stream marker; non-null shows the per-relay breakdown popup. var syncDetail by remember { mutableStateOf?>(null) } syncDetail?.let { detail -> @@ -321,9 +336,10 @@ private fun FeedLoaded( thickness = DividerThickness, ) - // Per-relay progress markers in the gap toward the next-older card, at the depth each relay has - // paged to (loading is driven by the look-ahead buffer above, not by these). olderCreatedAt is - // null past the oldest loaded card, so relays that reached the bottom sit there as "fully loaded". + // Per-relay markers in the gap toward the next-older card, at the depth each relay has paged to. + // The bulk load is buffer-driven (above); these mark each relay's frontier and drive the + // stalled-relay retry when scrolled into view (see the sentinel above). olderCreatedAt is null + // past the oldest loaded card, so relays that reached the bottom sit there as "fully loaded". if (limits.isNotEmpty()) { RelayReachMarkers( limits,