- When a chip has no alternative rails, match the right inset to the left so the
icon+amount pill stays symmetric (was cramped against the right edge).
- Alternative-rail circles shrunk toward the amount's font height (28→22dp) with
a smaller icon inside (ZapRailIcon gained a size param; alternatives render at
14dp), so they stay quiet next to the bigger coloured preferred logo.
- The amount text is now neutral (onSurface) instead of taking the rail's brand
colour — only the leading logo carries colour, so amounts don't shout across
the feed. Dropped the now-unused zapRailAccent helper.
https://claude.ai/code/session_01HNE2z7CSYZ2G8KwC5fziJn
Replaces the per-relay backoff-token approach (opaque WebsocketBuilder
config token + BasicRelayClient bookkeeping) with a much smaller check at
the source.
The connector already holds the two OkHttp clients, and those are rebuilt
only when something connection-relevant changes (Tor's SOCKS port appears,
wifi<->cellular switch). Everything else it wakes on — Tor bootstrap status
churn, connectivity blips, self-heal restarts — leaves the clients
untouched. So instead of threading a config token through quartz, the
connector now forces a backoff-skipping reconnect (ignoreRetryDelays=true)
only when an OkHttp client instance actually changed; otherwise it lets
each relay's exponential backoff decide. That stops the
reconnect-fail-reconnect loop while Tor boots, and still reconnects every
relay (Tor and clearnet alike) the instant the transport changes.
Reverts the quartz/OkHttpWebSocket changes from the previous commit and
wires the connector to take the StateFlows it actually consumes (the two
client flows + connectivity/tor status) instead of the manager objects,
which also decouples it from DualHttpClientManagerForRelays/
ConnectivityManager/TorManager.
https://claude.ai/code/session_01SCz8kdYs2FwesEyzbhmRPY
Bump the pixel sunglasses scale (1.45 -> 1.70) so the shades read bolder and
overhang the slimmed, rounded cashew body more prominently. Body outline and
1.2 stroke weight unchanged.
Long-press a preset chip to pick it up and drag it to a new position in the
FlowRow; release to drop. Hand-rolled (no new dependency):
- VM gains moveAmount(from, to) to reorder the preset list.
- Each chip records its bounds (boundsInParent) and is wrapped in key(amount) so
reordering doesn't restart the in-flight drag gesture; the dragged chip follows
the finger via graphicsLayer translation and lifts with zIndex.
- The drop target is the chip whose bounds contain the pointer; on cross-over the
list reorders and the drag offset is rebased so the chip stays under the finger.
Identity (drag state, bounds map, target) is keyed by the amount value, not the
slot index, to avoid stale-index bugs across reorders.
https://claude.ai/code/session_01HNE2z7CSYZ2G8KwC5fziJn
The Nostr Wallet Connect section (connect button, paste, QR, manual pubkey/relay/
secret) is removed from the zap-amount settings content. NWC setup now lives only
in the wallet area:
- The `nostr+walletconnect` connect deep link (`dlnwc`) now opens Wallet → Add NWC
with the URI prefilled (Route.WalletAddNwc gained an optional nip47 arg), instead
of the old shared NIP-47 setup screen.
- UpdateZapAmountContent loses its nip47uri parameter and the whole wallet-connect
block; both callers (zap settings + NIP-47 setup) updated. The NIP-47 setup
screen keeps its Lightning-address and payment-targets sections (both also
reachable from profile edit / EditPaymentTargets) and is no longer the deep-link
target.
https://claude.ai/code/session_01HNE2z7CSYZ2G8KwC5fziJn
Replace the blocky pixel-stepped cashew silhouette with a smooth rounded
outline (corner-cut and emitted as a compact Bézier spline) so the nut reads
as a clean curved shape instead of a staircase. Use a round stroke cap/join at
the same 1.2 weight; the pixel "deal-with-it" sunglasses stay a solid fill.
Quick-zap preset chips now render like the real feed chip instead of a plain
"⚡ N" InputChip: the amount with the rails a preset of that size could use —
the amount-tier default rail in colour on the left, the alternatives (cashu /
Lightning / on-chain) in monochrome — with an X to remove. The bolt emoji is
gone; Lightning is the Material Symbols bolt, matching the feed.
Refactor: ZapRail, ZapRailIcon, the tier thresholds, and a new
previewRailsFor / previewPreferredRail / zapRailAccent are now internal so both
the feed chip and the settings preview share one source of truth for rail
choice, ordering, colouring, and the default highlight.
https://claude.ai/code/session_01HNE2z7CSYZ2G8KwC5fziJn
Squeeze the cashew outline horizontally (~0.72) and scale the pixel sunglasses
up (~1.45) so the shades overhang the body for a bolder, more recognizable
mark. Stroke weight stays at 1.2 to match the shared Zap outline icon.
- Header is now a visual: your avatar → cashu zap glyph → arrow → the recipient's
avatar, centered, replacing the explanatory subtitle text.
- Multiple Lightning wallets: each configured NWC wallet is its own "Funds from"
option (by name), defaulting to the configured default wallet; the chosen
wallet's URI is used to auto-pay. Falls back to a single external-invoice option
when no NWC wallet is set up.
- Separated the two amounts: the send (zap) amount is fixed; the editable field is
now the top-up amount, defaulting to the shortfall and adjustable upward.
Funding mints/moves the top-up; the nutzap always sends the fixed amount.
- Muted the selected source styling — the radio button carries the signal, so the
card uses a faint primary wash + a thin low-opacity border instead of a heavy
2dp outline and filled container.
- Bottom summary now reads "Top up X sats to <mint> and zap <name> Y sats · fee ≈ Z
sats" (and "Zap <name> Y sats from <mint>" when no top-up is needed), using the
recipient's display name.
https://claude.ai/code/session_01HNE2z7CSYZ2G8KwC5fziJn
Convert the multi-tone "deez nuts" Cashu/nutzap logo into a single-color,
tintable outline icon so it behaves like a Material Symbol glyph: the cashew
body is a hollow stroke (1.2 weight, matching the shared Zap outline icon) and
the pixel sunglasses stay a solid fill so they read at small sizes.
Drop the `tint = Color.Unspecified` overrides at every call site (zap chips,
nutzap rows/gallery, redeem, wallet screens) so the icon now tints with the
surrounding content colour instead of being locked to the brand browns, and
remove the imports/comments that only existed to preserve the old multi-tone
rendering.
Before Tor is ready, relays were disconnecting and immediately
reconnecting, ignoring BasicRelayClient's exponential backoff.
RelayProxyClientConnector calls reconnect(ignoreRetryDelays = true) on
every infrastructure change. That flag flows pool-wide into
connectAndSyncFiltersIfDisconnected and unconditionally bypassed the
backoff gate. While Tor is still bootstrapping its SOCKS port isn't
listening, so each unrelated infra event (connectivity transitions,
self-heal restarts, Tor status churn) forced an immediate reconnect that
failed again — the backoff was computed (delay kept doubling) but never
consulted.
Make the bypass per-relay and conditional on the relay's transport
config actually changing since its last attempt:
- WebsocketBuilder gains connectionConfig(url): an opaque,
value-comparable token of the transport config (proxy + timeouts).
Default null = untracked, preserving legacy always-bypass behavior for
in-process/standalone/test/desktop builders.
- BasicRelayClient records the token at each attempt and only lets a
forced reconnect skip the backoff when the token changed. A Tor relay
whose SOCKS config is unchanged keeps honoring its backoff; the moment
Tor flips to active (proxy port appears) the token changes and it
reconnects immediately. Clearnet relays still retry immediately
whenever their own client changes.
- OkHttpWebSocket.Builder reports the proxy+timeout fingerprint, reused
by needsReconnect() to avoid drift.
Adds BasicRelayClientBackoffTest covering the honored/bypassed/untracked
cases.
https://claude.ai/code/session_01SCz8kdYs2FwesEyzbhmRPY
- The default action is now one tap target: the preferred rail's logo hugs the
amount (no separate button), and a tap anywhere on icon+amount fires that rail.
- Alternative rails render as distinct circular buttons (filled surfaceVariant
circle) to their right, so it's clear they're individually tappable.
- The amount takes the preferred rail's accent colour (BitcoinOrange for
Lightning/on-chain, primary for reload; neutral for the multi-tone cashu logo,
which has no single brand colour).
- Reworked the preview to exercise every rail combination across the amount tiers
(5 / 100 / 5k / 100k sats): all-rails, cashu-only, lightning-only, and a
funds-split reload scenario.
Refactor: split the per-rail logo (ZapRailIcon, click-free) from the tap target
so it can be reused both inside the default area and inside RailButton.
https://claude.ai/code/session_01HNE2z7CSYZ2G8KwC5fziJn
- Default rail is now amount-tiered: under 10 sats prefers cashu (Lightning
min/fees make tiny zaps awkward), over 10k sats prefers an on-chain
transaction, and the middle defaults to Lightning — each falling back through
the remaining rails when the tier's rail isn't available. The tap action and
the colored leftmost logo both follow this single `preferred` choice.
- The preferred rail's logo now sits to the LEFT of the amount (in colour); the
alternative rails follow on the right in monochrome.
- Shrank the cashu logo to 15dp (~17%) so it no longer dwarfs the Material
symbol rails it sits beside; the reload "+" badge scaled to match.
https://claude.ai/code/session_01HNE2z7CSYZ2G8KwC5fziJn
Top-up (formerly "Reload Mint") screen:
- Renamed throughout to "Top up mint" / "Top up & send zap".
- Amount is now editable (numeric field, seeded from the tapped preset). The VM
recomputes shortfall/sources/fees on every edit; if the chosen amount already
fits the target mint, the screen drops the funding step and just sends.
- Selected funding source is now obvious: radio button + 2dp primary border +
primaryContainer fill, instead of a faint 12%-alpha tint.
- Each source's balance/description moved to its own line under the title, so
the Lightning explanation wraps instead of clipping.
- Removed the redundant "Your balances" list (it duplicated the per-mint balance
the Funds-from rows already show); the target's current balance now sits in the
destination card.
- Summary reads "Send AAA sats from XXX to YYY, fee Z sats" (or "Send AAA sats to
YYY" when no top-up is needed).
- Balances are now reactive: the VM observes the wallet's tokenEntries/mints
flows and rebuilds as proofs arrive from relays, instead of a one-time snapshot
that could show only the first-loaded mint.
Zap chip:
- The preferred rail (what a plain tap triggers) is rendered first and in full
colour; the alternative rails follow to its right in monochrome.
https://claude.ai/code/session_01HNE2z7CSYZ2G8KwC5fziJn
The NEEDS_RELOAD affordance rendered two full 18dp icons side by side (cashu +
"+"), making the chip nearly twice as wide as the other rails. Overlay the "+"
as a small 11dp corner badge on the dimmed cashu logo so it occupies a single
logo's footprint like Lightning/on-chain.
https://claude.ai/code/session_01HNE2z7CSYZ2G8KwC5fziJn
RenderPodcastMetadata and RenderPodcastEpisode rendered their description
tag as a plain Text. Both now use TranslatableRichTextViewer (rich text +
translation), matching the podcast screen header. The metadata card finally
uses its previously-ignored canPreview/backgroundColor params; the episode
card gives its short description a distinct id so it doesn't share remember
state with the markdown body below it.
- The show description on the podcast screen now renders through
TranslatableRichTextViewer (rich text + translation), matching how a
profile's About is shown, instead of a plain Text. PodcastHeader takes a
nav for the rich content's links.
- Audio playback now shows a decorative per-episode waveform when the
source has none, instead of a flat strip — same behaviour as music
tracks. Extracted MusicTrack's private syntheticWaveformFor into a shared
wavefront/SyntheticWaveform.kt used by both music and podcast players; the
podcast player still prefers a real IMeta waveform when one is present.
Follow-up to dropping .sorted() from mergeZapAmounts: update ZapAmountMergeTest
to lock in first-seen order (Lightning list, then on-chain extras) rather than
ascending, so a user's saved ordering is provably preserved.
https://claude.ai/code/session_01HNE2z7CSYZ2G8KwC5fziJn
Correctness:
- ReloadMintViewModel.confirm() now guards against re-entry (only starts from
Configuring/Failed and flips to Working synchronously), so a double-tap or a
Failed-state Retry can't launch two pipelines that double-spend the source /
double-mint the target.
- sendNutzapAndFinish awaits the real CashuWalletState.sendNutzap (throws on
failure) and reports Done only on success — a send that fails after a
successful reload now surfaces as Failed instead of popping the screen on a
premature "done" and silently stranding the moved funds.
- The reload pipeline runs on the long-lived AccountViewModel scope; the VM now
holds its Job and cancels it in onCleared(), so leaving the screen stops the
(up to 3-minute) Lightning poll instead of hammering the mint unobserved.
- rebalance() poll budget widened to a steady ~60s so a merely-slow mint no
longer strands funds that already left the source.
- Mint a small headroom buffer (RELOAD_FEE_BUFFER_SATS) above the bare shortfall
so the follow-up nutzap's own swap fee doesn't leave the target a sat short;
the source-feasibility gate accounts for it.
Regressions from the settings merge:
- mergeZapAmounts / the picker no longer .sorted() the amounts — a user's saved
preset order is preserved instead of being silently reordered ascending.
- The on-chain send dialog falls back to DEFAULT_ONCHAIN_ZAP_SATS when the
unified list has nothing above the on-chain minimum, restoring the guaranteed
quick-pick preset.
- zapClick's one-tap (single-amount) path now checks rail capability and opens
the picker when the recipient can't receive Lightning, instead of firing a
doomed Lightning zap.
Cleanup:
- Centralized the mint-quote "settled" predicate as MintQuoteBolt11ResponseDto
.isSettled(); rebalance, ReloadMintViewModel and CashuWalletViewModel now
share it instead of three copies of paid==true || PAID || ISSUED.
https://claude.ai/code/session_01HNE2z7CSYZ2G8KwC5fziJn
Audit follow-ups on the podcast screen:
- Extract the duplicated GetMediaItem -> GetVideoController -> RenderVoicePlayer
block (and the FLAT_WAVEFORM constants) from both PodcastEpisode and
PodcastEpisodeListItem into one PodcastEpisodeAudioPlayer composable. The
border shape stays a parameter (bottom-rounded under the cover vs. fully
rounded in the list).
- Fix the list row clipping the 75dp play button: the row hard-coded 72dp,
below the voice player's 80dp minimum. Height now lives in the shared
player so both call sites stay identical.
- Header no longer flashes "0 episodes" while the relay request is in flight:
episodeCount is nullable and the count/divider only render once loaded.
- Drop the odd uppercasing of the episode date (looked wrong on relative
values like "3d").
When the cashu logo on an amount chip is NEEDS_RELOAD (you hold enough cashu,
but not in the recipient's mint), it now shows a dimmed logo with a "+" badge
that opens a full Reload Mint screen instead of silently falling back to
Lightning.
The screen tells the whole story and auto-sends the nutzap on success:
- Destination ("mint to reload") with its shortfall; selectable when the
recipient accepts several mints we hold.
- All cashu balances for context.
- Source ("funds from"): another mint (mint-to-mint rebalance, no new sats) or
Lightning. Mint rows are disabled when they can't cover shortfall + fee.
Funding paths:
- Rebalance: CashuWalletState.rebalance moves cashu between mints.
- Lightning: mint a fresh quote at the destination, auto-pay via NWC when a
wallet is configured (else show the invoice to copy), poll, then issue.
Wiring: new Route.ReloadMint + AppNavigation entry; a ReloadMintRequest handed
off via an LruCache on AccountViewModel (mirrors the manual-zap pattern);
onReloadNutzap threaded through the zap popup overloads and the chip; callers
(ReusableZapButton, ReactionsRow, NestActionBar) navigate to the screen.
IMPOSSIBLE (not enough cashu anywhere) keeps falling back to Lightning — the
reload screen is only for moving existing cashu.
https://claude.ai/code/session_01HNE2z7CSYZ2G8KwC5fziJn
Each item in the Podcasts feed (kind 10154 show metadata) now opens a
dedicated podcast screen built around the show's pubkey — per NIP-F4 a
podcast is its own keypair, so its metadata and every episode (kind 54)
share one author.
- New Route.Podcast(pubkey) + AppNavigation binding.
- PodcastScreen: modern hero header (cover art, title, websites, tappable
expand/collapse description, "N episodes" section) followed by every
episode in a LazyColumn.
- PodcastEpisodeListItem: compact "show list" row (date, title, summary)
with an inline audio player so episodes play without leaving the list.
- OnePodcast datasource (per-user EOSE manager) fetches the show metadata
and episodes from the podcast key's outbox relays.
- OnePodcastEpisodesFeedFilter + OnePodcastFeedViewModel scope LocalCache
to a single podcast's episodes.
- Podcasts feed metadata card is now clickable with a "View episodes"
affordance.
Foundation for the Reload Mint screen (NEEDS_RELOAD funding path):
- peekMintBalances(): per-mint spendable balance from in-memory proofs
(synchronous; feeds the balances list and source-feasibility checks).
- recipientSharedMints(): the recipient's accepted mints we also hold —
the candidate reload destinations.
- rebalance(source, target, sats): mints a fresh quote at the target and
pays its invoice by melting at the source, moving cashu between mints with
no new sats. Verifies the source covers amount + fee before spending,
abandons the unpaid quote otherwise, and leaves a resumable kind:7374 if the
target hasn't confirmed yet — money is never lost on failure.
https://claude.ai/code/session_01HNE2z7CSYZ2G8KwC5fziJn
The on-chain rail no longer has its own editable preset list. There is now
one zap-amount list used by every rail; on-chain simply filters it by the
shared MIN_ONCHAIN_ZAP_SATS floor (1000 sat), and the unified chip already
gates each logo per amount.
- Settings: drop the public onchainZapAmountChoices StateFlow and its editor
section in the Update Zap Amount dialog. The serialized field is kept for
backward/cross-client (NIP-78) compatibility and migration: on load the two
saved lists are unioned into zapAmountChoices (mergeZapAmounts), and on save
the on-chain-eligible subset is written back so older clients still get
sensible on-chain presets. The round-trip is idempotent.
- updateZapAmounts / changeOnchainZapAmounts lose the separate on-chain
parameter throughout (Account, AccountViewModel, UpdateZapAmountViewModel).
- OnchainZapSendDialog draws its quick presets from the single list filtered
by the minimum.
- MIN_ONCHAIN_ZAP_SATS is now a single shared constant in the model, replacing
the copies previously private to OnchainZapSendDialog and ReactionsRow.
- Remove the now-unused on-chain amount strings; clarify the zap-amounts
explainer to note amounts apply to all rails.
Adds ZapAmountMergeTest covering the union/dedup/idempotent-round-trip.
https://claude.ai/code/session_01HNE2z7CSYZ2G8KwC5fziJn
Replace the three parallel chip rows (cashu / lightning / on-chain) in the
zap amount popup with a single pill per amount that shows a tappable logo
for each rail that can actually pay it. Tapping the amount fires the
cheapest/fastest available rail (cashu funded -> lightning -> on-chain);
tapping a specific logo forces that rail.
Rail gating:
- Cashu: precise per-amount status (FUNDED / NEEDS_RELOAD / IMPOSSIBLE /
UNAVAILABLE) computed from in-memory proofs — the one balance that is free
to read synchronously. Only FUNDED is offered for now.
- Lightning: optimistic — an external wallet can pay any invoice, so it's
offered whenever the recipient can receive, with no sender-balance fetch.
- On-chain: offered when a backend is configured and the amount clears the
on-chain minimum; UTXO sufficiency stays a send-time check.
Also fixes the original "No proofs available at <mint>" failure: nutzap mint
selection now picks the shared mint where we hold the most balance instead of
the first one the recipient lists (peekNutzapFunding), so a zap no longer
fails when funds sit in a different shared mint. Cashu capability is now
resolved against the note author (who sendNutzap actually pays) rather than
split recipients.
Amount presets from the (still separate) lightning and on-chain settings are
merged into one sorted set for display.
https://claude.ai/code/session_01HNE2z7CSYZ2G8KwC5fziJn
The cashu zap chip previously fired its async send and closed the
popup with no visible motion until the kind:9321 round-tripped
back (1-2 seconds of "did the tap register?" silence). Wire the
same zappingProgress channel the lightning path uses so the bar
visibly animates from tap to completion.
Pieces:
- CashuWalletOps.sendNutzap: optional `onProgress: (Float) -> Unit`
emitting at the network checkpoints — 0.55 after the swap (the
long mint round-trip), 0.80 after the kind:9321 publish (the
recipient can see it), 0.95 after keep + delete, 1.0 after the
kind:7376 history.
- CashuWalletState.sendNutzap: forwards onProgress to ops and
also emits 0.20 after scrubLocallyStaleProofs so the bar moves
before the slow swap call begins.
- AccountViewModel.sendNutzap: accepts an `onProgress: (Float) -> Unit`
parameter (default no-op so existing callers compile unchanged)
and threads it through to CashuWalletState.
- ReactionsRow.onNutzap callback: emits 0.05 immediately on tap
for instant click feedback, then passes the same `onProgress`
channel the lightning chip uses so zappingProgress drives the
same progress indicator. The existing onError reset already
zeroes the bar on failure.
Resulting cadence at the UI:
tap → 0.05 (instant)
→ 0.20 (scrub done)
→ 0.55 (mint swap done)
→ 0.80 (kind:9321 out)
→ 0.95 (token rollover + NIP-09 out)
→ 1.00 (history out, bar hides).
The grace-period unsubscribe in LifecycleAwareKeyDataSourceSubscription ran
on the composition scope from rememberCoroutineScope(), whose dispatcher is
coupled to the UI frame clock. When the app is backgrounded the frame clock
stops ticking, so the pending unsubscribe could be starved and never fire.
Because closing the REQ is what drives the relay disconnect (via desiredRelays
-> RelayPool.updatePool), the connection could linger indefinitely. This is
most visible on the relay feed, whose dedicated one-off relay is kept alive by
nothing else.
Drive the grace timer from Lifecycle.currentStateFlow on a dedicated
Dispatchers.Default scope instead. collectLatest cancels the pending delay
automatically when the lifecycle returns to STARTED, preserving the 30s
app-switch grace while ensuring the timer fires reliably in the background.
https://claude.ai/code/session_01SesftJphLwvLtn1fJB5zx8
Opens the App Drawer (same as Cmd+K) for quick access to all
available screen types. Positioned between main nav and FEEDS section.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Revert spacer change (back to 60dp)
- Add top = 16.dp to LazyColumn contentPadding so first card has
extra spacing and slides nicely under the floating search header
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>