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).
The room name and the MoreVert overflow lived inline at the top of
the metadata Column, so they scrolled with the rest. Hoist them into
a Material 3 TopAppBar inside a Scaffold, matching every other major
screen in the app.
NestTopAppBar — new private composable:
- Title is `event.room()`, single-line ellipsis.
- Actions: Box wrapping the MoreVert IconButton + DropdownMenu
(Share for everyone, Edit gated on `isHost`).
- containerColor = Transparent so the themed background painted by
NestThemedScope's Surface (and any optional `bg` image) shows
through cleanly without a double-paint.
NestFullScreen body now:
Scaffold(
containerColor = Color.Transparent,
topBar = { NestTopAppBar(...) },
) { padding ->
Column(fillMaxSize.padding(padding)) {
Column(weight(1, false), verticalScroll, hpad+top pad) {
// summary, listener count, participants grid, host actions
// sheet, hand-raise queue, connection row, talk row, action
// row, reaction picker, host-leave confirm dialog
}
NestChatPanel(weight(1, true), hpad+bottom pad)
}
}
Side effects:
- Drops the manual windowInsetsPadding(safeDrawing) — Scaffold's
contentWindowInsets handles that and the TopAppBar's own
windowInsets handles the top edge.
- The EditNestSheet conditional moves to NestThemedScope's body
(after the Scaffold). It's a ModalBottomSheet, so position in
the tree doesn't change layout, but adjacency to the Scaffold
keeps the sheet/dialog boundary obvious.
https://claude.ai/code/session_01RDpuki4t8StSg1CZcXnV5b
The bottom-nav settings let users place any of these routes in the bar,
but the screens themselves never rendered AppBottomBar — so even when
configured as a tab, the bar was missing on Profile, Wallet, Lists,
BookmarkGroups, WebBookmarks, Drafts, InterestSets, EmojiPacks,
BrowseEmojiSets, and AllSettings.
AppBottomBar self-hides via nav.canPop(), so adding the bar is safe on
every entry: it appears only when the screen is the tab root, hidden
otherwise (drawer / deep link).
Same-tab tap scrolls the screen's main list to the top, mirroring the
existing tab behavior. State is hoisted as needed:
- WebBookmarks, Drafts, BrowseEmojiSets: feedState.sendToTop()
- Lists, BookmarkGroups, EmojiPacks: hoisted LazyListState/LazyGridState
through the feed-view helper, animateScrollTo(0)
- Wallet, InterestSets: hoisted LazyListState locally
- AllSettings: hoisted ScrollState (verticalScroll column)
- Profile: wrapped in Scaffold, hoisted ScrollState from RenderSurface
https://claude.ai/code/session_01PrirRcL7g8iX7vTqqLTkBS
Previously the room screen wrapped EVERYTHING — title, summary,
participants, talk row, action row, AND chat — in a single
verticalScroll Column. The chat panel sat at the bottom with a fixed
NEST_CHAT_PANEL_HEIGHT (420dp), which meant on tall phones the chat
left blank space below it and on small phones the chat was cramped.
Restructure NestFullScreen's body:
- Outer Column.fillMaxSize, no scroll, owns safeDrawing inset.
- Top metadata: Column with weight(1f, fill=false), internal
verticalScroll, horizontal+top padding. Caps at half the screen
so an over-tall participants list scrolls internally instead of
pushing the chat off-screen.
- NestChatPanel: Column-scoped weight(1f, fill=true), horizontal
padding + bottom inset. Always takes its full allocation —
chat dominates the screen, fixed-height fallback gone.
NestChatPanel is now a `ColumnScope.NestChatPanel` extension so its
modifier can use `weight()` from the caller. Inside the panel, the
message list Box also uses weight(1f, fill=true) of the panel's own
Column, so the LazyColumn fills everything except the composer.
NEST_CHAT_PANEL_HEIGHT constant removed — it was the source of the
fixed-height behavior the user explicitly didn't want.
https://claude.ai/code/session_01RDpuki4t8StSg1CZcXnV5b
Bottom-nav screens (Notifications, Discover, etc.) used to render the
bottom bar unconditionally. When reached as a non-tab-root entry —
drawer, deep link, in-app navigation that pushed a fresh copy with
different args — the bar should disappear, matching the back-arrow and
slide-animation rules.
AppBottomBar now takes an INav and returns early when nav.canPop() is
true. The check covers two valid cases for showing the bar:
- Tab-root entry (carries BOTTOM_NAV_ROOT_KEY, canPop is false).
- Graph start destination Home (no previous entry, canPop is false).
All 19 callers updated to pass nav; the existing click callback param
is renamed onClick to free up the nav name.
https://claude.ai/code/session_01PrirRcL7g8iX7vTqqLTkBS
NestThemedScope painted `themed.background` on a plain Box. Compose's
`LocalContentColor` was never updated, so it stayed at its
CompositionLocal default — `Color.Black` — for every Text inside.
On the user's dark theme that meant black-on-black for the room
title and the chat panel: unreadable.
AmethystTheme installs `MaterialTheme(colorScheme = ...)` but does
NOT wrap content in a Surface; screens that use Scaffold inherit
LocalContentColor via Scaffold's internal Surface. NestFullScreen
goes Column → verticalScroll, no Scaffold, no Surface, so the
black-default fell through.
Replace the bare `Box.background(themed.background)` with a Material
Surface that paints `color = themed.background` AND provides
`contentColor = themed.onBackground` to LocalContentColor for the
inner subtree. The `bg` image still overlays the Surface inside the
Box, exactly as before.
This was the actual cause of the user's "title impossible to read /
chat super dark" report — the event in question shipped no `c` tags
(only the unparseable `["color", "gradient-7"]`), so RoomTheme was
already Empty. The previous "all-or-nothing palette" gate (commit
15169de7) is still right for half-applied palettes; this Surface
fix is what makes empty / un-themed rooms read correctly on dark.
https://claude.ai/code/session_01RDpuki4t8StSg1CZcXnV5b
EGG-10 lets each `["c", hex, role]` tag stand alone, but a half-
applied palette (themed background + platform text, or themed text
+ platform background) collides with whichever system theme — light
or dark — the user is on. A room that ships ONLY `background=#FFE4B5`
ends up with platform-default white text on dark theme; unreadable.
Gate all three color overrides on having a complete bg + text pair
inside `RoomTheme.from(event)`. When either is missing the whole
palette drops to null and renderers fall through to the platform
theme. Background image (`bg`) and font tags still flow through —
they don't break contrast on their own.
Both consumers — NestThemedScope (room screen) and NestJoinCard
(lobby) — pick up the change without code edits because they read
through the same `RoomTheme` projection.
The nostrnests-style `["color", "gradient-7"]` tag was never parsed
(our `c`-tag parser ignores it), so events shipping only that don't
override anything either way; this fix targets the genuine half-
applied case (rooms that emit one EGG-10 c-tag without the matching
companion).
https://claude.ai/code/session_01RDpuki4t8StSg1CZcXnV5b
Both flags were stamped at the same call site (navBottomBar) and meant
the same thing — "this entry is a bottom-nav tab root". Collapse them
into a single key and use isBottomNavRoot() in composableFromEnd's
transition lambdas.
https://claude.ai/code/session_01PrirRcL7g8iX7vTqqLTkBS
Previous attempt cleared Home from the back stack on every bottom-nav
tap, which broke back-swipe: the user expects swipe-back from any tab
to return to Home, and swipe-back from Home to leave the app.
Restore that contract while still hiding the back arrow on tab roots:
- navBottomBar pops up to (but not including) Route.Home, so sibling
tabs are cleared while Home stays at the bottom of the stack.
- The new entry is stamped with BOTTOM_NAV_ROOT_KEY on its
savedStateHandle.
- Nav.canPop returns false when the current entry carries that flag,
even though previousBackStackEntry (Home) is non-null.
Home itself is the graph's start destination and has no previous entry,
so canPop returns false there without needing the flag.
https://claude.ai/code/session_01PrirRcL7g8iX7vTqqLTkBS