瀏覽代碼

feat(participants-pane) implement participants pane

master
Gabriel Imre 4 年之前
父節點
當前提交
d014a52ab3
沒有連結到貢獻者的電子郵件帳戶。
共有 49 個檔案被更改,包括 1547 行新增79 行删除
  1. 51
    0
      css/_participants-pane.scss
  2. 1
    0
      css/_variables.scss
  3. 8
    0
      css/_videolayout_default.scss
  4. 2
    0
      css/components/_input-slider.scss
  5. 1
    5
      css/filmstrip/_tile_view.scss
  6. 1
    0
      css/main.scss
  7. 14
    0
      lang/main.json
  8. 9
    1
      modules/UI/videolayout/LargeVideoManager.js
  9. 1
    1
      package.json
  10. 1
    0
      react/features/app/reducers.web.js
  11. 22
    6
      react/features/base/conference/functions.js
  12. 1
    1
      react/features/base/config/constants.js
  13. 3
    0
      react/features/base/icons/svg/close-circle.svg
  14. 7
    0
      react/features/base/icons/svg/index.js
  15. 3
    1
      react/features/base/icons/svg/message.svg
  16. 3
    0
      react/features/base/icons/svg/mic-blocked.svg
  17. 3
    0
      react/features/base/icons/svg/mic-disabled-hollow.svg
  18. 3
    0
      react/features/base/icons/svg/microphone-hollow.svg
  19. 1
    1
      react/features/base/icons/svg/mute-everyone-else.svg
  20. 3
    0
      react/features/base/icons/svg/participants.svg
  21. 3
    0
      react/features/base/icons/svg/raised-hand-hollow.svg
  22. 3
    0
      react/features/base/icons/svg/video-off.svg
  23. 12
    3
      react/features/base/responsive-ui/actions.js
  24. 75
    3
      react/features/base/tracks/functions.js
  25. 31
    18
      react/features/conference/components/web/Conference.js
  26. 14
    0
      react/features/filmstrip/subscriber.web.js
  27. 2
    1
      react/features/lobby/components/AbstractKnockingParticipantList.js
  28. 11
    0
      react/features/lobby/functions.js
  29. 9
    0
      react/features/participants-pane/actionTypes.js
  30. 26
    0
      react/features/participants-pane/actions.js
  31. 32
    0
      react/features/participants-pane/components/InviteButton.js
  32. 44
    0
      react/features/participants-pane/components/LobbyParticipantItem.js
  33. 35
    0
      react/features/participants-pane/components/LobbyParticipantList.js
  34. 166
    0
      react/features/participants-pane/components/MeetingParticipantContextMenu.js
  35. 55
    0
      react/features/participants-pane/components/MeetingParticipantItem.js
  36. 108
    0
      react/features/participants-pane/components/MeetingParticipantList.js
  37. 154
    0
      react/features/participants-pane/components/ParticipantItem.js
  38. 62
    0
      react/features/participants-pane/components/ParticipantsPane.js
  39. 15
    0
      react/features/participants-pane/components/RaisedHandIndicator.js
  40. 9
    0
      react/features/participants-pane/components/index.js
  41. 335
    0
      react/features/participants-pane/components/styled.js
  42. 22
    0
      react/features/participants-pane/constants.js
  43. 66
    0
      react/features/participants-pane/functions.js
  44. 35
    0
      react/features/participants-pane/reducer.js
  45. 10
    0
      react/features/participants-pane/theme.json
  46. 2
    2
      react/features/settings/components/web/audio/AudioSettingsContent.js
  47. 67
    30
      react/features/toolbox/components/web/Toolbox.js
  48. 5
    5
      react/features/toolbox/functions.web.js
  49. 1
    1
      react/features/video-menu/components/AbstractMuteEveryoneDialog.js

+ 51
- 0
css/_participants-pane.scss 查看文件

@@ -0,0 +1,51 @@
1
+.participants_pane {
2
+    background-color: $participantsPaneBgColor;
3
+    flex-shrink: 0;
4
+    overflow: hidden;
5
+    position: relative;
6
+    transition: width .16s ease-in-out;
7
+    width: 315px;
8
+    z-index: $zindex0;
9
+
10
+    &--closed {
11
+        width: 0;
12
+    }
13
+}
14
+
15
+.participants_pane-content {
16
+    display: flex;
17
+    flex-direction: column;
18
+    font-weight: 600;
19
+    height: 100%;
20
+    width: 315px;
21
+
22
+    & > *:first-child,
23
+    & > *:last-child {
24
+        flex-shrink: 0;
25
+    }
26
+}
27
+
28
+.participant-avatar {
29
+    margin: 8px 16px 8px 0;
30
+}
31
+
32
+@media (max-width: 375px) {
33
+    .participants_pane {
34
+        height: 100vh;
35
+        height: -webkit-fill-available;
36
+        left: 0;
37
+        position: fixed;
38
+        right: 0;
39
+        top: 0;
40
+        width: auto;
41
+
42
+        &--closed {
43
+            display: none;
44
+            width: auto;
45
+        }
46
+    }
47
+
48
+    .participants_pane-content {
49
+        width: 100%;
50
+    }
51
+}

+ 1
- 0
css/_variables.scss 查看文件

@@ -30,6 +30,7 @@ $defaultSideBarFontColor: #44A5FF;
30 30
 $defaultSemiDarkColor: #ACACAC;
31 31
 $defaultDarkColor: #2b3d5c;
32 32
 $defaultWarningColor: rgb(215, 121, 118);
33
+$participantsPaneBgColor: #141414;
33 34
 $presence-available: rgb(110, 176, 5);
34 35
 $presence-away: rgb(250, 201, 20);
35 36
 $presence-busy: rgb(233, 0, 27);

+ 8
- 0
css/_videolayout_default.scss 查看文件

@@ -1,5 +1,13 @@
1 1
 #videoconference_page {
2 2
     min-height: 100%;
3
+    position: relative;
4
+    transform: translate3d(0, 0, 0);
5
+    width: 100%;
6
+}
7
+
8
+#layout_wrapper {
9
+    display: flex;
10
+    height: 100%;
3 11
 }
4 12
 
5 13
 #videospace {

+ 2
- 0
css/components/_input-slider.scss 查看文件

@@ -1,3 +1,5 @@
1
+$rangeInputThumbSize: 14;
2
+
1 3
 /**
2 4
  * Disable the default webkit styles for range inputs (sliders).
3 5
  */

+ 1
- 5
css/filmstrip/_tile_view.scss 查看文件

@@ -16,7 +16,7 @@
16 16
         display: flex;
17 17
         flex-direction: column;
18 18
         height: 100%;
19
-        width: 100vw;
19
+        width: 100%;
20 20
     }
21 21
 
22 22
     .filmstrip__videos .videocontainer {
@@ -50,10 +50,6 @@
50 50
             &.shift-right {
51 51
                 margin-left: $sidebarWidth;
52 52
                 width: calc(100% - #{$sidebarWidth});
53
-
54
-                #filmstripRemoteVideos {
55
-                    width: calc(100vw - #{$sidebarWidth});
56
-                }
57 53
             }
58 54
         }
59 55
     }

+ 1
- 0
css/main.scss 查看文件

@@ -104,5 +104,6 @@ $flagsImagePath: "../images/";
104 104
 @import 'responsive';
105 105
 @import 'connection-status';
106 106
 @import 'drawer';
107
+@import 'participants-pane';
107 108
 
108 109
 /* Modules END */

+ 14
- 0
lang/main.json 查看文件

@@ -418,6 +418,7 @@
418 418
         "showSpeakerStats": "Show speaker stats",
419 419
         "toggleChat": "Open or close the chat",
420 420
         "toggleFilmstrip": "Show or hide video thumbnails",
421
+        "toggleParticipantsPane": "Show or hide the participants pane",
421 422
         "toggleScreensharing": "Switch between camera and screen sharing",
422 423
         "toggleShortcuts": "Show or hide keyboard shortcuts",
423 424
         "videoMute": "Start or stop your camera"
@@ -527,6 +528,16 @@
527 528
         "oldElectronClientDescription2": "latest build",
528 529
         "oldElectronClientDescription3": " now!"
529 530
     },
531
+    "participantsPane": {
532
+        "headings": {
533
+            "lobby": "Lobby ({{count}})",
534
+            "participantsList": "Meeting participants ({{count}})"
535
+        },
536
+        "actions": {
537
+            "muteAll": "Mute all",
538
+            "stopVideo": "Stop video"
539
+        }
540
+    },
530 541
     "passwordSetRemotely": "Set by another participant",
531 542
     "passwordDigitsOnly": "Up to {{number}} digits",
532 543
     "poweredby": "powered by",
@@ -745,6 +756,7 @@
745 756
             "muteEveryoneElse": "Mute everyone else",
746 757
             "muteEveryonesVideo": "Disable everyone's camera",
747 758
             "muteEveryoneElsesVideo": "Disable everyone else's camera",
759
+            "participants": "Participants",
748 760
             "pip": "Toggle Picture-in-Picture mode",
749 761
             "privateMessage": "Send private message",
750 762
             "profile": "Edit your profile",
@@ -807,6 +819,7 @@
807 819
         "noisyAudioInputTitle": "Your microphone appears to be noisy!",
808 820
         "noisyAudioInputDesc": "It sounds like your microphone is making noise, please consider muting or changing the device.",
809 821
         "openChat": "Open chat",
822
+        "participants": "Participants",
810 823
         "pip": "Enter Picture-in-Picture mode",
811 824
         "privateMessage": "Send private message",
812 825
         "profile": "Edit your profile",
@@ -942,6 +955,7 @@
942 955
         "header": "Help center"
943 956
     },
