ref(recording): convert recording label to react (#1915)

* ref(recording): convert recording label to react

- Create a RecordingLabel component for displaying the current
  recording state, as reflected in the redux store. This is
  needed for 1-on-1 mode to be completely in redux.
- Update the store with the recording state so RecordingLabel
  can update itself.
- Remove previous logic for updating the non-react label, which
  includes event emitting for filmstrip visibility changes,
  as RecordingLabel is hooked into redux updates.

* ref(recording): use status and type constants from lib

* make label really dumb, move logic back to Recording
This commit is contained in:
virtuacoplenny 2017-08-25 09:45:30 -07:00 committed by yanas
parent e04129bf4d
commit 735a596afe
13 changed files with 363 additions and 160 deletions

View File

@ -1,4 +1,3 @@
.recordingSpinner {
display: none;
vertical-align: top;
}

View File

@ -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;
}
}

View File

@ -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}.

View File

@ -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);

View File

@ -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 {
</div>
</div>
<span id = 'localConnectionMessage' />
{ interfaceConfig.filmStripOnly ? null : <VideoQualityLabel /> }
<span
className = 'video-state-indicator centeredVideoLabel'
id = 'recordingLabel'>
<span id = 'recordingLabelText' />
<img
className = 'recordingSpinner'
id = 'recordingSpinner'
src = 'images/spin.svg' />
</span>
<RecordingLabel />
</div>
);
}

View File

@ -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');

View File

@ -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
};
}

View File

@ -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 (
<span
className = { rootClassName }
id = 'recordingLabel'>
<span id = 'recordingLabelText'>
{ this.props.t(key) }
</span>
{ showSpinner
? <img
className = 'recordingSpinner'
id = 'recordingSpinner'
src = 'images/spin.svg' />
: null }
</span>
);
}
}
/**
* 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));

View File

@ -0,0 +1 @@
export { default as RecordingLabel } from './RecordingLabel';

View File

@ -0,0 +1,4 @@
export * from './actions';
export * from './components';
import './reducer';

View File

@ -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;
}
});

View File

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