"Verify this device" redesign (#30596)
* add variant of ResetIdentityBody for when the user has no verif. methods * no longer distinguish between the using having a passphrase or not * use vertical stack of buttons via EncryptionCard and update wording * swap logic order to match rendering order * use the same dialog when no verification options available * make it agree with the design more * allow signing out on initial login * apply styling changes and remove duplicate elements * fix and add tests * add missing snapshot * Apply suggestions from code review Co-authored-by: Richard van der Hoff <1389908+richvdh@users.noreply.github.com> * use a boolean property to disable blurring instead of adding a class * change string identifiers * apply changes from review -- simplify logic * change class name to avoid confusion --------- Co-authored-by: Richard van der Hoff <1389908+richvdh@users.noreply.github.com>
This commit is contained in:
@@ -7,6 +7,7 @@ Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
import React from "react";
|
||||
import { Glass } from "@vector-im/compound-web";
|
||||
|
||||
import { _t } from "../../../languageHandler";
|
||||
import { SetupEncryptionStore, Phase } from "../../../stores/SetupEncryptionStore";
|
||||
@@ -22,15 +23,17 @@ interface IProps {
|
||||
|
||||
interface IState {
|
||||
phase?: Phase;
|
||||
lostKeys: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Prompts the user to verify their device when they first log in.
|
||||
*/
|
||||
export default class CompleteSecurity extends React.Component<IProps, IState> {
|
||||
public constructor(props: IProps) {
|
||||
super(props);
|
||||
const store = SetupEncryptionStore.sharedInstance();
|
||||
store.start();
|
||||
this.state = { phase: store.phase, lostKeys: store.lostKeys() };
|
||||
this.state = { phase: store.phase };
|
||||
}
|
||||
|
||||
public componentDidMount(): void {
|
||||
@@ -40,7 +43,7 @@ export default class CompleteSecurity extends React.Component<IProps, IState> {
|
||||
|
||||
private onStoreUpdate = (): void => {
|
||||
const store = SetupEncryptionStore.sharedInstance();
|
||||
this.setState({ phase: store.phase, lostKeys: store.lostKeys() });
|
||||
this.setState({ phase: store.phase });
|
||||
};
|
||||
|
||||
private onSkipClick = (): void => {
|
||||
@@ -55,20 +58,14 @@ export default class CompleteSecurity extends React.Component<IProps, IState> {
|
||||
}
|
||||
|
||||
public render(): React.ReactNode {
|
||||
const { phase, lostKeys } = this.state;
|
||||
const { phase } = this.state;
|
||||
let icon;
|
||||
let title;
|
||||
|
||||
if (phase === Phase.Loading) {
|
||||
return null;
|
||||
} else if (phase === Phase.Intro) {
|
||||
if (lostKeys) {
|
||||
icon = <span className="mx_CompleteSecurity_headerIcon mx_E2EIcon_warning" />;
|
||||
title = _t("encryption|verification|after_new_login|unable_to_verify");
|
||||
} else {
|
||||
icon = <span className="mx_CompleteSecurity_headerIcon mx_E2EIcon_warning" />;
|
||||
title = _t("encryption|verification|after_new_login|verify_this_device");
|
||||
}
|
||||
// We don't specify an icon nor title since `SetupEncryptionBody` provides its own
|
||||
} else if (phase === Phase.Done) {
|
||||
icon = <span className="mx_CompleteSecurity_headerIcon mx_E2EIcon_verified" />;
|
||||
title = _t("encryption|verification|after_new_login|device_verified");
|
||||
@@ -98,17 +95,19 @@ export default class CompleteSecurity extends React.Component<IProps, IState> {
|
||||
}
|
||||
|
||||
return (
|
||||
<AuthPage>
|
||||
<CompleteSecurityBody>
|
||||
<h1 className="mx_CompleteSecurity_header">
|
||||
{icon}
|
||||
{title}
|
||||
{skipButton}
|
||||
</h1>
|
||||
<div className="mx_CompleteSecurity_body">
|
||||
<SetupEncryptionBody onFinished={this.props.onFinished} />
|
||||
</div>
|
||||
</CompleteSecurityBody>
|
||||
<AuthPage addBlur={false}>
|
||||
<Glass className="mx_Dialog_border">
|
||||
<CompleteSecurityBody>
|
||||
<h1 className="mx_CompleteSecurity_header">
|
||||
{icon}
|
||||
{title}
|
||||
{skipButton}
|
||||
</h1>
|
||||
<div className="mx_CompleteSecurity_body">
|
||||
<SetupEncryptionBody onFinished={this.props.onFinished} allowLogout={true} />
|
||||
</div>
|
||||
</CompleteSecurityBody>
|
||||
</Glass>
|
||||
</AuthPage>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -9,7 +9,9 @@ Please see LICENSE files in the repository root for full details.
|
||||
import React, { type JSX } from "react";
|
||||
import { type KeyBackupInfo, type VerificationRequest } from "matrix-js-sdk/src/crypto-api";
|
||||
import { logger } from "matrix-js-sdk/src/logger";
|
||||
import { type SecretStorageKeyDescription } from "matrix-js-sdk/src/secret-storage";
|
||||
import DevicesIcon from "@vector-im/compound-design-tokens/assets/web/icons/devices";
|
||||
import LockIcon from "@vector-im/compound-design-tokens/assets/web/icons/lock-solid";
|
||||
import { Button } from "@vector-im/compound-web";
|
||||
|
||||
import { _t } from "../../../languageHandler";
|
||||
import { MatrixClientPeg } from "../../../MatrixClientPeg";
|
||||
@@ -17,25 +19,38 @@ import Modal from "../../../Modal";
|
||||
import VerificationRequestDialog from "../../views/dialogs/VerificationRequestDialog";
|
||||
import { SetupEncryptionStore, Phase } from "../../../stores/SetupEncryptionStore";
|
||||
import EncryptionPanel from "../../views/right_panel/EncryptionPanel";
|
||||
import AccessibleButton, { type ButtonEvent } from "../../views/elements/AccessibleButton";
|
||||
import AccessibleButton from "../../views/elements/AccessibleButton";
|
||||
import Spinner from "../../views/elements/Spinner";
|
||||
import { ResetIdentityDialog } from "../../views/dialogs/ResetIdentityDialog";
|
||||
|
||||
function keyHasPassphrase(keyInfo: SecretStorageKeyDescription): boolean {
|
||||
return Boolean(keyInfo.passphrase && keyInfo.passphrase.salt && keyInfo.passphrase.iterations);
|
||||
}
|
||||
import { EncryptionCard } from "../../views/settings/encryption/EncryptionCard";
|
||||
import { EncryptionCardButtons } from "../../views/settings/encryption/EncryptionCardButtons";
|
||||
import { EncryptionCardEmphasisedContent } from "../../views/settings/encryption/EncryptionCardEmphasisedContent";
|
||||
import ExternalLink from "../../views/elements/ExternalLink";
|
||||
import dispatcher from "../../../dispatcher/dispatcher";
|
||||
|
||||
interface IProps {
|
||||
onFinished: () => void;
|
||||
/**
|
||||
* Offer the user an option to log out, instead of setting up encryption.
|
||||
*
|
||||
* This is used when this component is shown when the user is initially
|
||||
* prompted to set up encryption, before the user is shown the main chat
|
||||
* interface.
|
||||
*
|
||||
* Defaults to `false` if omitted.
|
||||
*/
|
||||
allowLogout?: boolean;
|
||||
}
|
||||
|
||||
interface IState {
|
||||
phase?: Phase;
|
||||
verificationRequest: VerificationRequest | null;
|
||||
backupInfo: KeyBackupInfo | null;
|
||||
lostKeys: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Component to set up encryption by verifying the current device.
|
||||
*/
|
||||
export default class SetupEncryptionBody extends React.Component<IProps, IState> {
|
||||
public constructor(props: IProps) {
|
||||
super(props);
|
||||
@@ -48,7 +63,6 @@ export default class SetupEncryptionBody extends React.Component<IProps, IState>
|
||||
// Because of the latter, it lives in the state.
|
||||
verificationRequest: store.verificationRequest,
|
||||
backupInfo: store.backupInfo,
|
||||
lostKeys: store.lostKeys(),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -67,7 +81,6 @@ export default class SetupEncryptionBody extends React.Component<IProps, IState>
|
||||
phase: store.phase,
|
||||
verificationRequest: store.verificationRequest,
|
||||
backupInfo: store.backupInfo,
|
||||
lostKeys: store.lostKeys(),
|
||||
});
|
||||
};
|
||||
|
||||
@@ -112,8 +125,8 @@ export default class SetupEncryptionBody extends React.Component<IProps, IState>
|
||||
store.returnAfterSkip();
|
||||
};
|
||||
|
||||
private onResetClick = (ev: ButtonEvent): void => {
|
||||
ev.preventDefault();
|
||||
private onCantConfirmClick = (): void => {
|
||||
const store = SetupEncryptionStore.sharedInstance();
|
||||
Modal.createDialog(ResetIdentityDialog, {
|
||||
onReset: () => {
|
||||
// The user completed the reset process - close this dialog
|
||||
@@ -121,10 +134,14 @@ export default class SetupEncryptionBody extends React.Component<IProps, IState>
|
||||
const store = SetupEncryptionStore.sharedInstance();
|
||||
store.done();
|
||||
},
|
||||
variant: "confirm",
|
||||
variant: store.lostKeys() ? "no_verification_method" : "confirm",
|
||||
});
|
||||
};
|
||||
|
||||
private onSignOutClick = (): void => {
|
||||
dispatcher.dispatch({ action: "logout" });
|
||||
};
|
||||
|
||||
private onDoneClick = (): void => {
|
||||
const store = SetupEncryptionStore.sharedInstance();
|
||||
store.done();
|
||||
@@ -136,7 +153,7 @@ export default class SetupEncryptionBody extends React.Component<IProps, IState>
|
||||
|
||||
public render(): React.ReactNode {
|
||||
const cli = MatrixClientPeg.safeGet();
|
||||
const { phase, lostKeys } = this.state;
|
||||
const { phase } = this.state;
|
||||
|
||||
if (this.state.verificationRequest && cli.getUser(this.state.verificationRequest.otherUserId)) {
|
||||
return (
|
||||
@@ -149,69 +166,59 @@ export default class SetupEncryptionBody extends React.Component<IProps, IState>
|
||||
/>
|
||||
);
|
||||
} else if (phase === Phase.Intro) {
|
||||
if (lostKeys) {
|
||||
return (
|
||||
<div>
|
||||
<p>{_t("encryption|verification|no_key_or_device")}</p>
|
||||
const store = SetupEncryptionStore.sharedInstance();
|
||||
|
||||
<div className="mx_CompleteSecurity_actionRow">
|
||||
<AccessibleButton kind="primary" onClick={this.onResetClick}>
|
||||
{_t("encryption|verification|reset_proceed_prompt")}
|
||||
</AccessibleButton>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
} else {
|
||||
const store = SetupEncryptionStore.sharedInstance();
|
||||
let recoveryKeyPrompt;
|
||||
if (store.keyInfo && keyHasPassphrase(store.keyInfo)) {
|
||||
recoveryKeyPrompt = _t("encryption|verification|verify_using_key_or_phrase");
|
||||
} else if (store.keyInfo) {
|
||||
recoveryKeyPrompt = _t("encryption|verification|verify_using_key");
|
||||
}
|
||||
|
||||
let useRecoveryKeyButton;
|
||||
if (recoveryKeyPrompt) {
|
||||
useRecoveryKeyButton = (
|
||||
<AccessibleButton kind="primary" onClick={this.onUsePassphraseClick}>
|
||||
{recoveryKeyPrompt}
|
||||
</AccessibleButton>
|
||||
);
|
||||
}
|
||||
|
||||
let verifyButton;
|
||||
if (store.hasDevicesToVerifyAgainst) {
|
||||
verifyButton = (
|
||||
<AccessibleButton kind="primary" onClick={this.onVerifyClick}>
|
||||
{_t("encryption|verification|verify_using_device")}
|
||||
</AccessibleButton>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<p>{_t("encryption|verification|verification_description")}</p>
|
||||
|
||||
<div className="mx_CompleteSecurity_actionRow">
|
||||
{verifyButton}
|
||||
{useRecoveryKeyButton}
|
||||
</div>
|
||||
<div className="mx_SetupEncryptionBody_reset">
|
||||
{_t("encryption|reset_all_button", undefined, {
|
||||
a: (sub) => (
|
||||
<AccessibleButton
|
||||
kind="link_inline"
|
||||
className="mx_SetupEncryptionBody_reset_link"
|
||||
onClick={this.onResetClick}
|
||||
>
|
||||
{sub}
|
||||
</AccessibleButton>
|
||||
),
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
let verifyButton;
|
||||
if (store.hasDevicesToVerifyAgainst) {
|
||||
verifyButton = (
|
||||
<Button kind="primary" onClick={this.onVerifyClick}>
|
||||
<DevicesIcon /> {_t("encryption|verification|use_another_device")}
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
|
||||
let useRecoveryKeyButton;
|
||||
if (store.keyInfo) {
|
||||
useRecoveryKeyButton = (
|
||||
<Button kind="primary" onClick={this.onUsePassphraseClick}>
|
||||
{_t("encryption|verification|use_recovery_key")}
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
|
||||
let signOutButton;
|
||||
if (this.props.allowLogout) {
|
||||
signOutButton = (
|
||||
<Button kind="tertiary" onClick={this.onSignOutClick}>
|
||||
{_t("action|sign_out")}
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<EncryptionCard
|
||||
title={_t("encryption|verification|confirm_identity_title")}
|
||||
Icon={LockIcon}
|
||||
className="mx_EncryptionCard_noBorder mx_SetupEncryptionBody"
|
||||
>
|
||||
<EncryptionCardEmphasisedContent>
|
||||
<span>{_t("encryption|verification|confirm_identity_description")}</span>
|
||||
<span>
|
||||
<ExternalLink href="https://element.io/help#encryption-device-verification">
|
||||
{_t("action|learn_more")}
|
||||
</ExternalLink>
|
||||
</span>
|
||||
</EncryptionCardEmphasisedContent>
|
||||
<EncryptionCardButtons>
|
||||
{verifyButton}
|
||||
{useRecoveryKeyButton}
|
||||
<Button kind="secondary" onClick={this.onCantConfirmClick}>
|
||||
{_t("encryption|verification|cant_confirm")}
|
||||
</Button>
|
||||
{signOutButton}
|
||||
</EncryptionCardButtons>
|
||||
</EncryptionCard>
|
||||
);
|
||||
} else if (phase === Phase.Done) {
|
||||
let message: JSX.Element;
|
||||
if (this.state.backupInfo) {
|
||||
|
||||
@@ -8,11 +8,22 @@ Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
import React from "react";
|
||||
import classNames from "classnames";
|
||||
|
||||
import SdkConfig from "../../../SdkConfig";
|
||||
import AuthFooter from "./AuthFooter";
|
||||
|
||||
export default class AuthPage extends React.PureComponent<React.PropsWithChildren> {
|
||||
interface IProps {
|
||||
/**
|
||||
* Whether to add a blurred shadow around the modal.
|
||||
*
|
||||
* If the modal component provides its own shadow or blurring, this can be
|
||||
* disabled. Defaults to `true`.
|
||||
*/
|
||||
addBlur?: boolean;
|
||||
}
|
||||
|
||||
export default class AuthPage extends React.PureComponent<React.PropsWithChildren<IProps>> {
|
||||
private static welcomeBackgroundUrl?: string;
|
||||
|
||||
// cache the url as a static to prevent it changing without refreshing
|
||||
@@ -58,14 +69,26 @@ export default class AuthPage extends React.PureComponent<React.PropsWithChildre
|
||||
const modalContentStyle: React.CSSProperties = {
|
||||
display: "flex",
|
||||
zIndex: 1,
|
||||
background: "rgba(255, 255, 255, 0.59)",
|
||||
borderRadius: "8px",
|
||||
};
|
||||
|
||||
let modalBlur;
|
||||
if (this.props.addBlur !== false) {
|
||||
// Blur out the background: add a `div` which covers the content behind the modal,
|
||||
// and blurs it out, and make the modal's background semitransparent.
|
||||
modalBlur = <div className="mx_AuthPage_modalBlur" style={blurStyle} />;
|
||||
modalContentStyle.background = "rgba(255, 255, 255, 0.59)";
|
||||
}
|
||||
|
||||
const modalClasses = classNames({
|
||||
mx_AuthPage_modal: true,
|
||||
mx_AuthPage_modal_withBlur: this.props.addBlur !== false,
|
||||
});
|
||||
|
||||
return (
|
||||
<div className="mx_AuthPage" style={pageStyle}>
|
||||
<div className="mx_AuthPage_modal" style={modalStyle}>
|
||||
<div className="mx_AuthPage_modalBlur" style={blurStyle} />
|
||||
<div className={modalClasses} style={modalStyle}>
|
||||
{modalBlur}
|
||||
<div className="mx_AuthPage_modalContent" style={modalContentStyle}>
|
||||
{this.props.children}
|
||||
</div>
|
||||
|
||||
@@ -10,55 +10,19 @@ import React from "react";
|
||||
|
||||
import SetupEncryptionBody from "../../../structures/auth/SetupEncryptionBody";
|
||||
import BaseDialog from "../BaseDialog";
|
||||
import { _t } from "../../../../languageHandler";
|
||||
import { SetupEncryptionStore, Phase } from "../../../../stores/SetupEncryptionStore";
|
||||
|
||||
function iconFromPhase(phase?: Phase): string {
|
||||
if (phase === Phase.Done) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
||||
return require("../../../../../res/img/e2e/verified-deprecated.svg").default;
|
||||
} else {
|
||||
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
||||
return require("../../../../../res/img/e2e/warning-deprecated.svg").default;
|
||||
}
|
||||
}
|
||||
|
||||
interface IProps {
|
||||
onFinished(): void;
|
||||
}
|
||||
interface IState {
|
||||
icon: string;
|
||||
}
|
||||
|
||||
export default class SetupEncryptionDialog extends React.Component<IProps, IState> {
|
||||
private store: SetupEncryptionStore;
|
||||
|
||||
export default class SetupEncryptionDialog extends React.Component<IProps> {
|
||||
public constructor(props: IProps) {
|
||||
super(props);
|
||||
|
||||
this.store = SetupEncryptionStore.sharedInstance();
|
||||
this.state = { icon: iconFromPhase(this.store.phase) };
|
||||
}
|
||||
|
||||
public componentDidMount(): void {
|
||||
this.store.on("update", this.onStoreUpdate);
|
||||
}
|
||||
|
||||
public componentWillUnmount(): void {
|
||||
this.store.removeListener("update", this.onStoreUpdate);
|
||||
}
|
||||
|
||||
private onStoreUpdate = (): void => {
|
||||
this.setState({ icon: iconFromPhase(this.store.phase) });
|
||||
};
|
||||
|
||||
public render(): React.ReactNode {
|
||||
return (
|
||||
<BaseDialog
|
||||
headerImage={this.state.icon}
|
||||
onFinished={this.props.onFinished}
|
||||
title={_t("encryption|verify_toast_title")}
|
||||
>
|
||||
<BaseDialog onFinished={this.props.onFinished} fixedWidth={false}>
|
||||
<SetupEncryptionBody onFinished={this.props.onFinished} />
|
||||
</BaseDialog>
|
||||
);
|
||||
|
||||
@@ -48,8 +48,12 @@ interface ResetIdentityBodyProps {
|
||||
* "forgot" is shown when the user chose 'Forgot recovery key?' during `SetupEncryptionToast`.
|
||||
*
|
||||
* "confirm" is shown when the user chose 'Reset all' during `SetupEncryptionBody`.
|
||||
*
|
||||
* "no_verification_method" is shown when the device is unverified and has no way of
|
||||
* obtaining the existing keys, and hence the identity needs to be reset to have
|
||||
* a cross-signed device.
|
||||
*/
|
||||
export type ResetIdentityBodyVariant = "compromised" | "forgot" | "sync_failed" | "confirm";
|
||||
export type ResetIdentityBodyVariant = "compromised" | "forgot" | "sync_failed" | "confirm" | "no_verification_method";
|
||||
|
||||
/**
|
||||
* User interface component allowing the user to reset their cryptographic identity.
|
||||
@@ -124,5 +128,7 @@ function titleForVariant(variant: ResetIdentityBodyVariant): string {
|
||||
return _t("settings|encryption|advanced|breadcrumb_title_sync_failed");
|
||||
case "forgot":
|
||||
return _t("settings|encryption|advanced|breadcrumb_title_forgot");
|
||||
case "no_verification_method":
|
||||
return _t("settings|encryption|advanced|breadcrumb_title_cant_confirm");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -969,7 +969,6 @@
|
||||
"title": "Recovery Method Removed",
|
||||
"warning": "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."
|
||||
},
|
||||
"reset_all_button": "Forgotten or lost all recovery methods? <a>Reset all</a>",
|
||||
"set_up_recovery": "Set up recovery",
|
||||
"set_up_recovery_toast_description": "Generate a recovery key that can be used to restore your encrypted message history in case you lose access to your devices.",
|
||||
"set_up_toast_title": "Set up Secure Backup",
|
||||
@@ -992,16 +991,18 @@
|
||||
"after_new_login": {
|
||||
"device_verified": "Device verified",
|
||||
"skip_verification": "Skip verification for now",
|
||||
"unable_to_verify": "Unable to verify this device",
|
||||
"verify_this_device": "Verify this device"
|
||||
},
|
||||
"cancelled": "You cancelled verification.",
|
||||
"cancelled_self": "You cancelled verification on your other device.",
|
||||
"cancelled_user": "%(displayName)s cancelled verification.",
|
||||
"cancelling": "Cancelling…",
|
||||
"cant_confirm": "Can't confirm?",
|
||||
"complete_action": "Got It",
|
||||
"complete_description": "You've successfully verified this user.",
|
||||
"complete_title": "Verified!",
|
||||
"confirm_identity_description": "Verify this device to set up secure messaging",
|
||||
"confirm_identity_title": "Confirm your identity",
|
||||
"error_starting_description": "We were unable to start a chat with the other user.",
|
||||
"error_starting_title": "Error starting verification",
|
||||
"explainer": "Secure messages with this user are end-to-end encrypted and not able to be read by third parties.",
|
||||
@@ -1027,7 +1028,6 @@
|
||||
"text": "Supply the ID and fingerprint of one of your own devices to verify it. NOTE this allows the other device to send and receive messages as you. IF SOMEONE TOLD YOU TO PASTE SOMETHING HERE, IT IS LIKELY YOU ARE BEING SCAMMED!",
|
||||
"wrong_fingerprint": "Unable to verify device '%(deviceId)s' - the supplied fingerprint '%(fingerprint)s' does not match the device fingerprint, '%(fprint)s'"
|
||||
},
|
||||
"no_key_or_device": "It looks like you don't have a Recovery 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 cryptographic identity.",
|
||||
"no_support_qr_emoji": "The device you are trying to verify doesn't support scanning a QR code or emoji verification, which is what %(brand)s supports. Try with a different client.",
|
||||
"other_party_cancelled": "The other party cancelled the verification.",
|
||||
"prompt_encrypted": "Verify all users in a room to ensure it's secure.",
|
||||
@@ -1043,7 +1043,6 @@
|
||||
"request_toast_accept_user": "Verify User",
|
||||
"request_toast_decline_counter": "Ignore (%(counter)s)",
|
||||
"request_toast_detail": "%(deviceId)s from %(ip)s",
|
||||
"reset_proceed_prompt": "Proceed with reset",
|
||||
"sas_caption_self": "Verify this device by confirming the following number appears on its screen.",
|
||||
"sas_caption_user": "Verify this user by confirming the following number appears on their screen.",
|
||||
"sas_description": "Compare a unique set of emoji if you don't have a camera on either device",
|
||||
@@ -1066,7 +1065,8 @@
|
||||
"unverified_sessions_toast_description": "Review to ensure your account is safe",
|
||||
"unverified_sessions_toast_reject": "Later",
|
||||
"unverified_sessions_toast_title": "You have unverified sessions",
|
||||
"verification_description": "Verify your identity to access encrypted messages and prove your identity to others. If you also use a mobile device, please open the app there before you proceed.",
|
||||
"use_another_device": "Use another device",
|
||||
"use_recovery_key": "Use recovery key",
|
||||
"verification_dialog_title_device": "Verify other device",
|
||||
"verification_dialog_title_user": "Verification Request",
|
||||
"verification_skip_warning": "Without verifying, you won't have access to all your messages and may appear as untrusted to others.",
|
||||
@@ -1076,9 +1076,6 @@
|
||||
"verify_emoji_prompt": "Verify by comparing unique emoji.",
|
||||
"verify_emoji_prompt_qr": "If you can't scan the code above, verify by comparing unique emoji.",
|
||||
"verify_later": "I'll verify later",
|
||||
"verify_using_device": "Verify with another device",
|
||||
"verify_using_key": "Verify with Recovery Key",
|
||||
"verify_using_key_or_phrase": "Verify with Recovery Key or Phrase",
|
||||
"waiting_for_user_accept": "Waiting for %(displayName)s to accept…",
|
||||
"waiting_other_device": "Waiting for you to verify on your other device…",
|
||||
"waiting_other_device_details": "Waiting for you to verify on your other device, %(deviceName)s (%(deviceId)s)…",
|
||||
@@ -2589,6 +2586,7 @@
|
||||
"breadcrumb_second_description": "You will lose any message history that’s stored only on the server",
|
||||
"breadcrumb_third_description": "You will need to verify all your existing devices and contacts again",
|
||||
"breadcrumb_title": "Are you sure you want to reset your identity?",
|
||||
"breadcrumb_title_cant_confirm": "You need to reset your identity",
|
||||
"breadcrumb_title_forgot": "Forgot your recovery key? You’ll need to reset your identity.",
|
||||
"breadcrumb_title_sync_failed": "Failed to sync key storage. You need to reset your identity.",
|
||||
"breadcrumb_warning": "Only do this if you believe your account has been compromised.",
|
||||
|
||||
Reference in New Issue
Block a user