Commit Graph
15857 Commits
Author SHA1 Message Date
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
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 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 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 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 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 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 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 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 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 88ba824a6e feat: read and surface rich Podcasting-2.0 episode tags
Parse the episode tags podstr emits beyond the basics — video, episode
number, season, transcript and chapters — and surface them in the UI.

quartz:
- Add VideoTag, EpisodeNumberTag, SeasonTag, TranscriptTag, ChaptersTag plus
  accessors and builder DSL on Podcasting20EpisodeEvent.
- Extend PodcastEpisode with episodeVideo / episodeNumber / episodeSeason /
  episodeTranscriptUrl / episodeChaptersUrl as interface defaults, so NIP-F4
  needs no change. PodcastAudio is now documented as audio-or-video media.
- Tests for round-trip + interface access and an all-absent default case.

amethyst:
- Extract shared PodcastBadge / PodcastLinkChip (PodcastChips.kt) and reuse
  them in the show card for visual consistency.
- Episode card: a season/episode badge, a "Video" badge, and Transcript /
  Chapters link chips that open the off-event documents; the player now falls
  back to the video source when an episode ships no audio.
- Compact show-page row: an "S2 · E5" prefix on the date line and the same
  audio→video media fallback.

Read-only. New strings for season/episode, video, transcript and chapters.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JGa1EM5KWyDo1o5Yr6sS18
2026-06-27 22:28:28 +00:00
Claude 7ea8af973d feat: surface richer podcast show metadata with a modern card
Parse and render the Podcasting-2.0 show fields the kind:30078 metadata carries
beyond the basics:

quartz:
- Extend PodcastShow with author, categories, funding URLs, explicit, complete
  and copyright as interface defaults — so NIP-F4 (kind:10154) needs no change
  and just returns empties, while Podcasting-2.0 overrides them.
- Podcasting20PodcastMetadata now parses categories, funding[], copyright,
  type, complete, locked, email and guid from the JSON content (value/V4V is
  still skipped). Covered by expanded tests incl. an all-absent default case.

amethyst:
- Rebuild the podcast metadata card: an author byline, tinted pills for
  Completed / Explicit / genre categories, a prominent filled "Support the show"
  button opening the funding URL, clickable website chips with a globe icon, and
  a subtle copyright footer — all Material3, no new icons/font subset needed.

Read-only. New strings: podcast_explicit/completed/premium/support_show/by_author.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JGa1EM5KWyDo1o5Yr6sS18
2026-06-27 22:08:03 +00:00
Claude 76b05bbb85 feat(amethyst): render Podcasting-2.0 trailers (kind:30055)
Trailers were parsed and cached but invisible. Surface them:

- PodcastTrailerListItem: a compact trailer row with a "Trailer" badge (and
  season, when present) that plays the media through the shared episode audio
  player via PodcastAudio. Used both on the show page and as the inline
  renderer (NoteCompose dispatches kind:30055 to it).
- OnePodcastEpisodesFeedFilter now also pulls kind:30055 trailers for the
  show's pubkey; PodcastScreen renders trailer rows distinctly from episodes
  and excludes them from the header's episode count.
- FilterOnePodcast requests the show author's full output — adds kind:30054
  (a pre-existing gap) and kind:30055 alongside the NIP-F4 kinds.

