瀏覽代碼

feat(participants-pane) implement participants pane

master
Gabriel Imre 4 年之前
父節點
當前提交
d014a52ab3
No account linked to committer's email address
共有 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 查看文件

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
 $defaultSemiDarkColor: #ACACAC;
30
 $defaultSemiDarkColor: #ACACAC;
31
 $defaultDarkColor: #2b3d5c;
31
 $defaultDarkColor: #2b3d5c;
32
 $defaultWarningColor: rgb(215, 121, 118);
32
 $defaultWarningColor: rgb(215, 121, 118);
33
+$participantsPaneBgColor: #141414;
33
 $presence-available: rgb(110, 176, 5);
34
 $presence-available: rgb(110, 176, 5);
34
 $presence-away: rgb(250, 201, 20);
35
 $presence-away: rgb(250, 201, 20);
35
 $presence-busy: rgb(233, 0, 27);
36
 $presence-busy: rgb(233, 0, 27);

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

1
 #videoconference_page {
1
 #videoconference_page {
2
     min-height: 100%;
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
 #videospace {
13
 #videospace {

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

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

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

16
         display: flex;
16
         display: flex;
17
         flex-direction: column;
17
         flex-direction: column;
18
         height: 100%;
18
         height: 100%;
19
-        width: 100vw;
19
+        width: 100%;
20
     }
20
     }
21
 
21
 
