Files
amethyst/nestsClient/plans/2026-04-26-tier2-coding-plan.md
T
Claude e71a2b26ef docs(audio-rooms): coding plans for Tier 1-4, one file per tier
The earlier integration audit identified the gaps; this is the
how-to-build-them. Split across four files plus an index so each
review / commit stays small:

  - 2026-04-26-tier-plans-index.md — top-level pointer + sequence
    dependencies + what's deliberately out of scope.

  - 2026-04-26-tier1-coding-plan.md — listener counter, presence
    aggregation, augmented kind-10312 tags (publishing/onstage),
    live chat (kind 1311), reactions (kind 7 / 9735), edit + close
    + scheduled rooms, role parsing + promote/demote, hand-raise
    queue, kick (kind 4312). Concrete file-level wiring for each
    step + suggested commit order (six independent PRs).

  - 2026-04-26-tier2-coding-plan.md — participant grid,
    per-avatar context menu (follow / mute / zap / promote / kick),
    zap entry points (room + speaker), share-via-naddr.

  - 2026-04-26-tier3-coding-plan.md — room theming PARSER ONLY
    (graceful fallback for themed rooms; full theming behind a
    later phase), background-audio + wake-lock audit checklist.

  - 2026-04-26-tier4-coding-plan.md — moq-auth token re-mint on
    long sessions and moq-lite Connection.Reload-equivalent
    reconnect with backoff. Step 1 is subsumed by Step 2 once the
    reconnect path is in.

No code changes — pure docs. Each plan names exact file paths,
new types, reused helpers, strings, tests, and call-out risks so
an implementer (or follow-up agent) can pick up Step N without
re-deriving the surrounding context.
2026-04-26 19:52:07 +00:00

5.8 KiB

Tier 2 — coding plan (participant grid, per-avatar context menu, zap, naddr share)

Concrete file-level plan for shipping Tier 2 of 2026-04-26-nostrnests-integration-audit.md. Assumes Tier 1 has landed (presence aggregation, role parsing, edit-room flow).

