Merge branch 'experimental' into travis/settings/positive

This commit is contained in:
Travis Ralston
2019-01-28 08:25:40 -07:00
committed by GitHub
50 changed files with 2044 additions and 873 deletions

View File

@@ -30,11 +30,6 @@ class HomePage extends React.Component {
static displayName = 'HomePage';
static propTypes = {
// URL base of the team server. Optional.
teamServerUrl: PropTypes.string,
// Team token. Optional. If set, used to get the static homepage of the team
// associated. If unset, homePageUrl will be used.
teamToken: PropTypes.string,
// URL to use as the iFrame src. Defaults to /home.html.
homePageUrl: PropTypes.string,
};
@@ -56,35 +51,29 @@ class HomePage extends React.Component {
componentWillMount() {
this._unmounted = false;
if (this.props.teamToken && this.props.teamServerUrl) {
this.setState({
iframeSrc: `${this.props.teamServerUrl}/static/${this.props.teamToken}/home.html`,
});
} else {
// we use request() to inline the homepage into the react component
// so that it can inherit CSS and theming easily rather than mess around
// with iframes and trying to synchronise document.stylesheets.
// we use request() to inline the homepage into the react component
// so that it can inherit CSS and theming easily rather than mess around
// with iframes and trying to synchronise document.stylesheets.
const src = this.props.homePageUrl || 'home.html';
const src = this.props.homePageUrl || 'home.html';
request(
{ method: "GET", url: src },
(err, response, body) => {
if (this._unmounted) {
return;
}
request(
{ method: "GET", url: src },
(err, response, body) => {
if (this._unmounted) {
return;
}
if (err || response.status < 200 || response.status >= 300) {
console.warn(`Error loading home page: ${err}`);
this.setState({ page: _t("Couldn't load home page") });
return;
}
if (err || response.status < 200 || response.status >= 300) {
console.warn(`Error loading home page: ${err}`);
this.setState({ page: _t("Couldn't load home page") });
return;
}
body = body.replace(/_t\(['"]([\s\S]*?)['"]\)/mg, (match, g1)=>this.translate(g1));
this.setState({ page: body });
},
);
}
body = body.replace(/_t\(['"]([\s\S]*?)['"]\)/mg, (match, g1)=>this.translate(g1));
this.setState({ page: body });
},
);
}
componentWillUnmount() {

View File

@@ -212,6 +212,7 @@ const LeftPanel = React.createClass({
<CallPreview ConferenceHandler={VectorConferenceHandler} />
<RoomList
ref={this.collectRoomList}
toolbarShown={this.props.toolbarShown}
collapsed={this.props.collapsed}
searchFilter={this.state.searchFilter}
ConferenceHandler={VectorConferenceHandler} />

View File

@@ -63,7 +63,6 @@ const LoggedInView = React.createClass({
// transitioned to PWLU)
onRegistered: PropTypes.func,
collapsedRhs: PropTypes.bool,
teamToken: PropTypes.string,
// Used by the RoomView to handle joining rooms
viaServers: PropTypes.arrayOf(PropTypes.string),
@@ -457,8 +456,6 @@ const LoggedInView = React.createClass({
pageElement = <UserSettings
onClose={this.props.onCloseAllSettings}
brand={this.props.config.brand}
referralBaseUrl={this.props.config.referralBaseUrl}
teamToken={this.props.teamToken}
/>;
break;
@@ -475,15 +472,7 @@ const LoggedInView = React.createClass({
case PageTypes.HomePage:
{
// If team server config is present, pass the teamServerURL. props.teamToken
// must also be set for the team page to be displayed, otherwise the
// welcomePageUrl is used (which might be undefined).
const teamServerUrl = this.props.config.teamServerConfig ?
this.props.config.teamServerConfig.teamServerURL : null;
pageElement = <HomePage
teamServerUrl={teamServerUrl}
teamToken={this.props.teamToken}
homePageUrl={this.props.config.welcomePageUrl}
/>;
}
@@ -556,6 +545,7 @@ const LoggedInView = React.createClass({
<DragDropContext onDragEnd={this._onDragEnd}>
<div ref={this._setResizeContainerRef} className={bodyClasses}>
<LeftPanel
toolbarShown={!!topBar}
collapsed={this.props.collapseLhs || this.state.collapseLhs || false}
disabled={this.props.leftDisabled}
/>

View File

@@ -75,8 +75,8 @@ const VIEWS = {
// 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 (a) we need to clear out indexeddb, and (b) we need to
// talk to the team server; while it is going on we show a big spinner.
// process because we need to clear out indexeddb. While it is going on we
// show a big spinner.
LOGGING_IN: 5,
// we are logged in with an active matrix client.
@@ -256,42 +256,6 @@ export default React.createClass({
MatrixClientPeg.opts.initialSyncLimit = this.props.config.sync_timeline_limit;
}
// To enable things like riot.im/geektime in a nicer way than rewriting the URL
// and appending a team token query parameter, use the first path segment to
// indicate a team, with "public" team tokens stored in the config teamTokenMap.
let routedTeamToken = null;
if (this.props.config.teamTokenMap) {
const teamName = window.location.pathname.split('/')[1];
if (teamName && this.props.config.teamTokenMap.hasOwnProperty(teamName)) {
routedTeamToken = this.props.config.teamTokenMap[teamName];
}
}
// Persist the team token across refreshes using sessionStorage. A new window or
// tab will not persist sessionStorage, but refreshes will.
if (this.props.startingFragmentQueryParams.team_token) {
window.sessionStorage.setItem(
'mx_team_token',
this.props.startingFragmentQueryParams.team_token,
);
}
// Use the locally-stored team token first, then as a fall-back, check to see if
// a referral link was used, which will contain a query parameter `team_token`.
this._teamToken = routedTeamToken ||
window.localStorage.getItem('mx_team_token') ||
window.sessionStorage.getItem('mx_team_token');
// Some users have ended up with "undefined" as their local storage team token,
// treat that as undefined.
if (this._teamToken === "undefined") {
this._teamToken = undefined;
}
if (this._teamToken) {
console.info(`Team token set to ${this._teamToken}`);
}
// Set up the default URLs (async)
if (this.getDefaultServerName() && !this.getDefaultHsUrl(false)) {
this.setState({loadingDefaultHomeserver: true});
@@ -360,9 +324,6 @@ export default React.createClass({
linkifyMatrix.onGroupClick = this.onGroupClick;
}
const teamServerConfig = this.props.config.teamServerConfig || {};
Lifecycle.initRtsClient(teamServerConfig.teamServerURL);
// the first thing to do is to try the token params in the query-string
Lifecycle.attemptTokenLogin(this.props.realQueryParams).then((loggedIn) => {
if (loggedIn) {
@@ -613,7 +574,7 @@ export default React.createClass({
case 'view_user_settings': {
if (SettingsStore.isFeatureEnabled("feature_tabbed_settings")) {
const UserSettingsDialog = sdk.getComponent("dialogs.UserSettingsDialog");
Modal.createTrackedDialog('User settings', '', UserSettingsDialog, {});
Modal.createTrackedDialog('User settings', '', UserSettingsDialog, {}, 'mx_SettingsDialog');
} else {
this._setPage(PageTypes.UserSettings);
this.notifyNewScreen('settings');
@@ -726,7 +687,7 @@ export default React.createClass({
});
break;
case 'on_logged_in':
this._onLoggedIn(payload.teamToken);
this._onLoggedIn();
break;
case 'on_logged_out':
this._onLoggedOut();
@@ -1196,16 +1157,10 @@ export default React.createClass({
/**
* Called when a new logged in session has started
*
* @param {string} teamToken
*/
_onLoggedIn: async function(teamToken) {
_onLoggedIn: async function() {
this.setStateForNewView({view: VIEWS.LOGGED_IN});
if (teamToken) {
// A team member has logged in, not a guest
this._teamToken = teamToken;
dis.dispatch({action: 'view_home_page'});
} else if (this._is_registered) {
if (this._is_registered) {
this._is_registered = false;
if (this.props.config.welcomeUserId && getCurrentLanguage().startsWith("en")) {
@@ -1261,7 +1216,6 @@ export default React.createClass({
currentRoomId: null,
page_type: PageTypes.RoomDirectory,
});
this._teamToken = null;
this._setPageSubtitle();
},
@@ -1278,6 +1232,7 @@ export default React.createClass({
this.firstSyncComplete = false;
this.firstSyncPromise = Promise.defer();
const cli = MatrixClientPeg.get();
const IncomingSasDialog = sdk.getComponent('views.dialogs.IncomingSasDialog');
// Allow the JS SDK to reap timeline events. This reduces the amount of
// memory consumed as the JS SDK stores multiple distinct copies of room
@@ -1477,6 +1432,12 @@ export default React.createClass({
}
});
cli.on("crypto.verification.start", (verifier) => {
Modal.createTrackedDialog('Incoming Verification', '', IncomingSasDialog, {
verifier,
});
});
// Fire the tinter right on startup to ensure the default theme is applied
// A later sync can/will correct the tint to be the right value for the user
const colorScheme = SettingsStore.getValue("roomColor");
@@ -1707,15 +1668,13 @@ export default React.createClass({
onReturnToAppClick: function() {
// treat it the same as if the user had completed the login
this._onLoggedIn(null);
this._onLoggedIn();
},
// returns a promise which resolves to the new MatrixClient
onRegistered: function(credentials, teamToken) {
// XXX: These both should be in state or ideally store(s) because we risk not
onRegistered: function(credentials) {
// XXX: This should be in state or ideally store(s) because we risk not
// rendering the most up-to-date view of state otherwise.
// teamToken may not be truthy
this._teamToken = teamToken;
this._is_registered = true;
return Lifecycle.setLoggedIn(credentials);
},
@@ -1888,7 +1847,6 @@ export default React.createClass({
onCloseAllSettings={this.onCloseAllSettings}
onRegistered={this.onRegistered}
currentRoomId={this.state.currentRoomId}
teamToken={this._teamToken}
showCookieBar={this.state.showCookieBar}
{...this.props}
{...this.state}
@@ -1929,7 +1887,6 @@ export default React.createClass({
defaultHsUrl={this.getDefaultHsUrl()}
defaultIsUrl={this.getDefaultIsUrl()}
brand={this.props.config.brand}
teamServerConfig={this.props.config.teamServerConfig}
customHsUrl={this.getCurrentHsUrl()}
customIsUrl={this.getCurrentIsUrl()}
makeRegistrationUrl={this._makeRegistrationUrl}

View File

@@ -313,6 +313,12 @@ const RoomSubList = React.createClass({
}
},
setHeight: function(height) {
if (this.refs.subList) {
this.refs.subList.style.height = `${height}px`;
}
},
render: function() {
const len = this.props.list.length + this.props.extraTiles.length;
if (len) {
@@ -322,13 +328,13 @@ const RoomSubList = React.createClass({
"mx_RoomSubList_nonEmpty": len && !this.state.hidden,
});
if (this.state.hidden) {
return <div className={subListClasses}>
return <div ref="subList" className={subListClasses}>
{this._getHeaderJsx()}
</div>;
} else {
const tiles = this.makeRoomTiles();
tiles.push(...this.props.extraTiles);
return <div className={subListClasses}>
return <div ref="subList" className={subListClasses}>
{this._getHeaderJsx()}
<IndicatorScrollbar ref="scroller" className="mx_RoomSubList_scroll">
{ tiles }
@@ -343,7 +349,7 @@ const RoomSubList = React.createClass({
}
return (
<div className="mx_RoomSubList">
<div ref="subList" className="mx_RoomSubList">
{ this._getHeaderJsx() }
{ content }
</div>

View File

@@ -167,13 +167,6 @@ module.exports = React.createClass({
onClose: PropTypes.func,
// The brand string given when creating email pushers
brand: PropTypes.string,
// The base URL to use in the referral link. Defaults to window.location.origin.
referralBaseUrl: PropTypes.string,
// Team token for the referral link. If falsy, the referral section will
// not appear
teamToken: PropTypes.string,
},
getDefaultProps: function() {
@@ -590,27 +583,6 @@ module.exports = React.createClass({
return <GroupUserSettings />;
},
_renderReferral: function() {
const teamToken = this.props.teamToken;
if (!teamToken) {
return null;
}
if (typeof teamToken !== 'string') {
console.warn('Team token not a string');
return null;
}
const href = (this.props.referralBaseUrl || window.location.origin) +
`/#/register?referrer=${this._me}&team_token=${teamToken}`;
return (
<div>
<h3>Referral</h3>
<div className="mx_UserSettings_section">
{ _t("Refer a friend to Riot:") } <a href={href} target="_blank" rel="noopener">{ href }</a>
</div>
</div>
);
},
onLanguageChange: function(newLang) {
if (this.state.language !== newLang) {
SettingsStore.setValue("language", null, SettingLevel.DEVICE, newLang);
@@ -1355,8 +1327,6 @@ module.exports = React.createClass({
{ this._renderGroupSettings() }
{ this._renderReferral() }
{ notificationArea }
{ this._renderUserInterfaceSettings() }

View File

@@ -23,9 +23,7 @@ import React from 'react';
import PropTypes from 'prop-types';
import sdk from '../../../index';
import MatrixClientPeg from '../../../MatrixClientPeg';
import RegistrationForm from '../../views/auth/RegistrationForm';
import RtsClient from '../../../RtsClient';
import { _t, _td } from '../../../languageHandler';
import SdkConfig from '../../../SdkConfig';
import { messageForResourceLimitError } from '../../../utils/ErrorUtils';
@@ -48,13 +46,6 @@ module.exports = React.createClass({
brand: PropTypes.string,
email: PropTypes.string,
referrer: PropTypes.string,
teamServerConfig: PropTypes.shape({
// Email address to request new teams
supportEmail: PropTypes.string.isRequired,
// URL of the riot-team-server to get team configurations and track referrals
teamServerURL: PropTypes.string.isRequired,
}),
teamSelected: PropTypes.object,
// The default server name to use when the user hasn't specified
// one. This is used when displaying the defaultHsUrl in the UI.
@@ -70,18 +61,11 @@ module.exports = React.createClass({
onLoginClick: PropTypes.func.isRequired,
onCancelClick: PropTypes.func,
onServerConfigChange: PropTypes.func.isRequired,
rtsClient: PropTypes.shape({
getTeamsConfig: PropTypes.func.isRequired,
trackReferral: PropTypes.func.isRequired,
getTeam: PropTypes.func.isRequired,
}),
},
getInitialState: function() {
return {
busy: false,
teamServerBusy: false,
errorText: null,
// We remember the values entered by the user because
// the registration form will be unmounted during the
@@ -106,37 +90,7 @@ module.exports = React.createClass({
componentWillMount: function() {
this._unmounted = false;
this._replaceClient();
if (
this.props.teamServerConfig &&
this.props.teamServerConfig.teamServerURL &&
!this._rtsClient
) {
this._rtsClient = this.props.rtsClient || new RtsClient(this.props.teamServerConfig.teamServerURL);
this.setState({
teamServerBusy: true,
});
// GET team configurations including domains, names and icons
this._rtsClient.getTeamsConfig().then((data) => {
const teamsConfig = {
teams: data,
supportEmail: this.props.teamServerConfig.supportEmail,
};
console.log('Setting teams config to ', teamsConfig);
this.setState({
teamsConfig: teamsConfig,
teamServerBusy: false,
});
}, (err) => {
console.error('Error retrieving config for teams', err);
this.setState({
teamServerBusy: false,
});
});
}
},
onServerConfigChange: function(config) {
@@ -191,7 +145,7 @@ module.exports = React.createClass({
});
},
_onUIAuthFinished: function(success, response, extra) {
_onUIAuthFinished: async function(success, response, extra) {
if (!success) {
let msg = response.message || response.toString();
// can we give a better error message?
@@ -240,58 +194,15 @@ module.exports = React.createClass({
doingUIAuth: false,
});
// Done regardless of `teamSelected`. People registering with non-team emails
// will just nop. The point of this being we might not have the email address
// that the user registered with at this stage (depending on whether this
// is the client they initiated registration).
let trackPromise = Promise.resolve(null);
if (this._rtsClient && extra.emailSid) {
// Track referral if this.props.referrer set, get team_token in order to
// retrieve team config and see welcome page etc.
trackPromise = this._rtsClient.trackReferral(
this.props.referrer || '', // Default to empty string = not referred
extra.emailSid,
extra.clientSecret,
).then((data) => {
const teamToken = data.team_token;
// Store for use /w welcome pages
window.localStorage.setItem('mx_team_token', teamToken);
this._rtsClient.getTeam(teamToken).then((team) => {
console.log(
`User successfully registered with team ${team.name}`,
);
if (!team.rooms) {
return;
}
// Auto-join rooms
team.rooms.forEach((room) => {
if (room.auto_join && room.room_id) {
console.log(`Auto-joining ${room.room_id}`);
MatrixClientPeg.get().joinRoom(room.room_id);
}
});
}, (err) => {
console.error('Error getting team config', err);
});
return teamToken;
}, (err) => {
console.error('Error tracking referral', err);
});
}
trackPromise.then((teamToken) => {
return this.props.onLoggedIn({
userId: response.user_id,
deviceId: response.device_id,
homeserverUrl: this._matrixClient.getHomeserverUrl(),
identityServerUrl: this._matrixClient.getIdentityServerUrl(),
accessToken: response.access_token,
}, teamToken);
}).then((cli) => {
return this._setupPushers(cli);
const cli = await this.props.onLoggedIn({
userId: response.user_id,
deviceId: response.device_id,
homeserverUrl: this._matrixClient.getHomeserverUrl(),
identityServerUrl: this._matrixClient.getIdentityServerUrl(),
accessToken: response.access_token,
});
this._setupPushers(cli);
},
_setupPushers: function(matrixClient) {
@@ -356,12 +267,6 @@ module.exports = React.createClass({
});
},
onTeamSelected: function(teamSelected) {
if (!this._unmounted) {
this.setState({ teamSelected });
}
},
onLoginClick: function(ev) {
ev.preventDefault();
ev.stopPropagation();
@@ -418,7 +323,7 @@ module.exports = React.createClass({
poll={true}
/>
);
} else if (this.state.busy || this.state.teamServerBusy || !this.state.flows) {
} else if (this.state.busy || !this.state.flows) {
registerBody = <Spinner />;
} else {
let serverConfigSection;
@@ -443,11 +348,9 @@ module.exports = React.createClass({
defaultPhoneCountry={this.state.formVals.phoneCountry}
defaultPhoneNumber={this.state.formVals.phoneNumber}
defaultPassword={this.state.formVals.password}
teamsConfig={this.state.teamsConfig}
minPasswordLength={MIN_PASSWORD_LENGTH}
onError={this.onFormValidationFailed}
onRegisterClick={this.onFormSubmit}
onTeamSelected={this.onTeamSelected}
flows={this.state.flows}
/>
{ serverConfigSection }
@@ -472,12 +375,7 @@ module.exports = React.createClass({
return (
<AuthPage>
<AuthHeader
icon={this.state.teamSelected ?
this.props.teamServerConfig.teamServerURL + "/static/common/" +
this.state.teamSelected.domain + "/icon.png" :
null}
/>
<AuthHeader />
<AuthBody>
<h2>{ _t('Create your account') }</h2>
{ registerBody }

View File

@@ -46,17 +46,6 @@ module.exports = React.createClass({
defaultPhoneNumber: PropTypes.string,
defaultUsername: PropTypes.string,
defaultPassword: PropTypes.string,
teamsConfig: PropTypes.shape({
// Email address to request new teams
supportEmail: PropTypes.string,
teams: PropTypes.arrayOf(PropTypes.shape({
// The displayed name of the team
"name": PropTypes.string,
// The domain of team email addresses
"domain": PropTypes.string,
})).required,
}),
minPasswordLength: PropTypes.number,
onError: PropTypes.func,
onRegisterClick: PropTypes.func.isRequired, // onRegisterClick(Object) => ?Promise
@@ -75,7 +64,6 @@ module.exports = React.createClass({
getInitialState: function() {
return {
fieldValid: {},
selectedTeam: null,
// The ISO2 country code selected in the phone number entry
phoneCountry: this.props.defaultPhoneCountry,
};
@@ -150,10 +138,6 @@ module.exports = React.createClass({
return true;
},
_isUniEmail: function(email) {
return email.endsWith('.ac.uk') || email.endsWith('.edu') || email.endsWith('matrix.org');
},
validateField: function(fieldID) {
const pwd1 = this.refs.password.value.trim();
const pwd2 = this.refs.passwordConfirm.value.trim();
@@ -161,24 +145,6 @@ module.exports = React.createClass({
switch (fieldID) {
case FIELD_EMAIL: {
const email = this.refs.email.value;
if (this.props.teamsConfig && this._isUniEmail(email)) {
const matchingTeam = this.props.teamsConfig.teams.find(
(team) => {
return email.split('@').pop() === team.domain;
},
) || null;
this.setState({
selectedTeam: matchingTeam,
showSupportEmail: !matchingTeam,
});
this.props.onTeamSelected(matchingTeam);
} else {
this.props.onTeamSelected(null);
this.setState({
selectedTeam: null,
showSupportEmail: false,
});
}
const emailValid = email === '' || Email.looksValid(email);
if (this._authStepIsRequired('m.login.email.identity') && (!emailValid || email === '')) {
this.markFieldValid(fieldID, false, "RegistrationForm.ERR_MISSING_EMAIL");
@@ -304,30 +270,6 @@ module.exports = React.createClass({
value={self.state.email} />
</div>
);
let belowEmailSection;
if (this.props.teamsConfig) {
if (this.props.teamsConfig.supportEmail && this.state.showSupportEmail) {
belowEmailSection = (
<p className="mx_Login_support">
Sorry, but your university is not registered with us just yet.&nbsp;
Email us on&nbsp;
<a href={"mailto:" + this.props.teamsConfig.supportEmail}>
{ this.props.teamsConfig.supportEmail }
</a>&nbsp;
to get your university signed up.
Or continue to register with Riot to enjoy our open source platform.
</p>
);
} else if (this.state.selectedTeam) {
belowEmailSection = (
<p className="mx_Login_support">
{_t("You are registering with %(SelectedTeamName)s", {
SelectedTeamName: this.state.selectedTeam.name,
})}
</p>
);
}
}
const CountryDropdown = sdk.getComponent('views.auth.CountryDropdown');
let phoneSection;
@@ -369,7 +311,6 @@ module.exports = React.createClass({
<div>
<form onSubmit={this.onSubmit}>
{ emailSection }
{ belowEmailSection }
{ phoneSection }
<input type="text" ref="username"
placeholder={placeholderUserName} defaultValue={this.props.defaultUsername}

View File

@@ -1,6 +1,7 @@
/*
Copyright 2016 OpenMarket Ltd
Copyright 2017 Vector Creations Ltd
Copyright 2019 New Vector Ltd
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
@@ -21,58 +22,258 @@ import MatrixClientPeg from '../../../MatrixClientPeg';
import sdk from '../../../index';
import * as FormattingUtils from '../../../utils/FormattingUtils';
import { _t } from '../../../languageHandler';
import SettingsStore from '../../../settings/SettingsStore';
import {verificationMethods} from 'matrix-js-sdk/lib/crypto';
export default function DeviceVerifyDialog(props) {
const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog");
const MODE_LEGACY = 'legacy';
const MODE_SAS = 'sas';
const key = FormattingUtils.formatCryptoKey(props.device.getFingerprint());
const body = (
<div>
<p>
{ _t("To verify that this device can be trusted, please contact its " +
"owner using some other means (e.g. in person or a phone call) " +
"and ask them whether the key they see in their User Settings " +
"for this device matches the key below:") }
</p>
<div className="mx_UserSettings_cryptoSection">
<ul>
<li><label>{ _t("Device name") }:</label> <span>{ props.device.getDisplayName() }</span></li>
<li><label>{ _t("Device ID") }:</label> <span><code>{ props.device.deviceId }</code></span></li>
<li><label>{ _t("Device key") }:</label> <span><code><b>{ key }</b></code></span></li>
</ul>
</div>
<p>
{ _t("If it matches, press the verify button below. " +
"If it doesn't, then someone else is intercepting this device " +
"and you probably want to press the blacklist button instead.") }
</p>
<p>
{ _t("In future this verification process will be more sophisticated.") }
</p>
</div>
);
const PHASE_START = 0;
const PHASE_WAIT_FOR_PARTNER_TO_ACCEPT = 1;
const PHASE_SHOW_SAS = 2;
const PHASE_WAIT_FOR_PARTNER_TO_CONFIRM = 3;
const PHASE_VERIFIED = 4;
const PHASE_CANCELLED = 5;
function onFinished(confirm) {
if (confirm) {
MatrixClientPeg.get().setDeviceVerified(
props.userId, props.device.deviceId, true,
);
}
props.onFinished(confirm);
export default class DeviceVerifyDialog extends React.Component {
static propTypes = {
userId: PropTypes.string.isRequired,
device: PropTypes.object.isRequired,
onFinished: PropTypes.func.isRequired,
};
constructor() {
super();
this._verifier = null;
this._showSasEvent = null;
this.state = {
phase: PHASE_START,
mode: SettingsStore.isFeatureEnabled("feature_sas") ? MODE_SAS : MODE_LEGACY,
sasVerified: false,
};
}
return (
<QuestionDialog
title={_t("Verify device")}
description={body}
button={_t("I verify that the keys match")}
onFinished={onFinished}
/>
);
componentWillUnmount() {
if (this._verifier) {
this._verifier.removeListener('show_sas', this._onVerifierShowSas);
this._verifier.cancel('User cancel');
}
}
_onSwitchToLegacyClick = () => {
this.setState({mode: MODE_LEGACY});
}
_onSwitchToSasClick = () => {
this.setState({mode: MODE_SAS});
}
_onCancelClick = () => {
this.props.onFinished(false);
}
_onLegacyFinished = (confirm) => {
if (confirm) {
MatrixClientPeg.get().setDeviceVerified(
this.props.userId, this.props.device.deviceId, true,
);
}
this.props.onFinished(confirm);
}
_onSasRequestClick = () => {
this.setState({
phase: PHASE_WAIT_FOR_PARTNER_TO_ACCEPT,
});
this._verifier = MatrixClientPeg.get().beginKeyVerification(
verificationMethods.SAS, this.props.userId, this.props.device.deviceId,
);
this._verifier.on('show_sas', this._onVerifierShowSas);
this._verifier.verify().then(() => {
this.setState({phase: PHASE_VERIFIED});
this._verifier.removeListener('show_sas', this._onVerifierShowSas);
this._verifier = null;
}).catch((e) => {
console.log("Verification failed", e);
this.setState({
phase: PHASE_CANCELLED,
});
this._verifier = null;
});
}
_onSasMatchesClick = () => {
this._showSasEvent.confirm();
this.setState({
phase: PHASE_WAIT_FOR_PARTNER_TO_CONFIRM,
});
}
_onVerifiedDoneClick = () => {
this.props.onFinished(true);
}
_onVerifierShowSas = (e) => {
this._showSasEvent = e;
this.setState({
phase: PHASE_SHOW_SAS,
});
}
_renderSasVerification() {
let body;
switch (this.state.phase) {
case PHASE_START:
body = this._renderSasVerificationPhaseStart();
break;
case PHASE_WAIT_FOR_PARTNER_TO_ACCEPT:
body = this._renderSasVerificationPhaseWaitAccept();
break;
case PHASE_SHOW_SAS:
body = this._renderSasVerificationPhaseShowSas();
break;
case PHASE_WAIT_FOR_PARTNER_TO_CONFIRM:
body = this._renderSasVerificationPhaseWaitForPartnerToConfirm();
break;
case PHASE_VERIFIED:
body = this._renderSasVerificationPhaseVerified();
break;
case PHASE_CANCELLED:
body = this._renderSasVerificationPhaseCancelled();
break;
}
const BaseDialog = sdk.getComponent("dialogs.BaseDialog");
return (
<BaseDialog
title={_t("Verify device")}
onFinished={this._onCancelClick}
>
{body}
</BaseDialog>
);
}
_renderSasVerificationPhaseStart() {
const AccessibleButton = sdk.getComponent('views.elements.AccessibleButton');
const DialogButtons = sdk.getComponent('views.elements.DialogButtons');
return (
<div>
<AccessibleButton
element="span" className="mx_linkButton" onClick={this._onSwitchToLegacyClick}
>
{_t("Use Legacy Verification (for older clients)")}
</AccessibleButton>
<p>
{ _t("Verify by comparing a short text string.") }
</p>
<p>
{_t(
"For maximum security, we recommend you do this in person or " +
"use another trusted means of communication.",
)}
</p>
<DialogButtons
primaryButton={_t('Begin Verifying')}
hasCancel={true}
onPrimaryButtonClick={this._onSasRequestClick}
onCancel={this._onCancelClick}
/>
</div>
);
}
_renderSasVerificationPhaseWaitAccept() {
const Spinner = sdk.getComponent("views.elements.Spinner");
return (
<div>
<Spinner />
<p>{_t("Waiting for partner to accept...")}</p>
</div>
);
}
_renderSasVerificationPhaseShowSas() {
const VerificationShowSas = sdk.getComponent('views.verification.VerificationShowSas');
return <VerificationShowSas
sas={this._showSasEvent.sas}
onCancel={this._onCancelClick}
onDone={this._onSasMatchesClick}
/>;
}
_renderSasVerificationPhaseWaitForPartnerToConfirm() {
const Spinner = sdk.getComponent('views.elements.Spinner');
return <div>
<Spinner />
<p>{_t(
"Waiting for %(userId)s to confirm...", {userId: this.props.userId},
)}</p>
</div>;
}
_renderSasVerificationPhaseVerified() {
const VerificationComplete = sdk.getComponent('views.verification.VerificationComplete');
return <VerificationComplete onDone={this._onVerifiedDoneClick} />;
}
_renderSasVerificationPhaseCancelled() {
const VerificationCancelled = sdk.getComponent('views.verification.VerificationCancelled');
return <VerificationCancelled onDone={this._onCancelClick} />;
}
_renderLegacyVerification() {
const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog");
const AccessibleButton = sdk.getComponent('views.elements.AccessibleButton');
const key = FormattingUtils.formatCryptoKey(this.props.device.getFingerprint());
const body = (
<div>
<AccessibleButton
element="span" className="mx_linkButton" onClick={this._onSwitchToSasClick}
>
{_t("Use two-way text verification")}
</AccessibleButton>
<p>
{ _t("To verify that this device can be trusted, please contact its " +
"owner using some other means (e.g. in person or a phone call) " +
"and ask them whether the key they see in their User Settings " +
"for this device matches the key below:") }
</p>
<div className="mx_UserSettings_cryptoSection">
<ul>
<li><label>{ _t("Device name") }:</label> <span>{ this.props.device.getDisplayName() }</span></li>
<li><label>{ _t("Device ID") }:</label> <span><code>{ this.props.device.deviceId }</code></span></li>
<li><label>{ _t("Device key") }:</label> <span><code><b>{ key }</b></code></span></li>
</ul>
</div>
<p>
{ _t("If it matches, press the verify button below. " +
"If it doesn't, then someone else is intercepting this device " +
"and you probably want to press the blacklist button instead.") }
</p>
</div>
);
return (
<QuestionDialog
title={_t("Verify device")}
description={body}
button={_t("I verify that the keys match")}
onFinished={this._onLegacyFinished}
/>
);
}
render() {
if (this.state.mode === MODE_LEGACY) {
return this._renderLegacyVerification();
} else {
return <div>
{this._renderSasVerification()}
</div>;
}
}
}
DeviceVerifyDialog.propTypes = {
userId: PropTypes.string.isRequired,
device: PropTypes.object.isRequired,
onFinished: PropTypes.func.isRequired,
};

View File

@@ -0,0 +1,182 @@
/*
Copyright 2019 New Vector Ltd
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 sdk from '../../../index';
import { _t } from '../../../languageHandler';
const PHASE_START = 0;
const PHASE_SHOW_SAS = 1;
const PHASE_WAIT_FOR_PARTNER_TO_CONFIRM = 2;
const PHASE_VERIFIED = 3;
const PHASE_CANCELLED = 4;
export default class IncomingSasDialog extends React.Component {
static propTypes = {
verifier: PropTypes.object.isRequired,
};
constructor(props) {
super(props);
this._showSasEvent = null;
this.state = {
phase: PHASE_START,
sasVerified: false,
};
this.props.verifier.on('show_sas', this._onVerifierShowSas);
this.props.verifier.on('cancel', this._onVerifierCancel);
}
componentWillUnmount() {
if (this.state.phase !== PHASE_CANCELLED && this.state.phase !== PHASE_VERIFIED) {
this.props.verifier.cancel('User cancel');
}
this.props.verifier.removeListener('show_sas', this._onVerifierShowSas);
}
_onFinished = () => {
this.props.onFinished(this.state.phase === PHASE_VERIFIED);
}
_onCancelClick = () => {
this.props.onFinished(this.state.phase === PHASE_VERIFIED);
}
_onContinueClick = () => {
this.setState({phase: PHASE_WAIT_FOR_PARTNER_TO_CONFIRM});
this.props.verifier.verify().then(() => {
this.setState({phase: PHASE_VERIFIED});
}).catch((e) => {
console.log("Verification failed", e);
});
}
_onVerifierShowSas = (e) => {
this._showSasEvent = e;
this.setState({
phase: PHASE_SHOW_SAS,
sas: e.sas,
});
}
_onVerifierCancel = (e) => {
this.setState({
phase: PHASE_CANCELLED,
});
}
_onSasMatchesClick = () => {
this._showSasEvent.confirm();
this.setState({
phase: PHASE_WAIT_FOR_PARTNER_TO_CONFIRM,
});
}
_onVerifiedDoneClick = () => {
this.props.onFinished(true);
}
_renderPhaseStart() {
const DialogButtons = sdk.getComponent('views.elements.DialogButtons');
return (
<div>
<h2>{this.props.verifier.userId}</h2>
<p>{_t(
"Verify this user to mark them as trusted. " +
"Trusting users gives you extra peace of mind when using " +
"end-to-end encrypted messages.",
)}</p>
<p>{_t(
// NB. Below wording adjusted to singular 'device' until we have
// cross-signing
"Verifying this user will mark their device as trusted, and " +
"also mark your device as trusted to them.",
)}</p>
<DialogButtons
primaryButton={_t('Continue')}
hasCancel={true}
onPrimaryButtonClick={this._onContinueClick}
onCancel={this._onCancelClick}
/>
</div>
);
}
_renderPhaseShowSas() {
const VerificationShowSas = sdk.getComponent('views.verification.VerificationShowSas');
return <VerificationShowSas
sas={this._showSasEvent.sas}
onCancel={this._onCancelClick}
onDone={this._onSasMatchesClick}
/>;
}
_renderPhaseWaitForPartnerToConfirm() {
const Spinner = sdk.getComponent("views.elements.Spinner");
return (
<div>
<Spinner />
<p>{_t("Waiting for partner to confirm...")}</p>
</div>
);
}
_renderPhaseVerified() {
const VerificationComplete = sdk.getComponent('views.verification.VerificationComplete');
return <VerificationComplete onDone={this._onVerifiedDoneClick} />;
}
_renderPhaseCancelled() {
const VerificationCancelled = sdk.getComponent('views.verification.VerificationCancelled');
return <VerificationCancelled onDone={this._onCancelClick} />;
}
render() {
let body;
switch (this.state.phase) {
case PHASE_START:
body = this._renderPhaseStart();
break;
case PHASE_SHOW_SAS:
body = this._renderPhaseShowSas();
break;
case PHASE_WAIT_FOR_PARTNER_TO_CONFIRM:
body = this._renderPhaseWaitForPartnerToConfirm();
break;
case PHASE_VERIFIED:
body = this._renderPhaseVerified();
break;
case PHASE_CANCELLED:
body = this._renderPhaseCancelled();
break;
}
const BaseDialog = sdk.getComponent("dialogs.BaseDialog");
return (
<BaseDialog
title={_t("Incoming Verification Request")}
onFinished={this._onFinished}
>
{body}
</BaseDialog>
);
}
}

View File

@@ -138,7 +138,9 @@ export default class LogoutDialog extends React.Component {
// once you can restorew a backup by verifying a device
description={_t(
"When signing in again, you can access encrypted chat history by " +
"restoring your key backup. You'll need your recovery key.",
"restoring your key backup. You'll need your recovery passphrase " +
"or, if you didn't set a recovery passphrase, your recovery key " +
"(that you downloaded).",
)}
button={_t("Sign out")}
onFinished={this._onFinished}

View File

@@ -0,0 +1,90 @@
/*
Copyright 2019 New Vector Ltd
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 {Tab, TabbedView} from "../../structures/TabbedView";
import {_t, _td} from "../../../languageHandler";
import AccessibleButton from "../elements/AccessibleButton";
import dis from '../../../dispatcher';
// TODO: Ditch this whole component
export class TempTab extends React.Component {
static propTypes = {
onClose: PropTypes.func.isRequired,
};
componentDidMount(): void {
dis.dispatch({action: "open_old_room_settings"});
this.props.onClose();
}
render() {
return <div>Hello World</div>;
}
}
export default class UserSettingsDialog extends React.Component {
static propTypes = {
onFinished: PropTypes.func.isRequired,
};
_getTabs() {
const tabs = [];
tabs.push(new Tab(
_td("General"),
"mx_RoomSettingsDialog_settingsIcon",
<div>General Test</div>,
));
tabs.push(new Tab(
_td("Security & Privacy"),
"mx_RoomSettingsDialog_securityIcon",
<div>Security Test</div>,
));
tabs.push(new Tab(
_td("Roles & Permissions"),
"mx_RoomSettingsDialog_rolesIcon",
<div>Roles Test</div>,
));
tabs.push(new Tab(
_td("Advanced"),
"mx_RoomSettingsDialog_warningIcon",
<div>Advanced Test</div>,
));
tabs.push(new Tab(
_td("Visit old settings"),
"mx_RoomSettingsDialog_warningIcon",
<TempTab onClose={this.props.onFinished} />,
));
return tabs;
}
render() {
return (
<div className="mx_RoomSettingsDialog">
<div className="mx_SettingsDialog_header">
{_t("Settings")}
<span className="mx_SettingsDialog_close">
<AccessibleButton className="mx_SettingsDialog_closeIcon" onClick={this.props.onFinished} />
</span>
</div>
<TabbedView tabs={this._getTabs()} />
</div>
);
}
}

View File

@@ -193,9 +193,6 @@ export default React.createClass({
return;
}
// XXX Implement RTS /register here
const teamToken = null;
this.props.onFinished(true, {
userId: response.user_id,
deviceId: response.device_id,
@@ -203,7 +200,6 @@ export default React.createClass({
identityServerUrl: this._matrixClient.getIdentityServerUrl(),
accessToken: response.access_token,
password: this._generatedPassword,
teamToken: teamToken,
});
},

View File

@@ -23,9 +23,11 @@ import GeneralSettingsTab from "../settings/tabs/GeneralSettingsTab";
import dis from '../../../dispatcher';
import SettingsStore from "../../../settings/SettingsStore";
import LabsSettingsTab from "../settings/tabs/LabsSettingsTab";
import SecuritySettingsTab from "../settings/tabs/SecuritySettingsTab";
import NotificationSettingsTab from "../settings/tabs/NotificationSettingsTab";
import PreferencesSettingsTab from "../settings/tabs/PreferencesSettingsTab";
import VoiceSettingsTab from "../settings/tabs/VoiceSettingsTab";
import HelpSettingsTab from "../settings/tabs/HelpSettingsTab";
// TODO: Ditch this whole component
export class TempTab extends React.Component {
@@ -74,7 +76,7 @@ export default class UserSettingsDialog extends React.Component {
tabs.push(new Tab(
_td("Security & Privacy"),
"mx_UserSettingsDialog_securityIcon",
<div>Security Test</div>,
<SecuritySettingsTab />,
));
if (SettingsStore.getLabsFeatures().length > 0) {
tabs.push(new Tab(
@@ -86,7 +88,7 @@ export default class UserSettingsDialog extends React.Component {
tabs.push(new Tab(
_td("Help & About"),
"mx_UserSettingsDialog_helpIcon",
<div>Help Test</div>,
<HelpSettingsTab closeSettingsFn={this.props.onFinished} />,
));
tabs.push(new Tab(
_td("Visit old settings"),
@@ -100,10 +102,10 @@ export default class UserSettingsDialog extends React.Component {
render() {
return (
<div className="mx_UserSettingsDialog">
<div className="mx_UserSettingsDialog_header">
<div className="mx_SettingsDialog_header">
{_t("Settings")}
<span className="mx_UserSettingsDialog_close">
<AccessibleButton className="mx_UserSettingsDialog_closeIcon" onClick={this.props.onFinished} />
<span className="mx_SettingsDialog_close">
<AccessibleButton className="mx_SettingsDialog_closeIcon" onClick={this.props.onFinished} />
</span>
</div>
<TabbedView tabs={this._getTabs()} />

View File

@@ -42,7 +42,9 @@ export default React.createClass({
componentWillUnmount: function() {
const cli = MatrixClientPeg.get();
cli.removeListener("deviceVerificationChanged", this.onDeviceVerificationChanged);
if (cli) {
cli.removeListener("deviceVerificationChanged", this.onDeviceVerificationChanged);
}
},
onDeviceVerificationChanged: function(userId, deviceId, deviceInfo) {

View File

@@ -25,7 +25,7 @@ export default class Field extends React.PureComponent {
type: PropTypes.string,
// The field's label string.
label: PropTypes.string,
// The field's placeholder string.
// The field's placeholder string. Defaults to the label.
placeholder: PropTypes.string,
// The type of field to create. Defaults to "input". Should be "input" or "select".
// To define options for a select, use <Field><option ... /></Field>
@@ -55,6 +55,7 @@ export default class Field extends React.PureComponent {
// Set some defaults for the element
extraProps.type = extraProps.type || "text";
extraProps.ref = "fieldInput";
extraProps.placeholder = extraProps.placeholder || extraProps.label;
const element = this.props.element || "input";
const fieldInput = React.createElement(element, extraProps, this.props.children);

View File

@@ -0,0 +1,103 @@
/*
Copyright 2019 New Vector Ltd.
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 classnames from 'classnames';
import sdk from '../../../index';
class HexVerifyPair extends React.Component {
static propTypes = {
text: PropTypes.string.isRequired,
index: PropTypes.number,
verified: PropTypes.bool,
onChange: PropTypes.func.isRequired,
}
_onClick = () => {
this.setState({verified: !this.props.verified});
this.props.onChange(this.props.index, !this.props.verified);
}
render() {
const classNames = {
mx_HexVerify_pair: true,
mx_HexVerify_pair_verified: this.props.verified,
};
const AccessibleButton = sdk.getComponent('views.elements.AccessibleButton');
return <AccessibleButton className={classnames(classNames)}
onClick={this._onClick}
>
{this.props.text}
</AccessibleButton>;
}
}
/*
* Helps a user verify a hexadecimal code matches one displayed
* elsewhere (eg. on a different device)
*/
export default class HexVerify extends React.Component {
static propTypes = {
text: PropTypes.string.isRequired,
onVerifiedStateChange: PropTypes.func,
}
static defaultProps = {
onVerifiedStateChange: function() {},
}
constructor(props) {
super(props);
this.state = {
pairsVerified: [],
};
for (let i = 0; i < props.text.length; i += 2) {
this.state.pairsVerified.push(false);
}
}
_onPairChange = (index, newVal) => {
const oldVerified = this.state.pairsVerified.reduce((acc, val) => {
return acc && val;
}, true);
const newPairsVerified = this.state.pairsVerified.slice(0);
newPairsVerified[index] = newVal;
const newVerified = newPairsVerified.reduce((acc, val) => {
return acc && val;
}, true);
this.setState({pairsVerified: newPairsVerified});
if (oldVerified !== newVerified) {
this.props.onVerifiedStateChange(newVerified);
}
}
render() {
const pairs = [];
for (let i = 0; i < this.props.text.length / 2; ++i) {
pairs.push(<HexVerifyPair key={i} index={i}
text={this.props.text.substr(i * 2, 2)}
verified={this.state.pairsVerified[i]}
onChange={this._onPairChange}
/>);
}
return <div className="mx_HexVerify">
{pairs}
</div>;
}
}

View File

@@ -36,7 +36,8 @@ import GroupStore from '../../../stores/GroupStore';
import RoomSubList from '../../structures/RoomSubList';
import ResizeHandle from '../elements/ResizeHandle';
import {Resizer, RoomSubListDistributor} from '../../../resizer'
import {Resizer} from '../../../resizer'
import {Layout, Distributor} from '../../../resizer/distributors/roomsublist2';
const HIDE_CONFERENCE_CHANS = true;
const STANDARD_TAGS_REGEX = /^(m\.(favourite|lowpriority|server_notice)|im\.vector\.fake\.(invite|recent|direct|archived))$/;
@@ -79,6 +80,23 @@ module.exports = React.createClass({
const collapsedJson = window.localStorage.getItem("mx_roomlist_collapsed");
this.subListSizes = sizesJson ? JSON.parse(sizesJson) : {};
this.collapsedState = collapsedJson ? JSON.parse(collapsedJson) : {};
this._layoutSections = [];
this._layout = new Layout((key, size) => {
const subList = this._subListRefs[key];
if (subList) {
subList.setHeight(size);
}
// update overflow indicators
this._checkSubListsOverflow();
// don't store height for collapsed sublists
if(!this.collapsedState[key]) {
this.subListSizes[key] = size;
window.localStorage.setItem("mx_roomlist_sizes",
JSON.stringify(this.subListSizes));
}
}, this.subListSizes, this.collapsedState);
return {
isLoadingLeftRooms: false,
totalRoomCount: null,
@@ -146,54 +164,38 @@ module.exports = React.createClass({
this._delayedRefreshRoomListLoopCount = 0;
},
_onSubListResize: function(newSize, id) {
if (!id) {
return;
}
if (typeof newSize === "string") {
newSize = Number.MAX_SAFE_INTEGER;
}
if (newSize === null) {
delete this.subListSizes[id];
} else {
this.subListSizes[id] = newSize;
}
window.localStorage.setItem("mx_roomlist_sizes", JSON.stringify(this.subListSizes));
// update overflow indicators
this._checkSubListsOverflow();
},
componentDidMount: function() {
this.dispatcherRef = dis.register(this.onAction);
const cfg = {
onResized: this._onSubListResize,
layout: this._layout,
};
this.resizer = new Resizer(this.resizeContainer, RoomSubListDistributor, cfg);
this.resizer = new Resizer(this.resizeContainer, Distributor, cfg);
this.resizer.setClassNames({
handle: "mx_ResizeHandle",
vertical: "mx_ResizeHandle_vertical",
reverse: "mx_ResizeHandle_reverse"
});
// load stored sizes
Object.keys(this.subListSizes).forEach((key) => {
this._restoreSubListSize(key);
});
this._layout.update(
this._layoutSections,
this.resizeContainer && this.resizeContainer.offsetHeight,
);
this._checkSubListsOverflow();
this.resizer.attach();
window.addEventListener("resize", this.onWindowResize);
this.mounted = true;
},
componentDidUpdate: function(prevProps) {
this._repositionIncomingCallBox(undefined, false);
if (this.props.searchFilter !== prevProps.searchFilter) {
// restore sizes
Object.keys(this.subListSizes).forEach((key) => {
this._restoreSubListSize(key);
});
this._checkSubListsOverflow();
}
// if (this.props.searchFilter !== prevProps.searchFilter) {
// this._checkSubListsOverflow();
// }
this._layout.update(
this._layoutSections,
this.resizeContainer && this.resizeContainer.clientHeight,
);
// TODO: call layout.setAvailableHeight, window height was changed when bannerShown prop was changed
},
onAction: function(payload) {
@@ -222,6 +224,7 @@ module.exports = React.createClass({
componentWillUnmount: function() {
this.mounted = false;
window.removeEventListener("resize", this.onWindowResize);
dis.unregister(this.dispatcherRef);
if (MatrixClientPeg.get()) {
MatrixClientPeg.get().removeListener("Room", this.onRoom);
@@ -251,6 +254,17 @@ module.exports = React.createClass({
this._delayedRefreshRoomList.cancelPendingCall();
},
onWindowResize: function() {
if (this.mounted && this._layout && this.resizeContainer &&
Array.isArray(this._layoutSections)
) {
this._layout.update(
this._layoutSections,
this.resizeContainer.offsetHeight
);
}
},
onRoom: function(room) {
this.updateVisibleRooms();
},
@@ -551,22 +565,16 @@ module.exports = React.createClass({
this.collapsedState[key] = collapsed;
window.localStorage.setItem("mx_roomlist_collapsed", JSON.stringify(this.collapsedState));
// load the persisted size configuration of the expanded sub list
if (!collapsed) {
this._restoreSubListSize(key);
if (collapsed) {
this._layout.collapseSection(key);
} else {
this._layout.expandSection(key, this.subListSizes[key]);
}
// check overflow, as sub lists sizes have changed
// important this happens after calling resize above
this._checkSubListsOverflow();
},
_restoreSubListSize(key) {
const size = this.subListSizes[key];
const handle = this.resizer.forHandleWithId(key);
if (handle) {
handle.resize(size);
}
},
// check overflow for scroll indicator gradient
_checkSubListsOverflow() {
Object.values(this._subListRefs).forEach(l => l.checkOverflow());
@@ -581,6 +589,7 @@ module.exports = React.createClass({
},
_mapSubListProps: function(subListsProps) {
this._layoutSections = [];
const defaultProps = {
collapsed: this.props.collapsed,
isFiltered: !!this.props.searchFilter,
@@ -599,6 +608,7 @@ module.exports = React.createClass({
return subListsProps.reduce((components, props, i) => {
props = Object.assign({}, defaultProps, props);
const isLast = i === subListsProps.length - 1;
const len = props.list.length + (props.extraTiles ? props.extraTiles.length : 0);
const {key, label, onHeaderClick, ... otherProps} = props;
const chosenKey = key || label;
const onSubListHeaderClick = (collapsed) => {
@@ -608,7 +618,10 @@ module.exports = React.createClass({
}
};
const startAsHidden = props.startAsHidden || this.collapsedState[chosenKey];
this._layoutSections.push({
id: chosenKey,
count: len,
});
let subList = (<RoomSubList
ref={this._subListRef.bind(this, chosenKey)}
startAsHidden={startAsHidden}

View File

@@ -62,7 +62,7 @@ export default class RoomRecoveryReminder extends React.PureComponent {
let unverifiedDevice;
for (const sig of backupSigStatus.sigs) {
if (!sig.device.isVerified()) {
if (sig.device && !sig.device.isVerified()) {
unverifiedDevice = sig.device;
break;
}
@@ -133,6 +133,7 @@ export default class RoomRecoveryReminder extends React.PureComponent {
const AccessibleButton = sdk.getComponent("views.elements.AccessibleButton");
let body;
let primaryCaption = _t("Set up");
if (this.state.error) {
body = <div className="error">
{_t("Unable to load key backup status")}
@@ -140,10 +141,20 @@ export default class RoomRecoveryReminder extends React.PureComponent {
} else if (this.state.unverifiedDevice) {
// A key backup exists for this account, but the creating device is not
// verified.
body = _t(
"To view your secure message history and ensure you can view new " +
"messages on future devices, set up Secure Message Recovery.",
);
body = <div>
<p>{_t(
"Secure Message Recovery has been set up on another device: <deviceName></deviceName>",
{},
{
deviceName: () => <i>{this.state.unverifiedDevice.unsigned.device_display_name}</i>,
},
)}</p>
<p>{_t(
"To view your secure message history and ensure you can view new " +
"messages on future devices, verify that device now.",
)}</p>
</div>;
primaryCaption = _t("Verify device");
} else {
// The default case assumes that a key backup doesn't exist for this account.
// (This component doesn't currently check that itself.)
@@ -167,7 +178,7 @@ export default class RoomRecoveryReminder extends React.PureComponent {
</AccessibleButton>
<AccessibleButton className="mx_RoomRecoveryReminder_button"
onClick={this.onSetupClick}>
{ _t("Set up") }
{primaryCaption}
</AccessibleButton>
</div>
</div>

View File

@@ -257,20 +257,17 @@ export default class KeyBackupPanel extends React.PureComponent {
{uploadStatus}
<div>{backupSigStatuses}</div><br />
<br />
<AccessibleButton className="mx_UserSettings_button"
onClick={this._restoreBackup}>
<AccessibleButton kind="primary" onClick={this._restoreBackup}>
{ _t("Restore backup") }
</AccessibleButton>&nbsp;&nbsp;&nbsp;
<AccessibleButton className="mx_UserSettings_button danger"
onClick={this._deleteBackup}>
<AccessibleButton kind="danger" onClick={this._deleteBackup}>
{ _t("Delete backup") }
</AccessibleButton>
</div>;
} else {
return <div>
{_t("No backup is present")}<br /><br />
<AccessibleButton className="mx_UserSettings_button"
onClick={this._startNewBackup}>
<AccessibleButton kind="primary" onClick={this._startNewBackup}>
{ _t("Start a new backup") }
</AccessibleButton>
</div>;

View File

@@ -0,0 +1,227 @@
/*
Copyright 2019 New Vector Ltd
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, getCurrentLanguage} from "../../../../languageHandler";
import MatrixClientPeg from "../../../../MatrixClientPeg";
import AccessibleButton from "../../elements/AccessibleButton";
import SdkConfig from "../../../../SdkConfig";
import createRoom from "../../../../createRoom";
const packageJson = require('../../../../../package.json');
const Modal = require("../../../../Modal");
const sdk = require("../../../../index");
const PlatformPeg = require("../../../../PlatformPeg");
// if this looks like a release, use the 'version' from package.json; else use
// the git sha. Prepend version with v, to look like riot-web version
const REACT_SDK_VERSION = 'dist' in packageJson ? packageJson.version : packageJson.gitHead || '<local>';
// Simple method to help prettify GH Release Tags and Commit Hashes.
const semVerRegex = /^v?(\d+\.\d+\.\d+(?:-rc.+)?)(?:-(?:\d+-g)?([0-9a-fA-F]+))?(?:-dirty)?$/i;
const ghVersionLabel = function(repo, token='') {
const match = token.match(semVerRegex);
let url;
if (match && match[1]) { // basic semVer string possibly with commit hash
url = (match.length > 1 && match[2])
? `https://github.com/${repo}/commit/${match[2]}`
: `https://github.com/${repo}/releases/tag/v${match[1]}`;
} else {
url = `https://github.com/${repo}/commit/${token.split('-')[0]}`;
}
return <a target="_blank" rel="noopener" href={url}>{ token }</a>;
};
export default class HelpSettingsTab extends React.Component {
static propTypes = {
closeSettingsFn: PropTypes.func.isRequired,
};
constructor() {
super();
this.state = {
vectorVersion: null,
canUpdate: false,
};
}
componentWillMount(): void {
PlatformPeg.get().getAppVersion().then((ver) => this.setState({vectorVersion: ver})).catch((e) => {
console.error("Error getting vector version: ", e);
});
PlatformPeg.get().canSelfUpdate().then((v) => this.setState({canUpdate: v})).catch((e) => {
console.error("Error getting self updatability: ", e);
});
}
_onClearCacheAndReload = (e) => {
if (!PlatformPeg.get()) return;
MatrixClientPeg.get().stopClient();
MatrixClientPeg.get().store.deleteAllData().done(() => {
PlatformPeg.get().reload();
});
};
_onBugReport = (e) => {
const BugReportDialog = sdk.getComponent("dialogs.BugReportDialog");
if (!BugReportDialog) {
return;
}
Modal.createTrackedDialog('Bug Report Dialog', '', BugReportDialog, {});
};
_onStartBotChat = (e) => {
this.props.closeSettingsFn();
createRoom({
dmUserId: SdkConfig.get().welcomeUserId,
andView: true,
});
};
_showSpoiler = (event) => {
const target = event.target;
target.innerHTML = target.getAttribute('data-spoiler');
const range = document.createRange();
range.selectNodeContents(target);
const selection = window.getSelection();
selection.removeAllRanges();
selection.addRange(range);
};
_renderLegal() {
const tocLinks = SdkConfig.get().terms_and_conditions_links;
if (!tocLinks) return null;
const legalLinks = [];
for (const tocEntry of SdkConfig.get().terms_and_conditions_links) {
legalLinks.push(<div key={tocEntry.url}>
<a href={tocEntry.url} rel="noopener" target="_blank">{tocEntry.text}</a>
</div>);
}
return (
<div className='mx_SettingsTab_section mx_HelpSettingsTab_versions'>
<span className='mx_SettingsTab_subheading'>{_t("Legal")}</span>
<div className='mx_SettingsTab_subsectionText'>
{legalLinks}
</div>
</div>
);
}
render() {
let faqText = _t('For help with using Riot, click <a>here</a>.', {}, {
'a': (sub) => <a href="https://about.riot.im/need-help/" rel='noopener' target='_blank'>{sub}</a>,
});
if (SdkConfig.get().welcomeUserId && getCurrentLanguage().startsWith('en')) {
faqText = (
<div>
{
_t('For help with using Riot, click <a>here</a> or start a chat with our ' +
'bot using the button below.', {}, {
'a': (sub) => <a href="https://about.riot.im/need-help/" rel='noopener'
target='_blank'>{sub}</a>,
})
}
<AccessibleButton onClick={this._onStartBotChat} kind='primary'>
{_t("Start a chat with Riot Bot")}
</AccessibleButton>
</div>
);
}
const reactSdkVersion = REACT_SDK_VERSION !== '<local>'
? ghVersionLabel('matrix-org/matrix-react-sdk', REACT_SDK_VERSION)
: REACT_SDK_VERSION;
const vectorVersion = this.state.vectorVersion
? ghVersionLabel('vector-im/riot-web', this.state.vectorVersion)
: 'unknown';
let olmVersion = MatrixClientPeg.get().olmVersion;
olmVersion = olmVersion ? `${olmVersion[0]}.${olmVersion[1]}.${olmVersion[2]}` : '<not-enabled>';
let updateButton = null;
if (this.state.canUpdate) {
const platform = PlatformPeg.get();
updateButton = (
<AccessibleButton onClick={platform.startUpdateCheck} kind='primary'>
{_t('Check for update')}
</AccessibleButton>
);
}
return (
<div className="mx_SettingsTab mx_HelpSettingsTab">
<div className="mx_SettingsTab_heading">{_t("Help & About")}</div>
<div className="mx_SettingsTab_section">
<span className='mx_SettingsTab_subheading'>{_t('Bug reporting')}</span>
<div className='mx_SettingsTab_subsectionText'>
{
_t( "If you've submitted a bug via GitHub, debug logs can help " +
"us track down the problem. Debug logs contain application " +
"usage data including your username, the IDs or aliases of " +
"the rooms or groups you have visited and the usernames of " +
"other users. They do not contain messages.",
)
}
<div className='mx_HelpSettingsTab_debugButton'>
<AccessibleButton onClick={this._onBugReport} kind='primary'>
{_t("Submit debug logs")}
</AccessibleButton>
</div>
<div className='mx_HelpSettingsTab_debugButton'>
<AccessibleButton onClick={this._onClearCacheAndReload} kind='danger'>
{_t("Clear Cache and Reload")}
</AccessibleButton>
</div>
</div>
</div>
<div className='mx_SettingsTab_section'>
<span className='mx_SettingsTab_subheading'>{_t("FAQ")}</span>
<div className='mx_SettingsTab_subsectionText'>
{faqText}
</div>
</div>
<div className='mx_SettingsTab_section mx_HelpSettingsTab_versions'>
<span className='mx_SettingsTab_subheading'>{_t("Versions")}</span>
<div className='mx_SettingsTab_subsectionText'>
{_t("matrix-react-sdk version:")} {reactSdkVersion}<br />
{_t("riot-web version:")} {vectorVersion}<br />
{_t("olm version:")} {olmVersion}<br />
{updateButton}
</div>
</div>
{this._renderLegal()}
<div className='mx_SettingsTab_section mx_HelpSettingsTab_versions'>
<span className='mx_SettingsTab_subheading'>{_t("Advanced")}</span>
<div className='mx_SettingsTab_subsectionText'>
{_t("Homeserver is")} {MatrixClientPeg.get().getHomeserverUrl()}<br />
{_t("Identity Server is")} {MatrixClientPeg.get().getIdentityServerUrl()}<br />
{_t("Access Token:") + ' '}
<AccessibleButton element="span" onClick={this._showSpoiler}
data-spoiler={MatrixClientPeg.get().getAccessToken()}>
&lt;{ _t("click to reveal") }&gt;
</AccessibleButton>
</div>
</div>
</div>
);
}
}

View File

@@ -0,0 +1,242 @@
/*
Copyright 2019 New Vector Ltd
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 SettingsStore, {SettingLevel} from "../../../../settings/SettingsStore";
import MatrixClientPeg from "../../../../MatrixClientPeg";
import * as FormattingUtils from "../../../../utils/FormattingUtils";
import AccessibleButton from "../../elements/AccessibleButton";
import Analytics from "../../../../Analytics";
import Promise from "bluebird";
import Modal from "../../../../Modal";
import sdk from "../../../../index";
export class IgnoredUser extends React.Component {
static propTypes = {
userId: PropTypes.string.isRequired,
onUnignored: PropTypes.func.isRequired,
};
_onUnignoreClicked = (e) => {
this.props.onUnignored(this.props.userId);
};
render() {
return (
<div className='mx_SecuritySettingsTab_ignoredUser'>
<AccessibleButton onClick={this._onUnignoreClicked} kind='primary_sm'>
{_t('Unignore')}
</AccessibleButton>
<span>{this.props.userId}</span>
</div>
);
}
}
export default class SecuritySettingsTab extends React.Component {
constructor() {
super();
this.state = {
ignoredUserIds: MatrixClientPeg.get().getIgnoredUsers(),
rejectingInvites: false,
};
}
_updateBlacklistDevicesFlag = (checked) => {
MatrixClientPeg.get().setGlobalBlacklistUnverifiedDevices(checked);
};
_updateAnalytics = (checked) => {
checked ? Analytics.enable() : Analytics.disable();
};
_onExportE2eKeysClicked = () => {
Modal.createTrackedDialogAsync('Export E2E Keys', '',
import('../../../../async-components/views/dialogs/ExportE2eKeysDialog'),
{matrixClient: MatrixClientPeg.get()},
);
};
_onImportE2eKeysClicked = () => {
Modal.createTrackedDialogAsync('Import E2E Keys', '',
import('../../../../async-components/views/dialogs/ImportE2eKeysDialog'),
{matrixClient: MatrixClientPeg.get()},
);
};
_onUserUnignored = async (userId) => {
// Don't use this.state to get the ignored user list as it might be
// ever so slightly outdated. Instead, prefer to get a fresh list and
// update that.
const ignoredUsers = MatrixClientPeg.get().getIgnoredUsers();
const index = ignoredUsers.indexOf(userId);
if (index !== -1) {
ignoredUsers.splice(index, 1);
MatrixClientPeg.get().setIgnoredUsers(ignoredUsers);
}
this.setState({ignoredUsers});
};
_onRejectAllInvitesClicked = (rooms, ev) => {
this.setState({
rejectingInvites: true,
});
// reject the invites
const promises = rooms.map((room) => {
return MatrixClientPeg.get().leave(room.roomId).catch((e) => {
// purposefully drop errors to the floor: we'll just have a non-zero number on the UI
// after trying to reject all the invites.
});
});
Promise.all(promises).then(() => {
this.setState({
rejectingInvites: false,
});
});
};
_renderCurrentDeviceInfo() {
const SettingsFlag = sdk.getComponent('views.elements.SettingsFlag');
const client = MatrixClientPeg.get();
const deviceId = client.deviceId;
let identityKey = client.getDeviceEd25519Key();
if (!identityKey) {
identityKey = _t("<not supported>");
} else {
identityKey = FormattingUtils.formatCryptoKey(identityKey);
}
let importExportButtons = null;
if (client.isCryptoEnabled()) {
importExportButtons = (
<div className='mx_SecuritySettingsTab_importExportButtons'>
<AccessibleButton kind='primary' onClick={this._onExportE2eKeysClicked}>
{_t("Export E2E room keys")}
</AccessibleButton>
<AccessibleButton kind='primary' onClick={this._onImportE2eKeysClicked}>
{_t("Import E2E room keys")}
</AccessibleButton>
</div>
);
}
return (
<div className='mx_SettingsTab_section'>
<span className='mx_SettingsTab_subheading'>{_t("Cryptography")}</span>
<ul className='mx_SettingsTab_subsectionText mx_SecuritySettingsTab_deviceInfo'>
<li>
<label>{_t("Device ID:")}</label>
<span><code>{deviceId}</code></span>
</li>
<li>
<label>{_t("Device key:")}</label>
<span><code><b>{identityKey}</b></code></span>
</li>
</ul>
{importExportButtons}
<SettingsFlag name='blacklistUnverifiedDevices' level={SettingLevel.DEVICE}
onChange={this._updateBlacklistDevicesFlag} />
</div>
);
}
_renderIgnoredUsers() {
if (!this.state.ignoredUserIds || this.state.ignoredUserIds.length === 0) return null;
const userIds = this.state.ignoredUserIds
.map((u) => <IgnoredUser userId={u} onUnignored={this._onUserUnignored} key={u} />);
return (
<div className='mx_SettingsTab_section'>
<span className='mx_SettingsTab_subheading'>{_t('Ignored users')}</span>
<div className='mx_SettingsTab_subsectionText'>
{userIds}
</div>
</div>
);
}
_renderRejectInvites() {
const invitedRooms = MatrixClientPeg.get().getRooms().filter((r) => {
return r.hasMembershipState(MatrixClientPeg.get().getUserId(), "invite");
});
if (invitedRooms.length === 0) {
return null;
}
const onClick = this._onRejectAllInvitesClicked.bind(this, invitedRooms);
return (
<div className='mx_SettingsTab_section'>
<span className='mx_SettingsTab_subheading'>{_t('Bulk options')}</span>
<AccessibleButton onClick={onClick} kind='danger' disabled={this.state.rejectingInvites}>
{_t("Reject all %(invitedRooms)s invites", {invitedRooms: invitedRooms.length})}
</AccessibleButton>
</div>
);
}
render() {
const DevicesPanel = sdk.getComponent('views.settings.DevicesPanel');
const SettingsFlag = sdk.getComponent('views.elements.SettingsFlag');
let keyBackup = null;
if (SettingsStore.isFeatureEnabled("feature_keybackup")) {
const KeyBackupPanel = sdk.getComponent('views.settings.KeyBackupPanel');
keyBackup = (
<div className='mx_SettingsTab_section'>
<span className="mx_SettingsTab_subheading">{_t("Key backup")}</span>
<div className='mx_SettingsTab_subsectionText'>
<KeyBackupPanel />
</div>
</div>
);
}
return (
<div className="mx_SettingsTab mx_SecuritySettingsTab">
<div className="mx_SettingsTab_heading">{_t("Security & Privacy")}</div>
<div className="mx_SettingsTab_section">
<span className="mx_SettingsTab_subheading">{_t("Devices")}</span>
<div className='mx_SettingsTab_subsectionText'>
<DevicesPanel />
</div>
</div>
{keyBackup}
{this._renderCurrentDeviceInfo()}
<div className='mx_SettingsTab_section'>
<span className="mx_SettingsTab_subheading">{_t("Analytics")}</span>
<div className='mx_SettingsTab_subsectionText'>
{_t("Riot collects anonymous analytics to allow us to improve the application.")}
&nbsp;
{_t("Privacy is important to us, so we don't collect any personal or " +
"identifiable data for our analytics.")}
<AccessibleButton className="mx_SettingsTab_linkBtn" onClick={Analytics.showDetailsModal}>
{_t("Learn more about how we use analytics.")}
</AccessibleButton>
</div>
<SettingsFlag name='analyticsOptIn' level={SettingLevel.DEVICE}
onChange={this._updateAnalytics} />
</div>
{this._renderIgnoredUsers()}
{this._renderRejectInvites()}
</div>
);
}
}

View File

@@ -0,0 +1,40 @@
/*
Copyright 2019 Vector Creations Ltd
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 sdk from '../../../index';
import { _t } from '../../../languageHandler';
export default class VerificationCancelled extends React.Component {
static propTypes = {
onDone: PropTypes.func.isRequired,
}
render() {
const DialogButtons = sdk.getComponent('views.elements.DialogButtons');
return <div>
<p>{_t(
"The other party cancelled the verification.",
)}</p>
<DialogButtons
primaryButton={_t('Cancel')}
hasCancel={false}
onPrimaryButtonClick={this.props.onDone}
/>
</div>;
}
}

View File

@@ -0,0 +1,42 @@
/*
Copyright 2019 Vector Creations Ltd
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 sdk from '../../../index';
import { _t } from '../../../languageHandler';
export default class VerificationComplete extends React.Component {
static propTypes = {
onDone: PropTypes.func.isRequired,
}
render() {
const DialogButtons = sdk.getComponent('views.elements.DialogButtons');
return <div>
<h2>{_t("Verified!")}</h2>
<p>{_t("You've successfully verified this user.")}</p>
<p>{_t(
"Secure messages with this user are end-to-end encrypted and not able to be " +
"read by third parties.",
)}</p>
<DialogButtons onPrimaryButtonClick={this.props.onDone}
primaryButton={_t("Got It")}
hasCancel={false}
/>
</div>;
}
}

View File

@@ -0,0 +1,65 @@
/*
Copyright 2019 Vector Creations Ltd
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 sdk from '../../../index';
import { _t } from '../../../languageHandler';
export default class VerificationShowSas extends React.Component {
static propTypes = {
onDone: PropTypes.func.isRequired,
onCancel: PropTypes.func.isRequired,
sas: PropTypes.string.isRequired,
}
constructor() {
super();
this.state = {
sasVerified: false,
};
}
_onVerifiedStateChange = (newVal) => {
this.setState({sasVerified: newVal});
}
render() {
const DialogButtons = sdk.getComponent('views.elements.DialogButtons');
const HexVerify = sdk.getComponent('views.elements.HexVerify');
return <div>
<p>{_t(
"Verify this user by confirming the following number appears on their screen.",
)}</p>
<p>{_t(
"For maximum security, we reccommend you do this in person or use another " +
"trusted means of communication.",
)}</p>
<HexVerify text={this.props.sas}
onVerifiedStateChange={this._onVerifiedStateChange}
/>
<p>{_t(
"To continue, click on each pair to confirm it's correct.",
)}</p>
<DialogButtons onPrimaryButtonClick={this.props.onDone}
primaryButton={_t("Continue")}
primaryDisabled={!this.state.sasVerified}
hasCancel={true}
onCancel={this.props.onCancel}
/>
</div>;
}
}