Add way to manage Restricted join rule in Room Settings

This commit is contained in:
Michael Telatynski 2021-07-02 14:51:55 +01:00
parent fb149c4ea2
commit e8f0412fe3
13 changed files with 857 additions and 265 deletions

View file

@ -87,6 +87,7 @@
@import "./views/dialogs/_IncomingSasDialog.scss";
@import "./views/dialogs/_InviteDialog.scss";
@import "./views/dialogs/_KeyboardShortcutsDialog.scss";
@import "./views/dialogs/_ManageRestrictedJoinRuleDialog.scss";
@import "./views/dialogs/_MessageEditHistoryDialog.scss";
@import "./views/dialogs/_ModalWidgetDialog.scss";
@import "./views/dialogs/_NewSessionReviewDialog.scss";

View file

@ -0,0 +1,147 @@
/*
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.
*/
.mx_ManageRestrictedJoinRuleDialog_wrapper {
.mx_Dialog {
display: flex;
flex-direction: column;
}
}
.mx_ManageRestrictedJoinRuleDialog {
width: 480px;
color: $primary-fg-color;
display: flex;
flex-direction: column;
flex-wrap: nowrap;
min-height: 0;
height: 60vh;
.mx_SearchBox {
// To match the space around the title
margin: 0 0 15px 0;
flex-grow: 0;
}
.mx_ManageRestrictedJoinRuleDialog_content {
flex-grow: 1;
}
.mx_ManageRestrictedJoinRuleDialog_noResults {
display: block;
margin-top: 24px;
}
.mx_ManageRestrictedJoinRuleDialog_section {
&:not(:first-child) {
margin-top: 24px;
}
> h3 {
margin: 0;
color: $secondary-fg-color;
font-size: $font-12px;
font-weight: $font-semi-bold;
line-height: $font-15px;
}
.mx_ManageRestrictedJoinRuleDialog_entry {
display: flex;
margin-top: 12px;
> div {
flex-grow: 1;
}
img.mx_RoomAvatar_isSpaceRoom,
.mx_RoomAvatar_isSpaceRoom img {
border-radius: 4px;
}
.mx_ManageRestrictedJoinRuleDialog_entry_name {
margin: 0 8px;
font-size: $font-15px;
line-height: 30px;
flex-grow: 1;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
}
.mx_ManageRestrictedJoinRuleDialog_entry_description {
margin-top: 8px;
font-size: $font-12px;
line-height: $font-15px;
color: $tertiary-fg-color;
}
.mx_Checkbox {
align-items: center;
}
}
}
.mx_ManageRestrictedJoinRuleDialog_section_spaces {
.mx_BaseAvatar {
margin-right: 12px;
}
.mx_BaseAvatar_image {
border-radius: 8px;
}
}
.mx_ManageRestrictedJoinRuleDialog_section_experimental {
position: relative;
border-radius: 8px;
margin: 12px 0;
padding: 8px 8px 8px 42px;
background-color: $header-panel-bg-color;
font-size: $font-12px;
line-height: $font-15px;
color: $secondary-fg-color;
&::before {
content: '';
position: absolute;
left: 10px;
top: calc(50% - 8px); // vertical centering
height: 16px;
width: 16px;
background-color: $secondary-fg-color;
mask-repeat: no-repeat;
mask-size: contain;
mask-image: url('$(res)/img/element-icons/room/room-summary.svg');
mask-position: center;
}
}
.mx_ManageRestrictedJoinRuleDialog_footer {
display: flex;
margin-top: 20px;
align-self: end;
.mx_AccessibleButton {
display: inline-block;
align-self: center;
& + .mx_AccessibleButton {
margin-left: 24px;
}
}
}
}

View file

