diff --git a/app.js b/app.js
index bbb1288b3b..8ffb192f17 100644
--- a/app.js
+++ b/app.js
@@ -192,6 +192,15 @@ $(document).ready(function () {
console.log("(TIME) document ready:\t", now);
URLProcessor.setConfigParametersFromUrl();
+
+ // TODO The execution of the mobile app starts from react/index.native.js.
+ // Similarly, the execution of the Web app should start from
+ // react/index.web.js for the sake of consistency and ease of understanding.
+ // Temporarily though because we are at the beginning of introducing React
+ // into the Web app, allow the execution of the Web app to start from app.js
+ // in order to reduce the complexity of the beginning step.
+ require('./react');
+
APP.init();
APP.translation.init(settings.getLanguage());
diff --git a/index.html b/index.html
index f24aeb54ee..7764d02765 100644
--- a/index.html
+++ b/index.html
@@ -34,7 +34,7 @@
window.removeEventListener(
'error', loadErrHandler, true /* capture phase */);
}
- }
+ };
window.addEventListener(
'error', loadErrHandler, true /* capture phase type of listener */);
@@ -50,141 +50,7 @@
-
diff --git a/package.json b/package.json
index 22d4bee889..19699e1a17 100644
--- a/package.json
+++ b/package.json
@@ -39,7 +39,9 @@
"react-native": "0.37.0",
"react-native-vector-icons": "^2.0.3",
"react-native-webrtc": "jitsi/react-native-webrtc",
- "react-redux": "^4.4.5",
+ "react-redux": "^4.4.6",
+ "react-router": "^3.0.0",
+ "react-router-redux": "^4.0.7",
"redux": "^3.5.2",
"redux-thunk": "^2.1.0",
"retry": "0.6.1",
diff --git a/react/features/app/components/App.web.js b/react/features/app/components/App.web.js
new file mode 100644
index 0000000000..63c0cd1f01
--- /dev/null
+++ b/react/features/app/components/App.web.js
@@ -0,0 +1,157 @@
+import React from 'react';
+import { Provider } from 'react-redux';
+import {
+ browserHistory,
+ Route,
+ Router
+} from 'react-router';
+import { push, syncHistoryWithStore } from 'react-router-redux';
+
+import { getDomain } from '../../base/connection';
+import { RouteRegistry } from '../../base/navigator';
+
+import { AbstractApp } from './AbstractApp';
+
+/**
+ * Root application component.
+ *
+ * @extends AbstractApp
+ */
+export class App extends AbstractApp {
+ /**
+ * Initializes a new App instance.
+ *
+ * @param {Object} props - The read-only React Component props with which
+ * the new instance is to be initialized.
+ */
+ constructor(props) {
+ super(props);
+
+ /**
+ * Create an enhanced history that syncs navigation events with the
+ * store.
+ * @link https://github.com/reactjs/react-router-redux#how-it-works
+ */
+ this.history = syncHistoryWithStore(browserHistory, props.store);
+
+ // Bind event handlers so they are only bound once for every instance.
+ this._onRouteEnter = this._onRouteEnter.bind(this);
+ this._routerCreateElement = this._routerCreateElement.bind(this);
+ }
+
+ /**
+ * Temporarily, prevents the super from dispatching Redux actions until they
+ * are integrated into the Web App.
+ *
+ * @returns {void}
+ */
+ componentWillMount() {
+ // FIXME Do not override the super once the dispatching of Redux actions
+ // is integrated into the Web App.
+ }
+
+ /**
+ * Temporarily, prevents the super from dispatching Redux actions until they
+ * are integrated into the Web App.
+ *
+ * @returns {void}
+ */
+ componentWillUnmount() {
+ // FIXME Do not override the super once the dispatching of Redux actions
+ // is integrated into the Web App.
+ }
+
+ /**
+ * Implements React's {@link Component#render()}.
+ *
+ * @inheritdoc
+ * @returns {ReactElement}
+ */
+ render() {
+ const routes = RouteRegistry.getRoutes();
+
+ return (
+
+
+ { routes.map(r =>
+
+ ) }
+
+
+ );
+ }
+
+ /**
+ * Navigates to a specific Route (via platform-specific means).
+ *
+ * @param {Route} route - The Route to which to navigate.
+ * @returns {void}
+ */
+ _navigate(route) {
+ let path = route.path;
+ const store = this.props.store;
+
+ // The syntax :room bellow is defined by react-router. It "matches a URL
+ // segment up to the next /, ?, or #. The matched string is called a
+ // param."
+ path
+ = path.replace(
+ /:room/g,
+ store.getState()['features/base/conference'].room);
+
+ return store.dispatch(push(path));
+ }
+
+ /**
+ * Invoked by react-router to notify this App that a Route is about to be
+ * rendered.
+ *
+ * @private
+ * @returns {void}
+ */
+ _onRouteEnter() {
+ // XXX The following is mandatory. Otherwise, moving back & forward
+ // through the browser's history could leave this App on the Conference
+ // page without a room name.
+
+ // Our Router configuration (at the time of this writing) is such that
+ // each Route corresponds to a single URL. Hence, entering into a Route
+ // is like opening a URL.
+
+ // XXX In order to unify work with URLs in web and native environments,
+ // we will construct URL here with correct domain from config.
+ const currentDomain = getDomain(this.props.store.getState);
+ const url
+ = new URL(window.location.pathname, `https://${currentDomain}`)
+ .toString();
+
+ this._openURL(url);
+ }
+
+ /**
+ * Create a ReactElement from the specified component and props on behalf of
+ * the associated Router.
+ *
+ * @param {Component} component - The component from which the ReactElement
+ * is to be created.
+ * @param {Object} props - The read-only React Component props with which
+ * the ReactElement is to be initialized.
+ * @private
+ * @returns {ReactElement}
+ */
+ _routerCreateElement(component, props) {
+ return this._createElement(component, props);
+ }
+}
+
+/**
+ * App component's property types.
+ *
+ * @static
+ */
+App.propTypes = AbstractApp.propTypes;
diff --git a/react/features/base/lib-jitsi-meet/_.native.js b/react/features/base/lib-jitsi-meet/_.native.js
index 738c4d2b8a..c9d3c31244 100644
--- a/react/features/base/lib-jitsi-meet/_.native.js
+++ b/react/features/base/lib-jitsi-meet/_.native.js
@@ -1 +1,33 @@
-export * from './native';
+import './native';
+
+// The library lib-jitsi-meet (externally) depends on the libraries jQuery and
+// Strophe
+(global => {
+ // jQuery
+ if (typeof global.$ === 'undefined') {
+ const jQuery = require('jquery');
+
+ jQuery(global);
+ global.$ = jQuery;
+ }
+
+ // Strophe
+ if (typeof global.Strophe === 'undefined') {
+ require('strophe');
+ require('strophejs-plugins/disco/strophe.disco');
+ require('strophejs-plugins/caps/strophe.caps.jsonly');
+ }
+})(global || window || this); // eslint-disable-line no-invalid-this
+
+// Re-export JitsiMeetJS from the library lib-jitsi-meet to (the other features
+// of) the project jitsi-meet-react.
+//
+// TODO The Web support implemented by the jitsi-meet project explicitly uses
+// the library lib-jitsi-meet as a binary and keeps it out of the application
+// bundle. The mobile support implemented by the jitsi-meet-react project did
+// not get to keeping the lib-jitsi-meet library out of the application bundle
+// and even used it from source. As an intermediate step, start using the
+// library lib-jitsi-meet as a binary on mobile at the time of this writing. In
+// the future, implement not packaging it in the application bundle.
+import JitsiMeetJS from 'lib-jitsi-meet/lib-jitsi-meet.min';
+export { JitsiMeetJS as default };
diff --git a/react/features/base/lib-jitsi-meet/_.web.js b/react/features/base/lib-jitsi-meet/_.web.js
new file mode 100644
index 0000000000..918f582ddf
--- /dev/null
+++ b/react/features/base/lib-jitsi-meet/_.web.js
@@ -0,0 +1,3 @@
+/* global JitsiMeetJS */
+
+export default JitsiMeetJS;
diff --git a/react/features/base/lib-jitsi-meet/index.js b/react/features/base/lib-jitsi-meet/index.js
index d2a4d8bf00..94ab52e119 100644
--- a/react/features/base/lib-jitsi-meet/index.js
+++ b/react/features/base/lib-jitsi-meet/index.js
@@ -1,36 +1,6 @@
-import './_';
-
-// The library lib-jitsi-meet (externally) depends on the libraries jQuery and
-// Strophe
-(global => {
- // jQuery
- if (typeof global.$ === 'undefined') {
- const jQuery = require('jquery');
-
- jQuery(global);
- global.$ = jQuery;
- }
-
- // Strophe
- if (typeof global.Strophe === 'undefined') {
- require('strophe');
- require('strophejs-plugins/disco/strophe.disco');
- require('strophejs-plugins/caps/strophe.caps.jsonly');
- }
-})(global || window || this); // eslint-disable-line no-invalid-this
-
// Re-export JitsiMeetJS from the library lib-jitsi-meet to (the other features
// of) the project jitsi-meet-react.
-//
-// TODO The Web support implemented by the jitsi-meet project explicitly uses
-// the library lib-jitsi-meet as a binary and keeps it out of the application
-// bundle. The mobile support implemented by the jitsi-meet-react project did
-// not get to keeping the lib-jitsi-meet library out of the application bundle
-// and even used it from source. As an intermediate step, start using the
-// library lib-jitsi-meet as a binary on mobile at the time of this writing. In
-// the future, implement not packaging it in the application bundle.
-import JitsiMeetJS from 'lib-jitsi-meet/lib-jitsi-meet.min';
-
+import JitsiMeetJS from './_';
export { JitsiMeetJS as default };
export * from './actions';
diff --git a/plugin.header.text.html b/react/features/base/media/components/_.web.js
similarity index 100%
rename from plugin.header.text.html
rename to react/features/base/media/components/_.web.js
diff --git a/plugin.welcomepage.footer.html b/react/features/base/util/loadScript.web.js
similarity index 100%
rename from plugin.welcomepage.footer.html
rename to react/features/base/util/loadScript.web.js
diff --git a/react/features/conference/components/Conference.js b/react/features/conference/components/Conference.native.js
similarity index 100%
rename from react/features/conference/components/Conference.js
rename to react/features/conference/components/Conference.native.js
diff --git a/react/features/conference/components/Conference.web.js b/react/features/conference/components/Conference.web.js
new file mode 100644
index 0000000000..422ae4918d
--- /dev/null
+++ b/react/features/conference/components/Conference.web.js
@@ -0,0 +1,133 @@
+import React, { Component } from 'react';
+
+/**
+ * For legacy reasons, inline style for display none.
+ * @type {{display: string}}
+ */
+const DISPLAY_NONE_STYLE = {
+ display: 'none'
+};
+
+/**
+ * Implements a React Component which renders initial conference layout
+ */
+export default class Conference extends Component {
+
+ /**
+ * Implements React's {@link Component#render()}.
+ *
+ * @inheritdoc
+ * @returns {ReactElement}
+ */
+ render() {
+ return (
+
+ );
+ }
+}
diff --git a/react/features/conference/components/ParticipantView.js b/react/features/conference/components/ParticipantView.native.js
similarity index 100%
rename from react/features/conference/components/ParticipantView.js
rename to react/features/conference/components/ParticipantView.native.js
diff --git a/react/features/conference/components/ParticipantView.web.js b/react/features/conference/components/ParticipantView.web.js
new file mode 100644
index 0000000000..e7d789ebc5
--- /dev/null
+++ b/react/features/conference/components/ParticipantView.web.js
@@ -0,0 +1,22 @@
+import { Component } from 'react';
+
+/**
+ * Implements a React Component which depicts a specific participant's avatar
+ * and video.
+ */
+export default class ParticipantView extends Component {
+
+ /**
+ * Implements React's {@link Component#render()}.
+ *
+ * @inheritdoc
+ * @returns {ReactElement|null}
+ */
+ render() {
+ // FIXME ParticipantView is supposed to be platform-independent.
+ // Temporarily though, ParticipantView is not in use on Web but has to
+ // exist in order to split App, Conference, and WelcomePage out of
+ // index.html.
+ return null;
+ }
+ }
diff --git a/react/features/welcome/components/WelcomePage.native.js b/react/features/welcome/components/WelcomePage.native.js
index caf9c98e30..23fa74dd67 100644
--- a/react/features/welcome/components/WelcomePage.native.js
+++ b/react/features/welcome/components/WelcomePage.native.js
@@ -22,7 +22,9 @@ class WelcomePage extends AbstractWelcomePage {
render() {
return (
- { this._renderLocalVideo() }
+ {
+ this._renderLocalVideo()
+ }
Enter room name
+ {
+ this._renderHeader()
+ }
+ {
+ this._renderMain()
+ }
+
+ );
+ }
+
+ /**
+ * Renders a feature with a specific index.
+ *
+ * @param {number} index - The index of the feature to render.
+ * @private
+ * @returns {ReactElement}
+ */
+ _renderFeature(index) {
+ return (
+
+ );
+ }
+
+ /**
+ * Renders a row of features.
+ *
+ * @param {number} beginIndex - The inclusive feature index to begin the row
+ * with.
+ * @param {number} endIndex - The exclusive feature index to end the row
+ * with.
+ * @private
+ * @returns {ReactElement}
+ */
+ _renderFeatureRow(beginIndex, endIndex) {
+ const features = [];
+
+ for (let index = beginIndex; index < endIndex; ++index) {
+ features.push(this._renderFeature(index));
+ }
+
+ return (
+
+ {
+ features
+ }
+
+ );
+ }
+
+ /**
+ * Renders the header part of this WelcomePage.
+ *
+ * @private
+ * @returns {ReactElement|null}
+ */
+ _renderHeader() {
+ return (
+
+ );
+ }
+
+ /**
+ * Renders the main part of this WelcomePage.
+ *
+ * @private
+ * @returns {ReactElement|null}
+ */
+ _renderMain() {
+ return (
+
+
+ {
+ this._renderFeatureRow(1, 5)
+ }
+ {
+ this._renderFeatureRow(5, 9)
+ }
+
+
+ );
+ }
+}
diff --git a/react/index.web.js b/react/index.web.js
new file mode 100644
index 0000000000..963b02b5ac
--- /dev/null
+++ b/react/index.web.js
@@ -0,0 +1,54 @@
+import React from 'react';
+import ReactDOM from 'react-dom';
+import { browserHistory } from 'react-router';
+import {
+ routerMiddleware,
+ routerReducer
+} from 'react-router-redux';
+import { compose, createStore } from 'redux';
+import Thunk from 'redux-thunk';
+
+import config from './config';
+import { App } from './features/app';
+import {
+ MiddlewareRegistry,
+ ReducerRegistry
+} from './features/base/redux';
+
+// Create combined reducer from all reducers in registry + routerReducer from
+// 'react-router-redux' module (stores location updates from history).
+// @see https://github.com/reactjs/react-router-redux#routerreducer.
+const reducer = ReducerRegistry.combineReducers({
+ routing: routerReducer
+});
+
+// Apply all registered middleware from the MiddlewareRegistry + additional
+// 3rd party middleware:
+// - Thunk - allows us to dispatch async actions easily. For more info
+// @see https://github.com/gaearon/redux-thunk.
+// - routerMiddleware - middleware from 'react-router-redux' module to track
+// changes in browser history inside Redux state. For more information
+// @see https://github.com/reactjs/react-router-redux.
+let middleware = MiddlewareRegistry.applyMiddleware(
+ Thunk,
+ routerMiddleware(browserHistory));
+
+// Try to enable Redux DevTools Chrome extension in order to make it available
+// for the purposes of facilitating development.
+let devToolsExtension;
+
+if (typeof window === 'object'
+ && (devToolsExtension = window.devToolsExtension)) {
+ middleware = compose(middleware, devToolsExtension());
+}
+
+// Create Redux store with our reducer and middleware.
+const store = createStore(reducer, middleware);
+
+// Render the main Component.
+ReactDOM.render(
+
,
+ document.getElementById('react'));