Merge jitsi-meet-react's mobile support

As a step toward merging jitsi-meet-react with jitsi-meet to share as
much source code as possible between mobile and Web, merge the part of
jitsi-meet-react's source tree which supports mobile inside the
jitsi-meet source tree and leave jitsi-meet-react's Web support in the
source code revision history but don't have it in master anymore because
it's different from jitsi-meet's Web support. In other words, the two
projects are mechanically merged at the file level and don't really
share source code between mobile and Web.
This commit is contained in:
Lyubomir Marinov
2016-10-07 10:28:54 -05:00
parent 1edebf83ae
commit 7f3ff13c18
103 changed files with 76 additions and 4165 deletions

View File

@@ -1,139 +0,0 @@
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);
}
/**
* Implements React's {@link Component#render()}.
*
* @inheritdoc
* @returns {ReactElement}
*/
render() {
const routes = RouteRegistry.getRoutes();
/* eslint-disable no-extra-parens */
return (
<Provider store = { this.props.store }>
<Router
createElement = { this._routerCreateElement }
history = { this.history }>
{ routes.map(r => (
<Route
component = { r.component }
key = { r.component }
onEnter = { this._onRouteEnter }
path = { r.path } />
)) }
</Router>
</Provider>
);
/* eslint-enable no-extra-parens */
}
/**
* 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

@@ -21,7 +21,15 @@ import './_';
// Re-export JitsiMeetJS from the library lib-jitsi-meet to (the other features
// of) the project jitsi-meet-react.
import JitsiMeetJS from 'lib-jitsi-meet';
//
// 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

@@ -1 +0,0 @@
export * from './web';

View File

@@ -1,50 +0,0 @@
import React, { Component } from 'react';
/**
* The React equivalent of Web's audio element.
*
* @extends Component
*/
export class Audio extends Component {
/**
* Implements React's {@link Component#render()}.
*
* @inheritdoc
* @returns {ReactElement}
*/
render() {
// TODO URL.releaseObjectURL on componentDid/WillUnmount
const src = this.props.stream
? URL.createObjectURL(this.props.stream)
: '';
return (
<audio
autoPlay = { true }
muted = { this.props.muted }
src = { src } />
);
}
/**
* Implements shouldComponentUpdate of React Component. We don't update
* component if stream has not changed.
*
* @inheritdoc
* @param {Object} nextProps - Props that component is going to receive.
* @returns {boolean}
*/
shouldComponentUpdate(nextProps) {
return (nextProps.stream || {}).id !== (this.props.stream || {}).id;
}
}
/**
* Audio component's property types.
*
* @static
*/
Audio.propTypes = {
muted: React.PropTypes.bool,
stream: React.PropTypes.object
};

View File

@@ -1,68 +0,0 @@
import React, { Component } from 'react';
import { styles } from './styles';
/**
* Web version of Audio component.
* @extends Component
*/
export class Video extends Component {
/**
* Implements React's {@link Component#render()}.
*
* @inheritdoc
* @returns {ReactElement|null}
*/
render() {
const stream = this.props.stream;
if (stream) {
// TODO URL.releaseObjectURL on componentDid/WillUnmount
const src = URL.createObjectURL(stream);
const style
= this.props.mirror ? styles.mirroredVideo : styles.video;
return (
<video
autoPlay = { true }
muted = { this.props.muted }
onPlaying = { this.props.onPlaying }
src = { src }
style = { style } />
);
}
return null;
}
/**
* Implements shouldComponentUpdate of React Component. We don't update
* component if stream has not changed.
*
* @inheritdoc
* @param {Object} nextProps - Props that component is going to receive.
* @returns {boolean}
*/
shouldComponentUpdate(nextProps) {
return (nextProps.stream || {}).id !== (this.props.stream || {}).id;
}
}
/**
* Video component's property types.
*
* @static
*/
Video.propTypes = {
mirror: React.PropTypes.bool,
muted: React.PropTypes.bool,
onPlaying: React.PropTypes.func,
stream: React.PropTypes.object,
/**
* Not used on Web. Introduced for the benefit of React Native. For more
* details, refer to the zOrder property of the Video class for React
* Native (i.e. ../native/Video.js).
*/
zOrder: React.PropTypes.number
};

View File

@@ -1,20 +0,0 @@
import { connect } from 'react-redux';
import { AbstractVideoTrack } from '../AbstractVideoTrack';
/**
* Component that renders video element for a specified video track.
*
* @extends AbstractVideoTrack
*/
class VideoTrack extends AbstractVideoTrack {
}
/**
* VideoTrack component's property types.
*
* @static
*/
VideoTrack.propTypes = AbstractVideoTrack.propTypes;
export default connect()(VideoTrack);

