Merge remote-tracking branch 'upstream/develop' into feature_confetti#14676

This commit is contained in:
Steffen Kolmer 2020-10-19 13:15:33 +02:00
commit c86964cd5e
478 changed files with 21997 additions and 13673 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

@ -24,6 +24,7 @@ import {makeUserPermalink} from "../../../utils/permalinks/Permalinks";
import BaseAvatar from "../avatars/BaseAvatar";
import AccessibleButton from "../elements/AccessibleButton";
import {replaceableComponent} from "../../../utils/replaceableComponent";
import SettingsStore from "../../../settings/SettingsStore";
@replaceableComponent("views.settings.BridgeTile")
export default class BridgeTile extends React.PureComponent {
@ -56,7 +57,7 @@ export default class BridgeTile extends React.PureComponent {
type={Pill.TYPE_USER_MENTION}
room={this.props.room}
url={makeUserPermalink(content.creator)}
shouldShowPillAvatar={true}
shouldShowPillAvatar={SettingsStore.getValue("Pill.shouldShowPillAvatar")}
/>,
});
}
@ -66,7 +67,7 @@ export default class BridgeTile extends React.PureComponent {
type={Pill.TYPE_USER_MENTION}
room={this.props.room}
url={makeUserPermalink(this.props.ev.getSender())}
shouldShowPillAvatar={true}
shouldShowPillAvatar={SettingsStore.getValue("Pill.shouldShowPillAvatar")}
/>,
});

View file

