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 Michael Telatynski <7t3chguy@gmail.com>
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.
@ -16,7 +18,7 @@ limitations under the License.
*/
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 { _t, _td } from "../../../languageHandler";
@ -36,6 +38,7 @@ import { VoipRoomSettingsTab } from "../settings/tabs/room/VoipRoomSettingsTab";
import { ActionPayload } from "../../../dispatcher/payloads";
import { NonEmptyArray } from "../../../@types/common";
import { PollHistoryTab } from "../settings/tabs/room/PollHistoryTab";
import ErrorBoundary from "../elements/ErrorBoundary";
export const ROOM_GENERAL_TAB = "ROOM_GENERAL_TAB";
export const ROOM_VOIP_TAB = "ROOM_VOIP_TAB";
@ -53,15 +56,17 @@ interface IProps {
}
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;
public constructor(props: IProps) {
super(props);
this.state = { roomName: "" };
const room = this.getRoom();
this.state = { room };
}
public componentDidMount(): void {
@ -70,6 +75,13 @@ export default class RoomSettingsDialog extends React.Component<IProps, IState>
this.onRoomName();
}
public componentDidUpdate(): void {
if (this.props.roomId !== this.state.room.roomId) {
const room = this.getRoom();
this.setState({ room });
}
}
public componentWillUnmount(): void {
if (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);
}
/**
* 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 => {
// When view changes below us, close the room settings
// 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 => {
this.setState({
roomName: MatrixClientPeg.get().getRoom(this.props.roomId)?.name ?? "",
});
// rerender when the room name changes
this.forceUpdate();
};
private getTabs(): NonEmptyArray<Tab> {
@ -100,7 +126,7 @@ export default class RoomSettingsDialog extends React.Component<IProps, IState>
ROOM_GENERAL_TAB,
_td("General"),
"mx_RoomSettingsDialog_settingsIcon",
<GeneralRoomSettingsTab roomId={this.props.roomId} />,
<GeneralRoomSettingsTab room={this.state.room} />,
"RoomSettingsGeneral",
),
);
@ -110,7 +136,7 @@ export default class RoomSettingsDialog extends React.Component<IProps, IState>
ROOM_VOIP_TAB,
_td("Voice & Video"),
"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,
_td("Security & Privacy"),
"mx_RoomSettingsDialog_securityIcon",
(
<SecurityRoomSettingsTab
roomId={this.props.roomId}
closeSettingsFn={() => this.props.onFinished(true)}
/>
),
<SecurityRoomSettingsTab room={this.state.room} closeSettingsFn={() => this.props.onFinished(true)} />,
"RoomSettingsSecurityPrivacy",
),
);
@ -133,7 +154,7 @@ export default class RoomSettingsDialog extends React.Component<IProps, IState>
ROOM_ROLES_TAB,
_td("Roles & Permissions"),
"mx_RoomSettingsDialog_rolesIcon",
<RolesRoomSettingsTab roomId={this.props.roomId} />,
<RolesRoomSettingsTab room={this.state.room} />,
"RoomSettingsRolesPermissions",
),
);
@ -144,7 +165,7 @@ export default class RoomSettingsDialog extends React.Component<IProps, IState>
"mx_RoomSettingsDialog_notificationsIcon",
(
<NotificationSettingsTab
roomId={this.props.roomId}
roomId={this.state.room.roomId}
closeSettingsFn={() => this.props.onFinished(true)}
/>
),
@ -158,7 +179,7 @@ export default class RoomSettingsDialog extends React.Component<IProps, IState>
ROOM_BRIDGES_TAB,
_td("Bridges"),
"mx_RoomSettingsDialog_bridgesIcon",
<BridgeSettingsTab roomId={this.props.roomId} />,
<BridgeSettingsTab room={this.state.room} />,
"RoomSettingsBridges",
),
);
@ -169,7 +190,7 @@ export default class RoomSettingsDialog extends React.Component<IProps, IState>
ROOM_POLL_HISTORY_TAB,
_td("Poll history"),
"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",
(
<AdvancedRoomSettingsTab
roomId={this.props.roomId}
room={this.state.room}
closeSettingsFn={() => this.props.onFinished(true)}
/>
),
@ -194,7 +215,7 @@ export default class RoomSettingsDialog extends React.Component<IProps, IState>
}
public render(): React.ReactNode {
const roomName = this.state.roomName;
const roomName = this.state.room.name;
return (
<BaseDialog
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,
_td("Roles & Permissions"),
"mx_RoomSettingsDialog_rolesIcon",
<RolesRoomSettingsTab roomId={space.roomId} />,
<RolesRoomSettingsTab room={space} />,
),
SettingsStore.getValue(UIFeature.AdvancedSettings)
? new Tab(
SpaceSettingsTab.Advanced,
_td("Advanced"),
"mx_RoomSettingsDialog_warningIcon",
<AdvancedRoomSettingsTab roomId={space.roomId} closeSettingsFn={onFinished} />,
<AdvancedRoomSettingsTab room={space} closeSettingsFn={onFinished} />,
)
: null,
].filter(Boolean) as NonEmptyArray<Tab>;

View File

@ -16,9 +16,9 @@ limitations under the License.
import React from "react";
import { EventType } from "matrix-js-sdk/src/@types/event";
import { Room } from "matrix-js-sdk/src/matrix";
import { _t } from "../../../../../languageHandler";
import { MatrixClientPeg } from "../../../../../MatrixClientPeg";
import AccessibleButton, { ButtonEvent } from "../../../elements/AccessibleButton";
import RoomUpgradeDialog from "../../../dialogs/RoomUpgradeDialog";
import Modal from "../../../../../Modal";
@ -29,7 +29,7 @@ import { ViewRoomPayload } from "../../../../../dispatcher/payloads/ViewRoomPayl
import SettingsStore from "../../../../../settings/SettingsStore";
interface IProps {
roomId: string;
room: Room;
closeSettingsFn(): void;
}
@ -64,8 +64,8 @@ export default class AdvancedRoomSettingsTab extends React.Component<IProps, ISt
this.state = {};
// we handle lack of this object gracefully later, so don't worry about it failing here.
const room = MatrixClientPeg.get().getRoom(this.props.roomId);
room?.getRecommendedVersion().then((v) => {
const room = this.props.room;
room.getRecommendedVersion().then((v) => {
const tombstone = room.currentState.getStateEvents(EventType.RoomTombstone, "");
const additionalStateChanges: Partial<IState> = {};
@ -85,8 +85,7 @@ export default class AdvancedRoomSettingsTab extends React.Component<IProps, ISt
}
private upgradeRoom = (): void => {
const room = MatrixClientPeg.get().getRoom(this.props.roomId);
if (room) Modal.createDialog(RoomUpgradeDialog, { room });
Modal.createDialog(RoomUpgradeDialog, { room: this.props.room });
};
private onOldRoomClicked = (e: ButtonEvent): void => {
@ -105,12 +104,11 @@ export default class AdvancedRoomSettingsTab extends React.Component<IProps, ISt
};
public render(): React.ReactNode {
const client = MatrixClientPeg.get();
const room = client.getRoom(this.props.roomId);
const isSpace = room?.isSpaceRoom();
const room = this.props.room;
const isSpace = room.isSpaceRoom();
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>;
}
@ -143,9 +141,9 @@ export default class AdvancedRoomSettingsTab extends React.Component<IProps, ISt
if (this.state.oldRoomId) {
let copy: string;
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 {
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 = (
@ -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_section mx_SettingsTab_subsectionText">
<span className="mx_SettingsTab_subheading">
{room?.isSpaceRoom() ? _t("Space information") : _t("Room information")}
{room.isSpaceRoom() ? _t("Space information") : _t("Room information")}
</span>
<div>
<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>
{unfederatableSection}
</div>
@ -172,7 +172,7 @@ export default class AdvancedRoomSettingsTab extends React.Component<IProps, ISt
<span className="mx_SettingsTab_subheading">{_t("Room version")}</span>
<div>
<span>{_t("Room version:")}</span>&nbsp;
{room?.getVersion()}
{room.getVersion()}
</div>
{oldRoomLink}
{roomUpgradeButton}

View File

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

View File

@ -15,6 +15,7 @@ limitations under the License.
*/
import React, { ContextType } from "react";
import { Room } from "matrix-js-sdk/src/matrix";
import { _t } from "../../../../../languageHandler";
import RoomProfileSettings from "../../../room_settings/RoomProfileSettings";
@ -28,7 +29,7 @@ import AliasSettings from "../../../room_settings/AliasSettings";
import PosthogTrackers from "../../../../../PosthogTrackers";
interface IProps {
roomId: string;
room: Room;
}
interface IState {
@ -50,7 +51,7 @@ export default class GeneralRoomSettingsTab extends React.Component<IProps, ISta
private onLeaveClick = (ev: ButtonEvent): void => {
dis.dispatch({
action: "leave_room",
room_id: this.props.roomId,
room_id: this.props.room.roomId,
});
PosthogTrackers.trackInteraction("WebRoomSettingsLeaveButton", ev);
@ -58,17 +59,18 @@ export default class GeneralRoomSettingsTab extends React.Component<IProps, ISta
public render(): React.ReactNode {
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 canSetCanonical = room?.currentState.mayClientSendStateEvent("m.room.canonical_alias", client);
const canonicalAliasEv = room?.currentState.getStateEvents("m.room.canonical_alias", "") ?? undefined;
const canSetCanonical = room.currentState.mayClientSendStateEvent("m.room.canonical_alias", client);
const canonicalAliasEv = room.currentState.getStateEvents("m.room.canonical_alias", "") ?? undefined;
const urlPreviewSettings =
room && SettingsStore.getValue(UIFeature.URLPreviews) ? <UrlPreviewSettings room={room} /> : null;
const urlPreviewSettings = SettingsStore.getValue(UIFeature.URLPreviews) ? (
<UrlPreviewSettings room={room} />
) : null;
let leaveSection;
if (room?.getMyMembership() === "join") {
if (room.getMyMembership() === "join") {
leaveSection = (
<>
<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_heading">{_t("General")}</div>
<div className="mx_SettingsTab_section mx_GeneralRoomSettingsTab_profileSection">
<RoomProfileSettings roomId={this.props.roomId} />
<RoomProfileSettings roomId={room.roomId} />
</div>
<div className="mx_SettingsTab_heading">{_t("Room Addresses")}</div>
<AliasSettings
roomId={this.props.roomId}
roomId={room.roomId}
canSetCanonicalAlias={canSetCanonical}
canSetAliases={canSetAliases}
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>) {
super(props, context);
this.roomProps = EchoChamber.forRoom(context.getRoom(this.props.roomId));
this.roomProps = EchoChamber.forRoom(context.getRoom(this.props.roomId)!);
let currentSound = "default";
const soundData = Notifier.getSoundForRoom(this.props.roomId);

View File

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

View File

@ -22,6 +22,7 @@ import { logger } from "matrix-js-sdk/src/logger";
import { throttle, get } from "lodash";
import { compare } from "matrix-js-sdk/src/utils";
import { IContent } from "matrix-js-sdk/src/models/event";
import { Room } from "matrix-js-sdk/src/matrix";
import { _t, _td } from "../../../../../languageHandler";
import { MatrixClientPeg } from "../../../../../MatrixClientPeg";
@ -129,7 +130,7 @@ export class BannedUser extends React.Component<IBannedUserProps> {
}
interface IProps {
roomId: string;
room: Room;
}
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 => {
if (state.roomId !== this.props.roomId) return;
if (state.roomId !== this.props.room.roomId) return;
this.onThisRoomMembership();
};
@ -171,8 +172,8 @@ export default class RolesRoomSettingsTab extends React.Component<IProps> {
private onPowerLevelsChanged = (value: number, powerLevelKey: string): void => {
const client = MatrixClientPeg.get();
const room = client.getRoom(this.props.roomId);
const plEvent = room?.currentState.getStateEvents(EventType.RoomPowerLevels, "");
const room = this.props.room;
const plEvent = room.currentState.getStateEvents(EventType.RoomPowerLevels, "");
let plContent = plEvent?.getContent() ?? {};
// 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;
} else {
const keyPath = powerLevelKey.split(".");
let parentObj: IContent | undefined;
let parentObj: IContent = {};
let currentObj = plContent;
for (const key of keyPath) {
if (!currentObj[key]) {
@ -198,7 +199,7 @@ export default class RolesRoomSettingsTab extends React.Component<IProps> {
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);
Modal.createDialog(ErrorDialog, {
@ -213,8 +214,8 @@ export default class RolesRoomSettingsTab extends React.Component<IProps> {
private onUserPowerLevelChanged = (value: number, powerLevelKey: string): void => {
const client = MatrixClientPeg.get();
const room = client.getRoom(this.props.roomId);
const plEvent = room?.currentState.getStateEvents(EventType.RoomPowerLevels, "");
const room = this.props.room;
const plEvent = room.currentState.getStateEvents(EventType.RoomPowerLevels, "");
let plContent = plEvent?.getContent() ?? {};
// Clone the power levels just in case
@ -224,7 +225,7 @@ export default class RolesRoomSettingsTab extends React.Component<IProps> {
if (!plContent["users"]) plContent["users"] = {};
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);
Modal.createDialog(ErrorDialog, {
@ -239,12 +240,12 @@ export default class RolesRoomSettingsTab extends React.Component<IProps> {
public render(): React.ReactNode {
const client = MatrixClientPeg.get();
const room = client.getRoom(this.props.roomId);
const isSpaceRoom = room?.isSpaceRoom();
const room = this.props.room;
const isSpaceRoom = room.isSpaceRoom();
const plEvent = room?.currentState.getStateEvents(EventType.RoomPowerLevels, "");
const plEvent = room.currentState.getStateEvents(EventType.RoomPowerLevels, "");
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> = {
// 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;
if (banned?.length) {
const canBanUsers = currentUserLevel >= banLevel;
@ -401,16 +402,16 @@ export default class RolesRoomSettingsTab extends React.Component<IProps> {
<ul>
{banned.map((member) => {
const banEvent = member.events.member?.getContent();
const sender = room?.getMember(member.events.member.getSender());
let bannedBy = member.events.member?.getSender(); // start by falling back to mxid
if (sender) bannedBy = sender.name;
const bannedById = member.events.member?.getSender();
const sender = bannedById ? room.getMember(bannedById) : undefined;
const bannedBy = sender?.name || bannedById; // fallback to mxid
return (
<BannedUser
key={member.userId}
canUnban={canBanUsers}
member={member}
reason={banEvent?.reason}
by={bannedBy}
by={bannedBy!}
/>
);
})}
@ -443,7 +444,7 @@ export default class RolesRoomSettingsTab extends React.Component<IProps> {
.filter(Boolean);
// 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];
}
@ -481,9 +482,7 @@ export default class RolesRoomSettingsTab extends React.Component<IProps> {
<div className="mx_SettingsTab mx_RolesRoomSettingsTab">
<div className="mx_SettingsTab_heading">{_t("Roles & Permissions")}</div>
{privilegedUsersSection}
{canChangeLevels && room !== null && (
<AddPrivilegedUsers room={room} defaultUserLevel={defaultUserLevel} />
)}
{canChangeLevels && <AddPrivilegedUsers room={room} defaultUserLevel={defaultUserLevel} />}
{mutedUsersSection}
{bannedUsersSection}
<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 { EventType } from "matrix-js-sdk/src/@types/event";
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 { _t } from "../../../../../languageHandler";
@ -42,7 +43,7 @@ import PosthogTrackers from "../../../../../PosthogTrackers";
import MatrixClientContext from "../../../../../contexts/MatrixClientContext";
interface IProps {
roomId: string;
room: Room;
closeSettingsFn: () => void;
}
@ -61,7 +62,7 @@ export default class SecurityRoomSettingsTab extends React.Component<IProps, ISt
public constructor(props: IProps, context: React.ContextType<typeof MatrixClientContext>) {
super(props, context);
const state = context.getRoom(this.props.roomId)?.currentState;
const state = this.props.room.currentState;
this.state = {
guestAccess: this.pullContentPropertyFromEvent<GuestAccess>(
@ -75,7 +76,7 @@ export default class SecurityRoomSettingsTab extends React.Component<IProps, ISt
HistoryVisibility.Shared,
),
hasAliases: false, // async loaded in componentDidMount
encrypted: context.isRoomEncrypted(this.props.roomId),
encrypted: context.isRoomEncrypted(this.props.room.roomId),
showAdvancedSection: false,
};
}
@ -104,7 +105,7 @@ export default class SecurityRoomSettingsTab extends React.Component<IProps, ISt
};
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, {
title: _t("Are you sure you want to add encryption to this public room?"),
description: (
@ -172,7 +173,9 @@ export default class SecurityRoomSettingsTab extends React.Component<IProps, ISt
const beforeEncrypted = this.state.encrypted;
this.setState({ encrypted: true });
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) => {
logger.error(e);
this.setState({ encrypted: beforeEncrypted });
@ -190,7 +193,7 @@ export default class SecurityRoomSettingsTab extends React.Component<IProps, ISt
this.context
.sendStateEvent(
this.props.roomId,
this.props.room.roomId,
EventType.RoomGuestAccess,
{
guest_access: guestAccess,
@ -222,7 +225,7 @@ export default class SecurityRoomSettingsTab extends React.Component<IProps, ISt
this.setState({ history: history });
this.context
.sendStateEvent(
this.props.roomId,
this.props.room.roomId,
EventType.RoomHistoryVisibility,
{
history_visibility: history,
@ -236,22 +239,21 @@ export default class SecurityRoomSettingsTab extends React.Component<IProps, ISt
};
private updateBlacklistDevicesFlag = (checked: boolean): void => {
this.context.getRoom(this.props.roomId)?.setBlacklistUnverifiedDevices(checked);
this.props.room.setBlacklistUnverifiedDevices(checked);
};
private async hasAliases(): Promise<boolean> {
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;
return Array.isArray(localAliases) && localAliases.length !== 0;
}
private renderJoinRule(): JSX.Element {
const client = this.context;
const room = client.getRoom(this.props.roomId);
const room = this.props.room;
let aliasWarning: JSX.Element | undefined;
if (room?.getJoinRule() === JoinRule.Public && !this.state.hasAliases) {
if (room.getJoinRule() === JoinRule.Public && !this.state.hasAliases) {
aliasWarning = (
<div className="mx_SecurityRoomSettingsTab_warning">
<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.", {
roomName: room?.name,
roomName: room.name,
});
return (
@ -342,7 +344,7 @@ export default class SecurityRoomSettingsTab extends React.Component<IProps, ISt
const client = this.context;
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 options = [
@ -393,7 +395,7 @@ export default class SecurityRoomSettingsTab extends React.Component<IProps, ISt
private renderAdvanced(): JSX.Element {
const client = this.context;
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);
return (
@ -416,9 +418,9 @@ export default class SecurityRoomSettingsTab extends React.Component<IProps, ISt
public render(): React.ReactNode {
const client = this.context;
const room = client.getRoom(this.props.roomId);
const room = this.props.room;
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;
let encryptionSettings: JSX.Element | undefined;
@ -428,7 +430,7 @@ export default class SecurityRoomSettingsTab extends React.Component<IProps, ISt
name="blacklistUnverifiedDevices"
level={SettingLevel.ROOM_DEVICE}
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();
let advanced: JSX.Element | undefined;
if (room?.getJoinRule() === JoinRule.Public) {
if (room.getJoinRule() === JoinRule.Public) {
advanced = (
<div className="mx_SettingsTab_section">
<AccessibleButton

View File

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

View File

@ -38,24 +38,46 @@ describe("<RoomSettingsDialog />", () => {
const roomId = "!room:server.org";
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");
beforeEach(() => {
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);
});
const getComponent = (onFinished = jest.fn()) =>
render(<RoomSettingsDialog roomId={roomId} onFinished={onFinished} />, {
const getComponent = (onFinished = jest.fn(), propRoomId = roomId) =>
render(<RoomSettingsDialog roomId={propRoomId} onFinished={onFinished} />, {
wrapper: ({ children }) => (
<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", () => {
it("renders default tabs correctly", () => {
const { container } = getComponent();

View File

@ -15,7 +15,7 @@ limitations under the License.
*/
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 { Room } from "matrix-js-sdk/src/models/room";
import { mocked } from "jest-mock";
@ -36,7 +36,7 @@ describe("AdvancedRoomSettingsTab", () => {
let room: Room;
const renderTab = (): RenderResult => {
return render(<AdvancedRoomSettingsTab roomId={roomId} closeSettingsFn={jest.fn()} />);
return render(<AdvancedRoomSettingsTab room={room} closeSettingsFn={jest.fn()} />);
};
beforeEach(() => {
@ -69,6 +69,22 @@ describe("AdvancedRoomSettingsTab", () => {
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) {
const createEvent = mkEvent({
event: true,
@ -143,5 +159,16 @@ describe("AdvancedRoomSettingsTab", () => {
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 { 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 { EventType } from "matrix-js-sdk/src/@types/event";
import { MatrixEvent } from "matrix-js-sdk/src/models/event";
import { Room } from "matrix-js-sdk/src/models/room";
import { mocked } from "jest-mock";
import { RoomMember } from "matrix-js-sdk/src/matrix";
import RolesRoomSettingsTab from "../../../../../../src/components/views/settings/tabs/room/RolesRoomSettingsTab";
import { mkStubRoom, stubClient } from "../../../../../test-utils";
@ -30,12 +31,13 @@ import SettingsStore from "../../../../../../src/settings/SettingsStore";
import { ElementCall } from "../../../../../../src/models/Call";
describe("RolesRoomSettingsTab", () => {
const userId = "@alice:server.org";
const roomId = "!room:example.com";
let cli: MatrixClient;
let room: Room;
const renderTab = (): RenderResult => {
return render(<RolesRoomSettingsTab roomId={roomId} />);
const renderTab = (propRoom: Room = room): RenderResult => {
return render(<RolesRoomSettingsTab room={propRoom} />);
};
const getVoiceBroadcastsSelect = (): HTMLElement => {
@ -183,4 +185,54 @@ describe("RolesRoomSettingsTab", () => {
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 { ElementCall } from "../../../../../../src/models/Call";
describe("RolesRoomSettingsTab", () => {
describe("VoipRoomSettingsTab", () => {
const roomId = "!room:example.com";
let cli: MatrixClient;
let room: Room;
const renderTab = (): RenderResult => {
return render(<VoipRoomSettingsTab roomId={roomId} />);
return render(<VoipRoomSettingsTab room={room} />);
};
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>
`;