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',