View File

@@ -1,3 +0,0 @@
export * from './Audio';
export * from './Video';
export { default as VideoTrack } from './VideoTrack';

View File

@@ -1,24 +0,0 @@
/**
* Make video element fill its container.
*/
const video = {
flex: 1,
objectFit: 'cover',
width: '100%'
};
/**
* Transform local videos to behave like a mirror.
*/
const mirroredVideo = {
...video,
transform: 'scaleX(-1)'
};
/**
* Web-specific styles for media components.
*/
export const styles = {
mirroredVideo,
video
};

View File

@@ -1,43 +0,0 @@
import { stopEventPropagation } from '../functions';
import AbstractContainer from './AbstractContainer';
/**
* Represents a container of React Component children with a style.
*
* @extends AbstractContainer
*/
export class Container extends AbstractContainer {
/**
* Implements React's {@link Component#render()}.
*
* @inheritdoc
* @returns {ReactElement}
*/
render() {
// eslint-disable-next-line prefer-const
let { onClick, style, visible, ...props } = this.props;
// visible
if (typeof visible !== 'undefined' && !visible) {
style = {
...style,
display: 'none'
};
}
// onClick
(typeof onClick === 'function')
&& (props.onClick = stopEventPropagation(onClick));
// eslint-disable-next-line object-property-newline
return this._render('div', { ...props, style });
}
}
/**
* Container component's property types.
*
* @static
*/
Container.propTypes = AbstractContainer.propTypes;

View File

@@ -1 +0,0 @@
export * from './web';

View File

@@ -1 +0,0 @@
export * from './styles';

View File

@@ -1 +0,0 @@
require('style!./FontAwesome.css');

View File

@@ -1,23 +0,0 @@
/**
* Shim style properties to work correctly on Web.
*
* Some generic properties used by react-native for styling require additional
* style fields to be included in order to work on Web. For example, setting the
* `flex` property to control the flexbox layout also requires setting the
* `display` property to `flexbox` for the `flex` style to take effect.
*
* Using this shimStyles method allows us to minimize the number of style
* declarations that need to be set or overridden for specific platforms.
*
* @param {Object} styles - A dictionary of named style definitions.
* @returns {Object}
*/
export function shimStyles(styles) {
// The flexbox layout must be explicitly chosen on Web by assigning flex to
// display. This way the React Native styles can be reused on Web.
if (styles.flex) {
styles.display = 'flex';
}
return styles;
}

View File

@@ -1,84 +0,0 @@
/**
* Loads a script from a specific source. This is an extended version of
* loadScript method from ScriptUtil in lib-jitsi-meet.
*
* @param {string} src - The source from the which the script is to be
* (down)loaded. Can be absolute or relative URL.
* @param {Object} options - Additional options.
* @param {boolean} options.async=true - True to asynchronously load the script
* or false to synchronously load the script.
* @param {boolean} options.prepend=false - True to schedule the loading of the
* script as soon as possible or false to schedule the loading of the script at
* the end of the scripts known at the time.
* @returns {void}
*/
export function loadScript(
src,
options = {
async: true,
prepend: false
}) {
return new Promise((resolve, reject) => {
const d = document;
const tagName = 'script';
const script = d.createElement(tagName);
const referenceNode = d.getElementsByTagName(tagName)[0];
let scriptSource = src;
if (isRelativeURL(src)) {
// Find the src URL of the current loaded script and use it as the
// base of the specified src (argument).
const scriptEl = document.currentScript;
if (scriptEl) {
const scriptSrc = scriptEl.src;
const baseScriptSrc
= scriptSrc.substring(0, scriptSrc.lastIndexOf('/') + 1);
if (scriptSrc && baseScriptSrc) {
scriptSource = new URL(src, baseScriptSrc).toString();
}
}
}
script.async = Boolean(options.async);
script.onerror = reject;
script.onload = resolve;
script.src = scriptSource;
if (referenceNode) {
if (options.prepend) {
referenceNode.parentNode.insertBefore(script, referenceNode);
} else {
referenceNode.parentNode.appendChild(script);
}
} else {
const head = d.getElementsByTagName('head')[0];
head.appendChild(script);
}
});
}
/**
* Determines if passed URL is relative or not.
*
* @param {string} url - URL.
* @returns {boolean}
*/
function isRelativeURL(url) {
let relative;
// XXX If the specified value is an absolute URL, then an URL object will be
// correctly initialized from it. Otherwise, an exception will be thrown and
// we will treat the specified value as a relative URL.
try {
new URL(url); // eslint-disable-line no-new
relative = false;
} catch (ex) {
relative = true;
}
return relative;
}

