mirror of
https://github.com/element-hq/element-web.git
synced 2025-09-17 11:04:05 +02:00
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 35569f35bb.
* 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 { FontWatcher } from "../settings/watchers/FontWatcher";
|
||||||
import { type JitsiCallMemberContent, JitsiCallMemberEventType } from "../call-types";
|
import { type JitsiCallMemberContent, JitsiCallMemberEventType } from "../call-types";
|
||||||
import SdkConfig from "../SdkConfig.ts";
|
import SdkConfig from "../SdkConfig.ts";
|
||||||
import RoomListStore from "../stores/room-list/RoomListStore.ts";
|
import DMRoomMap from "../utils/DMRoomMap.ts";
|
||||||
import { DefaultTagID } from "../stores/room-list/models.ts";
|
|
||||||
|
|
||||||
const TIMEOUT_MS = 16000;
|
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.
|
* A group call using MSC3401 and Element Call as a backend.
|
||||||
* (somewhat cheekily named)
|
* (somewhat cheekily named)
|
||||||
@@ -586,10 +592,24 @@ export class ElementCall extends Call {
|
|||||||
|
|
||||||
const room = client.getRoom(roomId);
|
const room = client.getRoom(roomId);
|
||||||
if (room !== null && !isVideoRoom(room)) {
|
if (room !== null && !isVideoRoom(room)) {
|
||||||
params.append(
|
const isDM = !!DMRoomMap.shared().getUserIdForRoomId(room.roomId);
|
||||||
"sendNotificationType",
|
const oldestCallMember = client.matrixRTC.getRoomSession(room).getOldestMembership();
|
||||||
RoomListStore.instance.getTagsForRoom(room).includes(DefaultTagID.DM) ? "ring" : "notification",
|
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");
|
const rageshakeSubmitUrl = SdkConfig.get("bug_report_endpoint_url");
|
||||||
|
|||||||
@@ -39,8 +39,16 @@ import {
|
|||||||
ConnectionState,
|
ConnectionState,
|
||||||
JitsiCall,
|
JitsiCall,
|
||||||
ElementCall,
|
ElementCall,
|
||||||
|
ElementCallIntent,
|
||||||
} from "../../../src/models/Call";
|
} 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 { MatrixClientPeg } from "../../../src/MatrixClientPeg";
|
||||||
import WidgetStore from "../../../src/stores/WidgetStore";
|
import WidgetStore from "../../../src/stores/WidgetStore";
|
||||||
import { WidgetMessagingStore } from "../../../src/stores/widgets/WidgetMessagingStore";
|
import { WidgetMessagingStore } from "../../../src/stores/widgets/WidgetMessagingStore";
|
||||||
@@ -50,8 +58,6 @@ import SettingsStore from "../../../src/settings/SettingsStore";
|
|||||||
import { Anonymity, PosthogAnalytics } from "../../../src/PosthogAnalytics";
|
import { Anonymity, PosthogAnalytics } from "../../../src/PosthogAnalytics";
|
||||||
import { type SettingKey } from "../../../src/settings/Settings.tsx";
|
import { type SettingKey } from "../../../src/settings/Settings.tsx";
|
||||||
import SdkConfig from "../../../src/SdkConfig.ts";
|
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";
|
import DMRoomMap from "../../../src/utils/DMRoomMap.ts";
|
||||||
|
|
||||||
const enabledSettings = new Set(["feature_group_calls", "feature_video_rooms", "feature_element_call_video_rooms"]);
|
const enabledSettings = new Set(["feature_group_calls", "feature_video_rooms", "feature_element_call_video_rooms"]);
|
||||||
@@ -65,6 +71,7 @@ const setUpClientRoomAndStores = (): {
|
|||||||
alice: RoomMember;
|
alice: RoomMember;
|
||||||
bob: RoomMember;
|
bob: RoomMember;
|
||||||
carol: RoomMember;
|
carol: RoomMember;
|
||||||
|
roomSession: Mocked<MatrixRTCSession>;
|
||||||
} => {
|
} => {
|
||||||
stubClient();
|
stubClient();
|
||||||
const client = mocked<MatrixClient>(MatrixClientPeg.safeGet());
|
const client = mocked<MatrixClient>(MatrixClientPeg.safeGet());
|
||||||
@@ -93,12 +100,13 @@ const setUpClientRoomAndStores = (): {
|
|||||||
jest.spyOn(room, "getMyMembership").mockReturnValue(KnownMembership.Join);
|
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.getRoom.mockImplementation((roomId) => (roomId === room.roomId ? room : null));
|
|
||||||
client.matrixRTC.getRoomSession.mockImplementation((roomId) => {
|
const roomSession = new MockEventEmitter({
|
||||||
const session = new EventEmitter() as MatrixRTCSession;
|
memberships: [],
|
||||||
session.memberships = [];
|
getOldestMembership: jest.fn().mockReturnValue(undefined),
|
||||||
return session;
|
}) as Mocked<MatrixRTCSession>;
|
||||||
});
|
|
||||||
|
client.matrixRTC.getRoomSession.mockReturnValue(roomSession);
|
||||||
client.getRooms.mockReturnValue([room]);
|
client.getRooms.mockReturnValue([room]);
|
||||||
client.getUserId.mockReturnValue(alice.userId);
|
client.getUserId.mockReturnValue(alice.userId);
|
||||||
client.getDeviceId.mockReturnValue("alices_device");
|
client.getDeviceId.mockReturnValue("alices_device");
|
||||||
@@ -120,7 +128,7 @@ const setUpClientRoomAndStores = (): {
|
|||||||
setupAsyncStoreWithClient(WidgetStore.instance, client);
|
setupAsyncStoreWithClient(WidgetStore.instance, client);
|
||||||
setupAsyncStoreWithClient(WidgetMessagingStore.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) => {
|
const cleanUpClientRoomAndStores = (client: MatrixClient, room: Room) => {
|
||||||
@@ -553,14 +561,14 @@ describe("ElementCall", () => {
|
|||||||
let client: Mocked<MatrixClient>;
|
let client: Mocked<MatrixClient>;
|
||||||
let room: Room;
|
let room: Room;
|
||||||
let alice: RoomMember;
|
let alice: RoomMember;
|
||||||
|
let roomSession: Mocked<MatrixRTCSession>;
|
||||||
function setRoomMembers(memberIds: string[]) {
|
function setRoomMembers(memberIds: string[]) {
|
||||||
jest.spyOn(room, "getJoinedMembers").mockReturnValue(memberIds.map((id) => ({ userId: id }) as RoomMember));
|
jest.spyOn(room, "getJoinedMembers").mockReturnValue(memberIds.map((id) => ({ userId: id }) as RoomMember));
|
||||||
}
|
}
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
jest.useFakeTimers();
|
jest.useFakeTimers();
|
||||||
({ client, room, alice } = setUpClientRoomAndStores());
|
({ client, room, alice, roomSession } = setUpClientRoomAndStores());
|
||||||
SdkConfig.reset();
|
SdkConfig.reset();
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -571,7 +579,16 @@ describe("ElementCall", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe("get", () => {
|
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", () => {
|
it("finds no calls", () => {
|
||||||
expect(Call.get(room)).toBeNull();
|
expect(Call.get(room)).toBeNull();
|
||||||
@@ -600,11 +617,7 @@ describe("ElementCall", () => {
|
|||||||
|
|
||||||
it("finds ongoing calls that are created by the session manager", async () => {
|
it("finds ongoing calls that are created by the session manager", async () => {
|
||||||
// There is an existing session created by another user in this room.
|
// There is an existing session created by another user in this room.
|
||||||
client.matrixRTC.getRoomSession.mockReturnValue({
|
roomSession.memberships.push({} as CallMembership);
|
||||||
on: (ev: any, fn: any) => {},
|
|
||||||
off: (ev: any, fn: any) => {},
|
|
||||||
memberships: [{ fakeVal: "fake membership" }],
|
|
||||||
} as unknown as MatrixRTCSession);
|
|
||||||
const call = Call.get(room);
|
const call = Call.get(room);
|
||||||
if (!(call instanceof ElementCall)) throw new Error("Failed to create call");
|
if (!(call instanceof ElementCall)) throw new Error("Failed to create call");
|
||||||
});
|
});
|
||||||
@@ -750,19 +763,50 @@ describe("ElementCall", () => {
|
|||||||
expect(urlParams.get("analyticsID")).toBeFalsy();
|
expect(urlParams.get("analyticsID")).toBeFalsy();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("requests ringing notifications in DMs", async () => {
|
it("requests ringing notifications and correct intent in DMs", async () => {
|
||||||
const tagsSpy = jest.spyOn(RoomListStore.instance, "getTagsForRoom");
|
getUserIdForRoomIdSpy.mockImplementation((roomId: string) =>
|
||||||
try {
|
room.roomId === roomId ? "any-user" : undefined,
|
||||||
tagsSpy.mockReturnValue([DefaultTagID.DM]);
|
);
|
||||||
ElementCall.create(room);
|
ElementCall.create(room);
|
||||||
const call = Call.get(room);
|
const call = Call.get(room);
|
||||||
if (!(call instanceof ElementCall)) throw new Error("Failed to create call");
|
if (!(call instanceof ElementCall)) throw new Error("Failed to create call");
|
||||||
|
|
||||||
const urlParams = new URLSearchParams(new URL(call.widget.url).hash.slice(1));
|
const urlParams = new URLSearchParams(new URL(call.widget.url).hash.slice(1));
|
||||||
expect(urlParams.get("sendNotificationType")).toBe("ring");
|
expect(urlParams.get("sendNotificationType")).toBe("ring");
|
||||||
} finally {
|
expect(urlParams.get("intent")).toBe(ElementCallIntent.StartCallDM);
|
||||||
tagsSpy.mockRestore();
|
});
|
||||||
}
|
|
||||||
|
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 () => {
|
it("requests visual notifications in non-DMs", async () => {
|
||||||
|
|||||||
@@ -44,6 +44,7 @@ import { CallStore } from "../../../src/stores/CallStore";
|
|||||||
import { MatrixClientPeg } from "../../../src/MatrixClientPeg";
|
import { MatrixClientPeg } from "../../../src/MatrixClientPeg";
|
||||||
import MediaDeviceHandler, { MediaDeviceKindEnum } from "../../../src/MediaDeviceHandler";
|
import MediaDeviceHandler, { MediaDeviceKindEnum } from "../../../src/MediaDeviceHandler";
|
||||||
import { storeRoomAliasInCache } from "../../../src/RoomAliasCache.ts";
|
import { storeRoomAliasInCache } from "../../../src/RoomAliasCache.ts";
|
||||||
|
import { type Call } from "../../../src/models/Call.ts";
|
||||||
|
|
||||||
jest.mock("../../../src/Modal");
|
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 () => {
|
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 setupAsyncStoreWithClient(CallStore.instance, MatrixClientPeg.safeGet());
|
||||||
await viewCall();
|
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 () => {
|
it("should display an error message when the room is unreachable via the roomId", async () => {
|
||||||
|
|||||||
Reference in New Issue
Block a user