Merge pull request #3796 from matrix-org/bwindels/verification-right-panel

Initial support for verification in right panel
This commit is contained in:
Bruno Windels 2020-01-20 17:18:32 +00:00 committed by GitHub
commit 95a0ebaf06
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
17 changed files with 411 additions and 346 deletions

View File

@ -63,7 +63,6 @@ import { countRoomsWithNotif } from '../../RoomNotifs';
import { ThemeWatcher } from "../../theme"; import { ThemeWatcher } from "../../theme";
import { storeRoomAliasInCache } from '../../RoomAliasCache'; import { storeRoomAliasInCache } from '../../RoomAliasCache';
import { defer } from "../../utils/promise"; import { defer } from "../../utils/promise";
import KeyVerificationStateObserver from '../../utils/KeyVerificationStateObserver';
import ToastStore from "../../stores/ToastStore"; import ToastStore from "../../stores/ToastStore";
/** constants for MatrixChat.state.view */ /** constants for MatrixChat.state.view */
@ -1454,18 +1453,13 @@ export default createReactClass({
if (SettingsStore.isFeatureEnabled("feature_cross_signing")) { if (SettingsStore.isFeatureEnabled("feature_cross_signing")) {
cli.on("crypto.verification.request", request => { cli.on("crypto.verification.request", request => {
let requestObserver; console.log(`MatrixChat got a .request ${request.channel.transactionId}`, request.event.getRoomId());
if (request.event.getRoomId()) { if (request.pending) {
requestObserver = new KeyVerificationStateObserver(
request.event, MatrixClientPeg.get());
}
if (!requestObserver || requestObserver.pending) {
ToastStore.sharedInstance().addOrReplaceToast({ ToastStore.sharedInstance().addOrReplaceToast({
key: 'verifreq_' + request.event.getId(), key: 'verifreq_' + request.channel.transactionId,
title: _t("Verification Request"), title: _t("Verification Request"),
icon: "verification", icon: "verification",
props: {request, requestObserver}, props: {request},
component: sdk.getComponent("toasts.VerificationRequestToast"), component: sdk.getComponent("toasts.VerificationRequestToast"),
}); });
} }

View File

@ -160,6 +160,7 @@ export default class RightPanel extends React.Component {
groupId: payload.groupId, groupId: payload.groupId,
member: payload.member, member: payload.member,
event: payload.event, event: payload.event,
verificationRequest: payload.verificationRequest,
}); });
} }
} }
@ -168,6 +169,7 @@ 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');
@ -235,6 +237,8 @@ export default class RightPanel extends React.Component {
panel = <NotificationPanel />; panel = <NotificationPanel />;
} else if (this.state.phase === RIGHT_PANEL_PHASES.FilePanel) { } else if (this.state.phase === RIGHT_PANEL_PHASES.FilePanel) {
panel = <FilePanel roomId={this.props.roomId} resizeNotifier={this.props.resizeNotifier} />; panel = <FilePanel roomId={this.props.roomId} resizeNotifier={this.props.resizeNotifier} />;
} else if (this.state.phase === RIGHT_PANEL_PHASES.EncryptionPanel) {
panel = <EncryptionPanel member={this.state.member} verificationRequest={this.state.verificationRequest} />;
} }
const classes = classNames("mx_RightPanel", "mx_fadable", { const classes = classNames("mx_RightPanel", "mx_fadable", {

View File

@ -1134,7 +1134,7 @@ const TimelinePanel = createReactClass({
const allowPartial = opts.allowPartial || false; const allowPartial = opts.allowPartial || false;
const messagePanel = this._messagePanel.current; const messagePanel = this._messagePanel.current;
if (messagePanel === undefined) return null; if (!messagePanel) return null;
const messagePanelNode = ReactDOM.findDOMNode(messagePanel); const messagePanelNode = ReactDOM.findDOMNode(messagePanel);
if (!messagePanelNode) return null; // sometimes this happens for fresh rooms/post-sync if (!messagePanelNode) return null; // sometimes this happens for fresh rooms/post-sync

View File

@ -24,8 +24,7 @@ import * as sdk from '../../../index';
import * as FormattingUtils from '../../../utils/FormattingUtils'; import * as FormattingUtils from '../../../utils/FormattingUtils';
import { _t } from '../../../languageHandler'; import { _t } from '../../../languageHandler';
import {verificationMethods} from 'matrix-js-sdk/src/crypto'; import {verificationMethods} from 'matrix-js-sdk/src/crypto';
import DMRoomMap from '../../../utils/DMRoomMap'; import {ensureDMExists} from "../../../createRoom";
import createRoom from "../../../createRoom";
import dis from "../../../dispatcher"; import dis from "../../../dispatcher";
import SettingsStore from '../../../settings/SettingsStore'; import SettingsStore from '../../../settings/SettingsStore';
@ -100,9 +99,15 @@ export default class DeviceVerifyDialog extends React.Component {
if (!verifyingOwnDevice && SettingsStore.getValue("feature_cross_signing")) { if (!verifyingOwnDevice && SettingsStore.getValue("feature_cross_signing")) {
const roomId = await ensureDMExistsAndOpen(this.props.userId); const roomId = await ensureDMExistsAndOpen(this.props.userId);
// throws upon cancellation before having started // throws upon cancellation before having started
this._verifier = await client.requestVerificationDM( const request = await client.requestVerificationDM(
this.props.userId, roomId, [verificationMethods.SAS], this.props.userId, roomId, [verificationMethods.SAS],
); );
await request.waitFor(r => r.ready || r.started);
if (request.ready) {
this._verifier = request.beginKeyVerification(verificationMethods.SAS);
} else {
this._verifier = request.verifier;
}
} else { } else {
this._verifier = client.beginKeyVerification( this._verifier = client.beginKeyVerification(
verificationMethods.SAS, this.props.userId, this.props.device.deviceId, verificationMethods.SAS, this.props.userId, this.props.device.deviceId,
@ -316,23 +321,7 @@ export default class DeviceVerifyDialog extends React.Component {
} }
async function ensureDMExistsAndOpen(userId) { async function ensureDMExistsAndOpen(userId) {
const client = MatrixClientPeg.get(); const roomId = ensureDMExists(MatrixClientPeg.get(), userId);
const roomIds = DMRoomMap.shared().getDMRoomsForUserId(userId);
const rooms = roomIds.map(id => client.getRoom(id));
const suitableDMRooms = rooms.filter(r => {
if (r && r.getMyMembership() === "join") {
const member = r.getMember(userId);
return member && (member.membership === "invite" || member.membership === "join");
}
return false;
});
let roomId;
if (suitableDMRooms.length) {
const room = suitableDMRooms[0];
roomId = room.roomId;
} else {
roomId = await createRoom({dmUserId: userId, spinner: false, andView: false});
}
// don't use andView and spinner in createRoom, together, they cause this dialog to close and reopen, // don't use andView and spinner in createRoom, together, they cause this dialog to close and reopen,
// we causes us to loose the verifier and restart, and we end up having two verification requests // we causes us to loose the verifier and restart, and we end up having two verification requests
dis.dispatch({ dis.dispatch({

View File

@ -19,102 +19,83 @@ import classNames from 'classnames';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import {MatrixClientPeg} from '../../../MatrixClientPeg'; import {MatrixClientPeg} from '../../../MatrixClientPeg';
import { _t } from '../../../languageHandler'; import { _t } from '../../../languageHandler';
import KeyVerificationStateObserver, {getNameForEventRoom, userLabelForEventRoom} import {getNameForEventRoom, userLabelForEventRoom}
from '../../../utils/KeyVerificationStateObserver'; from '../../../utils/KeyVerificationStateObserver';
export default class MKeyVerificationConclusion extends React.Component { export default class MKeyVerificationConclusion extends React.Component {
constructor(props) { constructor(props) {
super(props); super(props);
this.keyVerificationState = null;
this.state = {
done: false,
cancelled: false,
otherPartyUserId: null,
cancelPartyUserId: null,
};
const rel = this.props.mxEvent.getRelation();
if (rel) {
const client = MatrixClientPeg.get();
const room = client.getRoom(this.props.mxEvent.getRoomId());
const requestEvent = room.findEventById(rel.event_id);
if (requestEvent) {
this._createStateObserver(requestEvent, client);
this.state = this._copyState();
} else {
const findEvent = event => {
if (event.getId() === rel.event_id) {
this._createStateObserver(event, client);
this.setState(this._copyState());
room.removeListener("Room.timeline", findEvent);
}
};
room.on("Room.timeline", findEvent);
}
}
}
_createStateObserver(requestEvent, client) {
this.keyVerificationState = new KeyVerificationStateObserver(requestEvent, client, () => {
this.setState(this._copyState());
});
}
_copyState() {
const {done, cancelled, otherPartyUserId, cancelPartyUserId} = this.keyVerificationState;
return {done, cancelled, otherPartyUserId, cancelPartyUserId};
} }
componentDidMount() { componentDidMount() {
if (this.keyVerificationState) { const request = this.props.mxEvent.verificationRequest;
this.keyVerificationState.attach(); if (request) {
request.on("change", this._onRequestChanged);
} }
} }
componentWillUnmount() { componentWillUnmount() {
if (this.keyVerificationState) { const request = this.props.mxEvent.verificationRequest;
this.keyVerificationState.detach(); if (request) {
request.off("change", this._onRequestChanged);
} }
} }
_getName(userId) { _onRequestChanged = () => {
const roomId = this.props.mxEvent.getRoomId(); this.forceUpdate();
const client = MatrixClientPeg.get(); };
const room = client.getRoom(roomId);
const member = room.getMember(userId);
return member ? member.name : userId;
}
_userLabel(userId) { _shouldRender(mxEvent, request) {
const name = this._getName(userId); // normally should not happen
if (name !== userId) { if (!request) {
return _t("%(name)s (%(userId)s)", {name, userId}); return false;
} else {
return userId;
} }
// .cancel event that was sent after the verification finished, ignore
if (mxEvent.getType() === "m.key.verification.cancel" && !request.cancelled) {
return false;
}
// .done event that was sent after the verification cancelled, ignore
if (mxEvent.getType() === "m.key.verification.done" && !request.done) {
return false;
}
// request hasn't concluded yet
if (request.pending) {
return false;
}
return true;
} }
render() { render() {
const {mxEvent} = this.props; const {mxEvent} = this.props;
const request = mxEvent.verificationRequest;
if (!this._shouldRender(mxEvent, request)) {
return null;
}
const client = MatrixClientPeg.get(); const client = MatrixClientPeg.get();
const myUserId = client.getUserId(); const myUserId = client.getUserId();
let title; let title;
if (this.state.done) { if (request.done) {
title = _t("You verified %(name)s", {name: getNameForEventRoom(this.state.otherPartyUserId, mxEvent)}); title = _t("You verified %(name)s", {name: getNameForEventRoom(request.otherUserId, mxEvent)});
} else if (this.state.cancelled) { } else if (request.cancelled) {
if (mxEvent.getSender() === myUserId) { const userId = request.cancellingUserId;
if (userId === myUserId) {
title = _t("You cancelled verifying %(name)s", title = _t("You cancelled verifying %(name)s",
{name: getNameForEventRoom(this.state.otherPartyUserId, mxEvent)}); {name: getNameForEventRoom(request.otherUserId, mxEvent)});
} else if (mxEvent.getSender() === this.state.otherPartyUserId) { } else {
title = _t("%(name)s cancelled verifying", title = _t("%(name)s cancelled verifying",
{name: getNameForEventRoom(this.state.otherPartyUserId, mxEvent)}); {name: getNameForEventRoom(userId, mxEvent)});
} }
} }
if (title) { if (title) {
const subtitle = userLabelForEventRoom(this.state.otherPartyUserId, mxEvent); const subtitle = userLabelForEventRoom(request.otherUserId, mxEvent);
const classes = classNames("mx_EventTile_bubble", "mx_KeyVerification", "mx_KeyVerification_icon", { const classes = classNames("mx_EventTile_bubble", "mx_KeyVerification", "mx_KeyVerification_icon", {
mx_KeyVerification_icon_verified: this.state.done, mx_KeyVerification_icon_verified: request.done,
}); });
return (<div className={classes}> return (<div className={classes}>
<div className="mx_KeyVerification_title">{title}</div> <div className="mx_KeyVerification_title">{title}</div>

View File

@ -17,48 +17,66 @@ limitations under the License.
import React from 'react'; import React from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import {MatrixClientPeg} from '../../../MatrixClientPeg'; import {MatrixClientPeg} from '../../../MatrixClientPeg';
import {verificationMethods} from 'matrix-js-sdk/src/crypto';
import * as sdk from '../../../index'; import * as sdk from '../../../index';
import Modal from "../../../Modal";
import { _t } from '../../../languageHandler'; import { _t } from '../../../languageHandler';
import KeyVerificationStateObserver, {getNameForEventRoom, userLabelForEventRoom} import {getNameForEventRoom, userLabelForEventRoom}
from '../../../utils/KeyVerificationStateObserver'; from '../../../utils/KeyVerificationStateObserver';
import dis from "../../../dispatcher";
import {RIGHT_PANEL_PHASES} from "../../../stores/RightPanelStorePhases";
export default class MKeyVerificationRequest extends React.Component { export default class MKeyVerificationRequest extends React.Component {
constructor(props) { constructor(props) {
super(props); super(props);
this.keyVerificationState = new KeyVerificationStateObserver(this.props.mxEvent, MatrixClientPeg.get(), () => {
this.setState(this._copyState());
});
this.state = this._copyState();
}
_copyState() {
const {accepted, done, cancelled, cancelPartyUserId, otherPartyUserId} = this.keyVerificationState;
return {accepted, done, cancelled, cancelPartyUserId, otherPartyUserId};
} }
componentDidMount() { componentDidMount() {
this.keyVerificationState.attach(); const request = this.props.mxEvent.verificationRequest;
if (request) {
request.on("change", this._onRequestChanged);
}
} }
componentWillUnmount() { componentWillUnmount() {
this.keyVerificationState.detach(); const request = this.props.mxEvent.verificationRequest;
if (request) {
request.off("change", this._onRequestChanged);
}
} }
_onAcceptClicked = () => { _openRequest = () => {
const IncomingSasDialog = sdk.getComponent('views.dialogs.IncomingSasDialog'); const {verificationRequest} = this.props.mxEvent;
// todo: validate event, for example if it has sas in the methods. dis.dispatch({
const verifier = MatrixClientPeg.get().acceptVerificationDM(this.props.mxEvent, verificationMethods.SAS); action: "set_right_panel_phase",
Modal.createTrackedDialog('Incoming Verification', '', IncomingSasDialog, { phase: RIGHT_PANEL_PHASES.EncryptionPanel,
verifier, refireParams: {verificationRequest},
}, null, /* priority = */ false, /* static = */ true); });
}; };
_onRejectClicked = () => { _onRequestChanged = () => {
// todo: validate event, for example if it has sas in the methods. this.forceUpdate();
const verifier = MatrixClientPeg.get().acceptVerificationDM(this.props.mxEvent, verificationMethods.SAS); };
verifier.cancel("User declined");
_onAcceptClicked = async () => {
const request = this.props.mxEvent.verificationRequest;
if (request) {
try {
await request.accept();
this._openRequest();
} catch (err) {
console.error(err.message);
}
}
};
_onRejectClicked = async () => {
const request = this.props.mxEvent.verificationRequest;
if (request) {
try {
await request.cancel();
} catch (err) {
console.error(err.message);
}
}
}; };
_acceptedLabel(userId) { _acceptedLabel(userId) {
@ -82,46 +100,49 @@ export default class MKeyVerificationRequest extends React.Component {
} }
render() { render() {
const AccessibleButton = sdk.getComponent("elements.AccessibleButton");
const FormButton = sdk.getComponent("elements.FormButton");
const {mxEvent} = this.props; const {mxEvent} = this.props;
const fromUserId = mxEvent.getSender(); const request = mxEvent.verificationRequest;
const content = mxEvent.getContent();
const toUserId = content.to; if (!request || request.invalid) {
const client = MatrixClientPeg.get(); return null;
const myUserId = client.getUserId(); }
const isOwn = fromUserId === myUserId;
let title; let title;
let subtitle; let subtitle;
let stateNode; let stateNode;
if (this.state.accepted || this.state.cancelled) { const accepted = request.ready || request.started || request.done;
if (accepted || request.cancelled) {
let stateLabel; let stateLabel;
if (this.state.accepted) { if (accepted) {
stateLabel = this._acceptedLabel(toUserId); stateLabel = (<AccessibleButton onClick={this._openRequest}>
} else if (this.state.cancelled) { {this._acceptedLabel(request.receivingUserId)}
stateLabel = this._cancelledLabel(this.state.cancelPartyUserId); </AccessibleButton>);
} else {
stateLabel = this._cancelledLabel(request.cancellingUserId);
} }
stateNode = (<div className="mx_KeyVerification_state">{stateLabel}</div>); stateNode = (<div className="mx_KeyVerification_state">{stateLabel}</div>);
} }
if (toUserId === myUserId) { // request sent to us if (!request.initiatedByMe) {
title = (<div className="mx_KeyVerification_title">{ title = (<div className="mx_KeyVerification_title">{
_t("%(name)s wants to verify", {name: getNameForEventRoom(fromUserId, mxEvent)})}</div>); _t("%(name)s wants to verify", {name: getNameForEventRoom(request.requestingUserId, mxEvent)})}</div>);
subtitle = (<div className="mx_KeyVerification_subtitle">{ subtitle = (<div className="mx_KeyVerification_subtitle">{
userLabelForEventRoom(fromUserId, mxEvent)}</div>); userLabelForEventRoom(request.requestingUserId, mxEvent)}</div>);
const isResolved = !(this.state.accepted || this.state.cancelled || this.state.done); if (request.requested && !request.observeOnly) {
if (isResolved) {
const FormButton = sdk.getComponent("elements.FormButton");
stateNode = (<div className="mx_KeyVerification_buttons"> stateNode = (<div className="mx_KeyVerification_buttons">
<FormButton kind="danger" onClick={this._onRejectClicked} label={_t("Decline")} /> <FormButton kind="danger" onClick={this._onRejectClicked} label={_t("Decline")} />
<FormButton onClick={this._onAcceptClicked} label={_t("Accept")} /> <FormButton onClick={this._onAcceptClicked} label={_t("Accept")} />
</div>); </div>);
} }
} else if (isOwn) { // request sent by us } else { // request sent by us
title = (<div className="mx_KeyVerification_title">{ title = (<div className="mx_KeyVerification_title">{
_t("You sent a verification request")}</div>); _t("You sent a verification request")}</div>);
subtitle = (<div className="mx_KeyVerification_subtitle">{ subtitle = (<div className="mx_KeyVerification_subtitle">{
userLabelForEventRoom(this.state.otherPartyUserId, mxEvent)}</div>); userLabelForEventRoom(request.receivingUserId, mxEvent)}</div>);
} }
if (title) { if (title) {

View File

@ -0,0 +1,31 @@
/*
Copyright 2019 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import React from 'react';
import * as sdk from '../../../index';
import {_t} from "../../../languageHandler";
export default class EncryptionInfo extends React.PureComponent {
render() {
const AccessibleButton = sdk.getComponent('elements.AccessibleButton');
return (<div className="mx_UserInfo"><div className="mx_UserInfo_container">
<h3>{_t("Verify User")}</h3>
<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>
<AccessibleButton kind="primary" onClick={this.props.onStartVerification}>{_t("Start Verification")}</AccessibleButton>
</div></div>);
}
}

View File

@ -0,0 +1,48 @@
/*
Copyright 2019 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import React from 'react';
import EncryptionInfo from "./EncryptionInfo";
import VerificationPanel from "./VerificationPanel";
import {MatrixClientPeg} from "../../../MatrixClientPeg";
import {ensureDMExists} from "../../../createRoom";
export default class EncryptionPanel extends React.PureComponent {
constructor(props) {
super(props);
this.state = {};
}
render() {
const request = this.props.verificationRequest || this.state.verificationRequest;
const {member} = this.props;
if (request) {
return <VerificationPanel request={request} key={request.channel.transactionId} />;
} else if (member) {
return <EncryptionInfo onStartVerification={this._onStartVerification} member={member} />;
} else {
return <p>Not a member nor request, not sure what to render</p>;
}
}
_onStartVerification = async () => {
const client = MatrixClientPeg.get();
const {member} = this.props;
const roomId = await ensureDMExists(client, member.userId);
const verificationRequest = await client.requestVerificationDM(member.userId, roomId);
this.setState({verificationRequest});
};
}

View File

@ -28,6 +28,7 @@ import RightPanelStore from "../../../stores/RightPanelStore";
const MEMBER_PHASES = [ const MEMBER_PHASES = [
RIGHT_PANEL_PHASES.RoomMemberList, RIGHT_PANEL_PHASES.RoomMemberList,
RIGHT_PANEL_PHASES.RoomMemberInfo, RIGHT_PANEL_PHASES.RoomMemberInfo,
RIGHT_PANEL_PHASES.EncryptionPanel,
RIGHT_PANEL_PHASES.Room3pidMemberInfo, RIGHT_PANEL_PHASES.Room3pidMemberInfo,
]; ];

View File

@ -40,6 +40,7 @@ import E2EIcon from "../rooms/E2EIcon";
import {useEventEmitter} from "../../../hooks/useEventEmitter"; 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";
const _disambiguateDevices = (devices) => { const _disambiguateDevices = (devices) => {
const names = Object.create(null); const names = Object.create(null);
@ -117,6 +118,14 @@ function verifyDevice(userId, device) {
}, null, /* priority = */ false, /* static = */ true); }, null, /* priority = */ false, /* static = */ true);
} }
function verifyUser(user) {
dis.dispatch({
action: "set_right_panel_phase",
phase: RIGHT_PANEL_PHASES.EncryptionPanel,
refireParams: {member: user},
});
}
function DeviceItem({userId, device}) { function DeviceItem({userId, device}) {
const cli = useContext(MatrixClientContext); const cli = useContext(MatrixClientContext);
const deviceTrust = cli.checkDeviceTrust(userId, device.deviceId); const deviceTrust = cli.checkDeviceTrust(userId, device.deviceId);
@ -1225,15 +1234,13 @@ const UserInfo = ({user, groupId, roomId, onClose}) => {
setDevices(null); setDevices(null);
} }
} }
if (isRoomEncrypted) { _downloadDeviceList();
_downloadDeviceList();
}
// Handle being unmounted // Handle being unmounted
return () => { return () => {
cancelled = true; cancelled = true;
}; };
}, [cli, user.userId, isRoomEncrypted]); }, [cli, user.userId]);
// Listen to changes // Listen to changes
useEffect(() => { useEffect(() => {
@ -1249,18 +1256,13 @@ const UserInfo = ({user, groupId, roomId, onClose}) => {
}); });
} }
}; };
cli.on("deviceVerificationChanged", onDeviceVerificationChanged);
if (isRoomEncrypted) {
cli.on("deviceVerificationChanged", onDeviceVerificationChanged);
}
// Handle being unmounted // Handle being unmounted
return () => { return () => {
cancel = true; cancel = true;
if (isRoomEncrypted) { cli.removeListener("deviceVerificationChanged", onDeviceVerificationChanged);
cli.removeListener("deviceVerificationChanged", onDeviceVerificationChanged);
}
}; };
}, [cli, user.userId, isRoomEncrypted]); }, [cli, user.userId]);
let text; let text;
if (!isRoomEncrypted) { if (!isRoomEncrypted) {
@ -1275,22 +1277,24 @@ const UserInfo = ({user, groupId, roomId, onClose}) => {
text = _t("Messages in this room are end-to-end encrypted."); text = _t("Messages in this room are end-to-end encrypted.");
} }
const devicesSection = isRoomEncrypted ?
(<DevicesSection loading={devices === undefined} devices={devices} userId={user.userId} />) : null;
const userVerified = cli.checkUserTrust(user.userId).isVerified(); const userVerified = cli.checkUserTrust(user.userId).isVerified();
const isMe = user.userId === cli.getUserId();
let verifyButton; let verifyButton;
if (!userVerified) { if (!userVerified && !isMe) {
verifyButton = <AccessibleButton className="mx_UserInfo_verify" onClick={() => verifyDevice(user.userId, null)}> verifyButton = <AccessibleButton className="mx_UserInfo_verify" onClick={() => verifyUser(user)}>
{_t("Verify")} {_t("Verify")}
</AccessibleButton>; </AccessibleButton>;
} }
const devicesSection = <DevicesSection
loading={devices === undefined}
devices={devices} userId={user.userId} />;
const securitySection = ( const securitySection = (
<div className="mx_UserInfo_container"> <div className="mx_UserInfo_container">
<h3>{ _t("Security") }</h3> <h3>{ _t("Security") }</h3>
<p>{ text }</p> <p>{ text }</p>
{verifyButton} { verifyButton }
{ devicesSection } { devicesSection }
</div> </div>
); );
@ -1308,7 +1312,7 @@ const UserInfo = ({user, groupId, roomId, onClose}) => {
<div className="mx_UserInfo_container"> <div className="mx_UserInfo_container">
<div className="mx_UserInfo_profile"> <div className="mx_UserInfo_profile">
<div > <div>
<h2 aria-label={displayName}> <h2 aria-label={displayName}>
{ e2eIcon } { e2eIcon }
{ displayName } { displayName }

View File

@ -0,0 +1,119 @@
/*
Copyright 2019 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import React from 'react';
import * as sdk from '../../../index';
import {verificationMethods} from 'matrix-js-sdk/src/crypto';
export default class VerificationPanel extends React.PureComponent {
constructor(props) {
super(props);
this.state = {};
this._hasVerifier = !!props.request.verifier;
}
render() {
return <div className="mx_UserInfo">
<div className="mx_UserInfo_container">
{ this.renderStatus() }
</div>
</div>;
}
renderStatus() {
const AccessibleButton = sdk.getComponent('elements.AccessibleButton');
const Spinner = sdk.getComponent('elements.Spinner');
const {request} = this.props;
if (request.requested) {
return (<p>Waiting for {request.otherUserId} to accept ... <Spinner /></p>);
} else if (request.ready) {
const verifyButton = <AccessibleButton kind="primary" onClick={this._startSAS}>
Verify by emoji
</AccessibleButton>;
return (<p>{request.otherUserId} is ready, start {verifyButton}</p>);
} else if (request.started) {
if (this.state.sasWaitingForOtherParty) {
return <p>Waiting for {request.otherUserId} to confirm ...</p>;
} else if (this.state.sasEvent) {
const VerificationShowSas = sdk.getComponent('views.verification.VerificationShowSas');
return (<div>
<VerificationShowSas
sas={this.state.sasEvent.sas}
onCancel={this._onSasMismatchesClick}
onDone={this._onSasMatchesClick}
/>
</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>;
}
}
_startSAS = async () => {
const verifier = this.props.request.beginKeyVerification(verificationMethods.SAS);
try {
await verifier.verify();
} catch (err) {
console.error(err);
} finally {
this.setState({sasEvent: null});
}
};
_onSasMatchesClick = () => {
this.setState({sasWaitingForOtherParty: true});
this.state.sasEvent.confirm();
};
_onSasMismatchesClick = () => {
this.state.sasEvent.cancel();
};
_onVerifierShowSas = (sasEvent) => {
this.setState({sasEvent});
};
_onRequestChange = async () => {
const {request} = this.props;
if (!this._hasVerifier && !!request.verifier) {
request.verifier.on('show_sas', this._onVerifierShowSas);
try {
// on the requester side, this is also awaited in _startSAS,
// but that's ok as verify should return the same promise.
await request.verifier.verify();
} catch (err) {
console.error("error verify", err);
}
} else if (this._hasVerifier && !request.verifier) {
request.verifier.removeListener('show_sas', this._onVerifierShowSas);
}
this._hasVerifier = !!request.verifier;
this.forceUpdate();
};
componentDidMount() {
this.props.request.on("change", this._onRequestChange);
}
componentWillUnmount() {
this.props.request.off("change", this._onRequestChange);
}
}

View File

@ -18,55 +18,40 @@ import React from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import * as sdk from "../../../index"; import * as sdk from "../../../index";
import { _t } from '../../../languageHandler'; import { _t } from '../../../languageHandler';
import Modal from "../../../Modal";
import {MatrixClientPeg} from '../../../MatrixClientPeg'; import {MatrixClientPeg} from '../../../MatrixClientPeg';
import {verificationMethods} from 'matrix-js-sdk/src/crypto'; import {RIGHT_PANEL_PHASES} from "../../../stores/RightPanelStorePhases";
import KeyVerificationStateObserver, {userLabelForEventRoom} from "../../../utils/KeyVerificationStateObserver"; import {userLabelForEventRoom} from "../../../utils/KeyVerificationStateObserver";
import dis from "../../../dispatcher"; import dis from "../../../dispatcher";
import ToastStore from "../../../stores/ToastStore"; import ToastStore from "../../../stores/ToastStore";
export default class VerificationRequestToast extends React.PureComponent { export default class VerificationRequestToast extends React.PureComponent {
constructor(props) { constructor(props) {
super(props); super(props);
const {event, timeout} = props.request; this.state = {counter: Math.ceil(props.request.timeout / 1000)};
// to_device requests don't have a timestamp, so consider them age=0
const age = event.getTs() ? event.getLocalAge() : 0;
const remaining = Math.max(0, timeout - age);
const counter = Math.ceil(remaining / 1000);
this.state = {counter};
if (this.props.requestObserver) {
this.props.requestObserver.setCallback(this._checkRequestIsPending);
}
} }
componentDidMount() { componentDidMount() {
if (this.props.requestObserver) { const {request} = this.props;
this.props.requestObserver.attach();
this._checkRequestIsPending();
}
this._intervalHandle = setInterval(() => { this._intervalHandle = setInterval(() => {
let {counter} = this.state; let {counter} = this.state;
counter -= 1; counter = Math.max(0, counter - 1);
if (counter <= 0) { this.setState({counter});
this.cancel();
} else {
this.setState({counter});
}
}, 1000); }, 1000);
request.on("change", this._checkRequestIsPending);
} }
componentWillUnmount() { componentWillUnmount() {
clearInterval(this._intervalHandle); clearInterval(this._intervalHandle);
if (this.props.requestObserver) { const {request} = this.props;
this.props.requestObserver.detach(); request.off("change", this._checkRequestIsPending);
}
} }
_checkRequestIsPending = () => { _checkRequestIsPending = () => {
if (!this.props.requestObserver.pending) { const {request} = this.props;
if (request.ready || request.started || request.done || request.cancelled || request.observeOnly) {
ToastStore.sharedInstance().dismissToast(this.props.toastKey); ToastStore.sharedInstance().dismissToast(this.props.toastKey);
} }
} };
cancel = () => { cancel = () => {
ToastStore.sharedInstance().dismissToast(this.props.toastKey); ToastStore.sharedInstance().dismissToast(this.props.toastKey);
@ -77,9 +62,10 @@ export default class VerificationRequestToast extends React.PureComponent {
} }
} }
accept = () => { accept = async () => {
ToastStore.sharedInstance().dismissToast(this.props.toastKey); ToastStore.sharedInstance().dismissToast(this.props.toastKey);
const {event} = this.props.request; const {request} = this.props;
const {event} = request;
// no room id for to_device requests // no room id for to_device requests
if (event.getRoomId()) { if (event.getRoomId()) {
dis.dispatch({ dis.dispatch({
@ -88,18 +74,23 @@ export default class VerificationRequestToast extends React.PureComponent {
should_peek: false, should_peek: false,
}); });
} }
try {
const verifier = this.props.request.beginKeyVerification(verificationMethods.SAS); await request.accept();
const IncomingSasDialog = sdk.getComponent('views.dialogs.IncomingSasDialog'); dis.dispatch({
Modal.createTrackedDialog('Incoming Verification', '', IncomingSasDialog, { action: "set_right_panel_phase",
verifier, phase: RIGHT_PANEL_PHASES.EncryptionPanel,
}, null, /* priority = */ false, /* static = */ true); refireParams: {verificationRequest: request},
});
} catch (err) {
console.error(err.message);
}
}; };
render() { render() {
const FormButton = sdk.getComponent("elements.FormButton"); const FormButton = sdk.getComponent("elements.FormButton");
const {event} = this.props.request; const {request} = this.props;
const userId = event.getSender(); const {event} = request;
const userId = request.otherUserId;
let nameLabel = event.getRoomId() ? userLabelForEventRoom(userId, event) : userId; let nameLabel = event.getRoomId() ? userLabelForEventRoom(userId, event) : userId;
// for legacy to_device verification requests // for legacy to_device verification requests
if (nameLabel === userId) { if (nameLabel === userId) {
@ -121,6 +112,5 @@ export default class VerificationRequestToast extends React.PureComponent {
VerificationRequestToast.propTypes = { VerificationRequestToast.propTypes = {
request: PropTypes.object.isRequired, request: PropTypes.object.isRequired,
requestObserver: PropTypes.instanceOf(KeyVerificationStateObserver),
toastKey: PropTypes.string.isRequired, toastKey: PropTypes.string.isRequired,
}; };

View File

@ -1,5 +1,6 @@
/* /*
Copyright 2015, 2016 OpenMarket Ltd Copyright 2015, 2016 OpenMarket Ltd
Copyright 2019 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.
@ -20,7 +21,7 @@ import * as sdk from './index';
import { _t } from './languageHandler'; import { _t } from './languageHandler';
import dis from "./dispatcher"; import dis from "./dispatcher";
import * as Rooms from "./Rooms"; import * as Rooms from "./Rooms";
import DMRoomMap from "./utils/DMRoomMap";
import {getAddressType} from "./UserAddress"; import {getAddressType} from "./UserAddress";
/** /**
@ -139,3 +140,23 @@ export default function createRoom(opts) {
return null; return null;
}); });
} }
export async function ensureDMExists(client, userId) {
const roomIds = DMRoomMap.shared().getDMRoomsForUserId(userId);
const rooms = roomIds.map(id => client.getRoom(id));
const suitableDMRooms = rooms.filter(r => {
if (r && r.getMyMembership() === "join") {
const member = r.getMember(userId);
return member && (member.membership === "invite" || member.membership === "join");
}
return false;
});
let roomId;
if (suitableDMRooms.length) {
const room = suitableDMRooms[0];
roomId = room.roomId;
} else {
roomId = await createRoom({dmUserId: userId, spinner: false, andView: false});
}
return roomId;
}

View File

@ -1117,6 +1117,10 @@
"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.",
"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 maximum security, do this in person.": "For maximum security, do this in person.",
"Start Verification": "Start Verification",
"Members": "Members", "Members": "Members",
"Files": "Files", "Files": "Files",
"Trusted": "Trusted", "Trusted": "Trusted",

View File

@ -123,7 +123,11 @@ export default class RightPanelStore extends Store {
if (payload.action === 'view_room' || payload.action === 'view_group') { if (payload.action === 'view_room' || payload.action === 'view_group') {
// Reset to the member list if we're viewing member info // Reset to the member list if we're viewing member info
const memberInfoPhases = [RIGHT_PANEL_PHASES.RoomMemberInfo, RIGHT_PANEL_PHASES.Room3pidMemberInfo]; const memberInfoPhases = [
RIGHT_PANEL_PHASES.RoomMemberInfo,
RIGHT_PANEL_PHASES.Room3pidMemberInfo,
RIGHT_PANEL_PHASES.EncryptionPanel,
];
if (memberInfoPhases.includes(this._state.lastRoomPhase)) { if (memberInfoPhases.includes(this._state.lastRoomPhase)) {
this._setState({lastRoomPhase: RIGHT_PANEL_PHASES.RoomMemberList, lastRoomPhaseParams: {}}); this._setState({lastRoomPhase: RIGHT_PANEL_PHASES.RoomMemberList, lastRoomPhaseParams: {}});
} }

View File

@ -21,8 +21,9 @@ export const RIGHT_PANEL_PHASES = Object.freeze({
FilePanel: 'FilePanel', FilePanel: 'FilePanel',
NotificationPanel: 'NotificationPanel', NotificationPanel: 'NotificationPanel',
RoomMemberInfo: 'RoomMemberInfo', RoomMemberInfo: 'RoomMemberInfo',
Room3pidMemberInfo: 'Room3pidMemberInfo', EncryptionPanel: 'EncryptionPanel',
Room3pidMemberInfo: 'Room3pidMemberInfo',
// Group stuff // Group stuff
GroupMemberList: 'GroupMemberList', GroupMemberList: 'GroupMemberList',
GroupRoomList: 'GroupRoomList', GroupRoomList: 'GroupRoomList',

View File

@ -17,153 +17,6 @@ limitations under the License.
import {MatrixClientPeg} from '../MatrixClientPeg'; import {MatrixClientPeg} from '../MatrixClientPeg';
import { _t } from '../languageHandler'; import { _t } from '../languageHandler';
const SUB_EVENT_TYPES_OF_INTEREST = ["start", "cancel", "done"];
export default class KeyVerificationStateObserver {
constructor(requestEvent, client, updateCallback) {
this._requestEvent = requestEvent;
this._client = client;
this._updateCallback = updateCallback;
this.accepted = false;
this.done = false;
this.cancelled = false;
this._updateVerificationState();
}
get concluded() {
return this.accepted || this.done || this.cancelled;
}
get pending() {
return !this.concluded;
}
setCallback(callback) {
this._updateCallback = callback;
}
attach() {
this._requestEvent.on("Event.relationsCreated", this._onRelationsCreated);
for (const phaseName of SUB_EVENT_TYPES_OF_INTEREST) {
this._tryListenOnRelationsForType(`m.key.verification.${phaseName}`);
}
}
detach() {
const roomId = this._requestEvent.getRoomId();
const room = this._client.getRoom(roomId);
for (const phaseName of SUB_EVENT_TYPES_OF_INTEREST) {
const relations = room.getUnfilteredTimelineSet()
.getRelationsForEvent(this._requestEvent.getId(), "m.reference", `m.key.verification.${phaseName}`);
if (relations) {
relations.removeListener("Relations.add", this._onRelationsUpdated);
relations.removeListener("Relations.remove", this._onRelationsUpdated);
relations.removeListener("Relations.redaction", this._onRelationsUpdated);
}
}
this._requestEvent.removeListener("Event.relationsCreated", this._onRelationsCreated);
}
_onRelationsCreated = (relationType, eventType) => {
if (relationType !== "m.reference") {
return;
}
if (
eventType !== "m.key.verification.start" &&
eventType !== "m.key.verification.cancel" &&
eventType !== "m.key.verification.done"
) {
return;
}
this._tryListenOnRelationsForType(eventType);
this._updateVerificationState();
this._updateCallback();
};
_tryListenOnRelationsForType(eventType) {
const roomId = this._requestEvent.getRoomId();
const room = this._client.getRoom(roomId);
const relations = room.getUnfilteredTimelineSet()
.getRelationsForEvent(this._requestEvent.getId(), "m.reference", eventType);
if (relations) {
relations.on("Relations.add", this._onRelationsUpdated);
relations.on("Relations.remove", this._onRelationsUpdated);
relations.on("Relations.redaction", this._onRelationsUpdated);
}
}
_onRelationsUpdated = (event) => {
this._updateVerificationState();
this._updateCallback && this._updateCallback();
};
_updateVerificationState() {
const roomId = this._requestEvent.getRoomId();
const room = this._client.getRoom(roomId);
const timelineSet = room.getUnfilteredTimelineSet();
const fromUserId = this._requestEvent.getSender();
const content = this._requestEvent.getContent();
const toUserId = content.to;
this.cancelled = false;
this.done = false;
this.accepted = false;
this.otherPartyUserId = null;
this.cancelPartyUserId = null;
const startRelations = timelineSet.getRelationsForEvent(
this._requestEvent.getId(), "m.reference", "m.key.verification.start");
if (startRelations) {
for (const startEvent of startRelations.getRelations()) {
if (startEvent.getSender() === toUserId) {
this.accepted = true;
}
}
}
const doneRelations = timelineSet.getRelationsForEvent(
this._requestEvent.getId(), "m.reference", "m.key.verification.done");
if (doneRelations) {
let senderDone = false;
let receiverDone = false;
for (const doneEvent of doneRelations.getRelations()) {
if (doneEvent.getSender() === toUserId) {
receiverDone = true;
} else if (doneEvent.getSender() === fromUserId) {
senderDone = true;
}
}
if (senderDone && receiverDone) {
this.done = true;
}
}
if (!this.done) {
const cancelRelations = timelineSet.getRelationsForEvent(
this._requestEvent.getId(), "m.reference", "m.key.verification.cancel");
if (cancelRelations) {
let earliestCancelEvent;
for (const cancelEvent of cancelRelations.getRelations()) {
// only accept cancellation from the users involved
if (cancelEvent.getSender() === toUserId || cancelEvent.getSender() === fromUserId) {
this.cancelled = true;
if (!earliestCancelEvent || cancelEvent.getTs() < earliestCancelEvent.getTs()) {
earliestCancelEvent = cancelEvent;
}
}
}
if (earliestCancelEvent) {
this.cancelPartyUserId = earliestCancelEvent.getSender();
}
}
}
this.otherPartyUserId = fromUserId === this._client.getUserId() ? toUserId : fromUserId;
}
}
export function getNameForEventRoom(userId, mxEvent) { export function getNameForEventRoom(userId, mxEvent) {
const roomId = mxEvent.getRoomId(); const roomId = mxEvent.getRoomId();
const client = MatrixClientPeg.get(); const client = MatrixClientPeg.get();