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 35569f35bb254462dd86c16438dab38188db6fbc. * lint * Fix test * lint * Use other intents * Ensure we test all intents * lint * cleanup * Fix room check * Update imports * update test * Fix RoomViewStore test
This commit is contained in:
@@ -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");
|
||||
|
||||
@@ -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<MatrixRTCSession>;
|
||||
} => {
|
||||
stubClient();
|
||||
const client = mocked<MatrixClient>(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<MatrixRTCSession>;
|
||||
|
||||
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<MatrixClient>;
|
||||
let room: Room;
|
||||
let alice: RoomMember;
|
||||
|
||||
let roomSession: Mocked<MatrixRTCSession>;
|
||||
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 () => {
|
||||
|
||||
@@ -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 () => {
|
||||
|
||||
Reference in New Issue
Block a user