fix(nests): emit canonical title tag and keep mute badge visible while muted

Two interrelated bugs from the deployed-schema flip:

1. Rooms created in Amethyst didn't appear in nostrnests's "Live Now"
   lobby. The lobby filter in NestsUI's skeleton.js requires a `title`
   tag on every kind:30312 — events without one are dropped from
   `live`/`planned`/`ended`. RoomNameTag still emitted the legacy
   `room` name, so our rooms passed our own readers (which tolerate
   both names) but were invisible to NestsUI. Flip canonical to
   `title`, keep `room` as legacy on read.

2. The mute badge over the avatar disappeared shortly after toggling
   mute, even though the broadcaster was still muted. Two causes:

   - ParticipantsGrid gated both the muted ring and the MicStateBadge
     on `member.publishing`. But per the deployed kind-10312 semantics
     `publishing=0` whenever `muted=1` (NestRoomPresencePublisher
     mirrors this — `publishingNow = broadcasting && !isMuted`), so
     the badge that was supposed to *indicate* mute was hidden
     exactly when it became relevant. Show the badge when on stage
     and either publishing OR muted; gate the muted ring on `muted`
     alone.

   - The presence heartbeat captured `micMutedTag` / `publishingTag`
     / `onstageTag` / `handRaised` at LaunchedEffect launch and never
     refreshed them. The 500 ms debounced LaunchedEffect did publish
     the new mute state, but ~30 s later the heartbeat overwrote it
     with the captured pre-toggle values, flipping `publishing`
     back to 1 and `muted` back to 0 server-side. Wrap the dynamic
     fields in `rememberUpdatedState` so the heartbeat reads the
     latest snapshot on every refresh; keep the existing key set
     (event/handRaised/onstage) for prompt re-emission on those
     transitions.

Tests updated for the new canonical `title` emission; both jvmTest
suites pass.
This commit is contained in:
Claude
2026-04-29 20:45:32 +00:00
parent aadb347b84
commit 0fafd34537
5 changed files with 52 additions and 15 deletions
@@ -23,17 +23,24 @@ package com.vitorpamplona.quartz.nip53LiveActivities.meetingSpaces.tags
import com.vitorpamplona.quartz.nip01Core.core.has
import com.vitorpamplona.quartz.utils.ensure
/**
* Room display name on a NIP-53 / nests `kind:30312` event. Matches
* the deployed nostrnests reference, which writes `["title", name]`
* AND filters its lobby on the `title` tag's presence — kind-30312
* events without a `title` are dropped from "Live Now". The early
* EGG-01 draft called this `room`; we accept that name on read for
* older events but always emit `title`.
*/
class RoomNameTag {
companion object Companion {
const val TAG_NAME = "room"
const val TAG_NAME = "title"
/**
* Legacy alias used by first-generation nostrnests web clients,
* which reused the NIP-53 streaming-event `title` tag for the
* kind-30312 room name. Accepted on read; we always emit the
* Earlier EGG-01 draft name for the room display name.
* Accepted on read for back-compat; we always emit the
* canonical [TAG_NAME].
*/
const val LEGACY_TAG_NAME = "title"
const val LEGACY_TAG_NAME = "room"
fun parse(tag: Array<String>): String? {
ensure(tag.has(1)) { return null }
@@ -52,7 +52,7 @@ class MeetingSpaceEventBuildTest {
val byName = template.tags.groupBy { it[0] }
assertEquals("main-hall", byName["d"]?.single()?.get(1))
assertEquals("Main Hall", byName["room"]?.single()?.get(1))
assertEquals("Main Hall", byName["title"]?.single()?.get(1))
assertEquals("live", byName["status"]?.single()?.get(1))
assertEquals("https://meet.example.com/hall", byName["auth"]?.single()?.get(1))
assertEquals("https://api.example.com/hall", byName["streaming"]?.single()?.get(1))