View File

@@ -1,45 +0,0 @@
import React, { Component } from 'react';
import { styles } from './styles';
/**
* Display a participant avatar.
*/
export default class Avatar extends Component {
/**
* Implements React's {@link Component#render()}.
*
* @inheritdoc
*/
render() {
const style = {
// XXX Avatar is expected to display the whole image.
objectFit: 'contain',
...styles.avatar,
...this.props.style
};
return (
<img
src = { this.props.uri }
style = { style } />
);
}
}
/**
* Avatar component's property types.
*
* @static
*/
Avatar.propTypes = {
/**
* The optional style to add to an Avatar in order to customize its base
* look (and feel).
*/
style: React.PropTypes.object,
uri: React.PropTypes.string
};

View File

@@ -1 +0,0 @@
export * from './web';

View File

@@ -1,22 +0,0 @@
import React, { Component } from 'react';
import Icon from 'react-fontawesome';
import { styles } from './styles';
/**
* Thumbnail badge for displaying the audio mute status of a participant.
*/
export class AudioMutedIndicator extends Component {
/**
* Implements React's {@link Component#render()}.
*
* @inheritdoc
*/
render() {
return (
<Icon
name = 'microphone-slash'
style = { styles.audioMutedIndicator } />
);
}
}

View File

@@ -1,25 +0,0 @@
import React, { Component } from 'react';
import Icon from 'react-fontawesome';
import { styles } from './styles';
/**
* Thumbnail badge showing that the participant is the dominant speaker in
* the conference.
*/
export class DominantSpeakerIndicator extends Component {
/**
* Implements React's {@link Component#render()}.
*
* @inheritdoc
*/
render() {
return (
<div style = { styles.dominantSpeakerIndicatorBackground }>
<Icon
name = 'bullhorn'
style = { styles.dominantSpeakerIndicator } />
</div>
);
}
}

View File

@@ -1,22 +0,0 @@
import React, { Component } from 'react';
import Icon from 'react-fontawesome';
import { styles } from './styles';
/**
* Thumbnail badge showing that the participant is a conference moderator.
*/
export class ModeratorIndicator extends Component {
/**
* Implements React's {@link Component#render()}.
*
* @inheritdoc
*/
render() {
return (
<Icon
name = 'star'
style = { styles.moderatorIndicator } />
);
}
}

View File

@@ -1,24 +0,0 @@
import React, { Component } from 'react';
import Icon from 'react-fontawesome';
import { styles } from './styles';
/**
* Thumbnail badge for displaying the video mute status of a participant.
*/
export class VideoMutedIndicator extends Component {
/**
* Implements React's {@link Component#render()}.
*
* @inheritdoc
*/
render() {
// TODO: This should use video-camera-slash, but that doesn't exist in
// the fontawesome icon set yet.
return (
<Icon
name = 'eye-slash'
style = { styles.videoMutedIndicator } />
);
}
}

View File

@@ -1,5 +0,0 @@
export * from './AudioMutedIndicator';
export * from './DominantSpeakerIndicator';
export * from './ModeratorIndicator';
export * from './styles';
export * from './VideoMutedIndicator';

View File

@@ -1,46 +0,0 @@
import { createStyleSheet } from '../../../base/styles';
import { styles as platformIndependentStyles } from '../styles';
/**
* Web-specific styles for the film strip.
*/
export const styles = createStyleSheet(platformIndependentStyles, {
/**
* Audio muted indicator style.
*/
audioMutedIndicator: {
textShadow: '1px 1px 2px black'
},
/**
* Dominant speaker indicator background style.
*/
dominantSpeakerIndicatorBackground: {
height: 15,
width: 15
},
/**
* Moderator indicator style.
*/
moderatorIndicator: {
textShadow: '1px 1px 2px black'
},
/**
* Video thumbnail style.
*/
thumbnail: {
height: 120,
width: 120
},
/**
* Video muted indicator style.
*/
videoMutedIndicator: {
textShadow: '1px 1px 2px black'
}
});

View File

