Merge branch 'experimental' into travis/rsettings/tab/security

pull/21833/head
Travis Ralston 2019-01-29 22:15:55 -07:00
commit f86e224db6
6 changed files with 498 additions and 54 deletions

View File

@ -143,6 +143,7 @@
@import "./views/settings/tabs/_GeneralUserSettingsTab.scss";
@import "./views/settings/tabs/_HelpSettingsTab.scss";
@import "./views/settings/tabs/_PreferencesSettingsTab.scss";
@import "./views/settings/tabs/_RolesRoomSettingsTab.scss";
@import "./views/settings/tabs/_SecurityRoomSettingsTab.scss";
@import "./views/settings/tabs/_SecuritySettingsTab.scss";
@import "./views/settings/tabs/_SettingsTab.scss";

View File

@ -0,0 +1,24 @@
/*
Copyright 2019 New Vector Ltd
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_RolesRoomSettingsTab ul {
margin-bottom: 0;
}
.mx_RolesRoomSettingsTab_unbanBtn {
margin-right: 10px;
margin-bottom: 5px;
}

View File

@ -18,8 +18,10 @@ import React from 'react';
import PropTypes from 'prop-types';
import {Tab, TabbedView} from "../../structures/TabbedView";
import {_t, _td} from "../../../languageHandler";
import AdvancedRoomSettingsTab from "../settings/tabs/AdvancedRoomSettingsTab";
import AccessibleButton from "../elements/AccessibleButton";
import dis from '../../../dispatcher';
import RolesRoomSettingsTab from "../settings/tabs/RolesRoomSettingsTab";
import GeneralRoomSettingsTab from "../settings/tabs/GeneralRoomSettingsTab";
import SecurityRoomSettingsTab from "../settings/tabs/SecurityRoomSettingsTab";
@ -74,12 +76,12 @@ export default class RoomSettingsDialog extends React.Component {
tabs.push(new Tab(
_td("Roles & Permissions"),
"mx_RoomSettingsDialog_rolesIcon",
<div>Roles Test</div>,
<RolesRoomSettingsTab roomId={this.props.roomId} />,
));
tabs.push(new Tab(
_td("Advanced"),
"mx_RoomSettingsDialog_warningIcon",
<div>Advanced Test</div>,
<AdvancedRoomSettingsTab roomId={this.props.roomId} />,
));
tabs.push(new Tab(
_td("Visit old settings"),

View File

@ -0,0 +1,104 @@
/*
Copyright 2019 New Vector Ltd
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 PropTypes from 'prop-types';
import {_t} from "../../../../languageHandler";
import MatrixClientPeg from "../../../../MatrixClientPeg";
import sdk from "../../../../index";
import AccessibleButton from "../../elements/AccessibleButton";
import Modal from "../../../../Modal";
export default class AdvancedRoomSettingsTab extends React.Component {
static propTypes = {
roomId: PropTypes.string.isRequired,
};
constructor() {
super();
this.state = {
// This is eventually set to the value of room.getRecommendedVersion()
upgradeRecommendation: null,
};
}
componentWillMount() {
// we handle lack of this object gracefully later, so don't worry about it failing here.
MatrixClientPeg.get().getRoom(this.props.roomId).getRecommendedVersion().then((v) => {
this.setState({upgradeRecommendation: v});
});
}
_upgradeRoom = (e) => {
const RoomUpgradeDialog = sdk.getComponent('dialogs.RoomUpgradeDialog');
const room = MatrixClientPeg.get().getRoom(this.props.roomId);
Modal.createTrackedDialog('Upgrade Room Version', '', RoomUpgradeDialog, {room: room});
};
_openDevtools = (e) => {
const DevtoolsDialog = sdk.getComponent('dialogs.DevtoolsDialog');
Modal.createDialog(DevtoolsDialog, {roomId: this.props.roomId});
};
render() {
const client = MatrixClientPeg.get();
const room = client.getRoom(this.props.roomId);
let unfederatableSection;
const createEvent = room.currentState.getStateEvents('m.room.create', '');
if (createEvent && createEvent.getContent()['m.federate'] === false) {
unfederatableSection = <div>{_t('This room is not accessible by remote Matrix servers')}</div>;
}
let roomUpgradeButton;
if (this.state.upgradeRecommendation && this.state.upgradeRecommendation.needsUpgrade) {
roomUpgradeButton = (
<AccessibleButton onClick={this._upgradeRoom} kind='primary'>
{_t("Upgrade room to version %(ver)s", {ver: this.state.upgradeRecommendation.version})}
</AccessibleButton>
);
}
return (
<div className="mx_SettingsTab">
<div className="mx_SettingsTab_heading">{_t("Advanced")}</div>
<div className='mx_SettingsTab_section mx_SettingsTab_subsectionText'>
<span className='mx_SettingsTab_subheading'>{_t("Room information")}</span>
<div>
<span>{_t("Internal room ID:")}</span>&nbsp;
{this.props.roomId}
</div>
{unfederatableSection}
</div>
<div className='mx_SettingsTab_section mx_SettingsTab_subsectionText'>
<span className='mx_SettingsTab_subheading'>{_t("Room version")}</span>
<div>
<span>{_t("Room version:")}</span>&nbsp;
{room.getVersion()}
</div>
{roomUpgradeButton}
</div>
<div className='mx_SettingsTab_section mx_SettingsTab_subsectionText'>
<span className='mx_SettingsTab_subheading'>{_t("Developer options")}</span>
<AccessibleButton onClick={this._openDevtools} kind='primary'>
{_t("Open Devtools")}
</AccessibleButton>
</div>
</div>
);
}
}

View File

@ -0,0 +1,319 @@
/*
Copyright 2019 New Vector Ltd
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 PropTypes from 'prop-types';
import {_t, _td} from "../../../../languageHandler";
import MatrixClientPeg from "../../../../MatrixClientPeg";
import sdk from "../../../../index";
import AccessibleButton from "../../elements/AccessibleButton";
import Modal from "../../../../Modal";
const plEventsToLabels = {
// These will be translated for us later.
"m.room.avatar": _td("To change the room's avatar, you must be a"),
"m.room.name": _td("To change the room's name, you must be a"),
"m.room.canonical_alias": _td("To change the room's main address, you must be a"),
"m.room.history_visibility": _td("To change the room's history visibility, you must be a"),
"m.room.power_levels": _td("To change the permissions in the room, you must be a"),
"m.room.topic": _td("To change the topic, you must be a"),
"im.vector.modular.widgets": _td("To modify widgets in the room, you must be a"),
};
const plEventsToShow = {
// If an event is listed here, it will be shown in the PL settings. Defaults will be calculated.
"m.room.avatar": {isState: true},
"m.room.name": {isState: true},
"m.room.canonical_alias": {isState: true},
"m.room.history_visibility": {isState: true},
"m.room.power_levels": {isState: true},
"m.room.topic": {isState: true},
"im.vector.modular.widgets": {isState: true},
};
// parse a string as an integer; if the input is undefined, or cannot be parsed
// as an integer, return a default.
function parseIntWithDefault(val, def) {
const res = parseInt(val);
return isNaN(res) ? def : res;
}
export class BannedUser extends React.Component {
static propTypes = {
canUnban: PropTypes.bool,
member: PropTypes.object.isRequired, // js-sdk RoomMember
by: PropTypes.string.isRequired,
reason: PropTypes.string,
onUnbanned: PropTypes.func.isRequired,
};
_onUnbanClick = (e) => {
MatrixClientPeg.get().unban(this.props.member.roomId, this.props.member.userId).then(() => {
this.props.onUnbanned();
}).catch((err) => {
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
console.error("Failed to unban: " + err);
Modal.createTrackedDialog('Failed to unban', '', ErrorDialog, {
title: _t('Error'),
description: _t('Failed to unban'),
});
});
};
render() {
let unbanButton;
if (this.props.canUnban) {
unbanButton = (
<AccessibleButton kind='danger_sm' onClick={this._onUnbanClick}
className='mx_RolesRoomSettingsTab_unbanBtn'>
{ _t('Unban') }
</AccessibleButton>
);
}
const userId = this.props.member.name === this.props.member.userId ? null : this.props.member.userId;
return (
<li>
{unbanButton}
<span title={_t("Banned by %(displayName)s", {displayName: this.props.by})}>
<strong>{ this.props.member.name }</strong> {userId}
{this.props.reason ? " " + _t('Reason') + ": " + this.props.reason : ""}
</span>
</li>
);
}
}
export default class RolesRoomSettingsTab extends React.Component {
static propTypes = {
roomId: PropTypes.string.isRequired,
};
_populateDefaultPlEvents(eventsSection, stateLevel, eventsLevel) {
for (const desiredEvent of Object.keys(plEventsToShow)) {
if (!(desiredEvent in eventsSection)) {
eventsSection[desiredEvent] = (plEventsToShow[desiredEvent].isState ? stateLevel : eventsLevel);
}
}
}
render() {
const PowerSelector = sdk.getComponent('elements.PowerSelector');
const client = MatrixClientPeg.get();
const room = client.getRoom(this.props.roomId);
const plContent = room.currentState.getStateEvents('m.room.power_levels', '').getContent() || {};
const canChangeLevels = room.currentState.mayClientSendStateEvent('m.room.power_levels', client);
const powerLevelDescriptors = {
"users_default": {
desc: _t('The default role for new room members is'),
defaultValue: 0,
},
"events_default": {
desc: _t('To send messages, you must be a'),
defaultValue: 0,
},
"invite": {
desc: _t('To invite users into the room, you must be a'),
defaultValue: 50,
},
"state_default": {
desc: _t('To configure the room, you must be a'),
defaultValue: 50,
},
"kick": {
desc: _t('To kick users, you must be a'),
defaultValue: 50,
},
"ban": {
desc: _t('To ban users, you must be a'),
defaultValue: 50,
},
"redact": {
desc: _t('To remove other users\' messages, you must be a'),
defaultValue: 50,
},
"notifications.room": {
desc: _t('To notify everyone in the room, you must be a'),
defaultValue: 50,
},
};
const eventsLevels = plContent.events || {};
const userLevels = plContent.users || {};
const banLevel = parseIntWithDefault(plContent.ban, powerLevelDescriptors.ban.defaultValue);
const defaultUserLevel = parseIntWithDefault(
plContent.users_default,
powerLevelDescriptors.users_default.defaultValue,
);
let currentUserLevel = userLevels[client.getUserId()];
if (currentUserLevel === undefined) {
currentUserLevel = defaultUserLevel;
}
this._populateDefaultPlEvents(
eventsLevels,
parseIntWithDefault(plContent.state_default, powerLevelDescriptors.state_default.defaultValue),
parseIntWithDefault(plContent.events_default, powerLevelDescriptors.events_default.defaultValue),
);
let privilegedUsersSection = <div>{_t('No users have specific privileges in this room')}</div>;
let mutedUsersSection;
if (Object.keys(userLevels).length) {
const privilegedUsers = [];
const mutedUsers = [];
Object.keys(userLevels).forEach(function(user) {
if (userLevels[user] > defaultUserLevel) { // privileged
privilegedUsers.push(<li key={user}>
{ _t("%(user)s is a %(userRole)s", {
user: user,
userRole: <PowerSelector value={userLevels[user]} disabled={true} />,
}) }
</li>);
} else if (userLevels[user] < defaultUserLevel) { // muted
mutedUsers.push(<li key={user}>
{ _t("%(user)s is a %(userRole)s", {
user: user,
userRole: <PowerSelector value={userLevels[user]} disabled={true} />,
}) }
</li>);
}
});
// comparator for sorting PL users lexicographically on PL descending, MXID ascending. (case-insensitive)
const comparator = (a, b) => {
const plDiff = userLevels[b.key] - userLevels[a.key];
return plDiff !== 0 ? plDiff : a.key.toLocaleLowerCase().localeCompare(b.key.toLocaleLowerCase());
};
privilegedUsers.sort(comparator);
mutedUsers.sort(comparator);
if (privilegedUsers.length) {
privilegedUsersSection =
<div className='mx_SettingsTab_section mx_SettingsTab_subsectionText'>
<div className='mx_SettingsTab_subheading'>{ _t('Privileged Users') }</div>
<ul>
{privilegedUsers}
</ul>
</div>;
}
if (mutedUsers.length) {
mutedUsersSection =
<div className='mx_SettingsTab_section mx_SettingsTab_subsectionText'>
<div className='mx_SettingsTab_subheading'>{ _t('Muted Users') }</div>
<ul>
{mutedUsers}
</ul>
</div>;
}
}
const banned = room.getMembersWithMembership("ban");
let bannedUsersSection;
if (banned.length) {
const canBanUsers = currentUserLevel >= banLevel;
bannedUsersSection =
<div className='mx_SettingsTab_section mx_SettingsTab_subsectionText'>
<div className='mx_SettingsTab_subheading'>{ _t('Banned users') }</div>
<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;
return (
<BannedUser key={member.userId} canUnban={canBanUsers}
member={member} reason={banEvent.reason}
by={bannedBy} onUnbanned={this.forceUpdate} />
);
})}
</ul>
</div>;
}
const powerSelectors = Object.keys(powerLevelDescriptors).map((key, index) => {
const descriptor = powerLevelDescriptors[key];
const keyPath = key.split('.');
let currentObj = plContent;
for (const prop of keyPath) {
if (currentObj === undefined) {
break;
}
currentObj = currentObj[prop];
}
const value = parseIntWithDefault(currentObj, descriptor.defaultValue);
return <div key={index} className="">
<span>{descriptor.desc}&nbsp;</span>
<PowerSelector
value={value}
usersDefault={defaultUserLevel}
controlled={false}
disabled={!canChangeLevels || currentUserLevel < value}
powerLevelKey={key} // Will be sent as the second parameter to `onChange`
onChange={this.onPowerLevelsChanged}
/>
</div>;
});
const eventPowerSelectors = Object.keys(eventsLevels).map(function(eventType, i) {
let label = plEventsToLabels[eventType];
if (label) {
label = _t(label);
} else {
label = _t(
"To send events of type <eventType/>, you must be a", {},
{ 'eventType': <code>{ eventType }</code> },
);
}
return (
<div className="" key={eventType}>
<span>{label}&nbsp;</span>
<PowerSelector
value={eventsLevels[eventType]}
usersDefault={defaultUserLevel}
controlled={false}
disabled={!canChangeLevels || currentUserLevel < eventsLevels[eventType]}
powerLevelKey={"event_levels_" + eventType}
onChange={self.onPowerLevelsChanged}
/>
</div>
);
});
return (
<div className="mx_SettingsTab mx_RolesRoomSettingsTab">
<div className="mx_SettingsTab_heading">{_t("Roles & Permissions")}</div>
{privilegedUsersSection}
{mutedUsersSection}
{bannedUsersSection}
<div className='mx_SettingsTab_section mx_SettingsTab_subsectionText'>
<span className='mx_SettingsTab_subheading'>{_t("Permissions")}</span>
{powerSelectors}
{eventPowerSelectors}
</div>
</div>
);
}
}

View File

@ -430,6 +430,15 @@
"Upload profile picture": "Upload profile picture",
"Display Name": "Display Name",
"Save": "Save",
"This room is not accessible by remote Matrix servers": "This room is not accessible by remote Matrix servers",
"Upgrade room to version %(ver)s": "Upgrade room to version %(ver)s",
"Advanced": "Advanced",
"Room information": "Room information",
"Internal room ID:": "Internal room ID:",
"Room version": "Room version",
"Room version:": "Room version:",
"Developer options": "Developer options",
"Open Devtools": "Open Devtools",
"Flair": "Flair",
"General": "General",
"Room Addresses": "Room Addresses",
@ -466,7 +475,6 @@
"matrix-react-sdk version:": "matrix-react-sdk version:",
"riot-web version:": "riot-web version:",
"olm version:": "olm version:",
"Advanced": "Advanced",
"Homeserver is": "Homeserver is",
"Identity Server is": "Identity Server is",
"Access Token:": "Access Token:",
@ -481,28 +489,32 @@
"Room list": "Room list",
"Timeline": "Timeline",
"Autocomplete delay (ms)": "Autocomplete delay (ms)",
"End-to-end encryption is in beta and may not be reliable": "End-to-end encryption is in beta and may not be reliable",
"You should not yet trust it to secure data": "You should not yet trust it to secure data",
"Devices will not yet be able to decrypt history from before they joined the room": "Devices will not yet be able to decrypt history from before they joined the room",
"Once encryption is enabled for a room it cannot be turned off again (for now)": "Once encryption is enabled for a room it cannot be turned off again (for now)",
"Encrypted messages will not be visible on clients that do not yet implement encryption": "Encrypted messages will not be visible on clients that do not yet implement encryption",
"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",
"To link to this room, please add an alias.": "To link to this room, please add an alias.",
"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",
"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)",
"Once enabled, encryption cannot be disabled.": "Once enabled, encryption cannot be disabled.",
"Encrypted": "Encrypted",
"Security & Privacy": "Security & Privacy",
"Encryption": "Encryption",
"Who can access this room?": "Who can access this room?",
"Who can read history?": "Who can read history?",
"To change the room's avatar, you must be a": "To change the room's avatar, you must be a",
"To change the room's name, you must be a": "To change the room's name, you must be a",
"To change the room's main address, you must be a": "To change the room's main address, you must be a",
"To change the room's history visibility, you must be a": "To change the room's history visibility, you must be a",
"To change the permissions in the room, you must be a": "To change the permissions in the room, you must be a",
"To change the topic, you must be a": "To change the topic, you must be a",
"To modify widgets in the room, you must be a": "To modify widgets in the room, you must be a",
"Failed to unban": "Failed to unban",
"Unban": "Unban",
"Banned by %(displayName)s": "Banned by %(displayName)s",
"The default role for new room members is": "The default role for new room members is",
"To send messages, you must be a": "To send messages, you must be a",
"To invite users into the room, you must be a": "To invite users into the room, you must be a",
"To configure the room, you must be a": "To configure the room, you must be a",
"To kick users, you must be a": "To kick users, you must be a",
"To ban users, you must be a": "To ban users, you must be a",
"To remove other users' messages, you must be a": "To remove other users' messages, you must be a",
"To notify everyone in the room, you must be a": "To notify everyone in the room, you must be a",
"No users have specific privileges in this room": "No users have specific privileges in this room",
"%(user)s is a %(userRole)s": "%(user)s is a %(userRole)s",
"Privileged Users": "Privileged Users",
"Muted Users": "Muted Users",
"Banned users": "Banned users",
"To send events of type <eventType/>, you must be a": "To send events of type <eventType/>, you must be a",
"Roles & Permissions": "Roles & Permissions",
"Permissions": "Permissions",
"Unignore": "Unignore",
"<not supported>": "<not supported>",
"Import E2E room keys": "Import E2E room keys",
@ -564,7 +576,6 @@
"Disinvite this user?": "Disinvite this user?",
"Kick this user?": "Kick this user?",
"Failed to kick": "Failed to kick",
"Unban": "Unban",
"Ban": "Ban",
"Unban this user?": "Unban this user?",
"Ban this user?": "Ban this user?",
@ -707,15 +718,6 @@
"If you log out or use another device, you'll lose your secure message history. To prevent this, set up Secure Message Recovery.": "If you log out or use another device, you'll lose your secure message history. To prevent this, set up Secure Message Recovery.",
"Secure Message Recovery": "Secure Message Recovery",
"Don't ask again": "Don't ask again",
"To change the room's avatar, you must be a": "To change the room's avatar, you must be a",
"To change the room's name, you must be a": "To change the room's name, you must be a",
"To change the room's main address, you must be a": "To change the room's main address, you must be a",
"To change the room's history visibility, you must be a": "To change the room's history visibility, you must be a",
"To change the permissions in the room, you must be a": "To change the permissions in the room, you must be a",
"To change the topic, you must be a": "To change the topic, you must be a",
"To modify widgets in the room, you must be a": "To modify widgets in the room, you must be a",
"Failed to unban": "Failed to unban",
"Banned by %(displayName)s": "Banned by %(displayName)s",
"Privacy warning": "Privacy warning",
"Changes to who can read history will only apply to future messages in this room": "Changes to who can read history will only apply to future messages in this room",
"The visibility of existing history will be unchanged": "The visibility of existing history will be unchanged",
@ -725,28 +727,21 @@
"(warning: cannot be disabled again!)": "(warning: cannot be disabled again!)",
"Encryption is enabled in this room": "Encryption is enabled in this room",
"Encryption is not enabled in this room": "Encryption is not enabled in this room",
"The default role for new room members is": "The default role for new room members is",
"To send messages, you must be a": "To send messages, you must be a",
"To invite users into the room, you must be a": "To invite users into the room, you must be a",
"To configure the room, you must be a": "To configure the room, you must be a",
"To kick users, you must be a": "To kick users, you must be a",
"To ban users, you must be a": "To ban users, you must be a",
"To remove other users' messages, you must be a": "To remove other users' messages, you must be a",
"To notify everyone in the room, you must be a": "To notify everyone in the room, you must be a",
"No users have specific privileges in this room": "No users have specific privileges in this room",
"%(user)s is a %(userRole)s": "%(user)s is a %(userRole)s",
"Privileged Users": "Privileged Users",
"Muted Users": "Muted Users",
"Banned users": "Banned users",
"This room is not accessible by remote Matrix servers": "This room is not accessible by remote Matrix servers",
"Favourite": "Favourite",
"Tagged as: ": "Tagged as: ",
"To link to a room it must have <a>an address</a>.": "To link to a room it must have <a>an address</a>.",
"To send events of type <eventType/>, you must be a": "To send events of type <eventType/>, you must be a",
"Upgrade room to version %(ver)s": "Upgrade room to version %(ver)s",
"Open Devtools": "Open Devtools",
"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",
"Who can access this room?": "Who can access this room?",
"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",
"Publish this room to the public in %(domain)s's room directory?": "Publish this room to the public in %(domain)s's room directory?",
"Permissions": "Permissions",
"Who can read history?": "Who can read history?",
"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)",
"Internal room ID: ": "Internal room ID: ",
"Room version number: ": "Room version number: ",
"Add a topic": "Add a topic",
@ -1068,7 +1063,6 @@
"To help avoid duplicate issues, please <existingIssuesLink>view existing issues</existingIssuesLink> first (and add a +1) or <newIssueLink>create a new issue</newIssueLink> if you can't find it.": "To help avoid duplicate issues, please <existingIssuesLink>view existing issues</existingIssuesLink> first (and add a +1) or <newIssueLink>create a new issue</newIssueLink> if you can't find it.",
"Report bugs & give feedback": "Report bugs & give feedback",
"Go back": "Go back",
"Roles & Permissions": "Roles & Permissions",
"Visit old settings": "Visit old settings",
"Failed to upgrade room": "Failed to upgrade room",
"The room upgrade could not be completed": "The room upgrade could not be completed",