Commit Graph
15458 Commits
Author SHA1 Message Date
Claude 938f40d568 refactor(amethyst): fall back to quartz KindNames for unknown kinds
The relay kind chips, NIP-86 management screen, and app-recommendation
labels all map kinds to translated R.string labels via kindDisplayName(),
returning -1 for kinds without a localized name. Route that -1 case
through the shared quartz KindNames registry so any kind known to the
protocol layer renders its canonical English label instead of a bare
"k<kind>"/empty placeholder. Translations still win where they exist;
quartz is the canonical source for the rest.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011SapGdtAc1j7woifoCZ9fY
2026-06-21 21:10:31 +00:00
Claude b6c065d421 feat(quartz,cli): add KindNames registry + amy kind
Introduce a canonical, i18n-free event-kind registry in quartz:
`com.vitorpamplona.quartz.kinds.KindNames` maps each of the 145 known
kinds to an English label + the defining NIP. The data is ported from
Amethyst's relay-view `kindDisplayName` mapping (the NIP derived from
each event class's package), so the "what is kind N" knowledge now lives
once in quartz instead of only in the Android UI.

`amy kind <N|NAME>` looks a kind up by number (label + NIP) or searches
labels by name — a thin wrapper over KindNames, dispatched statelessly
(no account/network).

i18n split: quartz holds the canonical English (quartz is intentionally
translation-free); localized front ends overlay their own strings and can
fall back to KindNames.nameFor() for kinds they don't translate. amy
prints the English directly.

Verified: kind 1/0/30023/1059/24133 labels+NIPs, name search ("podcast"
→ 4), unknown → known:false, text + JSON output.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011SapGdtAc1j7woifoCZ9fY
2026-06-21 21:00:54 +00:00
Claude 749a301469 feat(cli): cheap nak-parity wins — key nip49, nip lookup, blossom check/mirror
- `amy key encrypt|decrypt` (NIP-49): encrypt a secret key to ncryptsec1…
  and back. Bidirectionally interop-verified vs the real nak binary
  (amy-encrypt → nak-decrypt and nak-encrypt → amy-decrypt both match).
- `amy nip N` / `amy nip list`: look up a NIP — the nostr-protocol/nips
  git repo FIRST, then a Nostr fallback (NIP-50 search over wiki kind:30818
  + long-form kind:30023 on search-capable relays). Slug normalization
  handles 1→01 and hex-suffixed NIPs (7d→7D, 5a→5A); titles parsed from the
  setext headings NIP docs use.
- `amy blossom check|mirror`: HEAD-check blobs (exit 1 if any missing, like
  nak) and request BUD-04 mirroring of a blob from a source URL.

Also persists the full introspected nak comparison into ROADMAP. `kind`
stays deferred — it needs a kind→schema registry that doesn't exist in
quartz (would be data/logic that doesn't belong in cli).

Verified: key round-trip + nak cross-impl both ways; nip repo titles for
01/46/5A/7D + Nostr fallback on miss; blossom check 200/404 + exit codes.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011SapGdtAc1j7woifoCZ9fY
2026-06-21 20:50:02 +00:00
Claude 2c37ae642c feat(nip46): handle auth_url challenges in the remote-signer client
Adds NIP-46 `auth_url` (web-authorization) support to quartz's remote
signer, so amy (and Amethyst) can use auth-requiring bunkers like
nsec.app / nsecbunker:

- Both BunkerResponse deserializers (kotlinx + the JVM/Android Jackson
  one actually used by OptimizedJsonMapper) now special-case
  `{result:"auth_url", error:<url>}` BEFORE the generic error branch,
  which previously flattened it to a plain error and dropped the marker.
- RemoteSignerManager gained an `onAuthUrl` callback and a wait loop: on
  an auth_url response it surfaces the URL once and keeps waiting for the
  real response under the same request id (UNLIMITED channel so both are
  buffered). newResponse peeks the pending entry (get, not remove) so the
  follow-up isn't dropped; the waiter still removes it in finally.
- NostrSignerRemote forwards `onAuthUrl`; amy's Context prints the URL to
  stderr and the pending command keeps waiting.

Tests: a deserializer test (auth_url keeps result+error) and a manager
test (auth_url surfaced via callback, then the real response resumes the
request) — both green; existing duplicate/late-response manager tests
still pass after the get-vs-remove change.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011SapGdtAc1j7woifoCZ9fY
2026-06-21 20:19:01 +00:00
Claude 8b7caa5b01 feat(cli): NIP-46 nostrconnect:// reverse flow (both sides)
Add the client-initiated NostrConnect flow to complete NIP-46 parity:

- `amy login --nostrconnect [--relay …] [--name N]` (client) mints a
  transport keypair, prints a nostrconnect:// offer, subscribes, and
  waits for the signer's connect ACK (a kind:24133 whose decrypted
  result echoes our secret). The ACK's author is the signer; it persists
  a bunker account acting as that key.
