From c66acfa131ecfd01d570a2b260a083d68bfe653b Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 10 May 2026 21:45:54 +0000 Subject: [PATCH] perf(RelayBadges): one sampled flow per note instead of three MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `RelayBadges` runs once per note in complete-UI mode. The closed view was opening three separate `relays.stateFlow` subscriptions (one per icon slot, each with its own `mapNotNull`), and both the closed and expanded views collected the relay list raw — every relay arrival on a fanned-out note triggered an immediate recomposition. Collapse to a single subscription that emits `relays.take(3)`, throttled with `sample(500)` and `distinctUntilChanged`. Slot widgets now pull from the resulting list instead of each owning a flow. Same treatment for the expanded `RenderAllRelayList` (now sampled) and `ShouldShowExpandButton` (wraps the cached `createMustShowExpandButtonFlows` lookup in `remember(note)` so the LRU is hit once per note instead of per recomposition). --- .../amethyst/ui/note/RelayListBox.kt | 64 +++++++++++-------- .../amethyst/ui/note/RelayListRow.kt | 3 +- 2 files changed, 41 insertions(+), 26 deletions(-) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/RelayListBox.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/RelayListBox.kt index 9f6e76aae..8a3d5551a 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/RelayListBox.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/RelayListBox.kt @@ -59,7 +59,11 @@ import com.vitorpamplona.amethyst.ui.theme.Size17Modifier import com.vitorpamplona.amethyst.ui.theme.StdVertSpacer import com.vitorpamplona.amethyst.ui.theme.noteComposeRelayBox import com.vitorpamplona.amethyst.ui.theme.placeholderText -import kotlinx.coroutines.flow.mapNotNull +import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl +import kotlinx.coroutines.FlowPreview +import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.sample @Composable fun RelayBadges( @@ -81,7 +85,7 @@ fun RelayBadges( } } -@OptIn(ExperimentalLayoutApi::class) +@OptIn(ExperimentalLayoutApi::class, FlowPreview::class) @Composable fun RenderAllRelayList( baseNote: Note, @@ -90,16 +94,26 @@ fun RenderAllRelayList( accountViewModel: AccountViewModel, nav: INav, ) { - val noteRelays by baseNote - .flow() - .relays.stateFlow - .collectAsStateWithLifecycle() + val flow = + remember(baseNote) { + baseNote + .flow() + .relays.stateFlow + .sample(500) + .map { it.note.relays } + .distinctUntilChanged() + } + + val relays by flow.collectAsStateWithLifecycle(baseNote.relays) FlowRow(modifier, verticalArrangement = verticalArrangement) { - noteRelays.note.relays.forEach { RenderRelay(it, accountViewModel, nav) } + relays.forEach { RenderRelay(it, accountViewModel, nav) } } } +// Single sampled subscription instead of one per slot: emits the first 3 relays from the note. +// Throttled to 500ms because relay arrivals can churn a list of an actively-fanned-out note. +@OptIn(FlowPreview::class) @Composable fun RenderClosedRelayList( baseNote: Note, @@ -108,32 +122,32 @@ fun RenderClosedRelayList( accountViewModel: AccountViewModel, nav: INav, ) { + val flow = + remember(baseNote) { + baseNote + .flow() + .relays.stateFlow + .sample(500) + .map { it.note.relays.take(3) } + .distinctUntilChanged() + } + + val initial = remember(baseNote) { baseNote.relays.take(3) } + val relays by flow.collectAsStateWithLifecycle(initial) + Row(modifier, verticalAlignment = verticalAlignment) { - WatchAndRenderRelay(baseNote, 0, accountViewModel, nav) - WatchAndRenderRelay(baseNote, 1, accountViewModel, nav) - WatchAndRenderRelay(baseNote, 2, accountViewModel, nav) + RenderRelaySlot(relays.getOrNull(0), accountViewModel, nav) + RenderRelaySlot(relays.getOrNull(1), accountViewModel, nav) + RenderRelaySlot(relays.getOrNull(2), accountViewModel, nav) } } @Composable -fun WatchAndRenderRelay( - baseNote: Note, - relayIndex: Int, +private fun RenderRelaySlot( + relay: NormalizedRelayUrl?, accountViewModel: AccountViewModel, nav: INav, ) { - val flow = - remember(baseNote, relayIndex) { - baseNote - .flow() - .relays.stateFlow - .mapNotNull { - it.note.relays.getOrNull(relayIndex) - } - } - - val relay by flow.collectAsStateWithLifecycle(baseNote.relays.getOrNull(relayIndex)) - CrossfadeIfEnabled(targetState = relay, label = "RenderRelay", modifier = Size17Modifier, accountViewModel = accountViewModel) { if (it != null) { RenderRelay(it, accountViewModel, nav) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/RelayListRow.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/RelayListRow.kt index 068b925f4..1d1672067 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/RelayListRow.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/RelayListRow.kt @@ -99,7 +99,8 @@ fun ShouldShowExpandButton( accountViewModel: AccountViewModel, content: @Composable () -> Unit, ) { - val showExpandButton by accountViewModel.createMustShowExpandButtonFlows(baseNote).collectAsStateWithLifecycle() + val flow = remember(baseNote) { accountViewModel.createMustShowExpandButtonFlows(baseNote) } + val showExpandButton by flow.collectAsStateWithLifecycle() if (showExpandButton) { content()