Skip to content
This repository has been archived by the owner on Sep 11, 2024. It is now read-only.

Add Element Call participant limit #9358

Merged
merged 11 commits into from
Oct 7, 2022
1 change: 1 addition & 0 deletions src/IConfigOptions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,7 @@ export interface IConfigOptions {
element_call: {
url: string;
use_exclusively: boolean;
participant_limit?: number;
};

logout_redirect_url?: string;
Expand Down
1 change: 1 addition & 0 deletions src/SdkConfig.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ export const DEFAULTS: IConfigOptions = {
element_call: {
url: "https://call.element.io",
use_exclusively: false,
participant_limit: 8,
},

// @ts-ignore - we deliberately use the camelCase version here so we trigger
Expand Down
34 changes: 26 additions & 8 deletions src/components/views/voip/CallView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -35,10 +35,11 @@ import IconizedContextMenu, {
} from "../context_menus/IconizedContextMenu";
import { aboveLeftOf, ContextMenuButton, useContextMenu } from "../../structures/ContextMenu";
import { Alignment } from "../elements/Tooltip";
import AccessibleButton, { ButtonEvent } from "../elements/AccessibleButton";
import { ButtonEvent } from "../elements/AccessibleButton";
import AccessibleTooltipButton from "../elements/AccessibleTooltipButton";
import FacePile from "../elements/FacePile";
import MemberAvatar from "../avatars/MemberAvatar";
import SdkConfig from "../../../SdkConfig";

interface DeviceButtonProps {
kind: string;
Expand Down Expand Up @@ -110,10 +111,11 @@ const MAX_FACES = 8;
interface LobbyProps {
room: Room;
connect: () => Promise<void>;
callIsFull?: boolean;
children?: ReactNode;
}

export const Lobby: FC<LobbyProps> = ({ room, connect, children }) => {
export const Lobby: FC<LobbyProps> = ({ room, callIsFull, connect, children }) => {
const [connecting, setConnecting] = useState(false);
const me = useMemo(() => room.getMember(room.myUserId)!, [room]);
const videoRef = useRef<HTMLVideoElement>(null);
Expand Down Expand Up @@ -197,6 +199,11 @@ export const Lobby: FC<LobbyProps> = ({ room, connect, children }) => {
}
}, [connect, setConnecting]);

const tooltip = useMemo(() => {
if (connecting) return _t("Connecting");
if (callIsFull) return _t("Sorry - this call is currently full");
SimonBrandner marked this conversation as resolved.
Show resolved Hide resolved
}, [connecting, callIsFull]);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What's the rationale for useMemo here, and on line 342? These don't seem like expensive operations.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there a reason not to use it?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As the memo all the things argument goes, not really. And in the future React is supposed to move to automatic memoization of everything, anyways


return <div className="mx_CallView_lobby">
{ children }
<div className="mx_CallView_preview">
Expand Down Expand Up @@ -233,14 +240,15 @@ export const Lobby: FC<LobbyProps> = ({ room, connect, children }) => {
/>
</div>
</div>
<AccessibleButton
<AccessibleTooltipButton
className="mx_CallView_connectButton"
kind="primary"
disabled={connecting}
disabled={connecting || callIsFull}
onClick={onConnectClick}
>
{ _t("Join") }
</AccessibleButton>
title={_t("Join")}
label={_t("Join")}
tooltip={tooltip}
/>
</div>;
};

Expand Down Expand Up @@ -331,6 +339,10 @@ const JoinCallView: FC<JoinCallViewProps> = ({ room, resizing, call }) => {
// We'll take this opportunity to tidy up our room state
useEffect(() => { call.clean(); }, [call]);

const callIsFull = useMemo(() => {
return participants.size >= SdkConfig.get("element_call").participant_limit;
}, [participants]);

let lobby: JSX.Element | null = null;
if (!connected) {
let facePile: JSX.Element | null = null;
Expand All @@ -344,7 +356,13 @@ const JoinCallView: FC<JoinCallViewProps> = ({ room, resizing, call }) => {
</div>;
}

lobby = <Lobby room={room} connect={connect}>{ facePile }</Lobby>;
lobby = <Lobby
room={room}
connect={connect}
callIsFull={callIsFull}
>
{ facePile }
</Lobby>;
}

return <div className="mx_CallView">
Expand Down
3 changes: 2 additions & 1 deletion src/i18n/strings/en_EN.json
Original file line number Diff line number Diff line change
Expand Up @@ -1045,6 +1045,8 @@
"You can use <code>/help</code> to list available commands. Did you mean to send this as a message?": "You can use <code>/help</code> to list available commands. Did you mean to send this as a message?",
"Hint: Begin your message with <code>//</code> to start it with a slash.": "Hint: Begin your message with <code>//</code> to start it with a slash.",
"Send as message": "Send as message",
"Connecting": "Connecting",
"Sorry - this call is currently full": "Sorry - this call is currently full",
"Audio devices": "Audio devices",
"Mute microphone": "Mute microphone",
"Unmute microphone": "Unmute microphone",
Expand All @@ -1064,7 +1066,6 @@
"You held the call <a>Switch</a>": "You held the call <a>Switch</a>",
"You held the call <a>Resume</a>": "You held the call <a>Resume</a>",
"%(peerName)s held the call": "%(peerName)s held the call",
"Connecting": "Connecting",
"Dialpad": "Dialpad",
"Mute the microphone": "Mute the microphone",
"Unmute the microphone": "Unmute the microphone",
Expand Down
22 changes: 22 additions & 0 deletions test/components/views/voip/CallView-test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import { MatrixClient, PendingEventOrdering } from "matrix-js-sdk/src/client";
import { Room } from "matrix-js-sdk/src/models/room";
import { RoomStateEvent } from "matrix-js-sdk/src/models/room-state";
import { Widget } from "matrix-widget-api";
import "@testing-library/jest-dom";

import type { RoomMember } from "matrix-js-sdk/src/models/room-member";
import type { ClientWidgetApi } from "matrix-widget-api";
Expand All @@ -38,6 +39,7 @@ import { CallView as _CallView } from "../../../../src/components/views/voip/Cal
import { WidgetMessagingStore } from "../../../../src/stores/widgets/WidgetMessagingStore";
import { CallStore } from "../../../../src/stores/CallStore";
import { Call, ConnectionState } from "../../../../src/models/Call";
import SdkConfig from "../../../../src/SdkConfig";

const CallView = wrapInMatrixClientContext(_CallView);

Expand Down Expand Up @@ -163,6 +165,26 @@ describe("CallLobby", () => {
fireEvent.click(screen.getByRole("button", { name: "Join" }));
await waitFor(() => expect(connectSpy).toHaveBeenCalled(), { interval: 1 });
});

it("disables join button when the participant limit has been exceeded", async () => {
const sdkConfigCopy = SdkConfig.get();
const bob = mkRoomMember(room.roomId, "@bob:example.org");
const carol = mkRoomMember(room.roomId, "@carol:example.org");

jest.spyOn(SdkConfig, "get").mockImplementation((name: string) => {
if (name === "element_call") return { participant_limit: 2 };
if (Boolean(name)) return sdkConfigCopy[name];
return sdkConfigCopy;
});
SimonBrandner marked this conversation as resolved.
Show resolved Hide resolved
call.participants = new Set([bob, carol]);

await renderView();
const connectSpy = jest.spyOn(call, "connect");
const joinButton = screen.getByRole("button", { name: "Join" });
expect(joinButton).toHaveAttribute("aria-disabled", "true");
fireEvent.click(joinButton);
await waitFor(() => expect(connectSpy).not.toHaveBeenCalled(), { interval: 1 });
});
});

describe("without an existing call", () => {
Expand Down