feat(filmstrip-pagination): mobile support.

This commit is contained in:
Hristo Terezov 2021-08-20 18:32:38 -05:00
parent 37acce3764
commit 7dd43d93b6
19 changed files with 675 additions and 447 deletions

View File

@ -551,7 +551,7 @@ function _updateLocalParticipantInConference({ dispatch, getState }, next, actio
const localParticipant = getLocalParticipant(getState); const localParticipant = getLocalParticipant(getState);
if (conference && participant.id === localParticipant.id) { if (conference && participant.id === localParticipant?.id) {
if ('name' in participant) { if ('name' in participant) {
conference.setDisplayName(participant.name); conference.setDisplayName(participant.name);
} }

View File

@ -29,12 +29,13 @@ const REDUCED_UI_THRESHOLD = 300;
*/ */
export function clientResized(clientWidth: number, clientHeight: number) { export function clientResized(clientWidth: number, clientHeight: number) {
return (dispatch: Dispatch<any>, getState: Function) => { return (dispatch: Dispatch<any>, getState: Function) => {
const state = getState();
const { isOpen: isChatOpen } = state['features/chat'];
const isParticipantsPaneOpen = getParticipantsPaneOpen(state);
let availableWidth = clientWidth; let availableWidth = clientWidth;
if (navigator.product !== 'ReactNative') { if (navigator.product !== 'ReactNative') {
const state = getState();
const { isOpen: isChatOpen } = state['features/chat'];
const isParticipantsPaneOpen = getParticipantsPaneOpen(state);
if (isChatOpen) { if (isChatOpen) {
availableWidth -= CHAT_SIZE; availableWidth -= CHAT_SIZE;
} }

View File

@ -1,6 +1,11 @@
// @flow // @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. * Sets whether the filmstrip is enabled.
@ -50,3 +55,23 @@ export function setRemoteParticipants(participants: Array<string>) {
participants 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
};
}

View File

@ -1,6 +1,10 @@
// @flow // @flow
import { getParticipantCountWithFake } from '../base/participants';
import { SET_TILE_VIEW_DIMENSIONS } from './actionTypes'; 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'; 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 the values are currently used. Check the description of {@link SET_TILE_VIEW_DIMENSIONS} for the full set
* of properties. * of properties.
* *
* @param {Object} dimensions - The tile view dimensions. * @returns {Function}
* @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
* }}
*/ */
export function setTileViewDimensions({ thumbnailSize }: Object) { export function setTileViewDimensions() {
return { 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, type: SET_TILE_VIEW_DIMENSIONS,
dimensions: { dimensions: {
thumbnailSize columns,
thumbnailSize: {
height: tileHeight,
width: tileWidth
} }
}
});
}; };
} }

View File

@ -7,7 +7,6 @@ import {
SET_HORIZONTAL_VIEW_DIMENSIONS, SET_HORIZONTAL_VIEW_DIMENSIONS,
SET_TILE_VIEW_DIMENSIONS, SET_TILE_VIEW_DIMENSIONS,
SET_VERTICAL_VIEW_DIMENSIONS, SET_VERTICAL_VIEW_DIMENSIONS,
SET_VISIBLE_REMOTE_PARTICIPANTS,
SET_VOLUME SET_VOLUME
} from './actionTypes'; } from './actionTypes';
import { import {
@ -159,23 +158,3 @@ export function setVolume(participantId: string, volume: number) {
volume 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
};
}

View File

@ -1,11 +1,13 @@
// @flow // @flow
import React, { Component } from 'react'; import React, { PureComponent } from 'react';
import { SafeAreaView, ScrollView } from 'react-native'; import { FlatList, SafeAreaView } from 'react-native';
import { getLocalParticipant } from '../../../base/participants';
import { Platform } from '../../../base/react'; import { Platform } from '../../../base/react';
import { connect } from '../../../base/redux'; import { connect } from '../../../base/redux';
import { ASPECT_RATIO_NARROW } from '../../../base/responsive-ui/constants'; import { ASPECT_RATIO_NARROW } from '../../../base/responsive-ui/constants';
import { setVisibleRemoteParticipants } from '../../actions';
import { isFilmstripVisible, shouldRemoteVideosBeVisible } from '../../functions'; import { isFilmstripVisible, shouldRemoteVideosBeVisible } from '../../functions';
import LocalThumbnail from './LocalThumbnail'; import LocalThumbnail from './LocalThumbnail';
@ -25,6 +27,12 @@ type Props = {
*/ */
_aspectRatio: Symbol, _aspectRatio: Symbol,
_clientWidth: number,
_clientHeight: number,
_localParticipantId: string,
/** /**
* The participants in the conference. * The participants in the conference.
*/ */
@ -33,7 +41,12 @@ type Props = {
/** /**
* The indicator which determines whether the filmstrip is visible. * 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 * @extends Component
*/ */
class Filmstrip extends Component<Props> { class Filmstrip extends PureComponent<Props> {
/** /**
* Whether the local participant should be rendered separately from the * Whether the local participant should be rendered separately from the
* remote participants i.e. outside of their {@link ScrollView}. * remote participants i.e. outside of their {@link ScrollView}.
*/ */
_separateLocalThumbnail: boolean; _separateLocalThumbnail: boolean;
/**
* The FlatList's viewabilityConfig.
*/
_viewabilityConfig: Object;
/** /**
* Constructor of the component. * 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 // do not have much of a choice but to continue rendering LocalThumbnail
// as any other remote Thumbnail on Android. // as any other remote Thumbnail on Android.
this._separateLocalThumbnail = Platform.OS !== '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} * @returns {ReactElement}
*/ */
render() { render() {
const { _aspectRatio, _participants, _visible } = this.props; const { _aspectRatio, _localParticipantId, _participants, _visible } = this.props;
if (!_visible) { if (!_visible) {
return null; return null;
@ -92,6 +211,13 @@ class Filmstrip extends Component<Props> {
const isNarrowAspectRatio = _aspectRatio === ASPECT_RATIO_NARROW; const isNarrowAspectRatio = _aspectRatio === ASPECT_RATIO_NARROW;
const filmstripStyle = isNarrowAspectRatio ? styles.filmstripNarrow : styles.filmstripWide; 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 ( return (
<SafeAreaView style = { filmstripStyle }> <SafeAreaView style = { filmstripStyle }>
@ -100,29 +226,20 @@ class Filmstrip extends Component<Props> {
&& !isNarrowAspectRatio && !isNarrowAspectRatio
&& <LocalThumbnail /> && <LocalThumbnail />
} }
<ScrollView <FlatList
data = { participants }
getItemLayout = { this._getItemLayout }
horizontal = { isNarrowAspectRatio } horizontal = { isNarrowAspectRatio }
initialNumToRender = { initialNumToRender }
key = { isNarrowAspectRatio ? 'narrow' : 'wide' }
keyExtractor = { this._keyExtractor }
onViewableItemsChanged = { this._onViewableItemsChanged }
renderItem = { this._renderThumbnail }
showsHorizontalScrollIndicator = { false } showsHorizontalScrollIndicator = { false }
showsVerticalScrollIndicator = { false } showsVerticalScrollIndicator = { false }
style = { styles.scrollView } > style = { styles.scrollView }
{ viewabilityConfig = { this._viewabilityConfig }
!this._separateLocalThumbnail && !isNarrowAspectRatio windowSize = { 2 } />
&& <LocalThumbnail />
}
{
this._sort(_participants, isNarrowAspectRatio)
.map(id => (
<Thumbnail
key = { id }
participantID = { id } />))
}
{
!this._separateLocalThumbnail && isNarrowAspectRatio
&& <LocalThumbnail />
}
</ScrollView>
{ {
this._separateLocalThumbnail && isNarrowAspectRatio this._separateLocalThumbnail && isNarrowAspectRatio
&& <LocalThumbnail /> && <LocalThumbnail />
@ -130,35 +247,6 @@ class Filmstrip extends Component<Props> {
</SafeAreaView> </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) { function _mapStateToProps(state) {
const { enabled, remoteParticipants } = state['features/filmstrip']; const { enabled, remoteParticipants } = state['features/filmstrip'];
const showRemoteVideos = shouldRemoteVideosBeVisible(state); const showRemoteVideos = shouldRemoteVideosBeVisible(state);
const responsiveUI = state['features/base/responsive-ui'];
return { return {
_aspectRatio: state['features/base/responsive-ui'].aspectRatio, _aspectRatio: state['features/base/responsive-ui'].aspectRatio,
_clientHeight: responsiveUI.clientHeight,
_clientWidth: responsiveUI.clientWidth,
_localParticipantId: getLocalParticipant(state)?.id,
_participants: showRemoteVideos ? remoteParticipants : NO_REMOTE_VIDEOS, _participants: showRemoteVideos ? remoteParticipants : NO_REMOTE_VIDEOS,
_visible: enabled && isFilmstripVisible(state) _visible: enabled && isFilmstripVisible(state)
}; };

View File

@ -1,6 +1,6 @@
// @flow // @flow
import React, { useCallback } from 'react'; import React, { PureComponent } from 'react';
import { View } from 'react-native'; import { View } from 'react-native';
import type { Dispatch } from 'redux'; import type { Dispatch } from 'redux';
@ -25,8 +25,8 @@ import { DisplayNameLabel } from '../../../display-name';
import { toggleToolboxVisible } from '../../../toolbox/actions.native'; import { toggleToolboxVisible } from '../../../toolbox/actions.native';
import { RemoteVideoMenu } from '../../../video-menu'; import { RemoteVideoMenu } from '../../../video-menu';
import ConnectionStatusComponent from '../../../video-menu/components/native/ConnectionStatusComponent'; import ConnectionStatusComponent from '../../../video-menu/components/native/ConnectionStatusComponent';
import SharedVideoMenu import SharedVideoMenu from '../../../video-menu/components/native/SharedVideoMenu';
from '../../../video-menu/components/native/SharedVideoMenu'; import { SQUARE_TILE_ASPECT_RATIO } from '../../constants';
import AudioMutedIndicator from './AudioMutedIndicator'; import AudioMutedIndicator from './AudioMutedIndicator';
import DominantSpeakerIndicator from './DominantSpeakerIndicator'; import DominantSpeakerIndicator from './DominantSpeakerIndicator';
@ -47,9 +47,19 @@ type Props = {
_audioMuted: boolean, _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. * Shared video local participant owner.
@ -57,9 +67,22 @@ type Props = {
_localVideoOwner: boolean, _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. * Whether to show the dominant speaker indicator or not.
@ -77,9 +100,9 @@ type Props = {
_styles: StyleType, _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 * If true, there will be no color overlay (tint) on the thumbnail
@ -93,6 +116,11 @@ type Props = {
*/ */
dispatch: Dispatch<any>, dispatch: Dispatch<any>,
/**
* The height of the thumnail.
*/
height: ?number,
/** /**
* The ID of the participant related to the thumbnail. * The ID of the participant related to the thumbnail.
*/ */
@ -103,11 +131,6 @@ type Props = {
*/ */
renderDisplayName: ?boolean, 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. * 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. * React component for video thumbnail.
*
* @param {Props} props - Properties passed to this functional component.
* @returns {Component} - A React component.
*/ */
function Thumbnail(props: Props) { class Thumbnail extends PureComponent<Props> {
const {
_audioMuted: audioMuted,
_largeVideo: largeVideo,
_localVideoOwner,
_renderDominantSpeakerIndicator: renderDominantSpeakerIndicator,
_renderModeratorIndicator: renderModeratorIndicator,
_participant: participant,
_styles,
_videoTrack: videoTrack,
dispatch,
disableTint,
renderDisplayName,
tileView
} = 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. * Creates new Thumbnail component.
if (typeof participant === 'undefined') { *
* @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; _onClick: () => void;
const participantInLargeVideo
= participantId === largeVideo.participantId; /**
const videoMuted = !videoTrack || videoTrack.muted; * Thumbnail click handler.
const isScreenShare = videoTrack && videoTrack.videoType === VIDEO_TYPE.DESKTOP; *
const onClick = useCallback(() => { * @returns {void}
*/
_onClick() {
const { _participantId, _pinned, dispatch, tileView } = this.props;
if (tileView) { if (tileView) {
dispatch(toggleToolboxVisible()); dispatch(toggleToolboxVisible());
} else { } 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, { dispatch(openDialog(ConnectionStatusComponent, {
participantID: participant.id participantID: _participantId
})); }));
} else if (participant.isFakeParticipant) { } else if (_isFakeParticipant) {
if (_localVideoOwner) { if (_localVideoOwner) {
dispatch(openDialog(SharedVideoMenu, { dispatch(openDialog(SharedVideoMenu, {
participant _participantId
})); }));
} }
} else { } else {
dispatch(openDialog(RemoteVideoMenu, { dispatch(openDialog(RemoteVideoMenu, {
participant participantId: _participantId
})); }));
} }
}, [ participant, dispatch ]); }
/**
* 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;
return ( return (
<Container <Container
onClick = { onClick } onClick = { this._onClick }
onLongPress = { onThumbnailLongPress } onLongPress = { this._onThumbnailLongPress }
style = { [ style = { [
styles.thumbnail, styles.thumbnail,
participant.pinned && !tileView _pinned && !tileView ? _styles.thumbnailPinned : null,
? _styles.thumbnailPinned : null, styleOverrides
props.styleOverrides || null
] } ] }
touchFeedback = { false }> touchFeedback = { false }>
<ParticipantView <ParticipantView
avatarSize = { tileView ? AVATAR_SIZE * 1.5 : AVATAR_SIZE } avatarSize = { tileView ? AVATAR_SIZE * 1.5 : AVATAR_SIZE }
disableVideo = { isScreenShare || participant.isFakeParticipant } disableVideo = { isScreenShare || _isFakeParticipant }
participantId = { participantId } participantId = { participantId }
style = { _styles.participantViewStyle } style = { _styles.participantViewStyle }
tintEnabled = { participantInLargeVideo && !disableTint } tintEnabled = { participantInLargeVideo && !disableTint }
tintStyle = { _styles.activeThumbnailTint } tintStyle = { _styles.activeThumbnailTint }
zOrder = { 1 } /> zOrder = { 1 } />
{
{ renderDisplayName && <Container style = { styles.displayNameContainer }> renderDisplayName
&& <Container style = { styles.displayNameContainer }>
<DisplayNameLabel participantId = { participantId } /> <DisplayNameLabel participantId = { participantId } />
</Container> } </Container>
}
{ renderModeratorIndicator { renderModeratorIndicator
&& <View style = { styles.moderatorIndicatorContainer }> && <View style = { styles.moderatorIndicatorContainer }>
<ModeratorIndicator /> <ModeratorIndicator />
</View>} </View>
}
{ !participant.isFakeParticipant && <View {
!_isFakeParticipant
&& <View
style = { [ style = { [
styles.thumbnailTopIndicatorContainer, styles.thumbnailTopIndicatorContainer,
styles.thumbnailTopLeftIndicatorContainer styles.thumbnailTopLeftIndicatorContainer
] }> ] }>
<RaisedHandIndicator participantId = { participant.id } /> <RaisedHandIndicator participantId = { participantId } />
{ renderDominantSpeakerIndicator && <DominantSpeakerIndicator /> } { renderDominantSpeakerIndicator && <DominantSpeakerIndicator /> }
</View> } </View>
}
{ !participant.isFakeParticipant && <View {
!_isFakeParticipant
&& <View
style = { [ style = { [
styles.thumbnailTopIndicatorContainer, styles.thumbnailTopIndicatorContainer,
styles.thumbnailTopRightIndicatorContainer styles.thumbnailTopRightIndicatorContainer
] }> ] }>
<ConnectionIndicator participantId = { participant.id } /> <ConnectionIndicator participantId = { participantId } />
</View> } </View>
}
{ !participant.isFakeParticipant && <Container style = { styles.thumbnailIndicatorContainer }> {
{ audioMuted !_isFakeParticipant
&& <AudioMutedIndicator /> } && <Container style = { styles.thumbnailIndicatorContainer }>
{ videoMuted { audioMuted && <AudioMutedIndicator /> }
&& <VideoMutedIndicator /> } { videoMuted && <VideoMutedIndicator /> }
{ isScreenShare { isScreenShare && <ScreenShareIndicator /> }
&& <ScreenShareIndicator /> } </Container>
</Container> } }
</Container> </Container>
); );
}
} }
/** /**
@ -255,21 +316,28 @@ function _mapStateToProps(state, ownProps) {
= getTrackByMediaTypeAndParticipant(tracks, MEDIA_TYPE.AUDIO, id); = getTrackByMediaTypeAndParticipant(tracks, MEDIA_TYPE.AUDIO, id);
const videoTrack const videoTrack
= getTrackByMediaTypeAndParticipant(tracks, MEDIA_TYPE.VIDEO, id); = getTrackByMediaTypeAndParticipant(tracks, MEDIA_TYPE.VIDEO, id);
const videoMuted = !videoTrack || videoTrack.muted;
const isScreenShare = videoTrack && videoTrack.videoType === VIDEO_TYPE.DESKTOP;
const participantCount = getParticipantCount(state); const participantCount = getParticipantCount(state);
const renderDominantSpeakerIndicator = participant && participant.dominantSpeaker && participantCount > 2; const renderDominantSpeakerIndicator = participant && participant.dominantSpeaker && participantCount > 2;
const _isEveryoneModerator = isEveryoneModerator(state); const _isEveryoneModerator = isEveryoneModerator(state);
const renderModeratorIndicator = !_isEveryoneModerator const renderModeratorIndicator = !_isEveryoneModerator
&& participant && participant.role === PARTICIPANT_ROLE.MODERATOR; && participant && participant.role === PARTICIPANT_ROLE.MODERATOR;
const participantInLargeVideo = id === largeVideo.participantId;
return { return {
_audioMuted: audioTrack?.muted ?? true, _audioMuted: audioTrack?.muted ?? true,
_largeVideo: largeVideo, _isScreenShare: isScreenShare,
_isFakeParticipant: participant?.isFakeParticipant,
_local: participant?.local,
_localVideoOwner: Boolean(ownerId === localParticipantId), _localVideoOwner: Boolean(ownerId === localParticipantId),
_participant: participant, _participantInLargeVideo: participantInLargeVideo,
_participantId: id,
_pinned: participant?.pinned,
_renderDominantSpeakerIndicator: renderDominantSpeakerIndicator, _renderDominantSpeakerIndicator: renderDominantSpeakerIndicator,
_renderModeratorIndicator: renderModeratorIndicator, _renderModeratorIndicator: renderModeratorIndicator,
_styles: ColorSchemeRegistry.get(state, 'Thumbnail'), _styles: ColorSchemeRegistry.get(state, 'Thumbnail'),
_videoTrack: videoTrack _videoMuted: videoMuted
}; };
} }

View File

@ -1,8 +1,8 @@
// @flow // @flow
import React, { Component } from 'react'; import React, { PureComponent } from 'react';
import { import {
ScrollView, FlatList,
TouchableWithoutFeedback, TouchableWithoutFeedback,
View View
} from 'react-native'; } from 'react-native';
@ -10,13 +10,11 @@ import type { Dispatch } from 'redux';
import { getLocalParticipant, getParticipantCountWithFake } from '../../../base/participants'; import { getLocalParticipant, getParticipantCountWithFake } from '../../../base/participants';
import { connect } from '../../../base/redux'; import { connect } from '../../../base/redux';
import { ASPECT_RATIO_NARROW } from '../../../base/responsive-ui/constants'; import { setVisibleRemoteParticipants } from '../../actions.web';
import { setTileViewDimensions } from '../../actions.native';
import Thumbnail from './Thumbnail'; import Thumbnail from './Thumbnail';
import styles from './styles'; import styles from './styles';
/** /**
* The type of the React {@link Component} props of {@link TileView}. * The type of the React {@link Component} props of {@link TileView}.
*/ */
@ -27,6 +25,11 @@ type Props = {
*/ */
_aspectRatio: Symbol, _aspectRatio: Symbol,
/**
* The number of columns.
*/
_columns: number,
/** /**
* Application's viewport height. * Application's viewport height.
*/ */
@ -47,6 +50,11 @@ type Props = {
*/ */
_remoteParticipants: Array<string>, _remoteParticipants: Array<string>,
/**
* The thumbnail height.
*/
_thumbnailHeight: number,
/** /**
* Application's viewport height. * Application's viewport height.
*/ */
@ -64,45 +72,81 @@ type Props = {
}; };
/** /**
* The margin for each side of the tile view. Taken away from the available * Implements a React {@link PureComponent} which displays thumbnails in a two
* 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. * dimensional grid.
* *
* @extends Component * @extends PureComponent
*/ */
class TileView extends Component<Props> { class TileView extends PureComponent<Props> {
/**
* Implements React's {@link Component#componentDidMount}.
*
* @inheritdoc
*/
componentDidMount() {
this._updateReceiverQuality();
}
/** /**
* Implements React's {@link Component#componentDidUpdate}. * The FlatList's viewabilityConfig.
*
* @inheritdoc
*/ */
componentDidUpdate() { _viewabilityConfig: Object;
this._updateReceiverQuality();
/**
* 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} * @returns {ReactElement}
*/ */
render() { render() {
const { _height, _width, onClick } = this.props; const { _columns, _height, _thumbnailHeight, _width, onClick } = this.props;
const rowElements = this._groupIntoRows(this._renderThumbnails(), this._getColumnCount()); const participants = this._getSortedParticipants();
const initialRowsToRender = Math.ceil(_height / (_thumbnailHeight + (2 * styles.thumbnail.margin)));
return ( if (this._flatListStyles.minHeight !== _height || this._flatListStyles.minWidth !== _width) {
<ScrollView this._flatListStyles = {
style = {{ ...styles.flatList,
...styles.tileView,
height: _height,
width: _width
}}>
<TouchableWithoutFeedback onPress = { onClick }>
<View
style = {{
...styles.tileViewRows,
minHeight: _height, minHeight: _height,
minWidth: _width minWidth: _width
}}> };
{ rowElements } }
if (this._contentContainerStyles.minHeight !== _height || this._contentContainerStyles.minWidth !== _width) {
this._contentContainerStyles = {
...styles.contentContainer,
minHeight: _height,
minWidth: _width
};
}
return (
<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> </View>
</TouchableWithoutFeedback> </TouchableWithoutFeedback>
</ScrollView>
); );
} }
/**
* 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. * Returns all participants with the local participant at the end.
* *
@ -168,114 +207,33 @@ class TileView extends Component<Props> {
*/ */
_getSortedParticipants() { _getSortedParticipants() {
const { _localParticipant, _remoteParticipants } = this.props; const { _localParticipant, _remoteParticipants } = this.props;
const participants = [ ..._remoteParticipants ]; const participants = [];
_localParticipant && participants.push(_localParticipant.id); _localParticipant && participants.push(_localParticipant.id);
return participants; return [ ...participants, ..._remoteParticipants ];
} }
_renderThumbnail: Object => Object;
/** /**
* Calculate the height and width for the tiles. * Creates React Element to display each participant in a thumbnail.
* *
* @private * @private
* @returns {Object} * @returns {ReactElement}
*/ */
_getTileDimensions() { _renderThumbnail({ item/* , index , separators */ }) {
const { _height, _participantCount, _width } = this.props; const { _thumbnailHeight } = 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 return (
// 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;
}
/**
* 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,
maxHeight: null,
maxWidth: null,
width: null
};
return this._getSortedParticipants()
.map(id => (
<Thumbnail <Thumbnail
disableTint = { true } disableTint = { true }
key = { id } height = { _thumbnailHeight }
participantID = { id } key = { item }
participantID = { item }
renderDisplayName = { true } renderDisplayName = { true }
styleOverrides = { styleOverrides } tileView = { true } />)
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
}
}));
} }
} }
@ -288,14 +246,18 @@ class TileView extends Component<Props> {
*/ */
function _mapStateToProps(state) { function _mapStateToProps(state) {
const responsiveUi = state['features/base/responsive-ui']; 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 { return {
_aspectRatio: responsiveUi.aspectRatio, _aspectRatio: responsiveUi.aspectRatio,
_columns: columns,
_height: responsiveUi.clientHeight, _height: responsiveUi.clientHeight,
_localParticipant: getLocalParticipant(state), _localParticipant: getLocalParticipant(state),
_participantCount: getParticipantCountWithFake(state), _participantCount: getParticipantCountWithFake(state),
_remoteParticipants: remoteParticipants, _remoteParticipants: remoteParticipants,
_thumbnailHeight: height,
_width: responsiveUi.clientWidth _width: responsiveUi.clientWidth
}; };
} }

View File

@ -14,6 +14,15 @@ export const AVATAR_SIZE = 50;
*/ */
export default { export default {
/**
* The FlatList content container styles
*/
contentContainer: {
alignItems: 'center',
justifyContent: 'center',
flex: 0
},
/** /**
* The display name container. * The display name container.
*/ */
@ -52,6 +61,22 @@ export default {
top: 0 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}. * Container of the {@link LocalThumbnail}.
*/ */
@ -122,19 +147,6 @@ export default {
thumbnailTopRightIndicatorContainer: { thumbnailTopRightIndicatorContainer: {
right: 0 right: 0
},
tileView: {
alignSelf: 'center'
},
tileViewRows: {
justifyContent: 'center'
},
tileViewRow: {
flexDirection: 'row',
justifyContent: 'center'
} }
}; };

View File

@ -220,3 +220,14 @@ export const HORIZONTAL_FILMSTRIP_MARGIN = 39;
* @type {number} * @type {number}
*/ */
export const SHOW_TOOLBAR_CONTEXT_MENU_AFTER = 600; 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;

View File

@ -3,6 +3,7 @@
import { getFeatureFlag, FILMSTRIP_ENABLED } from '../base/flags'; import { getFeatureFlag, FILMSTRIP_ENABLED } from '../base/flags';
import { getParticipantCountWithFake, getPinnedParticipant } from '../base/participants'; import { getParticipantCountWithFake, getPinnedParticipant } from '../base/participants';
import { toState } from '../base/redux'; import { toState } from '../base/redux';
import { ASPECT_RATIO_NARROW } from '../base/responsive-ui/constants';
export * from './functions.any'; export * from './functions.any';
@ -59,3 +60,31 @@ export function shouldRemoteVideosBeVisible(state: Object) {
|| disable1On1Mode); || 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);
}

View File

@ -2,7 +2,9 @@
import { PARTICIPANT_JOINED, PARTICIPANT_LEFT } from '../base/participants'; import { PARTICIPANT_JOINED, PARTICIPANT_LEFT } from '../base/participants';
import { MiddlewareRegistry } from '../base/redux'; 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 { updateRemoteParticipants, updateRemoteParticipantsOnLeave } from './functions';
import './subscriber'; import './subscriber';
@ -10,19 +12,22 @@ import './subscriber';
* The middleware of the feature Filmstrip. * The middleware of the feature Filmstrip.
*/ */
MiddlewareRegistry.register(store => next => action => { MiddlewareRegistry.register(store => next => action => {
if (action.type === PARTICIPANT_LEFT) {
updateRemoteParticipantsOnLeave(store, action.participant?.id);
}
const result = next(action); const result = next(action);
switch (action.type) { switch (action.type) {
case CLIENT_RESIZED:
case SET_ASPECT_RATIO:
store.dispatch(setTileViewDimensions());
break;
case PARTICIPANT_JOINED: { case PARTICIPANT_JOINED: {
updateRemoteParticipants(store); updateRemoteParticipants(store);
break; break;
} }
case PARTICIPANT_LEFT: {
updateRemoteParticipantsOnLeave(store, action.participant?.id);
break;
}
} }
return result; return result;
}); });

View File

@ -118,13 +118,9 @@ ReducerRegistry.register(
}; };
case SET_REMOTE_PARTICIPANTS: { case SET_REMOTE_PARTICIPANTS: {
state.remoteParticipants = action.participants; state.remoteParticipants = action.participants;
// TODO: implement this on mobile.
if (navigator.product !== 'ReactNative') {
const { visibleParticipantsStartIndex: startIndex, visibleParticipantsEndIndex: endIndex } = state; 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 }; return { ...state };
} }
@ -167,7 +163,9 @@ ReducerRegistry.register(
} }
delete state.participantsVolume[id]; delete state.participantsVolume[id];
return state; return {
...state
};
} }
} }

View File

@ -1,3 +1,42 @@
// @flow // @flow
import { getParticipantCountWithFake } from '../base/participants';
import { StateListenerRegistry } from '../base/redux';
import { getTileViewGridDimensions, shouldDisplayTileView } from '../video-layout';
import { setTileViewDimensions } from './actions';
import './subscriber.any'; 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)));
}
});

View File

@ -439,7 +439,7 @@ export function isDialOutEnabled(state: Object): boolean {
*/ */
export function isSipInviteEnabled(state: Object): boolean { export function isSipInviteEnabled(state: Object): boolean {
const { sipInviteUrl } = state['features/base/config']; const { sipInviteUrl } = state['features/base/config'];
const { features = {} } = getLocalParticipant(state); const { features = {} } = getLocalParticipant(state) || {};
return state['features/base/jwt'].jwt return state['features/base/jwt'].jwt
&& Boolean(sipInviteUrl) && Boolean(sipInviteUrl)

View File

@ -42,9 +42,9 @@ type Props = {
dispatch: Function, 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. * The color-schemed stylesheet of the BottomSheet.
@ -79,12 +79,7 @@ type Props = {
/** /**
* Display name of the participant retrieved from Redux. * Display name of the participant retrieved from Redux.
*/ */
_participantDisplayName: string, _participantDisplayName: string
/**
* The ID of the participant.
*/
_participantID: ?string,
} }
// eslint-disable-next-line prefer-const // eslint-disable-next-line prefer-const
@ -117,12 +112,12 @@ class RemoteVideoMenu extends PureComponent<Props> {
_disableRemoteMute, _disableRemoteMute,
_disableGrantModerator, _disableGrantModerator,
_isParticipantAvailable, _isParticipantAvailable,
participant participantId
} = this.props; } = this.props;
const buttonProps = { const buttonProps = {
afterClick: this._onCancel, afterClick: this._onCancel,
showLabel: true, showLabel: true,
participantID: participant.id, participantID: participantId,
styles: this.props._bottomSheetStyles.buttons styles: this.props._bottomSheetStyles.buttons
}; };
@ -141,7 +136,7 @@ class RemoteVideoMenu extends PureComponent<Props> {
<PrivateMessageButton { ...buttonProps } /> <PrivateMessageButton { ...buttonProps } />
<ConnectionStatusButton { ...buttonProps } /> <ConnectionStatusButton { ...buttonProps } />
{/* <Divider style = { styles.divider } />*/} {/* <Divider style = { styles.divider } />*/}
{/* <VolumeSlider participantID = { _participantID } />*/} {/* <VolumeSlider participantID = { participantId } />*/}
</BottomSheet> </BottomSheet>
); );
} }
@ -172,7 +167,7 @@ class RemoteVideoMenu extends PureComponent<Props> {
* @returns {React$Element} * @returns {React$Element}
*/ */
_renderMenuHeader() { _renderMenuHeader() {
const { _bottomSheetStyles, participant } = this.props; const { _bottomSheetStyles, participantId } = this.props;
return ( return (
<View <View
@ -180,7 +175,7 @@ class RemoteVideoMenu extends PureComponent<Props> {
_bottomSheetStyles.sheet, _bottomSheetStyles.sheet,
styles.participantNameContainer ] }> styles.participantNameContainer ] }>
<Avatar <Avatar
participantId = { participant.id } participantId = { participantId }
size = { AVATAR_SIZE } /> size = { AVATAR_SIZE } />
<Text style = { styles.participantNameLabel }> <Text style = { styles.participantNameLabel }>
{ this.props._participantDisplayName } { this.props._participantDisplayName }
@ -200,9 +195,9 @@ class RemoteVideoMenu extends PureComponent<Props> {
*/ */
function _mapStateToProps(state, ownProps) { function _mapStateToProps(state, ownProps) {
const kickOutEnabled = getFeatureFlag(state, KICK_OUT_ENABLED, true); const kickOutEnabled = getFeatureFlag(state, KICK_OUT_ENABLED, true);
const { participant } = ownProps; const { participantId } = ownProps;
const { remoteVideoMenu = {}, disableRemoteMute } = state['features/base/config']; const { remoteVideoMenu = {}, disableRemoteMute } = state['features/base/config'];
const isParticipantAvailable = getParticipantById(state, participant.id); const isParticipantAvailable = getParticipantById(state, participantId);
let { disableKick } = remoteVideoMenu; let { disableKick } = remoteVideoMenu;
disableKick = disableKick || !kickOutEnabled; disableKick = disableKick || !kickOutEnabled;
@ -213,8 +208,7 @@ function _mapStateToProps(state, ownProps) {
_disableRemoteMute: Boolean(disableRemoteMute), _disableRemoteMute: Boolean(disableRemoteMute),
_isOpen: isDialogOpen(state, RemoteVideoMenu_), _isOpen: isDialogOpen(state, RemoteVideoMenu_),
_isParticipantAvailable: Boolean(isParticipantAvailable), _isParticipantAvailable: Boolean(isParticipantAvailable),
_participantDisplayName: getParticipantDisplayName(state, participant.id), _participantDisplayName: getParticipantDisplayName(state, participantId)
_participantID: participant.id
}; };
} }

View File

@ -32,9 +32,9 @@ type Props = {
dispatch: Function, 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. * The color-schemed stylesheet of the BottomSheet.
@ -55,11 +55,6 @@ type Props = {
* Display name of the participant retrieved from Redux. * Display name of the participant retrieved from Redux.
*/ */
_participantDisplayName: string, _participantDisplayName: string,
/**
* The ID of the participant.
*/
_participantID: ?string,
} }
// eslint-disable-next-line prefer-const // eslint-disable-next-line prefer-const
@ -89,13 +84,13 @@ class SharedVideoMenu extends PureComponent<Props> {
render() { render() {
const { const {
_isParticipantAvailable, _isParticipantAvailable,
participant participantId
} = this.props; } = this.props;
const buttonProps = { const buttonProps = {
afterClick: this._onCancel, afterClick: this._onCancel,
showLabel: true, showLabel: true,
participantID: participant.id, participantID: participantId,
styles: this.props._bottomSheetStyles.buttons styles: this.props._bottomSheetStyles.buttons
}; };
@ -136,7 +131,7 @@ class SharedVideoMenu extends PureComponent<Props> {
* @returns {React$Element} * @returns {React$Element}
*/ */
_renderMenuHeader() { _renderMenuHeader() {
const { _bottomSheetStyles, participant } = this.props; const { _bottomSheetStyles, participantId } = this.props;
return ( return (
<View <View
@ -144,7 +139,7 @@ class SharedVideoMenu extends PureComponent<Props> {
_bottomSheetStyles.sheet, _bottomSheetStyles.sheet,
styles.participantNameContainer ] }> styles.participantNameContainer ] }>
<Avatar <Avatar
participantId = { participant.id } participantId = { participantId }
size = { AVATAR_SIZE } /> size = { AVATAR_SIZE } />
<Text style = { styles.participantNameLabel }> <Text style = { styles.participantNameLabel }>
{ this.props._participantDisplayName } { this.props._participantDisplayName }
@ -163,15 +158,14 @@ class SharedVideoMenu extends PureComponent<Props> {
* @returns {Props} * @returns {Props}
*/ */
function _mapStateToProps(state, ownProps) { function _mapStateToProps(state, ownProps) {
const { participant } = ownProps; const { participantId } = ownProps;
const isParticipantAvailable = getParticipantById(state, participant.id); const isParticipantAvailable = getParticipantById(state, participantId);
return { return {
_bottomSheetStyles: ColorSchemeRegistry.get(state, 'BottomSheet'), _bottomSheetStyles: ColorSchemeRegistry.get(state, 'BottomSheet'),
_isOpen: isDialogOpen(state, SharedVideoMenu_), _isOpen: isDialogOpen(state, SharedVideoMenu_),
_isParticipantAvailable: Boolean(isParticipantAvailable), _isParticipantAvailable: Boolean(isParticipantAvailable),
_participantDisplayName: getParticipantDisplayName(state, participant.id), _participantDisplayName: getParticipantDisplayName(state, participantId)
_participantID: participant.id
}; };
} }

View File

@ -191,12 +191,7 @@ function _updateReceiverVideoConstraints({ getState }) {
const { maxReceiverVideoQuality, preferredVideoQuality } = state['features/video-quality']; const { maxReceiverVideoQuality, preferredVideoQuality } = state['features/video-quality'];
const { participantId: largeVideoParticipantId } = state['features/large-video']; const { participantId: largeVideoParticipantId } = state['features/large-video'];
const maxFrameHeight = Math.min(maxReceiverVideoQuality, preferredVideoQuality); const maxFrameHeight = Math.min(maxReceiverVideoQuality, preferredVideoQuality);
let { visibleRemoteParticipants } = state['features/filmstrip']; const { 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 receiverConstraints = { const receiverConstraints = {
constraints: {}, constraints: {},