Skip to content
This repository has been archived by the owner on Sep 11, 2024. It is now read-only.

Commit

Permalink
Make cross-signing dialog clearer and more context-aware
Browse files Browse the repository at this point in the history
- Don't show loading spinners while waiting for user action
- When checking if there are other devices we can verify against, only
  look for devices that are actually cross-signed.
- Adjust displayed options depending on whether other devices and/or
  recovery keys exist, and add an option to reset cross-signing keys
  if necessary.
- Various minor clarifying adjustments to UI styling/text

Signed-off-by: Faye Duxovni <fayed@element.io>
  • Loading branch information
duxovni committed Oct 5, 2021
1 parent 79b52f8 commit 9a16b46
Show file tree
Hide file tree
Showing 9 changed files with 275 additions and 85 deletions.
1 change: 1 addition & 0 deletions res/css/_components.scss
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@
@import "./structures/_ViewSource.scss";
@import "./structures/auth/_CompleteSecurity.scss";
@import "./structures/auth/_Login.scss";
@import "./structures/auth/_SetupEncryptionBody.scss";
@import "./views/audio_messages/_AudioPlayer.scss";
@import "./views/audio_messages/_PlayPauseButton.scss";
@import "./views/audio_messages/_PlaybackContainer.scss";
Expand Down
13 changes: 13 additions & 0 deletions res/css/structures/auth/_CompleteSecurity.scss
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,19 @@ limitations under the License.
margin: 0 auto;
}

.mx_CompleteSecurity_skip {
mask: url('$(res)/img/feather-customised/cancel.svg');
mask-repeat: no-repeat;
mask-position: center;
mask-size: cover;
width: 18px;
height: 18px;
background-color: $dialog-close-fg-color;
cursor: pointer;
position: absolute;
right: 24px;
}

.mx_CompleteSecurity_body {
font-size: $font-15px;
}
Expand Down
24 changes: 24 additions & 0 deletions res/css/structures/auth/_SetupEncryptionBody.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
/*
Copyright 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.
*/

.mx_SetupEncryptionBody_reset {
color: $light-fg-color;
margin-top: $font-14px;

a.mx_SetupEncryptionBody_reset_link:is(:link, :hover, :visited) {
color: $warning-color;
}
}
35 changes: 30 additions & 5 deletions src/components/structures/auth/CompleteSecurity.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,13 +20,15 @@ import * as sdk from '../../../index';
import { SetupEncryptionStore, Phase } from '../../../stores/SetupEncryptionStore';
import SetupEncryptionBody from "./SetupEncryptionBody";
import { replaceableComponent } from "../../../utils/replaceableComponent";
import AccessibleButton from '../../views/elements/AccessibleButton';

interface IProps {
onFinished: () => void;
}

interface IState {
phase: Phase;
lostKeys: boolean;
}

@replaceableComponent("structures.auth.CompleteSecurity")
Expand All @@ -36,12 +38,17 @@ export default class CompleteSecurity extends React.Component<IProps, IState> {
const store = SetupEncryptionStore.sharedInstance();
store.on("update", this.onStoreUpdate);
store.start();
this.state = { phase: store.phase };
this.state = { phase: store.phase, lostKeys: store.lostKeys() };
}

private onStoreUpdate = (): void => {
const store = SetupEncryptionStore.sharedInstance();
this.setState({ phase: store.phase });
this.setState({ phase: store.phase, lostKeys: store.lostKeys() });
};

private onSkipClick = (): void => {
const store = SetupEncryptionStore.sharedInstance();
store.skip();
};

public componentWillUnmount(): void {
Expand All @@ -53,15 +60,20 @@ export default class CompleteSecurity extends React.Component<IProps, IState> {
public render() {
const AuthPage = sdk.getComponent("auth.AuthPage");
const CompleteSecurityBody = sdk.getComponent("auth.CompleteSecurityBody");
const { phase } = this.state;
const { phase, lostKeys } = this.state;
let icon;
let title;

if (phase === Phase.Loading) {
return null;
} else if (phase === Phase.Intro) {
icon = <span className="mx_CompleteSecurity_headerIcon mx_E2EIcon_warning" />;
title = _t("Verify this login");
if (lostKeys) {
icon = <span className="mx_CompleteSecurity_headerIcon mx_E2EIcon_warning" />;
title = _t("Unable to verify this login");
} else {
icon = <span className="mx_CompleteSecurity_headerIcon mx_E2EIcon_warning" />;
title = _t("Verify this login");
}
} else if (phase === Phase.Done) {
icon = <span className="mx_CompleteSecurity_headerIcon mx_E2EIcon_verified" />;
title = _t("Session verified");
Expand All @@ -71,16 +83,29 @@ export default class CompleteSecurity extends React.Component<IProps, IState> {
} else if (phase === Phase.Busy) {
icon = <span className="mx_CompleteSecurity_headerIcon mx_E2EIcon_warning" />;
title = _t("Verify this login");
} else if (phase === Phase.ConfirmReset) {
icon = <span className="mx_CompleteSecurity_headerIcon mx_E2EIcon_warning" />;
title = _t("Really reset verification keys?");
} else if (phase === Phase.Finished) {
// SetupEncryptionBody will take care of calling onFinished, we don't need to do anything
} else {
throw new Error(`Unknown phase ${phase}`);
}

let skipButton;
if (phase === Phase.Intro || phase === Phase.ConfirmReset) {
skipButton = (
<AccessibleButton onClick={this.onSkipClick} className="mx_CompleteSecurity_skip" aria-label={_t("Skip verification for now")} />
);
}

return (
<AuthPage>
<CompleteSecurityBody>
<h2 className="mx_CompleteSecurity_header">
{ icon }
{ title }
{ skipButton }
</h2>
<div className="mx_CompleteSecurity_body">
<SetupEncryptionBody onFinished={this.props.onFinished} />
Expand Down
144 changes: 103 additions & 41 deletions src/components/structures/auth/SetupEncryptionBody.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ interface IState {
phase: Phase;
verificationRequest: VerificationRequest;
backupInfo: IKeyBackupInfo;
lostKeys: boolean;
}

@replaceableComponent("structures.auth.SetupEncryptionBody")
Expand All @@ -62,6 +63,7 @@ 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(),
};
}

Expand All @@ -75,6 +77,7 @@ export default class SetupEncryptionBody extends React.Component<IProps, IState>
phase: store.phase,
verificationRequest: store.verificationRequest,
backupInfo: store.backupInfo,
lostKeys: store.lostKeys(),
});
};

Expand Down Expand Up @@ -105,11 +108,6 @@ export default class SetupEncryptionBody extends React.Component<IProps, IState>
});
};

private onSkipClick = () => {
const store = SetupEncryptionStore.sharedInstance();
store.skip();
};

private onSkipConfirmClick = () => {
const store = SetupEncryptionStore.sharedInstance();
store.skipConfirm();
Expand All @@ -120,6 +118,22 @@ export default class SetupEncryptionBody extends React.Component<IProps, IState>
store.returnAfterSkip();
};

private onResetClick = (ev: React.MouseEvent<HTMLAnchorElement>) => {
ev.preventDefault();
const store = SetupEncryptionStore.sharedInstance();
store.reset();
};

private onResetConfirmClick = () => {
const store = SetupEncryptionStore.sharedInstance();
store.resetConfirm();
};

private onResetBackClick = () => {
const store = SetupEncryptionStore.sharedInstance();
store.returnAfterReset();
};

