Merge remote-tracking branch 'upstream/develop' into feature/hidden-rrs

Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
This commit is contained in:
Šimon Brandner 2021-07-27 18:39:22 +02:00
commit 9761c38f3d
No known key found for this signature in database
GPG key ID: 55C211A1226CB17D
516 changed files with 13205 additions and 8585 deletions

View file

@ -59,7 +59,7 @@ const AvatarSetting = ({ avatarUrl, avatarAltText, avatarName, uploadAvatar, rem
let removeAvatarBtn;
if (avatarUrl && removeAvatar) {
removeAvatarBtn = <AccessibleButton onClick={removeAvatar} kind="link_sm">
{_t("Remove")}
{ _t("Remove") }
</AccessibleButton>;
}
@ -68,13 +68,13 @@ const AvatarSetting = ({ avatarUrl, avatarAltText, avatarName, uploadAvatar, rem
"mx_AvatarSetting_avatar_hovering": isHovering && uploadAvatar,
});
return <div className={avatarClasses}>
{avatarElement}
{ avatarElement }
<div className="mx_AvatarSetting_hover">
<div className="mx_AvatarSetting_hoverBg" />
<span>{_t("Upload")}</span>
<span>{ _t("Upload") }</span>
</div>
{uploadAvatarBtn}
{removeAvatarBtn}
{ uploadAvatarBtn }
{ removeAvatarBtn }
</div>;
};

View file

@ -91,24 +91,24 @@ export default class BridgeTile extends React.PureComponent<IProps> {
let creator = null;
if (content.creator) {
creator = <li>{_t("This bridge was provisioned by <user />.", {}, {
creator = <li>{ _t("This bridge was provisioned by <user />.", {}, {
user: () => <Pill
type={Pill.TYPE_USER_MENTION}
room={this.props.room}
url={makeUserPermalink(content.creator)}
shouldShowPillAvatar={SettingsStore.getValue("Pill.shouldShowPillAvatar")}
/>,
})}</li>;
}) }</li>;
}
const bot = <li>{_t("This bridge is managed by <user />.", {}, {
const bot = <li>{ _t("This bridge is managed by <user />.", {}, {
user: () => <Pill
type={Pill.TYPE_USER_MENTION}
room={this.props.room}
url={makeUserPermalink(content.bridgebot)}
shouldShowPillAvatar={SettingsStore.getValue("Pill.shouldShowPillAvatar")}
/>,
})}</li>;
}) }</li>;
let networkIcon;
@ -119,20 +119,20 @@ export default class BridgeTile extends React.PureComponent<IProps> {
width={48}
height={48}
resizeMethod='crop'
name={ protocolName }
idName={ protocolName }
url={ avatarUrl }
name={protocolName}
idName={protocolName}
url={avatarUrl}
/>;
} else {
networkIcon = <div className="noProtocolIcon"></div>;
networkIcon = <div className="noProtocolIcon" />;
}
let networkItem = null;
if (network) {
const networkName = network.displayname || network.id;
let networkLink = <span>{networkName}</span>;
let networkLink = <span>{ networkName }</span>;
if (typeof network.external_url === "string" && isUrlPermitted(network.external_url)) {
networkLink = (
<a href={network.external_url} target="_blank" rel="noreferrer noopener">{networkName}</a>
<a href={network.external_url} target="_blank" rel="noreferrer noopener">{ networkName }</a>
);
}
networkItem = _t("Workspace: <networkLink/>", {}, {
@ -140,26 +140,26 @@ export default class BridgeTile extends React.PureComponent<IProps> {
});
}
let channelLink = <span>{channelName}</span>;
let channelLink = <span>{ channelName }</span>;
if (typeof channel.external_url === "string" && isUrlPermitted(channel.external_url)) {
channelLink = <a href={channel.external_url} target="_blank" rel="noreferrer noopener">{channelName}</a>;
channelLink = <a href={channel.external_url} target="_blank" rel="noreferrer noopener">{ channelName }</a>;
}
const id = this.props.ev.getId();
return (<li key={id}>
<div className="column-icon">
{networkIcon}
{ networkIcon }
</div>
<div className="column-data">
<h3>{protocolName}</h3>
<h3>{ protocolName }</h3>
<p className="workspace-channel-details">
{networkItem}
<span className="channel">{_t("Channel: <channelLink/>", {}, {
{ networkItem }
<span className="channel">{ _t("Channel: <channelLink/>", {}, {
channelLink: () => channelLink,
})}</span>
}) }</span>
</p>
<ul className="metadata">
{creator} {bot}
{ creator } { bot }
</ul>
</div>
</li>);

View file

@ -148,13 +148,22 @@ export default class ChangeAvatar extends React.Component {
if (this.props.room && !this.avatarSet) {
const RoomAvatar = sdk.getComponent('avatars.RoomAvatar');
avatarImg = <RoomAvatar
room={this.props.room} width={this.props.width} height={this.props.height} resizeMethod='crop'
room={this.props.room}
width={this.props.width}
height={this.props.height}
resizeMethod='crop'
/>;
} else {
const BaseAvatar = sdk.getComponent("avatars.BaseAvatar");
// XXX: FIXME: once we track in the JS what our own displayname is(!) then use it here rather than ?
avatarImg = <BaseAvatar width={this.props.width} height={this.props.height} resizeMethod='crop'
name='?' idName={MatrixClientPeg.get().getUserIdLocalpart()} url={this.state.avatarUrl} />;
avatarImg = <BaseAvatar
width={this.props.width}
height={this.props.height}
resizeMethod='crop'
name='?'
idName={MatrixClientPeg.get().getUserIdLocalpart()}
url={this.state.avatarUrl}
/>;
}
let uploadSection;

View file

@ -99,7 +99,7 @@ export default class ChangePassword extends React.Component {
'and re-import them afterwards. ' +
'In future this will be improved.',
) }
{' '}
{ ' ' }
<a href="https://github.com/vector-im/element-web/issues/2671" target="_blank" rel="noreferrer noopener">
https://github.com/vector-im/element-web/issues/2671
</a>

View file

@ -163,29 +163,29 @@ export default class CrossSigningPanel extends React.PureComponent {
let errorSection;
if (error) {
errorSection = <div className="error">{error.toString()}</div>;
errorSection = <div className="error">{ error.toString() }</div>;
}
let summarisedStatus;
if (homeserverSupportsCrossSigning === undefined) {
summarisedStatus = <Spinner />;
} else if (!homeserverSupportsCrossSigning) {
summarisedStatus = <p>{_t(
summarisedStatus = <p>{ _t(
"Your homeserver does not support cross-signing.",
)}</p>;
) }</p>;
} else if (crossSigningReady) {
summarisedStatus = <p> {_t(
summarisedStatus = <p> { _t(
"Cross-signing is ready for use.",
)}</p>;
) }</p>;
} else if (crossSigningPrivateKeysInStorage) {
summarisedStatus = <p>{_t(
summarisedStatus = <p>{ _t(
"Your account has a cross-signing identity in secret storage, " +
"but it is not yet trusted by this session.",
)}</p>;
) }</p>;
} else {
summarisedStatus = <p>{_t(
summarisedStatus = <p>{ _t(
"Cross-signing is not set up.",
)}</p>;
) }</p>;
}
const keysExistAnywhere = (
@ -209,7 +209,7 @@ export default class CrossSigningPanel extends React.PureComponent {
if (!keysExistEverywhere && homeserverSupportsCrossSigning) {
actions.push(
<AccessibleButton key="setup" kind="primary" onClick={this._onBootstrapClick}>
{_t("Set up")}
{ _t("Set up") }
</AccessibleButton>,
);
}
@ -217,7 +217,7 @@ export default class CrossSigningPanel extends React.PureComponent {
if (keysExistAnywhere) {
actions.push(
<AccessibleButton key="reset" kind="danger" onClick={this._resetCrossSigning}>
{_t("Reset")}
{ _t("Reset") }
</AccessibleButton>,
);
}
@ -225,44 +225,44 @@ export default class CrossSigningPanel extends React.PureComponent {
let actionRow;
if (actions.length) {
actionRow = <div className="mx_CrossSigningPanel_buttonRow">
{actions}
{ actions }
</div>;
}
return (
<div>
{summarisedStatus}
{ summarisedStatus }
<details>
<summary>{_t("Advanced")}</summary>
<summary>{ _t("Advanced") }</summary>
<table className="mx_CrossSigningPanel_statusList"><tbody>
<tr>
<td>{_t("Cross-signing public keys:")}</td>
<td>{crossSigningPublicKeysOnDevice ? _t("in memory") : _t("not found")}</td>
<td>{ _t("Cross-signing public keys:") }</td>
<td>{ crossSigningPublicKeysOnDevice ? _t("in memory") : _t("not found") }</td>
</tr>
<tr>
<td>{_t("Cross-signing private keys:")}</td>
<td>{crossSigningPrivateKeysInStorage ? _t("in secret storage") : _t("not found in storage")}</td>
<td>{ _t("Cross-signing private keys:") }</td>
<td>{ crossSigningPrivateKeysInStorage ? _t("in secret storage") : _t("not found in storage") }</td>
</tr>
<tr>
<td>{_t("Master private key:")}</td>
<td>{masterPrivateKeyCached ? _t("cached locally") : _t("not found locally")}</td>
<td>{ _t("Master private key:") }</td>
<td>{ masterPrivateKeyCached ? _t("cached locally") : _t("not found locally") }</td>
</tr>
<tr>
<td>{_t("Self signing private key:")}</td>
<td>{selfSigningPrivateKeyCached ? _t("cached locally") : _t("not found locally")}</td>
<td>{ _t("Self signing private key:") }</td>
<td>{ selfSigningPrivateKeyCached ? _t("cached locally") : _t("not found locally") }</td>
</tr>
<tr>
<td>{_t("User signing private key:")}</td>
<td>{userSigningPrivateKeyCached ? _t("cached locally") : _t("not found locally")}</td>
<td>{ _t("User signing private key:") }</td>
<td>{ userSigningPrivateKeyCached ? _t("cached locally") : _t("not found locally") }</td>
</tr>
<tr>
<td>{_t("Homeserver feature support:")}</td>
<td>{homeserverSupportsCrossSigning ? _t("exists") : _t("not found")}</td>
<td>{ _t("Homeserver feature support:") }</td>
<td>{ homeserverSupportsCrossSigning ? _t("exists") : _t("not found") }</td>
</tr>
</tbody></table>
</details>
{errorSection}
{actionRow}
{ errorSection }
{ actionRow }
</div>
);
}

View file

@ -213,7 +213,7 @@ export default class DevicesPanel extends React.Component {
const deleteButton = this.state.deleting ?
<Spinner w={22} h={22} /> :
<AccessibleButton onClick={this._onDeleteClick} kind="danger_sm">
{ _t("Delete %(count)s sessions", { count: this.state.selectedDevices.length })}
{ _t("Delete %(count)s sessions", { count: this.state.selectedDevices.length }) }
</AccessibleButton>;
const classes = classNames(this.props.className, "mx_DevicesPanel");

View file

@ -25,14 +25,14 @@ const SETTING_MANUALLY_VERIFY_ALL_SESSIONS = "e2ee.manuallyVerifyAllSessions";
const E2eAdvancedPanel = props => {
return <div className="mx_SettingsTab_section">
<span className="mx_SettingsTab_subheading">{_t("Encryption")}</span>
<span className="mx_SettingsTab_subheading">{ _t("Encryption") }</span>
<SettingsFlag name={SETTING_MANUALLY_VERIFY_ALL_SESSIONS}
level={SettingLevel.DEVICE}
/>
<div className="mx_E2eAdvancedPanel_settingLongDescription">{_t(
<div className="mx_E2eAdvancedPanel_settingLongDescription">{ _t(
"Individually verify each session used by a user to mark it as trusted, not trusting cross-signed devices.",
)}</div>
) }</div>
</div>;
};

View file

@ -152,7 +152,7 @@ export default class EventIndexPanel extends React.Component<{}, IState> {
if (EventIndexPeg.get() !== null) {
eventIndexingSettings = (
<div>
<div className='mx_SettingsTab_subsectionText'>{_t(
<div className='mx_SettingsTab_subsectionText'>{ _t(
"Securely cache encrypted messages locally for them " +
"to appear in search results, using %(size)s to store messages from %(rooms)s rooms.",
{
@ -162,10 +162,10 @@ export default class EventIndexPanel extends React.Component<{}, IState> {
count: this.state.roomCount,
rooms: formatCountLong(this.state.roomCount),
},
)}</div>
) }</div>
<div>
<AccessibleButton kind="primary" onClick={this.onManage}>
{_t("Manage")}
{ _t("Manage") }
</AccessibleButton>
</div>
</div>
@ -173,16 +173,19 @@ export default class EventIndexPanel extends React.Component<{}, IState> {
} else if (!this.state.eventIndexingEnabled && EventIndexPeg.supportIsInstalled()) {
eventIndexingSettings = (
<div>
<div className='mx_SettingsTab_subsectionText'>{_t(
<div className='mx_SettingsTab_subsectionText'>{ _t(
"Securely cache encrypted messages locally for them to " +
"appear in search results.",
)}</div>
) }</div>
<div>
<AccessibleButton kind="primary" disabled={this.state.enabling}
onClick={this.onEnable}>
{_t("Enable")}
<AccessibleButton
kind="primary"
disabled={this.state.enabling}
onClick={this.onEnable}
>
{ _t("Enable") }
</AccessibleButton>
{this.state.enabling ? <InlineSpinner /> : <div />}
{ this.state.enabling ? <InlineSpinner /> : <div /> }
</div>
</div>
);
@ -194,7 +197,7 @@ export default class EventIndexPanel extends React.Component<{}, IState> {
);
eventIndexingSettings = (
<div className='mx_SettingsTab_subsectionText'>{_t(
<div className='mx_SettingsTab_subsectionText'>{ _t(
"%(brand)s is missing some components required for securely " +
"caching encrypted messages locally. If you'd like to " +
"experiment with this feature, build a custom %(brand)s Desktop " +
@ -203,15 +206,17 @@ export default class EventIndexPanel extends React.Component<{}, IState> {
brand,
},
{
nativeLink: sub => <a href={nativeLink}
target="_blank" rel="noreferrer noopener"
>{sub}</a>,
nativeLink: sub => <a
href={nativeLink}
target="_blank"
rel="noreferrer noopener"
>{ sub }</a>,
},
)}</div>
) }</div>
);
} else if (!EventIndexPeg.platformHasSupport()) {
eventIndexingSettings = (
<div className='mx_SettingsTab_subsectionText'>{_t(
<div className='mx_SettingsTab_subsectionText'>{ _t(
"%(brand)s can't securely cache encrypted messages locally " +
"while running in a web browser. Use <desktopLink>%(brand)s Desktop</desktopLink> " +
"for encrypted messages to appear in search results.",
@ -219,34 +224,36 @@ export default class EventIndexPanel extends React.Component<{}, IState> {
brand,
},
{
desktopLink: sub => <a href="https://element.io/get-started"
target="_blank" rel="noreferrer noopener"
>{sub}</a>,
desktopLink: sub => <a
href="https://element.io/get-started"
target="_blank"
rel="noreferrer noopener"
>{ sub }</a>,
},
)}</div>
) }</div>
);
} else {
eventIndexingSettings = (
<div className='mx_SettingsTab_subsectionText'>
<p>
{this.state.enabling
{ this.state.enabling
? <InlineSpinner />
: _t("Message search initialisation failed")
}
</p>
{EventIndexPeg.error && (
{ EventIndexPeg.error && (
<details>
<summary>{_t("Advanced")}</summary>
<summary>{ _t("Advanced") }</summary>
<code>
{EventIndexPeg.error.message}
{ EventIndexPeg.error.message }
</code>
<p>
<AccessibleButton key="delete" kind="danger" onClick={this.confirmEventStoreReset}>
{_t("Reset")}
{ _t("Reset") }
</AccessibleButton>
</p>
</details>
)}
) }
</div>
);
}

View file

@ -85,7 +85,7 @@ export default class IntegrationManager extends React.Component {
const Spinner = sdk.getComponent("elements.Spinner");
return (
<div className='mx_IntegrationManager_loading'>
<h3>{_t("Connecting to integration manager...")}</h3>
<h3>{ _t("Connecting to integration manager...") }</h3>
<Spinner />
</div>
);
@ -94,8 +94,8 @@ export default class IntegrationManager extends React.Component {
if (!this.props.connected || this.state.errored) {
return (
<div className='mx_IntegrationManager_error'>
<h3>{_t("Cannot connect to integration manager")}</h3>
<p>{_t("The integration manager is offline or it cannot reach your homeserver.")}</p>
<h3>{ _t("Cannot connect to integration manager") }</h3>
<p>{ _t("The integration manager is offline or it cannot reach your homeserver.") }</p>
</div>
);
}

View file

@ -1,917 +0,0 @@
/*
Copyright 2016 OpenMarket Ltd
Copyright 2020 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 from 'react';
import * as sdk from '../../../index';
import { _t } from '../../../languageHandler';
import { MatrixClientPeg } from '../../../MatrixClientPeg';
import SettingsStore from '../../../settings/SettingsStore';
import Modal from '../../../Modal';
import {
NotificationUtils,
VectorPushRulesDefinitions,
PushRuleVectorState,
ContentRules,
} from '../../../notifications';
import SdkConfig from "../../../SdkConfig";
import LabelledToggleSwitch from "../elements/LabelledToggleSwitch";
import AccessibleButton from "../elements/AccessibleButton";
import { SettingLevel } from "../../../settings/SettingLevel";
import { UIFeature } from "../../../settings/UIFeature";
import { replaceableComponent } from "../../../utils/replaceableComponent";
// TODO: this "view" component still has far too much application logic in it,
// which should be factored out to other files.
// TODO: this component also does a lot of direct poking into this.state, which
// is VERY NAUGHTY.
/**
* Rules that Vector used to set in order to override the actions of default rules.
* These are used to port peoples existing overrides to match the current API.
* These can be removed and forgotten once everyone has moved to the new client.
*/
const LEGACY_RULES = {
"im.vector.rule.contains_display_name": ".m.rule.contains_display_name",
"im.vector.rule.room_one_to_one": ".m.rule.room_one_to_one",
"im.vector.rule.room_message": ".m.rule.message",
"im.vector.rule.invite_for_me": ".m.rule.invite_for_me",
"im.vector.rule.call": ".m.rule.call",
"im.vector.rule.notices": ".m.rule.suppress_notices",
};
function portLegacyActions(actions) {
const decoded = NotificationUtils.decodeActions(actions);
if (decoded !== null) {
return NotificationUtils.encodeActions(decoded);
} else {
// We don't recognise one of the actions here, so we don't try to
// canonicalise them.
return actions;
}
}
@replaceableComponent("views.settings.Notifications")
export default class Notifications extends React.Component {
static phases = {
LOADING: "LOADING", // The component is loading or sending data to the hs
DISPLAY: "DISPLAY", // The component is ready and display data
ERROR: "ERROR", // There was an error
};
state = {
phase: Notifications.phases.LOADING,
masterPushRule: undefined, // The master rule ('.m.rule.master')
vectorPushRules: [], // HS default push rules displayed in Vector UI
vectorContentRules: { // Keyword push rules displayed in Vector UI
vectorState: PushRuleVectorState.ON,
rules: [],
},
externalPushRules: [], // Push rules (except content rule) that have been defined outside Vector UI
externalContentRules: [], // Keyword push rules that have been defined outside Vector UI
threepids: [], // used for email notifications
};
componentDidMount() {
this._refreshFromServer();
}
onEnableNotificationsChange = (checked) => {
const self = this;
this.setState({
phase: Notifications.phases.LOADING,
});
MatrixClientPeg.get().setPushRuleEnabled(
'global', self.state.masterPushRule.kind, self.state.masterPushRule.rule_id, !checked,
).then(function() {
self._refreshFromServer();
});
};
onEnableDesktopNotificationsChange = (checked) => {
SettingsStore.setValue(
"notificationsEnabled", null,
SettingLevel.DEVICE,
checked,
).finally(() => {
this.forceUpdate();
});
};
onEnableDesktopNotificationBodyChange = (checked) => {
SettingsStore.setValue(
"notificationBodyEnabled", null,
SettingLevel.DEVICE,
checked,
).finally(() => {
this.forceUpdate();
});
};
onEnableAudioNotificationsChange = (checked) => {
SettingsStore.setValue(
"audioNotificationsEnabled", null,
SettingLevel.DEVICE,
checked,
).finally(() => {
this.forceUpdate();
});
};
/*
* Returns the email pusher (pusher of type 'email') for a given
* email address. Email pushers all have the same app ID, so since
* pushers are unique over (app ID, pushkey), there will be at most
* one such pusher.
*/
getEmailPusher(pushers, address) {
if (pushers === undefined) {
return undefined;
}
for (let i = 0; i < pushers.length; ++i) {
if (pushers[i].kind === 'email' && pushers[i].pushkey === address) {
return pushers[i];
}
}
return undefined;
}
onEnableEmailNotificationsChange = (address, checked) => {
let emailPusherPromise;
if (checked) {
const data = {};
data['brand'] = SdkConfig.get().brand;
emailPusherPromise = MatrixClientPeg.get().setPusher({
kind: 'email',
app_id: 'm.email',
pushkey: address,
app_display_name: 'Email Notifications',
device_display_name: address,
lang: navigator.language,
data: data,
append: true, // We always append for email pushers since we don't want to stop other accounts notifying to the same email address
});
} else {
const emailPusher = this.getEmailPusher(this.state.pushers, address);
emailPusher.kind = null;
emailPusherPromise = MatrixClientPeg.get().setPusher(emailPusher);
}
emailPusherPromise.then(() => {
this._refreshFromServer();
}, (error) => {
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
Modal.createTrackedDialog('Error saving email notification preferences', '', ErrorDialog, {
title: _t('Error saving email notification preferences'),
description: _t('An error occurred whilst saving your email notification preferences.'),
});
});
};
onNotifStateButtonClicked = (event) => {
// FIXME: use .bind() rather than className metadata here surely
const vectorRuleId = event.target.className.split("-")[0];
const newPushRuleVectorState = event.target.className.split("-")[1];
if ("_keywords" === vectorRuleId) {
this._setKeywordsPushRuleVectorState(newPushRuleVectorState);
} else {
const rule = this.getRule(vectorRuleId);
if (rule) {
this._setPushRuleVectorState(rule, newPushRuleVectorState);
}
}
};
onKeywordsClicked = (event) => {
// Compute the keywords list to display
let keywords = [];
for (const i in this.state.vectorContentRules.rules) {
const rule = this.state.vectorContentRules.rules[i];
keywords.push(rule.pattern);
}
if (keywords.length) {
// As keeping the order of per-word push rules hs side is a bit tricky to code,
// display the keywords in alphabetical order to the user
keywords.sort();
keywords = keywords.join(", ");
} else {
keywords = "";
}
const TextInputDialog = sdk.getComponent("dialogs.TextInputDialog");
Modal.createTrackedDialog('Keywords Dialog', '', TextInputDialog, {
title: _t('Keywords'),
description: _t('Enter keywords separated by a comma:'),
button: _t('OK'),
value: keywords,
onFinished: (shouldLeave, newValue) => {
if (shouldLeave && newValue !== keywords) {
let newKeywords = newValue.split(',');
for (const i in newKeywords) {
newKeywords[i] = newKeywords[i].trim();
}
// Remove duplicates and empty
newKeywords = newKeywords.reduce(function(array, keyword) {
if (keyword !== "" && array.indexOf(keyword) < 0) {
array.push(keyword);
}
return array;
}, []);
this._setKeywords(newKeywords);
}
},
});
};
getRule(vectorRuleId) {
for (const i in this.state.vectorPushRules) {
const rule = this.state.vectorPushRules[i];
if (rule.vectorRuleId === vectorRuleId) {
return rule;
}
}
}
_setPushRuleVectorState(rule, newPushRuleVectorState) {
if (rule && rule.vectorState !== newPushRuleVectorState) {
this.setState({
phase: Notifications.phases.LOADING,
});
const self = this;
const cli = MatrixClientPeg.get();
const deferreds = [];
const ruleDefinition = VectorPushRulesDefinitions[rule.vectorRuleId];
if (rule.rule) {
const actions = ruleDefinition.vectorStateToActions[newPushRuleVectorState];
if (!actions) {
// The new state corresponds to disabling the rule.
deferreds.push(cli.setPushRuleEnabled('global', rule.rule.kind, rule.rule.rule_id, false));
} else {
// The new state corresponds to enabling the rule and setting specific actions
deferreds.push(this._updatePushRuleActions(rule.rule, actions, true));
}
}
Promise.all(deferreds).then(function() {
self._refreshFromServer();
}, function(error) {
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
console.error("Failed to change settings: " + error);
Modal.createTrackedDialog('Failed to change settings', '', ErrorDialog, {
title: _t('Failed to change settings'),
description: ((error && error.message) ? error.message : _t('Operation failed')),
onFinished: self._refreshFromServer,
});
});
}
}
_setKeywordsPushRuleVectorState(newPushRuleVectorState) {
// Is there really a change?
if (this.state.vectorContentRules.vectorState === newPushRuleVectorState
|| this.state.vectorContentRules.rules.length === 0) {
return;
}
const self = this;
const cli = MatrixClientPeg.get();
this.setState({
phase: Notifications.phases.LOADING,
});
// Update all rules in self.state.vectorContentRules
const deferreds = [];
for (const i in this.state.vectorContentRules.rules) {
const rule = this.state.vectorContentRules.rules[i];
let enabled; let actions;
switch (newPushRuleVectorState) {
case PushRuleVectorState.ON:
if (rule.actions.length !== 1) {
actions = PushRuleVectorState.actionsFor(PushRuleVectorState.ON);
}
if (this.state.vectorContentRules.vectorState === PushRuleVectorState.OFF) {
enabled = true;
}
break;
case PushRuleVectorState.LOUD:
if (rule.actions.length !== 3) {
actions = PushRuleVectorState.actionsFor(PushRuleVectorState.LOUD);
}
if (this.state.vectorContentRules.vectorState === PushRuleVectorState.OFF) {
enabled = true;
}
break;
case PushRuleVectorState.OFF:
enabled = false;
break;
}
if (actions) {
// Note that the workaround in _updatePushRuleActions will automatically
// enable the rule
deferreds.push(this._updatePushRuleActions(rule, actions, enabled));
} else if (enabled != undefined) {
deferreds.push(cli.setPushRuleEnabled('global', rule.kind, rule.rule_id, enabled));
}
}
Promise.all(deferreds).then(function(resps) {
self._refreshFromServer();
}, function(error) {
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
console.error("Can't update user notification settings: " + error);
Modal.createTrackedDialog('Can\'t update user notifcation settings', '', ErrorDialog, {
title: _t('Can\'t update user notification settings'),
description: ((error && error.message) ? error.message : _t('Operation failed')),
onFinished: self._refreshFromServer,
});
});
}
_setKeywords(newKeywords) {
this.setState({
phase: Notifications.phases.LOADING,
});
const self = this;
const cli = MatrixClientPeg.get();
const removeDeferreds = [];
// Remove per-word push rules of keywords that are no more in the list
const vectorContentRulesPatterns = [];
for (const i in self.state.vectorContentRules.rules) {
const rule = self.state.vectorContentRules.rules[i];
vectorContentRulesPatterns.push(rule.pattern);
if (newKeywords.indexOf(rule.pattern) < 0) {
removeDeferreds.push(cli.deletePushRule('global', rule.kind, rule.rule_id));
}
}
// If the keyword is part of `externalContentRules`, remove the rule
// before recreating it in the right Vector path
for (const i in self.state.externalContentRules) {
const rule = self.state.externalContentRules[i];
if (newKeywords.indexOf(rule.pattern) >= 0) {
removeDeferreds.push(cli.deletePushRule('global', rule.kind, rule.rule_id));
}
}
const onError = function(error) {
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
console.error("Failed to update keywords: " + error);
Modal.createTrackedDialog('Failed to update keywords', '', ErrorDialog, {
title: _t('Failed to update keywords'),
description: ((error && error.message) ? error.message : _t('Operation failed')),
onFinished: self._refreshFromServer,
});
};
// Then, add the new ones
Promise.all(removeDeferreds).then(function(resps) {
const deferreds = [];
let pushRuleVectorStateKind = self.state.vectorContentRules.vectorState;
if (pushRuleVectorStateKind === PushRuleVectorState.OFF) {
// When the current global keywords rule is OFF, we need to look at
// the flavor of rules in 'vectorContentRules' to apply the same actions
// when creating the new rule.
// Thus, this new rule will join the 'vectorContentRules' set.
if (self.state.vectorContentRules.rules.length) {
pushRuleVectorStateKind = PushRuleVectorState.contentRuleVectorStateKind(
self.state.vectorContentRules.rules[0],
);
} else {
// ON is default
pushRuleVectorStateKind = PushRuleVectorState.ON;
}
}
for (const i in newKeywords) {
const keyword = newKeywords[i];
if (vectorContentRulesPatterns.indexOf(keyword) < 0) {
if (self.state.vectorContentRules.vectorState !== PushRuleVectorState.OFF) {
deferreds.push(cli.addPushRule('global', 'content', keyword, {
actions: PushRuleVectorState.actionsFor(pushRuleVectorStateKind),
pattern: keyword,
}));
} else {
deferreds.push(self._addDisabledPushRule('global', 'content', keyword, {
actions: PushRuleVectorState.actionsFor(pushRuleVectorStateKind),
pattern: keyword,
}));
}
}
}
Promise.all(deferreds).then(function(resps) {
self._refreshFromServer();
}, onError);
}, onError);
}
// Create a push rule but disabled
_addDisabledPushRule(scope, kind, ruleId, body) {
const cli = MatrixClientPeg.get();
return cli.addPushRule(scope, kind, ruleId, body).then(() =>
cli.setPushRuleEnabled(scope, kind, ruleId, false),
);
}
// Check if any legacy im.vector rules need to be ported to the new API
// for overriding the actions of default rules.
_portRulesToNewAPI(rulesets) {
const needsUpdate = [];
const cli = MatrixClientPeg.get();
for (const kind in rulesets.global) {
const ruleset = rulesets.global[kind];
for (let i = 0; i < ruleset.length; ++i) {
const rule = ruleset[i];
if (rule.rule_id in LEGACY_RULES) {
console.log("Porting legacy rule", rule);
needsUpdate.push( function(kind, rule) {
return cli.setPushRuleActions(
'global', kind, LEGACY_RULES[rule.rule_id], portLegacyActions(rule.actions),
).then(() =>
cli.deletePushRule('global', kind, rule.rule_id),
).catch( (e) => {
console.warn(`Error when porting legacy rule: ${e}`);
});
}(kind, rule));
}
}
}
if (needsUpdate.length > 0) {
// If some of the rules need to be ported then wait for the porting
// to happen and then fetch the rules again.
return Promise.all(needsUpdate).then(() =>
cli.getPushRules(),
);
} else {
// Otherwise return the rules that we already have.
return rulesets;
}
}
_refreshFromServer = () => {
const self = this;
const pushRulesPromise = MatrixClientPeg.get().getPushRules().then(
self._portRulesToNewAPI,
).then(function(rulesets) {
/// XXX seriously? wtf is this?
MatrixClientPeg.get().pushRules = rulesets;
// Get homeserver default rules and triage them by categories
const ruleCategories = {
// The master rule (all notifications disabling)
'.m.rule.master': 'master',
// The default push rules displayed by Vector UI
'.m.rule.contains_display_name': 'vector',
'.m.rule.contains_user_name': 'vector',
'.m.rule.roomnotif': 'vector',
'.m.rule.room_one_to_one': 'vector',
'.m.rule.encrypted_room_one_to_one': 'vector',
'.m.rule.message': 'vector',
'.m.rule.encrypted': 'vector',
'.m.rule.invite_for_me': 'vector',
//'.m.rule.member_event': 'vector',
'.m.rule.call': 'vector',
'.m.rule.suppress_notices': 'vector',
'.m.rule.tombstone': 'vector',
// Others go to others
};
// HS default rules
const defaultRules = { master: [], vector: {}, others: [] };
for (const kind in rulesets.global) {
for (let i = 0; i < Object.keys(rulesets.global[kind]).length; ++i) {
const r = rulesets.global[kind][i];
const cat = ruleCategories[r.rule_id];
r.kind = kind;
if (r.rule_id[0] === '.') {
if (cat === 'vector') {
defaultRules.vector[r.rule_id] = r;
} else if (cat === 'master') {
defaultRules.master.push(r);
} else {
defaultRules['others'].push(r);
}
}
}
}
// Get the master rule if any defined by the hs
if (defaultRules.master.length > 0) {
self.state.masterPushRule = defaultRules.master[0];
}
// parse the keyword rules into our state
const contentRules = ContentRules.parseContentRules(rulesets);
self.state.vectorContentRules = {
vectorState: contentRules.vectorState,
rules: contentRules.rules,
};
self.state.externalContentRules = contentRules.externalRules;
// Build the rules displayed in the Vector UI matrix table
self.state.vectorPushRules = [];
self.state.externalPushRules = [];
const vectorRuleIds = [
'.m.rule.contains_display_name',
'.m.rule.contains_user_name',
'.m.rule.roomnotif',
'_keywords',
'.m.rule.room_one_to_one',
'.m.rule.encrypted_room_one_to_one',
'.m.rule.message',
'.m.rule.encrypted',
'.m.rule.invite_for_me',
//'im.vector.rule.member_event',
'.m.rule.call',
'.m.rule.suppress_notices',
'.m.rule.tombstone',
];
for (const i in vectorRuleIds) {
const vectorRuleId = vectorRuleIds[i];
if (vectorRuleId === '_keywords') {
// keywords needs a special handling
// For Vector UI, this is a single global push rule but translated in Matrix,
// it corresponds to all content push rules (stored in self.state.vectorContentRule)
self.state.vectorPushRules.push({
"vectorRuleId": "_keywords",
"description": (
<span>
{ _t('Messages containing <span>keywords</span>',
{},
{ 'span': (sub) =>
<span className="mx_UserNotifSettings_keywords" onClick={ self.onKeywordsClicked }>{sub}</span>,
},
)}
</span>
),
"vectorState": self.state.vectorContentRules.vectorState,
});
} else {
const ruleDefinition = VectorPushRulesDefinitions[vectorRuleId];
const rule = defaultRules.vector[vectorRuleId];
const vectorState = ruleDefinition.ruleToVectorState(rule);
//console.log("Refreshing vectorPushRules for " + vectorRuleId +", "+ ruleDefinition.description +", " + rule +", " + vectorState);
self.state.vectorPushRules.push({
"vectorRuleId": vectorRuleId,
"description": _t(ruleDefinition.description), // Text from VectorPushRulesDefinitions.js
"rule": rule,
"vectorState": vectorState,
});
// if there was a rule which we couldn't parse, add it to the external list
if (rule && !vectorState) {
rule.description = ruleDefinition.description;
self.state.externalPushRules.push(rule);
}
}
}
// Build the rules not managed by Vector UI
const otherRulesDescriptions = {
'.m.rule.message': _t('Notify for all other messages/rooms'),
'.m.rule.fallback': _t('Notify me for anything else'),
};
for (const i in defaultRules.others) {
const rule = defaultRules.others[i];
const ruleDescription = otherRulesDescriptions[rule.rule_id];
// Show enabled default rules that was modified by the user
if (ruleDescription && rule.enabled && !rule.default) {
rule.description = ruleDescription;
self.state.externalPushRules.push(rule);
}
}
});
const pushersPromise = MatrixClientPeg.get().getPushers().then(function(resp) {
self.setState({ pushers: resp.pushers });
});
Promise.all([pushRulesPromise, pushersPromise]).then(function() {
self.setState({
phase: Notifications.phases.DISPLAY,
});
}, function(error) {
console.error(error);
self.setState({
phase: Notifications.phases.ERROR,
});
}).finally(() => {
// actually explicitly update our state having been deep-manipulating it
self.setState({
masterPushRule: self.state.masterPushRule,
vectorContentRules: self.state.vectorContentRules,
vectorPushRules: self.state.vectorPushRules,
externalContentRules: self.state.externalContentRules,
externalPushRules: self.state.externalPushRules,
});
});
MatrixClientPeg.get().getThreePids().then((r) => this.setState({ threepids: r.threepids }));
};
_onClearNotifications = () => {
const cli = MatrixClientPeg.get();
cli.getRooms().forEach(r => {
if (r.getUnreadNotificationCount() > 0) {
const events = r.getLiveTimeline().getEvents();
if (events.length) cli.sendReadReceipt(events.pop());
}
});
};
_updatePushRuleActions(rule, actions, enabled) {
const cli = MatrixClientPeg.get();
return cli.setPushRuleActions(
'global', rule.kind, rule.rule_id, actions,
).then( function() {
// Then, if requested, enabled or disabled the rule
if (undefined != enabled) {
return cli.setPushRuleEnabled(
'global', rule.kind, rule.rule_id, enabled,
);
}
});
}
renderNotifRulesTableRow(title, className, pushRuleVectorState) {
return (
<tr key={ className }>
<th>
{ title }
</th>
<th>
<input className= {className + "-" + PushRuleVectorState.OFF}
type="radio"
checked={ pushRuleVectorState === PushRuleVectorState.OFF }
onChange={ this.onNotifStateButtonClicked } />
</th>
<th>
<input className= {className + "-" + PushRuleVectorState.ON}
type="radio"
checked={ pushRuleVectorState === PushRuleVectorState.ON }
onChange={ this.onNotifStateButtonClicked } />
</th>
<th>
<input className= {className + "-" + PushRuleVectorState.LOUD}
type="radio"
checked={ pushRuleVectorState === PushRuleVectorState.LOUD }
onChange={ this.onNotifStateButtonClicked } />
</th>
</tr>
);
}
renderNotifRulesTableRows() {
const rows = [];
for (const i in this.state.vectorPushRules) {
const rule = this.state.vectorPushRules[i];
if (rule.rule === undefined && rule.vectorRuleId.startsWith(".m.")) {
console.warn(`Skipping render of rule ${rule.vectorRuleId} due to no underlying rule`);
continue;
}
//console.log("rendering: " + rule.description + ", " + rule.vectorRuleId + ", " + rule.vectorState);
rows.push(this.renderNotifRulesTableRow(rule.description, rule.vectorRuleId, rule.vectorState));
}
return rows;
}
hasEmailPusher(pushers, address) {
if (pushers === undefined) {
return false;
}
for (let i = 0; i < pushers.length; ++i) {
if (pushers[i].kind === 'email' && pushers[i].pushkey === address) {
return true;
}
}
return false;
}
emailNotificationsRow(address, label) {
return <LabelledToggleSwitch value={this.hasEmailPusher(this.state.pushers, address)}
onChange={this.onEnableEmailNotificationsChange.bind(this, address)}
label={label} key={`emailNotif_${label}`} />;
}
render() {
let spinner;
if (this.state.phase === Notifications.phases.LOADING) {
const Loader = sdk.getComponent("elements.Spinner");
spinner = <Loader />;
}
let masterPushRuleDiv;
if (this.state.masterPushRule) {
masterPushRuleDiv = <LabelledToggleSwitch value={!this.state.masterPushRule.enabled}
onChange={this.onEnableNotificationsChange}
label={_t('Enable notifications for this account')} />;
}
let clearNotificationsButton;
if (MatrixClientPeg.get().getRooms().some(r => r.getUnreadNotificationCount() > 0)) {
clearNotificationsButton = <AccessibleButton onClick={this._onClearNotifications} kind='danger'>
{_t("Clear notifications")}
</AccessibleButton>;
}
// When enabled, the master rule inhibits all existing rules
// So do not show all notification settings
if (this.state.masterPushRule && this.state.masterPushRule.enabled) {
return (
<div>
{masterPushRuleDiv}
<div className="mx_UserNotifSettings_notifTable">
{ _t('All notifications are currently disabled for all targets.') }
</div>
{clearNotificationsButton}
</div>
);
}
const emailThreepids = this.state.threepids.filter((tp) => tp.medium === "email");
let emailNotificationsRows;
if (emailThreepids.length > 0) {
emailNotificationsRows = emailThreepids.map((threePid) => this.emailNotificationsRow(
threePid.address, `${_t('Enable email notifications')} (${threePid.address})`,
));
} else if (SettingsStore.getValue(UIFeature.ThirdPartyID)) {
emailNotificationsRows = <div>
{ _t('Add an email address to configure email notifications') }
</div>;
}
// Build external push rules
const externalRules = [];
for (const i in this.state.externalPushRules) {
const rule = this.state.externalPushRules[i];
externalRules.push(<li>{ _t(rule.description) }</li>);
}
// Show keywords not displayed by the vector UI as a single external push rule
let externalKeywords = [];
for (const i in this.state.externalContentRules) {
const rule = this.state.externalContentRules[i];
externalKeywords.push(rule.pattern);
}
if (externalKeywords.length) {
externalKeywords = externalKeywords.join(", ");
externalRules.push(<li>
{_t('Notifications on the following keywords follow rules which cant be displayed here:') }
{ externalKeywords }
</li>);
}
let devicesSection;
if (this.state.pushers === undefined) {
devicesSection = <div className="error">{ _t('Unable to fetch notification target list') }</div>;
} else if (this.state.pushers.length === 0) {
devicesSection = null;
} else {
// TODO: It would be great to be able to delete pushers from here too,
// and this wouldn't be hard to add.
const rows = [];
for (let i = 0; i < this.state.pushers.length; ++i) {
rows.push(<tr key={ i }>
<td>{this.state.pushers[i].app_display_name}</td>
<td>{this.state.pushers[i].device_display_name}</td>
</tr>);
}
devicesSection = (<table className="mx_UserNotifSettings_devicesTable">
<tbody>
{rows}
</tbody>
</table>);
}
if (devicesSection) {
devicesSection = (<div>
<h3>{ _t('Notification targets') }</h3>
{ devicesSection }
</div>);
}
let advancedSettings;
if (externalRules.length) {
const brand = SdkConfig.get().brand;
advancedSettings = (
<div>
<h3>{ _t('Advanced notification settings') }</h3>
{ _t('There are advanced notifications which are not shown here.') }<br />
{_t(
'You might have configured them in a client other than %(brand)s. ' +
'You cannot tune them in %(brand)s but they still apply.',
{ brand },
)}
<ul>
{ externalRules }
</ul>
</div>
);
}
return (
<div>
{masterPushRuleDiv}
<div className="mx_UserNotifSettings_notifTable">
{ spinner }
<LabelledToggleSwitch value={SettingsStore.getValue("notificationsEnabled")}
onChange={this.onEnableDesktopNotificationsChange}
label={_t('Enable desktop notifications for this session')} />
<LabelledToggleSwitch value={SettingsStore.getValue("notificationBodyEnabled")}
onChange={this.onEnableDesktopNotificationBodyChange}
label={_t('Show message in desktop notification')} />
<LabelledToggleSwitch value={SettingsStore.getValue("audioNotificationsEnabled")}
onChange={this.onEnableAudioNotificationsChange}
label={_t('Enable audible notifications for this session')} />
{ emailNotificationsRows }
<div className="mx_UserNotifSettings_pushRulesTableWrapper">
<table className="mx_UserNotifSettings_pushRulesTable">
<thead>
<tr>
<th width="55%"></th>
<th width="15%">{ _t('Off') }</th>
<th width="15%">{ _t('On') }</th>
<th width="15%">{ _t('Noisy') }</th>
</tr>
</thead>
<tbody>
{ this.renderNotifRulesTableRows() }
</tbody>
</table>
</div>
{ advancedSettings }
{ devicesSection }
{ clearNotificationsButton }
</div>
</div>
);
}
}

View file

@ -0,0 +1,647 @@
/*
Copyright 2016 - 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 from "react";
import Spinner from "../elements/Spinner";
import { MatrixClientPeg } from "../../../MatrixClientPeg";
import { IAnnotatedPushRule, IPusher, PushRuleAction, PushRuleKind, RuleId } from "matrix-js-sdk/src/@types/PushRules";
import {
ContentRules,
IContentRules,
PushRuleVectorState,
VectorPushRulesDefinitions,
VectorState,
} from "../../../notifications";
import { _t, TranslatedString } from "../../../languageHandler";
import { IThreepid, ThreepidMedium } from "matrix-js-sdk/src/@types/threepids";
import LabelledToggleSwitch from "../elements/LabelledToggleSwitch";
import SettingsStore from "../../../settings/SettingsStore";
import StyledRadioButton from "../elements/StyledRadioButton";
import { SettingLevel } from "../../../settings/SettingLevel";
import Modal from "../../../Modal";
import ErrorDialog from "../dialogs/ErrorDialog";
import SdkConfig from "../../../SdkConfig";
import AccessibleButton from "../elements/AccessibleButton";
import TagComposer from "../elements/TagComposer";
import { objectClone } from "../../../utils/objects";
import { arrayDiff } from "../../../utils/arrays";
// TODO: this "view" component still has far too much application logic in it,
// which should be factored out to other files.
enum Phase {
Loading = "loading",
Ready = "ready",
Persisting = "persisting", // technically a meta-state for Ready, but whatever
Error = "error",
}
enum RuleClass {
Master = "master",
// The vector sections map approximately to UI sections
VectorGlobal = "vector_global",
VectorMentions = "vector_mentions",
VectorOther = "vector_other",
Other = "other", // unknown rules, essentially
}
const KEYWORD_RULE_ID = "_keywords"; // used as a placeholder "Rule ID" throughout this component
const KEYWORD_RULE_CATEGORY = RuleClass.VectorMentions;
// This array doesn't care about categories: it's just used for a simple sort
const RULE_DISPLAY_ORDER: string[] = [
// Global
RuleId.DM,
RuleId.EncryptedDM,
RuleId.Message,
RuleId.EncryptedMessage,
// Mentions
RuleId.ContainsDisplayName,
RuleId.ContainsUserName,
RuleId.AtRoomNotification,
// Other
RuleId.InviteToSelf,
RuleId.IncomingCall,
RuleId.SuppressNotices,
RuleId.Tombstone,
];
interface IVectorPushRule {
ruleId: RuleId | typeof KEYWORD_RULE_ID | string;
rule?: IAnnotatedPushRule;
description: TranslatedString | string;
vectorState: VectorState;
}
interface IProps {}
interface IState {
phase: Phase;
// Optional stuff is required when `phase === Ready`
masterPushRule?: IAnnotatedPushRule;
vectorKeywordRuleInfo?: IContentRules;
vectorPushRules?: {
[category in RuleClass]?: IVectorPushRule[];
};
pushers?: IPusher[];
threepids?: IThreepid[];
}
export default class Notifications extends React.PureComponent<IProps, IState> {
public constructor(props: IProps) {
super(props);
this.state = {
phase: Phase.Loading,
};
}
private get isInhibited(): boolean {
// Caution: The master rule's enabled state is inverted from expectation. When
// the master rule is *enabled* it means all other rules are *disabled* (or
// inhibited). Conversely, when the master rule is *disabled* then all other rules
// are *enabled* (or operate fine).
return this.state.masterPushRule?.enabled;
}
public componentDidMount() {
// noinspection JSIgnoredPromiseFromCall
this.refreshFromServer();
}
private async refreshFromServer() {
try {
const newState = (await Promise.all([
this.refreshRules(),
this.refreshPushers(),
this.refreshThreepids(),
])).reduce((p, c) => Object.assign(c, p), {});
this.setState({
...newState,
phase: Phase.Ready,
});
} catch (e) {
console.error("Error setting up notifications for settings: ", e);
this.setState({ phase: Phase.Error });
}
}
private async refreshRules(): Promise<Partial<IState>> {
const ruleSets = await MatrixClientPeg.get().getPushRules();
const categories = {
[RuleId.Master]: RuleClass.Master,
[RuleId.DM]: RuleClass.VectorGlobal,
[RuleId.EncryptedDM]: RuleClass.VectorGlobal,
[RuleId.Message]: RuleClass.VectorGlobal,
[RuleId.EncryptedMessage]: RuleClass.VectorGlobal,
[RuleId.ContainsDisplayName]: RuleClass.VectorMentions,
[RuleId.ContainsUserName]: RuleClass.VectorMentions,
[RuleId.AtRoomNotification]: RuleClass.VectorMentions,
[RuleId.InviteToSelf]: RuleClass.VectorOther,
[RuleId.IncomingCall]: RuleClass.VectorOther,
[RuleId.SuppressNotices]: RuleClass.VectorOther,
[RuleId.Tombstone]: RuleClass.VectorOther,
// Everything maps to a generic "other" (unknown rule)
};
const defaultRules: {
[k in RuleClass]: IAnnotatedPushRule[];
} = {
[RuleClass.Master]: [],
[RuleClass.VectorGlobal]: [],
[RuleClass.VectorMentions]: [],
[RuleClass.VectorOther]: [],
[RuleClass.Other]: [],
};
for (const k in ruleSets.global) {
// noinspection JSUnfilteredForInLoop
const kind = k as PushRuleKind;
for (const r of ruleSets.global[kind]) {
const rule: IAnnotatedPushRule = Object.assign(r, { kind });
const category = categories[rule.rule_id] ?? RuleClass.Other;
if (rule.rule_id[0] === '.') {
defaultRules[category].push(rule);
}
}
}
const preparedNewState: Partial<IState> = {};
if (defaultRules.master.length > 0) {
preparedNewState.masterPushRule = defaultRules.master[0];
} else {
// XXX: Can this even happen? How do we safely recover?
throw new Error("Failed to locate a master push rule");
}
// Parse keyword rules
preparedNewState.vectorKeywordRuleInfo = ContentRules.parseContentRules(ruleSets);
// Prepare rendering for all of our known rules
preparedNewState.vectorPushRules = {};
const vectorCategories = [RuleClass.VectorGlobal, RuleClass.VectorMentions, RuleClass.VectorOther];
for (const category of vectorCategories) {
preparedNewState.vectorPushRules[category] = [];
for (const rule of defaultRules[category]) {
const definition = VectorPushRulesDefinitions[rule.rule_id];
const vectorState = definition.ruleToVectorState(rule);
preparedNewState.vectorPushRules[category].push({
ruleId: rule.rule_id,
rule, vectorState,
description: _t(definition.description),
});
}
// Quickly sort the rules for display purposes
preparedNewState.vectorPushRules[category].sort((a, b) => {
let idxA = RULE_DISPLAY_ORDER.indexOf(a.ruleId);
let idxB = RULE_DISPLAY_ORDER.indexOf(b.ruleId);
// Assume unknown things go at the end
if (idxA < 0) idxA = RULE_DISPLAY_ORDER.length;
if (idxB < 0) idxB = RULE_DISPLAY_ORDER.length;
return idxA - idxB;
});
if (category === KEYWORD_RULE_CATEGORY) {
preparedNewState.vectorPushRules[category].push({
ruleId: KEYWORD_RULE_ID,
description: _t("Messages containing keywords"),
vectorState: preparedNewState.vectorKeywordRuleInfo.vectorState,
});
}
}
return preparedNewState;
}
private refreshPushers(): Promise<Partial<IState>> {
return MatrixClientPeg.get().getPushers();
}
private refreshThreepids(): Promise<Partial<IState>> {
return MatrixClientPeg.get().getThreePids();
}
private showSaveError() {
Modal.createTrackedDialog('Error saving notification preferences', '', ErrorDialog, {
title: _t('Error saving notification preferences'),
description: _t('An error occurred whilst saving your notification preferences.'),
});
}
private onMasterRuleChanged = async (checked: boolean) => {
this.setState({ phase: Phase.Persisting });
try {
const masterRule = this.state.masterPushRule;
await MatrixClientPeg.get().setPushRuleEnabled('global', masterRule.kind, masterRule.rule_id, !checked);
await this.refreshFromServer();
} catch (e) {
this.setState({ phase: Phase.Error });
console.error("Error updating master push rule:", e);
this.showSaveError();
}
};
private onEmailNotificationsChanged = async (email: string, checked: boolean) => {
this.setState({ phase: Phase.Persisting });
try {
if (checked) {
await MatrixClientPeg.get().setPusher({
kind: "email",
app_id: "m.email",
pushkey: email,
app_display_name: "Email Notifications",
device_display_name: email,
lang: navigator.language,
data: {
brand: SdkConfig.get().brand,
},
// We always append for email pushers since we don't want to stop other
// accounts notifying to the same email address
append: true,
});
} else {
const pusher = this.state.pushers.find(p => p.kind === "email" && p.pushkey === email);
pusher.kind = null; // flag for delete
await MatrixClientPeg.get().setPusher(pusher);
}
await this.refreshFromServer();
} catch (e) {
this.setState({ phase: Phase.Error });
console.error("Error updating email pusher:", e);
this.showSaveError();
}
};
private onDesktopNotificationsChanged = async (checked: boolean) => {
await SettingsStore.setValue("notificationsEnabled", null, SettingLevel.DEVICE, checked);
this.forceUpdate(); // the toggle is controlled by SettingsStore#getValue()
};
private onDesktopShowBodyChanged = async (checked: boolean) => {
await SettingsStore.setValue("notificationBodyEnabled", null, SettingLevel.DEVICE, checked);
this.forceUpdate(); // the toggle is controlled by SettingsStore#getValue()
};
private onAudioNotificationsChanged = async (checked: boolean) => {
await SettingsStore.setValue("audioNotificationsEnabled", null, SettingLevel.DEVICE, checked);
this.forceUpdate(); // the toggle is controlled by SettingsStore#getValue()
};
private onRadioChecked = async (rule: IVectorPushRule, checkedState: VectorState) => {
this.setState({ phase: Phase.Persisting });
try {
const cli = MatrixClientPeg.get();
if (rule.ruleId === KEYWORD_RULE_ID) {
// Update all the keywords
for (const rule of this.state.vectorKeywordRuleInfo.rules) {
let enabled: boolean;
let actions: PushRuleAction[];
if (checkedState === VectorState.On) {
if (rule.actions.length !== 1) { // XXX: Magic number
actions = PushRuleVectorState.actionsFor(checkedState);
}
if (this.state.vectorKeywordRuleInfo.vectorState === VectorState.Off) {
enabled = true;
}
} else if (checkedState === VectorState.Loud) {
if (rule.actions.length !== 3) { // XXX: Magic number
actions = PushRuleVectorState.actionsFor(checkedState);
}
if (this.state.vectorKeywordRuleInfo.vectorState === VectorState.Off) {
enabled = true;
}
} else {
enabled = false;
}
if (actions) {
await cli.setPushRuleActions('global', rule.kind, rule.rule_id, actions);
}
if (enabled !== undefined) {
await cli.setPushRuleEnabled('global', rule.kind, rule.rule_id, enabled);
}
}
} else {
const definition = VectorPushRulesDefinitions[rule.ruleId];
const actions = definition.vectorStateToActions[checkedState];
if (!actions) {
await cli.setPushRuleEnabled('global', rule.rule.kind, rule.rule.rule_id, false);
} else {
await cli.setPushRuleActions('global', rule.rule.kind, rule.rule.rule_id, actions);
await cli.setPushRuleEnabled('global', rule.rule.kind, rule.rule.rule_id, true);
}
}
await this.refreshFromServer();
} catch (e) {
this.setState({ phase: Phase.Error });
console.error("Error updating push rule:", e);
this.showSaveError();
}
};
private onClearNotificationsClicked = () => {
MatrixClientPeg.get().getRooms().forEach(r => {
if (r.getUnreadNotificationCount() > 0) {
const events = r.getLiveTimeline().getEvents();
if (events.length) {
// noinspection JSIgnoredPromiseFromCall
MatrixClientPeg.get().sendReadReceipt(events[events.length - 1]);
}
}
});
};
private async setKeywords(keywords: string[], originalRules: IAnnotatedPushRule[]) {
try {
// De-duplicate and remove empties
keywords = Array.from(new Set(keywords)).filter(k => !!k);
const oldKeywords = Array.from(new Set(originalRules.map(r => r.pattern))).filter(k => !!k);
// Note: Technically because of the UI interaction (at the time of writing), the diff
// will only ever be +/-1 so we don't really have to worry about efficiently handling
// tons of keyword changes.
const diff = arrayDiff(oldKeywords, keywords);
for (const word of diff.removed) {
for (const rule of originalRules.filter(r => r.pattern === word)) {
await MatrixClientPeg.get().deletePushRule('global', rule.kind, rule.rule_id);
}
}
let ruleVectorState = this.state.vectorKeywordRuleInfo.vectorState;
if (ruleVectorState === VectorState.Off) {
// When the current global keywords rule is OFF, we need to look at
// the flavor of existing rules to apply the same actions
// when creating the new rule.
if (originalRules.length) {
ruleVectorState = PushRuleVectorState.contentRuleVectorStateKind(originalRules[0]);
} else {
ruleVectorState = VectorState.On; // default
}
}
const kind = PushRuleKind.ContentSpecific;
for (const word of diff.added) {
await MatrixClientPeg.get().addPushRule('global', kind, word, {
actions: PushRuleVectorState.actionsFor(ruleVectorState),
pattern: word,
});
if (ruleVectorState === VectorState.Off) {
await MatrixClientPeg.get().setPushRuleEnabled('global', kind, word, false);
}
}
await this.refreshFromServer();
} catch (e) {
this.setState({ phase: Phase.Error });
console.error("Error updating keyword push rules:", e);
this.showSaveError();
}
}
private onKeywordAdd = (keyword: string) => {
const originalRules = objectClone(this.state.vectorKeywordRuleInfo.rules);
// We add the keyword immediately as a sort of local echo effect
this.setState({
phase: Phase.Persisting,
vectorKeywordRuleInfo: {
...this.state.vectorKeywordRuleInfo,
rules: [
...this.state.vectorKeywordRuleInfo.rules,
// XXX: Horrible assumption that we don't need the remaining fields
{ pattern: keyword } as IAnnotatedPushRule,
],
},
}, async () => {
await this.setKeywords(this.state.vectorKeywordRuleInfo.rules.map(r => r.pattern), originalRules);
});
};
private onKeywordRemove = (keyword: string) => {
const originalRules = objectClone(this.state.vectorKeywordRuleInfo.rules);
// We remove the keyword immediately as a sort of local echo effect
this.setState({
phase: Phase.Persisting,
vectorKeywordRuleInfo: {
...this.state.vectorKeywordRuleInfo,
rules: this.state.vectorKeywordRuleInfo.rules.filter(r => r.pattern !== keyword),
},
}, async () => {
await this.setKeywords(this.state.vectorKeywordRuleInfo.rules.map(r => r.pattern), originalRules);
});
};
private renderTopSection() {
const masterSwitch = <LabelledToggleSwitch
value={!this.isInhibited}
label={_t("Enable for this account")}
onChange={this.onMasterRuleChanged}
disabled={this.state.phase === Phase.Persisting}
/>;
// If all the rules are inhibited, don't show anything.
if (this.isInhibited) {
return masterSwitch;
}
const emailSwitches = this.state.threepids.filter(t => t.medium === ThreepidMedium.Email)
.map(e => <LabelledToggleSwitch
key={e.address}
value={this.state.pushers.some(p => p.kind === "email" && p.pushkey === e.address)}
label={_t("Enable email notifications for %(email)s", { email: e.address })}
onChange={this.onEmailNotificationsChanged.bind(this, e.address)}
disabled={this.state.phase === Phase.Persisting}
/>);
return <>
{ masterSwitch }
<LabelledToggleSwitch
value={SettingsStore.getValue("notificationsEnabled")}
onChange={this.onDesktopNotificationsChanged}
label={_t('Enable desktop notifications for this session')}
disabled={this.state.phase === Phase.Persisting}
/>
<LabelledToggleSwitch
value={SettingsStore.getValue("notificationBodyEnabled")}
onChange={this.onDesktopShowBodyChanged}
label={_t('Show message in desktop notification')}
disabled={this.state.phase === Phase.Persisting}
/>
<LabelledToggleSwitch
value={SettingsStore.getValue("audioNotificationsEnabled")}
onChange={this.onAudioNotificationsChanged}
label={_t('Enable audible notifications for this session')}
disabled={this.state.phase === Phase.Persisting}
/>
{ emailSwitches }
</>;
}
private renderCategory(category: RuleClass) {
if (category !== RuleClass.VectorOther && this.isInhibited) {
return null; // nothing to show for the section
}
let clearNotifsButton: JSX.Element;
if (
category === RuleClass.VectorOther
&& MatrixClientPeg.get().getRooms().some(r => r.getUnreadNotificationCount() > 0)
) {
clearNotifsButton = <AccessibleButton
onClick={this.onClearNotificationsClicked}
kind='danger'
className='mx_UserNotifSettings_clearNotifsButton'
>{ _t("Clear notifications") }</AccessibleButton>;
}
if (category === RuleClass.VectorOther && this.isInhibited) {
// only render the utility buttons (if needed)
if (clearNotifsButton) {
return <div className='mx_UserNotifSettings_floatingSection'>
<div>{ _t("Other") }</div>
{ clearNotifsButton }
</div>;
}
return null;
}
let keywordComposer: JSX.Element;
if (category === RuleClass.VectorMentions) {
keywordComposer = <TagComposer
tags={this.state.vectorKeywordRuleInfo?.rules.map(r => r.pattern)}
onAdd={this.onKeywordAdd}
onRemove={this.onKeywordRemove}
disabled={this.state.phase === Phase.Persisting}
label={_t("Keyword")}
placeholder={_t("New keyword")}
/>;
}
const makeRadio = (r: IVectorPushRule, s: VectorState) => (
<StyledRadioButton
key={r.ruleId}
name={r.ruleId}
checked={r.vectorState === s}
onChange={this.onRadioChecked.bind(this, r, s)}
disabled={this.state.phase === Phase.Persisting}
/>
);
const rows = this.state.vectorPushRules[category].map(r => <tr key={category + r.ruleId}>
<td>{ r.description }</td>
<td>{ makeRadio(r, VectorState.Off) }</td>
<td>{ makeRadio(r, VectorState.On) }</td>
<td>{ makeRadio(r, VectorState.Loud) }</td>
</tr>);
let sectionName: TranslatedString;
switch (category) {
case RuleClass.VectorGlobal:
sectionName = _t("Global");
break;
case RuleClass.VectorMentions:
sectionName = _t("Mentions & keywords");
break;
case RuleClass.VectorOther:
sectionName = _t("Other");
break;
default:
throw new Error("Developer error: Unnamed notifications section: " + category);
}
return <>
<table className='mx_UserNotifSettings_pushRulesTable'>
<thead>
<tr>
<th>{ sectionName }</th>
<th>{ _t("Off") }</th>
<th>{ _t("On") }</th>
<th>{ _t("Noisy") }</th>
</tr>
</thead>
<tbody>
{ rows }
</tbody>
</table>
{ clearNotifsButton }
{ keywordComposer }
</>;
}
private renderTargets() {
if (this.isInhibited) return null; // no targets if there's no notifications
const rows = this.state.pushers.map(p => <tr key={p.kind+p.pushkey}>
<td>{ p.app_display_name }</td>
<td>{ p.device_display_name }</td>
</tr>);
if (!rows.length) return null; // no targets to show
return <div className='mx_UserNotifSettings_floatingSection'>
<div>{ _t("Notification targets") }</div>
<table>
<tbody>
{ rows }
</tbody>
</table>
</div>;
}
public render() {
if (this.state.phase === Phase.Loading) {
// Ends up default centered
return <Spinner />;
} else if (this.state.phase === Phase.Error) {
return <p>{ _t("There was an error loading your notification settings.") }</p>;
}
return <div className='mx_UserNotifSettings'>
{ this.renderTopSection() }
{ this.renderCategory(RuleClass.VectorGlobal) }
{ this.renderCategory(RuleClass.VectorMentions) }
{ this.renderCategory(RuleClass.VectorOther) }
{ this.renderTargets() }
</div>;
}
}

View file

@ -149,12 +149,12 @@ export default class ProfileSettings extends React.Component {
let hostingSignup = null;
if (hostingSignupLink) {
hostingSignup = <span className="mx_ProfileSettings_hostingSignup">
{_t(
{ _t(
"<a>Upgrade</a> to your own domain", {},
{
a: sub => <a href={hostingSignupLink} target="_blank" rel="noreferrer noopener">{sub}</a>,
a: sub => <a href={hostingSignupLink} target="_blank" rel="noreferrer noopener">{ sub }</a>,
},
)}
) }
<a href={hostingSignupLink} target="_blank" rel="noreferrer noopener">
<img src={require("../../../../res/img/external-link.svg")} width="11" height="10" alt='' />
</a>
@ -172,22 +172,24 @@ export default class ProfileSettings extends React.Component {
>
<input
type="file"
ref={this._avatarUpload} className="mx_ProfileSettings_avatarUpload"
ref={this._avatarUpload}
className="mx_ProfileSettings_avatarUpload"
onChange={this._onAvatarChanged}
accept="image/*"
/>
<div className="mx_ProfileSettings_profile">
<div className="mx_ProfileSettings_controls">
<span className="mx_SettingsTab_subheading">{_t("Profile")}</span>
<span className="mx_SettingsTab_subheading">{ _t("Profile") }</span>
<Field
label={_t("Display Name")}
type="text" value={this.state.displayName}
type="text"
value={this.state.displayName}
autoComplete="off"
onChange={this._onDisplayNameChanged}
/>
<p>
{this.state.userId}
{hostingSignup}
{ this.state.userId }
{ hostingSignup }
</p>
</div>
<AvatarSetting
@ -203,14 +205,14 @@ export default class ProfileSettings extends React.Component {
kind="link"
disabled={!this.state.enableProfileSave}
>
{_t("Cancel")}
{ _t("Cancel") }
</AccessibleButton>
<AccessibleButton
onClick={this._saveProfile}
kind="primary"
disabled={!this.state.enableProfileSave}
>
{_t("Save")}
{ _t("Save") }
</AccessibleButton>
</div>
</form>

View file

@ -221,7 +221,7 @@ export default class SecureBackupPanel extends React.PureComponent {
if (error) {
statusDescription = (
<div className="error">
{_t("Unable to load key backup status")}
{ _t("Unable to load key backup status") }
</div>
);
} else if (loading) {
@ -230,19 +230,19 @@ export default class SecureBackupPanel extends React.PureComponent {
let restoreButtonCaption = _t("Restore from Backup");
if (MatrixClientPeg.get().getKeyBackupEnabled()) {
statusDescription = <p> {_t("This session is backing up your keys. ")}</p>;
statusDescription = <p> { _t("This session is backing up your keys. ") }</p>;
} else {
statusDescription = <>
<p>{_t(
<p>{ _t(
"This session is <b>not backing up your keys</b>, " +
"but you do have an existing backup you can restore from " +
"and add to going forward.", {},
{ b: sub => <b>{sub}</b> },
)}</p>
<p>{_t(
{ b: sub => <b>{ sub }</b> },
) }</p>
<p>{ _t(
"Connect this session to key backup before signing out to avoid " +
"losing any keys that may only be on this session.",
)}</p>
) }</p>
</>;
restoreButtonCaption = _t("Connect this session to Key Backup");
}
@ -253,11 +253,11 @@ export default class SecureBackupPanel extends React.PureComponent {
uploadStatus = "";
} else if (sessionsRemaining > 0) {
uploadStatus = <div>
{_t("Backing up %(sessionsRemaining)s keys...", { sessionsRemaining })} <br />
{ _t("Backing up %(sessionsRemaining)s keys...", { sessionsRemaining }) } <br />
</div>;
} else {
uploadStatus = <div>
{_t("All keys backed up")} <br />
{ _t("All keys backed up") } <br />
</div>;
}
@ -265,13 +265,13 @@ export default class SecureBackupPanel extends React.PureComponent {
const deviceName = sig.device ? (sig.device.getDisplayName() || sig.device.deviceId) : null;
const validity = sub =>
<span className={sig.valid ? 'mx_SecureBackupPanel_sigValid' : 'mx_SecureBackupPanel_sigInvalid'}>
{sub}
{ sub }
</span>;
const verify = sub =>
<span className={sig.device && sig.deviceTrust.isVerified() ? 'mx_SecureBackupPanel_deviceVerified' : 'mx_SecureBackupPanel_deviceNotVerified'}>
{sub}
{ sub }
</span>;
const device = sub => <span className="mx_SecureBackupPanel_deviceName">{deviceName}</span>;
const device = sub => <span className="mx_SecureBackupPanel_deviceName">{ deviceName }</span>;
const fromThisDevice = (
sig.device &&
sig.device.getFingerprint() === MatrixClientPeg.get().getDeviceEd25519Key()
@ -339,7 +339,7 @@ export default class SecureBackupPanel extends React.PureComponent {
}
return <div key={i}>
{sigStatus}
{ sigStatus }
</div>;
});
if (backupSigStatus.sigs.length === 0) {
@ -353,45 +353,45 @@ export default class SecureBackupPanel extends React.PureComponent {
extraDetailsTableRows = <>
<tr>
<td>{_t("Backup version:")}</td>
<td>{backupInfo.version}</td>
<td>{ _t("Backup version:") }</td>
<td>{ backupInfo.version }</td>
</tr>
<tr>
<td>{_t("Algorithm:")}</td>
<td>{backupInfo.algorithm}</td>
<td>{ _t("Algorithm:") }</td>
<td>{ backupInfo.algorithm }</td>
</tr>
</>;
extraDetails = <>
{uploadStatus}
<div>{backupSigStatuses}</div>
<div>{trustedLocally}</div>
{ uploadStatus }
<div>{ backupSigStatuses }</div>
<div>{ trustedLocally }</div>
</>;
actions.push(
<AccessibleButton key="restore" kind="primary" onClick={this._restoreBackup}>
{restoreButtonCaption}
{ restoreButtonCaption }
</AccessibleButton>,
);
if (!isSecureBackupRequired()) {
actions.push(
<AccessibleButton key="delete" kind="danger" onClick={this._deleteBackup}>
{_t("Delete Backup")}
{ _t("Delete Backup") }
</AccessibleButton>,
);
}
} else {
statusDescription = <>
<p>{_t(
<p>{ _t(
"Your keys are <b>not being backed up from this session</b>.", {},
{ b: sub => <b>{sub}</b> },
)}</p>
<p>{_t("Back up your keys before signing out to avoid losing them.")}</p>
{ b: sub => <b>{ sub }</b> },
) }</p>
<p>{ _t("Back up your keys before signing out to avoid losing them.") }</p>
</>;
actions.push(
<AccessibleButton key="setup" kind="primary" onClick={this._startNewBackup}>
{_t("Set up")}
{ _t("Set up") }
</AccessibleButton>,
);
}
@ -399,7 +399,7 @@ export default class SecureBackupPanel extends React.PureComponent {
if (secretStorageKeyInAccount) {
actions.push(
<AccessibleButton key="reset" kind="danger" onClick={this._resetSecretStorage}>
{_t("Reset")}
{ _t("Reset") }
</AccessibleButton>,
);
}
@ -417,47 +417,47 @@ export default class SecureBackupPanel extends React.PureComponent {
let actionRow;
if (actions.length) {
actionRow = <div className="mx_SecureBackupPanel_buttonRow">
{actions}
{ actions }
</div>;
}
return (
<div>
<p>{_t(
<p>{ _t(
"Back up your encryption keys with your account data in case you " +
"lose access to your sessions. Your keys will be secured with a " +
"unique Security Key.",
)}</p>
{statusDescription}
) }</p>
{ statusDescription }
<details>
<summary>{_t("Advanced")}</summary>
<summary>{ _t("Advanced") }</summary>
<table className="mx_SecureBackupPanel_statusList"><tbody>
<tr>
<td>{_t("Backup key stored:")}</td>
<td>{ _t("Backup key stored:") }</td>
<td>{
backupKeyStored === true ? _t("in secret storage") : _t("not stored")
}</td>
</tr>
<tr>
<td>{_t("Backup key cached:")}</td>
<td>{ _t("Backup key cached:") }</td>
<td>
{backupKeyCached ? _t("cached locally") : _t("not found locally")}
{backupKeyWellFormedText}
{ backupKeyCached ? _t("cached locally") : _t("not found locally") }
{ backupKeyWellFormedText }
</td>
</tr>
<tr>
<td>{_t("Secret storage public key:")}</td>
<td>{secretStorageKeyInAccount ? _t("in account data") : _t("not found")}</td>
<td>{ _t("Secret storage public key:") }</td>
<td>{ secretStorageKeyInAccount ? _t("in account data") : _t("not found") }</td>
</tr>
<tr>
<td>{_t("Secret storage:")}</td>
<td>{secretStorageReady ? _t("ready") : _t("not ready")}</td>
<td>{ _t("Secret storage:") }</td>
<td>{ secretStorageReady ? _t("ready") : _t("not ready") }</td>
</tr>
{extraDetailsTableRows}
{ extraDetailsTableRows }
</tbody></table>
{extraDetails}
{ extraDetails }
</details>
{actionRow}
{ actionRow }
</div>
);
}

View file

@ -134,7 +134,7 @@ export default class SetIdServer extends React.Component<IProps, IState> {
{ _t("Checking server") }
</div>;
} else if (this.state.error) {
return <span className='warning'>{this.state.error}</span>;
return <span className='warning'>{ this.state.error }</span>;
} else {
return null;
}
@ -193,8 +193,8 @@ export default class SetIdServer extends React.Component<IProps, IState> {
"Disconnect from the identity server <current /> and " +
"connect to <new /> instead?", {},
{
current: sub => <b>{abbreviateUrl(currentClientIdServer)}</b>,
new: sub => <b>{abbreviateUrl(idServer)}</b>,
current: sub => <b>{ abbreviateUrl(currentClientIdServer) }</b>,
new: sub => <b>{ abbreviateUrl(idServer) }</b>,
},
),
button: _t("Continue"),
@ -224,10 +224,10 @@ export default class SetIdServer extends React.Component<IProps, IState> {
description: (
<div>
<span className="warning">
{_t("The identity server you have chosen does not have any terms of service.")}
{ _t("The identity server you have chosen does not have any terms of service.") }
</span>
<span>
&nbsp;{_t("Only continue if you trust the owner of the server.")}
&nbsp;{ _t("Only continue if you trust the owner of the server.") }
</span>
</div>
),
@ -243,7 +243,7 @@ export default class SetIdServer extends React.Component<IProps, IState> {
title: _t("Disconnect identity server"),
unboundMessage: _t(
"Disconnect from the identity server <idserver />?", {},
{ idserver: sub => <b>{abbreviateUrl(this.state.currentClientIdServer)}</b> },
{ idserver: sub => <b>{ abbreviateUrl(this.state.currentClientIdServer) }</b> },
),
button: _t("Disconnect"),
});
@ -278,41 +278,41 @@ export default class SetIdServer extends React.Component<IProps, IState> {
let message;
let danger = false;
const messageElements = {
idserver: sub => <b>{abbreviateUrl(currentClientIdServer)}</b>,
b: sub => <b>{sub}</b>,
idserver: sub => <b>{ abbreviateUrl(currentClientIdServer) }</b>,
b: sub => <b>{ sub }</b>,
};
if (!currentServerReachable) {
message = <div>
<p>{_t(
<p>{ _t(
"You should <b>remove your personal data</b> from identity server " +
"<idserver /> before disconnecting. Unfortunately, identity server " +
"<idserver /> is currently offline or cannot be reached.",
{}, messageElements,
)}</p>
<p>{_t("You should:")}</p>
) }</p>
<p>{ _t("You should:") }</p>
<ul>
<li>{_t(
<li>{ _t(
"check your browser plugins for anything that might block " +
"the identity server (such as Privacy Badger)",
)}</li>
<li>{_t("contact the administrators of identity server <idserver />", {}, {
) }</li>
<li>{ _t("contact the administrators of identity server <idserver />", {}, {
idserver: messageElements.idserver,
})}</li>
<li>{_t("wait and try again later")}</li>
}) }</li>
<li>{ _t("wait and try again later") }</li>
</ul>
</div>;
danger = true;
button = _t("Disconnect anyway");
} else if (boundThreepids.length) {
message = <div>
<p>{_t(
<p>{ _t(
"You are still <b>sharing your personal data</b> on the identity " +
"server <idserver />.", {}, messageElements,
)}</p>
<p>{_t(
) }</p>
<p>{ _t(
"We recommend that you remove your email addresses and phone numbers " +
"from the identity server before disconnecting.",
)}</p>
) }</p>
</div>;
danger = true;
button = _t("Disconnect anyway");
@ -361,13 +361,13 @@ export default class SetIdServer extends React.Component<IProps, IState> {
"You are currently using <server></server> to discover and be discoverable by " +
"existing contacts you know. You can change your identity server below.",
{},
{ server: sub => <b>{abbreviateUrl(idServerUrl)}</b> },
{ server: sub => <b>{ abbreviateUrl(idServerUrl) }</b> },
);
if (this.props.missingTerms) {
bodyText = _t(
"If you don't want to use <server /> to discover and be discoverable by existing " +
"contacts you know, enter another identity server below.",
{}, { server: sub => <b>{abbreviateUrl(idServerUrl)}</b> },
{}, { server: sub => <b>{ abbreviateUrl(idServerUrl) }</b> },
);
}
} else {
@ -399,9 +399,9 @@ export default class SetIdServer extends React.Component<IProps, IState> {
discoButtonContent = <InlineSpinner />;
}
discoSection = <div>
<span className="mx_SettingsTab_subsectionText">{discoBodyText}</span>
<span className="mx_SettingsTab_subsectionText">{ discoBodyText }</span>
<AccessibleButton onClick={this.onDisconnectClicked} kind="danger_sm">
{discoButtonContent}
{ discoButtonContent }
</AccessibleButton>
</div>;
}
@ -409,10 +409,10 @@ export default class SetIdServer extends React.Component<IProps, IState> {
return (
<form className="mx_SettingsTab_section mx_SetIdServer" onSubmit={this.checkIdServer}>
<span className="mx_SettingsTab_subheading">
{sectionTitle}
{ sectionTitle }
</span>
<span className="mx_SettingsTab_subsectionText">
{bodyText}
{ bodyText }
</span>
<Field
label={_t("Enter a new identity server")}
@ -426,11 +426,13 @@ export default class SetIdServer extends React.Component<IProps, IState> {
disabled={this.state.busy}
forceValidity={this.state.error ? false : null}
/>
<AccessibleButton type="submit" kind="primary_sm"
<AccessibleButton
type="submit"
kind="primary_sm"
onClick={this.checkIdServer}
disabled={!this.idServerChangeEnabled()}
>{_t("Change")}</AccessibleButton>
{discoSection}
>{ _t("Change") }</AccessibleButton>
{ discoSection }
</form>
);
}

View file

@ -68,7 +68,7 @@ export default class SetIntegrationManager extends React.Component<IProps, IStat
"Use an integration manager <b>(%(serverName)s)</b> to manage bots, widgets, " +
"and sticker packs.",
{ serverName: currentManager.name },
{ b: sub => <b>{sub}</b> },
{ b: sub => <b>{ sub }</b> },
);
} else {
bodyText = _t("Use an integration manager to manage bots, widgets, and sticker packs.");
@ -77,18 +77,18 @@ export default class SetIntegrationManager extends React.Component<IProps, IStat
return (
<div className='mx_SetIntegrationManager'>
<div className="mx_SettingsTab_heading">
<span>{_t("Manage integrations")}</span>
<span className="mx_SettingsTab_subheading">{managerName}</span>
<span>{ _t("Manage integrations") }</span>
<span className="mx_SettingsTab_subheading">{ managerName }</span>
<ToggleSwitch checked={this.state.provisioningEnabled} onChange={this.onProvisioningToggled} />
</div>
<span className="mx_SettingsTab_subsectionText">
{bodyText}
{ bodyText }
<br />
<br />
{_t(
{ _t(
"Integration managers receive configuration data, and can modify widgets, " +
"send room invites, and set power levels on your behalf.",
)}
) }
</span>
</div>
);

View file

@ -35,7 +35,7 @@ interface SpellCheckLanguagesIState {
}
export class ExistingSpellCheckLanguage extends React.Component<ExistingSpellCheckLanguageIProps> {
_onRemove = (e) => {
private onRemove = (e) => {
e.stopPropagation();
e.preventDefault();
@ -45,9 +45,9 @@ export class ExistingSpellCheckLanguage extends React.Component<ExistingSpellChe
render() {
return (
<div className="mx_ExistingSpellCheckLanguage">
<span className="mx_ExistingSpellCheckLanguage_language">{this.props.language}</span>
<AccessibleButton onClick={this._onRemove} kind="danger_sm">
{_t("Remove")}
<span className="mx_ExistingSpellCheckLanguage_language">{ this.props.language }</span>
<AccessibleButton onClick={this.onRemove} kind="danger_sm">
{ _t("Remove") }
</AccessibleButton>
</div>
);
@ -63,12 +63,12 @@ export default class SpellCheckLanguages extends React.Component<SpellCheckLangu
};
}
_onRemoved = (language) => {
private onRemoved = (language: string) => {
const languages = this.props.languages.filter((e) => e !== language);
this.props.onLanguagesChange(languages);
};
_onAddClick = (e) => {
private onAddClick = (e) => {
e.stopPropagation();
e.preventDefault();
@ -81,31 +81,31 @@ export default class SpellCheckLanguages extends React.Component<SpellCheckLangu
this.props.onLanguagesChange(this.props.languages);
};
_onNewLanguageChange = (language: string) => {
private onNewLanguageChange = (language: string) => {
if (this.state.newLanguage === language) return;
this.setState({ newLanguage: language });
};
render() {
const existingSpellCheckLanguages = this.props.languages.map((e) => {
return <ExistingSpellCheckLanguage language={e} onRemoved={this._onRemoved} key={e} />;
return <ExistingSpellCheckLanguage language={e} onRemoved={this.onRemoved} key={e} />;
});
const addButton = (
<AccessibleButton onClick={this._onAddClick} kind="primary">
{_t("Add")}
<AccessibleButton onClick={this.onAddClick} kind="primary">
{ _t("Add") }
</AccessibleButton>
);
return (
<div className="mx_SpellCheckLanguages">
{existingSpellCheckLanguages}
<form onSubmit={this._onAddClick} noValidate={true}>
{ existingSpellCheckLanguages }
<form onSubmit={this.onAddClick} noValidate={true}>
<SpellCheckLanguagesDropdown
className="mx_GeneralUserSettingsTab_spellCheckLanguageInput"
value={this.state.newLanguage}
onOptionChange={this._onNewLanguageChange} />
{addButton}
onOptionChange={this.onNewLanguageChange} />
{ addButton }
</form>
</div>
);

View file

@ -42,7 +42,7 @@ function getStatusText(status: UpdateCheckStatus, errorDetail?: string) {
return _t('Downloading update...');
case UpdateCheckStatus.Ready:
return _t("New version available. <a>Update now.</a>", {}, {
a: sub => <AccessibleButton kind="link" onClick={installUpdate}>{sub}</AccessibleButton>,
a: sub => <AccessibleButton kind="link" onClick={installUpdate}>{ sub }</AccessibleButton>,
});
}
}
@ -72,14 +72,14 @@ const UpdateCheckButton = () => {
let suffix;
if (state) {
suffix = <span className="mx_UpdateCheckButton_summary">
{getStatusText(state.status, state.detail)}
{busy && <InlineSpinner />}
{ getStatusText(state.status, state.detail) }
{ busy && <InlineSpinner /> }
</span>;
}
return <React.Fragment>
<AccessibleButton onClick={onCheckForUpdateClick} kind="primary" disabled={busy}>
{_t("Check for update")}
{ _t("Check for update") }
</AccessibleButton>
{ suffix }
</React.Fragment>;

View file

@ -88,21 +88,21 @@ export class ExistingEmailAddress extends React.Component {
return (
<div className="mx_ExistingEmailAddress">
<span className="mx_ExistingEmailAddress_promptText">
{_t("Remove %(email)s?", { email: this.props.email.address } )}
{ _t("Remove %(email)s?", { email: this.props.email.address } ) }
</span>
<AccessibleButton
onClick={this._onActuallyRemove}
kind="danger_sm"
className="mx_ExistingEmailAddress_confirmBtn"
>
{_t("Remove")}
{ _t("Remove") }
</AccessibleButton>
<AccessibleButton
onClick={this._onDontRemove}
kind="link_sm"
className="mx_ExistingEmailAddress_confirmBtn"
>
{_t("Cancel")}
{ _t("Cancel") }
</AccessibleButton>
</div>
);
@ -110,9 +110,9 @@ export class ExistingEmailAddress extends React.Component {
return (
<div className="mx_ExistingEmailAddress">
<span className="mx_ExistingEmailAddress_email">{this.props.email.address}</span>
<span className="mx_ExistingEmailAddress_email">{ this.props.email.address }</span>
<AccessibleButton onClick={this._onRemove} kind="danger_sm">
{_t("Remove")}
{ _t("Remove") }
</AccessibleButton>
</div>
);
@ -229,19 +229,19 @@ export default class EmailAddresses extends React.Component {
let addButton = (
<AccessibleButton onClick={this._onAddClick} kind="primary">
{_t("Add")}
{ _t("Add") }
</AccessibleButton>
);
if (this.state.verifying) {
addButton = (
<div>
<div>{_t("We've sent you an email to verify your address. Please follow the instructions there and then click the button below.")}</div>
<div>{ _t("We've sent you an email to verify your address. Please follow the instructions there and then click the button below.") }</div>
<AccessibleButton
onClick={this._onContinueClick}
kind="primary"
disabled={this.state.continueDisabled}
>
{_t("Continue")}
{ _t("Continue") }
</AccessibleButton>
</div>
);
@ -249,7 +249,7 @@ export default class EmailAddresses extends React.Component {
return (
<div className="mx_EmailAddresses">
{existingEmailElements}
{ existingEmailElements }
<form
onSubmit={this._onAddClick}
autoComplete="off"
@ -264,7 +264,7 @@ export default class EmailAddresses extends React.Component {
value={this.state.newEmailAddress}
onChange={this._onChangeNewEmailAddress}
/>
{addButton}
{ addButton }
</form>
</div>
);

View file

@ -83,21 +83,21 @@ export class ExistingPhoneNumber extends React.Component {
return (
<div className="mx_ExistingPhoneNumber">
<span className="mx_ExistingPhoneNumber_promptText">
{_t("Remove %(phone)s?", { phone: this.props.msisdn.address })}
{ _t("Remove %(phone)s?", { phone: this.props.msisdn.address }) }
</span>
<AccessibleButton
onClick={this._onActuallyRemove}
kind="danger_sm"
className="mx_ExistingPhoneNumber_confirmBtn"
>
{_t("Remove")}
{ _t("Remove") }
</AccessibleButton>
<AccessibleButton
onClick={this._onDontRemove}
kind="link_sm"
className="mx_ExistingPhoneNumber_confirmBtn"
>
{_t("Cancel")}
{ _t("Cancel") }
</AccessibleButton>
</div>
);
@ -105,9 +105,9 @@ export class ExistingPhoneNumber extends React.Component {
return (
<div className="mx_ExistingPhoneNumber">
<span className="mx_ExistingPhoneNumber_address">+{this.props.msisdn.address}</span>
<span className="mx_ExistingPhoneNumber_address">+{ this.props.msisdn.address }</span>
<AccessibleButton onClick={this._onRemove} kind="danger_sm">
{_t("Remove")}
{ _t("Remove") }
</AccessibleButton>
</div>
);
@ -230,7 +230,7 @@ export default class PhoneNumbers extends React.Component {
let addVerifySection = (
<AccessibleButton onClick={this._onAddClick} kind="primary">
{_t("Add")}
{ _t("Add") }
</AccessibleButton>
);
if (this.state.verifying) {
@ -238,10 +238,10 @@ export default class PhoneNumbers extends React.Component {
addVerifySection = (
<div>
<div>
{_t("A text message has been sent to +%(msisdn)s. " +
"Please enter the verification code it contains.", { msisdn: msisdn })}
{ _t("A text message has been sent to +%(msisdn)s. " +
"Please enter the verification code it contains.", { msisdn: msisdn }) }
<br />
{this.state.verifyError}
{ this.state.verifyError }
</div>
<form onSubmit={this._onContinueClick} autoComplete="off" noValidate={true}>
<Field
@ -257,7 +257,7 @@ export default class PhoneNumbers extends React.Component {
kind="primary"
disabled={this.state.continueDisabled}
>
{_t("Continue")}
{ _t("Continue") }
</AccessibleButton>
</form>
</div>
@ -274,7 +274,7 @@ export default class PhoneNumbers extends React.Component {
return (
<div className="mx_PhoneNumbers">
{existingPhoneElements}
{ existingPhoneElements }
<form onSubmit={this._onAddClick} autoComplete="off" noValidate={true} className="mx_PhoneNumbers_new">
<div className="mx_PhoneNumbers_input">
<Field
@ -288,7 +288,7 @@ export default class PhoneNumbers extends React.Component {
/>
</div>
</form>
{addVerifySection}
{ addVerifySection }
</div>
);
}

View file

@ -198,14 +198,14 @@ export class EmailAddress extends React.Component {
let status;
if (verifying) {
status = <span>
{_t("Verify the link in your inbox")}
{ _t("Verify the link in your inbox") }
<AccessibleButton
className="mx_ExistingEmailAddress_confirmBtn"
kind="primary_sm"
onClick={this.onContinueClick}
disabled={this.state.continueDisabled}
>
{_t("Complete")}
{ _t("Complete") }
</AccessibleButton>
</span>;
} else if (bound) {
@ -214,7 +214,7 @@ export class EmailAddress extends React.Component {
kind="danger_sm"
onClick={this.onRevokeClick}
>
{_t("Revoke")}
{ _t("Revoke") }
</AccessibleButton>;
} else {
status = <AccessibleButton
@ -222,14 +222,14 @@ export class EmailAddress extends React.Component {
kind="primary_sm"
onClick={this.onShareClick}
>
{_t("Share")}
{ _t("Share") }
</AccessibleButton>;
}
return (
<div className="mx_ExistingEmailAddress">
<span className="mx_ExistingEmailAddress_email">{address}</span>
{status}
<span className="mx_ExistingEmailAddress_email">{ address }</span>
{ status }
</div>
);
}
@ -249,13 +249,13 @@ export default class EmailAddresses extends React.Component {
});
} else {
content = <span className="mx_SettingsTab_subsectionText">
{_t("Discovery options will appear once you have added an email above.")}
{ _t("Discovery options will appear once you have added an email above.") }
</span>;
}
return (
<div className="mx_EmailAddresses">
{content}
{ content }
</div>
);
}

View file

@ -205,9 +205,9 @@ export class PhoneNumber extends React.Component {
if (verifying) {
status = <span className="mx_ExistingPhoneNumber_verification">
<span>
{_t("Please enter verification code sent via text.")}
{ _t("Please enter verification code sent via text.") }
<br />
{this.state.verifyError}
{ this.state.verifyError }
</span>
<form onSubmit={this.onContinueClick} autoComplete="off" noValidate={true}>
<Field
@ -226,7 +226,7 @@ export class PhoneNumber extends React.Component {
kind="danger_sm"
onClick={this.onRevokeClick}
>
{_t("Revoke")}
{ _t("Revoke") }
</AccessibleButton>;
} else {
status = <AccessibleButton
@ -234,14 +234,14 @@ export class PhoneNumber extends React.Component {
kind="primary_sm"
onClick={this.onShareClick}
>
{_t("Share")}
{ _t("Share") }
</AccessibleButton>;
}
return (
<div className="mx_ExistingPhoneNumber">
<span className="mx_ExistingPhoneNumber_address">+{address}</span>
{status}
<span className="mx_ExistingPhoneNumber_address">+{ address }</span>
{ status }
</div>
);
}
@ -261,13 +261,13 @@ export default class PhoneNumbers extends React.Component {
});
} else {
content = <span className="mx_SettingsTab_subsectionText">
{_t("Discovery options will appear once you have added a phone number above.")}
{ _t("Discovery options will appear once you have added a phone number above.") }
</span>;
}
return (
<div className="mx_PhoneNumbers">
{content}
{ content }
</div>
);
}

View file

@ -116,8 +116,8 @@ export default class AdvancedRoomSettingsTab extends React.Component<IProps, ISt
"to the new version of the room.</i> We'll post a link to the new room in the old " +
"version of the room - room members will have to click this link to join the new room.",
{}, {
"b": (sub) => <b>{sub}</b>,
"i": (sub) => <i>{sub}</i>,
"b": (sub) => <b>{ sub }</b>,
"i": (sub) => <i>{ sub }</i>,
},
) }
</p>

View file

@ -61,36 +61,36 @@ export default class BridgeSettingsTab extends React.Component<IProps> {
let content: JSX.Element;
if (bridgeEvents.length > 0) {
content = <div>
<p>{_t(
<p>{ _t(
"This room is bridging messages to the following platforms. " +
"<a>Learn more.</a>", {},
{
// TODO: We don't have this link yet: this will prevent the translators
// having to re-translate the string when we do.
a: sub => <a href={BRIDGES_LINK} target="_blank" rel="noreferrer noopener">{sub}</a>,
a: sub => <a href={BRIDGES_LINK} target="_blank" rel="noreferrer noopener">{ sub }</a>,
},
)}</p>
) }</p>
<ul className="mx_RoomSettingsDialog_BridgeList">
{ bridgeEvents.map((event) => this.renderBridgeCard(event, room)) }
</ul>
</div>;
} else {
content = <p>{_t(
content = <p>{ _t(
"This room isnt bridging messages to any platforms. " +
"<a>Learn more.</a>", {},
{
// TODO: We don't have this link yet: this will prevent the translators
// having to re-translate the string when we do.
a: sub => <a href={BRIDGES_LINK} target="_blank" rel="noreferrer noopener">{sub}</a>,
a: sub => <a href={BRIDGES_LINK} target="_blank" rel="noreferrer noopener">{ sub }</a>,
},
)}</p>;
) }</p>;
}
return (
<div className="mx_SettingsTab">
<div className="mx_SettingsTab_heading">{_t("Bridges")}</div>
<div className="mx_SettingsTab_heading">{ _t("Bridges") }</div>
<div className='mx_SettingsTab_section mx_SettingsTab_subsectionText'>
{content}
{ content }
</div>
</div>
);

View file

@ -65,7 +65,7 @@ export default class GeneralRoomSettingsTab extends React.Component {
const groupsEvent = room.currentState.getStateEvents("m.room.related_groups", "");
let urlPreviewSettings = <>
<span className='mx_SettingsTab_subheading'>{_t("URL Previews")}</span>
<span className='mx_SettingsTab_subheading'>{ _t("URL Previews") }</span>
<div className='mx_SettingsTab_section'>
<UrlPreviewSettings room={room} />
</div>
@ -77,7 +77,7 @@ export default class GeneralRoomSettingsTab extends React.Component {
let flairSection;
if (SettingsStore.getValue(UIFeature.Flair)) {
flairSection = <>
<span className='mx_SettingsTab_subheading'>{_t("Flair")}</span>
<span className='mx_SettingsTab_subheading'>{ _t("Flair") }</span>
<div className='mx_SettingsTab_section mx_SettingsTab_subsectionText'>
<RelatedGroupSettings
roomId={room.roomId}
@ -90,22 +90,25 @@ export default class GeneralRoomSettingsTab extends React.Component {
return (
<div className="mx_SettingsTab mx_GeneralRoomSettingsTab">
<div className="mx_SettingsTab_heading">{_t("General")}</div>
<div className="mx_SettingsTab_heading">{ _t("General") }</div>
<div className='mx_SettingsTab_section mx_GeneralRoomSettingsTab_profileSection'>
<RoomProfileSettings roomId={this.props.roomId} />
</div>
<div className="mx_SettingsTab_heading">{_t("Room Addresses")}</div>
<div className="mx_SettingsTab_heading">{ _t("Room Addresses") }</div>
<div className='mx_SettingsTab_section mx_SettingsTab_subsectionText'>
<AliasSettings roomId={this.props.roomId}
canSetCanonicalAlias={canSetCanonical} canSetAliases={canSetAliases}
canonicalAliasEvent={canonicalAliasEv} />
<AliasSettings
roomId={this.props.roomId}
canSetCanonicalAlias={canSetCanonical}
canSetAliases={canSetAliases}
canonicalAliasEvent={canonicalAliasEv}
/>
</div>
<div className="mx_SettingsTab_heading">{_t("Other")}</div>
<div className="mx_SettingsTab_heading">{ _t("Other") }</div>
{ flairSection }
{ urlPreviewSettings }
<span className='mx_SettingsTab_subheading'>{_t("Leave room")}</span>
<span className='mx_SettingsTab_subheading'>{ _t("Leave room") }</span>
<div className='mx_SettingsTab_section'>
<AccessibleButton kind='danger' onClick={this._onLeaveClick}>
{ _t('Leave room') }

View file

@ -142,36 +142,36 @@ export default class NotificationsSettingsTab extends React.Component {
if (this.state.uploadedFile) {
currentUploadedFile = (
<div>
<span>{_t("Uploaded sound")}: <code>{this.state.uploadedFile.name}</code></span>
<span>{ _t("Uploaded sound") }: <code>{ this.state.uploadedFile.name }</code></span>
</div>
);
}
return (
<div className="mx_SettingsTab">
<div className="mx_SettingsTab_heading">{_t("Notifications")}</div>
<div className="mx_SettingsTab_heading">{ _t("Notifications") }</div>
<div className='mx_SettingsTab_section mx_SettingsTab_subsectionText'>
<span className='mx_SettingsTab_subheading'>{_t("Sounds")}</span>
<span className='mx_SettingsTab_subheading'>{ _t("Sounds") }</span>
<div>
<span>{_t("Notification sound")}: <code>{this.state.currentSound}</code></span><br />
<span>{ _t("Notification sound") }: <code>{ this.state.currentSound }</code></span><br />
<AccessibleButton className="mx_NotificationSound_resetSound" disabled={this.state.currentSound == "default"} onClick={this._clearSound.bind(this)} kind="primary">
{_t("Reset")}
{ _t("Reset") }
</AccessibleButton>
</div>
<div>
<h3>{_t("Set a new custom sound")}</h3>
<h3>{ _t("Set a new custom sound") }</h3>
<form autoComplete="off" noValidate={true}>
<input ref={this._soundUpload} className="mx_NotificationSound_soundUpload" type="file" onChange={this._onSoundUploadChanged.bind(this)} accept="audio/*" />
</form>
{currentUploadedFile}
{ currentUploadedFile }
<AccessibleButton className="mx_NotificationSound_browse" onClick={this._triggerUploader.bind(this)} kind="primary">
{_t("Browse")}
{ _t("Browse") }
</AccessibleButton>
<AccessibleButton className="mx_NotificationSound_save" disabled={this.state.uploadedFile == null} onClick={this._onClickSaveSound.bind(this)} kind="primary">
{_t("Save")}
{ _t("Save") }
</AccessibleButton>
<br />
</div>

View file

@ -102,10 +102,10 @@ export class BannedUser extends React.Component<IBannedUserProps> {
const userId = this.props.member.name === this.props.member.userId ? null : this.props.member.userId;
return (
<li>
{unbanButton}
{ 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 : ""}
<strong>{ this.props.member.name }</strong> { userId }
{ this.props.reason ? " " + _t('Reason') + ": " + this.props.reason : "" }
</span>
</li>
);
@ -273,7 +273,7 @@ export default class RolesRoomSettingsTab extends React.Component<IProps> {
parseIntWithDefault(plContent.events_default, powerLevelDescriptors.events_default.defaultValue),
);
let privilegedUsersSection = <div>{_t('No users have specific privileges in this room')}</div>;
let privilegedUsersSection = <div>{ _t('No users have specific privileges in this room') }</div>;
let mutedUsersSection;
if (Object.keys(userLevels).length) {
const privilegedUsers = [];
@ -320,14 +320,14 @@ export default class RolesRoomSettingsTab extends React.Component<IProps> {
privilegedUsersSection =
<div className='mx_SettingsTab_section mx_SettingsTab_subsectionText'>
<div className='mx_SettingsTab_subheading'>{ _t('Privileged Users') }</div>
{privilegedUsers}
{ privilegedUsers }
</div>;
}
if (mutedUsers.length) {
mutedUsersSection =
<div className='mx_SettingsTab_section mx_SettingsTab_subsectionText'>
<div className='mx_SettingsTab_subheading'>{ _t('Muted Users') }</div>
{mutedUsers}
{ mutedUsers }
</div>;
}
}
@ -340,18 +340,21 @@ export default class RolesRoomSettingsTab extends React.Component<IProps> {
<div className='mx_SettingsTab_section mx_SettingsTab_subsectionText'>
<div className='mx_SettingsTab_subheading'>{ _t('Banned users') }</div>
<ul>
{banned.map((member) => {
{ 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}
<BannedUser
key={member.userId}
canUnban={canBanUsers}
member={member}
reason={banEvent.reason}
by={bannedBy}
/>
);
})}
}) }
</ul>
</div>;
}
@ -409,15 +412,15 @@ export default class RolesRoomSettingsTab extends React.Component<IProps> {
return (
<div className="mx_SettingsTab mx_RolesRoomSettingsTab">
<div className="mx_SettingsTab_heading">{_t("Roles & Permissions")}</div>
{privilegedUsersSection}
{mutedUsersSection}
{bannedUsersSection}
<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>
<p>{_t('Select the roles required to change various parts of the room')}</p>
{powerSelectors}
{eventPowerSelectors}
<span className='mx_SettingsTab_subheading'>{ _t("Permissions") }</span>
<p>{ _t('Select the roles required to change various parts of the room') }</p>
{ powerSelectors }
{ eventPowerSelectors }
</div>
</div>
);

View file

@ -15,52 +15,43 @@ limitations under the License.
*/
import React from 'react';
import { MatrixEvent } from "matrix-js-sdk/src/models/event";
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";
import { MatrixClientPeg } from "../../../../../MatrixClientPeg";
import LabelledToggleSwitch from "../../../elements/LabelledToggleSwitch";
import Modal from "../../../../../Modal";
import QuestionDialog from "../../../dialogs/QuestionDialog";
import StyledRadioGroup from '../../../elements/StyledRadioGroup';
import StyledRadioGroup, { IDefinition } from '../../../elements/StyledRadioGroup';
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";
import { arrayHasDiff } from "../../../../../utils/arrays";
import SettingsFlag from '../../../elements/SettingsFlag';
// Knock and private are reserved keywords which are not yet implemented.
export enum JoinRule {
Public = "public",
Knock = "knock",
Invite = "invite",
/**
* @deprecated Reserved. Should not be used.
*/
Private = "private",
}
export enum GuestAccess {
CanJoin = "can_join",
Forbidden = "forbidden",
}
export enum HistoryVisibility {
Invited = "invited",
Joined = "joined",
Shared = "shared",
WorldReadable = "world_readable",
}
interface IProps {
roomId: string;
}
interface IState {
joinRule: JoinRule;
restrictedAllowRoomIds?: string[];
guestAccess: GuestAccess;
history: HistoryVisibility;
hasAliases: boolean;
encrypted: boolean;
roomSupportsRestricted?: boolean;
preferredRestrictionVersion?: string;
showAdvancedSection: boolean;
}
@replaceableComponent("views.settings.tabs.room.SecurityRoomSettingsTab")
@ -70,44 +61,58 @@ 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,
};
}
// TODO: [REACT-WARNING] Move this to constructor
async UNSAFE_componentWillMount() { // eslint-disable-line camelcase
MatrixClientPeg.get().on("RoomState.events", this.onStateEvent);
UNSAFE_componentWillMount() { // eslint-disable-line
const cli = MatrixClientPeg.get();
cli.on("RoomState.events", this.onStateEvent);
const room = MatrixClientPeg.get().getRoom(this.props.roomId);
const room = cli.getRoom(this.props.roomId);
const state = room.currentState;
const joinRule: JoinRule = this.pullContentPropertyFromEvent(
state.getStateEvents("m.room.join_rules", ""),
const joinRuleEvent = state.getStateEvents(EventType.RoomJoinRules, "");
const joinRule: JoinRule = this.pullContentPropertyFromEvent<JoinRule>(
joinRuleEvent,
'join_rule',
JoinRule.Invite,
);
const guestAccess: GuestAccess = this.pullContentPropertyFromEvent(
state.getStateEvents("m.room.guest_access", ""),
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(
state.getStateEvents("m.room.history_visibility", ""),
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 });
const hasAliases = await this.hasAliases();
this.setState({ hasAliases });
const restrictedRoomCapabilities = SpaceStore.instance.restrictedJoinRuleSupport;
const roomSupportsRestricted = Array.isArray(restrictedRoomCapabilities?.support)
&& restrictedRoomCapabilities.support.includes(room.getVersion());
const preferredRestrictionVersion = roomSupportsRestricted ? undefined : restrictedRoomCapabilities?.preferred;
this.setState({ joinRule, restrictedAllowRoomIds, guestAccess, history, encrypted,
roomSupportsRestricted, preferredRestrictionVersion });
this.hasAliases().then(hasAliases => this.setState({ hasAliases }));
}
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() {
@ -115,13 +120,13 @@ export default class SecurityRoomSettingsTab extends React.Component<IProps, ISt
}
private onStateEvent = (e: MatrixEvent) => {
const refreshWhenTypes = [
'm.room.join_rules',
'm.room.guest_access',
'm.room.history_visibility',
'm.room.encryption',
const refreshWhenTypes: EventType[] = [
EventType.RoomJoinRules,
EventType.RoomGuestAccess,
EventType.RoomHistoryVisibility,
EventType.RoomEncryption,
];
if (refreshWhenTypes.includes(e.getType())) this.forceUpdate();
if (refreshWhenTypes.includes(e.getType() as EventType)) this.forceUpdate();
};
private onEncryptionChange = () => {
@ -133,9 +138,11 @@ export default class SecurityRoomSettingsTab extends React.Component<IProps, ISt
"may prevent many bots and bridges from working correctly. <a>Learn more about encryption.</a>",
{},
{
a: sub => <a href="https://element.io/help#encryption"
rel="noreferrer noopener" target="_blank"
>{sub}</a>,
a: sub => <a
href="https://element.io/help#encryption"
rel="noreferrer noopener"
target="_blank"
>{ sub }</a>,
},
),
onFinished: (confirm) => {
@ -147,7 +154,7 @@ export default class SecurityRoomSettingsTab extends React.Component<IProps, ISt
const beforeEncrypted = this.state.encrypted;
this.setState({ encrypted: true });
MatrixClientPeg.get().sendStateEvent(
this.props.roomId, "m.room.encryption",
this.props.roomId, EventType.RoomEncryption,
{ algorithm: "m.megolm.v1.aes-sha2" },
).catch((e) => {
console.error(e);
@ -157,89 +164,91 @@ export default class SecurityRoomSettingsTab extends React.Component<IProps, ISt
});
};
private fixGuestAccess = (e: React.MouseEvent) => {
e.preventDefault();
e.stopPropagation();
const joinRule = JoinRule.Invite;
const guestAccess = GuestAccess.CanJoin;
private onJoinRuleChange = async (joinRule: JoinRule) => {
const beforeJoinRule = this.state.joinRule;
const beforeGuestAccess = this.state.guestAccess;
this.setState({ joinRule, guestAccess });
let restrictedAllowRoomIds: string[];
if (joinRule === JoinRule.Restricted) {
const matrixClient = MatrixClientPeg.get();
const roomId = this.props.roomId;
const room = matrixClient.getRoom(roomId);
if (beforeJoinRule === JoinRule.Restricted || this.state.roomSupportsRestricted) {
// Have the user pick which spaces to allow joins from
restrictedAllowRoomIds = await this.editRestrictedRoomIds();
if (!Array.isArray(restrictedAllowRoomIds)) return;
} else if (this.state.preferredRestrictionVersion) {
// Block this action on a room upgrade otherwise it'd make their room unjoinable
const targetVersion = this.state.preferredRestrictionVersion;
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: (resp) => {
if (!resp?.continue) return;
upgradeRoom(room, targetVersion, resp.invite);
},
});
return;
}
}
if (beforeJoinRule === joinRule && !restrictedAllowRoomIds) return;
const content: IContent = {
join_rule: joinRule,
};
// pre-set the accepted spaces with the currently viewed one as per the microcopy
if (joinRule === JoinRule.Restricted) {
content.allow = restrictedAllowRoomIds.map(roomId => ({
"type": RestrictedAllowType.RoomMembership,
"room_id": roomId,
}));
}
this.setState({ joinRule, restrictedAllowRoomIds });
const client = MatrixClientPeg.get();
client.sendStateEvent(
this.props.roomId,
"m.room.join_rules",
{ join_rule: joinRule },
"",
).catch((e) => {
client.sendStateEvent(this.props.roomId, EventType.RoomJoinRules, content, "").catch((e) => {
console.error(e);
this.setState({ joinRule: beforeJoinRule });
});
client.sendStateEvent(
this.props.roomId,
"m.room.guest_access",
{ guest_access: guestAccess },
"",
).catch((e) => {
console.error(e);
this.setState({ guestAccess: beforeGuestAccess });
this.setState({
joinRule: beforeJoinRule,
restrictedAllowRoomIds: undefined,
});
});
};
private onRoomAccessRadioToggle = (roomAccess: string) => {
// join_rule
// INVITE | PUBLIC
// ----------------------+----------------
// guest CAN_JOIN | inv_only | pub_with_guest
// access ----------------------+----------------
// FORBIDDEN | inv_only | pub_no_guest
// ----------------------+----------------
// 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 "invite_only":
// no change - use defaults above
break;
case "public_no_guests":
joinRule = JoinRule.Public;
guestAccess = GuestAccess.Forbidden;
break;
case "public_with_guests":
joinRule = JoinRule.Public;
guestAccess = GuestAccess.CanJoin;
break;
}
const beforeJoinRule = this.state.joinRule;
const beforeGuestAccess = this.state.guestAccess;
this.setState({ joinRule, guestAccess });
private onRestrictedRoomIdsChange = (restrictedAllowRoomIds: string[]) => {
const beforeRestrictedAllowRoomIds = this.state.restrictedAllowRoomIds;
if (!arrayHasDiff(beforeRestrictedAllowRoomIds || [], restrictedAllowRoomIds)) return;
this.setState({ restrictedAllowRoomIds });
const client = MatrixClientPeg.get();
client.sendStateEvent(
this.props.roomId,
"m.room.join_rules",
{ join_rule: joinRule },
"",
).catch((e) => {
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({ joinRule: beforeJoinRule });
this.setState({ restrictedAllowRoomIds: beforeRestrictedAllowRoomIds });
});
client.sendStateEvent(
this.props.roomId,
"m.room.guest_access",
{ guest_access: guestAccess },
"",
).catch((e) => {
};
private onGuestAccessChange = (allowed: boolean) => {
const guestAccess = allowed ? GuestAccess.CanJoin : GuestAccess.Forbidden;
const beforeGuestAccess = this.state.guestAccess;
if (beforeGuestAccess === guestAccess) return;
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 });
});
@ -247,8 +256,10 @@ export default class SecurityRoomSettingsTab extends React.Component<IProps, ISt
private onHistoryRadioToggle = (history: HistoryVisibility) => {
const beforeHistory = this.state.history;
if (beforeHistory === history) return;
this.setState({ history: history });
MatrixClientPeg.get().sendStateEvent(this.props.roomId, "m.room.history_visibility", {
MatrixClientPeg.get().sendStateEvent(this.props.roomId, EventType.RoomHistoryVisibility, {
history_visibility: history,
}, "").catch((e) => {
console.error(e);
@ -268,74 +279,159 @@ export default class SecurityRoomSettingsTab extends React.Component<IProps, ISt
return Array.isArray(localAliases) && localAliases.length !== 0;
} else {
const room = cli.getRoom(this.props.roomId);
const aliasEvents = room.currentState.getStateEvents("m.room.aliases") || [];
const aliasEvents = room.currentState.getStateEvents(EventType.RoomAliases) || [];
const hasAliases = !!aliasEvents.find((ev) => (ev.getContent().aliases || []).length > 0);
return hasAliases;
}
}
private renderRoomAccess() {
private editRestrictedRoomIds = async (): Promise<string[] | undefined> => {
let selected = this.state.restrictedAllowRoomIds;
if (!selected?.length && SpaceStore.instance.activeSpace) {
selected = [SpaceStore.instance.activeSpace.roomId];
}
const matrixClient = MatrixClientPeg.get();
const { finished } = Modal.createTrackedDialog('Edit restricted', '', ManageRestrictedJoinRuleDialog, {
matrixClient,
room: matrixClient.getRoom(this.props.roomId),
selected,
}, "mx_ManageRestrictedJoinRuleDialog_wrapper");
const [restrictedAllowRoomIds] = await finished;
return restrictedAllowRoomIds;
};
private onEditRestrictedClick = async () => {
const restrictedAllowRoomIds = await this.editRestrictedRoomIds();
if (!Array.isArray(restrictedAllowRoomIds)) return;
if (restrictedAllowRoomIds.length > 0) {
this.onRestrictedRoomIdsChange(restrictedAllowRoomIds);
} else {
this.onJoinRuleChange(JoinRule.Invite);
}
};
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("m.room.join_rules", client)
&& room.currentState.mayClientSendStateEvent("m.room.guest_access", client);
let guestWarning = null;
if (joinRule !== 'public' && 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 === 'public' && !this.state.hasAliases) {
if (joinRule === JoinRule.Public && !this.state.hasAliases) {
aliasWarning = (
<div className='mx_SecurityRoomSettingsTab_warning'>
<img src={require("../../../../../../res/img/warning.svg")} width={15} height={15} />
<span>
{_t("To link to this room, please add an address.")}
{ _t("To link to this room, please add an address.") }
</span>
</div>
);
}
const radioDefinitions: IDefinition<JoinRule>[] = [{
value: JoinRule.Invite,
label: _t("Private (invite only)"),
description: _t("Only invited people can join."),
checked: this.state.joinRule === JoinRule.Invite
|| (this.state.joinRule === JoinRule.Restricted && !this.state.restrictedAllowRoomIds?.length),
}, {
value: JoinRule.Public,
label: _t("Public"),
description: _t("Anyone can find and join."),
}];
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 && this.state.restrictedAllowRoomIds?.length) {
const shownSpaces = this.state.restrictedAllowRoomIds
.map(roomId => client.getRoom(roomId))
.filter(room => room?.isSpaceRoom())
.slice(0, 4);
let moreText;
if (shownSpaces.length < this.state.restrictedAllowRoomIds.length) {
if (shownSpaces.length > 0) {
moreText = _t("& %(count)s more", {
count: this.state.restrictedAllowRoomIds.length - shownSpaces.length,
});
} else {
moreText = _t("Currently, %(count)s spaces have access", {
count: this.state.restrictedAllowRoomIds.length,
});
}
}
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>
<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>;
}) }
{ moreText && <span>{ moreText }</span> }
</div>
</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,
// if there are 0 allowed spaces then render it as invite only instead
checked: this.state.joinRule === JoinRule.Restricted && !!this.state.restrictedAllowRoomIds?.length,
});
}
return (
<div>
{guestWarning}
{aliasWarning}
<div className="mx_SecurityRoomSettingsTab_joinRule">
<div className="mx_SettingsTab_subsectionText">
<span>{ _t("Decide who can join %(roomName)s.", {
roomName: client.getRoom(this.props.roomId)?.name,
}) }</span>
</div>
{ aliasWarning }
<StyledRadioGroup
name="roomVis"
name="joinRule"
value={joinRule}
onChange={this.onRoomAccessRadioToggle}
definitions={[
{
value: "invite_only",
disabled: !canChangeAccess,
label: _t('Only people who have been invited'),
checked: joinRule !== "public",
},
{
value: "public_no_guests",
disabled: !canChangeAccess,
label: _t('Anyone who knows the room\'s link, apart from guests'),
checked: joinRule === "public" && guestAccess !== "can_join",
},
{
value: "public_with_guests",
disabled: !canChangeAccess,
label: _t("Anyone who knows the room's link, including guests"),
checked: joinRule === "public" && guestAccess === "can_join",
},
]}
onChange={this.onJoinRuleChange}
definitions={radioDefinitions}
disabled={!canChangeJoinRule}
/>
</div>
);
@ -345,7 +441,7 @@ export default class SecurityRoomSettingsTab extends React.Component<IProps, ISt
const client = MatrixClientPeg.get();
const history = this.state.history;
const state = client.getRoom(this.props.roomId).currentState;
const canChangeHistory = state.mayClientSendStateEvent('m.room.history_visibility', client);
const canChangeHistory = state.mayClientSendStateEvent(EventType.RoomHistoryVisibility, client);
const options = [
{
@ -373,8 +469,8 @@ export default class SecurityRoomSettingsTab extends React.Component<IProps, ISt
return (
<div>
<div>
{_t('Changes to who can read history will only apply to future messages in this room. ' +
'The visibility of existing history will be unchanged.')}
{ _t('Changes to who can read history will only apply to future messages in this room. ' +
'The visibility of existing history will be unchanged.') }
</div>
<StyledRadioGroup
name="historyVis"
@ -387,11 +483,35 @@ 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 <div className="mx_SettingsTab_section">
<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>
</div>;
}
render() {
const client = MatrixClientPeg.get();
const room = client.getRoom(this.props.roomId);
const isEncrypted = this.state.encrypted;
const hasEncryptionPermission = room.currentState.mayClientSendStateEvent("m.room.encryption", client);
const hasEncryptionPermission = room.currentState.mayClientSendStateEvent(EventType.RoomEncryption, client);
const canEnableEncryption = !isEncrypted && hasEncryptionPermission;
let encryptionSettings = null;
@ -405,9 +525,9 @@ export default class SecurityRoomSettingsTab extends React.Component<IProps, ISt
}
let historySection = (<>
<span className='mx_SettingsTab_subheading'>{_t("Who can read history?")}</span>
<span className='mx_SettingsTab_subheading'>{ _t("Who can read history?") }</span>
<div className='mx_SettingsTab_section mx_SettingsTab_subsectionText'>
{this.renderHistory()}
{ this.renderHistory() }
</div>
</>);
if (!SettingsStore.getValue(UIFeature.RoomHistorySettings)) {
@ -416,27 +536,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,6 +37,8 @@ import StyledRadioGroup from "../../../elements/StyledRadioGroup";
import { SettingLevel } from "../../../../../settings/SettingLevel";
import { UIFeature } from "../../../../../settings/UIFeature";
import { Layout } from "../../../../../settings/Layout";
import classNames from 'classnames';
import StyledRadioButton from '../../../elements/StyledRadioButton';
import { replaceableComponent } from "../../../../../utils/replaceableComponent";
import { compare } from "../../../../../utils/strings";
@ -241,6 +243,19 @@ export default class AppearanceUserSettingsTab extends React.Component<IProps, I
this.setState({ customThemeUrl: e.target.value });
};
private onLayoutChange = (e: React.ChangeEvent<HTMLInputElement>): void => {
let layout;
switch (e.target.value) {
case "irc": layout = Layout.IRC; break;
case "group": layout = Layout.Group; break;
case "bubble": layout = Layout.Bubble; break;
}
this.setState({ layout: layout });
SettingsStore.setValue("layout", null, SettingLevel.DEVICE, layout);
};
private onIRCLayoutChange = (enabled: boolean) => {
if (enabled) {
this.setState({ layout: Layout.IRC });
@ -260,7 +275,7 @@ export default class AppearanceUserSettingsTab extends React.Component<IProps, I
checked={this.state.useSystemTheme}
onChange={(e) => this.onUseSystemThemeChanged(e.target.checked)}
>
{SettingsStore.getDisplayName("use_system_theme")}
{ SettingsStore.getDisplayName("use_system_theme") }
</StyledCheckbox>
</div>;
}
@ -270,9 +285,9 @@ export default class AppearanceUserSettingsTab extends React.Component<IProps, I
let messageElement = null;
if (this.state.customThemeMessage.text) {
if (this.state.customThemeMessage.isError) {
messageElement = <div className='text-error'>{this.state.customThemeMessage.text}</div>;
messageElement = <div className='text-error'>{ this.state.customThemeMessage.text }</div>;
} else {
messageElement = <div className='text-success'>{this.state.customThemeMessage.text}</div>;
messageElement = <div className='text-success'>{ this.state.customThemeMessage.text }</div>;
}
}
customThemeForm = (
@ -288,10 +303,13 @@ export default class AppearanceUserSettingsTab extends React.Component<IProps, I
/>
<AccessibleButton
onClick={this.onAddCustomTheme}
type="submit" kind="primary_sm"
type="submit"
kind="primary_sm"
disabled={!this.state.customThemeUrl.trim()}
>{_t("Add theme")}</AccessibleButton>
{messageElement}
>
{ _t("Add theme") }
</AccessibleButton>
{ messageElement }
</form>
</div>
);
@ -306,8 +324,8 @@ export default class AppearanceUserSettingsTab extends React.Component<IProps, I
const orderedThemes = [...builtInThemes, ...customThemes];
return (
<div className="mx_SettingsTab_section mx_AppearanceUserSettingsTab_themeSection">
<span className="mx_SettingsTab_subheading">{_t("Theme")}</span>
{systemThemeSection}
<span className="mx_SettingsTab_subheading">{ _t("Theme") }</span>
{ systemThemeSection }
<div className="mx_ThemeSelectors">
<StyledRadioGroup
name="theme"
@ -322,7 +340,7 @@ export default class AppearanceUserSettingsTab extends React.Component<IProps, I
outlined
/>
</div>
{customThemeForm}
{ customThemeForm }
</div>
);
}
@ -330,7 +348,7 @@ export default class AppearanceUserSettingsTab extends React.Component<IProps, I
private renderFontSection() {
return <div className="mx_SettingsTab_section mx_AppearanceUserSettingsTab_fontScaling">
<span className="mx_SettingsTab_subheading">{_t("Font size")}</span>
<span className="mx_SettingsTab_subheading">{ _t("Font size") }</span>
<EventTilePreview
className="mx_AppearanceUserSettingsTab_fontSlider_preview"
message={this.MESSAGE_PREVIEW_TEXT}
@ -373,6 +391,75 @@ export default class AppearanceUserSettingsTab extends React.Component<IProps, I
</div>;
}
private renderLayoutSection = () => {
return <div className="mx_SettingsTab_section mx_AppearanceUserSettingsTab_Layout">
<span className="mx_SettingsTab_subheading">{ _t("Message layout") }</span>
<div className="mx_AppearanceUserSettingsTab_Layout_RadioButtons">
<label className={classNames("mx_AppearanceUserSettingsTab_Layout_RadioButton", {
mx_AppearanceUserSettingsTab_Layout_RadioButton_selected: this.state.layout == Layout.IRC,
})}>
<EventTilePreview
className="mx_AppearanceUserSettingsTab_Layout_RadioButton_preview"
message={this.MESSAGE_PREVIEW_TEXT}
layout={Layout.IRC}
userId={this.state.userId}
displayName={this.state.displayName}
avatarUrl={this.state.avatarUrl}
/>
<StyledRadioButton
name="layout"
value="irc"
checked={this.state.layout === Layout.IRC}
onChange={this.onLayoutChange}
>
{ _t("IRC") }
</StyledRadioButton>
</label>
<label className={classNames("mx_AppearanceUserSettingsTab_Layout_RadioButton", {
mx_AppearanceUserSettingsTab_Layout_RadioButton_selected: this.state.layout == Layout.Group,
})}>
<EventTilePreview
className="mx_AppearanceUserSettingsTab_Layout_RadioButton_preview"
message={this.MESSAGE_PREVIEW_TEXT}
layout={Layout.Group}
userId={this.state.userId}
displayName={this.state.displayName}
avatarUrl={this.state.avatarUrl}
/>
<StyledRadioButton
name="layout"
value="group"
checked={this.state.layout == Layout.Group}
onChange={this.onLayoutChange}
>
{ _t("Modern") }
</StyledRadioButton>
</label>
<label className={classNames("mx_AppearanceUserSettingsTab_Layout_RadioButton", {
mx_AppearanceUserSettingsTab_Layout_RadioButton_selected: this.state.layout === Layout.Bubble,
})}>
<EventTilePreview
className="mx_AppearanceUserSettingsTab_Layout_RadioButton_preview"
message={this.MESSAGE_PREVIEW_TEXT}
layout={Layout.Bubble}
userId={this.state.userId}
displayName={this.state.displayName}
avatarUrl={this.state.avatarUrl}
/>
<StyledRadioButton
name="layout"
value="bubble"
checked={this.state.layout == Layout.Bubble}
onChange={this.onLayoutChange}
>
{ _t("Message bubbles") }
</StyledRadioButton>
</label>
</div>
</div>;
};
private renderAdvancedSection() {
if (!SettingsStore.getValue(UIFeature.AdvancedSettings)) return null;
@ -381,7 +468,7 @@ export default class AppearanceUserSettingsTab extends React.Component<IProps, I
className="mx_AppearanceUserSettingsTab_AdvancedToggle"
onClick={() => this.setState({ showAdvanced: !this.state.showAdvanced })}
>
{this.state.showAdvanced ? _t("Hide advanced") : _t("Show advanced")}
{ this.state.showAdvanced ? _t("Hide advanced") : _t("Show advanced") }
</div>;
let advanced: React.ReactNode;
@ -396,14 +483,17 @@ export default class AppearanceUserSettingsTab extends React.Component<IProps, I
name="useCompactLayout"
level={SettingLevel.DEVICE}
useCheckbox={true}
disabled={this.state.layout == Layout.IRC}
disabled={this.state.layout !== Layout.Group}
/>
<StyledCheckbox
checked={this.state.layout == Layout.IRC}
onChange={(ev) => this.onIRCLayoutChange(ev.target.checked)}
>
{_t("Enable experimental, compact IRC style layout")}
</StyledCheckbox>
{ !SettingsStore.getValue("feature_new_layout_switcher") ?
<StyledCheckbox
checked={this.state.layout == Layout.IRC}
onChange={(ev) => this.onIRCLayoutChange(ev.target.checked)}
>
{ _t("Enable experimental, compact IRC style layout") }
</StyledCheckbox> : null
}
<SettingsFlag
name="useSystemFont"
@ -429,8 +519,8 @@ export default class AppearanceUserSettingsTab extends React.Component<IProps, I
</>;
}
return <div className="mx_SettingsTab_section mx_AppearanceUserSettingsTab_Advanced">
{toggle}
{advanced}
{ toggle }
{ advanced }
</div>;
}
@ -439,13 +529,14 @@ export default class AppearanceUserSettingsTab extends React.Component<IProps, I
return (
<div className="mx_SettingsTab mx_AppearanceUserSettingsTab">
<div className="mx_SettingsTab_heading">{_t("Customise your appearance")}</div>
<div className="mx_SettingsTab_heading">{ _t("Customise your appearance") }</div>
<div className="mx_SettingsTab_SubHeading">
{_t("Appearance Settings only affect this %(brand)s session.", { brand })}
{ _t("Appearance Settings only affect this %(brand)s session.", { brand }) }
</div>
{this.renderThemeSection()}
{this.renderFontSection()}
{this.renderAdvancedSection()}
{ this.renderThemeSection() }
{ SettingsStore.getValue("feature_new_layout_switcher") ? this.renderLayoutSection() : null }
{ this.renderFontSection() }
{ this.renderAdvancedSection() }
</div>
);
}

View file

@ -24,7 +24,7 @@ export default class FlairUserSettingsTab extends React.Component {
render() {
return (
<div className="mx_SettingsTab">
<span className="mx_SettingsTab_heading">{_t("Flair")}</span>
<span className="mx_SettingsTab_heading">{ _t("Flair") }</span>
<div className="mx_SettingsTab_section">
<GroupUserSettings />
</div>

View file

@ -289,11 +289,11 @@ export default class GeneralUserSettingsTab extends React.Component {
onMsisdnsChange={this._onMsisdnsChange}
/>;
threepidSection = <div>
<span className="mx_SettingsTab_subheading">{_t("Email addresses")}</span>
{emails}
<span className="mx_SettingsTab_subheading">{ _t("Email addresses") }</span>
{ emails }
<span className="mx_SettingsTab_subheading">{_t("Phone numbers")}</span>
{msisdns}
<span className="mx_SettingsTab_subheading">{ _t("Phone numbers") }</span>
{ msisdns }
</div>;
} else if (this.state.serverSupportsSeparateAddAndBind === null) {
threepidSection = <Spinner />;
@ -308,12 +308,12 @@ export default class GeneralUserSettingsTab extends React.Component {
return (
<div className="mx_SettingsTab_section mx_GeneralUserSettingsTab_accountSection">
<span className="mx_SettingsTab_subheading">{_t("Account")}</span>
<span className="mx_SettingsTab_subheading">{ _t("Account") }</span>
<p className="mx_SettingsTab_subsectionText">
{passwordChangeText}
{ passwordChangeText }
</p>
{passwordChangeForm}
{threepidSection}
{ passwordChangeForm }
{ threepidSection }
</div>
);
}
@ -322,7 +322,7 @@ export default class GeneralUserSettingsTab extends React.Component {
// TODO: Convert to new-styled Field
return (
<div className="mx_SettingsTab_section">
<span className="mx_SettingsTab_subheading">{_t("Language and region")}</span>
<span className="mx_SettingsTab_subheading">{ _t("Language and region") }</span>
<LanguageDropdown
className="mx_GeneralUserSettingsTab_languageInput"
onOptionChange={this._onLanguageChange}
@ -335,7 +335,7 @@ export default class GeneralUserSettingsTab extends React.Component {
_renderSpellCheckSection() {
return (
<div className="mx_SettingsTab_section">
<span className="mx_SettingsTab_subheading">{_t("Spell check dictionaries")}</span>
<span className="mx_SettingsTab_subheading">{ _t("Spell check dictionaries") }</span>
<SpellCheckSettings
languages={this.state.spellCheckLanguages}
onLanguagesChange={this._onSpellCheckLanguagesChange}
@ -350,11 +350,11 @@ export default class GeneralUserSettingsTab extends React.Component {
if (this.state.requiredPolicyInfo.hasTerms) {
const InlineTermsAgreement = sdk.getComponent("views.terms.InlineTermsAgreement");
const intro = <span className="mx_SettingsTab_subsectionText">
{_t(
{ _t(
"Agree to the identity server (%(serverName)s) Terms of Service to " +
"allow yourself to be discoverable by email address or phone number.",
{ serverName: this.state.idServerName },
)}
) }
</span>;
return (
<div>
@ -377,16 +377,16 @@ export default class GeneralUserSettingsTab extends React.Component {
const msisdns = this.state.loading3pids ? <Spinner /> : <PhoneNumbers msisdns={this.state.msisdns} />;
const threepidSection = this.state.haveIdServer ? <div className='mx_GeneralUserSettingsTab_discovery'>
<span className="mx_SettingsTab_subheading">{_t("Email addresses")}</span>
{emails}
<span className="mx_SettingsTab_subheading">{ _t("Email addresses") }</span>
{ emails }
<span className="mx_SettingsTab_subheading">{_t("Phone numbers")}</span>
{msisdns}
<span className="mx_SettingsTab_subheading">{ _t("Phone numbers") }</span>
{ msisdns }
</div> : null;
return (
<div className="mx_SettingsTab_section">
{threepidSection}
{ threepidSection }
{ /* has its own heading as it includes the current identity server */ }
<SetIdServer />
</div>
@ -397,12 +397,12 @@ export default class GeneralUserSettingsTab extends React.Component {
// TODO: Improve warning text for account deactivation
return (
<div className="mx_SettingsTab_section">
<span className="mx_SettingsTab_subheading">{_t("Account management")}</span>
<span className="mx_SettingsTab_subheading">{ _t("Account management") }</span>
<span className="mx_SettingsTab_subsectionText">
{_t("Deactivating your account is a permanent action - be careful!")}
{ _t("Deactivating your account is a permanent action - be careful!") }
</span>
<AccessibleButton onClick={this._onDeactivateClicked} kind="danger">
{_t("Deactivate Account")}
{ _t("Deactivate Account") }
</AccessibleButton>
</div>
);
@ -426,36 +426,40 @@ export default class GeneralUserSettingsTab extends React.Component {
const supportsMultiLanguageSpellCheck = plaf.supportsMultiLanguageSpellCheck();
const discoWarning = this.state.requiredPolicyInfo.hasTerms
? <img className='mx_GeneralUserSettingsTab_warningIcon'
? <img
className='mx_GeneralUserSettingsTab_warningIcon'
src={require("../../../../../../res/img/feather-customised/warning-triangle.svg")}
width="18" height="18" alt={_t("Warning")} />
width="18"
height="18"
alt={_t("Warning")}
/>
: null;
let accountManagementSection;
if (SettingsStore.getValue(UIFeature.Deactivate)) {
accountManagementSection = <>
<div className="mx_SettingsTab_heading">{_t("Deactivate account")}</div>
{this._renderManagementSection()}
<div className="mx_SettingsTab_heading">{ _t("Deactivate account") }</div>
{ this._renderManagementSection() }
</>;
}
let discoverySection;
if (SettingsStore.getValue(UIFeature.IdentityServer)) {
discoverySection = <>
<div className="mx_SettingsTab_heading">{discoWarning} {_t("Discovery")}</div>
{this._renderDiscoverySection()}
<div className="mx_SettingsTab_heading">{ discoWarning } { _t("Discovery") }</div>
{ this._renderDiscoverySection() }
</>;
}
return (
<div className="mx_SettingsTab">
<div className="mx_SettingsTab_heading">{_t("General")}</div>
{this._renderProfileSection()}
{this._renderAccountSection()}
{this._renderLanguageSection()}
{supportsMultiLanguageSpellCheck ? this._renderSpellCheckSection() : null}
<div className="mx_SettingsTab_heading">{ _t("General") }</div>
{ this._renderProfileSection() }
{ this._renderAccountSection() }
{ this._renderLanguageSection() }
{ supportsMultiLanguageSpellCheck ? this._renderSpellCheckSection() : null }
{ discoverySection }
{this._renderIntegrationManagerSection() /* Has its own title */}
{ this._renderIntegrationManagerSection() /* Has its own title */ }
{ accountManagementSection }
</div>
);

View file

@ -112,15 +112,15 @@ export default class HelpUserSettingsTab extends React.Component<IProps, IState>
const legalLinks = [];
for (const tocEntry of SdkConfig.get().terms_and_conditions_links) {
legalLinks.push(<div key={tocEntry.url}>
<a href={tocEntry.url} rel="noreferrer noopener" target="_blank">{tocEntry.text}</a>
<a href={tocEntry.url} rel="noreferrer noopener" target="_blank">{ tocEntry.text }</a>
</div>);
}
return (
<div className='mx_SettingsTab_section mx_HelpUserSettingsTab_versions'>
<span className='mx_SettingsTab_subheading'>{_t("Legal")}</span>
<span className='mx_SettingsTab_subheading'>{ _t("Legal") }</span>
<div className='mx_SettingsTab_subsectionText'>
{legalLinks}
{ legalLinks }
</div>
</div>
);
@ -131,31 +131,42 @@ export default class HelpUserSettingsTab extends React.Component<IProps, IState>
// Also, &nbsp; is ugly but necessary.
return (
<div className='mx_SettingsTab_section'>
<span className='mx_SettingsTab_subheading'>{_t("Credits")}</span>
<span className='mx_SettingsTab_subheading'>{ _t("Credits") }</span>
<ul>
<li>
The <a href="themes/element/img/backgrounds/lake.jpg" rel="noreferrer noopener"
target="_blank">default cover photo</a> is ©&nbsp;
<a href="https://www.flickr.com/golan" rel="noreferrer noopener"
target="_blank">Jesús Roncero</a> used under the terms of&nbsp;
<a href="https://creativecommons.org/licenses/by-sa/4.0/" rel="noreferrer noopener"
target="_blank">CC-BY-SA 4.0</a>.
The <a href="themes/element/img/backgrounds/lake.jpg" rel="noreferrer noopener" target="_blank">
default cover photo
</a> is ©&nbsp;
<a href="https://www.flickr.com/golan" rel="noreferrer noopener" target="_blank">
Jesús Roncero
</a> used under the terms of&nbsp;
<a href="https://creativecommons.org/licenses/by-sa/4.0/" rel="noreferrer noopener" target="_blank">
CC-BY-SA 4.0
</a>.
</li>
<li>
The <a href="https://github.com/matrix-org/twemoji-colr" rel="noreferrer noopener"
target="_blank">twemoji-colr</a> font is ©&nbsp;
<a href="https://mozilla.org" rel="noreferrer noopener"
target="_blank">Mozilla Foundation</a> used under the terms of&nbsp;
<a href="http://www.apache.org/licenses/LICENSE-2.0" rel="noreferrer noopener"
target="_blank">Apache 2.0</a>.
The <a
href="https://github.com/matrix-org/twemoji-colr"
rel="noreferrer noopener"
target="_blank"
>
twemoji-colr
</a> font is ©&nbsp;
<a href="https://mozilla.org" rel="noreferrer noopener" target="_blank">
Mozilla Foundation
</a> used under the terms of&nbsp;
<a href="http://www.apache.org/licenses/LICENSE-2.0" rel="noreferrer noopener" target="_blank">Apache 2.0</a>.
</li>
<li>
The <a href="https://twemoji.twitter.com/" rel="noreferrer noopener"
target="_blank">Twemoji</a> emoji art is ©&nbsp;
<a href="https://twemoji.twitter.com/" rel="noreferrer noopener"
target="_blank">Twitter, Inc and other contributors</a> used under the terms of&nbsp;
<a href="https://creativecommons.org/licenses/by/4.0/" rel="noreferrer noopener"
target="_blank">CC-BY 4.0</a>.
The <a href="https://twemoji.twitter.com/" rel="noreferrer noopener" target="_blank">
Twemoji
</a> emoji art is ©&nbsp;
<a href="https://twemoji.twitter.com/" rel="noreferrer noopener" target="_blank">
Twitter, Inc and other contributors
</a> used under the terms of&nbsp;
<a href="https://creativecommons.org/licenses/by/4.0/" rel="noreferrer noopener" target="_blank">
CC-BY 4.0
</a>.
</li>
</ul>
</div>
@ -189,14 +200,14 @@ export default class HelpUserSettingsTab extends React.Component<IProps, IState>
rel="noreferrer noopener"
target="_blank"
>
{sub}
{ sub }
</a>,
},
);
if (SdkConfig.get().welcomeUserId && getCurrentLanguage().startsWith('en')) {
faqText = (
<div>
{_t(
{ _t(
'For help with using %(brand)s, click <a>here</a> or start a chat with our ' +
'bot using the button below.',
{
@ -208,13 +219,13 @@ export default class HelpUserSettingsTab extends React.Component<IProps, IState>
rel='noreferrer noopener'
target='_blank'
>
{sub}
{ sub }
</a>,
},
)}
) }
<div>
<AccessibleButton onClick={this.onStartBotChat} kind='primary'>
{_t("Chat with %(brand)s Bot", { brand })}
{ _t("Chat with %(brand)s Bot", { brand }) }
</AccessibleButton>
</div>
</div>
@ -235,29 +246,30 @@ export default class HelpUserSettingsTab extends React.Component<IProps, IState>
if (SdkConfig.get().bug_report_endpoint_url) {
bugReportingSection = (
<div className="mx_SettingsTab_section">
<span className='mx_SettingsTab_subheading'>{_t('Bug reporting')}</span>
<span className='mx_SettingsTab_subheading'>{ _t('Bug reporting') }</span>
<div className='mx_SettingsTab_subsectionText'>
{_t(
{ _t(
"If you've submitted a bug via GitHub, debug logs can help " +
"us track down the problem. Debug logs contain application " +
"usage data including your username, the IDs or aliases of " +
"the rooms or groups you have visited and the usernames of " +
"other users. They do not contain messages.",
)}
) }
<div className='mx_HelpUserSettingsTab_debugButton'>
<AccessibleButton onClick={this.onBugReport} kind='primary'>
{_t("Submit debug logs")}
{ _t("Submit debug logs") }
</AccessibleButton>
</div>
{_t(
{ _t(
"To report a Matrix-related security issue, please read the Matrix.org " +
"<a>Security Disclosure Policy</a>.", {},
{
a: sub => <a href="https://matrix.org/security-disclosure-policy/"
rel="noreferrer noopener" target="_blank"
>{sub}</a>,
rel="noreferrer noopener"
target="_blank"
>{ sub }</a>,
},
)}
) }
</div>
</div>
);
@ -265,39 +277,39 @@ export default class HelpUserSettingsTab extends React.Component<IProps, IState>
return (
<div className="mx_SettingsTab mx_HelpUserSettingsTab">
<div className="mx_SettingsTab_heading">{_t("Help & About")}</div>
<div className="mx_SettingsTab_heading">{ _t("Help & About") }</div>
{ bugReportingSection }
<div className='mx_SettingsTab_section'>
<span className='mx_SettingsTab_subheading'>{_t("FAQ")}</span>
<span className='mx_SettingsTab_subheading'>{ _t("FAQ") }</span>
<div className='mx_SettingsTab_subsectionText'>
{faqText}
{ faqText }
</div>
<AccessibleButton kind="primary" onClick={KeyboardShortcuts.toggleDialog}>
{ _t("Keyboard Shortcuts") }
</AccessibleButton>
</div>
<div className='mx_SettingsTab_section mx_HelpUserSettingsTab_versions'>
<span className='mx_SettingsTab_subheading'>{_t("Versions")}</span>
<span className='mx_SettingsTab_subheading'>{ _t("Versions") }</span>
<div className='mx_SettingsTab_subsectionText'>
{_t("%(brand)s version:", { brand })} {appVersion}<br />
{_t("olm version:")} {olmVersion}<br />
{updateButton}
{ _t("%(brand)s version:", { brand }) } { appVersion }<br />
{ _t("olm version:") } { olmVersion }<br />
{ updateButton }
</div>
</div>
{this.renderLegal()}
{this.renderCredits()}
{ this.renderLegal() }
{ this.renderCredits() }
<div className='mx_SettingsTab_section mx_HelpUserSettingsTab_versions'>
<span className='mx_SettingsTab_subheading'>{_t("Advanced")}</span>
<span className='mx_SettingsTab_subheading'>{ _t("Advanced") }</span>
<div className='mx_SettingsTab_subsectionText'>
{_t("Homeserver is")} <code>{MatrixClientPeg.get().getHomeserverUrl()}</code><br />
{_t("Identity server is")} <code>{MatrixClientPeg.get().getIdentityServerUrl()}</code><br />
{ _t("Homeserver is") } <code>{ MatrixClientPeg.get().getHomeserverUrl() }</code><br />
{ _t("Identity server is") } <code>{ MatrixClientPeg.get().getIdentityServerUrl() }</code><br />
<br />
<details>
<summary>{_t("Access Token")}</summary><br />
<b>{_t("Your access token gives full access to your account."
+ " Do not share it with anyone." )}</b>
<summary>{ _t("Access Token") }</summary><br />
<b>{ _t("Your access token gives full access to your account."
+ " Do not share it with anyone." ) }</b>
<div className="mx_HelpUserSettingsTab_accessToken">
<code>{MatrixClientPeg.get().getAccessToken()}</code>
<code>{ MatrixClientPeg.get().getAccessToken() }</code>
<AccessibleTooltipButton
title={_t("Copy")}
onClick={this.onAccessTokenCopyClick}
@ -307,7 +319,7 @@ export default class HelpUserSettingsTab extends React.Component<IProps, IState>
</details><br />
<div className='mx_HelpUserSettingsTab_debugButton'>
<AccessibleButton onClick={this.onClearCacheAndReload} kind='danger'>
{_t("Clear cache and reload")}
{ _t("Clear cache and reload") }
</AccessibleButton>
</div>
</div>

View file

@ -95,15 +95,18 @@ export default class LabsUserSettingsTab extends React.Component {
return (
<div className="mx_SettingsTab mx_LabsUserSettingsTab">
<div className="mx_SettingsTab_heading">{_t("Labs")}</div>
<div className="mx_SettingsTab_heading">{ _t("Labs") }</div>
<div className='mx_SettingsTab_subsectionText'>
{
_t('Feeling experimental? Labs are the best way to get things early, ' +
'test out new features and help shape them before they actually launch. ' +
'<a>Learn more</a>.', {}, {
'a': (sub) => {
return <a href="https://github.com/vector-im/element-web/blob/develop/docs/labs.md"
rel='noreferrer noopener' target='_blank'>{sub}</a>;
return <a
href="https://github.com/vector-im/element-web/blob/develop/docs/labs.md"
rel='noreferrer noopener'
target='_blank'
>{ sub }</a>;
},
})
}

View file

@ -140,23 +140,23 @@ export default class MjolnirUserSettingsTab extends React.Component<{}, IState>
const name = room ? room.name : list.roomId;
const renderRules = (rules: ListRule[]) => {
if (rules.length === 0) return <i>{_t("None")}</i>;
if (rules.length === 0) return <i>{ _t("None") }</i>;
const tiles = [];
for (const rule of rules) {
tiles.push(<li key={rule.kind + rule.entity}><code>{rule.entity}</code></li>);
tiles.push(<li key={rule.kind + rule.entity}><code>{ rule.entity }</code></li>);
}
return <ul>{tiles}</ul>;
return <ul>{ tiles }</ul>;
};
Modal.createTrackedDialog('View Mjolnir list rules', '', QuestionDialog, {
title: _t("Ban list rules - %(roomName)s", { roomName: name }),
description: (
<div>
<h3>{_t("Server rules")}</h3>
{renderRules(list.serverRules)}
<h3>{_t("User rules")}</h3>
{renderRules(list.userRules)}
<h3>{ _t("Server rules") }</h3>
{ renderRules(list.serverRules) }
<h3>{ _t("User rules") }</h3>
{ renderRules(list.userRules) }
</div>
),
button: _t("Close"),
@ -167,7 +167,7 @@ export default class MjolnirUserSettingsTab extends React.Component<{}, IState>
private renderPersonalBanListRules() {
const list = Mjolnir.sharedInstance().getPersonalList();
const rules = list ? [...list.userRules, ...list.serverRules] : [];
if (!list || rules.length <= 0) return <i>{_t("You have not ignored anyone.")}</i>;
if (!list || rules.length <= 0) return <i>{ _t("You have not ignored anyone.") }</i>;
const tiles = [];
for (const rule of rules) {
@ -178,17 +178,17 @@ export default class MjolnirUserSettingsTab extends React.Component<{}, IState>
onClick={() => this.removePersonalRule(rule)}
disabled={this.state.busy}
>
{_t("Remove")}
{ _t("Remove") }
</AccessibleButton>&nbsp;
<code>{rule.entity}</code>
<code>{ rule.entity }</code>
</li>,
);
}
return (
<div>
<p>{_t("You are currently ignoring:")}</p>
<ul>{tiles}</ul>
<p>{ _t("You are currently ignoring:") }</p>
<ul>{ tiles }</ul>
</div>
);
}
@ -198,12 +198,12 @@ export default class MjolnirUserSettingsTab extends React.Component<{}, IState>
const lists = Mjolnir.sharedInstance().lists.filter(b => {
return personalList? personalList.roomId !== b.roomId : true;
});
if (!lists || lists.length <= 0) return <i>{_t("You are not subscribed to any lists")}</i>;
if (!lists || lists.length <= 0) return <i>{ _t("You are not subscribed to any lists") }</i>;
const tiles = [];
for (const list of lists) {
const room = MatrixClientPeg.get().getRoom(list.roomId);
const name = room ? <span>{room.name} (<code>{list.roomId}</code>)</span> : <code>list.roomId</code>;
const name = room ? <span>{ room.name } (<code>{ list.roomId }</code>)</span> : <code>list.roomId</code>;
tiles.push(
<li key={list.roomId} className="mx_MjolnirUserSettingsTab_listItem">
<AccessibleButton
@ -211,24 +211,24 @@ export default class MjolnirUserSettingsTab extends React.Component<{}, IState>
onClick={() => this.unsubscribeFromList(list)}
disabled={this.state.busy}
>
{_t("Unsubscribe")}
{ _t("Unsubscribe") }
</AccessibleButton>&nbsp;
<AccessibleButton
kind="primary_sm"
onClick={() => this.viewListRules(list)}
disabled={this.state.busy}
>
{_t("View rules")}
{ _t("View rules") }
</AccessibleButton>&nbsp;
{name}
{ name }
</li>,
);
}
return (
<div>
<p>{_t("You are currently subscribed to:")}</p>
<ul>{tiles}</ul>
<p>{ _t("You are currently subscribed to:") }</p>
<ul>{ tiles }</ul>
</div>
);
}
@ -238,37 +238,37 @@ export default class MjolnirUserSettingsTab extends React.Component<{}, IState>
return (
<div className="mx_SettingsTab mx_MjolnirUserSettingsTab">
<div className="mx_SettingsTab_heading">{_t("Ignored users")}</div>
<div className="mx_SettingsTab_heading">{ _t("Ignored users") }</div>
<div className="mx_SettingsTab_section">
<div className='mx_SettingsTab_subsectionText'>
<span className='warning'>{_t("⚠ These settings are meant for advanced users.")}</span><br />
<span className='warning'>{ _t("⚠ These settings are meant for advanced users.") }</span><br />
<br />
{_t(
{ _t(
"Add users and servers you want to ignore here. Use asterisks " +
"to have %(brand)s match any characters. For example, <code>@bot:*</code> " +
"would ignore all users that have the name 'bot' on any server.",
{ brand }, { code: (s) => <code>{s}</code> },
)}<br />
{ brand }, { code: (s) => <code>{ s }</code> },
) }<br />
<br />
{_t(
{ _t(
"Ignoring people is done through ban lists which contain rules for " +
"who to ban. Subscribing to a ban list means the users/servers blocked by " +
"that list will be hidden from you.",
)}
) }
</div>
</div>
<div className="mx_SettingsTab_section">
<span className="mx_SettingsTab_subheading">{_t("Personal ban list")}</span>
<span className="mx_SettingsTab_subheading">{ _t("Personal ban list") }</span>
<div className='mx_SettingsTab_subsectionText'>
{_t(
{ _t(
"Your personal ban list holds all the users/servers you personally don't " +
"want to see messages from. After ignoring your first user/server, a new room " +
"will show up in your room list named 'My Ban List' - stay in this room to keep " +
"the ban list in effect.",
)}
) }
</div>
<div>
{this.renderPersonalBanListRules()}
{ this.renderPersonalBanListRules() }
</div>
<div>
<form onSubmit={this.onAddPersonalRule} autoComplete="off">
@ -285,22 +285,22 @@ export default class MjolnirUserSettingsTab extends React.Component<{}, IState>
onClick={this.onAddPersonalRule}
disabled={this.state.busy}
>
{_t("Ignore")}
{ _t("Ignore") }
</AccessibleButton>
</form>
</div>
</div>
<div className="mx_SettingsTab_section">
<span className="mx_SettingsTab_subheading">{_t("Subscribed lists")}</span>
<span className="mx_SettingsTab_subheading">{ _t("Subscribed lists") }</span>
<div className='mx_SettingsTab_subsectionText'>
<span className='warning'>{_t("Subscribing to a ban list will cause you to join it!")}</span>
<span className='warning'>{ _t("Subscribing to a ban list will cause you to join it!") }</span>
&nbsp;
<span>{_t(
<span>{ _t(
"If this isn't what you want, please use a different tool to ignore users.",
)}</span>
) }</span>
</div>
<div>
{this.renderSubscribedBanLists()}
{ this.renderSubscribedBanLists() }
</div>
<div>
<form onSubmit={this.onSubscribeList} autoComplete="off">
@ -316,7 +316,7 @@ export default class MjolnirUserSettingsTab extends React.Component<{}, IState>
onClick={this.onSubscribeList}
disabled={this.state.busy}
>
{_t("Subscribe")}
{ _t("Subscribe") }
</AccessibleButton>
</form>
</div>

View file

@ -1,5 +1,5 @@
/*
Copyright 2019 New Vector Ltd
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.
@ -16,20 +16,15 @@ limitations under the License.
import React from 'react';
import { _t } from "../../../../../languageHandler";
import * as sdk from "../../../../../index";
import { replaceableComponent } from "../../../../../utils/replaceableComponent";
import Notifications from "../../Notifications";
@replaceableComponent("views.settings.tabs.user.NotificationUserSettingsTab")
export default class NotificationUserSettingsTab extends React.Component {
constructor() {
super();
}
render() {
const Notifications = sdk.getComponent("views.settings.Notifications");
return (
<div className="mx_SettingsTab mx_NotificationUserSettingsTab">
<div className="mx_SettingsTab_heading">{_t("Notifications")}</div>
<div className="mx_SettingsTab_heading">{ _t("Notifications") }</div>
<div className="mx_SettingsTab_section mx_SettingsTab_subsectionText">
<Notifications />
</div>

View file

@ -224,53 +224,53 @@ export default class PreferencesUserSettingsTab extends React.Component<{}, ISta
return (
<div className="mx_SettingsTab mx_PreferencesUserSettingsTab">
<div className="mx_SettingsTab_heading">{_t("Preferences")}</div>
<div className="mx_SettingsTab_heading">{ _t("Preferences") }</div>
<div className="mx_SettingsTab_section">
<span className="mx_SettingsTab_subheading">{_t("Room list")}</span>
{this.renderGroup(PreferencesUserSettingsTab.ROOM_LIST_SETTINGS)}
<span className="mx_SettingsTab_subheading">{ _t("Room list") }</span>
{ this.renderGroup(PreferencesUserSettingsTab.ROOM_LIST_SETTINGS) }
</div>
<div className="mx_SettingsTab_section">
<span className="mx_SettingsTab_subheading">{_t("Keyboard shortcuts")}</span>
<span className="mx_SettingsTab_subheading">{ _t("Keyboard shortcuts") }</span>
<AccessibleButton className="mx_SettingsFlag" onClick={KeyboardShortcuts.toggleDialog}>
{ _t("To view all keyboard shortcuts, click here.") }
</AccessibleButton>
{this.renderGroup(PreferencesUserSettingsTab.KEYBINDINGS_SETTINGS)}
{ this.renderGroup(PreferencesUserSettingsTab.KEYBINDINGS_SETTINGS) }
</div>
<div className="mx_SettingsTab_section">
<span className="mx_SettingsTab_subheading">{_t("Displaying time")}</span>
{this.renderGroup(PreferencesUserSettingsTab.TIME_SETTINGS)}
<span className="mx_SettingsTab_subheading">{ _t("Displaying time") }</span>
{ this.renderGroup(PreferencesUserSettingsTab.TIME_SETTINGS) }
</div>
<div className="mx_SettingsTab_section">
<span className="mx_SettingsTab_subheading">{_t("Composer")}</span>
{this.renderGroup(PreferencesUserSettingsTab.COMPOSER_SETTINGS)}
<span className="mx_SettingsTab_subheading">{ _t("Composer") }</span>
{ this.renderGroup(PreferencesUserSettingsTab.COMPOSER_SETTINGS) }
</div>
<div className="mx_SettingsTab_section">
<span className="mx_SettingsTab_subheading">{_t("Code blocks")}</span>
{this.renderGroup(PreferencesUserSettingsTab.CODE_BLOCKS_SETTINGS)}
<span className="mx_SettingsTab_subheading">{ _t("Code blocks") }</span>
{ this.renderGroup(PreferencesUserSettingsTab.CODE_BLOCKS_SETTINGS) }
</div>
<div className="mx_SettingsTab_section">
<span className="mx_SettingsTab_subheading">{_t("Images, GIFs and videos")}</span>
{this.renderGroup(PreferencesUserSettingsTab.IMAGES_AND_VIDEOS_SETTINGS)}
<span className="mx_SettingsTab_subheading">{ _t("Images, GIFs and videos") }</span>
{ this.renderGroup(PreferencesUserSettingsTab.IMAGES_AND_VIDEOS_SETTINGS) }
</div>
<div className="mx_SettingsTab_section">
<span className="mx_SettingsTab_subheading">{_t("Timeline")}</span>
{this.renderGroup(PreferencesUserSettingsTab.TIMELINE_SETTINGS)}
<span className="mx_SettingsTab_subheading">{ _t("Timeline") }</span>
{ this.renderGroup(PreferencesUserSettingsTab.TIMELINE_SETTINGS) }
</div>
<div className="mx_SettingsTab_section">
<span className="mx_SettingsTab_subheading">{_t("General")}</span>
{this.renderGroup(PreferencesUserSettingsTab.GENERAL_SETTINGS)}
{minimizeToTrayOption}
{autoHideMenuOption}
{autoLaunchOption}
{warnBeforeExitOption}
<span className="mx_SettingsTab_subheading">{ _t("General") }</span>
{ this.renderGroup(PreferencesUserSettingsTab.GENERAL_SETTINGS) }
{ minimizeToTrayOption }
{ autoHideMenuOption }
{ autoLaunchOption }
{ warnBeforeExitOption }
<Field
label={_t('Autocomplete delay (ms)')}
type='number'

View file

@ -215,10 +215,10 @@ export default class SecurityUserSettingsTab extends React.Component {
importExportButtons = (
<div className='mx_SecurityUserSettingsTab_importExportButtons'>
<AccessibleButton kind='primary' onClick={this._onExportE2eKeysClicked}>
{_t("Export E2E room keys")}
{ _t("Export E2E room keys") }
</AccessibleButton>
<AccessibleButton kind='primary' onClick={this._onImportE2eKeysClicked}>
{_t("Import E2E room keys")}
{ _t("Import E2E room keys") }
</AccessibleButton>
</div>
);
@ -235,19 +235,19 @@ export default class SecurityUserSettingsTab extends React.Component {
return (
<div className='mx_SettingsTab_section'>
<span className='mx_SettingsTab_subheading'>{_t("Cryptography")}</span>
<span className='mx_SettingsTab_subheading'>{ _t("Cryptography") }</span>
<ul className='mx_SettingsTab_subsectionText mx_SecurityUserSettingsTab_deviceInfo'>
<li>
<label>{_t("Session ID:")}</label>
<span><code>{deviceId}</code></span>
<label>{ _t("Session ID:") }</label>
<span><code>{ deviceId }</code></span>
</li>
<li>
<label>{_t("Session key:")}</label>
<span><code><b>{identityKey}</b></code></span>
<label>{ _t("Session key:") }</label>
<span><code><b>{ identityKey }</b></code></span>
</li>
</ul>
{importExportButtons}
{noSendUnverifiedSetting}
{ importExportButtons }
{ noSendUnverifiedSetting }
</div>
);
}
@ -270,9 +270,9 @@ export default class SecurityUserSettingsTab extends React.Component {
return (
<div className='mx_SettingsTab_section'>
<span className='mx_SettingsTab_subheading'>{_t('Ignored users')}</span>
<span className='mx_SettingsTab_subheading'>{ _t('Ignored users') }</span>
<div className='mx_SettingsTab_subsectionText'>
{userIds}
{ userIds }
</div>
</div>
);
@ -289,14 +289,14 @@ export default class SecurityUserSettingsTab extends React.Component {
const onClickReject = this._onRejectAllInvitesClicked.bind(this, invitedRooms);
return (
<div className='mx_SettingsTab_section mx_SecurityUserSettingsTab_bulkOptions'>
<span className='mx_SettingsTab_subheading'>{_t('Bulk options')}</span>
<span className='mx_SettingsTab_subheading'>{ _t('Bulk options') }</span>
<AccessibleButton onClick={onClickAccept} kind='primary' disabled={this.state.managingInvites}>
{_t("Accept all %(invitedRooms)s invites", { invitedRooms: this.state.invitedRoomAmt })}
{ _t("Accept all %(invitedRooms)s invites", { invitedRooms: this.state.invitedRoomAmt }) }
</AccessibleButton>
<AccessibleButton onClick={onClickReject} kind='danger' disabled={this.state.managingInvites}>
{_t("Reject all %(invitedRooms)s invites", { invitedRooms: this.state.invitedRoomAmt })}
{ _t("Reject all %(invitedRooms)s invites", { invitedRooms: this.state.invitedRoomAmt }) }
</AccessibleButton>
{this.state.managingInvites ? <InlineSpinner /> : <div />}
{ this.state.managingInvites ? <InlineSpinner /> : <div /> }
</div>
);
}
@ -309,7 +309,7 @@ export default class SecurityUserSettingsTab extends React.Component {
const secureBackup = (
<div className='mx_SettingsTab_section'>
<span className="mx_SettingsTab_subheading">{_t("Secure Backup")}</span>
<span className="mx_SettingsTab_subheading">{ _t("Secure Backup") }</span>
<div className='mx_SettingsTab_subsectionText'>
<SecureBackupPanel />
</div>
@ -318,7 +318,7 @@ export default class SecurityUserSettingsTab extends React.Component {
const eventIndex = (
<div className="mx_SettingsTab_section">
<span className="mx_SettingsTab_subheading">{_t("Message search")}</span>
<span className="mx_SettingsTab_subheading">{ _t("Message search") }</span>
<EventIndexPanel />
</div>
);
@ -330,7 +330,7 @@ export default class SecurityUserSettingsTab extends React.Component {
const CrossSigningPanel = sdk.getComponent('views.settings.CrossSigningPanel');
const crossSigning = (
<div className='mx_SettingsTab_section'>
<span className="mx_SettingsTab_subheading">{_t("Cross-signing")}</span>
<span className="mx_SettingsTab_subheading">{ _t("Cross-signing") }</span>
<div className='mx_SettingsTab_subsectionText'>
<CrossSigningPanel />
</div>
@ -348,19 +348,19 @@ export default class SecurityUserSettingsTab extends React.Component {
let privacySection;
if (Analytics.canEnable() || CountlyAnalytics.instance.canEnable()) {
privacySection = <React.Fragment>
<div className="mx_SettingsTab_heading">{_t("Privacy")}</div>
<div className="mx_SettingsTab_heading">{ _t("Privacy") }</div>
<div className="mx_SettingsTab_section">
<span className="mx_SettingsTab_subheading">{_t("Analytics")}</span>
<span className="mx_SettingsTab_subheading">{ _t("Analytics") }</span>
<div className="mx_SettingsTab_subsectionText">
{_t(
{ _t(
"%(brand)s collects anonymous analytics to allow us to improve the application.",
{ brand },
)}
) }
&nbsp;
{_t("Privacy is important to us, so we don't collect any personal or " +
"identifiable data for our analytics.")}
{ _t("Privacy is important to us, so we don't collect any personal or " +
"identifiable data for our analytics.") }
<AccessibleButton className="mx_SettingsTab_linkBtn" onClick={Analytics.showDetailsModal}>
{_t("Learn more about how we use analytics.")}
{ _t("Learn more about how we use analytics.") }
</AccessibleButton>
</div>
<SettingsFlag name="analyticsOptIn" level={SettingLevel.DEVICE} onChange={this._updateAnalytics} />
@ -377,11 +377,11 @@ export default class SecurityUserSettingsTab extends React.Component {
// only show the section if there's something to show
if (ignoreUsersPanel || invitesPanel || e2ePanel) {
advancedSection = <>
<div className="mx_SettingsTab_heading">{_t("Advanced")}</div>
<div className="mx_SettingsTab_heading">{ _t("Advanced") }</div>
<div className="mx_SettingsTab_section">
{ignoreUsersPanel}
{invitesPanel}
{e2ePanel}
{ ignoreUsersPanel }
{ invitesPanel }
{ e2ePanel }
</div>
</>;
}
@ -389,31 +389,31 @@ export default class SecurityUserSettingsTab extends React.Component {
return (
<div className="mx_SettingsTab mx_SecurityUserSettingsTab">
{warning}
<div className="mx_SettingsTab_heading">{_t("Where youre logged in")}</div>
{ warning }
<div className="mx_SettingsTab_heading">{ _t("Where youre logged in") }</div>
<div className="mx_SettingsTab_section">
<span>
{_t(
{ _t(
"Manage the names of and sign out of your sessions below or " +
"<a>verify them in your User Profile</a>.", {},
{
a: sub => <AccessibleButton kind="link" onClick={this._onGoToUserProfileClick}>
{sub}
{ sub }
</AccessibleButton>,
},
)}
) }
</span>
<div className='mx_SettingsTab_subsectionText'>
{_t("A session's public name is visible to people you communicate with")}
{ _t("A session's public name is visible to people you communicate with") }
<DevicesPanel />
</div>
</div>
<div className="mx_SettingsTab_heading">{_t("Encryption")}</div>
<div className="mx_SettingsTab_heading">{ _t("Encryption") }</div>
<div className="mx_SettingsTab_section">
{secureBackup}
{eventIndex}
{crossSigning}
{this._renderCurrentDeviceInfo()}
{ secureBackup }
{ eventIndex }
{ crossSigning }
{ this._renderCurrentDeviceInfo() }
</div>
{ privacySection }
{ advancedSection }

View file

@ -130,7 +130,7 @@ export default class VoiceUserSettingsTab extends React.Component<{}, IState> {
private renderDeviceOptions(devices: Array<MediaDeviceInfo>, category: MediaDeviceKindEnum): Array<JSX.Element> {
return devices.map((d) => {
return (<option key={`${category}-${d.deviceId}`} value={d.deviceId}>{d.label}</option>);
return (<option key={`${category}-${d.deviceId}`} value={d.deviceId}>{ d.label }</option>);
});
}
@ -159,9 +159,9 @@ export default class VoiceUserSettingsTab extends React.Component<{}, IState> {
if (!this.state.mediaDevices) {
requestButton = (
<div className='mx_VoiceUserSettingsTab_missingMediaPermissions'>
<p>{_t("Missing media permissions, click the button below to request.")}</p>
<p>{ _t("Missing media permissions, click the button below to request.") }</p>
<AccessibleButton onClick={this.requestMediaPermissions} kind="primary">
{_t("Request media permissions")}
{ _t("Request media permissions") }
</AccessibleButton>
</div>
);
@ -182,7 +182,7 @@ export default class VoiceUserSettingsTab extends React.Component<{}, IState> {
return (
<div className="mx_SettingsTab mx_VoiceUserSettingsTab">
<div className="mx_SettingsTab_heading">{_t("Voice & Video")}</div>
<div className="mx_SettingsTab_heading">{ _t("Voice & Video") }</div>
<div className="mx_SettingsTab_section">
{ requestButton }
{ speakerDropdown }