mirror of
https://github.com/element-hq/element-web
synced 2024-11-27 19:56:47 +03:00
Merge pull request #3796 from matrix-org/bwindels/verification-right-panel
Initial support for verification in right panel
This commit is contained in:
commit
95a0ebaf06
17 changed files with 411 additions and 346 deletions
|
@ -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"),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
@ -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", {
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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({
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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) {
|
||||||
|
|
31
src/components/views/right_panel/EncryptionInfo.js
Normal file
31
src/components/views/right_panel/EncryptionInfo.js
Normal 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>);
|
||||||
|
}
|
||||||
|
}
|
48
src/components/views/right_panel/EncryptionPanel.js
Normal file
48
src/components/views/right_panel/EncryptionPanel.js
Normal 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});
|
||||||
|
};
|
||||||
|
}
|
|
@ -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,
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|
|
@ -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 }
|
||||||
|
|
119
src/components/views/right_panel/VerificationPanel.js
Normal file
119
src/components/views/right_panel/VerificationPanel.js
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
|
@ -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,
|
||||||
};
|
};
|
||||||
|
|
|
@ -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;
|
||||||
|
}
|
||||||
|
|
|
@ -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",
|
||||||
|
|
|
@ -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: {}});
|
||||||
}
|
}
|
||||||
|
|
|
@ -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',
|
||||||
|
|
|
@ -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();
|
||||||
|
|
Loading…
Reference in a new issue