Modal dialog for displaying dominant speaker times

This commit is contained in:
Leonard Kim
2017-02-16 16:59:30 -08:00
committed by Дамян Минков
parent 59a74153dc
commit 989161159d
13 changed files with 505 additions and 2 deletions

View File

@@ -30,3 +30,25 @@ export function openDialog(component, componentProps) {
componentProps
};
}
/**
* Signals Dialog to open a dialog with the specified component if the component
* is not already open. If it is open, then Dialog is signaled to close
* its dialog.
*
* @param {Object} component - The component to display as dialog.
* @param {Object} componentProps - The properties needed for that component.
* @returns {Object}
*/
export function toggleDialog(component, componentProps) {
return (dispatch, getState) => {
const state = getState();
const dialogState = state['features/base/dialog'];
if (dialogState.component === component) {
dispatch(hideDialog());
} else {
dispatch(openDialog(component, componentProps));
}
};
}

View File

@@ -0,0 +1,29 @@
/**
* Counts how many whole hours are included in the given time total.
*
* @param {number} milliseconds - The millisecond total to get hours from.
* @returns {number}
*/
export function getHoursCount(milliseconds) {
return Math.floor(milliseconds / (60 * 60 * 1000));
}
/**
* Counts how many whole minutes are included in the given time total.
*
* @param {number} milliseconds - The millisecond total to get minutes from.
* @returns {number}
*/
export function getMinutesCount(milliseconds) {
return Math.floor(milliseconds / (60 * 1000) % 60);
}
/**
* Counts how many whole seconds are included in the given time total.
*
* @param {number} milliseconds - The millisecond total to get seconds from.
* @returns {number}
*/
export function getSecondsCount(milliseconds) {
return Math.floor(milliseconds / 1000 % 60);
}

View File

@@ -0,0 +1,150 @@
/* global APP, interfaceConfig */
import React, { Component } from 'react';
import { Dialog } from '../../base/dialog';
import { translate } from '../../base/i18n';
import SpeakerStatsItem from './SpeakerStatsItem';
import SpeakerStatsLabels from './SpeakerStatsLabels';
/**
* React component for displaying a list of speaker stats.
*
* @extends Component
*/
class SpeakerStats extends Component {
/**
* SpeakerStats component's property types.
*
* @static
*/
static propTypes = {
/**
* The JitsiConference from which stats will be pulled.
*/
conference: React.PropTypes.object,
/**
* The function to translate human-readable text.
*/
t: React.PropTypes.func
}
/**
* Initializes a new SpeakerStats instance.
*
* @param {Object} props - The read-only React Component props with which
* the new instance is to be initialized.
*/
constructor(props) {
super(props);
this.state = {
stats: {}
};
this._updateInterval = null;
this._updateStats = this._updateStats.bind(this);
}
/**
* Immediately request for updated speaker stats and begin
* polling for speaker stats updates.
*
* @inheritdoc
* @returns {void}
*/
componentWillMount() {
this._updateStats();
this._updateInterval = setInterval(this._updateStats, 1000);
}
/**
* Stop polling for speaker stats updates.
*
* @inheritdoc
* @returns {void}
*/
componentWillUnmount() {
clearInterval(this._updateInterval);
}
/**
* Implements React's {@link Component#render()}.
*
* @inheritdoc
* @returns {ReactElement}
*/
render() {
const userIds = Object.keys(this.state.stats);
const items = userIds.map(userId => this._createStatsItem(userId));
return (
<Dialog
cancelTitleKey = { 'dialog.close' }
submitDisabled = { true }
titleKey = 'speakerStats.speakerStats'>
<div className = 'speaker-stats'>
<SpeakerStatsLabels />
{ items }
</div>
</Dialog>
);
}
/**
* Update the internal state with the latest speaker stats.
*
* @returns {void}
* @private
*/
_updateStats() {
const stats = this.props.conference.getSpeakerStats();
this.setState({ stats });
}
/**
* Create a SpeakerStatsItem instance for the passed in user id.
*
* @param {string} userId - User id used to look up the associated
* speaker stats from the jitsi library.
* @returns {SpeakerStatsItem|null}
* @private
*/
_createStatsItem(userId) {
const statsModel = this.state.stats[userId];
if (!statsModel) {
return null;
}
const isDominantSpeaker = statsModel.isDominantSpeaker();
const dominantSpeakerTime = statsModel.getTotalDominantSpeakerTime();
const hasLeft = statsModel.hasLeft();
let displayName = '';
if (statsModel.isLocalStats()) {
const { t } = this.props;
const meString = t('me');
displayName = APP.settings.getDisplayName();
displayName = displayName ? `${displayName} (${meString})`
: meString;
} else {
displayName = this.state.stats[userId].getDisplayName()
|| interfaceConfig.DEFAULT_REMOTE_DISPLAY_NAME;
}
return (
<SpeakerStatsItem
displayName = { displayName }
dominantSpeakerTime = { dominantSpeakerTime }
hasLeft = { hasLeft }
isDominantSpeaker = { isDominantSpeaker }
key = { userId } />
);
}
}
export default translate(SpeakerStats);

