浏览代码

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
     getParticipantById,
23
     getParticipantById,
24
     pinParticipant,
24
     pinParticipant,
25
     kickParticipant,
25
     kickParticipant,
26
-    raiseHand
26
+    raiseHand,
27
+    isParticipantModerator
27
 } from '../../react/features/base/participants';
28
 } from '../../react/features/base/participants';
28
 import { updateSettings } from '../../react/features/base/settings';
29
 import { updateSettings } from '../../react/features/base/settings';
29
 import { isToggleCameraEnabled, toggleCamera } from '../../react/features/base/tracks';
30
 import { isToggleCameraEnabled, toggleCamera } from '../../react/features/base/tracks';
105
             const muteMediaType = mediaType ? mediaType : MEDIA_TYPE.AUDIO;
106
             const muteMediaType = mediaType ? mediaType : MEDIA_TYPE.AUDIO;
106
 
107
 
107
             sendAnalytics(createApiEvent('muted-everyone'));
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
         'toggle-lobby': isLobbyEnabled => {
118
         'toggle-lobby': isLobbyEnabled => {
117
             APP.store.dispatch(toggleLobbyMode(isLobbyEnabled));
119
             APP.store.dispatch(toggleLobbyMode(isLobbyEnabled));

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

49
 /**
49
 /**
50
  * Hides the notification with the participant that asked to unmute audio.
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
  * @returns {Object}
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
  * Hides the notification with the participant that asked to unmute.
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
  * @param {MediaType} mediaType - The media type.
63
  * @param {MediaType} mediaType - The media type.
64
  * @returns {Object}
64
  * @returns {Object}
65
  */
65
  */
66
-export function dismissPendingParticipant(id: string, mediaType: MediaType) {
66
+export function dismissPendingParticipant(participant: Object, mediaType: MediaType) {
67
     return {
67
     return {
68
         type: DISMISS_PENDING_PARTICIPANT,
68
         type: DISMISS_PENDING_PARTICIPANT,
69
-        id,
69
+        participant,
70
         mediaType
70
         mediaType
71
     };
71
     };
72
 }
72
 }
145
 /**
145
 /**
146
  * Shows a notification with the participant that asked to audio unmute.
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
  * @returns {Object}
149
  * @returns {Object}
150
  */
150
  */
151
-export function participantPendingAudio(id: string) {
151
+export function participantPendingAudio(participant: Object) {
152
     return {
152
     return {
153
         type: PARTICIPANT_PENDING_AUDIO,
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
 import { useSelector } from 'react-redux';
3
 import { useSelector } from 'react-redux';
4
 
4
 
5
 import NotificationWithParticipants from '../../notifications/components/web/NotificationWithParticipants';
5
 import NotificationWithParticipants from '../../notifications/components/web/NotificationWithParticipants';
6
-import { approveAudio, dismissPendingAudioParticipant } from '../actions';
6
+import {
7
+    approveParticipant,
8
+    dismissPendingAudioParticipant
9
+} from '../actions';
7
 import { getParticipantsAskingToAudioUnmute } from '../functions';
10
 import { getParticipantsAskingToAudioUnmute } from '../functions';
8
 
11
 
9
 
12
 
25
                 </div>
28
                 </div>
26
                 <NotificationWithParticipants
29
                 <NotificationWithParticipants
27
                     approveButtonText = { t('notify.unmute') }
30
                     approveButtonText = { t('notify.unmute') }
28
-                    onApprove = { approveAudio }
31
+                    onApprove = { approveParticipant }
29
                     onReject = { dismissPendingAudioParticipant }
32
                     onReject = { dismissPendingAudioParticipant }
30
                     participants = { participants }
33
                     participants = { participants }
31
                     rejectButtonText = { t('dialog.dismiss') }
34
                     rejectButtonText = { t('dialog.dismiss') }

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

1
 // @flow
1
 // @flow
2
 
2
 
3
 import { MEDIA_TYPE, type MediaType } from '../base/media/constants';
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
 import { MEDIA_TYPE_TO_WHITELIST_STORE_KEY, MEDIA_TYPE_TO_PENDING_STORE_KEY } from './constants';
6
 import { MEDIA_TYPE_TO_WHITELIST_STORE_KEY, MEDIA_TYPE_TO_PENDING_STORE_KEY } from './constants';
7
 
7
 
13
  */
13
  */
14
 const getState = state => state['features/av-moderation'];
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
  * Returns whether moderation is enabled per media type.
25
  * Returns whether moderation is enabled per media type.
18
  *
26
  *
33
  */
41
  */
34
 export const isEnabled = (mediaType: MediaType) => (state: Object) => isEnabledFromState(mediaType, state);
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
  * Returns whether local participant is approved to unmute a media type.
56
  * Returns whether local participant is approved to unmute a media type.
38
  *
57
  *
74
 /**
93
 /**
75
  * Returns a selector creator which determines if the participant is pending or not for a media type.
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
  * @param {MEDIA_TYPE} mediaType - The media type to check.
97
  * @param {MEDIA_TYPE} mediaType - The media type to check.
79
  * @returns {boolean}
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
     const storeKey = MEDIA_TYPE_TO_PENDING_STORE_KEY[mediaType];
101
     const storeKey = MEDIA_TYPE_TO_PENDING_STORE_KEY[mediaType];
83
     const arr = getState(state)[storeKey];
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
  */
113
  */
95
 export const getParticipantsAskingToAudioUnmute = (state: Object) => {
114
 export const getParticipantsAskingToAudioUnmute = (state: Object) => {
96
     if (isLocalParticipantModerator(state)) {
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
 
127
 
128
         // this is handled only by moderators
128
         // this is handled only by moderators
129
         if (audioModerationEnabled && isLocalParticipantModerator(state)) {
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
                 // if participant raises hand show notification
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
             } else {
136
             } else {
136
                 // if participant lowers hand hide notification
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
 /* @flow */
1
 /* @flow */
2
 
2
 
3
 import { MEDIA_TYPE } from '../base/media/constants';
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
 import { ReducerRegistry } from '../base/redux';
9
 import { ReducerRegistry } from '../base/redux';
5
 
10
 
6
 import {
11
 import {
11
     PARTICIPANT_APPROVED,
16
     PARTICIPANT_APPROVED,
12
     PARTICIPANT_PENDING_AUDIO
17
     PARTICIPANT_PENDING_AUDIO
13
 } from './actionTypes';
18
 } from './actionTypes';
19
+import { MEDIA_TYPE_TO_PENDING_STORE_KEY } from './constants';
14
 
20
 
15
 const initialState = {
21
 const initialState = {
16
     audioModerationEnabled: false,
22
     audioModerationEnabled: false,
21
     pendingVideo: []
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
 ReducerRegistry.register('features/av-moderation', (state = initialState, action) => {
65
 ReducerRegistry.register('features/av-moderation', (state = initialState, action) => {
25
 
66
 
26
     switch (action.type) {
67
     switch (action.type) {
65
     }
106
     }
66
 
107
 
67
     case PARTICIPANT_PENDING_AUDIO: {
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
             const updated = [ ...state.pendingAudio ];
113
             const updated = [ ...state.pendingAudio ];
73
 
114
 
74
-            updated.push(id);
115
+            updated.push(participant);
75
 
116
 
76
             return {
117
             return {
77
                 ...state,
118
                 ...state,
82
         return state;
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
     case DISMISS_PENDING_PARTICIPANT: {
185
     case DISMISS_PENDING_PARTICIPANT: {
86
-        const { id, mediaType } = action;
186
+        const { participant, mediaType } = action;
87
 
187
 
88
         if (mediaType === MEDIA_TYPE.AUDIO) {
188
         if (mediaType === MEDIA_TYPE.AUDIO) {
89
             return {
189
             return {
90
                 ...state,
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
         if (mediaType === MEDIA_TYPE.VIDEO) {
195
         if (mediaType === MEDIA_TYPE.VIDEO) {
96
             return {
196
             return {
97
                 ...state,
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
         return next(action);
398
         return next(action);
399
     }
399
     }
400
 
400
 
401
-    const participants = state['features/base/participants'];
402
     const id = action.participant.id;
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
     const actionName = id ? ACTION_PINNED : ACTION_UNPINNED;
404
     const actionName = id ? ACTION_PINNED : ACTION_UNPINNED;
406
     const local
405
     const local
407
         = (participantById && participantById.local)
406
         = (participantById && participantById.local)

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

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
  * @param {string} buttonName - The name of the button.
61
  * @param {string} buttonName - The name of the button.
62
  * {@link interfaceConfig}.
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
 /**
79
 /**
80
  * Returns local participant from Redux state.
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
  * {@code getState} function to be used to retrieve the state
83
  * {@code getState} function to be used to retrieve the state
85
  * features/base/participants.
84
  * features/base/participants.
86
  * @returns {(Participant|undefined)}
85
  * @returns {(Participant|undefined)}
87
  */
86
  */
88
 export function getLocalParticipant(stateful: Object | Function) {
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
 /**
108
 /**
110
  * Returns participant by ID from Redux state.
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
  * {@code getState} function to be used to retrieve the state
112
  * {@code getState} function to be used to retrieve the state
115
  * features/base/participants.
113
  * features/base/participants.
116
  * @param {string} id - The ID of the participant to retrieve.
114
  * @param {string} id - The ID of the participant to retrieve.
119
  */
117
  */
120
 export function getParticipantById(
118
 export function getParticipantById(
121
         stateful: Object | Function, id: string): ?Object {
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
  * Returns a count of the known participants in the passed in redux state,
141
  * Returns a count of the known participants in the passed in redux state,
129
  * excluding any fake participants.
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
  * {@code getState} function to be used to retrieve the state
145
  * {@code getState} function to be used to retrieve the state
134
  * features/base/participants.
146
  * features/base/participants.
135
  * @returns {number}
147
  * @returns {number}
136
  */
148
  */
137
 export function getParticipantCount(stateful: Object | Function) {
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
  * Returns a count of the known participants in the passed in redux state,
183
  * Returns a count of the known participants in the passed in redux state,
143
  * including fake participants.
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
  * {@code getState} function to be used to retrieve the state
187
  * {@code getState} function to be used to retrieve the state
148
  * features/base/participants.
188
  * features/base/participants.
149
  * @returns {number}
189
  * @returns {number}
150
  */
190
  */
151
 export function getParticipantCountWithFake(stateful: Object | Function) {
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
         : 'Fellow Jitster';
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
  * Returns the presence status of a participant associated with the passed id.
232
  * Returns the presence status of a participant associated with the passed id.
201
  *
233
  *
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
  * {@code getState} function to be used to retrieve the state
268
  * {@code getState} function to be used to retrieve the state
241
  * features/base/participants.
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
  * {@code getState} function to be used to retrieve the state
280
  * {@code getState} function to be used to retrieve the state
254
  * features/base/participants.
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
     return participant?.role === PARTICIPANT_ROLE.MODERATOR;
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
  * Returns true if all of the meeting participants are moderators.
324
  * Returns true if all of the meeting participants are moderators.
294
  *
325
  *
297
  * @returns {boolean}
328
  * @returns {boolean}
298
  */
329
  */
299
 export function isEveryoneModerator(stateful: Object | Function) {
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
  * @returns {boolean}
352
  * @returns {boolean}
322
  */
353
  */
323
 export function isLocalParticipantModerator(stateful: Object | Function) {
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
         return false;
360
         return false;
329
     }
361
     }
330
 
362
 
331
-    return isParticipantModerator(localParticipant);
363
+    return isParticipantModerator(local);
332
 }
364
 }
333
 
365
 
334
 /**
366
 /**
390
     for (let i = 0; i < AVATAR_CHECKER_FUNCTIONS.length; i++) {
422
     for (let i = 0; i < AVATAR_CHECKER_FUNCTIONS.length; i++) {
391
         const url = AVATAR_CHECKER_FUNCTIONS[i](participant, store);
423
         const url = AVATAR_CHECKER_FUNCTIONS[i](participant, store);
392
 
424
 
393
-        if (url) {
425
+        if (url !== null) {
394
             if (AVATAR_CHECKED_URLS.has(url)) {
426
             if (AVATAR_CHECKED_URLS.has(url)) {
395
                 if (AVATAR_CHECKED_URLS.get(url)) {
427
                 if (AVATAR_CHECKED_URLS.get(url)) {
396
                     return url;
428
                     return url;

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

1
 // @flow
1
 // @flow
2
 
2
 
3
+import { batch } from 'react-redux';
4
+
3
 import UIEvents from '../../../../service/UI/UIEvents';
5
 import UIEvents from '../../../../service/UI/UIEvents';
4
 import { toggleE2EE } from '../../e2ee/actions';
6
 import { toggleE2EE } from '../../e2ee/actions';
5
 import { NOTIFICATION_TIMEOUT, showNotification } from '../../notifications';
7
 import { NOTIFICATION_TIMEOUT, showNotification } from '../../notifications';
43
     getLocalParticipant,
45
     getLocalParticipant,
44
     getParticipantById,
46
     getParticipantById,
45
     getParticipantCount,
47
     getParticipantCount,
46
-    getParticipantDisplayName
48
+    getParticipantDisplayName,
49
+    getRemoteParticipants
47
 } from './functions';
50
 } from './functions';
48
 import { PARTICIPANT_JOINED_FILE, PARTICIPANT_LEFT_FILE } from './sounds';
51
 import { PARTICIPANT_JOINED_FILE, PARTICIPANT_LEFT_FILE } from './sounds';
49
 
52
 
182
 StateListenerRegistry.register(
185
 StateListenerRegistry.register(
183
     /* selector */ state => getCurrentConference(state),
186
     /* selector */ state => getCurrentConference(state),
184
     /* listener */ (conference, { dispatch, getState }) => {
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
     SET_LOADABLE_AVATAR_URL
12
     SET_LOADABLE_AVATAR_URL
13
 } from './actionTypes';
13
 } from './actionTypes';
14
 import { LOCAL_PARTICIPANT_DEFAULT_ID, PARTICIPANT_ROLE } from './constants';
14
 import { LOCAL_PARTICIPANT_DEFAULT_ID, PARTICIPANT_ROLE } from './constants';
15
+import { isParticipantModerator } from './functions';
15
 
16
 
16
 /**
17
 /**
17
  * Participant object.
18
  * Participant object.
51
     'pinned'
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
  * Listen for actions which add, remove, or update the set of participants in
66
  * Listen for actions which add, remove, or update the set of participants in
56
  * the conference.
67
  * the conference.
62
  * added/removed/modified.
73
  * added/removed/modified.
63
  * @returns {Participant[]}
74
  * @returns {Participant[]}
64
  */
75
  */
65
-ReducerRegistry.register('features/base/participants', (state = [], action) => {
76
+ReducerRegistry.register('features/base/participants', (state = DEFAULT_STATE, action) => {
66
     switch (action.type) {
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
     case SET_LOADABLE_AVATAR_URL:
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
     case PARTICIPANT_LEFT: {
227
     case PARTICIPANT_LEFT: {
78
         // XXX A remote participant is uniquely identified by their id in a
228
         // XXX A remote participant is uniquely identified by their id in a
79
         // specific JitsiConference instance. The local participant is uniquely
229
         // specific JitsiConference instance. The local participant is uniquely
81
         // (and the fact that the local participant "joins" at the beginning of
231
         // (and the fact that the local participant "joins" at the beginning of
82
         // the app and "leaves" at the end of the app).
232
         // the app and "leaves" at the end of the app).
83
         const { conference, id } = action.participant;
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
     return state;
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
  * Reducer function for a single participant.
340
  * Reducer function for a single participant.
103
  *
341
  *
112
  */
350
  */
113
 function _participant(state: Object = {}, action) {
351
 function _participant(state: Object = {}, action) {
114
     switch (action.type) {
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
     case SET_LOADABLE_AVATAR_URL:
353
     case SET_LOADABLE_AVATAR_URL:
137
     case PARTICIPANT_UPDATED: {
354
     case PARTICIPANT_UPDATED: {
138
         const { participant } = action; // eslint-disable-line no-shadow
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
     return state;
371
     return state;

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

21
 export const getTrackState = state => state['features/base/tracks'];
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
  * @param {Object} participant - Participant reference.
26
  * @param {Object} participant - Participant reference.
28
  * @param {MEDIA_TYPE} mediaType - Media type.
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
  * @param {Object} participant - Participant reference.
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
  * @param {Object} participant - Participant reference.
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
  * Creates a local video track for presenter. The constraints are computed based
70
  * Creates a local video track for presenter. The constraints are computed based

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

108
         _isModal: window.innerWidth <= SMALL_WIDTH_THRESHOLD,
108
         _isModal: window.innerWidth <= SMALL_WIDTH_THRESHOLD,
109
         _isOpen: isOpen,
109
         _isOpen: isOpen,
110
         _messages: messages,
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
 
288
 
289
     return {
289
     return {
290
         _configuredDisplayName: participant && participant.name,
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
  * }
6
  * }
7
  */
7
  */
8
 export const TOGGLE_E2EE = 'TOGGLE_E2EE';
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
 // @flow
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
  * Dispatches an action to enable / disable E2EE.
6
  * Dispatches an action to enable / disable E2EE.
14
         enabled
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
  * @returns {Props}
22
  * @returns {Props}
23
  */
23
  */
24
 export function _mapStateToProps(state: Object) {
24
 export function _mapStateToProps(state: Object) {
25
-    const participants = state['features/base/participants'];
26
-
27
     return {
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
 
5
 
6
 import { createE2EEEvent, sendAnalytics } from '../../analytics';
6
 import { createE2EEEvent, sendAnalytics } from '../../analytics';
7
 import { translate } from '../../base/i18n';
7
 import { translate } from '../../base/i18n';
8
-import { getParticipants } from '../../base/participants';
9
 import { Switch } from '../../base/react';
8
 import { Switch } from '../../base/react';
10
 import { connect } from '../../base/redux';
9
 import { connect } from '../../base/redux';
11
 import { toggleE2EE } from '../actions';
10
 import { toggleE2EE } from '../actions';
21
     /**
20
     /**
22
      * Indicates whether all participants in the conference currently support E2EE.
21
      * Indicates whether all participants in the conference currently support E2EE.
23
      */
22
      */
24
-    _everyoneSupportsE2EE: boolean,
23
+    _everyoneSupportE2EE: boolean,
25
 
24
 
26
     /**
25
     /**
27
      * The redux {@code dispatch} function.
26
      * The redux {@code dispatch} function.
96
      * @returns {ReactElement}
95
      * @returns {ReactElement}
97
      */
96
      */
98
     render() {
97
     render() {
99
-        const { _everyoneSupportsE2EE, t } = this.props;
98
+        const { _everyoneSupportE2EE, t } = this.props;
100
         const { enabled, expand } = this.state;
99
         const { enabled, expand } = this.state;
101
         const description = t('dialog.e2eeDescription');
100
         const description = t('dialog.e2eeDescription');
102
 
101
 
120
                     </span> }
119
                     </span> }
121
                 </p>
120
                 </p>
122
                 {
121
                 {
123
-                    !_everyoneSupportsE2EE
122
+                    !_everyoneSupportE2EE
124
                         && <span className = 'warning'>
123
                         && <span className = 'warning'>
125
                             { t('dialog.e2eeWarning') }
124
                             { t('dialog.e2eeWarning') }
126
                         </span>
125
                         </span>
195
  * @returns {Props}
194
  * @returns {Props}
196
  */
195
  */
197
 function mapStateToProps(state) {
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
     return {
199
     return {
202
         _enabled: enabled,
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
 // @flow
1
 // @flow
2
 
2
 
3
+import { batch } from 'react-redux';
4
+
3
 import { APP_WILL_MOUNT, APP_WILL_UNMOUNT } from '../base/app';
5
 import { APP_WILL_MOUNT, APP_WILL_UNMOUNT } from '../base/app';
4
 import { getCurrentConference } from '../base/conference';
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
 import { MiddlewareRegistry, StateListenerRegistry } from '../base/redux';
17
 import { MiddlewareRegistry, StateListenerRegistry } from '../base/redux';
7
 import { playSound, registerSound, unregisterSound } from '../base/sounds';
18
 import { playSound, registerSound, unregisterSound } from '../base/sounds';
8
 
19
 
9
 import { TOGGLE_E2EE } from './actionTypes';
20
 import { TOGGLE_E2EE } from './actionTypes';
10
-import { toggleE2EE } from './actions';
21
+import { setEveryoneEnabledE2EE, setEveryoneSupportE2EE, toggleE2EE } from './actions';
11
 import { E2EE_OFF_SOUND_ID, E2EE_ON_SOUND_ID } from './constants';
22
 import { E2EE_OFF_SOUND_ID, E2EE_ON_SOUND_ID } from './constants';
12
 import logger from './logger';
23
 import logger from './logger';
13
 import { E2EE_OFF_SOUND_FILE, E2EE_ON_SOUND_FILE } from './sounds';
24
 import { E2EE_OFF_SOUND_FILE, E2EE_ON_SOUND_FILE } from './sounds';
35
         dispatch(unregisterSound(E2EE_ON_SOUND_ID));
46
         dispatch(unregisterSound(E2EE_ON_SOUND_ID));
36
         break;
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
     case TOGGLE_E2EE: {
171
     case TOGGLE_E2EE: {
39
         const conference = getCurrentConference(getState);
172
         const conference = getCurrentConference(getState);
40
 
173
 

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

2
 
2
 
3
 import { ReducerRegistry } from '../base/redux';
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
 const DEFAULT_STATE = {
11
 const DEFAULT_STATE = {
8
     enabled: false
12
     enabled: false
18
             ...state,
22
             ...state,
19
             enabled: action.enabled
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
     default:
36
     default:
23
         return state;
37
         return state;

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

1
 // @flow
1
 // @flow
2
 import type { Dispatch } from 'redux';
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
 import {
6
 import {
7
     SET_HORIZONTAL_VIEW_DIMENSIONS,
7
     SET_HORIZONTAL_VIEW_DIMENSIONS,
127
  */
127
  */
128
 export function clickOnVideo(n: number) {
128
 export function clickOnVideo(n: number) {
129
     return (dispatch: Function, getState: Function) => {
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
         const nThParticipant = participants[n];
132
         const nThParticipant = participants[n];
132
         const { id, pinned } = nThParticipant;
133
         const { id, pinned } = nThParticipant;
133
 
134
 

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

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

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

1
 // @flow
1
 // @flow
2
 
2
 
3
-import React, { Component } from 'react';
3
+import React from 'react';
4
 import { View } from 'react-native';
4
 import { View } from 'react-native';
5
 
5
 
6
-import { getLocalParticipant } from '../../../base/participants';
7
-import { connect } from '../../../base/redux';
8
-
9
 import Thumbnail from './Thumbnail';
6
 import Thumbnail from './Thumbnail';
10
 import styles from './styles';
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
  * Component to render a local thumbnail that can be separated from the
10
  * Component to render a local thumbnail that can be separated from the
22
  * remote thumbnails later.
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
 // @flow
1
 // @flow
2
 
2
 
3
-import React from 'react';
3
+import React, { useCallback } from 'react';
4
 import { View } from 'react-native';
4
 import { View } from 'react-native';
5
 import type { Dispatch } from 'redux';
5
 import type { Dispatch } from 'redux';
6
 
6
 
12
     ParticipantView,
12
     ParticipantView,
13
     getParticipantCount,
13
     getParticipantCount,
14
     isEveryoneModerator,
14
     isEveryoneModerator,
15
-    pinParticipant
15
+    pinParticipant,
16
+    getParticipantByIdOrUndefined
16
 } from '../../../base/participants';
17
 } from '../../../base/participants';
17
 import { Container } from '../../../base/react';
18
 import { Container } from '../../../base/react';
18
 import { connect } from '../../../base/redux';
19
 import { connect } from '../../../base/redux';
48
     _largeVideo: Object,
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
      * Whether to show the dominant speaker indicator or not.
57
      * Whether to show the dominant speaker indicator or not.
90
     dispatch: Dispatch<any>,
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
      * Whether to display or hide the display name of the participant in the thumbnail.
94
      * Whether to display or hide the display name of the participant in the thumbnail.
120
     const {
116
     const {
121
         _audioMuted: audioMuted,
117
         _audioMuted: audioMuted,
122
         _largeVideo: largeVideo,
118
         _largeVideo: largeVideo,
123
-        _onClick,
124
-        _onThumbnailLongPress,
125
         _renderDominantSpeakerIndicator: renderDominantSpeakerIndicator,
119
         _renderDominantSpeakerIndicator: renderDominantSpeakerIndicator,
126
         _renderModeratorIndicator: renderModeratorIndicator,
120
         _renderModeratorIndicator: renderModeratorIndicator,
121
+        _participant: participant,
127
         _styles,
122
         _styles,
128
         _videoTrack: videoTrack,
123
         _videoTrack: videoTrack,
124
+        dispatch,
129
         disableTint,
125
         disableTint,
130
-        participant,
131
         renderDisplayName,
126
         renderDisplayName,
132
         tileView
127
         tileView
133
     } = props;
128
     } = props;
137
         = participantId === largeVideo.participantId;
132
         = participantId === largeVideo.participantId;
138
     const videoMuted = !videoTrack || videoTrack.muted;
133
     const videoMuted = !videoTrack || videoTrack.muted;
139
     const isScreenShare = videoTrack && videoTrack.videoType === VIDEO_TYPE.DESKTOP;
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
     return (
154
     return (
142
         <Container
155
         <Container
143
-            onClick = { _onClick }
144
-            onLongPress = { _onThumbnailLongPress }
156
+            onClick = { onClick }
157
+            onLongPress = { onThumbnailLongPress }
145
             style = { [
158
             style = { [
146
                 styles.thumbnail,
159
                 styles.thumbnail,
147
                 participant.pinned && !tileView
160
                 participant.pinned && !tileView
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
  * Function that maps parts of Redux state tree into component props.
215
  * Function that maps parts of Redux state tree into component props.
252
  *
216
  *
260
     // the stage i.e. as a large video.
224
     // the stage i.e. as a large video.
261
     const largeVideo = state['features/large-video'];
225
     const largeVideo = state['features/large-video'];
262
     const tracks = state['features/base/tracks'];
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
     const audioTrack
230
     const audioTrack
266
         = getTrackByMediaTypeAndParticipant(tracks, MEDIA_TYPE.AUDIO, id);
231
         = getTrackByMediaTypeAndParticipant(tracks, MEDIA_TYPE.AUDIO, id);
267
     const videoTrack
232
     const videoTrack
268
         = getTrackByMediaTypeAndParticipant(tracks, MEDIA_TYPE.VIDEO, id);
233
         = getTrackByMediaTypeAndParticipant(tracks, MEDIA_TYPE.VIDEO, id);
269
     const participantCount = getParticipantCount(state);
234
     const participantCount = getParticipantCount(state);
270
-    const renderDominantSpeakerIndicator = participant.dominantSpeaker && participantCount > 2;
235
+    const renderDominantSpeakerIndicator = participant && participant.dominantSpeaker && participantCount > 2;
271
     const _isEveryoneModerator = isEveryoneModerator(state);
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
     return {
240
     return {
275
         _audioMuted: audioTrack?.muted ?? true,
241
         _audioMuted: audioTrack?.muted ?? true,
276
         _largeVideo: largeVideo,
242
         _largeVideo: largeVideo,
243
+        _participant: participant,
277
         _renderDominantSpeakerIndicator: renderDominantSpeakerIndicator,
244
         _renderDominantSpeakerIndicator: renderDominantSpeakerIndicator,
278
         _renderModeratorIndicator: renderModeratorIndicator,
245
         _renderModeratorIndicator: renderModeratorIndicator,
279
         _styles: ColorSchemeRegistry.get(state, 'Thumbnail'),
246
         _styles: ColorSchemeRegistry.get(state, 'Thumbnail'),
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
 } from 'react-native';
8
 } from 'react-native';
9
 import type { Dispatch } from 'redux';
9
 import type { Dispatch } from 'redux';
10
 
10
 
11
+import { getLocalParticipant, getParticipantCountWithFake } from '../../../base/participants';
11
 import { connect } from '../../../base/redux';
12
 import { connect } from '../../../base/redux';
12
 import { ASPECT_RATIO_NARROW } from '../../../base/responsive-ui/constants';
13
 import { ASPECT_RATIO_NARROW } from '../../../base/responsive-ui/constants';
13
 import { setTileViewDimensions } from '../../actions.native';
14
 import { setTileViewDimensions } from '../../actions.native';
15
 import Thumbnail from './Thumbnail';
16
 import Thumbnail from './Thumbnail';
16
 import styles from './styles';
17
 import styles from './styles';
17
 
18
 
19
+
18
 /**
20
 /**
19
  * The type of the React {@link Component} props of {@link TileView}.
21
  * The type of the React {@link Component} props of {@link TileView}.
20
  */
22
  */
31
     _height: number,
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
      * Application's viewport height.
51
      * Application's viewport height.
131
      * @private
143
      * @private
132
      */
144
      */
133
     _getColumnCount() {
145
     _getColumnCount() {
134
-        const participantCount = this.props._participants.length;
146
+        const participantCount = this.props._participantCount;
135
 
147
 
136
         // For narrow view, tiles should stack on top of each other for a lonely
148
         // For narrow view, tiles should stack on top of each other for a lonely
137
         // call and a 1:1 call. Otherwise tiles should be grouped into rows of
149
         // call and a 1:1 call. Otherwise tiles should be grouped into rows of
155
      * @returns {Participant[]}
167
      * @returns {Participant[]}
156
      */
168
      */
157
     _getSortedParticipants() {
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
         return participants;
175
         return participants;
172
     }
176
     }
178
      * @returns {Object}
182
      * @returns {Object}
179
      */
183
      */
180
     _getTileDimensions() {
184
     _getTileDimensions() {
181
-        const { _height, _participants, _width } = this.props;
185
+        const { _height, _participantCount, _width } = this.props;
182
         const columns = this._getColumnCount();
186
         const columns = this._getColumnCount();
183
-        const participantCount = _participants.length;
184
         const heightToUse = _height - (MARGIN * 2);
187
         const heightToUse = _height - (MARGIN * 2);
185
         const widthToUse = _width - (MARGIN * 2);
188
         const widthToUse = _width - (MARGIN * 2);
186
         let tileWidth;
189
         let tileWidth;
187
 
190
 
188
         // If there is going to be at least two rows, ensure that at least two
191
         // If there is going to be at least two rows, ensure that at least two
189
         // rows display fully on screen.
192
         // rows display fully on screen.
190
-        if (participantCount / columns > 1) {
193
+        if (_participantCount / columns > 1) {
191
             tileWidth = Math.min(widthToUse / columns, heightToUse / 2);
194
             tileWidth = Math.min(widthToUse / columns, heightToUse / 2);
192
         } else {
195
         } else {
193
             tileWidth = Math.min(widthToUse / columns, heightToUse);
196
             tileWidth = Math.min(widthToUse / columns, heightToUse);
247
         };
250
         };
248
 
251
 
249
         return this._getSortedParticipants()
252
         return this._getSortedParticipants()
250
-            .map(participant => (
253
+            .map(id => (
251
                 <Thumbnail
254
                 <Thumbnail
252
                     disableTint = { true }
255
                     disableTint = { true }
253
-                    key = { participant.id }
254
-                    participant = { participant }
256
+                    key = { id }
257
+                    participantID = { id }
255
                     renderDisplayName = { true }
258
                     renderDisplayName = { true }
256
                     styleOverrides = { styleOverrides }
259
                     styleOverrides = { styleOverrides }
257
                     tileView = { true } />));
260
                     tileView = { true } />));
285
  */
288
  */
286
 function _mapStateToProps(state) {
289
 function _mapStateToProps(state) {
287
     const responsiveUi = state['features/base/responsive-ui'];
290
     const responsiveUi = state['features/base/responsive-ui'];
291
+    const { remoteParticipants } = state['features/filmstrip'];
288
 
292
 
289
     return {
293
     return {
290
         _aspectRatio: responsiveUi.aspectRatio,
294
         _aspectRatio: responsiveUi.aspectRatio,
291
         _height: responsiveUi.clientHeight,
295
         _height: responsiveUi.clientHeight,
292
-        _participants: state['features/base/participants'],
296
+        _localParticipant: getLocalParticipant(state),
297
+        _participantCount: getParticipantCountWithFake(state),
298
+        _remoteParticipants: remoteParticipants,
293
         _width: responsiveUi.clientWidth
299
         _width: responsiveUi.clientWidth
294
     };
300
     };
295
 }
301
 }

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

3
 import React, { Component } from 'react';
3
 import React, { Component } from 'react';
4
 
4
 
5
 import { MEDIA_TYPE } from '../../../base/media';
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
 import { connect } from '../../../base/redux';
7
 import { connect } from '../../../base/redux';
8
 import { getTrackByMediaTypeAndParticipant, isLocalTrackMuted, isRemoteTrackMuted } from '../../../base/tracks';
8
 import { getTrackByMediaTypeAndParticipant, isLocalTrackMuted, isRemoteTrackMuted } from '../../../base/tracks';
9
 import { getCurrentLayout, LAYOUTS } from '../../../video-layout';
9
 import { getCurrentLayout, LAYOUTS } from '../../../video-layout';
111
     const { participantID } = ownProps;
111
     const { participantID } = ownProps;
112
 
112
 
113
     // Only the local participant won't have id for the time when the conference is not yet joined.
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
     const tracks = state['features/base/tracks'];
116
     const tracks = state['features/base/tracks'];
117
     let isVideoMuted = true;
117
     let isVideoMuted = true;

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

9
 import JitsiMeetJS from '../../../base/lib-jitsi-meet/_';
9
 import JitsiMeetJS from '../../../base/lib-jitsi-meet/_';
10
 import { MEDIA_TYPE, VideoTrack } from '../../../base/media';
10
 import { MEDIA_TYPE, VideoTrack } from '../../../base/media';
11
 import {
11
 import {
12
-    getLocalParticipant,
13
-    getParticipantById,
12
+    getParticipantByIdOrUndefined,
14
     getParticipantCount,
13
     getParticipantCount,
15
     pinParticipant
14
     pinParticipant
16
 } from '../../../base/participants';
15
 } from '../../../base/participants';
1012
 function _mapStateToProps(state, ownProps): Object {
1011
 function _mapStateToProps(state, ownProps): Object {
1013
     const { participantID } = ownProps;
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
     const isLocal = participant?.local ?? true;
1016
     const isLocal = participant?.local ?? true;
1019
     const tracks = state['features/base/tracks'];
1017
     const tracks = state['features/base/tracks'];
1020
     const { participantsVolume } = state['features/filmstrip'];
1018
     const { participantsVolume } = state['features/filmstrip'];
1085
         _isDominantSpeakerDisabled: interfaceConfig.DISABLE_DOMINANT_SPEAKER_INDICATOR,
1083
         _isDominantSpeakerDisabled: interfaceConfig.DISABLE_DOMINANT_SPEAKER_INDICATOR,
1086
         _isScreenSharing: _videoTrack?.videoType === 'desktop',
1084
         _isScreenSharing: _videoTrack?.videoType === 'desktop',
1087
         _isTestModeEnabled: isTestModeEnabled(state),
1085
         _isTestModeEnabled: isTestModeEnabled(state),
1088
-        _isVideoPlayable: isVideoPlayable(state, id),
1086
+        _isVideoPlayable: id && isVideoPlayable(state, id),
1089
         _indicatorIconSize: NORMAL,
1087
         _indicatorIconSize: NORMAL,
1090
         _localFlipX: Boolean(localFlipX),
1088
         _localFlipX: Boolean(localFlipX),
1091
         _participant: participant,
1089
         _participant: participant,
1092
         _participantCountMoreThan2: getParticipantCount(state) > 2,
1090
         _participantCountMoreThan2: getParticipantCount(state) > 2,
1093
         _startSilent: Boolean(startSilent),
1091
         _startSilent: Boolean(startSilent),
1094
         _videoTrack,
1092
         _videoTrack,
1095
-        _volume: isLocal ? undefined : participantsVolume[id],
1093
+        _volume: isLocal ? undefined : id ? participantsVolume[id] : undefined,
1096
         ...size
1094
         ...size
1097
     };
1095
     };
1098
 }
1096
 }

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

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

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

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

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

5
 import { Avatar } from '../../../base/avatar';
5
 import { Avatar } from '../../../base/avatar';
6
 import { MEDIA_TYPE } from '../../../base/media';
6
 import { MEDIA_TYPE } from '../../../base/media';
7
 import {
7
 import {
8
-    getParticipants,
9
     getParticipantDisplayName,
8
     getParticipantDisplayName,
10
-    getParticipantPresenceStatus
9
+    getParticipantPresenceStatus,
10
+    getRemoteParticipants
11
 } from '../../../base/participants';
11
 } from '../../../base/participants';
12
 import { Container, Text } from '../../../base/react';
12
 import { Container, Text } from '../../../base/react';
13
 import { connect } from '../../../base/redux';
13
 import { connect } from '../../../base/redux';
135
 function _mapStateToProps(state) {
135
 function _mapStateToProps(state) {
136
     const _isVideoMuted
136
     const _isVideoMuted
137
         = isLocalTrackMuted(state['features/base/tracks'], MEDIA_TYPE.VIDEO);
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
     return {
154
     return {

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

6
 } from '../base/conference';
6
 } from '../base/conference';
7
 import {
7
 import {
8
     getLocalParticipant,
8
     getLocalParticipant,
9
+    getParticipantCount,
9
     getParticipantPresenceStatus,
10
     getParticipantPresenceStatus,
10
-    getParticipants,
11
+    getRemoteParticipants,
11
     PARTICIPANT_JOINED,
12
     PARTICIPANT_JOINED,
12
     PARTICIPANT_JOINED_SOUND_ID,
13
     PARTICIPANT_JOINED_SOUND_ID,
13
     PARTICIPANT_LEFT,
14
     PARTICIPANT_LEFT,
167
     if (!state['features/invite'].calleeInfoVisible) {
168
     if (!state['features/invite'].calleeInfoVisible) {
168
         return;
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
     if ((numberOfPoltergeists > 1 || numberOfRealParticipants > 1)
182
     if ((numberOfPoltergeists > 1 || numberOfRealParticipants > 1)
176
-        || (action.type === PARTICIPANT_LEFT && participants.length === 1)) {
183
+        || (action.type === PARTICIPANT_LEFT && participantCount === 1)) {
177
         store.dispatch(setCalleeInfoVisible(false));
184
         store.dispatch(setCalleeInfoVisible(false));
178
     }
185
     }
179
 }
186
 }

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

3
 import type { Dispatch } from 'redux';
3
 import type { Dispatch } from 'redux';
4
 
4
 
5
 import { MEDIA_TYPE } from '../base/media';
5
 import { MEDIA_TYPE } from '../base/media';
6
+import {
7
+    getDominantSpeakerParticipant,
8
+    getLocalParticipant,
9
+    getPinnedParticipant,
10
+    getRemoteParticipants
11
+} from '../base/participants';
6
 
12
 
7
 import {
13
 import {
8
     SELECT_LARGE_VIDEO_PARTICIPANT,
14
     SELECT_LARGE_VIDEO_PARTICIPANT,
92
 function _electParticipantInLargeVideo(state) {
98
 function _electParticipantInLargeVideo(state) {
93
     // 1. If a participant is pinned, they will be shown in the LargeVideo
99
     // 1. If a participant is pinned, they will be shown in the LargeVideo
94
     // (regardless of whether they are local or remote).
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
     if (participant) {
103
     if (participant) {
99
         return participant.id;
104
         return participant.id;
107
     }
112
     }
108
 
113
 
109
     // 3. Next, pick the dominant speaker (other than self).
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
         return participant.id;
117
         return participant.id;
113
     }
118
     }
114
 
119
 
120
+    // In case this is the local participant.
121
+    participant = undefined;
122
+
115
     // 4. Next, pick the most recent participant with video.
123
     // 4. Next, pick the most recent participant with video.
116
     const tracks = state['features/base/tracks'];
124
     const tracks = state['features/base/tracks'];
117
     const videoTrack = _electLastVisibleRemoteVideo(tracks);
125
     const videoTrack = _electLastVisibleRemoteVideo(tracks);
122
 
130
 
123
     // 5. As a last resort, select the participant that joined last (other than poltergist or other bot type
131
     // 5. As a last resort, select the participant that joined last (other than poltergist or other bot type
124
     // participants).
132
     // participants).
133
+
134
+    const participants = [ ...getRemoteParticipants(state).values() ];
135
+
125
     for (let i = participants.length; i > 0 && !participant; i--) {
136
     for (let i = participants.length; i > 0 && !participant; i--) {
126
         const p = participants[i - 1];
137
         const p = participants[i - 1];
127
 
138
 
131
         return participant.id;
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
 import { JitsiConferenceEvents } from '../../base/lib-jitsi-meet';
28
 import { JitsiConferenceEvents } from '../../base/lib-jitsi-meet';
29
 import { MEDIA_TYPE } from '../../base/media';
29
 import { MEDIA_TYPE } from '../../base/media';
30
 import { SET_AUDIO_MUTED, SET_VIDEO_MUTED } from '../../base/media/actionTypes';
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
 import { MiddlewareRegistry, StateListenerRegistry } from '../../base/redux';
38
 import { MiddlewareRegistry, StateListenerRegistry } from '../../base/redux';
33
 import { toggleScreensharing } from '../../base/tracks';
39
 import { toggleScreensharing } from '../../base/tracks';
34
 import { OPEN_CHAT, CLOSE_CHAT } from '../../chat';
40
 import { OPEN_CHAT, CLOSE_CHAT } from '../../chat';
268
 
274
 
269
     }, 100));
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
  * Registers for events sent from the native side via NativeEventEmitter.
296
  * Registers for events sent from the native side via NativeEventEmitter.
273
  *
297
  *
309
 
333
 
310
     eventEmitter.addListener(ExternalAPI.RETRIEVE_PARTICIPANTS_INFO, ({ requestId }) => {
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
         sendEvent(
347
         sendEvent(

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

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

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

6
 import { useDispatch, useSelector } from 'react-redux';
6
 import { useDispatch, useSelector } from 'react-redux';
7
 
7
 
8
 import { requestDisableModeration, requestEnableModeration } from '../../av-moderation/actions';
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
 import { openDialog } from '../../base/dialog';
13
 import { openDialog } from '../../base/dialog';
11
 import { Icon, IconCheck, IconVideoOff } from '../../base/icons';
14
 import { Icon, IconCheck, IconVideoOff } from '../../base/icons';
12
 import { MEDIA_TYPE } from '../../base/media';
15
 import { MEDIA_TYPE } from '../../base/media';
13
-import { getLocalParticipant } from '../../base/participants';
16
+import {
17
+    getLocalParticipant,
18
+    isEveryoneModerator
19
+} from '../../base/participants';
14
 import { MuteEveryonesVideoDialog } from '../../video-menu/components';
20
 import { MuteEveryonesVideoDialog } from '../../video-menu/components';
15
 
21
 
16
 import {
22
 import {
49
 
55
 
50
 export const FooterContextMenu = ({ onMouseLeave }: Props) => {
56
 export const FooterContextMenu = ({ onMouseLeave }: Props) => {
51
     const dispatch = useDispatch();
57
     const dispatch = useDispatch();
58
+    const isModerationSupported = useSelector(isAvModerationSupported());
59
+    const allModerators = useSelector(isEveryoneModerator);
52
     const isModerationEnabled = useSelector(isAvModerationEnabled(MEDIA_TYPE.AUDIO));
60
     const isModerationEnabled = useSelector(isAvModerationEnabled(MEDIA_TYPE.AUDIO));
53
     const { id } = useSelector(getLocalParticipant);
61
     const { id } = useSelector(getLocalParticipant);
54
     const { t } = useTranslation();
62
     const { t } = useTranslation();
75
                 <span>{ t('participantsPane.actions.stopEveryonesVideo') }</span>
83
                 <span>{ t('participantsPane.actions.stopEveryonesVideo') }</span>
76
             </ContextMenuItem>
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
         </ContextMenu>
112
         </ContextMenu>
100
     );
113
     );
101
 };
114
 };

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

7
 import { approveKnockingParticipant, rejectKnockingParticipant } from '../../lobby/actions';
7
 import { approveKnockingParticipant, rejectKnockingParticipant } from '../../lobby/actions';
8
 import { ACTION_TRIGGER, MEDIA_STATE } from '../constants';
8
 import { ACTION_TRIGGER, MEDIA_STATE } from '../constants';
9
 
9
 
10
-import { ParticipantItem } from './ParticipantItem';
10
+import ParticipantItem from './ParticipantItem';
11
 import { ParticipantActionButton } from './styled';
11
 import { ParticipantActionButton } from './styled';
12
 
12
 
13
 type Props = {
13
 type Props = {
28
         <ParticipantItem
28
         <ParticipantItem
29
             actionsTrigger = { ACTION_TRIGGER.PERMANENT }
29
             actionsTrigger = { ACTION_TRIGGER.PERMANENT }
30
             audioMediaState = { MEDIA_STATE.NONE }
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
             <ParticipantActionButton
37
             <ParticipantActionButton
35
                 onClick = { reject }>
38
                 onClick = { reject }>
36
                 {t('lobby.reject')}
39
                 {t('lobby.reject')}

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

1
 // @flow
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
 import { isToolbarButtonEnabled } from '../../base/config/functions.web';
5
 import { isToolbarButtonEnabled } from '../../base/config/functions.web';
8
 import { openDialog } from '../../base/dialog';
6
 import { openDialog } from '../../base/dialog';
7
+import { translate } from '../../base/i18n';
9
 import {
8
 import {
10
     IconCloseCircle,
9
     IconCloseCircle,
11
     IconCrown,
10
     IconCrown,
14
     IconMuteEveryoneElse,
13
     IconMuteEveryoneElse,
15
     IconVideoOff
14
     IconVideoOff
16
 } from '../../base/icons';
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
 import { openChat } from '../../chat/actions';
23
 import { openChat } from '../../chat/actions';
20
 import { GrantModeratorDialog, KickRemoteParticipantDialog, MuteEveryoneDialog } from '../../video-menu';
24
 import { GrantModeratorDialog, KickRemoteParticipantDialog, MuteEveryoneDialog } from '../../video-menu';
21
 import MuteRemoteParticipantsVideoDialog from '../../video-menu/components/web/MuteRemoteParticipantsVideoDialog';
25
 import MuteRemoteParticipantsVideoDialog from '../../video-menu/components/web/MuteRemoteParticipantsVideoDialog';
31
 
35
 
32
 type Props = {
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
      * Callback used to open a confirmation dialog for audio muting.
74
      * Callback used to open a confirmation dialog for audio muting.
36
      */
75
      */
57
     onSelect: Function,
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
             && offsetTarget?.offsetParent
234
             && offsetTarget?.offsetParent
86
             && offsetTarget.offsetParent instanceof HTMLElement
235
             && offsetTarget.offsetParent instanceof HTMLElement
87
         ) {
236
         ) {
88
-            const { current: container } = containerRef;
237
+            const { current: container } = this._containerRef;
89
             const { offsetTop, offsetParent: { offsetHeight, scrollTop } } = offsetTarget;
238
             const { offsetTop, offsetParent: { offsetHeight, scrollTop } } = offsetTarget;
90
             const outerHeight = getComputedOuterHeight(container);
239
             const outerHeight = getComputedOuterHeight(container);
91
 
240
 
93
                 ? offsetTop - outerHeight
242
                 ? offsetTop - outerHeight
94
                 : offsetTop;
243
                 : offsetTop;
95
 
244
 
96
-            setIsHidden(false);
245
+            this.setState({ isHidden: false });
97
         } else {
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
 // @flow
1
 // @flow
2
 
2
 
3
 import React from 'react';
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
 import ParticipantQuickAction from './ParticipantQuickAction';
12
 import ParticipantQuickAction from './ParticipantQuickAction';
13
 import { ParticipantActionEllipsis } from './styled';
13
 import { ParticipantActionEllipsis } from './styled';
14
 
14
 
15
 type Props = {
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
      * Is this item highlighted
61
      * Is this item highlighted
19
      */
62
      */
24
      */
67
      */
25
     muteAudio: Function,
68
     muteAudio: Function,
26
 
69
 
70
+    /**
71
+     * The translated text for the mute participant button.
72
+     */
73
+    muteParticipantButtonText: string,
74
+
27
     /**
75
     /**
28
      * Callback for the activation of this item's context menu
76
      * Callback for the activation of this item's context menu
29
      */
77
      */
35
     onLeave: Function,
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
     isHighlighted,
116
     isHighlighted,
45
     onContextMenu,
117
     onContextMenu,
46
     onLeave,
118
     onLeave,
47
     muteAudio,
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
     return (
124
     return (
56
         <ParticipantItem
125
         <ParticipantItem
57
             actionsTrigger = { ACTION_TRIGGER.HOVER }
126
             actionsTrigger = { ACTION_TRIGGER.HOVER }
58
-            audioMediaState = { audioMediaState }
127
+            audioMediaState = { _audioMediaState }
128
+            displayName = { _displayName }
59
             isHighlighted = { isHighlighted }
129
             isHighlighted = { isHighlighted }
130
+            local = { _local }
60
             onLeave = { onLeave }
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
             <ParticipantQuickAction
136
             <ParticipantQuickAction
64
-                isAudioMuted = { isAudioMuted }
137
+                askUnmuteText = { askUnmuteText }
138
+                buttonType = { _quickActionButtonType }
65
                 muteAudio = { muteAudio }
139
                 muteAudio = { muteAudio }
66
-                participant = { participant } />
140
+                muteParticipantButtonText = { muteParticipantButtonText }
141
+                participantID = { _participantID } />
67
             <ParticipantActionEllipsis
142
             <ParticipantActionEllipsis
68
-                aria-label = { t('MeetingParticipantItem.ParticipantActionEllipsis.options') }
143
+                aria-label = { participantActionEllipsisLabel }
69
                 onClick = { onContextMenu } />
144
                 onClick = { onContextMenu } />
70
         </ParticipantItem>
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
 // @flow
1
 // @flow
2
 
2
 
3
-import _ from 'lodash';
4
 import React, { useCallback, useRef, useState } from 'react';
3
 import React, { useCallback, useRef, useState } from 'react';
5
 import { useTranslation } from 'react-i18next';
4
 import { useTranslation } from 'react-i18next';
6
 import { useSelector, useDispatch } from 'react-redux';
5
 import { useSelector, useDispatch } from 'react-redux';
7
 
6
 
8
 import { openDialog } from '../../base/dialog';
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
 import MuteRemoteParticipantDialog from '../../video-menu/components/web/MuteRemoteParticipantDialog';
13
 import MuteRemoteParticipantDialog from '../../video-menu/components/web/MuteRemoteParticipantDialog';
11
 import { findStyledAncestor, shouldRenderInviteButton } from '../functions';
14
 import { findStyledAncestor, shouldRenderInviteButton } from '../functions';
12
 
15
 
13
 import { InviteButton } from './InviteButton';
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
 import { Heading, ParticipantContainer } from './styled';
19
 import { Heading, ParticipantContainer } from './styled';
17
 
20
 
18
 type NullProto = {
21
 type NullProto = {
20
   __proto__: null
23
   __proto__: null
21
 };
24
 };
22
 
25
 
23
-type RaiseContext = NullProto | {
26
+type RaiseContext = NullProto | {|
24
 
27
 
25
   /**
28
   /**
26
    * Target elements against which positioning calculations are made
29
    * Target elements against which positioning calculations are made
28
   offsetTarget?: HTMLElement,
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
 const initialState = Object.freeze(Object.create(null));
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
     const dispatch = useDispatch();
47
     const dispatch = useDispatch();
40
     const isMouseOverMenu = useRef(false);
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
     const showInviteButton = useSelector(shouldRenderInviteButton);
56
     const showInviteButton = useSelector(shouldRenderInviteButton);
43
     const [ raiseContext, setRaiseContext ] = useState<RaiseContext>(initialState);
57
     const [ raiseContext, setRaiseContext ] = useState<RaiseContext>(initialState);
44
     const { t } = useTranslation();
58
     const { t } = useTranslation();
61
         });
75
         });
62
     }, [ raiseContext ]);
76
     }, [ raiseContext ]);
63
 
77
 
64
-    const raiseMenu = useCallback((participant, target) => {
78
+    const raiseMenu = useCallback((participantID, target) => {
65
         setRaiseContext({
79
         setRaiseContext({
66
-            participant,
80
+            participantID,
67
             offsetTarget: findStyledAncestor(target, ParticipantContainer)
81
             offsetTarget: findStyledAncestor(target, ParticipantContainer)
68
         });
82
         });
69
     }, [ raiseContext ]);
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
             lowerMenu();
89
             lowerMenu();
76
         } else {
90
         } else {
77
-            raiseMenu(participant, e.target);
91
+            raiseMenu(participantID, e.target);
78
         }
92
         }
79
     }, [ raiseContext ]);
93
     }, [ raiseContext ]);
80
 
94
 
91
         dispatch(openDialog(MuteRemoteParticipantDialog, { participantID: id }));
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
     return (
140
     return (
95
     <>
141
     <>
96
-        <Heading>{t('participantsPane.headings.participantsList', { count: participants.length })}</Heading>
142
+        <Heading>{t('participantsPane.headings.participantsList', { count: participantsCount })}</Heading>
97
         {showInviteButton && <InviteButton />}
143
         {showInviteButton && <InviteButton />}
98
         <div>
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
         </div>
146
         </div>
109
         <MeetingParticipantContextMenu
147
         <MeetingParticipantContextMenu
110
             muteAudio = { muteAudio }
148
             muteAudio = { muteAudio }
114
             { ...raiseContext } />
152
             { ...raiseContext } />
115
     </>
153
     </>
116
     );
154
     );
117
-};
155
+}

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

1
 // @flow
1
 // @flow
2
 
2
 
3
 import React, { type Node } from 'react';
3
 import React, { type Node } from 'react';
4
-import { useTranslation } from 'react-i18next';
5
-import { useSelector } from 'react-redux';
6
 
4
 
7
 import { Avatar } from '../../base/avatar';
5
 import { Avatar } from '../../base/avatar';
8
 import {
6
 import {
12
     IconMicrophoneEmpty,
10
     IconMicrophoneEmpty,
13
     IconMicrophoneEmptySlash
11
     IconMicrophoneEmptySlash
14
 } from '../../base/icons';
12
 } from '../../base/icons';
15
-import { getParticipantDisplayNameWithId } from '../../base/participants';
16
 import { ACTION_TRIGGER, MEDIA_STATE, type ActionTrigger, type MediaState } from '../constants';
13
 import { ACTION_TRIGGER, MEDIA_STATE, type ActionTrigger, type MediaState } from '../constants';
17
 
14
 
18
 import { RaisedHandIndicator } from './RaisedHandIndicator';
15
 import { RaisedHandIndicator } from './RaisedHandIndicator';
100
      */
97
      */
101
     children: Node,
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
      * Is this item highlighted/raised
106
      * Is this item highlighted/raised
105
      */
107
      */
106
     isHighlighted?: boolean,
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
      * Callback for when the mouse leaves this component
116
      * Callback for when the mouse leaves this component
116
     onLeave?: Function,
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
      * Media state for video
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
     children,
148
     children,
131
     isHighlighted,
149
     isHighlighted,
132
     onLeave,
150
     onLeave,
133
     actionsTrigger = ACTION_TRIGGER.HOVER,
151
     actionsTrigger = ACTION_TRIGGER.HOVER,
134
     audioMediaState = MEDIA_STATE.NONE,
152
     audioMediaState = MEDIA_STATE.NONE,
135
     videoMuteState = MEDIA_STATE.NONE,
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
     const ParticipantActions = Actions[actionsTrigger];
160
     const ParticipantActions = Actions[actionsTrigger];
140
-    const { t } = useTranslation();
141
-    const displayName = name || useSelector(getParticipantDisplayNameWithId(p.id));
142
 
161
 
143
     return (
162
     return (
144
         <ParticipantContainer
163
         <ParticipantContainer
147
             trigger = { actionsTrigger }>
166
             trigger = { actionsTrigger }>
148
             <Avatar
167
             <Avatar
149
                 className = 'participant-avatar'
168
                 className = 'participant-avatar'
150
-                participantId = { p.id }
169
+                participantId = { participantID }
151
                 size = { 32 } />
170
                 size = { 32 } />
152
             <ParticipantContent>
171
             <ParticipantContent>
153
                 <ParticipantNameContainer>
172
                 <ParticipantNameContainer>
154
                     <ParticipantName>
173
                     <ParticipantName>
155
                         { displayName }
174
                         { displayName }
156
                     </ParticipantName>
175
                     </ParticipantName>
157
-                    { p.local ? <span>&nbsp;({t('chat.you')})</span> : null }
176
+                    { local ? <span>&nbsp;({ youText })</span> : null }
158
                 </ParticipantNameContainer>
177
                 </ParticipantNameContainer>
159
-                { !p.local && <ParticipantActions children = { children } /> }
178
+                { !local && <ParticipantActions children = { children } /> }
160
                 <ParticipantStates>
179
                 <ParticipantStates>
161
-                    {p.raisedHand && <RaisedHandIndicator />}
162
-                    {VideoStateIcons[videoMuteState]}
163
-                    {AudioStateIcons[audioMediaState]}
180
+                    { raisedHand && <RaisedHandIndicator /> }
181
+                    { VideoStateIcons[videoMuteState] }
182
+                    { AudioStateIcons[audioMediaState] }
164
                 </ParticipantStates>
183
                 </ParticipantStates>
165
             </ParticipantContent>
184
             </ParticipantContent>
166
         </ParticipantContainer>
185
         </ParticipantContainer>
167
     );
186
     );
168
-};
187
+}

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

1
 // @flow
1
 // @flow
2
 
2
 
3
 import React from 'react';
3
 import React from 'react';
4
-import { useTranslation } from 'react-i18next';
5
-import { useSelector } from 'react-redux';
6
 
4
 
7
 import { QUICK_ACTION_BUTTON } from '../constants';
5
 import { QUICK_ACTION_BUTTON } from '../constants';
8
-import { getQuickActionButtonType } from '../functions';
9
 
6
 
10
 import AskToUnmuteButton from './AskToUnmuteButton';
7
 import AskToUnmuteButton from './AskToUnmuteButton';
11
 import { QuickActionButton } from './styled';
8
 import { QuickActionButton } from './styled';
13
 type Props = {
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
      * Callback used to open a confirmation dialog for audio muting.
23
      * Callback used to open a confirmation dialog for audio muting.
22
      */
24
      */
23
     muteAudio: Function,
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
  * @param {Props} props - The props of the component.
38
  * @param {Props} props - The props of the component.
35
  * @returns {React$Element<'button'>}
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
     switch (buttonType) {
48
     switch (buttonType) {
43
     case QUICK_ACTION_BUTTON.MUTE: {
49
     case QUICK_ACTION_BUTTON.MUTE: {
44
         return (
50
         return (
45
             <QuickActionButton
51
             <QuickActionButton
46
-                onClick = { muteAudio(id) }
52
+                onClick = { muteAudio(participantID) }
47
                 primary = { true }>
53
                 primary = { true }>
48
-                {t('dialog.muteParticipantButton')}
54
+                { muteParticipantButtonText }
49
             </QuickActionButton>
55
             </QuickActionButton>
50
         );
56
         );
51
     }
57
     }
52
     case QUICK_ACTION_BUTTON.ASK_TO_UNMUTE: {
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
     default: {
65
     default: {
56
         return null;
66
         return null;

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

1
 // @flow
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
 import { ThemeProvider } from 'styled-components';
4
 import { ThemeProvider } from 'styled-components';
7
 
5
 
8
 import { openDialog } from '../../base/dialog';
6
 import { openDialog } from '../../base/dialog';
7
+import { translate } from '../../base/i18n';
9
 import {
8
 import {
10
     getParticipantCount,
9
     getParticipantCount,
11
-    isEveryoneModerator,
12
     isLocalParticipantModerator
10
     isLocalParticipantModerator
13
 } from '../../base/participants';
11
 } from '../../base/participants';
12
+import { connect } from '../../base/redux';
14
 import { MuteEveryoneDialog } from '../../video-menu/components/';
13
 import { MuteEveryoneDialog } from '../../video-menu/components/';
15
 import { close } from '../actions';
14
 import { close } from '../actions';
16
 import { classList, findStyledAncestor, getParticipantsPaneOpen } from '../functions';
15
 import { classList, findStyledAncestor, getParticipantsPaneOpen } from '../functions';
30
     Header
29
     Header
31
 } from './styled';
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
                 </div>
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
 export * from './InviteButton';
1
 export * from './InviteButton';
2
 export * from './LobbyParticipantItem';
2
 export * from './LobbyParticipantItem';
3
 export * from './LobbyParticipantList';
3
 export * from './LobbyParticipantList';
4
-export * from './MeetingParticipantContextMenu';
5
-export * from './MeetingParticipantItem';
6
 export * from './MeetingParticipantList';
4
 export * from './MeetingParticipantList';
7
-export * from './ParticipantItem';
8
-export * from './ParticipantsPane';
5
+export { default as ParticipantsPane } from './ParticipantsPane';
9
 export * from './ParticipantsPaneButton';
6
 export * from './ParticipantsPaneButton';
10
 export * from './RaisedHandIndicator';
7
 export * from './RaisedHandIndicator';

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

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
  * @param {MediaType} mediaType - The media type.
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
     if (getParticipantCount(state) > 2 && isEnabledFromState(mediaType, state)) {
52
     if (getParticipantCount(state) > 2 && isEnabledFromState(mediaType, state)) {
52
         if (participant.local) {
53
         if (participant.local) {
53
             return !isLocalParticipantApprovedFromState(mediaType, state);
54
             return !isLocalParticipantApprovedFromState(mediaType, state);
62
     }
63
     }
63
 
64
 
64
     return false;
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
  * @param {Object} participant - The participant.
71
  * @param {Object} participant - The participant.
71
  * @param {boolean} muted - The mute state of the participant.
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
     if (muted) {
77
     if (muted) {
76
-        if (isForceMuted(participant, MEDIA_TYPE.AUDIO)(state)) {
78
+        if (isForceMuted(participant, MEDIA_TYPE.AUDIO, state)) {
77
             return MEDIA_STATE.FORCE_MUTED;
79
             return MEDIA_STATE.FORCE_MUTED;
78
         }
80
         }
79
 
81
 
81
     }
83
     }
82
 
84
 
83
     return MEDIA_STATE.UNMUTED;
85
     return MEDIA_STATE.UNMUTED;
84
-};
86
+}
85
 
87
 
86
 
88
 
87
 /**
89
 /**
125
 export const getParticipantsPaneOpen = (state: Object) => Boolean(getState(state)?.isOpen);
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
  * The button is displayed when hovering a participant from the participant list.
131
  * The button is displayed when hovering a participant from the participant list.
130
  *
132
  *
131
  * @param {Object} participant - The participant.
133
  * @param {Object} participant - The participant.
132
  * @param {boolean} isAudioMuted - If audio is muted for the participant.
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
     // handled only by moderators
139
     // handled only by moderators
137
     if (isLocalParticipantModerator(state)) {
140
     if (isLocalParticipantModerator(state)) {
138
-        if (isForceMuted(participant, MEDIA_TYPE.AUDIO)(state)) {
141
+        if (isForceMuted(participant, MEDIA_TYPE.AUDIO, state)) {
139
             return QUICK_ACTION_BUTTON.ASK_TO_UNMUTE;
142
             return QUICK_ACTION_BUTTON.ASK_TO_UNMUTE;
140
         }
143
         }
141
         if (!isAudioMuted) {
144
         if (!isAudioMuted) {
144
     }
147
     }
145
 
148
 
146
     return QUICK_ACTION_BUTTON.NONE;
149
     return QUICK_ACTION_BUTTON.NONE;
147
-};
150
+}
148
 
151
 
149
 /**
152
 /**
150
  * Returns true if the invite button should be rendered.
153
  * Returns true if the invite button should be rendered.

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

144
         clientHeight,
144
         clientHeight,
145
         clientWidth,
145
         clientWidth,
146
         filmstripVisible: visible,
146
         filmstripVisible: visible,
147
-        isOwner: ownerId === localParticipant.id,
147
+        isOwner: ownerId === localParticipant?.id,
148
         videoUrl
148
         videoUrl
149
     };
149
     };
150
 }
150
 }

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

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

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

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

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

5
 import {
5
 import {
6
     getPinnedParticipant,
6
     getPinnedParticipant,
7
     getParticipantCount,
7
     getParticipantCount,
8
-    pinParticipant
8
+    pinParticipant,
9
+    getParticipantCountWithFake
9
 } from '../base/participants';
10
 } from '../base/participants';
10
 import {
11
 import {
11
     ASPECT_RATIO_BREAKPOINT,
12
     ASPECT_RATIO_BREAKPOINT,
101
     // When in tile view mode, we must discount ourselves (the local participant) because our
102
     // When in tile view mode, we must discount ourselves (the local participant) because our
102
     // tile is not visible.
103
     // tile is not visible.
103
     const { iAmRecorder } = state['features/base/config'];
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
     const columnsToMaintainASquare = Math.ceil(Math.sqrt(numberOfParticipants));
107
     const columnsToMaintainASquare = Math.ceil(Math.sqrt(numberOfParticipants));
107
     const columns = Math.min(columnsToMaintainASquare, maxColumns);
108
     const columns = Math.min(columnsToMaintainASquare, maxColumns);

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

20
 } from '../base/media';
20
 } from '../base/media';
21
 import {
21
 import {
22
     getLocalParticipant,
22
     getLocalParticipant,
23
+    getRemoteParticipants,
23
     muteRemoteParticipant
24
     muteRemoteParticipant
24
 } from '../base/participants';
25
 } from '../base/participants';
25
 
26
 
91
     return (dispatch: Dispatch<any>, getState: Function) => {
92
     return (dispatch: Dispatch<any>, getState: Function) => {
92
         const state = getState();
93
         const state = getState();
93
         const localId = getLocalParticipant(state).id;
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
 }

正在加载...
取消
保存