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 (fb47a4c → 71cf99d → 015b0d7); "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).
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
/authJWT mint, moq-lite Lite-03 listener + speaker, kind 30312 with title/summary/image/status/service/endpoint + single host participant, kind 10312 presence withhand+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:
- Parse
adminandspeakerroles on incoming events - Add a host UI "edit room → set role" path that re-publishes 30312
- 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
- MeetingSpace
ptags (with their role) - moq-lite announcements (active publishers, surfaced via the listener session's announce flow)
- 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.
13. Share room (naddr deep link) — ~0.5 day, user-visible
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= thenamespacefrom the request bodyget: [""](read-anything-under-root, for subscribers)put: [<pubkey>](publish only your own sub-namespace, forpublish: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 / ends — not 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.
Recommended punchlist order
- Listener-side presence aggregation + listener counter (#8, #10)
- Live chat panel (#1)
- Reactions (#2)
- Participant grid + per-avatar context menu + zap (#9, #11, #12)
- Role parsing + hand-raise queue + promote / demote / kick (#3, #4, #5)
- Edit room / close room (#6) + scheduled rooms (#7)
- Share via naddr (#13)
- moq-auth token refresh sanity check (#16) +
Connection.Reloadbackoff confirmation (#17) - 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