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:
2026-06-26 18:15:51 -04:00
parent fc8eb70617
commit bc85cd4984
5 changed files with 131 additions and 31 deletions
+39 -11
View File
@@ -356,20 +356,48 @@ export class CallControl extends EventEmitter implements CallControlState {
const doc = this.document;
if (!doc) return;
// Find the mute icon / aria-label element that identifies this participant
const userEl = doc.querySelector<HTMLElement>(`[aria-label="${CSS.escape(userId)}"]`);
// Walk up to the nearest video tile container
const tile =
userEl?.closest<HTMLElement>('[data-testid="videoTile"]') ??
userEl?.closest<HTMLElement>('[data-video-fit]');
// EC labels participant tiles inconsistently across versions — the user's
// matrix id may be the full aria-label, a substring of it, or carried on a
// data attribute (and sometimes the visible label is the display name, not
// the id at all). Try several strategies before giving up, then walk up to
// the enclosing video tile.
const findTile = (): HTMLElement | undefined => {
const escaped = CSS.escape(userId);
const el =
doc.querySelector<HTMLElement>(`[aria-label="${escaped}"]`) ??
doc.querySelector<HTMLElement>(`[data-testid="videoTile"][aria-label*="${escaped}"]`) ??
doc.querySelector<HTMLElement>(`[aria-label*="${escaped}"]`) ??
doc.querySelector<HTMLElement>(`[data-member-id="${escaped}"]`) ??
doc.querySelector<HTMLElement>(`[data-id="${escaped}"]`) ??
undefined;
return (
el?.closest<HTMLElement>('[data-testid="videoTile"]') ??
el?.closest<HTMLElement>('[data-video-fit]') ??
el ??
undefined
);
};
if (!this.spotlight) {
this.spotlightButton?.click();
const applyFocus = () => {
const tile = findTile();
if (tile) {
tile.click();
} else if (import.meta.env.DEV) {
console.warn(`[CallControl] focusCameraParticipant: no tile matched ${userId}`);
}
};
if (this.spotlight) {
// Already in spotlight — pin immediately.
applyFocus();
return;
}
if (tile) {
tile.click();
}
// Switching to spotlight re-renders EC's layout asynchronously; clicking the
// tile in the same tick would land in the old (grid) DOM. Toggle spotlight,
// then click on a later frame once the spotlight tiles have mounted.
this.spotlightButton?.click();
requestAnimationFrame(() => requestAnimationFrame(applyFocus));
}
public dispose() {