Files
amethyst/nestsClient/plans/2026-04-26-nostrnests-integration-audit.md
T
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

233 lines
11 KiB
Markdown

# 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`.
### 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` = 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 / `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
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`