405 lines
11 KiB
JavaScript
405 lines
11 KiB
JavaScript
// @flow
|
|
|
|
import moment from 'moment';
|
|
import React, { Component } from 'react';
|
|
import type { Dispatch } from 'redux';
|
|
|
|
import { Dialog } from '../../base/dialog';
|
|
import { translate } from '../../base/i18n';
|
|
import {
|
|
PARTICIPANT_ROLE,
|
|
getLocalParticipant
|
|
} from '../../base/participants';
|
|
import { connect } from '../../base/redux';
|
|
|
|
import { statsUpdate } from '../actions';
|
|
import { recordingController } from '../controller';
|
|
|
|
|
|
/**
|
|
* The type of the React {@code Component} props of
|
|
* {@link LocalRecordingInfoDialog}.
|
|
*/
|
|
type Props = {
|
|
|
|
/**
|
|
* Redux store dispatch function.
|
|
*/
|
|
dispatch: Dispatch<any>,
|
|
|
|
/**
|
|
* Current encoding format.
|
|
*/
|
|
encodingFormat: string,
|
|
|
|
/**
|
|
* Whether the local user is the moderator.
|
|
*/
|
|
isModerator: boolean,
|
|
|
|
/**
|
|
* Whether local recording is engaged.
|
|
*/
|
|
isEngaged: boolean,
|
|
|
|
/**
|
|
* The start time of the current local recording session.
|
|
* Used to calculate the duration of recording.
|
|
*/
|
|
recordingEngagedAt: Date,
|
|
|
|
/**
|
|
* Stats of all the participant.
|
|
*/
|
|
stats: Object,
|
|
|
|
/**
|
|
* Invoked to obtain translated strings.
|
|
*/
|
|
t: Function
|
|
}
|
|
|
|
/**
|
|
* The type of the React {@code Component} state of
|
|
* {@link LocalRecordingInfoDialog}.
|
|
*/
|
|
type State = {
|
|
|
|
/**
|
|
* The recording duration string to be displayed on the UI.
|
|
*/
|
|
durationString: string
|
|
}
|
|
|
|
/**
|
|
* A React Component with the contents for a dialog that shows information about
|
|
* local recording. For users with moderator rights, this is also the "control
|
|
* panel" for starting/stopping local recording on all clients.
|
|
*
|
|
* @extends Component
|
|
*/
|
|
class LocalRecordingInfoDialog extends Component<Props, State> {
|
|
|
|
/**
|
|
* Saves a handle to the timer for UI updates,
|
|
* so that it can be cancelled when the component unmounts.
|
|
*/
|
|
_timer: ?IntervalID;
|
|
|
|
/**
|
|
* Initializes a new {@code LocalRecordingInfoDialog} instance.
|
|
*
|
|
* @param {Props} props - The React {@code Component} props to initialize
|
|
* the new {@code LocalRecordingInfoDialog} instance with.
|
|
*/
|
|
constructor(props: Props) {
|
|
super(props);
|
|
this.state = {
|
|
durationString: ''
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Implements React's {@link Component#componentDidMount()}.
|
|
*
|
|
* @returns {void}
|
|
*/
|
|
componentDidMount() {
|
|
this._timer = setInterval(
|
|
() => {
|
|
this.setState((_prevState, props) => {
|
|
const nowTime = new Date();
|
|
|
|
return {
|
|
durationString: this._getDuration(nowTime,
|
|
props.recordingEngagedAt)
|
|
};
|
|
});
|
|
try {
|
|
this.props.dispatch(
|
|
statsUpdate(recordingController
|
|
.getParticipantsStats()));
|
|
} catch (e) {
|
|
// do nothing
|
|
}
|
|
},
|
|
1000
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Implements React's {@link Component#componentWillUnmount()}.
|
|
*
|
|
* @returns {void}
|
|
*/
|
|
componentWillUnmount() {
|
|
if (this._timer) {
|
|
clearInterval(this._timer);
|
|
this._timer = null;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Implements React's {@link Component#render()}.
|
|
*
|
|
* @inheritdoc
|
|
* @returns {ReactElement}
|
|
*/
|
|
render() {
|
|
const { isModerator, t } = this.props;
|
|
|
|
return (
|
|
<Dialog
|
|
cancelKey = { 'dialog.close' }
|
|
submitDisabled = { true }
|
|
titleKey = 'localRecording.dialogTitle'>
|
|
<div className = 'localrec-control'>
|
|
<span className = 'localrec-control-info-label'>
|
|
{`${t('localRecording.moderator')}:`}
|
|
</span>
|
|
<span className = 'info-value'>
|
|
{ isModerator
|
|
? t('localRecording.yes')
|
|
: t('localRecording.no') }
|
|
</span>
|
|
</div>
|
|
{ this._renderModeratorControls() }
|
|
{ this._renderDurationAndFormat() }
|
|
</Dialog>
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Renders the recording duration and encoding format. Only shown if local
|
|
* recording is engaged.
|
|
*
|
|
* @private
|
|
* @returns {ReactElement|null}
|
|
*/
|
|
_renderDurationAndFormat() {
|
|
const { encodingFormat, isEngaged, t } = this.props;
|
|
const { durationString } = this.state;
|
|
|
|
if (!isEngaged) {
|
|
return null;
|
|
}
|
|
|
|
return (
|
|
<div>
|
|
<div>
|
|
<span className = 'localrec-control-info-label'>
|
|
{`${t('localRecording.duration')}:`}
|
|
</span>
|
|
<span className = 'info-value'>
|
|
{ durationString === ''
|
|
? t('localRecording.durationNA')
|
|
: durationString }
|
|
</span>
|
|
</div>
|
|
<div>
|
|
<span className = 'localrec-control-info-label'>
|
|
{`${t('localRecording.encoding')}:`}
|
|
</span>
|
|
<span className = 'info-value'>
|
|
{ encodingFormat }
|
|
</span>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Returns React elements for displaying the local recording stats of
|
|
* each participant.
|
|
*
|
|
* @private
|
|
* @returns {ReactElement|null}
|
|
*/
|
|
_renderStats() {
|
|
const { stats } = this.props;
|
|
|
|
if (stats === undefined) {
|
|
return null;
|
|
}
|
|
const ids = Object.keys(stats);
|
|
|
|
return (
|
|
<div className = 'localrec-participant-stats' >
|
|
{ this._renderStatsHeader() }
|
|
{ ids.map((id, i) => this._renderStatsLine(i, id)) }
|
|
</div>
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Renders the stats for one participant.
|
|
*
|
|
* @private
|
|
* @param {*} lineKey - The key required by React for elements in lists.
|
|
* @param {*} id - The ID of the participant.
|
|
* @returns {ReactElement}
|
|
*/
|
|
_renderStatsLine(lineKey, id) {
|
|
const { stats } = this.props;
|
|
let statusClass = 'localrec-participant-stats-item__status-dot ';
|
|
|
|
statusClass += stats[id].recordingStats
|
|
? stats[id].recordingStats.isRecording
|
|
? 'status-on'
|
|
: 'status-off'
|
|
: 'status-unknown';
|
|
|
|
return (
|
|
<div
|
|
className = 'localrec-participant-stats-item'
|
|
key = { lineKey } >
|
|
<div className = 'localrec-participant-stats-item__status'>
|
|
<span className = { statusClass } />
|
|
</div>
|
|
<div className = 'localrec-participant-stats-item__name'>
|
|
{ stats[id].displayName || id }
|
|
</div>
|
|
<div className = 'localrec-participant-stats-item__sessionid'>
|
|
{ stats[id].recordingStats.currentSessionToken }
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Renders the participant stats header line.
|
|
*
|
|
* @private
|
|
* @returns {ReactElement}
|
|
*/
|
|
_renderStatsHeader() {
|
|
const { t } = this.props;
|
|
|
|
return (
|
|
<div className = 'localrec-participant-stats-item'>
|
|
<div className = 'localrec-participant-stats-item__status' />
|
|
<div className = 'localrec-participant-stats-item__name'>
|
|
{ t('localRecording.participant') }
|
|
</div>
|
|
<div className = 'localrec-participant-stats-item__sessionid'>
|
|
{ t('localRecording.sessionToken') }
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Renders the moderator-only controls: The stats of all users and the
|
|
* action links.
|
|
*
|
|
* @private
|
|
* @returns {ReactElement|null}
|
|
*/
|
|
_renderModeratorControls() {
|
|
const { isModerator, isEngaged, t } = this.props;
|
|
|
|
if (!isModerator) {
|
|
return null;
|
|
}
|
|
|
|
return (
|
|
<div>
|
|
<div className = 'localrec-control-action-links'>
|
|
<div className = 'localrec-control-action-link'>
|
|
{ isEngaged ? <a
|
|
onClick = { this._onStop }>
|
|
{ t('localRecording.stop') }
|
|
</a>
|
|
: <a
|
|
onClick = { this._onStart }>
|
|
{ t('localRecording.start') }
|
|
</a>
|
|
}
|
|
</div>
|
|
</div>
|
|
<div>
|
|
<span className = 'localrec-control-info-label'>
|
|
{`${t('localRecording.participantStats')}:`}
|
|
</span>
|
|
</div>
|
|
{ this._renderStats() }
|
|
</div>
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Creates a duration string "HH:MM:SS" from two Date objects.
|
|
*
|
|
* @param {Date} now - Current time.
|
|
* @param {Date} prev - Previous time, the time to be subtracted.
|
|
* @returns {string}
|
|
*/
|
|
_getDuration(now, prev) {
|
|
if (prev === null || prev === undefined) {
|
|
return '';
|
|
}
|
|
|
|
// Still a hack, as moment.js does not support formatting of duration
|
|
// (i.e. TimeDelta). Only works if total duration < 24 hours.
|
|
// But who is going to have a 24-hour long conference?
|
|
return moment(now - prev).utc()
|
|
.format('HH:mm:ss');
|
|
}
|
|
|
|
/**
|
|
* Callback function for the Start UI action.
|
|
*
|
|
* @private
|
|
* @returns {void}
|
|
*/
|
|
_onStart() {
|
|
recordingController.startRecording();
|
|
}
|
|
|
|
/**
|
|
* Callback function for the Stop UI action.
|
|
*
|
|
* @private
|
|
* @returns {void}
|
|
*/
|
|
_onStop() {
|
|
recordingController.stopRecording();
|
|
}
|
|
|
|
}
|
|
|
|
/**
|
|
* Maps (parts of) the Redux state to the associated props for the
|
|
* {@code LocalRecordingInfoDialog} component.
|
|
*
|
|
* @param {Object} state - The Redux state.
|
|
* @private
|
|
* @returns {{
|
|
* encodingFormat: string,
|
|
* isModerator: boolean,
|
|
* isEngaged: boolean,
|
|
* recordingEngagedAt: Date,
|
|
* stats: Object
|
|
* }}
|
|
*/
|
|
function _mapStateToProps(state) {
|
|
const {
|
|
encodingFormat,
|
|
isEngaged,
|
|
recordingEngagedAt,
|
|
stats
|
|
} = state['features/local-recording'];
|
|
const isModerator
|
|
= getLocalParticipant(state).role === PARTICIPANT_ROLE.MODERATOR;
|
|
|
|
return {
|
|
encodingFormat,
|
|
isModerator,
|
|
isEngaged,
|
|
recordingEngagedAt,
|
|
stats
|
|
};
|
|
}
|
|
|
|
export default translate(connect(_mapStateToProps)(LocalRecordingInfoDialog));
|