Split React components out of index.html

This commit is contained in:
Ilya Daynatovich 2016-11-23 15:46:46 -06:00 committed by Lyubomir Marinov
parent 57b0736ebb
commit c3428e8213
16 changed files with 570 additions and 170 deletions

9
app.js
View File

@ -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());

View File

@ -34,7 +34,7 @@
window.removeEventListener(
'error', loadErrHandler, true /* capture phase */);
}
}
};
window.addEventListener(
'error', loadErrHandler, true /* capture phase type of listener */);
</script>
@ -50,141 +50,7 @@
<!--#include virtual="plugin.head.html" -->
</head>
<body>
<div id="welcome_page">
<div id="welcome_page_header">
<a target="_new">
<div class="watermark leftwatermark"></div>
</a>
<a target="_new">
<div class="watermark rightwatermark"></div>
</a>
<a class="poweredby" href="http://jitsi.org" target="_new" ><span data-i18n="poweredby"></span> jitsi.org</a>
<div id="enter_room_container">
<div id="enter_room_form" >
<div id="domain_name"></div>
<div id="enter_room">
<input id="enter_room_field" type="text" autofocus/>
<div class="icon-reload" id="reload_roomname"></div>
<input id="enter_room_button" type="button" data-i18n="[value]welcomepage.go" value="GO" />
</div>
</div>
</div>
<div id="brand_header"></div>
<input type='checkbox' name='checkbox' id="disable_welcome"/>
<label for="disable_welcome" class="disable_welcome_position" data-i18n="welcomepage.disable"></label>
<div id="header_text">
<!--#include virtual="plugin.header.text.html" -->
</div>
</div>
<div id="welcome_page_main">
<div id="features">
<div class="feature_row">
<div class="feature_holder">
<div class="feature_icon" data-i18n="welcomepage.feature1.title" ></div>
<div class="feature_description" data-i18n="welcomepage.feature1.content" data-i18n-options='{ "postProcess": "resolveAppName" }'>
</div>
</div>
<div class="feature_holder">
<div class="feature_icon" data-i18n="welcomepage.feature2.title" ></div>
<div class="feature_description" data-i18n="welcomepage.feature2.content">
</div>
</div>
<div class="feature_holder">
<div class="feature_icon" data-i18n="welcomepage.feature3.title" ></div>
<div class="feature_description" data-i18n="welcomepage.feature3.content" data-i18n-options='{ "postProcess": "resolveAppName" }'>
</div>
</div>
<div class="feature_holder">
<div class="feature_icon" data-i18n="welcomepage.feature4.title" ></div>
<div class="feature_description" data-i18n="welcomepage.feature4.content">
</div>
</div>
</div>
<div class="feature_row">
<div class="feature_holder">
<div class="feature_icon" data-i18n="welcomepage.feature5.title" ></div>
<div class="feature_description" data-i18n="welcomepage.feature5.content" data-i18n-options='{ "postProcess": "resolveAppName" }'>
</div>
</div>
<div class="feature_holder">
<div class="feature_icon" data-i18n="welcomepage.feature6.title" ></div>
<div class="feature_description" data-i18n="welcomepage.feature6.content" data-i18n-options='{ "postProcess": "resolveAppName" }'>
</div>
</div>
<div class="feature_holder">
<div class="feature_icon" data-i18n="welcomepage.feature7.title" ></div>
<div class="feature_description" data-i18n="welcomepage.feature7.content" data-i18n-options='{ "postProcess": "resolveAppName" }'></div>
</div>
<div class="feature_holder">
<div class="feature_icon" data-i18n="welcomepage.feature8.title" ></div>
<div class="feature_description" data-i18n="welcomepage.feature8.content"></div>
</div>
</div>
</div>
</div>
<!--#include virtual="plugin.welcomepage.footer.html" -->
</div>
<div id="videoconference_page">
<div id="mainToolbarContainer">
<div id="notice" class="notice" style="display: none">
<span id="noticeText" class="noticeText"></span>
</div>
<div id="mainToolbar" class="toolbar"></div>
</div>
<div id="subject" class="hide"></div>
<div id="extendedToolbar" class="toolbar">
<div id="extendedToolbarButtons"></div>
<a class="button icon-feedback" id="feedbackButton"></a>
<div id="sideToolbarContainer"></div>
</div>
<div id="videospace">
<div id="largeVideoContainer" class="videocontainer">
<div id="sharedVideo"><div id="sharedVideoIFrame"></div></div>
<div id="etherpad"></div>
<a target="_new"><div class="watermark leftwatermark"></div></a>
<a target="_new"><div class="watermark rightwatermark"></div></a>
<a class="poweredby" href="http://jitsi.org" target="_new">
<span data-i18n="poweredby"></span> jitsi.org
</a>
<div id="dominantSpeaker">
<div class="dynamic-shadow"></div>
<img id="dominantSpeakerAvatar" src=""/>
</div>
<span id="remoteConnectionMessage"></span>
<div id="largeVideoWrapper">
<video id="largeVideo" muted="true" autoplay></video>
</div>
<span id="localConnectionMessage"></span>
<span id="videoResolutionLabel" class="video-state-indicator moveToCorner">HD</span>
<span id="recordingLabel" class="video-state-indicator centeredVideoLabel">
<span id="recordingLabelText"></span>
<img id="recordingSpinner" class="recordingSpinner" src="images/spin.svg"></img>
</span>
</div>
<div class="filmstrip">
<div class="filmstrip__videos" id="remoteVideos">
<span id="localVideoContainer" class="videocontainer">
<div class="videocontainer__background"></div>
<span id="localVideoWrapper">
<!--<video id="localVideo" autoplay muted></video> - is now per stream generated -->
</span>
<audio id="localAudio" autoplay muted></audio>
<div class="videocontainer__toolbar"></div>
<div class="videocontainer__toptoolbar"></div>
<div class="videocontainer__hoverOverlay"></div>
</span>
<audio id="userJoined" src="sounds/joined.wav" preload="auto"></audio>
<audio id="userLeft" src="sounds/left.wav" preload="auto"></audio>
</div>
</div>
</div>
</div>
<div id="react"></div>
<div id="keyboard-shortcuts" class="keyboard-shortcuts" style="display:none;">
<div class="content">
<ul id="keyboard-shortcuts-list" class="shortcuts-list">

