feat(filmstrip-pagination): mobile support.
This commit is contained in:
parent
37acce3764
commit
7dd43d93b6
|
@ -551,7 +551,7 @@ function _updateLocalParticipantInConference({ dispatch, getState }, next, actio
|
|||
|
||||
const localParticipant = getLocalParticipant(getState);
|
||||
|
||||
if (conference && participant.id === localParticipant.id) {
|
||||
if (conference && participant.id === localParticipant?.id) {
|
||||
if ('name' in participant) {
|
||||
conference.setDisplayName(participant.name);
|
||||
}
|
||||
|
|
|
@ -29,12 +29,13 @@ const REDUCED_UI_THRESHOLD = 300;
|
|||
*/
|
||||
export function clientResized(clientWidth: number, clientHeight: number) {
|
||||
return (dispatch: Dispatch<any>, getState: Function) => {
|
||||
const state = getState();
|
||||
const { isOpen: isChatOpen } = state['features/chat'];
|
||||
const isParticipantsPaneOpen = getParticipantsPaneOpen(state);
|
||||
let availableWidth = clientWidth;
|
||||
|
||||
if (navigator.product !== 'ReactNative') {
|
||||
const state = getState();
|
||||
const { isOpen: isChatOpen } = state['features/chat'];
|
||||
const isParticipantsPaneOpen = getParticipantsPaneOpen(state);
|
||||
|
||||
if (isChatOpen) {
|
||||
availableWidth -= CHAT_SIZE;
|
||||
}
|
||||
|
|
|
@ -1,6 +1,11 @@
|
|||
// @flow
|
||||
|
||||
import { SET_FILMSTRIP_ENABLED, SET_FILMSTRIP_VISIBLE, SET_REMOTE_PARTICIPANTS } from './actionTypes';
|
||||
import {
|
||||
SET_FILMSTRIP_ENABLED,
|
||||
SET_FILMSTRIP_VISIBLE,
|
||||
SET_REMOTE_PARTICIPANTS,
|
||||
SET_VISIBLE_REMOTE_PARTICIPANTS
|
||||
} from './actionTypes';
|
||||
|
||||
/**
|
||||
* Sets whether the filmstrip is enabled.
|
||||
|
@ -50,3 +55,23 @@ export function setRemoteParticipants(participants: Array<string>) {
|
|||
participants
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the list of the visible participants in the filmstrip by storing the start and end index from the remote
|
||||
* participants array.
|
||||
*
|
||||
* @param {number} startIndex - The start index from the remote participants array.
|
||||
* @param {number} endIndex - The end index from the remote participants array.
|
||||
* @returns {{
|
||||
* type: SET_VISIBLE_REMOTE_PARTICIPANTS,
|
||||
* startIndex: number,
|
||||
* endIndex: number
|
||||
* }}
|
||||
*/
|
||||
export function setVisibleRemoteParticipants(startIndex: number, endIndex: number) {
|
||||
return {
|
||||
type: SET_VISIBLE_REMOTE_PARTICIPANTS,
|
||||
startIndex,
|
||||
endIndex
|
||||
};
|
||||
}
|
||||
|
|
|
@ -1,6 +1,10 @@
|
|||
// @flow
|
||||
|
||||
import { getParticipantCountWithFake } from '../base/participants';
|
||||
|
||||
import { SET_TILE_VIEW_DIMENSIONS } from './actionTypes';
|
||||
import { SQUARE_TILE_ASPECT_RATIO, TILE_MARGIN } from './constants';
|
||||
import { getColumnCount } from './functions.native';
|
||||
|
||||
export * from './actions.any';
|
||||
|
||||
|
@ -9,20 +13,40 @@ export * from './actions.any';
|
|||
* of the values are currently used. Check the description of {@link SET_TILE_VIEW_DIMENSIONS} for the full set
|
||||
* of properties.
|
||||
*
|
||||
* @param {Object} dimensions - The tile view dimensions.
|
||||
* @param {Object} thumbnailSize - The size of an individual video thumbnail.
|
||||
* @param {number} thumbnailSize.height - The height of an individual video thumbnail.
|
||||
* @param {number} thumbnailSize.width - The width of an individual video thumbnail.
|
||||
* @returns {{
|
||||
* type: SET_TILE_VIEW_DIMENSIONS,
|
||||
* dimensions: Object
|
||||
* }}
|
||||
* @returns {Function}
|
||||
*/
|
||||
export function setTileViewDimensions({ thumbnailSize }: Object) {
|
||||
return {
|
||||
type: SET_TILE_VIEW_DIMENSIONS,
|
||||
dimensions: {
|
||||
thumbnailSize
|
||||
export function setTileViewDimensions() {
|
||||
return (dispatch: Function, getState: Function) => {
|
||||
const state = getState();
|
||||
const participantCount = getParticipantCountWithFake(state);
|
||||
const { clientHeight: height, clientWidth: width } = state['features/base/responsive-ui'];
|
||||
const columns = getColumnCount(state);
|
||||
const heightToUse = height - (TILE_MARGIN * 2);
|
||||
const widthToUse = width - (TILE_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);
|
||||
}
|
||||
|
||||
const tileHeight = Math.floor(tileWidth / SQUARE_TILE_ASPECT_RATIO);
|
||||
|
||||
tileWidth = Math.floor(tileWidth);
|
||||
|
||||
|
||||
dispatch({
|
||||
type: SET_TILE_VIEW_DIMENSIONS,
|
||||
dimensions: {
|
||||
columns,
|
||||
thumbnailSize: {
|
||||
height: tileHeight,
|
||||
width: tileWidth
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
}
|
||||
|
|
|
@ -7,7 +7,6 @@ import {
|
|||
SET_HORIZONTAL_VIEW_DIMENSIONS,
|
||||
SET_TILE_VIEW_DIMENSIONS,
|
||||
SET_VERTICAL_VIEW_DIMENSIONS,
|
||||
SET_VISIBLE_REMOTE_PARTICIPANTS,
|
||||
SET_VOLUME
|
||||
} from './actionTypes';
|
||||
import {
|
||||
|
@ -159,23 +158,3 @@ export function setVolume(participantId: string, volume: number) {
|
|||
volume
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the list of the visible participants in the filmstrip by storing the start and end index from the remote
|
||||
* participants array.
|
||||
*
|
||||
* @param {number} startIndex - The start index from the remote participants array.
|
||||
* @param {number} endIndex - The end index from the remote participants array.
|
||||
* @returns {{
|
||||
* type: SET_VISIBLE_REMOTE_PARTICIPANTS,
|
||||
* startIndex: number,
|
||||
* endIndex: number
|
||||
* }}
|
||||
*/
|
||||
export function setVisibleRemoteParticipants(startIndex: number, endIndex: number) {
|
||||
return {
|
||||
type: SET_VISIBLE_REMOTE_PARTICIPANTS,
|
||||
startIndex,
|
||||
endIndex
|
||||
};
|
||||
}
|
||||
|
|
|
@ -1,11 +1,13 @@
|
|||
// @flow
|
||||
|
||||
import React, { Component } from 'react';
|
||||
import { SafeAreaView, ScrollView } from 'react-native';
|
||||
import React, { PureComponent } from 'react';
|
||||
import { FlatList, SafeAreaView } from 'react-native';
|
||||
|
||||
import { getLocalParticipant } from '../../../base/participants';
|
||||
import { Platform } from '../../../base/react';
|
||||
import { connect } from '../../../base/redux';
|
||||
import { ASPECT_RATIO_NARROW } from '../../../base/responsive-ui/constants';
|
||||
import { setVisibleRemoteParticipants } from '../../actions';
|
||||
import { isFilmstripVisible, shouldRemoteVideosBeVisible } from '../../functions';
|
||||
|
||||
import LocalThumbnail from './LocalThumbnail';
|
||||
|
@ -25,6 +27,12 @@ type Props = {
|
|||
*/
|
||||
_aspectRatio: Symbol,
|
||||
|
||||
_clientWidth: number,
|
||||
|
||||
_clientHeight: number,
|
||||
|
||||
_localParticipantId: string,
|
||||
|
||||
/**
|
||||
* The participants in the conference.
|
||||
*/
|
||||
|
@ -33,7 +41,12 @@ type Props = {
|
|||
/**
|
||||
* The indicator which determines whether the filmstrip is visible.
|
||||
*/
|
||||
_visible: boolean
|
||||
_visible: boolean,
|
||||
|
||||
/**
|
||||
* Invoked to trigger state changes in Redux.
|
||||
*/
|
||||
dispatch: Function,
|
||||
};
|
||||
|
||||
/**
|
||||
|
@ -42,13 +55,18 @@ type Props = {
|
|||
*
|
||||
* @extends Component
|
||||
*/
|
||||
class Filmstrip extends Component<Props> {
|
||||
class Filmstrip extends PureComponent<Props> {
|
||||
/**
|
||||
* Whether the local participant should be rendered separately from the
|
||||
* remote participants i.e. outside of their {@link ScrollView}.
|
||||
*/
|
||||
_separateLocalThumbnail: boolean;
|
||||
|
||||
/**
|
||||
* The FlatList's viewabilityConfig.
|
||||
*/
|
||||
_viewabilityConfig: Object;
|
||||
|
||||
/**
|
||||
* Constructor of the component.
|
||||
*
|
||||
|
@ -75,6 +93,107 @@ class Filmstrip extends Component<Props> {
|
|||
// do not have much of a choice but to continue rendering LocalThumbnail
|
||||
// as any other remote Thumbnail on Android.
|
||||
this._separateLocalThumbnail = Platform.OS !== 'android';
|
||||
|
||||
this._viewabilityConfig = {
|
||||
itemVisiblePercentThreshold: 30
|
||||
};
|
||||
this._keyExtractor = this._keyExtractor.bind(this);
|
||||
this._getItemLayout = this._getItemLayout.bind(this);
|
||||
this._onViewableItemsChanged = this._onViewableItemsChanged.bind(this);
|
||||
this._renderThumbnail = this._renderThumbnail.bind(this);
|
||||
}
|
||||
|
||||
_keyExtractor: string => string;
|
||||
|
||||
/**
|
||||
* Returns a key for a passed item of the list.
|
||||
*
|
||||
* @param {string} item - The user ID.
|
||||
* @returns {string} - The user ID.
|
||||
*/
|
||||
_keyExtractor(item) {
|
||||
return item;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates the width and height of the filmstrip based on the screen size and aspect ratio.
|
||||
*
|
||||
* @returns {Object} - The width and the height.
|
||||
*/
|
||||
_getDimensions() {
|
||||
const { _aspectRatio, _clientWidth, _clientHeight } = this.props;
|
||||
const { height, width, margin } = styles.thumbnail;
|
||||
|
||||
if (_aspectRatio === ASPECT_RATIO_NARROW) {
|
||||
return {
|
||||
height,
|
||||
width: this._separateLocalThumbnail ? _clientWidth - width - (margin * 2) : _clientWidth
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
height: this._separateLocalThumbnail ? _clientHeight - height - (margin * 2) : _clientHeight,
|
||||
width
|
||||
};
|
||||
}
|
||||
|
||||
_getItemLayout: (?Array<string>, number) => {length: number, offset: number, index: number};
|
||||
|
||||
/**
|
||||
* Optimization for FlatList. Returns the length, offset and index for an item.
|
||||
*
|
||||
* @param {Array<string>} data - The data array with user IDs.
|
||||
* @param {number} index - The index number of the item.
|
||||
* @returns {Object}
|
||||
*/
|
||||
_getItemLayout(data, index) {
|
||||
const { _aspectRatio } = this.props;
|
||||
const isNarrowAspectRatio = _aspectRatio === ASPECT_RATIO_NARROW;
|
||||
const length = isNarrowAspectRatio ? styles.thumbnail.width : styles.thumbnail.height;
|
||||
|
||||
return {
|
||||
length,
|
||||
offset: length * index,
|
||||
index
|
||||
};
|
||||
}
|
||||
|
||||
_onViewableItemsChanged: Object => void;
|
||||
|
||||
/**
|
||||
* A handler for visible items changes.
|
||||
*
|
||||
* @param {Object} data - The visible items data.
|
||||
* @param {Array<Object>} data.viewableItems - The visible items array.
|
||||
* @returns {void}
|
||||
*/
|
||||
_onViewableItemsChanged({ viewableItems = [] }) {
|
||||
const indexArray: Array<number> = viewableItems.map(i => i.index);
|
||||
|
||||
// If the local video placed at the beginning we need to shift the start index of the remoteParticipants array
|
||||
// with 1 because and in the same time we don't need to adjust the end index because the end index will not be
|
||||
// included.
|
||||
const startIndex
|
||||
= this._separateLocalThumbnail ? Math.min(...indexArray) : Math.max(Math.min(...indexArray) - 1, 0);
|
||||
const endIndex = Math.max(...indexArray) + (this._separateLocalThumbnail ? 1 : 0);
|
||||
|
||||
this.props.dispatch(setVisibleRemoteParticipants(startIndex, endIndex));
|
||||
}
|
||||
|
||||
_renderThumbnail: Object => Object;
|
||||
|
||||
/**
|
||||
* Creates React Element to display each participant in a thumbnail.
|
||||
*
|
||||
* @private
|
||||
* @returns {ReactElement}
|
||||
*/
|
||||
_renderThumbnail({ item /* , index , separators */ }) {
|
||||
return (
|
||||
<Thumbnail
|
||||
key = { item }
|
||||
participantID = { item } />)
|
||||
;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -84,7 +203,7 @@ class Filmstrip extends Component<Props> {
|
|||
* @returns {ReactElement}
|
||||
*/
|
||||
render() {
|
||||
const { _aspectRatio, _participants, _visible } = this.props;
|
||||
const { _aspectRatio, _localParticipantId, _participants, _visible } = this.props;
|
||||
|
||||
if (!_visible) {
|
||||
return null;
|
||||
|
@ -92,6 +211,13 @@ class Filmstrip extends Component<Props> {
|
|||
|
||||
const isNarrowAspectRatio = _aspectRatio === ASPECT_RATIO_NARROW;
|
||||
const filmstripStyle = isNarrowAspectRatio ? styles.filmstripNarrow : styles.filmstripWide;
|
||||
const { height, width } = this._getDimensions();
|
||||
const { height: thumbnailHeight, width: thumbnailWidth, margin } = styles.thumbnail;
|
||||
const initialNumToRender = Math.ceil(isNarrowAspectRatio
|
||||
? width / (thumbnailWidth + (2 * margin))
|
||||
: height / (thumbnailHeight + (2 * margin))
|
||||
);
|
||||
const participants = this._separateLocalThumbnail ? _participants : [ _localParticipantId, ..._participants ];
|
||||
|
||||
return (
|
||||
<SafeAreaView style = { filmstripStyle }>
|
||||
|
@ -100,29 +226,20 @@ class Filmstrip extends Component<Props> {
|
|||
&& !isNarrowAspectRatio
|
||||
&& <LocalThumbnail />
|
||||
}
|
||||
<ScrollView
|
||||
<FlatList
|
||||
data = { participants }
|
||||
getItemLayout = { this._getItemLayout }
|
||||
horizontal = { isNarrowAspectRatio }
|
||||
initialNumToRender = { initialNumToRender }
|
||||
key = { isNarrowAspectRatio ? 'narrow' : 'wide' }
|
||||
keyExtractor = { this._keyExtractor }
|
||||
onViewableItemsChanged = { this._onViewableItemsChanged }
|
||||
renderItem = { this._renderThumbnail }
|
||||
showsHorizontalScrollIndicator = { false }
|
||||
showsVerticalScrollIndicator = { false }
|
||||
style = { styles.scrollView } >
|
||||
{
|
||||
!this._separateLocalThumbnail && !isNarrowAspectRatio
|
||||
&& <LocalThumbnail />
|
||||
}
|
||||
{
|
||||
|
||||
this._sort(_participants, isNarrowAspectRatio)
|
||||
.map(id => (
|
||||
<Thumbnail
|
||||
key = { id }
|
||||
participantID = { id } />))
|
||||
|
||||
}
|
||||
{
|
||||
!this._separateLocalThumbnail && isNarrowAspectRatio
|
||||
&& <LocalThumbnail />
|
||||
}
|
||||
</ScrollView>
|
||||
style = { styles.scrollView }
|
||||
viewabilityConfig = { this._viewabilityConfig }
|
||||
windowSize = { 2 } />
|
||||
{
|
||||
this._separateLocalThumbnail && isNarrowAspectRatio
|
||||
&& <LocalThumbnail />
|
||||
|
@ -130,35 +247,6 @@ class Filmstrip extends Component<Props> {
|
|||
</SafeAreaView>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sorts a specific array of {@code Participant}s in display order.
|
||||
*
|
||||
* @param {Participant[]} participants - The array of {@code Participant}s
|
||||
* to sort in display order.
|
||||
* @param {boolean} isNarrowAspectRatio - Indicates if the aspect ratio is
|
||||
* wide or narrow.
|
||||
* @private
|
||||
* @returns {Participant[]} A new array containing the elements of the
|
||||
* specified {@code participants} array sorted in display order.
|
||||
*/
|
||||
_sort(participants, isNarrowAspectRatio) {
|
||||
// XXX Array.prototype.sort() is not appropriate because (1) it operates
|
||||
// in place and (2) it is not necessarily stable.
|
||||
|
||||
const sortedParticipants = [
|
||||
...participants
|
||||
];
|
||||
|
||||
if (isNarrowAspectRatio) {
|
||||
// When the narrow aspect ratio is used, we want to have the remote
|
||||
// participants from right to left with the newest added/joined to
|
||||
// the leftmost side. The local participant is the leftmost item.
|
||||
sortedParticipants.reverse();
|
||||
}
|
||||
|
||||
return sortedParticipants;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -171,9 +259,13 @@ class Filmstrip extends Component<Props> {
|
|||
function _mapStateToProps(state) {
|
||||
const { enabled, remoteParticipants } = state['features/filmstrip'];
|
||||
const showRemoteVideos = shouldRemoteVideosBeVisible(state);
|
||||
const responsiveUI = state['features/base/responsive-ui'];
|
||||
|
||||
return {
|
||||
_aspectRatio: state['features/base/responsive-ui'].aspectRatio,
|
||||
_clientHeight: responsiveUI.clientHeight,
|
||||
_clientWidth: responsiveUI.clientWidth,
|
||||
_localParticipantId: getLocalParticipant(state)?.id,
|
||||
_participants: showRemoteVideos ? remoteParticipants : NO_REMOTE_VIDEOS,
|
||||
_visible: enabled && isFilmstripVisible(state)
|
||||
};
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
// @flow
|
||||
|
||||
import React, { useCallback } from 'react';
|
||||
import React, { PureComponent } from 'react';
|
||||
import { View } from 'react-native';
|
||||
import type { Dispatch } from 'redux';
|
||||
|
||||
|
@ -25,8 +25,8 @@ import { DisplayNameLabel } from '../../../display-name';
|
|||
import { toggleToolboxVisible } from '../../../toolbox/actions.native';
|
||||
import { RemoteVideoMenu } from '../../../video-menu';
|
||||
import ConnectionStatusComponent from '../../../video-menu/components/native/ConnectionStatusComponent';
|
||||
import SharedVideoMenu
|
||||
from '../../../video-menu/components/native/SharedVideoMenu';
|
||||
import SharedVideoMenu from '../../../video-menu/components/native/SharedVideoMenu';
|
||||
import { SQUARE_TILE_ASPECT_RATIO } from '../../constants';
|
||||
|
||||
import AudioMutedIndicator from './AudioMutedIndicator';
|
||||
import DominantSpeakerIndicator from './DominantSpeakerIndicator';
|
||||
|
@ -47,9 +47,19 @@ type Props = {
|
|||
_audioMuted: boolean,
|
||||
|
||||
/**
|
||||
* The Redux representation of the state "features/large-video".
|
||||
* Indicates whether the participant is fake.
|
||||
*/
|
||||
_largeVideo: Object,
|
||||
_isFakeParticipant: boolean,
|
||||
|
||||
/**
|
||||
* Indicates whether the participant is fake.
|
||||
*/
|
||||
_isScreenShare: boolean,
|
||||
|
||||
/**
|
||||
* Indicates whether the participant is local.
|
||||
*/
|
||||
_local: boolean,
|
||||
|
||||
/**
|
||||
* Shared video local participant owner.
|
||||
|
@ -57,9 +67,22 @@ type Props = {
|
|||
_localVideoOwner: boolean,
|
||||
|
||||
/**
|
||||
* The Redux representation of the participant to display.
|
||||
* The ID of the participant obtain from the participant object in Redux.
|
||||
*
|
||||
* NOTE: Generally it should be the same as the participantID prop except the case where the passed
|
||||
* participantID doesn't corespond to any of the existing participants.
|
||||
*/
|
||||
_participant: Object,
|
||||
_participantId: string,
|
||||
|
||||
/**
|
||||
* Indicates whether the participant is displayed on the large video.
|
||||
*/
|
||||
_participantInLargeVideo: boolean,
|
||||
|
||||
/**
|
||||
* Indicates whether the participant is pinned or not.
|
||||
*/
|
||||
_pinned: boolean,
|
||||
|
||||
/**
|
||||
* Whether to show the dominant speaker indicator or not.
|
||||
|
@ -77,9 +100,9 @@ type Props = {
|
|||
_styles: StyleType,
|
||||
|
||||
/**
|
||||
* The Redux representation of the participant's video track.
|
||||
* Indicates whether the participant is video muted.
|
||||
*/
|
||||
_videoTrack: Object,
|
||||
_videoMuted: boolean,
|
||||
|
||||
/**
|
||||
* If true, there will be no color overlay (tint) on the thumbnail
|
||||
|
@ -93,6 +116,11 @@ type Props = {
|
|||
*/
|
||||
dispatch: Dispatch<any>,
|
||||
|
||||
/**
|
||||
* The height of the thumnail.
|
||||
*/
|
||||
height: ?number,
|
||||
|
||||
/**
|
||||
* The ID of the participant related to the thumbnail.
|
||||
*/
|
||||
|
@ -103,11 +131,6 @@ type Props = {
|
|||
*/
|
||||
renderDisplayName: ?boolean,
|
||||
|
||||
/**
|
||||
* Optional styling to add or override on the Thumbnail component root.
|
||||
*/
|
||||
styleOverrides?: Object,
|
||||
|
||||
/**
|
||||
* If true, it tells the thumbnail that it needs to behave differently. E.g. react differently to a single tap.
|
||||
*/
|
||||
|
@ -116,121 +139,159 @@ type Props = {
|
|||
|
||||
/**
|
||||
* React component for video thumbnail.
|
||||
*
|
||||
* @param {Props} props - Properties passed to this functional component.
|
||||
* @returns {Component} - A React component.
|
||||
*/
|
||||
function Thumbnail(props: Props) {
|
||||
const {
|
||||
_audioMuted: audioMuted,
|
||||
_largeVideo: largeVideo,
|
||||
_localVideoOwner,
|
||||
_renderDominantSpeakerIndicator: renderDominantSpeakerIndicator,
|
||||
_renderModeratorIndicator: renderModeratorIndicator,
|
||||
_participant: participant,
|
||||
_styles,
|
||||
_videoTrack: videoTrack,
|
||||
dispatch,
|
||||
disableTint,
|
||||
renderDisplayName,
|
||||
tileView
|
||||
} = props;
|
||||
class Thumbnail extends PureComponent<Props> {
|
||||
|
||||
// It seems that on leave the Thumbnail for the left participant can be re-rendered.
|
||||
// This will happen when mapStateToProps is executed before the remoteParticipants list in redux is updated.
|
||||
if (typeof participant === 'undefined') {
|
||||
/**
|
||||
* Creates new Thumbnail component.
|
||||
*
|
||||
* @param {Props} props - The props of the component.
|
||||
* @returns {Thumbnail}
|
||||
*/
|
||||
constructor(props: Props) {
|
||||
super(props);
|
||||
|
||||
return null;
|
||||
this._onClick = this._onClick.bind(this);
|
||||
this._onThumbnailLongPress = this._onThumbnailLongPress.bind(this);
|
||||
}
|
||||
|
||||
const participantId = participant.id;
|
||||
const participantInLargeVideo
|
||||
= participantId === largeVideo.participantId;
|
||||
const videoMuted = !videoTrack || videoTrack.muted;
|
||||
const isScreenShare = videoTrack && videoTrack.videoType === VIDEO_TYPE.DESKTOP;
|
||||
const onClick = useCallback(() => {
|
||||
_onClick: () => void;
|
||||
|
||||
/**
|
||||
* Thumbnail click handler.
|
||||
*
|
||||
* @returns {void}
|
||||
*/
|
||||
_onClick() {
|
||||
const { _participantId, _pinned, dispatch, tileView } = this.props;
|
||||
|
||||
if (tileView) {
|
||||
dispatch(toggleToolboxVisible());
|
||||
} else {
|
||||
dispatch(pinParticipant(participant.pinned ? null : participant.id));
|
||||
dispatch(pinParticipant(_pinned ? null : _participantId));
|
||||
}
|
||||
}, [ participant, tileView, dispatch ]);
|
||||
const onThumbnailLongPress = useCallback(() => {
|
||||
if (participant.local) {
|
||||
}
|
||||
|
||||
_onThumbnailLongPress: () => void;
|
||||
|
||||
/**
|
||||
* Thumbnail long press handler.
|
||||
*
|
||||
* @returns {void}
|
||||
*/
|
||||
_onThumbnailLongPress() {
|
||||
const { _participantId, _local, _isFakeParticipant, _localVideoOwner, dispatch } = this.props;
|
||||
|
||||
if (_local) {
|
||||
dispatch(openDialog(ConnectionStatusComponent, {
|
||||
participantID: participant.id
|
||||
participantID: _participantId
|
||||
}));
|
||||
} else if (participant.isFakeParticipant) {
|
||||
} else if (_isFakeParticipant) {
|
||||
if (_localVideoOwner) {
|
||||
dispatch(openDialog(SharedVideoMenu, {
|
||||
participant
|
||||
_participantId
|
||||
}));
|
||||
}
|
||||
} else {
|
||||
dispatch(openDialog(RemoteVideoMenu, {
|
||||
participant
|
||||
participantId: _participantId
|
||||
}));
|
||||
}
|
||||
}, [ participant, dispatch ]);
|
||||
}
|
||||
|
||||
return (
|
||||
<Container
|
||||
onClick = { onClick }
|
||||
onLongPress = { onThumbnailLongPress }
|
||||
style = { [
|
||||
styles.thumbnail,
|
||||
participant.pinned && !tileView
|
||||
? _styles.thumbnailPinned : null,
|
||||
props.styleOverrides || null
|
||||
] }
|
||||
touchFeedback = { false }>
|
||||
/**
|
||||
* Implements React's {@link Component#render()}.
|
||||
*
|
||||
* @inheritdoc
|
||||
* @returns {ReactElement}
|
||||
*/
|
||||
render() {
|
||||
const {
|
||||
_audioMuted: audioMuted,
|
||||
_isScreenShare: isScreenShare,
|
||||
_isFakeParticipant,
|
||||
_renderDominantSpeakerIndicator: renderDominantSpeakerIndicator,
|
||||
_renderModeratorIndicator: renderModeratorIndicator,
|
||||
_participantId: participantId,
|
||||
_participantInLargeVideo: participantInLargeVideo,
|
||||
_pinned,
|
||||
_styles,
|
||||
_videoMuted: videoMuted,
|
||||
disableTint,
|
||||
height,
|
||||
renderDisplayName,
|
||||
tileView
|
||||
} = this.props;
|
||||
const styleOverrides = tileView ? {
|
||||
aspectRatio: SQUARE_TILE_ASPECT_RATIO,
|
||||
flex: 0,
|
||||
height,
|
||||
maxHeight: null,
|
||||
maxWidth: null,
|
||||
width: null
|
||||
} : null;
|
||||
|
||||
<ParticipantView
|
||||
avatarSize = { tileView ? AVATAR_SIZE * 1.5 : AVATAR_SIZE }
|
||||
disableVideo = { isScreenShare || participant.isFakeParticipant }
|
||||
participantId = { participantId }
|
||||
style = { _styles.participantViewStyle }
|
||||
tintEnabled = { participantInLargeVideo && !disableTint }
|
||||
tintStyle = { _styles.activeThumbnailTint }
|
||||
zOrder = { 1 } />
|
||||
|
||||
{ renderDisplayName && <Container style = { styles.displayNameContainer }>
|
||||
<DisplayNameLabel participantId = { participantId } />
|
||||
</Container> }
|
||||
|
||||
{ renderModeratorIndicator
|
||||
&& <View style = { styles.moderatorIndicatorContainer }>
|
||||
<ModeratorIndicator />
|
||||
</View>}
|
||||
|
||||
{ !participant.isFakeParticipant && <View
|
||||
return (
|
||||
<Container
|
||||
onClick = { this._onClick }
|
||||
onLongPress = { this._onThumbnailLongPress }
|
||||
style = { [
|
||||
styles.thumbnailTopIndicatorContainer,
|
||||
styles.thumbnailTopLeftIndicatorContainer
|
||||
] }>
|
||||
<RaisedHandIndicator participantId = { participant.id } />
|
||||
{ renderDominantSpeakerIndicator && <DominantSpeakerIndicator /> }
|
||||
</View> }
|
||||
|
||||
{ !participant.isFakeParticipant && <View
|
||||
style = { [
|
||||
styles.thumbnailTopIndicatorContainer,
|
||||
styles.thumbnailTopRightIndicatorContainer
|
||||
] }>
|
||||
<ConnectionIndicator participantId = { participant.id } />
|
||||
</View> }
|
||||
|
||||
{ !participant.isFakeParticipant && <Container style = { styles.thumbnailIndicatorContainer }>
|
||||
{ audioMuted
|
||||
&& <AudioMutedIndicator /> }
|
||||
{ videoMuted
|
||||
&& <VideoMutedIndicator /> }
|
||||
{ isScreenShare
|
||||
&& <ScreenShareIndicator /> }
|
||||
</Container> }
|
||||
|
||||
</Container>
|
||||
);
|
||||
styles.thumbnail,
|
||||
_pinned && !tileView ? _styles.thumbnailPinned : null,
|
||||
styleOverrides
|
||||
] }
|
||||
touchFeedback = { false }>
|
||||
<ParticipantView
|
||||
avatarSize = { tileView ? AVATAR_SIZE * 1.5 : AVATAR_SIZE }
|
||||
disableVideo = { isScreenShare || _isFakeParticipant }
|
||||
participantId = { participantId }
|
||||
style = { _styles.participantViewStyle }
|
||||
tintEnabled = { participantInLargeVideo && !disableTint }
|
||||
tintStyle = { _styles.activeThumbnailTint }
|
||||
zOrder = { 1 } />
|
||||
{
|
||||
renderDisplayName
|
||||
&& <Container style = { styles.displayNameContainer }>
|
||||
<DisplayNameLabel participantId = { participantId } />
|
||||
</Container>
|
||||
}
|
||||
{ renderModeratorIndicator
|
||||
&& <View style = { styles.moderatorIndicatorContainer }>
|
||||
<ModeratorIndicator />
|
||||
</View>
|
||||
}
|
||||
{
|
||||
!_isFakeParticipant
|
||||
&& <View
|
||||
style = { [
|
||||
styles.thumbnailTopIndicatorContainer,
|
||||
styles.thumbnailTopLeftIndicatorContainer
|
||||
] }>
|
||||
<RaisedHandIndicator participantId = { participantId } />
|
||||
{ renderDominantSpeakerIndicator && <DominantSpeakerIndicator /> }
|
||||
</View>
|
||||
}
|
||||
{
|
||||
!_isFakeParticipant
|
||||
&& <View
|
||||
style = { [
|
||||
styles.thumbnailTopIndicatorContainer,
|
||||
styles.thumbnailTopRightIndicatorContainer
|
||||
] }>
|
||||
<ConnectionIndicator participantId = { participantId } />
|
||||
</View>
|
||||
}
|
||||
{
|
||||
!_isFakeParticipant
|
||||
&& <Container style = { styles.thumbnailIndicatorContainer }>
|
||||
{ audioMuted && <AudioMutedIndicator /> }
|
||||
{ videoMuted && <VideoMutedIndicator /> }
|
||||
{ isScreenShare && <ScreenShareIndicator /> }
|
||||
</Container>
|
||||
}
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -255,21 +316,28 @@ function _mapStateToProps(state, ownProps) {
|
|||
= getTrackByMediaTypeAndParticipant(tracks, MEDIA_TYPE.AUDIO, id);
|
||||
const videoTrack
|
||||
= getTrackByMediaTypeAndParticipant(tracks, MEDIA_TYPE.VIDEO, id);
|
||||
const videoMuted = !videoTrack || videoTrack.muted;
|
||||
const isScreenShare = videoTrack && videoTrack.videoType === VIDEO_TYPE.DESKTOP;
|
||||
const participantCount = getParticipantCount(state);
|
||||
const renderDominantSpeakerIndicator = participant && participant.dominantSpeaker && participantCount > 2;
|
||||
const _isEveryoneModerator = isEveryoneModerator(state);
|
||||
const renderModeratorIndicator = !_isEveryoneModerator
|
||||
&& participant && participant.role === PARTICIPANT_ROLE.MODERATOR;
|
||||
const participantInLargeVideo = id === largeVideo.participantId;
|
||||
|
||||
return {
|
||||
_audioMuted: audioTrack?.muted ?? true,
|
||||
_largeVideo: largeVideo,
|
||||
_isScreenShare: isScreenShare,
|
||||
_isFakeParticipant: participant?.isFakeParticipant,
|
||||
_local: participant?.local,
|
||||
_localVideoOwner: Boolean(ownerId === localParticipantId),
|
||||
_participant: participant,
|
||||
_participantInLargeVideo: participantInLargeVideo,
|
||||
_participantId: id,
|
||||
_pinned: participant?.pinned,
|
||||
_renderDominantSpeakerIndicator: renderDominantSpeakerIndicator,
|
||||
_renderModeratorIndicator: renderModeratorIndicator,
|
||||
_styles: ColorSchemeRegistry.get(state, 'Thumbnail'),
|
||||
_videoTrack: videoTrack
|
||||
_videoMuted: videoMuted
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
// @flow
|
||||
|
||||
import React, { Component } from 'react';
|
||||
import React, { PureComponent } from 'react';
|
||||
import {
|
||||
ScrollView,
|
||||
FlatList,
|
||||
TouchableWithoutFeedback,
|
||||
View
|
||||
} from 'react-native';
|
||||
|
@ -10,13 +10,11 @@ import type { Dispatch } from 'redux';
|
|||
|
||||
import { getLocalParticipant, getParticipantCountWithFake } from '../../../base/participants';
|
||||
import { connect } from '../../../base/redux';
|
||||
import { ASPECT_RATIO_NARROW } from '../../../base/responsive-ui/constants';
|
||||
import { setTileViewDimensions } from '../../actions.native';
|
||||
import { setVisibleRemoteParticipants } from '../../actions.web';
|
||||
|
||||
import Thumbnail from './Thumbnail';
|
||||
import styles from './styles';
|
||||
|
||||
|
||||
/**
|
||||
* The type of the React {@link Component} props of {@link TileView}.
|
||||
*/
|
||||
|
@ -27,6 +25,11 @@ type Props = {
|
|||
*/
|
||||
_aspectRatio: Symbol,
|
||||
|
||||
/**
|
||||
* The number of columns.
|
||||
*/
|
||||
_columns: number,
|
||||
|
||||
/**
|
||||
* Application's viewport height.
|
||||
*/
|
||||
|
@ -47,6 +50,11 @@ type Props = {
|
|||
*/
|
||||
_remoteParticipants: Array<string>,
|
||||
|
||||
/**
|
||||
* The thumbnail height.
|
||||
*/
|
||||
_thumbnailHeight: number,
|
||||
|
||||
/**
|
||||
* Application's viewport height.
|
||||
*/
|
||||
|
@ -64,45 +72,81 @@ type Props = {
|
|||
};
|
||||
|
||||
/**
|
||||
* 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
|
||||
* Implements a React {@link PureComponent} which displays thumbnails in a two
|
||||
* dimensional grid.
|
||||
*
|
||||
* @extends Component
|
||||
* @extends PureComponent
|
||||
*/
|
||||
class TileView extends Component<Props> {
|
||||
/**
|
||||
* Implements React's {@link Component#componentDidMount}.
|
||||
*
|
||||
* @inheritdoc
|
||||
*/
|
||||
componentDidMount() {
|
||||
this._updateReceiverQuality();
|
||||
}
|
||||
class TileView extends PureComponent<Props> {
|
||||
|
||||
/**
|
||||
* Implements React's {@link Component#componentDidUpdate}.
|
||||
*
|
||||
* @inheritdoc
|
||||
* The FlatList's viewabilityConfig.
|
||||
*/
|
||||
componentDidUpdate() {
|
||||
this._updateReceiverQuality();
|
||||
_viewabilityConfig: Object;
|
||||
|
||||
/**
|
||||
* The styles for the FlatList.
|
||||
*/
|
||||
_flatListStyles: Object;
|
||||
|
||||
/**
|
||||
* The styles for the content container of the FlatList.
|
||||
*/
|
||||
_contentContainerStyles: Object;
|
||||
|
||||
/**
|
||||
* Creates new TileView component.
|
||||
*
|
||||
* @param {Props} props - The props of the component.
|
||||
*/
|
||||
constructor(props: Props) {
|
||||
super(props);
|
||||
|
||||
this._keyExtractor = this._keyExtractor.bind(this);
|
||||
this._onViewableItemsChanged = this._onViewableItemsChanged.bind(this);
|
||||
this._renderThumbnail = this._renderThumbnail.bind(this);
|
||||
this._viewabilityConfig = {
|
||||
itemVisiblePercentThreshold: 30
|
||||
};
|
||||
this._flatListStyles = {
|
||||
...styles.flatList
|
||||
};
|
||||
this._contentContainerStyles = {
|
||||
...styles.contentContainer
|
||||
};
|
||||
}
|
||||
|
||||
_keyExtractor: string => string;
|
||||
|
||||
/**
|
||||
* Returns a key for a passed item of the list.
|
||||
*
|
||||
* @param {string} item - The user ID.
|
||||
* @returns {string} - The user ID.
|
||||
*/
|
||||
_keyExtractor(item) {
|
||||
return item;
|
||||
}
|
||||
|
||||
_onViewableItemsChanged: Object => void;
|
||||
|
||||
/**
|
||||
* A handler for visible items changes.
|
||||
*
|
||||
* @param {Object} data - The visible items data.
|
||||
* @param {Array<Object>} data.viewableItems - The visible items array.
|
||||
* @returns {void}
|
||||
*/
|
||||
_onViewableItemsChanged({ viewableItems = [] }: { viewableItems: Array<Object> }) {
|
||||
const indexArray = viewableItems.map(i => i.index);
|
||||
|
||||
// We need to shift the start index of the remoteParticipants array with 1 because of the local video placed
|
||||
// at the beginning and in the same time we don't need to adjust the end index because the end index will not be
|
||||
// included.
|
||||
const startIndex = Math.max(Math.min(...indexArray) - 1, 0);
|
||||
const endIndex = Math.max(...indexArray);
|
||||
|
||||
this.props.dispatch(setVisibleRemoteParticipants(startIndex, endIndex));
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -112,54 +156,49 @@ class TileView extends Component<Props> {
|
|||
* @returns {ReactElement}
|
||||
*/
|
||||
render() {
|
||||
const { _height, _width, onClick } = this.props;
|
||||
const rowElements = this._groupIntoRows(this._renderThumbnails(), this._getColumnCount());
|
||||
const { _columns, _height, _thumbnailHeight, _width, onClick } = this.props;
|
||||
const participants = this._getSortedParticipants();
|
||||
const initialRowsToRender = Math.ceil(_height / (_thumbnailHeight + (2 * styles.thumbnail.margin)));
|
||||
|
||||
if (this._flatListStyles.minHeight !== _height || this._flatListStyles.minWidth !== _width) {
|
||||
this._flatListStyles = {
|
||||
...styles.flatList,
|
||||
minHeight: _height,
|
||||
minWidth: _width
|
||||
};
|
||||
}
|
||||
|
||||
if (this._contentContainerStyles.minHeight !== _height || this._contentContainerStyles.minWidth !== _width) {
|
||||
this._contentContainerStyles = {
|
||||
...styles.contentContainer,
|
||||
minHeight: _height,
|
||||
minWidth: _width
|
||||
};
|
||||
}
|
||||
|
||||
return (
|
||||
<ScrollView
|
||||
style = {{
|
||||
...styles.tileView,
|
||||
height: _height,
|
||||
width: _width
|
||||
}}>
|
||||
<TouchableWithoutFeedback onPress = { onClick }>
|
||||
<View
|
||||
style = {{
|
||||
...styles.tileViewRows,
|
||||
minHeight: _height,
|
||||
minWidth: _width
|
||||
}}>
|
||||
{ rowElements }
|
||||
</View>
|
||||
</TouchableWithoutFeedback>
|
||||
</ScrollView>
|
||||
<TouchableWithoutFeedback onPress = { onClick }>
|
||||
<View style = { styles.flatListContainer }>
|
||||
<FlatList
|
||||
contentContainerStyle = { this._contentContainerStyles }
|
||||
data = { participants }
|
||||
horizontal = { false }
|
||||
initialNumToRender = { initialRowsToRender }
|
||||
key = { _columns }
|
||||
keyExtractor = { this._keyExtractor }
|
||||
numColumns = { _columns }
|
||||
onViewableItemsChanged = { this._onViewableItemsChanged }
|
||||
renderItem = { this._renderThumbnail }
|
||||
showsHorizontalScrollIndicator = { false }
|
||||
showsVerticalScrollIndicator = { false }
|
||||
style = { this._flatListStyles }
|
||||
viewabilityConfig = { this._viewabilityConfig }
|
||||
windowSize = { 2 } />
|
||||
</View>
|
||||
</TouchableWithoutFeedback>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns how many columns should be displayed for tile view.
|
||||
*
|
||||
* @returns {number}
|
||||
* @private
|
||||
*/
|
||||
_getColumnCount() {
|
||||
const participantCount = this.props._participantCount;
|
||||
|
||||
// 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 (this.props._aspectRatio === ASPECT_RATIO_NARROW) {
|
||||
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.
|
||||
*
|
||||
|
@ -168,114 +207,33 @@ class TileView extends Component<Props> {
|
|||
*/
|
||||
_getSortedParticipants() {
|
||||
const { _localParticipant, _remoteParticipants } = this.props;
|
||||
const participants = [ ..._remoteParticipants ];
|
||||
const participants = [];
|
||||
|
||||
_localParticipant && participants.push(_localParticipant.id);
|
||||
|
||||
return participants;
|
||||
return [ ...participants, ..._remoteParticipants ];
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate the height and width for the tiles.
|
||||
*
|
||||
* @private
|
||||
* @returns {Object}
|
||||
*/
|
||||
_getTileDimensions() {
|
||||
const { _height, _participantCount, _width } = this.props;
|
||||
const columns = this._getColumnCount();
|
||||
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
|
||||
};
|
||||
}
|
||||
_renderThumbnail: Object => Object;
|
||||
|
||||
/**
|
||||
* 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;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates React Elements to display each participant in a thumbnail. Each
|
||||
* tile will be.
|
||||
* Creates React Element to display each participant in a thumbnail.
|
||||
*
|
||||
* @private
|
||||
* @returns {ReactElement[]}
|
||||
* @returns {ReactElement}
|
||||
*/
|
||||
_renderThumbnails() {
|
||||
const styleOverrides = {
|
||||
aspectRatio: TILE_ASPECT_RATIO,
|
||||
flex: 0,
|
||||
height: this._getTileDimensions().height,
|
||||
maxHeight: null,
|
||||
maxWidth: null,
|
||||
width: null
|
||||
};
|
||||
_renderThumbnail({ item/* , index , separators */ }) {
|
||||
const { _thumbnailHeight } = this.props;
|
||||
|
||||
return this._getSortedParticipants()
|
||||
.map(id => (
|
||||
<Thumbnail
|
||||
disableTint = { true }
|
||||
key = { id }
|
||||
participantID = { id }
|
||||
renderDisplayName = { true }
|
||||
styleOverrides = { styleOverrides }
|
||||
tileView = { true } />));
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the receiver video quality based on the dimensions of the thumbnails
|
||||
* that are displayed.
|
||||
*
|
||||
* @private
|
||||
* @returns {void}
|
||||
*/
|
||||
_updateReceiverQuality() {
|
||||
const { height, width } = this._getTileDimensions();
|
||||
|
||||
this.props.dispatch(setTileViewDimensions({
|
||||
thumbnailSize: {
|
||||
height,
|
||||
width
|
||||
}
|
||||
}));
|
||||
return (
|
||||
<Thumbnail
|
||||
disableTint = { true }
|
||||
height = { _thumbnailHeight }
|
||||
key = { item }
|
||||
participantID = { item }
|
||||
renderDisplayName = { true }
|
||||
tileView = { true } />)
|
||||
;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -288,14 +246,18 @@ class TileView extends Component<Props> {
|
|||
*/
|
||||
function _mapStateToProps(state) {
|
||||
const responsiveUi = state['features/base/responsive-ui'];
|
||||
const { remoteParticipants } = state['features/filmstrip'];
|
||||
const { remoteParticipants, tileViewDimensions } = state['features/filmstrip'];
|
||||
const { height } = tileViewDimensions.thumbnailSize;
|
||||
const { columns } = tileViewDimensions;
|
||||
|
||||
return {
|
||||
_aspectRatio: responsiveUi.aspectRatio,
|
||||
_columns: columns,
|
||||
_height: responsiveUi.clientHeight,
|
||||
_localParticipant: getLocalParticipant(state),
|
||||
_participantCount: getParticipantCountWithFake(state),
|
||||
_remoteParticipants: remoteParticipants,
|
||||
_thumbnailHeight: height,
|
||||
_width: responsiveUi.clientWidth
|
||||
};
|
||||
}
|
||||
|
|
|
@ -14,6 +14,15 @@ export const AVATAR_SIZE = 50;
|
|||
*/
|
||||
export default {
|
||||
|
||||
/**
|
||||
* The FlatList content container styles
|
||||
*/
|
||||
contentContainer: {
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
flex: 0
|
||||
},
|
||||
|
||||
/**
|
||||
* The display name container.
|
||||
*/
|
||||
|
@ -52,6 +61,22 @@ export default {
|
|||
top: 0
|
||||
},
|
||||
|
||||
/**
|
||||
* The styles for the FlatList container.
|
||||
*/
|
||||
flatListContainer: {
|
||||
flexGrow: 1,
|
||||
flexShrink: 1,
|
||||
flex: 0
|
||||
},
|
||||
|
||||
/**
|
||||
* The styles for the FlatList.
|
||||
*/
|
||||
flatList: {
|
||||
flex: 0
|
||||
},
|
||||
|
||||
/**
|
||||
* Container of the {@link LocalThumbnail}.
|
||||
*/
|
||||
|
@ -122,19 +147,6 @@ export default {
|
|||
|
||||
thumbnailTopRightIndicatorContainer: {
|
||||
right: 0
|
||||
},
|
||||
|
||||
tileView: {
|
||||
alignSelf: 'center'
|
||||
},
|
||||
|
||||
tileViewRows: {
|
||||
justifyContent: 'center'
|
||||
},
|
||||
|
||||
tileViewRow: {
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'center'
|
||||
}
|
||||
};
|
||||
|
||||
|
|
|
@ -15,7 +15,7 @@ type Props = {
|
|||
/**
|
||||
* The horizontal offset in px for the thumbnail. Used to center the thumbnails in the last row in tile view.
|
||||
*/
|
||||
_horizontalOffset: number,
|
||||
_horizontalOffset: number,
|
||||
|
||||
/**
|
||||
* The ID of the participant associated with the Thumbnail.
|
||||
|
|
|
@ -220,3 +220,14 @@ export const HORIZONTAL_FILMSTRIP_MARGIN = 39;
|
|||
* @type {number}
|
||||
*/
|
||||
export const SHOW_TOOLBAR_CONTEXT_MENU_AFTER = 600;
|
||||
|
||||
/**
|
||||
* The margin for each side of the tile view. Taken away from the available
|
||||
* height and width for the tile container to display in.
|
||||
*
|
||||
* NOTE: Mobile specific.
|
||||
*
|
||||
* @private
|
||||
* @type {number}
|
||||
*/
|
||||
export const TILE_MARGIN = 10;
|
||||
|
|
|
@ -3,6 +3,7 @@
|
|||
import { getFeatureFlag, FILMSTRIP_ENABLED } from '../base/flags';
|
||||
import { getParticipantCountWithFake, getPinnedParticipant } from '../base/participants';
|
||||
import { toState } from '../base/redux';
|
||||
import { ASPECT_RATIO_NARROW } from '../base/responsive-ui/constants';
|
||||
|
||||
export * from './functions.any';
|
||||
|
||||
|
@ -59,3 +60,31 @@ export function shouldRemoteVideosBeVisible(state: Object) {
|
|||
|
||||
|| disable1On1Mode);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns how many columns should be displayed for tile view.
|
||||
*
|
||||
* @param {Object | Function} stateful - The Object or Function that can be
|
||||
* resolved to a Redux state object with the toState function.
|
||||
* @returns {number} - The number of columns to be rendered in tile view.
|
||||
* @private
|
||||
*/
|
||||
export function getColumnCount(stateful: Object | Function) {
|
||||
const state = toState(stateful);
|
||||
const participantCount = getParticipantCountWithFake(state);
|
||||
const { aspectRatio } = state['features/base/responsive-ui'];
|
||||
|
||||
// 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 (aspectRatio === ASPECT_RATIO_NARROW) {
|
||||
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);
|
||||
}
|
||||
|
|
|
@ -2,7 +2,9 @@
|
|||
|
||||
import { PARTICIPANT_JOINED, PARTICIPANT_LEFT } from '../base/participants';
|
||||
import { MiddlewareRegistry } from '../base/redux';
|
||||
import { CLIENT_RESIZED, SET_ASPECT_RATIO } from '../base/responsive-ui';
|
||||
|
||||
import { setTileViewDimensions } from './actions';
|
||||
import { updateRemoteParticipants, updateRemoteParticipantsOnLeave } from './functions';
|
||||
import './subscriber';
|
||||
|
||||
|
@ -10,19 +12,22 @@ import './subscriber';
|
|||
* The middleware of the feature Filmstrip.
|
||||
*/
|
||||
MiddlewareRegistry.register(store => next => action => {
|
||||
if (action.type === PARTICIPANT_LEFT) {
|
||||
updateRemoteParticipantsOnLeave(store, action.participant?.id);
|
||||
}
|
||||
|
||||
const result = next(action);
|
||||
|
||||
switch (action.type) {
|
||||
case CLIENT_RESIZED:
|
||||
case SET_ASPECT_RATIO:
|
||||
store.dispatch(setTileViewDimensions());
|
||||
break;
|
||||
case PARTICIPANT_JOINED: {
|
||||
updateRemoteParticipants(store);
|
||||
break;
|
||||
}
|
||||
case PARTICIPANT_LEFT: {
|
||||
updateRemoteParticipantsOnLeave(store, action.participant?.id);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
});
|
||||
|
||||
|
|
|
@ -118,13 +118,9 @@ ReducerRegistry.register(
|
|||
};
|
||||
case SET_REMOTE_PARTICIPANTS: {
|
||||
state.remoteParticipants = action.participants;
|
||||
const { visibleParticipantsStartIndex: startIndex, visibleParticipantsEndIndex: endIndex } = state;
|
||||
|
||||
// TODO: implement this on mobile.
|
||||
if (navigator.product !== 'ReactNative') {
|
||||
const { visibleParticipantsStartIndex: startIndex, visibleParticipantsEndIndex: endIndex } = state;
|
||||
|
||||
state.visibleRemoteParticipants = new Set(state.remoteParticipants.slice(startIndex, endIndex + 1));
|
||||
}
|
||||
state.visibleRemoteParticipants = new Set(state.remoteParticipants.slice(startIndex, endIndex + 1));
|
||||
|
||||
return { ...state };
|
||||
}
|
||||
|
@ -167,7 +163,9 @@ ReducerRegistry.register(
|
|||
}
|
||||
delete state.participantsVolume[id];
|
||||
|
||||
return state;
|
||||
return {
|
||||
...state
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -1,3 +1,42 @@
|
|||
// @flow
|
||||
|
||||
import { getParticipantCountWithFake } from '../base/participants';
|
||||
import { StateListenerRegistry } from '../base/redux';
|
||||
import { getTileViewGridDimensions, shouldDisplayTileView } from '../video-layout';
|
||||
|
||||
import { setTileViewDimensions } from './actions';
|
||||
import './subscriber.any';
|
||||
|
||||
/**
|
||||
* Listens for changes in the number of participants to calculate the dimensions of the tile view grid and the tiles.
|
||||
*/
|
||||
StateListenerRegistry.register(
|
||||
/* selector */ state => {
|
||||
const participantCount = getParticipantCountWithFake(state);
|
||||
|
||||
if (participantCount < 5) { // the dimensions are updated only when the participant count is lower than 5.
|
||||
return participantCount;
|
||||
}
|
||||
|
||||
return 4; // make sure we don't update the dimensions.
|
||||
},
|
||||
/* listener */ (_, store) => {
|
||||
const state = store.getState();
|
||||
|
||||
if (shouldDisplayTileView(state)) {
|
||||
store.dispatch(setTileViewDimensions());
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* Listens for changes in the selected layout to calculate the dimensions of the tile view grid and horizontal view.
|
||||
*/
|
||||
StateListenerRegistry.register(
|
||||
/* selector */ state => shouldDisplayTileView(state),
|
||||
/* listener */ (isTileView, store) => {
|
||||
const state = store.getState();
|
||||
|
||||
if (isTileView) {
|
||||
store.dispatch(setTileViewDimensions(getTileViewGridDimensions(state)));
|
||||
}
|
||||
});
|
||||
|
|
|
@ -439,7 +439,7 @@ export function isDialOutEnabled(state: Object): boolean {
|
|||
*/
|
||||
export function isSipInviteEnabled(state: Object): boolean {
|
||||
const { sipInviteUrl } = state['features/base/config'];
|
||||
const { features = {} } = getLocalParticipant(state);
|
||||
const { features = {} } = getLocalParticipant(state) || {};
|
||||
|
||||
return state['features/base/jwt'].jwt
|
||||
&& Boolean(sipInviteUrl)
|
||||
|
|
|
@ -42,9 +42,9 @@ type Props = {
|
|||
dispatch: Function,
|
||||
|
||||
/**
|
||||
* The participant for which this menu opened for.
|
||||
* The ID of the participant for which this menu opened for.
|
||||
*/
|
||||
participant: Object,
|
||||
participantId: String,
|
||||
|
||||
/**
|
||||
* The color-schemed stylesheet of the BottomSheet.
|
||||
|
@ -79,12 +79,7 @@ type Props = {
|
|||
/**
|
||||
* Display name of the participant retrieved from Redux.
|
||||
*/
|
||||
_participantDisplayName: string,
|
||||
|
||||
/**
|
||||
* The ID of the participant.
|
||||
*/
|
||||
_participantID: ?string,
|
||||
_participantDisplayName: string
|
||||
}
|
||||
|
||||
// eslint-disable-next-line prefer-const
|
||||
|
@ -117,12 +112,12 @@ class RemoteVideoMenu extends PureComponent<Props> {
|
|||
_disableRemoteMute,
|
||||
_disableGrantModerator,
|
||||
_isParticipantAvailable,
|
||||
participant
|
||||
participantId
|
||||
} = this.props;
|
||||
const buttonProps = {
|
||||
afterClick: this._onCancel,
|
||||
showLabel: true,
|
||||
participantID: participant.id,
|
||||
participantID: participantId,
|
||||
styles: this.props._bottomSheetStyles.buttons
|
||||
};
|
||||
|
||||
|
@ -141,7 +136,7 @@ class RemoteVideoMenu extends PureComponent<Props> {
|
|||
<PrivateMessageButton { ...buttonProps } />
|
||||
<ConnectionStatusButton { ...buttonProps } />
|
||||
{/* <Divider style = { styles.divider } />*/}
|
||||
{/* <VolumeSlider participantID = { _participantID } />*/}
|
||||
{/* <VolumeSlider participantID = { participantId } />*/}
|
||||
</BottomSheet>
|
||||
);
|
||||
}
|
||||
|
@ -172,7 +167,7 @@ class RemoteVideoMenu extends PureComponent<Props> {
|
|||
* @returns {React$Element}
|
||||
*/
|
||||
_renderMenuHeader() {
|
||||
const { _bottomSheetStyles, participant } = this.props;
|
||||
const { _bottomSheetStyles, participantId } = this.props;
|
||||
|
||||
return (
|
||||
<View
|
||||
|
@ -180,7 +175,7 @@ class RemoteVideoMenu extends PureComponent<Props> {
|
|||
_bottomSheetStyles.sheet,
|
||||
styles.participantNameContainer ] }>
|
||||
<Avatar
|
||||
participantId = { participant.id }
|
||||
participantId = { participantId }
|
||||
size = { AVATAR_SIZE } />
|
||||
<Text style = { styles.participantNameLabel }>
|
||||
{ this.props._participantDisplayName }
|
||||
|
@ -200,9 +195,9 @@ class RemoteVideoMenu extends PureComponent<Props> {
|
|||
*/
|
||||
function _mapStateToProps(state, ownProps) {
|
||||
const kickOutEnabled = getFeatureFlag(state, KICK_OUT_ENABLED, true);
|
||||
const { participant } = ownProps;
|
||||
const { participantId } = ownProps;
|
||||
const { remoteVideoMenu = {}, disableRemoteMute } = state['features/base/config'];
|
||||
const isParticipantAvailable = getParticipantById(state, participant.id);
|
||||
const isParticipantAvailable = getParticipantById(state, participantId);
|
||||
let { disableKick } = remoteVideoMenu;
|
||||
|
||||
disableKick = disableKick || !kickOutEnabled;
|
||||
|
@ -213,8 +208,7 @@ function _mapStateToProps(state, ownProps) {
|
|||
_disableRemoteMute: Boolean(disableRemoteMute),
|
||||
_isOpen: isDialogOpen(state, RemoteVideoMenu_),
|
||||
_isParticipantAvailable: Boolean(isParticipantAvailable),
|
||||
_participantDisplayName: getParticipantDisplayName(state, participant.id),
|
||||
_participantID: participant.id
|
||||
_participantDisplayName: getParticipantDisplayName(state, participantId)
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
@ -32,9 +32,9 @@ type Props = {
|
|||
dispatch: Function,
|
||||
|
||||
/**
|
||||
* The participant for which this menu opened for.
|
||||
* The ID of the participant for which this menu opened for.
|
||||
*/
|
||||
participant: Object,
|
||||
participantId: string,
|
||||
|
||||
/**
|
||||
* The color-schemed stylesheet of the BottomSheet.
|
||||
|
@ -55,11 +55,6 @@ type Props = {
|
|||
* Display name of the participant retrieved from Redux.
|
||||
*/
|
||||
_participantDisplayName: string,
|
||||
|
||||
/**
|
||||
* The ID of the participant.
|
||||
*/
|
||||
_participantID: ?string,
|
||||
}
|
||||
|
||||
// eslint-disable-next-line prefer-const
|
||||
|
@ -89,13 +84,13 @@ class SharedVideoMenu extends PureComponent<Props> {
|
|||
render() {
|
||||
const {
|
||||
_isParticipantAvailable,
|
||||
participant
|
||||
participantId
|
||||
} = this.props;
|
||||
|
||||
const buttonProps = {
|
||||
afterClick: this._onCancel,
|
||||
showLabel: true,
|
||||
participantID: participant.id,
|
||||
participantID: participantId,
|
||||
styles: this.props._bottomSheetStyles.buttons
|
||||
};
|
||||
|
||||
|
@ -136,7 +131,7 @@ class SharedVideoMenu extends PureComponent<Props> {
|
|||
* @returns {React$Element}
|
||||
*/
|
||||
_renderMenuHeader() {
|
||||
const { _bottomSheetStyles, participant } = this.props;
|
||||
const { _bottomSheetStyles, participantId } = this.props;
|
||||
|
||||
return (
|
||||
<View
|
||||
|
@ -144,7 +139,7 @@ class SharedVideoMenu extends PureComponent<Props> {
|
|||
_bottomSheetStyles.sheet,
|
||||
styles.participantNameContainer ] }>
|
||||
<Avatar
|
||||
participantId = { participant.id }
|
||||
participantId = { participantId }
|
||||
size = { AVATAR_SIZE } />
|
||||
<Text style = { styles.participantNameLabel }>
|
||||
{ this.props._participantDisplayName }
|
||||
|
@ -163,15 +158,14 @@ class SharedVideoMenu extends PureComponent<Props> {
|
|||
* @returns {Props}
|
||||
*/
|
||||
function _mapStateToProps(state, ownProps) {
|
||||
const { participant } = ownProps;
|
||||
const isParticipantAvailable = getParticipantById(state, participant.id);
|
||||
const { participantId } = ownProps;
|
||||
const isParticipantAvailable = getParticipantById(state, participantId);
|
||||
|
||||
return {
|
||||
_bottomSheetStyles: ColorSchemeRegistry.get(state, 'BottomSheet'),
|
||||
_isOpen: isDialogOpen(state, SharedVideoMenu_),
|
||||
_isParticipantAvailable: Boolean(isParticipantAvailable),
|
||||
_participantDisplayName: getParticipantDisplayName(state, participant.id),
|
||||
_participantID: participant.id
|
||||
_participantDisplayName: getParticipantDisplayName(state, participantId)
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
@ -191,12 +191,7 @@ function _updateReceiverVideoConstraints({ getState }) {
|
|||
const { maxReceiverVideoQuality, preferredVideoQuality } = state['features/video-quality'];
|
||||
const { participantId: largeVideoParticipantId } = state['features/large-video'];
|
||||
const maxFrameHeight = Math.min(maxReceiverVideoQuality, preferredVideoQuality);
|
||||
let { visibleRemoteParticipants } = state['features/filmstrip'];
|
||||
|
||||
// TODO: implement this on mobile.
|
||||
if (navigator.product === 'ReactNative') {
|
||||
visibleRemoteParticipants = new Set(Array.from(state['features/base/participants'].remote.keys()));
|
||||
}
|
||||
const { visibleRemoteParticipants } = state['features/filmstrip'];
|
||||
|
||||
const receiverConstraints = {
|
||||
constraints: {},
|
||||
|
|
Loading…
Reference in New Issue