diff options
| -rw-r--r-- | src/app.js | 8 | ||||
| -rw-r--r-- | src/features/messages.js | 13 | ||||
| -rw-r--r-- | src/features/zaps.js | 6 | ||||
| -rw-r--r-- | src/i18n.js | 1 | ||||
| -rw-r--r-- | src/zap-animation.js | 5 | ||||
| -rw-r--r-- | tools/zap-feedback-test.js | 12 |
6 files changed, 37 insertions, 8 deletions
@@ -5376,6 +5376,14 @@ const ctx = { wallet.saveFeatureState('prefs', p); try { wallet.saveCache(); } catch {} }, + // the tap's sound effect — on unless switched off from the held-⚡ screen + zapSound: () => !wallet.loadFeatureState || wallet.loadFeatureState('prefs', {}).zapSound !== false, + setZapSound: (on) => { + const p = wallet.loadFeatureState('prefs', {}); + if (on) delete p.zapSound; else p.zapSound = false; + wallet.saveFeatureState('prefs', p); + try { wallet.saveCache(); } catch {} + }, // Per-recipient auto-pay budget: a fixed-amount LNURL/address the user // trusts to pay without confirmation, up to this many sats. Keyed by the // recipient's address/lnurl. Synced with the rest of prefs. diff --git a/src/features/messages.js b/src/features/messages.js index 24542b9..111758e 100644 --- a/src/features/messages.js +++ b/src/features/messages.js @@ -3119,7 +3119,7 @@ export function messagesFeature(ctx) { if (!id || !sats) return; const list = pendingOf(id)?.list || []; zapPending.set(id, [...list, { sats, at: Date.now(), state: 'flying' }]); - animateZap(sats, origin, id); + animateZap(sats, origin, id, zapSoundOn()); render(); // repaint when the in-flight chip would expire, so a zap nobody ever // reported on doesn't pulse forever @@ -3159,8 +3159,9 @@ 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. + const zapSoundOn = () => (ctx.zapSound ? ctx.zapSound() : true); function zapChip(id, { onClick, onHold, cls = '' } = {}) { - if (onClick) warmZapSound(); + if (onClick && zapSoundOn()) warmZapSound(); const z = zapTotals.get(id) || zapSeeds().get(id); const p = pendingOf(id); const optimistic = p ? p.sats : 0; @@ -4348,7 +4349,7 @@ export function messagesFeature(ctx) { // the main action, zapping this note with the new amount the second. function openZapSettings(pk, id, origin) { const cur = ctx.zapDefaultSat ? ctx.zapDefaultSat() : 0; - ui.zapSetup = { pk, npub: npubOf(pk), eventId: id, amount: String(cur || 21), origin, edit: true }; + ui.zapSetup = { pk, npub: npubOf(pk), eventId: id, amount: String(cur || 21), origin, edit: true, sound: zapSoundOn() }; render(); } @@ -4359,6 +4360,7 @@ export function messagesFeature(ctx) { const n = parseInt(s.amount, 10); if (!n || n <= 0) { toast(t('enterValidAmtForN', { n: 1 })); return 0; } ctx.setZapDefaultSat(n); + if (s.edit && ctx.setZapSound && s.sound !== zapSoundOn()) ctx.setZapSound(s.sound); return n; }; const saveAndZap = () => { @@ -4389,6 +4391,9 @@ export function messagesFeature(ctx) { h('div', { class: 'input-group' }, h('input', { type: 'number', min: '1', value: s.amount, onInput: (e) => { s.amount = e.target.value; } }), h('span', { class: 'small muted', style: 'align-self:center;padding:0 8px' }, 'sats')), + s.edit ? h('label', { class: 'row gap6', style: 'align-items:center;cursor:pointer' }, + h('input', { type: 'checkbox', checked: s.sound, style: 'width:18px;height:18px;accent-color:var(--accent);margin:0', onChange: (e) => { s.sound = e.target.checked; } }), + h('span', { class: 'small' }, t('zapSoundToggle'))) : null, s.edit ? [h('button', { class: 'btn-primary btn-block', onClick: saveOnly }, t('save')), h('button', { class: 'btn-ghost btn-block', onClick: saveAndZap }, t('zapSetupSave'))] @@ -4622,7 +4627,7 @@ 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 + if (canZap && zapSoundOn()) 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; diff --git a/src/features/zaps.js b/src/features/zaps.js index 54c8f58..d6b2271 100644 --- a/src/features/zaps.js +++ b/src/features/zaps.js @@ -476,7 +476,11 @@ export function zapsFeature(ctx) { style: 'text-align:right', onInput: (e) => { const n = parseInt(e.target.value, 10); if (n > 0) ctx.setZapDefaultSat(n); } }), h('span', { class: 'small muted', style: 'align-self:center;padding:0 6px' }, 'sats'))), - h('div', { class: 'small faint' }, t('zapDefaultHint')))]; + h('div', { class: 'small faint' }, t('zapDefaultHint')), + ctx.setZapSound ? h('label', { class: 'row gap6', style: 'align-items:center;cursor:pointer;margin-top:4px' }, + h('input', { type: 'checkbox', checked: ctx.zapSound(), style: 'width:18px;height:18px;accent-color:var(--accent);margin:0', + onChange: (e) => { ctx.setZapSound(e.target.checked); } }), + h('span', { class: 'small' }, t('zapSoundToggle'))) : null)]; }, }; } diff --git a/src/i18n.js b/src/i18n.js index 84d1a5a..066ebce 100644 --- a/src/i18n.js +++ b/src/i18n.js @@ -964,6 +964,7 @@ const STR = { zapSetupSave: 'Save and zap', zapSettingsDesc: 'This is what one tap on ⚡ sends — no form, no confirmation. Tap again while one is still on its way to send another.', zapAmountSaved: 'One tap now zaps {n} sats', + zapSoundToggle: 'Play a sound when you zap', zapDefaultLabel: 'One-tap zap amount', zapDefaultHint: 'What a single tap of the ⚡ button sends.', threadNoReplies: 'No replies yet.', diff --git a/src/zap-animation.js b/src/zap-animation.js index 20a6b60..d03285b 100644 --- a/src/zap-animation.js +++ b/src/zap-animation.js @@ -37,10 +37,11 @@ function tremblePost(id) { } } -export function animateZap(sats, origin, postId) { +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 (sound) sound.playZapSound(); + 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'); diff --git a/tools/zap-feedback-test.js b/tools/zap-feedback-test.js index bda3e23..21e04cf 100644 --- a/tools/zap-feedback-test.js +++ b/tools/zap-feedback-test.js @@ -13,12 +13,13 @@ import { arkFeature } from './src/features/ark.js'; ${domHelper} const me = '1'.repeat(64), author = '2'.repeat(64), id = '3'.repeat(64); const ev = { id, pubkey: author, kind: 1, tags: [], content: 'A little lightning goes a long way.' }; -let feature, ark, arkResolve, arkReject, amount = 21, mode = 'pending', calls = 0, toasts = [], loginPk = null; +let feature, ark, arkResolve, arkReject, amount = 21, sound = true, mode = 'pending', calls = 0, toasts = [], loginPk = null; const ui = { screen: 'wallet', chatOpen: true }; const wallet = { nostr: { pk: me }, loadFeatureState: () => ({}), saveFeatureState() {}, loadArkState() {}, registerCacheExtension() {}, nostrProfile: async () => { throw new Error('Payment unavailable'); } }; const ctx = { h, ui, wallet, render, toast: (msg) => toasts.push(msg), zapDefaultSat: () => amount, setZapDefaultSat: (n) => amount = n, + zapSound: () => sound, setZapSound: (on) => sound = on, brandHeader: () => null, testPay: () => new Promise((resolve, reject) => { arkResolve = resolve; arkReject = reject; }), showSend: () => { ui.chatOpen = false; }, @@ -194,6 +195,15 @@ try { await page.mouse.down(); await pause(200); await page.mouse.up(); await pause(100); check('a short press is still a tap', await page.evaluate((n) => test.calls === n + 1 && !document.querySelector('#setup input'), calls)); await page.evaluate(() => test.finishArk(true)); await pause(100); + // The same screen switches the sound off; the strike and tally stay. + await page.mouse.down(); await pause(650); await page.mouse.up(); await pause(100); + check('the held screen offers the sound switch, on by default', await page.$eval('#setup input[type=checkbox]', (e) => e.checked)); + await page.click('#setup input[type=checkbox]'); + await page.click('#setup .btn-primary'); await pause(100); + const starts = await page.evaluate(() => zapAudio.starts); + await page.click('.note-zap'); await pause(100); + check('with the sound off a tap still strikes and counts, silently', await page.evaluate((n, c) => zapAudio.starts === n && test.calls === c + 2 && !!document.querySelector('.zap-fx'), starts, calls)); + await page.evaluate(() => test.finishArk(true)); await pause(100); const silentPage = await browser.newPage(); silentPage.on('pageerror', e => errors.push(e.message)); await silentPage.evaluateOnNewDocument(() => { |