View File

@ -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",

View File

@ -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 (
<Provider store = { this.props.store }>
<Router
createElement = { this._routerCreateElement }
history = { this.history }>
{ routes.map(r =>
<Route
component = { r.component }
key = { r.component }
path = { r.path } />
) }
</Router>
</Provider>
);
}
/**
* 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;

View File

@ -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 };

View File

@ -0,0 +1,3 @@
/* global JitsiMeetJS */
export default JitsiMeetJS;

View File

@ -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';

View File

@ -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 (
<div id = 'videoconference_page'>
<div id = 'mainToolbarContainer'>
<div
className = 'notice'
id = 'notice'
style = { DISPLAY_NONE_STYLE }>
<span
className = 'noticeText'
id = 'noticeText' />
</div>
<div
className = 'toolbar'
id = 'mainToolbar' />
</div>
<div
className = 'hide'
id = 'subject' />
<div
className = 'toolbar'
id = 'extendedToolbar'>
<div id = 'extendedToolbarButtons' />
<a
className = 'button icon-feedback'
id = 'feedbackButton' />
<div id = 'sideToolbarContainer' />
</div>
<div id = 'videospace'>
<div
className = 'videocontainer'
id = 'largeVideoContainer'>
<div id = 'sharedVideo'>
<div id = 'sharedVideoIFrame' />
</div>
<div id = 'etherpad' />
<a target = '_new'>
<div className = 'watermark leftwatermark' />
</a>
<a target = '_new'>
<div className = 'watermark rightwatermark' />
</a>
<a
className = 'poweredby'
href = 'http://jitsi.org'
target = '_new'>
<span data-i18n = 'poweredby' /> jitsi.org
</a>
<div id = 'dominantSpeaker'>
<div className = 'dynamic-shadow' />
<img
id = 'dominantSpeakerAvatar'
src = '' />
</div>
<span id = 'remoteConnectionMessage' />
<div id = 'largeVideoWrapper'>
<video
autoPlay = { true }
id = 'largeVideo'
muted = 'true' />
</div>
<span id = 'localConnectionMessage' />
<span
className = 'video-state-indicator moveToCorner'
id = 'videoResolutionLabel'>HD</span>
<span
className
= 'video-state-indicator centeredVideoLabel'
id = 'recordingLabel'>
<span id = 'recordingLabelText' />
<img
className = 'recordingSpinner'
id = 'recordingSpinner'
src = 'images/spin.svg' />
</span>
</div>
<div className = 'filmstrip'>
<div
className = 'filmstrip__videos'
id = 'remoteVideos'>
<span
className = 'videocontainer'
id = 'localVideoContainer'>
<div
className = 'videocontainer__background' />
<span id = 'localVideoWrapper' />
<audio
autoPlay = { true }
id = 'localAudio'
muted = { true } />
<div className = 'videocontainer__toolbar' />
<div
className = 'videocontainer__toptoolbar' />
<div
className
= 'videocontainer__hoverOverlay' />
</span>
<audio
id = 'userJoined'
preload = 'auto'
src = 'sounds/joined.wav' />
<audio
id = 'userLeft'
preload = 'auto'
src = 'sounds/left.wav' />
</div>
</div>
</div>
</div>
);
}
}