@ -47,14 +47,14 @@ limitations under the License.
color: $settings-subsection-fg-color;
font-size: $font-14px;
display: block;
margin: 10px 100px 10px 0; // Align with the rest of the view
margin: 10px 80px 10px 0; // Align with the rest of the view
}
.mx_SettingsTab_section {
margin-bottom: 24px;
.mx_SettingsFlag {
margin-right: 100px;
margin-right: 80px;
margin-bottom: 10px;
}

View file

@ -14,6 +14,44 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
.mx_SecurityRoomSettingsTab {
.mx_SettingsTab_showAdvanced {
padding: 0;
margin-bottom: 16px;
}
.mx_SecurityRoomSettingsTab_spacesWithAccess {
> h4 {
color: $secondary-fg-color;
font-weight: $font-semi-bold;
font-size: $font-12px;
line-height: $font-15px;
text-transform: uppercase;
}
> span {
font-weight: 500;
font-size: $font-14px;
line-height: 32px; // matches height of avatar for v-align
color: $secondary-fg-color;
display: inline-block;
img.mx_RoomAvatar_isSpaceRoom,
.mx_RoomAvatar_isSpaceRoom img {
border-radius: 8px;
}
.mx_BaseAvatar {
margin-right: 8px;
}
& + span {
margin-left: 16px;
}
}
}
}
.mx_SecurityRoomSettingsTab_warning {
display: block;
@ -26,5 +64,51 @@ limitations under the License.
}
.mx_SecurityRoomSettingsTab_encryptionSection {
margin-bottom: 25px;
padding-bottom: 24px;
border-bottom: 1px solid $menu-border-color;
margin-bottom: 32px;
}
.mx_SecurityRoomSettingsTab_upgradeRequired {
margin-left: 16px;
padding: 4px 16px;
border: 1px solid $accent-color;
border-radius: 8px;
color: $accent-color;
font-size: $font-12px;
line-height: $font-15px;
}
.mx_SecurityRoomSettingsTab_joinRule {
.mx_RadioButton {
padding-top: 16px;
margin-bottom: 8px;
.mx_RadioButton_content {
margin-left: 14px;
font-weight: $font-semi-bold;
font-size: $font-15px;
line-height: $font-24px;
color: $primary-fg-color;
display: block;
}
}
> span {
display: inline-block;
margin-left: 34px;
margin-bottom: 16px;
font-size: $font-15px;
line-height: $font-24px;
color: $secondary-fg-color;
& + .mx_RadioButton {
border-top: 1px solid $menu-border-color;
}
}
.mx_AccessibleButton_kind_link {
padding: 0;
font-size: inherit;
}
}

View file

@ -35,7 +35,6 @@ import { getAddressType } from './UserAddress';
import { abbreviateUrl } from './utils/UrlUtils';
import { getDefaultIdentityServerUrl, useDefaultIdentityServer } from './utils/IdentityServerUtils';
import { isPermalinkHost, parsePermalink } from "./utils/permalinks/Permalinks";
import { inviteUsersToRoom } from "./RoomInvite";
import { WidgetType } from "./widgets/WidgetType";
import { Jitsi } from "./widgets/Jitsi";
import { parseFragment as parseHtml, Element as ChildElement } from "parse5";
@ -50,6 +49,7 @@ import { UIFeature } from "./settings/UIFeature";
import { CHAT_EFFECTS } from "./effects";
import CallHandler from "./CallHandler";
import { guessAndSetDMRoom } from "./Rooms";
import { upgradeRoom } from './utils/RoomUpgrade';
// XXX: workaround for https://github.com/microsoft/TypeScript/issues/31816
interface HTMLInputEvent extends Event {
@ -276,51 +276,8 @@ export const Commands = [
/*isPriority=*/false, /*isStatic=*/true);
return success(finished.then(async ([resp]) => {
if (!resp.continue) return;
let checkForUpgradeFn;
try {
const upgradePromise = cli.upgradeRoom(roomId, args);
// We have to wait for the js-sdk to give us the room back so
// we can more effectively abuse the MultiInviter behaviour
// which heavily relies on the Room object being available.
if (resp.invite) {
checkForUpgradeFn = async (newRoom) => {
// The upgradePromise should be done by the time we await it here.
const { replacement_room: newRoomId } = await upgradePromise;
if (newRoom.roomId !== newRoomId) return;
const toInvite = [
...room.getMembersWithMembership("join"),
...room.getMembersWithMembership("invite"),
].map(m => m.userId).filter(m => m !== cli.getUserId());
if (toInvite.length > 0) {
// Errors are handled internally to this function
await inviteUsersToRoom(newRoomId, toInvite);
}
cli.removeListener('Room', checkForUpgradeFn);
};
cli.on('Room', checkForUpgradeFn);
}
// We have to await after so that the checkForUpgradesFn has a proper reference
// to the new room's ID.
await upgradePromise;
} catch (e) {
console.error(e);
if (checkForUpgradeFn) cli.removeListener('Room', checkForUpgradeFn);
const ErrorDialog = sdk.getComponent('dialogs.ErrorDialog');
Modal.createTrackedDialog('Slash Commands', 'room upgrade error', ErrorDialog, {
title: _t('Error upgrading room'),
description: _t(
'Double check that your server supports the room version chosen and try again.'),
});
}
if (!resp?.continue) return;
await upgradeRoom(room, args, resp.invite);
}));
}
return reject(this.getUsage());

View file

@ -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}
/>

