Add Element Call room settings (#9347)
Co-authored-by: Robin <robin@robin.town>
This commit is contained in:
parent
4ff9681408
commit
26a74a193f
21 changed files with 539 additions and 67 deletions
|
@ -21,6 +21,10 @@ limitations under the License.
|
||||||
mask-image: url('$(res)/img/element-icons/settings.svg');
|
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 {
|
.mx_RoomSettingsDialog_securityIcon::before {
|
||||||
mask-image: url('$(res)/img/element-icons/security.svg');
|
mask-image: url('$(res)/img/element-icons/security.svg');
|
||||||
}
|
}
|
||||||
|
|
|
@ -119,6 +119,7 @@ export interface IConfigOptions {
|
||||||
element_call: {
|
element_call: {
|
||||||
url: string;
|
url: string;
|
||||||
use_exclusively: boolean;
|
use_exclusively: boolean;
|
||||||
|
brand: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
logout_redirect_url?: string;
|
logout_redirect_url?: string;
|
||||||
|
|
|
@ -33,6 +33,7 @@ export const DEFAULTS: IConfigOptions = {
|
||||||
element_call: {
|
element_call: {
|
||||||
url: "https://call.element.io",
|
url: "https://call.element.io",
|
||||||
use_exclusively: false,
|
use_exclusively: false,
|
||||||
|
brand: "Element Call",
|
||||||
},
|
},
|
||||||
|
|
||||||
// @ts-ignore - we deliberately use the camelCase version here so we trigger
|
// @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 { UIFeature } from "../../../settings/UIFeature";
|
||||||
import BaseDialog from "./BaseDialog";
|
import BaseDialog from "./BaseDialog";
|
||||||
import { Action } from '../../../dispatcher/actions';
|
import { Action } from '../../../dispatcher/actions';
|
||||||
|
import { VoipRoomSettingsTab } from "../settings/tabs/room/VoipRoomSettingsTab";
|
||||||
|
|
||||||
export const ROOM_GENERAL_TAB = "ROOM_GENERAL_TAB";
|
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_SECURITY_TAB = "ROOM_SECURITY_TAB";
|
||||||
export const ROOM_ROLES_TAB = "ROOM_ROLES_TAB";
|
export const ROOM_ROLES_TAB = "ROOM_ROLES_TAB";
|
||||||
export const ROOM_NOTIFICATIONS_TAB = "ROOM_NOTIFICATIONS_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} />,
|
<GeneralRoomSettingsTab roomId={this.props.roomId} />,
|
||||||
"RoomSettingsGeneral",
|
"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(
|
tabs.push(new Tab(
|
||||||
ROOM_SECURITY_TAB,
|
ROOM_SECURITY_TAB,
|
||||||
_td("Security & Privacy"),
|
_td("Security & Privacy"),
|
||||||
|
|
|
@ -21,7 +21,7 @@ import AccessibleButton from "./AccessibleButton";
|
||||||
import Tooltip, { Alignment } from './Tooltip';
|
import Tooltip, { Alignment } from './Tooltip';
|
||||||
|
|
||||||
interface IProps extends React.ComponentProps<typeof AccessibleButton> {
|
interface IProps extends React.ComponentProps<typeof AccessibleButton> {
|
||||||
title: string;
|
title?: string;
|
||||||
tooltip?: React.ReactNode;
|
tooltip?: React.ReactNode;
|
||||||
label?: string;
|
label?: string;
|
||||||
tooltipClassName?: string;
|
tooltipClassName?: string;
|
||||||
|
@ -78,7 +78,7 @@ export default class AccessibleTooltipButton extends React.PureComponent<IProps,
|
||||||
const { title, tooltip, children, tooltipClassName, forceHide, alignment, onHideTooltip,
|
const { title, tooltip, children, tooltipClassName, forceHide, alignment, onHideTooltip,
|
||||||
...props } = this.props;
|
...props } = this.props;
|
||||||
|
|
||||||
const tip = this.state.hover && <Tooltip
|
const tip = this.state.hover && (title || tooltip) && <Tooltip
|
||||||
tooltipClassName={tooltipClassName}
|
tooltipClassName={tooltipClassName}
|
||||||
label={tooltip || title}
|
label={tooltip || title}
|
||||||
alignment={alignment}
|
alignment={alignment}
|
||||||
|
@ -86,11 +86,11 @@ export default class AccessibleTooltipButton extends React.PureComponent<IProps,
|
||||||
return (
|
return (
|
||||||
<AccessibleButton
|
<AccessibleButton
|
||||||
{...props}
|
{...props}
|
||||||
onMouseOver={this.showTooltip}
|
onMouseOver={this.showTooltip || props.onMouseOver}
|
||||||
onMouseLeave={this.hideTooltip}
|
onMouseLeave={this.hideTooltip || props.onMouseLeave}
|
||||||
onFocus={this.onFocus}
|
onFocus={this.onFocus || props.onFocus}
|
||||||
onBlur={this.hideTooltip}
|
onBlur={this.hideTooltip || props.onBlur}
|
||||||
aria-label={title}
|
aria-label={title || props["aria-label"]}
|
||||||
>
|
>
|
||||||
{ children }
|
{ children }
|
||||||
{ this.props.label }
|
{ this.props.label }
|
||||||
|
|
|
@ -27,6 +27,8 @@ interface IProps {
|
||||||
label: string;
|
label: string;
|
||||||
// The translated caption for the switch
|
// The translated caption for the switch
|
||||||
caption?: string;
|
caption?: string;
|
||||||
|
// Tooltip to display
|
||||||
|
tooltip?: string;
|
||||||
// Whether or not to disable the toggle switch
|
// Whether or not to disable the toggle switch
|
||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
// True to put the toggle in front of the label
|
// 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}
|
checked={this.props.value}
|
||||||
disabled={this.props.disabled}
|
disabled={this.props.disabled}
|
||||||
onChange={this.props.onChange}
|
onChange={this.props.onChange}
|
||||||
aria-label={this.props.label}
|
title={this.props.label}
|
||||||
|
tooltip={this.props.tooltip}
|
||||||
/>;
|
/>;
|
||||||
|
|
||||||
if (this.props.toggleInFront) {
|
if (this.props.toggleInFront) {
|
||||||
|
@ -66,7 +69,7 @@ export default class LabelledToggleSwitch extends React.PureComponent<IProps> {
|
||||||
"mx_SettingsFlag_toggleInFront": this.props.toggleInFront,
|
"mx_SettingsFlag_toggleInFront": this.props.toggleInFront,
|
||||||
});
|
});
|
||||||
return (
|
return (
|
||||||
<div className={classes}>
|
<div data-testid={this.props["data-testid"]} className={classes}>
|
||||||
{ firstPart }
|
{ firstPart }
|
||||||
{ secondPart }
|
{ secondPart }
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -114,7 +114,7 @@ export default class SettingsFlag extends React.Component<IProps, IState> {
|
||||||
checked={this.state.value}
|
checked={this.state.value}
|
||||||
onChange={this.onChange}
|
onChange={this.onChange}
|
||||||
disabled={this.props.disabled || !canChange}
|
disabled={this.props.disabled || !canChange}
|
||||||
aria-label={label}
|
title={label}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
|
@ -18,21 +18,27 @@ limitations under the License.
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import classNames from "classnames";
|
import classNames from "classnames";
|
||||||
|
|
||||||
import AccessibleButton from "./AccessibleButton";
|
import AccessibleTooltipButton from "./AccessibleTooltipButton";
|
||||||
|
|
||||||
interface IProps {
|
interface IProps {
|
||||||
// Whether or not this toggle is in the 'on' position.
|
// Whether or not this toggle is in the 'on' position.
|
||||||
checked: boolean;
|
checked: boolean;
|
||||||
|
|
||||||
|
// Title to use
|
||||||
|
title?: string;
|
||||||
|
|
||||||
// Whether or not the user can interact with the switch
|
// Whether or not the user can interact with the switch
|
||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
|
|
||||||
|
// Tooltip to show
|
||||||
|
tooltip?: string;
|
||||||
|
|
||||||
// Called when the checked state changes. First argument will be the new state.
|
// Called when the checked state changes. First argument will be the new state.
|
||||||
onChange(checked: boolean): void;
|
onChange(checked: boolean): void;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Controlled Toggle Switch element, written with Accessibility in mind
|
// 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 = () => {
|
const _onClick = () => {
|
||||||
if (disabled) return;
|
if (disabled) return;
|
||||||
onChange(!checked);
|
onChange(!checked);
|
||||||
|
@ -45,14 +51,16 @@ export default ({ checked, disabled = false, onChange, ...props }: IProps) => {
|
||||||
});
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<AccessibleButton {...props}
|
<AccessibleTooltipButton {...props}
|
||||||
className={classes}
|
className={classes}
|
||||||
onClick={_onClick}
|
onClick={_onClick}
|
||||||
role="switch"
|
role="switch"
|
||||||
aria-checked={checked}
|
aria-checked={checked}
|
||||||
aria-disabled={disabled}
|
aria-disabled={disabled}
|
||||||
|
title={title}
|
||||||
|
tooltip={tooltip}
|
||||||
>
|
>
|
||||||
<div className="mx_ToggleSwitch_ball" />
|
<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;
|
let menu: JSX.Element | null = null;
|
||||||
if (menuOpen) {
|
if (menuOpen) {
|
||||||
const buttonRect = buttonRef.current!.getBoundingClientRect();
|
const buttonRect = buttonRef.current!.getBoundingClientRect();
|
||||||
|
const brand = SdkConfig.get("element_call").brand;
|
||||||
menu = <IconizedContextMenu {...aboveLeftOf(buttonRect)} onFinished={closeMenu}>
|
menu = <IconizedContextMenu {...aboveLeftOf(buttonRect)} onFinished={closeMenu}>
|
||||||
<IconizedContextMenuOptionList>
|
<IconizedContextMenuOptionList>
|
||||||
<IconizedContextMenuOption label={_t("Video call (Jitsi)")} onClick={onJitsiClick} />
|
<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>
|
</IconizedContextMenuOptionList>
|
||||||
</IconizedContextMenu>;
|
</IconizedContextMenu>;
|
||||||
}
|
}
|
||||||
|
|
|
@ -153,7 +153,7 @@ const DeviceDetails: React.FC<Props> = ({
|
||||||
checked={isPushNotificationsEnabled(pusher, localNotificationSettings)}
|
checked={isPushNotificationsEnabled(pusher, localNotificationSettings)}
|
||||||
disabled={isCheckboxDisabled(pusher, localNotificationSettings)}
|
disabled={isCheckboxDisabled(pusher, localNotificationSettings)}
|
||||||
onChange={checked => setPushNotifications?.(device.device_id, checked)}
|
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'
|
data-testid='device-detail-push-notification-checkbox'
|
||||||
/>
|
/>
|
||||||
<p className='mx_DeviceDetails_sectionHeading'>
|
<p className='mx_DeviceDetails_sectionHeading'>
|
||||||
|
|
|
@ -31,6 +31,8 @@ import PowerSelector from "../../../elements/PowerSelector";
|
||||||
import SettingsFieldset from '../../SettingsFieldset';
|
import SettingsFieldset from '../../SettingsFieldset';
|
||||||
import SettingsStore from "../../../../../settings/SettingsStore";
|
import SettingsStore from "../../../../../settings/SettingsStore";
|
||||||
import { VoiceBroadcastInfoEventType } from '../../../../../voice-broadcast';
|
import { VoiceBroadcastInfoEventType } from '../../../../../voice-broadcast';
|
||||||
|
import { ElementCall } from "../../../../../models/Call";
|
||||||
|
import SdkConfig from "../../../../../SdkConfig";
|
||||||
|
|
||||||
interface IEventShowOpts {
|
interface IEventShowOpts {
|
||||||
isState?: boolean;
|
isState?: boolean;
|
||||||
|
@ -60,6 +62,10 @@ const plEventsToShow: Record<string, IEventShowOpts> = {
|
||||||
[EventType.Reaction]: { isState: false, hideForSpace: true },
|
[EventType.Reaction]: { isState: false, hideForSpace: true },
|
||||||
[EventType.RoomRedaction]: { 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)
|
// 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 },
|
"im.vector.modular.widgets": { isState: true, hideForSpace: true },
|
||||||
[VoiceBroadcastInfoEventType]: { 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")) {
|
if (SettingsStore.getValue("feature_pinning")) {
|
||||||
plEventsToLabels[EventType.RoomPinnedEvents] = _td("Manage pinned events");
|
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> = {
|
const powerLevelDescriptors: Record<string, IPowerLevelDescriptor> = {
|
||||||
"users_default": {
|
"users_default": {
|
||||||
|
@ -435,7 +446,8 @@ export default class RolesRoomSettingsTab extends React.Component<IProps> {
|
||||||
|
|
||||||
let label = plEventsToLabels[eventType];
|
let label = plEventsToLabels[eventType];
|
||||||
if (label) {
|
if (label) {
|
||||||
label = _t(label);
|
const brand = SdkConfig.get("element_call").brand;
|
||||||
|
label = _t(label, { brand });
|
||||||
} else {
|
} else {
|
||||||
label = _t("Send %(eventType)s events", { eventType });
|
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 { privateShouldBeEncrypted } from "./utils/rooms";
|
||||||
import { waitForMember } from "./utils/membership";
|
import { waitForMember } from "./utils/membership";
|
||||||
import { PreferredRoomVersions } from "./utils/PreferredRoomVersions";
|
import { PreferredRoomVersions } from "./utils/PreferredRoomVersions";
|
||||||
|
import SettingsStore from "./settings/SettingsStore";
|
||||||
|
|
||||||
// we define a number of interfaces which take their names from the js-sdk
|
// we define a number of interfaces which take their names from the js-sdk
|
||||||
/* eslint-disable camelcase */
|
/* 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
|
// By default, view the room after creating it
|
||||||
|
|
|
@ -1650,6 +1650,8 @@
|
||||||
"Modify widgets": "Modify widgets",
|
"Modify widgets": "Modify widgets",
|
||||||
"Voice broadcasts": "Voice broadcasts",
|
"Voice broadcasts": "Voice broadcasts",
|
||||||
"Manage pinned events": "Manage pinned events",
|
"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",
|
"Default role": "Default role",
|
||||||
"Send messages": "Send messages",
|
"Send messages": "Send messages",
|
||||||
"Invite users": "Invite users",
|
"Invite users": "Invite users",
|
||||||
|
@ -1689,6 +1691,10 @@
|
||||||
"Security & Privacy": "Security & Privacy",
|
"Security & Privacy": "Security & Privacy",
|
||||||
"Once enabled, encryption cannot be disabled.": "Once enabled, encryption cannot be disabled.",
|
"Once enabled, encryption cannot be disabled.": "Once enabled, encryption cannot be disabled.",
|
||||||
"Encrypted": "Encrypted",
|
"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 revoke sharing for email address": "Unable to revoke sharing for email address",
|
||||||
"Unable to share email address": "Unable to share 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",
|
"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",
|
"Recently visited rooms": "Recently visited rooms",
|
||||||
"No recently visited rooms": "No recently visited rooms",
|
"No recently visited rooms": "No recently visited rooms",
|
||||||
"Video call (Jitsi)": "Video call (Jitsi)",
|
"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",
|
"Ongoing call": "Ongoing call",
|
||||||
"You do not have permission to start video calls": "You do not have permission to start video calls",
|
"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",
|
"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
|
Enable live location sharing
|
||||||
</span>
|
</span>
|
||||||
<_default
|
<_default
|
||||||
aria-label="Enable live location sharing"
|
|
||||||
checked={false}
|
checked={false}
|
||||||
onChange={[Function]}
|
onChange={[Function]}
|
||||||
|
title="Enable live location sharing"
|
||||||
>
|
>
|
||||||
<AccessibleButton
|
<AccessibleTooltipButton
|
||||||
aria-checked={false}
|
aria-checked={false}
|
||||||
aria-disabled={false}
|
aria-disabled={false}
|
||||||
aria-label="Enable live location sharing"
|
|
||||||
className="mx_ToggleSwitch mx_ToggleSwitch_enabled"
|
className="mx_ToggleSwitch mx_ToggleSwitch_enabled"
|
||||||
element="div"
|
|
||||||
onClick={[Function]}
|
onClick={[Function]}
|
||||||
role="switch"
|
role="switch"
|
||||||
tabIndex={0}
|
title="Enable live location sharing"
|
||||||
>
|
>
|
||||||
<div
|
<AccessibleButton
|
||||||
aria-checked={false}
|
aria-checked={false}
|
||||||
aria-disabled={false}
|
aria-disabled={false}
|
||||||
aria-label="Enable live location sharing"
|
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]}
|
onClick={[Function]}
|
||||||
onKeyDown={[Function]}
|
onFocus={[Function]}
|
||||||
onKeyUp={[Function]}
|
onMouseLeave={[Function]}
|
||||||
|
onMouseOver={[Function]}
|
||||||
role="switch"
|
role="switch"
|
||||||
tabIndex={0}
|
tabIndex={0}
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
className="mx_ToggleSwitch_ball"
|
aria-checked={false}
|
||||||
/>
|
aria-disabled={false}
|
||||||
</div>
|
aria-label="Enable live location sharing"
|
||||||
</AccessibleButton>
|
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>
|
</_default>
|
||||||
</div>
|
</div>
|
||||||
</LabelledToggleSwitch>
|
</LabelledToggleSwitch>
|
||||||
|
|
|
@ -505,7 +505,9 @@ describe("RoomHeader (React Testing Library)", () => {
|
||||||
+ "and there's an ongoing call",
|
+ "and there's an ongoing call",
|
||||||
async () => {
|
async () => {
|
||||||
mockEnabledSettings(["showCallButtonsInComposer", "feature_group_calls"]);
|
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);
|
await ElementCall.create(room);
|
||||||
|
|
||||||
renderHeader();
|
renderHeader();
|
||||||
|
@ -519,7 +521,9 @@ describe("RoomHeader (React Testing Library)", () => {
|
||||||
+ "use Element Call exclusively",
|
+ "use Element Call exclusively",
|
||||||
async () => {
|
async () => {
|
||||||
mockEnabledSettings(["showCallButtonsInComposer", "feature_group_calls"]);
|
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();
|
renderHeader();
|
||||||
expect(screen.queryByRole("button", { name: "Voice call" })).toBeNull();
|
expect(screen.queryByRole("button", { name: "Voice call" })).toBeNull();
|
||||||
|
@ -541,7 +545,9 @@ describe("RoomHeader (React Testing Library)", () => {
|
||||||
+ "and the user lacks permission",
|
+ "and the user lacks permission",
|
||||||
() => {
|
() => {
|
||||||
mockEnabledSettings(["showCallButtonsInComposer", "feature_group_calls"]);
|
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 });
|
mockEventPowerLevels({ [ElementCall.CALL_EVENT_TYPE.name]: 100 });
|
||||||
|
|
||||||
renderHeader();
|
renderHeader();
|
||||||
|
|
|
@ -18,37 +18,54 @@ exports[`<Notifications /> main notification switches email switches renders ema
|
||||||
Enable email notifications for tester@test.com
|
Enable email notifications for tester@test.com
|
||||||
</span>
|
</span>
|
||||||
<_default
|
<_default
|
||||||
aria-label="Enable email notifications for tester@test.com"
|
|
||||||
checked={false}
|
checked={false}
|
||||||
disabled={false}
|
disabled={false}
|
||||||
onChange={[Function]}
|
onChange={[Function]}
|
||||||
|
title="Enable email notifications for tester@test.com"
|
||||||
>
|
>
|
||||||
<AccessibleButton
|
<AccessibleTooltipButton
|
||||||
aria-checked={false}
|
aria-checked={false}
|
||||||
aria-disabled={false}
|
aria-disabled={false}
|
||||||
aria-label="Enable email notifications for tester@test.com"
|
|
||||||
className="mx_ToggleSwitch mx_ToggleSwitch_enabled"
|
className="mx_ToggleSwitch mx_ToggleSwitch_enabled"
|
||||||
element="div"
|
|
||||||
onClick={[Function]}
|
onClick={[Function]}
|
||||||
role="switch"
|
role="switch"
|
||||||
tabIndex={0}
|
title="Enable email notifications for tester@test.com"
|
||||||
>
|
>
|
||||||
<div
|
<AccessibleButton
|
||||||
aria-checked={false}
|
aria-checked={false}
|
||||||
aria-disabled={false}
|
aria-disabled={false}
|
||||||
aria-label="Enable email notifications for tester@test.com"
|
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]}
|
onClick={[Function]}
|
||||||
onKeyDown={[Function]}
|
onFocus={[Function]}
|
||||||
onKeyUp={[Function]}
|
onMouseLeave={[Function]}
|
||||||
|
onMouseOver={[Function]}
|
||||||
role="switch"
|
role="switch"
|
||||||
tabIndex={0}
|
tabIndex={0}
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
className="mx_ToggleSwitch_ball"
|
aria-checked={false}
|
||||||
/>
|
aria-disabled={false}
|
||||||
</div>
|
aria-label="Enable email notifications for tester@test.com"
|
||||||
</AccessibleButton>
|
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>
|
</_default>
|
||||||
</div>
|
</div>
|
||||||
</LabelledToggleSwitch>
|
</LabelledToggleSwitch>
|
||||||
|
@ -84,37 +101,54 @@ exports[`<Notifications /> main notification switches renders only enable notifi
|
||||||
</Caption>
|
</Caption>
|
||||||
</span>
|
</span>
|
||||||
<_default
|
<_default
|
||||||
aria-label="Enable notifications for this account"
|
|
||||||
checked={false}
|
checked={false}
|
||||||
disabled={false}
|
disabled={false}
|
||||||
onChange={[Function]}
|
onChange={[Function]}
|
||||||
|
title="Enable notifications for this account"
|
||||||
>
|
>
|
||||||
<AccessibleButton
|
<AccessibleTooltipButton
|
||||||
aria-checked={false}
|
aria-checked={false}
|
||||||
aria-disabled={false}
|
aria-disabled={false}
|
||||||
aria-label="Enable notifications for this account"
|
|
||||||
className="mx_ToggleSwitch mx_ToggleSwitch_enabled"
|
className="mx_ToggleSwitch mx_ToggleSwitch_enabled"
|
||||||
element="div"
|
|
||||||
onClick={[Function]}
|
onClick={[Function]}
|
||||||
role="switch"
|
role="switch"
|
||||||
tabIndex={0}
|
title="Enable notifications for this account"
|
||||||
>
|
>
|
||||||
<div
|
<AccessibleButton
|
||||||
aria-checked={false}
|
aria-checked={false}
|
||||||
aria-disabled={false}
|
aria-disabled={false}
|
||||||
aria-label="Enable notifications for this account"
|
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]}
|
onClick={[Function]}
|
||||||
onKeyDown={[Function]}
|
onFocus={[Function]}
|
||||||
onKeyUp={[Function]}
|
onMouseLeave={[Function]}
|
||||||
|
onMouseOver={[Function]}
|
||||||
role="switch"
|
role="switch"
|
||||||
tabIndex={0}
|
tabIndex={0}
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
className="mx_ToggleSwitch_ball"
|
aria-checked={false}
|
||||||
/>
|
aria-disabled={false}
|
||||||
</div>
|
aria-label="Enable notifications for this account"
|
||||||
</AccessibleButton>
|
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>
|
</_default>
|
||||||
</div>
|
</div>
|
||||||
</LabelledToggleSwitch>
|
</LabelledToggleSwitch>
|
||||||
|
|
|
@ -16,30 +16,35 @@ limitations under the License.
|
||||||
|
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import { fireEvent, render, RenderResult } from "@testing-library/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 RolesRoomSettingsTab from "../../../../../../src/components/views/settings/tabs/room/RolesRoomSettingsTab";
|
||||||
import { mkStubRoom, stubClient } from "../../../../../test-utils";
|
import { mkStubRoom, stubClient } from "../../../../../test-utils";
|
||||||
import { MatrixClientPeg } from "../../../../../../src/MatrixClientPeg";
|
import { MatrixClientPeg } from "../../../../../../src/MatrixClientPeg";
|
||||||
import { VoiceBroadcastInfoEventType } from "../../../../../../src/voice-broadcast";
|
import { VoiceBroadcastInfoEventType } from "../../../../../../src/voice-broadcast";
|
||||||
|
import SettingsStore from "../../../../../../src/settings/SettingsStore";
|
||||||
|
import { ElementCall } from "../../../../../../src/models/Call";
|
||||||
|
|
||||||
describe("RolesRoomSettingsTab", () => {
|
describe("RolesRoomSettingsTab", () => {
|
||||||
const roomId = "!room:example.com";
|
const roomId = "!room:example.com";
|
||||||
let rolesRoomSettingsTab: RenderResult;
|
|
||||||
let cli: MatrixClient;
|
let cli: MatrixClient;
|
||||||
|
|
||||||
|
const renderTab = (): RenderResult => {
|
||||||
|
return render(<RolesRoomSettingsTab roomId={roomId} />);
|
||||||
|
};
|
||||||
|
|
||||||
const getVoiceBroadcastsSelect = () => {
|
const getVoiceBroadcastsSelect = () => {
|
||||||
return rolesRoomSettingsTab.container.querySelector("select[label='Voice broadcasts']");
|
return renderTab().container.querySelector("select[label='Voice broadcasts']");
|
||||||
};
|
};
|
||||||
|
|
||||||
const getVoiceBroadcastsSelectedOption = () => {
|
const getVoiceBroadcastsSelectedOption = () => {
|
||||||
return rolesRoomSettingsTab.container.querySelector("select[label='Voice broadcasts'] option:checked");
|
return renderTab().container.querySelector("select[label='Voice broadcasts'] option:checked");
|
||||||
};
|
};
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
stubClient();
|
stubClient();
|
||||||
cli = MatrixClientPeg.get();
|
cli = MatrixClientPeg.get();
|
||||||
rolesRoomSettingsTab = render(<RolesRoomSettingsTab roomId={roomId} />);
|
|
||||||
mkStubRoom(roomId, "test room", cli);
|
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 WidgetUtils from "../src/utils/WidgetUtils";
|
||||||
import { JitsiCall, ElementCall } from "../src/models/Call";
|
import { JitsiCall, ElementCall } from "../src/models/Call";
|
||||||
import createRoom, { canEncryptToAllUsers } from '../src/createRoom';
|
import createRoom, { canEncryptToAllUsers } from '../src/createRoom';
|
||||||
|
import SettingsStore from "../src/settings/SettingsStore";
|
||||||
|
|
||||||
describe("createRoom", () => {
|
describe("createRoom", () => {
|
||||||
mockPlatformPeg();
|
mockPlatformPeg();
|
||||||
|
@ -85,7 +86,7 @@ describe("createRoom", () => {
|
||||||
[ElementCall.MEMBER_EVENT_TYPE.name]: callMemberPower,
|
[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
|
// We should have had enough power to be able to set up the call
|
||||||
expect(userPower).toBeGreaterThanOrEqual(callPower);
|
expect(userPower).toBeGreaterThanOrEqual(callPower);
|
||||||
|
@ -109,6 +110,26 @@ describe("createRoom", () => {
|
||||||
expect(createJitsiCallSpy).not.toHaveBeenCalled();
|
expect(createJitsiCallSpy).not.toHaveBeenCalled();
|
||||||
expect(createElementCallSpy).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", () => {
|
describe("canEncryptToAllUsers", () => {
|
||||||
|
|
|
@ -36,7 +36,7 @@ describe('recordClientInformation()', () => {
|
||||||
|
|
||||||
const sdkConfig: IConfigOptions = {
|
const sdkConfig: IConfigOptions = {
|
||||||
brand: 'Test Brand',
|
brand: 'Test Brand',
|
||||||
element_call: { url: '', use_exclusively: false },
|
element_call: { url: '', use_exclusively: false, brand: "Element Call" },
|
||||||
};
|
};
|
||||||
|
|
||||||
const platform = {
|
const platform = {
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue