+ );
+ }
+
+ /**
+ * Callback invoked to display {@code FeedbackDialog}.
+ *
+ * @private
+ * @returns {void}
+ */
+ _doOpenFeedback() {
+ this.props.dispatch(openFeedbackDialog());
+ }
+
+ /**
+ * Opens the dialog for inviting people directly into the conference.
+ *
+ * @private
+ * @returns {void}
+ */
+ _doOpenInvite() {
+ const { _addPeopleAvailable, _dialOutAvailable, dispatch } = this.props;
+
+ if (_addPeopleAvailable || _dialOutAvailable) {
+ dispatch(openDialog(AddPeopleDialog, {
+ enableAddPeople: _addPeopleAvailable,
+ enableDialOut: _dialOutAvailable
+ }));
+ }
+ }
+
+ /**
+ * 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 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() {
+ this.props.dispatch(toggleRecording());
+ }
+
+ /**
+ * Dispatches an action to toggle screensharing.
+ *
+ * @private
+ * @returns {void}
+ */
+ _doToggleScreenshare() {
+ if (this.props._desktopSharingEnabled) {
+ this.props.dispatch(toggleScreensharing());
+ }
+ }
+
+ /**
+ * Dispatches an action to toggle display of settings, be it the settings
+ * panel or directly to device selection.
+ *
+ * @private
+ * @returns {void}
+ */
+ _doToggleSettings() {
+ if (interfaceConfig.SETTINGS_SECTIONS.length === 1
+ && interfaceConfig.SETTINGS_SECTIONS.includes('devices')) {
+ this.props.dispatch(openDeviceSelectionDialog());
+ } else {
+ this.props.dispatch(toggleSettings());
+ }
+ }
+
+ /**
+ * 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.setState({ showOverflowMenu: 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._doOpenInvite();
+ }
+
+ _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();
+ }
+
+ _onToolbarToggleOverflowMenu: () => void;
+
+ /**
+ * Callback invoked to change whether the {@code OverflowMenu} is displayed
+ * or not.
+ *
+ * @private
+ * @returns {void}
+ */
+ _onToolbarToggleOverflowMenu() {
+ sendAnalytics(createToolbarEvent('overflow'));
+
+ this.setState({ showOverflowMenu: !this.state.showOverflowMenu });
+ }
+
+ _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() {
+ // No analytics handling is added here for the click as this action will
+ // exercise the old toolbar UI flow, which includes analytics handling.
+
+ this._doToggleRecording();
+ }
+
+ _onToolbarToggleScreenshare: () => void;
+
+ /**
+ * Creates an analytics toolbar event and dispatches an action for toggling
+ * screensharing.
+ *
+ * @private
+ * @returns {void}
+ */
+ _onToolbarToggleScreenshare() {
+ sendAnalytics(createShortcutEvent(
+ 'toggle.screen.sharing',
+ ACTION_SHORTCUT_TRIGGERED,
+ { enable: !this.props._screensharing }));
+
+ this._doToggleScreenshare();
+ }
+
+ _onToolbarToggleSettings: () => void;
+
+ /**
+ * Creates an analytics toolbar event and dispatches an action for toggling
+ * settings display.
+ *
+ * @private
+ * @returns {void}
+ */
+ _onToolbarToggleSettings() {
+ sendAnalytics(createToolbarEvent('settings'));
+
+ this._doToggleSettings();
+ }
+
+ _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 togglein screen sharing.
+ *
+ * @private
+ * @returns {ReactElement}
+ */
+ _renderDesktopSharingButton() {
+ const { _desktopSharingEnabled, _screensharing, t } = this.props;
+ const classNames = `icon-share-desktop ${
+ _screensharing ? 'toggled' : ''} ${
+ _desktopSharingEnabled ? '' : 'disabled'}`;
+ const tooltip = _desktopSharingEnabled
+ ? t('toolbar.sharescreen')
+ : interfaceConfig.DESKTOP_SHARING_BUTTON_DISABLED_TOOLTIP
+ || t('toolbar.sharescreenDisabled');
+
+ return (
+
+ );
+ }
+
+ /**
+ * Renders the list elements of the overflow menu.
+ *
+ * @private
+ * @returns {Array}
+ */
+ _renderOverflowMenuContent() {
+ const {
+ _editingDocument,
+ _etherpadInitialized,
+ _feedbackConfigured,
+ _fullScreen,
+ _sharingVideo,
+ t
+ } = this.props;
+
+ return [
+ this._shouldShowButton('profile')
+ && ,
+ this._shouldShowButton('settings')
+ && ,
+ this._shouldShowButton('sharedvideo')
+ && ,
+ this._shouldShowButton('etherpad')
+ && _etherpadInitialized
+ && ,
+ this._shouldShowButton('fullscreen')
+ && ,
+ this._renderRecordingButton(),
+ this._shouldShowButton('videoquality')
+ && ,
+ this._shouldShowButton('stats')
+ && ,
+ this._shouldShowButton('feedback')
+ && _feedbackConfigured
+ && ,
+ this._shouldShowButton('shortcuts')
+ &&
+ ];
+ }
+
+ /**
+ * Renders an {@code OverflowMenuItem} depending on the current recording
+ * state.
+ *
+ * @private
+ * @returns {ReactElement|null}
+ */
+ _renderRecordingButton() {
+ const {
+ _isRecording,
+ _recordingEnabled,
+ _recordingType,
+ t
+ } = this.props;
+
+ if (!_recordingEnabled || !this._shouldShowButton('recording')) {
+ return null;
+ }
+
+ let translationKey;
+
+ if (_recordingType === RECORDING_TYPES.JIBRI) {
+ translationKey = _isRecording
+ ? 'dialog.stopLiveStreaming'
+ : 'dialog.startLiveStreaming';
+ } else {
+ translationKey = _isRecording
+ ? '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._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,
+ enableRecording,
+ enableUserRolesBasedOnToken
+ } = state['features/base/config'];
+ const { isGuest } = state['features/base/jwt'];
+ const { isRecording, recordingType } = state['features/recording'];
+ const sharedVideoStatus = state['features/shared-video'].status;
+ const { current } = state['features/side-panel'];
+ const {
+ alwaysVisible,
+ fullScreen,
+ timeoutID,
+ visible
+ } = state['features/toolbox'];
+ const localParticipant = getLocalParticipant(state);
+ const localVideo = getLocalVideoTrack(state['features/base/tracks']);
+ const isModerator = localParticipant.role === PARTICIPANT_ROLE.MODERATOR;
+
+ return {
+ _addPeopleAvailable: !isGuest,
+ _chatOpen: current === 'chat_container',
+ _conference: conference,
+ _desktopSharingEnabled: desktopSharingEnabled,
+ _dialOutAvailable: isModerator
+ && conference && conference.isSIPCallingSupported()
+ && (!enableUserRolesBasedOnToken || !isGuest),
+ _dialog: Boolean(state['features/base/dialog'].component),
+ _editingDocument: Boolean(state['features/etherpad'].editing),
+ _etherpadInitialized: Boolean(state['features/etherpad'].initialized),
+ _feedbackConfigured: Boolean(callStatsID),
+ _isRecording: isRecording,
+ _fullScreen: fullScreen,
+ _localParticipantID: localParticipant.id,
+ _raisedHand: localParticipant.raisedHand,
+ _recordingEnabled: isModerator && enableRecording
+ && (conference && conference.isRecordingSupported()),
+ _recordingType: recordingType,
+ _screensharing: localVideo && localVideo.videoType === 'desktop',
+ _sharingVideo: sharedVideoStatus === 'playing'
+ || sharedVideoStatus === 'start'
+ || sharedVideoStatus === 'pause',
+ _visible: Boolean(timeoutID || visible || alwaysVisible)
+ };
+}
+
+export default translate(connect(_mapStateToProps)(ToolboxV2));
diff --git a/react/features/toolbox/components/buttons/AbstractAudioMuteButton.js b/react/features/toolbox/components/buttons/AbstractAudioMuteButton.js
new file mode 100644
index 000000000..c86e9a127
--- /dev/null
+++ b/react/features/toolbox/components/buttons/AbstractAudioMuteButton.js
@@ -0,0 +1,87 @@
+// @flow
+
+import PropTypes from 'prop-types';
+import { Component } from 'react';
+
+import {
+ AUDIO_MUTE,
+ createToolbarEvent,
+ sendAnalytics
+} from '../../../analytics';
+import {
+ VIDEO_MUTISM_AUTHORITY,
+ setAudioMuted
+} from '../../../base/media';
+
+/**
+ * An abstract implementation of a button for toggling audio mute.
+ */
+export default class AbstractAudioMuteButton extends Component<*> {
+ /**
+ * {@code AbstractAudioMuteButton} component's property types.
+ *
+ * @static
+ */
+ static propTypes = {
+ /**
+ * Whether or not the local microphone is muted.
+ */
+ _audioMuted: PropTypes.bool,
+
+ /**
+ * Invoked to toggle audio mute.
+ */
+ dispatch: PropTypes.func
+ };
+
+ /**
+ * Initializes a new {@code AbstractAudioMuteButton} instance.
+ *
+ * @param {Props} props - The read-only React {@code Component} props with
+ * which the new instance is to be initialized.
+ */
+ constructor(props: Object) {
+ super(props);
+
+ // Bind event handler so it is only bound once per instance.
+ this._onToolbarToggleAudio = this._onToolbarToggleAudio.bind(this);
+ }
+
+ /**
+ * Dispatches an action to toggle audio mute.
+ *
+ * @private
+ * @returns {void}
+ */
+ _doToggleAudio() {
+ // The user sees the reality i.e. the state of base/tracks and intends
+ // to change reality by tapping on the respective button i.e. the user
+ // sets the state of base/media. Whether the user's intention will turn
+ // into reality is a whole different story which is of no concern to the
+ // tapping.
+ this.props.dispatch(
+ setAudioMuted(
+ !this.props._audioMuted,
+ VIDEO_MUTISM_AUTHORITY.USER,
+ /* ensureTrack */ true));
+ }
+
+ _onToolbarToggleAudio: () => void;
+
+ /**
+ * Creates an analytics toolbar event and dispatches an action for toggling
+ * audio mute.
+ *
+ * @private
+ * @returns {void}
+ */
+ _onToolbarToggleAudio() {
+ sendAnalytics(createToolbarEvent(
+ AUDIO_MUTE,
+ {
+ enable: !this.props._audioMuted
+ }));
+
+ this._doToggleAudio();
+ }
+}
diff --git a/react/features/toolbox/components/buttons/AbstractHangupButton.js b/react/features/toolbox/components/buttons/AbstractHangupButton.js
new file mode 100644
index 000000000..0f6d765ff
--- /dev/null
+++ b/react/features/toolbox/components/buttons/AbstractHangupButton.js
@@ -0,0 +1,51 @@
+// @flow
+
+import { Component } from 'react';
+
+import {
+ createToolbarEvent,
+ sendAnalytics
+} from '../../../analytics';
+
+/**
+ * An abstract implementation of a button for leaving the conference.
+ */
+export default class AbstractHangupButton extends Component<*> {
+ /**
+ * Initializes a new {@code AbstractHangupButton} instance.
+ *
+ * @param {Props} props - The read-only React {@code Component} props with
+ * which the new instance is to be initialized.
+ */
+ constructor(props: Object) {
+ super(props);
+
+ // Bind event handler so it is only bound once per instance.
+ this._onToolbarHangup = this._onToolbarHangup.bind(this);
+ }
+
+ /**
+ * Dispatches an action for leaving the current conference.
+ *
+ * @private
+ * @returns {void}
+ */
+ _doHangup() {
+ /* to be implemented by descendants */
+ }
+
+ _onToolbarHangup: () => void;
+
+ /**
+ * Creates an analytics toolbar event and dispatches an action for leaving
+ * the current conference.
+ *
+ * @private
+ * @returns {void}
+ */
+ _onToolbarHangup() {
+ sendAnalytics(createToolbarEvent('hangup'));
+
+ this._doHangup();
+ }
+}
diff --git a/react/features/toolbox/components/buttons/AbstractVideoMuteButton.js b/react/features/toolbox/components/buttons/AbstractVideoMuteButton.js
new file mode 100644
index 000000000..43f0230f3
--- /dev/null
+++ b/react/features/toolbox/components/buttons/AbstractVideoMuteButton.js
@@ -0,0 +1,88 @@
+// @flow
+
+import PropTypes from 'prop-types';
+import { Component } from 'react';
+
+import {
+ VIDEO_MUTE,
+ createToolbarEvent,
+ sendAnalytics
+} from '../../../analytics';
+import {
+ VIDEO_MUTISM_AUTHORITY,
+ setVideoMuted
+} from '../../../base/media';
+
+/**
+ * An abstract implementation of a button for toggling video mute.
+ */
+export default class AbstractVideoMuteButton extends Component<*> {
+ /**
+ * {@code AbstractVideoMuteButton} component's property types.
+ *
+ * @static
+ */
+ static propTypes = {
+ /**
+ * Whether or not the local camera is muted.
+ */
+ _videoMuted: PropTypes.bool,
+
+ /**
+ * Invoked to toggle video mute.
+ */
+ dispatch: PropTypes.func
+ };
+
+ /**
+ * Initializes a new {@code AbstractVideoMuteButton} instance.
+ *
+ * @param {Props} props - The read-only React {@code Component} props with
+ * which the new instance is to be initialized.
+ */
+ constructor(props: Object) {
+ super(props);
+
+ // Bind event handler so it is only bound once per instance.
+ this._onToolbarToggleVideo = this._onToolbarToggleVideo.bind(this);
+ }
+
+ /**
+ * Dispatches an action to toggle the mute state of the video/camera.
+ *
+ * @private
+ * @returns {void}
+ */
+ _doToggleVideo() {
+ // The user sees the reality i.e. the state of base/tracks and intends
+ // to change reality by tapping on the respective button i.e. the user
+ // sets the state of base/media. Whether the user's intention will turn
+ // into reality is a whole different story which is of no concern to the
+ // tapping.
+ this.props.dispatch(
+ setVideoMuted(
+ !this.props._videoMuted,
+ VIDEO_MUTISM_AUTHORITY.USER,
+ /* ensureTrack */ true));
+ }
+
+
+ _onToolbarToggleVideo: () => void;
+
+ /**
+ * Creates an analytics toolbar event and dispatches an action for toggling
+ * video mute.
+ *
+ * @private
+ * @returns {void}
+ */
+ _onToolbarToggleVideo() {
+ sendAnalytics(createToolbarEvent(
+ VIDEO_MUTE,
+ {
+ enable: !this.props._videoMuted
+ }));
+
+ this._doToggleVideo();
+ }
+}
diff --git a/react/features/toolbox/components/buttons/AudioMuteButton.native.js b/react/features/toolbox/components/buttons/AudioMuteButton.native.js
new file mode 100644
index 000000000..b866ad82a
--- /dev/null
+++ b/react/features/toolbox/components/buttons/AudioMuteButton.native.js
@@ -0,0 +1,73 @@
+// @flow
+
+import PropTypes from 'prop-types';
+import React from 'react';
+import { connect } from 'react-redux';
+
+import { MEDIA_TYPE } from '../../../base/media';
+import { isLocalTrackMuted } from '../../../base/tracks';
+
+import AbstractAudioMuteButton from './AbstractAudioMuteButton';
+import ToolbarButton from '../ToolbarButton';
+
+/**
+ * Component that renders a toolbar button for toggling audio mute.
+ *
+ * @extends AbstractAudioMuteButton
+ */
+export class AudioMuteButton extends AbstractAudioMuteButton {
+ /**
+ * {@code AbstractAudioMuteButton} component's property types.
+ *
+ * @static
+ */
+ static propTypes = {
+ ...AbstractAudioMuteButton.propTypes,
+
+ /**
+ * Styles to be applied to the button and the icon to show.
+ */
+ buttonStyles: PropTypes.object
+ };
+
+ /**
+ * Implements React's {@link Component#render()}.
+ *
+ * @inheritdoc
+ * @returns {ReactElement}
+ */
+ render() {
+ const { buttonStyles } = this.props;
+
+ return (
+
+ );
+ }
+
+ _onToolbarToggleAudio: () => void;
+}
+
+/**
+ * Maps (parts of) the Redux state to the associated props for the
+ * {@code AudioMuteButton} component.
+ *
+ * @param {Object} state - The Redux state.
+ * @private
+ * @returns {{
+ * _audioMuted: boolean,
+ * }}
+ */
+function _mapStateToProps(state) {
+ const tracks = state['features/base/tracks'];
+
+ return {
+ _audioMuted: isLocalTrackMuted(tracks, MEDIA_TYPE.AUDIO)
+ };
+}
+
+
+export default connect(_mapStateToProps)(AudioMuteButton);
diff --git a/react/features/toolbox/components/buttons/AudioMuteButton.web.js b/react/features/toolbox/components/buttons/AudioMuteButton.web.js
new file mode 100644
index 000000000..27f8b6634
--- /dev/null
+++ b/react/features/toolbox/components/buttons/AudioMuteButton.web.js
@@ -0,0 +1,166 @@
+// @flow
+
+import PropTypes from 'prop-types';
+import React from 'react';
+import { connect } from 'react-redux';
+
+import {
+ ACTION_SHORTCUT_TRIGGERED,
+ AUDIO_MUTE,
+ createShortcutEvent,
+ sendAnalytics
+} from '../../../analytics';
+import { translate } from '../../../base/i18n';
+import { MEDIA_TYPE } from '../../../base/media';
+import { isLocalTrackMuted } from '../../../base/tracks';
+
+import AbstractAudioMuteButton from './AbstractAudioMuteButton';
+import ToolbarButtonV2 from '../ToolbarButtonV2';
+
+declare var APP: Object;
+
+/**
+ * Component that renders a toolbar button for toggling audio mute.
+ *
+ * @extends Component
+ */
+export class AudioMuteButton extends AbstractAudioMuteButton {
+ /**
+ * Default values for {@code AudioMuteButton} component's properties.
+ *
+ * @static
+ */
+ static defaultProps = {
+ tooltipPosition: 'top'
+ };
+
+ /**
+ * {@code AudioMuteButton} component's property types.
+ *
+ * @static
+ */
+ static propTypes = {
+ ...AbstractAudioMuteButton.propTypes,
+
+ /**
+ * The {@code JitsiConference} for the current conference.
+ */
+ _conference: PropTypes.object,
+
+ /**
+ * Invoked to update the audio mute status.
+ */
+ dispatch: PropTypes.func,
+
+ /**
+ * Invoked to obtain translated strings.
+ */
+ t: PropTypes.func,
+
+ /**
+ * Where the tooltip should display, relative to the button.
+ */
+ tooltipPosition: PropTypes.string
+ };
+
+ /**
+ * Initializes a new {@code AudioMuteButton} instance.
+ *
+ * @param {Props} props - The read-only React {@code Component} props with
+ * which the new instance is to be initialized.
+ */
+ constructor(props: Object) {
+ super(props);
+
+ // Bind event handlers so it is only bound once per instance.
+ this._onShortcutToggleAudio = this._onShortcutToggleAudio.bind(this);
+ }
+
+ /**
+ * Sets a keyboard shortcuts for toggling audio mute.
+ *
+ * @inheritdoc
+ * @returns {void}
+ */
+ componentDidMount() {
+ APP.keyboardshortcut.registerShortcut(
+ 'M',
+ null,
+ this._onShortcutToggleAudio,
+ 'keyboardShortcuts.mute');
+ }
+
+ /**
+ * Removes the registered keyboard shortcut handler.
+ *
+ * @inheritdoc
+ * @returns {void}
+ */
+ componentWillUnmount() {
+ APP.keyboardshortcut.unregisterShortcut('M');
+ }
+
+ /**
+ * Implements React's {@link Component#render()}.
+ *
+ * @inheritdoc
+ * @returns {ReactElement}
+ */
+ render() {
+ const { _audioMuted, _conference, t, tooltipPosition } = this.props;
+
+ return (
+
+ );
+ }
+
+ _doToggleAudio: () => void;
+
+ _onShortcutToggleAudio: () => void;
+
+ /**
+ * Creates an analytics keyboard shortcut event and dispatches an action for
+ * toggling audio mute.
+ *
+ * @private
+ * @returns {void}
+ */
+ _onShortcutToggleAudio() {
+ sendAnalytics(createShortcutEvent(
+ AUDIO_MUTE,
+ ACTION_SHORTCUT_TRIGGERED,
+ { enable: !this.props._audioMuted }));
+
+ this._doToggleAudio();
+ }
+
+ _onToolbarToggleAudio: () => void;
+}
+
+/**
+ * Maps (parts of) the Redux state to the associated props for the
+ * {@code AudioMuteButton} component.
+ *
+ * @param {Object} state - The Redux state.
+ * @private
+ * @returns {{
+ * _audioMuted: boolean,
+ * _conference: Object,
+ * }}
+ */
+function _mapStateToProps(state) {
+ const tracks = state['features/base/tracks'];
+
+ return {
+ _audioMuted: isLocalTrackMuted(tracks, MEDIA_TYPE.AUDIO),
+ _conference: state['features/base/conference'].conference
+ };
+}
+
+export default translate(connect(_mapStateToProps)(AudioMuteButton));
diff --git a/react/features/toolbox/components/buttons/HangupButton.native.js b/react/features/toolbox/components/buttons/HangupButton.native.js
new file mode 100644
index 000000000..d4d23f7b0
--- /dev/null
+++ b/react/features/toolbox/components/buttons/HangupButton.native.js
@@ -0,0 +1,63 @@
+// @flow
+
+import PropTypes from 'prop-types';
+import React from 'react';
+import { connect } from 'react-redux';
+
+import { appNavigate } from '../../../app';
+import { ColorPalette } from '../../../base/styles';
+
+import AbstractHangupButton from './AbstractHangupButton';
+import ToolbarButton from '../ToolbarButton';
+import styles from '../styles';
+
+/**
+ * Component that renders a toolbar button for leaving the current conference.
+ *
+ * @extends Component
+ */
+class HangupButton extends AbstractHangupButton {
+ /**
+ * {@code HangupButton} component's property types.
+ *
+ * @static
+ */
+ static propTypes = {
+ /**
+ * Invoked to leave the conference.
+ */
+ dispatch: PropTypes.func
+ };
+
+ /**
+ * Implements React's {@link Component#render()}.
+ *
+ * @inheritdoc
+ * @returns {ReactElement}
+ */
+ render() {
+ return (
+
+ );
+ }
+
+ /**
+ * Dispatches an action for leaving the current conference.
+ *
+ * @private
+ * @returns {void}
+ */
+ _doHangup() {
+ this.props.dispatch(appNavigate(undefined));
+ }
+
+ _onToolbarHangup: () => void;
+}
+
+export default connect()(HangupButton);
diff --git a/react/features/toolbox/components/buttons/HangupButton.web.js b/react/features/toolbox/components/buttons/HangupButton.web.js
new file mode 100644
index 000000000..fd883488b
--- /dev/null
+++ b/react/features/toolbox/components/buttons/HangupButton.web.js
@@ -0,0 +1,82 @@
+// @flow
+
+import React from 'react';
+import PropTypes from 'prop-types';
+
+import { connect } from 'react-redux';
+
+import { disconnect } from '../../../base/connection';
+import { translate } from '../../../base/i18n';
+
+import AbstractHangupButton from './AbstractHangupButton';
+import ToolbarButtonV2 from '../ToolbarButtonV2';
+
+/**
+ * Component that renders a toolbar button for leaving the current conference.
+ *
+ * @extends Component
+ */
+export class HangupButton extends AbstractHangupButton {
+ /**
+ * Default values for {@code HangupButton} component's properties.
+ *
+ * @static
+ */
+ static defaultProps = {
+ tooltipPosition: 'top'
+ };
+
+ /**
+ * {@code HangupButton} component's property types.
+ *
+ * @static
+ */
+ static propTypes = {
+ /**
+ * Invoked to trigger conference leave.
+ */
+ dispatch: PropTypes.func,
+
+ /**
+ * Invoked to obtain translated strings.
+ */
+ t: PropTypes.func,
+
+ /**
+ * Where the tooltip should display, relative to the button.
+ */
+ tooltipPosition: PropTypes.string
+ }
+
+ /**
+ * Implements React's {@link Component#render()}.
+ *
+ * @inheritdoc
+ * @returns {ReactElement}
+ */
+ render() {
+ const { t, tooltipPosition } = this.props;
+
+ return (
+
+ );
+ }
+
+ _onToolbarHangup: () => void;
+
+ /**
+ * Dispatches an action for leaving the current conference.
+ *
+ * @private
+ * @returns {void}
+ */
+ _doHangup() {
+ this.props.dispatch(disconnect(true));
+ }
+}
+
+export default translate(connect()(HangupButton));
diff --git a/react/features/toolbox/components/buttons/VideoMuteButton.native.js b/react/features/toolbox/components/buttons/VideoMuteButton.native.js
new file mode 100644
index 000000000..313e9ac02
--- /dev/null
+++ b/react/features/toolbox/components/buttons/VideoMuteButton.native.js
@@ -0,0 +1,82 @@
+// @flow
+
+import PropTypes from 'prop-types';
+import React from 'react';
+import { connect } from 'react-redux';
+
+import { MEDIA_TYPE } from '../../../base/media';
+import { isLocalTrackMuted } from '../../../base/tracks';
+
+import AbstractVideoMuteButton from './AbstractVideoMuteButton';
+import ToolbarButton from '../ToolbarButton';
+
+/**
+ * Component that renders a toolbar button for toggling video mute.
+ *
+ * @extends AbstractVideoMuteButton
+ */
+class VideoMuteButton extends AbstractVideoMuteButton {
+ /**
+ * {@code VideoMuteButton} component's property types.
+ *
+ * @static
+ */
+ static propTypes = {
+ ...AbstractVideoMuteButton.propTypes,
+
+ /**
+ * Whether or not the local participant is current in audio only mode.
+ * Video mute toggling is disabled in audio only mode.
+ */
+ _audioOnly: PropTypes.bool,
+
+ /**
+ * Styles to be applied to the button and the icon to show.
+ */
+ buttonStyles: PropTypes.object
+ };
+
+ /**
+ * Implements React's {@link Component#render()}.
+ *
+ * @inheritdoc
+ * @returns {ReactElement}
+ */
+ render() {
+ const { _audioOnly, buttonStyles } = this.props;
+
+ return (
+
+ );
+ }
+
+ _onToolbarToggleVideo: () => void;
+}
+
+/**
+ * Maps (parts of) the Redux state to the associated props for the
+ * {@code VideoMuteButton} component.
+ *
+ * @param {Object} state - The Redux state.
+ * @private
+ * @returns {{
+ * _audioOnly: boolean,
+ * _videoMuted: boolean
+ * }}
+ */
+function _mapStateToProps(state) {
+ const conference = state['features/base/conference'];
+ const tracks = state['features/base/tracks'];
+
+ return {
+ _audioOnly: Boolean(conference.audioOnly),
+ _videoMuted: isLocalTrackMuted(tracks, MEDIA_TYPE.VIDEO)
+ };
+}
+
+export default connect(_mapStateToProps)(VideoMuteButton);
diff --git a/react/features/toolbox/components/buttons/VideoMuteButton.web.js b/react/features/toolbox/components/buttons/VideoMuteButton.web.js
new file mode 100644
index 000000000..f6afe97d1
--- /dev/null
+++ b/react/features/toolbox/components/buttons/VideoMuteButton.web.js
@@ -0,0 +1,161 @@
+// @flow
+
+import PropTypes from 'prop-types';
+import React from 'react';
+import { connect } from 'react-redux';
+
+import {
+ ACTION_SHORTCUT_TRIGGERED,
+ VIDEO_MUTE,
+ createShortcutEvent,
+ sendAnalytics
+} from '../../../analytics';
+import { translate } from '../../../base/i18n';
+import { MEDIA_TYPE } from '../../../base/media';
+import { isLocalTrackMuted } from '../../../base/tracks';
+
+import AbstractVideoMuteButton from './AbstractVideoMuteButton';
+import ToolbarButtonV2 from '../ToolbarButtonV2';
+
+declare var APP: Object;
+
+/**
+ * Component that renders a toolbar button for toggling video mute.
+ *
+ * @extends AbstractVideoMuteButton
+ */
+export class VideoMuteButton extends AbstractVideoMuteButton {
+ /**
+ * Default values for {@code VideoMuteButton} component's properties.
+ *
+ * @static
+ */
+ static defaultProps = {
+ tooltipPosition: 'top'
+ };
+
+ /**
+ * {@code VideoMuteButton} component's property types.
+ *
+ * @static
+ */
+ static propTypes = {
+ ...AbstractVideoMuteButton.propTypes,
+
+ /**
+ * The {@code JitsiConference} for the current conference.
+ */
+ _conference: PropTypes.object,
+
+ /**
+ * Invoked to obtain translated strings.
+ */
+ t: PropTypes.func,
+
+ /**
+ * Where the tooltip should display, relative to the button.
+ */
+ tooltipPosition: PropTypes.string
+ };
+
+ /**
+ * Initializes a new {@code VideoMuteButton} instance.
+ *
+ * @param {Props} props - The read-only React {@code Component} props with
+ * which the new instance is to be initialized.
+ */
+ constructor(props: Object) {
+ super(props);
+
+ // Bind event handlers so they are only bound once per instance.
+ this._onShortcutToggleVideo = this._onShortcutToggleVideo.bind(this);
+ }
+
+ /**
+ * Sets a keyboard shortcuts for toggling video mute.
+ *
+ * @inheritdoc
+ * @returns {void}
+ */
+ componentDidMount() {
+ APP.keyboardshortcut.registerShortcut(
+ 'V',
+ null,
+ this._onShortcutToggleVideo,
+ 'keyboardShortcuts.videoMute');
+ }
+
+ /**
+ * Removes the registered keyboard shortcut handler.
+ *
+ * @inheritdoc
+ * @returns {void}
+ */
+ componentWillUnmount() {
+ APP.keyboardshortcut.unregisterShortcut('V');
+ }
+
+ /**
+ * Implements React's {@link Component#render()}.
+ *
+ * @inheritdoc
+ * @returns {ReactElement}
+ */
+ render() {
+ const { _conference, _videoMuted, t, tooltipPosition } = this.props;
+
+ return (
+
+ );
+ }
+
+ _doToggleVideo: () => void;
+
+ _onShortcutToggleVideo: () => void;
+
+ /**
+ * Creates an analytics keyboard shortcut event for and dispatches an action
+ * for toggling video mute.
+ *
+ * @private
+ * @returns {void}
+ */
+ _onShortcutToggleVideo() {
+ sendAnalytics(createShortcutEvent(
+ VIDEO_MUTE,
+ ACTION_SHORTCUT_TRIGGERED,
+ { enable: !this.props._videoMuted }));
+
+ this._doToggleVideo();
+ }
+
+ _onToolbarToggleVideo: () => void;
+}
+
+/**
+ * Maps (parts of) the Redux state to the associated props for the
+ * {@code AudioMuteButton} component.
+ *
+ * @param {Object} state - The Redux state.
+ * @private
+ * @returns {{
+ * _conference: Object,
+ * _videoMuted: boolean,
+ * }}
+ */
+function _mapStateToProps(state) {
+ const tracks = state['features/base/tracks'];
+
+ return {
+ _conference: state['features/base/conference'].conference,
+ _videoMuted: isLocalTrackMuted(tracks, MEDIA_TYPE.VIDEO)
+ };
+}
+
+export default translate(connect(_mapStateToProps)(VideoMuteButton));
diff --git a/react/features/toolbox/components/buttons/index.js b/react/features/toolbox/components/buttons/index.js
new file mode 100644
index 000000000..fb4d9d327
--- /dev/null
+++ b/react/features/toolbox/components/buttons/index.js
@@ -0,0 +1,3 @@
+export { default as AudioMuteButton } from './AudioMuteButton';
+export { default as HangupButton } from './HangupButton';
+export { default as VideoMuteButton } from './VideoMuteButton';
diff --git a/react/features/toolbox/components/index.js b/react/features/toolbox/components/index.js
index 0862cd09b..25b4f4eb1 100644
--- a/react/features/toolbox/components/index.js
+++ b/react/features/toolbox/components/index.js
@@ -1,4 +1,7 @@
export { default as ToolbarButton } from './ToolbarButton';
+export { default as ToolbarButtonV2 } from './ToolbarButtonV2';
export { default as ToolbarButtonWithDialog }
from './ToolbarButtonWithDialog';
export { default as Toolbox } from './Toolbox';
+export { default as ToolboxFilmstrip } from './ToolboxFilmstrip';
+export { default as ToolboxV2 } from './ToolboxV2';
diff --git a/react/features/toolbox/defaultToolbarButtons.web.js b/react/features/toolbox/defaultToolbarButtons.web.js
index 36513ac79..3f7c2df45 100644
--- a/react/features/toolbox/defaultToolbarButtons.web.js
+++ b/react/features/toolbox/defaultToolbarButtons.web.js
@@ -2,6 +2,7 @@
import React from 'react';
+import { setFullScreen } from '../toolbox';
import {
ACTION_SHORTCUT_TRIGGERED as TRIGGERED,
AUDIO_MUTE,
@@ -10,6 +11,10 @@ import {
createToolbarEvent,
sendAnalytics
} from '../analytics';
+import {
+ getLocalParticipant,
+ participantUpdated
+} from '../base/participants';
import { ParticipantCounter } from '../contact-list';
import { openDeviceSelectionDialog } from '../device-selection';
import { InfoDialogButton } from '../invite';
@@ -252,33 +257,37 @@ export default function getDefaultButtons() {
enabled: true,
id: 'toolbar_button_fullScreen',
onClick() {
- // TODO: why is the fullscreen button handled differently than
- // the fullscreen keyboard shortcut (one results in a direct
- // call to toggleFullScreen, while the other fires an
- // UIEvents.TOGGLE_FULLSCREEN event)?
+ const state = APP.store.getState();
+ const isFullScreen = Boolean(
+ state['features/toolbox'].fullScreen);
// The 'enable' attribute is set to true if the action resulted
// in fullscreen mode being enabled.
sendAnalytics(createToolbarEvent(
'toggle.fullscreen',
{
- enable: !APP.UI.isFullScreen()
+ enable: !isFullScreen
}));
- APP.UI.emitEvent(UIEvents.TOGGLE_FULLSCREEN);
+ APP.store.dispatch(setFullScreen(!isFullScreen));
},
shortcut: 'S',
shortcutAttr: 'toggleFullscreenPopover',
shortcutDescription: 'keyboardShortcuts.fullScreen',
shortcutFunc() {
+ const state = APP.store.getState();
+ const isFullScreen = Boolean(
+ state['features/toolbox'].fullScreen);
+
// The 'enable' attribute is set to true if the action resulted
// in fullscreen mode being enabled.
sendAnalytics(createShortcutEvent(
'toggle.fullscreen',
{
- enable: !APP.UI.isFullScreen()
+ enable: !isFullScreen
}));
- APP.UI.toggleFullScreen();
+
+ APP.store.dispatch(setFullScreen(!isFullScreen));
},
tooltipKey: 'toolbar.fullscreen'
},
@@ -394,27 +403,44 @@ export default function getDefaultButtons() {
id: 'toolbar_button_raisehand',
onClick() {
// TODO: reduce duplication with shortcutFunc below.
+ const localParticipant
+ = getLocalParticipant(APP.store.getState());
+ const currentRaisedHand = localParticipant.raisedHand;
// The 'enable' attribute is set to true if the pressing of the
// shortcut resulted in the hand being raised, and to false
// if it resulted in the hand being 'lowered'.
sendAnalytics(createToolbarEvent(
'raise.hand',
- { enable: !APP.conference.isHandRaised }));
- APP.conference.maybeToggleRaisedHand();
+ { enable: !currentRaisedHand }));
+
+ APP.store.dispatch(participantUpdated({
+ id: localParticipant.id,
+ local: true,
+ raisedHand: !currentRaisedHand
+ }));
},
shortcut: 'R',
shortcutAttr: 'raiseHandPopover',
shortcutDescription: 'keyboardShortcuts.raiseHand',
shortcutFunc() {
+ const localParticipant
+ = getLocalParticipant(APP.store.getState());
+ const currentRaisedHand = localParticipant.raisedHand;
+
// The 'enable' attribute is set to true if the pressing of the
// shortcut resulted in the hand being raised, and to false
// if it resulted in the hand being 'lowered'.
sendAnalytics(createShortcutEvent(
'toggle.raise.hand',
TRIGGERED,
- { enable: !APP.conference.isHandRaised }));
- APP.conference.maybeToggleRaisedHand();
+ { enable: !currentRaisedHand }));
+
+ APP.store.dispatch(participantUpdated({
+ id: localParticipant.id,
+ local: true,
+ raisedHand: !currentRaisedHand
+ }));
},
tooltipKey: 'toolbar.raiseHand'
},
diff --git a/react/features/toolbox/functions.web.js b/react/features/toolbox/functions.web.js
index 072becd61..a1b1da0cb 100644
--- a/react/features/toolbox/functions.web.js
+++ b/react/features/toolbox/functions.web.js
@@ -99,6 +99,17 @@ export function getToolbarClassNames(props: Object) {
};
}
+/**
+ * Helper for getting the height of the toolbox.
+ *
+ * @returns {number} The height of the toolbox.
+ */
+export function getToolboxHeight() {
+ const toolbox = document.getElementById('new-toolbox');
+
+ return (toolbox && toolbox.clientHeight) || 0;
+}
+
/**
* Indicates if a toolbar button is enabled.
*
diff --git a/react/features/toolbox/middleware.js b/react/features/toolbox/middleware.js
index be1a95b85..0b9cf8fdf 100644
--- a/react/features/toolbox/middleware.js
+++ b/react/features/toolbox/middleware.js
@@ -1,4 +1,4 @@
-/* @flow */
+// @flow
import {
MEDIA_TYPE,
@@ -8,8 +8,15 @@ import {
import { MiddlewareRegistry } from '../base/redux';
import { isLocalTrackMuted, TRACK_UPDATED } from '../base/tracks';
-import { setToolbarButton } from './actions';
-import { CLEAR_TOOLBOX_TIMEOUT, SET_TOOLBOX_TIMEOUT } from './actionTypes';
+import { setToolbarButton, toggleFullScreen } from './actions';
+import {
+ CLEAR_TOOLBOX_TIMEOUT,
+ FULL_SCREEN_CHANGED,
+ SET_TOOLBOX_TIMEOUT,
+ SET_FULL_SCREEN
+} from './actionTypes';
+
+declare var APP: Object;
/**
* Middleware which intercepts Toolbox actions to handle changes to the
@@ -27,9 +34,15 @@ MiddlewareRegistry.register(store => next => action => {
break;
}
+ case FULL_SCREEN_CHANGED:
+ return _fullScreenChanged(store, next, action);
+
case SET_AUDIO_AVAILABLE:
return _setMediaAvailableOrMuted(store, next, action);
+ case SET_FULL_SCREEN:
+ return _setFullScreen(next, action);
+
case SET_TOOLBOX_TIMEOUT: {
const { timeoutID } = store.getState()['features/toolbox'];
const { handler, timeoutMS } = action;
@@ -54,6 +67,26 @@ MiddlewareRegistry.register(store => next => action => {
return next(action);
});
+/**
+ * Updates the the redux state with the current known state of full screen.
+ *
+ * @param {Store} store - The redux store in which the specified action is being
+ * dispatched.
+ * @param {Dispatch} next - The redux dispatch function to dispatch the
+ * specified action to the specified store.
+ * @param {Action} action - The redux action FULL_SCREEN_CHANGED which is being
+ * dispatched in the specified store.
+ * @private
+ * @returns {Object} The value returned by {@code next(action)}.
+ */
+function _fullScreenChanged({ dispatch }, next, action) {
+ if (typeof APP === 'object') {
+ dispatch(toggleFullScreen(action.fullScreen));
+ }
+
+ return next(action);
+}
+
/**
* Adjusts the state of toolbar's microphone or camera button.
*
@@ -110,3 +143,46 @@ function _setMediaAvailableOrMuted({ dispatch, getState }, next, action) {
return result;
}
+
+/**
+ * Makes an external request to enter or exit full screen mode.
+ *
+ * @param {Dispatch} next - The redux dispatch function to dispatch the
+ * specified action to the specified store.
+ * @param {Action} action - The redux action SET_FULL_SCREEN which is being
+ * dispatched in the specified store.
+ * @private
+ * @returns {Object} The value returned by {@code next(action)}.
+ */
+function _setFullScreen(next, action) {
+ if (typeof APP === 'object') {
+ const { fullScreen } = action;
+
+ if (fullScreen) {
+ const documentElement = document.documentElement || {};
+
+ if (typeof documentElement.requestFullscreen === 'function') {
+ documentElement.requestFullscreen();
+ } else if (
+ typeof documentElement.msRequestFullscreen === 'function') {
+ documentElement.msRequestFullscreen();
+ } else if (
+ typeof documentElement.mozRequestFullScreen === 'function') {
+ documentElement.mozRequestFullScreen();
+ } else if (
+ typeof documentElement.webkitRequestFullscreen === 'function') {
+ documentElement.webkitRequestFullscreen();
+ }
+ } else if (typeof document.exitFullscreen === 'function') {
+ document.exitFullscreen();
+ } else if (typeof document.msExitFullscreen === 'function') {
+ document.msExitFullscreen();
+ } else if (typeof document.mozCancelFullScreen === 'function') {
+ document.mozCancelFullScreen();
+ } else if (typeof document.webkitExitFullscreen === 'function') {
+ document.webkitExitFullscreen();
+ }
+ }
+
+ return next(action);
+}
diff --git a/react/features/toolbox/reducer.js b/react/features/toolbox/reducer.js
index 51099494f..2327faa7f 100644
--- a/react/features/toolbox/reducer.js
+++ b/react/features/toolbox/reducer.js
@@ -4,6 +4,7 @@ import { ReducerRegistry } from '../base/redux';
import {
CLEAR_TOOLBOX_TIMEOUT,
+ FULL_SCREEN_CHANGED,
SET_DEFAULT_TOOLBOX_BUTTONS,
SET_SUBJECT,
SET_SUBJECT_SLIDE_IN,
@@ -132,6 +133,12 @@ ReducerRegistry.register(
timeoutID: undefined
};
+ case FULL_SCREEN_CHANGED:
+ return {
+ ...state,
+ fullScreen: action.fullScreen
+ };
+
case SET_DEFAULT_TOOLBOX_BUTTONS: {
const { primaryToolbarButtons, secondaryToolbarButtons } = action;
diff --git a/react/features/video-quality/components/VideoQualityButton.web.js b/react/features/video-quality/components/VideoQualityButton.web.js
index f985db09d..772e930d4 100644
--- a/react/features/video-quality/components/VideoQualityButton.web.js
+++ b/react/features/video-quality/components/VideoQualityButton.web.js
@@ -3,7 +3,7 @@ import React, { Component } from 'react';
import { ToolbarButtonWithDialog } from '../../toolbox';
-import { VideoQualityDialog } from './';
+import VideoQualitySlider from './VideoQualitySlider';
/**
* A configuration object to describe how {@code ToolbarButton} should render
@@ -49,7 +49,7 @@ class VideoQualityButton extends Component {
return (
);
}
diff --git a/react/features/video-quality/components/VideoQualityDialog.web.js b/react/features/video-quality/components/VideoQualityDialog.web.js
index 9383b4552..c1b2d4209 100644
--- a/react/features/video-quality/components/VideoQualityDialog.web.js
+++ b/react/features/video-quality/components/VideoQualityDialog.web.js
@@ -1,140 +1,16 @@
-import InlineMessage from '@atlaskit/inline-message';
-import PropTypes from 'prop-types';
import React, { Component } from 'react';
-import { connect } from 'react-redux';
-import {
- createToolbarEvent,
- sendAnalytics
-} from '../../analytics';
-import {
- VIDEO_QUALITY_LEVELS,
- setAudioOnly,
- setReceiveVideoQuality
-} from '../../base/conference';
-import { translate } from '../../base/i18n';
-import JitsiMeetJS from '../../base/lib-jitsi-meet';
+import { Dialog } from '../../base/dialog';
-const logger = require('jitsi-meet-logger').getLogger(__filename);
-
-const {
- HIGH,
- STANDARD,
- LOW
-} = VIDEO_QUALITY_LEVELS;
+import VideoQualitySlider from './VideoQualitySlider';
/**
- * Creates an analytics event for a press of one of the buttons in the video
- * quality dialog.
- *
- * @param {string} quality - The quality which was selected.
- * @returns {Object} The event in a format suitable for sending via
- * sendAnalytics.
- */
-const createEvent = function(quality) {
- return createToolbarEvent(
- 'video.quality',
- {
- quality
- });
-};
-
-/**
- * Implements a React {@link Component} which displays a dialog with a slider
- * for selecting a new receive video quality.
+ * Implements a React {@link Component} which displays the component
+ * {@code VideoQualitySlider} in a dialog.
*
* @extends Component
*/
-class VideoQualityDialog extends Component {
- /**
- * {@code VideoQualityDialog}'s property types.
- *
- * @static
- */
- static propTypes = {
- /**
- * Whether or not the conference is in audio only mode.
- */
- _audioOnly: PropTypes.bool,
-
- /**
- * Whether or not the conference is in peer to peer mode.
- */
- _p2p: PropTypes.bool,
-
- /**
- * The currently configured maximum quality resolution to be received
- * from remote participants.
- */
- _receiveVideoQuality: PropTypes.number,
-
- /**
- * Whether or not displaying video is supported in the current
- * environment. If false, the slider will be disabled.
- */
- _videoSupported: PropTypes.bool,
-
- /**
- * Invoked to request toggling of audio only mode.
- */
- dispatch: PropTypes.func,
-
- /**
- * Invoked to obtain translated strings.
- */
- t: PropTypes.func
- };
-
- /**
- * Initializes a new {@code VideoQualityDialog} instance.
- *
- * @param {Object} props - The read-only React Component props with which
- * the new instance is to be initialized.
- */
- constructor(props) {
- super(props);
-
- // Bind event handlers so they are only bound once for every instance.
- this._enableAudioOnly = this._enableAudioOnly.bind(this);
- this._enableHighDefinition = this._enableHighDefinition.bind(this);
- this._enableLowDefinition = this._enableLowDefinition.bind(this);
- this._enableStandardDefinition
- = this._enableStandardDefinition.bind(this);
- this._onSliderChange = this._onSliderChange.bind(this);
-
- /**
- * An array of configuration options for displaying a choice in the
- * input. The onSelect callback will be invoked when the option is
- * selected and videoQuality helps determine which choice matches with
- * the currently active quality level.
- *
- * @private
- * @type {Object[]}
- */
- this._sliderOptions = [
- {
- audioOnly: true,
- onSelect: this._enableAudioOnly,
- textKey: 'audioOnly.audioOnly'
- },
- {
- onSelect: this._enableLowDefinition,
- textKey: 'videoStatus.lowDefinition',
- videoQuality: LOW
- },
- {
- onSelect: this._enableStandardDefinition,
- textKey: 'videoStatus.standardDefinition',
- videoQuality: STANDARD
- },
- {
- onSelect: this._enableHighDefinition,
- textKey: 'videoStatus.highDefinition',
- videoQuality: HIGH
- }
- ];
- }
-
+export default class VideoQualityDialog extends Component {
/**
* Implements React's {@link Component#render()}.
*
@@ -142,252 +18,14 @@ class VideoQualityDialog extends Component {
* @returns {ReactElement}
*/
render() {
- const { _audioOnly, _p2p, _videoSupported, t } = this.props;
- const activeSliderOption = this._mapCurrentQualityToSliderValue();
-
- let classNames = 'video-quality-dialog';
- let warning = null;
-
- if (!_videoSupported) {
- classNames += ' video-not-supported';
- warning = this._renderAudioOnlyLockedMessage();
- } else if (_p2p && !_audioOnly) {
- warning = this._renderP2PMessage();
- }
-
return (
-
-
- { t('videoStatus.callQuality') }
-
-
- { warning }
-
-
-
- { /* FIXME: onChange and onMouseUp are both used for
- * compatibility with IE11. This workaround can be
- * removed after upgrading to React 16.
- */ }
-
-
-
-
- { this._createLabels(activeSliderOption) }
-
-
-
+
);
}
-
- /**
- * Creates a React Element for notifying that the browser is in audio only
- * and cannot be changed.
- *
- * @private
- * @returns {ReactElement}
- */
- _renderAudioOnlyLockedMessage() {
- const { t } = this.props;
-
- return (
-
- { t('videoStatus.onlyAudioSupported') }
-
- );
- }
-
- /**
- * Creates React Elements for notifying that peer to peer is enabled.
- *
- * @private
- * @returns {ReactElement}
- */
- _renderP2PMessage() {
- const { t } = this.props;
-
- return (
-
- { t('videoStatus.p2pVideoQualityDescription') }
-
- );
- }
-
- /**
- * Creates React Elements to display mock tick marks with associated labels.
- *
- * @param {number} activeLabelIndex - Which of the sliderOptions should
- * display as currently active.
- * @private
- * @returns {ReactElement[]}
- */
- _createLabels(activeLabelIndex) {
- const labelsCount = this._sliderOptions.length;
- const maxWidthOfLabel = `${100 / labelsCount}%`;
-
- return this._sliderOptions.map((sliderOption, index) => {
- const style = {
- maxWidth: maxWidthOfLabel,
- left: `${(index * 100) / (labelsCount - 1)}%`
- };
-
- const isActiveClass = activeLabelIndex === index ? 'active' : '';
- const className
- = `video-quality-dialog-label-container ${isActiveClass}`;
-
- return (
-
-
- { this.props.t(sliderOption.textKey) }
-
-
- );
- });
- }
-
- /**
- * Dispatches an action to enable audio only mode.
- *
- * @private
- * @returns {void}
- */
- _enableAudioOnly() {
- sendAnalytics(createEvent('audio.only'));
- logger.log('Video quality: audio only enabled');
- this.props.dispatch(setAudioOnly(true));
- }
-
- /**
- * Handles the action of the high definition video being selected.
- * Dispatches an action to receive high quality video from remote
- * participants.
- *
- * @private
- * @returns {void}
- */
- _enableHighDefinition() {
- sendAnalytics(createEvent('high'));
- logger.log('Video quality: high enabled');
- this.props.dispatch(setReceiveVideoQuality(HIGH));
- }
-
- /**
- * Dispatches an action to receive low quality video from remote
- * participants.
- *
- * @private
- * @returns {void}
- */
- _enableLowDefinition() {
- sendAnalytics(createEvent('low'));
- logger.log('Video quality: low enabled');
- this.props.dispatch(setReceiveVideoQuality(LOW));
- }
-
- /**
- * Dispatches an action to receive standard quality video from remote
- * participants.
- *
- * @private
- * @returns {void}
- */
- _enableStandardDefinition() {
- sendAnalytics(createEvent('standard'));
- logger.log('Video quality: standard enabled');
- this.props.dispatch(setReceiveVideoQuality(STANDARD));
- }
-
- /**
- * Matches the current video quality state with corresponding index of the
- * component's slider options.
- *
- * @private
- * @returns {void}
- */
- _mapCurrentQualityToSliderValue() {
- const { _audioOnly, _receiveVideoQuality } = this.props;
- const { _sliderOptions } = this;
-
- if (_audioOnly) {
- const audioOnlyOption = _sliderOptions.find(
- ({ audioOnly }) => audioOnly);
-
- return _sliderOptions.indexOf(audioOnlyOption);
- }
-
- const matchingOption = _sliderOptions.find(
- ({ videoQuality }) => videoQuality === _receiveVideoQuality);
-
- return _sliderOptions.indexOf(matchingOption);
- }
-
- /**
- * Invokes a callback when the selected video quality changes.
- *
- * @param {Object} event - The slider's change event.
- * @private
- * @returns {void}
- */
- _onSliderChange(event) {
- const { _audioOnly, _receiveVideoQuality } = this.props;
- const {
- audioOnly,
- onSelect,
- videoQuality
- } = this._sliderOptions[event.target.value];
-
- // Take no action if the newly chosen option does not change audio only
- // or video quality state.
- if ((_audioOnly && audioOnly)
- || (!_audioOnly && videoQuality === _receiveVideoQuality)) {
- return;
- }
-
- onSelect();
- }
}
-
-/**
- * Maps (parts of) the Redux state to the associated props for the
- * {@code VideoQualityDialog} component.
- *
- * @param {Object} state - The Redux state.
- * @private
- * @returns {{
- * _audioOnly: boolean,
- * _p2p: boolean,
- * _receiveVideoQuality: boolean
- * }}
- */
-function _mapStateToProps(state) {
- const {
- audioOnly,
- p2p,
- receiveVideoQuality
- } = state['features/base/conference'];
-
- return {
- _audioOnly: audioOnly,
- _p2p: p2p,
- _receiveVideoQuality: receiveVideoQuality,
- _videoSupported: JitsiMeetJS.mediaDevices.supportsVideo()
- };
-}
-
-export default translate(connect(_mapStateToProps)(VideoQualityDialog));
diff --git a/react/features/video-quality/components/VideoQualitySlider.native.js b/react/features/video-quality/components/VideoQualitySlider.native.js
new file mode 100644
index 000000000..e69de29bb
diff --git a/react/features/video-quality/components/VideoQualitySlider.web.js b/react/features/video-quality/components/VideoQualitySlider.web.js
new file mode 100644
index 000000000..9a39586c0
--- /dev/null
+++ b/react/features/video-quality/components/VideoQualitySlider.web.js
@@ -0,0 +1,393 @@
+import InlineMessage from '@atlaskit/inline-message';
+import PropTypes from 'prop-types';
+import React, { Component } from 'react';
+import { connect } from 'react-redux';
+
+import {
+ createToolbarEvent,
+ sendAnalytics
+} from '../../analytics';
+import {
+ VIDEO_QUALITY_LEVELS,
+ setAudioOnly,
+ setReceiveVideoQuality
+} from '../../base/conference';
+import { translate } from '../../base/i18n';
+import JitsiMeetJS from '../../base/lib-jitsi-meet';
+
+const logger = require('jitsi-meet-logger').getLogger(__filename);
+
+const {
+ HIGH,
+ STANDARD,
+ LOW
+} = VIDEO_QUALITY_LEVELS;
+
+/**
+ * Creates an analytics event for a press of one of the buttons in the video
+ * quality dialog.
+ *
+ * @param {string} quality - The quality which was selected.
+ * @returns {Object} The event in a format suitable for sending via
+ * sendAnalytics.
+ */
+const createEvent = function(quality) {
+ return createToolbarEvent(
+ 'video.quality',
+ {
+ quality
+ });
+};
+
+/**
+ * Implements a React {@link Component} which displays a slider for selecting a
+ * new receive video quality.
+ *
+ * @extends Component
+ */
+class VideoQualitySlider extends Component {
+ /**
+ * {@code VideoQualitySlider}'s property types.
+ *
+ * @static
+ */
+ static propTypes = {
+ /**
+ * Whether or not the conference is in audio only mode.
+ */
+ _audioOnly: PropTypes.bool,
+
+ /**
+ * Whether or not the conference is in peer to peer mode.
+ */
+ _p2p: PropTypes.bool,
+
+ /**
+ * The currently configured maximum quality resolution to be received
+ * from remote participants.
+ */
+ _receiveVideoQuality: PropTypes.number,
+
+ /**
+ * Whether or not displaying video is supported in the current
+ * environment. If false, the slider will be disabled.
+ */
+ _videoSupported: PropTypes.bool,
+
+ /**
+ * Invoked to request toggling of audio only mode.
+ */
+ dispatch: PropTypes.func,
+
+ /**
+ * Invoked to obtain translated strings.
+ */
+ t: PropTypes.func
+ };
+
+ /**
+ * Initializes a new {@code VideoQualitySlider} instance.
+ *
+ * @param {Object} props - The read-only React Component props with which
+ * the new instance is to be initialized.
+ */
+ constructor(props) {
+ super(props);
+
+ // Bind event handlers so they are only bound once for every instance.
+ this._enableAudioOnly = this._enableAudioOnly.bind(this);
+ this._enableHighDefinition = this._enableHighDefinition.bind(this);
+ this._enableLowDefinition = this._enableLowDefinition.bind(this);
+ this._enableStandardDefinition
+ = this._enableStandardDefinition.bind(this);
+ this._onSliderChange = this._onSliderChange.bind(this);
+
+ /**
+ * An array of configuration options for displaying a choice in the
+ * input. The onSelect callback will be invoked when the option is
+ * selected and videoQuality helps determine which choice matches with
+ * the currently active quality level.
+ *
+ * @private
+ * @type {Object[]}
+ */
+ this._sliderOptions = [
+ {
+ audioOnly: true,
+ onSelect: this._enableAudioOnly,
+ textKey: 'audioOnly.audioOnly'
+ },
+ {
+ onSelect: this._enableLowDefinition,
+ textKey: 'videoStatus.lowDefinition',
+ videoQuality: LOW
+ },
+ {
+ onSelect: this._enableStandardDefinition,
+ textKey: 'videoStatus.standardDefinition',
+ videoQuality: STANDARD
+ },
+ {
+ onSelect: this._enableHighDefinition,
+ textKey: 'videoStatus.highDefinition',
+ videoQuality: HIGH
+ }
+ ];
+ }
+
+ /**
+ * Implements React's {@link Component#render()}.
+ *
+ * @inheritdoc
+ * @returns {ReactElement}
+ */
+ render() {
+ const { _audioOnly, _p2p, _videoSupported, t } = this.props;
+ const activeSliderOption = this._mapCurrentQualityToSliderValue();
+
+ let classNames = 'video-quality-dialog';
+ let warning = null;
+
+ if (!_videoSupported) {
+ classNames += ' video-not-supported';
+ warning = this._renderAudioOnlyLockedMessage();
+ } else if (_p2p && !_audioOnly) {
+ warning = this._renderP2PMessage();
+ }
+
+ return (
+
+
+ { t('videoStatus.callQuality') }
+
+
+ { warning }
+
+
+
+ { /* FIXME: onChange and onMouseUp are both used for
+ * compatibility with IE11. This workaround can be
+ * removed after upgrading to React 16.
+ */ }
+
+
+
+
+ { this._createLabels(activeSliderOption) }
+
+
+
+ );
+ }
+
+ /**
+ * Creates a React Element for notifying that the browser is in audio only
+ * and cannot be changed.
+ *
+ * @private
+ * @returns {ReactElement}
+ */
+ _renderAudioOnlyLockedMessage() {
+ const { t } = this.props;
+
+ return (
+
+ { t('videoStatus.onlyAudioSupported') }
+
+ );
+ }
+
+ /**
+ * Creates React Elements for notifying that peer to peer is enabled.
+ *
+ * @private
+ * @returns {ReactElement}
+ */
+ _renderP2PMessage() {
+ const { t } = this.props;
+
+ return (
+
+ { t('videoStatus.p2pVideoQualityDescription') }
+
+ );
+ }
+
+ /**
+ * Creates React Elements to display mock tick marks with associated labels.
+ *
+ * @param {number} activeLabelIndex - Which of the sliderOptions should
+ * display as currently active.
+ * @private
+ * @returns {ReactElement[]}
+ */
+ _createLabels(activeLabelIndex) {
+ const labelsCount = this._sliderOptions.length;
+ const maxWidthOfLabel = `${100 / labelsCount}%`;
+
+ return this._sliderOptions.map((sliderOption, index) => {
+ const style = {
+ maxWidth: maxWidthOfLabel,
+ left: `${(index * 100) / (labelsCount - 1)}%`
+ };
+
+ const isActiveClass = activeLabelIndex === index ? 'active' : '';
+ const className
+ = `video-quality-dialog-label-container ${isActiveClass}`;
+
+ return (
+
+
+ { this.props.t(sliderOption.textKey) }
+
+
+ );
+ });
+ }
+
+ /**
+ * Dispatches an action to enable audio only mode.
+ *
+ * @private
+ * @returns {void}
+ */
+ _enableAudioOnly() {
+ sendAnalytics(createEvent('audio.only'));
+ logger.log('Video quality: audio only enabled');
+ this.props.dispatch(setAudioOnly(true));
+ }
+
+ /**
+ * Handles the action of the high definition video being selected.
+ * Dispatches an action to receive high quality video from remote
+ * participants.
+ *
+ * @private
+ * @returns {void}
+ */
+ _enableHighDefinition() {
+ sendAnalytics(createEvent('high'));
+ logger.log('Video quality: high enabled');
+ this.props.dispatch(setReceiveVideoQuality(HIGH));
+ }
+
+ /**
+ * Dispatches an action to receive low quality video from remote
+ * participants.
+ *
+ * @private
+ * @returns {void}
+ */
+ _enableLowDefinition() {
+ sendAnalytics(createEvent('low'));
+ logger.log('Video quality: low enabled');
+ this.props.dispatch(setReceiveVideoQuality(LOW));
+ }
+
+ /**
+ * Dispatches an action to receive standard quality video from remote
+ * participants.
+ *
+ * @private
+ * @returns {void}
+ */
+ _enableStandardDefinition() {
+ sendAnalytics(createEvent('standard'));
+ logger.log('Video quality: standard enabled');
+ this.props.dispatch(setReceiveVideoQuality(STANDARD));
+ }
+
+ /**
+ * Matches the current video quality state with corresponding index of the
+ * component's slider options.
+ *
+ * @private
+ * @returns {void}
+ */
+ _mapCurrentQualityToSliderValue() {
+ const { _audioOnly, _receiveVideoQuality } = this.props;
+ const { _sliderOptions } = this;
+
+ if (_audioOnly) {
+ const audioOnlyOption = _sliderOptions.find(
+ ({ audioOnly }) => audioOnly);
+
+ return _sliderOptions.indexOf(audioOnlyOption);
+ }
+
+ const matchingOption = _sliderOptions.find(
+ ({ videoQuality }) => videoQuality === _receiveVideoQuality);
+
+ return _sliderOptions.indexOf(matchingOption);
+ }
+
+ /**
+ * Invokes a callback when the selected video quality changes.
+ *
+ * @param {Object} event - The slider's change event.
+ * @private
+ * @returns {void}
+ */
+ _onSliderChange(event) {
+ const { _audioOnly, _receiveVideoQuality } = this.props;
+ const {
+ audioOnly,
+ onSelect,
+ videoQuality
+ } = this._sliderOptions[event.target.value];
+
+ // Take no action if the newly chosen option does not change audio only
+ // or video quality state.
+ if ((_audioOnly && audioOnly)
+ || (!_audioOnly && videoQuality === _receiveVideoQuality)) {
+ return;
+ }
+
+ onSelect();
+ }
+}
+
+/**
+ * Maps (parts of) the Redux state to the associated props for the
+ * {@code VideoQualitySlider} component.
+ *
+ * @param {Object} state - The Redux state.
+ * @private
+ * @returns {{
+ * _audioOnly: boolean,
+ * _p2p: boolean,
+ * _receiveVideoQuality: boolean
+ * }}
+ */
+function _mapStateToProps(state) {
+ const {
+ audioOnly,
+ p2p,
+ receiveVideoQuality
+ } = state['features/base/conference'];
+
+ return {
+ _audioOnly: audioOnly,
+ _p2p: p2p,
+ _receiveVideoQuality: receiveVideoQuality,
+ _videoSupported: JitsiMeetJS.mediaDevices.supportsVideo()
+ };
+}
+
+export default translate(connect(_mapStateToProps)(VideoQualitySlider));
diff --git a/service/UI/UIEvents.js b/service/UI/UIEvents.js
index dcaed5720..fb99426d9 100644
--- a/service/UI/UIEvents.js
+++ b/service/UI/UIEvents.js
@@ -66,7 +66,7 @@ export default {
* @see {TOGGLE_FILMSTRIP}
*/
TOGGLED_FILMSTRIP: 'UI.toggled_filmstrip',
-
+ TOGGLE_RECORDING: 'UI.toggle_recording',
TOGGLE_SCREENSHARING: 'UI.toggle_screensharing',
TOGGLED_SHARED_DOCUMENT: 'UI.toggled_shared_document',
CONTACT_CLICKED: 'UI.contact_clicked',