View File

@ -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;
}
}

View File

@ -22,7 +22,9 @@ class WelcomePage extends AbstractWelcomePage {
render() {
return (
<View style = { styles.container }>
{ this._renderLocalVideo() }
{
this._renderLocalVideo()
}
<View style = { styles.roomContainer }>
<Text style = { styles.title }>Enter room name</Text>
<TextInput

View File

@ -0,0 +1,150 @@
import React, { Component } from 'react';
/**
* The web container rendering the welcome page.
*/
export default class WelcomePage extends Component {
/**
* Implements React's {@link Component#render()}.
*
* @inheritdoc
* @returns {ReactElement|null}
*/
render() {
return (
<div id = 'welcome_page'>
{
this._renderHeader()
}
{
this._renderMain()
}
</div>
);
}
/**
* Renders a feature with a specific index.
*
* @param {number} index - The index of the feature to render.
* @private
* @returns {ReactElement}
*/
_renderFeature(index) {
return (
<div className = 'feature_holder'>
<div
className = 'feature_icon'
data-i18n = { `welcomepage.feature${index}.title` } />
<div
className = 'feature_description'
data-i18n = { `welcomepage.feature${index}.content` }
data-i18n-options = { JSON.stringify({
postProcess: 'resolveAppName'
}) } />
</div>
);
}
/**
* 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 (
<div className = 'feature_row'>
{
features
}
</div>
);
}
/**
* Renders the header part of this WelcomePage.
*
* @private
* @returns {ReactElement|null}
*/
_renderHeader() {
return (
<div id = 'welcome_page_header'>
<a target = '_new'>
<div className = 'watermark leftwatermark' />
</a>
<a target = '_new'>
<div className = 'watermark rightwatermark' />
</a>
<a
className = 'poweredby'
href = 'http://jitsi.org'
target = '_new'>
<span data-i18n = 'poweredby' /> jitsi.org
</a>
<div id = 'enter_room_container'>
<div id = 'enter_room_form'>
<div id = 'domain_name' />
<div id = 'enter_room'>
<input
autoFocus = { true }
id = 'enter_room_field'
type = 'text' />
<div
className = 'icon-reload'
id = 'reload_roomname' />
<input
data-i18n = '[value]welcomepage.go'
id = 'enter_room_button'
type = 'button'
value = 'GO' />
</div>
</div>
</div>
<div id = 'brand_header' />
<input
id = 'disable_welcome'
name = 'checkbox'
type = 'checkbox' />
<label
className = 'disable_welcome_position'
data-i18n = 'welcomepage.disable'
htmlFor = 'disable_welcome' />
<div id = 'header_text' />
</div>
);
}
/**
* Renders the main part of this WelcomePage.
*
* @private
* @returns {ReactElement|null}
*/
_renderMain() {
return (
<div id = 'welcome_page_main'>
<div id = 'features'>
{
this._renderFeatureRow(1, 5)
}
{
this._renderFeatureRow(5, 9)
}
</div>
</div>
);
}
}

54
react/index.web.js Normal file
View File

@ -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(
<App
config = { config }
store = { store }
url = { window.location.toString() } />,
document.getElementById('react'));