From 62a8ceecce7b85a07e6bb513738c8d4f7d649c0a Mon Sep 17 00:00:00 2001 From: yanas Date: Tue, 19 Sep 2017 13:41:04 -0500 Subject: [PATCH] feat(recording):Recording popup menu --- config.js | 2 +- css/_recording.scss | 38 +++ css/_videolayout_default.scss | 4 +- interface_config.js | 2 +- lang/main.json | 5 +- modules/UI/UI.js | 1 - modules/UI/recording/Recording.js | 63 +--- .../components/RecordingButton.web.js | 307 ++++++++++++++++++ react/features/recording/components/index.js | 1 + .../toolbox/defaultToolbarButtons.web.js | 9 +- 10 files changed, 370 insertions(+), 62 deletions(-) create mode 100644 react/features/recording/components/RecordingButton.web.js diff --git a/config.js b/config.js index 108ec737d..a178ff97d 100644 --- a/config.js +++ b/config.js @@ -152,7 +152,7 @@ var config = { // eslint-disable-line no-unused-vars // Whether to enable recording or not. //enableRecording: false, - // Type for recording: one of jibri or jirecon. + // Type for recording: one of jibri, jibri_file or jirecon. //recordingType: 'jibri', // Misc diff --git a/css/_recording.scss b/css/_recording.scss index b3577f38e..e1fd917df 100644 --- a/css/_recording.scss +++ b/css/_recording.scss @@ -1,3 +1,41 @@ +/** + * The recording spinner style. + */ .recordingSpinner { vertical-align: top; } + +/** + * The recording button popup style. + */ +.recording-popup-dialog { + width: 140px; + + /** + * The popup item style. + */ + .recording-popup-item { + font-size: 12px; + position: relative; + top: -5px; + } + + /** + * The popup title style. + */ + .recording-popup-title { + margin-bottom: 10px; + } +} + +/** + * The transcription label style. + */ +#transcriptionLabel { + border-radius: 3px; + + > span { + line-height: 40px; + padding-left: 12px; + } +} diff --git a/css/_videolayout_default.scss b/css/_videolayout_default.scss index 21a1fedbc..c6c2681f8 100644 --- a/css/_videolayout_default.scss +++ b/css/_videolayout_default.scss @@ -95,8 +95,8 @@ } .connection-indicator, - div.indicator-container, - { + div.indicator-container + { margin-right: 4px; } diff --git a/interface_config.js b/interface_config.js index 965875e36..95d085ea3 100644 --- a/interface_config.js +++ b/interface_config.js @@ -35,7 +35,7 @@ var interfaceConfig = { // eslint-disable-line no-unused-vars //main toolbar 'microphone', 'camera', 'desktop', 'invite', 'fullscreen', 'fodeviceselection', 'hangup', // jshint ignore:line //extended toolbar - 'profile', 'addtocall', 'contacts', 'chat', 'recording', 'etherpad', 'sharedvideo', 'dialout', 'settings', 'raisehand', 'videoquality', 'filmstrip'], // jshint ignore:line + 'profile', 'addtocall', 'contacts', 'chat', 'recording', 'transcription', 'etherpad', 'sharedvideo', 'dialout', 'settings', 'raisehand', 'videoquality', 'filmstrip'], // jshint ignore:line /** * Main Toolbar Buttons * All of them should be in TOOLBAR_BUTTONS diff --git a/lang/main.json b/lang/main.json index bdb6b4344..e19c865a3 100644 --- a/lang/main.json +++ b/lang/main.json @@ -401,7 +401,10 @@ "failedToStart": "Recording failed to start", "buttonTooltip": "Start / Stop recording", "error": "Recording failed. Please try again.", - "unavailable": "The recording service is currently unavailable. Please try again later." + "unavailable": "The recording service is currently unavailable. Please try again later.", + "recordingPopupTitle": "Recording options", + "videoRecordingLabel": "Video", + "transcriptionLabel": "Transcription" }, "liveStreaming": { diff --git a/modules/UI/UI.js b/modules/UI/UI.js index 9423bccef..9de6cf9e7 100644 --- a/modules/UI/UI.js +++ b/modules/UI/UI.js @@ -477,7 +477,6 @@ UI.updateLocalRole = isModerator => { APP.store.dispatch(showDialOutButton(isModerator)); APP.store.dispatch(showSharedVideoButton()); - Recording.showRecordingButton(isModerator); SettingsMenu.showStartMutedOptions(isModerator); SettingsMenu.showFollowMeOptions(isModerator); diff --git a/modules/UI/recording/Recording.js b/modules/UI/recording/Recording.js index 6c6b1ecc2..5e90e5624 100644 --- a/modules/UI/recording/Recording.js +++ b/modules/UI/recording/Recording.js @@ -1,4 +1,4 @@ -/* global APP, $, config, interfaceConfig, JitsiMeetJS */ +/* global APP, config, interfaceConfig, JitsiMeetJS */ /* * Copyright @ 2015 Atlassian Pty Ltd * @@ -249,8 +249,7 @@ function isStartingStatus(status) { /** * Manages the recording user interface and user experience. - * @type {{init, initRecordingButton, showRecordingButton, updateRecordingState, - * updateRecordingUI, checkAutoRecord}} + * @type {{init, updateRecordingState, updateRecordingUI, checkAutoRecord}} */ var Recording = { /** @@ -271,13 +270,6 @@ var Recording = { Object.assign(this, RECORDING_TRANSLATION_KEYS); } - // XXX Due to the React-ification of Toolbox, the HTMLElement with id - // toolbar_button_record may not exist yet. - $(document).on( - 'click', - '#toolbar_button_record', - ev => this._onToolbarButtonClick(ev)); - // If I am a recorder then I publish my recorder custom role to notify // everyone. if (config.iAmRecorder) { @@ -290,28 +282,6 @@ var Recording = { } }, - /** - * Initialise the recording button. - */ - initRecordingButton() { - const selector = $('#toolbar_button_record'); - - selector.addClass(this.baseClass); - selector.attr("data-i18n", "[content]" + this.recordingButtonTooltip); - APP.translation.translateElement(selector); - }, - - /** - * Shows or hides the 'recording' button. - * @param show {true} to show the recording button, {false} to hide it - */ - showRecordingButton(show) { - let shouldShow = show && _isRecordingButtonEnabled(); - let id = 'toolbar_button_record'; - - UIUtil.setVisible(id, shouldShow); - }, - /** * Updates the recording state UI. * @param recordingState gives us the current recording state @@ -338,6 +308,7 @@ var Recording = { this.currentState = recordingState; let labelDisplayConfiguration; + let recordingButtonToggled; switch (recordingState) { case Status.ON: @@ -348,7 +319,7 @@ var Recording = { showSpinner: recordingState === Status.RETRYING }; - this._setToolbarButtonToggled(true); + recordingButtonToggled = true; break; } @@ -372,7 +343,7 @@ var Recording = { : this.recordingOffKey }; - this._setToolbarButtonToggled(false); + recordingButtonToggled = false; setTimeout(function(){ APP.store.dispatch(hideRecordingLabel()); @@ -387,8 +358,6 @@ var Recording = { key: this.recordingPendingKey }; - this._setToolbarButtonToggled(false); - break; } @@ -398,7 +367,7 @@ var Recording = { key: this.recordingErrorKey }; - this._setToolbarButtonToggled(false); + recordingButtonToggled = false; break; } @@ -412,6 +381,7 @@ var Recording = { APP.store.dispatch(updateRecordingState({ labelDisplayConfiguration, + recordingButtonToggled, recordingState })); }, @@ -431,7 +401,7 @@ var Recording = { * * @returns {void} */ - _onToolbarButtonClick() { + toggleRecording() { if (dialog) { return; } @@ -451,7 +421,12 @@ var Recording = { } case Status.AVAILABLE: case Status.OFF: { - if (this.recordingType === 'jibri') + if (this.recordingType === 'jibri_file') { + this.eventEmitter.emit( + UIEvents.RECORDING_TOGGLED); + JitsiMeetJS.analytics.sendEvent('recording.started'); + } + else if (this.recordingType === 'jibri') _requestLiveStreamId().then(streamId => { this.eventEmitter.emit( UIEvents.RECORDING_TOGGLED, @@ -508,16 +483,6 @@ var Recording = { ); } } - }, - - /** - * Sets the toggled state of the recording toolbar button. - * - * @param {boolean} isToggled indicates if the button should be toggled - * or not - */ - _setToolbarButtonToggled(isToggled) { - $("#toolbar_button_record").toggleClass("toggled", isToggled); } }; diff --git a/react/features/recording/components/RecordingButton.web.js b/react/features/recording/components/RecordingButton.web.js new file mode 100644 index 000000000..9dce6e98c --- /dev/null +++ b/react/features/recording/components/RecordingButton.web.js @@ -0,0 +1,307 @@ +import AKInlineDialog from '@atlaskit/inline-dialog'; +import React, { Component } from 'react'; +import { connect } from 'react-redux'; +import { ToggleStateless } from '@atlaskit/toggle'; + +import { translate } from '../../base/i18n'; +import { isButtonEnabled, ToolbarButton } from '../../toolbox'; +import Recording from '../../../../modules/UI/recording/Recording'; + +const DEFAULT_BUTTON_CONFIGURATION = { + buttonName: 'recording', + classNames: [ 'button', 'icon-recEnable' ], + enabled: true, + id: 'toolbar_button_record', + tooltipKey: 'recording.buttonTooltip' +}; + +/** + * TOFIX: Copy paste from VideoQualityButton, we need a base class that supports + * inline dialogs and does that position thing. + * @type {{bottom: string, left: string, right: string, top: string}} + */ +const TOOLTIP_TO_DIALOG_POSITION = { + bottom: 'bottom center', + left: 'left middle', + right: 'right middle', + top: 'top center' +}; + +/** + * React {@code Component} for displaying an inline dialog for changing receive + * video settings. + * + * @extends Component + */ +class RecordingButton extends Component { + /** + * {@code RecordingButton}'s property types. + * + * @static + */ + static propTypes = { + /** + * The redux store representation of the JitsiConference. + */ + _conference: React.PropTypes.object, + + /** + * Indicates if the recording button has been toggled. + */ + _recordingButtonToggled: React.PropTypes.bool, + + /** + * Indicates if the audio recording option should be enabled. + */ + _recordingEnabled: React.PropTypes.bool, + + /** + * Indicates if the transcription option should be enabled. + */ + _transcriptionEnabled: React.PropTypes.bool, + + /** + * Whether or not the button is visible, based on the visibility of the + * toolbar. Used to automatically hide the inline dialog if not visible. + */ + _visible: React.PropTypes.bool, + + /** + * Invoked to obtain translated string. + */ + t: React.PropTypes.func, + + /** + * From which side tooltips should display. Will be re-used for + * displaying the inline dialog for video quality adjustment. + */ + tooltipPosition: React.PropTypes.string + }; + + /** + * Initializes a new {@code RecordingButton} instance. + * + * @param {Object} props - The read-only properties with which the new + * instance is to be initialized. + */ + constructor(props) { + super(props); + + this.state = { + /** + * Whether or not the inline dialog for adjusting received video + * quality is displayed. + */ + showRecordingDialog: false, + + isToggleRecordingChecked: false, + + isToggleTranscriptionChecked: false + }; + + // Bind event handlers so they are only bound once for every instance. + this._onDialogClose = this._onDialogClose.bind(this); + this._onDialogToggle = this._onDialogToggle.bind(this); + this._onToggleRecordingChange + = this._onToggleRecordingChange.bind(this); + this._onToggleTranscriptionChange + = this._onToggleTranscriptionChange.bind(this); + } + + /** + * Updates the toggled state depending on the _recordingButtonToggled + * prop. + * + * @param {Object} nextProps - The props that will be applied after the + * update. + * @inheritdoc + * @returns {void} + */ + componentWillUpdate(nextProps) { + if (nextProps._recordingButtonToggled + !== this.props._recordingButtonToggled + && nextProps._recordingButtonToggled + !== this.state.isToggleRecordingChecked) { + this.setState({ + isToggleRecordingChecked: nextProps._recordingButtonToggled + }); + } + } + + /** + * Implements React's {@link Component#render()}. + * + * @inheritdoc + * @returns {ReactElement} + */ + render() { + const { _visible, tooltipPosition } = this.props; + const buttonConfiguration = { + ...DEFAULT_BUTTON_CONFIGURATION, + classNames: [ + ...DEFAULT_BUTTON_CONFIGURATION.classNames, + this.state.showRecordingDialog ? 'toggled button-active' : '' + ] + }; + + const content = this._renderRecordingMenu(); + + return ( + + + + ); + } + + /** + * Hides the attached inline dialog. + * + * @private + * @returns {void} + */ + _onDialogClose() { + this.setState({ showRecordingDialog: false }); + } + + /** + * Toggles the display of the dialog. + * + * @private + * @returns {void} + */ + _onDialogToggle() { + this.setState({ + showRecordingDialog: !this.state.showRecordingDialog + }); + } + + /** + * Updates the current known state of the toggle selection. + * + * @param {Object} event - The DOM event from changing the toggle selection. + * @private + * @returns {void} + */ + _onToggleRecordingChange(event) { + this.setState({ + isToggleRecordingChecked: event.target.checked + }); + Recording.toggleRecording(); + } + + /** + * Updates the current known state of the toggle selection. + * + * @param {Object} event - The DOM event from changing the toggle selection. + * @private + * @returns {void} + */ + _onToggleTranscriptionChange(event) { + const checked = event.target.checked; + + this.setState({ + isToggleTranscriptionChecked: checked + }); + + checked + ? this.props._conference.startTranscriber() + : this.props._conference.stopTranscriber(); + } + + /** + * Creates a new {@code RemoteVideoMenu} with buttons for interacting with + * the remote participant. + * + * @private + * @returns {ReactElement} + */ + _renderRecordingMenu() { + const { + _recordingEnabled, + _transcriptionEnabled + } = this.props; + + const buttons = []; + + if (_recordingEnabled) { + buttons.push( +
+ + + { this.props.t('recording.videoRecordingLabel') } + +
+ ); + } + + if (_transcriptionEnabled) { + buttons.push( +
+ + + { this.props.t('recording.transcriptionLabel') } + +
+ ); + } + + if (buttons.length > 0) { + return ( +
+

+ { this.props.t('recording.recordingPopupTitle') } +

+ { buttons } +
+ ); + } + + return null; + } +} + +/** + * Maps (parts of) the Redux state to the associated {@code VideoQualityButton} + * component's props. + * + * @param {Object} state - The Redux state. + * @private + * @returns {{ + * _visible: boolean + * }} + */ +function _mapStateToProps(state) { + const { conference } = state['features/base/conference']; + const { enableRecording, enableUserRolesBasedOnToken } + = state['features/base/config']; + const { isGuest } = state['features/jwt']; + const { recordingButtonToggled } = state['features/recording']; + + return { + _conference: conference, + _visible: state['features/toolbox'].visible, + _recordingButtonToggled: recordingButtonToggled, + _recordingEnabled: isButtonEnabled('recording') + && enableRecording + && conference && conference.isRecordingSupported(), + _transcriptionEnabled: isButtonEnabled('transcription') + && (!enableUserRolesBasedOnToken || !isGuest) + }; +} + +export default translate(connect(_mapStateToProps)(RecordingButton)); diff --git a/react/features/recording/components/index.js b/react/features/recording/components/index.js index 90ce231bd..7f1cd05b0 100644 --- a/react/features/recording/components/index.js +++ b/react/features/recording/components/index.js @@ -1 +1,2 @@ export { default as RecordingLabel } from './RecordingLabel'; +export { default as RecordingButton } from './RecordingButton'; diff --git a/react/features/toolbox/defaultToolbarButtons.web.js b/react/features/toolbox/defaultToolbarButtons.web.js index d611a331f..a93ab3054 100644 --- a/react/features/toolbox/defaultToolbarButtons.web.js +++ b/react/features/toolbox/defaultToolbarButtons.web.js @@ -6,6 +6,7 @@ import { DEFAULT_AVATAR_RELATIVE_PATH } from '../base/participants'; import { openDeviceSelectionDialog } from '../device-selection'; import { openDialOutDialog } from '../dial-out'; import { openAddPeopleDialog, openInviteDialog } from '../invite'; +import { RecordingButton } from '../recording'; import { VideoQualityButton } from '../video-quality'; import UIEvents from '../../../service/UI/UIEvents'; @@ -368,13 +369,7 @@ const buttons: Object = { * initialization in the recording module. */ recording: { - classNames: [ 'button' ], - enabled: true, - - // will be displayed once the recording functionality is detected - hidden: true, - id: 'toolbar_button_record', - tooltipKey: 'liveStreaming.buttonTooltip' + component: RecordingButton }, /**