// @flow import React, { Component } from 'react'; import { connect } from 'react-redux'; import { ACTION_SHORTCUT_TRIGGERED, createShortcutEvent, createToolbarEvent, sendAnalytics } from '../../../analytics'; import { openDialog } from '../../../base/dialog'; import { translate } from '../../../base/i18n'; import { JitsiRecordingConstants } from '../../../base/lib-jitsi-meet'; import { PARTICIPANT_ROLE, getLocalParticipant, participantUpdated } from '../../../base/participants'; import { getLocalVideoTrack, toggleScreensharing } from '../../../base/tracks'; import { ChatCounter } from '../../../chat'; import { toggleDocument } from '../../../etherpad'; import { openFeedbackDialog } from '../../../feedback'; import { beginAddPeople, InfoDialogButton, isAddPeopleEnabled, isDialOutEnabled } from '../../../invite'; import { openKeyboardShortcutsDialog } from '../../../keyboard-shortcuts'; import { StartLiveStreamDialog, StartRecordingDialog, StopLiveStreamDialog, StopRecordingDialog, getActiveSession } from '../../../recording'; import { SettingsButton } from '../../../settings'; import { toggleSharedVideo } from '../../../shared-video'; import { toggleChat, toggleProfile } from '../../../side-panel'; import { SpeakerStats } from '../../../speaker-stats'; import { OverflowMenuVideoQualityItem, VideoQualityDialog } from '../../../video-quality'; import { setFullScreen, setOverflowMenuVisible, setToolbarHovered } from '../../actions'; import AudioMuteButton from '../AudioMuteButton'; import HangupButton from '../HangupButton'; import OverflowMenuButton from './OverflowMenuButton'; import OverflowMenuItem from './OverflowMenuItem'; import OverflowMenuProfileItem from './OverflowMenuProfileItem'; import ToolbarButton from './ToolbarButton'; import VideoMuteButton from '../VideoMuteButton'; /** * The type of the React {@code Component} props of {@link Toolbox}. */ type Props = { /** * Whether or not the chat feature is currently displayed. */ _chatOpen: boolean, /** * The {@code JitsiConference} for the current conference. */ _conference: Object, /** * Whether or not desktopsharing was explicitly configured to be disabled. */ _desktopSharingDisabledByConfig: boolean, /** * Whether or not screensharing is initialized. */ _desktopSharingEnabled: boolean, /** * Whether or not a dialog is displayed. */ _dialog: boolean, /** * Whether or not the local participant is currently editing a document. */ _editingDocument: boolean, /** * Whether or not collaborative document editing is enabled. */ _etherpadInitialized: boolean, /** * Whether or not call feedback can be sent. */ _feedbackConfigured: boolean, /** * The current file recording session, if any. */ _fileRecordingSession: Object, /** * Whether or not the app is currently in full screen. */ _fullScreen: boolean, /** * Whether or not invite should be hidden, regardless of feature * availability. */ _hideInviteButton: boolean, /** * Whether or not the current user is logged in through a JWT. */ _isGuest: boolean, /** * The current live streaming session, if any. */ _liveStreamingSession: ?Object, /** * The ID of the local participant. */ _localParticipantID: String, /** * Whether or not the overflow menu is visible. */ _overflowMenuVisible: boolean, /** * Whether or not the local participant's hand is raised. */ _raisedHand: boolean, /** * Whether or not the recording feature is enabled for use. */ _recordingEnabled: boolean, /** * Whether or not the local participant is screensharing. */ _screensharing: boolean, /** * Whether or not the local participant is sharing a YouTube video. */ _sharingVideo: boolean, /** * Flag showing whether toolbar is visible. */ _visible: boolean, /** * Set with the buttons which this Toolbox should display. */ _visibleButtons: Set, /** * Invoked to active other features of the app. */ dispatch: Function, /** * Invoked to obtain translated strings. */ t: Function }; declare var APP: Object; declare var interfaceConfig: Object; /** * Implements the conference toolbox on React/Web. * * @extends Component */ class Toolbox extends Component { /** * Initializes a new {@code Toolbox} instance. * * @param {Props} props - The read-only React {@code Component} props with * which the new instance is to be initialized. */ constructor(props: Props) { super(props); // Bind event handlers so they are only bound once per instance. this._onMouseOut = this._onMouseOut.bind(this); this._onMouseOver = this._onMouseOver.bind(this); this._onSetOverflowVisible = this._onSetOverflowVisible.bind(this); this._onShortcutToggleChat = this._onShortcutToggleChat.bind(this); this._onShortcutToggleFullScreen = this._onShortcutToggleFullScreen.bind(this); this._onShortcutToggleRaiseHand = this._onShortcutToggleRaiseHand.bind(this); this._onShortcutToggleScreenshare = this._onShortcutToggleScreenshare.bind(this); this._onToolbarOpenFeedback = this._onToolbarOpenFeedback.bind(this); this._onToolbarOpenInvite = this._onToolbarOpenInvite.bind(this); this._onToolbarOpenKeyboardShortcuts = this._onToolbarOpenKeyboardShortcuts.bind(this); this._onToolbarOpenSpeakerStats = this._onToolbarOpenSpeakerStats.bind(this); this._onToolbarOpenVideoQuality = this._onToolbarOpenVideoQuality.bind(this); this._onToolbarToggleChat = this._onToolbarToggleChat.bind(this); this._onToolbarToggleEtherpad = this._onToolbarToggleEtherpad.bind(this); this._onToolbarToggleFullScreen = this._onToolbarToggleFullScreen.bind(this); this._onToolbarToggleLiveStreaming = this._onToolbarToggleLiveStreaming.bind(this); this._onToolbarToggleProfile = this._onToolbarToggleProfile.bind(this); this._onToolbarToggleRaiseHand = this._onToolbarToggleRaiseHand.bind(this); this._onToolbarToggleRecording = this._onToolbarToggleRecording.bind(this); this._onToolbarToggleScreenshare = this._onToolbarToggleScreenshare.bind(this); this._onToolbarToggleSharedVideo = this._onToolbarToggleSharedVideo.bind(this); } /** * Sets keyboard shortcuts for to trigger ToolbarButtons actions. * * @inheritdoc * @returns {void} */ componentDidMount() { const KEYBOARD_SHORTCUTS = [ this._shouldShowButton('chat') && { character: 'C', exec: this._onShortcutToggleChat, helpDescription: 'keyboardShortcuts.toggleChat' }, this._shouldShowButton('desktop') && { character: 'D', exec: this._onShortcutToggleScreenshare, helpDescription: 'keyboardShortcuts.toggleScreensharing' }, this._shouldShowButton('raisehand') && { character: 'R', exec: this._onShortcutToggleRaiseHand, helpDescription: 'keyboardShortcuts.raiseHand' }, this._shouldShowButton('fullscreen') && { character: 'S', exec: this._onShortcutToggleFullScreen, helpDescription: 'keyboardShortcuts.fullScreen' } ]; KEYBOARD_SHORTCUTS.forEach(shortcut => { if (typeof shortcut === 'object') { APP.keyboardshortcut.registerShortcut( shortcut.character, null, shortcut.exec, shortcut.helpDescription); } }); } /** * Update the visibility of the {@code OverflowMenuButton}. * * @inheritdoc */ componentWillReceiveProps(nextProps) { // Ensure the dialog is closed when the toolbox becomes hidden. if (this.props._overflowMenuVisible && !nextProps._visible) { this._onSetOverflowVisible(false); } if (this.props._overflowMenuVisible && !this.props._dialog && nextProps._dialog) { this._onSetOverflowVisible(false); this.props.dispatch(setToolbarHovered(false)); } } /** * Removes keyboard shortcuts registered by this component. * * @inheritdoc * @returns {void} */ componentWillUnmount() { [ 'C', 'D', 'R', 'S' ].forEach(letter => APP.keyboardshortcut.unregisterShortcut(letter)); } /** * Implements React's {@link Component#render()}. * * @inheritdoc * @returns {ReactElement} */ render() { const { _chatOpen, _hideInviteButton, _overflowMenuVisible, _raisedHand, _visible, _visibleButtons, t } = this.props; const rootClassNames = `new-toolbox ${_visible ? 'visible' : ''} ${ _visibleButtons.size ? '' : 'no-buttons'}`; const overflowMenuContent = this._renderOverflowMenuContent(); const overflowHasItems = Boolean(overflowMenuContent.filter( child => child).length); return (
{ this._shouldShowButton('desktop') && this._renderDesktopSharingButton() } { this._shouldShowButton('raisehand') && } { this._shouldShowButton('chat') &&
}
{ this._shouldShowButton('invite') && !_hideInviteButton && } { this._shouldShowButton('info') && } { overflowHasItems &&
    { overflowMenuContent }
}
); } /** * Callback invoked to display {@code FeedbackDialog}. * * @private * @returns {void} */ _doOpenFeedback() { const { _conference } = this.props; this.props.dispatch(openFeedbackDialog(_conference)); } /** * Dispatches an action to display {@code KeyboardShortcuts}. * * @private * @returns {void} */ _doOpenKeyboardShorcuts() { this.props.dispatch(openKeyboardShortcutsDialog()); } /** * Callback invoked to display {@code SpeakerStats}. * * @private * @returns {void} */ _doOpenSpeakerStats() { this.props.dispatch(openDialog(SpeakerStats, { conference: this.props._conference })); } /** * Dispatches an action to toggle the video quality dialog. * * @private * @returns {void} */ _doOpenVideoQuality() { this.props.dispatch(openDialog(VideoQualityDialog)); } /** * Dispatches an action to toggle the display of chat. * * @private * @returns {void} */ _doToggleChat() { this.props.dispatch(toggleChat()); } /** * Dispatches an action to show or hide document editing. * * @private * @returns {void} */ _doToggleEtherpad() { this.props.dispatch(toggleDocument()); } /** * Dispatches an action to toggle screensharing. * * @private * @returns {void} */ _doToggleFullScreen() { const fullScreen = !this.props._fullScreen; this.props.dispatch(setFullScreen(fullScreen)); } /** * Dispatches an action to show a dialog for starting or stopping a live * streaming session. * * @private * @returns {void} */ _doToggleLiveStreaming() { const { _liveStreamingSession } = this.props; const dialogToDisplay = _liveStreamingSession ? StopLiveStreamDialog : StartLiveStreamDialog; this.props.dispatch( openDialog(dialogToDisplay, { session: _liveStreamingSession })); } /** * Dispatches an action to show or hide the profile edit panel. * * @private * @returns {void} */ _doToggleProfile() { this.props.dispatch(toggleProfile()); } /** * Dispatches an action to toggle the local participant's raised hand state. * * @private * @returns {void} */ _doToggleRaiseHand() { const { _localParticipantID, _raisedHand } = this.props; this.props.dispatch(participantUpdated({ id: _localParticipantID, local: true, raisedHand: !_raisedHand })); } /** * Dispatches an action to toggle recording. * * @private * @returns {void} */ _doToggleRecording() { const { _fileRecordingSession } = this.props; const dialog = _fileRecordingSession ? StopRecordingDialog : StartRecordingDialog; this.props.dispatch( openDialog(dialog, { session: _fileRecordingSession })); } /** * Dispatches an action to toggle screensharing. * * @private * @returns {void} */ _doToggleScreenshare() { if (this.props._desktopSharingEnabled) { this.props.dispatch(toggleScreensharing()); } } /** * Dispatches an action to toggle YouTube video sharing. * * @private * @returns {void} */ _doToggleSharedVideo() { this.props.dispatch(toggleSharedVideo()); } _onMouseOut: () => void; /** * Dispatches an action signaling the toolbar is not being hovered. * * @private * @returns {void} */ _onMouseOut() { this.props.dispatch(setToolbarHovered(false)); } _onMouseOver: () => void; /** * Dispatches an action signaling the toolbar is being hovered. * * @private * @returns {void} */ _onMouseOver() { this.props.dispatch(setToolbarHovered(true)); } _onSetOverflowVisible: (boolean) => void; /** * Sets the visibility of the overflow menu. * * @param {boolean} visible - Whether or not the overflow menu should be * displayed. * @private * @returns {void} */ _onSetOverflowVisible(visible) { this.props.dispatch(setOverflowMenuVisible(visible)); } _onShortcutToggleChat: () => void; /** * Creates an analytics keyboard shortcut event and dispatches an action for * toggling the display of chat. * * @private * @returns {void} */ _onShortcutToggleChat() { sendAnalytics(createShortcutEvent( 'toggle.chat', { enable: !this.props._chatOpen })); this._doToggleChat(); } _onShortcutToggleFullScreen: () => void; /** * Creates an analytics keyboard shortcut event and dispatches an action for * toggling full screen mode. * * @private * @returns {void} */ _onShortcutToggleFullScreen() { sendAnalytics(createShortcutEvent( 'toggle.fullscreen', { enable: !this.props._fullScreen })); this._doToggleFullScreen(); } _onShortcutToggleRaiseHand: () => void; /** * Creates an analytics keyboard shortcut event and dispatches an action for * toggling raise hand. * * @private * @returns {void} */ _onShortcutToggleRaiseHand() { sendAnalytics(createShortcutEvent( 'toggle.raise.hand', ACTION_SHORTCUT_TRIGGERED, { enable: !this.props._raisedHand })); this._doToggleRaiseHand(); } _onShortcutToggleScreenshare: () => void; /** * Creates an analytics keyboard shortcut event and dispatches an action for * toggling screensharing. * * @private * @returns {void} */ _onShortcutToggleScreenshare() { sendAnalytics(createToolbarEvent( 'screen.sharing', { enable: !this.props._screensharing })); this._doToggleScreenshare(); } _onToolbarOpenFeedback: () => void; /** * Creates an analytics toolbar event and dispatches an action for toggling * display of feedback. * * @private * @returns {void} */ _onToolbarOpenFeedback() { sendAnalytics(createToolbarEvent('feedback')); this._doOpenFeedback(); } _onToolbarOpenInvite: () => void; /** * Creates an analytics toolbar event and dispatches an action for opening * the modal for inviting people directly into the conference. * * @private * @returns {void} */ _onToolbarOpenInvite() { sendAnalytics(createToolbarEvent('invite')); this.props.dispatch(beginAddPeople()); } _onToolbarOpenKeyboardShortcuts: () => void; /** * Creates an analytics toolbar event and dispatches an action for opening * the modal for showing available keyboard shortcuts. * * @private * @returns {void} */ _onToolbarOpenKeyboardShortcuts() { sendAnalytics(createToolbarEvent('shortcuts')); this._doOpenKeyboardShorcuts(); } _onToolbarOpenSpeakerStats: () => void; /** * Creates an analytics toolbar event and dispatches an action for opening * the speaker stats modal. * * @private * @returns {void} */ _onToolbarOpenSpeakerStats() { sendAnalytics(createToolbarEvent('speaker.stats')); this._doOpenSpeakerStats(); } _onToolbarOpenVideoQuality: () => void; /** * Creates an analytics toolbar event and dispatches an action for toggling * open the video quality dialog. * * @private * @returns {void} */ _onToolbarOpenVideoQuality() { sendAnalytics(createToolbarEvent('video.quality')); this._doOpenVideoQuality(); } _onToolbarToggleChat: () => void; /** * Creates an analytics toolbar event and dispatches an action for toggling * the display of chat. * * @private * @returns {void} */ _onToolbarToggleChat() { sendAnalytics(createToolbarEvent( 'toggle.chat', { enable: !this.props._chatOpen })); this._doToggleChat(); } _onToolbarToggleEtherpad: () => void; /** * Creates an analytics toolbar event and dispatches an action for toggling * the display of document editing. * * @private * @returns {void} */ _onToolbarToggleEtherpad() { sendAnalytics(createToolbarEvent( 'toggle.etherpad', { enable: !this.props._editingDocument })); this._doToggleEtherpad(); } _onToolbarToggleFullScreen: () => void; /** * Creates an analytics toolbar event and dispatches an action for toggling * full screen mode. * * @private * @returns {void} */ _onToolbarToggleFullScreen() { sendAnalytics(createToolbarEvent( 'toggle.fullscreen', { enable: !this.props._fullScreen })); this._doToggleFullScreen(); } _onToolbarToggleLiveStreaming: () => void; /** * Starts the process for enabling or disabling live streaming. * * @private * @returns {void} */ _onToolbarToggleLiveStreaming() { sendAnalytics(createToolbarEvent( 'livestreaming.button', { 'is_streaming': Boolean(this.props._liveStreamingSession), type: JitsiRecordingConstants.mode.STREAM })); this._doToggleLiveStreaming(); } _onToolbarToggleProfile: () => void; /** * Creates an analytics toolbar event and dispatches an action for showing * or hiding the profile edit panel. * * @private * @returns {void} */ _onToolbarToggleProfile() { sendAnalytics(createToolbarEvent('profile')); this._doToggleProfile(); } _onToolbarToggleRaiseHand: () => void; /** * Creates an analytics toolbar event and dispatches an action for toggling * raise hand. * * @private * @returns {void} */ _onToolbarToggleRaiseHand() { sendAnalytics(createToolbarEvent( 'raise.hand', { enable: !this.props._raisedHand })); this._doToggleRaiseHand(); } _onToolbarToggleRecording: () => void; /** * Dispatches an action to toggle recording. * * @private * @returns {void} */ _onToolbarToggleRecording() { sendAnalytics(createToolbarEvent( 'recording.button', { 'is_recording': Boolean(this.props._fileRecordingSession), type: JitsiRecordingConstants.mode.FILE })); this._doToggleRecording(); } _onToolbarToggleScreenshare: () => void; /** * Creates an analytics toolbar event and dispatches an action for toggling * screensharing. * * @private * @returns {void} */ _onToolbarToggleScreenshare() { if (!this.props._desktopSharingEnabled) { return; } sendAnalytics(createShortcutEvent( 'toggle.screen.sharing', ACTION_SHORTCUT_TRIGGERED, { enable: !this.props._screensharing })); this._doToggleScreenshare(); } _onToolbarToggleSharedVideo: () => void; /** * Creates an analytics toolbar event and dispatches an action for toggling * the sharing of a YouTube video. * * @private * @returns {void} */ _onToolbarToggleSharedVideo() { sendAnalytics(createToolbarEvent('shared.video.toggled', { enable: !this.props._sharingVideo })); this._doToggleSharedVideo(); } /** * Renders a button for toggleing screen sharing. * * @private * @returns {ReactElement|null} */ _renderDesktopSharingButton() { const { _desktopSharingDisabledByConfig, _desktopSharingEnabled, _screensharing, t } = this.props; const disabledTooltipText = interfaceConfig.DESKTOP_SHARING_BUTTON_DISABLED_TOOLTIP; const showDisabledTooltip = disabledTooltipText && _desktopSharingDisabledByConfig; const visible = _desktopSharingEnabled || showDisabledTooltip; if (!visible) { return null; } const classNames = `icon-share-desktop ${ _screensharing ? 'toggled' : ''} ${ _desktopSharingEnabled ? '' : 'disabled'}`; const tooltip = showDisabledTooltip ? disabledTooltipText : t('dialog.shareYourScreen'); return ( ); } /** * Renders an {@code OverflowMenuItem} for starting or stopping a live * streaming of the current conference. * * @private * @returns {ReactElement} */ _renderLiveStreamingButton() { const { _liveStreamingSession, t } = this.props; const translationKey = _liveStreamingSession ? 'dialog.stopLiveStreaming' : 'dialog.startLiveStreaming'; return ( ); } /** * Renders the list elements of the overflow menu. * * @private * @returns {Array} */ _renderOverflowMenuContent() { const { _editingDocument, _etherpadInitialized, _feedbackConfigured, _fullScreen, _isGuest, _recordingEnabled, _sharingVideo, t } = this.props; return [ _isGuest && this._shouldShowButton('profile') && , this._shouldShowButton('videoquality') && , this._shouldShowButton('fullscreen') && , _recordingEnabled && this._shouldShowButton('livestreaming') && this._renderLiveStreamingButton(), _recordingEnabled && this._shouldShowButton('recording') && this._renderRecordingButton(), this._shouldShowButton('sharedvideo') && , this._shouldShowButton('etherpad') && _etherpadInitialized && , , this._shouldShowButton('stats') && , this._shouldShowButton('feedback') && _feedbackConfigured && , this._shouldShowButton('shortcuts') && ]; } /** * Renders an {@code OverflowMenuItem} to start or stop recording of the * current conference. * * @private * @returns {ReactElement|null} */ _renderRecordingButton() { const { _fileRecordingSession, t } = this.props; const translationKey = _fileRecordingSession ? 'dialog.stopRecording' : 'dialog.startRecording'; return ( ); } _shouldShowButton: (string) => boolean; /** * Returns if a button name has been explicitly configured to be displayed. * * @param {string} buttonName - The name of the button, as expected in * {@link intefaceConfig}. * @private * @returns {boolean} True if the button should be displayed. */ _shouldShowButton(buttonName) { return this.props._visibleButtons.has(buttonName); } } /** * Maps (parts of) the redux state to {@link Toolbox}'s React {@code Component} * props. * * @param {Object} state - The redux store/state. * @private * @returns {{}} */ function _mapStateToProps(state) { const { conference, desktopSharingEnabled } = state['features/base/conference']; const { callStatsID, disableDesktopSharing, enableRecording, iAmRecorder } = state['features/base/config']; const sharedVideoStatus = state['features/shared-video'].status; const { current } = state['features/side-panel']; const { alwaysVisible, fullScreen, overflowMenuVisible, timeoutID, visible } = state['features/toolbox']; const localParticipant = getLocalParticipant(state); const localVideo = getLocalVideoTrack(state['features/base/tracks']); const isModerator = localParticipant.role === PARTICIPANT_ROLE.MODERATOR; const addPeopleEnabled = isAddPeopleEnabled(state); const dialOutEnabled = isDialOutEnabled(state); return { _chatOpen: current === 'chat_container', _conference: conference, _desktopSharingEnabled: desktopSharingEnabled, _desktopSharingDisabledByConfig: disableDesktopSharing, _dialog: Boolean(state['features/base/dialog'].component), _editingDocument: Boolean(state['features/etherpad'].editing), _etherpadInitialized: Boolean(state['features/etherpad'].initialized), _feedbackConfigured: Boolean(callStatsID), _hideInviteButton: iAmRecorder || (!addPeopleEnabled && !dialOutEnabled), _isGuest: state['features/base/jwt'].isGuest, _fileRecordingSession: getActiveSession(state, JitsiRecordingConstants.mode.FILE), _fullScreen: fullScreen, _liveStreamingSession: getActiveSession(state, JitsiRecordingConstants.mode.STREAM), _localParticipantID: localParticipant.id, _overflowMenuVisible: overflowMenuVisible, _raisedHand: localParticipant.raisedHand, _recordingEnabled: isModerator && enableRecording, _screensharing: localVideo && localVideo.videoType === 'desktop', _sharingVideo: sharedVideoStatus === 'playing' || sharedVideoStatus === 'start' || sharedVideoStatus === 'pause', _visible: Boolean(timeoutID || visible || alwaysVisible), // XXX: We are not currently using state here, but in the future, when // interfaceConfig is part of redux we will. _visibleButtons: new Set(interfaceConfig.TOOLBAR_BUTTONS) }; } export default translate(connect(_mapStateToProps)(Toolbox));