Merge branch 'develop' into hs/bridge-info

This commit is contained in:
Will Hunt 2019-12-30 17:09:05 +01:00 committed by GitHub
commit 0a8cc416bf
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
203 changed files with 5331 additions and 2496 deletions

View 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>&nbsp;</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;

View 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>
);
}
}

View file

@ -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,

View file

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

View file

@ -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();

View file

@ -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>&nbsp;&nbsp;&nbsp;
<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>&nbsp;&nbsp;&nbsp;
<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>;
}
}

View file

@ -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}>

View file

@ -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

View file

@ -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}

View file

@ -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">

View file

@ -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>