Modal dialog for displaying dominant speaker times
This commit is contained in:
parent
59a74153dc
commit
989161159d
|
@ -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.
|
||||
*/
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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));
|
||||
}
|
||||
};
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
|
@ -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);
|
|
@ -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;
|
|
@ -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);
|
|
@ -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);
|
|
@ -0,0 +1 @@
|
|||
export { default as SpeakerStats } from './SpeakerStats';
|
|
@ -0,0 +1 @@
|
|||
export * from './components';
|
Loading…
Reference in New Issue