Commit Graph

4163 Commits

Author SHA1 Message Date
Claude fa04107865 feat(notifications): dispatch from LocalCache for all delivery sources
The NotificationRelayService consumes events directly into LocalCache, which
made EventNotificationConsumer's hasConsumed dedup check short-circuit every
subsequent push-notification path. As a result, no notifications fired while
the service was active.

Rework the notification pipeline so every delivery source (FCM, UnifiedPush,
Pokey, active relay subscriptions, NotificationRelayService) feeds LocalCache
and a single dispatcher observes it for notification-relevant kinds. Foreground
suppression is scoped to MainActivity (PiP/Call activities keep notifying),
with carve-outs for CallOffer and WakeUp so time-sensitive events always fire.

- Add NewEventMatchingFilter observable primitive alongside
  EventListMatchingFilter for per-event emission (no list accumulation).
- Add LocalCache.observeNewEvents<T>(filter) backed by the new observable.
- Add MainActivity.isResumed flag flipped in onResume/onPause.
- Add NotificationDispatcher that observes LocalCache for GiftWrap,
  EphemeralGiftWrap, PrivateDm, LnZap, Reaction, Chess, and WakeUp.
- Replace EventNotificationConsumer.consume / findAccountAndConsume with
  consumeFromCache, which skips the hasConsumed check (the observer itself
  provides first-delivery semantics) and gates non-priority kinds on
  MainActivity.isResumed.
- Rewire FCM/UnifiedPush/Pokey receivers to feed LocalCache directly.
- Wire the dispatcher into AppModules lifecycle.
2026-04-23 21:05:56 +00:00
davotoula 647d6f9841 fix: remove duplicate @Suppress annotation on SearchFilterRow
Kotlin rejects the annotation being applied twice on the same function —
broke compileFdroidDebugKotlin and compilePlayDebugKotlin. Keeping the
one above @Composable, removing the duplicate below.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-23 21:09:25 +02:00
davotoula 502e78bbd8 i18n: add translations for cs-rCZ, pt-rBR, sv-rSE, de-rDE
Fills in 60 newly-added English strings (always-on notifications, audio
rooms, battery optimization banner, bottom bar settings, drawer sections,
feed titles, search filters/scopes/sort modes, video player button
settings) across all four target locales, plus a handful of pre-existing
gaps (emoji_pack_count, interest_set_hashtag_count, badge_name_label,
new_community_relays_section) that were missing from individual locales.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-23 21:07:26 +02:00
Vitor Pamplona a84f2f1466 Merge pull request #2528 from davotoula/configurable-video-buttons
Configurable video buttons
2026-04-23 13:09:36 -04:00
davotoula f6a68eecab Video Player Buttons now sits between Reactions and Zaps in the Account Settings section 2026-04-23 19:02:59 +02:00
Vitor Pamplona dda935c1e9 Merge pull request #2527 from vitorpamplona/claude/remove-group-on-leave-JTVQb
Fix memory leak when leaving Marmot groups
2026-04-23 12:12:37 -04:00
Claude 62f4599407 fix: drop Marmot chatroom on leave so messages clear from caches
When the local user leaves a Marmot MLS group, MarmotManager already
wipes MLS state, the relay subscription and the persisted message log,
but the chatroom (and its decrypted inner messages) lingered in
marmotGroupList until the UI happened to call removeGroup. The CLI
leave path skipped that step entirely, and removeGroup itself only
dropped the chatroom from rooms — it left noteToGroupIndex entries and
the chatroom's own messages set in place, so notes stayed strongly
referenced and kept appearing in the Notification feed.

Move the in-memory cleanup into Account.leaveMarmotGroup, and have
MarmotGroupList.removeGroup also clear the chatroom's messages and the
note→group index. LocalCache holds notes weakly, so cutting the strong
refs is enough — GC reclaims them and the existing acceptableEvent
filter (which keys off marmotGroupList.rooms) hides anything still
in-flight.
2026-04-23 16:07:14 +00:00
Crowdin Bot f4dc793bda New Crowdin translations by GitHub Action 2026-04-23 15:42:39 +00:00
davotoula 78e21a0274 Code review:
Dead videoGroup parameter in inner RenderTopButtons overload. Confirmed via git show origin/main — the branch introduced it, and it's only passed through but never read inside the body (the outer overload uses
  videoGroup separately for VideoQualityPopup). Removed from signature and both call sites (preview + outer wrapper).

