mirror of
https://github.com/vitorpamplona/amethyst.git
synced 2026-08-11 08:47:33 +00:00
Merge pull request #3578 from vitorpamplona/claude/nip29-group-preloading-18kc3e
Prefetch recent group chat messages for list previews
This commit is contained in:
+61
-15
@@ -31,6 +31,9 @@ import com.vitorpamplona.quartz.nip01Core.relay.normalizer.RelayUrlNormalizer
|
||||
import com.vitorpamplona.quartz.nip29RelayGroups.metadata.GroupAdminsEvent
|
||||
import com.vitorpamplona.quartz.nip29RelayGroups.metadata.GroupMembersEvent
|
||||
import com.vitorpamplona.quartz.nip29RelayGroups.metadata.GroupMetadataEvent
|
||||
import com.vitorpamplona.quartz.nip29RelayGroups.tags.GroupIdTag
|
||||
import com.vitorpamplona.quartz.nip88Polls.poll.PollEvent
|
||||
import com.vitorpamplona.quartz.nipC7Chats.ChatEvent
|
||||
|
||||
/** One screen's request to keep the roster of the user's joined groups fresh. */
|
||||
class RelayGroupMyJoinedGroupsQueryState(
|
||||
@@ -49,6 +52,20 @@ private val RELAY_GROUP_ROSTER_KINDS =
|
||||
GroupMembersEvent.KIND,
|
||||
)
|
||||
|
||||
/**
|
||||
* Timeline kinds shown in a group's chat — chat messages and polls. Kept in sync with the
|
||||
* in-group feed ([com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.datasource
|
||||
* .subassemblies.filterMessagesToRelayGroup]) so the preview and the opened chat agree.
|
||||
*/
|
||||
private val RELAY_GROUP_PREVIEW_CONTENT_KINDS = listOf(ChatEvent.KIND, PollEvent.KIND)
|
||||
|
||||
/**
|
||||
* How many recent chat events to prefetch per joined group. Enough for the Messages-list
|
||||
* preview to reflect the true newest message and for opening the group to land on a populated
|
||||
* first screen. Matches [RELAY_GROUP_WARMUP_LIMIT].
|
||||
*/
|
||||
private const val RELAY_GROUP_JOINED_PREVIEW_LIMIT = 50
|
||||
|
||||
/**
|
||||
* Keeps the relay-signed roster (metadata/admins/members) of every group the user
|
||||
* has joined live while a groups-bearing screen is on top, so membership,
|
||||
@@ -56,8 +73,15 @@ private val RELAY_GROUP_ROSTER_KINDS =
|
||||
* without having to open each chat. This matters most for closed/private groups,
|
||||
* where the only way to confirm a join was admitted is a fresh 39002.
|
||||
*
|
||||
* One subscription per host relay, scoped by `#d` to just that relay's joined
|
||||
* group ids — so we don't pull a relay's whole directory, only what we're in.
|
||||
* On top of the roster it prefetches a bounded slice of each group's most recent chat
|
||||
* (kind 9 + polls), so the Messages-list preview shows the true newest message instead of
|
||||
* whatever kind-9 events happened to already be cached, and opening a group lands on
|
||||
* populated content. Without this the list would only ever surface "scattered" messages
|
||||
* that arrived through unrelated subscriptions until the group was actually opened.
|
||||
*
|
||||
* Roster is one `#d`-scoped filter per host relay (only what we're in, not the relay's whole
|
||||
* directory); content is one `#h`-scoped, limited filter per group (a per-filter limit can't be
|
||||
* shared across groups, and `#d`/`#h` can't be merged into a single filter).
|
||||
*/
|
||||
class RelayGroupMyJoinedGroupsFilterAssembler(
|
||||
client: INostrClient,
|
||||
@@ -85,21 +109,43 @@ class RelayGroupMyJoinedGroupsSubAssembler(
|
||||
val joined = key.account.relayGroupList.liveRelayGroupList.value
|
||||
if (joined.isEmpty()) return null
|
||||
|
||||
// Group the joined group ids by their host relay: one #d-scoped filter each.
|
||||
// Group the joined group ids by their host relay: one #d-scoped roster filter each.
|
||||
val idsByRelay = joined.groupBy({ it.relayUrl }, { it.groupId })
|
||||
|
||||
return idsByRelay.mapNotNull { (relayUrl, groupIds) ->
|
||||
val relay = RelayUrlNormalizer.normalizeOrNull(relayUrl) ?: return@mapNotNull null
|
||||
RelayBasedFilter(
|
||||
relay = relay,
|
||||
filter =
|
||||
Filter(
|
||||
kinds = RELAY_GROUP_ROSTER_KINDS,
|
||||
tags = mapOf("d" to groupIds.distinct()),
|
||||
since = since?.get(relay)?.time,
|
||||
),
|
||||
)
|
||||
}
|
||||
val rosterFilters =
|
||||
idsByRelay.mapNotNull { (relayUrl, groupIds) ->
|
||||
val relay = RelayUrlNormalizer.normalizeOrNull(relayUrl) ?: return@mapNotNull null
|
||||
RelayBasedFilter(
|
||||
relay = relay,
|
||||
filter =
|
||||
Filter(
|
||||
kinds = RELAY_GROUP_ROSTER_KINDS,
|
||||
tags = mapOf("d" to groupIds.distinct()),
|
||||
since = since?.get(relay)?.time,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
// One #h-scoped, limited content slice per joined group so list previews show the true
|
||||
// newest chat and opening the group lands on cached messages. A group's #d roster id and
|
||||
// its #h message id are the same string, but #d and #h can't be merged into one filter and
|
||||
// a limit is per-filter, so this stays one bounded filter per group.
|
||||
val contentFilters =
|
||||
joined.mapNotNull { group ->
|
||||
val relay = RelayUrlNormalizer.normalizeOrNull(group.relayUrl) ?: return@mapNotNull null
|
||||
RelayBasedFilter(
|
||||
relay = relay,
|
||||
filter =
|
||||
Filter(
|
||||
kinds = RELAY_GROUP_PREVIEW_CONTENT_KINDS,
|
||||
tags = mapOf(GroupIdTag.TAG_NAME to listOf(group.groupId)),
|
||||
limit = RELAY_GROUP_JOINED_PREVIEW_LIMIT,
|
||||
since = since?.get(relay)?.time,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
return rosterFilters + contentFilters
|
||||
}
|
||||
|
||||
override fun id(key: RelayGroupMyJoinedGroupsQueryState) = key.account
|
||||
|
||||
Reference in New Issue
Block a user