Merge branch 'develop' of https://github.com/matrix-org/matrix-react-sdk into t3chguy/roving

This commit is contained in:
Michael Telatynski 2020-01-20 20:48:11 +00:00
commit 397e116efb
68 changed files with 1948 additions and 626 deletions

View file

@ -83,7 +83,7 @@ export default createReactClass({
if (viewUserOnClick) {
onClick = () => {
dispatcher.dispatch({
dis.dispatch({
action: 'view_user',
member: this.props.member,
});

View file

@ -63,7 +63,7 @@ export default class MemberStatusMessageAvatar extends React.Component {
user.on("User._unstable_statusMessage", this._onStatusMessageCommitted);
}
componentWillUmount() {
componentWillUnmount() {
const { user } = this.props.member;
if (!user) {
return;

View file

@ -23,6 +23,7 @@ import * as sdk from '../../../index';
import SdkConfig from '../../../SdkConfig';
import Modal from '../../../Modal';
import { _t } from '../../../languageHandler';
import sendBugReport from '../../../rageshake/submit-rageshake';
export default class BugReportDialog extends React.Component {
constructor(props) {
@ -67,32 +68,30 @@ export default class BugReportDialog extends React.Component {
this.setState({ busy: true, progress: null, err: null });
this._sendProgressCallback(_t("Preparing to send logs"));
require(['../../../rageshake/submit-rageshake'], (s) => {
s(SdkConfig.get().bug_report_endpoint_url, {
userText,
sendLogs: true,
progressCallback: this._sendProgressCallback,
label: this.props.label,
}).then(() => {
if (!this._unmounted) {
this.props.onFinished(false);
const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog");
// N.B. first param is passed to piwik and so doesn't want i18n
Modal.createTrackedDialog('Bug report sent', '', QuestionDialog, {
title: _t('Logs sent'),
description: _t('Thank you!'),
hasCancelButton: false,
});
}
}, (err) => {
if (!this._unmounted) {
this.setState({
busy: false,
progress: null,
err: _t("Failed to send logs: ") + `${err.message}`,
});
}
});
sendBugReport(SdkConfig.get().bug_report_endpoint_url, {
userText,
sendLogs: true,
progressCallback: this._sendProgressCallback,
label: this.props.label,
}).then(() => {
if (!this._unmounted) {
this.props.onFinished(false);
const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog");
// N.B. first param is passed to piwik and so doesn't want i18n
Modal.createTrackedDialog('Bug report sent', '', QuestionDialog, {
title: _t('Logs sent'),
description: _t('Thank you!'),
hasCancelButton: false,
});
}
}, (err) => {
if (!this._unmounted) {
this.setState({
busy: false,
progress: null,
err: _t("Failed to send logs: ") + `${err.message}`,
});
}
});
}

View file

@ -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({

View file

@ -19,7 +19,7 @@ import PropTypes from 'prop-types';
import {_t} from "../../../languageHandler";
import * as sdk from "../../../index";
import {MatrixClientPeg} from "../../../MatrixClientPeg";
import {makeUserPermalink} from "../../../utils/permalinks/Permalinks";
import {makeRoomPermalink, makeUserPermalink} from "../../../utils/permalinks/Permalinks";
import DMRoomMap from "../../../utils/DMRoomMap";
import {RoomMember} from "matrix-js-sdk/src/matrix";
import SdkConfig from "../../../SdkConfig";
@ -31,8 +31,11 @@ import dis from "../../../dispatcher";
import IdentityAuthClient from "../../../IdentityAuthClient";
import Modal from "../../../Modal";
import {humanizeTime} from "../../../utils/humanize";
import createRoom from "../../../createRoom";
import {inviteMultipleToRoom} from "../../../RoomInvite";
// TODO: [TravisR] Make this generic for all kinds of invites
export const KIND_DM = "dm";
export const KIND_INVITE = "invite";
const INITIAL_ROOMS_SHOWN = 3; // Number of rooms to show at first
const INCREMENT_ROOMS_SHOWN = 5; // Number of rooms to add when 'show more' is clicked
@ -138,11 +141,11 @@ class DMUserTile extends React.PureComponent {
const avatarSize = 20;
const avatar = this.props.member.isEmail
? <img
className='mx_DMInviteDialog_userTile_avatar mx_DMInviteDialog_userTile_threepidAvatar'
className='mx_InviteDialog_userTile_avatar mx_InviteDialog_userTile_threepidAvatar'
src={require("../../../../res/img/icon-email-pill-avatar.svg")}
width={avatarSize} height={avatarSize} />
: <BaseAvatar
className='mx_DMInviteDialog_userTile_avatar'
className='mx_InviteDialog_userTile_avatar'
url={getHttpUriForMxc(
MatrixClientPeg.get().getHomeserverUrl(), this.props.member.getMxcAvatarUrl(),
avatarSize, avatarSize, "crop")}
@ -152,13 +155,13 @@ class DMUserTile extends React.PureComponent {
height={avatarSize} />;
return (
<span className='mx_DMInviteDialog_userTile'>
<span className='mx_DMInviteDialog_userTile_pill'>
<span className='mx_InviteDialog_userTile'>
<span className='mx_InviteDialog_userTile_pill'>
{avatar}
<span className='mx_DMInviteDialog_userTile_name'>{this.props.member.name}</span>
<span className='mx_InviteDialog_userTile_name'>{this.props.member.name}</span>
</span>
<AccessibleButton
className='mx_DMInviteDialog_userTile_remove'
className='mx_InviteDialog_userTile_remove'
onClick={this._onRemove}
>
<img src={require("../../../../res/img/icon-pill-remove.svg")} alt={_t('Remove')} width={8} height={8} />
@ -209,7 +212,7 @@ class DMRoomTile extends React.PureComponent {
// Highlight the word the user entered
const substr = str.substring(i, filterStr.length + i);
result.push(<span className='mx_DMInviteDialog_roomTile_highlight' key={i + 'bold'}>{substr}</span>);
result.push(<span className='mx_InviteDialog_roomTile_highlight' key={i + 'bold'}>{substr}</span>);
i += substr.length;
}
@ -227,7 +230,7 @@ class DMRoomTile extends React.PureComponent {
let timestamp = null;
if (this.props.lastActiveTs) {
const humanTs = humanizeTime(this.props.lastActiveTs);
timestamp = <span className='mx_DMInviteDialog_roomTile_time'>{humanTs}</span>;
timestamp = <span className='mx_InviteDialog_roomTile_time'>{humanTs}</span>;
}
const avatarSize = 36;
@ -247,61 +250,95 @@ class DMRoomTile extends React.PureComponent {
let checkmark = null;
if (this.props.isSelected) {
// To reduce flickering we put the 'selected' room tile above the real avatar
checkmark = <div className='mx_DMInviteDialog_roomTile_selected' />;
checkmark = <div className='mx_InviteDialog_roomTile_selected' />;
}
// To reduce flickering we put the checkmark on top of the actual avatar (prevents
// the browser from reloading the image source when the avatar remounts).
const stackedAvatar = (
<span className='mx_DMInviteDialog_roomTile_avatarStack'>
<span className='mx_InviteDialog_roomTile_avatarStack'>
{avatar}
{checkmark}
</span>
);
return (
<div className='mx_DMInviteDialog_roomTile' onClick={this._onClick}>
<div className='mx_InviteDialog_roomTile' onClick={this._onClick}>
{stackedAvatar}
<span className='mx_DMInviteDialog_roomTile_name'>{this._highlightName(this.props.member.name)}</span>
<span className='mx_DMInviteDialog_roomTile_userId'>{this._highlightName(this.props.member.userId)}</span>
<span className='mx_InviteDialog_roomTile_name'>{this._highlightName(this.props.member.name)}</span>
<span className='mx_InviteDialog_roomTile_userId'>{this._highlightName(this.props.member.userId)}</span>
{timestamp}
</div>
);
}
}
export default class DMInviteDialog extends React.PureComponent {
export default class InviteDialog extends React.PureComponent {
static propTypes = {
// Takes an array of user IDs/emails to invite.
onFinished: PropTypes.func.isRequired,
// The kind of invite being performed. Assumed to be KIND_DM if
// not provided.
kind: PropTypes.string,
// The room ID this dialog is for. Only required for KIND_INVITE.
roomId: PropTypes.string,
};
static defaultProps = {
kind: KIND_DM,
};
_debounceTimer: number = null;
_editorRef: any = null;
constructor() {
super();
constructor(props) {
super(props);
if (props.kind === KIND_INVITE && !props.roomId) {
throw new Error("When using KIND_INVITE a roomId is required for an InviteDialog");
}
let alreadyInvited = [];
if (props.roomId) {
const room = MatrixClientPeg.get().getRoom(props.roomId);
if (!room) throw new Error("Room ID given to InviteDialog does not look like a room");
alreadyInvited = [
...room.getMembersWithMembership('invite'),
...room.getMembersWithMembership('join'),
...room.getMembersWithMembership('ban'), // so we don't try to invite them
].map(m => m.userId);
}
this.state = {
targets: [], // array of Member objects (see interface above)
filterText: "",
recents: this._buildRecents(),
recents: this._buildRecents(alreadyInvited),
numRecentsShown: INITIAL_ROOMS_SHOWN,
suggestions: this._buildSuggestions(),
suggestions: this._buildSuggestions(alreadyInvited),
numSuggestionsShown: INITIAL_ROOMS_SHOWN,
serverResultsMixin: [], // { user: DirectoryMember, userId: string }[], like recents and suggestions
threepidResultsMixin: [], // { user: ThreepidMember, userId: string}[], like recents and suggestions
canUseIdentityServer: !!MatrixClientPeg.get().getIdentityServerUrl(),
tryingIdentityServer: false,
// These two flags are used for the 'Go' button to communicate what is going on.
busy: false,
errorText: null,
};
this._editorRef = createRef();
}
_buildRecents(): {userId: string, user: RoomMember, lastActive: number} {
_buildRecents(excludedTargetIds: string[]): {userId: string, user: RoomMember, lastActive: number} {
const rooms = DMRoomMap.shared().getUniqueRoomsWithIndividuals();
const recents = [];
for (const userId in rooms) {
// Filter out user IDs that are already in the room / should be excluded
if (excludedTargetIds.includes(userId)) continue;
const room = rooms[userId];
const member = room.getMember(userId);
if (!member) continue; // just skip people who don't have memberships for some reason
@ -320,7 +357,7 @@ export default class DMInviteDialog extends React.PureComponent {
return recents;
}
_buildSuggestions(): {userId: string, user: RoomMember} {
_buildSuggestions(excludedTargetIds: string[]): {userId: string, user: RoomMember} {
const maxConsideredMembers = 200;
const client = MatrixClientPeg.get();
const excludedUserIds = [client.getUserId(), SdkConfig.get()['welcomeUserId']];
@ -337,6 +374,11 @@ export default class DMInviteDialog extends React.PureComponent {
const joinedMembers = room.getJoinedMembers().filter(u => !excludedUserIds.includes(u.userId));
for (const member of joinedMembers) {
// Filter out user IDs that are already in the room / should be excluded
if (excludedTargetIds.includes(member.userId)) {
continue;
}
if (!members[member.userId]) {
members[member.userId] = {
member: member,
@ -369,6 +411,58 @@ export default class DMInviteDialog extends React.PureComponent {
return scores;
}, {});
// Now that we have scores for being in rooms, boost those people who have sent messages
// recently, as a way to improve the quality of suggestions. We do this by checking every
// room to see who has sent a message in the last few hours, and giving them a score
// which correlates to the freshness of their message. In theory, this results in suggestions
// which are closer to "continue this conversation" rather than "this person exists".
const trueJoinedRooms = client.getRooms().filter(r => r.getMyMembership() === 'join');
const now = (new Date()).getTime();
const earliestAgeConsidered = now - (60 * 60 * 1000); // 1 hour ago
const maxMessagesConsidered = 50; // so we don't iterate over a huge amount of traffic
const lastSpoke = {}; // userId: timestamp
const lastSpokeMembers = {}; // userId: room member
for (const room of trueJoinedRooms) {
// Skip low priority rooms and DMs
const isDm = DMRoomMap.shared().getUserIdForRoomId(room.roomId);
if (Object.keys(room.tags).includes("m.lowpriority") || isDm) {
continue;
}
const events = room.getLiveTimeline().getEvents(); // timelines are most recent last
for (let i = events.length - 1; i >= Math.max(0, events.length - maxMessagesConsidered); i--) {
const ev = events[i];
if (excludedUserIds.includes(ev.getSender())) {
continue;
}
if (ev.getTs() <= earliestAgeConsidered) {
break; // give up: all events from here on out are too old
}
if (!lastSpoke[ev.getSender()] || lastSpoke[ev.getSender()] < ev.getTs()) {
lastSpoke[ev.getSender()] = ev.getTs();
lastSpokeMembers[ev.getSender()] = room.getMember(ev.getSender());
}
}
}
for (const userId in lastSpoke) {
const ts = lastSpoke[userId];
const member = lastSpokeMembers[userId];
if (!member) continue; // skip people we somehow don't have profiles for
// Scores from being in a room give a 'good' score of about 1.0-1.5, so for our
// boost we'll try and award at least +1.0 for making the list, with +4.0 being
// an approximate maximum for being selected.
const distanceFromNow = Math.abs(now - ts); // abs to account for slight future messages
const inverseTime = (now - earliestAgeConsidered) - distanceFromNow;
const scoreBoost = Math.max(1, inverseTime / (15 * 60 * 1000)); // 15min segments to keep scores sane
let record = memberScores[userId];
if (!record) record = memberScores[userId] = {score: 0};
record.member = member;
record.score += scoreBoost;
}
const members = Object.values(memberScores);
members.sort((a, b) => {
if (a.score === b.score) {
@ -384,12 +478,101 @@ export default class DMInviteDialog extends React.PureComponent {
return members.map(m => ({userId: m.member.userId, user: m.member}));
}
_shouldAbortAfterInviteError(result): boolean {
const failedUsers = Object.keys(result.states).filter(a => result.states[a] === 'error');
if (failedUsers.length > 0) {
console.log("Failed to invite users: ", result);
this.setState({
busy: false,
errorText: _t("Failed to invite the following users to chat: %(csvUsers)s", {
csvUsers: failedUsers.join(", "),
}),
});
return true; // abort
}
return false;
}
_startDm = () => {
this.props.onFinished(this.state.targets.map(t => t.userId));
this.setState({busy: true});
const targetIds = this.state.targets.map(t => t.userId);
// Check if there is already a DM with these people and reuse it if possible.
const existingRoom = DMRoomMap.shared().getDMRoomForIdentifiers(targetIds);
if (existingRoom) {
dis.dispatch({
action: 'view_room',
room_id: existingRoom.roomId,
should_peek: false,
joining: false,
});
this.props.onFinished();
return;
}
// Check if it's a traditional DM and create the room if required.
// TODO: [Canonical DMs] Remove this check and instead just create the multi-person DM
let createRoomPromise = Promise.resolve();
if (targetIds.length === 1) {
createRoomPromise = createRoom({dmUserId: targetIds[0]});
} else {
// Create a boring room and try to invite the targets manually.
createRoomPromise = createRoom().then(roomId => {
return inviteMultipleToRoom(roomId, targetIds);
}).then(result => {
if (this._shouldAbortAfterInviteError(result)) {
return true; // abort
}
});
}
// the createRoom call will show the room for us, so we don't need to worry about that.
createRoomPromise.then(abort => {
if (abort === true) return; // only abort on true booleans, not roomIds or something
this.props.onFinished();
}).catch(err => {
console.error(err);
this.setState({
busy: false,
errorText: _t("We couldn't create your DM. Please check the users you want to invite and try again."),
});
});
};
_inviteUsers = () => {
this.setState({busy: true});
const targetIds = this.state.targets.map(t => t.userId);
const room = MatrixClientPeg.get().getRoom(this.props.roomId);
if (!room) {
console.error("Failed to find the room to invite users to");
this.setState({
busy: false,
errorText: _t("Something went wrong trying to invite the users."),
});
return;
}
inviteMultipleToRoom(this.props.roomId, targetIds).then(result => {
if (!this._shouldAbortAfterInviteError(result)) { // handles setting error message too
this.props.onFinished();
}
}).catch(err => {
console.error(err);
this.setState({
busy: false,
errorText: _t(
"We couldn't invite those users. Please check the users you want to invite and try again.",
),
});
});
};
_cancel = () => {
this.props.onFinished([]);
// We do not want the user to close the dialog while an action is in progress
if (this.state.busy) return;
this.props.onFinished();
};
_updateFilter = (e) => {
@ -599,7 +782,11 @@ export default class DMInviteDialog extends React.PureComponent {
let showNum = kind === 'recents' ? this.state.numRecentsShown : this.state.numSuggestionsShown;
const showMoreFn = kind === 'recents' ? this._showMoreRecents.bind(this) : this._showMoreSuggestions.bind(this);
const lastActive = (m) => kind === 'recents' ? m.lastActive : null;
const sectionName = kind === 'recents' ? _t("Recent Conversations") : _t("Suggestions");
let sectionName = kind === 'recents' ? _t("Recent Conversations") : _t("Suggestions");
if (this.props.kind === KIND_INVITE) {
sectionName = kind === 'recents' ? _t("Recently Direct Messaged") : _t("Suggestions");
}
// Mix in the server results if we have any, but only if we're searching. We track the additional
// members separately because we want to filter sourceMembers but trust the mixin arrays to have
@ -631,7 +818,7 @@ export default class DMInviteDialog extends React.PureComponent {
if (sourceMembers.length === 0 && additionalMembers.length === 0) {
return (
<div className='mx_DMInviteDialog_section'>
<div className='mx_InviteDialog_section'>
<h3>{sectionName}</h3>
<p>{_t("No results")}</p>
</div>
@ -672,7 +859,7 @@ export default class DMInviteDialog extends React.PureComponent {
/>
));
return (
<div className='mx_DMInviteDialog_section'>
<div className='mx_InviteDialog_section'>
<h3>{sectionName}</h3>
{tiles}
{showMore}
@ -695,7 +882,7 @@ export default class DMInviteDialog extends React.PureComponent {
/>
);
return (
<div className='mx_DMInviteDialog_editor' onClick={this._onClickInputArea}>
<div className='mx_InviteDialog_editor' onClick={this._onClickInputArea}>
{targets}
{input}
</div>
@ -739,35 +926,67 @@ export default class DMInviteDialog extends React.PureComponent {
render() {
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
const AccessibleButton = sdk.getComponent("elements.AccessibleButton");
const Spinner = sdk.getComponent("elements.Spinner");
let spinner = null;
if (this.state.busy) {
spinner = <Spinner w={20} h={20} />;
}
let title;
let helpText;
let buttonText;
let goButtonFn;
if (this.props.kind === KIND_DM) {
const userId = MatrixClientPeg.get().getUserId();
title = _t("Direct Messages");
helpText = _t(
"If you can't find someone, ask them for their username, or share your " +
"username (%(userId)s) or <a>profile link</a>.",
{userId},
{a: (sub) => <a href={makeUserPermalink(userId)} rel="noopener" target="_blank">{sub}</a>},
);
buttonText = _t("Go");
goButtonFn = this._startDm;
} else { // KIND_INVITE
title = _t("Invite to this room");
helpText = _t(
"If you can't find someone, ask them for their username (e.g. @user:server.com) or " +
"<a>share this room</a>.", {},
{a: (sub) => <a href={makeRoomPermalink(this.props.roomId)} rel="noopener" target="_blank">{sub}</a>},
);
buttonText = _t("Invite");
goButtonFn = this._inviteUsers;
}
const userId = MatrixClientPeg.get().getUserId();
return (
<BaseDialog
className='mx_DMInviteDialog'
className='mx_InviteDialog'
hasCancel={true}
onFinished={this._cancel}
title={_t("Direct Messages")}
title={title}
>
<div className='mx_DMInviteDialog_content'>
<p>
{_t(
"If you can't find someone, ask them for their username, or share your " +
"username (%(userId)s) or <a>profile link</a>.",
{userId},
{a: (sub) => <a href={makeUserPermalink(userId)} rel="noopener" target="_blank">{sub}</a>},
)}
</p>
<div className='mx_DMInviteDialog_addressBar'>
<div className='mx_InviteDialog_content'>
<p>{helpText}</p>
<div className='mx_InviteDialog_addressBar'>
{this._renderEditor()}
{this._renderIdentityServerWarning()}
<AccessibleButton
kind="primary"
onClick={this._startDm}
className='mx_DMInviteDialog_goButton'
>
{_t("Go")}
</AccessibleButton>
<div className='mx_InviteDialog_buttonAndSpinner'>
<AccessibleButton
kind="primary"
onClick={goButtonFn}
className='mx_InviteDialog_goButton'
disabled={this.state.busy}
>
{buttonText}
</AccessibleButton>
{spinner}
</div>
</div>
{this._renderIdentityServerWarning()}
<div className='error'>{this.state.errorText}</div>
{this._renderSection('recents')}
{this._renderSection('suggestions')}
</div>

View file

@ -22,6 +22,9 @@ import * as sdk from '../../../index';
import { _t, _td } from '../../../languageHandler';
// TODO: We can remove this once cross-signing is the only way.
// https://github.com/vector-im/riot-web/issues/11908
/**
* Dialog which asks the user whether they want to share their keys with
* an unverified device.

View file

@ -17,7 +17,7 @@ limitations under the License.
*/
import url from 'url';
import qs from 'querystring';
import qs from 'qs';
import React, {createRef} from 'react';
import PropTypes from 'prop-types';
import {MatrixClientPeg} from '../../../MatrixClientPeg';

View file

@ -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 (<div className={classes}>
<div className="mx_KeyVerification_title">{title}</div>

View file

@ -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 = (<AccessibleButton onClick={this._openRequest}>
{this._acceptedLabel(request.receivingUserId)}
</AccessibleButton>);
} else {
stateLabel = this._cancelledLabel(request.cancellingUserId);
}
stateNode = (<div className="mx_KeyVerification_state">{stateLabel}</div>);
}
if (toUserId === myUserId) { // request sent to us
if (!request.initiatedByMe) {
title = (<div className="mx_KeyVerification_title">{
_t("%(name)s wants to verify", {name: getNameForEventRoom(fromUserId, mxEvent)})}</div>);
_t("%(name)s wants to verify", {name: getNameForEventRoom(request.requestingUserId, mxEvent)})}</div>);
subtitle = (<div className="mx_KeyVerification_subtitle">{
userLabelForEventRoom(fromUserId, mxEvent)}</div>);
const isResolved = !(this.state.accepted || this.state.cancelled || this.state.done);
if (isResolved) {
const FormButton = sdk.getComponent("elements.FormButton");
userLabelForEventRoom(request.requestingUserId, mxEvent)}</div>);
if (request.requested && !request.observeOnly) {
stateNode = (<div className="mx_KeyVerification_buttons">
<FormButton kind="danger" onClick={this._onRejectClicked} label={_t("Decline")} />
<FormButton onClick={this._onAcceptClicked} label={_t("Accept")} />
</div>);
}
} else if (isOwn) { // request sent by us
} else { // request sent by us
title = (<div className="mx_KeyVerification_title">{
_t("You sent a verification request")}</div>);
subtitle = (<div className="mx_KeyVerification_subtitle">{
userLabelForEventRoom(this.state.otherPartyUserId, mxEvent)}</div>);
userLabelForEventRoom(request.receivingUserId, mxEvent)}</div>);
}
if (title) {

View file

@ -0,0 +1,31 @@
/*
Copyright 2019 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import React from 'react';
import * as sdk from '../../../index';
import {_t} from "../../../languageHandler";
export default class EncryptionInfo extends React.PureComponent {
render() {
const AccessibleButton = sdk.getComponent('elements.AccessibleButton');
return (<div className="mx_UserInfo"><div className="mx_UserInfo_container">
<h3>{_t("Verify User")}</h3>
<p>{_t("For extra security, verify this user by checking a one-time code on both of your devices.")}</p>
<p>{_t("For maximum security, do this in person.")}</p>
<AccessibleButton kind="primary" onClick={this.props.onStartVerification}>{_t("Start Verification")}</AccessibleButton>
</div></div>);
}
}

View file

@ -0,0 +1,48 @@
/*
Copyright 2019 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import React from 'react';
import EncryptionInfo from "./EncryptionInfo";
import VerificationPanel from "./VerificationPanel";
import {MatrixClientPeg} from "../../../MatrixClientPeg";
import {ensureDMExists} from "../../../createRoom";
export default class EncryptionPanel extends React.PureComponent {
constructor(props) {
super(props);
this.state = {};
}
render() {
const request = this.props.verificationRequest || this.state.verificationRequest;
const {member} = this.props;
if (request) {
return <VerificationPanel request={request} key={request.channel.transactionId} />;
} else if (member) {
return <EncryptionInfo onStartVerification={this._onStartVerification} member={member} />;
} else {
return <p>Not a member nor request, not sure what to render</p>;
}
}
_onStartVerification = async () => {
const client = MatrixClientPeg.get();
const {member} = this.props;
const roomId = await ensureDMExists(client, member.userId);
const verificationRequest = await client.requestVerificationDM(member.userId, roomId);
this.setState({verificationRequest});
};
}

View file

@ -66,8 +66,13 @@ export default class GroupHeaderButtons extends HeaderButtons {
}
_onMembersClicked() {
// This toggles for us, if needed
this.setPhase(RIGHT_PANEL_PHASES.GroupMemberList);
if (this.state.phase === RIGHT_PANEL_PHASES.GroupMemberInfo) {
// send the active phase to trigger a toggle
this.setPhase(RIGHT_PANEL_PHASES.GroupMemberInfo);
} else {
// This toggles for us, if needed
this.setPhase(RIGHT_PANEL_PHASES.GroupMemberList);
}
}
_onRoomsClicked() {

View file

@ -23,10 +23,12 @@ import { _t } from '../../../languageHandler';
import HeaderButton from './HeaderButton';
import HeaderButtons, {HEADER_KIND_ROOM} from './HeaderButtons';
import {RIGHT_PANEL_PHASES} from "../../../stores/RightPanelStorePhases";
import RightPanelStore from "../../../stores/RightPanelStore";
const MEMBER_PHASES = [
RIGHT_PANEL_PHASES.RoomMemberList,
RIGHT_PANEL_PHASES.RoomMemberInfo,
RIGHT_PANEL_PHASES.EncryptionPanel,
RIGHT_PANEL_PHASES.Room3pidMemberInfo,
];
@ -56,8 +58,13 @@ export default class RoomHeaderButtons extends HeaderButtons {
}
_onMembersClicked() {
// This toggles for us, if needed
this.setPhase(RIGHT_PANEL_PHASES.RoomMemberList);
if (this.state.phase === RIGHT_PANEL_PHASES.RoomMemberInfo) {
// send the active phase to trigger a toggle
this.setPhase(RIGHT_PANEL_PHASES.RoomMemberInfo, RightPanelStore.getSharedInstance().roomPanelPhaseParams);
} else {
// This toggles for us, if needed
this.setPhase(RIGHT_PANEL_PHASES.RoomMemberList);
}
}
_onFilesClicked() {

View file

@ -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 ?
(<DevicesSection loading={devices === undefined} devices={devices} userId={user.userId} />) : null;
const userVerified = cli.checkUserTrust(user.userId).isVerified();
const isMe = user.userId === cli.getUserId();
let verifyButton;
if (!userVerified) {
verifyButton = <AccessibleButton className="mx_UserInfo_verify" onClick={() => verifyDevice(user.userId, null)}>
if (!userVerified && !isMe) {
verifyButton = <AccessibleButton className="mx_UserInfo_verify" onClick={() => verifyUser(user)}>
{_t("Verify")}
</AccessibleButton>;
}
const devicesSection = <DevicesSection
loading={devices === undefined}
devices={devices} userId={user.userId} />;
const securitySection = (
<div className="mx_UserInfo_container">
<h3>{ _t("Security") }</h3>
<p>{ text }</p>
{verifyButton}
{ verifyButton }
{ devicesSection }
</div>
);
@ -1308,7 +1312,7 @@ const UserInfo = ({user, groupId, roomId, onClose}) => {
<div className="mx_UserInfo_container">
<div className="mx_UserInfo_profile">
<div >
<div>
<h2 aria-label={displayName}>
{ e2eIcon }
{ displayName }

View file

@ -0,0 +1,119 @@
/*
Copyright 2019 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import React from 'react';
import * as sdk from '../../../index';
import {verificationMethods} from 'matrix-js-sdk/src/crypto';
export default class VerificationPanel extends React.PureComponent {
constructor(props) {
super(props);
this.state = {};
this._hasVerifier = !!props.request.verifier;
}
render() {
return <div className="mx_UserInfo">
<div className="mx_UserInfo_container">
{ this.renderStatus() }
</div>
</div>;
}
renderStatus() {
const AccessibleButton = sdk.getComponent('elements.AccessibleButton');
const Spinner = sdk.getComponent('elements.Spinner');
const {request} = this.props;
if (request.requested) {
return (<p>Waiting for {request.otherUserId} to accept ... <Spinner /></p>);
} else if (request.ready) {
const verifyButton = <AccessibleButton kind="primary" onClick={this._startSAS}>
Verify by emoji
</AccessibleButton>;
return (<p>{request.otherUserId} is ready, start {verifyButton}</p>);
} else if (request.started) {
if (this.state.sasWaitingForOtherParty) {
return <p>Waiting for {request.otherUserId} to confirm ...</p>;
} else if (this.state.sasEvent) {
const VerificationShowSas = sdk.getComponent('views.verification.VerificationShowSas');
return (<div>
<VerificationShowSas
sas={this.state.sasEvent.sas}
onCancel={this._onSasMismatchesClick}
onDone={this._onSasMatchesClick}
/>
</div>);
} else {
return (<p>Setting up SAS verification...</p>);
}
} else if (request.done) {
return <p>verified {request.otherUserId}!!</p>;
} else if (request.cancelled) {
return <p>cancelled by {request.cancellingUserId}!</p>;
}
}
_startSAS = async () => {
const verifier = this.props.request.beginKeyVerification(verificationMethods.SAS);
try {
await verifier.verify();
} catch (err) {
console.error(err);
} finally {
this.setState({sasEvent: null});
}
};
_onSasMatchesClick = () => {
this.setState({sasWaitingForOtherParty: true});
this.state.sasEvent.confirm();
};
_onSasMismatchesClick = () => {
this.state.sasEvent.cancel();
};
_onVerifierShowSas = (sasEvent) => {
this.setState({sasEvent});
};
_onRequestChange = async () => {
const {request} = this.props;
if (!this._hasVerifier && !!request.verifier) {
request.verifier.on('show_sas', this._onVerifierShowSas);
try {
// on the requester side, this is also awaited in _startSAS,
// but that's ok as verify should return the same promise.
await request.verifier.verify();
} catch (err) {
console.error("error verify", err);
}
} else if (this._hasVerifier && !request.verifier) {
request.verifier.removeListener('show_sas', this._onVerifierShowSas);
}
this._hasVerifier = !!request.verifier;
this.forceUpdate();
};
componentDidMount() {
this.props.request.on("change", this._onRequestChange);
}
componentWillUnmount() {
this.props.request.off("change", this._onRequestChange);
}
}

View file

@ -209,8 +209,9 @@ export default class BasicMessageEditor extends React.Component {
const selectedParts = range.parts.map(p => p.serialize());
event.clipboardData.setData("application/x-riot-composer", JSON.stringify(selectedParts));
if (type === "cut") {
selection.deleteFromDocument();
range.replace([]);
// Remove the text, updating the model as appropriate
this._modifiedFlag = true;
replaceRangeAndMoveCaret(range, []);
}
event.preventDefault();
}
@ -259,8 +260,8 @@ export default class BasicMessageEditor extends React.Component {
const {caret, text} = getCaretOffsetAndText(this._editorRef, sel);
const newText = text.substr(0, caret.offset) + textToInsert + text.substr(caret.offset);
caret.offset += textToInsert.length;
this.props.model.update(newText, inputType, caret);
this._modifiedFlag = true;
this.props.model.update(newText, inputType, caret);
}
// this is used later to see if we need to recalculate the caret

View file

@ -60,14 +60,14 @@ export default class CrossSigningPanel extends React.PureComponent {
this.setState(this._getUpdatedStatus());
};
_getUpdatedStatus() {
async _getUpdatedStatus() {
// XXX: Add public accessors if we keep this around in production
const cli = MatrixClientPeg.get();
const crossSigning = cli._crypto._crossSigningInfo;
const secretStorage = cli._crypto._secretStorage;
const crossSigningPublicKeysOnDevice = crossSigning.getId();
const crossSigningPrivateKeysInStorage = crossSigning.isStoredInSecretStorage(secretStorage);
const secretStorageKeyInAccount = secretStorage.hasKey();
const crossSigningPrivateKeysInStorage = await crossSigning.isStoredInSecretStorage(secretStorage);
const secretStorageKeyInAccount = await secretStorage.hasKey();
return {
crossSigningPublicKeysOnDevice,

View file

@ -137,7 +137,7 @@ export default class BridgeSettingsTab extends React.Component {
const client = MatrixClientPeg.get();
const roomState = (client.getRoom(roomId)).currentState;
const bridgeEvents = Array.concat(...BRIDGE_EVENT_TYPES.map((typeName) =>
const bridgeEvents = [].concat(...BRIDGE_EVENT_TYPES.map((typeName) =>
Object.values(roomState.events[typeName] || {}),
));

View file

@ -160,8 +160,8 @@ export default class GeneralUserSettingsTab extends React.Component {
// for free. So we might as well use that for our own purposes.
const idServerUrl = MatrixClientPeg.get().getIdentityServerUrl();
const authClient = new IdentityAuthClient();
const idAccessToken = await authClient.getAccessToken({ check: false });
try {
const idAccessToken = await authClient.getAccessToken({ check: false });
await startTermsFlow([new Service(
SERVICE_TYPES.IS,
idServerUrl,

View file

@ -0,0 +1,57 @@
/*
Copyright 2020 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 * as sdk from "../../../index";
import { _t } from '../../../languageHandler';
import Modal from "../../../Modal";
import { MatrixClientPeg } from '../../../MatrixClientPeg';
import DeviceListener from '../../../DeviceListener';
export default class VerifySessionToast extends React.PureComponent {
static propTypes = {
toastKey: PropTypes.string.isRequired,
deviceId: PropTypes.string,
};
_onLaterClick = () => {
DeviceListener.sharedInstance().dismissVerification(this.props.deviceId);
};
_onVerifyClick = async () => {
const cli = MatrixClientPeg.get();
const DeviceVerifyDialog = sdk.getComponent('views.dialogs.DeviceVerifyDialog');
const device = await cli.getStoredDevice(cli.getUserId(), this.props.deviceId);
Modal.createTrackedDialog('New Session Verify', 'Starting dialog', DeviceVerifyDialog, {
userId: MatrixClientPeg.get().getUserId(),
device,
}, null, /* priority = */ false, /* static = */ true);
};
render() {
const FormButton = sdk.getComponent("elements.FormButton");
return (<div>
<div className="mx_Toast_description">{_t("Other users may not trust it")}</div>
<div className="mx_Toast_buttons" aria-live="off">
<FormButton label={_t("Later")} kind="danger" onClick={this._onLaterClick} />
<FormButton label={_t("Verify")} onClick={this._onVerifyClick} />
</div>
</div>);
}
}

View file

@ -18,57 +18,43 @@ 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) {
this.props.dismiss();
const {request} = this.props;
if (request.ready || request.started || request.done || request.cancelled || request.observeOnly) {
ToastStore.sharedInstance().dismissToast(this.props.toastKey);
}
}
};
cancel = () => {
this.props.dismiss();
ToastStore.sharedInstance().dismissToast(this.props.toastKey);
try {
this.props.request.cancel();
} catch (err) {
@ -76,9 +62,10 @@ export default class VerificationRequestToast extends React.PureComponent {
}
}
accept = () => {
this.props.dismiss();
const {event} = this.props.request;
accept = async () => {
ToastStore.sharedInstance().dismissToast(this.props.toastKey);
const {request} = this.props;
const {event} = request;
// no room id for to_device requests
if (event.getRoomId()) {
dis.dispatch({
@ -87,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) {
@ -119,7 +111,6 @@ export default class VerificationRequestToast extends React.PureComponent {
}
VerificationRequestToast.propTypes = {
dismiss: PropTypes.func.isRequired,
request: PropTypes.object.isRequired,
requestObserver: PropTypes.instanceOf(KeyVerificationStateObserver),
toastKey: PropTypes.string.isRequired,
};