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/_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..98bfa248ff 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 { + 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/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/components/views/messages/MKeyVerificationConclusion.js b/src/components/views/messages/MKeyVerificationConclusion.js new file mode 100644 index 0000000000..0bd8e2d3d8 --- /dev/null +++ b/src/components/views/messages/MKeyVerificationConclusion.js @@ -0,0 +1,132 @@ +/* +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/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/components/views/rooms/EventTile.js b/src/components/views/rooms/EventTile.js index 9497324f5a..786a72f5b3 100644 --- a/src/components/views/rooms/EventTile.js +++ b/src/components/views/rooms/EventTile.js @@ -33,12 +33,15 @@ 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'); 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', @@ -68,6 +71,31 @@ 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"; + } + } + } + // 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]; } @@ -527,8 +555,11 @@ 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); @@ -561,6 +592,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, @@ -596,7 +628,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) { @@ -794,7 +826,7 @@ module.exports = createReactClass({ { readAvatars } { sender } -
+
reacted with %(shortName)s": "reacted with %(shortName)s", 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, diff --git a/src/utils/KeyVerificationStateObserver.js b/src/utils/KeyVerificationStateObserver.js new file mode 100644 index 0000000000..7de50ec4bf --- /dev/null +++ b/src/utils/KeyVerificationStateObserver.js @@ -0,0 +1,170 @@ +/* +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; + } +} + +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; + } +}