diff --git a/package.json b/package.json index a8c54d1d741..26e650c2d8a 100644 --- a/package.json +++ b/package.json @@ -44,7 +44,7 @@ "start:all": "echo THIS IS FOR LEGACY PURPOSES ONLY. && yarn start:build", "start:build": "babel src -w -s -d lib --verbose --extensions \".ts,.js\"", "lint": "yarn lint:types && yarn lint:js && yarn lint:style", - "lint:js": "eslint --max-warnings 0 src test", + "lint:js": "eslint src test", "lint:js-fix": "eslint --fix src test", "lint:types": "tsc --noEmit --jsx react", "lint:style": "stylelint \"res/css/**/*.scss\"", diff --git a/res/css/_components.scss b/res/css/_components.scss index 09a5fd6e148..f1f76c15458 100644 --- a/res/css/_components.scss +++ b/res/css/_components.scss @@ -261,6 +261,7 @@ @import "./views/settings/_CryptographyPanel.scss"; @import "./views/settings/_DevicesPanel.scss"; @import "./views/settings/_E2eAdvancedPanel.scss"; +@import "./views/settings/_E2eTrustPanel.scss"; @import "./views/settings/_EmailAddresses.scss"; @import "./views/settings/_FontScalingPanel.scss"; @import "./views/settings/_ImageSizePanel.scss"; diff --git a/res/css/views/settings/_E2eTrustPanel.scss b/res/css/views/settings/_E2eTrustPanel.scss new file mode 100644 index 00000000000..3993688b118 --- /dev/null +++ b/res/css/views/settings/_E2eTrustPanel.scss @@ -0,0 +1,28 @@ +/* +Copyright 2022 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_E2eTrustPanel { + .mx_Slider { + margin: 0 20px; + .mx_Slider_selectionDot { + display: none; + } + .mx_Slider_label { + margin-top: 8px; + color: $primary-content; + } + } +} diff --git a/src/DeviceListener.ts b/src/DeviceListener.ts index adba51d1352..b1c8203b0fe 100644 --- a/src/DeviceListener.ts +++ b/src/DeviceListener.ts @@ -59,6 +59,14 @@ export default class DeviceListener { // The set of device IDs we're currently displaying toasts for private displayingToastsForDeviceIds = new Set(); + private hasViewedEncryptedRoom = false; + + public async viewingEncryptedRoom() { + this.hasViewedEncryptedRoom = true; + this.dismissedThisDeviceToast = false; + return this.recheck(); + } + static sharedInstance() { if (!window.mxDeviceListener) window.mxDeviceListener = new DeviceListener(); return window.mxDeviceListener; @@ -211,9 +219,11 @@ 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; - // 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)); + // // 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)); + // Show setup toasts once the user tries to view an encrypted room + return this.hasViewedEncryptedRoom; } private async recheck() { diff --git a/src/SlashCommands.tsx b/src/SlashCommands.tsx index 75254afb3c0..db92951e237 100644 --- a/src/SlashCommands.tsx +++ b/src/SlashCommands.tsx @@ -1010,14 +1010,14 @@ export const Commands = [ if (device.getFingerprint() === fingerprint) { throw newTranslatableError('Session already verified!'); } else { - throw newTranslatableError('WARNING: Session already verified, but keys do NOT MATCH!'); + throw newTranslatableError('WARNING: Device already verified, but keys do NOT MATCH!'); } } if (device.getFingerprint() !== fingerprint) { const fprint = device.getFingerprint(); throw newTranslatableError( - 'WARNING: KEY VERIFICATION FAILED! The signing key for %(userId)s and session' + + 'WARNING: KEY VERIFICATION FAILED! The signing key for %(userId)s and device' + ' %(deviceId)s is "%(fprint)s" which does not match the provided key ' + '"%(fingerprint)s". This could mean your communications are being intercepted!', { @@ -1038,7 +1038,7 @@ export const Commands = [

{ _t('The signing key you provided matches the signing key you received ' + - 'from %(userId)s\'s session %(deviceId)s. Session marked as verified.', + 'from %(userId)s\'s device %(deviceId)s. Device marked as verified.', { userId, deviceId }) }

diff --git a/src/async-components/views/dialogs/security/CreateKeyBackupDialog.tsx b/src/async-components/views/dialogs/security/CreateKeyBackupDialog.tsx index 560eaadab1d..ed4952bec61 100644 --- a/src/async-components/views/dialogs/security/CreateKeyBackupDialog.tsx +++ b/src/async-components/views/dialogs/security/CreateKeyBackupDialog.tsx @@ -373,12 +373,12 @@ export default class CreateKeyBackupDialog extends React.PureComponentcopied to your clipboard, paste it to:", + "Your recovery key has been copied to your clipboard, paste it to:", {}, { b: s => { s } }, ); } else if (this.state.downloaded) { introText = _t( - "Your Security Key is in your Downloads folder.", + "Your recovery key is in your Downloads folder.", {}, { b: s => { s } }, ); } @@ -419,7 +419,7 @@ export default class CreateKeyBackupDialog extends React.PureComponent { _t( "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.", + "encrypted message history if you log out or use another device.", ) }
- { _t("Generate a Security Key") } + { _t("Generate a secure messaging recovery key") }
-
{ _t("We'll generate a Security Key for you to store somewhere safe, like a password manager or a safe.") }
+
{ _t("We'll generate a secure messaging recovery key for you to store somewhere safe, like a password manager or a safe.") }
); } @@ -522,7 +522,7 @@ export default class CreateSecretStorageDialog extends React.PureComponent { _t("Enter a Security Phrase") } -
{ _t("Use a secret phrase only you know, and optionally save a Security Key to use for backup.") }
+
{ _t("Use a secret phrase only you know, and optionally save a recovery key to use for backup.") }
); } @@ -586,7 +586,7 @@ export default class CreateSecretStorageDialog extends React.PureComponent

{ _t( - "Upgrade this session to allow it to verify other sessions, " + + "Upgrade this device to allow it to verify other devices, " + "granting them access to encrypted messages and marking them " + "as trusted for other users.", ) }

@@ -718,7 +718,7 @@ export default class CreateSecretStorageDialog extends React.PureComponent

{ _t( - "Store your Security Key somewhere safe, like a password manager or a safe, " + + "Store your recovery key somewhere safe, like a password manager or a safe, " + "as it's used to safeguard your encrypted data.", ) }

@@ -799,7 +799,7 @@ export default class CreateSecretStorageDialog extends React.PureComponent return (

{ _t( - 'This process allows you to import encryption keys ' + + 'This process allows you to import message encryption keys ' + 'that you had previously exported from another Matrix ' + 'client. You will then be able to decrypt any ' + 'messages that the other client could decrypt.', diff --git a/src/async-components/views/dialogs/security/NewRecoveryMethodDialog.tsx b/src/async-components/views/dialogs/security/NewRecoveryMethodDialog.tsx index ff21dba7ca6..dd45b964695 100644 --- a/src/async-components/views/dialogs/security/NewRecoveryMethodDialog.tsx +++ b/src/async-components/views/dialogs/security/NewRecoveryMethodDialog.tsx @@ -71,7 +71,7 @@ export default class NewRecoveryMethodDialog extends React.PureComponent content =

{ newMethodDetected }

{ _t( - "This session is encrypting history using the new recovery method.", + "This device is encrypting history using the new recovery method.", ) }

{ hackWarning }

{ _t( - "This session has detected that your Security Phrase and key " + + "This device has detected that your Security Phrase and key " + "for Secure Messages have been removed.", ) }

{ _t( - "If you did this accidentally, you can setup Secure Messages on " + - "this session which will re-encrypt this session's message " + + "If you did this accidentally, you can set up Secure Messages on " + + "this device which will re-encrypt this device's message " + "history with a new recovery method.", ) }

{ _t( diff --git a/src/components/structures/MatrixChat.tsx b/src/components/structures/MatrixChat.tsx index 624909db31d..ce239212d33 100644 --- a/src/components/structures/MatrixChat.tsx +++ b/src/components/structures/MatrixChat.tsx @@ -91,7 +91,6 @@ import { showToast as showMobileGuideToast } from '../../toasts/MobileGuideToast import { shouldUseLoginForWelcome } from "../../utils/pages"; import RoomListStore from "../../stores/room-list/RoomListStore"; import { RoomUpdateCause } from "../../stores/room-list/models"; -import SecurityCustomisations from "../../customisations/Security"; import Spinner from "../views/elements/Spinner"; import QuestionDialog from "../views/dialogs/QuestionDialog"; import UserSettingsDialog from '../views/dialogs/UserSettingsDialog'; @@ -375,18 +374,36 @@ export default class MatrixChat extends React.PureComponent { return; } - const crossSigningIsSetUp = cli.getStoredCrossSigningForUser(cli.getUserId()); - if (crossSigningIsSetUp) { - if (SecurityCustomisations.SHOW_ENCRYPTION_SETUP_UI === false) { - this.onLoggedIn(); - } else { - this.setStateForNewView({ view: Views.COMPLETE_SECURITY }); - } - } else if (await cli.doesServerSupportUnstableFeature("org.matrix.e2e_cross_signing")) { - this.setStateForNewView({ view: Views.E2E_SETUP }); - } else { - this.onLoggedIn(); + if (MatrixClientPeg.currentUserIsJustRegistered()) { + // auto generate a recovery key + const recoveryKey = await cli.createRecoveryKeyFromPassphrase(); + + // use grace period for auth: + await cli.crypto.bootstrapCrossSigning({ + setupNewCrossSigning: true, + authUploadDeviceSigningKeys: async (makeRequest) => { await makeRequest(undefined); }, + }); + + await cli.bootstrapSecretStorage({ + setupNewKeyBackup: true, + setupNewSecretStorage: true, + createSecretStorageKey: () => Promise.resolve(recoveryKey), + }); } + // const crossSigningIsSetUp = cli.getStoredCrossSigningForUser(cli.getUserId()); + // if (crossSigningIsSetUp) { + // if (SecurityCustomisations.SHOW_ENCRYPTION_SETUP_UI === false) { + // this.onLoggedIn(); + // } else { + // this.setStateForNewView({ view: Views.COMPLETE_SECURITY }); + // } + // } else if (await cli.doesServerSupportUnstableFeature("org.matrix.e2e_cross_signing")) { + // this.setStateForNewView({ view: Views.E2E_SETUP }); + // } else { + // this.onLoggedIn(); + // } + this.onLoggedIn(); + this.setState({ pendingInitialSync: false }); } @@ -1496,7 +1513,7 @@ export default class MatrixChat extends React.PureComponent { Modal.createTrackedDialog('Signed out', '', ErrorDialog, { title: _t('Signed Out'), - description: _t('For security, this session has been signed out. Please sign in again.'), + description: _t('For security, this device has been signed out. Please sign in again.'), }); dis.dispatch({ diff --git a/src/components/structures/RoomView.tsx b/src/components/structures/RoomView.tsx index 5ef6f2282f9..4da26bc35b5 100644 --- a/src/components/structures/RoomView.tsx +++ b/src/components/structures/RoomView.tsx @@ -107,6 +107,7 @@ import { DoAfterSyncPreparedPayload } from '../../dispatcher/payloads/DoAfterSyn import FileDropTarget from './FileDropTarget'; import Measured from '../views/elements/Measured'; import { FocusComposerPayload } from '../../dispatcher/payloads/FocusComposerPayload'; +import DeviceListener from '../../DeviceListener'; import { haveRendererForEvent } from "../../events/EventTileFactory"; const DEBUG = false; @@ -1139,6 +1140,8 @@ export class RoomView extends React.Component { e2eStatus = await shieldStatusForRoom(this.context, room); } + await DeviceListener.sharedInstance().viewingEncryptedRoom(); + if (this.unmounted) return; this.setState({ e2eStatus }); } diff --git a/src/components/structures/UserMenu.tsx b/src/components/structures/UserMenu.tsx index 27cc08f0bcc..9b9e7c074f5 100644 --- a/src/components/structures/UserMenu.tsx +++ b/src/components/structures/UserMenu.tsx @@ -316,17 +316,12 @@ export default class UserMenu extends React.Component { this.setState({ contextMenuPosition: null }); // also close the menu }; - private onSignOutClick = async (ev: ButtonEvent) => { + private onSignOutClick = (ev: ButtonEvent) => { ev.preventDefault(); ev.stopPropagation(); - const cli = MatrixClientPeg.get(); - if (!cli || !cli.isCryptoEnabled() || !(await cli.exportRoomKeys())?.length) { - // log out without user prompt if they have no local megolm sessions - dis.dispatch({ action: 'logout' }); - } else { - Modal.createTrackedDialog('Logout from LeftPanel', '', LogoutDialog); - } + // always open dialog which will handle confirmation if needed + Modal.createTrackedDialog('Logout from LeftPanel', '', LogoutDialog, { noConfirm: true }); this.setState({ contextMenuPosition: null }); // also close the menu }; @@ -439,8 +434,18 @@ export default class UserMenu extends React.Component { /> this.onSettingsOpen(e, UserTab.Security)} + label={_t("Account")} + onClick={(e) => this.onSettingsOpen(e, UserTab.Account)} + /> + this.onSettingsOpen(e, UserTab.SecureMessaging)} + /> + this.onSettingsOpen(e, UserTab.Privacy)} /> { icon = ; title = _t("Unable to verify this device"); } else { - icon = ; - title = _t("Verify this device"); + icon = ; + title = _t("Secure messaging setup"); } } else if (phase === Phase.Done) { icon = ; @@ -79,11 +79,11 @@ export default class CompleteSecurity extends React.Component { icon = ; title = _t("Are you sure?"); } else if (phase === Phase.Busy) { - icon = ; + icon = ; title = _t("Verify this device"); } else if (phase === Phase.ConfirmReset) { icon = ; - title = _t("Really reset verification keys?"); + title = _t("Are you sure?"); } else if (phase === Phase.Finished) { // SetupEncryptionBody will take care of calling onFinished, we don't need to do anything } else { diff --git a/src/components/structures/auth/ForgotPassword.tsx b/src/components/structures/auth/ForgotPassword.tsx index 58b0073c443..4d04cdf6b21 100644 --- a/src/components/structures/auth/ForgotPassword.tsx +++ b/src/components/structures/auth/ForgotPassword.tsx @@ -180,8 +180,8 @@ export default class ForgotPassword extends React.Component {

{ _t( "Changing your password will reset any end-to-end encryption keys " + - "on all of your sessions, making encrypted chat history unreadable. Set up " + - "Key Backup or export your room keys from another session before resetting your " + + "on all of your devices, making encrypted chat history unreadable. Set up " + + "Key Backup or export your message keys from another device before resetting your " + "password.", ) }
, diff --git a/src/components/structures/auth/SetupEncryptionBody.tsx b/src/components/structures/auth/SetupEncryptionBody.tsx index 2553f8fbaa0..1a92fa41034 100644 --- a/src/components/structures/auth/SetupEncryptionBody.tsx +++ b/src/components/structures/auth/SetupEncryptionBody.tsx @@ -164,15 +164,15 @@ export default class SetupEncryptionBody extends React.Component return (

{ _t( - "It looks like you don't have a Security Key or any other devices you can " + - "verify against. This device will not be able to access old encrypted messages. " + + "It looks like you don't have a recovery key or any other devices you can use to " + + "set up secure messaging. As a result, this device won't be able to access past encrypted messages. " + "In order to verify your identity on this device, you'll need to reset " + - "your verification keys.", + "secure messaging completely.", ) }

- { _t("Proceed with reset") } + { _t("Reset secure messaging") }
@@ -181,9 +181,9 @@ export default class SetupEncryptionBody extends React.Component const store = SetupEncryptionStore.sharedInstance(); let recoveryKeyPrompt; if (store.keyInfo && keyHasPassphrase(store.keyInfo)) { - recoveryKeyPrompt = _t("Verify with Security Key or Phrase"); + recoveryKeyPrompt = _t("Use recovery key or passphrase"); } else if (store.keyInfo) { - recoveryKeyPrompt = _t("Verify with Security Key"); + recoveryKeyPrompt = _t("Use recovery key"); } let useRecoveryKeyButton; @@ -196,14 +196,17 @@ export default class SetupEncryptionBody extends React.Component let verifyButton; if (store.hasDevicesToVerifyAgainst) { verifyButton = - { _t("Verify with another device") } + { _t("Use another device") } ; } return (

{ _t( - "Verify your identity to access encrypted messages and prove your identity to others.", + "Set up secure messaging on this device to access past encrypted messages and allow others to trust it.", + ) }

+

{ _t( + "Please select how you would like to do the set up.", ) }

@@ -211,7 +214,7 @@ export default class SetupEncryptionBody extends React.Component { useRecoveryKeyButton }
- { _t("Forgotten or lost all recovery methods? Reset all", null, { + { _t("Forgotten or lost all set up methods? Reset secure messaging", null, { a: (sub) => + +
+
+
+
; + } + + private renderPhaseKeepItSafe(): JSX.Element { + let introText; + if (this.state.copied) { + introText = _t( + "Your recovery key has been copied to your clipboard, paste it to:", + {}, { b: s => { s } }, ); - } else { - Modal.createTrackedDialogAsync("Key Backup", "Key Backup", - import( - "../../../async-components/views/dialogs/security/CreateKeyBackupDialog" - ) as unknown as Promise>, - null, null, /* priority = */ false, /* static = */ true, + } else if (this.state.downloaded) { + introText = _t( + "Your recovery key is in your Downloads folder.", + {}, { b: s => { s } }, ); } + return
+ { introText } +
    +
  • { _t("Print it and store it somewhere safe", {}, { b: s => { s } }) }
  • +
  • { _t("Save it on a USB key or backup drive", {}, { b: s => { s } }) }
  • +
  • { _t("Copy it to your personal cloud storage", {}, { b: s => { s } }) }
  • +
