refactor(nests): keep room presence out of channel.notes entirely
Presence (kind-10312) was being stored in both `channel.notes` and the `presenceNotes` index. The mixed-kind `notes` map is dominated by chat in active rooms, and only HomeLiveFilter still read presence from it -- which is now migrated to scan presenceNotes directly. - LocalCache.consume(MeetingRoomPresenceEvent): drop the `channel.addNote` call; only addPresenceNote, plus addRelay so the channel's relay-counter still tracks where presence arrived from. - LiveActivitiesChannel: addPresenceNote / removePresenceNote emit on flowSet.notes so reactive observers (NestsFeedLoaded) still update. - HomeLiveFilter.shouldIncludeChannel: scan presenceNotes separately for follow-broadcast detection in audio rooms (chat scan unchanged). - HomeLiveFilter.followsThatParticipateOn: also count presenceNotes authors so audio-room hosts/speakers factor into the participation sort even when they haven't chatted. - ChannelFeedFilter: delete the isChatEvent workaround that was excluding presence from the chat feed -- presence no longer lands there.
This commit is contained in:
@@ -1549,28 +1549,24 @@ object LocalCache : ILocalCache, ICacheProvider {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Audio-room presence (kind-10312) — addressable storage AND
|
* Audio-room presence (kind-10312) — addressable storage plus an
|
||||||
* attach the version note to the room's [LiveActivitiesChannel].
|
* author-keyed entry in the room's
|
||||||
* The home live-bubble surfaces a room when a follow is
|
* [LiveActivitiesChannel.presenceNotes] index.
|
||||||
* 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).
|
|
||||||
*
|
*
|
||||||
* Also indexed under [LiveActivitiesChannel.presenceNotes] keyed
|
* Presence is intentionally NOT added to `channel.notes`: that
|
||||||
* by author so the Nests feed can answer "are there speakers on
|
* map is dominated by chat in active rooms and feeds that care
|
||||||
* stage?" without scanning the chat-dominated `notes` map.
|
* 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,
|
* Cross-room move handling: kind-10312 is replaceable per author,
|
||||||
* but the room a presence points to (`a`-tag) can change when a
|
* but the room a presence points to (`a`-tag) can change when a
|
||||||
* speaker hops between rooms. The replaceable cache only swaps the
|
* speaker hops between rooms. The replaceable cache only swaps
|
||||||
* addressable's content — it has no notion of which channel the
|
* the addressable's content — it has no notion of which channel
|
||||||
* old version was attached to. Without explicit eviction the old
|
* the old version was attached to. Without explicit eviction the
|
||||||
* room would keep surfacing as "live" via the stale entry until it
|
* old room would keep surfacing as "live" via the stale entry
|
||||||
* dropped out of the freshness window. Capture the prior room
|
* until it dropped out of the freshness window. Capture the prior
|
||||||
* before replacement and, when it differs, drop the author from
|
* room before replacement and, when it differs, drop the author
|
||||||
* the old channel's presence index and the old version note from
|
* from the old channel's presence index.
|
||||||
* its main `notes` index.
|
|
||||||
*/
|
*/
|
||||||
fun consume(
|
fun consume(
|
||||||
event: MeetingRoomPresenceEvent,
|
event: MeetingRoomPresenceEvent,
|
||||||
@@ -1583,24 +1579,17 @@ object LocalCache : ILocalCache, ICacheProvider {
|
|||||||
|
|
||||||
val new = consumeBaseReplaceable(event, relay, wasVerified)
|
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
|
val roomAddress = event.interactiveRoom()?.address ?: return new
|
||||||
if (roomAddress.kind != MeetingSpaceEvent.KIND) return new
|
if (roomAddress.kind != MeetingSpaceEvent.KIND) return new
|
||||||
|
|
||||||
if (isReplacement && priorRoomAddress != null && priorRoomAddress != roomAddress) {
|
if (isReplacement && priorRoomAddress != null && priorRoomAddress != roomAddress) {
|
||||||
getLiveActivityChannelIfExists(priorRoomAddress)?.let { priorChannel ->
|
getLiveActivityChannelIfExists(priorRoomAddress)?.removePresenceNote(event.pubKey)
|
||||||
priorChannel.removePresenceNote(event.pubKey)
|
|
||||||
getNoteIfExists(priorVersion.id)?.let { priorChannel.removeNote(it) }
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
val channel = getOrCreateLiveChannel(roomAddress)
|
val channel = getOrCreateLiveChannel(roomAddress)
|
||||||
val versionNote = getOrCreateNote(event.id)
|
val versionNote = getOrCreateNote(event.id)
|
||||||
channel.addNote(versionNote, relay)
|
|
||||||
channel.addPresenceNote(versionNote)
|
channel.addPresenceNote(versionNote)
|
||||||
|
if (relay != null) channel.addRelay(relay)
|
||||||
|
|
||||||
return new
|
return new
|
||||||
}
|
}
|
||||||
|
|||||||
+2
-16
@@ -26,7 +26,6 @@ import com.vitorpamplona.amethyst.model.Note
|
|||||||
import com.vitorpamplona.amethyst.ui.dal.AdditiveFeedFilter
|
import com.vitorpamplona.amethyst.ui.dal.AdditiveFeedFilter
|
||||||
import com.vitorpamplona.amethyst.ui.dal.ChangesFlowFilter
|
import com.vitorpamplona.amethyst.ui.dal.ChangesFlowFilter
|
||||||
import com.vitorpamplona.amethyst.ui.dal.DefaultFeedOrder
|
import com.vitorpamplona.amethyst.ui.dal.DefaultFeedOrder
|
||||||
import com.vitorpamplona.quartz.nip53LiveActivities.presence.MeetingRoomPresenceEvent
|
|
||||||
|
|
||||||
class ChannelFeedFilter(
|
class ChannelFeedFilter(
|
||||||
val channel: Channel,
|
val channel: Channel,
|
||||||
@@ -38,25 +37,12 @@ class ChannelFeedFilter(
|
|||||||
override fun changesFlow() = channel.changesFlow()
|
override fun changesFlow() = channel.changesFlow()
|
||||||
|
|
||||||
// returns the last Note of each user.
|
// returns the last Note of each user.
|
||||||
override fun feed(): List<Note> = sort(channel.notes.filterIntoSet { _, it -> isChatEvent(it) && account.isAcceptable(it) })
|
override fun feed(): List<Note> = sort(channel.notes.filterIntoSet { _, it -> account.isAcceptable(it) })
|
||||||
|
|
||||||
override fun applyFilter(newItems: Set<Note>): Set<Note> =
|
override fun applyFilter(newItems: Set<Note>): Set<Note> =
|
||||||
newItems
|
newItems
|
||||||
.filter { channel.notes.containsKey(it.idHex) && isChatEvent(it) && account.isAcceptable(it) }
|
.filter { channel.notes.containsKey(it.idHex) && account.isAcceptable(it) }
|
||||||
.toSet()
|
.toSet()
|
||||||
|
|
||||||
override fun sort(items: Set<Note>): List<Note> = items.sortedWith(DefaultFeedOrder)
|
override fun sort(items: Set<Note>): List<Note> = 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
|
|
||||||
}
|
}
|
||||||
|
|||||||
+22
@@ -103,6 +103,16 @@ class HomeLiveFilter(
|
|||||||
return true
|
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
|
return channel.notes
|
||||||
.filter { _, value ->
|
.filter { _, value ->
|
||||||
acceptableChatEvent(value, filterParams, timeLimit)
|
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
|
return count
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+12
-3
@@ -49,15 +49,22 @@ class LiveActivitiesChannel(
|
|||||||
* unbounded the way `notes` does. Empty for streaming channels
|
* unbounded the way `notes` does. Empty for streaming channels
|
||||||
* (kind-30311) — only kind-30312 rooms publish presence.
|
* (kind-30311) — only kind-30312 rooms publish presence.
|
||||||
*
|
*
|
||||||
* Lets feeds answer "is anyone live on stage in this room?"
|
* Presence lives ONLY here, not in the base `notes` map: the
|
||||||
* without scanning the chat-dominated `notes` map. See
|
* mixed-kind `notes` is dominated by chat in active rooms and
|
||||||
* [com.vitorpamplona.amethyst.ui.screen.loggedIn.nests.dal.NestsFeedFilter].
|
* 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<HexKey, Note>()
|
val presenceNotes = LargeCache<HexKey, Note>()
|
||||||
|
|
||||||
fun addPresenceNote(note: Note) {
|
fun addPresenceNote(note: Note) {
|
||||||
val author = note.author?.pubkeyHex ?: return
|
val author = note.author?.pubkeyHex ?: return
|
||||||
|
val previous = presenceNotes.get(author)
|
||||||
|
if (previous?.idHex == note.idHex) return
|
||||||
presenceNotes.put(author, note)
|
presenceNotes.put(author, note)
|
||||||
|
flowSet?.notes?.invalidateData()
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -68,7 +75,9 @@ class LiveActivitiesChannel(
|
|||||||
* freshness window.
|
* freshness window.
|
||||||
*/
|
*/
|
||||||
fun removePresenceNote(author: HexKey) {
|
fun removePresenceNote(author: HexKey) {
|
||||||
|
if (!presenceNotes.containsKey(author)) return
|
||||||
presenceNotes.remove(author)
|
presenceNotes.remove(author)
|
||||||
|
flowSet?.notes?.invalidateData()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun address() = address
|
fun address() = address
|
||||||
|
|||||||
Reference in New Issue
Block a user