fix nests room author shown in audience with no audio
nostrnests publishes kind-30312 without a self-`p`-tag for the room author — they're the implicit host. The lobby card already handles this (NestJoinCard) but the in-room code did not: the author fell into the pure-audience branch with role=null AND was missing from the audio subscription set, so the host appeared as a regular listener and no sound played. buildParticipantGrid now takes hostPubkey and synthesizes a virtual `["p", host, "", "host"]` when absent, so the existing presence-aware onstage/audience rules apply uniformly — including "host leaves stage" (onstage=0 → drops to audience, role stays HOST). NestActivityContent owns the grid now and derives onStageKeys from it, so the StageGrid render and the MoQ subscription set stay in lockstep when anyone (host or speaker) steps off stage.
This commit is contained in:
+21
-2
@@ -21,6 +21,8 @@
|
||||
package com.vitorpamplona.amethyst.commons.viewmodels
|
||||
|
||||
import androidx.compose.runtime.Immutable
|
||||
import com.vitorpamplona.quartz.nip01Core.core.HexKey
|
||||
import com.vitorpamplona.quartz.nip53LiveActivities.streaming.tags.ParticipantTag
|
||||
import com.vitorpamplona.quartz.nip53LiveActivities.streaming.tags.ROLE
|
||||
|
||||
/**
|
||||
@@ -70,16 +72,33 @@ data class ParticipantGrid(
|
||||
* * Audience: every pubkey with recent presence that isn't on
|
||||
* stage, plus participant-tagged users who haven't emitted
|
||||
* presence yet (rendered as `absent = true`).
|
||||
*
|
||||
* [hostPubkey] is the kind-30312 event signer. Per the EGG-07
|
||||
* convention (and nostrnests' production behaviour), the room
|
||||
* author is the implicit host even when they don't self-tag.
|
||||
* When the author isn't already in [participants], a synthetic
|
||||
* `["p", hostPubkey, "", "host"]` tag is prepended so the
|
||||
* presence-aware on-stage/audience rules apply uniformly —
|
||||
* including the "host stepped off stage" case (presence with
|
||||
* `onstage=0` → drops to audience, role stays HOST).
|
||||
*/
|
||||
fun buildParticipantGrid(
|
||||
participants: List<com.vitorpamplona.quartz.nip53LiveActivities.streaming.tags.ParticipantTag>,
|
||||
participants: List<ParticipantTag>,
|
||||
presences: Map<String, RoomPresence>,
|
||||
hostPubkey: HexKey? = null,
|
||||
): ParticipantGrid {
|
||||
val effectiveParticipants =
|
||||
if (hostPubkey != null && participants.none { it.pubKey == hostPubkey }) {
|
||||
listOf(ParticipantTag(hostPubkey, null, ROLE.HOST.code, null)) + participants
|
||||
} else {
|
||||
participants
|
||||
}
|
||||
|
||||
val onStage = mutableListOf<RoomMember>()
|
||||
val audience = mutableListOf<RoomMember>()
|
||||
|
||||
val seen = mutableSetOf<String>()
|
||||
for (p in participants) {
|
||||
for (p in effectiveParticipants) {
|
||||
seen += p.pubKey
|
||||
val pres = presences[p.pubKey]
|
||||
val role = p.effectiveRole()
|
||||
|
||||
+66
@@ -115,4 +115,70 @@ class ParticipantGridTest {
|
||||
assertEquals(0, grid.onStage.size)
|
||||
assertEquals(0, grid.audience.size)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun implicitHostNotInPTagsLandsOnStage() {
|
||||
// nostrnests publishes kind-30312 with no self-`p`-tag for
|
||||
// the room author — the author is the implicit host. Without
|
||||
// synthesizing them, they fell through into the pure-audience
|
||||
// branch (role=null) AND were left out of the audio
|
||||
// subscription set, producing "host shown in audience + no
|
||||
// sound" — the exact regression this case exists to catch.
|
||||
val grid =
|
||||
buildParticipantGrid(
|
||||
participants = listOf(pTag(speaker, ROLE.SPEAKER)),
|
||||
presences = emptyMap(),
|
||||
hostPubkey = host,
|
||||
)
|
||||
val hostRow = grid.onStage.firstOrNull { it.pubkey == host }
|
||||
assertEquals(ROLE.HOST, hostRow?.role)
|
||||
// No presence yet — host shows up greyed-out, matching how
|
||||
// p-tagged speakers without presence already render.
|
||||
assertEquals(true, hostRow?.absent)
|
||||
assertTrue(grid.audience.none { it.pubkey == host })
|
||||
}
|
||||
|
||||
@Test
|
||||
fun implicitHostWithOnstageTrueStaysOnStage() {
|
||||
val grid =
|
||||
buildParticipantGrid(
|
||||
participants = listOf(pTag(speaker, ROLE.SPEAKER)),
|
||||
presences = mapOf(host to presence(host, 100L, onstage = true)),
|
||||
hostPubkey = host,
|
||||
)
|
||||
val hostRow = grid.onStage.firstOrNull { it.pubkey == host }
|
||||
assertEquals(ROLE.HOST, hostRow?.role)
|
||||
assertEquals(false, hostRow?.absent)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun implicitHostLeavesStageDropsToAudienceKeepingHostRole() {
|
||||
// The host taps "Leave Stage" on nostrnests → kind-10312 with
|
||||
// onstage=0. They should drop to audience but the host crown
|
||||
// stays — exact same behaviour as an explicit speaker stepping
|
||||
// off the stage.
|
||||
val grid =
|
||||
buildParticipantGrid(
|
||||
participants = listOf(pTag(speaker, ROLE.SPEAKER)),
|
||||
presences = mapOf(host to presence(host, 100L, onstage = false)),
|
||||
hostPubkey = host,
|
||||
)
|
||||
assertTrue(grid.onStage.none { it.pubkey == host })
|
||||
val hostRow = grid.audience.firstOrNull { it.pubkey == host }
|
||||
assertEquals(ROLE.HOST, hostRow?.role)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun explicitHostInPTagsIsNotDuplicated() {
|
||||
// The author already tagged themselves with role=host. The
|
||||
// synthesized entry must NOT shadow / duplicate them.
|
||||
val grid =
|
||||
buildParticipantGrid(
|
||||
participants = listOf(pTag(host, ROLE.HOST), pTag(speaker, ROLE.SPEAKER)),
|
||||
presences = mapOf(host to presence(host, 100L)),
|
||||
hostPubkey = host,
|
||||
)
|
||||
assertEquals(2, grid.onStage.size)
|
||||
assertEquals(1, grid.onStage.count { it.pubkey == host })
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user