diff --git a/src/components/structures/MatrixChat.js b/src/components/structures/MatrixChat.js index 2797887f29..3ac8a93e3d 100644 --- a/src/components/structures/MatrixChat.js +++ b/src/components/structures/MatrixChat.js @@ -63,7 +63,6 @@ import { countRoomsWithNotif } from '../../RoomNotifs'; import { ThemeWatcher } from "../../theme"; import { storeRoomAliasInCache } from '../../RoomAliasCache'; import { defer } from "../../utils/promise"; -import KeyVerificationStateObserver from '../../utils/KeyVerificationStateObserver'; import ToastStore from "../../stores/ToastStore"; /** constants for MatrixChat.state.view */ @@ -1454,18 +1453,13 @@ export default createReactClass({ if (SettingsStore.isFeatureEnabled("feature_cross_signing")) { cli.on("crypto.verification.request", request => { - let requestObserver; - if (request.event.getRoomId()) { - requestObserver = new KeyVerificationStateObserver( - request.event, MatrixClientPeg.get()); - } - - if (!requestObserver || requestObserver.pending) { + console.log(`MatrixChat got a .request ${request.channel.transactionId}`, request.event.getRoomId()); + if (request.pending) { ToastStore.sharedInstance().addOrReplaceToast({ - key: 'verifreq_' + request.event.getId(), + key: 'verifreq_' + request.channel.transactionId, title: _t("Verification Request"), icon: "verification", - props: {request, requestObserver}, + props: {request}, component: sdk.getComponent("toasts.VerificationRequestToast"), }); } diff --git a/src/components/structures/RightPanel.js b/src/components/structures/RightPanel.js index a725b73d9a..dca89d0c35 100644 --- a/src/components/structures/RightPanel.js +++ b/src/components/structures/RightPanel.js @@ -160,6 +160,7 @@ export default class RightPanel extends React.Component { groupId: payload.groupId, member: payload.member, event: payload.event, + verificationRequest: payload.verificationRequest, }); } } @@ -168,6 +169,7 @@ export default class RightPanel extends React.Component { const MemberList = sdk.getComponent('rooms.MemberList'); const MemberInfo = sdk.getComponent('rooms.MemberInfo'); const UserInfo = sdk.getComponent('right_panel.UserInfo'); + const EncryptionPanel = sdk.getComponent('right_panel.EncryptionPanel'); const ThirdPartyMemberInfo = sdk.getComponent('rooms.ThirdPartyMemberInfo'); const NotificationPanel = sdk.getComponent('structures.NotificationPanel'); const FilePanel = sdk.getComponent('structures.FilePanel'); @@ -235,6 +237,8 @@ export default class RightPanel extends React.Component { panel = ; } else if (this.state.phase === RIGHT_PANEL_PHASES.FilePanel) { panel = ; + } else if (this.state.phase === RIGHT_PANEL_PHASES.EncryptionPanel) { + panel = ; } const classes = classNames("mx_RightPanel", "mx_fadable", { diff --git a/src/components/structures/TimelinePanel.js b/src/components/structures/TimelinePanel.js index 68e3e3b3ab..bd13981d1f 100644 --- a/src/components/structures/TimelinePanel.js +++ b/src/components/structures/TimelinePanel.js @@ -1134,7 +1134,7 @@ const TimelinePanel = createReactClass({ const allowPartial = opts.allowPartial || false; const messagePanel = this._messagePanel.current; - if (messagePanel === undefined) return null; + if (!messagePanel) return null; const messagePanelNode = ReactDOM.findDOMNode(messagePanel); if (!messagePanelNode) return null; // sometimes this happens for fresh rooms/post-sync diff --git a/src/components/views/dialogs/DeviceVerifyDialog.js b/src/components/views/dialogs/DeviceVerifyDialog.js index 477190dd4d..67e1649bb9 100644 --- a/src/components/views/dialogs/DeviceVerifyDialog.js +++ b/src/components/views/dialogs/DeviceVerifyDialog.js @@ -24,8 +24,7 @@ import * as sdk from '../../../index'; import * as FormattingUtils from '../../../utils/FormattingUtils'; import { _t } from '../../../languageHandler'; import {verificationMethods} from 'matrix-js-sdk/src/crypto'; -import DMRoomMap from '../../../utils/DMRoomMap'; -import createRoom from "../../../createRoom"; +import {ensureDMExists} from "../../../createRoom"; import dis from "../../../dispatcher"; import SettingsStore from '../../../settings/SettingsStore'; @@ -100,9 +99,15 @@ export default class DeviceVerifyDialog extends React.Component { if (!verifyingOwnDevice && SettingsStore.getValue("feature_cross_signing")) { const roomId = await ensureDMExistsAndOpen(this.props.userId); // throws upon cancellation before having started - this._verifier = await client.requestVerificationDM( + const request = await client.requestVerificationDM( 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 { this._verifier = client.beginKeyVerification( verificationMethods.SAS, this.props.userId, this.props.device.deviceId, @@ -316,23 +321,7 @@ export default class DeviceVerifyDialog extends React.Component { } async function ensureDMExistsAndOpen(userId) { - const client = MatrixClientPeg.get(); - 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}); - } + const roomId = ensureDMExists(MatrixClientPeg.get(), userId); // 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 dis.dispatch({ diff --git a/src/components/views/messages/MKeyVerificationConclusion.js b/src/components/views/messages/MKeyVerificationConclusion.js index f37f270a77..f51b97786b 100644 --- a/src/components/views/messages/MKeyVerificationConclusion.js +++ b/src/components/views/messages/MKeyVerificationConclusion.js @@ -19,102 +19,83 @@ import classNames from 'classnames'; import PropTypes from 'prop-types'; import {MatrixClientPeg} from '../../../MatrixClientPeg'; import { _t } from '../../../languageHandler'; -import KeyVerificationStateObserver, {getNameForEventRoom, userLabelForEventRoom} +import {getNameForEventRoom, userLabelForEventRoom} from '../../../utils/KeyVerificationStateObserver'; export default class MKeyVerificationConclusion extends React.Component { constructor(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() { - if (this.keyVerificationState) { - this.keyVerificationState.attach(); + const request = this.props.mxEvent.verificationRequest; + if (request) { + request.on("change", this._onRequestChanged); } } componentWillUnmount() { - if (this.keyVerificationState) { - this.keyVerificationState.detach(); + const request = this.props.mxEvent.verificationRequest; + if (request) { + request.off("change", this._onRequestChanged); } } - _getName(userId) { - const roomId = this.props.mxEvent.getRoomId(); - const client = MatrixClientPeg.get(); - const room = client.getRoom(roomId); - const member = room.getMember(userId); - return member ? member.name : userId; - } + _onRequestChanged = () => { + this.forceUpdate(); + }; - _userLabel(userId) { - const name = this._getName(userId); - if (name !== userId) { - return _t("%(name)s (%(userId)s)", {name, userId}); - } else { - return userId; + _shouldRender(mxEvent, request) { + // normally should not happen + if (!request) { + return false; } + // .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() { const {mxEvent} = this.props; + const request = mxEvent.verificationRequest; + + if (!this._shouldRender(mxEvent, request)) { + return null; + } + const client = MatrixClientPeg.get(); const myUserId = client.getUserId(); + let title; - if (this.state.done) { - title = _t("You verified %(name)s", {name: getNameForEventRoom(this.state.otherPartyUserId, mxEvent)}); - } else if (this.state.cancelled) { - if (mxEvent.getSender() === myUserId) { + if (request.done) { + title = _t("You verified %(name)s", {name: getNameForEventRoom(request.otherUserId, mxEvent)}); + } else if (request.cancelled) { + const userId = request.cancellingUserId; + if (userId === myUserId) { title = _t("You cancelled verifying %(name)s", - {name: getNameForEventRoom(this.state.otherPartyUserId, mxEvent)}); - } else if (mxEvent.getSender() === this.state.otherPartyUserId) { + {name: getNameForEventRoom(request.otherUserId, mxEvent)}); + } else { title = _t("%(name)s cancelled verifying", - {name: getNameForEventRoom(this.state.otherPartyUserId, mxEvent)}); + {name: getNameForEventRoom(userId, mxEvent)}); } } 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", { - mx_KeyVerification_icon_verified: this.state.done, + mx_KeyVerification_icon_verified: request.done, }); return (
{title}
diff --git a/src/components/views/messages/MKeyVerificationRequest.js b/src/components/views/messages/MKeyVerificationRequest.js index 69cc25dddc..ae793556d8 100644 --- a/src/components/views/messages/MKeyVerificationRequest.js +++ b/src/components/views/messages/MKeyVerificationRequest.js @@ -17,48 +17,66 @@ limitations under the License. import React from 'react'; import PropTypes from 'prop-types'; import {MatrixClientPeg} from '../../../MatrixClientPeg'; -import {verificationMethods} from 'matrix-js-sdk/src/crypto'; import * as sdk from '../../../index'; -import Modal from "../../../Modal"; import { _t } from '../../../languageHandler'; -import KeyVerificationStateObserver, {getNameForEventRoom, userLabelForEventRoom} +import {getNameForEventRoom, userLabelForEventRoom} from '../../../utils/KeyVerificationStateObserver'; +import dis from "../../../dispatcher"; +import {RIGHT_PANEL_PHASES} from "../../../stores/RightPanelStorePhases"; export default class MKeyVerificationRequest extends React.Component { constructor(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() { - this.keyVerificationState.attach(); + const request = this.props.mxEvent.verificationRequest; + if (request) { + request.on("change", this._onRequestChanged); + } } componentWillUnmount() { - this.keyVerificationState.detach(); + const request = this.props.mxEvent.verificationRequest; + if (request) { + request.off("change", this._onRequestChanged); + } } - _onAcceptClicked = () => { - const IncomingSasDialog = sdk.getComponent('views.dialogs.IncomingSasDialog'); - // todo: validate event, for example if it has sas in the methods. - const verifier = MatrixClientPeg.get().acceptVerificationDM(this.props.mxEvent, verificationMethods.SAS); - Modal.createTrackedDialog('Incoming Verification', '', IncomingSasDialog, { - verifier, - }, null, /* priority = */ false, /* static = */ true); + _openRequest = () => { + const {verificationRequest} = this.props.mxEvent; + dis.dispatch({ + action: "set_right_panel_phase", + phase: RIGHT_PANEL_PHASES.EncryptionPanel, + refireParams: {verificationRequest}, + }); }; - _onRejectClicked = () => { - // todo: validate event, for example if it has sas in the methods. - const verifier = MatrixClientPeg.get().acceptVerificationDM(this.props.mxEvent, verificationMethods.SAS); - verifier.cancel("User declined"); + _onRequestChanged = () => { + this.forceUpdate(); + }; + + _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) { @@ -82,46 +100,49 @@ export default class MKeyVerificationRequest extends React.Component { } render() { + const AccessibleButton = sdk.getComponent("elements.AccessibleButton"); + const FormButton = sdk.getComponent("elements.FormButton"); + const {mxEvent} = this.props; - const fromUserId = mxEvent.getSender(); - const content = mxEvent.getContent(); - const toUserId = content.to; - const client = MatrixClientPeg.get(); - const myUserId = client.getUserId(); - const isOwn = fromUserId === myUserId; + const request = mxEvent.verificationRequest; + + if (!request || request.invalid) { + return null; + } let title; let subtitle; let stateNode; - if (this.state.accepted || this.state.cancelled) { + const accepted = request.ready || request.started || request.done; + if (accepted || request.cancelled) { let stateLabel; - if (this.state.accepted) { - stateLabel = this._acceptedLabel(toUserId); - } else if (this.state.cancelled) { - stateLabel = this._cancelledLabel(this.state.cancelPartyUserId); + if (accepted) { + stateLabel = ( + {this._acceptedLabel(request.receivingUserId)} + ); + } else { + stateLabel = this._cancelledLabel(request.cancellingUserId); } stateNode = (
{stateLabel}
); } - if (toUserId === myUserId) { // request sent to us + if (!request.initiatedByMe) { title = (
{ - _t("%(name)s wants to verify", {name: getNameForEventRoom(fromUserId, mxEvent)})}
); + _t("%(name)s wants to verify", {name: getNameForEventRoom(request.requestingUserId, mxEvent)})}
); subtitle = (
{ - userLabelForEventRoom(fromUserId, mxEvent)}
); - const isResolved = !(this.state.accepted || this.state.cancelled || this.state.done); - if (isResolved) { - const FormButton = sdk.getComponent("elements.FormButton"); + userLabelForEventRoom(request.requestingUserId, mxEvent)}); + if (request.requested && !request.observeOnly) { stateNode = (
); } - } else if (isOwn) { // request sent by us + } else { // request sent by us title = (
{ _t("You sent a verification request")}
); subtitle = (
{ - userLabelForEventRoom(this.state.otherPartyUserId, mxEvent)}
); + userLabelForEventRoom(request.receivingUserId, mxEvent)}); } if (title) { diff --git a/src/components/views/right_panel/EncryptionInfo.js b/src/components/views/right_panel/EncryptionInfo.js new file mode 100644 index 0000000000..5770e9b086 --- /dev/null +++ b/src/components/views/right_panel/EncryptionInfo.js @@ -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 (
+

{_t("Verify User")}

+

{_t("For extra security, verify this user by checking a one-time code on both of your devices.")}

+

{_t("For maximum security, do this in person.")}

+ {_t("Start Verification")} +
); + } +} diff --git a/src/components/views/right_panel/EncryptionPanel.js b/src/components/views/right_panel/EncryptionPanel.js new file mode 100644 index 0000000000..4b3473935a --- /dev/null +++ b/src/components/views/right_panel/EncryptionPanel.js @@ -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 ; + } else if (member) { + return ; + } else { + return

Not a member nor request, not sure what to render

; + } + } + + _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}); + }; +} diff --git a/src/components/views/right_panel/RoomHeaderButtons.js b/src/components/views/right_panel/RoomHeaderButtons.js index 449ab3d686..a7accc23d8 100644 --- a/src/components/views/right_panel/RoomHeaderButtons.js +++ b/src/components/views/right_panel/RoomHeaderButtons.js @@ -28,6 +28,7 @@ import RightPanelStore from "../../../stores/RightPanelStore"; const MEMBER_PHASES = [ RIGHT_PANEL_PHASES.RoomMemberList, RIGHT_PANEL_PHASES.RoomMemberInfo, + RIGHT_PANEL_PHASES.EncryptionPanel, RIGHT_PANEL_PHASES.Room3pidMemberInfo, ]; diff --git a/src/components/views/right_panel/UserInfo.js b/src/components/views/right_panel/UserInfo.js index adeedc7884..5f7de42368 100644 --- a/src/components/views/right_panel/UserInfo.js +++ b/src/components/views/right_panel/UserInfo.js @@ -40,6 +40,7 @@ import E2EIcon from "../rooms/E2EIcon"; import {useEventEmitter} from "../../../hooks/useEventEmitter"; import {textualPowerLevel} from '../../../Roles'; import MatrixClientContext from "../../../contexts/MatrixClientContext"; +import {RIGHT_PANEL_PHASES} from "../../../stores/RightPanelStorePhases"; const _disambiguateDevices = (devices) => { const names = Object.create(null); @@ -117,6 +118,14 @@ function verifyDevice(userId, device) { }, 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}) { const cli = useContext(MatrixClientContext); const deviceTrust = cli.checkDeviceTrust(userId, device.deviceId); @@ -1225,15 +1234,13 @@ const UserInfo = ({user, groupId, roomId, onClose}) => { setDevices(null); } } - if (isRoomEncrypted) { - _downloadDeviceList(); - } + _downloadDeviceList(); // Handle being unmounted return () => { cancelled = true; }; - }, [cli, user.userId, isRoomEncrypted]); + }, [cli, user.userId]); // Listen to changes useEffect(() => { @@ -1249,18 +1256,13 @@ const UserInfo = ({user, groupId, roomId, onClose}) => { }); } }; - - if (isRoomEncrypted) { - cli.on("deviceVerificationChanged", onDeviceVerificationChanged); - } + cli.on("deviceVerificationChanged", onDeviceVerificationChanged); // Handle being unmounted return () => { cancel = true; - if (isRoomEncrypted) { - cli.removeListener("deviceVerificationChanged", onDeviceVerificationChanged); - } + cli.removeListener("deviceVerificationChanged", onDeviceVerificationChanged); }; - }, [cli, user.userId, isRoomEncrypted]); + }, [cli, user.userId]); let text; if (!isRoomEncrypted) { @@ -1275,22 +1277,24 @@ const UserInfo = ({user, groupId, roomId, onClose}) => { text = _t("Messages in this room are end-to-end encrypted."); } - const devicesSection = isRoomEncrypted ? - () : null; - const userVerified = cli.checkUserTrust(user.userId).isVerified(); + const isMe = user.userId === cli.getUserId(); let verifyButton; - if (!userVerified) { - verifyButton = verifyDevice(user.userId, null)}> + if (!userVerified && !isMe) { + verifyButton = verifyUser(user)}> {_t("Verify")} ; } + const devicesSection = ; + const securitySection = (

