diff --git a/src/models/Call.ts b/src/models/Call.ts index 11c4fa18c45..3ee028da97a 100644 --- a/src/models/Call.ts +++ b/src/models/Call.ts @@ -43,8 +43,7 @@ import { isVideoRoom } from "../utils/video-rooms"; import { FontWatcher } from "../settings/watchers/FontWatcher"; import { type JitsiCallMemberContent, JitsiCallMemberEventType } from "../call-types"; import SdkConfig from "../SdkConfig.ts"; -import RoomListStore from "../stores/room-list/RoomListStore.ts"; -import { DefaultTagID } from "../stores/room-list/models.ts"; +import DMRoomMap from "../utils/DMRoomMap.ts"; const TIMEOUT_MS = 16000; @@ -542,6 +541,13 @@ export class JitsiCall extends Call { }; } +export enum ElementCallIntent { + StartCall = "start_call", + JoinExisting = "join_existing", + StartCallDM = "start_call_dm", + JoinExistingDM = "join_existing_dm", +} + /** * A group call using MSC3401 and Element Call as a backend. * (somewhat cheekily named) @@ -586,10 +592,24 @@ export class ElementCall extends Call { const room = client.getRoom(roomId); if (room !== null && !isVideoRoom(room)) { - params.append( - "sendNotificationType", - RoomListStore.instance.getTagsForRoom(room).includes(DefaultTagID.DM) ? "ring" : "notification", - ); + const isDM = !!DMRoomMap.shared().getUserIdForRoomId(room.roomId); + const oldestCallMember = client.matrixRTC.getRoomSession(room).getOldestMembership(); + const hasCallStarted = !!oldestCallMember && oldestCallMember.sender !== client.getSafeUserId(); + if (isDM) { + params.append("sendNotificationType", "ring"); + if (hasCallStarted) { + params.append("intent", ElementCallIntent.JoinExistingDM); + } else { + params.append("intent", ElementCallIntent.StartCallDM); + } + } else { + params.append("sendNotificationType", "notification"); + if (hasCallStarted) { + params.append("intent", ElementCallIntent.JoinExisting); + } else { + params.append("intent", ElementCallIntent.StartCall); + } + } } const rageshakeSubmitUrl = SdkConfig.get("bug_report_endpoint_url"); diff --git a/test/unit-tests/models/Call-test.ts b/test/unit-tests/models/Call-test.ts index 24dc7f0bbb1..a91f951be33 100644 --- a/test/unit-tests/models/Call-test.ts +++ b/test/unit-tests/models/Call-test.ts @@ -39,8 +39,16 @@ import { ConnectionState, JitsiCall, ElementCall, + ElementCallIntent, } from "../../../src/models/Call"; -import { stubClient, mkEvent, mkRoomMember, setupAsyncStoreWithClient, mockPlatformPeg } from "../../test-utils"; +import { + stubClient, + mkEvent, + mkRoomMember, + setupAsyncStoreWithClient, + mockPlatformPeg, + MockEventEmitter, +} from "../../test-utils"; import { MatrixClientPeg } from "../../../src/MatrixClientPeg"; import WidgetStore from "../../../src/stores/WidgetStore"; import { WidgetMessagingStore } from "../../../src/stores/widgets/WidgetMessagingStore"; @@ -50,8 +58,6 @@ import SettingsStore from "../../../src/settings/SettingsStore"; import { Anonymity, PosthogAnalytics } from "../../../src/PosthogAnalytics"; import { type SettingKey } from "../../../src/settings/Settings.tsx"; import SdkConfig from "../../../src/SdkConfig.ts"; -import RoomListStore from "../../../src/stores/room-list/RoomListStore.ts"; -import { DefaultTagID } from "../../../src/stores/room-list/models.ts"; import DMRoomMap from "../../../src/utils/DMRoomMap.ts"; const enabledSettings = new Set(["feature_group_calls", "feature_video_rooms", "feature_element_call_video_rooms"]); @@ -65,6 +71,7 @@ const setUpClientRoomAndStores = (): { alice: RoomMember; bob: RoomMember; carol: RoomMember; + roomSession: Mocked; } => { stubClient(); const client = mocked(MatrixClientPeg.safeGet()); @@ -93,12 +100,13 @@ const setUpClientRoomAndStores = (): { jest.spyOn(room, "getMyMembership").mockReturnValue(KnownMembership.Join); client.getRoom.mockImplementation((roomId) => (roomId === room.roomId ? room : null)); - client.getRoom.mockImplementation((roomId) => (roomId === room.roomId ? room : null)); - client.matrixRTC.getRoomSession.mockImplementation((roomId) => { - const session = new EventEmitter() as MatrixRTCSession; - session.memberships = []; - return session; - }); + + const roomSession = new MockEventEmitter({ + memberships: [], + getOldestMembership: jest.fn().mockReturnValue(undefined), + }) as Mocked; + + client.matrixRTC.getRoomSession.mockReturnValue(roomSession); client.getRooms.mockReturnValue([room]); client.getUserId.mockReturnValue(alice.userId); client.getDeviceId.mockReturnValue("alices_device"); @@ -120,7 +128,7 @@ const setUpClientRoomAndStores = (): { setupAsyncStoreWithClient(WidgetStore.instance, client); setupAsyncStoreWithClient(WidgetMessagingStore.instance, client); - return { client, room, alice, bob, carol }; + return { client, room, alice, bob, carol, roomSession }; }; const cleanUpClientRoomAndStores = (client: MatrixClient, room: Room) => { @@ -553,14 +561,14 @@ describe("ElementCall", () => { let client: Mocked; let room: Room; let alice: RoomMember; - + let roomSession: Mocked; function setRoomMembers(memberIds: string[]) { jest.spyOn(room, "getJoinedMembers").mockReturnValue(memberIds.map((id) => ({ userId: id }) as RoomMember)); } beforeEach(() => { jest.useFakeTimers(); - ({ client, room, alice } = setUpClientRoomAndStores()); + ({ client, room, alice, roomSession } = setUpClientRoomAndStores()); SdkConfig.reset(); }); @@ -571,7 +579,16 @@ describe("ElementCall", () => { }); describe("get", () => { - afterEach(() => Call.get(room)?.destroy()); + let getUserIdForRoomIdSpy: jest.SpyInstance; + + beforeEach(() => { + getUserIdForRoomIdSpy = jest.spyOn(DMRoomMap.shared(), "getUserIdForRoomId"); + }); + + afterEach(() => { + Call.get(room)?.destroy(); + getUserIdForRoomIdSpy.mockRestore(); + }); it("finds no calls", () => { expect(Call.get(room)).toBeNull(); @@ -600,11 +617,7 @@ describe("ElementCall", () => { it("finds ongoing calls that are created by the session manager", async () => { // There is an existing session created by another user in this room. - client.matrixRTC.getRoomSession.mockReturnValue({ - on: (ev: any, fn: any) => {}, - off: (ev: any, fn: any) => {}, - memberships: [{ fakeVal: "fake membership" }], - } as unknown as MatrixRTCSession); + roomSession.memberships.push({} as CallMembership); const call = Call.get(room); if (!(call instanceof ElementCall)) throw new Error("Failed to create call"); }); @@ -750,19 +763,50 @@ describe("ElementCall", () => { expect(urlParams.get("analyticsID")).toBeFalsy(); }); - it("requests ringing notifications in DMs", async () => { - const tagsSpy = jest.spyOn(RoomListStore.instance, "getTagsForRoom"); - try { - tagsSpy.mockReturnValue([DefaultTagID.DM]); - ElementCall.create(room); - const call = Call.get(room); - if (!(call instanceof ElementCall)) throw new Error("Failed to create call"); + it("requests ringing notifications and correct intent in DMs", async () => { + getUserIdForRoomIdSpy.mockImplementation((roomId: string) => + room.roomId === roomId ? "any-user" : undefined, + ); + ElementCall.create(room); + const call = Call.get(room); + if (!(call instanceof ElementCall)) throw new Error("Failed to create call"); - const urlParams = new URLSearchParams(new URL(call.widget.url).hash.slice(1)); - expect(urlParams.get("sendNotificationType")).toBe("ring"); - } finally { - tagsSpy.mockRestore(); - } + const urlParams = new URLSearchParams(new URL(call.widget.url).hash.slice(1)); + expect(urlParams.get("sendNotificationType")).toBe("ring"); + expect(urlParams.get("intent")).toBe(ElementCallIntent.StartCallDM); + }); + + it("requests correct intent when answering DMs", async () => { + roomSession.getOldestMembership.mockReturnValue({} as CallMembership); + getUserIdForRoomIdSpy.mockImplementation((roomId: string) => + room.roomId === roomId ? "any-user" : undefined, + ); + ElementCall.create(room); + const call = Call.get(room); + if (!(call instanceof ElementCall)) throw new Error("Failed to create call"); + + const urlParams = new URLSearchParams(new URL(call.widget.url).hash.slice(1)); + expect(urlParams.get("intent")).toBe(ElementCallIntent.JoinExistingDM); + }); + + it("requests correct intent when creating a non-DM call", async () => { + roomSession.getOldestMembership.mockReturnValue(undefined); + ElementCall.create(room); + const call = Call.get(room); + if (!(call instanceof ElementCall)) throw new Error("Failed to create call"); + + const urlParams = new URLSearchParams(new URL(call.widget.url).hash.slice(1)); + expect(urlParams.get("intent")).toBe(ElementCallIntent.StartCall); + }); + + it("requests correct intent when joining a non-DM call", async () => { + roomSession.getOldestMembership.mockReturnValue({} as CallMembership); + ElementCall.create(room); + const call = Call.get(room); + if (!(call instanceof ElementCall)) throw new Error("Failed to create call"); + + const urlParams = new URLSearchParams(new URL(call.widget.url).hash.slice(1)); + expect(urlParams.get("intent")).toBe(ElementCallIntent.JoinExisting); }); it("requests visual notifications in non-DMs", async () => { diff --git a/test/unit-tests/stores/RoomViewStore-test.ts b/test/unit-tests/stores/RoomViewStore-test.ts index 4bd799c7aac..064cd691569 100644 --- a/test/unit-tests/stores/RoomViewStore-test.ts +++ b/test/unit-tests/stores/RoomViewStore-test.ts @@ -44,6 +44,7 @@ import { CallStore } from "../../../src/stores/CallStore"; import { MatrixClientPeg } from "../../../src/MatrixClientPeg"; import MediaDeviceHandler, { MediaDeviceKindEnum } from "../../../src/MediaDeviceHandler"; import { storeRoomAliasInCache } from "../../../src/RoomAliasCache.ts"; +import { type Call } from "../../../src/models/Call.ts"; jest.mock("../../../src/Modal"); @@ -361,8 +362,12 @@ describe("RoomViewStore", function () { }); it("when viewing a call without a broadcast, it should not raise an error", async () => { + const call = { presented: false } as Call; + const getCallSpy = jest.spyOn(CallStore.instance, "getCall").mockReturnValue(call); await setupAsyncStoreWithClient(CallStore.instance, MatrixClientPeg.safeGet()); await viewCall(); + expect(getCallSpy).toHaveBeenCalledWith(roomId); + expect(call.presented).toEqual(true); }); it("should display an error message when the room is unreachable via the roomId", async () => {