Add Voice and Video call in room header (#11444)

* Add Voice and Video call in room header

* Add thread icon in room header

* Add room notification icon to room header

* Fix linting

* Add tests for buttons in room header

* Add JSDoc

* micro optimisations

* Fix call disabled when hanging up

* Fix disabled state change on members count update

* Exclude functional members from members count optionally

* i18n
This commit is contained in:
Germain 2023-08-23 15:13:40 +01:00 committed by GitHub
parent c2e814ce95
commit 3acc9059ab
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
13 changed files with 709 additions and 48 deletions

View file

@ -15,9 +15,10 @@ limitations under the License.
*/
import React from "react";
import { render } from "@testing-library/react";
import { Room, EventType, MatrixEvent, PendingEventOrdering } from "matrix-js-sdk/src/matrix";
import { getAllByTitle, getByText, getByTitle, render, screen } from "@testing-library/react";
import { Room, EventType, MatrixEvent, PendingEventOrdering, MatrixCall } from "matrix-js-sdk/src/matrix";
import userEvent from "@testing-library/user-event";
import { CallType } from "matrix-js-sdk/src/webrtc/call";
import { stubClient } from "../../../test-utils";
import RoomHeader from "../../../../src/components/views/rooms/RoomHeader";
@ -25,6 +26,12 @@ import DMRoomMap from "../../../../src/utils/DMRoomMap";
import { MatrixClientPeg } from "../../../../src/MatrixClientPeg";
import RightPanelStore from "../../../../src/stores/right-panel/RightPanelStore";
import { RightPanelPhases } from "../../../../src/stores/right-panel/RightPanelStorePhases";
import LegacyCallHandler from "../../../../src/LegacyCallHandler";
import SettingsStore from "../../../../src/settings/SettingsStore";
import SdkConfig from "../../../../src/SdkConfig";
import dispatcher from "../../../../src/dispatcher/dispatcher";
import { CallStore } from "../../../../src/stores/CallStore";
import { Call, ElementCall } from "../../../../src/models/Call";
describe("Roomeader", () => {
let room: Room;
@ -45,6 +52,10 @@ describe("Roomeader", () => {
setCardSpy = jest.spyOn(RightPanelStore.instance, "setCard");
});
afterEach(() => {
jest.resetAllMocks();
});
it("renders the room header", () => {
const { container } = render(<RoomHeader room={room} />);
expect(container).toHaveTextContent(ROOM_ID);
@ -71,7 +82,219 @@ describe("Roomeader", () => {
it("opens the room summary", async () => {
const { container } = render(<RoomHeader room={room} />);
await userEvent.click(container.firstChild! as Element);
await userEvent.click(getByText(container, ROOM_ID));
expect(setCardSpy).toHaveBeenCalledWith({ phase: RightPanelPhases.RoomSummary });
});
it("opens the thread panel", async () => {
const { container } = render(<RoomHeader room={room} />);
await userEvent.click(getByTitle(container, "Threads"));
expect(setCardSpy).toHaveBeenCalledWith({ phase: RightPanelPhases.ThreadPanel });
});
it("opens the notifications panel", async () => {
const { container } = render(<RoomHeader room={room} />);
await userEvent.click(getByTitle(container, "Notifications"));
expect(setCardSpy).toHaveBeenCalledWith({ phase: RightPanelPhases.NotificationPanel });
});
describe("groups call disabled", () => {
it("you can't call if you're alone", () => {
mockRoomMembers(room, 1);
const { container } = render(<RoomHeader room={room} />);
for (const button of getAllByTitle(container, "There's no one here to call")) {
expect(button).toBeDisabled();
}
});
it("you can call when you're two in the room", async () => {
mockRoomMembers(room, 2);
const { container } = render(<RoomHeader room={room} />);
const voiceButton = getByTitle(container, "Voice call");
const videoButton = getByTitle(container, "Video call");
expect(voiceButton).not.toBeDisabled();
expect(videoButton).not.toBeDisabled();
const placeCallSpy = jest.spyOn(LegacyCallHandler.instance, "placeCall");
await userEvent.click(voiceButton);
expect(placeCallSpy).toHaveBeenLastCalledWith(room.roomId, CallType.Voice);
await userEvent.click(videoButton);
expect(placeCallSpy).toHaveBeenLastCalledWith(room.roomId, CallType.Video);
});
it("you can't call if there's already a call", () => {
mockRoomMembers(room, 2);
jest.spyOn(LegacyCallHandler.instance, "getCallForRoom").mockReturnValue(
// The JS-SDK does not export the class `MatrixCall` only the type
{} as MatrixCall,
);
const { container } = render(<RoomHeader room={room} />);
for (const button of getAllByTitle(container, "Ongoing call")) {
expect(button).toBeDisabled();
}
});
it("can calls in large rooms if able to edit widgets", () => {
mockRoomMembers(room, 10);
jest.spyOn(room.currentState, "mayClientSendStateEvent").mockReturnValue(true);
const { container } = render(<RoomHeader room={room} />);
expect(getByTitle(container, "Voice call")).not.toBeDisabled();
expect(getByTitle(container, "Video call")).not.toBeDisabled();
});
it("disable calls in large rooms by default", () => {
mockRoomMembers(room, 10);
jest.spyOn(room.currentState, "mayClientSendStateEvent").mockReturnValue(false);
const { container } = render(<RoomHeader room={room} />);
expect(getByTitle(container, "You do not have permission to start voice calls")).toBeDisabled();
expect(getByTitle(container, "You do not have permission to start video calls")).toBeDisabled();
});
});
describe("group call enabled", () => {
beforeEach(() => {
jest.spyOn(SettingsStore, "getValue").mockImplementation((feature) => feature === "feature_group_calls");
});
it("renders only the video call element", async () => {
jest.spyOn(SdkConfig, "get").mockReturnValue({ use_exclusively: true });
// allow element calls
jest.spyOn(room.currentState, "mayClientSendStateEvent").mockReturnValue(true);
const { container } = render(<RoomHeader room={room} />);
expect(screen.queryByTitle("Voice call")).toBeNull();
const videoCallButton = getByTitle(container, "Video call");
expect(videoCallButton).not.toBeDisabled();
const dispatcherSpy = jest.spyOn(dispatcher, "dispatch");
await userEvent.click(getByTitle(container, "Video call"));
expect(dispatcherSpy).toHaveBeenCalledWith(expect.objectContaining({ view_call: true }));
});
it("can call if there's an ongoing call", () => {
jest.spyOn(SdkConfig, "get").mockReturnValue({ use_exclusively: true });
// allow element calls
jest.spyOn(room.currentState, "mayClientSendStateEvent").mockReturnValue(true);
jest.spyOn(CallStore.instance, "getCall").mockReturnValue({} as Call);
const { container } = render(<RoomHeader room={room} />);
expect(getByTitle(container, "Ongoing call")).toBeDisabled();
});
it("disables calling if there's a jitsi call", () => {
mockRoomMembers(room, 2);
jest.spyOn(LegacyCallHandler.instance, "getCallForRoom").mockReturnValue(
// The JS-SDK does not export the class `MatrixCall` only the type
{} as MatrixCall,
);
const { container } = render(<RoomHeader room={room} />);
for (const button of getAllByTitle(container, "Ongoing call")) {
expect(button).toBeDisabled();
}
});
it("can't call if you have no friends", () => {
mockRoomMembers(room, 1);
const { container } = render(<RoomHeader room={room} />);
for (const button of getAllByTitle(container, "There's no one here to call")) {
expect(button).toBeDisabled();
}
});
it("calls using legacy or jitsi", async () => {
mockRoomMembers(room, 2);
const { container } = render(<RoomHeader room={room} />);
const voiceButton = getByTitle(container, "Voice call");
const videoButton = getByTitle(container, "Video call");
expect(voiceButton).not.toBeDisabled();
expect(videoButton).not.toBeDisabled();
const placeCallSpy = jest.spyOn(LegacyCallHandler.instance, "placeCall");
await userEvent.click(voiceButton);
expect(placeCallSpy).toHaveBeenLastCalledWith(room.roomId, CallType.Voice);
await userEvent.click(videoButton);
expect(placeCallSpy).toHaveBeenLastCalledWith(room.roomId, CallType.Video);
});
it("calls using legacy or jitsi for large rooms", async () => {
mockRoomMembers(room, 3);
jest.spyOn(room.currentState, "mayClientSendStateEvent").mockImplementation((key) => {
if (key === "im.vector.modular.widgets") return true;
return false;
});
const { container } = render(<RoomHeader room={room} />);
const voiceButton = getByTitle(container, "Voice call");
const videoButton = getByTitle(container, "Video call");
expect(voiceButton).not.toBeDisabled();
expect(videoButton).not.toBeDisabled();
const placeCallSpy = jest.spyOn(LegacyCallHandler.instance, "placeCall");
await userEvent.click(voiceButton);
expect(placeCallSpy).toHaveBeenLastCalledWith(room.roomId, CallType.Voice);
await userEvent.click(videoButton);
expect(placeCallSpy).toHaveBeenLastCalledWith(room.roomId, CallType.Video);
});
it("calls using element calls for large rooms", async () => {
mockRoomMembers(room, 3);
jest.spyOn(room.currentState, "mayClientSendStateEvent").mockImplementation((key) => {
if (key === "im.vector.modular.widgets") return true;
if (key === ElementCall.CALL_EVENT_TYPE.name) return true;
return false;
});
const { container } = render(<RoomHeader room={room} />);
const voiceButton = getByTitle(container, "Voice call");
const videoButton = getByTitle(container, "Video call");
expect(voiceButton).not.toBeDisabled();
expect(videoButton).not.toBeDisabled();
const placeCallSpy = jest.spyOn(LegacyCallHandler.instance, "placeCall");
await userEvent.click(voiceButton);
expect(placeCallSpy).toHaveBeenLastCalledWith(room.roomId, CallType.Voice);
const dispatcherSpy = jest.spyOn(dispatcher, "dispatch");
await userEvent.click(videoButton);
expect(dispatcherSpy).toHaveBeenCalledWith(expect.objectContaining({ view_call: true }));
});
});
});
/**
*
* @param count the number of users to create
*/
function mockRoomMembers(room: Room, count: number) {
const members = Array(count)
.fill(0)
.map((_, index) => ({
userId: `@user-${index}:example.org`,
name: `Member ${index}`,
rawDisplayName: `Member ${index}`,
roomId: room.roomId,
membership: "join",
getAvatarUrl: () => `mxc://avatar.url/user-${index}.png`,
getMxcAvatarUrl: () => `mxc://avatar.url/user-${index}.png`,
}));
room.currentState.setJoinedMemberCount(members.length);
room.getJoinedMembers = jest.fn().mockReturnValue(members);
}