mirror of
https://github.com/vitorpamplona/amethyst.git
synced 2026-08-10 16:33:27 +00:00
feat(nutzap): progress bar during send, matching lightning UX
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).
This commit is contained in:
@@ -616,6 +616,7 @@ class CashuWalletOps(
|
||||
zappedEvent: EventHintBundle<out Event>,
|
||||
message: String,
|
||||
available: List<TokenEntry>,
|
||||
onProgress: ((Float) -> Unit)? = null,
|
||||
): NutzapSent {
|
||||
if (amountSats <= 0) throw IllegalArgumentException("Amount must be positive")
|
||||
seedWarmer()
|
||||
@@ -628,6 +629,10 @@ class CashuWalletOps(
|
||||
recipientP2pkPubkeyHex = recipientP2pkPubkeyHex,
|
||||
targetSplit = amountSats,
|
||||
)
|
||||
// Mint round-trip done — biggest chunk of the wall clock. Surface
|
||||
// visible motion here so the user knows the click registered even
|
||||
// before the recipient sees the kind:9321.
|
||||
onProgress?.invoke(0.55f)
|
||||
|
||||
// Build the kind:9321 first so we have its id to reference from history.
|
||||
val proofJsons = swap.send.map { nutzapProofJson.encodeToString(NutzapProofJson.serializer(), it.toNutzapJson()) }
|
||||
@@ -642,6 +647,8 @@ class CashuWalletOps(
|
||||
)
|
||||
val nutzapEvent = signer.sign(nutzapTemplate)
|
||||
publish(nutzapEvent)
|
||||
// kind:9321 is out — recipient's auto-redeem can fire from here.
|
||||
onProgress?.invoke(0.80f)
|
||||
|
||||
// Roll over change locally if any.
|
||||
val keepEvent =
|
||||
@@ -663,6 +670,7 @@ class CashuWalletOps(
|
||||
publish(it)
|
||||
}
|
||||
}
|
||||
onProgress?.invoke(0.95f)
|
||||
|
||||
val historyTemplate =
|
||||
CashuSpendingHistoryEvent.build(
|
||||
@@ -677,6 +685,7 @@ class CashuWalletOps(
|
||||
)
|
||||
val historyEvent = signer.sign(historyTemplate)
|
||||
publish(historyEvent)
|
||||
onProgress?.invoke(1.0f)
|
||||
|
||||
return NutzapSent(
|
||||
nutzapEvent = nutzapEvent,
|
||||
|
||||
@@ -1021,6 +1021,7 @@ class CashuWalletState(
|
||||
recipientPubKey: HexKey,
|
||||
zappedEvent: EventHintBundle<out Event>,
|
||||
message: String = "",
|
||||
onProgress: ((Float) -> Unit)? = null,
|
||||
): NutzapSent {
|
||||
check(started) { "CashuWalletState.start() not called" }
|
||||
val target =
|
||||
@@ -1033,6 +1034,10 @@ class CashuWalletState(
|
||||
// partial-failure persists until the user clicks send. Heal
|
||||
// first so the selection below works on a known-fresh view.
|
||||
scrubLocallyStaleProofs(target.mintUrl)
|
||||
// First on-network step done. The caller already showed an
|
||||
// instant 0.05 when the chip was tapped; lift to 0.20 here so
|
||||
// the bar visibly moves even before the (slow) swap call.
|
||||
onProgress?.invoke(0.20f)
|
||||
|
||||
val available = _tokenEntries.value.filter { it.content.mint == target.mintUrl }
|
||||
if (available.isEmpty()) {
|
||||
@@ -1047,6 +1052,7 @@ class CashuWalletState(
|
||||
zappedEvent = zappedEvent,
|
||||
message = message,
|
||||
available = available,
|
||||
onProgress = onProgress,
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -2012,11 +2012,16 @@ fun ZapAmountChoicePopup(
|
||||
nutzapAmountChoices = if (nutzapEnabled) zapAmountChoices else persistentListOf(),
|
||||
onNutzap = { amountInSats ->
|
||||
onZapStarts()
|
||||
// Instant click feedback. Without this initial nudge
|
||||
// the bar stays flat for ~1s until scrubLocallyStale-
|
||||
// Proofs returns; the user thinks the tap was lost.
|
||||
onProgress(0.05f)
|
||||
accountViewModel.sendNutzap(
|
||||
baseNote = baseNote,
|
||||
amountSats = amountInSats,
|
||||
message = "",
|
||||
onError = onError,
|
||||
onProgress = onProgress,
|
||||
)
|
||||
visibilityState.targetState = false
|
||||
},
|
||||
|
||||
+2
@@ -923,6 +923,7 @@ class AccountViewModel(
|
||||
amountSats: Long,
|
||||
message: String,
|
||||
onError: (String, String, User?) -> Unit,
|
||||
onProgress: (Float) -> Unit = {},
|
||||
) = launchSigner {
|
||||
val recipient = baseNote.author?.pubkeyHex
|
||||
if (recipient == null) {
|
||||
@@ -948,6 +949,7 @@ class AccountViewModel(
|
||||
recipientPubKey = recipient,
|
||||
zappedEvent = zappedEvent,
|
||||
message = message,
|
||||
onProgress = onProgress,
|
||||
)
|
||||
// No success toast — the kind:9321 round-trips through the
|
||||
// cache, attaches to the target Note via addNutzap, and the
|
||||
|
||||
Reference in New Issue
Block a user