Commit Graph
2248 Commits
Author SHA1 Message Date
Claude 496cda2f22 feat(podcasts): dedicated Podcast Bookmarks screen + detail-screen toggle
Adds a Podcasts row to the Bookmark lists screen (mirroring Git
Repositories), opening a feed of just the bookmarked podcasts:

- quartz: isPodcastEvent() — a reusable predicate matching shows,
  episodes (NIP-F4 + Podcasting-2.0) and trailers, used to pull the
  podcast subset out of the mixed NIP-51 kind:10003 bookmark list.
- BookmarkPodcastsFeedFilter / ...FeedViewModel / BookmarkedPodcastsScreen
  filter the bookmark list (public + private) down to podcast notes,
  newest first, and render them with the standard podcast cards.
- Route.BookmarkedPodcasts + AppNavigation wiring; a "Podcasts" row with a
  live count in ListOfBookmarkGroupsFeedView.
- The single-podcast detail screen (PodcastScreen) gains a bookmark action
  in its top bar that reflects and toggles bookmarked state (reusing the
  stateful PodcastBookmarkButton). TopBarExtensibleWithBackButton now
  accepts an actions slot to host it.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JGa1EM5KWyDo1o5Yr6sS18
2026-06-30 23:05:29 +00:00
Claude 821e9bafdd fix: drop the "Mock Podcast" kind:10154 spam flood before consuming
Someone is flooding thousands of identical mock NIP-F4 show-metadata events.
They share an exact fingerprint — title "Mock Podcast", description and content
both "Headless test feed" — so match that and refuse to cache them.

- PodcastMetadataEvent.isMockSpam() encodes the fingerprint (all three fields
  must match, so a real show sharing one field is never flagged); unit-tested.
- LocalCache's PodcastMetadataEvent consume branch returns without storing when
  it matches, so the spam never reaches the cache, feeds, search, or the merged
  podcast list.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JGa1EM5KWyDo1o5Yr6sS18
2026-06-30 20:50:33 +00:00
Claude 13d654f6be Merge remote-tracking branch 'origin/main' into claude/podcast-event-kinds-merge-vv24gd 2026-06-30 19:23:05 +00:00
nrobi144 de2c970e3e feat(desktop): Follow Packs (NIP-51 kind 39089) discovery and follow flow
Adds a Follow Packs experience to Amethyst Desktop:

- New "Discover" sidebar destination with featured-pack hero, hashtag chips
  driven by NIP-12 `t` tags, a 3-up "From the pack" notes feed, and a
  right-rail of mini pack thumbnails.
- New "Follow Packs" launchable column (App Drawer + Discover "Browse all")
  with multi-field search across title, description, creator name/npub,
  and `t` tags.
- Pack detail overlay (read-only) with per-member Follow/Unfollow buttons
  that reflect the live kind-3 state, plus pack-level Follow all /
  Unfollow all with a dedupe-aware confirm dialog ("Follow N new (M
  already followed)").
- Bulk follow / unfollow batched into a single kind-3 publish via new
  `FollowActions.buildUnfollowBatch` and `Kind3FollowListState.follow/
  unfollow(users: List<User>)`. The mutating call sites are Mutex-
  protected against concurrent races.
- naddr → 39089 references in notes render as a rich inline card with
  avatar stack + Follow all CTA. Cache miss triggers a one-shot
  subscription; empty / deleted packs render minimal states.
- Shuffle button rotates both the featured pack and the gallery,
  excluding the last 5 shown.
- Pack image fields render via Coil `AsyncImage` with a deterministic
  gradient fallback.

Protocol additions:
- Quartz: `FollowListEvent.hashtags()` convenience accessor.

Bug fixes wrapped into the feature:
- `DesktopLocalCache.consumeContactList` now also loads the event into
  `addressableNotes` so `Kind3FollowListState.getFollowListEvent()`
  returns the user's actual kind-3. Without this, every bulk follow
  silently replaced (rather than appended to) the contact list.
