Commit Graph
15942 Commits
Author SHA1 Message Date
Claude 76ed4bc685 Merge remote-tracking branch 'origin/main' into claude/podcast-event-kinds-merge-vv24gd 2026-06-29 20:09:32 +00:00
Vitor PamplonaandGitHub 2152d5d5d6 Merge pull request #3416 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2026-06-29 15:47:30 -04:00
vitorpamplonaandgithub-actions[bot] 4060bfac39 chore: sync Crowdin translations and seed translator npub placeholders 2026-06-29 19:43:35 +00:00
Vitor PamplonaandGitHub f5879ab107 Merge pull request #3418 from vitorpamplona/claude/cashu-wallet-wizard-0zr280
Add Cashu wallet find-or-create wizard with cross-relay discovery
2026-06-29 15:40:59 -04:00
Claude a687e03461 Merge remote-tracking branch 'origin/main' into claude/cashu-wallet-wizard-0zr280
# Conflicts:
#	amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AccountViewModel.kt
2026-06-29 19:26:56 +00:00
Claude 124dcafdf9 fix(cashu): re-sign on adopt so a deleted wallet can't be re-deleted
The find-or-create wizard can surface a wallet the user previously
DELETED — a relay that missed the kind:5 still serves the kind:17375 to
the crawl. adoptDiscoveredWallet rebroadcast that event verbatim (same id,
same created_at), which loses to the prior NIP-09 deletion two ways:
DeletionEvent.build emits both an `e` tag (old id) and an `a` tag (the
replaceable 17375:pubkey: address), so relays reject the duplicate id and
re-delete every version with created_at <= the deletion's the moment the
kind:5 propagates back — on relays and in our own LocalCache. The
"reactivated" wallet would then silently vanish.

Adopt now re-signs a FRESH kind:17375 + kind:10019 (via publishWalletEvents)
with the discovered wallet's own mints and P2PK key. A new id escapes the
`e`-tag delete and created_at=now escapes the `a`-tag delete, while the
same key + mints preserve the nutzap address and all recoverable funds
(the NUT-13 seed derives from the key, not the event id). Falls back to a
verbatim rebroadcast only if the wallet can't be decrypted.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EqmMR2QiULS5QGosSgSQAe
2026-06-29 18:18:38 +00:00
Claude 0bfe9d370e fix(cashu): return to the Wallet hub after deleting the wallet
Deleting the Cashu wallet popped back to CashuWalletScreen, which on an
empty wallet auto-launches the find-or-create wizard — so the user was
funneled straight back into creating the wallet they just deleted.

Navigate to the top-level Wallet hub (Route.Wallet) via newStack instead,
which pops the Cashu screens off the back stack so the wizard never
composes.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EqmMR2QiULS5QGosSgSQAe
2026-06-29 18:13:18 +00:00
Claude d2dfd044cf feat(cashu): add "Wallet Created" celebration before the mint picker
In the find-or-create wizard, choosing "Create a new wallet" jumped
straight to the mint manager. Add a short celebratory interstitial first:
an animated check badge springs in (bouncy overshoot) behind an expanding
pulse ring, "Wallet Created" + "Now pick a few mints to host your sats"
fade up, a haptic fires, and a "Pick mints" button continues to the mint
selection.

The screen is purely presentational — the kind:17375 still isn't
published until the user adds a mint on the next screen — so "Pick mints"
uses popUpTo to replace the interstitial in the back stack, avoiding an
awkward return to the celebration.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EqmMR2QiULS5QGosSgSQAe
2026-06-29 18:02:31 +00:00
Claude 5dc09513f6 refactor: build the hub's REQ inline; round the create-podcast FAB
- FilterMyPodcast now builds its two Filters directly instead of routing through
  the topNav-oriented filterPodcastEventsByAuthors helper: episodes+trailers by
  author, and the kind:30078 show metadata constrained to #d=["podcast-metadata"].
  Same wire output, but the #d constraint and the reason for two filters are now
  visible at the call site rather than hidden behind a shared map parameter.
- The "create a podcast" FAB on the Podcasts feed was using the Material3 default
  shape (a rounded square); set shape = CircleShape to match every other FAB in
  the app.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JGa1EM5KWyDo1o5Yr6sS18
2026-06-29 16:06:45 +00:00
Claude 39a8b7473e fix(cashu): populate mint suggestions in the create-wallet picker
The mint picker (CashuMintsScreen) already renders NIP-87 directory
suggestions, but in the find-or-create wizard's "create a new wallet"
path they showed up empty while the edit-mints screen had them.