@ -16,14 +16,12 @@ limitations under the License.
import React from 'react';
import PropTypes from 'prop-types';
import createReactClass from 'create-react-class';
import {MatrixClientPeg} from "../../../MatrixClientPeg";
import * as sdk from '../../../index';
import { _t } from '../../../languageHandler';
export default createReactClass({
displayName: 'ChangeAvatar',
propTypes: {
export default class ChangeAvatar extends React.Component {
static propTypes = {
initialAvatarUrl: PropTypes.string,
room: PropTypes.object,
// if false, you need to call changeAvatar.onFileSelected yourself.
@ -31,36 +29,36 @@ export default createReactClass({
width: PropTypes.number,
height: PropTypes.number,
className: PropTypes.string,
},
};
Phases: {
static Phases = {
Display: "display",
Uploading: "uploading",
Error: "error",
},
};
getDefaultProps: function() {
return {
showUploadSection: true,
className: "",
width: 80,
height: 80,
};
},
static defaultProps = {
showUploadSection: true,
className: "",
width: 80,
height: 80,
};
getInitialState: function() {
return {
constructor(props) {
super(props);
this.state = {
avatarUrl: this.props.initialAvatarUrl,
phase: this.Phases.Display,
phase: ChangeAvatar.Phases.Display,
};
},
}
componentDidMount: function() {
componentDidMount() {
MatrixClientPeg.get().on("RoomState.events", this.onRoomStateEvents);
},
}
// TODO: [REACT-WARNING] Replace with appropriate lifecycle event
UNSAFE_componentWillReceiveProps: function(newProps) {
UNSAFE_componentWillReceiveProps(newProps) {
if (this.avatarSet) {
// don't clobber what the user has just set
return;
@ -68,15 +66,15 @@ export default createReactClass({
this.setState({
avatarUrl: newProps.initialAvatarUrl,
});
},
}
componentWillUnmount: function() {
componentWillUnmount() {
if (MatrixClientPeg.get()) {
MatrixClientPeg.get().removeListener("RoomState.events", this.onRoomStateEvents);
}
},
}
onRoomStateEvents: function(ev) {
onRoomStateEvents = (ev) => {
if (!this.props.room) {
return;
}
@ -90,13 +88,13 @@ export default createReactClass({
this.avatarSet = false;
this.setState({}); // force update
}
},
};
setAvatarFromFile: function(file) {
setAvatarFromFile(file) {
let newUrl = null;
this.setState({
phase: this.Phases.Uploading,
phase: ChangeAvatar.Phases.Uploading,
});
const self = this;
const httpPromise = MatrixClientPeg.get().uploadContent(file).then(function(url) {
@ -115,31 +113,31 @@ export default createReactClass({
httpPromise.then(function() {
self.setState({
phase: self.Phases.Display,
phase: ChangeAvatar.Phases.Display,
avatarUrl: MatrixClientPeg.get().mxcUrlToHttp(newUrl),
});
}, function(error) {
self.setState({
phase: self.Phases.Error,
phase: ChangeAvatar.Phases.Error,
});
self.onError(error);
});
return httpPromise;
},
}
onFileSelected: function(ev) {
onFileSelected = (ev) => {
this.avatarSet = true;
return this.setAvatarFromFile(ev.target.files[0]);
},
};
onError: function(error) {
onError = (error) => {
this.setState({
errorText: _t("Failed to upload profile picture!"),
});
},
};
render: function() {
render() {
let avatarImg;
// Having just set an avatar we just display that since it will take a little
// time to propagate through to the RoomAvatar.
@ -165,8 +163,8 @@ export default createReactClass({
}
switch (this.state.phase) {
case this.Phases.Display:
case this.Phases.Error:
case ChangeAvatar.Phases.Display:
case ChangeAvatar.Phases.Error:
return (
<div>
<div className={this.props.className}>
@ -175,11 +173,11 @@ export default createReactClass({
{ uploadSection }
</div>
);
case this.Phases.Uploading:
case ChangeAvatar.Phases.Uploading:
var Loader = sdk.getComponent("elements.Spinner");
return (
<Loader />
);
}
},
});
}
}

View file

@ -17,15 +17,12 @@ limitations under the License.
*/
import React from 'react';
import createReactClass from 'create-react-class';
import * as sdk from '../../../index';
import {MatrixClientPeg} from '../../../MatrixClientPeg';
import { _t } from '../../../languageHandler';
export default createReactClass({
displayName: 'ChangeDisplayName',
_getDisplayName: async function() {
export default class ChangeDisplayName extends React.Component {
_getDisplayName = async () => {
const cli = MatrixClientPeg.get();
try {
const res = await cli.getProfileInfo(cli.getUserId());
@ -33,16 +30,16 @@ export default createReactClass({
} catch (e) {
throw new Error("Failed to fetch display name");
}
},
};
_changeDisplayName: function(newDisplayname) {
_changeDisplayName = (newDisplayname) => {
const cli = MatrixClientPeg.get();
return cli.setDisplayName(newDisplayname).catch(function(e) {
throw new Error("Failed to set display name", e);
});
},
};
render: function() {
render() {
const EditableTextContainer = sdk.getComponent('elements.EditableTextContainer');
return (
<EditableTextContainer
@ -51,5 +48,5 @@ export default createReactClass({
blurToSubmit={true}
onSubmit={this._changeDisplayName} />
);
},
});
}
}

View file

@ -18,88 +18,58 @@ limitations under the License.
import Field from "../elements/Field";
import React from 'react';
import PropTypes from 'prop-types';
import createReactClass from 'create-react-class';
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 createReactClass({
displayName: 'ChangePassword',
propTypes: {
export default class ChangePassword extends React.Component {
static propTypes = {
onFinished: PropTypes.func,
onError: PropTypes.func,
onCheckPassword: PropTypes.func,
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,
},
};
Phases: {
static Phases = {
Edit: "edit",
Uploading: "uploading",
Error: "error",
},
};
getDefaultProps: function() {
return {
onFinished: function() {},
onError: function() {},
onCheckPassword: function(oldPass, newPass, confirmPass) {
if (newPass !== confirmPass) {
return {
error: _t("New passwords don't match"),
};
} else if (!newPass || newPass.length === 0) {
return {
error: _t("Passwords can't be empty"),
};
}
},
confirm: true,
};
},
static defaultProps = {
onFinished() {},
onError() {},
onCheckPassword(oldPass, newPass, confirmPass) {
if (newPass !== confirmPass) {
return {
error: _t("New passwords don't match"),
};
} else if (!newPass || newPass.length === 0) {
return {
error: _t("Passwords can't be empty"),
};
}
},
confirm: true,
}
getInitialState: function() {
return {
phase: this.Phases.Edit,
cachedPassword: null,
oldPassword: "",
newPassword: "",
newPasswordConfirm: "",
};
},
state = {
phase: ChangePassword.Phases.Edit,
oldPassword: "",
newPassword: "",
newPasswordConfirm: "",
};
componentDidMount: function() {
this._sessionStore = sessionStore;
this._sessionStoreToken = this._sessionStore.addListener(
this._setStateFromSessionStore,
);
this._setStateFromSessionStore();
},
componentWillUnmount: function() {
if (this._sessionStoreToken) {
this._sessionStoreToken.remove();
}
},
_setStateFromSessionStore: function() {
this.setState({
cachedPassword: this._sessionStore.getCachedPassword(),
});
},
changePassword: function(oldPassword, newPassword) {
changePassword(oldPassword, newPassword) {
const cli = MatrixClientPeg.get();
if (!this.props.confirm) {
@ -125,8 +95,11 @@ export default createReactClass({
</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>,
],
@ -136,9 +109,9 @@ export default createReactClass({
}
},
});
},
}
_changePassword: function(cli, oldPassword, newPassword) {
_changePassword(cli, oldPassword, newPassword) {
const authDict = {
type: 'm.login.password',
identifier: {
@ -152,13 +125,10 @@ export default createReactClass({
};
this.setState({
phase: this.Phases.Uploading,
phase: ChangePassword.Phases.Uploading,
});
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({
@ -172,53 +142,53 @@ export default createReactClass({
this.props.onError(err);
}).finally(() => {
this.setState({
phase: this.Phases.Edit,
phase: ChangePassword.Phases.Edit,
oldPassword: "",
newPassword: "",
newPasswordConfirm: "",
});
});
},
}
_optionallySetEmail: function() {
_optionallySetEmail() {
// Ask for an email otherwise the user has no way to reset their password
const SetEmailDialog = sdk.getComponent("dialogs.SetEmailDialog");
const modal = Modal.createTrackedDialog('Do you want to set an email address?', '', SetEmailDialog, {
title: _t('Do you want to set an email address?'),
});
return modal.finished.then(([confirmed]) => confirmed);
},
}
_onExportE2eKeysClicked: function() {
_onExportE2eKeysClicked = () => {
Modal.createTrackedDialogAsync('Export E2E Keys', 'Change Password',
import('../../../async-components/views/dialogs/ExportE2eKeysDialog'),
import('../../../async-components/views/dialogs/security/ExportE2eKeysDialog'),
{
matrixClient: MatrixClientPeg.get(),
},
);
},
};
onChangeOldPassword(ev) {
onChangeOldPassword = (ev) => {
this.setState({
oldPassword: ev.target.value,
});
},
};
onChangeNewPassword(ev) {
onChangeNewPassword = (ev) => {
this.setState({
newPassword: ev.target.value,
});
},
};
onChangeNewPasswordConfirm(ev) {
onChangeNewPasswordConfirm = (ev) => {
this.setState({
newPasswordConfirm: ev.target.value,
});
},
};
onClickChange: function(ev) {
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(
@ -229,39 +199,30 @@ export default createReactClass({
} else {
this.changePassword(oldPassword, newPassword);
}
},
};
render: function() {
render() {
// TODO: Live validation on `new pw == confirm pw`
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 this.Phases.Edit:
const passwordLabel = this.state.cachedPassword ?
_t('Password') : _t('New Password');
case ChangePassword.Phases.Edit:
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}
@ -278,17 +239,16 @@ export default createReactClass({
/>
</div>
<AccessibleButton className={buttonClassName} kind={this.props.buttonKind} onClick={this.onClickChange}>
{ _t('Change Password') }
{ this.props.buttonLabel || _t('Change Password') }
</AccessibleButton>
</form>
);
case this.Phases.Uploading:
var Loader = sdk.getComponent("elements.Spinner");
case ChangePassword.Phases.Uploading:
return (
<div className="mx_Dialog_content">
<Loader />
<Spinner />
</div>
);
}
},
});
}
}

View file

@ -19,8 +19,10 @@ import React from 'react';
import {MatrixClientPeg} from '../../../MatrixClientPeg';
import { _t } from '../../../languageHandler';
import * as sdk from '../../../index';
import { accessSecretStorage } from '../../../CrossSigningManager';
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) {
@ -30,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,
};
}
@ -65,7 +67,7 @@ export default class CrossSigningPanel extends React.PureComponent {
};
_onBootstrapClick = () => {
this._bootstrapSecureSecretStorage(false);
this._bootstrapCrossSigning({ forceReset: false });
};
onStatusChanged = () => {
@ -82,10 +84,6 @@ 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();
@ -96,44 +94,55 @@ export default class CrossSigningPanel extends React.PureComponent {
masterPrivateKeyCached,
selfSigningPrivateKeyCached,
userSigningPrivateKeyCached,
sessionBackupKeyCached,
sessionBackupKeyWellFormed,
secretStorageKeyInAccount,
homeserverSupportsCrossSigning,
crossSigningReady,
});
}
/**
* 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 });
},
});
}
@ -146,9 +155,6 @@ export default class CrossSigningPanel extends React.PureComponent {
masterPrivateKeyCached,
selfSigningPrivateKeyCached,
userSigningPrivateKeyCached,
sessionBackupKeyCached,
sessionBackupKeyWellFormed,
secretStorageKeyInAccount,
homeserverSupportsCrossSigning,
crossSigningReady,
} = this.state;
@ -160,69 +166,65 @@ export default class CrossSigningPanel extends React.PureComponent {
let summarisedStatus;
if (homeserverSupportsCrossSigning === undefined) {
const InlineSpinner = sdk.getComponent('views.elements.InlineSpinner');
summarisedStatus = <p><InlineSpinner /></p>;
summarisedStatus = <Spinner />;
} else if (!homeserverSupportsCrossSigning) {
summarisedStatus = <p>{_t(
"Your homeserver does not support cross-signing.",
)}</p>;
} else if (crossSigningReady) {
summarisedStatus = <p> {_t(
"Cross-signing and secret storage are enabled.",
"Cross-signing is ready for use.",
)}</p>;
} else if (crossSigningPrivateKeysInStorage) {
summarisedStatus = <p>{_t(
"Your account has a cross-signing identity in secret storage, but it " +
"is not yet trusted by this session.",
"Your account has a cross-signing identity in secret storage, " +
"but it is not yet trusted by this session.",
)}</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 (
@ -237,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>
@ -251,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>
@ -269,8 +260,7 @@ export default class CrossSigningPanel extends React.PureComponent {
</tbody></table>
</details>
{errorSection}
{bootstrapButton}
{resetButton}
{actionRow}
</div>
);
}

View file

@ -19,13 +19,14 @@ 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";
const E2eAdvancedPanel = props => {
const SettingsFlag = sdk.getComponent('views.elements.SettingsFlag');
return <div className="mx_SettingsTab_section">
<span className="mx_SettingsTab_subheading">{_t("Advanced")}</span>
<span className="mx_SettingsTab_subheading">{_t("Encryption")}</span>
<SettingsFlag name={SETTING_MANUALLY_VERIFY_ALL_SESSIONS}
level={SettingLevel.DEVICE}
@ -37,3 +38,7 @@ const E2eAdvancedPanel = props => {
};
export default E2eAdvancedPanel;
export function isE2eAdvancedPanelPossible(): boolean {
return SettingsStore.isEnabled(SETTING_MANUALLY_VERIFY_ALL_SESSIONS);
}

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

@ -16,7 +16,6 @@ limitations under the License.
*/
import React from 'react';
import createReactClass from 'create-react-class';
import * as sdk from '../../../index';
import { _t } from '../../../languageHandler';
import {MatrixClientPeg} from '../../../MatrixClientPeg';
@ -65,46 +64,42 @@ function portLegacyActions(actions) {
}
}
export default createReactClass({
displayName: 'Notifications',
phases: {
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
},
};
getInitialState: function() {
return {
phase: this.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
};
},
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: function() {
componentDidMount() {
this._refreshFromServer();
},
}
onEnableNotificationsChange: function(checked) {
onEnableNotificationsChange = (checked) => {
const self = this;
this.setState({
phase: this.phases.LOADING,
phase: Notifications.phases.LOADING,
});
MatrixClientPeg.get().setPushRuleEnabled('global', self.state.masterPushRule.kind, self.state.masterPushRule.rule_id, !checked).then(function() {
self._refreshFromServer();
});
},
};
onEnableDesktopNotificationsChange: function(checked) {
onEnableDesktopNotificationsChange = (checked) => {
SettingsStore.setValue(
"notificationsEnabled", null,
SettingLevel.DEVICE,
@ -112,9 +107,9 @@ export default createReactClass({
).finally(() => {
this.forceUpdate();
});
},
};
onEnableDesktopNotificationBodyChange: function(checked) {
onEnableDesktopNotificationBodyChange = (checked) => {
SettingsStore.setValue(
"notificationBodyEnabled", null,
SettingLevel.DEVICE,
@ -122,9 +117,9 @@ export default createReactClass({
).finally(() => {
this.forceUpdate();
});
},
};
onEnableAudioNotificationsChange: function(checked) {
onEnableAudioNotificationsChange = (checked) => {
SettingsStore.setValue(
"audioNotificationsEnabled", null,
SettingLevel.DEVICE,
@ -132,7 +127,7 @@ export default createReactClass({
).finally(() => {
this.forceUpdate();
});
},
};
/*
* Returns the email pusher (pusher of type 'email') for a given
@ -140,7 +135,7 @@ export default createReactClass({
* pushers are unique over (app ID, pushkey), there will be at most
* one such pusher.
*/
getEmailPusher: function(pushers, address) {
getEmailPusher(pushers, address) {
if (pushers === undefined) {
return undefined;
}
@ -150,9 +145,9 @@ export default createReactClass({
}
}
return undefined;
},
}
onEnableEmailNotificationsChange: function(address, checked) {
onEnableEmailNotificationsChange = (address, checked) => {
let emailPusherPromise;
if (checked) {
const data = {};
@ -181,9 +176,9 @@ export default createReactClass({
description: _t('An error occurred whilst saving your email notification preferences.'),
});
});
},
};
onNotifStateButtonClicked: function(event) {
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];
@ -196,11 +191,9 @@ export default createReactClass({
this._setPushRuleVectorState(rule, newPushRuleVectorState);
}
}
},
onKeywordsClicked: function(event) {
const self = this;
};
onKeywordsClicked = (event) => {
// Compute the keywords list to display
let keywords = [];
for (const i in this.state.vectorContentRules.rules) {
@ -223,7 +216,7 @@ export default createReactClass({
description: _t('Enter keywords separated by a comma:'),
button: _t('OK'),
value: keywords,
onFinished: function onFinished(should_leave, newValue) {
onFinished: (should_leave, newValue) => {
if (should_leave && newValue !== keywords) {
let newKeywords = newValue.split(',');
for (const i in newKeywords) {
@ -238,25 +231,25 @@ export default createReactClass({
return array;
}, []);
self._setKeywords(newKeywords);
this._setKeywords(newKeywords);
}
},
});
},
};
getRule: function(vectorRuleId) {
getRule(vectorRuleId) {
for (const i in this.state.vectorPushRules) {
const rule = this.state.vectorPushRules[i];
if (rule.vectorRuleId === vectorRuleId) {
return rule;
}
}
},
}
_setPushRuleVectorState: function(rule, newPushRuleVectorState) {
_setPushRuleVectorState(rule, newPushRuleVectorState) {
if (rule && rule.vectorState !== newPushRuleVectorState) {
this.setState({
phase: this.phases.LOADING,
phase: Notifications.phases.LOADING,
});
const self = this;
@ -288,9 +281,9 @@ export default createReactClass({
});
});
}
},
}
_setKeywordsPushRuleVectorState: function(newPushRuleVectorState) {
_setKeywordsPushRuleVectorState(newPushRuleVectorState) {
// Is there really a change?
if (this.state.vectorContentRules.vectorState === newPushRuleVectorState
|| this.state.vectorContentRules.rules.length === 0) {
@ -301,7 +294,7 @@ export default createReactClass({
const cli = MatrixClientPeg.get();
this.setState({
phase: this.phases.LOADING,
phase: Notifications.phases.LOADING,
});
// Update all rules in self.state.vectorContentRules
@ -356,11 +349,11 @@ export default createReactClass({
onFinished: self._refreshFromServer,
});
});
},
}
_setKeywords: function(newKeywords) {
_setKeywords(newKeywords) {
this.setState({
phase: this.phases.LOADING,
phase: Notifications.phases.LOADING,
});
const self = this;
@ -440,19 +433,19 @@ export default createReactClass({
self._refreshFromServer();
}, onError);
}, onError);
},
}
// Create a push rule but disabled
_addDisabledPushRule: function(scope, kind, ruleId, body) {
_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: function(rulesets) {
_portRulesToNewAPI(rulesets) {
const needsUpdate = [];
const cli = MatrixClientPeg.get();
@ -485,9 +478,9 @@ export default createReactClass({
// Otherwise return the rules that we already have.
return rulesets;
}
},
}
_refreshFromServer: function() {
_refreshFromServer = () => {
const self = this;
const pushRulesPromise = MatrixClientPeg.get().getPushRules().then(self._portRulesToNewAPI).then(function(rulesets) {
/// XXX seriously? wtf is this?
@ -636,12 +629,12 @@ export default createReactClass({
Promise.all([pushRulesPromise, pushersPromise]).then(function() {
self.setState({
phase: self.phases.DISPLAY,
phase: Notifications.phases.DISPLAY,
});
}, function(error) {
console.error(error);
self.setState({
phase: self.phases.ERROR,
phase: Notifications.phases.ERROR,
});
}).finally(() => {
// actually explicitly update our state having been deep-manipulating it
@ -655,9 +648,9 @@ export default createReactClass({
});
MatrixClientPeg.get().getThreePids().then((r) => this.setState({threepids: r.threepids}));
},
};
_onClearNotifications: function() {
_onClearNotifications = () => {
const cli = MatrixClientPeg.get();
cli.getRooms().forEach(r => {
@ -666,9 +659,9 @@ export default createReactClass({
if (events.length) cli.sendReadReceipt(events.pop());
}
});
},
};
_updatePushRuleActions: function(rule, actions, enabled) {
_updatePushRuleActions(rule, actions, enabled) {
const cli = MatrixClientPeg.get();
return cli.setPushRuleActions(
@ -681,9 +674,9 @@ export default createReactClass({
);
}
});
},
}
renderNotifRulesTableRow: function(title, className, pushRuleVectorState) {
renderNotifRulesTableRow(title, className, pushRuleVectorState) {
return (
<tr key={ className }>
<th>
@ -712,9 +705,9 @@ export default createReactClass({
</th>
</tr>
);
},
}
renderNotifRulesTableRows: function() {
renderNotifRulesTableRows() {
const rows = [];
for (const i in this.state.vectorPushRules) {
const rule = this.state.vectorPushRules[i];
@ -726,9 +719,9 @@ export default createReactClass({
rows.push(this.renderNotifRulesTableRow(rule.description, rule.vectorRuleId, rule.vectorState));
}
return rows;
},
}
hasEmailPusher: function(pushers, address) {
hasEmailPusher(pushers, address) {
if (pushers === undefined) {
return false;
}
@ -738,17 +731,17 @@ export default createReactClass({
}
}
return false;
},
}
emailNotificationsRow: function(address, label) {
emailNotificationsRow(address, label) {
return <LabelledToggleSwitch value={this.hasEmailPusher(this.state.pushers, address)}
onChange={this.onEnableEmailNotificationsChange.bind(this, address)}
label={label} key={`emailNotif_${label}`} />;
},
}
render: function() {
render() {
let spinner;
if (this.state.phase === this.phases.LOADING) {
if (this.state.phase === Notifications.phases.LOADING) {
const Loader = sdk.getComponent("elements.Spinner");
spinner = <Loader />;
}
@ -910,5 +903,5 @@ export default createReactClass({
</div>
);
},
});
}
}

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,26 @@ 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) {
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) {
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 +151,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 +180,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,12 +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);
@ -30,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,
};
}
@ -72,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();
@ -134,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(
@ -154,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 " +
@ -199,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 = "";
@ -225,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()
@ -306,55 +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");
}
const buttonRow = (
<div className="mx_KeyBackupPanel_buttonRow">
<AccessibleButton kind="primary" onClick={this._restoreBackup}>
{restoreButtonCaption}
</AccessibleButton>&nbsp;&nbsp;&nbsp;
<AccessibleButton kind="danger" onClick={this._deleteBackup}>
{_t("Delete Backup")}
</AccessibleButton>
</div>
extraDetailsTableRows = <>
<tr>
<td>{_t("Backup version:")}</td>
<td>{backupInfo.version}</td>
</tr>
<tr>
<td>{_t("Algorithm:")}</td>
<td>{backupInfo.algorithm}</td>
</tr>
</>;
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

@ -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>,
});
}
}

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,7 @@ import Modal from "../../../../../Modal";
import QuestionDialog from "../../../dialogs/QuestionDialog";
import StyledRadioGroup from '../../../elements/StyledRadioGroup';
import {SettingLevel} from "../../../../../settings/SettingLevel";
import SettingsStore from "../../../../../settings/SettingsStore";
export default class SecurityRoomSettingsTab extends React.Component {
static propTypes = {
@ -340,10 +341,13 @@ 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}
/>;
}
return (

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 {
}
@ -170,7 +171,7 @@ export default class AppearanceUserSettingsTab extends React.Component<IProps, I
"baseFontSize",
null,
SettingLevel.DEVICE,
parseInt(value, 10) - FontWatcher.SIZE_DIFF
parseInt(value, 10) - FontWatcher.SIZE_DIFF,
);
return {valid: true, feedback: _t('Use between %(min)s pt and %(max)s pt', {min, max})};
@ -294,7 +295,7 @@ export default class AppearanceUserSettingsTab extends React.Component<IProps, I
/>
</div>
{customThemeForm}
</div>
</div>
);
}
@ -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 {
'showDisplaynameChanges',
'showImages',
'dontShowChatEffects',
'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,10 @@ 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";
export class IgnoredUser extends React.Component {
static propTypes = {
@ -102,14 +106,14 @@ export default class SecurityUserSettingsTab extends React.Component {
_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 +220,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 +243,7 @@ export default class SecurityUserSettingsTab extends React.Component {
</li>
</ul>
{importExportButtons}
<SettingsFlag name='blacklistUnverifiedDevices' level={SettingLevel.DEVICE}
onChange={this._updateBlacklistDevicesFlag} />
{noSendUnverifiedSetting}
</div>
);
}
@ -288,12 +300,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 +322,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,12 +338,53 @@ export default class SecurityUserSettingsTab extends React.Component {
</div>;
}
let privacySection;
if (Analytics.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}
<div className="mx_SettingsTab_heading">{_t("Security & Privacy")}</div>
<div className="mx_SettingsTab_heading">{_t("Where youre logged in")}</div>
<div className="mx_SettingsTab_section">
<span className="mx_SettingsTab_subheading">{_t("Where youre logged in")}</span>
<span>
{_t(
"Manage the names of and sign out of your sessions below or " +
@ -351,30 +401,15 @@ export default class SecurityUserSettingsTab extends React.Component {
<DevicesPanel />
</div>
</div>
{keyBackup}
{eventIndex}
{crossSigning}
{this._renderCurrentDeviceInfo()}
<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 className="mx_SettingsTab_heading">{_t("Encryption")}</div>
<div className="mx_SettingsTab_section">
{secureBackup}
{eventIndex}
{crossSigning}
{this._renderCurrentDeviceInfo()}
</div>
{this._renderIgnoredUsers()}
{this._renderManageInvites()}
<E2eAdvancedPanel />
{ privacySection }
{ advancedSection }
</div>
);
}