WhiteNoise threads its kind:445 payloads across three inner kinds:
kind:9 for chat, kind:7 for emoji reactions, kind:5 for unreacts. The
Amethyst ingest pipeline was routing every inner event into the group
chatroom feed, so a kind:7 reaction rendered as a chat bubble whose
only content was the emoji, quoting the liked message via the target
`e` tag. That looked identical to a threaded reply. The actual kind:9
reply from `wn messages send --reply-to` never arrived, which made the
two look swapped in the UI.
Three changes to untangle this:
- MarmotGroupList.addMessage/restoreMessage now skip inner events with
kind:5 and kind:7 before they enter `MarmotGroupChatroom.messages`.
The reaction is still consumed by LocalCache so it attaches to the
target note's reaction row, and the deletion still revokes that
reaction — they just don't appear as standalone bubbles.
- LocalCache.computeReplyTo learned to derive thread parents for
`ChatEvent` (kind:9) from plain NIP-10 `e` tags in addition to the
existing NIP-18 `q` tag path. WhiteNoise emits `e`-tagged replies;
without this the reply bubble had no quote context in the feed.
- tools/marmot-interop/marmot-interop.sh: `wn messages send` exposes
its `reply_to` field through clap v4, which renames snake_case to
kebab-case by default. The script was passing `--reply_to`, which
clap rejected; the `|| true` + redirected stderr hid the error and
no reply was ever published. Use `--reply-to`.
https://claude.ai/code/session_01K3g1uWLhByoEdBS77zdF32
Before, every Marmot group landed unconditionally under the Known tab of the
Messages screen while the New Requests tab ignored groups entirely, so an
invite from a stranger appeared next to real conversations. The dedicated
Marmot group list screen split by ownerSentMessage only, so unreplied groups
where an admin was followed still showed as New Requests.
Replace both splits with a shared MarmotGroupChatroom.isKnown(followingKeySet):
- user has replied -> Known
- no known members and no messages -> New Requests
- otherwise Known iff any admin is in the follow set
Wire the combined Messages feeds (ChatroomListKnownFeedFilter and
ChatroomListNewFeedFilter) through the helper, invalidate dmNew on group list
changes so empty groups flow into New Requests, and have
MarmotGroupListScreen observe kind3FollowList so newly followed admins move
groups between tabs immediately.
Move the add-member search to a fixed position at the bottom of the screen
(matching the New Short Note pattern) so it stays visible regardless of how
far the user has scrolled through a large member list. Suggestions now render
above the input, and each suggestion row shows an explicit PersonAdd icon
button so the add action has a clear visual affordance.
Adds an optional trailingContent slot to ShowUserSuggestionList / UserLine
so callers can attach per-row actions without forking the component.
https://claude.ai/code/session_01JbRycm4g3LrqatnCVWVdGh
After creating a Marmot group, the Create Group screen stays on the
back stack, so pressing back from the new group chat returns to the
empty creation form instead of the Messages screen. Swap `nav.nav`
for `nav.popUpTo(..., Route.CreateMarmotGroup::class)` so the creation
screen is removed inclusively as we navigate into the group.
After leaving a Marmot group the user was dropped on the group list;
send them to the main Messages tab instead, which is the correct
starting point for picking another conversation.
https://claude.ai/code/session_01JaeqZwVNLKvUUvXWRzPmRP
- Fold the Remove Member screen into the info screen: each member row
now has an inline remove icon that opens a confirmation dialog.
- Fold the Add Member screen into the info screen: a search field with
a user-suggestion list lives directly above the member list, so
adding a member never requires leaving the screen.
- Move Leave Group out of the scrolling body into a top-bar action so
it is reachable regardless of member count.
- Shrink the relay strip (35dp tiles with a small activity dot) and
tuck it next to the group header; drop the "Relays" label and the
MLS epoch readout, which were visual clutter.
- Route the chat screen's Add Member button to the info screen and
delete the two standalone screens and their routes.
- Add headless interop test 17: A creates a group, adds B, removes B,
re-adds B, and verifies B can both receive and send messages. Guards
the reported regression where a re-added member came back without a
usable leaf and silently lost the ability to post.
https://claude.ai/code/session_01JaeqZwVNLKvUUvXWRzPmRP
When the user updates their relay configuration on the AllRelay screen,
the newly-selected relays often have no copy of the account's existing
settings (profile, follow list, mute list, other relay lists, etc.), so
the user appears to "lose" their account to anyone reading from those
relays until the next time each list is edited.
After saving each relay list, re-publish the relevant existing events
to the newly-selected relays (only when the set actually differs):
- Outbox/inbox (NIP-65) or private-storage or broadcast changes
republish every known account-settings event to the new relay set.
- KeyPackage relay-list changes republish all active kind:30443
KeyPackage events authored by this account.
- Local relay-list changes republish account-settings events locally.
- Indexer relay-list changes publish kind:0 and kind:3.
- Search relay-list changes publish kind:0.
Local relays now flow through a new Account.saveLocalRelayList so the
republish hook lives alongside every other relay-list save.
https://claude.ai/code/session_01PcvDaNyzT6Tk4D7jn75Jbm
The three Marmot stores (MLS group state, messages, KeyPackage bundles)
were all initialized with the global app filesDir, so chats and MLS
state from one account were visible to any other logged-in account.
Pass each store a per-account subdirectory (accounts/<pubkey>) derived
from the signer pubkey, so paths become:
files/accounts/<pubkey>/mls_groups/<groupId>/{state,retained,messages}
files/accounts/<pubkey>/marmot_keypackages/state
Existing in-memory state was already scoped per-Account; this closes the
last remaining cross-account leak on disk.
The DisappearingScaffold hid its top/bottom bars purely from overscroll
gestures, so a feed with too few items to scroll would end up with two
empty bands where the chrome used to be. Pure overscroll (consumed.y == 0)
from the fully-visible state is now ignored; once the bars have started
moving we know the list is scrollable and edge overscroll keeps feeding
bar motion as before.
The prior change tracked the "no slide" intent via a mutable flag on Nav
that every navigate method had to reset. Replace it with a per-entry
hint: navBottomBar writes SKIP_SLIDE_ANIMATION_KEY=true to the new
destination's savedStateHandle, and composableFromEnd reads it off
targetState (forward) / initialState (pop).
Benefits:
- No shared mutable state; no reset bookkeeping in nav/newStack/popUpTo.
- No race between interleaved navigate calls.
- Pop animations are automatically consistent with how the entry was
entered - backing out of a bottom-bar tab now also fades.
Drawer routes are registered with composableFromEnd (slide-from-right +
scale-out) because that matches the expected drawer push animation.
When a user pins one of those routes onto the bottom navigation bar
(feature added in 376e404c), the bottom-bar tap still replayed the
slide animation because the transition is bound to the Route type at
composable<> registration time, not to the caller.
Introduce a third navigation verb, INav.navBottomBar(route), that sets
a skipSlideAnimation flag on Nav before performing the same newStack-
style stack reset (popUpTo + launchSingleTop). composableFromEnd /
composableFromEndArgs read the flag in their enter/exit/popEnter/
popExit lambdas and return null when set, inheriting the NavHost-level
fade (matching what the five default bottom-bar routes already do).
nav.newStack behaviour and all drawer navigation paths are unchanged,
so non-bottom-bar callers (intent redirects in AppNavigation.kt and
every DrawerContent click) keep their slide.
Mirrors the Chess drawer entry pattern — `if (isDebug)` gates the
row. Audio rooms' Nostr surface (presence, hand-raise, chat) works,
but without the WebTransport audio backend the entry would bait
release-build users into a room they can't actually listen to.
One-line filter at the call site in DrawerContent.ListContent:
val feedsItems = if (isDebug) DrawerFeedsItems
else DrawerFeedsItems.filter { it != NavBarItem.AUDIO_ROOMS }
CatalogSection(..., feedsItems, ...)
Catalog entry + route + screen wiring all stay, so debug builds still
see it and the Nostr feature continues to be exercisable. When
`QuicWebTransportFactory.connect()` stops throwing NotImplemented the
two lines go away.
https://claude.ai/code/session_013nVLALALKaHVgHm9u5Cg8D
The branch already covers the Nostr-side of audio rooms (drawer
listing, 30312-event parsing, 10312 presence publishing, chat, hand-
raise). The audio-transport pieces (WebTransport + MoQ + Opus play-
back) are blocked on Phase 3b-2 (pure-Kotlin QUIC — see the plan in
`docs/plans/2026-04-22-pure-kotlin-quic-webtransport-plan.md`).
The problem: the in-progress AudioRoomConnectionViewModel was
auto-calling connectNestsListener() on stage enter, and the stub
`KwikWebTransportFactory` throws `NotImplemented`. That surfaced as a
persistent red "Audio failed: WebTransport NotImplemented" chip
every time a user opened any audio room — scary, confusing, and
misleading.
Hiding the broken UI until the transport lands:
- Deleted `AudioRoomConnectionViewModel.kt`. The
AudioRoomConnectionViewModel + ConnectionChip code is recoverable
from git (commit `64b33674`) when Phase 3b-2 makes the underlying
transport real.
- Removed the auto-connect LaunchedEffect + ConnectionChip render
from AudioRoomStage.
- Removed the mute button. Pressing mute when we're not producing a
mic stream would publish `["muted","0"|"1"]` on 10312 presence
with no corresponding audio signal — misleading to peers. The
builder-side support for the muted tag stays in
MeetingRoomPresenceEvent (Quartz) for future use; the UI will
re-add the button when audio capture actually runs.
- `publishPresence(..., muted = null)` so the muted tag is elided
from the broadcast event entirely while the feature is hidden.
- Dropped the unused strings (audio_room_mute, audio_room_unmute,
audio_room_conn_*) from strings.xml.
What still works on the branch (verified):
- Audio Rooms drawer entry lists kind 30312 rooms.
- Tap a room → live-activity channel screen with the audio-room
stage overlay showing title, summary, host+speaker avatars,
audience avatars.
- Kind 1311 chat (read + send) via the existing channel infra.
- Kind 10312 presence published on enter + every 30 s with the
correct `a`-tag and `["hand","1"|"0"]` reflecting local state.
- Hand-raise button toggles the `hand` tag — browser clients +
other NIP-53 peers can see the signal and hosts can promote.
What's hidden until Phase 3b-2 lands:
- WebTransport connection attempt, connection chip, mute button.
Re-enabling them is additive: resurrect AudioRoomConnectionViewModel
from git, paste three LaunchedEffect / DisposableEffect /
ConnectionChip blocks back into AudioRoomStage, restore the five
string resources, restore the mute button + the two-arg muted
presence call. Zero protocol impact on anything else.
https://claude.ai/code/session_013nVLALALKaHVgHm9u5Cg8D
Phase 3d-3 of the Clubhouse/nests integration. Wires the
nestsClient.connectNestsListener() facade into AudioRoomStage via a
dedicated Android ViewModel, and surfaces the listener's StateFlow
through a small assist chip so the user sees connection progress and
failure modes.
amethyst/audiorooms/room:
- New `AudioRoomConnectionViewModel` (Android `ViewModel`):
* Owns one `NestsListener` + one `AudioRoomPlayer` per host/speaker.
* `connect(event, signer)` resolves the room HTTP-side, opens
WebTransport, runs MoQ SETUP, then subscribes to every host +
speaker pubkey from the 30312 event and wires their Opus stream
through `MediaCodecOpusDecoder` -> `AudioTrackPlayer`.
* `disconnect()` is idempotent, also called from `onCleared()`.
* Mirrors the underlying NestsListener.state into its own
StateFlow so callers observe one source.
* Audience members are skipped — they don't publish audio.
- `AudioRoomStage` now mounts the ViewModel keyed by the room
address, auto-calls `connect()` in a LaunchedEffect, and
disconnects in a DisposableEffect.
- New `ConnectionChip` composable renders the listener state with
retry-on-tap for Idle / Failed / Closed, and color-codes Connected
(primary) and Failed (error) states.
amethyst:
- Added `:nestsClient` to the project's dependencies.
res:
- New strings for the five connection-chip states.
Behavior today: on opening an audio-room, the chip will report
`Connecting (OpeningTransport)` then immediately
`Failed: WebTransport NotImplemented: ...` because the Kwik handshake
in `KwikWebTransportFactory` is the next phase. Tapping the chip
retries — same outcome until 3b-2 ships. Everything else (presence,
chat, hand-raise, mute) is unaffected.
https://claude.ai/code/session_013nVLALALKaHVgHm9u5Cg8D
Phase 2 of the Clubhouse/nests integration: when the live-activity
channel screen is opened on a kind 30312 MeetingSpaceEvent, render an
audio-room "stage" above the chat that shows host/speaker/audience
avatars and exposes hand-raise + mute toggles backed by NIP-53 kind
10312 presence events.
Quartz:
- New MutedTag (`["muted","1"|"0"]`) and `muted()` builder helper for
MeetingRoomPresenceEvent.
- New MeetingRoomPresenceEvent.build() overload that accepts a 30312
MeetingSpaceEvent root (matching the existing 30313 overload) and
optionally encodes hand-raise + mute flags in one call.
Amethyst:
- AudioRoomStage composable: filters participants from the 30312 event
into hosts / speakers / audience, renders avatars, and publishes
presence on enter + every 30 s while composed (on dispose it pushes
one final lowered-hand presence so peers drop us before timeout).
- ChannelView wires AudioRoomStage in next to ShowVideoStreaming; the
latter is a no-op for non-30311 events so non-audio rooms are
unaffected.
No audio is captured yet — the mic toggle is a Nostr-only signal
until Phase 3 brings the MoQ/WebTransport transport.
https://claude.ai/code/session_013nVLALALKaHVgHm9u5Cg8D
Adds a new "Audio Rooms" entry in the drawer feeds section that lists
NIP-53 kind 30312 (Interactive Rooms / audio spaces) events, mirroring
the existing Live Streams architecture.
- AudioRoomsFeedFilter narrows LocalCache.liveChatChannels to
MeetingSpaceEvent (30312) and MeetingRoomEvent (30313), sorting by
OPEN > PRIVATE > CLOSED then by follow participation.
- AudioRoomsFilterAssembler/SubAssembler reuse makeLiveActivitiesFilter
so the wire-level REQs are shared with the Live Streams screen.
- AudioRoomsScreen/TopBar/FeedLoaded follow the Live Streams layout and
render via ChannelCardCompose.
This is phase 1 of the Clubhouse/nests integration plan: it only adds
the discovery surface. Joining, presence, audio transport (MoQ) and
room creation will arrive in subsequent PRs.
https://claude.ai/code/session_013nVLALALKaHVgHm9u5Cg8D
- stop() now uses stopService() instead of startService(STOP intent).
stopService() is a no-op when the service isn't running and avoids
starting a service just to stop it, which could throw on Android 12+.
- disableAllLayers() only runs when transitioning from enabled → disabled,
not on initial load with false. Users who never enabled the service
won't trigger any service/WorkManager/AlarmManager calls on login.
https://claude.ai/code/session_01LEPfmgGnwjB9a5SDFw5U8t
Five protocol-level fixes and a batch of harness correctness fixes to get
the headless Marmot/Whitenoise interop harness from 1/13 to 5/13 passing
cleanly, with the remaining failures all rooted in wn's per-account
serial event-processor retry backoff (which drops undecryptable
pre-membership commits after several minutes) rather than amy behaviour.
quartz + commons
----------------
* MarmotGroupData: hold CURRENT_VERSION at 2. mdk-core (the Rust MLS
engine used by whitenoise-rs) strict-rejects v3 payloads with
`ExtensionFormatError("Trailing bytes in NostrGroupDataExtension")`
— our v3 welcomes and GCE commits never apply, so every cross-client
group flow dies at welcome processing. We still parse v3 happily on
the way in; we just don't emit it until mdk publishes the
forward-compat fix MIP-01 mandates.
* MarmotManager.updateGroupMetadata: MERGE extensions instead of
REPLACING. RFC 9420 §12.1.7 says GCE proposals blow away the old
extension list; callers that pass only [marmot_group_data] dropped
[required_capabilities], which peers then reject. Preserve every slot
except the one we're updating.
* MarmotManager.createGroup: new optional `initialMetadata` parameter
that bakes MarmotGroupData into epoch-0 GroupContext.extensions
directly. Without it, creators had to publish a pre-membership
"bootstrap" commit that no later joiner could decrypt — each such
peer then burned their retry budget on an undecryptable kind:445
before seeing the real state. Threaded through MlsGroup.create /
MlsGroupManager.createGroup.
* MarmotManager.mlsGroupIdHex: new translation helper so any code
juggling the MIP-01 nostr_group_id (what amy indexes on) and the
MLS GroupContext groupId (what mdk indexes on) can cross-reference
them without reaching into MlsGroupManager directly.
amethyst module
---------------
* Account.leaveMarmotGroup: self-demote before SelfRemove per MIP-01,
and promote a surviving member to admin first if the caller is the
sole admin (otherwise we'd throw "admin depletion"). Matches the
cli/GroupMembershipCommands.leave flow.
cli (amy)
---------
* Context.syncIncoming: don't advance `giftWrapSince` on empty polls
(so the first-ever sync doesn't bump the cursor past every
past-timestamped wrap we've ever been sent), subtract 2 days lookback
when filtering (NIP-59 randomWithTwoDays gift wraps can have any
createdAt in the last 48h), and only advance `groupSince` for groups
we actually received events for.
* Context.syncIncoming: after ingest, if any Welcome consumed a
KeyPackage, rotate and publish a fresh one immediately. MIP-00
requires this — a KP can only be welcomed once and leaving the
consumed one on relays just means later senders invite us with a
bundle we no longer have private keys for.
* Context.resolveGroupId: accept either nostr_group_id (amy's primary
key) or the MLS GroupContext groupId (what wn emits) on every verb
that takes a group id. Wired through GroupAdd/Remove/Leave/Metadata
/Read, Message send/list, and all the await* verbs so harness
scripts never have to juggle both forms for a single group.
* GroupCreateCommand: bake initial metadata into epoch 0 (see the
quartz change above). Dropped the now-redundant bootstrap commit
publish and tightened the JSON output to include `mls_group_id`.
* GroupMembershipCommands.leave: self-demote admin before SelfRemove;
promote an heir if we're the only admin, otherwise the GCE would
deplete admins and the leave aborts.
* MarmotIngest.ingestGiftWrap: unwrap the sealed-rumor layer. NIP-59
wrap is gift-wrap(kind:1059) → seal(kind:13) → rumor; the old code
only unwrapped once and then checked `inner.kind == 444`, which is
always false because inner is actually the seal. Unseal once more
before the Welcome check. This single fix is what unsticks every
amy-side Welcome ingestion.
wn harness patches + scripts
----------------------------
* whitenoise-defaults-env.patch: honour $WHITENOISE_DISCOVERY_RELAYS in
`Relay::defaults()` (release builds otherwise bake damus.io / primal
/ nos.lol into every new account's NIP-65 / Inbox / KeyPackage
lists, which breaks publishing and prevents the inbox subscription
plane from ever reaching an operational state in a sandbox).
* setup.sh: sleep 2s after amy's initial kind:30443 publish so
nostr-rs-relay has a chance to fsync before wn's first targeted
discovery query. Without it wn's `keys check` races the relay's
WAL flush and intermittently returns NotFound.
* lib.sh: peel wn's `{"result": …}` wrapper in `jq_group_id`,
`wait_for_invite`, `wait_for_message`, `wait_for_member`. Post-v0.2
wn `--json` output nests everything under `.result` (and
`groups invites[]` nests further under `.group.mls_group_id`) —
these helpers were still pattern-matching on the flat shape, so
they returned empty strings for a perfectly good response.
* tests-{create,manage,extras}.sh: track both group IDs per test
(amy's nostr + wn's MLS), pass each CLI the id it understands, and
bump the post-commit wait timeouts to 90–120s so wn's exponential
retry backoff has time to work through the pre-membership commits
it can't decrypt and get to the ones it can.
https://claude.ai/code/session_016kAxdp6ubB5CnF9URhCEzP
The notification service no longer creates its own relay subscriptions.
Instead, it relies on the AccountFilterAssembler subscription from the
Compose tree, which covers notifications, gift wraps, metadata, follows,
relay lists, and drafts. This ensures follow/mute list changes are
reflected in notification filtering.
Heavy feed subscriptions (Home, Video, Discovery, ChatroomList) now use
LifecycleAwareKeyDataSourceSubscription which subscribes on ON_START and
unsubscribes on ON_STOP. When the app backgrounds, these feeds pause and
their outbox relays disconnect. Only inbox and DM relays stay connected
via AccountFilterAssembler.
This prevents bandwidth waste on feeds nobody is viewing while the
always-on service keeps relay connections alive.
https://claude.ai/code/session_01LEPfmgGnwjB9a5SDFw5U8t
The service now maintains two independent subscriptions:
- svc:notif: notification events on NIP-65 inbox relays
- svc:giftwrap: NIP-59 gift wraps on NIP-17 DM inbox relays
This ensures DM relays that aren't also notification inbox relays
stay connected when the app backgrounds. Both subscriptions update
reactively when relay lists change.
Also updates PULL_NOTIFICATION.md with detailed "why" explanations
for each resilience layer and documents the DM relay architecture.
https://claude.ai/code/session_01LEPfmgGnwjB9a5SDFw5U8t
Adds 7 improvements inspired by ntfy's battle-tested keepalive architecture:
1. onTaskRemoved() restart: When users swipe the app from recents,
some OEMs kill the foreground service. Now schedules a 1-second
alarm to restart immediately.
2. onDestroy() → AutoRestartReceiver: When the service is destroyed
(by system or OEM), broadcasts to AutoRestartReceiver which
enqueues a one-time WorkManager task to restart. Catches kills
that START_STICKY might miss.
3. ForegroundServiceStartNotAllowedException handling: On Android 12+,
starting a foreground service from background can throw. Now caught
gracefully instead of crashing.
4. Redundant startForeground() from onStartCommand(): Safety fallback
in case onCreate() didn't complete before onStartCommand() fired
(ntfy issue #1520 edge case).
5. WakeLock during notification processing: EventNotificationConsumer
now acquires a PARTIAL_WAKE_LOCK with 10-minute timeout to ensure
CPU stays awake long enough to decrypt and dispatch notifications
even in Doze mode.
6. Battery optimization exemption: Added REQUEST_IGNORE_BATTERY_OPTIMIZATIONS
permission and BatteryOptimizationHelper. Settings screen shows an
error-colored banner with "Fix now" button when always-on is enabled
but the app isn't whitelisted.
7. Network-available WorkManager pattern: Added scheduleOnNetworkAvailable()
to NotificationCatchUpWorker. Enqueues a one-time task with
NetworkType.CONNECTED constraint so the service restarts immediately
when connectivity returns, rather than waiting for the next periodic
worker.
https://claude.ai/code/session_01LEPfmgGnwjB9a5SDFw5U8t
Handle ACTION_MY_PACKAGE_REPLACED in BootCompletedReceiver so the
always-on notification service auto-restarts after app updates.
Without this, the service stays dead until the user opens the app
or reboots. Inspired by Pokey's approach.
https://claude.ai/code/session_01LEPfmgGnwjB9a5SDFw5U8t
Implements a 5-layer always-on notification system that maintains
persistent WebSocket connections to the user's inbox relays for
real-time notification delivery, eliminating the dependency on the
external push server seeing all relays.
Layer architecture:
- L1: Foreground service (specialUse type, no Android 15 time limit)
keeps the shared NostrClient alive by collecting relayServices flow
- L2: FCM/UnifiedPush (existing, unchanged) as wakeup trigger
- L3: WorkManager periodic worker (15-min catch-up safety net)
- L4: BOOT_COMPLETED receiver (restart service after reboot)
- L5: AlarmManager watchdog (5-min health check for OEM killers)
Key design: the foreground service shares the same NostrClient as the
UI. When the app foregrounds, both the UI and service are subscribers
to the relay pool. When the app backgrounds, UI subscriptions drop
but service subscriptions remain — zero reconnection needed.
New files:
- NotificationRelayService: foreground service with persistent notification
- BootCompletedReceiver: restarts service on device boot
- NotificationCatchUpWorker: WorkManager fallback for missed events
- ServiceWatchdogManager: AlarmManager-based health monitor
- AlwaysOnNotificationServiceManager: coordinates all 5 layers
Settings: opt-in toggle in App Settings, persisted per-account.
https://claude.ai/code/session_01LEPfmgGnwjB9a5SDFw5U8t
- Follow Packs was piggybacking on the Discovery top nav filter; give it a
dedicated defaultFollowPacksFollowList StateFlow, liveFollowPacksFollowLists,
and a DEFAULT_FOLLOW_PACKS_FOLLOW_LIST preference key so its top nav filter
no longer mutates Discovery (and vice versa).
- Persist Communities, Badges, Browse Emoji Sets, and Follow Packs top nav
filters in encrypted SharedPreferences so they survive app restart.
- Longs top nav was restricted to kind3GlobalPeople; switch it to
kind3GlobalPeopleRoutes to match the other media/reading feeds and expose
hashtag/geohash/relay/interest-set filters.
Previously the drawer hand-coded label + icon + route for every item,
duplicating what NavBarCatalog already stored. Adding a new screen
meant updating both files (and forgetting the catalog silently broke
the bottom-bar settings, as happened with Polls).
- Add POLLS to the catalog (was missing).
- Add three ordered id lists in NavBarItem.kt: DrawerNavigateItems,
DrawerYouItems, DrawerFeedsItems. Each drawer section iterates its
list and looks each id up in NavBarCatalog.
- Add CatalogSection + CatalogNavigationRow helpers in DrawerContent
that render a NavBarItemDef using the existing NavigationRow
overloads (Drawable vs Vector icon).
- Profile keeps its primary-color tint via a special case inside
CatalogSection (the only item with a non-default tint).
- Create (Share HLS Video / Chess) and System (IconRowRelays +
Settings) stay hand-coded: they contain non-catalog items or
custom composables.
Net: DrawerContent.kt shrinks by ~160 lines; adding a new drawer
screen is now a single catalog entry plus one list membership.
Users can now pick any subset of drawer items, in any order, to appear
in the bottom bar. If zero items are selected the bottom bar is hidden.
- NavBarItem enum + NavBarCatalog: every drawer destination gets an id,
label, icon, and route resolver so items can be looked up by id.
- UiSettings gains bottomBarItems: List<NavBarItem>; persisted in the
existing UiSharedPreferences DataStore as a comma-separated enum list.
- AppBottomBar reads the list from UiSettingsFlow and renders a zero-
height spacer (preserving nav-bar insets) when the list is empty.
- New Navigate section at the top of the drawer exposes Home, Messages,
Media, Discovery, Notifications as drawer destinations.
- New BottomBarSettingsScreen under All Settings lets users pin/unpin
entries and drag to reorder (same UX as ReactionsSettingsScreen).
Introduces a dedicated "Live Streams" screen that mirrors the Shorts
architecture (FeedContentState + FilterAssembler + SubAssembler) and
reuses the Discovery/LiveStreams rendering (ChannelCardCompose) and
relay filter builders (makeLiveActivitiesFilter) for the same top-nav
filters available on Shorts: Following, Global, Hashtag, Location,
Authors, Community, etc.
Refactors LocalPreferences.innerLoadCurrentAccountFromEncryptedStorage
to extract follow-list pref reads into a small helper so the method
stays under the JVM 65KB method size limit after adding the new
defaultLiveStreamsFollowList setting.
Introduces a dedicated Public Chats screen accessible from the left drawer,
mirroring the architecture of the Shorts screen. It reuses the existing
Discover public-chats renderer (ChannelCardCompose + RenderPublicChatChannelThumb)
and its relay-filter helpers (makePublicChatsFilter), but with its own feed
content state, filter assembler/sub-assembler, and top-nav follow-list filter
setting so the selection is independent from Discover.
- New Route.PublicChats, drawer entry, screen/top-bar/feed composables
- PublicChatsFeedFilter scans LocalCache.publicChatChannels
- defaultPublicChatsFollowList persisted in AccountSettings / LocalPreferences
- Live per-relay follow list flow on Account for relay subscription
Introduces a standalone Follow Packs screen (Route.FollowPacks)
accessible from the drawer, rendering FollowListEvent (kind 39089)
entries from LocalCache with the same top-nav filter as Shorts and
the Discovery tab. Reuses the existing defaultDiscoveryFollowList
setting and DiscoverFollowSetsFeedFilter logic so the two surfaces
stay in sync. Item rendering goes through ChannelCardCompose pinned
to FollowListEvent.KIND, matching the Discovery follow-packs tab.
Move topic chips below the author meta row and shrink them
(10sp gray text on a subtle fill, no border, tighter padding)
so the title, summary, and author are the primary focus.
Move the "defaults a new Amethyst account gets seeded with" into commons
so the UI and amy agree byte-for-byte on what a fresh account looks like.
commons additions:
- commons/defaults/Constants.kt — relay URL constants (moved from amethyst/)
- commons/defaults/AmethystDefaults.kt — DefaultChannels, DefaultNIP65RelaySet,
DefaultNIP65List, DefaultGlobalRelays,
DefaultDMRelayList, DefaultSearchRelayList,
DefaultIndexerRelayList (moved from
amethyst/model/AccountSettings.kt)
- commons/account/AccountBootstrapEvents.kt — data class + bootstrapAccountEvents(signer, name)
that builds the nine events a new Amethyst account publishes: kind:0, 3,
10002, 10050, 10051, 10099, 50, 51, + channel list. One source of truth.
Retrofits:
- AccountSessionManager.createNewAccount now delegates event construction to
the commons helper; it still packages them into AccountSettings for the
Android storage path.
- DefaultSignerPermissions stays in AccountSettings.kt — it uses Android
NIP-55 Permission/CommandType types.
- 19 import updates across amethyst/ to point at the new commons paths.
New CLI verbs:
- amy create [--name NAME]: mint a keypair, write identity.json, seed
relays.json with the Amethyst defaults, publish all nine bootstrap events
to DefaultNIP65RelaySet via NostrClient.publishAndConfirmDetailed.
- amy login KEY [--password X]: accept any identifier form Amethyst's login
screen accepts — nsec1, ncryptsec (+ --password, NIP-49), BIP-39 mnemonic
(NIP-06), npub1, nprofile1, 64-hex pubkey (default) or 64-hex privkey
(with --private), NIP-05 (name@domain.tld, HTTP lookup). Read-only keys
land with privKeyHex=null; Identity now carries that cleanly.
https://claude.ai/code/session_01M6dCKAF5Y1VyHGZPjzwDXq