瀏覽代碼

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
master
dimitardelchev93 3 年之前
父節點
當前提交
5e152b4a42
沒有連結到貢獻者的電子郵件帳戶。

+ 7
- 0
config.js 查看文件

@@ -150,6 +150,13 @@ var config = {
150 150
     // Specifies whether there will be a search field in speaker stats or not
151 151
     // disableSpeakerStatsSearch: false,
152 152
 
153
+    // Specifies whether participants in speaker stats should be ordered or not, and with what priority
154
+    // speakerStatsOrder: [
155
+    //  'role', <- Moderators on top
156
+    //  'name', <- Alphabetically by name
157
+    //  'hasLeft', <- The ones that have left in the bottom
158
+    // ] <- the order of the array elements determines priority
159
+
153 160
     // How many participants while in the tile view mode, before the receiving video quality is reduced from HD to SD.
154 161
     // Use -1 to disable.
155 162
     // maxFullResolutionParticipants: 2,

+ 1
- 0
react/features/app/middlewares.any.js 查看文件

@@ -42,6 +42,7 @@ import '../recording/middleware';
42 42
 import '../rejoin/middleware';
43 43
 import '../room-lock/middleware';
44 44
 import '../rtcstats/middleware';
45
+import '../speaker-stats/middleware';
45 46
 import '../subtitles/middleware';
46 47
 import '../toolbox/middleware';
47 48
 import '../transcribing/middleware';

+ 1
- 0
react/features/app/reducers.any.js 查看文件

@@ -46,6 +46,7 @@ import '../reactions/reducer';
46 46
 import '../recent-list/reducer';
47 47
 import '../recording/reducer';
48 48
 import '../settings/reducer';
49
+import '../speaker-stats/reducer';
49 50
 import '../subtitles/reducer';
50 51
 import '../screen-share/reducer';
51 52
 import '../toolbox/reducer';

+ 1
- 0
react/features/base/config/configWhitelist.js 查看文件

@@ -106,6 +106,7 @@ export default [
106 106
     'disableShowMoreStats',
107 107
     'disableRemoveRaisedHandOnFocus',
108 108
     'disableSpeakerStatsSearch',
109
+    'speakerStatsOrder',
109 110
     'disableSimulcast',
110 111
     'disableThirdPartyRequests',
111 112
     'disableTileView',

+ 16
- 0
react/features/base/util/helpers.js 查看文件

@@ -185,3 +185,19 @@ function parseShorthandColor(color) {
185 185
 
186 186
     return [ r, g, b ];
187 187
 }
188
+
189
+/**
190
+ * Sorts an object by a sort function, same functionality as array.sort().
191
+ *
192
+ * @param {Object} object - The data object.
193
+ * @param {Function} callback - The sort function.
194
+ * @returns {void}
195
+ */
196
+export function objectSort(object: Object, callback: Function) {
197
+    return Object.entries(object)
198
+        .sort(([ , a ], [ , b ]) => callback(a, b))
199
+        .reduce((row, [ key, value ]) => {
200
+            return { ...row,
201
+                [key]: value };
202
+        }, {});
203
+}

+ 39
- 0
react/features/speaker-stats/actionTypes.js 查看文件

@@ -0,0 +1,39 @@
1
+// @flow
2
+
3
+/**
4
+ * Action type to start search.
5
+ *
6
+ * {
7
+ *     type: INIT_SEARCH
8
+ * }
9
+ */
10
+export const INIT_SEARCH = 'INIT_SEARCH';
11
+
12
+/**
13
+ * Action type to start stats retrieval.
14
+ *
15
+ * {
16
+ *     type: INIT_UPDATE_STATS,
17
+ *     getSpeakerStats: Function
18
+ * }
19
+ */
20
+export const INIT_UPDATE_STATS = 'INIT_UPDATE_STATS';
21
+
22
+/**
23
+ * Action type to update stats.
24
+ *
25
+ * {
26
+ *     type: UPDATE_STATS,
27
+ *     stats: Object
28
+ * }
29
+ */
30
+export const UPDATE_STATS = 'UPDATE_STATS';
31
+
32
+/**
33
+ * Action type to initiate reordering of the stats.
34
+ *
35
+ * {
36
+ *     type: INIT_REORDER_STATS
37
+ * }
38
+ */
39
+export const INIT_REORDER_STATS = 'INIT_REORDER_STATS';

+ 58
- 0
react/features/speaker-stats/actions.js 查看文件

@@ -0,0 +1,58 @@
1
+// @flow
2
+
3
+import {
4
+    INIT_SEARCH,
5
+    INIT_UPDATE_STATS,
6
+    UPDATE_STATS,
7
+    INIT_REORDER_STATS
8
+} from './actionTypes';
9
+
10
+/**
11
+ * Starts a search by criteria.
12
+ *
13
+ * @param {string} criteria - The search criteria.
14
+ * @returns {Object}
15
+ */
16
+export function initSearch(criteria: string) {
17
+    return {
18
+        type: INIT_SEARCH,
19
+        criteria
20
+    };
21
+}
22
+
23
+/**
24
+ * Gets the new stats and triggers update.
25
+ *
26
+ * @param {Function} getSpeakerStats - Function to get the speaker stats.
27
+ * @returns {Object}
28
+ */
29
+export function initUpdateStats(getSpeakerStats: Function) {
30
+    return {
31
+        type: INIT_UPDATE_STATS,
32
+        getSpeakerStats
33
+    };
34
+}
35
+
36
+/**
37
+ * Updates the stats with new stats.
38
+ *
39
+ * @param {Object} stats - The new stats.
40
+ * @returns {Object}
41
+ */
42
+export function updateStats(stats: Object) {
43
+    return {
44
+        type: UPDATE_STATS,
45
+        stats
46
+    };
47
+}
48
+
49
+/**
50
+ * Initiates reordering of the stats.
51
+ *
52
+ * @returns {Object}
53
+ */
54
+export function initReorderStats() {
55
+    return {
56
+        type: INIT_REORDER_STATS
57
+    };
58
+}

+ 34
- 64
react/features/speaker-stats/components/SpeakerStats.js 查看文件

@@ -1,12 +1,16 @@
1 1
 // @flow
2 2
 
3 3
 import React, { Component } from 'react';
4
+import type { Dispatch } from 'redux';
4 5
 
5 6
 import { Dialog } from '../../base/dialog';
6 7
 import { translate } from '../../base/i18n';
7 8
 import { getLocalParticipant } from '../../base/participants';
8 9
 import { connect } from '../../base/redux';
9 10
 import { escapeRegexp } from '../../base/util';
11
+import { initUpdateStats, initSearch } from '../actions';
12
+import { SPEAKER_STATS_RELOAD_INTERVAL } from '../constants';
13
+import { getSpeakerStats, getSearchCriteria } from '../functions';
10 14
 
11 15
 import SpeakerStatsItem from './SpeakerStatsItem';
12 16
 import SpeakerStatsLabels from './SpeakerStatsLabels';
@@ -25,30 +29,29 @@ type Props = {
25 29
     _localDisplayName: string,
26 30
 
27 31
     /**
28
-     * The JitsiConference from which stats will be pulled.
32
+     * The speaker paricipant stats.
29 33
      */
30
-    conference: Object,
34
+    _stats: Object,
31 35
 
32 36
     /**
33
-     * The function to translate human-readable text.
37
+     * The search criteria.
34 38
      */
35
-    t: Function
36
-};
39
+    _criteria: string,
37 40
 
38
-/**
39
- * The type of the React {@code Component} state of {@link SpeakerStats}.
40
- */
41
-type State = {
41
+    /**
42
+     * The JitsiConference from which stats will be pulled.
43
+     */
44
+    conference: Object,
42 45
 
43 46
     /**
44
-     * The stats summary provided by the JitsiConference.
47
+     * Redux store dispatch method.
45 48
      */
46
-    stats: Object,
49
+    dispatch: Dispatch<any>,
47 50
 
48 51
     /**
49
-     * The search input criteria.
52
+     * The function to translate human-readable text.
50 53
      */
51
-    criteria: string,
54
+    t: Function
52 55
 };
53 56
 
54 57
 /**
@@ -56,7 +59,7 @@ type State = {
56 59
  *
57 60
  * @extends Component
58 61
  */
59
-class SpeakerStats extends Component<Props, State> {
62
+class SpeakerStats extends Component<Props> {
60 63
     _updateInterval: IntervalID;
61 64
 
62 65
     /**
@@ -68,14 +71,11 @@ class SpeakerStats extends Component<Props, State> {
68 71
     constructor(props) {
69 72
         super(props);
70 73
 
71
-        this.state = {
72
-            stats: this._getSpeakerStats(),
73
-            criteria: ''
74
-        };
75
-
76 74
         // Bind event handlers so they are only bound once per instance.
77 75
         this._updateStats = this._updateStats.bind(this);
78 76
         this._onSearch = this._onSearch.bind(this);
77
+
78
+        this._updateStats();
79 79
     }
80 80
 
81 81
     /**
@@ -84,7 +84,7 @@ class SpeakerStats extends Component<Props, State> {
84 84
      * @inheritdoc
85 85
      */
86 86
     componentDidMount() {
87
-        this._updateInterval = setInterval(this._updateStats, 1000);
87
+        this._updateInterval = setInterval(() => this._updateStats(), SPEAKER_STATS_RELOAD_INTERVAL);
88 88
     }
89 89
 
90 90
     /**
@@ -104,14 +104,14 @@ class SpeakerStats extends Component<Props, State> {
104 104
      * @returns {ReactElement}
105 105
      */
106 106
     render() {
107
-        const userIds = Object.keys(this.state.stats);
107
+        const userIds = Object.keys(this.props._stats);
108 108
         const items = userIds.map(userId => this._createStatsItem(userId));
109 109
 
110 110
         return (
111 111
             <Dialog
112
-                cancelKey = { 'dialog.close' }
112
+                cancelKey = 'dialog.close'
113 113
                 submitDisabled = { true }
114
-                titleKey = { 'speakerStats.speakerStats' }>
114
+                titleKey = 'speakerStats.speakerStats'>
115 115
                 <div className = 'speaker-stats'>
116 116
                     <SpeakerStatsSearch onSearch = { this._onSearch } />
117 117
                     <SpeakerStatsLabels />
@@ -121,32 +121,6 @@ class SpeakerStats extends Component<Props, State> {
121 121
         );
122 122
     }
123 123
 
124
-    /**
125
-     * Update the internal state with the latest speaker stats.
126
-     *
127
-     * @returns {void}
128
-     * @private
129
-     */
130
-    _getSpeakerStats() {
131
-        const stats = { ...this.props.conference.getSpeakerStats() };
132
-
133
-        if (this.state?.criteria) {
134
-            const searchRegex = new RegExp(this.state.criteria, 'gi');
135
-
136
-            for (const id in stats) {
137
-                if (stats[id].hasOwnProperty('_isLocalStats')) {
138
-                    const name = stats[id].isLocalStats() ? this.props._localDisplayName : stats[id].getDisplayName();
139
-
140
-                    if (!name || !name.match(searchRegex)) {
141
-                        delete stats[id];
142
-                    }
143
-                }
144
-            }
145
-        }
146
-
147
-        return stats;
148
-    }
149
-
150 124
     /**
151 125
      * Create a SpeakerStatsItem instance for the passed in user id.
152 126
      *
@@ -156,9 +130,9 @@ class SpeakerStats extends Component<Props, State> {
156 130
      * @private
157 131
      */
158 132
     _createStatsItem(userId) {
159
-        const statsModel = this.state.stats[userId];
133
+        const statsModel = this.props._stats[userId];
160 134
 
161
-        if (!statsModel) {
135
+        if (!statsModel || statsModel.hidden) {
162 136
             return null;
163 137
         }
164 138
 
@@ -177,7 +151,7 @@ class SpeakerStats extends Component<Props, State> {
177 151
                 = displayName ? `${displayName} (${meString})` : meString;
178 152
         } else {
179 153
             displayName
180
-                = this.state.stats[userId].getDisplayName()
154
+                = this.props._stats[userId].getDisplayName()
181 155
                     || interfaceConfig.DEFAULT_REMOTE_DISPLAY_NAME;
182 156
         }
183 157
 
@@ -201,10 +175,7 @@ class SpeakerStats extends Component<Props, State> {
201 175
      * @protected
202 176
      */
203 177
     _onSearch(criteria = '') {
204
-        this.setState({
205
-            ...this.state,
206
-            criteria: escapeRegexp(criteria)
207
-        });
178
+        this.props.dispatch(initSearch(escapeRegexp(criteria)));
208 179
     }
209 180
 
210 181
     _updateStats: () => void;
@@ -216,12 +187,7 @@ class SpeakerStats extends Component<Props, State> {
216 187
      * @private
217 188
      */
218 189
     _updateStats() {
219
-        const stats = this._getSpeakerStats();
220
-
221
-        this.setState({
222
-            ...this.state,
223
-            stats
224
-        });
190
+        this.props.dispatch(initUpdateStats(() => this.props.conference.getSpeakerStats()));
225 191
     }
226 192
 }
227 193
 
@@ -231,7 +197,9 @@ class SpeakerStats extends Component<Props, State> {
231 197
  * @param {Object} state - The redux state.
232 198
  * @private
233 199
  * @returns {{
234
- *     _localDisplayName: ?string
200
+ *     _localDisplayName: ?string,
201
+ *     _stats: Object,
202
+ *     _criteria: string,
235 203
  * }}
236 204
  */
237 205
 function _mapStateToProps(state) {
@@ -244,7 +212,9 @@ function _mapStateToProps(state) {
244 212
          * @private
245 213
          * @type {string|undefined}
246 214
          */
247
-        _localDisplayName: localParticipant && localParticipant.name
215
+        _localDisplayName: localParticipant && localParticipant.name,
216
+        _stats: getSpeakerStats(state),
217
+        _criteria: getSearchCriteria(state)
248 218
     };
249 219
 }
250 220
 

+ 1
- 0
react/features/speaker-stats/constants.js 查看文件

@@ -0,0 +1 @@
1
+export const SPEAKER_STATS_RELOAD_INTERVAL = 1000;

+ 168
- 0
react/features/speaker-stats/functions.js 查看文件

@@ -1,5 +1,10 @@
1 1
 // @flow
2 2
 
3
+import _ from 'lodash';
4
+
5
+import { getLocalParticipant, getParticipantById, PARTICIPANT_ROLE } from '../base/participants';
6
+import { objectSort } from '../base/util';
7
+
3 8
 /**
4 9
  * Checks if the speaker stats search is disabled.
5 10
  *
@@ -9,3 +14,166 @@
9 14
 export function isSpeakerStatsSearchDisabled(state: Object) {
10 15
     return state['features/base/config']?.disableSpeakerStatsSearch;
11 16
 }
17
+
18
+/**
19
+ * Gets whether participants in speaker stats should be ordered or not, and with what priority.
20
+ *
21
+ * @param {*} state - The redux state.
22
+ * @returns {Array<string>} - The speaker stats order array or an empty array.
23
+ */
24
+export function getSpeakerStatsOrder(state: Object) {
25
+    return state['features/base/config']?.speakerStatsOrder ?? [
26
+        'role',
27
+        'name',
28
+        'hasLeft'
29
+    ];
30
+}
31
+
32
+/**
33
+ * Gets speaker stats.
34
+ *
35
+ * @param {*} state - The redux state.
36
+ * @returns {Object} - The speaker stats.
37
+ */
38
+export function getSpeakerStats(state: Object) {
39
+    return state['features/speaker-stats']?.stats ?? {};
40
+}
41
+
42
+/**
43
+ * Gets speaker stats search criteria.
44
+ *
45
+ * @param {*} state - The redux state.
46
+ * @returns {string} - The search criteria.
47
+ */
48
+export function getSearchCriteria(state: Object) {
49
+    return state['features/speaker-stats']?.criteria ?? '';
50
+}
51
+
52
+/**
53
+ * Gets if speaker stats reorder is pending.
54
+ *
55
+ * @param {*} state - The redux state.
56
+ * @returns {boolean} - The pending reorder flag.
57
+ */
58
+export function getPendingReorder(state: Object) {
59
+    return state['features/speaker-stats']?.pendingReorder ?? false;
60
+}
61
+
62
+/**
63
+ * Get sorted speaker stats based on a configuration setting.
64
+ *
65
+ * @param {Object} state - The redux state.
66
+ * @param {Object} stats - The current speaker stats.
67
+ * @returns {Object} - Ordered speaker stats.
68
+ * @public
69
+ */
70
+export function getSortedSpeakerStats(state: Object, stats: Object) {
71
+    const orderConfig = getSpeakerStatsOrder(state);
72
+
73
+    if (orderConfig) {
74
+        const enhancedStats = getEnhancedStatsForOrdering(state, stats, orderConfig);
75
+        const sortedStats = objectSort(enhancedStats, (currentParticipant, nextParticipant) => {
76
+            if (orderConfig.includes('hasLeft')) {
77
+                if (nextParticipant.hasLeft() && !currentParticipant.hasLeft()) {
78
+                    return -1;
79
+                } else if (currentParticipant.hasLeft() && !nextParticipant.hasLeft()) {
80
+                    return 1;
81
+                }
82
+            }
83
+
84
+            let result;
85
+
86
+            for (const sortCriteria of orderConfig) {
87
+                switch (sortCriteria) {
88
+                case 'role':
89
+                    if (!nextParticipant.isModerator && currentParticipant.isModerator) {
90
+                        result = -1;
91
+                    } else if (!currentParticipant.isModerator && nextParticipant.isModerator) {
92
+                        result = 1;
93
+                    } else {
94
+                        result = 0;
95
+                    }
96
+                    break;
97
+                case 'name':
98
+                    result = (currentParticipant.displayName || '').localeCompare(
99
+                        nextParticipant.displayName || ''
100
+                    );
101
+                    break;
102
+                }
103
+
104
+                if (result !== 0) {
105
+                    break;
106
+                }
107
+            }
108
+
109
+            return result;
110
+        });
111
+
112
+        return sortedStats;
113
+    }
114
+}
115
+
116
+/**
117
+ * Enhance speaker stats to include data needed for ordering.
118
+ *
119
+ * @param {Object} state - The redux state.
120
+ * @param {Object} stats - Speaker stats.
121
+ * @param {Array<string>} orderConfig - Ordering configuration.
122
+ * @returns {Object} - Enhanced speaker stats.
123
+ * @public
124
+ */
125
+function getEnhancedStatsForOrdering(state, stats, orderConfig) {
126
+    if (!orderConfig) {
127
+        return stats;
128
+    }
129
+
130
+    for (const id in stats) {
131
+        if (stats[id].hasOwnProperty('_hasLeft') && !stats[id].hasLeft()) {
132
+            if (orderConfig.includes('name')) {
133
+                const localParticipant = getLocalParticipant(state);
134
+
135
+                if (stats[id].isLocalStats()) {
136
+                    stats[id].setDisplayName(localParticipant.name);
137
+                }
138
+            }
139
+
140
+            if (orderConfig.includes('role')) {
141
+                const participant = getParticipantById(state, stats[id].getUserId());
142
+
143
+                stats[id].isModerator = participant && participant.role === PARTICIPANT_ROLE.MODERATOR;
144
+            }
145
+        }
146
+    }
147
+
148
+    return stats;
149
+}
150
+
151
+/**
152
+ * Filter stats by search criteria.
153
+ *
154
+ * @param {Object} state - The redux state.
155
+ * @param {Object | undefined} stats - The unfiltered stats.
156
+ *
157
+ * @returns {Object} - Filtered speaker stats.
158
+ * @public
159
+ */
160
+export function filterBySearchCriteria(state: Object, stats: ?Object) {
161
+    const filteredStats = _.cloneDeep(stats ?? getSpeakerStats(state));
162
+    const criteria = getSearchCriteria(state);
163
+
164
+    if (criteria) {
165
+        const searchRegex = new RegExp(criteria, 'gi');
166
+
167
+        for (const id in filteredStats) {
168
+            if (filteredStats[id].hasOwnProperty('_isLocalStats')) {
169
+                const name = filteredStats[id].getDisplayName();
170
+
171
+                if (!name || !name.match(searchRegex)) {
172
+                    filteredStats[id].hidden = true;
173
+                }
174
+            }
175
+        }
176
+    }
177
+
178
+    return filteredStats;
179
+}

+ 49
- 0
react/features/speaker-stats/middleware.js 查看文件

@@ -0,0 +1,49 @@
1
+// @flow
2
+
3
+import {
4
+    PARTICIPANT_JOINED,
5
+    PARTICIPANT_KICKED,
6
+    PARTICIPANT_LEFT,
7
+    PARTICIPANT_UPDATED
8
+} from '../base/participants/actionTypes';
9
+import { MiddlewareRegistry } from '../base/redux';
10
+
11
+import { INIT_SEARCH, INIT_UPDATE_STATS } from './actionTypes';
12
+import { initReorderStats, updateStats } from './actions';
13
+import { filterBySearchCriteria, getSortedSpeakerStats, getPendingReorder } from './functions';
14
+
15
+MiddlewareRegistry.register(({ dispatch, getState }) => next => action => {
16
+    const result = next(action);
17
+
18
+    switch (action.type) {
19
+
20
+    case INIT_SEARCH: {
21
+        const state = getState();
22
+        const stats = filterBySearchCriteria(state);
23
+
24
+        dispatch(updateStats(stats));
25
+        break;
26
+    }
27
+
28
+    case INIT_UPDATE_STATS:
29
+        if (action.getSpeakerStats) {
30
+            const state = getState();
31
+            const speakerStats = { ...action.getSpeakerStats() };
32
+            const stats = filterBySearchCriteria(state, speakerStats);
33
+            const pendingReorder = getPendingReorder(state);
34
+
35
+            dispatch(updateStats(pendingReorder ? getSortedSpeakerStats(state, stats) : stats));
36
+        }
37
+        break;
38
+    case PARTICIPANT_JOINED:
39
+    case PARTICIPANT_LEFT:
40
+    case PARTICIPANT_KICKED:
41
+    case PARTICIPANT_UPDATED: {
42
+        dispatch(initReorderStats());
43
+
44
+        break;
45
+    }
46
+    }
47
+
48
+    return result;
49
+});

+ 119
- 0
react/features/speaker-stats/reducer.js 查看文件

@@ -0,0 +1,119 @@
1
+// @flow
2
+
3
+import _ from 'lodash';
4
+
5
+import { ReducerRegistry } from '../base/redux';
6
+
7
+import {
8
+    INIT_SEARCH,
9
+    UPDATE_STATS,
10
+    INIT_REORDER_STATS
11
+} from './actionTypes';
12
+
13
+/**
14
+ * The initial state of the feature speaker-stats.
15
+ *
16
+ * @type {Object}
17
+ */
18
+const INITIAL_STATE = {
19
+    stats: {},
20
+    pendingReorder: true,
21
+    criteria: ''
22
+};
23
+
24
+ReducerRegistry.register('features/speaker-stats', (state = _getInitialState(), action) => {
25
+    switch (action.type) {
26
+    case INIT_SEARCH:
27
+        return _updateCriteria(state, action);
28
+    case UPDATE_STATS:
29
+        return _updateStats(state, action);
30
+    case INIT_REORDER_STATS:
31
+        return _initReorderStats(state);
32
+    }
33
+
34
+    return state;
35
+});
36
+
37
+/**
38
+ * Gets the initial state of the feature speaker-stats.
39
+ *
40
+ * @returns {Object}
41
+ */
42
+function _getInitialState() {
43
+    return INITIAL_STATE;
44
+}
45
+
46
+/**
47
+ * Reduces a specific Redux action INIT_SEARCH of the feature
48
+ * speaker-stats.
49
+ *
50
+ * @param {Object} state - The Redux state of the feature speaker-stats.
51
+ * @param {Action} action - The Redux action INIT_SEARCH to reduce.
52
+ * @private
53
+ * @returns {Object} The new state after the reduction of the specified action.
54
+ */
55
+function _updateCriteria(state, { criteria }) {
56
+    return _.assign(
57
+        {},
58
+        state,
59
+        { criteria },
60
+    );
61
+}
62
+
63
+/**
64
+ * Reduces a specific Redux action UPDATE_STATS of the feature
65
+ * speaker-stats.
66
+ * The speaker stats order is based on the stats object properties.
67
+ * When updating without reordering, the new stats object properties are reordered
68
+ * as the last in state, otherwise the order would be lost on each update.
69
+ * If there was already a pending reorder, the stats object properties already have
70
+ * the correct order, so the property order is not changing.
71
+ *
72
+ * @param {Object} state - The Redux state of the feature speaker-stats.
73
+ * @param {Action} action - The Redux action UPDATE_STATS to reduce.
74
+ * @private
75
+ * @returns {Object} - The new state after the reduction of the specified action.
76
+ */
77
+function _updateStats(state, { stats }) {
78
+    const finalStats = state.pendingReorder ? stats : state.stats;
79
+
80
+    if (!state.pendingReorder) {
81
+        // Avoid reordering the speaker stats object properties
82
+        const finalKeys = Object.keys(stats);
83
+
84
+        finalKeys.forEach(newStatId => {
85
+            finalStats[newStatId] = _.clone(stats[newStatId]);
86
+        });
87
+
88
+        Object.keys(finalStats).forEach(key => {
89
+            if (!finalKeys.includes(key)) {
90
+                delete finalStats[key];
91
+            }
92
+        });
93
+    }
94
+
95
+    return _.assign(
96
+        {},
97
+        state,
98
+        {
99
+            stats: finalStats,
100
+            pendingReorder: false
101
+        },
102
+    );
103
+}
104
+
105
+/**
106
+ * Reduces a specific Redux action INIT_REORDER_STATS of the feature
107
+ * speaker-stats.
108
+ *
109
+ * @param {Object} state - The Redux state of the feature speaker-stats.
110
+ * @private
111
+ * @returns {Object} The new state after the reduction of the specified action.
112
+ */
113
+function _initReorderStats(state) {
114
+    return _.assign(
115
+        {},
116
+        state,
117
+        { pendingReorder: true },
118
+    );
119
+}

Loading…
取消
儲存