diff --git a/res/css/_components.scss b/res/css/_components.scss
index 5e7e9abd05..a2c6d4bb77 100644
--- a/res/css/_components.scss
+++ b/res/css/_components.scss
@@ -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/_SecuritySettingsTab.scss";
@import "./views/settings/tabs/_SettingsTab.scss";
@import "./views/settings/tabs/_VoiceSettingsTab.scss";
diff --git a/res/css/views/settings/tabs/_RolesRoomSettingsTab.scss b/res/css/views/settings/tabs/_RolesRoomSettingsTab.scss
new file mode 100644
index 0000000000..657d23af26
--- /dev/null
+++ b/res/css/views/settings/tabs/_RolesRoomSettingsTab.scss
@@ -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;
+}
\ No newline at end of file
diff --git a/src/components/views/dialogs/RoomSettingsDialog.js b/src/components/views/dialogs/RoomSettingsDialog.js
index 99e73fb2e0..f41eda7a40 100644
--- a/src/components/views/dialogs/RoomSettingsDialog.js
+++ b/src/components/views/dialogs/RoomSettingsDialog.js
@@ -20,6 +20,7 @@ import {Tab, TabbedView} from "../../structures/TabbedView";
import {_t, _td} from "../../../languageHandler";
import AccessibleButton from "../elements/AccessibleButton";
import dis from '../../../dispatcher';
+import RolesRoomSettingsTab from "../settings/tabs/RolesRoomSettingsTab";
import GeneralRoomSettingsTab from "../settings/tabs/GeneralRoomSettingsTab";
// TODO: Ditch this whole component
@@ -73,7 +74,7 @@ export default class RoomSettingsDialog extends React.Component {
tabs.push(new Tab(
_td("Roles & Permissions"),
"mx_RoomSettingsDialog_rolesIcon",
-
Roles Test
,
+ ,
));
tabs.push(new Tab(
_td("Advanced"),
diff --git a/src/components/views/settings/tabs/RolesRoomSettingsTab.js b/src/components/views/settings/tabs/RolesRoomSettingsTab.js
new file mode 100644
index 0000000000..24f94e9d44
--- /dev/null
+++ b/src/components/views/settings/tabs/RolesRoomSettingsTab.js
@@ -0,0 +1,328 @@
+/*
+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 = (
+
+ { _t('Unban') }
+
+ );
+ }
+
+ const userId = this.props.member.name === this.props.member.userId ? null : this.props.member.userId;
+ return (
+
+ {unbanButton}
+
+ { this.props.member.name } {userId}
+ {this.props.reason ? " " + _t('Reason') + ": " + this.props.reason : ""}
+
+
+ );
+ }
+}
+
+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 = {_t('No users have specific privileges in this room')}
;
+ let mutedUsersSection;
+ if (Object.keys(userLevels).length) {
+ const privilegedUsers = [];
+ const mutedUsers = [];
+
+ Object.keys(userLevels).forEach(function(user) {
+ if (userLevels[user] > defaultUserLevel) { // privileged
+ privilegedUsers.push(
+ { _t("%(user)s is a %(userRole)s", {
+ user: user,
+ userRole: ,
+ }) }
+ );
+ } else if (userLevels[user] < defaultUserLevel) { // muted
+ mutedUsers.push(
+ { _t("%(user)s is a %(userRole)s", {
+ user: user,
+ userRole: ,
+ }) }
+ );
+ }
+ });
+
+ // 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 =
+
+
{ _t('Privileged Users') }
+
+
;
+ }
+ if (mutedUsers.length) {
+ mutedUsersSection =
+
+
{ _t('Muted Users') }
+
+
;
+ }
+ }
+
+ const banned = room.getMembersWithMembership("ban");
+ let bannedUsersSection;
+ if (banned.length) {
+ const canBanUsers = currentUserLevel >= banLevel;
+ bannedUsersSection =
+
+
{ _t('Banned users') }
+
+ {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 (
+
+ );
+ })}
+
+
;
+ }
+
+
+
+ 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 ;
+ });
+
+ 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 , you must be a", {},
+ { 'eventType': { eventType }
},
+ );
+ }
+ return (
+
+ );
+ });
+
+ let unfederatableSection;
+ const createEvent = room.currentState.getStateEvents('m.room.create', '');
+ if (createEvent && createEvent.getContent()['m.federate'] === false) {
+ unfederatableSection = {_t('This room is not accessible by remote Matrix servers')}
;
+ }
+
+ return (
+
+
{_t("Roles & Permissions")}
+ {privilegedUsersSection}
+ {mutedUsersSection}
+ {bannedUsersSection}
+
+ {_t("Permissions")}
+ {powerSelectors}
+ {eventPowerSelectors}
+ {unfederatableSection}
+
+
+ );
+ }
+}
diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json
index 296165de9f..57734a2422 100644
--- a/src/i18n/strings/en_EN.json
+++ b/src/i18n/strings/en_EN.json
@@ -481,6 +481,33 @@
"Room list": "Room list",
"Timeline": "Timeline",
"Autocomplete delay (ms)": "Autocomplete delay (ms)",
+ "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 , you must be a": "To send events of type , you must be a",
+ "This room is not accessible by remote Matrix servers": "This room is not accessible by remote Matrix servers",
+ "Roles & Permissions": "Roles & Permissions",
+ "Permissions": "Permissions",
"Unignore": "Unignore",
"": "",
"Import E2E room keys": "Import E2E room keys",
@@ -543,7 +570,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?",
@@ -686,15 +712,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",
@@ -709,26 +726,11 @@
"(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 an address.": "To link to a room it must have an address.",
"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 send events of type , you must be a": "To send events of type , you must be a",
"Upgrade room to version %(ver)s": "Upgrade room to version %(ver)s",
"Open Devtools": "Open Devtools",
"Who can access this room?": "Who can access this room?",
@@ -741,7 +743,6 @@
"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)",
- "Permissions": "Permissions",
"Internal room ID: ": "Internal room ID: ",
"Room version number: ": "Room version number: ",
"Add a topic": "Add a topic",
@@ -1063,7 +1064,6 @@
"To help avoid duplicate issues, please view existing issues first (and add a +1) or create a new issue if you can't find it.": "To help avoid duplicate issues, please view existing issues first (and add a +1) or create a new issue 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",