diff --git a/react/features/speaker-stats/actionTypes.ts b/react/features/speaker-stats/actionTypes.ts index 06dfb54e2..369bb83e9 100644 --- a/react/features/speaker-stats/actionTypes.ts +++ b/react/features/speaker-stats/actionTypes.ts @@ -29,6 +29,14 @@ export const INIT_UPDATE_STATS = 'INIT_UPDATE_STATS'; */ export const UPDATE_STATS = 'UPDATE_STATS'; +/** + * Action type to update the speaker stats order. + * { + * type: UPDATE_SORTED_SPEAKER_STATS_IDS + * } + */ +export const UPDATE_SORTED_SPEAKER_STATS_IDS = 'UPDATE_SORTED_SPEAKER_STATS_IDS' + /** * Action type to initiate reordering of the stats. * diff --git a/react/features/speaker-stats/actions.any.js b/react/features/speaker-stats/actions.any.js index 8ea7c2890..807febacd 100644 --- a/react/features/speaker-stats/actions.any.js +++ b/react/features/speaker-stats/actions.any.js @@ -6,6 +6,7 @@ import { INIT_UPDATE_STATS, RESET_SEARCH_CRITERIA, TOGGLE_FACE_EXPRESSIONS, + UPDATE_SORTED_SPEAKER_STATS_IDS, UPDATE_STATS } from './actionTypes'; @@ -48,6 +49,19 @@ export function updateStats(stats: Object) { }; } +/** + * Updates the speaker stats order. + * + * @param {Object} participantIds - Participant ids. + * @returns {Object} + */ +export function updateSortedSpeakerStatsIds(participantIds: Array) { + return { + type: UPDATE_SORTED_SPEAKER_STATS_IDS, + participantIds + }; +} + /** * Initiates reordering of the stats. * diff --git a/react/features/speaker-stats/components/AbstractSpeakerStatsList.js b/react/features/speaker-stats/components/AbstractSpeakerStatsList.js index 099ebc930..6a0c2d8fb 100644 --- a/react/features/speaker-stats/components/AbstractSpeakerStatsList.js +++ b/react/features/speaker-stats/components/AbstractSpeakerStatsList.js @@ -21,7 +21,11 @@ const abstractSpeakerStatsList = (speakerStatsItem: Function, itemStyles?: Objec const dispatch = useDispatch(); const { t } = useTranslation(); const conference = useSelector(state => state['features/base/conference'].conference); - const { stats: speakerStats, showFaceExpressions } = useSelector(state => state['features/speaker-stats']); + const { + stats: speakerStats, + showFaceExpressions, + sortedSpeakerStatsIds + } = useSelector(state => state['features/speaker-stats']); const localParticipant = useSelector(getLocalParticipant); const { defaultRemoteDisplayName } = useSelector( state => state['features/base/config']) || {}; @@ -80,7 +84,10 @@ const abstractSpeakerStatsList = (speakerStatsItem: Function, itemStyles?: Objec }, [ faceExpressions ]); const localSpeakerStats = Object.keys(speakerStats).length === 0 ? getSpeakerStats() : speakerStats; - const userIds = Object.keys(localSpeakerStats).filter(id => localSpeakerStats[id] && !localSpeakerStats[id].hidden); + const localSortedSpeakerStatsIds + = sortedSpeakerStatsIds.length === 0 ? Object.keys(localSpeakerStats) : sortedSpeakerStatsIds; + + const userIds = localSortedSpeakerStatsIds.filter(id => localSpeakerStats[id] && !localSpeakerStats[id].hidden); return userIds.map(userId => { const statsModel = localSpeakerStats[userId]; diff --git a/react/features/speaker-stats/functions.js b/react/features/speaker-stats/functions.js index 69089baa0..651dc75d5 100644 --- a/react/features/speaker-stats/functions.js +++ b/react/features/speaker-stats/functions.js @@ -6,7 +6,6 @@ import { PARTICIPANT_ROLE, getParticipantById } from '../base/participants'; -import { objectSort } from '../base/util'; /** * Checks if the speaker stats search is disabled. @@ -63,56 +62,67 @@ export function getPendingReorder(state: Object) { } /** - * Get sorted speaker stats based on a configuration setting. + * Get sorted speaker stats ids based on a configuration setting. * * @param {Object} state - The redux state. * @param {Object} stats - The current speaker stats. - * @returns {Object} - Ordered speaker stats. + * @returns {Object} - Ordered speaker stats ids. * @public */ -export function getSortedSpeakerStats(state: Object, stats: Object) { +export function getSortedSpeakerStatsIds(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; + + return Object.entries(enhancedStats) + .sort(([ , a ], [ , b ]) => compareFn(a, b)) + .map(el => el[0]); + } + + /** + * + * Compares the order of two participants in the speaker stats list. + * + * @param {Object} currentParticipant - The first participant for comparison. + * @param {Object} nextParticipant - The second participant for comparison. + * @returns {number} - The sort order of the two participants. + */ + function compareFn(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; } - 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; - } + if (result !== 0) { + break; } + } - return result; - }); - - return sortedStats; + return result; } } diff --git a/react/features/speaker-stats/middleware.js b/react/features/speaker-stats/middleware.js index c58092954..b61d71123 100644 --- a/react/features/speaker-stats/middleware.js +++ b/react/features/speaker-stats/middleware.js @@ -13,8 +13,8 @@ import { INIT_UPDATE_STATS, RESET_SEARCH_CRITERIA } from './actionTypes'; -import { initReorderStats, updateStats } from './actions'; -import { filterBySearchCriteria, getPendingReorder, getSortedSpeakerStats, resetHiddenStats } from './functions'; +import { initReorderStats, updateSortedSpeakerStatsIds, updateStats } from './actions'; +import { filterBySearchCriteria, getPendingReorder, getSortedSpeakerStatsIds, resetHiddenStats } from './functions'; MiddlewareRegistry.register(({ dispatch, getState }) => next => action => { const result = next(action); @@ -35,8 +35,13 @@ MiddlewareRegistry.register(({ dispatch, getState }) => next => action => { const stats = filterBySearchCriteria(state, speakerStats); const pendingReorder = getPendingReorder(state); - dispatch(updateStats(pendingReorder ? getSortedSpeakerStats(state, stats) : stats)); + if (pendingReorder) { + dispatch(updateSortedSpeakerStatsIds(getSortedSpeakerStatsIds(state, stats))); + } + + dispatch(updateStats(stats)); } + break; case RESET_SEARCH_CRITERIA: { diff --git a/react/features/speaker-stats/reducer.ts b/react/features/speaker-stats/reducer.ts index 4d69b352a..ca95196d4 100644 --- a/react/features/speaker-stats/reducer.ts +++ b/react/features/speaker-stats/reducer.ts @@ -7,6 +7,7 @@ import { INIT_SEARCH, RESET_SEARCH_CRITERIA, TOGGLE_FACE_EXPRESSIONS, + UPDATE_SORTED_SPEAKER_STATS_IDS, UPDATE_STATS } from './actionTypes'; @@ -20,7 +21,8 @@ const INITIAL_STATE = { isOpen: false, pendingReorder: true, criteria: null, - showFaceExpressions: false + showFaceExpressions: false, + sortedSpeakerStatsIds: [] }; export interface ISpeakerStatsState { @@ -28,6 +30,7 @@ export interface ISpeakerStatsState { isOpen: boolean; pendingReorder: boolean; showFaceExpressions: boolean; + sortedSpeakerStatsIds: Array; stats: Object; } @@ -40,6 +43,8 @@ ReducerRegistry.register('features/speaker-stats', return _updateStats(state, action); case INIT_REORDER_STATS: return _initReorderStats(state); + case UPDATE_SORTED_SPEAKER_STATS_IDS: + return _updateSortedSpeakerStats(state, action); case RESET_SEARCH_CRITERIA: return _updateCriteria(state, { criteria: null }); case TOGGLE_FACE_EXPRESSIONS: { @@ -71,13 +76,7 @@ function _updateCriteria(state: ISpeakerStatsState, { criteria }: { criteria: st } /** - * 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. + * Reduces a specific Redux action UPDATE_STATS of the feature speaker-stats. * * @param {Object} state - The Redux state of the feature speaker-stats. * @param {Action} action - The Redux action UPDATE_STATS to reduce. @@ -85,31 +84,26 @@ function _updateCriteria(state: ISpeakerStatsState, { criteria }: { criteria: st * @returns {Object} - The new state after the reduction of the specified action. */ function _updateStats(state: ISpeakerStatsState, { stats }: { stats: any; }) { - const finalStats = state.pendingReorder ? stats : state.stats; + return { + ...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 UPDATE_SORTED_SPEAKER_STATS_IDS of the feature speaker-stats. + * + * @param {Object} state - The Redux state of the feature speaker-stats. + * @param {Action} action - The Redux action UPDATE_SORTED_SPEAKER_STATS_IDS to reduce. + * @private + * @returns {Object} The new state after the reduction of the specified action. + */ +function _updateSortedSpeakerStats(state: ISpeakerStatsState, { participantIds }: { participantIds: Array; }) { + return { + ...state, + sortedSpeakerStatsIds: participantIds, + pendingReorder: false + }; } /**