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

@ -714,6 +714,18 @@ export default {
sendFeedback (overallFeedback, detailedFeedback) {
return room.sendFeedback (overallFeedback, detailedFeedback);
},
/**
* Get speaker stats that track total dominant speaker time.
*
* @returns {object} A hash with keys being user ids and values being the
* library's SpeakerStats model used for calculating time as dominant
* speaker.
*/
getSpeakerStats() {
return room.getSpeakerStats();
},
/**
* Returns the connection times stored in the library.
*/

View File

@ -39,6 +39,7 @@
@import 'reload_overlay/reload_overlay';
@import 'modals/dialog';
@import 'modals/feedback/feedback';
@import 'modals/speaker_stats/speaker_stats';
@import 'videolayout_default';
@import 'notice';
@import 'popup_menu';

View File

@ -0,0 +1,56 @@
.speaker-stats {
list-style: none;
padding: 0;
color: $auiDialogColor;
width: 100%;
font-weight: 500;
.speaker-stats-item__status-dot {
position: relative;
display: block;
width: 9px;
height: 9px;
border-radius: 50%;
margin: 0 auto;
&.status-active {
background: green;
}
&.status-inactive {
background: gray;
}
}
.status-user-left {
color: $placeHolderColor;
}
.speaker-stats-item__status,
.speaker-stats-item__name,
.speaker-stats-item__time {
display: inline-block;
margin: 5px 0;
vertical-align: middle;
}
.speaker-stats-item__status {
width: 5%;
}
.speaker-stats-item__name {
width: 40%;
}
.speaker-stats-item__time {
width: 55%;
}
.speaker-stats-item:nth-child(even) {
background: whitesmoke;
}
.speaker-stats-item__name,
.speaker-stats-item__time {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
}

View File

@ -36,7 +36,8 @@
"toggleChat": "Open or close the chat",
"mute": "Mute or unmute your microphone",
"fullScreen": "Enter or exit full screen",
"videoMute": "Start or stop your camera"
"videoMute": "Start or stop your camera",
"showSpeakerStats": "Show speaker stats"
},
"welcomepage":{
"disable": "Don't show this page again",
@ -335,7 +336,8 @@
"remoteControlDeniedMessage": "__user__ rejected your remote control request!",
"remoteControlAllowedMessage": "__user__ accepted your remote control request!",
"remoteControlErrorMessage": "An error occurred while trying to request remote control permissions from __user__!",
"remoteControlStopMessage": "The remote control session ended!"
"remoteControlStopMessage": "The remote control session ended!",
"close": "Close"
},
"email":
{
@ -404,5 +406,14 @@
"streamIdHelp": "Where do I find this?",
"error": "Live streaming failed. Please try again.",
"busy": "All recorders are currently busy. Please try again later."
},
"speakerStats":
{
"hours": "__count__h",
"minutes": "__count__m",
"name": "Name",
"seconds": "__count__s",
"speakerStats": "Speaker Stats",
"speakerTime": "Speaker Time"
}
}

View File

@ -1,5 +1,10 @@
/* global APP, $, JitsiMeetJS */
import {
toggleDialog
} from '../../react/features/base/dialog';
import { SpeakerStats } from '../../react/features/speaker-stats';
/**
* The reference to the shortcut dialogs when opened.
*/
@ -29,6 +34,12 @@ function initGlobalShortcuts() {
});
KeyboardShortcut._addShortcutToHelp("SPACE","keyboardShortcuts.pushToTalk");
KeyboardShortcut.registerShortcut("T", null, () => {
APP.store.dispatch(toggleDialog(SpeakerStats, {
conference: APP.conference
}));
}, "keyboardShortcuts.showSpeakerStats");
/**
* FIXME: Currently focus keys are directly implemented below in onkeyup.
* They should be moved to the SmallVideo instead.

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