Commit Graph

11975 Commits

Author SHA1 Message Date
Claude 933b522273 feat(nestsClient): NIP-98 auth + nests room-info client
Phase 3a of the Clubhouse/nests integration. Adds a new KMP module
`nestsClient` (Android + JVM targets) that owns the HTTP control plane
for talking to a nests audio-room backend. No transport/audio yet —
that arrives in 3b with WebTransport + MoQ.

Surface:
- `NestsAuth.header(signer, url, method)` signs a kind 27235 event via
  Quartz's existing HTTPAuthorizationEvent and returns a ready-to-use
  `Authorization: Nostr <base64>` header value.
- `NestsRoomInfo` data class + tolerant JSON parser (ignores unknown
  fields so newer nests server revisions don't break older clients).
- `NestsClient.resolveRoom(serviceBase, roomId, signer)` calls
  `<serviceBase>/<roomId>` with the signed NIP-98 header and returns
  the MoQ endpoint + token the audio layer will need.
- `OkHttpNestsClient` (jvmAndroid) is the default implementation
  shared between Android and desktop JVM.

Tests:
- `NestsRoomInfoTest` (commonTest) covers full/minimal payloads,
  unknown-field tolerance, missing-endpoint rejection, and URL
  construction edge cases.
- `NestsAuthTest` (jvmTest) signs a real 27235 event, decodes the
  base64 back through Quartz's JacksonMapper, and asserts the
  signature verifies and the url+method tags bind correctly.

https://claude.ai/code/session_013nVLALALKaHVgHm9u5Cg8D
2026-04-22 02:58:24 +00:00
Claude a853ac66b0 fix(marmot): exclude newly-added leaves from UpdatePath resolution
RFC 9420 §12.4.1: when a Commit contains Add proposals, the committer
MUST exclude the new leaves from the copath-resolution used for path-
secret encryption. Joiners get the group state via the Welcome at epoch
N+1; they neither need nor expect a path secret for epoch N.

Quartz was including them. openmls (via mdk-core) then runs its own
resolution-minus-exclusion-list when picking which ciphertext to
decrypt, so its `resolution_position` landed one slot off from where
quartz had put the ciphertext for the existing member. The AEAD tag
naturally mismatched and the commit died with
`InvalidCommit(UpdatePathError(UnableToDecrypt))` — the blocker behind
every test that needs an existing openmls member to process a commit
produced by quartz (add, rename, remove, leave, promote, catchup).

Fix: in `MlsGroup.commit()`, capture `newLeafIndices` from the Add
proposals applied in this epoch and `filterNot` them out of every
copath node's resolution before HPKE-encrypting the path secret.

https://claude.ai/code/session_016kAxdp6ubB5CnF9URhCEzP
2026-04-22 02:41:09 +00:00
Claude 0db80c66b8 feat: audio room stage with NIP-53 presence (kind 10312)
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
2026-04-22 02:25:06 +00:00
Claude a07e22d7ab fix(marmot): sign + membership-tag PublicMessage commits
openmls (via mdk-core) strict-verifies both the FramedContentAuthData
signature and the membership_tag on every inbound PublicMessage from a
Member sender (RFC 9420 §6.1, §6.2). Quartz was framing commits with
empty placeholders for both fields, so any existing member processing a
quartz-originated commit tripped `openmls::ciphersuite: Incompatible
values` (constant-time compare of a zero-length tag against the 32-byte
expected HMAC). That's the reason every interop test that requires B to
process a commit from amy (add-member, rename, remove, leave, promote,
catchup) was failing at step one.

Fix computes both fields correctly:
  * capture pre-commit groupContext bytes, membership_key, and
    signingPrivateKey before the key schedule / epoch advance
  * build FramedContentTBS = version || wire_format || FramedContent ||
    serialized_context and SignWithLabel("FramedContentTBS", ..) with
    the committer's pre-commit leaf signing key
  * build AuthenticatedContentTBM = TBS || signature<V> ||
    confirmation_tag<V> and HMAC-SHA256 it with the pre-commit
    membership_key

No change to quartz's own processCommit path (it doesn't re-verify these
fields on inbound), so this is a one-way fix for outbound to openmls
peers.