- Added Material Symbols `Shuffle` codepoint and regenerated the
  bundled subset font (still 432 KB).
2026-06-30 12:05:16 +03:00
Vitor PamplonaandGitHub a0586a8613 Merge pull request #3422 from vitorpamplona/claude/event-tag-parsing-w7x43s
feat(nip89): parse and display app-handler t/i/a/client tags
2026-06-29 22:28:02 -04:00
Claude 250f918ebe feat(nip89): parse and display app-handler t/i/a/client tags
NIP-89 handler cards (kind 31990) previously only surfaced the supported
kinds and platform links, and even the platform links were silently dropped
when the link tag had no entity type. This widens both parsing and display:

- Fix PlatformLinkTag.match to accept 2-element link tags (entity type is
  optional per NIP-89), so e.g. NostrHub's `["android", "intent:..."]` links
  are no longer discarded.
- Parse the `i` supported-NIP tags (NostrHub points them at the NIP spec
  markdown files) into a new SupportedNipTag, plus accessors for `t`
  categories, `a` related addresses, and the `client` tag.
- Extend AppDefinitionEvent.build() with categories/supportedNips/
  relatedAddresses/client so creation stays symmetric with parsing.
- Render the new data in RenderAppDefinition: category chips, compact
  tappable rows for related addressable events (source repo, store listing,
  ...), a "via <client>" line, and NIP chips with a "+N" overflow that opens
  a bottom sheet listing every supported NIP linking to its spec.

The deprecated `alt` tag is intentionally not surfaced on the handler card.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FLfM7JUTr2vNiNUi4Ukmqn
2026-06-29 23:57:40 +00:00
Claude 6355784c98 Merge remote-tracking branch 'origin/main' into claude/podcast-event-kinds-merge-vv24gd 2026-06-29 23:51:54 +00:00
Claude 2ad4af2479 feat(git): rich project-home dashboard (stats, languages, social pulse)
Turns the repository home into a data-rich dashboard combining git facts
with the Nostr social layer:

- Social row: live zap / reaction / comment counts on the repo announcement
  (via observeNoteZaps/Reactions/ReplyCount).
- Stat tiles: branches, tags, file count, and last-updated relative time.
- Language breakdown bar: proportional colored segments computed from the
  snapshot's file tree by extension (new GitRepoSnapshot.walkFileNames()).
- Last-commit strip: tip commit summary, author, time and short SHA, exposed
  up-front via the new GitRepoSnapshot.tipCommit (no extra fetch).
- Nav cards now show open issue / PR counts.
- Recent-activity pulse: newest issues/patches merged from the feeds.

quartz: GitRepoSnapshot gains tipCommit (parsed in open()) and
walkFileNames() to enumerate blob paths for the language bar.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DpNmN8CvP6HnEsdTGAjVUr
2026-06-29 15:32:47 +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 5c29c277c8 Merge remote-tracking branch 'origin/main' into claude/git-repo-readme-code-tabs-e4uf6c 2026-06-28 22:29:16 +00:00
Claude 402c2fd3b4 feat: commit history in the git repository code browser
Add a History action to the Code tab that shows a git-log-style list of the
branch's recent commits, and opens the diff a commit introduced (vs its first
parent) with the shared diff viewer.

quartz: GitCommit + a commit-object parser (skips multi-line headers like a
signed gpgsig), and GitHttpClient.loadHistory() — a shallow tree:0 fetch
(commits only) walked most-recent-first. The fetch filter is generalized from
a blob:none boolean to a filter spec so tree:0 can be requested. Unit-tested
against a real SSH-signed commit object.

