Merge branch 'develop' into travis/dispatcher-types

This commit is contained in:
Travis Ralston 2020-05-15 09:56:02 -06:00
commit 798e7a1f86
38 changed files with 517 additions and 191 deletions

View file

@ -1971,6 +1971,11 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
render() {
// console.log(`Rendering MatrixChat with view ${this.state.view}`);
let fragmentAfterLogin = "";
if (this.props.initialScreenAfterLogin) {
fragmentAfterLogin = `/${this.props.initialScreenAfterLogin.screen}`;
}
let view;
if (this.state.view === Views.LOADING) {
@ -2050,7 +2055,7 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
}
} else if (this.state.view === Views.WELCOME) {
const Welcome = sdk.getComponent('auth.Welcome');
view = <Welcome {...this.getServerProperties()} />;
view = <Welcome {...this.getServerProperties()} fragmentAfterLogin={fragmentAfterLogin} />;
} else if (this.state.view === Views.REGISTER) {
const Registration = sdk.getComponent('structures.auth.Registration');
view = (
@ -2089,6 +2094,7 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
defaultDeviceDisplayName={this.props.defaultDeviceDisplayName}
onForgotPasswordClick={this.onForgotPasswordClick}
onServerConfigChange={this.onServerConfigChange}
fragmentAfterLogin={fragmentAfterLogin}
{...this.getServerProperties()}
/>
);
@ -2098,6 +2104,7 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
<SoftLogout
realQueryParams={this.props.realQueryParams}
onTokenLoginCompleted={this.props.onTokenLoginCompleted}
fragmentAfterLogin={fragmentAfterLogin}
/>
);
} else {

View file

@ -41,7 +41,7 @@ import * as ObjectUtils from '../../ObjectUtils';
import * as Rooms from '../../Rooms';
import eventSearch from '../../Searching';
import {isOnlyCtrlOrCmdKeyEvent, Key} from '../../Keyboard';
import {isOnlyCtrlOrCmdIgnoreShiftKeyEvent, isOnlyCtrlOrCmdKeyEvent, Key} from '../../Keyboard';
import MainSplit from './MainSplit';
import RightPanel from './RightPanel';
@ -588,6 +588,18 @@ export default createReactClass({
handled = true;
}
break;
case Key.PAGE_UP:
if (!ev.altKey && !ev.ctrlKey && ev.shiftKey && !ev.metaKey) {
this.jumpToReadMarker();
handled = true;
}
break;
case Key.U.toUpperCase():
if (isOnlyCtrlOrCmdIgnoreShiftKeyEvent(ev) && ev.shiftKey) {
dis.dispatch({ action: "upload_file" })
handled = true;
}
break;
}
if (handled) {

View file

@ -355,7 +355,8 @@ export default createReactClass({
ev.preventDefault();
ev.stopPropagation();
const ssoKind = step === 'm.login.sso' ? 'sso' : 'cas';
PlatformPeg.get().startSingleSignOn(this._loginLogic.createTemporaryClient(), ssoKind);
PlatformPeg.get().startSingleSignOn(this._loginLogic.createTemporaryClient(), ssoKind,
this.props.fragmentAfterLogin);
} else {
// Don't intercept - just go through to the register page
this.onRegisterClick(ev);
@ -628,7 +629,9 @@ export default createReactClass({
<SSOButton
className="mx_Login_sso_link mx_Login_submit"
matrixClient={this._loginLogic.createTemporaryClient()}
loginType={loginType} />
loginType={loginType}
fragmentAfterLogin={this.props.fragmentAfterLogin}
/>
</div>
);
},

View file

@ -243,10 +243,15 @@ export default createReactClass({
});
};
try {
await this._makeRegisterRequest({});
// This should never succeed since we specified an empty
// auth object.
console.log("Expecting 401 from register request but got success!");
// We do the first registration request ourselves to discover whether we need to
// do SSO instead. If we've already started the UI Auth process though, we don't
// need to.
if (!this.state.doingUIAuth) {
await this._makeRegisterRequest({});
// This should never succeed since we specified an empty
// auth object.
console.log("Expecting 401 from register request but got success!");
}
} catch (e) {
if (e.httpStatus === 401) {
this.setState({

View file

@ -244,7 +244,9 @@ export default class SoftLogout extends React.Component {
<p>{introText}</p>
<SSOButton
matrixClient={MatrixClientPeg.get()}
loginType={this.state.loginView === LOGIN_VIEW.CAS ? "cas" : "sso"} />
loginType={this.state.loginView === LOGIN_VIEW.CAS ? "cas" : "sso"}
fragmentAfterLogin={this.props.fragmentAfterLogin}
/>
</div>
);
}

View file

@ -412,14 +412,14 @@ export const EmailIdentityAuthEntry = createReactClass({
this.props.onPhaseChange(DEFAULT_PHASE);
},
getInitialState: function() {
return {
requestingToken: false,
};
},
render: function() {
if (this.state.requestingToken) {
// This component is now only displayed once the token has been requested,
// so we know the email has been sent. It can also get loaded after the user
// has clicked the validation link if the server takes a while to propagate
// the validation internally. If we're in the session spawned from clicking
// the validation link, we won't know the email address, so if we don't have it,
// assume that the link has been clicked and the server will realise when we poll.
if (this.props.inputs.emailAddress === undefined) {
const Loader = sdk.getComponent("elements.Spinner");
return <Loader />;
} else {

View file

@ -45,7 +45,8 @@ export default class Welcome extends React.PureComponent {
idBaseUrl: isUrl,
});
const plaf = PlatformPeg.get();
const callbackUrl = plaf.getSSOCallbackUrl(tmpClient.getHomeserverUrl(), tmpClient.getIdentityServerUrl());
const callbackUrl = plaf.getSSOCallbackUrl(tmpClient.getHomeserverUrl(), tmpClient.getIdentityServerUrl(),
this.props.fragmentAfterLogin);
return (
<AuthPage>

View file

@ -34,6 +34,7 @@ export default class DeactivateAccountDialog extends React.Component {
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.
@ -42,21 +43,7 @@ export default class DeactivateAccountDialog extends React.Component {
continueKind: null,
};
MatrixClientPeg.get().deactivateAccount(null, false).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});
} else {
this.setState({errStr: _t("Server did not return valid authentication information.")});
}
});
this._initAuth(/* shouldErase= */false);
}
_onStagePhaseChange = (stage, phase) => {
@ -124,13 +111,40 @@ export default class DeactivateAccountDialog extends React.Component {
_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');
@ -142,7 +156,7 @@ export default class DeactivateAccountDialog extends React.Component {
}
let auth = <div>{_t("Loading...")}</div>;
if (this.state.authData) {
if (this.state.authData && this.state.authEnabled) {
auth = (
<div>
{this.state.bodyText}

View file

@ -24,7 +24,7 @@ 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";
@ -221,7 +221,7 @@ export default class ShareDialog extends React.PureComponent<IProps, IState> {
<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) => (

View file

@ -0,0 +1,51 @@
/*
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 {toDataURL, QRCodeSegment, QRCodeToDataURLOptions} from "qrcode";
import classNames from "classnames";
import {_t} from "../../../languageHandler";
import Spinner from "./Spinner";
interface IProps extends QRCodeToDataURLOptions {
data: string | QRCodeSegment[];
className?: string;
}
const defaultOptions: QRCodeToDataURLOptions = {
errorCorrectionLevel: 'L', // we want it as trivial-looking as possible
};
const QRCode: React.FC<IProps> = ({data, className, ...options}) => {
const [dataUri, setUri] = React.useState<string>(null);
React.useEffect(() => {
let cancelled = false;
toDataURL(data, {...defaultOptions, ...options}).then(uri => {
if (cancelled) return;
setUri(uri);
});
return () => {
cancelled = true;
};
}, [JSON.stringify(data), options]);
return <div className={classNames("mx_QRCode", className)}>
{ dataUri ? <img src={dataUri} className="mx_VerificationQRCode" alt={_t("QR Code")} /> : <Spinner /> }
</div>;
};
export default QRCode;

View file

@ -21,9 +21,9 @@ import PlatformPeg from "../../../PlatformPeg";
import AccessibleButton from "./AccessibleButton";
import {_t} from "../../../languageHandler";
const SSOButton = ({matrixClient, loginType, ...props}) => {
const SSOButton = ({matrixClient, loginType, fragmentAfterLogin, ...props}) => {
const onClick = () => {
PlatformPeg.get().startSingleSignOn(matrixClient, loginType);
PlatformPeg.get().startSingleSignOn(matrixClient, loginType, fragmentAfterLogin);
};
return (
@ -36,6 +36,7 @@ const SSOButton = ({matrixClient, loginType, ...props}) => {
SSOButton.propTypes = {
matrixClient: PropTypes.object.isRequired, // does not use context as may use a temporary client
loginType: PropTypes.oneOf(["sso", "cas"]), // defaults to "sso" in base-apis
fragmentAfterLogin: PropTypes.string,
};
export default SSOButton;

View file

@ -17,8 +17,7 @@ limitations under the License.
import React from "react";
import PropTypes from "prop-types";
import {replaceableComponent} from "../../../../utils/replaceableComponent";
import Spinner from "../Spinner";
import * as QRCode from "qrcode";
import QRCode from "../QRCode";
@replaceableComponent("views.elements.crypto.VerificationQRCode")
export default class VerificationQRCode extends React.PureComponent {
@ -26,33 +25,12 @@ export default class VerificationQRCode extends React.PureComponent {
qrCodeData: PropTypes.object.isRequired,
};
constructor(props) {
super(props);
this.state = {
dataUri: null,
};
this.generateQrCode();
}
componentDidUpdate(prevProps): void {
if (JSON.stringify(this.props) === JSON.stringify(prevProps)) return; // No prop change
this.generateQRCode();
}
async generateQrCode() {
// Now actually assemble the QR code's data URI
const uri = await QRCode.toDataURL([{data: this.props.qrCodeData.buffer, mode: 'byte'}], {
errorCorrectionLevel: 'L', // we want it as trivial-looking as possible
});
this.setState({dataUri: uri});
}
render() {
if (!this.state.dataUri) {
return <div className='mx_VerificationQRCode'><Spinner /></div>;
}
return <img src={this.state.dataUri} className='mx_VerificationQRCode' />;
return (
<QRCode
data={[{data: this.props.qrCodeData.buffer, mode: 'byte'}]}
className="mx_VerificationQRCode"
width={196} />
);
}
}

View file

@ -114,8 +114,19 @@ class UploadButton extends React.Component {
this.onUploadFileInputChange = this.onUploadFileInputChange.bind(this);
this._uploadInput = createRef();
this._dispatcherRef = dis.register(this.onAction);
}
componentWillUnmount() {
dis.unregister(this._dispatcherRef);
}
onAction = payload => {
if (payload.action === "upload_file") {
this.onUploadClick();
}
};
onUploadClick(ev) {
if (MatrixClientPeg.get().isGuest()) {
dis.dispatch({action: 'require_registration'});
@ -128,7 +139,7 @@ class UploadButton extends React.Component {
if (ev.target.files.length === 0) return;
// take a copy so we can safely reset the value of the form control
// (Note it is a FileList: we can't use slice or sesnible iteration).
// (Note it is a FileList: we can't use slice or sensible iteration).
const tfiles = [];
for (let i = 0; i < ev.target.files.length; ++i) {
tfiles.push(ev.target.files[i]);

View file

@ -36,7 +36,6 @@ export default class CrossSigningPanel extends React.PureComponent {
userSigningPrivateKeyCached: false,
sessionBackupKeyCached: false,
secretStorageKeyInAccount: false,
secretStorageKeyNeedsUpgrade: null,
};
}
@ -88,7 +87,6 @@ export default class CrossSigningPanel extends React.PureComponent {
const homeserverSupportsCrossSigning =
await cli.doesServerSupportUnstableFeature("org.matrix.e2e_cross_signing");
const crossSigningReady = await cli.isCrossSigningReady();
const secretStorageKeyNeedsUpgrade = await cli.secretStorageKeyNeedsUpgrade();
this.setState({
crossSigningPublicKeysOnDevice,
@ -100,7 +98,6 @@ export default class CrossSigningPanel extends React.PureComponent {
secretStorageKeyInAccount,
homeserverSupportsCrossSigning,
crossSigningReady,
secretStorageKeyNeedsUpgrade,
});
}
@ -150,7 +147,6 @@ export default class CrossSigningPanel extends React.PureComponent {
secretStorageKeyInAccount,
homeserverSupportsCrossSigning,
crossSigningReady,
secretStorageKeyNeedsUpgrade,
} = this.state;
let errorSection;
@ -259,10 +255,6 @@ export default class CrossSigningPanel extends React.PureComponent {
<td>{_t("Homeserver feature support:")}</td>
<td>{homeserverSupportsCrossSigning ? _t("exists") : _t("not found")}</td>
</tr>
<tr>
<td>{_t("Secret Storage key format:")}</td>
<td>{secretStorageKeyNeedsUpgrade ? _t("outdated") : _t("up to date")}</td>
</tr>
</tbody></table>
</details>
{errorSection}

View file

@ -348,7 +348,7 @@ export default class GeneralUserSettingsTab extends React.Component {
// For newer homeservers with separate 3PID add and bind methods (MSC2290),
// there is no such concern, so we can always show the HS account 3PIDs.
if (this.state.haveIdServer || this.state.serverSupportsSeparateAddAndBind === true) {
const emails = this.state.loading3pids || true
const emails = this.state.loading3pids
? <Spinner />
: <EmailAddresses
emails={this.state.emails}

View file

@ -17,7 +17,6 @@ limitations under the License.
import React from 'react';
import PropTypes from 'prop-types';
import Modal from '../../../Modal';
import { MatrixClientPeg } from '../../../MatrixClientPeg';
import * as sdk from "../../../index";
import { _t } from '../../../languageHandler';
import DeviceListener from '../../../DeviceListener';
@ -31,7 +30,6 @@ export default class SetupEncryptionToast extends React.PureComponent {
'set_up_encryption',
'verify_this_session',
'upgrade_encryption',
'upgrade_ssss',
]).isRequired,
};
@ -39,24 +37,6 @@ export default class SetupEncryptionToast extends React.PureComponent {
DeviceListener.sharedInstance().dismissEncryptionSetup();
};
async _waitForCompletion() {
if (this.props.kind === 'upgrade_ssss') {
return new Promise(resolve => {
const recheck = async () => {
const needsUpgrade = await MatrixClientPeg.get().secretStorageKeyNeedsUpgrade();
if (!needsUpgrade) {
MatrixClientPeg.get().removeListener('accountData', recheck);
resolve();
}
};
MatrixClientPeg.get().on('accountData', recheck);
recheck();
});
} else {
return;
}
}
_onSetupClick = async () => {
if (this.props.kind === "verify_this_session") {
Modal.createTrackedDialog('Verify session', 'Verify session', SetupEncryptionDialog,
@ -68,7 +48,6 @@ export default class SetupEncryptionToast extends React.PureComponent {
);
try {
await accessSecretStorage();
await this._waitForCompletion();
} finally {
modal.close();
}
@ -82,8 +61,6 @@ export default class SetupEncryptionToast extends React.PureComponent {
return _t('Verify yourself & others to keep your chats safe');
case 'verify_this_session':
return _t('Other users may not trust it');
case 'upgrade_ssss':
return _t('Update your secure storage');
}
}
@ -92,7 +69,6 @@ export default class SetupEncryptionToast extends React.PureComponent {
case 'set_up_encryption':
return _t('Set up');
case 'upgrade_encryption':
case 'upgrade_ssss':
return _t('Upgrade');
case 'verify_this_session':
return _t('Verify');