Convert things to Typescript and re-use a generic component

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
pull/21833/head
Michael Telatynski 2020-05-22 13:29:53 +01:00
parent b21e5ba10b
commit 14cee41360
9 changed files with 342 additions and 303 deletions

View File

@ -14,19 +14,24 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import { MatrixClientPeg } from './MatrixClientPeg';
import {MatrixClientPeg} from './MatrixClientPeg';
import SettingsStore from './settings/SettingsStore';
import * as sdk from './index';
import { _t } from './languageHandler';
import ToastStore from './stores/ToastStore';
import {
hideToast as hideBulkUnverifiedSessionsToast,
showToast as showBulkUnverifiedSessionsToast
} from "./toasts/BulkUnverifiedSessionsToast";
import {
hideToast as hideSetupEncryptionToast,
Kind as SetupKind,
Kind,
showToast as showSetupEncryptionToast
} from "./toasts/SetupEncryptionToast";
import {
hideToast as hideUnverifiedSessionsToast,
showToast as showUnverifiedSessionsToast
} from "./toasts/UnverifiedSessionToast";
const KEY_BACKUP_POLL_INTERVAL = 5 * 60 * 1000;
const THIS_DEVICE_TOAST_KEY = 'setupencryption';
const OTHER_DEVICES_TOAST_KEY = 'reviewsessions';
function toastKey(deviceId) {
return "unverified_session_" + deviceId;
}
export default class DeviceListener {
// device IDs for which the user has dismissed the verify toast ('Later')
@ -82,7 +87,7 @@ export default class DeviceListener {
*
* @param {String[]} deviceIds List of device IDs to dismiss notifications for
*/
async dismissUnverifiedSessions(deviceIds: string[]) {
async dismissUnverifiedSessions(deviceIds: Iterable<string>) {
for (const d of deviceIds) {
this.dismissed.add(d);
}
@ -181,51 +186,25 @@ export default class DeviceListener {
const crossSigningReady = await cli.isCrossSigningReady();
if (this.dismissedThisDeviceToast) {
ToastStore.sharedInstance().dismissToast(THIS_DEVICE_TOAST_KEY);
if (this.dismissedThisDeviceToast || crossSigningReady) {
hideSetupEncryptionToast();
} else {
if (!crossSigningReady) {
// make sure our keys are finished downlaoding
await cli.downloadKeys([cli.getUserId()]);
// cross signing isn't enabled - nag to enable it
// There are 3 different toasts for:
if (cli.getStoredCrossSigningForUser(cli.getUserId())) {
// Cross-signing on account but this device doesn't trust the master key (verify this session)
ToastStore.sharedInstance().addOrReplaceToast({
key: THIS_DEVICE_TOAST_KEY,
title: _t("Verify this session"),
icon: "verification_warning",
props: {kind: 'verify_this_session'},
component: sdk.getComponent("toasts.SetupEncryptionToast"),
priority: 95,
});
} else {
const backupInfo = await this._getKeyBackupInfo();
if (backupInfo) {
// No cross-signing on account but key backup available (upgrade encryption)
ToastStore.sharedInstance().addOrReplaceToast({
key: THIS_DEVICE_TOAST_KEY,
title: _t("Encryption upgrade available"),
icon: "verification_warning",
props: {kind: 'upgrade_encryption'},
component: sdk.getComponent("toasts.SetupEncryptionToast"),
priority: 40,
});
} else {
// No cross-signing or key backup on account (set up encryption)
ToastStore.sharedInstance().addOrReplaceToast({
key: THIS_DEVICE_TOAST_KEY,
title: _t("Set up encryption"),
icon: "verification_warning",
props: {kind: 'set_up_encryption'},
component: sdk.getComponent("toasts.SetupEncryptionToast"),
priority: 40,
});
}
}
// make sure our keys are finished downloading
await cli.downloadKeys([cli.getUserId()]);
// cross signing isn't enabled - nag to enable it
// There are 3 different toasts for:
if (cli.getStoredCrossSigningForUser(cli.getUserId())) {
// Cross-signing on account but this device doesn't trust the master key (verify this session)
showSetupEncryptionToast(SetupKind.VERIFY_THIS_SESSION);
} else {
// cross-signing is ready, and we don't need to upgrade encryption
ToastStore.sharedInstance().dismissToast(THIS_DEVICE_TOAST_KEY);
const backupInfo = await this._getKeyBackupInfo();
if (backupInfo) {
// No cross-signing on account but key backup available (upgrade encryption)
showSetupEncryptionToast(Kind.UPGRADE_ENCRYPTION);
} else {
// No cross-signing or key backup on account (set up encryption)
showSetupEncryptionToast(Kind.SET_UP_ENCRYPTION);
}
}
}
@ -261,36 +240,20 @@ export default class DeviceListener {
// Display or hide the batch toast for old unverified sessions
if (oldUnverifiedDeviceIds.size > 0) {
ToastStore.sharedInstance().addOrReplaceToast({
key: OTHER_DEVICES_TOAST_KEY,
title: _t("Review where youre logged in"),
icon: "verification_warning",
props: {
deviceIds: oldUnverifiedDeviceIds,
},
component: sdk.getComponent("toasts.BulkUnverifiedSessionsToast"),
priority: 50,
});
showBulkUnverifiedSessionsToast(oldUnverifiedDeviceIds);
} else {
ToastStore.sharedInstance().dismissToast(OTHER_DEVICES_TOAST_KEY);
hideBulkUnverifiedSessionsToast();
}
// Show toasts for new unverified devices if they aren't already there
for (const deviceId of newUnverifiedDeviceIds) {
ToastStore.sharedInstance().addOrReplaceToast({
key: toastKey(deviceId),
title: _t("New login. Was this you?"),
icon: "verification_warning",
props: { deviceId },
component: sdk.getComponent("toasts.UnverifiedSessionToast"),
priority: 80,
});
showUnverifiedSessionsToast(deviceId);
}
// ...and hide any we don't need any more
for (const deviceId of this.displayingToastsForDeviceIds) {
if (!newUnverifiedDeviceIds.has(deviceId)) {
ToastStore.sharedInstance().dismissToast(toastKey(deviceId));
hideUnverifiedSessionsToast(deviceId);
}
}

View File

@ -1,56 +0,0 @@
/*
Copyright 2020 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import React from 'react';
import PropTypes from 'prop-types';
import { _t } from '../../../languageHandler';
import dis from "../../../dispatcher/dispatcher";
import { MatrixClientPeg } from '../../../MatrixClientPeg';
import DeviceListener from '../../../DeviceListener';
import FormButton from '../elements/FormButton';
import { replaceableComponent } from '../../../utils/replaceableComponent';
@replaceableComponent("views.toasts.BulkUnverifiedSessionsToast")
export default class BulkUnverifiedSessionsToast extends React.PureComponent {
static propTypes = {
deviceIds: PropTypes.array,
}
_onLaterClick = () => {
DeviceListener.sharedInstance().dismissUnverifiedSessions(this.props.deviceIds);
};
_onReviewClick = async () => {
DeviceListener.sharedInstance().dismissUnverifiedSessions(this.props.deviceIds);
dis.dispatch({
action: 'view_user_info',
userId: MatrixClientPeg.get().getUserId(),
});
};
render() {
return (<div>
<div className="mx_Toast_description">
{_t("Verify all your sessions to ensure your account & messages are safe")}
</div>
<div className="mx_Toast_buttons" aria-live="off">
<FormButton label={_t("Later")} kind="danger" onClick={this._onLaterClick} />
<FormButton label={_t("Review")} onClick={this._onReviewClick} />
</div>
</div>);
}
}

View File

@ -0,0 +1,42 @@
/*
Copyright 2020 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import React, {ReactChild} from "react";
import FormButton from "../elements/FormButton";
interface IProps {
description: ReactChild;
acceptLabel: string;
rejectLabel?: string;
onAccept();
onReject?();
}
const GenericToast: React.FC<IProps> = ({description, acceptLabel, rejectLabel, onAccept, onReject}) => {
return <div>
<div className="mx_Toast_description">
{ description }
</div>
<div className="mx_Toast_buttons" aria-live="off">
{onReject && rejectLabel && <FormButton label={rejectLabel} kind="danger" onClick={onReject} /> }
<FormButton label={acceptLabel} onClick={onAccept} />
</div>
</div>;
};
export default GenericToast;

View File

@ -1,88 +0,0 @@
/*
Copyright 2020 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import React from 'react';
import PropTypes from 'prop-types';
import Modal from '../../../Modal';
import * as sdk from "../../../index";
import { _t } from '../../../languageHandler';
import DeviceListener from '../../../DeviceListener';
import SetupEncryptionDialog from "../dialogs/SetupEncryptionDialog";
import { accessSecretStorage } from '../../../CrossSigningManager';
export default class SetupEncryptionToast extends React.PureComponent {
static propTypes = {
toastKey: PropTypes.string.isRequired,
kind: PropTypes.oneOf([
'set_up_encryption',
'verify_this_session',
'upgrade_encryption',
]).isRequired,
};
_onLaterClick = () => {
DeviceListener.sharedInstance().dismissEncryptionSetup();
};
_onSetupClick = async () => {
if (this.props.kind === "verify_this_session") {
Modal.createTrackedDialog('Verify session', 'Verify session', SetupEncryptionDialog,
{}, null, /* priority = */ false, /* static = */ true);
} else {
const Spinner = sdk.getComponent("elements.Spinner");
const modal = Modal.createDialog(
Spinner, null, 'mx_Dialog_spinner', /* priority */ false, /* static */ true,
);
try {
await accessSecretStorage();
} finally {
modal.close();
}
}
};
getDescription() {
switch (this.props.kind) {
case 'set_up_encryption':
case 'upgrade_encryption':
return _t('Verify yourself & others to keep your chats safe');
case 'verify_this_session':
return _t('Other users may not trust it');
}
}
getSetupCaption() {
switch (this.props.kind) {
case 'set_up_encryption':
return _t('Set up');
case 'upgrade_encryption':
return _t('Upgrade');
case 'verify_this_session':
return _t('Verify');
}
}
render() {
const FormButton = sdk.getComponent("elements.FormButton");
return (<div>
<div className="mx_Toast_description">{this.getDescription()}</div>
<div className="mx_Toast_buttons" aria-live="off">
<FormButton label={_t("Later")} kind="danger" onClick={this._onLaterClick} />
<FormButton label={this.getSetupCaption()} onClick={this._onSetupClick} />
</div>
</div>);
}
}

View File

@ -1,66 +0,0 @@
/*
Copyright 2020 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import React from 'react';
import PropTypes from 'prop-types';
import { _t } from '../../../languageHandler';
import { MatrixClientPeg } from '../../../MatrixClientPeg';
import Modal from '../../../Modal';
import DeviceListener from '../../../DeviceListener';
import NewSessionReviewDialog from '../dialogs/NewSessionReviewDialog';
import FormButton from '../elements/FormButton';
import { replaceableComponent } from '../../../utils/replaceableComponent';
@replaceableComponent("views.toasts.UnverifiedSessionToast")
export default class UnverifiedSessionToast extends React.PureComponent {
static propTypes = {
deviceId: PropTypes.string,
}
_onLaterClick = () => {
DeviceListener.sharedInstance().dismissUnverifiedSessions([this.props.deviceId]);
};
_onReviewClick = async () => {
const cli = MatrixClientPeg.get();
Modal.createTrackedDialog('New Session Review', 'Starting dialog', NewSessionReviewDialog, {
userId: cli.getUserId(),
device: cli.getStoredDevice(cli.getUserId(), this.props.deviceId),
onFinished: (r) => {
if (!r) {
/* This'll come back false if the user clicks "this wasn't me" and saw a warning dialog */
DeviceListener.sharedInstance().dismissUnverifiedSessions([this.props.deviceId]);
}
},
}, null, /* priority = */ false, /* static = */ true);
};
render() {
const cli = MatrixClientPeg.get();
const device = cli.getStoredDevice(cli.getUserId(), this.props.deviceId);
return (<div>
<div className="mx_Toast_description">
{_t(
"Verify the new login accessing your account: %(name)s", { name: device.getDisplayName()})}
</div>
<div className="mx_Toast_buttons" aria-live="off">
<FormButton label={_t("Later")} kind="danger" onClick={this._onLaterClick} />
<FormButton label={_t("Verify")} onClick={this._onReviewClick} />
</div>
</div>);
}
}