RenderTopButtons runs for every visible video tile, so recomputing the
top-bar / overflow action lists on every recomposition added avoidable
per-frame filter+map allocations. Memoize on the inputs that actually
change availability.

Also drop the outer Box around AnimatedOverflowMenuButton — the inner
OverflowMenuButton already provides its own sized anchor Box, so the
wrapper added no layout value.
2026-04-23 17:24:54 +02:00
Claude d0b2af752e feat: add configurable video player buttons
Mirror the existing reaction row settings pattern. Each of the six
configurable video player buttons (Fullscreen, Mute, Quality, Share,
Download, PictureInPicture) can be placed either in the top bar or in
the overflow menu, with drag-to-reorder within its chosen location.

- Data model: VideoPlayerAction, VideoButtonLocation, VideoPlayerButtonItem
  added to AccountSyncedSettingsInternal so the config syncs via NIP-78.
- StateFlow wrapper AccountVideoPlayerPreferences plumbed through
  AccountSettings, Account, and AccountViewModel.
- New settings screen at Route.VideoPlayerSettings mirroring the reaction
  row screen (drag-to-reorder list with per-row FilterChip location).
- RenderTopButtons partitions configured buttons into top-bar vs
  overflow lists; overflow icon is hidden entirely when empty. Existing
  availability rules are preserved (Fullscreen needs zoom handler,
  Quality needs HLS with multiple renditions, Download hidden on live
  streams, PictureInPicture hidden without device support).
- Volume is now observed via Player.Listener.onVolumeChanged so the
  overflow Mute row stays in sync with the current state.
- VideoQualityPopup extracted so the quality chooser can open from the
  overflow menu too.
2026-04-23 16:43:19 +02:00
Claude 79188e3f7f fix: scope Marmot messages in notification filter to the current account
The notification filter accepted any note whose `inGatherers` contained
a MarmotGroupChatroom, regardless of which account's list that chatroom
belonged to. Since notes live in the global LocalCache and accumulate
gatherer references across account switches (and are not cleaned up on
leave), old messages stayed in Notifications after switching accounts
or leaving a group.

Mirror the NIP-17 DM guarantee: only accept a Marmot message when the
gatherer is the same instance currently held by the active account's
`marmotGroupList.rooms[nostrGroupId]`.
2026-04-23 14:06:41 +00:00
davotoula 8c41d7ea90 pasting an npub1... / nprofile1... / nevent1... / naddr1... / note1... in the search bar will navigate to the target instead of just showing search results. 2026-04-23 15:02:36 +02:00
Vitor Pamplona b84d286ba9 Merge pull request #2522 from davotoula/improve-search-toggles
Improve search toggles
2026-04-23 08:48:12 -04:00
Vitor Pamplona 0e2acd8886 Merge pull request #2521 from davotoula/claude/fix-r8-room-workdatabase
fix for: release/benchmark crash on start-up
2026-04-23 08:43:39 -04:00
davotoula 458d9bcbab compact filter row with bottom sheet for source/follows/sort
Replaces the two-row, mixed-control filter UI under the search bar with a
single segmented row for scope (All/People/Notes) and a Tune icon button
that opens a ModalBottomSheet containing Source, Follows-only, and Sort
controls. A primary-color dot on the icon indicates non-default filter
state.

Also renames SearchSortOrder.DEFAULT_EVENT/DEFAULT_PEOPLE to
EVENT_DEFAULT/PEOPLE_DEFAULT for consistency with EVENT_OPTIONS/
PEOPLE_OPTIONS, and routes the sheet's reset button through these
canonical constants so the UI default cannot drift from the ViewModel.
2026-04-23 14:20:14 +02:00
davotoula 3b0bf07e58 add missing Column import and spotless formatting 2026-04-23 14:19:57 +02:00
Claude 376cbf8d10 feat(search): add scope, source, follows, sort toggles + bech32 auto-resolve (Android)
Android search previously dumped users, notes, hashtags, relays, and 3 channel
types into a single untoggled list and silently relied on NIP-50 relay search.
This adds a second-row control strip so users can pick what they're looking for
and how results are sourced/ranked.

