Bail out of RoomSettingsDialog when room is not found (#10662)

* hack to fix console noise from unfaked timers and clearAllModals

* remove old debug logging in AsyncWrapper

* pass room to room settings tabs

* add errorboundary for roomsettingsdialog

* apply strictnullchecks to tabs/room

* dedupe code to set toom in roomsettingdialog

* add unit tests

* test SecurityRoomSettingsTab

* remove snapshot

* strict fixes

* more tests

* 2% more test coverage

* remove roomName from RoomSettingsDialogs state
This commit is contained in:
Kerry 2023-04-27 13:20:02 +12:00 committed by GitHub
parent f6e8ffe750
commit 223892bf0e
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
19 changed files with 1077 additions and 121 deletions

View file

@ -1,6 +1,8 @@
/* /*
Copyright 2019 New Vector Ltd Copyright 2019 New Vector Ltd
Copyright 2019 Michael Telatynski <7t3chguy@gmail.com> Copyright 2019 Michael Telatynski <7t3chguy@gmail.com>
Copyright 2023 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License"); Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License. you may not use this file except in compliance with the License.
@ -16,7 +18,7 @@ limitations under the License.
*/ */
import React from "react"; import React from "react";
import { RoomEvent } from "matrix-js-sdk/src/models/room"; import { RoomEvent, Room } from "matrix-js-sdk/src/models/room";
import TabbedView, { Tab } from "../../structures/TabbedView"; import TabbedView, { Tab } from "../../structures/TabbedView";
import { _t, _td } from "../../../languageHandler"; import { _t, _td } from "../../../languageHandler";
@ -36,6 +38,7 @@ import { VoipRoomSettingsTab } from "../settings/tabs/room/VoipRoomSettingsTab";
import { ActionPayload } from "../../../dispatcher/payloads"; import { ActionPayload } from "../../../dispatcher/payloads";
import { NonEmptyArray } from "../../../@types/common"; import { NonEmptyArray } from "../../../@types/common";
import { PollHistoryTab } from "../settings/tabs/room/PollHistoryTab"; import { PollHistoryTab } from "../settings/tabs/room/PollHistoryTab";
import ErrorBoundary from "../elements/ErrorBoundary";
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_VOIP_TAB = "ROOM_VOIP_TAB";
@ -53,15 +56,17 @@ interface IProps {
} }
interface IState { interface IState {
roomName: string; room: Room;
} }
export default class RoomSettingsDialog extends React.Component<IProps, IState> { class RoomSettingsDialog extends React.Component<IProps, IState> {
private dispatcherRef: string; private dispatcherRef: string;
public constructor(props: IProps) { public constructor(props: IProps) {
super(props); super(props);
this.state = { roomName: "" };
const room = this.getRoom();
this.state = { room };
} }
public componentDidMount(): void { public componentDidMount(): void {
@ -70,6 +75,13 @@ export default class RoomSettingsDialog extends React.Component<IProps, IState>
this.onRoomName(); this.onRoomName();
} }
public componentDidUpdate(): void {
if (this.props.roomId !== this.state.room.roomId) {
const room = this.getRoom();
this.setState({ room });
}
}
public componentWillUnmount(): void { public componentWillUnmount(): void {
if (this.dispatcherRef) { if (this.dispatcherRef) {
dis.unregister(this.dispatcherRef); dis.unregister(this.dispatcherRef);
@ -78,6 +90,21 @@ export default class RoomSettingsDialog extends React.Component<IProps, IState>
MatrixClientPeg.get().removeListener(RoomEvent.Name, this.onRoomName); MatrixClientPeg.get().removeListener(RoomEvent.Name, this.onRoomName);
} }
/**
* Get room from client
* @returns Room
* @throws when room is not found
*/
private getRoom(): Room {
const room = MatrixClientPeg.get().getRoom(this.props.roomId)!;
// something is really wrong if we encounter this
if (!room) {
throw new Error(`Cannot find room ${this.props.roomId}`);
}
return room;
}
private onAction = (payload: ActionPayload): void => { private onAction = (payload: ActionPayload): void => {
// When view changes below us, close the room settings // When view changes below us, close the room settings
// whilst the modal is open this can only be triggered when someone hits Leave Room // whilst the modal is open this can only be triggered when someone hits Leave Room
@ -87,9 +114,8 @@ export default class RoomSettingsDialog extends React.Component<IProps, IState>
}; };
private onRoomName = (): void => { private onRoomName = (): void => {
this.setState({ // rerender when the room name changes
roomName: MatrixClientPeg.get().getRoom(this.props.roomId)?.name ?? "", this.forceUpdate();
});
}; };
private getTabs(): NonEmptyArray<Tab> { private getTabs(): NonEmptyArray<Tab> {
@ -100,7 +126,7 @@ export default class RoomSettingsDialog extends React.Component<IProps, IState>
ROOM_GENERAL_TAB, ROOM_GENERAL_TAB,
_td("General"), _td("General"),
"mx_RoomSettingsDialog_settingsIcon", "mx_RoomSettingsDialog_settingsIcon",
<GeneralRoomSettingsTab roomId={this.props.roomId} />, <GeneralRoomSettingsTab room={this.state.room} />,
"RoomSettingsGeneral", "RoomSettingsGeneral",
), ),
); );
@ -110,7 +136,7 @@ export default class RoomSettingsDialog extends React.Component<IProps, IState>
ROOM_VOIP_TAB, ROOM_VOIP_TAB,
_td("Voice & Video"), _td("Voice & Video"),
"mx_RoomSettingsDialog_voiceIcon", "mx_RoomSettingsDialog_voiceIcon",
<VoipRoomSettingsTab roomId={this.props.roomId} />, <VoipRoomSettingsTab room={this.state.room} />,
), ),
); );
} }
@ -119,12 +145,7 @@ export default class RoomSettingsDialog extends React.Component<IProps, IState>
ROOM_SECURITY_TAB, ROOM_SECURITY_TAB,
_td("Security & Privacy"), _td("Security & Privacy"),
"mx_RoomSettingsDialog_securityIcon", "mx_RoomSettingsDialog_securityIcon",
( <SecurityRoomSettingsTab room={this.state.room} closeSettingsFn={() => this.props.onFinished(true)} />,
<SecurityRoomSettingsTab
roomId={this.props.roomId}
closeSettingsFn={() => this.props.onFinished(true)}
/>
),
"RoomSettingsSecurityPrivacy", "RoomSettingsSecurityPrivacy",
), ),
); );
@ -133,7 +154,7 @@ export default class RoomSettingsDialog extends React.Component<IProps, IState>
ROOM_ROLES_TAB, ROOM_ROLES_TAB,
_td("Roles & Permissions"), _td("Roles & Permissions"),
"mx_RoomSettingsDialog_rolesIcon", "mx_RoomSettingsDialog_rolesIcon",
<RolesRoomSettingsTab roomId={this.props.roomId} />, <RolesRoomSettingsTab room={this.state.room} />,
"RoomSettingsRolesPermissions", "RoomSettingsRolesPermissions",
), ),
); );
@ -144,7 +165,7 @@ export default class RoomSettingsDialog extends React.Component<IProps, IState>
"mx_RoomSettingsDialog_notificationsIcon", "mx_RoomSettingsDialog_notificationsIcon",
( (
<NotificationSettingsTab <NotificationSettingsTab
roomId={this.props.roomId} roomId={this.state.room.roomId}
closeSettingsFn={() => this.props.onFinished(true)} closeSettingsFn={() => this.props.onFinished(true)}
/> />
), ),
@ -158,7 +179,7 @@ export default class RoomSettingsDialog extends React.Component<IProps, IState>
ROOM_BRIDGES_TAB, ROOM_BRIDGES_TAB,
_td("Bridges"), _td("Bridges"),
"mx_RoomSettingsDialog_bridgesIcon", "mx_RoomSettingsDialog_bridgesIcon",
<BridgeSettingsTab roomId={this.props.roomId} />, <BridgeSettingsTab room={this.state.room} />,
"RoomSettingsBridges", "RoomSettingsBridges",
), ),
); );
@ -169,7 +190,7 @@ export default class RoomSettingsDialog extends React.Component<IProps, IState>
ROOM_POLL_HISTORY_TAB, ROOM_POLL_HISTORY_TAB,
_td("Poll history"), _td("Poll history"),
"mx_RoomSettingsDialog_pollsIcon", "mx_RoomSettingsDialog_pollsIcon",
<PollHistoryTab roomId={this.props.roomId} onFinished={() => this.props.onFinished(true)} />, <PollHistoryTab room={this.state.room} onFinished={() => this.props.onFinished(true)} />,
), ),
); );
@ -181,7 +202,7 @@ export default class RoomSettingsDialog extends React.Component<IProps, IState>
"mx_RoomSettingsDialog_warningIcon", "mx_RoomSettingsDialog_warningIcon",
( (
<AdvancedRoomSettingsTab <AdvancedRoomSettingsTab
roomId={this.props.roomId} room={this.state.room}
closeSettingsFn={() => this.props.onFinished(true)} closeSettingsFn={() => this.props.onFinished(true)}
/> />
), ),
@ -194,7 +215,7 @@ export default class RoomSettingsDialog extends React.Component<IProps, IState>
} }
public render(): React.ReactNode { public render(): React.ReactNode {
const roomName = this.state.roomName; const roomName = this.state.room.name;
return ( return (
<BaseDialog <BaseDialog
className="mx_RoomSettingsDialog" className="mx_RoomSettingsDialog"
@ -213,3 +234,11 @@ export default class RoomSettingsDialog extends React.Component<IProps, IState>
); );
} }
} }
const WrappedRoomSettingsDialog: React.FC<IProps> = (props) => (
<ErrorBoundary>
<RoomSettingsDialog {...props} />
</ErrorBoundary>
);
export default WrappedRoomSettingsDialog;

