mirror of
https://github.com/vector-im/element-web.git
synced 2024-11-15 12:45:11 +08:00
Add Element Call room settings (#9347)
Co-authored-by: Robin <robin@robin.town>
This commit is contained in:
parent
4ff9681408
commit
26a74a193f
@ -21,6 +21,10 @@ limitations under the License.
|
||||
mask-image: url('$(res)/img/element-icons/settings.svg');
|
||||
}
|
||||
|
||||
.mx_RoomSettingsDialog_voiceIcon::before {
|
||||
mask-image: url('$(res)/img/element-icons/call/voice-call.svg');
|
||||
}
|
||||
|
||||
.mx_RoomSettingsDialog_securityIcon::before {
|
||||
mask-image: url('$(res)/img/element-icons/security.svg');
|
||||
}
|
||||
|
@ -119,6 +119,7 @@ export interface IConfigOptions {
|
||||
element_call: {
|
||||
url: string;
|
||||
use_exclusively: boolean;
|
||||
brand: string;
|
||||
};
|
||||
|
||||
logout_redirect_url?: string;
|
||||
|
@ -33,6 +33,7 @@ export const DEFAULTS: IConfigOptions = {
|
||||
element_call: {
|
||||
url: "https://call.element.io",
|
||||
use_exclusively: false,
|
||||
brand: "Element Call",
|
||||
},
|
||||
|
||||
// @ts-ignore - we deliberately use the camelCase version here so we trigger
|
||||
|
@ -32,8 +32,10 @@ import SettingsStore from "../../../settings/SettingsStore";
|
||||
import { UIFeature } from "../../../settings/UIFeature";
|
||||
import BaseDialog from "./BaseDialog";
|
||||
import { Action } from '../../../dispatcher/actions';
|
||||
import { VoipRoomSettingsTab } from "../settings/tabs/room/VoipRoomSettingsTab";
|
||||
|
||||
export const ROOM_GENERAL_TAB = "ROOM_GENERAL_TAB";
|
||||
export const ROOM_VOIP_TAB = "ROOM_VOIP_TAB";
|
||||
export const ROOM_SECURITY_TAB = "ROOM_SECURITY_TAB";
|
||||
export const ROOM_ROLES_TAB = "ROOM_ROLES_TAB";
|
||||
export const ROOM_NOTIFICATIONS_TAB = "ROOM_NOTIFICATIONS_TAB";
|
||||
@ -96,6 +98,14 @@ export default class RoomSettingsDialog extends React.Component<IProps, IState>
|
||||
<GeneralRoomSettingsTab roomId={this.props.roomId} />,
|
||||
"RoomSettingsGeneral",
|
||||
));
|
||||
if (SettingsStore.getValue("feature_group_calls")) {
|
||||
tabs.push(new Tab(
|
||||
ROOM_VOIP_TAB,
|
||||
_td("Voice & Video"),
|
||||
"mx_RoomSettingsDialog_voiceIcon",
|
||||
<VoipRoomSettingsTab roomId={this.props.roomId} />,
|
||||
));
|
||||
}
|
||||
tabs.push(new Tab(
|
||||
ROOM_SECURITY_TAB,
|
||||
_td("Security & Privacy"),
|
||||
|
@ -21,7 +21,7 @@ import AccessibleButton from "./AccessibleButton";
|
||||
import Tooltip, { Alignment } from './Tooltip';
|
||||
|
||||
interface IProps extends React.ComponentProps<typeof AccessibleButton> {
|
||||
title: string;
|
||||
title?: string;
|
||||
tooltip?: React.ReactNode;
|
||||
label?: string;
|
||||
tooltipClassName?: string;
|
||||
@ -78,7 +78,7 @@ export default class AccessibleTooltipButton extends React.PureComponent<IProps,
|
||||
const { title, tooltip, children, tooltipClassName, forceHide, alignment, onHideTooltip,
|
||||
...props } = this.props;
|
||||
|
||||
const tip = this.state.hover && <Tooltip
|
||||
const tip = this.state.hover && (title || tooltip) && <Tooltip
|
||||
tooltipClassName={tooltipClassName}
|
||||
label={tooltip || title}
|
||||
alignment={alignment}
|
||||
@ -86,11 +86,11 @@ export default class AccessibleTooltipButton extends React.PureComponent<IProps,
|
||||
return (
|
||||
<AccessibleButton
|
||||
{...props}
|
||||
onMouseOver={this.showTooltip}
|
||||
onMouseLeave={this.hideTooltip}
|
||||
onFocus={this.onFocus}
|
||||
onBlur={this.hideTooltip}
|
||||
aria-label={title}
|
||||
onMouseOver={this.showTooltip || props.onMouseOver}
|
||||
onMouseLeave={this.hideTooltip || props.onMouseLeave}
|
||||
onFocus={this.onFocus || props.onFocus}
|
||||
onBlur={this.hideTooltip || props.onBlur}
|
||||
aria-label={title || props["aria-label"]}
|
||||
>
|
||||
{ children }
|
||||
{ this.props.label }
|
||||
|
@ -27,6 +27,8 @@ interface IProps {
|
||||
label: string;
|
||||
// The translated caption for the switch
|
||||
caption?: string;
|
||||
// Tooltip to display
|
||||
tooltip?: string;
|
||||
// Whether or not to disable the toggle switch
|
||||
disabled?: boolean;
|
||||
// True to put the toggle in front of the label
|
||||
@ -53,7 +55,8 @@ export default class LabelledToggleSwitch extends React.PureComponent<IProps> {
|
||||
checked={this.props.value}
|
||||
disabled={this.props.disabled}
|
||||
onChange={this.props.onChange}
|
||||
aria-label={this.props.label}
|
||||
title={this.props.label}
|
||||
tooltip={this.props.tooltip}
|
||||
/>;
|
||||
|
||||
if (this.props.toggleInFront) {
|
||||
@ -66,7 +69,7 @@ export default class LabelledToggleSwitch extends React.PureComponent<IProps> {
|
||||
"mx_SettingsFlag_toggleInFront": this.props.toggleInFront,
|
||||
});
|
||||
return (
|
||||
<div className={classes}>
|
||||
<div data-testid={this.props["data-testid"]} className={classes}>
|
||||
{ firstPart }
|
||||
{ secondPart }
|
||||
</div>
|
||||
|
@ -114,7 +114,7 @@ export default class SettingsFlag extends React.Component<IProps, IState> {
|
||||
checked={this.state.value}
|
||||
onChange={this.onChange}
|
||||
disabled={this.props.disabled || !canChange}
|
||||
aria-label={label}
|
||||
title={label}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
@ -18,21 +18,27 @@ limitations under the License.
|
||||
import React from "react";
|
||||
import classNames from "classnames";
|
||||
|
||||
import AccessibleButton from "./AccessibleButton";
|
||||
import AccessibleTooltipButton from "./AccessibleTooltipButton";
|
||||
|
||||
interface IProps {
|
||||
// Whether or not this toggle is in the 'on' position.
|
||||
checked: boolean;
|
||||
|
||||
// Title to use
|
||||
title?: string;
|
||||
|
||||
// Whether or not the user can interact with the switch
|
||||
disabled?: boolean;
|
||||
|
||||
// Tooltip to show
|
||||
tooltip?: string;
|
||||
|
||||
// Called when the checked state changes. First argument will be the new state.
|
||||
onChange(checked: boolean): void;
|
||||
}
|
||||
|
||||
// Controlled Toggle Switch element, written with Accessibility in mind
|
||||
export default ({ checked, disabled = false, onChange, ...props }: IProps) => {
|
||||
export default ({ checked, disabled = false, title, tooltip, onChange, ...props }: IProps) => {
|
||||
const _onClick = () => {
|
||||
if (disabled) return;
|
||||
onChange(!checked);
|
||||
@ -45,14 +51,16 @@ export default ({ checked, disabled = false, onChange, ...props }: IProps) => {
|
||||
});
|
||||
|
||||
return (
|
||||
<AccessibleButton {...props}
|
||||
<AccessibleTooltipButton {...props}
|
||||
className={classes}
|
||||
onClick={_onClick}
|
||||
role="switch"
|
||||
aria-checked={checked}
|
||||
aria-disabled={disabled}
|
||||
title={title}
|
||||
tooltip={tooltip}
|
||||
>
|
||||
<div className="mx_ToggleSwitch_ball" />
|
||||
</AccessibleButton>
|
||||
</AccessibleTooltipButton>
|
||||
);
|
||||
};
|
||||
|
@ -195,10 +195,11 @@ const VideoCallButton: FC<VideoCallButtonProps> = ({ room, busy, setBusy, behavi
|
||||
let menu: JSX.Element | null = null;
|
||||
if (menuOpen) {
|
||||
const buttonRect = buttonRef.current!.getBoundingClientRect();
|
||||
const brand = SdkConfig.get("element_call").brand;
|
||||
menu = <IconizedContextMenu {...aboveLeftOf(buttonRect)} onFinished={closeMenu}>
|
||||
<IconizedContextMenuOptionList>
|
||||
<IconizedContextMenuOption label={_t("Video call (Jitsi)")} onClick={onJitsiClick} />
|
||||
<IconizedContextMenuOption label={_t("Video call (Element Call)")} onClick={onElementClick} />
|
||||
<IconizedContextMenuOption label={_t("Video call (%(brand)s)", { brand })} onClick={onElementClick} />
|
||||
</IconizedContextMenuOptionList>
|
||||
</IconizedContextMenu>;
|
||||
}
|
||||
|
@ -153,7 +153,7 @@ const DeviceDetails: React.FC<Props> = ({
|
||||
checked={isPushNotificationsEnabled(pusher, localNotificationSettings)}
|
||||
disabled={isCheckboxDisabled(pusher, localNotificationSettings)}
|
||||
onChange={checked => setPushNotifications?.(device.device_id, checked)}
|
||||
aria-label={_t("Toggle push notifications on this session.")}
|
||||
title={_t("Toggle push notifications on this session.")}
|
||||
data-testid='device-detail-push-notification-checkbox'
|
||||
/>
|
||||
<p className='mx_DeviceDetails_sectionHeading'>
|
||||
|
@ -31,6 +31,8 @@ import PowerSelector from "../../../elements/PowerSelector";
|
||||
import SettingsFieldset from '../../SettingsFieldset';
|
||||
import SettingsStore from "../../../../../settings/SettingsStore";
|
||||
import { VoiceBroadcastInfoEventType } from '../../../../../voice-broadcast';
|
||||
import { ElementCall } from "../../../../../models/Call";
|
||||
import SdkConfig from "../../../../../SdkConfig";
|
||||
|
||||
interface IEventShowOpts {
|
||||
isState?: boolean;
|
||||
@ -60,6 +62,10 @@ const plEventsToShow: Record<string, IEventShowOpts> = {
|
||||
[EventType.Reaction]: { isState: false, hideForSpace: true },
|
||||
[EventType.RoomRedaction]: { isState: false, hideForSpace: true },
|
||||
|
||||
// MSC3401: Native Group VoIP signaling
|
||||
[ElementCall.CALL_EVENT_TYPE.name]: { isState: true, hideForSpace: true },
|
||||
[ElementCall.MEMBER_EVENT_TYPE.name]: { isState: true, hideForSpace: true },
|
||||
|
||||
// TODO: Enable support for m.widget event type (https://github.com/vector-im/element-web/issues/13111)
|
||||
"im.vector.modular.widgets": { isState: true, hideForSpace: true },
|
||||
[VoiceBroadcastInfoEventType]: { isState: true, hideForSpace: true },
|
||||
@ -252,6 +258,11 @@ export default class RolesRoomSettingsTab extends React.Component<IProps> {
|
||||
if (SettingsStore.getValue("feature_pinning")) {
|
||||
plEventsToLabels[EventType.RoomPinnedEvents] = _td("Manage pinned events");
|
||||
}
|
||||
// MSC3401: Native Group VoIP signaling
|
||||
if (SettingsStore.getValue("feature_group_calls")) {
|
||||
plEventsToLabels[ElementCall.CALL_EVENT_TYPE.name] = _td("Start %(brand)s calls");
|
||||
plEventsToLabels[ElementCall.MEMBER_EVENT_TYPE.name] = _td("Join %(brand)s calls");
|
||||
}
|
||||
|
||||
const powerLevelDescriptors: Record<string, IPowerLevelDescriptor> = {
|
||||
"users_default": {
|
||||
@ -435,7 +446,8 @@ export default class RolesRoomSettingsTab extends React.Component<IProps> {
|
||||
|
||||
let label = plEventsToLabels[eventType];
|
||||
if (label) {
|
||||
label = _t(label);
|
||||
const brand = SdkConfig.get("element_call").brand;
|
||||
label = _t(label, { brand });
|
||||
} else {
|
||||
label = _t("Send %(eventType)s events", { eventType });
|
||||
}
|
||||
|
@ -0,0 +1,99 @@
|
||||
/*
|
||||
Copyright 2022 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import React, { useCallback, useMemo, useState } from 'react';
|
||||
import { JoinRule } from "matrix-js-sdk/src/@types/partials";
|
||||
import { EventType } from "matrix-js-sdk/src/@types/event";
|
||||
|
||||
import { _t } from "../../../../../languageHandler";
|
||||
import { MatrixClientPeg } from "../../../../../MatrixClientPeg";
|
||||
import LabelledToggleSwitch from "../../../elements/LabelledToggleSwitch";
|
||||
import SettingsSubsection from "../../shared/SettingsSubsection";
|
||||
import SettingsTab from "../SettingsTab";
|
||||
import { ElementCall } from "../../../../../models/Call";
|
||||
import { useRoomState } from "../../../../../hooks/useRoomState";
|
||||
import SdkConfig from "../../../../../SdkConfig";
|
||||
|
||||
interface ElementCallSwitchProps {
|
||||
roomId: string;
|
||||
}
|
||||
|
||||
const ElementCallSwitch: React.FC<ElementCallSwitchProps> = ({ roomId }) => {
|
||||
const room = useMemo(() => MatrixClientPeg.get().getRoom(roomId), [roomId]);
|
||||
const isPublic = useMemo(() => room.getJoinRule() === JoinRule.Public, [room]);
|
||||
const [content, events, maySend] = useRoomState(room, useCallback((state) => {
|
||||
const content = state?.getStateEvents(EventType.RoomPowerLevels, "")?.getContent();
|
||||
return [
|
||||
content ?? {},
|
||||
content?.["events"] ?? {},
|
||||
state?.maySendStateEvent(EventType.RoomPowerLevels, MatrixClientPeg.get().getUserId()),
|
||||
];
|
||||
}, []));
|
||||
|
||||
const [elementCallEnabled, setElementCallEnabled] = useState<boolean>(() => {
|
||||
return events[ElementCall.MEMBER_EVENT_TYPE.name] === 0;
|
||||
});
|
||||
|
||||
const onChange = useCallback((enabled: boolean): void => {
|
||||
setElementCallEnabled(enabled);
|
||||
|
||||
if (enabled) {
|
||||
const userLevel = events[EventType.RoomMessage] ?? content.users_default ?? 0;
|
||||
const moderatorLevel = content.kick ?? 50;
|
||||
|
||||
events[ElementCall.CALL_EVENT_TYPE.name] = isPublic ? moderatorLevel : userLevel;
|
||||
events[ElementCall.MEMBER_EVENT_TYPE.name] = userLevel;
|
||||
} else {
|
||||
const adminLevel = events[EventType.RoomPowerLevels] ?? content.state_default ?? 100;
|
||||
|
||||
events[ElementCall.CALL_EVENT_TYPE.name] = adminLevel;
|
||||
events[ElementCall.MEMBER_EVENT_TYPE.name] = adminLevel;
|
||||
}
|
||||
|
||||
MatrixClientPeg.get().sendStateEvent(roomId, EventType.RoomPowerLevels, {
|
||||
"events": events,
|
||||
...content,
|
||||
});
|
||||
}, [roomId, content, events, isPublic]);
|
||||
|
||||
const brand = SdkConfig.get("element_call").brand;
|
||||
|
||||
return <LabelledToggleSwitch
|
||||
data-testid="element-call-switch"
|
||||
label={_t("Enable %(brand)s as an additional calling option in this room", { brand })}
|
||||
caption={_t(
|
||||
"%(brand)s is end-to-end encrypted, " +
|
||||
"but is currently limited to smaller numbers of users.",
|
||||
{ brand },
|
||||
)}
|
||||
value={elementCallEnabled}
|
||||
onChange={onChange}
|
||||
disabled={!maySend}
|
||||
tooltip={_t("You do not have sufficient permissions to change this.")}
|
||||
/>;
|
||||
};
|
||||
|
||||
interface Props {
|
||||
roomId: string;
|
||||
}
|
||||
|
||||
export const VoipRoomSettingsTab: React.FC<Props> = ({ roomId }) => {
|
||||
return <SettingsTab heading={_t("Voice & Video")}>
|
||||
<SettingsSubsection heading={_t("Call type")}>
|
||||
<ElementCallSwitch roomId={roomId} />
|
||||
</SettingsSubsection>
|
||||
</SettingsTab>;
|
||||
};
|
@ -46,6 +46,7 @@ import { findDMForUser } from "./utils/dm/findDMForUser";
|
||||
import { privateShouldBeEncrypted } from "./utils/rooms";
|
||||
import { waitForMember } from "./utils/membership";
|
||||
import { PreferredRoomVersions } from "./utils/PreferredRoomVersions";
|
||||
import SettingsStore from "./settings/SettingsStore";
|
||||
|
||||
// we define a number of interfaces which take their names from the js-sdk
|
||||
/* eslint-disable camelcase */
|
||||
@ -168,6 +169,16 @@ export default async function createRoom(opts: IOpts): Promise<string | null> {
|
||||
},
|
||||
};
|
||||
}
|
||||
} else if (SettingsStore.getValue("feature_group_calls")) {
|
||||
createOpts.power_level_content_override = {
|
||||
events: {
|
||||
...DEFAULT_EVENT_POWER_LEVELS,
|
||||
// Element Call should be disabled by default
|
||||
[ElementCall.MEMBER_EVENT_TYPE.name]: 100,
|
||||
// Make sure only admins can enable it
|
||||
[ElementCall.CALL_EVENT_TYPE.name]: 100,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
// By default, view the room after creating it
|
||||
|
@ -1650,6 +1650,8 @@
|
||||
"Modify widgets": "Modify widgets",
|
||||
"Voice broadcasts": "Voice broadcasts",
|
||||
"Manage pinned events": "Manage pinned events",
|
||||
"Start %(brand)s calls": "Start %(brand)s calls",
|
||||
"Join %(brand)s calls": "Join %(brand)s calls",
|
||||
"Default role": "Default role",
|
||||
"Send messages": "Send messages",
|
||||
"Invite users": "Invite users",
|
||||
@ -1689,6 +1691,10 @@
|
||||
"Security & Privacy": "Security & Privacy",
|
||||
"Once enabled, encryption cannot be disabled.": "Once enabled, encryption cannot be disabled.",
|
||||
"Encrypted": "Encrypted",
|
||||
"Enable %(brand)s as an additional calling option in this room": "Enable %(brand)s as an additional calling option in this room",
|
||||
"%(brand)s is end-to-end encrypted, but is currently limited to smaller numbers of users.": "%(brand)s is end-to-end encrypted, but is currently limited to smaller numbers of users.",
|
||||
"You do not have sufficient permissions to change this.": "You do not have sufficient permissions to change this.",
|
||||
"Call type": "Call type",
|
||||
"Unable to revoke sharing for email address": "Unable to revoke sharing for email address",
|
||||
"Unable to share email address": "Unable to share email address",
|
||||
"Your email address hasn't been verified yet": "Your email address hasn't been verified yet",
|
||||
@ -1892,7 +1898,7 @@
|
||||
"Recently visited rooms": "Recently visited rooms",
|
||||
"No recently visited rooms": "No recently visited rooms",
|
||||
"Video call (Jitsi)": "Video call (Jitsi)",
|
||||
"Video call (Element Call)": "Video call (Element Call)",
|
||||
"Video call (%(brand)s)": "Video call (%(brand)s)",
|
||||
"Ongoing call": "Ongoing call",
|
||||
"You do not have permission to start video calls": "You do not have permission to start video calls",
|
||||
"There's no one here to call": "There's no one here to call",
|
||||
|
@ -42,36 +42,53 @@ exports[`<LocationShareMenu /> with live location disabled goes to labs flag scr
|
||||
Enable live location sharing
|
||||
</span>
|
||||
<_default
|
||||
aria-label="Enable live location sharing"
|
||||
checked={false}
|
||||
onChange={[Function]}
|
||||
title="Enable live location sharing"
|
||||
>
|
||||
<AccessibleButton
|
||||
<AccessibleTooltipButton
|
||||
aria-checked={false}
|
||||
aria-disabled={false}
|
||||
aria-label="Enable live location sharing"
|
||||
className="mx_ToggleSwitch mx_ToggleSwitch_enabled"
|
||||
element="div"
|
||||
onClick={[Function]}
|
||||
role="switch"
|
||||
tabIndex={0}
|
||||
title="Enable live location sharing"
|
||||
>
|
||||
<div
|
||||
<AccessibleButton
|
||||
aria-checked={false}
|
||||
aria-disabled={false}
|
||||
aria-label="Enable live location sharing"
|
||||
className="mx_AccessibleButton mx_ToggleSwitch mx_ToggleSwitch_enabled"
|
||||
className="mx_ToggleSwitch mx_ToggleSwitch_enabled"
|
||||
element="div"
|
||||
onBlur={[Function]}
|
||||
onClick={[Function]}
|
||||
onKeyDown={[Function]}
|
||||
onKeyUp={[Function]}
|
||||
onFocus={[Function]}
|
||||
onMouseLeave={[Function]}
|
||||
onMouseOver={[Function]}
|
||||
role="switch"
|
||||
tabIndex={0}
|
||||
>
|
||||
<div
|
||||
className="mx_ToggleSwitch_ball"
|
||||
/>
|
||||
</div>
|
||||
</AccessibleButton>
|
||||
aria-checked={false}
|
||||
aria-disabled={false}
|
||||
aria-label="Enable live location sharing"
|
||||
className="mx_AccessibleButton mx_ToggleSwitch mx_ToggleSwitch_enabled"
|
||||
onBlur={[Function]}
|
||||
onClick={[Function]}
|
||||
onFocus={[Function]}
|
||||
onKeyDown={[Function]}
|
||||
onKeyUp={[Function]}
|
||||
onMouseLeave={[Function]}
|
||||
onMouseOver={[Function]}
|
||||
role="switch"
|
||||
tabIndex={0}
|
||||
>
|
||||
<div
|
||||
className="mx_ToggleSwitch_ball"
|
||||
/>
|
||||
</div>
|
||||
</AccessibleButton>
|
||||
</AccessibleTooltipButton>
|
||||
</_default>
|
||||
</div>
|
||||
</LabelledToggleSwitch>
|
||||
|
@ -505,7 +505,9 @@ describe("RoomHeader (React Testing Library)", () => {
|
||||
+ "and there's an ongoing call",
|
||||
async () => {
|
||||
mockEnabledSettings(["showCallButtonsInComposer", "feature_group_calls"]);
|
||||
SdkConfig.put({ element_call: { url: "https://call.element.io", use_exclusively: true } });
|
||||
SdkConfig.put(
|
||||
{ element_call: { url: "https://call.element.io", use_exclusively: true, brand: "Element Call" } },
|
||||
);
|
||||
await ElementCall.create(room);
|
||||
|
||||
renderHeader();
|
||||
@ -519,7 +521,9 @@ describe("RoomHeader (React Testing Library)", () => {
|
||||
+ "use Element Call exclusively",
|
||||
async () => {
|
||||
mockEnabledSettings(["showCallButtonsInComposer", "feature_group_calls"]);
|
||||
SdkConfig.put({ element_call: { url: "https://call.element.io", use_exclusively: true } });
|
||||
SdkConfig.put(
|
||||
{ element_call: { url: "https://call.element.io", use_exclusively: true, brand: "Element Call" } },
|
||||
);
|
||||
|
||||
renderHeader();
|
||||
expect(screen.queryByRole("button", { name: "Voice call" })).toBeNull();
|
||||
@ -541,7 +545,9 @@ describe("RoomHeader (React Testing Library)", () => {
|
||||
+ "and the user lacks permission",
|
||||
() => {
|
||||
mockEnabledSettings(["showCallButtonsInComposer", "feature_group_calls"]);
|
||||
SdkConfig.put({ element_call: { url: "https://call.element.io", use_exclusively: true } });
|
||||
SdkConfig.put(
|
||||
{ element_call: { url: "https://call.element.io", use_exclusively: true, brand: "Element Call" } },
|
||||
);
|
||||
mockEventPowerLevels({ [ElementCall.CALL_EVENT_TYPE.name]: 100 });
|
||||
|
||||
renderHeader();
|
||||
|
@ -18,37 +18,54 @@ exports[`<Notifications /> main notification switches email switches renders ema
|
||||
Enable email notifications for tester@test.com
|
||||
</span>
|
||||
<_default
|
||||
aria-label="Enable email notifications for tester@test.com"
|
||||
checked={false}
|
||||
disabled={false}
|
||||
onChange={[Function]}
|
||||
title="Enable email notifications for tester@test.com"
|
||||
>
|
||||
<AccessibleButton
|
||||
<AccessibleTooltipButton
|
||||
aria-checked={false}
|
||||
aria-disabled={false}
|
||||
aria-label="Enable email notifications for tester@test.com"
|
||||
className="mx_ToggleSwitch mx_ToggleSwitch_enabled"
|
||||
element="div"
|
||||
onClick={[Function]}
|
||||
role="switch"
|
||||
tabIndex={0}
|
||||
title="Enable email notifications for tester@test.com"
|
||||
>
|
||||
<div
|
||||
<AccessibleButton
|
||||
aria-checked={false}
|
||||
aria-disabled={false}
|
||||
aria-label="Enable email notifications for tester@test.com"
|
||||
className="mx_AccessibleButton mx_ToggleSwitch mx_ToggleSwitch_enabled"
|
||||
className="mx_ToggleSwitch mx_ToggleSwitch_enabled"
|
||||
element="div"
|
||||
onBlur={[Function]}
|
||||
onClick={[Function]}
|
||||
onKeyDown={[Function]}
|
||||
onKeyUp={[Function]}
|
||||
onFocus={[Function]}
|
||||
onMouseLeave={[Function]}
|
||||
onMouseOver={[Function]}
|
||||
role="switch"
|
||||
tabIndex={0}
|
||||
>
|
||||
<div
|
||||
className="mx_ToggleSwitch_ball"
|
||||
/>
|
||||
</div>
|
||||
</AccessibleButton>
|
||||
aria-checked={false}
|
||||
aria-disabled={false}
|
||||
aria-label="Enable email notifications for tester@test.com"
|
||||
className="mx_AccessibleButton mx_ToggleSwitch mx_ToggleSwitch_enabled"
|
||||
onBlur={[Function]}
|
||||
onClick={[Function]}
|
||||
onFocus={[Function]}
|
||||
onKeyDown={[Function]}
|
||||
onKeyUp={[Function]}
|
||||
onMouseLeave={[Function]}
|
||||
onMouseOver={[Function]}
|
||||
role="switch"
|
||||
tabIndex={0}
|
||||
>
|
||||
<div
|
||||
className="mx_ToggleSwitch_ball"
|
||||
/>
|
||||
</div>
|
||||
</AccessibleButton>
|
||||
</AccessibleTooltipButton>
|
||||
</_default>
|
||||
</div>
|
||||
</LabelledToggleSwitch>
|
||||
@ -84,37 +101,54 @@ exports[`<Notifications /> main notification switches renders only enable notifi
|
||||
</Caption>
|
||||
</span>
|
||||
<_default
|
||||
aria-label="Enable notifications for this account"
|
||||
checked={false}
|
||||
disabled={false}
|
||||
onChange={[Function]}
|
||||
title="Enable notifications for this account"
|
||||
>
|
||||
<AccessibleButton
|
||||
<AccessibleTooltipButton
|
||||
aria-checked={false}
|
||||
aria-disabled={false}
|
||||
aria-label="Enable notifications for this account"
|
||||
className="mx_ToggleSwitch mx_ToggleSwitch_enabled"
|
||||
element="div"
|
||||
onClick={[Function]}
|
||||
role="switch"
|
||||
tabIndex={0}
|
||||
title="Enable notifications for this account"
|
||||
>
|
||||
<div
|
||||
<AccessibleButton
|
||||
aria-checked={false}
|
||||
aria-disabled={false}
|
||||
aria-label="Enable notifications for this account"
|
||||
className="mx_AccessibleButton mx_ToggleSwitch mx_ToggleSwitch_enabled"
|
||||
className="mx_ToggleSwitch mx_ToggleSwitch_enabled"
|
||||
element="div"
|
||||
onBlur={[Function]}
|
||||
onClick={[Function]}
|
||||
onKeyDown={[Function]}
|
||||
onKeyUp={[Function]}
|
||||
onFocus={[Function]}
|
||||
onMouseLeave={[Function]}
|
||||
onMouseOver={[Function]}
|
||||
role="switch"
|
||||
tabIndex={0}
|
||||
>
|
||||
<div
|
||||
className="mx_ToggleSwitch_ball"
|
||||
/>
|
||||
</div>
|
||||
</AccessibleButton>
|
||||
aria-checked={false}
|
||||
aria-disabled={false}
|
||||
aria-label="Enable notifications for this account"
|
||||
className="mx_AccessibleButton mx_ToggleSwitch mx_ToggleSwitch_enabled"
|
||||
onBlur={[Function]}
|
||||
onClick={[Function]}
|
||||
onFocus={[Function]}
|
||||
onKeyDown={[Function]}
|
||||
onKeyUp={[Function]}
|
||||
onMouseLeave={[Function]}
|
||||
onMouseOver={[Function]}
|
||||
role="switch"
|
||||
tabIndex={0}
|
||||
>
|
||||
<div
|
||||
className="mx_ToggleSwitch_ball"
|
||||
/>
|
||||
</div>
|
||||
</AccessibleButton>
|
||||
</AccessibleTooltipButton>
|
||||
</_default>
|
||||
</div>
|
||||
</LabelledToggleSwitch>
|
||||
|
@ -16,30 +16,35 @@ limitations under the License.
|
||||
|
||||
import React from "react";
|
||||
import { fireEvent, render, RenderResult } from "@testing-library/react";
|
||||
import { EventType, MatrixClient } from "matrix-js-sdk/src/matrix";
|
||||
import { MatrixClient } from "matrix-js-sdk/src/client";
|
||||
import { EventType } from "matrix-js-sdk/src/@types/event";
|
||||
|
||||
import RolesRoomSettingsTab from "../../../../../../src/components/views/settings/tabs/room/RolesRoomSettingsTab";
|
||||
import { mkStubRoom, stubClient } from "../../../../../test-utils";
|
||||
import { MatrixClientPeg } from "../../../../../../src/MatrixClientPeg";
|
||||
import { VoiceBroadcastInfoEventType } from "../../../../../../src/voice-broadcast";
|
||||
import SettingsStore from "../../../../../../src/settings/SettingsStore";
|
||||
import { ElementCall } from "../../../../../../src/models/Call";
|
||||
|
||||
describe("RolesRoomSettingsTab", () => {
|
||||
const roomId = "!room:example.com";
|
||||
let rolesRoomSettingsTab: RenderResult;
|
||||
let cli: MatrixClient;
|
||||
|
||||
const renderTab = (): RenderResult => {
|
||||
return render(<RolesRoomSettingsTab roomId={roomId} />);
|
||||
};
|
||||
|
||||
const getVoiceBroadcastsSelect = () => {
|
||||
return rolesRoomSettingsTab.container.querySelector("select[label='Voice broadcasts']");
|
||||
return renderTab().container.querySelector("select[label='Voice broadcasts']");
|
||||
};
|
||||
|
||||
const getVoiceBroadcastsSelectedOption = () => {
|
||||
return rolesRoomSettingsTab.container.querySelector("select[label='Voice broadcasts'] option:checked");
|
||||
return renderTab().container.querySelector("select[label='Voice broadcasts'] option:checked");
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
stubClient();
|
||||
cli = MatrixClientPeg.get();
|
||||
rolesRoomSettingsTab = render(<RolesRoomSettingsTab roomId={roomId} />);
|
||||
mkStubRoom(roomId, "test room", cli);
|
||||
});
|
||||
|
||||
@ -66,4 +71,96 @@ describe("RolesRoomSettingsTab", () => {
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("Element Call", () => {
|
||||
const setGroupCallsEnabled = (val: boolean): void => {
|
||||
jest.spyOn(SettingsStore, "getValue").mockImplementation((name: string) => {
|
||||
if (name === "feature_group_calls") return val;
|
||||
});
|
||||
};
|
||||
|
||||
const getStartCallSelect = (tab: RenderResult) => {
|
||||
return tab.container.querySelector("select[label='Start Element Call calls']");
|
||||
};
|
||||
|
||||
const getStartCallSelectedOption = (tab: RenderResult) => {
|
||||
return tab.container.querySelector("select[label='Start Element Call calls'] option:checked");
|
||||
};
|
||||
|
||||
const getJoinCallSelect = (tab: RenderResult) => {
|
||||
return tab.container.querySelector("select[label='Join Element Call calls']");
|
||||
};
|
||||
|
||||
const getJoinCallSelectedOption = (tab: RenderResult) => {
|
||||
return tab.container.querySelector("select[label='Join Element Call calls'] option:checked");
|
||||
};
|
||||
|
||||
describe("Element Call enabled", () => {
|
||||
beforeEach(() => {
|
||||
setGroupCallsEnabled(true);
|
||||
});
|
||||
|
||||
describe("Join Element calls", () => {
|
||||
it("defaults to moderator for joining calls", () => {
|
||||
expect(getJoinCallSelectedOption(renderTab())?.textContent).toBe("Moderator");
|
||||
});
|
||||
|
||||
it("can change joining calls power level", () => {
|
||||
const tab = renderTab();
|
||||
|
||||
fireEvent.change(getJoinCallSelect(tab), {
|
||||
target: { value: 0 },
|
||||
});
|
||||
|
||||
expect(getJoinCallSelectedOption(tab)?.textContent).toBe("Default");
|
||||
expect(cli.sendStateEvent).toHaveBeenCalledWith(
|
||||
roomId,
|
||||
EventType.RoomPowerLevels,
|
||||
{
|
||||
events: {
|
||||
[ElementCall.MEMBER_EVENT_TYPE.name]: 0,
|
||||
},
|
||||
},
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("Start Element calls", () => {
|
||||
it("defaults to moderator for starting calls", () => {
|
||||
expect(getStartCallSelectedOption(renderTab())?.textContent).toBe("Moderator");
|
||||
});
|
||||
|
||||
it("can change starting calls power level", () => {
|
||||
const tab = renderTab();
|
||||
|
||||
fireEvent.change(getStartCallSelect(tab), {
|
||||
target: { value: 0 },
|
||||
});
|
||||
|
||||
expect(getStartCallSelectedOption(tab)?.textContent).toBe("Default");
|
||||
expect(cli.sendStateEvent).toHaveBeenCalledWith(
|
||||
roomId,
|
||||
EventType.RoomPowerLevels,
|
||||
{
|
||||
events: {
|
||||
[ElementCall.CALL_EVENT_TYPE.name]: 0,
|
||||
},
|
||||
},
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it("hides when group calls disabled", () => {
|
||||
setGroupCallsEnabled(false);
|
||||
|
||||
const tab = renderTab();
|
||||
|
||||
expect(getStartCallSelect(tab)).toBeFalsy();
|
||||
expect(getStartCallSelectedOption(tab)).toBeFalsy();
|
||||
|
||||
expect(getJoinCallSelect(tab)).toBeFalsy();
|
||||
expect(getJoinCallSelectedOption(tab)).toBeFalsy();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -0,0 +1,141 @@
|
||||
/*
|
||||
Copyright 2022 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import React from "react";
|
||||
import { fireEvent, render, RenderResult, waitFor } from "@testing-library/react";
|
||||
import { MatrixClient } from "matrix-js-sdk/src/client";
|
||||
import { Room } from "matrix-js-sdk/src/models/room";
|
||||
import { MatrixEvent } from "matrix-js-sdk/src/models/event";
|
||||
import { EventType } from "matrix-js-sdk/src/@types/event";
|
||||
import { JoinRule } from "matrix-js-sdk/src/@types/partials";
|
||||
|
||||
import { mkStubRoom, stubClient } from "../../../../../test-utils";
|
||||
import { MatrixClientPeg } from "../../../../../../src/MatrixClientPeg";
|
||||
import { VoipRoomSettingsTab } from "../../../../../../src/components/views/settings/tabs/room/VoipRoomSettingsTab";
|
||||
import { ElementCall } from "../../../../../../src/models/Call";
|
||||
|
||||
describe("RolesRoomSettingsTab", () => {
|
||||
const roomId = "!room:example.com";
|
||||
let cli: MatrixClient;
|
||||
let room: Room;
|
||||
|
||||
const renderTab = (): RenderResult => {
|
||||
return render(<VoipRoomSettingsTab roomId={roomId} />);
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
stubClient();
|
||||
cli = MatrixClientPeg.get();
|
||||
room = mkStubRoom(roomId, "test room", cli);
|
||||
|
||||
jest.spyOn(cli, "sendStateEvent");
|
||||
jest.spyOn(cli, "getRoom").mockReturnValue(room);
|
||||
});
|
||||
|
||||
describe("Element Call", () => {
|
||||
const mockPowerLevels = (events): void => {
|
||||
jest.spyOn(room.currentState, "getStateEvents").mockReturnValue({
|
||||
getContent: () => ({
|
||||
events,
|
||||
}),
|
||||
} as unknown as MatrixEvent);
|
||||
};
|
||||
|
||||
const getElementCallSwitch = (tab: RenderResult): HTMLElement => {
|
||||
return tab.container.querySelector("[data-testid='element-call-switch']");
|
||||
};
|
||||
|
||||
describe("correct state", () => {
|
||||
it("shows enabled when call member power level is 0", () => {
|
||||
mockPowerLevels({ [ElementCall.MEMBER_EVENT_TYPE.name]: 0 });
|
||||
|
||||
const tab = renderTab();
|
||||
|
||||
expect(getElementCallSwitch(tab).querySelector("[aria-checked='true']")).toBeTruthy();
|
||||
});
|
||||
|
||||
it.each([1, 50, 100])("shows disabled when call member power level is 0", (level: number) => {
|
||||
mockPowerLevels({ [ElementCall.MEMBER_EVENT_TYPE.name]: level });
|
||||
|
||||
const tab = renderTab();
|
||||
|
||||
expect(getElementCallSwitch(tab).querySelector("[aria-checked='false']")).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
||||
describe("enabling/disabling", () => {
|
||||
describe("enabling Element calls", () => {
|
||||
beforeEach(() => {
|
||||
mockPowerLevels({ [ElementCall.MEMBER_EVENT_TYPE.name]: 100 });
|
||||
});
|
||||
|
||||
it("enables Element calls in public room", async () => {
|
||||
jest.spyOn(room, "getJoinRule").mockReturnValue(JoinRule.Public);
|
||||
|
||||
const tab = renderTab();
|
||||
|
||||
fireEvent.click(getElementCallSwitch(tab).querySelector(".mx_ToggleSwitch"));
|
||||
await waitFor(() => expect(cli.sendStateEvent).toHaveBeenCalledWith(
|
||||
room.roomId,
|
||||
EventType.RoomPowerLevels,
|
||||
expect.objectContaining({
|
||||
events: {
|
||||
[ElementCall.CALL_EVENT_TYPE.name]: 50,
|
||||
[ElementCall.MEMBER_EVENT_TYPE.name]: 0,
|
||||
},
|
||||
}),
|
||||
));
|
||||
});
|
||||
|
||||
it("enables Element calls in private room", async () => {
|
||||
jest.spyOn(room, "getJoinRule").mockReturnValue(JoinRule.Invite);
|
||||
|
||||
const tab = renderTab();
|
||||
|
||||
fireEvent.click(getElementCallSwitch(tab).querySelector(".mx_ToggleSwitch"));
|
||||
await waitFor(() => expect(cli.sendStateEvent).toHaveBeenCalledWith(
|
||||
room.roomId,
|
||||
EventType.RoomPowerLevels,
|
||||
expect.objectContaining({
|
||||
events: {
|
||||
[ElementCall.CALL_EVENT_TYPE.name]: 0,
|
||||
[ElementCall.MEMBER_EVENT_TYPE.name]: 0,
|
||||
},
|
||||
}),
|
||||
));
|
||||
});
|
||||
});
|
||||
|
||||
it("disables Element calls", async () => {
|
||||
mockPowerLevels({ [ElementCall.MEMBER_EVENT_TYPE.name]: 0 });
|
||||
|
||||
const tab = renderTab();
|
||||
|
||||
fireEvent.click(getElementCallSwitch(tab).querySelector(".mx_ToggleSwitch"));
|
||||
await waitFor(() => expect(cli.sendStateEvent).toHaveBeenCalledWith(
|
||||
room.roomId,
|
||||
EventType.RoomPowerLevels,
|
||||
expect.objectContaining({
|
||||
events: {
|
||||
[ElementCall.CALL_EVENT_TYPE.name]: 100,
|
||||
[ElementCall.MEMBER_EVENT_TYPE.name]: 100,
|
||||
},
|
||||
}),
|
||||
));
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
@ -25,6 +25,7 @@ import WidgetStore from "../src/stores/WidgetStore";
|
||||
import WidgetUtils from "../src/utils/WidgetUtils";
|
||||
import { JitsiCall, ElementCall } from "../src/models/Call";
|
||||
import createRoom, { canEncryptToAllUsers } from '../src/createRoom';
|
||||
import SettingsStore from "../src/settings/SettingsStore";
|
||||
|
||||
describe("createRoom", () => {
|
||||
mockPlatformPeg();
|
||||
@ -85,7 +86,7 @@ describe("createRoom", () => {
|
||||
[ElementCall.MEMBER_EVENT_TYPE.name]: callMemberPower,
|
||||
},
|
||||
},
|
||||
}]] = client.createRoom.mock.calls as any; // no good type
|
||||
}]] = client.createRoom.mock.calls;
|
||||
|
||||
// We should have had enough power to be able to set up the call
|
||||
expect(userPower).toBeGreaterThanOrEqual(callPower);
|
||||
@ -109,6 +110,26 @@ describe("createRoom", () => {
|
||||
expect(createJitsiCallSpy).not.toHaveBeenCalled();
|
||||
expect(createElementCallSpy).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("correctly sets up MSC3401 power levels", async () => {
|
||||
jest.spyOn(SettingsStore, "getValue").mockImplementation((name: string) => {
|
||||
if (name === "feature_group_calls") return true;
|
||||
});
|
||||
|
||||
await createRoom({});
|
||||
|
||||
const [[{
|
||||
power_level_content_override: {
|
||||
events: {
|
||||
[ElementCall.CALL_EVENT_TYPE.name]: callPower,
|
||||
[ElementCall.MEMBER_EVENT_TYPE.name]: callMemberPower,
|
||||
},
|
||||
},
|
||||
}]] = client.createRoom.mock.calls;
|
||||
|
||||
expect(callPower).toBe(100);
|
||||
expect(callMemberPower).toBe(100);
|
||||
});
|
||||
});
|
||||
|
||||
describe("canEncryptToAllUsers", () => {
|
||||
|
@ -36,7 +36,7 @@ describe('recordClientInformation()', () => {
|
||||
|
||||
const sdkConfig: IConfigOptions = {
|
||||
brand: 'Test Brand',
|
||||
element_call: { url: '', use_exclusively: false },
|
||||
element_call: { url: '', use_exclusively: false, brand: "Element Call" },
|
||||
};
|
||||
|
||||
const platform = {
|
||||
|
Loading…
Reference in New Issue
Block a user