Merge pull request #3995 from element-hq/toger5/fix-double-leave-sound

Fix play of second leave sound
This commit is contained in:
Robin
2026-06-02 14:46:43 +02:00
committed by GitHub
3 changed files with 35 additions and 17 deletions
+14 -10
View File
@@ -251,12 +251,15 @@ test.skip("GroupCallView plays a leave sound synchronously in widget mode", asyn
expect(leaveRTCSession).toHaveBeenCalledOnce();
});
test.skip("Should close widget when all other left and have time to play a sound", async () => {
test("Should close widget when all other left and have time to play a sound", async () => {
const user = userEvent.setup();
const widgetClosedCalled = Promise.withResolvers<void>();
let widgetClosedCalled = false;
const { promise: widgetClosedPromise, resolve: widgetClosedResolver } =
Promise.withResolvers<void>();
const widgetSendMock = vi.fn().mockImplementation((action: string) => {
if (action === ElementWidgetActions.Close) {
widgetClosedCalled.resolve();
widgetClosedCalled = true;
widgetClosedResolver();
}
});
const widgetStopMock = vi.fn().mockResolvedValue(undefined);
@@ -272,7 +275,7 @@ test.skip("Should close widget when all other left and have time to play a sound
lazyActions: new LazyEventEmitter(),
};
const resolvePlaySound = Promise.withResolvers<void>();
playSound = vi.fn().mockReturnValue(resolvePlaySound);
playSound = vi.fn().mockReturnValue(resolvePlaySound.promise);
(useAudioContext as MockedFunction<typeof useAudioContext>).mockReturnValue({
playSound,
playSoundLooping: vitest.fn(),
@@ -283,16 +286,17 @@ test.skip("Should close widget when all other left and have time to play a sound
const leaveButton = getByText("SimulateOtherLeft");
await user.click(leaveButton);
await flushPromises();
expect(widgetSendMock).not.toHaveBeenCalled();
expect(widgetClosedCalled).toBeFalsy();
resolvePlaySound.resolve();
await flushPromises();
expect(playSound).toHaveBeenCalledWith("left");
await widgetClosedCalled.promise;
// Expect the leave sound to be played but silent (volumeOverwrite = 0)
// The allOthersLeft effect should already play a leave sound for the last user in the call.
expect(playSound).toHaveBeenCalledWith("left", 0);
await widgetClosedPromise;
await flushPromises();
expect(widgetClosedCalled).toBeTruthy();
expect(widgetStopMock).toHaveBeenCalledOnce();
});
}, 80000);
test("Should close widget when all other left", async () => {
const user = userEvent.setup();
+18 -4
View File
@@ -120,7 +120,7 @@ export const GroupCallView: FC<Props> = ({
const muteAllAudio = useBehavior(muteAllAudio$);
const leaveSoundContext = useLatest(
useAudioContext({
useAudioContext<CallEventSounds>({
sounds: callEventAudioSounds,
latencyHint: "interactive",
muted: muteAllAudio,
@@ -318,12 +318,26 @@ export const GroupCallView: FC<Props> = ({
(
reason: "timeout" | "user" | "allOthersLeft" | "decline" | "error",
): void => {
let playSound: CallEventSounds = "left";
if (reason === "timeout" || reason === "decline") playSound = reason;
let audioPromise: Promise<void> | undefined = undefined;
switch (reason) {
case "allOthersLeft":
// When "allOthersLeft", the leaveSoundEffect$ in CallEventAudioRenderer
// already plays the "left" sound when the remote participant's media
// disappears. We play it here silenced (volumeOverwrite = 0) so we have the right duration in the audioPromise.
// (used to destory the widget)
audioPromise = leaveSoundContext.current?.playSound("left", 0);
break;
case "timeout":
case "decline":
audioPromise = leaveSoundContext.current?.playSound(reason);
break;
default:
audioPromise = leaveSoundContext.current?.playSound("left");
}
setJoined(false);
setLeft(true);
const audioPromise = leaveSoundContext.current?.playSound(playSound);
// We need to wait until the callEnded event is tracked on PostHog,
// otherwise the iframe may get killed first.
const posthogRequest = new Promise((resolve) => {
+3 -3
View File
@@ -114,7 +114,7 @@ interface Props<S extends string> {
}
interface UseAudioContext<S extends string> {
playSound(soundName: S): Promise<void>;
playSound(soundName: S, volumeOverwrite?: number): Promise<void>;
playSoundLooping(soundName: S, delayS?: number): () => Promise<void>;
/**
* Map of sound name to duration in seconds.
@@ -195,7 +195,7 @@ export function useAudioContext<S extends string>(
}
return {
playSound: async (name): Promise<void> => {
playSound: async (name, volumeOverwrite?: number): Promise<void> => {
if (!audioBuffers[name]) {
logger.debug(`Tried to play a sound that wasn't buffered (${name})`);
return;
@@ -203,7 +203,7 @@ export function useAudioContext<S extends string>(
return playSound(
audioContext,
audioBuffers[name],
soundEffectVolume * earpieceVolume,
volumeOverwrite ?? soundEffectVolume * earpieceVolume,
earpiecePan,
);
},