From dbc7f509929e078747d710790b39bcd43416f984 Mon Sep 17 00:00:00 2001 From: Hugh Nimmo-Smith Date: Mon, 4 Apr 2022 11:22:47 +0100 Subject: [PATCH 01/37] Wording around microphone and camera device permissions --- .../settings/tabs/user/VoiceUserSettingsTab.tsx | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/src/components/views/settings/tabs/user/VoiceUserSettingsTab.tsx b/src/components/views/settings/tabs/user/VoiceUserSettingsTab.tsx index b187e300a25..5f89a4f9743 100644 --- a/src/components/views/settings/tabs/user/VoiceUserSettingsTab.tsx +++ b/src/components/views/settings/tabs/user/VoiceUserSettingsTab.tsx @@ -106,9 +106,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 }, ), }); @@ -161,30 +161,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 } From de5b9a5234b9c66c35c8625162442313645cc6a5 Mon Sep 17 00:00:00 2001 From: Hugh Nimmo-Smith Date: Mon, 4 Apr 2022 11:24:04 +0100 Subject: [PATCH 02/37] Nasty hack to ignore missing translations without having to rebuild translations --- src/languageHandler.tsx | 3 +++ 1 file changed, 3 insertions(+) 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; } From 101c0d563ebced12439da0d63b5592f3c5da197b Mon Sep 17 00:00:00 2001 From: Hugh Nimmo-Smith Date: Mon, 4 Apr 2022 11:32:30 +0100 Subject: [PATCH 03/37] Wording updates for Verification => Setup secure messaging --- .../structures/auth/CompleteSecurity.tsx | 8 ++-- .../structures/auth/SetupEncryptionBody.tsx | 45 ++++++++++--------- .../dialogs/VerificationRequestDialog.tsx | 2 +- .../views/right_panel/EncryptionInfo.tsx | 26 ++++++++--- src/toasts/SetupEncryptionToast.ts | 12 ++--- 5 files changed, 53 insertions(+), 40 deletions(-) diff --git a/src/components/structures/auth/CompleteSecurity.tsx b/src/components/structures/auth/CompleteSecurity.tsx index 7043878a729..8f71f8a6853 100644 --- a/src/components/structures/auth/CompleteSecurity.tsx +++ b/src/components/structures/auth/CompleteSecurity.tsx @@ -71,8 +71,8 @@ export default class CompleteSecurity extends React.Component { 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 = ; @@ -81,11 +81,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/SetupEncryptionBody.tsx b/src/components/structures/auth/SetupEncryptionBody.tsx index 8783ae3f581..5411c6c5541 100644 --- a/src/components/structures/auth/SetupEncryptionBody.tsx +++ b/src/components/structures/auth/SetupEncryptionBody.tsx @@ -166,15 +166,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 complete " + + "the setup of 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") }
@@ -183,9 +183,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; @@ -198,14 +198,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.", + "Setup 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 setup.", ) }

@@ -213,7 +216,7 @@ export default class SetupEncryptionBody extends React.Component { useRecoveryKeyButton }
- { _t("Forgotten or lost all recovery methods? Reset all", null, { + { _t("Forgotten or lost all setup methods? Reset secure messaging", null, { a: (sub) =>
); } diff --git a/src/components/views/settings/CryptographyPanel.tsx b/src/components/views/settings/CryptographyPanel.tsx index 365c24ab40b..aad6b6b8a31 100644 --- a/src/components/views/settings/CryptographyPanel.tsx +++ b/src/components/views/settings/CryptographyPanel.tsx @@ -22,9 +22,6 @@ import Modal from '../../../Modal'; import { replaceableComponent } from "../../../utils/replaceableComponent"; 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 { } @@ -62,15 +59,6 @@ export default class CryptographyPanel extends React.Component { ); } - let noSendUnverifiedSetting; - if (SettingsStore.isEnabled("blacklistUnverifiedDevices")) { - noSendUnverifiedSetting = ; - } - return (
{ _t("Cryptography") } @@ -87,7 +75,6 @@ export default class CryptographyPanel extends React.Component { { importExportButtons } - { noSendUnverifiedSetting }
); } @@ -109,8 +96,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 dccfc705980..3e22f522ff4 100644 --- a/src/components/views/settings/DevicesPanelEntry.tsx +++ b/src/components/views/settings/DevicesPanelEntry.tsx @@ -40,6 +40,9 @@ interface IProps { onDeviceChange: () => void; onDeviceToggled: (device: IMyDevice) => void; selected: boolean; + canSignOut: boolean; + canRename: boolean; + canSelect: boolean; } interface IState { @@ -142,13 +145,13 @@ export default class DevicesPanelEntry extends React.Component { } 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 ?
: @@ -166,6 +169,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..06bf4c5fb7f 100644 --- a/src/components/views/settings/E2eAdvancedPanel.tsx +++ b/src/components/views/settings/E2eAdvancedPanel.tsx @@ -17,27 +17,44 @@ 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 session used by a user to mark it as trusted, not trusting cross-signed devices.", + ) }
+ : null; + + return
+ { _t("Trust") } + { 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..eabd339f9de --- /dev/null +++ b/src/components/views/settings/E2eDevicesPanel.tsx @@ -0,0 +1,333 @@ +/* +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 { replaceableComponent } from "../../../utils/replaceableComponent"; +import DevicesPanelEntry from "./DevicesPanelEntry"; +import Spinner from "../elements/Spinner"; +import AccessibleButton from "../elements/AccessibleButton"; + +interface IProps { + className?: string; +} + +interface IState { + devices: IMyDevice[]; + crossSigningInfo?: CrossSigningInfo; + deviceLoadError?: string; + selectedDevices: string[]; +} + +@replaceableComponent("views.settings.E2eDevicesPanel") +export default class E2eDevicesPanel extends React.Component { + private unmounted = false; + + constructor(props: IProps) { + super(props); + this.state = { + devices: [], + selectedDevices: [], + }; + 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 onDeviceSelectionToggled = (device: IMyDevice): void => { + if (this.unmounted) { return; } + + const deviceId = device.device_id; + this.setState((state, props) => { + // Make a copy of the selected devices, then add or remove the device + const selectedDevices = state.selectedDevices.slice(); + + const i = selectedDevices.indexOf(deviceId); + if (i === -1) { + selectedDevices.push(deviceId); + } else { + selectedDevices.splice(i, 1); + } + + return { selectedDevices }; + }); + }; + + private selectAll = (devices: IMyDevice[]): void => { + this.setState((state, props) => { + const selectedDevices = state.selectedDevices.slice(); + + for (const device of devices) { + const deviceId = device.device_id; + if (!selectedDevices.includes(deviceId)) { + selectedDevices.push(deviceId); + } + } + + return { selectedDevices }; + }); + }; + + private deselectAll = (devices: IMyDevice[]): void => { + this.setState((state, props) => { + const selectedDevices = state.selectedDevices.slice(); + + for (const device of devices) { + const deviceId = device.device_id; + const i = selectedDevices.indexOf(deviceId); + if (i !== -1) { + selectedDevices.splice(i, 1); + } + } + + return { selectedDevices }; + }); + }; + + 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 ; + }; + + 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 = []; + 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, description: string, deviceList: IMyDevice[]): JSX.Element => { + if (deviceList.length === 0) { + return ; + } + + let selectButton: JSX.Element; + if (deviceList.length > 1) { + const anySelected = deviceList.some((device) => this.state.selectedDevices.includes(device.device_id)); + const buttonAction = anySelected ? + () => { this.deselectAll(deviceList); } : + () => { this.selectAll(deviceList); }; + const buttonText = anySelected ? _t("Deselect all") : _t("Select all"); + selectButton =
+ + { buttonText } + +
; + } + + return +
+
+
+ { trustIcon } +
+
+ { title } +
+

{ description }

+ { selectButton } +
+ { deviceList.map(this.renderDevice) } +
; + }; + + const verifiedDevicesSection = section( + , + _t("Setup for secure messaging"), + _t("These devices are setup for end-to-end encryption and are able to access your past secure messages. They will also show as trusted to others."), + verifiedDevices, + ); + + const unverifiedDevicesSection = section( + , + _t("Not setup for secure messaging"), + _t("These devices have not been setup for end-to-end encryption messages and will show as untrusted to others. You can set them up so that you"), + unverifiedDevices, + ); + + const nonCryptoDevicesSection = section( + , + _t("Devices without secure messaging support"), + _t("These devices do not support secure messaging."), + nonCryptoDevices, + ); + + const otherDevicesSection = (otherDevices.length > 0) ? + + { verifiedDevicesSection } + { unverifiedDevicesSection } + { nonCryptoDevicesSection } + : + +
+
+ { _t("You aren't signed in to any other devices.") } +
+
; + + const thisDeviceDescription = this.isDeviceVerified(myDevice) ? _t("Secure messaging is setup on this device."): _t("Secure messaging is not setup on this device."); + + const classes = classNames(this.props.className, "mx_DevicesPanel"); + return ( +
+
+
+ { _t("This device") } +
+
+

{ thisDeviceDescription }

+ { this.renderDevice(myDevice) } + { otherDevicesSection } +
+ ); + } +} 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..152dce9db98 --- /dev/null +++ b/src/components/views/settings/tabs/user/AccountUserSettingsTab.tsx @@ -0,0 +1,412 @@ +/* +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 { SERVICE_TYPES } from "matrix-js-sdk/src/service-types"; +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 { Policies, Service, startTermsFlow } from "../../../../../Terms"; +import IdentityAuthClient from "../../../../../IdentityAuthClient"; +import { abbreviateUrl } from "../../../../../utils/UrlUtils"; +import { getThreepidsWithBindStatus } from '../../../../../boundThreepids'; +import Spinner from "../../../elements/Spinner"; +import { UIFeature } from "../../../../../settings/UIFeature"; +import { replaceableComponent } from "../../../../../utils/replaceableComponent"; +import { ActionPayload } from "../../../../../dispatcher/payloads"; +import ErrorDialog from "../../../dialogs/ErrorDialog"; +import AccountPhoneNumbers from "../../account/PhoneNumbers"; +import AccountEmailAddresses from "../../account/EmailAddresses"; +import DiscoveryEmailAddresses from "../../discovery/EmailAddresses"; +import DiscoveryPhoneNumbers from "../../discovery/PhoneNumbers"; +import ChangePassword from "../../ChangePassword"; +import InlineTermsAgreement from "../../../terms/InlineTermsAgreement"; +import SetIdServer from "../../SetIdServer"; +import AccountDevicesPanel from '../../AccountDevicesPanel'; + +interface IProps { + closeSettingsFn: () => void; +} + +interface IState { + haveIdServer: boolean; + serverSupportsSeparateAddAndBind: boolean; + idServerHasUnsignedTerms: boolean; + 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 + }; + emails: IThreepid[]; + msisdns: IThreepid[]; + loading3pids: boolean; // whether or not the emails and msisdns have been loaded + canChangePassword: boolean; + idServerName: string; +} + +@replaceableComponent("views.settings.tabs.user.AccountUserSettingsTab") +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, + 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 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(); + + // 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 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 sessions 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 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 */ } + +
+ ); + } + + 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 { + const discoWarning = this.state.requiredPolicyInfo.hasTerms + ? {_t("Warning")} + : null; + + let accountManagementSection; + if (SettingsStore.getValue(UIFeature.Deactivate)) { + accountManagementSection = <> +
{ _t("Danger zone") }
+ { this.renderManagementSection() } + ; + } + + let discoverySection; + if (SettingsStore.getValue(UIFeature.IdentityServer)) { + discoverySection = <> +
{ discoWarning } { _t("Discovery") }
+ { this.renderDiscoverySection() } + ; + } + + return ( +
+
{ _t("Account") }
+
+ { _t("Where you're signed in") } +
+ + { _t( + "Manage your signed-in devices below. " + + "A device's name is visible to people you communicate with.", + ) } + + +
+
+ { this.renderAccountSection() } + { discoverySection } + { accountManagementSection } +
+ ); + } +} diff --git a/src/components/views/settings/tabs/user/GeneralUserSettingsTab.tsx b/src/components/views/settings/tabs/user/GeneralUserSettingsTab.tsx index 14851b5becb..0f29e5255ff 100644 --- a/src/components/views/settings/tabs/user/GeneralUserSettingsTab.tsx +++ b/src/components/views/settings/tabs/user/GeneralUserSettingsTab.tsx @@ -17,9 +17,6 @@ limitations under the License. */ import React from 'react'; -import { SERVICE_TYPES } from "matrix-js-sdk/src/service-types"; -import { IThreepid } from "matrix-js-sdk/src/@types/threepids"; -import { logger } from "matrix-js-sdk/src/logger"; import { _t } from "../../../../../languageHandler"; import ProfileSettings from "../../ProfileSettings"; @@ -27,30 +24,11 @@ import * as languageHandler from "../../../../../languageHandler"; import SettingsStore from "../../../../../settings/SettingsStore"; import LanguageDropdown from "../../../elements/LanguageDropdown"; import SpellCheckSettings from "../../SpellCheckSettings"; -import AccessibleButton from "../../../elements/AccessibleButton"; -import DeactivateAccountDialog from "../../../dialogs/DeactivateAccountDialog"; import PlatformPeg from "../../../../../PlatformPeg"; -import { MatrixClientPeg } from "../../../../../MatrixClientPeg"; -import Modal from "../../../../../Modal"; -import dis from "../../../../../dispatcher/dispatcher"; -import { Policies, Service, startTermsFlow } from "../../../../../Terms"; -import IdentityAuthClient from "../../../../../IdentityAuthClient"; -import { abbreviateUrl } from "../../../../../utils/UrlUtils"; -import { getThreepidsWithBindStatus } from '../../../../../boundThreepids'; -import Spinner from "../../../elements/Spinner"; import { SettingLevel } from "../../../../../settings/SettingLevel"; -import { UIFeature } from "../../../../../settings/UIFeature"; import { replaceableComponent } from "../../../../../utils/replaceableComponent"; -import { ActionPayload } from "../../../../../dispatcher/payloads"; -import ErrorDialog from "../../../dialogs/ErrorDialog"; -import AccountPhoneNumbers from "../../account/PhoneNumbers"; -import AccountEmailAddresses from "../../account/EmailAddresses"; -import DiscoveryEmailAddresses from "../../discovery/EmailAddresses"; -import DiscoveryPhoneNumbers from "../../discovery/PhoneNumbers"; -import ChangePassword from "../../ChangePassword"; -import InlineTermsAgreement from "../../../terms/InlineTermsAgreement"; -import SetIdServer from "../../SetIdServer"; import SetIntegrationManager from "../../SetIntegrationManager"; +import { UIFeature } from '../../../../../settings/UIFeature'; interface IProps { closeSettingsFn: () => void; @@ -59,72 +37,17 @@ interface IProps { interface IState { language: string; spellCheckLanguages: string[]; - haveIdServer: boolean; - serverSupportsSeparateAddAndBind: boolean; - idServerHasUnsignedTerms: boolean; - 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 - }; - emails: IThreepid[]; - msisdns: IThreepid[]; - loading3pids: boolean; // whether or not the emails and msisdns have been loaded - canChangePassword: boolean; - idServerName: string; } @replaceableComponent("views.settings.tabs.user.GeneralUserSettingsTab") 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 { @@ -136,96 +59,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; @@ -247,40 +80,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 (
@@ -289,68 +88,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 ( @@ -377,64 +114,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; @@ -450,42 +129,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/PrivacyUserSettingsTab.tsx similarity index 54% rename from src/components/views/settings/tabs/user/MjolnirUserSettingsTab.tsx rename to src/components/views/settings/tabs/user/PrivacyUserSettingsTab.tsx index 88000892e4e..cdd8e9b9e10 100644 --- a/src/components/views/settings/tabs/user/MjolnirUserSettingsTab.tsx +++ b/src/components/views/settings/tabs/user/PrivacyUserSettingsTab.tsx @@ -15,6 +15,8 @@ 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"; @@ -29,25 +31,251 @@ 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'; + +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; } @replaceableComponent("views.settings.tabs.user.MjolnirUserSettingsTab") -export default class MjolnirUserSettingsTab extends React.Component<{}, IState> { +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, }; } + + 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); + this.mjolnirWatcher = SettingsStore.watchSetting("feature_mjolnir", null, this.mjolnirChanged); + } + + 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 }); }; @@ -238,6 +466,60 @@ export default class MjolnirUserSettingsTab extends React.Component<{}, IState> 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; + + return ( +
+
{ _t("Privacy") }
+ { analyticsSection } + { ignoreUsersPanel } + { invitesPanel } + { mjolnirSection } +
+ ); + } + + renderMjolnir(brand: string) { return (
{ _t("Ignored users") }
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..0ff61a50a76 --- /dev/null +++ b/src/components/views/settings/tabs/user/SecureMessagingUserSettingsTab.tsx @@ -0,0 +1,142 @@ +/* +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 "../../../../../createRoom"; +import SecureBackupPanel from "../../SecureBackupPanel"; +import SettingsStore from "../../../../../settings/SettingsStore"; +import { UIFeature } from "../../../../../settings/UIFeature"; +import E2eAdvancedPanel, { isE2eAdvancedPanelPossible } from "../../E2eAdvancedPanel"; +import { replaceableComponent } from "../../../../../utils/replaceableComponent"; +import CryptographyPanel from "../../CryptographyPanel"; +import E2eDevicesPanel from "../../E2eDevicesPanel"; +import CrossSigningPanel from "../../CrossSigningPanel"; +import EventIndexPanel from "../../EventIndexPanel"; +import { MatrixClientPeg } from '../../../../../MatrixClientPeg'; + +interface IProps { + closeSettingsFn: () => void; +} + +interface IState { + canChangePassword: boolean, +} + +@replaceableComponent("views.settings.tabs.user.SecureMessagingUserSettingsTab") +export default class SecureMessagingUserSettingsTab extends React.Component { + constructor(props: IProps) { + super(props); + + this.state = { + canChangePassword: false, + }; + } + + // 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 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({ canChangePassword }); + } + + 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 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)) { + const e2ePanel = isE2eAdvancedPanelPossible() ? : null; + // only show the section if there's something to show + if (e2ePanel) { + advancedSection = <> +
{ _t("Advanced") }
+
+ { e2ePanel } + { eventIndex } + { crossSigning } + +
+ ; + } + } + + return ( +
+ { warning } +
{ _t("Secure messaging") }
+

{ _t("Secure messages are protected using end-to-end encryption ") }

+
{ _t("Where you're signed in") }
+
+ + { _t( + "Manage your signed-in devices below. " + + "A device's name is visible to people you communicate with.", + ) } + + +
+
+ { secureBackup } +
+ { 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 aa66d0203a4..00000000000 --- a/src/components/views/settings/tabs/user/SecurityUserSettingsTab.tsx +++ /dev/null @@ -1,382 +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 { privateShouldBeEncrypted } from "../../../../../createRoom"; -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 { replaceableComponent } from "../../../../../utils/replaceableComponent"; -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"; - -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; -} - -@replaceableComponent("views.settings.tabs.user.SecurityUserSettingsTab") -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/toasts/BulkUnverifiedSessionsToast.ts b/src/toasts/BulkUnverifiedSessionsToast.ts index 0a35a913456..ee8bfa0cf38 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, }); }; diff --git a/src/toasts/UnverifiedSessionToast.ts b/src/toasts/UnverifiedSessionToast.ts index 05425b93c0e..5ac790b33ad 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, }); }; From 8dcc40a314be71eb9d6a4ed7a11abf8ec7a273ee Mon Sep 17 00:00:00 2001 From: Hugh Nimmo-Smith Date: Mon, 4 Apr 2022 12:53:07 +0100 Subject: [PATCH 05/37] Move Discovery settings into Privacy panel --- .../settings/discovery/EmailAddresses.tsx | 2 +- .../views/settings/discovery/PhoneNumbers.tsx | 2 +- .../tabs/user/AccountUserSettingsTab.tsx | 137 -------------- .../tabs/user/PrivacyUserSettingsTab.tsx | 174 ++++++++++++++++++ 4 files changed, 176 insertions(+), 139 deletions(-) diff --git a/src/components/views/settings/discovery/EmailAddresses.tsx b/src/components/views/settings/discovery/EmailAddresses.tsx index 316e20202e9..9b2147ee925 100644 --- a/src/components/views/settings/discovery/EmailAddresses.tsx +++ b/src/components/views/settings/discovery/EmailAddresses.tsx @@ -254,7 +254,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 13bef06e4b9..c5c72666298 100644 --- a/src/components/views/settings/discovery/PhoneNumbers.tsx +++ b/src/components/views/settings/discovery/PhoneNumbers.tsx @@ -270,7 +270,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/user/AccountUserSettingsTab.tsx b/src/components/views/settings/tabs/user/AccountUserSettingsTab.tsx index 152dce9db98..5bab1cb5838 100644 --- a/src/components/views/settings/tabs/user/AccountUserSettingsTab.tsx +++ b/src/components/views/settings/tabs/user/AccountUserSettingsTab.tsx @@ -17,7 +17,6 @@ limitations under the License. */ import React from 'react'; -import { SERVICE_TYPES } from "matrix-js-sdk/src/service-types"; import { IThreepid } from "matrix-js-sdk/src/@types/threepids"; import { logger } from "matrix-js-sdk/src/logger"; @@ -28,9 +27,6 @@ import DeactivateAccountDialog from "../../../dialogs/DeactivateAccountDialog"; import { MatrixClientPeg } from "../../../../../MatrixClientPeg"; import Modal from "../../../../../Modal"; import dis from "../../../../../dispatcher/dispatcher"; -import { Policies, Service, startTermsFlow } from "../../../../../Terms"; -import IdentityAuthClient from "../../../../../IdentityAuthClient"; -import { abbreviateUrl } from "../../../../../utils/UrlUtils"; import { getThreepidsWithBindStatus } from '../../../../../boundThreepids'; import Spinner from "../../../elements/Spinner"; import { UIFeature } from "../../../../../settings/UIFeature"; @@ -39,11 +35,7 @@ import { ActionPayload } from "../../../../../dispatcher/payloads"; import ErrorDialog from "../../../dialogs/ErrorDialog"; import AccountPhoneNumbers from "../../account/PhoneNumbers"; import AccountEmailAddresses from "../../account/EmailAddresses"; -import DiscoveryEmailAddresses from "../../discovery/EmailAddresses"; -import DiscoveryPhoneNumbers from "../../discovery/PhoneNumbers"; import ChangePassword from "../../ChangePassword"; -import InlineTermsAgreement from "../../../terms/InlineTermsAgreement"; -import SetIdServer from "../../SetIdServer"; import AccountDevicesPanel from '../../AccountDevicesPanel'; interface IProps { @@ -53,21 +45,10 @@ interface IProps { interface IState { haveIdServer: boolean; serverSupportsSeparateAddAndBind: boolean; - idServerHasUnsignedTerms: boolean; - 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 - }; emails: IThreepid[]; msisdns: IThreepid[]; loading3pids: boolean; // whether or not the emails and msisdns have been loaded canChangePassword: boolean; - idServerName: string; } @replaceableComponent("views.settings.tabs.user.AccountUserSettingsTab") @@ -80,18 +61,10 @@ export default class AccountUserSettingsTab extends React.Component { 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 = []; @@ -162,51 +132,6 @@ export default class AccountUserSettingsTab extends React.Component { - 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 onPasswordChangeError = (err): void => { // TODO: Figure out a design that doesn't involve replacing the current dialog let errMsg = err.error || err.message || ""; @@ -303,49 +228,6 @@ export default class AccountUserSettingsTab 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 ( @@ -362,16 +244,6 @@ export default class AccountUserSettingsTab extends React.Component - : null; - let accountManagementSection; if (SettingsStore.getValue(UIFeature.Deactivate)) { accountManagementSection = <> @@ -380,14 +252,6 @@ export default class AccountUserSettingsTab extends React.Component; } - let discoverySection; - if (SettingsStore.getValue(UIFeature.IdentityServer)) { - discoverySection = <> -
{ discoWarning } { _t("Discovery") }
- { this.renderDiscoverySection() } - ; - } - return (
{ _t("Account") }
@@ -404,7 +268,6 @@ export default class AccountUserSettingsTab extends React.Component
{ this.renderAccountSection() } - { discoverySection } { accountManagementSection }
); diff --git a/src/components/views/settings/tabs/user/PrivacyUserSettingsTab.tsx b/src/components/views/settings/tabs/user/PrivacyUserSettingsTab.tsx index cdd8e9b9e10..f6658febf52 100644 --- a/src/components/views/settings/tabs/user/PrivacyUserSettingsTab.tsx +++ b/src/components/views/settings/tabs/user/PrivacyUserSettingsTab.tsx @@ -15,9 +15,11 @@ 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"; @@ -41,6 +43,15 @@ 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; @@ -79,6 +90,21 @@ interface IState { 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; } @replaceableComponent("views.settings.tabs.user.MjolnirUserSettingsTab") @@ -101,15 +127,55 @@ export default class MjolnirUserSettingsTab extends React.Component { + 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(); } }; @@ -117,6 +183,7 @@ export default class MjolnirUserSettingsTab extends React.Component { + 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; @@ -508,9 +663,28 @@ export default class MjolnirUserSettingsTab extends React.Component + : null; + + let discoverySection; + if (SettingsStore.getValue(UIFeature.IdentityServer)) { + discoverySection = <> +
{ discoWarning } { _t("Discovery") }
+ { this.renderDiscoverySection() } + ; + } + return (
{ _t("Privacy") }
+ { discoverySection } { analyticsSection } { ignoreUsersPanel } { invitesPanel } From d1abcab61abed0cf74badb29d76ae9c59033996b Mon Sep 17 00:00:00 2001 From: Hugh Nimmo-Smith Date: Mon, 4 Apr 2022 19:57:28 +0100 Subject: [PATCH 06/37] Only prompt to setup cross-signing when you access an encrypted room --- src/DeviceListener.ts | 16 +++++++++++--- src/components/structures/MatrixChat.tsx | 27 ++++++++++++------------ src/components/structures/RoomView.tsx | 3 +++ 3 files changed, 30 insertions(+), 16 deletions(-) diff --git a/src/DeviceListener.ts b/src/DeviceListener.ts index 05f86453f1f..e00aef20079 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/components/structures/MatrixChat.tsx b/src/components/structures/MatrixChat.tsx index 969486c536f..4febc98684b 100644 --- a/src/components/structures/MatrixChat.tsx +++ b/src/components/structures/MatrixChat.tsx @@ -93,7 +93,6 @@ import { shouldUseLoginForWelcome } from "../../utils/pages"; import { replaceableComponent } from "../../utils/replaceableComponent"; 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, { UserTab } from '../views/dialogs/UserSettingsDialog'; @@ -376,18 +375,20 @@ 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(); - } + // 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 }); } diff --git a/src/components/structures/RoomView.tsx b/src/components/structures/RoomView.tsx index ab06369406e..44c0b2a688b 100644 --- a/src/components/structures/RoomView.tsx +++ b/src/components/structures/RoomView.tsx @@ -109,6 +109,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'; const DEBUG = false; let debuglog = function(msg: string) {}; @@ -1140,6 +1141,8 @@ export class RoomView extends React.Component { e2eStatus = await shieldStatusForRoom(this.context, room); } + await DeviceListener.sharedInstance().viewingEncryptedRoom(); + if (this.unmounted) return; this.setState({ e2eStatus }); } From 14305938236134fa284e7dd0eda0996db0bf90ab Mon Sep 17 00:00:00 2001 From: Hugh Nimmo-Smith Date: Mon, 4 Apr 2022 19:58:51 +0100 Subject: [PATCH 07/37] Prompt user to setup cross-signing when decryption fails on an event if cross-signing isn't set up --- src/components/views/rooms/EventTile.tsx | 133 ++++++++++++++++------- 1 file changed, 91 insertions(+), 42 deletions(-) diff --git a/src/components/views/rooms/EventTile.tsx b/src/components/views/rooms/EventTile.tsx index b73ec31e293..a527dcccbc8 100644 --- a/src/components/views/rooms/EventTile.tsx +++ b/src/components/views/rooms/EventTile.tsx @@ -83,6 +83,8 @@ import { shouldDisplayReply } from '../../../utils/Reply'; import PosthogTrackers from "../../../PosthogTrackers"; import TileErrorBoundary from '../messages/TileErrorBoundary'; import ThreadSummary, { ThreadMessagePreview } from './ThreadSummary'; +import Modal from '../../../Modal'; +import SetupEncryptionDialog from '../dialogs/security/SetupEncryptionDialog'; export type GetRelationsForEvent = (eventId: string, relationType: string, eventType: string) => Relations; @@ -344,6 +346,8 @@ interface IState { thread: Thread; threadNotification?: NotificationCountType; + + userHasSecureMessagingSetup: boolean; } // MUST be rendered within a RoomContext with a set timelineRenderingType @@ -389,6 +393,8 @@ export class UnwrappedEventTile extends React.Component { hover: false, thread, + + userHasSecureMessagingSetup: false, }; // don't do RR animations until we are mounted @@ -493,6 +499,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); @@ -577,6 +586,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) { @@ -696,6 +707,14 @@ export class UnwrappedEventTile extends React.Component { } }; + private async updateCryptoSetupState() { + const crypto = MatrixClientPeg.get().crypto; + const userHasSecureMessagingSetup = + await crypto.isCrossSigningReady() && await crypto.isSecretStorageReady(); + + this.setState({ userHasSecureMessagingSetup }); + } + private onUserVerificationChanged = (userId: string, _trustStatus: UserTrustLevel): void => { if (userId === this.props.mxEvent.getSender()) { this.verifyEvent(this.props.mxEvent); @@ -945,6 +964,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. @@ -1251,48 +1275,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 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 } + , + }, + ); + keyRequestInfo = +
+ + { keyRequestInfoContent } + + +
; + } else { + keyRequestInfo = +
+ + { _t( + 'Setup secure messaging to access this message.', + {}, + { + 'requestLink': (sub) => + + { sub } + , + }, + ) } + +
; + } + } let reactionsRow; if (!isRedacted) { From 96135f7010c34bcf835ab9ee22c9730af2d235e5 Mon Sep 17 00:00:00 2001 From: Hugh Nimmo-Smith Date: Mon, 4 Apr 2022 19:59:23 +0100 Subject: [PATCH 08/37] Move backup in to Advanced and Search to top level of Secure Messaging --- .../settings/tabs/user/SecureMessagingUserSettingsTab.tsx | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/src/components/views/settings/tabs/user/SecureMessagingUserSettingsTab.tsx b/src/components/views/settings/tabs/user/SecureMessagingUserSettingsTab.tsx index 0ff61a50a76..93d4144562d 100644 --- a/src/components/views/settings/tabs/user/SecureMessagingUserSettingsTab.tsx +++ b/src/components/views/settings/tabs/user/SecureMessagingUserSettingsTab.tsx @@ -108,8 +108,8 @@ export default class SecureMessagingUserSettingsTab extends React.Component
{ _t("Advanced") }
+ { secureBackup } { e2ePanel } - { eventIndex } { crossSigning }
@@ -131,9 +131,7 @@ export default class SecureMessagingUserSettingsTab extends React.Component -
-
- { secureBackup } + { eventIndex }
{ advancedSection }
From 2ccee9c313c863c74ccbef678567fb8011767236 Mon Sep 17 00:00:00 2001 From: Hugh Nimmo-Smith Date: Mon, 4 Apr 2022 20:03:06 +0100 Subject: [PATCH 09/37] Secure Messaging settings layout --- .../tabs/user/SecureMessagingUserSettingsTab.tsx | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/src/components/views/settings/tabs/user/SecureMessagingUserSettingsTab.tsx b/src/components/views/settings/tabs/user/SecureMessagingUserSettingsTab.tsx index 93d4144562d..4e4f5ce52ec 100644 --- a/src/components/views/settings/tabs/user/SecureMessagingUserSettingsTab.tsx +++ b/src/components/views/settings/tabs/user/SecureMessagingUserSettingsTab.tsx @@ -73,13 +73,6 @@ export default class SecureMessagingUserSettingsTab extends React.Component ); - 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 @@ -131,7 +124,10 @@ export default class SecureMessagingUserSettingsTab extends React.Component - { eventIndex } +
+
{ _t("Search") }
+
+
{ advancedSection }
From d9248da08b45b66b0465d41e8dc164d0d204f142 Mon Sep 17 00:00:00 2001 From: Hugh Nimmo-Smith Date: Mon, 4 Apr 2022 20:16:34 +0100 Subject: [PATCH 10/37] Fudge tests to pass --- test/DeviceListener-test.ts | 244 ------------------ .../KeyboardShortcut-test.tsx.snap | 6 +- .../KeyboardUserSettingsTab-test.tsx.snap | 12 +- 3 files changed, 9 insertions(+), 253 deletions(-) delete mode 100644 test/DeviceListener-test.ts diff --git a/test/DeviceListener-test.ts b/test/DeviceListener-test.ts deleted file mode 100644 index 957e622b354..00000000000 --- a/test/DeviceListener-test.ts +++ /dev/null @@ -1,244 +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 './skinned-sdk'; -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 From 2226263e6aae6fd4daa678cf28ffc4ebfa7784c7 Mon Sep 17 00:00:00 2001 From: Hugh Nimmo-Smith Date: Mon, 4 Apr 2022 20:22:12 +0100 Subject: [PATCH 11/37] Fix i8n + test --- src/components/views/rooms/EventTile.tsx | 4 +- src/i18n/strings/en_EN.json | 201 +++++++++++++---------- test/test-utils/test-utils.ts | 2 + 3 files changed, 114 insertions(+), 93 deletions(-) diff --git a/src/components/views/rooms/EventTile.tsx b/src/components/views/rooms/EventTile.tsx index a527dcccbc8..8fa308d6ee5 100644 --- a/src/components/views/rooms/EventTile.tsx +++ b/src/components/views/rooms/EventTile.tsx @@ -708,9 +708,9 @@ export class UnwrappedEventTile extends React.Component { }; private async updateCryptoSetupState() { - const crypto = MatrixClientPeg.get().crypto; + const client = MatrixClientPeg.get(); const userHasSecureMessagingSetup = - await crypto.isCrossSigningReady() && await crypto.isSecretStorageReady(); + await client.isCrossSigningReady() && await client.isSecretStorageReady(); this.setState({ userHasSecureMessagingSetup }); } diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index dea47408e99..cd6b56c5f6b 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -803,13 +803,13 @@ "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", + "Setup recovery for secure messaging": "Setup recovery for secure messaging", + "Secure messaging upgrade available": "Secure messaging upgrade available", + "Setup secure messaging on this device": "Setup secure messaging on this device", + "Setup": "Setup", "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", @@ -1172,6 +1172,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 .", @@ -1196,6 +1212,8 @@ "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.", "Cross-signing is not set up.": "Cross-signing is not set up.", + "Set up Secure Backup": "Set up Secure Backup", + "Verify this session": "Verify this session", "Reset": "Reset", "Cross-signing public keys:": "Cross-signing public keys:", "in memory": "in memory", @@ -1215,31 +1233,21 @@ "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", + "Verify": "Verify", "Sign Out": "Sign Out", - "Display Name": "Display Name", "Rename": "Rename", + "Display Name": "Display Name", "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.", + "Setup for secure messaging": "Setup for secure messaging", + "These devices are setup for end-to-end encryption and are able to access your past secure messages. They will also show as trusted to others.": "These devices are setup for end-to-end encryption and are able to access your past secure messages. They will also show as trusted to others.", + "Not setup for secure messaging": "Not setup for secure messaging", + "These devices have not been setup for end-to-end encryption messages and will show as untrusted to others. You can set them up so that you": "These devices have not been setup for end-to-end encryption messages and will show as untrusted to others. You can set them up so that you", + "Devices without secure messaging support": "Devices without secure messaging support", + "These devices do not support secure messaging.": "These devices do not support secure messaging.", + "Secure messaging is setup on this device.": "Secure messaging is setup on this device.", + "Secure messaging is not setup on this device.": "Secure messaging is not setup on this device.", "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", @@ -1389,24 +1397,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", "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. 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.", + "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.", "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", @@ -1431,6 +1440,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.", @@ -1450,7 +1481,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.", @@ -1464,36 +1499,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", "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 messaging": "Secure messaging", + "Secure messages are protected using end-to-end encryption ": "Secure messages are protected using end-to-end encryption ", + "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.", @@ -1504,17 +1515,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", @@ -1614,14 +1625,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", @@ -1647,6 +1658,7 @@ "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.", "Key request sent.": "Key request sent.", "Re-request encryption keys from your other sessions.": "Re-request encryption keys from your other sessions.", + "Setup secure messaging to access this message.": "Setup secure messaging to access this message.", "Message Actions": "Message Actions", "View in room": "View in room", "Copy link to thread": "Copy link to thread", @@ -1745,7 +1757,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", @@ -1918,10 +1929,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 setup secure messaging.": "Please check your other device(s) and accept the request to setup secure messaging.", + "Setup secure messaging for new device": "Setup 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.", @@ -2415,6 +2427,7 @@ "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.", + "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.", "Enable end-to-end encryption": "Enable end-to-end encryption", "You might enable this if the room will only be used for collaborating with internal teams on your homeserver. This cannot be changed later.": "You might enable this if the room will only be used for collaborating with internal teams on your homeserver. This cannot be changed later.", "You might disable this if the room will be used for collaborating with external teams who have their own homeserver. This cannot be changed later.": "You might disable this if the room will be used for collaborating with external teams who have their own homeserver. This cannot be changed later.", @@ -2753,8 +2766,10 @@ "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", + "Secure Messaging": "Secure Messaging", + "Shortcuts": "Shortcuts", + "Setup secure messaging": "Setup secure messaging", + "Secure messaging setup request": "Secure messaging setup request", "Approve widget permissions": "Approve widget permissions", "This widget would like to:": "This widget would like to:", "Approve": "Approve", @@ -3125,9 +3140,9 @@ "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?", + "Verify this device": "Verify this device", "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.", @@ -3182,18 +3197,21 @@ "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 complete the setup of 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 complete the setup of 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", + "Setup secure messaging on this device to access past encrypted messages and allow others to trust it.": "Setup 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 setup.": "Please select how you would like to do the setup.", + "Forgotten or lost all setup methods? Reset secure messaging": "Forgotten or lost all setup methods? Reset secure messaging", + "Secure messaging is now setup on this device and you can access your past encrypted message. Others will see this device as trusted.": "Secure messaging is now setup on this device and you can access your past encrypted message. Others will see this device as trusted.", + "Secure messaging is now setup on this device and others will see it as trusted.": "Secure messaging is now setup 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.", + "Setup later": "Setup 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", @@ -3296,6 +3314,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/test/test-utils/test-utils.ts b/test/test-utils/test-utils.ts index 45edc7fa15e..4d0aa4f2385 100644 --- a/test/test-utils/test-utils.ts +++ b/test/test-utils/test-utils.ts @@ -136,6 +136,8 @@ export function createTestClient(): MatrixClient { relations: jest.fn().mockRejectedValue(undefined), isCryptoEnabled: jest.fn().mockReturnValue(false), fetchRoomEvent: jest.fn(), + isCrossSigningReady: jest.fn().mockResolvedValue(true), + isSecretStorageReady: jest.fn().mockResolvedValue(true), } as unknown as MatrixClient; } From 2d59f1bd21df8cdce241adfc9ba7a2896b14c7a7 Mon Sep 17 00:00:00 2001 From: Hugh Nimmo-Smith Date: Mon, 4 Apr 2022 20:28:29 +0100 Subject: [PATCH 12/37] Lint fixes --- src/components/views/settings/E2eAdvancedPanel.tsx | 2 +- .../views/settings/tabs/user/PrivacyUserSettingsTab.tsx | 1 + .../views/settings/tabs/user/SecureMessagingUserSettingsTab.tsx | 2 +- 3 files changed, 3 insertions(+), 2 deletions(-) diff --git a/src/components/views/settings/E2eAdvancedPanel.tsx b/src/components/views/settings/E2eAdvancedPanel.tsx index 06bf4c5fb7f..6090ea648e1 100644 --- a/src/components/views/settings/E2eAdvancedPanel.tsx +++ b/src/components/views/settings/E2eAdvancedPanel.tsx @@ -26,7 +26,7 @@ const SETTING_MANUALLY_VERIFY_ALL_SESSIONS = "e2ee.manuallyVerifyAllSessions"; function updateBlacklistDevicesFlag(checked: boolean): void { MatrixClientPeg.get().setGlobalBlacklistUnverifiedDevices(checked); -}; +} const E2eAdvancedPanel = props => { const blacklistUnverifiedDevices = SettingsStore.isEnabled("blacklistUnverifiedDevices") ? diff --git a/src/components/views/settings/tabs/user/PrivacyUserSettingsTab.tsx b/src/components/views/settings/tabs/user/PrivacyUserSettingsTab.tsx index f6658febf52..b2942f2938e 100644 --- a/src/components/views/settings/tabs/user/PrivacyUserSettingsTab.tsx +++ b/src/components/views/settings/tabs/user/PrivacyUserSettingsTab.tsx @@ -666,6 +666,7 @@ export default class MjolnirUserSettingsTab extends React.Component Date: Mon, 4 Apr 2022 20:33:25 +0100 Subject: [PATCH 13/37] Ignore eslint warnings --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index abba2bb7535..237e9db200b 100644 --- a/package.json +++ b/package.json @@ -49,7 +49,7 @@ "start:all": "concurrently --kill-others-on-fail --prefix \"{time} [{name}]\" -n build,reskindex \"yarn start:build\" \"yarn reskindex:watch\"", "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\"", From 8367f061f08f4c010d70acc717aab7e583d44cba Mon Sep 17 00:00:00 2001 From: Hugh Nimmo-Smith Date: Mon, 4 Apr 2022 20:54:36 +0100 Subject: [PATCH 14/37] Account panel devices wording --- .../views/settings/tabs/user/AccountUserSettingsTab.tsx | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/components/views/settings/tabs/user/AccountUserSettingsTab.tsx b/src/components/views/settings/tabs/user/AccountUserSettingsTab.tsx index 5bab1cb5838..6b5600e958f 100644 --- a/src/components/views/settings/tabs/user/AccountUserSettingsTab.tsx +++ b/src/components/views/settings/tabs/user/AccountUserSettingsTab.tsx @@ -260,8 +260,7 @@ export default class AccountUserSettingsTab extends React.Component { _t( - "Manage your signed-in devices below. " + - "A device's name is visible to people you communicate with.", + "Manage your signed-in devices below.", ) } From baabe418d7d71e14904257adee03901d41e3a419 Mon Sep 17 00:00:00 2001 From: Hugh Nimmo-Smith Date: Mon, 4 Apr 2022 22:02:52 +0100 Subject: [PATCH 15/37] Wording for new login popup --- src/i18n/strings/en_EN.json | 10 +++++----- src/toasts/BulkUnverifiedSessionsToast.ts | 6 +++--- src/toasts/UnverifiedSessionToast.ts | 2 +- 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index cd6b56c5f6b..fbb7ff41d2f 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -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", @@ -812,7 +812,6 @@ "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", @@ -1410,7 +1409,7 @@ "Danger zone": "Danger zone", "Account": "Account", "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.", + "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 session.": "Appearance Settings only affect this %(brand)s session.", @@ -1504,6 +1503,7 @@ "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 messaging": "Secure messaging", "Secure messages are protected using end-to-end encryption ": "Secure messages are protected using end-to-end encryption ", + "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.", "Search": "Search", "Sidebar": "Sidebar", "Spaces to show": "Spaces to show", diff --git a/src/toasts/BulkUnverifiedSessionsToast.ts b/src/toasts/BulkUnverifiedSessionsToast.ts index ee8bfa0cf38..a7d654eee14 100644 --- a/src/toasts/BulkUnverifiedSessionsToast.ts +++ b/src/toasts/BulkUnverifiedSessionsToast.ts @@ -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/UnverifiedSessionToast.ts b/src/toasts/UnverifiedSessionToast.ts index 5ac790b33ad..d0ac3c19518 100644 --- a/src/toasts/UnverifiedSessionToast.ts +++ b/src/toasts/UnverifiedSessionToast.ts @@ -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, From 6f77b762323fec5c5563832b4b52423feeeea6b0 Mon Sep 17 00:00:00 2001 From: Hugh Nimmo-Smith Date: Tue, 5 Apr 2022 22:19:36 +0100 Subject: [PATCH 16/37] Setup => Set up when used as a verb I'm no English major --- .../security/RecoveryMethodRemovedDialog.tsx | 2 +- .../structures/auth/SetupEncryptionBody.tsx | 16 +++---- .../dialogs/VerificationRequestDialog.tsx | 2 +- .../views/right_panel/EncryptionInfo.tsx | 4 +- src/components/views/rooms/EventTile.tsx | 2 +- .../views/settings/E2eDevicesPanel.tsx | 10 ++--- src/i18n/strings/en_EN.json | 42 +++++++++---------- src/toasts/SetupEncryptionToast.ts | 4 +- 8 files changed, 41 insertions(+), 41 deletions(-) diff --git a/src/async-components/views/dialogs/security/RecoveryMethodRemovedDialog.tsx b/src/async-components/views/dialogs/security/RecoveryMethodRemovedDialog.tsx index a47e7636b38..9e31ff54c50 100644 --- a/src/async-components/views/dialogs/security/RecoveryMethodRemovedDialog.tsx +++ b/src/async-components/views/dialogs/security/RecoveryMethodRemovedDialog.tsx @@ -57,7 +57,7 @@ export default class RecoveryMethodRemovedDialog extends React.PureComponent

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

diff --git a/src/components/structures/auth/SetupEncryptionBody.tsx b/src/components/structures/auth/SetupEncryptionBody.tsx index 5411c6c5541..680837099cf 100644 --- a/src/components/structures/auth/SetupEncryptionBody.tsx +++ b/src/components/structures/auth/SetupEncryptionBody.tsx @@ -166,8 +166,8 @@ export default class SetupEncryptionBody extends React.Component return (

{ _t( - "It looks like you don't have a recovery key or any other devices you can use to complete " + - "the setup of secure messaging. As a result, this device won't be able to access past 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 " + "secure messaging completely.", ) }

@@ -205,10 +205,10 @@ export default class SetupEncryptionBody extends React.Component return (

{ _t( - "Setup 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.", ) }

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

@@ -216,7 +216,7 @@ export default class SetupEncryptionBody extends React.Component { useRecoveryKeyButton }
- { _t("Forgotten or lost all setup methods? Reset secure messaging", 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 Security 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 Security 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 session.", + ) } + + + +
; + } - 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')); + this.setState({ recoveryKey, hasUnsavedRecoveryKey, 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 ( + + ); } } } From 5a5251ffa24a5d349d53cf2acab6beac4bec2a8d Mon Sep 17 00:00:00 2001 From: Hugh Nimmo-Smith Date: Fri, 15 Apr 2022 13:54:55 +0100 Subject: [PATCH 25/37] Session => Device --- src/SlashCommands.tsx | 8 +- .../security/CreateKeyBackupDialog.tsx | 2 +- .../security/CreateSecretStorageDialog.tsx | 2 +- .../security/NewRecoveryMethodDialog.tsx | 2 +- .../security/RecoveryMethodRemovedDialog.tsx | 4 +- src/components/structures/MatrixChat.tsx | 2 +- .../structures/auth/ForgotPassword.tsx | 4 +- src/components/structures/auth/SoftLogout.tsx | 6 +- .../views/dialogs/ConfirmWipeDeviceDialog.tsx | 4 +- .../views/dialogs/CryptoStoreTooNewDialog.tsx | 2 +- .../views/dialogs/IncomingSasDialog.tsx | 4 +- .../ManualDeviceKeyVerificationDialog.tsx | 14 +- .../dialogs/SessionRestoreErrorDialog.tsx | 8 +- .../views/dialogs/StorageEvictedDialog.tsx | 4 +- .../views/dialogs/UntrustedDeviceDialog.tsx | 8 +- .../security/AccessSecretStorageDialog.tsx | 2 +- .../views/right_panel/EncryptionPanel.tsx | 2 +- src/components/views/right_panel/UserInfo.tsx | 10 +- src/components/views/rooms/E2EIcon.tsx | 6 +- src/components/views/rooms/EventTile.tsx | 16 +- .../views/settings/ChangePassword.tsx | 2 +- .../views/settings/CrossSigningPanel.tsx | 4 +- .../views/settings/DevicesPanelEntry.tsx | 2 +- .../views/settings/E2eAdvancedPanel.tsx | 2 +- .../views/settings/Notifications.tsx | 4 +- .../views/settings/SecureBackupPanel.tsx | 32 ++-- .../tabs/user/AccountUserSettingsTab.tsx | 2 +- .../tabs/user/AppearanceUserSettingsTab.tsx | 2 +- src/i18n/strings/en_EN.json | 176 +++++++++--------- 29 files changed, 165 insertions(+), 171 deletions(-) diff --git a/src/SlashCommands.tsx b/src/SlashCommands.tsx index 75254afb3c0..f219865db7a 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 }) }

@@ -1054,7 +1054,7 @@ export const Commands = [ }), new Command({ command: 'discardsession', - description: _td('Forces the current outbound group session in an encrypted room to be discarded'), + description: _td('Forces the current outbound group device in an encrypted room to be discarded'), runFn: function(roomId) { try { MatrixClientPeg.get().forceDiscardSession(roomId); diff --git a/src/async-components/views/dialogs/security/CreateKeyBackupDialog.tsx b/src/async-components/views/dialogs/security/CreateKeyBackupDialog.tsx index 560eaadab1d..74a27f8cb71 100644 --- a/src/async-components/views/dialogs/security/CreateKeyBackupDialog.tsx +++ b/src/async-components/views/dialogs/security/CreateKeyBackupDialog.tsx @@ -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( - "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.", ) }

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 set up Secure Messages on " + - "this session which will re-encrypt this session's message " + + "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 2ea67451681..ce239212d33 100644 --- a/src/components/structures/MatrixChat.tsx +++ b/src/components/structures/MatrixChat.tsx @@ -1513,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/auth/ForgotPassword.tsx b/src/components/structures/auth/ForgotPassword.tsx index 58b0073c443..3e23ac93f75 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 room keys from another device before resetting your " + "password.", ) }
, diff --git a/src/components/structures/auth/SoftLogout.tsx b/src/components/structures/auth/SoftLogout.tsx index 7a366d71225..36f73f4402b 100644 --- a/src/components/structures/auth/SoftLogout.tsx +++ b/src/components/structures/auth/SoftLogout.tsx @@ -266,8 +266,8 @@ export default class SoftLogout extends React.Component { let introText = null; // null is translated to something area specific in this function if (this.state.keyBackupNeeded) { introText = _t( - "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."); } if (this.state.loginView === LoginView.Password) { @@ -340,7 +340,7 @@ export default class SoftLogout extends React.Component {

{ _t( "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 " + + "on this device. Clear it if you're finished using this device, or want to sign " + "in to another account.", ) }

diff --git a/src/components/views/dialogs/ConfirmWipeDeviceDialog.tsx b/src/components/views/dialogs/ConfirmWipeDeviceDialog.tsx index a61b47c04f3..08f4376c18c 100644 --- a/src/components/views/dialogs/ConfirmWipeDeviceDialog.tsx +++ b/src/components/views/dialogs/ConfirmWipeDeviceDialog.tsx @@ -39,12 +39,12 @@ export default class ConfirmWipeDeviceDialog extends React.Component { className='mx_ConfirmWipeDeviceDialog' hasCancel={true} onFinished={this.props.onFinished} - title={_t("Clear all data in this session?")} + title={_t("Clear all data on this device?")} >

{ _t( - "Clearing all data from this session is permanent. Encrypted messages will be lost " + + "Clearing all data from this device is permanent. Encrypted messages will be lost " + "unless their keys have been backed up.", ) }

diff --git a/src/components/views/dialogs/CryptoStoreTooNewDialog.tsx b/src/components/views/dialogs/CryptoStoreTooNewDialog.tsx index e8d267414a1..b9979ee5176 100644 --- a/src/components/views/dialogs/CryptoStoreTooNewDialog.tsx +++ b/src/components/views/dialogs/CryptoStoreTooNewDialog.tsx @@ -53,7 +53,7 @@ const CryptoStoreTooNewDialog: React.FC = (props: IProps) => { const description = _t( - "You've previously used a newer version of %(brand)s with this session. " + + "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.", { brand }, diff --git a/src/components/views/dialogs/IncomingSasDialog.tsx b/src/components/views/dialogs/IncomingSasDialog.tsx index 2b15749c8d6..8d005642336 100644 --- a/src/components/views/dialogs/IncomingSasDialog.tsx +++ b/src/components/views/dialogs/IncomingSasDialog.tsx @@ -185,8 +185,8 @@ export default class IncomingSasDialog extends React.Component {

{ _t( // NB. Below wording adjusted to singular 'session' until we have // cross-signing - "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.", ) }

, ]; 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

{ _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.") }

= (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 bbf8b678f2f..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"; } 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 0611645910e..155420d796a 100644 --- a/src/components/views/rooms/EventTile.tsx +++ b/src/components/views/rooms/EventTile.tsx @@ -1172,15 +1172,15 @@ export class UnwrappedEventTile extends React.Component {

    { this.state.previouslyRequestedKeys ? - _t('Your key share request has been sent - please check your other sessions ' + + _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 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('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 sessions do not have the key for this message you will not ' + + { _t('If your other devices do not have the key for this message you will not ' + 'be able to decrypt them.') }

    @@ -1188,7 +1188,7 @@ export class UnwrappedEventTile extends React.Component { const keyRequestInfoContent = this.state.previouslyRequestedKeys ? _t('Key request sent.') : _t( - 'Re-request encryption keys from your other sessions.', + 'Re-request encryption keys from your other devices.', {}, { 'requestLink': (sub) => @@ -1533,7 +1533,7 @@ function E2ePadlockUndecryptable(props) { function E2ePadlockUnverified(props) { return ( - + ); } @@ -1545,7 +1545,7 @@ function E2ePadlockUnencrypted(props) { function E2ePadlockUnknown(props) { return ( - + ); } diff --git a/src/components/views/settings/ChangePassword.tsx b/src/components/views/settings/ChangePassword.tsx index f736e5f6f51..afea9181c98 100644 --- a/src/components/views/settings/ChangePassword.tsx +++ b/src/components/views/settings/ChangePassword.tsx @@ -95,7 +95,7 @@ export default class ChangePassword extends React.Component { description:
    { _t( - 'Changing password will currently reset any end-to-end encryption keys on all sessions, ' + + 'Changing password will currently reset any end-to-end encryption keys on all devices, ' + 'making encrypted chat history unreadable, unless you first export your room keys ' + 'and re-import them afterwards. ' + 'In future this will be improved.', 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/DevicesPanelEntry.tsx b/src/components/views/settings/DevicesPanelEntry.tsx index 70459694eec..d0766ea9220 100644 --- a/src/components/views/settings/DevicesPanelEntry.tsx +++ b/src/components/views/settings/DevicesPanelEntry.tsx @@ -77,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(); diff --git a/src/components/views/settings/E2eAdvancedPanel.tsx b/src/components/views/settings/E2eAdvancedPanel.tsx index 6090ea648e1..c0588b581b6 100644 --- a/src/components/views/settings/E2eAdvancedPanel.tsx +++ b/src/components/views/settings/E2eAdvancedPanel.tsx @@ -42,7 +42,7 @@ const E2eAdvancedPanel = props => { level={SettingLevel.DEVICE} />

    { _t( - "Individually verify each session 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.", ) }
    : null; 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..6c6127789d0 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.") }

    @@ -443,7 +443,7 @@ export default class SecureBackupPanel extends React.PureComponent<{}, IState> {

    { _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 " + + "lose access to your devices. Your keys will be secured with a " + "unique Security Key.", ) }

    { statusDescription } diff --git a/src/components/views/settings/tabs/user/AccountUserSettingsTab.tsx b/src/components/views/settings/tabs/user/AccountUserSettingsTab.tsx index b80e7362c4c..3e396ec3c0c 100644 --- a/src/components/views/settings/tabs/user/AccountUserSettingsTab.tsx +++ b/src/components/views/settings/tabs/user/AccountUserSettingsTab.tsx @@ -151,7 +151,7 @@ export default class AccountUserSettingsTab 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 }) }
    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 encryption keys with your account data in case you lose access to your devices. 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 devices. Your keys will be secured with a unique Security Key.", "Backup key stored:": "Backup key stored:", "not stored": "not stored", "Backup key cached:": "Backup key cached:", @@ -1402,7 +1402,7 @@ "Check for update": "Check for update", "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...", @@ -1416,7 +1416,7 @@ "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 session.": "Appearance Settings only affect this %(brand)s session.", + "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", "%(brand)s version:": "%(brand)s version:", @@ -1508,9 +1508,6 @@ "Secure messages are protected using end-to-end encryption ": "Secure messages are protected using end-to-end encryption ", "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.", "Search": "Search", - "Message search": "Message search", - "Your server admin has disabled secure messaging by default in private rooms & Direct Messages.": "Your server admin has disabled secure messaging by default in private rooms & Direct Messages.", - "Encryption": "Encryption", "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.", @@ -1620,6 +1617,7 @@ "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", + "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", @@ -1648,29 +1646,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...", @@ -1950,7 +1948,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", @@ -1973,13 +1971,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", @@ -2427,8 +2422,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.", @@ -2464,7 +2459,7 @@ "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.", + "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.", @@ -2550,7 +2545,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...", @@ -2617,11 +2612,17 @@ "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 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", + "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 session.": "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 session.", + "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", @@ -2635,13 +2636,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", @@ -2715,10 +2716,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", @@ -2748,8 +2749,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", @@ -2759,10 +2760,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", @@ -2794,7 +2795,7 @@ "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.", @@ -2829,6 +2830,7 @@ "Warning: You should only set up key backup from a trusted computer.": "Warning: You should only set up key backup from a trusted computer.", "Access your secure message history and set up secure messaging by entering your Security Key.": "Access your secure message history and set up secure messaging by entering your Security Key.", "If you've forgotten your Security Key you can ": "If you've forgotten your Security Key you can ", + "Verify this session": "Verify this session", "Send custom account data event": "Send custom account data event", "Send custom room account data event": "Send custom room account data event", "Event Type": "Event Type", @@ -3011,7 +3013,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", @@ -3152,10 +3154,9 @@ "Unable to verify this device": "Unable to verify this device", "Secure messaging setup": "Secure messaging setup", "Device verified": "Device verified", - "Verify this device": "Verify this device", "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 room 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 room 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.", @@ -3226,13 +3227,13 @@ "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", @@ -3254,16 +3255,8 @@ "Go back to set it again.": "Go back to set it again.", "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", @@ -3280,7 +3273,7 @@ "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.", "Unable to query secret storage status": "Unable to query secret storage status", @@ -3307,12 +3300,12 @@ "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 set up 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 set up 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", @@ -3324,6 +3317,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", From d95de4f9212c8fe57c16d159de0ed2b1782df4c2 Mon Sep 17 00:00:00 2001 From: Hugh Nimmo-Smith Date: Fri, 15 Apr 2022 13:59:57 +0100 Subject: [PATCH 26/37] More 4S Security Key => Recovery key --- .../security/CreateKeyBackupDialog.tsx | 4 +-- .../security/CreateSecretStorageDialog.tsx | 6 ++-- src/components/views/dialogs/LogoutDialog.tsx | 10 +++---- .../security/AccessSecretStorageDialog.tsx | 12 ++++---- .../views/settings/SecureBackupPanel.tsx | 4 +-- src/i18n/strings/en_EN.json | 28 ++++++++++--------- 6 files changed, 33 insertions(+), 31 deletions(-) diff --git a/src/async-components/views/dialogs/security/CreateKeyBackupDialog.tsx b/src/async-components/views/dialogs/security/CreateKeyBackupDialog.tsx index 74a27f8cb71..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 } }, ); } diff --git a/src/async-components/views/dialogs/security/CreateSecretStorageDialog.tsx b/src/async-components/views/dialogs/security/CreateSecretStorageDialog.tsx index 482f2459067..2cb74d50065 100644 --- a/src/async-components/views/dialogs/security/CreateSecretStorageDialog.tsx +++ b/src/async-components/views/dialogs/security/CreateSecretStorageDialog.tsx @@ -501,9 +501,9 @@ export default class CreateSecretStorageDialog extends React.PureComponent
    - { _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.") }
    ); } diff --git a/src/components/views/dialogs/LogoutDialog.tsx b/src/components/views/dialogs/LogoutDialog.tsx index 6120137415c..48822b189ef 100644 --- a/src/components/views/dialogs/LogoutDialog.tsx +++ b/src/components/views/dialogs/LogoutDialog.tsx @@ -122,15 +122,15 @@ export default class LogoutDialog extends React.Component { private renderPhaseShowKey(): JSX.Element { return

    { _t( - "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 recovery key is a safety net - you can use it to restore " + + "access to your encrypted messages if you forget you lose access to your devices.", ) }

    { _t( "Keep a copy of it somewhere secure, like a password manager or even a safe.", ) }

    - { _t("Your Security Key") } + { _t("Your recovery key") }
    @@ -153,12 +153,12 @@ export default class LogoutDialog extends React.Component { let introText; if (this.state.copied) { introText = _t( - "Your Security Key has been copied 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 } }, ); } diff --git a/src/components/views/dialogs/security/AccessSecretStorageDialog.tsx b/src/components/views/dialogs/security/AccessSecretStorageDialog.tsx index 3185a2cdf5b..48f2a9c9207 100644 --- a/src/components/views/dialogs/security/AccessSecretStorageDialog.tsx +++ b/src/components/views/dialogs/security/AccessSecretStorageDialog.tsx @@ -265,11 +265,11 @@ export default class AccessSecretStorageDialog extends React.PureComponent

    { _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({ @@ -377,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.") }

    { return (

    { _t( - "Back up your encryption keys with your account data in case you " + + "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 " + - "unique Security Key.", + "recovery key.", ) }

    { statusDescription }
    diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 25e9febc948..bdb1b358c08 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -1345,7 +1345,7 @@ "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 devices. 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 devices. 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:", @@ -2612,11 +2612,11 @@ "Leave all rooms": "Leave all rooms", "Leave some rooms": "Leave some rooms", "Leave space": "Leave space", - "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.", + "Your recovery key is a safety net - you can use it to restore access to your encrypted 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 encrypted messages if you forget you lose access to your devices.", "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.", + "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", @@ -2790,17 +2790,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 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", @@ -3255,6 +3255,8 @@ "Go back to set it again.": "Go back to set it again.", "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.", + "Your Security Key": "Your Security Key", "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 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", @@ -3265,9 +3267,9 @@ "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", From da283d727ec5c86de6f3f28e8521dfb2c49483f4 Mon Sep 17 00:00:00 2001 From: Hugh Nimmo-Smith Date: Fri, 15 Apr 2022 14:50:11 +0100 Subject: [PATCH 27/37] Security key => Recovery key --- .../views/dialogs/security/CreateSecretStorageDialog.tsx | 4 ++-- src/i18n/strings/en_EN.json | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/async-components/views/dialogs/security/CreateSecretStorageDialog.tsx b/src/async-components/views/dialogs/security/CreateSecretStorageDialog.tsx index 2cb74d50065..dd45a1eabfb 100644 --- a/src/async-components/views/dialogs/security/CreateSecretStorageDialog.tsx +++ b/src/async-components/views/dialogs/security/CreateSecretStorageDialog.tsx @@ -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 Date: Mon, 18 Apr 2022 15:02:51 +0100 Subject: [PATCH 28/37] Trust level slider --- res/css/_components.scss | 1 + res/css/views/settings/_E2eTrustPanel.scss | 28 +++ .../views/elements/SettingsFlag.tsx | 20 +++ .../views/settings/E2eAdvancedPanel.tsx | 1 - .../views/settings/E2eTrustPanel.tsx | 162 ++++++++++++++++++ .../user/SecureMessagingUserSettingsTab.tsx | 55 ++---- src/settings/Settings.tsx | 12 +- 7 files changed, 237 insertions(+), 42 deletions(-) create mode 100644 res/css/views/settings/_E2eTrustPanel.scss create mode 100644 src/components/views/settings/E2eTrustPanel.tsx 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/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/settings/E2eAdvancedPanel.tsx b/src/components/views/settings/E2eAdvancedPanel.tsx index c0588b581b6..eec3495bf99 100644 --- a/src/components/views/settings/E2eAdvancedPanel.tsx +++ b/src/components/views/settings/E2eAdvancedPanel.tsx @@ -47,7 +47,6 @@ const E2eAdvancedPanel = props => { : null; return
    - { _t("Trust") } { manuallyVerifyAllSessions } { blacklistUnverifiedDevices }
    ; 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/tabs/user/SecureMessagingUserSettingsTab.tsx b/src/components/views/settings/tabs/user/SecureMessagingUserSettingsTab.tsx index e601cee948a..0bcf0e4593d 100644 --- a/src/components/views/settings/tabs/user/SecureMessagingUserSettingsTab.tsx +++ b/src/components/views/settings/tabs/user/SecureMessagingUserSettingsTab.tsx @@ -21,50 +21,29 @@ import { privateShouldBeEncrypted } from "../../../../../utils/rooms"; import SecureBackupPanel from "../../SecureBackupPanel"; import SettingsStore from "../../../../../settings/SettingsStore"; import { UIFeature } from "../../../../../settings/UIFeature"; -import E2eAdvancedPanel, { isE2eAdvancedPanelPossible } from "../../E2eAdvancedPanel"; +import E2eTrustPanel from "../../E2eTrustPanel"; import CryptographyPanel from "../../CryptographyPanel"; import E2eDevicesPanel from "../../E2eDevicesPanel"; import CrossSigningPanel from "../../CrossSigningPanel"; import EventIndexPanel from "../../EventIndexPanel"; -import { MatrixClientPeg } from '../../../../../MatrixClientPeg'; interface IProps { closeSettingsFn: () => void; } -interface IState { - canChangePassword: boolean; -} +interface IState {} export default class SecureMessagingUserSettingsTab extends React.Component { constructor(props: IProps) { super(props); - this.state = { - canChangePassword: false, - }; - } - - // 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 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({ canChangePassword }); + this.state = {}; } public render(): JSX.Element { const secureBackup = (
    - { _t("Secure Backup") } + { _t("Message key backup") }
    @@ -93,36 +72,32 @@ export default class SecureMessagingUserSettingsTab extends React.Component : null; // only show the section if there's something to show - if (e2ePanel) { - advancedSection = <> -
    { _t("Advanced") }
    + advancedSection = <> +
    + { _t("Advanced") }
    { secureBackup } - { e2ePanel } { crossSigning }
    - ; - } +
    + ; } return (
    { warning }
    { _t("Secure messaging") }
    -

    { _t("Secure messages are protected using end-to-end encryption ") }

    -
    { _t("Where you're signed in") }
    +

    { _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( - "Manage your signed-in devices below. " + - "A device's name is visible to people you communicate with.", - ) } -
    +
    { _t("Trust") }
    +
    + +
    { _t("Search") }
    diff --git a/src/settings/Settings.tsx b/src/settings/Settings.tsx index 3181e0e2d55..8ca5c50dc89 100644 --- a/src/settings/Settings.tsx +++ b/src/settings/Settings.tsx @@ -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. From 74765d07e281701b8ee8f22dea3358c560f42b5c Mon Sep 17 00:00:00 2001 From: Hugh Nimmo-Smith Date: Tue, 19 Apr 2022 10:32:33 +0100 Subject: [PATCH 29/37] Allow device renaming in Account panel --- src/components/views/settings/AccountDevicesPanel.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/views/settings/AccountDevicesPanel.tsx b/src/components/views/settings/AccountDevicesPanel.tsx index 192c20af916..b52ddb32c68 100644 --- a/src/components/views/settings/AccountDevicesPanel.tsx +++ b/src/components/views/settings/AccountDevicesPanel.tsx @@ -244,7 +244,7 @@ export default class AccountDevicesPanel extends React.Component onDeviceToggled={this.onDeviceSelectionToggled} canSignOut={true} canSelect={true} - canRename={false} + canRename={true} />; }; From 6406b9a95f2ca361c602e841bef6b64c5b5344bb Mon Sep 17 00:00:00 2001 From: Hugh Nimmo-Smith Date: Tue, 19 Apr 2022 10:33:08 +0100 Subject: [PATCH 30/37] Secure setup modal title --- src/components/views/dialogs/security/SetupEncryptionDialog.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/views/dialogs/security/SetupEncryptionDialog.tsx b/src/components/views/dialogs/security/SetupEncryptionDialog.tsx index 1a945405023..4ac0cab8d2e 100644 --- a/src/components/views/dialogs/security/SetupEncryptionDialog.tsx +++ b/src/components/views/dialogs/security/SetupEncryptionDialog.tsx @@ -61,7 +61,7 @@ export default class SetupEncryptionDialog extends React.Component ; From c2f404a29136990a7af6215ebcb7416c6908d352 Mon Sep 17 00:00:00 2001 From: Hugh Nimmo-Smith Date: Tue, 19 Apr 2022 10:35:41 +0100 Subject: [PATCH 31/37] Take account of whether user has other devices whilst logging out --- src/components/views/dialogs/LogoutDialog.tsx | 22 ++++++++++++++----- 1 file changed, 16 insertions(+), 6 deletions(-) diff --git a/src/components/views/dialogs/LogoutDialog.tsx b/src/components/views/dialogs/LogoutDialog.tsx index 48822b189ef..0a62c1628be 100644 --- a/src/components/views/dialogs/LogoutDialog.tsx +++ b/src/components/views/dialogs/LogoutDialog.tsx @@ -47,6 +47,7 @@ interface IState { downloaded: boolean; recoveryKey: string | null; hasUnsavedRecoveryKey: boolean; + deviceCount: number; } export default class LogoutDialog extends React.Component { @@ -66,6 +67,7 @@ export default class LogoutDialog extends React.Component { downloaded: false, recoveryKey: null, hasUnsavedRecoveryKey: false, + deviceCount: 0, }; } @@ -121,10 +123,17 @@ export default class LogoutDialog extends React.Component { private renderPhaseShowKey(): JSX.Element { return
    -

    { _t( - "Your recovery key is a safety net - you can use it to restore " + - "access to your encrypted messages if you forget you lose access to your devices.", - ) }

    +

    { this.state.deviceCount > 1 ? + _t( + "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.", + ) : + _t( + "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. ", + ) + }

    { _t( "Keep a copy of it somewhere secure, like a password manager or even a safe.", ) }

    @@ -181,7 +190,7 @@ export default class LogoutDialog extends React.Component { 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 session.", + "encrypted message history if you log out or use another device.", ) } { const recoveryKey = localStorage.getItem('mx_4s_key'); const hasUnsavedRecoveryKey = recoveryKey && !(await cli.getAccountDataFromServer('m.secret_storage.key.export')); - this.setState({ recoveryKey, hasUnsavedRecoveryKey, loading: false }); + const deviceCount = (await cli.getDevices()).devices.length; + this.setState({ recoveryKey, hasUnsavedRecoveryKey, deviceCount, loading: false }); } private onFinished = (confirmed: boolean): void => { From 8e2c3846845abe2e0fa7b7bc51e5a5c358c35971 Mon Sep 17 00:00:00 2001 From: Hugh Nimmo-Smith Date: Tue, 19 Apr 2022 10:35:52 +0100 Subject: [PATCH 32/37] Improve layout of secure messaging devices --- .../views/settings/E2eDevicesPanel.tsx | 194 ++++-------------- 1 file changed, 41 insertions(+), 153 deletions(-) diff --git a/src/components/views/settings/E2eDevicesPanel.tsx b/src/components/views/settings/E2eDevicesPanel.tsx index c7f7ce1e281..63182fd7484 100644 --- a/src/components/views/settings/E2eDevicesPanel.tsx +++ b/src/components/views/settings/E2eDevicesPanel.tsx @@ -22,9 +22,10 @@ import { CrossSigningInfo } from "matrix-js-sdk/src/crypto/CrossSigning"; import { MatrixClientPeg } from '../../../MatrixClientPeg'; import { _t } from '../../../languageHandler'; -import DevicesPanelEntry from "./DevicesPanelEntry"; import Spinner from "../elements/Spinner"; import AccessibleButton from "../elements/AccessibleButton"; +import Modal from '../../../Modal'; +import SetupEncryptionDialog from '../dialogs/security/SetupEncryptionDialog'; interface IProps { className?: string; @@ -124,80 +125,10 @@ export default class E2eDevicesPanel extends React.Component { } } - private onDeviceSelectionToggled = (device: IMyDevice): void => { - if (this.unmounted) { return; } - - const deviceId = device.device_id; - this.setState((state, props) => { - // Make a copy of the selected devices, then add or remove the device - const selectedDevices = state.selectedDevices.slice(); - - const i = selectedDevices.indexOf(deviceId); - if (i === -1) { - selectedDevices.push(deviceId); - } else { - selectedDevices.splice(i, 1); - } - - return { selectedDevices }; - }); - }; - - private selectAll = (devices: IMyDevice[]): void => { - this.setState((state, props) => { - const selectedDevices = state.selectedDevices.slice(); - - for (const device of devices) { - const deviceId = device.device_id; - if (!selectedDevices.includes(deviceId)) { - selectedDevices.push(deviceId); - } - } - - return { selectedDevices }; - }); - }; - - private deselectAll = (devices: IMyDevice[]): void => { - this.setState((state, props) => { - const selectedDevices = state.selectedDevices.slice(); - - for (const device of devices) { - const deviceId = device.device_id; - const i = selectedDevices.indexOf(deviceId); - if (i !== -1) { - selectedDevices.splice(i, 1); - } - } - - return { selectedDevices }; - }); - }; - - 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 ; - }; + private async setupThisDevice() { + const { finished } = Modal.createTrackedDialog("Verify session", "Verify session", SetupEncryptionDialog); + await finished; + } public render(): JSX.Element { const loadError = ( @@ -239,81 +170,6 @@ export default class E2eDevicesPanel extends React.Component { } } - const section = (trustIcon: JSX.Element, title: string, description: string, deviceList: IMyDevice[]): JSX.Element => { - if (deviceList.length === 0) { - return ; - } - - let selectButton: JSX.Element; - if (deviceList.length > 1) { - const anySelected = deviceList.some((device) => this.state.selectedDevices.includes(device.device_id)); - const buttonAction = anySelected ? - () => { this.deselectAll(deviceList); } : - () => { this.selectAll(deviceList); }; - const buttonText = anySelected ? _t("Deselect all") : _t("Select all"); - selectButton =
    - - { buttonText } - -
    ; - } - - return -
    -
    -
    - { trustIcon } -
    -
    - { title } -
    -

    { description }

    - { selectButton } -
    - { deviceList.map(this.renderDevice) } -
    ; - }; - - const verifiedDevicesSection = section( - , - _t("Set up for secure messaging"), - _t("These devices are set up for end-to-end encryption and are able to access your past secure messages. They will also show as trusted to others."), - verifiedDevices, - ); - - const unverifiedDevicesSection = section( - , - _t("Not set up for secure messaging"), - _t("These devices have not been set up for end-to-end encryption messages and will show as untrusted to others. You can set them up so that you"), - unverifiedDevices, - ); - - const nonCryptoDevicesSection = section( - , - _t("Devices without secure messaging support"), - _t("These devices do not support secure messaging."), - nonCryptoDevices, - ); - - const otherDevicesSection = (otherDevices.length > 0) ? - - { verifiedDevicesSection } - { unverifiedDevicesSection } - { nonCryptoDevicesSection } - : - -
    -
    - { _t("You aren't signed in to any other devices.") } -
    -
    ; - - const thisDeviceDescription = this.isDeviceVerified(myDevice) ? _t("Secure messaging is set up on this device."): _t("Secure messaging is not set up on this device."); - const classes = classNames(this.props.className, "mx_DevicesPanel"); return (
    @@ -322,9 +178,41 @@ export default class E2eDevicesPanel extends React.Component { { _t("This device") }
    -

    { thisDeviceDescription }

    - { this.renderDevice(myDevice) } - { otherDevicesSection } + { this.isDeviceVerified(myDevice) ? + +

    { _t("Secure messaging is set up on this device.") }

    + + { _t("Save recovery key") } + +
    + : + +

    { _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 { verifiedDevices.length } other device{ verifiedDevices.length > 1 ? 's' : '' } that .

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

    You have { 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 { 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.") }

    + }
    ); } From 738eb362b690c6b61a229cc9548b5f06d2094cfb Mon Sep 17 00:00:00 2001 From: Hugh Nimmo-Smith Date: Tue, 19 Apr 2022 11:55:30 +0100 Subject: [PATCH 33/37] More wording on saving recovery key --- .../views/settings/E2eDevicesPanel.tsx | 21 +++++++++++++++---- 1 file changed, 17 insertions(+), 4 deletions(-) diff --git a/src/components/views/settings/E2eDevicesPanel.tsx b/src/components/views/settings/E2eDevicesPanel.tsx index 63182fd7484..086e2fa9a30 100644 --- a/src/components/views/settings/E2eDevicesPanel.tsx +++ b/src/components/views/settings/E2eDevicesPanel.tsx @@ -130,6 +130,10 @@ export default class E2eDevicesPanel extends React.Component { await finished; } + private notImplemented() { + alert('Not implemented'); + } + public render(): JSX.Element { const loadError = (
    @@ -170,6 +174,8 @@ export default class E2eDevicesPanel extends React.Component { } } + const hasRecoveryKey = !!localStorage.getItem('mx_4s_key'); + const classes = classNames(this.props.className, "mx_DevicesPanel"); return (
    @@ -180,10 +186,17 @@ export default class E2eDevicesPanel extends React.Component {
    { this.isDeviceVerified(myDevice) ? -

    { _t("Secure messaging is set up on this device.") }

    - - { _t("Save recovery key") } - +

    + { _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") } + + }
    : From b5919fcd2959cfeff1a82e16ca4410bff9f39bf9 Mon Sep 17 00:00:00 2001 From: Hugh Nimmo-Smith Date: Wed, 4 May 2022 09:41:56 -0400 Subject: [PATCH 34/37] Allow removal of recovery key from device --- .../views/settings/E2eDevicesPanel.tsx | 38 +++++++++++++++---- 1 file changed, 30 insertions(+), 8 deletions(-) diff --git a/src/components/views/settings/E2eDevicesPanel.tsx b/src/components/views/settings/E2eDevicesPanel.tsx index 086e2fa9a30..376176528db 100644 --- a/src/components/views/settings/E2eDevicesPanel.tsx +++ b/src/components/views/settings/E2eDevicesPanel.tsx @@ -36,6 +36,7 @@ interface IState { crossSigningInfo?: CrossSigningInfo; deviceLoadError?: string; selectedDevices: string[]; + hasRecoveryKey: boolean; } export default class E2eDevicesPanel extends React.Component { @@ -46,6 +47,7 @@ export default class E2eDevicesPanel extends React.Component { this.state = { devices: [], selectedDevices: [], + hasRecoveryKey: !!localStorage.getItem('mx_4s_key'), }; this.loadDevices = this.loadDevices.bind(this); } @@ -131,7 +133,20 @@ export default class E2eDevicesPanel extends React.Component { } private notImplemented() { - alert('Not implemented'); + 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 { @@ -160,9 +175,9 @@ export default class E2eDevicesPanel extends React.Component { const otherDevices = devices.filter((device) => (device.device_id !== myDeviceId)); otherDevices.sort(this.deviceCompare); - const verifiedDevices = []; - const unverifiedDevices = []; - const nonCryptoDevices = []; + const verifiedDevices: IMyDevice[] = []; + const unverifiedDevices: IMyDevice[] = []; + const nonCryptoDevices: IMyDevice[] = []; for (const device of otherDevices) { const verified = this.isDeviceVerified(device); if (verified === true) { @@ -174,7 +189,7 @@ export default class E2eDevicesPanel extends React.Component { } } - const hasRecoveryKey = !!localStorage.getItem('mx_4s_key'); + const { hasRecoveryKey } = this.state; const classes = classNames(this.props.className, "mx_DevicesPanel"); return ( @@ -197,6 +212,13 @@ export default class E2eDevicesPanel extends React.Component { { _t("Save recovery key") } } + { !hasRecoveryKey ? null : +
    + + { _t("Delete recovery key from this device") } + +
    + }
    : @@ -214,13 +236,13 @@ export default class E2eDevicesPanel extends React.Component { { (otherDevices.length > 0) ? { verifiedDevices.length === 0 ? null : -

    You have { verifiedDevices.length } other device{ verifiedDevices.length > 1 ? 's' : '' } that .

    +

    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 { unverifiedDevices.length } other device{ unverifiedDevices.length > 1 ? 's' : '' } that { unverifiedDevices.length > 1 ? 'are' : 'is' } not setup for secure messaging.

    +

    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 { nonCryptoDevices.length } other device{ nonCryptoDevices.length > 1 ? 's' : '' } that do{ nonCryptoDevices.length > 1 ? '' : 'es' } not support secure messaging.

    +

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

    }
    : From b45c3369d6e776205db0ab9727614fd61c0343f6 Mon Sep 17 00:00:00 2001 From: Hugh Nimmo-Smith Date: Wed, 4 May 2022 09:42:11 -0400 Subject: [PATCH 35/37] More "setup"=>"set up" --- .../security/SetupEncryptionDialog.tsx | 2 +- src/i18n/strings/en_EN.json | 37 +++++++++++-------- src/toasts/SetupEncryptionToast.ts | 4 +- 3 files changed, 25 insertions(+), 18 deletions(-) diff --git a/src/components/views/dialogs/security/SetupEncryptionDialog.tsx b/src/components/views/dialogs/security/SetupEncryptionDialog.tsx index 4ac0cab8d2e..aff51268a20 100644 --- a/src/components/views/dialogs/security/SetupEncryptionDialog.tsx +++ b/src/components/views/dialogs/security/SetupEncryptionDialog.tsx @@ -61,7 +61,7 @@ export default class SetupEncryptionDialog extends React.Component ; diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 20841cd8bc8..9485bacc604 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -806,7 +806,7 @@ "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", - "Setup": "Setup", + "Set up": "Set up", "Upgrade": "Upgrade", "Safeguard against losing access to encrypted messages & data": "Safeguard against losing access to encrypted messages & data", "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.", @@ -950,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", @@ -1244,13 +1245,20 @@ "Rename": "Rename", "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.", - "These devices are set up for end-to-end encryption and are able to access your past secure messages. They will also show as trusted to others.": "These devices are set up for end-to-end encryption and are able to access your past secure messages. They will also show as trusted to others.", - "Not set up for secure messaging": "Not set up for secure messaging", - "These devices have not been set up for end-to-end encryption messages and will show as untrusted to others. You can set them up so that you": "These devices have not been set up for end-to-end encryption messages and will show as untrusted to others. You can set them up so that you", - "Devices without secure messaging support": "Devices without secure messaging support", - "These devices do not support secure messaging.": "These devices do not support secure messaging.", "Secure messaging is set up on this device.": "Secure messaging is set up on this device.", - "Secure messaging is not set up on this device.": "Secure messaging is not 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", @@ -1342,7 +1350,6 @@ "Algorithm:": "Algorithm:", "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 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.", @@ -1502,11 +1509,11 @@ "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", - "Secure Backup": "Secure Backup", + "Message key backup": "Message key backup", "Cross-signing": "Cross-signing", "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 ": "Secure messages are protected using end-to-end encryption ", - "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.", + "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", @@ -2612,7 +2619,8 @@ "Leave all rooms": "Leave all rooms", "Leave some rooms": "Leave some rooms", "Leave space": "Leave space", - "Your recovery key is a safety net - you can use it to restore access to your encrypted 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 encrypted 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 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:", @@ -2620,7 +2628,7 @@ "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 session.": "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 session.", + "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?", @@ -2830,7 +2838,6 @@ "Warning: You should only set up key backup from a trusted computer.": "Warning: You should only set up key backup from a trusted computer.", "Access your secure message history and set up secure messaging by entering your Security Key.": "Access your secure message history and set up secure messaging by entering your Security Key.", "If you've forgotten your Security Key you can ": "If you've forgotten your Security Key you can ", - "Verify this session": "Verify this session", "Send custom account data event": "Send custom account data event", "Send custom room account data event": "Send custom room account data event", "Event Type": "Event Type", diff --git a/src/toasts/SetupEncryptionToast.ts b/src/toasts/SetupEncryptionToast.ts index 078c376cf19..922771aeacb 100644 --- a/src/toasts/SetupEncryptionToast.ts +++ b/src/toasts/SetupEncryptionToast.ts @@ -50,11 +50,11 @@ const getIcon = (kind: Kind) => { const getSetupCaption = (kind: Kind) => { switch (kind) { case Kind.SET_UP_ENCRYPTION: - return _t("Setup"); + return _t("Set up"); case Kind.UPGRADE_ENCRYPTION: return _t("Upgrade"); case Kind.VERIFY_THIS_SESSION: - return _t("Setup"); + return _t("Set up"); } }; From 54da80acf33b89aff6a26d987ddead66e1ef441a Mon Sep 17 00:00:00 2001 From: Hugh Nimmo-Smith Date: Wed, 4 May 2022 09:59:24 -0400 Subject: [PATCH 36/37] "room key" => "message key" --- .../views/dialogs/security/ImportE2eKeysDialog.tsx | 4 ++-- src/components/structures/auth/ForgotPassword.tsx | 2 +- .../views/dialogs/CryptoStoreTooNewDialog.tsx | 2 +- src/components/views/settings/ChangePassword.tsx | 4 ++-- src/components/views/settings/CryptographyPanel.tsx | 4 ++-- src/i18n/strings/en_EN.json | 13 ++++++------- 6 files changed, 14 insertions(+), 15 deletions(-) diff --git a/src/async-components/views/dialogs/security/ImportE2eKeysDialog.tsx b/src/async-components/views/dialogs/security/ImportE2eKeysDialog.tsx index 65bbe0a70ef..9d351c80de9 100644 --- a/src/async-components/views/dialogs/security/ImportE2eKeysDialog.tsx +++ b/src/async-components/views/dialogs/security/ImportE2eKeysDialog.tsx @@ -122,13 +122,13 @@ export default class ImportE2eKeysDialog extends React.Component 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/components/structures/auth/ForgotPassword.tsx b/src/components/structures/auth/ForgotPassword.tsx index 3e23ac93f75..4d04cdf6b21 100644 --- a/src/components/structures/auth/ForgotPassword.tsx +++ b/src/components/structures/auth/ForgotPassword.tsx @@ -181,7 +181,7 @@ export default class ForgotPassword extends React.Component { { _t( "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 room keys from another device before resetting your " + + "Key Backup or export your message keys from another device before resetting your " + "password.", ) }

    , diff --git a/src/components/views/dialogs/CryptoStoreTooNewDialog.tsx b/src/components/views/dialogs/CryptoStoreTooNewDialog.tsx index b9979ee5176..62c74c87ab1 100644 --- a/src/components/views/dialogs/CryptoStoreTooNewDialog.tsx +++ b/src/components/views/dialogs/CryptoStoreTooNewDialog.tsx @@ -35,7 +35,7 @@ const CryptoStoreTooNewDialog: React.FC = (props: IProps) => { Modal.createTrackedDialog('Logout e2e db too new', '', QuestionDialog, { title: _t("Sign out"), description: _t( - "To avoid losing your chat history, you must export your room keys " + + "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", { brand }, diff --git a/src/components/views/settings/ChangePassword.tsx b/src/components/views/settings/ChangePassword.tsx index afea9181c98..17e4b7f34b0 100644 --- a/src/components/views/settings/ChangePassword.tsx +++ b/src/components/views/settings/ChangePassword.tsx @@ -96,7 +96,7 @@ export default class ChangePassword extends React.Component {
    { _t( 'Changing password will currently reset any end-to-end encryption keys on all devices, ' + - 'making encrypted chat history unreadable, unless you first export your room keys ' + + '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/CryptographyPanel.tsx b/src/components/views/settings/CryptographyPanel.tsx index 77450d38af5..6591e6d4b5e 100644 --- a/src/components/views/settings/CryptographyPanel.tsx +++ b/src/components/views/settings/CryptographyPanel.tsx @@ -48,10 +48,10 @@ 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") }
    ); diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 9485bacc604..bca91da9b5f 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -1202,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 devices, 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 devices, 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?", @@ -1234,7 +1234,7 @@ "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:", @@ -2465,7 +2465,7 @@ "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", + "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", @@ -3163,7 +3163,7 @@ "Device verified": "Device verified", "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 devices, making encrypted chat history unreadable. Set up Key Backup or export your room 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 room 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.": "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.", @@ -3301,8 +3301,7 @@ "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", From 27f6e20cba2ae9ebf4f9a9165d3c28595ada8cd3 Mon Sep 17 00:00:00 2001 From: Hugh Nimmo-Smith Date: Wed, 4 May 2022 12:30:23 -0400 Subject: [PATCH 37/37] Update src/SlashCommands.tsx Co-authored-by: Catalan Lover <48515417+FSG-Cat@users.noreply.github.com> --- src/SlashCommands.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/SlashCommands.tsx b/src/SlashCommands.tsx index f219865db7a..db92951e237 100644 --- a/src/SlashCommands.tsx +++ b/src/SlashCommands.tsx @@ -1054,7 +1054,7 @@ export const Commands = [ }), new Command({ command: 'discardsession', - description: _td('Forces the current outbound group device in an encrypted room to be discarded'), + description: _td('Forces the current outbound group session in an encrypted room to be discarded'), runFn: function(roomId) { try { MatrixClientPeg.get().forceDiscardSession(roomId);