View file

@ -70,14 +70,14 @@ const SpaceSettingsDialog: React.FC<IProps> = ({ matrixClient: cli, space, onFin
SpaceSettingsTab.Roles, SpaceSettingsTab.Roles,
_td("Roles & Permissions"), _td("Roles & Permissions"),
"mx_RoomSettingsDialog_rolesIcon", "mx_RoomSettingsDialog_rolesIcon",
<RolesRoomSettingsTab roomId={space.roomId} />, <RolesRoomSettingsTab room={space} />,
), ),
SettingsStore.getValue(UIFeature.AdvancedSettings) SettingsStore.getValue(UIFeature.AdvancedSettings)
? new Tab( ? new Tab(
SpaceSettingsTab.Advanced, SpaceSettingsTab.Advanced,
_td("Advanced"), _td("Advanced"),
"mx_RoomSettingsDialog_warningIcon", "mx_RoomSettingsDialog_warningIcon",
<AdvancedRoomSettingsTab roomId={space.roomId} closeSettingsFn={onFinished} />, <AdvancedRoomSettingsTab room={space} closeSettingsFn={onFinished} />,
) )
: null, : null,
].filter(Boolean) as NonEmptyArray<Tab>; ].filter(Boolean) as NonEmptyArray<Tab>;

View file

@ -16,9 +16,9 @@ limitations under the License.
import React from "react"; import React from "react";
import { EventType } from "matrix-js-sdk/src/@types/event"; import { EventType } from "matrix-js-sdk/src/@types/event";
import { Room } from "matrix-js-sdk/src/matrix";
import { _t } from "../../../../../languageHandler"; import { _t } from "../../../../../languageHandler";
import { MatrixClientPeg } from "../../../../../MatrixClientPeg";
import AccessibleButton, { ButtonEvent } from "../../../elements/AccessibleButton"; import AccessibleButton, { ButtonEvent } from "../../../elements/AccessibleButton";
import RoomUpgradeDialog from "../../../dialogs/RoomUpgradeDialog"; import RoomUpgradeDialog from "../../../dialogs/RoomUpgradeDialog";
import Modal from "../../../../../Modal"; import Modal from "../../../../../Modal";
@ -29,7 +29,7 @@ import { ViewRoomPayload } from "../../../../../dispatcher/payloads/ViewRoomPayl
import SettingsStore from "../../../../../settings/SettingsStore"; import SettingsStore from "../../../../../settings/SettingsStore";
interface IProps { interface IProps {
roomId: string; room: Room;
closeSettingsFn(): void; closeSettingsFn(): void;
} }
@ -64,8 +64,8 @@ export default class AdvancedRoomSettingsTab extends React.Component<IProps, ISt
this.state = {}; this.state = {};
// we handle lack of this object gracefully later, so don't worry about it failing here. // we handle lack of this object gracefully later, so don't worry about it failing here.
const room = MatrixClientPeg.get().getRoom(this.props.roomId); const room = this.props.room;
room?.getRecommendedVersion().then((v) => { room.getRecommendedVersion().then((v) => {
const tombstone = room.currentState.getStateEvents(EventType.RoomTombstone, ""); const tombstone = room.currentState.getStateEvents(EventType.RoomTombstone, "");
const additionalStateChanges: Partial<IState> = {}; const additionalStateChanges: Partial<IState> = {};
@ -85,8 +85,7 @@ export default class AdvancedRoomSettingsTab extends React.Component<IProps, ISt
} }
private upgradeRoom = (): void => { private upgradeRoom = (): void => {
const room = MatrixClientPeg.get().getRoom(this.props.roomId); Modal.createDialog(RoomUpgradeDialog, { room: this.props.room });
if (room) Modal.createDialog(RoomUpgradeDialog, { room });
}; };
private onOldRoomClicked = (e: ButtonEvent): void => { private onOldRoomClicked = (e: ButtonEvent): void => {
@ -105,12 +104,11 @@ export default class AdvancedRoomSettingsTab extends React.Component<IProps, ISt
}; };
public render(): React.ReactNode { public render(): React.ReactNode {
const client = MatrixClientPeg.get(); const room = this.props.room;
const room = client.getRoom(this.props.roomId); const isSpace = room.isSpaceRoom();
const isSpace = room?.isSpaceRoom();
let unfederatableSection: JSX.Element | undefined; let unfederatableSection: JSX.Element | undefined;
if (room?.currentState.getStateEvents(EventType.RoomCreate, "")?.getContent()["m.federate"] === false) { if (room.currentState.getStateEvents(EventType.RoomCreate, "")?.getContent()["m.federate"] === false) {
unfederatableSection = <div>{_t("This room is not accessible by remote Matrix servers")}</div>; unfederatableSection = <div>{_t("This room is not accessible by remote Matrix servers")}</div>;
} }
@ -143,9 +141,9 @@ export default class AdvancedRoomSettingsTab extends React.Component<IProps, ISt
if (this.state.oldRoomId) { if (this.state.oldRoomId) {
let copy: string; let copy: string;
if (isSpace) { if (isSpace) {
copy = _t("View older version of %(spaceName)s.", { spaceName: room?.name ?? this.state.oldRoomId }); copy = _t("View older version of %(spaceName)s.", { spaceName: room.name ?? this.state.oldRoomId });
} else { } else {
copy = _t("View older messages in %(roomName)s.", { roomName: room?.name ?? this.state.oldRoomId }); copy = _t("View older messages in %(roomName)s.", { roomName: room.name ?? this.state.oldRoomId });
} }
oldRoomLink = ( oldRoomLink = (
@ -160,11 +158,13 @@ export default class AdvancedRoomSettingsTab extends React.Component<IProps, ISt
<div className="mx_SettingsTab_heading">{_t("Advanced")}</div> <div className="mx_SettingsTab_heading">{_t("Advanced")}</div>
<div className="mx_SettingsTab_section mx_SettingsTab_subsectionText"> <div className="mx_SettingsTab_section mx_SettingsTab_subsectionText">
<span className="mx_SettingsTab_subheading"> <span className="mx_SettingsTab_subheading">
{room?.isSpaceRoom() ? _t("Space information") : _t("Room information")} {room.isSpaceRoom() ? _t("Space information") : _t("Room information")}
</span> </span>
<div> <div>
<span>{_t("Internal room ID")}</span> <span>{_t("Internal room ID")}</span>
<CopyableText getTextToCopy={() => this.props.roomId}>{this.props.roomId}</CopyableText> <CopyableText getTextToCopy={() => this.props.room.roomId}>
{this.props.room.roomId}
</CopyableText>
</div> </div>
{unfederatableSection} {unfederatableSection}
</div> </div>
@ -172,7 +172,7 @@ export default class AdvancedRoomSettingsTab extends React.Component<IProps, ISt
<span className="mx_SettingsTab_subheading">{_t("Room version")}</span> <span className="mx_SettingsTab_subheading">{_t("Room version")}</span>
<div> <div>
<span>{_t("Room version:")}</span>&nbsp; <span>{_t("Room version:")}</span>&nbsp;
{room?.getVersion()} {room.getVersion()}
</div> </div>
{oldRoomLink} {oldRoomLink}
{roomUpgradeButton} {roomUpgradeButton}

View file

@ -30,7 +30,7 @@ const BRIDGE_EVENT_TYPES = [
const BRIDGES_LINK = "https://matrix.org/bridges/"; const BRIDGES_LINK = "https://matrix.org/bridges/";
interface IProps { interface IProps {
roomId: string; room: Room;
} }
export default class BridgeSettingsTab extends React.Component<IProps> { export default class BridgeSettingsTab extends React.Component<IProps> {
@ -51,9 +51,8 @@ export default class BridgeSettingsTab extends React.Component<IProps> {
public render(): React.ReactNode { public render(): React.ReactNode {
// This settings tab will only be invoked if the following function returns more // This settings tab will only be invoked if the following function returns more
// than 0 events, so no validation is needed at this stage. // than 0 events, so no validation is needed at this stage.
const bridgeEvents = BridgeSettingsTab.getBridgeStateEvents(this.props.roomId); const bridgeEvents = BridgeSettingsTab.getBridgeStateEvents(this.props.room.roomId);
const client = MatrixClientPeg.get(); const room = this.props.room;
const room = client.getRoom(this.props.roomId);
let content: JSX.Element; let content: JSX.Element;
if (bridgeEvents.length > 0) { if (bridgeEvents.length > 0) {

View file

@ -15,6 +15,7 @@ limitations under the License.
*/ */
import React, { ContextType } from "react"; import React, { ContextType } from "react";
import { Room } from "matrix-js-sdk/src/matrix";
import { _t } from "../../../../../languageHandler"; import { _t } from "../../../../../languageHandler";
import RoomProfileSettings from "../../../room_settings/RoomProfileSettings"; import RoomProfileSettings from "../../../room_settings/RoomProfileSettings";
@ -28,7 +29,7 @@ import AliasSettings from "../../../room_settings/AliasSettings";
import PosthogTrackers from "../../../../../PosthogTrackers"; import PosthogTrackers from "../../../../../PosthogTrackers";
interface IProps { interface IProps {
roomId: string; room: Room;
} }
interface IState { interface IState {
@ -50,7 +51,7 @@ export default class GeneralRoomSettingsTab extends React.Component<IProps, ISta
private onLeaveClick = (ev: ButtonEvent): void => { private onLeaveClick = (ev: ButtonEvent): void => {
dis.dispatch({ dis.dispatch({
action: "leave_room", action: "leave_room",
room_id: this.props.roomId, room_id: this.props.room.roomId,
}); });
PosthogTrackers.trackInteraction("WebRoomSettingsLeaveButton", ev); PosthogTrackers.trackInteraction("WebRoomSettingsLeaveButton", ev);
@ -58,17 +59,18 @@ export default class GeneralRoomSettingsTab extends React.Component<IProps, ISta
public render(): React.ReactNode { public render(): React.ReactNode {
const client = this.context; const client = this.context;
const room = client.getRoom(this.props.roomId); const room = this.props.room;
const canSetAliases = true; // Previously, we arbitrarily only allowed admins to do this const canSetAliases = true; // Previously, we arbitrarily only allowed admins to do this
const canSetCanonical = room?.currentState.mayClientSendStateEvent("m.room.canonical_alias", client); const canSetCanonical = room.currentState.mayClientSendStateEvent("m.room.canonical_alias", client);
const canonicalAliasEv = room?.currentState.getStateEvents("m.room.canonical_alias", "") ?? undefined; const canonicalAliasEv = room.currentState.getStateEvents("m.room.canonical_alias", "") ?? undefined;
const urlPreviewSettings = const urlPreviewSettings = SettingsStore.getValue(UIFeature.URLPreviews) ? (
room && SettingsStore.getValue(UIFeature.URLPreviews) ? <UrlPreviewSettings room={room} /> : null; <UrlPreviewSettings room={room} />
) : null;
let leaveSection; let leaveSection;
if (room?.getMyMembership() === "join") { if (room.getMyMembership() === "join") {
leaveSection = ( leaveSection = (
<> <>
<span className="mx_SettingsTab_subheading">{_t("Leave room")}</span> <span className="mx_SettingsTab_subheading">{_t("Leave room")}</span>
@ -85,12 +87,12 @@ export default class GeneralRoomSettingsTab extends React.Component<IProps, ISta
<div className="mx_SettingsTab mx_GeneralRoomSettingsTab"> <div className="mx_SettingsTab mx_GeneralRoomSettingsTab">
<div className="mx_SettingsTab_heading">{_t("General")}</div> <div className="mx_SettingsTab_heading">{_t("General")}</div>
<div className="mx_SettingsTab_section mx_GeneralRoomSettingsTab_profileSection"> <div className="mx_SettingsTab_section mx_GeneralRoomSettingsTab_profileSection">
<RoomProfileSettings roomId={this.props.roomId} /> <RoomProfileSettings roomId={room.roomId} />
</div> </div>
<div className="mx_SettingsTab_heading">{_t("Room Addresses")}</div> <div className="mx_SettingsTab_heading">{_t("Room Addresses")}</div>
<AliasSettings <AliasSettings
roomId={this.props.roomId} roomId={room.roomId}
canSetCanonicalAlias={canSetCanonical} canSetCanonicalAlias={canSetCanonical}
canSetAliases={canSetAliases} canSetAliases={canSetAliases}
canonicalAliasEvent={canonicalAliasEv} canonicalAliasEvent={canonicalAliasEv}

View file

@ -53,7 +53,7 @@ export default class NotificationsSettingsTab extends React.Component<IProps, IS
public constructor(props: IProps, context: React.ContextType<typeof MatrixClientContext>) { public constructor(props: IProps, context: React.ContextType<typeof MatrixClientContext>) {
super(props, context); super(props, context);
this.roomProps = EchoChamber.forRoom(context.getRoom(this.props.roomId)); this.roomProps = EchoChamber.forRoom(context.getRoom(this.props.roomId)!);
let currentSound = "default"; let currentSound = "default";
const soundData = Notifier.getSoundForRoom(this.props.roomId); const soundData = Notifier.getSoundForRoom(this.props.roomId);

View file

@ -15,23 +15,20 @@ limitations under the License.
*/ */
import React, { useContext } from "react"; import React, { useContext } from "react";
import { Room } from "matrix-js-sdk/src/matrix";
import MatrixClientContext from "../../../../../contexts/MatrixClientContext"; import MatrixClientContext from "../../../../../contexts/MatrixClientContext";
import { PollHistory } from "../../../polls/pollHistory/PollHistory"; import { PollHistory } from "../../../polls/pollHistory/PollHistory";
import { RoomPermalinkCreator } from "../../../../../utils/permalinks/Permalinks"; import { RoomPermalinkCreator } from "../../../../../utils/permalinks/Permalinks";
interface IProps { interface IProps {
roomId: string; room: Room;
onFinished: () => void; onFinished: () => void;
} }
export const PollHistoryTab: React.FC<IProps> = ({ roomId, onFinished }) => { export const PollHistoryTab: React.FC<IProps> = ({ room, onFinished }) => {
const matrixClient = useContext(MatrixClientContext); const matrixClient = useContext(MatrixClientContext);
const room = matrixClient.getRoom(roomId); const permalinkCreator = new RoomPermalinkCreator(room, room.roomId);
if (!room) {
return null;
}
const permalinkCreator = new RoomPermalinkCreator(room, roomId);
return ( return (
<div className="mx_SettingsTab"> <div className="mx_SettingsTab">

View file

@ -22,6 +22,7 @@ import { logger } from "matrix-js-sdk/src/logger";
import { throttle, get } from "lodash"; import { throttle, get } from "lodash";
import { compare } from "matrix-js-sdk/src/utils"; import { compare } from "matrix-js-sdk/src/utils";
import { IContent } from "matrix-js-sdk/src/models/event"; import { IContent } from "matrix-js-sdk/src/models/event";
import { Room } from "matrix-js-sdk/src/matrix";
import { _t, _td } from "../../../../../languageHandler"; import { _t, _td } from "../../../../../languageHandler";
import { MatrixClientPeg } from "../../../../../MatrixClientPeg"; import { MatrixClientPeg } from "../../../../../MatrixClientPeg";
@ -129,7 +130,7 @@ export class BannedUser extends React.Component<IBannedUserProps> {
} }
interface IProps { interface IProps {
roomId: string; room: Room;
} }
export default class RolesRoomSettingsTab extends React.Component<IProps> { export default class RolesRoomSettingsTab extends React.Component<IProps> {
@ -145,7 +146,7 @@ export default class RolesRoomSettingsTab extends React.Component<IProps> {
} }
private onRoomStateUpdate = (state: RoomState): void => { private onRoomStateUpdate = (state: RoomState): void => {
if (state.roomId !== this.props.roomId) return; if (state.roomId !== this.props.room.roomId) return;
this.onThisRoomMembership(); this.onThisRoomMembership();
}; };
@ -171,8 +172,8 @@ export default class RolesRoomSettingsTab extends React.Component<IProps> {
private onPowerLevelsChanged = (value: number, powerLevelKey: string): void => { private onPowerLevelsChanged = (value: number, powerLevelKey: string): void => {
const client = MatrixClientPeg.get(); const client = MatrixClientPeg.get();
const room = client.getRoom(this.props.roomId); const room = this.props.room;
const plEvent = room?.currentState.getStateEvents(EventType.RoomPowerLevels, ""); const plEvent = room.currentState.getStateEvents(EventType.RoomPowerLevels, "");
let plContent = plEvent?.getContent() ?? {}; let plContent = plEvent?.getContent() ?? {};
// Clone the power levels just in case // Clone the power levels just in case
@ -186,7 +187,7 @@ export default class RolesRoomSettingsTab extends React.Component<IProps> {
plContent["events"][powerLevelKey.slice(eventsLevelPrefix.length)] = value; plContent["events"][powerLevelKey.slice(eventsLevelPrefix.length)] = value;
} else { } else {
const keyPath = powerLevelKey.split("."); const keyPath = powerLevelKey.split(".");
let parentObj: IContent | undefined; let parentObj: IContent = {};
let currentObj = plContent; let currentObj = plContent;
for (const key of keyPath) { for (const key of keyPath) {
if (!currentObj[key]) { if (!currentObj[key]) {
@ -198,7 +199,7 @@ export default class RolesRoomSettingsTab extends React.Component<IProps> {
parentObj[keyPath[keyPath.length - 1]] = value; parentObj[keyPath[keyPath.length - 1]] = value;
} }
client.sendStateEvent(this.props.roomId, EventType.RoomPowerLevels, plContent).catch((e) => { client.sendStateEvent(this.props.room.roomId, EventType.RoomPowerLevels, plContent).catch((e) => {
logger.error(e); logger.error(e);
Modal.createDialog(ErrorDialog, { Modal.createDialog(ErrorDialog, {
@ -213,8 +214,8 @@ export default class RolesRoomSettingsTab extends React.Component<IProps> {
private onUserPowerLevelChanged = (value: number, powerLevelKey: string): void => { private onUserPowerLevelChanged = (value: number, powerLevelKey: string): void => {
const client = MatrixClientPeg.get(); const client = MatrixClientPeg.get();
const room = client.getRoom(this.props.roomId); const room = this.props.room;
const plEvent = room?.currentState.getStateEvents(EventType.RoomPowerLevels, ""); const plEvent = room.currentState.getStateEvents(EventType.RoomPowerLevels, "");
let plContent = plEvent?.getContent() ?? {}; let plContent = plEvent?.getContent() ?? {};
// Clone the power levels just in case // Clone the power levels just in case
@ -224,7 +225,7 @@ export default class RolesRoomSettingsTab extends React.Component<IProps> {
if (!plContent["users"]) plContent["users"] = {}; if (!plContent["users"]) plContent["users"] = {};
plContent["users"][powerLevelKey] = value; plContent["users"][powerLevelKey] = value;
client.sendStateEvent(this.props.roomId, EventType.RoomPowerLevels, plContent).catch((e) => { client.sendStateEvent(this.props.room.roomId, EventType.RoomPowerLevels, plContent).catch((e) => {
logger.error(e); logger.error(e);
Modal.createDialog(ErrorDialog, { Modal.createDialog(ErrorDialog, {
@ -239,12 +240,12 @@ export default class RolesRoomSettingsTab extends React.Component<IProps> {
public render(): React.ReactNode { public render(): React.ReactNode {
const client = MatrixClientPeg.get(); const client = MatrixClientPeg.get();
const room = client.getRoom(this.props.roomId); const room = this.props.room;
const isSpaceRoom = room?.isSpaceRoom(); const isSpaceRoom = room.isSpaceRoom();
const plEvent = room?.currentState.getStateEvents(EventType.RoomPowerLevels, ""); const plEvent = room.currentState.getStateEvents(EventType.RoomPowerLevels, "");
const plContent = plEvent ? plEvent.getContent() || {} : {}; const plContent = plEvent ? plEvent.getContent() || {} : {};
const canChangeLevels = room?.currentState.mayClientSendStateEvent(EventType.RoomPowerLevels, client); const canChangeLevels = room.currentState.mayClientSendStateEvent(EventType.RoomPowerLevels, client);
const plEventsToLabels: Record<EventType | string, string | null> = { const plEventsToLabels: Record<EventType | string, string | null> = {
// These will be translated for us later. // These will be translated for us later.
@ -392,7 +393,7 @@ export default class RolesRoomSettingsTab extends React.Component<IProps> {
} }
} }
const banned = room?.getMembersWithMembership("ban"); const banned = room.getMembersWithMembership("ban");
let bannedUsersSection: JSX.Element | undefined; let bannedUsersSection: JSX.Element | undefined;
if (banned?.length) { if (banned?.length) {
const canBanUsers = currentUserLevel >= banLevel; const canBanUsers = currentUserLevel >= banLevel;
@ -401,16 +402,16 @@ export default class RolesRoomSettingsTab extends React.Component<IProps> {
<ul> <ul>
{banned.map((member) => { {banned.map((member) => {
const banEvent = member.events.member?.getContent(); const banEvent = member.events.member?.getContent();
const sender = room?.getMember(member.events.member.getSender()); const bannedById = member.events.member?.getSender();
let bannedBy = member.events.member?.getSender(); // start by falling back to mxid const sender = bannedById ? room.getMember(bannedById) : undefined;
if (sender) bannedBy = sender.name; const bannedBy = sender?.name || bannedById; // fallback to mxid
return ( return (
<BannedUser <BannedUser
key={member.userId} key={member.userId}
canUnban={canBanUsers} canUnban={canBanUsers}
member={member} member={member}
reason={banEvent?.reason} reason={banEvent?.reason}
by={bannedBy} by={bannedBy!}
/> />
); );
})} })}
@ -443,7 +444,7 @@ export default class RolesRoomSettingsTab extends React.Component<IProps> {
.filter(Boolean); .filter(Boolean);
// hide the power level selector for enabling E2EE if it the room is already encrypted // hide the power level selector for enabling E2EE if it the room is already encrypted
if (client.isRoomEncrypted(this.props.roomId)) { if (client.isRoomEncrypted(this.props.room.roomId)) {
delete eventsLevels[EventType.RoomEncryption]; delete eventsLevels[EventType.RoomEncryption];
} }
@ -481,9 +482,7 @@ export default class RolesRoomSettingsTab extends React.Component<IProps> {
<div className="mx_SettingsTab mx_RolesRoomSettingsTab"> <div className="mx_SettingsTab mx_RolesRoomSettingsTab">
<div className="mx_SettingsTab_heading">{_t("Roles & Permissions")}</div> <div className="mx_SettingsTab_heading">{_t("Roles & Permissions")}</div>
{privilegedUsersSection} {privilegedUsersSection}
{canChangeLevels && room !== null && ( {canChangeLevels && <AddPrivilegedUsers room={room} defaultUserLevel={defaultUserLevel} />}
<AddPrivilegedUsers room={room} defaultUserLevel={defaultUserLevel} />
)}
{mutedUsersSection} {mutedUsersSection}
{bannedUsersSection} {bannedUsersSection}
<SettingsFieldset <SettingsFieldset

View file

@ -20,6 +20,7 @@ import { MatrixEvent } from "matrix-js-sdk/src/models/event";
import { RoomStateEvent } from "matrix-js-sdk/src/models/room-state"; import { RoomStateEvent } from "matrix-js-sdk/src/models/room-state";
import { EventType } from "matrix-js-sdk/src/@types/event"; import { EventType } from "matrix-js-sdk/src/@types/event";
import { logger } from "matrix-js-sdk/src/logger"; import { logger } from "matrix-js-sdk/src/logger";
import { Room } from "matrix-js-sdk/src/matrix";
import { Icon as WarningIcon } from "../../../../../../res/img/warning.svg"; import { Icon as WarningIcon } from "../../../../../../res/img/warning.svg";
import { _t } from "../../../../../languageHandler"; import { _t } from "../../../../../languageHandler";
@ -42,7 +43,7 @@ import PosthogTrackers from "../../../../../PosthogTrackers";
import MatrixClientContext from "../../../../../contexts/MatrixClientContext"; import MatrixClientContext from "../../../../../contexts/MatrixClientContext";
interface IProps { interface IProps {
roomId: string; room: Room;
closeSettingsFn: () => void; closeSettingsFn: () => void;
} }
@ -61,7 +62,7 @@ export default class SecurityRoomSettingsTab extends React.Component<IProps, ISt
public constructor(props: IProps, context: React.ContextType<typeof MatrixClientContext>) { public constructor(props: IProps, context: React.ContextType<typeof MatrixClientContext>) {
super(props, context); super(props, context);
const state = context.getRoom(this.props.roomId)?.currentState; const state = this.props.room.currentState;
this.state = { this.state = {
guestAccess: this.pullContentPropertyFromEvent<GuestAccess>( guestAccess: this.pullContentPropertyFromEvent<GuestAccess>(
@ -75,7 +76,7 @@ export default class SecurityRoomSettingsTab extends React.Component<IProps, ISt
HistoryVisibility.Shared, HistoryVisibility.Shared,
), ),
hasAliases: false, // async loaded in componentDidMount hasAliases: false, // async loaded in componentDidMount
encrypted: context.isRoomEncrypted(this.props.roomId), encrypted: context.isRoomEncrypted(this.props.room.roomId),
showAdvancedSection: false, showAdvancedSection: false,
}; };
} }
@ -104,7 +105,7 @@ export default class SecurityRoomSettingsTab extends React.Component<IProps, ISt
}; };
private onEncryptionChange = async (): Promise<void> => { private onEncryptionChange = async (): Promise<void> => {
if (this.context.getRoom(this.props.roomId)?.getJoinRule() === JoinRule.Public) { if (this.props.room.getJoinRule() === JoinRule.Public) {
const dialog = Modal.createDialog(QuestionDialog, { const dialog = Modal.createDialog(QuestionDialog, {
title: _t("Are you sure you want to add encryption to this public room?"), title: _t("Are you sure you want to add encryption to this public room?"),
description: ( description: (
@ -172,7 +173,9 @@ export default class SecurityRoomSettingsTab extends React.Component<IProps, ISt
const beforeEncrypted = this.state.encrypted; const beforeEncrypted = this.state.encrypted;
this.setState({ encrypted: true }); this.setState({ encrypted: true });
this.context this.context
.sendStateEvent(this.props.roomId, EventType.RoomEncryption, { algorithm: "m.megolm.v1.aes-sha2" }) .sendStateEvent(this.props.room.roomId, EventType.RoomEncryption, {
algorithm: "m.megolm.v1.aes-sha2",
})
.catch((e) => { .catch((e) => {
logger.error(e); logger.error(e);
this.setState({ encrypted: beforeEncrypted }); this.setState({ encrypted: beforeEncrypted });
@ -190,7 +193,7 @@ export default class SecurityRoomSettingsTab extends React.Component<IProps, ISt
this.context this.context
.sendStateEvent( .sendStateEvent(
this.props.roomId, this.props.room.roomId,
EventType.RoomGuestAccess, EventType.RoomGuestAccess,
{ {
guest_access: guestAccess, guest_access: guestAccess,
@ -222,7 +225,7 @@ export default class SecurityRoomSettingsTab extends React.Component<IProps, ISt
this.setState({ history: history }); this.setState({ history: history });
this.context this.context
.sendStateEvent( .sendStateEvent(
this.props.roomId, this.props.room.roomId,
EventType.RoomHistoryVisibility, EventType.RoomHistoryVisibility,
{ {
history_visibility: history, history_visibility: history,
@ -236,22 +239,21 @@ export default class SecurityRoomSettingsTab extends React.Component<IProps, ISt
}; };
private updateBlacklistDevicesFlag = (checked: boolean): void => { private updateBlacklistDevicesFlag = (checked: boolean): void => {
this.context.getRoom(this.props.roomId)?.setBlacklistUnverifiedDevices(checked); this.props.room.setBlacklistUnverifiedDevices(checked);
}; };
private async hasAliases(): Promise<boolean> { private async hasAliases(): Promise<boolean> {
const cli = this.context; const cli = this.context;
const response = await cli.getLocalAliases(this.props.roomId); const response = await cli.getLocalAliases(this.props.room.roomId);
const localAliases = response.aliases; const localAliases = response.aliases;
return Array.isArray(localAliases) && localAliases.length !== 0; return Array.isArray(localAliases) && localAliases.length !== 0;
} }
private renderJoinRule(): JSX.Element { private renderJoinRule(): JSX.Element {
const client = this.context; const room = this.props.room;
const room = client.getRoom(this.props.roomId);
let aliasWarning: JSX.Element | undefined; let aliasWarning: JSX.Element | undefined;
if (room?.getJoinRule() === JoinRule.Public && !this.state.hasAliases) { if (room.getJoinRule() === JoinRule.Public && !this.state.hasAliases) {
aliasWarning = ( aliasWarning = (
<div className="mx_SecurityRoomSettingsTab_warning"> <div className="mx_SecurityRoomSettingsTab_warning">
<WarningIcon width={15} height={15} /> <WarningIcon width={15} height={15} />
@ -260,7 +262,7 @@ export default class SecurityRoomSettingsTab extends React.Component<IProps, ISt
); );
} }
const description = _t("Decide who can join %(roomName)s.", { const description = _t("Decide who can join %(roomName)s.", {
roomName: room?.name, roomName: room.name,
}); });
return ( return (
@ -342,7 +344,7 @@ export default class SecurityRoomSettingsTab extends React.Component<IProps, ISt
const client = this.context; const client = this.context;
const history = this.state.history; const history = this.state.history;
const state = client.getRoom(this.props.roomId)?.currentState; const state = this.props.room.currentState;
const canChangeHistory = state?.mayClientSendStateEvent(EventType.RoomHistoryVisibility, client); const canChangeHistory = state?.mayClientSendStateEvent(EventType.RoomHistoryVisibility, client);
const options = [ const options = [
@ -393,7 +395,7 @@ export default class SecurityRoomSettingsTab extends React.Component<IProps, ISt
private renderAdvanced(): JSX.Element { private renderAdvanced(): JSX.Element {
const client = this.context; const client = this.context;
const guestAccess = this.state.guestAccess; const guestAccess = this.state.guestAccess;
const state = client.getRoom(this.props.roomId)?.currentState; const state = this.props.room.currentState;
const canSetGuestAccess = state?.mayClientSendStateEvent(EventType.RoomGuestAccess, client); const canSetGuestAccess = state?.mayClientSendStateEvent(EventType.RoomGuestAccess, client);
return ( return (
@ -416,9 +418,9 @@ export default class SecurityRoomSettingsTab extends React.Component<IProps, ISt
public render(): React.ReactNode { public render(): React.ReactNode {
const client = this.context; const client = this.context;
const room = client.getRoom(this.props.roomId); const room = this.props.room;
const isEncrypted = this.state.encrypted; const isEncrypted = this.state.encrypted;
const hasEncryptionPermission = room?.currentState.mayClientSendStateEvent(EventType.RoomEncryption, client); const hasEncryptionPermission = room.currentState.mayClientSendStateEvent(EventType.RoomEncryption, client);
const canEnableEncryption = !isEncrypted && hasEncryptionPermission; const canEnableEncryption = !isEncrypted && hasEncryptionPermission;
let encryptionSettings: JSX.Element | undefined; let encryptionSettings: JSX.Element | undefined;
@ -428,7 +430,7 @@ export default class SecurityRoomSettingsTab extends React.Component<IProps, ISt
name="blacklistUnverifiedDevices" name="blacklistUnverifiedDevices"
level={SettingLevel.ROOM_DEVICE} level={SettingLevel.ROOM_DEVICE}
onChange={this.updateBlacklistDevicesFlag} onChange={this.updateBlacklistDevicesFlag}
roomId={this.props.roomId} roomId={this.props.room.roomId}
/> />
); );
} }
@ -436,7 +438,7 @@ export default class SecurityRoomSettingsTab extends React.Component<IProps, ISt
const historySection = this.renderHistory(); const historySection = this.renderHistory();
let advanced: JSX.Element | undefined; let advanced: JSX.Element | undefined;
if (room?.getJoinRule() === JoinRule.Public) { if (room.getJoinRule() === JoinRule.Public) {
advanced = ( advanced = (
<div className="mx_SettingsTab_section"> <div className="mx_SettingsTab_section">
<AccessibleButton <AccessibleButton

View file

@ -18,6 +18,7 @@ import React, { useCallback, useMemo, useState } from "react";
import { JoinRule } from "matrix-js-sdk/src/@types/partials"; import { JoinRule } from "matrix-js-sdk/src/@types/partials";
import { EventType } from "matrix-js-sdk/src/@types/event"; import { EventType } from "matrix-js-sdk/src/@types/event";
import { RoomState } from "matrix-js-sdk/src/models/room-state"; import { RoomState } from "matrix-js-sdk/src/models/room-state";
import { Room } from "matrix-js-sdk/src/matrix";
import { _t } from "../../../../../languageHandler"; import { _t } from "../../../../../languageHandler";
import { MatrixClientPeg } from "../../../../../MatrixClientPeg"; import { MatrixClientPeg } from "../../../../../MatrixClientPeg";
@ -29,14 +30,13 @@ import { useRoomState } from "../../../../../hooks/useRoomState";
import SdkConfig, { DEFAULTS } from "../../../../../SdkConfig"; import SdkConfig, { DEFAULTS } from "../../../../../SdkConfig";
interface ElementCallSwitchProps { interface ElementCallSwitchProps {
roomId: string; room: Room;
} }
const ElementCallSwitch: React.FC<ElementCallSwitchProps> = ({ roomId }) => { const ElementCallSwitch: React.FC<ElementCallSwitchProps> = ({ room }) => {
const room = useMemo(() => MatrixClientPeg.get().getRoom(roomId), [roomId]); const isPublic = useMemo(() => room.getJoinRule() === JoinRule.Public, [room]);
const isPublic = useMemo(() => room?.getJoinRule() === JoinRule.Public, [room]);
const [content, events, maySend] = useRoomState( const [content, events, maySend] = useRoomState(
room ?? undefined, room,
useCallback((state: RoomState) => { useCallback((state: RoomState) => {
const content = state?.getStateEvents(EventType.RoomPowerLevels, "")?.getContent(); const content = state?.getStateEvents(EventType.RoomPowerLevels, "")?.getContent();
return [ return [
@ -68,12 +68,12 @@ const ElementCallSwitch: React.FC<ElementCallSwitchProps> = ({ roomId }) => {
events[ElementCall.MEMBER_EVENT_TYPE.name] = adminLevel; events[ElementCall.MEMBER_EVENT_TYPE.name] = adminLevel;
} }
MatrixClientPeg.get().sendStateEvent(roomId, EventType.RoomPowerLevels, { MatrixClientPeg.get().sendStateEvent(room.roomId, EventType.RoomPowerLevels, {
events: events, events: events,
...content, ...content,
}); });
}, },
[roomId, content, events, isPublic], [room.roomId, content, events, isPublic],
); );
const brand = SdkConfig.get("element_call").brand ?? DEFAULTS.element_call.brand; const brand = SdkConfig.get("element_call").brand ?? DEFAULTS.element_call.brand;
@ -95,14 +95,14 @@ const ElementCallSwitch: React.FC<ElementCallSwitchProps> = ({ roomId }) => {
}; };
interface Props { interface Props {
roomId: string; room: Room;
} }
export const VoipRoomSettingsTab: React.FC<Props> = ({ roomId }) => { export const VoipRoomSettingsTab: React.FC<Props> = ({ room }) => {
return ( return (
<SettingsTab heading={_t("Voice & Video")}> <SettingsTab heading={_t("Voice & Video")}>
<SettingsSubsection heading={_t("Call type")}> <SettingsSubsection heading={_t("Call type")}>
<ElementCallSwitch roomId={roomId} /> <ElementCallSwitch room={room} />
</SettingsSubsection> </SettingsSubsection>
</SettingsTab> </SettingsTab>
); );

View file

@ -38,24 +38,46 @@ describe("<RoomSettingsDialog />", () => {
const roomId = "!room:server.org"; const roomId = "!room:server.org";
const room = new Room(roomId, mockClient, userId); const room = new Room(roomId, mockClient, userId);
room.name = "Test Room";
const room2 = new Room("!room2:server.org", mockClient, userId);
room2.name = "Another Room";
jest.spyOn(SettingsStore, "getValue"); jest.spyOn(SettingsStore, "getValue");
beforeEach(() => { beforeEach(() => {
jest.clearAllMocks(); jest.clearAllMocks();
mockClient.getRoom.mockReturnValue(room); mockClient.getRoom.mockImplementation((roomId) => {
if (roomId === room.roomId) return room;
if (roomId === room2.roomId) return room2;
return null;
});
jest.spyOn(SettingsStore, "getValue").mockReset().mockReturnValue(false); jest.spyOn(SettingsStore, "getValue").mockReset().mockReturnValue(false);
}); });
const getComponent = (onFinished = jest.fn()) => const getComponent = (onFinished = jest.fn(), propRoomId = roomId) =>
render(<RoomSettingsDialog roomId={roomId} onFinished={onFinished} />, { render(<RoomSettingsDialog roomId={propRoomId} onFinished={onFinished} />, {
wrapper: ({ children }) => ( wrapper: ({ children }) => (
<MatrixClientContext.Provider value={mockClient}>{children}</MatrixClientContext.Provider> <MatrixClientContext.Provider value={mockClient}>{children}</MatrixClientContext.Provider>
), ),
}); });
it("catches errors when room is not found", () => {
getComponent(undefined, "!room-that-does-not-exist");
expect(screen.getByText("Something went wrong!")).toBeInTheDocument();
});
it("updates when roomId prop changes", () => {
const { rerender, getByText } = getComponent(undefined, roomId);
expect(getByText(`Room Settings - ${room.name}`)).toBeInTheDocument();
rerender(<RoomSettingsDialog roomId={room2.roomId} onFinished={jest.fn()} />);
expect(getByText(`Room Settings - ${room2.name}`)).toBeInTheDocument();
});
describe("Settings tabs", () => { describe("Settings tabs", () => {
it("renders default tabs correctly", () => { it("renders default tabs correctly", () => {
const { container } = getComponent(); const { container } = getComponent();

View file

@ -15,7 +15,7 @@ limitations under the License.
*/ */
import React from "react"; import React from "react";
import { fireEvent, render, RenderResult } from "@testing-library/react"; import { fireEvent, render, RenderResult, screen } from "@testing-library/react";
import { MatrixClient } from "matrix-js-sdk/src/client"; import { MatrixClient } from "matrix-js-sdk/src/client";
import { Room } from "matrix-js-sdk/src/models/room"; import { Room } from "matrix-js-sdk/src/models/room";
import { mocked } from "jest-mock"; import { mocked } from "jest-mock";
@ -36,7 +36,7 @@ describe("AdvancedRoomSettingsTab", () => {
let room: Room; let room: Room;
const renderTab = (): RenderResult => { const renderTab = (): RenderResult => {
return render(<AdvancedRoomSettingsTab roomId={roomId} closeSettingsFn={jest.fn()} />); return render(<AdvancedRoomSettingsTab room={room} closeSettingsFn={jest.fn()} />);
}; };
beforeEach(() => { beforeEach(() => {
@ -69,6 +69,22 @@ describe("AdvancedRoomSettingsTab", () => {
tab.getByText("custom_room_version_1"); tab.getByText("custom_room_version_1");
}); });
it("displays message when room cannot federate", () => {
const createEvent = new MatrixEvent({
sender: "@a:b.com",
type: EventType.RoomCreate,
content: { "m.federate": false },
room_id: room.roomId,
state_key: "",
});
jest.spyOn(room.currentState, "getStateEvents").mockImplementation((type) =>
type === EventType.RoomCreate ? createEvent : null,
);
renderTab();
expect(screen.getByText("This room is not accessible by remote Matrix servers")).toBeInTheDocument();
});
function mockStateEvents(room: Room) { function mockStateEvents(room: Room) {
const createEvent = mkEvent({ const createEvent = mkEvent({
event: true, event: true,
@ -143,5 +159,16 @@ describe("AdvancedRoomSettingsTab", () => {
metricsViaKeyboard: false, metricsViaKeyboard: false,
}); });
}); });
it("handles when room is a space", async () => {
mockStateEvents(room);
jest.spyOn(room, "isSpaceRoom").mockReturnValue(true);
mockStateEvents(room);
const tab = renderTab();
const link = await tab.findByText("View older version of test room.");
expect(link).toBeInTheDocument();
expect(screen.getByText("Space information")).toBeInTheDocument();
});
}); });
}); });

View file

@ -0,0 +1,60 @@
/*
Copyright 2023 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 { render } from "@testing-library/react";
import { MatrixEvent, Room } from "matrix-js-sdk/src/matrix";
import BridgeSettingsTab from "../../../../../../src/components/views/settings/tabs/room/BridgeSettingsTab";
import { getMockClientWithEventEmitter } from "../../../../../test-utils";
describe("<BridgeSettingsTab />", () => {
const userId = "@alice:server.org";
const client = getMockClientWithEventEmitter({
getRoom: jest.fn(),
});
const roomId = "!room:server.org";
const getComponent = (room: Room) => render(<BridgeSettingsTab room={room} />);
it("renders when room is not bridging messages to any platform", () => {
const room = new Room(roomId, client, userId);
const { container } = getComponent(room);
expect(container).toMatchSnapshot();
});
it("renders when room is bridging messages", () => {
const bridgeEvent = new MatrixEvent({
type: "uk.half-shot.bridge",
content: {
channel: { id: "channel-test" },
protocol: { id: "protocol-test" },
bridgebot: "test",
},
room_id: roomId,
state_key: "1",
});
const room = new Room(roomId, client, userId);
room.currentState.setStateEvents([bridgeEvent]);
client.getRoom.mockReturnValue(room);
const { container } = getComponent(room);
expect(container).toMatchSnapshot();
});
});

View file

@ -15,12 +15,13 @@ limitations under the License.
*/ */
import React from "react"; import React from "react";
import { fireEvent, render, RenderResult } from "@testing-library/react"; import { fireEvent, render, RenderResult, screen } from "@testing-library/react";
import { MatrixClient } from "matrix-js-sdk/src/client"; import { MatrixClient } from "matrix-js-sdk/src/client";
import { EventType } from "matrix-js-sdk/src/@types/event"; import { EventType } from "matrix-js-sdk/src/@types/event";
import { MatrixEvent } from "matrix-js-sdk/src/models/event"; import { MatrixEvent } from "matrix-js-sdk/src/models/event";
import { Room } from "matrix-js-sdk/src/models/room"; import { Room } from "matrix-js-sdk/src/models/room";
import { mocked } from "jest-mock"; import { mocked } from "jest-mock";
import { RoomMember } from "matrix-js-sdk/src/matrix";
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";
@ -30,12 +31,13 @@ import SettingsStore from "../../../../../../src/settings/SettingsStore";
import { ElementCall } from "../../../../../../src/models/Call"; import { ElementCall } from "../../../../../../src/models/Call";
describe("RolesRoomSettingsTab", () => { describe("RolesRoomSettingsTab", () => {
const userId = "@alice:server.org";
const roomId = "!room:example.com"; const roomId = "!room:example.com";
let cli: MatrixClient; let cli: MatrixClient;
let room: Room; let room: Room;
const renderTab = (): RenderResult => { const renderTab = (propRoom: Room = room): RenderResult => {
return render(<RolesRoomSettingsTab roomId={roomId} />); return render(<RolesRoomSettingsTab room={propRoom} />);
}; };
const getVoiceBroadcastsSelect = (): HTMLElement => { const getVoiceBroadcastsSelect = (): HTMLElement => {
@ -183,4 +185,54 @@ describe("RolesRoomSettingsTab", () => {
expect(getJoinCallSelectedOption(tab)).toBeFalsy(); expect(getJoinCallSelectedOption(tab)).toBeFalsy();
}); });
}); });
describe("Banned users", () => {
it("should not render banned section when no banned users", () => {
const room = new Room(roomId, cli, userId);
renderTab(room);
expect(screen.queryByText("Banned users")).not.toBeInTheDocument();
});
it("renders banned users", () => {
const bannedMember = new RoomMember(roomId, "@bob:server.org");
bannedMember.setMembershipEvent(
new MatrixEvent({
type: EventType.RoomMember,
content: {
membership: "ban",
reason: "just testing",
},
sender: userId,
}),
);
const room = new Room(roomId, cli, userId);
jest.spyOn(room, "getMembersWithMembership").mockReturnValue([bannedMember]);
renderTab(room);
expect(screen.getByText("Banned users").parentElement).toMatchSnapshot();
});
it("uses banners display name when available", () => {
const bannedMember = new RoomMember(roomId, "@bob:server.org");
const senderMember = new RoomMember(roomId, "@alice:server.org");
senderMember.name = "Alice";
bannedMember.setMembershipEvent(
new MatrixEvent({
type: EventType.RoomMember,
content: {
membership: "ban",
reason: "just testing",
},
sender: userId,
}),
);
const room = new Room(roomId, cli, userId);
jest.spyOn(room, "getMembersWithMembership").mockReturnValue([bannedMember]);
jest.spyOn(room, "getMember").mockReturnValue(senderMember);
renderTab(room);
expect(screen.getByTitle("Banned by Alice")).toBeInTheDocument();
});
});
}); });

View file

@ -0,0 +1,402 @@
/*
Copyright 2023 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, screen, within } from "@testing-library/react";
import { EventType, GuestAccess, HistoryVisibility, JoinRule, MatrixEvent, Room } from "matrix-js-sdk/src/matrix";
import { logger } from "matrix-js-sdk/src/logger";
import SecurityRoomSettingsTab from "../../../../../../src/components/views/settings/tabs/room/SecurityRoomSettingsTab";
import MatrixClientContext from "../../../../../../src/contexts/MatrixClientContext";
import SettingsStore from "../../../../../../src/settings/SettingsStore";
import { flushPromises, getMockClientWithEventEmitter, mockClientMethodsUser } from "../../../../../test-utils";
import { filterBoolean } from "../../../../../../src/utils/arrays";
describe("<SecurityRoomSettingsTab />", () => {
const userId = "@alice:server.org";
const client = getMockClientWithEventEmitter({
...mockClientMethodsUser(userId),
getRoom: jest.fn(),
isRoomEncrypted: jest.fn(),
getLocalAliases: jest.fn().mockReturnValue([]),
sendStateEvent: jest.fn(),
});
const roomId = "!room:server.org";
const getComponent = (room: Room, closeSettingsFn = jest.fn()) =>
render(<SecurityRoomSettingsTab room={room} closeSettingsFn={closeSettingsFn} />, {
wrapper: ({ children }) => (
<MatrixClientContext.Provider value={client}>{children}</MatrixClientContext.Provider>
),
});
const setRoomStateEvents = (
room: Room,
joinRule?: JoinRule,
guestAccess?: GuestAccess,
history?: HistoryVisibility,
): void => {
const events = filterBoolean<MatrixEvent>([
new MatrixEvent({
type: EventType.RoomCreate,
content: { version: "test" },
sender: userId,
state_key: "",
room_id: room.roomId,
}),
guestAccess &&
new MatrixEvent({
type: EventType.RoomGuestAccess,
content: { guest_access: guestAccess },
sender: userId,
state_key: "",
room_id: room.roomId,
}),
history &&
new MatrixEvent({
type: EventType.RoomHistoryVisibility,
content: { history_visibility: history },
sender: userId,
state_key: "",
room_id: room.roomId,
}),
joinRule &&
new MatrixEvent({
type: EventType.RoomJoinRules,
content: { join_rule: joinRule },
sender: userId,
state_key: "",
room_id: room.roomId,
}),
]);
room.currentState.setStateEvents(events);
};
beforeEach(() => {
client.sendStateEvent.mockReset().mockResolvedValue({ event_id: "test" });
client.isRoomEncrypted.mockReturnValue(false);
jest.spyOn(SettingsStore, "getValue").mockRestore();
});
describe("join rule", () => {
it("warns when trying to make an encrypted room public", async () => {
const room = new Room(roomId, client, userId);
client.isRoomEncrypted.mockReturnValue(true);
setRoomStateEvents(room, JoinRule.Invite);
getComponent(room);
fireEvent.click(screen.getByLabelText("Public"));
const modal = await screen.findByRole("dialog");
expect(modal).toMatchSnapshot();
fireEvent.click(screen.getByText("Cancel"));
// join rule not updated
expect(screen.getByLabelText("Private (invite only)").hasAttribute("checked")).toBeTruthy();
});
it("updates join rule", async () => {
const room = new Room(roomId, client, userId);
setRoomStateEvents(room, JoinRule.Invite);
getComponent(room);
fireEvent.click(screen.getByLabelText("Public"));
await flushPromises();
expect(client.sendStateEvent).toHaveBeenCalledWith(
room.roomId,
EventType.RoomJoinRules,
{
join_rule: JoinRule.Public,
},
"",
);
});
it("handles error when updating join rule fails", async () => {
const room = new Room(roomId, client, userId);
client.sendStateEvent.mockRejectedValue("oups");
setRoomStateEvents(room, JoinRule.Invite);
getComponent(room);
fireEvent.click(screen.getByLabelText("Public"));
await flushPromises();
const dialog = await screen.findByRole("dialog");
expect(dialog).toMatchSnapshot();
fireEvent.click(within(dialog).getByText("OK"));
});
it("displays advanced section toggle when join rule is public", () => {
const room = new Room(roomId, client, userId);
setRoomStateEvents(room, JoinRule.Public);
getComponent(room);
expect(screen.getByText("Show advanced")).toBeInTheDocument();
});
it("does not display advanced section toggle when join rule is not public", () => {
const room = new Room(roomId, client, userId);
setRoomStateEvents(room, JoinRule.Invite);
getComponent(room);
expect(screen.queryByText("Show advanced")).not.toBeInTheDocument();
});
});
describe("guest access", () => {
it("uses forbidden by default when room has no guest access event", () => {
const room = new Room(roomId, client, userId);
setRoomStateEvents(room, JoinRule.Public);
getComponent(room);
fireEvent.click(screen.getByText("Show advanced"));
expect(screen.getByLabelText("Enable guest access").getAttribute("aria-checked")).toBe("false");
});
it("updates guest access on toggle", () => {
const room = new Room(roomId, client, userId);
setRoomStateEvents(room, JoinRule.Public);
getComponent(room);
fireEvent.click(screen.getByText("Show advanced"));
fireEvent.click(screen.getByLabelText("Enable guest access"));
// toggle set immediately
expect(screen.getByLabelText("Enable guest access").getAttribute("aria-checked")).toBe("true");
expect(client.sendStateEvent).toHaveBeenCalledWith(
room.roomId,
EventType.RoomGuestAccess,
{ guest_access: GuestAccess.CanJoin },
"",
);
});
it("logs error and resets state when updating guest access fails", async () => {
client.sendStateEvent.mockRejectedValue("oups");
jest.spyOn(logger, "error").mockImplementation(() => {});
const room = new Room(roomId, client, userId);
setRoomStateEvents(room, JoinRule.Public, GuestAccess.CanJoin);
getComponent(room);
fireEvent.click(screen.getByText("Show advanced"));
fireEvent.click(screen.getByLabelText("Enable guest access"));
// toggle set immediately
expect(screen.getByLabelText("Enable guest access").getAttribute("aria-checked")).toBe("false");
await flushPromises();
expect(client.sendStateEvent).toHaveBeenCalled();
expect(logger.error).toHaveBeenCalledWith("oups");
// toggle reset to old value
expect(screen.getByLabelText("Enable guest access").getAttribute("aria-checked")).toBe("true");
});
});
describe("history visibility", () => {
it("does not render section when RoomHistorySettings feature is disabled", () => {
jest.spyOn(SettingsStore, "getValue").mockReturnValue(false);
const room = new Room(roomId, client, userId);
setRoomStateEvents(room);
getComponent(room);
expect(screen.queryByText("Who can read history")).not.toBeInTheDocument();
});
it("uses shared as default history visibility when no state event found", () => {
const room = new Room(roomId, client, userId);
setRoomStateEvents(room);
getComponent(room);
expect(screen.getByText("Who can read history?").parentElement).toMatchSnapshot();
expect(screen.getByDisplayValue(HistoryVisibility.Shared)).toBeChecked();
});
it("does not render world readable option when room is encrypted", () => {
const room = new Room(roomId, client, userId);
client.isRoomEncrypted.mockReturnValue(true);
setRoomStateEvents(room);
getComponent(room);
expect(screen.queryByDisplayValue(HistoryVisibility.WorldReadable)).not.toBeInTheDocument();
});
it("renders world readable option when room is encrypted and history is already set to world readable", () => {
const room = new Room(roomId, client, userId);
client.isRoomEncrypted.mockReturnValue(true);
setRoomStateEvents(room, undefined, undefined, HistoryVisibility.WorldReadable);
getComponent(room);
expect(screen.getByDisplayValue(HistoryVisibility.WorldReadable)).toBeInTheDocument();
});
it("updates history visibility", () => {
const room = new Room(roomId, client, userId);
getComponent(room);
fireEvent.click(screen.getByDisplayValue(HistoryVisibility.Invited));
// toggle updated immediately
expect(screen.getByDisplayValue(HistoryVisibility.Invited)).toBeChecked();
expect(client.sendStateEvent).toHaveBeenCalledWith(
room.roomId,
EventType.RoomHistoryVisibility,
{
history_visibility: HistoryVisibility.Invited,
},
"",
);
});
it("handles error when updating history visibility", async () => {
const room = new Room(roomId, client, userId);
client.sendStateEvent.mockRejectedValue("oups");
jest.spyOn(logger, "error").mockImplementation(() => {});
getComponent(room);
fireEvent.click(screen.getByDisplayValue(HistoryVisibility.Invited));
// toggle updated immediately
expect(screen.getByDisplayValue(HistoryVisibility.Invited)).toBeChecked();
await flushPromises();
// reset to before updated value
expect(screen.getByDisplayValue(HistoryVisibility.Shared)).toBeChecked();
expect(logger.error).toHaveBeenCalledWith("oups");
});
});
describe("encryption", () => {
it("displays encryption as enabled", () => {
const room = new Room(roomId, client, userId);
client.isRoomEncrypted.mockReturnValue(true);
setRoomStateEvents(room);
getComponent(room);
expect(screen.getByLabelText("Encrypted")).toBeChecked();
// can't disable encryption once enabled
expect(screen.getByLabelText("Encrypted").getAttribute("aria-disabled")).toEqual("true");
});
it("asks users to confirm when setting room to encrypted", async () => {
const room = new Room(roomId, client, userId);
setRoomStateEvents(room);
getComponent(room);
expect(screen.getByLabelText("Encrypted")).not.toBeChecked();
fireEvent.click(screen.getByLabelText("Encrypted"));
const dialog = await screen.findByRole("dialog");
fireEvent.click(within(dialog).getByText("Cancel"));
expect(client.sendStateEvent).not.toHaveBeenCalled();
expect(screen.getByLabelText("Encrypted")).not.toBeChecked();
});
it("enables encryption after confirmation", async () => {
const room = new Room(roomId, client, userId);
setRoomStateEvents(room);
getComponent(room);
expect(screen.getByLabelText("Encrypted")).not.toBeChecked();
fireEvent.click(screen.getByLabelText("Encrypted"));
const dialog = await screen.findByRole("dialog");
fireEvent.click(within(dialog).getByText("OK"));
expect(client.sendStateEvent).toHaveBeenCalledWith(room.roomId, EventType.RoomEncryption, {
algorithm: "m.megolm.v1.aes-sha2",
});
});
it("renders world readable option when room is encrypted and history is already set to world readable", () => {
const room = new Room(roomId, client, userId);
client.isRoomEncrypted.mockReturnValue(true);
setRoomStateEvents(room, undefined, undefined, HistoryVisibility.WorldReadable);
getComponent(room);
expect(screen.getByDisplayValue(HistoryVisibility.WorldReadable)).toBeInTheDocument();
});
it("updates history visibility", () => {
const room = new Room(roomId, client, userId);
getComponent(room);
fireEvent.click(screen.getByDisplayValue(HistoryVisibility.Invited));
// toggle updated immediately
expect(screen.getByDisplayValue(HistoryVisibility.Invited)).toBeChecked();
expect(client.sendStateEvent).toHaveBeenCalledWith(
room.roomId,
EventType.RoomHistoryVisibility,
{
history_visibility: HistoryVisibility.Invited,
},
"",
);
});
it("handles error when updating history visibility", async () => {
const room = new Room(roomId, client, userId);
client.sendStateEvent.mockRejectedValue("oups");
jest.spyOn(logger, "error").mockImplementation(() => {});
getComponent(room);
fireEvent.click(screen.getByDisplayValue(HistoryVisibility.Invited));
// toggle updated immediately
expect(screen.getByDisplayValue(HistoryVisibility.Invited)).toBeChecked();
await flushPromises();
// reset to before updated value
expect(screen.getByDisplayValue(HistoryVisibility.Shared)).toBeChecked();
expect(logger.error).toHaveBeenCalledWith("oups");
});
});
});

View file

@ -27,13 +27,13 @@ import { MatrixClientPeg } from "../../../../../../src/MatrixClientPeg";
import { VoipRoomSettingsTab } from "../../../../../../src/components/views/settings/tabs/room/VoipRoomSettingsTab"; import { VoipRoomSettingsTab } from "../../../../../../src/components/views/settings/tabs/room/VoipRoomSettingsTab";
import { ElementCall } from "../../../../../../src/models/Call"; import { ElementCall } from "../../../../../../src/models/Call";
describe("RolesRoomSettingsTab", () => { describe("VoipRoomSettingsTab", () => {
const roomId = "!room:example.com"; const roomId = "!room:example.com";
let cli: MatrixClient; let cli: MatrixClient;
let room: Room; let room: Room;
const renderTab = (): RenderResult => { const renderTab = (): RenderResult => {
return render(<VoipRoomSettingsTab roomId={roomId} />); return render(<VoipRoomSettingsTab room={room} />);
}; };
beforeEach(() => { beforeEach(() => {

View file

@ -0,0 +1,112 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`<BridgeSettingsTab /> renders when room is bridging messages 1`] = `
<div>
<div
class="mx_SettingsTab"
>
<div
class="mx_SettingsTab_heading"
>
Bridges
</div>
<div
class="mx_SettingsTab_section mx_SettingsTab_subsectionText"
>
<div>
<p>
<span>
This room is bridging messages to the following platforms.
<a
href="https://matrix.org/bridges/"
rel="noreferrer noopener"
target="_blank"
>
Learn more.
</a>
</span>
</p>
<ul
class="mx_RoomSettingsDialog_BridgeList"
>
<li
class="mx_RoomSettingsDialog_BridgeList_listItem"
>
<div
class="mx_RoomSettingsDialog_column_icon"
>
<div
class="mx_RoomSettingsDialog_noProtocolIcon"
/>
</div>
<div
class="mx_RoomSettingsDialog_column_data"
>
<h3
class="mx_RoomSettingsDialog_column_data_protocolName"
>
protocol-test
</h3>
<p
class="mx_RoomSettingsDialog_column_data_details mx_RoomSettingsDialog_workspace_channel_details"
>
<span
class="mx_RoomSettingsDialog_channel"
>
<span>
Channel:
<span>
channel-test
</span>
</span>
</span>
</p>
<ul
class="mx_RoomSettingsDialog_column_data_metadata mx_RoomSettingsDialog_metadata"
>
<li>
<span>
This bridge is managed by
.
</span>
</li>
</ul>
</div>
</li>
</ul>
</div>
</div>
</div>
</div>
`;
exports[`<BridgeSettingsTab /> renders when room is not bridging messages to any platform 1`] = `
<div>
<div
class="mx_SettingsTab"
>
<div
class="mx_SettingsTab_heading"
>
Bridges
</div>
<div
class="mx_SettingsTab_section mx_SettingsTab_subsectionText"
>
<p>
<span>
This room isn't bridging messages to any platforms.
<a
href="https://matrix.org/bridges/"
rel="noreferrer noopener"
target="_blank"
>
Learn more.
</a>
</span>
</p>
</div>
</div>
</div>
`;

View file

@ -0,0 +1,26 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`RolesRoomSettingsTab Banned users renders banned users 1`] = `
<fieldset
class="mx_SettingsFieldset"
>
<legend
class="mx_SettingsFieldset_legend"
>
Banned users
</legend>
<ul>
<li>
<span
title="Banned by @alice:server.org"
>
<strong>
@bob:server.org
</strong>
Reason: just testing
</span>
</li>
</ul>
</fieldset>
`;

View file

@ -0,0 +1,227 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`<SecurityRoomSettingsTab /> history visibility uses shared as default history visibility when no state event found 1`] = `
<fieldset
class="mx_SettingsFieldset"
>
<legend
class="mx_SettingsFieldset_legend"
>
Who can read history?
</legend>
<div
class="mx_SettingsFieldset_description"
>
Changes to who can read history will only apply to future messages in this room. The visibility of existing history will be unchanged.
</div>
<label
class="mx_StyledRadioButton mx_StyledRadioButton_enabled"
>
<input
id="historyVis-world_readable"
name="historyVis"
type="radio"
value="world_readable"
/>
<div>
<div />
</div>
<div
class="mx_StyledRadioButton_content"
>
Anyone
</div>
<div
class="mx_StyledRadioButton_spacer"
/>
</label>
<label
class="mx_StyledRadioButton mx_StyledRadioButton_enabled mx_StyledRadioButton_checked"
>
<input
checked=""
id="historyVis-shared"
name="historyVis"
type="radio"
value="shared"
/>
<div>
<div />
</div>
<div
class="mx_StyledRadioButton_content"
>
Members only (since the point in time of selecting this option)
</div>
<div
class="mx_StyledRadioButton_spacer"
/>
</label>
<label
class="mx_StyledRadioButton mx_StyledRadioButton_enabled"
>
<input
id="historyVis-invited"
name="historyVis"
type="radio"
value="invited"
/>
<div>
<div />
</div>
<div
class="mx_StyledRadioButton_content"
>
Members only (since they were invited)
</div>
<div
class="mx_StyledRadioButton_spacer"
/>
</label>
<label
class="mx_StyledRadioButton mx_StyledRadioButton_enabled"
>
<input
id="historyVis-joined"
name="historyVis"
type="radio"
value="joined"
/>
<div>
<div />
</div>
<div
class="mx_StyledRadioButton_content"
>
Members only (since they joined)
</div>
<div
class="mx_StyledRadioButton_spacer"
/>
</label>
</fieldset>
`;
exports[`<SecurityRoomSettingsTab /> join rule handles error when updating join rule fails 1`] = `
<div
aria-describedby="mx_Dialog_content"
aria-labelledby="mx_BaseDialog_title"
class="mx_ErrorDialog mx_Dialog_fixedWidth"
data-focus-lock-disabled="false"
role="dialog"
>
<div
class="mx_Dialog_header mx_Dialog_headerWithCancel"
>
<h2
class="mx_Heading_h2 mx_Dialog_title"
id="mx_BaseDialog_title"
>
Failed to update the join rules
</h2>
<div
aria-label="Close dialog"
class="mx_AccessibleButton mx_Dialog_cancelButton"
role="button"
tabindex="0"
/>
</div>
<div
class="mx_Dialog_content"
id="mx_Dialog_content"
>
Unknown failure
</div>
<div
class="mx_Dialog_buttons"
>
<button
class="mx_Dialog_primary"
>
OK
</button>
</div>
</div>
`;
exports[`<SecurityRoomSettingsTab /> join rule warns when trying to make an encrypted room public 1`] = `
<div
aria-describedby="mx_Dialog_content"
aria-labelledby="mx_BaseDialog_title"
class="mx_QuestionDialog mx_Dialog_fixedWidth"
data-focus-lock-disabled="false"
role="dialog"
>
<div
class="mx_Dialog_header mx_Dialog_headerWithCancel"
>
<h2
class="mx_Heading_h2 mx_Dialog_title"
id="mx_BaseDialog_title"
>
Are you sure you want to make this encrypted room public?
</h2>
<div
aria-label="Close dialog"
class="mx_AccessibleButton mx_Dialog_cancelButton"
role="button"
tabindex="0"
/>
</div>
<div
class="mx_Dialog_content"
id="mx_Dialog_content"
>
<div>
<p>
<span>
<b>
It's not recommended to make encrypted rooms public.
</b>
It will mean anyone can find and join the room, so anyone can read messages. You'll get none of the benefits of encryption. Encrypting messages in a public room will make receiving and sending messages slower.
</span>
</p>
<p>
<span>
To avoid these issues, create a
<div
class="mx_AccessibleButton mx_AccessibleButton_hasKind mx_AccessibleButton_kind_link_inline"
role="button"
tabindex="0"
>
new public room
</div>
for the conversation you plan to have.
</span>
</p>
</div>
</div>
<div
class="mx_Dialog_buttons"
>
<span
class="mx_Dialog_buttons_row"
>
<button
data-testid="dialog-cancel-button"
type="button"
>
Cancel
</button>
<button
class="mx_Dialog_primary"
data-testid="dialog-primary-button"
type="button"
>
OK
</button>
</span>
</div>
</div>
`;