Merge branch 'develop' into bwindels/roomlistlag

This commit is contained in:
Bruno Windels 2019-02-11 17:06:16 +01:00
commit 7c72fddf80
59 changed files with 2902 additions and 468 deletions

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.
@ -26,22 +27,27 @@ import sdk from '../../index';
import { MatrixClient } from 'matrix-js-sdk';
import classnames from 'classnames';
class HomePage extends React.Component {
static displayName = 'HomePage';
export default class EmbeddedPage extends React.PureComponent {
static propTypes = {
// URL to use as the iFrame src. Defaults to /home.html.
homePageUrl: PropTypes.string,
// URL to request embedded page content from
url: PropTypes.string,
// Class name prefix to apply for a given instance
className: PropTypes.string,
// Whether to wrap the page in a scrollbar
scrollbar: PropTypes.bool,
};
static contextTypes = {
matrixClient: PropTypes.instanceOf(MatrixClient),
};
state = {
iframeSrc: '',
constructor(props) {
super(props);
this.state = {
page: '',
};
};
}
translate(s) {
// default implementation - skins may wish to extend this
@ -51,22 +57,24 @@ class HomePage extends React.Component {
componentWillMount() {
this._unmounted = false;
// we use request() to inline the homepage into the react component
if (!this.props.url) {
return;
}
// we use request() to inline the page 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';
request(
{ method: "GET", url: src },
{ method: "GET", url: this.props.url },
(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") });
console.warn(`Error loading page: ${err}`);
this.setState({ page: _t("Couldn't load page") });
return;
}
@ -81,28 +89,28 @@ class HomePage extends React.Component {
}
render() {
const isGuest = this.context.matrixClient.isGuest();
const client = this.context.matrixClient;
const isGuest = client ? client.isGuest() : true;
const className = this.props.className;
const classes = classnames({
mx_HomePage: true,
mx_HomePage_guest: isGuest,
[className]: true,
[`${className}_guest`]: isGuest,
});
if (this.state.iframeSrc) {
return (
<div className={classes}>
<iframe src={ this.state.iframeSrc } />
</div>
);
} else {
const content = <div className={`${className}_body`}
dangerouslySetInnerHTML={{ __html: this.state.page }}
>
</div>;
if (this.props.scrollbar) {
const GeminiScrollbarWrapper = sdk.getComponent("elements.GeminiScrollbarWrapper");
return (
<GeminiScrollbarWrapper autoshow={true} className={classes}>
<div className="mx_HomePage_body" dangerouslySetInnerHTML={{ __html: this.state.page }}>
</div>
</GeminiScrollbarWrapper>
);
return <GeminiScrollbarWrapper autoshow={true} className={classes}>
{content}
</GeminiScrollbarWrapper>;
} else {
return <div className={classes}>
{content}
</div>;
}
}
}
module.exports = HomePage;

View file

@ -420,7 +420,7 @@ const LoggedInView = React.createClass({
render: function() {
const LeftPanel = sdk.getComponent('structures.LeftPanel');
const RoomView = sdk.getComponent('structures.RoomView');
const HomePage = sdk.getComponent('structures.HomePage');
const EmbeddedPage = sdk.getComponent('structures.EmbeddedPage');
const GroupView = sdk.getComponent('structures.GroupView');
const MyGroups = sdk.getComponent('structures.MyGroups');
const MatrixToolbar = sdk.getComponent('globals.MatrixToolbar');
@ -459,8 +459,20 @@ const LoggedInView = React.createClass({
case PageTypes.HomePage:
{
pageElement = <HomePage
homePageUrl={this.props.config.welcomePageUrl}
const pagesConfig = this.props.config.embeddedPages;
let pageUrl = null;
if (pagesConfig) {
pageUrl = pagesConfig.homeUrl;
}
if (!pageUrl) {
// This is a deprecated config option for the home page
// (despite the name, given we also now have a welcome
// page, which is not the same).
pageUrl = this.props.config.welcomePageUrl;
}
pageElement = <EmbeddedPage className="mx_HomePage"
url={pageUrl}
scrollbar={true}
/>;
}
break;

View file

@ -60,27 +60,30 @@ const VIEWS = {
// trying to re-animate a matrix client or register as a guest.
LOADING: 0,
// we are showing the welcome view
WELCOME: 1,
// we are showing the login view
LOGIN: 1,
LOGIN: 2,
// we are showing the registration view
REGISTER: 2,
REGISTER: 3,
// completeing the registration flow
POST_REGISTRATION: 3,
POST_REGISTRATION: 4,
// showing the 'forgot password' view
FORGOT_PASSWORD: 4,
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: 5,
LOGGING_IN: 6,
// we are logged in with an active matrix client.
LOGGED_IN: 6,
LOGGED_IN: 7,
};
// Actions that are redirected through the onboarding process prior to being
@ -354,8 +357,8 @@ export default React.createClass({
});
}).then((loadedSession) => {
if (!loadedSession) {
// fall back to showing the login screen
dis.dispatch({action: "start_login"});
// fall back to showing the welcome screen
dis.dispatch({action: "view_welcome_page"});
}
});
// Note we don't catch errors from this: we catch everything within
@ -606,6 +609,9 @@ export default React.createClass({
case 'view_group':
this._viewGroup(payload);
break;
case 'view_welcome_page':
this._viewWelcome();
break;
case 'view_home_page':
this._viewHome();
break;
@ -881,6 +887,13 @@ export default React.createClass({
this.notifyNewScreen('group/' + groupId);
},
_viewWelcome() {
this.setStateForNewView({
view: VIEWS.WELCOME,
});
this.notifyNewScreen('welcome');
},
_viewHome: function() {
// The home page requires the "logged in" view, so we'll set that.
this.setStateForNewView({
@ -954,11 +967,11 @@ export default React.createClass({
}
dis.dispatch({
action: 'require_registration',
// If the set_mxid dialog is cancelled, view /home because if the browser
// was pointing at /user/@someone:domain?action=chat, the URL needs to be
// reset so that they can revisit /user/.. // (and trigger
// If the set_mxid dialog is cancelled, view /welcome because if the
// browser was pointing at /user/@someone:domain?action=chat, the URL
// needs to be reset so that they can revisit /user/.. // (and trigger
// `_chatCreateOrReuse` again)
go_home_on_cancel: true,
go_welcome_on_cancel: true,
});
return;
}
@ -1180,7 +1193,11 @@ export default React.createClass({
room_id: localStorage.getItem('mx_last_room_id'),
});
} else {
dis.dispatch({action: 'view_home_page'});
if (MatrixClientPeg.get().isGuest()) {
dis.dispatch({action: 'view_welcome_page'});
} else {
dis.dispatch({action: 'view_home_page'});
}
}
},
@ -1466,6 +1483,10 @@ export default React.createClass({
dis.dispatch({
action: 'view_user_settings',
});
} else if (screen == 'welcome') {
dis.dispatch({
action: 'view_welcome_page',
});
} else if (screen == 'home') {
dis.dispatch({
action: 'view_home_page',
@ -1849,6 +1870,11 @@ export default React.createClass({
}
}
if (this.state.view === VIEWS.WELCOME) {
const Welcome = sdk.getComponent('auth.Welcome');
return <Welcome />;
}
if (this.state.view === VIEWS.REGISTER) {
const Registration = sdk.getComponent('structures.auth.Registration');
return (

View file

@ -1635,7 +1635,6 @@ module.exports = React.createClass({
);
const showRoomRecoveryReminder = (
SettingsStore.isFeatureEnabled("feature_keybackup") &&
SettingsStore.getValue("showRoomRecoveryReminder") &&
MatrixClientPeg.get().isRoomEncrypted(this.state.room.roomId) &&
!MatrixClientPeg.get().getKeyBackupEnabled()

View file

@ -0,0 +1,47 @@
/*
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 sdk from '../../../index';
import SdkConfig from '../../../SdkConfig';
export default class Welcome extends React.PureComponent {
render() {
const AuthPage = sdk.getComponent("auth.AuthPage");
const EmbeddedPage = sdk.getComponent('structures.EmbeddedPage');
const LanguageSelector = sdk.getComponent('auth.LanguageSelector');
const pagesConfig = SdkConfig.get().embeddedPages;
let pageUrl = null;
if (pagesConfig) {
pageUrl = pagesConfig.welcomeUrl;
}
if (!pageUrl) {
pageUrl = 'welcome.html';
}
return (
<AuthPage>
<div className="mx_Welcome">
<EmbeddedPage className="mx_WelcomePage"
url={pageUrl}
/>
<LanguageSelector />
</div>
</AuthPage>
);
}
}

View file

@ -1,5 +1,5 @@
/*
Copyright 2018 New Vector Ltd
Copyright 2018, 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.
@ -19,6 +19,7 @@ import dis from '../../../dispatcher';
import { _t } from '../../../languageHandler';
import LogoutDialog from "../dialogs/LogoutDialog";
import Modal from "../../../Modal";
import SdkConfig from '../../../SdkConfig';
export class TopLeftMenu extends React.Component {
constructor() {
@ -27,8 +28,28 @@ export class TopLeftMenu extends React.Component {
this.signOut = this.signOut.bind(this);
}
hasHomePage() {
const config = SdkConfig.get();
const pagesConfig = config.embeddedPages;
if (pagesConfig && pagesConfig.homeUrl) {
return true;
}
// This is a deprecated config option for the home page
// (despite the name, given we also now have a welcome
// page, which is not the same).
return !!config.welcomePageUrl;
}
render() {
let homePageSection = null;
if (this.hasHomePage()) {
homePageSection = <ul className="mx_TopLeftMenu_section">
<li className="mx_TopLeftMenu_icon_home" onClick={this.viewHomePage}>{_t("Home")}</li>
</ul>;
}
return <div className="mx_TopLeftMenu">
{homePageSection}
<ul className="mx_TopLeftMenu_section">
<li className="mx_TopLeftMenu_icon_settings" onClick={this.openSettings}>{_t("Settings")}</li>
</ul>
@ -38,6 +59,11 @@ export class TopLeftMenu extends React.Component {
</div>;
}
viewHomePage() {
dis.dispatch({action: 'view_home_page'});
this.closeMenu();
}
openSettings() {
dis.dispatch({action: 'view_user_settings'});
this.closeMenu();

View file

@ -22,7 +22,6 @@ 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';
const MODE_LEGACY = 'legacy';
@ -48,7 +47,7 @@ export default class DeviceVerifyDialog extends React.Component {
this._showSasEvent = null;
this.state = {
phase: PHASE_START,
mode: SettingsStore.isFeatureEnabled("feature_sas") ? MODE_SAS : MODE_LEGACY,
mode: MODE_SAS,
sasVerified: false,
};
}
@ -61,6 +60,11 @@ export default class DeviceVerifyDialog extends React.Component {
}
_onSwitchToLegacyClick = () => {
if (this._verifier) {
this._verifier.removeListener('show_sas', this._onVerifierShowSas);
this._verifier.cancel('User cancel');
this._verifier = null;
}
this.setState({mode: MODE_LEGACY});
}
@ -185,11 +189,21 @@ export default class DeviceVerifyDialog extends React.Component {
_renderSasVerificationPhaseWaitAccept() {
const Spinner = sdk.getComponent("views.elements.Spinner");
const AccessibleButton = sdk.getComponent('views.elements.AccessibleButton');
return (
<div>
<Spinner />
<p>{_t("Waiting for partner to accept...")}</p>
<p>{_t(
"Nothing appearing? Not all clients support interactive verification yet. " +
"<button>Use legacy verification</button>.",
{}, {button: sub => <AccessibleButton element='span' className="mx_linkButton"
onClick={this._onSwitchToLegacyClick}
>
{sub}
</AccessibleButton>},
)}</p>
</div>
);
}

View file

@ -20,7 +20,6 @@ import sdk from '../../../index';
import dis from '../../../dispatcher';
import { _t } from '../../../languageHandler';
import MatrixClientPeg from '../../../MatrixClientPeg';
import SettingsStore from "../../../settings/SettingsStore";
export default class LogoutDialog extends React.Component {
constructor() {
@ -79,86 +78,59 @@ export default class LogoutDialog extends React.Component {
}
render() {
let description;
if (SettingsStore.isFeatureEnabled("feature_keybackup")) {
description = <div>
<p>{_t(
"When you log out, you'll lose your secure message history. To prevent " +
"this, set up a recovery method.",
)}</p>
<p>{_t(
"Alternatively, advanced users can also manually export encryption keys in " +
"<a>Settings</a> before logging out.", {},
{
a: sub => <a href='#/settings' onClick={this._onSettingsLinkClick}>{sub}</a>,
},
)}</p>
</div>;
} else {
description = <div>{_t(
"For security, logging out will delete any end-to-end " +
"encryption keys from this browser. If you want to be able " +
"to decrypt your conversation history from future Riot sessions, " +
"please export your room keys for safe-keeping.",
)}</div>;
}
const description = <div>
<p>{_t(
"When you log out, you'll lose your secure message history. To prevent " +
"this, set up a recovery method.",
)}</p>
<p>{_t(
"Alternatively, advanced users can also manually export encryption keys in " +
"<a>Settings</a> before logging out.", {},
{
a: sub => <a href='#/settings' onClick={this._onSettingsLinkClick}>{sub}</a>,
},
)}</p>
</div>;
if (SettingsStore.isFeatureEnabled("feature_keybackup")) {
if (!MatrixClientPeg.get().getKeyBackupEnabled()) {
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
const DialogButtons = sdk.getComponent('views.elements.DialogButtons');
// Not quite a standard question dialog as the primary button cancels
// the action and does something else instead, whilst non-default button
// confirms the action.
return (<BaseDialog
title={_t("Warning!")}
contentId='mx_Dialog_content'
if (!MatrixClientPeg.get().getKeyBackupEnabled()) {
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
const DialogButtons = sdk.getComponent('views.elements.DialogButtons');
// Not quite a standard question dialog as the primary button cancels
// the action and does something else instead, whilst non-default button
// confirms the action.
return (<BaseDialog
title={_t("Warning!")}
contentId='mx_Dialog_content'
hasCancel={false}
onFinsihed={this._onFinished}
>
<div className="mx_Dialog_content" id='mx_Dialog_content'>
{ description }
</div>
<DialogButtons primaryButton={_t('Set a Recovery Method')}
hasCancel={false}
onFinsihed={this._onFinished}
onPrimaryButtonClick={this._onSetRecoveryMethodClick}
focus={true}
>
<div className="mx_Dialog_content" id='mx_Dialog_content'>
{ description }
</div>
<DialogButtons primaryButton={_t('Set a Recovery Method')}
hasCancel={false}
onPrimaryButtonClick={this._onSetRecoveryMethodClick}
focus={true}
>
<button onClick={this._onLogoutConfirm}>
{_t("I understand, log out without")}
</button>
</DialogButtons>
</BaseDialog>);
} else {
const QuestionDialog = sdk.getComponent('views.dialogs.QuestionDialog');
return (<QuestionDialog
hasCancelButton={true}
title={_t("Sign out")}
// TODO: This is made up by me and would need to also mention verifying
// 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 passphrase " +
"or, if you didn't set a recovery passphrase, your recovery key " +
"(that you downloaded).",
)}
button={_t("Sign out")}
onFinished={this._onFinished}
/>);
}
<button onClick={this._onLogoutConfirm}>
{_t("I understand, log out without")}
</button>
</DialogButtons>
</BaseDialog>);
} else {
const QuestionDialog = sdk.getComponent('views.dialogs.QuestionDialog');
return (<QuestionDialog
hasCancelButton={true}
title={_t("Sign out")}
description={description}
// TODO: This is made up by me and would need to also mention verifying
// once you can restore 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 passphrase " +
"or, if you didn't set a recovery passphrase, your recovery key " +
"(that you downloaded).",
)}
button={_t("Sign out")}
extraButtons={[
(<button key="export" className="mx_Dialog_primary"
onClick={this._onExportE2eKeysClicked}>
{ _t("Export E2E room keys") }
</button>),
]}
onFinished={this._onFinished}
/>);
}

View file

@ -20,12 +20,12 @@ import PropTypes from 'prop-types';
import {emojifyText, containsEmoji} from '../../../HtmlUtils';
export default function EmojiText(props) {
const {element, children, ...restProps} = props;
const {element, children, addAlt, ...restProps} = props;
// fast path: simple regex to detect strings that don't contain
// emoji and just return them
if (containsEmoji(children)) {
restProps.dangerouslySetInnerHTML = emojifyText(children);
restProps.dangerouslySetInnerHTML = emojifyText(children, addAlt);
return React.createElement(element, restProps);
} else {
return React.createElement(element, restProps, children);
@ -39,4 +39,5 @@ EmojiText.propTypes = {
EmojiText.defaultProps = {
element: 'span',
addAlt: true,
};

View file

@ -209,10 +209,15 @@ module.exports = React.createClass({
const topicElement =
<div className="mx_RoomHeader_topic" ref="topic" title={topic} dir="auto">{ topic }</div>;
const avatarSize = 28;
const roomAvatar = (
<RoomAvatar room={this.props.room} width={avatarSize} height={avatarSize} oobData={this.props.oobData}
viewAvatarOnClick={true} />
);
let roomAvatar;
if (this.props.room) {
roomAvatar = (<RoomAvatar
room={this.props.room}
width={avatarSize}
height={avatarSize}
oobData={this.props.oobData}
viewAvatarOnClick={true} />);
}
if (this.props.onSettingsClick) {
settingsButton =

View file

@ -38,7 +38,7 @@ export default class RoomRecoveryReminder extends React.PureComponent {
this.state = {
loading: true,
error: null,
unverifiedDevice: null,
backupInfo: null,
};
}
@ -47,10 +47,12 @@ export default class RoomRecoveryReminder extends React.PureComponent {
}
async _loadBackupStatus() {
let backupSigStatus;
try {
const backupInfo = await MatrixClientPeg.get().getKeyBackupVersion();
backupSigStatus = await MatrixClientPeg.get().isKeyBackupTrusted(backupInfo);
this.setState({
loading: false,
backupInfo,
});
} catch (e) {
console.log("Unable to fetch key backup status", e);
this.setState({
@ -59,44 +61,20 @@ export default class RoomRecoveryReminder extends React.PureComponent {
});
return;
}
let unverifiedDevice;
for (const sig of backupSigStatus.sigs) {
if (sig.device && !sig.device.isVerified()) {
unverifiedDevice = sig.device;
break;
}
}
this.setState({
loading: false,
unverifiedDevice,
});
}
showSetupDialog = () => {
if (this.state.unverifiedDevice) {
if (this.state.backupInfo) {
// A key backup exists for this account, but the creating device is not
// verified, so we'll show the device verify dialog.
// TODO: Should change to a restore key backup flow that checks the recovery
// passphrase while at the same time also cross-signing the device as well in
// a single flow (for cases where a key backup exists but the backup creating
// device is unverified). Since we don't have that yet, we'll look for an
// unverified device and verify it. Note that this means we won't restore
// keys yet; instead we'll only trust the backup for sending our own new keys
// to it.
const DeviceVerifyDialog = sdk.getComponent('views.dialogs.DeviceVerifyDialog');
Modal.createTrackedDialog('Device Verify Dialog', '', DeviceVerifyDialog, {
userId: MatrixClientPeg.get().credentials.userId,
device: this.state.unverifiedDevice,
});
return;
// verified, so restore the backup which will give us the keys from it and
// allow us to trust it (ie. upload keys to it)
const RestoreKeyBackupDialog = sdk.getComponent('dialogs.keybackup.RestoreKeyBackupDialog');
Modal.createTrackedDialog('Restore Backup', '', RestoreKeyBackupDialog, {});
} else {
Modal.createTrackedDialogAsync("Key Backup", "Key Backup",
import("../../../async-components/views/dialogs/keybackup/CreateKeyBackupDialog"),
);
}
// The default case assumes that a key backup doesn't exist for this account, so
// we'll show the create key backup flow.
Modal.createTrackedDialogAsync("Key Backup", "Key Backup",
import("../../../async-components/views/dialogs/keybackup/CreateKeyBackupDialog"),
);
}
onDontAskAgainClick = () => {
@ -133,53 +111,40 @@ 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")}
</div>;
} else if (this.state.unverifiedDevice) {
// A key backup exists for this account, but the creating device is not
// verified.
} else if (this.state.backupInfo) {
// A key backup exists for this account, but we're not using it.
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.",
"Secure Key Backup should be active on all of your devices to avoid " +
"losing access to your encrypted messages.",
)}</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.)
body = _t(
"If you log out or use another device, you'll lose your " +
"secure message history. To prevent this, set up Secure " +
"Message Recovery.",
"Securely back up your decryption keys to the server to make sure " +
"you'll always be able to read your encrypted messages.",
);
}
return (
<div className="mx_RoomRecoveryReminder">
<div className="mx_RoomRecoveryReminder_header">{_t(
"Secure Message Recovery",
"Don't risk losing your encrypted messages!",
)}</div>
<div className="mx_RoomRecoveryReminder_body">{body}</div>
<div className="mx_RoomRecoveryReminder_buttons">
<AccessibleButton className="mx_RoomRecoveryReminder_button mx_RoomRecoveryReminder_secondary"
onClick={this.onDontAskAgainClick}>
{ _t("Don't ask again") }
</AccessibleButton>
<AccessibleButton className="mx_RoomRecoveryReminder_button"
onClick={this.onSetupClick}>
{primaryCaption}
{_t("Activate Secure Key Backup")}
</AccessibleButton>
<p><AccessibleButton className="mx_RoomRecoveryReminder_secondary mx_linkButton"
onClick={this.onDontAskAgainClick}>
{ _t("No thanks, I'll download a copy of my decryption keys before I log out") }
</AccessibleButton></p>
</div>
</div>
);

View file

@ -27,7 +27,6 @@ export default class KeyBackupPanel extends React.PureComponent {
this._startNewBackup = this._startNewBackup.bind(this);
this._deleteBackup = this._deleteBackup.bind(this);
this._verifyDevice = this._verifyDevice.bind(this);
this._onKeyBackupSessionsRemaining =
this._onKeyBackupSessionsRemaining.bind(this);
this._onKeyBackupStatus = this._onKeyBackupStatus.bind(this);
@ -133,19 +132,6 @@ export default class KeyBackupPanel extends React.PureComponent {
});
}
_verifyDevice(e) {
const device = this.state.backupSigStatus.sigs[e.target.getAttribute('data-sigindex')].device;
const DeviceVerifyDialog = sdk.getComponent('views.dialogs.DeviceVerifyDialog');
Modal.createTrackedDialog('Device Verify Dialog', '', DeviceVerifyDialog, {
userId: MatrixClientPeg.get().credentials.userId,
device: device,
onFinished: () => {
this._loadBackupStatus();
},
});
}
render() {
const Spinner = sdk.getComponent("elements.Spinner");
const AccessibleButton = sdk.getComponent("elements.AccessibleButton");
@ -163,9 +149,8 @@ export default class KeyBackupPanel extends React.PureComponent {
if (MatrixClientPeg.get().getKeyBackupEnabled()) {
clientBackupStatus = _t("This device is using key backup");
} else {
// XXX: display why and how to fix it
clientBackupStatus = _t(
"This device is <b>not</b> using key backup", {},
"This device is <b>not</b> using key backup. Restore the backup to start using it.", {},
{b: x => <b>{x}</b>},
);
}
@ -233,17 +218,8 @@ export default class KeyBackupPanel extends React.PureComponent {
);
}
let verifyButton;
if (sig.device && !sig.device.isVerified()) {
verifyButton = <div><br /><AccessibleButton className="mx_GeneralButton"
onClick={this._verifyDevice} data-sigindex={i}>
{ _t("Verify...") }
</AccessibleButton></div>;
}
return <div key={i}>
{sigStatus}
{verifyButton}
</div>;
});
if (this.state.backupSigStatus.sigs.length === 0) {
@ -256,12 +232,15 @@ export default class KeyBackupPanel extends React.PureComponent {
}
return <div>
<div>{_t("Backup version: ")}{this.state.backupInfo.version}</div>
<div>{_t("Algorithm: ")}{this.state.backupInfo.algorithm}</div>
<div>{clientBackupStatus}</div>
{uploadStatus}
<div>{backupSigStatuses}</div>
<div>{trustedLocally}</div>
<details>
<summary>{_t("Advanced")}</summary>
<div>{_t("Backup version: ")}{this.state.backupInfo.version}</div>
<div>{_t("Algorithm: ")}{this.state.backupInfo.algorithm}</div>
{uploadStatus}
<div>{backupSigStatuses}</div>
<div>{trustedLocally}</div>
</details>
<p>
<AccessibleButton kind="primary" onClick={this._restoreBackup}>
{ _t("Restore backup") }

View file

@ -17,7 +17,7 @@ limitations under the License.
import React from 'react';
import PropTypes from 'prop-types';
import {_t} from "../../../../languageHandler";
import SettingsStore, {SettingLevel} from "../../../../settings/SettingsStore";
import {SettingLevel} from "../../../../settings/SettingsStore";
import MatrixClientPeg from "../../../../MatrixClientPeg";
import * as FormattingUtils from "../../../../utils/FormattingUtils";
import AccessibleButton from "../../elements/AccessibleButton";
@ -196,18 +196,15 @@ export default class SecuritySettingsTab extends React.Component {
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>
const KeyBackupPanel = sdk.getComponent('views.settings.KeyBackupPanel');
const 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">

View file

@ -17,13 +17,17 @@ limitations under the License.
import React from 'react';
import PropTypes from 'prop-types';
import sdk from '../../../index';
import { _t } from '../../../languageHandler';
import { _t, _td } from '../../../languageHandler';
function capFirst(s) {
return s.charAt(0).toUpperCase() + s.slice(1);
}
export default class VerificationShowSas extends React.Component {
static propTypes = {
onDone: PropTypes.func.isRequired,
onCancel: PropTypes.func.isRequired,
sas: PropTypes.string.isRequired,
sas: PropTypes.object.isRequired,
}
constructor() {
@ -32,17 +36,55 @@ export default class VerificationShowSas extends React.Component {
render() {
const DialogButtons = sdk.getComponent('views.elements.DialogButtons');
return <div className="mx_VerificationShowSas">
<p>{_t(
const EmojiText = sdk.getComponent('views.elements.EmojiText');
let sasDisplay;
let sasCaption;
if (this.props.sas.emoji) {
const emojiBlocks = this.props.sas.emoji.map(
(emoji, i) => <div className="mx_VerificationShowSas_emojiSas_block" key={i}>
<div className="mx_VerificationShowSas_emojiSas_emoji">
<EmojiText addAlt={false}>{emoji[0]}</EmojiText>
</div>
<div className="mx_VerificationShowSas_emojiSas_label">
{_t(capFirst(emoji[1]))}
</div>
</div>,
);
sasDisplay = <div className="mx_VerificationShowSas_emojiSas">
{emojiBlocks}
</div>;
sasCaption = _t(
"Verify this user by confirming the following emoji appear on their screen.",
);
} else if (this.props.sas.decimal) {
const numberBlocks = this.props.sas.decimal.map((num, i) => <span key={i}>
{num}
</span>);
sasDisplay = <div className="mx_VerificationShowSas_decimalSas">
{numberBlocks}
</div>;
sasCaption = _t(
"Verify this user by confirming the following number appears on their screen.",
)}</p>
);
} else {
return <div>
{_t("Unable to find a supported verification method.")}
<DialogButtons
primaryButton={_t('Cancel')}
hasCancel={false}
onPrimaryButtonClick={this.props.onCancel}
/>
</div>;
}
return <div className="mx_VerificationShowSas">
<p>{sasCaption}</p>
<p>{_t(
"For maximum security, we recommend you do this in person or use another " +
"trusted means of communication.",
)}</p>
<div className="mx_VerificationShowSas_sas">
{this.props.sas}
</div>
{sasDisplay}
<DialogButtons onPrimaryButtonClick={this.props.onDone}
primaryButton={_t("Continue")}
hasCancel={true}
@ -51,3 +93,69 @@ export default class VerificationShowSas extends React.Component {
</div>;
}
}
// List of Emoji strings from the js-sdk, for i18n
_td("Dog");
_td("Cat");
_td("Lion");
_td("Horse");
_td("Unicorn");
_td("Pig");
_td("Elephant");
_td("Rabbit");
_td("Panda");
_td("Rooster");
_td("Penguin");
_td("Turtle");
_td("Fish");
_td("Octopus");
_td("Butterfly");
_td("Flower");
_td("Tree");
_td("Cactus");
_td("Mushroom");
_td("Globe");
_td("Moon");
_td("Cloud");
_td("Fire");
_td("Banana");
_td("Apple");
_td("Strawberry");
_td("Corn");
_td("Pizza");
_td("Cake");
_td("Heart");
_td("Smiley");
_td("Robot");
_td("Hat");
_td("Glasses");
_td("Spanner");
_td("Santa");
_td("Thumbs up");
_td("Umbrella");
_td("Hourglass");
_td("Clock");
_td("Gift");
_td("Light bulb");
_td("Book");
_td("Pencil");
_td("Paperclip");
_td("Scisors");
_td("Padlock");
_td("Key");
_td("Hammer");
_td("Telephone");
_td("Flag");
_td("Train");
_td("Bicycle");
_td("Aeroplane");
_td("Rocket");
_td("Trophy");
_td("Ball");
_td("Guitar");
_td("Trumpet");
_td("Bell");
_td("Anchor");
_td("Headphones");
_td("Folder");
_td("Pin");