Files
amethyst/nestsClient/plans/2026-04-26-nostrnests-integration-audit.md
Claude 8b5af5d496 docs(audio-rooms): refresh against shipped state + nostrnests gap audit
Existing plan docs were written before the moq-lite swap, the
create-space + kind-10112 work, and the harness / submodule findings.
This refresh aligns them with what's actually live on the branch and
captures the work still ahead.

  - 2026-04-26-audio-rooms-completion.md — flipped to a STATUS-FIRST
    layout: implementation table for every protocol/transport/UI
    surface, "pending" table for the remaining items (reconnect,
    level meters, Desktop / iOS, Nests parity), pointers section
    refreshed.
  - 2026-04-26-moq-lite-gap.md — marked DONE with the commit range
    that landed it (fb47a4c71cf99d015b0d7); "When picking up"
    section now points at the shipped surface first, raw protocol
    references second.
  - 2026-04-22-nip-audio-rooms-draft.md — major surgery to match
    today's nostrnests reality:
      * status banner up top calling out the revision
      * dependencies dropped IETF MoQ-transport, added moq-lite
        Lite-03 + ALPN "moq-lite-03"
      * HTTP control plane: GET <service>/<room-d-tag> → POST /auth
        with {namespace, publish}, returning {token}; documented the
        JWT claim shape (root, get, put), 600 s lifetime, regex
        on `namespace`, JWKS endpoint, error matrix
      * Audio transport: replaced IETF SETUP / TrackNamespace tuples
        / OBJECT_DATAGRAM with moq-lite Lite-03 (ControlType varint,
        per-bidi message types, group uni streams, audio/data track,
        no in-band SETUP, FIN-as-unsubscribe semantics)
      * New event-kind sections: kind 4312 (admin command / kick),
        kind 10112 (audio-room server list)
      * Reconciliation section explaining what changed from the
        original draft and why
  - NEW: 2026-04-26-nostrnests-integration-audit.md — punchlist of
    every nostrnests/NestsUI feature we don't yet ship, sourced from
    a code-walk of the React app + moq-auth + API.md (which is
    LiveKit-era and dead). Tier 1 (low-effort, visible): chat,
    reactions, role parsing + promotion, hand-raise queue, kick
    (kind 4312), edit/close room, scheduled rooms, listener counter.
    Tier 2: participant grid, augmented presence tags
    (publishing/onstage), per-avatar context menu + zap, share via
    naddr. Tier 3: room theming. Tier 4: token-refresh +
    Connection.Reload sanity checks.

Verified `:nestsClient:jvmTest` + `:amethyst:compilePlayDebugKotlin`
both still green after the doc changes (no code touched).
2026-04-26 19:23:41 +00:00

11 KiB

Nostrnests integration audit (2026-04-26)

What nostrnests/NestsUI-v2 does that Amethyst doesn't yet — sourced from a code-walk of the React app, the moq-auth sidecar, and the moq-lite reference. Use this as the punchlist for the next chunks of audio-room work.

