[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 */
|
||||
|
||||
import PropTypes from 'prop-types';
|
||||
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
|
||||
* a style.
|
||||
*
|
||||
* @extends Component
|
||||
*/
|
||||
export default class AbstractContainer extends Component<*> {
|
||||
/**
|
||||
* {@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
|
||||
};
|
||||
|
||||
export default class AbstractContainer<P: Props> extends Component<P> {
|
||||
/**
|
||||
* Renders this {@code AbstractContainer} as a React {@code Component} of a
|
||||
* specific type.
|
||||
|
@ -61,7 +78,7 @@ export default class AbstractContainer extends Component<*> {
|
|||
* @protected
|
||||
* @returns {ReactElement}
|
||||
*/
|
||||
_render(type, props) {
|
||||
_render(type, props?: P) {
|
||||
const {
|
||||
children,
|
||||
|
||||
|
|
|
@ -8,20 +8,14 @@ import {
|
|||
} from 'react-native';
|
||||
|
||||
import AbstractContainer from '../AbstractContainer';
|
||||
import type { Props } from '../AbstractContainer';
|
||||
|
||||
/**
|
||||
* Represents a container of React Native/mobile {@link Component} children.
|
||||
*
|
||||
* @extends AbstractContainer
|
||||
*/
|
||||
export default class Container extends AbstractContainer {
|
||||
/**
|
||||
* {@code Container} component's property types.
|
||||
*
|
||||
* @static
|
||||
*/
|
||||
static propTypes = AbstractContainer.propTypes;
|
||||
|
||||
export default class Container<P: Props> extends AbstractContainer<P> {
|
||||
/**
|
||||
* Implements React's {@link Component#render()}.
|
||||
*
|
||||
|
|
|
@ -1,13 +1,14 @@
|
|||
/* @flow */
|
||||
|
||||
import AbstractContainer from '../AbstractContainer';
|
||||
import type { Props } from '../AbstractContainer';
|
||||
|
||||
/**
|
||||
* Represents a container of React/Web {@link Component} children with a style.
|
||||
*
|
||||
* @extends AbstractContainer
|
||||
*/
|
||||
export default class Container extends AbstractContainer {
|
||||
export default class Container<P: Props> extends AbstractContainer<P> {
|
||||
/**
|
||||
* {@code Container} component's property types.
|
||||
*
|
||||
|
|
|
@ -17,12 +17,18 @@ import {
|
|||
import { TestConnectionInfo } from '../../base/testing';
|
||||
import { createDesiredLocalTracks } from '../../base/tracks';
|
||||
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 { CalleeInfoContainer } from '../../invite';
|
||||
import { NotificationsContainer } from '../../notifications';
|
||||
import { Captions } from '../../subtitles';
|
||||
import { setToolboxVisible, Toolbox } from '../../toolbox';
|
||||
import { shouldDisplayTileView } from '../../video-layout';
|
||||
|
||||
import styles from './styles';
|
||||
|
||||
|
@ -115,6 +121,13 @@ type Props = {
|
|||
*/
|
||||
_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.
|
||||
*
|
||||
|
@ -252,6 +265,12 @@ class Conference extends Component<Props> {
|
|||
* @returns {ReactElement}
|
||||
*/
|
||||
render() {
|
||||
const {
|
||||
_connecting,
|
||||
_reducedUI,
|
||||
_shouldDisplayTileView
|
||||
} = this.props;
|
||||
|
||||
return (
|
||||
<Container style = { styles.conference }>
|
||||
<StatusBar
|
||||
|
@ -261,20 +280,23 @@ class Conference extends Component<Props> {
|
|||
|
||||
{/*
|
||||
* 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.
|
||||
*/
|
||||
this.props._reducedUI || <CalleeInfoContainer />
|
||||
_reducedUI || <CalleeInfoContainer />
|
||||
}
|
||||
|
||||
{/*
|
||||
* The activity/loading indicator goes above everything, except
|
||||
* the toolbox/toolbars and the dialogs.
|
||||
*/
|
||||
this.props._connecting
|
||||
_connecting
|
||||
&& <TintedView>
|
||||
<LoadingIndicator />
|
||||
</TintedView>
|
||||
|
@ -304,8 +326,9 @@ class Conference extends Component<Props> {
|
|||
* name and grouping stem from the fact that these two
|
||||
* React Components depict the videos of the conference's
|
||||
* participants.
|
||||
*/}
|
||||
<Filmstrip />
|
||||
*/
|
||||
_shouldDisplayTileView ? undefined : <Filmstrip />
|
||||
}
|
||||
</View>
|
||||
|
||||
<TestConnectionInfo />
|
||||
|
@ -548,6 +571,14 @@ function _mapStateToProps(state) {
|
|||
*/
|
||||
_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.
|
||||
*
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import PropTypes from 'prop-types';
|
||||
// @flow
|
||||
|
||||
import React, { Component } from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
|
@ -18,31 +19,67 @@ import { AVATAR_SIZE } from '../styles';
|
|||
import styles from './styles';
|
||||
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.
|
||||
*
|
||||
* @extends Component
|
||||
*/
|
||||
class Thumbnail extends Component {
|
||||
/**
|
||||
* Thumbnail component's property types.
|
||||
*
|
||||
* @static
|
||||
*/
|
||||
static propTypes = {
|
||||
_audioTrack: PropTypes.object,
|
||||
_largeVideo: PropTypes.object,
|
||||
_videoTrack: PropTypes.object,
|
||||
dispatch: PropTypes.func,
|
||||
participant: PropTypes.object
|
||||
};
|
||||
|
||||
class Thumbnail extends Component<Props> {
|
||||
/**
|
||||
* Initializes new Video Thumbnail component.
|
||||
*
|
||||
* @param {Object} props - Component props.
|
||||
*/
|
||||
constructor(props) {
|
||||
constructor(props: Props) {
|
||||
super(props);
|
||||
|
||||
// Bind event handlers so they are only bound once for every instance.
|
||||
|
@ -56,19 +93,14 @@ class Thumbnail extends Component {
|
|||
* @returns {ReactElement}
|
||||
*/
|
||||
render() {
|
||||
const audioTrack = this.props._audioTrack;
|
||||
const largeVideo = this.props._largeVideo;
|
||||
const participant = this.props.participant;
|
||||
const videoTrack = this.props._videoTrack;
|
||||
|
||||
let style = styles.thumbnail;
|
||||
|
||||
if (participant.pinned) {
|
||||
style = {
|
||||
...style,
|
||||
...styles.thumbnailPinned
|
||||
};
|
||||
}
|
||||
const {
|
||||
_audioTrack: audioTrack,
|
||||
_largeVideo: largeVideo,
|
||||
_videoTrack: videoTrack,
|
||||
disablePin,
|
||||
disableTint,
|
||||
participant
|
||||
} = this.props;
|
||||
|
||||
// We don't render audio in any of the following:
|
||||
// 1. The audio (source) is muted. There's no practical reason (that we
|
||||
|
@ -85,8 +117,13 @@ class Thumbnail extends Component {
|
|||
|
||||
return (
|
||||
<Container
|
||||
onClick = { this._onClick }
|
||||
style = { style }>
|
||||
onClick = { disablePin ? undefined : this._onClick }
|
||||
style = { [
|
||||
styles.thumbnail,
|
||||
participant.pinned && !disablePin
|
||||
? styles.thumbnailPinned : null,
|
||||
this.props.styleOverrides || null
|
||||
] }>
|
||||
|
||||
{ renderAudio
|
||||
&& <Audio
|
||||
|
@ -96,7 +133,7 @@ class Thumbnail extends Component {
|
|||
<ParticipantView
|
||||
avatarSize = { AVATAR_SIZE }
|
||||
participantId = { participantId }
|
||||
tintEnabled = { participantInLargeVideo }
|
||||
tintEnabled = { participantInLargeVideo && !disableTint }
|
||||
zOrder = { 1 } />
|
||||
|
||||
{ participant.role === PARTICIPANT_ROLE.MODERATOR
|
||||
|
@ -117,6 +154,8 @@ class Thumbnail extends Component {
|
|||
);
|
||||
}
|
||||
|
||||
_onClick: () => void;
|
||||
|
||||
/**
|
||||
* 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 TileView } from './TileView';
|
||||
export { default as styles } from './styles';
|
||||
|
|
|
@ -145,5 +145,18 @@ export default {
|
|||
width: 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.
|
||||
*/
|
||||
onPress: Function,
|
||||
onClick: Function,
|
||||
|
||||
/**
|
||||
* The ID of the participant (to be) depicted by LargeVideo.
|
||||
|
@ -114,8 +114,8 @@ class LargeVideo extends Component<Props, State> {
|
|||
useConnectivityInfoLabel
|
||||
} = this.state;
|
||||
const {
|
||||
onPress,
|
||||
_participantId
|
||||
_participantId,
|
||||
onClick
|
||||
} = this.props;
|
||||
|
||||
return (
|
||||
|
@ -123,7 +123,7 @@ class LargeVideo extends Component<Props, State> {
|
|||
onDimensionsChanged = { this._onDimensionsChanged }>
|
||||
<ParticipantView
|
||||
avatarSize = { avatarSize }
|
||||
onPress = { onPress }
|
||||
onPress = { onClick }
|
||||
participantId = { _participantId }
|
||||
style = { styles.largeVideo }
|
||||
testHintId = 'org.jitsi.meet.LargeVideo'
|
||||
|
|
|
@ -9,6 +9,7 @@ import { PictureInPictureButton } from '../../../mobile/picture-in-picture';
|
|||
import { LiveStreamButton, RecordButton } from '../../../recording';
|
||||
import { RoomLockButton } from '../../../room-lock';
|
||||
import { ClosedCaptionButton } from '../../../subtitles';
|
||||
import { TileViewButton } from '../../../video-layout';
|
||||
|
||||
import AudioOnlyButton from './AudioOnlyButton';
|
||||
import { overflowMenuItemStyles } from './styles';
|
||||
|
@ -73,6 +74,7 @@ class OverflowMenu extends Component<Props> {
|
|||
<ClosedCaptionButton { ...buttonProps } />
|
||||
<RecordButton { ...buttonProps } />
|
||||
<LiveStreamButton { ...buttonProps } />
|
||||
<TileViewButton { ...buttonProps } />
|
||||
<PictureInPictureButton { ...buttonProps } />
|
||||
</BottomSheet>
|
||||
);
|
||||
|
|
|
@ -38,6 +38,7 @@ type Props = AbstractButtonProps & {
|
|||
class TileViewButton<P: Props> extends AbstractButton<P, *> {
|
||||
accessibilityLabel = 'toolbar.accessibilityLabel.tileView';
|
||||
iconName = 'icon-tiles-many';
|
||||
label = 'toolbar.tileViewToggle';
|
||||
toggledIconName = 'icon-tiles-many toggled';
|
||||
tooltip = 'toolbar.tileViewToggle';
|
||||
|
||||
|
|
|
@ -74,7 +74,8 @@ export function shouldDisplayTileView(state: Object = {}) {
|
|||
return Boolean(
|
||||
state['features/video-layout']
|
||||
&& 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
|
||||
// mobile which does not have interfaceConfig. On web, tile view
|
||||
|
|
Loading…
Reference in New Issue