Merge branch 'develop' into ctrl-enter-send

This commit is contained in:
Clemens Zeidler 2020-11-19 19:40:38 +13:00
commit 1346416d20
451 changed files with 25135 additions and 10894 deletions

View file

@ -14,25 +14,25 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import React, {useCallback} from "react";
import React, {useState} from "react";
import PropTypes from "prop-types";
import * as sdk from "../../../index";
import {_t} from "../../../languageHandler";
import Modal from "../../../Modal";
import AccessibleButton from "../elements/AccessibleButton";
import classNames from "classnames";
const AvatarSetting = ({avatarUrl, avatarAltText, avatarName, uploadAvatar, removeAvatar}) => {
const AccessibleButton = sdk.getComponent('elements.AccessibleButton');
const [isHovering, setIsHovering] = useState(false);
const hoveringProps = {
onMouseEnter: () => setIsHovering(true),
onMouseLeave: () => setIsHovering(false),
};
const openImageView = useCallback(() => {
const ImageView = sdk.getComponent("elements.ImageView");
Modal.createDialog(ImageView, {
src: avatarUrl,
name: avatarName,
}, "mx_Dialog_lightbox");
}, [avatarUrl, avatarName]);
let avatarElement = <div className="mx_AvatarSetting_avatarPlaceholder" />;
let avatarElement = <AccessibleButton
element="div"
onClick={uploadAvatar}
className="mx_AvatarSetting_avatarPlaceholder"
{...hoveringProps}
/>;
if (avatarUrl) {
avatarElement = (
<AccessibleButton
@ -40,16 +40,20 @@ const AvatarSetting = ({avatarUrl, avatarAltText, avatarName, uploadAvatar, remo
src={avatarUrl}
alt={avatarAltText}
aria-label={avatarAltText}
onClick={openImageView} />
onClick={uploadAvatar}
{...hoveringProps}
/>
);
}
let uploadAvatarBtn;
if (uploadAvatar) {
// insert an empty div to be the host for a css mask containing the upload.svg
uploadAvatarBtn = <AccessibleButton onClick={uploadAvatar} kind="primary">
{_t("Upload")}
</AccessibleButton>;
uploadAvatarBtn = <AccessibleButton
onClick={uploadAvatar}
className='mx_AvatarSetting_uploadButton'
{...hoveringProps}
/>;
}
let removeAvatarBtn;
@ -59,10 +63,18 @@ const AvatarSetting = ({avatarUrl, avatarAltText, avatarName, uploadAvatar, remo
</AccessibleButton>;
}
return <div className="mx_AvatarSetting_avatar">
{ avatarElement }
{ uploadAvatarBtn }
{ removeAvatarBtn }
const avatarClasses = classNames({
"mx_AvatarSetting_avatar": true,
"mx_AvatarSetting_avatar_hovering": isHovering,
});
return <div className={avatarClasses}>
{avatarElement}
<div className="mx_AvatarSetting_hover">
<div className="mx_AvatarSetting_hoverBg" />
<span>{_t("Upload")}</span>
</div>
{uploadAvatarBtn}
{removeAvatarBtn}
</div>;
};

View file

@ -19,6 +19,7 @@ import PropTypes from 'prop-types';
import {MatrixClientPeg} from "../../../MatrixClientPeg";
import * as sdk from '../../../index';
import { _t } from '../../../languageHandler';
import Spinner from '../elements/Spinner';
export default class ChangeAvatar extends React.Component {
static propTypes = {
@ -58,7 +59,7 @@ export default class ChangeAvatar extends React.Component {
}
// TODO: [REACT-WARNING] Replace with appropriate lifecycle event
UNSAFE_componentWillReceiveProps(newProps) {
UNSAFE_componentWillReceiveProps(newProps) { // eslint-disable-line camelcase
if (this.avatarSet) {
// don't clobber what the user has just set
return;
@ -143,7 +144,9 @@ export default class ChangeAvatar extends React.Component {
// time to propagate through to the RoomAvatar.
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' />;
avatarImg = <RoomAvatar
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 ?
@ -174,9 +177,8 @@ export default class ChangeAvatar extends React.Component {
</div>
);
case ChangeAvatar.Phases.Uploading:
var Loader = sdk.getComponent("elements.Spinner");
return (
<Loader />
<Spinner />
);
}
}

View file

@ -19,14 +19,12 @@ import Field from "../elements/Field";
import React from 'react';
import PropTypes from 'prop-types';
import {MatrixClientPeg} from "../../../MatrixClientPeg";
import dis from "../../../dispatcher/dispatcher";
import AccessibleButton from '../elements/AccessibleButton';
import Spinner from '../elements/Spinner';
import { _t } from '../../../languageHandler';
import * as sdk from "../../../index";
import Modal from "../../../Modal";
import sessionStore from '../../../stores/SessionStore';
export default class ChangePassword extends React.Component {
static propTypes = {
onFinished: PropTypes.func,
@ -35,6 +33,7 @@ export default class ChangePassword extends React.Component {
rowClassName: PropTypes.string,
buttonClassName: PropTypes.string,
buttonKind: PropTypes.string,
buttonLabel: PropTypes.string,
confirm: PropTypes.bool,
// Whether to autoFocus the new password input
autoFocusNewPasswordInput: PropTypes.bool,
@ -65,33 +64,11 @@ export default class ChangePassword extends React.Component {
state = {
phase: ChangePassword.Phases.Edit,
cachedPassword: null,
oldPassword: "",
newPassword: "",
newPasswordConfirm: "",
};
componentDidMount() {
this._sessionStore = sessionStore;
this._sessionStoreToken = this._sessionStore.addListener(
this._setStateFromSessionStore,
);
this._setStateFromSessionStore();
}
componentWillUnmount() {
if (this._sessionStoreToken) {
this._sessionStoreToken.remove();
}
}
_setStateFromSessionStore = () => {
this.setState({
cachedPassword: this._sessionStore.getCachedPassword(),
});
};
changePassword(oldPassword, newPassword) {
const cli = MatrixClientPeg.get();
@ -118,8 +95,11 @@ export default class ChangePassword extends React.Component {
</div>,
button: _t("Continue"),
extraButtons: [
<button className="mx_Dialog_primary"
onClick={this._onExportE2eKeysClicked}>
<button
key="exportRoomKeys"
className="mx_Dialog_primary"
onClick={this._onExportE2eKeysClicked}
>
{ _t('Export E2E room keys') }
</button>,
],
@ -149,9 +129,6 @@ export default class ChangePassword extends React.Component {
});
cli.setPassword(authDict, newPassword).then(() => {
// Notify SessionStore that the user's password was changed
dis.dispatch({action: 'password_changed'});
if (this.props.shouldAskForEmail) {
return this._optionallySetEmail().then((confirmed) => {
this.props.onFinished({
@ -184,7 +161,7 @@ export default class ChangePassword extends React.Component {
_onExportE2eKeysClicked = () => {
Modal.createTrackedDialogAsync('Export E2E Keys', 'Change Password',
import('../../../async-components/views/dialogs/ExportE2eKeysDialog'),
import('../../../async-components/views/dialogs/security/ExportE2eKeysDialog'),
{
matrixClient: MatrixClientPeg.get(),
},
@ -211,7 +188,7 @@ export default class ChangePassword extends React.Component {
onClickChange = (ev) => {
ev.preventDefault();
const oldPassword = this.state.cachedPassword || this.state.oldPassword;
const oldPassword = this.state.oldPassword;
const newPassword = this.state.newPassword;
const confirmPassword = this.state.newPasswordConfirm;
const err = this.props.onCheckPassword(
@ -230,31 +207,22 @@ export default class ChangePassword extends React.Component {
const rowClassName = this.props.rowClassName;
const buttonClassName = this.props.buttonClassName;
let currentPassword = null;
if (!this.state.cachedPassword) {
currentPassword = (
<div className={rowClassName}>
<Field
type="password"
label={_t('Current password')}
value={this.state.oldPassword}
onChange={this.onChangeOldPassword}
/>
</div>
);
}
switch (this.state.phase) {
case ChangePassword.Phases.Edit:
const passwordLabel = this.state.cachedPassword ?
_t('Password') : _t('New Password');
return (
<form className={this.props.className} onSubmit={this.onClickChange}>
{ currentPassword }
<div className={rowClassName}>
<Field
type="password"
label={passwordLabel}
label={_t('Current password')}
value={this.state.oldPassword}
onChange={this.onChangeOldPassword}
/>
</div>
<div className={rowClassName}>
<Field
type="password"
label={_t('New Password')}
value={this.state.newPassword}
autoFocus={this.props.autoFocusNewPasswordInput}
onChange={this.onChangeNewPassword}
@ -271,15 +239,14 @@ export default class ChangePassword extends React.Component {
/>
</div>
<AccessibleButton className={buttonClassName} kind={this.props.buttonKind} onClick={this.onClickChange}>
{ _t('Change Password') }
{ this.props.buttonLabel || _t('Change Password') }
</AccessibleButton>
</form>
);
case ChangePassword.Phases.Uploading:
var Loader = sdk.getComponent("elements.Spinner");
return (
<div className="mx_Dialog_content">
<Loader />
<Spinner />
</div>
);
}

View file

@ -19,9 +19,10 @@ import React from 'react';
import {MatrixClientPeg} from '../../../MatrixClientPeg';
import { _t } from '../../../languageHandler';
import * as sdk from '../../../index';
import { accessSecretStorage } from '../../../SecurityManager';
import Modal from '../../../Modal';
import Spinner from '../elements/Spinner';
import InteractiveAuthDialog from '../dialogs/InteractiveAuthDialog';
import ConfirmDestroyCrossSigningDialog from '../dialogs/security/ConfirmDestroyCrossSigningDialog';
export default class CrossSigningPanel extends React.PureComponent {
constructor(props) {
@ -31,13 +32,13 @@ export default class CrossSigningPanel extends React.PureComponent {
this.state = {
error: null,
crossSigningPublicKeysOnDevice: false,
crossSigningPrivateKeysInStorage: false,
masterPrivateKeyCached: false,
selfSigningPrivateKeyCached: false,
userSigningPrivateKeyCached: false,
sessionBackupKeyCached: false,
secretStorageKeyInAccount: false,
crossSigningPublicKeysOnDevice: null,
crossSigningPrivateKeysInStorage: null,
masterPrivateKeyCached: null,
selfSigningPrivateKeyCached: null,
userSigningPrivateKeyCached: null,
homeserverSupportsCrossSigning: null,
crossSigningReady: null,
};
}
@ -66,7 +67,7 @@ export default class CrossSigningPanel extends React.PureComponent {
};
_onBootstrapClick = () => {
this._bootstrapSecureSecretStorage(false);
this._bootstrapCrossSigning({ forceReset: false });
};
onStatusChanged = () => {
@ -83,14 +84,9 @@ export default class CrossSigningPanel extends React.PureComponent {
const masterPrivateKeyCached = !!(pkCache && await pkCache.getCrossSigningKeyCache("master"));
const selfSigningPrivateKeyCached = !!(pkCache && await pkCache.getCrossSigningKeyCache("self_signing"));
const userSigningPrivateKeyCached = !!(pkCache && await pkCache.getCrossSigningKeyCache("user_signing"));
const sessionBackupKeyFromCache = await cli._crypto.getSessionBackupPrivateKey();
const sessionBackupKeyCached = !!(sessionBackupKeyFromCache);
const sessionBackupKeyWellFormed = sessionBackupKeyFromCache instanceof Uint8Array;
const secretStorageKeyInAccount = await secretStorage.hasKey();
const homeserverSupportsCrossSigning =
await cli.doesServerSupportUnstableFeature("org.matrix.e2e_cross_signing");
const crossSigningReady = await cli.isCrossSigningReady();
const secretStorageReady = await cli.isSecretStorageReady();
this.setState({
crossSigningPublicKeysOnDevice,
@ -98,45 +94,55 @@ export default class CrossSigningPanel extends React.PureComponent {
masterPrivateKeyCached,
selfSigningPrivateKeyCached,
userSigningPrivateKeyCached,
sessionBackupKeyCached,
sessionBackupKeyWellFormed,
secretStorageKeyInAccount,
homeserverSupportsCrossSigning,
crossSigningReady,
secretStorageReady,
});
}
/**
* Bootstrapping secret storage may take one of these paths:
* 1. Create secret storage from a passphrase and store cross-signing keys
* in secret storage.
* Bootstrapping cross-signing take one of these paths:
* 1. Create cross-signing keys locally and store in secret storage (if it
* already exists on the account).
* 2. Access existing secret storage by requesting passphrase and accessing
* cross-signing keys as needed.
* 3. All keys are loaded and there's nothing to do.
* @param {bool} [forceReset] Bootstrap again even if keys already present
*/
_bootstrapSecureSecretStorage = async (forceReset=false) => {
_bootstrapCrossSigning = async ({ forceReset = false }) => {
this.setState({ error: null });
try {
await accessSecretStorage(() => undefined, forceReset);
const cli = MatrixClientPeg.get();
await cli.bootstrapCrossSigning({
authUploadDeviceSigningKeys: async (makeRequest) => {
const { finished } = Modal.createTrackedDialog(
'Cross-signing keys dialog', '', InteractiveAuthDialog,
{
title: _t("Setting up keys"),
matrixClient: cli,
makeRequest,
},
);
const [confirmed] = await finished;
if (!confirmed) {
throw new Error("Cross-signing key upload auth canceled");
}
},
setupNewCrossSigning: forceReset,
});
} catch (e) {
this.setState({ error: e });
console.error("Error bootstrapping secret storage", e);
console.error("Error bootstrapping cross-signing", e);
}
if (this._unmounted) return;
this._getUpdatedStatus();
}
onDestroyStorage = (act) => {
if (!act) return;
this._bootstrapSecureSecretStorage(true);
}
_destroySecureSecretStorage = () => {
const ConfirmDestroyCrossSigningDialog = sdk.getComponent("dialogs.ConfirmDestroyCrossSigningDialog");
_resetCrossSigning = () => {
Modal.createDialog(ConfirmDestroyCrossSigningDialog, {
onFinished: this.onDestroyStorage,
onFinished: (act) => {
if (!act) return;
this._bootstrapCrossSigning({ forceReset: true });
},
});
}
@ -149,12 +155,8 @@ export default class CrossSigningPanel extends React.PureComponent {
masterPrivateKeyCached,
selfSigningPrivateKeyCached,
userSigningPrivateKeyCached,
sessionBackupKeyCached,
sessionBackupKeyWellFormed,
secretStorageKeyInAccount,
homeserverSupportsCrossSigning,
crossSigningReady,
secretStorageReady,
} = this.state;
let errorSection;
@ -169,14 +171,9 @@ export default class CrossSigningPanel extends React.PureComponent {
summarisedStatus = <p>{_t(
"Your homeserver does not support cross-signing.",
)}</p>;
} else if (crossSigningReady && secretStorageReady) {
} else if (crossSigningReady) {
summarisedStatus = <p> {_t(
"Cross-signing and secret storage are ready for use.",
)}</p>;
} else if (crossSigningReady && !secretStorageReady) {
summarisedStatus = <p> {_t(
"Cross-signing is ready for use, but secret storage is " +
"currently not being used to backup your keys.",
"Cross-signing is ready for use.",
)}</p>;
} else if (crossSigningPrivateKeysInStorage) {
summarisedStatus = <p>{_t(
@ -185,52 +182,49 @@ export default class CrossSigningPanel extends React.PureComponent {
)}</p>;
} else {
summarisedStatus = <p>{_t(
"Cross-signing and secret storage are not yet set up.",
"Cross-signing is not set up.",
)}</p>;
}
const keysExistAnywhere = (
secretStorageKeyInAccount ||
crossSigningPublicKeysOnDevice ||
crossSigningPrivateKeysInStorage ||
crossSigningPublicKeysOnDevice
masterPrivateKeyCached ||
selfSigningPrivateKeyCached ||
userSigningPrivateKeyCached
);
const keysExistEverywhere = (
secretStorageKeyInAccount &&
crossSigningPublicKeysOnDevice &&
crossSigningPrivateKeysInStorage &&
crossSigningPublicKeysOnDevice
masterPrivateKeyCached &&
selfSigningPrivateKeyCached &&
userSigningPrivateKeyCached
);
let resetButton;
if (keysExistAnywhere) {
resetButton = (
<div className="mx_CrossSigningPanel_buttonRow">
<AccessibleButton kind="danger" onClick={this._destroySecureSecretStorage}>
{_t("Reset cross-signing and secret storage")}
</AccessibleButton>
</div>
);
}
const actions = [];
// TODO: determine how better to expose this to users in addition to prompts at login/toast
let bootstrapButton;
if (!keysExistEverywhere && homeserverSupportsCrossSigning) {
bootstrapButton = (
<div className="mx_CrossSigningPanel_buttonRow">
<AccessibleButton kind="primary" onClick={this._onBootstrapClick}>
{_t("Bootstrap cross-signing and secret storage")}
</AccessibleButton>
</div>
actions.push(
<AccessibleButton key="setup" kind="primary" onClick={this._onBootstrapClick}>
{_t("Set up")}
</AccessibleButton>,
);
}
let sessionBackupKeyWellFormedText = "";
if (sessionBackupKeyCached) {
sessionBackupKeyWellFormedText = ", ";
if (sessionBackupKeyWellFormed) {
sessionBackupKeyWellFormedText += _t("well formed");
} else {
sessionBackupKeyWellFormedText += _t("unexpected type");
}
if (keysExistAnywhere) {
actions.push(
<AccessibleButton key="reset" kind="danger" onClick={this._resetCrossSigning}>
{_t("Reset")}
</AccessibleButton>,
);
}
let actionRow;
if (actions.length) {
actionRow = <div className="mx_CrossSigningPanel_buttonRow">
{actions}
</div>;
}
return (
@ -245,7 +239,7 @@ export default class CrossSigningPanel extends React.PureComponent {
</tr>
<tr>
<td>{_t("Cross-signing private keys:")}</td>
<td>{crossSigningPrivateKeysInStorage ? _t("in secret storage") : _t("not found")}</td>
<td>{crossSigningPrivateKeysInStorage ? _t("in secret storage") : _t("not found in storage")}</td>
</tr>
<tr>
<td>{_t("Master private key:")}</td>
@ -259,17 +253,6 @@ export default class CrossSigningPanel extends React.PureComponent {
<td>{_t("User signing private key:")}</td>
<td>{userSigningPrivateKeyCached ? _t("cached locally") : _t("not found locally")}</td>
</tr>
<tr>
<td>{_t("Session backup key:")}</td>
<td>
{sessionBackupKeyCached ? _t("cached locally") : _t("not found locally")}
{sessionBackupKeyWellFormedText}
</td>
</tr>
<tr>
<td>{_t("Secret storage public key:")}</td>
<td>{secretStorageKeyInAccount ? _t("in account data") : _t("not found")}</td>
</tr>
<tr>
<td>{_t("Homeserver feature support:")}</td>
<td>{homeserverSupportsCrossSigning ? _t("exists") : _t("not found")}</td>
@ -277,8 +260,7 @@ export default class CrossSigningPanel extends React.PureComponent {
</tbody></table>
</details>
{errorSection}
{bootstrapButton}
{resetButton}
{actionRow}
</div>
);
}

View file

@ -74,7 +74,7 @@ export default class DevicesPanel extends React.Component {
}
/**
/*
* compare two devices, sorting from most-recently-seen to least-recently-seen
* (and then, for stability, by device id)
*/

View file

@ -19,6 +19,7 @@ import React from 'react';
import * as sdk from '../../../index';
import {_t} from "../../../languageHandler";
import {SettingLevel} from "../../../settings/SettingLevel";
import SettingsStore from "../../../settings/SettingsStore";
const SETTING_MANUALLY_VERIFY_ALL_SESSIONS = "e2ee.manuallyVerifyAllSessions";
@ -37,3 +38,7 @@ const E2eAdvancedPanel = props => {
};
export default E2eAdvancedPanel;
export function isE2eAdvancedPanelPossible(): boolean {
return SettingsStore.isEnabled(SETTING_MANUALLY_VERIFY_ALL_SESSIONS);
}

View file

@ -129,11 +129,16 @@ export default class EventIndexPanel extends React.Component {
eventIndexingSettings = (
<div>
<div className='mx_SettingsTab_subsectionText'>
{_t( "Securely cache encrypted messages locally for them " +
"to appear in search results, using ")
} {formatBytes(this.state.eventIndexSize, 0)}
{_t( " to store messages from ")}
{formatCountLong(this.state.roomCount)} {_t("rooms.")}
{_t("Securely cache encrypted messages locally for them " +
"to appear in search results, using %(size)s to store messages from %(rooms)s rooms.",
{
size: formatBytes(this.state.eventIndexSize, 0),
// This drives the singular / plural string
// selection for "room" / "rooms" only.
count: this.state.roomCount,
rooms: formatCountLong(this.state.roomCount),
},
)}
</div>
<div>
<AccessibleButton kind="primary" onClick={this._onManage}>

View file

@ -42,6 +42,14 @@ export default class IntegrationManager extends React.Component {
loading: false,
};
constructor(props) {
super(props);
this.state = {
errored: false,
};
}
componentDidMount() {
this.dispatcherRef = dis.register(this.onAction);
document.addEventListener("keydown", this.onKeyDown);
@ -66,6 +74,10 @@ export default class IntegrationManager extends React.Component {
}
};
onError = () => {
this.setState({ errored: true });
};
render() {
if (this.props.loading) {
const Spinner = sdk.getComponent("elements.Spinner");
@ -77,7 +89,7 @@ export default class IntegrationManager extends React.Component {
);
}
if (!this.props.connected) {
if (!this.props.connected || this.state.errored) {
return (
<div className='mx_IntegrationManager_error'>
<h3>{_t("Cannot connect to integration manager")}</h3>
@ -86,6 +98,6 @@ export default class IntegrationManager extends React.Component {
);
}
return <iframe src={this.props.url}></iframe>;
return <iframe src={this.props.url} onError={this.onError} />;
}
}

View file

@ -31,6 +31,7 @@ import SdkConfig from "../../../SdkConfig";
import LabelledToggleSwitch from "../elements/LabelledToggleSwitch";
import AccessibleButton from "../elements/AccessibleButton";
import {SettingLevel} from "../../../settings/SettingLevel";
import {UIFeature} from "../../../settings/UIFeature";
// TODO: this "view" component still has far too much application logic in it,
// which should be factored out to other files.
@ -94,7 +95,9 @@ export default class Notifications extends React.Component {
phase: Notifications.phases.LOADING,
});
MatrixClientPeg.get().setPushRuleEnabled('global', self.state.masterPushRule.kind, self.state.masterPushRule.rule_id, !checked).then(function() {
MatrixClientPeg.get().setPushRuleEnabled(
'global', self.state.masterPushRule.kind, self.state.masterPushRule.rule_id, !checked,
).then(function() {
self._refreshFromServer();
});
};
@ -216,8 +219,8 @@ export default class Notifications extends React.Component {
description: _t('Enter keywords separated by a comma:'),
button: _t('OK'),
value: keywords,
onFinished: (should_leave, newValue) => {
if (should_leave && newValue !== keywords) {
onFinished: (shouldLeave, newValue) => {
if (shouldLeave && newValue !== keywords) {
let newKeywords = newValue.split(',');
for (const i in newKeywords) {
newKeywords[i] = newKeywords[i].trim();
@ -403,7 +406,9 @@ export default class Notifications extends React.Component {
// 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]);
pushRuleVectorStateKind = PushRuleVectorState.contentRuleVectorStateKind(
self.state.vectorContentRules.rules[0],
);
} else {
// ON is default
pushRuleVectorStateKind = PushRuleVectorState.ON;
@ -415,10 +420,9 @@ export default class Notifications extends React.Component {
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,
deferreds.push(cli.addPushRule('global', 'content', keyword, {
actions: PushRuleVectorState.actionsFor(pushRuleVectorStateKind),
pattern: keyword,
}));
} else {
deferreds.push(self._addDisabledPushRule('global', 'content', keyword, {
@ -482,12 +486,14 @@ export default class Notifications extends React.Component {
_refreshFromServer = () => {
const self = this;
const pushRulesPromise = MatrixClientPeg.get().getPushRules().then(self._portRulesToNewAPI).then(function(rulesets) {
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 rule_categories = {
const ruleCategories = {
// The master rule (all notifications disabling)
'.m.rule.master': 'master',
@ -514,7 +520,7 @@ export default class Notifications extends React.Component {
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 = rule_categories[r.rule_id];
const cat = ruleCategories[r.rule_id];
r.kind = kind;
if (r.rule_id[0] === '.') {
@ -750,7 +756,7 @@ export default class Notifications extends React.Component {
if (this.state.masterPushRule) {
masterPushRuleDiv = <LabelledToggleSwitch value={!this.state.masterPushRule.enabled}
onChange={this.onEnableNotificationsChange}
label={_t('Enable notifications for this account')}/>;
label={_t('Enable notifications for this account')} />;
}
let clearNotificationsButton;
@ -778,14 +784,14 @@ export default class Notifications extends React.Component {
const emailThreepids = this.state.threepids.filter((tp) => tp.medium === "email");
let emailNotificationsRows;
if (emailThreepids.length === 0) {
emailNotificationsRows = <div>
{ _t('Add an email address to configure email notifications') }
</div>;
} else {
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
@ -803,7 +809,10 @@ export default class Notifications extends React.Component {
}
if (externalKeywords.length) {
externalKeywords = externalKeywords.join(", ");
externalRules.push(<li>{ _t('Notifications on the following keywords follow rules which cant be displayed here:') } { externalKeywords }</li>);
externalRules.push(<li>
{_t('Notifications on the following keywords follow rules which cant be displayed here:') }
{ externalKeywords }
</li>);
}
let devicesSection;

View file

@ -18,30 +18,23 @@ import React, {createRef} from 'react';
import {_t} from "../../../languageHandler";
import {MatrixClientPeg} from "../../../MatrixClientPeg";
import Field from "../elements/Field";
import {User} from "matrix-js-sdk";
import { getHostingLink } from '../../../utils/HostingLink';
import * as sdk from "../../../index";
import {OwnProfileStore} from "../../../stores/OwnProfileStore";
import Modal from "../../../Modal";
import ErrorDialog from "../dialogs/ErrorDialog";
export default class ProfileSettings extends React.Component {
constructor() {
super();
const client = MatrixClientPeg.get();
let user = client.getUser(client.getUserId());
if (!user) {
// XXX: We shouldn't have to do this.
// There seems to be a condition where the User object won't exist until a room
// exists on the account. To work around this, we'll just create a temporary User
// and use that.
console.warn("User object not found - creating one for ProfileSettings");
user = new User(client.getUserId());
}
let avatarUrl = user.avatarUrl;
let avatarUrl = OwnProfileStore.instance.avatarMxc;
if (avatarUrl) avatarUrl = client.mxcUrlToHttp(avatarUrl, 96, 96, 'crop', false);
this.state = {
userId: user.userId,
originalDisplayName: user.rawDisplayName,
displayName: user.rawDisplayName,
userId: client.getUserId(),
originalDisplayName: OwnProfileStore.instance.displayName,
displayName: OwnProfileStore.instance.displayName,
originalAvatarUrl: avatarUrl,
avatarUrl: avatarUrl,
avatarFile: null,
@ -65,6 +58,15 @@ export default class ProfileSettings extends React.Component {
});
};
_clearProfile = async (e) => {
e.stopPropagation();
e.preventDefault();
if (!this.state.enableProfileSave) return;
this._removeAvatar();
this.setState({enableProfileSave: false, displayName: this.state.originalDisplayName});
};
_saveProfile = async (e) => {
e.stopPropagation();
e.preventDefault();
@ -75,21 +77,30 @@ export default class ProfileSettings extends React.Component {
const client = MatrixClientPeg.get();
const newState = {};
// TODO: What do we do about errors?
try {
if (this.state.originalDisplayName !== this.state.displayName) {
await client.setDisplayName(this.state.displayName);
newState.originalDisplayName = this.state.displayName;
}
if (this.state.originalDisplayName !== this.state.displayName) {
await client.setDisplayName(this.state.displayName);
newState.originalDisplayName = this.state.displayName;
}
if (this.state.avatarFile) {
const uri = await client.uploadContent(this.state.avatarFile);
await client.setAvatarUrl(uri);
newState.avatarUrl = client.mxcUrlToHttp(uri, 96, 96, 'crop', false);
newState.originalAvatarUrl = newState.avatarUrl;
newState.avatarFile = null;
} else if (this.state.originalAvatarUrl !== this.state.avatarUrl) {
await client.setAvatarUrl(""); // use empty string as Synapse 500s on undefined
if (this.state.avatarFile) {
console.log(
`Uploading new avatar, ${this.state.avatarFile.name} of type ${this.state.avatarFile.type},` +
` (${this.state.avatarFile.size}) bytes`);
const uri = await client.uploadContent(this.state.avatarFile);
await client.setAvatarUrl(uri);
newState.avatarUrl = client.mxcUrlToHttp(uri, 96, 96, 'crop', false);
newState.originalAvatarUrl = newState.avatarUrl;
newState.avatarFile = null;
} else if (this.state.originalAvatarUrl !== this.state.avatarUrl) {
await client.setAvatarUrl(""); // use empty string as Synapse 500s on undefined
}
} catch (err) {
console.log("Failed to save profile", err);
Modal.createTrackedDialog('Failed to save profile', '', ErrorDialog, {
title: _t("Failed to save your profile"),
description: ((err && err.message) ? err.message : _t("The operation could not be completed")),
});
}
this.setState(newState);
@ -144,18 +155,27 @@ export default class ProfileSettings extends React.Component {
const AccessibleButton = sdk.getComponent('elements.AccessibleButton');
const AvatarSetting = sdk.getComponent('settings.AvatarSetting');
return (
<form onSubmit={this._saveProfile} autoComplete="off" noValidate={true}>
<form
onSubmit={this._saveProfile}
autoComplete="off"
noValidate={true}
className="mx_ProfileSettings_profileForm"
>
<input type="file" 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>
<Field
label={_t("Display Name")}
type="text" value={this.state.displayName}
autoComplete="off"
onChange={this._onDisplayNameChanged}
/>
<p>
{this.state.userId}
{hostingSignup}
</p>
<Field label={_t("Display Name")}
type="text" value={this.state.displayName} autoComplete="off"
onChange={this._onDisplayNameChanged} />
</div>
<AvatarSetting
avatarUrl={this.state.avatarUrl}
@ -164,10 +184,22 @@ export default class ProfileSettings extends React.Component {
uploadAvatar={this._uploadAvatar}
removeAvatar={this._removeAvatar} />
</div>
<AccessibleButton onClick={this._saveProfile} kind="primary"
disabled={!this.state.enableProfileSave}>
{_t("Save")}
</AccessibleButton>
<div className="mx_ProfileSettings_buttons">
<AccessibleButton
onClick={this._clearProfile}
kind="link"
disabled={!this.state.enableProfileSave}
>
{_t("Cancel")}
</AccessibleButton>
<AccessibleButton
onClick={this._saveProfile}
kind="primary"
disabled={!this.state.enableProfileSave}
>
{_t("Save")}
</AccessibleButton>
</div>
</form>
);
}

View file

@ -17,13 +17,17 @@ limitations under the License.
import React from 'react';
import * as sdk from '../../../index';
import {MatrixClientPeg} from '../../../MatrixClientPeg';
import { _t } from '../../../languageHandler';
import Modal from '../../../Modal';
import { isSecureBackupRequired } from '../../../utils/WellKnownUtils';
import Spinner from '../elements/Spinner';
import AccessibleButton from '../elements/AccessibleButton';
import QuestionDialog from '../dialogs/QuestionDialog';
import RestoreKeyBackupDialog from '../dialogs/security/RestoreKeyBackupDialog';
import { accessSecretStorage } from '../../../SecurityManager';
export default class KeyBackupPanel extends React.PureComponent {
export default class SecureBackupPanel extends React.PureComponent {
constructor(props) {
super(props);
@ -31,9 +35,13 @@ export default class KeyBackupPanel extends React.PureComponent {
this.state = {
loading: true,
error: null,
backupKeyStored: null,
backupKeyCached: null,
backupKeyWellFormed: null,
secretStorageKeyInAccount: null,
secretStorageReady: null,
backupInfo: null,
backupSigStatus: null,
backupKeyStored: null,
sessionsRemaining: 0,
};
}
@ -73,59 +81,76 @@ export default class KeyBackupPanel extends React.PureComponent {
}
async _checkKeyBackupStatus() {
this._getUpdatedDiagnostics();
try {
const {backupInfo, trustInfo} = await MatrixClientPeg.get().checkKeyBackup();
const backupKeyStored = Boolean(await MatrixClientPeg.get().isKeyBackupKeyStored());
this.setState({
loading: false,
error: null,
backupInfo,
backupSigStatus: trustInfo,
backupKeyStored,
error: null,
loading: false,
});
} catch (e) {
console.log("Unable to fetch check backup status", e);
if (this._unmounted) return;
this.setState({
loading: false,
error: e,
backupInfo: null,
backupSigStatus: null,
backupKeyStored: null,
loading: false,
});
}
}
async _loadBackupStatus() {
this.setState({loading: true});
this.setState({ loading: true });
this._getUpdatedDiagnostics();
try {
const backupInfo = await MatrixClientPeg.get().getKeyBackupVersion();
const backupSigStatus = await MatrixClientPeg.get().isKeyBackupTrusted(backupInfo);
const backupKeyStored = await MatrixClientPeg.get().isKeyBackupKeyStored();
if (this._unmounted) return;
this.setState({
loading: false,
error: null,
backupInfo,
backupSigStatus,
backupKeyStored,
loading: false,
});
} catch (e) {
console.log("Unable to fetch key backup status", e);
if (this._unmounted) return;
this.setState({
loading: false,
error: e,
backupInfo: null,
backupSigStatus: null,
backupKeyStored: null,
loading: false,
});
}
}
async _getUpdatedDiagnostics() {
const cli = MatrixClientPeg.get();
const secretStorage = cli._crypto._secretStorage;
const backupKeyStored = !!(await cli.isKeyBackupKeyStored());
const backupKeyFromCache = await cli._crypto.getSessionBackupPrivateKey();
const backupKeyCached = !!(backupKeyFromCache);
const backupKeyWellFormed = backupKeyFromCache instanceof Uint8Array;
const secretStorageKeyInAccount = await secretStorage.hasKey();
const secretStorageReady = await cli.isSecretStorageReady();
if (this._unmounted) return;
this.setState({
backupKeyStored,
backupKeyCached,
backupKeyWellFormed,
secretStorageKeyInAccount,
secretStorageReady,
});
}
_startNewBackup = () => {
Modal.createTrackedDialogAsync('Key Backup', 'Key Backup',
import('../../../async-components/views/dialogs/keybackup/CreateKeyBackupDialog'),
import('../../../async-components/views/dialogs/security/CreateKeyBackupDialog'),
{
onFinished: () => {
this._loadBackupStatus();
@ -135,7 +160,6 @@ export default class KeyBackupPanel extends React.PureComponent {
}
_deleteBackup = () => {
const QuestionDialog = sdk.getComponent('dialogs.QuestionDialog');
Modal.createTrackedDialog('Delete Backup', '', QuestionDialog, {
title: _t('Delete Backup'),
description: _t(
@ -155,41 +179,58 @@ export default class KeyBackupPanel extends React.PureComponent {
}
_restoreBackup = async () => {
const RestoreKeyBackupDialog = sdk.getComponent('dialogs.keybackup.RestoreKeyBackupDialog');
Modal.createTrackedDialog(
'Restore Backup', '', RestoreKeyBackupDialog, null, null,
/* priority = */ false, /* static = */ true,
);
}
render() {
const Spinner = sdk.getComponent("elements.Spinner");
const AccessibleButton = sdk.getComponent("elements.AccessibleButton");
const encryptedMessageAreEncrypted = _t(
"Encrypted messages are secured with end-to-end encryption. " +
"Only you and the recipient(s) have the keys to read these messages.",
);
_resetSecretStorage = async () => {
this.setState({ error: null });
try {
await accessSecretStorage(() => { }, /* forceReset = */ true);
} catch (e) {
console.error("Error resetting secret storage", e);
if (this._unmounted) return;
this.setState({ error: e });
}
if (this._unmounted) return;
this._loadBackupStatus();
}
if (this.state.error) {
return (
render() {
const {
loading,
error,
backupKeyStored,
backupKeyCached,
backupKeyWellFormed,
secretStorageKeyInAccount,
secretStorageReady,
backupInfo,
backupSigStatus,
sessionsRemaining,
} = this.state;
let statusDescription;
let extraDetailsTableRows;
let extraDetails;
const actions = [];
if (error) {
statusDescription = (
<div className="error">
{_t("Unable to load key backup status")}
</div>
);
} else if (this.state.loading) {
return <Spinner />;
} else if (this.state.backupInfo) {
let clientBackupStatus;
} else if (loading) {
statusDescription = <Spinner />;
} else if (backupInfo) {
let restoreButtonCaption = _t("Restore from Backup");
if (MatrixClientPeg.get().getKeyBackupEnabled()) {
clientBackupStatus = <div>
<p>{encryptedMessageAreEncrypted}</p>
<p> {_t("This session is backing up your keys. ")}</p>
</div>;
statusDescription = <p> {_t("This session is backing up your keys. ")}</p>;
} else {
clientBackupStatus = <div>
<p>{encryptedMessageAreEncrypted}</p>
statusDescription = <>
<p>{_t(
"This session is <b>not backing up your keys</b>, " +
"but you do have an existing backup you can restore from " +
@ -200,19 +241,11 @@ export default class KeyBackupPanel extends React.PureComponent {
"Connect this session to key backup before signing out to avoid " +
"losing any keys that may only be on this session.",
)}</p>
</div>;
</>;
restoreButtonCaption = _t("Connect this session to Key Backup");
}
let keyStatus;
if (this.state.backupKeyStored === true) {
keyStatus = _t("in secret storage");
} else {
keyStatus = _t("not stored");
}
let uploadStatus;
const { sessionsRemaining } = this.state;
if (!MatrixClientPeg.get().getKeyBackupEnabled()) {
// No upload status to show when backup disabled.
uploadStatus = "";
@ -226,17 +259,17 @@ export default class KeyBackupPanel extends React.PureComponent {
</div>;
}
let backupSigStatuses = this.state.backupSigStatus.sigs.map((sig, i) => {
let backupSigStatuses = backupSigStatus.sigs.map((sig, i) => {
const deviceName = sig.device ? (sig.device.getDisplayName() || sig.device.deviceId) : null;
const validity = sub =>
<span className={sig.valid ? 'mx_KeyBackupPanel_sigValid' : 'mx_KeyBackupPanel_sigInvalid'}>
<span className={sig.valid ? 'mx_SecureBackupPanel_sigValid' : 'mx_SecureBackupPanel_sigInvalid'}>
{sub}
</span>;
const verify = sub =>
<span className={sig.device && sig.deviceTrust.isVerified() ? 'mx_KeyBackupPanel_deviceVerified' : 'mx_KeyBackupPanel_deviceNotVerified'}>
<span className={sig.device && sig.deviceTrust.isVerified() ? 'mx_SecureBackupPanel_deviceVerified' : 'mx_SecureBackupPanel_deviceNotVerified'}>
{sub}
</span>;
const device = sub => <span className="mx_KeyBackupPanel_deviceName">{deviceName}</span>;
const device = sub => <span className="mx_SecureBackupPanel_deviceName">{deviceName}</span>;
const fromThisDevice = (
sig.device &&
sig.device.getFingerprint() === MatrixClientPeg.get().getDeviceEd25519Key()
@ -307,60 +340,123 @@ export default class KeyBackupPanel extends React.PureComponent {
{sigStatus}
</div>;
});
if (this.state.backupSigStatus.sigs.length === 0) {
if (backupSigStatus.sigs.length === 0) {
backupSigStatuses = _t("Backup is not signed by any of your sessions");
}
let trustedLocally;
if (this.state.backupSigStatus.trusted_locally) {
if (backupSigStatus.trusted_locally) {
trustedLocally = _t("This backup is trusted because it has been restored on this session");
}
let deleteBackupButton;
if (!isSecureBackupRequired()) {
deleteBackupButton = <AccessibleButton kind="danger" onClick={this._deleteBackup}>
{_t("Delete Backup")}
</AccessibleButton>;
}
extraDetailsTableRows = <>
<tr>
<td>{_t("Backup version:")}</td>
<td>{backupInfo.version}</td>
</tr>
<tr>
<td>{_t("Algorithm:")}</td>
<td>{backupInfo.algorithm}</td>
</tr>
</>;
const buttonRow = (
<div className="mx_KeyBackupPanel_buttonRow">
<AccessibleButton kind="primary" onClick={this._restoreBackup}>
{restoreButtonCaption}
</AccessibleButton>&nbsp;&nbsp;&nbsp;
{deleteBackupButton}
</div>
extraDetails = <>
{uploadStatus}
<div>{backupSigStatuses}</div>
<div>{trustedLocally}</div>
</>;
actions.push(
<AccessibleButton key="restore" kind="primary" onClick={this._restoreBackup}>
{restoreButtonCaption}
</AccessibleButton>,
);
return <div>
<div>{clientBackupStatus}</div>
<details>
<summary>{_t("Advanced")}</summary>
<div>{_t("Backup version: ")}{this.state.backupInfo.version}</div>
<div>{_t("Algorithm: ")}{this.state.backupInfo.algorithm}</div>
<div>{_t("Backup key stored: ")}{keyStatus}</div>
{uploadStatus}
<div>{backupSigStatuses}</div>
<div>{trustedLocally}</div>
</details>
{buttonRow}
</div>;
if (!isSecureBackupRequired()) {
actions.push(
<AccessibleButton key="delete" kind="danger" onClick={this._deleteBackup}>
{_t("Delete Backup")}
</AccessibleButton>,
);
}
} else {
return <div>
<div>
<p>{_t(
"Your keys are <b>not being backed up from this session</b>.", {},
{b: sub => <b>{sub}</b>},
)}</p>
<p>{encryptedMessageAreEncrypted}</p>
<p>{_t("Back up your keys before signing out to avoid losing them.")}</p>
</div>
<div className="mx_KeyBackupPanel_buttonRow">
<AccessibleButton kind="primary" onClick={this._startNewBackup}>
{_t("Start using Key Backup")}
</AccessibleButton>
</div>
statusDescription = <>
<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>
</>;
actions.push(
<AccessibleButton key="setup" kind="primary" onClick={this._startNewBackup}>
{_t("Set up")}
</AccessibleButton>,
);
}
if (secretStorageKeyInAccount) {
actions.push(
<AccessibleButton key="reset" kind="danger" onClick={this._resetSecretStorage}>
{_t("Reset")}
</AccessibleButton>,
);
}
let backupKeyWellFormedText = "";
if (backupKeyCached) {
backupKeyWellFormedText = ", ";
if (backupKeyWellFormed) {
backupKeyWellFormedText += _t("well formed");
} else {
backupKeyWellFormedText += _t("unexpected type");
}
}
let actionRow;
if (actions.length) {
actionRow = <div className="mx_SecureBackupPanel_buttonRow">
{actions}
</div>;
}
return (
<div>
<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 Recovery Key.",
)}</p>
{statusDescription}
<details>
<summary>{_t("Advanced")}</summary>
<table className="mx_SecureBackupPanel_statusList"><tbody>
<tr>
<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>
{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>
</tr>
<tr>
<td>{_t("Secret storage:")}</td>
<td>{secretStorageReady ? _t("ready") : _t("not ready")}</td>
</tr>
{extraDetailsTableRows}
</tbody></table>
{extraDetails}
</details>
{actionRow}
</div>
);
}
}

View file

@ -46,9 +46,10 @@ export default class BridgeSettingsTab extends React.Component<IProps> {
const client = MatrixClientPeg.get();
const roomState = client.getRoom(roomId).currentState;
return [].concat(...BRIDGE_EVENT_TYPES.map((typeName) =>
Array.from(roomState.events.get(typeName).values()),
));
return BRIDGE_EVENT_TYPES.map(typeName => {
const events = roomState.events.get(typeName);
return events ? Array.from(events.values()) : [];
}).flat(1);
}
render() {

View file

@ -22,6 +22,8 @@ import * as sdk from "../../../../..";
import AccessibleButton from "../../../elements/AccessibleButton";
import dis from "../../../../../dispatcher/dispatcher";
import MatrixClientContext from "../../../../../contexts/MatrixClientContext";
import SettingsStore from "../../../../../settings/SettingsStore";
import {UIFeature} from "../../../../../settings/UIFeature";
export default class GeneralRoomSettingsTab extends React.Component {
static propTypes = {
@ -61,6 +63,28 @@ export default class GeneralRoomSettingsTab extends React.Component {
const canChangeGroups = room.currentState.mayClientSendStateEvent("m.room.related_groups", client);
const groupsEvent = room.currentState.getStateEvents("m.room.related_groups", "");
let urlPreviewSettings = <>
<span className='mx_SettingsTab_subheading'>{_t("URL Previews")}</span>
<div className='mx_SettingsTab_section'>
<UrlPreviewSettings room={room} />
</div>
</>;
if (!SettingsStore.getValue(UIFeature.URLPreviews)) {
urlPreviewSettings = null;
}
let flairSection;
if (SettingsStore.getValue(UIFeature.Flair)) {
flairSection = <>
<span className='mx_SettingsTab_subheading'>{_t("Flair")}</span>
<div className='mx_SettingsTab_section mx_SettingsTab_subsectionText'>
<RelatedGroupSettings roomId={room.roomId}
canSetRelatedGroups={canChangeGroups}
relatedGroupsEvent={groupsEvent} />
</div>
</>;
}
return (
<div className="mx_SettingsTab mx_GeneralRoomSettingsTab">
<div className="mx_SettingsTab_heading">{_t("General")}</div>
@ -75,17 +99,8 @@ export default class GeneralRoomSettingsTab extends React.Component {
canonicalAliasEvent={canonicalAliasEv} aliasEvents={aliasEvents} />
</div>
<div className="mx_SettingsTab_heading">{_t("Other")}</div>
<span className='mx_SettingsTab_subheading'>{_t("Flair")}</span>
<div className='mx_SettingsTab_section mx_SettingsTab_subsectionText'>
<RelatedGroupSettings roomId={room.roomId}
canSetRelatedGroups={canChangeGroups}
relatedGroupsEvent={groupsEvent} />
</div>
<span className='mx_SettingsTab_subheading'>{_t("URL Previews")}</span>
<div className='mx_SettingsTab_section'>
<UrlPreviewSettings room={room} />
</div>
{ flairSection }
{ urlPreviewSettings }
<span className='mx_SettingsTab_subheading'>{_t("Leave room")}</span>
<div className='mx_SettingsTab_section'>

View file

@ -239,7 +239,7 @@ export default class RolesRoomSettingsTab extends React.Component {
defaultValue: 50,
},
"redact": {
desc: _t('Remove messages'),
desc: _t('Remove messages sent by others'),
defaultValue: 50,
},
"notifications.room": {

View file

@ -24,6 +24,8 @@ import Modal from "../../../../../Modal";
import QuestionDialog from "../../../dialogs/QuestionDialog";
import StyledRadioGroup from '../../../elements/StyledRadioGroup';
import {SettingLevel} from "../../../../../settings/SettingLevel";
import SettingsStore from "../../../../../settings/SettingsStore";
import {UIFeature} from "../../../../../settings/UIFeature";
export default class SecurityRoomSettingsTab extends React.Component {
static propTypes = {
@ -340,10 +342,23 @@ export default class SecurityRoomSettingsTab extends React.Component {
const canEnableEncryption = !isEncrypted && hasEncryptionPermission;
let encryptionSettings = null;
if (isEncrypted) {
encryptionSettings = <SettingsFlag name="blacklistUnverifiedDevices" level={SettingLevel.ROOM_DEVICE}
onChange={this._updateBlacklistDevicesFlag}
roomId={this.props.roomId} />;
if (isEncrypted && SettingsStore.isEnabled("blacklistUnverifiedDevices")) {
encryptionSettings = <SettingsFlag
name="blacklistUnverifiedDevices"
level={SettingLevel.ROOM_DEVICE}
onChange={this._updateBlacklistDevicesFlag}
roomId={this.props.roomId}
/>;
}
let historySection = (<>
<span className='mx_SettingsTab_subheading'>{_t("Who can read history?")}</span>
<div className='mx_SettingsTab_section mx_SettingsTab_subsectionText'>
{this._renderHistory()}
</div>
</>);
if (!SettingsStore.getValue(UIFeature.RoomHistorySettings)) {
historySection = null;
}
return (
@ -367,10 +382,7 @@ export default class SecurityRoomSettingsTab extends React.Component {
{this._renderRoomAccess()}
</div>
<span className='mx_SettingsTab_subheading'>{_t("Who can read history?")}</span>
<div className='mx_SettingsTab_section mx_SettingsTab_subsectionText'>
{this._renderHistory()}
</div>
{historySection}
</div>
);
}

View file

@ -36,6 +36,7 @@ import EventTilePreview from '../../../elements/EventTilePreview';
import StyledRadioGroup from "../../../elements/StyledRadioGroup";
import classNames from 'classnames';
import { SettingLevel } from "../../../../../settings/SettingLevel";
import {UIFeature} from "../../../../../settings/UIFeature";
interface IProps {
}
@ -386,6 +387,8 @@ export default class AppearanceUserSettingsTab extends React.Component<IProps, I
};
private renderAdvancedSection() {
if (!SettingsStore.getValue(UIFeature.AdvancedSettings)) return null;
const brand = SdkConfig.get().brand;
const toggle = <div
className="mx_AppearanceUserSettingsTab_AdvancedToggle"

View file

@ -37,6 +37,7 @@ import {abbreviateUrl} from "../../../../../utils/UrlUtils";
import { getThreepidsWithBindStatus } from '../../../../../boundThreepids';
import Spinner from "../../../elements/Spinner";
import {SettingLevel} from "../../../../../settings/SettingLevel";
import {UIFeature} from "../../../../../settings/UIFeature";
export default class GeneralUserSettingsTab extends React.Component {
static propTypes = {
@ -220,7 +221,6 @@ export default class GeneralUserSettingsTab extends React.Component {
_renderProfileSection() {
return (
<div className="mx_SettingsTab_section">
<span className="mx_SettingsTab_subheading">{_t("Profile")}</span>
<ProfileSettings />
</div>
);
@ -247,7 +247,9 @@ export default class GeneralUserSettingsTab extends React.Component {
// validate 3PID ownership even if we're just adding to the homeserver only.
// For newer homeservers with separate 3PID add and bind methods (MSC2290),
// there is no such concern, so we can always show the HS account 3PIDs.
if (this.state.haveIdServer || this.state.serverSupportsSeparateAddAndBind === true) {
if (SettingsStore.getValue(UIFeature.ThirdPartyID) &&
(this.state.haveIdServer || this.state.serverSupportsSeparateAddAndBind === true)
) {
const emails = this.state.loading3pids
? <Spinner />
: <EmailAddresses
@ -366,6 +368,8 @@ export default class GeneralUserSettingsTab extends React.Component {
}
_renderIntegrationManagerSection() {
if (!SettingsStore.getValue(UIFeature.Widgets)) return null;
const SetIntegrationManager = sdk.getComponent("views.settings.SetIntegrationManager");
return (
@ -383,17 +387,31 @@ export default class GeneralUserSettingsTab extends React.Component {
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()}
</>;
}
let discoverySection;
if (SettingsStore.getValue(UIFeature.IdentityServer)) {
discoverySection = <>
<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()}
<div className="mx_SettingsTab_heading">{discoWarning} {_t("Discovery")}</div>
{this._renderDiscoverySection()}
{ discoverySection }
{this._renderIntegrationManagerSection() /* Has its own title */}
<div className="mx_SettingsTab_heading">{_t("Deactivate account")}</div>
{this._renderManagementSection()}
{ accountManagementSection }
</div>
);
}

View file

@ -204,9 +204,9 @@ export default class HelpUserSettingsTab extends React.Component {
updateButton = <UpdateCheckButton />;
}
return (
<div className="mx_SettingsTab mx_HelpUserSettingsTab">
<div className="mx_SettingsTab_heading">{_t("Help & About")}</div>
let bugReportingSection;
if (SdkConfig.get().bug_report_endpoint_url) {
bugReportingSection = (
<div className="mx_SettingsTab_section">
<span className='mx_SettingsTab_subheading'>{_t('Bug reporting')}</span>
<div className='mx_SettingsTab_subsectionText'>
@ -223,22 +223,24 @@ export default class HelpUserSettingsTab extends React.Component {
{_t("Submit debug logs")}
</AccessibleButton>
</div>
<div className='mx_HelpUserSettingsTab_debugButton'>
<AccessibleButton onClick={this._onClearCacheAndReload} kind='danger'>
{_t("Clear cache and reload")}
</AccessibleButton>
</div>
{
_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>
);
}
return (
<div className="mx_SettingsTab mx_HelpUserSettingsTab">
<div className="mx_SettingsTab_heading">{_t("Help & About")}</div>
{ bugReportingSection }
<div className='mx_SettingsTab_section'>
<span className='mx_SettingsTab_subheading'>{_t("FAQ")}</span>
<div className='mx_SettingsTab_subsectionText'>
@ -268,6 +270,11 @@ export default class HelpUserSettingsTab extends React.Component {
data-spoiler={MatrixClientPeg.get().getAccessToken()}>
&lt;{ _t("click to reveal") }&gt;
</AccessibleButton>
<div className='mx_HelpUserSettingsTab_debugButton'>
<AccessibleButton onClick={this._onClearCacheAndReload} kind='danger'>
{_t("Clear cache and reload")}
</AccessibleButton>
</div>
</div>
</div>
</div>

View file

@ -50,11 +50,10 @@ export default class PreferencesUserSettingsTab extends React.Component {
'showAvatarChanges',
'showDisplaynameChanges',
'showImages',
'Pill.shouldShowPillAvatar',
];
static ADVANCED_SETTINGS = [
'alwaysShowEncryptionIcons',
'Pill.shouldShowPillAvatar',
static GENERAL_SETTINGS = [
'TagPanel.enableTagPanel',
'promptBeforeInviteUnknownUsers',
// Start automatically after startup (electron-only)
@ -140,7 +139,9 @@ export default class PreferencesUserSettingsTab extends React.Component {
_renderGroup(settingIds) {
const SettingsFlag = sdk.getComponent("views.elements.SettingsFlag");
return settingIds.map(i => <SettingsFlag key={i} name={i} level={SettingLevel.ACCOUNT} />);
return settingIds.filter(SettingsStore.isEnabled).map(i => {
return <SettingsFlag key={i} name={i} level={SettingLevel.ACCOUNT} />;
});
}
render() {
@ -188,8 +189,8 @@ export default class PreferencesUserSettingsTab extends React.Component {
</div>
<div className="mx_SettingsTab_section">
<span className="mx_SettingsTab_subheading">{_t("Advanced")}</span>
{this._renderGroup(PreferencesUserSettingsTab.ADVANCED_SETTINGS)}
<span className="mx_SettingsTab_subheading">{_t("General")}</span>
{this._renderGroup(PreferencesUserSettingsTab.GENERAL_SETTINGS)}
{minimizeToTrayOption}
{autoHideMenuOption}
{autoLaunchOption}

View file

@ -29,6 +29,11 @@ import {sleep} from "../../../../../utils/promise";
import dis from "../../../../../dispatcher/dispatcher";
import {privateShouldBeEncrypted} from "../../../../../createRoom";
import {SettingLevel} from "../../../../../settings/SettingLevel";
import SecureBackupPanel from "../../SecureBackupPanel";
import SettingsStore from "../../../../../settings/SettingsStore";
import {UIFeature} from "../../../../../settings/UIFeature";
import {isE2eAdvancedPanelPossible} from "../../E2eAdvancedPanel";
import CountlyAnalytics from "../../../../../CountlyAnalytics";
export class IgnoredUser extends React.Component {
static propTypes = {
@ -98,18 +103,19 @@ export default class SecurityUserSettingsTab extends React.Component {
_updateAnalytics = (checked) => {
checked ? Analytics.enable() : Analytics.disable();
CountlyAnalytics.instance.enable(/* anonymous = */ !checked);
};
_onExportE2eKeysClicked = () => {
Modal.createTrackedDialogAsync('Export E2E Keys', '',
import('../../../../../async-components/views/dialogs/ExportE2eKeysDialog'),
import('../../../../../async-components/views/dialogs/security/ExportE2eKeysDialog'),
{matrixClient: MatrixClientPeg.get()},
);
};
_onImportE2eKeysClicked = () => {
Modal.createTrackedDialogAsync('Import E2E Keys', '',
import('../../../../../async-components/views/dialogs/ImportE2eKeysDialog'),
import('../../../../../async-components/views/dialogs/security/ImportE2eKeysDialog'),
{matrixClient: MatrixClientPeg.get()},
);
};
@ -216,6 +222,15 @@ export default class SecurityUserSettingsTab extends React.Component {
);
}
let noSendUnverifiedSetting;
if (SettingsStore.isEnabled("blacklistUnverifiedDevices")) {
noSendUnverifiedSetting = <SettingsFlag
name='blacklistUnverifiedDevices'
level={SettingLevel.DEVICE}
onChange={this._updateBlacklistDevicesFlag}
/>;
}
return (
<div className='mx_SettingsTab_section'>
<span className='mx_SettingsTab_subheading'>{_t("Cryptography")}</span>
@ -230,8 +245,7 @@ export default class SecurityUserSettingsTab extends React.Component {
</li>
</ul>
{importExportButtons}
<SettingsFlag name='blacklistUnverifiedDevices' level={SettingLevel.DEVICE}
onChange={this._updateBlacklistDevicesFlag} />
{noSendUnverifiedSetting}
</div>
);
}
@ -288,12 +302,11 @@ export default class SecurityUserSettingsTab extends React.Component {
const SettingsFlag = sdk.getComponent('views.elements.SettingsFlag');
const EventIndexPanel = sdk.getComponent('views.settings.EventIndexPanel');
const KeyBackupPanel = sdk.getComponent('views.settings.KeyBackupPanel');
const keyBackup = (
const secureBackup = (
<div className='mx_SettingsTab_section'>
<span className="mx_SettingsTab_subheading">{_t("Key backup")}</span>
<span className="mx_SettingsTab_subheading">{_t("Secure Backup")}</span>
<div className='mx_SettingsTab_subsectionText'>
<KeyBackupPanel />
<SecureBackupPanel />
</div>
</div>
);
@ -311,15 +324,13 @@ export default class SecurityUserSettingsTab extends React.Component {
// can remove this.
const CrossSigningPanel = sdk.getComponent('views.settings.CrossSigningPanel');
const crossSigning = (
<div className='mx_SettingsTab_section'>
<span className="mx_SettingsTab_subheading">{_t("Cross-signing")}</span>
<div className='mx_SettingsTab_subsectionText'>
<CrossSigningPanel />
</div>
<div className='mx_SettingsTab_section'>
<span className="mx_SettingsTab_subheading">{_t("Cross-signing")}</span>
<div className='mx_SettingsTab_subsectionText'>
<CrossSigningPanel />
</div>
);
const E2eAdvancedPanel = sdk.getComponent('views.settings.E2eAdvancedPanel');
</div>
);
let warning;
if (!privateShouldBeEncrypted()) {
@ -329,6 +340,48 @@ export default class SecurityUserSettingsTab extends React.Component {
</div>;
}
let privacySection;
if (Analytics.canEnable() || CountlyAnalytics.instance.canEnable()) {
privacySection = <React.Fragment>
<div className="mx_SettingsTab_heading">{_t("Privacy")}</div>
<div className="mx_SettingsTab_section">
<span className="mx_SettingsTab_subheading">{_t("Analytics")}</span>
<div className="mx_SettingsTab_subsectionText">
{_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.")}
<AccessibleButton className="mx_SettingsTab_linkBtn" onClick={Analytics.showDetailsModal}>
{_t("Learn more about how we use analytics.")}
</AccessibleButton>
</div>
<SettingsFlag name="analyticsOptIn" level={SettingLevel.DEVICE} onChange={this._updateAnalytics} />
</div>
</React.Fragment>;
}
const E2eAdvancedPanel = sdk.getComponent('views.settings.E2eAdvancedPanel');
let advancedSection;
if (SettingsStore.getValue(UIFeature.AdvancedSettings)) {
const ignoreUsersPanel = this._renderIgnoredUsers();
const invitesPanel = this._renderManageInvites();
const e2ePanel = isE2eAdvancedPanelPossible() ? <E2eAdvancedPanel /> : null;
// 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_section">
{ignoreUsersPanel}
{invitesPanel}
{e2ePanel}
</div>
</>;
}
}
return (
<div className="mx_SettingsTab mx_SecurityUserSettingsTab">
{warning}
@ -352,35 +405,13 @@ export default class SecurityUserSettingsTab extends React.Component {
</div>
<div className="mx_SettingsTab_heading">{_t("Encryption")}</div>
<div className="mx_SettingsTab_section">
{keyBackup}
{secureBackup}
{eventIndex}
{crossSigning}
{this._renderCurrentDeviceInfo()}
</div>
<div className="mx_SettingsTab_heading">{_t("Privacy")}</div>
<div className="mx_SettingsTab_section">
<span className="mx_SettingsTab_subheading">{_t("Analytics")}</span>
<div className='mx_SettingsTab_subsectionText'>
{_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.")}
<AccessibleButton className="mx_SettingsTab_linkBtn" onClick={Analytics.showDetailsModal}>
{_t("Learn more about how we use analytics.")}
</AccessibleButton>
</div>
<SettingsFlag name='analyticsOptIn' level={SettingLevel.DEVICE}
onChange={this._updateAnalytics} />
</div>
<div className="mx_SettingsTab_heading">{_t("Advanced")}</div>
<div className="mx_SettingsTab_section">
{this._renderIgnoredUsers()}
{this._renderManageInvites()}
<E2eAdvancedPanel />
</div>
{ privacySection }
{ advancedSection }
</div>
);
}