diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/LocalCache.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/LocalCache.kt index 10a748f07..ae6d013ef 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/LocalCache.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/LocalCache.kt @@ -1549,28 +1549,24 @@ object LocalCache : ILocalCache, ICacheProvider { } /** - * Audio-room presence (kind-10312) — addressable storage AND - * attach the version note to the room's [LiveActivitiesChannel]. - * The home live-bubble surfaces a room when a follow is - * publishing in it; that fan-out walks `channel.notes`, so the - * presence event needs to be in there alongside chat. Without - * this, presence-driven inclusion can't see follows broadcasting - * in the room (only chat-driven inclusion would fire). + * Audio-room presence (kind-10312) — addressable storage plus an + * author-keyed entry in the room's + * [LiveActivitiesChannel.presenceNotes] index. * - * Also indexed under [LiveActivitiesChannel.presenceNotes] keyed - * by author so the Nests feed can answer "are there speakers on - * stage?" without scanning the chat-dominated `notes` map. + * Presence is intentionally NOT added to `channel.notes`: that + * map is dominated by chat in active rooms and feeds that care + * about presence (Nests drawer, home live-bubble, NestsFeedLoaded) + * iterate `presenceNotes` directly for an O(speakers) scan. * * Cross-room move handling: kind-10312 is replaceable per author, * but the room a presence points to (`a`-tag) can change when a - * speaker hops between rooms. The replaceable cache only swaps the - * addressable's content — it has no notion of which channel the - * old version was attached to. Without explicit eviction the old - * room would keep surfacing as "live" via the stale entry until it - * dropped out of the freshness window. Capture the prior room - * before replacement and, when it differs, drop the author from - * the old channel's presence index and the old version note from - * its main `notes` index. + * speaker hops between rooms. The replaceable cache only swaps + * the addressable's content — it has no notion of which channel + * the old version was attached to. Without explicit eviction the + * old room would keep surfacing as "live" via the stale entry + * until it dropped out of the freshness window. Capture the prior + * room before replacement and, when it differs, drop the author + * from the old channel's presence index. */ fun consume( event: MeetingRoomPresenceEvent, @@ -1583,24 +1579,17 @@ object LocalCache : ILocalCache, ICacheProvider { val new = consumeBaseReplaceable(event, relay, wasVerified) - // The replaceable cache keys this on the AUTHOR's address - // (kind=10312, pubkey, fixed-d-tag) — independent of the - // room. To wire the room bubble we also attach the version - // note to the room's channel keyed by its kind-30312 address. val roomAddress = event.interactiveRoom()?.address ?: return new if (roomAddress.kind != MeetingSpaceEvent.KIND) return new if (isReplacement && priorRoomAddress != null && priorRoomAddress != roomAddress) { - getLiveActivityChannelIfExists(priorRoomAddress)?.let { priorChannel -> - priorChannel.removePresenceNote(event.pubKey) - getNoteIfExists(priorVersion.id)?.let { priorChannel.removeNote(it) } - } + getLiveActivityChannelIfExists(priorRoomAddress)?.removePresenceNote(event.pubKey) } val channel = getOrCreateLiveChannel(roomAddress) val versionNote = getOrCreateNote(event.id) - channel.addNote(versionNote, relay) channel.addPresenceNote(versionNote) + if (relay != null) channel.addRelay(relay) return new } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/dal/ChannelFeedFilter.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/dal/ChannelFeedFilter.kt index 485b97765..fea85e90b 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/dal/ChannelFeedFilter.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/dal/ChannelFeedFilter.kt @@ -26,7 +26,6 @@ import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.ui.dal.AdditiveFeedFilter import com.vitorpamplona.amethyst.ui.dal.ChangesFlowFilter import com.vitorpamplona.amethyst.ui.dal.DefaultFeedOrder -import com.vitorpamplona.quartz.nip53LiveActivities.presence.MeetingRoomPresenceEvent class ChannelFeedFilter( val channel: Channel, @@ -38,25 +37,12 @@ class ChannelFeedFilter( override fun changesFlow() = channel.changesFlow() // returns the last Note of each user. - override fun feed(): List = sort(channel.notes.filterIntoSet { _, it -> isChatEvent(it) && account.isAcceptable(it) }) + override fun feed(): List = sort(channel.notes.filterIntoSet { _, it -> account.isAcceptable(it) }) override fun applyFilter(newItems: Set): Set = newItems - .filter { channel.notes.containsKey(it.idHex) && isChatEvent(it) && account.isAcceptable(it) } + .filter { channel.notes.containsKey(it.idHex) && account.isAcceptable(it) } .toSet() override fun sort(items: Set): List = items.sortedWith(DefaultFeedOrder) - - /** - * Reject non-chat events that get attached to a [Channel] for other - * surfaces. The current case: kind-10312 - * [MeetingRoomPresenceEvent]s land in `channel.notes` so the home - * live-bubble can detect a follow broadcasting in a Nest - * (HomeLiveFilter scans channel.notes); they have no chat content - * and would otherwise render as an empty card in the chat panel. - * - * Anything else is passed through — chat messages, zaps, raids, - * clips, channel-create / metadata events all belong here. - */ - private fun isChatEvent(note: Note): Boolean = note.event !is MeetingRoomPresenceEvent } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/dal/HomeLiveFilter.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/dal/HomeLiveFilter.kt index 491189e5a..29d067e40 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/dal/HomeLiveFilter.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/dal/HomeLiveFilter.kt @@ -103,6 +103,16 @@ class HomeLiveFilter( return true } + // Audio-room presence (kind-10312) lives in `presenceNotes`, + // not `notes`. Check it separately so a follow broadcasting in + // a Nest still surfaces the bubble even if no chat happened. + val hasPresence = + channel.presenceNotes + .filter { _, note -> + acceptableChatEvent(note, filterParams, timeLimit) + }.isNotEmpty() + if (hasPresence) return true + return channel.notes .filter { _, value -> acceptableChatEvent(value, filterParams, timeLimit) @@ -284,6 +294,18 @@ class HomeLiveFilter( } } + // Audio-room presence is indexed separately from `notes`. Add + // its author count so audio-room hosts/speakers still factor + // into the follow-participation sort even when they haven't + // chatted in the room. + if (channel is LiveActivitiesChannel) { + channel.presenceNotes.forEach { authorHex, _ -> + if (followingSet == null || authorHex in followingSet) { + count++ + } + } + } + return count } } diff --git a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/nip53LiveActivities/LiveActivitiesChannel.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/nip53LiveActivities/LiveActivitiesChannel.kt index 1857bcffc..75b202dae 100644 --- a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/nip53LiveActivities/LiveActivitiesChannel.kt +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/nip53LiveActivities/LiveActivitiesChannel.kt @@ -49,15 +49,22 @@ class LiveActivitiesChannel( * unbounded the way `notes` does. Empty for streaming channels * (kind-30311) — only kind-30312 rooms publish presence. * - * Lets feeds answer "is anyone live on stage in this room?" - * without scanning the chat-dominated `notes` map. See - * [com.vitorpamplona.amethyst.ui.screen.loggedIn.nests.dal.NestsFeedFilter]. + * Presence lives ONLY here, not in the base `notes` map: the + * mixed-kind `notes` is dominated by chat in active rooms and + * iterating it just to find presence is wasteful. Feeds that need + * "is anyone live on stage in this room?" iterate this index + * directly. See + * [com.vitorpamplona.amethyst.ui.screen.loggedIn.nests.dal.NestsFeedFilter] + * and [com.vitorpamplona.amethyst.ui.screen.loggedIn.home.dal.HomeLiveFilter]. */ val presenceNotes = LargeCache() fun addPresenceNote(note: Note) { val author = note.author?.pubkeyHex ?: return + val previous = presenceNotes.get(author) + if (previous?.idHex == note.idHex) return presenceNotes.put(author, note) + flowSet?.notes?.invalidateData() } /** @@ -68,7 +75,9 @@ class LiveActivitiesChannel( * freshness window. */ fun removePresenceNote(author: HexKey) { + if (!presenceNotes.containsKey(author)) return presenceNotes.remove(author) + flowSet?.notes?.invalidateData() } fun address() = address