- `amy bunker connect nostrconnect://…` (signer) parses a client offer,
  sends the secret-echo ACK, then services that client's requests on the
  offer's relays. Shares the serve loop with `amy bunker`.

NostrConnect.kt holds the offer parse/build (+percent-decode) and the
client handshake. BunkerCommand grew a `connect` sub-mode and factored
the request loop into serve().

Verified end-to-end:
- amy client ⇄ real `nak bunker connect`: amy learns nak's key and signs;
  event authored by nak, signature valid.
- amy client ⇄ amy `bunker connect`: same, authored by the host key.

Only `auth_url` challenges remain unimplemented in the NIP-46 surface.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011SapGdtAc1j7woifoCZ9fY
2026-06-21 20:04:03 +00:00
Claude 695a5b4b25 fix(cli): nak-compatible bunker — percent-encoded URIs + get_relays
Close the NIP-46 interop gap against the real nak binary:

- `Identity.fromBunkerUri` now URL-decodes the relay/secret params. nak
  emits `bunker://<pk>?relay=wss%3A%2F%2F…&secret=…`; without decoding,
  `amy login` of a nak bunker URI produced a broken relay and never
  connected. (This was the one real break — found by testing vs nak.)
- `amy bunker` now percent-encodes the relay/secret in the URI it prints,
  matching nak's output format.
- `amy bunker` implements `get_relays` (returns its relay set), so nak
  clients that probe it get a proper reply instead of an error.

Verified end-to-end with `go install`-built nak over relay.damus.io,
both directions:
- `amy login bunker://` ⇄ `nak bunker`: amy signs, event authored by
  nak's key, signature valid.
- `nak event --sec bunker://<amy>` ⇄ `amy bunker`: nak signs through
  amy, event authored by amy's key; amy logs connect→ok, sign_event→ok.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011SapGdtAc1j7woifoCZ9fY
2026-06-21 19:52:37 +00:00
Claude 54a6b44324 feat(cli): NIP-46 bunker — remote signer server + bunker login
Add both halves of NIP-46 remote signing so two amy processes interop:

- `amy bunker [--relay …] [--secret S] [--timeout SECS]` runs a remote
  signer for the active local-key account: prints a bunker:// URI, then
  subscribes to kind:24133, decrypts each BunkerRequest, dispatches to
  ctx.signer (connect/get_public_key/sign_event/nip04/nip44/ping), and
  publishes the encrypted BunkerResponse. Long-running like `subscribe`.
- `amy login bunker://PUBKEY?relay=…&secret=…` creates a remote-signer
  account: mints a local transport keypair, records the connection, and
  acts as PUBKEY. Context builds a NostrSignerRemote (vs the local
  NostrSignerInternal) and runs openSubscription()+connect() in prepare();
  every signing/encryption call is delegated to the bunker.

Storage: IdentityFile gains a `bunker` block; the transport key is kept
in the existing SecretStore `secret` field. Identity/DataDir load+save
handle the remote-signer account type; `canSign` reflects "writeable via
bunker".

