Adds cli/tests/nests/nests-interop.sh — a 47-test operator-driven
interop script that walks a tester through every audio-room edge case
between Amethyst Android and the nostrnests.com NestsUI-v2 web client.
Coverage: bidirectional host/listener flows, audio round-trip on
moq-lite Lite-03, hand-raise + role promotion/demotion, mute, kind:7
reactions (including NIP-30 custom emoji + 30 s overlay clear), kind:1311
in-room chat (text + emoji + link + image upload + history backfill),
kind:4312 kick, room edit + close, scheduled rooms (status=planned),
multi-speaker, background audio + PIP, network drop reconnect, 10-min
JWT refresh, custom moq servers (kind:10112), naddr deep links, and
edge cases (long titles, empty rooms, leave-stage, dedupe, race).
The script follows the marmot/marmot-interop.sh pattern: shared
logging + result helpers, color-coded prompts (yellow=Amethyst,
magenta=web, cyan=optional 3rd identity), p/f/s confirms recorded to a
TSV, and a final summary table. Skip / only / keep-state flags are
supported for iteration.
Pure manual harness — amy does not yet ship `amy nests <verb>` so
nothing is automatable from the CLI side.
Mirror the listener's ReconnectingNestsListener for the publish side.
moq-auth issues 600 s bearer tokens; without proactive refresh, a user
holding the stage past 10 minutes silently drops when the relay tears
down the WebTransport session and stays off the air until they manually
re-tap Talk. The new wrapper recycles the session at 540 s so the relay
never sees an expired token, and re-issues publishing onto each fresh
session with the user's mute intent replayed on the new handle.
VM swap is a one-line change to DefaultNestsSpeakerConnector. The
caller-owned BroadcastHandle is now the wrapper's stable handle that
survives every refresh.
Six unit tests cover happy path, refresh-without-failure-state, mute
replay across recycle, close idempotence, first-attempt-failure
exception propagation, and post-close startBroadcasting guard.
https://claude.ai/code/session_01HXf3zG3F2ev2ASeQju7Y5S
Drops STAGE_CELL_MIN and AUDIENCE_CELL_MIN from 112.dp to 96.dp, which
lets a Pixel-class 411.dp phone fit 4 cells per row instead of 3 (and
unlocks 5/6 columns earlier on tablets). The 100.dp avatars overflow
~2.dp into the 6.dp horizontal arrangement on each side, so neighbors
still show a ~2.dp visible gap. Names ellipsize to the cell width as
before.
100dp avatars across stage and audience tabs, names centered under each
cell, and the avatar border now distinguishes actively speaking (green)
from publishing-but-muted (red) at a glance — the existing mic-pill
keeps its semantics underneath.
UsernameDisplay gains an optional textAlign so the grid can opt in to
centered labels without disturbing other call sites.
The activity class lives in `…nests.room.activity.NestActivity` but the
manifest entry was missing the `.activity` segment, so tapping a Nests
feed card crashed with ActivityNotFoundException.
AppBottomBar hides itself on canPop entries by emitting nothing into the
bar slot. DisappearingScaffold previously read the slot's measured
height as 0 and skipped applying navigationBarsPadding (its
`bottomBar == null` branch only fires when the lambda itself is null),
which let the FAB and feed content slide under the system navigation
bar. Fall back to the system-nav-bar inset when the slot measures zero
so the FAB lines up with the bar-visible position and content stays
clear of the gesture/3-button nav.
https://claude.ai/code/session_01QSk7CnjNtbcgS3XBD5WEZy
Drop the dedicated NestThumbStatusFilterAssembler and have the
thumb's liveness gate piggyback on NestRoomFilterAssembler, which
already subscribes to kinds=[1311, 10312, 7] + admin (4312) per
room a-tag. observeRoomLatestPresence is now a private composable
in NestsFeedLoaded.kt that opens NestRoomFilterAssemblerSubscription
and reads the latest cached MeetingRoomPresenceEvent createdAt off
the room's LiveActivitiesChannel.
Trade-off: the thumb pays for the room's full chat + reaction
history, not just presence. Bounded by LazyColumn — only currently-
composed thumbs hold subscriptions — and the cache it warms is
exactly what NestActivity needs when the user opens the room. If
wire load becomes a concern, swap to a limit:1 lite mode on the
existing assembler.
Net: -159 / +51 lines, one less filter assembler in the coordinator.
Extends the canPop FAB-position fix to every screen that hosts a
floating action button: Pictures, Videos, Articles, Longs, Shorts,
Polls, Products, Communities, Nests, Badges, Discover, WebBookmarks,
Messages, Home, GeoHash, Hashtag, Relay, Community, OldBookmarks,
EmojiPack, ChessLobby, MarmotGroups, the bookmark management screens
and the migration FAB. Adds a small FabBottomBarPadded helper so call
sites that use named FAB composables stay tidy.
https://claude.ai/code/session_01QSk7CnjNtbcgS3XBD5WEZy
The kind-30312 status tag goes stale silently when a host crashes
without publishing a CLOSED replacement, so checkStatus()'s 8-hour
fallback was the only liveness signal — abandoned rooms still
showed LiveFlag for up to 8h after the audio stopped.
Adds a per-thumb relay probe: kinds=[10312], #a=[roomATag], limit=1.
The relay returns the single most-recent presence event among all
speakers in the room, then keeps streaming so fresh heartbeats land
in real time. Speakers re-emit kind-10312 every ~60 s while
publishing, so a presence newer than 180 s means at least one
speaker is still in the room; older flips the badge to EndedFlag
even though the 30312 event still says OPEN.
UX: optimistic on first paint (LiveFlag while we have no presence
data), self-corrects within one round-trip once the relay replies.
Files:
- NestThumbStatusFilterAssembler.kt: per-room limit:1 REQ +
observeNestRoomLatestPresence composable that scans the
LiveActivitiesChannel's notes for max(createdAt) of cached
presence events.
- RelaySubscriptionsCoordinator.kt: register nestThumbStatus.
- NestsFeedLoaded.kt: thread card.id into a private
RenderLiveOrEndedFromPresence helper that gates the OPEN badge
on freshness.
AppBottomBar returns nothing on canPop entries (drawer pushes, in-app
navigations), so the floating action button drops by the bar's height
when navigating into a screen and rises again on a tab root. Reserve the
50dp gap at the FAB site via a fabBottomBarPadding(nav) modifier so the
button stays at a consistent vertical position regardless of the bar's
visibility on EmojiPacks, BookmarkGroups, and InterestSets.
https://claude.ai/code/session_01QSk7CnjNtbcgS3XBD5WEZy
NestsFeedLoaded.RenderLiveSpacesThumb gates LiveFlag/EndedFlag on
the kind-30312 status tag verbatim, so a host that crashed without
publishing a CLOSED replacement leaves the room flagged "live"
indefinitely. checkStatus() applies the same 8-hour age-out the
event's own helper uses, so abandoned OPEN rooms surface as
EndedFlag instead.
Also drop a stray println debug ("AABBCC …") in the same map block.
Mirrors the Profile Edit banner upload: a SelectSingleFromGallery
leadingIcon on the Cover Image field picks media, strips metadata,
compresses, and uploads to the user's configured Blossom (or NIP-96)
server. The returned URL fills the field so the existing publish path
tags it on the kind-30312 MeetingSpaceEvent.
The previous refactor renamed EditNestViewModelTest and
RoomParticipantActionsTest into edit/ and participants/ subdirs but
the package declarations stayed at the old flat path, so the tests
no longer compiled against the relocated production code.
The nests/room/ package had grown to 23+ files in one flat directory,
making it hard to find related composables and lifecycle helpers. Each
file is now grouped by responsibility:
activity/ Android Activity entry + AccountViewModel bridge
lobby/ Pre-join lobby card (NestJoinCard, JoinNestButton)
lifecycle/ Side-effect composables wiring VM <-> system / cache
screen/ Top-level layouts (FullScreen, PipScreen, ActionBar)
stage/ Speaker / audience grids and overlays
participants/ Per-participant host actions + role mutations
reactions/ Reaction picker
chat/ Chat panel and composer (also absorbs the old send/)
edit/ Edit-room sheet + ViewModel
theme/ Themed scope + URL font loader
Also drops the unused StagePeopleRow composable from NestCommon and
inlines its remaining one-liner connectingLabel helper into
NestActionBar (the only caller), removing NestCommon entirely.
External imports in 7 sibling files were updated to point at the new
subpackage paths.
Resolves NestFullScreen.kt by keeping the redesigned layout (header
strip, stage grid, tabs, sticky NestActionBar) while dropping the
NestThemedScope wrapper that main removed in ff00dcf3 ("screens get
too dark"). The RoomTheme import goes with it; the stale doc-comment
reference to NestThemedScope is updated accordingly.
Replaces the single scrolling Column (title → speakers row → audience
row → host queue → connection chip → talk row → buttons → chat) with
a Scaffold-bounded layout that scales to large rooms:
- Header strip with LIVE chip + listener count (uses the existing
nest_listener_count plural for the first time) and a 1-line
ellipsised summary. Topbar stays slim.
- StageGrid: vertical adaptive LazyVerticalGrid capped at 220dp,
so a 30-speaker room scrolls inside the strip without pushing
the chat below the fold.
- Tab bar (Chat / Audience · N / Hands · N) with badge counts.
Hands tab is host-only and only shown while the queue is non-empty.
- AudienceGrid: vertical adaptive LazyVerticalGrid that fills the
selected tab — handles 1,000+ listeners without performance issues
(the old single-row LazyHorizontalGrid only let you see ~5 at a time).
- NestActionBar: sticky bottom bar consolidating the old ConnectionRow,
TalkRow, and hand/react/leave row. Layout adapts to connection /
broadcast / on-stage state; failure messages get a thin status
strip above the bar instead of stealing button slots.
- Hand-raise control hides when the user is on stage (the action it
represents — "I want to speak" — no longer applies).
The PIP screen and chat panel internals are untouched.
The back button and the more-options menu used to float as
absolute-positioned overlays inside ProfileHeader. They are now
hosted in a transparent ShorterTopAppBar wired into the Scaffold's
topBar slot, so the banner can still render edge-to-edge as the
background by setting contentWindowInsets to zero and only applying
the bottom padding from Scaffold to the body.
Completes the copy approach started in 57ebded6:
- NestEditFieldRow.kt: copy of EditFieldRow, drops the BackHandler
(the activity owns back; draft auto-save runs on every keystroke
so there is nothing to flush) and points every callback at
NestNewMessageViewModel.
- NestFileUploadDialog.kt: copy of ChannelFileUploadDialog, header
shows the room host's avatar + room name from the loaded
MeetingSpaceEvent.
- NestChatPanel.kt: replaces the slim inline composer with
NestEditFieldRow + NestNewMessageViewModel. Chat message list is
still rendered from NestViewModel.chat — the new VM is scoped to
*editing and sending* messages only, per the brief.
- NestNewMessageViewModel.sendPostSync: switched from
signAndSendPrivately(template, emptySet()) to
signAndComputeBroadcast(template) so messages actually reach the
user's default relays (matches the original slim composer's
behaviour).
Pivot from sharing ChannelNewMessageViewModel (previous commit) to a
literal per-nest copy as requested.
State of this commit (incomplete — DO NOT MERGE):
- EditFieldRow.kt: reverted to original (no interceptBackPress flag)
- NestNewMessageViewModel.kt: new file, copy of ChannelNewMessageViewModel
narrowed to MeetingSpaceEvent + LiveActivitiesChatMessageEvent only
Still TODO before this branch is usable:
- Copy EditFieldRow into nests/room/send/NestEditFieldRow.kt
- Copy ChannelFileUploadDialog into nests/room/send/NestFileUploadDialog.kt
- Rewire NestChatPanel to use these copies (it currently still calls
ChannelNewMessageViewModel + EditFieldRow from the previous commit's
approach, which won't compile against the reverted EditFieldRow
signature once NestChatPanel is also reverted)
User asked to stop mid-work — committing as WIP so the tree is clean.
Wire the in-room chat composer to ChannelNewMessageViewModel +
EditFieldRow so nest chat picks up @-mention picker, file/image/
video upload, reply preview, NIP-37 draft auto-save, emoji
suggestions, content warnings, expiration, geohash, and zap-
split forwarding — the same affordances every other public chat
surface has.
The composer is backed by a LiveActivitiesChannel keyed by the
meeting space's address. ChannelNewMessageViewModel.createTemplate()
already handles the LiveActivitiesChannel branch: when channel.info
is null (which it always is for kind-30312, since info is typed to
the kind-30311 LiveActivitiesEvent), it falls through to
LiveActivitiesChatMessageEvent.message(post, channel.toATag()) —
the exact event shape the previous slim composer was emitting.
EditFieldRow gains an interceptBackPress flag so nest can opt out
of the channel-screen back-handler that would clear the field and
call nav.popBack() (a no-op on BouncingIntentNav).