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

171 lines
5.8 KiB
Markdown

# 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:
```kotlin
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 profile** → `nav.nav(Route.User(pubkey))`
- **Follow / Unfollow** → `accountViewModel.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.