Thin assembly over quartz nip46RemoteSigner (NostrSignerRemote,
BunkerRequest*/BunkerResponse*, NostrConnectEvent). Verified end-to-end
over relay.damus.io: alice hosts a bunker, bob logs in and `amy event`
signs remotely — the note is authored by alice's key and verifies
(id_ok + signature_ok); bunker logs connect→ok, sign_event→ok.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011SapGdtAc1j7woifoCZ9fY
2026-06-21 19:38:56 +00:00
Claude 69d03bacca feat(cli): add nak-style git (NIP-34) + podcast (NIP-F4) commands
git (repository metadata + collaboration events; clone/push packfile
transport is out of scope):
- git announce  publish a kind:30617 repository announcement
- git list      list a user's repo announcements
- git show      print one announcement (naddr or kind:pubkey:id), cache-first
- git issue     publish a kind:1621 issue against a repo (fetches the repo
                announcement to build the EventHintBundle)

podcast (NIP-F4):
- podcast metadata  publish kind:10154 show metadata (replaceable)
- podcast publish   publish a kind:54 episode (--audio URL[,URL])
- podcast list      list a user's metadata + episodes

Thin assembly over quartz GitRepositoryEvent / GitIssueEvent /
PodcastMetadataEvent / PodcastEpisodeEvent. Verified offline: announce->
show cache round-trip (name/clone/hashtags), issue kind:1621 against the
repo, podcast metadata kind:10154 + episode kind:54.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011SapGdtAc1j7woifoCZ9fY
2026-06-21 19:13:21 +00:00
Claude fb35c85de4 feat(cli): add nak-style sync (NIP-77 Negentropy) primitive
`amy sync --relay URL [filter] [--down] [--up]` reconciles the local
event store with a relay using NIP-77 Negentropy:

- negotiate the symmetric difference under the filter (drives quartz
  NegentropySession over a raw WebSocket, mirroring geode's interop
  driver),
- --down (default) downloads the ids the relay has and we lack via
  Context.drain,
- --up uploads the events we have and the relay lacks via
  Context.publish.

Filter flags match fetch/subscribe; an empty filter reconciles the
whole store. Verified against relay.damus.io: cold store -> need=60,
downloaded=60; immediate re-sync -> need=0 (idempotent).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011SapGdtAc1j7woifoCZ9fY
2026-06-21 19:09:18 +00:00
Claude b3b23df675 Merge origin/main into claude/loving-hopper-rn553t
Integrate the `refactor(cli): simplify amy dispatch, Context lifecycle,
and store handling` change (Commands.kt removed, new route() sub-verb
helper, Context.open(dataDir).use { } lifecycle) with the nak-parity
Tier-1 commands added on this branch.

Re-wiring:
- Dropped Commands.kt; wired the 16 new verbs directly into Main.kt's
  dispatch (stateless: decode/encode/verify/key/filter + relay info
  interception; account-path: event/publish/fetch/subscribe/count/
  encrypt/decrypt/gift/outbox/blossom).
- Converted every new command from hand-rolled try/finally to
  Context.open(dataDir).use { ctx -> } and the multi-verb dispatchers
  (gift/blossom/key) to the shared route() helper.
- RelayCommands.dispatch now routes via route(); `relay info` stays a
  stateless Main interception and is also in the route map.

Verified post-merge: build green + smoke test across stateless,
account-path, and route() sub-verb paths (decode/key/filter, event->
verify round-trip, gift/blossom bad-verb errors, relay info on damus).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011SapGdtAc1j7woifoCZ9fY
2026-06-21 18:47:18 +00:00
Vitor PamplonaandGitHub abd540e3c2 Merge pull request #3326 from vitorpamplona/claude/relaxed-meitner-3yr85y
Refactor command dispatch: extract Router helper, remove Commands.kt
2026-06-21 14:39:03 -04:00
Vitor PamplonaandGitHub 1909f402ab Merge pull request #3325 from vitorpamplona/claude/feed-media-prefetch
feat(feeds): prefetch media + pre-parse text ahead of the viewport
2026-06-21 14:36:50 -04:00
Claude 8e1059a4ab feat(cli): add nak-style blossom blob commands (upload/download/list/delete)
Sixth batch of nak parity — Blossom (NIP-B7 / BUD-01/02/04):