{ _t("Security") }

{ text }

- {verifyButton} + { verifyButton } { devicesSection }
); @@ -1308,7 +1312,7 @@ const UserInfo = ({user, groupId, roomId, onClose}) => {
-
+

{ e2eIcon } { displayName } diff --git a/src/components/views/right_panel/VerificationPanel.js b/src/components/views/right_panel/VerificationPanel.js new file mode 100644 index 0000000000..0d28e1568f --- /dev/null +++ b/src/components/views/right_panel/VerificationPanel.js @@ -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
+
+ { this.renderStatus() } +
+
; + } + + renderStatus() { + const AccessibleButton = sdk.getComponent('elements.AccessibleButton'); + const Spinner = sdk.getComponent('elements.Spinner'); + const {request} = this.props; + + if (request.requested) { + return (

Waiting for {request.otherUserId} to accept ...

); + } else if (request.ready) { + const verifyButton = + Verify by emoji + ; + return (

{request.otherUserId} is ready, start {verifyButton}

); + } else if (request.started) { + if (this.state.sasWaitingForOtherParty) { + return

Waiting for {request.otherUserId} to confirm ...

; + } else if (this.state.sasEvent) { + const VerificationShowSas = sdk.getComponent('views.verification.VerificationShowSas'); + return (
+ +
); + } else { + return (

Setting up SAS verification...

); + } + } else if (request.done) { + return

verified {request.otherUserId}!!

; + } else if (request.cancelled) { + return

cancelled by {request.cancellingUserId}!

; + } + } + + _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); + } +} diff --git a/src/components/views/toasts/VerificationRequestToast.js b/src/components/views/toasts/VerificationRequestToast.js index 18db5eae66..479a3e3f93 100644 --- a/src/components/views/toasts/VerificationRequestToast.js +++ b/src/components/views/toasts/VerificationRequestToast.js @@ -18,55 +18,40 @@ import React from 'react'; import PropTypes from 'prop-types'; import * as sdk from "../../../index"; import { _t } from '../../../languageHandler'; -import Modal from "../../../Modal"; import {MatrixClientPeg} from '../../../MatrixClientPeg'; -import {verificationMethods} from 'matrix-js-sdk/src/crypto'; -import KeyVerificationStateObserver, {userLabelForEventRoom} from "../../../utils/KeyVerificationStateObserver"; +import {RIGHT_PANEL_PHASES} from "../../../stores/RightPanelStorePhases"; +import {userLabelForEventRoom} from "../../../utils/KeyVerificationStateObserver"; import dis from "../../../dispatcher"; import ToastStore from "../../../stores/ToastStore"; export default class VerificationRequestToast extends React.PureComponent { constructor(props) { super(props); - const {event, timeout} = props.request; - // 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); - } + this.state = {counter: Math.ceil(props.request.timeout / 1000)}; } componentDidMount() { - if (this.props.requestObserver) { - this.props.requestObserver.attach(); - this._checkRequestIsPending(); - } + const {request} = this.props; this._intervalHandle = setInterval(() => { let {counter} = this.state; - counter -= 1; - if (counter <= 0) { - this.cancel(); - } else { - this.setState({counter}); - } + counter = Math.max(0, counter - 1); + this.setState({counter}); }, 1000); + request.on("change", this._checkRequestIsPending); } componentWillUnmount() { clearInterval(this._intervalHandle); - if (this.props.requestObserver) { - this.props.requestObserver.detach(); - } + const {request} = this.props; + request.off("change", this._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); } - } + }; cancel = () => { 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); - const {event} = this.props.request; + const {request} = this.props; + const {event} = request; // no room id for to_device requests if (event.getRoomId()) { dis.dispatch({ @@ -88,18 +74,23 @@ export default class VerificationRequestToast extends React.PureComponent { should_peek: false, }); } - - const verifier = this.props.request.beginKeyVerification(verificationMethods.SAS); - const IncomingSasDialog = sdk.getComponent('views.dialogs.IncomingSasDialog'); - Modal.createTrackedDialog('Incoming Verification', '', IncomingSasDialog, { - verifier, - }, null, /* priority = */ false, /* static = */ true); + try { + await request.accept(); + dis.dispatch({ + action: "set_right_panel_phase", + phase: RIGHT_PANEL_PHASES.EncryptionPanel, + refireParams: {verificationRequest: request}, + }); + } catch (err) { + console.error(err.message); + } }; render() { const FormButton = sdk.getComponent("elements.FormButton"); - const {event} = this.props.request; - const userId = event.getSender(); + const {request} = this.props; + const {event} = request; + const userId = request.otherUserId; let nameLabel = event.getRoomId() ? userLabelForEventRoom(userId, event) : userId; // for legacy to_device verification requests if (nameLabel === userId) { @@ -121,6 +112,5 @@ export default class VerificationRequestToast extends React.PureComponent { VerificationRequestToast.propTypes = { request: PropTypes.object.isRequired, - requestObserver: PropTypes.instanceOf(KeyVerificationStateObserver), toastKey: PropTypes.string.isRequired, }; diff --git a/src/createRoom.js b/src/createRoom.js index 867d6409eb..cde9e8b03e 100644 --- a/src/createRoom.js +++ b/src/createRoom.js @@ -1,5 +1,6 @@ /* Copyright 2015, 2016 OpenMarket Ltd +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. @@ -20,7 +21,7 @@ import * as sdk from './index'; import { _t } from './languageHandler'; import dis from "./dispatcher"; import * as Rooms from "./Rooms"; - +import DMRoomMap from "./utils/DMRoomMap"; import {getAddressType} from "./UserAddress"; /** @@ -139,3 +140,23 @@ export default function createRoom(opts) { 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; +} diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 4af203177c..5f1886cd56 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -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.", "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.", + "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", "Files": "Files", "Trusted": "Trusted", diff --git a/src/stores/RightPanelStore.js b/src/stores/RightPanelStore.js index 02775b847b..1b3cb3d64b 100644 --- a/src/stores/RightPanelStore.js +++ b/src/stores/RightPanelStore.js @@ -123,7 +123,11 @@ export default class RightPanelStore extends Store { if (payload.action === 'view_room' || payload.action === 'view_group') { // 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)) { this._setState({lastRoomPhase: RIGHT_PANEL_PHASES.RoomMemberList, lastRoomPhaseParams: {}}); } diff --git a/src/stores/RightPanelStorePhases.js b/src/stores/RightPanelStorePhases.js index 96807ebf5b..d9af320233 100644 --- a/src/stores/RightPanelStorePhases.js +++ b/src/stores/RightPanelStorePhases.js @@ -21,8 +21,9 @@ export const RIGHT_PANEL_PHASES = Object.freeze({ FilePanel: 'FilePanel', NotificationPanel: 'NotificationPanel', RoomMemberInfo: 'RoomMemberInfo', - Room3pidMemberInfo: 'Room3pidMemberInfo', + EncryptionPanel: 'EncryptionPanel', + Room3pidMemberInfo: 'Room3pidMemberInfo', // Group stuff GroupMemberList: 'GroupMemberList', GroupRoomList: 'GroupRoomList', diff --git a/src/utils/KeyVerificationStateObserver.js b/src/utils/KeyVerificationStateObserver.js index d84a5f584e..1a35319186 100644 --- a/src/utils/KeyVerificationStateObserver.js +++ b/src/utils/KeyVerificationStateObserver.js @@ -17,153 +17,6 @@ limitations under the License. import {MatrixClientPeg} from '../MatrixClientPeg'; 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) { const roomId = mxEvent.getRoomId(); const client = MatrixClientPeg.get();