Cause: openMintDirectory() subscribed the directory against a one-shot
snapshot of the account's outbox relays (acc.outboxRelays.flow.value). A
freshly-restored account reaching the create path often still has its
relay lists loading, so the snapshot was empty, hit
CashuMintDirectoryState's empty-relay early-out, and never retried.
Outbox-only relays also don't reliably carry the broad NIP-87 mint
announcements.

Fix: subscribe reactively to the union of the user's OUTBOX relays and
their INDEXER relays. The flow re-subscribes the moment relays arrive,
and indexer relays aggregate NIP-87 mint data and fall back to a curated
default set — so the picker is populated even for a brand-new user with
no wallet and no recommendations of their own yet.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EqmMR2QiULS5QGosSgSQAe
2026-06-29 16:01:55 +00:00
Claude 64567c7056 refactor: move V4VSplitEditorState to commons for cross-front-end reuse
The value-for-value split editor's state holder is pure snapshot state over
quartz types + a commons User — no Account, LocalCache, AccountViewModel, or
Android dependency — so per the commons architecture (state holders belong in
commons, CLI-safe where practical) it moves to commons.podcasts. A future
Desktop/iOS V4V editor can now drive the same state; the editor composable stays
platform-side (it needs AccountViewModel + user search).

This is the only podcast app-layer file that's free of amethyst-only
foundations: the rest of the podcast UI / ViewModels / feed filters /
subscriptions are coupled to AccountViewModel, LocalCache, Account, the
per-user subscription framework, or Android media/upload — the same foundations
every feature in the app shares, none of which live in commons — so they stay in
amethyst (as does, for the same reason, the analogous music composer). The
podcast protocol itself was already fully shared: all 50 quartz podcast files
live in commonMain.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JGa1EM5KWyDo1o5Yr6sS18
2026-06-28 23:01:29 +00:00
Claude 31f62a1787 Merge remote-tracking branch 'origin/main' into claude/podcast-event-kinds-merge-vv24gd 2026-06-28 22:29:42 +00:00
Claude 32ac685387 feat: add Nostr users to V4V splits by search, with avatar + name
The split editor previously only took raw, hand-typed Lightning addresses / node
pubkeys — no Nostr users, no avatars, no search. Bring it up to the Amethyst
standard used by zap-splits.

Adding a recipient now leads with a user search (reusing UserSuggestionState +
ShowUserSuggestionList): type a name or @handle, pick a person, and they're added
rendered with their avatar (BaseUserPicture) and display name (UsernameDisplay),
with their lud16 lightning address resolved automatically at save time. Picking a
user with no Lightning address is rejected with a toast. A manual "Add address"
fallback remains for raw destinations — a node pubkey for keysend, or a non-Nostr
lightning address — which keep the type toggle + text field.

Each recipient still shows its live percentage of the total weight and an
optional fee flag. (Recipients loaded from an existing value block arrive as raw
addresses, since the wire format stores only the Lightning destination.)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JGa1EM5KWyDo1o5Yr6sS18
2026-06-28 21:56:38 +00:00
Claude cd73020dd8 feat: V4V split editor in the podcast show and episode composers
Lets a creator define their value-for-value splits in-app, closing the
create→get-paid loop: set up recipients on the show (and override per episode),
publish, and listeners' boosts/streams fan out to those destinations.

Adds a reusable V4VSplitEditorState + V4VSplitEditor composable: a card with one
row per recipient (name, Lightning-address vs node/keysend toggle, address,
weight, optional fee) and an "Add recipient" action, showing each recipient's
live percentage of the total weight. toPodcastValue() rebuilds the PodcastValue
on save (null when there are no payable recipients); a loaded block's suggested
amount/currency/enabled are carried through untouched.

