Commit Graph

4127 Commits

Author SHA1 Message Date
Vitor Pamplona 95f189338a Merge pull request #2513 from vitorpamplona/claude/fix-whitenoise-reactions-EWq2j
Filter non-displayable messages from Marmot group chat feed
2026-04-22 19:39:42 -04:00
Claude c4e7e0d9b4 fix: don't render Marmot reactions/deletions as chat bubbles
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
2026-04-22 22:07:15 +00:00
Vitor Pamplona 7a932c03e3 Merge pull request #2511 from vitorpamplona/claude/fix-marmot-group-tabs-zLq2N
Implement known/new group classification based on follow set
2026-04-22 18:02:58 -04:00
Claude 11ba03334c fix: classify Marmot groups in Messages Known/New tabs by admin follow
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.
2026-04-22 21:40:23 +00:00
Claude 119959e942 feat: pin add-member field to bottom of Marmots group info with add button
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
2026-04-22 21:16:25 +00:00
Claude 7cc53dc8ba fix: pop Create Group screen when navigating to new Marmot group
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.
2026-04-22 20:40:53 +00:00
Claude a10fdb934a fix: route Leave Group back to the Messages screen
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
2026-04-22 19:36:44 +00:00
Claude 65c56d0035 feat: inline member management on Marmot group info screen
- 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
2026-04-22 19:28:25 +00:00
Vitor Pamplona 2d6f626ee3 Merge pull request #2502 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2026-04-22 14:44:42 -04:00
Vitor Pamplona 21c367302b Merge pull request #2506 from vitorpamplona/claude/fix-scaffold-spacing-NDAkw
Fix disappearing bars hiding on pure overscroll for non-scrollable lists
2026-04-22 14:44:35 -04:00
Crowdin Bot 642aa6fe1f New Crowdin translations by GitHub Action 2026-04-22 18:44:32 +00:00
Vitor Pamplona 74aa940098 Merge pull request #2504 from vitorpamplona/claude/republish-relay-events-mo64n
Auto-republish account settings when relay lists change
2026-04-22 14:42:52 -04:00
Claude 36eda1cdea feat(relays): republish account settings when AllRelay lists change
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
2026-04-22 18:32:28 +00:00
Claude bd2fae9ca5 fix(marmot): scope Marmot storage per-account to prevent chat leakage
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.
2026-04-22 18:30:24 +00:00
Claude b5b01f6eda fix(scaffold): keep bars visible on non-scrollable lists
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.
2026-04-22 18:28:41 +00:00
Claude f59597c55f refactor: move skip-slide hint to NavBackStackEntry.savedStateHandle
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.
2026-04-22 16:24:17 +00:00
Claude ed68b79938 fix: skip slide animation for user-pinned bottom bar routes
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.
2026-04-22 16:08:04 +00:00
Vitor Pamplona 57ff68895f Fixes iconography inconsistency 2026-04-22 11:22:01 -04:00
Vitor Pamplona c21831bd3e Merge pull request #2481 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2026-04-22 09:56:01 -04:00
Vitor Pamplona e5dd70b12c Merge pull request #1983 from vitorpamplona/claude/always-on-notification-service-NuW9I
Add always-on notification service for real-time Nostr relay connections
2026-04-22 09:54:57 -04:00
Crowdin Bot 19ee474ab6 New Crowdin translations by GitHub Action 2026-04-22 13:51:58 +00:00
Vitor Pamplona 03f5f18894 Merge pull request #2494 from vitorpamplona/claude/clubhouse-event-listing-Kta46
Add MoQ-transport client and audio rooms support
2026-04-22 09:50:02 -04:00
Claude 6aa47c2fec chore(audio-rooms): hide drawer entry in release builds
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
2026-04-22 13:44:29 +00:00
Vitor Pamplona 1d7f2e7e6d Merge pull request #2493 from vitorpamplona/claude/fix-marmot-interop-tests-3ZSSj
Fix MLS commit cryptography and add comprehensive validation
2026-04-22 09:42:41 -04:00
Claude 3ca613d64b fix(audio-rooms): hide audio-transport UI that has no backend yet
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
2026-04-22 13:38:35 +00:00
Claude 64b3367472 feat: AudioRoomConnectionViewModel + connection chip in stage
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
2026-04-22 07:29:42 +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 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 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 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 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 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
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 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 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 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
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 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