private onDoneClick = () => {
const store = SetupEncryptionStore.sharedInstance();
store.done();
Expand All @@ -132,6 +146,7 @@ export default class SetupEncryptionBody extends React.Component<IProps, IState>
public render() {
const {
phase,
lostKeys,
} = this.state;

if (this.state.verificationRequest) {
Expand All @@ -143,43 +158,67 @@ export default class SetupEncryptionBody extends React.Component<IProps, IState>
isRoomEncrypted={false}
/>;
} else if (phase === Phase.Intro) {
const store = SetupEncryptionStore.sharedInstance();
let recoveryKeyPrompt;
if (store.keyInfo && keyHasPassphrase(store.keyInfo)) {
recoveryKeyPrompt = _t("Use Security Key or Phrase");
} else if (store.keyInfo) {
recoveryKeyPrompt = _t("Use Security Key");
}
if (lostKeys) {
return (
<div>
<p>{ _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. " +
"In order to verify your identity on this device, you'll need to reset " +
"your verification keys.",
) }</p>

let useRecoveryKeyButton;
if (recoveryKeyPrompt) {
useRecoveryKeyButton = <AccessibleButton kind="link" onClick={this.onUsePassphraseClick}>
{ recoveryKeyPrompt }
</AccessibleButton>;
}
<div className="mx_CompleteSecurity_actionRow">
<AccessibleButton kind="primary" onClick={this.onResetConfirmClick}>
{ _t("Proceed with reset") }
</AccessibleButton>
</div>
</div>
);
} else {
const store = SetupEncryptionStore.sharedInstance();
let recoveryKeyPrompt;
if (store.keyInfo && keyHasPassphrase(store.keyInfo)) {
recoveryKeyPrompt = _t("Verify with Security Key or Phrase");
} else if (store.keyInfo) {
recoveryKeyPrompt = _t("Verify with Security Key");
}

let verifyButton;
if (store.hasDevicesToVerifyAgainst) {
verifyButton = <AccessibleButton kind="primary" onClick={this.onVerifyClick}>
{ _t("Use another login") }
</AccessibleButton>;
}
let useRecoveryKeyButton;
if (recoveryKeyPrompt) {
useRecoveryKeyButton = <AccessibleButton kind="primary" onClick={this.onUsePassphraseClick}>
{ recoveryKeyPrompt }
</AccessibleButton>;
}

return (
<div>
<p>{ _t(
"Verify your identity to access encrypted messages and prove your identity to others.",
) }</p>
let verifyButton;
if (store.hasDevicesToVerifyAgainst) {
verifyButton = <AccessibleButton kind="primary" onClick={this.onVerifyClick}>
{ _t("Verify with another login") }
</AccessibleButton>;
}

<div className="mx_CompleteSecurity_actionRow">
{ verifyButton }
{ useRecoveryKeyButton }
<AccessibleButton kind="danger" onClick={this.onSkipClick}>
{ _t("Skip") }
</AccessibleButton>
return (
<div>
<p>{ _t(
"Verify your identity to access encrypted messages and prove your identity to others.",
) }</p>

<div className="mx_CompleteSecurity_actionRow">
{ verifyButton }
{ useRecoveryKeyButton }
</div>
<div className="mx_SetupEncryptionBody_reset">
{ _t("Forgotten or lost all recovery methods? <a>Reset all</a>", null, {
a: (sub) => <a
href=""
onClick={this.onResetClick}
className="mx_SetupEncryptionBody_reset_link">{ sub }</a>,
}) }
</div>
</div>
</div>
);
);
}
} else if (phase === Phase.Done) {
let message;
if (this.state.backupInfo) {
Expand Down Expand Up @@ -215,21 +254,44 @@ export default class SetupEncryptionBody extends React.Component<IProps, IState>
) }</p>
<div className="mx_CompleteSecurity_actionRow">
<AccessibleButton
className="warning"
kind="secondary"
kind="danger_outline"
onClick={this.onSkipConfirmClick}
>
{ _t("Skip") }
{ _t("I'll verify later") }
</AccessibleButton>
<AccessibleButton
kind="danger"
kind="primary"
onClick={this.onSkipBackClick}
>
{ _t("Go Back") }
</AccessibleButton>
</div>
</div>
);
} else if (phase === Phase.ConfirmReset) {
return (
<div>
<p>{ _t(
"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.",
) }</p>
<p>{ _t(
"Please only proceed if you're sure you've lost all of your other " +
"devices and your security key.",
) }</p>

<div className="mx_CompleteSecurity_actionRow">
<AccessibleButton kind="danger_outline" onClick={this.onResetConfirmClick}>
{ _t("Proceed with reset") }
</AccessibleButton>
<AccessibleButton kind="primary" onClick={this.onResetBackClick}>
{ _t("Go Back") }
</AccessibleButton>
</div>
</div>
);
} else if (phase === Phase.Busy || phase === Phase.Loading) {
return <Spinner />;
} else {
Expand Down
18 changes: 10 additions & 8 deletions src/components/views/right_panel/EncryptionInfo.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -49,16 +49,18 @@ const EncryptionInfo: React.FC<IProps> = ({
isSelfVerification,
}: IProps) => {
let content: JSX.Element;
if (waitingForOtherParty || waitingForNetwork) {
if (waitingForOtherParty && isSelfVerification) {
content = (
<div>
{ _t("To proceed, please accept the verification request on your other login.") }
</div>
);
} else if (waitingForOtherParty || waitingForNetwork) {
let text: string;
if (waitingForOtherParty) {
if (isSelfVerification) {
text = _t("Accept on your other login…");
} else {
text = _t("Waiting for %(displayName)s to accept…", {
displayName: (member as User).displayName || (member as RoomMember).name || member.userId,
});
}
text = _t("Waiting for %(displayName)s to accept…", {
displayName: (member as User).displayName || (member as RoomMember).name || member.userId,
});
} else {
text = _t("Accepting…");
}
Expand Down
Loading

0 comments on commit 9a16b46

Please sign in to comment.