PodcastBookmarkButton read `note in bookmarks.public` — an identity-based
List<Note> containment that is unreliable for addressable shows/episodes
(the rendered note instance may differ from the one rebuilt from the
a-tag), so the icon never flipped and there was no way to tell a podcast
was already bookmarked.
Switch to the public bookmark id/address sets (the same reactive pattern
the working git-repository bookmark button uses): match note.address for
addressable notes and note.idHex otherwise. The icon now reliably shows
bookmarked vs not (filled/primary vs outline/onSurfaceVariant), and each
tap fires a confirming toast so the action is never silent.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JGa1EM5KWyDo1o5Yr6sS18
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
The standard zap button now detects a Podcasting-2.0 value-for-value block
on a podcast note (episode or show) and pays that split instead of a plain
author zap — so V4V becomes a first-class zap rather than a separate, no-
receipt payment:
- AccountViewModel.zap() intercepts a note carrying a value block and routes
the chosen amount to the V4V split. Intercepting at zap() covers every
entry point (one-tap, amount popup, custom dialog, polls) with no UI churn.
- V4VPaymentHandler gains asZap/zapType: lnaddress shares now attach a NIP-57
zap request, so a Nostr-aware provider mints a zappable invoice and
publishes a receipt (driving the zap button's icon/counter). Node shares
stay keysend — no LNURL, so a receipt is impossible there by protocol.
- Per-minute streaming pays with asZap=false so it doesn't publish a receipt
every minute.
- The dedicated "Send value" button is now redundant and removed;
PodcastValueSplits becomes a pure recipient/percentage breakdown display
next to the zap button.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JGa1EM5KWyDo1o5Yr6sS18
Make the single-podcast detail screen spec-neutral so a Podcasting-2.0
show (kind 30078, d=podcast-metadata, e.g. "Soapbox Sessions") renders
its real cover art and title instead of the default Amethyst banner and
"Podcasts" fallback:
- PodcastHeader/PodcastScreen now take a resolved PodcastShow? instead of
a NIP-F4-only PodcastMetadataEvent?, resolving from either the kind
10154 (F4) or kind 30078 (P2.0) metadata event.
- FilterOnePodcast adds a #d=podcast-metadata filter on kind 30078 so the
P2.0 show metadata is actually fetched on deep-links/fresh loads.
- NoteMaster (thread/full view) now dispatches Podcasting20EpisodeEvent,
Podcasting20TrailerEvent, and the kind 30078 podcast-metadata variant
to the shared podcast renderers, matching NoteCompose.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JGa1EM5KWyDo1o5Yr6sS18
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
The two podcast feeds used kind3GlobalPeopleRoutes, which omits the "Mine"
option — so a creator had no way to filter the feed down to their own published
shows/episodes (and the default follow-list view is usually empty for podcasts,
since NIP-F4 shows are their own keypairs you rarely kind:3-follow).
Add a podcastRoutes catalog in TopNavFilterState that mirrors musicRoutes
(content catalog + Mine + interests + mute list) and point both podcast top bars
at it. "Mine" resolves to an AuthorsTopNavPerRelayFilterSet of the user's own
pubkey, which the podcast SubAssemblyHelper already dispatches through
filterPodcastEventsByAuthors — so selecting it actually fetches the creator's
own catalog, not a dead option.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JGa1EM5KWyDo1o5Yr6sS18
Adds a repo-root cross-module roll-up that stitches the 10 per-folder
plans/ indexes into one view: totals, a per-module status table, and a
"live work" section listing every in-progress / queued / abandoned plan
with links. Shipped plans stay in each folder's archive/ and are linked
via the per-module README.
142 plans: 122 shipped, 9 in-progress, 8 queued, 3 abandoned.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016hpUivtmq4pgzqRbY6MYrA
Audited all 143 plan files across the 10 plans/ folders. Each plan now
carries a Status header (shipped | in-progress | queued | abandoned)
backed by codebase evidence, and every folder has a README.md index
grouping plans by status.
Shipped plans were moved into a per-folder plans/archive/ (via git mv,
history preserved) so each plans/ folder surfaces only live work:
shipped (archived): 122 in-progress: 8 queued: 7 abandoned: 4
docs/plans/ is the frozen legacy folder; its plans were stamped and
indexed in place (48 of 52 archived) but it remains closed to new plans.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016hpUivtmq4pgzqRbY6MYrA
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).
Add a direct link to each running app's editable Connected Apps
permission-detail screen from its top pull-down sheet, so users can
change the trust level and per-capability grants as they navigate.
Covers every top pull-down rendering:
- Embedded napplet/nsite and web-app tabs (Compose TopControlSheet),
keyed by the napplet `pubkey:dtag` coordinate or the web client's
`browser:<origin>`.
- Full-screen sandbox host and direct-browser activities (native
NappletControlSheet), via a new MSG_OPEN_PERMISSIONS IPC: the host
(which can't state its own coordinate) sends its launch token or
visited origin, and the main-process broker resolves the trusted
coordinate and opens MainActivity through a `connectedapp?coordinate=`
deep link added to uriToRoute.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019dXhUL8To3qVXJVBZGCmhU
Refine the app-handler card rendering:
- NIP "Implements" bottom sheet now shows a wrapping row of clickable NIP
chips that open each spec in the browser, instead of a list of raw URLs.
- "Handles" gets the same "+N" overflow -> bottom sheet treatment as
"Implements", listing every handled kind.
- Related (`a`-tag) references now render as the referenced event's author
avatar + its real name with a short kind label, since these are vouched
for by the handler's author. Software Application shows the app name
(not the package-id d-tag) and is labelled "App"; Git repositories and
NIP-text events use their name/title, falling back to the d-tag until the
event loads.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FLfM7JUTr2vNiNUi4Ukmqn
Pressing Back on the "Connect to Nostr" first-connect dialog resolves to
AppConnectResult.Cancelled, which added the app's coordinate to an in-memory
`sessionCancelled` set. That set suppressed every future connect prompt for the
entire broker lifetime, so a later request — e.g. the user re-clicking "login"
in the in-app browser — was silently denied and the dialog never reappeared.
Because a Cancel persists nothing, the app also never showed up in Connected
Apps, leaving the user with nothing to clear to recover.
Replace the permanent suppression with a short, self-clearing cooldown
(`cancelledUntil` map): a Cancel suppresses re-prompts only briefly so the
burst of requests a page/napplet fires on load doesn't relaunch the dialog per
request, while a deliberate retry seconds later prompts again. The clock is
injectable for deterministic tests.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HDcts4HzwSff4fy6oSVA6D
The full-screen direct-WebView browser (NappletBrowserActivity, launched when
you type an address) and the sandbox host (NappletHostActivity) used an older
console design that diverged from the embedded tabs' Compose chrome:
- The Console row in NappletControlSheet was a plain action row and the bottom
pull-up grabber was always visible. Make Console a Switch toggle (like the Tor
row / the Compose TopControlSheet), and hide the whole NappletConsolePanel
until the toggle is on — turning it on reveals the sheet already pulled up,
mirroring BottomConsoleSheet.
- The console grabber/panel sat at elevation 0 while the top sheet's panel is at
6dp, so an open top sheet drew over the console when they overlapped (e.g. in
landscape). Elevate the console sheet above the top sheet so its pull tab and
log render on top, matching the Compose layer where BottomConsoleSheet is
composed after TopControlSheet.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01A9kfpNrBRB8WJNi67NHGGX
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
Now that observeEvents re-emits the whole matching list each time,
GitStatusIndex and GitPullRequestUpdateIndex no longer need the imperative
launch/collect-into-MutableStateFlow wrapper carried over from the old
newEventBundles version. Each is now a single observeEvents().map { reduce }
.stateIn(scope, Eagerly, null) — dropping startIfNeeded(), the AtomicBoolean
double-start guard, and the MutableStateFlow/asStateFlow pair.
Eagerly (not WhileSubscribed) is required: callers read .value synchronously
(isClosedOrResolved, the feed filters, the home open-count derivations) and
must not see a stale map when nobody is collecting. All startIfNeeded() call
sites removed; stateIn shares one upstream subscription across collectors.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DpNmN8CvP6HnEsdTGAjVUr
Replace the third "Repositories" tab on the default bookmark screen with a
dedicated entry on the bookmark-lists screen, mirroring how Pinned Notes
works: a row in ListOfBookmarkGroupsFeedView that opens its own
BookmarkedRepositoriesScreen via the new Route.BookmarkedRepositories.
The row shows the bookmarked-repo count from
gitRepositoryListState.publicRepositoryAddressSet; the screen renders the
BookmarkRepositoriesFeedViewModel feed (moved to a repositories/ package),
invalidates on bookmark changes, and preloads any uncached repo
announcements via the EventFinder. Reverts the tab added to
BookmarkListScreen.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DpNmN8CvP6HnEsdTGAjVUr
- GitStatusIndex now subscribes to a kind-1630..1633-filtered
LocalCache.observeEvents instead of LocalCache.live.newEventBundles,
matching the GitPullRequestUpdateIndex change: the indexed observable
seeds from the cache index and re-emits the full list on each new status
event, so the manual onStart full-cache scan and per-bundle type
filtering are gone and the collector just reduces to latest-per-target.
- Bookmark screen gains a third "Repositories" tab listing the user's
bookmarked (NIP-51 kind 10018) git repositories. New
BookmarkRepositoriesFeedFilter resolves the public repository address set
to addressable notes (newest first); the screen invalidates it on
publicRepositoryAddressSet changes and preloads any uncached repo
announcements via the EventFinder.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DpNmN8CvP6HnEsdTGAjVUr
GitPullRequestUpdateIndex now subscribes to a kind-1619-filtered
LocalCache.observeEvents instead of LocalCache.live.newEventBundles. The
indexed observable seeds its matching set from the cache index (via init())
and re-emits the full list on each new PR update, so the manual onStart
full-cache scan and the per-bundle type filtering over every event of every
kind are both gone. The collector just reduces the list to the
latest-per-parent map. PR updates are rare, so recomputing the whole map per
emission is cheaper than scanning every bundle.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DpNmN8CvP6HnEsdTGAjVUr
- Top-bar repo bookmark toggle now switches between BookmarkAdd (with +)
and Bookmark glyphs instead of two identical glyphs, so the icon
visibly changes shape (not just tint) when starred/unstarred.
- Add a thin HorizontalDivider after the ReactionsRow on the repo home.
- Code browser: opening/closing a file or changing folders swaps the
scrollable in place, landing the new view at the top with no scroll
delta, which left the disappearing top bar stranded at its hidden
offset over a blank band. Expose the scaffold bar state via
LocalDisappearingBarState and reset it to visible on each in-place view
change.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DpNmN8CvP6HnEsdTGAjVUr
The commonMain GitRepositoryListState used Dispatchers.IO without importing
the multiplatform kotlinx.coroutines.IO extension, so it resolved to the
JVM-only member and broke the iOS native compile
(:commons:compileKotlinIosSimulatorArm64). Matches BookmarkListState.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DpNmN8CvP6HnEsdTGAjVUr
- Converts the New Issue composer from an AlertDialog to a dedicated screen
(new Route.GitRepositoryNewIssue + GitNewIssueScreen with its own top bar
and a Create action). The Issues FAB now navigates to it.
- The extended FAB rendered square because the app theme sets shapes.large
(the extended-FAB default shape) to 0.dp; pin an explicit RoundedCornerShape.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DpNmN8CvP6HnEsdTGAjVUr
- Repos that can't be cloned over http(s) (e.g. Iris's htree://) now show a
"Hosted externally" notice with an open-in-browser link instead of an empty
dashboard — on the home, the Code screen, and the feed repo card.
- Removed the "Maintained by" row from the project home.
- Tighter home section spacing (12 → 8dp) and a smaller bottom padding on the
feed repo card so the last-commit line sits closer to the reaction row.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DpNmN8CvP6HnEsdTGAjVUr
- Snapshot cache: a process-wide GitRepoSnapshotCache keyed by repo address.
The browser ViewModel serves an already-fetched default-branch snapshot
synchronously, so the stats render in share-to-image and don't re-fetch when
switching screens.
- Issues/PR screens: filter chips now live inside the disappearing top bar
(via the scaffold's belowBar slot) so they hide with it instead of leaving a
static black band; the feed uses normal content padding.
- New issue is now an extended FAB on the Issues screen.
- Top bar shows the repo description as a single-line subtitle under the name;
removed the duplicate description from the home body.
- Tighter spacing: home sections, code header rows (branch row / search /
breadcrumb).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DpNmN8CvP6HnEsdTGAjVUr
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
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
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
- Recent-activity rows navigate to the issue/PR they represent (routeFor).
- The last-commit strip is now tappable: on the home it opens the Code
screen; on the feed repo card it opens the repository.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DpNmN8CvP6HnEsdTGAjVUr
Home screen:
- Nav cards: tighter vertical spacing; badges now count only OPEN issues/PRs,
derived from the live GitStatusIndex (started on the home so the split is
correct without visiting the Issues screen first).
- Moved the reaction row to after the recent-activity pulse.
- Added the standard 3-dot note menu (MoreOptionsButton) to the top bar.
Feed card (RenderGitRepositoryEvent):
- Replaced the web/clone links with the same stat tiles + language bar +
last-commit strip used on the home, loaded from a lazily-fetched shallow
snapshot. (Factory made internal so the card can build the browser VM.)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DpNmN8CvP6HnEsdTGAjVUr
- Social bar now renders the app's canonical ReactionsRow (reply, boost,
like, zap, zapraiser, reaction gallery) instead of a bespoke subset, so
the repository announcement behaves exactly like any other note.
- Code browser file rows: replace the heavy 32dp boxed icon with a plain
20dp icon and tighten spacing/padding, reducing the oversized horizontal
gap before the file name.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DpNmN8CvP6HnEsdTGAjVUr