Add way to manage Restricted join rule in Room Settings
This commit is contained in:
parent
fb149c4ea2
commit
e8f0412fe3
13 changed files with 857 additions and 265 deletions
|
@ -13,9 +13,11 @@ 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, { ComponentProps } from 'react';
|
||||
import { Room } from 'matrix-js-sdk/src/models/room';
|
||||
import { ResizeMethod } from 'matrix-js-sdk/src/@types/partials';
|
||||
import classNames from "classnames";
|
||||
|
||||
import BaseAvatar from './BaseAvatar';
|
||||
import ImageView from '../elements/ImageView';
|
||||
|
@ -31,11 +33,14 @@ interface IProps extends Omit<ComponentProps<typeof BaseAvatar>, "name" | "idNam
|
|||
// oobData.avatarUrl should be set (else there
|
||||
// would be nowhere to get the avatar from)
|
||||
room?: Room;
|
||||
oobData?: IOOBData;
|
||||
oobData?: IOOBData & {
|
||||
roomId?: string;
|
||||
};
|
||||
width?: number;
|
||||
height?: number;
|
||||
resizeMethod?: ResizeMethod;
|
||||
viewAvatarOnClick?: boolean;
|
||||
className?: string;
|
||||
onClick?(): void;
|
||||
}
|
||||
|
||||
|
@ -128,14 +133,16 @@ export default class RoomAvatar extends React.Component<IProps, IState> {
|
|||
};
|
||||
|
||||
public render() {
|
||||
const { room, oobData, viewAvatarOnClick, onClick, ...otherProps } = this.props;
|
||||
|
||||
const roomName = room ? room.name : oobData.name;
|
||||
const { room, oobData, viewAvatarOnClick, onClick, className, ...otherProps } = this.props;
|
||||
|
||||
return (
|
||||
<BaseAvatar {...otherProps}
|
||||
name={roomName}
|
||||
idName={room ? room.roomId : null}
|
||||
<BaseAvatar
|
||||
{...otherProps}
|
||||
className={classNames(className, {
|
||||
mx_RoomAvatar_isSpaceRoom: room?.isSpaceRoom(),
|
||||
})}
|
||||
name={room ? room.name : oobData.name}
|
||||
idName={room ? room.roomId : oobData.roomId}
|
||||
urls={this.state.urls}
|
||||
onClick={viewAvatarOnClick && this.state.urls[0] ? this.onRoomAvatarClick : onClick}
|
||||
/>
|
||||
|
|
182
src/components/views/dialogs/ManageRestrictedJoinRuleDialog.tsx
Normal file
182
src/components/views/dialogs/ManageRestrictedJoinRuleDialog.tsx
Normal file
|
@ -0,0 +1,182 @@
|
|||
/*
|
||||
Copyright 2021 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, { useMemo, useState } from "react";
|
||||
import { Room } from "matrix-js-sdk/src/models/room";
|
||||
|
||||
import { _t } from '../../../languageHandler';
|
||||
import { IDialogProps } from "./IDialogProps";
|
||||
import BaseDialog from "./BaseDialog";
|
||||
import SearchBox from "../../structures/SearchBox";
|
||||
import SpaceStore from "../../../stores/SpaceStore";
|
||||
import RoomAvatar from "../avatars/RoomAvatar";
|
||||
import AccessibleButton from "../elements/AccessibleButton";
|
||||
import AutoHideScrollbar from "../../structures/AutoHideScrollbar";
|
||||
import StyledCheckbox from "../elements/StyledCheckbox";
|
||||
import MatrixClientContext from "../../../contexts/MatrixClientContext";
|
||||
|
||||
interface IProps extends IDialogProps {
|
||||
room: Room;
|
||||
selected?: string[];
|
||||
}
|
||||
|
||||
const Entry = ({ room, checked, onChange }) => {
|
||||
const localRoom = room instanceof Room;
|
||||
|
||||
let description;
|
||||
if (localRoom) {
|
||||
description = _t("%(count)s members", { count: room.getJoinedMemberCount() });
|
||||
const numChildRooms = SpaceStore.instance.getChildRooms(room.roomId).length;
|
||||
if (numChildRooms > 0) {
|
||||
description += " · " + _t("%(count)s rooms", { count: numChildRooms });
|
||||
}
|
||||
}
|
||||
|
||||
return <label className="mx_ManageRestrictedJoinRuleDialog_entry">
|
||||
<div>
|
||||
<div>
|
||||
{ localRoom
|
||||
? <RoomAvatar room={room} height={20} width={20} />
|
||||
: <RoomAvatar oobData={room} height={20} width={20} />
|
||||
}
|
||||
<span className="mx_ManageRestrictedJoinRuleDialog_entry_name">{ room.name }</span>
|
||||
</div>
|
||||
{ description && <div className="mx_ManageRestrictedJoinRuleDialog_entry_description">
|
||||
{ description }
|
||||
</div> }
|
||||
</div>
|
||||
<StyledCheckbox
|
||||
onChange={onChange ? (e) => onChange(e.target.checked) : null}
|
||||
checked={checked}
|
||||
disabled={!onChange}
|
||||
/>
|
||||
</label>;
|
||||
};
|
||||
|
||||
const ManageRestrictedJoinRuleDialog: React.FC<IProps> = ({ room, selected = [], onFinished }) => {
|
||||
const cli = room.client;
|
||||
const [newSelected, setNewSelected] = useState(new Set<string>(selected));
|
||||
const [query, setQuery] = useState("");
|
||||
const lcQuery = query.toLowerCase().trim();
|
||||
|
||||
const [spacesContainingRoom, otherEntries] = useMemo(() => {
|
||||
const spaces = cli.getVisibleRooms().filter(r => r.getMyMembership() === "join" && r.isSpaceRoom());
|
||||
return [
|
||||
spaces.filter(r => SpaceStore.instance.getSpaceFilteredRoomIds(r).has(room.roomId)),
|
||||
selected.map(roomId => {
|
||||
const room = cli.getRoom(roomId);
|
||||
if (!room) {
|
||||
return { roomId, name: roomId } as Room;
|
||||
}
|
||||
if (room.getMyMembership() !== "join" || !room.isSpaceRoom()) {
|
||||
return room;
|
||||
}
|
||||
}).filter(Boolean),
|
||||
];
|
||||
}, [cli, selected, room.roomId]);
|
||||
|
||||
const [filteredSpacesContainingRooms, filteredOtherEntries] = useMemo(() => [
|
||||
spacesContainingRoom.filter(r => r.name.toLowerCase().includes(lcQuery)),
|
||||
otherEntries.filter(r => r.name.toLowerCase().includes(lcQuery)),
|
||||
], [spacesContainingRoom, otherEntries, lcQuery]);
|
||||
|
||||
const onChange = (checked: boolean, room: Room): void => {
|
||||
if (checked) {
|
||||
newSelected.add(room.roomId);
|
||||
} else {
|
||||
newSelected.delete(room.roomId);
|
||||
}
|
||||
setNewSelected(new Set(newSelected));
|
||||
};
|
||||
|
||||
return <BaseDialog
|
||||
title={_t("Select spaces")}
|
||||
className="mx_ManageRestrictedJoinRuleDialog"
|
||||
onFinished={onFinished}
|
||||
fixedWidth={false}
|
||||
>
|
||||
<p>
|
||||
{ _t("Decide which spaces can access this room. " +
|
||||
"If a space is selected its members will be able to find and join <RoomName/>.", {}, {
|
||||
RoomName: () => <b>{ room.name }</b>,
|
||||
})}
|
||||
</p>
|
||||
<MatrixClientContext.Provider value={cli}>
|
||||
<SearchBox
|
||||
className="mx_textinput_icon mx_textinput_search"
|
||||
placeholder={ _t("Search spaces") }
|
||||
onSearch={setQuery}
|
||||
autoComplete={true}
|
||||
autoFocus={true}
|
||||
/>
|
||||
<AutoHideScrollbar className="mx_ManageRestrictedJoinRuleDialog_content">
|
||||
{ filteredSpacesContainingRooms.length > 0 ? (
|
||||
<div className="mx_ManageRestrictedJoinRuleDialog_section">
|
||||
<h3>{ _t("Spaces you know that contain this room") }</h3>
|
||||
{ filteredSpacesContainingRooms.map(space => {
|
||||
return <Entry
|
||||
key={space.roomId}
|
||||
room={space}
|
||||
checked={newSelected.has(space.roomId)}
|
||||
onChange={(checked: boolean) => {
|
||||
onChange(checked, space);
|
||||
}}
|
||||
/>;
|
||||
}) }
|
||||
</div>
|
||||
) : undefined }
|
||||
|
||||
{ filteredOtherEntries.length > 0 ? (
|
||||
<div className="mx_ManageRestrictedJoinRuleDialog_section">
|
||||
<h3>{ _t("Other spaces or rooms you might not know") }</h3>
|
||||
<div className="mx_ManageRestrictedJoinRuleDialog_section_experimental">
|
||||
<div>{ _t("These are likely ones other room admins are a part of.") }</div>
|
||||
</div>
|
||||
{ filteredOtherEntries.map(space => {
|
||||
return <Entry
|
||||
key={space.roomId}
|
||||
room={space}
|
||||
checked={newSelected.has(space.roomId)}
|
||||
onChange={(checked: boolean) => {
|
||||
onChange(checked, space);
|
||||
}}
|
||||
/>;
|
||||
}) }
|
||||
</div>
|
||||
) : null }
|
||||
|
||||
{ filteredSpacesContainingRooms.length + filteredOtherEntries.length < 1
|
||||
? <span className="mx_ManageRestrictedJoinRuleDialog_noResults">
|
||||
{ _t("No results") }
|
||||
</span>
|
||||
: undefined
|
||||
}
|
||||
</AutoHideScrollbar>
|
||||
|
||||
<div className="mx_ManageRestrictedJoinRuleDialog_footer">
|
||||
<AccessibleButton kind="primary_outline" onClick={() => onFinished()}>
|
||||
{ _t("Cancel") }
|
||||
</AccessibleButton>
|
||||
<AccessibleButton kind="primary" onClick={() => onFinished(Array.from(newSelected))}>
|
||||
{ _t("Confirm") }
|
||||
</AccessibleButton>
|
||||
</div>
|
||||
</MatrixClientContext.Provider>
|
||||
</BaseDialog>;
|
||||
};
|
||||
|
||||
export default ManageRestrictedJoinRuleDialog;
|
||||
|
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
Copyright 2019, 2020 The Matrix.org Foundation C.I.C.
|
||||
Copyright 2019 - 2021 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.
|
||||
|
@ -14,86 +14,95 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { ReactNode } from 'react';
|
||||
import { EventType } from 'matrix-js-sdk/src/@types/event';
|
||||
import { JoinRule } from 'matrix-js-sdk/src/@types/partials';
|
||||
|
||||
import { _t } from "../../../languageHandler";
|
||||
import SdkConfig from "../../../SdkConfig";
|
||||
import * as sdk from "../../../index";
|
||||
import LabelledToggleSwitch from "../elements/LabelledToggleSwitch";
|
||||
import { MatrixClientPeg } from "../../../MatrixClientPeg";
|
||||
import Modal from "../../../Modal";
|
||||
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
||||
import { IDialogProps } from "./IDialogProps";
|
||||
import BugReportDialog from './BugReportDialog';
|
||||
import BaseDialog from "./BaseDialog";
|
||||
import DialogButtons from "../elements/DialogButtons";
|
||||
|
||||
interface IProps extends IDialogProps {
|
||||
roomId: string;
|
||||
targetVersion: string;
|
||||
description?: ReactNode;
|
||||
}
|
||||
|
||||
interface IState {
|
||||
inviteUsersToNewRoom: boolean;
|
||||
}
|
||||
|
||||
@replaceableComponent("views.dialogs.RoomUpgradeWarningDialog")
|
||||
export default class RoomUpgradeWarningDialog extends React.Component {
|
||||
static propTypes = {
|
||||
onFinished: PropTypes.func.isRequired,
|
||||
roomId: PropTypes.string.isRequired,
|
||||
targetVersion: PropTypes.string.isRequired,
|
||||
};
|
||||
export default class RoomUpgradeWarningDialog extends React.Component<IProps, IState> {
|
||||
private readonly isPrivate: boolean;
|
||||
private readonly currentVersion: string;
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
const room = MatrixClientPeg.get().getRoom(this.props.roomId);
|
||||
const joinRules = room ? room.currentState.getStateEvents("m.room.join_rules", "") : null;
|
||||
const isPrivate = joinRules ? joinRules.getContent()['join_rule'] !== 'public' : true;
|
||||
const joinRules = room?.currentState.getStateEvents(EventType.RoomJoinRules, "");
|
||||
this.isPrivate = joinRules?.getContent()['join_rule'] !== JoinRule.Public ?? true;
|
||||
this.currentVersion = room?.getVersion() || "1";
|
||||
|
||||
this.state = {
|
||||
currentVersion: room ? room.getVersion() : "1",
|
||||
isPrivate,
|
||||
inviteUsersToNewRoom: true,
|
||||
};
|
||||
}
|
||||
|
||||
_onContinue = () => {
|
||||
this.props.onFinished({ continue: true, invite: this.state.isPrivate && this.state.inviteUsersToNewRoom });
|
||||
private onContinue = () => {
|
||||
this.props.onFinished({ continue: true, invite: this.isPrivate && this.state.inviteUsersToNewRoom });
|
||||
};
|
||||
|
||||
_onCancel = () => {
|
||||
private onCancel = () => {
|
||||
this.props.onFinished({ continue: false, invite: false });
|
||||
};
|
||||
|
||||
_onInviteUsersToggle = (newVal) => {
|
||||
this.setState({ inviteUsersToNewRoom: newVal });
|
||||
private onInviteUsersToggle = (inviteUsersToNewRoom: boolean) => {
|
||||
this.setState({ inviteUsersToNewRoom });
|
||||
};
|
||||
|
||||
_openBugReportDialog = (e) => {
|
||||
private openBugReportDialog = (e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
||||
const BugReportDialog = sdk.getComponent("dialogs.BugReportDialog");
|
||||
Modal.createTrackedDialog('Bug Report Dialog', '', BugReportDialog, {});
|
||||
};
|
||||
|
||||
render() {
|
||||
const brand = SdkConfig.get().brand;
|
||||
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
|
||||
const DialogButtons = sdk.getComponent('views.elements.DialogButtons');
|
||||
|
||||
let inviteToggle = null;
|
||||
if (this.state.isPrivate) {
|
||||
if (this.isPrivate) {
|
||||
inviteToggle = (
|
||||
<LabelledToggleSwitch
|
||||
value={this.state.inviteUsersToNewRoom}
|
||||
onChange={this._onInviteUsersToggle}
|
||||
label={_t("Automatically invite users")} />
|
||||
onChange={this.onInviteUsersToggle}
|
||||
label={_t("Automatically invite members from this room to the new one")} />
|
||||
);
|
||||
}
|
||||
|
||||
const title = this.state.isPrivate ? _t("Upgrade private room") : _t("Upgrade public room");
|
||||
const title = this.isPrivate ? _t("Upgrade private room") : _t("Upgrade public room");
|
||||
|
||||
let bugReports = (
|
||||
<p>
|
||||
{_t(
|
||||
{ _t(
|
||||
"This usually only affects how the room is processed on the server. If you're " +
|
||||
"having problems with your %(brand)s, please report a bug.", { brand },
|
||||
)}
|
||||
) }
|
||||
</p>
|
||||
);
|
||||
if (SdkConfig.get().bug_report_endpoint_url) {
|
||||
bugReports = (
|
||||
<p>
|
||||
{_t(
|
||||
{ _t(
|
||||
"This usually only affects how the room is processed on the server. If you're " +
|
||||
"having problems with your %(brand)s, please <a>report a bug</a>.",
|
||||
{
|
||||
|
@ -101,10 +110,10 @@ export default class RoomUpgradeWarningDialog extends React.Component {
|
|||
},
|
||||
{
|
||||
"a": (sub) => {
|
||||
return <a href='#' onClick={this._openBugReportDialog}>{sub}</a>;
|
||||
return <a href='#' onClick={this.openBugReportDialog}>{sub}</a>;
|
||||
},
|
||||
},
|
||||
)}
|
||||
) }
|
||||
</p>
|
||||
);
|
||||
}
|
||||
|
@ -119,29 +128,37 @@ export default class RoomUpgradeWarningDialog extends React.Component {
|
|||
>
|
||||
<div>
|
||||
<p>
|
||||
{_t(
|
||||
{ this.props.description || _t(
|
||||
"Upgrading a room is an advanced action and is usually recommended when a room " +
|
||||
"is unstable due to bugs, missing features or security vulnerabilities.",
|
||||
)}
|
||||
) }
|
||||
</p>
|
||||
{bugReports}
|
||||
<p>
|
||||
{ _t(
|
||||
"<b>Please note upgrading will make a new version of the room</b>. " +
|
||||
"All current messages will stay in this archived room.", {}, {
|
||||
b: sub => <b>{ sub }</b>,
|
||||
},
|
||||
) }
|
||||
</p>
|
||||
{ bugReports }
|
||||
<p>
|
||||
{_t(
|
||||
"You'll upgrade this room from <oldVersion /> to <newVersion />.",
|
||||
{},
|
||||
{
|
||||
oldVersion: () => <code>{this.state.currentVersion}</code>,
|
||||
newVersion: () => <code>{this.props.targetVersion}</code>,
|
||||
oldVersion: () => <code>{ this.currentVersion }</code>,
|
||||
newVersion: () => <code>{ this.props.targetVersion }</code>,
|
||||
},
|
||||
)}
|
||||
</p>
|
||||
{inviteToggle}
|
||||
{ inviteToggle }
|
||||
</div>
|
||||
<DialogButtons
|
||||
primaryButton={_t("Upgrade")}
|
||||
onPrimaryButtonClick={this._onContinue}
|
||||
onPrimaryButtonClick={this.onContinue}
|
||||
cancelButton={_t("Cancel")}
|
||||
onCancel={this._onCancel}
|
||||
onCancel={this.onCancel}
|
||||
/>
|
||||
</BaseDialog>
|
||||
);
|
|
@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
import React from "react";
|
||||
import React, { ReactNode } from "react";
|
||||
import classNames from "classnames";
|
||||
|
||||
import StyledRadioButton from "./StyledRadioButton";
|
||||
|
@ -23,8 +23,8 @@ export interface IDefinition<T extends string> {
|
|||
value: T;
|
||||
className?: string;
|
||||
disabled?: boolean;
|
||||
label: React.ReactChild;
|
||||
description?: React.ReactChild;
|
||||
label: ReactNode;
|
||||
description?: ReactNode;
|
||||
checked?: boolean; // If provided it will override the value comparison done in the group
|
||||
}
|
||||
|
||||
|
|
|
@ -15,9 +15,8 @@ limitations under the License.
|
|||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { GuestAccess, HistoryVisibility, JoinRule } from "matrix-js-sdk/src/@types/partials";
|
||||
import { MatrixEvent } from "matrix-js-sdk/src/models/event";
|
||||
import { IRoomVersionsCapability } from 'matrix-js-sdk/src/client';
|
||||
import { GuestAccess, HistoryVisibility, JoinRule, RestrictedAllowType } from "matrix-js-sdk/src/@types/partials";
|
||||
import { IContent, MatrixEvent } from "matrix-js-sdk/src/models/event";
|
||||
import { EventType } from 'matrix-js-sdk/src/@types/event';
|
||||
|
||||
import { _t } from "../../../../../languageHandler";
|
||||
|
@ -31,6 +30,12 @@ import { SettingLevel } from "../../../../../settings/SettingLevel";
|
|||
import SettingsStore from "../../../../../settings/SettingsStore";
|
||||
import { UIFeature } from "../../../../../settings/UIFeature";
|
||||
import { replaceableComponent } from "../../../../../utils/replaceableComponent";
|
||||
import AccessibleButton from "../../../elements/AccessibleButton";
|
||||
import SpaceStore from "../../../../../stores/SpaceStore";
|
||||
import RoomAvatar from "../../../avatars/RoomAvatar";
|
||||
import ManageRestrictedJoinRuleDialog from '../../../dialogs/ManageRestrictedJoinRuleDialog';
|
||||
import RoomUpgradeWarningDialog from '../../../dialogs/RoomUpgradeWarningDialog';
|
||||
import { upgradeRoom } from "../../../../../utils/RoomUpgrade";
|
||||
|
||||
interface IProps {
|
||||
roomId: string;
|
||||
|
@ -38,18 +43,14 @@ interface IProps {
|
|||
|
||||
interface IState {
|
||||
joinRule: JoinRule;
|
||||
restrictedAllowRoomIds?: string[];
|
||||
guestAccess: GuestAccess;
|
||||
history: HistoryVisibility;
|
||||
hasAliases: boolean;
|
||||
encrypted: boolean;
|
||||
roomVersionsCapability?: IRoomVersionsCapability;
|
||||
}
|
||||
|
||||
enum RoomVisibility {
|
||||
InviteOnly = "invite_only",
|
||||
PublicNoGuests = "public_no_guests",
|
||||
PublicWithGuests = "public_with_guests",
|
||||
Restricted = "restricted",
|
||||
roomSupportsRestricted?: boolean;
|
||||
preferredRestrictionVersion?: string;
|
||||
showAdvancedSection: boolean;
|
||||
}
|
||||
|
||||
@replaceableComponent("views.settings.tabs.room.SecurityRoomSettingsTab")
|
||||
|
@ -59,10 +60,11 @@ export default class SecurityRoomSettingsTab extends React.Component<IProps, ISt
|
|||
|
||||
this.state = {
|
||||
joinRule: JoinRule.Invite,
|
||||
guestAccess: GuestAccess.CanJoin,
|
||||
guestAccess: GuestAccess.Forbidden,
|
||||
history: HistoryVisibility.Shared,
|
||||
hasAliases: false,
|
||||
encrypted: false,
|
||||
showAdvancedSection: false,
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -74,34 +76,47 @@ export default class SecurityRoomSettingsTab extends React.Component<IProps, ISt
|
|||
const room = cli.getRoom(this.props.roomId);
|
||||
const state = room.currentState;
|
||||
|
||||
const joinRule: JoinRule = this.pullContentPropertyFromEvent(
|
||||
state.getStateEvents(EventType.RoomJoinRules, ""),
|
||||
const joinRuleEvent = state.getStateEvents(EventType.RoomJoinRules, "");
|
||||
const joinRule: JoinRule = this.pullContentPropertyFromEvent<JoinRule>(
|
||||
joinRuleEvent,
|
||||
'join_rule',
|
||||
JoinRule.Invite,
|
||||
);
|
||||
const guestAccess: GuestAccess = this.pullContentPropertyFromEvent(
|
||||
const restrictedAllowRoomIds = joinRule === JoinRule.Restricted
|
||||
? joinRuleEvent?.getContent().allow
|
||||
?.filter(a => a.type === RestrictedAllowType.RoomMembership)
|
||||
?.map(a => a.room_id)
|
||||
: undefined;
|
||||
|
||||
const guestAccess: GuestAccess = this.pullContentPropertyFromEvent<GuestAccess>(
|
||||
state.getStateEvents(EventType.RoomGuestAccess, ""),
|
||||
'guest_access',
|
||||
GuestAccess.Forbidden,
|
||||
);
|
||||
const history: HistoryVisibility = this.pullContentPropertyFromEvent(
|
||||
const history: HistoryVisibility = this.pullContentPropertyFromEvent<HistoryVisibility>(
|
||||
state.getStateEvents(EventType.RoomHistoryVisibility, ""),
|
||||
'history_visibility',
|
||||
HistoryVisibility.Shared,
|
||||
);
|
||||
|
||||
const encrypted = MatrixClientPeg.get().isRoomEncrypted(this.props.roomId);
|
||||
this.setState({ joinRule, guestAccess, history, encrypted });
|
||||
this.setState({ joinRule, restrictedAllowRoomIds, guestAccess, history, encrypted });
|
||||
|
||||
this.hasAliases().then(hasAliases => this.setState({ hasAliases }));
|
||||
cli.getCapabilities().then(capabilities => this.setState({
|
||||
roomVersionsCapability: capabilities["m.room_versions"],
|
||||
}));
|
||||
cli.getCapabilities().then(capabilities => {
|
||||
const roomCapabilities = capabilities["org.matrix.msc3244.room_capabilities"];
|
||||
const roomSupportsRestricted = roomCapabilities && Array.isArray(roomCapabilities["restricted"]?.support) &&
|
||||
roomCapabilities["restricted"].support.includes(room.getVersion());
|
||||
const preferredRestrictionVersion = roomSupportsRestricted
|
||||
? roomCapabilities?.["restricted"].preferred
|
||||
: undefined;
|
||||
|
||||
this.setState({ roomSupportsRestricted, preferredRestrictionVersion });
|
||||
});
|
||||
}
|
||||
|
||||
private pullContentPropertyFromEvent<T>(event: MatrixEvent, key: string, defaultValue: T): T {
|
||||
if (!event || !event.getContent()) return defaultValue;
|
||||
return event.getContent()[key] || defaultValue;
|
||||
return event?.getContent()[key] || defaultValue;
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
|
@ -151,81 +166,80 @@ export default class SecurityRoomSettingsTab extends React.Component<IProps, ISt
|
|||
});
|
||||
};
|
||||
|
||||
private fixGuestAccess = (e: React.MouseEvent) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
private onJoinRuleChange = (joinRule: JoinRule) => {
|
||||
if (joinRule === JoinRule.Restricted &&
|
||||
!this.state.roomSupportsRestricted &&
|
||||
this.state.preferredRestrictionVersion
|
||||
) {
|
||||
const cli = MatrixClientPeg.get();
|
||||
const roomId = this.props.roomId;
|
||||
const room = cli.getRoom(roomId);
|
||||
const targetVersion = this.state.preferredRestrictionVersion;
|
||||
const activeSpace = SpaceStore.instance.activeSpace;
|
||||
Modal.createTrackedDialog('Restricted join rule upgrade', '', RoomUpgradeWarningDialog, {
|
||||
roomId,
|
||||
targetVersion,
|
||||
description: _t("This upgrade will allow members of selected spaces " +
|
||||
"access to this room without an invite."),
|
||||
onFinished: async (resp) => {
|
||||
if (!resp?.continue) return;
|
||||
const { replacement_room: newRoomId } = await upgradeRoom(room, targetVersion, resp.invite);
|
||||
|
||||
const guestAccess = GuestAccess.CanJoin;
|
||||
const content: IContent = {
|
||||
join_rule: JoinRule.Restricted,
|
||||
};
|
||||
|
||||
if (activeSpace) {
|
||||
content.allow = [{
|
||||
"type": RestrictedAllowType.RoomMembership,
|
||||
"room_id": activeSpace.roomId,
|
||||
}];
|
||||
}
|
||||
|
||||
cli.sendStateEvent(newRoomId, EventType.RoomJoinRules, content);
|
||||
},
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const beforeJoinRule = this.state.joinRule;
|
||||
this.setState({ joinRule });
|
||||
|
||||
const client = MatrixClientPeg.get();
|
||||
client.sendStateEvent(this.props.roomId, EventType.RoomJoinRules, {
|
||||
join_rule: joinRule,
|
||||
}, "").catch((e) => {
|
||||
console.error(e);
|
||||
this.setState({ joinRule: beforeJoinRule });
|
||||
});
|
||||
};
|
||||
|
||||
private onRestrictedRoomIdsChange = (restrictedAllowRoomIds: string[]) => {
|
||||
const beforeRestrictedAllowRoomIds = this.state.restrictedAllowRoomIds;
|
||||
this.setState({ restrictedAllowRoomIds });
|
||||
|
||||
const client = MatrixClientPeg.get();
|
||||
client.sendStateEvent(this.props.roomId, EventType.RoomJoinRules, {
|
||||
join_rule: JoinRule.Restricted,
|
||||
allow: restrictedAllowRoomIds.map(roomId => ({
|
||||
"type": RestrictedAllowType.RoomMembership,
|
||||
"room_id": roomId,
|
||||
})),
|
||||
}, "").catch((e) => {
|
||||
console.error(e);
|
||||
this.setState({ restrictedAllowRoomIds: beforeRestrictedAllowRoomIds });
|
||||
});
|
||||
};
|
||||
|
||||
private onGuestAccessChange = (allowed: boolean) => {
|
||||
const guestAccess = allowed ? GuestAccess.CanJoin : GuestAccess.Forbidden;
|
||||
const beforeGuestAccess = this.state.guestAccess;
|
||||
this.setState({ guestAccess });
|
||||
|
||||
const client = MatrixClientPeg.get();
|
||||
client.sendStateEvent(
|
||||
this.props.roomId,
|
||||
EventType.RoomGuestAccess,
|
||||
{ guest_access: guestAccess, },
|
||||
"",
|
||||
).catch((e) => {
|
||||
console.error(e);
|
||||
this.setState({ guestAccess: beforeGuestAccess });
|
||||
});
|
||||
};
|
||||
|
||||
private onRoomAccessRadioToggle = (roomAccess: RoomVisibility) => {
|
||||
// join_rule
|
||||
// INVITE | PUBLIC | RESTRICTED
|
||||
// -----------+----------+----------------+-------------
|
||||
// guest CAN_JOIN | inv_only | pub_with_guest | restricted
|
||||
// access -----------+----------+----------------+-------------
|
||||
// FORBIDDEN | inv_only | pub_no_guest | restricted
|
||||
// -----------+----------+----------------+-------------
|
||||
|
||||
// we always set guests can_join here as it makes no sense to have
|
||||
// an invite-only room that guests can't join. If you explicitly
|
||||
// invite them, you clearly want them to join, whether they're a
|
||||
// guest or not. In practice, guest_access should probably have
|
||||
// been implemented as part of the join_rules enum.
|
||||
let joinRule = JoinRule.Invite;
|
||||
let guestAccess = GuestAccess.CanJoin;
|
||||
|
||||
switch (roomAccess) {
|
||||
case RoomVisibility.InviteOnly:
|
||||
// no change - use defaults above
|
||||
break;
|
||||
case RoomVisibility.Restricted:
|
||||
joinRule = JoinRule.Restricted;
|
||||
break;
|
||||
case RoomVisibility.PublicNoGuests:
|
||||
joinRule = JoinRule.Public;
|
||||
guestAccess = GuestAccess.Forbidden;
|
||||
break;
|
||||
case RoomVisibility.PublicWithGuests:
|
||||
joinRule = JoinRule.Public;
|
||||
guestAccess = GuestAccess.CanJoin;
|
||||
break;
|
||||
}
|
||||
|
||||
const beforeJoinRule = this.state.joinRule;
|
||||
const beforeGuestAccess = this.state.guestAccess;
|
||||
this.setState({ joinRule, guestAccess });
|
||||
|
||||
const client = MatrixClientPeg.get();
|
||||
client.sendStateEvent(
|
||||
this.props.roomId,
|
||||
EventType.RoomJoinRules, {
|
||||
join_rule: joinRule,
|
||||
}, "",
|
||||
).catch((e) => {
|
||||
console.error(e);
|
||||
this.setState({ joinRule: beforeJoinRule });
|
||||
});
|
||||
client.sendStateEvent(
|
||||
this.props.roomId,
|
||||
EventType.RoomGuestAccess, {
|
||||
client.sendStateEvent(this.props.roomId, EventType.RoomGuestAccess, {
|
||||
guest_access: guestAccess,
|
||||
}, "",
|
||||
).catch((e) => {
|
||||
}, "").catch((e) => {
|
||||
console.error(e);
|
||||
this.setState({ guestAccess: beforeGuestAccess });
|
||||
});
|
||||
|
@ -260,27 +274,25 @@ export default class SecurityRoomSettingsTab extends React.Component<IProps, ISt
|
|||
}
|
||||
}
|
||||
|
||||
private renderRoomAccess() {
|
||||
private onEditRestrictedClick = () => {
|
||||
const matrixClient = MatrixClientPeg.get();
|
||||
Modal.createTrackedDialog('Edit restricted', '', ManageRestrictedJoinRuleDialog, {
|
||||
matrixClient,
|
||||
room: matrixClient.getRoom(this.props.roomId),
|
||||
selected: this.state.restrictedAllowRoomIds,
|
||||
onFinished: (restrictedAllowRoomIds?: string[]) => {
|
||||
if (!Array.isArray(restrictedAllowRoomIds)) return;
|
||||
this.onRestrictedRoomIdsChange(restrictedAllowRoomIds);
|
||||
},
|
||||
}, "mx_ManageRestrictedJoinRuleDialog_wrapper");
|
||||
};
|
||||
|
||||
private renderJoinRule() {
|
||||
const client = MatrixClientPeg.get();
|
||||
const room = client.getRoom(this.props.roomId);
|
||||
const joinRule = this.state.joinRule;
|
||||
const guestAccess = this.state.guestAccess;
|
||||
|
||||
const canChangeAccess = room.currentState.mayClientSendStateEvent(EventType.RoomJoinRules, client)
|
||||
&& room.currentState.mayClientSendStateEvent(EventType.RoomGuestAccess, client);
|
||||
|
||||
let guestWarning = null;
|
||||
if (joinRule !== JoinRule.Public && guestAccess === GuestAccess.Forbidden) {
|
||||
guestWarning = (
|
||||
<div className='mx_SecurityRoomSettingsTab_warning'>
|
||||
<img src={require("../../../../../../res/img/warning.svg")} width={15} height={15} />
|
||||
<span>
|
||||
{_t("Guests cannot join this room even if explicitly invited.")}
|
||||
<a href="" onClick={this.fixGuestAccess}>{_t("Click here to fix")}</a>
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
const canChangeJoinRule = room.currentState.mayClientSendStateEvent(EventType.RoomJoinRules, client);
|
||||
|
||||
let aliasWarning = null;
|
||||
if (joinRule === JoinRule.Public && !this.state.hasAliases) {
|
||||
|
@ -294,46 +306,98 @@ export default class SecurityRoomSettingsTab extends React.Component<IProps, ISt
|
|||
);
|
||||
}
|
||||
|
||||
const radioDefinitions: IDefinition<RoomVisibility>[] = [
|
||||
{
|
||||
value: RoomVisibility.InviteOnly,
|
||||
label: _t('Only people who have been invited'),
|
||||
checked: joinRule !== JoinRule.Public && joinRule !== JoinRule.Restricted,
|
||||
},
|
||||
{
|
||||
value: RoomVisibility.PublicNoGuests,
|
||||
label: _t('Anyone who knows the room\'s link, apart from guests'),
|
||||
checked: joinRule === JoinRule.Public && guestAccess !== GuestAccess.CanJoin,
|
||||
},
|
||||
{
|
||||
value: RoomVisibility.PublicWithGuests,
|
||||
label: _t("Anyone who knows the room's link, including guests"),
|
||||
checked: joinRule === JoinRule.Public && guestAccess === GuestAccess.CanJoin,
|
||||
},
|
||||
];
|
||||
const radioDefinitions: IDefinition<JoinRule>[] = [{
|
||||
value: JoinRule.Invite,
|
||||
label: _t("Private (invite only)"),
|
||||
description: _t("Only invited people can join."),
|
||||
}, {
|
||||
value: JoinRule.Public,
|
||||
label: _t("Public (anyone)"),
|
||||
description: _t("Anyone can find and join."),
|
||||
}];
|
||||
|
||||
const roomCapabilities = this.state.roomVersionsCapability?.["org.matrix.msc3244.room_capabilities"];
|
||||
if (roomCapabilities?.["restricted"]) {
|
||||
if (Array.isArray(roomCapabilities["restricted"]?.support) &&
|
||||
roomCapabilities["restricted"].support.includes(room.getVersion() ?? "1")
|
||||
) {
|
||||
radioDefinitions.unshift({
|
||||
value: RoomVisibility.Restricted,
|
||||
label: _t("Only people in certain spaces or those who have been invited (TODO copy)"),
|
||||
checked: joinRule === JoinRule.Restricted,
|
||||
});
|
||||
if (this.state.roomSupportsRestricted ||
|
||||
this.state.preferredRestrictionVersion ||
|
||||
joinRule === JoinRule.Restricted
|
||||
) {
|
||||
let upgradeRequiredPill;
|
||||
if (this.state.preferredRestrictionVersion) {
|
||||
upgradeRequiredPill = <span className="mx_SecurityRoomSettingsTab_upgradeRequired">
|
||||
{ _t("Upgrade required") }
|
||||
</span>;
|
||||
}
|
||||
|
||||
let description;
|
||||
if (joinRule === JoinRule.Restricted) {
|
||||
let spacesWhichCanAccess;
|
||||
if (this.state.restrictedAllowRoomIds?.length) {
|
||||
const shownSpaces = this.state.restrictedAllowRoomIds
|
||||
.map(roomId => client.getRoom(roomId))
|
||||
.filter(Boolean)
|
||||
.slice(0, 4);
|
||||
|
||||
spacesWhichCanAccess = <div className="mx_SecurityRoomSettingsTab_spacesWithAccess">
|
||||
<h4>{ _t("Spaces with access") }</h4>
|
||||
{ shownSpaces.map(room => {
|
||||
return <span key={room.roomId}>
|
||||
<RoomAvatar room={room} height={32} width={32} />
|
||||
{ room.name }
|
||||
</span>;
|
||||
})}
|
||||
{ shownSpaces.length < this.state.restrictedAllowRoomIds.length && <span>
|
||||
{ _t("& %(count)s more", {
|
||||
count: this.state.restrictedAllowRoomIds.length - shownSpaces.length,
|
||||
}) }
|
||||
</span> }
|
||||
</div>;
|
||||
}
|
||||
|
||||
description = <div>
|
||||
<span>
|
||||
{ _t("Anyone in a space can find and join. <a>Edit which spaces can access here.</a>", {}, {
|
||||
a: sub => <AccessibleButton
|
||||
disabled={!canChangeJoinRule}
|
||||
onClick={this.onEditRestrictedClick}
|
||||
kind="link"
|
||||
>
|
||||
{ sub }
|
||||
</AccessibleButton>,
|
||||
}) }
|
||||
</span>
|
||||
{ spacesWhichCanAccess }
|
||||
</div>;
|
||||
} else if (SpaceStore.instance.activeSpace) {
|
||||
description = _t("Anyone in %(spaceName)s can find and join. You can select other spaces too.", {
|
||||
spaceName: SpaceStore.instance.activeSpace.name,
|
||||
});
|
||||
} else {
|
||||
description = _t("Anyone in a space can find and join. You can select multiple spaces.");
|
||||
}
|
||||
|
||||
radioDefinitions.splice(1, 0, {
|
||||
value: JoinRule.Restricted,
|
||||
label: <>
|
||||
{ _t("Space members") }
|
||||
{ upgradeRequiredPill }
|
||||
</>,
|
||||
description,
|
||||
});
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
{ guestWarning }
|
||||
<div className="mx_SecurityRoomSettingsTab_joinRule">
|
||||
<div className="mx_SettingsTab_subsectionText">
|
||||
<span>{ _t("Decide who can view and join %(roomName)s.", {
|
||||
roomName: client.getRoom(this.props.roomId)?.name,
|
||||
}) }</span>
|
||||
</div>
|
||||
{ aliasWarning }
|
||||
<StyledRadioGroup
|
||||
name="roomVis"
|
||||
onChange={this.onRoomAccessRadioToggle}
|
||||
name="joinRule"
|
||||
value={joinRule}
|
||||
onChange={this.onJoinRuleChange}
|
||||
definitions={radioDefinitions}
|
||||
disabled={!canChangeAccess}
|
||||
disabled={!canChangeJoinRule}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
@ -382,6 +446,30 @@ export default class SecurityRoomSettingsTab extends React.Component<IProps, ISt
|
|||
);
|
||||
}
|
||||
|
||||
private toggleAdvancedSection = () => {
|
||||
this.setState({ showAdvancedSection: !this.state.showAdvancedSection });
|
||||
};
|
||||
|
||||
private renderAdvanced() {
|
||||
const client = MatrixClientPeg.get();
|
||||
const guestAccess = this.state.guestAccess;
|
||||
const state = client.getRoom(this.props.roomId).currentState;
|
||||
const canSetGuestAccess = state.mayClientSendStateEvent(EventType.RoomGuestAccess, client);
|
||||
|
||||
return <>
|
||||
<LabelledToggleSwitch
|
||||
value={guestAccess === GuestAccess.CanJoin}
|
||||
onChange={this.onGuestAccessChange}
|
||||
disabled={!canSetGuestAccess}
|
||||
label={_t("Enable guest access")}
|
||||
/>
|
||||
<p>
|
||||
{ _t("People with supported clients will be able to join " +
|
||||
"the room without having a registered account.") }
|
||||
</p>
|
||||
</>;
|
||||
}
|
||||
|
||||
render() {
|
||||
const SettingsFlag = sdk.getComponent("elements.SettingsFlag");
|
||||
|
||||
|
@ -413,27 +501,39 @@ export default class SecurityRoomSettingsTab extends React.Component<IProps, ISt
|
|||
|
||||
return (
|
||||
<div className="mx_SettingsTab mx_SecurityRoomSettingsTab">
|
||||
<div className="mx_SettingsTab_heading">{_t("Security & Privacy")}</div>
|
||||
<div className="mx_SettingsTab_heading">{ _t("Security & Privacy") }</div>
|
||||
|
||||
<span className='mx_SettingsTab_subheading'>{_t("Encryption")}</span>
|
||||
<span className='mx_SettingsTab_subheading'>{ _t("Encryption") }</span>
|
||||
<div className='mx_SettingsTab_section mx_SecurityRoomSettingsTab_encryptionSection'>
|
||||
<div>
|
||||
<div className='mx_SettingsTab_subsectionText'>
|
||||
<span>{_t("Once enabled, encryption cannot be disabled.")}</span>
|
||||
<span>{ _t("Once enabled, encryption cannot be disabled.") }</span>
|
||||
</div>
|
||||
<LabelledToggleSwitch value={isEncrypted} onChange={this.onEncryptionChange}
|
||||
label={_t("Encrypted")} disabled={!canEnableEncryption}
|
||||
<LabelledToggleSwitch
|
||||
value={isEncrypted}
|
||||
onChange={this.onEncryptionChange}
|
||||
label={_t("Encrypted")}
|
||||
disabled={!canEnableEncryption}
|
||||
/>
|
||||
</div>
|
||||
{encryptionSettings}
|
||||
{ encryptionSettings }
|
||||
</div>
|
||||
|
||||
<span className='mx_SettingsTab_subheading'>{_t("Who can access this room?")}</span>
|
||||
<span className='mx_SettingsTab_subheading'>{_t("Access")}</span>
|
||||
<div className='mx_SettingsTab_section mx_SettingsTab_subsectionText'>
|
||||
{this.renderRoomAccess()}
|
||||
{ this.renderJoinRule() }
|
||||
</div>
|
||||
|
||||
{historySection}
|
||||
<AccessibleButton
|
||||
onClick={this.toggleAdvancedSection}
|
||||
kind="link"
|
||||
className="mx_SettingsTab_showAdvanced"
|
||||
>
|
||||
{ this.state.showAdvancedSection ? _t("Hide advanced") : _t("Show advanced") }
|
||||
</AccessibleButton>
|
||||
{ this.state.showAdvancedSection && this.renderAdvanced() }
|
||||
|
||||
{ historySection }
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue