feat(nests): hide rooms with no fresh speakers from drawer feed

Adds hasFreshSpeakers gate to NestsFeedFilter so OPEN/PRIVATE rooms
whose live speaker slate is empty are dropped — a room with no fresh
kind-10312 presence carrying onstage=1 has effectively ended even if
its kind-30312 status still says live.
This commit is contained in:
Claude
2026-04-29 22:39:59 +00:00
parent 4f45de9b86
commit 7c61eaf4e3
@@ -91,6 +91,7 @@ class NestsFeedFilter(
if (!hasMinimumNestFields(noteEvent)) return@filterTo false
if (!isWithinPlannedWindow(noteEvent, now)) return@filterTo false
if (!isLiveByPresence(noteEvent, presenceCutoff)) return@filterTo false
if (!hasFreshSpeakers(noteEvent, presenceCutoff)) return@filterTo false
if (filterParams.match(noteEvent, it.relays)) return@filterTo true
@@ -181,6 +182,37 @@ class NestsFeedFilter(
return fresh
}
/**
* Drop OPEN/PRIVATE rooms whose live speaker slate is empty. A room
* with no fresh kind-10312 presence carrying `onstage=1` has no one
* left on stage — even if the kind-30312 status still says `live`,
* there is nothing to listen to and the room has effectively ended.
*
* Same created-at grace as [isLiveByPresence] so brand-new rooms
* surface before the first speaker heartbeat arrives. CLOSED and
* PLANNED rooms bypass this gate for the same reasons listed on
* [isLiveByPresence].
*/
private fun hasFreshSpeakers(
event: MeetingSpaceEvent,
presenceCutoff: Long,
): Boolean {
val status = event.status()
if (status != StatusTag.STATUS.LIVE && status != StatusTag.STATUS.PRIVATE) return true
if (event.createdAt > presenceCutoff) return true
val channel = LocalCache.getLiveActivityChannelIfExists(event.address()) ?: return false
var hasSpeaker = false
channel.notes.forEach { _, note ->
if (hasSpeaker) return@forEach
val e = note.event
if (e is MeetingRoomPresenceEvent && e.createdAt > presenceCutoff && e.onstage() == true) {
hasSpeaker = true
}
}
return hasSpeaker
}
/**
* Author set used for p-tag follow expansion. Returns null for
* top filters where expansion is not meaningful (mute/block, the