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
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
- 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
- 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
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
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
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>
- Add AccountState.Loading as initial state (was LoggedOut)
- Show centered "Amethyst" + spinner while accounts load from storage
- After loadSavedAccount(): transition to LoggedIn or LoggedOut
- No more 0.5s flash of login screen when account exists
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- COLLAPSED_WIDTH 56dp → 64dp so icons aren't truncated
- Tor connected: use Security icon (filled shield) instead of Shield
- Tor off/connecting/error: keep outlined Shield icon
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Replace compact icon Row at bottom with SidebarNavItem-style items
- Tor: shows "Tor: Off/Connecting/Connected/Error" with Shield icon
- Bunker: shows "Bunker: OK" with Favorite icon (only when connected)
- Both use same shape, hover, and label pattern as other sidebar items
- Collapsed mode: icon-only with tooltip, same as nav items
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Add LocalOpenFullSearch CompositionLocal (navigates to Search column)
- Provided at Main.kt level alongside LocalFeedSearchActive
- FeedScreen reads it directly — no param threading needed
- "Open full search" link in expanded header now actually opens Search column
- Removed unused onOpenSearch param from DeckColumnContainer
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- SearchPill: move hoverHighlight() inside Surface content Row so it's
clipped to the pill's 36dp height (was drawing on parent Row height)
- AccountSwitcherDropdown: reduce IconButton from 48dp to 40dp so ripple
circle fits within collapsed sidebar width
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- SidebarNavItem/SidebarFeedItem: .clip().clickable().background()
— ripple now clipped to RoundedCornerShape(8dp) bounds
- ColumnHeader: .padding() before .pointerInput() — gesture detection
respects horizontal padding
- SearchPill: .clip(pill shape) before .hoverHighlight() — hover
drawBehind rect clipped to pill shape, not parent rectangle
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Sidebar onNavigate callback now sets searchActiveState.value = false,
clearing the feed search expansion and sidebar dim overlay when the
user clicks any nav item (Home, Messages, Settings, etc.)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Relay query debounce stays at 300ms (AdvancedSearchBarState default)
- Separate 1s debounce on searchText: when user stops typing for 1s,
save the query to SearchHistoryStore (assumes intent confirmed)
- LaunchedEffect(searchText.text) auto-cancels on each keystroke,
so only fires after 1s of inactivity
- No duplicates: addToHistory() deduplicates by serialized query
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Increase relay query debounce from 300ms to 1000ms for inline search
(reduces unnecessary relay load while typing)
- Save query to SearchHistoryStore when search collapses (if non-empty)
- SearchHistoryStore.addToHistory() already deduplicates by serialized query
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- LinearProgressIndicator at top of expanded card (animated in/out)
- Loading state: centered icon + "Searching N relays..."
- Empty state: "No results found" / "No search relays configured"
- Results stream in incrementally from relays
- 1s debounce for relay queries, save to search history (no dupes)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Show "Searching N relays..." with spinner while waiting for results
- Show "No search relays configured" if searchRelays is empty
- Observe isSearching, peopleResults, noteResults directly for state
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
FeedTabsHeader now creates actual NIP-50 relay subscriptions when
typing in the inline search, matching the full SearchScreen wiring:
- Collect debouncedQuery (300ms) from AdvancedSearchBarState
- Access searchRelays from LocalRelayCategories
- rememberSubscription for people search (MetadataEvent kind 0)
- rememberSubscription for note search (SearchFilterFactory filters)
- Results flow into SearchResultsList (reused from search/ package)
- 10s timeout for silent relays
- Subscriptions auto-cleanup on collapse via DisposableEffect
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>