Merge branch 'develop' into hs/bridge-info
This commit is contained in:
commit
0a8cc416bf
203 changed files with 5331 additions and 2496 deletions
78
src/components/views/settings/AvatarSetting.js
Normal file
78
src/components/views/settings/AvatarSetting.js
Normal file
|
@ -0,0 +1,78 @@
|
|||
/*
|
||||
Copyright 2019 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import React, {useCallback} from "react";
|
||||
import PropTypes from "prop-types";
|
||||
|
||||
import sdk from "../../../index";
|
||||
import {_t} from "../../../languageHandler";
|
||||
import Modal from "../../../Modal";
|
||||
|
||||
const AvatarSetting = ({avatarUrl, avatarAltText, avatarName, uploadAvatar, removeAvatar}) => {
|
||||
const AccessibleButton = sdk.getComponent('elements.AccessibleButton');
|
||||
|
||||
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" />;
|
||||
if (avatarUrl) {
|
||||
avatarElement = (
|
||||
<AccessibleButton
|
||||
element="img"
|
||||
src={avatarUrl}
|
||||
alt={avatarAltText}
|
||||
aria-label={avatarAltText}
|
||||
onClick={openImageView} />
|
||||
);
|
||||
}
|
||||
|
||||
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">
|
||||
<div> </div>
|
||||
{_t("Upload")}
|
||||
</AccessibleButton>;
|
||||
}
|
||||
|
||||
let removeAvatarBtn;
|
||||
if (avatarUrl && removeAvatar) {
|
||||
removeAvatarBtn = <AccessibleButton onClick={removeAvatar} kind="link_sm">
|
||||
{_t("Remove")}
|
||||
</AccessibleButton>;
|
||||
}
|
||||
|
||||
return <div className="mx_AvatarSetting_avatar">
|
||||
{ avatarElement }
|
||||
{ uploadAvatarBtn }
|
||||
{ removeAvatarBtn }
|
||||
</div>;
|
||||
};
|
||||
|
||||
AvatarSetting.propTypes = {
|
||||
avatarUrl: PropTypes.string,
|
||||
avatarName: PropTypes.string.isRequired, // name of user/room the avatar belongs to
|
||||
uploadAvatar: PropTypes.func,
|
||||
removeAvatar: PropTypes.func,
|
||||
avatarAltText: PropTypes.string.isRequired,
|
||||
};
|
||||
|
||||
export default AvatarSetting;
|
169
src/components/views/settings/CrossSigningPanel.js
Normal file
169
src/components/views/settings/CrossSigningPanel.js
Normal file
|
@ -0,0 +1,169 @@
|
|||
/*
|
||||
Copyright 2019 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
|
||||
import MatrixClientPeg from '../../../MatrixClientPeg';
|
||||
import { _t } from '../../../languageHandler';
|
||||
import sdk from '../../../index';
|
||||
import { accessSecretStorage } from '../../../CrossSigningManager';
|
||||
|
||||
export default class CrossSigningPanel extends React.PureComponent {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this._unmounted = false;
|
||||
|
||||
this.state = {
|
||||
error: null,
|
||||
...this._getUpdatedStatus(),
|
||||
};
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
const cli = MatrixClientPeg.get();
|
||||
cli.on("accountData", this.onAccountData);
|
||||
cli.on("userTrustStatusChanged", this.onStatusChanged);
|
||||
cli.on("crossSigning.keysChanged", this.onStatusChanged);
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
this._unmounted = true;
|
||||
const cli = MatrixClientPeg.get();
|
||||
if (!cli) return;
|
||||
cli.removeListener("accountData", this.onAccountData);
|
||||
cli.removeListener("userTrustStatusChanged", this.onStatusChanged);
|
||||
cli.removeListener("crossSigning.keysChanged", this.onStatusChanged);
|
||||
}
|
||||
|
||||
onAccountData = (event) => {
|
||||
const type = event.getType();
|
||||
if (type.startsWith("m.cross_signing") || type.startsWith("m.secret_storage")) {
|
||||
this.setState(this._getUpdatedStatus());
|
||||
}
|
||||
};
|
||||
|
||||
onStatusChanged = () => {
|
||||
this.setState(this._getUpdatedStatus());
|
||||
};
|
||||
|
||||
_getUpdatedStatus() {
|
||||
// XXX: Add public accessors if we keep this around in production
|
||||
const cli = MatrixClientPeg.get();
|
||||
const crossSigning = cli._crypto._crossSigningInfo;
|
||||
const secretStorage = cli._crypto._secretStorage;
|
||||
const crossSigningPublicKeysOnDevice = crossSigning.getId();
|
||||
const crossSigningPrivateKeysInStorage = crossSigning.isStoredInSecretStorage(secretStorage);
|
||||
const secretStorageKeyInAccount = secretStorage.hasKey();
|
||||
|
||||
return {
|
||||
crossSigningPublicKeysOnDevice,
|
||||
crossSigningPrivateKeysInStorage,
|
||||
secretStorageKeyInAccount,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Bootstrapping secret storage may take one of these paths:
|
||||
* 1. Create secret storage from a passphrase and store cross-signing keys
|
||||
* in secret storage.
|
||||
* 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.
|
||||
*/
|
||||
_bootstrapSecureSecretStorage = async () => {
|
||||
this.setState({ error: null });
|
||||
try {
|
||||
await accessSecretStorage();
|
||||
} catch (e) {
|
||||
this.setState({ error: e });
|
||||
console.error("Error bootstrapping secret storage", e);
|
||||
}
|
||||
if (this._unmounted) return;
|
||||
this.setState(this._getUpdatedStatus());
|
||||
}
|
||||
|
||||
render() {
|
||||
const AccessibleButton = sdk.getComponent("elements.AccessibleButton");
|
||||
const {
|
||||
error,
|
||||
crossSigningPublicKeysOnDevice,
|
||||
crossSigningPrivateKeysInStorage,
|
||||
secretStorageKeyInAccount,
|
||||
} = this.state;
|
||||
|
||||
let errorSection;
|
||||
if (error) {
|
||||
errorSection = <div className="error">{error.toString()}</div>;
|
||||
}
|
||||
|
||||
const enabled = (
|
||||
crossSigningPublicKeysOnDevice &&
|
||||
crossSigningPrivateKeysInStorage &&
|
||||
secretStorageKeyInAccount
|
||||
);
|
||||
|
||||
let summarisedStatus;
|
||||
if (enabled) {
|
||||
summarisedStatus = <p>✅ {_t(
|
||||
"Cross-signing and secret storage are enabled.",
|
||||
)}</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 device.",
|
||||
)}</p>;
|
||||
} else {
|
||||
summarisedStatus = <p>{_t(
|
||||
"Cross-signing and secret storage are not yet set up.",
|
||||
)}</p>;
|
||||
}
|
||||
|
||||
let bootstrapButton;
|
||||
if (!enabled) {
|
||||
bootstrapButton = <div className="mx_CrossSigningPanel_buttonRow">
|
||||
<AccessibleButton kind="primary" onClick={this._bootstrapSecureSecretStorage}>
|
||||
{_t("Bootstrap cross-signing and secret storage")}
|
||||
</AccessibleButton>
|
||||
</div>;
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
{summarisedStatus}
|
||||
<details>
|
||||
<summary>{_t("Advanced")}</summary>
|
||||
<table className="mx_CrossSigningPanel_statusList"><tbody>
|
||||
<tr>
|
||||
<td>{_t("Cross-signing public keys:")}</td>
|
||||
<td>{crossSigningPublicKeysOnDevice ? _t("on device") : _t("not found")}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>{_t("Cross-signing private keys:")}</td>
|
||||
<td>{crossSigningPrivateKeysInStorage ? _t("in secret storage") : _t("not found")}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>{_t("Secret storage public key:")}</td>
|
||||
<td>{secretStorageKeyInAccount ? _t("in account data") : _t("not found")}</td>
|
||||
</tr>
|
||||
</tbody></table>
|
||||
</details>
|
||||
{errorSection}
|
||||
{bootstrapButton}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
|
@ -25,8 +25,8 @@ import { _t } from '../../../languageHandler';
|
|||
import Modal from '../../../Modal';
|
||||
|
||||
export default class DevicesPanel extends React.Component {
|
||||
constructor(props, context) {
|
||||
super(props, context);
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
devices: undefined,
|
||||
|
|
|
@ -23,8 +23,8 @@ import MatrixClientPeg from '../../../MatrixClientPeg';
|
|||
import {formatDate} from '../../../DateUtils';
|
||||
|
||||
export default class DevicesPanelEntry extends React.Component {
|
||||
constructor(props, context) {
|
||||
super(props, context);
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this._unmounted = false;
|
||||
this.onDeviceToggled = this.onDeviceToggled.bind(this);
|
||||
|
|
|
@ -20,6 +20,7 @@ import PropTypes from 'prop-types';
|
|||
import sdk from '../../../index';
|
||||
import { _t } from '../../../languageHandler';
|
||||
import dis from '../../../dispatcher';
|
||||
import {Key} from "../../../Keyboard";
|
||||
|
||||
export default class IntegrationManager extends React.Component {
|
||||
static propTypes = {
|
||||
|
@ -52,7 +53,7 @@ export default class IntegrationManager extends React.Component {
|
|||
}
|
||||
|
||||
onKeyDown = (ev) => {
|
||||
if (ev.keyCode === 27) { // escape
|
||||
if (ev.key === Key.ESCAPE) {
|
||||
ev.stopPropagation();
|
||||
ev.preventDefault();
|
||||
this.props.onFinished();
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
/*
|
||||
Copyright 2018 New Vector Ltd
|
||||
Copyright 2019 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
|
@ -20,23 +21,20 @@ import sdk from '../../../index';
|
|||
import MatrixClientPeg from '../../../MatrixClientPeg';
|
||||
import { _t } from '../../../languageHandler';
|
||||
import Modal from '../../../Modal';
|
||||
import SettingsStore from '../../../../lib/settings/SettingsStore';
|
||||
import { accessSecretStorage } from '../../../CrossSigningManager';
|
||||
|
||||
export default class KeyBackupPanel extends React.PureComponent {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this._startNewBackup = this._startNewBackup.bind(this);
|
||||
this._deleteBackup = this._deleteBackup.bind(this);
|
||||
this._onKeyBackupSessionsRemaining =
|
||||
this._onKeyBackupSessionsRemaining.bind(this);
|
||||
this._onKeyBackupStatus = this._onKeyBackupStatus.bind(this);
|
||||
this._restoreBackup = this._restoreBackup.bind(this);
|
||||
|
||||
this._unmounted = false;
|
||||
this.state = {
|
||||
loading: true,
|
||||
error: null,
|
||||
backupInfo: null,
|
||||
backupSigStatus: null,
|
||||
backupKeyStored: null,
|
||||
sessionsRemaining: 0,
|
||||
};
|
||||
}
|
||||
|
@ -63,13 +61,13 @@ export default class KeyBackupPanel extends React.PureComponent {
|
|||
}
|
||||
}
|
||||
|
||||
_onKeyBackupSessionsRemaining(sessionsRemaining) {
|
||||
_onKeyBackupSessionsRemaining = (sessionsRemaining) => {
|
||||
this.setState({
|
||||
sessionsRemaining,
|
||||
});
|
||||
}
|
||||
|
||||
_onKeyBackupStatus() {
|
||||
_onKeyBackupStatus = () => {
|
||||
// This just loads the current backup status rather than forcing
|
||||
// a re-check otherwise we risk causing infinite loops
|
||||
this._loadBackupStatus();
|
||||
|
@ -78,9 +76,11 @@ export default class KeyBackupPanel extends React.PureComponent {
|
|||
async _checkKeyBackupStatus() {
|
||||
try {
|
||||
const {backupInfo, trustInfo} = await MatrixClientPeg.get().checkKeyBackup();
|
||||
const backupKeyStored = await MatrixClientPeg.get().isKeyBackupKeyStored();
|
||||
this.setState({
|
||||
backupInfo,
|
||||
backupSigStatus: trustInfo,
|
||||
backupKeyStored,
|
||||
error: null,
|
||||
loading: false,
|
||||
});
|
||||
|
@ -91,6 +91,7 @@ export default class KeyBackupPanel extends React.PureComponent {
|
|||
error: e,
|
||||
backupInfo: null,
|
||||
backupSigStatus: null,
|
||||
backupKeyStored: null,
|
||||
loading: false,
|
||||
});
|
||||
}
|
||||
|
@ -101,11 +102,13 @@ export default class KeyBackupPanel extends React.PureComponent {
|
|||
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({
|
||||
error: null,
|
||||
backupInfo,
|
||||
backupSigStatus,
|
||||
backupKeyStored,
|
||||
loading: false,
|
||||
});
|
||||
} catch (e) {
|
||||
|
@ -115,12 +118,13 @@ export default class KeyBackupPanel extends React.PureComponent {
|
|||
error: e,
|
||||
backupInfo: null,
|
||||
backupSigStatus: null,
|
||||
backupKeyStored: null,
|
||||
loading: false,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
_startNewBackup() {
|
||||
_startNewBackup = () => {
|
||||
Modal.createTrackedDialogAsync('Key Backup', 'Key Backup',
|
||||
import('../../../async-components/views/dialogs/keybackup/CreateKeyBackupDialog'),
|
||||
{
|
||||
|
@ -131,7 +135,32 @@ export default class KeyBackupPanel extends React.PureComponent {
|
|||
);
|
||||
}
|
||||
|
||||
_deleteBackup() {
|
||||
_startNewBackupWithSecureSecretStorage = async () => {
|
||||
const cli = MatrixClientPeg.get();
|
||||
let info;
|
||||
try {
|
||||
await accessSecretStorage(async () => {
|
||||
info = await cli.prepareKeyBackupVersion(
|
||||
null /* random key */,
|
||||
{ secureSecretStorage: true },
|
||||
);
|
||||
info = await cli.createKeyBackupVersion(info);
|
||||
});
|
||||
await MatrixClientPeg.get().scheduleAllGroupSessionsForBackup();
|
||||
this._loadBackupStatus();
|
||||
} catch (e) {
|
||||
console.error("Error creating key backup", e);
|
||||
// TODO: If creating a version succeeds, but backup fails, should we
|
||||
// delete the version, disable backup, or do nothing? If we just
|
||||
// disable without deleting, we'll enable on next app reload since
|
||||
// it is trusted.
|
||||
if (info && info.version) {
|
||||
MatrixClientPeg.get().deleteKeyBackupVersion(info.version);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
_deleteBackup = () => {
|
||||
const QuestionDialog = sdk.getComponent('dialogs.QuestionDialog');
|
||||
Modal.createTrackedDialog('Delete Backup', '', QuestionDialog, {
|
||||
title: _t('Delete Backup'),
|
||||
|
@ -151,10 +180,23 @@ export default class KeyBackupPanel extends React.PureComponent {
|
|||
});
|
||||
}
|
||||
|
||||
_restoreBackup() {
|
||||
const RestoreKeyBackupDialog = sdk.getComponent('dialogs.keybackup.RestoreKeyBackupDialog');
|
||||
Modal.createTrackedDialog('Restore Backup', '', RestoreKeyBackupDialog, {
|
||||
});
|
||||
_restoreBackup = async () => {
|
||||
// Use legacy path if backup key not stored in secret storage
|
||||
if (!this.state.backupKeyStored) {
|
||||
const RestoreKeyBackupDialog = sdk.getComponent('dialogs.keybackup.RestoreKeyBackupDialog');
|
||||
Modal.createTrackedDialog('Restore Backup', '', RestoreKeyBackupDialog);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await accessSecretStorage(async () => {
|
||||
await MatrixClientPeg.get().restoreKeyBackupWithSecretStorage(
|
||||
this.state.backupInfo,
|
||||
);
|
||||
});
|
||||
} catch (e) {
|
||||
console.log("Error restoring backup", e);
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
|
@ -199,6 +241,13 @@ export default class KeyBackupPanel extends React.PureComponent {
|
|||
restoreButtonCaption = _t("Connect this device 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()) {
|
||||
|
@ -229,10 +278,29 @@ export default class KeyBackupPanel extends React.PureComponent {
|
|||
sig.device &&
|
||||
sig.device.getFingerprint() === MatrixClientPeg.get().getDeviceEd25519Key()
|
||||
);
|
||||
const fromThisUser = (
|
||||
sig.crossSigningId &&
|
||||
sig.deviceId === MatrixClientPeg.get().getCrossSigningId()
|
||||
);
|
||||
let sigStatus;
|
||||
if (!sig.device) {
|
||||
if (sig.valid && fromThisUser) {
|
||||
sigStatus = _t(
|
||||
"Backup has a signature from <verify>unknown</verify> device with ID %(deviceId)s.",
|
||||
"Backup has a <validity>valid</validity> signature from this user",
|
||||
{}, { validity },
|
||||
);
|
||||
} else if (!sig.valid && fromThisUser) {
|
||||
sigStatus = _t(
|
||||
"Backup has a <validity>invalid</validity> signature from this user",
|
||||
{}, { validity },
|
||||
);
|
||||
} else if (sig.crossSigningId) {
|
||||
sigStatus = _t(
|
||||
"Backup has a signature from <verify>unknown</verify> user with ID %(deviceId)s",
|
||||
{ deviceId: sig.deviceId }, { verify },
|
||||
);
|
||||
} else if (!sig.device) {
|
||||
sigStatus = _t(
|
||||
"Backup has a signature from <verify>unknown</verify> device with ID %(deviceId)s",
|
||||
{ deviceId: sig.deviceId }, { verify },
|
||||
);
|
||||
} else if (sig.valid && fromThisDevice) {
|
||||
|
@ -285,26 +353,54 @@ export default class KeyBackupPanel extends React.PureComponent {
|
|||
trustedLocally = _t("This backup is trusted because it has been restored on this device");
|
||||
}
|
||||
|
||||
let buttonRow = (
|
||||
<div className="mx_KeyBackupPanel_buttonRow">
|
||||
<AccessibleButton kind="primary" onClick={this._restoreBackup}>
|
||||
{restoreButtonCaption}
|
||||
</AccessibleButton>
|
||||
<AccessibleButton kind="danger" onClick={this._deleteBackup}>
|
||||
{_t("Delete Backup")}
|
||||
</AccessibleButton>
|
||||
</div>
|
||||
);
|
||||
if (this.state.backupKeyStored && !SettingsStore.isFeatureEnabled("feature_cross_signing")) {
|
||||
buttonRow = <p>⚠️ {_t(
|
||||
"Backup key stored in secret storage, but this feature is not " +
|
||||
"enabled on this device. Please enable cross-signing in Labs to " +
|
||||
"modify key backup state.",
|
||||
)}</p>;
|
||||
}
|
||||
|
||||
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>
|
||||
<p>
|
||||
<AccessibleButton kind="primary" onClick={this._restoreBackup}>
|
||||
{restoreButtonCaption}
|
||||
</AccessibleButton>
|
||||
<AccessibleButton kind="danger" onClick={this._deleteBackup}>
|
||||
{ _t("Delete Backup") }
|
||||
</AccessibleButton>
|
||||
</p>
|
||||
{buttonRow}
|
||||
</div>;
|
||||
} else {
|
||||
// This is a temporary button for testing the new path which stores
|
||||
// the key backup key in SSSS. Initialising SSSS depends on
|
||||
// cross-signing and is part of the same project, so we only show
|
||||
// this mode when the cross-signing feature is enabled.
|
||||
// TODO: Clean this up when removing the feature flag.
|
||||
let secureSecretStorageKeyBackup;
|
||||
if (SettingsStore.isFeatureEnabled("feature_cross_signing")) {
|
||||
secureSecretStorageKeyBackup = (
|
||||
<div className="mx_KeyBackupPanel_buttonRow">
|
||||
<AccessibleButton kind="primary" onClick={this._startNewBackupWithSecureSecretStorage}>
|
||||
{_t("Start using Key Backup with Secure Secret Storage")}
|
||||
</AccessibleButton>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return <div>
|
||||
<div>
|
||||
<p>{_t(
|
||||
|
@ -314,9 +410,12 @@ export default class KeyBackupPanel extends React.PureComponent {
|
|||
<p>{encryptedMessageAreEncrypted}</p>
|
||||
<p>{_t("Back up your keys before signing out to avoid losing them.")}</p>
|
||||
</div>
|
||||
<AccessibleButton kind="primary" onClick={this._startNewBackup}>
|
||||
{ _t("Start using Key Backup") }
|
||||
</AccessibleButton>
|
||||
<div className="mx_KeyBackupPanel_buttonRow">
|
||||
<AccessibleButton kind="primary" onClick={this._startNewBackup}>
|
||||
{_t("Start using Key Backup")}
|
||||
</AccessibleButton>
|
||||
</div>
|
||||
{secureSecretStorageKeyBackup}
|
||||
</div>;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -14,14 +14,13 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import React, {createRef} from 'react';
|
||||
import {_t} from "../../../languageHandler";
|
||||
import MatrixClientPeg from "../../../MatrixClientPeg";
|
||||
import Field from "../elements/Field";
|
||||
import AccessibleButton from "../elements/AccessibleButton";
|
||||
import classNames from 'classnames';
|
||||
import {User} from "matrix-js-sdk";
|
||||
import { getHostingLink } from '../../../utils/HostingLink';
|
||||
import sdk from "../../../index";
|
||||
|
||||
export default class ProfileSettings extends React.Component {
|
||||
constructor() {
|
||||
|
@ -48,13 +47,22 @@ export default class ProfileSettings extends React.Component {
|
|||
avatarFile: null,
|
||||
enableProfileSave: false,
|
||||
};
|
||||
|
||||
this._avatarUpload = createRef();
|
||||
}
|
||||
|
||||
_uploadAvatar = (e) => {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
_uploadAvatar = () => {
|
||||
this._avatarUpload.current.click();
|
||||
};
|
||||
|
||||
this.refs.avatarUpload.click();
|
||||
_removeAvatar = () => {
|
||||
// clear file upload field so same file can be selected
|
||||
this._avatarUpload.current.value = "";
|
||||
this.setState({
|
||||
avatarUrl: undefined,
|
||||
avatarFile: undefined,
|
||||
enableProfileSave: true,
|
||||
});
|
||||
};
|
||||
|
||||
_saveProfile = async (e) => {
|
||||
|
@ -115,29 +123,6 @@ export default class ProfileSettings extends React.Component {
|
|||
};
|
||||
|
||||
render() {
|
||||
// TODO: Why is rendering a box with an overlay so complicated? Can the DOM be reduced?
|
||||
|
||||
let showOverlayAnyways = true;
|
||||
let avatarElement = <div className="mx_ProfileSettings_avatarPlaceholder" />;
|
||||
if (this.state.avatarUrl) {
|
||||
showOverlayAnyways = false;
|
||||
avatarElement = <img src={this.state.avatarUrl}
|
||||
alt={_t("Profile picture")} />;
|
||||
}
|
||||
|
||||
const avatarOverlayClasses = classNames({
|
||||
"mx_ProfileSettings_avatarOverlay": true,
|
||||
"mx_ProfileSettings_avatarOverlay_show": showOverlayAnyways,
|
||||
});
|
||||
const avatarHoverElement = (
|
||||
<div className={avatarOverlayClasses} onClick={this._uploadAvatar}>
|
||||
<span className="mx_ProfileSettings_avatarOverlayText">{_t("Upload profile picture")}</span>
|
||||
<div className="mx_ProfileSettings_avatarOverlayImgContainer">
|
||||
<div className="mx_ProfileSettings_avatarOverlayImg" />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
const hostingSignupLink = getHostingLink('user-settings');
|
||||
let hostingSignup = null;
|
||||
if (hostingSignupLink) {
|
||||
|
@ -154,9 +139,11 @@ export default class ProfileSettings extends React.Component {
|
|||
</span>;
|
||||
}
|
||||
|
||||
const AccessibleButton = sdk.getComponent('elements.AccessibleButton');
|
||||
const AvatarSetting = sdk.getComponent('settings.AvatarSetting');
|
||||
return (
|
||||
<form onSubmit={this._saveProfile} autoComplete="off" noValidate={true}>
|
||||
<input type="file" ref="avatarUpload" className="mx_ProfileSettings_avatarUpload"
|
||||
<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">
|
||||
|
@ -168,10 +155,12 @@ export default class ProfileSettings extends React.Component {
|
|||
type="text" value={this.state.displayName} autoComplete="off"
|
||||
onChange={this._onDisplayNameChanged} />
|
||||
</div>
|
||||
<div className="mx_ProfileSettings_avatar">
|
||||
{avatarElement}
|
||||
{avatarHoverElement}
|
||||
</div>
|
||||
<AvatarSetting
|
||||
avatarUrl={this.state.avatarUrl}
|
||||
avatarName={this.state.displayName || this.state.userId}
|
||||
avatarAltText={_t("Profile picture")}
|
||||
uploadAvatar={this._uploadAvatar}
|
||||
removeAvatar={this._removeAvatar} />
|
||||
</div>
|
||||
<AccessibleButton onClick={this._saveProfile} kind="primary"
|
||||
disabled={!this.state.enableProfileSave}>
|
||||
|
|
|
@ -18,22 +18,19 @@ import React from 'react';
|
|||
import PropTypes from 'prop-types';
|
||||
import {_t} from "../../../../../languageHandler";
|
||||
import RoomProfileSettings from "../../../room_settings/RoomProfileSettings";
|
||||
import MatrixClientPeg from "../../../../../MatrixClientPeg";
|
||||
import sdk from "../../../../..";
|
||||
import AccessibleButton from "../../../elements/AccessibleButton";
|
||||
import {MatrixClient} from "matrix-js-sdk";
|
||||
import dis from "../../../../../dispatcher";
|
||||
import LabelledToggleSwitch from "../../../elements/LabelledToggleSwitch";
|
||||
import MatrixClientContext from "../../../../../contexts/MatrixClientContext";
|
||||
|
||||
export default class GeneralRoomSettingsTab extends React.Component {
|
||||
static childContextTypes = {
|
||||
matrixClient: PropTypes.instanceOf(MatrixClient),
|
||||
};
|
||||
|
||||
static propTypes = {
|
||||
roomId: PropTypes.string.isRequired,
|
||||
};
|
||||
|
||||
static contextType = MatrixClientContext;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
|
@ -42,14 +39,8 @@ export default class GeneralRoomSettingsTab extends React.Component {
|
|||
};
|
||||
}
|
||||
|
||||
getChildContext() {
|
||||
return {
|
||||
matrixClient: MatrixClientPeg.get(),
|
||||
};
|
||||
}
|
||||
|
||||
componentWillMount() {
|
||||
MatrixClientPeg.get().getRoomDirectoryVisibility(this.props.roomId).then((result => {
|
||||
this.context.getRoomDirectoryVisibility(this.props.roomId).then((result => {
|
||||
this.setState({isRoomPublished: result.visibility === 'public'});
|
||||
}));
|
||||
}
|
||||
|
@ -59,7 +50,7 @@ export default class GeneralRoomSettingsTab extends React.Component {
|
|||
const newValue = !valueBefore;
|
||||
this.setState({isRoomPublished: newValue});
|
||||
|
||||
MatrixClientPeg.get().setRoomDirectoryVisibility(
|
||||
this.context.setRoomDirectoryVisibility(
|
||||
this.props.roomId,
|
||||
newValue ? 'public' : 'private',
|
||||
).catch(() => {
|
||||
|
@ -80,7 +71,7 @@ export default class GeneralRoomSettingsTab extends React.Component {
|
|||
const RelatedGroupSettings = sdk.getComponent("room_settings.RelatedGroupSettings");
|
||||
const UrlPreviewSettings = sdk.getComponent("room_settings.UrlPreviewSettings");
|
||||
|
||||
const client = MatrixClientPeg.get();
|
||||
const client = this.context;
|
||||
const room = client.getRoom(this.props.roomId);
|
||||
|
||||
const canSetAliases = true; // Previously, we arbitrarily only allowed admins to do this
|
||||
|
|
|
@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import React, {createRef} from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import {_t} from "../../../../../languageHandler";
|
||||
import MatrixClientPeg from "../../../../../MatrixClientPeg";
|
||||
|
@ -44,13 +44,15 @@ export default class NotificationsSettingsTab extends React.Component {
|
|||
}
|
||||
this.setState({currentSound: soundData.name || soundData.url});
|
||||
});
|
||||
|
||||
this._soundUpload = createRef();
|
||||
}
|
||||
|
||||
async _triggerUploader(e) {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
|
||||
this.refs.soundUpload.click();
|
||||
this._soundUpload.current.click();
|
||||
}
|
||||
|
||||
async _onSoundUploadChanged(e) {
|
||||
|
@ -157,7 +159,7 @@ export default class NotificationsSettingsTab extends React.Component {
|
|||
<div>
|
||||
<h3>{_t("Set a new custom sound")}</h3>
|
||||
<form autoComplete="off" noValidate={true}>
|
||||
<input ref="soundUpload" className="mx_NotificationSound_soundUpload" type="file" onChange={this._onSoundUploadChanged.bind(this)} accept="audio/*" />
|
||||
<input ref={this._soundUpload} className="mx_NotificationSound_soundUpload" type="file" onChange={this._onSoundUploadChanged.bind(this)} accept="audio/*" />
|
||||
</form>
|
||||
|
||||
{currentUploadedFile}
|
||||
|
|
|
@ -17,25 +17,8 @@ limitations under the License.
|
|||
import React from 'react';
|
||||
import {_t} from "../../../../../languageHandler";
|
||||
import GroupUserSettings from "../../../groups/GroupUserSettings";
|
||||
import MatrixClientPeg from "../../../../../MatrixClientPeg";
|
||||
import PropTypes from "prop-types";
|
||||
import {MatrixClient} from "matrix-js-sdk";
|
||||
|
||||
export default class FlairUserSettingsTab extends React.Component {
|
||||
static childContextTypes = {
|
||||
matrixClient: PropTypes.instanceOf(MatrixClient),
|
||||
};
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
}
|
||||
|
||||
getChildContext() {
|
||||
return {
|
||||
matrixClient: MatrixClientPeg.get(),
|
||||
};
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div className="mx_SettingsTab">
|
||||
|
|
|
@ -17,7 +17,7 @@ limitations under the License.
|
|||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import {_t} from "../../../../../languageHandler";
|
||||
import {SettingLevel} from "../../../../../settings/SettingsStore";
|
||||
import SettingsStore, {SettingLevel} from "../../../../../settings/SettingsStore";
|
||||
import MatrixClientPeg from "../../../../../MatrixClientPeg";
|
||||
import * as FormattingUtils from "../../../../../utils/FormattingUtils";
|
||||
import AccessibleButton from "../../../elements/AccessibleButton";
|
||||
|
@ -37,12 +37,13 @@ export class IgnoredUser extends React.Component {
|
|||
};
|
||||
|
||||
render() {
|
||||
const id = `mx_SecurityUserSettingsTab_ignoredUser_${this.props.userId}`;
|
||||
return (
|
||||
<div className='mx_SecurityUserSettingsTab_ignoredUser'>
|
||||
<AccessibleButton onClick={this._onUnignoreClicked} kind='primary_sm'>
|
||||
{_t('Unignore')}
|
||||
<AccessibleButton onClick={this._onUnignoreClicked} kind='primary_sm' aria-describedby={id}>
|
||||
{ _t('Unignore') }
|
||||
</AccessibleButton>
|
||||
<span>{this.props.userId}</span>
|
||||
<span id={id}>{ this.props.userId }</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
@ -252,6 +253,23 @@ export default class SecurityUserSettingsTab extends React.Component {
|
|||
</div>
|
||||
);
|
||||
|
||||
// XXX: There's no such panel in the current cross-signing designs, but
|
||||
// it's useful to have for testing the feature. If there's no interest
|
||||
// in having advanced details here once all flows are implemented, we
|
||||
// can remove this.
|
||||
const CrossSigningPanel = sdk.getComponent('views.settings.CrossSigningPanel');
|
||||
let crossSigning;
|
||||
if (SettingsStore.isFeatureEnabled("feature_cross_signing")) {
|
||||
crossSigning = (
|
||||
<div className='mx_SettingsTab_section'>
|
||||
<span className="mx_SettingsTab_subheading">{_t("Cross-signing")}</span>
|
||||
<div className='mx_SettingsTab_subsectionText'>
|
||||
<CrossSigningPanel />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="mx_SettingsTab mx_SecurityUserSettingsTab">
|
||||
<div className="mx_SettingsTab_heading">{_t("Security & Privacy")}</div>
|
||||
|
@ -263,6 +281,7 @@ export default class SecurityUserSettingsTab extends React.Component {
|
|||
</div>
|
||||
</div>
|
||||
{keyBackup}
|
||||
{crossSigning}
|
||||
{this._renderCurrentDeviceInfo()}
|
||||
<div className='mx_SettingsTab_section'>
|
||||
<span className="mx_SettingsTab_subheading">{_t("Analytics")}</span>
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue