Merge branch 'develop' into ctrl-enter-send
This commit is contained in:
commit
1346416d20
451 changed files with 25135 additions and 10894 deletions
|
@ -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>;
|
||||
};
|
||||
|
||||
|
|
|
@ -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 />
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
*/
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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}>
|
||||
|
|
|
@ -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} />;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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 can’t be displayed here:') } { externalKeywords }</li>);
|
||||
externalRules.push(<li>
|
||||
{_t('Notifications on the following keywords follow rules which can’t be displayed here:') }
|
||||
{ externalKeywords }
|
||||
</li>);
|
||||
}
|
||||
|
||||
let devicesSection;
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
{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>
|
||||
);
|
||||
}
|
||||
}
|
|
@ -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() {
|
||||
|
|
|
@ -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'>
|
||||
|
|
|
@ -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": {
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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()}>
|
||||
<{ _t("click to reveal") }>
|
||||
</AccessibleButton>
|
||||
<div className='mx_HelpUserSettingsTab_debugButton'>
|
||||
<AccessibleButton onClick={this._onClearCacheAndReload} kind='danger'>
|
||||
{_t("Clear cache and reload")}
|
||||
</AccessibleButton>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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 },
|
||||
)}
|
||||
|
||||
{_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 },
|
||||
)}
|
||||
|
||||
{_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>
|
||||
);
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue