diff --git a/package.json b/package.json index 22df9c37c2..156cbb1bc8 100644 --- a/package.json +++ b/package.json @@ -149,7 +149,6 @@ "eslint-plugin-flowtype": "^2.50.3", "eslint-plugin-react": "^7.20.3", "eslint-plugin-react-hooks": "^2.5.1", - "file-loader": "^3.0.1", "glob": "^5.0.15", "jest": "^24.9.0", "jest-canvas-mock": "^2.2.0", @@ -158,7 +157,6 @@ "matrix-react-test-utils": "^0.2.2", "react-test-renderer": "^16.13.1", "rimraf": "^2.7.1", - "source-map-loader": "^0.2.4", "stylelint": "^9.10.1", "stylelint-config-standard": "^18.3.0", "stylelint-scss": "^3.18.0", diff --git a/res/css/_components.scss b/res/css/_components.scss index 4c83dd7a31..4c253c7d9a 100644 --- a/res/css/_components.scss +++ b/res/css/_components.scss @@ -91,11 +91,12 @@ @import "./views/dialogs/_UploadConfirmDialog.scss"; @import "./views/dialogs/_UserSettingsDialog.scss"; @import "./views/dialogs/_WidgetOpenIDPermissionsDialog.scss"; -@import "./views/dialogs/keybackup/_CreateKeyBackupDialog.scss"; -@import "./views/dialogs/keybackup/_KeyBackupFailedDialog.scss"; -@import "./views/dialogs/keybackup/_RestoreKeyBackupDialog.scss"; -@import "./views/dialogs/secretstorage/_AccessSecretStorageDialog.scss"; -@import "./views/dialogs/secretstorage/_CreateSecretStorageDialog.scss"; +@import "./views/dialogs/security/_AccessSecretStorageDialog.scss"; +@import "./views/dialogs/security/_CreateCrossSigningDialog.scss"; +@import "./views/dialogs/security/_CreateKeyBackupDialog.scss"; +@import "./views/dialogs/security/_CreateSecretStorageDialog.scss"; +@import "./views/dialogs/security/_KeyBackupFailedDialog.scss"; +@import "./views/dialogs/security/_RestoreKeyBackupDialog.scss"; @import "./views/directory/_NetworkDropdown.scss"; @import "./views/elements/_AccessibleButton.scss"; @import "./views/elements/_AddressSelector.scss"; @@ -188,7 +189,6 @@ @import "./views/rooms/_RoomHeader.scss"; @import "./views/rooms/_RoomList.scss"; @import "./views/rooms/_RoomPreviewBar.scss"; -@import "./views/rooms/_RoomRecoveryReminder.scss"; @import "./views/rooms/_RoomSublist.scss"; @import "./views/rooms/_RoomTile.scss"; @import "./views/rooms/_RoomUpgradeWarningBar.scss"; diff --git a/res/css/structures/_ToastContainer.scss b/res/css/structures/_ToastContainer.scss index 544dcbc180..c381668a6a 100644 --- a/res/css/structures/_ToastContainer.scss +++ b/res/css/structures/_ToastContainer.scss @@ -80,6 +80,11 @@ limitations under the License. } } + &.mx_Toast_icon_secure_backup::after { + mask-image: url('$(res)/img/feather-customised/secure-backup.svg'); + background-color: $primary-fg-color; + } + .mx_Toast_title, .mx_Toast_body { grid-column: 2; } diff --git a/res/css/views/auth/_Welcome.scss b/res/css/views/auth/_Welcome.scss index 9043289184..f0e2b3de33 100644 --- a/res/css/views/auth/_Welcome.scss +++ b/res/css/views/auth/_Welcome.scss @@ -18,6 +18,12 @@ limitations under the License. display: flex; flex-direction: column; align-items: center; + + &.mx_WelcomePage_registrationDisabled { + .mx_ButtonCreateAccount { + display: none; + } + } } .mx_Welcome .mx_AuthBody_language { diff --git a/res/css/views/dialogs/_ShareDialog.scss b/res/css/views/dialogs/_ShareDialog.scss index c343b872fd..ce3fdd021f 100644 --- a/res/css/views/dialogs/_ShareDialog.scss +++ b/res/css/views/dialogs/_ShareDialog.scss @@ -71,9 +71,12 @@ limitations under the License. margin-right: 64px; } +.mx_ShareDialog_qrcode_container + .mx_ShareDialog_social_container { + width: 299px; +} + .mx_ShareDialog_social_container { display: inline-block; - width: 299px; } .mx_ShareDialog_social_icon { diff --git a/res/css/views/dialogs/secretstorage/_AccessSecretStorageDialog.scss b/res/css/views/dialogs/security/_AccessSecretStorageDialog.scss similarity index 100% rename from res/css/views/dialogs/secretstorage/_AccessSecretStorageDialog.scss rename to res/css/views/dialogs/security/_AccessSecretStorageDialog.scss diff --git a/res/css/views/dialogs/security/_CreateCrossSigningDialog.scss b/res/css/views/dialogs/security/_CreateCrossSigningDialog.scss new file mode 100644 index 0000000000..8303e02b9e --- /dev/null +++ b/res/css/views/dialogs/security/_CreateCrossSigningDialog.scss @@ -0,0 +1,33 @@ +/* +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_CreateCrossSigningDialog { + // Why you ask? Because CompleteSecurityBody is 600px so this is the width + // we end up when in there, but when in our own dialog we set our own width + // so need to fix it to something sensible as otherwise we'd end up either + // really wide or really narrow depending on the phase. I bet you wish you + // never asked. + width: 560px; + + details .mx_AccessibleButton { + margin: 1em 0; // emulate paragraph spacing because we can't put this button in a paragraph due to HTML rules + } +} + +.mx_CreateCrossSigningDialog .mx_Dialog_title { + /* TODO: Consider setting this for all dialog titles. */ + margin-bottom: 1em; +} diff --git a/res/css/views/dialogs/keybackup/_CreateKeyBackupDialog.scss b/res/css/views/dialogs/security/_CreateKeyBackupDialog.scss similarity index 100% rename from res/css/views/dialogs/keybackup/_CreateKeyBackupDialog.scss rename to res/css/views/dialogs/security/_CreateKeyBackupDialog.scss diff --git a/res/css/views/dialogs/secretstorage/_CreateSecretStorageDialog.scss b/res/css/views/dialogs/security/_CreateSecretStorageDialog.scss similarity index 100% rename from res/css/views/dialogs/secretstorage/_CreateSecretStorageDialog.scss rename to res/css/views/dialogs/security/_CreateSecretStorageDialog.scss diff --git a/res/css/views/dialogs/keybackup/_KeyBackupFailedDialog.scss b/res/css/views/dialogs/security/_KeyBackupFailedDialog.scss similarity index 100% rename from res/css/views/dialogs/keybackup/_KeyBackupFailedDialog.scss rename to res/css/views/dialogs/security/_KeyBackupFailedDialog.scss diff --git a/res/css/views/dialogs/keybackup/_RestoreKeyBackupDialog.scss b/res/css/views/dialogs/security/_RestoreKeyBackupDialog.scss similarity index 100% rename from res/css/views/dialogs/keybackup/_RestoreKeyBackupDialog.scss rename to res/css/views/dialogs/security/_RestoreKeyBackupDialog.scss diff --git a/res/css/views/rooms/_RoomRecoveryReminder.scss b/res/css/views/rooms/_RoomRecoveryReminder.scss deleted file mode 100644 index 09b28ae235..0000000000 --- a/res/css/views/rooms/_RoomRecoveryReminder.scss +++ /dev/null @@ -1,39 +0,0 @@ -/* -Copyright 2018 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. -*/ - -.mx_RoomRecoveryReminder { - display: flex; - flex-direction: column; - text-align: center; - background-color: $room-warning-bg-color; - padding: 20px; - border: 1px solid $primary-hairline-color; - border-bottom: unset; -} - -.mx_RoomRecoveryReminder_header { - font-weight: bold; - margin-bottom: 1em; -} - -.mx_RoomRecoveryReminder_body { - margin-bottom: 1em; -} - -.mx_RoomRecoveryReminder_secondary { - font-size: 90%; - margin-top: 1em; -} diff --git a/res/css/views/settings/_CrossSigningPanel.scss b/res/css/views/settings/_CrossSigningPanel.scss index fa9f76a963..12a0e36835 100644 --- a/res/css/views/settings/_CrossSigningPanel.scss +++ b/res/css/views/settings/_CrossSigningPanel.scss @@ -28,4 +28,8 @@ limitations under the License. .mx_CrossSigningPanel_buttonRow { margin: 1em 0; + + :nth-child(n + 1) { + margin-inline-end: 10px; + } } diff --git a/src/DeviceListener.ts b/src/DeviceListener.ts index aa0508924d..df494e6bdd 100644 --- a/src/DeviceListener.ts +++ b/src/DeviceListener.ts @@ -29,11 +29,10 @@ import { hideToast as hideUnverifiedSessionsToast, showToast as showUnverifiedSessionsToast, } from "./toasts/UnverifiedSessionToast"; -import { privateShouldBeEncrypted } from "./createRoom"; import { isSecretStorageBeingAccessed, accessSecretStorage } from "./SecurityManager"; import { isSecureBackupRequired } from './utils/WellKnownUtils'; import { isLoggedIn } from './components/structures/MatrixChat'; - +import { MatrixEvent } from "matrix-js-sdk/src/models/event"; const KEY_BACKUP_POLL_INTERVAL = 5 * 60 * 1000; @@ -66,6 +65,7 @@ export default class DeviceListener { MatrixClientPeg.get().on('crossSigning.keysChanged', this._onCrossSingingKeysChanged); MatrixClientPeg.get().on('accountData', this._onAccountData); MatrixClientPeg.get().on('sync', this._onSync); + MatrixClientPeg.get().on('RoomState.events', this._onRoomStateEvents); this.dispatcherRef = dis.register(this._onAction); this._recheck(); } @@ -79,6 +79,7 @@ export default class DeviceListener { MatrixClientPeg.get().removeListener('crossSigning.keysChanged', this._onCrossSingingKeysChanged); MatrixClientPeg.get().removeListener('accountData', this._onAccountData); MatrixClientPeg.get().removeListener('sync', this._onSync); + MatrixClientPeg.get().removeListener('RoomState.events', this._onRoomStateEvents); } if (this.dispatcherRef) { dis.unregister(this.dispatcherRef); @@ -169,6 +170,16 @@ export default class DeviceListener { if (state === 'PREPARED' && prevState === null) this._recheck(); }; + _onRoomStateEvents = (ev: MatrixEvent) => { + if (ev.getType() !== "m.room.encryption") { + return; + } + + // If a room changes to encrypted, re-check as it may be our first + // encrypted room. This also catches encrypted room creation as well. + this._recheck(); + }; + _onAction = ({ action }) => { if (action !== "on_logged_in") return; this._recheck(); @@ -189,9 +200,7 @@ export default class DeviceListener { // If we're in the middle of a secret storage operation, we're likely // modifying the state involved here, so don't add new toasts to setup. if (isSecretStorageBeingAccessed()) return false; - // In a default configuration, show the toasts. If the well-known config causes e2ee default to be false - // then do not show the toasts until user is in at least one encrypted room. - if (privateShouldBeEncrypted()) return true; + // Show setup toasts once the user is in at least one encrypted room. const cli = MatrixClientPeg.get(); return cli && cli.getRooms().some(r => cli.isRoomEncrypted(r.roomId)); } @@ -207,8 +216,6 @@ export default class DeviceListener { // (we add a listener on sync to do once check after the initial sync is done) if (!cli.isInitialSyncComplete()) return; - // JRS: This will change again in the next PR which moves secret storage - // later in the process. const crossSigningReady = await cli.isCrossSigningReady(); const secretStorageReady = await cli.isSecretStorageReady(); const allSystemsReady = crossSigningReady && secretStorageReady; diff --git a/src/SecurityManager.js b/src/SecurityManager.js index cc7db3ead7..f6b9c993d0 100644 --- a/src/SecurityManager.js +++ b/src/SecurityManager.js @@ -22,6 +22,8 @@ import { decodeRecoveryKey } from 'matrix-js-sdk/src/crypto/recoverykey'; import { _t } from './languageHandler'; import {encodeBase64} from "matrix-js-sdk/src/crypto/olmlib"; import { isSecureBackupRequired } from './utils/WellKnownUtils'; +import AccessSecretStorageDialog from './components/views/dialogs/security/AccessSecretStorageDialog'; +import RestoreKeyBackupDialog from './components/views/dialogs/security/RestoreKeyBackupDialog'; // This stores the secret storage private keys in memory for the JS SDK. This is // only meant to act as a cache to avoid prompting the user multiple times @@ -87,8 +89,6 @@ async function getSecretStorageKey({ keys: keyInfos }, ssssItemName) { return decodeRecoveryKey(recoveryKey); } }; - const AccessSecretStorageDialog = - sdk.getComponent("dialogs.secretstorage.AccessSecretStorageDialog"); const { finished } = Modal.createTrackedDialog("Access Secret Storage dialog", "", AccessSecretStorageDialog, /* props= */ @@ -181,7 +181,6 @@ export const crossSigningCallbacks = { export async function promptForBackupPassphrase() { let key; - const RestoreKeyBackupDialog = sdk.getComponent('dialogs.keybackup.RestoreKeyBackupDialog'); const { finished } = Modal.createTrackedDialog('Restore Backup', '', RestoreKeyBackupDialog, { showSummary: false, keyCallback: k => key = k, }, null, /* priority = */ false, /* static = */ true); @@ -221,7 +220,7 @@ export async function accessSecretStorage(func = async () => { }, forceReset = f // This dialog calls bootstrap itself after guiding the user through // passphrase creation. const { finished } = Modal.createTrackedDialogAsync('Create Secret Storage dialog', '', - import("./async-components/views/dialogs/secretstorage/CreateSecretStorageDialog"), + import("./async-components/views/dialogs/security/CreateSecretStorageDialog"), { forceReset, }, diff --git a/src/async-components/views/dialogs/keybackup/IgnoreRecoveryReminderDialog.js b/src/async-components/views/dialogs/keybackup/IgnoreRecoveryReminderDialog.js deleted file mode 100644 index b79911c66e..0000000000 --- a/src/async-components/views/dialogs/keybackup/IgnoreRecoveryReminderDialog.js +++ /dev/null @@ -1,70 +0,0 @@ -/* -Copyright 2018 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 PropTypes from "prop-types"; -import * as sdk from "../../../../index"; -import { _t } from "../../../../languageHandler"; - -export default class IgnoreRecoveryReminderDialog extends React.PureComponent { - static propTypes = { - onDontAskAgain: PropTypes.func.isRequired, - onFinished: PropTypes.func.isRequired, - onSetup: PropTypes.func.isRequired, - } - - onDontAskAgainClick = () => { - this.props.onFinished(); - this.props.onDontAskAgain(); - } - - onSetupClick = () => { - this.props.onFinished(); - this.props.onSetup(); - } - - render() { - const BaseDialog = sdk.getComponent("views.dialogs.BaseDialog"); - const DialogButtons = sdk.getComponent("views.elements.DialogButtons"); - - return ( - -
-

{_t( - "Without setting up Secure Message Recovery, " + - "you'll lose your secure message history when you " + - "log out.", - )}

-

{_t( - "If you don't want to set this up now, you can later " + - "in Settings.", - )}

-
- -
-
-
- ); - } -} diff --git a/src/async-components/views/dialogs/keybackup/CreateKeyBackupDialog.js b/src/async-components/views/dialogs/security/CreateKeyBackupDialog.js similarity index 100% rename from src/async-components/views/dialogs/keybackup/CreateKeyBackupDialog.js rename to src/async-components/views/dialogs/security/CreateKeyBackupDialog.js diff --git a/src/async-components/views/dialogs/secretstorage/CreateSecretStorageDialog.js b/src/async-components/views/dialogs/security/CreateSecretStorageDialog.js similarity index 97% rename from src/async-components/views/dialogs/secretstorage/CreateSecretStorageDialog.js rename to src/async-components/views/dialogs/security/CreateSecretStorageDialog.js index d4b1a73c3e..f3b52da141 100644 --- a/src/async-components/views/dialogs/secretstorage/CreateSecretStorageDialog.js +++ b/src/async-components/views/dialogs/security/CreateSecretStorageDialog.js @@ -30,6 +30,7 @@ import StyledRadioButton from '../../../../components/views/elements/StyledRadio import AccessibleButton from "../../../../components/views/elements/AccessibleButton"; import DialogButtons from "../../../../components/views/elements/DialogButtons"; import InlineSpinner from "../../../../components/views/elements/InlineSpinner"; +import RestoreKeyBackupDialog from "../../../../components/views/dialogs/security/RestoreKeyBackupDialog"; import { isSecureBackupRequired } from '../../../../utils/WellKnownUtils'; const PHASE_LOADING = 0; @@ -280,21 +281,21 @@ export default class CreateSecretStorageDialog extends React.PureComponent { const { forceReset } = this.props; try { - // JRS: In an upcoming change, the cross-signing steps will be - // removed from here and this will instead be about secret storage - // only. if (forceReset) { - console.log("Forcing cross-signing and secret storage reset"); + console.log("Forcing secret storage reset"); await cli.bootstrapSecretStorage({ createSecretStorageKey: async () => this._recoveryKey, setupNewKeyBackup: true, setupNewSecretStorage: true, }); - await cli.bootstrapCrossSigning({ - authUploadDeviceSigningKeys: this._doBootstrapUIAuth, - setupNewCrossSigning: true, - }); } else { + // For password authentication users after 2020-09, this cross-signing + // step will be a no-op since it is now setup during registration or login + // when needed. We should keep this here to cover other cases such as: + // * Users with existing sessions prior to 2020-09 changes + // * SSO authentication users which require interactive auth to upload + // keys (and also happen to skip all post-authentication flows at the + // moment via token login) await cli.bootstrapCrossSigning({ authUploadDeviceSigningKeys: this._doBootstrapUIAuth, }); @@ -341,7 +342,6 @@ export default class CreateSecretStorageDialog extends React.PureComponent { // so let's stash it here, rather than prompting for it twice. const keyCallback = k => this._backupKey = k; - const RestoreKeyBackupDialog = sdk.getComponent('dialogs.keybackup.RestoreKeyBackupDialog'); const { finished } = Modal.createTrackedDialog( 'Restore Backup', '', RestoreKeyBackupDialog, { diff --git a/src/async-components/views/dialogs/ExportE2eKeysDialog.js b/src/async-components/views/dialogs/security/ExportE2eKeysDialog.js similarity index 97% rename from src/async-components/views/dialogs/ExportE2eKeysDialog.js rename to src/async-components/views/dialogs/security/ExportE2eKeysDialog.js index 406ffd8749..4dd296a8f1 100644 --- a/src/async-components/views/dialogs/ExportE2eKeysDialog.js +++ b/src/async-components/views/dialogs/security/ExportE2eKeysDialog.js @@ -17,11 +17,11 @@ limitations under the License. import FileSaver from 'file-saver'; import React, {createRef} from 'react'; import PropTypes from 'prop-types'; -import { _t } from '../../../languageHandler'; +import { _t } from '../../../../languageHandler'; import { MatrixClient } from 'matrix-js-sdk'; -import * as MegolmExportEncryption from '../../../utils/MegolmExportEncryption'; -import * as sdk from '../../../index'; +import * as MegolmExportEncryption from '../../../../utils/MegolmExportEncryption'; +import * as sdk from '../../../../index'; const PHASE_EDIT = 1; const PHASE_EXPORTING = 2; diff --git a/src/async-components/views/dialogs/ImportE2eKeysDialog.js b/src/async-components/views/dialogs/security/ImportE2eKeysDialog.js similarity index 97% rename from src/async-components/views/dialogs/ImportE2eKeysDialog.js rename to src/async-components/views/dialogs/security/ImportE2eKeysDialog.js index c2d17f681d..e7bae3578b 100644 --- a/src/async-components/views/dialogs/ImportE2eKeysDialog.js +++ b/src/async-components/views/dialogs/security/ImportE2eKeysDialog.js @@ -18,9 +18,9 @@ import React, {createRef} from 'react'; import PropTypes from 'prop-types'; import { MatrixClient } from 'matrix-js-sdk'; -import * as MegolmExportEncryption from '../../../utils/MegolmExportEncryption'; -import * as sdk from '../../../index'; -import { _t } from '../../../languageHandler'; +import * as MegolmExportEncryption from '../../../../utils/MegolmExportEncryption'; +import * as sdk from '../../../../index'; +import { _t } from '../../../../languageHandler'; function readFileAsArrayBuffer(file) { return new Promise((resolve, reject) => { diff --git a/src/async-components/views/dialogs/keybackup/NewRecoveryMethodDialog.js b/src/async-components/views/dialogs/security/NewRecoveryMethodDialog.js similarity index 97% rename from src/async-components/views/dialogs/keybackup/NewRecoveryMethodDialog.js rename to src/async-components/views/dialogs/security/NewRecoveryMethodDialog.js index 74552a5c08..9f5045635d 100644 --- a/src/async-components/views/dialogs/keybackup/NewRecoveryMethodDialog.js +++ b/src/async-components/views/dialogs/security/NewRecoveryMethodDialog.js @@ -22,6 +22,7 @@ import {MatrixClientPeg} from '../../../../MatrixClientPeg'; import dis from "../../../../dispatcher/dispatcher"; import { _t } from "../../../../languageHandler"; import Modal from "../../../../Modal"; +import RestoreKeyBackupDialog from "../../../../components/views/dialogs/security/RestoreKeyBackupDialog"; import {Action} from "../../../../dispatcher/actions"; export default class NewRecoveryMethodDialog extends React.PureComponent { @@ -41,7 +42,6 @@ export default class NewRecoveryMethodDialog extends React.PureComponent { } onSetupClick = async () => { - const RestoreKeyBackupDialog = sdk.getComponent('dialogs.keybackup.RestoreKeyBackupDialog'); Modal.createTrackedDialog( 'Restore Backup', '', RestoreKeyBackupDialog, { onFinished: this.props.onFinished, diff --git a/src/async-components/views/dialogs/keybackup/RecoveryMethodRemovedDialog.js b/src/async-components/views/dialogs/security/RecoveryMethodRemovedDialog.js similarity index 100% rename from src/async-components/views/dialogs/keybackup/RecoveryMethodRemovedDialog.js rename to src/async-components/views/dialogs/security/RecoveryMethodRemovedDialog.js diff --git a/src/components/structures/MatrixChat.tsx b/src/components/structures/MatrixChat.tsx index 1875d80fa4..ea1f424af6 100644 --- a/src/components/structures/MatrixChat.tsx +++ b/src/components/structures/MatrixChat.tsx @@ -79,6 +79,7 @@ import { SettingLevel } from "../../settings/SettingLevel"; import { leaveRoomBehaviour } from "../../utils/membership"; import CreateCommunityPrototypeDialog from "../views/dialogs/CreateCommunityPrototypeDialog"; import ThreepidInviteStore, { IThreepidInvite, IThreepidInviteWireFormat } from "../../stores/ThreepidInviteStore"; +import {UIFeature} from "../../settings/UIFeature"; /** constants for MatrixChat.state.view */ export enum Views { @@ -1372,15 +1373,19 @@ export default class MatrixChat extends React.PureComponent { ready: true, }); }); - cli.on('Call.incoming', function(call) { - // we dispatch this synchronously to make sure that the event - // handlers on the call are set up immediately (so that if - // we get an immediate hangup, we don't get a stuck call) - dis.dispatch({ - action: 'incoming_call', - call: call, - }, true); - }); + + if (SettingsStore.getValue(UIFeature.Voip)) { + cli.on('Call.incoming', function(call) { + // we dispatch this synchronously to make sure that the event + // handlers on the call are set up immediately (so that if + // we get an immediate hangup, we don't get a stuck call) + dis.dispatch({ + action: 'incoming_call', + call: call, + }, true); + }); + } + cli.on('Session.logged_out', function(errObj) { if (Lifecycle.isLoggingOut()) return; @@ -1496,12 +1501,12 @@ export default class MatrixChat extends React.PureComponent { if (haveNewVersion) { Modal.createTrackedDialogAsync('New Recovery Method', 'New Recovery Method', - import('../../async-components/views/dialogs/keybackup/NewRecoveryMethodDialog'), + import('../../async-components/views/dialogs/security/NewRecoveryMethodDialog'), { newVersionInfo }, ); } else { Modal.createTrackedDialogAsync('Recovery Method Removed', 'Recovery Method Removed', - import('../../async-components/views/dialogs/keybackup/RecoveryMethodRemovedDialog'), + import('../../async-components/views/dialogs/security/RecoveryMethodRemovedDialog'), ); } }); @@ -1876,6 +1881,13 @@ export default class MatrixChat extends React.PureComponent { return this.props.makeRegistrationUrl(params); }; + /** + * After registration or login, we run various post-auth steps before entering the app + * proper, such setting up cross-signing or verifying the new session. + * + * Note: SSO users (and any others using token login) currently do not pass through + * this, as they instead jump straight into the app after `attemptTokenLogin`. + */ onUserCompletedLoginFlow = async (credentials: object, password: string) => { this.accountPassword = password; // self-destruct the password after 5mins @@ -1942,7 +1954,7 @@ export default class MatrixChat extends React.PureComponent { render() { const fragmentAfterLogin = this.getFragmentAfterLogin(); - let view; + let view = null; if (this.state.view === Views.LOADING) { const Spinner = sdk.getComponent('elements.Spinner'); @@ -2021,7 +2033,7 @@ export default class MatrixChat extends React.PureComponent { } else if (this.state.view === Views.WELCOME) { const Welcome = sdk.getComponent('auth.Welcome'); view = ; - } else if (this.state.view === Views.REGISTER) { + } else if (this.state.view === Views.REGISTER && SettingsStore.getValue(UIFeature.Registration)) { const Registration = sdk.getComponent('structures.auth.Registration'); const email = ThreepidInviteStore.instance.pickBestInvite()?.toEmail; view = ( @@ -2039,7 +2051,7 @@ export default class MatrixChat extends React.PureComponent { {...this.getServerProperties()} /> ); - } else if (this.state.view === Views.FORGOT_PASSWORD) { + } else if (this.state.view === Views.FORGOT_PASSWORD && SettingsStore.getValue(UIFeature.PasswordReset)) { const ForgotPassword = sdk.getComponent('structures.auth.ForgotPassword'); view = ( { /> ); } else if (this.state.view === Views.LOGIN) { + const showPasswordReset = SettingsStore.getValue(UIFeature.PasswordReset); const Login = sdk.getComponent('structures.auth.Login'); view = ( { onRegisterClick={this.onRegisterClick} fallbackHsUrl={this.getFallbackHsUrl()} defaultDeviceDisplayName={this.props.defaultDeviceDisplayName} - onForgotPasswordClick={this.onForgotPasswordClick} + onForgotPasswordClick={showPasswordReset ? this.onForgotPasswordClick : undefined} onServerConfigChange={this.onServerConfigChange} fragmentAfterLogin={fragmentAfterLogin} {...this.getServerProperties()} diff --git a/src/components/structures/MessagePanel.js b/src/components/structures/MessagePanel.js index 230d136e04..fe7b20a2d9 100644 --- a/src/components/structures/MessagePanel.js +++ b/src/components/structures/MessagePanel.js @@ -135,6 +135,9 @@ export default class MessagePanel extends React.Component { // whether to use the irc layout useIRCLayout: PropTypes.bool, + + // whether or not to show flair at all + enableFlair: PropTypes.bool, }; // Force props to be loaded for useIRCLayout @@ -579,7 +582,8 @@ export default class MessagePanel extends React.Component { data-scroll-tokens={scrollToken} > - , diff --git a/src/components/structures/RoomView.tsx b/src/components/structures/RoomView.tsx index 3fed7d9e23..c0d76607c0 100644 --- a/src/components/structures/RoomView.tsx +++ b/src/components/structures/RoomView.tsx @@ -65,7 +65,6 @@ import RoomPreviewBar from "../views/rooms/RoomPreviewBar"; import ForwardMessage from "../views/rooms/ForwardMessage"; import SearchBar from "../views/rooms/SearchBar"; import RoomUpgradeWarningBar from "../views/rooms/RoomUpgradeWarningBar"; -import RoomRecoveryReminder from "../views/rooms/RoomRecoveryReminder"; import PinnedEventsPanel from "../views/rooms/PinnedEventsPanel"; import AuxPanel from "../views/rooms/AuxPanel"; import RoomHeader from "../views/rooms/RoomHeader"; @@ -816,12 +815,6 @@ export default class RoomView extends React.Component { } }; - private onRoomRecoveryReminderDontAskAgain = () => { - // Called when the option to not ask again is set: - // force an update to hide the recovery reminder - this.forceUpdate(); - }; - private onKeyBackupStatus = () => { // Key backup status changes affect whether the in-room recovery // reminder is displayed. @@ -1858,13 +1851,6 @@ export default class RoomView extends React.Component { this.state.room.userMayUpgradeRoom(this.context.credentials.userId) ); - const showRoomRecoveryReminder = ( - this.context.isCryptoEnabled() && - SettingsStore.getValue("showRoomRecoveryReminder") && - this.context.isRoomEncrypted(this.state.room.roomId) && - this.context.getKeyBackupEnabled() === false - ); - const hiddenHighlightCount = this.getHiddenHighlightCount(); let aux = null; @@ -1884,9 +1870,6 @@ export default class RoomView extends React.Component { } else if (showRoomUpgradeBar) { aux = ; hideCancel = true; - } else if (showRoomRecoveryReminder) { - aux = ; - hideCancel = true; } else if (this.state.showingPinned) { hideCancel = true; // has own cancel aux = ; diff --git a/src/components/structures/TimelinePanel.js b/src/components/structures/TimelinePanel.js index 97f9ba48ed..8bbc66bf40 100644 --- a/src/components/structures/TimelinePanel.js +++ b/src/components/structures/TimelinePanel.js @@ -35,6 +35,7 @@ import Timer from '../../utils/Timer'; import shouldHideEvent from '../../shouldHideEvent'; import EditorStateTransfer from '../../utils/EditorStateTransfer'; import {haveTileForEvent} from "../views/rooms/EventTile"; +import {UIFeature} from "../../settings/UIFeature"; const PAGINATE_SIZE = 20; const INITIAL_SIZE = 20; @@ -1446,6 +1447,7 @@ class TimelinePanel extends React.Component { editState={this.state.editState} showReactions={this.props.showReactions} useIRCLayout={this.props.useIRCLayout} + enableFlair={SettingsStore.getValue(UIFeature.Flair)} /> ); } diff --git a/src/components/structures/auth/E2eSetup.js b/src/components/structures/auth/E2eSetup.js index 9b390d24cc..6df8158002 100644 --- a/src/components/structures/auth/E2eSetup.js +++ b/src/components/structures/auth/E2eSetup.js @@ -16,8 +16,9 @@ limitations under the License. import React from 'react'; import PropTypes from 'prop-types'; -import AsyncWrapper from '../../../AsyncWrapper'; -import * as sdk from '../../../index'; +import AuthPage from '../../views/auth/AuthPage'; +import CompleteSecurityBody from '../../views/auth/CompleteSecurityBody'; +import CreateCrossSigningDialog from '../../views/dialogs/security/CreateCrossSigningDialog'; export default class E2eSetup extends React.Component { static propTypes = { @@ -25,21 +26,11 @@ export default class E2eSetup extends React.Component { accountPassword: PropTypes.string, }; - constructor() { - super(); - // awkwardly indented because https://github.com/eslint/eslint/issues/11310 - this._createStorageDialogPromise = - import("../../../async-components/views/dialogs/secretstorage/CreateSecretStorageDialog"); - } - render() { - const AuthPage = sdk.getComponent("auth.AuthPage"); - const CompleteSecurityBody = sdk.getComponent("auth.CompleteSecurityBody"); return ( - diff --git a/src/components/structures/auth/Login.js b/src/components/structures/auth/Login.js index a20bf0dd0a..118eed59e3 100644 --- a/src/components/structures/auth/Login.js +++ b/src/components/structures/auth/Login.js @@ -28,6 +28,8 @@ import classNames from "classnames"; import AuthPage from "../../views/auth/AuthPage"; import SSOButton from "../../views/elements/SSOButton"; import PlatformPeg from '../../../PlatformPeg'; +import SettingsStore from "../../../settings/SettingsStore"; +import {UIFeature} from "../../../settings/UIFeature"; // For validating phone numbers without country codes const PHONE_NUMBER_REGEX = /^[0-9()\-\s]*$/; @@ -679,7 +681,7 @@ export default class LoginComponent extends React.Component { {_t("If you've joined lots of rooms, this might take a while")} } ; - } else { + } else if (SettingsStore.getValue(UIFeature.Registration)) { footer = ( { _t('Create account') } diff --git a/src/components/views/auth/Welcome.js b/src/components/views/auth/Welcome.js index 5a30a02490..21032f4f1a 100644 --- a/src/components/views/auth/Welcome.js +++ b/src/components/views/auth/Welcome.js @@ -15,10 +15,14 @@ limitations under the License. */ import React from 'react'; +import classNames from "classnames"; + import * as sdk from '../../../index'; import SdkConfig from '../../../SdkConfig'; import AuthPage from "./AuthPage"; import {_td} from "../../../languageHandler"; +import SettingsStore from "../../../settings/SettingsStore"; +import {UIFeature} from "../../../settings/UIFeature"; // translatable strings for Welcome pages _td("Sign in with SSO"); @@ -39,7 +43,9 @@ export default class Welcome extends React.PureComponent { return ( -
+
this.setState({canChangeEncryption: !isForced})); } _roomCreateOptions() { @@ -68,7 +72,13 @@ export default class CreateRoomDialog extends React.Component { } if (!this.state.isPublic) { - opts.encryption = this.state.isEncrypted; + if (this.state.canChangeEncryption) { + opts.encryption = this.state.isEncrypted; + } else { + // the server should automatically do this for us, but for safety + // we'll demand it too. + opts.encryption = true; + } } if (CommunityPrototypeStore.instance.getSelectedCommunityId()) { @@ -208,7 +218,11 @@ export default class CreateRoomDialog extends React.Component { if (!this.state.isPublic) { let microcopy; if (privateShouldBeEncrypted()) { - microcopy = _t("You can’t disable this later. Bridges & most bots won’t work yet."); + if (this.state.canChangeEncryption) { + microcopy = _t("You can’t disable this later. Bridges & most bots won’t work yet."); + } else { + microcopy = _t("Your server requires encryption to be enabled in private rooms."); + } } else { microcopy = _t("Your server admin has disabled end-to-end encryption by default " + "in private rooms & Direct Messages."); @@ -219,6 +233,7 @@ export default class CreateRoomDialog extends React.Component { onChange={this.onEncryptedChange} value={this.state.isEncrypted} className='mx_CreateRoomDialog_e2eSwitch' // for end-to-end tests + disabled={!this.state.canChangeEncryption} />

{ microcopy }

; diff --git a/src/components/views/dialogs/InviteDialog.js b/src/components/views/dialogs/InviteDialog.js index 80d8f1fc2c..73101056f3 100644 --- a/src/components/views/dialogs/InviteDialog.js +++ b/src/components/views/dialogs/InviteDialog.js @@ -38,6 +38,8 @@ import {Action} from "../../../dispatcher/actions"; import {DefaultTagID} from "../../../stores/room-list/models"; import RoomListStore from "../../../stores/room-list/RoomListStore"; import {CommunityPrototypeStore} from "../../../stores/CommunityPrototypeStore"; +import SettingsStore from "../../../settings/SettingsStore"; +import {UIFeature} from "../../../settings/UIFeature"; // we have a number of types defined from the Matrix spec which can't reasonably be altered here. /* eslint-disable camelcase */ @@ -549,7 +551,7 @@ export default class InviteDialog extends React.PureComponent { if (this.state.filterText.startsWith('@')) { // Assume mxid newMember = new DirectoryMember({user_id: this.state.filterText, display_name: null, avatar_url: null}); - } else { + } else if (SettingsStore.getValue(UIFeature.IdentityServer)) { // Assume email newMember = new ThreepidMember(this.state.filterText); } @@ -734,7 +736,7 @@ export default class InviteDialog extends React.PureComponent { this.setState({tryingIdentityServer: true}); return; } - if (term.indexOf('@') > 0 && Email.looksValid(term)) { + if (term.indexOf('@') > 0 && Email.looksValid(term) && SettingsStore.getValue(UIFeature.IdentityServer)) { // Start off by suggesting the plain email while we try and resolve it // to a real account. this.setState({ @@ -1037,7 +1039,9 @@ export default class InviteDialog extends React.PureComponent { } _renderIdentityServerWarning() { - if (!this.state.tryingIdentityServer || this.state.canUseIdentityServer) { + if (!this.state.tryingIdentityServer || this.state.canUseIdentityServer || + !SettingsStore.getValue(UIFeature.IdentityServer) + ) { return null; } @@ -1086,22 +1090,38 @@ export default class InviteDialog extends React.PureComponent { let buttonText; let goButtonFn; + const identityServersEnabled = SettingsStore.getValue(UIFeature.IdentityServer); + const userId = MatrixClientPeg.get().getUserId(); if (this.props.kind === KIND_DM) { title = _t("Direct Messages"); - helpText = _t( - "Start a conversation with someone using their name, username (like ) or email address.", - {}, - {userId: () => { - return
{userId}; - }}, - ); + + if (identityServersEnabled) { + helpText = _t( + "Start a conversation with someone using their name, username (like ) or email address.", + {}, + {userId: () => { + return ( + {userId} + ); + }}, + ); + } else { + helpText = _t( + "Start a conversation with someone using their name or username (like ).", + {}, + {userId: () => { + return ( + {userId} + ); + }}, + ); + } + if (CommunityPrototypeStore.instance.getSelectedCommunityId()) { const communityName = CommunityPrototypeStore.instance.getSelectedCommunityName(); - helpText = _t( - "Start a conversation with someone using their name, username (like ) or email address. " + - "This won't invite them to %(communityName)s. To invite someone to %(communityName)s, click " + - "here.", + const inviteText = _t("This won't invite them to %(communityName)s. " + + "To invite someone to %(communityName)s, click here", {communityName}, { userId: () => { return ( @@ -1122,21 +1142,40 @@ export default class InviteDialog extends React.PureComponent { }, }, ); + helpText = + { helpText } {inviteText} + ; } buttonText = _t("Go"); goButtonFn = this._startDm; } else { // KIND_INVITE title = _t("Invite to this room"); - helpText = _t( - "Invite someone using their name, username (like ), email address or share this room.", - {}, - { - userId: () => - {userId}, - a: (sub) => - {sub}, - }, - ); + + if (identityServersEnabled) { + helpText = _t( + "Invite someone using their name, username (like ), email address or " + + "share this room.", + {}, + { + userId: () => + {userId}, + a: (sub) => + {sub}, + }, + ); + } else { + helpText = _t( + "Invite someone using their name, username (like ) or share this room.", + {}, + { + userId: () => + {userId}, + a: (sub) => + {sub}, + }, + ); + } + buttonText = _t("Invite"); goButtonFn = this._inviteUsers; } diff --git a/src/components/views/dialogs/LogoutDialog.js b/src/components/views/dialogs/LogoutDialog.js index 930acaa0b8..af36dba2b6 100644 --- a/src/components/views/dialogs/LogoutDialog.js +++ b/src/components/views/dialogs/LogoutDialog.js @@ -20,7 +20,8 @@ import Modal from '../../../Modal'; import * as sdk from '../../../index'; import dis from '../../../dispatcher/dispatcher'; import { _t } from '../../../languageHandler'; -import {MatrixClientPeg} from '../../../MatrixClientPeg'; +import { MatrixClientPeg } from '../../../MatrixClientPeg'; +import RestoreKeyBackupDialog from './security/RestoreKeyBackupDialog'; export default class LogoutDialog extends React.Component { defaultProps = { @@ -73,7 +74,7 @@ export default class LogoutDialog extends React.Component { _onExportE2eKeysClicked() { Modal.createTrackedDialogAsync('Export E2E Keys', '', - import('../../../async-components/views/dialogs/ExportE2eKeysDialog'), + import('../../../async-components/views/dialogs/security/ExportE2eKeysDialog'), { matrixClient: MatrixClientPeg.get(), }, @@ -93,14 +94,13 @@ export default class LogoutDialog extends React.Component { // A key backup exists for this account, but the creating device is not // verified, so restore the backup which will give us the keys from it and // allow us to trust it (ie. upload keys to it) - const RestoreKeyBackupDialog = sdk.getComponent('dialogs.keybackup.RestoreKeyBackupDialog'); Modal.createTrackedDialog( 'Restore Backup', '', RestoreKeyBackupDialog, null, null, /* priority = */ false, /* static = */ true, ); } else { Modal.createTrackedDialogAsync("Key Backup", "Key Backup", - import("../../../async-components/views/dialogs/keybackup/CreateKeyBackupDialog"), + import("../../../async-components/views/dialogs/security/CreateKeyBackupDialog"), null, null, /* priority = */ false, /* static = */ true, ); } diff --git a/src/components/views/dialogs/RoomSettingsDialog.js b/src/components/views/dialogs/RoomSettingsDialog.js index 613708e436..a43b284c42 100644 --- a/src/components/views/dialogs/RoomSettingsDialog.js +++ b/src/components/views/dialogs/RoomSettingsDialog.js @@ -29,6 +29,7 @@ import * as sdk from "../../../index"; import {MatrixClientPeg} from "../../../MatrixClientPeg"; import dis from "../../../dispatcher/dispatcher"; import SettingsStore from "../../../settings/SettingsStore"; +import {UIFeature} from "../../../settings/UIFeature"; export const ROOM_GENERAL_TAB = "ROOM_GENERAL_TAB"; export const ROOM_SECURITY_TAB = "ROOM_SECURITY_TAB"; @@ -96,12 +97,14 @@ export default class RoomSettingsDialog extends React.Component { )); } - tabs.push(new Tab( - ROOM_ADVANCED_TAB, - _td("Advanced"), - "mx_RoomSettingsDialog_warningIcon", - , - )); + if (SettingsStore.getValue(UIFeature.AdvancedSettings)) { + tabs.push(new Tab( + ROOM_ADVANCED_TAB, + _td("Advanced"), + "mx_RoomSettingsDialog_warningIcon", + , + )); + } return tabs; } diff --git a/src/components/views/dialogs/ShareDialog.tsx b/src/components/views/dialogs/ShareDialog.tsx index e849f7efe3..1569977d58 100644 --- a/src/components/views/dialogs/ShareDialog.tsx +++ b/src/components/views/dialogs/ShareDialog.tsx @@ -32,6 +32,8 @@ import {copyPlaintext, selectText} from "../../../utils/strings"; import StyledCheckbox from '../elements/StyledCheckbox'; import AccessibleTooltipButton from '../elements/AccessibleTooltipButton'; import { IDialogProps } from "./IDialogProps"; +import SettingsStore from "../../../settings/SettingsStore"; +import {UIFeature} from "../../../settings/UIFeature"; const socials = [ { @@ -197,6 +199,35 @@ export default class ShareDialog extends React.PureComponent { const matrixToUrl = this.getUrl(); const encodedUrl = encodeURIComponent(matrixToUrl); + const showQrCode = SettingsStore.getValue(UIFeature.ShareQRCode); + const showSocials = SettingsStore.getValue(UIFeature.ShareSocial); + + let qrSocialSection; + if (showQrCode || showSocials) { + qrSocialSection = <> +
+
+ { showQrCode &&
+ +
} + { showSocials &&
+ { socials.map((social) => ( + + {social.name} + + )) } +
} +
+ ; + } + const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog'); return { />
{ checkbox } -
- -
-
- -
-
- { socials.map((social) => ( - - {social.name} - - )) } -
-
+ { qrSocialSection }
; } diff --git a/src/components/views/dialogs/UserSettingsDialog.js b/src/components/views/dialogs/UserSettingsDialog.js index ffde03fe31..7164540aea 100644 --- a/src/components/views/dialogs/UserSettingsDialog.js +++ b/src/components/views/dialogs/UserSettingsDialog.js @@ -32,6 +32,7 @@ import FlairUserSettingsTab from "../settings/tabs/user/FlairUserSettingsTab"; import * as sdk from "../../../index"; import SdkConfig from "../../../SdkConfig"; import MjolnirUserSettingsTab from "../settings/tabs/user/MjolnirUserSettingsTab"; +import {UIFeature} from "../../../settings/UIFeature"; export const USER_GENERAL_TAB = "USER_GENERAL_TAB"; export const USER_APPEARANCE_TAB = "USER_APPEARANCE_TAB"; @@ -86,12 +87,14 @@ export default class UserSettingsDialog extends React.Component { "mx_UserSettingsDialog_appearanceIcon", , )); - tabs.push(new Tab( - USER_FLAIR_TAB, - _td("Flair"), - "mx_UserSettingsDialog_flairIcon", - , - )); + if (SettingsStore.getValue(UIFeature.Flair)) { + tabs.push(new Tab( + USER_FLAIR_TAB, + _td("Flair"), + "mx_UserSettingsDialog_flairIcon", + , + )); + } tabs.push(new Tab( USER_NOTIFICATIONS_TAB, _td("Notifications"), @@ -104,12 +107,16 @@ export default class UserSettingsDialog extends React.Component { "mx_UserSettingsDialog_preferencesIcon", , )); - tabs.push(new Tab( - USER_VOICE_TAB, - _td("Voice & Video"), - "mx_UserSettingsDialog_voiceIcon", - , - )); + + if (SettingsStore.getValue(UIFeature.Voip)) { + tabs.push(new Tab( + USER_VOICE_TAB, + _td("Voice & Video"), + "mx_UserSettingsDialog_voiceIcon", + , + )); + } + tabs.push(new Tab( USER_SECURITY_TAB, _td("Security & Privacy"), diff --git a/src/components/views/dialogs/secretstorage/AccessSecretStorageDialog.js b/src/components/views/dialogs/security/AccessSecretStorageDialog.js similarity index 100% rename from src/components/views/dialogs/secretstorage/AccessSecretStorageDialog.js rename to src/components/views/dialogs/security/AccessSecretStorageDialog.js diff --git a/src/components/views/dialogs/ConfirmDestroyCrossSigningDialog.js b/src/components/views/dialogs/security/ConfirmDestroyCrossSigningDialog.js similarity index 96% rename from src/components/views/dialogs/ConfirmDestroyCrossSigningDialog.js rename to src/components/views/dialogs/security/ConfirmDestroyCrossSigningDialog.js index 9e1980e98d..abc1586205 100644 --- a/src/components/views/dialogs/ConfirmDestroyCrossSigningDialog.js +++ b/src/components/views/dialogs/security/ConfirmDestroyCrossSigningDialog.js @@ -16,8 +16,8 @@ limitations under the License. import React from 'react'; import PropTypes from 'prop-types'; -import {_t} from "../../../languageHandler"; -import * as sdk from "../../../index"; +import {_t} from "../../../../languageHandler"; +import * as sdk from "../../../../index"; export default class ConfirmDestroyCrossSigningDialog extends React.Component { static propTypes = { diff --git a/src/components/views/dialogs/security/CreateCrossSigningDialog.js b/src/components/views/dialogs/security/CreateCrossSigningDialog.js new file mode 100644 index 0000000000..226419e759 --- /dev/null +++ b/src/components/views/dialogs/security/CreateCrossSigningDialog.js @@ -0,0 +1,187 @@ +/* +Copyright 2018, 2019 New Vector Ltd +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. +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 { _t } from '../../../../languageHandler'; +import Modal from '../../../../Modal'; +import { SSOAuthEntry } from '../../auth/InteractiveAuthEntryComponents'; +import DialogButtons from '../../elements/DialogButtons'; +import BaseDialog from '../BaseDialog'; +import Spinner from '../../elements/Spinner'; +import InteractiveAuthDialog from '../InteractiveAuthDialog'; + +/* + * Walks the user through the process of creating a cross-signing keys. In most + * cases, only a spinner is shown, but for more complex auth like SSO, the user + * may need to complete some steps to proceed. + */ +export default class CreateCrossSigningDialog extends React.PureComponent { + static propTypes = { + accountPassword: PropTypes.string, + }; + + constructor(props) { + super(props); + + this.state = { + error: null, + // Does the server offer a UI auth flow with just m.login.password + // for /keys/device_signing/upload? + canUploadKeysWithPasswordOnly: null, + accountPassword: props.accountPassword || "", + }; + + if (this.state.accountPassword) { + // If we have an account password in memory, let's simplify and + // assume it means password auth is also supported for device + // signing key upload as well. This avoids hitting the server to + // test auth flows, which may be slow under high load. + this.state.canUploadKeysWithPasswordOnly = true; + } else { + this._queryKeyUploadAuth(); + } + } + + componentDidMount() { + this._bootstrapCrossSigning(); + } + + async _queryKeyUploadAuth() { + try { + await MatrixClientPeg.get().uploadDeviceSigningKeys(null, {}); + // We should never get here: the server should always require + // UI auth to upload device signing keys. If we do, we upload + // no keys which would be a no-op. + console.log("uploadDeviceSigningKeys unexpectedly succeeded without UI auth!"); + } catch (error) { + if (!error.data || !error.data.flows) { + console.log("uploadDeviceSigningKeys advertised no flows!"); + return; + } + const canUploadKeysWithPasswordOnly = error.data.flows.some(f => { + return f.stages.length === 1 && f.stages[0] === 'm.login.password'; + }); + this.setState({ + canUploadKeysWithPasswordOnly, + }); + } + } + + _doBootstrapUIAuth = async (makeRequest) => { + if (this.state.canUploadKeysWithPasswordOnly && this.state.accountPassword) { + await makeRequest({ + type: 'm.login.password', + identifier: { + type: 'm.id.user', + user: MatrixClientPeg.get().getUserId(), + }, + // TODO: Remove `user` once servers support proper UIA + // See https://github.com/matrix-org/synapse/issues/5665 + user: MatrixClientPeg.get().getUserId(), + password: this.state.accountPassword, + }); + } else { + const dialogAesthetics = { + [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 encryption setup"), + body: _t("Click the button below to confirm setting up encryption."), + continueText: _t("Confirm"), + continueKind: "primary", + }, + }; + + const { finished } = Modal.createTrackedDialog( + 'Cross-signing keys dialog', '', InteractiveAuthDialog, + { + title: _t("Setting up keys"), + matrixClient: MatrixClientPeg.get(), + makeRequest, + aestheticsForStagePhases: { + [SSOAuthEntry.LOGIN_TYPE]: dialogAesthetics, + [SSOAuthEntry.UNSTABLE_LOGIN_TYPE]: dialogAesthetics, + }, + }, + ); + const [confirmed] = await finished; + if (!confirmed) { + throw new Error("Cross-signing key upload auth canceled"); + } + } + } + + _bootstrapCrossSigning = async () => { + this.setState({ + error: null, + }); + + const cli = MatrixClientPeg.get(); + + try { + await cli.bootstrapCrossSigning({ + authUploadDeviceSigningKeys: this._doBootstrapUIAuth, + }); + this.props.onFinished(true); + } catch (e) { + this.setState({ error: e }); + console.error("Error bootstrapping cross-signing", e); + } + } + + _onCancel = () => { + this.props.onFinished(false); + } + + render() { + let content; + if (this.state.error) { + content =
+

{_t("Unable to set up keys")}

+
+ +
+
; + } else { + content =
+ +
; + } + + return ( + +
+ {content} +
+
+ ); + } +} diff --git a/src/components/views/dialogs/keybackup/RestoreKeyBackupDialog.js b/src/components/views/dialogs/security/RestoreKeyBackupDialog.js similarity index 100% rename from src/components/views/dialogs/keybackup/RestoreKeyBackupDialog.js rename to src/components/views/dialogs/security/RestoreKeyBackupDialog.js diff --git a/src/components/views/dialogs/SetupEncryptionDialog.js b/src/components/views/dialogs/security/SetupEncryptionDialog.js similarity index 80% rename from src/components/views/dialogs/SetupEncryptionDialog.js rename to src/components/views/dialogs/security/SetupEncryptionDialog.js index d7723de588..9ce3144534 100644 --- a/src/components/views/dialogs/SetupEncryptionDialog.js +++ b/src/components/views/dialogs/security/SetupEncryptionDialog.js @@ -16,16 +16,16 @@ 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'; +import SetupEncryptionBody from '../../../structures/auth/SetupEncryptionBody'; +import BaseDialog from '../BaseDialog'; +import { _t } from '../../../../languageHandler'; +import { SetupEncryptionStore, PHASE_DONE } from '../../../../stores/SetupEncryptionStore'; function iconFromPhase(phase) { if (phase === PHASE_DONE) { - return require("../../../../res/img/e2e/verified.svg"); + return require("../../../../../res/img/e2e/verified.svg"); } else { - return require("../../../../res/img/e2e/warning.svg"); + return require("../../../../../res/img/e2e/warning.svg"); } } diff --git a/src/components/views/elements/EventTilePreview.tsx b/src/components/views/elements/EventTilePreview.tsx index 61e5f5381d..35019a901e 100644 --- a/src/components/views/elements/EventTilePreview.tsx +++ b/src/components/views/elements/EventTilePreview.tsx @@ -21,6 +21,8 @@ import { MatrixEvent } from 'matrix-js-sdk/src/models/event'; import * as Avatar from '../../../Avatar'; import { MatrixClientPeg } from '../../../MatrixClientPeg'; import EventTile from '../rooms/EventTile'; +import SettingsStore from "../../../settings/SettingsStore"; +import {UIFeature} from "../../../settings/UIFeature"; interface IProps { /** @@ -121,7 +123,11 @@ export default class EventTilePreview extends React.Component { }); return
- +
; } } diff --git a/src/components/views/elements/ReplyThread.js b/src/components/views/elements/ReplyThread.js index 70592c72c5..2d17c858a2 100644 --- a/src/components/views/elements/ReplyThread.js +++ b/src/components/views/elements/ReplyThread.js @@ -28,6 +28,7 @@ import escapeHtml from "escape-html"; import MatrixClientContext from "../../../contexts/MatrixClientContext"; import {Action} from "../../../dispatcher/actions"; import sanitizeHtml from "sanitize-html"; +import {UIFeature} from "../../../settings/UIFeature"; // This component does no cycle detection, simply because the only way to make such a cycle would be to // craft event_id's, using a homeserver that generates predictable event IDs; even then the impact would @@ -366,6 +367,7 @@ export default class ReplyThread extends React.Component { isRedacted={ev.isRedacted()} isTwelveHour={SettingsStore.getValue("showTwelveHourTimestamps")} useIRCLayout={this.props.useIRCLayout} + enableFlair={SettingsStore.getValue(UIFeature.Flair)} /> ; }); diff --git a/src/components/views/rooms/EventTile.js b/src/components/views/rooms/EventTile.js index ab9f240f2d..f444fb1f1a 100644 --- a/src/components/views/rooms/EventTile.js +++ b/src/components/views/rooms/EventTile.js @@ -206,6 +206,9 @@ export default class EventTile extends React.Component { // whether to use the irc layout useIRCLayout: PropTypes.bool, + + // whether or not to show flair at all + enableFlair: PropTypes.bool, }; static defaultProps = { @@ -736,10 +739,10 @@ export default class EventTile extends React.Component { else if (msgtype === 'm.file') text = _td('%(senderName)s uploaded a file'); sender = ; } else { - sender = ; + sender = ; } } diff --git a/src/components/views/rooms/ReplyPreview.js b/src/components/views/rooms/ReplyPreview.js index de70338245..c7872d95ed 100644 --- a/src/components/views/rooms/ReplyPreview.js +++ b/src/components/views/rooms/ReplyPreview.js @@ -22,6 +22,7 @@ import RoomViewStore from '../../../stores/RoomViewStore'; import SettingsStore from "../../../settings/SettingsStore"; import PropTypes from "prop-types"; import {RoomPermalinkCreator} from "../../../utils/permalinks/Permalinks"; +import {UIFeature} from "../../../settings/UIFeature"; function cancelQuoting() { dis.dispatch({ @@ -80,11 +81,14 @@ export default class ReplyPreview extends React.Component { onClick={cancelQuoting} />
- +
; } diff --git a/src/components/views/rooms/RoomRecoveryReminder.js b/src/components/views/rooms/RoomRecoveryReminder.js deleted file mode 100644 index 859df6dd1b..0000000000 --- a/src/components/views/rooms/RoomRecoveryReminder.js +++ /dev/null @@ -1,170 +0,0 @@ -/* -Copyright 2018, 2019 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. -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 * as sdk from "../../../index"; -import { _t } from "../../../languageHandler"; -import Modal from "../../../Modal"; -import {MatrixClientPeg} from "../../../MatrixClientPeg"; -import SettingsStore from "../../../settings/SettingsStore"; -import {SettingLevel} from "../../../settings/SettingLevel"; - -export default class RoomRecoveryReminder extends React.PureComponent { - static propTypes = { - // called if the user sets the option to suppress this reminder in the future - onDontAskAgainSet: PropTypes.func, - } - - static defaultProps = { - onDontAskAgainSet: function() {}, - } - - constructor(props) { - super(props); - - this.state = { - loading: true, - error: null, - backupInfo: null, - notNowClicked: false, - }; - } - - componentDidMount() { - this._loadBackupStatus(); - } - - async _loadBackupStatus() { - try { - const backupInfo = await MatrixClientPeg.get().getKeyBackupVersion(); - this.setState({ - loading: false, - backupInfo, - }); - } catch (e) { - console.log("Unable to fetch key backup status", e); - this.setState({ - loading: false, - error: e, - }); - } - } - - showSetupDialog = () => { - if (this.state.backupInfo) { - // A key backup exists for this account, but the creating device is not - // verified, so restore the backup which will give us the keys from it and - // allow us to trust it (ie. upload keys to it) - const RestoreKeyBackupDialog = sdk.getComponent('dialogs.keybackup.RestoreKeyBackupDialog'); - Modal.createTrackedDialog( - 'Restore Backup', '', RestoreKeyBackupDialog, null, null, - /* priority = */ false, /* static = */ true, - ); - } else { - Modal.createTrackedDialogAsync("Key Backup", "Key Backup", - import("../../../async-components/views/dialogs/keybackup/CreateKeyBackupDialog"), - null, null, /* priority = */ false, /* static = */ true, - ); - } - } - - onOnNotNowClick = () => { - this.setState({notNowClicked: true}); - } - - onDontAskAgainClick = () => { - // When you choose "Don't ask again" from the room reminder, we show a - // dialog to confirm the choice. - Modal.createTrackedDialogAsync("Ignore Recovery Reminder", "Ignore Recovery Reminder", - import("../../../async-components/views/dialogs/keybackup/IgnoreRecoveryReminderDialog"), - { - onDontAskAgain: async () => { - await SettingsStore.setValue( - "showRoomRecoveryReminder", - null, - SettingLevel.ACCOUNT, - false, - ); - this.props.onDontAskAgainSet(); - }, - onSetup: () => { - this.showSetupDialog(); - }, - }, - ); - } - - onSetupClick = () => { - this.showSetupDialog(); - } - - render() { - // If there was an error loading just don't display the banner: we'll try again - // next time the user switchs to the room. - if (this.state.error || this.state.loading || this.state.notNowClicked) { - return null; - } - - const AccessibleButton = sdk.getComponent("views.elements.AccessibleButton"); - - let setupCaption; - if (this.state.backupInfo) { - setupCaption = _t("Connect this session to Key Backup"); - } else { - setupCaption = _t("Start using Key Backup"); - } - - return ( -
-
{_t( - "Never lose encrypted messages", - )}
-
-

{_t( - "Messages in this room are secured with end-to-end " + - "encryption. Only you and the recipient(s) have the " + - "keys to read these messages.", - )}

-

{_t( - "Securely back up your keys to avoid losing them. " + - "Learn more.", {}, - { - // TODO: We don't have this link yet: this will prevent the translators - // having to re-translate the string when we do. - a: sub => '', - }, - )}

-
-
- - {setupCaption} - - - { _t("Not now") } - - - { _t("Don't ask me again") } - -
-
- ); - } -} diff --git a/src/components/views/rooms/SearchResultTile.js b/src/components/views/rooms/SearchResultTile.js index 136bd23729..8b2a9c2d61 100644 --- a/src/components/views/rooms/SearchResultTile.js +++ b/src/components/views/rooms/SearchResultTile.js @@ -19,6 +19,8 @@ import React from 'react'; import PropTypes from 'prop-types'; import * as sdk from '../../../index'; import {haveTileForEvent} from "./EventTile"; +import SettingsStore from "../../../settings/SettingsStore"; +import {UIFeature} from "../../../settings/UIFeature"; export default class SearchResultTile extends React.Component { static propTypes = { @@ -45,18 +47,27 @@ export default class SearchResultTile extends React.Component { const ret = []; const timeline = result.context.getTimeline(); - for (var j = 0; j < timeline.length; j++) { + for (let j = 0; j < timeline.length; j++) { const ev = timeline[j]; - var highlights; + let highlights; const contextual = (j != result.context.getOurEventIndex()); if (!contextual) { highlights = this.props.searchHighlights; } if (haveTileForEvent(ev)) { - ret.push(); + ret.push(( + + )); } } return ( diff --git a/src/components/views/settings/ChangePassword.js b/src/components/views/settings/ChangePassword.js index 725f04dede..0b62f1fa81 100644 --- a/src/components/views/settings/ChangePassword.js +++ b/src/components/views/settings/ChangePassword.js @@ -184,7 +184,7 @@ export default class ChangePassword extends React.Component { _onExportE2eKeysClicked = () => { Modal.createTrackedDialogAsync('Export E2E Keys', 'Change Password', - import('../../../async-components/views/dialogs/ExportE2eKeysDialog'), + import('../../../async-components/views/dialogs/security/ExportE2eKeysDialog'), { matrixClient: MatrixClientPeg.get(), }, diff --git a/src/components/views/settings/CrossSigningPanel.js b/src/components/views/settings/CrossSigningPanel.js index a0ca84645f..1c548bd9d8 100644 --- a/src/components/views/settings/CrossSigningPanel.js +++ b/src/components/views/settings/CrossSigningPanel.js @@ -22,6 +22,7 @@ import * as sdk from '../../../index'; import Modal from '../../../Modal'; import Spinner from '../elements/Spinner'; import InteractiveAuthDialog from '../dialogs/InteractiveAuthDialog'; +import ConfirmDestroyCrossSigningDialog from '../dialogs/security/ConfirmDestroyCrossSigningDialog'; export default class CrossSigningPanel extends React.PureComponent { constructor(props) { @@ -137,7 +138,6 @@ export default class CrossSigningPanel extends React.PureComponent { } _resetCrossSigning = () => { - const ConfirmDestroyCrossSigningDialog = sdk.getComponent("dialogs.ConfirmDestroyCrossSigningDialog"); Modal.createDialog(ConfirmDestroyCrossSigningDialog, { onFinished: (act) => { if (!act) return; @@ -187,37 +187,46 @@ export default class CrossSigningPanel extends React.PureComponent { } const keysExistAnywhere = ( + crossSigningPublicKeysOnDevice || crossSigningPrivateKeysInStorage || - crossSigningPublicKeysOnDevice + masterPrivateKeyCached || + selfSigningPrivateKeyCached || + userSigningPrivateKeyCached ); const keysExistEverywhere = ( + crossSigningPublicKeysOnDevice && crossSigningPrivateKeysInStorage && - crossSigningPublicKeysOnDevice + masterPrivateKeyCached && + selfSigningPrivateKeyCached && + userSigningPrivateKeyCached ); - let resetButton; - if (keysExistAnywhere) { - resetButton = ( -
- - {_t("Reset")} - -
+ const actions = []; + + // TODO: determine how better to expose this to users in addition to prompts at login/toast + if (!keysExistEverywhere && homeserverSupportsCrossSigning) { + actions.push( + + {_t("Set up")} + , ); } - // TODO: determine how better to expose this to users in addition to prompts at login/toast - let bootstrapButton; - if (!keysExistEverywhere && homeserverSupportsCrossSigning) { - bootstrapButton = ( -
- - {_t("Set up")} - -
+ if (keysExistAnywhere) { + actions.push( + + {_t("Reset")} + , ); } + let actionRow; + if (actions.length) { + actionRow =
+ {actions} +
; + } + return (
{summarisedStatus} @@ -230,7 +239,7 @@ export default class CrossSigningPanel extends React.PureComponent { {_t("Cross-signing private keys:")} - {crossSigningPrivateKeysInStorage ? _t("in secret storage") : _t("not found")} + {crossSigningPrivateKeysInStorage ? _t("in secret storage") : _t("not found in storage")} {_t("Master private key:")} @@ -251,8 +260,7 @@ export default class CrossSigningPanel extends React.PureComponent { {errorSection} - {bootstrapButton} - {resetButton} + {actionRow}
); } diff --git a/src/components/views/settings/SecureBackupPanel.js b/src/components/views/settings/SecureBackupPanel.js index 7f0655d54a..3547efc3f2 100644 --- a/src/components/views/settings/SecureBackupPanel.js +++ b/src/components/views/settings/SecureBackupPanel.js @@ -24,7 +24,7 @@ import { isSecureBackupRequired } from '../../../utils/WellKnownUtils'; import Spinner from '../elements/Spinner'; import AccessibleButton from '../elements/AccessibleButton'; import QuestionDialog from '../dialogs/QuestionDialog'; -import RestoreKeyBackupDialog from '../dialogs/keybackup/RestoreKeyBackupDialog'; +import RestoreKeyBackupDialog from '../dialogs/security/RestoreKeyBackupDialog'; import { accessSecretStorage } from '../../../SecurityManager'; export default class SecureBackupPanel extends React.PureComponent { @@ -131,7 +131,7 @@ export default class SecureBackupPanel extends React.PureComponent { const cli = MatrixClientPeg.get(); const secretStorage = cli._crypto._secretStorage; - const backupKeyStored = await cli.isKeyBackupKeyStored(); + const backupKeyStored = !!(await cli.isKeyBackupKeyStored()); const backupKeyFromCache = await cli._crypto.getSessionBackupPrivateKey(); const backupKeyCached = !!(backupKeyFromCache); const backupKeyWellFormed = backupKeyFromCache instanceof Uint8Array; @@ -150,7 +150,7 @@ export default class SecureBackupPanel extends React.PureComponent { _startNewBackup = () => { Modal.createTrackedDialogAsync('Key Backup', 'Key Backup', - import('../../../async-components/views/dialogs/keybackup/CreateKeyBackupDialog'), + import('../../../async-components/views/dialogs/security/CreateKeyBackupDialog'), { onFinished: () => { this._loadBackupStatus(); @@ -367,14 +367,14 @@ export default class SecureBackupPanel extends React.PureComponent { ; actions.push( - + {restoreButtonCaption} , ); if (!isSecureBackupRequired()) { actions.push( - + {_t("Delete Backup")} , ); @@ -388,7 +388,7 @@ export default class SecureBackupPanel extends React.PureComponent {

{_t("Back up your keys before signing out to avoid losing them.")}

; actions.push( - + {_t("Set up")} , ); @@ -396,7 +396,7 @@ export default class SecureBackupPanel extends React.PureComponent { if (secretStorageKeyInAccount) { actions.push( - + {_t("Reset")} , ); diff --git a/src/components/views/settings/tabs/room/GeneralRoomSettingsTab.js b/src/components/views/settings/tabs/room/GeneralRoomSettingsTab.js index 90eb60e632..9b8004d9d6 100644 --- a/src/components/views/settings/tabs/room/GeneralRoomSettingsTab.js +++ b/src/components/views/settings/tabs/room/GeneralRoomSettingsTab.js @@ -73,6 +73,18 @@ export default class GeneralRoomSettingsTab extends React.Component { urlPreviewSettings = null; } + let flairSection; + if (SettingsStore.getValue(UIFeature.Flair)) { + flairSection = <> + {_t("Flair")} +
+ +
+ ; + } + return (
{_t("General")}
@@ -87,14 +99,8 @@ export default class GeneralRoomSettingsTab extends React.Component { canonicalAliasEvent={canonicalAliasEv} aliasEvents={aliasEvents} />
{_t("Other")}
- {_t("Flair")} -
- -
- - {urlPreviewSettings} + { flairSection } + { urlPreviewSettings } {_t("Leave room")}
diff --git a/src/components/views/settings/tabs/user/AppearanceUserSettingsTab.tsx b/src/components/views/settings/tabs/user/AppearanceUserSettingsTab.tsx index b4c05a2ecb..9f9acd8e3c 100644 --- a/src/components/views/settings/tabs/user/AppearanceUserSettingsTab.tsx +++ b/src/components/views/settings/tabs/user/AppearanceUserSettingsTab.tsx @@ -36,6 +36,7 @@ import EventTilePreview from '../../../elements/EventTilePreview'; import StyledRadioGroup from "../../../elements/StyledRadioGroup"; import classNames from 'classnames'; import { SettingLevel } from "../../../../../settings/SettingLevel"; +import {UIFeature} from "../../../../../settings/UIFeature"; interface IProps { } @@ -386,6 +387,8 @@ export default class AppearanceUserSettingsTab extends React.Component : : null; + let accountManagementSection; + if (SettingsStore.getValue(UIFeature.Deactivate)) { + accountManagementSection = <> +
{_t("Deactivate account")}
+ {this._renderManagementSection()} + ; + } + + let discoverySection; + if (SettingsStore.getValue(UIFeature.IdentityServer)) { + discoverySection = <> +
{discoWarning} {_t("Discovery")}
+ {this._renderDiscoverySection()} + ; + } + return (
{_t("General")}
{this._renderProfileSection()} {this._renderAccountSection()} {this._renderLanguageSection()} -
{discoWarning} {_t("Discovery")}
- {this._renderDiscoverySection()} + { discoverySection } {this._renderIntegrationManagerSection() /* Has its own title */} -
{_t("Deactivate account")}
- {this._renderManagementSection()} + { accountManagementSection }
); } diff --git a/src/components/views/settings/tabs/user/PreferencesUserSettingsTab.js b/src/components/views/settings/tabs/user/PreferencesUserSettingsTab.js index d5dafe146a..347cfccd56 100644 --- a/src/components/views/settings/tabs/user/PreferencesUserSettingsTab.js +++ b/src/components/views/settings/tabs/user/PreferencesUserSettingsTab.js @@ -50,10 +50,10 @@ export default class PreferencesUserSettingsTab extends React.Component { 'showAvatarChanges', 'showDisplaynameChanges', 'showImages', + 'Pill.shouldShowPillAvatar', ]; - static ADVANCED_SETTINGS = [ - 'Pill.shouldShowPillAvatar', + static GENERAL_SETTINGS = [ 'TagPanel.enableTagPanel', 'promptBeforeInviteUnknownUsers', // Start automatically after startup (electron-only) @@ -191,8 +191,8 @@ export default class PreferencesUserSettingsTab extends React.Component {
- {_t("Advanced")} - {this._renderGroup(PreferencesUserSettingsTab.ADVANCED_SETTINGS)} + {_t("General")} + {this._renderGroup(PreferencesUserSettingsTab.GENERAL_SETTINGS)} {minimizeToTrayOption} {autoHideMenuOption} {autoLaunchOption} diff --git a/src/components/views/settings/tabs/user/SecurityUserSettingsTab.js b/src/components/views/settings/tabs/user/SecurityUserSettingsTab.js index 0c49f108d6..9984baeb13 100644 --- a/src/components/views/settings/tabs/user/SecurityUserSettingsTab.js +++ b/src/components/views/settings/tabs/user/SecurityUserSettingsTab.js @@ -30,6 +30,8 @@ import dis from "../../../../../dispatcher/dispatcher"; import {privateShouldBeEncrypted} from "../../../../../createRoom"; import {SettingLevel} from "../../../../../settings/SettingLevel"; import SecureBackupPanel from "../../SecureBackupPanel"; +import SettingsStore from "../../../../../settings/SettingsStore"; +import {UIFeature} from "../../../../../settings/UIFeature"; export class IgnoredUser extends React.Component { static propTypes = { @@ -103,14 +105,14 @@ export default class SecurityUserSettingsTab extends React.Component { _onExportE2eKeysClicked = () => { Modal.createTrackedDialogAsync('Export E2E Keys', '', - import('../../../../../async-components/views/dialogs/ExportE2eKeysDialog'), + import('../../../../../async-components/views/dialogs/security/ExportE2eKeysDialog'), {matrixClient: MatrixClientPeg.get()}, ); }; _onImportE2eKeysClicked = () => { Modal.createTrackedDialogAsync('Import E2E Keys', '', - import('../../../../../async-components/views/dialogs/ImportE2eKeysDialog'), + import('../../../../../async-components/views/dialogs/security/ImportE2eKeysDialog'), {matrixClient: MatrixClientPeg.get()}, ); }; @@ -311,15 +313,13 @@ export default class SecurityUserSettingsTab extends React.Component { // can remove this. const CrossSigningPanel = sdk.getComponent('views.settings.CrossSigningPanel'); const crossSigning = ( -
- {_t("Cross-signing")} -
- -
+
+ {_t("Cross-signing")} +
+
- ); - - const E2eAdvancedPanel = sdk.getComponent('views.settings.E2eAdvancedPanel'); +
+ ); let warning; if (!privateShouldBeEncrypted()) { @@ -352,6 +352,19 @@ export default class SecurityUserSettingsTab extends React.Component { ; } + const E2eAdvancedPanel = sdk.getComponent('views.settings.E2eAdvancedPanel'); + let advancedSection; + if (SettingsStore.getValue(UIFeature.AdvancedSettings)) { + advancedSection = <> +
{_t("Advanced")}
+
+ {this._renderIgnoredUsers()} + {this._renderManageInvites()} + +
+ ; + } + return (
{warning} @@ -381,12 +394,7 @@ export default class SecurityUserSettingsTab extends React.Component { {this._renderCurrentDeviceInfo()}
{ privacySection } -
{_t("Advanced")}
-
- {this._renderIgnoredUsers()} - {this._renderManageInvites()} - -
+ { advancedSection }
); } diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index d4053f4418..024809f214 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -411,13 +411,12 @@ "Set password": "Set password", "To return to your account in future you need to set a password": "To return to your account in future you need to set a password", "Set Password": "Set Password", - "Set up encryption": "Set up encryption", + "Set up Secure Backup": "Set up Secure Backup", "Encryption upgrade available": "Encryption upgrade available", "Verify this session": "Verify this session", - "Set up": "Set up", "Upgrade": "Upgrade", "Verify": "Verify", - "Verify yourself & others to keep your chats safe": "Verify yourself & others to keep your chats safe", + "Safeguard against losing access to encrypted messages & data": "Safeguard against losing access to encrypted messages & data", "Other users may not trust it": "Other users may not trust it", "New login. Was this you?": "New login. Was this you?", "Verify the new login accessing your account: %(name)s": "Verify the new login accessing your account: %(name)s", @@ -474,7 +473,6 @@ "Show timestamps in 12 hour format (e.g. 2:30pm)": "Show timestamps in 12 hour format (e.g. 2:30pm)", "Always show message timestamps": "Always show message timestamps", "Autoplay GIFs and videos": "Autoplay GIFs and videos", - "Show a reminder to enable Secure Message Recovery in encrypted rooms": "Show a reminder to enable Secure Message Recovery in encrypted rooms", "Enable automatic language detection for syntax highlighting": "Enable automatic language detection for syntax highlighting", "Show avatars in user and room mentions": "Show avatars in user and room mentions", "Enable big emoji in chat": "Enable big emoji in chat", @@ -652,12 +650,14 @@ "Cross-signing is ready for use.": "Cross-signing is ready for use.", "Your account has a cross-signing identity in secret storage, but it is not yet trusted by this session.": "Your account has a cross-signing identity in secret storage, but it is not yet trusted by this session.", "Cross-signing is not set up.": "Cross-signing is not set up.", + "Set up": "Set up", "Reset": "Reset", "Cross-signing public keys:": "Cross-signing public keys:", "in memory": "in memory", "not found": "not found", "Cross-signing private keys:": "Cross-signing private keys:", "in secret storage": "in secret storage", + "not found in storage": "not found in storage", "Master private key:": "Master private key:", "cached locally": "cached locally", "not found locally": "not found locally", @@ -832,9 +832,9 @@ "Account management": "Account management", "Deactivating your account is a permanent action - be careful!": "Deactivating your account is a permanent action - be careful!", "Deactivate Account": "Deactivate Account", - "General": "General", - "Discovery": "Discovery", "Deactivate account": "Deactivate account", + "Discovery": "Discovery", + "General": "General", "Legal": "Legal", "Credits": "Credits", "For help with using %(brand)s, click here.": "For help with using %(brand)s, click here.", @@ -1171,12 +1171,6 @@ "%(roomName)s is not accessible at this time.": "%(roomName)s is not accessible at this time.", "Try again later, or ask a room admin to check if you have access.": "Try again later, or ask a room admin to check if you have access.", "%(errcode)s was returned while trying to access the room. If you think you're seeing this message in error, please submit a bug report.": "%(errcode)s was returned while trying to access the room. If you think you're seeing this message in error, please submit a bug report.", - "Start using Key Backup": "Start using Key Backup", - "Never lose encrypted messages": "Never lose encrypted messages", - "Messages in this room are secured with end-to-end encryption. Only you and the recipient(s) have the keys to read these messages.": "Messages in this room are secured with end-to-end encryption. Only you and the recipient(s) have the keys to read these messages.", - "Securely back up your keys to avoid losing them. Learn more.": "Securely back up your keys to avoid losing them. Learn more.", - "Not now": "Not now", - "Don't ask me again": "Don't ask me again", "Appearance": "Appearance", "Show rooms with unread messages first": "Show rooms with unread messages first", "Show previews of messages": "Show previews of messages", @@ -1634,9 +1628,6 @@ "Invite people to join %(communityName)s": "Invite people to join %(communityName)s", "You cannot delete this message. (%(code)s)": "You cannot delete this message. (%(code)s)", "Removing…": "Removing…", - "Destroy cross-signing keys?": "Destroy cross-signing keys?", - "Deleting cross-signing keys is permanent. Anyone you have verified with will see security alerts. You almost certainly don't want to do this, unless you've lost every device you can cross-sign from.": "Deleting cross-signing keys is permanent. Anyone you have verified with will see security alerts. You almost certainly don't want to do this, unless you've lost every device you can cross-sign from.", - "Clear cross-signing keys": "Clear cross-signing keys", "Confirm Removal": "Confirm Removal", "Are you sure you wish to remove (delete) this event? Note that if you delete a room name or topic change, it could undo the change.": "Are you sure you wish to remove (delete) this event? Note that if you delete a room name or topic change, it could undo the change.", "Clear all data in this session?": "Clear all data in this session?", @@ -1663,6 +1654,7 @@ "Private rooms can be found and joined by invitation only. Public rooms can be found and joined by anyone.": "Private rooms can be found and joined by invitation only. Public rooms can be found and joined by anyone.", "Private rooms can be found and joined by invitation only. Public rooms can be found and joined by anyone in this community.": "Private rooms can be found and joined by invitation only. Public rooms can be found and joined by anyone in this community.", "You can’t disable this later. Bridges & most bots won’t work yet.": "You can’t disable this later. Bridges & most bots won’t work yet.", + "Your server requires encryption to be enabled in private rooms.": "Your server requires encryption to be enabled in private rooms.", "Enable end-to-end encryption": "Enable end-to-end encryption", "You might enable this if the room will only be used for collaborating with internal teams on your homeserver. This cannot be changed later.": "You might enable this if the room will only be used for collaborating with internal teams on your homeserver. This cannot be changed later.", "You might disable this if the room will be used for collaborating with external teams who have their own homeserver. This cannot be changed later.": "You might disable this if the room will be used for collaborating with external teams who have their own homeserver. This cannot be changed later.", @@ -1736,9 +1728,11 @@ "Recently Direct Messaged": "Recently Direct Messaged", "Direct Messages": "Direct Messages", "Start a conversation with someone using their name, username (like ) or email address.": "Start a conversation with someone using their name, username (like ) or email address.", - "Start a conversation with someone using their name, username (like ) or email address. This won't invite them to %(communityName)s. To invite someone to %(communityName)s, click here.": "Start a conversation with someone using their name, username (like ) or email address. This won't invite them to %(communityName)s. To invite someone to %(communityName)s, click here.", + "Start a conversation with someone using their name or username (like ).": "Start a conversation with someone using their name or username (like ).", + "This won't invite them to %(communityName)s. To invite someone to %(communityName)s, click here": "This won't invite them to %(communityName)s. To invite someone to %(communityName)s, click here", "Go": "Go", "Invite someone using their name, username (like ), email address or share this room.": "Invite someone using their name, username (like ), email address or share this room.", + "Invite someone using their name, username (like ) or share this room.": "Invite someone using their name, username (like ) or share this room.", "a new master key signature": "a new master key signature", "a new cross-signing key signature": "a new cross-signing key signature", "a device cross-signing signature": "a device cross-signing signature", @@ -1756,6 +1750,7 @@ "%(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)s now uses 3-5x less memory, by only loading information about other users when needed. Please wait whilst we resynchronise with the server!", "Updating %(brand)s": "Updating %(brand)s", "Encrypted messages are secured with end-to-end encryption. Only you and the recipient(s) have the keys to read these messages.": "Encrypted messages are secured with end-to-end encryption. Only you and the recipient(s) have the keys to read these messages.", + "Start using Key Backup": "Start using Key Backup", "I don't want my encrypted messages": "I don't want my encrypted messages", "Manually export keys": "Manually export keys", "You'll lose access to your encrypted messages": "You'll lose access to your encrypted messages", @@ -1889,6 +1884,13 @@ "Enter your Security Phrase or to continue.": "Enter your Security Phrase or to continue.", "Security Key": "Security Key", "Use your Security Key to continue.": "Use your Security Key to continue.", + "Destroy cross-signing keys?": "Destroy cross-signing keys?", + "Deleting cross-signing keys is permanent. Anyone you have verified with will see security alerts. You almost certainly don't want to do this, unless you've lost every device you can cross-sign from.": "Deleting cross-signing keys is permanent. Anyone you have verified with will see security alerts. You almost certainly don't want to do this, unless you've lost every device you can cross-sign from.", + "Clear cross-signing keys": "Clear cross-signing keys", + "Confirm encryption setup": "Confirm encryption setup", + "Click the button below to confirm setting up encryption.": "Click the button below to confirm setting up encryption.", + "Unable to set up keys": "Unable to set up keys", + "Retry": "Retry", "Restoring keys from backup": "Restoring keys from backup", "Fetching keys from server...": "Fetching keys from server...", "%(completed)s of %(total)s keys restored": "%(completed)s of %(total)s keys restored", @@ -2238,6 +2240,57 @@ "Room Autocomplete": "Room Autocomplete", "Users": "Users", "User Autocomplete": "User Autocomplete", + "We'll store an encrypted copy of your keys on our server. Secure your backup with a recovery passphrase.": "We'll store an encrypted copy of your keys on our server. Secure your backup with a recovery passphrase.", + "For maximum security, this should be different from your account password.": "For maximum security, this should be different from your account password.", + "Enter a recovery passphrase": "Enter a recovery passphrase", + "Great! This recovery passphrase looks strong enough.": "Great! This recovery passphrase looks strong enough.", + "Set up with a recovery key": "Set up with a recovery key", + "That matches!": "That matches!", + "Use a different passphrase?": "Use a different passphrase?", + "That doesn't match.": "That doesn't match.", + "Go back to set it again.": "Go back to set it again.", + "Please enter your recovery passphrase a second time to confirm.": "Please enter your recovery passphrase a second time to confirm.", + "Repeat your recovery passphrase...": "Repeat your recovery passphrase...", + "Your recovery key is a safety net - you can use it to restore access to your encrypted messages if you forget your recovery passphrase.": "Your recovery key is a safety net - you can use it to restore access to your encrypted messages if you forget your recovery passphrase.", + "Keep a copy of it somewhere secure, like a password manager or even a safe.": "Keep a copy of it somewhere secure, like a password manager or even a safe.", + "Your recovery key": "Your recovery key", + "Download": "Download", + "Your recovery key has been copied to your clipboard, paste it to:": "Your recovery key has been copied to your clipboard, paste it to:", + "Your recovery key is in your Downloads folder.": "Your recovery key is in your Downloads folder.", + "Print it and store it somewhere safe": "Print it and store it somewhere safe", + "Save it on a USB key or backup drive": "Save it on a USB key or backup drive", + "Copy it to your personal cloud storage": "Copy it to your personal cloud storage", + "Your keys are being backed up (the first backup could take a few minutes).": "Your keys are being backed up (the first backup could take a few minutes).", + "Without setting up Secure Message Recovery, you won't be able to restore your encrypted message history if you log out or use another session.": "Without setting up Secure Message Recovery, you won't be able to restore your encrypted message history if you log out or use another session.", + "Set up Secure Message Recovery": "Set up Secure Message Recovery", + "Secure your backup with a recovery passphrase": "Secure your backup with a recovery passphrase", + "Confirm your recovery passphrase": "Confirm your recovery passphrase", + "Make a copy of your recovery key": "Make a copy of your recovery key", + "Starting backup...": "Starting backup...", + "Success!": "Success!", + "Create key backup": "Create key backup", + "Unable to create key backup": "Unable to create key backup", + "Safeguard against losing access to encrypted messages & data by backing up encryption keys on your server.": "Safeguard against losing access to encrypted messages & data by backing up encryption keys on your server.", + "Generate a Security Key": "Generate a Security Key", + "We’ll generate a Security Key for you to store somewhere safe, like a password manager or a safe.": "We’ll generate a Security Key for you to store somewhere safe, like a password manager or a safe.", + "Enter a Security Phrase": "Enter a Security Phrase", + "Use a secret phrase only you know, and optionally save a Security Key to use for backup.": "Use a secret phrase only you know, and optionally save a Security Key to use for backup.", + "Enter your account password to confirm the upgrade:": "Enter your account password to confirm the upgrade:", + "Restore your key backup to upgrade your encryption": "Restore your key backup to upgrade your encryption", + "Restore": "Restore", + "You'll need to authenticate with the server to confirm the upgrade.": "You'll need to authenticate with the server to confirm the upgrade.", + "Upgrade this session to allow it to verify other sessions, granting them access to encrypted messages and marking them as trusted for other users.": "Upgrade this session to allow it to verify other sessions, granting them access to encrypted messages and marking them as trusted for other users.", + "Enter a security phrase only you know, as it’s used to safeguard your data. To be secure, you shouldn’t re-use your account password.": "Enter a security phrase only you know, as it’s used to safeguard your data. To be secure, you shouldn’t re-use your account password.", + "Enter your recovery passphrase a second time to confirm it.": "Enter your recovery passphrase a second time to confirm it.", + "Store your Security Key somewhere safe, like a password manager or a safe, as it’s used to safeguard your encrypted data.": "Store your Security Key somewhere safe, like a password manager or a safe, as it’s used to safeguard your encrypted data.", + "Unable to query secret storage status": "Unable to query secret storage status", + "If you cancel now, you may lose encrypted messages & data if you lose access to your logins.": "If you cancel now, you may lose encrypted messages & data if you lose access to your logins.", + "You can also set up Secure Backup & manage your keys in Settings.": "You can also set up Secure Backup & manage your keys in Settings.", + "Upgrade your encryption": "Upgrade your encryption", + "Set a Security Phrase": "Set a Security Phrase", + "Confirm Security Phrase": "Confirm Security Phrase", + "Save your Security Key": "Save your Security Key", + "Unable to set up secret storage": "Unable to set up secret storage", "Passphrases must match": "Passphrases must match", "Passphrase must not be empty": "Passphrase must not be empty", "Unknown error": "Unknown error", @@ -2252,64 +2305,6 @@ "The export file will be protected with a passphrase. You should enter the passphrase here, to decrypt the file.": "The export file will be protected with a passphrase. You should enter the passphrase here, to decrypt the file.", "File to import": "File to import", "Import": "Import", - "Confirm encryption setup": "Confirm encryption setup", - "Click the button below to confirm setting up encryption.": "Click the button below to confirm setting up encryption.", - "Safeguard against losing access to encrypted messages & data by backing up encryption keys on your server.": "Safeguard against losing access to encrypted messages & data by backing up encryption keys on your server.", - "Generate a Security Key": "Generate a Security Key", - "We’ll generate a Security Key for you to store somewhere safe, like a password manager or a safe.": "We’ll generate a Security Key for you to store somewhere safe, like a password manager or a safe.", - "Enter a Security Phrase": "Enter a Security Phrase", - "Use a secret phrase only you know, and optionally save a Security Key to use for backup.": "Use a secret phrase only you know, and optionally save a Security Key to use for backup.", - "Enter your account password to confirm the upgrade:": "Enter your account password to confirm the upgrade:", - "Restore your key backup to upgrade your encryption": "Restore your key backup to upgrade your encryption", - "Restore": "Restore", - "You'll need to authenticate with the server to confirm the upgrade.": "You'll need to authenticate with the server to confirm the upgrade.", - "Upgrade this session to allow it to verify other sessions, granting them access to encrypted messages and marking them as trusted for other users.": "Upgrade this session to allow it to verify other sessions, granting them access to encrypted messages and marking them as trusted for other users.", - "Enter a security phrase only you know, as it’s used to safeguard your data. To be secure, you shouldn’t re-use your account password.": "Enter a security phrase only you know, as it’s used to safeguard your data. To be secure, you shouldn’t re-use your account password.", - "Enter a recovery passphrase": "Enter a recovery passphrase", - "Great! This recovery passphrase looks strong enough.": "Great! This recovery passphrase looks strong enough.", - "That matches!": "That matches!", - "Use a different passphrase?": "Use a different passphrase?", - "That doesn't match.": "That doesn't match.", - "Go back to set it again.": "Go back to set it again.", - "Enter your recovery passphrase a second time to confirm it.": "Enter your recovery passphrase a second time to confirm it.", - "Confirm your recovery passphrase": "Confirm your recovery passphrase", - "Store your Security Key somewhere safe, like a password manager or a safe, as it’s used to safeguard your encrypted data.": "Store your Security Key somewhere safe, like a password manager or a safe, as it’s used to safeguard your encrypted data.", - "Download": "Download", - "Unable to query secret storage status": "Unable to query secret storage status", - "Retry": "Retry", - "If you cancel now, you may lose encrypted messages & data if you lose access to your logins.": "If you cancel now, you may lose encrypted messages & data if you lose access to your logins.", - "You can also set up Secure Backup & manage your keys in Settings.": "You can also set up Secure Backup & manage your keys in Settings.", - "Set up Secure Backup": "Set up Secure Backup", - "Upgrade your encryption": "Upgrade your encryption", - "Set a Security Phrase": "Set a Security Phrase", - "Confirm Security Phrase": "Confirm Security Phrase", - "Save your Security Key": "Save your Security Key", - "Unable to set up secret storage": "Unable to set up secret storage", - "We'll store an encrypted copy of your keys on our server. Secure your backup with a recovery passphrase.": "We'll store an encrypted copy of your keys on our server. Secure your backup with a recovery passphrase.", - "For maximum security, this should be different from your account password.": "For maximum security, this should be different from your account password.", - "Set up with a recovery key": "Set up with a recovery key", - "Please enter your recovery passphrase a second time to confirm.": "Please enter your recovery passphrase a second time to confirm.", - "Repeat your recovery passphrase...": "Repeat your recovery passphrase...", - "Your recovery key is a safety net - you can use it to restore access to your encrypted messages if you forget your recovery passphrase.": "Your recovery key is a safety net - you can use it to restore access to your encrypted messages if you forget your recovery passphrase.", - "Keep a copy of it somewhere secure, like a password manager or even a safe.": "Keep a copy of it somewhere secure, like a password manager or even a safe.", - "Your recovery key": "Your recovery key", - "Your recovery key has been copied to your clipboard, paste it to:": "Your recovery key has been copied to your clipboard, paste it to:", - "Your recovery key is in your Downloads folder.": "Your recovery key is in your Downloads folder.", - "Print it and store it somewhere safe": "Print it and store it somewhere safe", - "Save it on a USB key or backup drive": "Save it on a USB key or backup drive", - "Copy it to your personal cloud storage": "Copy it to your personal cloud storage", - "Your keys are being backed up (the first backup could take a few minutes).": "Your keys are being backed up (the first backup could take a few minutes).", - "Without setting up Secure Message Recovery, you won't be able to restore your encrypted message history if you log out or use another session.": "Without setting up Secure Message Recovery, you won't be able to restore your encrypted message history if you log out or use another session.", - "Set up Secure Message Recovery": "Set up Secure Message Recovery", - "Secure your backup with a recovery passphrase": "Secure your backup with a recovery passphrase", - "Make a copy of your recovery key": "Make a copy of your recovery key", - "Starting backup...": "Starting backup...", - "Success!": "Success!", - "Create key backup": "Create key backup", - "Unable to create key backup": "Unable to create key backup", - "Without setting up Secure Message Recovery, you'll lose your secure message history when you log out.": "Without setting up Secure Message Recovery, you'll lose your secure message history when you log out.", - "If you don't want to set this up now, you can later in Settings.": "If you don't want to set this up now, you can later in Settings.", - "Don't ask again": "Don't ask again", "New Recovery Method": "New Recovery Method", "A new recovery passphrase and key for Secure Messages have been detected.": "A new recovery passphrase and key for Secure Messages have been detected.", "If you didn't set the new recovery method, an attacker may be trying to access your account. Change your account password and set a new recovery method immediately in Settings.": "If you didn't set the new recovery method, an attacker may be trying to access your account. Change your account password and set a new recovery method immediately in Settings.", diff --git a/src/rageshake/submit-rageshake.ts b/src/rageshake/submit-rageshake.ts index dd60cde16d..d361f6b0dd 100644 --- a/src/rageshake/submit-rageshake.ts +++ b/src/rageshake/submit-rageshake.ts @@ -112,6 +112,7 @@ async function collectBugReport(opts: IOpts = {}, gzipLogs = true) { body.append("secret_storage_ready", String(await client.isSecretStorageReady())); body.append("secret_storage_key_in_account", String(!!(await secretStorage.hasKey()))); + body.append("session_backup_key_in_secret_storage", String(!!(await client.isKeyBackupKeyStored()))); const sessionBackupKeyFromCache = await client._crypto.getSessionBackupPrivateKey(); body.append("session_backup_key_cached", String(!!sessionBackupKeyFromCache)); body.append("session_backup_key_well_formed", String(sessionBackupKeyFromCache instanceof Uint8Array)); diff --git a/src/settings/Settings.ts b/src/settings/Settings.ts index 21b3935c3e..d78f75b346 100644 --- a/src/settings/Settings.ts +++ b/src/settings/Settings.ts @@ -281,11 +281,6 @@ export const SETTINGS: {[setting: string]: ISetting} = { displayName: _td('Autoplay GIFs and videos'), default: false, }, - "showRoomRecoveryReminder": { - supportedLevels: LEVELS_ACCOUNT_SETTINGS, - displayName: _td('Show a reminder to enable Secure Message Recovery in encrypted rooms'), - default: true, - }, "enableSyntaxHighlightLanguageDetection": { supportedLevels: LEVELS_ACCOUNT_SETTINGS, displayName: _td('Enable automatic language detection for syntax highlighting'), @@ -588,6 +583,7 @@ export const SETTINGS: {[setting: string]: ISetting} = { "showCallButtonsInComposer": { supportedLevels: LEVELS_DEVICE_ONLY_SETTINGS_WITH_CONFIG, default: true, + controller: new UIFeatureController(UIFeature.Voip), }, "e2ee.manuallyVerifyAllSessions": { supportedLevels: LEVELS_DEVICE_ONLY_SETTINGS, @@ -622,8 +618,50 @@ export const SETTINGS: {[setting: string]: ISetting} = { supportedLevels: LEVELS_UI_FEATURE, default: true, }, + [UIFeature.Voip]: { + supportedLevels: LEVELS_UI_FEATURE, + default: true, + }, [UIFeature.Feedback]: { supportedLevels: LEVELS_UI_FEATURE, default: true, }, + [UIFeature.Registration]: { + supportedLevels: LEVELS_UI_FEATURE, + default: true, + }, + [UIFeature.PasswordReset]: { + supportedLevels: LEVELS_UI_FEATURE, + default: true, + }, + [UIFeature.Deactivate]: { + supportedLevels: LEVELS_UI_FEATURE, + default: true, + }, + [UIFeature.ShareQRCode]: { + supportedLevels: LEVELS_UI_FEATURE, + default: true, + }, + [UIFeature.ShareSocial]: { + supportedLevels: LEVELS_UI_FEATURE, + default: true, + }, + [UIFeature.IdentityServer]: { + supportedLevels: LEVELS_UI_FEATURE, + default: true, + // Identity Server (Discovery) Settings make no sense if 3PIDs in general are hidden + controller: new UIFeatureController(UIFeature.ThirdPartyID), + }, + [UIFeature.ThirdPartyID]: { + supportedLevels: LEVELS_UI_FEATURE, + default: true, + }, + [UIFeature.Flair]: { + supportedLevels: LEVELS_UI_FEATURE, + default: true, + }, + [UIFeature.AdvancedSettings]: { + supportedLevels: LEVELS_UI_FEATURE, + default: true, + }, }; diff --git a/src/settings/UIFeature.ts b/src/settings/UIFeature.ts index dddef82df1..5c579054f0 100644 --- a/src/settings/UIFeature.ts +++ b/src/settings/UIFeature.ts @@ -18,5 +18,15 @@ limitations under the License. export enum UIFeature { URLPreviews = "UIFeature.urlPreviews", Widgets = "UIFeature.widgets", + Voip = "UIFeature.voip", Feedback = "UIFeature.feedback", + Registration = "UIFeature.registration", + PasswordReset = "UIFeature.passwordReset", + Deactivate = "UIFeature.deactivate", + ShareQRCode = "UIFeature.shareQrCode", + ShareSocial = "UIFeature.shareSocial", + IdentityServer = "UIFeature.identityServer", + ThirdPartyID = "UIFeature.thirdPartyId", + Flair = "UIFeature.flair", + AdvancedSettings = "UIFeature.advancedSettings", } diff --git a/src/stores/RoomViewStore.tsx b/src/stores/RoomViewStore.tsx index a0f0fb8f68..be1141fa1e 100644 --- a/src/stores/RoomViewStore.tsx +++ b/src/stores/RoomViewStore.tsx @@ -18,6 +18,7 @@ limitations under the License. import React from "react"; import {Store} from 'flux/utils'; +import {MatrixError} from "matrix-js-sdk/src/http-api"; import dis from '../dispatcher/dispatcher'; import {MatrixClientPeg} from '../MatrixClientPeg'; @@ -26,6 +27,9 @@ import Modal from '../Modal'; import { _t } from '../languageHandler'; import { getCachedRoomIDForAlias, storeRoomAliasInCache } from '../RoomAliasCache'; import {ActionPayload} from "../dispatcher/payloads"; +import {retry} from "../utils/promise"; + +const NUM_JOIN_RETRY = 5; const INITIAL_STATE = { // Whether we're joining the currently viewed room (see isJoining()) @@ -259,24 +263,32 @@ class RoomViewStore extends Store { }); } - private joinRoom(payload: ActionPayload) { + private async joinRoom(payload: ActionPayload) { this.setState({ joining: true, }); - MatrixClientPeg.get().joinRoom( - this.state.roomAlias || this.state.roomId, payload.opts, - ).then(() => { + + const cli = MatrixClientPeg.get(); + const address = this.state.roomAlias || this.state.roomId; + try { + await retry(() => cli.joinRoom(address, payload.opts), NUM_JOIN_RETRY, (err) => { + // if we received a Gateway timeout then retry + return err.httpStatus === 504; + }); + // We do *not* clear the 'joining' flag because the Room object and/or our 'joined' member event may not // have come down the sync stream yet, and that's the point at which we'd consider the user joined to the // room. dis.dispatch({ action: 'join_room_ready' }); - }, (err) => { + } catch (err) { dis.dispatch({ action: 'join_room_error', err: err, }); + let msg = err.message ? err.message : JSON.stringify(err); console.log("Failed to join room:", msg); + if (err.name === "ConnectionError") { msg = _t("There was an error joining the room"); } else if (err.errcode === 'M_INCOMPATIBLE_ROOM_VERSION') { @@ -296,12 +308,13 @@ class RoomViewStore extends Store { } } } + const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); Modal.createTrackedDialog('Failed to join room', '', ErrorDialog, { title: _t("Failed to join room"), description: msg, }); - }); + } } private getInvitingUserId(roomId: string): string { diff --git a/src/toasts/SetupEncryptionToast.ts b/src/toasts/SetupEncryptionToast.ts index 9dbc4acafc..5aa030e497 100644 --- a/src/toasts/SetupEncryptionToast.ts +++ b/src/toasts/SetupEncryptionToast.ts @@ -18,7 +18,7 @@ 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 SetupEncryptionDialog from "../components/views/dialogs/security/SetupEncryptionDialog"; import { accessSecretStorage } from "../SecurityManager"; import ToastStore from "../stores/ToastStore"; import GenericToast from "../components/views/toasts/GenericToast"; @@ -28,7 +28,7 @@ const TOAST_KEY = "setupencryption"; const getTitle = (kind: Kind) => { switch (kind) { case Kind.SET_UP_ENCRYPTION: - return _t("Set up encryption"); + return _t("Set up Secure Backup"); case Kind.UPGRADE_ENCRYPTION: return _t("Encryption upgrade available"); case Kind.VERIFY_THIS_SESSION: @@ -36,10 +36,20 @@ const getTitle = (kind: Kind) => { } }; +const getIcon = (kind: Kind) => { + switch (kind) { + case Kind.SET_UP_ENCRYPTION: + case Kind.UPGRADE_ENCRYPTION: + return "secure_backup"; + case Kind.VERIFY_THIS_SESSION: + return "verification_warning"; + } +}; + const getSetupCaption = (kind: Kind) => { switch (kind) { case Kind.SET_UP_ENCRYPTION: - return _t("Set up"); + return _t("Continue"); case Kind.UPGRADE_ENCRYPTION: return _t("Upgrade"); case Kind.VERIFY_THIS_SESSION: @@ -51,7 +61,7 @@ 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"); + return _t("Safeguard against losing access to encrypted messages & data"); case Kind.VERIFY_THIS_SESSION: return _t("Other users may not trust it"); } @@ -88,7 +98,7 @@ export const showToast = (kind: Kind) => { ToastStore.sharedInstance().addOrReplaceToast({ key: TOAST_KEY, title: getTitle(kind), - icon: "verification_warning", + icon: getIcon(kind), props: { description: getDescription(kind), acceptLabel: getSetupCaption(kind), diff --git a/src/utils/promise.ts b/src/utils/promise.ts index d3ae2c3d1b..f828ddfdaf 100644 --- a/src/utils/promise.ts +++ b/src/utils/promise.ts @@ -68,3 +68,21 @@ export function allSettled(promises: Promise[]): Promise(fn: () => Promise, num: number, predicate?: (e: E) => boolean) { + let lastErr: E; + for (let i = 0; i < num; i++) { + try { + const v = await fn(); + // If `await fn()` throws then we won't reach here + return v; + } catch (err) { + if (predicate && !predicate(err)) { + throw err; + } + lastErr = err; + } + } + throw lastErr; +} diff --git a/test/components/views/dialogs/AccessSecretStorageDialog-test.js b/test/components/views/dialogs/AccessSecretStorageDialog-test.js index 5a8dcbf763..7c4b2996c9 100644 --- a/test/components/views/dialogs/AccessSecretStorageDialog-test.js +++ b/test/components/views/dialogs/AccessSecretStorageDialog-test.js @@ -20,7 +20,7 @@ import sdk from '../../../skinned-sdk'; import {MatrixClientPeg} from '../../../../src/MatrixClientPeg'; import { stubClient } from '../../../test-utils'; -const AccessSecretStorageDialog = sdk.getComponent("dialogs.secretstorage.AccessSecretStorageDialog"); +const AccessSecretStorageDialog = sdk.getComponent("dialogs.security.AccessSecretStorageDialog"); describe("AccessSecretStorageDialog", function() { it("Closes the dialog if _onRecoveryKeyNext is called with a valid key", (done) => { diff --git a/test/end-to-end-tests/src/scenarios/e2e-encryption.js b/test/end-to-end-tests/src/scenarios/e2e-encryption.js index d31d2c0d57..20e8af2947 100644 --- a/test/end-to-end-tests/src/scenarios/e2e-encryption.js +++ b/test/end-to-end-tests/src/scenarios/e2e-encryption.js @@ -21,6 +21,7 @@ const {receiveMessage} = require('../usecases/timeline'); const {createDm} = require('../usecases/create-room'); const {checkRoomSettings} = require('../usecases/room-settings'); const {startSasVerifcation, acceptSasVerification} = require('../usecases/verify'); +const { setupSecureBackup } = require('../usecases/security'); const assert = require('assert'); module.exports = async function e2eEncryptionScenarios(alice, bob) { @@ -43,4 +44,5 @@ module.exports = async function e2eEncryptionScenarios(alice, bob) { const bobMessage = "You've got to tell me!"; await sendMessage(bob, bobMessage); await receiveMessage(alice, {sender: "bob", body: bobMessage, encrypted: true}); + await setupSecureBackup(alice); }; diff --git a/test/end-to-end-tests/src/usecases/security.js b/test/end-to-end-tests/src/usecases/security.js new file mode 100644 index 0000000000..31540874e9 --- /dev/null +++ b/test/end-to-end-tests/src/usecases/security.js @@ -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. +*/ + +const { acceptToast } = require("./toasts"); + +async function setupSecureBackup(session) { + session.log.step("sets up Secure Backup"); + + await acceptToast(session, "Set up Secure Backup"); + + // Continue with the default (generate a security key) + const xsignContButton = await session.query('.mx_CreateSecretStorageDialog .mx_Dialog_buttons .mx_Dialog_primary'); + await xsignContButton.click(); + + //ignore the recovery key + //TODO: It's probably important for the tests to know the recovery key + const copyButton = await session.query('.mx_CreateSecretStorageDialog_recoveryKeyButtons_copyBtn'); + await copyButton.click(); + + //acknowledge that we copied the recovery key to a safe place + const copyContinueButton = await session.query( + '.mx_CreateSecretStorageDialog .mx_Dialog_buttons .mx_Dialog_primary', + ); + await copyContinueButton.click(); + + session.log.done(); +} + +module.exports = { setupSecureBackup }; diff --git a/test/end-to-end-tests/src/usecases/signup.js b/test/end-to-end-tests/src/usecases/signup.js index fd41ef1a71..ef8a259091 100644 --- a/test/end-to-end-tests/src/usecases/signup.js +++ b/test/end-to-end-tests/src/usecases/signup.js @@ -79,21 +79,6 @@ module.exports = async function signup(session, username, password, homeserver) const acceptButton = await session.query('.mx_InteractiveAuthEntryComponents_termsSubmit'); await acceptButton.click(); - // Continue with the default (generate a security key) - const xsignContButton = await session.query('.mx_CreateSecretStorageDialog .mx_Dialog_buttons .mx_Dialog_primary'); - await xsignContButton.click(); - - //ignore the recovery key - //TODO: It's probably important for the tests to know the recovery key - const copyButton = await session.query('.mx_CreateSecretStorageDialog_recoveryKeyButtons_copyBtn'); - await copyButton.click(); - - //acknowledge that we copied the recovery key to a safe place - const copyContinueButton = await session.query( - '.mx_CreateSecretStorageDialog .mx_Dialog_buttons .mx_Dialog_primary', - ); - await copyContinueButton.click(); - //wait for registration to finish so the hash gets set //onhashchange better? diff --git a/yarn.lock b/yarn.lock index 515ddddfc7..efc1f0eae1 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1907,17 +1907,7 @@ airbnb-prop-types@^2.15.0: prop-types-exact "^1.2.0" react-is "^16.9.0" -ajv-errors@^1.0.0: - version "1.0.1" - resolved "https://registry.yarnpkg.com/ajv-errors/-/ajv-errors-1.0.1.tgz#f35986aceb91afadec4102fbd85014950cefa64d" - integrity sha512-DCRfO/4nQ+89p/RK43i8Ezd41EqdGIU4ld7nGF8OQ14oc/we5rEntLCUa7+jrn3nn83BosfwZA0wb4pon2o8iQ== - -ajv-keywords@^3.1.0: - version "3.4.1" - resolved "https://registry.yarnpkg.com/ajv-keywords/-/ajv-keywords-3.4.1.tgz#ef916e271c64ac12171fd8384eaae6b2345854da" - integrity sha512-RO1ibKvd27e6FEShVFfPALuHI3WjSVNeK5FIsmme/LYRNxjKuNj+Dt7bucLa6NdSv3JcVTyMlm9kGR84z1XpaQ== - -ajv@^6.1.0, ajv@^6.10.0, ajv@^6.10.2, ajv@^6.5.5: +ajv@^6.10.0, ajv@^6.10.2, ajv@^6.5.5: version "6.12.2" resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.12.2.tgz#c629c5eced17baf314437918d2da88c99d5958cd" integrity sha512-k+V+hzjm5q/Mr8ef/1Y9goCmlsK4I6Sm74teeyGvFk1XrOsbsKLjEdrvny42CZ+a8sXbk8KWpY/bDwS+FLL2UQ== @@ -2142,13 +2132,6 @@ async-limiter@~1.0.0: resolved "https://registry.yarnpkg.com/async-limiter/-/async-limiter-1.0.1.tgz#dd379e94f0db8310b08291f9d64c3209766617fd" integrity sha512-csOlWGAcRFJaI6m+F2WKdnMKr4HhdhFVBk0H/QbJFMCr+uO2kwohwXQPxw/9OCxp05r5ghVBFSyioixx3gfkNQ== -async@^2.5.0: - version "2.6.3" - resolved "https://registry.yarnpkg.com/async/-/async-2.6.3.tgz#d72625e2344a3656e3a3ad4fa749fa83299d82ff" - integrity sha512-zflvls11DCy+dQWzTW2dzuilv8Z5X/pjfmZOWba6TNIVDm+2UDaJmXSOXlasHKfNBs8oo3M0aT50fDEWfKZjXg== - dependencies: - lodash "^4.17.14" - asynckit@^0.4.0: version "0.4.0" resolved "https://registry.yarnpkg.com/asynckit/-/asynckit-0.4.0.tgz#c79ed97f7f34cb8f2ba1bc9790bcc366474b4b79" @@ -2294,11 +2277,6 @@ bcrypt-pbkdf@^1.0.0: dependencies: tweetnacl "^0.14.3" -big.js@^5.2.2: - version "5.2.2" - resolved "https://registry.yarnpkg.com/big.js/-/big.js-5.2.2.tgz#65f0af382f578bcdc742bd9c281e9cb2d7768328" - integrity sha512-vyL2OymJxmarO8gxMr0mhChsO9QGwhynfuu4+MHTAW6czfq9humCB7rKpUjDd9YUiDPU4mzpyupFSvOClAwbmQ== - binary-extensions@^1.0.0: version "1.13.1" resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-1.13.1.tgz#598afe54755b2868a5330d2aff9d4ebb53209b65" @@ -3285,11 +3263,6 @@ emojibase-regex@^4.0.1: resolved "https://registry.yarnpkg.com/emojibase-regex/-/emojibase-regex-4.0.1.tgz#a2cd4bbb42825422da9ec72f15e970bc2c90b46a" integrity sha512-S42UHkFfz15i4NNz+wi9iMKFo+B6Kalc6PJLpYX0BUANViXw4vSyYZMFdBGRLduSabWHuEcTLZl9xOa2YP3eJw== -emojis-list@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/emojis-list/-/emojis-list-3.0.0.tgz#5570662046ad29e2e916e71aae260abdff4f6a78" - integrity sha512-/kyM18EfinwXZbno9FyUGeFh87KC8HRQBQGildHZbEuRyWFOmv1U10o9BBp8XVZDVNNuQKyIGIu5ZYAAXJ0V2Q== - encoding@^0.1.11: version "0.1.12" resolved "https://registry.yarnpkg.com/encoding/-/encoding-0.1.12.tgz#538b66f3ee62cd1ab51ec323829d1f9480c74beb" @@ -4018,14 +3991,6 @@ file-entry-cache@^5.0.1: dependencies: flat-cache "^2.0.1" -file-loader@^3.0.1: - version "3.0.1" - resolved "https://registry.yarnpkg.com/file-loader/-/file-loader-3.0.1.tgz#f8e0ba0b599918b51adfe45d66d1e771ad560faa" - integrity sha512-4sNIOXgtH/9WZq4NvlfU3Opn5ynUsqBwSLyM+I7UOwdGigTBYfVVQEwe/msZNX/j4pCJTIM14Fsw66Svo1oVrw== - dependencies: - loader-utils "^1.0.2" - schema-utils "^1.0.0" - file-saver@^1.3.8: version "1.3.8" resolved "https://registry.yarnpkg.com/file-saver/-/file-saver-1.3.8.tgz#e68a30c7cb044e2fb362b428469feb291c2e09d8" @@ -5746,15 +5711,6 @@ load-json-file@^4.0.0: pify "^3.0.0" strip-bom "^3.0.0" -loader-utils@^1.0.2, loader-utils@^1.1.0: - version "1.4.0" - resolved "https://registry.yarnpkg.com/loader-utils/-/loader-utils-1.4.0.tgz#c579b5e34cb34b1a74edc6c1fb36bfa371d5a613" - integrity sha512-qH0WSMBtn/oHuwjy/NucEgbx5dbxxnxup9s4PVXJUDHZBQY+s0NWA9rJf53RBnQZxfch7euUui7hpoAPvALZdA== - dependencies: - big.js "^5.2.2" - emojis-list "^3.0.0" - json5 "^1.0.1" - locate-path@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-2.0.0.tgz#2b568b265eec944c6d9c0de9c3dbbbca0354cd8e" @@ -7684,15 +7640,6 @@ scheduler@^0.19.1: loose-envify "^1.1.0" object-assign "^4.1.1" -schema-utils@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/schema-utils/-/schema-utils-1.0.0.tgz#0b79a93204d7b600d4b2850d1f66c2a34951c770" - integrity sha512-i27Mic4KovM/lnGsy8whRCHhc7VicJajAjTrYg11K9zfZXnYIt4k5F+kZkwjnrhKzLic/HLU4j11mjsz2G/75g== - dependencies: - ajv "^6.1.0" - ajv-errors "^1.0.0" - ajv-keywords "^3.1.0" - "semver@2 || 3 || 4 || 5", semver@^5.4.1, semver@^5.5.0, semver@^5.5.1, semver@^5.6.0, semver@^5.7.0, semver@^5.7.1: version "5.7.1" resolved "https://registry.yarnpkg.com/semver/-/semver-5.7.1.tgz#a954f931aeba508d307bbf069eff0c01c96116f7" @@ -7850,14 +7797,6 @@ socks@~2.3.2: ip "1.1.5" smart-buffer "^4.1.0" -source-map-loader@^0.2.4: - version "0.2.4" - resolved "https://registry.yarnpkg.com/source-map-loader/-/source-map-loader-0.2.4.tgz#c18b0dc6e23bf66f6792437557c569a11e072271" - integrity sha512-OU6UJUty+i2JDpTItnizPrlpOIBLmQbWMuBg9q5bVtnHACqw1tn9nNwqJLbv0/00JjnJb/Ee5g5WS5vrRv7zIQ== - dependencies: - async "^2.5.0" - loader-utils "^1.1.0" - source-map-resolve@^0.5.0: version "0.5.3" resolved "https://registry.yarnpkg.com/source-map-resolve/-/source-map-resolve-0.5.3.tgz#190866bece7553e1f8f267a2ee82c606b5509a1a"