22
     .filmstrip__videos .videocontainer {
22
     .filmstrip__videos .videocontainer {
50
             &.shift-right {
50
             &.shift-right {
51
                 margin-left: $sidebarWidth;
51
                 margin-left: $sidebarWidth;
52
                 width: calc(100% - #{$sidebarWidth});
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
 @import 'responsive';
104
 @import 'responsive';
105
 @import 'connection-status';
105
 @import 'connection-status';
106
 @import 'drawer';
106
 @import 'drawer';
107
+@import 'participants-pane';
107
 
108
 
108
 /* Modules END */
109
 /* Modules END */

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

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

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

19
 import {
19
 import {
20
     updateKnownLargeVideoResolution
20
     updateKnownLargeVideoResolution
21
 } from '../../../react/features/large-video/actions';
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
 import { PresenceLabel } from '../../../react/features/presence-status';
24
 import { PresenceLabel } from '../../../react/features/presence-status';
23
 import { shouldDisplayTileView } from '../../../react/features/video-layout';
25
 import { shouldDisplayTileView } from '../../../react/features/video-layout';
24
 /* eslint-enable no-unused-vars */
26
 /* eslint-enable no-unused-vars */
366
         }
368
         }
367
 
369
 
368
         let widthToUse = this.preferredWidth || window.innerWidth;
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
         if (isOpen && window.innerWidth > 580) {
379
         if (isOpen && window.innerWidth > 580) {
372
             /**
380
             /**

+ 1
- 1
package.json 查看文件

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

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

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

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

20
 } from './constants';
20
 } from './constants';
21
 import logger from './logger';
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
  * Attach a set of local tracks to a conference.
40
  * Attach a set of local tracks to a conference.
25
  *
41
  *
123
 export function forEachConference(
139
 export function forEachConference(
124
         stateful: Function | Object,
140
         stateful: Function | Object,
125
         predicate: (Object, URL) => boolean) {
141
         predicate: (Object, URL) => boolean) {
126
-    const state = toState(stateful)['features/base/conference'];
142
+    const state = getConferenceState(toState(stateful));
127
 
143
 
128
     for (const v of Object.values(state)) {
144
     for (const v of Object.values(state)) {
129
         // Does the value of the base/conference's property look like a
145
         // Does the value of the base/conference's property look like a
157
     const state = toState(stateful);
173
     const state = toState(stateful);
158
     const { callee } = state['features/base/jwt'];
174
     const { callee } = state['features/base/jwt'];
159
     const { callDisplayName } = state['features/base/config'];
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
     return pendingSubjectChange
178
     return pendingSubjectChange
163
         || subject
179
         || subject
174
  * @returns {string} - The name of the conference formatted for the title.
190
  * @returns {string} - The name of the conference formatted for the title.
175
  */
191
  */
176
 export function getConferenceNameForTitle(stateful: Function | Object) {
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
 */
202
 */
187
 export function getConferenceTimestamp(stateful: Function | Object): number {
203
 export function getConferenceTimestamp(stateful: Function | Object): number {
188
     const state = toState(stateful);
204
     const state = toState(stateful);
189
-    const { conferenceTimestamp } = state['features/base/conference'];
205
+    const { conferenceTimestamp } = getConferenceState(state);
190
 
206
 
191
     return conferenceTimestamp;
207
     return conferenceTimestamp;
192
 }
208
 }
203
  */
219
  */
204
 export function getCurrentConference(stateful: Function | Object) {
220
 export function getCurrentConference(stateful: Function | Object) {
205
     const { conference, joining, leaving, membersOnly, passwordRequired }
221
     const { conference, joining, leaving, membersOnly, passwordRequired }
206
-        = toState(stateful)['features/base/conference'];
222
+        = getConferenceState(toState(stateful));
207
 
223
 
208
     // There is a precedence
224
     // There is a precedence
209
     if (conference) {
225
     if (conference) {
220
  * @returns {string}
236
  * @returns {string}
221
  */
237
  */
222
 export function getRoomName(state: Object): string {
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
     'microphone', 'camera', 'closedcaptions', 'desktop', 'embedmeeting', 'fullscreen',
17
     'microphone', 'camera', 'closedcaptions', 'desktop', 'embedmeeting', 'fullscreen',
18
     'fodeviceselection', 'hangup', 'profile', 'chat', 'recording',
18
     'fodeviceselection', 'hangup', 'profile', 'chat', 'recording',
19
     'livestreaming', 'etherpad', 'sharedvideo', 'shareaudio', 'settings', 'raisehand',
19
     'livestreaming', 'etherpad', 'sharedvideo', 'shareaudio', 'settings', 'raisehand',
20
-    'videoquality', 'filmstrip', 'invite', 'feedback', 'stats', 'shortcuts',
20
+    'videoquality', 'filmstrip', 'participants-pane', 'feedback', 'stats', 'shortcuts',
21
     'tileview', 'select-background', 'download', 'help', 'mute-everyone', 'mute-video-everyone',
21
     'tileview', 'select-background', 'download', 'help', 'mute-everyone', 'mute-video-everyone',
22
     'security', 'toggle-camera'
22
     'security', 'toggle-camera'
23
 ];
23
 ];

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

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

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

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

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

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
-<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
 <g clip-path="url(#clip0)">
2
 <g clip-path="url(#clip0)">
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" />
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
 <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" />
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 查看文件

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

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

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
 import type { Dispatch } from 'redux';
3
 import type { Dispatch } from 'redux';
4
 
4
 
5
 import { CHAT_SIZE } from '../../chat/constants';
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
 import { CLIENT_RESIZED, SET_ASPECT_RATIO, SET_REDUCED_UI } from './actionTypes';
9
 import { CLIENT_RESIZED, SET_ASPECT_RATIO, SET_REDUCED_UI } from './actionTypes';
8
 import { ASPECT_RATIO_NARROW, ASPECT_RATIO_WIDE } from './constants';
10
 import { ASPECT_RATIO_NARROW, ASPECT_RATIO_WIDE } from './constants';
28
 export function clientResized(clientWidth: number, clientHeight: number) {
30
 export function clientResized(clientWidth: number, clientHeight: number) {
29
     return (dispatch: Dispatch<any>, getState: Function) => {
31
     return (dispatch: Dispatch<any>, getState: Function) => {
30
         const state = getState();
32
         const state = getState();
31
-        const { isOpen } = state['features/chat'];
33
+        const { isOpen: isChatOpen } = state['features/chat'];
34
+        const isParticipantsPaneOpen = getParticipantsPaneOpen(state);
32
         let availableWidth = clientWidth;
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
         return dispatch({
47
         return dispatch({

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

12
 import loadEffects from './loadEffects';
12
 import loadEffects from './loadEffects';
13
 import logger from './logger';
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
  * Creates a local video track for presenter. The constraints are computed based
88
  * Creates a local video track for presenter. The constraints are computed based
17
  * on the height of the desktop that is being shared.
89
  * on the height of the desktop that is being shared.
311
  * @returns {Object}
383
  * @returns {Object}
312
  */
384
  */
313
 export function getLocalJitsiVideoTrack(state) {
385
 export function getLocalJitsiVideoTrack(state) {
314
-    const track = getLocalVideoTrack(state['features/base/tracks']);
386
+    const track = getLocalVideoTrack(getTrackState(state));
315
 
387
 
316
     return track?.jitsiTrack;
388
     return track?.jitsiTrack;
317
 }
389
 }
323
  * @returns {Object}
395
  * @returns {Object}
324
  */
396
  */
325
 export function getLocalJitsiAudioTrack(state) {
397
 export function getLocalJitsiAudioTrack(state) {
326
-    const track = getLocalAudioTrack(state['features/base/tracks']);
398
+    const track = getLocalAudioTrack(getTrackState(state));
327
 
399
 
328
     return track?.jitsiTrack;
400
     return track?.jitsiTrack;
329
 }
401
 }
413
  * @returns {boolean}
485
  * @returns {boolean}
414
  */
486
  */
415
 export function isLocalVideoTrackDesktop(state) {
487
 export function isLocalVideoTrackDesktop(state) {
416
-    const videoTrack = getLocalVideoTrack(state['features/base/tracks']);
488
+    const videoTrack = getLocalVideoTrack(getTrackState(state));
417
 
489
 
418
     return videoTrack && videoTrack.videoType === VIDEO_TYPE.DESKTOP;
490
     return videoTrack && videoTrack.videoType === VIDEO_TYPE.DESKTOP;
419
 }
491
 }

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

14
 import { CalleeInfoContainer } from '../../../invite';
14
 import { CalleeInfoContainer } from '../../../invite';
15
 import { LargeVideo } from '../../../large-video';
15
 import { LargeVideo } from '../../../large-video';
16
 import { KnockingParticipantList, LobbyScreen } from '../../../lobby';
16
 import { KnockingParticipantList, LobbyScreen } from '../../../lobby';
17
+import { ParticipantsPane } from '../../../participants-pane/components';
18
+import { getParticipantsPaneOpen } from '../../../participants-pane/functions';
17
 import { Prejoin, isPrejoinPageVisible } from '../../../prejoin';
19
 import { Prejoin, isPrejoinPageVisible } from '../../../prejoin';
18
 import { fullScreenChanged, showToolbox } from '../../../toolbox/actions.web';
20
 import { fullScreenChanged, showToolbox } from '../../../toolbox/actions.web';
19
 import { Toolbox } from '../../../toolbox/components/web';
21
 import { Toolbox } from '../../../toolbox/components/web';
72
      */
74
      */
73
     _isLobbyScreenVisible: boolean,
75
     _isLobbyScreenVisible: boolean,
74
 
76
 
77
+    /**
78
+     * If participants pane is visible or not.
79
+     */
80
+    _isParticipantsPaneVisible: boolean,
81
+
75
     /**
82
     /**
76
      * The CSS class to apply to the root of {@link Conference} to modify the
83
      * The CSS class to apply to the root of {@link Conference} to modify the
77
      * application layout.
84
      * application layout.
179
     render() {
186
     render() {
180
         const {
187
         const {
181
             _isLobbyScreenVisible,
188
             _isLobbyScreenVisible,
189
+            _isParticipantsPaneVisible,
182
             _layoutClassName,
190
             _layoutClassName,
183
             _showPrejoin
191
             _showPrejoin
184
         } = this.props;
192
         } = this.props;
185
 
193
 
186
         return (
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
             </div>
221
             </div>
210
         );
222
         );
211
     }
223
     }
297
         ...abstractMapStateToProps(state),
309
         ...abstractMapStateToProps(state),
298
         _backgroundAlpha: state['features/base/config'].backgroundAlpha,
310
         _backgroundAlpha: state['features/base/config'].backgroundAlpha,
299
         _isLobbyScreenVisible: state['features/base/dialog']?.component === LobbyScreen,
311
         _isLobbyScreenVisible: state['features/base/dialog']?.component === LobbyScreen,
312
+        _isParticipantsPaneVisible: getParticipantsPaneOpen(state),
300
         _layoutClassName: LAYOUT_CLASSNAMES[getCurrentLayout(state)],
313
         _layoutClassName: LAYOUT_CLASSNAMES[getCurrentLayout(state)],
301
         _roomName: getConferenceNameForTitle(state),
314
         _roomName: getConferenceNameForTitle(state),
302
         _showPrejoin: isPrejoinPageVisible(state)
315
         _showPrejoin: isPrejoinPageVisible(state)

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

3
 import { StateListenerRegistry, equals } from '../base/redux';
3
 import { StateListenerRegistry, equals } from '../base/redux';
4
 import { clientResized } from '../base/responsive-ui';
4
 import { clientResized } from '../base/responsive-ui';
5
 import { setFilmstripVisible } from '../filmstrip/actions';
5
 import { setFilmstripVisible } from '../filmstrip/actions';
6
+import { getParticipantsPaneOpen } from '../participants-pane/functions';
6
 import { setOverflowDrawer } from '../toolbox/actions.web';
7
 import { setOverflowDrawer } from '../toolbox/actions.web';
7
 import { getCurrentLayout, getTileViewGridDimensions, shouldDisplayTileView, LAYOUTS } from '../video-layout';
8
 import { getCurrentLayout, getTileViewGridDimensions, shouldDisplayTileView, LAYOUTS } from '../video-layout';
8
 
9
 
92
         store.dispatch(clientResized(innerWidth, innerHeight));
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
  * Listens for changes in the client width to determine whether the overflow menu(s) should be displayed as drawers.
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
 
4
 
5
 import { isLocalParticipantModerator } from '../../base/participants';
5
 import { isLocalParticipantModerator } from '../../base/participants';
6
 import { setKnockingParticipantApproval } from '../actions';
6
 import { setKnockingParticipantApproval } from '../actions';
7
+import { getLobbyState } from '../functions';
7
 
8
 
8
 export type Props = {
9
 export type Props = {
9
 
10
 
66
  * @returns {Props}
67
  * @returns {Props}
67
  */
68
  */
68
 export function mapStateToProps(state: Object): $Shape<Props> {
69
 export function mapStateToProps(state: Object): $Shape<Props> {
69
-    const { knockingParticipants, lobbyEnabled } = state['features/lobby'];
70
+    const { knockingParticipants, lobbyEnabled } = getLobbyState(state);
70
 
71
 
71
     return {
72
     return {
72
         _participants: knockingParticipants,
73
         _participants: knockingParticipants,

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

19
     IconExitFullScreen,
19
     IconExitFullScreen,
20
     IconFeedback,
20
     IconFeedback,
21
     IconFullScreen,
21
     IconFullScreen,
22
-    IconInviteMore,
22
+    IconParticipants,
23
     IconPresentation,
23
     IconPresentation,
24
     IconRaisedHand,
24
     IconRaisedHand,
25
     IconRec,
25
     IconRec,
37
 import { getLocalVideoTrack, toggleScreensharing } from '../../../base/tracks';
37
 import { getLocalVideoTrack, toggleScreensharing } from '../../../base/tracks';
38
 import { isVpaasMeeting } from '../../../billing-counter/functions';
38
 import { isVpaasMeeting } from '../../../billing-counter/functions';
39
 import { ChatCounter, toggleChat } from '../../../chat';
39
 import { ChatCounter, toggleChat } from '../../../chat';
40
-import { InviteMore } from '../../../conference';
41
 import { EmbedMeetingDialog } from '../../../embed-meeting';
40
 import { EmbedMeetingDialog } from '../../../embed-meeting';
42
 import { SharedDocumentButton } from '../../../etherpad';
41
 import { SharedDocumentButton } from '../../../etherpad';
43
 import { openFeedbackDialog } from '../../../feedback';
42
 import { openFeedbackDialog } from '../../../feedback';
44
-import { beginAddPeople } from '../../../invite';
45
 import { openKeyboardShortcutsDialog } from '../../../keyboard-shortcuts';
43
 import { openKeyboardShortcutsDialog } from '../../../keyboard-shortcuts';
46
 import { LocalRecordingInfoDialog } from '../../../local-recording';
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
 import {
50
 import {
48
     LiveStreamButton,
51
     LiveStreamButton,
49
     RecordButton
52
     RecordButton
179
      */
182
      */
180
     _overflowMenuVisible: boolean,
183
     _overflowMenuVisible: boolean,
181
 
184
 
185
+    /**
186
+     * Whether or not the participants pane is open.
187
+     */
188
+    _participantsPaneOpen: boolean,
189
+
182
     /**
190
     /**
183
      * Whether or not the local participant's hand is raised.
191
      * Whether or not the local participant's hand is raised.
184
      */
192
      */
240
 
248
 
241
         this._onShortcutToggleChat = this._onShortcutToggleChat.bind(this);
249
         this._onShortcutToggleChat = this._onShortcutToggleChat.bind(this);
242
         this._onShortcutToggleFullScreen = this._onShortcutToggleFullScreen.bind(this);
250
         this._onShortcutToggleFullScreen = this._onShortcutToggleFullScreen.bind(this);
251
+        this._onShortcutToggleParticipantsPane = this._onShortcutToggleParticipantsPane.bind(this);
243
         this._onShortcutToggleRaiseHand = this._onShortcutToggleRaiseHand.bind(this);
252
         this._onShortcutToggleRaiseHand = this._onShortcutToggleRaiseHand.bind(this);
244
         this._onShortcutToggleScreenshare = this._onShortcutToggleScreenshare.bind(this);
253
         this._onShortcutToggleScreenshare = this._onShortcutToggleScreenshare.bind(this);
245
         this._onShortcutToggleVideoQuality = this._onShortcutToggleVideoQuality.bind(this);
254
         this._onShortcutToggleVideoQuality = this._onShortcutToggleVideoQuality.bind(this);
246
         this._onToolbarOpenFeedback = this._onToolbarOpenFeedback.bind(this);
255
         this._onToolbarOpenFeedback = this._onToolbarOpenFeedback.bind(this);
247
-        this._onToolbarOpenInvite = this._onToolbarOpenInvite.bind(this);
256
+        this._onToolbarToggleParticipantsPane = this._onToolbarToggleParticipantsPane.bind(this);
248
         this._onToolbarOpenKeyboardShortcuts = this._onToolbarOpenKeyboardShortcuts.bind(this);
257
         this._onToolbarOpenKeyboardShortcuts = this._onToolbarOpenKeyboardShortcuts.bind(this);
249
         this._onToolbarOpenSpeakerStats = this._onToolbarOpenSpeakerStats.bind(this);
258
         this._onToolbarOpenSpeakerStats = this._onToolbarOpenSpeakerStats.bind(this);
250
         this._onToolbarOpenEmbedMeeting = this._onToolbarOpenEmbedMeeting.bind(this);
259
         this._onToolbarOpenEmbedMeeting = this._onToolbarOpenEmbedMeeting.bind(this);
282
                 exec: this._onShortcutToggleScreenshare,
291
                 exec: this._onShortcutToggleScreenshare,
283
                 helpDescription: 'keyboardShortcuts.toggleScreensharing'
292
                 helpDescription: 'keyboardShortcuts.toggleScreensharing'
284
             },
293
             },
294
+            this._shouldShowButton('participants-pane') && {
295
+                character: 'P',
296
+                exec: this._onShortcutToggleParticipantsPane,
297
+                helpDescription: 'keyboardShortcuts.toggleParticipantsPane'
298
+            },
285
             this._shouldShowButton('raisehand') && {
299
             this._shouldShowButton('raisehand') && {
286
                 character: 'R',
300
                 character: 'R',
287
                 exec: this._onShortcutToggleRaiseHand,
301
                 exec: this._onShortcutToggleRaiseHand,
577
         this._doToggleChat();
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
     _onShortcutToggleVideoQuality: () => void;
613
     _onShortcutToggleVideoQuality: () => void;
581
 
614
 
582
     /**
615
     /**
694
         this._doOpenFeedback();
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
      * @private
735
      * @private
704
      * @returns {void}
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
     _onToolbarOpenKeyboardShortcuts: () => void;
748
     _onToolbarOpenKeyboardShortcuts: () => void;
1163
                     text = { t(`toolbar.${_raisedHand ? 'lowerYourHand' : 'raiseYourHand'}`) } />);
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
         if (this._shouldShowButton('tileview')) {
1222
         if (this._shouldShowButton('tileview')) {
1167
             buttons.has('tileview')
1223
             buttons.has('tileview')
1168
                 ? mainMenuAdditionalButtons.push(
1224
                 ? mainMenuAdditionalButtons.push(
1175
                         showLabel = { true } />);
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
         return {
1234
         return {
1198
             mainMenuAdditionalButtons,
1235
             mainMenuAdditionalButtons,
1199
             overflowMenuAdditionalButtons
1236
             overflowMenuAdditionalButtons
1254
         return (
1291
         return (
1255
             <div className = { containerClassName }>
1292
             <div className = { containerClassName }>
1256
                 <div className = 'toolbox-content-wrapper'>
1293
                 <div className = 'toolbox-content-wrapper'>
1257
-                    <InviteMore />
1258
                     <div className = 'toolbox-content-items'>
1294
                     <div className = 'toolbox-content-items'>
1259
                         { this._renderAudioButton() }
1295
                         { this._renderAudioButton() }
1260
                         { this._renderVideoButton() }
1296
                         { this._renderVideoButton() }
1344
         _localRecState: localRecordingStates,
1380
         _localRecState: localRecordingStates,
1345
         _locked: locked,
1381
         _locked: locked,
1346
         _overflowMenuVisible: overflowMenuVisible,
1382
         _overflowMenuVisible: overflowMenuVisible,
1383
+        _participantsPaneOpen: getParticipantsPaneOpen(state),
1347
         _raisedHand: localParticipant.raisedHand,
1384
         _raisedHand: localParticipant.raisedHand,
1348
         _screensharing: (localVideo && localVideo.videoType === 'desktop') || isScreenAudioShared(state),
1385
         _screensharing: (localVideo && localVideo.videoType === 'desktop') || isScreenAudioShared(state),
1349
         _visible: isToolboxVisible(state),
1386
         _visible: isToolboxVisible(state),

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

25
     switch (true) {
25
     switch (true) {
26
     case width >= WIDTH.FIT_9_ICONS: {
26
     case width >= WIDTH.FIT_9_ICONS: {
27
         buttons = isMobile
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
         break;
30
         break;
31
     }
31
     }
32
 
32
 
33
     case width >= WIDTH.FIT_8_ICONS: {
33
     case width >= WIDTH.FIT_8_ICONS: {
34
-        buttons = [ 'desktop', 'chat', 'raisehand', 'invite', 'overflow' ];
34
+        buttons = [ 'desktop', 'chat', 'raisehand', 'participants-pane', 'overflow' ];
35
         break;
35
         break;
36
     }
36
     }
37
 
37
 
38
     case width >= WIDTH.FIT_7_ICONS: {
38
     case width >= WIDTH.FIT_7_ICONS: {
39
-        buttons = [ 'desktop', 'chat', 'invite', 'overflow' ];
39
+        buttons = [ 'desktop', 'chat', 'participants-pane', 'overflow' ];
40
         break;
40
         break;
41
     }
41
     }
42
 
42
 
43
     case width >= WIDTH.FIT_6_ICONS: {
43
     case width >= WIDTH.FIT_6_ICONS: {
44
-        buttons = [ 'chat', 'invite', 'overflow' ];
44
+        buttons = [ 'chat', 'participants-pane', 'overflow' ];
45
         break;
45
         break;
46
     }
46
     }
47
 
47
 

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

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

Loading…
取消
儲存