Merge branches 'develop' and 'travis/download-logs' of github.com:matrix-org/matrix-react-sdk into travis/download-logs

 Conflicts:
	src/i18n/strings/en_EN.json
	src/rageshake/submit-rageshake.ts
This commit is contained in:
Michael Telatynski 2020-07-21 22:22:28 +01:00
commit 70c81cc377
1182 changed files with 47388 additions and 27457 deletions

View file

@ -24,7 +24,7 @@ import createReactClass from 'create-react-class';
import { _t, _td } from '../../../languageHandler';
import * as sdk from '../../../index';
import {MatrixClientPeg} from '../../../MatrixClientPeg';
import dis from '../../../dispatcher';
import dis from '../../../dispatcher/dispatcher';
import { addressTypes, getAddressType } from '../../../UserAddress.js';
import GroupStore from '../../../stores/GroupStore';
import * as Email from '../../../email';
@ -33,6 +33,7 @@ import { getDefaultIdentityServerUrl, useDefaultIdentityServer } from '../../../
import { abbreviateUrl } from '../../../utils/UrlUtils';
import {sleep} from "../../../utils/promise";
import {Key} from "../../../Keyboard";
import {Action} from "../../../dispatcher/actions";
const TRUNCATE_QUERY_LIST = 40;
const QUERY_USER_DIRECTORY_DEBOUNCE_MS = 200;
@ -107,6 +108,7 @@ export default createReactClass({
};
},
// TODO: [REACT-WARNING] Replace component with real class, use constructor for refs
UNSAFE_componentWillMount: function() {
this._textinput = createRef();
},
@ -614,7 +616,7 @@ export default createReactClass({
onManageSettingsClick(e) {
e.preventDefault();
dis.dispatch({ action: 'view_user_settings' });
dis.fire(Action.ViewUserSettings);
this.onCancel();
},

View file

@ -75,8 +75,12 @@ export default createReactClass({
// If provided, this is used to add a aria-describedby attribute
contentId: PropTypes.string,
// optional additional class for the title element
titleClass: PropTypes.string,
// optional additional class for the title element (basically anything that can be passed to classnames)
titleClass: PropTypes.oneOfType([
PropTypes.string,
PropTypes.object,
PropTypes.arrayOf(PropTypes.string),
]),
},
getDefaultProps: function() {
@ -86,7 +90,8 @@ export default createReactClass({
};
},
componentWillMount() {
// TODO: [REACT-WARNING] Move this to constructor
UNSAFE_componentWillMount() {
this._matrixClient = MatrixClientPeg.get();
},
@ -143,6 +148,7 @@ export default createReactClass({
>
<div className={classNames('mx_Dialog_header', {
'mx_Dialog_headerWithButton': !!this.props.headerButton,
'mx_Dialog_headerWithCancel': !!cancelButton,
})}>
<div className={classNames('mx_Dialog_title', this.props.titleClass)} id='mx_BaseDialog_title'>
{headerImage}

View file

@ -164,12 +164,20 @@ export default class BugReportDialog extends React.Component {
);
}
let warning;
if (window.Modernizr && Object.values(window.Modernizr).some(support => support === false)) {
warning = <p><b>
{ _t("Reminder: Your browser is unsupported, so your experience may be unpredictable.") }
</b></p>;
}
return (
<BaseDialog className="mx_BugReportDialog" onFinished={this._onCancel}
title={_t('Submit debug logs')}
contentId='mx_Dialog_content'
>
<div className="mx_Dialog_content" id='mx_Dialog_content'>
{ warning }
<p>
{ _t(
"Debug logs contain application usage data including your " +

View file

@ -55,7 +55,8 @@ export default createReactClass({
askReason: false,
}),
componentWillMount: function() {
// TODO: [REACT-WARNING] Move this to constructor
UNSAFE_componentWillMount: function() {
this._reasonField = null;
},

View file

@ -18,7 +18,7 @@ import React from 'react';
import createReactClass from 'create-react-class';
import PropTypes from 'prop-types';
import * as sdk from '../../../index';
import dis from '../../../dispatcher';
import dis from '../../../dispatcher/dispatcher';
import { _t } from '../../../languageHandler';
import {MatrixClientPeg} from '../../../MatrixClientPeg';

View file

@ -24,17 +24,20 @@ import withValidation from '../elements/Validation';
import { _t } from '../../../languageHandler';
import {MatrixClientPeg} from '../../../MatrixClientPeg';
import {Key} from "../../../Keyboard";
import {privateShouldBeEncrypted} from "../../../createRoom";
export default createReactClass({
displayName: 'CreateRoomDialog',
propTypes: {
onFinished: PropTypes.func.isRequired,
defaultPublic: PropTypes.bool,
},
getInitialState() {
const config = SdkConfig.get();
return {
isPublic: false,
isPublic: this.props.defaultPublic || false,
isEncrypted: privateShouldBeEncrypted(),
name: "",
topic: "",
alias: "",
@ -62,6 +65,11 @@ export default createReactClass({
if (this.state.noFederate) {
createOpts.creation_content = {'m.federate': false};
}
if (!this.state.isPublic) {
opts.encryption = this.state.isEncrypted;
}
return opts;
},
@ -127,6 +135,10 @@ export default createReactClass({
this.setState({isPublic});
},
onEncryptedChange(isEncrypted) {
this.setState({isEncrypted});
},
onAliasChange(alias) {
this.setState({alias});
},
@ -166,11 +178,10 @@ export default createReactClass({
const LabelledToggleSwitch = sdk.getComponent('views.elements.LabelledToggleSwitch');
const RoomAliasField = sdk.getComponent('views.elements.RoomAliasField');
let privateLabel;
let publicLabel;
let publicPrivateLabel;
let aliasField;
if (this.state.isPublic) {
publicLabel = (<p>{_t("Set a room alias to easily share your room with other people.")}</p>);
publicPrivateLabel = (<p>{_t("Set a room address to easily share your room with other people.")}</p>);
const domain = MatrixClientPeg.get().getDomain();
aliasField = (
<div className="mx_CreateRoomDialog_aliasContainer">
@ -178,7 +189,27 @@ export default createReactClass({
</div>
);
} else {
privateLabel = (<p>{_t("This room is private, and can only be joined by invitation.")}</p>);
publicPrivateLabel = (<p>{_t("This room is private, and can only be joined by invitation.")}</p>);
}
let e2eeSection;
if (!this.state.isPublic) {
let microcopy;
if (privateShouldBeEncrypted()) {
microcopy = _t("You cant disable this later. Bridges & most bots wont work yet.");
} else {
microcopy = _t("Your server admin has disabled end-to-end encryption by default " +
"in private rooms & Direct Messages.");
}
e2eeSection = <React.Fragment>
<LabelledToggleSwitch
label={ _t("Enable end-to-end encryption")}
onChange={this.onEncryptedChange}
value={this.state.isEncrypted}
className='mx_CreateRoomDialog_e2eSwitch' // for end-to-end tests
/>
<p>{ microcopy }</p>
</React.Fragment>;
}
const title = this.state.isPublic ? _t('Create a public room') : _t('Create a private room');
@ -189,10 +220,10 @@ export default createReactClass({
<form onSubmit={this.onOk} onKeyDown={this._onKeyDown}>
<div className="mx_Dialog_content">
<Field ref={ref => this._nameFieldRef = ref} label={ _t('Name') } onChange={this.onNameChange} onValidate={this.onNameValidate} value={this.state.name} className="mx_CreateRoomDialog_name" />
<Field label={ _t('Topic (optional)') } onChange={this.onTopicChange} value={this.state.topic} />
<Field label={ _t('Topic (optional)') } onChange={this.onTopicChange} value={this.state.topic} className="mx_CreateRoomDialog_topic" />
<LabelledToggleSwitch label={ _t("Make this room public")} onChange={this.onPublicChange} value={this.state.isPublic} />
{ privateLabel }
{ publicLabel }
{ publicPrivateLabel }
{ e2eeSection }
{ aliasField }
<details ref={this.collectDetailsRef} className="mx_CreateRoomDialog_details">
<summary className="mx_CreateRoomDialog_details_summary">{ this.state.detailsOpen ? _t('Hide advanced') : _t('Show advanced') }</summary>

View file

@ -1,5 +1,6 @@
/*
Copyright 2018 New Vector Ltd
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.
@ -16,11 +17,14 @@ limitations under the License.
import React from 'react';
import * as sdk from '../../../index';
import dis from '../../../dispatcher';
import dis from '../../../dispatcher/dispatcher';
import { _t } from '../../../languageHandler';
import SdkConfig from '../../../SdkConfig';
import Modal from '../../../Modal';
export default (props) => {
const brand = SdkConfig.get().brand;
const _onLogoutClicked = () => {
const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog");
Modal.createTrackedDialog('Logout e2e db too new', '', QuestionDialog, {
@ -28,7 +32,8 @@ export default (props) => {
description: _t(
"To avoid losing your chat history, you must export your room keys " +
"before logging out. You will need to go back to the newer version of " +
"Riot to do this",
"%(brand)s to do this",
{ brand },
),
button: _t("Sign out"),
focus: false,
@ -42,10 +47,11 @@ export default (props) => {
};
const description =
_t("You've previously used a newer version of Riot on %(host)s. " +
_t(
"You've previously used a newer version of %(brand)s with this session. " +
"To use this version again with end to end encryption, you will " +
"need to sign out and back in again. ",
{host: props.host},
"need to sign out and back in again.",
{ brand },
);
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');

View file

@ -1,6 +1,6 @@
/*
Copyright 2016 OpenMarket 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.
@ -23,106 +23,161 @@ import Analytics from '../../../Analytics';
import {MatrixClientPeg} from '../../../MatrixClientPeg';
import * as Lifecycle from '../../../Lifecycle';
import { _t } from '../../../languageHandler';
import InteractiveAuth, {ERROR_USER_CANCELLED} from "../../structures/InteractiveAuth";
import {DEFAULT_PHASE, PasswordAuthEntry, SSOAuthEntry} from "../auth/InteractiveAuthEntryComponents";
import StyledCheckbox from "../elements/StyledCheckbox";
export default class DeactivateAccountDialog extends React.Component {
constructor(props) {
super(props);
this._onOk = this._onOk.bind(this);
this._onCancel = this._onCancel.bind(this);
this._onPasswordFieldChange = this._onPasswordFieldChange.bind(this);
this._onEraseFieldChange = this._onEraseFieldChange.bind(this);
this.state = {
password: "",
busy: false,
shouldErase: false,
errStr: null,
authData: null, // for UIA
authEnabled: true, // see usages for information
// A few strings that are passed to InteractiveAuth for design or are displayed
// next to the InteractiveAuth component.
bodyText: null,
continueText: null,
continueKind: null,
};
this._initAuth(/* shouldErase= */false);
}
_onPasswordFieldChange(ev) {
this.setState({
password: ev.target.value,
});
}
_onStagePhaseChange = (stage, phase) => {
const dialogAesthetics = {
[SSOAuthEntry.PHASE_PREAUTH]: {
body: _t("Confirm your account deactivation by using Single Sign On to prove your identity."),
continueText: _t("Single Sign On"),
continueKind: "danger",
},
[SSOAuthEntry.PHASE_POSTAUTH]: {
body: _t("Are you sure you want to deactivate your account? This is irreversible."),
continueText: _t("Confirm account deactivation"),
continueKind: "danger",
},
};
_onEraseFieldChange(ev) {
this.setState({
shouldErase: ev.target.checked,
});
}
async _onOk() {
this.setState({busy: true});
try {
// This assumes that the HS requires password UI auth
// for this endpoint. In reality it could be any UI auth.
const auth = {
type: 'm.login.password',
// TODO: Remove `user` once servers support proper UIA
// See https://github.com/vector-im/riot-web/issues/10312
user: MatrixClientPeg.get().credentials.userId,
identifier: {
type: "m.id.user",
user: MatrixClientPeg.get().credentials.userId,
// This is the same as aestheticsForStagePhases in InteractiveAuthDialog minus the `title`
const DEACTIVATE_AESTHETICS = {
[SSOAuthEntry.LOGIN_TYPE]: dialogAesthetics,
[SSOAuthEntry.UNSTABLE_LOGIN_TYPE]: dialogAesthetics,
[PasswordAuthEntry.LOGIN_TYPE]: {
[DEFAULT_PHASE]: {
body: _t("To continue, please enter your password:"),
},
password: this.state.password,
};
await MatrixClientPeg.get().deactivateAccount(auth, this.state.shouldErase);
} catch (err) {
let errStr = _t('Unknown error');
// https://matrix.org/jira/browse/SYN-744
if (err.httpStatus === 401 || err.httpStatus === 403) {
errStr = _t('Incorrect password');
}
this.setState({
busy: false,
errStr: errStr,
});
},
};
const aesthetics = DEACTIVATE_AESTHETICS[stage];
let bodyText = null;
let continueText = null;
let continueKind = null;
if (aesthetics) {
const phaseAesthetics = aesthetics[phase];
if (phaseAesthetics && phaseAesthetics.body) bodyText = phaseAesthetics.body;
if (phaseAesthetics && phaseAesthetics.continueText) continueText = phaseAesthetics.continueText;
if (phaseAesthetics && phaseAesthetics.continueKind) continueKind = phaseAesthetics.continueKind;
}
this.setState({bodyText, continueText, continueKind});
};
_onUIAuthFinished = (success, result, extra) => {
if (success) return; // great! makeRequest() will be called too.
if (result === ERROR_USER_CANCELLED) {
this._onCancel();
return;
}
Analytics.trackEvent('Account', 'Deactivate Account');
Lifecycle.onLoggedOut();
this.props.onFinished(true);
}
console.error("Error during UI Auth:", {result, extra});
this.setState({errStr: _t("There was a problem communicating with the server. Please try again.")});
};
_onUIAuthComplete = (auth) => {
MatrixClientPeg.get().deactivateAccount(auth, this.state.shouldErase).then(r => {
// Deactivation worked - logout & close this dialog
Analytics.trackEvent('Account', 'Deactivate Account');
Lifecycle.onLoggedOut();
this.props.onFinished(true);
}).catch(e => {
console.error(e);
this.setState({errStr: _t("There was a problem communicating with the server. Please try again.")});
});
};
_onEraseFieldChange = (ev) => {
this.setState({
shouldErase: ev.target.checked,
// Disable the auth form because we're going to have to reinitialize the auth
// information. We do this because we can't modify the parameters in the UIA
// session, and the user will have selected something which changes the request.
// Therefore, we throw away the last auth session and try a new one.
authEnabled: false,
});
// As mentioned above, set up for auth again to get updated UIA session info
this._initAuth(/* shouldErase= */ev.target.checked);
};
_onCancel() {
this.props.onFinished(false);
}
_initAuth(shouldErase) {
MatrixClientPeg.get().deactivateAccount(null, shouldErase).then(r => {
// If we got here, oops. The server didn't require any auth.
// Our application lifecycle will catch the error and do the logout bits.
// We'll try to log something in an vain attempt to record what happened (storage
// is also obliterated on logout).
console.warn("User's account got deactivated without confirmation: Server had no auth");
this.setState({errStr: _t("Server did not require any authentication")});
}).catch(e => {
if (e && e.httpStatus === 401 && e.data) {
// Valid UIA response
this.setState({authData: e.data, authEnabled: true});
} else {
this.setState({errStr: _t("Server did not return valid authentication information.")});
}
});
}
render() {
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
const Loader = sdk.getComponent("elements.Spinner");
let passwordBoxClass = '';
let error = null;
if (this.state.errStr) {
error = <div className="error">
{ this.state.errStr }
</div>;
passwordBoxClass = 'error';
}
const okLabel = this.state.busy ? <Loader /> : _t('Deactivate Account');
const okEnabled = this.state.password && !this.state.busy;
let cancelButton = null;
if (!this.state.busy) {
cancelButton = <button onClick={this._onCancel} autoFocus={true}>
{ _t("Cancel") }
</button>;
let auth = <div>{_t("Loading...")}</div>;
if (this.state.authData && this.state.authEnabled) {
auth = (
<div>
{this.state.bodyText}
<InteractiveAuth
matrixClient={MatrixClientPeg.get()}
authData={this.state.authData}
makeRequest={this._onUIAuthComplete}
onAuthFinished={this._onUIAuthFinished}
onStagePhaseChange={this._onStagePhaseChange}
continueText={this.state.continueText}
continueKind={this.state.continueKind}
/>
</div>
);
}
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}
onEnterPressed={this.onOk}
titleClass="danger"
title={_t("Deactivate Account")}
>
@ -155,45 +210,24 @@ export default class DeactivateAccountDialog extends React.Component {
<div className="mx_DeactivateAccountDialog_input_section">
<p>
<label htmlFor="mx_DeactivateAccountDialog_erase_account_input">
<input
id="mx_DeactivateAccountDialog_erase_account_input"
type="checkbox"
checked={this.state.shouldErase}
onChange={this._onEraseFieldChange}
/>
{ _t(
<StyledCheckbox
checked={this.state.shouldErase}
onChange={this._onEraseFieldChange}
>
{_t(
"Please forget all messages I have sent when my account is deactivated " +
"(<b>Warning:</b> this will cause future users to see an incomplete view " +
"of conversations)",
{},
{ b: (sub) => <b>{ sub }</b> },
) }
</label>
)}
</StyledCheckbox>
</p>
<p>{ _t("To continue, please enter your password:") }</p>
<Field
type="password"
label={_t('Password')}
onChange={this._onPasswordFieldChange}
value={this.state.password}
className={passwordBoxClass}
/>
{error}
{auth}
</div>
{ error }
</div>
<div className="mx_Dialog_buttons">
<button
className="mx_Dialog_primary danger"
onClick={this._onOk}
disabled={!okEnabled}
>
{ okLabel }
</button>
{ cancelButton }
</div>
</BaseDialog>
);

View file

@ -1,376 +0,0 @@
/*
Copyright 2016 OpenMarket Ltd
Copyright 2017 Vector Creations Ltd
Copyright 2019 New Vector Ltd
Copyright 2019 Michael Telatynski <7t3chguy@gmail.com>
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 * as FormattingUtils from '../../../utils/FormattingUtils';
import { _t } from '../../../languageHandler';
import {verificationMethods} from 'matrix-js-sdk/src/crypto';
import {ensureDMExists} from "../../../createRoom";
import dis from "../../../dispatcher";
import SettingsStore from '../../../settings/SettingsStore';
import {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_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 = {
userId: PropTypes.string.isRequired,
device: PropTypes.object.isRequired,
onFinished: PropTypes.func.isRequired,
};
constructor() {
super();
this._verifier = null;
this._showSasEvent = null;
this._request = null;
this.state = {
phase: PHASE_START,
mode: MODE_SAS,
sasVerified: false,
};
}
componentWillUnmount() {
if (this._verifier) {
this._verifier.removeListener('show_sas', this._onVerifierShowSas);
this._verifier.cancel('User cancel');
}
}
_onSwitchToLegacyClick = () => {
if (this._verifier) {
this._verifier.removeListener('show_sas', this._onVerifierShowSas);
this._verifier.cancel('User cancel');
this._verifier = null;
}
this.setState({mode: MODE_LEGACY});
}
_onSwitchToSasClick = () => {
this.setState({mode: MODE_SAS});
}
_onCancelClick = () => {
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(
this.props.userId, this.props.device.deviceId, true,
);
}
this.props.onFinished(confirm);
}
_onSasRequestClick = async () => {
this.setState({
phase: PHASE_WAIT_FOR_PARTNER_TO_ACCEPT,
});
const client = MatrixClientPeg.get();
const verifyingOwnDevice = this.props.userId === client.getUserId();
try {
if (!verifyingOwnDevice && SettingsStore.getValue("feature_cross_signing")) {
const roomId = await ensureDMExistsAndOpen(this.props.userId);
// throws upon cancellation before having started
const request = await client.requestVerificationDM(
this.props.userId, roomId,
);
await request.waitFor(r => r.ready || r.started);
if (request.ready) {
this._verifier = request.beginKeyVerification(verificationMethods.SAS);
} 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,
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();
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;
}
}
_onSasMatchesClick = () => {
this._showSasEvent.confirm();
this.setState({
phase: PHASE_WAIT_FOR_PARTNER_TO_CONFIRM,
});
}
_onVerifiedDoneClick = () => {
this.props.onFinished(true);
}
_onVerifierShowSas = (e) => {
this._showSasEvent = e;
this.setState({
phase: PHASE_SHOW_SAS,
});
}
_renderSasVerification() {
let body;
switch (this.state.phase) {
case PHASE_START:
body = this._renderVerificationPhaseStart();
break;
case PHASE_WAIT_FOR_PARTNER_TO_ACCEPT:
body = this._renderVerificationPhaseWaitAccept();
break;
case PHASE_PICK_VERIFICATION_OPTION:
body = this._renderVerificationPhasePick();
break;
case PHASE_SHOW_SAS:
body = this._renderSasVerificationPhaseShowSas();
break;
case PHASE_WAIT_FOR_PARTNER_TO_CONFIRM:
body = this._renderSasVerificationPhaseWaitForPartnerToConfirm();
break;
case PHASE_VERIFIED:
body = this._renderVerificationPhaseVerified();
break;
case PHASE_CANCELLED:
body = this._renderVerificationPhaseCancelled();
break;
}
const BaseDialog = sdk.getComponent("dialogs.BaseDialog");
return (
<BaseDialog
title={_t("Verify session")}
onFinished={this._onCancelClick}
>
{body}
</BaseDialog>
);
}
_renderVerificationPhaseStart() {
const AccessibleButton = sdk.getComponent('views.elements.AccessibleButton');
const DialogButtons = sdk.getComponent('views.elements.DialogButtons');
return (
<div>
<AccessibleButton
element="span" className="mx_linkButton" onClick={this._onSwitchToLegacyClick}
>
{_t("Use Legacy Verification (for older clients)")}
</AccessibleButton>
<p>
{ _t("Verify by comparing a short text string.") }
</p>
<p>
{_t("To be secure, do this in person or use a trusted way to communicate.")}
</p>
<DialogButtons
primaryButton={_t('Begin Verifying')}
hasCancel={true}
onPrimaryButtonClick={this._onSasRequestClick}
onCancel={this._onCancelClick}
/>
</div>
);
}
_renderVerificationPhaseWaitAccept() {
const Spinner = sdk.getComponent("views.elements.Spinner");
const AccessibleButton = sdk.getComponent('views.elements.AccessibleButton');
return (
<div>
<Spinner />
<p>{_t("Waiting for partner to accept...")}</p>
<p>{_t(
"Nothing appearing? Not all clients support interactive verification yet. " +
"<button>Use legacy verification</button>.",
{}, {button: sub => <AccessibleButton element='span' className="mx_linkButton"
onClick={this._onSwitchToLegacyClick}
>
{sub}
</AccessibleButton>},
)}</p>
</div>
);
}
_renderVerificationPhasePick() {
return <VerificationQREmojiOptions
request={this._request}
onCancel={this._onCancelClick}
onStartEmoji={this._onUseSasClick}
/>;
}
_renderSasVerificationPhaseShowSas() {
const VerificationShowSas = sdk.getComponent('views.verification.VerificationShowSas');
return <VerificationShowSas
sas={this._showSasEvent.sas}
onCancel={this._onCancelClick}
onDone={this._onSasMatchesClick}
isSelf={MatrixClientPeg.get().getUserId() === this.props.userId}
onStartEmoji={this._onUseSasClick}
/>;
}
_renderSasVerificationPhaseWaitForPartnerToConfirm() {
const Spinner = sdk.getComponent('views.elements.Spinner');
return <div>
<Spinner />
<p>{_t(
"Waiting for %(userId)s to confirm...", {userId: this.props.userId},
)}</p>
</div>;
}
_renderVerificationPhaseVerified() {
const VerificationComplete = sdk.getComponent('views.verification.VerificationComplete');
return <VerificationComplete onDone={this._onVerifiedDoneClick} />;
}
_renderVerificationPhaseCancelled() {
const VerificationCancelled = sdk.getComponent('views.verification.VerificationCancelled');
return <VerificationCancelled onDone={this._onCancelClick} />;
}
_renderLegacyVerification() {
const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog");
const AccessibleButton = sdk.getComponent('views.elements.AccessibleButton');
let text;
if (MatrixClientPeg.get().getUserId() === this.props.userId) {
text = _t("To verify that this session can be trusted, please check that the key you see " +
"in User Settings on that device matches the key below:");
} else {
text = _t("To verify that this session can be trusted, please contact its owner using some other " +
"means (e.g. in person or a phone call) and ask them whether the key they see in their User Settings " +
"for this session matches the key below:");
}
const key = FormattingUtils.formatCryptoKey(this.props.device.getFingerprint());
const body = (
<div>
<AccessibleButton
element="span" className="mx_linkButton" onClick={this._onSwitchToSasClick}
>
{_t("Use two-way text verification")}
</AccessibleButton>
<p>
{ text }
</p>
<div className="mx_DeviceVerifyDialog_cryptoSection">
<ul>
<li><label>{ _t("Session name") }:</label> <span>{ this.props.device.getDisplayName() }</span></li>
<li><label>{ _t("Session ID") }:</label> <span><code>{ this.props.device.deviceId }</code></span></li>
<li><label>{ _t("Session key") }:</label> <span><code><b>{ key }</b></code></span></li>
</ul>
</div>
<p>
{ _t("If it matches, press the verify button below. " +
"If it doesn't, then someone else is intercepting this session " +
"and you probably want to press the blacklist button instead.") }
</p>
</div>
);
return (
<QuestionDialog
title={_t("Verify session")}
description={body}
button={_t("I verify that the keys match")}
onFinished={this._onLegacyFinished}
/>
);
}
render() {
if (this.state.mode === MODE_LEGACY) {
return this._renderLegacyVerification();
} else {
return <div>
{this._renderSasVerification()}
</div>;
}
}
}
async function ensureDMExistsAndOpen(userId) {
const roomId = await ensureDMExists(MatrixClientPeg.get(), userId);
// don't use andView and spinner in createRoom, together, they cause this dialog to close and reopen,
// we causes us to loose the verifier and restart, and we end up having two verification requests
dis.dispatch({
action: 'view_room',
room_id: roomId,
should_peek: false,
});
return roomId;
}

View file

@ -19,7 +19,7 @@ import PropTypes from 'prop-types';
import * as sdk from '../../../index';
import SyntaxHighlight from '../elements/SyntaxHighlight';
import { _t } from '../../../languageHandler';
import { Room } from "matrix-js-sdk";
import { Room, MatrixEvent } from "matrix-js-sdk";
import Field from "../elements/Field";
import MatrixClientContext from "../../../contexts/MatrixClientContext";
import {useEventEmitter} from "../../../hooks/useEventEmitter";
@ -267,7 +267,8 @@ class FilteredList extends React.PureComponent {
};
}
componentWillReceiveProps(nextProps) {
// TODO: [REACT-WARNING] Replace with appropriate lifecycle event
UNSAFE_componentWillReceiveProps(nextProps) { // eslint-disable-line camelcase
if (this.props.children === nextProps.children && this.props.query === nextProps.query) return;
this.setState({
filteredChildren: FilteredList.filterChildren(nextProps.children, nextProps.query),
@ -326,6 +327,8 @@ class RoomStateExplorer extends React.PureComponent {
static contextType = MatrixClientContext;
roomStateEvents: Map<string, Map<string, MatrixEvent>>;
constructor(props) {
super(props);
@ -411,30 +414,26 @@ class RoomStateExplorer extends React.PureComponent {
if (this.state.eventType === null) {
list = <FilteredList query={this.state.queryEventType} onChange={this.onQueryEventType}>
{
Object.keys(this.roomStateEvents).map((evType) => {
const stateGroup = this.roomStateEvents[evType];
const stateKeys = Object.keys(stateGroup);
Array.from(this.roomStateEvents.entries()).map(([eventType, allStateKeys]) => {
let onClickFn;
if (stateKeys.length === 1 && stateKeys[0] === '') {
onClickFn = this.onViewSourceClick(stateGroup[stateKeys[0]]);
if (allStateKeys.size === 1 && allStateKeys.has("")) {
onClickFn = this.onViewSourceClick(allStateKeys.get(""));
} else {
onClickFn = this.browseEventType(evType);
onClickFn = this.browseEventType(eventType);
}
return <button className={classes} key={evType} onClick={onClickFn}>
{ evType }
return <button className={classes} key={eventType} onClick={onClickFn}>
{eventType}
</button>;
})
}
</FilteredList>;
} else {
const stateGroup = this.roomStateEvents[this.state.eventType];
const stateGroup = this.roomStateEvents.get(this.state.eventType);
list = <FilteredList query={this.state.queryStateKey} onChange={this.onQueryStateKey}>
{
Object.keys(stateGroup).map((stateKey) => {
const ev = stateGroup[stateKey];
Array.from(stateGroup.entries()).map(([stateKey, ev]) => {
return <button className={classes} key={stateKey} onClick={this.onViewSourceClick(ev)}>
{ stateKey }
</button>;
@ -695,6 +694,9 @@ class VerificationExplorer extends React.Component {
<VerificationRequest txnId={txnId} request={request} key={txnId} />,
)}
</div>
<div className="mx_Dialog_buttons">
<button onClick={this.props.onBack}>{_t("Back")}</button>
</div>
</div>);
}
}

View file

@ -196,7 +196,8 @@ export default class IncomingSasDialog extends React.Component {
sas={this._showSasEvent.sas}
onCancel={this._onCancelClick}
onDone={this._onSasMatchesClick}
isSelf={this.props.verifier.userId == MatrixClientPeg.get().getUserId()}
isSelf={this.props.verifier.userId === MatrixClientPeg.get().getUserId()}
inDialog={true}
/>;
}

View file

@ -18,7 +18,8 @@ import React from 'react';
import PropTypes from 'prop-types';
import {_t} from "../../../languageHandler";
import * as sdk from "../../../index";
import dis from '../../../dispatcher';
import dis from '../../../dispatcher/dispatcher';
import {Action} from "../../../dispatcher/actions";
export default class IntegrationsDisabledDialog extends React.Component {
static propTypes = {
@ -31,7 +32,7 @@ export default class IntegrationsDisabledDialog extends React.Component {
_onOpenSettingsClick = () => {
this.props.onFinished();
dis.dispatch({action: "view_user_settings"});
dis.fire(Action.ViewUserSettings);
};
render() {

View file

@ -1,5 +1,5 @@
/*
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.
@ -17,6 +17,7 @@ limitations under the License.
import React from 'react';
import PropTypes from 'prop-types';
import {_t} from "../../../languageHandler";
import SdkConfig from "../../../SdkConfig";
import * as sdk from "../../../index";
export default class IntegrationsImpossibleDialog extends React.Component {
@ -29,6 +30,7 @@ export default class IntegrationsImpossibleDialog extends React.Component {
};
render() {
const brand = SdkConfig.get().brand;
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
const DialogButtons = sdk.getComponent('views.elements.DialogButtons');
@ -39,8 +41,9 @@ export default class IntegrationsImpossibleDialog extends React.Component {
<div className='mx_IntegrationsImpossibleDialog_content'>
<p>
{_t(
"Your Riot doesn't allow you to use an Integration Manager to do this. " +
"Your %(brand)s doesn't allow you to use an Integration Manager to do this. " +
"Please contact an admin.",
{ brand },
)}
</p>
</div>

View file

@ -1,6 +1,7 @@
/*
Copyright 2016 OpenMarket Ltd
Copyright 2017 Vector Creations Ltd
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.
@ -23,6 +24,8 @@ import * as sdk from '../../../index';
import { _t } from '../../../languageHandler';
import AccessibleButton from '../elements/AccessibleButton';
import {ERROR_USER_CANCELLED} from "../../structures/InteractiveAuth";
import {SSOAuthEntry} from "../auth/InteractiveAuthEntryComponents";
export default createReactClass({
displayName: 'InteractiveAuthDialog',
@ -44,12 +47,60 @@ export default createReactClass({
onFinished: PropTypes.func.isRequired,
// Optional title and body to show when not showing a particular stage
title: PropTypes.string,
body: PropTypes.string,
// Optional title and body pairs for particular stages and phases within
// those stages. Object structure/example is:
// {
// "org.example.stage_type": {
// 1: {
// "body": "This is a body for phase 1" of org.example.stage_type,
// "title": "Title for phase 1 of org.example.stage_type"
// },
// 2: {
// "body": "This is a body for phase 2 of org.example.stage_type",
// "title": "Title for phase 2 of org.example.stage_type"
// "continueText": "Confirm identity with Example Auth",
// "continueKind": "danger"
// }
// }
// }
//
// Default is defined in _getDefaultDialogAesthetics()
aestheticsForStagePhases: PropTypes.object,
},
getInitialState: function() {
return {
authError: null,
// See _onUpdateStagePhase()
uiaStage: null,
uiaStagePhase: null,
};
},
_getDefaultDialogAesthetics: function() {
const ssoAesthetics = {
[SSOAuthEntry.PHASE_PREAUTH]: {
title: _t("Use Single Sign On to continue"),
body: _t("To continue, use Single Sign On to prove your identity."),
continueText: _t("Single Sign On"),
continueKind: "primary",
},
[SSOAuthEntry.PHASE_POSTAUTH]: {
title: _t("Confirm to continue"),
body: _t("Click the button below to confirm your identity."),
continueText: _t("Confirm"),
continueKind: "primary",
},
};
return {
[SSOAuthEntry.LOGIN_TYPE]: ssoAesthetics,
[SSOAuthEntry.UNSTABLE_LOGIN_TYPE]: ssoAesthetics,
};
},
@ -57,12 +108,21 @@ export default createReactClass({
if (success) {
this.props.onFinished(true, result);
} else {
this.setState({
authError: result,
});
if (result === ERROR_USER_CANCELLED) {
this.props.onFinished(false, null);
} else {
this.setState({
authError: result,
});
}
}
},
_onUpdateStagePhase: function(newStage, newPhase) {
// We copy the stage and stage phase params into state for title selection in render()
this.setState({uiaStage: newStage, uiaStagePhase: newPhase});
},
_onDismissClick: function() {
this.props.onFinished(false);
},
@ -71,6 +131,24 @@ export default createReactClass({
const InteractiveAuth = sdk.getComponent("structures.InteractiveAuth");
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
// Let's pick a title, body, and other params text that we'll show to the user. The order
// is most specific first, so stagePhase > our props > defaults.
let title = this.state.authError ? 'Error' : (this.props.title || _t('Authentication'));
let body = this.state.authError ? null : this.props.body;
let continueText = null;
let continueKind = null;
const dialogAesthetics = this.props.aestheticsForStagePhases || this._getDefaultDialogAesthetics();
if (!this.state.authError && dialogAesthetics) {
if (dialogAesthetics[this.state.uiaStage]) {
const aesthetics = dialogAesthetics[this.state.uiaStage][this.state.uiaStagePhase];
if (aesthetics && aesthetics.title) title = aesthetics.title;
if (aesthetics && aesthetics.body) body = aesthetics.body;
if (aesthetics && aesthetics.continueText) continueText = aesthetics.continueText;
if (aesthetics && aesthetics.continueKind) continueKind = aesthetics.continueKind;
}
}
let content;
if (this.state.authError) {
content = (
@ -88,11 +166,16 @@ export default createReactClass({
} else {
content = (
<div id='mx_Dialog_content'>
<InteractiveAuth ref={this._collectInteractiveAuth}
{body}
<InteractiveAuth
ref={this._collectInteractiveAuth}
matrixClient={this.props.matrixClient}
authData={this.props.authData}
makeRequest={this.props.makeRequest}
onAuthFinished={this._onAuthFinished}
onStagePhaseChange={this._onUpdateStagePhase}
continueText={continueText}
continueKind={continueKind}
/>
</div>
);
@ -101,7 +184,7 @@ export default createReactClass({
return (
<BaseDialog className="mx_InteractiveAuthDialog"
onFinished={this.props.onFinished}
title={this.state.authError ? 'Error' : (this.props.title || _t('Authentication'))}
title={title}
contentId='mx_Dialog_content'
>
{ content }

View file

@ -27,15 +27,16 @@ import {getHttpUriForMxc} from "matrix-js-sdk/src/content-repo";
import * as Email from "../../../email";
import {getDefaultIdentityServerUrl, useDefaultIdentityServer} from "../../../utils/IdentityServerUtils";
import {abbreviateUrl} from "../../../utils/UrlUtils";
import dis from "../../../dispatcher";
import dis from "../../../dispatcher/dispatcher";
import IdentityAuthClient from "../../../IdentityAuthClient";
import Modal from "../../../Modal";
import {humanizeTime} from "../../../utils/humanize";
import createRoom, {canEncryptToAllUsers} from "../../../createRoom";
import createRoom, {canEncryptToAllUsers, privateShouldBeEncrypted} from "../../../createRoom";
import {inviteMultipleToRoom} from "../../../RoomInvite";
import SettingsStore from '../../../settings/SettingsStore';
import RoomListStore, {TAG_DM} from "../../../stores/RoomListStore";
import {Key} from "../../../Keyboard";
import {Action} from "../../../dispatcher/actions";
import {DefaultTagID} from "../../../stores/room-list/models";
import RoomListStore from "../../../stores/room-list/RoomListStore";
export const KIND_DM = "dm";
export const KIND_INVITE = "invite";
@ -343,10 +344,9 @@ export default class InviteDialog extends React.PureComponent {
_buildRecents(excludedTargetIds: Set<string>): {userId: string, user: RoomMember, lastActive: number} {
const rooms = DMRoomMap.shared().getUniqueRoomsWithIndividuals(); // map of userId => js-sdk Room
// Also pull in all the rooms tagged as TAG_DM so we don't miss anything. Sometimes the
// Also pull in all the rooms tagged as DefaultTagID.DM so we don't miss anything. Sometimes the
// room list doesn't tag the room for the DMRoomMap, but does for the room list.
const taggedRooms = RoomListStore.getRoomLists();
const dmTaggedRooms = taggedRooms[TAG_DM];
const dmTaggedRooms = RoomListStore.instance.orderedLists[DefaultTagID.DM];
const myUserId = MatrixClientPeg.get().getUserId();
for (const dmRoom of dmTaggedRooms) {
const otherMembers = dmRoom.getJoinedMembers().filter(u => u.userId !== myUserId);
@ -574,13 +574,16 @@ export default class InviteDialog extends React.PureComponent {
const createRoomOptions = {inlineErrors: true};
if (SettingsStore.isFeatureEnabled("feature_cross_signing")) {
if (privateShouldBeEncrypted()) {
// Check whether all users have uploaded device keys before.
// If so, enable encryption in the new room.
const client = MatrixClientPeg.get();
const allHaveDeviceKeys = await canEncryptToAllUsers(client, targetIds);
if (allHaveDeviceKeys) {
createRoomOptions.encryption = true;
const has3PidMembers = targets.some(t => t instanceof ThreepidMember);
if (!has3PidMembers) {
const client = MatrixClientPeg.get();
const allHaveDeviceKeys = await canEncryptToAllUsers(client, targetIds);
if (allHaveDeviceKeys) {
createRoomOptions.encryption = true;
}
}
}
@ -899,7 +902,7 @@ export default class InviteDialog extends React.PureComponent {
_onManageSettingsClick = (e) => {
e.preventDefault();
dis.dispatch({ action: 'view_user_settings' });
dis.fire(Action.ViewUserSettings);
this.props.onFinished();
};
@ -1067,9 +1070,8 @@ export default class InviteDialog extends React.PureComponent {
let buttonText;
let goButtonFn;
const userId = MatrixClientPeg.get().getUserId();
if (this.props.kind === KIND_DM) {
const userId = MatrixClientPeg.get().getUserId();
title = _t("Direct Messages");
helpText = _t(
"Start a conversation with someone using their name, username (like <userId/>) or email address.",
@ -1083,9 +1085,11 @@ export default class InviteDialog extends React.PureComponent {
} else { // KIND_INVITE
title = _t("Invite to this room");
helpText = _t(
"If you can't find someone, ask them for their username (e.g. @user:server.com) or " +
"<a>share this room</a>.", {},
"Invite someone using their name, username (like <userId/>), email address or <a>share this room</a>.",
{},
{
userId: () =>
<a href={makeUserPermalink(userId)} rel="noreferrer noopener" target="_blank">{userId}</a>,
a: (sub) =>
<a href={makeRoomPermalink(this.props.roomId)} rel="noreferrer noopener" target="_blank">{sub}</a>,
},

View file

@ -1,178 +0,0 @@
/*
Copyright 2017 Vector Creations Ltd
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 Modal from '../../../Modal';
import React from 'react';
import createReactClass from 'create-react-class';
import PropTypes from 'prop-types';
import * as sdk from '../../../index';
import { _t, _td } from '../../../languageHandler';
// TODO: We can remove this once cross-signing is the only way.
// https://github.com/vector-im/riot-web/issues/11908
/**
* Dialog which asks the user whether they want to share their keys with
* an unverified device.
*
* onFinished is called with `true` if the key should be shared, `false` if it
* should not, and `undefined` if the dialog is cancelled. (In other words:
* truthy: do the key share. falsy: don't share the keys).
*/
export default createReactClass({
propTypes: {
matrixClient: PropTypes.object.isRequired,
userId: PropTypes.string.isRequired,
deviceId: PropTypes.string.isRequired,
onFinished: PropTypes.func.isRequired,
},
getInitialState: function() {
return {
deviceInfo: null,
wasNewDevice: false,
};
},
componentDidMount: function() {
this._unmounted = false;
const userId = this.props.userId;
const deviceId = this.props.deviceId;
// give the client a chance to refresh the device list
this.props.matrixClient.downloadKeys([userId], false).then((r) => {
if (this._unmounted) { return; }
const deviceInfo = r[userId][deviceId];
if (!deviceInfo) {
console.warn(`No details found for session ${userId}:${deviceId}`);
this.props.onFinished(false);
return;
}
const wasNewDevice = !deviceInfo.isKnown();
this.setState({
deviceInfo: deviceInfo,
wasNewDevice: wasNewDevice,
});
// if the device was new before, it's not any more.
if (wasNewDevice) {
this.props.matrixClient.setDeviceKnown(
userId,
deviceId,
true,
);
}
});
},
componentWillUnmount: function() {
this._unmounted = true;
},
_onVerifyClicked: function() {
const DeviceVerifyDialog = sdk.getComponent('views.dialogs.DeviceVerifyDialog');
console.log("KeyShareDialog: Starting verify dialog");
Modal.createTrackedDialog('Key Share', 'Starting dialog', DeviceVerifyDialog, {
userId: this.props.userId,
device: this.state.deviceInfo,
onFinished: (verified) => {
if (verified) {
// can automatically share the keys now.
this.props.onFinished(true);
}
},
}, null, /* priority = */ false, /* static = */ true);
},
_onShareClicked: function() {
console.log("KeyShareDialog: User clicked 'share'");
this.props.onFinished(true);
},
_onIgnoreClicked: function() {
console.log("KeyShareDialog: User clicked 'ignore'");
this.props.onFinished(false);
},
_renderContent: function() {
const displayName = this.state.deviceInfo.getDisplayName() ||
this.state.deviceInfo.deviceId;
let text;
if (this.state.wasNewDevice) {
text = _td("You added a new session '%(displayName)s', which is"
+ " requesting encryption keys.");
} else {
text = _td("Your unverified session '%(displayName)s' is requesting"
+ " encryption keys.");
}
text = _t(text, {displayName: displayName});
return (
<div id='mx_Dialog_content'>
<p>{ text }</p>
<div className="mx_Dialog_buttons">
<button onClick={this._onVerifyClicked} autoFocus="true">
{ _t('Start verification') }
</button>
<button onClick={this._onShareClicked}>
{ _t('Share without verifying') }
</button>
<button onClick={this._onIgnoreClicked}>
{ _t('Ignore request') }
</button>
</div>
</div>
);
},
render: function() {
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
const Spinner = sdk.getComponent('views.elements.Spinner');
let content;
if (this.state.deviceInfo) {
content = this._renderContent();
} else {
content = (
<div id='mx_Dialog_content'>
<p>{ _t('Loading session info...') }</p>
<Spinner />
</div>
);
}
return (
<BaseDialog className='mx_KeyShareRequestDialog'
onFinished={this.props.onFinished}
title={_t('Encryption key request')}
contentId='mx_Dialog_content'
>
{ content }
</BaseDialog>
);
},
});

View file

@ -17,6 +17,7 @@ limitations under the License.
import React, {useState, useCallback, useRef} from 'react';
import * as sdk from '../../../index';
import { _t } from '../../../languageHandler';
import SdkConfig from '../../../SdkConfig';
export default function KeySignatureUploadFailedDialog({
failures,
@ -65,9 +66,10 @@ export default function KeySignatureUploadFailedDialog({
let body;
if (!success && !cancelled && continuation && retry > 0) {
const reason = causes.get(source) || defaultCause;
const brand = SdkConfig.get().brand;
body = (<div>
<p>{_t("Riot encountered an error during upload of:")}</p>
<p>{_t("%(brand)s encountered an error during upload of:", { brand })}</p>
<p>{reason}</p>
{retrying && <Spinner />}
<pre>{JSON.stringify(failures, null, 2)}</pre>
@ -85,7 +87,7 @@ export default function KeySignatureUploadFailedDialog({
<span>{_t("Upload completed")}</span> :
cancelled ?
<span>{_t("Cancelled signature upload")}</span> :
<span>{_t("Unabled to upload")}</span>}
<span>{_t("Unable to upload")}</span>}
<DialogButtons
primaryButton={_t("OK")}
hasCancel={false}

View file

@ -1,5 +1,6 @@
/*
Copyright 2018 New Vector Ltd
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.
@ -17,17 +18,28 @@ limitations under the License.
import React from 'react';
import QuestionDialog from './QuestionDialog';
import { _t } from '../../../languageHandler';
import SdkConfig from '../../../SdkConfig';
export default (props) => {
const description1 =
_t("You've previously used Riot on %(host)s with lazy loading of members enabled. " +
"In this version lazy loading is disabled. " +
"As the local cache is not compatible between these two settings, " +
"Riot needs to resync your account.",
{host: props.host});
const description2 = _t("If the other version of Riot is still open in another tab, " +
"please close it as using Riot on the same host with both " +
"lazy loading enabled and disabled simultaneously will cause issues.");
const brand = SdkConfig.get().brand;
const description1 = _t(
"You've previously used %(brand)s on %(host)s with lazy loading of members enabled. " +
"In this version lazy loading is disabled. " +
"As the local cache is not compatible between these two settings, " +
"%(brand)s needs to resync your account.",
{
brand,
host: props.host,
},
);
const description2 = _t(
"If the other version of %(brand)s is still open in another tab, " +
"please close it as using %(brand)s on the same host with both " +
"lazy loading enabled and disabled simultaneously will cause issues.",
{
brand,
},
);
return (<QuestionDialog
hasCancelButton={false}

View file

@ -1,5 +1,6 @@
/*
Copyright 2018 New Vector Ltd
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.
@ -17,15 +18,21 @@ limitations under the License.
import React from 'react';
import QuestionDialog from './QuestionDialog';
import { _t } from '../../../languageHandler';
import SdkConfig from '../../../SdkConfig';
export default (props) => {
const brand = SdkConfig.get().brand;
const description =
_t("Riot now uses 3-5x less memory, by only loading information about other users"
+ " when needed. Please wait whilst we resynchronise with the server!");
_t(
"%(brand)s now uses 3-5x less memory, by only loading information " +
"about other users when needed. Please wait whilst we resynchronise " +
"with the server!",
{ brand },
);
return (<QuestionDialog
hasCancelButton={false}
title={_t("Updating Riot")}
title={_t("Updating %(brand)s", { brand })}
description={<div>{description}</div>}
button={_t("OK")}
onFinished={props.onFinished}

View file

@ -18,7 +18,7 @@ limitations under the License.
import React from 'react';
import Modal from '../../../Modal';
import * as sdk from '../../../index';
import dis from '../../../dispatcher';
import dis from '../../../dispatcher/dispatcher';
import { _t } from '../../../languageHandler';
import {MatrixClientPeg} from '../../../MatrixClientPeg';

View file

@ -0,0 +1,116 @@
/*
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 * as React from 'react';
import * as PropTypes from 'prop-types';
import BaseDialog from './BaseDialog';
import { _t } from '../../../languageHandler';
import DialogButtons from '../elements/DialogButtons';
export enum RebrandDialogKind {
NAG,
ONE_TIME,
}
interface IProps {
onFinished: (bool) => void;
kind: RebrandDialogKind;
targetUrl?: string;
}
export default class RebrandDialog extends React.PureComponent<IProps> {
private onDoneClick = () => {
this.props.onFinished(true);
};
private onGoToElementClick = () => {
this.props.onFinished(true);
};
private onRemindMeLaterClick = () => {
this.props.onFinished(false);
};
private getPrettyTargetUrl() {
const u = new URL(this.props.targetUrl);
let ret = u.host;
if (u.pathname !== '/') ret += u.pathname;
return ret;
}
getBodyText() {
if (this.props.kind === RebrandDialogKind.NAG) {
return _t(
"Use your account to sign in to the latest version of the app at <a />", {},
{
a: sub => <a href={this.props.targetUrl} rel="noopener noreferrer" target="_blank">{this.getPrettyTargetUrl()}</a>,
},
);
} else {
return _t(
"Youre already signed in and good to go here, but you can also grab the latest " +
"versions of the app on all platforms at <a>element.io/get-started</a>.", {},
{
a: sub => <a href="https://element.io/get-started" rel="noopener noreferrer" target="_blank">{sub}</a>,
},
);
}
}
getDialogButtons() {
if (this.props.kind === RebrandDialogKind.NAG) {
return <DialogButtons primaryButton={_t("Go to Element")}
primaryButtonClass='primary'
onPrimaryButtonClick={this.onGoToElementClick}
hasCancel={true}
cancelButton={"Remind me later"}
onCancel={this.onRemindMeLaterClick}
focus={true}
/>;
} else {
return <DialogButtons primaryButton={_t("Done")}
primaryButtonClass='primary'
hasCancel={false}
onPrimaryButtonClick={this.onDoneClick}
focus={true}
/>;
}
}
render() {
return <BaseDialog title={_t("Were excited to announce Riot is now Element!")}
className='mx_RebrandDialog'
contentId='mx_Dialog_content'
onFinished={this.props.onFinished}
hasCancel={false}
>
<div className="mx_RebrandDialog_body">{this.getBodyText()}</div>
<div className="mx_RebrandDialog_logoContainer">
<img className="mx_RebrandDialog_logo" src={require("../../../../res/img/riot-logo.svg")} alt="Riot Logo" />
<span className="mx_RebrandDialog_chevron" />
<img className="mx_RebrandDialog_logo" src={require("../../../../res/img/element-logo.svg")} alt="Element Logo" />
</div>
<div>
{_t(
"Learn more at <a>element.io/previously-riot</a>", {}, {
a: sub => <a href="https://element.io/previously-riot" rel="noopener noreferrer" target="_blank">{sub}</a>,
}
)}
</div>
{this.getDialogButtons()}
</BaseDialog>;
}
}

View file

@ -27,21 +27,28 @@ import NotificationSettingsTab from "../settings/tabs/room/NotificationSettingsT
import BridgeSettingsTab from "../settings/tabs/room/BridgeSettingsTab";
import * as sdk from "../../../index";
import {MatrixClientPeg} from "../../../MatrixClientPeg";
import dis from "../../../dispatcher";
import dis from "../../../dispatcher/dispatcher";
import SettingsStore from "../../../settings/SettingsStore";
export const ROOM_GENERAL_TAB = "ROOM_GENERAL_TAB";
export const ROOM_SECURITY_TAB = "ROOM_SECURITY_TAB";
export const ROOM_ROLES_TAB = "ROOM_ROLES_TAB";
export const ROOM_NOTIFICATIONS_TAB = "ROOM_NOTIFICATIONS_TAB";
export const ROOM_BRIDGES_TAB = "ROOM_BRIDGES_TAB";
export const ROOM_ADVANCED_TAB = "ROOM_ADVANCED_TAB";
export default class RoomSettingsDialog extends React.Component {
static propTypes = {
roomId: PropTypes.string.isRequired,
onFinished: PropTypes.func.isRequired,
};
componentWillMount() {
componentDidMount() {
this._dispatcherRef = dis.register(this._onAction);
}
componentWillUnmount() {
dis.unregister(this._dispatcherRef);
if (this._dispatcherRef) dis.unregister(this._dispatcherRef);
}
_onAction = (payload) => {
@ -56,28 +63,33 @@ export default class RoomSettingsDialog extends React.Component {
const tabs = [];
tabs.push(new Tab(
ROOM_GENERAL_TAB,
_td("General"),
"mx_RoomSettingsDialog_settingsIcon",
<GeneralRoomSettingsTab roomId={this.props.roomId} />,
));
tabs.push(new Tab(
ROOM_SECURITY_TAB,
_td("Security & Privacy"),
"mx_RoomSettingsDialog_securityIcon",
<SecurityRoomSettingsTab roomId={this.props.roomId} />,
));
tabs.push(new Tab(
ROOM_ROLES_TAB,
_td("Roles & Permissions"),
"mx_RoomSettingsDialog_rolesIcon",
<RolesRoomSettingsTab roomId={this.props.roomId} />,
));
tabs.push(new Tab(
ROOM_NOTIFICATIONS_TAB,
_td("Notifications"),
"mx_RoomSettingsDialog_rolesIcon",
"mx_RoomSettingsDialog_notificationsIcon",
<NotificationSettingsTab roomId={this.props.roomId} />,
));
if (SettingsStore.isFeatureEnabled("feature_bridge_state")) {
tabs.push(new Tab(
ROOM_BRIDGES_TAB,
_td("Bridges"),
"mx_RoomSettingsDialog_bridgesIcon",
<BridgeSettingsTab roomId={this.props.roomId} />,
@ -85,6 +97,7 @@ export default class RoomSettingsDialog extends React.Component {
}
tabs.push(new Tab(
ROOM_ADVANCED_TAB,
_td("Advanced"),
"mx_RoomSettingsDialog_warningIcon",
<AdvancedRoomSettingsTab roomId={this.props.roomId} closeSettingsFn={this.props.onFinished} />,

View file

@ -30,7 +30,7 @@ export default createReactClass({
onFinished: PropTypes.func.isRequired,
},
componentWillMount: async function() {
componentDidMount: async function() {
const recommended = await this.props.room.getRecommendedVersion();
this._targetVersion = recommended.version;
this.setState({busy: false});

View file

@ -1,5 +1,5 @@
/*
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.
@ -17,6 +17,7 @@ limitations under the License.
import React from 'react';
import PropTypes from 'prop-types';
import {_t} from "../../../languageHandler";
import SdkConfig from "../../../SdkConfig";
import * as sdk from "../../../index";
import LabelledToggleSwitch from "../elements/LabelledToggleSwitch";
import {MatrixClientPeg} from "../../../MatrixClientPeg";
@ -63,6 +64,7 @@ export default class RoomUpgradeWarningDialog extends React.Component {
};
render() {
const brand = SdkConfig.get().brand;
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
const DialogButtons = sdk.getComponent('views.elements.DialogButtons');
@ -96,8 +98,11 @@ export default class RoomUpgradeWarningDialog extends React.Component {
<p>
{_t(
"This usually only affects how the room is processed on the server. If you're " +
"having problems with your Riot, please <a>report a bug</a>.",
{}, {
"having problems with your %(brand)s, please <a>report a bug</a>.",
{
brand,
},
{
"a": (sub) => {
return <a href='#' onClick={this._openBugReportDialog}>{sub}</a>;
},

View file

@ -1,6 +1,7 @@
/*
Copyright 2017 Vector Creations Ltd
Copyright 2018 New Vector Ltd
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.
@ -56,6 +57,7 @@ export default createReactClass({
},
render: function() {
const brand = SdkConfig.get().brand;
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
const DialogButtons = sdk.getComponent('views.elements.DialogButtons');
@ -94,9 +96,10 @@ export default createReactClass({
<p>{ _t("We encountered an error trying to restore your previous session.") }</p>
<p>{ _t(
"If you have previously used a more recent version of Riot, your session " +
"If you have previously used a more recent version of %(brand)s, your session " +
"may be incompatible with this version. Close this window and return " +
"to the more recent version.",
{ brand },
) }</p>
<p>{ _t(

View file

@ -62,6 +62,7 @@ export default createReactClass({
};
},
// TODO: [REACT-WARNING] Replace component with real class, use constructor for refs
UNSAFE_componentWillMount: function() {
this._input_value = createRef();
this._uiAuth = createRef();

View file

@ -75,8 +75,8 @@ export default createReactClass({
};
},
componentWillMount: function() {
console.info('SetPasswordDialog component will mount');
componentDidMount: function() {
console.info('SetPasswordDialog component did mount');
},
_onPasswordChanged: function(res) {

View file

@ -14,16 +14,52 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import React from 'react';
import PropTypes from 'prop-types';
import SetupEncryptionBody from '../../structures/auth/SetupEncryptionBody';
import BaseDialog from './BaseDialog';
import { _t } from '../../../languageHandler';
import { SetupEncryptionStore, PHASE_DONE } from '../../../stores/SetupEncryptionStore';
export default function SetupEncryptionDialog({onFinished}) {
return <BaseDialog
headerImage={require("../../../../res/img/e2e/warning.svg")}
onFinished={onFinished}
title={_t("Verify this session")}
>
<SetupEncryptionBody onFinished={onFinished} />
</BaseDialog>;
function iconFromPhase(phase) {
if (phase === PHASE_DONE) {
return require("../../../../res/img/e2e/verified.svg");
} else {
return require("../../../../res/img/e2e/warning.svg");
}
}
export default class SetupEncryptionDialog extends React.Component {
static propTypes = {
onFinished: PropTypes.func.isRequired,
};
constructor() {
super();
this.store = SetupEncryptionStore.sharedInstance();
this.state = {icon: iconFromPhase(this.store.phase)};
}
componentDidMount() {
this.store.on("update", this._onStoreUpdate);
}
componentWillUnmount() {
this.store.removeListener("update", this._onStoreUpdate);
}
_onStoreUpdate = () => {
this.setState({icon: iconFromPhase(this.store.phase)});
};
render() {
return <BaseDialog
headerImage={this.state.icon}
onFinished={this.props.onFinished}
title={_t("Verify this session")}
>
<SetupEncryptionBody onFinished={this.props.onFinished} />
</BaseDialog>;
}
}

View file

@ -1,5 +1,6 @@
/*
Copyright 2018 New Vector Ltd
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.
@ -14,15 +15,21 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import React, {createRef} from 'react';
import PropTypes from 'prop-types';
import {Room, User, Group, RoomMember, MatrixEvent} from 'matrix-js-sdk';
import * as React from 'react';
import * as PropTypes from 'prop-types';
import {Room} from "matrix-js-sdk/src/models/room";
import {User} from "matrix-js-sdk/src/models/user";
import {Group} from "matrix-js-sdk/src/models/group";
import {RoomMember} from "matrix-js-sdk/src/models/room-member";
import {MatrixEvent} from "matrix-js-sdk/src/models/event";
import * as sdk from '../../../index';
import { _t } from '../../../languageHandler';
import QRCode from 'qrcode-react';
import QRCode from "../elements/QRCode";
import {RoomPermalinkCreator, makeGroupPermalink, makeUserPermalink} from "../../../utils/permalinks/Permalinks";
import * as ContextMenu from "../../structures/ContextMenu";
import {toRightOf} from "../../structures/ContextMenu";
import {copyPlaintext, selectText} from "../../../utils/strings";
import StyledCheckbox from '../elements/StyledCheckbox';
const socials = [
{
@ -52,7 +59,18 @@ const socials = [
},
];
export default class ShareDialog extends React.Component {
interface IProps {
onFinished: () => void;
target: Room | User | Group | RoomMember | MatrixEvent;
permalinkCreator: RoomPermalinkCreator;
}
interface IState {
linkSpecificEvent: boolean;
permalinkCreator: RoomPermalinkCreator;
}
export default class ShareDialog extends React.PureComponent<IProps, IState> {
static propTypes = {
onFinished: PropTypes.func.isRequired,
target: PropTypes.oneOfType([
@ -64,55 +82,45 @@ export default class ShareDialog extends React.Component {
]).isRequired,
};
protected closeCopiedTooltip: () => void;
constructor(props) {
super(props);
this.onCopyClick = this.onCopyClick.bind(this);
this.onLinkSpecificEventCheckboxClick = this.onLinkSpecificEventCheckboxClick.bind(this);
let permalinkCreator: RoomPermalinkCreator = null;
if (props.target instanceof Room) {
permalinkCreator = new RoomPermalinkCreator(props.target);
permalinkCreator.load();
}
this.state = {
// MatrixEvent defaults to share linkSpecificEvent
linkSpecificEvent: this.props.target instanceof MatrixEvent,
permalinkCreator,
};
this._link = createRef();
}
static _selectText(target) {
const range = document.createRange();
range.selectNodeContents(target);
const selection = window.getSelection();
selection.removeAllRanges();
selection.addRange(range);
}
static onLinkClick(e) {
e.preventDefault();
const {target} = e;
ShareDialog._selectText(target);
selectText(e.target);
}
onCopyClick(e) {
async onCopyClick(e) {
e.preventDefault();
const target = e.target; // copy target before we go async and React throws it away
ShareDialog._selectText(this._link.current);
let successful;
try {
successful = document.execCommand('copy');
} catch (err) {
console.error('Failed to copy: ', err);
}
const buttonRect = e.target.getBoundingClientRect();
const successful = await copyPlaintext(this.getUrl());
const buttonRect = target.getBoundingClientRect();
const GenericTextContextMenu = sdk.getComponent('context_menus.GenericTextContextMenu');
const {close} = ContextMenu.createMenu(GenericTextContextMenu, {
...toRightOf(buttonRect, 2),
message: successful ? _t('Copied!') : _t('Failed to copy'),
});
// Drop a reference to this close handler for componentWillUnmount
this.closeCopiedTooltip = e.target.onmouseleave = close;
this.closeCopiedTooltip = target.onmouseleave = close;
}
onLinkSpecificEventCheckboxClick() {
@ -121,24 +129,38 @@ export default class ShareDialog extends React.Component {
});
}
componentWillMount() {
if (this.props.target instanceof Room) {
const permalinkCreator = new RoomPermalinkCreator(this.props.target);
permalinkCreator.load();
this.setState({permalinkCreator});
}
}
componentWillUnmount() {
// if the Copied tooltip is open then get rid of it, there are ways to close the modal which wouldn't close
// the tooltip otherwise, such as pressing Escape or clicking X really quickly
if (this.closeCopiedTooltip) this.closeCopiedTooltip();
}
render() {
let title;
getUrl() {
let matrixToUrl;
if (this.props.target instanceof Room) {
if (this.state.linkSpecificEvent) {
const events = this.props.target.getLiveTimeline().getEvents();
matrixToUrl = this.state.permalinkCreator.forEvent(events[events.length - 1].getId());
} else {
matrixToUrl = this.state.permalinkCreator.forRoom();
}
} else if (this.props.target instanceof User || this.props.target instanceof RoomMember) {
matrixToUrl = makeUserPermalink(this.props.target.userId);
} else if (this.props.target instanceof Group) {
matrixToUrl = makeGroupPermalink(this.props.target.groupId);
} else if (this.props.target instanceof MatrixEvent) {
if (this.state.linkSpecificEvent) {
matrixToUrl = this.props.permalinkCreator.forEvent(this.props.target.getId());
} else {
matrixToUrl = this.props.permalinkCreator.forRoom();
}
}
return matrixToUrl;
}
render() {
let title;
let checkbox;
if (this.props.target instanceof Room) {
@ -147,46 +169,31 @@ export default class ShareDialog extends React.Component {
const events = this.props.target.getLiveTimeline().getEvents();
if (events.length > 0) {
checkbox = <div>
<input type="checkbox"
id="mx_ShareDialog_checkbox"
checked={this.state.linkSpecificEvent}
onChange={this.onLinkSpecificEventCheckboxClick} />
<label htmlFor="mx_ShareDialog_checkbox">
<StyledCheckbox
checked={this.state.linkSpecificEvent}
onChange={this.onLinkSpecificEventCheckboxClick}
>
{ _t('Link to most recent message') }
</label>
</StyledCheckbox>
</div>;
}
if (this.state.linkSpecificEvent) {
matrixToUrl = this.state.permalinkCreator.forEvent(events[events.length - 1].getId());
} else {
matrixToUrl = this.state.permalinkCreator.forRoom();
}
} else if (this.props.target instanceof User || this.props.target instanceof RoomMember) {
title = _t('Share User');
matrixToUrl = makeUserPermalink(this.props.target.userId);
} else if (this.props.target instanceof Group) {
title = _t('Share Community');
matrixToUrl = makeGroupPermalink(this.props.target.groupId);
} else if (this.props.target instanceof MatrixEvent) {
title = _t('Share Room Message');
checkbox = <div>
<input type="checkbox"
id="mx_ShareDialog_checkbox"
<StyledCheckbox
checked={this.state.linkSpecificEvent}
onClick={this.onLinkSpecificEventCheckboxClick} />
<label htmlFor="mx_ShareDialog_checkbox">
onClick={this.onLinkSpecificEventCheckboxClick}
>
{ _t('Link to selected message') }
</label>
</StyledCheckbox>
</div>;
if (this.state.linkSpecificEvent) {
matrixToUrl = this.props.permalinkCreator.forEvent(this.props.target.getId());
} else {
matrixToUrl = this.props.permalinkCreator.forRoom();
}
}
const matrixToUrl = this.getUrl();
const encodedUrl = encodeURIComponent(matrixToUrl);
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
@ -197,8 +204,7 @@ export default class ShareDialog extends React.Component {
>
<div className="mx_ShareDialog_content">
<div className="mx_ShareDialog_matrixto">
<a ref={this._link}
href={matrixToUrl}
<a href={matrixToUrl}
onClick={ShareDialog.onLinkClick}
className="mx_ShareDialog_matrixto_link"
>
@ -214,20 +220,21 @@ export default class ShareDialog extends React.Component {
<div className="mx_ShareDialog_split">
<div className="mx_ShareDialog_qrcode_container">
<QRCode value={matrixToUrl} size={256} logoWidth={48} logo={require("../../../../res/img/matrix-m.svg")} />
<QRCode data={matrixToUrl} width={256} />
</div>
<div className="mx_ShareDialog_social_container">
{
socials.map((social) => <a rel="noreferrer noopener"
target="_blank"
key={social.name}
name={social.name}
href={social.url(encodedUrl)}
className="mx_ShareDialog_social_icon"
{ socials.map((social) => (
<a
rel="noreferrer noopener"
target="_blank"
key={social.name}
title={social.name}
href={social.url(encodedUrl)}
className="mx_ShareDialog_social_icon"
>
<img src={social.img} alt={social.name} height={64} width={64} />
</a>)
}
</a>
)) }
</div>
</div>
</div>

View file

@ -16,14 +16,14 @@ limitations under the License.
import React from 'react';
import {_t} from "../../../languageHandler";
import {CommandCategories, CommandMap} from "../../../SlashCommands";
import {CommandCategories, Commands} from "../../../SlashCommands";
import * as sdk from "../../../index";
export default ({onFinished}) => {
const InfoDialog = sdk.getComponent('dialogs.InfoDialog');
const categories = {};
Object.values(CommandMap).forEach(cmd => {
Commands.forEach(cmd => {
if (!categories[cmd.category]) {
categories[cmd.category] = [];
}
@ -41,7 +41,7 @@ export default ({onFinished}) => {
categories[category].forEach(cmd => {
rows.push(<tr key={cmd.command}>
<td><strong>{cmd.command}</strong></td>
<td><strong>{cmd.getCommand()}</strong></td>
<td>{cmd.args}</td>
<td>{cmd.description}</td>
</tr>);

View file

@ -55,6 +55,7 @@ export default createReactClass({
};
},
// TODO: [REACT-WARNING] Replace component with real class, use constructor for refs
UNSAFE_componentWillMount: function() {
this._field = createRef();
},

View file

@ -1,187 +0,0 @@
/*
Copyright 2017 Vector Creations Ltd
Copyright 2017 New Vector Ltd
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 createReactClass from 'create-react-class';
import PropTypes from 'prop-types';
import * as sdk from '../../../index';
import {MatrixClientPeg} from '../../../MatrixClientPeg';
import { _t } from '../../../languageHandler';
import SettingsStore from "../../../settings/SettingsStore";
import { markAllDevicesKnown } from '../../../cryptodevices';
function UserUnknownDeviceList(props) {
const MemberDeviceInfo = sdk.getComponent('rooms.MemberDeviceInfo');
const {userId, userDevices} = props;
const deviceListEntries = Object.keys(userDevices).map((deviceId) =>
<li key={deviceId}><MemberDeviceInfo device={userDevices[deviceId]} userId={userId} showDeviceId={true} /></li>,
);
return (
<ul className="mx_UnknownDeviceDialog_deviceList">
{ deviceListEntries }
</ul>
);
}
UserUnknownDeviceList.propTypes = {
userId: PropTypes.string.isRequired,
// map from deviceid -> deviceinfo
userDevices: PropTypes.object.isRequired,
};
function UnknownDeviceList(props) {
const {devices} = props;
const userListEntries = Object.keys(devices).map((userId) =>
<li key={userId}>
<p>{ userId }:</p>
<UserUnknownDeviceList userId={userId} userDevices={devices[userId]} />
</li>,
);
return <ul>{ userListEntries }</ul>;
}
UnknownDeviceList.propTypes = {
// map from userid -> deviceid -> deviceinfo
devices: PropTypes.object.isRequired,
};
export default createReactClass({
displayName: 'UnknownDeviceDialog',
propTypes: {
room: PropTypes.object.isRequired,
// map from userid -> deviceid -> deviceinfo or null if devices are not yet loaded
devices: PropTypes.object,
onFinished: PropTypes.func.isRequired,
// Label for the button that marks all devices known and tries the send again
sendAnywayLabel: PropTypes.string.isRequired,
// Label for the button that to send the event if you've verified all devices
sendLabel: PropTypes.string.isRequired,
// function to retry the request once all devices are verified / known
onSend: PropTypes.func.isRequired,
},
componentWillMount: function() {
MatrixClientPeg.get().on("deviceVerificationChanged", this._onDeviceVerificationChanged);
},
componentWillUnmount: function() {
if (MatrixClientPeg.get()) {
MatrixClientPeg.get().removeListener("deviceVerificationChanged", this._onDeviceVerificationChanged);
}
},
_onDeviceVerificationChanged: function(userId, deviceId, deviceInfo) {
if (this.props.devices[userId] && this.props.devices[userId][deviceId]) {
// XXX: Mutating props :/
this.props.devices[userId][deviceId] = deviceInfo;
this.forceUpdate();
}
},
_onDismissClicked: function() {
this.props.onFinished();
},
_onSendAnywayClicked: function() {
markAllDevicesKnown(MatrixClientPeg.get(), this.props.devices);
this.props.onFinished();
this.props.onSend();
},
_onSendClicked: function() {
this.props.onFinished();
this.props.onSend();
},
render: function() {
if (this.props.devices === null) {
const Spinner = sdk.getComponent("elements.Spinner");
return <Spinner />;
}
let warning;
if (SettingsStore.getValue("blacklistUnverifiedDevices", this.props.room.roomId)) {
warning = (
<h4>
{ _t("You are currently blacklisting unverified sessions; to send " +
"messages to these sessions you must verify them.") }
</h4>
);
} else {
warning = (
<div>
<p>
{ _t("We recommend you go through the verification process " +
"for each session to confirm they belong to their legitimate owner, " +
"but you can resend the message without verifying if you prefer.") }
</p>
</div>
);
}
let haveUnknownDevices = false;
Object.keys(this.props.devices).forEach((userId) => {
Object.keys(this.props.devices[userId]).map((deviceId) => {
const device = this.props.devices[userId][deviceId];
if (device.isUnverified() && !device.isKnown()) {
haveUnknownDevices = true;
}
});
});
const sendButtonOnClick = haveUnknownDevices ? this._onSendAnywayClicked : this._onSendClicked;
const sendButtonLabel = haveUnknownDevices ? this.props.sendAnywayLabel : this.props.sendAnywayLabel;
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
const DialogButtons = sdk.getComponent('views.elements.DialogButtons');
return (
<BaseDialog className='mx_UnknownDeviceDialog'
onFinished={this.props.onFinished}
title={_t('Room contains unknown sessions')}
contentId='mx_Dialog_content'
>
<div className="mx_Dialog_content" id='mx_Dialog_content'>
<h4>
{ _t('"%(RoomName)s" contains sessions that you haven\'t seen before.', {RoomName: this.props.room.name}) }
</h4>
{ warning }
{ _t("Unknown sessions") }:
<UnknownDeviceList devices={this.props.devices} />
</div>
<DialogButtons primaryButton={sendButtonLabel}
onPrimaryButtonClick={sendButtonOnClick}
onCancel={this._onDismissClicked} />
</BaseDialog>
);
// XXX: do we want to give the user the option to enable blacklistUnverifiedDevices for this room (or globally) at this point?
// It feels like confused users will likely turn it on and then disappear in a cloud of UISIs...
},
});

View file

@ -84,7 +84,7 @@ export default class UploadConfirmDialog extends React.Component {
preview = <div>
<div>
<img className="mx_UploadConfirmDialog_fileIcon"
src={require("../../../../res/img/files.png")}
src={require("../../../../res/img/feather-customised/files.svg")}
/>
{this.props.file.name} ({filesize(this.props.file.size)})
</div>

View file

@ -22,6 +22,7 @@ import {_t, _td} from "../../../languageHandler";
import GeneralUserSettingsTab from "../settings/tabs/user/GeneralUserSettingsTab";
import SettingsStore from "../../../settings/SettingsStore";
import LabsUserSettingsTab from "../settings/tabs/user/LabsUserSettingsTab";
import AppearanceUserSettingsTab from "../settings/tabs/user/AppearanceUserSettingsTab";
import SecurityUserSettingsTab from "../settings/tabs/user/SecurityUserSettingsTab";
import NotificationUserSettingsTab from "../settings/tabs/user/NotificationUserSettingsTab";
import PreferencesUserSettingsTab from "../settings/tabs/user/PreferencesUserSettingsTab";
@ -32,9 +33,21 @@ import * as sdk from "../../../index";
import SdkConfig from "../../../SdkConfig";
import MjolnirUserSettingsTab from "../settings/tabs/user/MjolnirUserSettingsTab";
export const USER_GENERAL_TAB = "USER_GENERAL_TAB";
export const USER_APPEARANCE_TAB = "USER_APPEARANCE_TAB";
export const USER_FLAIR_TAB = "USER_FLAIR_TAB";
export const USER_NOTIFICATIONS_TAB = "USER_NOTIFICATIONS_TAB";
export const USER_PREFERENCES_TAB = "USER_PREFERENCES_TAB";
export const USER_VOICE_TAB = "USER_VOICE_TAB";
export const USER_SECURITY_TAB = "USER_SECURITY_TAB";
export const USER_LABS_TAB = "USER_LABS_TAB";
export const USER_MJOLNIR_TAB = "USER_MJOLNIR_TAB";
export const USER_HELP_TAB = "USER_HELP_TAB";
export default class UserSettingsDialog extends React.Component {
static propTypes = {
onFinished: PropTypes.func.isRequired,
initialTabId: PropTypes.string,
};
constructor() {
@ -62,37 +75,50 @@ export default class UserSettingsDialog extends React.Component {
const tabs = [];
tabs.push(new Tab(
USER_GENERAL_TAB,
_td("General"),
"mx_UserSettingsDialog_settingsIcon",
<GeneralUserSettingsTab closeSettingsFn={this.props.onFinished} />,
));
tabs.push(new Tab(
USER_APPEARANCE_TAB,
_td("Appearance"),
"mx_UserSettingsDialog_appearanceIcon",
<AppearanceUserSettingsTab />,
));
tabs.push(new Tab(
USER_FLAIR_TAB,
_td("Flair"),
"mx_UserSettingsDialog_flairIcon",
<FlairUserSettingsTab />,
));
tabs.push(new Tab(
USER_NOTIFICATIONS_TAB,
_td("Notifications"),
"mx_UserSettingsDialog_bellIcon",
<NotificationUserSettingsTab />,
));
tabs.push(new Tab(
USER_PREFERENCES_TAB,
_td("Preferences"),
"mx_UserSettingsDialog_preferencesIcon",
<PreferencesUserSettingsTab />,
));
tabs.push(new Tab(
USER_VOICE_TAB,
_td("Voice & Video"),
"mx_UserSettingsDialog_voiceIcon",
<VoiceUserSettingsTab />,
));
tabs.push(new Tab(
USER_SECURITY_TAB,
_td("Security & Privacy"),
"mx_UserSettingsDialog_securityIcon",
<SecurityUserSettingsTab />,
<SecurityUserSettingsTab closeSettingsFn={this.props.onFinished} />,
));
if (SdkConfig.get()['showLabsSettings'] || SettingsStore.getLabsFeatures().length > 0) {
tabs.push(new Tab(
USER_LABS_TAB,
_td("Labs"),
"mx_UserSettingsDialog_labsIcon",
<LabsUserSettingsTab />,
@ -100,12 +126,14 @@ export default class UserSettingsDialog extends React.Component {
}
if (this.state.mjolnirEnabled) {
tabs.push(new Tab(
USER_MJOLNIR_TAB,
_td("Ignored users"),
"mx_UserSettingsDialog_mjolnirIcon",
<MjolnirUserSettingsTab />,
));
}
tabs.push(new Tab(
USER_HELP_TAB,
_td("Help & About"),
"mx_UserSettingsDialog_helpIcon",
<HelpUserSettingsTab closeSettingsFn={this.props.onFinished} />,
@ -121,7 +149,7 @@ export default class UserSettingsDialog extends React.Component {
<BaseDialog className='mx_UserSettingsDialog' hasCancel={true}
onFinished={this.props.onFinished} title={_t("Settings")}>
<div className='ms_SettingsDialog_content'>
<TabbedView tabs={this._getTabs()} />
<TabbedView tabs={this._getTabs()} initialTabId={this.props.initialTabId} />
</div>
</BaseDialog>
);

View file

@ -30,16 +30,29 @@ export default class VerificationRequestDialog extends React.Component {
constructor(...args) {
super(...args);
this.onFinished = this.onFinished.bind(this);
this.state = {};
if (this.props.verificationRequest) {
this.state.verificationRequest = this.props.verificationRequest;
} else if (this.props.verificationRequestPromise) {
this.props.verificationRequestPromise.then(r => {
this.setState({verificationRequest: r});
});
}
}
render() {
const BaseDialog = sdk.getComponent("views.dialogs.BaseDialog");
const EncryptionPanel = sdk.getComponent("views.right_panel.EncryptionPanel");
const request = this.state.verificationRequest;
const otherUserId = request && request.otherUserId;
const member = this.props.member ||
MatrixClientPeg.get().getUser(this.props.verificationRequest.otherUserId);
otherUserId && MatrixClientPeg.get().getUser(otherUserId);
const title = request && request.isSelfVerification ?
_t("Verify other session") : _t("Verification Request");
return <BaseDialog className="mx_InfoDialog" onFinished={this.onFinished}
contentId="mx_Dialog_content"
title={_t("Verification Request")}
title={title}
hasCancel={true}
>
<EncryptionPanel

View file

@ -20,7 +20,6 @@ import PropTypes from 'prop-types';
import * as sdk from '../../../../index';
import {MatrixClientPeg} from '../../../../MatrixClientPeg';
import { MatrixClient } from 'matrix-js-sdk';
import Modal from '../../../../Modal';
import { _t } from '../../../../languageHandler';
import { accessSecretStorage } from '../../../../CrossSigningManager';
@ -59,6 +58,7 @@ export default class RestoreKeyBackupDialog extends React.PureComponent {
forceRecoveryKey: false,
passPhrase: '',
restoreType: null,
progress: { stage: "prefetch" },
};
}
@ -80,16 +80,15 @@ export default class RestoreKeyBackupDialog extends React.PureComponent {
});
}
_progressCallback = (data) => {
this.setState({
progress: data,
});
}
_onResetRecoveryClick = () => {
this.props.onFinished(false);
Modal.createTrackedDialogAsync('Key Backup', 'Key Backup',
import('../../../../async-components/views/dialogs/keybackup/CreateKeyBackupDialog'),
{
onFinished: () => {
this._loadBackupStatus();
},
}, null, /* priority = */ false, /* static = */ true,
);
accessSecretStorage(() => {}, /* forceReset = */ true);
}
_onRecoveryKeyChange = (e) => {
@ -110,6 +109,7 @@ export default class RestoreKeyBackupDialog extends React.PureComponent {
// is the right one and restoring it is currently the only way we can do this.
const recoverInfo = await MatrixClientPeg.get().restoreKeyBackupWithPassword(
this.state.passPhrase, undefined, undefined, this.state.backupInfo,
{ progressCallback: this._progressCallback },
);
if (this.props.keyCallback) {
const key = await MatrixClientPeg.get().keyBackupKeyFromPassword(
@ -146,6 +146,7 @@ export default class RestoreKeyBackupDialog extends React.PureComponent {
try {
const recoverInfo = await MatrixClientPeg.get().restoreKeyBackupWithRecoveryKey(
this.state.recoveryKey, undefined, undefined, this.state.backupInfo,
{ progressCallback: this._progressCallback },
);
if (this.props.keyCallback) {
const key = MatrixClientPeg.get().keyBackupKeyFromRecoveryKey(this.state.recoveryKey);
@ -184,7 +185,8 @@ export default class RestoreKeyBackupDialog extends React.PureComponent {
// `accessSecretStorage` may prompt for storage access as needed.
const recoverInfo = await accessSecretStorage(async () => {
return MatrixClientPeg.get().restoreKeyBackupWithSecretStorage(
this.state.backupInfo,
this.state.backupInfo, undefined, undefined,
{ progressCallback: this._progressCallback },
);
});
this.setState({
@ -207,6 +209,7 @@ export default class RestoreKeyBackupDialog extends React.PureComponent {
undefined, /* targetRoomId */
undefined, /* targetSessionId */
backupInfo,
{ progressCallback: this._progressCallback },
);
this.setState({
recoverInfo,
@ -224,8 +227,10 @@ export default class RestoreKeyBackupDialog extends React.PureComponent {
loadError: null,
});
try {
const backupInfo = await MatrixClientPeg.get().getKeyBackupVersion();
const backupKeyStored = await MatrixClientPeg.get().isKeyBackupKeyStored();
const cli = MatrixClientPeg.get();
const backupInfo = await cli.getKeyBackupVersion();
const has4S = await cli.hasSecretStorageKey();
const backupKeyStored = has4S && await cli.isKeyBackupKeyStored();
this.setState({
backupInfo,
backupKeyStored,
@ -272,8 +277,20 @@ export default class RestoreKeyBackupDialog extends React.PureComponent {
let content;
let title;
if (this.state.loading) {
title = _t("Loading...");
content = <Spinner />;
title = _t("Restoring keys from backup");
let details;
if (this.state.progress.stage === "fetch") {
details = _t("Fetching keys from server...");
} else if (this.state.progress.stage === "load_keys") {
const { total, successes, failures } = this.state.progress;
details = _t("%(completed)s of %(total)s keys restored", { total, completed: successes + failures });
} else if (this.state.progress.stage === "prefetch") {
details = _t("Fetching keys from server...");
}
content = <div>
<div>{details}</div>
<Spinner />
</div>;
} else if (this.state.loadError) {
title = _t("Error");
content = _t("Unable to load backup status");
@ -283,7 +300,7 @@ export default class RestoreKeyBackupDialog extends React.PureComponent {
title = _t("Recovery key mismatch");
content = <div>
<p>{_t(
"Backup could not be decrypted with this key: " +
"Backup could not be decrypted with this recovery key: " +
"please verify that you entered the correct recovery key.",
)}</p>
</div>;
@ -291,7 +308,7 @@ export default class RestoreKeyBackupDialog extends React.PureComponent {
title = _t("Incorrect recovery passphrase");
content = <div>
<p>{_t(
"Backup could not be decrypted with this passphrase: " +
"Backup could not be decrypted with this recovery passphrase: " +
"please verify that you entered the correct recovery passphrase.",
)}</p>
</div>;
@ -305,7 +322,7 @@ export default class RestoreKeyBackupDialog extends React.PureComponent {
content = _t("No backup found!");
} else if (this.state.recoverInfo) {
const DialogButtons = sdk.getComponent('views.elements.DialogButtons');
title = _t("Backup restored");
title = _t("Keys restored");
let failedToDecrypt;
if (this.state.recoverInfo.total > this.state.recoverInfo.imported) {
failedToDecrypt = <p>{_t(
@ -314,7 +331,7 @@ export default class RestoreKeyBackupDialog extends React.PureComponent {
)}</p>;
}
content = <div>
<p>{_t("Restored %(sessionCount)s session keys", {sessionCount: this.state.recoverInfo.imported})}</p>
<p>{_t("Successfully restored %(sessionCount)s keys", {sessionCount: this.state.recoverInfo.imported})}</p>
{failedToDecrypt}
<DialogButtons primaryButton={_t('OK')}
onPrimaryButtonClick={this._onDone}
@ -435,7 +452,7 @@ export default class RestoreKeyBackupDialog extends React.PureComponent {
onFinished={this.props.onFinished}
title={title}
>
<div>
<div className='mx_RestoreKeyBackupDialog_content'>
{content}
</div>
</BaseDialog>

View file

@ -15,13 +15,25 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import { debounce } from 'lodash';
import classNames from 'classnames';
import React from 'react';
import PropTypes from "prop-types";
import * as sdk from '../../../../index';
import {MatrixClientPeg} from '../../../../MatrixClientPeg';
import Field from '../../elements/Field';
import AccessibleButton from '../../elements/AccessibleButton';
import { _t } from '../../../../languageHandler';
// Maximum acceptable size of a key file. It's 59 characters including the spaces we encode,
// so this should be plenty and allow for people putting extra whitespace in the file because
// maybe that's a thing people would do?
const KEY_FILE_MAX_SIZE = 128;
// Don't shout at the user that their key is invalid every time they type a key: wait a short time
const VALIDATION_THROTTLE_MS = 200;
/*
* Access Secure Secret Storage by requesting the user's passphrase.
*/
@ -35,9 +47,14 @@ export default class AccessSecretStorageDialog extends React.PureComponent {
constructor(props) {
super(props);
this._fileUpload = React.createRef();
this.state = {
recoveryKey: "",
recoveryKeyValid: false,
recoveryKeyValid: null,
recoveryKeyCorrect: null,
recoveryKeyFileError: null,
forceRecoveryKey: false,
passPhrase: '',
keyMatches: null,
@ -54,17 +71,89 @@ export default class AccessSecretStorageDialog extends React.PureComponent {
});
}
_onResetRecoveryClick = () => {
this.props.onFinished(false);
throw new Error("Resetting secret storage unimplemented");
_validateRecoveryKeyOnChange = debounce(() => {
this._validateRecoveryKey();
}, VALIDATION_THROTTLE_MS);
async _validateRecoveryKey() {
if (this.state.recoveryKey === '') {
this.setState({
recoveryKeyValid: null,
recoveryKeyCorrect: null,
});
return;
}
try {
const cli = MatrixClientPeg.get();
const decodedKey = cli.keyBackupKeyFromRecoveryKey(this.state.recoveryKey);
const correct = await cli.checkSecretStorageKey(
decodedKey, this.props.keyInfo,
);
this.setState({
recoveryKeyValid: true,
recoveryKeyCorrect: correct,
});
} catch (e) {
this.setState({
recoveryKeyValid: false,
recoveryKeyCorrect: false,
});
}
}
_onRecoveryKeyChange = (e) => {
this.setState({
recoveryKey: e.target.value,
recoveryKeyValid: MatrixClientPeg.get().isValidRecoveryKey(e.target.value),
keyMatches: null,
recoveryKeyFileError: null,
});
// also clear the file upload control so that the user can upload the same file
// the did before (otherwise the onchange wouldn't fire)
if (this._fileUpload.current) this._fileUpload.current.value = null;
// We don't use Field's validation here because a) we want it in a separate place rather
// than in a tooltip and b) we want it to display feedback based on the uploaded file
// as well as the text box. Ideally we would refactor Field's validation logic so we could
// re-use some of it.
this._validateRecoveryKeyOnChange();
}
_onRecoveryKeyFileChange = async e => {
if (e.target.files.length === 0) return;
const f = e.target.files[0];
if (f.size > KEY_FILE_MAX_SIZE) {
this.setState({
recoveryKeyFileError: true,
recoveryKeyCorrect: false,
recoveryKeyValid: false,
});
} else {
const contents = await f.text();
// test it's within the base58 alphabet. We could be more strict here, eg. require the
// right number of characters, but it's really just to make sure that what we're reading is
// text because we'll put it in the text field.
if (/^[123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz\s]+$/.test(contents)) {
this.setState({
recoveryKeyFileError: null,
recoveryKey: contents.trim(),
});
this._validateRecoveryKey();
} else {
this.setState({
recoveryKeyFileError: true,
recoveryKeyCorrect: false,
recoveryKeyValid: false,
recoveryKey: '',
});
}
}
}
_onRecoveryKeyFileUploadClick = () => {
this._fileUpload.current.click();
}
_onPassPhraseNext = async (e) => {
@ -104,6 +193,20 @@ export default class AccessSecretStorageDialog extends React.PureComponent {
});
}
getKeyValidationText() {
if (this.state.recoveryKeyFileError) {
return _t("Wrong file type");
} else if (this.state.recoveryKeyCorrect) {
return _t("Looks good!");
} else if (this.state.recoveryKeyValid) {
return _t("Wrong Recovery Key");
} else if (this.state.recoveryKeyValid === null) {
return '';
} else {
return _t("Invalid Recovery Key");
}
}
render() {
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
@ -116,17 +219,19 @@ export default class AccessSecretStorageDialog extends React.PureComponent {
let content;
let title;
let titleClass;
if (hasPassphrase && !this.state.forceRecoveryKey) {
const DialogButtons = sdk.getComponent('views.elements.DialogButtons');
const AccessibleButton = sdk.getComponent('elements.AccessibleButton');
title = _t("Enter secret storage passphrase");
title = _t("Security Phrase");
titleClass = ['mx_AccessSecretStorageDialog_titleWithIcon mx_AccessSecretStorageDialog_securePhraseTitle'];
let keyStatus;
if (this.state.keyMatches === false) {
keyStatus = <div className="mx_AccessSecretStorageDialog_keyStatus">
{"\uD83D\uDC4E "}{_t(
"Unable to access secret storage. Please verify that you " +
"entered the correct passphrase.",
"Unable to access secret storage. " +
"Please verify that you entered the correct recovery passphrase.",
)}
</div>;
} else {
@ -135,13 +240,15 @@ export default class AccessSecretStorageDialog extends React.PureComponent {
content = <div>
<p>{_t(
"<b>Warning</b>: You should only access secret storage " +
"from a trusted computer.", {},
{ b: sub => <b>{sub}</b> },
)}</p>
<p>{_t(
"Access your secure message history and your cross-signing " +
"identity for verifying other sessions by entering your passphrase.",
"Enter your Security Phrase or <button>Use your Security Key</button> to continue.", {},
{
button: s => <AccessibleButton className="mx_linkButton"
element="span"
onClick={this._onUseRecoveryKeyClick}
>
{s}
</AccessibleButton>,
},
)}</p>
<form className="mx_AccessSecretStorageDialog_primaryContainer" onSubmit={this._onPassPhraseNext}>
@ -152,10 +259,11 @@ export default class AccessSecretStorageDialog extends React.PureComponent {
value={this.state.passPhrase}
autoFocus={true}
autoComplete="new-password"
placeholder={_t("Security Phrase")}
/>
{keyStatus}
<DialogButtons
primaryButton={_t('Next')}
primaryButton={_t('Continue')}
onPrimaryButtonClick={this._onPassPhraseNext}
hasCancel={true}
onCancel={this._onCancel}
@ -163,88 +271,61 @@ export default class AccessSecretStorageDialog extends React.PureComponent {
primaryDisabled={this.state.passPhrase.length === 0}
/>
</form>
{_t(
"If you've forgotten your passphrase you can "+
"<button1>use your recovery key</button1> or " +
"<button2>set up new recovery options</button2>."
, {}, {
button1: s => <AccessibleButton className="mx_linkButton"
element="span"
onClick={this._onUseRecoveryKeyClick}
>
{s}
</AccessibleButton>,
button2: s => <AccessibleButton className="mx_linkButton"
element="span"
onClick={this._onResetRecoveryClick}
>
{s}
</AccessibleButton>,
})}
</div>;
} else {
title = _t("Enter secret storage recovery key");
title = _t("Security Key");
titleClass = ['mx_AccessSecretStorageDialog_titleWithIcon mx_AccessSecretStorageDialog_secureBackupTitle'];
const DialogButtons = sdk.getComponent('views.elements.DialogButtons');
const AccessibleButton = sdk.getComponent('elements.AccessibleButton');
let keyStatus;
if (this.state.recoveryKey.length === 0) {
keyStatus = <div className="mx_AccessSecretStorageDialog_keyStatus" />;
} else if (this.state.keyMatches === false) {
keyStatus = <div className="mx_AccessSecretStorageDialog_keyStatus">
{"\uD83D\uDC4E "}{_t(
"Unable to access secret storage. Please verify that you " +
"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")}
</div>;
}
const feedbackClasses = classNames({
'mx_AccessSecretStorageDialog_recoveryKeyFeedback': true,
'mx_AccessSecretStorageDialog_recoveryKeyFeedback_valid': this.state.recoveryKeyCorrect === true,
'mx_AccessSecretStorageDialog_recoveryKeyFeedback_invalid': this.state.recoveryKeyCorrect === false,
});
const recoveryKeyFeedback = <div className={feedbackClasses}>
{this.getKeyValidationText()}
</div>;
content = <div>
<p>{_t(
"<b>Warning</b>: You should only access secret storage " +
"from a trusted computer.", {},
{ b: sub => <b>{sub}</b> },
)}</p>
<p>{_t(
"Access your secure message history and your cross-signing " +
"identity for verifying other sessions by entering your recovery key.",
)}</p>
<p>{_t("Use your Security Key to continue.")}</p>
<form className="mx_AccessSecretStorageDialog_primaryContainer" onSubmit={this._onRecoveryKeyNext}>
<input className="mx_AccessSecretStorageDialog_recoveryKeyInput"
onChange={this._onRecoveryKeyChange}
value={this.state.recoveryKey}
autoFocus={true}
/>
{keyStatus}
<form className="mx_AccessSecretStorageDialog_primaryContainer" onSubmit={this._onRecoveryKeyNext} spellCheck={false}>
<div className="mx_AccessSecretStorageDialog_recoveryKeyEntry">
<div className="mx_AccessSecretStorageDialog_recoveryKeyEntry_textInput">
<Field
type="text"
label={_t('Security Key')}
value={this.state.recoveryKey}
onChange={this._onRecoveryKeyChange}
forceValidity={this.state.recoveryKeyCorrect}
/>
</div>
<span className="mx_AccessSecretStorageDialog_recoveryKeyEntry_entryControlSeparatorText">
{_t("or")}
</span>
<div>
<input type="file"
className="mx_AccessSecretStorageDialog_recoveryKeyEntry_fileInput"
ref={this._fileUpload}
onChange={this._onRecoveryKeyFileChange}
/>
<AccessibleButton kind="primary" onClick={this._onRecoveryKeyFileUploadClick}>
{_t("Upload")}
</AccessibleButton>
</div>
</div>
{recoveryKeyFeedback}
<DialogButtons
primaryButton={_t('Next')}
primaryButton={_t('Continue')}
onPrimaryButtonClick={this._onRecoveryKeyNext}
hasCancel={true}
cancelButton={_t("Go Back")}
cancelButtonClass='danger'
onCancel={this._onCancel}
focus={false}
primaryDisabled={!this.state.recoveryKeyValid}
/>
</form>
{_t(
"If you've forgotten your recovery key you can "+
"<button>set up new recovery options</button>."
, {}, {
button: s => <AccessibleButton className="mx_linkButton"
element="span"
onClick={this._onResetRecoveryClick}
>
{s}
</AccessibleButton>,
})}
</div>;
}
@ -252,6 +333,7 @@ export default class AccessSecretStorageDialog extends React.PureComponent {
<BaseDialog className='mx_AccessSecretStorageDialog'
onFinished={this.props.onFinished}
title={title}
titleClass={titleClass}
>
<div>
{content}