// 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, withSound = true) { // 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 (!withSound) {} // switched off from the held-⚡ screen: the strike stays else 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 = '