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 (
);
+ }
+
+ 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;
+ }
+}