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",