Commit Graph
14072 Commits
Author SHA1 Message Date
Claude 5efb5d90e5 feat(amethyst): zap verbs + LLM-friendly kdocs + Gemini discovery plan
Three deliverables:

1) Two new write verbs:
   * zapUser(user, sats, comment?) — builds the NIP-57 kind:9734
     profile zap and fetches a BOLT11 invoice from the recipient's
     Lightning service. Returns the invoice — caller pastes into a
     Lightning wallet (no NWC auto-pay yet). 21 sats default,
     1M sats cap, 280-char comment cap.
   * zapEvent(eventId, sats, comment?) — same but for a specific
     note, with full NIP-57 zap-split support via
     ZapActions.buildEventZapRequestsForSplits. Returns one invoice
     per recipient when the post carries `zap` tags.

   Total verb count: 21 (8 read for feeds/profiles, 3 read for
   identity / followers, 4 read for inbox/zaps/streams, 4 write
   for note/follow/unfollow/dm, 2 write for zaps).

2) Reworked every verb's kdoc first sentence into an LLM-friendly
   "use when..." trigger phrase. Gemini's tool picker matches user
   queries against the descriptions (we generate them via
   @AppFunction(isDescribedByKDoc = true)) — phrasing like "Find a
   person on Nostr by name. Use when the user wants to look someone
   up..." gives the model concrete prompts to recognise instead of
   internal NIP names.

   Affected: searchProfiles, getRecentFromFollows, getNotesByUser,
   getProfile, searchByHashtag, getActiveAccountInfo, getRecentDms,
   getZapsReceived, postNote, followUser, unfollowUser, sendDm,
   zapUser, zapEvent.

3) amethyst/plans/2026-05-26-appfunctions-gemini-discovery.md —
   verification protocol for testing on-device whether Gemini's
   tool picker actually surfaces our verbs from natural-language
   prompts. Includes specific test prompts mapped to expected
   verbs, fallback diagnostics (clear AppSearch + restart), and
   the conditions under which it'd be worth defining our own
   @AppFunctionSchemaDefinition namespace.

Plus minor: comment parameters switched to nullable (String? = null)
because KSP rejects non-nullable types with defaults.
2026-05-26 00:01:13 +00:00
Claude 8e1b31a550 feat(amethyst): four write verbs for Gemini — postNote, follow, unfollow, sendDm
Phase 4 from the signer-prompt plan, scoped to Option B (refuse NIP-55
with a typed NotSupportedException). Internal-key and NIP-46 bunker
accounts can now publish from Gemini.

