From a73f4f5803b315e4e4890c5ed5f2afa4906539f8 Mon Sep 17 00:00:00 2001 From: Will Hunt Date: Tue, 9 Sep 2025 16:45:42 +0100 Subject: [PATCH] Stop ringing and remove toast if another device answers a RTC call. (#30728) * Stop ringing if another device answers a call. * Add test * fix check --- src/Notifier.ts | 16 ++++++++++- test/unit-tests/Notifier-test.ts | 47 ++++++++++++++++++++++++++++++++ 2 files changed, 62 insertions(+), 1 deletion(-) diff --git a/src/Notifier.ts b/src/Notifier.ts index 7dce26d6bd..dd47b8b204 100644 --- a/src/Notifier.ts +++ b/src/Notifier.ts @@ -486,13 +486,27 @@ class NotifierClass extends TypedEventEmitter m.sender === cli.getUserId()); + if (EventType.GroupCallMemberPrefix === type && thisUserHasConnectedDevice) { + const content = ev.getContent(); + + if (typeof content.call_id !== "string") { + logger.warn( + "Received malformatted GroupCallMemberPrefix event. Did not contain 'call_id' of type 'string'", + ); + return; + } + // One of our devices has joined the call, so dismiss it. + ToastStore.sharedInstance().dismissToast(getIncomingCallToastKey(content.call_id, room.roomId)); + } // Check maximum age (<= 15 seconds) of a call notify event that will trigger a ringing notification - if (EventType.CallNotify === ev.getType() && (ev.getAge() ?? 0) < 15000 && !thisUserHasConnectedDevice) { + else if (EventType.CallNotify === type && (ev.getAge() ?? 0) < 15000 && !thisUserHasConnectedDevice) { const content = ev.getContent(); const roomId = ev.getRoomId(); + if (typeof content.call_id !== "string") { logger.warn("Received malformatted CallNotify event. Did not contain 'call_id' of type 'string'"); return; diff --git a/test/unit-tests/Notifier-test.ts b/test/unit-tests/Notifier-test.ts index 92df3bc385..e5b8f84f51 100644 --- a/test/unit-tests/Notifier-test.ts +++ b/test/unit-tests/Notifier-test.ts @@ -371,6 +371,7 @@ describe("Notifier", () => { beforeEach(() => { jest.spyOn(SettingsStore, "getValue").mockReturnValue(true); jest.spyOn(ToastStore.sharedInstance(), "addOrReplaceToast"); + jest.spyOn(ToastStore.sharedInstance(), "dismissToast"); mockClient.getPushActionsForEvent.mockReturnValue({ notify: true, @@ -443,6 +444,52 @@ describe("Notifier", () => { spyCallMemberships.mockRestore(); }); + it("dismisses call notification when another device answers the call", () => { + const notifyEvent = emitCallNotifyEvent(); + const spyCallMemberships = jest.spyOn(MatrixRTCSession, "callMembershipsForRoom"); + + expect(ToastStore.sharedInstance().addOrReplaceToast).toHaveBeenCalledWith( + expect.objectContaining({ + key: getIncomingCallToastKey(notifyEvent.getContent().call_id ?? "", roomId), + priority: 100, + component: IncomingCallToast, + bodyClassName: "mx_IncomingCallToast", + props: { notifyEvent }, + }), + ); + // Mock ourselves joining the call. + spyCallMemberships.mockReturnValue([ + new CallMembership( + mkEvent({ + event: true, + room: testRoom.roomId, + user: userId, + type: EventType.GroupCallMemberPrefix, + content: {}, + }), + { + call_id: "123", + application: "m.call", + focus_active: { type: "livekit" }, + foci_preferred: [], + device_id: "DEVICE", + }, + ), + ]); + const callEvent = mkEvent({ + type: EventType.GroupCallMemberPrefix, + user: "@alice:foo", + room: roomId, + content: { + call_id: "abc123", + }, + event: true, + }); + emitLiveEvent(callEvent); + expect(ToastStore.sharedInstance().dismissToast).toHaveBeenCalled(); + spyCallMemberships.mockRestore(); + }); + it("should not show toast when calling with non-group call event", () => { emitCallNotifyEvent("event_type");