Merge pull request #3950 from matrix-org/t3chguy/cs_verification_decoration

Cross Signing Right Panel Verification Decoration
This commit is contained in:
Michael Telatynski 2020-01-28 23:57:48 +00:00 committed by GitHub
commit 2c973f7467
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
15 changed files with 751 additions and 385 deletions

View file

@ -143,7 +143,9 @@
@import "./views/messages/_TextualEvent.scss"; @import "./views/messages/_TextualEvent.scss";
@import "./views/messages/_UnknownBody.scss"; @import "./views/messages/_UnknownBody.scss";
@import "./views/messages/_ViewSourceEvent.scss"; @import "./views/messages/_ViewSourceEvent.scss";
@import "./views/right_panel/_EncryptionInfo.scss";
@import "./views/right_panel/_UserInfo.scss"; @import "./views/right_panel/_UserInfo.scss";
@import "./views/right_panel/_VerificationPanel.scss";
@import "./views/room_settings/_AliasSettings.scss"; @import "./views/room_settings/_AliasSettings.scss";
@import "./views/room_settings/_ColorSettings.scss"; @import "./views/room_settings/_ColorSettings.scss";
@import "./views/rooms/_AppsDrawer.scss"; @import "./views/rooms/_AppsDrawer.scss";

View file

@ -0,0 +1,26 @@
/*
Copyright 2020 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
.mx_UserInfo {
.mx_EncryptionInfo_spinner {
.mx_Spinner {
margin-top: 25px;
margin-bottom: 15px;
}
text-align: center;
}
}

View file

@ -49,12 +49,17 @@ limitations under the License.
} }
.mx_UserInfo_container { .mx_UserInfo_container {
padding: 0 16px 16px 16px; padding: 8px 16px;
}
.mx_UserInfo_separator {
border-bottom: 1px solid lightgray; border-bottom: 1px solid lightgray;
} }
.mx_UserInfo_memberDetailsContainer { .mx_UserInfo_memberDetailsContainer {
padding-top: 0;
padding-bottom: 0; padding-bottom: 0;
margin-bottom: 8px;
} }
.mx_RoomTile_nameContainer { .mx_RoomTile_nameContainer {
@ -76,6 +81,7 @@ limitations under the License.
.mx_UserInfo_avatar > div { .mx_UserInfo_avatar > div {
max-width: 30vh; max-width: 30vh;
margin: 0 auto; margin: 0 auto;
transition: 0.5s;
} }
.mx_UserInfo_avatar > div > div { .mx_UserInfo_avatar > div > div {
@ -105,6 +111,7 @@ limitations under the License.
// override the calculated sizes so that the letter isn't HUGE // override the calculated sizes so that the letter isn't HUGE
font-size: 56px !important; font-size: 56px !important;
width: 100% !important; width: 100% !important;
transition: font-size 0.5s;
} }
.mx_UserInfo_avatar .mx_BaseAvatar.mx_BaseAvatar_image { .mx_UserInfo_avatar .mx_BaseAvatar.mx_BaseAvatar_image {
@ -204,10 +211,9 @@ limitations under the License.
padding-bottom: 16px; padding-bottom: 16px;
} }
.mx_UserInfo_scrollContainer .mx_UserInfo_container { .mx_UserInfo_scrollContainer:not(.mx_UserInfo_separator) {
padding-top: 16px; padding-top: 16px;
padding-bottom: 0; padding-bottom: 0;
border-bottom: none;
> :not(h3) { > :not(h3) {
margin-left: 8px; margin-left: 8px;
@ -256,11 +262,17 @@ limitations under the License.
.mx_UserInfo_verify { .mx_UserInfo_verify {
display: block; display: block;
background-color: $accent-color;
color: $accent-fg-color;
border-radius: 4px;
padding: 7px 1.5em;
text-align: center;
margin: 16px 0; margin: 16px 0;
} }
} }
.mx_UserInfo.mx_UserInfo_smallAvatar {
.mx_UserInfo_avatar > div {
max-width: 72px;
margin: 0 auto;
}
.mx_UserInfo_avatar .mx_BaseAvatar_initial {
font-size: 40px !important; // override the other override because here the avatar is smaller
}
}

View file

@ -0,0 +1,37 @@
/*
Copyright 2020 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
.mx_UserInfo {
.mx_VerificationPanel_verified_section .mx_E2EIcon {
margin: 0 auto;
}
.mx_VerificationPanel_qrCode {
padding: 4px 4px 0 4px;
background: white;
border-radius: 4px;
width: max-content;
max-width: 100%;
margin: 0 auto;
canvas {
// override height and width which are set on the element directly
height: auto !important;
width: 100% !important;
max-width: 240px;
}
}
}

View file

@ -169,7 +169,6 @@ export default class RightPanel extends React.Component {
const MemberList = sdk.getComponent('rooms.MemberList'); const MemberList = sdk.getComponent('rooms.MemberList');
const MemberInfo = sdk.getComponent('rooms.MemberInfo'); const MemberInfo = sdk.getComponent('rooms.MemberInfo');
const UserInfo = sdk.getComponent('right_panel.UserInfo'); const UserInfo = sdk.getComponent('right_panel.UserInfo');
const EncryptionPanel = sdk.getComponent('right_panel.EncryptionPanel');
const ThirdPartyMemberInfo = sdk.getComponent('rooms.ThirdPartyMemberInfo'); const ThirdPartyMemberInfo = sdk.getComponent('rooms.ThirdPartyMemberInfo');
const NotificationPanel = sdk.getComponent('structures.NotificationPanel'); const NotificationPanel = sdk.getComponent('structures.NotificationPanel');
const FilePanel = sdk.getComponent('structures.FilePanel'); const FilePanel = sdk.getComponent('structures.FilePanel');
@ -181,64 +180,82 @@ export default class RightPanel extends React.Component {
let panel = <div />; let panel = <div />;
if (this.props.roomId && this.state.phase === RIGHT_PANEL_PHASES.RoomMemberList) { switch (this.state.phase) {
panel = <MemberList roomId={this.props.roomId} key={this.props.roomId} />; case RIGHT_PANEL_PHASES.RoomMemberList:
} else if (this.props.groupId && this.state.phase === RIGHT_PANEL_PHASES.GroupMemberList) { if (this.props.roomId) {
panel = <GroupMemberList groupId={this.props.groupId} key={this.props.groupId} />; panel = <MemberList roomId={this.props.roomId} key={this.props.roomId} />;
} else if (this.state.phase === RIGHT_PANEL_PHASES.GroupRoomList) { }
panel = <GroupRoomList groupId={this.props.groupId} key={this.props.groupId} />; break;
} else if (this.state.phase === RIGHT_PANEL_PHASES.RoomMemberInfo) { case RIGHT_PANEL_PHASES.GroupMemberList:
if (SettingsStore.isFeatureEnabled("feature_cross_signing")) { if (this.props.groupId) {
const onClose = () => { panel = <GroupMemberList groupId={this.props.groupId} key={this.props.groupId} />;
dis.dispatch({ }
action: "view_user", break;
member: null, case RIGHT_PANEL_PHASES.GroupRoomList:
}); panel = <GroupRoomList groupId={this.props.groupId} key={this.props.groupId} />;
}; break;
panel = <UserInfo case RIGHT_PANEL_PHASES.RoomMemberInfo:
user={this.state.member} case RIGHT_PANEL_PHASES.EncryptionPanel:
roomId={this.props.roomId} if (SettingsStore.isFeatureEnabled("feature_cross_signing")) {
key={this.props.roomId || this.state.member.userId} const onClose = () => {
onClose={onClose} dis.dispatch({
/>; action: "view_user",
} else { member: this.state.phase === RIGHT_PANEL_PHASES.EncryptionPanel ? this.state.member : null,
panel = <MemberInfo member={this.state.member} key={this.props.roomId || this.state.member.userId} />; });
} };
} else if (this.state.phase === RIGHT_PANEL_PHASES.Room3pidMemberInfo) { panel = <UserInfo
panel = <ThirdPartyMemberInfo event={this.state.event} key={this.props.roomId} />; user={this.state.member}
} else if (this.state.phase === RIGHT_PANEL_PHASES.GroupMemberInfo) { roomId={this.props.roomId}
if (SettingsStore.isFeatureEnabled("feature_cross_signing")) { key={this.props.roomId || this.state.member.userId}
const onClose = () => { onClose={onClose}
dis.dispatch({ phase={this.state.phase}
action: "view_user", verificationRequest={this.state.verificationRequest}
member: null, />;
}); } else {
}; panel = <MemberInfo
panel = <UserInfo member={this.state.member}
user={this.state.member} key={this.props.roomId || this.state.member.userId}
groupId={this.props.groupId} />;
key={this.state.member.userId} }
onClose={onClose} />; break;
} else { case RIGHT_PANEL_PHASES.Room3pidMemberInfo:
panel = ( panel = <ThirdPartyMemberInfo event={this.state.event} key={this.props.roomId} />;
<GroupMemberInfo break;
groupMember={this.state.member} case RIGHT_PANEL_PHASES.GroupMemberInfo:
if (SettingsStore.isFeatureEnabled("feature_cross_signing")) {
const onClose = () => {
dis.dispatch({
action: "view_user",
member: null,
});
};
panel = <UserInfo
user={this.state.member}
groupId={this.props.groupId} groupId={this.props.groupId}
key={this.state.member.user_id} key={this.state.member.userId}
/> onClose={onClose} />;
); } else {
} panel = (
} else if (this.state.phase === RIGHT_PANEL_PHASES.GroupRoomInfo) { <GroupMemberInfo
panel = <GroupRoomInfo groupMember={this.state.member}
groupRoomId={this.state.groupRoomId} groupId={this.props.groupId}
groupId={this.props.groupId} key={this.state.member.user_id}
key={this.state.groupRoomId} />; />
} else if (this.state.phase === RIGHT_PANEL_PHASES.NotificationPanel) { );
panel = <NotificationPanel />; }
} else if (this.state.phase === RIGHT_PANEL_PHASES.FilePanel) { break;
panel = <FilePanel roomId={this.props.roomId} resizeNotifier={this.props.resizeNotifier} />; case RIGHT_PANEL_PHASES.GroupRoomInfo:
} else if (this.state.phase === RIGHT_PANEL_PHASES.EncryptionPanel) { panel = <GroupRoomInfo
panel = <EncryptionPanel member={this.state.member} verificationRequest={this.state.verificationRequest} />; groupRoomId={this.state.groupRoomId}
groupId={this.props.groupId}
key={this.state.groupRoomId} />;
break;
case RIGHT_PANEL_PHASES.NotificationPanel:
panel = <NotificationPanel />;
break;
case RIGHT_PANEL_PHASES.FilePanel:
panel = <FilePanel roomId={this.props.roomId} resizeNotifier={this.props.resizeNotifier} />;
break;
} }
const classes = classNames("mx_RightPanel", "mx_fadable", { const classes = classNames("mx_RightPanel", "mx_fadable", {

View file

@ -42,6 +42,7 @@ export default createReactClass({
button: PropTypes.string, button: PropTypes.string,
focus: PropTypes.bool, focus: PropTypes.bool,
onFinished: PropTypes.func.isRequired, onFinished: PropTypes.func.isRequired,
headerImage: PropTypes.string,
}, },
getDefaultProps: function() { getDefaultProps: function() {
@ -56,9 +57,12 @@ export default createReactClass({
render: function() { render: function() {
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog'); const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
return ( return (
<BaseDialog className="mx_ErrorDialog" onFinished={this.props.onFinished} <BaseDialog
title={this.props.title || _t('Error')} className="mx_ErrorDialog"
contentId='mx_Dialog_content' onFinished={this.props.onFinished}
title={this.props.title || _t('Error')}
headerImage={this.props.headerImage}
contentId='mx_Dialog_content'
> >
<div className="mx_Dialog_content" id='mx_Dialog_content'> <div className="mx_Dialog_content" id='mx_Dialog_content'>
{ this.props.description || _t('An error has occurred.') } { this.props.description || _t('An error has occurred.') }

View file

@ -51,6 +51,6 @@ export default class VerificationQRCode extends React.PureComponent {
const uri = `https://matrix.to/#/${this.props.keyholderUserId}?${qs.stringify(query)}`; const uri = `https://matrix.to/#/${this.props.keyholderUserId}?${qs.stringify(query)}`;
return <QRCode value={uri} size={256} logoWidth={48} logo={require("../../../../../res/img/matrix-m.svg")} />; return <QRCode value={uri} size={512} logoWidth={64} logo={require("../../../../../res/img/matrix-m.svg")} />;
} }
} }

View file

@ -45,10 +45,11 @@ export default class MKeyVerificationRequest extends React.Component {
_openRequest = () => { _openRequest = () => {
const {verificationRequest} = this.props.mxEvent; const {verificationRequest} = this.props.mxEvent;
const member = MatrixClientPeg.get().getUser(verificationRequest.otherUserId);
dis.dispatch({ dis.dispatch({
action: "set_right_panel_phase", action: "set_right_panel_phase",
phase: RIGHT_PANEL_PHASES.EncryptionPanel, phase: RIGHT_PANEL_PHASES.EncryptionPanel,
refireParams: {verificationRequest}, refireParams: {verificationRequest, member},
}); });
}; };

View file

@ -1,5 +1,5 @@
/* /*
Copyright 2019 The Matrix.org Foundation C.I.C. Copyright 2019, 2020 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License"); Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License. you may not use this file except in compliance with the License.
@ -14,18 +14,58 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
import React from 'react'; import React from "react";
import * as sdk from '../../../index'; import PropTypes from "prop-types";
import * as sdk from "../../../index";
import {_t} from "../../../languageHandler"; import {_t} from "../../../languageHandler";
export default class EncryptionInfo extends React.PureComponent { export const PendingActionSpinner = ({text}) => {
render() { const Spinner = sdk.getComponent('elements.Spinner');
return <div className="mx_EncryptionInfo_spinner">
<Spinner />
{ text }
</div>;
};
const EncryptionInfo = ({pending, member, onStartVerification}) => {
let content;
if (pending) {
const text = _t("Waiting for %(displayName)s to accept…", {
displayName: member.displayName || member.name || member.userId,
});
content = <PendingActionSpinner text={text} />;
} else {
const AccessibleButton = sdk.getComponent('elements.AccessibleButton'); const AccessibleButton = sdk.getComponent('elements.AccessibleButton');
return (<div className="mx_UserInfo"><div className="mx_UserInfo_container"> content = (
<h3>{_t("Verify User")}</h3> <AccessibleButton kind="primary" className="mx_UserInfo_verify" onClick={onStartVerification}>
<p>{_t("For extra security, verify this user by checking a one-time code on both of your devices.")}</p> {_t("Start Verification")}
<p>{_t("For maximum security, do this in person.")}</p> </AccessibleButton>
<AccessibleButton kind="primary" onClick={this.props.onStartVerification}>{_t("Start Verification")}</AccessibleButton> );
</div></div>);
} }
}
return <React.Fragment>
<div className="mx_UserInfo_container">
<h3>{_t("Encryption")}</h3>
<div>
<p>{_t("Messages in this room are end-to-end encrypted.")}</p>
<p>{_t("Your messages are secured and only you and the recipient have the unique keys to unlock them.")}</p>
</div>
</div>
<div className="mx_UserInfo_container">
<h3>{_t("Verify User")}</h3>
<div>
<p>{_t("For extra security, verify this user by checking a one-time code on both of your devices.")}</p>
<p>{_t("For maximum security, do this in person.")}</p>
{ content }
</div>
</div>
</React.Fragment>;
};
EncryptionInfo.propTypes = {
member: PropTypes.object.isRequired,
onStartVerification: PropTypes.func.isRequired,
request: PropTypes.object,
};
export default EncryptionInfo;

View file

@ -1,5 +1,5 @@
/* /*
Copyright 2019 The Matrix.org Foundation C.I.C. Copyright 2019, 2020 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License"); Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License. you may not use this file except in compliance with the License.
@ -14,35 +14,81 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
import React from 'react'; import React, {useCallback, useEffect, useState} from "react";
import PropTypes from "prop-types";
import EncryptionInfo from "./EncryptionInfo"; import EncryptionInfo from "./EncryptionInfo";
import VerificationPanel from "./VerificationPanel"; import VerificationPanel from "./VerificationPanel";
import {MatrixClientPeg} from "../../../MatrixClientPeg"; import {MatrixClientPeg} from "../../../MatrixClientPeg";
import {ensureDMExists} from "../../../createRoom"; import {ensureDMExists} from "../../../createRoom";
import {useEventEmitter} from "../../../hooks/useEventEmitter";
import Modal from "../../../Modal";
import {PHASE_REQUESTED} from "matrix-js-sdk/src/crypto/verification/request/VerificationRequest";
import * as sdk from "../../../index";
import {_t} from "../../../languageHandler";
export default class EncryptionPanel extends React.PureComponent { // cancellation codes which constitute a key mismatch
constructor(props) { const MISMATCHES = ["m.key_mismatch", "m.user_error", "m.mismatched_sas"];
super(props);
this.state = {};
}
render() { const EncryptionPanel = ({verificationRequest, member, onClose}) => {
const request = this.props.verificationRequest || this.state.verificationRequest; const [request, setRequest] = useState(verificationRequest);
const {member} = this.props; useEffect(() => {
if (request) { setRequest(verificationRequest);
return <VerificationPanel request={request} key={request.channel.transactionId} />; }, [verificationRequest]);
} else if (member) {
return <EncryptionInfo onStartVerification={this._onStartVerification} member={member} />; const [phase, setPhase] = useState(false);
} else { const changeHandler = useCallback(() => {
return <p>Not a member nor request, not sure what to render</p>; // handle transitions -> cancelled for mismatches which fire a modal instead of showing a card
if (request && request.cancelled && MISMATCHES.includes(request.cancellationCode)) {
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
Modal.createTrackedDialog("Verification failed", "insecure", ErrorDialog, {
headerImage: require("../../../../res/img/e2e/warning.svg"),
title: _t("Your messages are not secure"),
description: <div>
{_t("One of the following may be compromised:")}
<ul>
<li>{_t("Your homeserver")}</li>
<li>{_t("The homeserver the user youre verifying is connected to")}</li>
<li>{_t("Yours, or the other users internet connection")}</li>
<li>{_t("Yours, or the other users device")}</li>
</ul>
</div>,
onFinished: onClose,
});
return; // don't update phase here as we will be transitioning away from this view shortly
} }
}
_onStartVerification = async () => { if (request) {
const client = MatrixClientPeg.get(); setPhase(request.phase);
const {member} = this.props; }
const roomId = await ensureDMExists(client, member.userId); }, [onClose, request]);
const verificationRequest = await client.requestVerificationDM(member.userId, roomId); useEventEmitter(request, "change", changeHandler);
this.setState({verificationRequest});
}; const onStartVerification = useCallback(async () => {
} const cli = MatrixClientPeg.get();
const roomId = await ensureDMExists(cli, member.userId);
const verificationRequest = await cli.requestVerificationDM(member.userId, roomId);
setRequest(verificationRequest);
}, [member.userId]);
const requested = request && phase === PHASE_REQUESTED;
if (!request || requested) {
return <EncryptionInfo onStartVerification={onStartVerification} member={member} pending={requested} />;
} else {
return (
<VerificationPanel
onClose={onClose}
member={member}
request={request}
key={request.channel.transactionId}
phase={phase} />
);
}
};
EncryptionPanel.propTypes = {
member: PropTypes.object.isRequired,
onClose: PropTypes.func.isRequired,
verificationRequest: PropTypes.object,
};
export default EncryptionPanel;

View file

@ -41,6 +41,7 @@ import {useEventEmitter} from "../../../hooks/useEventEmitter";
import {textualPowerLevel} from '../../../Roles'; import {textualPowerLevel} from '../../../Roles';
import MatrixClientContext from "../../../contexts/MatrixClientContext"; import MatrixClientContext from "../../../contexts/MatrixClientContext";
import {RIGHT_PANEL_PHASES} from "../../../stores/RightPanelStorePhases"; import {RIGHT_PANEL_PHASES} from "../../../stores/RightPanelStorePhases";
import EncryptionPanel from "./EncryptionPanel";
const _disambiguateDevices = (devices) => { const _disambiguateDevices = (devices) => {
const names = Object.create(null); const names = Object.create(null);
@ -59,7 +60,7 @@ const _disambiguateDevices = (devices) => {
} }
}; };
const _getE2EStatus = (cli, userId, devices) => { export const getE2EStatus = (cli, userId, devices) => {
if (!SettingsStore.isFeatureEnabled("feature_cross_signing")) { if (!SettingsStore.isFeatureEnabled("feature_cross_signing")) {
const hasUnverifiedDevice = devices.some((device) => device.isUnverified()); const hasUnverifiedDevice = devices.some((device) => device.isUnverified());
return hasUnverifiedDevice ? "warning" : "verified"; return hasUnverifiedDevice ? "warning" : "verified";
@ -1053,33 +1054,85 @@ const PowerLevelEditor = ({user, room, roomPermissions, onFinished}) => {
); );
}; };
const UserInfo = ({user, groupId, roomId, onClose}) => { export const useDevices = (userId) => {
const cli = useContext(MatrixClientContext); const cli = useContext(MatrixClientContext);
// Load room if we are given a room id and memoize it // undefined means yet to be loaded, null means failed to load, otherwise list of devices
const room = useMemo(() => roomId ? cli.getRoom(roomId) : null, [cli, roomId]); const [devices, setDevices] = useState(undefined);
// fetch latest room member if we have a room, so we don't show historical information, falling back to user // Download device lists
const member = useMemo(() => room ? (room.getMember(user.userId) || user) : user, [room, user]); useEffect(() => {
setDevices(undefined);
// only display the devices list if our client supports E2E let cancelled = false;
const _enableDevices = cli.isCryptoEnabled();
async function _downloadDeviceList() {
try {
await cli.downloadKeys([userId], true);
const devices = await cli.getStoredDevicesForUser(userId);
if (cancelled) {
// we got cancelled - presumably a different user now
return;
}
_disambiguateDevices(devices);
setDevices(devices);
} catch (err) {
setDevices(null);
}
}
_downloadDeviceList();
// Handle being unmounted
return () => {
cancelled = true;
};
}, [cli, userId]);
// Listen to changes
useEffect(() => {
let cancel = false;
const onDeviceVerificationChanged = (_userId, device) => {
if (_userId === userId) {
// no need to re-download the whole thing; just update our copy of the list.
// Promise.resolve to handle transition from static result to promise; can be removed in future
Promise.resolve(cli.getStoredDevicesForUser(userId)).then((devices) => {
if (cancel) return;
setDevices(devices);
});
}
};
cli.on("deviceVerificationChanged", onDeviceVerificationChanged);
// Handle being unmounted
return () => {
cancel = true;
cli.removeListener("deviceVerificationChanged", onDeviceVerificationChanged);
};
}, [cli, userId]);
return devices;
};
const BasicUserInfo = ({room, member, groupId, devices, isRoomEncrypted}) => {
const cli = useContext(MatrixClientContext);
const powerLevels = useRoomPowerLevels(cli, room); const powerLevels = useRoomPowerLevels(cli, room);
// Load whether or not we are a Synapse Admin // Load whether or not we are a Synapse Admin
const isSynapseAdmin = useIsSynapseAdmin(cli); const isSynapseAdmin = useIsSynapseAdmin(cli);
// Check whether the user is ignored // Check whether the user is ignored
const [isIgnored, setIsIgnored] = useState(cli.isUserIgnored(user.userId)); const [isIgnored, setIsIgnored] = useState(cli.isUserIgnored(member.userId));
// Recheck if the user or client changes // Recheck if the user or client changes
useEffect(() => { useEffect(() => {
setIsIgnored(cli.isUserIgnored(user.userId)); setIsIgnored(cli.isUserIgnored(member.userId));
}, [cli, user.userId]); }, [cli, member.userId]);
// Recheck also if we receive new accountData m.ignored_user_list // Recheck also if we receive new accountData m.ignored_user_list
const accountDataHandler = useCallback((ev) => { const accountDataHandler = useCallback((ev) => {
if (ev.getType() === "m.ignored_user_list") { if (ev.getType() === "m.ignored_user_list") {
setIsIgnored(cli.isUserIgnored(user.userId)); setIsIgnored(cli.isUserIgnored(member.userId));
} }
}, [cli, user.userId]); }, [cli, member.userId]);
useEventEmitter(cli, "accountData", accountDataHandler); useEventEmitter(cli, "accountData", accountDataHandler);
// Count of how many operations are currently in progress, if > 0 then show a Spinner // Count of how many operations are currently in progress, if > 0 then show a Spinner
@ -1110,7 +1163,7 @@ const UserInfo = ({user, groupId, roomId, onClose}) => {
const [accepted] = await finished; const [accepted] = await finished;
if (!accepted) return; if (!accepted) return;
try { try {
await cli.deactivateSynapseUser(user.userId); await cli.deactivateSynapseUser(member.userId);
} catch (err) { } catch (err) {
console.error("Failed to deactivate user"); console.error("Failed to deactivate user");
console.error(err); console.error(err);
@ -1121,21 +1174,7 @@ const UserInfo = ({user, groupId, roomId, onClose}) => {
description: ((err && err.message) ? err.message : _t("Operation failed")), description: ((err && err.message) ? err.message : _t("Operation failed")),
}); });
} }
}, [cli, user.userId]); }, [cli, member.userId]);
const onMemberAvatarClick = useCallback(() => {
const avatarUrl = member.getMxcAvatarUrl ? member.getMxcAvatarUrl() : member.avatarUrl;
if (!avatarUrl) return;
const httpUrl = cli.mxcUrlToHttp(avatarUrl);
const ImageView = sdk.getComponent("elements.ImageView");
const params = {
src: httpUrl,
name: member.name,
};
Modal.createDialog(ImageView, params, "mx_Dialog_lightbox");
}, [cli, member]);
let synapseDeactivateButton; let synapseDeactivateButton;
let spinner; let spinner;
@ -1143,7 +1182,7 @@ const UserInfo = ({user, groupId, roomId, onClose}) => {
// We don't need a perfect check here, just something to pass as "probably not our homeserver". If // We don't need a perfect check here, just something to pass as "probably not our homeserver". If
// someone does figure out how to bypass this check the worst that happens is an error. // someone does figure out how to bypass this check the worst that happens is an error.
// FIXME this should be using cli instead of MatrixClientPeg.matrixClient // FIXME this should be using cli instead of MatrixClientPeg.matrixClient
if (isSynapseAdmin && user.userId.endsWith(`:${MatrixClientPeg.getHomeserverName()}`)) { if (isSynapseAdmin && member.userId.endsWith(`:${MatrixClientPeg.getHomeserverName()}`)) {
synapseDeactivateButton = ( synapseDeactivateButton = (
<AccessibleButton onClick={onSynapseDeactivate} className="mx_UserInfo_field mx_UserInfo_destructive"> <AccessibleButton onClick={onSynapseDeactivate} className="mx_UserInfo_field mx_UserInfo_destructive">
{_t("Deactivate user")} {_t("Deactivate user")}
@ -1167,7 +1206,7 @@ const UserInfo = ({user, groupId, roomId, onClose}) => {
adminToolsContainer = ( adminToolsContainer = (
<GroupAdminToolsSection <GroupAdminToolsSection
groupId={groupId} groupId={groupId}
groupMember={user} groupMember={member}
startUpdating={startUpdating} startUpdating={startUpdating}
stopUpdating={stopUpdating}> stopUpdating={stopUpdating}>
{ synapseDeactivateButton } { synapseDeactivateButton }
@ -1186,7 +1225,124 @@ const UserInfo = ({user, groupId, roomId, onClose}) => {
spinner = <Loader imgClassName="mx_ContextualMenu_spinner" />; spinner = <Loader imgClassName="mx_ContextualMenu_spinner" />;
} }
const displayName = member.name || member.displayname; const memberDetails = (
<PowerLevelSection
powerLevels={powerLevels}
user={member}
room={room}
roomPermissions={roomPermissions}
/>
);
// only display the devices list if our client supports E2E
const _enableDevices = cli.isCryptoEnabled();
let text;
if (!isRoomEncrypted) {
if (!_enableDevices) {
text = _t("This client does not support end-to-end encryption.");
} else if (room) {
text = _t("Messages in this room are not end-to-end encrypted.");
} else {
// TODO what to render for GroupMember
}
} else {
text = _t("Messages in this room are end-to-end encrypted.");
}
const userTrust = cli.checkUserTrust(member.userId);
const userVerified = SettingsStore.isFeatureEnabled("feature_cross_signing") ?
userTrust.isCrossSigningVerified() :
userTrust.isVerified();
const isMe = member.userId === cli.getUserId();
let verifyButton;
if (isRoomEncrypted && !userVerified && !isMe) {
verifyButton = (
<AccessibleButton kind="primary" className="mx_UserInfo_verify" onClick={() => verifyUser(member)}>
{_t("Verify")}
</AccessibleButton>
);
}
let devicesSection;
if (isRoomEncrypted) {
devicesSection = <DevicesSection
loading={devices === undefined}
devices={devices}
userId={member.userId} />;
}
const securitySection = (
<div className="mx_UserInfo_container">
<h3>{ _t("Security") }</h3>
<p>{ text }</p>
{ verifyButton }
{ devicesSection }
</div>
);
return <React.Fragment>
{ memberDetails &&
<div className="mx_UserInfo_container mx_UserInfo_separator mx_UserInfo_memberDetailsContainer">
<div className="mx_UserInfo_memberDetails">
{ memberDetails }
</div>
</div> }
{ securitySection }
<UserOptionsSection
devices={devices}
canInvite={roomPermissions.canInvite}
isIgnored={isIgnored}
member={member} />
{ adminToolsContainer }
{ spinner }
</React.Fragment>;
};
const UserInfoHeader = ({onClose, member, e2eStatus}) => {
const cli = useContext(MatrixClientContext);
let closeButton;
if (onClose) {
closeButton = <AccessibleButton className="mx_UserInfo_cancel" onClick={onClose} title={_t('Close')}>
<div />
</AccessibleButton>;
}
const onMemberAvatarClick = useCallback(() => {
const avatarUrl = member.getMxcAvatarUrl ? member.getMxcAvatarUrl() : member.avatarUrl;
if (!avatarUrl) return;
const httpUrl = cli.mxcUrlToHttp(avatarUrl);
const ImageView = sdk.getComponent("elements.ImageView");
const params = {
src: httpUrl,
name: member.name,
};
Modal.createDialog(ImageView, params, "mx_Dialog_lightbox");
}, [cli, member]);
const MemberAvatar = sdk.getComponent('avatars.MemberAvatar');
const avatarElement = (
<div className="mx_UserInfo_avatar">
<div>
<div>
<MemberAvatar
member={member}
width={2 * 0.3 * window.innerHeight} // 2x@30vh
height={2 * 0.3 * window.innerHeight} // 2x@30vh
resizeMethod="scale"
fallbackUserId={member.userId}
onClick={onMemberAvatarClick}
urls={member.avatarUrl ? [member.avatarUrl] : undefined} />
</div>
</div>
</div>
);
let presenceState; let presenceState;
let presenceLastActiveAgo; let presenceLastActiveAgo;
@ -1222,181 +1378,79 @@ const UserInfo = ({user, groupId, roomId, onClose}) => {
statusLabel = <span className="mx_UserInfo_statusMessage">{ statusMessage }</span>; statusLabel = <span className="mx_UserInfo_statusMessage">{ statusMessage }</span>;
} }
// const avatarUrl = user.getMxcAvatarUrl ? user.getMxcAvatarUrl() : user.avatarUrl;
const MemberAvatar = sdk.getComponent('avatars.MemberAvatar');
const avatarElement = (
<div className="mx_UserInfo_avatar">
<div>
<div>
<MemberAvatar
member={member}
width={2 * 0.3 * window.innerHeight} // 2x@30vh
height={2 * 0.3 * window.innerHeight} // 2x@30vh
resizeMethod="scale"
fallbackUserId={member.userId}
onClick={onMemberAvatarClick}
urls={member.avatarUrl ? [member.avatarUrl] : undefined} />
</div>
</div>
</div>
);
let closeButton;
if (onClose) {
closeButton = <AccessibleButton className="mx_UserInfo_cancel" onClick={onClose} title={_t('Close')}>
<div />
</AccessibleButton>;
}
const memberDetails = (
<PowerLevelSection
powerLevels={powerLevels}
user={member}
room={room}
roomPermissions={roomPermissions}
/>
);
const isRoomEncrypted = useIsEncrypted(cli, room);
// undefined means yet to be loaded, null means failed to load, otherwise list of devices
const [devices, setDevices] = useState(undefined);
// Download device lists
useEffect(() => {
setDevices(undefined);
let cancelled = false;
async function _downloadDeviceList() {
try {
await cli.downloadKeys([user.userId], true);
const devices = await cli.getStoredDevicesForUser(user.userId);
if (cancelled) {
// we got cancelled - presumably a different user now
return;
}
_disambiguateDevices(devices);
setDevices(devices);
} catch (err) {
setDevices(null);
}
}
_downloadDeviceList();
// Handle being unmounted
return () => {
cancelled = true;
};
}, [cli, user.userId]);
// Listen to changes
useEffect(() => {
let cancel = false;
const onDeviceVerificationChanged = (_userId, device) => {
if (_userId === user.userId) {
// no need to re-download the whole thing; just update our copy of the list.
// Promise.resolve to handle transition from static result to promise; can be removed in future
Promise.resolve(cli.getStoredDevicesForUser(user.userId)).then((devices) => {
if (cancel) return;
setDevices(devices);
});
}
};
cli.on("deviceVerificationChanged", onDeviceVerificationChanged);
// Handle being unmounted
return () => {
cancel = true;
cli.removeListener("deviceVerificationChanged", onDeviceVerificationChanged);
};
}, [cli, user.userId]);
let text;
if (!isRoomEncrypted) {
if (!_enableDevices) {
text = _t("This client does not support end-to-end encryption.");
} else if (room) {
text = _t("Messages in this room are not end-to-end encrypted.");
} else {
// TODO what to render for GroupMember
}
} else {
text = _t("Messages in this room are end-to-end encrypted.");
}
const userTrust = cli.checkUserTrust(user.userId);
const userVerified = SettingsStore.isFeatureEnabled("feature_cross_signing") ?
userTrust.isCrossSigningVerified() :
userTrust.isVerified();
const isMe = user.userId === cli.getUserId();
let verifyButton;
if (isRoomEncrypted && !userVerified && !isMe) {
verifyButton = <AccessibleButton className="mx_UserInfo_verify" onClick={() => verifyUser(user)}>
{_t("Verify")}
</AccessibleButton>;
}
let devicesSection;
if (isRoomEncrypted) {
devicesSection = <DevicesSection
loading={devices === undefined}
devices={devices} userId={user.userId} />;
}
const securitySection = (
<div className="mx_UserInfo_container">
<h3>{ _t("Security") }</h3>
<p>{ text }</p>
{ verifyButton }
{ devicesSection }
</div>
);
let e2eIcon; let e2eIcon;
if (isRoomEncrypted && devices) { if (e2eStatus) {
const e2eStatus = _getE2EStatus(cli, user.userId, devices);
e2eIcon = <E2EIcon size={18} status={e2eStatus} isUser={true} />; e2eIcon = <E2EIcon size={18} status={e2eStatus} isUser={true} />;
} }
return ( const displayName = member.name || member.displayname;
<div className="mx_UserInfo" role="tabpanel"> return <React.Fragment>
<AutoHideScrollbar className="mx_UserInfo_scrollContainer"> { closeButton }
{ closeButton } { avatarElement }
{ avatarElement }
<div className="mx_UserInfo_container"> <div className="mx_UserInfo_container mx_UserInfo_separator">
<div className="mx_UserInfo_profile"> <div className="mx_UserInfo_profile">
<div> <div>
<h2 aria-label={displayName}> <h2 aria-label={displayName}>
{ e2eIcon } { e2eIcon }
{ displayName } { displayName }
</h2> </h2>
</div>
<div>{ user.userId }</div>
<div className="mx_UserInfo_profileStatus">
{presenceLabel}
{statusLabel}
</div>
</div>
</div> </div>
<div>{ member.userId }</div>
<div className="mx_UserInfo_profileStatus">
{presenceLabel}
{statusLabel}
</div>
</div>
</div>
</React.Fragment>;
};
{ memberDetails && <div className="mx_UserInfo_container mx_UserInfo_memberDetailsContainer"> const UserInfo = ({user, groupId, roomId, onClose, phase=RIGHT_PANEL_PHASES.RoomMemberInfo, ...props}) => {
<div className="mx_UserInfo_memberDetails"> const cli = useContext(MatrixClientContext);
{ memberDetails }
</div>
</div> }
{ securitySection } // Load room if we are given a room id and memoize it
<UserOptionsSection const room = useMemo(() => roomId ? cli.getRoom(roomId) : null, [cli, roomId]);
// fetch latest room member if we have a room, so we don't show historical information, falling back to user
const member = useMemo(() => room ? (room.getMember(user.userId) || user) : user, [room, user]);
const isRoomEncrypted = useIsEncrypted(cli, room);
const devices = useDevices(user.userId);
let e2eStatus;
if (isRoomEncrypted && devices) {
e2eStatus = getE2EStatus(cli, user.userId, devices);
}
const classes = ["mx_UserInfo"];
let content;
switch (phase) {
case RIGHT_PANEL_PHASES.RoomMemberInfo:
case RIGHT_PANEL_PHASES.GroupMemberInfo:
content = (
<BasicUserInfo
room={room}
member={member}
groupId={groupId}
devices={devices} devices={devices}
canInvite={roomPermissions.canInvite} isRoomEncrypted={isRoomEncrypted} />
isIgnored={isIgnored} );
member={member} /> break;
case RIGHT_PANEL_PHASES.EncryptionPanel:
classes.push("mx_UserInfo_smallAvatar");
content = (
<EncryptionPanel {...props} member={member} onClose={onClose} />
);
break;
}
{ adminToolsContainer } return (
<div className={classes.join(" ")} role="tabpanel">
<AutoHideScrollbar className="mx_UserInfo_scrollContainer">
<UserInfoHeader member={member} e2eStatus={e2eStatus} onClose={onClose} />
{ spinner } { content }
</AutoHideScrollbar> </AutoHideScrollbar>
</div> </div>
); );

View file

@ -1,5 +1,5 @@
/* /*
Copyright 2019 The Matrix.org Foundation C.I.C. Copyright 2019, 2020 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License"); Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License. you may not use this file except in compliance with the License.
@ -15,11 +15,20 @@ limitations under the License.
*/ */
import React from 'react'; import React from 'react';
import * as sdk from '../../../index'; import * as sdk from '../../../index';
import {verificationMethods} from 'matrix-js-sdk/src/crypto'; import {verificationMethods} from 'matrix-js-sdk/src/crypto';
import VerificationQRCode from "../elements/crypto/VerificationQRCode"; import VerificationQRCode from "../elements/crypto/VerificationQRCode";
import {VerificationRequest} from "matrix-js-sdk/src/crypto/verification/request/VerificationRequest";
import {MatrixClientPeg} from "../../../MatrixClientPeg"; import {MatrixClientPeg} from "../../../MatrixClientPeg";
import {_t} from "../../../languageHandler";
import E2EIcon from "../rooms/E2EIcon";
import {
PHASE_READY,
PHASE_DONE,
PHASE_STARTED,
PHASE_CANCELLED,
} from "matrix-js-sdk/src/crypto/verification/request/VerificationRequest";
import Spinner from "../elements/Spinner";
export default class VerificationPanel extends React.PureComponent { export default class VerificationPanel extends React.PureComponent {
constructor(props) { constructor(props) {
@ -28,65 +37,144 @@ export default class VerificationPanel extends React.PureComponent {
this._hasVerifier = !!props.request.verifier; this._hasVerifier = !!props.request.verifier;
} }
render() { renderQRPhase(pending) {
return <div className="mx_UserInfo"> const {member, request} = this.props;
const AccessibleButton = sdk.getComponent('elements.AccessibleButton');
let button;
if (pending) {
button = <Spinner />;
} else {
button = (
<AccessibleButton kind="primary" className="mx_UserInfo_verify" onClick={this._startSAS}>
{_t("Verify by emoji")}
</AccessibleButton>
);
}
const cli = MatrixClientPeg.get();
const crossSigningInfo = cli.getStoredCrossSigningForUser(request.otherUserId);
if (!crossSigningInfo || !request.requestEvent || !request.requestEvent.getId()) {
// for whatever reason we can't generate a QR code, offer only SAS Verification
return <div className="mx_UserInfo_container">
<h3>Verify by emoji</h3>
<p>{_t("Verify by comparing unique emoji.")}</p>
{ button }
</div>;
}
const myKeyId = cli.getCrossSigningId();
const qrCodeKeys = [
[cli.getDeviceId(), cli.getDeviceEd25519Key()],
[myKeyId, myKeyId],
];
// TODO: add way to open camera to scan a QR code
return <React.Fragment>
<div className="mx_UserInfo_container"> <div className="mx_UserInfo_container">
{ this.renderStatus() } <h3>Verify by scanning</h3>
<p>{_t("Ask %(displayName)s to scan your code:", {
displayName: member.displayName || member.name || member.userId,
})}</p>
<div className="mx_VerificationPanel_qrCode">
<VerificationQRCode
keyholderUserId={MatrixClientPeg.get().getUserId()}
requestEventId={request.requestEvent.getId()}
otherUserKey={crossSigningInfo.getId("master")}
secret={request.encodedSharedSecret}
keys={qrCodeKeys}
/>
</div>
</div> </div>
</div>;
<div className="mx_UserInfo_container">
<h3>Verify by emoji</h3>
<p>{_t("If you can't scan the code above, verify by comparing unique emoji.")}</p>
{ button }
</div>
</React.Fragment>;
} }
renderStatus() { renderVerifiedPhase() {
const {member} = this.props;
const AccessibleButton = sdk.getComponent('elements.AccessibleButton'); const AccessibleButton = sdk.getComponent('elements.AccessibleButton');
const Spinner = sdk.getComponent('elements.Spinner'); return (
const {request: req} = this.props; <div className="mx_UserInfo_container mx_VerificationPanel_verified_section">
const request: VerificationRequest = req; <h3>Verified</h3>
<p>{_t("You've successfully verified %(displayName)s!", {
displayName: member.displayName || member.name || member.userId,
})}</p>
<E2EIcon isUser={true} status="verified" size={128} />
<p>Verify all users in a room to ensure it's secure.</p>
if (request.requested) { <AccessibleButton kind="primary" className="mx_UserInfo_verify" onClick={this.props.onClose}>
return (<p>Waiting for {request.otherUserId} to accept ... <Spinner /></p>); {_t("Got it")}
} else if (request.ready) { </AccessibleButton>
const verifyButton = <AccessibleButton kind="primary" onClick={this._startSAS}> </div>
Verify by emoji );
</AccessibleButton>; }
const crossSigningInfo = MatrixClientPeg.get().getStoredCrossSigningForUser(request.otherUserId); renderCancelledPhase() {
const myKeyId = MatrixClientPeg.get().getCrossSigningId(); const {member, request} = this.props;
if (request.requestEvent && request.requestEvent.getId() && crossSigningInfo) {
const qrCodeKeys = [
[MatrixClientPeg.get().getDeviceId(), MatrixClientPeg.get().getDeviceEd25519Key()],
[myKeyId, myKeyId],
];
const qrCode = <VerificationQRCode
keyholderUserId={MatrixClientPeg.get().getUserId()}
requestEventId={request.requestEvent.getId()}
otherUserKey={crossSigningInfo.getId("master")}
secret={request.encodedSharedSecret}
keys={qrCodeKeys}
/>;
return (<p>{request.otherUserId} is ready, start {verifyButton} or have them scan: {qrCode}</p>);
}
return (<p>{request.otherUserId} is ready, start {verifyButton}</p>); const AccessibleButton = sdk.getComponent('elements.AccessibleButton');
} else if (request.started) {
if (this.state.sasWaitingForOtherParty) { let text;
return <p>Waiting for {request.otherUserId} to confirm ...</p>; if (request.cancellationCode === "m.timeout") {
} else if (this.state.sasEvent) { text = _t("Verification timed out. Start verification again from their profile.");
const VerificationShowSas = sdk.getComponent('views.verification.VerificationShowSas'); } else if (request.cancellingUserId === request.otherUserId) {
return (<div> text = _t("%(displayName)s cancelled verification. Start verification again from their profile.", {
<VerificationShowSas displayName: member.displayName || member.name || member.userId,
sas={this.state.sasEvent.sas} });
onCancel={this._onSasMismatchesClick} } else {
onDone={this._onSasMatchesClick} text = _t("You cancelled verification. Start verification again from their profile.");
/>
</div>);
} else {
return (<p>Setting up SAS verification...</p>);
}
} else if (request.done) {
return <p>verified {request.otherUserId}!!</p>;
} else if (request.cancelled) {
return <p>cancelled by {request.cancellingUserId}!</p>;
} }
return (
<div className="mx_UserInfo_container">
<h3>Verification cancelled</h3>
<p>{ text }</p>
<AccessibleButton kind="primary" className="mx_UserInfo_verify" onClick={this.props.onClose}>
{_t("Got it")}
</AccessibleButton>
</div>
);
}
render() {
const {member} = this.props;
const displayName = member.displayName || member.name || member.userId;
switch (this.props.phase) {
case PHASE_READY:
return this.renderQRPhase();
case PHASE_STARTED:
if (this.state.sasEvent) {
const VerificationShowSas = sdk.getComponent('views.verification.VerificationShowSas');
return <div className="mx_UserInfo_container">
<h3>Compare emoji</h3>
<VerificationShowSas
displayName={displayName}
sas={this.state.sasEvent.sas}
onCancel={this._onSasMismatchesClick}
onDone={this._onSasMatchesClick}
/>
</div>;
} else {
return this.renderQRPhase(true); // keep showing same phase but with a spinner
}
case PHASE_DONE:
return this.renderVerifiedPhase();
case PHASE_CANCELLED:
return this.renderCancelledPhase();
}
return null;
} }
_startSAS = async () => { _startSAS = async () => {
@ -95,18 +183,15 @@ export default class VerificationPanel extends React.PureComponent {
await verifier.verify(); await verifier.verify();
} catch (err) { } catch (err) {
console.error(err); console.error(err);
} finally {
this.setState({sasEvent: null});
} }
}; };
_onSasMatchesClick = () => { _onSasMatchesClick = () => {
this.setState({sasWaitingForOtherParty: true});
this.state.sasEvent.confirm(); this.state.sasEvent.confirm();
}; };
_onSasMismatchesClick = () => { _onSasMismatchesClick = () => {
this.state.sasEvent.cancel(); this.state.sasEvent.mismatch();
}; };
_onVerifierShowSas = (sasEvent) => { _onVerifierShowSas = (sasEvent) => {
@ -128,7 +213,6 @@ export default class VerificationPanel extends React.PureComponent {
request.verifier.removeListener('show_sas', this._onVerifierShowSas); request.verifier.removeListener('show_sas', this._onVerifierShowSas);
} }
this._hasVerifier = !!request.verifier; this._hasVerifier = !!request.verifier;
this.forceUpdate();
}; };
componentDidMount() { componentDidMount() {

View file

@ -82,10 +82,14 @@ export default class VerificationRequestToast extends React.PureComponent {
should_peek: false, should_peek: false,
}); });
await request.accept(); await request.accept();
const cli = MatrixClientPeg.get();
dis.dispatch({ dis.dispatch({
action: "set_right_panel_phase", action: "set_right_panel_phase",
phase: RIGHT_PANEL_PHASES.EncryptionPanel, phase: RIGHT_PANEL_PHASES.EncryptionPanel,
refireParams: {verificationRequest: request}, refireParams: {
verificationRequest: request,
member: cli.getUser(request.otherUserId),
},
}); });
} else if (request.channel.deviceId && request.verifier) { } else if (request.channel.deviceId && request.verifier) {
// show to_device verifications in dialog still // show to_device verifications in dialog still

View file

@ -16,8 +16,10 @@ limitations under the License.
import React from 'react'; import React from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import * as sdk from '../../../index';
import { _t, _td } from '../../../languageHandler'; import { _t, _td } from '../../../languageHandler';
import {PendingActionSpinner} from "../right_panel/EncryptionInfo";
import AccessibleButton from "../elements/AccessibleButton";
import DialogButtons from "../elements/DialogButtons";
function capFirst(s) { function capFirst(s) {
return s.charAt(0).toUpperCase() + s.slice(1); return s.charAt(0).toUpperCase() + s.slice(1);
@ -25,18 +27,26 @@ function capFirst(s) {
export default class VerificationShowSas extends React.Component { export default class VerificationShowSas extends React.Component {
static propTypes = { static propTypes = {
displayName: PropTypes.string.isRequired,
onDone: PropTypes.func.isRequired, onDone: PropTypes.func.isRequired,
onCancel: PropTypes.func.isRequired, onCancel: PropTypes.func.isRequired,
sas: PropTypes.object.isRequired, sas: PropTypes.object.isRequired,
};
constructor(props) {
super(props);
this.state = {
pending: false,
};
} }
constructor() { onMatchClick = () => {
super(); this.setState({ pending: true });
} this.props.onDone();
};
render() { render() {
const DialogButtons = sdk.getComponent('views.elements.DialogButtons');
let sasDisplay; let sasDisplay;
let sasCaption; let sasCaption;
if (this.props.sas.emoji) { if (this.props.sas.emoji) {
@ -69,26 +79,34 @@ export default class VerificationShowSas extends React.Component {
} else { } else {
return <div> return <div>
{_t("Unable to find a supported verification method.")} {_t("Unable to find a supported verification method.")}
<DialogButtons <AccessibleButton kind="primary" onClick={this.props.onCancel} className="mx_UserInfo_verify">
primaryButton={_t('Cancel')} {_t('Cancel')}
hasCancel={false} </AccessibleButton>
onPrimaryButtonClick={this.props.onCancel}
/>
</div>; </div>;
} }
let confirm;
if (this.state.pending) {
const {displayName} = this.props;
const text = _t("Waiting for %(displayName)s to verify…", {displayName});
confirm = <PendingActionSpinner text={text} />;
} else {
// FIXME: stop using DialogButtons here once this component is only used in the right panel verification
confirm = <DialogButtons
primaryButton={_t("They match")}
onPrimaryButtonClick={this.onMatchClick}
primaryButtonClassName="mx_UserInfo_verify"
cancelButton={_t("They don't match")}
onCancel={this.props.onCancel}
cancelButtonClass="mx_UserInfo_verify"
/>;
}
return <div className="mx_VerificationShowSas"> return <div className="mx_VerificationShowSas">
<p>{sasCaption}</p> <p>{sasCaption}</p>
<p>{_t( <p>{_t("For ultimate security, do this in person or use another way to communicate.")}</p>
"For maximum security, we recommend you do this in person or use another " +
"trusted means of communication.",
)}</p>
{sasDisplay} {sasDisplay}
<DialogButtons onPrimaryButtonClick={this.props.onDone} {confirm}
primaryButton={_t("Continue")}
hasCancel={true}
onCancel={this.props.onCancel}
/>
</div>; </div>;
} }
} }

View file

@ -448,7 +448,10 @@
"Verify this user by confirming the following number appears on their screen.": "Verify this user by confirming the following number appears on their screen.", "Verify this user by confirming the following number appears on their screen.": "Verify this user by confirming the following number appears on their screen.",
"Unable to find a supported verification method.": "Unable to find a supported verification method.", "Unable to find a supported verification method.": "Unable to find a supported verification method.",
"Cancel": "Cancel", "Cancel": "Cancel",
"For maximum security, we recommend you do this in person or use another trusted means of communication.": "For maximum security, we recommend you do this in person or use another trusted means of communication.", "Waiting for %(displayName)s to verify…": "Waiting for %(displayName)s to verify…",
"They match": "They match",
"They don't match": "They don't match",
"For ultimate security, do this in person or use another way to communicate.": "For ultimate security, do this in person or use another way to communicate.",
"Dog": "Dog", "Dog": "Dog",
"Cat": "Cat", "Cat": "Cat",
"Lion": "Lion", "Lion": "Lion",
@ -1141,10 +1144,19 @@
"URL previews are disabled by default for participants in this room.": "URL previews are disabled by default for participants in this room.", "URL previews are disabled by default for participants in this room.": "URL previews are disabled by default for participants in this room.",
"In encrypted rooms, like this one, URL previews are disabled by default to ensure that your homeserver (where the previews are generated) cannot gather information about links you see in this room.": "In encrypted rooms, like this one, URL previews are disabled by default to ensure that your homeserver (where the previews are generated) cannot gather information about links you see in this room.", "In encrypted rooms, like this one, URL previews are disabled by default to ensure that your homeserver (where the previews are generated) cannot gather information about links you see in this room.": "In encrypted rooms, like this one, URL previews are disabled by default to ensure that your homeserver (where the previews are generated) cannot gather information about links you see in this room.",
"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.", "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.",
"Waiting for %(displayName)s to accept…": "Waiting for %(displayName)s to accept…",
"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.",
"Verify User": "Verify User", "Verify User": "Verify User",
"For extra security, verify this user by checking a one-time code on both of your devices.": "For extra security, verify this user by checking a one-time code on both of your devices.", "For extra security, verify this user by checking a one-time code on both of your devices.": "For extra security, verify this user by checking a one-time code on both of your devices.",
"For maximum security, do this in person.": "For maximum security, do this in person.", "For maximum security, do this in person.": "For maximum security, do this in person.",
"Start Verification": "Start Verification", "Your messages are not secure": "Your messages are not secure",
"One of the following may be compromised:": "One of the following may be compromised:",
"Your homeserver": "Your homeserver",
"The homeserver the user youre verifying is connected to": "The homeserver the user youre verifying is connected to",
"Yours, or the other users internet connection": "Yours, or the other users internet connection",
"Yours, or the other users device": "Yours, or the other users device",
"Members": "Members", "Members": "Members",
"Files": "Files", "Files": "Files",
"Trusted": "Trusted", "Trusted": "Trusted",
@ -1161,8 +1173,16 @@
"<strong>%(role)s</strong> in %(roomName)s": "<strong>%(role)s</strong> in %(roomName)s", "<strong>%(role)s</strong> in %(roomName)s": "<strong>%(role)s</strong> in %(roomName)s",
"This client does not support end-to-end encryption.": "This client does not support end-to-end encryption.", "This client does not support end-to-end encryption.": "This client does not support end-to-end encryption.",
"Messages in this room are not end-to-end encrypted.": "Messages in this room are not end-to-end encrypted.", "Messages in this room are not end-to-end encrypted.": "Messages in this room are not end-to-end encrypted.",
"Messages in this room are end-to-end encrypted.": "Messages in this room are end-to-end encrypted.",
"Security": "Security", "Security": "Security",
"Verify by emoji": "Verify by emoji",
"Verify by comparing unique emoji.": "Verify by comparing unique emoji.",
"Ask %(displayName)s to scan your code:": "Ask %(displayName)s to scan your code:",
"If you can't scan the code above, verify by comparing unique emoji.": "If you can't scan the code above, verify by comparing unique emoji.",
"You've successfully verified %(displayName)s!": "You've successfully verified %(displayName)s!",
"Got it": "Got it",
"Verification timed out. Start verification again from their profile.": "Verification timed out. Start verification again from their profile.",
"%(displayName)s cancelled verification. Start verification again from their profile.": "%(displayName)s cancelled verification. Start verification again from their profile.",
"You cancelled verification. Start verification again from their profile.": "You cancelled verification. Start verification again from their profile.",
"Sunday": "Sunday", "Sunday": "Sunday",
"Monday": "Monday", "Monday": "Monday",
"Tuesday": "Tuesday", "Tuesday": "Tuesday",
@ -1442,6 +1462,7 @@
"Verify device": "Verify device", "Verify device": "Verify device",
"Use Legacy Verification (for older clients)": "Use Legacy Verification (for older clients)", "Use Legacy Verification (for older clients)": "Use Legacy Verification (for older clients)",
"Verify by comparing a short text string.": "Verify by comparing a short text string.", "Verify by comparing a short text string.": "Verify by comparing a short text string.",
"For maximum security, we recommend you do this in person or use another trusted means of communication.": "For maximum security, we recommend you do this in person or use another trusted means of communication.",
"Begin Verifying": "Begin Verifying", "Begin Verifying": "Begin Verifying",
"Waiting for partner to accept...": "Waiting for partner to accept...", "Waiting for partner to accept...": "Waiting for partner to accept...",
"Nothing appearing? Not all clients support interactive verification yet. <button>Use legacy verification</button>.": "Nothing appearing? Not all clients support interactive verification yet. <button>Use legacy verification</button>.", "Nothing appearing? Not all clients support interactive verification yet. <button>Use legacy verification</button>.": "Nothing appearing? Not all clients support interactive verification yet. <button>Use legacy verification</button>.",