fix(rn,ParticipantPane) optimize

Use a FlatList to avoid loading all participants at once.
This commit is contained in:
Hristo Terezov 2021-09-01 18:13:16 -05:00 committed by Saúl Ibarra Corretgé
parent 221cf67d0a
commit dfd53f4041
5 changed files with 254 additions and 126 deletions

View File

@ -29,28 +29,28 @@ export function showContextMenuReject(participant: Object) {
* @param {string} participantID - The selected meeting participant id. * @param {string} participantID - The selected meeting participant id.
* @returns {Function} * @returns {Function}
*/ */
export function showConnectionStatus(participantID: String) { export function showConnectionStatus(participantID: string) {
return openDialog(ConnectionStatusComponent, { participantID }); return openDialog(ConnectionStatusComponent, { participantID });
} }
/** /**
* Displays the context menu for the selected meeting participant. * Displays the context menu for the selected meeting participant.
* *
* @param {Object} participant - The selected meeting participant. * @param {string} participantId - The ID of the selected meeting participant.
* @returns {Function} * @returns {Function}
*/ */
export function showContextMenuDetails(participant: Object) { export function showContextMenuDetails(participantId: string) {
return openDialog(RemoteVideoMenu, { participant }); return openDialog(RemoteVideoMenu, { participantId });
} }
/** /**
* Displays the shared video menu. * Displays the shared video menu.
* *
* @param {Object} participant - The selected meeting participant. * @param {string} participantId - The ID of the selected meeting participant.
* @returns {Function} * @returns {Function}
*/ */
export function showSharedVideoMenu(participant: Object) { export function showSharedVideoMenu(participantId: string) {
return openDialog(SharedVideoMenu, { participant }); return openDialog(SharedVideoMenu, { participantId });
} }
/** /**

View File

@ -2,7 +2,7 @@
import React, { useCallback } from 'react'; import React, { useCallback } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { Text, View } from 'react-native'; import { ScrollView, Text, View } from 'react-native';
import { Button, withTheme } from 'react-native-paper'; import { Button, withTheme } from 'react-native-paper';
import { useDispatch, useSelector } from 'react-redux'; import { useDispatch, useSelector } from 'react-redux';
@ -54,6 +54,7 @@ const LobbyParticipantList = ({ theme }: Props) => {
) )
} }
</View> </View>
<ScrollView>
{ {
participants.map(p => ( participants.map(p => (
<LobbyParticipantItem <LobbyParticipantItem
@ -61,6 +62,7 @@ const LobbyParticipantList = ({ theme }: Props) => {
participant = { p } />) participant = { p } />)
) )
} }
</ScrollView>
</View> </View>
); );
}; };

View File

@ -1,9 +1,10 @@
// @flow // @flow
import React from 'react'; import React, { PureComponent } from 'react';
import { translate } from '../../../base/i18n'; import { translate } from '../../../base/i18n';
import { import {
getLocalParticipant,
getParticipantByIdOrUndefined, getParticipantByIdOrUndefined,
getParticipantDisplayName getParticipantDisplayName
} from '../../../base/participants'; } from '../../../base/participants';
@ -12,6 +13,7 @@ import {
isParticipantAudioMuted, isParticipantAudioMuted,
isParticipantVideoMuted isParticipantVideoMuted
} from '../../../base/tracks'; } from '../../../base/tracks';
import { showConnectionStatus, showContextMenuDetails, showSharedVideoMenu } from '../../actions.native';
import { MEDIA_STATE } from '../../constants'; import { MEDIA_STATE } from '../../constants';
import type { MediaState } from '../../constants'; import type { MediaState } from '../../constants';
import { getParticipantAudioMediaState } from '../../functions'; import { getParticipantAudioMediaState } from '../../functions';
@ -31,6 +33,11 @@ type Props = {
*/ */
_displayName: string, _displayName: string,
/**
* True if the participant is fake.
*/
_isFakeParticipant: boolean,
/** /**
* True if the participant is video muted. * True if the participant is video muted.
*/ */
@ -41,6 +48,11 @@ type Props = {
*/ */
_local: boolean, _local: boolean,
/**
* Shared video local participant owner.
*/
_localVideoOwner: boolean,
/** /**
* The participant ID. * The participant ID.
*/ */
@ -52,9 +64,9 @@ type Props = {
_raisedHand: boolean, _raisedHand: boolean,
/** /**
* Callback to invoke when item is pressed. * The redux dispatch function.
*/ */
onPress: Function, dispatch: Function,
/** /**
* The ID of the participant. * The ID of the participant.
@ -64,30 +76,75 @@ type Props = {
/** /**
* Implements the MeetingParticipantItem component. * Implements the MeetingParticipantItem component.
*/
class MeetingParticipantItem extends PureComponent<Props> {
/**
* Creates new MeetingParticipantItem instance.
* *
* @param {Props} props - The props of the component. * @param {Props} props - The props of the component.
*/
constructor(props: Props) {
super(props);
this._onPress = this._onPress.bind(this);
}
_onPress: () => void;
/**
* Handles MeetingParticipantItem press events.
*
* @returns {void}
*/
_onPress() {
const {
_local,
_localVideoOwner,
_isFakeParticipant,
_participantID,
dispatch
} = this.props;
if (_isFakeParticipant && _localVideoOwner) {
dispatch(showSharedVideoMenu(_participantID));
} else if (!_isFakeParticipant) {
if (_local) {
dispatch(showConnectionStatus(_participantID));
} else {
dispatch(showContextMenuDetails(_participantID));
}
} // else no-op
}
/**
* Implements React's {@link Component#render()}.
*
* @inheritdoc
* @returns {ReactElement} * @returns {ReactElement}
*/ */
function MeetingParticipantItem({ render() {
const {
_audioMediaState, _audioMediaState,
_displayName, _displayName,
_isVideoMuted, _isVideoMuted,
_local, _local,
_participantID, _participantID,
_raisedHand, _raisedHand
onPress } = this.props;
}: Props) {
return ( return (
<ParticipantItem <ParticipantItem
audioMediaState = { _audioMediaState } audioMediaState = { _audioMediaState }
displayName = { _displayName } displayName = { _displayName }
isKnockingParticipant = { false } isKnockingParticipant = { false }
local = { _local } local = { _local }
onPress = { onPress } onPress = { this._onPress }
participantID = { _participantID } participantID = { _participantID }
raisedHand = { _raisedHand } raisedHand = { _raisedHand }
videoMediaState = { _isVideoMuted ? MEDIA_STATE.MUTED : MEDIA_STATE.UNMUTED } /> videoMediaState = { _isVideoMuted ? MEDIA_STATE.MUTED : MEDIA_STATE.UNMUTED } />
); );
}
} }
/** /**
@ -100,19 +157,21 @@ function MeetingParticipantItem({
*/ */
function mapStateToProps(state, ownProps): Object { function mapStateToProps(state, ownProps): Object {
const { participantID } = ownProps; const { participantID } = ownProps;
const { ownerId } = state['features/shared-video'];
const localParticipantId = getLocalParticipant(state).id;
const participant = getParticipantByIdOrUndefined(state, participantID); const participant = getParticipantByIdOrUndefined(state, participantID);
const _isAudioMuted = isParticipantAudioMuted(participant, state); const _isAudioMuted = isParticipantAudioMuted(participant, state);
const isVideoMuted = isParticipantVideoMuted(participant, state); const isVideoMuted = isParticipantVideoMuted(participant, state);
const audioMediaState = getParticipantAudioMediaState( const audioMediaState = getParticipantAudioMediaState(participant, _isAudioMuted, state);
participant, _isAudioMuted, state
);
return { return {
_audioMediaState: audioMediaState, _audioMediaState: audioMediaState,
_displayName: getParticipantDisplayName(state, participant?.id), _displayName: getParticipantDisplayName(state, participant?.id),
_isAudioMuted, _isAudioMuted,
_isFakeParticipant: Boolean(participant?.isFakeParticipant),
_isVideoMuted: isVideoMuted, _isVideoMuted: isVideoMuted,
_local: Boolean(participant?.local), _local: Boolean(participant?.local),
_localVideoOwner: Boolean(ownerId === localParticipantId),
_participantID: participant?.id, _participantID: participant?.id,
_raisedHand: Boolean(participant?.raisedHand) _raisedHand: Boolean(participant?.raisedHand)
}; };

View File

@ -1,25 +1,14 @@
// @flow // @flow
import React, { useCallback } from 'react'; import React, { PureComponent } from 'react';
import { useTranslation } from 'react-i18next'; import { FlatList, Text, View } from 'react-native';
import { Text, View } from 'react-native';
import { Button } from 'react-native-paper'; import { Button } from 'react-native-paper';
import { useDispatch, useSelector } from 'react-redux';
import { translate } from '../../../base/i18n';
import { Icon, IconInviteMore } from '../../../base/icons'; import { Icon, IconInviteMore } from '../../../base/icons';
import { import { getLocalParticipant, getParticipantCountWithFake } from '../../../base/participants';
getLocalParticipant,
getParticipantCountWithFake,
getSortedParticipants
} from '../../../base/participants';
import { connect } from '../../../base/redux'; import { connect } from '../../../base/redux';
import { doInvitePeople } from '../../../invite/actions.native'; import { doInvitePeople } from '../../../invite/actions.native';
import {
showConnectionStatus,
showContextMenuDetails,
showSharedVideoMenu
} from '../../actions.native';
import { shouldRenderInviteButton } from '../../functions'; import { shouldRenderInviteButton } from '../../functions';
import MeetingParticipantItem from './MeetingParticipantItem'; import MeetingParticipantItem from './MeetingParticipantItem';
@ -28,74 +17,150 @@ import styles from './styles';
type Props = { type Props = {
/** /**
* Shared video local participant owner. * The ID of the local participant.
*/ */
_localVideoOwner: boolean _localParticipantId: string,
/**
* The number of participants in the conference.
*/
_participantsCount: number,
/**
* Whether or not to show the invite button.
*/
_showInviteButton: boolean,
/**
* The remote participants.
*/
_sortedRemoteParticipants: Map<string, string>,
/**
* The redux dispatch function.
*/
dispatch: Function,
/**
* Translation function.
*/
t: Function
} }
const MeetingParticipantList = ({ _localVideoOwner }: Props) => { /**
const dispatch = useDispatch(); * The meeting participant list component.
const onInvite = useCallback(() => dispatch(doInvitePeople()), [ dispatch ]); */
const participantsCount = useSelector(getParticipantCountWithFake); class MeetingParticipantList extends PureComponent<Props> {
const sortedParticipants = useSelector(getSortedParticipants);
const showInviteButton = useSelector(shouldRenderInviteButton);
const { t } = useTranslation();
// eslint-disable-next-line react/no-multi-comp /**
const renderParticipant = p => { * Creates new MeetingParticipantList instance.
if (p.isFakeParticipant) { *
if (_localVideoOwner) { * @param {Props} props - The props of the component.
*/
constructor(props: Props) {
super(props);
this._keyExtractor = this._keyExtractor.bind(this);
this._onInvite = this._onInvite.bind(this);
this._renderParticipant = this._renderParticipant.bind(this);
}
_keyExtractor: Function;
/**
* 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;
}
_onInvite: () => void;
/**
* Handles ivite button presses.
*
* @returns {void}
*/
_onInvite() {
this.props.dispatch(doInvitePeople());
}
/**
* Renders the "invite more" icon.
*
* @returns {ReactElement}
*/
_renderInviteMoreIcon() {
return ( return (
<MeetingParticipantItem <Icon
key = { p.id } size = { 20 }
/* eslint-disable-next-line react/jsx-no-bind,no-confusing-arrow */ src = { IconInviteMore } />
onPress = { () => dispatch(showSharedVideoMenu(p)) }
participantID = { p.id } />
); );
} }
_renderParticipant: Object => Object;
/**
* Renders a participant.
*
* @param {Object} flatListItem - Information about the item to be rendered.
* @param {string} flatListItem.item - The ID of the participant.
* @returns {ReactElement}
*/
_renderParticipant({ item/* , index, separators */ }) {
return ( return (
<MeetingParticipantItem <MeetingParticipantItem
key = { p.id } key = { item }
participantID = { p.id } /> participantID = { item } />
); );
} }
return ( /**
<MeetingParticipantItem * Implements React's {@link Component#render()}.
key = { p.id } *
/* eslint-disable-next-line react/jsx-no-bind,no-confusing-arrow */ * @inheritdoc
onPress = { () => p.local * @returns {ReactElement}
? dispatch(showConnectionStatus(p.id)) : dispatch(showContextMenuDetails(p)) } */
participantID = { p.id } /> render() {
); const {
}; _localParticipantId,
_participantsCount,
_showInviteButton,
_sortedRemoteParticipants,
t
} = this.props;
return ( return (
<View style = { styles.meetingList }> <View style = { styles.meetingList }>
<Text style = { styles.meetingListDescription }> <Text style = { styles.meetingListDescription }>
{t('participantsPane.headings.participantsList', {t('participantsPane.headings.participantsList',
{ count: participantsCount })} { count: _participantsCount })}
</Text> </Text>
{ {
showInviteButton _showInviteButton
&& <Button && <Button
children = { t('participantsPane.actions.invite') } children = { t('participantsPane.actions.invite') }
/* eslint-disable-next-line react/jsx-no-bind */ icon = { this._renderInviteMoreIcon }
icon = { () =>
(<Icon
size = { 20 }
src = { IconInviteMore } />)
}
labelStyle = { styles.inviteLabel } labelStyle = { styles.inviteLabel }
mode = 'contained' mode = 'contained'
onPress = { onInvite } onPress = { this._onInvite }
style = { styles.inviteButton } /> style = { styles.inviteButton } />
} }
{ sortedParticipants.map(renderParticipant) } <FlatList
bounces = { false }
data = { [ _localParticipantId, ..._sortedRemoteParticipants ] }
horizontal = { false }
keyExtractor = { this._keyExtractor }
renderItem = { this._renderParticipant }
showsHorizontalScrollIndicator = { false }
windowSize = { 2 } />
</View> </View>
); );
}; }
}
/** /**
* Maps (parts of) the redux state to the associated props for this component. * Maps (parts of) the redux state to the associated props for this component.
@ -105,12 +170,16 @@ const MeetingParticipantList = ({ _localVideoOwner }: Props) => {
* @returns {Props} * @returns {Props}
*/ */
function _mapStateToProps(state): Object { function _mapStateToProps(state): Object {
const { ownerId } = state['features/shared-video']; const _participantsCount = getParticipantCountWithFake(state);
const localParticipantId = getLocalParticipant(state).id; const { remoteParticipants } = state['features/filmstrip'];
const _showInviteButton = shouldRenderInviteButton(state);
return { return {
_localVideoOwner: Boolean(ownerId === localParticipantId) _participantsCount,
_showInviteButton,
_sortedRemoteParticipants: remoteParticipants,
_localParticipantId: getLocalParticipant(state)?.id
}; };
} }
export default connect(_mapStateToProps)(MeetingParticipantList); export default translate(connect(_mapStateToProps)(MeetingParticipantList));

View File

@ -2,7 +2,7 @@
import React, { useCallback } from 'react'; import React, { useCallback } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { ScrollView, View } from 'react-native'; import { View } from 'react-native';
import { Button } from 'react-native-paper'; import { Button } from 'react-native-paper';
import { useDispatch, useSelector } from 'react-redux'; import { useDispatch, useSelector } from 'react-redux';
@ -45,10 +45,8 @@ const ParticipantsPane = () => {
}} }}
onClose = { closePane } onClose = { closePane }
style = { styles.participantsPane }> style = { styles.participantsPane }>
<ScrollView>
<LobbyParticipantList /> <LobbyParticipantList />
<MeetingParticipantList /> <MeetingParticipantList />
</ScrollView>
{ {
isLocalModerator isLocalModerator
&& <View style = { styles.footer }> && <View style = { styles.footer }>