Wired into both composers, replacing the previous preserve-only passthrough:
- Show editor: edits the show-level split (kind:30078 value block).
- Episode editor: edits the episode-level override (in More details).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JGa1EM5KWyDo1o5Yr6sS18
2026-06-28 21:29:00 +00:00
Claude 6db98c2a73 feat: back the podcast authoring hub with a dedicated REQ
The "Your podcast" hub previously listed only what happened to be in LocalCache,
so on a fresh install (or before the feed had loaded the creator's events) it
could show an empty or stale catalog.

Add a MyPodcast subscription (mirrors the OnePodcast assembler trio) that, while
the hub is on screen, keeps a REQ open for the creator's OWN Podcasting-2.0
catalog on their outbox relays: the addressable episodes (30054) and trailers
(30055) by author, plus the show-metadata kind:30078 constrained to
#d=["podcast-metadata"] (reusing the existing constant so the overloaded app-data
kind isn't pulled wholesale). Registered in RelaySubscriptionsCoordinator.

The hub now also reacts to LocalCache.live.newEventBundles — when the creator's
own episodes/trailers/metadata arrive over the REQ, the lists refresh in place
rather than only on resume.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JGa1EM5KWyDo1o5Yr6sS18
2026-06-28 21:17:02 +00:00
Vitor PamplonaandGitHub 447d48bb83 Merge pull request #3395 from vitorpamplona/claude/keen-dijkstra-tzvqgd
Add Nostr signer permissions & relay auth management UI
2026-06-28 16:30:41 -04:00
Claude 97df84ee65 feat: in-app Android authoring for Podcasting-2.0 podcasts
Adds a full create/edit experience so a creator can publish a podcast from the
phone, authoring as themselves (Podcasting-2.0 model: the account is the creator,
episodes/trailers are addressable and editable in place).

A "Your podcast" hub (mic FAB on the Podcasts feed) shows the creator's show or a
create CTA, the new-episode/new-trailer/edit-show entry points, and lists their
published episodes and trailers (tap an episode to edit). Three composers:

- Episode (kind:30054): cover + audio upload through Blossom/NIP-96 (auto-fills
  duration/title from the picked file's metadata), title, summary, and a
  collapsible "More details" section for season/number, video, transcript,
  chapters, and topics. Create + edit + delete; edits preserve the original
  pubdate and any value-for-value splits.
- Show metadata (kind:30078, d=podcast-metadata): cover + the channel fields,
  categories/funding as comma lists, episodic/serial toggle, and explicit /
  complete / locked switches. One per account, create-or-edit in place; the
  podcast GUID and value block are preserved.
- Trailer (kind:30055): title, a short audio/video clip (upload or URL), season.

The upload + media-probe mechanics are shared across the three composers in
PodcastComposerMedia, mirroring the music-track composer's pattern. Publishing
goes through account.signAndComputeBroadcast so events land in LocalCache and
broadcast to the creator's outbox relays. This pairs the existing CLI
(`amy podcast20`) with a native mobile authoring path.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JGa1EM5KWyDo1o5Yr6sS18
2026-06-28 20:20:24 +00:00
Claude 62f8cbe48a fix: resolve nsite icons and titles by trying both napplet and nsite manifest kinds
NSite manifests (kinds 15128/35128) were never found because all coordinate
construction hardcoded napplet kinds (15129/35129). The connected apps list
and detail screen therefore showed no icon or title for nsite entries.

- Add rememberManifestIconModel(author, identifier) in NappletFavoriteIcon.kt:
  tries napplet coord first, falls back to nsite coord. resolveIconBlob already
  handled all four event types — just needed the right coordinate.
- Replace rememberNappletManifest with rememberManifestEvent in
  ConnectedAppsScreen.kt: watches both napplet and nsite notes, returns
  whichever carries an event. NappletAppCard dispatches on the event type
  to extract title/icon from NappletManifest, RootSiteEvent, or NamedSiteEvent.
- ConnectedAppDetailScreen.AppIdentityHeader: swap hardcoded kind+
  rememberNappletIconModel call for rememberManifestIconModel(author, identifier).
- resolveNappletMeta in NappletManifestLookup.kt: widen the cache filter to
  include nsite kinds and dispatch title/icon extraction across all four types.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013hTFpoExYYLYEGGtXBx6ZT
2026-06-28 20:07:11 +00:00
Claude 95a2d9c44e Revert "feat: let ExoPlayer handle audio focus (pause on calls / other media apps)"
This reverts commit bce50d1d01.
2026-06-28 19:45:41 +00:00
Claude bce50d1d01 feat: let ExoPlayer handle audio focus (pause on calls / other media apps)
The shared feed/podcast/music ExoPlayer previously set no audio attributes and
did not handle audio focus, so a phone call or another media app starting would
not pause playback — meaning V4V streaming payments could keep accruing during a
call even though the user wasn't really listening.

Set USAGE_MEDIA audio attributes with handleAudioFocus = true on the pooled
player. ExoPlayer now pauses on focus loss (a call, another app's playback) and
ducks for transient interruptions; pausing flips isPlaying false, so the
streaming-payment accrual stops with it for free.

Tradeoff: in Media3 a muted player still requests focus while playWhenReady is
true, so muted feed autoplay now requests audio focus too. Acceptable for
correct call/interruption behavior; can be scoped to audio-only players later if
it proves disruptive.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JGa1EM5KWyDo1o5Yr6sS18
2026-06-28 19:37:46 +00:00
Claude c464c7cdba feat: replace capability toggle with dialog and fix napplet icon loading
- Capability rows now open a dialog with radio options (Ask each time /
  Always allow / Never allow) instead of the confusing Switch+revoke
  button combo; requiresPerUseConsent capabilities omit "Always allow"
- AppIdentityHeader now reactively loads napplet/nsite icons via
  rememberNappletIconModel using the full kind:pubkey:identifier coord

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013hTFpoExYYLYEGGtXBx6ZT
2026-06-28 16:22:01 +00:00
Claude dedabe6451 fix: only stream V4V sats while audio is audible, not merely "playing"
isPlaying stays true when the player is muted (the voice player's mute button
sets volume to 0) or when the system media volume is at 0 — so the previous gate
could keep spending sats per minute while the user hears nothing (e.g. they
muted and pocketed the phone). Hitting pause and locking the screen were already
safe (pause flips isPlaying false; a screen-locked podcast that keeps playing is
audible listening), but muting was a real silent-spend hole.

Tighten the per-minute accrual gate to require the audio is genuinely audible:
playing AND no playback error AND in-app controller volume > 0 AND system
STREAM_MUSIC volume > 0. The whole read is guarded, so a released controller or
missing AudioManager resolves to "not audible" and stops accrual.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JGa1EM5KWyDo1o5Yr6sS18
2026-06-28 16:14:41 +00:00
Claude bee418fd17 fix: clarify capability toggle and revoke button semantics in ConnectedAppDetail
The toggle ON/OFF state was ambiguous (users couldn't tell if OFF meant
"ask me" or "permanently deny"). The revoke/Block icon looked like a deny
action but actually resets to "ask me each time".

- Add "Allow always" (primary) / "Never allow" (error) label above the
  Switch so the toggle's two states are explicit
- Swap MaterialSymbols.Block for MaterialSymbols.Refresh on the reset
  button — Refresh reads as "start over / go back to asking"
- Update its content description to "Ask me each time"
- Rename "Blocked" to "Requires per-use approval" for per-use-consent
  capabilities to explain why there's no toggle

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013hTFpoExYYLYEGGtXBx6ZT
2026-06-28 16:06:36 +00:00
Claude 1fe9bcec68 feat: per-minute V4V streaming payments, gated strictly to real playback
Adds the streaming half of Podcasting-2.0 value-for-value: a "Stream sats"
toggle on the episode player that, while on, pays the value split once per full
minute of playback at a chosen sats/minute rate (boostagram action "stream").

The hard requirement is that it must never pay while the user isn't listening,
so accrual is bound tightly to genuine playback rather than a free-running timer:

- The control lives inside the player composable, so navigating away, scrolling
  it out of a feed, or tearing down the screen disposes it and stops streaming.
- Each second the engine re-reads the live MediaController.isPlaying and only
  accrues when audio is actually playing and there's no playback error. The
  player already pauses itself on background / off-screen / audio-focus loss /
  error, so every one of those halts accrual for free. A released controller
  reads as not-playing (guarded).
- Only whole, actually-played minutes are billed; a partial minute is dropped
  when the session ends (never rounded up). This rule is a pure, unit-tested
  unit (PodcastStreamingAccrual).
- The toggle defaults OFF and uses plain remember (not rememberSaveable), so it
  never silently resumes after a rotation or process death — the user re-opts in.
- Streaming is gated to an in-app wallet (NWC / CLINK debit); we never auto-fire
  an external wallet intent every minute. Per-minute errors are swallowed (no
  toast spam) while one-off boosts still surface errors.

The selected rate is always shown on the toggle and a live "streamed N sats this
session" counter makes the spend visible.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JGa1EM5KWyDo1o5Yr6sS18
2026-06-28 16:03:56 +00:00
Claude f502b09b55 feat: rename See more/less to Show/Hide Event and make JSON scrollable
The raw event JSON toggle in the signer consent dialog is now labeled
"Show Event" / "Hide Event" instead of the generic "See more" / "See less".
The expanded JSON block also gains horizontal scroll (softWrap=false +
horizontalScroll) so wide event JSON doesn't get clipped on narrow screens.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013hTFpoExYYLYEGGtXBx6ZT
2026-06-28 15:57:52 +00:00
Claude a71ba61370 feat: execute Podcasting-2.0 value-for-value (V4V) Lightning splits
Adds payment execution to the V4V value blocks that were previously
display-only. A "Send value" button on the value card opens the account's
zap-amount picker; choosing an amount fans the weighted shares out to every
recipient, mirroring how a NIP-57 zap-split is paid.

quartz (pure, tested):
- PodcastValue.computeShares() — splits a total across recipients by relative
  weight, honoring `fee` recipients that take their split as a percent off the
  top. Returns PodcastValueShare (recipient + millisats).
- PodcastBoostagram — the satoshis.stream keysend metadata blob carried in TLV
  record 7629169, with the registered field names and unset fields omitted.
- PODCAST_TLV_RECORD / TYPE_NODE / TYPE_LNADDRESS constants.

amethyst:
- V4VPaymentHandler — the execution engine. lnaddress recipients resolve to a
  BOLT-11 via LNURL-pay and pay through the user's default source (NWC, CLINK
  debit, or external wallet intent), same rails as a zap. node recipients pay
  by NWC keysend (pay_keysend) carrying the boostagram TLV plus any per-recipient
  custom TLV; keysend is NWC-only, so node recipients are skipped with a clear
  error when no NWC wallet is configured.
- AccountViewModel.payV4V() wrapper + the "Send value" amount picker on the
  value card, wired for both episode and show value blocks.

V4V recipients are raw Lightning destinations, not Nostr users, so there is no
zap request and no zap receipt — just the payment.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JGa1EM5KWyDo1o5Yr6sS18
2026-06-28 15:43:22 +00:00
Claude 3cc0ce09d4 feat: show website favicon and domain in all browser-connected app views
Web app entries (browser:https://...) now display:
- The captured favicon from BrowserIconRegistry (same source as the
  bottom-nav favourite website icon) in ConnectedAppsScreen,
  ConnectedAppDetailScreen, and all three permission/consent dialogs
- The domain name (host) as the title instead of the full URL, via
  OmniboxInput.hostOf() in loadDetailState, NappletConsentSummary,
  buildSignerConsentInfo, and buildConnectInfo
- A globe icon (FavoriteApp.WebApp) instead of the grid icon
  (FavoriteApp.NostrApp) in all consent dialog headers

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013hTFpoExYYLYEGGtXBx6ZT
2026-06-28 15:41:27 +00:00
Claude 6f9bf33595 fix: show globe icon and domain for browser-connected web apps
The permission ledger stores browser-visited origins as `browser:<url>`
where "browser" is a sentinel non-pubkey. All previous code treated every
entry as a napplet, causing NPub.create("browser") to fail and
LocalCache.checkGetOrCreateAddressableNote to return null for every entry
-- so icons and titles never resolved.

Now ConnectedAppCard splits on author == "browser": web-app entries get a
globe icon (FavoriteApp.WebApp), the domain extracted from the URL as
title, and no npub row. Napplet entries (real hex pubkeys) continue using
the reactive manifest lookup. The relay subscription now also filters out
"browser" from the authors set so no invalid pubkey is sent to relays.

ConnectedAppDetailScreen receives the same fix in AppIdentityHeader, using
FavoriteApp.WebApp for browser entries so the globe icon appears there too.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013hTFpoExYYLYEGGtXBx6ZT
2026-06-28 15:17:04 +00:00
Claude 0d51dff41a feat: live relay subscription for connected-apps manifest screen
Replaces the one-shot fetchAll with a ComposeSubscriptionManager that
holds an open relay subscription to NIP-5D manifests (kinds 15129/35129)
for exactly the set of authors stored in the permission ledger, while
ConnectedAppsScreen is in composition.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013hTFpoExYYLYEGGtXBx6ZT
2026-06-28 14:03:17 +00:00
Claude 5984d20042 feat: verify NIP-F4 podcast authors against their kind:10064 counter-claims
A show's kind:10154 metadata can name any pubkey as an author (host, co-host,
editor) via `p` tags, but those claims are unverified — the show can list
anyone. NIP-F4 lets the named author publish their own kind:10064
AuthoredPodcastsEvent listing the podcasts they actually author, which closes
the loop.

On the single-podcast header, render each claimed author as a row (avatar,
name, role) and cross-check it: the author's 10064 is fetched + observed
lazily via observeNoteEvent, and a "verified" check badge appears only when
that 10064 lists this podcast's pubkey.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JGa1EM5KWyDo1o5Yr6sS18
2026-06-28 13:48:47 +00:00
Claude 7b2399eb51 feat: inline Podcasting-2.0 chapter list (expand-to-load)
Turn the episode "Chapters" affordance from a link-out into an inline,
timestamped chapter list.

quartz:
- PodcastChapters / PodcastChapter (@Serializable) parse the off-event
  podcast-namespace chapters.json (version + startTime/title/img/url/toc).
  Lenient parse with a malformed-input test.

amethyst:
- PodcastChaptersSection fetches the chapters document with the app's
  preview HTTP client (Tor/proxy aware) off the main thread and renders
  `timestamp — title` rows in a tinted card; empty/failed renders nothing.
- The episode card's "Chapters" chip now toggles this section instead of
  opening the URL. Fetch is lazy — gated behind the toggle — so scrolling a
  feed never triggers network.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JGa1EM5KWyDo1o5Yr6sS18
2026-06-28 01:38:00 +00:00
Claude 1f5650fe73 feat(amethyst): bookmark podcasts via the existing NIP-51 bookmark list
Rather than build a parallel favorites/subscribe stack for NIP-F4's kind:10054
list, reuse the bookmark list (kind 10003) that already holds multiple kinds via
a/e references — a public bookmark matches the "soft public recommendation"
intent of the favorites list, and the whole chain (Account.addPublicBookmark
branching on addressable vs regular notes, the kind-agnostic Bookmarks feed that
resolves both e- and a-tags) already supports it.

Add a PodcastBookmarkButton toggle and place it in the show and episode card
title rows. Works across all podcast kinds: NIP-F4 shows (10154) / episodes (54)
and Podcasting-2.0 shows (30078) / episodes (30054); bookmarked podcasts then
appear in the standard Bookmarks screen, rendered through the same cards.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JGa1EM5KWyDo1o5Yr6sS18
2026-06-28 01:12:56 +00:00
Claude 05b3e3e25e fix: make ConnectedAppsScreen fully reactive for title and icon
The previous approach baked title/iconUrl into ConnectedAppEntry at
load time (a snapshot) and passed pubkey:identifier to
rememberNappletIconModel which expects kind:pubkey:dtag — so both
title and icon never updated from LocalCache.

Now:
- ConnectedAppEntry only holds coordinate + signerPolicy
- Each ConnectedAppCard builds the full kind:pubkey:dtag coordinate
  (kind 15129 for root napplets with empty identifier, 35129 for named)
- rememberNappletManifest observes the live AddressableNote in
  LocalCache so title/iconUrl update reactively as manifests arrive
- rememberNappletIconModel receives the correct full coordinate so
  blossom blob icons load exactly like FavoriteAppsScreen
- The relay fetch now covers all connected apps (not just those with
  missing iconUrl) so manifests arrive promptly

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013hTFpoExYYLYEGGtXBx6ZT
2026-06-28 01:09:00 +00:00
Claude d90165a4f6 feat: Podcasting-2.0 value-for-value (V4V) splits — parse, display, publish
Make the Podcasting-2.0 `value` block first-class across read and publish. Actual
Lightning execution (keysend to node recipients, LNURL fan-out to lnaddress
recipients, weighted by split) is a separate wallet/NWC effort and is NOT done
here — this lands the data model, display, and authoring.

quartz:
- PodcastValue / PodcastValueRecipient (@Serializable): amount, currency,
  recipients[] (name, type node|lnaddress, address, split weight, fee, custom*).
- Episode `["value", "<json>"]` tag (ValueTag) + accessor/builder; show value is
  parsed from the kind:30078 JSON. Exposed via the shared abstraction as
  PodcastEpisode.episodeValue() and PodcastShow.showValue() (interface defaults,
  so NIP-F4 returns null). Round-trip + JSON-parse tests.

amethyst:
- PodcastValueSplits: a tinted "Value-for-Value" card listing each recipient
  with its address and computed share, rendered on both the episode and show
  cards when a value block is present.

cli:
- `podcast20 episode`/`metadata` gain `--value-json` to publish the block;
  malformed JSON is rejected as bad_args. Verified end-to-end against the CLI.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JGa1EM5KWyDo1o5Yr6sS18
2026-06-28 00:35:10 +00:00
Claude e0a6ed5ffc fix: i18n kind names in signer consent; live icons in connected apps list
- Extract kindDisplayName() to KindDisplayName.kt and add kindNameFor()
  helper that picks the translated string resource when available,
  falls back to KindNames English map, then "k<number>"
- NostrSignerOpLabels: use kindNameFor() so "sign for Notes (kind: 1)"
  is translated instead of always English
- ConnectedAppsScreen: wire rememberNappletIconModel so the card icons
  load from blossom just like FavoriteAppsScreen does
- Remove the now-duplicate kindDisplayName() definition from
  RelayInformationScreen.kt

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013hTFpoExYYLYEGGtXBx6ZT
2026-06-28 00:01:02 +00:00
Claude e8edd94317 test: verify NIP-22 comments work on podcast episodes; add RootScope marker
Verification of kind:1111 comments on podcast episodes: they already work
end-to-end (parse, build, route to the thread screen, thread assembly, composer,
and the reply/reaction subscriptions all treat any kind as a valid root — nothing
gates on the RootScope marker). Added a quartz test that drives the real
CommentEvent.replyBuilder path and asserts:
- a comment on a Podcasting-2.0 episode (30054) roots on its `a`/`A` address,
- a comment on a NIP-F4 episode (54) roots on its `e`/`E` event id,
- both are kind:1111 and carry the root-kind tag.

Also closes a small consistency gap: every other commentable content type
(articles, all video kinds, pictures, highlights, wiki, polls, …) implements the
RootScope marker, but the podcast events did not. Add it to PodcastEpisodeEvent,
Podcasting20EpisodeEvent and Podcasting20TrailerEvent. Harmless today (no code
does `is RootScope`), but it documents intent and future-proofs any such check.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JGa1EM5KWyDo1o5Yr6sS18
2026-06-27 23:20:18 +00:00
Claude f2fbe260d5 feat: show kind name in signer consent label; fix icon race in ConnectedAppsScreen
- NostrSignerOp.SignKind label now renders "sign for <Kind Name> (kind: N)"
  using KindNames.nameFor(), falling back to "sign kind N event" for unknowns
- Fix ConnectedAppsScreen race: merge two LaunchedEffects into one so
  items is set before observeEvents fires and before the relay fetch runs;
  previously both could find items==null and return early, leaving icons blank
2026-06-27 23:17:56 +00:00
Claude 7fd0741778 fix: center-align text in deny/block OutlinedButtons
Remove the Modifier.fillMaxWidth() + TextAlign.Start that was forcing
button labels to the left edge; buttons now use the default centered layout.
2026-06-27 23:09:31 +00:00
Claude 8573e4fd2f feat(cli): publish Podcasting-2.0 podcasts via amy podcast20
Add a separate command group for authoring the Podcasting-2.0 (podstr) kinds,
kept distinct from the NIP-F4 `podcast` commands because the models differ —
here the logged-in account is the creator and signs everything with its own key,
and episodes/trailers are addressable (d-tag) events.

  amy podcast20 metadata --title T [...]   kind:30078 show metadata (JSON body)
  amy podcast20 episode  --title T --audio URL[,URL] [...]   kind:30054 episode
  amy podcast20 trailer  --title T --url URL [...]           kind:30055 trailer
  amy podcast20 list [USER] [--limit N]    metadata + episodes + trailers

Episodes accept the full rich tag set (video, episode/season, transcript,
chapters, topics, duration); d-tags and the RFC2822 pubdate are auto-generated
when omitted. Thin assembly only — added Podcasting20PodcastMetadata.build() in
quartz so JSON-body construction stays out of cli (covered by a round-trip test).

Verified end-to-end against the running CLI: all three commands build, sign and
emit the expected kinds (30078/30054/30055) with correct d-tags and the --json
single-line contract.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JGa1EM5KWyDo1o5Yr6sS18
2026-06-27 23:05:01 +00:00
Claude 546d4ce9c8 Merge remote-tracking branch 'origin/main' into claude/cashu-wallet-wizard-0zr280 2026-06-27 23:00:26 +00:00
Claude 2e81a71f19 refactor(napplet): replace newEventBundles with LocalCache.observeEvents for manifest updates
LocalCache.observeEvents<Event>(filter) handles both the initial snapshot and
subsequent insertions via the observables registry, replacing the manual
bundle-scan over newEventBundles. Fewer moving parts and the filter is scoped
to manifest kinds rather than scanning every arriving event bundle.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013hTFpoExYYLYEGGtXBx6ZT
2026-06-27 22:58:53 +00:00
Claude debe3c3a48 feat(napplet): rich app cards with dynamic manifest loading in Connected Apps list
- Add authorPubKey (npub) to ConnectedAppEntry and show it in each card below the
  domain, so users can verify who published the napplet/nsite
- Increase icon to 48dp and use surfaceVariant card background to match the detail
  screen's AppIdentityHeader style
- After the initial cache-based load, subscribe to LocalCache.live.newEventBundles
  and re-resolve metadata (title + icon) for all entries whenever a manifest event
  (kind 15129 / 35129) arrives — handles manifests delivered by any relay subscription
- For apps whose icon is not yet cached, issue a one-shot relay fetch (kinds 15129/35129
  for the relevant author set) via account.client.fetchAll; inject results into
  LocalCache.justConsume so the newEventBundles observer picks them up and updates
  the list without a full reload

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013hTFpoExYYLYEGGtXBx6ZT
2026-06-27 22:58:53 +00:00
Claude 1086f4cf5e fix(napplet): unify DataStore cache key to file path, fixing connected-apps crash
allPolicies() keyed the LargeCache on the filename ("nsp_HASH") while storeFor()
keyed it on the coordinate string. Both point at the same .preferences_pb file,
so getOrCreate created two live DataStore instances for one file —
DataStore's own singleton guard then threw IllegalStateException.

Fix: use file.absolutePath as the cache key in both code paths so the second
call always returns the already-open DataStore instance instead of creating a
new one.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013hTFpoExYYLYEGGtXBx6ZT
2026-06-27 22:58:53 +00:00
Claude 94ac370379 fix(napplet): consistent button styling, fix DataStore crash, icon loading, dead-code cleanup
- Make all four consent actions (Always Allow / Allow Once / Deny Once / Always Deny)
  full-width OutlinedButton/Button so they are visually consistent; remove TextButton
  with left-aligned text that clashed with the centered primary buttons
- Move domain/coordinate label into the header Column directly below the subtitle so
  the URL is contextually grouped with the app identity rather than floating near buttons
- Fix DataStore multiple-instances crash: add nappletPermissionStore and signerPermissionStore
  as lazy singletons in AppModules; warm them up on IO thread to avoid StrictMode
  DiskReadViolation; update NappletBrokerService and connected-apps screens to use the
  shared singletons instead of creating independent instances
- Propagate iconUrl through NappletConsentInfo / NappletSignerConsentInfo / NappletConnectInfo
  data classes; resolve napplet metadata (title + icon) in the broker-side builders
  (NappletConsentSummary, NostrSignerOpLabels) so dialogs receive it ready to display
- Replace manual buildEventJson (org.json) with JacksonMapper.toJsonPretty(EventTemplate)
  in NostrSignerOpLabels; add toJsonPretty(EventTemplate<*>) overload to JacksonMapper
- Delete dead NappletPermissionsScreen.kt and NappletSignerPermissionsScreen.kt (never
  navigated to); remove Route.NappletPermissions and its composable registration

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013hTFpoExYYLYEGGtXBx6ZT
2026-06-27 22:58:52 +00:00
Claude 840237fa72 fix(napplet): convert capability consent to floating dialog, match consent style
Replace the AlertDialog with the same Surface(extraLarge)/Dialog pattern
used by the signer consent and connect dialogs. Shows app icon + name +
capability category in the centered header, operation detail (with any
content preview) in a selectable surfaceVariant box, and the same button
hierarchy: Always allow (Button, primary) / Allow once (FilledTonalButton)
then Never allow / Not now as left-aligned TextButtons below a divider.
When the capability is per-use only (payments), Allow once is promoted
to the primary Button.
2026-06-27 22:58:52 +00:00
Claude 9dcfc3a0ed fix(napplet): convert connect screen to floating dialog, match consent style
Replace the full-screen semi-transparent overlay with a floating card
Dialog (same Surface/shape/elevation as the signer consent dialog) so
both permission prompts look consistent. Header now shows the app icon,
name, and "wants to connect to your Nostr account" subtitle instead of
the generic "Connect to Nostr" headline. Block button style matches the
deny row in the signer consent.
2026-06-27 22:58:52 +00:00
Claude af2fcc46b6 feat(napplet): promote Always Allow to primary action in signer consent
Move AllowForOp to the primary Button (filled) since always allowing the
operation is the preferred choice, with AllowOnce as the secondary
FilledTonalButton. The time-bound and allow-all options stay in "More
options", keeping the nuclear allow-all less discoverable.
2026-06-27 22:58:52 +00:00
Claude c7566f2685 fix(napplet): center connect dialog, fix label color and domain display
- Wrap content in a scrollable Box with Alignment.Center so the dialog
  is vertically centered instead of stuck at the top
- Explicitly set onSurface color on the PolicyOption label so it stays
  visible regardless of Surface background tint
- Fix buildConnectInfo to show the napplet identifier (e.g. "browser")
  in the block button instead of the raw coordinate prefix (pubkey)
2026-06-27 22:58:52 +00:00
Claude 343263cb2c fix: use CommonsR for napplet_untitled after resource move to commons
The string moved to commons/src/androidMain/res/values/strings.xml
in the upstream merge; update the two remaining call sites.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013hTFpoExYYLYEGGtXBx6ZT
2026-06-27 22:58:52 +00:00
Claude d05247541d refactor: extract shared napplet abstractions, remove what-comments
- Extract resolveNappletMeta() to NappletManifestLookup.kt, replacing
  three private copies of the same manifest lookup across
  ConnectedAppsScreen, ConnectedAppDetailScreen, NappletPermissionsScreen,
  and NappletSignerConsentActivity.
- Extract PolicyCard composable to PolicyCard.kt, shared between
  ConnectedAppDetailScreen and RelayAuthSettingsScreen (was duplicated).
- Extract NappletCapability.symbol() to NappletCapabilityExt.kt, shared
  between ConnectedAppDetailScreen and NappletPermissionsScreen.
- Drop what-comments on kind 1/6/7 lines in NostrSignerPermissionLedger.
- Reword TrustedRelayListState stateIn comment to note private-tag absence
  on first boot.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013hTFpoExYYLYEGGtXBx6ZT
2026-06-27 22:58:52 +00:00