fix: show a just-joined NIP-29 group on the Messages tab immediately

The Messages tab (INLINE mode) mapped each joined group to its newest cached
kind-9 message and dropped it when none existed. Since the joined-groups
subscription only fetches roster kinds (39000/1/2), not chat, a group you just
joined stayed invisible until you opened it (loading messages) or posted — unlike
Marmot groups, which already fall back to a placeholder row.

Mirror the Marmot pattern for relay groups:
 - RelayGroupChannel.placeholderNote(): a cached synthetic note that adds the
   channel as a gatherer, so the existing Messages row renderer resolves it back
   to the group (RelayGroupRoomCompose already handles a null-event note).
 - ChatroomListKnownFeedFilter.feed(): fall back to placeholderNote() when the
   group has no loaded message.
 - AccountFeedContentStates: rebuild dmKnown when relayGroupList (kind 10009)
   changes — join/leave doesn't flow through newEventBundles, so without this the
   placeholder wouldn't appear until a later event.

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-09 20:22:00 +00:00
parent 2ffd64c170
commit dae4ade55a
3 changed files with 38 additions and 3 deletions
@@ -167,6 +167,17 @@ class AccountFeedContentStates(
}
}
// Same for the NIP-29 joined-group list (kind 10009): joining/leaving changes the list but
// doesn't flow through newEventBundles, so force a rebuild — otherwise a just-joined group
// (whose messages haven't loaded yet) wouldn't appear on the Messages tab until a later event.
scope.launch(Dispatchers.IO) {
account.relayGroupList.liveRelayGroupList
.drop(1)
.collect {
dmKnown.invalidateData()
}
}
// Pinning/unpinning a room only changes sort order, not membership, so no
// chat event flows through LocalCache. Force a rebuild to re-sort. This
// also fires when pins arrive via the synced AppSpecificData event.
@@ -102,12 +102,14 @@ class ChatroomListKnownFeedFilter(
if (account.settings.relayGroupViewMode.value == RelayGroupViewMode.INLINE) {
account.relayGroupList.liveRelayGroupList.value.mapNotNull { groupTag ->
val relay = RelayUrlNormalizer.normalizeOrNull(groupTag.relayUrl) ?: return@mapNotNull null
LocalCache
.getOrCreateRelayGroupChannel(GroupId(groupTag.groupId, relay))
.notes
val channel = LocalCache.getOrCreateRelayGroupChannel(GroupId(groupTag.groupId, relay))
// Newest loaded message, or a placeholder row so a just-joined group shows up on
// Messages before its first kind-9 arrives (mirrors the Marmot-group path above).
channel.notes
.filter { _, it -> account.isAcceptable(it) && it.event != null }
.sortedByDefaultFeedOrder()
.firstOrNull()
?: channel.placeholderNote()
}
} else {
emptyList()
@@ -23,6 +23,8 @@ package com.vitorpamplona.amethyst.commons.model.nip29RelayGroups
import androidx.compose.runtime.Stable
import com.vitorpamplona.amethyst.commons.model.Channel
import com.vitorpamplona.amethyst.commons.model.Note
import com.vitorpamplona.amethyst.commons.util.KmpLock
import com.vitorpamplona.amethyst.commons.util.withLock
import com.vitorpamplona.quartz.nip01Core.core.HexKey
import com.vitorpamplona.quartz.nip19Bech32.entities.NAddress
import com.vitorpamplona.quartz.nip29RelayGroups.GroupId
@@ -203,4 +205,24 @@ class RelayGroupChannel(
groupId.id.contains(prefix, true) ||
event?.name()?.contains(prefix, true) == true ||
event?.about()?.contains(prefix, true) == true
// Synthetic note representing this group in list views (the Messages tab) before any message
// has loaded — so a group the user just joined shows up immediately instead of waiting for its
// first cached kind-9. Mirrors MarmotGroupChatroom.placeholderNote(): adds this channel as a
// gatherer so the list row resolves back to it, and is cached with a stable id so equality-based
// feed diffing treats it as the same row across refreshes.
private val placeholderLock = KmpLock()
private var cachedPlaceholder: Note? = null
fun placeholderNote(): Note =
placeholderLock.withLock {
cachedPlaceholder ?: Note(placeholderIdHex(groupId)).apply {
addGatherer(this@RelayGroupChannel)
cachedPlaceholder = this
}
}
companion object {
fun placeholderIdHex(groupId: GroupId): HexKey = "relaygroup-empty-${groupId.toKey()}"
}
}