562 lines
17 KiB
JavaScript
562 lines
17 KiB
JavaScript
// @flow
|
|
import { withStyles } from '@material-ui/styles';
|
|
import React, { Component } from 'react';
|
|
|
|
import { createBreakoutRoomsEvent, sendAnalytics } from '../../../analytics';
|
|
import { approveParticipant } from '../../../av-moderation/actions';
|
|
import { Avatar } from '../../../base/avatar';
|
|
import { ContextMenu, ContextMenuItemGroup } from '../../../base/components';
|
|
import { isToolbarButtonEnabled } from '../../../base/config/functions.web';
|
|
import { openDialog } from '../../../base/dialog';
|
|
import { isIosMobileBrowser } from '../../../base/environment/utils';
|
|
import { translate } from '../../../base/i18n';
|
|
import {
|
|
IconCloseCircle,
|
|
IconCrown,
|
|
IconMessage,
|
|
IconMicDisabled,
|
|
IconMicrophone,
|
|
IconMuteEveryoneElse,
|
|
IconRingGroup,
|
|
IconShareVideo,
|
|
IconVideoOff
|
|
} from '../../../base/icons';
|
|
import { MEDIA_TYPE } from '../../../base/media';
|
|
import {
|
|
getLocalParticipant,
|
|
getParticipantByIdOrUndefined,
|
|
isLocalParticipantModerator,
|
|
isParticipantModerator
|
|
} from '../../../base/participants';
|
|
import { connect } from '../../../base/redux';
|
|
import { isParticipantAudioMuted, isParticipantVideoMuted } from '../../../base/tracks';
|
|
import { sendParticipantToRoom } from '../../../breakout-rooms/actions';
|
|
import { getBreakoutRooms, getCurrentRoomId } from '../../../breakout-rooms/functions';
|
|
import { openChatById } from '../../../chat/actions';
|
|
import { setVolume } from '../../../filmstrip/actions.web';
|
|
import { stopSharedVideo } from '../../../shared-video/actions.any';
|
|
import { GrantModeratorDialog, KickRemoteParticipantDialog, MuteEveryoneDialog } from '../../../video-menu';
|
|
import { VolumeSlider } from '../../../video-menu/components/web';
|
|
import MuteRemoteParticipantsVideoDialog from '../../../video-menu/components/web/MuteRemoteParticipantsVideoDialog';
|
|
import { isForceMuted } from '../../functions';
|
|
|
|
type Props = {
|
|
|
|
/**
|
|
* Whether or not the participant is audio force muted.
|
|
*/
|
|
_isAudioForceMuted: boolean,
|
|
|
|
/**
|
|
* The id of the current room.
|
|
*/
|
|
_currentRoomId: String,
|
|
|
|
/**
|
|
* True if the local participant is moderator and false otherwise.
|
|
*/
|
|
_isLocalModerator: boolean,
|
|
|
|
/**
|
|
* True if the chat button is enabled and false otherwise.
|
|
*/
|
|
_isChatButtonEnabled: boolean,
|
|
|
|
/**
|
|
* True if the participant is moderator and false otherwise.
|
|
*/
|
|
_isParticipantModerator: boolean,
|
|
|
|
/**
|
|
* True if the participant is video muted and false otherwise.
|
|
*/
|
|
_isParticipantVideoMuted: boolean,
|
|
|
|
/**
|
|
* True if the participant is audio muted and false otherwise.
|
|
*/
|
|
_isParticipantAudioMuted: boolean,
|
|
|
|
/**
|
|
* Whether or not the participant is video force muted.
|
|
*/
|
|
_isVideoForceMuted: boolean,
|
|
|
|
/**
|
|
* Shared video local participant owner.
|
|
*/
|
|
_localVideoOwner: boolean,
|
|
|
|
/**
|
|
* Participant reference.
|
|
*/
|
|
_participant: Object,
|
|
|
|
/**
|
|
* Rooms reference.
|
|
*/
|
|
_rooms: Array<Object>,
|
|
|
|
/**
|
|
* A value between 0 and 1 indicating the volume of the participant's
|
|
* audio element.
|
|
*/
|
|
_volume: ?number,
|
|
|
|
/**
|
|
* Closes a drawer if open.
|
|
*/
|
|
closeDrawer: Function,
|
|
|
|
/**
|
|
* An object containing the CSS classes.
|
|
*/
|
|
classes?: {[ key: string]: string},
|
|
|
|
/**
|
|
* The dispatch function from redux.
|
|
*/
|
|
dispatch: Function,
|
|
|
|
/**
|
|
* The participant for which the drawer is open.
|
|
* It contains the displayName & participantID.
|
|
*/
|
|
drawerParticipant: Object,
|
|
|
|
/**
|
|
* Callback used to open a confirmation dialog for audio muting.
|
|
*/
|
|
muteAudio: Function,
|
|
|
|
/**
|
|
* Target elements against which positioning calculations are made.
|
|
*/
|
|
offsetTarget?: HTMLElement,
|
|
|
|
/**
|
|
* Callback for the mouse entering the component.
|
|
*/
|
|
onEnter: Function,
|
|
|
|
/**
|
|
* Callback for the mouse leaving the component.
|
|
*/
|
|
onLeave: Function,
|
|
|
|
/**
|
|
* Callback for making a selection in the menu.
|
|
*/
|
|
onSelect: Function,
|
|
|
|
/**
|
|
* The ID of the participant.
|
|
*/
|
|
participantID?: string,
|
|
|
|
/**
|
|
* True if an overflow drawer should be displayed.
|
|
*/
|
|
overflowDrawer: boolean,
|
|
|
|
/**
|
|
* The translate function.
|
|
*/
|
|
t: Function
|
|
};
|
|
|
|
const styles = theme => {
|
|
return {
|
|
text: {
|
|
color: theme.palette.text02,
|
|
padding: '10px 16px',
|
|
height: '40px',
|
|
overflow: 'hidden',
|
|
display: 'flex',
|
|
alignItems: 'center',
|
|
boxSizing: 'border-box'
|
|
}
|
|
};
|
|
};
|
|
|
|
/**
|
|
* Implements the MeetingParticipantContextMenu component.
|
|
*/
|
|
class MeetingParticipantContextMenu extends Component<Props> {
|
|
|
|
/**
|
|
* Creates new instance of MeetingParticipantContextMenu.
|
|
*
|
|
* @param {Props} props - The props.
|
|
*/
|
|
constructor(props: Props) {
|
|
super(props);
|
|
|
|
this._getCurrentParticipantId = this._getCurrentParticipantId.bind(this);
|
|
this._onGrantModerator = this._onGrantModerator.bind(this);
|
|
this._onKick = this._onKick.bind(this);
|
|
this._onMuteEveryoneElse = this._onMuteEveryoneElse.bind(this);
|
|
this._onMuteVideo = this._onMuteVideo.bind(this);
|
|
this._onSendPrivateMessage = this._onSendPrivateMessage.bind(this);
|
|
this._onStopSharedVideo = this._onStopSharedVideo.bind(this);
|
|
this._onSendToRoom = this._onSendToRoom.bind(this);
|
|
this._onVolumeChange = this._onVolumeChange.bind(this);
|
|
this._onAskToUnmute = this._onAskToUnmute.bind(this);
|
|
}
|
|
|
|
_getCurrentParticipantId: () => string;
|
|
|
|
/**
|
|
* Returns the participant id for the item we want to operate.
|
|
*
|
|
* @returns {void}
|
|
*/
|
|
_getCurrentParticipantId() {
|
|
const { _participant, drawerParticipant, overflowDrawer } = this.props;
|
|
|
|
return overflowDrawer ? drawerParticipant?.participantID : _participant?.id;
|
|
}
|
|
|
|
_onGrantModerator: () => void;
|
|
|
|
/**
|
|
* Grant moderator permissions.
|
|
*
|
|
* @returns {void}
|
|
*/
|
|
_onGrantModerator() {
|
|
this.props.dispatch(openDialog(GrantModeratorDialog, {
|
|
participantID: this._getCurrentParticipantId()
|
|
}));
|
|
}
|
|
|
|
_onKick: () => void;
|
|
|
|
/**
|
|
* Kicks the participant.
|
|
*
|
|
* @returns {void}
|
|
*/
|
|
_onKick() {
|
|
this.props.dispatch(openDialog(KickRemoteParticipantDialog, {
|
|
participantID: this._getCurrentParticipantId()
|
|
}));
|
|
}
|
|
|
|
_onStopSharedVideo: () => void;
|
|
|
|
/**
|
|
* Stops shared video.
|
|
*
|
|
* @returns {void}
|
|
*/
|
|
_onStopSharedVideo() {
|
|
const { dispatch, onSelect } = this.props;
|
|
|
|
onSelect(true);
|
|
dispatch(stopSharedVideo());
|
|
}
|
|
|
|
_onMuteEveryoneElse: () => void;
|
|
|
|
/**
|
|
* Mutes everyone else.
|
|
*
|
|
* @returns {void}
|
|
*/
|
|
_onMuteEveryoneElse() {
|
|
this.props.dispatch(openDialog(MuteEveryoneDialog, {
|
|
exclude: [ this._getCurrentParticipantId() ]
|
|
}));
|
|
}
|
|
|
|
_onMuteVideo: () => void;
|
|
|
|
/**
|
|
* Mutes the video of the selected participant.
|
|
*
|
|
* @returns {void}
|
|
*/
|
|
_onMuteVideo() {
|
|
this.props.dispatch(openDialog(MuteRemoteParticipantsVideoDialog, {
|
|
participantID: this._getCurrentParticipantId()
|
|
}));
|
|
}
|
|
|
|
_onSendPrivateMessage: () => void;
|
|
|
|
/**
|
|
* Sends private message.
|
|
*
|
|
* @returns {void}
|
|
*/
|
|
_onSendPrivateMessage() {
|
|
const { dispatch } = this.props;
|
|
|
|
dispatch(openChatById(this._getCurrentParticipantId()));
|
|
}
|
|
|
|
_onSendToRoom: (room: Object) => void;
|
|
|
|
/**
|
|
* Sends a participant to a room.
|
|
*
|
|
* @param {Object} room - The room that the participant should be moved to.
|
|
* @returns {void}
|
|
*/
|
|
_onSendToRoom(room: Object) {
|
|
return () => {
|
|
const { _participant, dispatch } = this.props;
|
|
|
|
sendAnalytics(createBreakoutRoomsEvent('send.participant.to.room'));
|
|
dispatch(sendParticipantToRoom(_participant.id, room.id));
|
|
};
|
|
}
|
|
|
|
_onVolumeChange: (number) => void;
|
|
|
|
/**
|
|
* Handles volume changes.
|
|
*
|
|
* @param {number} value - The new value for the volume.
|
|
* @returns {void}
|
|
*/
|
|
_onVolumeChange(value) {
|
|
const { _participant, dispatch } = this.props;
|
|
const { id } = _participant;
|
|
|
|
dispatch(setVolume(id, value));
|
|
}
|
|
|
|
_onAskToUnmute: () => void;
|
|
|
|
/**
|
|
* Handles click on ask to unmute.
|
|
*
|
|
* @returns {void}
|
|
*/
|
|
_onAskToUnmute() {
|
|
const { _participant, dispatch } = this.props;
|
|
const { id } = _participant;
|
|
|
|
dispatch(approveParticipant(id));
|
|
}
|
|
|
|
|
|
/**
|
|
* Implements React's {@link Component#render()}.
|
|
*
|
|
* @inheritdoc
|
|
* @returns {ReactElement}
|
|
*/
|
|
render() {
|
|
const {
|
|
_isAudioForceMuted,
|
|
_currentRoomId,
|
|
_isLocalModerator,
|
|
_isChatButtonEnabled,
|
|
_isParticipantModerator,
|
|
_isParticipantVideoMuted,
|
|
_isParticipantAudioMuted,
|
|
_isVideoForceMuted,
|
|
_localVideoOwner,
|
|
_participant,
|
|
_rooms,
|
|
_volume = 1,
|
|
classes,
|
|
closeDrawer,
|
|
drawerParticipant,
|
|
offsetTarget,
|
|
onEnter,
|
|
onLeave,
|
|
onSelect,
|
|
overflowDrawer,
|
|
muteAudio,
|
|
t
|
|
} = this.props;
|
|
|
|
if (!_participant) {
|
|
return null;
|
|
}
|
|
|
|
const showVolumeSlider = !isIosMobileBrowser()
|
|
&& overflowDrawer
|
|
&& typeof _volume === 'number'
|
|
&& !isNaN(_volume);
|
|
|
|
const fakeParticipantActions = [ {
|
|
accessibilityLabel: t('toolbar.stopSharedVideo'),
|
|
icon: IconShareVideo,
|
|
onClick: this._onStopSharedVideo,
|
|
text: t('toolbar.stopSharedVideo')
|
|
} ];
|
|
|
|
const moderatorActions1 = [
|
|
overflowDrawer && (_isAudioForceMuted || _isVideoForceMuted) ? {
|
|
accessibilityLabel: t(_isAudioForceMuted
|
|
? 'participantsPane.actions.askUnmute'
|
|
: 'participantsPane.actions.allowVideo'),
|
|
icon: IconMicrophone,
|
|
onClick: this._onAskToUnmute,
|
|
text: t(_isAudioForceMuted
|
|
? 'participantsPane.actions.askUnmute'
|
|
: 'participantsPane.actions.allowVideo')
|
|
} : null,
|
|
!_isParticipantAudioMuted && overflowDrawer ? {
|
|
accessibilityLabel: t('dialog.muteParticipantButton'),
|
|
icon: IconMicDisabled,
|
|
onClick: muteAudio(_participant),
|
|
text: t('dialog.muteParticipantButton')
|
|
} : null, {
|
|
accessibilityLabel: t('toolbar.accessibilityLabel.muteEveryoneElse'),
|
|
icon: IconMuteEveryoneElse,
|
|
onClick: this._onMuteEveryoneElse,
|
|
text: t('toolbar.accessibilityLabel.muteEveryoneElse')
|
|
},
|
|
_isParticipantVideoMuted ? null : {
|
|
accessibilityLabel: t('participantsPane.actions.stopVideo'),
|
|
icon: IconVideoOff,
|
|
onClick: this._onMuteVideo,
|
|
text: t('participantsPane.actions.stopVideo')
|
|
}
|
|
].filter(Boolean);
|
|
|
|
const moderatorActions2 = [
|
|
_isLocalModerator && !_isParticipantModerator ? {
|
|
accessibilityLabel: t('toolbar.accessibilityLabel.grantModerator'),
|
|
icon: IconCrown,
|
|
onClick: this._onGrantModerator,
|
|
text: t('toolbar.accessibilityLabel.grantModerator')
|
|
} : null,
|
|
_isLocalModerator ? {
|
|
accessibilityLabel: t('videothumbnail.kick'),
|
|
icon: IconCloseCircle,
|
|
onClick: this._onKick,
|
|
text: t('videothumbnail.kick')
|
|
} : null,
|
|
_isChatButtonEnabled ? {
|
|
accessibilityLabel: t('toolbar.accessibilityLabel.privateMessage'),
|
|
icon: IconMessage,
|
|
onClick: this._onSendPrivateMessage,
|
|
text: t('toolbar.accessibilityLabel.privateMessage')
|
|
} : null
|
|
].filter(Boolean);
|
|
|
|
const breakoutRoomActions = _rooms.map(room => {
|
|
if (room.id !== _currentRoomId) {
|
|
return {
|
|
accessibilityLabel: room.name || t('breakoutRooms.mainRoom'),
|
|
icon: IconRingGroup,
|
|
onClick: this._onSendToRoom(room),
|
|
text: room.name || t('breakoutRooms.mainRoom')
|
|
};
|
|
}
|
|
|
|
return null;
|
|
}
|
|
).filter(Boolean);
|
|
|
|
const actions
|
|
= _participant?.isFakeParticipant ? (
|
|
<>
|
|
{_localVideoOwner && (
|
|
<ContextMenuItemGroup
|
|
actions = { fakeParticipantActions } />
|
|
)}
|
|
</>
|
|
) : (
|
|
<>
|
|
{_isLocalModerator
|
|
&& <ContextMenuItemGroup actions = { moderatorActions1 } />
|
|
}
|
|
|
|
<ContextMenuItemGroup actions = { moderatorActions2 } />
|
|
|
|
{
|
|
_isLocalModerator && _rooms.length > 1
|
|
&& <ContextMenuItemGroup actions = { breakoutRoomActions } >
|
|
<div className = { classes && classes.text }>
|
|
{t('breakoutRooms.actions.sendToBreakoutRoom')}
|
|
</div>
|
|
</ContextMenuItemGroup>
|
|
}
|
|
{ showVolumeSlider
|
|
&& <ContextMenuItemGroup>
|
|
<VolumeSlider
|
|
initialValue = { _volume }
|
|
key = 'volume-slider'
|
|
onChange = { this._onVolumeChange } />
|
|
</ContextMenuItemGroup>
|
|
}
|
|
</>
|
|
);
|
|
|
|
return (
|
|
<ContextMenu
|
|
entity = { _participant }
|
|
isDrawerOpen = { drawerParticipant }
|
|
offsetTarget = { offsetTarget }
|
|
onClick = { onSelect }
|
|
onDrawerClose = { closeDrawer }
|
|
onMouseEnter = { onEnter }
|
|
onMouseLeave = { onLeave }>
|
|
{overflowDrawer && <ContextMenuItemGroup
|
|
actions = { [ {
|
|
accessibilityLabel: drawerParticipant && drawerParticipant.displayName,
|
|
customIcon: <Avatar
|
|
participantId = { drawerParticipant && drawerParticipant.participantID }
|
|
size = { 20 } />,
|
|
text: drawerParticipant && drawerParticipant.displayName
|
|
} ] } />}
|
|
{actions}
|
|
</ContextMenu>
|
|
);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Maps (parts of) the redux state to the associated props for this component.
|
|
*
|
|
* @param {Object} state - The Redux state.
|
|
* @param {Object} ownProps - The own props of the component.
|
|
* @private
|
|
* @returns {Props}
|
|
*/
|
|
function _mapStateToProps(state, ownProps): Object {
|
|
const { participantID, overflowDrawer, drawerParticipant } = ownProps;
|
|
const { ownerId } = state['features/shared-video'];
|
|
const localParticipantId = getLocalParticipant(state).id;
|
|
|
|
const participant = getParticipantByIdOrUndefined(state,
|
|
overflowDrawer ? drawerParticipant?.participantID : participantID);
|
|
|
|
const _currentRoomId = getCurrentRoomId(state);
|
|
const _isLocalModerator = isLocalParticipantModerator(state);
|
|
const _isChatButtonEnabled = isToolbarButtonEnabled('chat', state);
|
|
const _isParticipantVideoMuted = isParticipantVideoMuted(participant, state);
|
|
const _isParticipantAudioMuted = isParticipantAudioMuted(participant, state);
|
|
const _isParticipantModerator = isParticipantModerator(participant);
|
|
const _rooms = Object.values(getBreakoutRooms(state));
|
|
|
|
const { participantsVolume } = state['features/filmstrip'];
|
|
const id = participant?.id;
|
|
const isLocal = participant?.local ?? true;
|
|
|
|
return {
|
|
_isAudioForceMuted: isForceMuted(participant, MEDIA_TYPE.AUDIO, state),
|
|
_currentRoomId,
|
|
_isLocalModerator,
|
|
_isChatButtonEnabled,
|
|
_isParticipantModerator,
|
|
_isParticipantVideoMuted,
|
|
_isParticipantAudioMuted,
|
|
_isVideoForceMuted: isForceMuted(participant, MEDIA_TYPE.VIDEO, state),
|
|
_localVideoOwner: Boolean(ownerId === localParticipantId),
|
|
_participant: participant,
|
|
_rooms,
|
|
_volume: isLocal ? undefined : id ? participantsVolume[id] : undefined
|
|
};
|
|
}
|
|
|
|
export default translate(connect(_mapStateToProps)(withStyles(styles)(MeetingParticipantContextMenu)));
|