Merge branch 'develop' of https://github.com/matrix-org/matrix-react-sdk into t3chguy/roving
This commit is contained in:
commit
397e116efb
68 changed files with 1948 additions and 626 deletions
|
@ -2,7 +2,7 @@
|
|||
Copyright 2015, 2016 OpenMarket Ltd
|
||||
Copyright 2017 Vector Creations Ltd
|
||||
Copyright 2017-2019 New Vector Ltd
|
||||
Copyright 2019 The Matrix.org Foundation C.I.C.
|
||||
Copyright 2019, 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.
|
||||
|
@ -21,6 +21,7 @@ import React from 'react';
|
|||
import createReactClass from 'create-react-class';
|
||||
import PropTypes from 'prop-types';
|
||||
import * as Matrix from "matrix-js-sdk";
|
||||
import { isCryptoAvailable } from 'matrix-js-sdk/src/crypto';
|
||||
|
||||
// focus-visible is a Polyfill for the :focus-visible CSS pseudo-attribute used by _AccessibleButton.scss
|
||||
import 'focus-visible';
|
||||
|
@ -62,7 +63,7 @@ import { countRoomsWithNotif } from '../../RoomNotifs';
|
|||
import { ThemeWatcher } from "../../theme";
|
||||
import { storeRoomAliasInCache } from '../../RoomAliasCache';
|
||||
import { defer } from "../../utils/promise";
|
||||
import KeyVerificationStateObserver from '../../utils/KeyVerificationStateObserver';
|
||||
import ToastStore from "../../stores/ToastStore";
|
||||
|
||||
/** constants for MatrixChat.state.view */
|
||||
export const VIEWS = {
|
||||
|
@ -79,18 +80,14 @@ export const VIEWS = {
|
|||
// we are showing the registration view
|
||||
REGISTER: 3,
|
||||
|
||||
// completeing the registration flow
|
||||
// completing the registration flow
|
||||
POST_REGISTRATION: 4,
|
||||
|
||||
// showing the 'forgot password' view
|
||||
FORGOT_PASSWORD: 5,
|
||||
|
||||
// we have valid matrix credentials (either via an explicit login, via the
|
||||
// initial re-animation/guest registration, or via a registration), and are
|
||||
// now setting up a matrixclient to talk to it. This isn't an instant
|
||||
// process because we need to clear out indexeddb. While it is going on we
|
||||
// show a big spinner.
|
||||
LOGGING_IN: 6,
|
||||
// showing flow to trust this new device with cross-signing
|
||||
COMPLETE_SECURITY: 6,
|
||||
|
||||
// we are logged in with an active matrix client.
|
||||
LOGGED_IN: 7,
|
||||
|
@ -656,16 +653,12 @@ export default createReactClass({
|
|||
});
|
||||
break;
|
||||
}
|
||||
case 'on_logging_in':
|
||||
// We are now logging in, so set the state to reflect that
|
||||
// NB. This does not touch 'ready' since if our dispatches
|
||||
// are delayed, the sync could already have completed
|
||||
this.setStateForNewView({
|
||||
view: VIEWS.LOGGING_IN,
|
||||
});
|
||||
break;
|
||||
case 'on_logged_in':
|
||||
if (!Lifecycle.isSoftLogout()) {
|
||||
if (
|
||||
!Lifecycle.isSoftLogout() &&
|
||||
this.state.view !== VIEWS.LOGIN &&
|
||||
this.state.view !== VIEWS.COMPLETE_SECURITY
|
||||
) {
|
||||
this._onLoggedIn();
|
||||
}
|
||||
break;
|
||||
|
@ -1169,7 +1162,7 @@ export default createReactClass({
|
|||
if (this.props.config.welcomeUserId && getCurrentLanguage().startsWith("en")) {
|
||||
const welcomeUserRoom = await this._startWelcomeUserChat();
|
||||
if (welcomeUserRoom === null) {
|
||||
// We didn't rediret to the welcome user room, so show
|
||||
// We didn't redirect to the welcome user room, so show
|
||||
// the homepage.
|
||||
dis.dispatch({action: 'view_home_page'});
|
||||
}
|
||||
|
@ -1389,6 +1382,8 @@ export default createReactClass({
|
|||
cli.on("Session.logged_out", () => dft.stop());
|
||||
cli.on("Event.decrypted", (e, err) => dft.eventDecrypted(e, err));
|
||||
|
||||
// TODO: We can remove this once cross-signing is the only way.
|
||||
// https://github.com/vector-im/riot-web/issues/11908
|
||||
const krh = new KeyRequestHandler(cli);
|
||||
cli.on("crypto.roomKeyRequest", (req) => {
|
||||
krh.handleKeyRequest(req);
|
||||
|
@ -1458,22 +1453,14 @@ export default createReactClass({
|
|||
|
||||
if (SettingsStore.isFeatureEnabled("feature_cross_signing")) {
|
||||
cli.on("crypto.verification.request", request => {
|
||||
let requestObserver;
|
||||
if (request.event.getRoomId()) {
|
||||
requestObserver = new KeyVerificationStateObserver(
|
||||
request.event, MatrixClientPeg.get());
|
||||
}
|
||||
|
||||
if (!requestObserver || requestObserver.pending) {
|
||||
dis.dispatch({
|
||||
action: "show_toast",
|
||||
toast: {
|
||||
key: request.event.getId(),
|
||||
title: _t("Verification Request"),
|
||||
icon: "verification",
|
||||
props: {request, requestObserver},
|
||||
component: sdk.getComponent("toasts.VerificationRequestToast"),
|
||||
},
|
||||
console.log(`MatrixChat got a .request ${request.channel.transactionId}`, request.event.getRoomId());
|
||||
if (request.pending) {
|
||||
ToastStore.sharedInstance().addOrReplaceToast({
|
||||
key: 'verifreq_' + request.channel.transactionId,
|
||||
title: _t("Verification Request"),
|
||||
icon: "verification",
|
||||
props: {request},
|
||||
component: sdk.getComponent("toasts.VerificationRequestToast"),
|
||||
});
|
||||
}
|
||||
});
|
||||
|
@ -1573,6 +1560,10 @@ export default createReactClass({
|
|||
dis.dispatch({
|
||||
action: 'view_my_groups',
|
||||
});
|
||||
} else if (screen === 'complete_security') {
|
||||
dis.dispatch({
|
||||
action: 'start_complete_security',
|
||||
});
|
||||
} else if (screen == 'post_registration') {
|
||||
dis.dispatch({
|
||||
action: 'start_post_registration',
|
||||
|
@ -1822,21 +1813,69 @@ export default createReactClass({
|
|||
this._loggedInView = ref;
|
||||
},
|
||||
|
||||
async onUserCompletedLoginFlow(credentials) {
|
||||
// Wait for the client to be logged in (but not started)
|
||||
// which is enough to ask the server about account data.
|
||||
const loggedIn = new Promise(resolve => {
|
||||
const actionHandlerRef = dis.register(payload => {
|
||||
if (payload.action !== "on_logged_in") {
|
||||
return;
|
||||
}
|
||||
dis.unregister(actionHandlerRef);
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
|
||||
// Create and start the client in the background
|
||||
Lifecycle.setLoggedIn(credentials);
|
||||
await loggedIn;
|
||||
|
||||
const cli = MatrixClientPeg.get();
|
||||
// We're checking `isCryptoAvailable` here instead of `isCryptoEnabled`
|
||||
// because the client hasn't been started yet.
|
||||
if (!isCryptoAvailable()) {
|
||||
this._onLoggedIn();
|
||||
}
|
||||
|
||||
// Test for the master cross-signing key in SSSS as a quick proxy for
|
||||
// whether cross-signing has been set up on the account.
|
||||
let masterKeyInStorage = false;
|
||||
try {
|
||||
masterKeyInStorage = !!await cli.getAccountDataFromServer("m.cross_signing.master");
|
||||
} catch (e) {
|
||||
if (e.errcode !== "M_NOT_FOUND") throw e;
|
||||
}
|
||||
|
||||
if (masterKeyInStorage) {
|
||||
this.setStateForNewView({ view: VIEWS.COMPLETE_SECURITY });
|
||||
} else {
|
||||
this._onLoggedIn();
|
||||
}
|
||||
},
|
||||
|
||||
onCompleteSecurityFinished() {
|
||||
this._onLoggedIn();
|
||||
},
|
||||
|
||||
render: function() {
|
||||
// console.log(`Rendering MatrixChat with view ${this.state.view}`);
|
||||
|
||||
let view;
|
||||
|
||||
if (
|
||||
this.state.view === VIEWS.LOADING ||
|
||||
this.state.view === VIEWS.LOGGING_IN
|
||||
) {
|
||||
if (this.state.view === VIEWS.LOADING) {
|
||||
const Spinner = sdk.getComponent('elements.Spinner');
|
||||
view = (
|
||||
<div className="mx_MatrixChat_splash">
|
||||
<Spinner />
|
||||
</div>
|
||||
);
|
||||
} else if (this.state.view === VIEWS.COMPLETE_SECURITY) {
|
||||
const CompleteSecurity = sdk.getComponent('structures.auth.CompleteSecurity');
|
||||
view = (
|
||||
<CompleteSecurity
|
||||
onFinished={this.onCompleteSecurityFinished}
|
||||
/>
|
||||
);
|
||||
} else if (this.state.view === VIEWS.POST_REGISTRATION) {
|
||||
// needs to be before normal PageTypes as you are logged in technically
|
||||
const PostRegistration = sdk.getComponent('structures.auth.PostRegistration');
|
||||
|
@ -1921,7 +1960,7 @@ export default createReactClass({
|
|||
const Login = sdk.getComponent('structures.auth.Login');
|
||||
view = (
|
||||
<Login
|
||||
onLoggedIn={Lifecycle.setLoggedIn}
|
||||
onLoggedIn={this.onUserCompletedLoginFlow}
|
||||
onRegisterClick={this.onRegisterClick}
|
||||
fallbackHsUrl={this.getFallbackHsUrl()}
|
||||
defaultDeviceDisplayName={this.props.defaultDeviceDisplayName}
|
||||
|
|
|
@ -160,6 +160,7 @@ export default class RightPanel extends React.Component {
|
|||
groupId: payload.groupId,
|
||||
member: payload.member,
|
||||
event: payload.event,
|
||||
verificationRequest: payload.verificationRequest,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -168,6 +169,7 @@ export default class RightPanel extends React.Component {
|
|||
const MemberList = sdk.getComponent('rooms.MemberList');
|
||||
const MemberInfo = sdk.getComponent('rooms.MemberInfo');
|
||||
const UserInfo = sdk.getComponent('right_panel.UserInfo');
|
||||
const EncryptionPanel = sdk.getComponent('right_panel.EncryptionPanel');
|
||||
const ThirdPartyMemberInfo = sdk.getComponent('rooms.ThirdPartyMemberInfo');
|
||||
const NotificationPanel = sdk.getComponent('structures.NotificationPanel');
|
||||
const FilePanel = sdk.getComponent('structures.FilePanel');
|
||||
|
@ -235,6 +237,8 @@ export default class RightPanel extends React.Component {
|
|||
panel = <NotificationPanel />;
|
||||
} else if (this.state.phase === RIGHT_PANEL_PHASES.FilePanel) {
|
||||
panel = <FilePanel roomId={this.props.roomId} resizeNotifier={this.props.resizeNotifier} />;
|
||||
} else if (this.state.phase === RIGHT_PANEL_PHASES.EncryptionPanel) {
|
||||
panel = <EncryptionPanel member={this.state.member} verificationRequest={this.state.verificationRequest} />;
|
||||
}
|
||||
|
||||
const classes = classNames("mx_RightPanel", "mx_fadable", {
|
||||
|
|
|
@ -173,6 +173,7 @@ export default createReactClass({
|
|||
MatrixClientPeg.get().on("accountData", this.onAccountData);
|
||||
MatrixClientPeg.get().on("crypto.keyBackupStatus", this.onKeyBackupStatus);
|
||||
MatrixClientPeg.get().on("deviceVerificationChanged", this.onDeviceVerificationChanged);
|
||||
MatrixClientPeg.get().on("userTrustStatusChanged", this.onUserVerificationChanged);
|
||||
// Start listening for RoomViewStore updates
|
||||
this._roomStoreToken = RoomViewStore.addListener(this._onRoomViewStoreUpdate);
|
||||
this._onRoomViewStoreUpdate(true);
|
||||
|
@ -492,6 +493,7 @@ export default createReactClass({
|
|||
MatrixClientPeg.get().removeListener("accountData", this.onAccountData);
|
||||
MatrixClientPeg.get().removeListener("crypto.keyBackupStatus", this.onKeyBackupStatus);
|
||||
MatrixClientPeg.get().removeListener("deviceVerificationChanged", this.onDeviceVerificationChanged);
|
||||
MatrixClientPeg.get().removeListener("userTrustStatusChanged", this.onUserVerificationChanged);
|
||||
}
|
||||
|
||||
window.removeEventListener('beforeunload', this.onPageUnload);
|
||||
|
@ -762,6 +764,14 @@ export default createReactClass({
|
|||
this._updateE2EStatus(room);
|
||||
},
|
||||
|
||||
onUserVerificationChanged: function(userId, _trustStatus) {
|
||||
const room = this.state.room;
|
||||
if (!room.currentState.getMember(userId)) {
|
||||
return;
|
||||
}
|
||||
this._updateE2EStatus(room);
|
||||
},
|
||||
|
||||
_updateE2EStatus: async function(room) {
|
||||
const cli = MatrixClientPeg.get();
|
||||
if (!cli.isRoomEncrypted(room.roomId)) {
|
||||
|
@ -782,32 +792,41 @@ export default createReactClass({
|
|||
e2eStatus: hasUnverifiedDevices ? "warning" : "verified",
|
||||
});
|
||||
});
|
||||
debuglog("e2e check is warning/verified only as cross-signing is off");
|
||||
return;
|
||||
}
|
||||
|
||||
/* At this point, the user has encryption on and cross-signing on */
|
||||
const e2eMembers = await room.getEncryptionTargetMembers();
|
||||
for (const member of e2eMembers) {
|
||||
const { userId } = member;
|
||||
const userVerified = cli.checkUserTrust(userId).isCrossSigningVerified();
|
||||
if (!userVerified) {
|
||||
this.setState({
|
||||
e2eStatus: "warning",
|
||||
});
|
||||
return;
|
||||
}
|
||||
const verified = [];
|
||||
const unverified = [];
|
||||
e2eMembers.map(({userId}) => userId)
|
||||
.filter((userId) => userId !== cli.getUserId())
|
||||
.forEach((userId) => {
|
||||
(cli.checkUserTrust(userId).isCrossSigningVerified() ?
|
||||
verified : unverified).push(userId)
|
||||
});
|
||||
|
||||
debuglog("e2e verified", verified, "unverified", unverified);
|
||||
|
||||
/* Check all verified user devices. */
|
||||
for (const userId of verified) {
|
||||
const devices = await cli.getStoredDevicesForUser(userId);
|
||||
const allDevicesVerified = devices.every(device => {
|
||||
const { deviceId } = device;
|
||||
return cli.checkDeviceTrust(userId, deviceId).isCrossSigningVerified();
|
||||
const allDevicesVerified = devices.every(({deviceId}) => {
|
||||
return cli.checkDeviceTrust(userId, deviceId).isVerified();
|
||||
});
|
||||
if (!allDevicesVerified) {
|
||||
this.setState({
|
||||
e2eStatus: "warning",
|
||||
});
|
||||
debuglog("e2e status set to warning as not all users trust all of their devices." +
|
||||
" Aborted on user", userId);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
this.setState({
|
||||
e2eStatus: "verified",
|
||||
e2eStatus: unverified.length === 0 ? "verified" : "normal",
|
||||
});
|
||||
},
|
||||
|
||||
|
|
|
@ -1134,9 +1134,11 @@ const TimelinePanel = createReactClass({
|
|||
const allowPartial = opts.allowPartial || false;
|
||||
|
||||
const messagePanel = this._messagePanel.current;
|
||||
if (messagePanel === undefined) return null;
|
||||
if (!messagePanel) return null;
|
||||
|
||||
const wrapperRect = ReactDOM.findDOMNode(messagePanel).getBoundingClientRect();
|
||||
const messagePanelNode = ReactDOM.findDOMNode(messagePanel);
|
||||
if (!messagePanelNode) return null; // sometimes this happens for fresh rooms/post-sync
|
||||
const wrapperRect = messagePanelNode.getBoundingClientRect();
|
||||
const myUserId = MatrixClientPeg.get().credentials.userId;
|
||||
|
||||
const isNodeInView = (node) => {
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
Copyright 2019 The Matrix.org Foundation C.I.C.
|
||||
Copyright 2019, 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.
|
||||
|
@ -15,37 +15,26 @@ limitations under the License.
|
|||
*/
|
||||
|
||||
import * as React from "react";
|
||||
import dis from "../../dispatcher";
|
||||
import { _t } from '../../languageHandler';
|
||||
import ToastStore from "../../stores/ToastStore";
|
||||
import classNames from "classnames";
|
||||
|
||||
export default class ToastContainer extends React.Component {
|
||||
constructor() {
|
||||
super();
|
||||
this.state = {toasts: []};
|
||||
this.state = {toasts: ToastStore.sharedInstance().getToasts()};
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this._dispatcherRef = dis.register(this.onAction);
|
||||
ToastStore.sharedInstance().on('update', this._onToastStoreUpdate);
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
dis.unregister(this._dispatcherRef);
|
||||
ToastStore.sharedInstance().removeListener('update', this._onToastStoreUpdate);
|
||||
}
|
||||
|
||||
onAction = (payload) => {
|
||||
if (payload.action === "show_toast") {
|
||||
this._addToast(payload.toast);
|
||||
}
|
||||
};
|
||||
|
||||
_addToast(toast) {
|
||||
this.setState({toasts: this.state.toasts.concat(toast)});
|
||||
}
|
||||
|
||||
dismissTopToast = () => {
|
||||
const [, ...remaining] = this.state.toasts;
|
||||
this.setState({toasts: remaining});
|
||||
_onToastStoreUpdate = () => {
|
||||
this.setState({toasts: ToastStore.sharedInstance().getToasts()});
|
||||
};
|
||||
|
||||
render() {
|
||||
|
@ -62,8 +51,8 @@ export default class ToastContainer extends React.Component {
|
|||
const countIndicator = isStacked ? _t(" (1/%(totalCount)s)", {totalCount}) : null;
|
||||
|
||||
const toastProps = Object.assign({}, props, {
|
||||
dismiss: this.dismissTopToast,
|
||||
key,
|
||||
toastKey: key,
|
||||
});
|
||||
toast = (<div className={toastClasses}>
|
||||
<h2>{title}{countIndicator}</h2>
|
||||
|
|
177
src/components/structures/auth/CompleteSecurity.js
Normal file
177
src/components/structures/auth/CompleteSecurity.js
Normal file
|
@ -0,0 +1,177 @@
|
|||
/*
|
||||
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 { _t } from '../../../languageHandler';
|
||||
import * as sdk from '../../../index';
|
||||
import { MatrixClientPeg } from '../../../MatrixClientPeg';
|
||||
import { accessSecretStorage } from '../../../CrossSigningManager';
|
||||
|
||||
const PHASE_INTRO = 0;
|
||||
const PHASE_DONE = 1;
|
||||
const PHASE_CONFIRM_SKIP = 2;
|
||||
|
||||
export default class CompleteSecurity extends React.Component {
|
||||
static propTypes = {
|
||||
onFinished: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
this.state = {
|
||||
phase: PHASE_INTRO,
|
||||
};
|
||||
}
|
||||
|
||||
onStartClick = async () => {
|
||||
const cli = MatrixClientPeg.get();
|
||||
try {
|
||||
await accessSecretStorage(async () => {
|
||||
await cli.checkOwnCrossSigningTrust();
|
||||
});
|
||||
this.setState({
|
||||
phase: PHASE_DONE,
|
||||
});
|
||||
} catch (e) {
|
||||
// this will throw if the user hits cancel, so ignore
|
||||
}
|
||||
}
|
||||
|
||||
onSkipClick = () => {
|
||||
this.setState({
|
||||
phase: PHASE_CONFIRM_SKIP,
|
||||
});
|
||||
}
|
||||
|
||||
onSkipConfirmClick = () => {
|
||||
this.props.onFinished();
|
||||
}
|
||||
|
||||
onSkipBackClick = () => {
|
||||
this.setState({
|
||||
phase: PHASE_INTRO,
|
||||
});
|
||||
}
|
||||
|
||||
onDoneClick = () => {
|
||||
this.props.onFinished();
|
||||
}
|
||||
|
||||
render() {
|
||||
const AuthPage = sdk.getComponent("auth.AuthPage");
|
||||
const AuthHeader = sdk.getComponent("auth.AuthHeader");
|
||||
const AuthBody = sdk.getComponent("auth.AuthBody");
|
||||
const AccessibleButton = sdk.getComponent("elements.AccessibleButton");
|
||||
|
||||
const {
|
||||
phase,
|
||||
} = this.state;
|
||||
|
||||
let icon;
|
||||
let title;
|
||||
let body;
|
||||
if (phase === PHASE_INTRO) {
|
||||
icon = <span className="mx_CompleteSecurity_headerIcon mx_E2EIcon_warning"></span>;
|
||||
title = _t("Complete security");
|
||||
body = (
|
||||
<div>
|
||||
<p>{_t(
|
||||
"Verify this session to grant it access to encrypted messages.",
|
||||
)}</p>
|
||||
<div className="mx_CompleteSecurity_actionRow">
|
||||
<AccessibleButton
|
||||
kind="danger"
|
||||
onClick={this.onSkipClick}
|
||||
>
|
||||
{_t("Skip")}
|
||||
</AccessibleButton>
|
||||
<AccessibleButton
|
||||
kind="primary"
|
||||
onClick={this.onStartClick}
|
||||
>
|
||||
{_t("Start")}
|
||||
</AccessibleButton>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
} else if (phase === PHASE_DONE) {
|
||||
icon = <span className="mx_CompleteSecurity_headerIcon mx_E2EIcon_verified"></span>;
|
||||
title = _t("Session verified");
|
||||
body = (
|
||||
<div>
|
||||
<div className="mx_CompleteSecurity_heroIcon mx_E2EIcon_verified"></div>
|
||||
<p>{_t(
|
||||
"Your new session is now verified. It has access to your " +
|
||||
"encrypted messages, and other users will see it as trusted.",
|
||||
)}</p>
|
||||
<div className="mx_CompleteSecurity_actionRow">
|
||||
<AccessibleButton
|
||||
kind="primary"
|
||||
onClick={this.onDoneClick}
|
||||
>
|
||||
{_t("Done")}
|
||||
</AccessibleButton>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
} else if (phase === PHASE_CONFIRM_SKIP) {
|
||||
icon = <span className="mx_CompleteSecurity_headerIcon mx_E2EIcon_warning"></span>;
|
||||
title = _t("Are you sure?");
|
||||
body = (
|
||||
<div>
|
||||
<p>{_t(
|
||||
"Without completing security on this device, it won’t have " +
|
||||
"access to encrypted messages.",
|
||||
)}</p>
|
||||
<div className="mx_CompleteSecurity_actionRow">
|
||||
<AccessibleButton
|
||||
className="warning"
|
||||
kind="secondary"
|
||||
onClick={this.onSkipConfirmClick}
|
||||
>
|
||||
{_t("Skip")}
|
||||
</AccessibleButton>
|
||||
<AccessibleButton
|
||||
kind="danger"
|
||||
onClick={this.onSkipBackClick}
|
||||
>
|
||||
{_t("Go Back")}
|
||||
</AccessibleButton>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
} else {
|
||||
throw new Error(`Unknown phase ${phase}`);
|
||||
}
|
||||
|
||||
return (
|
||||
<AuthPage>
|
||||
<AuthHeader />
|
||||
<AuthBody>
|
||||
<h2 className="mx_CompleteSecurity_header">
|
||||
{icon}
|
||||
{title}
|
||||
</h2>
|
||||
<div className="mx_CompleteSecurity_body">
|
||||
{body}
|
||||
</div>
|
||||
</AuthBody>
|
||||
</AuthPage>
|
||||
);
|
||||
}
|
||||
}
|
|
@ -66,7 +66,7 @@ export default class SoftLogout extends React.Component {
|
|||
componentDidMount(): void {
|
||||
// We've ended up here when we don't need to - navigate to login
|
||||
if (!Lifecycle.isSoftLogout()) {
|
||||
dis.dispatch({action: "on_logged_in"});
|
||||
dis.dispatch({action: "start_login"});
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
|
@ -83,7 +83,7 @@ export default createReactClass({
|
|||
|
||||
if (viewUserOnClick) {
|
||||
onClick = () => {
|
||||
dispatcher.dispatch({
|
||||
dis.dispatch({
|
||||
action: 'view_user',
|
||||
member: this.props.member,
|
||||
});
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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}`,
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
@ -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({
|
||||
|
|
|
@ -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>
|
|
@ -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.
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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) {
|
||||
|
|
31
src/components/views/right_panel/EncryptionInfo.js
Normal file
31
src/components/views/right_panel/EncryptionInfo.js
Normal 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>);
|
||||
}
|
||||
}
|
48
src/components/views/right_panel/EncryptionPanel.js
Normal file
48
src/components/views/right_panel/EncryptionPanel.js
Normal 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});
|
||||
};
|
||||
}
|
|
@ -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() {
|
||||
|
|
|
@ -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() {
|
||||
|
|
|
@ -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 }
|
||||
|
|
119
src/components/views/right_panel/VerificationPanel.js
Normal file
119
src/components/views/right_panel/VerificationPanel.js
Normal 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);
|
||||
}
|
||||
}
|
|
@ -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
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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] || {}),
|
||||
));
|
||||
|
||||
|
|
|
@ -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,
|
||||
|
|
57
src/components/views/toasts/NewSessionToast.js
Normal file
57
src/components/views/toasts/NewSessionToast.js
Normal 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>);
|
||||
}
|
||||
}
|
|
@ -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,
|
||||
};
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue