feat(native-participants-pane) implemented review remarks pt. 1

This commit is contained in:
Calin Chitu 2021-06-29 17:05:11 +03:00 committed by Hristo Terezov
parent 36cb896680
commit c6e50ad439
14 changed files with 123 additions and 89 deletions

View File

@ -60,7 +60,15 @@ export function getRecordingSharingUrl(state: Object) {
return state['features/base/config'].recordingSharingUrl; return state['features/base/config'].recordingSharingUrl;
} }
/* eslint-disable max-params, no-shadow */ /**
* Selector used to get if the new participant is starting silent.
*
* @param {Object} state - The global state.
* @returns {string}
*/
export function getIsStartingSilent(state: Object) {
return Boolean(state['features/base/config'].startSilent);
}
/** /**
* Overrides JSON properties in {@code config} and * Overrides JSON properties in {@code config} and

View File

@ -56,9 +56,7 @@ class HeaderWithNavigation extends Component<Props> {
const { hideHeaderWithNavigation, onPressBack, onPressForward } = this.props; const { hideHeaderWithNavigation, onPressBack, onPressForward } = this.props;
return ( return (
<> !{ hideHeaderWithNavigation }
{
!hideHeaderWithNavigation
&& <Header> && <Header>
{ onPressBack && <BackButton onPress = { onPressBack } /> } { onPressBack && <BackButton onPress = { onPressBack } /> }
<HeaderLabel labelKey = { this.props.headerLabelKey } /> <HeaderLabel labelKey = { this.props.headerLabelKey } />
@ -67,8 +65,6 @@ class HeaderWithNavigation extends Component<Props> {
labelKey = { this.props.forwardLabelKey } labelKey = { this.props.forwardLabelKey }
onPress = { onPressForward } /> } onPress = { onPressForward } /> }
</Header> </Header>
}
</>
); );
} }
} }

View File

@ -75,7 +75,7 @@ type Props = AbstractProps & {
/** /**
* The indicator which determines if the participants pane is open. * The indicator which determines if the participants pane is open.
*/ */
_isOpen: boolean, _isParticipantsPaneOpen: boolean,
/** /**
* The ID of the participant currently on stage (if any) * The ID of the participant currently on stage (if any)
@ -243,7 +243,7 @@ class Conference extends AbstractConference<Props, *> {
_renderContent() { _renderContent() {
const { const {
_connecting, _connecting,
_isOpen, _isParticipantsPaneOpen,
_largeVideoParticipantId, _largeVideoParticipantId,
_reducedUI, _reducedUI,
_shouldDisplayTileView _shouldDisplayTileView
@ -309,7 +309,7 @@ class Conference extends AbstractConference<Props, *> {
{_shouldDisplayTileView && <Toolbox />} {_shouldDisplayTileView && <Toolbox />}
{ _isOpen && <ParticipantsPane /> } { _isParticipantsPaneOpen && <ParticipantsPane /> }
</> </>
); );
@ -423,7 +423,7 @@ function _mapStateToProps(state) {
_connecting: Boolean(connecting_), _connecting: Boolean(connecting_),
_filmstripVisible: isFilmstripVisible(state), _filmstripVisible: isFilmstripVisible(state),
_fullscreenEnabled: getFeatureFlag(state, FULLSCREEN_ENABLED, true), _fullscreenEnabled: getFeatureFlag(state, FULLSCREEN_ENABLED, true),
_isOpen: isOpen, _isParticipantsPaneOpen: isOpen,
_largeVideoParticipantId: state['features/large-video'].participantId, _largeVideoParticipantId: state['features/large-video'].participantId,
_pictureInPictureEnabled: getFeatureFlag(state, PIP_ENABLED), _pictureInPictureEnabled: getFeatureFlag(state, PIP_ENABLED),
_reducedUI: reducedUI, _reducedUI: reducedUI,

View File

@ -4,9 +4,10 @@ import React, { useCallback, useState } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { TouchableOpacity, View } from 'react-native'; import { TouchableOpacity, View } from 'react-native';
import { Divider, Text } from 'react-native-paper'; import { Divider, Text } from 'react-native-paper';
import { useDispatch, useSelector, useStore } from 'react-redux'; import { useDispatch, useSelector } from 'react-redux';
import { Avatar } from '../../../base/avatar'; import { Avatar } from '../../../base/avatar';
import { getIsStartingSilent } from '../../../base/config';
import { hideDialog, openDialog } from '../../../base/dialog/actions'; import { hideDialog, openDialog } from '../../../base/dialog/actions';
import BottomSheet from '../../../base/dialog/components/native/BottomSheet'; import BottomSheet from '../../../base/dialog/components/native/BottomSheet';
import { import {
@ -37,14 +38,14 @@ type Props = {
export const ContextMenuMeetingParticipantDetails = ({ participant: p }: Props) => { export const ContextMenuMeetingParticipantDetails = ({ participant: p }: Props) => {
const [ volume, setVolume ] = useState(undefined); const [ volume, setVolume ] = useState(undefined);
const store = useStore();
const startSilent = store.getState['features/base/config'];
const dispatch = useDispatch(); const dispatch = useDispatch();
const cancel = useCallback(() => dispatch(hideDialog()), [ dispatch ]); const cancel = useCallback(() => dispatch(hideDialog()), [ dispatch ]);
const changeVolume = useCallback(() => setVolume(volume), [ volume ]); const changeVolume = useCallback(() => setVolume(volume), [ setVolume, volume ]);
const displayName = p.name; const displayName = p.name;
const isLocalModerator = useSelector(isLocalParticipantModerator); const isLocalModerator = useSelector(isLocalParticipantModerator);
const isParticipantVideoMuted = useSelector(getIsParticipantVideoMuted(p)); const isParticipantVideoMuted = useSelector(getIsParticipantVideoMuted(p));
const isStartingSilent = useSelector(getIsStartingSilent);
const kickRemoteParticipant = useCallback(() => { const kickRemoteParticipant = useCallback(() => {
dispatch(openDialog(KickRemoteParticipantDialog, { dispatch(openDialog(KickRemoteParticipantDialog, {
participantID: p.id participantID: p.id
@ -65,7 +66,7 @@ export const ContextMenuMeetingParticipantDetails = ({ participant: p }: Props)
participantID: p.id participantID: p.id
})); }));
}, [ dispatch, p ]); }, [ dispatch, p ]);
const onVolumeChange = startSilent ? undefined : changeVolume; const onVolumeChange = isStartingSilent ? undefined : changeVolume;
const sendPrivateMessage = useCallback(() => { const sendPrivateMessage = useCallback(() => {
dispatch(hideDialog()); dispatch(hideDialog());
dispatch(openChat(p)); dispatch(openChat(p));

View File

@ -4,7 +4,7 @@ import React, { useCallback } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { TouchableOpacity } from 'react-native'; import { TouchableOpacity } from 'react-native';
import { Text } from 'react-native-paper'; import { Text } from 'react-native-paper';
import { useDispatch } from 'react-redux'; import { useDispatch, useSelector } from 'react-redux';
import { openDialog, hideDialog } from '../../../base/dialog/actions'; import { openDialog, hideDialog } from '../../../base/dialog/actions';
import BottomSheet from '../../../base/dialog/components/native/BottomSheet'; import BottomSheet from '../../../base/dialog/components/native/BottomSheet';
@ -12,26 +12,22 @@ import {
Icon, IconMicDisabledHollow, Icon, IconMicDisabledHollow,
IconVideoOff IconVideoOff
} from '../../../base/icons'; } from '../../../base/icons';
import { MEDIA_TYPE } from '../../../base/media'; import { getLocalParticipant } from '../../../base/participants';
import { BlockAudioVideoDialog } from '../../../video-menu'; import { BlockAudioVideoDialog } from '../../../video-menu';
import { import MuteEveryonesVideoDialog
muteAllParticipants from '../../../video-menu/components/native/MuteEveryonesVideoDialog';
} from '../../../video-menu/actions.any';
import styles from './styles'; import styles from './styles';
type Props = {
/** export const ContextMenuMore = () => {
* Array of participant IDs to not mute
*/
exclude: Array<string>
};
export const ContextMenuMore = ({ exclude }: Props) => {
const dispatch = useDispatch(); const dispatch = useDispatch();
const blockAudioVideo = useCallback(() => dispatch(openDialog(BlockAudioVideoDialog)), [ dispatch ]); const blockAudioVideo = useCallback(() => dispatch(openDialog(BlockAudioVideoDialog)), [ dispatch ]);
const cancel = useCallback(() => dispatch(hideDialog()), [ dispatch ]); const cancel = useCallback(() => dispatch(hideDialog()), [ dispatch ]);
const muteEveryoneVideo = useCallback(() => dispatch(muteAllParticipants(exclude, MEDIA_TYPE.VIDEO)), [ dispatch ]); const { id } = useSelector(getLocalParticipant);
const muteAllVideo = useCallback(() =>
dispatch(openDialog(MuteEveryonesVideoDialog,
{ exclude: [ id ] })),
[ dispatch ]);
const { t } = useTranslation(); const { t } = useTranslation();
return ( return (
@ -39,7 +35,7 @@ export const ContextMenuMore = ({ exclude }: Props) => {
onCancel = { cancel } onCancel = { cancel }
style = { styles.contextMenuMore }> style = { styles.contextMenuMore }>
<TouchableOpacity <TouchableOpacity
onPress = { muteEveryoneVideo } onPress = { muteAllVideo }
style = { styles.contextMenuItem }> style = { styles.contextMenuItem }>
<Icon <Icon
size = { 20 } size = { 20 }

View File

@ -5,9 +5,9 @@ import { useTranslation } from 'react-i18next';
import { Button } from 'react-native-paper'; import { Button } from 'react-native-paper';
import { useDispatch } from 'react-redux'; import { useDispatch } from 'react-redux';
import { setKnockingParticipantApproval } from '../../../lobby/actions.native'; import { approveKnockingParticipant } from '../../../lobby/actions.native';
import { showContextMenuReject } from '../../actions.native'; import { showContextMenuReject } from '../../actions.native';
import { MediaState } from '../../constants'; import { MEDIA_STATE } from '../../constants';
import ParticipantItem from './ParticipantItem'; import ParticipantItem from './ParticipantItem';
import styles from './styles'; import styles from './styles';
@ -22,18 +22,18 @@ type Props = {
export const LobbyParticipantItem = ({ participant: p }: Props) => { export const LobbyParticipantItem = ({ participant: p }: Props) => {
const dispatch = useDispatch(); const dispatch = useDispatch();
const admit = useCallback(() => dispatch(setKnockingParticipantApproval(p.id, true), [ dispatch ])); const admit = useCallback(() => dispatch(approveKnockingParticipant(p.id), [ dispatch ]));
const openContextMenuReject = useCallback(() => dispatch(showContextMenuReject(p), [ dispatch ])); const openContextMenuReject = useCallback(() => dispatch(showContextMenuReject(p), [ dispatch ]));
const { t } = useTranslation(); const { t } = useTranslation();
return ( return (
<ParticipantItem <ParticipantItem
audioMuteState = { MediaState.Muted } audioMediaState = { MEDIA_STATE.NONE }
isKnockingParticipant = { true } isKnockingParticipant = { true }
name = { p.name } name = { p.name }
onPress = { openContextMenuReject } onPress = { openContextMenuReject }
participant = { p } participant = { p }
videoMuteState = { MediaState.ForceMuted }> videoMediaState = { MEDIA_STATE.NONE }>
<Button <Button
children = { t('lobby.admit') } children = { t('lobby.admit') }
contentStyle = { styles.participantActionsButtonContent } contentStyle = { styles.participantActionsButtonContent }

View File

@ -6,8 +6,8 @@ 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 { useDispatch, useSelector } from 'react-redux';
import { admitMultiple } from '../../../lobby/actions.native';
import { getLobbyState } from '../../../lobby/functions'; import { getLobbyState } from '../../../lobby/functions';
import { admitAllKnockingParticipants } from '../../../video-menu/actions.any';
import { LobbyParticipantItem } from './LobbyParticipantItem'; import { LobbyParticipantItem } from './LobbyParticipantItem';
import styles from './styles'; import styles from './styles';
@ -20,7 +20,7 @@ export const LobbyParticipantList = () => {
const dispatch = useDispatch(); const dispatch = useDispatch();
const admitAll = useCallback(() => const admitAll = useCallback(() =>
dispatch(admitAllKnockingParticipants(participants, lobbyEnabled)), dispatch(admitMultiple(participants)),
[ dispatch ]); [ dispatch ]);
const { t } = useTranslation(); const { t } = useTranslation();

View File

@ -9,6 +9,7 @@ import {
} from '../../../base/tracks'; } from '../../../base/tracks';
import { showContextMenuDetails } from '../../actions.native'; import { showContextMenuDetails } from '../../actions.native';
import { MEDIA_STATE } from '../../constants'; import { MEDIA_STATE } from '../../constants';
import { getParticipantAudioMediaState } from '../../functions';
import ParticipantItem from './ParticipantItem'; import ParticipantItem from './ParticipantItem';
@ -25,15 +26,17 @@ export const MeetingParticipantItem = ({ participant: p }: Props) => {
const dispatch = useDispatch(); const dispatch = useDispatch();
const isAudioMuted = useSelector(getIsParticipantAudioMuted(p)); const isAudioMuted = useSelector(getIsParticipantAudioMuted(p));
const isVideoMuted = useSelector(getIsParticipantVideoMuted(p)); const isVideoMuted = useSelector(getIsParticipantVideoMuted(p));
const audioMediaState = useSelector(getParticipantAudioMediaState(p, isAudioMuted));
const openContextMenuDetails = useCallback(() => !p.local && dispatch(showContextMenuDetails(p), [ dispatch ])); const openContextMenuDetails = useCallback(() => !p.local && dispatch(showContextMenuDetails(p), [ dispatch ]));
return ( return (
<ParticipantItem <ParticipantItem
audioMuteState = { isAudioMuted ? MEDIA_STATE.MUTED : MEDIA_STATE.UNMUTED } audioMediaState = { audioMediaState }
isKnockingParticipant = { false } isKnockingParticipant = { false }
name = { p.name } name = { p.name }
onPress = { openContextMenuDetails } onPress = { openContextMenuDetails }
participant = { p } participant = { p }
videoMuteState = { isVideoMuted ? MEDIA_STATE.Muted : MEDIA_STATE.Unmuted } /> videoMediaState = { isVideoMuted ? MEDIA_STATE.MUTED : MEDIA_STATE.UNMUTED } />
); );
}; };

View File

@ -4,7 +4,7 @@ import React, { useCallback } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { 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, useStore } from 'react-redux'; import { useDispatch, useSelector } from 'react-redux';
import { Icon, IconInviteMore } from '../../../base/icons'; import { Icon, IconInviteMore } from '../../../base/icons';
import { getParticipants } from '../../../base/participants'; import { getParticipants } from '../../../base/participants';
@ -18,9 +18,7 @@ export const MeetingParticipantList = () => {
const dispatch = useDispatch(); const dispatch = useDispatch();
const onInvite = useCallback(() => dispatch(doInvitePeople()), [ dispatch ]); const onInvite = useCallback(() => dispatch(doInvitePeople()), [ dispatch ]);
const showInviteButton = useSelector(shouldRenderInviteButton); const showInviteButton = useSelector(shouldRenderInviteButton);
const store = useStore(); const participants = useSelector(getParticipants);
const state = store.getState();
const participants = getParticipants(state);
const { t } = useTranslation(); const { t } = useTranslation();
return ( return (

View File

@ -9,11 +9,7 @@ import { useSelector } from 'react-redux';
import { Avatar } from '../../../base/avatar'; import { Avatar } from '../../../base/avatar';
import { getParticipantDisplayNameWithId } from '../../../base/participants'; import { getParticipantDisplayNameWithId } from '../../../base/participants';
import { import { MEDIA_STATE, type MediaState, AudioStateIcons, VideoStateIcons } from '../../constants';
AudioStateIcons,
MediaState,
VideoStateIcons
} from '../../constants';
import { RaisedHandIndicator } from './RaisedHandIndicator'; import { RaisedHandIndicator } from './RaisedHandIndicator';
import styles from './styles'; import styles from './styles';
@ -40,11 +36,6 @@ type Props = {
*/ */
name?: string, name?: string,
/**
* Callback for when the mouse leaves this component
*/
onLeave?: Function,
/** /**
* Callback to be invoked on pressing the participant item. * Callback to be invoked on pressing the participant item.
*/ */
@ -72,8 +63,8 @@ function ParticipantItem({
name, name,
onPress, onPress,
participant: p, participant: p,
audioMediaState = MediaState.None, audioMediaState = MEDIA_STATE.NONE,
videoMediaState = MediaState.None videoMediaState = MEDIA_STATE.NONE
}: Props) { }: Props) {
const displayName = name || useSelector(getParticipantDisplayNameWithId(p.id)); const displayName = name || useSelector(getParticipantDisplayNameWithId(p.id));
@ -82,7 +73,6 @@ function ParticipantItem({
return ( return (
<View style = { styles.participantContainer } > <View style = { styles.participantContainer } >
<TouchableOpacity <TouchableOpacity
/* eslint-disable-next-line react/jsx-no-bind */
onPress = { onPress } onPress = { onPress }
style = { styles.participantContent }> style = { styles.participantContent }>
<Avatar <Avatar

View File

@ -10,6 +10,7 @@ import { openDialog } from '../../../base/dialog';
import { Icon, IconClose, IconHorizontalPoints } from '../../../base/icons'; import { Icon, IconClose, IconHorizontalPoints } from '../../../base/icons';
import { JitsiModal } from '../../../base/modal'; import { JitsiModal } from '../../../base/modal';
import { import {
getParticipantCount, isEveryoneModerator,
isLocalParticipantModerator isLocalParticipantModerator
} from '../../../base/participants'; } from '../../../base/participants';
import MuteEveryoneDialog import MuteEveryoneDialog
@ -19,7 +20,7 @@ import { close } from '../../actions.native';
import { ContextMenuMore } from './ContextMenuMore'; import { ContextMenuMore } from './ContextMenuMore';
import { LobbyParticipantList } from './LobbyParticipantList'; import { LobbyParticipantList } from './LobbyParticipantList';
import { MeetingParticipantList } from './MeetingParticipantList'; import { MeetingParticipantList } from './MeetingParticipantList';
import styles from './styles'; import styles, { button } from './styles';
/** /**
* Participant pane. * Participant pane.
@ -31,6 +32,9 @@ const ParticipantsPane = () => {
const openMoreMenu = useCallback(() => dispatch(openDialog(ContextMenuMore)), [ dispatch ]); const openMoreMenu = useCallback(() => dispatch(openDialog(ContextMenuMore)), [ dispatch ]);
const closePane = useCallback(() => dispatch(close()), [ dispatch ]); const closePane = useCallback(() => dispatch(close()), [ dispatch ]);
const isLocalModerator = useSelector(isLocalParticipantModerator); const isLocalModerator = useSelector(isLocalParticipantModerator);
const participantsCount = useSelector(getParticipantCount);
const everyoneModerator = useSelector(isEveryoneModerator);
const showContextMenu = !everyoneModerator && participantsCount > 2;
const muteAll = useCallback(() => dispatch(openDialog(MuteEveryoneDialog)), const muteAll = useCallback(() => dispatch(openDialog(MuteEveryoneDialog)),
[ dispatch ]); [ dispatch ]);
const { t } = useTranslation(); const { t } = useTranslation();
@ -65,18 +69,21 @@ const ParticipantsPane = () => {
labelStyle = { styles.muteAllLabel } labelStyle = { styles.muteAllLabel }
mode = 'contained' mode = 'contained'
onPress = { muteAll } onPress = { muteAll }
style = { styles.muteAllButton } /> style = { showContextMenu ? styles.muteAllButton : button } />
<Button {
contentStyle = { styles.moreIcon } showContextMenu
/* eslint-disable-next-line react/jsx-no-bind */ && <Button
icon = { () => contentStyle = { styles.moreIcon }
(<Icon /* eslint-disable-next-line react/jsx-no-bind */
size = { 24 } icon = { () =>
src = { IconHorizontalPoints } />) (<Icon
} size = { 24 }
mode = 'contained' src = { IconHorizontalPoints } />)
onPress = { openMoreMenu } }
style = { styles.moreButton } /> mode = 'contained'
onPress = { openMoreMenu }
style = { styles.moreButton } />
}
</View> </View>
} }
</JitsiModal> </JitsiModal>

View File

@ -25,7 +25,7 @@ const flexContent = {
/** /**
* The style of the participants pane buttons. * The style of the participants pane buttons.
*/ */
const button = { export const button = {
alignItems: 'center', alignItems: 'center',
backgroundColor: BaseTheme.palette.action02, backgroundColor: BaseTheme.palette.action02,
borderRadius: BaseTheme.shape.borderRadius, borderRadius: BaseTheme.shape.borderRadius,

View File

@ -1,5 +1,13 @@
// @flow // @flow
import React from 'react';
import {
Icon, IconCameraEmpty, IconCameraEmptyDisabled,
IconMicrophoneEmpty,
IconMicrophoneEmptySlash
} from '../base/icons';
/** /**
* Reducer key for the feature. * Reducer key for the feature.
*/ */
@ -46,3 +54,49 @@ export const QUICK_ACTION_BUTTON: {
ASK_TO_UNMUTE: 'AskToUnmute', ASK_TO_UNMUTE: 'AskToUnmute',
NONE: 'None' NONE: 'None'
}; };
/**
* Icon mapping for possible participant audio states.
*/
export const AudioStateIcons: {[MediaState]: React$Element<any> | null} = {
[MEDIA_STATE.FORCE_MUTED]: (
<Icon
color = '#E04757'
size = { 16 }
src = { IconMicrophoneEmptySlash } />
),
[MEDIA_STATE.MUTED]: (
<Icon
size = { 16 }
src = { IconMicrophoneEmptySlash } />
),
[MEDIA_STATE.UNMUTED]: (
<Icon
color = '#1EC26A'
size = { 16 }
src = { IconMicrophoneEmpty } />
),
[MEDIA_STATE.NONE]: null
};
/**
* Icon mapping for possible participant video states.
*/
export const VideoStateIcons = {
[MEDIA_STATE.FORCE_MUTED]: (
<Icon
size = { 16 }
src = { IconCameraEmptyDisabled } />
),
[MEDIA_STATE.MUTED]: (
<Icon
size = { 16 }
src = { IconCameraEmptyDisabled } />
),
[MEDIA_STATE.UNMUTED]: (
<Icon
size = { 16 }
src = { IconCameraEmpty } />
),
[MEDIA_STATE.NONE]: null
};

View File

@ -24,7 +24,6 @@ import {
muteRemoteParticipant muteRemoteParticipant
} from '../base/participants'; } from '../base/participants';
import { getIsParticipantAudioMuted } from '../base/tracks'; import { getIsParticipantAudioMuted } from '../base/tracks';
import { setKnockingParticipantApproval } from '../lobby/actions';
declare var APP: Object; declare var APP: Object;
@ -109,24 +108,6 @@ export function muteAllParticipants(exclude: Array<string>, mediaType: MEDIA_TYP
}; };
} }
/**
* Admit all knocking participants.
*
* @param {Array<Object>} knockingParticipants - Array of participants waiting in lobby.
* @param {boolean} lobbyEnabled - Is lobby mode enabled.
*
* @returns {Function}
*/
export function admitAllKnockingParticipants(knockingParticipants: Array<Object>, lobbyEnabled: boolean) {
return (dispatch: Dispatch<any>) => {
const knockingParticipantsIds = knockingParticipants.map(participant => participant.id);
knockingParticipantsIds
.map(id => lobbyEnabled && setKnockingParticipantApproval(id, true))
.map(dispatch);
};
}
/** /**
* Don't allow participants to unmute video/audio. * Don't allow participants to unmute video/audio.