Skip to content

Commit 1e0cdf7

Browse files
authored
Set Element Call "intents" when starting and answering DM calls. (#30730)
* Start to implement intents for DM calls. * Refactor and fix intent bugs * Do not default skipLobby in Element Web * Remove hacks * cleanup * Don't template skipLobby or returnToLobby but inject as required * Revert "Don't template skipLobby or returnToLobby but inject as required" This reverts commit 35569f3. * lint * Fix test * lint * Use other intents * Ensure we test all intents * lint * cleanup * Fix room check * Update imports * update test * Fix RoomViewStore test
1 parent 33d3df2 commit 1e0cdf7

File tree

3 files changed

+105
-36
lines changed

3 files changed

+105
-36
lines changed

src/models/Call.ts

Lines changed: 26 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -43,8 +43,7 @@ import { isVideoRoom } from "../utils/video-rooms";
4343
import { FontWatcher } from "../settings/watchers/FontWatcher";
4444
import { type JitsiCallMemberContent, JitsiCallMemberEventType } from "../call-types";
4545
import SdkConfig from "../SdkConfig.ts";
46-
import RoomListStore from "../stores/room-list/RoomListStore.ts";
47-
import { DefaultTagID } from "../stores/room-list/models.ts";
46+
import DMRoomMap from "../utils/DMRoomMap.ts";
4847

4948
const TIMEOUT_MS = 16000;
5049

@@ -542,6 +541,13 @@ export class JitsiCall extends Call {
542541
};
543542
}
544543

