浏览代码

feat: Participants optimisations (#9515)

* fix(participants): Change from array to Map

* fix(unload): optimise

* feat: Introduces new states for e2ee feature.

Stores everyoneSupportsE2EE and everyoneEnabledE2EE to minimize looping through participants list.

squash: Uses participants map and go over the elements only once.

* feat: Optimizes isEveryoneModerator to do less frequent checks in all participants.

* fix: Drops deep equal from participants pane and uses the map.

* fix(SharedVideo): isVideoPlaying

* fix(participants): Optimise isEveryoneModerator

* fix(e2e): Optimise everyoneEnabledE2EE

* fix: JS errors.

* ref(participants): remove getParticipants

* fix(participants): Prepare for PR.

* fix: Changes participants pane to be component.

The functional component was always rendered:
`prev props: {} !== {} :next props`.

* feat: Optimization to skip participants list on pane closed.

* fix: The participants list shows and the local participant.

* fix: Fix wrong action name for av-moderation.

* fix: Minimizes the number of render calls of av moderation notification.

* fix: Fix iterating over remote participants.

* fix: Fixes lint error.

* fix: Reflects participant updates for av-moderation.

* fix(ParticipantPane): to work with IDs.

* fix(av-moderation): on PARTCIPANT_UPDATE

* fix(ParticipantPane): close delay.

* fix: address code review comments

* fix(API): mute-everyone

* fix: bugs

* fix(Thumbnail): on mobile.

* fix(ParticipantPane): Close context menu on click.

* fix: Handles few error when local participant is undefined.

* feat: Hides AV moderation if not supported.

* fix: Show mute all video.

* fix: Fixes updating participant for av moderation.

Co-authored-by: damencho <damencho@jitsi.org>
j8
Hristo Terezov 3 年前
父节点
当前提交
0bdc7d42c5
没有帐户链接到提交者的电子邮件
共有 52 个文件被更改,包括 1911 次插入818 次删除
  1. 9
    7
      modules/API/API.js
  2. 9
    9
      react/features/av-moderation/actions.js
  3. 5
    2
      react/features/av-moderation/components/AudioModerationNotifications.js
  4. 25
    8
      react/features/av-moderation/functions.js
  5. 6
    4
      react/features/av-moderation/middleware.js
  6. 107
    7
      react/features/av-moderation/reducer.js
  7. 2
    3
      react/features/base/conference/middleware.any.js
  8. 8
    5
      react/features/base/config/functions.web.js
  9. 104
    72
      react/features/base/participants/functions.js
  10. 10
    6
      react/features/base/participants/middleware.js
  11. 263
    59
      react/features/base/participants/reducer.js
  12. 27
    45
      react/features/base/tracks/functions.js
  13. 1
    1
      react/features/chat/components/AbstractChat.js
  14. 1
    2
      react/features/display-name/components/web/DisplayName.js
  15. 19
    0
      react/features/e2ee/actionTypes.js
  16. 33
    1
      react/features/e2ee/actions.js
  17. 1
    3
      react/features/e2ee/components/AbstractE2EELabel.js
  18. 5
    7
      react/features/e2ee/components/E2EESection.js
  19. 135
    2
      react/features/e2ee/middleware.js
  20. 15
    1
      react/features/e2ee/reducer.js
  21. 3
    2
      react/features/filmstrip/actions.web.js
  22. 5
    6
      react/features/filmstrip/components/native/Filmstrip.js
  23. 8
    50
      react/features/filmstrip/components/native/LocalThumbnail.js
  24. 37
    70
      react/features/filmstrip/components/native/Thumbnail.js
  25. 27
    21
      react/features/filmstrip/components/native/TileView.js
  26. 2
    2
      react/features/filmstrip/components/web/StatusIndicators.js
  27. 5
    7
      react/features/filmstrip/components/web/Thumbnail.js
  28. 2
    3
      react/features/filmstrip/functions.native.js
  29. 2
    1
      react/features/filmstrip/subscriber.web.js
  30. 3
    3
      react/features/invite/actions.any.js
  31. 16
    16
      react/features/invite/components/callee-info/CalleeInfo.js
  32. 13
    6
      react/features/invite/middleware.any.js
  33. 16
    5
      react/features/large-video/actions.any.js
  34. 34
    11
      react/features/mobile/external-api/middleware.js
  35. 7
    5
      react/features/participants-pane/components/AskToUnmuteButton.js
  36. 36
    23
      react/features/participants-pane/components/FooterContextMenu.js
  37. 7
    4
      react/features/participants-pane/components/LobbyParticipantItem.js
  38. 322
    112
      react/features/participants-pane/components/MeetingParticipantContextMenu.js
  39. 130
    23
      react/features/participants-pane/components/MeetingParticipantItem.js
  40. 65
    27
      react/features/participants-pane/components/MeetingParticipantList.js
  41. 40
    21
      react/features/participants-pane/components/ParticipantItem.js
  42. 25
    15
      react/features/participants-pane/components/ParticipantQuickAction.js
  43. 234
    71
      react/features/participants-pane/components/ParticipantsPane.js
  44. 1
    4
      react/features/participants-pane/components/index.js
  45. 18
    15
      react/features/participants-pane/functions.js
  46. 1
    1
      react/features/shared-video/components/web/SharedVideo.js
  47. 12
    4
      react/features/shared-video/functions.js
  48. 2
    1
      react/features/subtitles/components/Captions.web.js
  49. 35
    33
      react/features/toolbox/components/web/Toolbox.js
  50. 2
    1
      react/features/toolbox/functions.native.js
  51. 3
    2
      react/features/video-layout/functions.js
  52. 13
    9
      react/features/video-menu/actions.any.js

+ 9
- 7
modules/API/API.js 查看文件

@@ -23,7 +23,8 @@ import {
23 23
     getParticipantById,
24 24
     pinParticipant,
25 25
     kickParticipant,
26
-    raiseHand
26
+    raiseHand,
27
+    isParticipantModerator
27 28
 } from '../../react/features/base/participants';
28 29
 import { updateSettings } from '../../react/features/base/settings';
29 30
 import { isToggleCameraEnabled, toggleCamera } from '../../react/features/base/tracks';
@@ -105,13 +106,14 @@ function initCommands() {
105 106
             const muteMediaType = mediaType ? mediaType : MEDIA_TYPE.AUDIO;
106 107
 
107 108
             sendAnalytics(createApiEvent('muted-everyone'));
108
-            const participants = APP.store.getState()['features/base/participants'];
109
-            const localIds = participants
110
-                .filter(participant => participant.local)
111
-                .filter(participant => participant.role === 'moderator')
112
-                .map(participant => participant.id);
109
+            const localParticipant = getLocalParticipant(APP.store.getState());
110
+            const exclude = [];
111
+
112
+            if (localParticipant && isParticipantModerator(localParticipant)) {
113
+                exclude.push(localParticipant.id);
114
+            }
113 115
 
114
-            APP.store.dispatch(muteAllParticipants(localIds, muteMediaType));
116
+            APP.store.dispatch(muteAllParticipants(exclude, muteMediaType));
115 117
         },
116 118
         'toggle-lobby': isLobbyEnabled => {
117 119
             APP.store.dispatch(toggleLobbyMode(isLobbyEnabled));

+ 9
- 9
react/features/av-moderation/actions.js 查看文件

@@ -49,24 +49,24 @@ export const disableModeration = (mediaType: MediaType, actor: Object) => {
49 49
 /**
50 50
  * Hides the notification with the participant that asked to unmute audio.
51 51
  *
52
- * @param {string} id - The participant id.
52
+ * @param {Object} participant - The participant for which the notification to be hidden.
53 53
  * @returns {Object}
54 54
  */
55
-export function dismissPendingAudioParticipant(id: string) {
56
-    return dismissPendingParticipant(id, MEDIA_TYPE.AUDIO);
55
+export function dismissPendingAudioParticipant(participant: Object) {
56
+    return dismissPendingParticipant(participant, MEDIA_TYPE.AUDIO);
57 57
 }
58 58
 
59 59
 /**
60 60
  * Hides the notification with the participant that asked to unmute.
61 61
  *
62
- * @param {string} id - The participant id.
62
+ * @param {Object} participant - The participant for which the notification to be hidden.
63 63
  * @param {MediaType} mediaType - The media type.
64 64
  * @returns {Object}
65 65
  */
66
-export function dismissPendingParticipant(id: string, mediaType: MediaType) {
66
+export function dismissPendingParticipant(participant: Object, mediaType: MediaType) {
67 67
     return {
68 68
         type: DISMISS_PENDING_PARTICIPANT,
69
-        id,
69
+        participant,
70 70
         mediaType
71 71
     };
72 72
 }
@@ -145,13 +145,13 @@ export function showModeratedNotification(mediaType: MediaType) {
145 145
 /**
146 146
  * Shows a notification with the participant that asked to audio unmute.
147 147
  *
148
- * @param {string} id - The participant id.
148
+ * @param {Object} participant - The participant for which is the notification.
149 149
  * @returns {Object}
150 150
  */
151
-export function participantPendingAudio(id: string) {
151
+export function participantPendingAudio(participant: Object) {
152 152
     return {
153 153
         type: PARTICIPANT_PENDING_AUDIO,
154
-        id
154
+        participant
155 155
     };
156 156
 }
157 157
 

+ 5
- 2
react/features/av-moderation/components/AudioModerationNotifications.js 查看文件

@@ -3,7 +3,10 @@ import { useTranslation } from 'react-i18next';
3 3
 import { useSelector } from 'react-redux';
4 4
 
5 5
 import NotificationWithParticipants from '../../notifications/components/web/NotificationWithParticipants';
6
-import { approveAudio, dismissPendingAudioParticipant } from '../actions';
6
+import {
7
+    approveParticipant,
8
+    dismissPendingAudioParticipant
9
+} from '../actions';
7 10
 import { getParticipantsAskingToAudioUnmute } from '../functions';
8 11
 
9 12
 
@@ -25,7 +28,7 @@ export default function() {
25 28
                 </div>
26 29
                 <NotificationWithParticipants
27 30
                     approveButtonText = { t('notify.unmute') }
28
-                    onApprove = { approveAudio }
31
+                    onApprove = { approveParticipant }
29 32
                     onReject = { dismissPendingAudioParticipant }
30 33
                     participants = { participants }
31 34
                     rejectButtonText = { t('dialog.dismiss') }

+ 25
- 8
react/features/av-moderation/functions.js 查看文件

@@ -1,7 +1,7 @@
1 1
 // @flow
2 2
 
3 3
 import { MEDIA_TYPE, type MediaType } from '../base/media/constants';
4
-import { getParticipantById, isLocalParticipantModerator } from '../base/participants/functions';
4
+import { isLocalParticipantModerator } from '../base/participants/functions';
5 5
 
6 6
 import { MEDIA_TYPE_TO_WHITELIST_STORE_KEY, MEDIA_TYPE_TO_PENDING_STORE_KEY } from './constants';
7 7
 
@@ -13,6 +13,14 @@ import { MEDIA_TYPE_TO_WHITELIST_STORE_KEY, MEDIA_TYPE_TO_PENDING_STORE_KEY } fr
13 13
  */
14 14
 const getState = state => state['features/av-moderation'];
15 15
 
16
+/**
17
+ * We use to construct once the empty array so we can keep the same instance between calls
18
+ * of getParticipantsAskingToAudioUnmute.
19
+ *
20
+ * @type {*[]}
21
+ */
22
+const EMPTY_ARRAY = [];
23
+
16 24
 /**
17 25
  * Returns whether moderation is enabled per media type.
18 26
  *
@@ -33,6 +41,17 @@ export const isEnabledFromState = (mediaType: MediaType, state: Object) =>
33 41
  */
34 42
 export const isEnabled = (mediaType: MediaType) => (state: Object) => isEnabledFromState(mediaType, state);
35 43
 
44
+/**
45
+ * Returns whether moderation is supported by the backend.
46
+ *
47
+ * @returns {null|boolean}
48
+ */
49
+export const isSupported = () => (state: Object) => {
50
+    const { conference } = state['features/base/conference'];
51
+
52
+    return conference ? conference.isAVModerationSupported() : false;
53
+};
54
+
36 55
 /**
37 56
  * Returns whether local participant is approved to unmute a media type.
38 57
  *
@@ -74,15 +93,15 @@ export const isParticipantApproved = (id: string, mediaType: MediaType) => (stat
74 93
 /**
75 94
  * Returns a selector creator which determines if the participant is pending or not for a media type.
76 95
  *
77
- * @param {string} id - The participant id.
96
+ * @param {Participant} participant - The participant.
78 97
  * @param {MEDIA_TYPE} mediaType - The media type to check.
79 98
  * @returns {boolean}
80 99
  */
81
-export const isParticipantPending = (id: string, mediaType: MediaType) => (state: Object) => {
100
+export const isParticipantPending = (participant: Object, mediaType: MediaType) => (state: Object) => {
82 101
     const storeKey = MEDIA_TYPE_TO_PENDING_STORE_KEY[mediaType];
83 102
     const arr = getState(state)[storeKey];
84 103
 
85
-    return Boolean(arr.find(pending => pending === id));
104
+    return Boolean(arr.find(pending => pending.id === participant.id));
86 105
 };
87 106
 
88 107
 /**
@@ -94,12 +113,10 @@ export const isParticipantPending = (id: string, mediaType: MediaType) => (state
94 113
  */
95 114
 export const getParticipantsAskingToAudioUnmute = (state: Object) => {
96 115
     if (isLocalParticipantModerator(state)) {
97
-        const ids = getState(state).pendingAudio;
98
-
99
-        return ids.map(id => getParticipantById(state, id)).filter(Boolean);
116
+        return getState(state).pendingAudio;
100 117
     }
101 118
 
102
-    return [];
119
+    return EMPTY_ARRAY;
103 120
 };
104 121
 
105 122
 /**

+ 6
- 4
react/features/av-moderation/middleware.js 查看文件

@@ -127,14 +127,16 @@ MiddlewareRegistry.register(({ dispatch, getState }) => next => action => {
127 127
 
128 128
         // this is handled only by moderators
129 129
         if (audioModerationEnabled && isLocalParticipantModerator(state)) {
130
-            const { participant: { id, raisedHand } } = action;
130
+            const participant = action.participant;
131 131
 
132
-            if (raisedHand) {
132
+            if (participant.raisedHand) {
133 133
                 // if participant raises hand show notification
134
-                !isParticipantApproved(id, MEDIA_TYPE.AUDIO)(state) && dispatch(participantPendingAudio(id));
134
+                !isParticipantApproved(participant.id, MEDIA_TYPE.AUDIO)(state)
135
+                    && dispatch(participantPendingAudio(participant));
135 136
             } else {
136 137
                 // if participant lowers hand hide notification
137
-                isParticipantPending(id, MEDIA_TYPE.AUDIO)(state) && dispatch(dismissPendingAudioParticipant(id));
138
+                isParticipantPending(participant, MEDIA_TYPE.AUDIO)(state)
139
+                    && dispatch(dismissPendingAudioParticipant(participant));
138 140
             }
139 141
         }
140 142
 

+ 107
- 7
react/features/av-moderation/reducer.js 查看文件

@@ -1,6 +1,11 @@
1 1
 /* @flow */
2 2
 
3 3
 import { MEDIA_TYPE } from '../base/media/constants';
4
+import type { MediaType } from '../base/media/constants';
5
+import {
6
+    PARTICIPANT_LEFT,
7
+    PARTICIPANT_UPDATED
8
+} from '../base/participants';
4 9
 import { ReducerRegistry } from '../base/redux';
5 10
 
6 11
 import {
@@ -11,6 +16,7 @@ import {
11 16
     PARTICIPANT_APPROVED,
12 17
     PARTICIPANT_PENDING_AUDIO
13 18
 } from './actionTypes';
19
+import { MEDIA_TYPE_TO_PENDING_STORE_KEY } from './constants';
14 20
 
15 21
 const initialState = {
16 22
     audioModerationEnabled: false,
@@ -21,6 +27,41 @@ const initialState = {
21 27
     pendingVideo: []
22 28
 };
23 29
 
30
+/**
31
+ Updates a participant in the state for the specified media type.
32
+ *
33
+ * @param {MediaType} mediaType - The media type.
34
+ * @param {Object} participant - Information about participant to be modified.
35
+ * @param {Object} state - The current state.
36
+ * @private
37
+ * @returns {boolean} - Whether state instance was modified.
38
+ */
39
+function _updatePendingParticipant(mediaType: MediaType, participant, state: Object = {}) {
40
+    let arrayItemChanged = false;
41
+    const storeKey = MEDIA_TYPE_TO_PENDING_STORE_KEY[mediaType];
42
+    const arr = state[storeKey];
43
+    const newArr = arr.map(pending => {
44
+        if (pending.id === participant.id) {
45
+            arrayItemChanged = true;
46
+
47
+            return {
48
+                ...pending,
49
+                ...participant
50
+            };
51
+        }
52
+
53
+        return pending;
54
+    });
55
+
56
+    if (arrayItemChanged) {
57
+        state[storeKey] = newArr;
58
+
59
+        return true;
60
+    }
61
+
62
+    return false;
63
+}
64
+
24 65
 ReducerRegistry.register('features/av-moderation', (state = initialState, action) => {
25 66
 
26 67
     switch (action.type) {
@@ -65,13 +106,13 @@ ReducerRegistry.register('features/av-moderation', (state = initialState, action
65 106
     }
66 107
 
67 108
     case PARTICIPANT_PENDING_AUDIO: {
68
-        const { id } = action;
109
+        const { participant } = action;
69 110
 
70
-        // Add participant to pendigAudio array only if it's not already added
71
-        if (!state.pendingAudio.find(pending => pending === id)) {
111
+        // Add participant to pendingAudio array only if it's not already added
112
+        if (!state.pendingAudio.find(pending => pending.id === participant.id)) {
72 113
             const updated = [ ...state.pendingAudio ];
73 114
 
74
-            updated.push(id);
115
+            updated.push(participant);
75 116
 
76 117
             return {
77 118
                 ...state,
@@ -82,20 +123,79 @@ ReducerRegistry.register('features/av-moderation', (state = initialState, action
82 123
         return state;
83 124
     }
84 125
 
126
+    case PARTICIPANT_UPDATED: {
127
+        const participant = action.participant;
128
+        const { audioModerationEnabled, videoModerationEnabled } = state;
129
+        let hasStateChanged = false;
130
+
131
+        // skips changing the reference of pendingAudio or pendingVideo,
132
+        // if there is no change in the elements
133
+        if (audioModerationEnabled) {
134
+            hasStateChanged = _updatePendingParticipant(MEDIA_TYPE.AUDIO, participant, state);
135
+        }
136
+
137
+        if (videoModerationEnabled) {
138
+            hasStateChanged = _updatePendingParticipant(MEDIA_TYPE.VIDEO, participant, state);
139
+        }
140
+
141
+        // If the state has changed we need to return a new object reference in order to trigger subscriber updates.
142
+        if (hasStateChanged) {
143
+            return {
144
+                ...state
145
+            };
146
+        }
147
+
148
+        return state;
149
+    }
150
+    case PARTICIPANT_LEFT: {
151
+        const participant = action.participant;
152
+        const { audioModerationEnabled, videoModerationEnabled } = state;
153
+        let hasStateChanged = false;
154
+
155
+        // skips changing the reference of pendingAudio or pendingVideo,
156
+        // if there is no change in the elements
157
+        if (audioModerationEnabled) {
158
+            const newPendingAudio = state.pendingAudio.filter(pending => pending.id !== participant.id);
159
+
160
+            if (state.pendingAudio.length !== newPendingAudio.length) {
161
+                state.pendingAudio = newPendingAudio;
162
+                hasStateChanged = true;
163
+            }
164
+        }
165
+
166
+        if (videoModerationEnabled) {
167
+            const newPendingVideo = state.pendingVideo.filter(pending => pending.id !== participant.id);
168
+
169
+            if (state.pendingVideo.length !== newPendingVideo.length) {
170
+                state.pendingVideo = newPendingVideo;
171
+                hasStateChanged = true;
172
+            }
173
+        }
174
+
175
+        // If the state has changed we need to return a new object reference in order to trigger subscriber updates.
176
+        if (hasStateChanged) {
177
+            return {
178
+                ...state
179
+            };
180
+        }
181
+
182
+        return state;
183
+    }
184
+
85 185
     case DISMISS_PENDING_PARTICIPANT: {
86
-        const { id, mediaType } = action;
186
+        const { participant, mediaType } = action;
87 187
 
88 188
         if (mediaType === MEDIA_TYPE.AUDIO) {
89 189
             return {
90 190
                 ...state,
91
-                pendingAudio: state.pendingAudio.filter(pending => pending !== id)
191
+                pendingAudio: state.pendingAudio.filter(pending => pending.id !== participant.id)
92 192
             };
93 193
         }
94 194
 
95 195
         if (mediaType === MEDIA_TYPE.VIDEO) {
96 196
             return {
97 197
                 ...state,
98
-                pendingAudio: state.pendingVideo.filter(pending => pending !== id)
198
+                pendingVideo: state.pendingVideo.filter(pending => pending.id !== participant.id)
99 199
             };
100 200
         }
101 201
 

+ 2
- 3
react/features/base/conference/middleware.any.js 查看文件

@@ -398,10 +398,9 @@ function _pinParticipant({ getState }, next, action) {
398 398
         return next(action);
399 399
     }
400 400
 
401
-    const participants = state['features/base/participants'];
402 401
     const id = action.participant.id;
403
-    const participantById = getParticipantById(participants, id);
404
-    const pinnedParticipant = getPinnedParticipant(participants);
402
+    const participantById = getParticipantById(state, id);
403
+    const pinnedParticipant = getPinnedParticipant(state);
405 404
     const actionName = id ? ACTION_PINNED : ACTION_UNPINNED;
406 405
     const local
407 406
         = (participantById && participantById.local)

+ 8
- 5
react/features/base/config/functions.web.js 查看文件

@@ -56,12 +56,15 @@ export function getToolbarButtons(state: Object): Array<string> {
56 56
 }
57 57
 
58 58
 /**
59
- * Curried selector to check if the specified button is enabled.
59
+ * Checks if the specified button is enabled.
60 60
  *
61 61
  * @param {string} buttonName - The name of the button.
62 62
  * {@link interfaceConfig}.
63
- * @returns {Function} - Selector that returns a boolean.
63
+ * @param {Object|Array<string>} state - The redux state or the array with the enabled buttons.
64
+ * @returns {boolean} - True if the button is enabled and false otherwise.
64 65
  */
65
-export const isToolbarButtonEnabled = (buttonName: string) =>
66
-    (state: Object): boolean =>
67
-        getToolbarButtons(state).includes(buttonName);
66
+export function isToolbarButtonEnabled(buttonName: string, state: Object | Array<string>) {
67
+    const buttons = Array.isArray(state) ? state : getToolbarButtons(state);
68
+
69
+    return buttons.includes(buttonName);
70
+}

+ 104
- 72
react/features/base/participants/functions.js 查看文件

@@ -79,16 +79,15 @@ export function getFirstLoadableAvatarUrl(participant: Object, store: Store<any,
79 79
 /**
80 80
  * Returns local participant from Redux state.
81 81
  *
82
- * @param {(Function|Object|Participant[])} stateful - The redux state
83
- * features/base/participants, the (whole) redux state, or redux's
82
+ * @param {(Function|Object)} stateful - The (whole) redux state, or redux's
84 83
  * {@code getState} function to be used to retrieve the state
85 84
  * features/base/participants.
86 85
  * @returns {(Participant|undefined)}
87 86
  */
88 87
 export function getLocalParticipant(stateful: Object | Function) {
89
-    const participants = _getAllParticipants(stateful);
88
+    const state = toState(stateful)['features/base/participants'];
90 89
 
91
-    return participants.find(p => p.local);
90
+    return state.local;
92 91
 }
93 92
 
94 93
 /**
@@ -109,8 +108,7 @@ export function getNormalizedDisplayName(name: string) {
109 108
 /**
110 109
  * Returns participant by ID from Redux state.
111 110
  *
112
- * @param {(Function|Object|Participant[])} stateful - The redux state
113
- * features/base/participants, the (whole) redux state, or redux's
111
+ * @param {(Function|Object)} stateful - The (whole) redux state, or redux's
114 112
  * {@code getState} function to be used to retrieve the state
115 113
  * features/base/participants.
116 114
  * @param {string} id - The ID of the participant to retrieve.
@@ -119,37 +117,82 @@ export function getNormalizedDisplayName(name: string) {
119 117
  */
120 118
 export function getParticipantById(
121 119
         stateful: Object | Function, id: string): ?Object {
122
-    const participants = _getAllParticipants(stateful);
120
+    const state = toState(stateful)['features/base/participants'];
121
+    const { local, remote } = state;
123 122
 
124
-    return participants.find(p => p.id === id);
123
+    return remote.get(id) || (local?.id === id ? local : undefined);
124
+}
125
+
126
+/**
127
+ * Returns the participant with the ID matching the passed ID or the local participant if the ID is
128
+ * undefined.
129
+ *
130
+ * @param {(Function|Object)} stateful - The (whole) redux state, or redux's
131
+ * {@code getState} function to be used to retrieve the state
132
+ * features/base/participants.
133
+ * @param {string|undefined} [participantID] - An optional partipantID argument.
134
+ * @returns {Participant|undefined}
135
+ */
136
+export function getParticipantByIdOrUndefined(stateful: Object | Function, participantID: ?string) {
137
+    return participantID ? getParticipantById(stateful, participantID) : getLocalParticipant(stateful);
125 138
 }
126 139
 
127 140
 /**
128 141
  * Returns a count of the known participants in the passed in redux state,
129 142
  * excluding any fake participants.
130 143
  *
131
- * @param {(Function|Object|Participant[])} stateful - The redux state
132
- * features/base/participants, the (whole) redux state, or redux's
144
+ * @param {(Function|Object)} stateful - The (whole) redux state, or redux's
133 145
  * {@code getState} function to be used to retrieve the state
134 146
  * features/base/participants.
135 147
  * @returns {number}
136 148
  */
137 149
 export function getParticipantCount(stateful: Object | Function) {
138
-    return getParticipants(stateful).length;
150
+    const state = toState(stateful)['features/base/participants'];
151
+    const { local, remote, fakeParticipants } = state;
152
+
153
+    return remote.size - fakeParticipants.size + (local ? 1 : 0);
154
+}
155
+
156
+/**
157
+ * Returns the Map with fake participants.
158
+ *
159
+ * @param {(Function|Object)} stateful - The (whole) redux state, or redux's
160
+ * {@code getState} function to be used to retrieve the state
161
+ * features/base/participants.
162
+ * @returns {Map<string, Participant>} - The Map with fake participants.
163
+ */
164
+export function getFakeParticipants(stateful: Object | Function) {
165
+    return toState(stateful)['features/base/participants'].fakeParticipants;
166
+}
167
+
168
+/**
169
+ * Returns a count of the known remote participants in the passed in redux state.
170
+ *
171
+ * @param {(Function|Object)} stateful - The (whole) redux state, or redux's
172
+ * {@code getState} function to be used to retrieve the state
173
+ * features/base/participants.
174
+ * @returns {number}
175
+ */
176
+export function getRemoteParticipantCount(stateful: Object | Function) {
177
+    const state = toState(stateful)['features/base/participants'];
178
+
179
+    return state.remote.size;
139 180
 }
140 181
 
141 182
 /**
142 183
  * Returns a count of the known participants in the passed in redux state,
143 184
  * including fake participants.
144 185
  *
145
- * @param {(Function|Object|Participant[])} stateful - The redux state
146
- * features/base/participants, the (whole) redux state, or redux's
186
+ * @param {(Function|Object)} stateful - The (whole) redux state, or redux's
147 187
  * {@code getState} function to be used to retrieve the state
148 188
  * features/base/participants.
149 189
  * @returns {number}
150 190
  */
151 191
 export function getParticipantCountWithFake(stateful: Object | Function) {
152
-    return _getAllParticipants(stateful).length;
192
+    const state = toState(stateful)['features/base/participants'];
193
+    const { local, remote } = state;
194
+
195
+    return remote.size + (local ? 1 : 0);
153 196
 }
154 197
 
155 198
 /**
@@ -185,17 +228,6 @@ export function getParticipantDisplayName(
185 228
         : 'Fellow Jitster';
186 229
 }
187 230
 
188
-/**
189
- * Curried version of getParticipantDisplayName.
190
- *
191
- * @see {@link getParticipantDisplayName}
192
- * @param {string} id - The ID of the participant's display name to retrieve.
193
- * @returns {Function}
194
- */
195
-export const getParticipantDisplayNameWithId = (id: string) =>
196
-    (state: Object | Function) =>
197
-        getParticipantDisplayName(state, id);
198
-
199 231
 /**
200 232
  * Returns the presence status of a participant associated with the passed id.
201 233
  *
@@ -219,64 +251,45 @@ export function getParticipantPresenceStatus(
219 251
 }
220 252
 
221 253
 /**
222
- * Selectors for getting all known participants with fake participants filtered
223
- * out.
254
+ * Returns true if there is at least 1 participant with screen sharing feature and false otherwise.
224 255
  *
225
- * @param {(Function|Object|Participant[])} stateful - The redux state
226
- * features/base/participants, the (whole) redux state, or redux's
227
- * {@code getState} function to be used to retrieve the state
228
- * features/base/participants.
229
- * @returns {Participant[]}
256
+ * @param {(Function|Object)} stateful - The (whole) redux state, or redux's
257
+ * {@code getState} function to be used to retrieve the state.
258
+ * @returns {boolean}
230 259
  */
231
-export function getParticipants(stateful: Object | Function) {
232
-    return _getAllParticipants(stateful).filter(p => !p.isFakeParticipant);
260
+export function haveParticipantWithScreenSharingFeature(stateful: Object | Function) {
261
+    return toState(stateful)['features/base/participants'].haveParticipantWithScreenSharingFeature;
233 262
 }
234 263
 
235 264
 /**
236
- * Returns the participant which has its pinned state set to truthy.
265
+ * Selectors for getting all remote participants.
237 266
  *
238
- * @param {(Function|Object|Participant[])} stateful - The redux state
239
- * features/base/participants, the (whole) redux state, or redux's
267
+ * @param {(Function|Object)} stateful - The (whole) redux state, or redux's
240 268
  * {@code getState} function to be used to retrieve the state
241 269
  * features/base/participants.
242
- * @returns {(Participant|undefined)}
270
+ * @returns {Map<string, Object>}
243 271
  */
244
-export function getPinnedParticipant(stateful: Object | Function) {
245
-    return _getAllParticipants(stateful).find(p => p.pinned);
272
+export function getRemoteParticipants(stateful: Object | Function) {
273
+    return toState(stateful)['features/base/participants'].remote;
246 274
 }
247 275
 
248 276
 /**
249
- * Returns array of participants from Redux state.
277
+ * Returns the participant which has its pinned state set to truthy.
250 278
  *
251
- * @param {(Function|Object|Participant[])} stateful - The redux state
252
- * features/base/participants, the (whole) redux state, or redux's
279
+ * @param {(Function|Object)} stateful - The (whole) redux state, or redux's
253 280
  * {@code getState} function to be used to retrieve the state
254 281
  * features/base/participants.
255
- * @private
256
- * @returns {Participant[]}
282
+ * @returns {(Participant|undefined)}
257 283
  */
258
-function _getAllParticipants(stateful) {
259
-    return (
260
-        Array.isArray(stateful)
261
-            ? stateful
262
-            : toState(stateful)['features/base/participants'] || []);
263
-}
284
+export function getPinnedParticipant(stateful: Object | Function) {
285
+    const state = toState(stateful)['features/base/participants'];
286
+    const { pinnedParticipant } = state;
264 287
 
265
-/**
266
- * Returns the youtube fake participant.
267
- * At the moment it is considered the youtube participant the only fake participant in the list.
268
- *
269
- * @param {(Function|Object|Participant[])} stateful - The redux state
270
- * features/base/participants, the (whole) redux state, or redux's
271
- * {@code getState} function to be used to retrieve the state
272
- * features/base/participants.
273
- * @private
274
- * @returns {Participant}
275
- */
276
-export function getYoutubeParticipant(stateful: Object | Function) {
277
-    const participants = _getAllParticipants(stateful);
288
+    if (!pinnedParticipant) {
289
+        return undefined;
290
+    }
278 291
 
279
-    return participants.filter(p => p.isFakeParticipant)[0];
292
+    return getParticipantById(stateful, pinnedParticipant);
280 293
 }
281 294
 
282 295
 /**
@@ -289,6 +302,24 @@ export function isParticipantModerator(participant: Object) {
289 302
     return participant?.role === PARTICIPANT_ROLE.MODERATOR;
290 303
 }
291 304
 
305
+/**
306
+ * Returns the dominant speaker participant.
307
+ *
308
+ * @param {(Function|Object)} stateful - The (whole) redux state or redux's
309
+ * {@code getState} function to be used to retrieve the state features/base/participants.
310
+ * @returns {Participant} - The participant from the redux store.
311
+ */
312
+export function getDominantSpeakerParticipant(stateful: Object | Function) {
313
+    const state = toState(stateful)['features/base/participants'];
314
+    const { dominantSpeaker } = state;
315
+
316
+    if (!dominantSpeaker) {
317
+        return undefined;
318
+    }
319
+
320
+    return getParticipantById(stateful, dominantSpeaker);
321
+}
322
+
292 323
 /**
293 324
  * Returns true if all of the meeting participants are moderators.
294 325
  *
@@ -297,9 +328,9 @@ export function isParticipantModerator(participant: Object) {
297 328
  * @returns {boolean}
298 329
  */
299 330
 export function isEveryoneModerator(stateful: Object | Function) {
300
-    const participants = _getAllParticipants(stateful);
331
+    const state = toState(stateful)['features/base/participants'];
301 332
 
302
-    return participants.every(isParticipantModerator);
333
+    return state.everyoneIsModerator === true;
303 334
 }
304 335
 
305 336
 /**
@@ -321,14 +352,15 @@ export function isIconUrl(icon: ?string | ?Object) {
321 352
  * @returns {boolean}
322 353
  */
323 354
 export function isLocalParticipantModerator(stateful: Object | Function) {
324
-    const state = toState(stateful);
325
-    const localParticipant = getLocalParticipant(state);
355
+    const state = toState(stateful)['features/base/participants'];
356
+
357
+    const { local } = state;
326 358
 
327
-    if (!localParticipant) {
359
+    if (!local) {
328 360
         return false;
329 361
     }
330 362
 
331
-    return isParticipantModerator(localParticipant);
363
+    return isParticipantModerator(local);
332 364
 }
333 365
 
334 366
 /**
@@ -390,7 +422,7 @@ async function _getFirstLoadableAvatarUrl(participant, store) {
390 422
     for (let i = 0; i < AVATAR_CHECKER_FUNCTIONS.length; i++) {
391 423
         const url = AVATAR_CHECKER_FUNCTIONS[i](participant, store);
392 424
 
393
-        if (url) {
425
+        if (url !== null) {
394 426
             if (AVATAR_CHECKED_URLS.has(url)) {
395 427
                 if (AVATAR_CHECKED_URLS.get(url)) {
396 428
                     return url;

+ 10
- 6
react/features/base/participants/middleware.js 查看文件

@@ -1,5 +1,7 @@
1 1
 // @flow
2 2
 
3
+import { batch } from 'react-redux';
4
+
3 5
 import UIEvents from '../../../../service/UI/UIEvents';
4 6
 import { toggleE2EE } from '../../e2ee/actions';
5 7
 import { NOTIFICATION_TIMEOUT, showNotification } from '../../notifications';
@@ -43,7 +45,8 @@ import {
43 45
     getLocalParticipant,
44 46
     getParticipantById,
45 47
     getParticipantCount,
46
-    getParticipantDisplayName
48
+    getParticipantDisplayName,
49
+    getRemoteParticipants
47 50
 } from './functions';
48 51
 import { PARTICIPANT_JOINED_FILE, PARTICIPANT_LEFT_FILE } from './sounds';
49 52
 
@@ -182,11 +185,12 @@ MiddlewareRegistry.register(store => next => action => {
182 185
 StateListenerRegistry.register(
183 186
     /* selector */ state => getCurrentConference(state),
184 187
     /* listener */ (conference, { dispatch, getState }) => {
185
-        for (const p of getState()['features/base/participants']) {
186
-            !p.local
187
-                && (!conference || p.conference !== conference)
188
-                && dispatch(participantLeft(p.id, p.conference, p.isReplaced));
189
-        }
188
+        batch(() => {
189
+            for (const [ id, p ] of getRemoteParticipants(getState())) {
190
+                (!conference || p.conference !== conference)
191
+                    && dispatch(participantLeft(id, p.conference, p.isReplaced));
192
+            }
193
+        });
190 194
     });
191 195
 
192 196
 /**

+ 263
- 59
react/features/base/participants/reducer.js 查看文件

@@ -12,6 +12,7 @@ import {
12 12
     SET_LOADABLE_AVATAR_URL
13 13
 } from './actionTypes';
14 14
 import { LOCAL_PARTICIPANT_DEFAULT_ID, PARTICIPANT_ROLE } from './constants';
15
+import { isParticipantModerator } from './functions';
15 16
 
16 17
 /**
17 18
  * Participant object.
@@ -51,6 +52,16 @@ const PARTICIPANT_PROPS_TO_OMIT_WHEN_UPDATE = [
51 52
     'pinned'
52 53
 ];
53 54
 
55
+const DEFAULT_STATE = {
56
+    haveParticipantWithScreenSharingFeature: false,
57
+    dominantSpeaker: undefined,
58
+    everyoneIsModerator: false,
59
+    pinnedParticipant: undefined,
60
+    local: undefined,
61
+    remote: new Map(),
62
+    fakeParticipants: new Map()
63
+};
64
+
54 65
 /**
55 66
  * Listen for actions which add, remove, or update the set of participants in
56 67
  * the conference.
@@ -62,18 +73,157 @@ const PARTICIPANT_PROPS_TO_OMIT_WHEN_UPDATE = [
62 73
  * added/removed/modified.
63 74
  * @returns {Participant[]}
64 75
  */
65
-ReducerRegistry.register('features/base/participants', (state = [], action) => {
76
+ReducerRegistry.register('features/base/participants', (state = DEFAULT_STATE, action) => {
66 77
     switch (action.type) {
78
+    case PARTICIPANT_ID_CHANGED: {
79
+        const { local } = state;
80
+
81
+        if (local) {
82
+            state.local = {
83
+                ...local,
84
+                id: action.newValue
85
+            };
86
+
87
+            return {
88
+                ...state
89
+            };
90
+        }
91
+
92
+        return state;
93
+    }
94
+    case DOMINANT_SPEAKER_CHANGED: {
95
+        const { participant } = action;
96
+        const { id } = participant;
97
+        const { dominantSpeaker } = state;
98
+
99
+        // Only one dominant speaker is allowed.
100
+        if (dominantSpeaker) {
101
+            _updateParticipantProperty(state, dominantSpeaker, 'dominantSpeaker', false);
102
+        }
103
+
104
+        if (_updateParticipantProperty(state, id, 'dominantSpeaker', true)) {
105
+            return {
106
+                ...state,
107
+                dominantSpeaker: id
108
+            };
109
+        }
110
+
111
+        delete state.dominantSpeaker;
112
+
113
+        return {
114
+            ...state
115
+        };
116
+    }
117
+    case PIN_PARTICIPANT: {
118
+        const { participant } = action;
119
+        const { id } = participant;
120
+        const { pinnedParticipant } = state;
121
+
122
+        // Only one pinned participant is allowed.
123
+        if (pinnedParticipant) {
124
+            _updateParticipantProperty(state, pinnedParticipant, 'pinned', false);
125
+        }
126
+
127
+        if (_updateParticipantProperty(state, id, 'pinned', true)) {
128
+            return {
129
+                ...state,
130
+                pinnedParticipant: id
131
+            };
132
+        }
133
+
134
+        delete state.pinnedParticipant;
135
+
136
+        return {
137
+            ...state
138
+        };
139
+    }
67 140
     case SET_LOADABLE_AVATAR_URL:
68
-    case DOMINANT_SPEAKER_CHANGED:
69
-    case PARTICIPANT_ID_CHANGED:
70
-    case PARTICIPANT_UPDATED:
71
-    case PIN_PARTICIPANT:
72
-        return state.map(p => _participant(p, action));
141
+    case PARTICIPANT_UPDATED: {
142
+        const { participant } = action;
143
+        let { id } = participant;
144
+        const { local } = participant;
145
+
146
+        if (!id && local) {
147
+            id = LOCAL_PARTICIPANT_DEFAULT_ID;
148
+        }
149
+
150
+        let newParticipant;
151
+
152
+        if (state.remote.has(id)) {
153
+            newParticipant = _participant(state.remote.get(id), action);
154
+            state.remote.set(id, newParticipant);
155
+        } else if (id === state.local?.id) {
156
+            newParticipant = state.local = _participant(state.local, action);
157
+        }
158
+
159
+        if (newParticipant) {
160
+
161
+            // everyoneIsModerator calculation:
162
+            const isModerator = isParticipantModerator(newParticipant);
163
+
164
+            if (state.everyoneIsModerator && !isModerator) {
165
+                state.everyoneIsModerator = false;
166
+            } else if (!state.everyoneIsModerator && isModerator) {
167
+                state.everyoneIsModerator = _isEveryoneModerator(state);
168
+            }
169
+
170
+            // haveParticipantWithScreenSharingFeature calculation:
171
+            const { features = {} } = participant;
172
+
173
+            // Currently we use only PARTICIPANT_UPDATED to set a feature to enabled and we never disable it.
174
+            if (String(features['screen-sharing']) === 'true') {
175
+                state.haveParticipantWithScreenSharingFeature = true;
176
+            }
177
+        }
178
+
179
+        return {
180
+            ...state
181
+        };
182
+    }
183
+    case PARTICIPANT_JOINED: {
184
+        const participant = _participantJoined(action);
185
+        const { pinnedParticipant, dominantSpeaker } = state;
186
+
187
+        if (participant.pinned) {
188
+            if (pinnedParticipant) {
189
+                _updateParticipantProperty(state, pinnedParticipant, 'pinned', false);
190
+            }
191
+
192
+            state.pinnedParticipant = participant.id;
193
+        }
194
+
195
+        if (participant.dominantSpeaker) {
196
+            if (dominantSpeaker) {
197
+                _updateParticipantProperty(state, dominantSpeaker, 'dominantSpeaker', false);
198
+            }
199
+            state.dominantSpeaker = participant.id;
200
+        }
201
+
202
+        const isModerator = isParticipantModerator(participant);
203
+        const { local, remote } = state;
204
+
205
+        if (state.everyoneIsModerator && !isModerator) {
206
+            state.everyoneIsModerator = false;
207
+        } else if (!local && remote.size === 0 && isModerator) {
208
+            state.everyoneIsModerator = true;
209
+        }
210
+
211
+        if (participant.local) {
212
+            return {
213
+                ...state,
214
+                local: participant
215
+            };
216
+        }
217
+
218
+        state.remote.set(participant.id, participant);
73 219
 
74
-    case PARTICIPANT_JOINED:
75
-        return [ ...state, _participantJoined(action) ];
220
+        if (participant.isFakeParticipant) {
221
+            state.fakeParticipants.set(participant.id, participant);
222
+        }
76 223
 
224
+        return { ...state };
225
+
226
+    }
77 227
     case PARTICIPANT_LEFT: {
78 228
         // XXX A remote participant is uniquely identified by their id in a
79 229
         // specific JitsiConference instance. The local participant is uniquely
@@ -81,23 +231,111 @@ ReducerRegistry.register('features/base/participants', (state = [], action) => {
81 231
         // (and the fact that the local participant "joins" at the beginning of
82 232
         // the app and "leaves" at the end of the app).
83 233
         const { conference, id } = action.participant;
234
+        const { fakeParticipants, remote, local, dominantSpeaker, pinnedParticipant } = state;
235
+        let oldParticipant = remote.get(id);
236
+
237
+        if (oldParticipant && oldParticipant.conference === conference) {
238
+            remote.delete(id);
239
+        } else if (local?.id === id) {
240
+            oldParticipant = state.local;
241
+            delete state.local;
242
+        } else {
243
+            // no participant found
244
+            return state;
245
+        }
246
+
247
+        if (!state.everyoneIsModerator && !isParticipantModerator(oldParticipant)) {
248
+            state.everyoneIsModerator = _isEveryoneModerator(state);
249
+        }
84 250
 
85
-        return state.filter(p =>
86
-            !(
87
-                p.id === id
251
+        const { features = {} } = oldParticipant || {};
88 252
 
89
-                    // XXX Do not allow collisions in the IDs of the local
90
-                    // participant and a remote participant cause the removal of
91
-                    // the local participant when the remote participant's
92
-                    // removal is requested.
93
-                    && p.conference === conference
94
-                    && (conference || p.local)));
253
+        if (state.haveParticipantWithScreenSharingFeature && String(features['screen-sharing']) === 'true') {
254
+            const { features: localFeatures = {} } = state.local || {};
255
+
256
+            if (String(localFeatures['screen-sharing']) !== 'true') {
257
+                state.haveParticipantWithScreenSharingFeature = false;
258
+
259
+                // eslint-disable-next-line no-unused-vars
260
+                for (const [ key, participant ] of state.remote) {
261
+                    const { features: f = {} } = participant;
262
+
263
+                    if (String(f['screen-sharing']) === 'true') {
264
+                        state.haveParticipantWithScreenSharingFeature = true;
265
+                        break;
266
+                    }
267
+                }
268
+            }
269
+
270
+
271
+        }
272
+
273
+        if (dominantSpeaker === id) {
274
+            state.dominantSpeaker = undefined;
275
+        }
276
+
277
+        if (pinnedParticipant === id) {
278
+            state.pinnedParticipant = undefined;
279
+        }
280
+
281
+        if (fakeParticipants.has(id)) {
282
+            fakeParticipants.delete(id);
283
+        }
284
+
285
+        return { ...state };
95 286
     }
96 287
     }
97 288
 
98 289
     return state;
99 290
 });
100 291
 
292
+/**
293
+ * Loops trough the participants in the state in order to check if all participants are moderators.
294
+ *
295
+ * @param {Object} state - The local participant redux state.
296
+ * @returns {boolean}
297
+ */
298
+function _isEveryoneModerator(state) {
299
+    if (isParticipantModerator(state.local)) {
300
+        // eslint-disable-next-line no-unused-vars
301
+        for (const [ k, p ] of state.remote) {
302
+            if (!isParticipantModerator(p)) {
303
+                return false;
304
+            }
305
+        }
306
+
307
+        return true;
308
+    }
309
+
310
+    return false;
311
+}
312
+
313
+
314
+/**
315
+ * Updates a specific property for a participant.
316
+ *
317
+ * @param {State} state - The redux state.
318
+ * @param {string} id - The ID of the participant.
319
+ * @param {string} property - The property to update.
320
+ * @param {*} value - The new value.
321
+ * @returns {boolean} - True if a participant was updated and false otherwise.
322
+ */
323
+function _updateParticipantProperty(state, id, property, value) {
324
+    const { remote, local } = state;
325
+
326
+    if (remote.has(id)) {
327
+        remote.set(id, set(remote.get(id), property, value));
328
+
329
+        return true;
330
+    } else if (local?.id === id) {
331
+        state.local = set(local, property, value);
332
+
333
+        return true;
334
+    }
335
+
336
+    return false;
337
+}
338
+
101 339
 /**
102 340
  * Reducer function for a single participant.
103 341
  *
@@ -112,56 +350,22 @@ ReducerRegistry.register('features/base/participants', (state = [], action) => {
112 350
  */
113 351
 function _participant(state: Object = {}, action) {
114 352
     switch (action.type) {
115
-    case DOMINANT_SPEAKER_CHANGED:
116
-        // Only one dominant speaker is allowed.
117
-        return (
118
-            set(state, 'dominantSpeaker', state.id === action.participant.id));
119
-
120
-    case PARTICIPANT_ID_CHANGED: {
121
-        // A participant is identified by an id-conference pair. Only the local
122
-        // participant is with an undefined conference.
123
-        const { conference } = action;
124
-
125
-        if (state.id === action.oldValue
126
-                && state.conference === conference
127
-                && (conference || state.local)) {
128
-            return {
129
-                ...state,
130
-                id: action.newValue
131
-            };
132
-        }
133
-        break;
134
-    }
135
-
136 353
     case SET_LOADABLE_AVATAR_URL:
137 354
     case PARTICIPANT_UPDATED: {
138 355
         const { participant } = action; // eslint-disable-line no-shadow
139
-        let { id } = participant;
140
-        const { local } = participant;
141 356
 
142
-        if (!id && local) {
143
-            id = LOCAL_PARTICIPANT_DEFAULT_ID;
144
-        }
145
-
146
-        if (state.id === id) {
147
-            const newState = { ...state };
357
+        const newState = { ...state };
148 358
 
149
-            for (const key in participant) {
150
-                if (participant.hasOwnProperty(key)
151
-                        && PARTICIPANT_PROPS_TO_OMIT_WHEN_UPDATE.indexOf(key)
152
-                            === -1) {
153
-                    newState[key] = participant[key];
154
-                }
359
+        for (const key in participant) {
360
+            if (participant.hasOwnProperty(key)
361
+                    && PARTICIPANT_PROPS_TO_OMIT_WHEN_UPDATE.indexOf(key)
362
+                        === -1) {
363
+                newState[key] = participant[key];
155 364
             }
156
-
157
-            return newState;
158 365
         }
159
-        break;
160
-    }
161 366
 
162
-    case PIN_PARTICIPANT:
163
-        // Currently, only one pinned participant is allowed.
164
-        return set(state, 'pinned', state.id === action.participant.id);
367
+        return newState;
368
+    }
165 369
     }
166 370
 
167 371
     return state;

+ 27
- 45
react/features/base/tracks/functions.js 查看文件

@@ -21,68 +21,50 @@ import logger from './logger';
21 21
 export const getTrackState = state => state['features/base/tracks'];
22 22
 
23 23
 /**
24
- * Higher-order function that returns a selector for a specific participant
25
- * and media type.
24
+ * Checks if the passed media type is muted for the participant.
26 25
  *
27 26
  * @param {Object} participant - Participant reference.
28 27
  * @param {MEDIA_TYPE} mediaType - Media type.
29
- * @returns {Function} Selector.
28
+ * @param {Object} state - Global state.
29
+ * @returns {boolean} - Is the media type muted for the participant.
30 30
  */
31
-export const getIsParticipantMediaMuted = (participant, mediaType) =>
32
-
33
-    /**
34
-     * Bound selector.
35
-     *
36
-     * @param {Object} state - Global state.
37
-     * @returns {boolean} Is the media type muted for the participant.
38
-     */
39
-    state => {
40
-        if (!participant) {
41
-            return;
42
-        }
31
+export function isParticipantMediaMuted(participant, mediaType, state) {
32
+    if (!participant) {
33
+        return false;
34
+    }
43 35
 
44
-        const tracks = getTrackState(state);
36
+    const tracks = getTrackState(state);
45 37
 
46
-        if (participant?.local) {
47
-            return isLocalTrackMuted(tracks, mediaType);
48
-        } else if (!participant?.isFakeParticipant) {
49
-            return isRemoteTrackMuted(tracks, mediaType, participant.id);
50
-        }
38
+    if (participant?.local) {
39
+        return isLocalTrackMuted(tracks, mediaType);
40
+    } else if (!participant?.isFakeParticipant) {
41
+        return isRemoteTrackMuted(tracks, mediaType, participant.id);
42
+    }
51 43
 
52
-        return true;
53
-    };
44
+    return true;
45
+}
54 46
 
55 47
 /**
56
- * Higher-order function that returns a selector for a specific participant.
48
+ * Checks if the participant is audio muted.
57 49
  *
58 50
  * @param {Object} participant - Participant reference.
59
- * @returns {Function} Selector.
51
+ * @param {Object} state - Global state.
52
+ * @returns {boolean} - Is audio muted for the participant.
60 53
  */
61
-export const getIsParticipantAudioMuted = participant =>
62
-
63
-    /**
64
-     * Bound selector.
65
-     *
66
-     * @param {Object} state - Global state.
67
-     * @returns {boolean} Is audio muted for the participant.
68
-     */
69
-    state => getIsParticipantMediaMuted(participant, MEDIA_TYPE.AUDIO)(state);
54
+export function isParticipantAudioMuted(participant, state) {
55
+    return isParticipantMediaMuted(participant, MEDIA_TYPE.AUDIO, state);
56
+}
70 57
 
71 58
 /**
72
- * Higher-order function that returns a selector for a specific participant.
59
+ * Checks if the participant is video muted.
73 60
  *
74 61
  * @param {Object} participant - Participant reference.
75
- * @returns {Function} Selector.
62
+ * @param {Object} state - Global state.
63
+ * @returns {boolean} - Is video muted for the participant.
76 64
  */
77
-export const getIsParticipantVideoMuted = participant =>
78
-
79
-    /**
80
-     * Bound selector.
81
-     *
82
-     * @param {Object} state - Global state.
83
-     * @returns {boolean} Is video muted for the participant.
84
-     */
85
-    state => getIsParticipantMediaMuted(participant, MEDIA_TYPE.VIDEO)(state);
65
+export function isParticipantVideoMuted(participant, state) {
66
+    return isParticipantMediaMuted(participant, MEDIA_TYPE.VIDEO, state);
67
+}
86 68
 
87 69
 /**
88 70
  * Creates a local video track for presenter. The constraints are computed based

+ 1
- 1
react/features/chat/components/AbstractChat.js 查看文件

@@ -108,6 +108,6 @@ export function _mapStateToProps(state: Object) {
108 108
         _isModal: window.innerWidth <= SMALL_WIDTH_THRESHOLD,
109 109
         _isOpen: isOpen,
110 110
         _messages: messages,
111
-        _showNamePrompt: !_localParticipant.name
111
+        _showNamePrompt: !_localParticipant?.name
112 112
     };
113 113
 }

+ 1
- 2
react/features/display-name/components/web/DisplayName.js 查看文件

@@ -288,8 +288,7 @@ function _mapStateToProps(state, ownProps) {
288 288
 
289 289
     return {
290 290
         _configuredDisplayName: participant && participant.name,
291
-        _nameToDisplay: getParticipantDisplayName(
292
-            state, participantID)
291
+        _nameToDisplay: getParticipantDisplayName(state, participantID)
293 292
     };
294 293
 }
295 294
 

+ 19
- 0
react/features/e2ee/actionTypes.js 查看文件

@@ -6,3 +6,22 @@
6 6
  * }
7 7
  */
8 8
 export const TOGGLE_E2EE = 'TOGGLE_E2EE';
9
+
10
+/**
11
+ * The type of the action which signals to set new value whether everyone has E2EE enabled.
12
+ *
13
+ * {
14
+ *     type: SET_EVERYONE_ENABLED_E2EE,
15
+ *     everyoneEnabledE2EE: boolean
16
+ * }
17
+ */
18
+export const SET_EVERYONE_ENABLED_E2EE = 'SET_EVERYONE_ENABLED_E2EE';
19
+
20
+/**
21
+ * The type of the action which signals to set new value whether everyone supports E2EE.
22
+ *
23
+ * {
24
+ *     type: SET_EVERYONE_SUPPORT_E2EE
25
+ * }
26
+ */
27
+export const SET_EVERYONE_SUPPORT_E2EE = 'SET_EVERYONE_SUPPORT_E2EE';

+ 33
- 1
react/features/e2ee/actions.js 查看文件

@@ -1,6 +1,6 @@
1 1
 // @flow
2 2
 
3
-import { TOGGLE_E2EE } from './actionTypes';
3
+import { SET_EVERYONE_ENABLED_E2EE, SET_EVERYONE_SUPPORT_E2EE, TOGGLE_E2EE } from './actionTypes';
4 4
 
5 5
 /**
6 6
  * Dispatches an action to enable / disable E2EE.
@@ -14,3 +14,35 @@ export function toggleE2EE(enabled: boolean) {
14 14
         enabled
15 15
     };
16 16
 }
17
+
18
+/**
19
+ * Set new value whether everyone has E2EE enabled.
20
+ *
21
+ * @param {boolean} everyoneEnabledE2EE - The new value.
22
+ * @returns {{
23
+ *     type: SET_EVERYONE_ENABLED_E2EE,
24
+ *     everyoneEnabledE2EE: boolean
25
+ * }}
26
+ */
27
+export function setEveryoneEnabledE2EE(everyoneEnabledE2EE: boolean) {
28
+    return {
29
+        type: SET_EVERYONE_ENABLED_E2EE,
30
+        everyoneEnabledE2EE
31
+    };
32
+}
33
+
34
+/**
35
+ * Set new value whether everyone support E2EE.
36
+ *
37
+ * @param {boolean} everyoneSupportE2EE - The new value.
38
+ * @returns {{
39
+ *     type: SET_EVERYONE_SUPPORT_E2EE,
40
+ *     everyoneSupportE2EE: boolean
41
+ * }}
42
+ */
43
+export function setEveryoneSupportE2EE(everyoneSupportE2EE: boolean) {
44
+    return {
45
+        type: SET_EVERYONE_SUPPORT_E2EE,
46
+        everyoneSupportE2EE
47
+    };
48
+}

+ 1
- 3
react/features/e2ee/components/AbstractE2EELabel.js 查看文件

@@ -22,9 +22,7 @@ export type Props = {
22 22
  * @returns {Props}
23 23
  */
24 24
 export function _mapStateToProps(state: Object) {
25
-    const participants = state['features/base/participants'];
26
-
27 25
     return {
28
-        _showLabel: participants.every(p => p.e2eeEnabled)
26
+        _showLabel: state['features/e2ee'].everyoneEnabledE2EE
29 27
     };
30 28
 }

+ 5
- 7
react/features/e2ee/components/E2EESection.js 查看文件

@@ -5,7 +5,6 @@ import type { Dispatch } from 'redux';
5 5
 
6 6
 import { createE2EEEvent, sendAnalytics } from '../../analytics';
7 7
 import { translate } from '../../base/i18n';
8
-import { getParticipants } from '../../base/participants';
9 8
 import { Switch } from '../../base/react';
10 9
 import { connect } from '../../base/redux';
11 10
 import { toggleE2EE } from '../actions';
@@ -21,7 +20,7 @@ type Props = {
21 20
     /**
22 21
      * Indicates whether all participants in the conference currently support E2EE.
23 22
      */
24
-    _everyoneSupportsE2EE: boolean,
23
+    _everyoneSupportE2EE: boolean,
25 24
 
26 25
     /**
27 26
      * The redux {@code dispatch} function.
@@ -96,7 +95,7 @@ class E2EESection extends Component<Props, State> {
96 95
      * @returns {ReactElement}
97 96
      */
98 97
     render() {
99
-        const { _everyoneSupportsE2EE, t } = this.props;
98
+        const { _everyoneSupportE2EE, t } = this.props;
100 99
         const { enabled, expand } = this.state;
101 100
         const description = t('dialog.e2eeDescription');
102 101
 
@@ -120,7 +119,7 @@ class E2EESection extends Component<Props, State> {
120 119
                     </span> }
121 120
                 </p>
122 121
                 {
123
-                    !_everyoneSupportsE2EE
122
+                    !_everyoneSupportE2EE
124 123
                         && <span className = 'warning'>
125 124
                             { t('dialog.e2eeWarning') }
126 125
                         </span>
@@ -195,12 +194,11 @@ class E2EESection extends Component<Props, State> {
195 194
  * @returns {Props}
196 195
  */
197 196
 function mapStateToProps(state) {
198
-    const { enabled } = state['features/e2ee'];
199
-    const participants = getParticipants(state).filter(p => !p.local);
197
+    const { enabled, everyoneSupportE2EE } = state['features/e2ee'];
200 198
 
201 199
     return {
202 200
         _enabled: enabled,
203
-        _everyoneSupportsE2EE: participants.every(p => Boolean(p.e2eeSupported))
201
+        _everyoneSupportE2EE: everyoneSupportE2EE
204 202
     };
205 203
 }
206 204
 

+ 135
- 2
react/features/e2ee/middleware.js 查看文件

@@ -1,13 +1,24 @@
1 1
 // @flow
2 2
 
3
+import { batch } from 'react-redux';
4
+
3 5
 import { APP_WILL_MOUNT, APP_WILL_UNMOUNT } from '../base/app';
4 6
 import { getCurrentConference } from '../base/conference';
5
-import { getLocalParticipant, participantUpdated } from '../base/participants';
7
+import {
8
+    getLocalParticipant,
9
+    getParticipantById,
10
+    getParticipantCount,
11
+    PARTICIPANT_JOINED,
12
+    PARTICIPANT_LEFT,
13
+    PARTICIPANT_UPDATED,
14
+    participantUpdated,
15
+    getRemoteParticipants
16
+} from '../base/participants';
6 17
 import { MiddlewareRegistry, StateListenerRegistry } from '../base/redux';
7 18
 import { playSound, registerSound, unregisterSound } from '../base/sounds';
8 19
 
9 20
 import { TOGGLE_E2EE } from './actionTypes';
10
-import { toggleE2EE } from './actions';
21
+import { setEveryoneEnabledE2EE, setEveryoneSupportE2EE, toggleE2EE } from './actions';
11 22
 import { E2EE_OFF_SOUND_ID, E2EE_ON_SOUND_ID } from './constants';
12 23
 import logger from './logger';
13 24
 import { E2EE_OFF_SOUND_FILE, E2EE_ON_SOUND_FILE } from './sounds';
@@ -35,6 +46,128 @@ MiddlewareRegistry.register(({ dispatch, getState }) => next => action => {
35 46
         dispatch(unregisterSound(E2EE_ON_SOUND_ID));
36 47
         break;
37 48
 
49
+    case PARTICIPANT_UPDATED: {
50
+        const { id, e2eeEnabled, e2eeSupported } = action.participant;
51
+        const oldParticipant = getParticipantById(getState(), id);
52
+        const result = next(action);
53
+
54
+        if (e2eeEnabled !== oldParticipant?.e2eeEnabled
55
+            || e2eeSupported !== oldParticipant?.e2eeSupported) {
56
+            const state = getState();
57
+            let newEveryoneSupportE2EE = true;
58
+            let newEveryoneEnabledE2EE = true;
59
+
60
+            // eslint-disable-next-line no-unused-vars
61
+            for (const [ key, p ] of getRemoteParticipants(state)) {
62
+                if (!p.e2eeEnabled) {
63
+                    newEveryoneEnabledE2EE = false;
64
+                }
65
+
66
+                if (!p.e2eeSupported) {
67
+                    newEveryoneSupportE2EE = false;
68
+                }
69
+
70
+                if (!newEveryoneEnabledE2EE && !newEveryoneSupportE2EE) {
71
+                    break;
72
+                }
73
+            }
74
+
75
+            if (!getLocalParticipant(state)?.e2eeEnabled) {
76
+                newEveryoneEnabledE2EE = false;
77
+            }
78
+
79
+            batch(() => {
80
+                dispatch(setEveryoneEnabledE2EE(newEveryoneEnabledE2EE));
81
+                dispatch(setEveryoneSupportE2EE(newEveryoneSupportE2EE));
82
+            });
83
+        }
84
+
85
+        return result;
86
+    }
87
+    case PARTICIPANT_JOINED: {
88
+        const result = next(action);
89
+        const { e2eeEnabled, e2eeSupported, local } = action.participant;
90
+        const { everyoneEnabledE2EE } = getState()['features/e2ee'];
91
+        const participantCount = getParticipantCount(getState());
92
+
93
+        // the initial values
94
+        if (participantCount === 1) {
95
+            batch(() => {
96
+                dispatch(setEveryoneEnabledE2EE(e2eeEnabled));
97
+                dispatch(setEveryoneSupportE2EE(e2eeSupported));
98
+            });
99
+        }
100
+
101
+        // if all had it enabled and this one disabled it, change value in store
102
+        // otherwise there is no change in the value we store
103
+        if (everyoneEnabledE2EE && !e2eeEnabled) {
104
+            dispatch(setEveryoneEnabledE2EE(false));
105
+        }
106
+
107
+        if (local) {
108
+            return result;
109
+        }
110
+
111
+        const { everyoneSupportE2EE } = getState()['features/e2ee'];
112
+
113
+        // if all supported it and this one does not, change value in store
114
+        // otherwise there is no change in the value we store
115
+        if (everyoneSupportE2EE && !e2eeSupported) {
116
+            dispatch(setEveryoneSupportE2EE(false));
117
+        }
118
+
119
+        return result;
120
+    }
121
+
122
+    case PARTICIPANT_LEFT: {
123
+        const previosState = getState();
124
+        const participant = getParticipantById(previosState, action.participant?.id) || {};
125
+        const result = next(action);
126
+        const newState = getState();
127
+        const { e2eeEnabled = false, e2eeSupported = false } = participant;
128
+
129
+        const { everyoneEnabledE2EE, everyoneSupportE2EE } = newState['features/e2ee'];
130
+
131
+
132
+        // if it was not enabled by everyone, and the participant leaving had it disabled, or if it was not supported
133
+        // by everyone, and the participant leaving had it not supported let's check is it enabled for all that stay
134
+        if ((!everyoneEnabledE2EE && !e2eeEnabled) || (!everyoneSupportE2EE && !e2eeSupported)) {
135
+            let latestEveryoneEnabledE2EE = true;
136
+            let latestEveryoneSupportE2EE = true;
137
+
138
+            // eslint-disable-next-line no-unused-vars
139
+            for (const [ key, p ] of getRemoteParticipants(newState)) {
140
+                if (!p.e2eeEnabled) {
141
+                    latestEveryoneEnabledE2EE = false;
142
+                }
143
+
144
+                if (!p.e2eeSupported) {
145
+                    latestEveryoneSupportE2EE = false;
146
+                }
147
+
148
+                if (!latestEveryoneEnabledE2EE && !latestEveryoneSupportE2EE) {
149
+                    break;
150
+                }
151
+            }
152
+
153
+            if (!getLocalParticipant(newState)?.e2eeEnabled) {
154
+                latestEveryoneEnabledE2EE = false;
155
+            }
156
+
157
+            batch(() => {
158
+                if (!everyoneEnabledE2EE && latestEveryoneEnabledE2EE) {
159
+                    dispatch(setEveryoneEnabledE2EE(true));
160
+                }
161
+
162
+                if (!everyoneSupportE2EE && latestEveryoneSupportE2EE) {
163
+                    dispatch(setEveryoneSupportE2EE(true));
164
+                }
165
+            });
166
+        }
167
+
168
+        return result;
169
+    }
170
+
38 171
     case TOGGLE_E2EE: {
39 172
         const conference = getCurrentConference(getState);
40 173
 

+ 15
- 1
react/features/e2ee/reducer.js 查看文件

@@ -2,7 +2,11 @@
2 2
 
3 3
 import { ReducerRegistry } from '../base/redux';
4 4
 
5
-import { TOGGLE_E2EE } from './actionTypes';
5
+import {
6
+    SET_EVERYONE_ENABLED_E2EE,
7
+    SET_EVERYONE_SUPPORT_E2EE,
8
+    TOGGLE_E2EE
9
+} from './actionTypes';
6 10
 
7 11
 const DEFAULT_STATE = {
8 12
     enabled: false
@@ -18,6 +22,16 @@ ReducerRegistry.register('features/e2ee', (state = DEFAULT_STATE, action) => {
18 22
             ...state,
19 23
             enabled: action.enabled
20 24
         };
25
+    case SET_EVERYONE_ENABLED_E2EE:
26
+        return {
27
+            ...state,
28
+            everyoneEnabledE2EE: action.everyoneEnabledE2EE
29
+        };
30
+    case SET_EVERYONE_SUPPORT_E2EE:
31
+        return {
32
+            ...state,
33
+            everyoneSupportE2EE: action.everyoneSupportE2EE
34
+        };
21 35
 
22 36
     default:
23 37
         return state;

+ 3
- 2
react/features/filmstrip/actions.web.js 查看文件

@@ -1,7 +1,7 @@
1 1
 // @flow
2 2
 import type { Dispatch } from 'redux';
3 3
 
4
-import { pinParticipant } from '../base/participants';
4
+import { getLocalParticipant, getRemoteParticipants, pinParticipant } from '../base/participants';
5 5
 
6 6
 import {
7 7
     SET_HORIZONTAL_VIEW_DIMENSIONS,
@@ -127,7 +127,8 @@ export function setHorizontalViewDimensions() {
127 127
  */
128 128
 export function clickOnVideo(n: number) {
129 129
     return (dispatch: Function, getState: Function) => {
130
-        const participants = getState()['features/base/participants'];
130
+        const state = getState();
131
+        const participants = [ getLocalParticipant(state), ...getRemoteParticipants(state).values() ];
131 132
         const nThParticipant = participants[n];
132 133
         const { id, pinned } = nThParticipant;
133 134
 

+ 5
- 6
react/features/filmstrip/components/native/Filmstrip.js 查看文件

@@ -109,10 +109,10 @@ class Filmstrip extends Component<Props> {
109 109
                     {
110 110
 
111 111
                         this._sort(_participants, isNarrowAspectRatio)
112
-                            .map(p => (
112
+                            .map(id => (
113 113
                                 <Thumbnail
114
-                                    key = { p.id }
115
-                                    participant = { p } />))
114
+                                    key = { id }
115
+                                    participantID = { id } />))
116 116
 
117 117
                     }
118 118
                     {
@@ -166,12 +166,11 @@ class Filmstrip extends Component<Props> {
166 166
  * @returns {Props}
167 167
  */
168 168
 function _mapStateToProps(state) {
169
-    const participants = state['features/base/participants'];
170
-    const { enabled } = state['features/filmstrip'];
169
+    const { enabled, remoteParticipants } = state['features/filmstrip'];
171 170
 
172 171
     return {
173 172
         _aspectRatio: state['features/base/responsive-ui'].aspectRatio,
174
-        _participants: participants.filter(p => !p.local),
173
+        _participants: remoteParticipants,
175 174
         _visible: enabled && isFilmstripVisible(state)
176 175
     };
177 176
 }

+ 8
- 50
react/features/filmstrip/components/native/LocalThumbnail.js 查看文件

@@ -1,63 +1,21 @@
1 1
 // @flow
2 2
 
3
-import React, { Component } from 'react';
3
+import React from 'react';
4 4
 import { View } from 'react-native';
5 5
 
6
-import { getLocalParticipant } from '../../../base/participants';
7
-import { connect } from '../../../base/redux';
8
-
9 6
 import Thumbnail from './Thumbnail';
10 7
 import styles from './styles';
11 8
 
12
-type Props = {
13
-
14
-    /**
15
-     * The local participant.
16
-     */
17
-    _localParticipant: Object
18
-};
19
-
20 9
 /**
21 10
  * Component to render a local thumbnail that can be separated from the
22 11
  * remote thumbnails later.
23
- */
24
-class LocalThumbnail extends Component<Props> {
25
-    /**
26
-     * Implements React Component's render.
27
-     *
28
-     * @inheritdoc
29
-     */
30
-    render() {
31
-        const { _localParticipant } = this.props;
32
-
33
-        return (
34
-            <View style = { styles.localThumbnail }>
35
-                <Thumbnail participant = { _localParticipant } />
36
-            </View>
37
-        );
38
-    }
39
-}
40
-
41
-/**
42
- * Maps (parts of) the redux state to the associated {@code LocalThumbnail}'s
43
- * props.
44 12
  *
45
- * @param {Object} state - The redux state.
46
- * @private
47
- * @returns {{
48
- *     _localParticipant: Participant
49
- * }}
13
+ * @returns {ReactElement}
50 14
  */
51
-function _mapStateToProps(state) {
52
-    return {
53
-        /**
54
-         * The local participant.
55
-         *
56
-         * @private
57
-         * @type {Participant}
58
-         */
59
-        _localParticipant: getLocalParticipant(state)
60
-    };
15
+export default function LocalThumbnail() {
16
+    return (
17
+        <View style = { styles.localThumbnail }>
18
+            <Thumbnail />
19
+        </View>
20
+    );
61 21
 }
62
-
63
-export default connect(_mapStateToProps)(LocalThumbnail);

+ 37
- 70
react/features/filmstrip/components/native/Thumbnail.js 查看文件

@@ -1,6 +1,6 @@
1 1
 // @flow
2 2
 
3
-import React from 'react';
3
+import React, { useCallback } from 'react';
4 4
 import { View } from 'react-native';
5 5
 import type { Dispatch } from 'redux';
6 6
 
@@ -12,7 +12,8 @@ import {
12 12
     ParticipantView,
13 13
     getParticipantCount,
14 14
     isEveryoneModerator,
15
-    pinParticipant
15
+    pinParticipant,
16
+    getParticipantByIdOrUndefined
16 17
 } from '../../../base/participants';
17 18
 import { Container } from '../../../base/react';
18 19
 import { connect } from '../../../base/redux';
@@ -48,14 +49,9 @@ type Props = {
48 49
     _largeVideo: Object,
49 50
 
50 51
     /**
51
-     * Handles click/tap event on the thumbnail.
52
-     */
53
-    _onClick: ?Function,
54
-
55
-    /**
56
-     * Handles long press on the thumbnail.
52
+     * The Redux representation of the participant to display.
57 53
      */
58
-    _onThumbnailLongPress: ?Function,
54
+     _participant: Object,
59 55
 
60 56
     /**
61 57
      * Whether to show the dominant speaker indicator or not.
@@ -90,9 +86,9 @@ type Props = {
90 86
     dispatch: Dispatch<any>,
91 87
 
92 88
     /**
93
-     * The Redux representation of the participant to display.
89
+     * The ID of the participant related to the thumbnail.
94 90
      */
95
-    participant: Object,
91
+    participantID: ?string,
96 92
 
97 93
     /**
98 94
      * Whether to display or hide the display name of the participant in the thumbnail.
@@ -120,14 +116,13 @@ function Thumbnail(props: Props) {
120 116
     const {
121 117
         _audioMuted: audioMuted,
122 118
         _largeVideo: largeVideo,
123
-        _onClick,
124
-        _onThumbnailLongPress,
125 119
         _renderDominantSpeakerIndicator: renderDominantSpeakerIndicator,
126 120
         _renderModeratorIndicator: renderModeratorIndicator,
121
+        _participant: participant,
127 122
         _styles,
128 123
         _videoTrack: videoTrack,
124
+        dispatch,
129 125
         disableTint,
130
-        participant,
131 126
         renderDisplayName,
132 127
         tileView
133 128
     } = props;
@@ -137,11 +132,29 @@ function Thumbnail(props: Props) {
137 132
         = participantId === largeVideo.participantId;
138 133
     const videoMuted = !videoTrack || videoTrack.muted;
139 134
     const isScreenShare = videoTrack && videoTrack.videoType === VIDEO_TYPE.DESKTOP;
135
+    const onClick = useCallback(() => {
136
+        if (tileView) {
137
+            dispatch(toggleToolboxVisible());
138
+        } else {
139
+            dispatch(pinParticipant(participant.pinned ? null : participant.id));
140
+        }
141
+    }, [ participant, tileView, dispatch ]);
142
+    const onThumbnailLongPress = useCallback(() => {
143
+        if (participant.local) {
144
+            dispatch(openDialog(ConnectionStatusComponent, {
145
+                participantID: participant.id
146
+            }));
147
+        } else {
148
+            dispatch(openDialog(RemoteVideoMenu, {
149
+                participant
150
+            }));
151
+        }
152
+    }, [ participant, dispatch ]);
140 153
 
141 154
     return (
142 155
         <Container
143
-            onClick = { _onClick }
144
-            onLongPress = { _onThumbnailLongPress }
156
+            onClick = { onClick }
157
+            onLongPress = { onThumbnailLongPress }
145 158
             style = { [
146 159
                 styles.thumbnail,
147 160
                 participant.pinned && !tileView
@@ -198,55 +211,6 @@ function Thumbnail(props: Props) {
198 211
     );
199 212
 }
200 213
 
201
-/**
202
- * Maps part of redux actions to component's props.
203
- *
204
- * @param {Function} dispatch - Redux's {@code dispatch} function.
205
- * @param {Props} ownProps - The own props of the component.
206
- * @returns {{
207
- *     _onClick: Function,
208
- *     _onShowRemoteVideoMenu: Function
209
- * }}
210
- */
211
-function _mapDispatchToProps(dispatch: Function, ownProps): Object {
212
-    return {
213
-        /**
214
-         * Handles click/tap event on the thumbnail.
215
-         *
216
-         * @protected
217
-         * @returns {void}
218
-         */
219
-        _onClick() {
220
-            const { participant, tileView } = ownProps;
221
-
222
-            if (tileView) {
223
-                dispatch(toggleToolboxVisible());
224
-            } else {
225
-                dispatch(pinParticipant(participant.pinned ? null : participant.id));
226
-            }
227
-        },
228
-
229
-        /**
230
-         * Handles long press on the thumbnail.
231
-         *
232
-         * @returns {void}
233
-         */
234
-        _onThumbnailLongPress() {
235
-            const { participant } = ownProps;
236
-
237
-            if (participant.local) {
238
-                dispatch(openDialog(ConnectionStatusComponent, {
239
-                    participantID: participant.id
240
-                }));
241
-            } else {
242
-                dispatch(openDialog(RemoteVideoMenu, {
243
-                    participant
244
-                }));
245
-            }
246
-        }
247
-    };
248
-}
249
-
250 214
 /**
251 215
  * Function that maps parts of Redux state tree into component props.
252 216
  *
@@ -260,20 +224,23 @@ function _mapStateToProps(state, ownProps) {
260 224
     // the stage i.e. as a large video.
261 225
     const largeVideo = state['features/large-video'];
262 226
     const tracks = state['features/base/tracks'];
263
-    const { participant } = ownProps;
264
-    const id = participant.id;
227
+    const { participantID } = ownProps;
228
+    const participant = getParticipantByIdOrUndefined(state, participantID);
229
+    const id = participant?.id;
265 230
     const audioTrack
266 231
         = getTrackByMediaTypeAndParticipant(tracks, MEDIA_TYPE.AUDIO, id);
267 232
     const videoTrack
268 233
         = getTrackByMediaTypeAndParticipant(tracks, MEDIA_TYPE.VIDEO, id);
269 234
     const participantCount = getParticipantCount(state);
270
-    const renderDominantSpeakerIndicator = participant.dominantSpeaker && participantCount > 2;
235
+    const renderDominantSpeakerIndicator = participant && participant.dominantSpeaker && participantCount > 2;
271 236
     const _isEveryoneModerator = isEveryoneModerator(state);
272
-    const renderModeratorIndicator = !_isEveryoneModerator && participant.role === PARTICIPANT_ROLE.MODERATOR;
237
+    const renderModeratorIndicator = !_isEveryoneModerator
238
+        && participant && participant.role === PARTICIPANT_ROLE.MODERATOR;
273 239
 
274 240
     return {
275 241
         _audioMuted: audioTrack?.muted ?? true,
276 242
         _largeVideo: largeVideo,
243
+        _participant: participant,
277 244
         _renderDominantSpeakerIndicator: renderDominantSpeakerIndicator,
278 245
         _renderModeratorIndicator: renderModeratorIndicator,
279 246
         _styles: ColorSchemeRegistry.get(state, 'Thumbnail'),
@@ -281,4 +248,4 @@ function _mapStateToProps(state, ownProps) {
281 248
     };
282 249
 }
283 250
 
284
-export default connect(_mapStateToProps, _mapDispatchToProps)(Thumbnail);
251
+export default connect(_mapStateToProps)(Thumbnail);

+ 27
- 21
react/features/filmstrip/components/native/TileView.js 查看文件

@@ -8,6 +8,7 @@ import {
8 8
 } from 'react-native';
9 9
 import type { Dispatch } from 'redux';
10 10
 
11
+import { getLocalParticipant, getParticipantCountWithFake } from '../../../base/participants';
11 12
 import { connect } from '../../../base/redux';
12 13
 import { ASPECT_RATIO_NARROW } from '../../../base/responsive-ui/constants';
13 14
 import { setTileViewDimensions } from '../../actions.native';
@@ -15,6 +16,7 @@ import { setTileViewDimensions } from '../../actions.native';
15 16
 import Thumbnail from './Thumbnail';
16 17
 import styles from './styles';
17 18
 
19
+
18 20
 /**
19 21
  * The type of the React {@link Component} props of {@link TileView}.
20 22
  */
@@ -31,9 +33,19 @@ type Props = {
31 33
     _height: number,
32 34
 
33 35
     /**
34
-     * The participants in the conference.
36
+     * The local participant.
35 37
      */
36
-    _participants: Array<Object>,
38
+    _localParticipant: Object,
39
+
40
+    /**
41
+     * The number of participants in the conference.
42
+     */
43
+    _participantCount: number,
44
+
45
+    /**
46
+     * An array with the IDs of the remote participants in the conference.
47
+     */
48
+    _remoteParticipants: Array<string>,
37 49
 
38 50
     /**
39 51
      * Application's viewport height.
@@ -131,7 +143,7 @@ class TileView extends Component<Props> {
131 143
      * @private
132 144
      */
133 145
     _getColumnCount() {
134
-        const participantCount = this.props._participants.length;
146
+        const participantCount = this.props._participantCount;
135 147
 
136 148
         // For narrow view, tiles should stack on top of each other for a lonely
137 149
         // call and a 1:1 call. Otherwise tiles should be grouped into rows of
@@ -155,18 +167,10 @@ class TileView extends Component<Props> {
155 167
      * @returns {Participant[]}
156 168
      */
157 169
     _getSortedParticipants() {
158
-        const participants = [];
159
-        let localParticipant;
160
-
161
-        for (const participant of this.props._participants) {
162
-            if (participant.local) {
163
-                localParticipant = participant;
164
-            } else {
165
-                participants.push(participant);
166
-            }
167
-        }
170
+        const { _localParticipant, _remoteParticipants } = this.props;
171
+        const participants = [ ..._remoteParticipants ];
168 172
 
169
-        localParticipant && participants.push(localParticipant);
173
+        _localParticipant && participants.push(_localParticipant.id);
170 174
 
171 175
         return participants;
172 176
     }
@@ -178,16 +182,15 @@ class TileView extends Component<Props> {
178 182
      * @returns {Object}
179 183
      */
180 184
     _getTileDimensions() {
181
-        const { _height, _participants, _width } = this.props;
185
+        const { _height, _participantCount, _width } = this.props;
182 186
         const columns = this._getColumnCount();
183
-        const participantCount = _participants.length;
184 187
         const heightToUse = _height - (MARGIN * 2);
185 188
         const widthToUse = _width - (MARGIN * 2);
186 189
         let tileWidth;
187 190
 
188 191
         // If there is going to be at least two rows, ensure that at least two
189 192
         // rows display fully on screen.
190
-        if (participantCount / columns > 1) {
193
+        if (_participantCount / columns > 1) {
191 194
             tileWidth = Math.min(widthToUse / columns, heightToUse / 2);
192 195
         } else {
193 196
             tileWidth = Math.min(widthToUse / columns, heightToUse);
@@ -247,11 +250,11 @@ class TileView extends Component<Props> {
247 250
         };
248 251
 
249 252
         return this._getSortedParticipants()
250
-            .map(participant => (
253
+            .map(id => (
251 254
                 <Thumbnail
252 255
                     disableTint = { true }
253
-                    key = { participant.id }
254
-                    participant = { participant }
256
+                    key = { id }
257
+                    participantID = { id }
255 258
                     renderDisplayName = { true }
256 259
                     styleOverrides = { styleOverrides }
257 260
                     tileView = { true } />));
@@ -285,11 +288,14 @@ class TileView extends Component<Props> {
285 288
  */
286 289
 function _mapStateToProps(state) {
287 290
     const responsiveUi = state['features/base/responsive-ui'];
291
+    const { remoteParticipants } = state['features/filmstrip'];
288 292
 
289 293
     return {
290 294
         _aspectRatio: responsiveUi.aspectRatio,
291 295
         _height: responsiveUi.clientHeight,
292
-        _participants: state['features/base/participants'],
296
+        _localParticipant: getLocalParticipant(state),
297
+        _participantCount: getParticipantCountWithFake(state),
298
+        _remoteParticipants: remoteParticipants,
293 299
         _width: responsiveUi.clientWidth
294 300
     };
295 301
 }

+ 2
- 2
react/features/filmstrip/components/web/StatusIndicators.js 查看文件

@@ -3,7 +3,7 @@
3 3
 import React, { Component } from 'react';
4 4
 
5 5
 import { MEDIA_TYPE } from '../../../base/media';
6
-import { getLocalParticipant, getParticipantById, PARTICIPANT_ROLE } from '../../../base/participants';
6
+import { getParticipantByIdOrUndefined, PARTICIPANT_ROLE } from '../../../base/participants';
7 7
 import { connect } from '../../../base/redux';
8 8
 import { getTrackByMediaTypeAndParticipant, isLocalTrackMuted, isRemoteTrackMuted } from '../../../base/tracks';
9 9
 import { getCurrentLayout, LAYOUTS } from '../../../video-layout';
@@ -111,7 +111,7 @@ function _mapStateToProps(state, ownProps) {
111 111
     const { participantID } = ownProps;
112 112
 
113 113
     // Only the local participant won't have id for the time when the conference is not yet joined.
114
-    const participant = participantID ? getParticipantById(state, participantID) : getLocalParticipant(state);
114
+    const participant = getParticipantByIdOrUndefined(state, participantID);
115 115
 
116 116
     const tracks = state['features/base/tracks'];
117 117
     let isVideoMuted = true;

+ 5
- 7
react/features/filmstrip/components/web/Thumbnail.js 查看文件

@@ -9,8 +9,7 @@ import { Avatar } from '../../../base/avatar';
9 9
 import JitsiMeetJS from '../../../base/lib-jitsi-meet/_';
10 10
 import { MEDIA_TYPE, VideoTrack } from '../../../base/media';
11 11
 import {
12
-    getLocalParticipant,
13
-    getParticipantById,
12
+    getParticipantByIdOrUndefined,
14 13
     getParticipantCount,
15 14
     pinParticipant
16 15
 } from '../../../base/participants';
@@ -1012,9 +1011,8 @@ class Thumbnail extends Component<Props, State> {
1012 1011
 function _mapStateToProps(state, ownProps): Object {
1013 1012
     const { participantID } = ownProps;
1014 1013
 
1015
-    // Only the local participant won't have id for the time when the conference is not yet joined.
1016
-    const participant = participantID ? getParticipantById(state, participantID) : getLocalParticipant(state);
1017
-    const { id } = participant;
1014
+    const participant = getParticipantByIdOrUndefined(state, participantID);
1015
+    const id = participant?.id;
1018 1016
     const isLocal = participant?.local ?? true;
1019 1017
     const tracks = state['features/base/tracks'];
1020 1018
     const { participantsVolume } = state['features/filmstrip'];
@@ -1085,14 +1083,14 @@ function _mapStateToProps(state, ownProps): Object {
1085 1083
         _isDominantSpeakerDisabled: interfaceConfig.DISABLE_DOMINANT_SPEAKER_INDICATOR,
1086 1084
         _isScreenSharing: _videoTrack?.videoType === 'desktop',
1087 1085
         _isTestModeEnabled: isTestModeEnabled(state),
1088
-        _isVideoPlayable: isVideoPlayable(state, id),
1086
+        _isVideoPlayable: id && isVideoPlayable(state, id),
1089 1087
         _indicatorIconSize: NORMAL,
1090 1088
         _localFlipX: Boolean(localFlipX),
1091 1089
         _participant: participant,
1092 1090
         _participantCountMoreThan2: getParticipantCount(state) > 2,
1093 1091
         _startSilent: Boolean(startSilent),
1094 1092
         _videoTrack,
1095
-        _volume: isLocal ? undefined : participantsVolume[id],
1093
+        _volume: isLocal ? undefined : id ? participantsVolume[id] : undefined,
1096 1094
         ...size
1097 1095
     };
1098 1096
 }

+ 2
- 3
react/features/filmstrip/functions.native.js 查看文件

@@ -1,6 +1,7 @@
1 1
 // @flow
2 2
 
3 3
 import { getFeatureFlag, FILMSTRIP_ENABLED } from '../base/flags';
4
+import { getParticipantCountWithFake } from '../base/participants';
4 5
 import { toState } from '../base/redux';
5 6
 
6 7
 /**
@@ -22,7 +23,5 @@ export function isFilmstripVisible(stateful: Object | Function) {
22 23
         return false;
23 24
     }
24 25
 
25
-    const { length: participantCount } = state['features/base/participants'];
26
-
27
-    return participantCount > 1;
26
+    return getParticipantCountWithFake(state) > 1;
28 27
 }

+ 2
- 1
react/features/filmstrip/subscriber.web.js 查看文件

@@ -1,5 +1,6 @@
1 1
 // @flow
2 2
 
3
+import { getParticipantCountWithFake } from '../base/participants';
3 4
 import { StateListenerRegistry, equals } from '../base/redux';
4 5
 import { clientResized } from '../base/responsive-ui';
5 6
 import { setFilmstripVisible } from '../filmstrip/actions';
@@ -19,7 +20,7 @@ import {
19 20
  * Listens for changes in the number of participants to calculate the dimensions of the tile view grid and the tiles.
20 21
  */
21 22
 StateListenerRegistry.register(
22
-    /* selector */ state => state['features/base/participants'].length,
23
+    /* selector */ getParticipantCountWithFake,
23 24
     /* listener */ (numberOfParticipants, store) => {
24 25
         const state = store.getState();
25 26
 

+ 3
- 3
react/features/invite/actions.any.js 查看文件

@@ -3,7 +3,7 @@
3 3
 import type { Dispatch } from 'redux';
4 4
 
5 5
 import { getInviteURL } from '../base/connection';
6
-import { getLocalParticipant, getParticipants } from '../base/participants';
6
+import { getLocalParticipant, getParticipantCount } from '../base/participants';
7 7
 import { inviteVideoRooms } from '../videosipgw';
8 8
 
9 9
 import {
@@ -71,14 +71,14 @@ export function invite(
71 71
             dispatch: Dispatch<any>,
72 72
             getState: Function): Promise<Array<Object>> => {
73 73
         const state = getState();
74
-        const participants = getParticipants(state);
74
+        const participantsCount = getParticipantCount(state);
75 75
         const { calleeInfoVisible } = state['features/invite'];
76 76
 
77 77
         if (showCalleeInfo
78 78
                 && !calleeInfoVisible
79 79
                 && invitees.length === 1
80 80
                 && invitees[0].type === INVITE_TYPES.USER
81
-                && participants.length === 1) {
81
+                && participantsCount === 1) {
82 82
             dispatch(setCalleeInfoVisible(true, invitees[0]));
83 83
         }
84 84
 

+ 16
- 16
react/features/invite/components/callee-info/CalleeInfo.js 查看文件

@@ -5,9 +5,9 @@ import React, { Component } from 'react';
5 5
 import { Avatar } from '../../../base/avatar';
6 6
 import { MEDIA_TYPE } from '../../../base/media';
7 7
 import {
8
-    getParticipants,
9 8
     getParticipantDisplayName,
10
-    getParticipantPresenceStatus
9
+    getParticipantPresenceStatus,
10
+    getRemoteParticipants
11 11
 } from '../../../base/participants';
12 12
 import { Container, Text } from '../../../base/react';
13 13
 import { connect } from '../../../base/redux';
@@ -135,20 +135,20 @@ class CalleeInfo extends Component<Props> {
135 135
 function _mapStateToProps(state) {
136 136
     const _isVideoMuted
137 137
         = isLocalTrackMuted(state['features/base/tracks'], MEDIA_TYPE.VIDEO);
138
-    const poltergeist
139
-        = getParticipants(state).find(p => p.botType === 'poltergeist');
140
-
141
-    if (poltergeist) {
142
-        const { id } = poltergeist;
143
-
144
-        return {
145
-            _callee: {
146
-                id,
147
-                name: getParticipantDisplayName(state, id),
148
-                status: getParticipantPresenceStatus(state, id)
149
-            },
150
-            _isVideoMuted
151
-        };
138
+
139
+    // This would be expensive for big calls but the component will be mounted only when there are up
140
+    // to 3 participants in the call.
141
+    for (const [ id, p ] of getRemoteParticipants(state)) {
142
+        if (p.botType === 'poltergeist') {
143
+            return {
144
+                _callee: {
145
+                    id,
146
+                    name: getParticipantDisplayName(state, id),
147
+                    status: getParticipantPresenceStatus(state, id)
148
+                },
149
+                _isVideoMuted
150
+            };
151
+        }
152 152
     }
153 153
 
154 154
     return {

+ 13
- 6
react/features/invite/middleware.any.js 查看文件

@@ -6,8 +6,9 @@ import {
6 6
 } from '../base/conference';
7 7
 import {
8 8
     getLocalParticipant,
9
+    getParticipantCount,
9 10
     getParticipantPresenceStatus,
10
-    getParticipants,
11
+    getRemoteParticipants,
11 12
     PARTICIPANT_JOINED,
12 13
     PARTICIPANT_JOINED_SOUND_ID,
13 14
     PARTICIPANT_LEFT,
@@ -167,13 +168,19 @@ function _maybeHideCalleeInfo(action, store) {
167 168
     if (!state['features/invite'].calleeInfoVisible) {
168 169
         return;
169 170
     }
170
-    const participants = getParticipants(state);
171
-    const numberOfPoltergeists
172
-        = participants.filter(p => p.botType === 'poltergeist').length;
173
-    const numberOfRealParticipants = participants.length - numberOfPoltergeists;
171
+    const participants = getRemoteParticipants(state);
172
+    const participantCount = getParticipantCount(state);
173
+    let numberOfPoltergeists = 0;
174
+
175
+    participants.forEach(p => {
176
+        if (p.botType === 'poltergeist') {
177
+            numberOfPoltergeists++;
178
+        }
179
+    });
180
+    const numberOfRealParticipants = participantCount - numberOfPoltergeists;
174 181
 
175 182
     if ((numberOfPoltergeists > 1 || numberOfRealParticipants > 1)
176
-        || (action.type === PARTICIPANT_LEFT && participants.length === 1)) {
183
+        || (action.type === PARTICIPANT_LEFT && participantCount === 1)) {
177 184
         store.dispatch(setCalleeInfoVisible(false));
178 185
     }
179 186
 }

+ 16
- 5
react/features/large-video/actions.any.js 查看文件

@@ -3,6 +3,12 @@
3 3
 import type { Dispatch } from 'redux';
4 4
 
5 5
 import { MEDIA_TYPE } from '../base/media';
6
+import {
7
+    getDominantSpeakerParticipant,
8
+    getLocalParticipant,
9
+    getPinnedParticipant,
10
+    getRemoteParticipants
11
+} from '../base/participants';
6 12
 
7 13
 import {
8 14
     SELECT_LARGE_VIDEO_PARTICIPANT,
@@ -92,8 +98,7 @@ function _electLastVisibleRemoteVideo(tracks) {
92 98
 function _electParticipantInLargeVideo(state) {
93 99
     // 1. If a participant is pinned, they will be shown in the LargeVideo
94 100
     // (regardless of whether they are local or remote).
95
-    const participants = state['features/base/participants'];
96
-    let participant = participants.find(p => p.pinned);
101
+    let participant = getPinnedParticipant(state);
97 102
 
98 103
     if (participant) {
99 104
         return participant.id;
@@ -107,11 +112,14 @@ function _electParticipantInLargeVideo(state) {
107 112
     }
108 113
 
109 114
     // 3. Next, pick the dominant speaker (other than self).
110
-    participant = participants.find(p => p.dominantSpeaker && !p.local);
111
-    if (participant) {
115
+    participant = getDominantSpeakerParticipant(state);
116
+    if (participant && !participant.local) {
112 117
         return participant.id;
113 118
     }
114 119
 
120
+    // In case this is the local participant.
121
+    participant = undefined;
122
+
115 123
     // 4. Next, pick the most recent participant with video.
116 124
     const tracks = state['features/base/tracks'];
117 125
     const videoTrack = _electLastVisibleRemoteVideo(tracks);
@@ -122,6 +130,9 @@ function _electParticipantInLargeVideo(state) {
122 130
 
123 131
     // 5. As a last resort, select the participant that joined last (other than poltergist or other bot type
124 132
     // participants).
133
+
134
+    const participants = [ ...getRemoteParticipants(state).values() ];
135
+
125 136
     for (let i = participants.length; i > 0 && !participant; i--) {
126 137
         const p = participants[i - 1];
127 138
 
@@ -131,5 +142,5 @@ function _electParticipantInLargeVideo(state) {
131 142
         return participant.id;
132 143
     }
133 144
 
134
-    return participants.find(p => p.local)?.id;
145
+    return getLocalParticipant(state)?.id;
135 146
 }

+ 34
- 11
react/features/mobile/external-api/middleware.js 查看文件

@@ -28,7 +28,13 @@ import {
28 28
 import { JitsiConferenceEvents } from '../../base/lib-jitsi-meet';
29 29
 import { MEDIA_TYPE } from '../../base/media';
30 30
 import { SET_AUDIO_MUTED, SET_VIDEO_MUTED } from '../../base/media/actionTypes';
31
-import { PARTICIPANT_JOINED, PARTICIPANT_LEFT, getParticipants, getParticipantById } from '../../base/participants';
31
+import {
32
+    PARTICIPANT_JOINED,
33
+    PARTICIPANT_LEFT,
34
+    getParticipantById,
35
+    getRemoteParticipants,
36
+    getLocalParticipant
37
+} from '../../base/participants';
32 38
 import { MiddlewareRegistry, StateListenerRegistry } from '../../base/redux';
33 39
 import { toggleScreensharing } from '../../base/tracks';
34 40
 import { OPEN_CHAT, CLOSE_CHAT } from '../../chat';
@@ -268,6 +274,24 @@ StateListenerRegistry.register(
268 274
 
269 275
     }, 100));
270 276
 
277
+/**
278
+ * Returns a participant info object based on the passed participant object from redux.
279
+ *
280
+ * @param {Participant} participant - The participant object from the redux store.
281
+ * @returns {Object} - The participant info object.
282
+ */
283
+function _participantToParticipantInfo(participant) {
284
+    return {
285
+        isLocal: participant.local,
286
+        email: participant.email,
287
+        name: participant.name,
288
+        participantId: participant.id,
289
+        displayName: participant.displayName,
290
+        avatarUrl: participant.avatarURL,
291
+        role: participant.role
292
+    };
293
+}
294
+
271 295
 /**
272 296
  * Registers for events sent from the native side via NativeEventEmitter.
273 297
  *
@@ -309,16 +333,15 @@ function _registerForNativeEvents(store) {
309 333
 
310 334
     eventEmitter.addListener(ExternalAPI.RETRIEVE_PARTICIPANTS_INFO, ({ requestId }) => {
311 335
 
312
-        const participantsInfo = getParticipants(store).map(participant => {
313
-            return {
314
-                isLocal: participant.local,
315
-                email: participant.email,
316
-                name: participant.name,
317
-                participantId: participant.id,
318
-                displayName: participant.displayName,
319
-                avatarUrl: participant.avatarURL,
320
-                role: participant.role
321
-            };
336
+        const participantsInfo = [];
337
+        const remoteParticipants = getRemoteParticipants(store);
338
+        const localParticipant = getLocalParticipant(store);
339
+
340
+        participantsInfo.push(_participantToParticipantInfo(localParticipant));
341
+        remoteParticipants.forEach(participant => {
342
+            if (!participant.isFakeParticipant) {
343
+                participantsInfo.push(_participantToParticipantInfo(participant));
344
+            }
322 345
         });
323 346
 
324 347
         sendEvent(

+ 7
- 5
react/features/participants-pane/components/AskToUnmuteButton.js 查看文件

@@ -1,7 +1,6 @@
1 1
 // @flow
2 2
 
3 3
 import React, { useCallback } from 'react';
4
-import { useTranslation } from 'react-i18next';
5 4
 import { useDispatch } from 'react-redux';
6 5
 
7 6
 import { approveParticipant } from '../../av-moderation/actions';
@@ -10,6 +9,11 @@ import { QuickActionButton } from './styled';
10 9
 
11 10
 type Props = {
12 11
 
12
+    /**
13
+     * The translated ask unmute text.
14
+     */
15
+    askUnmuteText: string,
16
+
13 17
     /**
14 18
      * Participant id.
15 19
      */
@@ -22,10 +26,8 @@ type Props = {
22 26
  * @param {Object} participant - Participant reference.
23 27
  * @returns {React$Element<'button'>}
24 28
  */
25
-export default function({ id }: Props) {
29
+export default function AskToUnmuteButton({ id, askUnmuteText }: Props) {
26 30
     const dispatch = useDispatch();
27
-    const { t } = useTranslation();
28
-
29 31
     const askToUnmute = useCallback(() => {
30 32
         dispatch(approveParticipant(id));
31 33
     }, [ dispatch, id ]);
@@ -37,7 +39,7 @@ export default function({ id }: Props) {
37 39
             theme = {{
38 40
                 panePadding: 16
39 41
             }}>
40
-            {t('participantsPane.actions.askUnmute')}
42
+            { askUnmuteText }
41 43
         </QuickActionButton>
42 44
     );
43 45
 }

+ 36
- 23
react/features/participants-pane/components/FooterContextMenu.js 查看文件

@@ -6,11 +6,17 @@ import { useTranslation } from 'react-i18next';
6 6
 import { useDispatch, useSelector } from 'react-redux';
7 7
 
8 8
 import { requestDisableModeration, requestEnableModeration } from '../../av-moderation/actions';
9
-import { isEnabled as isAvModerationEnabled } from '../../av-moderation/functions';
9
+import {
10
+    isEnabled as isAvModerationEnabled,
11
+    isSupported as isAvModerationSupported
12
+} from '../../av-moderation/functions';
10 13
 import { openDialog } from '../../base/dialog';
11 14
 import { Icon, IconCheck, IconVideoOff } from '../../base/icons';
12 15
 import { MEDIA_TYPE } from '../../base/media';
13
-import { getLocalParticipant } from '../../base/participants';
16
+import {
17
+    getLocalParticipant,
18
+    isEveryoneModerator
19
+} from '../../base/participants';
14 20
 import { MuteEveryonesVideoDialog } from '../../video-menu/components';
15 21
 
16 22
 import {
@@ -49,6 +55,8 @@ type Props = {
49 55
 
50 56
 export const FooterContextMenu = ({ onMouseLeave }: Props) => {
51 57
     const dispatch = useDispatch();
58
+    const isModerationSupported = useSelector(isAvModerationSupported());
59
+    const allModerators = useSelector(isEveryoneModerator);
52 60
     const isModerationEnabled = useSelector(isAvModerationEnabled(MEDIA_TYPE.AUDIO));
53 61
     const { id } = useSelector(getLocalParticipant);
54 62
     const { t } = useTranslation();
@@ -75,27 +83,32 @@ export const FooterContextMenu = ({ onMouseLeave }: Props) => {
75 83
                 <span>{ t('participantsPane.actions.stopEveryonesVideo') }</span>
76 84
             </ContextMenuItem>
77 85
 
78
-            <div className = { classes.text }>
79
-                {t('participantsPane.actions.allow')}
80
-            </div>
81
-            { isModerationEnabled ? (
82
-                <ContextMenuItem
83
-                    id = 'participants-pane-context-menu-start-moderation'
84
-                    onClick = { disable }>
85
-                    <span className = { classes.paddedAction }>
86
-                        { t('participantsPane.actions.startModeration') }
87
-                    </span>
88
-                </ContextMenuItem>
89
-            ) : (
90
-                <ContextMenuItem
91
-                    id = 'participants-pane-context-menu-stop-moderation'
92
-                    onClick = { enable }>
93
-                    <Icon
94
-                        size = { 20 }
95
-                        src = { IconCheck } />
96
-                    <span>{ t('participantsPane.actions.startModeration') }</span>
97
-                </ContextMenuItem>
98
-            )}
86
+            { isModerationSupported && !allModerators ? (
87
+                <>
88
+                    <div className = { classes.text }>
89
+                        {t('participantsPane.actions.allow')}
90
+                    </div>
91
+                    { isModerationEnabled ? (
92
+                        <ContextMenuItem
93
+                            id = 'participants-pane-context-menu-start-moderation'
94
+                            onClick = { disable }>
95
+                            <span className = { classes.paddedAction }>
96
+                                { t('participantsPane.actions.startModeration') }
97
+                            </span>
98
+                        </ContextMenuItem>
99
+                    ) : (
100
+                        <ContextMenuItem
101
+                            id = 'participants-pane-context-menu-stop-moderation'
102
+                            onClick = { enable }>
103
+                            <Icon
104
+                                size = { 20 }
105
+                                src = { IconCheck } />
106
+                            <span>{ t('participantsPane.actions.startModeration') }</span>
107
+                        </ContextMenuItem>
108
+                    )}
109
+                </>
110
+            ) : undefined
111
+            }
99 112
         </ContextMenu>
100 113
     );
101 114
 };

+ 7
- 4
react/features/participants-pane/components/LobbyParticipantItem.js 查看文件

@@ -7,7 +7,7 @@ import { useDispatch } from 'react-redux';
7 7
 import { approveKnockingParticipant, rejectKnockingParticipant } from '../../lobby/actions';
8 8
 import { ACTION_TRIGGER, MEDIA_STATE } from '../constants';
9 9
 
10
-import { ParticipantItem } from './ParticipantItem';
10
+import ParticipantItem from './ParticipantItem';
11 11
 import { ParticipantActionButton } from './styled';
12 12
 
13 13
 type Props = {
@@ -28,9 +28,12 @@ export const LobbyParticipantItem = ({ participant: p }: Props) => {
28 28
         <ParticipantItem
29 29
             actionsTrigger = { ACTION_TRIGGER.PERMANENT }
30 30
             audioMediaState = { MEDIA_STATE.NONE }
31
-            name = { p.name }
32
-            participant = { p }
33
-            videoMuteState = { MEDIA_STATE.NONE }>
31
+            displayName = { p.name }
32
+            local = { p.local }
33
+            participantID = { p.id }
34
+            raisedHand = { p.raisedHand }
35
+            videoMuteState = { MEDIA_STATE.NONE }
36
+            youText = { t('chat.you') }>
34 37
             <ParticipantActionButton
35 38
                 onClick = { reject }>
36 39
                 {t('lobby.reject')}

+ 322
- 112
react/features/participants-pane/components/MeetingParticipantContextMenu.js 查看文件

@@ -1,11 +1,10 @@
1 1
 // @flow
2 2
 
3
-import React, { useCallback, useLayoutEffect, useRef, useState } from 'react';
4
-import { useTranslation } from 'react-i18next';
5
-import { useDispatch, useSelector } from 'react-redux';
3
+import React, { Component } from 'react';
6 4
 
7 5
 import { isToolbarButtonEnabled } from '../../base/config/functions.web';
8 6
 import { openDialog } from '../../base/dialog';
7
+import { translate } from '../../base/i18n';
9 8
 import {
10 9
     IconCloseCircle,
11 10
     IconCrown,
@@ -14,8 +13,13 @@ import {
14 13
     IconMuteEveryoneElse,
15 14
     IconVideoOff
16 15
 } from '../../base/icons';
17
-import { isLocalParticipantModerator, isParticipantModerator } from '../../base/participants';
18
-import { getIsParticipantAudioMuted, getIsParticipantVideoMuted } from '../../base/tracks';
16
+import {
17
+    getParticipantByIdOrUndefined,
18
+    isLocalParticipantModerator,
19
+    isParticipantModerator
20
+} from '../../base/participants';
21
+import { connect } from '../../base/redux';
22
+import { isParticipantAudioMuted, isParticipantVideoMuted } from '../../base/tracks';
19 23
 import { openChat } from '../../chat/actions';
20 24
 import { GrantModeratorDialog, KickRemoteParticipantDialog, MuteEveryoneDialog } from '../../video-menu';
21 25
 import MuteRemoteParticipantsVideoDialog from '../../video-menu/components/web/MuteRemoteParticipantsVideoDialog';
@@ -31,6 +35,41 @@ import {
31 35
 
32 36
 type Props = {
33 37
 
38
+    /**
39
+     * True if the local participant is moderator and false otherwise.
40
+     */
41
+    _isLocalModerator: boolean,
42
+
43
+    /**
44
+     * True if the chat button is enabled and false otherwise.
45
+     */
46
+    _isChatButtonEnabled: boolean,
47
+
48
+    /**
49
+     * True if the participant is moderator and false otherwise.
50
+     */
51
+    _isParticipantModerator: boolean,
52
+
53
+    /**
54
+     * True if the participant is video muted and false otherwise.
55
+     */
56
+    _isParticipantVideoMuted: boolean,
57
+
58
+    /**
59
+     * True if the participant is audio muted and false otherwise.
60
+     */
61
+    _isParticipantAudioMuted: boolean,
62
+
63
+    /**
64
+     * Participant reference
65
+     */
66
+    _participant: Object,
67
+
68
+    /**
69
+     * The dispatch function from redux.
70
+     */
71
+    dispatch: Function,
72
+
34 73
     /**
35 74
      * Callback used to open a confirmation dialog for audio muting.
36 75
      */
@@ -57,35 +96,145 @@ type Props = {
57 96
     onSelect: Function,
58 97
 
59 98
     /**
60
-     * Participant reference
99
+     * The ID of the participant.
100
+     */
101
+    participantID: string,
102
+
103
+    /**
104
+     * The translate function.
105
+     */
106
+    t: Function
107
+};
108
+
109
+type State = {
110
+
111
+    /**
112
+     * If true the context menu will be hidden.
61 113
      */
62
-    participant: Object
114
+    isHidden: boolean
63 115
 };
64 116
 
65
-export const MeetingParticipantContextMenu = ({
66
-    offsetTarget,
67
-    onEnter,
68
-    onLeave,
69
-    onSelect,
70
-    muteAudio,
71
-    participant
72
-}: Props) => {
73
-    const dispatch = useDispatch();
74
-    const containerRef = useRef(null);
75
-    const isLocalModerator = useSelector(isLocalParticipantModerator);
76
-    const isChatButtonEnabled = useSelector(isToolbarButtonEnabled('chat'));
77
-    const isParticipantVideoMuted = useSelector(getIsParticipantVideoMuted(participant));
78
-    const isParticipantAudioMuted = useSelector(getIsParticipantAudioMuted(participant));
79
-    const [ isHidden, setIsHidden ] = useState(true);
80
-    const { t } = useTranslation();
81
-
82
-    useLayoutEffect(() => {
83
-        if (participant
84
-            && containerRef.current
117
+/**
118
+ * Implements the MeetingParticipantContextMenu component.
119
+ */
120
+class MeetingParticipantContextMenu extends Component<Props, State> {
121
+
122
+    /**
123
+     * Reference to the context menu container div.
124
+     */
125
+    _containerRef: Object;
126
+
127
+    /**
128
+     * Creates new instance of MeetingParticipantContextMenu.
129
+     *
130
+     * @param {Props} props - The props.
131
+     */
132
+    constructor(props: Props) {
133
+        super(props);
134
+
135
+        this.state = {
136
+            isHidden: true
137
+        };
138
+
139
+        this._containerRef = React.createRef();
140
+
141
+        this._onGrantModerator = this._onGrantModerator.bind(this);
142
+        this._onKick = this._onKick.bind(this);
143
+        this._onMuteEveryoneElse = this._onMuteEveryoneElse.bind(this);
144
+        this._onMuteVideo = this._onMuteVideo.bind(this);
145
+        this._onSendPrivateMessage = this._onSendPrivateMessage.bind(this);
146
+        this._position = this._position.bind(this);
147
+    }
148
+
149
+    _onGrantModerator: () => void;
150
+
151
+    /**
152
+     * Grant moderator permissions.
153
+     *
154
+     * @returns {void}
155
+     */
156
+    _onGrantModerator() {
157
+        const { _participant, dispatch } = this.props;
158
+
159
+        dispatch(openDialog(GrantModeratorDialog, {
160
+            participantID: _participant?.id
161
+        }));
162
+    }
163
+
164
+    _onKick: () => void;
165
+
166
+    /**
167
+     * Kicks the participant.
168
+     *
169
+     * @returns {void}
170
+     */
171
+    _onKick() {
172
+        const { _participant, dispatch } = this.props;
173
+
174
+        dispatch(openDialog(KickRemoteParticipantDialog, {
175
+            participantID: _participant?.id
176
+        }));
177
+    }
178
+
179
+    _onMuteEveryoneElse: () => void;
180
+
181
+    /**
182
+     * Mutes everyone else.
183
+     *
184
+     * @returns {void}
185
+     */
186
+    _onMuteEveryoneElse() {
187
+        const { _participant, dispatch } = this.props;
188
+
189
+        dispatch(openDialog(MuteEveryoneDialog, {
190
+            exclude: [ _participant?.id ]
191
+        }));
192
+    }
193
+
194
+    _onMuteVideo: () => void;
195
+
196
+    /**
197
+     * Mutes the video of the selected participant.
198
+     *
199
+     * @returns {void}
200
+     */
201
+    _onMuteVideo() {
202
+        const { _participant, dispatch } = this.props;
203
+
204
+        dispatch(openDialog(MuteRemoteParticipantsVideoDialog, {
205
+            participantID: _participant?.id
206
+        }));
207
+    }
208
+
209
+    _onSendPrivateMessage: () => void;
210
+
211
+    /**
212
+     * Sends private message.
213
+     *
214
+     * @returns {void}
215
+     */
216
+    _onSendPrivateMessage() {
217
+        const { _participant, dispatch } = this.props;
218
+
219
+        dispatch(openChat(_participant));
220
+    }
221
+
222
+    _position: () => void;
223
+
224
+    /**
225
+     * Positions the context menu.
226
+     *
227
+     * @returns {void}
228
+     */
229
+    _position() {
230
+        const { _participant, offsetTarget } = this.props;
231
+
232
+        if (_participant
233
+            && this._containerRef.current
85 234
             && offsetTarget?.offsetParent
86 235
             && offsetTarget.offsetParent instanceof HTMLElement
87 236
         ) {
88
-            const { current: container } = containerRef;
237
+            const { current: container } = this._containerRef;
89 238
             const { offsetTop, offsetParent: { offsetHeight, scrollTop } } = offsetTarget;
90 239
             const outerHeight = getComputedOuterHeight(container);
91 240
 
@@ -93,97 +242,158 @@ export const MeetingParticipantContextMenu = ({
93 242
                 ? offsetTop - outerHeight
94 243
                 : offsetTop;
95 244
 
96
-            setIsHidden(false);
245
+            this.setState({ isHidden: false });
97 246
         } else {
98
-            setIsHidden(true);
247
+            this.setState({ isHidden: true });
99 248
         }
100
-    }, [ participant, offsetTarget ]);
249
+    }
101 250
 
102
-    const grantModerator = useCallback(() => {
103
-        dispatch(openDialog(GrantModeratorDialog, {
104
-            participantID: participant.id
105
-        }));
106
-    }, [ dispatch, participant ]);
251
+    /**
252
+     * Implements React Component's componentDidMount.
253
+     *
254
+     * @inheritdoc
255
+     * @returns {void}
256
+     */
257
+    componentDidMount() {
258
+        this._position();
259
+    }
107 260
 
108
-    const kick = useCallback(() => {
109
-        dispatch(openDialog(KickRemoteParticipantDialog, {
110
-            participantID: participant.id
111
-        }));
112
-    }, [ dispatch, participant ]);
261
+    /**
262
+     * Implements React Component's componentDidUpdate.
263
+     *
264
+     * @inheritdoc
265
+     */
266
+    componentDidUpdate(prevProps: Props) {
267
+        if (prevProps.offsetTarget !== this.props.offsetTarget || prevProps._participant !== this.props._participant) {
268
+            this._position();
269
+        }
270
+    }
113 271
 
114
-    const muteEveryoneElse = useCallback(() => {
115
-        dispatch(openDialog(MuteEveryoneDialog, {
116
-            exclude: [ participant.id ]
117
-        }));
118
-    }, [ dispatch, participant ]);
272
+    /**
273
+     * Implements React's {@link Component#render()}.
274
+     *
275
+     * @inheritdoc
276
+     * @returns {ReactElement}
277
+     */
278
+    render() {
279
+        const {
280
+            _isLocalModerator,
281
+            _isChatButtonEnabled,
282
+            _isParticipantModerator,
283
+            _isParticipantVideoMuted,
284
+            _isParticipantAudioMuted,
285
+            _participant,
286
+            onEnter,
287
+            onLeave,
288
+            onSelect,
289
+            muteAudio,
290
+            t
291
+        } = this.props;
119 292
 
120
-    const muteVideo = useCallback(() => {
121
-        dispatch(openDialog(MuteRemoteParticipantsVideoDialog, {
122
-            participantID: participant.id
123
-        }));
124
-    }, [ dispatch, participant ]);
293
+        if (!_participant) {
294
+            return null;
295
+        }
296
+
297
+        return (
298
+            <ContextMenu
299
+                className = { ignoredChildClassName }
300
+                innerRef = { this._containerRef }
301
+                isHidden = { this.state.isHidden }
302
+                onClick = { onSelect }
303
+                onMouseEnter = { onEnter }
304
+                onMouseLeave = { onLeave }>
305
+                <ContextMenuItemGroup>
306
+                    {
307
+                        _isLocalModerator && (
308
+                            <>
309
+                                {
310
+                                    !_isParticipantAudioMuted
311
+                                        && <ContextMenuItem onClick = { muteAudio(_participant) }>
312
+                                            <ContextMenuIcon src = { IconMicDisabled } />
313
+                                            <span>{t('dialog.muteParticipantButton')}</span>
314
+                                        </ContextMenuItem>
315
+                                }
125 316
 
126
-    const sendPrivateMessage = useCallback(() => {
127
-        dispatch(openChat(participant));
128
-    }, [ dispatch, participant ]);
317
+                                <ContextMenuItem onClick = { this._onMuteEveryoneElse }>
318
+                                    <ContextMenuIcon src = { IconMuteEveryoneElse } />
319
+                                    <span>{t('toolbar.accessibilityLabel.muteEveryoneElse')}</span>
320
+                                </ContextMenuItem>
321
+                            </>
322
+                        )
323
+                    }
129 324
 
130
-    if (!participant) {
131
-        return null;
325
+                    {
326
+                        _isLocalModerator && (
327
+                            _isParticipantVideoMuted || (
328
+                                <ContextMenuItem onClick = { this._onMuteVideo }>
329
+                                    <ContextMenuIcon src = { IconVideoOff } />
330
+                                    <span>{t('participantsPane.actions.stopVideo')}</span>
331
+                                </ContextMenuItem>
332
+                            )
333
+                        )
334
+                    }
335
+                </ContextMenuItemGroup>
336
+
337
+                <ContextMenuItemGroup>
338
+                    {
339
+                        _isLocalModerator && (
340
+                            <>
341
+                                {
342
+                                    !_isParticipantModerator && (
343
+                                        <ContextMenuItem onClick = { this._onGrantModerator }>
344
+                                            <ContextMenuIcon src = { IconCrown } />
345
+                                            <span>{t('toolbar.accessibilityLabel.grantModerator')}</span>
346
+                                        </ContextMenuItem>
347
+                                    )
348
+                                }
349
+                                <ContextMenuItem onClick = { this._onKick }>
350
+                                    <ContextMenuIcon src = { IconCloseCircle } />
351
+                                    <span>{ t('videothumbnail.kick') }</span>
352
+                                </ContextMenuItem>
353
+                            </>
354
+                        )
355
+                    }
356
+                    {
357
+                        _isChatButtonEnabled && (
358
+                            <ContextMenuItem onClick = { this._onSendPrivateMessage }>
359
+                                <ContextMenuIcon src = { IconMessage } />
360
+                                <span>{t('toolbar.accessibilityLabel.privateMessage')}</span>
361
+                            </ContextMenuItem>
362
+                        )
363
+                    }
364
+                </ContextMenuItemGroup>
365
+            </ContextMenu>
366
+        );
132 367
     }
368
+}
133 369
 
134
-    return (
135
-        <ContextMenu
136
-            className = { ignoredChildClassName }
137
-            innerRef = { containerRef }
138
-            isHidden = { isHidden }
139
-            onClick = { onSelect }
140
-            onMouseEnter = { onEnter }
141
-            onMouseLeave = { onLeave }>
142
-            <ContextMenuItemGroup>
143
-                {isLocalModerator && (
144
-                    <>
145
-                        {!isParticipantAudioMuted
146
-                         && <ContextMenuItem onClick = { muteAudio(participant) }>
147
-                             <ContextMenuIcon src = { IconMicDisabled } />
148
-                             <span>{t('dialog.muteParticipantButton')}</span>
149
-                         </ContextMenuItem>}
150
-
151
-                        <ContextMenuItem onClick = { muteEveryoneElse }>
152
-                            <ContextMenuIcon src = { IconMuteEveryoneElse } />
153
-                            <span>{t('toolbar.accessibilityLabel.muteEveryoneElse')}</span>
154
-                        </ContextMenuItem>
155
-                    </>
156
-                )}
157
-
158
-                {isLocalModerator && (isParticipantVideoMuted || (
159
-                    <ContextMenuItem onClick = { muteVideo }>
160
-                        <ContextMenuIcon src = { IconVideoOff } />
161
-                        <span>{t('participantsPane.actions.stopVideo')}</span>
162
-                    </ContextMenuItem>
163
-                ))}
164
-            </ContextMenuItemGroup>
165
-
166
-            <ContextMenuItemGroup>
167
-                {isLocalModerator && (
168
-                    <>
169
-                        {!isParticipantModerator(participant)
170
-                        && <ContextMenuItem onClick = { grantModerator }>
171
-                            <ContextMenuIcon src = { IconCrown } />
172
-                            <span>{t('toolbar.accessibilityLabel.grantModerator')}</span>
173
-                        </ContextMenuItem>}
174
-                        <ContextMenuItem onClick = { kick }>
175
-                            <ContextMenuIcon src = { IconCloseCircle } />
176
-                            <span>{t('videothumbnail.kick')}</span>
177
-                        </ContextMenuItem>
178
-                    </>
179
-                )}
180
-                {isChatButtonEnabled && (
181
-                    <ContextMenuItem onClick = { sendPrivateMessage }>
182
-                        <ContextMenuIcon src = { IconMessage } />
183
-                        <span>{t('toolbar.accessibilityLabel.privateMessage')}</span>
184
-                    </ContextMenuItem>
185
-                )}
186
-            </ContextMenuItemGroup>
187
-        </ContextMenu>
188
-    );
189
-};
370
+/**
371
+ * Maps (parts of) the redux state to the associated props for this component.
372
+ *
373
+ * @param {Object} state - The Redux state.
374
+ * @param {Object} ownProps - The own props of the component.
375
+ * @private
376
+ * @returns {Props}
377
+ */
378
+function _mapStateToProps(state, ownProps): Object {
379
+    const { participantID } = ownProps;
380
+
381
+    const participant = getParticipantByIdOrUndefined(state, participantID);
382
+
383
+    const _isLocalModerator = isLocalParticipantModerator(state);
384
+    const _isChatButtonEnabled = isToolbarButtonEnabled('chat', state);
385
+    const _isParticipantVideoMuted = isParticipantVideoMuted(participant, state);
386
+    const _isParticipantAudioMuted = isParticipantAudioMuted(participant, state);
387
+    const _isParticipantModerator = isParticipantModerator(participant);
388
+
389
+    return {
390
+        _isLocalModerator,
391
+        _isChatButtonEnabled,
392
+        _isParticipantModerator,
393
+        _isParticipantVideoMuted,
394
+        _isParticipantAudioMuted,
395
+        _participant: participant
396
+    };
397
+}
398
+
399
+export default translate(connect(_mapStateToProps)(MeetingParticipantContextMenu));

+ 130
- 23
react/features/participants-pane/components/MeetingParticipantItem.js 查看文件

@@ -1,19 +1,62 @@
1 1
 // @flow
2 2
 
3 3
 import React from 'react';
4
-import { useTranslation } from 'react-i18next';
5
-import { useSelector } from 'react-redux';
6 4
 
7
-import { getIsParticipantAudioMuted, getIsParticipantVideoMuted } from '../../base/tracks';
8
-import { ACTION_TRIGGER, MEDIA_STATE } from '../constants';
9
-import { getParticipantAudioMediaState } from '../functions';
5
+import { getParticipantByIdOrUndefined, getParticipantDisplayName } from '../../base/participants';
6
+import { connect } from '../../base/redux';
7
+import { isParticipantAudioMuted, isParticipantVideoMuted } from '../../base/tracks';
8
+import { ACTION_TRIGGER, MEDIA_STATE, type MediaState } from '../constants';
9
+import { getParticipantAudioMediaState, getQuickActionButtonType } from '../functions';
10 10
 
11
-import { ParticipantItem } from './ParticipantItem';
11
+import ParticipantItem from './ParticipantItem';
12 12
 import ParticipantQuickAction from './ParticipantQuickAction';
13 13
 import { ParticipantActionEllipsis } from './styled';
14 14
 
15 15
 type Props = {
16 16
 
17
+    /**
18
+     * Media state for audio.
19
+     */
20
+    _audioMediaState: MediaState,
21
+
22
+    /**
23
+     * The display name of the participant.
24
+     */
25
+    _displayName: string,
26
+
27
+    /**
28
+     * True if the participant is video muted.
29
+     */
30
+    _isVideoMuted: boolean,
31
+
32
+    /**
33
+     * True if the participant is the local participant.
34
+     */
35
+    _local: boolean,
36
+
37
+    /**
38
+     * The participant ID.
39
+     *
40
+     * NOTE: This ID may be different from participantID prop in the case when we pass undefined for the local
41
+     * participant. In this case the local participant ID will be filled trough _participantID prop.
42
+     */
43
+    _participantID: string,
44
+
45
+    /**
46
+     * The type of button to be rendered for the quick action.
47
+     */
48
+    _quickActionButtonType: string,
49
+
50
+    /**
51
+     * True if the participant have raised hand.
52
+     */
53
+    _raisedHand: boolean,
54
+
55
+    /**
56
+     * The translated ask unmute text for the qiuck action buttons.
57
+     */
58
+    askUnmuteText: string,
59
+
17 60
     /**
18 61
      * Is this item highlighted
19 62
      */
@@ -24,6 +67,11 @@ type Props = {
24 67
      */
25 68
     muteAudio: Function,
26 69
 
70
+    /**
71
+     * The translated text for the mute participant button.
72
+     */
73
+    muteParticipantButtonText: string,
74
+
27 75
     /**
28 76
      * Callback for the activation of this item's context menu
29 77
      */
@@ -35,38 +83,97 @@ type Props = {
35 83
     onLeave: Function,
36 84
 
37 85
     /**
38
-     * Participant reference
86
+     * The aria-label for the ellipsis action.
87
+     */
88
+    participantActionEllipsisLabel: string,
89
+
90
+    /**
91
+     * The ID of the participant.
39 92
      */
40
-    participant: Object
93
+    participantID: ?string,
94
+
95
+    /**
96
+     * The translated "you" text.
97
+     */
98
+    youText: string
41 99
 };
42 100
 
43
-export const MeetingParticipantItem = ({
101
+/**
102
+ * Implements the MeetingParticipantItem component.
103
+ *
104
+ * @param {Props} props - The props of the component.
105
+ * @returns {ReactElement}
106
+ */
107
+function MeetingParticipantItem({
108
+    _audioMediaState,
109
+    _displayName,
110
+    _isVideoMuted,
111
+    _local,
112
+    _participantID,
113
+    _quickActionButtonType,
114
+    _raisedHand,
115
+    askUnmuteText,
44 116
     isHighlighted,
45 117
     onContextMenu,
46 118
     onLeave,
47 119
     muteAudio,
48
-    participant
49
-}: Props) => {
50
-    const { t } = useTranslation();
51
-    const isAudioMuted = useSelector(getIsParticipantAudioMuted(participant));
52
-    const isVideoMuted = useSelector(getIsParticipantVideoMuted(participant));
53
-    const audioMediaState = useSelector(getParticipantAudioMediaState(participant, isAudioMuted));
54
-
120
+    muteParticipantButtonText,
121
+    participantActionEllipsisLabel,
122
+    youText
123
+}: Props) {
55 124
     return (
56 125
         <ParticipantItem
57 126
             actionsTrigger = { ACTION_TRIGGER.HOVER }
58
-            audioMediaState = { audioMediaState }
127
+            audioMediaState = { _audioMediaState }
128
+            displayName = { _displayName }
59 129
             isHighlighted = { isHighlighted }
130
+            local = { _local }
60 131
             onLeave = { onLeave }
61
-            participant = { participant }
62
-            videoMuteState = { isVideoMuted ? MEDIA_STATE.MUTED : MEDIA_STATE.UNMUTED }>
132
+            participantID = { _participantID }
133
+            raisedHand = { _raisedHand }
134
+            videoMuteState = { _isVideoMuted ? MEDIA_STATE.MUTED : MEDIA_STATE.UNMUTED }
135
+            youText = { youText }>
63 136
             <ParticipantQuickAction
64
-                isAudioMuted = { isAudioMuted }
137
+                askUnmuteText = { askUnmuteText }
138
+                buttonType = { _quickActionButtonType }
65 139
                 muteAudio = { muteAudio }
66
-                participant = { participant } />
140
+                muteParticipantButtonText = { muteParticipantButtonText }
141
+                participantID = { _participantID } />
67 142
             <ParticipantActionEllipsis
68
-                aria-label = { t('MeetingParticipantItem.ParticipantActionEllipsis.options') }
143
+                aria-label = { participantActionEllipsisLabel }
69 144
                 onClick = { onContextMenu } />
70 145
         </ParticipantItem>
71 146
     );
72
-};
147
+}
148
+
149
+/**
150
+ * Maps (parts of) the redux state to the associated props for this component.
151
+ *
152
+ * @param {Object} state - The Redux state.
153
+ * @param {Object} ownProps - The own props of the component.
154
+ * @private
155
+ * @returns {Props}
156
+ */
157
+function _mapStateToProps(state, ownProps): Object {
158
+    const { participantID } = ownProps;
159
+
160
+    const participant = getParticipantByIdOrUndefined(state, participantID);
161
+
162
+    const _isAudioMuted = isParticipantAudioMuted(participant, state);
163
+    const _isVideoMuted = isParticipantVideoMuted(participant, state);
164
+    const _audioMediaState = getParticipantAudioMediaState(participant, _isAudioMuted, state);
165
+    const _quickActionButtonType = getQuickActionButtonType(participant, _isAudioMuted, state);
166
+
167
+    return {
168
+        _audioMediaState,
169
+        _displayName: getParticipantDisplayName(state, participant?.id),
170
+        _isAudioMuted,
171
+        _isVideoMuted,
172
+        _local: Boolean(participant?.local),
173
+        _participantID: participant?.id,
174
+        _quickActionButtonType,
175
+        _raisedHand: Boolean(participant?.raisedHand)
176
+    };
177
+}
178
+
179
+export default connect(_mapStateToProps)(MeetingParticipantItem);

+ 65
- 27
react/features/participants-pane/components/MeetingParticipantList.js 查看文件

@@ -1,18 +1,21 @@
1 1
 // @flow
2 2
 
3
-import _ from 'lodash';
4 3
 import React, { useCallback, useRef, useState } from 'react';
5 4
 import { useTranslation } from 'react-i18next';
6 5
 import { useSelector, useDispatch } from 'react-redux';
7 6
 
8 7
 import { openDialog } from '../../base/dialog';
9
-import { getParticipants } from '../../base/participants';
8
+import {
9
+    getLocalParticipant,
10
+    getParticipantCountWithFake,
11
+    getRemoteParticipants
12
+} from '../../base/participants';
10 13
 import MuteRemoteParticipantDialog from '../../video-menu/components/web/MuteRemoteParticipantDialog';
11 14
 import { findStyledAncestor, shouldRenderInviteButton } from '../functions';
12 15
 
13 16
 import { InviteButton } from './InviteButton';
14
-import { MeetingParticipantContextMenu } from './MeetingParticipantContextMenu';
15
-import { MeetingParticipantItem } from './MeetingParticipantItem';
17
+import MeetingParticipantContextMenu from './MeetingParticipantContextMenu';
18
+import MeetingParticipantItem from './MeetingParticipantItem';
16 19
 import { Heading, ParticipantContainer } from './styled';
17 20
 
18 21
 type NullProto = {
@@ -20,7 +23,7 @@ type NullProto = {
20 23
   __proto__: null
21 24
 };
22 25
 
23
-type RaiseContext = NullProto | {
26
+type RaiseContext = NullProto | {|
24 27
 
25 28
   /**
26 29
    * Target elements against which positioning calculations are made
@@ -28,17 +31,28 @@ type RaiseContext = NullProto | {
28 31
   offsetTarget?: HTMLElement,
29 32
 
30 33
   /**
31
-   * Participant reference
34
+   * The ID of the participant.
32 35
    */
33
-  participant?: Object,
34
-};
36
+  participantID?: String,
37
+|};
35 38
 
36 39
 const initialState = Object.freeze(Object.create(null));
37 40
 
38
-export const MeetingParticipantList = () => {
41
+/**
42
+ * Renders the MeetingParticipantList component.
43
+ *
44
+ * @returns {ReactNode} - The component.
45
+ */
46
+export function MeetingParticipantList() {
39 47
     const dispatch = useDispatch();
40 48
     const isMouseOverMenu = useRef(false);
41
-    const participants = useSelector(getParticipants, _.isEqual);
49
+    const participants = useSelector(getRemoteParticipants);
50
+    const localParticipant = useSelector(getLocalParticipant);
51
+
52
+    // This is very important as getRemoteParticipants is not changing its reference object
53
+    // and we will not re-render on change, but if count changes we will do
54
+    const participantsCount = useSelector(getParticipantCountWithFake);
55
+
42 56
     const showInviteButton = useSelector(shouldRenderInviteButton);
43 57
     const [ raiseContext, setRaiseContext ] = useState<RaiseContext>(initialState);
44 58
     const { t } = useTranslation();
@@ -61,20 +75,20 @@ export const MeetingParticipantList = () => {
61 75
         });
62 76
     }, [ raiseContext ]);
63 77
 
64
-    const raiseMenu = useCallback((participant, target) => {
78
+    const raiseMenu = useCallback((participantID, target) => {
65 79
         setRaiseContext({
66
-            participant,
80
+            participantID,
67 81
             offsetTarget: findStyledAncestor(target, ParticipantContainer)
68 82
         });
69 83
     }, [ raiseContext ]);
70 84
 
71
-    const toggleMenu = useCallback(participant => e => {
72
-        const { participant: raisedParticipant } = raiseContext;
85
+    const toggleMenu = useCallback(participantID => e => {
86
+        const { participantID: raisedParticipant } = raiseContext;
73 87
 
74
-        if (raisedParticipant && raisedParticipant === participant) {
88
+        if (raisedParticipant && raisedParticipant === participantID) {
75 89
             lowerMenu();
76 90
         } else {
77
-            raiseMenu(participant, e.target);
91
+            raiseMenu(participantID, e.target);
78 92
         }
79 93
     }, [ raiseContext ]);
80 94
 
@@ -91,20 +105,44 @@ export const MeetingParticipantList = () => {
91 105
         dispatch(openDialog(MuteRemoteParticipantDialog, { participantID: id }));
92 106
     });
93 107
 
108
+    // FIXME:
109
+    // It seems that useTranslation is not very scallable. Unmount 500 components that have the useTranslation hook is
110
+    // taking more than 10s. To workaround the issue we need to pass the texts as props. This is temporary and dirty
111
+    // solution!!!
112
+    // One potential proper fix would be to use react-window component in order to lower the number of components
113
+    // mounted.
114
+    const participantActionEllipsisLabel = t('MeetingParticipantItem.ParticipantActionEllipsis.options');
115
+    const youText = t('chat.you');
116
+    const askUnmuteText = t('participantsPane.actions.askUnmute');
117
+    const muteParticipantButtonText = t('dialog.muteParticipantButton');
118
+
119
+    const renderParticipant = id => (
120
+        <MeetingParticipantItem
121
+            askUnmuteText = { askUnmuteText }
122
+            isHighlighted = { raiseContext.participantID === id }
123
+            key = { id }
124
+            muteAudio = { muteAudio }
125
+            muteParticipantButtonText = { muteParticipantButtonText }
126
+            onContextMenu = { toggleMenu(id) }
127
+            onLeave = { lowerMenu }
128
+            participantActionEllipsisLabel = { participantActionEllipsisLabel }
129
+            participantID = { id }
130
+            youText = { youText } />
131
+    );
132
+
133
+    const items = [];
134
+
135
+    localParticipant && items.push(renderParticipant(localParticipant?.id));
136
+    participants.forEach(p => {
137
+        items.push(renderParticipant(p?.id));
138
+    });
139
+
94 140
     return (
95 141
     <>
96
-        <Heading>{t('participantsPane.headings.participantsList', { count: participants.length })}</Heading>
142
+        <Heading>{t('participantsPane.headings.participantsList', { count: participantsCount })}</Heading>
97 143
         {showInviteButton && <InviteButton />}
98 144
         <div>
99
-            {participants.map(p => (
100
-                <MeetingParticipantItem
101
-                    isHighlighted = { raiseContext.participant === p }
102
-                    key = { p.id }
103
-                    muteAudio = { muteAudio }
104
-                    onContextMenu = { toggleMenu(p) }
105
-                    onLeave = { lowerMenu }
106
-                    participant = { p } />
107
-            ))}
145
+            { items }
108 146
         </div>
109 147
         <MeetingParticipantContextMenu
110 148
             muteAudio = { muteAudio }
@@ -114,4 +152,4 @@ export const MeetingParticipantList = () => {
114 152
             { ...raiseContext } />
115 153
     </>
116 154
     );
117
-};
155
+}

+ 40
- 21
react/features/participants-pane/components/ParticipantItem.js 查看文件

@@ -1,8 +1,6 @@
1 1
 // @flow
2 2
 
3 3
 import React, { type Node } from 'react';
4
-import { useTranslation } from 'react-i18next';
5
-import { useSelector } from 'react-redux';
6 4
 
7 5
 import { Avatar } from '../../base/avatar';
8 6
 import {
@@ -12,7 +10,6 @@ import {
12 10
     IconMicrophoneEmpty,
13 11
     IconMicrophoneEmptySlash
14 12
 } from '../../base/icons';
15
-import { getParticipantDisplayNameWithId } from '../../base/participants';
16 13
 import { ACTION_TRIGGER, MEDIA_STATE, type ActionTrigger, type MediaState } from '../constants';
17 14
 
18 15
 import { RaisedHandIndicator } from './RaisedHandIndicator';
@@ -100,15 +97,20 @@ type Props = {
100 97
      */
101 98
     children: Node,
102 99
 
100
+    /**
101
+     * The name of the participant. Used for showing lobby names.
102
+     */
103
+    displayName: string,
104
+
103 105
     /**
104 106
      * Is this item highlighted/raised
105 107
      */
106 108
     isHighlighted?: boolean,
107 109
 
108 110
     /**
109
-     * The name of the participant. Used for showing lobby names.
111
+     * True if the participant is local.
110 112
      */
111
-    name?: string,
113
+    local: boolean,
112 114
 
113 115
     /**
114 116
      * Callback for when the mouse leaves this component
@@ -116,29 +118,46 @@ type Props = {
116 118
     onLeave?: Function,
117 119
 
118 120
     /**
119
-     * Participant reference
121
+     * The ID of the participant.
122
+     */
123
+    participantID: string,
124
+
125
+    /**
126
+     * True if the participant have raised hand.
120 127
      */
121
-    participant: Object,
128
+    raisedHand: boolean,
122 129
 
123 130
     /**
124 131
      * Media state for video
125 132
      */
126
-    videoMuteState: MediaState
133
+    videoMuteState: MediaState,
134
+
135
+    /**
136
+     * The translated "you" text.
137
+     */
138
+    youText: string
127 139
 }
128 140
 
129
-export const ParticipantItem = ({
141
+/**
142
+ * A component representing a participant entry in ParticipantPane and Lobby.
143
+ *
144
+ * @param {Props} props - The props of the component.
145
+ * @returns {ReactNode}
146
+ */
147
+export default function ParticipantItem({
130 148
     children,
131 149
     isHighlighted,
132 150
     onLeave,
133 151
     actionsTrigger = ACTION_TRIGGER.HOVER,
134 152
     audioMediaState = MEDIA_STATE.NONE,
135 153
     videoMuteState = MEDIA_STATE.NONE,
136
-    name,
137
-    participant: p
138
-}: Props) => {
154
+    displayName,
155
+    participantID,
156
+    local,
157
+    raisedHand,
158
+    youText
159
+}: Props) {
139 160
     const ParticipantActions = Actions[actionsTrigger];
140
-    const { t } = useTranslation();
141
-    const displayName = name || useSelector(getParticipantDisplayNameWithId(p.id));
142 161
 
143 162
     return (
144 163
         <ParticipantContainer
@@ -147,22 +166,22 @@ export const ParticipantItem = ({
147 166
             trigger = { actionsTrigger }>
148 167
             <Avatar
149 168
                 className = 'participant-avatar'
150
-                participantId = { p.id }
169
+                participantId = { participantID }
151 170
                 size = { 32 } />
152 171
             <ParticipantContent>
153 172
                 <ParticipantNameContainer>
154 173
                     <ParticipantName>
155 174
                         { displayName }
156 175
                     </ParticipantName>
157
-                    { p.local ? <span>&nbsp;({t('chat.you')})</span> : null }
176
+                    { local ? <span>&nbsp;({ youText })</span> : null }
158 177
                 </ParticipantNameContainer>
159
-                { !p.local && <ParticipantActions children = { children } /> }
178
+                { !local && <ParticipantActions children = { children } /> }
160 179
                 <ParticipantStates>
161
-                    {p.raisedHand && <RaisedHandIndicator />}
162
-                    {VideoStateIcons[videoMuteState]}
163
-                    {AudioStateIcons[audioMediaState]}
180
+                    { raisedHand && <RaisedHandIndicator /> }
181
+                    { VideoStateIcons[videoMuteState] }
182
+                    { AudioStateIcons[audioMediaState] }
164 183
                 </ParticipantStates>
165 184
             </ParticipantContent>
166 185
         </ParticipantContainer>
167 186
     );
168
-};
187
+}

+ 25
- 15
react/features/participants-pane/components/ParticipantQuickAction.js 查看文件

@@ -1,11 +1,8 @@
1 1
 // @flow
2 2
 
3 3
 import React from 'react';
4
-import { useTranslation } from 'react-i18next';
5
-import { useSelector } from 'react-redux';
6 4
 
7 5
 import { QUICK_ACTION_BUTTON } from '../constants';
8
-import { getQuickActionButtonType } from '../functions';
9 6
 
10 7
 import AskToUnmuteButton from './AskToUnmuteButton';
11 8
 import { QuickActionButton } from './styled';
@@ -13,19 +10,26 @@ import { QuickActionButton } from './styled';
13 10
 type Props = {
14 11
 
15 12
     /**
16
-     * If audio is muted for the current participant.
13
+     * The translated "ask unmute" text.
17 14
      */
18
-    isAudioMuted: Boolean,
15
+    askUnmuteText: string,
16
+
17
+    /**
18
+     * The type of button to be displayed.
19
+     */
20
+    buttonType: string,
19 21
 
20 22
     /**
21 23
      * Callback used to open a confirmation dialog for audio muting.
22 24
      */
23 25
     muteAudio: Function,
24 26
 
27
+    muteParticipantButtonText: string,
28
+
25 29
     /**
26
-     * Participant.
30
+     * The ID of the participant.
27 31
      */
28
-    participant: Object,
32
+    participantID: string,
29 33
 }
30 34
 
31 35
 /**
@@ -34,23 +38,29 @@ type Props = {
34 38
  * @param {Props} props - The props of the component.
35 39
  * @returns {React$Element<'button'>}
36 40
  */
37
-export default function({ isAudioMuted, muteAudio, participant }: Props) {
38
-    const buttonType = useSelector(getQuickActionButtonType(participant, isAudioMuted));
39
-    const { id } = participant;
40
-    const { t } = useTranslation();
41
-
41
+export default function ParticipantQuickAction({
42
+    askUnmuteText,
43
+    buttonType,
44
+    muteAudio,
45
+    muteParticipantButtonText,
46
+    participantID
47
+}: Props) {
42 48
     switch (buttonType) {
43 49
     case QUICK_ACTION_BUTTON.MUTE: {
44 50
         return (
45 51
             <QuickActionButton
46
-                onClick = { muteAudio(id) }
52
+                onClick = { muteAudio(participantID) }
47 53
                 primary = { true }>
48
-                {t('dialog.muteParticipantButton')}
54
+                { muteParticipantButtonText }
49 55
             </QuickActionButton>
50 56
         );
51 57
     }
52 58
     case QUICK_ACTION_BUTTON.ASK_TO_UNMUTE: {
53
-        return <AskToUnmuteButton id = { id } />;
59
+        return (
60
+            <AskToUnmuteButton
61
+                askUnmuteText = { askUnmuteText }
62
+                id = { participantID } />
63
+        );
54 64
     }
55 65
     default: {
56 66
         return null;

+ 234
- 71
react/features/participants-pane/components/ParticipantsPane.js 查看文件

@@ -1,16 +1,15 @@
1 1
 // @flow
2 2
 
3
-import React, { useCallback, useEffect, useState } from 'react';
4
-import { useTranslation } from 'react-i18next';
5
-import { useDispatch, useSelector } from 'react-redux';
3
+import React, { Component } from 'react';
6 4
 import { ThemeProvider } from 'styled-components';
7 5
 
8 6
 import { openDialog } from '../../base/dialog';
7
+import { translate } from '../../base/i18n';
9 8
 import {
10 9
     getParticipantCount,
11
-    isEveryoneModerator,
12 10
     isLocalParticipantModerator
13 11
 } from '../../base/participants';
12
+import { connect } from '../../base/redux';
14 13
 import { MuteEveryoneDialog } from '../../video-menu/components/';
15 14
 import { close } from '../actions';
16 15
 import { classList, findStyledAncestor, getParticipantsPaneOpen } from '../functions';
@@ -30,74 +29,238 @@ import {
30 29
     Header
31 30
 } from './styled';
32 31
 
33
-export const ParticipantsPane = () => {
34
-    const dispatch = useDispatch();
35
-    const paneOpen = useSelector(getParticipantsPaneOpen);
36
-    const isLocalModerator = useSelector(isLocalParticipantModerator);
37
-    const participantsCount = useSelector(getParticipantCount);
38
-    const everyoneModerator = useSelector(isEveryoneModerator);
39
-    const showContextMenu = !everyoneModerator && participantsCount > 2;
32
+/**
33
+ * The type of the React {@code Component} props of {@link ParticipantsPane}.
34
+ */
35
+type Props = {
40 36
 
41
-    const [ contextOpen, setContextOpen ] = useState(false);
42
-    const { t } = useTranslation();
37
+    /**
38
+     * Is the participants pane open.
39
+     */
40
+    _paneOpen: boolean,
43 41
 
44
-    const closePane = useCallback(() => dispatch(close(), [ dispatch ]));
45
-    const closePaneKeyPress = useCallback(e => {
46
-        if (closePane && (e.key === ' ' || e.key === 'Enter')) {
47
-            e.preventDefault();
48
-            closePane();
42
+    /**
43
+     * Whether to show context menu.
44
+     */
45
+    _showContextMenu: boolean,
46
+
47
+    /**
48
+     * Whether to show the footer menu.
49
+     */
50
+    _showFooter: boolean,
51
+
52
+    /**
53
+     * The Redux dispatch function.
54
+     */
55
+    dispatch: Function,
56
+
57
+    /**
58
+     * The i18n translate function.
59
+     */
60
+    t: Function
61
+};
62
+
63
+/**
64
+ * The type of the React {@code Component} state of {@link ParticipantsPane}.
65
+ */
66
+type State = {
67
+
68
+    /**
69
+     * Indicates if the footer context menu is open.
70
+     */
71
+    contextOpen: boolean,
72
+};
73
+
74
+/**
75
+ * Implements the participants list.
76
+ */
77
+class ParticipantsPane extends Component<Props, State> {
78
+    /**
79
+     * Initializes a new {@code ParticipantsPane} instance.
80
+     *
81
+     * @inheritdoc
82
+     */
83
+    constructor(props) {
84
+        super(props);
85
+
86
+        this.state = {
87
+            contextOpen: false
88
+        };
89
+
90
+        // Bind event handlers so they are only bound once per instance.
91
+        this._onClosePane = this._onClosePane.bind(this);
92
+        this._onKeyPress = this._onKeyPress.bind(this);
93
+        this._onMuteAll = this._onMuteAll.bind(this);
94
+        this._onToggleContext = this._onToggleContext.bind(this);
95
+        this._onWindowClickListener = this._onWindowClickListener.bind(this);
96
+    }
97
+
98
+
99
+    /**
100
+     * Implements React's {@link Component#componentDidMount()}.
101
+     *
102
+     * @inheritdoc
103
+     */
104
+    componentDidMount() {
105
+        window.addEventListener('click', this._onWindowClickListener);
106
+    }
107
+
108
+    /**
109
+     * Implements React's {@link Component#componentWillUnmount()}.
110
+     *
111
+     * @inheritdoc
112
+     */
113
+    componentWillUnmount() {
114
+        window.removeEventListener('click', this._onWindowClickListener);
115
+    }
116
+
117
+    /**
118
+     * Implements React's {@link Component#render}.
119
+     *
120
+     * @inheritdoc
121
+     */
122
+    render() {
123
+        const {
124
+            _paneOpen,
125
+            _showContextMenu,
126
+            _showFooter,
127
+            t
128
+        } = this.props;
129
+
130
+        // when the pane is not open optimize to not
131
+        // execute the MeetingParticipantList render for large list of participants
132
+        if (!_paneOpen) {
133
+            return null;
49 134
         }
50
-    }, [ closePane ]);
51
-    const muteAll = useCallback(() => dispatch(openDialog(MuteEveryoneDialog)), [ dispatch ]);
52
-
53
-    useEffect(() => {
54
-        const handler = [ 'click', e => {
55
-            if (!findStyledAncestor(e.target, FooterEllipsisContainer)) {
56
-                setContextOpen(false);
57
-            }
58
-        } ];
59
-
60
-        window.addEventListener(...handler);
61
-
62
-        return () => window.removeEventListener(...handler);
63
-    }, [ contextOpen ]);
64
-
65
-    const toggleContext = useCallback(() => setContextOpen(!contextOpen), [ contextOpen, setContextOpen ]);
66
-
67
-    return (
68
-        <ThemeProvider theme = { theme }>
69
-            <div className = { classList('participants_pane', !paneOpen && 'participants_pane--closed') }>
70
-                <div className = 'participants_pane-content'>
71
-                    <Header>
72
-                        <Close
73
-                            aria-label = { t('participantsPane.close', 'Close') }
74
-                            onClick = { closePane }
75
-                            onKeyPress = { closePaneKeyPress }
76
-                            role = 'button'
77
-                            tabIndex = { 0 } />
78
-                    </Header>
79
-                    <Container>
80
-                        <LobbyParticipantList />
81
-                        <AntiCollapse />
82
-                        <MeetingParticipantList />
83
-                    </Container>
84
-                    {isLocalModerator && (
85
-                        <Footer>
86
-                            <FooterButton onClick = { muteAll }>
87
-                                {t('participantsPane.actions.muteAll')}
88
-                            </FooterButton>
89
-                            {showContextMenu && (
90
-                                <FooterEllipsisContainer>
91
-                                    <FooterEllipsisButton
92
-                                        id = 'participants-pane-context-menu'
93
-                                        onClick = { toggleContext } />
94
-                                    {contextOpen && <FooterContextMenu onMouseLeave = { toggleContext } />}
95
-                                </FooterEllipsisContainer>
96
-                            )}
97
-                        </Footer>
98
-                    )}
135
+
136
+        return (
137
+            <ThemeProvider theme = { theme }>
138
+                <div className = { classList('participants_pane', !_paneOpen && 'participants_pane--closed') }>
139
+                    <div className = 'participants_pane-content'>
140
+                        <Header>
141
+                            <Close
142
+                                aria-label = { t('participantsPane.close', 'Close') }
143
+                                onClick = { this._onClosePane }
144
+                                onKeyPress = { this._onKeyPress }
145
+                                role = 'button'
146
+                                tabIndex = { 0 } />
147
+                        </Header>
148
+                        <Container>
149
+                            <LobbyParticipantList />
150
+                            <AntiCollapse />
151
+                            <MeetingParticipantList />
152
+                        </Container>
153
+                        {_showFooter && (
154
+                            <Footer>
155
+                                <FooterButton onClick = { this._onMuteAll }>
156
+                                    {t('participantsPane.actions.muteAll')}
157
+                                </FooterButton>
158
+                                {_showContextMenu && (
159
+                                    <FooterEllipsisContainer>
160
+                                        <FooterEllipsisButton
161
+                                            id = 'participants-pane-context-menu'
162
+                                            onClick = { this._onToggleContext } />
163
+                                        {this.state.contextOpen
164
+                                            && <FooterContextMenu onMouseLeave = { this._onToggleContext } />}
165
+                                    </FooterEllipsisContainer>
166
+                                )}
167
+                            </Footer>
168
+                        )}
169
+                    </div>
99 170
                 </div>
100
-            </div>
101
-        </ThemeProvider>
102
-    );
103
-};
171
+            </ThemeProvider>
172
+        );
173
+    }
174
+
175
+    _onClosePane: () => void;
176
+
177
+    /**
178
+     * Callback for closing the participant pane.
179
+     *
180
+     * @private
181
+     * @returns {void}
182
+     */
183
+    _onClosePane() {
184
+        this.props.dispatch(close());
185
+    }
186
+
187
+    _onKeyPress: (Object) => void;
188
+
189
+    /**
190
+     * KeyPress handler for accessibility for closing the participants pane.
191
+     *
192
+     * @param {Object} e - The key event to handle.
193
+     *
194
+     * @returns {void}
195
+     */
196
+    _onKeyPress(e) {
197
+        if (e.key === ' ' || e.key === 'Enter') {
198
+            e.preventDefault();
199
+            this._onClosePane();
200
+        }
201
+    }
202
+
203
+    _onMuteAll: () => void;
204
+
205
+    /**
206
+     * The handler for clicking mute all button.
207
+     *
208
+     * @returns {void}
209
+     */
210
+    _onMuteAll() {
211
+        this.props.dispatch(openDialog(MuteEveryoneDialog));
212
+    }
213
+
214
+    _onToggleContext: () => void;
215
+
216
+    /**
217
+     * Handler for toggling open/close of the footer context menu.
218
+     *
219
+     * @returns {void}
220
+     */
221
+    _onToggleContext() {
222
+        this.setState({
223
+            contextOpen: !this.state.contextOpen
224
+        });
225
+    }
226
+
227
+    _onWindowClickListener: (event: Object) => void;
228
+
229
+    /**
230
+     * Window click event listener.
231
+     *
232
+     * @param {Event} e - The click event.
233
+     * @returns {void}
234
+     */
235
+    _onWindowClickListener(e) {
236
+        if (this.state.contextOpen && !findStyledAncestor(e.target, FooterEllipsisContainer)) {
237
+            this.setState({
238
+                contextOpen: false
239
+            });
240
+        }
241
+    }
242
+}
243
+
244
+/**
245
+ * Maps (parts of) the redux state to the React {@code Component} props of
246
+ * {@code ParticipantsPane}.
247
+ *
248
+ * @param {Object} state - The redux state.
249
+ * @protected
250
+ * @returns {{
251
+ *     _paneOpen: boolean,
252
+ *     _showContextMenu: boolean,
253
+ *     _showFooter: boolean
254
+ * }}
255
+ */
256
+function _mapStateToProps(state: Object) {
257
+    const isPaneOpen = getParticipantsPaneOpen(state);
258
+
259
+    return {
260
+        _paneOpen: isPaneOpen,
261
+        _showContextMenu: isPaneOpen && getParticipantCount(state) > 2,
262
+        _showFooter: isPaneOpen && isLocalParticipantModerator(state)
263
+    };
264
+}
265
+
266
+export default translate(connect(_mapStateToProps)(ParticipantsPane));

+ 1
- 4
react/features/participants-pane/components/index.js 查看文件

@@ -1,10 +1,7 @@
1 1
 export * from './InviteButton';
2 2
 export * from './LobbyParticipantItem';
3 3
 export * from './LobbyParticipantList';
4
-export * from './MeetingParticipantContextMenu';
5
-export * from './MeetingParticipantItem';
6 4
 export * from './MeetingParticipantList';
7
-export * from './ParticipantItem';
8
-export * from './ParticipantsPane';
5
+export { default as ParticipantsPane } from './ParticipantsPane';
9 6
 export * from './ParticipantsPaneButton';
10 7
 export * from './RaisedHandIndicator';

+ 18
- 15
react/features/participants-pane/functions.js 查看文件

@@ -41,13 +41,14 @@ export const findStyledAncestor = (target: Object, component: any) => {
41 41
 };
42 42
 
43 43
 /**
44
- * Returns a selector used to determine if a participant is force muted.
44
+ * Checks if a participant is force muted.
45 45
  *
46
- * @param {Object} participant - The participant id.
46
+ * @param {Object} participant - The participant.
47 47
  * @param {MediaType} mediaType - The media type.
48
- * @returns {MediaState}.
48
+ * @param {Object} state - The redux state.
49
+ * @returns {MediaState}
49 50
  */
50
-export const isForceMuted = (participant: Object, mediaType: MediaType) => (state: Object) => {
51
+export function isForceMuted(participant: Object, mediaType: MediaType, state: Object) {
51 52
     if (getParticipantCount(state) > 2 && isEnabledFromState(mediaType, state)) {
52 53
         if (participant.local) {
53 54
             return !isLocalParticipantApprovedFromState(mediaType, state);
@@ -62,18 +63,19 @@ export const isForceMuted = (participant: Object, mediaType: MediaType) => (stat
62 63
     }
63 64
 
64 65
     return false;
65
-};
66
+}
66 67
 
67 68
 /**
68
- * Returns a selector used to determine the audio media state (the mic icon) for a participant.
69
+ * Determines the audio media state (the mic icon) for a participant.
69 70
  *
70 71
  * @param {Object} participant - The participant.
71 72
  * @param {boolean} muted - The mute state of the participant.
72
- * @returns {MediaState}.
73
+ * @param {Object} state - The redux state.
74
+ * @returns {MediaState}
73 75
  */
74
-export const getParticipantAudioMediaState = (participant: Object, muted: Boolean) => (state: Object) => {
76
+export function getParticipantAudioMediaState(participant: Object, muted: Boolean, state: Object) {
75 77
     if (muted) {
76
-        if (isForceMuted(participant, MEDIA_TYPE.AUDIO)(state)) {
78
+        if (isForceMuted(participant, MEDIA_TYPE.AUDIO, state)) {
77 79
             return MEDIA_STATE.FORCE_MUTED;
78 80
         }
79 81
 
@@ -81,7 +83,7 @@ export const getParticipantAudioMediaState = (participant: Object, muted: Boolea
81 83
     }
82 84
 
83 85
     return MEDIA_STATE.UNMUTED;
84
-};
86
+}
85 87
 
86 88
 
87 89
 /**
@@ -125,17 +127,18 @@ const getState = (state: Object) => state[REDUCER_KEY];
125 127
 export const getParticipantsPaneOpen = (state: Object) => Boolean(getState(state)?.isOpen);
126 128
 
127 129
 /**
128
- * Returns a selector used to determine the type of quick action button to be displayed for a participant.
130
+ * Returns the type of quick action button to be displayed for a participant.
129 131
  * The button is displayed when hovering a participant from the participant list.
130 132
  *
131 133
  * @param {Object} participant - The participant.
132 134
  * @param {boolean} isAudioMuted - If audio is muted for the participant.
133
- * @returns {Function}
135
+ * @param {Object} state - The redux state.
136
+ * @returns {string} - The type of the quick action button.
134 137
  */
135
-export const getQuickActionButtonType = (participant: Object, isAudioMuted: Boolean) => (state: Object) => {
138
+export function getQuickActionButtonType(participant: Object, isAudioMuted: Boolean, state: Object) {
136 139
     // handled only by moderators
137 140
     if (isLocalParticipantModerator(state)) {
138
-        if (isForceMuted(participant, MEDIA_TYPE.AUDIO)(state)) {
141
+        if (isForceMuted(participant, MEDIA_TYPE.AUDIO, state)) {
139 142
             return QUICK_ACTION_BUTTON.ASK_TO_UNMUTE;
140 143
         }
141 144
         if (!isAudioMuted) {
@@ -144,7 +147,7 @@ export const getQuickActionButtonType = (participant: Object, isAudioMuted: Bool
144 147
     }
145 148
 
146 149
     return QUICK_ACTION_BUTTON.NONE;
147
-};
150
+}
148 151
 
149 152
 /**
150 153
  * Returns true if the invite button should be rendered.

+ 1
- 1
react/features/shared-video/components/web/SharedVideo.js 查看文件

@@ -144,7 +144,7 @@ function _mapStateToProps(state) {
144 144
         clientHeight,
145 145
         clientWidth,
146 146
         filmstripVisible: visible,
147
-        isOwner: ownerId === localParticipant.id,
147
+        isOwner: ownerId === localParticipant?.id,
148 148
         videoUrl
149 149
     };
150 150
 }

+ 12
- 4
react/features/shared-video/functions.js 查看文件

@@ -1,6 +1,6 @@
1 1
 // @flow
2 2
 
3
-import { getParticipants } from '../base/participants';
3
+import { getFakeParticipants } from '../base/participants';
4 4
 
5 5
 import { VIDEO_PLAYER_PARTICIPANT_NAME, YOUTUBE_PLAYER_PARTICIPANT_NAME } from './constants';
6 6
 
@@ -41,7 +41,15 @@ export function isSharingStatus(status: string) {
41 41
  * @returns {boolean}
42 42
  */
43 43
 export function isVideoPlaying(stateful: Object | Function): boolean {
44
-    return Boolean(getParticipants(stateful).find(p => p.isFakeParticipant
45
-        && (p.name === VIDEO_PLAYER_PARTICIPANT_NAME || p.name === YOUTUBE_PLAYER_PARTICIPANT_NAME))
46
-    );
44
+    let videoPlaying = false;
45
+
46
+    // eslint-disable-next-line no-unused-vars
47
+    for (const [ id, p ] of getFakeParticipants(stateful)) {
48
+        if (p.name === VIDEO_PLAYER_PARTICIPANT_NAME || p.name === YOUTUBE_PLAYER_PARTICIPANT_NAME) {
49
+            videoPlaying = true;
50
+            break;
51
+        }
52
+    }
53
+
54
+    return videoPlaying;
47 55
 }

+ 2
- 1
react/features/subtitles/components/Captions.web.js 查看文件

@@ -2,6 +2,7 @@
2 2
 
3 3
 import React from 'react';
4 4
 
5
+import { getParticipantCountWithFake } from '../../base/participants';
5 6
 import { connect } from '../../base/redux';
6 7
 
7 8
 import {
@@ -75,7 +76,7 @@ class Captions
75 76
 function mapStateToProps(state) {
76 77
     return {
77 78
         ..._abstractMapStateToProps(state),
78
-        _isLifted: state['features/base/participants'].length < 2
79
+        _isLifted: getParticipantCountWithFake(state) < 2
79 80
     };
80 81
 }
81 82
 

+ 35
- 33
react/features/toolbox/components/web/Toolbox.js 查看文件

@@ -17,7 +17,7 @@ import { translate } from '../../../base/i18n';
17 17
 import JitsiMeetJS from '../../../base/lib-jitsi-meet';
18 18
 import {
19 19
     getLocalParticipant,
20
-    getParticipants,
20
+    haveParticipantWithScreenSharingFeature,
21 21
     raiseHand
22 22
 } from '../../../base/participants';
23 23
 import { connect } from '../../../base/redux';
@@ -183,15 +183,20 @@ type Props = {
183 183
     _raisedHand: boolean,
184 184
 
185 185
     /**
186
-     * Whether or not the local participant is screensharing.
186
+     * Whether or not the local participant is screenSharing.
187 187
      */
188
-    _screensharing: boolean,
188
+    _screenSharing: boolean,
189 189
 
190 190
     /**
191 191
      * Whether or not the local participant is sharing a YouTube video.
192 192
      */
193 193
     _sharingVideo: boolean,
194 194
 
195
+    /**
196
+     * The enabled buttons.
197
+     */
198
+    _toolbarButtons: Array<string>,
199
+
195 200
     /**
196 201
      * Flag showing whether toolbar is visible.
197 202
      */
@@ -202,11 +207,6 @@ type Props = {
202 207
      */
203 208
     _visibleButtons: Array<string>,
204 209
 
205
-    /**
206
-     * Handler to check if a button is enabled.
207
-     */
208
-     _shouldShowButton: Function,
209
-
210 210
     /**
211 211
      * Returns the selected virtual source object.
212 212
      */
@@ -269,38 +269,39 @@ class Toolbox extends Component<Props> {
269 269
      * @returns {void}
270 270
      */
271 271
     componentDidMount() {
272
+        const { _toolbarButtons } = this.props;
272 273
         const KEYBOARD_SHORTCUTS = [
273
-            this.props._shouldShowButton('videoquality') && {
274
+            isToolbarButtonEnabled('videoquality', _toolbarButtons) && {
274 275
                 character: 'A',
275 276
                 exec: this._onShortcutToggleVideoQuality,
276 277
                 helpDescription: 'toolbar.callQuality'
277 278
             },
278
-            this.props._shouldShowButton('chat') && {
279
+            isToolbarButtonEnabled('chat', _toolbarButtons) && {
279 280
                 character: 'C',
280 281
                 exec: this._onShortcutToggleChat,
281 282
                 helpDescription: 'keyboardShortcuts.toggleChat'
282 283
             },
283
-            this.props._shouldShowButton('desktop') && {
284
+            isToolbarButtonEnabled('desktop', _toolbarButtons) && {
284 285
                 character: 'D',
285 286
                 exec: this._onShortcutToggleScreenshare,
286 287
                 helpDescription: 'keyboardShortcuts.toggleScreensharing'
287 288
             },
288
-            this.props._shouldShowButton('participants-pane') && {
289
+            isToolbarButtonEnabled('participants-pane', _toolbarButtons) && {
289 290
                 character: 'P',
290 291
                 exec: this._onShortcutToggleParticipantsPane,
291 292
                 helpDescription: 'keyboardShortcuts.toggleParticipantsPane'
292 293
             },
293
-            this.props._shouldShowButton('raisehand') && {
294
+            isToolbarButtonEnabled('raisehand', _toolbarButtons) && {
294 295
                 character: 'R',
295 296
                 exec: this._onShortcutToggleRaiseHand,
296 297
                 helpDescription: 'keyboardShortcuts.raiseHand'
297 298
             },
298
-            this.props._shouldShowButton('fullscreen') && {
299
+            isToolbarButtonEnabled('fullscreen', _toolbarButtons) && {
299 300
                 character: 'S',
300 301
                 exec: this._onShortcutToggleFullScreen,
301 302
                 helpDescription: 'keyboardShortcuts.fullScreen'
302 303
             },
303
-            this.props._shouldShowButton('tileview') && {
304
+            isToolbarButtonEnabled('tileview', _toolbarButtons) && {
304 305
                 character: 'W',
305 306
                 exec: this._onShortcutToggleTileView,
306 307
                 helpDescription: 'toolbar.tileViewToggle'
@@ -509,7 +510,7 @@ class Toolbox extends Component<Props> {
509 510
         const {
510 511
             _feedbackConfigured,
511 512
             _isMobile,
512
-            _screensharing
513
+            _screenSharing
513 514
         } = this.props;
514 515
 
515 516
         const microphone = {
@@ -644,7 +645,7 @@ class Toolbox extends Component<Props> {
644 645
             group: 3
645 646
         };
646 647
 
647
-        const virtualBackground = !_screensharing && checkBlurSupport() && {
648
+        const virtualBackground = !_screenSharing && checkBlurSupport() && {
648 649
             key: 'select-background',
649 650
             Content: VideoBackgroundButton,
650 651
             group: 3
@@ -734,12 +735,12 @@ class Toolbox extends Component<Props> {
734 735
     _getVisibleButtons() {
735 736
         const {
736 737
             _clientWidth,
737
-            _shouldShowButton
738
+            _toolbarButtons
738 739
         } = this.props;
739 740
 
740 741
 
741 742
         const buttons = this._getAllButtons();
742
-        const isHangupVisible = _shouldShowButton('hangup');
743
+        const isHangupVisible = isToolbarButtonEnabled('hangup', _toolbarButtons);
743 744
         const { order } = THRESHOLDS.find(({ width }) => _clientWidth > width)
744 745
             || THRESHOLDS[THRESHOLDS.length - 1];
745 746
         let sliceIndex = order.length + 2;
@@ -749,7 +750,7 @@ class Toolbox extends Component<Props> {
749 750
         const filtered = [
750 751
             ...order.map(key => buttons[key]),
751 752
             ...Object.values(buttons).filter((button, index) => !order.includes(keys[index]))
752
-        ].filter(Boolean).filter(({ key }) => _shouldShowButton(key));
753
+        ].filter(Boolean).filter(({ key }) => isToolbarButtonEnabled(key, _toolbarButtons));
753 754
 
754 755
         if (isHangupVisible) {
755 756
             sliceIndex -= 1;
@@ -934,7 +935,7 @@ class Toolbox extends Component<Props> {
934 935
                 'toggle.screen.sharing',
935 936
                 ACTION_SHORTCUT_TRIGGERED,
936 937
                 {
937
-                    enable: !this.props._screensharing
938
+                    enable: !this.props._screenSharing
938 939
                 }));
939 940
 
940 941
         this._doToggleScreenshare();
@@ -1053,7 +1054,7 @@ class Toolbox extends Component<Props> {
1053 1054
         sendAnalytics(createToolbarEvent(
1054 1055
             'toggle.screen.sharing',
1055 1056
             ACTION_SHORTCUT_TRIGGERED,
1056
-            { enable: !this.props._screensharing }));
1057
+            { enable: !this.props._screenSharing }));
1057 1058
 
1058 1059
         this._closeOverflowMenuIfOpen();
1059 1060
         this._doToggleScreenshare();
@@ -1116,6 +1117,7 @@ class Toolbox extends Component<Props> {
1116 1117
         const {
1117 1118
             _isMobile,
1118 1119
             _overflowMenuVisible,
1120
+            _toolbarButtons,
1119 1121
             t
1120 1122
         } = this.props;
1121 1123
 
@@ -1169,7 +1171,7 @@ class Toolbox extends Component<Props> {
1169 1171
                         <HangupButton
1170 1172
                             customClass = 'hangup-button'
1171 1173
                             key = 'hangup-button'
1172
-                            visible = { this.props._shouldShowButton('hangup') } />
1174
+                            visible = { isToolbarButtonEnabled('hangup', _toolbarButtons) } />
1173 1175
                     </div>
1174 1176
                 </div>
1175 1177
             </div>
@@ -1203,12 +1205,12 @@ function _mapStateToProps(state) {
1203 1205
     let desktopSharingDisabledTooltipKey;
1204 1206
 
1205 1207
     if (enableFeaturesBasedOnToken) {
1206
-        // we enable desktop sharing if any participant already have this
1207
-        // feature enabled
1208
-        desktopSharingEnabled = getParticipants(state)
1209
-            .find(({ features = {} }) =>
1210
-                String(features['screen-sharing']) === 'true') !== undefined;
1211
-        desktopSharingDisabledTooltipKey = 'dialog.shareYourScreenDisabled';
1208
+        if (desktopSharingEnabled) {
1209
+            // we enable desktop sharing if any participant already have this
1210
+            // feature enabled and if the user supports it.
1211
+            desktopSharingEnabled = haveParticipantWithScreenSharingFeature(state);
1212
+            desktopSharingDisabledTooltipKey = 'dialog.shareYourScreenDisabled';
1213
+        }
1212 1214
     }
1213 1215
 
1214 1216
     return {
@@ -1226,13 +1228,13 @@ function _mapStateToProps(state) {
1226 1228
         _isVpaasMeeting: isVpaasMeeting(state),
1227 1229
         _fullScreen: fullScreen,
1228 1230
         _tileViewEnabled: shouldDisplayTileView(state),
1229
-        _localParticipantID: localParticipant.id,
1231
+        _localParticipantID: localParticipant?.id,
1230 1232
         _localVideo: localVideo,
1231 1233
         _overflowMenuVisible: overflowMenuVisible,
1232 1234
         _participantsPaneOpen: getParticipantsPaneOpen(state),
1233
-        _raisedHand: localParticipant.raisedHand,
1234
-        _screensharing: isScreenVideoShared(state),
1235
-        _shouldShowButton: buttonName => isToolbarButtonEnabled(buttonName)(state),
1235
+        _raisedHand: localParticipant?.raisedHand,
1236
+        _screenSharing: isScreenVideoShared(state),
1237
+        _toolbarButtons: getToolbarButtons(state),
1236 1238
         _visible: isToolboxVisible(state),
1237 1239
         _visibleButtons: getToolbarButtons(state)
1238 1240
     };

+ 2
- 1
react/features/toolbox/functions.native.js 查看文件

@@ -2,6 +2,7 @@
2 2
 
3 3
 import { hasAvailableDevices } from '../base/devices';
4 4
 import { TOOLBOX_ALWAYS_VISIBLE, getFeatureFlag, TOOLBOX_ENABLED } from '../base/flags';
5
+import { getParticipantCountWithFake } from '../base/participants';
5 6
 import { toState } from '../base/redux';
6 7
 import { isLocalVideoTrackDesktop } from '../base/tracks';
7 8
 
@@ -60,7 +61,7 @@ export function getMovableButtons(width: number): Set<string> {
60 61
 export function isToolboxVisible(stateful: Object | Function) {
61 62
     const state = toState(stateful);
62 63
     const { alwaysVisible, enabled, visible } = state['features/toolbox'];
63
-    const { length: participantCount } = state['features/base/participants'];
64
+    const participantCount = getParticipantCountWithFake(state);
64 65
     const alwaysVisibleFlag = getFeatureFlag(state, TOOLBOX_ALWAYS_VISIBLE, false);
65 66
     const enabledFlag = getFeatureFlag(state, TOOLBOX_ENABLED, true);
66 67
 

+ 3
- 2
react/features/video-layout/functions.js 查看文件

@@ -5,7 +5,8 @@ import { getFeatureFlag, TILE_VIEW_ENABLED } from '../base/flags';
5 5
 import {
6 6
     getPinnedParticipant,
7 7
     getParticipantCount,
8
-    pinParticipant
8
+    pinParticipant,
9
+    getParticipantCountWithFake
9 10
 } from '../base/participants';
10 11
 import {
11 12
     ASPECT_RATIO_BREAKPOINT,
@@ -101,7 +102,7 @@ export function getTileViewGridDimensions(state: Object) {
101 102
     // When in tile view mode, we must discount ourselves (the local participant) because our
102 103
     // tile is not visible.
103 104
     const { iAmRecorder } = state['features/base/config'];
104
-    const numberOfParticipants = state['features/base/participants'].length - (iAmRecorder ? 1 : 0);
105
+    const numberOfParticipants = getParticipantCountWithFake(state) - (iAmRecorder ? 1 : 0);
105 106
 
106 107
     const columnsToMaintainASquare = Math.ceil(Math.sqrt(numberOfParticipants));
107 108
     const columns = Math.min(columnsToMaintainASquare, maxColumns);

+ 13
- 9
react/features/video-menu/actions.any.js 查看文件

@@ -20,6 +20,7 @@ import {
20 20
 } from '../base/media';
21 21
 import {
22 22
     getLocalParticipant,
23
+    getRemoteParticipants,
23 24
     muteRemoteParticipant
24 25
 } from '../base/participants';
25 26
 
@@ -91,14 +92,17 @@ export function muteAllParticipants(exclude: Array<string>, mediaType: MEDIA_TYP
91 92
     return (dispatch: Dispatch<any>, getState: Function) => {
92 93
         const state = getState();
93 94
         const localId = getLocalParticipant(state).id;
94
-        const participantIds = state['features/base/participants']
95
-            .map(p => p.id);
96
-
97
-        /* eslint-disable no-confusing-arrow */
98
-        participantIds
99
-            .filter(id => !exclude.includes(id))
100
-            .map(id => id === localId ? muteLocal(true, mediaType) : muteRemote(id, mediaType))
101
-            .map(dispatch);
102
-        /* eslint-enable no-confusing-arrow */
95
+
96
+        if (!exclude.includes(localId)) {
97
+            dispatch(muteLocal(true, mediaType));
98
+        }
99
+
100
+        getRemoteParticipants(state).forEach((p, id) => {
101
+            if (exclude.includes(id)) {
102
+                return;
103
+            }
104
+
105
+            dispatch(muteRemote(id, mediaType));
106
+        });
103 107
     };
104 108
 }

正在加载...
取消
保存