View 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;

View file

@ -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>
);

View file

@ -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
}

View file

@ -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.")}&nbsp;
<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>
);
}

View file

@ -37,8 +37,6 @@ import { VIRTUAL_ROOM_EVENT_TYPE } from "./CallHandler";
import SpaceStore from "./stores/SpaceStore";
import { makeSpaceParentEvent } from "./utils/space";
import { Action } from "./dispatcher/actions";
import { ICreateRoomOpts } from "matrix-js-sdk/src/@types/requests";
import { Preset, Visibility } from "matrix-js-sdk/src/@types/partials";
// we define a number of interfaces which take their names from the js-sdk
/* eslint-disable camelcase */
@ -53,6 +51,7 @@ export interface IOpts {
andView?: boolean;
associatedWithCommunity?: string;
parentSpace?: Room;
joinRule?: JoinRule;
}
/**
@ -153,10 +152,10 @@ export default async function createRoom(opts: IOpts): Promise<string | null> {
},
});
if (opts.parentSpace.getJoinRule() !== JoinRule.Public && opts.createOpts.preset !== Preset.PublicChat) {
if (opts.joinRule === JoinRule.Restricted) {
const serverCapabilities = await client.getCapabilities();
const roomCapabilities = serverCapabilities?.["m.room_versions"]?.["org.matrix.msc3244.room_capabilities"];
if (roomCapabilities?.["restricted"]) {
if (roomCapabilities?.["restricted"]?.preferred) {
opts.createOpts.room_version = roomCapabilities?.["restricted"].preferred;
opts.createOpts.initial_state.push({
@ -168,11 +167,18 @@ export default async function createRoom(opts: IOpts): Promise<string | null> {
"room_id": opts.parentSpace.roomId,
}],
},
})
});
}
}
}
if (opts.joinRule !== JoinRule.Restricted) {
opts.createOpts.initial_state.push({
type: EventType.RoomJoinRules,
content: { join_rule: opts.joinRule },
});
}
let modal;
if (opts.spinner) modal = Modal.createDialog(Loader, null, 'mx_Dialog_spinner');

View file

@ -434,8 +434,6 @@
"To use it, just wait for autocomplete results to load and tab through them.": "To use it, just wait for autocomplete results to load and tab through them.",
"Upgrades a room to a new version": "Upgrades a room to a new version",
"You do not have the required permissions to use this command.": "You do not have the required permissions to use this command.",
"Error upgrading room": "Error upgrading room",
"Double check that your server supports the room version chosen and try again.": "Double check that your server supports the room version chosen and try again.",
"Changes your display nickname": "Changes your display nickname",
"Changes your display nickname in the current room only": "Changes your display nickname in the current room only",
"Changes the avatar of the current room": "Changes the avatar of the current room",
@ -728,6 +726,8 @@
"Common names and surnames are easy to guess": "Common names and surnames are easy to guess",
"Straight rows of keys are easy to guess": "Straight rows of keys are easy to guess",
"Short keyboard patterns are easy to guess": "Short keyboard patterns are easy to guess",
"Error upgrading room": "Error upgrading room",
"Double check that your server supports the room version chosen and try again.": "Double check that your server supports the room version chosen and try again.",
"Invite to %(spaceName)s": "Invite to %(spaceName)s",
"Share your public space": "Share your public space",
"Unknown App": "Unknown App",
@ -1435,22 +1435,31 @@
"Select the roles required to change various parts of the room": "Select the roles required to change various parts of the room",
"Enable encryption?": "Enable encryption?",
"Once enabled, encryption for a room cannot be disabled. Messages sent in an encrypted room cannot be seen by the server, only by the participants of the room. Enabling encryption may prevent many bots and bridges from working correctly. <a>Learn more about encryption.</a>": "Once enabled, encryption for a room cannot be disabled. Messages sent in an encrypted room cannot be seen by the server, only by the participants of the room. Enabling encryption may prevent many bots and bridges from working correctly. <a>Learn more about encryption.</a>",
"Guests cannot join this room even if explicitly invited.": "Guests cannot join this room even if explicitly invited.",
"Click here to fix": "Click here to fix",
"This upgrade will allow members of selected spaces access to this room without an invite.": "This upgrade will allow members of selected spaces access to this room without an invite.",
"To link to this room, please add an address.": "To link to this room, please add an address.",
"Only people who have been invited": "Only people who have been invited",
"Anyone who knows the room's link, apart from guests": "Anyone who knows the room's link, apart from guests",
"Anyone who knows the room's link, including guests": "Anyone who knows the room's link, including guests",
"Private (invite only)": "Private (invite only)",
"Only invited people can join.": "Only invited people can join.",
"Public (anyone)": "Public (anyone)",
"Anyone can find and join.": "Anyone can find and join.",
"Upgrade required": "Upgrade required",
"Spaces with access": "Spaces with access",
"& %(count)s more|other": "& %(count)s more",
"Anyone in a space can find and join. <a>Edit which spaces can access here.</a>": "Anyone in a space can find and join. <a>Edit which spaces can access here.</a>",
"Anyone in %(spaceName)s can find and join. You can select other spaces too.": "Anyone in %(spaceName)s can find and join. You can select other spaces too.",
"Anyone in a space can find and join. You can select multiple spaces.": "Anyone in a space can find and join. You can select multiple spaces.",
"Space members": "Space members",
"Decide who can view and join %(roomName)s.": "Decide who can view and join %(roomName)s.",
"Changes to who can read history will only apply to future messages in this room. The visibility of existing history will be unchanged.": "Changes to who can read history will only apply to future messages in this room. The visibility of existing history will be unchanged.",
"Anyone": "Anyone",
"Members only (since the point in time of selecting this option)": "Members only (since the point in time of selecting this option)",
"Members only (since they were invited)": "Members only (since they were invited)",
"Members only (since they joined)": "Members only (since they joined)",
"People with supported clients will be able to join the room without having a registered account.": "People with supported clients will be able to join the room without having a registered account.",
"Who can read history?": "Who can read history?",
"Security & Privacy": "Security & Privacy",
"Once enabled, encryption cannot be disabled.": "Once enabled, encryption cannot be disabled.",
"Encrypted": "Encrypted",
"Who can access this room?": "Who can access this room?",
"Access": "Access",
"Unable to revoke sharing for email address": "Unable to revoke sharing for email address",
"Unable to share email address": "Unable to share email address",
"Your email address hasn't been verified yet": "Your email address hasn't been verified yet",
@ -2331,6 +2340,16 @@
"Manually export keys": "Manually export keys",
"You'll lose access to your encrypted messages": "You'll lose access to your encrypted messages",
"Are you sure you want to sign out?": "Are you sure you want to sign out?",
"%(count)s members|other": "%(count)s members",
"%(count)s members|one": "%(count)s member",
"%(count)s rooms|other": "%(count)s rooms",
"%(count)s rooms|one": "%(count)s room",
"Select spaces": "Select spaces",
"Decide which spaces can access this room. If a space is selected its members will be able to find and join <RoomName/>.": "Decide which spaces can access this room. If a space is selected its members will be able to find and join <RoomName/>.",
"Search spaces": "Search spaces",
"Spaces you know that contain this room": "Spaces you know that contain this room",
"Other spaces or rooms you might not know": "Other spaces or rooms you might not know",
"These are likely ones other room admins are a part of.": "These are likely ones other room admins are a part of.",
"Confirm by comparing the following with the User Settings in your other session:": "Confirm by comparing the following with the User Settings in your other session:",
"Confirm this user's session by comparing the following with their User Settings:": "Confirm this user's session by comparing the following with their User Settings:",
"Session name": "Session name",
@ -2374,12 +2393,13 @@
"Update any local room aliases to point to the new room": "Update any local room aliases to point to the new room",
"Stop users from speaking in the old version of the room, and post a message advising users to move to the new room": "Stop users from speaking in the old version of the room, and post a message advising users to move to the new room",
"Put a link back to the old room at the start of the new room so people can see old messages": "Put a link back to the old room at the start of the new room so people can see old messages",
"Automatically invite users": "Automatically invite users",
"Automatically invite members from this room to the new one": "Automatically invite members from this room to the new one",
"Upgrade private room": "Upgrade private room",
"Upgrade public room": "Upgrade public room",
"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.": "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.",
"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>.": "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>.",
"Upgrading a room is an advanced action and is usually recommended when a room is unstable due to bugs, missing features or security vulnerabilities.": "Upgrading a room is an advanced action and is usually recommended when a room is unstable due to bugs, missing features or security vulnerabilities.",
"<b>Please note upgrading will make a new version of the room</b>. All current messages will stay in this archived room.": "<b>Please note upgrading will make a new version of the room</b>. All current messages will stay in this archived room.",
"You'll upgrade this room from <oldVersion /> to <newVersion />.": "You'll upgrade this room from <oldVersion /> to <newVersion />.",
"Resend": "Resend",
"You're all caught up.": "You're all caught up.",
@ -2636,6 +2656,7 @@
"You are an administrator of this community": "You are an administrator of this community",
"You are a member of this community": "You are a member of this community",
"Who can join this community?": "Who can join this community?",
"Only people who have been invited": "Only people who have been invited",
"Everyone": "Everyone",
"Your community hasn't got a Long Description, a HTML page to show to community members.<br />Click here to open settings and give it one!": "Your community hasn't got a Long Description, a HTML page to show to community members.<br />Click here to open settings and give it one!",
"Long Description (HTML)": "Long Description (HTML)",
@ -2733,10 +2754,6 @@
"You have %(count)s unread notifications in a prior version of this room.|other": "You have %(count)s unread notifications in a prior version of this room.",
"You have %(count)s unread notifications in a prior version of this room.|one": "You have %(count)s unread notification in a prior version of this room.",
"You don't have permission": "You don't have permission",
"%(count)s members|other": "%(count)s members",
"%(count)s members|one": "%(count)s member",
"%(count)s rooms|other": "%(count)s rooms",
"%(count)s rooms|one": "%(count)s room",
"This room is suggested as a good one to join": "This room is suggested as a good one to join",
"Suggested": "Suggested",
"Your server does not support showing space hierarchies.": "Your server does not support showing space hierarchies.",

74
src/utils/RoomUpgrade.ts Normal file
View file

@ -0,0 +1,74 @@
/*
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 { Room } from "matrix-js-sdk/src/models/room";
import { inviteUsersToRoom } from "../RoomInvite";
import Modal from "../Modal";
import { _t } from "../languageHandler";
import ErrorDialog from "../components/views/dialogs/ErrorDialog";
export async function upgradeRoom(
room: Room,
targetVersion: string,
inviteUsers = false,
// eslint-disable-next-line camelcase
): Promise<{ replacement_room: string }> {
const cli = room.client;
let checkForUpgradeFn: (room: Room) => Promise<void>;
try {
const upgradePromise = cli.upgradeRoom(room.roomId, targetVersion);
// We have to wait for the js-sdk to give us the room back so
// we can more effectively abuse the MultiInviter behaviour
// which heavily relies on the Room object being available.
if (inviteUsers) {
checkForUpgradeFn = async (newRoom: Room) => {
// The upgradePromise should be done by the time we await it here.
const { replacement_room: newRoomId } = await upgradePromise;
if (newRoom.roomId !== newRoomId) return;
const toInvite = [
...room.getMembersWithMembership("join"),
...room.getMembersWithMembership("invite"),
].map(m => m.userId).filter(m => m !== cli.getUserId());
if (toInvite.length > 0) {
// Errors are handled internally to this function
await inviteUsersToRoom(newRoomId, toInvite);
}
cli.removeListener('Room', checkForUpgradeFn);
};
cli.on('Room', checkForUpgradeFn);
}
// We have to await after so that the checkForUpgradesFn has a proper reference
// to the new room's ID.
return upgradePromise;
} catch (e) {
console.error(e);
if (checkForUpgradeFn) cli.removeListener('Room', checkForUpgradeFn);
Modal.createTrackedDialog('Slash Commands', 'room upgrade error', ErrorDialog, {
title: _t('Error upgrading room'),
description: _t('Double check that your server supports the room version chosen and try again.'),
});
throw e;
}
}