Two new strings (podcast_trailer, podcast_trailer_season). Read-only; trailers
stay scoped to the show page and aren't mixed into the global episodes feed.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JGa1EM5KWyDo1o5Yr6sS18
2026-06-27 21:12:47 +00:00
Claude fc2a6588c2 feat(amethyst): subscribe to Podcasting-2.0 show metadata (kind:30078 #d)
Closes the read path for podstr-style podcast shows so they arrive proactively
instead of only rendering when already cached.

makePodcastsFilter now emits two REQs: the existing NIP-F4 kind:10154 shows plus
a kind:30078 REQ constrained to `#d=["podcast-metadata"]` (the app-data kind is
overloaded, so the d-tag constraint is mandatory). To carry that constraint, an
optional `additionalTags` map is threaded through every topNav podcast
subassembly variant (authors, follows, muted-authors, global, hashtag, geohash,
all-communities, single-community); variants that already pin their own tags
(#t/#g/#a) merge it via the new mergeFilterTags helper. The default is null, so
episode and NIP-F4 REQs are byte-identical to before.

Covered by MergeFilterTagsTest.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JGa1EM5KWyDo1o5Yr6sS18
2026-06-27 20:26:32 +00:00
Claude b840e3496f feat: merge NIP-F4 and Podcasting-2.0 podcast SHOWS into one list
Companion to the episode merge: unify show-level metadata across both drafts.

quartz:
- Add a spec-neutral PodcastShow interface (title/image/description/websites).
- NIP-F4 PodcastMetadataEvent (kind:10154) implements it directly.
- Add Podcasting20PodcastMetadata, a read-only view over a kind:30078 NIP-78
  app-data event with d="podcast-metadata" whose channel fields live in a JSON
  content blob (lenient parse; unknown keys like value/funding/categories are
  ignored). resolvePodcastShow()/isPodcastShowEvent() adapt either kind.

amethyst:
- PodcastsFeedFilter merges kind:10154 with kind:30078 (d="podcast-metadata"),
  both from LocalCache.addressables, gated by isPodcastShowEvent so the 30078
  scan ignores unrelated app-data.
- RenderPodcastMetadata renders via PodcastShow; NoteCompose dispatches a
  podcast-metadata app-data note to it and keeps the text fallback otherwise.

Read support only. The kind:30078 metadata still needs a d-constrained relay
subscription to arrive proactively — that touches the topNav subassembly
plumbing and is left as the remaining step; shows already in cache merge and
render today.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JGa1EM5KWyDo1o5Yr6sS18
2026-06-27 20:16:38 +00:00
Claude bb31f0381b feat(amethyst): surface Podcasting-2.0 episodes in the merged podcast feed
Wire kind:30054 (and kind:30055 trailers) end-to-end so Podcasting-2.0
episodes appear alongside NIP-F4 kind:54 episodes in one list:

- LocalCache consumes both new kinds as addressable replaceables.
- PodcastEpisodesFeedFilter and OnePodcastEpisodesFeedFilter now merge
  kind:54 (LocalCache.notes) with kind:30054 (LocalCache.addressables),
  gating on the shared PodcastEpisode interface.
- The episode renderer, compact list row, and inline audio player read
  through PodcastEpisode / PodcastAudio, so one render path serves both
  kinds; NoteCompose dispatches kind:30054 to it.
- Relay subscriptions request kind:30054 alongside kind:54.

Read support only — Amethyst still publishes NIP-F4.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JGa1EM5KWyDo1o5Yr6sS18
2026-06-27 19:56:34 +00:00
Claude 3d822dac07 feat(quartz): merge NIP-F4 and Podcasting-2.0 podcast kinds into one episode list
Amethyst's podcast support (NIP-F4) and derekross/podstr use incompatible
identity models: NIP-F4 makes each podcast its own keypair with regular
kind:54 episodes, while the Podcasting-2.0 draft signs editable, addressable
kind:30054 episodes with the human creator's key. The two cannot share a wire
kind, but a client can still render them in one list.

Add the Podcasting-2.0 episode (kind:30054) and trailer (kind:30055) event
classes plus their tags, and introduce a spec-neutral `PodcastEpisode`
abstraction (with `PodcastAudio`) that both kind:54 and kind:30054 implement.
Feeds and UI can now depend on the shared interface and surface both kind sets
in a single, ordered podcast/episode list. NIP-F4 remains Amethyst's publish
format; this only adds read/parse support for the Podcasting-2.0 kinds.

Register both new kinds in EventFactory and KindNames. Covered by round-trip,
spec-example-JSON, and a unified-list test proving both kinds flow through the
shared abstraction.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JGa1EM5KWyDo1o5Yr6sS18
2026-06-27 18:38:10 +00:00
Vitor PamplonaandGitHub 2e47eaf3a6 Merge pull request #3406 from vitorpamplona/claude/git-repositories-feed-7xov6e
Add Git Repositories feed screen with community and author filtering
2026-06-27 12:50:28 -04:00
Vitor PamplonaandGitHub 02aa877f4f Merge pull request #3405 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2026-06-27 12:45:07 -04:00
vitorpamplonaandgithub-actions[bot] fc95f64458 chore: sync Crowdin translations and seed translator npub placeholders 2026-06-27 16:40:09 +00:00
Vitor PamplonaandGitHub be1b0bb851 Merge pull request #3404 from vitorpamplona/fix/loading-account-ui-thread-stall
fix(perf): unfreeze the cold-start "Loading account" screen in debug builds
2026-06-27 12:37:41 -04:00
Claude a81825b5cd feat: add Git Repositories feed
Add a new top-level feed for NIP-34 Git repository announcements
(kind 30617), mirroring the Pictures/Videos/Workouts feeds.

- GitRepositoriesFeedFilter scans LocalCache addressables for kind 30617
- Full top-nav filter support (follows, authors, global, hashtag,
  geohash, communities, muted) via a per-relay sub-assembler set
- Wired into AccountFeedContentStates, the relay subscription
  coordinator, bottom-bar preloaders, navigation, drawer and the
  persisted per-feed follow-list setting
- Reuses the shared RenderGitRepositoryEvent card via the standard feed
  render path, enriched with topic chips and a personal-fork badge

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GS371vPHy3PhMfQyeAHmZC
2026-06-27 16:27:48 +00:00
Vitor PamplonaandClaude Opus 4.8 981fe528dc chore(debug): add Compose composition tracing deps (debug-only)
Adds androidx.compose.runtime:runtime-tracing and tracing-perfetto so
recompositions show up as named slices in Perfetto system traces — the
tool used to attribute the cold-start feed first-paint cost to specific
composables. debugImplementation only (not shipped); all Apache-2.0.
Usage (runtime-enable broadcast) documented inline.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-27 12:26:45 -04:00
Vitor PamplonaandClaude Opus 4.8 de1362561f fix(perf): collect debug memory snapshot off the main thread
The debug-only MemoryUsageChip ("X/YMB" top-bar indicator, gated on
isDebug) polls collectMemorySnapshot() every 2s from a produceState
block, which runs on the main thread. That reads coil3.disk.DiskLruCache
.size(), a @Synchronized call. On cold start the Coil disk cache holds
that monitor for several seconds (journal init + the burst of image
writes from the initial relay event flood), so the UI thread blocked
inside size() — the "Loading account" frame couldn't repaint until it
returned. Profiling showed a single ~8s render frame and the UI thread
"blocking from coil3.disk.DiskLruCache.size()".

Collect the snapshot via withContext(Dispatchers.IO) so the synchronized
read blocks a background thread instead of the UI. The "Loading account"
stall on cold start drops from ~15-20s to ~5s. Debug-only path, so this
never affected release builds.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-27 12:26:44 -04:00
Vitor PamplonaandGitHub 9d0f444b6f Merge pull request #3403 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2026-06-27 08:56:59 -04:00
vitorpamplonaandgithub-actions[bot] 0afb4ada07 chore: sync Crowdin translations and seed translator npub placeholders 2026-06-27 12:55:24 +00:00
Vitor PamplonaandGitHub 7684c3725f Merge pull request #3400 from roguehashrate/feat/payment-targets
added support for more payment targets
2026-06-27 08:53:33 -04:00
Vitor PamplonaandGitHub 3832f31e52 Merge pull request #3401 from vitorpamplona/fix/video-decode-stall-fallback
fix(video): show browser-fallback overlay when a decoder silently stalls
2026-06-27 08:53:10 -04:00
Vitor PamplonaandGitHub ede3b798a3 Merge pull request #3402 from vitorpamplona/claude/web-app-preload-check-ev4r28
Cache favorited nsite/napplet manifests for offline resolution
2026-06-27 08:52:36 -04:00
Vitor PamplonaandClaude Opus 4.8 c0fadf9473 fix(video): show browser-fallback overlay when a decoder silently stalls
Some codec failures never surface as a PlaybackException: a software HEVC
decoder that can't keep up (e.g. iPhone-recorded hvc1 video on a device
without a HEVC hardware decoder) just parks the player in STATE_BUFFERING
forever — the buffer fills to the LoadControl cap, the playhead never leaves
0, and no error is ever raised. WatchPlaybackErrors only listened for
onPlayerErrorChanged, so the existing RenderPlaybackError "Open in browser"
overlay never showed and the user stared at a blank buffering box.

Add a decode-stall watchdog that polls the controller and synthesizes a
PlaybackException (ERROR_CODE_DECODING_FORMAT_UNSUPPORTED) once the player
sits in STATE_BUFFERING, wanting to play, with >=2s of media buffered ahead
(decoder is fed, not network-starved) yet a frozen playhead for 8s. The
buffer-ahead guard distinguishes a hung decoder from genuine network
starvation, whose buffer is depleted and so is never flagged.

Recovery is automatic: the overlay clears on the STATE_READY transition and,
belt-and-suspenders, the watchdog drops it the instant the playhead advances
again — so a slow device that eventually decodes "just plays." Also narrow
the recovery-clear to STATE_READY only (clearing on STATE_BUFFERING would
wipe the synthetic error instantly) and clear on onMediaItemTransition so a
pooled player starting a new video resets cleanly.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-27 08:49:29 -04:00
roguehashrate cac768175c added support for more payment targets 2026-06-26 21:35:06 -05:00
Claude 7340d93f0c feat: preload and cache pinned nsite/napplet manifests
Pinned web apps in the bottom nav warm reliably because EmbeddedTabFactory
only needs their URL, but a pinned nsite/napplet (FavoriteApp.NostrApp) could
not warm: favorites store only a kind:pubkey:dtag coordinate, and nothing
pulled that addressable manifest into LocalCache until the user opened the
napplet/nsite discovery screen. So embedParams() returned null and the
EmbeddedTabPreloader gave up.

Add FavoriteAppManifestPreloader, mounted once in the logged-in shell
(independent of the API-30 embedded-surface gate, since the full-screen
launcher benefits too). For each NostrApp favorite it drives the existing
EventFinder (via observeNote) to fetch the manifest's coordinate into
LocalCache, so the preloader and launcher can resolve it.

Also cache the resolved manifest event JSON device-locally in
FavoriteAppsRegistry (a second DataStore key, same single-key shape as the
favorites list) and seed LocalCache from it when relays stay silent shortly
after launch, so a pinned nsite/napplet resolves instantly and offline on the
next cold start. The cached copy is re-verified (wasVerified=false) before it
enters the cache, and refreshed whenever a newer manifest arrives.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LroBCry1UiXWf9Y4fk4b9h
2026-06-27 01:43:38 +00:00
Vitor PamplonaandGitHub d8ab76d536 Merge pull request #3399 from vitorpamplona/claude/android-accountviewmodel-store-xy21sw
refactor: scope account ViewModelStore with Lifecycle 2.11 rememberViewModelStoreOwner
2026-06-26 21:02:57 -04:00
Claude b3a4e13b85 refactor: scope account ViewModelStore with Lifecycle 2.11 rememberViewModelStoreOwner
The per-account ViewModelStore was managed by a hand-rolled registry
(StoreOwnerRegistry + ScopedViewModelStoreOwner + a RememberObserver) that
tracked configuration changes manually. Its own TODO admitted it could not
clear a store detached around a configuration change, so AccountViewModels
(and their child ViewModels, feed states and relay subscriptions) leaked and
stayed active after switching accounts.

Replace the whole registry with androidx.lifecycle 2.11's
rememberViewModelStoreOwner (already on the classpath at 2.11.0). The owner is
keyed by the account public key via key(): while an account stays logged in
the owner survives recompositions and configuration changes (it is parented to
the Activity's LocalViewModelStoreOwner); when the account changes the previous
owner leaves the composition and its ViewModelStore is cleared immediately.

Deletes ~95 lines of lifecycle plumbing.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0154te1AiD1Ykz1HCa8ao2Vo
2026-06-27 00:50:32 +00:00
Vitor PamplonaandGitHub 23a1c8af13 Merge pull request #3398 from vitorpamplona/claude/fix-remember-feed-content-padding-7ipbs4
Move rememberFeedContentPadding to commons.ui.layouts
2026-06-26 20:41:33 -04:00
Claude a101f53935 fix: correct rememberFeedContentPadding import path in GitItemListRow
The import referenced the non-existent
com.vitorpamplona.amethyst.ui.layouts package. The function lives in
commons under com.vitorpamplona.amethyst.commons.ui.layouts.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Q1LfVV9ZRuktCa7XQTzsLe
2026-06-27 00:40:18 +00:00
Vitor PamplonaandGitHub 8607a44ebd Merge pull request #3390 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2026-06-26 20:37:33 -04:00
Vitor PamplonaandGitHub 1fc98e0687 Merge pull request #3396 from vitorpamplona/claude/reproducible-build-dsigxh
Make Arti native library builds reproducible
2026-06-26 20:37:15 -04:00
Claude 6bb2f8045d build: pre-merge audit fixes for reproducibility work
Bugs / inconsistencies found while reviewing the branch for merge:

- dependenciesInfo comment falsely claimed Play "still derives this data
  server-side, nothing is lost." Not true: includeInBundle=false means the
  .aab carries no dependency metadata, so Play Console's dependency-insights /
  SDK-vulnerability alerts go unpopulated (uploads still succeed). Corrected
  the comment and the BUILDING.md framing (it called the blob "the one
  remaining blocker" when the Arti .so was the bigger one).

- Version-bump workflow was broken: the README told you to run
  `build-arti.sh --clean` to refresh Cargo.lock, but the build is now --locked
  (fails on a stale lock) and the clone moved to the canonical /tmp path. Added
  a dedicated `--regen-lock` mode (clone + cargo generate-lockfile, no NDK
  needed) and pointed the docs at it. Verified it reproduces the committed lock
  byte-for-byte.

- verify-reproducible.sh: new helper that builds twice and diffs to prove
  byte-for-byte reproducibility; uses portable sha256 (sha256sum/shasum) and
  plain `sort` so it runs on macOS too.

- README verify recipe referenced paths that only resolved from the repo root
  while telling you to cd into tools/arti-build — replaced with the helper.

- rust-toolchain.toml listed four Android targets but only two ABIs ship a
  .so; trimmed to match (check_prerequisites adds any other on the fly).

- BUILDING.md: documented that the bundled Arti .so is reproducible-from-source
  and that secp256k1/webrtc are version-pinned Maven prebuilts.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JtjUcSjjpu4auFndw1QKeU
2026-06-27 00:19:26 +00:00
vitorpamplonaandgithub-actions[bot] cc72e8045a chore: sync Crowdin translations and seed translator npub placeholders 2026-06-27 00:14:28 +00:00
Vitor PamplonaandGitHub ceca5d52ae Merge pull request #3394 from vitorpamplona/claude/composable-memory-ci-build-jxpwkv
Move UI components to commons module for better code sharing
2026-06-26 20:12:09 -04:00
Claude 94677d1677 refactor: extract self-contained layout/preview leaves to :commons
Continues moving genuinely platform-agnostic leaves out of the :amethyst app
module so they compile once in :commons instead of across all six app variants.

Moved (no Android coupling, no foundation deps):
  - ui/layouts/DisappearingBarState, DisappearingBarNestedScroll, PaddingMerge
    -> commons commonMain (com.vitorpamplona.amethyst.commons.ui.layouts)
  - ui/components/UrlPreviewState
    -> commons jvmAndroid (it references commons.preview.UrlInfoItem, which
       lives in the jvmAndroid source set)

Consumers (incl. the existing DisappearingBar*Test unit tests, which stay in
:amethyst and now import from commons) updated to the new packages. No behavior
change.

Verified: :commons, :amethyst compilePlayDebugKotlin + compilePlayDebugUnitTest,
and :desktopApp:compileKotlin build clean; spotless applied.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SNKcfjNszUZPQShYJjfmnf
2026-06-27 00:06:52 +00:00
Vitor PamplonaandGitHub 2dc80f899e Merge pull request #3393 from vitorpamplona/claude/repo-tabs-status-split-p2ducw
Add Open/Closed filter to Git repo Issues and Patches tabs
2026-06-26 19:40:59 -04:00
Claude ed3a893d9d build: build Arti at a canonical path for cross-environment reproducibility
Empirical finding: with the toolchain pin, locked deps, and
--remap-path-prefix all in place, two host builds of libarti_android.so at
the *same* path are byte-for-byte identical, but two builds at *different*
paths still differ — not in any embedded string (no path leaks into the
binary) but in the order rustc lays out functions/data, which it derives
from the real on-disk artifact paths. --remap-path-prefix only rewrites
embedded strings, not that internal ordering.

So compile in a fixed location (/tmp/amethyst-arti-build, overridable via
ARTI_REPRO_DIR) in both build-arti.sh and build-arti-host.sh. Any checkout
then produces matching bytes, which is what lets F-Droid / a verifier build
at the same canonical path and reproduce the shipped .so. This mirrors how
Rust libraries are reproduced elsewhere (F-Droid builds Rust at a fixed
path too).

Corrects the README, which previously implied path remapping alone gave
path-independent output.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JtjUcSjjpu4auFndw1QKeU
2026-06-26 23:40:18 +00:00
Vitor PamplonaandClaude Opus 4.8 13892db3ae Merge PR: fix(account): adding a read-only npub no longer clobbers a signing account
Merges nostr proposal e283c388 into main (replaces the closed e05208d9 with a
minimal guard). Adding/scanning your own read-only npub for a pubkey you already
hold the nsec for no longer downgrades the signing account — on Android it
wiped cached lists + disabled notifications, on desktop it orphaned the key.
Guarded at the single persistence point on each platform; desktop regression
test verified failing without the guard.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-26 19:24:01 -04:00