瀏覽代碼

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
No account linked to committer's email address

+ 7
- 0
config.js 查看文件

150
     // Specifies whether there will be a search field in speaker stats or not
150
     // Specifies whether there will be a search field in speaker stats or not
151
     // disableSpeakerStatsSearch: false,
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
     // How many participants while in the tile view mode, before the receiving video quality is reduced from HD to SD.
160
     // How many participants while in the tile view mode, before the receiving video quality is reduced from HD to SD.
154
     // Use -1 to disable.
161
     // Use -1 to disable.
155
     // maxFullResolutionParticipants: 2,
162
     // maxFullResolutionParticipants: 2,

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

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

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

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

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

106
     'disableShowMoreStats',
106
     'disableShowMoreStats',
107
     'disableRemoveRaisedHandOnFocus',
107
     'disableRemoveRaisedHandOnFocus',
108
     'disableSpeakerStatsSearch',
108
     'disableSpeakerStatsSearch',
109
+    'speakerStatsOrder',
109
     'disableSimulcast',
110
     'disableSimulcast',
110
     'disableThirdPartyRequests',
111
     'disableThirdPartyRequests',
111
     'disableTileView',
112
     'disableTileView',

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

185
 
185
 
186
     return [ r, g, b ];
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 查看文件

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 查看文件

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
 // @flow
1
 // @flow
2
 
2
 
3
 import React, { Component } from 'react';
3
 import React, { Component } from 'react';
4
+import type { Dispatch } from 'redux';
4
 
5
 
5
 import { Dialog } from '../../base/dialog';
6
 import { Dialog } from '../../base/dialog';
6
 import { translate } from '../../base/i18n';
7
 import { translate } from '../../base/i18n';
7
 import { getLocalParticipant } from '../../base/participants';
8
 import { getLocalParticipant } from '../../base/participants';
8
 import { connect } from '../../base/redux';
9
 import { connect } from '../../base/redux';
9
 import { escapeRegexp } from '../../base/util';
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
 import SpeakerStatsItem from './SpeakerStatsItem';
15
 import SpeakerStatsItem from './SpeakerStatsItem';
12
 import SpeakerStatsLabels from './SpeakerStatsLabels';
16
 import SpeakerStatsLabels from './SpeakerStatsLabels';
25
     _localDisplayName: string,
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
  *
59
  *
57
  * @extends Component
60
  * @extends Component
58
  */
61
  */
59
-class SpeakerStats extends Component<Props, State> {
62
+class SpeakerStats extends Component<Props> {
60
     _updateInterval: IntervalID;
63
     _updateInterval: IntervalID;
61
 
64
 
62
     /**
65
     /**
68
     constructor(props) {
71
     constructor(props) {
69
         super(props);
72
         super(props);
70
 
73
 
71
-        this.state = {
72
-            stats: this._getSpeakerStats(),
73
-            criteria: ''
74
-        };
75
-
76
         // Bind event handlers so they are only bound once per instance.
74
         // Bind event handlers so they are only bound once per instance.
77
         this._updateStats = this._updateStats.bind(this);
75
         this._updateStats = this._updateStats.bind(this);
78
         this._onSearch = this._onSearch.bind(this);
76
         this._onSearch = this._onSearch.bind(this);
77
+
78
+        this._updateStats();
79
     }
79
     }
80
 
80
 
81
     /**
81
     /**
84
      * @inheritdoc
84
      * @inheritdoc
85
      */
85
      */
86
     componentDidMount() {
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
      * @returns {ReactElement}
104
      * @returns {ReactElement}
105
      */
105
      */
106
     render() {
106
     render() {
107
-        const userIds = Object.keys(this.state.stats);
107
+        const userIds = Object.keys(this.props._stats);
108
         const items = userIds.map(userId => this._createStatsItem(userId));
108
         const items = userIds.map(userId => this._createStatsItem(userId));
109
 
109
 
110
         return (
110
         return (
111
             <Dialog
111
             <Dialog
112
-                cancelKey = { 'dialog.close' }
112
+                cancelKey = 'dialog.close'
113
                 submitDisabled = { true }
113
                 submitDisabled = { true }
114
-                titleKey = { 'speakerStats.speakerStats' }>
114
+                titleKey = 'speakerStats.speakerStats'>
115
                 <div className = 'speaker-stats'>
115
                 <div className = 'speaker-stats'>
116
                     <SpeakerStatsSearch onSearch = { this._onSearch } />
116
                     <SpeakerStatsSearch onSearch = { this._onSearch } />
117
                     <SpeakerStatsLabels />
117
                     <SpeakerStatsLabels />
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
      * Create a SpeakerStatsItem instance for the passed in user id.
125
      * Create a SpeakerStatsItem instance for the passed in user id.
152
      *
126
      *
156
      * @private
130
      * @private
157
      */
131
      */
158
     _createStatsItem(userId) {
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
             return null;
136
             return null;
163
         }
137
         }
164
 
138
 
177
                 = displayName ? `${displayName} (${meString})` : meString;
151
                 = displayName ? `${displayName} (${meString})` : meString;
178
         } else {
152
         } else {
179
             displayName
153
             displayName
180
-                = this.state.stats[userId].getDisplayName()
154
+                = this.props._stats[userId].getDisplayName()
181
                     || interfaceConfig.DEFAULT_REMOTE_DISPLAY_NAME;
155
                     || interfaceConfig.DEFAULT_REMOTE_DISPLAY_NAME;
182
         }
156
         }
183
 
157
 
201
      * @protected
175
      * @protected
202
      */
176
      */
203
     _onSearch(criteria = '') {
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
     _updateStats: () => void;
181
     _updateStats: () => void;
216
      * @private
187
      * @private
217
      */
188
      */
218
     _updateStats() {
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
  * @param {Object} state - The redux state.
197
  * @param {Object} state - The redux state.
232
  * @private
198
  * @private
233
  * @returns {{
199
  * @returns {{
234
- *     _localDisplayName: ?string
200
+ *     _localDisplayName: ?string,
201
+ *     _stats: Object,
202
+ *     _criteria: string,
235
  * }}
203
  * }}
236
  */
204
  */
237
 function _mapStateToProps(state) {
205
 function _mapStateToProps(state) {
244
          * @private
212
          * @private
245
          * @type {string|undefined}
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 查看文件

1
+export const SPEAKER_STATS_RELOAD_INTERVAL = 1000;

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

1
 // @flow
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
  * Checks if the speaker stats search is disabled.
9
  * Checks if the speaker stats search is disabled.
5
  *
10
  *
9
 export function isSpeakerStatsSearchDisabled(state: Object) {
14
 export function isSpeakerStatsSearchDisabled(state: Object) {
10
     return state['features/base/config']?.disableSpeakerStatsSearch;
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 查看文件

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 查看文件

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…
取消
儲存