@@ -1,89 +0,0 @@
import React from 'react';
import { connect } from 'react-redux';
import { MEDIA_TYPE } from '../../base/media';
import { Container } from '../../base/react';
import { ColorPalette } from '../../base/styles';
import {
AbstractToolbar,
mapStateToProps
} from './AbstractToolbar';
import { styles } from './styles';
import ToolbarButton from './ToolbarButton';
/**
* Implements the conference toolbar on Web.
*
* @extends AbstractToolbar
*/
class Toolbar extends AbstractToolbar {
/**
* Implements React's {@link Component#render()}.
*
* @inheritdoc
* @returns {ReactElement}
*/
render() {
const audioButtonStyles = this._getMuteButtonStyles(MEDIA_TYPE.AUDIO);
const videoButtonStyles = this._getMuteButtonStyles(MEDIA_TYPE.VIDEO);
return (
<Container
style = { styles.toolbarContainer }
visible = { this.props.visible }>
<div style = { styles.toolbarButtonsContainer }>
<ToolbarButton
iconName = { audioButtonStyles.iconName }
iconStyle = { audioButtonStyles.iconStyle }
// eslint-disable-next-line react/jsx-handler-names
onClick = { this._toggleAudio }
style = { audioButtonStyles.buttonStyle } />
<ToolbarButton
iconName = 'phone'
iconStyle = { styles.icon }
onClick = { this._onHangup }
style = {{
...styles.toolbarButton,
backgroundColor: ColorPalette.jitsiRed
}} />
<ToolbarButton
iconName = { videoButtonStyles.iconName }
iconStyle = { videoButtonStyles.iconStyle }
// eslint-disable-next-line react/jsx-handler-names
onClick = { this._toggleVideo }
style = { videoButtonStyles.buttonStyle } />
</div>
</Container>
);
}
}
/**
* Additional properties for various icons, which are now platform-dependent.
* This is done to have common logic of generating styles for web and native.
* TODO As soon as we have common font sets for web and native, this will no
* longer be required.
*/
Object.assign(Toolbar.prototype, {
audioIcon: 'microphone',
audioMutedIcon: 'microphone-slash',
videoIcon: 'video-camera',
// TODO Currently, for web version we're using default FontAwesome font set,
// which doesn't have 'slashed' version of 'video-camera' icon. But this
// should be changed as soon as we start to use custom Jitsi icons.
videoMutedIcon: 'video-camera'
});
/**
* Toolbar component's property types.
*
* @static
*/
Toolbar.propTypes = AbstractToolbar.propTypes;
export default connect(mapStateToProps)(Toolbar);

View File

@@ -1,47 +0,0 @@
import React from 'react';
import Icon from 'react-fontawesome';
import { stopEventPropagation } from '../../base/react';
import AbstractToolbarButton from './AbstractToolbarButton';
/**
* Represents a button in Toolbar on Web.
*
* @extends AbstractToolbarButton
*/
export default class ToolbarButton extends AbstractToolbarButton {
/**
* Renders the button of this Toolbar button.
*
* @param {Object} children - The children, if any, to be rendered inside
* the button. Presumably, contains the icon of this Toolbar button.
* @protected
* @returns {ReactElement} The button of this Toolbar button.
*/
_renderButton(children) {
const props = {};
'onClick' in this.props
&& (props.onClick = stopEventPropagation(this.props.onClick));
'style' in this.props && (props.style = this.props.style);
return React.createElement('button', props, children);
}
// eslint-disable-next-line valid-jsdoc
/**
* @inheritdoc
*/
_renderIcon() {
return super._renderIcon(Icon);
}
}
/**
* ToolbarButton component's property types.
*
* @static
*/
ToolbarButton.propTypes = AbstractToolbarButton.propTypes;

View File

@@ -1,53 +0,0 @@
import React from 'react';
import { connect } from 'react-redux';
import {
AbstractWelcomePage,
mapStateToProps
} from './AbstractWelcomePage';
import { styles } from './styles';
/**
* The web container rendering the welcome page.
*
* @extends AbstractWelcomePage
*/
class WelcomePage extends AbstractWelcomePage {
/**
* Renders a prompt for entering a room name.
*
* @returns {ReactElement}
*/
render() {
/* eslint-disable react/jsx-no-bind */
return (
<div style = { styles.container }>
{ this._renderLocalVideo() }
<div style = { styles.roomContainer }>
<p style = { styles.title }>Enter room name</p>
<input
onChange = { ev => this._onRoomChange(ev.target.value) }
style = { styles.textInput }
type = 'text'
value = { this.state.room || '' } />
<button
disabled = { this._isJoinDisabled() }
onClick = { this._onJoinClick }
style = { styles.button }>JOIN</button>
</div>
</div>
);
/* eslint-enable react/jsx-no-bind */
}
}
/**
* WelcomePage component's property types.
*
* @static
*/
WelcomePage.propTypes = AbstractWelcomePage.propTypes;
export default connect(mapStateToProps)(WelcomePage);