// @flow import React, { Component } from 'react'; import { isToolbarButtonEnabled } from '../../base/config/functions.web'; import { openDialog } from '../../base/dialog'; import { translate } from '../../base/i18n'; import { IconCloseCircle, IconCrown, IconMessage, IconMicDisabled, IconMuteEveryoneElse, IconVideoOff } from '../../base/icons'; import { getParticipantByIdOrUndefined, isLocalParticipantModerator, isParticipantModerator } from '../../base/participants'; import { connect } from '../../base/redux'; import { isParticipantAudioMuted, isParticipantVideoMuted } from '../../base/tracks'; import { openChat } from '../../chat/actions'; import { GrantModeratorDialog, KickRemoteParticipantDialog, MuteEveryoneDialog } from '../../video-menu'; import MuteRemoteParticipantsVideoDialog from '../../video-menu/components/web/MuteRemoteParticipantsVideoDialog'; import { getComputedOuterHeight } from '../functions'; import { ContextMenu, ContextMenuIcon, ContextMenuItem, ContextMenuItemGroup, ignoredChildClassName } from './styled'; type Props = { /** * 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, /** * Participant reference */ _participant: Object, /** * The dispatch function from redux. */ dispatch: Function, /** * 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, /** * The translate function. */ t: Function }; type State = { /** * If true the context menu will be hidden. */ isHidden: boolean }; /** * Implements the MeetingParticipantContextMenu component. */ class MeetingParticipantContextMenu extends Component { /** * Reference to the context menu container div. */ _containerRef: Object; /** * Creates new instance of MeetingParticipantContextMenu. * * @param {Props} props - The props. */ constructor(props: Props) { super(props); this.state = { isHidden: true }; this._containerRef = React.createRef(); 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._position = this._position.bind(this); } _onGrantModerator: () => void; /** * Grant moderator permissions. * * @returns {void} */ _onGrantModerator() { const { _participant, dispatch } = this.props; dispatch(openDialog(GrantModeratorDialog, { participantID: _participant?.id })); } _onKick: () => void; /** * Kicks the participant. * * @returns {void} */ _onKick() { const { _participant, dispatch } = this.props; dispatch(openDialog(KickRemoteParticipantDialog, { participantID: _participant?.id })); } _onMuteEveryoneElse: () => void; /** * Mutes everyone else. * * @returns {void} */ _onMuteEveryoneElse() { const { _participant, dispatch } = this.props; dispatch(openDialog(MuteEveryoneDialog, { exclude: [ _participant?.id ] })); } _onMuteVideo: () => void; /** * Mutes the video of the selected participant. * * @returns {void} */ _onMuteVideo() { const { _participant, dispatch } = this.props; dispatch(openDialog(MuteRemoteParticipantsVideoDialog, { participantID: _participant?.id })); } _onSendPrivateMessage: () => void; /** * Sends private message. * * @returns {void} */ _onSendPrivateMessage() { const { _participant, dispatch } = this.props; dispatch(openChat(_participant)); } _position: () => void; /** * Positions the context menu. * * @returns {void} */ _position() { const { _participant, offsetTarget } = this.props; if (_participant && this._containerRef.current && offsetTarget?.offsetParent && offsetTarget.offsetParent instanceof HTMLElement ) { const { current: container } = this._containerRef; const { offsetTop, offsetParent: { offsetHeight, scrollTop } } = offsetTarget; const outerHeight = getComputedOuterHeight(container); container.style.top = offsetTop + outerHeight > offsetHeight + scrollTop ? offsetTop - outerHeight : offsetTop; this.setState({ isHidden: false }); } else { this.setState({ isHidden: true }); } } /** * Implements React Component's componentDidMount. * * @inheritdoc * @returns {void} */ componentDidMount() { this._position(); } /** * Implements React Component's componentDidUpdate. * * @inheritdoc */ componentDidUpdate(prevProps: Props) { if (prevProps.offsetTarget !== this.props.offsetTarget || prevProps._participant !== this.props._participant) { this._position(); } } /** * Implements React's {@link Component#render()}. * * @inheritdoc * @returns {ReactElement} */ render() { const { _isLocalModerator, _isChatButtonEnabled, _isParticipantModerator, _isParticipantVideoMuted, _isParticipantAudioMuted, _participant, onEnter, onLeave, onSelect, muteAudio, t } = this.props; if (!_participant) { return null; } return ( { _isLocalModerator && ( <> { !_isParticipantAudioMuted && {t('dialog.muteParticipantButton')} } {t('toolbar.accessibilityLabel.muteEveryoneElse')} ) } { _isLocalModerator && ( _isParticipantVideoMuted || ( {t('participantsPane.actions.stopVideo')} ) ) } { _isLocalModerator && ( <> { !_isParticipantModerator && ( {t('toolbar.accessibilityLabel.grantModerator')} ) } { t('videothumbnail.kick') } ) } { _isChatButtonEnabled && ( {t('toolbar.accessibilityLabel.privateMessage')} ) } ); } } /** * 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 } = ownProps; const participant = getParticipantByIdOrUndefined(state, participantID); const _isLocalModerator = isLocalParticipantModerator(state); const _isChatButtonEnabled = isToolbarButtonEnabled('chat', state); const _isParticipantVideoMuted = isParticipantVideoMuted(participant, state); const _isParticipantAudioMuted = isParticipantAudioMuted(participant, state); const _isParticipantModerator = isParticipantModerator(participant); return { _isLocalModerator, _isChatButtonEnabled, _isParticipantModerator, _isParticipantVideoMuted, _isParticipantAudioMuted, _participant: participant }; } export default translate(connect(_mapStateToProps)(MeetingParticipantContextMenu));