Merge branch 'develop' into luke/kill-mimage-fixupheight

This commit is contained in:
Luke Barnard 2018-06-11 14:24:40 +01:00 committed by GitHub
commit 2120858e5f
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
67 changed files with 3449 additions and 1744 deletions

View file

@ -33,10 +33,21 @@ export default class DeactivateAccountDialog extends React.Component {
this._onOk = this._onOk.bind(this);
this._onCancel = this._onCancel.bind(this);
this._onPasswordFieldChange = this._onPasswordFieldChange.bind(this);
this._onEraseFieldChange = this._onEraseFieldChange.bind(this);
const deactivationPreferences =
MatrixClientPeg.get().getAccountData('im.riot.account_deactivation_preferences');
const shouldErase = (
deactivationPreferences &&
deactivationPreferences.getContent() &&
deactivationPreferences.getContent().shouldErase
) || false;
this.state = {
confirmButtonEnabled: false,
busy: false,
shouldErase,
errStr: null,
};
}
@ -47,19 +58,55 @@ export default class DeactivateAccountDialog extends React.Component {
});
}
_onOk() {
// This assumes that the HS requires password UI auth
// for this endpoint. In reality it could be any UI auth.
_onEraseFieldChange(ev) {
this.setState({
shouldErase: ev.target.checked,
});
}
async _onOk() {
this.setState({busy: true});
MatrixClientPeg.get().deactivateAccount({
type: 'm.login.password',
user: MatrixClientPeg.get().credentials.userId,
password: this._passwordField.value,
}).done(() => {
Analytics.trackEvent('Account', 'Deactivate Account');
Lifecycle.onLoggedOut();
this.props.onFinished(false);
}, (err) => {
// Before we deactivate the account insert an event into
// the user's account data indicating that they wish to be
// erased from the homeserver.
//
// We do this because the API for erasing after deactivation
// might not be supported by the connected homeserver. Leaving
// an indication in account data is only best-effort, and
// in the worse case, the HS maintainer would have to run a
// script to erase deactivated accounts that have shouldErase
// set to true in im.riot.account_deactivation_preferences.
//
// Note: The preferences are scoped to Riot, hence the
// "im.riot..." event type.
//
// Note: This may have already been set on previous attempts
// where, for example, the user entered the wrong password.
// This is fine because the UI always indicates the preference
// prior to us calling `deactivateAccount`.
try {
await MatrixClientPeg.get().setAccountData('im.riot.account_deactivation_preferences', {
shouldErase: this.state.shouldErase,
});
} catch (err) {
this.setState({
busy: false,
errStr: _t('Failed to indicate account erasure'),
});
return;
}
try {
// This assumes that the HS requires password UI auth
// for this endpoint. In reality it could be any UI auth.
const auth = {
type: 'm.login.password',
user: MatrixClientPeg.get().credentials.userId,
password: this._passwordField.value,
};
await MatrixClientPeg.get().deactivateAccount(auth, this.state.shouldErase);
} catch (err) {
let errStr = _t('Unknown error');
// https://matrix.org/jira/browse/SYN-744
if (err.httpStatus == 401 || err.httpStatus == 403) {
@ -70,7 +117,12 @@ export default class DeactivateAccountDialog extends React.Component {
busy: false,
errStr: errStr,
});
});
return;
}
Analytics.trackEvent('Account', 'Deactivate Account');
Lifecycle.onLoggedOut();
this.props.onFinished(false);
}
_onCancel() {
@ -105,21 +157,64 @@ export default class DeactivateAccountDialog extends React.Component {
onFinished={this.props.onFinished}
onEnterPressed={this.onOk}
titleClass="danger"
title={_t("Deactivate Account")}>
title={_t("Deactivate Account")}
>
<div className="mx_Dialog_content">
<p>{ _t("This will make your account permanently unusable. You will not be able to re-register the same user ID.") }</p>
<p>{ _t(
"This will make your account permanently unusable. " +
"You will not be able to log in, and no one will be able to re-register the same " +
"user ID. " +
"This will cause your account to leave all rooms it is participating in, and it " +
"will remove your account details from your identity server. " +
"<b>This action is irreversible.</b>",
{},
{ b: (sub) => <b> { sub } </b> },
) }</p>
<p>{ _t("This action is irreversible.") }</p>
<p>{ _t(
"Deactivating your account <b>does not by default cause us to forget messages you " +
"have sent.</b> " +
"If you would like us to forget your messages, please tick the box below.",
{},
{ b: (sub) => <b> { sub } </b> },
) }</p>
<p>{ _t("To continue, please enter your password.") }</p>
<p>{ _t(
"Message visibility in Matrix is similar to email. " +
"Our forgetting your messages means that messages you have sent will not be shared " +
"with any new or unregistered users, but registered users who already have access " +
"to these messages will still have access to their copy.",
) }</p>
<div className="mx_DeactivateAccountDialog_input_section">
<p>
<label htmlFor="mx_DeactivateAccountDialog_erase_account_input">
<input
id="mx_DeactivateAccountDialog_erase_account_input"
type="checkbox"
checked={this.state.shouldErase}
onChange={this._onEraseFieldChange}
/>
{ _t(
"Please forget all messages I have sent when my account is deactivated " +
"(<b>Warning:</b> this will cause future users to see an incomplete view " +
"of conversations)",
{},
{ b: (sub) => <b>{ sub }</b> },
) }
</label>
</p>
<p>{ _t("To continue, please enter your password:") }</p>
<input
type="password"
placeholder={_t("password")}
onChange={this._onPasswordFieldChange}
ref={(e) => {this._passwordField = e;}}
className={passwordBoxClass}
/>
</div>
<p>{ _t("Password") }:</p>
<input
type="password"
onChange={this._onPasswordFieldChange}
ref={(e) => {this._passwordField = e;}}
className={passwordBoxClass}
/>
{ error }
</div>
<div className="mx_Dialog_buttons">

View file

@ -132,17 +132,17 @@ class SendCustomEvent extends GenericEditor {
}
return <div>
<div className="mx_Dialog_content">
<div className="mx_DevTools_content">
{ this.textInput('eventType', _t('Event Type')) }
{ this.state.isStateEvent && this.textInput('stateKey', _t('State Key')) }
<br />
<div className="mx_UserSettings_profileLabelCell">
<div className="mx_DevTools_inputLabelCell">
<label htmlFor="evContent"> { _t('Event Content') } </label>
</div>
<div>
<textarea id="evContent" onChange={this._onChange} value={this.state.evContent} className="mx_TextInputDialog_input" cols="63" rows="5" />
<textarea id="evContent" onChange={this._onChange} value={this.state.evContent} className="mx_DevTools_textarea" />
</div>
</div>
<div className="mx_Dialog_buttons">
@ -219,15 +219,15 @@ class SendAccountData extends GenericEditor {
}
return <div>
<div className="mx_Dialog_content">
<div className="mx_DevTools_content">
{ this.textInput('eventType', _t('Event Type')) }
<br />
<div className="mx_UserSettings_profileLabelCell">
<div className="mx_DevTools_inputLabelCell">
<label htmlFor="evContent"> { _t('Event Content') } </label>
</div>
<div>
<textarea id="evContent" onChange={this._onChange} value={this.state.evContent} className="mx_TextInputDialog_input" cols="63" rows="5" />
<textarea id="evContent" onChange={this._onChange} value={this.state.evContent} className="mx_DevTools_textarea" />
</div>
</div>
<div className="mx_Dialog_buttons">
@ -485,7 +485,7 @@ class AccountDataExplorer extends DevtoolsComponent {
}
return <div className="mx_ViewSource">
<div className="mx_Dialog_content">
<div className="mx_DevTools_content">
<SyntaxHighlight className="json">
{ JSON.stringify(this.state.event.event, null, 2) }
</SyntaxHighlight>

View file

@ -67,6 +67,7 @@ export default React.createClass({
{ this.props.description }
</div>
<DialogButtons primaryButton={this.props.button || _t('OK')}
cancelButton={this.props.cancelButton}
onPrimaryButtonClick={this.onOk}
primaryButtonClass={primaryButtonClass}
focus={this.props.focus}

View file

@ -2,6 +2,7 @@ import React from 'react';
import PropTypes from 'prop-types';
import url from 'url';
import { _t } from '../../../languageHandler';
import WidgetUtils from "../../../WidgetUtils";
export default class AppPermission extends React.Component {
constructor(props) {
@ -19,7 +20,7 @@ export default class AppPermission extends React.Component {
const searchParams = new URLSearchParams(wurl.search);
if (this.isScalarWurl(wurl) && searchParams && searchParams.get('url')) {
if (WidgetUtils.isScalarUrl(wurl) && searchParams && searchParams.get('url')) {
curl = url.parse(searchParams.get('url'));
if (curl) {
curl.search = curl.query = "";
@ -33,25 +34,16 @@ export default class AppPermission extends React.Component {
return curlString;
}
isScalarWurl(wurl) {
if (wurl && wurl.hostname && (
wurl.hostname === 'scalar.vector.im' ||
wurl.hostname === 'scalar-staging.riot.im' ||
wurl.hostname === 'scalar-develop.riot.im' ||
wurl.hostname === 'demo.riot.im' ||
wurl.hostname === 'localhost'
)) {
return true;
}
return false;
}
render() {
let e2eWarningText;
if (this.props.isRoomEncrypted) {
e2eWarningText =
<span className='mx_AppPermissionWarningTextLabel'>{ _t('NOTE: Apps are not end-to-end encrypted') }</span>;
}
const cookieWarning =
<span className='mx_AppPermissionWarningTextLabel'>
{ _t('Warning: This widget might use cookies.') }
</span>;
return (
<div className='mx_AppPermissionWarning'>
<div className='mx_AppPermissionWarningImage'>
@ -60,6 +52,7 @@ export default class AppPermission extends React.Component {
<div className='mx_AppPermissionWarningText'>
<span className='mx_AppPermissionWarningTextLabel'>{ _t('Do you want to load widget from URL:') }</span> <span className='mx_AppPermissionWarningTextURL'>{ this.state.curlBase }</span>
{ e2eWarningText }
{ cookieWarning }
</div>
<input
className='mx_AppPermissionButton'

View file

@ -25,7 +25,6 @@ import PlatformPeg from '../../../PlatformPeg';
import ScalarAuthClient from '../../../ScalarAuthClient';
import WidgetMessaging from '../../../WidgetMessaging';
import TintableSvgButton from './TintableSvgButton';
import SdkConfig from '../../../SdkConfig';
import Modal from '../../../Modal';
import { _t, _td } from '../../../languageHandler';
import sdk from '../../../index';
@ -55,6 +54,7 @@ export default class AppTile extends React.Component {
this._grantWidgetPermission = this._grantWidgetPermission.bind(this);
this._revokeWidgetPermission = this._revokeWidgetPermission.bind(this);
this._onPopoutWidgetClick = this._onPopoutWidgetClick.bind(this);
this._onReloadWidgetClick = this._onReloadWidgetClick.bind(this);
}
/**
@ -120,30 +120,6 @@ export default class AppTile extends React.Component {
return u.format();
}
/**
* Returns true if specified url is a scalar URL, typically https://scalar.vector.im/api
* @param {[type]} url URL to check
* @return {Boolean} True if specified URL is a scalar URL
*/
isScalarUrl(url) {
if (!url) {
console.error('Scalar URL check failed. No URL specified');
return false;
}
let scalarUrls = SdkConfig.get().integrations_widgets_urls;
if (!scalarUrls || scalarUrls.length == 0) {
scalarUrls = [SdkConfig.get().integrations_rest_url];
}
for (let i = 0; i < scalarUrls.length; i++) {
if (url.startsWith(scalarUrls[i])) {
return true;
}
}
return false;
}
isMixedContent() {
const parentContentProtocol = window.location.protocol;
const u = url.parse(this.props.url);
@ -199,7 +175,7 @@ export default class AppTile extends React.Component {
setScalarToken() {
this.setState({initialising: true});
if (!this.isScalarUrl(this.props.url)) {
if (!WidgetUtils.isScalarUrl(this.props.url)) {
console.warn('Non-scalar widget, not setting scalar token!', url);
this.setState({
error: null,
@ -269,7 +245,12 @@ export default class AppTile extends React.Component {
event.origin = event.originalEvent.origin;
}
if (!this.state.widgetUrl.startsWith(event.origin)) {
const widgetUrlObj = url.parse(this.state.widgetUrl);
const eventOrigin = url.parse(event.origin);
if (
eventOrigin.protocol !== widgetUrlObj.protocol ||
eventOrigin.host !== widgetUrlObj.host
) {
return;
}
@ -519,6 +500,11 @@ export default class AppTile extends React.Component {
{ target: '_blank', href: this._getSafeUrl(), rel: 'noopener noreferrer'}).click();
}
_onReloadWidgetClick(e) {
// Reload iframe in this way to avoid cross-origin restrictions
this.refs.appFrame.src = this.refs.appFrame.src;
}
render() {
let appTileBody;
@ -606,6 +592,7 @@ export default class AppTile extends React.Component {
const showPictureSnapshotButton = this._hasCapability('m.capability.screenshot') && this.props.show;
const showPictureSnapshotIcon = 'img/camera_green.svg';
const popoutWidgetIcon = 'img/button-new-window.svg';
const reloadWidgetIcon = 'img/button-refresh.svg';
const windowStateIcon = (this.props.show ? 'img/minimize.svg' : 'img/maximize.svg');
return (
@ -624,6 +611,16 @@ export default class AppTile extends React.Component {
{ this.props.showTitle && this._getTileTitle() }
</span>
<span className="mx_AppTileMenuBarWidgets">
{ /* Reload widget */ }
{ this.props.showReload && <TintableSvgButton
src={reloadWidgetIcon}
className="mx_AppTileMenuBarWidget mx_AppTileMenuBarWidgetPadding"
title={_t('Reload widget')}
onClick={this._onReloadWidgetClick}
width="10"
height="10"
/> }
{ /* Popout widget */ }
{ this.props.showPopout && <TintableSvgButton
src={popoutWidgetIcon}
@ -707,6 +704,11 @@ AppTile.propTypes = {
showDelete: PropTypes.bool,
// Optionally hide the popout widget icon
showPopout: PropTypes.bool,
// Optionally show the reload widget icon
// This is not currently intended for use with production widgets. However
// it can be useful when developing persistent widgets in order to avoid
// having to reload all of riot to get new widget content.
showReload: PropTypes.bool,
// Widget capabilities to allow by default (without user confirmation)
// NOTE -- Use with caution. This is intended to aid better integration / UX
// basic widget capabilities, e.g. injecting sticker message events.
@ -726,6 +728,7 @@ AppTile.defaultProps = {
showMinimise: true,
showDelete: true,
showPopout: true,
showReload: false,
handleMinimisePointerEvents: false,
whitelistCapabilities: [],
userWidget: false,

View file

@ -17,6 +17,7 @@ limitations under the License.
import TagTile from './TagTile';
import React from 'react';
import { Draggable } from 'react-beautiful-dnd';
export default function DNDTagTile(props) {

View file

@ -29,6 +29,9 @@ module.exports = React.createClass({
// The primary button which is styled differently and has default focus.
primaryButton: PropTypes.node.isRequired,
// A node to insert into the cancel button instead of default "Cancel"
cancelButton: PropTypes.node,
// onClick handler for the primary button.
onPrimaryButtonClick: PropTypes.func.isRequired,
@ -60,9 +63,9 @@ module.exports = React.createClass({
primaryButtonClassName += " " + this.props.primaryButtonClass;
}
let cancelButton;
if (this.props.hasCancel) {
if (this.props.cancelButton || this.props.hasCancel) {
cancelButton = <button onClick={this._onCancelClick} disabled={this.props.disabled}>
{ _t("Cancel") }
{ this.props.cancelButton || _t("Cancel") }
</button>;
}
return (

View file

@ -19,6 +19,7 @@ import PropTypes from 'prop-types';
import dis from '../../../dispatcher';
import { _t } from '../../../languageHandler';
import sdk from '../../../index';
import Analytics from '../../../Analytics';
export default class CookieBar extends React.Component {
static propTypes = {
@ -29,6 +30,10 @@ export default class CookieBar extends React.Component {
super();
}
onUsageDataClicked() {
Analytics.showDetailsModal();
}
onAccept() {
dis.dispatch({
action: 'accept_cookies',
@ -49,11 +54,18 @@ export default class CookieBar extends React.Component {
<img className="mx_MatrixToolbar_warning" src="img/warning.svg" width="24" height="23" alt="Warning" />
<div className="mx_MatrixToolbar_content">
{ this.props.policyUrl ? _t(
"Help improve Riot by sending usage data? " +
"This will use a cookie. " +
"(See our <PolicyLink>cookie and privacy policies</PolicyLink>).",
"Please help improve Riot.im by sending <UsageDataLink>anonymous usage data</UsageDataLink>. " +
"This will use a cookie " +
"(please see our <PolicyLink>Cookie Policy</PolicyLink>).",
{},
{
'UsageDataLink': (sub) => <a
className="mx_MatrixToolbar_link"
href="javascript:;"
onClick={this.onUsageDataClicked}
>
{ sub }
</a>,
// XXX: We need to link to the page that explains our cookies
'PolicyLink': (sub) => <a
className="mx_MatrixToolbar_link"
@ -64,10 +76,23 @@ export default class CookieBar extends React.Component {
</a>
,
},
) : _t("Help improve Riot by sending usage data? This will use a cookie.") }
) : _t(
"Please help improve Riot.im by sending <UsageDataLink>anonymous usage data</UsageDataLink>. " +
"This will use a cookie.",
{},
{
'UsageDataLink': (sub) => <a
className="mx_MatrixToolbar_link"
href="javascript:;"
onClick={this.onUsageDataClicked}
>
{ sub }
</a>,
},
) }
</div>
<AccessibleButton element='button' className="mx_MatrixToolbar_action" onClick={this.onAccept}>
{ _t("Yes please") }
{ _t("Yes, I want to help!") }
</AccessibleButton>
<AccessibleButton className="mx_MatrixToolbar_close" onClick={this.onReject}>
<img src="img/cancel.svg" width="18" height="18" />

View file

@ -29,8 +29,6 @@ import Promise from 'bluebird';
import { _t } from '../../../languageHandler';
import SettingsStore from "../../../settings/SettingsStore";
const THUMBNAIL_MAX_HEIGHT = 600;
export default class extends React.Component {
displayName: 'MImageBody'
@ -40,6 +38,9 @@ export default class extends React.Component {
/* called when the image has loaded */
onWidgetLoad: PropTypes.func.isRequired,
/* the maximum image height to use */
maxImageHeight: PropTypes.number,
}
static contextTypes = {
@ -239,7 +240,7 @@ export default class extends React.Component {
_messageContent(contentUrl, thumbUrl, content) {
// The maximum height of the thumbnail as it is rendered as an <img>
const maxHeight = Math.min(THUMBNAIL_MAX_HEIGHT, content.info.h);
const maxHeight = Math.min(this.props.maxImageHeight || 600, content.info.h);
// The maximum width of the thumbnail, as dictated by its natural
// maximum height.
const maxWidth = content.info.w * maxHeight / content.info.h;

View file

@ -39,8 +39,11 @@ module.exports = React.createClass({
/* callback called when dynamic content in events are loaded */
onWidgetLoad: PropTypes.func,
/* the shsape of the tile, used */
/* the shape of the tile, used */
tileShape: PropTypes.string,
/* the maximum image height to use, if the event is an image */
maxImageHeight: PropTypes.number,
},
getEventTileOps: function() {
@ -78,6 +81,7 @@ module.exports = React.createClass({
highlightLink={this.props.highlightLink}
showUrlPreview={this.props.showUrlPreview}
tileShape={this.props.tileShape}
maxImageHeight={this.props.maxImageHeight}
onWidgetLoad={this.props.onWidgetLoad} />;
},
});

View file

@ -270,7 +270,7 @@ module.exports = React.createClass({
// console.log("comparing " + this.memberString(memberA) + " and " + this.memberString(memberB));
if (userA.currentlyActive && userB.currentlyActive) {
if ((userA.currentlyActive && userB.currentlyActive) || !this._showPresence) {
// console.log(memberA.name + " and " + memberB.name + " are both active");
if (memberA.powerLevel === memberB.powerLevel) {
// console.log(memberA + " and " + memberB + " have same power level");

View file

@ -22,6 +22,7 @@ import AccessibleButton from "../elements/AccessibleButton";
import MessageEvent from "../messages/MessageEvent";
import MemberAvatar from "../avatars/MemberAvatar";
import { _t } from '../../../languageHandler';
import {formatFullDate} from '../../../DateUtils';
module.exports = React.createClass({
displayName: 'PinnedEventTile',
@ -80,11 +81,20 @@ module.exports = React.createClass({
{ unpinButton }
</div>
<MemberAvatar member={sender} width={avatarSize} height={avatarSize} />
<span className="mx_PinnedEventTile_senderAvatar">
<MemberAvatar member={sender} width={avatarSize} height={avatarSize} />
</span>
<span className="mx_PinnedEventTile_sender">
{ sender.name }
</span>
<MessageEvent mxEvent={this.props.mxEvent} className="mx_PinnedEventTile_body" />
<span className="mx_PinnedEventTile_timestamp">
{ formatFullDate(new Date(this.props.mxEvent.getTs())) }
</span>
<div className="mx_PinnedEventTile_message">
<MessageEvent mxEvent={this.props.mxEvent} className="mx_PinnedEventTile_body" maxImageHeight={150}
onWidgetLoad={() => {}} // we need to give this, apparently
/>
</div>
</div>
);
},

View file

@ -39,6 +39,19 @@ module.exports = React.createClass({
componentDidMount: function() {
this._updatePinnedMessages();
MatrixClientPeg.get().on("RoomState.events", this._onStateEvent);
},
componentWillUnmount: function() {
if (MatrixClientPeg.get()) {
MatrixClientPeg.get().removeListener("RoomState.events", this._onStateEvent);
}
},
_onStateEvent: function(ev) {
if (ev.getRoomId() === this.props.room.roomId && ev.getType() === "m.room.pinned_events") {
this._updatePinnedMessages();
}
},
_updatePinnedMessages: function() {

View file

@ -395,7 +395,17 @@ module.exports = React.createClass({
powerLevels["events"] = Object.assign({}, this.state.powerLevels["events"] || {});
powerLevels["events"][powerLevelKey.slice(eventsLevelPrefix.length)] = value;
} else {
powerLevels[powerLevelKey] = value;
const keyPath = powerLevelKey.split('.');
let parentObj;
let currentObj = powerLevels;
for (const key of keyPath) {
if (!currentObj[key]) {
currentObj[key] = {};
}
parentObj = currentObj;
currentObj = currentObj[key];
}
parentObj[keyPath[keyPath.length - 1]] = value;
}
this.setState({
powerLevels,
@ -664,6 +674,10 @@ module.exports = React.createClass({
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 banLevel = parseIntWithDefault(powerLevels.ban, powerLevelDescriptors.ban.defaultValue);
@ -695,26 +709,57 @@ module.exports = React.createClass({
relatedGroupsEvent={this.props.room.currentState.getStateEvents('m.room.related_groups', '')}
/>;
let userLevelsSection;
let privilegedUsersSection = <div>{ _t('No users have specific privileges in this room') }.</div>; // default
let mutedUsersSection;
if (Object.keys(userLevels).length) {
userLevelsSection =
<div>
<h3>{ _t('Privileged Users') }</h3>
<ul className="mx_RoomSettings_userLevels">
{ Object.keys(userLevels).map(function(user, i) {
return (
<li className="mx_RoomSettings_userLevel" key={user}>
{ _t("%(user)s is a %(userRole)s", {
user: user,
userRole: <PowerSelector value={userLevels[user]} disabled={true} />,
}) }
</li>
);
const privilegedUsers = [];
const mutedUsers = [];
Object.keys(userLevels).forEach(function(user) {
if (userLevels[user] > defaultUserLevel) { // privileged
privilegedUsers.push(<li className="mx_RoomSettings_userLevel" key={user}>
{ _t("%(user)s is a %(userRole)s", {
user: user,
userRole: <PowerSelector value={userLevels[user]} disabled={true} />,
}) }
</ul>
</div>;
} else {
userLevelsSection = <div>{ _t('No users have specific privileges in this room') }.</div>;
</li>);
} else if (userLevels[user] < defaultUserLevel) { // muted
mutedUsers.push(<li className="mx_RoomSettings_userLevel" 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>
<h3>{ _t('Privileged Users') }</h3>
<ul className="mx_RoomSettings_userLevels">
{ privilegedUsers }
</ul>
</div>;
}
if (mutedUsers.length) {
mutedUsersSection =
<div>
<h3>{ _t('Muted Users') }</h3>
<ul className="mx_RoomSettings_userLevels">
{ mutedUsers }
</ul>
</div>;
}
}
const banned = this.props.room.getMembersWithMembership("ban");
@ -834,7 +879,16 @@ module.exports = React.createClass({
const powerSelectors = Object.keys(powerLevelDescriptors).map((key, index) => {
const descriptor = powerLevelDescriptors[key];
const value = parseIntWithDefault(powerLevels[key], descriptor.defaultValue);
const keyPath = key.split('.');
let currentObj = powerLevels;
for (const prop of keyPath) {
if (currentObj === undefined) {
break;
}
currentObj = currentObj[prop];
}
const value = parseIntWithDefault(currentObj, descriptor.defaultValue);
return <div key={index} className="mx_RoomSettings_powerLevel">
<span className="mx_RoomSettings_powerLevelKey">
{ descriptor.desc }
@ -979,8 +1033,8 @@ module.exports = React.createClass({
{ unfederatableSection }
</div>
{ userLevelsSection }
{ privilegedUsersSection }
{ mutedUsersSection }
{ bannedUsersSection }
<h3>{ _t('Advanced') }</h3>