diff --git a/react/features/app/functions.native.js b/react/features/app/functions.native.js index 6d21cb66e..0807fdc05 100644 --- a/react/features/app/functions.native.js +++ b/react/features/app/functions.native.js @@ -1,7 +1,7 @@ import { isRoomValid } from '../base/conference'; import { RouteRegistry } from '../base/react'; import { Conference } from '../conference'; -import { WelcomePage } from '../welcome'; +import { BlankWelcomePage, WelcomePage } from '../welcome'; /** * Determines which route is to be rendered in order to depict a specific Redux @@ -28,7 +28,10 @@ export function _getRouteToRender(stateOrGetState) { // React-ive way anyway so it's all the same difference. const { app } = state['features/app']; - component = app && app.props.welcomePageEnabled ? WelcomePage : null; + component + = app && app.props.welcomePageEnabled + ? WelcomePage + : BlankWelcomePage; } return RouteRegistry.getRouteByComponent(component); diff --git a/react/features/app/middleware.js b/react/features/app/middleware.js index e154bb138..a9e13223f 100644 --- a/react/features/app/middleware.js +++ b/react/features/app/middleware.js @@ -5,7 +5,6 @@ import { SET_LOCATION_URL } from '../base/connection'; import { MiddlewareRegistry } from '../base/redux'; -import { createLocalTracksA, destroyLocalTracks } from '../base/tracks'; MiddlewareRegistry.register(store => next => action => { switch (action.type) { @@ -71,38 +70,11 @@ function _connectionEstablished(store, next, action) { * @private * @returns {void} */ -function _navigate({ dispatch, getState }) { +function _navigate({ getState }) { const state = getState(); const { app, getRouteToRender } = state['features/app']; const routeToRender = getRouteToRender && getRouteToRender(state); - // FIXME The following is logic specific to the user experience of the - // mobile/React Native app. Firstly, I don't like that it's here at all. - // Secondly, I copied the mobile/React Native detection from - // react/features/base/config/reducer.js because I couldn't iron out an - // abstraction. Because of the first point, I'm leaving the second point - // unresolved to attract attention to the fact that the following needs more - // thinking. - if (navigator.product === 'ReactNative') { - // Create/destroy the local tracks as needed: create them the first time - // we are going to render an actual route (be that the WelcomePage or - // the Conference). - // - // When the WelcomePage is disabled, the app will transition to the - // null/undefined route. Detect these transitions and create/destroy the - // local tracks so the camera doesn't stay open if the app is not - // rendering any component. - if (typeof routeToRender === 'undefined' || routeToRender === null) { - // Destroy the local tracks if there is no route to render and there - // is no WelcomePage. - app.props.welcomePageEnabled || dispatch(destroyLocalTracks()); - } else { - // Create the local tracks if they haven't been created yet. - state['features/base/tracks'].some(t => t.local) - || dispatch(createLocalTracksA()); - } - } - return app._navigate(routeToRender); } diff --git a/react/features/base/tracks/actions.js b/react/features/base/tracks/actions.js index 54bd89261..6c7f4007e 100644 --- a/react/features/base/tracks/actions.js +++ b/react/features/base/tracks/actions.js @@ -10,6 +10,38 @@ import { getLocalParticipant } from '../participants'; import { TRACK_ADDED, TRACK_REMOVED, TRACK_UPDATED } from './actionTypes'; import { createLocalTracksF } from './functions'; +/** + * Requests the creating of the desired media type tracks. Desire is expressed + * by base/media. This function will dispatch a {@code createLocalTracksA} + * action for the "missing" types, that is, the ones which base/media would + * like to have (unmuted tracks) but are not present yet. + * + * @returns {Function} + */ +export function createDesiredLocalTracks() { + return (dispatch, getState) => { + const state = getState(); + const desiredTypes = []; + + state['features/base/media'].audio.muted + || desiredTypes.push(MEDIA_TYPE.AUDIO); + Boolean(state['features/base/media'].video.muted) + || desiredTypes.push(MEDIA_TYPE.VIDEO); + + const availableTypes + = state['features/base/tracks'] + .filter(t => t.local) + .map(t => t.mediaType); + + // We need to create the desired tracks which are not already available. + const createTypes + = desiredTypes.filter(type => availableTypes.indexOf(type) === -1); + + createTypes.length + && dispatch(createLocalTracksA({ devices: createTypes })); + }; +} + /** * Request to start capturing local audio and/or video. By default, the user * facing camera will be selected. diff --git a/react/features/conference/components/Conference.native.js b/react/features/conference/components/Conference.native.js index 3711395fc..6afcafc9a 100644 --- a/react/features/conference/components/Conference.native.js +++ b/react/features/conference/components/Conference.native.js @@ -4,6 +4,7 @@ import { connect as reactReduxConnect } from 'react-redux'; import { connect, disconnect } from '../../base/connection'; import { DialogContainer } from '../../base/dialog'; import { Container } from '../../base/react'; +import { createDesiredLocalTracks } from '../../base/tracks'; import { Filmstrip } from '../../filmstrip'; import { LargeVideo } from '../../large-video'; import { OverlayContainer } from '../../overlay'; @@ -222,19 +223,21 @@ class Conference extends Component { function _mapDispatchToProps(dispatch) { return { /** - * Dispatched an action connecting to the conference. + * Dispatches actions to create the desired local tracks and for + * connecting to the conference. * - * @returns {Object} Dispatched action. + * @returns {void} * @private */ _onConnect() { + dispatch(createDesiredLocalTracks()); dispatch(connect()); }, /** * Dispatches an action disconnecting from the conference. * - * @returns {Object} Dispatched action. + * @returns {void} * @private */ _onDisconnect() { @@ -246,7 +249,7 @@ function _mapDispatchToProps(dispatch) { * * @param {boolean} visible - True to show the Toolbox or false to hide * it. - * @returns {Object} Dispatched action. + * @returns {void} * @private */ _setToolboxVisible(visible: boolean) { diff --git a/react/features/welcome/components/BlankWelcomePage.js b/react/features/welcome/components/BlankWelcomePage.js new file mode 100644 index 000000000..b2651f418 --- /dev/null +++ b/react/features/welcome/components/BlankWelcomePage.js @@ -0,0 +1,49 @@ +import PropTypes from 'prop-types'; +import { Component } from 'react'; +import { connect } from 'react-redux'; + +import { destroyLocalTracks } from '../../base/tracks'; + +/** + * Component for rendering a blank welcome page. It renders absolutely nothing + * and destroys local tracks upon being mounted, since no media is desired when + * this component is rendered. + * + * The use case is mainly mobile, where SDK users probably disable the welcome + * page, but using it on the web in the future is not out of the question. + */ +class BlankWelcomePage extends Component { + /** + * {@code BlankWelcomePage} component's property types. + * + * @static + */ + static propTypes = { + dispatch: PropTypes.func + }; + + /** + * Destroys the local tracks (if any) since no media is desired when this + * component is rendered. + * + * @inheritdoc + * @returns {void} + */ + componentWillMount() { + this.props.dispatch(destroyLocalTracks()); + } + + /** + * Implements React's {@link Component#render()}. In this particular case + * we return null, because the entire purpose of this component is to render + * nothing. + * + * @inheritdoc + * @returns {null} + */ + render() { + return null; + } +} + +export default connect()(BlankWelcomePage); diff --git a/react/features/welcome/components/WelcomePage.native.js b/react/features/welcome/components/WelcomePage.native.js index 3be938bc5..1d37a7f99 100644 --- a/react/features/welcome/components/WelcomePage.native.js +++ b/react/features/welcome/components/WelcomePage.native.js @@ -3,8 +3,10 @@ import { TextInput, TouchableHighlight, View } from 'react-native'; import { connect } from 'react-redux'; import { translate } from '../../base/i18n'; +import { MEDIA_TYPE } from '../../base/media'; import { Link, Text } from '../../base/react'; import { ColorPalette } from '../../base/styles'; +import { createLocalTracksA } from '../../base/tracks'; import { AbstractWelcomePage, _mapStateToProps } from './AbstractWelcomePage'; import styles from './styles'; @@ -35,7 +37,20 @@ class WelcomePage extends AbstractWelcomePage { * * @static */ - static propTypes = AbstractWelcomePage.propTypes + static propTypes = AbstractWelcomePage.propTypes; + + /** + * Creates a video track if not already available. + * + * @inheritdoc + * @returns {void} + */ + componentWillMount() { + const { dispatch, _localVideoTrack } = this.props; + + (typeof _localVideoTrack === 'undefined') + && dispatch(createLocalTracksA({ devices: [ MEDIA_TYPE.VIDEO ] })); + } /** * Renders a prompt for entering a room name. diff --git a/react/features/welcome/components/index.js b/react/features/welcome/components/index.js index 404735edd..c23d36edf 100644 --- a/react/features/welcome/components/index.js +++ b/react/features/welcome/components/index.js @@ -1 +1,2 @@ +export { default as BlankWelcomePage } from './BlankWelcomePage'; export { default as WelcomePage } from './WelcomePage'; diff --git a/react/features/welcome/route.js b/react/features/welcome/route.js index 06a6f2c92..808a2b564 100644 --- a/react/features/welcome/route.js +++ b/react/features/welcome/route.js @@ -2,12 +2,21 @@ import { RouteRegistry } from '../base/react'; -import { WelcomePage } from './components'; +import { BlankWelcomePage, WelcomePage } from './components'; import { generateRoomWithoutSeparator } from './roomnameGenerator'; declare var APP: Object; declare var config: Object; +/** + * Register route for BlankWelcomePage. + */ +RouteRegistry.register({ + component: BlankWelcomePage, + undefined, + path: '/#blank' +}); + /** * Register route for WelcomePage. */