Queue message sound while audio is priming

This commit is contained in:
Your Name
2026-09-30 17:05:32 +08:00
parent 06df51ca08
commit dd77ba903b
2 changed files with 57 additions and 1 deletions
+11 -1
View File
@@ -1,5 +1,6 @@
let audio: HTMLAudioElement | null = null; let audio: HTMLAudioElement | null = null;
let priming = false; let priming = false;
let pendingMessagePlay = false;
async function primeSound(): Promise<void> { async function primeSound(): Promise<void> {
if (priming) return; if (priming) return;
@@ -14,9 +15,14 @@ async function primeSound(): Promise<void> {
window.removeEventListener('pointerdown', primeSound); window.removeEventListener('pointerdown', primeSound);
window.removeEventListener('keydown', primeSound); window.removeEventListener('keydown', primeSound);
} catch { } catch {
try { if (audio) audio.muted = false; } catch { /* Audio may be unavailable. */ } // Keep gesture listeners so the next gesture can retry.
} finally { } finally {
try { if (audio) audio.muted = false; } catch { /* Audio may be unavailable. */ }
priming = false; priming = false;
if (pendingMessagePlay) {
pendingMessagePlay = false;
playMessageNotificationSound();
}
} }
} }
@@ -24,6 +30,10 @@ window.addEventListener('pointerdown', primeSound);
window.addEventListener('keydown', primeSound); window.addEventListener('keydown', primeSound);
export function playMessageNotificationSound(): void { export function playMessageNotificationSound(): void {
if (priming) {
pendingMessagePlay = true;
return;
}
try { try {
audio ??= new Audio('/message-notification.mp3'); audio ??= new Audio('/message-notification.mp3');
audio.currentTime = 0; audio.currentTime = 0;
@@ -130,6 +130,52 @@ test('failed gesture prime unmutes and retries on the next gesture', async () =>
assert.equal(attempts, 3); assert.equal(attempts, 3);
}); });
async function delayedPrimeFixture() {
const handlers = new Map();
window.addEventListener = (type, handler) => handlers.set(type, handler);
window.removeEventListener = (type) => handlers.delete(type);
const plays = [];
let resolvePrime;
let rejectPrime;
const primePlayback = new Promise((resolve, reject) => { resolvePrime = resolve; rejectPrime = reject; });
let instance;
globalThis.Audio = class {
constructor() { instance = this; }
currentTime = 7;
muted = false;
play() { plays.push(this.muted); return plays.length === 1 ? primePlayback : Promise.resolve(); }
pause() {}
};
const sound = await freshSound();
return { handlers, plays, sound, get instance() { return instance; }, resolvePrime, rejectPrime };
}
test('messages during a successful delayed prime play once unmuted after priming', async () => {
const fixture = await delayedPrimeFixture();
const gesture = fixture.handlers.get('pointerdown')();
fixture.sound.playMessageNotificationSound();
fixture.sound.playMessageNotificationSound();
assert.deepEqual(fixture.plays, [true]);
fixture.resolvePrime();
await gesture;
assert.deepEqual(fixture.plays, [true, false]);
assert.equal(fixture.instance.currentTime, 0);
assert.equal(fixture.instance.muted, false);
assert.equal(fixture.handlers.size, 0);
});
test('messages during a failed delayed prime play once unmuted after priming', async () => {
const fixture = await delayedPrimeFixture();
const gesture = fixture.handlers.get('pointerdown')();
fixture.sound.playMessageNotificationSound();
assert.deepEqual(fixture.plays, [true]);
fixture.rejectPrime(new Error('blocked'));
await gesture;
assert.deepEqual(fixture.plays, [true, false]);
assert.equal(fixture.instance.muted, false);
assert.equal(fixture.handlers.size, 2);
});
test('incoming alerts gate own, visible and muted messages, but sound needs no notification permission', async () => { test('incoming alerts gate own, visible and muted messages, but sound needs no notification permission', async () => {
let plays = 0; let plays = 0;
globalThis.Audio = class { currentTime = 0; play() { plays++; return Promise.resolve(); } }; globalThis.Audio = class { currentTime = 0; play() { plays++; return Promise.resolve(); } };