diff --git a/babel.config.js b/babel.config.js index c83be72518..3c0c3fcb85 100644 --- a/babel.config.js +++ b/babel.config.js @@ -2,19 +2,16 @@ module.exports = { "sourceMaps": "inline", "presets": [ ["@babel/preset-env", { - "targets": { - "browsers": [ - "last 2 versions" - ] - }, - "modules": "commonjs" + "targets": [ + "last 2 Chrome versions", "last 2 Firefox versions", "last 2 Safari versions" + ], }], "@babel/preset-typescript", "@babel/preset-flow", "@babel/preset-react" ], "plugins": [ - ["@babel/plugin-proposal-decorators", { "legacy": true }], + ["@babel/plugin-proposal-decorators", {decoratorsBeforeExport: true}], "@babel/plugin-proposal-export-default-from", "@babel/plugin-proposal-numeric-separator", "@babel/plugin-proposal-class-properties", diff --git a/res/css/_components.scss b/res/css/_components.scss index a2cfc94c79..2b513474c9 100644 --- a/res/css/_components.scss +++ b/res/css/_components.scss @@ -66,6 +66,7 @@ @import "./views/dialogs/_IncomingSasDialog.scss"; @import "./views/dialogs/_InviteDialog.scss"; @import "./views/dialogs/_MessageEditHistoryDialog.scss"; +@import "./views/dialogs/_NewSessionReviewDialog.scss"; @import "./views/dialogs/_RoomSettingsDialog.scss"; @import "./views/dialogs/_RoomUpgradeDialog.scss"; @import "./views/dialogs/_RoomUpgradeWarningDialog.scss"; diff --git a/res/css/views/dialogs/_NewSessionReviewDialog.scss b/res/css/views/dialogs/_NewSessionReviewDialog.scss new file mode 100644 index 0000000000..7e35fe941e --- /dev/null +++ b/res/css/views/dialogs/_NewSessionReviewDialog.scss @@ -0,0 +1,37 @@ +/* +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. +*/ + +.mx_NewSessionReviewDialog_header { + display: flex; + align-items: center; + margin-top: 0; +} + +.mx_NewSessionReviewDialog_headerIcon { + width: 24px; + height: 24px; + margin-right: 4px; + position: relative; +} + +.mx_NewSessionReviewDialog_deviceName { + font-weight: 600; +} + +.mx_NewSessionReviewDialog_deviceID { + font-size: 12px; + color: $notice-secondary-color; +} diff --git a/src/Skinner.js b/src/Skinner.js index 3baecc9fb3..87c5a7be7f 100644 --- a/src/Skinner.js +++ b/src/Skinner.js @@ -20,6 +20,7 @@ class Skinner { } getComponent(name) { + if (!name) throw new Error(`Invalid component name: ${name}`); if (this.components === null) { throw new Error( "Attempted to get a component before a skin has been loaded."+ @@ -41,13 +42,7 @@ class Skinner { }; // Check the skin first - let comp = doLookup(this.components); - - // If that failed, check against our own components - if (!comp) { - // Lazily load our own components because they might end up calling .getComponent() - comp = doLookup(require("./component-index").components); - } + const comp = doLookup(this.components); // Just return nothing instead of erroring - the consumer should be smart enough to // handle this at this point. @@ -75,6 +70,13 @@ class Skinner { const comp = skinObject.components[compKeys[i]]; this.addComponent(compKeys[i], comp); } + + // Now that we have a skin, load our components too + const idx = require("./component-index"); + if (!idx || !idx.components) throw new Error("Invalid react-sdk component index"); + for (const c in idx.components) { + if (!this.components[c]) this.components[c] = idx.components[c]; + } } addComponent(name, comp) { diff --git a/src/components/structures/MatrixChat.js b/src/components/structures/MatrixChat.js index 133d74db45..1b4d0e9609 100644 --- a/src/components/structures/MatrixChat.js +++ b/src/components/structures/MatrixChat.js @@ -1834,6 +1834,7 @@ export default createReactClass({ this._accountPassword = null; this._accountPasswordTimer = null; }, 60 * 5 * 1000); + // Wait for the client to be logged in (but not started) // which is enough to ask the server about account data. const loggedIn = new Promise(resolve => { @@ -1867,6 +1868,9 @@ export default createReactClass({ } if (masterKeyInStorage) { + // Auto-enable cross-signing for the new session when key found in + // secret storage. + SettingsStore.setFeatureEnabled("feature_cross_signing", true); this.setStateForNewView({ view: VIEWS.COMPLETE_SECURITY }); } else if (SettingsStore.isFeatureEnabled("feature_cross_signing")) { // This will only work if the feature is set to 'enable' in the config, diff --git a/src/components/structures/RoomView.js b/src/components/structures/RoomView.js index 60fff5f1e3..2d669f9243 100644 --- a/src/components/structures/RoomView.js +++ b/src/components/structures/RoomView.js @@ -811,7 +811,7 @@ export default createReactClass({ debuglog("e2e verified", verified, "unverified", unverified); /* Check all verified user devices. */ - for (const userId of verified) { + for (const userId of [...verified, cli.getUserId()]) { const devices = await cli.getStoredDevicesForUser(userId); const anyDeviceNotVerified = devices.some(({deviceId}) => { return !cli.checkDeviceTrust(userId, deviceId).isVerified(); diff --git a/src/components/views/dialogs/NewSessionReviewDialog.js b/src/components/views/dialogs/NewSessionReviewDialog.js new file mode 100644 index 0000000000..2d2bcc8f35 --- /dev/null +++ b/src/components/views/dialogs/NewSessionReviewDialog.js @@ -0,0 +1,91 @@ +/* +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 Modal from '../../../Modal'; +import { replaceableComponent } from '../../../utils/replaceableComponent'; +import DeviceVerifyDialog from './DeviceVerifyDialog'; +import BaseDialog from './BaseDialog'; +import DialogButtons from '../elements/DialogButtons'; + +@replaceableComponent("views.dialogs.NewSessionReviewDialog") +export default class NewSessionReviewDialog extends React.PureComponent { + static propTypes = { + userId: PropTypes.string.isRequired, + device: PropTypes.object.isRequired, + onFinished: PropTypes.func.isRequired, + } + + onCancelClick = () => { + this.props.onFinished(false); + } + + onContinueClick = () => { + const { userId, device } = this.props; + Modal.createTrackedDialog('New Session Verification', 'Starting dialog', DeviceVerifyDialog, { + userId, + device, + }, null, /* priority = */ false, /* static = */ true); + } + + render() { + const { device } = this.props; + + const icon = ; + const titleText = _t("New session"); + + const title =

+ {icon} + {titleText} +

; + + return ( + +
+

{_t( + "Use this session to verify your new one, " + + "granting it access to encrypted messages:", + )}

+
+
+ + {device.getDisplayName()} + + ({device.deviceId}) + +
+
+

{_t( + "If you didn’t sign in to this session, " + + "your account may be compromised.", + )}

+ +
+
+ ); + } +} diff --git a/src/components/views/elements/DialogButtons.js b/src/components/views/elements/DialogButtons.js index ee15bfc3f2..9223b5ade8 100644 --- a/src/components/views/elements/DialogButtons.js +++ b/src/components/views/elements/DialogButtons.js @@ -83,7 +83,7 @@ export default createReactClass({ // primary in the DOM so will get form submissions unless we make it not a submit. type="button" onClick={this._onCancelClick} - className={this.props.cancelButtonClass} + className={this.props.cancelButtonClass} disabled={this.props.disabled} > { this.props.cancelButton || _t("Cancel") } diff --git a/src/components/views/rooms/RoomTile.js b/src/components/views/rooms/RoomTile.js index 41975fe7b8..41d43476ea 100644 --- a/src/components/views/rooms/RoomTile.js +++ b/src/components/views/rooms/RoomTile.js @@ -166,7 +166,7 @@ export default createReactClass({ }); /* Check all verified user devices. */ - for (const userId of verified) { + for (const userId of [...verified, cli.getUserId()]) { const devices = await cli.getStoredDevicesForUser(userId); const allDevicesVerified = devices.every(({deviceId}) => { return cli.checkDeviceTrust(userId, deviceId).isVerified(); diff --git a/src/components/views/toasts/NewSessionToast.js b/src/components/views/toasts/NewSessionToast.js index 3b60f59131..80564f3494 100644 --- a/src/components/views/toasts/NewSessionToast.js +++ b/src/components/views/toasts/NewSessionToast.js @@ -16,12 +16,15 @@ limitations under the License. import React from 'react'; import PropTypes from 'prop-types'; -import * as sdk from "../../../index"; import { _t } from '../../../languageHandler'; import Modal from "../../../Modal"; import { MatrixClientPeg } from '../../../MatrixClientPeg'; import DeviceListener from '../../../DeviceListener'; +import NewSessionReviewDialog from '../dialogs/NewSessionReviewDialog'; +import FormButton from '../elements/FormButton'; +import { replaceableComponent } from '../../../utils/replaceableComponent'; +@replaceableComponent("views.toasts.VerifySessionToast") export default class VerifySessionToast extends React.PureComponent { static propTypes = { toastKey: PropTypes.string.isRequired, @@ -34,18 +37,16 @@ export default class VerifySessionToast extends React.PureComponent { _onReviewClick = async () => { const cli = MatrixClientPeg.get(); - const DeviceVerifyDialog = sdk.getComponent('views.dialogs.DeviceVerifyDialog'); const device = await cli.getStoredDevice(cli.getUserId(), this.props.deviceId); - Modal.createTrackedDialog('New Session Verify', 'Starting dialog', DeviceVerifyDialog, { + Modal.createTrackedDialog('New Session Review', 'Starting dialog', NewSessionReviewDialog, { userId: MatrixClientPeg.get().getUserId(), device, }, null, /* priority = */ false, /* static = */ true); }; render() { - const FormButton = sdk.getComponent("elements.FormButton"); return (
{_t("Review & verify your new session")}
diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 23ca730d97..7ccce9e7f6 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -1508,6 +1508,10 @@ "Are you sure you want to sign out?": "Are you sure you want to sign out?", "Your homeserver doesn't seem to support this feature.": "Your homeserver doesn't seem to support this feature.", "Message edits": "Message edits", + "New session": "New session", + "Use this session to verify your new one, granting it access to encrypted messages:": "Use this session to verify your new one, granting it access to encrypted messages:", + "If you didn’t sign in to this session, your account may be compromised.": "If you didn’t sign in to this session, your account may be compromised.", + "This wasn't me": "This wasn't me", "If you run into any bugs or have feedback you'd like to share, please let us know on GitHub.": "If you run into any bugs or have feedback you'd like to share, please let us know on GitHub.", "To help avoid duplicate issues, please view existing issues first (and add a +1) or create a new issue if you can't find it.": "To help avoid duplicate issues, please view existing issues first (and add a +1) or create a new issue if you can't find it.", "Report bugs & give feedback": "Report bugs & give feedback", diff --git a/src/utils/replaceableComponent.ts b/src/utils/replaceableComponent.ts index 9f617b27f3..281ff4c1ac 100644 --- a/src/utils/replaceableComponent.ts +++ b/src/utils/replaceableComponent.ts @@ -32,9 +32,13 @@ import * as sdk from '../index'; * with a skinned version. If no skinned version is available, this component * will be used. */ -export function replaceableComponent(name: string, origComponent: React.Component) { +export function replaceableComponent(name: string) { // Decorators return a function to override the class (origComponent). This // ultimately assumes that `getComponent()` won't throw an error and instead // return a falsey value like `null` when the skin doesn't have a component. - return () => sdk.getComponent(name) || origComponent; + return (origComponent) => { + const c = sdk.getComponent(name) || origComponent; + c.kind = "class"; // appeases babel + return c; + }; }