Commons:
- New SearchScope { ALL, PEOPLE, NOTES } and SearchSource { LOCAL, RELAYS }
- SearchSortOrder: add POPULAR; EVENT_OPTIONS now [RELEVANCE, NEWEST, POPULAR]

Android SearchBarViewModel:
- scope / source / followsOnly / sortOrder StateFlows
- searchResultsUsers + searchResultsNotes respect scope, follows, and sort
- POPULAR sorts notes by Note.zapsAmount (descending)
- Follows-only filters authors (includes replies since filter is by author)
- directEventResolver: bech32 nevent/note/naddr -> Route.Note (auto-navigate)
- updateDataSource skips relay emit when source == LOCAL

Android SearchScreen:
- Second row: [All | People | Notes] SegmentedButton + Sort dropdown
- Third row: [Local | Relays] SegmentedButton + Follows-only chip
- bech32 hit triggers nav.nav(route) and clears the search

Desktop parity deferred - Desktop already ships an advanced panel that covers
most of this (kinds/authors/dates/hashtags/language/exclude terms + sort +
bech32 direct lookup). The new enums live in commons so Desktop can adopt.

Build note: gradle 9.3.1 can't be downloaded in this environment, so Android
and Desktop compilation were not verified here.
2026-04-23 13:17:06 +02:00
davotoula ac1b197336 fix(proguard): keep no-arg constructor of Room *_Impl classes
R8 in playBenchmark (minifyEnabled) stripped the no-arg <init> from
WorkDatabase_Impl because -keepnames preserves names but not members.
Room instantiates generated *_Impl subclasses reflectively via
Class.getDeclaredConstructor(), so startup crashed with
NoSuchMethodException in androidx.startup.InitializationProvider.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-23 13:11:49 +02:00
Claude 81d65e96a8 fix(playback): route HLS URIs to HlsMediaSource via MediaItem mimeType
ExoPlayer's URI-based content-type inference only fires for http(s)
schemes + known extensions, so HLS playback fails in two cases Amethyst
ships regularly:

  1. NIP-71 video events with BUD-10 blossom URIs. Hash-based URIs carry
     no extension and the imeta's application/vnd.apple.mpegurl never
     reaches the MediaItem — MediaItemCache dropped it.
  2. Bare BUD-10 URIs pasted into kind 1 posts, which have no imeta at
     all. Even though the URI contains `.m3u8` in its path, ExoPlayer's
     inference skips custom schemes (`blossom:`).

Forward a normalized mimeType onto MediaItem.Builder.setMimeType so
DefaultMediaSourceFactory routes to HlsMediaSource regardless of scheme
or extension:

  - Normalize IANA Apple HLS variants (application/vnd.apple.mpegurl,
    application/x-mpegurl, audio/x-mpegurl, audio/mpegurl) to
    MimeTypes.APPLICATION_M3U8.
  - Fall back to `.m3u8` URI detection when no imeta mimeType is
    present. Matches the existing isLiveStreaming(url) heuristic.

Adds diagnostic logs at MediaItemCache.compute and
CustomMediaSourceFactory.createMediaSource so the routing decision is
observable in logcat, matching the diagnostic-logs pattern already used
elsewhere in playback.
2026-04-23 12:28:21 +02:00
Claude 3a2068a792 fix(marmot): buffer + retry UndecryptableOuterLayer events after epoch advance
Test 12 (offline catch-up) was almost passing: the retained-epoch
fallback (c88923a3) brought through the 5 epoch-1 application messages
the user sent before adding C, but 3 epoch-2 messages + the rename
commit still went missing. Only on app restart did they appear — the
relay re-delivered and the replayed arrival order happened to put the
epoch-2-advancing commit before the epoch-2 events.