New @AppFunction methods:

  * postNote(text) — kind:1 short text note. Caps at 8000 chars to
    catch accidentally-pasted documents; publishes to outbox relays
    with per-relay ack reported.

  * followUser(user) / unfollowUser(user) — kind:3 contact list
    update via FollowActions. Detects already-following / not-
    following and returns WriteResult.unchanged() rather than
    re-publishing the same kind:3. New follows stamp the relay hint
    from the target's cached kind:10002 write list, mirroring
    User.bestRelayHint().

  * sendDm(recipient, text) — NIP-17 gift-wrap via DmActions.buildTextDm.
    Resolves per-recipient relay set through DmActions.resolveDmRelays
    (permissive mode — falls back through NIP-65 read to bootstrap so
    Gemini users don't trip on the strict kind:10050 rule). Returns
    one DmDelivery per wrap (recipient + sender's own copy).

Signer gating — requireInProcessSigner():
  * Read-only signers (npub-only login) → AppFunctionNotSupportedException
    "sign in with a private key or NIP-46 bunker to publish".
  * NIP-55 external signers (Amber) → AppFunctionNotSupportedException
    "open Amethyst directly to complete the action". Detected via
    qualified class name to avoid hard-coupling the bridge to the
    nip55AndroidSigner module.
  * NostrSignerInternal / NostrSignerRemote — sign in-process; the
    NIP-46 round-trip already suspends through .sign(), no special
    handling needed.

New @AppFunctionSerializable types:
  * WriteResult — { changed, eventId?, publishedTo, rejectedBy }
  * SendDmResult — { messageEventId, deliveries: List<DmDelivery> }
  * DmDelivery — { recipientNpub, recipientPubkeyHex, wrapId,
                   publishedTo, rejectedBy, relaySource }

All 19 verbs now registered in the generated dispatcher (15 read + 4
write). app_metadata.xml updated so Gemini's tool picker pitches the
broader surface, including the NIP-55 caveat.
2026-05-25 23:41:58 +00:00
Claude ac105ca2f3 chore(amethyst): enrich AppFunction outputs so Gemini can name names
Every verb that returned a pubkey now also returns the best-effort
display name from the local kind:0 cache. Before this commit Gemini
could only say "you got a DM from npub1abc…" — now it can say
"you got a DM from Alice" because the LLM has the field at hand
instead of having to chain another lookup.

  * NoteHit gains authorDisplayName (cache-resolved, null when the
    author's kind:0 isn't local yet). Applied to every verb that
    returns notes: searchNotes / getRecentFromFollows / getNotesByUser /
    searchByHashtag / getMyRecentNotes / getMyMentions /
    getRepliesToNote / searchArticles.

  * DmMessage gains fromDisplayName + sentByMe — the latter lets
    the caller distinguish "Alice said X" from "I said Y" when both
    appear in the same thread snapshot.

  * LiveStreamHit gains streamingUrl (was missing entirely — without
    it the verb is useless, you can't watch a stream you can't open)
    plus hostDisplayName.

  * getProfile cache-hit path now actually populates `about` — was
    silently null before because the early-return branch didn't read
    it out of UserInfo. Cache-miss path was always correct.

Implementation: one `displayNameOf(HexKey): String?` helper reads from
Amethyst.instance.cache (LocalCache) — the same cache the foreground
UI uses. Zero allocations beyond the lookup, no network round-trip.
2026-05-25 23:11:10 +00:00
Claude c10ed49631 feat(amethyst): Tier 2+3 read-only Gemini verbs — 15 total
Now exposing the full read-only Nostr surface to Gemini. Seven new
@AppFunction methods on top of the previous eight:

  * getMyRecentNotes(limit) — author=me filter on kind:1.

  * getMyMentions(limit) — p-tag=me filter on kind:1. "Did anyone @ me?".

  * getRepliesToNote(eventId, limit) — e-tag=eventId filter on kind:1.
    Pair with getMyRecentNotes(1) for "did anyone respond to my last post?".

  * getZapsReceived(hoursBack) — drains kind:9735 receipts addressed to
    the user in the window, parses the bolt11 invoice from each, sums
    sats. Returns total + zap count + unique zappers + count of
    receipts whose bolt11 was unparseable.

  * getRecentDms(peer?, hoursBack, limit) — NIP-17 gift-wrap drain +
    unwrapAndUnsealOrNull decrypt. kind:14 text DMs only for v1 (skip
    kind:15 encrypted-file headers to keep payloads bounded). Widens
    the `since` filter by 2 days for NIP-59's randomised-past
    created_at trick, then trims back to the requested window.

  * searchArticles(query, limit) — same as searchNotes but kind:30023
    long-form articles. Content snippet truncated at 2000 chars so a
    book-length article doesn't blow up the AppFunctions response;
    Gemini can ask the user whether to fetch the full article via a
    different verb.

  * getLiveStreams(limit) — NIP-53 kind:30311 with status=live (uses
    quartz's 8-hour staleness guard via LiveActivitiesEvent.isLive).
    Returns title, summary, host npub, start time, event id.

Plus updated res/xml/app_metadata.xml so Gemini's tool picker pitches
the full surface to users.

KSP-verified — 15 verbs total in the generated dispatcher:

    getActiveAccountInfo  getFollowing            getLiveStreams
    getMyMentions         getMyRecentNotes        getNotesByUser
    getProfile            getRecentDms            getRecentFromFollows
    getRepliesToNote      getZapsReceived         searchArticles
    searchByHashtag       searchNotes             searchProfiles

Write verbs (post, follow, zap, sendDm) still deferred behind the
signer-prompt plan in amethyst/plans/2026-05-25-appfunctions-signer-prompts.md
— no behavior change there.
2026-05-25 23:06:47 +00:00
Claude 5c46d0a7b3 feat(amethyst): five more read-only Gemini verbs — full Tier 1 read surface
After the on-device round-trip proved the AppFunctions plumbing works,
adding the verbs that make Gemini actually useful for a Nostr user.
All read-only, no signer interaction, all build on existing actions /
Account state.

  * getRecentFromFollows(limit) — "what's happening on Nostr today?"
    Drains recent kind:1 from people the user follows; same relay set
    the home-feed UI uses (account.homeRelays).

  * getNotesByUser(user, limit) — "what did Vitor post recently?"
    Accepts npub or 64-hex. Prefers the target's NIP-65 write relays
    when cached, falls back to the active account's home relays.

  * getProfile(user) — "who is npub1xq5...?". Cache-first via
    LocalCache; falls back to a short network drain for unseen users.
    Returns GetProfileResult{found, profile} so callers know whether
    the user just isn't in cache or doesn't have a kind:0 yet.

  * searchByHashtag(hashtag, limit) — "find Nostr posts about Bitcoin".
    NIP-12 `t` tag filter, lowercased to match the client convention.

  * getActiveAccountInfo() — "who am I logged in as?" Diagnostic verb
    returning npub, display name, follow count, outbox + DM relay
    counts. Distinguishes signed-in from signed-out via a flag rather
    than a magic empty result.

Plus:
  * decodeUserOrThrow helper for npub/hex parsing, throws
    AppFunctionInvalidArgumentException with a typed message so
    callers see "expected npub1… or 64-char hex" instead of a stack.
  * TextNoteEvent.toNoteHit helper — extracted from the existing
    searchNotes path to avoid duplication.
  * Updated res/xml/app_metadata.xml description so Gemini's tool
    picker can pitch a broader summary to the user.

KSP-verified: $AmethystAppFunctions_AppFunctionInvoker now dispatches
all eight verbs (the three from the previous commits plus these five).
2026-05-25 22:58:30 +00:00
Claude 0619788644 fix(amethyst): supply app_metadata so AppFunctions discovery actually works
After fixing the missing aggregated XML, Pixel 8 logcat still showed:
  D AppFunctions: Unable to resolve AppFunctionMetadata.

Comparing against Google's FilipFan/AppFunctionsPilot sample turned up
a separate metadata pointer the system requires:

  <property
      android:name="android.app.appfunctions.app_metadata"
      android:resource="@xml/app_metadata" />

This goes on the <application> element (not the service) and points to
an XML resource — distinct from the asset-side `app_functions.xml`
that the library auto-merges onto the service. The asset metadata
declares "here are my function ids and schemas"; the resource
metadata gives the agent a user-facing summary like "Search Nostr and
read your follows" to show users before they grant access.

Without the resource, the system can find our service and our
function list but can't resolve the descriptive metadata it shows
the user — so Gemini's tool picker stays empty.

Two new files:
  * amethyst/src/play/res/xml/app_metadata.xml — short description +
    displayDescription. Update when the @AppFunction surface grows.
  * play AndroidManifest <property> pointing at the resource.

Also dropped our explicit <service> declaration for
PlatformAppFunctionService — confirmed via the appfunctions-service
AAR that the library auto-merges that exact entry, complete with
permission + intent-filter, so our copy was redundant.
2026-05-25 22:03:46 +00:00
Claude 82188b719a fix(amethyst): generate app_functions.xml so the system can resolve metadata
The androidx.appfunctions-compiler runs in per-module mode by default,
emitting only the dispatcher Kotlin code. The aggregator that builds
the `app_functions.xml` + `app_functions_v2.xml` assets is gated behind
a KSP argument that was off.

Symptom on a Pixel 8 running our APK:
  D AppFunctions: Unable to resolve AppFunctionMetadata.

Without the aggregated asset, the manifest's
`android.app.appfunctions` property pointed at a file that didn't
exist; the System UI couldn't enumerate our @AppFunction methods so
Gemini's tool picker never saw them.

Setting `appfunctions:aggregateAppFunctions = "true"` on the amethyst
module turns the aggregator on. Verified post-build:
  assets/app_functions.xml           (688 bytes — manifest pointer + ids)
  assets/app_functions_v2.xml        (19.7 KB — full schemas + kdoc descriptions)
Both list searchProfiles / searchNotes / getFollowing with the kdoc
descriptions Gemini will render.

Library modules (commons/quartz) would set this to "false" — only the
final app emits the aggregate. We don't currently apply the KSP plugin
in any library module, so this is the only place that matters.
2026-05-25 21:50:56 +00:00
Claude 95ca12231c feat(amethyst): two more read-only Gemini verbs + signer-prompt plan
Read-only surface for the Gemini bridge now covers profiles, notes, and
the active account's follow set:

  * searchNotes(query, limit) — NIP-50 search over kind:1 short text
    notes via SearchActions.searchNotesFilter + INostrClient.fetchAll.
    Returns NoteHit list (eventId, npub, content, createdAt). alpha09
    of androidx.appfunctions doesn't support List<Int> parameters, so
    no caller-configurable kinds — kind:1 only for now.

  * getFollowing(limit) — reads account.kind3FollowList.userList.value
    (already resolved through LocalCache) and projects to FollowedUser
    with display-name / nip05 / picture from cached kind:0. Reports
    totalFollowing so the caller knows when limit truncated the list.

Both verbs follow the searchProfiles pattern: snapshot active account +
client at entry, never re-query sessionManager during the dispatch.

Plus amethyst/plans/2026-05-25-appfunctions-signer-prompts.md —
design plan for write verbs. Three signers (Internal / Remote /
External), three different latency + interaction models. Concrete
proposal: Internal first via postNote pilot, Remote as a follow-up,
External via PendingIntent (Option A) or NotSupportedException
(Option B — recommended for v1) depending on what bundle keys the
system shell respects. Open questions enumerated so the experiment
day is bounded.
2026-05-25 00:18:31 +00:00
Claude 31cfb53b25 feat(commons): extract NIP-17 DM verbs into shared actions package
Fourth verb extraction alongside FollowActions / SearchActions /
ZapActions. Closes the largest remaining amy-expert "thin assembly"
violation in cli/.

Two pieces moved out of cli/.../DmCommands.kt into commons:

  * DmActions.resolveDmRelays applies the strict-kind:10050 → NIP-65-
    read → bootstrap fallback policy the in-app flow uses. Returns a
    DmRelaySet with a typed RelaySource (KIND_10050 / NIP65_READ /
    BOOTSTRAP / NONE) so callers can surface the source — amy emits
    it on stdout, a future Gemini adapter could mention it in the
    assistant response.

  * DmActions.buildTextDm / buildFileDmReference are thin wrappers
    over NIP17Factory.createMessageNIP17 / createEncryptedFileNIP17
    that build the kind:14 / kind:15 template and gift-wrap in one
    call. Matches the FollowActions / ZapActions builder shape.

amy's DmCommands is now genuinely thin assembly: requireUserHex,
flag plumbing, call DmActions, render JSON. The 583-line file shrank
slightly and — more importantly — no longer carries NIP-17 logic
the rest of the codebase needs to look at.

Receive-side decrypt loop (3 lines of unwrapAndUnsealOrNull) stays in
amy; too small to extract and tightly coupled to amy's per-relay
attribution.

10 new tests for DmActions: strict/permissive fallback chain, null
recipient lists, RelaySource enum stability, and a smoke test that
buildTextDm produces a kind:14 with the right wrap count (sender +
recipient).
2026-05-24 23:45:33 +00:00
Claude 86434ea550 refactor(amethyst): use INostrClient.fetchAll instead of inlining the drain loop
quartz already ships an extension that does exactly what
AmethystAppFunctions.drain reimplemented — subscribe with the given
filters, collect events until every relay sends EOSE / closed /
cannot-connect or the timeout elapses, unsubscribe, dedup by id,
return sorted newest-first. See
quartz/.../relay/client/accessories/NostrClientFetchAllExt.kt.

Replacing the local drain with `client.fetchAll(filters, timeoutMs)`
trims 70+ lines of subscription listener boilerplate and gives the
adapter the same behavior the rest of the codebase already trusts.

amy's Context.drain stays — it adds per-event signature verification
and persistence to the file event store (the trust boundary for amy)
that fetchAll doesn't do.
2026-05-24 23:16:12 +00:00
Claude b150556f1e refactor(amethyst): drop PlayAmethyst — appfunctions doesn't need it
The previous wiring forced Amethyst to be `open`, added a 30-line
PlayAmethyst subclass that only implemented AppFunctionConfiguration
.Provider, and used tools:replace="android:name" in the play manifest
to swap classes. The justification was that the appfunctions runtime
discovers @AppFunction host classes via Application.appFunctionConfiguration.

Reading the KSP-generated dispatcher
($AmethystAppFunctions_AppFunctionInvoker.kt) shows that's only half
true. The invoker passes a default-construction fallback lambda when
instantiating the host class, and ConfigurableAppFunctionFactory takes
that fallback as a constructor argument. Provider is only consulted to
*override* construction — required for classes with non-default
constructors, optional otherwise.

AmethystAppFunctions has a no-arg constructor, so:
  * PlayAmethyst is deleted entirely
  * Amethyst goes back to `class Amethyst : Application()` (no `open`)
  * Play manifest reverts to plain `android:name=".Amethyst"`, no
    tools:replace gymnastics

Verified by assemblePlayDebug (APK builds clean) and the merged play
manifest still pinning the appfunctions service. If we ever add a
host class with constructor parameters (an Account-injected one, say),
we'll need to add Provider back — kdoc on AmethystAppFunctions
documents that.
2026-05-24 23:10:37 +00:00
Claude 29236d7801 chore(commons,cli,amethyst): three correctness wins + caller-responsibility kdoc
Closes the remaining items from the comparative review of the extracted
actions against the in-app Amethyst flows. All small, all surfaced by the
review.

  * amy follow now stamps the relay hint on new contact-list `p` tags.
    Best-effort read from the target's cached kind:10002 advertised
    relay list (first writeRelaysNorm). Mirrors User.bestRelayHint() —
    follows added via amy no longer have empty relayUri.

  * amy search user now dedups by pubkey (sorted newest-first) instead
    of by event id, matching the App Functions adapter. Multiple relays
    surfacing different kind:0 revisions for the same author collapse
    to one hit.

  * AmethystAppFunctions.searchProfiles captures the active account AND
    the relay client at function entry, then never touches sessionManager
    or Amethyst.instance again during the drain. Closes the account-
    switch race surfaced in the review.

  * FollowActions / SearchActions / ZapActions kdoc now lists the
    caller-side responsibilities each builder leaves to the consumer
    (publish, writeable check, relay hint, pseudo-kind filtering,
    LN round-trip, receipt verification, etc.). Documents the design
    rather than letting it leak through reviews.
2026-05-24 21:35:49 +00:00
Claude 54b09ea6e2 fix(commons): split-aware zap requests stop misrouting funds on multi-party notes
The previous ZapActions.buildEventZapRequest signed a single zap request
to a single recipient. Notes carrying NIP-57 zap-split tags, NIP-53
live-activity host tags, or NIP-89 app-definition metadata expect the
payment to be distributed across multiple parties — so `amy zap event`
silently overpaid one party and underpaid the rest. The correctness
review on the action-set flagged this as the only real bug in the
extracted verbs; this commit fixes it.

  * ZapSplitResolver — new commonMain object mirroring the resolution
    order in ZapPaymentHandler.kt (splits > live-activity hosts > app
    metadata > author fallback). Pure logic; pubkey→LN-address lookup
    is passed in as a suspend lambda so amy reads from its file store
    and Android reads from LocalCache, no shared cache-coupling.

  * ZapActions.buildEventZapRequestsForSplits — high-level helper that
    composes the resolver with per-share LnZapRequestEvent signing.
    Each request's `relays` tag unions sender + author + recipient
    inbox relays so the kind:9735 receipt routes to every interested
    party (matches signAllZapRequests in the Android handler).

  * amy zap event — rewired to the split-aware path. JSON output now
    enumerates each recipient with its share, LN address, request id,
    and BOLT11 invoice (or per-recipient invoice_error). Profile zaps
    (amy zap user) keep the simple single-recipient path since they
    have no split tags.

Tests: 12 new cases — LN-address splits, weighted pubkey splits, author
fallback, drop-silently-on-missing-LN, relay unioning, share rounding.
All 41 action tests green; both Android flavors compile.
2026-05-24 21:10:30 +00:00
Claude 17cee60aac feat(amethyst): expose searchProfiles to Gemini via androidx.appfunctions
First Phase 2 verb wired through to the Android App Functions runtime so
Gemini (and other system agents) can drive Amethyst.

Scope is intentionally narrow:
  * One read-only verb (searchProfiles), built on top of the existing
    SearchActions in commons. No write verbs yet — they need a story
    for NIP-46 / NIP-55 signer prompts from a background dispatcher.
  * Play channel only. appfunctions 1.0.0-alpha09 is a Google AI alpha;
    F-Droid builds continue to ship without any Google AI dependencies.

Architecture:
  * AmethystAppFunctions — plain Kotlin host with @AppFunction methods.
    The KSP-driven appfunctions-compiler discovers them and generates
    the dispatch metadata XML at build time.
  * PlayAmethyst — play-only Application subclass implementing
    AppFunctionConfiguration.Provider; supplies the factory the
    library uses to construct the host class. Manifest replaces
    android:name in the play flavor only; F-Droid keeps the unmodified
    Amethyst class.
  * The androidx-provided PlatformAppFunctionService is registered in
    the play manifest as the bind point — Amethyst doesn't ship a
    custom Service.

KSP is now a project-wide plugin (apply false at the root); applied in
amethyst/ to run the appfunctions-compiler over the play sourceSet.

Amethyst becomes `open class` so PlayAmethyst can extend it. No other
behavior change.
2026-05-24 18:23:59 +00:00
Claude 2e47cb7110 feat(commons): add NIP-57 zap verbs in shared actions package
Third verb extraction alongside FollowActions / SearchActions, scoped
to event building so the action stays target-agnostic (commonMain,
no JVM/Android coupling).

  * buildUserZapRequest / buildEventZapRequest wrap the two
    LnZapRequestEvent.create overloads with a uniform call shape and
    sensible defaults (PUBLIC zap, no LNURL, no poll).
  * extractLnAddress pulls lud16 (preferred) or lud06 from a kind:0
    metadata event, returning null when neither is set.
  * satsToMillisats covers the sats→msats conversion that every
    caller would otherwise duplicate.

Wires up amy zap user|event as the first consumer. The Lightning
round-trip (LNURL fetch + invoice retrieval) goes through the existing
LightningAddressResolver in commons/jvmAndroid; the BOLT11 invoice is
printed but not auto-paid since amy has no NWC wallet wired up yet.
2026-05-24 16:33:15 +00:00
Claude cde609203c feat(commons): add NIP-50 search verbs in shared actions package
Introduce SearchActions alongside FollowActions as the second of the
shared "verbs" usable by amy CLI and a future Android App Functions
adapter for Gemini.

  * searchProfilesFilter / searchNotesFilter build the relay-side
    Filter with the NIP-50 `search` field set; blank queries return
    null so callers don't issue unconstrained searches that relays
    would reject anyway.
  * resolveSearchRelays picks the caller's kind:10007 list when
    configured (decrypting NIP-44 private entries via the signer) and
    falls back to DefaultSearchRelayList — the same set the Android UI
    uses when the user has no list of their own.

Wires up amy search user|note as the first consumer.
2026-05-24 16:23:34 +00:00
Claude 257756438d feat(commons): extract follow/unfollow verbs into shared actions package
Introduce commons/.../actions/FollowActions as the canonical, non-UI
entry point for NIP-02 kind:3 mutations. Accepts pubkeys as HexKey
rather than the Compose-bound User model, so callers without a cache
(amy CLI, future Android App Functions adapter for Gemini, automation
scripts) can drive follow/unfollow directly.

Kind3FollowListState.follow/unfollow now delegate to FollowActions,
preserving the existing Account.follow(user) signature on Android.
Behavior is unchanged for UI callers.

Wires up amy follow/unfollow as the first consumer — fetches the
freshest kind:3 from outbox relays before mutating so concurrent
follows from another client are preserved.
2026-05-24 16:04:20 +00:00
Vitor PamplonaandGitHub 74a646c7eb Merge pull request #3040 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2026-05-23 17:30:01 -04:00
Crowdin Bot 05eb35a4ca New Crowdin translations by GitHub Action 2026-05-23 21:14:31 +00:00
Vitor PamplonaandGitHub 4758562f88 Merge pull request #3043 from vitorpamplona/claude/multi-npub-external-signer-login-U5GIb
Fix account cache race condition in setDefaultAccount
2026-05-23 17:12:55 -04:00
Vitor PamplonaandGitHub d82143e392 Merge pull request #3042 from vitorpamplona/claude/reaction-row-padding-bug-DaQO0
Fix reaction row layout for icon-only rightmost items
2026-05-23 17:09:55 -04:00
Claude 99b7ca76be fix: only skip the weighted slice for an icon-only last reaction
The previous attempt weighted every item, which made even Share collapse
to the left of its slice instead of pinning to the right edge.

Restore the natural-width carve-out for the last item, but gate it on
`!showCounter` — Share/Pay have no counter so they stay flush against
the right padding as before; Zap/Like/etc. become weighted when last so
the counter doesn't sprawl out to the edge and the row stays balanced.
2026-05-23 21:06:42 +00:00
Vitor PamplonaandGitHub 2ef738de14 Merge pull request #3039 from vitorpamplona/claude/fix-zaps-display-tHV2a
NIP-BC onchain zaps: add verification state machine & reverify driver
2026-05-23 17:04:22 -04:00
Claude e5b0755d9b fix: secondary external-signer login lands on onboarding when switching
LocalPreferences.setDefaultAccount called setCurrentAccount before
saveToEncryptedStorage. setCurrentAccount emits the new list onto the
savedAccounts MutableStateFlow, which AlwaysOnNotificationServiceManager
collects and reacts to by calling loadAccountConfigFromEncryptedStorage
for every saved account — including the just-added one. That call hit
encryptedPreferences(newNpub) before NOSTR_PUBKEY had been written, got
null, and cached the null in cachedAccounts.

cachedAccounts is a process-lifetime map, so the poisoned entry survived
the eventual disk write. Every subsequent switchUser to that account
took the cached null path, fell through to requestLoginUI(), and AccountScreen
rendered LoggedOffSetup — the onboarding screen with TOS unchecked, asking
the user to re-do the Amber handshake.

Write the per-npub file first, then seed the cache with the in-memory
AccountSettings, then publish onto the savedAccounts flow. Also stop
caching null returns in loadAccountConfigFromEncryptedStorage so any
future racy reader can't poison the cache either.
2026-05-23 20:16:13 +00:00
Claude 18fb75285e fix: keep reaction icons evenly distributed when Share is disabled
The reaction row gave every item except the last a `Modifier.weight()`,
which made the last item collapse to its natural width and hug the right
edge of the content area. With Share (icon-only) as the default last
item, all icons appeared evenly distributed.

When the user disabled Share, the last weighted slot moved to Zap. Zap
renders icon + counter, so its natural-width row took more space at the
right and pulled the rightmost icon away from where the other icons sat
(each at the left of a now-wider weighted slice), leaving the row
looking unbalanced.

Give every reaction an equal weighted slice so icons sit at the left of
their slice regardless of which reactions are enabled. The unused space
at the end of the last slice naturally provides the right-side padding
where Share used to sit.
2026-05-23 19:27:19 +00:00
Claude daa83959b6 refactor(onchain-zaps): extract verification coordinator from LocalCache
Moves the asynchronous chain-verification side of NIP-BC onchain zaps out of
LocalCache into a dedicated OnchainZapResolver class living alongside other
NIP-specific subpackages under model/nipBCOnchainZaps/. LocalCache shrinks by
~240 lines and now owns only the synchronous event-dispatch responsibility:
loading the event, attaching the optimistic UNVERIFIED entry for the sender's
own zap, and delegating the verifier launch to the resolver.

The resolver owns:
- launchVerification(event, source, repliesTo) — async fire-and-forget
- reverifyOnchainZapsForNote(note) — used by the gallery's screen-driven loop
- onchainTipHeightFlow — shared chain-tip poller, lazy + WhileSubscribed
- verifyingEventIds / reverifyingNoteIds — in-flight de-duplication
- reverifySemaphore — parallelism cap

OnchainZapGallery now calls LocalCache.onchainZapResolver.{reverifyOnchainZaps
ForNote, onchainTipHeightFlow} directly. consume(OnchainZapEvent) passes the
already-computed repliesTo into launchVerification so the new-event path
doesn't recompute it on the verifier side.

No behavior change — all 22 onchain-zap tests still pass.
2026-05-23 16:13:19 +00:00
Vitor PamplonaandGitHub fc5587f46f Merge pull request #3038 from nrobi144/feat/desktop-wallet-zapping
feat(desktop): wallet zapping, LNURL-pay send, QR receive, and session persistence
2026-05-23 12:05:56 -04:00
Vitor PamplonaandGitHub 0461072254 Merge pull request #3041 from vitorpamplona/claude/jolly-cray-6vKga
Makes the NWC process less strict, while checking for inconsistencies after the request reply is processed.
2026-05-23 12:05:09 -04:00
Vitor PamplonaandGitHub fc7813afbb Merge pull request #3036 from vitorpamplona/claude/affectionate-gauss-UWDJ4
Add NIP-82 Software Applications support with dedicated feed
2026-05-23 12:00:08 -04:00
nrobi144andClaude Opus 4.6 e4691f6d93 fix(desktop): remove obsolete ZapDialogLogicTest
Test referenced formatSats, DEFAULT_ZAP_AMOUNTS, and ZapType which
were removed/made private in upstream merge.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-23 15:45:54 +03:00
nrobi144andClaude Opus 4.6 2b14b77acf feat(desktop): support LNURL-pay and lightning addresses in send dialog
Rewrite SendDialog with sealed state machine that auto-detects input
type (BOLT11, LNURL bech32, lightning address). For LNURL/address:
resolves endpoint, shows amount form with min/max hint, optional
comment field, fetches invoice, then pays via NWC. Strips lightning:
URI prefix. Inline copiable errors with retry.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-23 15:40:19 +03:00
nrobi144 a5405fef34 Merge remote-tracking branch 'upstream/main' into feat/desktop-wallet-zapping
# Conflicts:
#	desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/FeedScreen.kt
#	desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/NoteActions.kt
2026-05-23 15:19:47 +03:00
nrobi144andClaude Opus 4.6 4936d187fe fix(desktop): improve send/receive dialogs and LNURL error surfacing
SendDialog: switch to Dialog+Card with X close, inline copiable error
messages, button resets to "Pay Invoice" on error for retry.

LightningAddressResolver: return error body from callback responses so
server error messages (e.g. "Recipient wallet error") surface to user
instead of generic "Failed to fetch invoice". Also check "message"
field in addition to "reason" for error extraction.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-23 15:17:08 +03:00
nrobi144andClaude Opus 4.6 00708d92d3 feat(desktop): redesign receive dialog with QR code and cleaner UX
Replace AlertDialog with Dialog+Card pattern. Invoice created state now
shows centered amount, description, 240dp QR code, and full-width
"Copy Invoice" button. Close via top-right X button. Input form gets
full-width "Create Invoice" button.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-23 14:35:18 +03:00
nrobi144andClaude Opus 4.6 562cd3355b fix(desktop): fix feed cold-boot race and remove NWC diagnostic println
Add LaunchedEffect that rescans cache when followedUsers populates after
startup, fixing empty feed when contact list arrives after initial scan.
Remove diagnostic println from NwcPaymentHandler.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-23 14:02:48 +03:00
nrobi144andClaude Opus 4.6 e690292bbd fix(desktop): fix nsec session not persisting across restarts
LoginScreen's fire-and-forget save coroutine used rememberCoroutineScope
which got cancelled when the composable left composition after login.
Move saveCurrentAccount() to onLoginSuccess in Main.kt which uses the
app-level scope that survives recomposition. Fixes both nsec login and
generate-new-account flows.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-23 14:02:36 +03:00
Vitor PamplonaandGitHub 4391fae915 Merge pull request #3037 from vitorpamplona/claude/pin-followed-chats-iSRJ9
feat: pin followed public chats to the top of the Public Chats feed
2026-05-22 20:00:53 -04:00
Claude 0313dcf3fa fix(onchain-zaps): clear second-audit findings
Addresses the 15 issues from the second audit pass. Key changes:

- Per-event resolution flag (`Note.onchainZapResolved`) replaces the unbounded
  rejection blocklist. The flag is set on terminal verifier verdicts
  (Confirmed or hard-Rejected) and gates the verifier launch in `consume()`.
  Travels with the Note so it clears on `removeAllChildNotes()`.

- Per-event in-flight set (`verifyingEventIds`) deduplicates concurrent
  verifier launches across `consume()` echoes and `reverifyOnchainZapsForNote`
  races. Solves: profile-only zaps bypassing the all-CONFIRMED guard,
  Rejected entries re-firing the verifier on every echo, and the
  consume()/reverify TOCTOU race.

- Per-note reverify gate (`reverifyingNoteIds`) prevents multiple visible
  galleries from launching concurrent reverify passes for the same note.

- `removeOnchainZapForSource` now refuses to remove a CONFIRMED entry — only
  an explicit fresh CONFIRMED replacement can change one. Prevents the
  cross-target downgrade where one target's transient ZERO_VERIFIED_AMOUNT
  erases a sibling target's already-confirmed entry. Also non-nullable
  pubkey parameter to close the null-vs-null comparison hole.

- `innerAddOnchainZap` dedup tightened: exact structural equality skips
  spurious flowSet invalidations on relay echoes, but same-level + equal
  verifiedSats from a DIFFERENT source now replaces (fixes multi-signer
  attribution lock-in).

- Tip flow uses explicit try/catch that re-throws CancellationException
  instead of `runCatching` (same fix the previous audit applied to the
  verifier). Lazy initializer falls back to a constant-null StateFlow if
  `Amethyst.instance` isn't initialized yet, instead of throwing.

- Gallery driver: unconditional first-view kick (no longer waits for the
  tip flow's first non-null emission), separate effect keyed on pending
  entry count so a fresh UNVERIFIED arrival kicks reverify immediately
  instead of waiting up to 60s for the next tip poll.

- `observeNoteZaps`'s memoization now keys on the `onchainZaps` map
  reference so lightning-zap traffic on the same note doesn't churn the
  onchain gallery.

- `reverifyOnchainZapsForNote` uses `supervisorScope` so a single failed
  verifier doesn't cancel its siblings, and the semaphore permits bump
  from 4 → 8 reduces head-of-line blocking when many galleries reverify
  concurrently.
2026-05-22 22:42:25 +00:00
Vitor Pamplona 585b28163a Better rendering of Public Chats 2026-05-22 18:25:07 -04:00
Vitor PamplonaandGitHub 2c8ed6c64f Merge pull request #3031 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2026-05-22 18:22:46 -04:00
Crowdin Bot a699920e96 New Crowdin translations by GitHub Action 2026-05-22 22:06:54 +00:00
Vitor PamplonaandGitHub 653ca7ce88 Merge pull request #3034 from nrobi144/feat/desktop-note-action-ux
feat(desktop): note action bar — long-press details popups + right-click customize
2026-05-22 18:05:22 -04:00
Vitor PamplonaandGitHub ae56a295d4 Merge pull request #3035 from greenart7c3/claude/epic-newton-OZLCC
Use URL SHA for Blossom bridge, not imeta hash
2026-05-22 14:53:00 -04:00
Claude 862dce27fe fix(blossom-bridge): always use URL sha, ignore imeta x
On resizing CDNs the imeta `x` (post-resize hash) can differ from the
`ox` (original hash) embedded in the URL. The bridge previously preferred
`explicitHash` over the URL's sha for "authoritative casing", but the
upstream file on `xs` is named after the URL's sha, not the imeta hash.
For URLs like https://image.nostr.build/<ox>.png with imeta x=<post-resize>
the cache would request /<x>.png and 404 on miss.

Always use the sha parsed from the URL path; drop the explicitHash
parameter. `extractSha256FromUrlPath` already lowercases, so the casing
concern is moot.
2026-05-22 16:23:34 +00:00
nrobi144andClaude Opus 4.6 3185df21b6 fix(desktop): reactive counters, quote boost, and boost detail popup
- Add kind 1 (replies) to interaction subscriptions
- Key count reads on FlowSet state for reactive updates
- Wire Quote menu item to ComposeNoteDialog with q-tag support
- Add BoostsPopup on long-press repost icon (who boosted)
- ComposeNoteDialog now accepts quoteOf param with nostr: URI pre-fill

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-22 14:03:48 +03:00
nrobi144andClaude Opus 4.6 27543ac304 feat(desktop): long-press details popups + right-click customize for note actions
- Long-press zap icon → floating popup with zap receipts (sender, amount, message)
- Long-press like icon → floating popup with reactions grouped by emoji
- Right-click like icon → emoji picker (DropdownMenu with 6 common emojis)
- Right-click repost icon → Repost/Quote options (DropdownMenu)
- Right-click zap icon → custom zap dialog (preserved existing behavior)
- Long-press reply → opens thread (same as click)
- ActivePopup sealed class ensures only one popup open at a time
- Popup + ElevatedCard for rich content, DropdownMenu for option lists
- combinedClickable with explicit ripple preserves IconButton UX
- PopupProperties(focusable = true) for desktop click-outside dismiss
- @Immutable on ZapReceipt for Compose stability
- Note param added to NoteActionsRow, passed from FeedScreen

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-22 07:00:31 +03:00
nrobi144andClaude Opus 4.6 746dab51b4 fix(desktop): fix NWC relay connection, disconnect crash, and balance error handling
- Remove premature ensureRelayConnected check — NostrClient connects
  on subscribe/publish via sendOrConnectAndSync
- Fix disconnect crash: use appScope instead of rememberCoroutineScope
  to survive recomposition when nwcConnection goes null
- Surface balance errors/timeouts as snackbars instead of silent swallow
- Add ensureRelayConnected helper to RelayConnectionManager
- Add Phase 2 embedded wallet research doc

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-22 06:23:05 +03:00
Claude a338574f44 fix(nwc): surface rejected spoof replies in timeout message
When a kind-23195 event arrives signed by someone other than the wallet
service we sent the request to, we now count it on the pending entry and
leave the entry in place so the legitimate reply can still resolve. But
if no legitimate reply arrives and the 30s timeout fires, the user used
to see a generic "Wallet request timed out" — indistinguishable from
"the wallet is just slow", even when an active attacker was forging
replies and dropping the real ones.

Carry the per-request spoof count through to the timeout error message:

  - NwcPaymentTracker.PendingRequest gains an AtomicInteger spoofAttempts.
    onResponseReceived increments it on WrongAuthor.
  - New tracker method spoofAttemptsFor(requestId) reads the count.
  - Account exposes nwcSpoofAttempts() and cleanupNwcRequest() so the
    UI doesn't need to reach into LocalCache.
  - Account.sendNwcRequestToWallet now returns the request event id so
    callers can identify the pending entry.
  - WalletViewModel.launchTimeout takes a () -> HexKey? provider and
    fetches the spoof count when the timeout fires. The error becomes
    "Wallet request timed out — N replies were rejected because they
    were signed by an unexpected key. Your relay may be untrusted."
    Also calls cleanupNwcRequest on timeout to avoid leaking the entry.

Silent on the happy path: a forged reply followed by the real one does
not trigger any user-facing message — the spoof count is discarded with
the matched entry.
2026-05-21 22:29:55 +00:00
Claude b17bb8339e feat(nip82): render bundled assets inside the release card
Replace the placeholder "N assets bundled" line with a real list of
compact rows for each `e`-tagged Software Asset in the release.

Each row loads the referenced asset event id through
`LoadAssetNote` (uses LocalCache first, falls back to
`checkGetOrCreateNote` for ids never seen) and then
`observeNoteEvent<SoftwareAssetEvent>` — which both observes the
LocalCache flow and registers the note with `EventFinder` so the
relay round-trips the missing asset event. When the asset arrives
the row recomposes with MIME, version, optional variant, size,
platform chips, and a Download link to the asset url.

The standalone `RenderSoftwareAsset` card (kind 3063 in a feed or
thread) is unchanged; this only fills out the release detail view.
2026-05-21 21:41:52 +00:00
Claude 3f91cb1689 fix: actually fetch ChannelCreateEvent (kind 40) and widen the relay set
filterMissingChannelsById had an inverted isEmpty() check that emitted
zero filters, so kind 40 was never requested from any relay. Channels
discovered from kind 42 messages stayed as empty stubs unless the
creator also happened to publish a kind 41 metadata update findable on
the same relay — which is why most cards in the Public Chats feed
loaded with no name or picture.

Fix:
- Drop the inverted condition; mapOfSet guarantees non-empty values, so
  emit a RelayBasedFilter for every (relay, channelIds) entry.
- Widen the relay set per channel to include the user's search and
  indexer relay lists. Falls back to DefaultSearchRelayList /
  DefaultIndexerRelayList when those lists are empty.
- Plumb the Account through ChannelFinderQueryState so the assembler
  can read the search/indexer flows. Mirrors EventFinderQueryState.
2026-05-21 21:35:27 +00:00