amethyst: GitCommitLog (log list + per-commit diff), a History button on the
repo header, and ViewModel hooks (loadHistory / commitDiff).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DpNmN8CvP6HnEsdTGAjVUr
2026-06-28 21:27:12 +00:00
Claude 9207209b0b feat: word-level intra-line highlighting in git diffs
Emphasize what changed inside a modified line, not just the whole line.
A new pure IntralineDiff helper pairs each delete with its corresponding
add within a hunk and trims the common prefix/suffix to the differing
middle; GitDiffView layers a stronger add/delete background over just that
character span, on top of the existing syntax highlighting.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DpNmN8CvP6HnEsdTGAjVUr
2026-06-28 21:14:57 +00:00
Claude a44ee6b312 feat: finish git PR/patch review — computed diffs and status actions
Pull requests reference a clone URL + commit rather than embedding a patch,
so their changes were invisible. Now the app computes and renders them.

- quartz: a pure Myers O(ND) line-diff (LineDiff) producing the same
  GitDiffHunk model the embedded-patch parser uses, unit-tested against git
  -U3 output and with a reconstruction property check.
- quartz: GitHttpClient.computeDiff(cloneUrl, head, base?) — fetches both
  commit trees, finds changed files by oid, batch-fetches the differing
  blobs and line-diffs each into a ParsedPatch (base = merge base, or HEAD).
- amethyst: a "View changes" section on the PR card that loads the diff over
  the git client and renders it with the shared GitDiffView.
- amethyst: GitStatusActions generalizes the issue open/close controls to
  issues, patches and PRs — patches/PRs additionally get "Mark merged"
  (GitStatusAppliedEvent). Visible to the author and repo owner/maintainers.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DpNmN8CvP6HnEsdTGAjVUr
2026-06-28 20:33:31 +00:00
Claude cd934fb0ea feat: git code browser branch/tag switch, file search, image preview; issue labels
Code browser:
- Branch & tag switching: the client now exposes all refs from ls-refs;
  a branch/tag picker on the repo header reloads the tree at the chosen
  ref (GitRepositoryBrowserViewModel.switchRef).
- In-tree filename search: a search field filters the whole tree
  (GitRepoSnapshot.searchFiles) and shows matching paths.
- Image preview: png/jpg/gif/webp/bmp blobs render via Coil instead of the
  binary notice.

Issues:
- Labels at creation: the new-issue composer takes a comma/space separated
  label list, written as NIP-34 `t` tags.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DpNmN8CvP6HnEsdTGAjVUr
2026-06-28 20:12:00 +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 39727d819e feat: flagship git review — diff viewer, issue management, polished Issues tab
Build out the repository screen into a project-review hub.

PR/patch diff viewer:
- quartz: UnifiedDiffParser turns a NIP-34 patch (kind 1617) `git
  format-patch` body into a commit message + structured per-file diffs,
  bounding each hunk by its header counts so the mbox "-- " signature is
  never miscounted. Unit-tested against a real git-generated patch.
- amethyst: GitDiffView renders a GitHub-style file-by-file diff —
  stat summary, collapsible file cards, +/- line coloring, old/new line
  numbers, and per-line syntax highlighting reused from the code browser
  (size-guarded). Wired into the patch card; feeds keep the compact preview.

Issue management:
- New-issue composer (subject + body) that builds, signs and broadcasts a
  GitIssueEvent via the account signer; reachable from the Issues tab.
- Author/maintainer Open/Close controls on the issue card, publishing
  GitStatusOpen/Closed events that flow back through GitStatusIndex.

