Support using Element Call for voice calls in DMs (#30817)

* Add voiceOnly options.

* tweaks

* Nearly working demo

* Lots of minor fixes

* Better working version

* remove unused payload

* bits and pieces

* Cleanup based on new hints

* Simple refactor for skipLobby (and remove returnToLobby)

* Tidyup

* Remove unused tests

* Update tests for voice calls

* Add video room support.

* Add a test for video rooms

* tidy

* remove console log line

* lint and tests

* Bunch of fixes

* Fixes

* Use correct title

* make linter happier

* Update tests

* cleanup

* Drop only

* update snaps

* Document

* lint

* Update snapshots

* Remove duplicate test

* add brackets

* fix jest
This commit is contained in:
Will Hunt
2025-11-17 11:50:22 +00:00
committed by GitHub
parent 3d683ec5c6
commit f3a880f1c3
25 changed files with 365 additions and 112 deletions

View File

@@ -7,6 +7,7 @@
import React from "react";
import { render, screen } from "jest-matrix-react";
import { CallType } from "matrix-js-sdk/src/webrtc/call";
import { RoomNotificationState } from "../../../../../src/stores/notifications/RoomNotificationState";
import { NotificationDecoration } from "../../../../../src/components/views/rooms/NotificationDecoration";
@@ -22,7 +23,7 @@ describe("<NotificationDecoration />", () => {
it("should not render if RoomNotificationState.hasAnyNotificationOrActivity=true", () => {
jest.spyOn(roomNotificationState, "hasAnyNotificationOrActivity", "get").mockReturnValue(false);
render(<NotificationDecoration notificationState={roomNotificationState} hasVideoCall={false} />);
render(<NotificationDecoration notificationState={roomNotificationState} callType={undefined} />);
expect(screen.queryByTestId("notification-decoration")).toBeNull();
});
@@ -30,7 +31,7 @@ describe("<NotificationDecoration />", () => {
jest.spyOn(roomNotificationState, "hasAnyNotificationOrActivity", "get").mockReturnValue(true);
jest.spyOn(roomNotificationState, "isUnsentMessage", "get").mockReturnValue(true);
const { asFragment } = render(
<NotificationDecoration notificationState={roomNotificationState} hasVideoCall={false} />,
<NotificationDecoration notificationState={roomNotificationState} callType={undefined} />,
);
expect(asFragment()).toMatchSnapshot();
});
@@ -39,7 +40,7 @@ describe("<NotificationDecoration />", () => {
jest.spyOn(roomNotificationState, "hasAnyNotificationOrActivity", "get").mockReturnValue(true);
jest.spyOn(roomNotificationState, "invited", "get").mockReturnValue(true);
const { asFragment } = render(
<NotificationDecoration notificationState={roomNotificationState} hasVideoCall={false} />,
<NotificationDecoration notificationState={roomNotificationState} callType={undefined} />,
);
expect(asFragment()).toMatchSnapshot();
});
@@ -49,7 +50,7 @@ describe("<NotificationDecoration />", () => {
jest.spyOn(roomNotificationState, "isMention", "get").mockReturnValue(true);
jest.spyOn(roomNotificationState, "count", "get").mockReturnValue(1);
const { asFragment } = render(
<NotificationDecoration notificationState={roomNotificationState} hasVideoCall={false} />,
<NotificationDecoration notificationState={roomNotificationState} callType={undefined} />,
);
expect(asFragment()).toMatchSnapshot();
});
@@ -59,7 +60,7 @@ describe("<NotificationDecoration />", () => {
jest.spyOn(roomNotificationState, "isNotification", "get").mockReturnValue(true);
jest.spyOn(roomNotificationState, "count", "get").mockReturnValue(1);
const { asFragment } = render(
<NotificationDecoration notificationState={roomNotificationState} hasVideoCall={false} />,
<NotificationDecoration notificationState={roomNotificationState} callType={undefined} />,
);
expect(asFragment()).toMatchSnapshot();
});
@@ -69,7 +70,7 @@ describe("<NotificationDecoration />", () => {
jest.spyOn(roomNotificationState, "isNotification", "get").mockReturnValue(true);
jest.spyOn(roomNotificationState, "count", "get").mockReturnValue(0);
const { asFragment } = render(
<NotificationDecoration notificationState={roomNotificationState} hasVideoCall={false} />,
<NotificationDecoration notificationState={roomNotificationState} callType={undefined} />,
);
expect(asFragment()).toMatchSnapshot();
});
@@ -78,7 +79,7 @@ describe("<NotificationDecoration />", () => {
jest.spyOn(roomNotificationState, "hasAnyNotificationOrActivity", "get").mockReturnValue(true);
jest.spyOn(roomNotificationState, "isActivityNotification", "get").mockReturnValue(true);
const { asFragment } = render(
<NotificationDecoration notificationState={roomNotificationState} hasVideoCall={false} />,
<NotificationDecoration notificationState={roomNotificationState} callType={undefined} />,
);
expect(asFragment()).toMatchSnapshot();
});
@@ -87,14 +88,21 @@ describe("<NotificationDecoration />", () => {
jest.spyOn(roomNotificationState, "hasAnyNotificationOrActivity", "get").mockReturnValue(true);
jest.spyOn(roomNotificationState, "muted", "get").mockReturnValue(true);
const { asFragment } = render(
<NotificationDecoration notificationState={roomNotificationState} hasVideoCall={false} />,
<NotificationDecoration notificationState={roomNotificationState} callType={undefined} />,
);
expect(asFragment()).toMatchSnapshot();
});
it("should render the video decoration", () => {
it("should render the video call decoration", () => {
jest.spyOn(roomNotificationState, "hasAnyNotificationOrActivity", "get").mockReturnValue(false);
const { asFragment } = render(
<NotificationDecoration notificationState={roomNotificationState} hasVideoCall={true} />,
<NotificationDecoration notificationState={roomNotificationState} callType={CallType.Video} />,
);
expect(asFragment()).toMatchSnapshot();
});
it("should render the audio call decoration", () => {
jest.spyOn(roomNotificationState, "hasAnyNotificationOrActivity", "get").mockReturnValue(false);
const { asFragment } = render(
<NotificationDecoration notificationState={roomNotificationState} callType={CallType.Voice} />,
);
expect(asFragment()).toMatchSnapshot();
});

View File

@@ -10,6 +10,7 @@ import { type MatrixClient, type Room } from "matrix-js-sdk/src/matrix";
import { render, screen, waitFor } from "jest-matrix-react";
import userEvent from "@testing-library/user-event";
import { mocked } from "jest-mock";
import { CallType } from "matrix-js-sdk/src/webrtc/call";
import { mkRoom, stubClient, withClientContextRenderOptions } from "../../../../../test-utils";
import { RoomListItemView } from "../../../../../../src/components/views/rooms/RoomListPanel/RoomListItemView";
@@ -64,6 +65,7 @@ describe("<RoomListItemView />", () => {
isBold: false,
isVideoRoom: false,
callConnectionState: null,
callType: CallType.Video,
hasParticipantInCall: false,
name: room.name,
showNotificationDecoration: false,

View File

@@ -103,6 +103,17 @@ exports[`<RoomListItemView /> should display notification decoration 1`] = `
data-testid="notification-decoration"
style="--mx-flex-display: flex; --mx-flex-direction: row; --mx-flex-align: center; --mx-flex-justify: center; --mx-flex-gap: var(--cpd-space-1x); --mx-flex-wrap: nowrap;"
>
<svg
fill="var(--cpd-color-icon-accent-primary)"
height="20px"
viewBox="0 0 24 24"
width="20px"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M6 4h10a2 2 0 0 1 2 2v4.286l3.35-2.871a1 1 0 0 1 1.65.76v7.65a1 1 0 0 1-1.65.76L18 13.715V18a2 2 0 0 1-2 2H6a4 4 0 0 1-4-4V8a4 4 0 0 1 4-4"
/>
</svg>
<span
class="_unread-counter_9mg0k_8"
>

View File

@@ -16,6 +16,28 @@ exports[`<NotificationDecoration /> should render the activity decoration 1`] =
</DocumentFragment>
`;
exports[`<NotificationDecoration /> should render the audio call decoration 1`] = `
<DocumentFragment>
<div
class="_flex_4dswl_9"
data-testid="notification-decoration"
style="--mx-flex-display: flex; --mx-flex-direction: row; --mx-flex-align: center; --mx-flex-justify: center; --mx-flex-gap: var(--cpd-space-1x); --mx-flex-wrap: nowrap;"
>
<svg
fill="var(--cpd-color-icon-accent-primary)"
height="20px"
viewBox="0 0 24 24"
width="20px"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="m20.958 16.374.039 3.527q0 .427-.33.756-.33.33-.756.33a16 16 0 0 1-6.57-1.105 16.2 16.2 0 0 1-5.563-3.663 16.1 16.1 0 0 1-3.653-5.573 16.3 16.3 0 0 1-1.115-6.56q0-.427.33-.757T4.095 3l3.528.039a1.07 1.07 0 0 1 1.085.93l.543 3.954q.039.271-.039.504a1.1 1.1 0 0 1-.271.426l-1.64 1.64q.505 1.008 1.154 1.909c.433.6 1.444 1.696 1.444 1.696s1.095 1.01 1.696 1.444q.9.65 1.909 1.153l1.64-1.64q.193-.193.426-.27t.504-.04l3.954.543q.406.059.668.359t.262.727"
/>
</svg>
</div>
</DocumentFragment>
`;
exports[`<NotificationDecoration /> should render the invitation decoration 1`] = `
<DocumentFragment>
<div
@@ -142,7 +164,7 @@ exports[`<NotificationDecoration /> should render the unset message decoration 1
</DocumentFragment>
`;
exports[`<NotificationDecoration /> should render the video decoration 1`] = `
exports[`<NotificationDecoration /> should render the video call decoration 1`] = `
<DocumentFragment>
<div
class="_flex_4dswl_9"

View File

@@ -32,6 +32,7 @@ import { MatrixClientPeg } from "../../../../../src/MatrixClientPeg";
import { CallView as _CallView } from "../../../../../src/components/views/voip/CallView";
import { WidgetMessagingStore } from "../../../../../src/stores/widgets/WidgetMessagingStore";
import { CallStore } from "../../../../../src/stores/CallStore";
import DMRoomMap from "../../../../../src/utils/DMRoomMap";
const CallView = wrapInMatrixClientContext(_CallView);
@@ -50,6 +51,7 @@ describe("CallView", () => {
stubClient();
client = mocked(MatrixClientPeg.safeGet());
DMRoomMap.makeShared(client);
room = new Room("!1:example.org", client, "@alice:example.org", {
pendingEventOrdering: PendingEventOrdering.Detached,