From 9c4470e599006e15214d45267c1e551026dbc6ed Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Thu, 7 Nov 2019 17:36:16 +0100 Subject: [PATCH 01/12] helper class to track the state of the verification as we will have 2 tiles, and both need to track the status of the verification request, I've put the logic for tracking the state in this helper class to use from both tiles. --- src/utils/KeyVerificationStateObserver.js | 153 ++++++++++++++++++++++ 1 file changed, 153 insertions(+) create mode 100644 src/utils/KeyVerificationStateObserver.js diff --git a/src/utils/KeyVerificationStateObserver.js b/src/utils/KeyVerificationStateObserver.js new file mode 100644 index 0000000000..b049b5d426 --- /dev/null +++ b/src/utils/KeyVerificationStateObserver.js @@ -0,0 +1,153 @@ +/* +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 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(); + } + + 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(); + }; + + _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; + } +} From 5c9e80a0ba12478e6358487538dc2ced2d2b2f8a Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Thu, 7 Nov 2019 17:38:52 +0100 Subject: [PATCH 02/12] add feature flag and send verification using DM from dialog if enabled --- .../views/dialogs/DeviceVerifyDialog.js | 59 ++++++++++++++++--- src/i18n/strings/en_EN.json | 1 + src/settings/Settings.js | 6 ++ 3 files changed, 58 insertions(+), 8 deletions(-) diff --git a/src/components/views/dialogs/DeviceVerifyDialog.js b/src/components/views/dialogs/DeviceVerifyDialog.js index 710a92aa39..0e191cc192 100644 --- a/src/components/views/dialogs/DeviceVerifyDialog.js +++ b/src/components/views/dialogs/DeviceVerifyDialog.js @@ -24,6 +24,10 @@ import sdk from '../../../index'; import * as FormattingUtils from '../../../utils/FormattingUtils'; import { _t } from '../../../languageHandler'; import {verificationMethods} from 'matrix-js-sdk/lib/crypto'; +import DMRoomMap from '../../../utils/DMRoomMap'; +import createRoom from "../../../createRoom"; +import dis from "../../../dispatcher"; +import SettingsStore from '../../../settings/SettingsStore'; const MODE_LEGACY = 'legacy'; const MODE_SAS = 'sas'; @@ -86,25 +90,37 @@ export default class DeviceVerifyDialog extends React.Component { this.props.onFinished(confirm); } - _onSasRequestClick = () => { + _onSasRequestClick = async () => { this.setState({ phase: PHASE_WAIT_FOR_PARTNER_TO_ACCEPT, }); - this._verifier = MatrixClientPeg.get().beginKeyVerification( - verificationMethods.SAS, this.props.userId, this.props.device.deviceId, - ); - this._verifier.on('show_sas', this._onVerifierShowSas); - this._verifier.verify().then(() => { + const client = MatrixClientPeg.get(); + const verifyingOwnDevice = this.props.userId === client.getUserId(); + try { + if (!verifyingOwnDevice && SettingsStore.getValue("feature_dm_verification")) { + const roomId = await ensureDMExistsAndOpen(this.props.userId); + // throws upon cancellation before having started + this._verifier = await client.requestVerificationDM( + this.props.userId, roomId, [verificationMethods.SAS], + ); + } else { + this._verifier = client.beginKeyVerification( + verificationMethods.SAS, this.props.userId, this.props.device.deviceId, + ); + } + this._verifier.on('show_sas', this._onVerifierShowSas); + // throws upon cancellation + await this._verifier.verify(); this.setState({phase: PHASE_VERIFIED}); this._verifier.removeListener('show_sas', this._onVerifierShowSas); this._verifier = null; - }).catch((e) => { + } catch (e) { console.log("Verification failed", e); this.setState({ phase: PHASE_CANCELLED, }); this._verifier = null; - }); + } } _onSasMatchesClick = () => { @@ -299,3 +315,30 @@ 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}); + } + // 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({ + action: 'view_room', + room_id: roomId, + should_peek: false, + }); + return roomId; +} diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 5af7e26b79..bcf68f8e4b 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -339,6 +339,7 @@ "Render simple counters in room header": "Render simple counters in room header", "Multiple integration managers": "Multiple integration managers", "Use the new, consistent UserInfo panel for Room Members and Group Members": "Use the new, consistent UserInfo panel for Room Members and Group Members", + "Send verification requests in direct message": "Send verification requests in direct message", "Use the new, faster, composer for writing messages": "Use the new, faster, composer for writing messages", "Enable Emoji suggestions while typing": "Enable Emoji suggestions while typing", "Use compact timeline layout": "Use compact timeline layout", diff --git a/src/settings/Settings.js b/src/settings/Settings.js index 2220435cb9..b169a0f29c 100644 --- a/src/settings/Settings.js +++ b/src/settings/Settings.js @@ -126,6 +126,12 @@ export const SETTINGS = { supportedLevels: LEVELS_FEATURE, default: false, }, + "feature_dm_verification": { + isFeature: true, + displayName: _td("Send verification requests in direct message"), + supportedLevels: LEVELS_FEATURE, + default: false, + }, "useCiderComposer": { displayName: _td("Use the new, faster, composer for writing messages"), supportedLevels: LEVELS_ACCOUNT_SETTINGS, From 0d2f9c42152c870d93f2259fc587c22d5822dc45 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Thu, 7 Nov 2019 17:39:50 +0100 Subject: [PATCH 03/12] add verification request tile + styling --- res/css/_components.scss | 1 + .../messages/_MKeyVerificationRequest.scss | 93 ++++++++++++ res/css/views/rooms/_EventTile.scss | 25 ++++ res/themes/light/css/_light.scss | 2 + .../views/messages/MKeyVerificationRequest.js | 141 ++++++++++++++++++ src/utils/KeyVerificationStateObserver.js | 17 +++ 6 files changed, 279 insertions(+) create mode 100644 res/css/views/messages/_MKeyVerificationRequest.scss create mode 100644 src/components/views/messages/MKeyVerificationRequest.js diff --git a/res/css/_components.scss b/res/css/_components.scss index 29c4d2c84c..5d26185393 100644 --- a/res/css/_components.scss +++ b/res/css/_components.scss @@ -118,6 +118,7 @@ @import "./views/messages/_MEmoteBody.scss"; @import "./views/messages/_MFileBody.scss"; @import "./views/messages/_MImageBody.scss"; +@import "./views/messages/_MKeyVerificationRequest.scss"; @import "./views/messages/_MNoticeBody.scss"; @import "./views/messages/_MStickerBody.scss"; @import "./views/messages/_MTextBody.scss"; diff --git a/res/css/views/messages/_MKeyVerificationRequest.scss b/res/css/views/messages/_MKeyVerificationRequest.scss new file mode 100644 index 0000000000..aff44e4109 --- /dev/null +++ b/res/css/views/messages/_MKeyVerificationRequest.scss @@ -0,0 +1,93 @@ +/* +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. +*/ + +.mx_KeyVerification { + + display: grid; + grid-template-columns: 24px minmax(0, 1fr) min-content; + + &.mx_KeyVerification_icon::after { + grid-column: 1; + grid-row: 1 / 3; + width: 12px; + height: 16px; + content: ""; + mask: url("$(res)/img/e2e/verified.svg"); + mask-repeat: no-repeat; + mask-size: 100%; + margin-top: 4px; + background-color: $primary-fg-color; + } + + &.mx_KeyVerification_icon_verified::after { + background-color: $accent-color; + } + + .mx_KeyVerification_title, .mx_KeyVerification_subtitle, .mx_KeyVerification_state { + overflow-wrap: break-word; + } + + .mx_KeyVerification_title { + font-weight: 600; + font-size: 15px; + grid-column: 2; + grid-row: 1; + } + + .mx_KeyVerification_subtitle { + grid-column: 2; + grid-row: 2; + } + + .mx_KeyVerification_state, .mx_KeyVerification_subtitle { + font-size: 12px; + } + + .mx_KeyVerification_state, .mx_KeyVerification_buttons { + grid-column: 3; + grid-row: 1 / 3; + } + + .mx_KeyVerification_buttons { + align-items: center; + display: flex; + + .mx_AccessibleButton_kind_decline { + color: $notice-primary-color; + background-color: $notice-primary-bg-color; + } + + .mx_AccessibleButton_kind_accept { + color: $accent-color; + background-color: $accent-bg-color; + } + + [role=button] { + margin: 10px; + padding: 7px 15px; + border-radius: 5px; + height: min-content; + } + } + + .mx_KeyVerification_state { + width: 130px; + padding: 10px 20px; + margin: auto 0; + text-align: center; + color: $notice-secondary-color; + } +} diff --git a/res/css/views/rooms/_EventTile.scss b/res/css/views/rooms/_EventTile.scss index db34200b16..04c1065092 100644 --- a/res/css/views/rooms/_EventTile.scss +++ b/res/css/views/rooms/_EventTile.scss @@ -22,6 +22,15 @@ limitations under the License. position: relative; } +.mx_EventTile_bubble { + background-color: $dark-panel-bg-color; + padding: 10px; + border-radius: 5px; + margin: 10px auto; + max-width: 75%; + box-sizing: border-box; +} + .mx_EventTile.mx_EventTile_info { padding-top: 0px; } @@ -112,6 +121,21 @@ limitations under the License. line-height: 22px; } +.mx_EventTile_bubbleContainer.mx_EventTile_bubbleContainer { + display: grid; + grid-template-columns: 1fr 100px; + + .mx_EventTile_line { + margin-right: 0px; + grid-column: 1 / 3; + padding: 0; + } + + .mx_EventTile_msgOption { + grid-column: 2; + } +} + .mx_EventTile_reply { margin-right: 10px; } @@ -617,4 +641,5 @@ div.mx_EventTile_notSent.mx_EventTile_redacted .mx_UnknownBody { } } } + /* stylelint-enable no-descending-specificity */ diff --git a/res/themes/light/css/_light.scss b/res/themes/light/css/_light.scss index b412261d10..dcd7ce166e 100644 --- a/res/themes/light/css/_light.scss +++ b/res/themes/light/css/_light.scss @@ -12,7 +12,9 @@ $monospace-font-family: Inconsolata, Twemoji, 'Apple Color Emoji', 'Segoe UI Emo // unified palette // try to use these colors when possible $accent-color: #03b381; +$accent-bg-color: rgba(115, 247, 91, 0.08); $notice-primary-color: #ff4b55; +$notice-primary-bg-color: rgba(255, 75, 85, 0.08); $notice-secondary-color: #61708b; $header-panel-bg-color: #f3f8fd; diff --git a/src/components/views/messages/MKeyVerificationRequest.js b/src/components/views/messages/MKeyVerificationRequest.js new file mode 100644 index 0000000000..21d82309ed --- /dev/null +++ b/src/components/views/messages/MKeyVerificationRequest.js @@ -0,0 +1,141 @@ +/* +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 PropTypes from 'prop-types'; +import MatrixClientPeg from '../../../MatrixClientPeg'; +import {verificationMethods} from 'matrix-js-sdk/lib/crypto'; +import sdk from '../../../index'; +import Modal from "../../../Modal"; +import { _t } from '../../../languageHandler'; +import KeyVerificationStateObserver, {getNameForEventRoom, userLabelForEventRoom} + from '../../../utils/KeyVerificationStateObserver'; + +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(); + } + + componentWillUnmount() { + this.keyVerificationState.detach(); + } + + _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, + }); + }; + + _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"); + }; + + _acceptedLabel(userId) { + const client = MatrixClientPeg.get(); + const myUserId = client.getUserId(); + if (userId === myUserId) { + return _t("You accepted"); + } else { + return _t("%(name)s accepted", {name: getNameForEventRoom(userId, this.props.mxEvent)}); + } + } + + _cancelledLabel(userId) { + const client = MatrixClientPeg.get(); + const myUserId = client.getUserId(); + if (userId === myUserId) { + return _t("You cancelled"); + } else { + return _t("%(name)s cancelled", {name: getNameForEventRoom(userId, this.props.mxEvent)}); + } + } + + render() { + 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; + + let title; + let subtitle; + let stateNode; + + if (this.state.accepted || this.state.cancelled) { + let stateLabel; + if (this.state.accepted) { + stateLabel = this._acceptedLabel(toUserId); + } else if (this.state.cancelled) { + stateLabel = this._cancelledLabel(this.state.cancelPartyUserId); + } + stateNode = (
{stateLabel}
); + } + + if (toUserId === myUserId) { // request sent to us + title = (
{ + _t("%(name)s wants to verify", {name: getNameForEventRoom(fromUserId, mxEvent)})}
); + subtitle = (
{ + userLabelForEventRoom(fromUserId, mxEvent)}
); + const isResolved = !(this.state.accepted || this.state.cancelled || this.state.done); + if (isResolved) { + const AccessibleButton = sdk.getComponent("elements.AccessibleButton"); + stateNode = (
+ {_t("Decline")} + {_t("Accept")} +
); + } + } else if (isOwn) { // request sent by us + title = (
{ + _t("You sent a verification request")}
); + subtitle = (
{ + userLabelForEventRoom(this.state.otherPartyUserId, mxEvent)}
); + } + + if (title) { + return (
+ {title} + {subtitle} + {stateNode} +
); + } + return null; + } +} + +MKeyVerificationRequest.propTypes = { + /* the MatrixEvent to show */ + mxEvent: PropTypes.object.isRequired, +}; diff --git a/src/utils/KeyVerificationStateObserver.js b/src/utils/KeyVerificationStateObserver.js index b049b5d426..7de50ec4bf 100644 --- a/src/utils/KeyVerificationStateObserver.js +++ b/src/utils/KeyVerificationStateObserver.js @@ -151,3 +151,20 @@ export default class KeyVerificationStateObserver { this.otherPartyUserId = fromUserId === this._client.getUserId() ? toUserId : fromUserId; } } + +export function getNameForEventRoom(userId, mxEvent) { + const roomId = mxEvent.getRoomId(); + const client = MatrixClientPeg.get(); + const room = client.getRoom(roomId); + const member = room.getMember(userId); + return member ? member.name : userId; +} + +export function userLabelForEventRoom(userId, mxEvent) { + const name = getNameForEventRoom(userId, mxEvent); + if (name !== userId) { + return _t("%(name)s (%(userId)s)", {name, userId}); + } else { + return userId; + } +} From e8c21a341cf8dc96f3f57b8824bf5d78663ad660 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Thu, 7 Nov 2019 17:40:22 +0100 Subject: [PATCH 04/12] add key verification conclusion tile --- .../messages/MKeyVerificationConclusion.js | 130 ++++++++++++++++++ src/i18n/strings/en_EN.json | 10 ++ 2 files changed, 140 insertions(+) create mode 100644 src/components/views/messages/MKeyVerificationConclusion.js diff --git a/src/components/views/messages/MKeyVerificationConclusion.js b/src/components/views/messages/MKeyVerificationConclusion.js new file mode 100644 index 0000000000..e955d6159d --- /dev/null +++ b/src/components/views/messages/MKeyVerificationConclusion.js @@ -0,0 +1,130 @@ +/* +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 classNames from 'classnames'; +import PropTypes from 'prop-types'; +import MatrixClientPeg from '../../../MatrixClientPeg'; +import { _t } from '../../../languageHandler'; +import KeyVerificationStateObserver, {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(); + } + } + + componentWillUnmount() { + if (this.keyVerificationState) { + this.keyVerificationState.detach(); + } + } + + _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; + } + + _userLabel(userId) { + const name = this._getName(userId); + if (name !== userId) { + return _t("%(name)s (%(userId)s)", {name, userId}); + } else { + return userId; + } + } + + render() { + const {mxEvent} = this.props; + 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) { + title = _t("You cancelled verifying %(name)s", {name: getNameForEventRoom(this.state.otherPartyUserId, mxEvent)}); + } else if (mxEvent.getSender() === this.state.otherPartyUserId) { + title = _t("%(name)s cancelled verifying", {name: getNameForEventRoom(this.state.otherPartyUserId, mxEvent)}); + } + } + + if (title) { + const subtitle = userLabelForEventRoom(this.state.otherPartyUserId, mxEvent); + const classes = classNames("mx_EventTile_bubble", "mx_KeyVerification", "mx_KeyVerification_icon", { + mx_KeyVerification_icon_verified: this.state.done, + }); + return (
+
{title}
+
{subtitle}
+
); + } + + return null; + } +} + +MKeyVerificationConclusion.propTypes = { + /* the MatrixEvent to show */ + mxEvent: PropTypes.object.isRequired, +}; diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index bcf68f8e4b..1dcd2a5129 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -1065,6 +1065,16 @@ "Invalid file%(extra)s": "Invalid file%(extra)s", "Error decrypting image": "Error decrypting image", "Show image": "Show image", + "%(name)s (%(userId)s)": "%(name)s (%(userId)s)", + "You verified %(name)s": "You verified %(name)s", + "You cancelled verifying %(name)s": "You cancelled verifying %(name)s", + "%(name)s cancelled verifying": "%(name)s cancelled verifying", + "You accepted": "You accepted", + "%(name)s accepted": "%(name)s accepted", + "You cancelled": "You cancelled", + "%(name)s cancelled": "%(name)s cancelled", + "%(name)s wants to verify": "%(name)s wants to verify", + "You sent a verification request": "You sent a verification request", "Error decrypting video": "Error decrypting video", "Show all": "Show all", "reacted with %(shortName)s": "reacted with %(shortName)s", From 9d67fa9fa185ded789cedb0abbdf7943d7c58f63 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Thu, 7 Nov 2019 17:42:06 +0100 Subject: [PATCH 05/12] render verification request with correct tile only if the request was send by or to us, otherwise ignore. --- src/components/views/rooms/EventTile.js | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/src/components/views/rooms/EventTile.js b/src/components/views/rooms/EventTile.js index 9497324f5a..fca77fcaf6 100644 --- a/src/components/views/rooms/EventTile.js +++ b/src/components/views/rooms/EventTile.js @@ -33,6 +33,7 @@ import dis from '../../../dispatcher'; import SettingsStore from "../../../settings/SettingsStore"; import {EventStatus, MatrixClient} from 'matrix-js-sdk'; import {formatTime} from "../../../DateUtils"; +import MatrixClientPeg from '../../../MatrixClientPeg'; const ObjectUtils = require('../../../ObjectUtils'); @@ -68,6 +69,21 @@ const stateEventTileTypes = { function getHandlerTile(ev) { const type = ev.getType(); + + // don't show verification requests we're not involved in, + // not even when showing hidden events + if (type === "m.room.message") { + const content = ev.getContent(); + if (content && content.msgtype === "m.key.verification.request") { + const client = MatrixClientPeg.get(); + const me = client && client.getUserId(); + if (ev.getSender() !== me && content.to !== me) { + return undefined; + } else { + return "messages.MKeyVerificationRequest"; + } + } + } return ev.isState() ? stateEventTileTypes[type] : eventTileTypes[type]; } From d7f5252f9afa237841f9c78baac9ae33c91230b8 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Thu, 7 Nov 2019 17:43:50 +0100 Subject: [PATCH 06/12] render done and cancel event as conclusion tile don't render any done events not sent by us, as done events are sent by both parties and we don't want to render two conclusion tiles. cancel events should be only sent by one party. --- src/components/views/rooms/EventTile.js | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/src/components/views/rooms/EventTile.js b/src/components/views/rooms/EventTile.js index fca77fcaf6..de13fa30e2 100644 --- a/src/components/views/rooms/EventTile.js +++ b/src/components/views/rooms/EventTile.js @@ -40,6 +40,8 @@ const ObjectUtils = require('../../../ObjectUtils'); const eventTileTypes = { 'm.room.message': 'messages.MessageEvent', 'm.sticker': 'messages.MessageEvent', + 'm.key.verification.cancel': 'messages.MKeyVerificationConclusion', + 'm.key.verification.done': 'messages.MKeyVerificationConclusion', 'm.call.invite': 'messages.TextualEvent', 'm.call.answer': 'messages.TextualEvent', 'm.call.hangup': 'messages.TextualEvent', @@ -84,6 +86,16 @@ function getHandlerTile(ev) { } } } + // these events are sent by both parties during verification, but we only want to render one + // tile once the verification concludes, so filter out the one from the other party. + if (type === "m.key.verification.done") { + const client = MatrixClientPeg.get(); + const me = client && client.getUserId(); + if (ev.getSender() !== me) { + return undefined; + } + } + return ev.isState() ? stateEventTileTypes[type] : eventTileTypes[type]; } From 805c83779a2bf1420fb40118041a197b6f26e828 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Thu, 7 Nov 2019 17:44:53 +0100 Subject: [PATCH 07/12] support bubble tile style for verification tiles --- src/components/views/rooms/EventTile.js | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/src/components/views/rooms/EventTile.js b/src/components/views/rooms/EventTile.js index de13fa30e2..7105ee2635 100644 --- a/src/components/views/rooms/EventTile.js +++ b/src/components/views/rooms/EventTile.js @@ -555,8 +555,10 @@ module.exports = createReactClass({ const eventType = this.props.mxEvent.getType(); // Info messages are basically information about commands processed on a room + const isBubbleMessage = eventType.startsWith("m.key.verification") || + (eventType === "m.room.message" && msgtype.startsWith("m.key.verification")); let isInfoMessage = ( - eventType !== 'm.room.message' && eventType !== 'm.sticker' && eventType != 'm.room.create' + !isBubbleMessage && eventType !== 'm.room.message' && eventType !== 'm.sticker' && eventType != 'm.room.create' ); let tileHandler = getHandlerTile(this.props.mxEvent); @@ -589,6 +591,7 @@ module.exports = createReactClass({ const isEditing = !!this.props.editState; const classes = classNames({ + mx_EventTile_bubbleContainer: isBubbleMessage, mx_EventTile: true, mx_EventTile_isEditing: isEditing, mx_EventTile_info: isInfoMessage, @@ -624,7 +627,7 @@ module.exports = createReactClass({ if (this.props.tileShape === "notif") { avatarSize = 24; needsSenderProfile = true; - } else if (tileHandler === 'messages.RoomCreate') { + } else if (tileHandler === 'messages.RoomCreate' || isBubbleMessage) { avatarSize = 0; needsSenderProfile = false; } else if (isInfoMessage) { @@ -822,7 +825,7 @@ module.exports = createReactClass({ { readAvatars } { sender } -
+
Date: Thu, 7 Nov 2019 20:01:33 +0100 Subject: [PATCH 08/12] string has moved in i18n apparently --- src/i18n/strings/en_EN.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 1dcd2a5129..5f6e327944 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -291,6 +291,7 @@ "%(items)s and %(count)s others|other": "%(items)s and %(count)s others", "%(items)s and %(count)s others|one": "%(items)s and one other", "%(items)s and %(lastItem)s": "%(items)s and %(lastItem)s", + "%(name)s (%(userId)s)": "%(name)s (%(userId)s)", "Your browser does not support the required cryptography extensions": "Your browser does not support the required cryptography extensions", "Not a valid Riot keyfile": "Not a valid Riot keyfile", "Authentication check failed: incorrect password?": "Authentication check failed: incorrect password?", @@ -1065,7 +1066,6 @@ "Invalid file%(extra)s": "Invalid file%(extra)s", "Error decrypting image": "Error decrypting image", "Show image": "Show image", - "%(name)s (%(userId)s)": "%(name)s (%(userId)s)", "You verified %(name)s": "You verified %(name)s", "You cancelled verifying %(name)s": "You cancelled verifying %(name)s", "%(name)s cancelled verifying": "%(name)s cancelled verifying", From d83f3632f6f39f7f6d017dc2d54a3e514aea3434 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Thu, 7 Nov 2019 20:04:36 +0100 Subject: [PATCH 09/12] make the linter happy --- src/components/views/messages/MKeyVerificationConclusion.js | 6 ++++-- src/components/views/rooms/EventTile.js | 3 ++- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/src/components/views/messages/MKeyVerificationConclusion.js b/src/components/views/messages/MKeyVerificationConclusion.js index e955d6159d..0bd8e2d3d8 100644 --- a/src/components/views/messages/MKeyVerificationConclusion.js +++ b/src/components/views/messages/MKeyVerificationConclusion.js @@ -103,9 +103,11 @@ export default class MKeyVerificationConclusion extends React.Component { title = _t("You verified %(name)s", {name: getNameForEventRoom(this.state.otherPartyUserId, mxEvent)}); } else if (this.state.cancelled) { if (mxEvent.getSender() === myUserId) { - title = _t("You cancelled verifying %(name)s", {name: getNameForEventRoom(this.state.otherPartyUserId, mxEvent)}); + title = _t("You cancelled verifying %(name)s", + {name: getNameForEventRoom(this.state.otherPartyUserId, mxEvent)}); } else if (mxEvent.getSender() === this.state.otherPartyUserId) { - title = _t("%(name)s cancelled verifying", {name: getNameForEventRoom(this.state.otherPartyUserId, mxEvent)}); + title = _t("%(name)s cancelled verifying", + {name: getNameForEventRoom(this.state.otherPartyUserId, mxEvent)}); } } diff --git a/src/components/views/rooms/EventTile.js b/src/components/views/rooms/EventTile.js index 7105ee2635..786a72f5b3 100644 --- a/src/components/views/rooms/EventTile.js +++ b/src/components/views/rooms/EventTile.js @@ -558,7 +558,8 @@ module.exports = createReactClass({ const isBubbleMessage = eventType.startsWith("m.key.verification") || (eventType === "m.room.message" && msgtype.startsWith("m.key.verification")); let isInfoMessage = ( - !isBubbleMessage && eventType !== 'm.room.message' && eventType !== 'm.sticker' && eventType != 'm.room.create' + !isBubbleMessage && eventType !== 'm.room.message' && + eventType !== 'm.sticker' && eventType != 'm.room.create' ); let tileHandler = getHandlerTile(this.props.mxEvent); From 2516d8ee61de0a9f76ee4c44fc8084de2b7befa4 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Thu, 7 Nov 2019 20:11:31 +0100 Subject: [PATCH 10/12] fix repeated css class --- res/css/views/rooms/_EventTile.scss | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/res/css/views/rooms/_EventTile.scss b/res/css/views/rooms/_EventTile.scss index 04c1065092..98bfa248ff 100644 --- a/res/css/views/rooms/_EventTile.scss +++ b/res/css/views/rooms/_EventTile.scss @@ -121,7 +121,7 @@ limitations under the License. line-height: 22px; } -.mx_EventTile_bubbleContainer.mx_EventTile_bubbleContainer { +.mx_EventTile_bubbleContainer { display: grid; grid-template-columns: 1fr 100px; From 4283f9ec74b50748ebcc18360c68dab5e0c86eae Mon Sep 17 00:00:00 2001 From: David Baker Date: Fri, 8 Nov 2019 16:01:53 +0000 Subject: [PATCH 11/12] Split CSS rule to fix descending specificity lint error --- res/css/views/rooms/_EventTile.scss | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/res/css/views/rooms/_EventTile.scss b/res/css/views/rooms/_EventTile.scss index 98bfa248ff..81924d2be3 100644 --- a/res/css/views/rooms/_EventTile.scss +++ b/res/css/views/rooms/_EventTile.scss @@ -130,10 +130,6 @@ limitations under the License. grid-column: 1 / 3; padding: 0; } - - .mx_EventTile_msgOption { - grid-column: 2; - } } .mx_EventTile_reply { @@ -278,6 +274,10 @@ div.mx_EventTile_notSent.mx_EventTile_redacted .mx_UnknownBody { margin-right: 10px; } +.mx_EventTile_bubbleContainer.mx_EventTile_msgOption { + grid-column: 2; +} + .mx_EventTile_msgOption a { text-decoration: none; } From 3070ee6d7b370ae13f9d0d2b6e7f759daf83bb0a Mon Sep 17 00:00:00 2001 From: David Baker Date: Fri, 8 Nov 2019 16:10:51 +0000 Subject: [PATCH 12/12] Put back the grouped rule & disable the linting rule instead --- .stylelintrc.js | 1 + res/css/views/rooms/_EventTile.scss | 8 ++++---- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/.stylelintrc.js b/.stylelintrc.js index f028c76cc0..1690f2186f 100644 --- a/.stylelintrc.js +++ b/.stylelintrc.js @@ -15,6 +15,7 @@ module.exports = { "number-leading-zero": null, "selector-list-comma-newline-after": null, "at-rule-no-unknown": null, + "no-descending-specificity": null, "scss/at-rule-no-unknown": [true, { // https://github.com/vector-im/riot-web/issues/10544 "ignoreAtRules": ["define-mixin"], diff --git a/res/css/views/rooms/_EventTile.scss b/res/css/views/rooms/_EventTile.scss index 81924d2be3..98bfa248ff 100644 --- a/res/css/views/rooms/_EventTile.scss +++ b/res/css/views/rooms/_EventTile.scss @@ -130,6 +130,10 @@ limitations under the License. grid-column: 1 / 3; padding: 0; } + + .mx_EventTile_msgOption { + grid-column: 2; + } } .mx_EventTile_reply { @@ -274,10 +278,6 @@ div.mx_EventTile_notSent.mx_EventTile_redacted .mx_UnknownBody { margin-right: 10px; } -.mx_EventTile_bubbleContainer.mx_EventTile_msgOption { - grid-column: 2; -} - .mx_EventTile_msgOption a { text-decoration: none; }