Merge branches 'develop' and 't3chguy/usercontent' of github.com:matrix-org/matrix-react-sdk into t3chguy/usercontent

This commit is contained in:
Michael Telatynski 2020-02-13 16:36:05 +00:00
commit 12aae12218
47 changed files with 1769 additions and 338 deletions

View file

@ -115,6 +115,7 @@ export default class MessagePanel extends React.Component {
// previous positions the read marker has been in, so we can
// display 'ghost' read markers that are animating away
ghostReadMarkers: [],
showTypingNotifications: SettingsStore.getValue("showTypingNotifications"),
};
// opaque readreceipt info for each userId; used by ReadReceiptMarker
@ -164,6 +165,9 @@ export default class MessagePanel extends React.Component {
this._readMarkerNode = createRef();
this._whoIsTyping = createRef();
this._scrollPanel = createRef();
this._showTypingNotificationsWatcherRef =
SettingsStore.watchSetting("showTypingNotifications", null, this.onShowTypingNotificationsChange);
}
componentDidMount() {
@ -172,6 +176,7 @@ export default class MessagePanel extends React.Component {
componentWillUnmount() {
this._isMounted = false;
SettingsStore.unwatchSetting(this._showTypingNotificationsWatcherRef);
}
componentDidUpdate(prevProps, prevState) {
@ -184,6 +189,12 @@ export default class MessagePanel extends React.Component {
}
}
onShowTypingNotificationsChange = () => {
this.setState({
showTypingNotifications: SettingsStore.getValue("showTypingNotifications"),
});
};
/* get the DOM node representing the given event */
getNodeForEventId(eventId) {
if (!this.eventNodes) {
@ -921,7 +932,7 @@ export default class MessagePanel extends React.Component {
);
let whoIsTyping;
if (this.props.room && !this.props.tileShape) {
if (this.props.room && !this.props.tileShape && this.state.showTypingNotifications) {
whoIsTyping = (<WhoIsTypingTile
room={this.props.room}
onShown={this._onTypingShown}

View file

@ -160,6 +160,7 @@ export default createReactClass({
onKeyDown={ this._onKeyDown }
onBlur={this._onBlur}
placeholder={ placeholder }
autoComplete="off"
/>
{ clearButton }
</div>

View file

@ -83,12 +83,13 @@ export default class CompleteSecurity extends React.Component {
}
}
onVerificationRequest = (request) => {
onVerificationRequest = async (request) => {
if (request.otherUserId !== MatrixClientPeg.get().getUserId()) return;
if (this.state.verificationRequest) {
this.state.verificationRequest.off("change", this.onVerificationRequestChange);
}
await request.accept();
request.on("change", this.onVerificationRequestChange);
this.setState({
verificationRequest: request,
@ -138,9 +139,12 @@ export default class CompleteSecurity extends React.Component {
let body;
if (this.state.verificationRequest) {
const IncomingSasDialog = sdk.getComponent("views.dialogs.IncomingSasDialog");
body = <IncomingSasDialog verifier={this.state.verificationRequest.verifier}
onFinished={this.props.onFinished}
const EncryptionPanel = sdk.getComponent("views.right_panel.EncryptionPanel");
body = <EncryptionPanel
layout="dialog"
verificationRequest={this.state.verificationRequest}
onClose={this.props.onFinished}
member={MatrixClientPeg.get().getUser(this.state.verificationRequest.otherUserId)}
/>;
} else if (phase === PHASE_INTRO) {
icon = <span className="mx_CompleteSecurity_headerIcon mx_E2EIcon_warning"></span>;

View file

@ -61,13 +61,9 @@ export default createReactClass({
} else {
console.log("Loading recaptcha script...");
window.mx_on_recaptcha_loaded = () => {this._onCaptchaLoaded();};
let protocol = global.location.protocol;
if (protocol !== "http:") {
protocol = "https:";
}
const scriptTag = document.createElement('script');
scriptTag.setAttribute(
'src', `${protocol}//www.recaptcha.net/recaptcha/api.js?onload=mx_on_recaptcha_loaded&render=explicit`,
'src', `https://www.recaptcha.net/recaptcha/api.js?onload=mx_on_recaptcha_loaded&render=explicit`,
);
this._recaptchaContainer.current.appendChild(scriptTag);
}

View file

@ -486,6 +486,7 @@ export default createReactClass({
id="mx_RegistrationForm_password"
ref={field => this[FIELD_PASSWORD] = field}
type="password"
autoComplete="new-password"
label={_t("Password")}
value={this.state.password}
onChange={this.onPasswordChange}
@ -499,6 +500,7 @@ export default createReactClass({
id="mx_RegistrationForm_passwordConfirm"
ref={field => this[FIELD_PASSWORD_CONFIRM] = field}
type="password"
autoComplete="new-password"
label={_t("Confirm")}
value={this.state.passwordConfirm}
onChange={this.onPasswordConfirmChange}

View file

@ -274,15 +274,13 @@ export default class ServerConfig extends React.PureComponent {
: null;
return (
<div className="mx_ServerConfig">
<form className="mx_ServerConfig" onSubmit={this.onSubmit} autoComplete="off">
<h3>{_t("Other servers")}</h3>
{errorText}
{this._renderHomeserverSection()}
{this._renderIdentityServerSection()}
<form onSubmit={this.onSubmit} autoComplete="off" action={null}>
{submitButton}
</form>
</div>
{submitButton}
</form>
);
}
}

View file

@ -0,0 +1,65 @@
/*
Copyright 2020 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 PropTypes from 'prop-types';
import {_t} from "../../../languageHandler";
import * as sdk from "../../../index";
export default class ConfirmDestroyCrossSigningDialog extends React.Component {
static propTypes = {
onFinished: PropTypes.func.isRequired,
};
_onConfirm = () => {
this.props.onFinished(true);
};
_onDecline = () => {
this.props.onFinished(false);
};
render() {
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
const DialogButtons = sdk.getComponent('views.elements.DialogButtons');
return (
<BaseDialog
className='mx_ConfirmDestroyCrossSigningDialog'
hasCancel={true}
onFinished={this.props.onFinished}
title={_t("Destroy cross-signing keys?")}>
<div className='mx_ConfirmDestroyCrossSigningDialog_content'>
<p>
{_t(
"Deleting cross-signing keys is permanent. " +
"Anyone you have verified with will see security alerts. " +
"You almost certainly don't want to do this, unless " +
"you've lost every device you can cross-sign from.",
)}
</p>
</div>
<DialogButtons
primaryButton={_t("Clear cross-signing keys")}
onPrimaryButtonClick={this._onConfirm}
primaryButtonClass="danger"
cancelButton={_t("Cancel")}
onCancel={this._onDecline}
/>
</BaseDialog>
);
}
}

View file

@ -118,6 +118,7 @@ export default class DeactivateAccountDialog extends React.Component {
const Field = sdk.getComponent('elements.Field');
// this is on purpose not a <form /> to prevent Enter triggering submission, to further prevent accidents
return (
<BaseDialog className="mx_DeactivateAccountDialog"
onFinished={this.props.onFinished}

View file

@ -27,16 +27,19 @@ import {verificationMethods} from 'matrix-js-sdk/src/crypto';
import {ensureDMExists} from "../../../createRoom";
import dis from "../../../dispatcher";
import SettingsStore from '../../../settings/SettingsStore';
import {SCAN_QR_CODE_METHOD, SHOW_QR_CODE_METHOD} from "matrix-js-sdk/src/crypto/verification/QRCode";
import VerificationQREmojiOptions from "../verification/VerificationQREmojiOptions";
const MODE_LEGACY = 'legacy';
const MODE_SAS = 'sas';
const PHASE_START = 0;
const PHASE_WAIT_FOR_PARTNER_TO_ACCEPT = 1;
const PHASE_SHOW_SAS = 2;
const PHASE_WAIT_FOR_PARTNER_TO_CONFIRM = 3;
const PHASE_VERIFIED = 4;
const PHASE_CANCELLED = 5;
const PHASE_PICK_VERIFICATION_OPTION = 2;
const PHASE_SHOW_SAS = 3;
const PHASE_WAIT_FOR_PARTNER_TO_CONFIRM = 4;
const PHASE_VERIFIED = 5;
const PHASE_CANCELLED = 6;
export default class DeviceVerifyDialog extends React.Component {
static propTypes = {
@ -49,6 +52,7 @@ export default class DeviceVerifyDialog extends React.Component {
super();
this._verifier = null;
this._showSasEvent = null;
this._request = null;
this.state = {
phase: PHASE_START,
mode: MODE_SAS,
@ -80,6 +84,25 @@ export default class DeviceVerifyDialog extends React.Component {
this.props.onFinished(false);
}
_onUseSasClick = async () => {
try {
this._verifier = this._request.beginKeyVerification(verificationMethods.SAS);
this._verifier.on('show_sas', this._onVerifierShowSas);
// throws upon cancellation
await this._verifier.verify();
this.setState({phase: PHASE_VERIFIED});
this._verifier.removeListener('show_sas', this._onVerifierShowSas);
this._verifier = null;
} catch (e) {
console.log("Verification failed", e);
this.setState({
phase: PHASE_CANCELLED,
});
this._verifier = null;
this._request = null;
}
};
_onLegacyFinished = (confirm) => {
if (confirm) {
MatrixClientPeg.get().setDeviceVerified(
@ -100,7 +123,7 @@ export default class DeviceVerifyDialog extends React.Component {
const roomId = await ensureDMExistsAndOpen(this.props.userId);
// throws upon cancellation before having started
const request = await client.requestVerificationDM(
this.props.userId, roomId, [verificationMethods.SAS],
this.props.userId, roomId,
);
await request.waitFor(r => r.ready || r.started);
if (request.ready) {
@ -108,11 +131,22 @@ export default class DeviceVerifyDialog extends React.Component {
} else {
this._verifier = request.verifier;
}
} else if (verifyingOwnDevice && SettingsStore.isFeatureEnabled("feature_cross_signing")) {
this._request = await client.requestVerification(this.props.userId, [
verificationMethods.SAS,
SHOW_QR_CODE_METHOD,
SCAN_QR_CODE_METHOD,
verificationMethods.RECIPROCATE_QR_CODE,
]);
await this._request.waitFor(r => r.ready || r.started);
this.setState({phase: PHASE_PICK_VERIFICATION_OPTION});
} else {
this._verifier = client.beginKeyVerification(
verificationMethods.SAS, this.props.userId, this.props.device.deviceId,
);
}
if (!this._verifier) return;
this._verifier.on('show_sas', this._onVerifierShowSas);
// throws upon cancellation
await this._verifier.verify();
@ -150,10 +184,13 @@ export default class DeviceVerifyDialog extends React.Component {
let body;
switch (this.state.phase) {
case PHASE_START:
body = this._renderSasVerificationPhaseStart();
body = this._renderVerificationPhaseStart();
break;
case PHASE_WAIT_FOR_PARTNER_TO_ACCEPT:
body = this._renderSasVerificationPhaseWaitAccept();
body = this._renderVerificationPhaseWaitAccept();
break;
case PHASE_PICK_VERIFICATION_OPTION:
body = this._renderVerificationPhasePick();
break;
case PHASE_SHOW_SAS:
body = this._renderSasVerificationPhaseShowSas();
@ -162,10 +199,10 @@ export default class DeviceVerifyDialog extends React.Component {
body = this._renderSasVerificationPhaseWaitForPartnerToConfirm();
break;
case PHASE_VERIFIED:
body = this._renderSasVerificationPhaseVerified();
body = this._renderVerificationPhaseVerified();
break;
case PHASE_CANCELLED:
body = this._renderSasVerificationPhaseCancelled();
body = this._renderVerificationPhaseCancelled();
break;
}
@ -180,7 +217,7 @@ export default class DeviceVerifyDialog extends React.Component {
);
}
_renderSasVerificationPhaseStart() {
_renderVerificationPhaseStart() {
const AccessibleButton = sdk.getComponent('views.elements.AccessibleButton');
const DialogButtons = sdk.getComponent('views.elements.DialogButtons');
return (
@ -206,7 +243,7 @@ export default class DeviceVerifyDialog extends React.Component {
);
}
_renderSasVerificationPhaseWaitAccept() {
_renderVerificationPhaseWaitAccept() {
const Spinner = sdk.getComponent("views.elements.Spinner");
const AccessibleButton = sdk.getComponent('views.elements.AccessibleButton');
@ -227,6 +264,14 @@ export default class DeviceVerifyDialog extends React.Component {
);
}
_renderVerificationPhasePick() {
return <VerificationQREmojiOptions
request={this._request}
onCancel={this._onCancelClick}
onStartEmoji={this._onUseSasClick}
/>;
}
_renderSasVerificationPhaseShowSas() {
const VerificationShowSas = sdk.getComponent('views.verification.VerificationShowSas');
return <VerificationShowSas
@ -234,6 +279,7 @@ export default class DeviceVerifyDialog extends React.Component {
onCancel={this._onCancelClick}
onDone={this._onSasMatchesClick}
isSelf={MatrixClientPeg.get().getUserId() === this.props.userId}
onStartEmoji={this._onUseSasClick}
/>;
}
@ -247,12 +293,12 @@ export default class DeviceVerifyDialog extends React.Component {
</div>;
}
_renderSasVerificationPhaseVerified() {
_renderVerificationPhaseVerified() {
const VerificationComplete = sdk.getComponent('views.verification.VerificationComplete');
return <VerificationComplete onDone={this._onVerifiedDoneClick} />;
}
_renderSasVerificationPhaseCancelled() {
_renderVerificationPhaseCancelled() {
const VerificationCancelled = sdk.getComponent('views.verification.VerificationCancelled');
return <VerificationCancelled onDone={this._onCancelClick} />;
}

View file

@ -19,9 +19,10 @@ import PropTypes from 'prop-types';
import { _t } from '../../../languageHandler';
import Modal from '../../../Modal';
import { replaceableComponent } from '../../../utils/replaceableComponent';
import DeviceVerifyDialog from './DeviceVerifyDialog';
import VerificationRequestDialog from './VerificationRequestDialog';
import BaseDialog from './BaseDialog';
import DialogButtons from '../elements/DialogButtons';
import {MatrixClientPeg} from "../../../MatrixClientPeg";
@replaceableComponent("views.dialogs.NewSessionReviewDialog")
export default class NewSessionReviewDialog extends React.PureComponent {
@ -35,12 +36,18 @@ export default class NewSessionReviewDialog extends React.PureComponent {
this.props.onFinished(false);
}
onContinueClick = () => {
onContinueClick = async () => {
const { userId, device } = this.props;
Modal.createTrackedDialog('New Session Verification', 'Starting dialog', DeviceVerifyDialog, {
const cli = MatrixClientPeg.get();
const request = await cli.requestVerification(
userId,
device,
}, null, /* priority = */ false, /* static = */ true);
[device.deviceId],
);
this.props.onFinished(true);
Modal.createTrackedDialog('New Session Verification', 'Starting dialog', VerificationRequestDialog, {
verificationRequest: request,
});
}
render() {

View file

@ -31,6 +31,7 @@ export default createReactClass({
danger: PropTypes.bool,
focus: PropTypes.bool,
onFinished: PropTypes.func.isRequired,
headerImage: PropTypes.string,
},
getDefaultProps: function() {
@ -63,6 +64,7 @@ export default createReactClass({
<BaseDialog className="mx_QuestionDialog" onFinished={this.props.onFinished}
title={this.props.title}
contentId='mx_Dialog_content'
headerImage={this.props.headerImage}
hasCancel={this.props.hasCancelButton}
>
<div className="mx_Dialog_content" id='mx_Dialog_content'>

View file

@ -0,0 +1,45 @@
/*
Copyright 2020 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 PropTypes from 'prop-types';
import {MatrixClientPeg} from '../../../MatrixClientPeg';
import * as sdk from '../../../index';
import { _t } from '../../../languageHandler';
export default class VerificationRequestDialog extends React.Component {
static propTypes = {
verificationRequest: PropTypes.object.isRequired,
onFinished: PropTypes.func.isRequired,
};
render() {
const BaseDialog = sdk.getComponent("views.dialogs.BaseDialog");
const EncryptionPanel = sdk.getComponent("views.right_panel.EncryptionPanel");
return <BaseDialog className="mx_InfoDialog" onFinished={this.props.onFinished}
contentId="mx_Dialog_content"
title={_t("Verification Request")}
hasCancel={true}
>
<EncryptionPanel
layout="dialog"
verificationRequest={this.props.verificationRequest}
onClose={this.props.onFinished}
member={MatrixClientPeg.get().getUser(this.props.verificationRequest.otherUserId)}
/>
</BaseDialog>;
}
}

View file

@ -22,7 +22,6 @@ import {MatrixClientPeg} from '../../../../MatrixClientPeg';
import { MatrixClient } from 'matrix-js-sdk';
import Modal from '../../../../Modal';
import { _t } from '../../../../languageHandler';
import {Key} from "../../../../Keyboard";
import { accessSecretStorage } from '../../../../CrossSigningManager';
const RESTORE_TYPE_PASSPHRASE = 0;
@ -125,6 +124,8 @@ export default class RestoreKeyBackupDialog extends React.PureComponent {
}
_onRecoveryKeyNext = async () => {
if (!this.state.recoveryKeyValid) return;
this.setState({
loading: true,
restoreError: null,
@ -157,18 +158,6 @@ export default class RestoreKeyBackupDialog extends React.PureComponent {
});
}
_onPassPhraseKeyPress = (e) => {
if (e.key === Key.ENTER) {
this._onPassPhraseNext();
}
}
_onRecoveryKeyKeyPress = (e) => {
if (e.key === Key.ENTER && this.state.recoveryKeyValid) {
this._onRecoveryKeyNext();
}
}
async _restoreWithSecretStorage() {
this.setState({
loading: true,
@ -305,21 +294,22 @@ export default class RestoreKeyBackupDialog extends React.PureComponent {
"messaging by entering your recovery passphrase.",
)}</p>
<div className="mx_RestoreKeyBackupDialog_primaryContainer">
<form className="mx_RestoreKeyBackupDialog_primaryContainer">
<input type="password"
className="mx_RestoreKeyBackupDialog_passPhraseInput"
onChange={this._onPassPhraseChange}
onKeyPress={this._onPassPhraseKeyPress}
value={this.state.passPhrase}
autoFocus={true}
/>
<DialogButtons primaryButton={_t('Next')}
<DialogButtons
primaryButton={_t('Next')}
onPrimaryButtonClick={this._onPassPhraseNext}
primaryIsSubmit={true}
hasCancel={true}
onCancel={this._onCancel}
focus={false}
/>
</div>
</form>
{_t(
"If you've forgotten your recovery passphrase you can "+
"<button1>use your recovery key</button1> or " +
@ -371,7 +361,6 @@ export default class RestoreKeyBackupDialog extends React.PureComponent {
<div className="mx_RestoreKeyBackupDialog_primaryContainer">
<input className="mx_RestoreKeyBackupDialog_recoveryKeyInput"
onChange={this._onRecoveryKeyChange}
onKeyPress={this._onRecoveryKeyKeyPress}
value={this.state.recoveryKey}
autoFocus={true}
/>

View file

@ -1,6 +1,6 @@
/*
Copyright 2018, 2019 New Vector Ltd
Copyright 2019 The Matrix.org Foundation C.I.C.
Copyright 2019, 2020 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.
@ -21,7 +21,6 @@ import * as sdk from '../../../../index';
import {MatrixClientPeg} from '../../../../MatrixClientPeg';
import { _t } from '../../../../languageHandler';
import { Key } from "../../../../Keyboard";
/*
* Access Secure Secret Storage by requesting the user's passphrase.
@ -68,7 +67,11 @@ export default class AccessSecretStorageDialog extends React.PureComponent {
});
}
_onPassPhraseNext = async () => {
_onPassPhraseNext = async (e) => {
e.preventDefault();
if (this.state.passPhrase.length <= 0) return;
this.setState({ keyMatches: null });
const input = { passphrase: this.state.passPhrase };
const keyMatches = await this.props.checkPrivateKey(input);
@ -79,7 +82,11 @@ export default class AccessSecretStorageDialog extends React.PureComponent {
}
}
_onRecoveryKeyNext = async () => {
_onRecoveryKeyNext = async (e) => {
e.preventDefault();
if (!this.state.recoveryKeyValid) return;
this.setState({ keyMatches: null });
const input = { recoveryKey: this.state.recoveryKey };
const keyMatches = await this.props.checkPrivateKey(input);
@ -97,18 +104,6 @@ export default class AccessSecretStorageDialog extends React.PureComponent {
});
}
_onPassPhraseKeyPress = (e) => {
if (e.key === Key.ENTER && this.state.passPhrase.length > 0) {
this._onPassPhraseNext();
}
}
_onRecoveryKeyKeyPress = (e) => {
if (e.key === Key.ENTER && this.state.recoveryKeyValid) {
this._onRecoveryKeyNext();
}
}
render() {
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
@ -135,7 +130,7 @@ export default class AccessSecretStorageDialog extends React.PureComponent {
)}
</div>;
} else {
keyStatus = <div className="mx_AccessSecretStorageDialog_keyStatus"></div>;
keyStatus = <div className="mx_AccessSecretStorageDialog_keyStatus" />;
}
content = <div>
@ -149,23 +144,25 @@ export default class AccessSecretStorageDialog extends React.PureComponent {
"identity for verifying other sessions by entering your passphrase.",
)}</p>
<div className="mx_AccessSecretStorageDialog_primaryContainer">
<input type="password"
<form className="mx_AccessSecretStorageDialog_primaryContainer" onSubmit={this._onPassPhraseNext}>
<input
type="password"
className="mx_AccessSecretStorageDialog_passPhraseInput"
onChange={this._onPassPhraseChange}
onKeyPress={this._onPassPhraseKeyPress}
value={this.state.passPhrase}
autoFocus={true}
autoComplete="new-password"
/>
{keyStatus}
<DialogButtons primaryButton={_t('Next')}
<DialogButtons
primaryButton={_t('Next')}
onPrimaryButtonClick={this._onPassPhraseNext}
hasCancel={true}
onCancel={this._onCancel}
focus={false}
primaryDisabled={this.state.passPhrase.length === 0}
/>
</div>
</form>
{_t(
"If you've forgotten your passphrase you can "+
"<button1>use your recovery key</button1> or " +
@ -192,11 +189,7 @@ export default class AccessSecretStorageDialog extends React.PureComponent {
let keyStatus;
if (this.state.recoveryKey.length === 0) {
keyStatus = <div className="mx_AccessSecretStorageDialog_keyStatus"></div>;
} else if (this.state.recoveryKeyValid) {
keyStatus = <div className="mx_AccessSecretStorageDialog_keyStatus">
{"\uD83D\uDC4D "}{_t("This looks like a valid recovery key!")}
</div>;
keyStatus = <div className="mx_AccessSecretStorageDialog_keyStatus" />;
} else if (this.state.keyMatches === false) {
keyStatus = <div className="mx_AccessSecretStorageDialog_keyStatus">
{"\uD83D\uDC4E "}{_t(
@ -204,6 +197,10 @@ export default class AccessSecretStorageDialog extends React.PureComponent {
"entered the correct recovery key.",
)}
</div>;
} else if (this.state.recoveryKeyValid) {
keyStatus = <div className="mx_AccessSecretStorageDialog_keyStatus">
{"\uD83D\uDC4D "}{_t("This looks like a valid recovery key!")}
</div>;
} else {
keyStatus = <div className="mx_AccessSecretStorageDialog_keyStatus">
{"\uD83D\uDC4E "}{_t("Not a valid recovery key")}
@ -221,22 +218,22 @@ export default class AccessSecretStorageDialog extends React.PureComponent {
"identity for verifying other sessions by entering your recovery key.",
)}</p>
<div className="mx_AccessSecretStorageDialog_primaryContainer">
<form className="mx_AccessSecretStorageDialog_primaryContainer" onSubmit={this._onRecoveryKeyNext}>
<input className="mx_AccessSecretStorageDialog_recoveryKeyInput"
onChange={this._onRecoveryKeyChange}
onKeyPress={this._onRecoveryKeyKeyPress}
value={this.state.recoveryKey}
autoFocus={true}
/>
{keyStatus}
<DialogButtons primaryButton={_t('Next')}
<DialogButtons
primaryButton={_t('Next')}
onPrimaryButtonClick={this._onRecoveryKeyNext}
hasCancel={true}
onCancel={this._onCancel}
focus={false}
primaryDisabled={!this.state.recoveryKeyValid}
/>
</div>
</form>
{_t(
"If you've forgotten your recovery key you can "+
"<button>set up new recovery options</button>."

View file

@ -211,7 +211,7 @@ const Pill = createReactClass({
if (room) {
linkText = "@room";
if (this.props.shouldShowPillAvatar) {
avatar = <RoomAvatar room={room} width={16} height={16} />;
avatar = <RoomAvatar room={room} width={16} height={16} aria-hidden="true" />;
}
pillClass = 'mx_AtRoomPill';
}
@ -225,7 +225,7 @@ const Pill = createReactClass({
member.rawDisplayName = member.rawDisplayName || '';
linkText = member.rawDisplayName;
if (this.props.shouldShowPillAvatar) {
avatar = <MemberAvatar member={member} width={16} height={16} />;
avatar = <MemberAvatar member={member} width={16} height={16} aria-hidden="true" />;
}
pillClass = 'mx_UserPill';
href = null;
@ -238,7 +238,7 @@ const Pill = createReactClass({
if (room) {
linkText = (room ? getDisplayAliasForRoom(room) : null) || resource;
if (this.props.shouldShowPillAvatar) {
avatar = <RoomAvatar room={room} width={16} height={16} />;
avatar = <RoomAvatar room={room} width={16} height={16} aria-hidden="true" />;
}
pillClass = 'mx_RoomPill';
}
@ -251,7 +251,7 @@ const Pill = createReactClass({
linkText = groupId;
if (this.props.shouldShowPillAvatar) {
avatar = <BaseAvatar name={name || groupId} width={16} height={16}
avatar = <BaseAvatar name={name || groupId} width={16} height={16} aria-hidden="true"
url={avatarUrl ? cli.mxcUrlToHttp(avatarUrl, 16, 16) : null} />;
}
pillClass = 'mx_GroupPill';

View file

@ -27,6 +27,7 @@ import {RIGHT_PANEL_PHASES} from "../../../stores/RightPanelStorePhases";
export default class MKeyVerificationRequest extends React.Component {
constructor(props) {
super(props);
this.state = {};
}
componentDidMount() {
@ -58,6 +59,7 @@ export default class MKeyVerificationRequest extends React.Component {
};
_onAcceptClicked = async () => {
this.setState({acceptOrCancelClicked: true});
const request = this.props.mxEvent.verificationRequest;
if (request) {
try {
@ -70,6 +72,7 @@ export default class MKeyVerificationRequest extends React.Component {
};
_onRejectClicked = async () => {
this.setState({acceptOrCancelClicked: true});
const request = this.props.mxEvent.verificationRequest;
if (request) {
try {
@ -135,9 +138,10 @@ export default class MKeyVerificationRequest extends React.Component {
subtitle = (<div className="mx_cryptoEvent_subtitle">{
userLabelForEventRoom(request.requestingUserId, mxEvent.getRoomId())}</div>);
if (request.requested && !request.observeOnly) {
const disabled = this.state.acceptOrCancelClicked;
stateNode = (<div className="mx_cryptoEvent_buttons">
<FormButton kind="danger" onClick={this._onRejectClicked} label={_t("Decline")} />
<FormButton onClick={this._onAcceptClicked} label={_t("Accept")} />
<FormButton disabled={disabled} kind="danger" onClick={this._onRejectClicked} label={_t("Decline")} />
<FormButton disabled={disabled} onClick={this._onAcceptClicked} label={_t("Accept")} />
</div>);
}
} else { // request sent by us

View file

@ -30,7 +30,7 @@ import {_t} from "../../../languageHandler";
// cancellation codes which constitute a key mismatch
const MISMATCHES = ["m.key_mismatch", "m.user_error", "m.mismatched_sas"];
const EncryptionPanel = ({verificationRequest, member, onClose}) => {
const EncryptionPanel = ({verificationRequest, member, onClose, layout}) => {
const [request, setRequest] = useState(verificationRequest);
useEffect(() => {
setRequest(verificationRequest);
@ -77,6 +77,7 @@ const EncryptionPanel = ({verificationRequest, member, onClose}) => {
} else {
return (
<VerificationPanel
layout={layout}
onClose={onClose}
member={member}
request={request}
@ -89,6 +90,7 @@ EncryptionPanel.propTypes = {
member: PropTypes.object.isRequired,
onClose: PropTypes.func.isRequired,
verificationRequest: PropTypes.object,
layout: PropTypes.string,
};
export default EncryptionPanel;

View file

@ -135,12 +135,37 @@ function useIsEncrypted(cli, room) {
return isEncrypted;
}
function verifyDevice(userId, device) {
const DeviceVerifyDialog = sdk.getComponent('views.dialogs.DeviceVerifyDialog');
Modal.createTrackedDialog('Device Verify Dialog', '', DeviceVerifyDialog, {
userId: userId,
device: device,
}, null, /* priority = */ false, /* static = */ true);
async function verifyDevice(userId, device) {
const cli = MatrixClientPeg.get();
const member = cli.getUser(userId);
const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog");
Modal.createTrackedDialog("Verification warning", "unverified session", QuestionDialog, {
headerImage: require("../../../../res/img/e2e/warning.svg"),
title: _t("Not Trusted"),
description: <div>
<p>{_t("%(name)s (%(userId)s) signed in to a new session without verifying it:", {name: member.displayName, userId})}</p>
<p>{device.getDisplayName()} ({device.deviceId})</p>
<p>{_t("Ask this user to verify their session, or manually verify it below.")}</p>
</div>,
onFinished: async (doneClicked) => {
const manuallyVerifyClicked = !doneClicked;
if (!manuallyVerifyClicked) {
return;
}
const cli = MatrixClientPeg.get();
const verificationRequest = await cli.requestVerification(
userId,
[device.deviceId],
);
dis.dispatch({
action: "set_right_panel_phase",
phase: RIGHT_PANEL_PHASES.EncryptionPanel,
refireParams: {member, verificationRequest},
});
},
primaryButton: _t("Done"),
cancelButton: _t("Manually Verify"),
});
}
function verifyUser(user) {

View file

@ -34,6 +34,7 @@ import Spinner from "../elements/Spinner";
export default class VerificationPanel extends React.PureComponent {
static propTypes = {
layout: PropTypes.string,
request: PropTypes.object.isRequired,
member: PropTypes.object.isRequired,
phase: PropTypes.oneOf([
@ -69,12 +70,40 @@ export default class VerificationPanel extends React.PureComponent {
const {member} = this.props;
const AccessibleButton = sdk.getComponent('elements.AccessibleButton');
if (this.props.layout === 'dialog') {
// HACK: This is a terrible idea.
let qrCode = <div className='mx_VerificationPanel_QRPhase_noQR'><Spinner /></div>;
if (this.state.qrCodeProps) {
qrCode = <VerificationQRCode {...this.state.qrCodeProps} />;
}
return (
<div>
{_t("Verify this session by completing one of the following:")}
<div className='mx_VerificationPanel_QRPhase_startOptions'>
<div className='mx_VerificationPanel_QRPhase_startOption'>
<p>{_t("Scan this unique code")}</p>
{qrCode}
</div>
<div className='mx_VerificationPanel_QRPhase_betweenText'>{_t("or")}</div>
<div className='mx_VerificationPanel_QRPhase_startOption'>
<p>{_t("Compare unique emoji")}</p>
<span className='mx_VerificationPanel_QRPhase_helpText'>{_t("Compare a unique set of emoji if you don't have a camera on either device")}</span>
<AccessibleButton disabled={this.state.emojiButtonClicked} onClick={this._startSAS} kind='primary'>
{_t("Start")}
</AccessibleButton>
</div>
</div>
</div>
);
}
let button;
if (pending) {
button = <Spinner />;
} else {
const disabled = this.state.emojiButtonClicked;
button = (
<AccessibleButton kind="primary" className="mx_UserInfo_wideButton" onClick={this._startSAS}>
<AccessibleButton disabled={disabled} kind="primary" className="mx_UserInfo_wideButton" onClick={this._startSAS}>
{_t("Verify by emoji")}
</AccessibleButton>
);
@ -82,9 +111,8 @@ export default class VerificationPanel extends React.PureComponent {
if (!this.state.qrCodeProps) {
return <div className="mx_UserInfo_container">
<h3>Verify by emoji</h3>
<h3>{_t("Verify by emoji")}</h3>
<p>{_t("Verify by comparing unique emoji.")}</p>
{ button }
</div>;
}
@ -92,7 +120,7 @@ export default class VerificationPanel extends React.PureComponent {
// TODO: add way to open camera to scan a QR code
return <React.Fragment>
<div className="mx_UserInfo_container">
<h3>Verify by scanning</h3>
<h3>{_t("Verify by scanning")}</h3>
<p>{_t("Ask %(displayName)s to scan your code:", {
displayName: member.displayName || member.name || member.userId,
})}</p>
@ -103,7 +131,7 @@ export default class VerificationPanel extends React.PureComponent {
</div>
<div className="mx_UserInfo_container">
<h3>Verify by emoji</h3>
<h3>{_t("Verify by emoji")}</h3>
<p>{_t("If you can't scan the code above, verify by comparing unique emoji.")}</p>
{ button }
@ -121,7 +149,7 @@ export default class VerificationPanel extends React.PureComponent {
<p>{_t("You've successfully verified %(displayName)s!", {
displayName: member.displayName || member.name || member.userId,
})}</p>
<E2EIcon isUser={true} status="verified" size={128} />
<E2EIcon isUser={true} status="verified" size={128} hideTooltip={true} />
<p>Verify all users in a room to ensure it's secure.</p>
<AccessibleButton kind="primary" className="mx_UserInfo_wideButton" onClick={this.props.onClose}>
@ -192,6 +220,7 @@ export default class VerificationPanel extends React.PureComponent {
}
_startSAS = async () => {
this.setState({emojiButtonClicked: true});
const verifier = this.props.request.beginKeyVerification(verificationMethods.SAS);
try {
await verifier.verify();

View file

@ -51,7 +51,7 @@ const legacyRoomTitles = {
[E2E_STATE.VERIFIED]: _td("All sessions in this encrypted room are trusted"),
};
const E2EIcon = ({isUser, status, className, size, onClick}) => {
const E2EIcon = ({isUser, status, className, size, onClick, hideTooltip}) => {
const [hover, setHover] = useState(false);
const classes = classNames({
@ -82,7 +82,7 @@ const E2EIcon = ({isUser, status, className, size, onClick}) => {
const onMouseOut = () => setHover(false);
let tip;
if (hover) {
if (hover && !hideTooltip) {
tip = <Tooltip label={e2eTitle ? _t(e2eTitle) : ""} />;
}

View file

@ -253,20 +253,24 @@ export default createReactClass({
<form className={this.props.className} onSubmit={this.onClickChange}>
{ currentPassword }
<div className={rowClassName}>
<Field id="mx_ChangePassword_newPassword"
<Field
id="mx_ChangePassword_newPassword"
type="password"
label={passwordLabel}
value={this.state.newPassword}
autoFocus={this.props.autoFocusNewPasswordInput}
onChange={this.onChangeNewPassword}
autoComplete="new-password"
/>
</div>
<div className={rowClassName}>
<Field id="mx_ChangePassword_newPasswordConfirm"
<Field
id="mx_ChangePassword_newPasswordConfirm"
type="password"
label={_t("Confirm password")}
value={this.state.newPasswordConfirm}
onChange={this.onChangeNewPasswordConfirm}
autoComplete="new-password"
/>
</div>
<AccessibleButton className={buttonClassName} kind={this.props.buttonKind} onClick={this.onClickChange}>

View file

@ -20,6 +20,7 @@ import {MatrixClientPeg} from '../../../MatrixClientPeg';
import { _t } from '../../../languageHandler';
import * as sdk from '../../../index';
import { accessSecretStorage } from '../../../CrossSigningManager';
import Modal from '../../../Modal';
export default class CrossSigningPanel extends React.PureComponent {
constructor(props) {
@ -86,11 +87,12 @@ export default class CrossSigningPanel extends React.PureComponent {
* 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} [force] Bootstrap again even if keys already present
*/
_bootstrapSecureSecretStorage = async () => {
_bootstrapSecureSecretStorage = async (force=false) => {
this.setState({ error: null });
try {
await accessSecretStorage();
await accessSecretStorage(() => undefined, force);
} catch (e) {
this.setState({ error: e });
console.error("Error bootstrapping secret storage", e);
@ -99,6 +101,18 @@ export default class CrossSigningPanel extends React.PureComponent {
this._getUpdatedStatus();
}
onDestroyStorage = (act) => {
if (!act) return;
this._bootstrapSecureSecretStorage(true);
}
_destroySecureSecretStorage = () => {
const ConfirmDestoryCrossSigningDialog = sdk.getComponent("dialogs.ConfirmDestroyCrossSigningDialog");
Modal.createDialog(ConfirmDestoryCrossSigningDialog, {
onFinished: this.onDestroyStorage,
});
}
render() {
const AccessibleButton = sdk.getComponent("elements.AccessibleButton");
const {
@ -142,6 +156,12 @@ export default class CrossSigningPanel extends React.PureComponent {
{_t("Bootstrap cross-signing and secret storage")}
</AccessibleButton>
</div>;
} else {
bootstrapButton = <div className="mx_CrossSigningPanel_buttonRow">
<AccessibleButton kind="danger" onClick={this._destroySecureSecretStorage}>
{_t("Reset cross-signing and secret storage")}
</AccessibleButton>
</div>;
}
return (

View file

@ -32,6 +32,7 @@ export default class PreferencesUserSettingsTab extends React.Component {
];
static TIMELINE_SETTINGS = [
'showTypingNotifications',
'autoplayGifsAndVideos',
'urlPreviewsEnabled',
'TextualBody.enableBigEmoji',

View file

@ -58,10 +58,7 @@ export default class VerificationRequestToast extends React.PureComponent {
_checkRequestIsPending = () => {
const {request} = this.props;
const isPendingInRoomRequest = request.channel.roomId &&
!(request.ready || request.started || request.done || request.cancelled || request.observeOnly);
const isPendingDeviceRequest = request.channel.deviceId && request.started;
if (!isPendingInRoomRequest && !isPendingDeviceRequest) {
if (request.ready || request.done || request.cancelled || request.observeOnly) {
ToastStore.sharedInstance().dismissToast(this.props.toastKey);
}
};
@ -79,15 +76,15 @@ export default class VerificationRequestToast extends React.PureComponent {
ToastStore.sharedInstance().dismissToast(this.props.toastKey);
const {request} = this.props;
// no room id for to_device requests
const cli = MatrixClientPeg.get();
try {
await request.accept();
if (request.channel.roomId) {
dis.dispatch({
action: 'view_room',
room_id: request.channel.roomId,
should_peek: false,
});
await request.accept();
const cli = MatrixClientPeg.get();
dis.dispatch({
action: "set_right_panel_phase",
phase: RIGHT_PANEL_PHASES.EncryptionPanel,
@ -96,11 +93,10 @@ export default class VerificationRequestToast extends React.PureComponent {
member: cli.getUser(request.otherUserId),
},
});
} else if (request.channel.deviceId && request.verifier) {
// show to_device verifications in dialog still
const IncomingSasDialog = sdk.getComponent("views.dialogs.IncomingSasDialog");
Modal.createTrackedDialog('Incoming Verification', '', IncomingSasDialog, {
verifier: request.verifier,
} else {
const VerificationRequestDialog = sdk.getComponent("views.dialogs.VerificationRequestDialog");
Modal.createTrackedDialog('Incoming Verification', '', VerificationRequestDialog, {
verificationRequest: request,
}, null, /* priority = */ false, /* static = */ true);
}
} catch (err) {

View file

@ -0,0 +1,82 @@
/*
Copyright 2020 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 PropTypes from 'prop-types';
import { _t } from '../../../languageHandler';
import AccessibleButton from "../elements/AccessibleButton";
import {replaceableComponent} from "../../../utils/replaceableComponent";
import VerificationQRCode from "../elements/crypto/VerificationQRCode";
import {VerificationRequest} from "matrix-js-sdk/src/crypto/verification/request/VerificationRequest";
import Spinner from "../elements/Spinner";
@replaceableComponent("views.verification.VerificationQREmojiOptions")
export default class VerificationQREmojiOptions extends React.Component {
static propTypes = {
request: PropTypes.object.isRequired,
onCancel: PropTypes.func.isRequired,
onStartEmoji: PropTypes.func.isRequired,
};
constructor(props) {
super(props);
this.state = {
qrProps: null,
};
this._prepareQrCode(props.request);
}
async _prepareQrCode(request: VerificationRequest) {
try {
const props = await VerificationQRCode.getPropsForRequest(request);
this.setState({qrProps: props});
} catch (e) {
console.error(e);
// We just won't show a QR code
}
}
render() {
let qrCode = <div className='mx_VerificationQREmojiOptions_noQR'><Spinner /></div>;
if (this.state.qrProps) {
qrCode = <VerificationQRCode {...this.state.qrProps} />;
}
return (
<div>
{_t("Verify this session by completing one of the following:")}
<div className='mx_IncomingSasDialog_startOptions'>
<div className='mx_IncomingSasDialog_startOption'>
<p>{_t("Scan this unique code")}</p>
{qrCode}
</div>
<div className='mx_IncomingSasDialog_betweenText'>{_t("or")}</div>
<div className='mx_IncomingSasDialog_startOption'>
<p>{_t("Compare unique emoji")}</p>
<span className='mx_IncomingSasDialog_helpText'>{_t("Compare a unique set of emoji if you don't have a camera on either device")}</span>
<AccessibleButton onClick={this.props.onStartEmoji} kind='primary'>
{_t("Start")}
</AccessibleButton>
</div>
</div>
<AccessibleButton onClick={this.props.onCancel} kind='danger'>
{_t("Cancel")}
</AccessibleButton>
</div>
);
}
}