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') }
    + +
    ; + } + + + + 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
    + {descriptor.desc}  + +
    ; + }); + + 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 ( +
    + {label}  + +
    + ); + }); + + 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",