Skip to content

Commit 42b4c83

Browse files
authored
Merge pull request #3547 from element-hq/valere/fix_blank_widget_auto_leave
fix: Send close widget action on auto-leave
2 parents cdc3c2b + c2f541f commit 42b4c83

File tree

3 files changed

+120
-6
lines changed

3 files changed

+120
-6
lines changed

src/room/GroupCallView.test.tsx

Lines changed: 112 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ import userEvent from "@testing-library/user-event";
2929
import { type RelationsContainer } from "matrix-js-sdk/lib/models/relations-container";
3030
import { useState } from "react";
3131
import { TooltipProvider } from "@vector-im/compound-web";
32+
import { type ITransport } from "matrix-widget-api";
3233

3334
import { prefetchSounds } from "../soundUtils";
3435
import { useAudioContext } from "../useAudioContext";
@@ -43,7 +44,7 @@ import {
4344
MockRTCSession,
4445
} from "../utils/test";
4546
import { GroupCallView } from "./GroupCallView";
46-
import { type WidgetHelpers } from "../widget";
47+
import { ElementWidgetActions, type WidgetHelpers } from "../widget";
4748
import { LazyEventEmitter } from "../LazyEventEmitter";
4849
import { MatrixRTCTransportMissingError } from "../utils/errors";
4950
import { ProcessorProvider } from "../livekit/TrackProcessorContext";
@@ -112,6 +113,10 @@ beforeEach(() => {
112113
return (
113114
<div>
114115
<button onClick={() => onLeave("user")}>Leave</button>
116+
<button onClick={() => onLeave("allOthersLeft")}>
117+
SimulateOtherLeft
118+
</button>
119+
<button onClick={() => onLeave("error")}>SimulateErrorLeft</button>
115120
</div>
116121
);
117122
},
@@ -243,6 +248,112 @@ test.skip("GroupCallView plays a leave sound synchronously in widget mode", asyn
243248
expect(leaveRTCSession).toHaveBeenCalledOnce();
244249
});
245250

