diff --git a/react/features/app/components/App.native.js b/react/features/app/components/App.native.js index b7794f35e..d311fd93b 100644 --- a/react/features/app/components/App.native.js +++ b/react/features/app/components/App.native.js @@ -1,10 +1,12 @@ /* global __DEV__ */ import PropTypes from 'prop-types'; +import React from 'react'; import { Linking } from 'react-native'; import '../../analytics'; import '../../authentication'; +import { AspectRatioDetector } from '../../base/aspect-ratio'; import { Platform } from '../../base/react'; import '../../mobile/audio-mode'; import '../../mobile/background'; @@ -86,6 +88,19 @@ export class App extends AbstractApp { super.componentWillUnmount(); } + /** + * Overrides the super method to inject {@link AspectRatioDetector} as + * the top most component. + * + * @override + */ + _createElement(component, props) { + return ( + + {super._createElement(component, props)} + ); + } + /** * Attempts to disable the use of React Native * {@link ExceptionsManager#handleException} on platforms and in diff --git a/react/features/base/aspect-ratio/actionTypes.js b/react/features/base/aspect-ratio/actionTypes.js new file mode 100644 index 000000000..f634474f8 --- /dev/null +++ b/react/features/base/aspect-ratio/actionTypes.js @@ -0,0 +1,9 @@ +/** + * The type of (redux) action which signals that a new aspect ratio has been + * detected by the app. + * { + * type: SET_ASPECT_RATIO, + * aspectRatio: Symbol + * } + */ +export const SET_ASPECT_RATIO = Symbol('SET_ASPECT_RATIO'); diff --git a/react/features/base/aspect-ratio/actions.js b/react/features/base/aspect-ratio/actions.js new file mode 100644 index 000000000..6d95c87ab --- /dev/null +++ b/react/features/base/aspect-ratio/actions.js @@ -0,0 +1,22 @@ +/* @flow */ + +import { SET_ASPECT_RATIO } from './actionTypes'; +import { ASPECT_RATIO_NARROW, ASPECT_RATIO_WIDE } from './constants'; + +/** + * Calculates new aspect ratio for the app based on provided width and height + * values. + * + * @param {number} width - The width of the app's area used on the screen. + * @param {number} height - The height of the app's area used on the screen. + * @returns {{ + * type: SET_ASPECT_RATIO, + * aspectRatio: Symbol + * }} + */ +export function calculateNewAspectRatio(width: number, height: number): Object { + return { + type: SET_ASPECT_RATIO, + aspectRatio: width > height ? ASPECT_RATIO_WIDE : ASPECT_RATIO_NARROW + }; +} diff --git a/react/features/base/aspect-ratio/components/AspectRatioAware.js b/react/features/base/aspect-ratio/components/AspectRatioAware.js new file mode 100644 index 000000000..e566bd5a4 --- /dev/null +++ b/react/features/base/aspect-ratio/components/AspectRatioAware.js @@ -0,0 +1,68 @@ +// @flow +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import { connect } from 'react-redux'; + +import { ASPECT_RATIO_NARROW } from '../constants'; + +/** + * Decorates given React component class into {@link AspectRatioAwareWrapper} + * which provides the aspectRatio property updated on each Redux state + * change. + * + * @param {ReactClass} WrapperComponent - A React component class to be wrapped. + * @returns {AspectRatioAwareWrapper} + */ +export function AspectRatioAware( + WrapperComponent: ReactClass<*>): ReactClass<*> { + return connect(_mapStateToProps)( + class AspectRatioAwareWrapper extends Component { + /** + * Properties of the aspect ratio aware wrapper. + */ + static propTypes = { + /** + * Either {@link ASPECT_RATIO_NARROW} or + * {@link ASPECT_RATIO_WIDE}. + */ + aspectRatio: PropTypes.symbol + } + + /** + * Implement's React render method to wrap the nested component. + * + * @returns {XML} + */ + render(): React$Element<*> { + return ; + } + }); +} + +/** + * Maps Redux state to {@link AspectRatioAwareWrapper} properties. + * + * @param {Object} state - The Redux whole state. + * @returns {{ + * aspectRatio: Symbol + * }} + * @private + */ +function _mapStateToProps(state) { + return { + aspectRatio: state['features/base/aspect-ratio'].aspectRatio + }; +} + +/** + * Checks if given React component decorated in {@link AspectRatioAwareWrapper} + * has currently the {@link ASPECT_RATIO_NARROW} set in the aspect ratio + * property. + * + * @param {AspectRatioAwareWrapper} component - A + * {@link AspectRatioAwareWrapper} which has aspectRation property. + * @returns {boolean} + */ +export function isNarrowAspectRatio(component: ReactClass<*>) { + return component.props.aspectRatio === ASPECT_RATIO_NARROW; +} diff --git a/react/features/base/aspect-ratio/components/AspectRatioDetector.native.js b/react/features/base/aspect-ratio/components/AspectRatioDetector.native.js new file mode 100644 index 000000000..f5f73b865 --- /dev/null +++ b/react/features/base/aspect-ratio/components/AspectRatioDetector.native.js @@ -0,0 +1,74 @@ +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import { View } from 'react-native'; +import { connect } from 'react-redux'; + +import { calculateNewAspectRatio } from '../actions'; +import styles from './styles'; + +/** + * A root {@link View} which captures the 'onLayout' event and figures out + * the aspect ratio of the app. + */ +class AspectRatioDetector extends Component { + /** + * AspectRatioDetector component's property types. + * + * @static + */ + static propTypes = { + /** + * The "onLayout" handler. + */ + _onLayout: PropTypes.func, + + /** + * Any nested components. + */ + children: PropTypes.object + }; + + /** + * Renders the root view and it's children. + * + * @returns {Component} + */ + render() { + return ( + + {this.props.children} + ); + } +} + +/** + * Maps dispatching of the aspect ratio actions to React component props. + * + * @param {Function} dispatch - Redux action dispatcher. + * @private + * @returns {{ + * _onLayout: Function + * }} + */ +function _mapDispatchToProps(dispatch) { + return { + /** + * Handles the "on layout" View's event and dispatches aspect ratio + * changed action. + * + * @param {{ width: number, height: number }} event - The "on layout" + * event structure passed by react-native. + * @returns {void} + * @private + */ + _onLayout(event) { + const { width, height } = event.nativeEvent.layout; + + dispatch(calculateNewAspectRatio(width, height)); + } + }; +} + +export default connect(undefined, _mapDispatchToProps)(AspectRatioDetector); diff --git a/react/features/base/aspect-ratio/components/index.js b/react/features/base/aspect-ratio/components/index.js new file mode 100644 index 000000000..30b6e5b50 --- /dev/null +++ b/react/features/base/aspect-ratio/components/index.js @@ -0,0 +1,2 @@ +export * from './AspectRatioAware'; +export { default as AspectRatioDetector } from './AspectRatioDetector'; diff --git a/react/features/base/aspect-ratio/components/styles.js b/react/features/base/aspect-ratio/components/styles.js new file mode 100644 index 000000000..199ef0aa3 --- /dev/null +++ b/react/features/base/aspect-ratio/components/styles.js @@ -0,0 +1,14 @@ +import { createStyleSheet, fixAndroidViewClipping } from '../../styles/index'; + +/** + * The styles of the feature app. + */ +export default createStyleSheet({ + /** + * The style for {@link AspectRatioDetector} root view used on react-native. + */ + aspectRatioDetectorStyle: fixAndroidViewClipping({ + alignSelf: 'stretch', + flex: 1 + }) +}); diff --git a/react/features/base/aspect-ratio/constants.js b/react/features/base/aspect-ratio/constants.js new file mode 100644 index 000000000..893042500 --- /dev/null +++ b/react/features/base/aspect-ratio/constants.js @@ -0,0 +1,15 @@ +/** + * The aspect ratio constant indicates that the app area's width is smaller than + * the height. + * + * @type {Symbol} + */ +export const ASPECT_RATIO_NARROW = Symbol('ASPECT_RATIO_NARROW'); + +/** + * Aspect ratio constant indicates that the app area's width is larger than + * the height. + * + * @type {Symbol} + */ +export const ASPECT_RATIO_WIDE = Symbol('ASPECT_RATIO_WIDE'); diff --git a/react/features/base/aspect-ratio/index.js b/react/features/base/aspect-ratio/index.js new file mode 100644 index 000000000..46a992195 --- /dev/null +++ b/react/features/base/aspect-ratio/index.js @@ -0,0 +1,6 @@ +export * from './actions'; +export * from './actionTypes'; +export * from './components'; +export * from './constants'; + +import './reducer'; diff --git a/react/features/base/aspect-ratio/reducer.js b/react/features/base/aspect-ratio/reducer.js new file mode 100644 index 000000000..440d5f130 --- /dev/null +++ b/react/features/base/aspect-ratio/reducer.js @@ -0,0 +1,19 @@ +import { ReducerRegistry, set } from '../redux'; + +import { SET_ASPECT_RATIO } from './actionTypes'; +import { ASPECT_RATIO_NARROW } from './constants'; + +const INITIAL_STATE = { + aspectRatio: ASPECT_RATIO_NARROW +}; + +ReducerRegistry.register( +'features/base/aspect-ratio', +(state = INITIAL_STATE, action) => { + switch (action.type) { + case SET_ASPECT_RATIO: + return set(state, 'aspectRatio', action.aspectRatio); + } + + return state; +}); diff --git a/react/features/conference/components/Conference.native.js b/react/features/conference/components/Conference.native.js index b6a7286b6..d616aeb55 100644 --- a/react/features/conference/components/Conference.native.js +++ b/react/features/conference/components/Conference.native.js @@ -184,15 +184,6 @@ class Conference extends Component { */} - {/* - * The Filmstrip is in a stacking layer above the LargeVideo. - * The LargeVideo and the Filmstrip form what the Web/React app - * calls "videospace". Presumably, the name and grouping stem - * from the fact that these two React Components depict the - * videos of the conference's participants. - */} - - {/* * The overlays need to be bellow the Toolbox so that the user * may tap the ToolbarButtons. @@ -209,10 +200,22 @@ class Conference extends Component { } - {/* - * The Toolbox is in a stacking layer above the Filmstrip. - */} - + + {/* + * The Toolbox is in a stacking layer above the Filmstrip. + */} + + {/* + * The Filmstrip is in a stacking layer above + * the LargeVideo. + * The LargeVideo and the Filmstrip form what the Web/React + * app calls "videospace". Presumably, the name and + * grouping stem from the fact that these two React + * Components depict the videos of the conference's + * participants. + */} + + {/* * The dialogs are in the topmost stacking layers. diff --git a/react/features/conference/components/styles.js b/react/features/conference/components/styles.js index 8b3fe78d0..d96c99eb5 100644 --- a/react/features/conference/components/styles.js +++ b/react/features/conference/components/styles.js @@ -38,5 +38,19 @@ export default createStyleSheet({ // contrast and translucency. backgroundColor: ColorPalette.appBackground, opacity: 0.5 + }, + + /** + * The style of the view which expands over the whole conference area and + * splits it between both the filmstrip and the toolbox. + */ + toolboxAndFilmstripContainer: { + bottom: 0, + flexDirection: 'column', + left: 0, + justifyContent: 'flex-end', + position: 'absolute', + right: 0, + top: 0 } }); diff --git a/react/features/filmstrip/components/Filmstrip.native.js b/react/features/filmstrip/components/Filmstrip.native.js index 7ceceb8c0..41be88aee 100644 --- a/react/features/filmstrip/components/Filmstrip.native.js +++ b/react/features/filmstrip/components/Filmstrip.native.js @@ -5,6 +5,7 @@ import React, { Component } from 'react'; import { ScrollView } from 'react-native'; import { connect } from 'react-redux'; +import { AspectRatioAware, isNarrowAspectRatio } from '../../base/aspect-ratio'; import { Container } from '../../base/react'; import Thumbnail from './Thumbnail'; @@ -47,15 +48,16 @@ class Filmstrip extends Component<*> { * @returns {ReactElement} */ render() { + const filmstripStyle + = isNarrowAspectRatio(this) + ? styles.filmstripNarrow : styles.filmstripWide; + return ( + style = { filmstripStyle } + visible = { this.props._visible } > { @@ -121,6 +123,8 @@ class Filmstrip extends Component<*> { * }} */ function _mapStateToProps(state) { + const participants = state['features/base/participants']; + return { /** * The participants in the conference. @@ -128,20 +132,20 @@ function _mapStateToProps(state) { * @private * @type {Participant[]} */ - _participants: state['features/base/participants'], + _participants: participants, /** * The indicator which determines whether the filmstrip is visible. * * XXX The React Component Filmstrip is used on mobile only at the time - * of this writing and on mobile the filmstrip is visible when the - * toolbar is not. + * of this writing and on mobile the filmstrip is when there are at + * least 2 participants in the conference (including the local one). * * @private * @type {boolean} */ - _visible: !state['features/toolbox'].visible + _visible: participants.length > 1 }; } -export default connect(_mapStateToProps)(Filmstrip); +export default connect(_mapStateToProps)(AspectRatioAware(Filmstrip)); diff --git a/react/features/filmstrip/components/styles.js b/react/features/filmstrip/components/styles.js index 00850469a..832cdc8e5 100644 --- a/react/features/filmstrip/components/styles.js +++ b/react/features/filmstrip/components/styles.js @@ -1,6 +1,14 @@ import { Platform } from '../../base/react'; import { BoxModel, ColorPalette } from '../../base/styles'; +/** + * The base filmstrip style shared between narrow and wide versions. + */ +const filmstripBaseStyle = { + flexGrow: 0, + flexDirection: 'column' +}; + /** * The styles of the feature filmstrip common to both Web and native. */ @@ -40,26 +48,28 @@ export default { }, /** - * The style of the Container which represents the very filmstrip. + * The style of the narrow filmstrip version which displays thumbnails + * in a row at the bottom of the screen. */ - filmstrip: { + filmstripNarrow: { + ...filmstripBaseStyle, alignItems: 'flex-end', - alignSelf: 'stretch', - bottom: BoxModel.margin, - flex: 1, - flexDirection: 'column', - left: 0, - position: 'absolute', - right: 0 + height: 90, + marginBottom: BoxModel.margin, + marginLeft: BoxModel.margin, + marginRight: BoxModel.margin }, /** - * The style of the content container of the ScrollView which is placed - * inside filmstrip and which contains the participants' thumbnails in order - * to allow scrolling through them if they do not fit within the display. + * The style of the wide version of the filmstrip which appears as a column + * on the short side of the screen. */ - filmstripScrollViewContentContainer: { - paddingHorizontal: BoxModel.padding + filmstripWide: { + ...filmstripBaseStyle, + bottom: BoxModel.margin, + left: BoxModel.margin, + position: 'absolute', + top: BoxModel.margin }, /** @@ -86,8 +96,7 @@ export default { borderWidth: 1, flex: 1, justifyContent: 'center', - marginLeft: 2, - marginRight: 2, + margin: 2, overflow: 'hidden', position: 'relative' }, diff --git a/react/features/toolbox/components/Toolbox.native.js b/react/features/toolbox/components/Toolbox.native.js index 2a916b74d..c420fc967 100644 --- a/react/features/toolbox/components/Toolbox.native.js +++ b/react/features/toolbox/components/Toolbox.native.js @@ -4,6 +4,7 @@ import { View } from 'react-native'; import { connect } from 'react-redux'; import { sendAnalyticsEvent } from '../../analytics'; +import { AspectRatioAware, isNarrowAspectRatio } from '../../base/aspect-ratio'; import { toggleAudioOnly } from '../../base/conference'; import { MEDIA_TYPE, @@ -119,15 +120,25 @@ class Toolbox extends Component { * @returns {ReactElement} */ render() { + if (!this.props._visible) { + return null; + } + return ( + style = { + isNarrowAspectRatio(this) + ? styles.toolbarContainerNarrow + : styles.toolbarContainerWide } > { - this._renderPrimaryToolbar() + isNarrowAspectRatio(this) + ? this._renderSecondaryToolbar() + : this._renderPrimaryToolbar() } { - this._renderSecondaryToolbar() + isNarrowAspectRatio(this) + ? this._renderPrimaryToolbar() + : this._renderSecondaryToolbar() } ); @@ -420,4 +431,5 @@ function _mapStateToProps(state) { }; } -export default connect(_mapStateToProps, _mapDispatchToProps)(Toolbox); +export default connect(_mapStateToProps, _mapDispatchToProps)( + AspectRatioAware(Toolbox)); diff --git a/react/features/toolbox/components/styles.js b/react/features/toolbox/components/styles.js index e1b0af6de..5f239da50 100644 --- a/react/features/toolbox/components/styles.js +++ b/react/features/toolbox/components/styles.js @@ -6,7 +6,7 @@ import { BoxModel, ColorPalette, createStyleSheet } from '../../base/styles'; * @type {Object} */ const _toolbar = { - flex: 1, + flex: 0, position: 'absolute' }; @@ -86,7 +86,7 @@ export default createStyleSheet({ */ primaryToolbar: { ..._toolbar, - bottom: 3 * BoxModel.margin, + bottom: 0, flexDirection: 'row', justifyContent: 'center', left: 0, @@ -135,9 +135,23 @@ export default createStyleSheet({ /** * The style of the root/top-level {@link Container} of {@link Toolbox} - * which contains {@link Toolbar}s. + * which contains {@link Toolbar}s. This is narrow layout version which + * spans from the top of the screen to the top of the filmstrip located at + * the bottom of the screen. */ - toolbarContainer: { + toolbarContainerNarrow: { + flexDirection: 'column', + flexGrow: 1 + }, + + /** + * The style of the root/top-level {@link Container} of {@link Toolbox} + * which contains {@link Toolbar}s. This is wide layout version which + * spans from the top to the bottom of the screen and is located to + * the right of the filmstrip which is displayed as a column on the left + * side of the screen. + */ + toolbarContainerWide: { bottom: 0, left: 0, position: 'absolute',