https://claude.ai/code/session_016kAxdp6ubB5CnF9URhCEzP
2026-04-22 02:22:09 +00:00
Claude 1fd89b23e5 feat: audio rooms drawer listing (NIP-53 kind 30312)
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
2026-04-22 01:57:16 +00:00
Claude 0b7b1353e8 fix(marmot-interop): broaden skip-retry patch to all MdkCoreError
Narrowing the previous patch to only MlsMessageUnprocessable /
MlsMessagePreviouslyFailed still leaves wn stuck retrying
\`MdkCoreError("Failed to decrypt message with any exporter secret")\`
— mdk surfaces that as an Err variant rather than the Unprocessable
result enum, so it took the full 10-attempt exponential backoff (~17
minutes) before giving up. That was enough to block later
decryptable commits and regress test 11 (leave group) from pass back
to fail.

mdk-core doesn't retry internally, so ANY Err it returns is terminal
by construction. Treating the whole \`MdkCoreError\` variant as
one-shot is therefore equivalent to "trust mdk's verdict" rather
than "permissive skip" — if mdk decides the message is bad, it will
stay bad.

Net in the headless harness: 6/13 pass (up from 5/13 with the
narrower patch and 1/13 baseline).

https://claude.ai/code/session_016kAxdp6ubB5CnF9URhCEzP
2026-04-22 01:29:19 +00:00
Claude e4b5b12a0b fix: prevent unnecessary service lifecycle on startup for non-users
- 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
2026-04-22 00:55:15 +00:00
Claude 8ba424295c fix(marmot-interop): drop retries for provably-unprocessable MLS messages
mdk-core's `MessageProcessingResult::Unprocessable` means the received
kind:445 is genuinely outside this member's decrypt horizon — it's from
a pre-membership epoch, or an epoch we've retained past, and no amount
of retrying will make it decryptable. wn's account event processor was
running that down the same exponential retry ladder as a legitimate
transient failure (10 attempts, 1s..512s backoff, ~17 minutes to
exhaust), which in the interop harness meant every later decryptable
commit — add-member, rename, leave — had to wait behind a queue of
doomed retries and raced the 30s test timeout.

Add a third wn patch, `whitenoise-skip-unprocessable-retry.patch`,
that treats `MlsMessageUnprocessable` and `MlsMessagePreviouslyFailed`
as terminal: log the warning once, skip the reschedule. Applied in
preflight via `setup.sh`.

https://claude.ai/code/session_016kAxdp6ubB5CnF9URhCEzP
2026-04-22 00:50:10 +00:00
Vitor Pamplona 66f7753c8b Merge pull request #2492 from vitorpamplona/claude/amy-cli-guide-cuLaP
docs: add Amy CLI development guides and skill reference
2026-04-21 20:42:27 -04:00
Claude f08b010f50 fix(marmot,cli,interop): interop-compatible Marmot flows and harness correctness
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
2026-04-22 00:28:45 +00:00
Claude 9b9084ffaf docs(cli): split amy docs by responsibility + add amy-expert skill
Separate the single DEVELOPMENT.md into focused docs per audience:

- cli/README.md — trim agent/interop sub-sections; user-facing contract,
  commands, data-dir, troubleshooting only.
- cli/DEVELOPMENT.md — pared down to architecture, how-to-add-a-command,
  output conventions, testing, housekeeping.
- cli/ROADMAP.md (new) — north-star, parity matrix, ordered milestones,
  non-goals. The live checklist for CLI feature parity.
- cli/plans/2026-04-21-cli-distribution.md (new) — packaging strategy
  (Homebrew / winget / Scoop / deb / rpm / AUR / AppImage).
- commons/plans/2026-04-21-event-renderer.md (new) — cross-cutting
  renderer design owned by commons, consumed by cli + desktop + android.

Agentic routing:

- .claude/skills/amy-expert/SKILL.md (new) — routing triggers + the
  five hard rules (thin-layer, JSON contract, non-interactive,
  data-dir-is-world, extract-before-adding).
- references/command-template.md, extraction-recipe.md,
  output-conventions.md — bundled copy-paste references.

Root .claude/CLAUDE.md:

- Adds cli/ to the module list with its sharing rule.
- Registers amy-expert in the skills table.
- Documents per-module plans/ convention; freezes docs/plans/.

https://claude.ai/code/session_01BQ5ZHwa8BAgEQ9zeM4CKhW
2026-04-22 00:23:00 +00:00
Vitor Pamplona b765bbe0a1 Merge pull request #2491 from vitorpamplona/claude/nip-53-compliance-OEzbS
Add NIP-53 proof of agreement and event builders
2026-04-21 20:20:50 -04:00
Claude 2f78849845 fix(quartz): align NIP-53 Live Activities with spec
- MeetingRoomPresenceEvent (kind 10312) now extends BaseReplaceableEvent
  instead of BaseAddressableEvent. NIP-53 states this kind is a regular
  replaceable event ("presence can only be indicated in one room at a
  time"), and LocalCache already routes it through consumeBaseReplaceable.
  createAddress/createAddressATag/createAddressTag now take only pubKey.

- Add TagArrayBuilder extensions for streaming (30311), meeting space
  (30312) and meeting room (30313) tags, plus build() helpers that assemble
  the required tags per NIP-53.

- Add ProofOfAgreement utility to sign and verify the schnorr proof that
  NIP-53 places in the 5th element of a participant p-tag
  (SHA256 of kind:pubkey:dTag signed by the participant's private key).
  Includes LiveActivitiesEvent.hasValidProof() convenience.
2026-04-21 23:48:11 +00:00
Claude 35a84ffaf9 docs(cli): add user README and development roadmap for amy
Add cli/README.md covering the JSON-output contract, install via
installDist, today's command surface (identity / relays / marmot),
and use from agents + interop tests. Add cli/DEVELOPMENT.md with the
feature-parity matrix vs Amethyst, the extract-from-Android recipe
(amethyst/ -> commons/ -> cli/), command template, EventRenderer
plan shared with Desktop, testing strategy focused on what quartz
and commons don't already cover, distribution matrix across macOS /
Windows / Linux / Nix / Zapstore, and an ordered 10-step roadmap.

https://claude.ai/code/session_01BQ5ZHwa8BAgEQ9zeM4CKhW
2026-04-21 23:26:06 +00:00
Claude fc6b7871c0 refactor: reuse AccountFilterAssembler, pause heavy feeds on background
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
2026-04-21 23:24:11 +00:00
Claude 4763ae57c0 feat: add DM inbox relay subscriptions to notification service
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
2026-04-21 23:24:11 +00:00
Claude 315b1e4942 docs: add PULL_NOTIFICATION.md describing the always-on service
Documents the architecture, 8-layer auto-restart defense, WakeLock
strategy, battery optimization guidance, seamless foreground/background
transitions, and all files involved.

https://claude.ai/code/session_01LEPfmgGnwjB9a5SDFw5U8t
2026-04-21 23:24:11 +00:00
Claude b451cb1f19 feat: harden notification service with ntfy-inspired resilience
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
2026-04-21 23:24:11 +00:00
Claude 6d42ab8341 feat: restart notification service after app updates
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
2026-04-21 23:24:11 +00:00
Claude ae548a851d feat: add always-on notification relay service
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
2026-04-21 23:24:10 +00:00
Vitor Pamplona 39f9cc350a Merge pull request #2490 from vitorpamplona/claude/fix-feeds-topnavfilter-ZfdnN
Add follow packs support to default follow list preferences
2026-04-21 19:19:42 -04:00
Vitor Pamplona 57388fd075 Merge pull request #2489 from vitorpamplona/claude/configurable-bottom-nav-rFdQw
Add customizable bottom navigation bar with drag-to-reorder UI
2026-04-21 19:18:31 -04:00
Claude e2acacc7a1 fix: give each Feeds top nav filter its own Account + LocalPreferences flow
- 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.
2026-04-21 23:14:05 +00:00
Claude 5e75d7a643 Render drawer Navigate/You/Feeds sections from NavBarCatalog
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.
2026-04-21 23:11:22 +00:00
Claude 1c93d7db96 Add PublicChats, FollowPacks, LiveStreams to NavBarItem catalog
These three drawer screens landed on main. Register them in the catalog
so they can be pinned to the bottom bar from the settings screen.
2026-04-21 22:51:46 +00:00
Vitor Pamplona ae1549b884 Merge pull request #2488 from vitorpamplona/claude/debug-marmot-whitenoise-oO26C
Add CLI interface (amy) for Marmot/MLS group operations
2026-04-21 18:47:56 -04:00
Claude 569a785963 Merge remote-tracking branch 'origin/main' into claude/configurable-bottom-nav-rFdQw 2026-04-21 22:46:38 +00:00
Vitor Pamplona 4f82b68d06 Merge pull request #2487 from vitorpamplona/claude/add-public-chats-screen-LSgH4
Add Public Chats screen with feed filtering and relay subscriptions
2026-04-21 18:42:44 -04:00
Claude 25781a8815 fix(quartz): send JVM PlatformLog output to stderr
On the JVM, Log.d / .i / .w / .e were printing through \`println()\` which
lands on stdout. The amy CLI uses stdout as its JSON contract — any
Marmot/MLS command would restore state, emit a burst of DEBUG lines,
then emit the JSON object, and every caller piping through jq failed
to parse because the first non-empty line was a log line.

Switch to \`System.err.println\` inside PlatformLog.jvm.kt. Conventional
for CLIs (data on stdout, diagnostics on stderr), and the desktop app
already redirects as part of packaging. Callers who want everything
mixed can still \`2>&1\` at the shell level.

Fixes amy marmot group create / group add / message send etc. when
their output is captured by a shell script.

https://claude.ai/code/session_01M6dCKAF5Y1VyHGZPjzwDXq
2026-04-21 22:38:38 +00:00
Claude 11f14f7497 Merge remote-tracking branch 'origin/main' into claude/add-public-chats-screen-LSgH4
# Conflicts:
#	amethyst/src/main/java/com/vitorpamplona/amethyst/LocalPreferences.kt
#	amethyst/src/main/java/com/vitorpamplona/amethyst/model/Account.kt
#	amethyst/src/main/java/com/vitorpamplona/amethyst/model/AccountSettings.kt
#	amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/RelaySubscriptionsCoordinator.kt
#	amethyst/src/main/java/com/vitorpamplona/amethyst/ui/feeds/RememberForeverStates.kt
#	amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/AppNavigation.kt
#	amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/drawer/DrawerContent.kt
#	amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/routes/Routes.kt
#	amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AccountFeedContentStates.kt
#	amethyst/src/main/res/values/strings.xml
2026-04-21 22:35:13 +00:00
Claude 020abccf57 fix(cli): mark Identity.hasPrivateKey @JsonIgnore
Jackson was writing the computed \`hasPrivateKey\` into identity.json
and then refusing to read it back ("Unrecognized field hasPrivateKey")
on the next invocation. All amy commands that reload the data-dir
(\`marmot group create\`, \`marmot key-package publish\`, …) exited 1 at
the Context.open step. Pure boolean getter, no persistence needed.

https://claude.ai/code/session_01M6dCKAF5Y1VyHGZPjzwDXq
2026-04-21 22:30:08 +00:00
Claude 760e682b89 fix(marmot-interop): parse post-v0.2 \wn --json whoami\ shape
wn now wraps account listings in {"result": [{...}]} so extract_pubkey's
object + array fallbacks both missed it and ensure_identity bailed with
"could not determine \$who npub" immediately after create-identity.

Also: create-identity on a fresh box can exit non-zero with
"failed to connect to any relays" even when the account *was* created
locally. Probe --json whoami afterwards before calling that a fatal.

https://claude.ai/code/session_01M6dCKAF5Y1VyHGZPjzwDXq
2026-04-21 22:27:20 +00:00
Vitor Pamplona c9127111d2 Merge pull request #2486 from vitorpamplona/claude/add-livestreams-screen-gr5Ak
Add Live Streams feed screen with filtering and discovery
2026-04-21 18:26:19 -04:00
Claude 376e404c96 Make bottom navigation bar configurable
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).
2026-04-21 22:23:15 +00:00
Claude 37515ec975 feat(marmot-interop): let wnd run in sandboxes without kernel keyring
Containers / CI often block the keyutils syscalls wnd uses by default
to store secret keys (add_key returns EACCES, keyctl_search returns
ENOSYS). The integration-tests feature ships a mock keyring store, but
the stock wnd binary doesn't wire it up. Add a tiny opt-in patch.

Changes:
- headless/patches/whitenoise-mock-keyring.patch: if built with the
  integration-tests feature and \$WHITENOISE_MOCK_KEYRING is set, wnd
  calls Whitenoise::initialize_mock_keyring_store() before the normal
  init path. Harmless on production builds.
- headless/setup.sh: apply both harness patches generically from a
  list; build wn/wnd with --features cli,integration-tests; export
  WHITENOISE_MOCK_KEYRING=1 alongside WHITENOISE_DISCOVERY_RELAYS when
  launching each daemon.
- preflight swaps keyctl for patch in required tools — we no longer
  need a real session keyring.

https://claude.ai/code/session_01M6dCKAF5Y1VyHGZPjzwDXq
2026-04-21 22:20:54 +00:00
Claude 3de0814e4f Add Live Streams screen accessible from the left drawer
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.
2026-04-21 22:19:16 +00:00
Claude 6213fb2197 feat(marmot-interop): patch wnd to honour \$WHITENOISE_DISCOVERY_RELAYS
Without this, wnd exits on startup with NoRelayConnections in any
environment that can't reach wnd's baked-in public discovery relay
set (nos.lol, relay.damus.io, …). The headless harness runs entirely
on a loopback relay, so hitting the public internet is never OK.

Changes:
- headless/patches/whitenoise-discovery-env.patch: adds an env-var
  override at the top of DiscoveryPlaneConfig::curated_default_relays.
  When \$WHITENOISE_DISCOVERY_RELAYS is set (comma-separated) we use
  that list instead of the hardcoded public one.
- headless/setup.sh: applies the patch in preflight (idempotent —
  checks for a .headless-discovery-patched marker), invalidates the
  previous wn/wnd build on first patch, rebuilds, and threads
  \$WHITENOISE_DISCOVERY_RELAYS=\$RELAY_URL into every start_daemon
  invocation.

https://claude.ai/code/session_01M6dCKAF5Y1VyHGZPjzwDXq
2026-04-21 22:14:16 +00:00
Claude 871a01c4aa feat: add standalone Public Chats screen
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
2026-04-21 22:13:51 +00:00
Vitor Pamplona 78b7fd228f Merge pull request #2484 from vitorpamplona/claude/reduce-topic-chips-emphasis-gI6Jr
De-emphasize topic chips in LongFormHeader
2026-04-21 18:13:21 -04:00
Vitor Pamplona 7cc6f05daa Merge pull request #2485 from vitorpamplona/claude/add-follow-packs-screen-pRAmv
Add Follow Packs discovery feed screen
2026-04-21 18:12:54 -04:00
Claude 6c137636e5 Add Follow Packs drawer screen mirroring Shorts architecture
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.
2026-04-21 22:07:21 +00:00
Claude bed3514700 chore(marmot-interop): check for protoc in preflight
nostr-rs-relay's build.rs needs protoc to compile the nauthz gRPC
definitions. Without it the build fails deep in prost-build with a
panic that's hard to read, so fail up front with a pointer to
`apt-get install protobuf-compiler`.

https://claude.ai/code/session_01M6dCKAF5Y1VyHGZPjzwDXq
2026-04-21 22:06:59 +00:00
Claude 51ad472046 feat(marmot-interop): run headless suite against a local nostr-rs-relay
Every test now flows through one loopback nostr-rs-relay on
ws://127.0.0.1:8080 — no public relays, no egress. Addresses the
repeated 503s we saw from the sandbox and the kind:445 rejections we
hit on public relays even with internet.

Changes:
- Preflight clones https://github.com/scsibug/nostr-rs-relay into
  state-headless/nostr-rs-relay and runs `cargo build --release`
  (~3 min first run). Cache hits on subsequent runs.
- New start_local_relay / stop_local_relay functions render a minimal
  config.toml each run (binds to 127.0.0.1, no kind blacklist, data
  under state-headless/relay) and wait up to 20s for the port.
- configure_relays collapsed to a single-relay path — A/B/C all point
  at \$RELAY_URL. Kept publish-lists + key-package publish as smoke
  signals for the advertise path.
- Flags pared down: drop --local-relays (always local now), add
  --port N for when 8080 is taken. --no-build still honoured.
- trap wires relay shutdown alongside daemon shutdown.

https://claude.ai/code/session_01M6dCKAF5Y1VyHGZPjzwDXq
2026-04-21 22:04:28 +00:00
Claude c2e2d4eca8 De-emphasize topic chips in LongFormHeader
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.
2026-04-21 21:47:28 +00:00
Claude 559c69660f feat(cli,commons): amy login + amy create, share defaults with Amethyst
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
2026-04-21 21:41:47 +00:00
Claude 39e11d29c1 feat(marmot-interop): add zero-prompt headless harness driving amy
Matches the 13 scenarios in marmot-interop.sh but runs A via the new
`amy` CLI instead of prompting the human. Identities B and C still go
through `wn`/`wnd` from whitenoise-rs.

Layout:
- marmot-interop-headless.sh    — top-level entrypoint
- headless/setup.sh             — preflight, daemon lifecycle, identities
- headless/helpers.sh           — amy wrappers + assertion helpers
- headless/tests-create.sh      — tests 01..05
- headless/tests-manage.sh      — tests 06, 07, 08, 11
- headless/tests-extras.sh      — tests 09, 10, 12, 13

Reuses lib.sh for colours, record_result, wait_for_invite/message,
jq_group_id, and dump_daemon_diagnostics.

Keeps the interactive marmot-interop.sh intact for final UI sign-off.

https://claude.ai/code/session_01M6dCKAF5Y1VyHGZPjzwDXq
2026-04-21 21:18:48 +00:00
Vitor Pamplona 53db6fa10e Merge pull request #2483 from vitorpamplona/claude/review-claude-files-hJo5h
Expand skill library with feed patterns, auth signers, and relay client docs
2026-04-21 17:08:08 -04:00
Claude 60edd473c7 Refreshes .claude/ skill library: fixes stale refs, adds 4 new skills
- Updates CLAUDE.md tech stack to current versions (Compose 1.10.3, Kotlin 2.3.20).
- Reframes kotlin-multiplatform iOS as mature; adds secp256k1-kmp 0.23.0 references.
- Updates desktop-expert Main.kt references (code grew from ~270 to 1341 lines and
  NavigationRail moved to ui/deck/SinglePaneLayout.kt); replaces obsolete
  "hardcoded ctrl = true" anti-pattern note with accurate isMacOS branching.
- Removes compose-desktop.md (superseded by desktop-expert/).
- Adds nostr-expert references: nip19-bech32, event-factory, crypto-and-encryption,
  large-cache. Adds kotlin-expert/common-utilities, compose-expert/rich-text-parsing,
  android-expert/image-loading.
- New skills: account-state (Account + LocalCache), relay-client (subscriptions,
  filter assemblers, preloaders), feed-patterns (FeedFilter + FeedViewModel family),
  auth-signers (NostrSigner across internal / NIP-46 / NIP-55).
2026-04-21 21:00:45 +00:00
Vitor Pamplona 9b945d32e8 Merge pull request #2482 from vitorpamplona/claude/reorganize-app-navigation-wkjQ6
Organize drawer navigation into collapsible sections
2026-04-21 16:59:55 -04:00
Claude bf28fbf26f Add @Preview composables for CollapsibleSection and ListContent
CollapsibleSectionPreview shows two sections with representative rows so
header styling, expand state, and chevron alignment can be verified
against light/dark themes. ListContentPreview wires mockAccountViewModel
and EmptyNav to render the whole reorganized drawer body.
2026-04-21 20:53:11 +00:00