251+
test.skip("Should close widget when all other left and have time to play a sound", async () => {
252+
const user = userEvent.setup();
253+
const widgetClosedCalled = Promise.withResolvers<void>();
254+
const widgetSendMock = vi.fn().mockImplementation((action: string) => {
255+
if (action === ElementWidgetActions.Close) {
256+
widgetClosedCalled.resolve();
257+
}
258+
});
259+
const widgetStopMock = vi.fn().mockResolvedValue(undefined);
260+
const widget = {
261+
api: {
262+
setAlwaysOnScreen: vi.fn().mockResolvedValue(true),
263+
transport: {
264+
send: widgetSendMock,
265+
reply: vi.fn().mockResolvedValue(undefined),
266+
stop: widgetStopMock,
267+
} as unknown as ITransport,
268+
} as Partial<WidgetHelpers["api"]>,
269+
lazyActions: new LazyEventEmitter(),
270+
};
271+
const resolvePlaySound = Promise.withResolvers<void>();
272+
playSound = vi.fn().mockReturnValue(resolvePlaySound);
273+
(useAudioContext as MockedFunction<typeof useAudioContext>).mockReturnValue({
274+
playSound,
275+
playSoundLooping: vitest.fn(),
276+
soundDuration: {},
277+
});
278+
279+
const { getByText } = createGroupCallView(widget as WidgetHelpers);
280+
const leaveButton = getByText("SimulateOtherLeft");
281+
await user.click(leaveButton);
282+
await flushPromises();
283+
expect(widgetSendMock).not.toHaveBeenCalled();
284+
resolvePlaySound.resolve();
285+
await flushPromises();
286+
287+
expect(playSound).toHaveBeenCalledWith("left");
288+
289+
await widgetClosedCalled.promise;
290+
await flushPromises();
291+
expect(widgetStopMock).toHaveBeenCalledOnce();
292+
});
293+
294+
test("Should close widget when all other left", async () => {
295+
const user = userEvent.setup();
296+
const widgetClosedCalled = Promise.withResolvers<void>();
297+
const widgetSendMock = vi.fn().mockImplementation((action: string) => {
298+
if (action === ElementWidgetActions.Close) {
299+
widgetClosedCalled.resolve();
300+
}
301+
});
302+
const widgetStopMock = vi.fn().mockResolvedValue(undefined);
303+
const widget = {
304+
api: {
305+
setAlwaysOnScreen: vi.fn().mockResolvedValue(true),
306+
transport: {
307+
send: widgetSendMock,
308+
reply: vi.fn().mockResolvedValue(undefined),
309+
stop: widgetStopMock,
310+
} as unknown as ITransport,
311+
} as Partial<WidgetHelpers["api"]>,
312+
lazyActions: new LazyEventEmitter(),
313+
};
314+
315+
const { getByText } = createGroupCallView(widget as WidgetHelpers);
316+
const leaveButton = getByText("SimulateOtherLeft");
317+
await user.click(leaveButton);
318+
await flushPromises();
319+
320+
await widgetClosedCalled.promise;
321+
await flushPromises();
322+
expect(widgetStopMock).toHaveBeenCalledOnce();
323+
});
324+
325+
test("Should not close widget when auto leave due to error", async () => {
326+
const user = userEvent.setup();
327+
328+
const widgetStopMock = vi.fn().mockResolvedValue(undefined);
329+
const widgetSendMock = vi.fn().mockResolvedValue(undefined);
330+
const widget = {
331+
api: {
332+
setAlwaysOnScreen: vi.fn().mockResolvedValue(true),
333+
transport: {
334+
send: widgetSendMock,
335+
reply: vi.fn().mockResolvedValue(undefined),
336+
stop: widgetStopMock,
337+
} as unknown as ITransport,
338+
} as Partial<WidgetHelpers["api"]>,
339+
lazyActions: new LazyEventEmitter(),
340+
};
341+
342+
const alwaysOnScreenSpy = vi.spyOn(widget.api, "setAlwaysOnScreen");
343+
344+
const { getByText } = createGroupCallView(widget as WidgetHelpers);
345+
const leaveButton = getByText("SimulateErrorLeft");
346+
await user.click(leaveButton);
347+
await flushPromises();
348+
349+
// When onLeft is called, we first set always on screen to false
350+
await waitFor(() => expect(alwaysOnScreenSpy).toHaveBeenCalledWith(false));
351+
await flushPromises();
352+
// But then we do not close the widget automatically
353+
expect(widgetStopMock).not.toHaveBeenCalledOnce();
354+
expect(widgetSendMock).not.toHaveBeenCalledOnce();
355+
});
356+
246357
test.skip("GroupCallView leaves the session when an error occurs", async () => {
247358
(ActiveCall as MockedFunction<typeof ActiveCall>).mockImplementation(() => {
248359
const [error, setError] = useState<Error | null>(null);

src/room/GroupCallView.tsx

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -313,7 +313,9 @@ export const GroupCallView: FC<Props> = ({
313313
const navigate = useNavigate();
314314

315315
const onLeft = useCallback(
316-
(reason: "timeout" | "user" | "allOthersLeft" | "decline"): void => {
316+
(
317+
reason: "timeout" | "user" | "allOthersLeft" | "decline" | "error",
318+
): void => {
317319
let playSound: CallEventSounds = "left";
318320
if (reason === "timeout" || reason === "decline") playSound = reason;
319321

@@ -366,7 +368,7 @@ export const GroupCallView: FC<Props> = ({
366368
}
367369
// On a normal user hangup we can shut down and close the widget. But if an
368370
// error occurs we should keep the widget open until the user reads it.
369-
if (reason === "user" && !getUrlParams().returnToLobby) {
371+
if (reason != "error" && !getUrlParams().returnToLobby) {
370372
try {
371373
await widget.api.transport.send(ElementWidgetActions.Close, {});
372374
} catch (e) {
@@ -518,8 +520,7 @@ export const GroupCallView: FC<Props> = ({
518520
}}
519521
onError={
520522
(/**error*/) => {
521-
// TODO this should not be "user". It needs a new case
522-
if (rtcSession.isJoined()) onLeft("user");
523+
if (rtcSession.isJoined()) onLeft("error");
523524
}
524525
}
525526
>

src/room/InCallView.tsx

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -115,7 +115,9 @@ export interface ActiveCallProps
115115
extends Omit<InCallViewProps, "vm" | "livekitRoom" | "connState"> {
116116
e2eeSystem: EncryptionSystem;
117117
// TODO refactor those reasons into an enum
118-
onLeft: (reason: "user" | "timeout" | "decline" | "allOthersLeft") => void;
118+
onLeft: (
119+
reason: "user" | "timeout" | "decline" | "allOthersLeft" | "error",
120+
) => void;
119121
}
120122

121123
export const ActiveCall: FC<ActiveCallProps> = (props) => {

0 commit comments

Comments
 (0)