Merge branch 'develop' into bwindels/roomlistlag
This commit is contained in:
commit
7c72fddf80
59 changed files with 2902 additions and 468 deletions
|
@ -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;
|
|
@ -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;
|
||||
|
|
|
@ -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 (
|
||||
|
|
|
@ -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()
|
||||
|
|
47
src/components/views/auth/Welcome.js
Normal file
47
src/components/views/auth/Welcome.js
Normal 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>
|
||||
);
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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}
|
||||
/>);
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
};
|
||||
|
|
|
@ -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 =
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
|
|
|
@ -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") }
|
||||
|
|
|
@ -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">
|
||||
|
|
|
@ -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");
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue