diff --git a/css/_recording.scss b/css/_recording.scss index aa56227a7..b3577f38e 100644 --- a/css/_recording.scss +++ b/css/_recording.scss @@ -1,4 +1,3 @@ .recordingSpinner { - display: none; vertical-align: top; -} \ No newline at end of file +} diff --git a/css/modals/video-quality/_video-quality.scss b/css/modals/video-quality/_video-quality.scss index ef7ec87d2..c442560cf 100644 --- a/css/modals/video-quality/_video-quality.scss +++ b/css/modals/video-quality/_video-quality.scss @@ -154,12 +154,15 @@ bottom: 45%; border-radius: 2px; display: none; - -webkit-transition: all 2s 2s linear; - transition: all 2s 2s linear; + padding: 10px; + transform: translate(-50%, 0); z-index: $centeredVideoLabelZ; &.moveToCorner { bottom: auto; + transform: none; + -webkit-transition: all 2s 2s linear; + transition: all 2s 2s linear; } } diff --git a/modules/UI/recording/Recording.js b/modules/UI/recording/Recording.js index 99295d9bc..6c6b1ecc2 100644 --- a/modules/UI/recording/Recording.js +++ b/modules/UI/recording/Recording.js @@ -22,6 +22,50 @@ import VideoLayout from '../videolayout/VideoLayout'; import { setToolboxEnabled } from '../../../react/features/toolbox'; import { setNotificationsEnabled } from '../../../react/features/notifications'; +import { + hideRecordingLabel, + updateRecordingState +} from '../../../react/features/recording'; + +const Status = JitsiMeetJS.constants.recordingStatus; + +/** + * Translation keys to use for display in the UI when recording the conference + * but not streaming live. + * + * @private + * @type {Object} + */ +export const RECORDING_TRANSLATION_KEYS = { + failedToStartKey: 'recording.failedToStart', + recordingBusy: 'liveStreaming.busy', + recordingButtonTooltip: 'recording.buttonTooltip', + recordingErrorKey: 'recording.error', + recordingOffKey: 'recording.off', + recordingOnKey: 'recording.on', + recordingPendingKey: 'recording.pending', + recordingTitle: 'dialog.recording', + recordingUnavailable: 'recording.unavailable' +}; + +/** + * Translation keys to use for display in the UI when the recording mode is + * currently streaming live. + * + * @private + * @type {Object} + */ +export const STREAMING_TRANSLATION_KEYS = { + failedToStartKey: 'liveStreaming.failedToStart', + recordingBusy: 'liveStreaming.busy', + recordingButtonTooltip: 'liveStreaming.buttonTooltip', + recordingErrorKey: 'liveStreaming.error', + recordingOffKey: 'liveStreaming.off', + recordingOnKey: 'liveStreaming.on', + recordingPendingKey: 'liveStreaming.pending', + recordingTitle: 'dialog.liveStreaming', + recordingUnavailable: 'liveStreaming.unavailable' +}; /** * The dialog for user input. @@ -194,57 +238,6 @@ function _showStopRecordingPrompt(recordingType) { }); } -/** - * Moves the element given by {selector} to the top right corner of the screen. - * Set additional classes that can be used to style the selector relative to the - * state of the filmstrip. - * - * @param selector the selector for the element to move - * @param move {true} to move the element, {false} to move it back to its intial - * position - */ -function moveToCorner(selector, move) { - let moveToCornerClass = "moveToCorner"; - let containsClass = selector.hasClass(moveToCornerClass); - - if (move && !containsClass) - selector.addClass(moveToCornerClass); - else if (!move && containsClass) - selector.removeClass(moveToCornerClass); - - const { - remoteVideosVisible, - visible - } = APP.store.getState()['features/filmstrip']; - const filmstripWasHidden = selector.hasClass('without-filmstrip'); - const filmstipIsOpening = filmstripWasHidden && visible; - selector.toggleClass('opening', filmstipIsOpening); - - selector.toggleClass('with-filmstrip', visible); - selector.toggleClass('without-filmstrip', !visible); - - selector.toggleClass('with-remote-videos', remoteVideosVisible); - selector.toggleClass('without-remote-videos', !remoteVideosVisible); -} - -/** - * The status of the recorder. - * FIXME: Those constants should come from the library. - * @type {{ON: string, OFF: string, AVAILABLE: string, - * UNAVAILABLE: string, PENDING: string}} - */ -var Status = { - ON: "on", - OFF: "off", - AVAILABLE: "available", - UNAVAILABLE: "unavailable", - PENDING: "pending", - RETRYING: "retrying", - ERROR: "error", - FAILED: "failed", - BUSY: "busy" -}; - /** * Checks whether if the given status is either PENDING or RETRYING * @param status {Status} Jibri status to be checked @@ -271,27 +264,11 @@ var Recording = { if (recordingType === 'jibri') { this.baseClass = "fa fa-play-circle"; - this.recordingTitle = "dialog.liveStreaming"; - this.recordingOnKey = "liveStreaming.on"; - this.recordingOffKey = "liveStreaming.off"; - this.recordingPendingKey = "liveStreaming.pending"; - this.failedToStartKey = "liveStreaming.failedToStart"; - this.recordingErrorKey = "liveStreaming.error"; - this.recordingButtonTooltip = "liveStreaming.buttonTooltip"; - this.recordingUnavailable = "liveStreaming.unavailable"; - this.recordingBusy = "liveStreaming.busy"; + Object.assign(this, STREAMING_TRANSLATION_KEYS); } else { this.baseClass = "icon-recEnable"; - this.recordingTitle = "dialog.recording"; - this.recordingOnKey = "recording.on"; - this.recordingOffKey = "recording.off"; - this.recordingPendingKey = "recording.pending"; - this.failedToStartKey = "recording.failedToStart"; - this.recordingErrorKey = "recording.error"; - this.recordingButtonTooltip = "recording.buttonTooltip"; - this.recordingUnavailable = "recording.unavailable"; - this.recordingBusy = "liveStreaming.busy"; + Object.assign(this, RECORDING_TRANSLATION_KEYS); } // XXX Due to the React-ification of Toolbox, the HTMLElement with id @@ -311,10 +288,6 @@ var Recording = { APP.store.dispatch(setNotificationsEnabled(false)); APP.UI.messageHandler.enablePopups(false); } - - this.eventEmitter.addListener(UIEvents.UPDATED_FILMSTRIP_DISPLAY, () =>{ - this._updateStatusLabel(); - }); }, /** @@ -364,65 +337,85 @@ var Recording = { let oldState = this.currentState; this.currentState = recordingState; - // TODO: handle recording state=available - if (recordingState === Status.ON || - recordingState === Status.RETRYING) { + let labelDisplayConfiguration; + + switch (recordingState) { + case Status.ON: + case Status.RETRYING: { + labelDisplayConfiguration = { + centered: false, + key: this.recordingOnKey, + showSpinner: recordingState === Status.RETRYING + }; this._setToolbarButtonToggled(true); - this._updateStatusLabel(this.recordingOnKey, false); + break; } - else if (recordingState === Status.OFF - || recordingState === Status.UNAVAILABLE - || recordingState === Status.BUSY - || recordingState === Status.FAILED) { - // We don't want to do any changes if this is - // an availability change. - if (oldState !== Status.ON - && !isStartingStatus(oldState)) + case Status.OFF: + case Status.BUSY: + case Status.FAILED: + case Status.UNAVAILABLE: { + const wasInStartingStatus = isStartingStatus(oldState); + + // We don't want UI changes if this is an availability change. + if (oldState !== Status.ON && !wasInStartingStatus) { + APP.store.dispatch(updateRecordingState({ recordingState })); return; + } + + labelDisplayConfiguration = { + centered: true, + key: wasInStartingStatus + ? this.failedToStartKey + : this.recordingOffKey + }; this._setToolbarButtonToggled(false); - let messageKey; - if (isStartingStatus(oldState)) - messageKey = this.failedToStartKey; - else - messageKey = this.recordingOffKey; - - this._updateStatusLabel(messageKey, true); - setTimeout(function(){ - $('#recordingLabel').css({display: "none"}); + APP.store.dispatch(hideRecordingLabel()); }, 5000); + + break; } - else if (recordingState === Status.PENDING) { + + case Status.PENDING: { + labelDisplayConfiguration = { + centered: true, + key: this.recordingPendingKey + }; this._setToolbarButtonToggled(false); - this._updateStatusLabel(this.recordingPendingKey, true); + break; } - else if (recordingState === Status.ERROR - || recordingState === Status.FAILED) { + + case Status.ERROR: { + labelDisplayConfiguration = { + centered: true, + key: this.recordingErrorKey + }; this._setToolbarButtonToggled(false); - this._updateStatusLabel(this.recordingErrorKey, true); + break; } - let labelSelector = $('#recordingLabel'); + // Return an empty label display configuration to indicate no label + // should be displayed. The Status.AVAIABLE case is handled here. + default: { + labelDisplayConfiguration = null; + } + } - // We don't show the label for available state. - if (recordingState !== Status.AVAILABLE - && !labelSelector.is(":visible")) - labelSelector.css({display: "inline-block"}); - - // Recording spinner - let spinnerId = 'recordingSpinner'; - UIUtil.setVisible( - spinnerId, recordingState === Status.RETRYING); + APP.store.dispatch(updateRecordingState({ + labelDisplayConfiguration, + recordingState + })); }, + // checks whether recording is enabled and whether we have params // to start automatically recording checkAutoRecord() { @@ -432,21 +425,6 @@ var Recording = { this.predefinedToken); } }, - /** - * Updates the status label. - * @param textKey the text to show - * @param isCentered indicates if the label should be centered on the window - * or moved to the top right corner. - */ - _updateStatusLabel(textKey, isCentered) { - let labelSelector = $('#recordingLabel'); - let labelTextSelector = $('#recordingLabelText'); - - moveToCorner(labelSelector, !isCentered); - - labelTextSelector.attr("data-i18n", textKey); - APP.translation.translateElement(labelSelector); - }, /** * Handles {@code click} on {@code toolbar_button_record}. diff --git a/react/features/filmstrip/middleware.js b/react/features/filmstrip/middleware.js index 132776a70..637b8d941 100644 --- a/react/features/filmstrip/middleware.js +++ b/react/features/filmstrip/middleware.js @@ -4,12 +4,6 @@ import { MiddlewareRegistry } from '../base/redux'; import { SET_CALL_OVERLAY_VISIBLE } from '../jwt'; import Filmstrip from '../../../modules/UI/videolayout/Filmstrip'; -import UIEvents from '../../../service/UI/UIEvents'; - -import { - SET_FILMSTRIP_REMOTE_VIDEOS_VISIBLITY, - SET_FILMSTRIP_VISIBILITY -} from './actionTypes'; declare var APP: Object; @@ -37,16 +31,6 @@ MiddlewareRegistry.register(({ getState }) => next => action => { return result; } break; - - case SET_FILMSTRIP_REMOTE_VIDEOS_VISIBLITY: - case SET_FILMSTRIP_VISIBILITY: { - const result = next(action); - - typeof APP === 'undefined' - || APP.UI.emitEvent(UIEvents.UPDATED_FILMSTRIP_DISPLAY); - - return result; - } } return next(action); diff --git a/react/features/large-video/components/LargeVideo.web.js b/react/features/large-video/components/LargeVideo.web.js index 732ea1bf7..c8f82107f 100644 --- a/react/features/large-video/components/LargeVideo.web.js +++ b/react/features/large-video/components/LargeVideo.web.js @@ -4,6 +4,7 @@ import React, { Component } from 'react'; import { Watermarks } from '../../base/react'; import { VideoQualityLabel } from '../../video-quality'; +import { RecordingLabel } from '../../recording'; declare var interfaceConfig: Object; @@ -67,18 +68,8 @@ export default class LargeVideo extends Component { - { interfaceConfig.filmStripOnly ? null : } - - - - - + ); } diff --git a/react/features/recording/actionTypes.js b/react/features/recording/actionTypes.js new file mode 100644 index 000000000..dc85a1cec --- /dev/null +++ b/react/features/recording/actionTypes.js @@ -0,0 +1,22 @@ +/** + * The type of Redux action which signals for the label indicating current + * recording state to stop displaying. + * + * { + * type: HIDE_RECORDING_LABEL + * } + * @public + */ +export const HIDE_RECORDING_LABEL = Symbol('HIDE_RECORDING_LABEL'); + +/** + * The type of Redux action which updates the current known state of the + * recording feature. + * + * { + * type: RECORDING_STATE_UPDATED, + * recordingState: string + * } + * @public + */ +export const RECORDING_STATE_UPDATED = Symbol('RECORDING_STATE_UPDATED'); diff --git a/react/features/recording/actions.js b/react/features/recording/actions.js new file mode 100644 index 000000000..8b74ae7d6 --- /dev/null +++ b/react/features/recording/actions.js @@ -0,0 +1,31 @@ +import { HIDE_RECORDING_LABEL, RECORDING_STATE_UPDATED } from './actionTypes'; + +/** + * Hides any displayed recording label, regardless of current recording state. + * + * @returns {{ + * type: HIDE_RECORDING_LABEL + * }} + */ +export function hideRecordingLabel() { + return { + type: HIDE_RECORDING_LABEL + }; +} + +/** + * Updates the redux state for the recording feature. + * + * @param {Object} recordingState - The new state to merge with the existing + * state in redux. + * @returns {{ + * type: RECORDING_STATE_UPDATED, + * recordingState: Object + * }} + */ +export function updateRecordingState(recordingState = {}) { + return { + type: RECORDING_STATE_UPDATED, + recordingState + }; +} diff --git a/react/features/recording/components/RecordingLabel.native.js b/react/features/recording/components/RecordingLabel.native.js new file mode 100644 index 000000000..e69de29bb diff --git a/react/features/recording/components/RecordingLabel.web.js b/react/features/recording/components/RecordingLabel.web.js new file mode 100644 index 000000000..e3dd9d280 --- /dev/null +++ b/react/features/recording/components/RecordingLabel.web.js @@ -0,0 +1,171 @@ +import React, { Component } from 'react'; +import { connect } from 'react-redux'; + +import { translate } from '../../base/i18n'; + +/** + * Implements a React {@link Component} which displays the current state of + * conference recording. Currently it uses CSS to display itself automatically + * when there is a recording state update. + * + * @extends {Component} + */ +class RecordingLabel extends Component { + /** + * {@code RecordingLabel} component's property types. + * + * @static + */ + static propTypes = { + /** + * Whether or not the filmstrip is currently visible or toggled to + * hidden. Depending on the filmstrip state, different CSS classes will + * be set to allow for adjusting of {@code RecordingLabel} positioning. + */ + _filmstripVisible: React.PropTypes.bool, + + /** + * An object to describe the {@code RecordingLabel} content. If no + * translation key to display is specified, the label will apply CSS to + * itself so it can be made invisible. + * {{ + * centered: boolean, + * key: string, + * showSpinner: boolean + * }} + */ + _labelDisplayConfiguration: React.PropTypes.object, + + /** + * Whether or not remote videos within the filmstrip are currently + * visible. Depending on the visibility state, coupled with filmstrip + * visibility, CSS classes will be set to allow for adjusting of + * {@code RecordingLabel} positioning. + */ + _remoteVideosVisible: React.PropTypes.bool, + + /** + * Invoked to obtain translated string. + */ + t: React.PropTypes.func + }; + + /** + * Initializes a new {@code RecordingLabel} 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 filmstrip was not visible but has transitioned + * in the latest component update to visible. This boolean is used + * to set a class for position animations. + * + * @type {boolean} + */ + filmstripBecomingVisible: false + }; + } + + /** + * Updates the state for whether or not the filmstrip is being toggled to + * display after having being hidden. + * + * @inheritdoc + * @param {Object} nextProps - The read-only props which this Component will + * receive. + * @returns {void} + */ + componentWillReceiveProps(nextProps) { + this.setState({ + filmstripBecomingVisible: nextProps._filmstripVisible + && !this.props._filmstripVisible + }); + } + + /** + * Implements React's {@link Component#render()}. + * + * @inheritdoc + * @returns {ReactElement} + */ + render() { + const { _labelDisplayConfiguration } = this.props; + const { centered, key, showSpinner } = _labelDisplayConfiguration || {}; + + const isVisible = Boolean(key); + const rootClassName = [ + 'video-state-indicator centeredVideoLabel', + isVisible ? 'show-inline' : '', + centered ? '' : 'moveToCorner', + this.state.filmstripBecomingVisible ? 'opening' : '', + this.props._filmstripVisible + ? 'with-filmstrip' : 'without-filmstrip', + this.props._remoteVideosVisible + ? 'with-remote-videos' : 'without-remote-videos' + ].join(' '); + + return ( + + + { this.props.t(key) } + + { showSpinner + ? + : null } + + ); + } +} + +/** + * Maps (parts of) the Redux state to the associated {@code RecordingLabel} + * component's props. + * + * @param {Object} state - The Redux state. + * @private + * @returns {{ + * _filmstripVisible: boolean, + * _labelDisplayConfiguration: Object, + * _remoteVideosVisible: boolean, + * }} + */ +function _mapStateToProps(state) { + const { remoteVideosVisible, visible } = state['features/filmstrip']; + const { labelDisplayConfiguration } = state['features/recording']; + + return { + /** + * Whether or not the filmstrip is currently set to be displayed. + * + * @type {boolean} + */ + _filmstripVisible: visible, + + /** + * An object describing how {@code RecordingLabel} should display its + * contents. + * + * @type {Object} + */ + _labelDisplayConfiguration: labelDisplayConfiguration, + + /** + * Whether or not remote videos are displayed in the filmstrip. + * + * @type {boolean} + */ + _remoteVideosVisible: remoteVideosVisible + }; +} + +export default translate(connect(_mapStateToProps)(RecordingLabel)); diff --git a/react/features/recording/components/index.js b/react/features/recording/components/index.js new file mode 100644 index 000000000..90ce231bd --- /dev/null +++ b/react/features/recording/components/index.js @@ -0,0 +1 @@ +export { default as RecordingLabel } from './RecordingLabel'; diff --git a/react/features/recording/index.js b/react/features/recording/index.js new file mode 100644 index 000000000..582e1f9dd --- /dev/null +++ b/react/features/recording/index.js @@ -0,0 +1,4 @@ +export * from './actions'; +export * from './components'; + +import './reducer'; diff --git a/react/features/recording/reducer.js b/react/features/recording/reducer.js new file mode 100644 index 000000000..16406ed9d --- /dev/null +++ b/react/features/recording/reducer.js @@ -0,0 +1,24 @@ +import { ReducerRegistry } from '../base/redux'; +import { HIDE_RECORDING_LABEL, RECORDING_STATE_UPDATED } from './actionTypes'; + +/** + * Reduces the Redux actions of the feature features/recording. + */ +ReducerRegistry.register('features/recording', (state = {}, action) => { + switch (action.type) { + case HIDE_RECORDING_LABEL: + return { + ...state, + labelDisplayConfiguration: null + }; + + case RECORDING_STATE_UPDATED: + return { + ...state, + ...action.recordingState + }; + + default: + return state; + } +}); diff --git a/service/UI/UIEvents.js b/service/UI/UIEvents.js index 3f033d1aa..19a4689f6 100644 --- a/service/UI/UIEvents.js +++ b/service/UI/UIEvents.js @@ -66,11 +66,6 @@ export default { */ TOGGLED_FILMSTRIP: "UI.toggled_filmstrip", - /** - * Notifies that the filmstrip has updated its appearance, such as by - * toggling or removing videos or adding videos. - */ - UPDATED_FILMSTRIP_DISPLAY: "UI.updated_filmstrip_display", TOGGLE_SCREENSHARING: "UI.toggle_screensharing", TOGGLED_SHARED_DOCUMENT: "UI.toggled_shared_document", CONTACT_CLICKED: "UI.contact_clicked",