- `amy blossom upload --server URL FILE [--mime-type M]` — authed upload,
  prints the blob URL + sha256.
- `amy blossom download URL|HASH [--out FILE]` — public download (accepts
  a full URL or a HASH + --server).
- `amy blossom list --server URL [USER]` — list a user's blobs (authed).
- `amy blossom delete HASH --server URL` — delete a blob you own.

Reuses commons BlossomClient + BlossomAuth and quartz
BlossomAuthorizationEvent / sha256; list/delete use OkHttp directly with
the quartz-built kind:24242 auth header. Verified a full upload->download
sha256 round-trip against blossom.primal.net and an authed list.

This completes the Tier-1 nak-parity surface (decode/encode/verify/key/
event/publish/fetch/subscribe/count/encrypt/decrypt/gift/filter/relay
info/outbox/blossom); only the kind/nip reference lookups remain.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011SapGdtAc1j7woifoCZ9fY
2026-06-21 17:46:27 +00:00
Vitor PamplonaandClaude Opus 4.8 df1982a7f6 feat(feeds): prefetch media + pre-parse text ahead of the viewport
Warms the next/previous few feed notes off the main thread so media and
link previews are ready before the user scrolls to them, and pre-parses
rich-text bodies into the shared cache so scroll-time composition is a
cache hit instead of a UI-thread parse.

Per upcoming note (off Dispatchers.Default, deduped via a per-feed set,
cancelled on a new visible range):
- Pre-parses TextNote/Comment bodies through CachedRichTextParser using the
  renderer's exact key, so the composition reads a cached parse. Other kinds
  still get their media/links discovered, just without the render-cache warm.
- Prefetches images into Coil — inline content images, NIP-92 imeta blobs,
  NIP-94 file-header url tags, and video poster frames — and records each
  decoded aspect ratio in MediaAspectRatioCache so the box is reserved on
  first layout (no jump). Video poster ratios seed the video URL's box too.
- Warms OpenGraph/link previews via UrlCachedPreviewer.
Gated on showImages()/showUrlPreview() so it honors data-saver/Wi-Fi-only.
Video bytes are deliberately not prefetched (large, HLS, player pool already
starts fast).

Wired centrally at the two feed dispatchers (RenderFeedContentState,
RenderFeedState) so every list feed routed through them is covered without
per-screen wiring, plus direct hooks for the custom-render feeds (hashtag,
profile notes) and a LazyGridState variant for the grid feeds (gallery,
products, discover).

CachedRichTextParser is now content-addressed (memoized contentHash on
ImmutableListOfLists) so an off-thread pre-parse maps to the same entry the
renderer looks up, and its cache grows 50 -> 500 to hold the prefetch +
multi-feed working set.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-21 13:44:39 -04:00
Claude 62d0b16740 feat(cli): add nak-style filter, outbox, relay info primitives
Fifth batch of nak parity:

- `amy filter [filter flags]` — stateless: assemble + print a NIP-01
  filter JSON from the same flags fetch/subscribe use (no query sent).
- `amy outbox USER` — show a user's NIP-65 read/write relays (outbox
  model), cache-first with a relay-drain fallback / --refresh.
- `amy relay info URL` — stateless NIP-11 info-document fetch over HTTP
  (Accept: application/nostr+json), parsed by quartz
  Nip11RelayInformation.