Root cause: kind:445 events arrive from the relay in subscription
order, not epoch order. When B drives an offline-catch-up sequence
like

    msg-1..5 (epoch 1) → add C (epoch 1→2) → msg-6..8 (epoch 2) →
    rename (epoch 2→3)

the receiver can easily see {msg-6, msg-7, msg-8, rename, add-C,
msg-1..5}. Everything from the rename onward arrives before the
add-C commit, so the outer ChaCha20-Poly1305 layer fails with
UndecryptableOuterLayer — we don't have the epoch-2 exporter yet.
Quartz correctly reports the failure, but the receiver just logged
"likely from before our join" and dropped the event on the floor.
Nothing retried when the add-C commit eventually landed and the
epoch caught up.

Fix it at the GroupEventHandler level, where we can both see the
original GroupEvent and hook into every CommitProcessed:

  - Keep a bounded per-group ArrayDeque<GroupEvent> of events that
    failed with UndecryptableOuterLayer. FIFO-evict at 64 entries so
    a genuinely pre-join stream can't pin unbounded memory.

  - On every CommitProcessed for a group, drain that group's queue
    and re-run each event through the normal add() path. A successful
    retry may itself emit another CommitProcessed and trigger recursive
    draining — that's fine, epochs are strictly monotonic so recursion
    depth is bounded by the queue size.

  - Concurrent-safe: all queue mutation is guarded by a Mutex.

Ambient discriminator: we can't tell "epoch too old, from before our
join" from "epoch too new, commit hasn't arrived yet" just from
UndecryptableOuterLayer — both report it. Buffering both is fine:
truly-pre-join events stay buffered until FIFO eviction pushes them
out (no correctness problem, just a few dozen bytes of memory for a
bounded time).
2026-04-23 01:44:32 +00:00
Claude ab064d3cd3 Merge remote-tracking branch 'origin/main' into claude/fix-marmot-polling-R83Bs
# Conflicts:
#	amethyst/src/main/java/com/vitorpamplona/amethyst/model/Account.kt
#	amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/DecryptAndIndexProcessor.kt
2026-04-23 01:11:10 +00:00
Vitor Pamplona 7666ed5cc8 Merge pull request #2507 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2026-04-22 21:06:39 -04:00
Vitor Pamplona e12c7ca595 Merge pull request #2515 from vitorpamplona/claude/fix-marmot-reactions-gtBUg
Fix MIP-03 inner event verification in group messages
2026-04-22 21:06:04 -04:00
Claude 23573c0a5b Merge remote-tracking branch 'origin/main' into claude/fix-marmot-polling-R83Bs 2026-04-23 00:52:09 +00:00
Crowdin Bot 19a92e9bf5 New Crowdin translations by GitHub Action 2026-04-23 00:50:42 +00:00
Claude 5106e572a5 fix: consume Marmot inner reactions/deletions as verified
MIP-03 inner events inside a kind:445 GroupEvent are unsigned (sig is
empty or placeholder), so LocalCache's `justVerify()` fails and
`consume(ReactionEvent)` skips the branch that attaches the reaction to
its target note via `addReaction()`. The same applies to kind:5
deletions. Side-channel inner events from other Marmot clients
(WhiteNoise in particular) were therefore silently dropped by
LocalCache, so an inbound emoji reaction never appeared on the target
chat bubble.

NIP-17 has the same "unsigned inner rumor" shape and already hands the
inner event to `justConsume(..., wasVerified = true)`. Align Marmot with
that contract at the two inbound call sites:
- `GroupEventHandler.add` (fresh kind:445 from relay)
- `Account` restore-from-disk loop (persisted plaintext at app start)

Authenticity is already guaranteed by the MLS layer:
`MarmotInboundProcessor.processPrivateMessage` rejects any inner event
whose `pubkey` doesn't match the MLS sender credential identity before
ever emitting an `ApplicationMessage`.