Already shipped (don't re-add to the gap list): HTTP /auth JWT mint, moq-lite Lite-03 listener + speaker, kind 30312 with title/summary/image/status/service/endpoint + single host participant, kind 10312 presence with hand + muted + final "leaving" emit, kind 10112 host-server list (Settings UI), foreground service, PIP, mute, "Start space" sheet that publishes the kind-30312 with the user as host.

What API.md is NOT

nostrnests/API.md documents a LiveKit-era HTTP surface (PUT /api/v1/nests, /permissions, /recording*, /info, /guest). That whole file is dead — the moq-lite refactor dropped every endpoint except POST /auth and GET /.well-known/jwks.json. There is no recording surface, no permissions endpoint, no /info, no /guest. Don't waste implementation time on those.

Tier 1 — Visible, low-effort (ship first)

1. Live chat (kind 1311) — ~1 day, user-visible

NestsUI publishes {kind:1311, content, tags:[["a", roomATag]]} and subscribes to {kinds:[1311], "#a":[roomATag]} for the in-room chat panel. Quartz already has LiveActivitiesChatMessageEvent at quartz/.../nip53LiveActivities/chat/LiveActivitiesChatMessageEvent.kt:132 — just not wired into AudioRoomFullScreen. Need a chat pane + RoomChatViewModel reading LocalCache filtered by #a and a AudioRoomsSubAssembler subscription.

Source: NestsUI-v2/src/components/WriteMessage.tsx:31-37, hooks/useChatMessages.ts:31-66.

2. Reactions (kind 7 + custom emoji) — ~1 day, user-visible

Emoji reactions tagged ["a", roomATag]; NIP-30 ["emoji", shortcode, url] for custom. Floating overlay shows last 30 s of reactions on the avatar. ReactionEvent already in Quartz at quartz/.../nip25Reactions/ReactionEvent.kt:73. Need a small picker + per-avatar overlay.

Source: components/ReactionsButton.tsx:31-53, hooks/useRoomReactions.ts:14-22 (queries kinds 7 + 9735), components/ReactionOverlay.tsx, RoomContextProvider.tsx:105 (30-second visibility window).

3. Speaker / admin promotion via p-tag role markers — 2 days, user-visible

Role lives in the p tag's 4th element: ["p", pubkey, "<relay>", "host"|"admin"|"speaker"]. Promotion = host/admin re-publishes the kind-30312 with the target's role updated; demotion / "remove from stage" = drop the role.

Amethyst today only emits a single host. Need to:

  1. Parse admin and speaker roles on incoming events
  2. Add a host UI "edit room → set role" path that re-publishes 30312
  3. Gate "publish microphone" on having host|admin|speaker (a plain listener should NOT auto-publish unless promoted)

Source: lib/const.ts:27-31 (ParticipantRole), lib/room.ts:66-74 (parser), components/ProfileCard.tsx:106-119 (updateRoomParticipant), hooks/useIsAdmin.ts:25-31.

4. Hand-raise queue surface for hosts — ~0.5 day after #3, user-visible

There is no separate "approval" event. Host inspects the kind-10312 presence list, sees ["hand","1"], and uses the same role re-publish flow above to promote. Just a "raised hands" section in the host's participant grid. Source: hooks/useRoomPresence.ts:14-39, ParticipantsGrid.tsx:69.

5. Kick (kind 4312 admin command) — ~1 day, user-visible

Ephemeral event:

{ kind: 4312, content: "",
  tags: [["a", roomATag], ["p", target], ["action", "kick"]] }

Targets watch for it; if signed by host/admin in last 60 s, they self-disconnect. Host also drops the target's p-tag from the 30312.

Need a new Quartz AdminCommandEvent (kind 4312, currently absent), client subscription, and auto-disconnect handler.

Source: lib/const.ts:18 (ADMIN_COMMAND = 4312), components/ProfileCard.tsx:121-134, hooks/useAdminCommands.ts:38-74.

6. Edit room / close room — ~0.5 day, user-visible

Host re-publishes the same d-tag 30312 with new title/summary/image, or with ["status","ended"] to close. Amethyst's CreateAudioRoomSheet covers create; need a host-only "edit" sheet for an existing room.

Source: components/EditRoomDialog.tsx:125-213. Theme tags (c, f, bg) can be ignored for now (see #14).

7. Scheduled / planned rooms — 2-4 hours, user-visible

["status","planned"] + ["starts","<unix>"]. StatusTag.STATUS and starts already exist in Quartz; the create-sheet just doesn't expose a date/time picker yet.

Source: pages/NewRoom.tsx:50-58.

8. Listener counter ("N listening") — ~2 hours, user-visible

Subscribe to {kinds:[10312], "#a":[roomATag], since: now-300}, dedupe by pubkey, render presenceList.length. Currently Amethyst emits its own presence but doesn't read others'. Unblocks #4 and #9.

Source: pages/RoomPage.tsx:67-68, hooks/useRoomPresence.ts:14-37.

Tier 2 — Visible, medium-effort

9. Participant grid (speakers vs listeners) — 2-3 days, user-visible

Render every pubkey from

  1. MeetingSpace p tags (with their role)
  2. moq-lite announcements (active publishers, surfaced via the listener session's announce flow)
  3. Recent kind-10312 presence

Speakers = host|admin|speaker minus presences with ["onstage","0"].

Source: components/ParticipantsGrid.tsx:75-101. Note new presence tags Amethyst doesn't emit yet — ["publishing","0|1"], ["onstage","0|1"] — see #10.

10. Augment kind-10312 with publishing + onstage tags — ~1 hour, infrastructure

usePresence.ts:33-41 emits

["a", roomATag], ["hand","0|1"], ["publishing","0|1"], ["muted","0|1"], ["onstage","0|1"]

"Leave the stage" UX hinges on onstage=0. Extend the existing 10312 emitter in AudioRoomActivityContent.publishPresence.

11. Per-participant context menu — 1 day, user-visible

Tap an avatar → View profile, Follow (kind 3), Mute (kind 10000), Zap. All exist elsewhere in Amethyst — assemble in a room-scoped sheet.

Source: components/ProfileCard.tsx:177-258.

12. Zap support inside the room — ~0.5 day, user-visible

Standard NIP-57 zap of the room event itself or a specific speaker. Reactions hook also pulls kind:9735 so paid zaps appear in the reaction stream. Reuse Amethyst's existing zap UI.

Source: components/ZapDialog.tsx, hooks/useZaps.ts.

ShareDialog builds an naddr from the room event and offers "share to Nostr" (publishes a kind 1 referencing the naddr) plus clipboard. Quartz already has NAddress.create.

Source: components/ShareDialog.tsx:45, lib/room.ts:77-83 (buildRoomNaddr).

Tier 3 — Larger / lower priority

14. Room theming (tags c, f, bg + kinds 36767 / 16767) — 3-5 days

Inline color triplet ["c", hex, "background|text|primary"], font ["f", family, url], background ["bg", "url <u>", "mode <tile|cover>"]. Optional a-tag pointing to a kind-36767 Ditto theme; per-user kind-16767 profile theme.

Effort range: full theming is 3-5 days; just parsing the tags so themed rooms don't render badly is ~0.5 day. Compose Multiplatform can pull dynamic colors but font loading is platform-specific.

Source: lib/const.ts:21-24, lib/ditto-theme.ts, components/ThemeChooser.tsx, pages/NewRoom.tsx:69-88.

15. Background audio + wake-lock audit — ~2 hours

NestsUI uses useWakeLock, useAudioKeepAlive, useBackgroundAudio. Amethyst already has AudioRoomForegroundService + PIP; just confirm PARTIAL_WAKE_LOCK is acquired during a broadcast. Probably already covered.

Tier 4 — Infrastructure (mostly already correct)

16. moq-auth token lifetime — ~2 hours

/auth JWT lives 600 s (10 min). No refresh endpoint — re-mint on expiry. Confirm Amethyst re-mints before the token's exp on long sessions.

JWT claims emitted by the sidecar:

  • root = the namespace from the request body
  • get: [""] (read-anything-under-root, for subscribers)
  • put: [<pubkey>] (publish only your own sub-namespace, for publish:true)
  • iat / exp

Source: moq-auth/src/index.ts:160-166.

17. moq-lite features unused (mostly intentional)

Feature NestsUI Amethyst Verdict
Multi-track / video not used (audio only) not used
Fetch / replay not used not used
Per-track priority / bitrate probes not used not used
Connection.Reload auto-reconnect (1s→2s→…→30s backoff) yes MAYBE GAP confirm MoqLiteSession reconnects with backoff on transport failure; if not, add ~0.5 day.
WebSocket transport fallback yes (browser) no (WebTransport only over QUIC) not a gap on Android
Announcement-driven participant discovery yes needs to expose announce flow on MoqLiteNestsListener small wire-up; covered by #9

Source: transport/moq-transport.ts:145-151, 257-265, 386-422.

18. NIP-71 / hashtags / spotlight / endsnot a gap

None of these are emitted by NestsUI on kind 30312. The only tags it writes: d, title, summary, status, starts, color, image, streaming, auth, relays, p, plus theme tags. No NIP-71 streaming tags, no hashtags, no spotlight, no ends. Don't chase parity here.

  1. Listener-side presence aggregation + listener counter (#8, #10)
  2. Live chat panel (#1)
  3. Reactions (#2)
  4. Participant grid + per-avatar context menu + zap (#9, #11, #12)
  5. Role parsing + hand-raise queue + promote / demote / kick (#3, #4, #5)
  6. Edit room / close room (#6) + scheduled rooms (#7)
  7. Share via naddr (#13)
  8. moq-auth token refresh sanity check (#16) + Connection.Reload backoff confirmation (#17)
  9. Room theme parsing — graceful fallback only (#14)

Items 1-3 give you "feels like a real audio room" inside ~3 days. Items 4-5 unlock the full host workflow inside another ~1 week. Everything else is polish.

Key files for the implementer

  • Listener: nestsClient/src/commonMain/kotlin/com/vitorpamplona/nestsclient/MoqLiteNestsListener.kt
  • MeetingSpace event: quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip53LiveActivities/meetingSpaces/MeetingSpaceEvent.kt
  • Live-chat event (kind 1311): quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip53LiveActivities/chat/LiveActivitiesChatMessageEvent.kt
  • Full-screen room UI: amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/audiorooms/room/AudioRoomFullScreen.kt
  • Subscriptions for the rooms feed: amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/audiorooms/datasource/AudioRoomsSubAssembler.kt