Merge pull request #2597 from matrix-org/jryans/add-welcome-page

SDK support for welcome page
This commit is contained in:
David Baker 2019-02-08 17:17:22 +00:00 committed by GitHub
commit f99c56fedb
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
12 changed files with 208 additions and 59 deletions

View file

@ -36,6 +36,7 @@
@import "./views/auth/_LanguageSelector.scss"; @import "./views/auth/_LanguageSelector.scss";
@import "./views/auth/_ServerConfig.scss"; @import "./views/auth/_ServerConfig.scss";
@import "./views/auth/_ServerTypeSelector.scss"; @import "./views/auth/_ServerTypeSelector.scss";
@import "./views/auth/_Welcome.scss";
@import "./views/avatars/_BaseAvatar.scss"; @import "./views/avatars/_BaseAvatar.scss";
@import "./views/avatars/_MemberStatusMessageAvatar.scss"; @import "./views/avatars/_MemberStatusMessageAvatar.scss";
@import "./views/context_menus/_MessageContextMenu.scss"; @import "./views/context_menus/_MessageContextMenu.scss";

View file

@ -1,6 +1,7 @@
/* /*
Copyright 2016 OpenMarket Ltd Copyright 2016 OpenMarket Ltd
Copyright 2017 Vector Creations Ltd Copyright 2017 Vector Creations Ltd
Copyright 2019 New Vector Ltd
Licensed under the Apache License, Version 2.0 (the "License"); Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License. you may not use this file except in compliance with the License.
@ -22,10 +23,3 @@ limitations under the License.
margin-left: auto; margin-left: auto;
margin-right: auto; margin-right: auto;
} }
.mx_HomePage iframe {
display: block;
width: 100%;
height: 100%;
border: 0px;
}

View file

@ -0,0 +1,26 @@
/*
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.
*/
.mx_Welcome {
display: flex;
flex-direction: column;
align-items: center;
}
.mx_Welcome .mx_AuthBody_language {
width: 120px;
margin-bottom: 10px;
}

View file

@ -27,6 +27,10 @@ limitations under the License.
margin: 5px 0; margin: 5px 0;
padding: 0; padding: 0;
li.mx_TopLeftMenu_icon_home::after {
mask-image: url('$(res)/img/feather-icons/home.svg');
}
li.mx_TopLeftMenu_icon_settings::after { li.mx_TopLeftMenu_icon_settings::after {
mask-image: url('$(res)/img/feather-icons/settings.svg'); mask-image: url('$(res)/img/feather-icons/settings.svg');
} }

View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-home"><path d="M3 9l9-7 9 7v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"></path><polyline points="9 22 9 12 15 12 15 22"></polyline></svg>

After

Width:  |  Height:  |  Size: 332 B

View file

@ -35,8 +35,10 @@ export const SAFE_LOCALPART_REGEX = /^[a-z0-9=_\-./]+$/;
* on what the HS supports * on what the HS supports
* *
* @param {object} options * @param {object} options
* @param {bool} options.go_home_on_cancel If true, goes to * @param {bool} options.go_home_on_cancel
* the hame page if the user cancels the action * If true, goes to the home page if the user cancels the action
* @param {bool} options.go_welcome_on_cancel
* If true, goes to the welcome page if the user cancels the action
*/ */
export async function startAnyRegistrationFlow(options) { export async function startAnyRegistrationFlow(options) {
if (options === undefined) options = {}; if (options === undefined) options = {};
@ -73,6 +75,8 @@ export async function startAnyRegistrationFlow(options) {
dis.dispatch({action: 'start_registration'}); dis.dispatch({action: 'start_registration'});
} else if (options.go_home_on_cancel) { } else if (options.go_home_on_cancel) {
dis.dispatch({action: 'view_home_page'}); dis.dispatch({action: 'view_home_page'});
} else if (options.go_welcome_on_cancel) {
dis.dispatch({action: 'view_welcome_page'});
} }
}, },
}); });

View file

@ -1,6 +1,7 @@
/* /*
Copyright 2016 OpenMarket Ltd Copyright 2016 OpenMarket Ltd
Copyright 2017 Vector Creations Ltd Copyright 2017 Vector Creations Ltd
Copyright 2019 New Vector Ltd
Licensed under the Apache License, Version 2.0 (the "License"); Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with 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 { MatrixClient } from 'matrix-js-sdk';
import classnames from 'classnames'; import classnames from 'classnames';
class HomePage extends React.Component { export default class EmbeddedPage extends React.PureComponent {
static displayName = 'HomePage';
static propTypes = { static propTypes = {
// URL to use as the iFrame src. Defaults to /home.html. // URL to request embedded page content from
homePageUrl: PropTypes.string, 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 = { static contextTypes = {
matrixClient: PropTypes.instanceOf(MatrixClient), matrixClient: PropTypes.instanceOf(MatrixClient),
}; };
state = { constructor(props) {
iframeSrc: '', super(props);
this.state = {
page: '', page: '',
}; };
}
translate(s) { translate(s) {
// default implementation - skins may wish to extend this // default implementation - skins may wish to extend this
@ -51,22 +57,24 @@ class HomePage extends React.Component {
componentWillMount() { componentWillMount() {
this._unmounted = false; 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 // so that it can inherit CSS and theming easily rather than mess around
// with iframes and trying to synchronise document.stylesheets. // with iframes and trying to synchronise document.stylesheets.
const src = this.props.homePageUrl || 'home.html';
request( request(
{ method: "GET", url: src }, { method: "GET", url: this.props.url },
(err, response, body) => { (err, response, body) => {
if (this._unmounted) { if (this._unmounted) {
return; return;
} }
if (err || response.status < 200 || response.status >= 300) { if (err || response.status < 200 || response.status >= 300) {
console.warn(`Error loading home page: ${err}`); console.warn(`Error loading page: ${err}`);
this.setState({ page: _t("Couldn't load home page") }); this.setState({ page: _t("Couldn't load page") });
return; return;
} }
@ -81,28 +89,28 @@ class HomePage extends React.Component {
} }
render() { 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({ const classes = classnames({
mx_HomePage: true, [className]: true,
mx_HomePage_guest: isGuest, [`${className}_guest`]: isGuest,
}); });
if (this.state.iframeSrc) { const content = <div className={`${className}_body`}
return ( dangerouslySetInnerHTML={{ __html: this.state.page }}
<div className={classes}> >
<iframe src={ this.state.iframeSrc } /> </div>;
</div>
); if (this.props.scrollbar) {
} else {
const GeminiScrollbarWrapper = sdk.getComponent("elements.GeminiScrollbarWrapper"); const GeminiScrollbarWrapper = sdk.getComponent("elements.GeminiScrollbarWrapper");
return ( return <GeminiScrollbarWrapper autoshow={true} className={classes}>
<GeminiScrollbarWrapper autoshow={true} className={classes}> {content}
<div className="mx_HomePage_body" dangerouslySetInnerHTML={{ __html: this.state.page }}> </GeminiScrollbarWrapper>;
</div> } else {
</GeminiScrollbarWrapper> return <div className={classes}>
); {content}
</div>;
} }
} }
} }
module.exports = HomePage;

View file

@ -420,7 +420,7 @@ const LoggedInView = React.createClass({
render: function() { render: function() {
const LeftPanel = sdk.getComponent('structures.LeftPanel'); const LeftPanel = sdk.getComponent('structures.LeftPanel');
const RoomView = sdk.getComponent('structures.RoomView'); 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 GroupView = sdk.getComponent('structures.GroupView');
const MyGroups = sdk.getComponent('structures.MyGroups'); const MyGroups = sdk.getComponent('structures.MyGroups');
const MatrixToolbar = sdk.getComponent('globals.MatrixToolbar'); const MatrixToolbar = sdk.getComponent('globals.MatrixToolbar');
@ -459,8 +459,20 @@ const LoggedInView = React.createClass({
case PageTypes.HomePage: case PageTypes.HomePage:
{ {
pageElement = <HomePage const pagesConfig = this.props.config.embeddedPages;
homePageUrl={this.props.config.welcomePageUrl} 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; break;

View file

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

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"); Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with 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 { _t } from '../../../languageHandler';
import LogoutDialog from "../dialogs/LogoutDialog"; import LogoutDialog from "../dialogs/LogoutDialog";
import Modal from "../../../Modal"; import Modal from "../../../Modal";
import SdkConfig from '../../../SdkConfig';
export class TopLeftMenu extends React.Component { export class TopLeftMenu extends React.Component {
constructor() { constructor() {
@ -27,8 +28,28 @@ export class TopLeftMenu extends React.Component {
this.signOut = this.signOut.bind(this); 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() { 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"> return <div className="mx_TopLeftMenu">
{homePageSection}
<ul className="mx_TopLeftMenu_section"> <ul className="mx_TopLeftMenu_section">
<li className="mx_TopLeftMenu_icon_settings" onClick={this.openSettings}>{_t("Settings")}</li> <li className="mx_TopLeftMenu_icon_settings" onClick={this.openSettings}>{_t("Settings")}</li>
</ul> </ul>
@ -38,6 +59,11 @@ export class TopLeftMenu extends React.Component {
</div>; </div>;
} }
viewHomePage() {
dis.dispatch({action: 'view_home_page'});
this.closeMenu();
}
openSettings() { openSettings() {
dis.dispatch({action: 'view_user_settings'}); dis.dispatch({action: 'view_user_settings'});
this.closeMenu(); this.closeMenu();

View file

@ -1279,6 +1279,7 @@
"<safariLink>Safari</safariLink> and <operaLink>Opera</operaLink> work too.": "<safariLink>Safari</safariLink> and <operaLink>Opera</operaLink> work too.", "<safariLink>Safari</safariLink> and <operaLink>Opera</operaLink> work too.": "<safariLink>Safari</safariLink> and <operaLink>Opera</operaLink> work too.",
"With your current browser, the look and feel of the application may be completely incorrect, and some or all features may not function. If you want to try it anyway you can continue, but you are on your own in terms of any issues you may encounter!": "With your current browser, the look and feel of the application may be completely incorrect, and some or all features may not function. If you want to try it anyway you can continue, but you are on your own in terms of any issues you may encounter!", "With your current browser, the look and feel of the application may be completely incorrect, and some or all features may not function. If you want to try it anyway you can continue, but you are on your own in terms of any issues you may encounter!": "With your current browser, the look and feel of the application may be completely incorrect, and some or all features may not function. If you want to try it anyway you can continue, but you are on your own in terms of any issues you may encounter!",
"I understand the risks and wish to continue": "I understand the risks and wish to continue", "I understand the risks and wish to continue": "I understand the risks and wish to continue",
"Couldn't load page": "Couldn't load page",
"You must <a>register</a> to use this functionality": "You must <a>register</a> to use this functionality", "You must <a>register</a> to use this functionality": "You must <a>register</a> to use this functionality",
"You must join the room to see its files": "You must join the room to see its files", "You must join the room to see its files": "You must join the room to see its files",
"There are no visible files in this room": "There are no visible files in this room", "There are no visible files in this room": "There are no visible files in this room",
@ -1323,7 +1324,6 @@
"Community %(groupId)s not found": "Community %(groupId)s not found", "Community %(groupId)s not found": "Community %(groupId)s not found",
"This homeserver does not support communities": "This homeserver does not support communities", "This homeserver does not support communities": "This homeserver does not support communities",
"Failed to load %(groupId)s": "Failed to load %(groupId)s", "Failed to load %(groupId)s": "Failed to load %(groupId)s",
"Couldn't load home page": "Couldn't load home page",
"Invalid configuration: Cannot supply a default homeserver URL and a default server name": "Invalid configuration: Cannot supply a default homeserver URL and a default server name", "Invalid configuration: Cannot supply a default homeserver URL and a default server name": "Invalid configuration: Cannot supply a default homeserver URL and a default server name",
"Failed to reject invitation": "Failed to reject invitation", "Failed to reject invitation": "Failed to reject invitation",
"This room is not public. You will not be able to rejoin without an invite.": "This room is not public. You will not be able to rejoin without an invite.", "This room is not public. You will not be able to rejoin without an invite.": "This room is not public. You will not be able to rejoin without an invite.",