feat: warm each group's recent messages on a relay's channel list

Opening a group from the relay's channel list used to start its chat from a cold
load. Mount the existing RelayGroupWarmupSubscription on every visible card
(content-only — the directory subscription already streams metadata), so a tap
lands on already-cached messages.

Add a contentLimit to the warmup (default 50, unchanged for discovery's "50+"
signal); the channel list passes ~10 — a first screen's worth. Bounded to
visible rows by the LazyColumn and released as they scroll off.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01B5MLY4hq5LXJ2D5WeLRyXj
This commit is contained in:
Claude
2026-07-10 00:13:53 +00:00
parent 1ecc97de76
commit cb90362bae
3 changed files with 29 additions and 8 deletions
@@ -60,6 +60,7 @@ import com.vitorpamplona.amethyst.ui.navigation.routes.Route
import com.vitorpamplona.amethyst.ui.navigation.routes.routeFor
import com.vitorpamplona.amethyst.ui.navigation.topbars.TopBarExtensibleWithBackButton
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.relayGroup.datasource.RelayGroupWarmupSubscription
import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.relayGroup.datasource.RelayGroupsOnRelaySubscription
import com.vitorpamplona.amethyst.ui.stringRes
import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter
@@ -67,11 +68,15 @@ import com.vitorpamplona.quartz.nip01Core.relay.normalizer.RelayUrlNormalizer
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.displayUrl
import com.vitorpamplona.quartz.nip29RelayGroups.metadata.GroupMetadataEvent
/** A first screen's worth of recent messages to prefetch per visible group card, ahead of a tap. */
private const val CHANNEL_LIST_WARMUP_LIMIT = 10
/**
* Lists every channel a relay hosts (its kind 39000-39003 directory), so the user
* can browse and open channels on that relay. The relay's directory is streamed by
* [RelayGroupsOnRelaySubscription] and consumed into per-group channels; this
* screen reads them back for the relay and renders them.
* screen reads them back for the relay and renders them. Each visible card also warms
* its group's recent messages so opening a chat lands on cached content.
*/
@Composable
fun RelayGroupChannelListScreen(
@@ -156,6 +161,18 @@ private fun RelayGroupChannelRow(
val joined = channel.membershipOf(myPubkey).isMember()
val memberCount = channel.memberCount()
// Anticipate a tap: while this row is on-screen, prefetch a first screen's worth of recent
// messages for its group (content only — the directory subscription already streams metadata),
// so opening the chat lands on cached content instead of a blank load. Bounded to visible rows
// by the LazyColumn, and released as they scroll off.
RelayGroupWarmupSubscription(
channel,
accountViewModel.dataSources().relayGroupWarmup,
accountViewModel,
contentOnly = true,
contentLimit = CHANNEL_LIST_WARMUP_LIMIT,
)
Row(
modifier =
Modifier
@@ -40,6 +40,8 @@ class RelayGroupWarmupQueryState(
val channel: RelayGroupChannel,
/** When true, prefetch only recent content — the caller's screen already streams metadata. */
val contentOnly: Boolean = false,
/** How many recent content events to prefetch ahead of a tap. */
val contentLimit: Int = RELAY_GROUP_WARMUP_LIMIT,
)
/** Newest content kinds we prefetch so opening the card lands on populated screens. */
@@ -47,11 +49,12 @@ private val RELAY_GROUP_WARMUP_CONTENT_KINDS =
listOf(ChatEvent.KIND, PollEvent.KIND, ThreadEvent.KIND, CommentEvent.KIND)
/**
* How many recent events to pull ahead of a tap — enough to fill the first screen AND drive the
* discovery card's "50+ messages" activity signal (a chat that returns the full page reads as
* "50+"; fewer shows the exact loaded count).
* Default number of recent events to pull ahead of a tap — enough to fill the first screen AND drive
* the discovery card's "50+ messages" activity signal (a chat that returns the full page reads as
* "50+"; fewer shows the exact loaded count). Callers that only need a first-screen preview (e.g. a
* relay's channel list) pass a smaller [RelayGroupWarmupQueryState.contentLimit].
*/
private const val RELAY_GROUP_WARMUP_LIMIT = 50
const val RELAY_GROUP_WARMUP_LIMIT = 50
/**
* Warms a NIP-29 group referenced inline (a group-link card) without opening it:
@@ -92,7 +95,7 @@ class RelayGroupWarmupSubAssembler(
Filter(
kinds = RELAY_GROUP_WARMUP_CONTENT_KINDS,
tags = mapOf(GroupIdTag.TAG_NAME to listOf(groupId.id)),
limit = RELAY_GROUP_WARMUP_LIMIT,
limit = key.contentLimit,
since = since?.get(groupId.relayUrl)?.time,
),
)
@@ -37,10 +37,11 @@ fun RelayGroupWarmupSubscription(
dataSource: RelayGroupWarmupFilterAssembler,
accountViewModel: AccountViewModel,
contentOnly: Boolean = false,
contentLimit: Int = RELAY_GROUP_WARMUP_LIMIT,
) {
val state =
remember(channel.groupId, contentOnly) {
RelayGroupWarmupQueryState(channel, contentOnly)
remember(channel.groupId, contentOnly, contentLimit) {
RelayGroupWarmupQueryState(channel, contentOnly, contentLimit)
}
LifecycleAwareKeyDataSourceSubscription(state, dataSource)