+ + + +
; + } - // close dialog - this.props.onFinished(true); - }; + private renderPhaseOptOutConfirm(): JSX.Element { + return
+ { _t( + "Without saving your secure messaging recovey key, you won't be able to restore your " + + "encrypted message history if you log out or use another device.", + ) } + + + +
; + } - private onLogoutConfirm = (): void => { - dis.dispatch({ action: 'logout' }); + private titleForPhase(phase: Phase): string { + switch (phase) { + case Phase.OptOutConfirm: + return _t('Warning!'); + case Phase.ShowKey: + case Phase.KeepItSafe: + default: + return _t('Save your secure messaging recovery key'); + } + } + private async checkRecoveryKeyState() { + const cli = MatrixClientPeg.get(); + const recoveryKey = localStorage.getItem('mx_4s_key'); + const hasUnsavedRecoveryKey = recoveryKey && + !(await cli.getAccountDataFromServer('m.secret_storage.key.export')); + const deviceCount = (await cli.getDevices()).devices.length; + this.setState({ recoveryKey, hasUnsavedRecoveryKey, deviceCount, loading: false }); + } + + private onFinished = (confirmed: boolean): void => { + if (confirmed) { + dis.dispatch({ action: 'logout' }); + } // close dialog - this.props.onFinished(true); + this.props.onFinished(confirmed); }; - render() { - if (this.state.shouldLoadBackupStatus) { - const description =
-

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

-

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

-
; - - let dialogContent; - if (this.state.loading) { - dialogContent = ; - } else { - let setupButtonCaption; - if (this.state.backupInfo) { - setupButtonCaption = _t("Connect this session to Key Backup"); - } else { - // if there's an error fetching the backup info, we'll just assume there's - // no backup for the purpose of the button caption - setupButtonCaption = _t("Start using Key Backup"); - } - - dialogContent =
-
- { description } + public render(): JSX.Element { + if (this.state.loading) { + return ( + +
+
- - - -
- { _t("Advanced") } -

-
-
; + + ); + } else if (this.state.hasUnsavedRecoveryKey) { + let content; + switch (this.state.phase) { + case Phase.ShowKey: + content = this.renderPhaseShowKey(); + break; + case Phase.KeepItSafe: + content = this.renderPhaseKeepItSafe(); + break; + case Phase.OptOutConfirm: + content = this.renderPhaseOptOutConfirm(); + break; } - // Not quite a standard question dialog as the primary button cancels - // the action and does something else instead, whilst non-default button - // confirms the action. - return ( - { dialogContent } - ); + return ( + +
+ { content } +
+
+ ); + } else if (this.props.noConfirm) { + // log out without user prompt if they have no local megolm sessions + dis.dispatch({ action: 'logout' }); } else { - return (); + return ( + + ); } } } diff --git a/src/components/views/dialogs/ManualDeviceKeyVerificationDialog.tsx b/src/components/views/dialogs/ManualDeviceKeyVerificationDialog.tsx index ef5a40a8b0c..3ee65bc6a8a 100644 --- a/src/components/views/dialogs/ManualDeviceKeyVerificationDialog.tsx +++ b/src/components/views/dialogs/ManualDeviceKeyVerificationDialog.tsx @@ -45,9 +45,9 @@ export default class ManualDeviceKeyVerificationDialog extends React.Component
    -
  • { this.props.device.getDisplayName() }
  • -
  • { this.props.device.deviceId }
  • -
  • { key }
  • +
  • { this.props.device.getDisplayName() }
  • +
  • { this.props.device.deviceId }
  • +
  • { key }

@@ -71,9 +71,9 @@ export default class ManualDeviceKeyVerificationDialog extends React.Component ); diff --git a/src/components/views/dialogs/SessionRestoreErrorDialog.tsx b/src/components/views/dialogs/SessionRestoreErrorDialog.tsx index 72b35671bac..e3297888f4e 100644 --- a/src/components/views/dialogs/SessionRestoreErrorDialog.tsx +++ b/src/components/views/dialogs/SessionRestoreErrorDialog.tsx @@ -87,15 +87,15 @@ export default class SessionRestoreErrorDialog extends React.Component {

-

{ _t("We encountered an error trying to restore your previous session.") }

+

{ _t("We encountered an error trying to use the data stored on this device.") }

{ _t( - "If you have previously used a more recent version of %(brand)s, your session " + + "If you have previously used a more recent version of %(brand)s, your data " + "may be incompatible with this version. Close this window and return " + "to the more recent version.", { brand }, @@ -103,7 +103,7 @@ export default class SessionRestoreErrorDialog extends React.Component {

{ _t( "Clearing your browser's storage may fix the problem, but will sign you " + - "out and cause any encrypted chat history to become unreadable.", + "out and may cause any encrypted chat history to become unreadable.", ) }

{ dialogButtons } diff --git a/src/components/views/dialogs/StorageEvictedDialog.tsx b/src/components/views/dialogs/StorageEvictedDialog.tsx index cd01fef2b8f..630712ce6f7 100644 --- a/src/components/views/dialogs/StorageEvictedDialog.tsx +++ b/src/components/views/dialogs/StorageEvictedDialog.tsx @@ -55,13 +55,13 @@ export default class StorageEvictedDialog extends React.Component {

{ _t( - "Some session data, including encrypted message keys, is " + + "Some data, including encrypted message keys, is " + "missing. Sign out and sign in to fix this, restoring keys " + "from backup.", ) }

diff --git a/src/components/views/dialogs/UntrustedDeviceDialog.tsx b/src/components/views/dialogs/UntrustedDeviceDialog.tsx index 8039a67511e..a04aef263d8 100644 --- a/src/components/views/dialogs/UntrustedDeviceDialog.tsx +++ b/src/components/views/dialogs/UntrustedDeviceDialog.tsx @@ -35,12 +35,12 @@ const UntrustedDeviceDialog: React.FC = ({ device, user, onFinished }) = let newSessionText; if (MatrixClientPeg.get().getUserId() === user.userId) { - newSessionText = _t("You signed in to a new session without verifying it:"); - askToVerifyText = _t("Verify your other session using one of the options below."); + newSessionText = _t("You signed in to a new device without verifying it:"); + askToVerifyText = _t("Verify your other device using one of the options below."); } else { - newSessionText = _t("%(name)s (%(userId)s) signed in to a new session without verifying it:", + newSessionText = _t("%(name)s (%(userId)s) signed in to a new device without verifying it:", { name: user.displayName, userId: user.userId }); - askToVerifyText = _t("Ask this user to verify their session, or manually verify it below."); + askToVerifyText = _t("Ask this user to verify their device, or manually verify it below."); } return { - private mjolnirWatcher: string; - constructor(props) { super(props); this.state = { - mjolnirEnabled: SettingsStore.getValue("feature_mjolnir"), }; } - public componentDidMount(): void { - this.mjolnirWatcher = SettingsStore.watchSetting("feature_mjolnir", null, this.mjolnirChanged); - } - - public componentWillUnmount(): void { - SettingsStore.unwatchSetting(this.mjolnirWatcher); - } - - private mjolnirChanged: CallbackFn = (settingName, roomId, atLevel, newValue) => { - // We can cheat because we know what levels a feature is tracked at, and how it is tracked - this.setState({ mjolnirEnabled: newValue }); - }; - private getTabs() { const tabs = []; @@ -80,11 +64,25 @@ export default class UserSettingsDialog extends React.Component "UserSettingsGeneral", )); tabs.push(new Tab( - UserTab.Appearance, - _td("Appearance"), - "mx_UserSettingsDialog_appearanceIcon", - , - "UserSettingsAppearance", + UserTab.Account, + _td("Account"), + "mx_UserSettingsDialog_settingsIcon", + , + "UserSettingsGeneral", + )); + tabs.push(new Tab( + UserTab.SecureMessaging, + _td("Secure messaging"), + "mx_UserSettingsDialog_securityIcon", + , + "UserSettingsSecurityPrivacy", + )); + tabs.push(new Tab( + UserTab.Privacy, + _td("Privacy"), + "mx_UserSettingsDialog_mjolnirIcon", + , + "UserSettingMjolnir", )); tabs.push(new Tab( UserTab.Notifications, @@ -94,18 +92,11 @@ export default class UserSettingsDialog extends React.Component "UserSettingsNotifications", )); tabs.push(new Tab( - UserTab.Preferences, - _td("Preferences"), - "mx_UserSettingsDialog_preferencesIcon", - , - "UserSettingsPreferences", - )); - tabs.push(new Tab( - UserTab.Keyboard, - _td("Keyboard"), - "mx_UserSettingsDialog_keyboardIcon", - , - "UserSettingsKeyboard", + UserTab.Appearance, + _td("Appearance"), + "mx_UserSettingsDialog_appearanceIcon", + , + "UserSettingsAppearance", )); tabs.push(new Tab( UserTab.Sidebar, @@ -115,23 +106,23 @@ export default class UserSettingsDialog extends React.Component "UserSettingsSidebar", )); + tabs.push(new Tab( + UserTab.Preferences, + _td("Preferences"), + "mx_UserSettingsDialog_preferencesIcon", + , + "UserSettingsPreferences", + )); if (SettingsStore.getValue(UIFeature.Voip)) { tabs.push(new Tab( UserTab.Voice, - _td("Voice & Video"), + _td("Audio & Video"), "mx_UserSettingsDialog_voiceIcon", , "UserSettingsVoiceVideo", )); } - tabs.push(new Tab( - UserTab.Security, - _td("Security & Privacy"), - "mx_UserSettingsDialog_securityIcon", - , - "UserSettingsSecurityPrivacy", - )); // Show the Labs tab if enabled or if there are any active betas if (SdkConfig.get("show_labs_settings") || SettingsStore.getFeatureSettingNames().some(k => SettingsStore.getBetaInfo(k)) @@ -144,15 +135,13 @@ export default class UserSettingsDialog extends React.Component "UserSettingsLabs", )); } - if (this.state.mjolnirEnabled) { - tabs.push(new Tab( - UserTab.Mjolnir, - _td("Ignored users"), - "mx_UserSettingsDialog_mjolnirIcon", - , - "UserSettingMjolnir", - )); - } + tabs.push(new Tab( + UserTab.Keyboard, + _td("Shortcuts"), + "mx_UserSettingsDialog_keyboardIcon", + , + "UserSettingsKeyboard", + )); tabs.push(new Tab( UserTab.Help, _td("Help & About"), diff --git a/src/components/views/dialogs/UserTab.ts b/src/components/views/dialogs/UserTab.ts index b5b2782c0d0..4e48181a1ed 100644 --- a/src/components/views/dialogs/UserTab.ts +++ b/src/components/views/dialogs/UserTab.ts @@ -16,14 +16,15 @@ limitations under the License. export enum UserTab { General = "USER_GENERAL_TAB", + Account = "USER_GENERAL_ACCOUNT", Appearance = "USER_APPEARANCE_TAB", Notifications = "USER_NOTIFICATIONS_TAB", Preferences = "USER_PREFERENCES_TAB", Keyboard = "USER_KEYBOARD_TAB", Sidebar = "USER_SIDEBAR_TAB", Voice = "USER_VOICE_TAB", - Security = "USER_SECURITY_TAB", + SecureMessaging = "USER_SECUREMESSAGING_TAB", Labs = "USER_LABS_TAB", - Mjolnir = "USER_MJOLNIR_TAB", + Privacy = "USER_MJOLNIR_TAB", Help = "USER_HELP_TAB", } diff --git a/src/components/views/dialogs/VerificationRequestDialog.tsx b/src/components/views/dialogs/VerificationRequestDialog.tsx index 8830ee29f7c..f450715ba8b 100644 --- a/src/components/views/dialogs/VerificationRequestDialog.tsx +++ b/src/components/views/dialogs/VerificationRequestDialog.tsx @@ -53,7 +53,7 @@ export default class VerificationRequestDialog extends React.Component

{ _t("Only do this if you have no other device to complete verification with.") }

-

{ _t("If you reset everything, you will restart with no trusted sessions, no trusted users, and " +

{ _t("If you reset everything, you will restart with no trusted devices, no trusted users, and " + "might not be able to see past messages.") }

{ _t( - "Enter your Security Phrase or to continue.", {}, + "Enter your Security Phrase or to continue.", {}, { button: s =>

; } else { - title = _t("Security Key"); + title = _t("Recovery key"); titleClass = ['mx_AccessSecretStorageDialog_titleWithIcon mx_AccessSecretStorageDialog_secureBackupTitle']; const feedbackClasses = classNames({ @@ -376,7 +377,7 @@ export default class AccessSecretStorageDialog extends React.PureComponent; content =
-

{ _t("Use your Security Key to continue.") }

+

{ _t("Use your recovery key to continue.") }

; diff --git a/src/components/views/elements/AppPermission.tsx b/src/components/views/elements/AppPermission.tsx index fd53981bca3..9f03001cae8 100644 --- a/src/components/views/elements/AppPermission.tsx +++ b/src/components/views/elements/AppPermission.tsx @@ -125,7 +125,7 @@ export default class AppPermission extends React.Component { : _t("Using this widget may share data with %(widgetDomain)s.", { widgetDomain: this.state.widgetDomain }, { helpIcon: () => warningTooltip }); - const encryptionWarning = this.props.isRoomEncrypted ? _t("Widgets do not use message encryption.") : null; + const encryptionWarning = this.props.isRoomEncrypted ? _t("Widgets cannot use secure messaging.") : null; return (
diff --git a/src/components/views/elements/DesktopBuildsNotice.tsx b/src/components/views/elements/DesktopBuildsNotice.tsx index cb664f02d01..a0996dd0624 100644 --- a/src/components/views/elements/DesktopBuildsNotice.tsx +++ b/src/components/views/elements/DesktopBuildsNotice.tsx @@ -49,7 +49,7 @@ export default function DesktopBuildsNotice({ isRoomEncrypted, kind }: IProps) { evt.preventDefault(); dis.dispatch({ action: Action.ViewUserSettings, - initialTabId: UserTab.Security, + initialTabId: UserTab.SecureMessaging, }); }} > diff --git a/src/components/views/elements/SettingsFlag.tsx b/src/components/views/elements/SettingsFlag.tsx index 3437440f00f..72d6beb7be7 100644 --- a/src/components/views/elements/SettingsFlag.tsx +++ b/src/components/views/elements/SettingsFlag.tsx @@ -54,6 +54,26 @@ export default class SettingsFlag extends React.Component { }; } + watcher: string; + + // eslint-disable-next-line @typescript-eslint/naming-convention + UNSAFE_componentWillReceiveProps(nextProps: Readonly, nextContext: any): void { + if (this.watcher) { + SettingsStore.unwatchSetting(this.watcher); + } + this.watcher = SettingsStore.watchSetting(nextProps.name, nextProps.roomId, this.settingUpdated); + } + + componentWillUnmount(): void { + if (this.watcher) { + SettingsStore.unwatchSetting(this.watcher); + } + } + + private settingUpdated = (_a, _b, _c, _d, newVal: any) => { + this.setState({ value: newVal }); + }; + private onChange = async (checked: boolean) => { await this.save(checked); this.setState({ value: checked }); diff --git a/src/components/views/messages/EncryptionEvent.tsx b/src/components/views/messages/EncryptionEvent.tsx index 809cf75f760..78c16b775ee 100644 --- a/src/components/views/messages/EncryptionEvent.tsx +++ b/src/components/views/messages/EncryptionEvent.tsx @@ -47,19 +47,19 @@ const EncryptionEvent = forwardRef(({ mxEvent, timestamp let subtitle: string; const dmPartner = DMRoomMap.shared().getUserIdForRoomId(roomId); if (prevContent.algorithm === ALGORITHM) { - subtitle = _t("Some encryption parameters have been changed."); + subtitle = _t("Some secure messaging encryption parameters have been changed."); } else if (dmPartner) { const displayName = cli?.getRoom(roomId)?.getMember(dmPartner)?.rawDisplayName || dmPartner; - subtitle = _t("Messages here are end-to-end encrypted. " + + subtitle = _t("Messages here are secured by end-to-end encrypted. " + "Verify %(displayName)s in their profile - tap on their avatar.", { displayName }); } else { - subtitle = _t("Messages in this room are end-to-end encrypted. " + + subtitle = _t("Messages in this room are secured by end-to-end encrypted. " + "When people join, you can verify them in their profile, just tap on their avatar."); } return ; @@ -69,15 +69,15 @@ const EncryptionEvent = forwardRef(({ mxEvent, timestamp return ; } return ; diff --git a/src/components/views/right_panel/EncryptionInfo.tsx b/src/components/views/right_panel/EncryptionInfo.tsx index 7525dfd2082..3f5da547442 100644 --- a/src/components/views/right_panel/EncryptionInfo.tsx +++ b/src/components/views/right_panel/EncryptionInfo.tsx @@ -49,12 +49,24 @@ const EncryptionInfo: React.FC = ({ isSelfVerification, }: IProps) => { let content: JSX.Element; - if (waitingForOtherParty && isSelfVerification) { - content = ( -
- { _t("To proceed, please accept the verification request on your other device.") } -
- ); + if (isSelfVerification) { + if (waitingForOtherParty) { + content = ( +
+ { _t("Please check your other device(s) and accept the request to set up secure messaging.") } +
+ ); + } else { + content = ( + + { _t("Set up secure messaging for new device") } + + ); + } } else if (waitingForOtherParty || waitingForNetwork) { let text: string; if (waitingForOtherParty) { @@ -72,7 +84,7 @@ const EncryptionInfo: React.FC = ({ className="mx_UserInfo_wideButton mx_UserInfo_startVerification" onClick={onStartVerification} > - { _t("Start Verification") } + { _t("Start verification") } ); } diff --git a/src/components/views/right_panel/EncryptionPanel.tsx b/src/components/views/right_panel/EncryptionPanel.tsx index c2dfd7a379b..6247444c5b5 100644 --- a/src/components/views/right_panel/EncryptionPanel.tsx +++ b/src/components/views/right_panel/EncryptionPanel.tsx @@ -93,7 +93,7 @@ const EncryptionPanel: React.FC = (props: IProps) => {
  • { _t("Your homeserver") }
  • { _t("The homeserver the user you're verifying is connected to") }
  • { _t("Yours, or the other users' internet connection") }
  • -
  • { _t("Yours, or the other users' session") }
  • +
  • { _t("Yours, or the other users' device") }
  • , onFinished: onClose, diff --git a/src/components/views/right_panel/UserInfo.tsx b/src/components/views/right_panel/UserInfo.tsx index 02181968316..f0426ce373c 100644 --- a/src/components/views/right_panel/UserInfo.tsx +++ b/src/components/views/right_panel/UserInfo.tsx @@ -250,7 +250,7 @@ function DevicesSection({ devices, userId, loading }: {devices: IDevice[], userI return ; } if (devices === null) { - return <>{ _t("Unable to load session list") }; + return <>{ _t("Unable to load device list") }; } const isMe = userId === cli.getUserId(); const deviceTrusts = devices.map(d => cli.checkDeviceTrust(userId, d.deviceId)); @@ -279,13 +279,13 @@ function DevicesSection({ devices, userId, loading }: {devices: IDevice[], userI unverifiedDevices.push(device); } } - expandCountCaption = _t("%(count)s verified sessions", { count: expandSectionDevices.length }); - expandHideCaption = _t("Hide verified sessions"); + expandCountCaption = _t("%(count)s verified devices", { count: expandSectionDevices.length }); + expandHideCaption = _t("Hide verified devices"); expandIconClasses += " mx_E2EIcon_verified"; } else { expandSectionDevices = devices; - expandCountCaption = _t("%(count)s sessions", { count: devices.length }); - expandHideCaption = _t("Hide sessions"); + expandCountCaption = _t("%(count)s devices", { count: devices.length }); + expandHideCaption = _t("Hide devices"); expandIconClasses += " mx_E2EIcon_normal"; } @@ -1261,12 +1261,12 @@ const BasicUserInfo: React.FC<{ let text; if (!isRoomEncrypted) { if (!cryptoEnabled) { - text = _t("This client does not support end-to-end encryption."); + text = _t("This client does not support secure messaging."); } else if (room && !room.isSpaceRoom()) { - text = _t("Messages in this room are not end-to-end encrypted."); + text = _t("Secure messaging is not enabled for this room."); } } else if (!room.isSpaceRoom()) { - text = _t("Messages in this room are end-to-end encrypted."); + text = _t("Messages in this room are sent securely using end-to-end encryption."); } let verifyButton; @@ -1298,7 +1298,7 @@ const BasicUserInfo: React.FC<{ } }} > - { _t("Verify") } + { _t("Verify this user") } ); } else if (!showDeviceListSpinner) { @@ -1317,7 +1317,7 @@ const BasicUserInfo: React.FC<{ onClick={() => { dis.dispatch({ action: Action.ViewUserSettings, - initialTabId: UserTab.Security, + initialTabId: UserTab.SecureMessaging, }); }} > diff --git a/src/components/views/rooms/E2EIcon.tsx b/src/components/views/rooms/E2EIcon.tsx index 1a6db4606c9..a57f3e41629 100644 --- a/src/components/views/rooms/E2EIcon.tsx +++ b/src/components/views/rooms/E2EIcon.tsx @@ -32,12 +32,12 @@ export enum E2EState { } const crossSigningUserTitles: { [key in E2EState]?: string } = { - [E2EState.Warning]: _td("This user has not verified all of their sessions."), + [E2EState.Warning]: _td("This user has not verified all of their devices."), [E2EState.Normal]: _td("You have not verified this user."), - [E2EState.Verified]: _td("You have verified this user. This user has verified all of their sessions."), + [E2EState.Verified]: _td("You have verified this user. This user has verified all of their devices."), }; const crossSigningRoomTitles: { [key in E2EState]?: string } = { - [E2EState.Warning]: _td("Someone is using an unknown session"), + [E2EState.Warning]: _td("Someone is using an unknown device"), [E2EState.Normal]: _td("This room is end-to-end encrypted"), [E2EState.Verified]: _td("Everyone in this room is verified"), }; diff --git a/src/components/views/rooms/EventTile.tsx b/src/components/views/rooms/EventTile.tsx index f886482fbaf..155420d796a 100644 --- a/src/components/views/rooms/EventTile.tsx +++ b/src/components/views/rooms/EventTile.tsx @@ -74,6 +74,8 @@ import { ViewRoomPayload } from "../../../dispatcher/payloads/ViewRoomPayload"; import { shouldDisplayReply } from '../../../utils/Reply'; import PosthogTrackers from "../../../PosthogTrackers"; import TileErrorBoundary from '../messages/TileErrorBoundary'; +import Modal from '../../../Modal'; +import SetupEncryptionDialog from '../dialogs/security/SetupEncryptionDialog'; import { haveRendererForEvent, isMessageEvent, renderTile } from "../../../events/EventTileFactory"; import ThreadSummary, { ThreadMessagePreview } from "./ThreadSummary"; @@ -224,6 +226,8 @@ interface IState { thread: Thread; threadNotification?: NotificationCountType; + + userHasSecureMessagingSetup: boolean; } // MUST be rendered within a RoomContext with a set timelineRenderingType @@ -268,6 +272,8 @@ export class UnwrappedEventTile extends React.Component { hover: false, thread, + + userHasSecureMessagingSetup: false, }; // don't do RR animations until we are mounted @@ -372,6 +378,9 @@ export class UnwrappedEventTile extends React.Component { } } + void this.updateCryptoSetupState(); + client.on(CryptoEvent.KeyBackupStatus, this.updateCryptoSetupState); + if (SettingsStore.getValue("feature_thread")) { this.props.mxEvent.on(ThreadEvent.Update, this.updateThread); @@ -456,6 +465,8 @@ export class UnwrappedEventTile extends React.Component { this.props.mxEvent.off(ThreadEvent.Update, this.updateThread); } + client.removeListener(CryptoEvent.KeyBackupStatus, this.updateCryptoSetupState); + const room = MatrixClientPeg.get().getRoom(this.props.mxEvent.getRoomId()); room?.off(ThreadEvent.New, this.onNewThread); if (this.threadState) { @@ -585,6 +596,14 @@ export class UnwrappedEventTile extends React.Component { } }; + private async updateCryptoSetupState() { + const client = MatrixClientPeg.get(); + const userHasSecureMessagingSetup = + await client.isCrossSigningReady() && await client.isSecretStorageReady(); + + this.setState({ userHasSecureMessagingSetup }); + } + private onUserVerificationChanged = (userId: string, _trustStatus: UserTrustLevel): void => { if (userId === this.props.mxEvent.getSender()) { this.verifyEvent(this.props.mxEvent); @@ -834,6 +853,11 @@ export class UnwrappedEventTile extends React.Component { MatrixClientPeg.get().cancelAndResendEventRoomKeyRequest(this.props.mxEvent); }; + private onSetupSecureMessagingClick = () => { + Modal.createTrackedDialog("Verify session", "Verify session", SetupEncryptionDialog, + {}, null, /* priority = */ false, /* static = */ true); + }; + private onPermalinkClicked = e => { // This allows the permalink to be opened in a new tab/window or copied as // matrix.to, but also for it to enable routing within Element when clicked. @@ -1141,48 +1165,73 @@ export class UnwrappedEventTile extends React.Component { const timestamp = showTimestamp && ts ? messageTimestamp : null; - const keyRequestHelpText = -
    -

    - { this.state.previouslyRequestedKeys ? - _t('Your key share request has been sent - please check your other sessions ' + - 'for key share requests.') : - _t('Key share requests are sent to your other sessions automatically. If you ' + - 'rejected or dismissed the key share request on your other sessions, click ' + - 'here to request the keys for this session again.') - } -

    -

    - { _t('If your other sessions do not have the key for this message you will not ' + - 'be able to decrypt them.') - } -

    -
    ; - const keyRequestInfoContent = this.state.previouslyRequestedKeys ? - _t('Key request sent.') : - _t( - 'Re-request encryption keys from your other sessions.', - {}, - { - 'requestLink': (sub) => - - { sub } - , - }, - ); - - const keyRequestInfo = isEncryptionFailure && !isRedacted ? -
    - - { keyRequestInfoContent } - - -
    : null; + let keyRequestInfo; + if (isEncryptionFailure && !isRedacted) { + if (this.state.userHasSecureMessagingSetup) { + const keyRequestHelpText = +
    +

    + { this.state.previouslyRequestedKeys ? + _t('Your key share request has been sent - please check your other devices ' + + 'for key share requests.') : + _t('Key share requests are sent to your other devices automatically. If you ' + + 'rejected or dismissed the key share request on your other devices, click ' + + 'here to request the keys for this device again.') + } +

    +

    + { _t('If your other devices do not have the key for this message you will not ' + + 'be able to decrypt them.') + } +

    +
    ; + const keyRequestInfoContent = this.state.previouslyRequestedKeys ? + _t('Key request sent.') : + _t( + 'Re-request encryption keys from your other devices.', + {}, + { + 'requestLink': (sub) => + + { sub } + , + }, + ); + keyRequestInfo = +
    + + { keyRequestInfoContent } + + +
    ; + } else { + keyRequestInfo = +
    + + { _t( + 'Set up secure messaging to access this message.', + {}, + { + 'requestLink': (sub) => + + { sub } + , + }, + ) } + +
    ; + } + } let reactionsRow; if (!isRedacted) { @@ -1484,7 +1533,7 @@ function E2ePadlockUndecryptable(props) { function E2ePadlockUnverified(props) { return ( - + ); } @@ -1496,7 +1545,7 @@ function E2ePadlockUnencrypted(props) { function E2ePadlockUnknown(props) { return ( - + ); } diff --git a/src/components/views/rooms/HistoryTile.tsx b/src/components/views/rooms/HistoryTile.tsx index 9247ead012e..737008b89ce 100644 --- a/src/components/views/rooms/HistoryTile.tsx +++ b/src/components/views/rooms/HistoryTile.tsx @@ -34,7 +34,7 @@ const HistoryTile = () => { } else if (historyState == "joined") { subtitle = _t("You don't have permission to view messages from before you joined."); } else if (encryptionState) { - subtitle = _t("Encrypted messages before this point are unavailable."); + subtitle = _t("Secure messages before this point are unavailable."); } return { let subButton; if (room.currentState.mayClientSendStateEvent(EventType.RoomEncryption, MatrixClientPeg.get())) { subButton = ( - { _t("Enable encryption in settings.") } + { _t("Enable secure messaging in settings.") } ); } @@ -220,7 +220,7 @@ const NewRoomIntro = () => { { !hasExpectedEncryptionSettings(cli, room) && ( ) } diff --git a/src/components/views/settings/DevicesPanel.tsx b/src/components/views/settings/AccountDevicesPanel.tsx similarity index 79% rename from src/components/views/settings/DevicesPanel.tsx rename to src/components/views/settings/AccountDevicesPanel.tsx index 4779c013be5..b52ddb32c68 100644 --- a/src/components/views/settings/DevicesPanel.tsx +++ b/src/components/views/settings/AccountDevicesPanel.tsx @@ -18,7 +18,6 @@ import React from 'react'; import classNames from 'classnames'; import { IMyDevice } from "matrix-js-sdk/src/client"; import { logger } from "matrix-js-sdk/src/logger"; -import { CrossSigningInfo } from "matrix-js-sdk/src/crypto/CrossSigning"; import { MatrixClientPeg } from '../../../MatrixClientPeg'; import { _t } from '../../../languageHandler'; @@ -35,13 +34,12 @@ interface IProps { interface IState { devices: IMyDevice[]; - crossSigningInfo?: CrossSigningInfo; deviceLoadError?: string; selectedDevices: string[]; deleting?: boolean; } -export default class DevicesPanel extends React.Component { +export default class AccountDevicesPanel extends React.Component { private unmounted = false; constructor(props: IProps) { @@ -112,22 +110,6 @@ export default class DevicesPanel extends React.Component { return (idA < idB) ? -1 : (idA > idB) ? 1 : 0; } - private isDeviceVerified(device: IMyDevice): boolean | null { - try { - const cli = MatrixClientPeg.get(); - const deviceInfo = cli.getStoredDevice(cli.getUserId(), device.device_id); - return this.state.crossSigningInfo.checkDeviceTrust( - this.state.crossSigningInfo, - deviceInfo, - false, - true, - ).isCrossSigningVerified(); - } catch (e) { - console.error("Error getting device cross-signing info", e); - return null; - } - } - private onDeviceSelectionToggled = (device: IMyDevice): void => { if (this.unmounted) { return; } @@ -248,23 +230,21 @@ export default class DevicesPanel extends React.Component { private renderDevice = (device: IMyDevice): JSX.Element => { const myDeviceId = MatrixClientPeg.get().getDeviceId(); - const myDevice = this.state.devices.find((device) => (device.device_id === myDeviceId)); const isOwnDevice = device.device_id === myDeviceId; - // If our own device is unverified, it can't verify other - // devices, it can only request verification for itself - const canBeVerified = (myDevice && this.isDeviceVerified(myDevice)) || isOwnDevice; - return ; }; @@ -294,21 +274,7 @@ export default class DevicesPanel extends React.Component { const otherDevices = devices.filter((device) => (device.device_id !== myDeviceId)); otherDevices.sort(this.deviceCompare); - const verifiedDevices = []; - const unverifiedDevices = []; - const nonCryptoDevices = []; - for (const device of otherDevices) { - const verified = this.isDeviceVerified(device); - if (verified === true) { - verifiedDevices.push(device); - } else if (verified === false) { - unverifiedDevices.push(device); - } else { - nonCryptoDevices.push(device); - } - } - - const section = (trustIcon: JSX.Element, title: string, deviceList: IMyDevice[]): JSX.Element => { + const section = (deviceList: IMyDevice[]): JSX.Element => { if (deviceList.length === 0) { return ; } @@ -333,35 +299,13 @@ export default class DevicesPanel extends React.Component { return
    -
    -
    - { trustIcon } -
    -
    - { title } -
    - { selectButton } -
    + { selectButton } { deviceList.map(this.renderDevice) }
    ; }; - const verifiedDevicesSection = section( - , - _t("Verified devices"), - verifiedDevices, - ); - - const unverifiedDevicesSection = section( - , - _t("Unverified devices"), - unverifiedDevices, - ); - - const nonCryptoDevicesSection = section( - , - _t("Devices without encryption support"), - nonCryptoDevices, + const otherDevicesSection = section( + otherDevices, ); const deleteButton = this.state.deleting ? @@ -375,17 +319,15 @@ export default class DevicesPanel extends React.Component { { _t("Sign out %(count)s selected devices", { count: this.state.selectedDevices.length }) } ; - const otherDevicesSection = (otherDevices.length > 0) ? + const devicesSection = (otherDevices.length > 0) ? - { verifiedDevicesSection } - { unverifiedDevicesSection } - { nonCryptoDevicesSection } + { otherDevicesSection } { deleteButton } :
    - { _t("You aren't signed into any other devices.") } + { _t("You aren't signed in to any other devices.") }
    ; @@ -398,7 +340,7 @@ export default class DevicesPanel extends React.Component {
    { this.renderDevice(myDevice) } - { otherDevicesSection } + { devicesSection }
    ); } diff --git a/src/components/views/settings/ChangePassword.tsx b/src/components/views/settings/ChangePassword.tsx index f736e5f6f51..17e4b7f34b0 100644 --- a/src/components/views/settings/ChangePassword.tsx +++ b/src/components/views/settings/ChangePassword.tsx @@ -95,8 +95,8 @@ export default class ChangePassword extends React.Component { description:
    { _t( - 'Changing password will currently reset any end-to-end encryption keys on all sessions, ' + - 'making encrypted chat history unreadable, unless you first export your room keys ' + + 'Changing password will currently reset any end-to-end encryption keys on all devices, ' + + 'making encrypted chat history unreadable, unless you first export your messaage keys ' + 'and re-import them afterwards. ' + 'In future this will be improved.', ) } @@ -112,7 +112,7 @@ export default class ChangePassword extends React.Component { className="mx_Dialog_primary" onClick={this.onExportE2eKeysClicked} > - { _t('Export E2E room keys') } + { _t('Export message keys') } , ], onFinished: (confirmed) => { diff --git a/src/components/views/settings/CrossSigningPanel.tsx b/src/components/views/settings/CrossSigningPanel.tsx index 5c6e650c9fb..9fa54a8ba34 100644 --- a/src/components/views/settings/CrossSigningPanel.tsx +++ b/src/components/views/settings/CrossSigningPanel.tsx @@ -197,7 +197,7 @@ export default class CrossSigningPanel extends React.PureComponent<{}, IState> { } else if (crossSigningPrivateKeysInStorage) { summarisedStatus =

    { _t( "Your account has a cross-signing identity in secret storage, " + - "but it is not yet trusted by this session.", + "but it is not yet trusted by this device.", ) }

    ; } else { summarisedStatus =

    { _t( @@ -226,7 +226,7 @@ export default class CrossSigningPanel extends React.PureComponent<{}, IState> { if (!keysExistEverywhere && homeserverSupportsCrossSigning) { let buttonCaption = _t("Set up Secure Backup"); if (crossSigningPrivateKeysInStorage) { - buttonCaption = _t("Verify this session"); + buttonCaption = _t("Verify this device"); } actions.push( diff --git a/src/components/views/settings/CryptographyPanel.tsx b/src/components/views/settings/CryptographyPanel.tsx index dd580058afb..6591e6d4b5e 100644 --- a/src/components/views/settings/CryptographyPanel.tsx +++ b/src/components/views/settings/CryptographyPanel.tsx @@ -21,9 +21,6 @@ import { _t } from '../../../languageHandler'; import Modal from '../../../Modal'; import AccessibleButton from "../elements/AccessibleButton"; import * as FormattingUtils from "../../../utils/FormattingUtils"; -import SettingsStore from "../../../settings/SettingsStore"; -import SettingsFlag from "../elements/SettingsFlag"; -import { SettingLevel } from "../../../settings/SettingLevel"; interface IProps { } @@ -51,24 +48,15 @@ export default class CryptographyPanel extends React.Component { importExportButtons = (

    - { _t("Export E2E room keys") } + { _t("Export message keys") } - { _t("Import E2E room keys") } + { _t("Import message keys") }
    ); } - let noSendUnverifiedSetting; - if (SettingsStore.isEnabled("blacklistUnverifiedDevices")) { - noSendUnverifiedSetting = ; - } - return (
    { _t("Cryptography") } @@ -85,7 +73,6 @@ export default class CryptographyPanel extends React.Component { { importExportButtons } - { noSendUnverifiedSetting }
    ); } @@ -107,8 +94,4 @@ export default class CryptographyPanel extends React.Component { { matrixClient: MatrixClientPeg.get() }, ); }; - - private updateBlacklistDevicesFlag = (checked): void => { - MatrixClientPeg.get().setGlobalBlacklistUnverifiedDevices(checked); - }; } diff --git a/src/components/views/settings/DevicesPanelEntry.tsx b/src/components/views/settings/DevicesPanelEntry.tsx index 33a5939aa20..d0766ea9220 100644 --- a/src/components/views/settings/DevicesPanelEntry.tsx +++ b/src/components/views/settings/DevicesPanelEntry.tsx @@ -39,6 +39,9 @@ interface IProps { onDeviceChange: () => void; onDeviceToggled: (device: IMyDevice) => void; selected: boolean; + canSignOut: boolean; + canRename: boolean; + canSelect: boolean; } interface IState { @@ -74,7 +77,7 @@ export default class DevicesPanelEntry extends React.Component { await MatrixClientPeg.get().setDeviceDetails(this.props.device.device_id, { display_name: this.state.displayName, }).catch((e) => { - logger.error("Error setting session display name", e); + logger.error("Error setting device display name", e); throw new Error(_t("Failed to set display name")); }); this.props.onDeviceChange(); @@ -134,19 +137,19 @@ export default class DevicesPanelEntry extends React.Component { iconClass = this.props.verified ? "mx_E2EIcon_verified" : "mx_E2EIcon_warning"; if (!this.props.verified && this.props.canBeVerified) { verifyButton = - { _t("Verify") } + { _t("Set up for secure messaging") } ; } } let signOutButton: JSX.Element; - if (this.props.isOwnDevice) { + if (this.props.isOwnDevice && this.props.canSignOut) { signOutButton = { _t("Sign Out") } ; } - const left = this.props.isOwnDevice ? + const left = this.props.isOwnDevice || ! this.props.canSelect ?
    : @@ -164,6 +167,11 @@ export default class DevicesPanelEntry extends React.Component { { device.device_id } ; + const renameButton = this.props.canRename ? + + { _t("Rename") } + : null; + const buttons = this.state.renaming ? { { signOutButton } { verifyButton } - - { _t("Rename") } - + { renameButton } ; return ( diff --git a/src/components/views/settings/E2eAdvancedPanel.tsx b/src/components/views/settings/E2eAdvancedPanel.tsx index ebb778deb40..eec3495bf99 100644 --- a/src/components/views/settings/E2eAdvancedPanel.tsx +++ b/src/components/views/settings/E2eAdvancedPanel.tsx @@ -17,27 +17,43 @@ limitations under the License. import React from 'react'; import { _t } from "../../../languageHandler"; +import { MatrixClientPeg } from '../../../MatrixClientPeg'; import { SettingLevel } from "../../../settings/SettingLevel"; import SettingsStore from "../../../settings/SettingsStore"; import SettingsFlag from '../elements/SettingsFlag'; const SETTING_MANUALLY_VERIFY_ALL_SESSIONS = "e2ee.manuallyVerifyAllSessions"; -const E2eAdvancedPanel = props => { - return
    - { _t("Encryption") } +function updateBlacklistDevicesFlag(checked: boolean): void { + MatrixClientPeg.get().setGlobalBlacklistUnverifiedDevices(checked); +} - { + const blacklistUnverifiedDevices = SettingsStore.isEnabled("blacklistUnverifiedDevices") ? + -
    { _t( - "Individually verify each session used by a user to mark it as trusted, not trusting cross-signed devices.", - ) }
    + onChange={updateBlacklistDevicesFlag} + /> : null; + + const manuallyVerifyAllSessions = SettingsStore.isEnabled(SETTING_MANUALLY_VERIFY_ALL_SESSIONS) ? + <> + +
    { _t( + "Individually verify each device used by a user to mark it as trusted, not trusting cross-signed devices.", + ) }
    + : null; + + return
    + { manuallyVerifyAllSessions } + { blacklistUnverifiedDevices }
    ; }; export default E2eAdvancedPanel; export function isE2eAdvancedPanelPossible(): boolean { - return SettingsStore.isEnabled(SETTING_MANUALLY_VERIFY_ALL_SESSIONS); + return SettingsStore.isEnabled(SETTING_MANUALLY_VERIFY_ALL_SESSIONS) || SettingsStore.isEnabled("blacklistUnverifiedDevices"); } diff --git a/src/components/views/settings/E2eDevicesPanel.tsx b/src/components/views/settings/E2eDevicesPanel.tsx new file mode 100644 index 00000000000..376176528db --- /dev/null +++ b/src/components/views/settings/E2eDevicesPanel.tsx @@ -0,0 +1,254 @@ +/* +Copyright 2016 - 2021 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 classNames from 'classnames'; +import { IMyDevice } from "matrix-js-sdk/src/client"; +import { logger } from "matrix-js-sdk/src/logger"; +import { CrossSigningInfo } from "matrix-js-sdk/src/crypto/CrossSigning"; + +import { MatrixClientPeg } from '../../../MatrixClientPeg'; +import { _t } from '../../../languageHandler'; +import Spinner from "../elements/Spinner"; +import AccessibleButton from "../elements/AccessibleButton"; +import Modal from '../../../Modal'; +import SetupEncryptionDialog from '../dialogs/security/SetupEncryptionDialog'; + +interface IProps { + className?: string; +} + +interface IState { + devices: IMyDevice[]; + crossSigningInfo?: CrossSigningInfo; + deviceLoadError?: string; + selectedDevices: string[]; + hasRecoveryKey: boolean; +} + +export default class E2eDevicesPanel extends React.Component { + private unmounted = false; + + constructor(props: IProps) { + super(props); + this.state = { + devices: [], + selectedDevices: [], + hasRecoveryKey: !!localStorage.getItem('mx_4s_key'), + }; + this.loadDevices = this.loadDevices.bind(this); + } + + public componentDidMount(): void { + this.loadDevices(); + } + + public componentWillUnmount(): void { + this.unmounted = true; + } + + private loadDevices(): void { + const cli = MatrixClientPeg.get(); + cli.getDevices().then( + (resp) => { + if (this.unmounted) { return; } + + const crossSigningInfo = cli.getStoredCrossSigningForUser(cli.getUserId()); + this.setState((state, props) => { + const deviceIds = resp.devices.map((device) => device.device_id); + const selectedDevices = state.selectedDevices.filter( + (deviceId) => deviceIds.includes(deviceId), + ); + return { + devices: resp.devices || [], + selectedDevices, + crossSigningInfo: crossSigningInfo, + }; + }); + console.log(this.state); + }, + (error) => { + if (this.unmounted) { return; } + let errtxt; + if (error.httpStatus == 404) { + // 404 probably means the HS doesn't yet support the API. + errtxt = _t("Your homeserver does not support device management."); + } else { + logger.error("Error loading sessions:", error); + errtxt = _t("Unable to load device list"); + } + this.setState({ deviceLoadError: errtxt }); + }, + ); + } + + /* + * compare two devices, sorting from most-recently-seen to least-recently-seen + * (and then, for stability, by device id) + */ + private deviceCompare(a: IMyDevice, b: IMyDevice): number { + // return < 0 if a comes before b, > 0 if a comes after b. + const lastSeenDelta = + (b.last_seen_ts || 0) - (a.last_seen_ts || 0); + + if (lastSeenDelta !== 0) { return lastSeenDelta; } + + const idA = a.device_id; + const idB = b.device_id; + return (idA < idB) ? -1 : (idA > idB) ? 1 : 0; + } + + private isDeviceVerified(device: IMyDevice): boolean | null { + try { + const cli = MatrixClientPeg.get(); + const deviceInfo = cli.getStoredDevice(cli.getUserId(), device.device_id); + return this.state.crossSigningInfo.checkDeviceTrust( + this.state.crossSigningInfo, + deviceInfo, + false, + true, + ).isCrossSigningVerified(); + } catch (e) { + console.error("Error getting device cross-signing info", e); + return null; + } + } + + private async setupThisDevice() { + const { finished } = Modal.createTrackedDialog("Verify session", "Verify session", SetupEncryptionDialog); + await finished; + } + + private notImplemented() { + alert(`Not implemented. But here is the recovery key: ${localStorage.getItem('mx_4s_key')}`); + } + + private async deleteRecoveryKey() { + const cli = MatrixClientPeg.get(); + if (!await cli.getAccountDataFromServer('m.secret_storage.key.export')) { + alert(`You need to save your recovery key before you can remove it from this device.`); + return; + } + const x = confirm(`Are you sure you want to remove the key from this device?`); + if (x) { + localStorage.removeItem('mx_4s_key'); + this.setState({ hasRecoveryKey: false }); + } + } + + public render(): JSX.Element { + const loadError = ( +
    + { this.state.deviceLoadError } +
    + ); + + if (this.state.deviceLoadError !== undefined) { + return loadError; + } + + const devices = this.state.devices; + if (devices === undefined) { + // still loading + return ; + } + + const myDeviceId = MatrixClientPeg.get().getDeviceId(); + const myDevice = devices.find((device) => (device.device_id === myDeviceId)); + if (!myDevice) { + return loadError; + } + + const otherDevices = devices.filter((device) => (device.device_id !== myDeviceId)); + otherDevices.sort(this.deviceCompare); + + const verifiedDevices: IMyDevice[] = []; + const unverifiedDevices: IMyDevice[] = []; + const nonCryptoDevices: IMyDevice[] = []; + for (const device of otherDevices) { + const verified = this.isDeviceVerified(device); + if (verified === true) { + verifiedDevices.push(device); + } else if (verified === false) { + unverifiedDevices.push(device); + } else { + nonCryptoDevices.push(device); + } + } + + const { hasRecoveryKey } = this.state; + + const classes = classNames(this.props.className, "mx_DevicesPanel"); + return ( +
    +
    +
    + { _t("This device") } +
    +
    + { this.isDeviceVerified(myDevice) ? + +

    + { _t("Secure messaging is set up on this device.") } + { !hasRecoveryKey ? null : + Your recovery key is stored on this device for safe keeping. + } +

    + { !hasRecoveryKey ? null : + + { _t("Save recovery key") } + + } + { !hasRecoveryKey ? null : +
    + + { _t("Delete recovery key from this device") } + +
    + } +
    + : + +

    { _t("Secure messaging is not set up on this device. Set up secure messaging to access past encrypted messages and allow others to trust it.") }

    + + { _t("Set up now") } + +
    + } +
    +
    + { _t("Other devices") } +
    +
    + { (otherDevices.length > 0) ? + + { verifiedDevices.length === 0 ? null : +

    You have x.device_id).join(' ')}>{ verifiedDevices.length } other device{ verifiedDevices.length > 1 ? 's' : '' } that are setup for secure messaging.

    + } + { unverifiedDevices.length === 0 ? null : +

    You have x.device_id).join(' ')}>{ unverifiedDevices.length } other device{ unverifiedDevices.length > 1 ? 's' : '' } that { unverifiedDevices.length > 1 ? 'are' : 'is' } not setup for secure messaging.

    + } + { nonCryptoDevices.length === 0 ? null : +

    You have x.device_id).join(' ')}>{ nonCryptoDevices.length } other device{ nonCryptoDevices.length > 1 ? 's' : '' } that do{ nonCryptoDevices.length > 1 ? '' : 'es' } not support secure messaging.

    + } +
    + : +

    { _t("You aren't signed in to any other devices.") }

    + } +
    + ); + } +} diff --git a/src/components/views/settings/E2eTrustPanel.tsx b/src/components/views/settings/E2eTrustPanel.tsx new file mode 100644 index 00000000000..32fe30a1e7b --- /dev/null +++ b/src/components/views/settings/E2eTrustPanel.tsx @@ -0,0 +1,162 @@ +/* +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 { _t } from "../../../languageHandler"; +import { MatrixClientPeg } from '../../../MatrixClientPeg'; +import { SettingLevel } from "../../../settings/SettingLevel"; +import SettingsStore from "../../../settings/SettingsStore"; +import SettingsFlag from '../elements/SettingsFlag'; +import Slider from '../elements/Slider'; + +const SETTING_MANUALLY_VERIFY_ALL_SESSIONS = "e2ee.manuallyVerifyAllSessions"; + +function updateBlacklistDevicesFlag(checked: boolean): void { + MatrixClientPeg.get().setGlobalBlacklistUnverifiedDevices(checked); +} + +const levelNames = [ + _t('Lax'), + _t('Default'), + _t('Strict'), + _t('Paranoid'), +]; + +const levelDescriptions = [ + _t('Secure messages will be sent to all recipients and devices irrespective of whether they have verified them.'), + _t('Secure messages will only be sent to devices of a recipient where the recipient has completed verification of that device.'), + _t('Secure messages will only be sent to recipients whom you have completed verification with.'), + _t('Secure messages will only be sent to recipient devices that you have verified yourself.'), +]; + +async function set(value: number) { + let settings: [boolean, boolean, boolean] | undefined; + + switch (value) { + case 0: + settings = [false, false, false]; + break; + case 1: + settings = [true, false, false]; + break; + case 2: + settings = [true, true, false]; + break; + case 3: + settings = [true, true, true]; + break; + } + + const [blacklistNonCrossSignedDevices, blacklistUnverifiedDevices, manuallyVerifyAllSessions] = settings; + if (settings) { + await SettingsStore.setValue('e2ee.blacklistNonCrossSignedDevices', null, SettingLevel.DEVICE, blacklistNonCrossSignedDevices); + await SettingsStore.setValue('blacklistUnverifiedDevices', null, SettingLevel.DEVICE, blacklistUnverifiedDevices); + await SettingsStore.setValue(SETTING_MANUALLY_VERIFY_ALL_SESSIONS, null, SettingLevel.DEVICE, manuallyVerifyAllSessions); + } +} + +function get(): number { + const blacklistNonCrossSignedDevices = SettingsStore.getValueAt( + SettingLevel.DEVICE, + 'e2ee.blacklistNonCrossSignedDevices', + ); + + const blacklistUnverifiedDevices = SettingsStore.getValueAt( + SettingLevel.DEVICE, + 'blacklistUnverifiedDevices', + ); + + const manuallyVerifyAllSessions = SettingsStore.getValueAt( + SettingLevel.DEVICE, + SETTING_MANUALLY_VERIFY_ALL_SESSIONS, + ); + + if (blacklistUnverifiedDevices && manuallyVerifyAllSessions && blacklistNonCrossSignedDevices) { + return 3; + } else if (blacklistUnverifiedDevices && blacklistNonCrossSignedDevices) { + return 2; + } else if (blacklistNonCrossSignedDevices && !blacklistUnverifiedDevices && !manuallyVerifyAllSessions) { + return 1; + } else if (!blacklistNonCrossSignedDevices && !blacklistUnverifiedDevices && !manuallyVerifyAllSessions) { + return 0; + } + + return 4; +} + +interface IProps { +} + +interface IState { + value: number; +} + +export default class E2eTrustPanel extends React.Component { + constructor(props: IProps) { + super(props); + this.state = { + value: get(), + }; + } + + watcherReferences: string[] = []; + + componentDidMount(): void { + this.watcherReferences = ['e2ee.blacklistNonCrossSignedDevices', 'blacklistUnverifiedDevices', SETTING_MANUALLY_VERIFY_ALL_SESSIONS] + .map(x => SettingsStore.watchSetting(x, null, this.updateValue)); + } + + componentWillUnmount(): void { + this.watcherReferences.forEach(x => SettingsStore.unwatchSetting(x)); + } + + updateValue = () => { + this.setState({ + value: get(), + }); + }; + + render() { + return
    + i)} + value={this.state.value} + onSelectionChange={set} + displayFunc={i => levelNames[i]} + disabled={this.state.value >= levelNames.length} + /> +

    { this.state.value >= levelNames.length ? _t('Custom level, use the advanced controls below.') : levelDescriptions[get()] }

    +
    + { _t("Advanced") } +
    + + + +
    +
    +
    ; + } +} diff --git a/src/components/views/settings/Notifications.tsx b/src/components/views/settings/Notifications.tsx index 2f05b5e4dc5..6ad91a64793 100644 --- a/src/components/views/settings/Notifications.tsx +++ b/src/components/views/settings/Notifications.tsx @@ -501,7 +501,7 @@ export default class Notifications extends React.PureComponent { data-test-id='notif-setting-notificationsEnabled' value={SettingsStore.getValue("notificationsEnabled")} onChange={this.onDesktopNotificationsChanged} - label={_t('Enable desktop notifications for this session')} + label={_t('Enable desktop notifications for this device')} disabled={this.state.phase === Phase.Persisting} /> @@ -517,7 +517,7 @@ export default class Notifications extends React.PureComponent { data-test-id='notif-setting-audioNotificationsEnabled' value={SettingsStore.getValue("audioNotificationsEnabled")} onChange={this.onAudioNotificationsChanged} - label={_t('Enable audible notifications for this session')} + label={_t('Enable audible notifications for this device')} disabled={this.state.phase === Phase.Persisting} /> diff --git a/src/components/views/settings/SecureBackupPanel.tsx b/src/components/views/settings/SecureBackupPanel.tsx index da5df6c7a71..3ff6fbf54a6 100644 --- a/src/components/views/settings/SecureBackupPanel.tsx +++ b/src/components/views/settings/SecureBackupPanel.tsx @@ -248,21 +248,21 @@ export default class SecureBackupPanel extends React.PureComponent<{}, IState> { let restoreButtonCaption = _t("Restore from Backup"); if (MatrixClientPeg.get().getKeyBackupEnabled()) { - statusDescription =

    ✅ { _t("This session is backing up your keys. ") }

    ; + statusDescription =

    ✅ { _t("This device is backing up your keys. ") }

    ; } else { statusDescription = <>

    { _t( - "This session is not backing up your keys, " + + "This device is not backing up your keys, " + "but you do have an existing backup you can restore from " + "and add to going forward.", {}, { b: sub => { sub } }, ) }

    { _t( - "Connect this session to key backup before signing out to avoid " + - "losing any keys that may only be on this session.", + "Connect this device to key backup before signing out to avoid " + + "losing any keys that may only be on this device.", ) }

    ; - restoreButtonCaption = _t("Connect this session to Key Backup"); + restoreButtonCaption = _t("Connect this device to Key Backup"); } let uploadStatus; @@ -316,42 +316,42 @@ export default class SecureBackupPanel extends React.PureComponent<{}, IState> { ); } else if (!sig.device) { sigStatus = _t( - "Backup has a signature from unknown session with ID %(deviceId)s", + "Backup has a signature from unknown device with ID %(deviceId)s", { deviceId: sig.deviceId }, { verify }, ); } else if (sig.valid && fromThisDevice) { sigStatus = _t( - "Backup has a valid signature from this session", + "Backup has a valid signature from this device", {}, { validity }, ); } else if (!sig.valid && fromThisDevice) { // it can happen... sigStatus = _t( - "Backup has an invalid signature from this session", + "Backup has an invalid signature from this device", {}, { validity }, ); } else if (sig.valid && sig.deviceTrust.isVerified()) { sigStatus = _t( "Backup has a valid signature from " + - "verified session ", + "verified device ", {}, { validity, verify, device }, ); } else if (sig.valid && !sig.deviceTrust.isVerified()) { sigStatus = _t( "Backup has a valid signature from " + - "unverified session ", + "unverified device ", {}, { validity, verify, device }, ); } else if (!sig.valid && sig.deviceTrust.isVerified()) { sigStatus = _t( "Backup has an invalid signature from " + - "verified session ", + "verified device ", {}, { validity, verify, device }, ); } else if (!sig.valid && !sig.deviceTrust.isVerified()) { sigStatus = _t( "Backup has an invalid signature from " + - "unverified session ", + "unverified device ", {}, { validity, verify, device }, ); } @@ -361,12 +361,12 @@ export default class SecureBackupPanel extends React.PureComponent<{}, IState> {
    ; }); if (backupSigStatus.sigs.length === 0) { - backupSigStatuses = _t("Backup is not signed by any of your sessions"); + backupSigStatuses = _t("Backup is not signed by any of your devices"); } let trustedLocally; if (backupSigStatus.trusted_locally) { - trustedLocally = _t("This backup is trusted because it has been restored on this session"); + trustedLocally = _t("This backup is trusted because it has been restored on this device"); } extraDetailsTableRows = <> @@ -402,7 +402,7 @@ export default class SecureBackupPanel extends React.PureComponent<{}, IState> { } else { statusDescription = <>

    { _t( - "Your keys are not being backed up from this session.", {}, + "Your keys are not being backed up from this device.", {}, { b: sub => { sub } }, ) }

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

    @@ -442,9 +442,9 @@ export default class SecureBackupPanel extends React.PureComponent<{}, IState> { return (

    { _t( - "Back up your encryption keys with your account data in case you " + - "lose access to your sessions. Your keys will be secured with a " + - "unique Security Key.", + "Back up your secure messaging keys with your account data in case you " + + "lose access to your devices. Your keys will be secured with a " + + "recovery key.", ) }

    { statusDescription }
    diff --git a/src/components/views/settings/discovery/EmailAddresses.tsx b/src/components/views/settings/discovery/EmailAddresses.tsx index 8d94797ef00..4447749a5a9 100644 --- a/src/components/views/settings/discovery/EmailAddresses.tsx +++ b/src/components/views/settings/discovery/EmailAddresses.tsx @@ -252,7 +252,7 @@ export default class EmailAddresses extends React.Component { }); } else { content = - { _t("Discovery options will appear once you have added an email above.") } + { _t("Discovery options will appear once you have added an email to your account.") } ; } diff --git a/src/components/views/settings/discovery/PhoneNumbers.tsx b/src/components/views/settings/discovery/PhoneNumbers.tsx index add8759a85e..9dc01848008 100644 --- a/src/components/views/settings/discovery/PhoneNumbers.tsx +++ b/src/components/views/settings/discovery/PhoneNumbers.tsx @@ -268,7 +268,7 @@ export default class PhoneNumbers extends React.Component { }); } else { content = - { _t("Discovery options will appear once you have added a phone number above.") } + { _t("Discovery options will appear once you have added a phone number to your account.") } ; } diff --git a/src/components/views/settings/tabs/room/RolesRoomSettingsTab.tsx b/src/components/views/settings/tabs/room/RolesRoomSettingsTab.tsx index e519ec0bf7f..31abe19a3c7 100644 --- a/src/components/views/settings/tabs/room/RolesRoomSettingsTab.tsx +++ b/src/components/views/settings/tabs/room/RolesRoomSettingsTab.tsx @@ -237,7 +237,7 @@ export default class RolesRoomSettingsTab extends React.Component { [EventType.RoomPowerLevels]: _td("Change permissions"), [EventType.RoomTopic]: isSpaceRoom ? _td("Change description") : _td("Change topic"), [EventType.RoomTombstone]: _td("Upgrade the room"), - [EventType.RoomEncryption]: _td("Enable room encryption"), + [EventType.RoomEncryption]: _td("Enable secure messaging"), [EventType.RoomServerAcl]: _td("Change server ACLs"), [EventType.Reaction]: _td("Send reactions"), [EventType.RoomRedaction]: _td("Remove messages sent by me"), diff --git a/src/components/views/settings/tabs/room/SecurityRoomSettingsTab.tsx b/src/components/views/settings/tabs/room/SecurityRoomSettingsTab.tsx index b9e2d945947..8f4fe2ea04f 100644 --- a/src/components/views/settings/tabs/room/SecurityRoomSettingsTab.tsx +++ b/src/components/views/settings/tabs/room/SecurityRoomSettingsTab.tsx @@ -125,19 +125,19 @@ export default class SecurityRoomSettingsTab extends React.Component { if (MatrixClientPeg.get().getRoom(this.props.roomId)?.getJoinRule() === JoinRule.Public) { const dialog = Modal.createTrackedDialog('Confirm Public Encrypted Room', '', QuestionDialog, { - title: _t('Are you sure you want to add encryption to this public room?'), + title: _t('Are you sure you want to enable secure messaging for this public room?'), description:

    { _t( - "It's not recommended to add encryption to public rooms." + + "It's not recommended to enable secure messaging in public rooms." + "Anyone can find and join public rooms, so anyone can read messages in them. " + - "You'll get none of the benefits of encryption, and you won't be able to turn it " + - "off later. Encrypting messages in a public room will make receiving and sending " + + "You'll get none of the benefits of end-to-end encryption, and you won't be able to turn it " + + "off later. Secure messaging in a public room will also make receiving and sending " + "messages slower.", null, { "b": (sub) => { sub } }, ) }

    { _t( - "To avoid these issues, create a new encrypted room for " + + "To avoid these issues, create a new secure messaging room for " + "the conversation you plan to have.", null, { @@ -158,11 +158,11 @@ export default class SecurityRoomSettingsTab extends React.ComponentLearn more about encryption.", + "Once enabled, secure messaging for a room cannot be disabled. Messages sent in a secure messaging " + + "room cannot be seen by the server, only by the participants of the room. Enabling secure messaging " + + "may prevent many bots and bridges from working correctly. Learn more about secure messaging.", {}, { a: sub => { _t( "It's not recommended to make encrypted rooms public. " + "It will mean anyone can find and join the room, so anyone can read messages. " + - "You'll get none of the benefits of encryption. Encrypting messages in a public " + + "You'll get none of the benefits of end-to-end encryption. Encrypting messages in a public " + "room will make receiving and sending messages slower.", null, { "b": (sub) => { sub } }, @@ -438,11 +438,11 @@ export default class SecurityRoomSettingsTab extends React.Component

    { _t("Security & Privacy") }
    - + { encryptionSettings } diff --git a/src/components/views/settings/tabs/user/AccountUserSettingsTab.tsx b/src/components/views/settings/tabs/user/AccountUserSettingsTab.tsx new file mode 100644 index 00000000000..3e396ec3c0c --- /dev/null +++ b/src/components/views/settings/tabs/user/AccountUserSettingsTab.tsx @@ -0,0 +1,272 @@ +/* +Copyright 2019 New Vector Ltd +Copyright 2019 The Matrix.org Foundation C.I.C. +Copyright 2019 Michael Telatynski <7t3chguy@gmail.com> + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +import React from 'react'; +import { IThreepid } from "matrix-js-sdk/src/@types/threepids"; +import { logger } from "matrix-js-sdk/src/logger"; + +import { _t } from "../../../../../languageHandler"; +import SettingsStore from "../../../../../settings/SettingsStore"; +import AccessibleButton from "../../../elements/AccessibleButton"; +import DeactivateAccountDialog from "../../../dialogs/DeactivateAccountDialog"; +import { MatrixClientPeg } from "../../../../../MatrixClientPeg"; +import Modal from "../../../../../Modal"; +import dis from "../../../../../dispatcher/dispatcher"; +import { getThreepidsWithBindStatus } from '../../../../../boundThreepids'; +import Spinner from "../../../elements/Spinner"; +import { UIFeature } from "../../../../../settings/UIFeature"; +import { ActionPayload } from "../../../../../dispatcher/payloads"; +import ErrorDialog from "../../../dialogs/ErrorDialog"; +import AccountPhoneNumbers from "../../account/PhoneNumbers"; +import AccountEmailAddresses from "../../account/EmailAddresses"; +import ChangePassword from "../../ChangePassword"; +import AccountDevicesPanel from '../../AccountDevicesPanel'; + +interface IProps { + closeSettingsFn: () => void; +} + +interface IState { + haveIdServer: boolean; + serverSupportsSeparateAddAndBind: boolean; + emails: IThreepid[]; + msisdns: IThreepid[]; + loading3pids: boolean; // whether or not the emails and msisdns have been loaded + canChangePassword: boolean; +} + +export default class AccountUserSettingsTab extends React.Component { + private readonly dispatcherRef: string; + + constructor(props: IProps) { + super(props); + + this.state = { + haveIdServer: Boolean(MatrixClientPeg.get().getIdentityServerUrl()), + serverSupportsSeparateAddAndBind: null, + emails: [], + msisdns: [], + loading3pids: true, // whether or not the emails and msisdns have been loaded + canChangePassword: false, + }; + + this.dispatcherRef = dis.register(this.onAction); + } + + // TODO: [REACT-WARNING] Move this to constructor + // eslint-disable-next-line @typescript-eslint/naming-convention, camelcase + public async UNSAFE_componentWillMount(): Promise { + const cli = MatrixClientPeg.get(); + + const serverSupportsSeparateAddAndBind = await cli.doesServerSupportSeparateAddAndBind(); + + const capabilities = await cli.getCapabilities(); // this is cached + const changePasswordCap = capabilities['m.change_password']; + + // You can change your password so long as the capability isn't explicitly disabled. The implicit + // behaviour is you can change your password when the capability is missing or has not-false as + // the enabled flag value. + const canChangePassword = !changePasswordCap || changePasswordCap['enabled'] !== false; + + this.setState({ serverSupportsSeparateAddAndBind, canChangePassword }); + + this.getThreepidState(); + } + + public componentWillUnmount(): void { + dis.unregister(this.dispatcherRef); + } + + private onAction = (payload: ActionPayload): void => { + if (payload.action === 'id_server_changed') { + this.setState({ haveIdServer: Boolean(MatrixClientPeg.get().getIdentityServerUrl()) }); + this.getThreepidState(); + } + }; + + private onEmailsChange = (emails: IThreepid[]): void => { + this.setState({ emails }); + }; + + private onMsisdnsChange = (msisdns: IThreepid[]): void => { + this.setState({ msisdns }); + }; + + private async getThreepidState(): Promise { + const cli = MatrixClientPeg.get(); + + // Need to get 3PIDs generally for Account section and possibly also for + // Discovery (assuming we have an IS and terms are agreed). + let threepids = []; + try { + threepids = await getThreepidsWithBindStatus(cli); + } catch (e) { + const idServerUrl = MatrixClientPeg.get().getIdentityServerUrl(); + logger.warn( + `Unable to reach identity server at ${idServerUrl} to check ` + + `for 3PIDs bindings in Settings`, + ); + logger.warn(e); + } + this.setState({ + emails: threepids.filter((a) => a.medium === 'email'), + msisdns: threepids.filter((a) => a.medium === 'msisdn'), + loading3pids: false, + }); + } + + private onPasswordChangeError = (err): void => { + // TODO: Figure out a design that doesn't involve replacing the current dialog + let errMsg = err.error || err.message || ""; + if (err.httpStatus === 403) { + errMsg = _t("Failed to change password. Is your password correct?"); + } else if (!errMsg) { + errMsg += ` (HTTP status ${err.httpStatus})`; + } + logger.error("Failed to change password: " + errMsg); + Modal.createTrackedDialog('Failed to change password', '', ErrorDialog, { + title: _t("Error"), + description: errMsg, + }); + }; + + private onPasswordChanged = (): void => { + // TODO: Figure out a design that doesn't involve replacing the current dialog + Modal.createTrackedDialog('Password changed', '', ErrorDialog, { + title: _t("Success"), + description: _t( + "Your password was successfully changed. You will not receive " + + "push notifications on other devices until you log back in to them", + ) + ".", + }); + }; + + private onDeactivateClicked = (): void => { + Modal.createTrackedDialog('Deactivate Account', '', DeactivateAccountDialog, { + onFinished: (success) => { + if (success) this.props.closeSettingsFn(); + }, + }); + }; + + private renderAccountSection(): JSX.Element { + let passwordChangeForm = ( + + ); + + let threepidSection = null; + + // For older homeservers without separate 3PID add and bind methods (MSC2290), + // we use a combo add with bind option API which requires an identity server to + // validate 3PID ownership even if we're just adding to the homeserver only. + // For newer homeservers with separate 3PID add and bind methods (MSC2290), + // there is no such concern, so we can always show the HS account 3PIDs. + if (SettingsStore.getValue(UIFeature.ThirdPartyID) && + (this.state.haveIdServer || this.state.serverSupportsSeparateAddAndBind === true) + ) { + const emails = this.state.loading3pids + ? + : ; + const msisdns = this.state.loading3pids + ? + : ; + threepidSection =
    + { _t("Email addresses") } + { emails } + + { _t("Phone numbers") } + { msisdns } +
    ; + } else if (this.state.serverSupportsSeparateAddAndBind === null) { + threepidSection = ; + } + + let passwordChangeText = _t("Set a new account password..."); + if (!this.state.canChangePassword) { + // Just don't show anything if you can't do anything. + passwordChangeText = null; + passwordChangeForm = null; + } + + return ( +
    + { _t("Change password") } +

    + { passwordChangeText } +

    + { passwordChangeForm } + { threepidSection } +
    + ); + } + + private renderManagementSection(): JSX.Element { + // TODO: Improve warning text for account deactivation + return ( +
    + { _t("Close account") } + + { _t("Deactivating your account is a permanent action - be careful!") } + + + { _t("Deactivate Account") } + +
    + ); + } + + public render(): JSX.Element { + let accountManagementSection; + if (SettingsStore.getValue(UIFeature.Deactivate)) { + accountManagementSection = <> +
    { _t("Danger zone") }
    + { this.renderManagementSection() } + ; + } + + return ( +
    +
    { _t("Account") }
    +
    + { _t("Where you're signed in") } +
    + + { _t( + "Manage your signed-in devices below.", + ) } + + +
    +
    + { this.renderAccountSection() } + { accountManagementSection } +
    + ); + } +} diff --git a/src/components/views/settings/tabs/user/AppearanceUserSettingsTab.tsx b/src/components/views/settings/tabs/user/AppearanceUserSettingsTab.tsx index 30e7882f105..e5679dfd51a 100644 --- a/src/components/views/settings/tabs/user/AppearanceUserSettingsTab.tsx +++ b/src/components/views/settings/tabs/user/AppearanceUserSettingsTab.tsx @@ -143,7 +143,7 @@ export default class AppearanceUserSettingsTab extends React.Component
    { _t("Customise your appearance") }
    - { _t("Appearance Settings only affect this %(brand)s session.", { brand }) } + { _t("Appearance Settings only affect this %(brand)s device.", { brand }) }
    void; // Promise resolve function for startTermsFlow callback - }; - emails: IThreepid[]; - msisdns: IThreepid[]; - loading3pids: boolean; // whether or not the emails and msisdns have been loaded - canChangePassword: boolean; - idServerName: string; } export default class GeneralUserSettingsTab extends React.Component { - private readonly dispatcherRef: string; - constructor(props: IProps) { super(props); this.state = { language: languageHandler.getCurrentLanguage(), spellCheckLanguages: [], - haveIdServer: Boolean(MatrixClientPeg.get().getIdentityServerUrl()), - serverSupportsSeparateAddAndBind: null, - idServerHasUnsignedTerms: false, - requiredPolicyInfo: { // This object is passed along to a component for handling - hasTerms: false, - policiesAndServices: null, // From the startTermsFlow callback - agreedUrls: null, // From the startTermsFlow callback - resolve: null, // Promise resolve function for startTermsFlow callback - }, - emails: [], - msisdns: [], - loading3pids: true, // whether or not the emails and msisdns have been loaded - canChangePassword: false, - idServerName: null, }; - - this.dispatcherRef = dis.register(this.onAction); - } - - // TODO: [REACT-WARNING] Move this to constructor - // eslint-disable-next-line @typescript-eslint/naming-convention, camelcase - public async UNSAFE_componentWillMount(): Promise { - const cli = MatrixClientPeg.get(); - - const serverSupportsSeparateAddAndBind = await cli.doesServerSupportSeparateAddAndBind(); - - const capabilities = await cli.getCapabilities(); // this is cached - const changePasswordCap = capabilities['m.change_password']; - - // You can change your password so long as the capability isn't explicitly disabled. The implicit - // behaviour is you can change your password when the capability is missing or has not-false as - // the enabled flag value. - const canChangePassword = !changePasswordCap || changePasswordCap['enabled'] !== false; - - this.setState({ serverSupportsSeparateAddAndBind, canChangePassword }); - - this.getThreepidState(); } public async componentDidMount(): Promise { @@ -134,96 +57,6 @@ export default class GeneralUserSettingsTab extends React.Component { - if (payload.action === 'id_server_changed') { - this.setState({ haveIdServer: Boolean(MatrixClientPeg.get().getIdentityServerUrl()) }); - this.getThreepidState(); - } - }; - - private onEmailsChange = (emails: IThreepid[]): void => { - this.setState({ emails }); - }; - - private onMsisdnsChange = (msisdns: IThreepid[]): void => { - this.setState({ msisdns }); - }; - - private async getThreepidState(): Promise { - const cli = MatrixClientPeg.get(); - - // Check to see if terms need accepting - this.checkTerms(); - - // Need to get 3PIDs generally for Account section and possibly also for - // Discovery (assuming we have an IS and terms are agreed). - let threepids = []; - try { - threepids = await getThreepidsWithBindStatus(cli); - } catch (e) { - const idServerUrl = MatrixClientPeg.get().getIdentityServerUrl(); - logger.warn( - `Unable to reach identity server at ${idServerUrl} to check ` + - `for 3PIDs bindings in Settings`, - ); - logger.warn(e); - } - this.setState({ - emails: threepids.filter((a) => a.medium === 'email'), - msisdns: threepids.filter((a) => a.medium === 'msisdn'), - loading3pids: false, - }); - } - - private async checkTerms(): Promise { - if (!this.state.haveIdServer) { - this.setState({ idServerHasUnsignedTerms: false }); - return; - } - - // By starting the terms flow we get the logic for checking which terms the user has signed - // for free. So we might as well use that for our own purposes. - const idServerUrl = MatrixClientPeg.get().getIdentityServerUrl(); - const authClient = new IdentityAuthClient(); - try { - const idAccessToken = await authClient.getAccessToken({ check: false }); - await startTermsFlow([new Service( - SERVICE_TYPES.IS, - idServerUrl, - idAccessToken, - )], (policiesAndServices, agreedUrls, extraClassNames) => { - return new Promise((resolve, reject) => { - this.setState({ - idServerName: abbreviateUrl(idServerUrl), - requiredPolicyInfo: { - hasTerms: true, - policiesAndServices, - agreedUrls, - resolve, - }, - }); - }); - }); - // User accepted all terms - this.setState({ - requiredPolicyInfo: { - hasTerms: false, - ...this.state.requiredPolicyInfo, - }, - }); - } catch (e) { - logger.warn( - `Unable to reach identity server at ${idServerUrl} to check ` + - `for terms in Settings`, - ); - logger.warn(e); - } - } - private onLanguageChange = (newLanguage: string): void => { if (this.state.language === newLanguage) return; @@ -245,40 +78,6 @@ export default class GeneralUserSettingsTab extends React.Component { - // TODO: Figure out a design that doesn't involve replacing the current dialog - let errMsg = err.error || err.message || ""; - if (err.httpStatus === 403) { - errMsg = _t("Failed to change password. Is your password correct?"); - } else if (!errMsg) { - errMsg += ` (HTTP status ${err.httpStatus})`; - } - logger.error("Failed to change password: " + errMsg); - Modal.createTrackedDialog('Failed to change password', '', ErrorDialog, { - title: _t("Error"), - description: errMsg, - }); - }; - - private onPasswordChanged = (): void => { - // TODO: Figure out a design that doesn't involve replacing the current dialog - Modal.createTrackedDialog('Password changed', '', ErrorDialog, { - title: _t("Success"), - description: _t( - "Your password was successfully changed. You will not receive " + - "push notifications on other sessions until you log back in to them", - ) + ".", - }); - }; - - private onDeactivateClicked = (): void => { - Modal.createTrackedDialog('Deactivate Account', '', DeactivateAccountDialog, { - onFinished: (success) => { - if (success) this.props.closeSettingsFn(); - }, - }); - }; - private renderProfileSection(): JSX.Element { return (
    @@ -287,68 +86,6 @@ export default class GeneralUserSettingsTab extends React.Component - ); - - let threepidSection = null; - - // For older homeservers without separate 3PID add and bind methods (MSC2290), - // we use a combo add with bind option API which requires an identity server to - // validate 3PID ownership even if we're just adding to the homeserver only. - // For newer homeservers with separate 3PID add and bind methods (MSC2290), - // there is no such concern, so we can always show the HS account 3PIDs. - if (SettingsStore.getValue(UIFeature.ThirdPartyID) && - (this.state.haveIdServer || this.state.serverSupportsSeparateAddAndBind === true) - ) { - const emails = this.state.loading3pids - ? - : ; - const msisdns = this.state.loading3pids - ? - : ; - threepidSection =
    - { _t("Email addresses") } - { emails } - - { _t("Phone numbers") } - { msisdns } -
    ; - } else if (this.state.serverSupportsSeparateAddAndBind === null) { - threepidSection = ; - } - - let passwordChangeText = _t("Set a new account password..."); - if (!this.state.canChangePassword) { - // Just don't show anything if you can't do anything. - passwordChangeText = null; - passwordChangeForm = null; - } - - return ( -
    - { _t("Account") } -

    - { passwordChangeText } -

    - { passwordChangeForm } - { threepidSection } -
    - ); - } - private renderLanguageSection(): JSX.Element { // TODO: Convert to new-styled Field return ( @@ -375,64 +112,6 @@ export default class GeneralUserSettingsTab extends React.Component - { _t( - "Agree to the identity server (%(serverName)s) Terms of Service to " + - "allow yourself to be discoverable by email address or phone number.", - { serverName: this.state.idServerName }, - ) } - ; - return ( -
    - - { /* has its own heading as it includes the current identity server */ } - -
    - ); - } - - const emails = this.state.loading3pids ? : ; - const msisdns = this.state.loading3pids ? : ; - - const threepidSection = this.state.haveIdServer ?
    - { _t("Email addresses") } - { emails } - - { _t("Phone numbers") } - { msisdns } -
    : null; - - return ( -
    - { threepidSection } - { /* has its own heading as it includes the current identity server */ } - -
    - ); - } - - private renderManagementSection(): JSX.Element { - // TODO: Improve warning text for account deactivation - return ( -
    - { _t("Account management") } - - { _t("Deactivating your account is a permanent action - be careful!") } - - - { _t("Deactivate Account") } - -
    - ); - } - private renderIntegrationManagerSection(): JSX.Element { if (!SettingsStore.getValue(UIFeature.Widgets)) return null; @@ -448,42 +127,13 @@ export default class GeneralUserSettingsTab 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() } { supportsMultiLanguageSpellCheck ? this.renderSpellCheckSection() : null } - { discoverySection } { this.renderIntegrationManagerSection() /* Has its own title */ } - { accountManagementSection }
    ); } diff --git a/src/components/views/settings/tabs/user/MjolnirUserSettingsTab.tsx b/src/components/views/settings/tabs/user/MjolnirUserSettingsTab.tsx deleted file mode 100644 index 709b3e8c56c..00000000000 --- a/src/components/views/settings/tabs/user/MjolnirUserSettingsTab.tsx +++ /dev/null @@ -1,327 +0,0 @@ -/* -Copyright 2019-2021 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 { logger } from "matrix-js-sdk/src/logger"; - -import { _t } from "../../../../../languageHandler"; -import SdkConfig from "../../../../../SdkConfig"; -import { Mjolnir } from "../../../../../mjolnir/Mjolnir"; -import { ListRule } from "../../../../../mjolnir/ListRule"; -import { BanList, RULE_SERVER, RULE_USER } from "../../../../../mjolnir/BanList"; -import Modal from "../../../../../Modal"; -import { MatrixClientPeg } from "../../../../../MatrixClientPeg"; -import ErrorDialog from "../../../dialogs/ErrorDialog"; -import QuestionDialog from "../../../dialogs/QuestionDialog"; -import AccessibleButton from "../../../elements/AccessibleButton"; -import Field from "../../../elements/Field"; - -interface IState { - busy: boolean; - newPersonalRule: string; - newList: string; -} - -export default class MjolnirUserSettingsTab extends React.Component<{}, IState> { - constructor(props) { - super(props); - - this.state = { - busy: false, - newPersonalRule: "", - newList: "", - }; - } - - private onPersonalRuleChanged = (e) => { - this.setState({ newPersonalRule: e.target.value }); - }; - - private onNewListChanged = (e) => { - this.setState({ newList: e.target.value }); - }; - - private onAddPersonalRule = async (e) => { - e.preventDefault(); - e.stopPropagation(); - - let kind = RULE_SERVER; - if (this.state.newPersonalRule.startsWith("@")) { - kind = RULE_USER; - } - - this.setState({ busy: true }); - try { - const list = await Mjolnir.sharedInstance().getOrCreatePersonalList(); - await list.banEntity(kind, this.state.newPersonalRule, _t("Ignored/Blocked")); - this.setState({ newPersonalRule: "" }); // this will also cause the new rule to be rendered - } catch (e) { - logger.error(e); - - Modal.createTrackedDialog('Failed to add Mjolnir rule', '', ErrorDialog, { - title: _t('Error adding ignored user/server'), - description: _t('Something went wrong. Please try again or view your console for hints.'), - }); - } finally { - this.setState({ busy: false }); - } - }; - - private onSubscribeList = async (e) => { - e.preventDefault(); - e.stopPropagation(); - - this.setState({ busy: true }); - try { - const room = await MatrixClientPeg.get().joinRoom(this.state.newList); - await Mjolnir.sharedInstance().subscribeToList(room.roomId); - this.setState({ newList: "" }); // this will also cause the new rule to be rendered - } catch (e) { - logger.error(e); - - Modal.createTrackedDialog('Failed to subscribe to Mjolnir list', '', ErrorDialog, { - title: _t('Error subscribing to list'), - description: _t('Please verify the room ID or address and try again.'), - }); - } finally { - this.setState({ busy: false }); - } - }; - - private async removePersonalRule(rule: ListRule) { - this.setState({ busy: true }); - try { - const list = Mjolnir.sharedInstance().getPersonalList(); - await list.unbanEntity(rule.kind, rule.entity); - } catch (e) { - logger.error(e); - - Modal.createTrackedDialog('Failed to remove Mjolnir rule', '', ErrorDialog, { - title: _t('Error removing ignored user/server'), - description: _t('Something went wrong. Please try again or view your console for hints.'), - }); - } finally { - this.setState({ busy: false }); - } - } - - private async unsubscribeFromList(list: BanList) { - this.setState({ busy: true }); - try { - await Mjolnir.sharedInstance().unsubscribeFromList(list.roomId); - await MatrixClientPeg.get().leave(list.roomId); - } catch (e) { - logger.error(e); - - Modal.createTrackedDialog('Failed to unsubscribe from Mjolnir list', '', ErrorDialog, { - title: _t('Error unsubscribing from list'), - description: _t('Please try again or view your console for hints.'), - }); - } finally { - this.setState({ busy: false }); - } - } - - private viewListRules(list: BanList) { - const room = MatrixClientPeg.get().getRoom(list.roomId); - const name = room ? room.name : list.roomId; - - const renderRules = (rules: ListRule[]) => { - if (rules.length === 0) return { _t("None") }; - - const tiles = []; - for (const rule of rules) { - tiles.push(
  • { rule.entity }
  • ); - } - return
      { tiles }
    ; - }; - - Modal.createTrackedDialog('View Mjolnir list rules', '', QuestionDialog, { - title: _t("Ban list rules - %(roomName)s", { roomName: name }), - description: ( -
    -

    { _t("Server rules") }

    - { renderRules(list.serverRules) } -

    { _t("User rules") }

    - { renderRules(list.userRules) } -
    - ), - button: _t("Close"), - hasCancelButton: false, - }); - } - - private renderPersonalBanListRules() { - const list = Mjolnir.sharedInstance().getPersonalList(); - const rules = list ? [...list.userRules, ...list.serverRules] : []; - if (!list || rules.length <= 0) return { _t("You have not ignored anyone.") }; - - const tiles = []; - for (const rule of rules) { - tiles.push( -
  • - this.removePersonalRule(rule)} - disabled={this.state.busy} - > - { _t("Remove") } -   - { rule.entity } -
  • , - ); - } - - return ( -
    -

    { _t("You are currently ignoring:") }

    -
      { tiles }
    -
    - ); - } - - private renderSubscribedBanLists() { - const personalList = Mjolnir.sharedInstance().getPersonalList(); - const lists = Mjolnir.sharedInstance().lists.filter(b => { - return personalList? personalList.roomId !== b.roomId : true; - }); - if (!lists || lists.length <= 0) return { _t("You are not subscribed to any lists") }; - - const tiles = []; - for (const list of lists) { - const room = MatrixClientPeg.get().getRoom(list.roomId); - const name = room ? { room.name } ({ list.roomId }) : list.roomId; - tiles.push( -
  • - this.unsubscribeFromList(list)} - disabled={this.state.busy} - > - { _t("Unsubscribe") } -   - this.viewListRules(list)} - disabled={this.state.busy} - > - { _t("View rules") } -   - { name } -
  • , - ); - } - - return ( -
    -

    { _t("You are currently subscribed to:") }

    -
      { tiles }
    -
    - ); - } - - render() { - const brand = SdkConfig.get().brand; - - return ( -
    -
    { _t("Ignored users") }
    -
    -
    - { _t("âš  These settings are meant for advanced users.") }
    -
    - { _t( - "Add users and servers you want to ignore here. Use asterisks " + - "to have %(brand)s match any characters. For example, @bot:* " + - "would ignore all users that have the name 'bot' on any server.", - { brand }, { code: (s) => { s } }, - ) }
    -
    - { _t( - "Ignoring people is done through ban lists which contain rules for " + - "who to ban. Subscribing to a ban list means the users/servers blocked by " + - "that list will be hidden from you.", - ) } -
    -
    -
    - { _t("Personal ban list") } -
    - { _t( - "Your personal ban list holds all the users/servers you personally don't " + - "want to see messages from. After ignoring your first user/server, a new room " + - "will show up in your room list named 'My Ban List' - stay in this room to keep " + - "the ban list in effect.", - ) } -
    -
    - { this.renderPersonalBanListRules() } -
    -
    - - - - { _t("Ignore") } - - -
    -
    -
    - { _t("Subscribed lists") } -
    - { _t("Subscribing to a ban list will cause you to join it!") } -   - { _t( - "If this isn't what you want, please use a different tool to ignore users.", - ) } -
    -
    - { this.renderSubscribedBanLists() } -
    -
    -
    - - - { _t("Subscribe") } - - -
    -
    -
    - ); - } -} diff --git a/src/components/views/settings/tabs/user/PrivacyUserSettingsTab.tsx b/src/components/views/settings/tabs/user/PrivacyUserSettingsTab.tsx new file mode 100644 index 00000000000..f2e209f11fe --- /dev/null +++ b/src/components/views/settings/tabs/user/PrivacyUserSettingsTab.tsx @@ -0,0 +1,784 @@ +/* +Copyright 2019-2021 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 { SERVICE_TYPES } from "matrix-js-sdk/src/service-types"; +import { sleep } from "matrix-js-sdk/src/utils"; +import { Room, RoomEvent } from "matrix-js-sdk/src/models/room"; +import { logger } from "matrix-js-sdk/src/logger"; +import { IThreepid } from "matrix-js-sdk/src/@types/threepids"; + +import { _t } from "../../../../../languageHandler"; +import SdkConfig from "../../../../../SdkConfig"; +import { Mjolnir } from "../../../../../mjolnir/Mjolnir"; +import { ListRule } from "../../../../../mjolnir/ListRule"; +import { BanList, RULE_SERVER, RULE_USER } from "../../../../../mjolnir/BanList"; +import Modal from "../../../../../Modal"; +import { MatrixClientPeg } from "../../../../../MatrixClientPeg"; +import ErrorDialog from "../../../dialogs/ErrorDialog"; +import QuestionDialog from "../../../dialogs/QuestionDialog"; +import AccessibleButton from "../../../elements/AccessibleButton"; +import Field from "../../../elements/Field"; +import { PosthogAnalytics } from '../../../../../PosthogAnalytics'; +import Analytics from '../../../../../Analytics'; +import SettingsFlag from '../../../elements/SettingsFlag'; +import { SettingLevel } from '../../../../../settings/SettingLevel'; +import { showDialog as showAnalyticsLearnMoreDialog } from "../../../dialogs/AnalyticsLearnMoreDialog"; +import { ActionPayload } from '../../../../../dispatcher/payloads'; +import dis from "../../../../../dispatcher/dispatcher"; +import InlineSpinner from '../../../elements/InlineSpinner'; +import SettingsStore, { CallbackFn } from '../../../../../settings/SettingsStore'; +import { UIFeature } from '../../../../../settings/UIFeature'; +import { Policies, Service, startTermsFlow } from "../../../../../Terms"; +import IdentityAuthClient from "../../../../../IdentityAuthClient"; +import { abbreviateUrl } from "../../../../../utils/UrlUtils"; +import DiscoveryEmailAddresses from "../../discovery/EmailAddresses"; +import DiscoveryPhoneNumbers from "../../discovery/PhoneNumbers"; +import InlineTermsAgreement from "../../../terms/InlineTermsAgreement"; +import SetIdServer from "../../SetIdServer"; +import { getThreepidsWithBindStatus } from '../../../../../boundThreepids'; +import Spinner from "../../../elements/Spinner"; + +interface IIgnoredUserProps { + userId: string; + onUnignored: (userId: string) => void; + inProgress: boolean; +} + +export class IgnoredUser extends React.Component { + private onUnignoreClicked = (): void => { + this.props.onUnignored(this.props.userId); + }; + + public render(): JSX.Element { + const id = `mx_SecurityUserSettingsTab_ignoredUser_${this.props.userId}`; + return ( +
    + + { _t('Unignore') } + + { this.props.userId } +
    + ); + } +} + +interface IProps { + closeSettingsFn: () => void; +} + +interface IState { + mjolnirEnabled: boolean; + busy: boolean; + newPersonalRule: string; + newList: string; + ignoredUserIds: string[]; + waitingUnignored: string[]; + managingInvites: boolean; + invitedRoomIds: Set; + requiredPolicyInfo: { // This object is passed along to a component for handling + hasTerms: boolean; + policiesAndServices: { + service: Service; + policies: Policies; + }[]; // From the startTermsFlow callback + agreedUrls: string[]; // From the startTermsFlow callback + resolve: (values: string[]) => void; // Promise resolve function for startTermsFlow callback + }; + idServerHasUnsignedTerms: boolean; + emails: IThreepid[]; + msisdns: IThreepid[]; + loading3pids: boolean; // whether or not the emails and msisdns have been loaded + idServerName: string; + haveIdServer: boolean; +} + +export default class MjolnirUserSettingsTab extends React.Component { + private dispatcherRef: string; + private mjolnirWatcher: string; + + constructor(props) { + super(props); + + // Get rooms we're invited to + const invitedRoomIds = new Set(this.getInvitedRooms().map(room => room.roomId)); + + this.state = { + mjolnirEnabled: SettingsStore.getValue("feature_mjolnir"), + busy: false, + newPersonalRule: "", + newList: "", + ignoredUserIds: MatrixClientPeg.get().getIgnoredUsers(), + waitingUnignored: [], + managingInvites: false, + invitedRoomIds, + requiredPolicyInfo: { // This object is passed along to a component for handling + hasTerms: false, + policiesAndServices: null, // From the startTermsFlow callback + agreedUrls: null, // From the startTermsFlow callback + resolve: null, // Promise resolve function for startTermsFlow callback + }, + idServerHasUnsignedTerms: false, + emails: [], + msisdns: [], + loading3pids: false, + idServerName: null, + haveIdServer: Boolean(MatrixClientPeg.get().getIdentityServerUrl()), + }; + } + + private async getThreepidState(): Promise { + const cli = MatrixClientPeg.get(); + + // Check to see if terms need accepting + this.checkTerms(); + + // Need to get 3PIDs generally for Account section and possibly also for + // Discovery (assuming we have an IS and terms are agreed). + let threepids = []; + try { + threepids = await getThreepidsWithBindStatus(cli); + } catch (e) { + const idServerUrl = MatrixClientPeg.get().getIdentityServerUrl(); + logger.warn( + `Unable to reach identity server at ${idServerUrl} to check ` + + `for 3PIDs bindings in Settings`, + ); + logger.warn(e); + } + this.setState({ + emails: threepids.filter((a) => a.medium === 'email'), + msisdns: threepids.filter((a) => a.medium === 'msisdn'), + loading3pids: false, + }); + } + + private onAction = ({ action }: ActionPayload) => { + if (action === "ignore_state_changed") { + const ignoredUserIds = MatrixClientPeg.get().getIgnoredUsers(); + const newWaitingUnignored = this.state.waitingUnignored.filter(e => ignoredUserIds.includes(e)); + this.setState({ ignoredUserIds, waitingUnignored: newWaitingUnignored }); + } else if (action === 'id_server_changed') { + this.setState({ haveIdServer: Boolean(MatrixClientPeg.get().getIdentityServerUrl()) }); + this.getThreepidState(); + } + }; + + public componentDidMount(): void { + this.dispatcherRef = dis.register(this.onAction); + MatrixClientPeg.get().on(RoomEvent.MyMembership, this.onMyMembership); + this.mjolnirWatcher = SettingsStore.watchSetting("feature_mjolnir", null, this.mjolnirChanged); + this.getThreepidState(); + } + + public componentWillUnmount(): void { + dis.unregister(this.dispatcherRef); + MatrixClientPeg.get().removeListener(RoomEvent.MyMembership, this.onMyMembership); + SettingsStore.unwatchSetting(this.mjolnirWatcher); + } + + private mjolnirChanged: CallbackFn = (settingName, roomId, atLevel, newValue) => { + // We can cheat because we know what levels a feature is tracked at, and how it is tracked + this.setState({ mjolnirEnabled: newValue }); + }; + + private updateAnalytics = (checked: boolean): void => { + checked ? Analytics.enable() : Analytics.disable(); + }; + + private onMyMembership = (room: Room, membership: string): void => { + if (room.isSpaceRoom()) { + return; + } + + if (membership === "invite") { + this.addInvitedRoom(room); + } else if (this.state.invitedRoomIds.has(room.roomId)) { + // The user isn't invited anymore + this.removeInvitedRoom(room.roomId); + } + }; + + private addInvitedRoom = (room: Room): void => { + this.setState(({ invitedRoomIds }) => ({ + invitedRoomIds: new Set(invitedRoomIds).add(room.roomId), + })); + }; + + private removeInvitedRoom = (roomId: string): void => { + this.setState(({ invitedRoomIds }) => { + const newInvitedRoomIds = new Set(invitedRoomIds); + newInvitedRoomIds.delete(roomId); + + return { + invitedRoomIds: newInvitedRoomIds, + }; + }); + }; + + private onUserUnignored = async (userId: string): Promise => { + const { ignoredUserIds, waitingUnignored } = this.state; + const currentlyIgnoredUserIds = ignoredUserIds.filter(e => !waitingUnignored.includes(e)); + + const index = currentlyIgnoredUserIds.indexOf(userId); + if (index !== -1) { + currentlyIgnoredUserIds.splice(index, 1); + this.setState(({ waitingUnignored }) => ({ waitingUnignored: [...waitingUnignored, userId] })); + MatrixClientPeg.get().setIgnoredUsers(currentlyIgnoredUserIds); + } + }; + + private getInvitedRooms = (): Room[] => { + return MatrixClientPeg.get().getRooms().filter((r) => { + return r.hasMembershipState(MatrixClientPeg.get().getUserId(), "invite"); + }); + }; + + private manageInvites = async (accept: boolean): Promise => { + this.setState({ + managingInvites: true, + }); + + // iterate with a normal for loop in order to retry on action failure + const invitedRoomIdsValues = Array.from(this.state.invitedRoomIds); + + // Execute all acceptances/rejections sequentially + const cli = MatrixClientPeg.get(); + const action = accept ? cli.joinRoom.bind(cli) : cli.leave.bind(cli); + for (let i = 0; i < invitedRoomIdsValues.length; i++) { + const roomId = invitedRoomIdsValues[i]; + + // Accept/reject invite + await action(roomId).then(() => { + // No error, update invited rooms button + this.removeInvitedRoom(roomId); + }, async (e) => { + // Action failure + if (e.errcode === "M_LIMIT_EXCEEDED") { + // Add a delay between each invite change in order to avoid rate + // limiting by the server. + await sleep(e.retry_after_ms || 2500); + + // Redo last action + i--; + } else { + // Print out error with joining/leaving room + logger.warn(e); + } + }); + } + + this.setState({ + managingInvites: false, + }); + }; + + private onAcceptAllInvitesClicked = (): void => { + this.manageInvites(true); + }; + + private onRejectAllInvitesClicked = (): void => { + this.manageInvites(false); + }; + + private renderIgnoredUsers(): JSX.Element { + const { waitingUnignored, ignoredUserIds } = this.state; + + const userIds = !ignoredUserIds?.length + ? _t('You have no ignored users.') + : ignoredUserIds.map((u) => { + return ( + + ); + }); + + return ( +
    + { _t('Ignored users') } +
    + { userIds } +
    +
    + ); + } + + private renderManageInvites(): JSX.Element { + const { invitedRoomIds } = this.state; + + if (invitedRoomIds.size === 0) { + return null; + } + + return ( +
    + { _t('Bulk options') } + + { _t("Accept all %(invitedRooms)s invites", { invitedRooms: invitedRoomIds.size }) } + + + { _t("Reject all %(invitedRooms)s invites", { invitedRooms: invitedRoomIds.size }) } + + { this.state.managingInvites ? :
    } +
    + ); + } + + private onPersonalRuleChanged = (e) => { + this.setState({ newPersonalRule: e.target.value }); + }; + + private onNewListChanged = (e) => { + this.setState({ newList: e.target.value }); + }; + + private onAddPersonalRule = async (e) => { + e.preventDefault(); + e.stopPropagation(); + + let kind = RULE_SERVER; + if (this.state.newPersonalRule.startsWith("@")) { + kind = RULE_USER; + } + + this.setState({ busy: true }); + try { + const list = await Mjolnir.sharedInstance().getOrCreatePersonalList(); + await list.banEntity(kind, this.state.newPersonalRule, _t("Ignored/Blocked")); + this.setState({ newPersonalRule: "" }); // this will also cause the new rule to be rendered + } catch (e) { + logger.error(e); + + Modal.createTrackedDialog('Failed to add Mjolnir rule', '', ErrorDialog, { + title: _t('Error adding ignored user/server'), + description: _t('Something went wrong. Please try again or view your console for hints.'), + }); + } finally { + this.setState({ busy: false }); + } + }; + + private onSubscribeList = async (e) => { + e.preventDefault(); + e.stopPropagation(); + + this.setState({ busy: true }); + try { + const room = await MatrixClientPeg.get().joinRoom(this.state.newList); + await Mjolnir.sharedInstance().subscribeToList(room.roomId); + this.setState({ newList: "" }); // this will also cause the new rule to be rendered + } catch (e) { + logger.error(e); + + Modal.createTrackedDialog('Failed to subscribe to Mjolnir list', '', ErrorDialog, { + title: _t('Error subscribing to list'), + description: _t('Please verify the room ID or address and try again.'), + }); + } finally { + this.setState({ busy: false }); + } + }; + + private async removePersonalRule(rule: ListRule) { + this.setState({ busy: true }); + try { + const list = Mjolnir.sharedInstance().getPersonalList(); + await list.unbanEntity(rule.kind, rule.entity); + } catch (e) { + logger.error(e); + + Modal.createTrackedDialog('Failed to remove Mjolnir rule', '', ErrorDialog, { + title: _t('Error removing ignored user/server'), + description: _t('Something went wrong. Please try again or view your console for hints.'), + }); + } finally { + this.setState({ busy: false }); + } + } + + private async unsubscribeFromList(list: BanList) { + this.setState({ busy: true }); + try { + await Mjolnir.sharedInstance().unsubscribeFromList(list.roomId); + await MatrixClientPeg.get().leave(list.roomId); + } catch (e) { + logger.error(e); + + Modal.createTrackedDialog('Failed to unsubscribe from Mjolnir list', '', ErrorDialog, { + title: _t('Error unsubscribing from list'), + description: _t('Please try again or view your console for hints.'), + }); + } finally { + this.setState({ busy: false }); + } + } + + private viewListRules(list: BanList) { + const room = MatrixClientPeg.get().getRoom(list.roomId); + const name = room ? room.name : list.roomId; + + const renderRules = (rules: ListRule[]) => { + if (rules.length === 0) return { _t("None") }; + + const tiles = []; + for (const rule of rules) { + tiles.push(
  • { rule.entity }
  • ); + } + return
      { tiles }
    ; + }; + + Modal.createTrackedDialog('View Mjolnir list rules', '', QuestionDialog, { + title: _t("Ban list rules - %(roomName)s", { roomName: name }), + description: ( +
    +

    { _t("Server rules") }

    + { renderRules(list.serverRules) } +

    { _t("User rules") }

    + { renderRules(list.userRules) } +
    + ), + button: _t("Close"), + hasCancelButton: false, + }); + } + + private renderPersonalBanListRules() { + const list = Mjolnir.sharedInstance().getPersonalList(); + const rules = list ? [...list.userRules, ...list.serverRules] : []; + if (!list || rules.length <= 0) return { _t("You have not ignored anyone.") }; + + const tiles = []; + for (const rule of rules) { + tiles.push( +
  • + this.removePersonalRule(rule)} + disabled={this.state.busy} + > + { _t("Remove") } +   + { rule.entity } +
  • , + ); + } + + return ( +
    +

    { _t("You are currently ignoring:") }

    +
      { tiles }
    +
    + ); + } + + private renderSubscribedBanLists() { + const personalList = Mjolnir.sharedInstance().getPersonalList(); + const lists = Mjolnir.sharedInstance().lists.filter(b => { + return personalList? personalList.roomId !== b.roomId : true; + }); + if (!lists || lists.length <= 0) return { _t("You are not subscribed to any lists") }; + + const tiles = []; + for (const list of lists) { + const room = MatrixClientPeg.get().getRoom(list.roomId); + const name = room ? { room.name } ({ list.roomId }) : list.roomId; + tiles.push( +
  • + this.unsubscribeFromList(list)} + disabled={this.state.busy} + > + { _t("Unsubscribe") } +   + this.viewListRules(list)} + disabled={this.state.busy} + > + { _t("View rules") } +   + { name } +
  • , + ); + } + + return ( +
    +

    { _t("You are currently subscribed to:") }

    +
      { tiles }
    +
    + ); + } + + private async checkTerms(): Promise { + if (!this.state.haveIdServer) { + this.setState({ idServerHasUnsignedTerms: false }); + return; + } + + // By starting the terms flow we get the logic for checking which terms the user has signed + // for free. So we might as well use that for our own purposes. + const idServerUrl = MatrixClientPeg.get().getIdentityServerUrl(); + const authClient = new IdentityAuthClient(); + try { + const idAccessToken = await authClient.getAccessToken({ check: false }); + await startTermsFlow([new Service( + SERVICE_TYPES.IS, + idServerUrl, + idAccessToken, + )], (policiesAndServices, agreedUrls, extraClassNames) => { + return new Promise((resolve, reject) => { + this.setState({ + idServerName: abbreviateUrl(idServerUrl), + requiredPolicyInfo: { + hasTerms: true, + policiesAndServices, + agreedUrls, + resolve, + }, + }); + }); + }); + // User accepted all terms + this.setState({ + requiredPolicyInfo: { + hasTerms: false, + ...this.state.requiredPolicyInfo, + }, + }); + } catch (e) { + logger.warn( + `Unable to reach identity server at ${idServerUrl} to check ` + + `for terms in Settings`, + ); + logger.warn(e); + } + } + + private renderDiscoverySection(): JSX.Element { + if (this.state.requiredPolicyInfo.hasTerms) { + const intro = + { _t( + "Agree to the identity server (%(serverName)s) Terms of Service to " + + "allow yourself to be discoverable by email address or phone number.", + { serverName: this.state.idServerName }, + ) } + ; + return ( +
    + + { /* has its own heading as it includes the current identity server */ } + +
    + ); + } + + const emails = this.state.loading3pids ? : ; + const msisdns = this.state.loading3pids ? : ; + + const threepidSection = this.state.haveIdServer ?
    + { _t("Email addresses") } + { emails } + + { _t("Phone numbers") } + { msisdns } +
    : null; + + return ( +
    + { threepidSection } + { /* has its own heading as it includes the current identity server */ } + +
    + ); + } + + render() { + const brand = SdkConfig.get().brand; + + let analyticsSection; + if (Analytics.canEnable() || PosthogAnalytics.instance.isEnabled()) { + const onClickAnalyticsLearnMore = () => { + if (PosthogAnalytics.instance.isEnabled()) { + showAnalyticsLearnMoreDialog({ + primaryButton: _t("Okay"), + hasCancel: false, + }); + } else { + Analytics.showDetailsModal(); + } + }; + analyticsSection = +
    { _t("Analytics") }
    +
    +

    + { _t("Share anonymous data to help us identify issues. Nothing personal. " + + "No third parties.") } +

    +

    + + { _t("Learn more") } + +

    + { + PosthogAnalytics.instance.isEnabled() ? + : + + } +
    +
    ; + } + + const ignoreUsersPanel = SettingsStore.getValue(UIFeature.AdvancedSettings) ? this.renderIgnoredUsers() : null; + const invitesPanel = SettingsStore.getValue(UIFeature.AdvancedSettings) ? this.renderManageInvites() : null; + + const mjolnirSection = this.state.mjolnirEnabled ? this.renderMjolnir(brand) : null; + + const discoWarning = this.state.requiredPolicyInfo.hasTerms + ? {_t("Warning")} + : null; + + let discoverySection; + if (SettingsStore.getValue(UIFeature.IdentityServer)) { + discoverySection = <> +
    { discoWarning } { _t("Discovery") }
    + { this.renderDiscoverySection() } + ; + } + + return ( +
    +
    { _t("Privacy") }
    + { discoverySection } + { analyticsSection } + { ignoreUsersPanel } + { invitesPanel } + { mjolnirSection } +
    + ); + } + + renderMjolnir(brand: string) { + return ( +
    +
    { _t("Ignored users") }
    +
    +
    + { _t("âš  These settings are meant for advanced users.") }
    +
    + { _t( + "Add users and servers you want to ignore here. Use asterisks " + + "to have %(brand)s match any characters. For example, @bot:* " + + "would ignore all users that have the name 'bot' on any server.", + { brand }, { code: (s) => { s } }, + ) }
    +
    + { _t( + "Ignoring people is done through ban lists which contain rules for " + + "who to ban. Subscribing to a ban list means the users/servers blocked by " + + "that list will be hidden from you.", + ) } +
    +
    +
    + { _t("Personal ban list") } +
    + { _t( + "Your personal ban list holds all the users/servers you personally don't " + + "want to see messages from. After ignoring your first user/server, a new room " + + "will show up in your room list named 'My Ban List' - stay in this room to keep " + + "the ban list in effect.", + ) } +
    +
    + { this.renderPersonalBanListRules() } +
    +
    +
    + + + { _t("Ignore") } + + +
    +
    +
    + { _t("Subscribed lists") } +
    + { _t("Subscribing to a ban list will cause you to join it!") } +   + { _t( + "If this isn't what you want, please use a different tool to ignore users.", + ) } +
    +
    + { this.renderSubscribedBanLists() } +
    +
    +
    + + + { _t("Subscribe") } + + +
    +
    +
    + ); + } +} diff --git a/src/components/views/settings/tabs/user/SecureMessagingUserSettingsTab.tsx b/src/components/views/settings/tabs/user/SecureMessagingUserSettingsTab.tsx new file mode 100644 index 00000000000..0bcf0e4593d --- /dev/null +++ b/src/components/views/settings/tabs/user/SecureMessagingUserSettingsTab.tsx @@ -0,0 +1,109 @@ +/* +Copyright 2019 - 2022 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 { _t } from "../../../../../languageHandler"; +import { privateShouldBeEncrypted } from "../../../../../utils/rooms"; +import SecureBackupPanel from "../../SecureBackupPanel"; +import SettingsStore from "../../../../../settings/SettingsStore"; +import { UIFeature } from "../../../../../settings/UIFeature"; +import E2eTrustPanel from "../../E2eTrustPanel"; +import CryptographyPanel from "../../CryptographyPanel"; +import E2eDevicesPanel from "../../E2eDevicesPanel"; +import CrossSigningPanel from "../../CrossSigningPanel"; +import EventIndexPanel from "../../EventIndexPanel"; + +interface IProps { + closeSettingsFn: () => void; +} + +interface IState {} + +export default class SecureMessagingUserSettingsTab extends React.Component { + constructor(props: IProps) { + super(props); + + this.state = {}; + } + + public render(): JSX.Element { + const secureBackup = ( +
    + { _t("Message key backup") } +
    + +
    +
    + ); + + // XXX: There's no such panel in the current cross-signing designs, but + // it's useful to have for testing the feature. If there's no interest + // in having advanced details here once all flows are implemented, we + // can remove this. + const crossSigning = ( +
    + { _t("Cross-signing") } +
    + +
    +
    + ); + + let warning; + if (!privateShouldBeEncrypted()) { + warning =
    + { _t("Your server admin has disabled secure messaging by default. You will need to enable it on individual rooms and Direct Messages where you want it.") } +
    ; + } + + let advancedSection; + if (SettingsStore.getValue(UIFeature.AdvancedSettings)) { + // only show the section if there's something to show + advancedSection = <> +
    + { _t("Advanced") } +
    + { secureBackup } + { crossSigning } + +
    +
    + ; + } + + return ( +
    + { warning } +
    { _t("Secure messaging") }
    +

    { _t("Secure messages are protected using end-to-end encryption. This ensures that only you and your intended recipients can read them.") }

    +
    { _t("Your devices") }
    +
    + +
    +
    { _t("Trust") }
    +
    + +
    +
    { _t("Search") }
    +
    + +
    + { advancedSection } +
    + ); + } +} diff --git a/src/components/views/settings/tabs/user/SecurityUserSettingsTab.tsx b/src/components/views/settings/tabs/user/SecurityUserSettingsTab.tsx deleted file mode 100644 index e577cd873fc..00000000000 --- a/src/components/views/settings/tabs/user/SecurityUserSettingsTab.tsx +++ /dev/null @@ -1,380 +0,0 @@ -/* -Copyright 2019 - 2022 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 { sleep } from "matrix-js-sdk/src/utils"; -import { Room, RoomEvent } from "matrix-js-sdk/src/models/room"; -import { logger } from "matrix-js-sdk/src/logger"; - -import { _t } from "../../../../../languageHandler"; -import { MatrixClientPeg } from "../../../../../MatrixClientPeg"; -import AccessibleButton from "../../../elements/AccessibleButton"; -import Analytics from "../../../../../Analytics"; -import dis from "../../../../../dispatcher/dispatcher"; -import { SettingLevel } from "../../../../../settings/SettingLevel"; -import SecureBackupPanel from "../../SecureBackupPanel"; -import SettingsStore from "../../../../../settings/SettingsStore"; -import { UIFeature } from "../../../../../settings/UIFeature"; -import E2eAdvancedPanel, { isE2eAdvancedPanelPossible } from "../../E2eAdvancedPanel"; -import { ActionPayload } from "../../../../../dispatcher/payloads"; -import CryptographyPanel from "../../CryptographyPanel"; -import DevicesPanel from "../../DevicesPanel"; -import SettingsFlag from "../../../elements/SettingsFlag"; -import CrossSigningPanel from "../../CrossSigningPanel"; -import EventIndexPanel from "../../EventIndexPanel"; -import InlineSpinner from "../../../elements/InlineSpinner"; -import { PosthogAnalytics } from "../../../../../PosthogAnalytics"; -import { showDialog as showAnalyticsLearnMoreDialog } from "../../../dialogs/AnalyticsLearnMoreDialog"; -import { privateShouldBeEncrypted } from "../../../../../utils/rooms"; - -interface IIgnoredUserProps { - userId: string; - onUnignored: (userId: string) => void; - inProgress: boolean; -} - -export class IgnoredUser extends React.Component { - private onUnignoreClicked = (): void => { - this.props.onUnignored(this.props.userId); - }; - - public render(): JSX.Element { - const id = `mx_SecurityUserSettingsTab_ignoredUser_${this.props.userId}`; - return ( -
    - - { _t('Unignore') } - - { this.props.userId } -
    - ); - } -} - -interface IProps { - closeSettingsFn: () => void; -} - -interface IState { - ignoredUserIds: string[]; - waitingUnignored: string[]; - managingInvites: boolean; - invitedRoomIds: Set; -} - -export default class SecurityUserSettingsTab extends React.Component { - private dispatcherRef: string; - - constructor(props: IProps) { - super(props); - - // Get rooms we're invited to - const invitedRoomIds = new Set(this.getInvitedRooms().map(room => room.roomId)); - - this.state = { - ignoredUserIds: MatrixClientPeg.get().getIgnoredUsers(), - waitingUnignored: [], - managingInvites: false, - invitedRoomIds, - }; - } - - private onAction = ({ action }: ActionPayload) => { - if (action === "ignore_state_changed") { - const ignoredUserIds = MatrixClientPeg.get().getIgnoredUsers(); - const newWaitingUnignored = this.state.waitingUnignored.filter(e => ignoredUserIds.includes(e)); - this.setState({ ignoredUserIds, waitingUnignored: newWaitingUnignored }); - } - }; - - public componentDidMount(): void { - this.dispatcherRef = dis.register(this.onAction); - MatrixClientPeg.get().on(RoomEvent.MyMembership, this.onMyMembership); - } - - public componentWillUnmount(): void { - dis.unregister(this.dispatcherRef); - MatrixClientPeg.get().removeListener(RoomEvent.MyMembership, this.onMyMembership); - } - - private updateAnalytics = (checked: boolean): void => { - checked ? Analytics.enable() : Analytics.disable(); - }; - - private onMyMembership = (room: Room, membership: string): void => { - if (room.isSpaceRoom()) { - return; - } - - if (membership === "invite") { - this.addInvitedRoom(room); - } else if (this.state.invitedRoomIds.has(room.roomId)) { - // The user isn't invited anymore - this.removeInvitedRoom(room.roomId); - } - }; - - private addInvitedRoom = (room: Room): void => { - this.setState(({ invitedRoomIds }) => ({ - invitedRoomIds: new Set(invitedRoomIds).add(room.roomId), - })); - }; - - private removeInvitedRoom = (roomId: string): void => { - this.setState(({ invitedRoomIds }) => { - const newInvitedRoomIds = new Set(invitedRoomIds); - newInvitedRoomIds.delete(roomId); - - return { - invitedRoomIds: newInvitedRoomIds, - }; - }); - }; - - private onUserUnignored = async (userId: string): Promise => { - const { ignoredUserIds, waitingUnignored } = this.state; - const currentlyIgnoredUserIds = ignoredUserIds.filter(e => !waitingUnignored.includes(e)); - - const index = currentlyIgnoredUserIds.indexOf(userId); - if (index !== -1) { - currentlyIgnoredUserIds.splice(index, 1); - this.setState(({ waitingUnignored }) => ({ waitingUnignored: [...waitingUnignored, userId] })); - MatrixClientPeg.get().setIgnoredUsers(currentlyIgnoredUserIds); - } - }; - - private getInvitedRooms = (): Room[] => { - return MatrixClientPeg.get().getRooms().filter((r) => { - return r.hasMembershipState(MatrixClientPeg.get().getUserId(), "invite"); - }); - }; - - private manageInvites = async (accept: boolean): Promise => { - this.setState({ - managingInvites: true, - }); - - // iterate with a normal for loop in order to retry on action failure - const invitedRoomIdsValues = Array.from(this.state.invitedRoomIds); - - // Execute all acceptances/rejections sequentially - const cli = MatrixClientPeg.get(); - const action = accept ? cli.joinRoom.bind(cli) : cli.leave.bind(cli); - for (let i = 0; i < invitedRoomIdsValues.length; i++) { - const roomId = invitedRoomIdsValues[i]; - - // Accept/reject invite - await action(roomId).then(() => { - // No error, update invited rooms button - this.removeInvitedRoom(roomId); - }, async (e) => { - // Action failure - if (e.errcode === "M_LIMIT_EXCEEDED") { - // Add a delay between each invite change in order to avoid rate - // limiting by the server. - await sleep(e.retry_after_ms || 2500); - - // Redo last action - i--; - } else { - // Print out error with joining/leaving room - logger.warn(e); - } - }); - } - - this.setState({ - managingInvites: false, - }); - }; - - private onAcceptAllInvitesClicked = (): void => { - this.manageInvites(true); - }; - - private onRejectAllInvitesClicked = (): void => { - this.manageInvites(false); - }; - - private renderIgnoredUsers(): JSX.Element { - const { waitingUnignored, ignoredUserIds } = this.state; - - const userIds = !ignoredUserIds?.length - ? _t('You have no ignored users.') - : ignoredUserIds.map((u) => { - return ( - - ); - }); - - return ( -
    - { _t('Ignored users') } -
    - { userIds } -
    -
    - ); - } - - private renderManageInvites(): JSX.Element { - const { invitedRoomIds } = this.state; - - if (invitedRoomIds.size === 0) { - return null; - } - - return ( -
    - { _t('Bulk options') } - - { _t("Accept all %(invitedRooms)s invites", { invitedRooms: invitedRoomIds.size }) } - - - { _t("Reject all %(invitedRooms)s invites", { invitedRooms: invitedRoomIds.size }) } - - { this.state.managingInvites ? :
    } -
    - ); - } - - public render(): JSX.Element { - const secureBackup = ( -
    - { _t("Secure Backup") } -
    - -
    -
    - ); - - const eventIndex = ( -
    - { _t("Message search") } - -
    - ); - - // XXX: There's no such panel in the current cross-signing designs, but - // it's useful to have for testing the feature. If there's no interest - // in having advanced details here once all flows are implemented, we - // can remove this. - const crossSigning = ( -
    - { _t("Cross-signing") } -
    - -
    -
    - ); - - let warning; - if (!privateShouldBeEncrypted()) { - warning =
    - { _t("Your server admin has disabled end-to-end encryption by default " + - "in private rooms & Direct Messages.") } -
    ; - } - - let privacySection; - if (Analytics.canEnable() || PosthogAnalytics.instance.isEnabled()) { - const onClickAnalyticsLearnMore = () => { - if (PosthogAnalytics.instance.isEnabled()) { - showAnalyticsLearnMoreDialog({ - primaryButton: _t("Okay"), - hasCancel: false, - }); - } else { - Analytics.showDetailsModal(); - } - }; - privacySection = -
    { _t("Privacy") }
    -
    - { _t("Analytics") } -
    -

    - { _t("Share anonymous data to help us identify issues. Nothing personal. " + - "No third parties.") } -

    -

    - - { _t("Learn more") } - -

    -
    - { - PosthogAnalytics.instance.isEnabled() ? - : - - } -
    -
    ; - } - - let advancedSection; - if (SettingsStore.getValue(UIFeature.AdvancedSettings)) { - const ignoreUsersPanel = this.renderIgnoredUsers(); - const invitesPanel = this.renderManageInvites(); - const e2ePanel = isE2eAdvancedPanelPossible() ? : null; - // only show the section if there's something to show - if (ignoreUsersPanel || invitesPanel || e2ePanel) { - advancedSection = <> -
    { _t("Advanced") }
    -
    - { ignoreUsersPanel } - { invitesPanel } - { e2ePanel } -
    - ; - } - } - - return ( -
    - { warning } -
    { _t("Where you're signed in") }
    -
    - - { _t( - "Manage your signed-in devices below. " + - "A device's name is visible to people you communicate with.", - ) } - - -
    -
    { _t("Encryption") }
    -
    - { secureBackup } - { eventIndex } - { crossSigning } - -
    - { privacySection } - { advancedSection } -
    - ); - } -} diff --git a/src/components/views/settings/tabs/user/VoiceUserSettingsTab.tsx b/src/components/views/settings/tabs/user/VoiceUserSettingsTab.tsx index 18a0b0b288c..7093314eacd 100644 --- a/src/components/views/settings/tabs/user/VoiceUserSettingsTab.tsx +++ b/src/components/views/settings/tabs/user/VoiceUserSettingsTab.tsx @@ -104,9 +104,9 @@ export default class VoiceUserSettingsTab extends React.Component<{}, IState> { logger.log("Failed to list userMedia devices", error); const brand = SdkConfig.get().brand; Modal.createTrackedDialog('No media permissions', '', ErrorDialog, { - title: _t('No media permissions'), + title: _t('Unable to access microphone and camera'), description: _t( - 'You may need to manually permit %(brand)s to access your microphone/webcam', + 'You may need to manually permit %(brand)s to access your microphone and camera.', { brand }, ), }); @@ -159,30 +159,30 @@ export default class VoiceUserSettingsTab extends React.Component<{}, IState> { if (!this.state.mediaDevices) { requestButton = (
    -

    { _t("Missing media permissions, click the button below to request.") }

    +

    { _t("%(brand)s needs permission to access your microphone and camera. Use the button below to request access.", { brand: SdkConfig.get().brand }) }

    - { _t("Request media permissions") } + { _t("Request access") }
    ); } else if (this.state.mediaDevices) { speakerDropdown = ( this.renderDropdown(MediaDeviceKindEnum.AudioOutput, _t("Audio Output")) || -

    { _t('No Audio Outputs detected') }

    +

    { _t('No audio output detected') }

    ); microphoneDropdown = ( this.renderDropdown(MediaDeviceKindEnum.AudioInput, _t("Microphone")) || -

    { _t('No Microphones detected') }

    +

    { _t('No microphone detected') }

    ); webcamDropdown = ( this.renderDropdown(MediaDeviceKindEnum.VideoInput, _t("Camera")) || -

    { _t('No Webcams detected') }

    +

    { _t('No camera detected') }

    ); } return (
    -
    { _t("Voice & Video") }
    +
    { _t("Audio & Video") }
    { requestButton } { speakerDropdown } diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 445c90d6c00..bca91da9b5f 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -461,11 +461,11 @@ "Verifies a user, session, and pubkey tuple": "Verifies a user, session, and pubkey tuple", "Unknown (user, session) pair: (%(userId)s, %(deviceId)s)": "Unknown (user, session) pair: (%(userId)s, %(deviceId)s)", "Session already verified!": "Session already verified!", - "WARNING: Session already verified, but keys do NOT MATCH!": "WARNING: Session already verified, but keys do NOT MATCH!", - "WARNING: KEY VERIFICATION FAILED! The signing key for %(userId)s and session %(deviceId)s is \"%(fprint)s\" which does not match the provided key \"%(fingerprint)s\". This could mean your communications are being intercepted!": "WARNING: KEY VERIFICATION FAILED! The signing key for %(userId)s and session %(deviceId)s is \"%(fprint)s\" which does not match the provided key \"%(fingerprint)s\". This could mean your communications are being intercepted!", + "WARNING: Device already verified, but keys do NOT MATCH!": "WARNING: Device already verified, but keys do NOT MATCH!", + "WARNING: KEY VERIFICATION FAILED! The signing key for %(userId)s and device %(deviceId)s is \"%(fprint)s\" which does not match the provided key \"%(fingerprint)s\". This could mean your communications are being intercepted!": "WARNING: KEY VERIFICATION FAILED! The signing key for %(userId)s and device %(deviceId)s is \"%(fprint)s\" which does not match the provided key \"%(fingerprint)s\". This could mean your communications are being intercepted!", "Verified key": "Verified key", - "The signing key you provided matches the signing key you received from %(userId)s's session %(deviceId)s. Session marked as verified.": "The signing key you provided matches the signing key you received from %(userId)s's session %(deviceId)s. Session marked as verified.", - "Forces the current outbound group session in an encrypted room to be discarded": "Forces the current outbound group session in an encrypted room to be discarded", + "The signing key you provided matches the signing key you received from %(userId)s's device %(deviceId)s. Device marked as verified.": "The signing key you provided matches the signing key you received from %(userId)s's device %(deviceId)s. Device marked as verified.", + "Forces the current outbound group device in an encrypted room to be discarded": "Forces the current outbound group device in an encrypted room to be discarded", "Sends the given message coloured as a rainbow": "Sends the given message coloured as a rainbow", "Sends the given emote coloured as a rainbow": "Sends the given emote coloured as a rainbow", "Displays list of commands with usages and descriptions": "Displays list of commands with usages and descriptions", @@ -780,9 +780,9 @@ "Share anonymous data to help us identify issues. Nothing personal. No third parties. Learn More": "Share anonymous data to help us identify issues. Nothing personal. No third parties. Learn More", "Yes": "Yes", "No": "No", - "You have unverified logins": "You have unverified logins", - "Review to ensure your account is safe": "Review to ensure your account is safe", - "Review": "Review", + "New logins. Were they you?": "New logins. Were they you?", + "Check your logged in devices to ensure your account is safe and that they can access your secure messages.": "Check your logged in devices to ensure your account is safe and that they can access your secure messages.", + "Check your logins": "Check your logins", "Later": "Later", "Don't miss a reply": "Don't miss a reply", "Notifications": "Notifications", @@ -803,16 +803,15 @@ "Contact your server admin.": "Contact your server admin.", "Warning": "Warning", "Ok": "Ok", - "Set up Secure Backup": "Set up Secure Backup", - "Encryption upgrade available": "Encryption upgrade available", - "Verify this session": "Verify this session", + "Set up recovery for secure messaging": "Set up recovery for secure messaging", + "Secure messaging upgrade available": "Secure messaging upgrade available", + "Set up secure messaging on this device": "Set up secure messaging on this device", + "Set up": "Set up", "Upgrade": "Upgrade", - "Verify": "Verify", "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", + "You won't be able to receive secure messages nor access past secure messages until you complete setup.": "You won't be able to receive secure messages nor access past secure messages until you complete setup.", "New login. Was this you?": "New login. Was this you?", "%(deviceId)s from %(ip)s": "%(deviceId)s from %(ip)s", - "Check your devices": "Check your devices", "What's new?": "What's new?", "What's New": "What's New", "Update": "Update", @@ -858,7 +857,7 @@ "Moderation": "Moderation", "Message Previews": "Message Previews", "Themes": "Themes", - "Encryption": "Encryption", + "Secure messaging": "Secure messaging", "Experimental": "Experimental", "Developer": "Developer", "Let moderators hide messages pending moderation.": "Let moderators hide messages pending moderation.", @@ -951,7 +950,8 @@ "Show previews/thumbnails for images": "Show previews/thumbnails for images", "Enable message search in encrypted rooms": "Enable message search in encrypted rooms", "How fast should messages be downloaded.": "How fast should messages be downloaded.", - "Manually verify all remote sessions": "Manually verify all remote sessions", + "Verify each individual device used by a user before sending secure messages to it, not trusting the recipient's cross-signing": "Verify each individual device used by a user before sending secure messages to it, not trusting the recipient's cross-signing", + "Don't send secure messages to devices that have not been verified by the recipient": "Don't send secure messages to devices that have not been verified by the recipient", "IRC display name width": "IRC display name width", "Show chat effects (animations when receiving e.g. confetti)": "Show chat effects (animations when receiving e.g. confetti)", "Show all rooms in Home": "Show all rooms in Home", @@ -1177,6 +1177,22 @@ "Jump to first unread room.": "Jump to first unread room.", "Jump to first invite.": "Jump to first invite.", "Space options": "Space options", + "Your homeserver does not support device management.": "Your homeserver does not support device management.", + "Unable to load device list": "Unable to load device list", + "Confirm logging out these devices by using Single Sign On to prove your identity.|other": "Confirm logging out these devices by using Single Sign On to prove your identity.", + "Confirm logging out these devices by using Single Sign On to prove your identity.|one": "Confirm logging out this device by using Single Sign On to prove your identity.", + "Confirm signing out these devices": "Confirm signing out these devices", + "Click the button below to confirm signing out these devices.|other": "Click the button below to confirm signing out these devices.", + "Click the button below to confirm signing out these devices.|one": "Click the button below to confirm signing out this device.", + "Sign out devices|other": "Sign out devices", + "Sign out devices|one": "Sign out device", + "Authentication": "Authentication", + "Deselect all": "Deselect all", + "Select all": "Select all", + "Sign out %(count)s selected devices|other": "Sign out %(count)s selected devices", + "Sign out %(count)s selected devices|one": "Sign out %(count)s selected device", + "You aren't signed in to any other devices.": "You aren't signed in to any other devices.", + "This device": "This device", "Remove": "Remove", "This bridge was provisioned by .": "This bridge was provisioned by .", "This bridge is managed by .": "This bridge is managed by .", @@ -1186,8 +1202,8 @@ "Upload new:": "Upload new:", "No display name": "No display name", "Warning!": "Warning!", - "Changing password will currently reset any end-to-end encryption keys on all sessions, making encrypted chat history unreadable, unless you first export your room keys and re-import them afterwards. In future this will be improved.": "Changing password will currently reset any end-to-end encryption keys on all sessions, making encrypted chat history unreadable, unless you first export your room keys and re-import them afterwards. In future this will be improved.", - "Export E2E room keys": "Export E2E room keys", + "Changing password will currently reset any end-to-end encryption keys on all devices, making encrypted chat history unreadable, unless you first export your messaage keys and re-import them afterwards. In future this will be improved.": "Changing password will currently reset any end-to-end encryption keys on all devices, making encrypted chat history unreadable, unless you first export your messaage keys and re-import them afterwards. In future this will be improved.", + "Export message keys": "Export message keys", "New passwords don't match": "New passwords don't match", "Passwords can't be empty": "Passwords can't be empty", "Do you want to set an email address?": "Do you want to set an email address?", @@ -1199,8 +1215,10 @@ "Your homeserver does not support cross-signing.": "Your homeserver does not support cross-signing.", "Cross-signing is ready for use.": "Cross-signing is ready for use.", "Cross-signing is ready but keys are not backed up.": "Cross-signing is ready but keys are not backed up.", - "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.", + "Your account has a cross-signing identity in secret storage, but it is not yet trusted by this device.": "Your account has a cross-signing identity in secret storage, but it is not yet trusted by this device.", "Cross-signing is not set up.": "Cross-signing is not set up.", + "Set up Secure Backup": "Set up Secure Backup", + "Verify this device": "Verify this device", "Reset": "Reset", "Cross-signing public keys:": "Cross-signing public keys:", "in memory": "in memory", @@ -1216,35 +1234,31 @@ "Homeserver feature support:": "Homeserver feature support:", "exists": "exists", "": "", - "Import E2E room keys": "Import E2E room keys", + "Import message keys": "Import message keys", "Cryptography": "Cryptography", "Session ID:": "Session ID:", "Session key:": "Session key:", - "Your homeserver does not support device management.": "Your homeserver does not support device management.", - "Unable to load device list": "Unable to load device list", - "Confirm logging out these devices by using Single Sign On to prove your identity.|other": "Confirm logging out these devices by using Single Sign On to prove your identity.", - "Confirm logging out these devices by using Single Sign On to prove your identity.|one": "Confirm logging out this device by using Single Sign On to prove your identity.", - "Confirm signing out these devices": "Confirm signing out these devices", - "Click the button below to confirm signing out these devices.|other": "Click the button below to confirm signing out these devices.", - "Click the button below to confirm signing out these devices.|one": "Click the button below to confirm signing out this device.", - "Sign out devices|other": "Sign out devices", - "Sign out devices|one": "Sign out device", - "Authentication": "Authentication", - "Deselect all": "Deselect all", - "Select all": "Select all", - "Verified devices": "Verified devices", - "Unverified devices": "Unverified devices", - "Devices without encryption support": "Devices without encryption support", - "Sign out %(count)s selected devices|other": "Sign out %(count)s selected devices", - "Sign out %(count)s selected devices|one": "Sign out %(count)s selected device", - "You aren't signed into any other devices.": "You aren't signed into any other devices.", - "This device": "This device", "Failed to set display name": "Failed to set display name", "Last seen %(date)s at %(ip)s": "Last seen %(date)s at %(ip)s", + "Set up for secure messaging": "Set up for secure messaging", "Sign Out": "Sign Out", - "Display Name": "Display Name", "Rename": "Rename", - "Individually verify each session used by a user to mark it as trusted, not trusting cross-signed devices.": "Individually verify each session used by a user to mark it as trusted, not trusting cross-signed devices.", + "Display Name": "Display Name", + "Individually verify each device used by a user to mark it as trusted, not trusting cross-signed devices.": "Individually verify each device used by a user to mark it as trusted, not trusting cross-signed devices.", + "Secure messaging is set up on this device.": "Secure messaging is set up on this device.", + "Save recovery key": "Save recovery key", + "Delete recovery key from this device": "Delete recovery key from this device", + "Secure messaging is not set up on this device. Set up secure messaging to access past encrypted messages and allow others to trust it.": "Secure messaging is not set up on this device. Set up secure messaging to access past encrypted messages and allow others to trust it.", + "Set up now": "Set up now", + "Other devices": "Other devices", + "Lax": "Lax", + "Strict": "Strict", + "Paranoid": "Paranoid", + "Secure messages will be sent to all recipients and devices irrespective of whether they have verified them.": "Secure messages will be sent to all recipients and devices irrespective of whether they have verified them.", + "Secure messages will only be sent to devices of a recipient where the recipient has completed verification of that device.": "Secure messages will only be sent to devices of a recipient where the recipient has completed verification of that device.", + "Secure messages will only be sent to recipients whom you have completed verification with.": "Secure messages will only be sent to recipients whom you have completed verification with.", + "Secure messages will only be sent to recipient devices that you have verified yourself.": "Secure messages will only be sent to recipient devices that you have verified yourself.", + "Custom level, use the advanced controls below.": "Custom level, use the advanced controls below.", "Securely cache encrypted messages locally for them to appear in search results, using %(size)s to store messages from %(rooms)s rooms.|other": "Securely cache encrypted messages locally for them to appear in search results, using %(size)s to store messages from %(rooms)s rooms.", "Securely cache encrypted messages locally for them to appear in search results, using %(size)s to store messages from %(rooms)s rooms.|one": "Securely cache encrypted messages locally for them to appear in search results, using %(size)s to store messages from %(rooms)s room.", "Manage": "Manage", @@ -1292,9 +1306,9 @@ "An error occurred whilst saving your notification preferences.": "An error occurred whilst saving your notification preferences.", "Enable for this account": "Enable for this account", "Enable email notifications for %(email)s": "Enable email notifications for %(email)s", - "Enable desktop notifications for this session": "Enable desktop notifications for this session", + "Enable desktop notifications for this device": "Enable desktop notifications for this device", "Show message in desktop notification": "Show message in desktop notification", - "Enable audible notifications for this session": "Enable audible notifications for this session", + "Enable audible notifications for this device": "Enable audible notifications for this device", "Clear notifications": "Clear notifications", "Keyword": "Keyword", "New keyword": "New keyword", @@ -1314,32 +1328,31 @@ "Are you sure? You will lose your encrypted messages if your keys are not backed up properly.": "Are you sure? You will lose your encrypted messages if your keys are not backed up properly.", "Unable to load key backup status": "Unable to load key backup status", "Restore from Backup": "Restore from Backup", - "This session is backing up your keys. ": "This session is backing up your keys. ", - "This session is not backing up your keys, but you do have an existing backup you can restore from and add to going forward.": "This session is not backing up your keys, but you do have an existing backup you can restore from and add to going forward.", - "Connect this session to key backup before signing out to avoid losing any keys that may only be on this session.": "Connect this session to key backup before signing out to avoid losing any keys that may only be on this session.", - "Connect this session to Key Backup": "Connect this session to Key Backup", + "This device is backing up your keys. ": "This device is backing up your keys. ", + "This device is not backing up your keys, but you do have an existing backup you can restore from and add to going forward.": "This device is not backing up your keys, but you do have an existing backup you can restore from and add to going forward.", + "Connect this device to key backup before signing out to avoid losing any keys that may only be on this device.": "Connect this device to key backup before signing out to avoid losing any keys that may only be on this device.", + "Connect this device to Key Backup": "Connect this device to Key Backup", "Backing up %(sessionsRemaining)s keys...": "Backing up %(sessionsRemaining)s keys...", "All keys backed up": "All keys backed up", "Backup has a valid signature from this user": "Backup has a valid signature from this user", "Backup has a invalid signature from this user": "Backup has a invalid signature from this user", "Backup has a signature from unknown user with ID %(deviceId)s": "Backup has a signature from unknown user with ID %(deviceId)s", - "Backup has a signature from unknown session with ID %(deviceId)s": "Backup has a signature from unknown session with ID %(deviceId)s", - "Backup has a valid signature from this session": "Backup has a valid signature from this session", - "Backup has an invalid signature from this session": "Backup has an invalid signature from this session", - "Backup has a valid signature from verified session ": "Backup has a valid signature from verified session ", - "Backup has a valid signature from unverified session ": "Backup has a valid signature from unverified session ", - "Backup has an invalid signature from verified session ": "Backup has an invalid signature from verified session ", - "Backup has an invalid signature from unverified session ": "Backup has an invalid signature from unverified session ", - "Backup is not signed by any of your sessions": "Backup is not signed by any of your sessions", - "This backup is trusted because it has been restored on this session": "This backup is trusted because it has been restored on this session", + "Backup has a signature from unknown device with ID %(deviceId)s": "Backup has a signature from unknown device with ID %(deviceId)s", + "Backup has a valid signature from this device": "Backup has a valid signature from this device", + "Backup has an invalid signature from this device": "Backup has an invalid signature from this device", + "Backup has a valid signature from verified device ": "Backup has a valid signature from verified device ", + "Backup has a valid signature from unverified device ": "Backup has a valid signature from unverified device ", + "Backup has an invalid signature from verified device ": "Backup has an invalid signature from verified device ", + "Backup has an invalid signature from unverified device ": "Backup has an invalid signature from unverified device ", + "Backup is not signed by any of your devices": "Backup is not signed by any of your devices", + "This backup is trusted because it has been restored on this device": "This backup is trusted because it has been restored on this device", "Backup version:": "Backup version:", "Algorithm:": "Algorithm:", - "Your keys are not being backed up from this session.": "Your keys are not being backed up from this session.", + "Your keys are not being backed up from this device.": "Your keys are not being backed up from this device.", "Back up your keys before signing out to avoid losing them.": "Back up your keys before signing out to avoid losing them.", - "Set up": "Set up", "well formed": "well formed", "unexpected type": "unexpected type", - "Back up your encryption keys with your account data in case you lose access to your sessions. Your keys will be secured with a unique Security Key.": "Back up your encryption keys with your account data in case you lose access to your sessions. Your keys will be secured with a unique Security Key.", + "Back up your secure messaging keys with your account data in case you lose access to your devices. Your keys will be secured with a recovery key.": "Back up your secure messaging keys with your account data in case you lose access to your devices. Your keys will be secured with a recovery key.", "Backup key stored:": "Backup key stored:", "not stored": "not stored", "Backup key cached:": "Backup key cached:", @@ -1394,24 +1407,25 @@ "Downloading update...": "Downloading update...", "New version available. Update now.": "New version available. Update now.", "Check for update": "Check for update", - "Set the name of a font installed on your system & %(brand)s will attempt to use it.": "Set the name of a font installed on your system & %(brand)s will attempt to use it.", - "Customise your appearance": "Customise your appearance", - "Appearance Settings only affect this %(brand)s session.": "Appearance Settings only affect this %(brand)s session.", "Failed to change password. Is your password correct?": "Failed to change password. Is your password correct?", "Success": "Success", - "Your password was successfully changed. You will not receive push notifications on other sessions until you log back in to them": "Your password was successfully changed. You will not receive push notifications on other sessions until you log back in to them", + "Your password was successfully changed. You will not receive push notifications on other devices until you log back in to them": "Your password was successfully changed. You will not receive push notifications on other devices until you log back in to them", "Email addresses": "Email addresses", "Phone numbers": "Phone numbers", "Set a new account password...": "Set a new account password...", + "Change password": "Change password", + "Close account": "Close account", + "Deactivating your account is a permanent action - be careful!": "Deactivating your account is a permanent action - be careful!", + "Deactivate Account": "Deactivate Account", + "Danger zone": "Danger zone", "Account": "Account", + "Where you're signed in": "Where you're signed in", + "Manage your signed-in devices below.": "Manage your signed-in devices below.", + "Set the name of a font installed on your system & %(brand)s will attempt to use it.": "Set the name of a font installed on your system & %(brand)s will attempt to use it.", + "Customise your appearance": "Customise your appearance", + "Appearance Settings only affect this %(brand)s device.": "Appearance Settings only affect this %(brand)s device.", "Language and region": "Language and region", "Spell check dictionaries": "Spell check dictionaries", - "Agree to the identity server (%(serverName)s) Terms of Service to allow yourself to be discoverable by email address or phone number.": "Agree to the identity server (%(serverName)s) Terms of Service to allow yourself to be discoverable by email address or phone number.", - "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", - "Deactivate account": "Deactivate account", - "Discovery": "Discovery", "%(brand)s version:": "%(brand)s version:", "Olm version:": "Olm version:", "Legal": "Legal", @@ -1436,6 +1450,28 @@ "Keyboard": "Keyboard", "Labs": "Labs", "Feeling experimental? Labs are the best way to get things early, test out new features and help shape them before they actually launch. Learn more.": "Feeling experimental? Labs are the best way to get things early, test out new features and help shape them before they actually launch. Learn more.", + "Start automatically after system login": "Start automatically after system login", + "Warn before quitting": "Warn before quitting", + "Always show the window menu bar": "Always show the window menu bar", + "Show tray icon and minimise window to it on close": "Show tray icon and minimise window to it on close", + "Preferences": "Preferences", + "Room list": "Room list", + "Keyboard shortcuts": "Keyboard shortcuts", + "To view all keyboard shortcuts, click here.": "To view all keyboard shortcuts, click here.", + "Displaying time": "Displaying time", + "Composer": "Composer", + "Code blocks": "Code blocks", + "Images, GIFs and videos": "Images, GIFs and videos", + "Timeline": "Timeline", + "Autocomplete delay (ms)": "Autocomplete delay (ms)", + "Read Marker lifetime (ms)": "Read Marker lifetime (ms)", + "Read Marker off-screen lifetime (ms)": "Read Marker off-screen lifetime (ms)", + "Unignore": "Unignore", + "You have no ignored users.": "You have no ignored users.", + "Ignored users": "Ignored users", + "Bulk options": "Bulk options", + "Accept all %(invitedRooms)s invites": "Accept all %(invitedRooms)s invites", + "Reject all %(invitedRooms)s invites": "Reject all %(invitedRooms)s invites", "Ignored/Blocked": "Ignored/Blocked", "Error adding ignored user/server": "Error adding ignored user/server", "Something went wrong. Please try again or view your console for hints.": "Something went wrong. Please try again or view your console for hints.", @@ -1455,7 +1491,11 @@ "Unsubscribe": "Unsubscribe", "View rules": "View rules", "You are currently subscribed to:": "You are currently subscribed to:", - "Ignored users": "Ignored users", + "Agree to the identity server (%(serverName)s) Terms of Service to allow yourself to be discoverable by email address or phone number.": "Agree to the identity server (%(serverName)s) Terms of Service to allow yourself to be discoverable by email address or phone number.", + "Okay": "Okay", + "Share anonymous data to help us identify issues. Nothing personal. No third parties.": "Share anonymous data to help us identify issues. Nothing personal. No third parties.", + "Discovery": "Discovery", + "Privacy": "Privacy", "⚠ These settings are meant for advanced users.": "⚠ These settings are meant for advanced users.", "Add users and servers you want to ignore here. Use asterisks to have %(brand)s match any characters. For example, @bot:* would ignore all users that have the name 'bot' on any server.": "Add users and servers you want to ignore here. Use asterisks to have %(brand)s match any characters. For example, @bot:* would ignore all users that have the name 'bot' on any server.", "Ignoring people is done through ban lists which contain rules for who to ban. Subscribing to a ban list means the users/servers blocked by that list will be hidden from you.": "Ignoring people is done through ban lists which contain rules for who to ban. Subscribing to a ban list means the users/servers blocked by that list will be hidden from you.", @@ -1469,36 +1509,12 @@ "If this isn't what you want, please use a different tool to ignore users.": "If this isn't what you want, please use a different tool to ignore users.", "Room ID or address of ban list": "Room ID or address of ban list", "Subscribe": "Subscribe", - "Start automatically after system login": "Start automatically after system login", - "Warn before quitting": "Warn before quitting", - "Always show the window menu bar": "Always show the window menu bar", - "Show tray icon and minimise window to it on close": "Show tray icon and minimise window to it on close", - "Preferences": "Preferences", - "Room list": "Room list", - "Keyboard shortcuts": "Keyboard shortcuts", - "To view all keyboard shortcuts, click here.": "To view all keyboard shortcuts, click here.", - "Displaying time": "Displaying time", - "Composer": "Composer", - "Code blocks": "Code blocks", - "Images, GIFs and videos": "Images, GIFs and videos", - "Timeline": "Timeline", - "Autocomplete delay (ms)": "Autocomplete delay (ms)", - "Read Marker lifetime (ms)": "Read Marker lifetime (ms)", - "Read Marker off-screen lifetime (ms)": "Read Marker off-screen lifetime (ms)", - "Unignore": "Unignore", - "You have no ignored users.": "You have no ignored users.", - "Bulk options": "Bulk options", - "Accept all %(invitedRooms)s invites": "Accept all %(invitedRooms)s invites", - "Reject all %(invitedRooms)s invites": "Reject all %(invitedRooms)s invites", - "Secure Backup": "Secure Backup", - "Message search": "Message search", + "Message key backup": "Message key backup", "Cross-signing": "Cross-signing", - "Your server admin has disabled end-to-end encryption by default in private rooms & Direct Messages.": "Your server admin has disabled end-to-end encryption by default in private rooms & Direct Messages.", - "Okay": "Okay", - "Privacy": "Privacy", - "Share anonymous data to help us identify issues. Nothing personal. No third parties.": "Share anonymous data to help us identify issues. Nothing personal. No third parties.", - "Where you're signed in": "Where you're signed in", - "Manage your signed-in devices below. A device's name is visible to people you communicate with.": "Manage your signed-in devices below. A device's name is visible to people you communicate with.", + "Your server admin has disabled secure messaging by default. You will need to enable it on individual rooms and Direct Messages where you want it.": "Your server admin has disabled secure messaging by default. You will need to enable it on individual rooms and Direct Messages where you want it.", + "Secure messages are protected using end-to-end encryption. This ensures that only you and your intended recipients can read them.": "Secure messages are protected using end-to-end encryption. This ensures that only you and your intended recipients can read them.", + "Your devices": "Your devices", + "Search": "Search", "Sidebar": "Sidebar", "Spaces to show": "Spaces to show", "Spaces are ways to group rooms and people. Alongside the spaces you're in, you can use some pre-built ones too.": "Spaces are ways to group rooms and people. Alongside the spaces you're in, you can use some pre-built ones too.", @@ -1509,17 +1525,17 @@ "Rooms outside of a space": "Rooms outside of a space", "Group all your rooms that aren't part of a space in one place.": "Group all your rooms that aren't part of a space in one place.", "Default Device": "Default Device", - "No media permissions": "No media permissions", - "You may need to manually permit %(brand)s to access your microphone/webcam": "You may need to manually permit %(brand)s to access your microphone/webcam", - "Missing media permissions, click the button below to request.": "Missing media permissions, click the button below to request.", - "Request media permissions": "Request media permissions", + "Unable to access microphone and camera": "Unable to access microphone and camera", + "You may need to manually permit %(brand)s to access your microphone and camera.": "You may need to manually permit %(brand)s to access your microphone and camera.", + "%(brand)s needs permission to access your microphone and camera. Use the button below to request access.": "%(brand)s needs permission to access your microphone and camera. Use the button below to request access.", + "Request access": "Request access", "Audio Output": "Audio Output", - "No Audio Outputs detected": "No Audio Outputs detected", + "No audio output detected": "No audio output detected", "Microphone": "Microphone", - "No Microphones detected": "No Microphones detected", + "No microphone detected": "No microphone detected", "Camera": "Camera", - "No Webcams detected": "No Webcams detected", - "Voice & Video": "Voice & Video", + "No camera detected": "No camera detected", + "Audio & Video": "Audio & Video", "This room is not accessible by remote Matrix servers": "This room is not accessible by remote Matrix servers", "Warning: Upgrading a room will not automatically migrate room members to the new version of the room. We'll post a link to the new room in the old version of the room - room members will have to click this link to join the new room.": "Warning: Upgrading a room will not automatically migrate room members to the new version of the room. We'll post a link to the new room in the old version of the room - room members will have to click this link to join the new room.", "Upgrade this space to the recommended room version": "Upgrade this space to the recommended room version", @@ -1565,7 +1581,7 @@ "Change description": "Change description", "Change topic": "Change topic", "Upgrade the room": "Upgrade the room", - "Enable room encryption": "Enable room encryption", + "Enable secure messaging": "Enable secure messaging", "Change server ACLs": "Change server ACLs", "Send reactions": "Send reactions", "Remove messages sent by me": "Remove messages sent by me", @@ -1588,17 +1604,17 @@ "Permissions": "Permissions", "Select the roles required to change various parts of the space": "Select the roles required to change various parts of the space", "Select the roles required to change various parts of the room": "Select the roles required to change various parts of the room", - "Are you sure you want to add encryption to this public room?": "Are you sure you want to add encryption to this public room?", - "It's not recommended to add encryption to public rooms.Anyone can find and join public rooms, so anyone can read messages in them. You'll get none of the benefits of encryption, and you won't be able to turn it off later. Encrypting messages in a public room will make receiving and sending messages slower.": "It's not recommended to add encryption to public rooms.Anyone can find and join public rooms, so anyone can read messages in them. You'll get none of the benefits of encryption, and you won't be able to turn it off later. Encrypting messages in a public room will make receiving and sending messages slower.", - "To avoid these issues, create a new encrypted room for the conversation you plan to have.": "To avoid these issues, create a new encrypted room for the conversation you plan to have.", - "Enable encryption?": "Enable encryption?", - "Once enabled, encryption for a room cannot be disabled. Messages sent in an encrypted room cannot be seen by the server, only by the participants of the room. Enabling encryption may prevent many bots and bridges from working correctly. Learn more about encryption.": "Once enabled, encryption for a room cannot be disabled. Messages sent in an encrypted room cannot be seen by the server, only by the participants of the room. Enabling encryption may prevent many bots and bridges from working correctly. Learn more about encryption.", + "Are you sure you want to enable secure messaging for this public room?": "Are you sure you want to enable secure messaging for this public room?", + "It's not recommended to enable secure messaging in public rooms.Anyone can find and join public rooms, so anyone can read messages in them. You'll get none of the benefits of end-to-end encryption, and you won't be able to turn it off later. Secure messaging in a public room will also make receiving and sending messages slower.": "It's not recommended to enable secure messaging in public rooms.Anyone can find and join public rooms, so anyone can read messages in them. You'll get none of the benefits of end-to-end encryption, and you won't be able to turn it off later. Secure messaging in a public room will also make receiving and sending messages slower.", + "To avoid these issues, create a new secure messaging room for the conversation you plan to have.": "To avoid these issues, create a new secure messaging room for the conversation you plan to have.", + "Enable secure messaging?": "Enable secure messaging?", + "Once enabled, secure messaging for a room cannot be disabled. Messages sent in a secure messaging room cannot be seen by the server, only by the participants of the room. Enabling secure messaging may prevent many bots and bridges from working correctly. Learn more about secure messaging.": "Once enabled, secure messaging for a room cannot be disabled. Messages sent in a secure messaging room cannot be seen by the server, only by the participants of the room. Enabling secure messaging may prevent many bots and bridges from working correctly. Learn more about secure messaging.", "To link to this room, please add an address.": "To link to this room, please add an address.", "Decide who can join %(roomName)s.": "Decide who can join %(roomName)s.", "Failed to update the join rules": "Failed to update the join rules", "Unknown failure": "Unknown failure", "Are you sure you want to make this encrypted room public?": "Are you sure you want to make this encrypted room public?", - "It's not recommended to make encrypted rooms public. It will mean anyone can find and join the room, so anyone can read messages. You'll get none of the benefits of encryption. Encrypting messages in a public room will make receiving and sending messages slower.": "It's not recommended to make encrypted rooms public. It will mean anyone can find and join the room, so anyone can read messages. You'll get none of the benefits of encryption. Encrypting messages in a public room will make receiving and sending messages slower.", + "It's not recommended to make encrypted rooms public. It will mean anyone can find and join the room, so anyone can read messages. You'll get none of the benefits of end-to-end encryption. Encrypting messages in a public room will make receiving and sending messages slower.": "It's not recommended to make encrypted rooms public. It will mean anyone can find and join the room, so anyone can read messages. You'll get none of the benefits of end-to-end encryption. Encrypting messages in a public room will make receiving and sending messages slower.", "To avoid these issues, create a new public room for the conversation you plan to have.": "To avoid these issues, create a new public room for the conversation you plan to have.", "Members only (since the point in time of selecting this option)": "Members only (since the point in time of selecting this option)", "Members only (since they were invited)": "Members only (since they were invited)", @@ -1608,8 +1624,8 @@ "Who can read history?": "Who can read history?", "People with supported clients will be able to join the room without having a registered account.": "People with supported clients will be able to join the room without having a registered account.", "Security & Privacy": "Security & Privacy", - "Once enabled, encryption cannot be disabled.": "Once enabled, encryption cannot be disabled.", - "Encrypted": "Encrypted", + "Encryption": "Encryption", + "Once enabled, secure messaging cannot be disabled.": "Once enabled, secure messaging cannot be disabled.", "Unable to revoke sharing for email address": "Unable to revoke sharing for email address", "Unable to share email address": "Unable to share email address", "Your email address hasn't been verified yet": "Your email address hasn't been verified yet", @@ -1619,14 +1635,14 @@ "Complete": "Complete", "Revoke": "Revoke", "Share": "Share", - "Discovery options will appear once you have added an email above.": "Discovery options will appear once you have added an email above.", + "Discovery options will appear once you have added an email to your account.": "Discovery options will appear once you have added an email to your account.", "Unable to revoke sharing for phone number": "Unable to revoke sharing for phone number", "Unable to share phone number": "Unable to share phone number", "Unable to verify phone number.": "Unable to verify phone number.", "Incorrect verification code": "Incorrect verification code", "Please enter verification code sent via text.": "Please enter verification code sent via text.", "Verification code": "Verification code", - "Discovery options will appear once you have added a phone number above.": "Discovery options will appear once you have added a phone number above.", + "Discovery options will appear once you have added a phone number to your account.": "Discovery options will appear once you have added a phone number to your account.", "Unable to remove contact information": "Unable to remove contact information", "Remove %(email)s?": "Remove %(email)s?", "Invalid Email Address": "Invalid Email Address", @@ -1637,28 +1653,29 @@ "Remove %(phone)s?": "Remove %(phone)s?", "A text message has been sent to +%(msisdn)s. Please enter the verification code it contains.": "A text message has been sent to +%(msisdn)s. Please enter the verification code it contains.", "Phone Number": "Phone Number", - "This user has not verified all of their sessions.": "This user has not verified all of their sessions.", + "This user has not verified all of their devices.": "This user has not verified all of their devices.", "You have not verified this user.": "You have not verified this user.", - "You have verified this user. This user has verified all of their sessions.": "You have verified this user. This user has verified all of their sessions.", - "Someone is using an unknown session": "Someone is using an unknown session", + "You have verified this user. This user has verified all of their devices.": "You have verified this user. This user has verified all of their devices.", + "Someone is using an unknown device": "Someone is using an unknown device", "This room is end-to-end encrypted": "This room is end-to-end encrypted", "Everyone in this room is verified": "Everyone in this room is verified", "Edit message": "Edit message", "Mod": "Mod", "From a thread": "From a thread", "This event could not be displayed": "This event could not be displayed", - "Your key share request has been sent - please check your other sessions for key share requests.": "Your key share request has been sent - please check your other sessions for key share requests.", - "Key share requests are sent to your other sessions automatically. If you rejected or dismissed the key share request on your other sessions, click here to request the keys for this session again.": "Key share requests are sent to your other sessions automatically. If you rejected or dismissed the key share request on your other sessions, click here to request the keys for this session again.", - "If your other sessions do not have the key for this message you will not be able to decrypt them.": "If your other sessions do not have the key for this message you will not be able to decrypt them.", + "Your key share request has been sent - please check your other devices for key share requests.": "Your key share request has been sent - please check your other devices for key share requests.", + "Key share requests are sent to your other devices automatically. If you rejected or dismissed the key share request on your other devices, click here to request the keys for this device again.": "Key share requests are sent to your other devices automatically. If you rejected or dismissed the key share request on your other devices, click here to request the keys for this device again.", + "If your other devices do not have the key for this message you will not be able to decrypt them.": "If your other devices do not have the key for this message you will not be able to decrypt them.", "Key request sent.": "Key request sent.", - "Re-request encryption keys from your other sessions.": "Re-request encryption keys from your other sessions.", + "Re-request encryption keys from your other devices.": "Re-request encryption keys from your other devices.", + "Set up secure messaging to access this message.": "Set up secure messaging to access this message.", "Message Actions": "Message Actions", "View in room": "View in room", "Copy link to thread": "Copy link to thread", "This message cannot be decrypted": "This message cannot be decrypted", - "Encrypted by an unverified session": "Encrypted by an unverified session", + "Encrypted by an unverified device": "Encrypted by an unverified device", "Unencrypted": "Unencrypted", - "Encrypted by a deleted session": "Encrypted by a deleted session", + "Encrypted by a deleted device": "Encrypted by a deleted device", "The authenticity of this encrypted message can't be guaranteed on this device.": "The authenticity of this encrypted message can't be guaranteed on this device.", "Sending your message...": "Sending your message...", "Encrypting your message...": "Encrypting your message...", @@ -1666,7 +1683,7 @@ "Failed to send": "Failed to send", "You don't have permission to view messages from before you were invited.": "You don't have permission to view messages from before you were invited.", "You don't have permission to view messages from before you joined.": "You don't have permission to view messages from before you joined.", - "Encrypted messages before this point are unavailable.": "Encrypted messages before this point are unavailable.", + "Secure messages before this point are unavailable.": "Secure messages before this point are unavailable.", "You can't see earlier messages": "You can't see earlier messages", "Scroll to most recent messages": "Scroll to most recent messages", "Show %(count)s other previews|other": "Show %(count)s other previews", @@ -1714,8 +1731,8 @@ "Add a photo, so people can easily spot your room.": "Add a photo, so people can easily spot your room.", "This is the start of .": "This is the start of .", "Your private messages are normally encrypted, but this room isn't. Usually this is due to an unsupported device or method being used, like email invites.": "Your private messages are normally encrypted, but this room isn't. Usually this is due to an unsupported device or method being used, like email invites.", - "Enable encryption in settings.": "Enable encryption in settings.", - "End-to-end encryption isn't enabled": "End-to-end encryption isn't enabled", + "Enable secure messaging in settings.": "Enable secure messaging in settings.", + "Secure messaging isn't enabled": "Secure messaging isn't enabled", "Message didn't send. Click for info.": "Message didn't send. Click for info.", "Unpin": "Unpin", "View message": "View message", @@ -1750,7 +1767,6 @@ "Forget room": "Forget room", "Hide Widgets": "Hide Widgets", "Show Widgets": "Show Widgets", - "Search": "Search", "Start new chat": "Start new chat", "Invite to space": "Invite to space", "You do not have permissions to invite people to this space": "You do not have permissions to invite people to this space", @@ -1923,10 +1939,11 @@ "When someone puts a URL in their message, a URL preview can be shown to give more information about that link such as the title, description, and an image from the website.": "When someone puts a URL in their message, a URL preview can be shown to give more information about that link such as the title, description, and an image from the website.", "URL Previews": "URL Previews", "Back": "Back", - "To proceed, please accept the verification request on your other device.": "To proceed, please accept the verification request on your other device.", + "Please check your other device(s) and accept the request to set up secure messaging.": "Please check your other device(s) and accept the request to set up secure messaging.", + "Set up secure messaging for new device": "Set up secure messaging for new device", "Waiting for %(displayName)s to accept…": "Waiting for %(displayName)s to accept…", "Accepting…": "Accepting…", - "Start Verification": "Start Verification", + "Start verification": "Start verification", "Messages in this room are end-to-end encrypted.": "Messages in this room are end-to-end encrypted.", "Your messages are secured and only you and the recipient have the unique keys to unlock them.": "Your messages are secured and only you and the recipient have the unique keys to unlock them.", "Messages in this room are not end-to-end encrypted.": "Messages in this room are not end-to-end encrypted.", @@ -1938,7 +1955,7 @@ "Your homeserver": "Your homeserver", "The homeserver the user you're verifying is connected to": "The homeserver the user you're verifying is connected to", "Yours, or the other users' internet connection": "Yours, or the other users' internet connection", - "Yours, or the other users' session": "Yours, or the other users' session", + "Yours, or the other users' device": "Yours, or the other users' device", "Nothing pinned, yet": "Nothing pinned, yet", "If you have permissions, open the menu on any message and select Pin to stick them here.": "If you have permissions, open the menu on any message and select Pin to stick them here.", "Pinned messages": "Pinned messages", @@ -1951,6 +1968,7 @@ "Set my room layout for everyone": "Set my room layout for everyone", "Edit widgets, bridges & bots": "Edit widgets, bridges & bots", "Add widgets, bridges & bots": "Add widgets, bridges & bots", + "Encrypted": "Encrypted", "Not encrypted": "Not encrypted", "About": "About", "Files": "Files", @@ -1960,13 +1978,10 @@ "Room settings": "Room settings", "Trusted": "Trusted", "Not trusted": "Not trusted", - "Unable to load session list": "Unable to load session list", - "%(count)s verified sessions|other": "%(count)s verified sessions", - "%(count)s verified sessions|one": "1 verified session", - "Hide verified sessions": "Hide verified sessions", - "%(count)s sessions|other": "%(count)s sessions", - "%(count)s sessions|one": "%(count)s session", - "Hide sessions": "Hide sessions", + "%(count)s verified devices|other": "%(count)s verified devices", + "Hide verified devices": "Hide verified devices", + "%(count)s devices|other": "%(count)s devices", + "Hide devices": "Hide devices", "Message": "Message", "Jump to read receipt": "Jump to read receipt", "Mention": "Mention", @@ -2005,7 +2020,10 @@ "Deactivate user": "Deactivate user", "Failed to deactivate user": "Failed to deactivate user", "Role in ": "Role in ", - "This client does not support end-to-end encryption.": "This client does not support end-to-end encryption.", + "This client does not support secure messaging.": "This client does not support secure messaging.", + "Secure messaging is not enabled for this room.": "Secure messaging is not enabled for this room.", + "Messages in this room are sent securely using end-to-end encryption.": "Messages in this room are sent securely using end-to-end encryption.", + "Verify this user": "Verify this user", "Edit devices": "Edit devices", "Security": "Security", "The device you are trying to verify doesn't support scanning a QR code or emoji verification, which is what %(brand)s supports. Try with a different client.": "The device you are trying to verify doesn't support scanning a QR code or emoji verification, which is what %(brand)s supports. Try with a different client.", @@ -2064,13 +2082,14 @@ "Decrypting": "Decrypting", "Download": "Download", "View Source": "View Source", - "Some encryption parameters have been changed.": "Some encryption parameters have been changed.", - "Messages here are end-to-end encrypted. Verify %(displayName)s in their profile - tap on their avatar.": "Messages here are end-to-end encrypted. Verify %(displayName)s in their profile - tap on their avatar.", - "Messages in this room are end-to-end encrypted. When people join, you can verify them in their profile, just tap on their avatar.": "Messages in this room are end-to-end encrypted. When people join, you can verify them in their profile, just tap on their avatar.", + "Some secure messaging encryption parameters have been changed.": "Some secure messaging encryption parameters have been changed.", + "Messages here are secured by end-to-end encrypted. Verify %(displayName)s in their profile - tap on their avatar.": "Messages here are secured by end-to-end encrypted. Verify %(displayName)s in their profile - tap on their avatar.", + "Messages in this room are secured by end-to-end encrypted. When people join, you can verify them in their profile, just tap on their avatar.": "Messages in this room are secured by end-to-end encrypted. When people join, you can verify them in their profile, just tap on their avatar.", + "Secure messaging enabled": "Secure messaging enabled", "Encryption enabled": "Encryption enabled", - "Ignored attempt to disable encryption": "Ignored attempt to disable encryption", - "Encryption not enabled": "Encryption not enabled", - "The encryption used by this room isn't supported.": "The encryption used by this room isn't supported.", + "Ignored attempt to disable secure messaging": "Ignored attempt to disable secure messaging", + "Secure messaging not enabled": "Secure messaging not enabled", + "The secure messaging used by this room isn't supported.": "The secure messaging used by this room isn't supported.", "Message pending moderation: %(reason)s": "Message pending moderation: %(reason)s", "Message pending moderation": "Message pending moderation", "Pick a date to jump to": "Pick a date to jump to", @@ -2194,7 +2213,7 @@ "Widget ID": "Widget ID", "Using this widget may share data with %(widgetDomain)s & your integration manager.": "Using this widget may share data with %(widgetDomain)s & your integration manager.", "Using this widget may share data with %(widgetDomain)s.": "Using this widget may share data with %(widgetDomain)s.", - "Widgets do not use message encryption.": "Widgets do not use message encryption.", + "Widgets cannot use secure messaging.": "Widgets cannot use secure messaging.", "Widget added by": "Widget added by", "This widget may use cookies.": "This widget may use cookies.", "Loading...": "Loading...", @@ -2410,8 +2429,8 @@ "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.", "Reason (optional)": "Reason (optional)", - "Clear all data in this session?": "Clear all data in this session?", - "Clearing all data from this session is permanent. Encrypted messages will be lost unless their keys have been backed up.": "Clearing all data from this session is permanent. Encrypted messages will be lost unless their keys have been backed up.", + "Clear all data on this device?": "Clear all data on this device?", + "Clearing all data from this device is permanent. Encrypted messages will be lost unless their keys have been backed up.": "Clearing all data from this device is permanent. Encrypted messages will be lost unless their keys have been backed up.", "Clear all data": "Clear all data", "Please enter a name for the room": "Please enter a name for the room", "Everyone in will be able to find and join this room.": "Everyone in will be able to find and join this room.", @@ -2420,8 +2439,8 @@ "Anyone will be able to find and join this room.": "Anyone will be able to find and join this room.", "Only people invited will be able to find and join this room.": "Only people invited will be able to find and join this room.", "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", + "Your server requires secure messaging to be enabled in private rooms.": "Your server requires secure messaging to be enabled in private rooms.", + "Your server admin has set a default to not use secure messaging in private rooms & Direct Messages.": "Your server admin has set a default to not use secure messaging in private rooms & Direct Messages.", "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.", "Create a video room": "Create a video room", @@ -2446,8 +2465,8 @@ "Want to add an existing space instead?": "Want to add an existing space instead?", "Adding...": "Adding...", "Sign out": "Sign out", - "To avoid losing your chat history, you must export your room keys before logging out. You will need to go back to the newer version of %(brand)s to do this": "To avoid losing your chat history, you must export your room keys before logging out. You will need to go back to the newer version of %(brand)s to do this", - "You've previously used a newer version of %(brand)s with this session. To use this version again with end to end encryption, you will need to sign out and back in again.": "You've previously used a newer version of %(brand)s with this session. To use this version again with end to end encryption, you will need to sign out and back in again.", + "To avoid losing your chat history, you must export your message keys before logging out. You will need to go back to the newer version of %(brand)s to do this": "To avoid losing your chat history, you must export your message keys before logging out. You will need to go back to the newer version of %(brand)s to do this", + "You've previously used a newer version of %(brand)s with this device. To use this version again with end to end encryption, you will need to sign out and back in again.": "You've previously used a newer version of %(brand)s with this device. To use this version again with end to end encryption, you will need to sign out and back in again.", "Incompatible Database": "Incompatible Database", "Continue With Encryption Disabled": "Continue With Encryption Disabled", "Confirm your account deactivation by using Single Sign On to prove your identity.": "Confirm your account deactivation by using Single Sign On to prove your identity.", @@ -2533,7 +2552,7 @@ "Minimise dialog": "Minimise dialog", "Upgrade to %(hostSignupBrand)s": "Upgrade to %(hostSignupBrand)s", "Verify this user to mark them as trusted. Trusting users gives you extra peace of mind when using end-to-end encrypted messages.": "Verify this user to mark them as trusted. Trusting users gives you extra peace of mind when using end-to-end encrypted messages.", - "Verifying this user will mark their session as trusted, and also mark your session as trusted to them.": "Verifying this user will mark their session as trusted, and also mark your session as trusted to them.", + "Verifying this user will mark their device as trusted, and also mark your device as trusted to them.": "Verifying this user will mark their device as trusted, and also mark your device as trusted to them.", "Verify this device to mark it as trusted. Trusting this device gives you and other users extra peace of mind when using end-to-end encrypted messages.": "Verify this device to mark it as trusted. Trusting this device gives you and other users extra peace of mind when using end-to-end encrypted messages.", "Verifying this device will mark it as trusted, and users who have verified with you will trust this device.": "Verifying this device will mark it as trusted, and users who have verified with you will trust this device.", "Waiting for partner to confirm...": "Waiting for partner to confirm...", @@ -2600,11 +2619,18 @@ "Leave all rooms": "Leave all rooms", "Leave some rooms": "Leave some rooms", "Leave space": "Leave space", - "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", + "Your recovery key is a safety net - you can use it to restore access to your secure message messages if you forget you lose access to your devices.": "Your recovery key is a safety net - you can use it to restore access to your secure message messages if you forget you lose access to your devices.", + "Your recovery key is used to restore access to your secure messages. As you don't have any other devices, if you don't save the recovery key you will lose access to your secure messages by signing out. ": "Your recovery key is used to restore access to your secure messages. As you don't have any other devices, if you don't save the recovery key you will lose access to your secure messages by signing out. ", + "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", + "Without saving your secure messaging recovey key, you won't be able to restore your encrypted message history if you log out or use another device.": "Without saving your secure messaging recovey key, you won't be able to restore your encrypted message history if you log out or use another device.", + "Save your secure messaging recovery key": "Save your secure messaging recovery key", + "Checking secure messaging backup state": "Checking secure messaging backup state", "Are you sure you want to sign out?": "Are you sure you want to sign out?", "%(count)s members|other": "%(count)s members", "%(count)s members|one": "%(count)s member", @@ -2618,13 +2644,13 @@ "Spaces you know that contain this room": "Spaces you know that contain this room", "Other spaces or rooms you might not know": "Other spaces or rooms you might not know", "These are likely ones other room admins are a part of.": "These are likely ones other room admins are a part of.", - "Confirm by comparing the following with the User Settings in your other session:": "Confirm by comparing the following with the User Settings in your other session:", - "Confirm this user's session by comparing the following with their User Settings:": "Confirm this user's session by comparing the following with their User Settings:", - "Session name": "Session name", - "Session ID": "Session ID", - "Session key": "Session key", + "Confirm by comparing the following with the User Settings in your other device:": "Confirm by comparing the following with the User Settings in your other device:", + "Confirm this user's device by comparing the following with their User Settings:": "Confirm this user's device by comparing the following with their User Settings:", + "Device name": "Device name", + "Device ID": "Device ID", + "Device key": "Device key", "If they don't match, the security of your communication may be compromised.": "If they don't match, the security of your communication may be compromised.", - "Verify session": "Verify session", + "Verify device": "Verify device", "Your homeserver doesn't seem to support this feature.": "Your homeserver doesn't seem to support this feature.", "Message edits": "Message edits", "Modal Widget": "Modal Widget", @@ -2698,10 +2724,10 @@ "Clear Storage and Sign Out": "Clear Storage and Sign Out", "Send Logs": "Send Logs", "Refresh": "Refresh", - "Unable to restore session": "Unable to restore session", - "We encountered an error trying to restore your previous session.": "We encountered an error trying to restore your previous session.", - "If you have previously used a more recent version of %(brand)s, your session may be incompatible with this version. Close this window and return to the more recent version.": "If you have previously used a more recent version of %(brand)s, your session may be incompatible with this version. Close this window and return to the more recent version.", - "Clearing your browser's storage may fix the problem, but will sign you out and cause any encrypted chat history to become unreadable.": "Clearing your browser's storage may fix the problem, but will sign you out and cause any encrypted chat history to become unreadable.", + "Unable to restore data": "Unable to restore data", + "We encountered an error trying to use the data stored on this device.": "We encountered an error trying to use the data stored on this device.", + "If you have previously used a more recent version of %(brand)s, your data may be incompatible with this version. Close this window and return to the more recent version.": "If you have previously used a more recent version of %(brand)s, your data may be incompatible with this version. Close this window and return to the more recent version.", + "Clearing your browser's storage may fix the problem, but will sign you out and may cause any encrypted chat history to become unreadable.": "Clearing your browser's storage may fix the problem, but will sign you out and may cause any encrypted chat history to become unreadable.", "Verification Pending": "Verification Pending", "Please check your email and click on the link it contains. Once this is done, click continue.": "Please check your email and click on the link it contains. Once this is done, click continue.", "Email address": "Email address", @@ -2731,8 +2757,8 @@ "Search Dialog": "Search Dialog", "Results not as expected? Please give feedback.": "Results not as expected? Please give feedback.", "To help us prevent this in future, please send us logs.": "To help us prevent this in future, please send us logs.", - "Missing session data": "Missing session data", - "Some session data, including encrypted message keys, is missing. Sign out and sign in to fix this, restoring keys from backup.": "Some session data, including encrypted message keys, is missing. Sign out and sign in to fix this, restoring keys from backup.", + "Missing data": "Missing data", + "Some data, including encrypted message keys, is missing. Sign out and sign in to fix this, restoring keys from backup.": "Some data, including encrypted message keys, is missing. Sign out and sign in to fix this, restoring keys from backup.", "Your browser likely removed this data when running low on disk space.": "Your browser likely removed this data when running low on disk space.", "Find others by phone or email": "Find others by phone or email", "Be found by phone or email": "Be found by phone or email", @@ -2742,10 +2768,10 @@ "Summary": "Summary", "Document": "Document", "Next": "Next", - "You signed in to a new session without verifying it:": "You signed in to a new session without verifying it:", - "Verify your other session using one of the options below.": "Verify your other session using one of the options below.", - "%(name)s (%(userId)s) signed in to a new session without verifying it:": "%(name)s (%(userId)s) signed in to a new session without verifying it:", - "Ask this user to verify their session, or manually verify it below.": "Ask this user to verify their session, or manually verify it below.", + "You signed in to a new device without verifying it:": "You signed in to a new device without verifying it:", + "Verify your other device using one of the options below.": "Verify your other device using one of the options below.", + "%(name)s (%(userId)s) signed in to a new device without verifying it:": "%(name)s (%(userId)s) signed in to a new device without verifying it:", + "Ask this user to verify their device, or manually verify it below.": "Ask this user to verify their device, or manually verify it below.", "Not Trusted": "Not Trusted", "Manually Verify by Text": "Manually Verify by Text", "Interactively verify by Emoji": "Interactively verify by Emoji", @@ -2759,8 +2785,9 @@ "Upload %(count)s other files|one": "Upload %(count)s other file", "Cancel All": "Cancel All", "Upload Error": "Upload Error", - "Verify other device": "Verify other device", - "Verification Request": "Verification Request", + "Shortcuts": "Shortcuts", + "Set up secure messaging": "Set up secure messaging", + "Secure messaging set up request": "Secure messaging set up request", "Approve widget permissions": "Approve widget permissions", "This widget would like to:": "This widget would like to:", "Approve": "Approve", @@ -2771,17 +2798,17 @@ "Remember this": "Remember this", "Wrong file type": "Wrong file type", "Looks good!": "Looks good!", - "Wrong Security Key": "Wrong Security Key", - "Invalid Security Key": "Invalid Security Key", + "Wrong recovery key": "Wrong recovery key", + "Invalid recovery key": "Invalid recovery key", "Forgotten or lost all recovery methods? Reset all": "Forgotten or lost all recovery methods? Reset all", "Reset everything": "Reset everything", "Only do this if you have no other device to complete verification with.": "Only do this if you have no other device to complete verification with.", - "If you reset everything, you will restart with no trusted sessions, no trusted users, and might not be able to see past messages.": "If you reset everything, you will restart with no trusted sessions, no trusted users, and might not be able to see past messages.", + "If you reset everything, you will restart with no trusted devices, no trusted users, and might not be able to see past messages.": "If you reset everything, you will restart with no trusted devices, no trusted users, and might not be able to see past messages.", "Security Phrase": "Security Phrase", "Unable to access secret storage. Please verify that you entered the correct Security Phrase.": "Unable to access secret storage. Please verify that you entered the correct Security Phrase.", - "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.", + "Enter your Security Phrase or to continue.": "Enter your Security Phrase or to continue.", + "Recovery key": "Recovery key", + "Use your recovery key to continue.": "Use your recovery 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", @@ -2993,7 +3020,7 @@ "New search beta available": "New search beta available", "We're testing a new search to make finding what you want quicker.\n": "We're testing a new search to make finding what you want quicker.\n", "Signed Out": "Signed Out", - "For security, this session has been signed out. Please sign in again.": "For security, this session has been signed out. Please sign in again.", + "For security, this device has been signed out. Please sign in again.": "For security, this device has been signed out. Please sign in again.", "Terms and Conditions": "Terms and Conditions", "To continue using the %(homeserverDomain)s homeserver you must review and agree to our terms and conditions.": "To continue using the %(homeserverDomain)s homeserver you must review and agree to our terms and conditions.", "Review terms and conditions": "Review terms and conditions", @@ -3132,12 +3159,11 @@ "Original event source": "Original event source", "Event ID: %(eventId)s": "Event ID: %(eventId)s", "Unable to verify this device": "Unable to verify this device", - "Verify this device": "Verify this device", + "Secure messaging setup": "Secure messaging setup", "Device verified": "Device verified", - "Really reset verification keys?": "Really reset verification keys?", "Skip verification for now": "Skip verification for now", "Failed to send email": "Failed to send email", - "Changing your password will reset any end-to-end encryption keys on all of your sessions, making encrypted chat history unreadable. Set up Key Backup or export your room keys from another session before resetting your password.": "Changing your password will reset any end-to-end encryption keys on all of your sessions, making encrypted chat history unreadable. Set up Key Backup or export your room keys from another session before resetting your password.", + "Changing your password will reset any end-to-end encryption keys on all of your devices, making encrypted chat history unreadable. Set up Key Backup or export your message keys from another device before resetting your password.": "Changing your password will reset any end-to-end encryption keys on all of your devices, making encrypted chat history unreadable. Set up Key Backup or export your message keys from another device before resetting your password.", "The email address linked to your account must be entered.": "The email address linked to your account must be entered.", "The email address doesn't appear to be valid.": "The email address doesn't appear to be valid.", "A new password must be entered.": "A new password must be entered.", @@ -3189,29 +3215,32 @@ "Create account": "Create account", "Host account on": "Host account on", "Decide where your account is hosted": "Decide where your account is hosted", - "It looks like you don't have a Security Key or any other devices you can verify against. This device will not be able to access old encrypted messages. In order to verify your identity on this device, you'll need to reset your verification keys.": "It looks like you don't have a Security Key or any other devices you can verify against. This device will not be able to access old encrypted messages. In order to verify your identity on this device, you'll need to reset your verification keys.", + "It looks like you don't have a recovery key or any other devices you can use to set up secure messaging. As a result, this device won't be able to access past encrypted messages. In order to verify your identity on this device, you'll need to reset secure messaging completely.": "It looks like you don't have a recovery key or any other devices you can use to set up secure messaging. As a result, this device won't be able to access past encrypted messages. In order to verify your identity on this device, you'll need to reset secure messaging completely.", + "Reset secure messaging": "Reset secure messaging", + "Use recovery key or passphrase": "Use recovery key or passphrase", + "Use recovery key": "Use recovery key", + "Use another device": "Use another device", + "Set up secure messaging on this device to access past encrypted messages and allow others to trust it.": "Set up secure messaging on this device to access past encrypted messages and allow others to trust it.", + "Please select how you would like to do the set up.": "Please select how you would like to do the set up.", + "Forgotten or lost all set up methods? Reset secure messaging": "Forgotten or lost all set up methods? Reset secure messaging", + "Secure messaging is now set up on this device and you can access your past encrypted message. Others will see this device as trusted.": "Secure messaging is now set up on this device and you can access your past encrypted message. Others will see this device as trusted.", + "Secure messaging is now set up on this device and others will see it as trusted.": "Secure messaging is now set up on this device and others will see it as trusted.", + "Without setting up secure messaging you won't have access to past encrypted messages. This device may also appear as untrusted to others.": "Without setting up secure messaging you won't have access to past encrypted messages. This device may also appear as untrusted to others.", + "Set up later": "Set up later", + "By resetting secure messaging you will lose access to your past encrypted messages. Also, any contact who has previously verified you will see this device as untrusted.": "By resetting secure messaging you will lose access to your past encrypted messages. Also, any contact who has previously verified you will see this device as untrusted.", + "You should only proceed if you are certain that you cannot access your other devices and have lost your recovery key.": "You should only proceed if you are certain that you cannot access your other devices and have lost your recovery key.", "Proceed with reset": "Proceed with reset", - "Verify with Security Key or Phrase": "Verify with Security Key or Phrase", - "Verify with Security Key": "Verify with Security Key", - "Verify with another device": "Verify with another device", - "Verify your identity to access encrypted messages and prove your identity to others.": "Verify your identity to access encrypted messages and prove your identity to others.", - "Your new device is now verified. It has access to your encrypted messages, and other users will see it as trusted.": "Your new device is now verified. It has access to your encrypted messages, and other users will see it as trusted.", - "Your new device is now verified. Other users will see it as trusted.": "Your new device is now verified. Other users will see it as trusted.", - "Without verifying, you won't have access to all your messages and may appear as untrusted to others.": "Without verifying, you won't have access to all your messages and may appear as untrusted to others.", - "I'll verify later": "I'll verify later", - "Resetting your verification keys cannot be undone. After resetting, you won't have access to old encrypted messages, and any friends who have previously verified you will see security warnings until you re-verify with them.": "Resetting your verification keys cannot be undone. After resetting, you won't have access to old encrypted messages, and any friends who have previously verified you will see security warnings until you re-verify with them.", - "Please only proceed if you're sure you've lost all of your other devices and your security key.": "Please only proceed if you're sure you've lost all of your other devices and your security key.", "Failed to re-authenticate due to a homeserver problem": "Failed to re-authenticate due to a homeserver problem", "Incorrect password": "Incorrect password", "Failed to re-authenticate": "Failed to re-authenticate", "Forgotten your password?": "Forgotten your password?", - "Regain access to your account and recover encryption keys stored in this session. Without them, you won't be able to read all of your secure messages in any session.": "Regain access to your account and recover encryption keys stored in this session. Without them, you won't be able to read all of your secure messages in any session.", + "Regain access to your account and recover encryption keys stored on this device. Without them, you won't be able to read all of your secure messages in any device.": "Regain access to your account and recover encryption keys stored on this device. Without them, you won't be able to read all of your secure messages in any device.", "Enter your password to sign in and regain access to your account.": "Enter your password to sign in and regain access to your account.", "Sign in and regain access to your account.": "Sign in and regain access to your account.", "You cannot sign in to your account. Please contact your homeserver admin for more information.": "You cannot sign in to your account. Please contact your homeserver admin for more information.", "You're signed out": "You're signed out", "Clear personal data": "Clear personal data", - "Warning: Your personal data (including encryption keys) is still stored in this session. Clear it if you're finished using this session, or want to sign in to another account.": "Warning: Your personal data (including encryption keys) is still stored in this session. Clear it if you're finished using this session, or want to sign in to another account.", + "Warning: Your personal data (including encryption keys) is still stored on this device. Clear it if you're finished using this device, or want to sign in to another account.": "Warning: Your personal data (including encryption keys) is still stored on this device. Clear it if you're finished using this device, or want to sign in to another account.", "Commands": "Commands", "Command Autocomplete": "Command Autocomplete", "Emoji Autocomplete": "Emoji Autocomplete", @@ -3234,15 +3263,9 @@ "Enter your Security Phrase a second time to confirm it.": "Enter your Security Phrase a second time to confirm it.", "Repeat your Security Phrase...": "Repeat your Security Phrase...", "Your Security Key is a safety net - you can use it to restore access to your encrypted messages if you forget your Security Phrase.": "Your Security Key is a safety net - you can use it to restore access to your encrypted messages if you forget your Security Phrase.", - "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 Security Key": "Your Security Key", - "Your Security Key has been copied to your clipboard, paste it to:": "Your Security Key has been copied to your clipboard, paste it to:", - "Your Security Key is in your Downloads folder.": "Your Security 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.", + "Without setting up Secure Message Recovery, you won't be able to restore your encrypted message history if you log out or use another device.": "Without setting up Secure Message Recovery, you won't be able to restore your encrypted message history if you log out or use another device.", "Set up Secure Message Recovery": "Set up Secure Message Recovery", "Secure your backup with a Security Phrase": "Secure your backup with a Security Phrase", "Confirm your Security Phrase": "Confirm your Security Phrase", @@ -3251,24 +3274,24 @@ "Success!": "Success!", "Create key backup": "Create key backup", "Unable to create key backup": "Unable to create key backup", - "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.", - "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.", + "Generate a secure messaging recovery key": "Generate a secure messaging recovery key", + "We'll generate a secure messaging recovery key for you to store somewhere safe, like a password manager or a safe.": "We'll generate a secure messaging recovery key for you to store somewhere safe, like a password manager or a safe.", + "Use a secret phrase only you know, and optionally save a recovery key to use for backup.": "Use a secret phrase only you know, and optionally save a recovery key to use for 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.", "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.", + "Upgrade this device to allow it to verify other devices, granting them access to encrypted messages and marking them as trusted for other users.": "Upgrade this device to allow it to verify other devices, 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.", - "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.", + "Store your recovery key somewhere safe, like a password manager or a safe, as it's used to safeguard your encrypted data.": "Store your recovery 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", + "Save your recovery key": "Save your recovery 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", @@ -3278,20 +3301,19 @@ "The exported file will allow anyone who can read it to decrypt any encrypted messages that you can see, so you should be careful to keep it secure. To help with this, you should enter a passphrase below, which will be used to encrypt the exported data. It will only be possible to import the data by using the same passphrase.": "The exported file will allow anyone who can read it to decrypt any encrypted messages that you can see, so you should be careful to keep it secure. To help with this, you should enter a passphrase below, which will be used to encrypt the exported data. It will only be possible to import the data by using the same passphrase.", "Enter passphrase": "Enter passphrase", "Confirm passphrase": "Confirm passphrase", - "Import room keys": "Import room keys", - "This process allows you to import encryption keys that you had previously exported from another Matrix client. You will then be able to decrypt any messages that the other client could decrypt.": "This process allows you to import encryption keys that you had previously exported from another Matrix client. You will then be able to decrypt any messages that the other client could decrypt.", + "This process allows you to import message encryption keys that you had previously exported from another Matrix client. You will then be able to decrypt any messages that the other client could decrypt.": "This process allows you to import message encryption keys that you had previously exported from another Matrix client. You will then be able to decrypt any messages that the other client could decrypt.", "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", "New Recovery Method": "New Recovery Method", "A new Security Phrase and key for Secure Messages have been detected.": "A new Security Phrase 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.", - "This session is encrypting history using the new recovery method.": "This session is encrypting history using the new recovery method.", + "This device is encrypting history using the new recovery method.": "This device is encrypting history using the new recovery method.", "Go to Settings": "Go to Settings", "Set up Secure Messages": "Set up Secure Messages", "Recovery Method Removed": "Recovery Method Removed", - "This session has detected that your Security Phrase and key for Secure Messages have been removed.": "This session has detected that your Security Phrase and key for Secure Messages have been removed.", - "If you did this accidentally, you can setup Secure Messages on this session which will re-encrypt this session's message history with a new recovery method.": "If you did this accidentally, you can setup Secure Messages on this session which will re-encrypt this session's message history with a new recovery method.", + "This device has detected that your Security Phrase and key for Secure Messages have been removed.": "This device has detected that your Security Phrase and key for Secure Messages have been removed.", + "If you did this accidentally, you can set up Secure Messages on this device which will re-encrypt this device's message history with a new recovery method.": "If you did this accidentally, you can set up Secure Messages on this device which will re-encrypt this device's message history with a new recovery method.", "If you didn't remove the 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 remove the 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 disabled, messages from encrypted rooms won't appear in search results.": "If disabled, messages from encrypted rooms won't appear in search results.", "Disable": "Disable", @@ -3303,6 +3325,7 @@ "Indexed rooms:": "Indexed rooms:", "%(doneRooms)s out of %(totalRooms)s": "%(doneRooms)s out of %(totalRooms)s", "Message downloading sleep time(ms)": "Message downloading sleep time(ms)", + "Message search": "Message search", "Failed to set direct chat tag": "Failed to set direct chat tag", "Failed to remove tag %(tagName)s from room": "Failed to remove tag %(tagName)s from room", "Failed to add tag %(tagName)s to room": "Failed to add tag %(tagName)s to room", diff --git a/src/languageHandler.tsx b/src/languageHandler.tsx index a2037e640f6..a10c951c4c8 100644 --- a/src/languageHandler.tsx +++ b/src/languageHandler.tsx @@ -45,6 +45,9 @@ counterpart.setSeparator('|'); const FALLBACK_LOCALE = 'en'; counterpart.setFallbackLocale(FALLBACK_LOCALE); +// FIXME: remove this nasty hack to make build easier during dev - +setMissingEntryGenerator(key => key.split("|", 2)[1]); + export interface ITranslatableError extends Error { translatedMessage: string; } diff --git a/src/settings/Settings.tsx b/src/settings/Settings.tsx index 8cd842b74e5..8ca5c50dc89 100644 --- a/src/settings/Settings.tsx +++ b/src/settings/Settings.tsx @@ -109,7 +109,7 @@ export const labGroupNames: Record = { [LabGroup.Analytics]: _td("Analytics"), [LabGroup.MessagePreviews]: _td("Message Previews"), [LabGroup.Themes]: _td("Themes"), - [LabGroup.Encryption]: _td("Encryption"), + [LabGroup.Encryption]: _td("Secure messaging"), [LabGroup.Experimental]: _td("Experimental"), [LabGroup.Developer]: _td("Developer"), }; @@ -840,7 +840,7 @@ export const SETTINGS: {[setting: string]: ISetting} = { }, "e2ee.manuallyVerifyAllSessions": { supportedLevels: LEVELS_DEVICE_ONLY_SETTINGS, - displayName: _td("Manually verify all remote sessions"), + displayName: _td("Verify each individual device used by a user before sending secure messages to it, not trusting the recipient's cross-signing"), default: false, controller: new OrderedMultiController([ // Apply the feature controller first to ensure that the setting doesn't @@ -852,6 +852,16 @@ export const SETTINGS: {[setting: string]: ISetting} = { ), ]), }, + "e2ee.blacklistNonCrossSignedDevices": { + supportedLevels: LEVELS_DEVICE_ONLY_SETTINGS, + displayName: _td("Don't send secure messages to devices that have not been verified by the recipient"), + default: true, + controller: new OrderedMultiController([ + new PushToMatrixClientController( + MatrixClient.prototype.setTrustDevicesThatAreNotCrossSignedByTheRecipient, true, + ), + ]), + }, "ircDisplayNameWidth": { // We specifically want to have room-device > device so that users may set a device default // with a per-room override. diff --git a/src/toasts/BulkUnverifiedSessionsToast.ts b/src/toasts/BulkUnverifiedSessionsToast.ts index 6ddb0d7db57..b3fce76e900 100644 --- a/src/toasts/BulkUnverifiedSessionsToast.ts +++ b/src/toasts/BulkUnverifiedSessionsToast.ts @@ -30,7 +30,7 @@ export const showToast = (deviceIds: Set) => { dis.dispatch({ action: Action.ViewUserSettings, - initialTabId: UserTab.Security, + initialTabId: UserTab.SecureMessaging, }); }; @@ -40,11 +40,11 @@ export const showToast = (deviceIds: Set) => { ToastStore.sharedInstance().addOrReplaceToast({ key: TOAST_KEY, - title: _t("You have unverified logins"), + title: _t("New logins. Were they you?"), icon: "verification_warning", props: { - description: _t("Review to ensure your account is safe"), - acceptLabel: _t("Review"), + description: _t("Check your logged in devices to ensure your account is safe and that they can access your secure messages."), + acceptLabel: _t("Check your logins"), onAccept, rejectLabel: _t("Later"), onReject, diff --git a/src/toasts/SetupEncryptionToast.ts b/src/toasts/SetupEncryptionToast.ts index bdaeb5142f1..922771aeacb 100644 --- a/src/toasts/SetupEncryptionToast.ts +++ b/src/toasts/SetupEncryptionToast.ts @@ -29,11 +29,11 @@ const TOAST_KEY = "setupencryption"; const getTitle = (kind: Kind) => { switch (kind) { case Kind.SET_UP_ENCRYPTION: - return _t("Set up Secure Backup"); + return _t("Set up recovery for secure messaging"); case Kind.UPGRADE_ENCRYPTION: - return _t("Encryption upgrade available"); + return _t("Secure messaging upgrade available"); case Kind.VERIFY_THIS_SESSION: - return _t("Verify this session"); + return _t("Set up secure messaging on this device"); } }; @@ -50,11 +50,11 @@ const getIcon = (kind: Kind) => { const getSetupCaption = (kind: Kind) => { switch (kind) { case Kind.SET_UP_ENCRYPTION: - return _t("Continue"); + return _t("Set up"); case Kind.UPGRADE_ENCRYPTION: return _t("Upgrade"); case Kind.VERIFY_THIS_SESSION: - return _t("Verify"); + return _t("Set up"); } }; @@ -64,7 +64,7 @@ const getDescription = (kind: Kind) => { case Kind.UPGRADE_ENCRYPTION: return _t("Safeguard against losing access to encrypted messages & data"); case Kind.VERIFY_THIS_SESSION: - return _t("Other users may not trust it"); + return _t("You won't be able to receive secure messages nor access past secure messages until you complete setup."); } }; diff --git a/src/toasts/UnverifiedSessionToast.ts b/src/toasts/UnverifiedSessionToast.ts index d0db97cd08f..05e6d35d171 100644 --- a/src/toasts/UnverifiedSessionToast.ts +++ b/src/toasts/UnverifiedSessionToast.ts @@ -34,7 +34,7 @@ export const showToast = async (deviceId: string) => { DeviceListener.sharedInstance().dismissUnverifiedSessions([deviceId]); dis.dispatch({ action: Action.ViewUserSettings, - initialTabId: UserTab.Security, + initialTabId: UserTab.SecureMessaging, }); }; @@ -54,7 +54,7 @@ export const showToast = async (deviceId: string) => { deviceId, ip: device.last_seen_ip, }), - acceptLabel: _t("Check your devices"), + acceptLabel: _t("Check your logins"), onAccept, rejectLabel: _t("Later"), onReject, diff --git a/test/DeviceListener-test.ts b/test/DeviceListener-test.ts deleted file mode 100644 index 06405674416..00000000000 --- a/test/DeviceListener-test.ts +++ /dev/null @@ -1,243 +0,0 @@ - -/* -Copyright 2022 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 { EventEmitter } from "events"; -import { mocked } from "jest-mock"; -import { Room } from "matrix-js-sdk/src/matrix"; - -import DeviceListener from "../src/DeviceListener"; -import { MatrixClientPeg } from "../src/MatrixClientPeg"; -import * as SetupEncryptionToast from "../src/toasts/SetupEncryptionToast"; -import * as UnverifiedSessionToast from "../src/toasts/UnverifiedSessionToast"; -import * as BulkUnverifiedSessionsToast from "../src/toasts/BulkUnverifiedSessionsToast"; -import { isSecretStorageBeingAccessed } from "../src/SecurityManager"; -import dis from "../src/dispatcher/dispatcher"; -import { Action } from "../src/dispatcher/actions"; - -// don't litter test console with logs -jest.mock("matrix-js-sdk/src/logger"); - -jest.mock("../src/dispatcher/dispatcher", () => ({ - dispatch: jest.fn(), - register: jest.fn(), -})); - -jest.mock("../src/SecurityManager", () => ({ - isSecretStorageBeingAccessed: jest.fn(), accessSecretStorage: jest.fn(), -})); - -class MockClient extends EventEmitter { - getUserId = jest.fn(); - getKeyBackupVersion = jest.fn().mockResolvedValue(undefined); - getRooms = jest.fn().mockReturnValue([]); - doesServerSupportUnstableFeature = jest.fn().mockResolvedValue(true); - isCrossSigningReady = jest.fn().mockResolvedValue(true); - isSecretStorageReady = jest.fn().mockResolvedValue(true); - isCryptoEnabled = jest.fn().mockReturnValue(true); - isInitialSyncComplete = jest.fn().mockReturnValue(true); - getKeyBackupEnabled = jest.fn(); - getStoredDevicesForUser = jest.fn().mockReturnValue([]); - getCrossSigningId = jest.fn(); - getStoredCrossSigningForUser = jest.fn(); - waitForClientWellKnown = jest.fn(); - downloadKeys = jest.fn(); - isRoomEncrypted = jest.fn(); - getClientWellKnown = jest.fn(); -} -const mockDispatcher = mocked(dis); -const flushPromises = async () => await new Promise(process.nextTick); - -describe('DeviceListener', () => { - let mockClient; - - // spy on various toasts' hide and show functions - // easier than mocking - jest.spyOn(SetupEncryptionToast, 'showToast'); - jest.spyOn(SetupEncryptionToast, 'hideToast'); - jest.spyOn(BulkUnverifiedSessionsToast, 'showToast'); - jest.spyOn(BulkUnverifiedSessionsToast, 'hideToast'); - jest.spyOn(UnverifiedSessionToast, 'showToast'); - jest.spyOn(UnverifiedSessionToast, 'hideToast'); - - beforeEach(() => { - jest.resetAllMocks(); - mockClient = new MockClient(); - jest.spyOn(MatrixClientPeg, 'get').mockReturnValue(mockClient); - }); - - const createAndStart = async (): Promise => { - const instance = new DeviceListener(); - instance.start(); - await flushPromises(); - return instance; - }; - - describe('recheck', () => { - it('does nothing when cross signing feature is not supported', async () => { - mockClient.doesServerSupportUnstableFeature.mockResolvedValue(false); - await createAndStart(); - - expect(mockClient.isCrossSigningReady).not.toHaveBeenCalled(); - }); - it('does nothing when crypto is not enabled', async () => { - mockClient.isCryptoEnabled.mockReturnValue(false); - await createAndStart(); - - expect(mockClient.isCrossSigningReady).not.toHaveBeenCalled(); - }); - it('does nothing when initial sync is not complete', async () => { - mockClient.isInitialSyncComplete.mockReturnValue(false); - await createAndStart(); - - expect(mockClient.isCrossSigningReady).not.toHaveBeenCalled(); - }); - - describe('set up encryption', () => { - const rooms = [ - { roomId: '!room1' }, - { roomId: '!room2' }, - ] as unknown as Room[]; - - beforeEach(() => { - mockClient.isCrossSigningReady.mockResolvedValue(false); - mockClient.isSecretStorageReady.mockResolvedValue(false); - mockClient.getRooms.mockReturnValue(rooms); - mockClient.isRoomEncrypted.mockReturnValue(true); - }); - - it('hides setup encryption toast when cross signing and secret storage are ready', async () => { - mockClient.isCrossSigningReady.mockResolvedValue(true); - mockClient.isSecretStorageReady.mockResolvedValue(true); - await createAndStart(); - expect(SetupEncryptionToast.hideToast).toHaveBeenCalled(); - }); - - it('hides setup encryption toast when it is dismissed', async () => { - const instance = await createAndStart(); - instance.dismissEncryptionSetup(); - await flushPromises(); - expect(SetupEncryptionToast.hideToast).toHaveBeenCalled(); - }); - - it('does not do any checks or show any toasts when secret storage is being accessed', async () => { - mocked(isSecretStorageBeingAccessed).mockReturnValue(true); - await createAndStart(); - - expect(mockClient.downloadKeys).not.toHaveBeenCalled(); - expect(SetupEncryptionToast.showToast).not.toHaveBeenCalled(); - }); - - it('does not do any checks or show any toasts when no rooms are encrypted', async () => { - mockClient.isRoomEncrypted.mockReturnValue(false); - await createAndStart(); - - expect(mockClient.downloadKeys).not.toHaveBeenCalled(); - expect(SetupEncryptionToast.showToast).not.toHaveBeenCalled(); - }); - - describe('when user does not have a cross signing id on this device', () => { - beforeEach(() => { - mockClient.getCrossSigningId.mockReturnValue(undefined); - }); - - it('shows verify session toast when account has cross signing', async () => { - mockClient.getStoredCrossSigningForUser.mockReturnValue(true); - await createAndStart(); - - expect(mockClient.downloadKeys).toHaveBeenCalled(); - expect(SetupEncryptionToast.showToast).toHaveBeenCalledWith( - SetupEncryptionToast.Kind.VERIFY_THIS_SESSION); - }); - - it('checks key backup status when when account has cross signing', async () => { - mockClient.getCrossSigningId.mockReturnValue(undefined); - mockClient.getStoredCrossSigningForUser.mockReturnValue(true); - await createAndStart(); - - expect(mockClient.getKeyBackupEnabled).toHaveBeenCalled(); - }); - }); - - describe('when user does have a cross signing id on this device', () => { - beforeEach(() => { - mockClient.getCrossSigningId.mockReturnValue('abc'); - }); - - it('shows upgrade encryption toast when user has a key backup available', async () => { - // non falsy response - mockClient.getKeyBackupVersion.mockResolvedValue({}); - await createAndStart(); - - expect(SetupEncryptionToast.showToast).toHaveBeenCalledWith( - SetupEncryptionToast.Kind.UPGRADE_ENCRYPTION); - }); - }); - }); - - describe('key backup status', () => { - it('checks keybackup status when cross signing and secret storage are ready', async () => { - // default mocks set cross signing and secret storage to ready - await createAndStart(); - expect(mockClient.getKeyBackupEnabled).toHaveBeenCalled(); - expect(mockDispatcher.dispatch).not.toHaveBeenCalled(); - }); - - it('checks keybackup status when setup encryption toast has been dismissed', async () => { - mockClient.isCrossSigningReady.mockResolvedValue(false); - const instance = await createAndStart(); - - instance.dismissEncryptionSetup(); - await flushPromises(); - - expect(mockClient.getKeyBackupEnabled).toHaveBeenCalled(); - }); - - it('does not dispatch keybackup event when key backup check is not finished', async () => { - // returns null when key backup status hasn't finished being checked - mockClient.getKeyBackupEnabled.mockReturnValue(null); - await createAndStart(); - expect(mockDispatcher.dispatch).not.toHaveBeenCalled(); - }); - - it('dispatches keybackup event when key backup is not enabled', async () => { - mockClient.getKeyBackupEnabled.mockReturnValue(false); - await createAndStart(); - expect(mockDispatcher.dispatch).toHaveBeenCalledWith({ action: Action.ReportKeyBackupNotEnabled }); - }); - - it('does not check key backup status again after check is complete', async () => { - mockClient.getKeyBackupEnabled.mockReturnValue(null); - const instance = await createAndStart(); - expect(mockClient.getKeyBackupEnabled).toHaveBeenCalled(); - - // keyback check now complete - mockClient.getKeyBackupEnabled.mockReturnValue(true); - - // trigger a recheck - instance.dismissEncryptionSetup(); - await flushPromises(); - expect(mockClient.getKeyBackupEnabled).toHaveBeenCalledTimes(2); - - // trigger another recheck - instance.dismissEncryptionSetup(); - await flushPromises(); - // not called again, check was complete last time - expect(mockClient.getKeyBackupEnabled).toHaveBeenCalledTimes(2); - }); - }); - }); -}); diff --git a/test/components/views/settings/__snapshots__/KeyboardShortcut-test.tsx.snap b/test/components/views/settings/__snapshots__/KeyboardShortcut-test.tsx.snap index e4468c802f0..23062d79809 100644 --- a/test/components/views/settings/__snapshots__/KeyboardShortcut-test.tsx.snap +++ b/test/components/views/settings/__snapshots__/KeyboardShortcut-test.tsx.snap @@ -32,7 +32,7 @@ exports[`KeyboardShortcut doesn't render same modifier twice 1`] = ` > - missing translation: en|Ctrl + Ctrl + @@ -70,7 +70,7 @@ exports[`KeyboardShortcut doesn't render same modifier twice 2`] = ` > - missing translation: en|Ctrl + Ctrl + @@ -95,7 +95,7 @@ exports[`KeyboardShortcut renders alternative key name 1`] = ` > - missing translation: en|Page Down + Page Down + diff --git a/test/components/views/settings/tabs/user/__snapshots__/KeyboardUserSettingsTab-test.tsx.snap b/test/components/views/settings/tabs/user/__snapshots__/KeyboardUserSettingsTab-test.tsx.snap index caeea350f7b..71bab7f6129 100644 --- a/test/components/views/settings/tabs/user/__snapshots__/KeyboardUserSettingsTab-test.tsx.snap +++ b/test/components/views/settings/tabs/user/__snapshots__/KeyboardUserSettingsTab-test.tsx.snap @@ -8,7 +8,7 @@ exports[`KeyboardUserSettingsTab renders list of keyboard shortcuts 1`] = `
    - missing translation: en|Keyboard + Keyboard
    - missing translation: en|Composer + Composer
    @@ -59,7 +59,7 @@ exports[`KeyboardUserSettingsTab renders list of keyboard shortcuts 1`] = ` > - missing translation: en|Ctrl + Ctrl + @@ -103,7 +103,7 @@ exports[`KeyboardUserSettingsTab renders list of keyboard shortcuts 1`] = ` > - missing translation: en|Ctrl + Ctrl + @@ -145,7 +145,7 @@ exports[`KeyboardUserSettingsTab renders list of keyboard shortcuts 1`] = `
    - missing translation: en|Navigation + Navigation
    @@ -173,7 +173,7 @@ exports[`KeyboardUserSettingsTab renders list of keyboard shortcuts 1`] = ` > - missing translation: en|Enter + Enter diff --git a/test/test-utils/test-utils.ts b/test/test-utils/test-utils.ts index 14bd85bc5c2..b9d59025fe3 100644 --- a/test/test-utils/test-utils.ts +++ b/test/test-utils/test-utils.ts @@ -139,6 +139,8 @@ export function createTestClient(): MatrixClient { isCryptoEnabled: jest.fn().mockReturnValue(false), downloadKeys: jest.fn(), fetchRoomEvent: jest.fn(), + isCrossSigningReady: jest.fn().mockResolvedValue(true), + isSecretStorageReady: jest.fn().mockResolvedValue(true), } as unknown as MatrixClient; }