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.
5.8 KiB
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:
- On-stage — pubkeys in the kind-30312's
ptags with rolehost,admin, orspeaker, minus those whose latest presence has["onstage", "0"]. - Currently broadcasting — pubkeys with active moq-lite
announces visible on
MoqLiteSession.announce(prefix=room).updates. - 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 ofMoqLiteAnnounceupdates; need to expose it onMoqLiteNestsListenervia aliveBroadcasters: 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 onlyActivesuffixes (drop onEnded).participantGrid: StateFlow<ParticipantGrid>derived fromevent.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— exposediscoverPublishers(prefix: String): Flow<MoqLiteAnnounce>(thin wrapper that delegates to the underlyingMoqLiteSession.announce(prefix).updates).amethyst/.../audiorooms/room/ParticipantsGrid.kt(NEW) — ComposeLazyVerticalGridwith 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 currentonStage/audienceblocks withParticipantsGrid.
Risk / open
- Decide what to render when a
kind-30312ptag 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; auditAccountViewModel.ktfor the public surface.accountViewModel.zap(...)for zaps (next step).MutedUsersScreenpatterns for mute / unmute.
New
amethyst/.../audiorooms/room/ParticipantContextSheet.kt(NEW) —ModalBottomSheetwith 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
- View profile →
- Call site:
ParticipantsGridcellonLongClick(or tap, for parity with nostrnests web behaviour) opens the sheet keyed on thepubkey.
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/ZapBottomSheetcomposable and itsaccountViewModel.zapplumbing. - 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:
- From the context sheet (Step 2) — zap a single participant.
- 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 aNote(AddressableNoteover 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— hascreate(...).- Amethyst likely has a generic
ShareDialogfornaddr/neventshare — 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
ShareDialogwith the resultingnaddr1...+ a pre-fillednostr:naddr1...clipboard option + a "Share to Nostr" button that opens the existingShortNotePostScreenpre-loaded with the naddr URI.
- Build
Strings
audio_room_share_action, audio_room_share_text (template like
"Join {room name} {nostr:naddr1...}").
Suggested commit order
- participant-grid (Step 1) — the foundation; everything else hangs off it.
- context-sheet skeleton (Step 2 without role / kick rows; reuses Tier 1 Step 5 wiring once it's in).
- zap (Step 3) — plumbing-only.
- naddr share (Step 4) — pure UI.
Tiers 3 and 4 live in sibling docs.