aboutsummaryrefslogtreecommitdiffstats
path: root/src/features/messages.js
diff options
context:
space:
mode:
Diffstat (limited to 'src/features/messages.js')
-rw-r--r--src/features/messages.js65
1 files changed, 57 insertions, 8 deletions
diff --git a/src/features/messages.js b/src/features/messages.js
index 5535362..625f10c 100644
--- a/src/features/messages.js
+++ b/src/features/messages.js
@@ -1839,7 +1839,9 @@ export function messagesFeature(ctx) {
if (cur.seen.has(ev.id)) continue;
cur.seen.add(ev.id);
cur.sats += sats;
- if (from && my.includes(from)) cur.mine = true;
+ // our own zap's receipt: the real thing has landed, so the optimistic
+ // amount the chip has been carrying since the tap steps aside
+ if (from && my.includes(from)) { cur.mine = true; zapPending.delete(x[1]); }
changed = true;
}
if (changed) scheduleRepaint();
@@ -1873,15 +1875,53 @@ export function messagesFeature(ctx) {
: n < 1e6 ? Math.round(n / 1000) + 'k'
: (n / 1e6).toFixed(1).replace(/\.0$/, '') + 'M';
const BOLT_SVG = '<svg width="11" height="11" viewBox="0 0 24 24" fill="currentColor" stroke="none" style="display:block"><path d="M13 2L4 14h7l-1 8 9-12h-7l1-8z"/></svg>';
- // The chip: a little bolt + the sats total; absent until the first receipt.
+
+ // A tapped zap takes a few seconds to pay, and its public receipt trails
+ // the payment by seconds more — long enough that the tap looked ignored.
+ // So the chip appears the instant you tap, carrying your amount and
+ // pulsing while the payment is in flight; it goes solid when the payment
+ // lands and hands over to the real receipt when that arrives. A failure
+ // takes it straight back off.
+ const ZAP_FLIGHT_MS = 45_000; // nothing reported back: assume it's gone
+ const ZAP_PAID_MS = 5 * 60_000; // paid, but the receipt never showed
+ const zapPending = new Map(); // id -> { sats, at, paid }
+ const pendingLive = (p) => p && Date.now() - p.at < (p.paid ? ZAP_PAID_MS : ZAP_FLIGHT_MS);
+ function pendingOf(id) {
+ const p = zapPending.get(id);
+ if (p && !pendingLive(p)) { zapPending.delete(id); return null; }
+ return p || null;
+ }
+ function markZapPending(id, sats) {
+ if (!id || !sats) return;
+ zapPending.set(id, { sats, at: Date.now(), paid: false });
+ scheduleRepaint();
+ // repaint when the in-flight chip would expire, so a zap nobody ever
+ // reported on doesn't pulse forever
+ setTimeout(scheduleRepaint, ZAP_FLIGHT_MS + 200);
+ }
+ // A zap flow reporting back: paid (keep the amount, stop pulsing) or
+ // failed (the chip was never real — take it off).
+ function settleZap(id, ok, sats) {
+ if (!id) return;
+ if (!ok) { if (zapPending.delete(id)) scheduleRepaint(); return; }
+ const p = zapPending.get(id) || { sats: sats || 0, at: Date.now() };
+ zapPending.set(id, { ...p, at: Date.now(), paid: true, sats: sats || p.sats });
+ scheduleRepaint();
+ }
+ // The chip: a little bolt + the sats total. Absent until the first receipt
+ // — or until you zap it yourself, which is its own kind of receipt.
function zapChip(id, { onClick, cls = '' } = {}) {
const z = zapTotals.get(id);
- if (!z || !z.sats) return null;
+ const p = pendingOf(id);
+ const sats = (z ? z.sats : 0) + (p ? p.sats : 0);
+ if (!sats) return null;
+ const flying = p && !p.paid;
return h('span', {
- class: 'zap-tally' + (z.mine ? ' on' : '') + (onClick ? ' clickable' : '') + (cls ? ' ' + cls : ''),
- title: t('zapTallyTitle', { n: z.sats.toLocaleString() }),
+ class: 'zap-tally' + ((z && z.mine) || p ? ' on' : '') + (flying ? ' flying' : '')
+ + (onClick ? ' clickable' : '') + (cls ? ' ' + cls : ''),
+ title: flying ? t('zapSending') : t('zapTallyTitle', { n: sats.toLocaleString() }),
onClick: onClick ? (e) => { e.stopPropagation(); onClick(); } : undefined,
- }, h('span', { style: 'display:flex', html: BOLT_SVG }), fmtSats(z.sats));
+ }, h('span', { style: 'display:flex', html: BOLT_SVG }), fmtSats(sats));
}
// Zap a chat message or DM: same one-tap flow as a post (ark first,
// Lightning fallback, the amount remembered from the first time).
@@ -2023,8 +2063,10 @@ export function messagesFeature(ctx) {
const npubStr = npubOf(pk);
const def = ctx.zapDefaultSat ? ctx.zapDefaultSat() : 0;
if (!def) { ui.zapSetup = { pk, npub: npubStr, eventId: ev.id, amount: '21' }; render(); return; }
+ markZapPending(ev.id, def); // the chip answers the tap; the flow reports back
if (!hook('zapNpub', pk, npubStr, ev.id, def) && !hook('lnZapNpub', pk, npubStr, ev.id, def)) {
// no instant path in this build — the classic form flow
+ settleZap(ev.id, false);
ui.profilePk = null;
ui.chatOpen = false;
ui.tab = 'send';
@@ -2051,7 +2093,9 @@ export function messagesFeature(ctx) {
const { pk, npub, eventId } = s;
ui.zapSetup = null;
render();
- if (!hook('zapNpub', pk, npub, eventId, n)) hook('lnZapNpub', pk, npub, eventId, n);
+ markZapPending(eventId, n);
+ if (!hook('zapNpub', pk, npub, eventId, n) && !hook('lnZapNpub', pk, npub, eventId, n)) settleZap(eventId, false);
+ recheckZap(eventId);
} }, t('zapSetupSave'))),
h('button', { class: 'btn-ghost btn-block', onClick: () => { ui.zapSetup = null; render(); } }, t('back')));
}
@@ -3475,6 +3519,11 @@ export function messagesFeature(ctx) {
return {
id: 'messages',
+ // A zap flow reporting how its payment went, so the chip that appeared
+ // on tap can stop pulsing (paid) or disappear (failed / never fired).
+ // Also the way a zap sent from a form reaches the tally right away,
+ // without waiting on its receipt.
+ zapSettled(eventId, ok, sats) { settleZap(eventId, ok, sats); return true; },
// Chat lives behind a header button and takes over the whole screen —
// no balance card, no tabs; each view carries its own way back.
// The bare avatar node for the app header's identity menu.
@@ -3662,7 +3711,7 @@ export function messagesFeature(ctx) {
listsSynced = false;
if (zapLiveUnsub) { try { zapLiveUnsub(); } catch {} zapLiveUnsub = null; }
clearTimeout(zapTimer); zapTimer = null; zapQueue = new Set(); zapRecent = [];
- zapTotals.clear(); zapAsked.clear(); // 'mine' is per identity — refetch under the next
+ zapTotals.clear(); zapAsked.clear(); zapPending.clear(); // 'mine' is per identity — refetch under the next
},
};
}