Issues tab polish:
- Open/Closed filter chips, label (#topic) chips on each row.

Adds git_diff_files_changed plural and issue/diff strings. No new icons.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DpNmN8CvP6HnEsdTGAjVUr
2026-06-28 16:00:35 +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 291fda5728 feat: add README and Code tabs to the git repository screen
Render the repository README in the first tab and add a Code tab that
browses the repo's file tree and renders source files (syntax-highlighted),
reading directly from the NIP-34 clone URL over the git smart-HTTP v2
protocol (works with GRASP/ngit bare servers as well as GitHub/GitLab).

quartz (jvmAndroid): a from-scratch git smart-HTTP v2 client — pkt-line
codec, packfile parser with OFS/REF delta resolution and SHA-1 oids,
tree/commit parsers, and a high-level browser that fetches a shallow
filter=blob:none snapshot (one request for the whole tree) and lazily
pulls file blobs on demand. Offline tests run against real captured
GitHub wire bytes plus a git-generated OFS-delta pack.

amethyst: README tab (rich markdown), Code tab (folders-first browser
with breadcrumb navigation + a file viewer that renders markdown or
syntax-highlighted source), a browser ViewModel, new UI strings, and a
Folder material symbol (font subset regenerated). Syntax highlighting
uses dev.snipme:highlights (Apache-2.0, permissive).

Tabs are now: README, Code, Overview, Issues, Patches & PRs.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DpNmN8CvP6HnEsdTGAjVUr
2026-06-28 14:46:32 +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 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 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 d57c3f71db fix: update quartz auth tests to match new two-arg signWithAllLoggedInUsers
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013hTFpoExYYLYEGGtXBx6ZT
2026-06-27 22:58:51 +00:00
Claude 328790ac9f feat: time-bound signer grants, last-used tracking, and relay AUTH settings (NIP-42)
- Add AllowForSession and AllowUntil(expiresAt) signer grant types so users
  can grant temporary access (session, 24h, 30d) from the consent dialog
- Track per-app lastUsed timestamp in NostrSignerPermissionStore and update
  it on every granted signing operation
- Auto-expire timed grants: decide() clears expired op decisions before
  returning, so no background sweep is needed
- Add NappletBroker.sessionAllows in-memory set for session grants (cleared
  on broker destroy, never persisted)
- Implement full NIP-42 relay auth policy system:
  - RelayAuthPolicy enum (ALWAYS / NEVER / IF_IN_MY_LIST) stored in
    AccountSettings and persisted in LocalPreferences
  - RelayAuthDecision (ALLOW / DENY) per-relay overrides in DataStore
  - RelayAuthPermissionLedger combining global policy + per-relay overrides
  - DataStoreRelayAuthPermissionStore writing to relay_auth.preferences_pb
- Wire relay auth into AuthCoordinator: subscribeLedger/unsubscribeLedger
  lets each logged-in account contribute its own policy; signWithAllLoggedInUsers
  now receives the relay URL so it can check the ledger before signing
- Update RelayAuthenticator (quartz) to pass relay URL in the signing lambda
- Add RelayAuthSubscription composable that subscribes both the account and
  its ledger when a screen is active
- Add RelayAuthSettingsScreen: global policy radio picker + per-relay
  override list with toggle and remove; reachable from Settings
- Add Route.RelayAuthSettings, AppNavigation wiring, and SettingsCatalog entry

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013hTFpoExYYLYEGGtXBx6ZT
2026-06-27 22:58:51 +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 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 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
Claude a8db6674c4 feat: compact Issue/PR list rows on the git repository screen
Render the repository screen's Issues and Patches & PRs tabs with a
lightweight one-line-per-item row (author picture, name, NIP-05, subject,
time, status pill and the shared 3-dot options) instead of the full
NoteCompose renderer, which is tuned for items shown inside a regular
feed. Injected via RefresheableFeedView's existing onLoaded slot, so the
feed filters and status-split view models are untouched.

The rows reuse and re-layer the same gating NoteCompose applies — event
loading (WatchNoteEvent), mute/block/report hiding
(CheckHiddenFeedWatchBlockAndReport) and the long-press quick-action menu
— so blocked authors and reported items are hidden here exactly as
elsewhere. No body or media is rendered, so sensitive content never
reaches this list.

Adds GitPatchEvent.subject(), which parses the patch title from the
git-format-patch Subject header (stripping the [PATCH n/m] prefix and
unfolding continuation lines), with unit tests.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013gs6pxiq58X18Fkz9wdZhU
2026-06-26 22:55:46 +00:00
Claude 0092b4f726 perf: single-pass icon-path selection; resolve on event not NoteState
NappletIconPath.choose scanned the whole path list once per conventional name
(O(PRIORITY x N)), allocating a filter list and recomputing each basename up to
20 times per path. A manifest is a whole static site, so N can be large. Collapse
to one O(N) pass with a name->rank map: basename computed once per path, no
intermediate lists, and the loose-raster fallback is skipped entirely once an
exact match is in hand. Behavior is unchanged (the 11 selection tests still pass).

Also key the favorite-icon resolution on the manifest event rather than the whole
NoteState, so paths()/choose() don't re-run on unrelated metadata bumps (a
reaction or zap tracked on the note).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AaPdt8EnSrSBuzdudrxzTH
2026-06-26 19:00:53 +00:00
Claude eb6dfc7a4a feat: show nsite/napplet bundled icon on favorite tabs
Favorited web apps replace the bottom-nav glyph with the site's favicon, but
nsites (NIP-5A) and napplets (NIP-5D) fell back to the generic grid glyph.

