fix(av-moderation): buttons for participants pane (#12977)
* fix(av): buttons for participants pane * fix tests * fix lint * rename cliked from participant pane
This commit is contained in:
parent
f727b9295f
commit
02c232440e
|
@ -15,11 +15,15 @@ export type MediaType = 'audio' | 'video' | 'screenshare';
|
||||||
*
|
*
|
||||||
* @enum {string}
|
* @enum {string}
|
||||||
*/
|
*/
|
||||||
export const MEDIA_TYPE: { [key: string]: MediaType; } = {
|
export const MEDIA_TYPE: {
|
||||||
AUDIO: 'audio',
|
AUDIO: MediaType;
|
||||||
SCREENSHARE: 'screenshare',
|
SCREENSHARE: MediaType;
|
||||||
VIDEO: 'video'
|
VIDEO: MediaType;
|
||||||
};
|
} = {
|
||||||
|
AUDIO: 'audio',
|
||||||
|
SCREENSHARE: 'screenshare',
|
||||||
|
VIDEO: 'video'
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
/* eslint-disable no-bitwise */
|
/* eslint-disable no-bitwise */
|
||||||
|
|
|
@ -2,7 +2,6 @@
|
||||||
|
|
||||||
import React, { useCallback, useEffect, useState } from 'react';
|
import React, { useCallback, useEffect, useState } from 'react';
|
||||||
|
|
||||||
import { translate } from '../../../base/i18n';
|
|
||||||
import { JitsiTrackEvents } from '../../../base/lib-jitsi-meet';
|
import { JitsiTrackEvents } from '../../../base/lib-jitsi-meet';
|
||||||
import { MEDIA_TYPE } from '../../../base/media';
|
import { MEDIA_TYPE } from '../../../base/media';
|
||||||
import {
|
import {
|
||||||
|
@ -163,9 +162,9 @@ type Props = {
|
||||||
participantID: ?string,
|
participantID: ?string,
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The translate function.
|
* Callback used to stop a participant's video.
|
||||||
*/
|
*/
|
||||||
t: Function,
|
stopVideo: Function,
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The translated "you" text.
|
* The translated "you" text.
|
||||||
|
@ -192,17 +191,15 @@ function MeetingParticipantItem({
|
||||||
_quickActionButtonType,
|
_quickActionButtonType,
|
||||||
_raisedHand,
|
_raisedHand,
|
||||||
_videoMediaState,
|
_videoMediaState,
|
||||||
askUnmuteText,
|
|
||||||
isHighlighted,
|
isHighlighted,
|
||||||
isInBreakoutRoom,
|
isInBreakoutRoom,
|
||||||
muteAudio,
|
muteAudio,
|
||||||
muteParticipantButtonText,
|
|
||||||
onContextMenu,
|
onContextMenu,
|
||||||
onLeave,
|
onLeave,
|
||||||
openDrawerForParticipant,
|
openDrawerForParticipant,
|
||||||
overflowDrawer,
|
overflowDrawer,
|
||||||
participantActionEllipsisLabel,
|
participantActionEllipsisLabel,
|
||||||
t,
|
stopVideo,
|
||||||
youText
|
youText
|
||||||
}: Props) {
|
}: Props) {
|
||||||
|
|
||||||
|
@ -242,12 +239,6 @@ function MeetingParticipantItem({
|
||||||
const audioMediaState = _audioMediaState === MEDIA_STATE.UNMUTED && hasAudioLevels
|
const audioMediaState = _audioMediaState === MEDIA_STATE.UNMUTED && hasAudioLevels
|
||||||
? MEDIA_STATE.DOMINANT_SPEAKER : _audioMediaState;
|
? MEDIA_STATE.DOMINANT_SPEAKER : _audioMediaState;
|
||||||
|
|
||||||
let askToUnmuteText = askUnmuteText;
|
|
||||||
|
|
||||||
if (_audioMediaState !== MEDIA_STATE.FORCE_MUTED && _videoMediaState === MEDIA_STATE.FORCE_MUTED) {
|
|
||||||
askToUnmuteText = t('participantsPane.actions.allowVideo');
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ParticipantItem
|
<ParticipantItem
|
||||||
actionsTrigger = { ACTION_TRIGGER.HOVER }
|
actionsTrigger = { ACTION_TRIGGER.HOVER }
|
||||||
|
@ -273,16 +264,16 @@ function MeetingParticipantItem({
|
||||||
&& <>
|
&& <>
|
||||||
{!isInBreakoutRoom && (
|
{!isInBreakoutRoom && (
|
||||||
<ParticipantQuickAction
|
<ParticipantQuickAction
|
||||||
askUnmuteText = { askToUnmuteText }
|
|
||||||
buttonType = { _quickActionButtonType }
|
buttonType = { _quickActionButtonType }
|
||||||
muteAudio = { muteAudio }
|
muteAudio = { muteAudio }
|
||||||
muteParticipantButtonText = { muteParticipantButtonText }
|
|
||||||
participantID = { _participantID }
|
participantID = { _participantID }
|
||||||
participantName = { _displayName } />
|
participantName = { _displayName }
|
||||||
|
stopVideo = { stopVideo } />
|
||||||
)}
|
)}
|
||||||
<ParticipantActionEllipsis
|
<ParticipantActionEllipsis
|
||||||
accessibilityLabel = { participantActionEllipsisLabel }
|
accessibilityLabel = { participantActionEllipsisLabel }
|
||||||
onClick = { onContextMenu } />
|
onClick = { onContextMenu }
|
||||||
|
participantID = { _participantID } />
|
||||||
</>
|
</>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -318,7 +309,7 @@ function _mapStateToProps(state, ownProps): Object {
|
||||||
const _isVideoMuted = isParticipantVideoMuted(participant, state);
|
const _isVideoMuted = isParticipantVideoMuted(participant, state);
|
||||||
const _audioMediaState = getParticipantAudioMediaState(participant, _isAudioMuted, state);
|
const _audioMediaState = getParticipantAudioMediaState(participant, _isAudioMuted, state);
|
||||||
const _videoMediaState = getParticipantVideoMediaState(participant, _isVideoMuted, state);
|
const _videoMediaState = getParticipantVideoMediaState(participant, _isVideoMuted, state);
|
||||||
const _quickActionButtonType = getQuickActionButtonType(participant, _isAudioMuted, state);
|
const _quickActionButtonType = getQuickActionButtonType(participant, _isAudioMuted, _isVideoMuted, state);
|
||||||
|
|
||||||
const tracks = state['features/base/tracks'];
|
const tracks = state['features/base/tracks'];
|
||||||
const _audioTrack = participantID === localParticipantId
|
const _audioTrack = participantID === localParticipantId
|
||||||
|
@ -342,4 +333,4 @@ function _mapStateToProps(state, ownProps): Object {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export default translate(connect(_mapStateToProps)(MeetingParticipantItem));
|
export default connect(_mapStateToProps)(MeetingParticipantItem);
|
||||||
|
|
|
@ -66,6 +66,11 @@ type Props = {
|
||||||
*/
|
*/
|
||||||
searchString?: string,
|
searchString?: string,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Callback used to stop a participant's video.
|
||||||
|
*/
|
||||||
|
stopVideo: Function,
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The translated "you" text.
|
* The translated "you" text.
|
||||||
*/
|
*/
|
||||||
|
@ -78,28 +83,25 @@ type Props = {
|
||||||
* @returns {ReactNode}
|
* @returns {ReactNode}
|
||||||
*/
|
*/
|
||||||
function MeetingParticipantItems({
|
function MeetingParticipantItems({
|
||||||
askUnmuteText,
|
|
||||||
isInBreakoutRoom,
|
isInBreakoutRoom,
|
||||||
lowerMenu,
|
lowerMenu,
|
||||||
toggleMenu,
|
toggleMenu,
|
||||||
muteAudio,
|
muteAudio,
|
||||||
muteParticipantButtonText,
|
|
||||||
participantIds,
|
participantIds,
|
||||||
openDrawerForParticipant,
|
openDrawerForParticipant,
|
||||||
overflowDrawer,
|
overflowDrawer,
|
||||||
raiseContextId,
|
raiseContextId,
|
||||||
participantActionEllipsisLabel,
|
participantActionEllipsisLabel,
|
||||||
searchString,
|
searchString,
|
||||||
|
stopVideo,
|
||||||
youText
|
youText
|
||||||
}: Props) {
|
}: Props) {
|
||||||
const renderParticipant = id => (
|
const renderParticipant = id => (
|
||||||
<MeetingParticipantItem
|
<MeetingParticipantItem
|
||||||
askUnmuteText = { askUnmuteText }
|
|
||||||
isHighlighted = { raiseContextId === id }
|
isHighlighted = { raiseContextId === id }
|
||||||
isInBreakoutRoom = { isInBreakoutRoom }
|
isInBreakoutRoom = { isInBreakoutRoom }
|
||||||
key = { id }
|
key = { id }
|
||||||
muteAudio = { muteAudio }
|
muteAudio = { muteAudio }
|
||||||
muteParticipantButtonText = { muteParticipantButtonText }
|
|
||||||
onContextMenu = { toggleMenu(id) }
|
onContextMenu = { toggleMenu(id) }
|
||||||
onLeave = { lowerMenu }
|
onLeave = { lowerMenu }
|
||||||
openDrawerForParticipant = { openDrawerForParticipant }
|
openDrawerForParticipant = { openDrawerForParticipant }
|
||||||
|
@ -107,6 +109,7 @@ function MeetingParticipantItems({
|
||||||
participantActionEllipsisLabel = { participantActionEllipsisLabel }
|
participantActionEllipsisLabel = { participantActionEllipsisLabel }
|
||||||
participantID = { id }
|
participantID = { id }
|
||||||
searchString = { searchString }
|
searchString = { searchString }
|
||||||
|
stopVideo = { stopVideo }
|
||||||
youText = { youText } />
|
youText = { youText } />
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
|
@ -5,7 +5,7 @@ import { useDispatch, useSelector } from 'react-redux';
|
||||||
import { makeStyles } from 'tss-react/mui';
|
import { makeStyles } from 'tss-react/mui';
|
||||||
|
|
||||||
import { IReduxState } from '../../../app/types';
|
import { IReduxState } from '../../../app/types';
|
||||||
import { rejectParticipantAudio } from '../../../av-moderation/actions';
|
import { rejectParticipantAudio, rejectParticipantVideo } from '../../../av-moderation/actions';
|
||||||
import participantsPaneTheme from '../../../base/components/themes/participantsPaneTheme.json';
|
import participantsPaneTheme from '../../../base/components/themes/participantsPaneTheme.json';
|
||||||
import { isToolbarButtonEnabled } from '../../../base/config/functions.web';
|
import { isToolbarButtonEnabled } from '../../../base/config/functions.web';
|
||||||
import { MEDIA_TYPE } from '../../../base/media/constants';
|
import { MEDIA_TYPE } from '../../../base/media/constants';
|
||||||
|
@ -88,11 +88,14 @@ function MeetingParticipants({
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
const [ lowerMenu, , toggleMenu, menuEnter, menuLeave, raiseContext ] = useContextMenu();
|
const [ lowerMenu, , toggleMenu, menuEnter, menuLeave, raiseContext ] = useContextMenu();
|
||||||
|
|
||||||
const muteAudio = useCallback(id => () => {
|
const muteAudio = useCallback(id => () => {
|
||||||
dispatch(muteRemote(id, MEDIA_TYPE.AUDIO));
|
dispatch(muteRemote(id, MEDIA_TYPE.AUDIO));
|
||||||
dispatch(rejectParticipantAudio(id));
|
dispatch(rejectParticipantAudio(id));
|
||||||
}, [ dispatch ]);
|
}, [ dispatch ]);
|
||||||
|
const stopVideo = useCallback(id => () => {
|
||||||
|
dispatch(muteRemote(id, MEDIA_TYPE.VIDEO));
|
||||||
|
dispatch(rejectParticipantVideo(id));
|
||||||
|
}, [ dispatch ]);
|
||||||
const [ drawerParticipant, closeDrawer, openDrawerForParticipant ] = useParticipantDrawer();
|
const [ drawerParticipant, closeDrawer, openDrawerForParticipant ] = useParticipantDrawer();
|
||||||
|
|
||||||
// FIXME:
|
// FIXME:
|
||||||
|
@ -103,8 +106,6 @@ function MeetingParticipants({
|
||||||
// mounted.
|
// mounted.
|
||||||
const participantActionEllipsisLabel = t('participantsPane.actions.moreParticipantOptions');
|
const participantActionEllipsisLabel = t('participantsPane.actions.moreParticipantOptions');
|
||||||
const youText = t('chat.you');
|
const youText = t('chat.you');
|
||||||
const askUnmuteText = t('participantsPane.actions.askUnmute');
|
|
||||||
const muteParticipantButtonText = t('dialog.muteParticipantButton');
|
|
||||||
const isBreakoutRoom = useSelector(isInBreakoutRoom);
|
const isBreakoutRoom = useSelector(isInBreakoutRoom);
|
||||||
const visitorsCount = useSelector((state: IReduxState) => state['features/visitors'].count);
|
const visitorsCount = useSelector((state: IReduxState) => state['features/visitors'].count);
|
||||||
|
|
||||||
|
@ -136,11 +137,9 @@ function MeetingParticipants({
|
||||||
value = { searchString } />
|
value = { searchString } />
|
||||||
<div>
|
<div>
|
||||||
<MeetingParticipantItems
|
<MeetingParticipantItems
|
||||||
askUnmuteText = { askUnmuteText }
|
|
||||||
isInBreakoutRoom = { isBreakoutRoom }
|
isInBreakoutRoom = { isBreakoutRoom }
|
||||||
lowerMenu = { lowerMenu }
|
lowerMenu = { lowerMenu }
|
||||||
muteAudio = { muteAudio }
|
muteAudio = { muteAudio }
|
||||||
muteParticipantButtonText = { muteParticipantButtonText }
|
|
||||||
openDrawerForParticipant = { openDrawerForParticipant }
|
openDrawerForParticipant = { openDrawerForParticipant }
|
||||||
overflowDrawer = { overflowDrawer }
|
overflowDrawer = { overflowDrawer }
|
||||||
participantActionEllipsisLabel = { participantActionEllipsisLabel }
|
participantActionEllipsisLabel = { participantActionEllipsisLabel }
|
||||||
|
@ -148,6 +147,7 @@ function MeetingParticipants({
|
||||||
participantsCount = { participantsCount }
|
participantsCount = { participantsCount }
|
||||||
raiseContextId = { raiseContext.entity }
|
raiseContextId = { raiseContext.entity }
|
||||||
searchString = { normalizeAccents(searchString) }
|
searchString = { normalizeAccents(searchString) }
|
||||||
|
stopVideo = { stopVideo }
|
||||||
toggleMenu = { toggleMenu }
|
toggleMenu = { toggleMenu }
|
||||||
youText = { youText } />
|
youText = { youText } />
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -14,14 +14,17 @@ interface IProps {
|
||||||
* Click handler function.
|
* Click handler function.
|
||||||
*/
|
*/
|
||||||
onClick: () => void;
|
onClick: () => void;
|
||||||
|
|
||||||
|
participantID?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
const ParticipantActionEllipsis = ({ accessibilityLabel, onClick }: IProps) => (
|
const ParticipantActionEllipsis = ({ accessibilityLabel, onClick, participantID }: IProps) => (
|
||||||
<Button
|
<Button
|
||||||
accessibilityLabel = { accessibilityLabel }
|
accessibilityLabel = { accessibilityLabel }
|
||||||
icon = { IconDotsHorizontal }
|
icon = { IconDotsHorizontal }
|
||||||
onClick = { onClick }
|
onClick = { onClick }
|
||||||
size = 'small' />
|
size = 'small'
|
||||||
|
testId = { participantID ? `participant-more-options-${participantID}` : undefined } />
|
||||||
);
|
);
|
||||||
|
|
||||||
export default ParticipantActionEllipsis;
|
export default ParticipantActionEllipsis;
|
||||||
|
|
|
@ -3,7 +3,7 @@ import { useTranslation } from 'react-i18next';
|
||||||
import { useDispatch } from 'react-redux';
|
import { useDispatch } from 'react-redux';
|
||||||
import { makeStyles } from 'tss-react/mui';
|
import { makeStyles } from 'tss-react/mui';
|
||||||
|
|
||||||
import { approveParticipant } from '../../../av-moderation/actions';
|
import { approveParticipantAudio, approveParticipantVideo } from '../../../av-moderation/actions';
|
||||||
import Button from '../../../base/ui/components/web/Button';
|
import Button from '../../../base/ui/components/web/Button';
|
||||||
import { QUICK_ACTION_BUTTON } from '../../constants';
|
import { QUICK_ACTION_BUTTON } from '../../constants';
|
||||||
|
|
||||||
|
@ -43,6 +43,12 @@ interface IProps {
|
||||||
* The name of the participant.
|
* The name of the participant.
|
||||||
*/
|
*/
|
||||||
participantName: string;
|
participantName: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Callback used to stop a participant's video.
|
||||||
|
*/
|
||||||
|
stopVideo: Function;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const useStyles = makeStyles()(theme => {
|
const useStyles = makeStyles()(theme => {
|
||||||
|
@ -54,19 +60,22 @@ const useStyles = makeStyles()(theme => {
|
||||||
});
|
});
|
||||||
|
|
||||||
const ParticipantQuickAction = ({
|
const ParticipantQuickAction = ({
|
||||||
askUnmuteText,
|
|
||||||
buttonType,
|
buttonType,
|
||||||
muteAudio,
|
muteAudio,
|
||||||
muteParticipantButtonText,
|
|
||||||
participantID,
|
participantID,
|
||||||
participantName
|
participantName,
|
||||||
|
stopVideo
|
||||||
}: IProps) => {
|
}: IProps) => {
|
||||||
const { classes: styles } = useStyles();
|
const { classes: styles } = useStyles();
|
||||||
const dispatch = useDispatch();
|
const dispatch = useDispatch();
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
const askToUnmute = useCallback(() => {
|
const askToUnmute = useCallback(() => {
|
||||||
dispatch(approveParticipant(participantID));
|
dispatch(approveParticipantAudio(participantID));
|
||||||
|
}, [ dispatch, participantID ]);
|
||||||
|
|
||||||
|
const allowVideo = useCallback(() => {
|
||||||
|
dispatch(approveParticipantVideo(participantID));
|
||||||
}, [ dispatch, participantID ]);
|
}, [ dispatch, participantID ]);
|
||||||
|
|
||||||
switch (buttonType) {
|
switch (buttonType) {
|
||||||
|
@ -75,10 +84,10 @@ const ParticipantQuickAction = ({
|
||||||
<Button
|
<Button
|
||||||
accessibilityLabel = { `${t('participantsPane.actions.mute')} ${participantName}` }
|
accessibilityLabel = { `${t('participantsPane.actions.mute')} ${participantName}` }
|
||||||
className = { styles.button }
|
className = { styles.button }
|
||||||
label = { muteParticipantButtonText }
|
label = { t('participantsPane.actions.mute') }
|
||||||
onClick = { muteAudio(participantID) }
|
onClick = { muteAudio(participantID) }
|
||||||
size = 'small'
|
size = 'small'
|
||||||
testId = { `mute-${participantID}` } />
|
testId = { `mute-audio-${participantID}` } />
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
case QUICK_ACTION_BUTTON.ASK_TO_UNMUTE: {
|
case QUICK_ACTION_BUTTON.ASK_TO_UNMUTE: {
|
||||||
|
@ -86,10 +95,32 @@ const ParticipantQuickAction = ({
|
||||||
<Button
|
<Button
|
||||||
accessibilityLabel = { `${t('participantsPane.actions.askUnmute')} ${participantName}` }
|
accessibilityLabel = { `${t('participantsPane.actions.askUnmute')} ${participantName}` }
|
||||||
className = { styles.button }
|
className = { styles.button }
|
||||||
label = { askUnmuteText }
|
label = { t('participantsPane.actions.askUnmute') }
|
||||||
onClick = { askToUnmute }
|
onClick = { askToUnmute }
|
||||||
size = 'small'
|
size = 'small'
|
||||||
testId = { `unmute-${participantID}` } />
|
testId = { `unmute-audio-${participantID}` } />
|
||||||
|
);
|
||||||
|
}
|
||||||
|
case QUICK_ACTION_BUTTON.ALLOW_VIDEO: {
|
||||||
|
return (
|
||||||
|
<Button
|
||||||
|
accessibilityLabel = { `${t('participantsPane.actions.askUnmute')} ${participantName}` }
|
||||||
|
className = { styles.button }
|
||||||
|
label = { t('participantsPane.actions.allowVideo') }
|
||||||
|
onClick = { allowVideo }
|
||||||
|
size = 'small'
|
||||||
|
testId = { `unmute-video-${participantID}` } />
|
||||||
|
);
|
||||||
|
}
|
||||||
|
case QUICK_ACTION_BUTTON.STOP_VIDEO: {
|
||||||
|
return (
|
||||||
|
<Button
|
||||||
|
accessibilityLabel = { `${t('participantsPane.actions.mute')} ${participantName}` }
|
||||||
|
className = { styles.button }
|
||||||
|
label = { t('participantsPane.actions.stopVideo') }
|
||||||
|
onClick = { stopVideo(participantID) }
|
||||||
|
size = 'small'
|
||||||
|
testId = { `mute-video-${participantID}` } />
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
default: {
|
default: {
|
||||||
|
|
|
@ -36,19 +36,23 @@ export const MEDIA_STATE: { [key: string]: MediaState; } = {
|
||||||
NONE: 'None'
|
NONE: 'None'
|
||||||
};
|
};
|
||||||
|
|
||||||
export type QuickActionButtonType = 'Mute' | 'AskToUnmute' | 'None';
|
export type QuickActionButtonType = 'Mute' | 'AskToUnmute' | 'AllowVideo' | 'StopVideo' | 'None';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Enum of possible participant mute button states.
|
* Enum of possible participant mute button states.
|
||||||
*/
|
*/
|
||||||
export const QUICK_ACTION_BUTTON: {
|
export const QUICK_ACTION_BUTTON: {
|
||||||
|
ALLOW_VIDEO: QuickActionButtonType;
|
||||||
ASK_TO_UNMUTE: QuickActionButtonType;
|
ASK_TO_UNMUTE: QuickActionButtonType;
|
||||||
MUTE: QuickActionButtonType;
|
MUTE: QuickActionButtonType;
|
||||||
NONE: QuickActionButtonType;
|
NONE: QuickActionButtonType;
|
||||||
|
STOP_VIDEO: QuickActionButtonType;
|
||||||
} = {
|
} = {
|
||||||
|
ALLOW_VIDEO: 'AllowVideo',
|
||||||
MUTE: 'Mute',
|
MUTE: 'Mute',
|
||||||
ASK_TO_UNMUTE: 'AskToUnmute',
|
ASK_TO_UNMUTE: 'AskToUnmute',
|
||||||
NONE: 'None'
|
NONE: 'None',
|
||||||
|
STOP_VIDEO: 'StopVideo'
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -131,15 +131,28 @@ export const getParticipantsPaneOpen = (state: IReduxState) => Boolean(getState(
|
||||||
*
|
*
|
||||||
* @param {IParticipant} participant - The participant.
|
* @param {IParticipant} participant - The participant.
|
||||||
* @param {boolean} isAudioMuted - If audio is muted for the participant.
|
* @param {boolean} isAudioMuted - If audio is muted for the participant.
|
||||||
|
* @param {boolean} isVideoMuted - If audio is muted for the participant.
|
||||||
* @param {IReduxState} state - The redux state.
|
* @param {IReduxState} state - The redux state.
|
||||||
* @returns {string} - The type of the quick action button.
|
* @returns {string} - The type of the quick action button.
|
||||||
*/
|
*/
|
||||||
export function getQuickActionButtonType(participant: IParticipant, isAudioMuted: Boolean, state: IReduxState) {
|
export function getQuickActionButtonType(
|
||||||
|
participant: IParticipant,
|
||||||
|
isAudioMuted: Boolean,
|
||||||
|
isVideoMuted: Boolean,
|
||||||
|
state: IReduxState) {
|
||||||
// handled only by moderators
|
// handled only by moderators
|
||||||
|
const isVideoForceMuted = isForceMuted(participant, MEDIA_TYPE.VIDEO, state);
|
||||||
|
|
||||||
if (isLocalParticipantModerator(state)) {
|
if (isLocalParticipantModerator(state)) {
|
||||||
if (!isAudioMuted) {
|
if (!isAudioMuted) {
|
||||||
return QUICK_ACTION_BUTTON.MUTE;
|
return QUICK_ACTION_BUTTON.MUTE;
|
||||||
}
|
}
|
||||||
|
if (!isVideoMuted) {
|
||||||
|
return QUICK_ACTION_BUTTON.STOP_VIDEO;
|
||||||
|
}
|
||||||
|
if (isVideoForceMuted) {
|
||||||
|
return QUICK_ACTION_BUTTON.ALLOW_VIDEO;
|
||||||
|
}
|
||||||
if (isSupported()(state)) {
|
if (isSupported()(state)) {
|
||||||
return QUICK_ACTION_BUTTON.ASK_TO_UNMUTE;
|
return QUICK_ACTION_BUTTON.ASK_TO_UNMUTE;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,49 +0,0 @@
|
||||||
// @flow
|
|
||||||
|
|
||||||
import React, { useCallback } from 'react';
|
|
||||||
import { useTranslation } from 'react-i18next';
|
|
||||||
import { useDispatch } from 'react-redux';
|
|
||||||
|
|
||||||
import { approveParticipant } from '../../../av-moderation/actions';
|
|
||||||
import { IconMic } from '../../../base/icons';
|
|
||||||
import ContextMenuItem from '../../../base/ui/components/web/ContextMenuItem';
|
|
||||||
|
|
||||||
type Props = {
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Whether or not the participant is audio force muted.
|
|
||||||
*/
|
|
||||||
isAudioForceMuted: boolean,
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Whether or not the participant is video force muted.
|
|
||||||
*/
|
|
||||||
isVideoForceMuted: boolean,
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The ID for the participant on which the button will act.
|
|
||||||
*/
|
|
||||||
participantID: string
|
|
||||||
}
|
|
||||||
|
|
||||||
const AskToUnmuteButton = ({ isAudioForceMuted, isVideoForceMuted, participantID }: Props) => {
|
|
||||||
const dispatch = useDispatch();
|
|
||||||
const { t } = useTranslation();
|
|
||||||
const _onClick = useCallback(() => {
|
|
||||||
dispatch(approveParticipant(participantID));
|
|
||||||
}, [ participantID ]);
|
|
||||||
|
|
||||||
const text = isAudioForceMuted || !isVideoForceMuted
|
|
||||||
? t('participantsPane.actions.askUnmute')
|
|
||||||
: t('participantsPane.actions.allowVideo');
|
|
||||||
|
|
||||||
return (
|
|
||||||
<ContextMenuItem
|
|
||||||
accessibilityLabel = { text }
|
|
||||||
icon = { IconMic }
|
|
||||||
onClick = { _onClick }
|
|
||||||
text = { text } />
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default AskToUnmuteButton;
|
|
|
@ -0,0 +1,61 @@
|
||||||
|
// @flow
|
||||||
|
|
||||||
|
import React, { useCallback, useMemo } from 'react';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import { useDispatch } from 'react-redux';
|
||||||
|
|
||||||
|
import { approveParticipantAudio, approveParticipantVideo } from '../../../av-moderation/actions';
|
||||||
|
import { IconMic, IconVideo } from '../../../base/icons/svg';
|
||||||
|
import { MEDIA_TYPE, MediaType } from '../../../base/media/constants';
|
||||||
|
import ContextMenuItem from '../../../base/ui/components/web/ContextMenuItem';
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
|
||||||
|
buttonType: MediaType;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The ID for the participant on which the button will act.
|
||||||
|
*/
|
||||||
|
participantID: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
const AskToUnmuteButton = ({ buttonType, participantID }: Props) => {
|
||||||
|
const dispatch = useDispatch();
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const _onClick = useCallback(() => {
|
||||||
|
if (buttonType === MEDIA_TYPE.AUDIO) {
|
||||||
|
dispatch(approveParticipantAudio(participantID));
|
||||||
|
} else if (buttonType === MEDIA_TYPE.VIDEO) {
|
||||||
|
dispatch(approveParticipantVideo(participantID));
|
||||||
|
}
|
||||||
|
}, [ participantID, buttonType ]);
|
||||||
|
|
||||||
|
const text = useMemo(() => {
|
||||||
|
if (buttonType === MEDIA_TYPE.AUDIO) {
|
||||||
|
return t('participantsPane.actions.askUnmute');
|
||||||
|
} else if (buttonType === MEDIA_TYPE.VIDEO) {
|
||||||
|
return t('participantsPane.actions.allowVideo');
|
||||||
|
}
|
||||||
|
|
||||||
|
return '';
|
||||||
|
}, [ buttonType ]);
|
||||||
|
|
||||||
|
const icon = useMemo(() => {
|
||||||
|
if (buttonType === MEDIA_TYPE.AUDIO) {
|
||||||
|
return IconMic;
|
||||||
|
} else if (buttonType === MEDIA_TYPE.VIDEO) {
|
||||||
|
return IconVideo;
|
||||||
|
}
|
||||||
|
}, [ buttonType ]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ContextMenuItem
|
||||||
|
accessibilityLabel = { text }
|
||||||
|
icon = { icon }
|
||||||
|
onClick = { _onClick }
|
||||||
|
testId = { `unmute-${buttonType}-${participantID}` }
|
||||||
|
text = { text } />
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default AskToUnmuteButton;
|
|
@ -1,6 +1,6 @@
|
||||||
/* eslint-disable lines-around-comment */
|
/* eslint-disable lines-around-comment */
|
||||||
|
|
||||||
import React, { useCallback } from 'react';
|
import React, { useCallback, useMemo } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { useDispatch, useSelector } from 'react-redux';
|
import { useDispatch, useSelector } from 'react-redux';
|
||||||
import { makeStyles } from 'tss-react/mui';
|
import { makeStyles } from 'tss-react/mui';
|
||||||
|
@ -14,14 +14,15 @@ import { MEDIA_TYPE } from '../../../base/media/constants';
|
||||||
import { PARTICIPANT_ROLE } from '../../../base/participants/constants';
|
import { PARTICIPANT_ROLE } from '../../../base/participants/constants';
|
||||||
import { getLocalParticipant } from '../../../base/participants/functions';
|
import { getLocalParticipant } from '../../../base/participants/functions';
|
||||||
import { IParticipant } from '../../../base/participants/types';
|
import { IParticipant } from '../../../base/participants/types';
|
||||||
import { isParticipantAudioMuted } from '../../../base/tracks/functions';
|
import { isParticipantAudioMuted, isParticipantVideoMuted } from '../../../base/tracks/functions.any';
|
||||||
import ContextMenu from '../../../base/ui/components/web/ContextMenu';
|
import ContextMenu from '../../../base/ui/components/web/ContextMenu';
|
||||||
import ContextMenuItemGroup from '../../../base/ui/components/web/ContextMenuItemGroup';
|
import ContextMenuItemGroup from '../../../base/ui/components/web/ContextMenuItemGroup';
|
||||||
import { getBreakoutRooms, getCurrentRoomId, isInBreakoutRoom } from '../../../breakout-rooms/functions';
|
import { getBreakoutRooms, getCurrentRoomId, isInBreakoutRoom } from '../../../breakout-rooms/functions';
|
||||||
import { displayVerification } from '../../../e2ee/functions';
|
import { displayVerification } from '../../../e2ee/functions';
|
||||||
import { setVolume } from '../../../filmstrip/actions.web';
|
import { setVolume } from '../../../filmstrip/actions.web';
|
||||||
import { isStageFilmstripAvailable } from '../../../filmstrip/functions.web';
|
import { isStageFilmstripAvailable } from '../../../filmstrip/functions.web';
|
||||||
import { isForceMuted } from '../../../participants-pane/functions';
|
import { QUICK_ACTION_BUTTON } from '../../../participants-pane/constants';
|
||||||
|
import { getQuickActionButtonType, isForceMuted } from '../../../participants-pane/functions';
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
import { requestRemoteControl, stopController } from '../../../remote-control';
|
import { requestRemoteControl, stopController } from '../../../remote-control';
|
||||||
import { showOverflowDrawer } from '../../../toolbox/functions.web';
|
import { showOverflowDrawer } from '../../../toolbox/functions.web';
|
||||||
|
@ -139,11 +140,10 @@ const ParticipantContextMenu = ({
|
||||||
|
|
||||||
const localParticipant = useSelector(getLocalParticipant);
|
const localParticipant = useSelector(getLocalParticipant);
|
||||||
const _isModerator = Boolean(localParticipant?.role === PARTICIPANT_ROLE.MODERATOR);
|
const _isModerator = Boolean(localParticipant?.role === PARTICIPANT_ROLE.MODERATOR);
|
||||||
const _isAudioForceMuted = useSelector<IReduxState>(state =>
|
|
||||||
isForceMuted(participant, MEDIA_TYPE.AUDIO, state));
|
|
||||||
const _isVideoForceMuted = useSelector<IReduxState>(state =>
|
const _isVideoForceMuted = useSelector<IReduxState>(state =>
|
||||||
isForceMuted(participant, MEDIA_TYPE.VIDEO, state));
|
isForceMuted(participant, MEDIA_TYPE.VIDEO, state));
|
||||||
const _isAudioMuted = useSelector((state: IReduxState) => isParticipantAudioMuted(participant, state));
|
const _isAudioMuted = useSelector((state: IReduxState) => isParticipantAudioMuted(participant, state));
|
||||||
|
const _isVideoMuted = useSelector((state: IReduxState) => isParticipantVideoMuted(participant, state));
|
||||||
const _overflowDrawer: boolean = useSelector(showOverflowDrawer);
|
const _overflowDrawer: boolean = useSelector(showOverflowDrawer);
|
||||||
const { remoteVideoMenu = {}, disableRemoteMute, startSilent, customParticipantMenuButtons }
|
const { remoteVideoMenu = {}, disableRemoteMute, startSilent, customParticipantMenuButtons }
|
||||||
= useSelector((state: IReduxState) => state['features/base/config']);
|
= useSelector((state: IReduxState) => state['features/base/config']);
|
||||||
|
@ -172,6 +172,12 @@ const ParticipantContextMenu = ({
|
||||||
}
|
}
|
||||||
, [ thumbnailMenu, _overflowDrawer, drawerParticipant, participant ]);
|
, [ thumbnailMenu, _overflowDrawer, drawerParticipant, participant ]);
|
||||||
|
|
||||||
|
const isClickedFromParticipantPane = useMemo(
|
||||||
|
() => !_overflowDrawer && !thumbnailMenu,
|
||||||
|
[ _overflowDrawer, thumbnailMenu ]);
|
||||||
|
const quickActionButtonType = useSelector((state: IReduxState) =>
|
||||||
|
getQuickActionButtonType(participant, _isAudioMuted, _isVideoMuted, state));
|
||||||
|
|
||||||
const buttons: JSX.Element[] = [];
|
const buttons: JSX.Element[] = [];
|
||||||
const buttons2: JSX.Element[] = [];
|
const buttons2: JSX.Element[] = [];
|
||||||
|
|
||||||
|
@ -182,30 +188,44 @@ const ParticipantContextMenu = ({
|
||||||
&& !isNaN(_volume);
|
&& !isNaN(_volume);
|
||||||
|
|
||||||
if (_isModerator) {
|
if (_isModerator) {
|
||||||
if ((thumbnailMenu || _overflowDrawer) && isModerationSupported && _isAudioMuted) {
|
if (isModerationSupported) {
|
||||||
buttons.push(<AskToUnmuteButton
|
if (_isAudioMuted
|
||||||
isAudioForceMuted = { _isAudioForceMuted }
|
&& !(isClickedFromParticipantPane && quickActionButtonType === QUICK_ACTION_BUTTON.ASK_TO_UNMUTE)) {
|
||||||
isVideoForceMuted = { _isVideoForceMuted }
|
buttons.push(<AskToUnmuteButton
|
||||||
key = 'ask-unmute'
|
buttonType = { MEDIA_TYPE.AUDIO }
|
||||||
participantID = { _getCurrentParticipantId() } />
|
key = 'ask-unmute'
|
||||||
);
|
participantID = { _getCurrentParticipantId() } />
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (_isVideoForceMuted
|
||||||
|
&& !(isClickedFromParticipantPane && quickActionButtonType === QUICK_ACTION_BUTTON.ALLOW_VIDEO)) {
|
||||||
|
buttons.push(<AskToUnmuteButton
|
||||||
|
buttonType = { MEDIA_TYPE.VIDEO }
|
||||||
|
key = 'allow-video'
|
||||||
|
participantID = { _getCurrentParticipantId() } />
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if (!disableRemoteMute) {
|
if (!disableRemoteMute) {
|
||||||
buttons.push(
|
if (!(isClickedFromParticipantPane && quickActionButtonType === QUICK_ACTION_BUTTON.MUTE)) {
|
||||||
<MuteButton
|
buttons.push(
|
||||||
key = 'mute'
|
<MuteButton
|
||||||
participantID = { _getCurrentParticipantId() } />
|
key = 'mute'
|
||||||
);
|
participantID = { _getCurrentParticipantId() } />
|
||||||
|
);
|
||||||
|
}
|
||||||
buttons.push(
|
buttons.push(
|
||||||
<MuteEveryoneElseButton
|
<MuteEveryoneElseButton
|
||||||
key = 'mute-others'
|
key = 'mute-others'
|
||||||
participantID = { _getCurrentParticipantId() } />
|
participantID = { _getCurrentParticipantId() } />
|
||||||
);
|
);
|
||||||
buttons.push(
|
if (!(isClickedFromParticipantPane && quickActionButtonType === QUICK_ACTION_BUTTON.STOP_VIDEO)) {
|
||||||
<MuteVideoButton
|
buttons.push(
|
||||||
key = 'mute-video'
|
<MuteVideoButton
|
||||||
participantID = { _getCurrentParticipantId() } />
|
key = 'mute-video'
|
||||||
);
|
participantID = { _getCurrentParticipantId() } />
|
||||||
|
);
|
||||||
|
}
|
||||||
buttons.push(
|
buttons.push(
|
||||||
<MuteEveryoneElsesVideoButton
|
<MuteEveryoneElsesVideoButton
|
||||||
key = 'mute-others-video'
|
key = 'mute-others-video'
|
||||||
|
|
Loading…
Reference in New Issue