feat: Additional setting to order participants in speaker stats (#9751)
* Additional setting to order participants in speaker stats #9742 * Setting to order speaker stats optimisations #9742 * Lint fixes #9742 * Replace APP references #9742 * Lint fixes #9742 * Setting to order speaker stats optimisations 2 #9742 * Lint fixes #9742 * Remove unnecessary param #9742 * Add more speaker-stats reducer _updateStats docs #9742
This commit is contained in:
parent
db473dfef5
commit
5e152b4a42
|
@ -150,6 +150,13 @@ var config = {
|
||||||
// Specifies whether there will be a search field in speaker stats or not
|
// Specifies whether there will be a search field in speaker stats or not
|
||||||
// disableSpeakerStatsSearch: false,
|
// disableSpeakerStatsSearch: false,
|
||||||
|
|
||||||
|
// Specifies whether participants in speaker stats should be ordered or not, and with what priority
|
||||||
|
// speakerStatsOrder: [
|
||||||
|
// 'role', <- Moderators on top
|
||||||
|
// 'name', <- Alphabetically by name
|
||||||
|
// 'hasLeft', <- The ones that have left in the bottom
|
||||||
|
// ] <- the order of the array elements determines priority
|
||||||
|
|
||||||
// How many participants while in the tile view mode, before the receiving video quality is reduced from HD to SD.
|
// How many participants while in the tile view mode, before the receiving video quality is reduced from HD to SD.
|
||||||
// Use -1 to disable.
|
// Use -1 to disable.
|
||||||
// maxFullResolutionParticipants: 2,
|
// maxFullResolutionParticipants: 2,
|
||||||
|
|
|
@ -42,6 +42,7 @@ import '../recording/middleware';
|
||||||
import '../rejoin/middleware';
|
import '../rejoin/middleware';
|
||||||
import '../room-lock/middleware';
|
import '../room-lock/middleware';
|
||||||
import '../rtcstats/middleware';
|
import '../rtcstats/middleware';
|
||||||
|
import '../speaker-stats/middleware';
|
||||||
import '../subtitles/middleware';
|
import '../subtitles/middleware';
|
||||||
import '../toolbox/middleware';
|
import '../toolbox/middleware';
|
||||||
import '../transcribing/middleware';
|
import '../transcribing/middleware';
|
||||||
|
|
|
@ -46,6 +46,7 @@ import '../reactions/reducer';
|
||||||
import '../recent-list/reducer';
|
import '../recent-list/reducer';
|
||||||
import '../recording/reducer';
|
import '../recording/reducer';
|
||||||
import '../settings/reducer';
|
import '../settings/reducer';
|
||||||
|
import '../speaker-stats/reducer';
|
||||||
import '../subtitles/reducer';
|
import '../subtitles/reducer';
|
||||||
import '../screen-share/reducer';
|
import '../screen-share/reducer';
|
||||||
import '../toolbox/reducer';
|
import '../toolbox/reducer';
|
||||||
|
|
|
@ -106,6 +106,7 @@ export default [
|
||||||
'disableShowMoreStats',
|
'disableShowMoreStats',
|
||||||
'disableRemoveRaisedHandOnFocus',
|
'disableRemoveRaisedHandOnFocus',
|
||||||
'disableSpeakerStatsSearch',
|
'disableSpeakerStatsSearch',
|
||||||
|
'speakerStatsOrder',
|
||||||
'disableSimulcast',
|
'disableSimulcast',
|
||||||
'disableThirdPartyRequests',
|
'disableThirdPartyRequests',
|
||||||
'disableTileView',
|
'disableTileView',
|
||||||
|
|
|
@ -185,3 +185,19 @@ function parseShorthandColor(color) {
|
||||||
|
|
||||||
return [ r, g, b ];
|
return [ r, g, b ];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sorts an object by a sort function, same functionality as array.sort().
|
||||||
|
*
|
||||||
|
* @param {Object} object - The data object.
|
||||||
|
* @param {Function} callback - The sort function.
|
||||||
|
* @returns {void}
|
||||||
|
*/
|
||||||
|
export function objectSort(object: Object, callback: Function) {
|
||||||
|
return Object.entries(object)
|
||||||
|
.sort(([ , a ], [ , b ]) => callback(a, b))
|
||||||
|
.reduce((row, [ key, value ]) => {
|
||||||
|
return { ...row,
|
||||||
|
[key]: value };
|
||||||
|
}, {});
|
||||||
|
}
|
||||||
|
|
|
@ -0,0 +1,39 @@
|
||||||
|
// @flow
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Action type to start search.
|
||||||
|
*
|
||||||
|
* {
|
||||||
|
* type: INIT_SEARCH
|
||||||
|
* }
|
||||||
|
*/
|
||||||
|
export const INIT_SEARCH = 'INIT_SEARCH';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Action type to start stats retrieval.
|
||||||
|
*
|
||||||
|
* {
|
||||||
|
* type: INIT_UPDATE_STATS,
|
||||||
|
* getSpeakerStats: Function
|
||||||
|
* }
|
||||||
|
*/
|
||||||
|
export const INIT_UPDATE_STATS = 'INIT_UPDATE_STATS';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Action type to update stats.
|
||||||
|
*
|
||||||
|
* {
|
||||||
|
* type: UPDATE_STATS,
|
||||||
|
* stats: Object
|
||||||
|
* }
|
||||||
|
*/
|
||||||
|
export const UPDATE_STATS = 'UPDATE_STATS';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Action type to initiate reordering of the stats.
|
||||||
|
*
|
||||||
|
* {
|
||||||
|
* type: INIT_REORDER_STATS
|
||||||
|
* }
|
||||||
|
*/
|
||||||
|
export const INIT_REORDER_STATS = 'INIT_REORDER_STATS';
|
|
@ -0,0 +1,58 @@
|
||||||
|
// @flow
|
||||||
|
|
||||||
|
import {
|
||||||
|
INIT_SEARCH,
|
||||||
|
INIT_UPDATE_STATS,
|
||||||
|
UPDATE_STATS,
|
||||||
|
INIT_REORDER_STATS
|
||||||
|
} from './actionTypes';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Starts a search by criteria.
|
||||||
|
*
|
||||||
|
* @param {string} criteria - The search criteria.
|
||||||
|
* @returns {Object}
|
||||||
|
*/
|
||||||
|
export function initSearch(criteria: string) {
|
||||||
|
return {
|
||||||
|
type: INIT_SEARCH,
|
||||||
|
criteria
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the new stats and triggers update.
|
||||||
|
*
|
||||||
|
* @param {Function} getSpeakerStats - Function to get the speaker stats.
|
||||||
|
* @returns {Object}
|
||||||
|
*/
|
||||||
|
export function initUpdateStats(getSpeakerStats: Function) {
|
||||||
|
return {
|
||||||
|
type: INIT_UPDATE_STATS,
|
||||||
|
getSpeakerStats
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Updates the stats with new stats.
|
||||||
|
*
|
||||||
|
* @param {Object} stats - The new stats.
|
||||||
|
* @returns {Object}
|
||||||
|
*/
|
||||||
|
export function updateStats(stats: Object) {
|
||||||
|
return {
|
||||||
|
type: UPDATE_STATS,
|
||||||
|
stats
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initiates reordering of the stats.
|
||||||
|
*
|
||||||
|
* @returns {Object}
|
||||||
|
*/
|
||||||
|
export function initReorderStats() {
|
||||||
|
return {
|
||||||
|
type: INIT_REORDER_STATS
|
||||||
|
};
|
||||||
|
}
|
|
@ -1,12 +1,16 @@
|
||||||
// @flow
|
// @flow
|
||||||
|
|
||||||
import React, { Component } from 'react';
|
import React, { Component } from 'react';
|
||||||
|
import type { Dispatch } from 'redux';
|
||||||
|
|
||||||
import { Dialog } from '../../base/dialog';
|
import { Dialog } from '../../base/dialog';
|
||||||
import { translate } from '../../base/i18n';
|
import { translate } from '../../base/i18n';
|
||||||
import { getLocalParticipant } from '../../base/participants';
|
import { getLocalParticipant } from '../../base/participants';
|
||||||
import { connect } from '../../base/redux';
|
import { connect } from '../../base/redux';
|
||||||
import { escapeRegexp } from '../../base/util';
|
import { escapeRegexp } from '../../base/util';
|
||||||
|
import { initUpdateStats, initSearch } from '../actions';
|
||||||
|
import { SPEAKER_STATS_RELOAD_INTERVAL } from '../constants';
|
||||||
|
import { getSpeakerStats, getSearchCriteria } from '../functions';
|
||||||
|
|
||||||
import SpeakerStatsItem from './SpeakerStatsItem';
|
import SpeakerStatsItem from './SpeakerStatsItem';
|
||||||
import SpeakerStatsLabels from './SpeakerStatsLabels';
|
import SpeakerStatsLabels from './SpeakerStatsLabels';
|
||||||
|
@ -24,39 +28,38 @@ type Props = {
|
||||||
*/
|
*/
|
||||||
_localDisplayName: string,
|
_localDisplayName: string,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The speaker paricipant stats.
|
||||||
|
*/
|
||||||
|
_stats: Object,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The search criteria.
|
||||||
|
*/
|
||||||
|
_criteria: string,
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The JitsiConference from which stats will be pulled.
|
* The JitsiConference from which stats will be pulled.
|
||||||
*/
|
*/
|
||||||
conference: Object,
|
conference: Object,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Redux store dispatch method.
|
||||||
|
*/
|
||||||
|
dispatch: Dispatch<any>,
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The function to translate human-readable text.
|
* The function to translate human-readable text.
|
||||||
*/
|
*/
|
||||||
t: Function
|
t: Function
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
|
||||||
* The type of the React {@code Component} state of {@link SpeakerStats}.
|
|
||||||
*/
|
|
||||||
type State = {
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The stats summary provided by the JitsiConference.
|
|
||||||
*/
|
|
||||||
stats: Object,
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The search input criteria.
|
|
||||||
*/
|
|
||||||
criteria: string,
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* React component for displaying a list of speaker stats.
|
* React component for displaying a list of speaker stats.
|
||||||
*
|
*
|
||||||
* @extends Component
|
* @extends Component
|
||||||
*/
|
*/
|
||||||
class SpeakerStats extends Component<Props, State> {
|
class SpeakerStats extends Component<Props> {
|
||||||
_updateInterval: IntervalID;
|
_updateInterval: IntervalID;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -68,14 +71,11 @@ class SpeakerStats extends Component<Props, State> {
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
super(props);
|
super(props);
|
||||||
|
|
||||||
this.state = {
|
|
||||||
stats: this._getSpeakerStats(),
|
|
||||||
criteria: ''
|
|
||||||
};
|
|
||||||
|
|
||||||
// Bind event handlers so they are only bound once per instance.
|
// Bind event handlers so they are only bound once per instance.
|
||||||
this._updateStats = this._updateStats.bind(this);
|
this._updateStats = this._updateStats.bind(this);
|
||||||
this._onSearch = this._onSearch.bind(this);
|
this._onSearch = this._onSearch.bind(this);
|
||||||
|
|
||||||
|
this._updateStats();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -84,7 +84,7 @@ class SpeakerStats extends Component<Props, State> {
|
||||||
* @inheritdoc
|
* @inheritdoc
|
||||||
*/
|
*/
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
this._updateInterval = setInterval(this._updateStats, 1000);
|
this._updateInterval = setInterval(() => this._updateStats(), SPEAKER_STATS_RELOAD_INTERVAL);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -104,14 +104,14 @@ class SpeakerStats extends Component<Props, State> {
|
||||||
* @returns {ReactElement}
|
* @returns {ReactElement}
|
||||||
*/
|
*/
|
||||||
render() {
|
render() {
|
||||||
const userIds = Object.keys(this.state.stats);
|
const userIds = Object.keys(this.props._stats);
|
||||||
const items = userIds.map(userId => this._createStatsItem(userId));
|
const items = userIds.map(userId => this._createStatsItem(userId));
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog
|
<Dialog
|
||||||
cancelKey = { 'dialog.close' }
|
cancelKey = 'dialog.close'
|
||||||
submitDisabled = { true }
|
submitDisabled = { true }
|
||||||
titleKey = { 'speakerStats.speakerStats' }>
|
titleKey = 'speakerStats.speakerStats'>
|
||||||
<div className = 'speaker-stats'>
|
<div className = 'speaker-stats'>
|
||||||
<SpeakerStatsSearch onSearch = { this._onSearch } />
|
<SpeakerStatsSearch onSearch = { this._onSearch } />
|
||||||
<SpeakerStatsLabels />
|
<SpeakerStatsLabels />
|
||||||
|
@ -121,32 +121,6 @@ class SpeakerStats extends Component<Props, State> {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Update the internal state with the latest speaker stats.
|
|
||||||
*
|
|
||||||
* @returns {void}
|
|
||||||
* @private
|
|
||||||
*/
|
|
||||||
_getSpeakerStats() {
|
|
||||||
const stats = { ...this.props.conference.getSpeakerStats() };
|
|
||||||
|
|
||||||
if (this.state?.criteria) {
|
|
||||||
const searchRegex = new RegExp(this.state.criteria, 'gi');
|
|
||||||
|
|
||||||
for (const id in stats) {
|
|
||||||
if (stats[id].hasOwnProperty('_isLocalStats')) {
|
|
||||||
const name = stats[id].isLocalStats() ? this.props._localDisplayName : stats[id].getDisplayName();
|
|
||||||
|
|
||||||
if (!name || !name.match(searchRegex)) {
|
|
||||||
delete stats[id];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return stats;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create a SpeakerStatsItem instance for the passed in user id.
|
* Create a SpeakerStatsItem instance for the passed in user id.
|
||||||
*
|
*
|
||||||
|
@ -156,9 +130,9 @@ class SpeakerStats extends Component<Props, State> {
|
||||||
* @private
|
* @private
|
||||||
*/
|
*/
|
||||||
_createStatsItem(userId) {
|
_createStatsItem(userId) {
|
||||||
const statsModel = this.state.stats[userId];
|
const statsModel = this.props._stats[userId];
|
||||||
|
|
||||||
if (!statsModel) {
|
if (!statsModel || statsModel.hidden) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -177,7 +151,7 @@ class SpeakerStats extends Component<Props, State> {
|
||||||
= displayName ? `${displayName} (${meString})` : meString;
|
= displayName ? `${displayName} (${meString})` : meString;
|
||||||
} else {
|
} else {
|
||||||
displayName
|
displayName
|
||||||
= this.state.stats[userId].getDisplayName()
|
= this.props._stats[userId].getDisplayName()
|
||||||
|| interfaceConfig.DEFAULT_REMOTE_DISPLAY_NAME;
|
|| interfaceConfig.DEFAULT_REMOTE_DISPLAY_NAME;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -201,10 +175,7 @@ class SpeakerStats extends Component<Props, State> {
|
||||||
* @protected
|
* @protected
|
||||||
*/
|
*/
|
||||||
_onSearch(criteria = '') {
|
_onSearch(criteria = '') {
|
||||||
this.setState({
|
this.props.dispatch(initSearch(escapeRegexp(criteria)));
|
||||||
...this.state,
|
|
||||||
criteria: escapeRegexp(criteria)
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
_updateStats: () => void;
|
_updateStats: () => void;
|
||||||
|
@ -216,12 +187,7 @@ class SpeakerStats extends Component<Props, State> {
|
||||||
* @private
|
* @private
|
||||||
*/
|
*/
|
||||||
_updateStats() {
|
_updateStats() {
|
||||||
const stats = this._getSpeakerStats();
|
this.props.dispatch(initUpdateStats(() => this.props.conference.getSpeakerStats()));
|
||||||
|
|
||||||
this.setState({
|
|
||||||
...this.state,
|
|
||||||
stats
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -231,7 +197,9 @@ class SpeakerStats extends Component<Props, State> {
|
||||||
* @param {Object} state - The redux state.
|
* @param {Object} state - The redux state.
|
||||||
* @private
|
* @private
|
||||||
* @returns {{
|
* @returns {{
|
||||||
* _localDisplayName: ?string
|
* _localDisplayName: ?string,
|
||||||
|
* _stats: Object,
|
||||||
|
* _criteria: string,
|
||||||
* }}
|
* }}
|
||||||
*/
|
*/
|
||||||
function _mapStateToProps(state) {
|
function _mapStateToProps(state) {
|
||||||
|
@ -244,7 +212,9 @@ function _mapStateToProps(state) {
|
||||||
* @private
|
* @private
|
||||||
* @type {string|undefined}
|
* @type {string|undefined}
|
||||||
*/
|
*/
|
||||||
_localDisplayName: localParticipant && localParticipant.name
|
_localDisplayName: localParticipant && localParticipant.name,
|
||||||
|
_stats: getSpeakerStats(state),
|
||||||
|
_criteria: getSearchCriteria(state)
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1 @@
|
||||||
|
export const SPEAKER_STATS_RELOAD_INTERVAL = 1000;
|
|
@ -1,5 +1,10 @@
|
||||||
// @flow
|
// @flow
|
||||||
|
|
||||||
|
import _ from 'lodash';
|
||||||
|
|
||||||
|
import { getLocalParticipant, getParticipantById, PARTICIPANT_ROLE } from '../base/participants';
|
||||||
|
import { objectSort } from '../base/util';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Checks if the speaker stats search is disabled.
|
* Checks if the speaker stats search is disabled.
|
||||||
*
|
*
|
||||||
|
@ -9,3 +14,166 @@
|
||||||
export function isSpeakerStatsSearchDisabled(state: Object) {
|
export function isSpeakerStatsSearchDisabled(state: Object) {
|
||||||
return state['features/base/config']?.disableSpeakerStatsSearch;
|
return state['features/base/config']?.disableSpeakerStatsSearch;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets whether participants in speaker stats should be ordered or not, and with what priority.
|
||||||
|
*
|
||||||
|
* @param {*} state - The redux state.
|
||||||
|
* @returns {Array<string>} - The speaker stats order array or an empty array.
|
||||||
|
*/
|
||||||
|
export function getSpeakerStatsOrder(state: Object) {
|
||||||
|
return state['features/base/config']?.speakerStatsOrder ?? [
|
||||||
|
'role',
|
||||||
|
'name',
|
||||||
|
'hasLeft'
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets speaker stats.
|
||||||
|
*
|
||||||
|
* @param {*} state - The redux state.
|
||||||
|
* @returns {Object} - The speaker stats.
|
||||||
|
*/
|
||||||
|
export function getSpeakerStats(state: Object) {
|
||||||
|
return state['features/speaker-stats']?.stats ?? {};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets speaker stats search criteria.
|
||||||
|
*
|
||||||
|
* @param {*} state - The redux state.
|
||||||
|
* @returns {string} - The search criteria.
|
||||||
|
*/
|
||||||
|
export function getSearchCriteria(state: Object) {
|
||||||
|
return state['features/speaker-stats']?.criteria ?? '';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets if speaker stats reorder is pending.
|
||||||
|
*
|
||||||
|
* @param {*} state - The redux state.
|
||||||
|
* @returns {boolean} - The pending reorder flag.
|
||||||
|
*/
|
||||||
|
export function getPendingReorder(state: Object) {
|
||||||
|
return state['features/speaker-stats']?.pendingReorder ?? false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get sorted speaker stats based on a configuration setting.
|
||||||
|
*
|
||||||
|
* @param {Object} state - The redux state.
|
||||||
|
* @param {Object} stats - The current speaker stats.
|
||||||
|
* @returns {Object} - Ordered speaker stats.
|
||||||
|
* @public
|
||||||
|
*/
|
||||||
|
export function getSortedSpeakerStats(state: Object, stats: Object) {
|
||||||
|
const orderConfig = getSpeakerStatsOrder(state);
|
||||||
|
|
||||||
|
if (orderConfig) {
|
||||||
|
const enhancedStats = getEnhancedStatsForOrdering(state, stats, orderConfig);
|
||||||
|
const sortedStats = objectSort(enhancedStats, (currentParticipant, nextParticipant) => {
|
||||||
|
if (orderConfig.includes('hasLeft')) {
|
||||||
|
if (nextParticipant.hasLeft() && !currentParticipant.hasLeft()) {
|
||||||
|
return -1;
|
||||||
|
} else if (currentParticipant.hasLeft() && !nextParticipant.hasLeft()) {
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let result;
|
||||||
|
|
||||||
|
for (const sortCriteria of orderConfig) {
|
||||||
|
switch (sortCriteria) {
|
||||||
|
case 'role':
|
||||||
|
if (!nextParticipant.isModerator && currentParticipant.isModerator) {
|
||||||
|
result = -1;
|
||||||
|
} else if (!currentParticipant.isModerator && nextParticipant.isModerator) {
|
||||||
|
result = 1;
|
||||||
|
} else {
|
||||||
|
result = 0;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case 'name':
|
||||||
|
result = (currentParticipant.displayName || '').localeCompare(
|
||||||
|
nextParticipant.displayName || ''
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (result !== 0) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
});
|
||||||
|
|
||||||
|
return sortedStats;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Enhance speaker stats to include data needed for ordering.
|
||||||
|
*
|
||||||
|
* @param {Object} state - The redux state.
|
||||||
|
* @param {Object} stats - Speaker stats.
|
||||||
|
* @param {Array<string>} orderConfig - Ordering configuration.
|
||||||
|
* @returns {Object} - Enhanced speaker stats.
|
||||||
|
* @public
|
||||||
|
*/
|
||||||
|
function getEnhancedStatsForOrdering(state, stats, orderConfig) {
|
||||||
|
if (!orderConfig) {
|
||||||
|
return stats;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const id in stats) {
|
||||||
|
if (stats[id].hasOwnProperty('_hasLeft') && !stats[id].hasLeft()) {
|
||||||
|
if (orderConfig.includes('name')) {
|
||||||
|
const localParticipant = getLocalParticipant(state);
|
||||||
|
|
||||||
|
if (stats[id].isLocalStats()) {
|
||||||
|
stats[id].setDisplayName(localParticipant.name);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (orderConfig.includes('role')) {
|
||||||
|
const participant = getParticipantById(state, stats[id].getUserId());
|
||||||
|
|
||||||
|
stats[id].isModerator = participant && participant.role === PARTICIPANT_ROLE.MODERATOR;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return stats;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Filter stats by search criteria.
|
||||||
|
*
|
||||||
|
* @param {Object} state - The redux state.
|
||||||
|
* @param {Object | undefined} stats - The unfiltered stats.
|
||||||
|
*
|
||||||
|
* @returns {Object} - Filtered speaker stats.
|
||||||
|
* @public
|
||||||
|
*/
|
||||||
|
export function filterBySearchCriteria(state: Object, stats: ?Object) {
|
||||||
|
const filteredStats = _.cloneDeep(stats ?? getSpeakerStats(state));
|
||||||
|
const criteria = getSearchCriteria(state);
|
||||||
|
|
||||||
|
if (criteria) {
|
||||||
|
const searchRegex = new RegExp(criteria, 'gi');
|
||||||
|
|
||||||
|
for (const id in filteredStats) {
|
||||||
|
if (filteredStats[id].hasOwnProperty('_isLocalStats')) {
|
||||||
|
const name = filteredStats[id].getDisplayName();
|
||||||
|
|
||||||
|
if (!name || !name.match(searchRegex)) {
|
||||||
|
filteredStats[id].hidden = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return filteredStats;
|
||||||
|
}
|
||||||
|
|
|
@ -0,0 +1,49 @@
|
||||||
|
// @flow
|
||||||
|
|
||||||
|
import {
|
||||||
|
PARTICIPANT_JOINED,
|
||||||
|
PARTICIPANT_KICKED,
|
||||||
|
PARTICIPANT_LEFT,
|
||||||
|
PARTICIPANT_UPDATED
|
||||||
|
} from '../base/participants/actionTypes';
|
||||||
|
import { MiddlewareRegistry } from '../base/redux';
|
||||||
|
|
||||||
|
import { INIT_SEARCH, INIT_UPDATE_STATS } from './actionTypes';
|
||||||
|
import { initReorderStats, updateStats } from './actions';
|
||||||
|
import { filterBySearchCriteria, getSortedSpeakerStats, getPendingReorder } from './functions';
|
||||||
|
|
||||||
|
MiddlewareRegistry.register(({ dispatch, getState }) => next => action => {
|
||||||
|
const result = next(action);
|
||||||
|
|
||||||
|
switch (action.type) {
|
||||||
|
|
||||||
|
case INIT_SEARCH: {
|
||||||
|
const state = getState();
|
||||||
|
const stats = filterBySearchCriteria(state);
|
||||||
|
|
||||||
|
dispatch(updateStats(stats));
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
case INIT_UPDATE_STATS:
|
||||||
|
if (action.getSpeakerStats) {
|
||||||
|
const state = getState();
|
||||||
|
const speakerStats = { ...action.getSpeakerStats() };
|
||||||
|
const stats = filterBySearchCriteria(state, speakerStats);
|
||||||
|
const pendingReorder = getPendingReorder(state);
|
||||||
|
|
||||||
|
dispatch(updateStats(pendingReorder ? getSortedSpeakerStats(state, stats) : stats));
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case PARTICIPANT_JOINED:
|
||||||
|
case PARTICIPANT_LEFT:
|
||||||
|
case PARTICIPANT_KICKED:
|
||||||
|
case PARTICIPANT_UPDATED: {
|
||||||
|
dispatch(initReorderStats());
|
||||||
|
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
});
|
|
@ -0,0 +1,119 @@
|
||||||
|
// @flow
|
||||||
|
|
||||||
|
import _ from 'lodash';
|
||||||
|
|
||||||
|
import { ReducerRegistry } from '../base/redux';
|
||||||
|
|
||||||
|
import {
|
||||||
|
INIT_SEARCH,
|
||||||
|
UPDATE_STATS,
|
||||||
|
INIT_REORDER_STATS
|
||||||
|
} from './actionTypes';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The initial state of the feature speaker-stats.
|
||||||
|
*
|
||||||
|
* @type {Object}
|
||||||
|
*/
|
||||||
|
const INITIAL_STATE = {
|
||||||
|
stats: {},
|
||||||
|
pendingReorder: true,
|
||||||
|
criteria: ''
|
||||||
|
};
|
||||||
|
|
||||||
|
ReducerRegistry.register('features/speaker-stats', (state = _getInitialState(), action) => {
|
||||||
|
switch (action.type) {
|
||||||
|
case INIT_SEARCH:
|
||||||
|
return _updateCriteria(state, action);
|
||||||
|
case UPDATE_STATS:
|
||||||
|
return _updateStats(state, action);
|
||||||
|
case INIT_REORDER_STATS:
|
||||||
|
return _initReorderStats(state);
|
||||||
|
}
|
||||||
|
|
||||||
|
return state;
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the initial state of the feature speaker-stats.
|
||||||
|
*
|
||||||
|
* @returns {Object}
|
||||||
|
*/
|
||||||
|
function _getInitialState() {
|
||||||
|
return INITIAL_STATE;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reduces a specific Redux action INIT_SEARCH of the feature
|
||||||
|
* speaker-stats.
|
||||||
|
*
|
||||||
|
* @param {Object} state - The Redux state of the feature speaker-stats.
|
||||||
|
* @param {Action} action - The Redux action INIT_SEARCH to reduce.
|
||||||
|
* @private
|
||||||
|
* @returns {Object} The new state after the reduction of the specified action.
|
||||||
|
*/
|
||||||
|
function _updateCriteria(state, { criteria }) {
|
||||||
|
return _.assign(
|
||||||
|
{},
|
||||||
|
state,
|
||||||
|
{ criteria },
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reduces a specific Redux action UPDATE_STATS of the feature
|
||||||
|
* speaker-stats.
|
||||||
|
* The speaker stats order is based on the stats object properties.
|
||||||
|
* When updating without reordering, the new stats object properties are reordered
|
||||||
|
* as the last in state, otherwise the order would be lost on each update.
|
||||||
|
* If there was already a pending reorder, the stats object properties already have
|
||||||
|
* the correct order, so the property order is not changing.
|
||||||
|
*
|
||||||
|
* @param {Object} state - The Redux state of the feature speaker-stats.
|
||||||
|
* @param {Action} action - The Redux action UPDATE_STATS to reduce.
|
||||||
|
* @private
|
||||||
|
* @returns {Object} - The new state after the reduction of the specified action.
|
||||||
|
*/
|
||||||
|
function _updateStats(state, { stats }) {
|
||||||
|
const finalStats = state.pendingReorder ? stats : state.stats;
|
||||||
|
|
||||||
|
if (!state.pendingReorder) {
|
||||||
|
// Avoid reordering the speaker stats object properties
|
||||||
|
const finalKeys = Object.keys(stats);
|
||||||
|
|
||||||
|
finalKeys.forEach(newStatId => {
|
||||||
|
finalStats[newStatId] = _.clone(stats[newStatId]);
|
||||||
|
});
|
||||||
|
|
||||||
|
Object.keys(finalStats).forEach(key => {
|
||||||
|
if (!finalKeys.includes(key)) {
|
||||||
|
delete finalStats[key];
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return _.assign(
|
||||||
|
{},
|
||||||
|
state,
|
||||||
|
{
|
||||||
|
stats: finalStats,
|
||||||
|
pendingReorder: false
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reduces a specific Redux action INIT_REORDER_STATS of the feature
|
||||||
|
* speaker-stats.
|
||||||
|
*
|
||||||
|
* @param {Object} state - The Redux state of the feature speaker-stats.
|
||||||
|
* @private
|
||||||
|
* @returns {Object} The new state after the reduction of the specified action.
|
||||||
|
*/
|
||||||
|
function _initReorderStats(state) {
|
||||||
|
return _.assign(
|
||||||
|
{},
|
||||||
|
state,
|
||||||
|
{ pendingReorder: true },
|
||||||
|
);
|
||||||
|
}
|
Loading…
Reference in New Issue