e71a2b26ef
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.
171 lines
5.8 KiB
Markdown
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.
|