filter + relay info dispatch before account resolution (no ~/.amy
needed). Verified against relay.damus.io (NIP-11 doc) and fiatjaf's
kind:10002 (outbox read/write sets).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011SapGdtAc1j7woifoCZ9fY
2026-06-21 17:42:28 +00:00
Claude 0ec3fc69e8 feat(cli): add nak-style count, encrypt/decrypt, gift primitives
Fourth batch of nak parity:

- `amy count [filter]` — NIP-45 COUNT, per-relay match counts (reuses
  quartz INostrClient.count). Reports per-relay + max as `total`.
- `amy encrypt --to USER [TEXT]` / `amy decrypt --from USER [CIPHER]` —
  raw NIP-44 (default) or NIP-04 (--nip04) with the active account key.
- `amy gift wrap --to USER [EVENT]` / `amy gift unwrap [WRAP]` — NIP-59
  seal+wrap and unwrap+unseal (reuses SealedRumorEvent/GiftWrapEvent).

Text/ciphertext/JSON all read from arg or stdin. Verified with two
local accounts: NIP-44 + NIP-04 round-trips, gift wrap(1059)->unwrap
recovering the inner note + author, and count=60 against relay.damus.io.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011SapGdtAc1j7woifoCZ9fY
2026-06-21 17:38:01 +00:00
Claude afc3c83baa feat(cli): add nak-style fetch + subscribe query primitives
Third batch of nak parity — the fetch-vs-subscribe split of nak's `req`:

- `amy fetch [filter] [--timeout SECS]` — one-shot query: open a
  subscription, collect until every relay sends EOSE (or timeout),
  dedupe by id, sort newest-first, cap at --limit (default 100), exit.
- `amy subscribe [filter] [--timeout SECS]` — live stream: print each
  matching event as it arrives (NDJSON under --json), until timeout or
  interrupt.

Shared filter flags (--kind/--author/--id/--tag/--since/--until/--limit
/--search) are assembled by RawEventSupport.buildFilter; --author/--id
accept npub/nevent/note/naddr or hex (local decode). fetch reuses
Context.drain; subscribe uses the client subscription directly and
verifies each event before printing. Verified against relay.damus.io
(kind:0 by author, kind:1 stream).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011SapGdtAc1j7woifoCZ9fY
2026-06-21 17:34:24 +00:00
Claude b539609dfe feat(cli): add nak-style event + publish primitives
Second batch of nak parity:

- `amy event --kind N [--content …] [--tags JSON] [--created-at TS]`
  builds and signs an arbitrary event with the active account. Prints
  the signed event by default; `--publish` / `--relay` broadcasts it.
- `amy publish [EVENT-JSON] [--relay …]` broadcasts a pre-made, signed
  event (verified before broadcast; reads stdin when no arg).

Both reuse quartz EventTemplate/NostrSigner.sign and the existing
Context.publish path. New RawEventSupport holds the shared arg/stdin +
relay-target helpers for the raw-event verbs. Verified offline via an
event -> verify round-trip.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011SapGdtAc1j7woifoCZ9fY
2026-06-21 17:30:47 +00:00
Claude 48b988d2f7 refactor(cli): simplify amy dispatch, Context lifecycle, and store handling
Four mechanical simplifications to amy with no change to the public
CLI/JSON contract:

1. Drop the Commands.kt pass-through layer. Main.kt now calls each
   command object directly; the file is repurposed into Router.kt,
   holding a single shared `route(name, tail, usage, routes)` helper.

2. Replace the `Context.open(dataDir)` + `try { } finally { ctx.close() }`
   boilerplate (~46 sites) with `Context.open(dataDir).use { ctx -> }`
   now that Context is AutoCloseable.

3. Remove the reflection-based `storeIsInitialized()` in Context; track
   the lazy event store via `Lazy.isInitialized()` instead.

4. Route every `*Commands.dispatch` through the `route` helper, dropping
   the repeated empty-check + unknown-verb `when` boilerplate.