The webapp trick (capturing onReceivedIcon from the WebView) can't work here:
nsites/napplets render inside a cross-origin sandboxed iframe under a trusted
shell, so onReceivedIcon only ever reports the shell's main-frame icon, never
the applet's. Instead, derive the icon from the manifest's own bundled blobs —
content-addressed, sha256-verified, and Tor-routed like the rest of the site.

- quartz: NappletIconPath picks the best conventional icon path (favicon/icon/
  apple-touch-icon, raster over ico/svg, shallower path wins) from a manifest's
  path tags; exposed as iconBlob() on the four nsite/napplet event kinds. Unit
  tested.
- amethyst: rememberNappletIconModel resolves the live manifest from LocalCache,
  prefetches the icon blob into the shared verified cache off the composition
  thread, and returns a file:// model. AppBottomBar and FavoriteAppsScreen feed
  it into FavoriteAppIcon for NostrApp favorites, mirroring the webapp path.

Priority: bundled blob > manifest icon URL tag > type glyph.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AaPdt8EnSrSBuzdudrxzTH
2026-06-26 15:55:12 +00:00
Vitor PamplonaandGitHub db13633fcc Merge pull request #3315 from LubuSeb/lubu/url-comments-304
Add URL-scoped comment feeds
2026-06-25 09:13:19 -04:00
Claude da4207319b fix(nip46): pin client metadata to 4th connect param, backfill empties
Per NIP-46 (nostr-protocol/nips#2381), client metadata is the 4th
positional connect param. When it is present but the optional secret or
permissions are not, those slots are now back-filled with empty strings
so the metadata always lands at index 3. Parsing maps empty placeholders
back to null. When no metadata is sent, the array stays as short as
possible for backward compatibility.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PYpupiVAq4VyHDdjrYyPdi
2026-06-24 12:08:06 +00:00
Claude 55bc75512a feat(nip46): support optional client metadata in connect request
Implements nostr-protocol/nips#2381: a client MAY attach an optional
4th positional parameter to the NIP-46 `connect` request carrying a
JSON-stringified `{name, url, image}` object, mirroring the fields
already present in `nostrconnect://` URIs. This lets a bunker:// paired
signer show who is asking to connect.

- quartz: add BunkerClientMetadata and a clientMetadata field on
  BunkerRequestConnect; serialize it as the 4th param (omitted when
  empty) and parse it back, degrading malformed/empty JSON to null.
- quartz: NostrSignerRemote carries and sends clientMetadata on
  connect() and threads it through fromBunkerUri().
- commons: BunkerLoginUseCase.execute() accepts optional clientMetadata.
- desktopApp: advertise Amethyst's metadata on bunker login.
- cli: the receiving bunker logs the connecting client's identity
  (display-only; never gates the ACK on it, since the client pubkey is
  unauthenticated in bunker:// pairing).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PYpupiVAq4VyHDdjrYyPdi
2026-06-23 16:17:19 +00:00
Claude a2eae42c07 feat(napplets): app-store-style card + icon manifest tag; demote capabilities
Redesign the shared StaticWebsiteCard (used by the feed AND the napplets browse
screen) to look like an app entry instead of a manifest dump: square app icon
(with a colored monogram fallback), name, a NAPPLET/WEBSITE type label, a short
description, and an Open button. The technical details users don't care about —
declared capabilities, Blossom servers, source URL — move behind a tap-to-expand
"What it can access" disclosure; capabilities are still re-confirmed at the
consent prompt when actually used and remain fully manageable in the permissions
screen.

Add an `icon` tag (NIP-5A/5D) end-to-end:
- quartz: IconTag + siteIcon() accessor/builder, NappletManifest.icon(), and an
  icon param on all four site/napplet build() factories (+ round-trip test).
- amy: `--icon URL` on `nsite/napplet publish`, surfaced in the publish output.
- card: renders the icon via Coil, monogram fallback when absent.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016ncMHuBBVHEf7spAoSssde
2026-06-22 17:13:41 +00:00
Claude 08947510d9 Merge remote-tracking branch 'origin/main' into claude/awesome-pasteur-xwiwad 2026-06-22 15:04:44 +00:00
Claude df15414424 Merge remote-tracking branch 'origin/main' into claude/awesome-pasteur-xwiwad
# Conflicts:
#	amethyst/src/main/res/values/strings.xml
2026-06-22 14:50:16 +00:00
Claude 3df9f831b8 feat(quartz): complete KindNames registry for every supported kind
Fill the KindNames registry from 145 to 280 entries so every event kind
quartz defines a class for has a canonical English label + NIP. Adds the
whole NIP-90 DVM request/response set, NIP-29 relay groups, NIP-60/61
Cashu wallet + nutzaps, NIP-43 relay members, WebRTC calls (NIP-AC),
marketplace (NIP-15), git PRs/state (NIP-34), CLINK, NIP-51 curation
sets, NIP-85 assertions, Marmot MLS events, and more.

For the handful of kind numbers shared by multiple classes, the registry
keeps one canonical entry (e.g. CashuToken over the deprecated nip61
TokenEvent, ExternalIdentities over GalleryList, ReleaseArtifactSet over
SoftwareRelease). Kind 1 stays "Notes" rather than the bounty value-add
helper that reuses it.

Also point `amy nip`'s Nostr fallback at quartz's canonical NipText kind
30817 (NipTextEvent) alongside the wiki and long-form kinds.

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

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

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

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

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

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

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

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011SapGdtAc1j7woifoCZ9fY
2026-06-21 20:19:01 +00:00
Claude 0d2ac83ebb feat(nsite): runtime hardening — blossom: fetch, sniffing, server fallback, blob cache
Closes the remaining nsite/napplet runtime gaps, all keeping content
integrity (every blob is sha256-verified) and the sandbox intact:

- resource.bytes blossom: scheme — blossom:<sha256> fetches from the
  user's kind:10063 Blossom servers and verifies the hash before
  returning. nostr: stays deferred (bytes semantics unspecified).
- Content-type byte-sniffing in the resolver: when a manifest path has
  no/unknown extension, sniff magic bytes (png/jpeg/webp/gif/pdf/wasm/...).
  Text/markup is never sniffed so HTML detection stays extension-driven.
  Unit-tested in quartz.
- kind:10063 server fallback: the launcher augments the manifest's servers
  with the author's published Blossom list (best-effort, when cached).
- Blob caching: the host OkHttp client caches blobs on disk with a forced
  immutable policy (content-addressed). The resolver re-verifies every
  served blob's sha256, so a stale/poisoned cache entry can't be served.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016ncMHuBBVHEf7spAoSssde
2026-06-21 20:11:15 +00:00
Vitor PamplonaandGitHub fa11a94613 Merge pull request #3323 from vitorpamplona/claude/powr-workout-interop-r1efh0
Add POWR / NIP-101e strength workout interop (read-only)
2026-06-21 11:52:55 -04:00
LubuSeb 4e6d8e106c Add URL-scoped comment feeds 2026-06-20 23:26:29 +02:00
Claude e2f2c4654e feat(fitness): resolve POWR exercise templates (kind 33401)
Render real exercise names in POWR workout cards instead of slug-derived
labels by fetching the referenced kind-33401 exercise templates.

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

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QJwXz6CWez8r7trgHXT545
2026-06-20 19:24:25 +00:00
Claude 445a20b2f3 fix: preserve user-provided media descriptions for accessibility
The blanket NIP-31 alt removal also dropped genuine accessibility
descriptions (image descriptions for the blind) on a few media paths where
the user's caption was only stored in the event-level alt tag. Restore them
via the proper, non-deprecated fields:

- NIP-94 FileHeaderEvent (kind 1063) and NIP-17 encrypted file headers
  (kind 15): write the `alt` tag only when the user actually provided a
  caption (the NIP-94 accessibility description), never the old boilerplate
  fallback.
- MIP-04 encrypted group media (kind 9): route the caption into the imeta
  `alt` field via buildMip04IMetaTag instead of an event-level alt tag.

Re-adds the narrowly-scoped TagArrayBuilder.alt() / AltTag.assemble() write
helpers (documented as accessibility-only, not for deprecated NIP-31
boilerplate). Boilerplate alt tags on all other event kinds remain removed.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014xAESAz1H1VNjmQpMVqBXj
2026-06-20 19:19:16 +00:00
Claude b9ac370f4f feat(fitness): render POWR kind-1301 strength workouts
POWR and RUNSTR both publish kind 1301 but with incompatible tag schemas.
A POWR event previously rendered with the raw "33401:...:back-squat-bb"
coordinate as its activity label, no duration, and none of the set data.

Parse the POWR / NIP-101e dialect in quartz and render it in Amethyst:

- type tag for the activity (strength/circuit/emom/amrap), preferred over
  the RUNSTR exercise verb; coordinate-form exercise tags no longer leak as
  a verb.
- start/end session timestamps -> derived duration; completed flag.
- structured per-set exercise tags (kg weights, reps, rpe, set_type),
  grouped per exercise template with volume/top-weight aggregates.
- WorkoutDisplay now shows Exercises/Sets/Volume stats and a per-exercise
  breakdown (e.g. "Back Squat Bb -> 3 x 8 x 84 kg") in the viewer's unit.

Rendering interop only; Amethyst still publishes the RUNSTR-canonical form.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QJwXz6CWez8r7trgHXT545
2026-06-20 16:47:39 +00:00
Claude 18f736abf7 refactor: stop writing deprecated NIP-31 alt tags on events we create
The NIP-31 event-level "alt" tag is deprecated, so Amethyst no longer
emits it on any event it builds. Removed all `alt(...)` builder calls and
`AltTag.assemble(...)` insertions across every event kind in quartz (and
the few app-side builders), along with the now-unused `ALT`/`ALT_DESCRIPTION`
companion constants and the `TagArrayBuilder.alt()` / `AltTag.assemble()`
write helpers.

Reading alt tags from incoming events is kept (AltTag.parse/match,
TagArray.alt(), Event.alt()) for interop with clients that still send them,
and the imeta media accessibility `alt` field (NIP-92/94) is untouched.

Updated/removed tests that asserted alt-tag presence and refreshed the
deterministic event-id/sig golden masters in UpdateMetadataTest.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014xAESAz1H1VNjmQpMVqBXj
2026-06-20 16:46:25 +00:00
Claude 96c5d9bcb6 feat: reply with kind 1111 to new Amethyst kind-1 thread roots
When replying to a note that is a kind 1 TextNoteEvent, is the root of a
new thread (no e-tags), and was itself posted from Amethyst (NIP-89
client tag), build a NIP-22 kind 1111 CommentEvent instead of a kind 1
reply. Forks keep using kind 1.

Applies across all kind-1 reply paths: the Android composer
(ShortNotePostViewModel), the notification quick-reply
(NotificationReplyReceiver), and the desktop composer (ComposeNoteDialog).

Adds Event.isClient / TagArray.isClient helpers (NIP-89, case-insensitive)
with unit coverage.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01V7RyevA6jL1NuY7uev2agS
2026-06-20 15:00:04 +00:00