Step 1 — Participant grid (#9)

The flagship UI piece. Renders three groups:

  1. On-stage — pubkeys in the kind-30312's p tags with role host, admin, or speaker, minus those whose latest presence has ["onstage", "0"].
  2. Currently broadcasting — pubkeys with active moq-lite announces visible on MoqLiteSession.announce(prefix=room).updates.
  3. Audience — every pubkey from kind-10312 presence in the last 5 min that isn't on stage.

Reuse

  • commons/.../viewmodels/AudioRoomViewModel.presences (Tier 1 Step 1) — gives us {pubkey → flags + lastSeen}.
  • MoqLiteSession.announce(prefix) — already returns a flow of MoqLiteAnnounce updates; need to expose it on MoqLiteNestsListener via a liveBroadcasters: StateFlow<Set<String>>.

New

  • commons/.../viewmodels/AudioRoomViewModel.kt:
    • liveBroadcasters: StateFlow<Set<String>> populated by an announce-please subscription against the room's namespace prefix. Tracks only Active suffixes (drop on Ended).
    • participantGrid: StateFlow<ParticipantGrid> derived from event.participants() + presences + liveBroadcasters.
  • commons/.../viewmodels/ParticipantGrid.kt (NEW) — pure data:
    data class ParticipantGrid(
        val onStage: List<RoomMember>,
        val audience: List<RoomMember>,
    )
    data class RoomMember(
        val pubkey: String,
        val role: ROLE?,                  // null for plain listeners
        val handRaised: Boolean,
        val muted: Boolean?,
        val publishing: Boolean,
        val lastSeenSeconds: Int?,
    )
    
  • nestsClient/.../MoqLiteNestsListener.kt — expose discoverPublishers(prefix: String): Flow<MoqLiteAnnounce> (thin wrapper that delegates to the underlying MoqLiteSession.announce(prefix).updates).
  • amethyst/.../audiorooms/room/ParticipantsGrid.kt (NEW) — Compose LazyVerticalGrid with section headers ("On stage", "Audience"). Each cell is the avatar + small status icons (mic on/off, raised hand, broadcasting indicator).
  • amethyst/.../audiorooms/room/AudioRoomFullScreen.kt — replace the current onStage / audience blocks with ParticipantsGrid.

Risk / open

  • Decide what to render when a kind-30312 p tag has no presence (member never joined): show greyed-out? hide entirely? nostrnests greys out — match for parity.

Step 2 — Per-participant context menu (#11)

A bottom sheet that opens on avatar tap.

Reuse

  • accountViewModel.toggleFollow(user) and similar helpers exist; audit AccountViewModel.kt for the public surface.
  • accountViewModel.zap(...) for zaps (next step).
  • MutedUsersScreen patterns for mute / unmute.

New

  • amethyst/.../audiorooms/room/ParticipantContextSheet.kt (NEW) — ModalBottomSheet with rows:
    • View profilenav.nav(Route.User(pubkey))
    • Follow / UnfollowaccountViewModel.toggleFollow
    • Mute → mute-list edit
    • Zap → opens the existing zap dialog (Step 3 below)
    • Promote to speaker / Demote → only when local user is host/admin (Tier 1 Step 5 surface)
    • Kick → only when local user is host/admin
  • Call site: ParticipantsGrid cell onLongClick (or tap, for parity with nostrnests web behaviour) opens the sheet keyed on the pubkey.

Strings

audio_room_participant_view_profile, audio_room_participant_follow, audio_room_participant_unfollow, audio_room_participant_mute, audio_room_participant_zap, (role + kick strings already added in Tier 1).


Step 3 — Zap support inside the room (#12)

Mostly a matter of plumbing the existing zap dialog.

Reuse

  • Amethyst's existing zap flow — find the ZapDialog / ZapBottomSheet composable and its accountViewModel.zap plumbing.
  • Reactions sub from Tier 1 Step 3 — already pulls kinds=[7,9735], #a=[roomATag], so a paid zap appears in the reaction overlay automatically.

New

  • Two zap entry points:
    1. From the context sheet (Step 2) — zap a single participant.
    2. From a "Zap room" button in AudioRoomFullScreen.kt's overflow — zap the kind-30312 host pubkey, tag the room address.
  • A small RoomZapAdapter.kt (NEW) that builds the zap request pointing at either the room (NIP-57 ["a", roomATag]) or a speaker (NIP-57 ["p", pubkey]).

Risk

  • The existing zap dialog likely takes a Note. The room IS a Note (AddressableNote over the kind-30312); confirm the dialog accepts it. If not, add an overload accepting (authorPubkey, optionalRoomATag).

Step 4 — Share via naddr (#13)

Reuse

  • quartz/.../nip19Bech32/NAddress.kt — has create(...).
  • Amethyst likely has a generic ShareDialog for naddr / nevent share — find it, reuse.

New

  • amethyst/.../audiorooms/room/AudioRoomFullScreen.kt — overflow menu "Share room":
    • Build NAddress.create(KIND, hostPubkey, dTag, relays) from the room event.
    • Open ShareDialog with the resulting naddr1... + a pre-filled nostr:naddr1... clipboard option + a "Share to Nostr" button that opens the existing ShortNotePostScreen pre-loaded with the naddr URI.

Strings

audio_room_share_action, audio_room_share_text (template like "Join {room name} {nostr:naddr1...}").


Suggested commit order

  1. participant-grid (Step 1) — the foundation; everything else hangs off it.
  2. context-sheet skeleton (Step 2 without role / kick rows; reuses Tier 1 Step 5 wiring once it's in).
  3. zap (Step 3) — plumbing-only.
  4. naddr share (Step 4) — pure UI.

Tiers 3 and 4 live in sibling docs.