feat: display filmstrip on the short side

Adds the ability to detect app area's aspect ratio on react-native
through the features/base/aspect-ratio.

Makes conference, filmstrip and toolbox react to the aspect ratio
changes and display filmstrip on the shorter side of the screen.
This commit is contained in:
paweldomas 2017-10-13 11:13:46 -05:00 committed by Lyubo Marinov
parent 2b46c37077
commit c0a7d6144a
16 changed files with 349 additions and 49 deletions

View File

@ -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 (
<AspectRatioDetector>
{super._createElement(component, props)}
</AspectRatioDetector>);
}
/**
* Attempts to disable the use of React Native
* {@link ExceptionsManager#handleException} on platforms and in

View File

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

View File

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

View File

@ -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 <tt>aspectRatio</tt> 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 <WrapperComponent { ...this.props } />;
}
});
}
/**
* 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 <tt>aspectRation</tt> property.
* @returns {boolean}
*/
export function isNarrowAspectRatio(component: ReactClass<*>) {
return component.props.aspectRatio === ASPECT_RATIO_NARROW;
}

View File

@ -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 (
<View
onLayout = { this.props._onLayout }
style = { styles.aspectRatioDetectorStyle } >
{this.props.children}
</View>);
}
}
/**
* 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);

View File

@ -0,0 +1,2 @@
export * from './AspectRatioAware';
export { default as AspectRatioDetector } from './AspectRatioDetector';

View File

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

View File

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

View File

@ -0,0 +1,6 @@
export * from './actions';
export * from './actionTypes';
export * from './components';
export * from './constants';
import './reducer';

View File

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

View File

@ -184,15 +184,6 @@ class Conference extends Component {
*/}
<LargeVideo />
{/*
* 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.
*/}
<Filmstrip />
{/*
* The overlays need to be bellow the Toolbox so that the user
* may tap the ToolbarButtons.
@ -209,10 +200,22 @@ class Conference extends Component {
</View>
}
{/*
* The Toolbox is in a stacking layer above the Filmstrip.
*/}
<Toolbox />
<View style = { styles.toolboxAndFilmstripContainer } >
{/*
* The Toolbox is in a stacking layer above the Filmstrip.
*/}
<Toolbox />
{/*
* 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.
*/}
<Filmstrip />
</View>
{/*
* The dialogs are in the topmost stacking layers.

View File

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

View File

@ -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 (
<Container
style = { styles.filmstrip }
visible = { this.props._visible }>
style = { filmstripStyle }
visible = { this.props._visible } >
<ScrollView
contentContainerStyle
= { styles.filmstripScrollViewContentContainer }
horizontal = { true }
horizontal = { isNarrowAspectRatio(this) }
showsHorizontalScrollIndicator = { false }
showsVerticalScrollIndicator = { false }>
{
@ -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));

View File

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

View File

@ -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 (
<Container
style = { styles.toolbarContainer }
visible = { this.props._visible }>
style = {
isNarrowAspectRatio(this)
? styles.toolbarContainerNarrow
: styles.toolbarContainerWide } >
{
this._renderPrimaryToolbar()
isNarrowAspectRatio(this)
? this._renderSecondaryToolbar()
: this._renderPrimaryToolbar()
}
{
this._renderSecondaryToolbar()
isNarrowAspectRatio(this)
? this._renderPrimaryToolbar()
: this._renderSecondaryToolbar()
}
</Container>
);
@ -420,4 +431,5 @@ function _mapStateToProps(state) {
};
}
export default connect(_mapStateToProps, _mapDispatchToProps)(Toolbox);
export default connect(_mapStateToProps, _mapDispatchToProps)(
AspectRatioAware(Toolbox));

View File

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