From dae4ade55a142bdc02cb9485aafd0772c66e0a07 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 9 Jul 2026 20:22:00 +0000 Subject: [PATCH] fix: show a just-joined NIP-29 group on the Messages tab immediately MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 Claude-Session: https://claude.ai/code/session_01B5MLY4hq5LXJ2D5WeLRyXj --- .../loggedIn/AccountFeedContentStates.kt | 11 ++++++++++ .../rooms/dal/ChatroomListKnownFeedFilter.kt | 8 ++++--- .../nip29RelayGroups/RelayGroupChannel.kt | 22 +++++++++++++++++++ 3 files changed, 38 insertions(+), 3 deletions(-) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AccountFeedContentStates.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AccountFeedContentStates.kt index cb90c18215..2faf0a678e 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AccountFeedContentStates.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AccountFeedContentStates.kt @@ -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. diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/dal/ChatroomListKnownFeedFilter.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/dal/ChatroomListKnownFeedFilter.kt index 9d0eb04e56..a945ac5ca6 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/dal/ChatroomListKnownFeedFilter.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/dal/ChatroomListKnownFeedFilter.kt @@ -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() diff --git a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/nip29RelayGroups/RelayGroupChannel.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/nip29RelayGroups/RelayGroupChannel.kt index d3464474d3..4a05bfa61c 100644 --- a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/nip29RelayGroups/RelayGroupChannel.kt +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/nip29RelayGroups/RelayGroupChannel.kt @@ -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()}" + } }