Net -282 lines. Docs (cli/DEVELOPMENT.md, amy-expert skill + template)
updated to the new wiring.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QjEvS812aPLZ6nM2XLobzF
2026-06-21 17:24:00 +00:00
Claude b41160f1cb feat(cli): add nak-style stateless primitives (decode, encode, verify, key)
Add the first batch of nak parity commands to amy — the army-knife
primitives that operate purely on their arguments, with no account or
network. They dispatch before account resolution (like `use`), so they
run with zero `~/.amy/` state:

- `amy decode ENTITY`   NIP-19/21 entity -> JSON
- `amy encode <type> …` raw parts -> NIP-19 entity
- `amy verify [JSON]`   id-hash + signature check (reads stdin)
- `amy key generate|public`  mint a keypair / derive a pubkey

All four are thin wrappers over quartz (Nip19Parser, the NIP-19
entities, Event.verifyId/verifySignature, KeyPair) per the cli
thin-assembly rule. README + ROADMAP updated with a nak-parity matrix.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011SapGdtAc1j7woifoCZ9fY
2026-06-21 17:13:05 +00:00
Vitor PamplonaandGitHub fa11a94613 Merge pull request #3323 from vitorpamplona/claude/powr-workout-interop-r1efh0
Add POWR / NIP-101e strength workout interop (read-only)
2026-06-21 11:52:55 -04:00
Vitor PamplonaandGitHub c503d2ee56 Merge pull request #3324 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2026-06-21 11:40:30 -04:00
Claude 35af6ca580 fix(fitness): store kind-33401 exercise templates in LocalCache
justConsumeInnerInner's when(event) has no generic addressable fallback —
its else branch logs "Event Not Supported" and drops the event. A kind-33401
ExerciseTemplateEvent arriving from a relay was therefore never stored as an
AddressableNote, so the fetched template never attached to its placeholder
note and the workout card's exercise title never resolved. Dispatch it to
consumeBaseReplaceable like the other addressable definitions.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QJwXz6CWez8r7trgHXT545
2026-06-21 15:39:55 +00:00
vitorpamplonaandgithub-actions[bot] 4aa713e2db chore: sync Crowdin translations and seed translator npub placeholders 2026-06-21 15:36:45 +00:00
Vitor PamplonaandGitHub d25f80bcb3 Merge pull request #3321 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2026-06-21 11:35:14 -04:00
vitorpamplonaandgithub-actions[bot] eb04e09d26 chore: sync Crowdin translations and seed translator npub placeholders 2026-06-21 14:32:14 +00:00
Vitor PamplonaandGitHub 7b93bfb5b9 Merge pull request #3313 from vitorpamplona/claude/recommended-apps-search-3628xk
Add search and follow-list filtering to app recommendations
2026-06-21 10:30:39 -04:00
Vitor PamplonaandGitHub 5f2e879d31 Merge pull request #3314 from vitorpamplona/claude/public-chat-notifications-qmn4gh
Notify on public chat replies without p-tags
2026-06-21 10:29:54 -04:00
Vitor PamplonaandGitHub d34453ec17 Merge pull request #3312 from vitorpamplona/claude/share-options-drawer-70nrq4
Extract share options to reusable bottom sheet
2026-06-21 10:27:36 -04:00
Claude 278dac7fea Merge remote-tracking branch 'origin/main' into claude/recommended-apps-search-3628xk
# Conflicts:
#	amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/apps/recommendations/ProfileAppRecommendationsScreen.kt
#	amethyst/src/main/res/values/strings.xml
2026-06-21 14:11:37 +00:00
Vitor PamplonaandGitHub 43f8919ce3 Merge pull request #3322 from vitorpamplona/claude/app-recommendations-search-h8bklq
feat: add a search box to the App Recommendations screen
2026-06-21 09:58:32 -04:00
David KasparandGitHub 4a577485cf Merge pull request #3319 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2026-06-21 11:56:34 +01:00
Claude eaa6c147e4 feat: add a search box to the App Recommendations screen
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
2026-06-21 02:40:00 +00:00
vitorpamplonaandgithub-actions[bot] 3dcc46de2b chore: sync Crowdin translations and seed translator npub placeholders 2026-06-21 02:25:11 +00:00
Vitor PamplonaandGitHub 6227cfd220 Merge pull request #3320 from vitorpamplona/claude/modest-mayer-8mo1zd
feat: add Notification setting to show or hide Messages on the Notification tab
2026-06-20 22:22:58 -04:00
Vitor PamplonaandGitHub da5525c15d Merge pull request #3318 from vitorpamplona/claude/reply-reaction-android-tray-9bkgcx
fix: surface reply, not root, for multi-e-tag reactions in Android tray
2026-06-20 22:21:09 -04:00
Claude 966ba17455 fix: surface reply, not root, for multi-e-tag reactions in Android tray
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
2026-06-21 00:17:46 +00:00
Claude bd8beb43e1 feat: add Notification setting to show or hide Messages on the Notification tab
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
2026-06-20 20:27:32 +00:00
Claude e3d3cebdd4 refactor(notifications): tighten public-chat reply walk and its docs
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
2026-06-20 20:08:08 +00:00
Claude 45656e5476 docs: clarify observeNotes replacement caveat; hoist initial-value scan
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
2026-06-20 20:00:33 +00:00
Claude 8c56bc64c8 feat(fitness): render kind-33401 exercise templates as a card
A standalone POWR exercise template (kind 33401) opened via naddr, a quote,
or in a thread previously fell through to RenderTextEvent, showing only its
bare instruction text with no title. Add ExerciseTemplateDisplay (equipment
icon, title, equipment/difficulty subtitle, instructions via the rich-text
pipeline) and dispatch to it from NoteCompose.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QJwXz6CWez8r7trgHXT545
2026-06-20 19:56:49 +00:00
Claude b835819f97 refactor: de-duplicate shareId and correct ShareActionRows docs
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
2026-06-20 19:37:00 +00:00
Claude a8feb88c2e refactor: derive app list from observeNotes, drop tick + rescan
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
2026-06-20 19:30:33 +00:00
Vitor PamplonaandGitHub cdfe007fb4 Merge pull request #3309 from vitorpamplona/claude/remove-alt-tags-events-786j5p
Remove unused NIP-31 alt tag surrogate code and clean up imports
2026-06-20 15:26:00 -04:00
Claude e2f2c4654e feat(fitness): resolve POWR exercise templates (kind 33401)
Render real exercise names in POWR workout cards instead of slug-derived
labels by fetching the referenced kind-33401 exercise templates.

