fix(calls,matrix): address review findings from agent code review
- CallEmbed watchdog now SELF-HEALS: a genuine ready/joined signal arriving after the 25s timeout clears the error and notifies subscribers with undefined, so a slow-but-successful EC load no longer strands the user on the recovery screen over a live call. Listener dispatch wrapped in try/catch. - ringtones: synth notes route through a per-session master gain; stop() ramps it to 0 so the ring is silenced instantly on answer instead of letting the last scheduled phrase ring out over call audio. - IncomingCallBanner: ping fires exactly once per incoming call (guarded by refEventId) instead of re-pinging when ringtone settings change mid-banner. - focusCameraParticipant: try multiple tile selectors (EC labels vary by version), defer the tile click past EC's async spotlight layout switch (rAF x2), and dev-warn when no tile matches so testers get signal. - uploadContent: a cancelled upload (mx.cancelUpload -> AbortError) is no longer treated as retryable — previously the retry loop could resurrect an upload the user just cancelled. Also retry on 408. - addRoomIdToMDirect/removeRoomIdFromMDirect: guard against a corrupt m.direct whose values aren't arrays. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -78,7 +78,7 @@ const PHRASES: Record<
|
||||
},
|
||||
};
|
||||
|
||||
const playPhrase = (style: SynthStyle, volume: number): void => {
|
||||
const playPhrase = (style: SynthStyle, volume: number, destination: AudioNode): void => {
|
||||
const ctx = getCtx();
|
||||
if (!ctx) return;
|
||||
const { type, gain: peak, notes } = PHRASES[style];
|
||||
@@ -96,7 +96,7 @@ const playPhrase = (style: SynthStyle, volume: number): void => {
|
||||
gain.gain.linearRampToValueAtTime(scaledPeak, start + 0.015);
|
||||
gain.gain.exponentialRampToValueAtTime(0.0001, start + dur);
|
||||
osc.connect(gain);
|
||||
gain.connect(ctx.destination);
|
||||
gain.connect(destination);
|
||||
osc.start(start);
|
||||
osc.stop(start + dur + 0.02);
|
||||
});
|
||||
@@ -121,11 +121,41 @@ const startClassic = (volume: number, loop: boolean): (() => void) => {
|
||||
};
|
||||
|
||||
const startSynth = (style: SynthStyle, volume: number, loop: boolean): (() => void) => {
|
||||
playPhrase(style, volume);
|
||||
if (!loop) return () => undefined;
|
||||
const period = PHRASES[style].period * 1000;
|
||||
const id = window.setInterval(() => playPhrase(style, volume), period);
|
||||
return () => window.clearInterval(id);
|
||||
const ctx = getCtx();
|
||||
if (!ctx) return () => undefined;
|
||||
// All notes route through a per-session master gain so stop() can silence
|
||||
// everything instantly — including notes already scheduled slightly in the
|
||||
// future — instead of letting the last phrase ring out after the user answers.
|
||||
const master = ctx.createGain();
|
||||
master.gain.value = 1;
|
||||
master.connect(ctx.destination);
|
||||
|
||||
playPhrase(style, volume, master);
|
||||
const id = loop
|
||||
? window.setInterval(() => playPhrase(style, volume, master), PHRASES[style].period * 1000)
|
||||
: 0;
|
||||
|
||||
let stopped = false;
|
||||
return () => {
|
||||
if (stopped) return;
|
||||
stopped = true;
|
||||
if (id) window.clearInterval(id);
|
||||
try {
|
||||
const now = ctx.currentTime;
|
||||
master.gain.cancelScheduledValues(now);
|
||||
master.gain.setValueAtTime(master.gain.value, now);
|
||||
master.gain.linearRampToValueAtTime(0, now + 0.03);
|
||||
} catch {
|
||||
/* context may be closed */
|
||||
}
|
||||
window.setTimeout(() => {
|
||||
try {
|
||||
master.disconnect();
|
||||
} catch {
|
||||
/* already disconnected */
|
||||
}
|
||||
}, 100);
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user