View File

@ -14,8 +14,8 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import React from 'react';
import PropTypes from 'prop-types';
import React from "react";
import * as sdk from "../../../index";
import { _t } from '../../../languageHandler';
import {MatrixClientPeg} from '../../../MatrixClientPeg';
@ -24,8 +24,23 @@ import {userLabelForEventRoom} from "../../../utils/KeyVerificationStateObserver
import dis from "../../../dispatcher/dispatcher";
import ToastStore from "../../../stores/ToastStore";
import Modal from "../../../Modal";
import GenericToast from "./GenericToast";
import {VerificationRequest} from "matrix-js-sdk/src/crypto/verification/request/VerificationRequest";
import {DeviceInfo} from "matrix-js-sdk/src/crypto/deviceinfo";
interface IProps {
toastKey: string;
request: VerificationRequest;
}
interface IState {
counter: number;
device?: DeviceInfo;
}
export default class VerificationRequestToast extends React.PureComponent<IProps, IState> {
private intervalHandle: NodeJS.Timeout;
export default class VerificationRequestToast extends React.PureComponent {
constructor(props) {
super(props);
this.state = {counter: Math.ceil(props.request.timeout / 1000)};
@ -34,7 +49,7 @@ export default class VerificationRequestToast extends React.PureComponent {
async componentDidMount() {
const {request} = this.props;
if (request.timeout && request.timeout > 0) {
this._intervalHandle = setInterval(() => {
this.intervalHandle = setInterval(() => {
let {counter} = this.state;
counter = Math.max(0, counter - 1);
this.setState({counter});
@ -56,7 +71,7 @@ export default class VerificationRequestToast extends React.PureComponent {
}
componentWillUnmount() {
clearInterval(this._intervalHandle);
clearInterval(this.intervalHandle);
const {request} = this.props;
request.off("change", this._checkRequestIsPending);
}
@ -110,7 +125,6 @@ export default class VerificationRequestToast extends React.PureComponent {
};
render() {
const FormButton = sdk.getComponent("elements.FormButton");
const {request} = this.props;
let nameLabel;
if (request.isSelfVerification) {
@ -133,20 +147,16 @@ export default class VerificationRequestToast extends React.PureComponent {
}
}
}
const declineLabel = this.state.counter == 0 ?
const declineLabel = this.state.counter === 0 ?
_t("Decline") :
_t("Decline (%(counter)s)", {counter: this.state.counter});
return (<div>
<div className="mx_Toast_description">{nameLabel}</div>
<div className="mx_Toast_buttons" aria-live="off">
<FormButton label={declineLabel} kind="danger" onClick={this.cancel} />
<FormButton label={_t("Accept")} onClick={this.accept} />
</div>
</div>);
return <GenericToast
description={nameLabel}
acceptLabel={_t("Accept")}
onAccept={this.accept}
rejectLabel={declineLabel}
onReject={this.cancel}
/>;
}
}
VerificationRequestToast.propTypes = {
request: PropTypes.object.isRequired,
toastKey: PropTypes.string.isRequired,
};

View File

@ -0,0 +1,58 @@
/*
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 { _t } from '../languageHandler';
import dis from "../dispatcher/dispatcher";
import { MatrixClientPeg } from '../MatrixClientPeg';
import DeviceListener from '../DeviceListener';
import GenericToast from "../components/views/toasts/GenericToast";
import ToastStore from "../stores/ToastStore";
const TOAST_KEY = "reviewsessions";
export const showToast = (deviceIds: Set<string>) => {
const onAccept = () => {
DeviceListener.sharedInstance().dismissUnverifiedSessions(deviceIds);
dis.dispatch({
action: 'view_user_info',
userId: MatrixClientPeg.get().getUserId(),
});
};
const onReject = () => {
DeviceListener.sharedInstance().dismissUnverifiedSessions(deviceIds);
};
ToastStore.sharedInstance().addOrReplaceToast({
key: TOAST_KEY,
title: _t("Review where youre logged in"),
icon: "verification_warning",
props: {
description: _t("Verify all your sessions to ensure your account & messages are safe"),
acceptLabel: _t("Review"),
onAccept,
rejectLabel: _t("Later"),
onReject,
},
component: GenericToast,
priority: 50,
});
};
export const hideToast = () => {
ToastStore.sharedInstance().dismissToast(TOAST_KEY);
};

View File

@ -0,0 +1,106 @@
/*
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 Modal from "../Modal";
import * as sdk from "../index";
import { _t } from "../languageHandler";
import DeviceListener from "../DeviceListener";
import SetupEncryptionDialog from "../components/views/dialogs/SetupEncryptionDialog";
import { accessSecretStorage } from "../CrossSigningManager";
import ToastStore from "../stores/ToastStore";
import GenericToast from "../components/views/toasts/GenericToast";
const TOAST_KEY = "setupencryption";
const getTitle = (kind: Kind) => {
switch (kind) {
case Kind.SET_UP_ENCRYPTION:
return _t("Set up encryption");
case Kind.UPGRADE_ENCRYPTION:
return _t("Encryption upgrade available");
case Kind.VERIFY_THIS_SESSION:
return _t("Verify this session");
}
};
const getSetupCaption = (kind: Kind) => {
switch (kind) {
case Kind.SET_UP_ENCRYPTION:
return _t("Set up");
case Kind.UPGRADE_ENCRYPTION:
return _t("Upgrade");
case Kind.VERIFY_THIS_SESSION:
return _t("Verify");
}
};
const getDescription = (kind: Kind) => {
switch (kind) {
case Kind.SET_UP_ENCRYPTION:
case Kind.UPGRADE_ENCRYPTION:
return _t("Verify yourself & others to keep your chats safe");
case Kind.VERIFY_THIS_SESSION:
return _t("Other users may not trust it");
}
};
export enum Kind {
SET_UP_ENCRYPTION = "set_up_encryption",
UPGRADE_ENCRYPTION = "upgrade_encryption",
VERIFY_THIS_SESSION = "verify_this_session",
}
const onReject = () => {
DeviceListener.sharedInstance().dismissEncryptionSetup();
};
export const showToast = (kind: Kind) => {
const onAccept = async () => {
if (kind === Kind.VERIFY_THIS_SESSION) {
Modal.createTrackedDialog("Verify session", "Verify session", SetupEncryptionDialog,
{}, null, /* priority = */ false, /* static = */ true);
} else {
const Spinner = sdk.getComponent("elements.Spinner");
const modal = Modal.createDialog(
Spinner, null, "mx_Dialog_spinner", /* priority */ false, /* static */ true,
);
try {
await accessSecretStorage();
} finally {
modal.close();
}
}
};
ToastStore.sharedInstance().addOrReplaceToast({
key: TOAST_KEY,
title: getTitle(kind),
icon: "verification_warning",
props: {
description: getDescription(kind),
acceptLabel: getSetupCaption(kind),
onAccept,
rejectLabel: _t("Later"),
onReject,
},
component: GenericToast,
priority: kind === Kind.VERIFY_THIS_SESSION ? 95 : 40,
});
};
export const hideToast = () => {
ToastStore.sharedInstance().dismissToast(TOAST_KEY);
};

View File

@ -0,0 +1,70 @@
/*
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 { _t } from '../languageHandler';
import { MatrixClientPeg } from '../MatrixClientPeg';
import Modal from '../Modal';
import DeviceListener from '../DeviceListener';
import NewSessionReviewDialog from '../components/views/dialogs/NewSessionReviewDialog';
import ToastStore from "../stores/ToastStore";
import GenericToast from "../components/views/toasts/GenericToast";
function toastKey(deviceId: string) {
return "unverified_session_" + deviceId;
}
export const showToast = (deviceId: string) => {
const cli = MatrixClientPeg.get();
const onAccept = () => {
Modal.createTrackedDialog('New Session Review', 'Starting dialog', NewSessionReviewDialog, {
userId: cli.getUserId(),
device: cli.getStoredDevice(cli.getUserId(), deviceId),
onFinished: (r) => {
if (!r) {
/* This'll come back false if the user clicks "this wasn't me" and saw a warning dialog */
DeviceListener.sharedInstance().dismissUnverifiedSessions([deviceId]);
}
},
}, null, /* priority = */ false, /* static = */ true);
};
const onReject = () => {
DeviceListener.sharedInstance().dismissUnverifiedSessions([deviceId]);
};
const device = cli.getStoredDevice(cli.getUserId(), deviceId);
ToastStore.sharedInstance().addOrReplaceToast({
key: toastKey(deviceId),
title: _t("New login. Was this you?"),
icon: "verification_warning",
props: {
description: _t(
"Verify the new login accessing your account: %(name)s", { name: device.getDisplayName()}),
acceptLabel: _t("Verify"),
onAccept,
rejectLabel: _t("Later"),
onReject,
},
component: GenericToast,
priority: 80,
});
};
export const hideToast = (deviceId: string) => {
ToastStore.sharedInstance().dismissToast(deviceId);
};