- ExerciseTemplateEvent (kind 33401, addressable) with title/format/
  format_units/equipment/difficulty accessors; registered in EventFactory.
- WorkoutRecordEvent now implements AddressHintProvider: linkedAddressIds
  (deduped 33401 + 33402 coordinates) seed the gatherer so the card
  re-renders when a template arrives, and addressHints feed the relay-hint
  index with the relay.powr.build hints so the fetch reaches where POWR
  published the templates.
- WorkoutDisplay resolves each exercise via LoadAddressableNote +
  observeNoteEvent<ExerciseTemplateEvent>, showing the template title once
  fetched and the slug until then.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QJwXz6CWez8r7trgHXT545
2026-06-20 19:24:25 +00:00
Claude 2ec5c81fd3 perf(notifications): keep the public-chat reply gate inline so it short-circuits
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
2026-06-20 19:22:23 +00:00
Claude 445a20b2f3 fix: preserve user-provided media descriptions for accessibility
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
2026-06-20 19:19:16 +00:00
Vitor PamplonaandGitHub 6235c407c3 Merge pull request #3311 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2026-06-20 15:16:31 -04:00
Claude 21805f6857 refactor: use indexed observeNewEvents for app definition ticks
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
2026-06-20 19:13:30 +00:00
vitorpamplonaandgithub-actions[bot] 64eeeb84bc chore: sync Crowdin translations and seed translator npub placeholders 2026-06-20 19:11:55 +00:00