544+
export enum ElementCallIntent {
545+
StartCall = "start_call",
546+
JoinExisting = "join_existing",
547+
StartCallDM = "start_call_dm",
548+
JoinExistingDM = "join_existing_dm",
549+
}
550+
545551
/**
546552
* A group call using MSC3401 and Element Call as a backend.
547553
* (somewhat cheekily named)
@@ -586,10 +592,24 @@ export class ElementCall extends Call {
586592

587593
const room = client.getRoom(roomId);
588594
if (room !== null && !isVideoRoom(room)) {
589-
params.append(
590-
"sendNotificationType",
591-
RoomListStore.instance.getTagsForRoom(room).includes(DefaultTagID.DM) ? "ring" : "notification",
592-
);
595+
const isDM = !!DMRoomMap.shared().getUserIdForRoomId(room.roomId);
596+
const oldestCallMember = client.matrixRTC.getRoomSession(room).getOldestMembership();
597+
const hasCallStarted = !!oldestCallMember && oldestCallMember.sender !== client.getSafeUserId();
598+
if (isDM) {
599+
params.append("sendNotificationType", "ring");
600+
if (hasCallStarted) {
601+
params.append("intent", ElementCallIntent.JoinExistingDM);
602+
} else {
603+
params.append("intent", ElementCallIntent.StartCallDM);
604+
}
605+
} else {
606+
params.append("sendNotificationType", "notification");
607+
if (hasCallStarted) {
608+
params.append("intent", ElementCallIntent.JoinExisting);
609+
} else {
610+
params.append("intent", ElementCallIntent.StartCall);
611+
}
612+
}
593613
}
594614

595615
const rageshakeSubmitUrl = SdkConfig.get("bug_report_endpoint_url");

test/unit-tests/models/Call-test.ts

Lines changed: 74 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -39,8 +39,16 @@ import {
3939
ConnectionState,
4040
JitsiCall,
4141
ElementCall,
42+
ElementCallIntent,
4243
} from "../../../src/models/Call";
43-
import { stubClient, mkEvent, mkRoomMember, setupAsyncStoreWithClient, mockPlatformPeg } from "../../test-utils";
44+
import {
45+
stubClient,
46+
mkEvent,
47+
mkRoomMember,
48+
setupAsyncStoreWithClient,
49+
mockPlatformPeg,
50+
MockEventEmitter,
51+
} from "../../test-utils";
4452
import { MatrixClientPeg } from "../../../src/MatrixClientPeg";
4553
import WidgetStore from "../../../src/stores/WidgetStore";
4654
import { WidgetMessagingStore } from "../../../src/stores/widgets/WidgetMessagingStore";
@@ -50,8 +58,6 @@ import SettingsStore from "../../../src/settings/SettingsStore";
5058
import { Anonymity, PosthogAnalytics } from "../../../src/PosthogAnalytics";
5159
import { type SettingKey } from "../../../src/settings/Settings.tsx";
5260
import SdkConfig from "../../../src/SdkConfig.ts";
53-
import RoomListStore from "../../../src/stores/room-list/RoomListStore.ts";
54-
import { DefaultTagID } from "../../../src/stores/room-list/models.ts";
5561
import DMRoomMap from "../../../src/utils/DMRoomMap.ts";
5662

5763
const enabledSettings = new Set(["feature_group_calls", "feature_video_rooms", "feature_element_call_video_rooms"]);
@@ -65,6 +71,7 @@ const setUpClientRoomAndStores = (): {
6571
alice: RoomMember;
6672
bob: RoomMember;
6773
carol: RoomMember;
74+
roomSession: Mocked<MatrixRTCSession>;
6875
} => {
6976
stubClient();
7077
const client = mocked<MatrixClient>(MatrixClientPeg.safeGet());
@@ -93,12 +100,13 @@ const setUpClientRoomAndStores = (): {
93100
jest.spyOn(room, "getMyMembership").mockReturnValue(KnownMembership.Join);
94101

95102
client.getRoom.mockImplementation((roomId) => (roomId === room.roomId ? room : null));
96-
client.getRoom.mockImplementation((roomId) => (roomId === room.roomId ? room : null));
97-
client.matrixRTC.getRoomSession.mockImplementation((roomId) => {
98-
const session = new EventEmitter() as MatrixRTCSession;
99-
session.memberships = [];
100-
return session;
101-
});
103+
104+
const roomSession = new MockEventEmitter({
105+
memberships: [],
106+
getOldestMembership: jest.fn().mockReturnValue(undefined),
107+
}) as Mocked<MatrixRTCSession>;
108+
109+
client.matrixRTC.getRoomSession.mockReturnValue(roomSession);
102110
client.getRooms.mockReturnValue([room]);
103111
client.getUserId.mockReturnValue(alice.userId);
104112
client.getDeviceId.mockReturnValue("alices_device");
@@ -120,7 +128,7 @@ const setUpClientRoomAndStores = (): {
120128
setupAsyncStoreWithClient(WidgetStore.instance, client);
121129
setupAsyncStoreWithClient(WidgetMessagingStore.instance, client);
122130

123-
return { client, room, alice, bob, carol };
131+
return { client, room, alice, bob, carol, roomSession };
124132
};
125133

126134
const cleanUpClientRoomAndStores = (client: MatrixClient, room: Room) => {
@@ -553,14 +561,14 @@ describe("ElementCall", () => {
553561
let client: Mocked<MatrixClient>;
554562
let room: Room;
555563
let alice: RoomMember;
556-
564+
let roomSession: Mocked<MatrixRTCSession>;
557565
function setRoomMembers(memberIds: string[]) {
558566
jest.spyOn(room, "getJoinedMembers").mockReturnValue(memberIds.map((id) => ({ userId: id }) as RoomMember));
559567
}
560568

561569
beforeEach(() => {
562570
jest.useFakeTimers();
563-
({ client, room, alice } = setUpClientRoomAndStores());
571+
({ client, room, alice, roomSession } = setUpClientRoomAndStores());
564572
SdkConfig.reset();
565573
});
566574

@@ -571,7 +579,16 @@ describe("ElementCall", () => {
571579
});
572580

573581
describe("get", () => {
574-
afterEach(() => Call.get(room)?.destroy());
582+
let getUserIdForRoomIdSpy: jest.SpyInstance;
583+
584+
beforeEach(() => {
585+
getUserIdForRoomIdSpy = jest.spyOn(DMRoomMap.shared(), "getUserIdForRoomId");
586+
});
587+
588+
afterEach(() => {
589+
Call.get(room)?.destroy();
590+
getUserIdForRoomIdSpy.mockRestore();
591+
});
575592

576593
it("finds no calls", () => {
577594
expect(Call.get(room)).toBeNull();
@@ -600,11 +617,7 @@ describe("ElementCall", () => {
600617

601618
it("finds ongoing calls that are created by the session manager", async () => {
602619
// There is an existing session created by another user in this room.
603-
client.matrixRTC.getRoomSession.mockReturnValue({
604-
on: (ev: any, fn: any) => {},
605-
off: (ev: any, fn: any) => {},
606-
memberships: [{ fakeVal: "fake membership" }],
607-
} as unknown as MatrixRTCSession);
620+
roomSession.memberships.push({} as CallMembership);
608621
const call = Call.get(room);
609622
if (!(call instanceof ElementCall)) throw new Error("Failed to create call");
610623
});
@@ -750,19 +763,50 @@ describe("ElementCall", () => {
750763
expect(urlParams.get("analyticsID")).toBeFalsy();
751764
});
752765

753-
it("requests ringing notifications in DMs", async () => {
754-
const tagsSpy = jest.spyOn(RoomListStore.instance, "getTagsForRoom");
755-
try {
756-
tagsSpy.mockReturnValue([DefaultTagID.DM]);
757-
ElementCall.create(room);
758-
const call = Call.get(room);
759-
if (!(call instanceof ElementCall)) throw new Error("Failed to create call");
766+
it("requests ringing notifications and correct intent in DMs", async () => {
767+
getUserIdForRoomIdSpy.mockImplementation((roomId: string) =>
768+
room.roomId === roomId ? "any-user" : undefined,
769+
);
770+
ElementCall.create(room);
771+
const call = Call.get(room);
772+
if (!(call instanceof ElementCall)) throw new Error("Failed to create call");
760773

761-
const urlParams = new URLSearchParams(new URL(call.widget.url).hash.slice(1));
762-
expect(urlParams.get("sendNotificationType")).toBe("ring");
763-
} finally {
764-
tagsSpy.mockRestore();
765-
}
774+
const urlParams = new URLSearchParams(new URL(call.widget.url).hash.slice(1));
775+
expect(urlParams.get("sendNotificationType")).toBe("ring");
776+
expect(urlParams.get("intent")).toBe(ElementCallIntent.StartCallDM);
777+
});
778+
779+
it("requests correct intent when answering DMs", async () => {
780+
roomSession.getOldestMembership.mockReturnValue({} as CallMembership);
781+
getUserIdForRoomIdSpy.mockImplementation((roomId: string) =>
782+
room.roomId === roomId ? "any-user" : undefined,
783+
);
784+
ElementCall.create(room);
785+
const call = Call.get(room);
786+
if (!(call instanceof ElementCall)) throw new Error("Failed to create call");
787+
788+
const urlParams = new URLSearchParams(new URL(call.widget.url).hash.slice(1));
789+
expect(urlParams.get("intent")).toBe(ElementCallIntent.JoinExistingDM);
790+
});
791+
792+
it("requests correct intent when creating a non-DM call", async () => {
793+
roomSession.getOldestMembership.mockReturnValue(undefined);
794+
ElementCall.create(room);
795+
const call = Call.get(room);
796+
if (!(call instanceof ElementCall)) throw new Error("Failed to create call");
797+
798+
const urlParams = new URLSearchParams(new URL(call.widget.url).hash.slice(1));
799+
expect(urlParams.get("intent")).toBe(ElementCallIntent.StartCall);
800+
});
801+
802+
it("requests correct intent when joining a non-DM call", async () => {
803+
roomSession.getOldestMembership.mockReturnValue({} as CallMembership);
804+
ElementCall.create(room);
805+
const call = Call.get(room);
806+
if (!(call instanceof ElementCall)) throw new Error("Failed to create call");
807+
808+
const urlParams = new URLSearchParams(new URL(call.widget.url).hash.slice(1));
809+
expect(urlParams.get("intent")).toBe(ElementCallIntent.JoinExisting);
766810
});
767811

768812
it("requests visual notifications in non-DMs", async () => {

test/unit-tests/stores/RoomViewStore-test.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ import { CallStore } from "../../../src/stores/CallStore";
4444
import { MatrixClientPeg } from "../../../src/MatrixClientPeg";
4545
import MediaDeviceHandler, { MediaDeviceKindEnum } from "../../../src/MediaDeviceHandler";
4646
import { storeRoomAliasInCache } from "../../../src/RoomAliasCache.ts";
47+
import { type Call } from "../../../src/models/Call.ts";
4748

4849
jest.mock("../../../src/Modal");
4950

@@ -361,8 +362,12 @@ describe("RoomViewStore", function () {
361362
});
362363

363364
it("when viewing a call without a broadcast, it should not raise an error", async () => {
365+
const call = { presented: false } as Call;
366+
const getCallSpy = jest.spyOn(CallStore.instance, "getCall").mockReturnValue(call);
364367
await setupAsyncStoreWithClient(CallStore.instance, MatrixClientPeg.safeGet());
365368
await viewCall();
369+
expect(getCallSpy).toHaveBeenCalledWith(roomId);
370+
expect(call.presented).toEqual(true);
366371
});
367372

368373
it("should display an error message when the room is unreachable via the roomId", async () => {

0 commit comments

Comments
 (0)