https://claude.ai/code/session_01KhVtLJAEkNCwpRWUUTqDFM
2026-04-23 00:34:51 +00:00
Claude e6e3120cd2 feat(marmot): let admins grant/revoke admin privileges from group info UI
Adds a star toggle next to each member in MarmotGroupInfoScreen that
admins can use to promote a non-admin to admin or demote a fellow
admin. Both paths show a confirmation dialog and publish a
GroupContextExtensions commit that rewrites admin_pubkeys. The toggle
is hidden for non-admin viewers and for the signer's own row (self
demotion must go through the leave flow). The demote button is also
hidden when the member is the only remaining admin so we never hit the
MIP-03 admin-depletion guard in MlsGroup at commit time.

Account.grantMarmotGroupAdmin / revokeMarmotGroupAdmin reuse
updateMarmotGroupMetadata so the new admin list ships alongside the
signer's outbox relays, keeping the canonical relay set consistent
with other metadata commits.
2026-04-23 00:28:54 +00:00
Claude d05742a8b6 debug(marmot): log publishMarmotKeyPackages path for Test 13 diagnosis
Test 13 (KeyPackage rotation) fails with "no new KP event_id observed"
— the interop log shows three `needsKeyPackageRotation=true` triggers
(once per group join), each of which invokes
`account.publishMarmotKeyPackages()` from `processMarmotWelcomeFlow`,
but no subsequent `kind:30443` publish ever appears in the logcat.

`publishMarmotKeyPackages` was completely silent: no-op guards, the
needsRotation check, the rotated-events count, and the per-event
publish all ran without a log line. We can't tell whether the rotation
produces events that never reach a relay, produces zero events,
short-circuits on needsRotation=false, or is never called at all.

Mirror the addMarmotGroupMember log pattern: entry guards (manager
null / not writeable), the needsRotation + relay count at decision
time, the rotateConsumedKeyPackages produce-count, and a per-event
publish line with id + target relay list. Next rerun of Test 13 will
tell us which branch is silently dropping the rotation.
2026-04-23 00:18:56 +00:00
Claude a4e031353a Merge remote-tracking branch 'origin/main' into claude/fix-marmot-polling-R83Bs 2026-04-22 23:41:40 +00:00
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 7295c12c82 Merge remote-tracking branch 'origin/main' into claude/fix-marmot-polling-R83Bs 2026-04-22 21:37:27 +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 f0eaff638e Merge remote-tracking branch 'origin/main' into claude/fix-marmot-polling-R83Bs 2026-04-22 20:20:13 +00:00
Claude 5a18dc41c8 fix(marmot): inner events are unsigned rumors per MIP-03
MIP-03 ("Application Messages" → Security Requirements) is explicit:

  > "Inner events MUST remain unsigned (no `sig` field)
  >  This ensures leaked events cannot be published to public relays."

Authentication comes from (a) MLS framing — the sender's LeafNode
credential signs the outer MLS Application message — and (b) the
mandatory check in MarmotInboundProcessor.processPrivateMessage that
the inner event's `pubkey` equals the MLS sender's credential identity.
The Nostr Schnorr signature is redundant, and leaving it populated
means a leaked plaintext can be replayed as a valid public kind:9.

whitenoise-rs is spec-compliant (builds via `UnsignedEvent::new()` +
`ensure_id()`, never calls sign). Amethyst was the non-conforming side
on both directions:

1. Send path (`MarmotManager.buildTextMessage`,
   `AccountViewModel.sendMarmotGroupMediaMessage`) called
   `signer.sign(template)`, producing a signed rumor that shipped a
   valid Schnorr signature through the encrypted channel. Switch both
   to `RumorAssembler.assembleRumor` — same id derivation (SHA-256
   over canonicalized [0, pubkey, createdAt, kind, tags, content]),
   but `sig = ""`.

2. Restore path (`Account.kt` on startup reload of persisted inner
   events) called `cache.justConsume(innerEvent, null, false)`, which
   routed through `consumeRegularEvent` → `justVerify` → `event.verify()`
   → FAIL on empty sig → Note registered with `event = null`, message
   never rendered. Pass `wasVerified = true`, matching what the live
   receive path already does after the previous commit (573c5c2b).

