fix(call): ringtone loudness, caller decline notice, All-Muted badge
Three issues from live testing:
- A1: the 'classic' ringtone (call.ogg, mastered near full scale) was much
louder than the synthesized styles. Attenuate it (CLASSIC_GAIN 0.45) so all
ringtones sit at a comparable level.
- A3/A4: the caller had no indication when a DM/group callee declined — their
UI kept "ringing" until the notification lifetime expired. IncomingCallListener
now listens for RTCDecline events for a call we're hosting in the room and
toasts the caller ("<name> declined your call").
- G1: the PiP "All muted" badge fired when any single remote participant muted.
useRemoteAllMuted now returns true only when there is >=1 remote and every
remote participant is muted.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -37,6 +37,7 @@ import {
|
|||||||
useCallStart,
|
useCallStart,
|
||||||
} from '../hooks/useCallEmbed';
|
} from '../hooks/useCallEmbed';
|
||||||
import { callChatAtom, callEmbedAtom } from '../state/callEmbed';
|
import { callChatAtom, callEmbedAtom } from '../state/callEmbed';
|
||||||
|
import { toastQueueAtom } from '../state/toast';
|
||||||
import { CallEmbed, useCallControlState } from '../plugins/call';
|
import { CallEmbed, useCallControlState } from '../plugins/call';
|
||||||
import { useSelectedRoom } from '../hooks/router/useSelectedRoom';
|
import { useSelectedRoom } from '../hooks/router/useSelectedRoom';
|
||||||
import { ScreenSize, useScreenSizeContext } from '../hooks/useScreenSize';
|
import { ScreenSize, useScreenSizeContext } from '../hooks/useScreenSize';
|
||||||
@@ -405,6 +406,7 @@ function IncomingCallListener({ callEmbed, joined }: IncomingCallListenerProps)
|
|||||||
const mx = useMatrixClient();
|
const mx = useMatrixClient();
|
||||||
const directs = useAtomValue(mDirectAtom);
|
const directs = useAtomValue(mDirectAtom);
|
||||||
const { navigateRoom } = useRoomNavigate();
|
const { navigateRoom } = useRoomNavigate();
|
||||||
|
const setToast = useSetAtom(toastQueueAtom);
|
||||||
|
|
||||||
const [callInfo, setCallInfo] = useState<IncomingCallInfo>();
|
const [callInfo, setCallInfo] = useState<IncomingCallInfo>();
|
||||||
const dm = callInfo ? directs.has(callInfo.room.roomId) : false;
|
const dm = callInfo ? directs.has(callInfo.room.roomId) : false;
|
||||||
@@ -424,6 +426,31 @@ function IncomingCallListener({ callEmbed, joined }: IncomingCallListenerProps)
|
|||||||
await event.getDecryptionPromise();
|
await event.getDecryptionPromise();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Caller-side: a participant declined a call we're hosting in this room.
|
||||||
|
// Without this the caller's UI keeps "ringing" until the notification
|
||||||
|
// lifetime expires, with no indication the callee said no.
|
||||||
|
if (event.getType() === EventType.RTCDecline) {
|
||||||
|
const decliner = event.getSender();
|
||||||
|
if (
|
||||||
|
data.liveEvent &&
|
||||||
|
room &&
|
||||||
|
decliner &&
|
||||||
|
decliner !== mx.getSafeUserId() &&
|
||||||
|
callEmbed?.roomId === room.roomId
|
||||||
|
) {
|
||||||
|
const declinerName =
|
||||||
|
getMemberDisplayName(room, decliner) ?? getMxIdLocalPart(decliner) ?? decliner;
|
||||||
|
setToast({
|
||||||
|
id: `rtc-decline-${event.getId() ?? decliner}`,
|
||||||
|
displayName: declinerName,
|
||||||
|
body: 'Declined your call',
|
||||||
|
roomName: room.name,
|
||||||
|
roomId: room.roomId,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (
|
if (
|
||||||
!room ||
|
!room ||
|
||||||
event.getType() !== EventType.RTCNotification ||
|
event.getType() !== EventType.RTCNotification ||
|
||||||
@@ -486,7 +513,7 @@ function IncomingCallListener({ callEmbed, joined }: IncomingCallListenerProps)
|
|||||||
|
|
||||||
setCallInfo(info);
|
setCallInfo(info);
|
||||||
},
|
},
|
||||||
[mx, directs],
|
[mx, directs, callEmbed, setToast],
|
||||||
);
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|||||||
@@ -145,13 +145,17 @@ export const useRemoteAllMuted = (callEmbed: CallEmbed | undefined): boolean =>
|
|||||||
// Each participant's mute icon has data-muted="true"|"false" and
|
// Each participant's mute icon has data-muted="true"|"false" and
|
||||||
// aria-label set to their Matrix user ID.
|
// aria-label set to their Matrix user ID.
|
||||||
const muteIcons = doc.querySelectorAll<HTMLElement>('[data-muted]');
|
const muteIcons = doc.querySelectorAll<HTMLElement>('[data-muted]');
|
||||||
let anyRemoteMuted = false;
|
let remoteCount = 0;
|
||||||
|
let remoteMutedCount = 0;
|
||||||
muteIcons.forEach((el) => {
|
muteIcons.forEach((el) => {
|
||||||
const userId = el.getAttribute('aria-label') ?? '';
|
const userId = el.getAttribute('aria-label') ?? '';
|
||||||
if (userId === localUserId) return;
|
if (userId === localUserId) return;
|
||||||
if (el.getAttribute('data-muted') === 'true') anyRemoteMuted = true;
|
remoteCount += 1;
|
||||||
|
if (el.getAttribute('data-muted') === 'true') remoteMutedCount += 1;
|
||||||
});
|
});
|
||||||
setMuted(anyRemoteMuted);
|
// "All muted" badge: true only when there is at least one remote
|
||||||
|
// participant and every one of them is muted (not merely any single one).
|
||||||
|
setMuted(remoteCount > 0 && remoteMutedCount === remoteCount);
|
||||||
};
|
};
|
||||||
|
|
||||||
let tileObserver: MutationObserver | undefined;
|
let tileObserver: MutationObserver | undefined;
|
||||||
|
|||||||
@@ -102,12 +102,17 @@ const playPhrase = (style: SynthStyle, volume: number, destination: AudioNode):
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// The bundled call.ogg is mastered near full scale, so at equal `volume` it is
|
||||||
|
// perceptibly much louder than the synthesized styles (which peak at ~0.12–0.3).
|
||||||
|
// Attenuate it so all ringtones sit at a comparable loudness.
|
||||||
|
const CLASSIC_GAIN = 0.45;
|
||||||
|
|
||||||
const startClassic = (volume: number, loop: boolean): (() => void) => {
|
const startClassic = (volume: number, loop: boolean): (() => void) => {
|
||||||
let audio: HTMLAudioElement | undefined;
|
let audio: HTMLAudioElement | undefined;
|
||||||
try {
|
try {
|
||||||
audio = new Audio(CallSound);
|
audio = new Audio(CallSound);
|
||||||
audio.loop = loop;
|
audio.loop = loop;
|
||||||
audio.volume = clamp01(volume);
|
audio.volume = clamp01(volume) * CLASSIC_GAIN;
|
||||||
audio.play().catch(() => undefined);
|
audio.play().catch(() => undefined);
|
||||||
} catch {
|
} catch {
|
||||||
audio = undefined;
|
audio = undefined;
|
||||||
|
|||||||
Reference in New Issue
Block a user