View File

@@ -0,0 +1,69 @@
import React, { Component } from 'react';
import TimeElapsed from './TimeElapsed';
/**
* React component for display an individual user's speaker stats.
*
* @extends Component
*/
class SpeakerStatsItem extends Component {
/**
* SpeakerStatsItem component's property types.
*
* @static
*/
static propTypes = {
/**
* The name of the participant.
*/
displayName: React.PropTypes.string,
/**
* The total milliseconds the participant has been dominant speaker.
*/
dominantSpeakerTime: React.PropTypes.number,
/**
* True if the participant is no longer in the meeting.
*/
hasLeft: React.PropTypes.bool,
/**
* True if the participant is currently the dominant speaker.
*/
isDominantSpeaker: React.PropTypes.bool
}
/**
* Implements React's {@link Component#render()}.
*
* @inheritdoc
* @returns {ReactElement}
*/
render() {
const hasLeftClass = this.props.hasLeft ? 'status-user-left' : '';
const rowDisplayClass = `speaker-stats-item ${hasLeftClass}`;
const dotClass = this.props.isDominantSpeaker
? 'status-active' : 'status-inactive';
const speakerStatusClass = `speaker-stats-item__status-dot ${dotClass}`;
return (
<div className = { rowDisplayClass }>
<div className = 'speaker-stats-item__status'>
<span className = { speakerStatusClass } />
</div>
<div className = 'speaker-stats-item__name'>
{ this.props.displayName }
</div>
<div className = 'speaker-stats-item__time'>
<TimeElapsed
time = { this.props.dominantSpeakerTime } />
</div>
</div>
);
}
}
export default SpeakerStatsItem;

View File

@@ -0,0 +1,46 @@
import React, { Component } from 'react';
import { translate } from '../../base/i18n';
/**
* React component for labeling speaker stats column items.
*
* @extends Component
*/
class SpeakerStatsLabels extends Component {
/**
* SpeakerStatsLabels component's property types.
*
* @static
*/
static propTypes = {
/**
* The function to translate human-readable text.
*/
t: React.PropTypes.func
}
/**
* Implements React's {@link Component#render()}.
*
* @inheritdoc
* @returns {ReactElement}
*/
render() {
const { t } = this.props;
return (
<div className = 'speaker-stats-item__labels'>
<div className = 'speaker-stats-item__status' />
<div className = 'speaker-stats-item__name'>
{ t('speakerStats.name') }
</div>
<div className = 'speaker-stats-item__time'>
{ t('speakerStats.speakerTime') }
</div>
</div>
);
}
}
export default translate(SpeakerStatsLabels);

View File

@@ -0,0 +1,94 @@
import React, { Component } from 'react';
import { translate } from '../../base/i18n';
import {
getHoursCount,
getMinutesCount,
getSecondsCount
} from '../../base/util/timeUtils';
/**
* React component for displaying total time elapsed. Converts a total count of
* milliseconds into a more humanized form: "# hours, # minutes, # seconds".
* With a time of 0, "0s" will be displayed.
*
* @extends Component
*/
class TimeElapsed extends Component {
/**
* TimeElapsed component's property types.
*
* @static
*/
static propTypes = {
/**
* The function to translate human-readable text.
*/
t: React.PropTypes.func,
/**
* The milliseconds to be converted into a humanized format.
*/
time: React.PropTypes.number
}
/**
* Implements React's {@link Component#render()}.
*
* @inheritdoc
* @returns {ReactElement}
*/
render() {
const hours = getHoursCount(this.props.time);
const minutes = getMinutesCount(this.props.time);
const seconds = getSecondsCount(this.props.time);
const timeElapsed = [];
if (hours) {
const hourPassed = this._createTimeDisplay(hours,
'speakerStats.hours', 'hours');
timeElapsed.push(hourPassed);
}
if (hours || minutes) {
const minutesPassed = this._createTimeDisplay(minutes,
'speakerStats.minutes', 'minutes');
timeElapsed.push(minutesPassed);
}
const secondsPassed = this._createTimeDisplay(seconds,
'speakerStats.seconds', 'seconds');
timeElapsed.push(secondsPassed);
return (
<div>
{ timeElapsed }
</div>
);
}
/**
* Returns a ReactElement to display the passed in count and a count noun.
*
* @private
* @param {number} count - The number used for display and to check for
* count noun plurality.
* @param {string} countNounKey - Translation key for the time's count noun.
* @param {string} countType - What is being counted. Used as the element's
* key for react to iterate upon.
* @returns {ReactElement}
*/
_createTimeDisplay(count, countNounKey, countType) {
const { t } = this.props;
return (
<span key = { countType } > { t(countNounKey, { count }) } </span>
);
}
}
export default translate(TimeElapsed);

View File

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

View File

@@ -0,0 +1 @@
export * from './components';