944 957
     "lobby": {
958
+        "admit": "Admit",
945 959
         "knockingParticipantList": "Knocking participant list",
946 960
         "allow": "Allow",
947 961
         "backToKnockModeButton": "No password, ask to join instead",

+ 9
- 1
modules/UI/videolayout/LargeVideoManager.js 查看文件

@@ -19,6 +19,8 @@ import { CHAT_SIZE } from '../../../react/features/chat';
19 19
 import {
20 20
     updateKnownLargeVideoResolution
21 21
 } from '../../../react/features/large-video/actions';
22
+import { getParticipantsPaneOpen } from '../../../react/features/participants-pane/functions';
23
+import theme from '../../../react/features/participants-pane/theme.json';
22 24
 import { PresenceLabel } from '../../../react/features/presence-status';
23 25
 import { shouldDisplayTileView } from '../../../react/features/video-layout';
24 26
 /* eslint-enable no-unused-vars */
@@ -366,7 +368,13 @@ export default class LargeVideoManager {
366 368
         }
367 369
 
368 370
         let widthToUse = this.preferredWidth || window.innerWidth;
369
-        const { isOpen } = APP.store.getState()['features/chat'];
371
+        const state = APP.store.getState();
372
+        const { isOpen } = state['features/chat'];
373
+        const isParticipantsPaneOpen = getParticipantsPaneOpen(state);
374
+
375
+        if (isParticipantsPaneOpen) {
376
+            widthToUse -= theme.participantsPaneWidth;
377
+        }
370 378
 
371 379
         if (isOpen && window.innerWidth > 580) {
372 380
             /**

+ 1
- 1
package.json 查看文件

@@ -48,10 +48,10 @@
48 48
     "i18next": "17.0.6",
49 49
     "i18next-browser-languagedetector": "3.0.1",
50 50
     "i18next-xhr-backend": "3.0.0",
51
-    "jQuery-Impromptu": "github:trentrichardson/jQuery-Impromptu#v6.0.0",
52 51
     "jitsi-meet-logger": "github:jitsi/jitsi-meet-logger#v1.0.0",
53 52
     "jquery": "3.5.1",
54 53
     "jquery-i18next": "1.2.1",
54
+    "jQuery-Impromptu": "github:trentrichardson/jQuery-Impromptu#v6.0.0",
55 55
     "js-md5": "0.6.1",
56 56
     "jwt-decode": "2.2.0",
57 57
     "lib-jitsi-meet": "github:jitsi/lib-jitsi-meet#0dc1540a44131d4c287993d2cfe0ed8e5ae70d4c",

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

@@ -6,6 +6,7 @@ import '../feedback/reducer';
6 6
 import '../local-recording/reducer';
7 7
 import '../no-audio-signal/reducer';
8 8
 import '../noise-detection/reducer';
9
+import '../participants-pane/reducer';
9 10
 import '../power-monitor/reducer';
10 11
 import '../prejoin/reducer';
11 12
 import '../remote-control/reducer';

+ 22
- 6
react/features/base/conference/functions.js 查看文件

@@ -20,6 +20,22 @@ import {
20 20
 } from './constants';
21 21
 import logger from './logger';
22 22
 
23
+/**
24
+ * Returns root conference state.
25
+ *
26
+ * @param {Object} state - Global state.
27
+ * @returns {Object} Conference state.
28
+ */
29
+export const getConferenceState = (state: Object) => state['features/base/conference'];
30
+
31
+/**
32
+ * Is the conference joined or not.
33
+ *
34
+ * @param {Object} state - Global state.
35
+ * @returns {boolean}
36
+ */
37
+export const getIsConferenceJoined = (state: Object) => Boolean(getConferenceState(state).conference);
38
+
23 39
 /**
24 40
  * Attach a set of local tracks to a conference.
25 41
  *
@@ -123,7 +139,7 @@ export function commonUserLeftHandling(
123 139
 export function forEachConference(
124 140
         stateful: Function | Object,
125 141
         predicate: (Object, URL) => boolean) {
126
-    const state = toState(stateful)['features/base/conference'];
142
+    const state = getConferenceState(toState(stateful));
127 143
 
128 144
     for (const v of Object.values(state)) {
129 145
         // Does the value of the base/conference's property look like a
@@ -157,7 +173,7 @@ export function getConferenceName(stateful: Function | Object): string {
157 173
     const state = toState(stateful);
158 174
     const { callee } = state['features/base/jwt'];
159 175
     const { callDisplayName } = state['features/base/config'];
160
-    const { pendingSubjectChange, room, subject } = state['features/base/conference'];
176
+    const { pendingSubjectChange, room, subject } = getConferenceState(state);
161 177
 
162 178
     return pendingSubjectChange
163 179
         || subject
@@ -174,7 +190,7 @@ export function getConferenceName(stateful: Function | Object): string {
174 190
  * @returns {string} - The name of the conference formatted for the title.
175 191
  */
176 192
 export function getConferenceNameForTitle(stateful: Function | Object) {
177
-    return safeStartCase(safeDecodeURIComponent(toState(stateful)['features/base/conference'].room));
193
+    return safeStartCase(safeDecodeURIComponent(getConferenceState(toState(stateful)).room));
178 194
 }
179 195
 
180 196
 /**
@@ -186,7 +202,7 @@ export function getConferenceNameForTitle(stateful: Function | Object) {
186 202
 */
187 203
 export function getConferenceTimestamp(stateful: Function | Object): number {
188 204
     const state = toState(stateful);
189
-    const { conferenceTimestamp } = state['features/base/conference'];
205
+    const { conferenceTimestamp } = getConferenceState(state);
190 206
 
191 207
     return conferenceTimestamp;
192 208
 }
@@ -203,7 +219,7 @@ export function getConferenceTimestamp(stateful: Function | Object): number {
203 219
  */
204 220
 export function getCurrentConference(stateful: Function | Object) {
205 221
     const { conference, joining, leaving, membersOnly, passwordRequired }
206
-        = toState(stateful)['features/base/conference'];
222
+        = getConferenceState(toState(stateful));
207 223
 
208 224
     // There is a precedence
209 225
     if (conference) {
@@ -220,7 +236,7 @@ export function getCurrentConference(stateful: Function | Object) {
220 236
  * @returns {string}
221 237
  */
222 238
 export function getRoomName(state: Object): string {
223
-    return state['features/base/conference'].room;
239
+    return getConferenceState(state).room;
224 240
 }
225 241
 
226 242
 /**

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

@@ -17,7 +17,7 @@ export const TOOLBAR_BUTTONS = [
17 17
     'microphone', 'camera', 'closedcaptions', 'desktop', 'embedmeeting', 'fullscreen',
18 18
     'fodeviceselection', 'hangup', 'profile', 'chat', 'recording',
19 19
     'livestreaming', 'etherpad', 'sharedvideo', 'shareaudio', 'settings', 'raisehand',
20
-    'videoquality', 'filmstrip', 'invite', 'feedback', 'stats', 'shortcuts',
20
+    'videoquality', 'filmstrip', 'participants-pane', 'feedback', 'stats', 'shortcuts',
21 21
     'tileview', 'select-background', 'download', 'help', 'mute-everyone', 'mute-video-everyone',
22 22
     'security', 'toggle-camera'
23 23
 ];

+ 3
- 0
react/features/base/icons/svg/close-circle.svg 查看文件

@@ -0,0 +1,3 @@
1
+<svg width="20" height="20" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg">
2
+<path fill-rule="evenodd" clip-rule="evenodd" d="M10.0001 16.6666C13.682 16.6666 16.6667 13.6819 16.6667 9.99996C16.6667 6.31806 13.682 3.33329 10.0001 3.33329C6.31818 3.33329 3.33341 6.31806 3.33341 9.99996C3.33341 13.6819 6.31818 16.6666 10.0001 16.6666ZM10.0001 18.3333C5.39771 18.3333 1.66675 14.6023 1.66675 9.99996C1.66675 5.39759 5.39771 1.66663 10.0001 1.66663C14.6025 1.66663 18.3334 5.39759 18.3334 9.99996C18.3334 14.6023 14.6025 18.3333 10.0001 18.3333ZM10.0001 8.82145L12.3571 6.46443C12.6825 6.13899 13.2102 6.13899 13.5356 6.46443C13.8611 6.78986 13.8611 7.3175 13.5356 7.64294L11.1786 9.99996L13.5356 12.357C13.8611 12.6824 13.8611 13.2101 13.5356 13.5355C13.2102 13.8609 12.6825 13.8609 12.3571 13.5355L10.0001 11.1785L7.64306 13.5355C7.31762 13.8609 6.78998 13.8609 6.46455 13.5355C6.13911 13.2101 6.13911 12.6824 6.46455 12.357L8.82157 9.99996L6.46455 7.64294C6.13911 7.3175 6.13911 6.78986 6.46455 6.46443C6.78998 6.13899 7.31762 6.13899 7.64306 6.46443L10.0001 8.82145Z"/>
3
+</svg>

+ 7
- 0
react/features/base/icons/svg/index.js 查看文件

@@ -27,6 +27,7 @@ export { default as IconChatSend } from './send.svg';
27 27
 export { default as IconChatUnread } from './chat-unread.svg';
28 28
 export { default as IconCheck } from './check.svg';
29 29
 export { default as IconClose } from './close.svg';
30
+export { default as IconCloseCircle } from './close-circle.svg';
30 31
 export { default as IconCloseX } from './close-x.svg';
31 32
 export { default as IconClosedCaption } from './closed_caption.svg';
32 33
 export { default as IconCloseSmall } from './close-small.svg';
@@ -68,10 +69,13 @@ export { default as IconMenuThumb } from './thumb-menu.svg';
68 69
 export { default as IconMenuUp } from './menu-up.svg';
69 70
 export { default as IconMessage } from './message.svg';
70 71
 export { default as IconMeter } from './meter.svg';
72
+export { default as IconMicBlockedHollow } from './mic-blocked.svg';
71 73
 export { default as IconMicDisabled } from './mic-disabled.svg';
74
+export { default as IconMicDisabledHollow } from './mic-disabled-hollow.svg';
72 75
 export { default as IconMicrophone } from './microphone.svg';
73 76
 export { default as IconMicrophoneEmpty } from './microphone-empty.svg';
74 77
 export { default as IconMicrophoneEmptySlash } from './microphone-empty-slash.svg';
78
+export { default as IconMicrophoneHollow } from './microphone-hollow.svg';
75 79
 export { default as IconModerator } from './star.svg';
76 80
 export { default as IconMuteEveryone } from './mute-everyone.svg';
77 81
 export { default as IconMuteEveryoneElse } from './mute-everyone-else.svg';
@@ -80,11 +84,13 @@ export { default as IconMuteVideoEveryoneElse } from './mute-video-everyone-else
80 84
 export { default as IconNotificationJoin } from './navigate_next.svg';
81 85
 export { default as IconOpenInNew } from './open_in_new.svg';
82 86
 export { default as IconOutlook } from './office365.svg';
87
+export { default as IconParticipants } from './participants.svg';
83 88
 export { default as IconPhone } from './phone.svg';
84 89
 export { default as IconPin } from './enlarge.svg';
85 90
 export { default as IconPlane } from './paper-plane.svg';
86 91
 export { default as IconPresentation } from './presentation.svg';
87 92
 export { default as IconRaisedHand } from './raised-hand.svg';
93
+export { default as IconRaisedHandHollow } from './raised-hand-hollow.svg';
88 94
 export { default as IconRec } from './rec.svg';
89 95
 export { default as IconRemoteControlStart } from './play.svg';
90 96
 export { default as IconRemoteControlStop } from './stop.svg';
@@ -109,6 +115,7 @@ export { default as IconSwitchCamera } from './switch-camera.svg';
109 115
 export { default as IconTileView } from './tiles-many.svg';
110 116
 export { default as IconToggleRecording } from './camera-take-picture.svg';
111 117
 export { default as IconTrash } from './trash.svg';
118
+export { default as IconVideoOff } from './video-off.svg';
112 119
 export { default as IconVideoQualityAudioOnly } from './AUD.svg';
113 120
 export { default as IconVideoQualityHD } from './HD.svg';
114 121
 export { default as IconVideoQualityLD } from './LD.svg';

+ 3
- 1
react/features/base/icons/svg/message.svg 查看文件

@@ -1 +1,3 @@
1
-<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><path d="M20 2H4c-1.1 0-1.99.9-1.99 2L2 22l4-4h14c1.1 0 2-.9 2-2V4c0-1.1-.9-2-2-2zm-2 12H6v-2h12v2zm0-3H6V9h12v2zm0-3H6V6h12v2z"/><path d="M0 0h24v24H0z" fill="none"/></svg>
1
+<svg width="20" height="20" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg">
2
+<path fill-rule="evenodd" clip-rule="evenodd" d="M14.1667 18.3598C14.627 18.3598 15.0001 17.9867 15.0001 17.5265V13.3333H17.5001C17.9603 13.3333 18.3334 12.9602 18.3334 12.5V2.49996C18.3334 2.03972 17.9603 1.66663 17.5001 1.66663H2.50008C2.03984 1.66663 1.66675 2.03972 1.66675 2.49996V12.5C1.66675 12.9602 2.03984 13.3333 2.50008 13.3333H9.62979L13.5238 18.0566C13.6821 18.2486 13.9179 18.3598 14.1667 18.3598ZM3.33341 3.33329H16.6667V11.6666H13.3334V15.2057L10.4158 11.6666H3.33341V3.33329Z" />
3
+</svg>

+ 3
- 0
react/features/base/icons/svg/mic-blocked.svg 查看文件

@@ -0,0 +1,3 @@
1
+<svg width="20" height="20" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg">
2
+<path fill-rule="evenodd" clip-rule="evenodd" d="M9.99992 1.66669C11.8409 1.66669 13.3333 3.15907 13.3333 5.00002V10C13.3333 11.5555 12.2678 12.8621 10.8269 13.23C10.8311 13.2638 10.8333 13.2983 10.8333 13.3334V14.9309C11.1452 14.8785 11.4474 14.7973 11.7369 14.69C12.0292 13.2018 13.2017 12.0293 14.6899 11.737C14.8904 11.196 14.9999 10.6108 14.9999 10C14.9999 9.53978 15.373 9.16669 15.8333 9.16669C16.2935 9.16669 16.6666 9.53978 16.6666 10C16.6666 10.6246 16.5807 11.2292 16.4201 11.8025C18.0039 12.2412 19.1666 13.6932 19.1666 15.4167C19.1666 17.4878 17.4877 19.1667 15.4166 19.1667C13.6931 19.1667 12.2411 18.004 11.8024 16.4202C11.4881 16.5082 11.1644 16.5738 10.8333 16.6151V17.5C10.8333 17.9603 10.4602 18.3334 9.99992 18.3334C9.53968 18.3334 9.16659 17.9603 9.16659 17.5V16.6151C5.87799 16.205 3.33325 13.3997 3.33325 10C3.33325 9.53978 3.70635 9.16669 4.16659 9.16669C4.62682 9.16669 4.99992 9.53978 4.99992 10C4.99992 12.4775 6.80182 14.5342 9.16659 14.9309V13.3334C9.16659 13.2983 9.16875 13.2638 9.17294 13.23C7.73203 12.8621 6.66659 11.5555 6.66659 10V5.00002C6.66659 3.15907 8.15897 1.66669 9.99992 1.66669ZM9.99992 3.33335C9.07944 3.33335 8.33325 4.07955 8.33325 5.00002V10C8.33325 10.9205 9.07944 11.6667 9.99992 11.6667C10.9204 11.6667 11.6666 10.9205 11.6666 10V5.00002C11.6666 4.07955 10.9204 3.33335 9.99992 3.33335ZM13.3333 15V15.8334H17.4999V15H13.3333Z" />
3
+</svg>

+ 3
- 0
react/features/base/icons/svg/mic-disabled-hollow.svg 查看文件

@@ -0,0 +1,3 @@
1
+<svg width="18" height="18" viewBox="0 0 18 18" fill="none" xmlns="http://www.w3.org/2000/svg">
2
+<path fill-rule="evenodd" clip-rule="evenodd" d="M6 7.07804V9C6 10.3999 6.9589 11.5759 8.25572 11.907C8.25195 11.9374 8.25 11.9685 8.25 12V13.4378C6.12171 13.0807 4.5 11.2297 4.5 9C4.5 8.58579 4.16421 8.25 3.75 8.25C3.33579 8.25 3 8.58579 3 9C3 12.0597 5.29027 14.5845 8.25 14.9536V15.75C8.25 16.1642 8.58579 16.5 9 16.5C9.41421 16.5 9.75 16.1642 9.75 15.75V14.9536C10.8412 14.8175 11.8415 14.3884 12.6694 13.7475L15.1986 16.2766C15.4964 16.5744 15.9791 16.5745 16.2768 16.2768C16.5745 15.9791 16.5744 15.4964 16.2766 15.1986L13.7475 12.6694C13.7502 12.6659 13.753 12.6623 13.7557 12.6588L12.6831 11.5861C12.6805 11.5898 12.6779 11.5935 12.6753 11.5972L11.5911 10.513C11.5934 10.5091 11.5957 10.5051 11.598 10.5011L10.4566 9.35965C10.4554 9.3647 10.4541 9.36974 10.4528 9.37476L7.5 6.42196V6.40304L6 4.90304V4.92196L2.80143 1.72339C2.50364 1.4256 2.02091 1.42553 1.72322 1.72322C1.42553 2.02091 1.4256 2.50364 1.72339 2.80143L6 7.07804ZM7.5 8.57804V9C7.5 9.82843 8.17157 10.5 9 10.5C9.1294 10.5 9.25498 10.4836 9.37476 10.4528L7.5 8.57804ZM10.513 11.5911C10.2756 11.73 10.0175 11.8372 9.74428 11.907C9.74805 11.9374 9.75 11.9685 9.75 12V13.4378C10.4295 13.3238 11.0573 13.0575 11.5972 12.6753L10.513 11.5911ZM12 8.74696L10.5 7.24696V4.5C10.5 3.67157 9.82843 3 9 3C8.25144 3 7.63095 3.54832 7.51827 4.26522L6.34845 3.09541C6.85223 2.14635 7.85064 1.5 9 1.5C10.6569 1.5 12 2.84315 12 4.5V8.74696ZM13.3623 10.1092L14.5462 11.2932C14.8386 10.5867 15 9.81218 15 9C15 8.58579 14.6642 8.25 14.25 8.25C13.8358 8.25 13.5 8.58579 13.5 9C13.5 9.38278 13.4522 9.7544 13.3623 10.1092Z" />
3
+</svg>

+ 3
- 0
react/features/base/icons/svg/microphone-hollow.svg 查看文件

@@ -0,0 +1,3 @@
1
+<svg width="18" height="18" viewBox="0 0 18 18" fill="none" xmlns="http://www.w3.org/2000/svg">
2
+<path fill-rule="evenodd" clip-rule="evenodd" d="M12 4.5C12 2.84315 10.6569 1.5 9 1.5C7.34315 1.5 6 2.84315 6 4.5V9C6 10.3999 6.9589 11.5759 8.25572 11.907C8.25195 11.9374 8.25 11.9685 8.25 12V13.4378C6.12171 13.0807 4.5 11.2297 4.5 9C4.5 8.58579 4.16421 8.25 3.75 8.25C3.33579 8.25 3 8.58579 3 9C3 12.0597 5.29027 14.5845 8.25 14.9536V15.75C8.25 16.1642 8.58579 16.5 9 16.5C9.41421 16.5 9.75 16.1642 9.75 15.75V14.9536C12.7097 14.5845 15 12.0597 15 9C15 8.58579 14.6642 8.25 14.25 8.25C13.8358 8.25 13.5 8.58579 13.5 9C13.5 11.2297 11.8783 13.0807 9.75 13.4378V12C9.75 11.9685 9.74805 11.9374 9.74428 11.907C11.0411 11.5759 12 10.3999 12 9V4.5ZM9 3C8.17157 3 7.5 3.67157 7.5 4.5V9C7.5 9.82843 8.17157 10.5 9 10.5C9.82843 10.5 10.5 9.82843 10.5 9V4.5C10.5 3.67157 9.82843 3 9 3Z" />
3
+</svg>

+ 1
- 1
react/features/base/icons/svg/mute-everyone-else.svg 查看文件

@@ -1,4 +1,4 @@
1
-<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
1
+<svg width="24" height="24" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
2 2
 <g clip-path="url(#clip0)">
3 3
 <path fill-rule="evenodd" clip-rule="evenodd" d="M6 13.078V15C6 16.3999 6.9589 17.5759 8.25572 17.907C8.25195 17.9374 8.25 17.9685 8.25 18V19.4378C6.12171 19.0807 4.5 17.2297 4.5 15C4.5 14.5858 4.16421 14.25 3.75 14.25C3.33579 14.25 3 14.5858 3 15C3 18.0597 5.29027 20.5845 8.25 20.9536V21.75C8.25 22.1642 8.58579 22.5 9 22.5C9.41421 22.5 9.75 22.1642 9.75 21.75V20.9536C10.8412 20.8175 11.8415 20.3884 12.6694 19.7475L15.1986 22.2766C15.4964 22.5744 15.9791 22.5745 16.2768 22.2768C16.5745 21.9791 16.5744 21.4964 16.2766 21.1986L13.7475 18.6694C13.7502 18.6659 13.753 18.6623 13.7557 18.6588L12.6831 17.5861C12.6805 17.5898 12.6779 17.5935 12.6753 17.5972L11.5911 16.513C11.5934 16.5091 11.5957 16.5051 11.598 16.5011L10.4566 15.3596C10.4554 15.3647 10.4541 15.3697 10.4528 15.3748L7.5 12.422V12.403L6 10.903V10.922L2.80143 7.72339C2.50364 7.4256 2.02091 7.42553 1.72322 7.72322C1.42553 8.02091 1.4256 8.50364 1.72339 8.80143L6 13.078ZM7.5 14.578V15C7.5 15.8284 8.17157 16.5 9 16.5C9.1294 16.5 9.25498 16.4836 9.37476 16.4528L7.5 14.578ZM10.513 17.5911C10.2756 17.73 10.0175 17.8372 9.74428 17.907C9.74805 17.9374 9.75 17.9685 9.75 18V19.4378C10.4295 19.3238 11.0573 19.0575 11.5972 18.6753L10.513 17.5911ZM12 14.747L10.5 13.247V10.5C10.5 9.67157 9.82843 9 9 9C8.25144 9 7.63095 9.54832 7.51827 10.2652L6.34845 9.09541C6.85223 8.14635 7.85064 7.5 9 7.5C10.6569 7.5 12 8.84315 12 10.5V14.747ZM13.3623 16.1092L14.5462 17.2932C14.8386 16.5867 15 15.8122 15 15C15 14.5858 14.6642 14.25 14.25 14.25C13.8358 14.25 13.5 14.5858 13.5 15C13.5 15.3828 13.4522 15.7544 13.3623 16.1092Z" />
4 4
 <path fill-rule="evenodd" clip-rule="evenodd" d="M16 4.71869V6C16 6.93329 16.6393 7.71727 17.5038 7.93797C17.5013 7.95829 17.5 7.97899 17.5 8V8.95852C16.0811 8.72048 15 7.4865 15 6C15 5.72386 14.7761 5.5 14.5 5.5C14.2239 5.5 14 5.72386 14 6C14 8.03981 15.5268 9.723 17.5 9.96905V10.5C17.5 10.7761 17.7239 11 18 11C18.2761 11 18.5 10.7761 18.5 10.5V9.96905C19.2275 9.87834 19.8943 9.59227 20.4463 9.16499L22.1324 10.8511C22.3309 11.0496 22.6527 11.0496 22.8512 10.8512C23.0496 10.6527 23.0496 10.3309 22.8511 10.1324L21.165 8.4463C21.1668 8.44393 21.1687 8.44155 21.1705 8.43918L20.4554 7.7241C20.4537 7.72656 20.4519 7.72903 20.4502 7.73149L19.7274 7.00869C19.7289 7.00603 19.7305 7.00338 19.732 7.00072L18.9711 6.23977C18.9702 6.24313 18.9694 6.24649 18.9685 6.24984L17 4.28131V4.26869L16 3.26869V3.28131L13.8676 1.14893C13.6691 0.950402 13.3473 0.950351 13.1488 1.14881C12.9504 1.34727 12.9504 1.6691 13.1489 1.86762L16 4.71869ZM17 5.71869V6C17 6.55228 17.4477 7 18 7C18.0863 7 18.17 6.98908 18.2498 6.96854L17 5.71869ZM19.0087 7.72738C18.8504 7.81999 18.6783 7.89148 18.4962 7.93797C18.4987 7.95829 18.5 7.97899 18.5 8V8.95852C18.953 8.88252 19.3715 8.70502 19.7315 8.45019L19.0087 7.72738ZM20 5.83131L19 4.83131V3C19 2.44772 18.5523 2 18 2C17.501 2 17.0873 2.36555 17.0122 2.84348L16.2323 2.06361C16.5682 1.4309 17.2338 1 18 1C19.1046 1 20 1.89543 20 3V5.83131ZM20.9082 6.73948L21.6975 7.52877C21.8924 7.05778 22 6.54145 22 6C22 5.72386 21.7761 5.5 21.5 5.5C21.2239 5.5 21 5.72386 21 6C21 6.25519 20.9681 6.50294 20.9082 6.73948Z" />

+ 3
- 0
react/features/base/icons/svg/participants.svg 查看文件

@@ -0,0 +1,3 @@
1
+<svg width="22" height="22" viewBox="0 0 22 22" xmlns="http://www.w3.org/2000/svg">
2
+<path fill-rule="evenodd" clip-rule="evenodd" d="M18.7916 15.5833H15.4218C15.3159 14.9082 15.1566 14.2976 14.9401 13.75H18.3275C18.331 13.6843 18.3333 13.6079 18.3333 13.5208C18.3333 10.1308 17.531 9.16667 14.6666 9.16667C13.9217 9.16667 13.3162 9.23188 12.828 9.38802C12.8315 9.31453 12.8333 9.24072 12.8333 9.16667C12.8333 7.88484 12.3071 6.72592 11.4589 5.89413C11.4931 4.15185 12.9162 2.75 14.6666 2.75C16.4386 2.75 17.875 4.18642 17.875 5.95833C17.875 6.619 17.6753 7.23302 17.333 7.74334C19.4136 8.53185 20.1666 10.4577 20.1666 13.5208C20.1666 14.8958 19.7083 15.5833 18.7916 15.5833ZM16.0416 5.95833C16.0416 6.71772 15.426 7.33333 14.6666 7.33333C13.9073 7.33333 13.2916 6.71772 13.2916 5.95833C13.2916 5.19894 13.9073 4.58333 14.6666 4.58333C15.426 4.58333 16.0416 5.19894 16.0416 5.95833ZM3.43748 20.1667C2.36804 20.1667 1.83331 19.4028 1.83331 17.875C1.83331 14.3822 2.75854 12.2203 5.33347 11.3892C4.86283 10.7726 4.58331 10.0023 4.58331 9.16667C4.58331 7.14162 6.22494 5.5 8.24998 5.5C10.275 5.5 11.9166 7.14162 11.9166 9.16667C11.9166 10.0023 11.6371 10.7726 11.1665 11.3892C13.7414 12.2203 14.6666 14.3822 14.6666 17.875C14.6666 19.4028 14.1319 20.1667 13.0625 20.1667H3.43748ZM10.0833 9.16667C10.0833 10.1792 9.2625 11 8.24998 11C7.23746 11 6.41665 10.1792 6.41665 9.16667C6.41665 8.15414 7.23746 7.33333 8.24998 7.33333C9.2625 7.33333 10.0833 8.15414 10.0833 9.16667ZM12.8333 17.875C12.8333 18.0711 12.8222 18.2237 12.8084 18.3333H3.69156C3.6778 18.2237 3.66665 18.0711 3.66665 17.875C3.66665 14.0191 4.7028 12.8333 8.24998 12.8333C11.7972 12.8333 12.8333 14.0191 12.8333 17.875Z" />
3
+</svg>

+ 3
- 0
react/features/base/icons/svg/raised-hand-hollow.svg 查看文件

@@ -0,0 +1,3 @@
1
+<svg width="18" height="18" viewBox="0 0 18 18" xmlns="http://www.w3.org/2000/svg">
2
+<path fill-rule="evenodd" clip-rule="evenodd" d="M9.74988 2.625V3.95455V9V9.75C9.74988 10.1642 10.0857 10.5 10.4999 10.5C10.9141 10.5 11.2499 10.1642 11.2499 9.75V9V3.95455C11.2499 3.87516 11.3876 3.75 11.6249 3.75C11.8622 3.75 11.9999 3.87516 11.9999 3.95455V5.625V9.75C11.9999 10.1642 12.3357 10.5 12.7499 10.5C13.1641 10.5 13.4999 10.1642 13.4999 9.75V5.625C13.4999 5.41789 13.6678 5.25 13.8749 5.25C14.082 5.25 14.2499 5.41789 14.2499 5.625V11.2811C14.0457 13.3687 12.266 15 10.1249 15C8.71915 15 7.43958 14.2916 6.68469 13.1525L6.682 13.1532L3.82247 8.85337C3.68527 8.65681 3.7265 8.42298 3.89615 8.30418C4.06581 8.18539 4.29963 8.22662 4.41843 8.39627L5.37775 9.83045L5.37678 9.8311C5.60841 10.1745 6.07456 10.2651 6.41796 10.0335C6.62525 9.89367 6.74042 9.66839 6.74821 9.43624L6.74988 9.43673V4.125C6.74988 3.91789 6.91777 3.75 7.12488 3.75C7.33199 3.75 7.49988 3.91789 7.49988 4.125V9V9.75C7.49988 10.1642 7.83567 10.5 8.24988 10.5C8.66409 10.5 8.99988 10.1642 8.99988 9.75V9V4.125V2.625C8.99988 2.41789 9.16777 2.25 9.37488 2.25C9.58199 2.25 9.74988 2.41789 9.74988 2.625ZM15.7366 11.2652L15.7499 11.2586V10.875V5.625C15.7499 4.58947 14.9104 3.75 13.8749 3.75C13.7434 3.75 13.615 3.76354 13.4912 3.78929C13.3998 2.92544 12.5991 2.25 11.6249 2.25C11.4859 2.25 11.3504 2.26375 11.22 2.28984C11.062 1.41423 10.296 0.75 9.37488 0.75C8.4524 0.75 7.68552 1.41617 7.52906 2.29368C7.39889 2.26508 7.26364 2.25 7.12488 2.25C6.08934 2.25 5.24988 3.08947 5.24988 4.125V7.12106C4.61807 6.63808 3.72195 6.595 3.03579 7.07546C2.18753 7.66941 1.98138 8.83856 2.57533 9.68682L5.27627 13.7284C6.25454 15.3871 8.05975 16.5 10.1249 16.5C13.1003 16.5 15.5362 14.1898 15.7366 11.2652Z" />
3
+</svg>

+ 3
- 0
react/features/base/icons/svg/video-off.svg 查看文件

@@ -0,0 +1,3 @@
1
+<svg width="20" height="20" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg">
2
+<path fill-rule="evenodd" clip-rule="evenodd" d="M6.21892 4.99996H6.19791L3.11278 1.91484C2.78191 1.58396 2.24554 1.58388 1.91477 1.91465C1.584 2.24542 1.58409 2.78179 1.91496 3.11266L3.80226 4.99996H3.33341C2.41294 4.99996 1.66675 5.74615 1.66675 6.66663V13.3333C1.66675 14.2538 2.41294 15 3.33341 15H12.5001C12.8673 15 13.2068 14.8812 13.4823 14.68L16.8874 18.0851C17.2183 18.416 17.7546 18.416 18.0854 18.0853C18.4162 17.7545 18.4161 17.2181 18.0852 16.8873L14.1667 12.9688V12.9478L12.5001 11.2811V11.3021L7.86457 6.66663H7.88559L6.21892 4.99996ZM12.5001 8.88547V8.33329V6.66663H10.2812L8.61457 4.99996H12.5001C13.4206 4.99996 14.1667 5.74615 14.1667 6.66663V7.38091L17.0866 5.71241C17.4862 5.48407 17.9953 5.6229 18.2236 6.02249C18.2956 6.14841 18.3334 6.29092 18.3334 6.43594V13.564C18.3334 13.8767 18.1612 14.1492 17.9064 14.2917L14.5104 10.8958L16.6667 12.128V7.87193L14.1667 9.3005V10.5521L12.5001 8.88547ZM3.33341 6.66663H5.46892L12.1356 13.3333H3.33341V6.66663Z" />
3
+</svg>

+ 12
- 3
react/features/base/responsive-ui/actions.js 查看文件

@@ -3,6 +3,8 @@
3 3
 import type { Dispatch } from 'redux';
4 4
 
5 5
 import { CHAT_SIZE } from '../../chat/constants';
6
+import { getParticipantsPaneOpen } from '../../participants-pane/functions';
7
+import theme from '../../participants-pane/theme.json';
6 8
 
7 9
 import { CLIENT_RESIZED, SET_ASPECT_RATIO, SET_REDUCED_UI } from './actionTypes';
8 10
 import { ASPECT_RATIO_NARROW, ASPECT_RATIO_WIDE } from './constants';
@@ -28,11 +30,18 @@ const REDUCED_UI_THRESHOLD = 300;
28 30
 export function clientResized(clientWidth: number, clientHeight: number) {
29 31
     return (dispatch: Dispatch<any>, getState: Function) => {
30 32
         const state = getState();
31
-        const { isOpen } = state['features/chat'];
33
+        const { isOpen: isChatOpen } = state['features/chat'];
34
+        const isParticipantsPaneOpen = getParticipantsPaneOpen(state);
32 35
         let availableWidth = clientWidth;
33 36
 
34
-        if (isOpen && navigator.product !== 'ReactNative') {
35
-            availableWidth -= CHAT_SIZE;
37
+        if (navigator.product !== 'ReactNative') {
38
+            if (isChatOpen) {
39
+                availableWidth -= CHAT_SIZE;
40
+            }
41
+
42
+            if (isParticipantsPaneOpen) {
43
+                availableWidth -= theme.participantsPaneWidth;
44
+            }
36 45
         }
37 46
 
38 47
         return dispatch({

+ 75
- 3
react/features/base/tracks/functions.js 查看文件

@@ -12,6 +12,78 @@ import {
12 12
 import loadEffects from './loadEffects';
13 13
 import logger from './logger';
14 14
 
15
+/**
16
+ * Returns root tracks state.
17
+ *
18
+ * @param {Object} state - Global state.
19
+ * @returns {Object} Tracks state.
20
+ */
21
+export const getTrackState = state => state['features/base/tracks'];
22
+
23
+/**
24
+ * Higher-order function that returns a selector for a specific participant
25
+ * and media type.
26
+ *
27
+ * @param {Object} participant - Participant reference.
28
+ * @param {MEDIA_TYPE} mediaType - Media type.
29
+ * @returns {Function} Selector.
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
+        }
43
+
44
+        const tracks = getTrackState(state);
45
+
46
+        if (participant?.local) {
47
+            return isLocalTrackMuted(tracks, mediaType);
48
+        } else if (!participant?.isFakeParticipant) {
49
+            return isRemoteTrackMuted(tracks, mediaType, participant.id);
50
+        }
51
+
52
+        return true;
53
+    };
54
+
55
+/**
56
+ * Higher-order function that returns a selector for a specific participant.
57
+ *
58
+ * @param {Object} participant - Participant reference.
59
+ * @returns {Function} Selector.
60
+ */
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);
70
+
71
+/**
72
+ * Higher-order function that returns a selector for a specific participant.
73
+ *
74
+ * @param {Object} participant - Participant reference.
75
+ * @returns {Function} Selector.
76
+ */
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);
86
+
15 87
 /**
16 88
  * Creates a local video track for presenter. The constraints are computed based
17 89
  * on the height of the desktop that is being shared.
@@ -311,7 +383,7 @@ export function getLocalVideoType(tracks) {
311 383
  * @returns {Object}
312 384
  */
313 385
 export function getLocalJitsiVideoTrack(state) {
314
-    const track = getLocalVideoTrack(state['features/base/tracks']);
386
+    const track = getLocalVideoTrack(getTrackState(state));
315 387
 
316 388
     return track?.jitsiTrack;
317 389
 }
@@ -323,7 +395,7 @@ export function getLocalJitsiVideoTrack(state) {
323 395
  * @returns {Object}
324 396
  */
325 397
 export function getLocalJitsiAudioTrack(state) {
326
-    const track = getLocalAudioTrack(state['features/base/tracks']);
398
+    const track = getLocalAudioTrack(getTrackState(state));
327 399
 
328 400
     return track?.jitsiTrack;
329 401
 }
@@ -413,7 +485,7 @@ export function isLocalTrackMuted(tracks, mediaType) {
413 485
  * @returns {boolean}
414 486
  */
415 487
 export function isLocalVideoTrackDesktop(state) {
416
-    const videoTrack = getLocalVideoTrack(state['features/base/tracks']);
488
+    const videoTrack = getLocalVideoTrack(getTrackState(state));
417 489
 
418 490
     return videoTrack && videoTrack.videoType === VIDEO_TYPE.DESKTOP;
419 491
 }

+ 31
- 18
react/features/conference/components/web/Conference.js 查看文件

@@ -14,6 +14,8 @@ import { Filmstrip } from '../../../filmstrip';
14 14
 import { CalleeInfoContainer } from '../../../invite';
15 15
 import { LargeVideo } from '../../../large-video';
16 16
 import { KnockingParticipantList, LobbyScreen } from '../../../lobby';
17
+import { ParticipantsPane } from '../../../participants-pane/components';
18
+import { getParticipantsPaneOpen } from '../../../participants-pane/functions';
17 19
 import { Prejoin, isPrejoinPageVisible } from '../../../prejoin';
18 20
 import { fullScreenChanged, showToolbox } from '../../../toolbox/actions.web';
19 21
 import { Toolbox } from '../../../toolbox/components/web';
@@ -72,6 +74,11 @@ type Props = AbstractProps & {
72 74
      */
73 75
     _isLobbyScreenVisible: boolean,
74 76
 
77
+    /**
78
+     * If participants pane is visible or not.
79
+     */
80
+    _isParticipantsPaneVisible: boolean,
81
+
75 82
     /**
76 83
      * The CSS class to apply to the root of {@link Conference} to modify the
77 84
      * application layout.
@@ -179,33 +186,38 @@ class Conference extends AbstractConference<Props, *> {
179 186
     render() {
180 187
         const {
181 188
             _isLobbyScreenVisible,
189
+            _isParticipantsPaneVisible,
182 190
             _layoutClassName,
183 191
             _showPrejoin
184 192
         } = this.props;
185 193
 
186 194
         return (
187
-            <div
188
-                className = { _layoutClassName }
189
-                id = 'videoconference_page'
190
-                onMouseMove = { this._onShowToolbar }
191
-                ref = { this._setBackground }>
192
-                <ConferenceInfo />
193
-
194
-                <Notice />
195
-                <div id = 'videospace'>
196
-                    <LargeVideo />
197
-                    <KnockingParticipantList />
198
-                    <Filmstrip />
199
-                </div>
195
+            <div id = 'layout_wrapper'>
196
+                <div
197
+                    className = { _layoutClassName }
198
+                    id = 'videoconference_page'
199
+                    onMouseMove = { this._onShowToolbar }
200
+                    ref = { this._setBackground }>
201
+                    <ConferenceInfo />
200 202
 
201
-                { _showPrejoin || _isLobbyScreenVisible || <Toolbox /> }
202
-                <Chat />
203
+                    <Notice />
204
+                    <div id = 'videospace'>
205
+                        <LargeVideo />
206
+                        {!_isParticipantsPaneVisible && <KnockingParticipantList />}
207
+                        <Filmstrip />
208
+                    </div>
203 209
 
204
-                { this.renderNotificationsContainer() }
210
+                    { _showPrejoin || _isLobbyScreenVisible || <Toolbox /> }
211
+                    <Chat />
205 212
 
206
-                <CalleeInfoContainer />
213
+                    { this.renderNotificationsContainer() }
207 214
 
208
-                { _showPrejoin && <Prejoin />}
215
+                    <CalleeInfoContainer />
216
+
217
+                    { _showPrejoin && <Prejoin />}
218
+
219
+                </div>
220
+                <ParticipantsPane />
209 221
             </div>
210 222
         );
211 223
     }
@@ -297,6 +309,7 @@ function _mapStateToProps(state) {
297 309
         ...abstractMapStateToProps(state),
298 310
         _backgroundAlpha: state['features/base/config'].backgroundAlpha,
299 311
         _isLobbyScreenVisible: state['features/base/dialog']?.component === LobbyScreen,
312
+        _isParticipantsPaneVisible: getParticipantsPaneOpen(state),
300 313
         _layoutClassName: LAYOUT_CLASSNAMES[getCurrentLayout(state)],
301 314
         _roomName: getConferenceNameForTitle(state),
302 315
         _showPrejoin: isPrejoinPageVisible(state)

+ 14
- 0
react/features/filmstrip/subscriber.web.js 查看文件

@@ -3,6 +3,7 @@
3 3
 import { StateListenerRegistry, equals } from '../base/redux';
4 4
 import { clientResized } from '../base/responsive-ui';
5 5
 import { setFilmstripVisible } from '../filmstrip/actions';
6
+import { getParticipantsPaneOpen } from '../participants-pane/functions';
6 7
 import { setOverflowDrawer } from '../toolbox/actions.web';
7 8
 import { getCurrentLayout, getTileViewGridDimensions, shouldDisplayTileView, LAYOUTS } from '../video-layout';
8 9
 
@@ -92,6 +93,19 @@ StateListenerRegistry.register(
92 93
         store.dispatch(clientResized(innerWidth, innerHeight));
93 94
     });
94 95
 
96
+/**
97
+ * Listens for changes in the participant pane state to calculate the
98
+ * dimensions of the tile view grid and the tiles.
99
+ */
100
+StateListenerRegistry.register(
101
+    /* selector */ getParticipantsPaneOpen,
102
+    /* listener */ (isOpen, store) => {
103
+        const { innerWidth, innerHeight } = window;
104
+
105
+        store.dispatch(clientResized(innerWidth, innerHeight));
106
+    });
107
+
108
+
95 109
 /**
96 110
  * Listens for changes in the client width to determine whether the overflow menu(s) should be displayed as drawers.
97 111
  */

+ 2
- 1
react/features/lobby/components/AbstractKnockingParticipantList.js 查看文件

@@ -4,6 +4,7 @@ import { PureComponent } from 'react';
4 4
 
5 5
 import { isLocalParticipantModerator } from '../../base/participants';
6 6
 import { setKnockingParticipantApproval } from '../actions';
7
+import { getLobbyState } from '../functions';
7 8
 
8 9
 export type Props = {
9 10
 
@@ -66,7 +67,7 @@ export default class AbstractKnockingParticipantList<P: Props = Props> extends P
66 67
  * @returns {Props}
67 68
  */
68 69
 export function mapStateToProps(state: Object): $Shape<Props> {
69
-    const { knockingParticipants, lobbyEnabled } = state['features/lobby'];
70
+    const { knockingParticipants, lobbyEnabled } = getLobbyState(state);
70 71
 
71 72
     return {
72 73
         _participants: knockingParticipants,

+ 11
- 0
react/features/lobby/functions.js 查看文件

@@ -21,3 +21,14 @@ export function setKnockingParticipantApproval(getState: Function, id: string, a
21 21
         }
22 22
     }
23 23
 }
24
+
25
+
26
+/**
27
+ * Selector to return lobby state.
28
+ *
29
+ * @param {any} state - State object.
30
+ * @returns {any}
31
+ */
32
+export function getLobbyState(state: any) {
33
+    return state['features/lobby'];
34
+}

+ 9
- 0
react/features/participants-pane/actionTypes.js 查看文件

@@ -0,0 +1,9 @@
1
+/**
2
+ * Action type to signal the closing of the participants pane.
3
+ */
4
+export const PARTICIPANTS_PANE_CLOSE = 'PARTICIPANTS_PANE_CLOSE';
5
+
6
+/**
7
+ * Action type to signal the opening of the participants pane.
8
+ */
9
+export const PARTICIPANTS_PANE_OPEN = 'PARTICIPANTS_PANE_OPEN';

+ 26
- 0
react/features/participants-pane/actions.js 查看文件

@@ -0,0 +1,26 @@
1
+import {
2
+    PARTICIPANTS_PANE_CLOSE,
3
+    PARTICIPANTS_PANE_OPEN
4
+} from './actionTypes';
5
+
6
+/**
7
+ * Action to close the participants pane.
8
+ *
9
+ * @returns {Object}
10
+ */
11
+export const close = () => {
12
+    return {
13
+        type: PARTICIPANTS_PANE_CLOSE
14
+    };
15
+};
16
+
17
+/**
18
+ * Action to open the participants pane.
19
+ *
20
+ * @returns {Object}
21
+ */
22
+export const open = () => {
23
+    return {
24
+        type: PARTICIPANTS_PANE_OPEN
25
+    };
26
+};

+ 32
- 0
react/features/participants-pane/components/InviteButton.js 查看文件

@@ -0,0 +1,32 @@
1
+// @flow
2
+
3
+import React, { useCallback } from 'react';
4
+import { useTranslation } from 'react-i18next';
5
+import { useDispatch } from 'react-redux';
6
+
7
+import { createToolbarEvent, sendAnalytics } from '../../analytics';
8
+import { Icon, IconInviteMore } from '../../base/icons';
9
+import { beginAddPeople } from '../../invite';
10
+
11
+import { ParticipantInviteButton } from './styled';
12
+
13
+export const InviteButton = () => {
14
+    const dispatch = useDispatch();
15
+    const { t } = useTranslation();
16
+
17
+    const onInvite = useCallback(() => {
18
+        sendAnalytics(createToolbarEvent('invite'));
19
+        dispatch(beginAddPeople());
20
+    }, [ dispatch ]);
21
+
22
+    return (
23
+        <ParticipantInviteButton
24
+            aria-label = { t('toolbar.accessibilityLabel.invite') }
25
+            onClick = { onInvite }>
26
+            <Icon
27
+                size = { 20 }
28
+                src = { IconInviteMore } />
29
+            <span>Invite Someone</span>
30
+        </ParticipantInviteButton>
31
+    );
32
+};

+ 44
- 0
react/features/participants-pane/components/LobbyParticipantItem.js 查看文件

@@ -0,0 +1,44 @@
1
+// @flow
2
+
3
+import React, { useCallback } from 'react';
4
+import { useTranslation } from 'react-i18next';
5
+import { useDispatch } from 'react-redux';
6
+
7
+import { setKnockingParticipantApproval } from '../../lobby/actions';
8
+import { ActionTrigger, MediaState } from '../constants';
9
+
10
+import { ParticipantItem } from './ParticipantItem';
11
+import { ParticipantActionButton } from './styled';
12
+
13
+type Props = {
14
+
15
+    /**
16
+     * Participant reference
17
+     */
18
+    participant: Object
19
+};
20
+
21
+export const LobbyParticipantItem = ({ participant: p }: Props) => {
22
+    const dispatch = useDispatch();
23
+    const admit = useCallback(() => dispatch(setKnockingParticipantApproval(p.id, true), [ dispatch ]));
24
+    const reject = useCallback(() => dispatch(setKnockingParticipantApproval(p.id, false), [ dispatch ]));
25
+    const { t } = useTranslation();
26
+
27
+    return (
28
+        <ParticipantItem
29
+            actionsTrigger = { ActionTrigger.Permanent }
30
+            audioMuteState = { MediaState.None }
31
+            participant = { p }
32
+            videoMuteState = { MediaState.None }>
33
+            <ParticipantActionButton
34
+                onClick = { reject }>
35
+                {t('lobby.reject')}
36
+            </ParticipantActionButton>
37
+            <ParticipantActionButton
38
+                onClick = { admit }
39
+                primary = { true }>
40
+                {t('lobby.admit')}
41
+            </ParticipantActionButton>
42
+        </ParticipantItem>
43
+    );
44
+};

+ 35
- 0
react/features/participants-pane/components/LobbyParticipantList.js 查看文件

@@ -0,0 +1,35 @@
1
+// @flow
2
+
3
+import React from 'react';
4
+import { useTranslation } from 'react-i18next';
5
+import { useSelector } from 'react-redux';
6
+
7
+import { getLobbyState } from '../../lobby/functions';
8
+
9
+import { LobbyParticipantItem } from './LobbyParticipantItem';
10
+import { Heading } from './styled';
11
+
12
+export const LobbyParticipantList = () => {
13
+    const {
14
+        lobbyEnabled,
15
+        knockingParticipants: participants
16
+    } = useSelector(getLobbyState);
17
+    const { t } = useTranslation();
18
+
19
+    if (!lobbyEnabled || !participants.length) {
20
+        return null;
21
+    }
22
+
23
+    return (
24
+    <>
25
+        <Heading>{t('participantsPane.headings.lobby', { count: participants.length })}</Heading>
26
+        <div>
27
+            {participants.map(p => (
28
+                <LobbyParticipantItem
29
+                    key = { p.id }
30
+                    participant = { p } />)
31
+            )}
32
+        </div>
33
+    </>
34
+    );
35
+};

+ 166
- 0
react/features/participants-pane/components/MeetingParticipantContextMenu.js 查看文件

@@ -0,0 +1,166 @@
1
+// @flow
2
+
3
+import React, { useCallback, useLayoutEffect, useRef, useState } from 'react';
4
+import { useTranslation } from 'react-i18next';
5
+import { useDispatch, useSelector } from 'react-redux';
6
+
7
+import { openDialog } from '../../base/dialog';
8
+import {
9
+    IconCloseCircle,
10
+    IconCrown,
11
+    IconMessage,
12
+    IconMuteEveryoneElse,
13
+    IconVideoOff
14
+} from '../../base/icons';
15
+import { isLocalParticipantModerator } from '../../base/participants';
16
+import { getIsParticipantVideoMuted } from '../../base/tracks';
17
+import { openChat } from '../../chat/actions';
18
+import { GrantModeratorDialog, KickRemoteParticipantDialog, MuteEveryoneDialog } from '../../video-menu';
19
+import MuteRemoteParticipantsVideoDialog from '../../video-menu/components/web/MuteRemoteParticipantsVideoDialog';
20
+import { getComputedOuterHeight } from '../functions';
21
+
22
+import {
23
+    ContextMenu,
24
+    ContextMenuIcon,
25
+    ContextMenuItem,
26
+    ContextMenuItemGroup,
27
+    ignoredChildClassName
28
+} from './styled';
29
+
30
+type Props = {
31
+
32
+    /**
33
+     * Target elements against which positioning calculations are made
34
+     */
35
+    offsetTarget: HTMLElement,
36
+
37
+    /**
38
+     * Callback for the mouse entering the component
39
+     */
40
+    onEnter: Function,
41
+
42
+    /**
43
+     * Callback for the mouse leaving the component
44
+     */
45
+    onLeave: Function,
46
+
47
+    /**
48
+     * Callback for making a selection in the menu
49
+     */
50
+    onSelect: Function,
51
+
52
+    /**
53
+     * Participant reference
54
+     */
55
+    participant: Object
56
+};
57
+
58
+export const MeetingParticipantContextMenu = ({
59
+    offsetTarget,
60
+    onEnter,
61
+    onLeave,
62
+    onSelect,
63
+    participant
64
+}: Props) => {
65
+    const dispatch = useDispatch();
66
+    const containerRef = useRef(null);
67
+    const isLocalModerator = useSelector(isLocalParticipantModerator);
68
+    const isParticipantVideoMuted = useSelector(getIsParticipantVideoMuted(participant));
69
+    const [ isHidden, setIsHidden ] = useState(true);
70
+    const { t } = useTranslation();
71
+
72
+    useLayoutEffect(() => {
73
+        if (participant
74
+            && containerRef.current
75
+            && offsetTarget?.offsetParent
76
+            && offsetTarget.offsetParent instanceof HTMLElement
77
+        ) {
78
+            const { current: container } = containerRef;
79
+            const { offsetTop, offsetParent: { offsetHeight, scrollTop } } = offsetTarget;
80
+            const outerHeight = getComputedOuterHeight(container);
81
+
82
+            container.style.top = offsetTop + outerHeight > offsetHeight + scrollTop
83
+                ? offsetTop - outerHeight
84
+                : offsetTop;
85
+
86
+            setIsHidden(false);
87
+        } else {
88
+            setIsHidden(true);
89
+        }
90
+    }, [ participant, offsetTarget ]);
91
+
92
+    const grantModerator = useCallback(() => {
93
+        dispatch(openDialog(GrantModeratorDialog, {
94
+            participantID: participant.id
95
+        }));
96
+    }, [ dispatch, participant ]);
97
+
98
+    const kick = useCallback(() => {
99
+        dispatch(openDialog(KickRemoteParticipantDialog, {
100
+            participantID: participant.id
101
+        }));
102
+    }, [ dispatch, participant ]);
103
+
104
+    const muteEveryoneElse = useCallback(() => {
105
+        dispatch(openDialog(MuteEveryoneDialog, {
106
+            exclude: [ participant.id ]
107
+        }));
108
+    }, [ dispatch, participant ]);
109
+
110
+    const muteVideo = useCallback(() => {
111
+        dispatch(openDialog(MuteRemoteParticipantsVideoDialog, {
112
+            participantID: participant.id
113
+        }));
114
+    }, [ dispatch, participant ]);
115
+
116
+    const sendPrivateMessage = useCallback(() => {
117
+        dispatch(openChat(participant));
118
+    }, [ dispatch, participant ]);
119
+
120
+    if (!participant) {
121
+        return null;
122
+    }
123
+
124
+    return (
125
+        <ContextMenu
126
+            className = { ignoredChildClassName }
127
+            innerRef = { containerRef }
128
+            isHidden = { isHidden }
129
+            onClick = { onSelect }
130
+            onMouseEnter = { onEnter }
131
+            onMouseLeave = { onLeave }>
132
+            <ContextMenuItemGroup>
133
+                {isLocalModerator && (
134
+                    <ContextMenuItem onClick = { muteEveryoneElse }>
135
+                        <ContextMenuIcon src = { IconMuteEveryoneElse } />
136
+                        <span>{t('toolbar.accessibilityLabel.muteEveryoneElse')}</span>
137
+                    </ContextMenuItem>
138
+                )}
139
+                {isLocalModerator && (isParticipantVideoMuted || (
140
+                    <ContextMenuItem onClick = { muteVideo }>
141
+                        <ContextMenuIcon src = { IconVideoOff } />
142
+                        <span>{t('participantsPane.actions.stopVideo')}</span>
143
+                    </ContextMenuItem>
144
+                ))}
145
+            </ContextMenuItemGroup>
146
+            <ContextMenuItemGroup>
147
+                {isLocalModerator && (
148
+                    <ContextMenuItem onClick = { grantModerator }>
149
+                        <ContextMenuIcon src = { IconCrown } />
150
+                        <span>{t('toolbar.accessibilityLabel.grantModerator')}</span>
151
+                    </ContextMenuItem>
152
+                )}
153
+                {isLocalModerator && (
154
+                    <ContextMenuItem onClick = { kick }>
155
+                        <ContextMenuIcon src = { IconCloseCircle } />
156
+                        <span>{t('videothumbnail.kick')}</span>
157
+                    </ContextMenuItem>
158
+                )}
159
+                <ContextMenuItem onClick = { sendPrivateMessage }>
160
+                    <ContextMenuIcon src = { IconMessage } />
161
+                    <span>{t('toolbar.accessibilityLabel.privateMessage')}</span>
162
+                </ContextMenuItem>
163
+            </ContextMenuItemGroup>
164
+        </ContextMenu>
165
+    );
166
+};

+ 55
- 0
react/features/participants-pane/components/MeetingParticipantItem.js 查看文件

@@ -0,0 +1,55 @@
1
+// @flow
2
+
3
+import React from 'react';
4
+import { useSelector } from 'react-redux';
5
+
6
+import { getIsParticipantAudioMuted, getIsParticipantVideoMuted } from '../../base/tracks';
7
+import { ActionTrigger, MediaState } from '../constants';
8
+
9
+import { ParticipantItem } from './ParticipantItem';
10
+import { ParticipantActionEllipsis } from './styled';
11
+
12
+type Props = {
13
+
14
+    /**
15
+     * Is this item highlighted
16
+     */
17
+    isHighlighted: boolean,
18
+
19
+    /**
20
+     * Callback for the activation of this item's context menu
21
+     */
22
+    onContextMenu: Function,
23
+
24
+    /**
25
+     * Callback for the mouse leaving this item
26
+     */
27
+    onLeave: Function,
28
+
29
+    /**
30
+     * Participant reference
31
+     */
32
+    participant: Object
33
+};
34
+
35
+export const MeetingParticipantItem = ({
36
+    isHighlighted,
37
+    onContextMenu,
38
+    onLeave,
39
+    participant
40
+}: Props) => {
41
+    const isAudioMuted = useSelector(getIsParticipantAudioMuted(participant));
42
+    const isVideoMuted = useSelector(getIsParticipantVideoMuted(participant));
43
+
44
+    return (
45
+        <ParticipantItem
46
+            actionsTrigger = { ActionTrigger.Hover }
47
+            audioMuteState = { isAudioMuted ? MediaState.Muted : MediaState.Unmuted }
48
+            isHighlighted = { isHighlighted }
49
+            onLeave = { onLeave }
50
+            participant = { participant }
51
+            videoMuteState = { isVideoMuted ? MediaState.Muted : MediaState.Unmuted }>
52
+            <ParticipantActionEllipsis onClick = { onContextMenu } />
53
+        </ParticipantItem>
54
+    );
55
+};

+ 108
- 0
react/features/participants-pane/components/MeetingParticipantList.js 查看文件

@@ -0,0 +1,108 @@
1
+// @flow
2
+
3
+import _ from 'lodash';
4
+import React, { useCallback, useRef, useState } from 'react';
5
+import { useTranslation } from 'react-i18next';
6
+import { useSelector } from 'react-redux';
7
+
8
+import { getParticipants } from '../../base/participants';
9
+import { findStyledAncestor } from '../functions';
10
+
11
+import { InviteButton } from './InviteButton';
12
+import { MeetingParticipantContextMenu } from './MeetingParticipantContextMenu';
13
+import { MeetingParticipantItem } from './MeetingParticipantItem';
14
+import { Heading, ParticipantContainer } from './styled';
15
+
16
+type NullProto = {
17
+  [key: string]: any,
18
+  __proto__: null
19
+};
20
+
21
+type RaiseContext = NullProto | {
22
+
23
+  /**
24
+   * Target elements against which positioning calculations are made
25
+   */
26
+  offsetTarget?: HTMLElement,
27
+
28
+  /**
29
+   * Participant reference
30
+   */
31
+  participant?: Object,
32
+};
33
+
34
+const initialState = Object.freeze(Object.create(null));
35
+
36
+export const MeetingParticipantList = () => {
37
+    const isMouseOverMenu = useRef(false);
38
+    const participants = useSelector(getParticipants, _.isEqual);
39
+    const [ raiseContext, setRaiseContext ] = useState<RaiseContext>(initialState);
40
+    const { t } = useTranslation();
41
+
42
+    const lowerMenu = useCallback(() => {
43
+        /**
44
+         * We are tracking mouse movement over the active participant item and
45
+         * the context menu. Due to the order of enter/leave events, we need to
46
+         * defer checking if the mouse is over the context menu with
47
+         * queueMicrotask
48
+         */
49
+        window.queueMicrotask(() => {
50
+            if (isMouseOverMenu.current) {
51
+                return;
52
+            }
53
+
54
+            if (raiseContext !== initialState) {
55
+                setRaiseContext(initialState);
56
+            }
57
+        });
58
+    }, [ raiseContext ]);
59
+
60
+    const raiseMenu = useCallback((participant, target) => {
61
+        setRaiseContext({
62
+            participant,
63
+            offsetTarget: findStyledAncestor(target, ParticipantContainer)
64
+        });
65
+    }, [ raiseContext ]);
66
+
67
+    const toggleMenu = useCallback(participant => e => {
68
+        const { participant: raisedParticipant } = raiseContext;
69
+
70
+        if (raisedParticipant && raisedParticipant === participant) {
71
+            lowerMenu();
72
+        } else {
73
+            raiseMenu(participant, e.target);
74
+        }
75
+    }, [ raiseContext ]);
76
+
77
+    const menuEnter = useCallback(() => {
78
+        isMouseOverMenu.current = true;
79
+    }, []);
80
+
81
+    const menuLeave = useCallback(() => {
82
+        isMouseOverMenu.current = false;
83
+        lowerMenu();
84
+    }, [ lowerMenu ]);
85
+
86
+    return (
87
+    <>
88
+        <Heading>{t('participantsPane.headings.participantsList', { count: participants.length })}</Heading>
89
+        <InviteButton />
90
+        <div>
91
+            {participants.map(p => (
92
+                <MeetingParticipantItem
93
+                    isHighlighted = { raiseContext.participant === p }
94
+                    key = { p.id }
95
+                    onContextMenu = { toggleMenu(p) }
96
+                    onLeave = { lowerMenu }
97
+                    participant = { p } />
98
+            ))}
99
+        </div>
100
+        <MeetingParticipantContextMenu
101
+            onEnter = { menuEnter }
102
+            onLeave = { menuLeave }
103
+            onSelect = { lowerMenu }
104
+            { ...raiseContext } />
105
+    </>
106
+    );
107
+};
108
+

+ 154
- 0
react/features/participants-pane/components/ParticipantItem.js 查看文件

@@ -0,0 +1,154 @@
1
+// @flow
2
+
3
+import React, { type Node } from 'react';
4
+import { useTranslation } from 'react-i18next';
5
+
6
+import { Avatar } from '../../base/avatar';
7
+import {
8
+    Icon,
9
+    IconCameraEmpty,
10
+    IconCameraEmptyDisabled,
11
+    IconMicrophoneEmpty,
12
+    IconMicrophoneEmptySlash
13
+} from '../../base/icons';
14
+import { ActionTrigger, MediaState } from '../constants';
15
+
16
+import { RaisedHandIndicator } from './RaisedHandIndicator';
17
+import {
18
+    ParticipantActionsHover,
19
+    ParticipantActionsPermanent,
20
+    ParticipantContainer,
21
+    ParticipantContent,
22
+    ParticipantName,
23
+    ParticipantNameContainer,
24
+    ParticipantStates
25
+} from './styled';
26
+
27
+/**
28
+ * Participant actions component mapping depending on trigger type.
29
+ */
30
+const Actions = {
31
+    [ActionTrigger.Hover]: ParticipantActionsHover,
32
+    [ActionTrigger.Permanent]: ParticipantActionsPermanent
33
+};
34
+
35
+/**
36
+ * Icon mapping for possible participant audio states.
37
+ */
38
+const AudioStateIcons = {
39
+    [MediaState.ForceMuted]: (
40
+        <Icon
41
+            size = { 16 }
42
+            src = { IconMicrophoneEmptySlash } />
43
+    ),
44
+    [MediaState.Muted]: (
45
+        <Icon
46
+            size = { 16 }
47
+            src = { IconMicrophoneEmptySlash } />
48
+    ),
49
+    [MediaState.Unmuted]: (
50
+        <Icon
51
+            size = { 16 }
52
+            src = { IconMicrophoneEmpty } />
53
+    ),
54
+    [MediaState.None]: null
55
+};
56
+
57
+/**
58
+ * Icon mapping for possible participant video states.
59
+ */
60
+const VideoStateIcons = {
61
+    [MediaState.ForceMuted]: (
62
+        <Icon
63
+            size = { 16 }
64
+            src = { IconCameraEmptyDisabled } />
65
+    ),
66
+    [MediaState.Muted]: (
67
+        <Icon
68
+            size = { 16 }
69
+            src = { IconCameraEmptyDisabled } />
70
+    ),
71
+    [MediaState.Unmuted]: (
72
+        <Icon
73
+            size = { 16 }
74
+            src = { IconCameraEmpty } />
75
+    ),
76
+    [MediaState.None]: null
77
+};
78
+
79
+type Props = {
80
+
81
+    /**
82
+     * Type of trigger for the participant actions
83
+     */
84
+    actionsTrigger: ActionTrigger,
85
+
86
+    /**
87
+     * Media state for audio
88
+     */
89
+    audioMuteState: MediaState,
90
+
91
+    /**
92
+     * React children
93
+     */
94
+    children: Node,
95
+
96
+    /**
97
+     * Is this item highlighted/raised
98
+     */
99
+    isHighlighted?: boolean,
100
+
101
+    /**
102
+     * Callback for when the mouse leaves this component
103
+     */
104
+    onLeave?: Function,
105
+
106
+    /**
107
+     * Participant reference
108
+     */
109
+    participant: Object,
110
+
111
+    /**
112
+     * Media state for video
113
+     */
114
+    videoMuteState: MediaState
115
+}
116
+
117
+export const ParticipantItem = ({
118
+    children,
119
+    isHighlighted,
120
+    onLeave,
121
+    actionsTrigger = ActionTrigger.Hover,
122
+    audioMuteState = MediaState.None,
123
+    videoMuteState = MediaState.None,
124
+    participant: p
125
+}: Props) => {
126
+    const ParticipantActions = Actions[actionsTrigger];
127
+    const { t } = useTranslation();
128
+
129
+    return (
130
+        <ParticipantContainer
131
+            isHighlighted = { isHighlighted }
132
+            onMouseLeave = { onLeave }
133
+            trigger = { actionsTrigger }>
134
+            <Avatar
135
+                className = 'participant-avatar'
136
+                participantId = { p.id }
137
+                size = { 32 } />
138
+            <ParticipantContent>
139
+                <ParticipantNameContainer>
140
+                    <ParticipantName>
141
+                        { p.name }
142
+                    </ParticipantName>
143
+                    { p.local ? <span>&nbsp;({t('chat.you')})</span> : null }
144
+                </ParticipantNameContainer>
145
+                { !p.local && <ParticipantActions children = { children } /> }
146
+                <ParticipantStates>
147
+                    {p.raisedHand && <RaisedHandIndicator />}
148
+                    {VideoStateIcons[videoMuteState]}
149
+                    {AudioStateIcons[audioMuteState]}
150
+                </ParticipantStates>
151
+            </ParticipantContent>
152
+        </ParticipantContainer>
153
+    );
154
+};

+ 62
- 0
react/features/participants-pane/components/ParticipantsPane.js 查看文件

@@ -0,0 +1,62 @@
1
+// @flow
2
+
3
+import React, { useCallback } from 'react';
4
+import { useTranslation } from 'react-i18next';
5
+import { useDispatch, useSelector } from 'react-redux';
6
+import { ThemeProvider } from 'styled-components';
7
+
8
+import { openDialog } from '../../base/dialog';
9
+import { isLocalParticipantModerator } from '../../base/participants';
10
+import { MuteEveryoneDialog } from '../../video-menu/components/';
11
+import { close } from '../actions';
12
+import { classList, getParticipantsPaneOpen } from '../functions';
13
+import theme from '../theme.json';
14
+
15
+import { LobbyParticipantList } from './LobbyParticipantList';
16
+import { MeetingParticipantList } from './MeetingParticipantList';
17
+import {
18
+    AntiCollapse,
19
+    Close,
20
+    Container,
21
+    Footer,
22
+    FooterButton,
23
+    Header
24
+} from './styled';
25
+
26
+export const ParticipantsPane = () => {
27
+    const dispatch = useDispatch();
28
+    const paneOpen = useSelector(getParticipantsPaneOpen);
29
+    const isLocalModerator = useSelector(isLocalParticipantModerator);
30
+    const { t } = useTranslation();
31
+
32
+    const closePane = useCallback(() => dispatch(close(), [ dispatch ]));
33
+    const muteAll = useCallback(() => dispatch(openDialog(MuteEveryoneDialog)), [ dispatch ]);
34
+
35
+    return (
36
+        <ThemeProvider theme = { theme }>
37
+            <div
38
+                className = { classList(
39
+          'participants_pane',
40
+          !paneOpen && 'participants_pane--closed'
41
+                ) }>
42
+                <div className = 'participants_pane-content'>
43
+                    <Header>
44
+                        <Close onClick = { closePane } />
45
+                    </Header>
46
+                    <Container>
47
+                        <LobbyParticipantList />
48
+                        <AntiCollapse />
49
+                        <MeetingParticipantList />
50
+                    </Container>
51
+                    {isLocalModerator && (
52
+                        <Footer>
53
+                            <FooterButton onClick = { muteAll }>
54
+                                {t('participantsPane.actions.muteAll')}
55
+                            </FooterButton>
56
+                        </Footer>
57
+                    )}
58
+                </div>
59
+            </div>
60
+        </ThemeProvider>
61
+    );
62
+};

+ 15
- 0
react/features/participants-pane/components/RaisedHandIndicator.js 查看文件

@@ -0,0 +1,15 @@
1
+// @flow
2
+
3
+import React from 'react';
4
+
5
+import { Icon, IconRaisedHandHollow } from '../../base/icons';
6
+
7
+import { RaisedHandIndicatorBackground } from './styled';
8
+
9
+export const RaisedHandIndicator = () => (
10
+    <RaisedHandIndicatorBackground>
11
+        <Icon
12
+            size = { 15 }
13
+            src = { IconRaisedHandHollow } />
14
+    </RaisedHandIndicatorBackground>
15
+);

+ 9
- 0
react/features/participants-pane/components/index.js 查看文件

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

+ 335
- 0
react/features/participants-pane/components/styled.js 查看文件

@@ -0,0 +1,335 @@
1
+import React from 'react';
2
+import styled from 'styled-components';
3
+
4
+import { Icon, IconHorizontalPoints } from '../../base/icons';
5
+import { ActionTrigger } from '../constants';
6
+
7
+export const ignoredChildClassName = 'ignore-child';
8
+
9
+export const AntiCollapse = styled.br`
10
+  font-size: 0;
11
+`;
12
+
13
+export const Button = styled.button`
14
+  align-items: center;
15
+  background-color: ${
16
+    // eslint-disable-next-line no-confusing-arrow
17
+    props => props.primary ? '#0056E0' : '#3D3D3D'
18
+};
19
+  border: 0;
20
+  border-radius: 6px;
21
+  display: flex;
22
+  font-weight: unset;
23
+  justify-content: center;
24
+
25
+  &:hover {
26
+    background-color: ${
27
+    // eslint-disable-next-line no-confusing-arrow
28
+    props => props.primary ? '#246FE5' : '#525252'
29
+};
30
+  }
31
+`;
32
+
33
+export const Container = styled.div`
34
+  box-sizing: border-box;
35
+  flex: 1;
36
+  overflow-y: auto;
37
+  position: relative;
38
+  padding: 0 ${props => props.theme.panePadding}px;
39
+
40
+  & > * + *:not(.${ignoredChildClassName}) {
41
+    margin-top: 16px;
42
+  }
43
+
44
+  &::-webkit-scrollbar {
45
+    display: none;
46
+  }
47
+`;
48
+
49
+export const ContextMenu = styled.div.attrs(props => {
50
+    return {
51
+        className: props.className
52
+    };
53
+})`
54
+  background-color: #292929;
55
+  border-radius: 3px;
56
+  box-shadow: 0px 3px 16px rgba(0, 0, 0, 0.6), 0px 0px 4px 1px rgba(0, 0, 0, 0.25);
57
+  color: white;
58
+  font-size: ${props => props.theme.contextFontSize}px;
59
+  font-weight: ${props => props.theme.contextFontWeight};
60
+  margin-top: ${props => {
61
+        const {
62
+            participantActionButtonHeight,
63
+            participantItemHeight
64
+        } = props.theme;
65
+
66
+        return ((3 * participantItemHeight) + participantActionButtonHeight) / 4;
67
+    }}px;
68
+  position: absolute;
69
+  right: ${props => props.theme.panePadding}px;
70
+  top: 0;
71
+  z-index: 2;
72
+
73
+  & > li {
74
+    list-style: none;
75
+  }
76
+
77
+  ${props => props.isHidden && `
78
+    pointer-events: none;
79
+    visibility: hidden;
80
+  `}
81
+`;
82
+
83
+export const ContextMenuIcon = styled(Icon).attrs({
84
+    size: 20
85
+})`
86
+  & > svg {
87
+    fill: #a4b8d1;
88
+  }
89
+`;
90
+
91
+export const ContextMenuItem = styled.div`
92
+  align-items: center;
93
+  box-sizing: border-box;
94
+  cursor: pointer;
95
+  display: flex;
96
+  height: 40px;
97
+  padding: 8px 16px;
98
+
99
+  & > *:not(:last-child) {
100
+    margin-right: 16px;
101
+  }
102
+
103
+  &:hover {
104
+    background-color: #525252;
105
+  }
106
+`;
107
+
108
+export const ContextMenuItemGroup = styled.div`
109
+  &:not(:empty) {
110
+    padding: 8px 0;
111
+  }
112
+
113
+  & + &:not(:empty) {
114
+    border-top: 1px solid #4C4D50;
115
+  }
116
+`;
117
+
118
+export const Close = styled.div`
119
+  align-items: center;
120
+  cursor: pointer;
121
+  display: flex;
122
+  height: 20px;
123
+  justify-content: center;
124
+  width: 20px;
125
+
126
+  &:before, &:after {
127
+    content: '';
128
+    background-color: #a4b8d1;
129
+    border-radius: 2px;
130
+    height: 2px;
131
+    position: absolute;
132
+    transform-origin: center center;
133
+    width: 21px;
134
+  }
135
+
136
+  &:before {
137
+    transform: rotate(45deg);
138
+  }
139
+
140
+  &:after {
141
+    transform: rotate(-45deg);
142
+  }
143
+`;
144
+
145
+export const Footer = styled.div`
146
+  background-color: #141414;
147
+  display: flex;
148
+  justify-content: flex-end;
149
+  padding: 24px ${props => props.theme.panePadding}px;
150
+
151
+  & > *:not(:last-child) {
152
+    margin-right: 16px;
153
+  }
154
+`;
155
+
156
+export const FooterButton = styled(Button)`
157
+  height: 40px;
158
+  font-size: 15px;
159
+  padding: 0 16px;
160
+`;
161
+
162
+export const FooterEllipsisButton = styled(FooterButton).attrs({
163
+    children: <Icon src = { IconHorizontalPoints } />
164
+})`
165
+  padding: 8px;
166
+`;
167
+
168
+export const FooterEllipsisContainer = styled.div`
169
+  position: relative;
170
+`;
171
+
172
+export const Header = styled.div`
173
+  align-items: center;
174
+  box-sizing: border-box;
175
+  display: flex;
176
+  height: ${props => props.theme.headerSize}px;
177
+  padding: 0 20px;
178
+`;
179
+
180
+export const Heading = styled.div`
181
+  color: #d1dbe8;
182
+  font-style: normal;
183
+  font-size: 15px;
184
+  line-height: 24px;
185
+  margin: 8px 0 ${props => props.theme.panePadding}px;
186
+`;
187
+
188
+export const ParticipantActionButton = styled(Button)`
189
+  height: ${props => props.theme.participantActionButtonHeight}px;
190
+  padding: 6px 10px;
191
+`;
192
+
193
+export const ParticipantActionEllipsis = styled(ParticipantActionButton).attrs({
194
+    children: <Icon src = { IconHorizontalPoints } />,
195
+    primary: true
196
+})`
197
+  padding: 6px;
198
+`;
199
+
200
+export const ParticipantActions = styled.div`
201
+  align-items: center;
202
+  z-index: 1;
203
+
204
+  & > *:not(:last-child) {
205
+    margin-right: 8px;
206
+  }
207
+`;
208
+
209
+export const ParticipantActionsHover = styled(ParticipantActions)`
210
+  background-color: #292929;
211
+  bottom: 1px;
212
+  display: none;
213
+  position: absolute;
214
+  right: ${props => props.theme.panePadding};
215
+  top: 0;
216
+
217
+  &:after {
218
+    content: '';
219
+    background: linear-gradient(90deg, rgba(0, 0, 0, 0) 0%, #292929 100%);
220
+    bottom: 0;
221
+    display: block;
222
+    left: 0;
223
+    pointer-events: none;
224
+    position: absolute;
225
+    top: 0;
226
+    transform: translateX(-100%);
227
+    width: 40px;
228
+  }
229
+`;
230
+
231
+export const ParticipantActionsPermanent = styled(ParticipantActions)`
232
+  display: flex;
233
+`;
234
+
235
+export const ParticipantContent = styled.div`
236
+  align-items: center;
237
+  box-shadow: inset 0px -1px 0px rgba(255, 255, 255, 0.15);
238
+  display: flex;
239
+  flex: 1;
240
+  height: 100%;
241
+  overflow: hidden;
242
+  padding-right: ${props => props.theme.panePadding}px;
243
+`;
244
+
245
+export const ParticipantContainer = styled.div`
246
+  align-items: center;
247
+  color: white;
248
+  display: flex;
249
+  font-size: 13px;
250
+  height: ${props => props.theme.participantItemHeight}px;
251
+  margin: 0 -${props => props.theme.panePadding}px;
252
+  padding-left: ${props => props.theme.panePadding}px;
253
+  position: relative;
254
+
255
+  ${props => !props.isHighlighted && '&:hover {'}
256
+    background-color: #292929;
257
+
258
+    & ${ParticipantActions} {
259
+      ${props => props.trigger === ActionTrigger.Hover && `
260
+        display: flex;
261
+      `}
262
+    }
263
+
264
+    & ${ParticipantContent} {
265
+      box-shadow: none;
266
+    }
267
+  ${props => !props.isHighlighted && '}'}
268
+`;
269
+
270
+export const ParticipantInviteButton = styled(Button).attrs({
271
+    primary: true
272
+})`
273
+  font-size: 15px;
274
+  height: 40px;
275
+  width: 100%;
276
+
277
+  & > *:not(:last-child) {
278
+    margin-right: 8px;
279
+  }
280
+`;
281
+
282
+export const ParticipantName = styled.div`
283
+  overflow: hidden;
284
+  text-overflow: ellipsis;
285
+  white-space: nowrap;
286
+`;
287
+
288
+export const ParticipantNameContainer = styled.div`
289
+  display: flex;
290
+  flex: 1;
291
+  margin-right: 8px;
292
+  overflow: hidden;
293
+`;
294
+
295
+export const ParticipantStates = styled.div`
296
+  display: flex;
297
+  justify-content: flex-end;
298
+
299
+  & > * {
300
+    align-items: center;
301
+    display: flex;
302
+    justify-content: center;
303
+  }
304
+
305
+  & > *:not(:last-child) {
306
+    margin-right: 8px;
307
+  }
308
+`;
309
+
310
+export const RaisedHandIndicatorBackground = styled.div`
311
+  background-color: #ed9e1b;
312
+  border-radius: 3px;
313
+  height: 24px;
314
+  width: 24px;
315
+`;
316
+
317
+export const VolumeInput = styled.input.attrs({
318
+    type: 'range'
319
+})`
320
+  width: 100%;
321
+`;
322
+
323
+export const VolumeInputContainer = styled.div`
324
+  position: relative;
325
+  width: 100%;
326
+`;
327
+
328
+export const VolumeOverlay = styled.div`
329
+  background-color: #0376da;
330
+  border-radius: 1px 0 0 1px;
331
+  height: 100%;
332
+  left: 0;
333
+  pointer-events: none;
334
+  position: absolute;
335
+`;

+ 22
- 0
react/features/participants-pane/constants.js 查看文件

@@ -0,0 +1,22 @@
1
+/**
2
+ * Reducer key for the feature.
3
+ */
4
+export const REDUCER_KEY = 'features/participants-pane';
5
+
6
+/**
7
+ * Enum of possible participant action triggers.
8
+ */
9
+export const ActionTrigger = {
10
+    Hover: 'ActionTrigger.Hover',
11
+    Permanent: 'ActionTrigger.Permanent'
12
+};
13
+
14
+/**
15
+ * Enum of possible participant media states.
16
+ */
17
+export const MediaState = {
18
+    Muted: 'MediaState.Muted',
19
+    ForceMuted: 'MediaState.ForceMuted',
20
+    Unmuted: 'MediaState.Unmuted',
21
+    None: 'MediaState.None'
22
+};

+ 66
- 0
react/features/participants-pane/functions.js 查看文件

@@ -0,0 +1,66 @@
1
+import { REDUCER_KEY } from './constants';
2
+
3
+/**
4
+ * Generates a class attribute value.
5
+ *
6
+ * @param {Iterable<string>} args - String iterable.
7
+ * @returns {string} Class attribute value.
8
+ */
9
+export const classList = (...args) => args.filter(Boolean).join(' ');
10
+
11
+
12
+/**
13
+ * Find the first styled ancestor component of an element.
14
+ *
15
+ * @param {Element} target - Element to look up.
16
+ * @param {StyledComponentClass} component - Styled component reference.
17
+ * @returns {Element|null} Ancestor.
18
+ */
19
+export const findStyledAncestor = (target, component) => {
20
+    if (!target || target.matches(`.${component.styledComponentId}`)) {
21
+        return target;
22
+    }
23
+
24
+    return findStyledAncestor(target.parentElement, component);
25
+};
26
+
27
+/**
28
+ * Get a style property from a style declaration as a float.
29
+ *
30
+ * @param {CSSStyleDeclaration} styles - Style declaration.
31
+ * @param {string} name - Property name.
32
+ * @returns {number} Float value.
33
+ */
34
+export const getFloatStyleProperty = (styles, name) =>
35
+    parseFloat(styles.getPropertyValue(name));
36
+
37
+/**
38
+ * Gets the outer height of an element, including margins.
39
+ *
40
+ * @param {Element} element - Target element.
41
+ * @returns {number} Computed height.
42
+ */
43
+export const getComputedOuterHeight = element => {
44
+    const computedStyle = getComputedStyle(element);
45
+
46
+    return element.offsetHeight
47
+    + getFloatStyleProperty(computedStyle, 'margin-top')
48
+    + getFloatStyleProperty(computedStyle, 'margin-bottom');
49
+};
50
+
51
+/**
52
+ * Returns this feature's root state.
53
+ *
54
+ * @param {Object} state - Global state.
55
+ * @returns {Object} Feature state.
56
+ */
57
+const getState = state => state[REDUCER_KEY];
58
+
59
+/**
60
+ * Is the participants pane open.
61
+ *
62
+ * @param {Object} state - Global state.
63
+ * @returns {boolean} Is the participants pane open.
64
+ */
65
+export const getParticipantsPaneOpen = state => Boolean(getState(state).isOpen);
66
+

+ 35
- 0
react/features/participants-pane/reducer.js 查看文件

@@ -0,0 +1,35 @@
1
+import { ReducerRegistry } from '../base/redux';
2
+
3
+import {
4
+    PARTICIPANTS_PANE_CLOSE,
5
+    PARTICIPANTS_PANE_OPEN
6
+} from './actionTypes';
7
+import { REDUCER_KEY } from './constants';
8
+
9
+const DEFAULT_STATE = {
10
+    isOpen: false
11
+};
12
+
13
+/**
14
+ * Listen for actions that mutate the participants pane state
15
+ */
16
+ReducerRegistry.register(
17
+    REDUCER_KEY, (state = DEFAULT_STATE, action) => {
18
+        switch (action.type) {
19
+        case PARTICIPANTS_PANE_CLOSE:
20
+            return {
21
+                ...state,
22
+                isOpen: false
23
+            };
24
+
25
+        case PARTICIPANTS_PANE_OPEN:
26
+            return {
27
+                ...state,
28
+                isOpen: true
29
+            };
30
+
31
+        default:
32
+            return state;
33
+        }
34
+    },
35
+);

+ 10
- 0
react/features/participants-pane/theme.json 查看文件

@@ -0,0 +1,10 @@
1
+{
2
+  "contextFontSize": 14,
3
+  "contextFontWeight": 400,
4
+  "headerSize": 60,
5
+  "panePadding": 16,
6
+  "participantActionButtonHeight": 32,
7
+  "participantItemHeight": 48,
8
+  "participantsPaneWidth": 315,
9
+  "rangeInputThumbSize": 14
10
+}

+ 2
- 2
react/features/settings/components/web/audio/AudioSettingsContent.js 查看文件

@@ -3,7 +3,7 @@
3 3
 import React, { Component } from 'react';
4 4
 
5 5
 import { translate } from '../../../../base/i18n';
6
-import { IconMicrophoneEmpty, IconVolumeEmpty } from '../../../../base/icons';
6
+import { IconMicrophoneHollow, IconVolumeEmpty } from '../../../../base/icons';
7 7
 import JitsiMeetJS from '../../../../base/lib-jitsi-meet';
8 8
 import { equals } from '../../../../base/redux';
9 9
 import { createLocalAudioTracks } from '../../../functions';
@@ -248,7 +248,7 @@ class AudioSettingsContent extends Component<Props, State> {
248 248
             <div>
249 249
                 <div className = 'audio-preview-content'>
250 250
                     <AudioSettingsHeader
251
-                        IconComponent = { IconMicrophoneEmpty }
251
+                        IconComponent = { IconMicrophoneHollow }
252 252
                         text = { t('settings.microphones') } />
253 253
                     {this.state.audioTracks.map((data, i) =>
254 254
                         this._renderMicrophoneEntry(data, i),

+ 67
- 30
react/features/toolbox/components/web/Toolbox.js 查看文件

@@ -19,7 +19,7 @@ import {
19 19
     IconExitFullScreen,
20 20
     IconFeedback,
21 21
     IconFullScreen,
22
-    IconInviteMore,
22
+    IconParticipants,
23 23
     IconPresentation,
24 24
     IconRaisedHand,
25 25
     IconRec,
@@ -37,13 +37,16 @@ import { OverflowMenuItem } from '../../../base/toolbox/components';
37 37
 import { getLocalVideoTrack, toggleScreensharing } from '../../../base/tracks';
38 38
 import { isVpaasMeeting } from '../../../billing-counter/functions';
39 39
 import { ChatCounter, toggleChat } from '../../../chat';
40
-import { InviteMore } from '../../../conference';
41 40
 import { EmbedMeetingDialog } from '../../../embed-meeting';
42 41
 import { SharedDocumentButton } from '../../../etherpad';
43 42
 import { openFeedbackDialog } from '../../../feedback';
44
-import { beginAddPeople } from '../../../invite';
45 43
 import { openKeyboardShortcutsDialog } from '../../../keyboard-shortcuts';
46 44
 import { LocalRecordingInfoDialog } from '../../../local-recording';
45
+import {
46
+    close as closeParticipantsPane,
47
+    open as openParticipantsPane
48
+} from '../../../participants-pane/actions';
49
+import { getParticipantsPaneOpen } from '../../../participants-pane/functions';
47 50
 import {
48 51
     LiveStreamButton,
49 52
     RecordButton
@@ -179,6 +182,11 @@ type Props = {
179 182
      */
180 183
     _overflowMenuVisible: boolean,
181 184
 
185
+    /**
186
+     * Whether or not the participants pane is open.
187
+     */
188
+    _participantsPaneOpen: boolean,
189
+
182 190
     /**
183 191
      * Whether or not the local participant's hand is raised.
184 192
      */
@@ -240,11 +248,12 @@ class Toolbox extends Component<Props> {
240 248
 
241 249
         this._onShortcutToggleChat = this._onShortcutToggleChat.bind(this);
242 250
         this._onShortcutToggleFullScreen = this._onShortcutToggleFullScreen.bind(this);
251
+        this._onShortcutToggleParticipantsPane = this._onShortcutToggleParticipantsPane.bind(this);
243 252
         this._onShortcutToggleRaiseHand = this._onShortcutToggleRaiseHand.bind(this);
244 253
         this._onShortcutToggleScreenshare = this._onShortcutToggleScreenshare.bind(this);
245 254
         this._onShortcutToggleVideoQuality = this._onShortcutToggleVideoQuality.bind(this);
246 255
         this._onToolbarOpenFeedback = this._onToolbarOpenFeedback.bind(this);
247
-        this._onToolbarOpenInvite = this._onToolbarOpenInvite.bind(this);
256
+        this._onToolbarToggleParticipantsPane = this._onToolbarToggleParticipantsPane.bind(this);
248 257
         this._onToolbarOpenKeyboardShortcuts = this._onToolbarOpenKeyboardShortcuts.bind(this);
249 258
         this._onToolbarOpenSpeakerStats = this._onToolbarOpenSpeakerStats.bind(this);
250 259
         this._onToolbarOpenEmbedMeeting = this._onToolbarOpenEmbedMeeting.bind(this);
@@ -282,6 +291,11 @@ class Toolbox extends Component<Props> {
282 291
                 exec: this._onShortcutToggleScreenshare,
283 292
                 helpDescription: 'keyboardShortcuts.toggleScreensharing'
284 293
             },
294
+            this._shouldShowButton('participants-pane') && {
295
+                character: 'P',
296
+                exec: this._onShortcutToggleParticipantsPane,
297
+                helpDescription: 'keyboardShortcuts.toggleParticipantsPane'
298
+            },
285 299
             this._shouldShowButton('raisehand') && {
286 300
                 character: 'R',
287 301
                 exec: this._onShortcutToggleRaiseHand,
@@ -577,6 +591,25 @@ class Toolbox extends Component<Props> {
577 591
         this._doToggleChat();
578 592
     }
579 593
 
594
+    _onShortcutToggleParticipantsPane: () => void;
595
+
596
+    /**
597
+     * Creates an analytics keyboard shortcut event and dispatches an action for
598
+     * toggling the display of the participants pane.
599
+     *
600
+     * @private
601
+     * @returns {void}
602
+     */
603
+    _onShortcutToggleParticipantsPane() {
604
+        sendAnalytics(createShortcutEvent(
605
+            'toggle.participants-pane',
606
+            {
607
+                enable: !this.props._participantsPaneOpen
608
+            }));
609
+
610
+        this._onToolbarToggleParticipantsPane();
611
+    }
612
+
580 613
     _onShortcutToggleVideoQuality: () => void;
581 614
 
582 615
     /**
@@ -694,18 +727,22 @@ class Toolbox extends Component<Props> {
694 727
         this._doOpenFeedback();
695 728
     }
696 729
 
697
-    _onToolbarOpenInvite: () => void;
730
+    _onToolbarToggleParticipantsPane: () => void;
698 731
 
699 732
     /**
700
-     * Creates an analytics toolbar event and dispatches an action for opening
701
-     * the modal for inviting people directly into the conference.
733
+     * Dispatches an action for toggling the participants pane.
702 734
      *
703 735
      * @private
704 736
      * @returns {void}
705 737
      */
706
-    _onToolbarOpenInvite() {
707
-        sendAnalytics(createToolbarEvent('invite'));
708
-        this.props.dispatch(beginAddPeople());
738
+    _onToolbarToggleParticipantsPane() {
739
+        const { dispatch, _participantsPaneOpen } = this.props;
740
+
741
+        if (_participantsPaneOpen) {
742
+            dispatch(closeParticipantsPane());
743
+        } else {
744
+            dispatch(openParticipantsPane());
745
+        }
709 746
     }
710 747
 
711 748
     _onToolbarOpenKeyboardShortcuts: () => void;
@@ -1163,6 +1200,25 @@ class Toolbox extends Component<Props> {
1163 1200
                     text = { t(`toolbar.${_raisedHand ? 'lowerYourHand' : 'raiseYourHand'}`) } />);
1164 1201
         }
1165 1202
 
1203
+        if (this._shouldShowButton('participants-pane') || this._shouldShowButton('invite')) {
1204
+            buttons.has('participants-pane')
1205
+                ? mainMenuAdditionalButtons.push(
1206
+                    <ToolbarButton
1207
+                        accessibilityLabel = { t('toolbar.accessibilityLabel.participants') }
1208
+                        icon = { IconParticipants }
1209
+                        onClick = { this._onToolbarToggleParticipantsPane }
1210
+                        toggled = { this.props._participantsPaneOpen }
1211
+                        tooltip = { t('toolbar.participants') } />)
1212
+                : overflowMenuAdditionalButtons.push(
1213
+                    <OverflowMenuItem
1214
+                        accessibilityLabel = { t('toolbar.accessibilityLabel.participants') }
1215
+                        icon = { IconParticipants }
1216
+                        key = 'participants-pane'
1217
+                        onClick = { this._onToolbarToggleParticipantsPane }
1218
+                        text = { t('toolbar.participants') } />
1219
+                );
1220
+        }
1221
+
1166 1222
         if (this._shouldShowButton('tileview')) {
1167 1223
             buttons.has('tileview')
1168 1224
                 ? mainMenuAdditionalButtons.push(
@@ -1175,25 +1231,6 @@ class Toolbox extends Component<Props> {
1175 1231
                         showLabel = { true } />);
1176 1232
         }
1177 1233
 
1178
-        if (this._shouldShowButton('invite')) {
1179
-            buttons.has('invite')
1180
-                ? mainMenuAdditionalButtons.push(
1181
-                    <ToolbarButton
1182
-                        accessibilityLabel = { t('toolbar.accessibilityLabel.invite') }
1183
-                        icon = { IconInviteMore }
1184
-                        key = 'invite'
1185
-                        onClick = { this._onToolbarOpenInvite }
1186
-                        tooltip = { t('toolbar.invite') } />)
1187
-                : overflowMenuAdditionalButtons.push(
1188
-                    <OverflowMenuItem
1189
-                        accessibilityLabel = { t('toolbar.accessibilityLabel.invite') }
1190
-                        icon = { IconInviteMore }
1191
-                        key = 'invite'
1192
-                        onClick = { this._onToolbarOpenInvite }
1193
-                        text = { t('toolbar.invite') } />
1194
-                );
1195
-        }
1196
-
1197 1234
         return {
1198 1235
             mainMenuAdditionalButtons,
1199 1236
             overflowMenuAdditionalButtons
@@ -1254,7 +1291,6 @@ class Toolbox extends Component<Props> {
1254 1291
         return (
1255 1292
             <div className = { containerClassName }>
1256 1293
                 <div className = 'toolbox-content-wrapper'>
1257
-                    <InviteMore />
1258 1294
                     <div className = 'toolbox-content-items'>
1259 1295
                         { this._renderAudioButton() }
1260 1296
                         { this._renderVideoButton() }
@@ -1344,6 +1380,7 @@ function _mapStateToProps(state) {
1344 1380
         _localRecState: localRecordingStates,
1345 1381
         _locked: locked,
1346 1382
         _overflowMenuVisible: overflowMenuVisible,
1383
+        _participantsPaneOpen: getParticipantsPaneOpen(state),
1347 1384
         _raisedHand: localParticipant.raisedHand,
1348 1385
         _screensharing: (localVideo && localVideo.videoType === 'desktop') || isScreenAudioShared(state),
1349 1386
         _visible: isToolboxVisible(state),

+ 5
- 5
react/features/toolbox/functions.web.js 查看文件

@@ -25,23 +25,23 @@ export function getToolbarAdditionalButtons(width: number, isMobile: boolean): S
25 25
     switch (true) {
26 26
     case width >= WIDTH.FIT_9_ICONS: {
27 27
         buttons = isMobile
28
-            ? [ 'chat', 'raisehand', 'tileview', 'invite', 'overflow' ]
29
-            : [ 'desktop', 'chat', 'raisehand', 'tileview', 'invite', 'overflow' ];
28
+            ? [ 'chat', 'raisehand', 'tileview', 'participants-pane', 'overflow' ]
29
+            : [ 'desktop', 'chat', 'raisehand', 'tileview', 'participants-pane', 'overflow' ];
30 30
         break;
31 31
     }
32 32
 
33 33
     case width >= WIDTH.FIT_8_ICONS: {
34
-        buttons = [ 'desktop', 'chat', 'raisehand', 'invite', 'overflow' ];
34
+        buttons = [ 'desktop', 'chat', 'raisehand', 'participants-pane', 'overflow' ];
35 35
         break;
36 36
     }
37 37
 
38 38
     case width >= WIDTH.FIT_7_ICONS: {
39
-        buttons = [ 'desktop', 'chat', 'invite', 'overflow' ];
39
+        buttons = [ 'desktop', 'chat', 'participants-pane', 'overflow' ];
40 40
         break;
41 41
     }
42 42
 
43 43
     case width >= WIDTH.FIT_6_ICONS: {
44
-        buttons = [ 'chat', 'invite', 'overflow' ];
44
+        buttons = [ 'chat', 'participants-pane', 'overflow' ];
45 45
         break;
46 46
     }
47 47
 

+ 1
- 1
react/features/video-menu/components/AbstractMuteEveryoneDialog.js 查看文件

@@ -84,7 +84,7 @@ export default class AbstractMuteEveryoneDialog<P: Props> extends AbstractMuteRe
84 84
  * @returns {Props}
85 85
  */
86 86
 export function abstractMapStateToProps(state: Object, ownProps: Props) {
87
-    const { exclude, t } = ownProps;
87
+    const { exclude = [], t } = ownProps;
88 88
 
89 89
     const whom = exclude
90 90
         // eslint-disable-next-line no-confusing-arrow

Loading…
取消
儲存