The Recommended Apps screen listed every discovered app definition in a
single sorted LazyColumn with no way to filter, so finding a specific app
to recommend meant scrolling the whole list. Users couldn't find a search
because there wasn't one.
Add an always-visible outlined search field below the description that
filters the list by app name (case-insensitive). Rows whose kind 31990
definition hasn't arrived yet have no name to match, so they only appear
when the box is empty. A dedicated empty state shows when a query matches
nothing.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EjuK6g3KNkyEEnti6JtDEi
A kind:7 reaction can carry several `e` tags (e.g. the thread root plus
the actually-reacted-to reply). Per NIP-25 the last `e` tag is the event
being reacted to, but the notification consumer resolved the reacted note
via `originalPost().firstOrNull()`, which picked the root. The tray then
showed the like as if it targeted the root note instead of the reply.
Switch to `lastOrNull()` to match NIP-25 and the in-app reaction card,
which already resolves the target with `note.replyTo?.lastOrNull()`.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018EEQN8Sy7mQ81BEC3rUu5o
Adds a "Show Messages" toggle to the Display section of Notification Settings.
When disabled, direct/group messages (NIP-17 chats, NIP-04 DMs, and Marmot
group messages) are filtered out of the Notification tab, keeping them only in
the Messages tab. Defaults to on, preserving existing behavior.
The setting is persisted per-account in AccountSettings/LocalPreferences and is
included in the notification feed key so the feed refreshes immediately when
toggled.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018FwwcaBRyvnJFWaDLoQ16d
Audit follow-ups, no behaviour change:
- isNotifiablePublicChatReply bails before allocating the HashSet/ArrayDeque
scratch when the channel message has no parents (top-level posts — the
common case), and builds the deque straight from the parent list.
- Correct the docstring: a kind-42 replyTo holds only the immediate parent
(the channel root is filtered out), so the chain is walked hop-by-hop
through each cached ancestor — the previous wording implied replyTo already
held the ancestors.
- Trim the over-long inline comment on the acceptableEvent gate.
- Make the multi-hop test prove what it claims: assert the immediate parent
is not me, so the walk only passes by climbing to the grandparent.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PxDVCSWe1RwZ51vABBwqbG
Audit follow-up. The previous comment claimed an in-place addressable
replacement "not re-emitting here is fine" because AppRow watches each note;
that conflated per-row content (which does stay live) with list membership and
sort order (which do not re-evaluate on a kind-31990 replacement). Documents
the actual behavior and why it is acceptable. Also hoists the nested
remember{} initial-value cache scan out of the collectAsStateWithLifecycle
argument for readability; no behavior change.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01J8oLe5cMzSY8AzTiULJZiW
Hoist the AddressableNote-vs-id shareId computation out of the two image
share rows, and fix the KDoc rationale: "Share as Image" shares a local PNG
(no relay write); the rows are hidden on private rumors because they expose
the note's content publicly, not because they publish an e-tag.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Jc7PP3PLwT4spvjB2c72pk
The screen previously used observeNewEvents purely as an invalidation tick
to re-scan LocalCache.addressables on every app-definition insertion. Since
observeNotes already seeds with the cached kind-31990 notes and re-emits as
new ones arrive, the candidate list now derives straight from those notes:
the appDefinitionsTick counter and the repeated full-cache rescan are gone.
Initial value is seeded from the current cache snapshot to preserve the
first-frame content.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01J8oLe5cMzSY8AzTiULJZiW
Hoisting the p-tag/public-chat-reply check into a pre-computed val made it
eager: the tag scan (and, for channel messages, the reply-chain walk) ran on
every Note in the cache, even the overwhelming majority rejected by the cheap
`kind in NOTIFICATION_KINDS` check. Inline it back into the && chain in its
original 4th position so that kind check short-circuits ahead of it, and order
the OR so the cheaper tag scan runs before the reply-chain walk.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PxDVCSWe1RwZ51vABBwqbG
The blanket NIP-31 alt removal also dropped genuine accessibility
descriptions (image descriptions for the blind) on a few media paths where
the user's caption was only stored in the event-level alt tag. Restore them
via the proper, non-deprecated fields:
- NIP-94 FileHeaderEvent (kind 1063) and NIP-17 encrypted file headers
(kind 15): write the `alt` tag only when the user actually provided a
caption (the NIP-94 accessibility description), never the old boilerplate
fallback.
- MIP-04 encrypted group media (kind 9): route the caption into the imeta
`alt` field via buildMip04IMetaTag instead of an event-level alt tag.
Re-adds the narrowly-scoped TagArrayBuilder.alt() / AltTag.assemble() write
helpers (documented as accessibility-only, not for deprecated NIP-31
boilerplate). Boilerplate alt tags on all other event kinds remain removed.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014xAESAz1H1VNjmQpMVqBXj
Replaces the global newEventBundles firehose (woke on every new event of
every kind, then filtered for AppDefinitionEvent client-side) with the
indexed LocalCache.observeNewEvents for kind 31990, so the cache delivers
only app-definition insertions to the recompute tick.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01J8oLe5cMzSY8AzTiULJZiW
Tray notifications were only suppressed for new events while the app was
foregrounded (MainActivity.isResumed); notifications already posted while
backgrounded lingered even after the user opened the app and viewed them.
Per-event tray notifications are keyed by the triggering event's
id.hashCode(), so when a note bearing such an id is marked read in-app
(loadAndMarkAsRead's onIsNew branch), cancel the matching notification.
Childless group summaries are cleaned up so the tray doesn't keep an
empty summary around. Covers replies/mentions (Notification feed via
NoteCompose) and DMs (ChatMessageCompose); grouped reaction/zap cards
have no 1:1 event mapping and are left untouched.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XsnDifyUtaWUq4wGu4smZf
Per review, the shared element is now just the three true Share options
(browser link, image file, image URL) in ShareActionRows, surfaced by the
ShareOptionsBottomSheet drawer.
- The 3-dot menu keeps its four copy-to-clipboard rows inline, exactly as
before; its share section is now a single "Share" row that opens the drawer
instead of doing the share directly.
- NoteDropDownMenu owns the drawer state and renders the sheet in place of the
menu dialog, so the existing direct callers (MultiSetCompose,
MessageSetCompose) need no changes.
- The reaction-row Share button opens the same drawer.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Jc7PP3PLwT4spvjB2c72pk
Wires the shared FeedFilterSpinner into the Recommended apps (NIP-89)
screen so it matches the other feed screens. The selection persists per
account via a new defaultAppRecommendationsFollowList setting and resolves
through the existing topNavFilterFlow machinery. App definitions only carry
an author dimension, so the Follows-style filters narrow the list to apps
made by those authors (matchAuthor); hashtag/relay/community variants are
no-ops on apps, as expected. Combines with the local text search and adds a
filter-empty message.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01J8oLe5cMzSY8AzTiULJZiW
The NIP-31 event-level "alt" tag is deprecated, so Amethyst no longer
emits it on any event it builds. Removed all `alt(...)` builder calls and
`AltTag.assemble(...)` insertions across every event kind in quartz (and
the few app-side builders), along with the now-unused `ALT`/`ALT_DESCRIPTION`
companion constants and the `TagArrayBuilder.alt()` / `AltTag.assemble()`
write helpers.
Reading alt tags from incoming events is kept (AltTag.parse/match,
TagArray.alt(), Event.alt()) for interop with clients that still send them,
and the imeta media accessibility `alt` field (NIP-92/94) is untouched.
Updated/removed tests that asserted alt-tag presence and refreshed the
deterministic event-id/sig golden masters in UpdateMetadataTest.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014xAESAz1H1VNjmQpMVqBXj
Public chats (NIP-28, kind 42) routinely reply to a user without adding a
`p` tag, so the existing mention gate (Event.isTaggedUser) silently dropped
them from both the in-app Notifications feed and Android tray push.
Add NotificationFeedFilter.isNotifiablePublicChatReply: a cache-only check
that walks a channel message's reply chain for one of the user's own
messages — covering a direct reply to my message ("the previous message was
mine") and later messages in a thread I'm already part of ("an active
thread"). It is bounded (depth + visited-set) and reads only Note.replyTo,
so the push dispatcher and the feed can both consult it without loading the
account or decrypting anything.
Wire it as an OR alongside the p-tag gate in the three relevance sites that
share the rule — NotificationFeedFilter.acceptableEvent, the
NotificationDispatcher observer predicate, and EventNotificationConsumer —
while keeping tagsAnEventByUser as the scoping AND so unrelated channel
chatter never leaks through, even in Global mode.
Route ChannelMessageEvent to its own tray handler: reply-to-me renders as a
threaded reply (with inline reply action) grouped by channel so a busy room
collapses into one notification; a pure p-tag citation still renders as a
mention. Muting a thread suppresses it via the existing isAcceptable gate.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PxDVCSWe1RwZ51vABBwqbG
Tapping Share in a note's reaction row now opens a bottom drawer with the
same Copy & Share options as the 3-dot menu (Copy Text, Copy Author ID,
Copy Note ID, Copy raw JSON, Share link, Share as Image, Share as Image
URL) instead of jumping straight to the system share sheet.
The seven rows are extracted into a shared ShareCopyActionRows composable so
the 3-dot menu and the new ShareOptionsBottomSheet stay in sync.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Jc7PP3PLwT4spvjB2c72pk
Extract hardcoded user-facing strings into string resources so they can be
translated, and delete string resources that are no longer referenced
anywhere in the codebase.
Localization (new string/plurals resources + stringRes/pluralStringResource
call sites):
- Marmot (MLS) group chat feature: list, chat, info, create, and edit
screens, plus dialogs and toasts.
- OnchainSection (Copy/Send), DvmContentDiscoveryScreen (pay invoice),
OtsSettingsSection (explorer API settings). Reused existing generic
strings (back, cancel, save, remove, leave, description, members, send,
clear) where available.
Cleanup:
- Removed 367 string/plurals resources with no remaining references, along
with their translations across all locale files. References were resolved
across Kotlin/Java (including multi-line R.string wraps), XML, gradle
resValue, and intra-resource @string lookups to avoid removing live
strings.
Verified with :amethyst:compilePlayDebugKotlin, compileFdroidDebugKotlin,
and processPlay/FdroidDebugResources (aapt resource linking) plus
spotlessCheck.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SxvNWmNASbvna4CD5QJFo9
Adds an in-screen text filter at the top of the Recommended apps
(NIP-89 kind 31990) screen that filters the loaded app list by name
and description as the user types, with a clear button and an
empty-results message.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01J8oLe5cMzSY8AzTiULJZiW
Custom-emoji (NIP-30) reactions arrive with content ":shortcode:" backed by
an ["emoji", shortcode, url] tag. Previously the notification showed the raw
":shortcode:" text in the title. Since notification text strips ImageSpans,
the emoji image is now composited as a badge on the reactor's avatar
(largeIcon) and the title keeps just the author's name.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UvWKvm3YW6hS5gsJSqePaG
When replying to a note that is a kind 1 TextNoteEvent, is the root of a
new thread (no e-tags), and was itself posted from Amethyst (NIP-89
client tag), build a NIP-22 kind 1111 CommentEvent instead of a kind 1
reply. Forks keep using kind 1.
Applies across all kind-1 reply paths: the Android composer
(ShortNotePostViewModel), the notification quick-reply
(NotificationReplyReceiver), and the desktop composer (ComposeNoteDialog).
Adds Event.isClient / TagArray.isClient helpers (NIP-89, case-insensitive)
with unit coverage.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01V7RyevA6jL1NuY7uev2agS
The media OkHttp client keeps 32 connections warm in a 5-minute pool but,
unlike the relay client, set no pingInterval. Hosts like blossom.primal.net
silently drop idle HTTP/2 connections between feed-scroll bursts. OkHttp then
pulls a dead connection from the pool and the request stalls until the read
timeout (30s wifi / 90s mobile), which users see as "the first image after a
pause takes forever".
Device MediaHttp logs showed the signature repeatedly:
blossom.primal.net total=30000ms ttfb=-1ms conn=reused error=SocketTimeout
Add a 10s HTTP/2 keepalive ping so dead pooled connections are detected in
seconds and retryOnConnectionFailure re-issues on a fresh one, mirroring what
the relay client already does.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Previously the Crowdin workflow ran two independent jobs that each opened
their own pull request: the crowdin/github-action sync PR ("New Crowdin
Translations") and the peter-evans seed PR ("Seed translator npub
placeholders").
Collapse them into a single job: the Crowdin action now only downloads
translations into the working tree (push_translations/create_pull_request
disabled), the seed script edits translators.json in the same checkout, and
one create-pull-request step opens a single combined PR on
l10n_crowdin_translations.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Kxm4Pq3rm4doVLqJ2LaXtr
The version-chip observer filtered releases by author only, so an
author with many apps pulled every release they ever published into the
observer's working set. Narrow the filter on the release `i` tag (the
app id) so only this app's releases are loaded.
A blind limit is avoided on purpose: LocalCache.filter applies
take(limit) before sorting by created_at, so a limit could drop the
latest release the chip needs.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EbCTsBoGtBCJ1rTKCcar6w
Replace the global newEventBundles + full-cache rescan with an
index-driven LocalCache.observeNotes(kind 30063 / author) observer, the
established idiom (NestLobbyScreen, OpenPollsState, DvmContentDiscovery).
The FilterIndex only wakes the observer when a matching release is
inserted, instead of re-scanning the whole addressables map on every
event app-wide.
Extract the NIP-82 release parsing (dTag-prefix + kind-30063 collision
handling) into shared helpers reused by findLatestNip82Release and
findAllNip82Releases.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EbCTsBoGtBCJ1rTKCcar6w