[RN] Implement tile view
* feat(tile-view): initial implementation for mobile - Create a tile view component for displaying thumbnails in a two-dimensional grid. - Update the existing TileViewButton so it shows a label in the overflow menu. - Modify conference so it can display TileView while hiding Filmstrip. - Modify Thumbnail so its width/height can be set and to prevent pinning while in tile view mode. * use style array for thumbnail styles * change ternary to math.min for expressiveness * use dimensiondetector * pass explicit disableTint prop * use makeAspectRatioAware instead of aspectRatio prop * update docs * fix docs again (fix laziest copy/paste job I've ever done) * large-video: rename onPress prop to onClick * change forEach to for...of * use truthy check fallthrough logic instead of explicit if * put tile view button second to last in menu * move spacer to a constant * the magical incantation to make flow shut up
This commit is contained in:
parent
37ff77cd5b
commit
c25d6eb9a8
|
@ -1,54 +1,71 @@
|
||||||
/* @flow */
|
/* @flow */
|
||||||
|
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import React, { Component } from 'react';
|
import React, { Component } from 'react';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@code AbstractContainer} component's property types.
|
||||||
|
*/
|
||||||
|
export type Props = {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An optional accessibility label to apply to the container root.
|
||||||
|
*/
|
||||||
|
accessibilityLabel?: string,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether or not this element is an accessibility element.
|
||||||
|
*/
|
||||||
|
accessible?: boolean,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* React Elements to display within the component.
|
||||||
|
*/
|
||||||
|
children: React$Node | Object,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The event handler/listener to be invoked when this
|
||||||
|
* {@code AbstractContainer} is clicked on Web or pressed on React
|
||||||
|
* Native. If {@code onClick} is defined and {@link touchFeedback} is
|
||||||
|
* undefined, {@code touchFeedback} is considered defined as
|
||||||
|
* {@code true}.
|
||||||
|
*/
|
||||||
|
onClick?: ?Function,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The style (as in stylesheet) to be applied to this
|
||||||
|
* {@code AbstractContainer}.
|
||||||
|
*/
|
||||||
|
style?: Array<?string> | Object,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* If this instance is to provide visual feedback when touched, then
|
||||||
|
* {@code true}; otherwise, {@code false}. If {@code touchFeedback} is
|
||||||
|
* undefined and {@link onClick} is defined, {@code touchFeedback} is
|
||||||
|
* considered defined as {@code true}.
|
||||||
|
*/
|
||||||
|
touchFeedback?: ?Function,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Color to display when clicked.
|
||||||
|
*/
|
||||||
|
underlayColor?: string,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* If this {@code AbstractContainer} is to be visible, then {@code true}
|
||||||
|
* or {@code false} if this instance is to be hidden or not rendered at
|
||||||
|
* all.
|
||||||
|
*/
|
||||||
|
visible?: ?boolean
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Abstract (base) class for container of React {@link Component} children with
|
* Abstract (base) class for container of React {@link Component} children with
|
||||||
* a style.
|
* a style.
|
||||||
*
|
*
|
||||||
* @extends Component
|
* @extends Component
|
||||||
*/
|
*/
|
||||||
export default class AbstractContainer extends Component<*> {
|
export default class AbstractContainer<P: Props> extends Component<P> {
|
||||||
/**
|
|
||||||
* {@code AbstractContainer} component's property types.
|
|
||||||
*
|
|
||||||
* @static
|
|
||||||
*/
|
|
||||||
static propTypes = {
|
|
||||||
children: PropTypes.node,
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The event handler/listener to be invoked when this
|
|
||||||
* {@code AbstractContainer} is clicked on Web or pressed on React
|
|
||||||
* Native. If {@code onClick} is defined and {@link touchFeedback} is
|
|
||||||
* undefined, {@code touchFeedback} is considered defined as
|
|
||||||
* {@code true}.
|
|
||||||
*/
|
|
||||||
onClick: PropTypes.func,
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The style (as in stylesheet) to be applied to this
|
|
||||||
* {@code AbstractContainer}.
|
|
||||||
*/
|
|
||||||
style: PropTypes.object,
|
|
||||||
|
|
||||||
/**
|
|
||||||
* If this instance is to provide visual feedback when touched, then
|
|
||||||
* {@code true}; otherwise, {@code false}. If {@code touchFeedback} is
|
|
||||||
* undefined and {@link onClick} is defined, {@code touchFeedback} is
|
|
||||||
* considered defined as {@code true}.
|
|
||||||
*/
|
|
||||||
touchFeedback: PropTypes.bool,
|
|
||||||
|
|
||||||
/**
|
|
||||||
* If this {@code AbstractContainer} is to be visible, then {@code true}
|
|
||||||
* or {@code false} if this instance is to be hidden or not rendered at
|
|
||||||
* all.
|
|
||||||
*/
|
|
||||||
visible: PropTypes.bool
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Renders this {@code AbstractContainer} as a React {@code Component} of a
|
* Renders this {@code AbstractContainer} as a React {@code Component} of a
|
||||||
* specific type.
|
* specific type.
|
||||||
|
@ -61,7 +78,7 @@ export default class AbstractContainer extends Component<*> {
|
||||||
* @protected
|
* @protected
|
||||||
* @returns {ReactElement}
|
* @returns {ReactElement}
|
||||||
*/
|
*/
|
||||||
_render(type, props) {
|
_render(type, props?: P) {
|
||||||
const {
|
const {
|
||||||
children,
|
children,
|
||||||
|
|
||||||
|
|
|
@ -8,20 +8,14 @@ import {
|
||||||
} from 'react-native';
|
} from 'react-native';
|
||||||
|
|
||||||
import AbstractContainer from '../AbstractContainer';
|
import AbstractContainer from '../AbstractContainer';
|
||||||
|
import type { Props } from '../AbstractContainer';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Represents a container of React Native/mobile {@link Component} children.
|
* Represents a container of React Native/mobile {@link Component} children.
|
||||||
*
|
*
|
||||||
* @extends AbstractContainer
|
* @extends AbstractContainer
|
||||||
*/
|
*/
|
||||||
export default class Container extends AbstractContainer {
|
export default class Container<P: Props> extends AbstractContainer<P> {
|
||||||
/**
|
|
||||||
* {@code Container} component's property types.
|
|
||||||
*
|
|
||||||
* @static
|
|
||||||
*/
|
|
||||||
static propTypes = AbstractContainer.propTypes;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Implements React's {@link Component#render()}.
|
* Implements React's {@link Component#render()}.
|
||||||
*
|
*
|
||||||
|
|
|
@ -1,13 +1,14 @@
|
||||||
/* @flow */
|
/* @flow */
|
||||||
|
|
||||||
import AbstractContainer from '../AbstractContainer';
|
import AbstractContainer from '../AbstractContainer';
|
||||||
|
import type { Props } from '../AbstractContainer';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Represents a container of React/Web {@link Component} children with a style.
|
* Represents a container of React/Web {@link Component} children with a style.
|
||||||
*
|
*
|
||||||
* @extends AbstractContainer
|
* @extends AbstractContainer
|
||||||
*/
|
*/
|
||||||
export default class Container extends AbstractContainer {
|
export default class Container<P: Props> extends AbstractContainer<P> {
|
||||||
/**
|
/**
|
||||||
* {@code Container} component's property types.
|
* {@code Container} component's property types.
|
||||||
*
|
*
|
||||||
|
|
|
@ -17,12 +17,18 @@ import {
|
||||||
import { TestConnectionInfo } from '../../base/testing';
|
import { TestConnectionInfo } from '../../base/testing';
|
||||||
import { createDesiredLocalTracks } from '../../base/tracks';
|
import { createDesiredLocalTracks } from '../../base/tracks';
|
||||||
import { ConferenceNotification } from '../../calendar-sync';
|
import { ConferenceNotification } from '../../calendar-sync';
|
||||||
import { FILMSTRIP_SIZE, Filmstrip, isFilmstripVisible } from '../../filmstrip';
|
import {
|
||||||
|
FILMSTRIP_SIZE,
|
||||||
|
Filmstrip,
|
||||||
|
isFilmstripVisible,
|
||||||
|
TileView
|
||||||
|
} from '../../filmstrip';
|
||||||
import { LargeVideo } from '../../large-video';
|
import { LargeVideo } from '../../large-video';
|
||||||
import { CalleeInfoContainer } from '../../invite';
|
import { CalleeInfoContainer } from '../../invite';
|
||||||
import { NotificationsContainer } from '../../notifications';
|
import { NotificationsContainer } from '../../notifications';
|
||||||
import { Captions } from '../../subtitles';
|
import { Captions } from '../../subtitles';
|
||||||
import { setToolboxVisible, Toolbox } from '../../toolbox';
|
import { setToolboxVisible, Toolbox } from '../../toolbox';
|
||||||
|
import { shouldDisplayTileView } from '../../video-layout';
|
||||||
|
|
||||||
import styles from './styles';
|
import styles from './styles';
|
||||||
|
|
||||||
|
@ -115,6 +121,13 @@ type Props = {
|
||||||
*/
|
*/
|
||||||
_setToolboxVisible: Function,
|
_setToolboxVisible: Function,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether or not the layout should change to support tile view mode.
|
||||||
|
*
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
_shouldDisplayTileView: boolean,
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The indicator which determines whether the Toolbox is visible.
|
* The indicator which determines whether the Toolbox is visible.
|
||||||
*
|
*
|
||||||
|
@ -252,6 +265,12 @@ class Conference extends Component<Props> {
|
||||||
* @returns {ReactElement}
|
* @returns {ReactElement}
|
||||||
*/
|
*/
|
||||||
render() {
|
render() {
|
||||||
|
const {
|
||||||
|
_connecting,
|
||||||
|
_reducedUI,
|
||||||
|
_shouldDisplayTileView
|
||||||
|
} = this.props;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Container style = { styles.conference }>
|
<Container style = { styles.conference }>
|
||||||
<StatusBar
|
<StatusBar
|
||||||
|
@ -261,20 +280,23 @@ class Conference extends Component<Props> {
|
||||||
|
|
||||||
{/*
|
{/*
|
||||||
* The LargeVideo is the lowermost stacking layer.
|
* The LargeVideo is the lowermost stacking layer.
|
||||||
*/}
|
*/
|
||||||
<LargeVideo onPress = { this._onClick } />
|
_shouldDisplayTileView
|
||||||
|
? <TileView onClick = { this._onClick } />
|
||||||
|
: <LargeVideo onClick = { this._onClick } />
|
||||||
|
}
|
||||||
|
|
||||||
{/*
|
{/*
|
||||||
* If there is a ringing call, show the callee's info.
|
* If there is a ringing call, show the callee's info.
|
||||||
*/
|
*/
|
||||||
this.props._reducedUI || <CalleeInfoContainer />
|
_reducedUI || <CalleeInfoContainer />
|
||||||
}
|
}
|
||||||
|
|
||||||
{/*
|
{/*
|
||||||
* The activity/loading indicator goes above everything, except
|
* The activity/loading indicator goes above everything, except
|
||||||
* the toolbox/toolbars and the dialogs.
|
* the toolbox/toolbars and the dialogs.
|
||||||
*/
|
*/
|
||||||
this.props._connecting
|
_connecting
|
||||||
&& <TintedView>
|
&& <TintedView>
|
||||||
<LoadingIndicator />
|
<LoadingIndicator />
|
||||||
</TintedView>
|
</TintedView>
|
||||||
|
@ -304,8 +326,9 @@ class Conference extends Component<Props> {
|
||||||
* name and grouping stem from the fact that these two
|
* name and grouping stem from the fact that these two
|
||||||
* React Components depict the videos of the conference's
|
* React Components depict the videos of the conference's
|
||||||
* participants.
|
* participants.
|
||||||
*/}
|
*/
|
||||||
<Filmstrip />
|
_shouldDisplayTileView ? undefined : <Filmstrip />
|
||||||
|
}
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
<TestConnectionInfo />
|
<TestConnectionInfo />
|
||||||
|
@ -548,6 +571,14 @@ function _mapStateToProps(state) {
|
||||||
*/
|
*/
|
||||||
_room: room,
|
_room: room,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether or not the layout should change to support tile view mode.
|
||||||
|
*
|
||||||
|
* @private
|
||||||
|
* @type {boolean}
|
||||||
|
*/
|
||||||
|
_shouldDisplayTileView: shouldDisplayTileView(state),
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The indicator which determines whether the Toolbox is visible.
|
* The indicator which determines whether the Toolbox is visible.
|
||||||
*
|
*
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
import PropTypes from 'prop-types';
|
// @flow
|
||||||
|
|
||||||
import React, { Component } from 'react';
|
import React, { Component } from 'react';
|
||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
|
|
||||||
|
@ -18,31 +19,67 @@ import { AVATAR_SIZE } from '../styles';
|
||||||
import styles from './styles';
|
import styles from './styles';
|
||||||
import VideoMutedIndicator from './VideoMutedIndicator';
|
import VideoMutedIndicator from './VideoMutedIndicator';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Thumbnail component's property types.
|
||||||
|
*/
|
||||||
|
type Props = {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The Redux representation of the participant's audio track.
|
||||||
|
*/
|
||||||
|
_audioTrack: Object,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The Redux representation of the state "features/large-video".
|
||||||
|
*/
|
||||||
|
_largeVideo: Object,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The Redux representation of the participant's video track.
|
||||||
|
*/
|
||||||
|
_videoTrack: Object,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* If true, tapping on the thumbnail will not pin the participant to large
|
||||||
|
* video. By default tapping does pin the participant.
|
||||||
|
*/
|
||||||
|
disablePin?: boolean,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* If true, there will be no color overlay (tint) on the thumbnail
|
||||||
|
* indicating the participant associated with the thumbnail is displayed on
|
||||||
|
* large video. By default there will be a tint.
|
||||||
|
*/
|
||||||
|
disableTint?: boolean,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Invoked to trigger state changes in Redux.
|
||||||
|
*/
|
||||||
|
dispatch: Dispatch<*>,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The Redux representation of the participant to display.
|
||||||
|
*/
|
||||||
|
participant: Object,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Optional styling to add or override on the Thumbnail component root.
|
||||||
|
*/
|
||||||
|
styleOverrides?: Object
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* React component for video thumbnail.
|
* React component for video thumbnail.
|
||||||
*
|
*
|
||||||
* @extends Component
|
* @extends Component
|
||||||
*/
|
*/
|
||||||
class Thumbnail extends Component {
|
class Thumbnail extends Component<Props> {
|
||||||
/**
|
|
||||||
* Thumbnail component's property types.
|
|
||||||
*
|
|
||||||
* @static
|
|
||||||
*/
|
|
||||||
static propTypes = {
|
|
||||||
_audioTrack: PropTypes.object,
|
|
||||||
_largeVideo: PropTypes.object,
|
|
||||||
_videoTrack: PropTypes.object,
|
|
||||||
dispatch: PropTypes.func,
|
|
||||||
participant: PropTypes.object
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Initializes new Video Thumbnail component.
|
* Initializes new Video Thumbnail component.
|
||||||
*
|
*
|
||||||
* @param {Object} props - Component props.
|
* @param {Object} props - Component props.
|
||||||
*/
|
*/
|
||||||
constructor(props) {
|
constructor(props: Props) {
|
||||||
super(props);
|
super(props);
|
||||||
|
|
||||||
// Bind event handlers so they are only bound once for every instance.
|
// Bind event handlers so they are only bound once for every instance.
|
||||||
|
@ -56,19 +93,14 @@ class Thumbnail extends Component {
|
||||||
* @returns {ReactElement}
|
* @returns {ReactElement}
|
||||||
*/
|
*/
|
||||||
render() {
|
render() {
|
||||||
const audioTrack = this.props._audioTrack;
|
const {
|
||||||
const largeVideo = this.props._largeVideo;
|
_audioTrack: audioTrack,
|
||||||
const participant = this.props.participant;
|
_largeVideo: largeVideo,
|
||||||
const videoTrack = this.props._videoTrack;
|
_videoTrack: videoTrack,
|
||||||
|
disablePin,
|
||||||
let style = styles.thumbnail;
|
disableTint,
|
||||||
|
participant
|
||||||
if (participant.pinned) {
|
} = this.props;
|
||||||
style = {
|
|
||||||
...style,
|
|
||||||
...styles.thumbnailPinned
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
// We don't render audio in any of the following:
|
// We don't render audio in any of the following:
|
||||||
// 1. The audio (source) is muted. There's no practical reason (that we
|
// 1. The audio (source) is muted. There's no practical reason (that we
|
||||||
|
@ -85,8 +117,13 @@ class Thumbnail extends Component {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Container
|
<Container
|
||||||
onClick = { this._onClick }
|
onClick = { disablePin ? undefined : this._onClick }
|
||||||
style = { style }>
|
style = { [
|
||||||
|
styles.thumbnail,
|
||||||
|
participant.pinned && !disablePin
|
||||||
|
? styles.thumbnailPinned : null,
|
||||||
|
this.props.styleOverrides || null
|
||||||
|
] }>
|
||||||
|
|
||||||
{ renderAudio
|
{ renderAudio
|
||||||
&& <Audio
|
&& <Audio
|
||||||
|
@ -96,7 +133,7 @@ class Thumbnail extends Component {
|
||||||
<ParticipantView
|
<ParticipantView
|
||||||
avatarSize = { AVATAR_SIZE }
|
avatarSize = { AVATAR_SIZE }
|
||||||
participantId = { participantId }
|
participantId = { participantId }
|
||||||
tintEnabled = { participantInLargeVideo }
|
tintEnabled = { participantInLargeVideo && !disableTint }
|
||||||
zOrder = { 1 } />
|
zOrder = { 1 } />
|
||||||
|
|
||||||
{ participant.role === PARTICIPANT_ROLE.MODERATOR
|
{ participant.role === PARTICIPANT_ROLE.MODERATOR
|
||||||
|
@ -117,6 +154,8 @@ class Thumbnail extends Component {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_onClick: () => void;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Handles click/tap event on the thumbnail.
|
* Handles click/tap event on the thumbnail.
|
||||||
*
|
*
|
||||||
|
|
|
@ -0,0 +1,337 @@
|
||||||
|
// @flow
|
||||||
|
|
||||||
|
import React, { Component } from 'react';
|
||||||
|
import {
|
||||||
|
ScrollView,
|
||||||
|
TouchableWithoutFeedback,
|
||||||
|
View
|
||||||
|
} from 'react-native';
|
||||||
|
import { connect } from 'react-redux';
|
||||||
|
|
||||||
|
import {
|
||||||
|
getNearestReceiverVideoQualityLevel,
|
||||||
|
setMaxReceiverVideoQuality
|
||||||
|
} from '../../../base/conference';
|
||||||
|
import {
|
||||||
|
DimensionsDetector,
|
||||||
|
isNarrowAspectRatio,
|
||||||
|
makeAspectRatioAware
|
||||||
|
} from '../../../base/responsive-ui';
|
||||||
|
|
||||||
|
import Thumbnail from './Thumbnail';
|
||||||
|
import styles from './styles';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The type of the React {@link Component} props of {@link TileView}.
|
||||||
|
*/
|
||||||
|
type Props = {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The participants in the conference.
|
||||||
|
*/
|
||||||
|
_participants: Array<Object>,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Invoked to update the receiver video quality.
|
||||||
|
*/
|
||||||
|
dispatch: Dispatch<*>,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Callback to invoke when tile view is tapped.
|
||||||
|
*/
|
||||||
|
onClick: Function
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The type of the React {@link Component} state of {@link TileView}.
|
||||||
|
*/
|
||||||
|
type State = {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The available width for {@link TileView} to occupy.
|
||||||
|
*/
|
||||||
|
height: number,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The available height for {@link TileView} to occupy.
|
||||||
|
*/
|
||||||
|
width: number
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The margin for each side of the tile view. Taken away from the available
|
||||||
|
* height and width for the tile container to display in.
|
||||||
|
*
|
||||||
|
* @private
|
||||||
|
* @type {number}
|
||||||
|
*/
|
||||||
|
const MARGIN = 10;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The aspect ratio the tiles should display in.
|
||||||
|
*
|
||||||
|
* @private
|
||||||
|
* @type {number}
|
||||||
|
*/
|
||||||
|
const TILE_ASPECT_RATIO = 1;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Implements a React {@link Component} which displays thumbnails in a two
|
||||||
|
* dimensional grid.
|
||||||
|
*
|
||||||
|
* @extends Component
|
||||||
|
*/
|
||||||
|
class TileView extends Component<Props, State> {
|
||||||
|
state = {
|
||||||
|
height: 0,
|
||||||
|
width: 0
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initializes a new {@code TileView} instance.
|
||||||
|
*
|
||||||
|
* @param {Object} props - The read-only properties with which the new
|
||||||
|
* instance is to be initialized.
|
||||||
|
*/
|
||||||
|
constructor(props: Props) {
|
||||||
|
super(props);
|
||||||
|
|
||||||
|
// Bind event handler so it is only bound once per instance.
|
||||||
|
this._onDimensionsChanged = this._onDimensionsChanged.bind(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Implements React's {@link Component#componentDidMount}.
|
||||||
|
*
|
||||||
|
* @inheritdoc
|
||||||
|
*/
|
||||||
|
componentDidMount() {
|
||||||
|
this._updateReceiverQuality();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Implements React's {@link Component#componentDidUpdate}.
|
||||||
|
*
|
||||||
|
* @inheritdoc
|
||||||
|
*/
|
||||||
|
componentDidUpdate() {
|
||||||
|
this._updateReceiverQuality();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Implements React's {@link Component#render()}.
|
||||||
|
*
|
||||||
|
* @inheritdoc
|
||||||
|
* @returns {ReactElement}
|
||||||
|
*/
|
||||||
|
render() {
|
||||||
|
const { onClick } = this.props;
|
||||||
|
const { height, width } = this.state;
|
||||||
|
const rowElements = this._groupIntoRows(
|
||||||
|
this._renderThumbnails(), this._getColumnCount());
|
||||||
|
|
||||||
|
return (
|
||||||
|
<DimensionsDetector
|
||||||
|
onDimensionsChanged = { this._onDimensionsChanged }>
|
||||||
|
<ScrollView
|
||||||
|
style = {{
|
||||||
|
...styles.tileView,
|
||||||
|
height,
|
||||||
|
width
|
||||||
|
}}>
|
||||||
|
<TouchableWithoutFeedback onPress = { onClick }>
|
||||||
|
<View
|
||||||
|
style = {{
|
||||||
|
...styles.tileViewRows,
|
||||||
|
minHeight: height,
|
||||||
|
minWidth: width
|
||||||
|
}}>
|
||||||
|
{ rowElements }
|
||||||
|
</View>
|
||||||
|
</TouchableWithoutFeedback>
|
||||||
|
</ScrollView>
|
||||||
|
</DimensionsDetector>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns how many columns should be displayed for tile view.
|
||||||
|
*
|
||||||
|
* @returns {number}
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
_getColumnCount() {
|
||||||
|
const participantCount = this.props._participants.length;
|
||||||
|
|
||||||
|
// For narrow view, tiles should stack on top of each other for a lonely
|
||||||
|
// call and a 1:1 call. Otherwise tiles should be grouped into rows of
|
||||||
|
// two.
|
||||||
|
if (isNarrowAspectRatio(this)) {
|
||||||
|
return participantCount >= 3 ? 2 : 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (participantCount === 4) {
|
||||||
|
// In wide view, a four person call should display as a 2x2 grid.
|
||||||
|
return 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
return Math.min(3, participantCount);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns all participants with the local participant at the end.
|
||||||
|
*
|
||||||
|
* @private
|
||||||
|
* @returns {Participant[]}
|
||||||
|
*/
|
||||||
|
_getSortedParticipants() {
|
||||||
|
const participants = [];
|
||||||
|
let localParticipant;
|
||||||
|
|
||||||
|
for (const participant of this.props._participants) {
|
||||||
|
if (participant.local) {
|
||||||
|
localParticipant = participant;
|
||||||
|
} else {
|
||||||
|
participants.push(participant);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
localParticipant && participants.push(localParticipant);
|
||||||
|
|
||||||
|
return participants;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calculate the height and width for the tiles.
|
||||||
|
*
|
||||||
|
* @private
|
||||||
|
* @returns {Object}
|
||||||
|
*/
|
||||||
|
_getTileDimensions() {
|
||||||
|
const { _participants } = this.props;
|
||||||
|
const { height, width } = this.state;
|
||||||
|
const columns = this._getColumnCount();
|
||||||
|
const participantCount = _participants.length;
|
||||||
|
const heightToUse = height - (MARGIN * 2);
|
||||||
|
const widthToUse = width - (MARGIN * 2);
|
||||||
|
let tileWidth;
|
||||||
|
|
||||||
|
// If there is going to be at least two rows, ensure that at least two
|
||||||
|
// rows display fully on screen.
|
||||||
|
if (participantCount / columns > 1) {
|
||||||
|
tileWidth
|
||||||
|
= Math.min(widthToUse / columns, heightToUse / 2);
|
||||||
|
} else {
|
||||||
|
tileWidth = Math.min(widthToUse / columns, heightToUse);
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
height: tileWidth / TILE_ASPECT_RATIO,
|
||||||
|
width: tileWidth
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Splits a list of thumbnails into React Elements with a maximum of
|
||||||
|
* {@link rowLength} thumbnails in each.
|
||||||
|
*
|
||||||
|
* @param {Array} thumbnails - The list of thumbnails that should be split
|
||||||
|
* into separate row groupings.
|
||||||
|
* @param {number} rowLength - How many thumbnails should be in each row.
|
||||||
|
* @private
|
||||||
|
* @returns {ReactElement[]}
|
||||||
|
*/
|
||||||
|
_groupIntoRows(thumbnails, rowLength) {
|
||||||
|
const rowElements = [];
|
||||||
|
|
||||||
|
for (let i = 0; i < thumbnails.length; i++) {
|
||||||
|
if (i % rowLength === 0) {
|
||||||
|
const thumbnailsInRow
|
||||||
|
= thumbnails.slice(i, i + rowLength);
|
||||||
|
|
||||||
|
rowElements.push(
|
||||||
|
<View
|
||||||
|
key = { rowElements.length }
|
||||||
|
style = { styles.tileViewRow }>
|
||||||
|
{ thumbnailsInRow }
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return rowElements;
|
||||||
|
}
|
||||||
|
|
||||||
|
_onDimensionsChanged: (width: number, height: number) => void;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Updates the known available state for {@link TileView} to occupy.
|
||||||
|
*
|
||||||
|
* @param {number} width - The component's current width.
|
||||||
|
* @param {number} height - The component's current height.
|
||||||
|
* @private
|
||||||
|
* @returns {void}
|
||||||
|
*/
|
||||||
|
_onDimensionsChanged(width: number, height: number) {
|
||||||
|
this.setState({
|
||||||
|
height,
|
||||||
|
width
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates React Elements to display each participant in a thumbnail. Each
|
||||||
|
* tile will be.
|
||||||
|
*
|
||||||
|
* @private
|
||||||
|
* @returns {ReactElement[]}
|
||||||
|
*/
|
||||||
|
_renderThumbnails() {
|
||||||
|
const styleOverrides = {
|
||||||
|
aspectRatio: TILE_ASPECT_RATIO,
|
||||||
|
flex: 0,
|
||||||
|
height: this._getTileDimensions().height,
|
||||||
|
width: null
|
||||||
|
};
|
||||||
|
|
||||||
|
return this._getSortedParticipants()
|
||||||
|
.map(participant => (
|
||||||
|
<Thumbnail
|
||||||
|
disablePin = { true }
|
||||||
|
disableTint = { true }
|
||||||
|
key = { participant.id }
|
||||||
|
participant = { participant }
|
||||||
|
styleOverrides = { styleOverrides } />));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the receiver video quality based on the dimensions of the thumbnails
|
||||||
|
* that are displayed.
|
||||||
|
*
|
||||||
|
* @private
|
||||||
|
* @returns {void}
|
||||||
|
*/
|
||||||
|
_updateReceiverQuality() {
|
||||||
|
const { height } = this._getTileDimensions();
|
||||||
|
const qualityLevel = getNearestReceiverVideoQualityLevel(height);
|
||||||
|
|
||||||
|
this.props.dispatch(setMaxReceiverVideoQuality(qualityLevel));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Maps (parts of) the redux state to the associated {@code TileView}'s props.
|
||||||
|
*
|
||||||
|
* @param {Object} state - The redux state.
|
||||||
|
* @private
|
||||||
|
* @returns {{
|
||||||
|
* _participants: Participant[]
|
||||||
|
* }}
|
||||||
|
*/
|
||||||
|
function _mapStateToProps(state) {
|
||||||
|
return {
|
||||||
|
_participants: state['features/base/participants']
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export default connect(_mapStateToProps)(makeAspectRatioAware(TileView));
|
|
@ -1,2 +1,3 @@
|
||||||
export { default as Filmstrip } from './Filmstrip';
|
export { default as Filmstrip } from './Filmstrip';
|
||||||
|
export { default as TileView } from './TileView';
|
||||||
export { default as styles } from './styles';
|
export { default as styles } from './styles';
|
||||||
|
|
|
@ -145,5 +145,18 @@ export default {
|
||||||
width: 5
|
width: 5
|
||||||
},
|
},
|
||||||
shadowRadius: 5
|
shadowRadius: 5
|
||||||
|
},
|
||||||
|
|
||||||
|
tileView: {
|
||||||
|
alignSelf: 'center'
|
||||||
|
},
|
||||||
|
|
||||||
|
tileViewRows: {
|
||||||
|
justifyContent: 'center'
|
||||||
|
},
|
||||||
|
|
||||||
|
tileViewRow: {
|
||||||
|
flexDirection: 'row',
|
||||||
|
justifyContent: 'center'
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
|
@ -17,7 +17,7 @@ type Props = {
|
||||||
/**
|
/**
|
||||||
* Callback to invoke when the {@code LargeVideo} is clicked/pressed.
|
* Callback to invoke when the {@code LargeVideo} is clicked/pressed.
|
||||||
*/
|
*/
|
||||||
onPress: Function,
|
onClick: Function,
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The ID of the participant (to be) depicted by LargeVideo.
|
* The ID of the participant (to be) depicted by LargeVideo.
|
||||||
|
@ -114,8 +114,8 @@ class LargeVideo extends Component<Props, State> {
|
||||||
useConnectivityInfoLabel
|
useConnectivityInfoLabel
|
||||||
} = this.state;
|
} = this.state;
|
||||||
const {
|
const {
|
||||||
onPress,
|
_participantId,
|
||||||
_participantId
|
onClick
|
||||||
} = this.props;
|
} = this.props;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -123,7 +123,7 @@ class LargeVideo extends Component<Props, State> {
|
||||||
onDimensionsChanged = { this._onDimensionsChanged }>
|
onDimensionsChanged = { this._onDimensionsChanged }>
|
||||||
<ParticipantView
|
<ParticipantView
|
||||||
avatarSize = { avatarSize }
|
avatarSize = { avatarSize }
|
||||||
onPress = { onPress }
|
onPress = { onClick }
|
||||||
participantId = { _participantId }
|
participantId = { _participantId }
|
||||||
style = { styles.largeVideo }
|
style = { styles.largeVideo }
|
||||||
testHintId = 'org.jitsi.meet.LargeVideo'
|
testHintId = 'org.jitsi.meet.LargeVideo'
|
||||||
|
|
|
@ -9,6 +9,7 @@ import { PictureInPictureButton } from '../../../mobile/picture-in-picture';
|
||||||
import { LiveStreamButton, RecordButton } from '../../../recording';
|
import { LiveStreamButton, RecordButton } from '../../../recording';
|
||||||
import { RoomLockButton } from '../../../room-lock';
|
import { RoomLockButton } from '../../../room-lock';
|
||||||
import { ClosedCaptionButton } from '../../../subtitles';
|
import { ClosedCaptionButton } from '../../../subtitles';
|
||||||
|
import { TileViewButton } from '../../../video-layout';
|
||||||
|
|
||||||
import AudioOnlyButton from './AudioOnlyButton';
|
import AudioOnlyButton from './AudioOnlyButton';
|
||||||
import { overflowMenuItemStyles } from './styles';
|
import { overflowMenuItemStyles } from './styles';
|
||||||
|
@ -73,6 +74,7 @@ class OverflowMenu extends Component<Props> {
|
||||||
<ClosedCaptionButton { ...buttonProps } />
|
<ClosedCaptionButton { ...buttonProps } />
|
||||||
<RecordButton { ...buttonProps } />
|
<RecordButton { ...buttonProps } />
|
||||||
<LiveStreamButton { ...buttonProps } />
|
<LiveStreamButton { ...buttonProps } />
|
||||||
|
<TileViewButton { ...buttonProps } />
|
||||||
<PictureInPictureButton { ...buttonProps } />
|
<PictureInPictureButton { ...buttonProps } />
|
||||||
</BottomSheet>
|
</BottomSheet>
|
||||||
);
|
);
|
||||||
|
|
|
@ -38,6 +38,7 @@ type Props = AbstractButtonProps & {
|
||||||
class TileViewButton<P: Props> extends AbstractButton<P, *> {
|
class TileViewButton<P: Props> extends AbstractButton<P, *> {
|
||||||
accessibilityLabel = 'toolbar.accessibilityLabel.tileView';
|
accessibilityLabel = 'toolbar.accessibilityLabel.tileView';
|
||||||
iconName = 'icon-tiles-many';
|
iconName = 'icon-tiles-many';
|
||||||
|
label = 'toolbar.tileViewToggle';
|
||||||
toggledIconName = 'icon-tiles-many toggled';
|
toggledIconName = 'icon-tiles-many toggled';
|
||||||
tooltip = 'toolbar.tileViewToggle';
|
tooltip = 'toolbar.tileViewToggle';
|
||||||
|
|
||||||
|
|
|
@ -74,7 +74,8 @@ export function shouldDisplayTileView(state: Object = {}) {
|
||||||
return Boolean(
|
return Boolean(
|
||||||
state['features/video-layout']
|
state['features/video-layout']
|
||||||
&& state['features/video-layout'].tileViewEnabled
|
&& state['features/video-layout'].tileViewEnabled
|
||||||
&& !state['features/etherpad'].editing
|
&& (!state['features/etherpad']
|
||||||
|
|| !state['features/etherpad'].editing)
|
||||||
|
|
||||||
// Truthy check is needed for interfaceConfig to prevent errors on
|
// Truthy check is needed for interfaceConfig to prevent errors on
|
||||||
// mobile which does not have interfaceConfig. On web, tile view
|
// mobile which does not have interfaceConfig. On web, tile view
|
||||||
|
|
Loading…
Reference in New Issue