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
A NIP-53 kind-30313 [MeetingRoomEvent] references its parent kind-30312
[MeetingSpaceEvent] via the standard `["a", "30312:<host>:<d>"]` tag.
Without surfacing it, a meeting card in a feed lost the context of
which Nest the meeting belonged to — users couldn't tell if "Office
Hours Q3" lived inside their company nest or someone else's.
Render a small "In <Space Name>" label above the meeting's title
inside [RenderMeetingRoomEventInner]:
- Resolves the parent address via `MeetingRoomEvent.interactiveRoom()`
→ `MeetingSpaceTag.address`. Bails when the address points at
something other than kind:30312 (no broken links).
- Loads the parent through the existing `LoadAddressableNote`
helper. When the kind:30312 hasn't propagated to LocalCache yet,
the row falls back to a generic "In a Nest" label and tapping
routes through the standard `routeFor` thread path which
auto-fetches.
- Tap navigates to the parent space's thread view via `routeFor`
+ `nav.nav` — the same path Discovery / NoteCompose use for
quote-into-thread navigation.
Two new strings:
- meeting_room_in_space ("In %1$s")
- meeting_room_in_space_unknown ("a Nest" — fallback when the
parent space isn't yet resolvable)
https://claude.ai/code/session_01RDpuki4t8StSg1CZcXnV5b
User changed direction — the Nests / Meeting Rooms separation was
the wrong call. Kind 30313 stays out of the Nests list, but doesn't
get its own drawer entry either; it surfaces through the standard
NoteCompose / thread paths instead.
Reverted:
- Deleted: meetingrooms/MeetingRoomsScreen, MeetingRoomsTopBar,
MeetingRoomsFeedLoaded, dal/MeetingRoomsFeedFilter.
- Removed Route.MeetingRooms, NavBarItem.MEETING_ROOMS, the
catalog entry, the drawer entry.
- Removed meetingRoomsFeed from AccountFeedContentStates and
ScrollStateKeys.MEETING_ROOMS_SCREEN.
- Removed `meeting_rooms` string.
- Updated AppNavigation imports.
Kept:
- NestsFeedFilter narrowed to kind:30312 only (per user: "only
show kind 30312 in the items of the NestScreen"). The dual-kind
branches stay gone.
- Doc comment updated to note that kind:30313 renders on the
standard NoteCompose / thread paths outside the Nests drawer.
https://claude.ai/code/session_01RDpuki4t8StSg1CZcXnV5b
Discovery's RenderLiveActivityThumb cards already render a creator
+ name + like + zap row at the bottom — but only when the channel
holds a kind:30311 LiveActivitiesEvent. Nests (kind:30312) and
Meeting Rooms (kind:30313) populate channels with the same address
shape but `LiveActivitiesChannel.info` is null for them, so the
reactions row was hidden.
ShortLiveActivityChannelHeader:
- Add an `addressableNote: Note?` parameter to the inner overload.
The outer (LiveActivitiesChannel) overload fills it from
`LocalCache.getAddressableNoteIfExists(channel.address)`, which
works for any kind that registers an addressable note.
- Move the like/zap row OUT of the `liveActivitiesEvent?.let { }`
gate so it renders for any addressable note — kind:30311,
30312, 30313 all share it now.
- Keep the LIVE / OFFLINE flag inside the LiveActivitiesEvent gate
(it pings the streaming URL — stream-specific). Extracted into
a small `LiveChannelLiveFlag` helper so the reaction row can
render independently.
MeetingRoomsFeedLoaded:
- Swap from RenderMeetingRoomEvent to RenderLiveActivityThumb so
the Meeting Rooms list visually matches the Nests list and
Discovery — same cover-image card with status flag, participant
gallery, and bottom header (creator + name + reactions).
Other call sites (LiveActivityTopBar, LiveActivitiesChannelHeader)
use the outer `baseChannel` overload and pick up the new behavior
automatically.
https://claude.ai/code/session_01RDpuki4t8StSg1CZcXnV5b
popUpTo(route) { inclusive = true } only pops if `route` is already in
the stack. From Home, tapping any other bottom-nav tab left Home in the
back stack, so canPop() returned true and the back arrow appeared on a
root tab.
Pop up to the graph's start destination instead (also inclusive). This
clears Home — and any drawer/deep-push entries above it — before
navigating to the new bottom-nav root, so each bottom-nav tap leaves
exactly one entry in the stack.
https://claude.ai/code/session_01PrirRcL7g8iX7vTqqLTkBS
Nests (kind:30312) and Meetings (kind:30313) were sharing one drawer
entry / one feed; the only thing they have in common is being NIP-53
events. They have different lifecycles (audio rooms vs scheduled
meetings) and different status enums (open/private/closed/planned vs
live/planned/ended). Mixing them under the Nests label was confusing.
Split:
- MeetingRoomsFeedFilter (new) — narrows to MeetingRoomEvent (30313),
with title-must-be-non-blank gate (mirrors the EGG-01-style minimum-
fields rule the Nests feed applies). Sort by status (LIVE > PLANNED
> ENDED), then participants among follows, then `starts` / created.
- NestsFeedFilter (existing) — narrowed to MeetingSpaceEvent (30312)
only. Drops the dual-kind branches in `innerApplyFilter` and the
parallel sub-room enum in `convertStatusToOrder`. Same EGG-01 gate
(room/status/service/endpoint).
UI:
- New MeetingRoomsScreen / MeetingRoomsFeedLoaded / MeetingRoomsTopBar.
Read-only — the app doesn't compose kind-30313 events locally; tap
routes a card to the standard thread view via `routeFor`.
- Reuses the existing in-feed renderer
(`note.types.RenderMeetingRoomEvent`) for visual consistency
with the Note thread surface.
- Mounts NestsFilterAssemblerSubscription so the wire side stays a
single REQ regardless of which screen the user opens first
(`makeLiveActivitiesFilter` already covers 30311/30312/30313/1311
in one filter).
Wiring:
- Route.MeetingRooms → MeetingRoomsScreen in AppNavigation.
- NavBarItem.MEETING_ROOMS in the catalog (Groups icon, label
"Meeting Rooms"). Same isDebug gate as NESTS while the surface is
still in development.
- meetingRoomsFeed in AccountFeedContentStates (mirroring nestsFeed
for updateFeedWith / deleteFromFeed propagation).
- ScrollStateKeys.MEETING_ROOMS_SCREEN.
- New string `meeting_rooms` ("Meeting Rooms").
https://claude.ai/code/session_01RDpuki4t8StSg1CZcXnV5b
The 280-line `NestActivityBody` orchestrated VM creation, four event
collectors, two eviction tickers, kick handling, two PIP bridges, the
foreground-service lifecycle, the kind-10312 heartbeat / debounce /
final-leave publisher, AND the screen routing. Each concern was clear
on its own but lost in the wall of LaunchedEffects.
Split into three sibling files, each named for its concern. The body
shrinks to ~70 lines of named composable wiring calls — the room's
lifecycle is now legible end-to-end at a glance.
NestRoomLifecycle.kt — VM + activity bridges:
rememberNestViewModel(room, signer)
AutoConnectAndTrackSpeakers(viewModel, onStageKeys)
LeaveOnKick(viewModel, onLeave)
PipBridge(ui, pipMuteSignal, viewModel, onMuteState, onConnectedChange)
NestForegroundServiceLifecycle(ui)
NestRoomEventCollectors.kt — read-side LocalCache → VM:
NestRoomEventCollectors(viewModel, event, roomATag, localPubkey)
internally:
PresenceCollector + PresenceEvictionTicker
ChatCollector
ReactionsCollector + ReactionsEvictionTicker
AdminCommandsCollector (with the host/moderator authority gate)
NestRoomPresencePublisher.kt — write-side kind-10312:
NestPresencePublisher(account, event, ui, handRaised)
internally: heartbeat (30 s) + mute-debounce (500 ms) + onDispose
final leave on a non-cancellable scope so it survives mid-network
scope cancellation.
publishPresence helper relocates here too.
NestActivityContent.kt — now 170 lines:
- NestActivityContent (entry, parses address)
- NestActivityBody (orchestration only, ~70 lines)
- publishPresence helper moved out
- dropped the half-dozen now-unused imports
Behavior identical: every LaunchedEffect / DisposableEffect ran
before the split runs after, with the same keys, same dispatchers,
same scopes. No new coroutines, no removed.
https://claude.ai/code/session_01RDpuki4t8StSg1CZcXnV5b
The room screen used to fan out four parallel relay subscriptions
through separate ComposeSubscriptionManagers — RoomChat, RoomPresence,
RoomReactions, RoomAdminCommands — each opening its own REQ per outbox
relay. They all keyed on the same dimensions (roomATag, account) and
ran in the same place (NestActivityContent), so the multi-sub
fan-out was wire waste.
Replace with a single NestRoomFilterAssembler that issues two Filters
inside one REQ per relay:
- {kinds: [1311, 10312, 7], "#a": [roomATag]}
→ chat, presence, reactions on the shared a-tag gate
- {kinds: [4312], "#a": [roomATag], "#p": [localPubkey]}
→ admin commands; #p restriction stays on a separate Filter so
it doesn't over-restrict the chat / presence / reaction kinds
Net wire change: 4 REQs per relay → 1 REQ per relay with 2 Filters,
same event coverage. Same kinds, same #a/#p semantics, same outbox
relay set.
Files removed:
RoomChatFilterAssembler.kt
RoomPresenceFilterAssembler.kt
RoomPresenceFilterAssemblerSubscription.kt
RoomReactionsFilterAssembler.kt
RoomAdminCommandsFilterAssembler.kt
Files updated:
NestActivityContent.kt — one NestRoomFilterAssemblerSubscription
call replaces four. The per-kind LocalCache.observeEvents
LaunchedEffects stay; only the wire side got collapsed.
RelaySubscriptionsCoordinator.kt — register `nestRoom` instead of
the four old per-room assemblers.
NestsFilterAssembler (the rooms-list level subscription) is unchanged
— different keying (per-user, per-follow-list), different lifecycle.
https://claude.ai/code/session_01RDpuki4t8StSg1CZcXnV5b
The room content was sliding under the status bar at the top and the
gesture / nav bar at the bottom because the outer Column only had
fillMaxSize + 16dp content padding. With Amethyst running edge-to-edge
the title cropped behind the status bar and the chat composer / Leave
button sat under the system nav bar.
Wrap the content Column in `windowInsetsPadding(WindowInsets.safeDrawing)`
INSIDE NestThemedScope but BEFORE the existing 16dp padding, so:
- The themed background color + optional `bg` image still paint
edge-to-edge (NestThemedScope's outer Box stays full-bleed).
- The room body — title, participants, chat, talk row, Leave —
is inset within the safe-drawing area.
PIP screen unchanged; the system manages PIP layout itself.
https://claude.ai/code/session_01RDpuki4t8StSg1CZcXnV5b
Two-part fix for kind-10312 [MeetingRoomPresenceEvent] showing up as
empty cards in the channel chat panel.
(1) Filter at the chat-feed level. ChannelFeedFilter now rejects
MeetingRoomPresenceEvent — these only land in `channel.notes` so the
home live-bubble (HomeLiveFilter) can detect a follow broadcasting
in a Nest by walking channel.notes. They aren't chat content; the
chat panel rendering them as empty rows was a leak from that
home-bubble plumbing.
The filter is in ChannelFeedFilter rather than upstream in
LocalCache.consume because removing presence from channel.notes
would silently break the home-bubble's broadcasting-now detection.
This way the channel keeps the events for non-chat consumers and
the chat just refuses to surface them.
(2) Renderer fallback. NoteCompose now dispatches kind-10312 to
RenderMeetingRoomPresence — a one-line italic status row ("@user ·
raised their hand" / "stepped on stage" / "is speaking" / "joined
as audience" / "left the nest"). After (1) this only shows up on
non-chat surfaces (search results, thread view of a quoted
presence, profile timeline, …), but renders informatively instead
of as the empty card it was before.
Five new strings under nest_presence_*.
https://claude.ai/code/session_01RDpuki4t8StSg1CZcXnV5b
NestsFeedFilter rendered any kind:30312 the relay handed us, including
shells with only a `d` and `a` tag (no room name, no status, no
service / endpoint). The lobby card then drew an empty title row with
a Join button that would 410 unknown_room — confusing UX.
Add a feed-filter gate: a kind:30312 must carry all four of EGG-01
rule 2's required tags before it shows up:
- room (display name)
- status (any recognized value, including the "live" / "ended"
legacy aliases nostrnests-web emits)
- service (auth sidecar URL)
- endpoint (MoQ relay URL)
Closed rooms that DO have all four still render — they may carry a
`recording` tag (EGG-11) and the listen-back affordance is the only
path to that audio post-close.
Sub-rooms (kind:30313 MeetingRoomEvent) get a parallel gate — must
carry a non-blank title.
https://claude.ai/code/session_01RDpuki4t8StSg1CZcXnV5b
NestThemedScope was overriding MaterialTheme.colorScheme but the Box
holding the room body never explicitly painted the themed background
color — only consumers that read MaterialTheme.colorScheme.background
themselves picked up the change. A room that shipped
`["c", hex, "background"]` (EGG-10) but no `bg` image therefore
fell back to the platform surface and never looked themed.
Add `.background(themed.background)` to the Box. The optional `bg`
image still paints on top (per EGG-10 rule 3 — image overlays the
color); when no image is present, the themed color tints the whole
screen, matching the lobby card's containerColor pattern from the
last commit.
PIP screen left as-is: it's a small system-managed floating window
where the platform surface color is the right call.
https://claude.ai/code/session_01RDpuki4t8StSg1CZcXnV5b
Replaces the title-and-summary stub with a proper lobby card.
Render order top-to-bottom:
1. Cover image (16:9, ContentScale.Crop) when the kind:30312 ships
an `image` tag. Skipped silently when absent.
2. Title + status flag row — reuses the existing
MeetingSpaceOpenFlag / MeetingSpacePrivateFlag /
MeetingSpaceClosedFlag / MeetingSpacePlannedFlag composables, so
LIVE/PRIVATE/CLOSED/PLANNED badges visually match the in-feed
RenderMeetingSpaceEvent surface.
3. Summary, capped at 3 lines with ellipsis.
4. Host row: 36dp avatar + UsernameDisplay + "Host" label. Avatar is
tappable → profile route via the `nav` parameter.
5. Speaker row: up to 5 24dp avatars from the kind:30312 `p`-tags
(role=speaker | admin), with a "+N" overflow label. Each avatar
navigates to its profile.
6. "Join nest" button bottom-right.
Theming applies the room's color tags (EGG-10):
- `["c", hex, "background"]` → Card containerColor
- `["c", hex, "text"]` → all body text colors
- `["c", hex, "primary"]` → JoinNestButton containerColor (passed
via new optional `primaryColorOverride` parameter so the in-feed
JoinNestButton call from RenderMeetingSpaceEvent stays unchanged)
Background image (`bg` tag) is intentionally NOT applied at the lobby
card level — it's reserved for the in-room screen via NestThemedScope
(which does need the full-bleed shader-based tile mode). Painting it
on a card would compete with the cover.
API change: NestJoinCard now takes `nav: INav`. The single call site
in ChannelView already has nav in scope.
New string: nest_lobby_host_label = "Host".
https://claude.ai/code/session_01RDpuki4t8StSg1CZcXnV5b
Bottom-nav taps clear the stack with popUpTo(route) { inclusive = true },
so a back arrow on those screens has nothing to pop. Switch the back-arrow
visibility to a runtime check on INav.canPop() — true when the destination
has a previous back-stack entry (drawer or any deep push), false when it's
the root (bottom-nav entry).
Applied across:
- TopBarWithBackButton: now takes nav: INav and renders the icon only
when nav.canPop()
- UserDrawerSearchTopBar: shows back arrow when canPop, drawer opener
otherwise (covers Home, Messages, Video, Discover, Notifications,
Communities, Articles, Pictures, Shorts, PublicChats, FollowPacks,
LiveStreams, AudioRooms, Longs, Polls, Badges, Products,
BrowseEmojiSets)
- WebBookmarks, Drafts, Wallet: wrap custom navigationIcon in canPop
- ProfileHeader: floating back arrow on the banner when canPop
Brings ParticipantsGrid and TalkRow closer to the nostrnests web UI
without restructuring the screen.
ParticipantsGrid:
- HandRaiseBadge: yellow circle with PanTool glyph at the top-right
of every member with handRaised=true. Subtle vertical bounce loop
via rememberInfiniteTransition (mirrors nostrnests'
animate-bounce). Shows in BOTH on-stage and audience sections —
the audience hand-up is the queue.
- MicStateBadge: bottom-center pill on on-stage speakers who are
publishing. Color encodes:
green — speaking now (in speakingNow)
red — publishing but muted (presence.muted=1)
primary — publishing, mic open, currently silent
Skipped for audience (showMicBadge=false on the audience section)
and for on-stage members who aren't publishing.
TalkRow:
- "Leave stage" OutlinedButton added in both Idle and Broadcasting
states. Drops the user off-stage via NestViewModel.setOnStage(false)
(presence emits onstage="0" → ParticipantGrid drops them to the
audience section). The Broadcasting variant also calls
stopBroadcast() so the audio session ends with the role change.
- "Stop talking" stays for users who want to pause audio without
leaving the stage.
New string: nest_leave_stage = "Leave stage".
Colors are hardcoded (yellow-500 / green-500) to match nostrnests
exactly — they read consistently in dark and light modes against the
participant tile background. Theming hook is a follow-up.
https://claude.ai/code/session_01RDpuki4t8StSg1CZcXnV5b
The previous fix sorted + joined the user pubkeys into a comma-separated
String to keep the key Bundle-storable. That brought back the
StringBuilder + char[] + String + sorted-List allocation per visible
chatroom that the typed-key rework had eliminated.
ChatroomKey.users is often a kotlinx PersistentOrderedSet, which isn't
java.io.Serializable, so we can't just hold the Set reference as-is.
Copying into a HashSet costs one HashMap allocation per key, much less
than the joined-String path, and Set equality stays order-independent so
two equivalent chatrooms still hash to the same bucket.
LazyColumn keys are persisted in a SaveableStateHolder, which on Android
must be Bundle-storable (primitives, Parcelable, or Serializable). The
recent perf rework wrapped keys in plain data classes, which Kotlin does
not auto-mark Serializable, so opening a public chat crashed with:
java.lang.IllegalArgumentException: Type of the key
PublicChannelLazyKey(channelId=...) is not supported.
Mark the sealed interface Serializable, and decompose RoomId/ChatroomKey
fields (which live in quartz commonMain and can't depend on the JVM-only
Serializable interface) into String primitives. Each variant still wraps
a stable per-chatroom identity, so reorders move rows instead of
recreating them.
Replaces the v2 ChannelFeedViewModel/ChannelNewMessageViewModel detour
(commit bdb82f0) with a chat panel that:
- Reads messages from `NestViewModel.chat` directly (single source of
truth for the room — same VM that holds presence, reactions, and
hand-raise queue),
- Renders each message via `ChatroomMessageCompose` — the exact same
per-row composable that LiveStream chat uses in
`ChatFeedLoaded` — so visual rendering (avatars, names,
timestamps, NIP-21 mentions, embedded media, link previews,
reactions count) matches the rest of Amethyst's chat surfaces,
- Uses a slim composer reusing `ThinPaddingTextField` +
`ThinSendButton` (the same components inside `EditFieldRow`),
- LazyColumn with `reverseLayout=true` and the message list reversed,
so newest is at the bottom and auto-scroll works naturally.
Sends route through `accountViewModel.account.signAndComputeBroadcast`
of `LiveActivitiesChatMessageEvent.message(text, roomATag)` — same wire
path the room subscription is already listening on.
Composer is intentionally slim for v1: text-only. Drafts, @-mention
picker, file attachments, and reply preview are pinned to
ChannelNewMessageViewModel and out of scope here. Adding any of them is
a follow-up that can either (a) port the relevant primitives onto
NestViewModel or (b) widen NestViewModel to expose those fields.
New: `BouncingIntentNav` — Activity-context INav that translates the
small set of nav requests a chat row generates (Profile / Note /
Hashtag / EventRedirect / LiveActivityChannel / Community /
PublicChatChannel) into `nostr:` URIs and bounces them as ACTION_VIEW
Intents at MainActivity. AppNavigation already routes intent.data
through `uriToRoute`, so destinations land in the main app's NavHost
without receiver-side changes. NestActivity stays running in its own
task while the user explores; back from MainActivity returns to the
room with audio uninterrupted (foreground service).
Routes that don't have a `nostr:` mapping (settings, drafts, private
chatrooms, etc.) are no-ops in the bouncing nav — those aren't
reachable from a chat-row tap anyway.
https://claude.ai/code/session_01RDpuki4t8StSg1CZcXnV5b
The hand-rolled NestChatPanel — bare LazyColumn of avatar + name +
plain-text rows + an OutlinedTextField composer — is replaced by the
same `RefreshingChatroomFeedView` + `EditFieldRow` stack that
`LiveActivityChannelView` uses for kind:30311 streaming chats.
The kind-30312 address already registers a `LiveActivitiesChannel`
inside `LocalCache.liveChatChannels` (via
`LocalCache.consume(LiveActivitiesChatMessageEvent)`), so handing the
channel to `ChannelFeedViewModel.Factory` is enough — no kind-30311-
specific path needed. The kind-1311 relay subscription stays where it
is in `NestActivityContent.RoomChatFilterAssemblerSubscription`, which
populates `channel.notes` exactly the same way.
Net effect for the user: chat now renders with the rest of Amethyst's
chat features — NIP-21 mention rendering, embedded images / videos,
reply previews, draft handling, mention/file pickers, replies — instead
of plain bodies in a 220dp box.
Layout intentionally unchanged elsewhere: the chat panel keeps its
embedded place at the bottom of NestFullScreen's verticalScroll Column
(top section is good per user). The chat list gets a fixed 420dp height
because a vertically-scrollable LazyColumn child cannot share its
parent's verticalScroll.
Nav: the activity has no Compose nav graph, so an `EmptyNav()` is
passed. Visual rendering is correct; in-message tap navigation (profile
tap, embedded note open, …) is a no-op until we plumb a deep-link-
bouncing INav. Outside scope for this commit.
`NestViewModel.chat` StateFlow is now unused by the panel (kept for now
to avoid touching the VM in the same change; cleanup is a follow-up).
https://claude.ai/code/session_01RDpuki4t8StSg1CZcXnV5b
The AI-suggested alt-text chip was using androidx.compose.material.icons.*,
which the project no longer pulls in (migrated to MaterialSymbols a while
back). The file failed to compile until the dep was either re-added or
the icons migrated. Switching to MaterialSymbols.AutoAwesome / .Close.
When the thread-screen master note is a kind:30312 MeetingSpaceEvent
(Nest) or kind:30313 MeetingRoomEvent, route through
RenderMeetingSpaceEvent / RenderMeetingRoomEvent — same path
NoteCompose uses for non-master entries (NoteCompose.kt:1034-1040).
Without this, the master note in the ThreadFeedView's FullBleedNoteCompose
fell through to the generic note body and showed neither the Nest card
nor the Join CTA. Users who landed on a Nest via search, quote, or naddr
deep-link saw a blank-looking thread; tapping the in-feed card now (since
the routing fix in 98202b6) opens NestActivity directly, but the thread
view itself was the remaining gap.
https://claude.ai/code/session_01RDpuki4t8StSg1CZcXnV5b
The Pause action called stopSelf(), but onDestroy() then triggered the
auto-restart broadcast (because alwaysOnNotificationService was still
enabled), so the notification reappeared seconds later. There was no way
to actually pause without toggling the setting, so the button was just
confusing.
Drops the ACTION_STOP intent, the notification action, and the
always_on_notif_stop string from all locales. Also includes incidental
spotless fixes the pre-commit hook required.
When the host taps "Leave" we now show a 3-button confirmation:
- Close room → re-publish kind:30312 with status="closed", then leave
- Just leave → leave; room stays open until the 8h staleness window
- Cancel → dismiss
Audience-side leave flow unchanged. Without this prompt, hosts who tap
Leave silently abandoned the room — listeners saw it as "live" with no
audio until the EGG-01 rule 7 staleness timeout, eight hours later.
Implementation:
- `closeMeetingSpace(accountViewModel, event)` builds a verbatim
FormState from the event and reuses
EditAudioRoomViewModel.buildEditTemplate(... STATUS.CLOSED). Bypasses
the VM's mutable state so unsaved edits in an open Edit sheet can't
leak into the close payload.
- On publish failure we still leave (the user asked to) and surface a
toast — the room will auto-close at the 8h timeout.
Process death (host backgrounded + Android-killed) is still handled by
the same 8h fallback; no extra wire changes.
https://claude.ai/code/session_01RDpuki4t8StSg1CZcXnV5b
The first-party nostrnests web client (NestsUI-v2) emits kind-30312
events using NIP-53 *streaming-event* tag names rather than the
kind-30312 names defined by NIP-53 itself. Confirmed against
`nostrnests/nests/NestsUI-v2/src/components/EditRoomDialog.tsx` and
`useRoomList.ts`:
- room name: `title` (NIP-53 spec for 30312: `room`)
- MoQ relay: `streaming` (NIP-53 spec for 30312: `endpoint`)
- moq-auth: `auth` (NIP-53 spec for 30312: `service`)
- status: `live` (NIP-53 spec for 30312: `open` / `private`
/ `closed` / `planned`)
Our parsers followed the NIP-53 spec, so events from the production
nostrnests web app fell through to status=null, which `checkStatus`
then surfaced as "Ended" in the live-rooms UI. Add legacy-alias
acceptance to all four parsers — read-side only; we still emit the
canonical NIP-53 forms, matching EGG-01.
Per-tag changes:
StatusTag: "live" → OPEN, "ended" → CLOSED
RoomNameTag: "title" alias → room
EndpointUrlTag: "streaming" alias → endpoint
ServiceUrlTag: "auth" alias → service
No emit-side changes; no spec change.
https://claude.ai/code/session_01RDpuki4t8StSg1CZcXnV5b
When the + FAB is tapped on Audio Rooms and the user's kind:10112
nests-server list is empty (or has no http(s) entries), show an
AlertDialog offering to add the built-in default
(CreateAudioRoomViewModel.DEFAULT_SERVICE_URL — moq.nostrnests.com)
to their list, then continue into the create-room sheet.
Wired through the existing Account.sendNestsServersList path used by
the Settings screen's NestsServersViewModel.save(), so the resulting
kind:10112 propagates to the user's outbox identically. On signer/
network failure surface a toast and keep the dialog dismissed (the
user can retry from Settings).
If the user already has a usable server, behavior is unchanged — the
FAB opens the create-room sheet directly.
https://claude.ai/code/session_01RDpuki4t8StSg1CZcXnV5b
ChannelCardCompose wraps content in ClickableNote, whose default onClick
calls routeFor(note, account). For MeetingSpaceEvent (kind:30312) that
falls through routeForInner's `is AddressableEvent` branch
(RouteMaker.kt:159-161) and returns Route.Note(addressTag) — the thread
view, which is the bug.
Bypass ChannelCardCompose for the rooms feed: AudioRoomFeedCard
renders RenderLiveActivityThumb directly inside a Column.clickable that
launches AudioRoomActivity, mirroring the home live-bubble pattern in
RenderLiveActivityBubble.kt:70-95. Falls back to the thread route when
service/endpoint/d are missing, same as the bubble.
https://claude.ai/code/session_01RDpuki4t8StSg1CZcXnV5b
- Cache the AICore FeatureStatus check per service instance — was
re-running an RPC on every image attach.
- Two-pass decode with inSampleSize so 12 MP camera shots become
~1024 px before we hand them to the describer (avoids 40+ MB
ARGB_8888 allocations and the GC churn that follows).
- Switch ML Kit clients to var + lazy-on-first-use so close() no
longer triggers init for clients we never invoked.
- Wrap the composable's suggestAltText call in try/finally so a
cancellation mid-inference resets the spinner state.