Existing on-disk persisted messages from older signed-rumor builds
still load — `wasVerified=true` skips sig verify entirely, so both
legacy signed and spec-correct unsigned rumors deserialize cleanly.
2026-04-22 20:14:07 +00:00
Claude 573c5c2b06 fix(marmot): trust MLS authentication, skip Nostr verify on inner events
The previous commit (6ad522b5) shielded the chatroom from the mistaken
"duplicate" drop but was treating the symptom. The real cause of the
"inner event already in cache" log — reported while Test 02's chat
stayed completely empty — is that `cache.justConsume(innerEvent, null,
wasVerified=false)` runs `event.verify()` on a wn-signed inner kind:9,
that verify fails (likely JSON-canonicalization skew or a rumor-style
empty signature), so `consumeRegularEvent` bails at line 633 before
ever setting `note.event`. LocalCache then always had the empty Note
registered by id but with no event content — subsequent arrivals re-hit
line 615 (`note.event != null` is still false, not the "duplicate"
path), retried verify, failed again, and the chatroom never got a
populated message to render.

The inner event has already been cryptographically authenticated by
MLS — `MarmotInboundProcessor.processPrivateMessage` rejects any inner
event whose `pubkey` doesn't match the MLS sender's credential
identity (MIP-03, line 430). Running Nostr's `event.verify()` again
is a redundant gate. Pass `wasVerified=true`: `consumeRegularEvent`
skips `justVerify` and calls `note.loadEvent(...)` on first arrival,
chatroom-add works immediately, and the test passes.

Also tightens the "already in cache" comment to reflect what that
branch actually means now (a legitimate hydrated-by-startup-restore
duplicate, not a silent verify failure masquerading as one).
2026-04-22 20:03:54 +00:00
Claude 6ad522b52b fix(marmot): surface inbound app messages even when LocalCache says duplicate
The B->Amethyst kind:445 reply was decrypting fine — the harness's
new outbound-side diagnostics + the Amethyst logcat showed a clean
ApplicationMessage path right up to the chatroom-add step:

    GroupEventHandler.add: kind:445 id=988221d0… groupId=ee27bc48…
    GroupEventHandler.add: ApplicationMessage decrypted innerKind=9
                            innerId=ebf35373… author=170df8d1…
    GroupEventHandler.add: inner event already in cache (duplicate)

`cache.justConsume` returned false because the inner event id was
already in LocalCache (a prior session persisted the same plaintext
via persistDecryptedMessage and the startup restore loop in
Account.kt:3034-3044 reloaded it into the cache before this kind:445
arrived). The live-receive branch in DecryptAndIndexProcessor was
gated on `isNew` and silently dropped the message — never calling
marmotGroupList.addMessage — so the chatroom never saw B's reply
even though decryption succeeded.

Mirror what the restore path does: always surface the inner note
in the chatroom. `MarmotGroupChatroom.addMessageSync` is itself
idempotent (dedupes by Note identity), so the call is safe even
when the message was already added through another path.
`persistDecryptedMessage` stays guarded on `isNew` because the
message store is append-only and a re-persist would grow the
on-disk log unboundedly across decrypt retries.
2026-04-22 19:50:50 +00:00
Claude bca829df30 Merge remote-tracking branch 'origin/main' into claude/fix-marmot-polling-R83Bs 2026-04-22 19:37:25 +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
Claude 8416afd965 debug(marmot): log removeMarmotGroupMember + syncMetadataTo transitions
The bug report: after tapping Remove on the Marmot Group Info screen,
the target npub stays in the member list, and the only log line is
the round-tripped kind:445 hitting the "Duplicate" branch in
GroupEventHandler.add. The author-side path (Account.removeMarmotGroupMember
→ MarmotManager.removeMember → syncMetadataTo → publish) has no logging,
so there's no way to tell which step broke — the MLS tree mutation,
the local member-list push to chatroom.members, or the UI recomposition.

Mirror the addMarmotGroupMember logging style on removeMarmotGroupMember
(entry, no-op guards, built commit id, publish) so we can see whether
the author path even runs end-to-end, and have syncMetadataTo log the
group id and the previous→new member count so the StateFlow update
becomes observable. No behavior change.
2026-04-22 19:12:19 +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