diff options
Diffstat (limited to 'src')
| -rw-r--r-- | src/assets/zap.mp3.base64.txt | 1 | ||||
| -rw-r--r-- | src/features/ark.js | 15 | ||||
| -rw-r--r-- | src/features/messages.js | 234 | ||||
| -rw-r--r-- | src/features/zaps.js | 9 | ||||
| -rw-r--r-- | src/i18n.js | 2 | ||||
| -rw-r--r-- | src/style.css | 40 | ||||
| -rw-r--r-- | src/zap-animation.js | 83 | ||||
| -rw-r--r-- | src/zap-sound.js | 59 |
8 files changed, 341 insertions, 102 deletions
diff --git a/src/assets/zap.mp3.base64.txt b/src/assets/zap.mp3.base64.txt new file mode 100644 index 0000000..10ae0a5 --- /dev/null +++ b/src/assets/zap.mp3.base64.txt @@ -0,0 +1 @@ +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 diff --git a/src/features/ark.js b/src/features/ark.js index 22ba5fd..dbc97f6 100644 --- a/src/features/ark.js +++ b/src/features/ark.js @@ -1514,7 +1514,16 @@ export function arkFeature(ctx) { // arkoor send and publishes a receipt referencing the delivered vtxo. // Kinds are provisional until a NIP lands. const ARK_INFO_KIND = 10037; // replaceable: ["ark", <address>], ["network", <net>] - const ARK_ZAP_KIND = 9737; // receipt: ["p", pk], ["e", note?], ["amount", sats], ["vtxo", id], ["network", net] + const ARK_ZAP_KIND = 9737; // receipt: ["p", pk], ["e", note?], ["amount", sats], ["vtxo", id], ["network", net], ["P", sender?] + // The receipt is signed by the wallet key, which has no profile. When the + // wallet is linked to a nostr login, name that identity as the sender (the + // same "P" tag NIP-57 receipts use) so the zap reads as the person, not + // as an npub nobody recognizes. + const zapSenderTag = () => { + const id = ctx.hook('nostrLoginIdentity'); + const me = wallet.nostrPubkey && wallet.nostrPubkey(); + return id && id.pubkey && id.pubkey !== me ? [['P', id.pubkey]] : []; + }; // npub decode without importing the nostr stack (keeps ark-only builds lean) function npubToHex(s) { @@ -1671,7 +1680,7 @@ export function arkFeature(ctx) { kind: ARK_ZAP_KIND, content: (z.comment || '').slice(0, 280), tags: [['p', z.pk], ...(z.eventId ? [['e', z.eventId]] : []), - ['amount', String(sats)], ['vtxo', vtxoId], ['network', getNetwork()]], + ['amount', String(sats)], ['vtxo', vtxoId], ['network', getNetwork()], ...zapSenderTag()], }).catch(() => {}); } @@ -1715,7 +1724,7 @@ export function arkFeature(ctx) { kind: ARK_ZAP_KIND, content: (z.comment || '').slice(0, 280), tags: [['p', z.pk], ...(z.eventId ? [['e', z.eventId]] : []), - ['amount', String(sats)], ['network', getNetwork()], ['gift', locked.url]], + ['amount', String(sats)], ['network', getNetwork()], ['gift', locked.url], ...zapSenderTag()], }).catch(() => {}); const dmText = t('giftDmText', { amount: fmtAmount(sats) + ' ' + unitLabel(), link: locked.url, code: locked.claimCode }); if (wallet.sendNostrDM) { diff --git a/src/features/messages.js b/src/features/messages.js index acde0cb..958cd8d 100644 --- a/src/features/messages.js +++ b/src/features/messages.js @@ -29,13 +29,14 @@ import { packAddr, packNaddr, parsePackRef, parsePackAddr, parseEmojiSet, } from '../emoji.js'; import { saveInbox } from '../dm-inbox.js'; -import { makeSearcher, resultRows, fallbackAvatar, warmSearch } from '../recipient-search.js'; +import { makeSearcher, resultRows, fallbackAvatar, warmSearch, punkImageUrl } from '../recipient-search.js'; import { getNetwork } from '../api.js'; import { decodeBolt11 } from '../ark/lightning.js'; import { hexToBytes, bytesToHex } from '@noble/hashes/utils'; import { sha256 } from '@noble/hashes/sha256'; import { base64urlnopad } from '@scure/base'; import { t } from '../i18n.js'; +import { animateZap, warmZapSound } from '../zap-animation.js'; import { SIGNER_SILENT } from '../dm.js'; // The coinos community's join material lives in ../community.js — shared @@ -544,7 +545,7 @@ export function messagesFeature(ctx) { if (!p || (!p.name && !p.picture)) return; const s = wallet.loadFeatureState('profiles', {}); s[pk] = { name: p.name || null, picture: p.picture || null, nip05: p.nip05 || null, lud16: p.lud16 || null, - about: p.about || null, banner: p.banner || null, t: Date.now(), + about: p.about || null, banner: p.banner || null, eventAt: p.eventAt || 0, t: Date.now(), ...(p.thumbFor === p.picture && p.thumb ? { thumb: p.thumb, thumbFor: p.thumbFor, thumbPx: p.thumbPx || 0 } : {}), ...(p.thumbFail ? { thumbFail: p.thumbFail, thumbFailAt: p.thumbFailAt || 0, thumbFails: p.thumbFails || 1, @@ -582,7 +583,7 @@ export function messagesFeature(ctx) { const PUNK_PIC_RE = /^(?:https?:\/\/(?:[a-z0-9-]+\.)*coinos\.io\/)?punks\/(\d{1,2})\.webp$/i; const localPunk = (url, big) => { const m = PUNK_PIC_RE.exec(url || ''); - return m ? (big ? `punks/${m[1]}.webp` : `punks-sm/${m[1]}.webp`) : null; + return m ? punkImageUrl(m[1], !big) : null; }; // A refreshed profile whose picture hasn't changed keeps the thumbnail we @@ -755,7 +756,10 @@ export function messagesFeature(ctx) { function applyProfile(pk, ev) { let m = null; try { m = JSON.parse(ev.content); } catch { m = null; } - const p = m ? { + if (!m || typeof m !== 'object' || Array.isArray(m)) return false; + const prev = profiles.get(pk); + if ((prev?.eventAt || 0) > ev.created_at) return true; + const p = { name: m.display_name || m.name || null, picture: m.picture || null, nip05: m.nip05 || null, @@ -767,14 +771,20 @@ export function messagesFeature(ctx) { // renders anyway. about: typeof m.about === 'string' ? m.about.slice(0, 1000) : null, banner: typeof m.banner === 'string' ? m.banner.slice(0, 400) : null, - } : null; - // An empty answer must never clobber a remembered face with a punk: keep - // what we had and just refresh the clock. - const prev = profiles.get(pk); - const entry = keepThumb(pk, p ? { ...p, t: Date.now(), miss: 0 } : { ...(prev || {}), t: Date.now() }); + }; + const entry = keepThumb(pk, { ...p, eventAt: ev.created_at, t: Date.now(), miss: 0 }); profiles.set(pk, entry); + // A page and the batched avatar lookup must share their answer. A slow + // page miss must never erase a name/photo the batch already found. + if (fullInFlight.has(pk) || fullProfiles.has(pk)) { + fullProfiles.set(pk, m); + fullFetched.add(pk); + fullMisses.delete(pk); + persistPage('full', pk, m); + } persistProfile(pk, entry); preloadPicture(entry); + return true; } async function fetchProfiles(pks) { @@ -789,7 +799,7 @@ export function messagesFeature(ctx) { const c = newest.get(ev.pubkey); if (!c || ev.created_at > c.created_at) newest.set(ev.pubkey, ev); } - for (const [pk, ev] of newest) { applyProfile(pk, ev); found.add(pk); } + for (const [pk, ev] of newest) { if (applyProfile(pk, ev)) found.add(pk); } // paint what this batch found before going after the stragglers — // the outbox fallback below can take seconds, and there's no reason // for a face we already have to wait behind one we don't @@ -808,7 +818,7 @@ export function messagesFeature(ctx) { const c = idxNewest.get(ev.pubkey); if (!c || ev.created_at > c.created_at) idxNewest.set(ev.pubkey, ev); } - for (const [pk, ev] of idxNewest) { applyProfile(pk, ev); found.add(pk); } + for (const [pk, ev] of idxNewest) { if (applyProfile(pk, ev)) found.add(pk); } if (idxNewest.size) scheduleRepaint(); } const stillLeft = pks.filter((pk) => !found.has(pk)); @@ -822,7 +832,7 @@ export function messagesFeature(ctx) { const c = newest.get(ev.pubkey); if (!c || ev.created_at > c.created_at) newest.set(ev.pubkey, ev); } - for (const [pk, ev] of newest) { applyProfile(pk, ev); found.add(pk); } + for (const [pk, ev] of newest) { if (applyProfile(pk, ev)) found.add(pk); } })); } // nobody has one: mark the clock so it's retried in minutes, not asked @@ -1471,7 +1481,7 @@ export function messagesFeature(ctx) { close(); setTimeout(() => document.getElementById('msg-draft')?.focus(), 50); }), - !ui.emojiPick && !mine && canZapPk(m.author) ? item('⚡', t('msgZap'), () => { close(); zapMessage(m.author, m.rumor.id); }) : null, + !ui.emojiPick && canZapPk(m.author) ? item('⚡', t('msgZap'), () => { close(); zapMessage(m.author, m.rumor.id); }) : null, ui.emojiPick ? null : item('⧉', t('copy'), async () => { try { await navigator.clipboard.writeText(msgSnippet(room, m, 100000)); toast(t('copied')); } catch {} close(); @@ -2773,6 +2783,8 @@ export function messagesFeature(ctx) { const fullProfiles = new Map(); // pk -> { raw kind0 content object, fetched_at } const fullFetched = new Set(); // fetched from relays this session + const fullInFlight = new Map(); + const fullMisses = new Set(); // The profile-page cache SURVIVES refreshes: bios and recent posts serve // instantly from storage while a background fetch freshens them. Bounded: @@ -2807,32 +2819,20 @@ export function messagesFeature(ctx) { const cached = pageCache().full[pk]; if (cached) fullProfiles.set(pk, cached.v); } - if (fullFetched.has(pk)) return; - fullFetched.add(pk); - queryOn([...new Set([...PROFILE_RELAYS, ...DM_RELAYS])], { kinds: [0], authors: [pk] }, 3500).then((evs) => { - const newest = evs.sort((a, b) => b.created_at - a.created_at)[0]; - let m = {}; - try { m = newest ? JSON.parse(newest.content) : {}; } catch {} - fullProfiles.set(pk, m); - persistPage('full', pk, m); - // A full fetch is the freshest word on this profile — stamp it into the - // light cache (and persist) so a picture changed elsewhere replaces the - // stale one everywhere within a session, not after the 24h TTL. The - // repaint covers the header/chat avatars, not just an open profile page. - const entry = { - name: m.display_name || m.name || null, picture: m.picture || null, - nip05: m.nip05 || null, lud16: typeof m.lud16 === 'string' ? m.lud16.trim() : null, - about: typeof m.about === 'string' ? m.about.slice(0, 1000) : null, - banner: typeof m.banner === 'string' ? m.banner.slice(0, 400) : null, - t: Date.now(), - }; - keepThumb(pk, entry); - profiles.set(pk, entry); - persistProfile(pk, entry); - preloadPicture(entry); + if (fullFetched.has(pk)) return Promise.resolve(); + if (fullInFlight.has(pk)) return fullInFlight.get(pk); + fullMisses.delete(pk); + // Use the same index-relay + author's-relay fallbacks as feed avatars. + // Only a real kind-0 response marks a page fetched; relay silence is + // retryable and leaves cached profile details intact. + const pending = fetchProfiles([pk]).catch(() => {}).finally(() => { + fullInFlight.delete(pk); + if (!fullFetched.has(pk)) fullMisses.add(pk); if (ui.profilePk === pk) render(); else scheduleRepaint(); - }).catch(() => { if (!fullProfiles.has(pk)) fullProfiles.set(pk, {}); }); + }); + fullInFlight.set(pk, pending); + return pending; } // Warm a profile PAGE (full kind-0 + latest notes + relay list) before @@ -2888,7 +2888,10 @@ export function messagesFeature(ctx) { // ...and, in the same REQ, what else happened to a note: likes (kind 7) // and boosts (kind 6). One round trip for all three. const NOTE_KINDS = [9735, 9737, 7, 6]; - const reacts = new Map(); // note id -> Map(emoji -> Set<pubkey>) + const reacts = new Map(); // note id -> Map([emoji, url] -> { emoji, url, who: Set<pubkey> }) + // Resolve from this reaction's own tag: packs can reuse the same shortcode + // for different pictures, and an unrelated event must not change its face. + const postReactNode = ({ emoji, url }) => url ? emojiImg(shortcodeOf(emoji), url) : emoji; const boosts = new Map(); // note id -> Set<pubkey> const seenNoteEv = new Set(); // event ids already counted const myReactEv = new Map(); // note id -> the id of OUR reaction, so it can be withdrawn @@ -2919,7 +2922,13 @@ export function messagesFeature(ctx) { try { return String((JSON.parse(tagOf(ev, 'description') || 'null') || {}).content || '').slice(0, 200); } catch { return ''; } } function zapperOf(ev) { - if (ev.kind === 9737) return ev.pubkey; + if (ev.kind === 9737) { + // Signed by the wallet key; the sender tag names the person behind it. + // Older receipts of ours have no such tag — read them as us all the same. + const sender = tagOf(ev, 'P') || ev.pubkey; + const id = hook('nostrLoginIdentity'); + return id && wallet.nostr && sender === wallet.nostr.pk ? id.pubkey : sender; + } if (tagOf(ev, 'P')) return tagOf(ev, 'P'); try { return (JSON.parse(tagOf(ev, 'description') || 'null') || {}).pubkey || null; } catch { return null; } } @@ -2938,11 +2947,15 @@ export function messagesFeature(ctx) { boosts.get(id).add(ev.pubkey); } else { // '+' and an empty content both mean a plain like - const emoji = !ev.content || ev.content === '+' ? '\u2764\ufe0f' : ev.content.slice(0, 12); + const code = shortcodeOf(ev.content); + const emoji = !ev.content || ev.content === '+' ? '\u2764\ufe0f' : code ? ev.content : ev.content.slice(0, 12); + const url = code ? emojiTagMap(ev.tags).get(code) || null : null; + const key = JSON.stringify([emoji, url]); + noteEmoji(ev); if (!reacts.has(id)) reacts.set(id, new Map()); const m = reacts.get(id); - if (!m.has(emoji)) m.set(emoji, new Set()); - m.get(emoji).add(ev.pubkey); + if (!m.has(key)) m.set(key, { emoji, url, who: new Set() }); + m.get(key).who.add(ev.pubkey); // ours, wherever it was sent from — so it can be taken back here if (myPubkeys().includes(ev.pubkey)) myReactEv.set(id, ev.id); } @@ -3084,10 +3097,11 @@ export function messagesFeature(ctx) { if (Date.now() - p.at > pendTtl(p)) { zapPending.delete(id); return null; } return p; } - function markZapPending(id, sats) { + function markZapPending(id, sats, origin) { if (!id || !sats) return; zapPending.set(id, { sats, at: Date.now(), state: 'flying' }); - scheduleRepaint(); + animateZap(sats, origin, id); + render(); // 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); @@ -3118,6 +3132,7 @@ export function messagesFeature(ctx) { // 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 = '' } = {}) { + if (onClick) warmZapSound(); const z = zapTotals.get(id) || zapSeeds().get(id); const p = pendingOf(id); const optimistic = p && p.state !== 'void' ? p.sats : 0; @@ -4195,20 +4210,18 @@ export function messagesFeature(ctx) { // asks once and remembers. // Whether a ⚡ makes sense for this author from this wallet: not // ourselves, and an instant path (Ark) or a Lightning fallback exists. - const canZapPk = (pk) => !!pk && !isMe(pk) && !!(hook('arkReady') || hook('canLnZap')); - function zapNote(pk, ev) { + // Your own posts and messages too: a zap to yourself is a harmless + // round-trip, and one bar that behaves the same everywhere beats a special case. + const canZapPk = (pk) => !!pk && !!(hook('arkReady') || hook('canLnZap')); + function zapNote(pk, ev, origin) { + if (pendingOf(ev.id)?.state === 'flying') return; 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 (!def) { ui.zapSetup = { pk, npub: npubStr, eventId: ev.id, amount: '21', origin }; render(); return; } + markZapPending(ev.id, def, origin); // strike now; settlement only corrects the tally 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; - ctx.showSend(); - render(); - if (!hook('zapNpub', pk, npubStr, ev.id)) hook('lnZapNpub', pk, npubStr, ev.id); + toast('⚡ ' + t('lnZapFailed'), 4000); } } @@ -4230,8 +4243,11 @@ export function messagesFeature(ctx) { const { pk, npub, eventId } = s; ui.zapSetup = null; render(); - markZapPending(eventId, n); - if (!hook('zapNpub', pk, npub, eventId, n) && !hook('lnZapNpub', pk, npub, eventId, n)) settleZap(eventId, false); + markZapPending(eventId, n, s.origin); + if (!hook('zapNpub', pk, npub, eventId, n) && !hook('lnZapNpub', pk, npub, eventId, n)) { + settleZap(eventId, false); + toast('⚡ ' + t('lnZapFailed'), 4000); + } recheckZap(eventId); } }, t('zapSetupSave'))), h('button', { class: 'btn-ghost btn-block', onClick: () => { ui.zapSetup = null; render(); } }, t('back'))); @@ -4248,7 +4264,7 @@ export function messagesFeature(ctx) { const m = reacts.get(id); if (!m) return null; const mine = myPubkeys(); - for (const [emoji, who] of m) for (const pk of mine) if (who.has(pk)) return emoji; + for (const reaction of m.values()) for (const pk of mine) if (reaction.who.has(pk)) return reaction; return null; }; const iBoosted = (id) => { @@ -4264,10 +4280,22 @@ export function messagesFeature(ctx) { function replyToNote(ev) { if (ui.noteThread && ui.noteThread.rootId === rootIdOf(ev)) { ui.noteThread.focusId = ev.id; - render(); - } else openNoteThread(ev); - if (ui.noteThread) ui.noteThread.refocus = true; - setTimeout(() => document.querySelector('.thread-reply-input')?.focus(), 120); + ui.noteThread.seed = ev; + } else ui.noteThread = { rootId: rootIdOf(ev), focusId: ev.id, seed: ev }; + const s = ui.noteThread; + ui.profOverThread = false; + s.scrollPending = false; // focus the composer, without a competing note scroll + s.refocus = true; + render(); + // Focus in the tap handler so mobile browsers can open the keyboard. + focusThreadReply(s); + } + function focusThreadReply(s) { + if (ui.noteThread !== s || ui.profOverThread) return; + const input = document.querySelector('.thread-reply-input'); + if (!input) return; + input.focus({ preventScroll: true }); + input.scrollIntoView({ block: 'nearest' }); } async function unreact(ev) { @@ -4279,7 +4307,7 @@ export function messagesFeature(ctx) { // that don't go on showing it to other people — worth knowing, not // worth refusing to try. const m = reacts.get(ev.id); - if (m) for (const [emoji, who] of [...m]) { for (const pk of myPubkeys()) who.delete(pk); if (!who.size) m.delete(emoji); } + if (m) for (const [key, { who }] of [...m]) { for (const pk of myPubkeys()) who.delete(pk); if (!who.size) m.delete(key); } render(); const mineEv = myReactEv.get(ev.id); if (!mineEv) return; @@ -4297,7 +4325,7 @@ export function messagesFeature(ctx) { const relays = await noteRelaysFor(ev); const partial = { kind: 7, content: emoji || '❤️', created_at: Math.floor(Date.now() / 1000), - tags: [['e', ev.id], ['p', ev.pubkey], CLIENT_TAG], + tags: [['e', ev.id], ['p', ev.pubkey], ...reactEmojiTags(emoji), CLIENT_TAG], }; const evt = id.signer instanceof Uint8Array ? finalizeEvent(partial, id.signer) : await id.signer.signEvent(partial); myReactEv.set(ev.id, evt.id); @@ -4451,14 +4479,15 @@ export function messagesFeature(ctx) { const I_ZAP = ICON('<path d="M13 2L4 14h7l-1 8 9-12h-7l1-8z"/>', true); function noteActions(pk, ev, { canZap }) { + if (canZap) warmZapSound(); // the clip is decoded before the first tap const mineReact = myReactOn(ev.id); const rm = reacts.get(ev.id); - const likeN = rm ? [...rm.values()].reduce((n, who) => n + who.size, 0) : 0; + const likeN = rm ? [...rm.values()].reduce((n, { who }) => n + who.size, 0) : 0; const boostN = (boosts.get(ev.id) || new Set()).size; const btn = (icon, label, count, on, onClick, cls = '') => h('button', { class: 'note-act' + (on ? ' on' : '') + (cls ? ' ' + cls : ''), title: label, 'aria-label': label, 'aria-disabled': onClick ? undefined : 'true', - onClick: (e) => { e.stopPropagation(); if (onClick) onClick(); }, + onClick: (e) => { e.stopPropagation(); if (onClick) onClick(e); }, }, typeof icon === 'string' && icon.startsWith('<svg') ? h('span', { style: 'display:flex', html: icon }) @@ -4478,23 +4507,24 @@ export function messagesFeature(ctx) { btn(I_BOOST, t('postBoost'), boostN, iBoosted(ev.id), () => boostNote(ev).catch(() => {})), btn(I_QUOTE, t('postQuote'), 0, false, () => quoteNote(ev)), // tap to choose how you feel about it; tap again to take it back - btn(mineReact || I_HEART(false), t('postLike'), likeN, !!mineReact, + btn(mineReact ? postReactNode(mineReact) : I_HEART(false), t('postLike'), likeN, !!mineReact, () => { if (mineReact) unreact(ev).catch(() => {}); else { ui.reactPick = ev; render(); } }), - // shown whenever there is a tally to read, even where zapping is off - // (own post, no wallet) — then it is just a number, not a button - canZap || zapSats - ? btn(I_ZAP, zapLabel, zapSats ? fmtSats(zapSats) : 0, zapMine, - canZap ? () => { zapNote(pk, ev); recheckZap(ev.id); } : null, - 'note-zap' + (zapSats ? ' zapped' : '') + (zapFlying ? ' flying' : '')) - : null, - // who did all that: a small chevron, only once there is anyone to show - whoCount(ev.id) - ? h('button', { - class: 'note-act note-who-toggle' + (whoOpen(ev.id) ? ' on' : ''), - title: t('postWho'), 'aria-label': t('postWho'), 'aria-expanded': whoOpen(ev.id) ? 'true' : 'false', - onClick: (e) => { e.stopPropagation(); toggleWho(ev.id); }, - }, h('span', { class: 'note-who-chev' + (whoOpen(ev.id) ? ' open' : ''), html: I_CHEV })) - : null); + // always in the bar — the same row of buttons on every post, so nothing + // shifts when a wallet comes online; without one it is just the number + btn(I_ZAP, zapLabel, zapSats ? fmtSats(zapSats) : 0, zapMine, + canZap ? (e) => { zapNote(pk, ev, e.currentTarget.getBoundingClientRect()); recheckZap(ev.id); } : null, + 'note-zap' + (zapSats ? ' zapped' : '') + (zapFlying ? ' flying' : '')), + // who did all that: a small chevron, always in place — dimmed until + // there is someone to show, so the first reaction doesn't reflow the bar + (() => { + const n = whoCount(ev.id), open = n && whoOpen(ev.id); + return h('button', { + class: 'note-act note-who-toggle' + (open ? ' on' : ''), + title: t('postWho'), 'aria-label': t('postWho'), 'aria-expanded': open ? 'true' : 'false', + 'aria-disabled': n ? undefined : 'true', + onClick: (e) => { e.stopPropagation(); if (n) toggleWho(ev.id); }, + }, h('span', { class: 'note-who-chev' + (open ? ' open' : ''), html: I_CHEV })); + })()); } // ---- who reacted, boosted and zapped ------------------------------------ @@ -4507,7 +4537,7 @@ export function messagesFeature(ctx) { const toggleWho = (id) => { if (whoOpenIds.has(id)) whoOpenIds.delete(id); else whoOpenIds.add(id); render(); }; function whoCount(id) { const rm = reacts.get(id); - let n = rm ? [...rm.values()].reduce((k, who) => k + who.size, 0) : 0; + let n = rm ? [...rm.values()].reduce((k, { who }) => k + who.size, 0) : 0; n += (boosts.get(id) || new Set()).size; n += (zapWho.get(id) || new Map()).size; return n; @@ -4536,8 +4566,8 @@ export function messagesFeature(ctx) { person(z.pk, h('span', { class: 'note-who-sats' }, fmtSats(z.sats) + ' sats' + (z.text ? ' · ' + z.text : ''))))))); } - for (const [emoji, set] of [...rm.entries()].sort((a, b) => b[1].size - a[1].size)) { - lines.push(line(h('span', { class: 'note-act-emoji' }, emoji), people([...set]))); + for (const reaction of [...rm.values()].sort((a, b) => b.who.size - a.who.size)) { + lines.push(line(h('span', { class: 'note-act-emoji' }, postReactNode(reaction)), people([...reaction.who]))); } if (bs.size) lines.push(line(h('span', { style: 'display:flex', html: I_BOOST }), people([...bs]))); if (!lines.length) return null; @@ -4567,7 +4597,7 @@ export function messagesFeature(ctx) { // profile is actually opened. profileOf(pk); const isReply = ev.tags.some((x) => x[0] === 'e'); - const canZap = !isMe(pk) && !!(hook('arkReady') || hook('canLnZap')); + const canZap = canZapPk(pk); // an optimistic post mid-publish: visible but not yet a real event — // dimmed, and no thread/reply/zap until its signed self takes over const pending = !!ev.pending; @@ -4575,6 +4605,7 @@ export function messagesFeature(ctx) { if (!pending) watchZaps([ev.id]); return h('div', { class: 'row', + 'data-zap-post': ev.id, 'data-focus-note': focus ? '1' : undefined, style: 'gap:10px;align-items:flex-start;padding:10px 0' + (openable ? ';cursor:pointer' : '') @@ -4621,8 +4652,15 @@ export function messagesFeature(ctx) { function threadFor(seed) { const rootId = rootIdOf(seed); let c = threadCache.get(rootId); - if (c) return c; - c = { status: 'loading', rootId, root: seed.id === rootId ? seed : null, replies: [] }; + if (c) { + if (seed.id === rootId) c.root ||= seed; + else if (!c.replies.some((ev) => ev.id === seed.id)) + c.replies = [...c.replies, seed].sort((a, b) => a.created_at - b.created_at); + return c; + } + // We already have the tapped post. Keep it visible and replyable even + // if the root is unavailable or the relay's limited result omits it. + c = { status: 'loading', rootId, root: seed.id === rootId ? seed : null, replies: seed.id === rootId ? [] : [seed] }; threadCache.set(rootId, c); (async () => { // the conversation's home relays are the root author's (NIP-10 outbox); @@ -4634,7 +4672,7 @@ export function messagesFeature(ctx) { ]); if (!c.root) c.root = (roots || [])[0] || null; const seen = new Set([rootId]); - c.replies = (replies || []) + c.replies = [...c.replies, ...(replies || [])] .filter((e) => !seen.has(e.id) && seen.add(e.id)) .sort((a, b) => a.created_at - b.created_at); c.status = 'ready'; @@ -4646,7 +4684,8 @@ export function messagesFeature(ctx) { // button asked for it. if (ui.noteThread.refocus) { ui.noteThread.refocus = false; - setTimeout(() => document.querySelector('.thread-reply-input')?.focus(), 30); + const s = ui.noteThread; + setTimeout(() => focusThreadReply(s), 30); } } })().catch(() => { @@ -5119,6 +5158,9 @@ export function messagesFeature(ctx) { // full header on profiles too — losing the search button here made // finding the NEXT person a trek back home ctx.brandHeader(!ui.pubProf && wallet.loaded), + fullMisses.has(pk) ? h('div', { class: 'row between gap6 small muted' }, + t(full || lp.eventAt || lp.name || lp.picture ? 'profFetchFailed' : 'profNotFound'), + h('button', { class: 'btn-sm', onClick: () => { fetchFullProfile(pk); render(); } }, t('retry'))) : null, h('div', { class: 'card col', style: 'gap:12px' }, bannerUrl ? h('div', { class: 'profile-banner', style: `background-image:url(${JSON.stringify(bannerUrl)})` }) : null, h('div', { class: 'row gap6', style: 'align-items:center' }, @@ -5954,11 +5996,11 @@ export function messagesFeature(ctx) { }, h('div', { class: 'chat-avatar fallback' }, name.slice(0, 2)), h('div', { class: 'col grow', style: 'min-width:0;gap:1px' }, - h('div', { class: 'row between' }, - h('span', { class: 'chat-name' }, name), - unread ? h('i', { class: 'thread-dot' }) : null), + h('span', { class: 'chat-name' }, name), h('div', { class: 'muted small' }, - memberCount ? t('msgMembers', { n: memberCount }) : t('msgEncrypted')))); + memberCount ? t('msgMembers', { n: memberCount }) : t('msgEncrypted'))), + // the row's own centring puts the dot mid-height, off the edge + unread ? h('i', { class: 'thread-dot' }) : null); }))); return h('div', { class: 'card col chat-page', style: 'gap:10px' }, ...kids); @@ -6068,7 +6110,7 @@ export function messagesFeature(ctx) { title: t('msgReact'), onClick: (e) => { e.stopPropagation(); sendReaction(room, chId, m, emoji); }, }, reactNode(emoji), n > 1 ? ' ' + n : ''))) - : null)(zapChip(m.rumor.id, { cls: 'chat-react', onClick: !mine && canZapPk(m.author) ? () => zapMessage(m.author, m.rumor.id) : null })))) + : null)(zapChip(m.rumor.id, { cls: 'chat-react', onClick: canZapPk(m.author) ? () => zapMessage(m.author, m.rumor.id) : null })))) ); }); } @@ -6244,7 +6286,7 @@ export function messagesFeature(ctx) { close(); setTimeout(() => document.getElementById('msg-draft')?.focus(), 50); }), - !ui.emojiPick && !m.mine && canZapPk(m.rumor.pubkey) ? item('⚡', t('msgZap'), () => { close(); zapMessage(m.rumor.pubkey, m.rumor.id); }) : null, + !ui.emojiPick && canZapPk(m.rumor.pubkey) ? item('⚡', t('msgZap'), () => { close(); zapMessage(m.rumor.pubkey, m.rumor.id); }) : null, ui.emojiPick ? null : item('⧉', t('copy'), async () => { try { await navigator.clipboard.writeText(m.rumor.content); toast(t('copied')); } catch {} close(); @@ -6273,7 +6315,7 @@ export function messagesFeature(ctx) { watchZaps(msgs.slice(-150).map((m) => m.rumor.id)); const dmChips = (m) => { const reacts = dmReacts.get(m.rumor.id); - const zc = zapChip(m.rumor.id, { cls: 'chat-react', onClick: !m.mine && canZapPk(m.rumor.pubkey) ? () => zapMessage(m.rumor.pubkey, m.rumor.id) : null }); + const zc = zapChip(m.rumor.id, { cls: 'chat-react', onClick: canZapPk(m.rumor.pubkey) ? () => zapMessage(m.rumor.pubkey, m.rumor.id) : null }); if ((!reacts || !reacts.size) && !zc) return null; const myReact = reacts && my.map((pk) => reacts.get(pk)).find(Boolean); const counts = new Map(); diff --git a/src/features/zaps.js b/src/features/zaps.js index 77f8475..54c8f58 100644 --- a/src/features/zaps.js +++ b/src/features/zaps.js @@ -214,7 +214,7 @@ export function zapsFeature(ctx) { } const invoice = await requestInvoice(p, { amountMsat: msat, zapRequest, lnurlBech32: t2.lnurlBech32 }); await hook('arkPayInvoice', invoice, { maxAmountSat: sats + 50 }); - toast('⚡ ' + t('zapSentShort', { n: fmtAmount(sats) + ' ' + unitLabel() })); + if (!target.eventId) toast('⚡ ' + t('zapSentShort', { n: fmtAmount(sats) + ' ' + unitLabel() })); render(); } @@ -454,10 +454,13 @@ export function zapsFeature(ctx) { // this went, either way autoZap({ kind: 'npub', pk, eventId: eventId || null }, autoSat) .then(() => hook('zapSettled', eventId, true, autoSat)) - .catch((e) => { hook('zapSettled', eventId, false); toast('⚡ ' + e.message); }); + .catch((e) => { hook('zapSettled', eventId, false); toast('⚡ ' + (e.message || t('lnZapFailed')), 4000); }); return true; } - if (autoSat) hook('zapSettled', eventId, false); // one tap couldn't serve it — the form takes over + if (autoSat) { + hook('zapSettled', eventId, false); + if (eventId) { toast('⚡ ' + t('lnZapFailed'), 4000); return true; } + } begin({ kind: 'npub', pk, eventId: eventId || null }, shortNpub(npub || npubOf(pk))); return true; }, diff --git a/src/i18n.js b/src/i18n.js index 0d5f32f..0dd7513 100644 --- a/src/i18n.js +++ b/src/i18n.js @@ -102,6 +102,8 @@ const STR = { profUsername: 'Username', profUploadPic: 'Upload photo', profDisplayName: 'Display name', + profFetchFailed: 'Couldn’t load the latest profile.', + profNotFound: 'No profile details found on the available relays.', profUnameInvalid: 'Usernames can only use letters, numbers, dots, dashes and underscores.', lockSaveDesc: 'The accounts already saved on this device are protected by a password. Enter it to save this account with them and lock.', lockAction: 'Lock', diff --git a/src/style.css b/src/style.css index 3fd3de2..3629dfc 100644 --- a/src/style.css +++ b/src/style.css @@ -499,6 +499,43 @@ hr.divider { margin: 16px 0; } .zap-tally { display: inline-flex; align-items: center; gap: 2px; font-size: 11px; font-weight: 650; color: #e6a800; white-space: nowrap; flex-shrink: 0; } .zap-tally.chat-react { font-size: 12px; padding: 1px 7px 1px 5px; } .zap-tally.clickable { cursor: pointer; } +/* A tap-local lightning strike, followed by a floating amount and embers. */ +.zap-fx { position: fixed; inset: 0; z-index: 10000; pointer-events: none; overflow: hidden; contain: strict; } +.zap-fx-impact { position: absolute; left: var(--zap-x); top: var(--zap-y); width: 0; height: 0; color: #ffc83d; } +.zap-fx-bolt { position: absolute; width: 82px; height: 180px; left: -41px; bottom: 0; fill: #fff5c2; filter: drop-shadow(0 0 5px #fff3a1) drop-shadow(0 0 13px #ff9d00); transform-origin: 50% 100%; animation: zap-strike 430ms ease-out both; } +.zap-fx-flash, .zap-fx-ring { position: absolute; left: -28px; top: -28px; width: 56px; height: 56px; border-radius: 50%; animation: zap-impact 550ms 60ms ease-out both; } +.zap-fx-flash { background: radial-gradient(circle, #fffce0, #ffca45 25%, #ff980066 50%, transparent 70%); } +.zap-fx-ring { border: 2px solid #ffd76a; box-shadow: 0 0 12px #ffad32, inset 0 0 10px #ffad3266; animation-name: zap-ring; } +.zap-fx-amount { position: absolute; left: var(--zap-label-x); top: -60px; display: flex; align-items: baseline; gap: 6px; white-space: nowrap; color: #fff2ad; text-shadow: 0 2px 2px #713500, 0 0 9px #ff9c00, 0 0 22px #ff9500aa; animation: zap-amount 1450ms cubic-bezier(.16, 1, .3, 1) both; } +.zap-fx-amount strong { font-size: clamp(34px, 9vw, 48px); font-weight: 900; line-height: 1; letter-spacing: -.045em; } +.zap-fx-amount span { font-size: 13px; font-weight: 800; letter-spacing: .04em; } +.zap-fx-spark { position: absolute; width: 10px; height: 3px; left: -5px; top: -2px; border-radius: 3px; background: #ffe997; box-shadow: 0 0 6px #ffaf20; animation: zap-spark 720ms var(--delay) ease-out both; } +.zap-fx-spark.ember { left: calc(var(--zap-label-x) - 2px); top: -80px; width: 3px; height: 3px; background: #fff4c2; animation-duration: 750ms; } +@keyframes zap-strike { + 0% { opacity: 0; transform: scaleY(.15); } + 16%, 38% { opacity: 1; transform: scaleY(1); } + 65% { opacity: .8; } + 100% { opacity: 0; transform: scaleY(1.04); } +} +@keyframes zap-impact { 0% { opacity: 1; transform: scale(.2); } 100% { opacity: 0; transform: scale(2.4); } } +@keyframes zap-ring { 0% { opacity: .9; transform: scale(.25); } 100% { opacity: 0; transform: scale(2); } } +@keyframes zap-amount { + 0% { opacity: 0; transform: translate(-50%, 35px) scale(.35) rotate(-9deg); } + 16% { opacity: 1; transform: translate(-50%, -8px) scale(1.2) rotate(-5deg); } + 30% { opacity: 1; transform: translate(-50%, -4px) scale(1) rotate(-3deg); } + 58% { opacity: 1; filter: blur(0); } + 100% { opacity: 0; transform: translate(-50%, -65px) scale(1.08) rotate(2deg); filter: blur(3px); } +} +@keyframes zap-spark { + 0% { opacity: 0; transform: translate(0, 0) rotate(var(--angle)) scale(.4); } + 12% { opacity: 1; } + 100% { opacity: 0; transform: translate(var(--dx), var(--dy)) rotate(var(--angle)) scale(.1); } +} +@media (prefers-reduced-motion: reduce) { + .zap-fx-bolt, .zap-fx-flash, .zap-fx-ring, .zap-fx-spark { display: none; } + .zap-fx-amount { animation: zap-amount-reduced 1200ms ease-out both; } + @keyframes zap-amount-reduced { 0%, 65% { opacity: 1; transform: translateX(-50%); } 100% { opacity: 0; transform: translateX(-50%); } } +} /* A zap in flight: the chip lands the moment you tap and breathes until the payment settles — the bolt carries the pulse, the number stays readable. */ .zap-tally.flying { animation: zap-breathe 1.15s ease-in-out infinite; } @@ -529,6 +566,7 @@ hr.divider { margin: 16px 0; } .chat-preview { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } .thread-when { display: inline-flex; align-items: center; gap: 6px; flex-shrink: 0; } .thread-dot { width: 8px; height: 8px; border-radius: 50%; background: var(--red); flex-shrink: 0; } +.chat-thread-row > .thread-dot { margin-right: 8px; } .chat-thread-row.unread .chat-name { font-weight: 700; } .chat-thread-row.unread .chat-preview { color: var(--ink); } .chat-invite { padding: 10px 12px; border-bottom: 1px solid var(--line); background: var(--surface2); flex: 0 0 auto; max-height: 40vh; overflow-y: auto; } @@ -574,6 +612,8 @@ hr.divider { margin: 16px 0; } .note-act-emoji { font-size: 17px; line-height: 1; } .note-act-n { font-size: 12px; font-weight: 600; color: var(--muted); } .note-who-toggle.on { color: var(--muted); } +.note-who-toggle[aria-disabled] { opacity: .3; cursor: default; } +.note-who-toggle[aria-disabled]:hover { background: none; color: var(--faint); } .note-who-chev { display: flex; transition: transform .2s; } .note-who-chev.open { transform: rotate(180deg); } /* who reacted, boosted and zapped: opened by the chevron on the bar */ diff --git a/src/zap-animation.js b/src/zap-animation.js new file mode 100644 index 0000000..20a6b60 --- /dev/null +++ b/src/zap-animation.js @@ -0,0 +1,83 @@ +// Outside the app's morphed tree so relay updates cannot restart the effect. +// This is tap feedback only: payment settlement never plays it again. + +// The clip is 62KB of base64 — messages.js can't be deferred (it's the boot +// shell), so the sound is its own chunk instead, fetched once a zappable +// post or chat message is on screen. By the first tap it's decoded; and if +// it isn't, the sound keeps the tap's own clock and stays silent when late. +let soundModule = null, sound = null; +export function warmZapSound() { + if (!soundModule) { + soundModule = import('./zap-sound.js') + .then((m) => (sound = m)) + .catch(() => { soundModule = null; return null; }); + } + return soundModule; +} + +const trembles = new WeakMap(); +function tremblePost(id) { + if (!id || window.matchMedia('(prefers-reduced-motion: reduce)').matches) return; + for (const post of document.querySelectorAll(`[data-zap-post="${CSS.escape(id)}"]`)) { + // Web Animations survive the app's DOM morphs without adding a class + // that a balance update could remove or accidentally restart. + trembles.get(post)?.cancel(); + const animation = post.animate([ + { transform: 'none' }, + { transform: 'translate(-3px, 1px) rotate(-.3deg)' }, + { transform: 'translate(3px, -1px) rotate(.3deg)' }, + { transform: 'translate(-2.5px, 0) rotate(-.2deg)' }, + { transform: 'translate(2px, 1px) rotate(.2deg)' }, + { transform: 'translate(-1.5px, -1px)' }, + { transform: 'translate(1px, 0)' }, + { transform: 'none' }, + ], { duration: 360, easing: 'ease-out' }); + trembles.set(post, animation); + animation.onfinish = () => { if (trembles.get(post) === animation) trembles.delete(post); }; + } +} + +export function animateZap(sats, origin, postId) { + // Warm: play inside the tap itself (the audio context resumes on a + // gesture). Cold: play when the chunk lands, if that's still soon enough. + if (sound) sound.playZapSound(); + else { const tappedAt = performance.now(); warmZapSound().then((m) => { if (m) m.playZapSound(tappedAt); }); } + try { tremblePost(postId); } catch {} // Optional motion must never block a zap. + const layer = document.createElement('div'); + layer.className = 'zap-fx'; + layer.setAttribute('aria-hidden', 'true'); + const x = origin ? origin.left + origin.width / 2 : innerWidth / 2; + const y = origin ? origin.top + origin.height / 2 : innerHeight / 2; + layer.style.setProperty('--zap-x', `${x}px`); + layer.style.setProperty('--zap-y', `${Math.max(110, y)}px`); + layer.innerHTML = '<div class="zap-fx-impact">' + + '<svg class="zap-fx-bolt" viewBox="0 0 100 180"><path d="M62 0 25 65 52 59 15 122 43 110 50 180 72 91 48 100 88 35 59 43Z"/></svg>' + + '<i class="zap-fx-flash"></i><i class="zap-fx-ring"></i>' + + '<div class="zap-fx-amount"><strong></strong><span>sats</span></div></div>'; + layer.querySelector('strong').textContent = '+' + sats.toLocaleString(); + const impact = layer.firstElementChild; + for (let i = 0; i < 26; i++) { + const spark = document.createElement('i'); + const angle = i * 2.39996; + const distance = 38 + (i % 6) * 15; + spark.className = 'zap-fx-spark' + (i >= 16 ? ' ember' : ''); + spark.style.setProperty('--dx', `${Math.cos(angle) * distance}px`); + spark.style.setProperty('--dy', `${Math.sin(angle) * distance - 35}px`); + spark.style.setProperty('--angle', `${angle}rad`); + spark.style.setProperty('--delay', `${i >= 16 ? 600 + (i % 5) * 55 : 60 + (i % 4) * 20}ms`); + impact.append(spark); + } + document.body.append(layer); + // Leave room for the pop's 1.2x overshoot, even for long amounts at an edge. + const label = layer.querySelector('.zap-fx-amount'); + if (label.offsetWidth > (innerWidth - 32) / 1.2) { + const strong = label.querySelector('strong'); + strong.style.fontSize = `${parseFloat(getComputedStyle(strong).fontSize) * (innerWidth - 80) / (label.offsetWidth * 1.2)}px`; + } + const margin = label.offsetWidth * .6 + 12; + const labelX = Math.max(margin, Math.min(innerWidth - margin, x)); + layer.style.setProperty('--zap-label-x', `${labelX - x}px`); + const clean = () => { layer.remove(); window.removeEventListener('pagehide', clean); }; + window.addEventListener('pagehide', clean, { once: true }); + setTimeout(clean, 1700); +} diff --git a/src/zap-sound.js b/src/zap-sound.js new file mode 100644 index 0000000..fcf6d1f --- /dev/null +++ b/src/zap-sound.js @@ -0,0 +1,59 @@ +// The supplied zap.mp3, encoded verbatim so hosted and standalone builds +// both include it. Decode ahead of the tap; no network request is needed. +import mp3 from './assets/zap.mp3.base64.txt' with { type: 'text' }; + +let context, buffer, loading, playing, sequence = 0; + +function prepareZap() { + const AudioCtx = window.AudioContext || window.webkitAudioContext; + if (!AudioCtx) return null; + if (!context || context.state === 'closed') { + context = new AudioCtx({ latencyHint: 'interactive' }); + buffer = null; + loading = null; + } + if (!loading) { + const ctx = context; + const bytes = Uint8Array.from(atob(mp3.trim()), c => c.charCodeAt(0)); + loading = ctx.decodeAudioData(bytes.buffer).then(decoded => { + if (context === ctx) buffer = decoded; + return decoded; + }); + loading.catch(() => { if (context === ctx) loading = null; }); + } + return loading; +} + +// Decoding is allowed while the audio context is suspended; the tap resumes +// playback. A browser without audio must still be able to load the wallet. +try { prepareZap(); } catch {} + +// tappedAt: when the finger landed — the caller's clock, since this module +// may still have been loading when it did. +export function playZapSound(tappedAt = performance.now()) { + try { + const ready = prepareZap(); + if (!ready) return; + const ctx = context, token = ++sequence; + if (ctx.state !== 'running') ctx.resume().catch(() => {}); + const start = decoded => { + // A very early tap may beat decoding. Never queue an old sound behind + // a later zap or surprise the user after a stalled decoder recovers. + if (token !== sequence || context !== ctx || performance.now() - tappedAt > 250) return; + if (playing) { playing.stop(); playing.disconnect(); } + const source = ctx.createBufferSource(), gain = ctx.createGain(); + source.buffer = decoded; + gain.gain.value = .65; + source.connect(gain); + gain.connect(ctx.destination); + source.onended = () => { + source.disconnect(); gain.disconnect(); + if (playing === source) playing = null; + }; + playing = source; + source.start(); + }; + if (buffer) start(buffer); + else ready.then(start).catch(() => {}); + } catch {} // Unavailable audio never interrupts a payment or its animation. +} |
