feat(quartz): augment kind-10312 presence with publishing + onstage

Adds two NIP-53 presence tags used by nostrnests' room UI:

  ["publishing", "0|1"] — peer is actively pushing audio packets
  ["onstage",    "0|1"] — peer holds a speaker slot vs audience

These complement the existing "hand" and "muted" tags. They're
independent: a speaker can be onstage but paused (onstage=1,
publishing=0), or broadcasting silently (onstage=1, publishing=1,
muted=1). The participant grid (Tier 2) and listener counter (Tier 1
#8) consume them.

  - PublishingTag / OnstageTag classes mirror HandRaisedTag's parse +
    assemble shape exactly so the DSL (TagArrayBuilderExt) extends
    uniformly.
  - MeetingRoomPresenceEvent.publishing() / onstage() accessor parsers
    return Boolean? (null when the tag isn't present) so callers can
    distinguish "explicitly false" from "absent".
  - The MeetingSpaceEvent overload of build() now accepts both fields
    as nullable params; the MeetingRoomEvent overload deliberately
    doesn't (those are tier-2 video meetings, not Clubhouse rooms).
  - Round-trip tests pin the wire format and the "absent → null"
    behaviour.
This commit is contained in:
Claude
2026-04-26 21:32:59 +00:00
parent a2f4adda0c
commit 5a5eaa3b50
5 changed files with 161 additions and 0 deletions
@@ -49,6 +49,45 @@ class MeetingRoomPresenceEventTest {
assertEquals("${MeetingRoomPresenceEvent.KIND}:$participant:", event.addressTag())
}
@Test
fun parsesPublishingAndOnstageTags() {
val event =
MeetingRoomPresenceEvent(
id = "0".repeat(64),
pubKey = participant,
createdAt = 1_700_000_000L,
tags =
arrayOf(
arrayOf("a", "30312:${"b".repeat(64)}:room"),
arrayOf("hand", "0"),
arrayOf("muted", "1"),
arrayOf("publishing", "1"),
arrayOf("onstage", "1"),
),
content = "",
sig = "0".repeat(128),
)
assertEquals(false, event.handRaised())
assertEquals(true, event.muted())
assertEquals(true, event.publishing())
assertEquals(true, event.onstage())
}
@Test
fun missingPublishingAndOnstageTagsReturnNull() {
val event =
MeetingRoomPresenceEvent(
id = "0".repeat(64),
pubKey = participant,
createdAt = 1_700_000_000L,
tags = arrayOf(arrayOf("a", "30312:${"b".repeat(64)}:room")),
content = "",
sig = "0".repeat(128),
)
assertEquals(null, event.publishing())
assertEquals(null, event.onstage())
}
@Test
fun createAddressKeysByPubKeyOnly() {
val a = MeetingRoomPresenceEvent.createAddress(participant)