Browse Source

feat(av-moderation) Updated Advanced moderation (#9875)

Co-authored-by: Vlad Piersec <vlad.piersec@8x8.com>
master
robertpin 3 years ago
parent
commit
1dc8bfa631
No account linked to committer's email address
55 changed files with 1432 additions and 577 deletions
  1. 1
    1
      css/_participants-pane.scss
  2. 1
    0
      css/main.scss
  3. 19
    0
      css/modals/mute/_mute-dialog.scss
  4. 0
    0
      eslint
  5. 29
    23
      lang/main.json
  6. 1
    0
      package.json
  7. 24
    6
      react/features/av-moderation/actionTypes.js
  8. 47
    13
      react/features/av-moderation/actions.js
  9. 0
    38
      react/features/av-moderation/components/AudioModerationNotifications.js
  10. 20
    40
      react/features/av-moderation/middleware.js
  11. 1
    1
      react/features/base/icons/svg/camera-empty-disabled.svg
  12. 23
    0
      react/features/base/media/middleware.js
  13. 12
    0
      react/features/base/participants/actionTypes.js
  14. 20
    8
      react/features/base/participants/actions.js
  15. 30
    2
      react/features/base/participants/functions.js
  16. 56
    7
      react/features/base/participants/middleware.js
  17. 9
    1
      react/features/base/participants/reducer.js
  18. 8
    4
      react/features/base/tracks/middleware.js
  19. 22
    0
      react/features/chat/actions.web.js
  20. 0
    2
      react/features/conference/components/web/Conference.js
  21. 10
    0
      react/features/notifications/actionTypes.js
  22. 14
    0
      react/features/notifications/actions.js
  23. 16
    23
      react/features/notifications/middleware.js
  24. 9
    0
      react/features/notifications/reducer.js
  25. 71
    21
      react/features/participants-pane/components/FooterContextMenu.js
  26. 23
    13
      react/features/participants-pane/components/web/LobbyParticipantItem.js
  27. 47
    0
      react/features/participants-pane/components/web/LobbyParticipantItems.js
  28. 0
    72
      react/features/participants-pane/components/web/LobbyParticipantList.js
  29. 134
    0
      react/features/participants-pane/components/web/LobbyParticipants.js
  30. 216
    105
      react/features/participants-pane/components/web/MeetingParticipantContextMenu.js
  31. 50
    32
      react/features/participants-pane/components/web/MeetingParticipantItem.js
  32. 103
    0
      react/features/participants-pane/components/web/MeetingParticipantItems.js
  33. 53
    46
      react/features/participants-pane/components/web/MeetingParticipants.js
  34. 23
    5
      react/features/participants-pane/components/web/ParticipantItem.js
  35. 39
    5
      react/features/participants-pane/components/web/ParticipantsPane.js
  36. 0
    2
      react/features/participants-pane/components/web/index.js
  37. 27
    1
      react/features/participants-pane/components/web/styled.js
  38. 1
    0
      react/features/participants-pane/constants.js
  39. 24
    2
      react/features/participants-pane/functions.js
  40. 46
    0
      react/features/participants-pane/hooks.js
  41. 8
    3
      react/features/talk-while-muted/middleware.js
  42. 11
    0
      react/features/toolbox/functions.web.js
  43. 20
    1
      react/features/video-menu/components/AbstractGrantModeratorDialog.js
  44. 2
    4
      react/features/video-menu/components/AbstractMuteButton.js
  45. 41
    4
      react/features/video-menu/components/AbstractMuteEveryoneDialog.js
  46. 44
    5
      react/features/video-menu/components/AbstractMuteEveryonesVideoDialog.js
  47. 2
    2
      react/features/video-menu/components/AbstractMuteRemoteParticipantDialog.js
  48. 2
    2
      react/features/video-menu/components/AbstractMuteRemoteParticipantsVideoDialog.js
  49. 0
    32
      react/features/video-menu/components/native/MuteRemoteParticipantDialog.js
  50. 0
    1
      react/features/video-menu/components/native/index.js
  51. 3
    3
      react/features/video-menu/components/web/GrantModeratorDialog.js
  52. 36
    3
      react/features/video-menu/components/web/MuteEveryoneDialog.js
  53. 34
    2
      react/features/video-menu/components/web/MuteEveryonesVideoDialog.js
  54. 0
    41
      react/features/video-menu/components/web/MuteRemoteParticipantDialog.js
  55. 0
    1
      react/features/video-menu/components/web/index.js

+ 1
- 1
css/_participants-pane.scss View File

@@ -29,7 +29,7 @@
29 29
     margin: 8px 16px 8px 0;
30 30
 }
31 31
 
32
-@media (max-width: 375px) {
32
+@media (max-width: 580px) {
33 33
     .participants_pane {
34 34
         height: 100vh;
35 35
         height: -webkit-fill-available;

+ 1
- 0
css/main.scss View File

@@ -98,6 +98,7 @@ $flagsImagePath: "../images/";
98 98
 @import 'country-picker';
99 99
 @import 'modals/invite/invite_more';
100 100
 @import 'modals/security/security';
101
+@import 'modals/mute/mute-dialog';
101 102
 @import 'e2ee';
102 103
 @import 'responsive';
103 104
 @import 'drawer';

+ 19
- 0
css/modals/mute/_mute-dialog.scss View File

@@ -0,0 +1,19 @@
1
+.mute-dialog {
2
+	.separator-line {
3
+        margin: 24px 0 24px -20px;
4
+        padding: 0 20px;
5
+        width: 100%;
6
+        height: 1px;
7
+        background: #5E6D7A;
8
+    }
9
+
10
+	.control-row {
11
+        display: flex;
12
+        justify-content: space-between;
13
+        margin-top: 15px;
14
+
15
+        label {
16
+            font-size: 14px;
17
+        }
18
+    }
19
+}

+ 0
- 0
eslint View File


+ 29
- 23
lang/main.json View File

@@ -216,8 +216,8 @@
216 216
         "embedMeeting": "Embed meeting",
217 217
         "error": "Error",
218 218
         "gracefulShutdown": "Our service is currently down for maintenance. Please try again later.",
219
-        "grantModeratorDialog": "Are you sure you want to make this participant a moderator?",
220
-        "grantModeratorTitle": "Grant moderator",
219
+        "grantModeratorDialog": "Are you sure you want to grant moderator rights to {{participantName}}?",
220
+        "grantModeratorTitle": "Grant moderator rights",
221 221
         "hideShareAudioHelper": "Don't show this dialog again",
222 222
         "IamHost": "I am the host",
223 223
         "incorrectRoomLockPassword": "Incorrect password",
@@ -247,15 +247,19 @@
247 247
         "micPermissionDeniedError": "You have not granted permission to use your microphone. You can still join the conference but others won't hear you. Use the camera button in the address bar to fix this.",
248 248
         "micTimeoutError": "Could not start audio source. Timeout occured!",
249 249
         "micUnknownError": "Cannot use microphone for an unknown reason.",
250
+        "moderationAudioLabel": "Allow attendees to unmute themselves",
251
+        "moderationVideoLabel": "Allow attendees to start their video",
250 252
         "muteEveryoneElseDialog": "Once muted, you won't be able to unmute them, but they can unmute themselves at any time.",
251 253
         "muteEveryoneElseTitle": "Mute everyone except {{whom}}?",
252
-        "muteEveryoneDialog": "Are you sure you want to mute everyone? You won't be able to unmute them, but they can unmute themselves at any time.",
254
+        "muteEveryoneDialog": "The participants can unmute themselves at any time.",
255
+        "muteEveryoneDialogModerationOn": "The participants can send a request to speak at any time.",
253 256
         "muteEveryoneTitle": "Mute everyone?",
254 257
         "muteEveryoneElsesVideoDialog": "Once the camera is disabled, you won't be able to turn it back on, but they can turn it back on at any time.",
255
-        "muteEveryoneElsesVideoTitle": "Disable everyone's camera except {{whom}}?",
256
-        "muteEveryonesVideoDialog": "Are you sure you want to disable everyone's camera? You won't be able to turn it back on, but they can turn it back on at any time.",
258
+        "muteEveryoneElsesVideoTitle": "Stop everyone's video except {{whom}}?",
259
+        "muteEveryonesVideoDialog": "The participants can turn on their video at any time.",
260
+        "muteEveryonesVideoDialogModerationOn": "The participants can send a request to turn on their video at any time.",
257 261
         "muteEveryonesVideoDialogOk": "Disable",
258
-        "muteEveryonesVideoTitle": "Disable everyone's camera?",
262
+        "muteEveryonesVideoTitle": "Stop everyone's video?",
259 263
         "muteEveryoneSelf": "yourself",
260 264
         "muteEveryoneStartMuted": "Everyone starts muted from now on",
261 265
         "muteParticipantBody": "You won't be able to unmute them, but they can unmute themselves at any time.",
@@ -263,7 +267,7 @@
263 267
         "muteParticipantDialog": "Are you sure you want to mute this participant? You won't be able to unmute them, but they can unmute themselves at any time.",
264 268
         "muteParticipantsVideoDialog": "Are you sure you want to turn off this participant's camera? You won't be able to turn the camera back on, but they can turn it back on at any time.",
265 269
         "muteParticipantTitle": "Mute this participant?",
266
-        "muteParticipantsVideoButton": "Disable camera",
270
+        "muteParticipantsVideoButton": "Stop camera",
267 271
         "muteParticipantsVideoTitle": "Disable camera of this participant?",
268 272
         "muteParticipantsVideoBody": "You won't be able to turn the camera back on, but they can turn it back on at any time.",
269 273
         "noDropboxToken": "No valid Dropbox token",
@@ -542,29 +546,30 @@
542 546
     "lockRoomPasswordUppercase": "Password",
543 547
     "me": "me",
544 548
     "notify": {
549
+        "allowAction": "Allow",
550
+        "allowedUnmute": "You can unmute your microphone, start your camera or share your screen.",
545 551
         "connectedOneMember": "{{name}} joined the meeting",
546 552
         "connectedThreePlusMembers": "{{name}} and many others joined the meeting",
547 553
         "connectedTwoMembers": "{{first}} and {{second}} joined the meeting",
548 554
         "disconnected": "disconnected",
549 555
         "focus": "Conference focus",
550 556
         "focusFail": "{{component}} not available - retry in {{ms}} sec",
551
-        "grantedTo": "Moderator rights granted to {{to}}!",
552
-        "hostAskedUnmute": "The host would like you to unmute",
557
+        "hostAskedUnmute": "The moderator would like you to speak",
553 558
         "invitedOneMember": "{{name}} has been invited",
554 559
         "invitedThreePlusMembers": "{{name}} and {{count}} others have been invited",
555 560
         "invitedTwoMembers": "{{first}} and {{second}} have been invited",
556 561
         "kickParticipant": "{{kicked}} was kicked by {{kicker}}",
557 562
         "me": "Me",
558
-        "moderator": "Moderator rights granted!",
563
+        "moderator": "You're now a moderator",
559 564
         "muted": "You have started the conversation muted.",
560 565
         "mutedTitle": "You're muted!",
561
-        "mutedRemotelyTitle": "You have been muted by {{participantDisplayName}}!",
566
+        "mutedRemotelyTitle": "You've been muted by the moderator",
562 567
         "mutedRemotelyDescription": "You can always unmute when you're ready to speak. Mute back when you're done to keep noise away from the meeting.",
563
-        "videoMutedRemotelyTitle": "Your camera has been disabled by {{participantDisplayName}}!",
568
+        "videoMutedRemotelyTitle": "Your camera has been turned off by the moderator",
564 569
         "videoMutedRemotelyDescription": "You can always turn it on again.",
565 570
         "passwordRemovedRemotely": "$t(lockRoomPasswordUppercase) removed by another participant",
566 571
         "passwordSetRemotely": "$t(lockRoomPasswordUppercase) set by another participant",
567
-        "raisedHand": "{{name}} would like to speak.",
572
+        "raisedHand": "Would like to speak.",
568 573
         "screenShareNoAudio": " Share audio box was not checked in the window selection screen.",
569 574
         "screenShareNoAudioTitle": "Couldn't share system audio!",
570 575
         "somebody": "Somebody",
@@ -580,12 +585,12 @@
580 585
         "oldElectronClientDescription1": "You appear to be using an old version of the Jitsi Meet client which has known security vulnerabilities. Please make sure you update to our ",
581 586
         "oldElectronClientDescription2": "latest build",
582 587
         "oldElectronClientDescription3": " now!",
583
-        "moderationInEffectDescription": "Please raise hand if you want to speak",
584
-        "moderationInEffectCSDescription": "Please raise hand if you want to share your video",
585
-        "moderationInEffectVideoDescription": "Please raise your hand if you want your video to be visible",
586
-        "moderationInEffectTitle": "The microphone is muted by the moderator",
587
-        "moderationInEffectCSTitle": "Content sharing is disabled by moderator",
588
-        "moderationInEffectVideoTitle": "The video is muted by the moderator",
588
+        "moderationInEffectDescription": "Please raise hand if you want to speak.",
589
+        "moderationInEffectCSDescription": "Please raise hand if you want to share your screen.",
590
+        "moderationInEffectVideoDescription": "Please raise your hand if you want to start your camera.",
591
+        "moderationInEffectTitle": "Your microphone is muted by the moderator",
592
+        "moderationInEffectCSTitle": "Screen sharing is blocked by the moderator",
593
+        "moderationInEffectVideoTitle": "Your camera is blocked by the moderator",
589 594
         "moderationRequestFromModerator": "The host would like you to unmute",
590 595
         "moderationRequestFromParticipant": "Wants to speak",
591 596
         "moderationStartedTitle": "Moderation started",
@@ -605,16 +610,17 @@
605 610
         },
606 611
         "actions": {
607 612
             "allow": "Allow attendees to:",
613
+            "audioModeration": "Unmute themselves",
608 614
             "blockEveryoneMicCamera": "Block everyone's mic and camera",
609 615
             "invite": "Invite Someone",
610 616
             "askUnmute": "Ask to unmute",
611 617
             "mute": "Mute",
612 618
             "muteAll": "Mute all",
613 619
             "muteEveryoneElse": "Mute everyone else",
614
-            "startModeration": "Unmute themselves or start video",
615 620
             "stopEveryonesVideo": "Stop everyone's video",
616 621
             "stopVideo": "Stop video",
617
-            "unblockEveryoneMicCamera": "Unblock everyone's mic and camera"
622
+            "unblockEveryoneMicCamera": "Unblock everyone's mic and camera",
623
+            "videoModeration": "Start video"
618 624
         }
619 625
     },
620 626
     "passwordSetRemotely": "Set by another participant",
@@ -868,7 +874,7 @@
868 874
             "embedMeeting": "Embed meeting",
869 875
             "feedback": "Leave feedback",
870 876
             "fullScreen": "Toggle full screen",
871
-            "grantModerator": "Grant Moderator",
877
+            "grantModerator": "Grant Moderator Rights",
872 878
             "hangup": "Leave the meeting",
873 879
             "help": "Help",
874 880
             "invite": "Invite people",
@@ -1054,7 +1060,7 @@
1054 1060
         "domuteOthers": "Mute everyone else",
1055 1061
         "domuteVideoOfOthers": "Disable camera of everyone else",
1056 1062
         "flip": "Flip",
1057
-        "grantModerator": "Grant Moderator",
1063
+        "grantModerator": "Grant Moderator Rights",
1058 1064
         "kick": "Kick out",
1059 1065
         "moderator": "Moderator",
1060 1066
         "mute": "Participant is muted",

+ 1
- 0
package.json View File

@@ -47,6 +47,7 @@
47 47
     "base64-js": "1.3.1",
48 48
     "bc-css-flags": "3.0.0",
49 49
     "clipboard-copy": "4.0.1",
50
+    "clsx": "1.1.1",
50 51
     "dropbox": "10.7.0",
51 52
     "focus-visible": "5.1.0",
52 53
     "i18n-iso-countries": "6.8.0",

+ 24
- 6
react/features/av-moderation/actionTypes.js View File

@@ -29,22 +29,40 @@ export const ENABLE_MODERATION = 'ENABLE_MODERATION';
29 29
 
30 30
 
31 31
 /**
32
- * The type of (redux) action which signals that A/V Moderation disable has been requested.
32
+ * The type of (redux) action which signals that Audio Moderation disable has been requested.
33 33
  *
34 34
  * {
35
- *     type: REQUEST_DISABLE_MODERATION
35
+ *     type: REQUEST_DISABLE_AUDIO_MODERATION
36 36
  * }
37 37
  */
38
-export const REQUEST_DISABLE_MODERATION = 'REQUEST_DISABLE_MODERATION';
38
+export const REQUEST_DISABLE_AUDIO_MODERATION = 'REQUEST_DISABLE_AUDIO_MODERATION';
39 39
 
40 40
 /**
41
- * The type of (redux) action which signals that A/V Moderation enable has been requested.
41
+ * The type of (redux) action which signals that Video Moderation disable has been requested.
42 42
  *
43 43
  * {
44
- *     type: REQUEST_ENABLE_MODERATION
44
+ *     type: REQUEST_DISABLE_VIDEO_MODERATION
45 45
  * }
46 46
  */
47
-export const REQUEST_ENABLE_MODERATION = 'REQUEST_ENABLE_MODERATION';
47
+export const REQUEST_DISABLE_VIDEO_MODERATION = 'REQUEST_DISABLE_VIDEO_MODERATION';
48
+
49
+/**
50
+ * The type of (redux) action which signals that Audio Moderation enable has been requested.
51
+ *
52
+ * {
53
+ *     type: REQUEST_ENABLE_AUDIO_MODERATION
54
+ * }
55
+ */
56
+export const REQUEST_ENABLE_AUDIO_MODERATION = 'REQUEST_ENABLE_AUDIO_MODERATION';
57
+
58
+/**
59
+ * The type of (redux) action which signals that Video Moderation enable has been requested.
60
+ *
61
+ * {
62
+ *     type: REQUEST_ENABLE_VIDEO_MODERATION
63
+ * }
64
+ */
65
+export const REQUEST_ENABLE_VIDEO_MODERATION = 'REQUEST_ENABLE_VIDEO_MODERATION';
48 66
 
49 67
 /**
50 68
  * The type of (redux) action which signals that the local participant had been approved.

+ 47
- 13
react/features/av-moderation/actions.js View File

@@ -11,9 +11,12 @@ import {
11 11
     LOCAL_PARTICIPANT_MODERATION_NOTIFICATION,
12 12
     PARTICIPANT_APPROVED,
13 13
     PARTICIPANT_PENDING_AUDIO,
14
-    REQUEST_DISABLE_MODERATION,
15
-    REQUEST_ENABLE_MODERATION
14
+    REQUEST_DISABLE_AUDIO_MODERATION,
15
+    REQUEST_ENABLE_AUDIO_MODERATION,
16
+    REQUEST_DISABLE_VIDEO_MODERATION,
17
+    REQUEST_ENABLE_VIDEO_MODERATION
16 18
 } from './actionTypes';
19
+import { isEnabledFromState } from './functions';
17 20
 
18 21
 /**
19 22
  * Action used by moderator to approve audio and video for a participant.
@@ -22,10 +25,15 @@ import {
22 25
  * @returns {void}
23 26
  */
24 27
 export const approveParticipant = (id: string) => (dispatch: Function, getState: Function) => {
25
-    const { conference } = getConferenceState(getState());
28
+    const state = getState();
29
+    const { conference } = getConferenceState(state);
26 30
 
27
-    conference.avModerationApprove(MEDIA_TYPE.AUDIO, id);
28
-    conference.avModerationApprove(MEDIA_TYPE.VIDEO, id);
31
+    if (isEnabledFromState(MEDIA_TYPE.AUDIO, state)) {
32
+        conference.avModerationApprove(MEDIA_TYPE.AUDIO, id);
33
+    }
34
+    if (isEnabledFromState(MEDIA_TYPE.VIDEO, state)) {
35
+        conference.avModerationApprove(MEDIA_TYPE.VIDEO, id);
36
+    }
29 37
 };
30 38
 
31 39
 /**
@@ -89,28 +97,54 @@ export const enableModeration = (mediaType: MediaType, actor: Object) => {
89 97
 };
90 98
 
91 99
 /**
92
- * Requests disable of audio and video moderation.
100
+ * Requests disable of audio moderation.
93 101
  *
94 102
  * @returns {{
95
- *     type: REQUEST_DISABLE_MODERATED_AUDIO
103
+ *     type: REQUEST_DISABLE_AUDIO_MODERATION
96 104
  * }}
97 105
  */
98
-export const requestDisableModeration = () => {
106
+export const requestDisableAudioModeration = () => {
99 107
     return {
100
-        type: REQUEST_DISABLE_MODERATION
108
+        type: REQUEST_DISABLE_AUDIO_MODERATION
101 109
     };
102 110
 };
103 111
 
104 112
 /**
105
- * Requests enabled audio & video moderation.
113
+ * Requests disable of video moderation.
106 114
  *
107 115
  * @returns {{
108
- *     type: REQUEST_ENABLE_MODERATED_AUDIO
116
+ *     type: REQUEST_DISABLE_VIDEO_MODERATION
117
+ * }}
118
+ */
119
+export const requestDisableVideoModeration = () => {
120
+    return {
121
+        type: REQUEST_DISABLE_VIDEO_MODERATION
122
+    };
123
+};
124
+
125
+/**
126
+ * Requests enable of audio moderation.
127
+ *
128
+ * @returns {{
129
+ *     type: REQUEST_ENABLE_AUDIO_MODERATION
130
+ * }}
131
+ */
132
+export const requestEnableAudioModeration = () => {
133
+    return {
134
+        type: REQUEST_ENABLE_AUDIO_MODERATION
135
+    };
136
+};
137
+
138
+/**
139
+ * Requests enable of video moderation.
140
+ *
141
+ * @returns {{
142
+ *     type: REQUEST_ENABLE_VIDEO_MODERATION
109 143
  * }}
110 144
  */
111
-export const requestEnableModeration = () => {
145
+export const requestEnableVideoModeration = () => {
112 146
     return {
113
-        type: REQUEST_ENABLE_MODERATION
147
+        type: REQUEST_ENABLE_VIDEO_MODERATION
114 148
     };
115 149
 };
116 150
 

+ 0
- 38
react/features/av-moderation/components/AudioModerationNotifications.js View File

@@ -1,38 +0,0 @@
1
-import React from 'react';
2
-import { useTranslation } from 'react-i18next';
3
-import { useSelector } from 'react-redux';
4
-
5
-import NotificationWithParticipants from '../../notifications/components/web/NotificationWithParticipants';
6
-import {
7
-    approveParticipant,
8
-    dismissPendingAudioParticipant
9
-} from '../actions';
10
-import { getParticipantsAskingToAudioUnmute } from '../functions';
11
-
12
-
13
-/**
14
- * Component used to display a list of participants who asked to be unmuted.
15
- * This is visible only to moderators.
16
- *
17
- * @returns {React$Element<'ul'> | null}
18
- */
19
-export default function() {
20
-    const participants = useSelector(getParticipantsAskingToAudioUnmute);
21
-    const { t } = useTranslation();
22
-
23
-    return participants.length
24
-        ? (
25
-            <>
26
-                <div className = 'title'>
27
-                    { t('raisedHand') }
28
-                </div>
29
-                <NotificationWithParticipants
30
-                    approveButtonText = { t('notify.unmute') }
31
-                    onApprove = { approveParticipant }
32
-                    onReject = { dismissPendingAudioParticipant }
33
-                    participants = { participants }
34
-                    rejectButtonText = { t('dialog.dismiss') }
35
-                    testIdPrefix = 'avModeration' />
36
-            </>
37
-        ) : null;
38
-}

+ 20
- 40
react/features/av-moderation/middleware.js View File

@@ -6,7 +6,6 @@ import { JitsiConferenceEvents } from '../base/lib-jitsi-meet';
6 6
 import { MEDIA_TYPE } from '../base/media';
7 7
 import {
8 8
     getLocalParticipant,
9
-    getParticipantDisplayName,
10 9
     getRemoteParticipants,
11 10
     isLocalParticipantModerator,
12 11
     isParticipantModerator,
@@ -16,16 +15,16 @@ import {
16 15
 import { MiddlewareRegistry, StateListenerRegistry } from '../base/redux';
17 16
 import {
18 17
     hideNotification,
19
-    NOTIFICATION_TIMEOUT,
20 18
     showNotification
21 19
 } from '../notifications';
20
+import { muteLocal } from '../video-menu/actions.any';
22 21
 
23 22
 import {
24
-    DISABLE_MODERATION,
25
-    ENABLE_MODERATION,
26 23
     LOCAL_PARTICIPANT_MODERATION_NOTIFICATION,
27
-    REQUEST_DISABLE_MODERATION,
28
-    REQUEST_ENABLE_MODERATION
24
+    REQUEST_DISABLE_AUDIO_MODERATION,
25
+    REQUEST_DISABLE_VIDEO_MODERATION,
26
+    REQUEST_ENABLE_AUDIO_MODERATION,
27
+    REQUEST_ENABLE_VIDEO_MODERATION
29 28
 } from './actionTypes';
30 29
 import {
31 30
     disableModeration,
@@ -47,29 +46,10 @@ const AUDIO_MODERATION_NOTIFICATION_ID = 'audio-moderation';
47 46
 const CS_MODERATION_NOTIFICATION_ID = 'video-moderation';
48 47
 
49 48
 MiddlewareRegistry.register(({ dispatch, getState }) => next => action => {
50
-    const { actor, mediaType, type } = action;
49
+    const { type } = action;
50
+    const { conference } = getConferenceState(getState());
51 51
 
52 52
     switch (type) {
53
-    case DISABLE_MODERATION:
54
-    case ENABLE_MODERATION: {
55
-        // Audio & video moderation are both enabled at the same time.
56
-        // Avoid displaying 2 different notifications.
57
-        if (mediaType === MEDIA_TYPE.VIDEO) {
58
-            const titleKey = type === ENABLE_MODERATION
59
-                ? 'notify.moderationStartedTitle'
60
-                : 'notify.moderationStoppedTitle';
61
-
62
-            dispatch(showNotification({
63
-                descriptionKey: actor ? 'notify.moderationToggleDescription' : undefined,
64
-                descriptionArguments: actor ? {
65
-                    participantDisplayName: getParticipantDisplayName(getState, actor.getId())
66
-                } : undefined,
67
-                titleKey
68
-            }, NOTIFICATION_TIMEOUT));
69
-        }
70
-
71
-        break;
72
-    }
73 53
     case LOCAL_PARTICIPANT_MODERATION_NOTIFICATION: {
74 54
         let descriptionKey;
75 55
         let titleKey;
@@ -78,19 +58,16 @@ MiddlewareRegistry.register(({ dispatch, getState }) => next => action => {
78 58
         switch (action.mediaType) {
79 59
         case MEDIA_TYPE.AUDIO: {
80 60
             titleKey = 'notify.moderationInEffectTitle';
81
-            descriptionKey = 'notify.moderationInEffectDescription';
82 61
             uid = AUDIO_MODERATION_NOTIFICATION_ID;
83 62
             break;
84 63
         }
85 64
         case MEDIA_TYPE.VIDEO: {
86 65
             titleKey = 'notify.moderationInEffectVideoTitle';
87
-            descriptionKey = 'notify.moderationInEffectVideoDescription';
88 66
             uid = VIDEO_MODERATION_NOTIFICATION_ID;
89 67
             break;
90 68
         }
91 69
         case MEDIA_TYPE.PRESENTER: {
92 70
             titleKey = 'notify.moderationInEffectCSTitle';
93
-            descriptionKey = 'notify.moderationInEffectCSDescription';
94 71
             uid = CS_MODERATION_NOTIFICATION_ID;
95 72
             break;
96 73
         }
@@ -110,17 +87,19 @@ MiddlewareRegistry.register(({ dispatch, getState }) => next => action => {
110 87
 
111 88
         break;
112 89
     }
113
-    case REQUEST_DISABLE_MODERATION: {
114
-        const { conference } = getConferenceState(getState());
115
-
90
+    case REQUEST_DISABLE_AUDIO_MODERATION: {
116 91
         conference.disableAVModeration(MEDIA_TYPE.AUDIO);
92
+        break;
93
+    }
94
+    case REQUEST_DISABLE_VIDEO_MODERATION: {
117 95
         conference.disableAVModeration(MEDIA_TYPE.VIDEO);
118 96
         break;
119 97
     }
120
-    case REQUEST_ENABLE_MODERATION: {
121
-        const { conference } = getConferenceState(getState());
122
-
98
+    case REQUEST_ENABLE_AUDIO_MODERATION: {
123 99
         conference.enableAVModeration(MEDIA_TYPE.AUDIO);
100
+        break;
101
+    }
102
+    case REQUEST_ENABLE_VIDEO_MODERATION: {
124 103
         conference.enableAVModeration(MEDIA_TYPE.VIDEO);
125 104
         break;
126 105
     }
@@ -174,11 +153,12 @@ StateListenerRegistry.register(
174 153
 
175 154
                 // Audio & video moderation are both enabled at the same time.
176 155
                 // Avoid displaying 2 different notifications.
177
-                if (mediaType === MEDIA_TYPE.VIDEO) {
156
+                if (mediaType === MEDIA_TYPE.AUDIO) {
178 157
                     dispatch(showNotification({
179
-                        titleKey: 'notify.unmute',
180
-                        descriptionKey: 'notify.hostAskedUnmute',
181
-                        sticky: true
158
+                        titleKey: 'notify.hostAskedUnmute',
159
+                        sticky: true,
160
+                        customActionNameKey: 'notify.unmute',
161
+                        customActionHandler: () => dispatch(muteLocal(false, MEDIA_TYPE.AUDIO))
182 162
                     }));
183 163
                 }
184 164
             });

+ 1
- 1
react/features/base/icons/svg/camera-empty-disabled.svg View File

@@ -1,3 +1,3 @@
1
-<svg width="22" height="22" viewBox="0 0 22 22" fill="none" xmlns="http://www.w3.org/2000/svg">
1
+<svg width="22" height="22" viewBox="0 0 22 22" xmlns="http://www.w3.org/2000/svg">
2 2
 <path fill-rule="evenodd" clip-rule="evenodd" d="M6.84074 5.49992H6.81762L3.42398 2.10629C3.06002 1.74232 2.47001 1.74223 2.10617 2.10608C1.74232 2.46992 1.74241 3.05993 2.10638 3.42389L4.1824 5.49992H3.66668C2.65415 5.49992 1.83334 6.32073 1.83334 7.33325V14.6666C1.83334 15.6791 2.65415 16.4999 3.66668 16.4999H13.75C14.154 16.4999 14.5274 16.3693 14.8304 16.1479L18.576 19.8936C18.94 20.2575 19.53 20.2576 19.8939 19.8938C20.2577 19.5299 20.2576 18.9399 19.8936 18.5759L15.5833 14.2656V14.2425L13.75 12.4092V12.4323L8.65095 7.33325H8.67407L6.84074 5.49992ZM13.75 9.77398V9.16659V7.33325H11.3093L9.47595 5.49992H13.75C14.7625 5.49992 15.5833 6.32073 15.5833 7.33325V8.11897L18.7952 6.28361C19.2348 6.03243 19.7947 6.18515 20.0459 6.62471C20.125 6.76321 20.1667 6.91998 20.1667 7.0795V14.9203C20.1667 15.2643 19.9772 15.5641 19.6969 15.7209L15.9614 11.9853L18.3333 13.3408V8.65908L15.5833 10.2305V11.6073L13.75 9.77398ZM3.66668 7.33325H6.01574L13.3491 14.6666H3.66668V7.33325Z" />
3 3
 </svg>

+ 23
- 0
react/features/base/media/middleware.js View File

@@ -8,12 +8,15 @@ import {
8 8
     sendAnalytics
9 9
 } from '../../analytics';
10 10
 import { APP_STATE_CHANGED } from '../../mobile/background';
11
+import { isForceMuted } from '../../participants-pane/functions';
11 12
 import { SET_AUDIO_ONLY, setAudioOnly } from '../audio-only';
12 13
 import { isRoomValid, SET_ROOM } from '../conference';
14
+import { getLocalParticipant } from '../participants';
13 15
 import { MiddlewareRegistry } from '../redux';
14 16
 import { getPropertyValue } from '../settings';
15 17
 import { isLocalVideoTrackDesktop, setTrackMuted, TRACK_ADDED } from '../tracks';
16 18
 
19
+import { SET_AUDIO_MUTED, SET_VIDEO_MUTED } from './actionTypes';
17 20
 import { setAudioMuted, setCameraFacingMode, setVideoMuted } from './actions';
18 21
 import {
19 22
     CAMERA_FACING_MODE,
@@ -55,6 +58,26 @@ MiddlewareRegistry.register(store => next => action => {
55 58
 
56 59
         return result;
57 60
     }
61
+
62
+    case SET_AUDIO_MUTED: {
63
+        const state = store.getState();
64
+        const participant = getLocalParticipant(state);
65
+
66
+        if (!action.muted && isForceMuted(participant, MEDIA_TYPE.AUDIO, state)) {
67
+            return;
68
+        }
69
+        break;
70
+    }
71
+
72
+    case SET_VIDEO_MUTED: {
73
+        const state = store.getState();
74
+        const participant = getLocalParticipant(state);
75
+
76
+        if (!action.muted && isForceMuted(participant, MEDIA_TYPE.VIDEO, state)) {
77
+            return;
78
+        }
79
+        break;
80
+    }
58 81
     }
59 82
 
60 83
     return next(action);

+ 12
- 0
react/features/base/participants/actionTypes.js View File

@@ -180,3 +180,15 @@ export const SET_LOADABLE_AVATAR_URL = 'SET_LOADABLE_AVATAR_URL';
180 180
  * }
181 181
  */
182 182
 export const LOCAL_PARTICIPANT_RAISE_HAND = 'LOCAL_PARTICIPANT_RAISE_HAND';
183
+
184
+/**
185
+ * Updates participant in raise hand queue.
186
+ * {
187
+ *     type: RAISE_HAND_UPDATED,
188
+ *     participant: {
189
+ *         id: string,
190
+ *         raiseHand: boolean
191
+ *     }
192
+ * }
193
+ */
194
+export const RAISE_HAND_UPDATED = 'RAISE_HAND_UPDATED';

+ 20
- 8
react/features/base/participants/actions.js View File

@@ -15,7 +15,8 @@ import {
15 15
     PARTICIPANT_LEFT,
16 16
     PARTICIPANT_UPDATED,
17 17
     PIN_PARTICIPANT,
18
-    SET_LOADABLE_AVATAR_URL
18
+    SET_LOADABLE_AVATAR_URL,
19
+    RAISE_HAND_UPDATED
19 20
 } from './actionTypes';
20 21
 import {
21 22
     DISCO_REMOTE_CONTROL_FEATURE
@@ -465,7 +466,7 @@ export function participantUpdated(participant = {}) {
465 466
  * @returns {Promise}
466 467
  */
467 468
 export function participantMutedUs(participant, track) {
468
-    return (dispatch, getState) => {
469
+    return dispatch => {
469 470
         if (!participant) {
470 471
             return;
471 472
         }
@@ -473,12 +474,7 @@ export function participantMutedUs(participant, track) {
473 474
         const isAudio = track.isAudioTrack();
474 475
 
475 476
         dispatch(showNotification({
476
-            descriptionKey: isAudio ? 'notify.mutedRemotelyDescription' : 'notify.videoMutedRemotelyDescription',
477
-            titleKey: isAudio ? 'notify.mutedRemotelyTitle' : 'notify.videoMutedRemotelyTitle',
478
-            titleArguments: {
479
-                participantDisplayName:
480
-                    getParticipantDisplayName(getState, participant.getId())
481
-            }
477
+            titleKey: isAudio ? 'notify.mutedRemotelyTitle' : 'notify.videoMutedRemotelyTitle'
482 478
         }));
483 479
     };
484 480
 }
@@ -574,3 +570,19 @@ export function raiseHand(enabled) {
574 570
         enabled
575 571
     };
576 572
 }
573
+
574
+/**
575
+ * Update raise hand queue of participants.
576
+ *
577
+ * @param {Object} participant - Participant that updated raised hand.
578
+ * @returns {{
579
+ *      type: RAISE_HAND_UPDATED,
580
+ *      participant: Object
581
+ * }}
582
+ */
583
+export function raiseHandUpdateQueue(participant) {
584
+    return {
585
+        type: RAISE_HAND_UPDATED,
586
+        participant
587
+    };
588
+}

+ 30
- 2
react/features/base/participants/functions.js View File

@@ -456,21 +456,35 @@ async function _getFirstLoadableAvatarUrl(participant, store) {
456 456
 export function getSortedParticipants(stateful: Object | Function) {
457 457
     const localParticipant = getLocalParticipant(stateful);
458 458
     const remoteParticipants = getRemoteParticipants(stateful);
459
+    const raisedHandParticipantIds = getRaiseHandsQueue(stateful);
459 460
 
460 461
     const items = [];
461 462
     const dominantSpeaker = getDominantSpeakerParticipant(stateful);
463
+    const raisedHandParticipants = [];
464
+
465
+    raisedHandParticipantIds
466
+        .map(id => remoteParticipants.get(id) || localParticipant)
467
+        .forEach(p => {
468
+            if (p !== dominantSpeaker) {
469
+                raisedHandParticipants.push(p);
470
+            }
471
+        });
462 472
 
463 473
     remoteParticipants.forEach(p => {
464
-        if (p !== dominantSpeaker) {
474
+        if (p !== dominantSpeaker && !raisedHandParticipantIds.find(id => p.id === id)) {
465 475
             items.push(p);
466 476
         }
467 477
     });
468 478
 
479
+    if (!raisedHandParticipantIds.find(id => localParticipant.id === id)) {
480
+        items.push(localParticipant);
481
+    }
482
+
469 483
     items.sort((a, b) =>
470 484
         getParticipantDisplayName(stateful, a.id).localeCompare(getParticipantDisplayName(stateful, b.id))
471 485
     );
472 486
 
473
-    items.unshift(localParticipant);
487
+    items.unshift(...raisedHandParticipants);
474 488
 
475 489
     if (dominantSpeaker && dominantSpeaker !== localParticipant) {
476 490
         items.unshift(dominantSpeaker);
@@ -492,3 +506,17 @@ export function getSortedParticipantIds(stateful: Object | Function): Array<stri
492 506
 
493 507
     return participantIds;
494 508
 }
509
+
510
+/**
511
+ * Get the participants queue with raised hands.
512
+ *
513
+ * @param {(Function|Object)} stateful - The (whole) redux state, or redux's
514
+ * {@code getState} function to be used to retrieve the state
515
+ * features/base/participants.
516
+ * @returns {Array<string>}
517
+ */
518
+export function getRaiseHandsQueue(stateful: Object | Function): Array<string> {
519
+    const { raisedHandsQueue } = toState(stateful)['features/base/participants'];
520
+
521
+    return raisedHandsQueue;
522
+}

+ 56
- 7
react/features/base/participants/middleware.js View File

@@ -3,8 +3,10 @@
3 3
 import { batch } from 'react-redux';
4 4
 
5 5
 import UIEvents from '../../../../service/UI/UIEvents';
6
+import { approveParticipant } from '../../av-moderation/actions';
6 7
 import { toggleE2EE } from '../../e2ee/actions';
7 8
 import { NOTIFICATION_TIMEOUT, showNotification } from '../../notifications';
9
+import { isForceMuted } from '../../participants-pane/functions';
8 10
 import { CALLING, INVITED } from '../../presence-status';
9 11
 import { RAISE_HAND_SOUND_ID } from '../../reactions/constants';
10 12
 import { APP_WILL_MOUNT, APP_WILL_UNMOUNT } from '../app';
@@ -15,6 +17,7 @@ import {
15 17
 } from '../conference';
16 18
 import { getDisableRemoveRaisedHandOnFocus } from '../config/functions.any';
17 19
 import { JitsiConferenceEvents } from '../lib-jitsi-meet';
20
+import { MEDIA_TYPE } from '../media';
18 21
 import { MiddlewareRegistry, StateListenerRegistry } from '../redux';
19 22
 import { playSound, registerSound, unregisterSound } from '../sounds';
20 23
 
@@ -27,7 +30,8 @@ import {
27 30
     PARTICIPANT_DISPLAY_NAME_CHANGED,
28 31
     PARTICIPANT_JOINED,
29 32
     PARTICIPANT_LEFT,
30
-    PARTICIPANT_UPDATED
33
+    PARTICIPANT_UPDATED,
34
+    RAISE_HAND_UPDATED
31 35
 } from './actionTypes';
32 36
 import {
33 37
     localParticipantIdChanged,
@@ -35,6 +39,7 @@ import {
35 39
     localParticipantLeft,
36 40
     participantLeft,
37 41
     participantUpdated,
42
+    raiseHandUpdateQueue,
38 43
     setLoadableAvatarUrl
39 44
 } from './actions';
40 45
 import {
@@ -48,7 +53,9 @@ import {
48 53
     getParticipantById,
49 54
     getParticipantCount,
50 55
     getParticipantDisplayName,
51
-    getRemoteParticipants
56
+    getRaiseHandsQueue,
57
+    getRemoteParticipants,
58
+    isLocalParticipantModerator
52 59
 } from './functions';
53 60
 import { PARTICIPANT_JOINED_FILE, PARTICIPANT_LEFT_FILE } from './sounds';
54 61
 
@@ -122,6 +129,11 @@ MiddlewareRegistry.register(store => next => action => {
122 129
         const { enabled } = action;
123 130
         const localId = getLocalParticipant(store.getState())?.id;
124 131
 
132
+        store.dispatch(raiseHandUpdateQueue({
133
+            id: localId,
134
+            raisedHand: enabled
135
+        }));
136
+
125 137
         store.dispatch(participantUpdated({
126 138
             // XXX Only the local participant is allowed to update without
127 139
             // stating the JitsiConference instance (i.e. participant property
@@ -162,6 +174,21 @@ MiddlewareRegistry.register(store => next => action => {
162 174
         break;
163 175
     }
164 176
 
177
+    case RAISE_HAND_UPDATED: {
178
+        const { participant } = action;
179
+        const queue = getRaiseHandsQueue(store.getState());
180
+
181
+        if (participant.raisedHand) {
182
+            queue.push(participant.id);
183
+            action.queue = queue;
184
+        } else {
185
+            const filteredQueue = queue.filter(id => id !== participant.id);
186
+
187
+            action.queue = filteredQueue;
188
+        }
189
+        break;
190
+    }
191
+
165 192
     case PARTICIPANT_JOINED: {
166 193
         _maybePlaySounds(store, action);
167 194
 
@@ -424,6 +451,7 @@ function _participantJoinedOrUpdated(store, next, action) {
424 451
     // Send an external update of the local participant's raised hand state
425 452
     // if a new raised hand state is defined in the action.
426 453
     if (typeof raisedHand !== 'undefined') {
454
+
427 455
         if (local) {
428 456
             const { conference } = getState()['features/base/conference'];
429 457
 
@@ -476,6 +504,7 @@ function _participantJoinedOrUpdated(store, next, action) {
476 504
  */
477 505
 function _raiseHandUpdated({ dispatch, getState }, conference, participantId, newValue) {
478 506
     const raisedHand = newValue === 'true';
507
+    const state = getState();
479 508
 
480 509
     dispatch(participantUpdated({
481 510
         conference,
@@ -483,17 +512,37 @@ function _raiseHandUpdated({ dispatch, getState }, conference, participantId, ne
483 512
         raisedHand
484 513
     }));
485 514
 
515
+    dispatch(raiseHandUpdateQueue({
516
+        id: participantId,
517
+        raisedHand
518
+    }));
519
+
486 520
     if (typeof APP !== 'undefined') {
487 521
         APP.API.notifyRaiseHandUpdated(participantId, raisedHand);
488 522
     }
489 523
 
524
+    const isModerator = isLocalParticipantModerator(state);
525
+    const participant = getParticipantById(state, participantId);
526
+    let shouldDisplayAllowAction = false;
527
+
528
+    if (isModerator) {
529
+        shouldDisplayAllowAction = isForceMuted(participant, MEDIA_TYPE.AUDIO, state)
530
+            || isForceMuted(participant, MEDIA_TYPE.VIDEO, state);
531
+    }
532
+
533
+    const action = shouldDisplayAllowAction ? {
534
+        customActionNameKey: 'notify.allowAction',
535
+        customActionHandler: () => dispatch(approveParticipant(participantId))
536
+    } : {};
537
+
490 538
     if (raisedHand) {
491 539
         dispatch(showNotification({
492
-            titleArguments: {
493
-                name: getParticipantDisplayName(getState, participantId)
494
-            },
495
-            titleKey: 'notify.raisedHand'
496
-        }, NOTIFICATION_TIMEOUT));
540
+            titleKey: 'notify.somebody',
541
+            title: getParticipantDisplayName(state, participantId),
542
+            descriptionKey: 'notify.raisedHand',
543
+            raiseHandNotification: true,
544
+            ...action
545
+        }, NOTIFICATION_TIMEOUT * (shouldDisplayAllowAction ? 2 : 1)));
497 546
         dispatch(playSound(RAISE_HAND_SOUND_ID));
498 547
     }
499 548
 }

+ 9
- 1
react/features/base/participants/reducer.js View File

@@ -10,6 +10,7 @@ import {
10 10
     PARTICIPANT_LEFT,
11 11
     PARTICIPANT_UPDATED,
12 12
     PIN_PARTICIPANT,
13
+    RAISE_HAND_UPDATED,
13 14
     SET_LOADABLE_AVATAR_URL
14 15
 } from './actionTypes';
15 16
 import { LOCAL_PARTICIPANT_DEFAULT_ID, PARTICIPANT_ROLE } from './constants';
@@ -63,7 +64,8 @@ const DEFAULT_STATE = {
63 64
     remote: new Map(),
64 65
     sortedRemoteParticipants: new Map(),
65 66
     sortedRemoteScreenshares: new Map(),
66
-    speakersList: new Map()
67
+    speakersList: new Map(),
68
+    raisedHandsQueue: []
67 69
 };
68 70
 
69 71
 /**
@@ -318,6 +320,12 @@ ReducerRegistry.register('features/base/participants', (state = DEFAULT_STATE, a
318 320
 
319 321
         return { ...state };
320 322
     }
323
+    case RAISE_HAND_UPDATED: {
324
+        return {
325
+            ...state,
326
+            raisedHandsQueue: action.queue
327
+        };
328
+    }
321 329
     case SCREEN_SHARE_REMOTE_PARTICIPANTS_UPDATED: {
322 330
         const { participantIds } = action;
323 331
         const sortedSharesList = [];

+ 8
- 4
react/features/base/tracks/middleware.js View File

@@ -14,7 +14,8 @@ import {
14 14
     SET_VIDEO_MUTED,
15 15
     VIDEO_MUTISM_AUTHORITY,
16 16
     TOGGLE_CAMERA_FACING_MODE,
17
-    toggleCameraFacingMode
17
+    toggleCameraFacingMode,
18
+    VIDEO_TYPE
18 19
 } from '../media';
19 20
 import { MiddlewareRegistry } from '../redux';
20 21
 
@@ -28,6 +29,7 @@ import {
28 29
 import {
29 30
     createLocalTracksA,
30 31
     showNoDataFromSourceVideoError,
32
+    toggleScreensharing,
31 33
     trackNoDataFromSourceNotificationInfoChanged
32 34
 } from './actions';
33 35
 import {
@@ -137,9 +139,9 @@ MiddlewareRegistry.register(store => next => action => {
137 139
 
138 140
     case TOGGLE_SCREENSHARING:
139 141
         if (typeof APP === 'object') {
140
-
141 142
             // check for A/V Moderation when trying to start screen sharing
142
-            if (action.enabled && shouldShowModeratedNotification(MEDIA_TYPE.VIDEO, store.getState())) {
143
+            if ((action.enabled || action.enabled === undefined)
144
+                && shouldShowModeratedNotification(MEDIA_TYPE.VIDEO, store.getState())) {
143 145
                 store.dispatch(showModeratedNotification(MEDIA_TYPE.PRESENTER));
144 146
 
145 147
                 return;
@@ -171,8 +173,10 @@ MiddlewareRegistry.register(store => next => action => {
171 173
                 // Do not change the video mute state for local presenter tracks.
172 174
                 if (jitsiTrack.type === MEDIA_TYPE.PRESENTER) {
173 175
                     APP.conference.mutePresenter(muted);
174
-                } else if (jitsiTrack.isLocal()) {
176
+                } else if (jitsiTrack.isLocal() && !(jitsiTrack.videoType === VIDEO_TYPE.DESKTOP)) {
175 177
                     APP.conference.setVideoMuteStatus();
178
+                } else if (jitsiTrack.isLocal() && muted && jitsiTrack.videoType === VIDEO_TYPE.DESKTOP) {
179
+                    store.dispatch(toggleScreensharing(false));
176 180
                 } else {
177 181
                     APP.UI.setVideoMuted(participantID);
178 182
                 }

+ 22
- 0
react/features/chat/actions.web.js View File

@@ -3,6 +3,7 @@
3 3
 import type { Dispatch } from 'redux';
4 4
 
5 5
 import VideoLayout from '../../../modules/UI/videolayout/VideoLayout';
6
+import { getParticipantById } from '../base/participants/functions';
6 7
 
7 8
 import { OPEN_CHAT } from './actionTypes';
8 9
 import { closeChat } from './actions.any';
@@ -27,6 +28,27 @@ export function openChat(participant: Object) {
27 28
     };
28 29
 }
29 30
 
31
+/**
32
+ * Displays the chat panel for a participant identified by an id.
33
+ *
34
+ * @param {string} id - The id of the participant.
35
+ * @returns {{
36
+ *     participant: Participant,
37
+ *     type: OPEN_CHAT
38
+ * }}
39
+ */
40
+export function openChatById(id: string) {
41
+    return function(dispatch: (Object) => Object, getState: Function) {
42
+        const participant = getParticipantById(getState(), id);
43
+
44
+        return dispatch({
45
+            participant,
46
+            type: OPEN_CHAT
47
+        });
48
+    };
49
+}
50
+
51
+
30 52
 /**
31 53
  * Toggles display of the chat panel.
32 54
  *

+ 0
- 2
react/features/conference/components/web/Conference.js View File

@@ -4,7 +4,6 @@ import _ from 'lodash';
4 4
 import React from 'react';
5 5
 
6 6
 import VideoLayout from '../../../../../modules/UI/videolayout/VideoLayout';
7
-import AudioModerationNotifications from '../../../av-moderation/components/AudioModerationNotifications';
8 7
 import { getConferenceNameForTitle } from '../../../base/conference';
9 8
 import { connect, disconnect } from '../../../base/connection';
10 9
 import { translate } from '../../../base/i18n';
@@ -233,7 +232,6 @@ class Conference extends AbstractConference<Props, *> {
233 232
                         {!_isParticipantsPaneVisible
234 233
                          && <div id = 'notification-participant-list'>
235 234
                              <KnockingParticipantList />
236
-                             <AudioModerationNotifications />
237 235
                          </div>}
238 236
                         <Filmstrip />
239 237
                     </div>

+ 10
- 0
react/features/notifications/actionTypes.js View File

@@ -45,3 +45,13 @@ export const SHOW_NOTIFICATION = 'SHOW_NOTIFICATION';
45 45
  * }
46 46
  */
47 47
 export const SET_NOTIFICATIONS_ENABLED = 'SET_NOTIFICATIONS_ENABLED';
48
+
49
+/**
50
+ * The type of (redux) action which signals that raise hand notifications
51
+ * should be dismissed.
52
+ *
53
+ * {
54
+ *     type: HIDE_RAISE_HAND_NOTIFICATIONS
55
+ * }
56
+ */
57
+export const HIDE_RAISE_HAND_NOTIFICATIONS = 'HIDE_RAISE_HAND_NOTIFICATIONS';

+ 14
- 0
react/features/notifications/actions.js View File

@@ -9,6 +9,7 @@ import { getParticipantCount } from '../base/participants/functions';
9 9
 import {
10 10
     CLEAR_NOTIFICATIONS,
11 11
     HIDE_NOTIFICATION,
12
+    HIDE_RAISE_HAND_NOTIFICATIONS,
12 13
     SET_NOTIFICATIONS_ENABLED,
13 14
     SHOW_NOTIFICATION
14 15
 } from './actionTypes';
@@ -48,6 +49,19 @@ export function hideNotification(uid: string) {
48 49
     };
49 50
 }
50 51
 
52
+/**
53
+ * Removes the raise hand notifications.
54
+ *
55
+ * @returns {{
56
+ *     type: HIDE_RAISE_HAND_NOTIFICATIONS
57
+ * }}
58
+ */
59
+export function hideRaiseHandNotifications() {
60
+    return {
61
+        type: HIDE_RAISE_HAND_NOTIFICATIONS
62
+    };
63
+}
64
+
51 65
 /**
52 66
  * Stops notifications from being displayed.
53 67
  *

+ 16
- 23
react/features/notifications/middleware.js View File

@@ -7,12 +7,15 @@ import {
7 7
     PARTICIPANT_ROLE,
8 8
     PARTICIPANT_UPDATED,
9 9
     getParticipantById,
10
-    getParticipantDisplayName
10
+    getParticipantDisplayName,
11
+    getLocalParticipant
11 12
 } from '../base/participants';
12 13
 import { MiddlewareRegistry, StateListenerRegistry } from '../base/redux';
14
+import { PARTICIPANTS_PANE_OPEN } from '../participants-pane/actionTypes';
13 15
 
14 16
 import {
15 17
     clearNotifications,
18
+    hideRaiseHandNotifications,
16 19
     showNotification,
17 20
     showParticipantJoinedNotification
18 21
 } from './actions';
@@ -42,22 +45,6 @@ MiddlewareRegistry.register(store => next => action => {
42 45
             ));
43 46
         }
44 47
 
45
-        if (typeof interfaceConfig === 'object'
46
-                && !interfaceConfig.DISABLE_FOCUS_INDICATOR && p.role === PARTICIPANT_ROLE.MODERATOR) {
47
-            // Do not show the notification for mobile and also when the focus indicator is disabled.
48
-            const displayName = getParticipantDisplayName(state, p.id);
49
-
50
-            if (!p.isReplacing) {
51
-                dispatch(showNotification({
52
-                    descriptionArguments: { to: displayName || '$t(notify.somebody)' },
53
-                    descriptionKey: 'notify.grantedTo',
54
-                    titleKey: 'notify.somebody',
55
-                    title: displayName
56
-                },
57
-                NOTIFICATION_TIMEOUT));
58
-            }
59
-        }
60
-
61 48
         return result;
62 49
     }
63 50
     case PARTICIPANT_LEFT: {
@@ -82,30 +69,36 @@ MiddlewareRegistry.register(store => next => action => {
82 69
         return next(action);
83 70
     }
84 71
     case PARTICIPANT_UPDATED: {
85
-        if (typeof interfaceConfig === 'undefined' || interfaceConfig.DISABLE_FOCUS_INDICATOR) {
72
+        if (typeof interfaceConfig === 'undefined') {
86 73
             // Do not show the notification for mobile and also when the focus indicator is disabled.
87 74
             return next(action);
88 75
         }
89 76
 
90 77
         const { id, role } = action.participant;
91 78
         const state = store.getState();
79
+        const localParticipant = getLocalParticipant(state);
80
+
81
+        if (localParticipant.id !== id) {
82
+            return next(action);
83
+        }
84
+
92 85
         const oldParticipant = getParticipantById(state, id);
93 86
         const oldRole = oldParticipant?.role;
94 87
 
95 88
         if (oldRole && oldRole !== role && role === PARTICIPANT_ROLE.MODERATOR) {
96
-            const displayName = getParticipantDisplayName(state, id);
97 89
 
98 90
             store.dispatch(showNotification({
99
-                descriptionArguments: { to: displayName || '$t(notify.somebody)' },
100
-                descriptionKey: 'notify.grantedTo',
101
-                titleKey: 'notify.somebody',
102
-                title: displayName
91
+                titleKey: 'notify.moderator'
103 92
             },
104 93
             NOTIFICATION_TIMEOUT));
105 94
         }
106 95
 
107 96
         return next(action);
108 97
     }
98
+    case PARTICIPANTS_PANE_OPEN: {
99
+        store.dispatch(hideRaiseHandNotifications());
100
+        break;
101
+    }
109 102
     }
110 103
 
111 104
     return next(action);

+ 9
- 0
react/features/notifications/reducer.js View File

@@ -5,6 +5,7 @@ import { ReducerRegistry } from '../base/redux';
5 5
 import {
6 6
     CLEAR_NOTIFICATIONS,
7 7
     HIDE_NOTIFICATION,
8
+    HIDE_RAISE_HAND_NOTIFICATIONS,
8 9
     SET_NOTIFICATIONS_ENABLED,
9 10
     SHOW_NOTIFICATION
10 11
 } from './actionTypes';
@@ -43,6 +44,14 @@ ReducerRegistry.register('features/notifications',
43 44
                     notification => notification.uid !== action.uid)
44 45
             };
45 46
 
47
+        case HIDE_RAISE_HAND_NOTIFICATIONS:
48
+            return {
49
+                ...state,
50
+                notifications: state.notifications.filter(
51
+                    notification => !notification.props.raiseHandNotification
52
+                )
53
+            };
54
+
46 55
         case SET_NOTIFICATIONS_ENABLED:
47 56
             return {
48 57
                 ...state,

+ 71
- 21
react/features/participants-pane/components/FooterContextMenu.js View File

@@ -1,11 +1,17 @@
1 1
 // @flow
2 2
 
3 3
 import { makeStyles } from '@material-ui/core/styles';
4
+import clsx from 'clsx';
4 5
 import React, { useCallback } from 'react';
5 6
 import { useTranslation } from 'react-i18next';
6 7
 import { useDispatch, useSelector } from 'react-redux';
7 8
 
8
-import { requestDisableModeration, requestEnableModeration } from '../../av-moderation/actions';
9
+import {
10
+    requestDisableAudioModeration,
11
+    requestDisableVideoModeration,
12
+    requestEnableAudioModeration,
13
+    requestEnableVideoModeration
14
+} from '../../av-moderation/actions';
9 15
 import {
10 16
     isEnabled as isAvModerationEnabled,
11 17
     isSupported as isAvModerationSupported
@@ -13,7 +19,10 @@ import {
13 19
 import { openDialog } from '../../base/dialog';
14 20
 import { Icon, IconCheck, IconVideoOff } from '../../base/icons';
15 21
 import { MEDIA_TYPE } from '../../base/media';
16
-import { getLocalParticipant } from '../../base/participants';
22
+import {
23
+    getParticipantCount,
24
+    isEveryoneModerator
25
+} from '../../base/participants';
17 26
 import { MuteEveryonesVideoDialog } from '../../video-menu/components';
18 27
 
19 28
 import {
@@ -33,6 +42,17 @@ const useStyles = makeStyles(() => {
33 42
             transform: 'translateY(-100%)',
34 43
             width: '283px'
35 44
         },
45
+        drawer: {
46
+            width: '100%',
47
+            top: 'auto',
48
+            bottom: 0,
49
+            transform: 'none',
50
+            position: 'relative',
51
+
52
+            '& > div': {
53
+                lineHeight: '32px'
54
+            }
55
+        },
36 56
         text: {
37 57
             color: '#C2C2C2',
38 58
             padding: '10px 16px 10px 52px'
@@ -45,31 +65,43 @@ const useStyles = makeStyles(() => {
45 65
 
46 66
 type Props = {
47 67
 
48
-  /**
49
-   * Callback for the mouse leaving this item
50
-   */
51
-  onMouseLeave: Function
68
+    /**
69
+     * Whether the menu is displayed inside a drawer.
70
+     */
71
+    inDrawer?: boolean,
72
+
73
+    /**
74
+     * Callback for the mouse leaving this item.
75
+     */
76
+    onMouseLeave?: Function
52 77
 };
53 78
 
54
-export const FooterContextMenu = ({ onMouseLeave }: Props) => {
79
+export const FooterContextMenu = ({ inDrawer, onMouseLeave }: Props) => {
55 80
     const dispatch = useDispatch();
56 81
     const isModerationSupported = useSelector(isAvModerationSupported());
57
-    const isModerationEnabled = useSelector(isAvModerationEnabled(MEDIA_TYPE.AUDIO));
58
-    const { id } = useSelector(getLocalParticipant);
82
+    const allModerators = useSelector(isEveryoneModerator);
83
+    const participantCount = useSelector(getParticipantCount);
84
+    const isAudioModerationEnabled = useSelector(isAvModerationEnabled(MEDIA_TYPE.AUDIO));
85
+    const isVideoModerationEnabled = useSelector(isAvModerationEnabled(MEDIA_TYPE.VIDEO));
86
+
59 87
     const { t } = useTranslation();
60 88
 
61
-    const disable = useCallback(() => dispatch(requestDisableModeration()), [ dispatch ]);
89
+    const disableAudioModeration = useCallback(() => dispatch(requestDisableAudioModeration()), [ dispatch ]);
90
+
91
+    const disableVideoModeration = useCallback(() => dispatch(requestDisableVideoModeration()), [ dispatch ]);
62 92
 
63
-    const enable = useCallback(() => dispatch(requestEnableModeration()), [ dispatch ]);
93
+    const enableAudioModeration = useCallback(() => dispatch(requestEnableAudioModeration()), [ dispatch ]);
94
+
95
+    const enableVideoModeration = useCallback(() => dispatch(requestEnableVideoModeration()), [ dispatch ]);
64 96
 
65 97
     const classes = useStyles();
66 98
 
67 99
     const muteAllVideo = useCallback(
68
-        () => dispatch(openDialog(MuteEveryonesVideoDialog, { exclude: [ id ] })), [ dispatch ]);
100
+        () => dispatch(openDialog(MuteEveryonesVideoDialog)), [ dispatch ]);
69 101
 
70 102
     return (
71 103
         <ContextMenu
72
-            className = { classes.contextMenu }
104
+            className = { clsx(classes.contextMenu, inDrawer && clsx(classes.drawer)) }
73 105
             onMouseLeave = { onMouseLeave }>
74 106
             <ContextMenuItemGroup>
75 107
                 <ContextMenuItem
@@ -81,27 +113,45 @@ export const FooterContextMenu = ({ onMouseLeave }: Props) => {
81 113
                     <span>{ t('participantsPane.actions.stopEveryonesVideo') }</span>
82 114
                 </ContextMenuItem>
83 115
             </ContextMenuItemGroup>
84
-            { isModerationSupported ? (
116
+            {isModerationSupported && (participantCount === 1 || !allModerators) ? (
85 117
                 <ContextMenuItemGroup>
86 118
                     <div className = { classes.text }>
87 119
                         {t('participantsPane.actions.allow')}
88 120
                     </div>
89
-                    { isModerationEnabled ? (
121
+                    { isAudioModerationEnabled ? (
122
+                        <ContextMenuItem
123
+                            id = 'participants-pane-context-menu-stop-audio-moderation'
124
+                            onClick = { disableAudioModeration }>
125
+                            <span className = { classes.paddedAction }>
126
+                                {t('participantsPane.actions.audioModeration') }
127
+                            </span>
128
+                        </ContextMenuItem>
129
+                    ) : (
130
+                        <ContextMenuItem
131
+                            id = 'participants-pane-context-menu-start-audio-moderation'
132
+                            onClick = { enableAudioModeration }>
133
+                            <Icon
134
+                                size = { 20 }
135
+                                src = { IconCheck } />
136
+                            <span>{t('participantsPane.actions.audioModeration') }</span>
137
+                        </ContextMenuItem>
138
+                    )}
139
+                    { isVideoModerationEnabled ? (
90 140
                         <ContextMenuItem
91
-                            id = 'participants-pane-context-menu-stop-moderation'
92
-                            onClick = { disable }>
141
+                            id = 'participants-pane-context-menu-stop-video-moderation'
142
+                            onClick = { disableVideoModeration }>
93 143
                             <span className = { classes.paddedAction }>
94
-                                { t('participantsPane.actions.startModeration') }
144
+                                {t('participantsPane.actions.videoModeration')}
95 145
                             </span>
96 146
                         </ContextMenuItem>
97 147
                     ) : (
98 148
                         <ContextMenuItem
99
-                            id = 'participants-pane-context-menu-start-moderation'
100
-                            onClick = { enable }>
149
+                            id = 'participants-pane-context-menu-start-video-moderation'
150
+                            onClick = { enableVideoModeration }>
101 151
                             <Icon
102 152
                                 size = { 20 }
103 153
                                 src = { IconCheck } />
104
-                            <span>{ t('participantsPane.actions.startModeration') }</span>
154
+                            <span>{t('participantsPane.actions.videoModeration')}</span>
105 155
                         </ContextMenuItem>
106 156
                     )}
107 157
                 </ContextMenuItemGroup>

+ 23
- 13
react/features/participants-pane/components/web/LobbyParticipantItem.js View File

@@ -1,27 +1,39 @@
1 1
 // @flow
2 2
 
3
-import React, { useCallback } from 'react';
3
+import React from 'react';
4 4
 import { useTranslation } from 'react-i18next';
5
-import { useDispatch } from 'react-redux';
6 5
 
7
-import { approveKnockingParticipant, rejectKnockingParticipant } from '../../../lobby/actions';
8 6
 import { ACTION_TRIGGER, MEDIA_STATE } from '../../constants';
7
+import { useLobbyActions } from '../../hooks';
9 8
 
10 9
 import ParticipantItem from './ParticipantItem';
11 10
 import { ParticipantActionButton } from './styled';
12 11
 
13 12
 type Props = {
14 13
 
14
+    /**
15
+     * If an overflow drawer should be displayed.
16
+     */
17
+    overflowDrawer: boolean,
18
+
19
+    /**
20
+     * Callback used to open a drawer with admit/reject actions.
21
+     */
22
+    openDrawerForParticipant: Function,
23
+
15 24
     /**
16 25
      * Participant reference
17 26
      */
18 27
     participant: Object
19 28
 };
20 29
 
21
-export const LobbyParticipantItem = ({ participant: p }: Props) => {
22
-    const dispatch = useDispatch();
23
-    const admit = useCallback(() => dispatch(approveKnockingParticipant(p.id), [ dispatch ]));
24
-    const reject = useCallback(() => dispatch(rejectKnockingParticipant(p.id), [ dispatch ]));
30
+export const LobbyParticipantItem = ({
31
+    overflowDrawer,
32
+    participant: p,
33
+    openDrawerForParticipant
34
+}: Props) => {
35
+    const { id } = p;
36
+    const [ admit ] = useLobbyActions({ participantID: id });
25 37
     const { t } = useTranslation();
26 38
 
27 39
     return (
@@ -30,14 +42,12 @@ export const LobbyParticipantItem = ({ participant: p }: Props) => {
30 42
             audioMediaState = { MEDIA_STATE.NONE }
31 43
             displayName = { p.name }
32 44
             local = { p.local }
33
-            participantID = { p.id }
45
+            openDrawerForParticipant = { openDrawerForParticipant }
46
+            overflowDrawer = { overflowDrawer }
47
+            participantID = { id }
34 48
             raisedHand = { p.raisedHand }
35
-            videoMuteState = { MEDIA_STATE.NONE }
49
+            videoMediaState = { MEDIA_STATE.NONE }
36 50
             youText = { t('chat.you') }>
37
-            <ParticipantActionButton
38
-                onClick = { reject }>
39
-                {t('lobby.reject')}
40
-            </ParticipantActionButton>
41 51
             <ParticipantActionButton
42 52
                 onClick = { admit }
43 53
                 primary = { true }>

+ 47
- 0
react/features/participants-pane/components/web/LobbyParticipantItems.js View File

@@ -0,0 +1,47 @@
1
+// @flow
2
+
3
+import React from 'react';
4
+
5
+import { LobbyParticipantItem } from './LobbyParticipantItem';
6
+
7
+type Props = {
8
+
9
+    /**
10
+     * Opens a drawer with actions for a knocking participant.
11
+     */
12
+    openDrawerForParticipant: Function,
13
+
14
+    /**
15
+     * If a drawer with actions should be displayed.
16
+     */
17
+    overflowDrawer: boolean,
18
+
19
+    /**
20
+     * List with the knocking participants.
21
+     */
22
+    participants: Array<Object>
23
+}
24
+
25
+/**
26
+ * Component used to display a list of knocking participants.
27
+ *
28
+ * @param {Object} props - The props of the component.
29
+ * @returns {ReactNode}
30
+ */
31
+function LobbyParticipantItems({ openDrawerForParticipant, overflowDrawer, participants }: Props) {
32
+
33
+    return (
34
+        <div>
35
+            {participants.map(p => (
36
+                <LobbyParticipantItem
37
+                    key = { p.id }
38
+                    openDrawerForParticipant = { openDrawerForParticipant }
39
+                    overflowDrawer = { overflowDrawer }
40
+                    participant = { p } />)
41
+            )}
42
+        </div>
43
+    );
44
+}
45
+
46
+// Memoize the component in order to avoid rerender on drawer open/close.
47
+export default React.memo<Props>(LobbyParticipantItems);

+ 0
- 72
react/features/participants-pane/components/web/LobbyParticipantList.js View File

@@ -1,72 +0,0 @@
1
-// @flow
2
-
3
-import { makeStyles } from '@material-ui/core/styles';
4
-import React, { useCallback } from 'react';
5
-import { useTranslation } from 'react-i18next';
6
-import { useSelector, useDispatch } from 'react-redux';
7
-
8
-import { withPixelLineHeight } from '../../../base/styles/functions.web';
9
-import { admitMultiple } from '../../../lobby/actions.web';
10
-import { getKnockingParticipants, getLobbyEnabled } from '../../../lobby/functions';
11
-
12
-import { LobbyParticipantItem } from './LobbyParticipantItem';
13
-
14
-const useStyles = makeStyles(theme => {
15
-    return {
16
-        headingContainer: {
17
-            alignItems: 'center',
18
-            display: 'flex',
19
-            justifyContent: 'space-between'
20
-        },
21
-        heading: {
22
-            ...withPixelLineHeight(theme.typography.heading7),
23
-            color: theme.palette.text02
24
-        },
25
-        link: {
26
-            ...withPixelLineHeight(theme.typography.labelBold),
27
-            color: theme.palette.link01,
28
-            cursor: 'pointer'
29
-        }
30
-    };
31
-});
32
-
33
-
34
-export const LobbyParticipantList = () => {
35
-    const lobbyEnabled = useSelector(getLobbyEnabled);
36
-    const participants = useSelector(getKnockingParticipants);
37
-
38
-    const { t } = useTranslation();
39
-    const classes = useStyles();
40
-    const dispatch = useDispatch();
41
-    const admitAll = useCallback(() => {
42
-        dispatch(admitMultiple(participants));
43
-    }, [ dispatch, participants ]);
44
-
45
-    if (!lobbyEnabled || !participants.length) {
46
-        return null;
47
-    }
48
-
49
-    return (
50
-    <>
51
-        <div className = { classes.headingContainer }>
52
-            <div className = { classes.heading }>
53
-                {t('participantsPane.headings.lobby', { count: participants.length })}
54
-            </div>
55
-            {
56
-                participants.length > 1 && (
57
-                    <div
58
-                        className = { classes.link }
59
-                        onClick = { admitAll }>{t('lobby.admitAll')}</div>
60
-                )
61
-            }
62
-        </div>
63
-        <div>
64
-            {participants.map(p => (
65
-                <LobbyParticipantItem
66
-                    key = { p.id }
67
-                    participant = { p } />)
68
-            )}
69
-        </div>
70
-    </>
71
-    );
72
-};

+ 134
- 0
react/features/participants-pane/components/web/LobbyParticipants.js View File

@@ -0,0 +1,134 @@
1
+// @flow
2
+
3
+import { makeStyles } from '@material-ui/core/styles';
4
+import React, { useCallback } from 'react';
5
+import { useTranslation } from 'react-i18next';
6
+import { useSelector, useDispatch } from 'react-redux';
7
+
8
+import { Avatar } from '../../../base/avatar';
9
+import { Icon, IconCheck, IconClose } from '../../../base/icons';
10
+import { withPixelLineHeight } from '../../../base/styles/functions.web';
11
+import { admitMultiple } from '../../../lobby/actions.web';
12
+import { getLobbyEnabled, getKnockingParticipants } from '../../../lobby/functions';
13
+import { Drawer, DrawerPortal } from '../../../toolbox/components/web';
14
+import { showOverflowDrawer } from '../../../toolbox/functions';
15
+import { useLobbyActions, useParticipantDrawer } from '../../hooks';
16
+
17
+import LobbyParticipantItems from './LobbyParticipantItems';
18
+
19
+const useStyles = makeStyles(theme => {
20
+    return {
21
+        drawerActions: {
22
+            listStyleType: 'none',
23
+            margin: 0,
24
+            padding: 0
25
+        },
26
+        drawerItem: {
27
+            alignItems: 'center',
28
+            color: theme.palette.text01,
29
+            display: 'flex',
30
+            padding: '12px 16px',
31
+            ...withPixelLineHeight(theme.typography.bodyShortRegularLarge),
32
+
33
+            '&:first-child': {
34
+                marginTop: '15px'
35
+
36
+            },
37
+
38
+            '&:hover': {
39
+                cursor: 'pointer',
40
+                background: theme.palette.action02
41
+            }
42
+        },
43
+        icon: {
44
+            marginRight: 16
45
+        },
46
+        headingContainer: {
47
+            alignItems: 'center',
48
+            display: 'flex',
49
+            justifyContent: 'space-between'
50
+        },
51
+        heading: {
52
+            ...withPixelLineHeight(theme.typography.heading7),
53
+            color: theme.palette.text02
54
+        },
55
+        link: {
56
+            ...withPixelLineHeight(theme.typography.labelBold),
57
+            color: theme.palette.link01,
58
+            cursor: 'pointer'
59
+        }
60
+    };
61
+});
62
+
63
+/**
64
+ * Component used to display a list of participants waiting in the lobby.
65
+ *
66
+ * @returns {ReactNode}
67
+ */
68
+export default function LobbyParticipants() {
69
+    const lobbyEnabled = useSelector(getLobbyEnabled);
70
+    const participants = useSelector(getKnockingParticipants);
71
+    const { t } = useTranslation();
72
+    const classes = useStyles();
73
+    const dispatch = useDispatch();
74
+    const admitAll = useCallback(() => {
75
+        dispatch(admitMultiple(participants));
76
+    }, [ dispatch, participants ]);
77
+    const overflowDrawer = useSelector(showOverflowDrawer);
78
+    const [ drawerParticipant, closeDrawer, openDrawerForParticipant ] = useParticipantDrawer();
79
+    const [ admit, reject ] = useLobbyActions(drawerParticipant, closeDrawer);
80
+
81
+    if (!lobbyEnabled || !participants.length) {
82
+        return null;
83
+    }
84
+
85
+    return (
86
+    <>
87
+        <div className = { classes.headingContainer }>
88
+            <div className = { classes.heading }>
89
+                {t('participantsPane.headings.lobby', { count: participants.length })}
90
+            </div>
91
+            <div
92
+                className = { classes.link }
93
+                onClick = { admitAll }>{t('lobby.admitAll')}</div>
94
+        </div>
95
+        <LobbyParticipantItems
96
+            openDrawerForParticipant = { openDrawerForParticipant }
97
+            overflowDrawer = { overflowDrawer }
98
+            participants = { participants } />
99
+        <DrawerPortal>
100
+            <Drawer
101
+                isOpen = { Boolean(drawerParticipant && overflowDrawer) }
102
+                onClose = { closeDrawer }>
103
+                <ul className = { classes.drawerActions }>
104
+                    <li className = { classes.drawerItem }>
105
+                        <Avatar
106
+                            className = { classes.icon }
107
+                            participantId = { drawerParticipant && drawerParticipant.participantID }
108
+                            size = { 20 } />
109
+                        <span>{ drawerParticipant && drawerParticipant.displayName }</span>
110
+                    </li>
111
+                    <li
112
+                        className = { classes.drawerItem }
113
+                        onClick = { admit }>
114
+                        <Icon
115
+                            className = { classes.icon }
116
+                            size = { 20 }
117
+                            src = { IconCheck } />
118
+                        <span>{ t('lobby.admit') }</span>
119
+                    </li>
120
+                    <li
121
+                        className = { classes.drawerItem }
122
+                        onClick = { reject }>
123
+                        <Icon
124
+                            className = { classes.icon }
125
+                            size = { 20 }
126
+                            src = { IconClose } />
127
+                        <span>{ t('lobby.reject')}</span>
128
+                    </li>
129
+                </ul>
130
+            </Drawer>
131
+        </DrawerPortal>
132
+    </>
133
+    );
134
+}

+ 216
- 105
react/features/participants-pane/components/web/MeetingParticipantContextMenu.js View File

@@ -1,7 +1,8 @@
1 1
 // @flow
2
-
2
+import { withStyles } from '@material-ui/core/styles';
3 3
 import React, { Component } from 'react';
4 4
 
5
+import { Avatar } from '../../../base/avatar';
5 6
 import { isToolbarButtonEnabled } from '../../../base/config/functions.web';
6 7
 import { openDialog } from '../../../base/dialog';
7 8
 import { translate } from '../../../base/i18n';
@@ -21,10 +22,13 @@ import {
21 22
     isParticipantModerator
22 23
 } from '../../../base/participants';
23 24
 import { connect } from '../../../base/redux';
25
+import { withPixelLineHeight } from '../../../base/styles/functions.web';
24 26
 import { isParticipantAudioMuted, isParticipantVideoMuted } from '../../../base/tracks';
25
-import { openChat } from '../../../chat/actions';
26
-import { stopSharedVideo } from '../../../shared-video/actions.any';
27
+import { openChatById } from '../../../chat/actions';
28
+import { setVolume } from '../../../filmstrip/actions.web';
29
+import { Drawer, DrawerPortal } from '../../../toolbox/components/web';
27 30
 import { GrantModeratorDialog, KickRemoteParticipantDialog, MuteEveryoneDialog } from '../../../video-menu';
31
+import { VolumeSlider } from '../../../video-menu/components/web';
28 32
 import MuteRemoteParticipantsVideoDialog from '../../../video-menu/components/web/MuteRemoteParticipantsVideoDialog';
29 33
 import { getComputedOuterHeight } from '../../functions';
30 34
 
@@ -73,11 +77,33 @@ type Props = {
73 77
      */
74 78
     _participant: Object,
75 79
 
80
+    /**
81
+     * A value between 0 and 1 indicating the volume of the participant's
82
+     * audio element.
83
+     */
84
+    _volume: ?number,
85
+
86
+    /**
87
+     * Closes a drawer if open.
88
+     */
89
+    closeDrawer: Function,
90
+
91
+    /**
92
+     * An object containing the CSS classes.
93
+     */
94
+    classes?: {[ key: string]: string},
95
+
76 96
     /**
77 97
      * The dispatch function from redux.
78 98
      */
79 99
     dispatch: Function,
80 100
 
101
+    /**
102
+     * The participant for which the drawer is open.
103
+     * It contains the displayName & participantID.
104
+     */
105
+    drawerParticipant: Object,
106
+
81 107
     /**
82 108
      * Callback used to open a confirmation dialog for audio muting.
83 109
      */
@@ -108,6 +134,12 @@ type Props = {
108 134
      */
109 135
     participantID: string,
110 136
 
137
+    /**
138
+     * True if an overflow drawer should be displayed.
139
+     */
140
+    overflowDrawer: boolean,
141
+
142
+
111 143
     /**
112 144
      * The translate function.
113 145
      */
@@ -122,6 +154,25 @@ type State = {
122 154
     isHidden: boolean
123 155
 };
124 156
 
157
+const styles = theme => {
158
+    return {
159
+        drawer: {
160
+            '& > div': {
161
+                ...withPixelLineHeight(theme.typography.bodyShortRegularLarge),
162
+                lineHeight: '32px',
163
+
164
+                '& svg': {
165
+                    fill: theme.palette.icon01
166
+                }
167
+            },
168
+            '&:first-child': {
169
+                marginTop: 15
170
+            }
171
+        }
172
+    };
173
+};
174
+
175
+
125 176
 /**
126 177
  * Implements the MeetingParticipantContextMenu component.
127 178
  */
@@ -146,13 +197,27 @@ class MeetingParticipantContextMenu extends Component<Props, State> {
146 197
 
147 198
         this._containerRef = React.createRef();
148 199
 
200
+        this._getCurrentParticipantId = this._getCurrentParticipantId.bind(this);
149 201
         this._onGrantModerator = this._onGrantModerator.bind(this);
150 202
         this._onKick = this._onKick.bind(this);
151 203
         this._onMuteEveryoneElse = this._onMuteEveryoneElse.bind(this);
152 204
         this._onMuteVideo = this._onMuteVideo.bind(this);
153 205
         this._onSendPrivateMessage = this._onSendPrivateMessage.bind(this);
154
-        this._onStopSharedVideo = this._onStopSharedVideo.bind(this);
155 206
         this._position = this._position.bind(this);
207
+        this._onVolumeChange = this._onVolumeChange.bind(this);
208
+    }
209
+
210
+    _getCurrentParticipantId: () => string;
211
+
212
+    /**
213
+     * Returns the participant id for the item we want to operate.
214
+     *
215
+     * @returns {void}
216
+     */
217
+    _getCurrentParticipantId() {
218
+        const { _participant, drawerParticipant, overflowDrawer } = this.props;
219
+
220
+        return overflowDrawer ? drawerParticipant?.participantID : _participant?.id;
156 221
     }
157 222
 
158 223
     _onGrantModerator: () => void;
@@ -163,10 +228,8 @@ class MeetingParticipantContextMenu extends Component<Props, State> {
163 228
      * @returns {void}
164 229
      */
165 230
     _onGrantModerator() {
166
-        const { _participant, dispatch } = this.props;
167
-
168
-        dispatch(openDialog(GrantModeratorDialog, {
169
-            participantID: _participant?.id
231
+        this.props.dispatch(openDialog(GrantModeratorDialog, {
232
+            participantID: this._getCurrentParticipantId()
170 233
         }));
171 234
     }
172 235
 
@@ -178,10 +241,8 @@ class MeetingParticipantContextMenu extends Component<Props, State> {
178 241
      * @returns {void}
179 242
      */
180 243
     _onKick() {
181
-        const { _participant, dispatch } = this.props;
182
-
183
-        dispatch(openDialog(KickRemoteParticipantDialog, {
184
-            participantID: _participant?.id
244
+        this.props.dispatch(openDialog(KickRemoteParticipantDialog, {
245
+            participantID: this._getCurrentParticipantId()
185 246
         }));
186 247
     }
187 248
 
@@ -195,7 +256,7 @@ class MeetingParticipantContextMenu extends Component<Props, State> {
195 256
     _onStopSharedVideo() {
196 257
         const { dispatch } = this.props;
197 258
 
198
-        dispatch(stopSharedVideo());
259
+        dispatch(this._onStopSharedVideo());
199 260
     }
200 261
 
201 262
     _onMuteEveryoneElse: () => void;
@@ -206,10 +267,8 @@ class MeetingParticipantContextMenu extends Component<Props, State> {
206 267
      * @returns {void}
207 268
      */
208 269
     _onMuteEveryoneElse() {
209
-        const { _participant, dispatch } = this.props;
210
-
211
-        dispatch(openDialog(MuteEveryoneDialog, {
212
-            exclude: [ _participant?.id ]
270
+        this.props.dispatch(openDialog(MuteEveryoneDialog, {
271
+            exclude: [ this._getCurrentParticipantId() ]
213 272
         }));
214 273
     }
215 274
 
@@ -221,10 +280,8 @@ class MeetingParticipantContextMenu extends Component<Props, State> {
221 280
      * @returns {void}
222 281
      */
223 282
     _onMuteVideo() {
224
-        const { _participant, dispatch } = this.props;
225
-
226
-        dispatch(openDialog(MuteRemoteParticipantsVideoDialog, {
227
-            participantID: _participant?.id
283
+        this.props.dispatch(openDialog(MuteRemoteParticipantsVideoDialog, {
284
+            participantID: this._getCurrentParticipantId()
228 285
         }));
229 286
     }
230 287
 
@@ -236,9 +293,10 @@ class MeetingParticipantContextMenu extends Component<Props, State> {
236 293
      * @returns {void}
237 294
      */
238 295
     _onSendPrivateMessage() {
239
-        const { _participant, dispatch } = this.props;
296
+        const { closeDrawer, dispatch, overflowDrawer } = this.props;
240 297
 
241
-        dispatch(openChat(_participant));
298
+        dispatch(openChatById(this._getCurrentParticipantId()));
299
+        overflowDrawer && closeDrawer();
242 300
     }
243 301
 
244 302
     _position: () => void;
@@ -270,6 +328,21 @@ class MeetingParticipantContextMenu extends Component<Props, State> {
270 328
         }
271 329
     }
272 330
 
331
+    _onVolumeChange: (number) => void;
332
+
333
+    /**
334
+     * Handles volume changes.
335
+     *
336
+     * @param {number} value - The new value for the volume.
337
+     * @returns {void}
338
+     */
339
+    _onVolumeChange(value) {
340
+        const { _participant, dispatch } = this.props;
341
+        const { id } = _participant;
342
+
343
+        dispatch(setVolume(id, value));
344
+    }
345
+
273 346
     /**
274 347
      * Implements React Component's componentDidMount.
275 348
      *
@@ -306,9 +379,14 @@ class MeetingParticipantContextMenu extends Component<Props, State> {
306 379
             _isParticipantAudioMuted,
307 380
             _localVideoOwner,
308 381
             _participant,
382
+            _volume = 1,
383
+            classes,
384
+            closeDrawer,
385
+            drawerParticipant,
309 386
             onEnter,
310 387
             onLeave,
311 388
             onSelect,
389
+            overflowDrawer,
312 390
             muteAudio,
313 391
             t
314 392
         } = this.props;
@@ -317,90 +395,116 @@ class MeetingParticipantContextMenu extends Component<Props, State> {
317 395
             return null;
318 396
         }
319 397
 
320
-        return (
321
-            <ContextMenu
322
-                className = { ignoredChildClassName }
323
-                innerRef = { this._containerRef }
324
-                isHidden = { this.state.isHidden }
325
-                onClick = { onSelect }
326
-                onMouseEnter = { onEnter }
327
-                onMouseLeave = { onLeave }>
328
-                {
329
-                    !_participant.isFakeParticipant && (
330
-                        <>
331
-                            <ContextMenuItemGroup>
398
+        const actions
399
+            = _participant.isFakeParticipant ? (
400
+                <>
401
+                    {_localVideoOwner && (
402
+                        <ContextMenuItem onClick = { this._onStopSharedVideo }>
403
+                            <ContextMenuIcon src = { IconShareVideo } />
404
+                            <span>{t('toolbar.stopSharedVideo')}</span>
405
+                        </ContextMenuItem>
406
+                    )}
407
+                </>
408
+            ) : (
409
+                <>
410
+                    {_isLocalModerator && (
411
+                        <ContextMenuItemGroup>
412
+                            <>
332 413
                                 {
333
-                                    _isLocalModerator && (
334
-                                        <>
335
-                                            {
336
-                                                !_isParticipantAudioMuted
337
-                                                && <ContextMenuItem onClick = { muteAudio(_participant) }>
338
-                                                    <ContextMenuIcon src = { IconMicDisabled } />
339
-                                                    <span>{t('dialog.muteParticipantButton')}</span>
340
-                                                </ContextMenuItem>
341
-                                            }
342
-
343
-                                            <ContextMenuItem onClick = { this._onMuteEveryoneElse }>
344
-                                                <ContextMenuIcon src = { IconMuteEveryoneElse } />
345
-                                                <span>{t('toolbar.accessibilityLabel.muteEveryoneElse')}</span>
346
-                                            </ContextMenuItem>
347
-                                        </>
348
-                                    )
414
+                                    !_isParticipantAudioMuted && overflowDrawer
415
+                                    && <ContextMenuItem onClick = { muteAudio(_participant) }>
416
+                                        <ContextMenuIcon src = { IconMicDisabled } />
417
+                                        <span>{t('dialog.muteParticipantButton')}</span>
418
+                                    </ContextMenuItem>
349 419
                                 }
350 420
 
351
-                                {
352
-                                    _isLocalModerator && (
353
-                                        _isParticipantVideoMuted || (
354
-                                            <ContextMenuItem onClick = { this._onMuteVideo }>
355
-                                                <ContextMenuIcon src = { IconVideoOff } />
356
-                                                <span>{t('participantsPane.actions.stopVideo')}</span>
357
-                                            </ContextMenuItem>
358
-                                        )
359
-                                    )
360
-                                }
361
-                            </ContextMenuItemGroup>
421
+                                <ContextMenuItem onClick = { this._onMuteEveryoneElse }>
422
+                                    <ContextMenuIcon src = { IconMuteEveryoneElse } />
423
+                                    <span>{t('toolbar.accessibilityLabel.muteEveryoneElse')}</span>
424
+                                </ContextMenuItem>
425
+                            </>
426
+
427
+                            {
428
+                                _isParticipantVideoMuted || (
429
+                                    <ContextMenuItem onClick = { this._onMuteVideo }>
430
+                                        <ContextMenuIcon src = { IconVideoOff } />
431
+                                        <span>{t('participantsPane.actions.stopVideo')}</span>
432
+                                    </ContextMenuItem>
433
+                                )
434
+                            }
435
+                        </ContextMenuItemGroup>
436
+                    )}
437
+
438
+                    <ContextMenuItemGroup>
439
+                        {
440
+                            _isLocalModerator && (
441
+                                    <>
442
+                                        {
443
+                                            !_isParticipantModerator && (
444
+                                                <ContextMenuItem onClick = { this._onGrantModerator }>
445
+                                                    <ContextMenuIcon src = { IconCrown } />
446
+                                                    <span>{t('toolbar.accessibilityLabel.grantModerator')}</span>
447
+                                                </ContextMenuItem>
448
+                                            )
449
+                                        }
450
+                                        <ContextMenuItem onClick = { this._onKick }>
451
+                                            <ContextMenuIcon src = { IconCloseCircle } />
452
+                                            <span>{ t('videothumbnail.kick') }</span>
453
+                                        </ContextMenuItem>
454
+                                    </>
455
+                            )
456
+                        }
457
+                        {
458
+                            _isChatButtonEnabled && (
459
+                                <ContextMenuItem onClick = { this._onSendPrivateMessage }>
460
+                                    <ContextMenuIcon src = { IconMessage } />
461
+                                    <span>{t('toolbar.accessibilityLabel.privateMessage')}</span>
462
+                                </ContextMenuItem>
463
+                            )
464
+                        }
465
+                    </ContextMenuItemGroup>
466
+                    { overflowDrawer && typeof _volume === 'number' && !isNaN(_volume)
467
+                        && <ContextMenuItemGroup>
468
+                            <VolumeSlider
469
+                                initialValue = { _volume }
470
+                                key = 'volume-slider'
471
+                                onChange = { this._onVolumeChange } />
472
+                        </ContextMenuItemGroup>
473
+                    }
474
+                </>
475
+            );
362 476
 
477
+        return (
478
+            <>
479
+                { !overflowDrawer
480
+                  && <ContextMenu
481
+                      className = { ignoredChildClassName }
482
+                      innerRef = { this._containerRef }
483
+                      isHidden = { this.state.isHidden }
484
+                      onClick = { onSelect }
485
+                      onMouseEnter = { onEnter }
486
+                      onMouseLeave = { onLeave }>
487
+                      { actions }
488
+                  </ContextMenu>}
489
+
490
+                <DrawerPortal>
491
+                    <Drawer
492
+                        isOpen = { drawerParticipant && overflowDrawer }
493
+                        onClose = { closeDrawer }>
494
+                        <div className = { classes && classes.drawer }>
363 495
                             <ContextMenuItemGroup>
364
-                                {
365
-                                    _isLocalModerator && (
366
-                                        <>
367
-                                            {
368
-                                                !_isParticipantModerator && (
369
-                                                    <ContextMenuItem onClick = { this._onGrantModerator }>
370
-                                                        <ContextMenuIcon src = { IconCrown } />
371
-                                                        <span>{t('toolbar.accessibilityLabel.grantModerator')}</span>
372
-                                                    </ContextMenuItem>
373
-                                                )
374
-                                            }
375
-                                            <ContextMenuItem onClick = { this._onKick }>
376
-                                                <ContextMenuIcon src = { IconCloseCircle } />
377
-                                                <span>{ t('videothumbnail.kick') }</span>
378
-                                            </ContextMenuItem>
379
-                                        </>
380
-                                    )
381
-                                }
382
-                                {
383
-                                    _isChatButtonEnabled && (
384
-                                        <ContextMenuItem onClick = { this._onSendPrivateMessage }>
385
-                                            <ContextMenuIcon src = { IconMessage } />
386
-                                            <span>{t('toolbar.accessibilityLabel.privateMessage')}</span>
387
-                                        </ContextMenuItem>
388
-                                    )
389
-                                }
496
+                                <ContextMenuItem>
497
+                                    <Avatar
498
+                                        participantId = { drawerParticipant && drawerParticipant.participantID }
499
+                                        size = { 20 } />
500
+                                    <span>{ drawerParticipant && drawerParticipant.displayName }</span>
501
+                                </ContextMenuItem>
390 502
                             </ContextMenuItemGroup>
391
-                        </>
392
-                    )
393
-                }
394
-
395
-                {
396
-                    _participant.isFakeParticipant && _localVideoOwner && (
397
-                        <ContextMenuItem onClick = { this._onStopSharedVideo }>
398
-                            <ContextMenuIcon src = { IconShareVideo } />
399
-                            <span>{t('toolbar.stopSharedVideo')}</span>
400
-                        </ContextMenuItem>
401
-                    )
402
-                }
403
-            </ContextMenu>
503
+                            { actions }
504
+                        </div>
505
+                    </Drawer>
506
+                </DrawerPortal>
507
+            </>
404 508
         );
405 509
     }
406 510
 }
@@ -414,10 +518,12 @@ class MeetingParticipantContextMenu extends Component<Props, State> {
414 518
  * @returns {Props}
415 519
  */
416 520
 function _mapStateToProps(state, ownProps): Object {
417
-    const { participantID } = ownProps;
521
+    const { participantID, overflowDrawer, drawerParticipant } = ownProps;
418 522
     const { ownerId } = state['features/shared-video'];
419 523
     const localParticipantId = getLocalParticipant(state).id;
420
-    const participant = getParticipantByIdOrUndefined(state, participantID);
524
+
525
+    const participant = getParticipantByIdOrUndefined(state,
526
+        overflowDrawer ? drawerParticipant?.participantID : participantID);
421 527
 
422 528
     const _isLocalModerator = isLocalParticipantModerator(state);
423 529
     const _isChatButtonEnabled = isToolbarButtonEnabled('chat', state);
@@ -425,6 +531,10 @@ function _mapStateToProps(state, ownProps): Object {
425 531
     const _isParticipantAudioMuted = isParticipantAudioMuted(participant, state);
426 532
     const _isParticipantModerator = isParticipantModerator(participant);
427 533
 
534
+    const { participantsVolume } = state['features/filmstrip'];
535
+    const id = participant?.id;
536
+    const isLocal = participant?.local ?? true;
537
+
428 538
     return {
429 539
         _isLocalModerator,
430 540
         _isChatButtonEnabled,
@@ -432,8 +542,9 @@ function _mapStateToProps(state, ownProps): Object {
432 542
         _isParticipantVideoMuted,
433 543
         _isParticipantAudioMuted,
434 544
         _localVideoOwner: Boolean(ownerId === localParticipantId),
435
-        _participant: participant
545
+        _participant: participant,
546
+        _volume: isLocal ? undefined : id ? participantsVolume[id] : undefined
436 547
     };
437 548
 }
438 549
 
439
-export default translate(connect(_mapStateToProps)(MeetingParticipantContextMenu));
550
+export default withStyles(styles)(translate(connect(_mapStateToProps)(MeetingParticipantContextMenu)));

+ 50
- 32
react/features/participants-pane/components/web/MeetingParticipantItem.js View File

@@ -9,8 +9,12 @@ import {
9 9
 } from '../../../base/participants';
10 10
 import { connect } from '../../../base/redux';
11 11
 import { isParticipantAudioMuted, isParticipantVideoMuted } from '../../../base/tracks';
12
-import { ACTION_TRIGGER, MEDIA_STATE, type MediaState } from '../../constants';
13
-import { getParticipantAudioMediaState, getQuickActionButtonType } from '../../functions';
12
+import { ACTION_TRIGGER, type MediaState } from '../../constants';
13
+import {
14
+    getParticipantAudioMediaState,
15
+    getParticipantVideoMediaState,
16
+    getQuickActionButtonType
17
+} from '../../functions';
14 18
 import ParticipantQuickAction from '../ParticipantQuickAction';
15 19
 
16 20
 import ParticipantItem from './ParticipantItem';
@@ -24,19 +28,20 @@ type Props = {
24 28
     _audioMediaState: MediaState,
25 29
 
26 30
     /**
27
-     * The display name of the participant.
31
+     * Media state for video.
28 32
      */
29
-    _displayName: string,
33
+    _videoMediaState: MediaState,
34
+
30 35
 
31 36
     /**
32
-     * True if the participant is video muted.
37
+     * The display name of the participant.
33 38
      */
34
-    _isVideoMuted: boolean,
39
+    _displayName: string,
35 40
 
36 41
     /**
37 42
      * True if the participant is the local participant.
38 43
      */
39
-    _local: boolean,
44
+    _local: Boolean,
40 45
 
41 46
     /**
42 47
      * Shared video local participant owner.
@@ -96,6 +101,17 @@ type Props = {
96 101
      */
97 102
     onLeave: Function,
98 103
 
104
+    /**
105
+     * Callback used to open an actions drawer for a participant.
106
+     */
107
+    openDrawerForParticipant: Function,
108
+
109
+    /**
110
+     * True if an overflow drawer should be displayed.
111
+     */
112
+    overflowDrawer: boolean,
113
+
114
+
99 115
     /**
100 116
      * The aria-label for the ellipsis action.
101 117
      */
@@ -120,20 +136,22 @@ type Props = {
120 136
  */
121 137
 function MeetingParticipantItem({
122 138
     _audioMediaState,
139
+    _videoMediaState,
123 140
     _displayName,
124
-    _isVideoMuted,
125
-    _localVideoOwner,
126 141
     _local,
142
+    _localVideoOwner,
127 143
     _participant,
128 144
     _participantID,
129 145
     _quickActionButtonType,
130 146
     _raisedHand,
131 147
     askUnmuteText,
132 148
     isHighlighted,
133
-    onContextMenu,
134
-    onLeave,
135 149
     muteAudio,
136 150
     muteParticipantButtonText,
151
+    onContextMenu,
152
+    onLeave,
153
+    openDrawerForParticipant,
154
+    overflowDrawer,
137 155
     participantActionEllipsisLabel,
138 156
     youText
139 157
 }: Props) {
@@ -145,32 +163,32 @@ function MeetingParticipantItem({
145 163
             isHighlighted = { isHighlighted }
146 164
             local = { _local }
147 165
             onLeave = { onLeave }
166
+            openDrawerForParticipant = { openDrawerForParticipant }
167
+            overflowDrawer = { overflowDrawer }
148 168
             participantID = { _participantID }
149 169
             raisedHand = { _raisedHand }
150
-            videoMuteState = { _isVideoMuted ? MEDIA_STATE.MUTED : MEDIA_STATE.UNMUTED }
170
+            videoMediaState = { _videoMediaState }
151 171
             youText = { youText }>
152
-            {
153
-                !_participant.isFakeParticipant && (
154
-                    <>
155
-                        <ParticipantQuickAction
156
-                            askUnmuteText = { askUnmuteText }
157
-                            buttonType = { _quickActionButtonType }
158
-                            muteAudio = { muteAudio }
159
-                            muteParticipantButtonText = { muteParticipantButtonText }
160
-                            participantID = { _participantID } />
161
-                        <ParticipantActionEllipsis
162
-                            aria-label = { participantActionEllipsisLabel }
163
-                            onClick = { onContextMenu } />
164
-                    </>
165
-                )
166
-            }
167
-            {
168
-                _participant.isFakeParticipant && _localVideoOwner && (
172
+
173
+            {!overflowDrawer && !_participant.isFakeParticipant
174
+                && <>
175
+                    <ParticipantQuickAction
176
+                        askUnmuteText = { askUnmuteText }
177
+                        buttonType = { _quickActionButtonType }
178
+                        muteAudio = { muteAudio }
179
+                        muteParticipantButtonText = { muteParticipantButtonText }
180
+                        participantID = { _participantID } />
169 181
                     <ParticipantActionEllipsis
170 182
                         aria-label = { participantActionEllipsisLabel }
171 183
                         onClick = { onContextMenu } />
172
-                )
184
+                 </>
173 185
             }
186
+
187
+            {!overflowDrawer && _localVideoOwner && _participant.isFakeParticipant && (
188
+                <ParticipantActionEllipsis
189
+                    aria-label = { participantActionEllipsisLabel }
190
+                    onClick = { onContextMenu } />
191
+            )}
174 192
         </ParticipantItem>
175 193
     );
176 194
 }
@@ -193,13 +211,13 @@ function _mapStateToProps(state, ownProps): Object {
193 211
     const _isAudioMuted = isParticipantAudioMuted(participant, state);
194 212
     const _isVideoMuted = isParticipantVideoMuted(participant, state);
195 213
     const _audioMediaState = getParticipantAudioMediaState(participant, _isAudioMuted, state);
214
+    const _videoMediaState = getParticipantVideoMediaState(participant, _isVideoMuted, state);
196 215
     const _quickActionButtonType = getQuickActionButtonType(participant, _isAudioMuted, state);
197 216
 
198 217
     return {
199 218
         _audioMediaState,
219
+        _videoMediaState,
200 220
         _displayName: getParticipantDisplayName(state, participant?.id),
201
-        _isAudioMuted,
202
-        _isVideoMuted,
203 221
         _local: Boolean(participant?.local),
204 222
         _localVideoOwner: Boolean(ownerId === localParticipantId),
205 223
         _participant: participant,

+ 103
- 0
react/features/participants-pane/components/web/MeetingParticipantItems.js View File

@@ -0,0 +1,103 @@
1
+// @flow
2
+
3
+import React from 'react';
4
+
5
+import MeetingParticipantItem from './MeetingParticipantItem';
6
+
7
+type Props = {
8
+
9
+    /**
10
+     * The translated ask unmute text for the qiuck action buttons.
11
+     */
12
+    askUnmuteText: string,
13
+
14
+    /**
15
+     * Callback for the mouse leaving this item
16
+     */
17
+    lowerMenu: Function,
18
+
19
+    /**
20
+     * Callback for the activation of this item's context menu
21
+     */
22
+    toggleMenu: Function,
23
+
24
+    /**
25
+     * Callback used to open a confirmation dialog for audio muting.
26
+     */
27
+    muteAudio: Function,
28
+
29
+    /**
30
+     * The translated text for the mute participant button.
31
+     */
32
+    muteParticipantButtonText: string,
33
+
34
+    /**
35
+     * The meeting participants.
36
+     */
37
+     participantIds: Array<string>,
38
+
39
+    /**
40
+     * Callback used to open an actions drawer for a participant.
41
+     */
42
+    openDrawerForParticipant: Function,
43
+
44
+    /**
45
+     * True if an overflow drawer should be displayed.
46
+     */
47
+    overflowDrawer: boolean,
48
+
49
+    /**
50
+     * The if of the participant for which the context menu should be open.
51
+     */
52
+    raiseContextId?: string,
53
+
54
+    /**
55
+     * The aria-label for the ellipsis action.
56
+     */
57
+    participantActionEllipsisLabel: string,
58
+
59
+    /**
60
+     * The translated "you" text.
61
+     */
62
+    youText: string
63
+}
64
+
65
+/**
66
+ * Component used to display a list of meeting participants.
67
+ *
68
+ * @returns {ReactNode}
69
+ */
70
+function MeetingParticipantItems({
71
+    askUnmuteText,
72
+    lowerMenu,
73
+    toggleMenu,
74
+    muteAudio,
75
+    muteParticipantButtonText,
76
+    participantIds,
77
+    openDrawerForParticipant,
78
+    overflowDrawer,
79
+    raiseContextId,
80
+    participantActionEllipsisLabel,
81
+    youText
82
+}) {
83
+    const renderParticipant = id => (
84
+        <MeetingParticipantItem
85
+            askUnmuteText = { askUnmuteText }
86
+            isHighlighted = { raiseContextId === id }
87
+            key = { id }
88
+            muteAudio = { muteAudio }
89
+            muteParticipantButtonText = { muteParticipantButtonText }
90
+            onContextMenu = { toggleMenu(id) }
91
+            onLeave = { lowerMenu }
92
+            openDrawerForParticipant = { openDrawerForParticipant }
93
+            overflowDrawer = { overflowDrawer }
94
+            participantActionEllipsisLabel = { participantActionEllipsisLabel }
95
+            participantID = { id }
96
+            youText = { youText } />
97
+    );
98
+
99
+    return participantIds.map(renderParticipant);
100
+}
101
+
102
+// Memoize the component in order to avoid rerender on drawer open/close.
103
+export default React.memo<Props>(MeetingParticipantItems);

react/features/participants-pane/components/web/MeetingParticipantList.js → react/features/participants-pane/components/web/MeetingParticipants.js View File

@@ -5,36 +5,38 @@ import { useTranslation } from 'react-i18next';
5 5
 import { useDispatch } from 'react-redux';
6 6
 
7 7
 import { isToolbarButtonEnabled } from '../../../base/config/functions.web';
8
-import { openDialog } from '../../../base/dialog';
8
+import { MEDIA_TYPE } from '../../../base/media';
9 9
 import {
10 10
     getParticipantCountWithFake,
11 11
     getSortedParticipantIds
12 12
 } from '../../../base/participants';
13 13
 import { connect } from '../../../base/redux';
14
-import MuteRemoteParticipantDialog from '../../../video-menu/components/web/MuteRemoteParticipantDialog';
14
+import { showOverflowDrawer } from '../../../toolbox/functions';
15
+import { muteRemote } from '../../../video-menu/actions.any';
15 16
 import { findStyledAncestor, shouldRenderInviteButton } from '../../functions';
17
+import { useParticipantDrawer } from '../../hooks';
16 18
 
17 19
 import { InviteButton } from './InviteButton';
18 20
 import MeetingParticipantContextMenu from './MeetingParticipantContextMenu';
19
-import MeetingParticipantItem from './MeetingParticipantItem';
21
+import MeetingParticipantItems from './MeetingParticipantItems';
20 22
 import { Heading, ParticipantContainer } from './styled';
21 23
 
22 24
 type NullProto = {
23
-  [key: string]: any,
24
-  __proto__: null
25
+    [key: string]: any,
26
+    __proto__: null
25 27
 };
26 28
 
27 29
 type RaiseContext = NullProto | {|
28 30
 
29
-  /**
30
-   * Target elements against which positioning calculations are made.
31
-   */
32
-  offsetTarget?: HTMLElement,
31
+    /**
32
+     * Target elements against which positioning calculations are made.
33
+     */
34
+    offsetTarget?: HTMLElement,
33 35
 
34
-  /**
35
-   * The ID of the participant.
36
-   */
37
-  participantID?: String,
36
+    /**
37
+     * The ID of the participant.
38
+     */
39
+    participantID ?: string,
38 40
 |};
39 41
 
40 42
 const initialState = Object.freeze(Object.create(null));
@@ -49,11 +51,11 @@ const initialState = Object.freeze(Object.create(null));
49 51
  *
50 52
  * @returns {ReactNode} - The component.
51 53
  */
52
-function MeetingParticipantList({ participantsCount, showInviteButton, sortedParticipantIds = [] }) {
54
+function MeetingParticipants({ participantsCount, showInviteButton, overflowDrawer, sortedParticipantIds = [] }) {
53 55
     const dispatch = useDispatch();
54 56
     const isMouseOverMenu = useRef(false);
55 57
 
56
-    const [ raiseContext, setRaiseContext ] = useState<RaiseContext>(initialState);
58
+    const [ raiseContext, setRaiseContext ] = useState < RaiseContext >(initialState);
57 59
     const { t } = useTranslation();
58 60
 
59 61
     const lowerMenu = useCallback(() => {
@@ -101,8 +103,9 @@ function MeetingParticipantList({ participantsCount, showInviteButton, sortedPar
101 103
     }, [ lowerMenu ]);
102 104
 
103 105
     const muteAudio = useCallback(id => () => {
104
-        dispatch(openDialog(MuteRemoteParticipantDialog, { participantID: id }));
105
-    });
106
+        dispatch(muteRemote(id, MEDIA_TYPE.AUDIO));
107
+    }, [ dispatch ]);
108
+    const [ drawerParticipant, closeDrawer, openDrawerForParticipant ] = useParticipantDrawer();
106 109
 
107 110
     // FIXME:
108 111
     // It seems that useTranslation is not very scallable. Unmount 500 components that have the useTranslation hook is
@@ -115,34 +118,35 @@ function MeetingParticipantList({ participantsCount, showInviteButton, sortedPar
115 118
     const askUnmuteText = t('participantsPane.actions.askUnmute');
116 119
     const muteParticipantButtonText = t('dialog.muteParticipantButton');
117 120
 
118
-    const renderParticipant = id => (
119
-        <MeetingParticipantItem
120
-            askUnmuteText = { askUnmuteText }
121
-            isHighlighted = { raiseContext.participantID === id }
122
-            key = { id }
123
-            muteAudio = { muteAudio }
124
-            muteParticipantButtonText = { muteParticipantButtonText }
125
-            onContextMenu = { toggleMenu(id) }
126
-            onLeave = { lowerMenu }
127
-            participantActionEllipsisLabel = { participantActionEllipsisLabel }
128
-            participantID = { id }
129
-            youText = { youText } />
130
-    );
131
-
132 121
     return (
133
-    <>
134
-        <Heading>{t('participantsPane.headings.participantsList', { count: participantsCount })}</Heading>
135
-        {showInviteButton && <InviteButton />}
136
-        <div>
137
-            {sortedParticipantIds.map(renderParticipant)}
138
-        </div>
139
-        <MeetingParticipantContextMenu
140
-            muteAudio = { muteAudio }
141
-            onEnter = { menuEnter }
142
-            onLeave = { menuLeave }
143
-            onSelect = { lowerMenu }
144
-            { ...raiseContext } />
145
-    </>
122
+        <>
123
+            <Heading>{t('participantsPane.headings.participantsList', { count: participantsCount })}</Heading>
124
+            {showInviteButton && <InviteButton />}
125
+            <div>
126
+                <MeetingParticipantItems
127
+                    askUnmuteText = { askUnmuteText }
128
+                    lowerMenu = { lowerMenu }
129
+                    muteAudio = { muteAudio }
130
+                    muteParticipantButtonText = { muteParticipantButtonText }
131
+                    openDrawerForParticipant = { openDrawerForParticipant }
132
+                    overflowDrawer = { overflowDrawer }
133
+                    participantActionEllipsisLabel = { participantActionEllipsisLabel }
134
+                    participantIds = { sortedParticipantIds }
135
+                    participantsCount = { participantsCount }
136
+                    raiseContextId = { raiseContext.participantID }
137
+                    toggleMenu = { toggleMenu }
138
+                    youText = { youText } />
139
+            </div>
140
+            <MeetingParticipantContextMenu
141
+                closeDrawer = { closeDrawer }
142
+                drawerParticipant = { drawerParticipant }
143
+                muteAudio = { muteAudio }
144
+                onEnter = { menuEnter }
145
+                onLeave = { menuLeave }
146
+                onSelect = { lowerMenu }
147
+                overflowDrawer = { overflowDrawer }
148
+                { ...raiseContext } />
149
+        </>
146 150
     );
147 151
 }
148 152
 
@@ -163,11 +167,14 @@ function _mapStateToProps(state): Object {
163 167
 
164 168
     const showInviteButton = shouldRenderInviteButton(state) && isToolbarButtonEnabled('invite', state);
165 169
 
170
+    const overflowDrawer = showOverflowDrawer(state);
171
+
166 172
     return {
167 173
         sortedParticipantIds,
168 174
         participantsCount,
169
-        showInviteButton
175
+        showInviteButton,
176
+        overflowDrawer
170 177
     };
171 178
 }
172 179
 
173
-export default connect(_mapStateToProps)(MeetingParticipantList);
180
+export default connect(_mapStateToProps)(MeetingParticipants);

+ 23
- 5
react/features/participants-pane/components/web/ParticipantItem.js View File

@@ -1,6 +1,6 @@
1 1
 // @flow
2 2
 
3
-import React, { type Node } from 'react';
3
+import React, { type Node, useCallback } from 'react';
4 4
 
5 5
 import { Avatar } from '../../../base/avatar';
6 6
 import {
@@ -61,13 +61,23 @@ type Props = {
61 61
     /**
62 62
      * True if the participant is local.
63 63
      */
64
-    local: boolean,
64
+    local: Boolean,
65
+
66
+    /**
67
+     * Opens a drawer with participant actions.
68
+     */
69
+    openDrawerForParticipant: Function,
65 70
 
66 71
     /**
67 72
      * Callback for when the mouse leaves this component
68 73
      */
69 74
     onLeave?: Function,
70 75
 
76
+    /**
77
+     * If an overflow drawer can be opened.
78
+     */
79
+    overflowDrawer?: boolean,
80
+
71 81
     /**
72 82
      * The ID of the participant.
73 83
      */
@@ -81,7 +91,7 @@ type Props = {
81 91
     /**
82 92
      * Media state for video
83 93
      */
84
-    videoMuteState: MediaState,
94
+    videoMediaState: MediaState,
85 95
 
86 96
     /**
87 97
      * The translated "you" text.
@@ -101,20 +111,28 @@ export default function ParticipantItem({
101 111
     onLeave,
102 112
     actionsTrigger = ACTION_TRIGGER.HOVER,
103 113
     audioMediaState = MEDIA_STATE.NONE,
104
-    videoMuteState = MEDIA_STATE.NONE,
114
+    videoMediaState = MEDIA_STATE.NONE,
105 115
     displayName,
106 116
     participantID,
107 117
     local,
118
+    openDrawerForParticipant,
119
+    overflowDrawer,
108 120
     raisedHand,
109 121
     youText
110 122
 }: Props) {
111 123
     const ParticipantActions = Actions[actionsTrigger];
124
+    const onClick = useCallback(
125
+        () => openDrawerForParticipant({
126
+            participantID,
127
+            displayName
128
+        }));
112 129
 
113 130
     return (
114 131
         <ParticipantContainer
115 132
             id = { `participant-item-${participantID}` }
116 133
             isHighlighted = { isHighlighted }
117 134
             local = { local }
135
+            onClick = { !local && overflowDrawer ? onClick : undefined }
118 136
             onMouseLeave = { onLeave }
119 137
             trigger = { actionsTrigger }>
120 138
             <Avatar
@@ -131,7 +149,7 @@ export default function ParticipantItem({
131 149
                 { !local && <ParticipantActions children = { children } /> }
132 150
                 <ParticipantStates>
133 151
                     { raisedHand && <RaisedHandIndicator /> }
134
-                    { VideoStateIcons[videoMuteState] }
152
+                    { VideoStateIcons[videoMediaState] }
135 153
                     { AudioStateIcons[audioMediaState] }
136 154
                 </ParticipantStates>
137 155
             </ParticipantContent>

+ 39
- 5
react/features/participants-pane/components/web/ParticipantsPane.js View File

@@ -7,14 +7,16 @@ import { openDialog } from '../../../base/dialog';
7 7
 import { translate } from '../../../base/i18n';
8 8
 import { isLocalParticipantModerator } from '../../../base/participants';
9 9
 import { connect } from '../../../base/redux';
10
+import { Drawer, DrawerPortal } from '../../../toolbox/components/web';
11
+import { showOverflowDrawer } from '../../../toolbox/functions';
10 12
 import { MuteEveryoneDialog } from '../../../video-menu/components/';
11 13
 import { close } from '../../actions';
12 14
 import { classList, findStyledAncestor, getParticipantsPaneOpen } from '../../functions';
13 15
 import theme from '../../theme.json';
14 16
 import { FooterContextMenu } from '../FooterContextMenu';
15 17
 
16
-import { LobbyParticipantList } from './LobbyParticipantList';
17
-import MeetingParticipantList from './MeetingParticipantList';
18
+import LobbyParticipants from './LobbyParticipants';
19
+import MeetingParticipants from './MeetingParticipants';
18 20
 import {
19 21
     AntiCollapse,
20 22
     Close,
@@ -31,6 +33,11 @@ import {
31 33
  */
32 34
 type Props = {
33 35
 
36
+    /**
37
+     * Whether to display the context menu  as a drawer.
38
+     */
39
+    _overflowDrawer: boolean,
40
+
34 41
     /**
35 42
      * Is the participants pane open.
36 43
      */
@@ -81,6 +88,7 @@ class ParticipantsPane extends Component<Props, State> {
81 88
 
82 89
         // Bind event handlers so they are only bound once per instance.
83 90
         this._onClosePane = this._onClosePane.bind(this);
91
+        this._onDrawerClose = this._onDrawerClose.bind(this);
84 92
         this._onKeyPress = this._onKeyPress.bind(this);
85 93
         this._onMuteAll = this._onMuteAll.bind(this);
86 94
         this._onToggleContext = this._onToggleContext.bind(this);
@@ -113,10 +121,12 @@ class ParticipantsPane extends Component<Props, State> {
113 121
      */
114 122
     render() {
115 123
         const {
124
+            _overflowDrawer,
116 125
             _paneOpen,
117 126
             _showFooter,
118 127
             t
119 128
         } = this.props;
129
+        const { contextOpen } = this.state;
120 130
 
121 131
         // when the pane is not open optimize to not
122 132
         // execute the MeetingParticipantList render for large list of participants
@@ -137,9 +147,9 @@ class ParticipantsPane extends Component<Props, State> {
137 147
                                 tabIndex = { 0 } />
138 148
                         </Header>
139 149
                         <Container>
140
-                            <LobbyParticipantList />
150
+                            <LobbyParticipants />
141 151
                             <AntiCollapse />
142
-                            <MeetingParticipantList />
152
+                            <MeetingParticipants />
143 153
                         </Container>
144 154
                         {_showFooter && (
145 155
                             <Footer>
@@ -150,12 +160,19 @@ class ParticipantsPane extends Component<Props, State> {
150 160
                                     <FooterEllipsisButton
151 161
                                         id = 'participants-pane-context-menu'
152 162
                                         onClick = { this._onToggleContext } />
153
-                                    {this.state.contextOpen
163
+                                    {this.state.contextOpen && !_overflowDrawer
154 164
                                         && <FooterContextMenu onMouseLeave = { this._onToggleContext } />}
155 165
                                 </FooterEllipsisContainer>
156 166
                             </Footer>
157 167
                         )}
158 168
                     </div>
169
+                    <DrawerPortal>
170
+                        <Drawer
171
+                            isOpen = { contextOpen && _overflowDrawer }
172
+                            onClose = { this._onDrawerClose }>
173
+                            <FooterContextMenu inDrawer = { true } />
174
+                        </Drawer>
175
+                    </DrawerPortal>
159 176
                 </div>
160 177
             </ThemeProvider>
161 178
         );
@@ -173,6 +190,20 @@ class ParticipantsPane extends Component<Props, State> {
173 190
         this.props.dispatch(close());
174 191
     }
175 192
 
193
+    _onDrawerClose: () => void
194
+
195
+    /**
196
+     * Callback for closing the drawer.
197
+     *
198
+     * @private
199
+     * @returns {void}
200
+     */
201
+    _onDrawerClose() {
202
+        this.setState({
203
+            contextOpen: false
204
+        });
205
+    }
206
+
176 207
     _onKeyPress: (Object) => void;
177 208
 
178 209
     /**
@@ -228,6 +259,8 @@ class ParticipantsPane extends Component<Props, State> {
228 259
             });
229 260
         }
230 261
     }
262
+
263
+
231 264
 }
232 265
 
233 266
 /**
@@ -245,6 +278,7 @@ function _mapStateToProps(state: Object) {
245 278
     const isPaneOpen = getParticipantsPaneOpen(state);
246 279
 
247 280
     return {
281
+        _overflowDrawer: showOverflowDrawer(state),
248 282
         _paneOpen: isPaneOpen,
249 283
         _showFooter: isPaneOpen && isLocalParticipantModerator(state)
250 284
     };

+ 0
- 2
react/features/participants-pane/components/web/index.js View File

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

+ 27
- 1
react/features/participants-pane/components/web/styled.js View File

@@ -4,6 +4,8 @@ import styled from 'styled-components';
4 4
 import { Icon, IconHorizontalPoints } from '../../../base/icons';
5 5
 import { ACTION_TRIGGER } from '../../constants';
6 6
 
7
+const MD_BREAKPOINT = '580px';
8
+
7 9
 export const ignoredChildClassName = 'ignore-child';
8 10
 
9 11
 export const AntiCollapse = styled.br`
@@ -89,7 +91,7 @@ export const ContextMenuIcon = styled(Icon).attrs({
89 91
     size: 20
90 92
 })`
91 93
   & > svg {
92
-    fill: #a4b8d1;
94
+    fill: #ffffff;
93 95
   }
94 96
 `;
95 97
 
@@ -162,6 +164,12 @@ export const FooterButton = styled(Button)`
162 164
   height: 40px;
163 165
   font-size: 15px;
164 166
   padding: 0 16px;
167
+
168
+  @media (max-width: ${MD_BREAKPOINT}) {
169
+    font-size: 16px;
170
+    height: 48px;
171
+    min-width: 48px;
172
+  }
165 173
 `;
166 174
 
167 175
 export const FooterEllipsisButton = styled(FooterButton).attrs({
@@ -188,6 +196,10 @@ export const Heading = styled.div`
188 196
   font-size: 15px;
189 197
   line-height: 24px;
190 198
   margin: 8px 0 ${props => props.theme.panePadding}px;
199
+
200
+  @media (max-width: ${MD_BREAKPOINT}) {
201
+    font-size: 16px;
202
+  }
191 203
 `;
192 204
 
193 205
 export const ParticipantActionButton = styled(Button)`
@@ -275,6 +287,11 @@ export const ParticipantContainer = styled.div`
275 287
   padding-left: ${props => props.theme.panePadding}px;
276 288
   position: relative;
277 289
 
290
+  @media (max-width: ${MD_BREAKPOINT}) {
291
+    font-size: 16px;
292
+    height: 64px;
293
+  }
294
+
278 295
   &:hover {
279 296
     ${ParticipantStates} {
280 297
       ${props => !props.local && 'display: none'};
@@ -293,6 +310,10 @@ export const ParticipantContainer = styled.div`
293 310
     & ${ParticipantContent} {
294 311
       box-shadow: none;
295 312
     }
313
+
314
+    & ${ParticipantStates} {
315
+      display: none;
316
+    }
296 317
   ${props => !props.isHighlighted && '}'}
297 318
 `;
298 319
 
@@ -306,6 +327,11 @@ export const ParticipantInviteButton = styled(Button).attrs({
306 327
   & > *:not(:last-child) {
307 328
     margin-right: 8px;
308 329
   }
330
+
331
+  @media (max-width: ${MD_BREAKPOINT}) {
332
+    font-size: 16px;
333
+    height: 48px;
334
+  }
309 335
 `;
310 336
 
311 337
 export const ParticipantName = styled.div`

+ 1
- 0
react/features/participants-pane/constants.js View File

@@ -94,6 +94,7 @@ export const AudioStateIcons: {[MediaState]: React$Element<any> | null} = {
94 94
 export const VideoStateIcons = {
95 95
     [MEDIA_STATE.FORCE_MUTED]: (
96 96
         <Icon
97
+            color = '#E04757'
97 98
             size = { 16 }
98 99
             src = { IconCameraEmptyDisabled } />
99 100
     ),

+ 24
- 2
react/features/participants-pane/functions.js View File

@@ -71,17 +71,39 @@ export function isForceMuted(participant: Object, mediaType: MediaType, state: O
71 71
  * @param {Object} participant - The participant.
72 72
  * @param {boolean} muted - The mute state of the participant.
73 73
  * @param {Object} state - The redux state.
74
+ * @param {boolean} ignoreDominantSpeaker - Whether to ignore the dominant speaker state.
74 75
  * @returns {MediaState}
75 76
  */
76 77
 export function getParticipantAudioMediaState(participant: Object, muted: Boolean, state: Object) {
77 78
     const dominantSpeaker = getDominantSpeakerParticipant(state);
78 79
 
80
+    if (muted) {
81
+        if (isForceMuted(participant, MEDIA_TYPE.AUDIO, state)) {
82
+            return MEDIA_STATE.FORCE_MUTED;
83
+        }
84
+
85
+        return MEDIA_STATE.MUTED;
86
+    }
87
+
79 88
     if (participant === dominantSpeaker) {
80 89
         return MEDIA_STATE.DOMINANT_SPEAKER;
81 90
     }
82 91
 
92
+    return MEDIA_STATE.UNMUTED;
93
+}
94
+
95
+/**
96
+ * Determines the video media state (the mic icon) for a participant.
97
+ *
98
+ * @param {Object} participant - The participant.
99
+ * @param {boolean} muted - The mute state of the participant.
100
+ * @param {Object} state - The redux state.
101
+ * @param {boolean} ignoreDominantSpeaker - Whether to ignore the dominant speaker state.
102
+ * @returns {MediaState}
103
+ */
104
+export function getParticipantVideoMediaState(participant: Object, muted: Boolean, state: Object) {
83 105
     if (muted) {
84
-        if (isForceMuted(participant, MEDIA_TYPE.AUDIO, state)) {
106
+        if (isForceMuted(participant, MEDIA_TYPE.VIDEO, state)) {
85 107
             return MEDIA_STATE.FORCE_MUTED;
86 108
         }
87 109
 
@@ -144,7 +166,7 @@ export const getParticipantsPaneOpen = (state: Object) => Boolean(getState(state
144 166
 export function getQuickActionButtonType(participant: Object, isAudioMuted: Boolean, state: Object) {
145 167
     // handled only by moderators
146 168
     if (isLocalParticipantModerator(state)) {
147
-        if (isForceMuted(participant, MEDIA_TYPE.AUDIO, state)) {
169
+        if (isForceMuted(participant, MEDIA_TYPE.AUDIO, state) || isForceMuted(participant, MEDIA_TYPE.VIDEO, state)) {
148 170
             return QUICK_ACTION_BUTTON.ASK_TO_UNMUTE;
149 171
         }
150 172
         if (!isAudioMuted) {

+ 46
- 0
react/features/participants-pane/hooks.js View File

@@ -0,0 +1,46 @@
1
+import { useCallback, useState } from 'react';
2
+import { useDispatch } from 'react-redux';
3
+
4
+import { approveKnockingParticipant, rejectKnockingParticipant } from '../lobby/actions';
5
+
6
+/**
7
+ * Hook used to create admit/reject lobby actions.
8
+ *
9
+ * @param {Object} participant - The participant for which the actions are created.
10
+ * @param {Function} closeDrawer - Callback for closing the drawer.
11
+ * @returns {Array<Function>}
12
+ */
13
+export function useLobbyActions(participant, closeDrawer) {
14
+    const dispatch = useDispatch();
15
+
16
+    return [
17
+        useCallback(e => {
18
+            e.stopPropagation();
19
+            dispatch(approveKnockingParticipant(participant && participant.participantID));
20
+            closeDrawer && closeDrawer();
21
+        }, [ dispatch, closeDrawer ]),
22
+
23
+        useCallback(() => {
24
+            dispatch(rejectKnockingParticipant(participant && participant.participantID));
25
+            closeDrawer && closeDrawer();
26
+        }, [ dispatch, closeDrawer ])
27
+    ];
28
+}
29
+
30
+/**
31
+ * Hook used to create actions & state for opening a drawer.
32
+ *
33
+ * @returns {Array<any>}
34
+ */
35
+export function useParticipantDrawer() {
36
+    const [ drawerParticipant, openDrawerForParticipant ] = useState(null);
37
+    const closeDrawer = useCallback(() => {
38
+        openDrawerForParticipant(null);
39
+    });
40
+
41
+    return [
42
+        drawerParticipant,
43
+        closeDrawer,
44
+        openDrawerForParticipant
45
+    ];
46
+}

+ 8
- 3
react/features/talk-while-muted/middleware.js View File

@@ -3,13 +3,15 @@
3 3
 import { APP_WILL_MOUNT, APP_WILL_UNMOUNT } from '../base/app';
4 4
 import { CONFERENCE_JOINED } from '../base/conference';
5 5
 import { JitsiConferenceEvents } from '../base/lib-jitsi-meet';
6
-import { setAudioMuted } from '../base/media';
6
+import { MEDIA_TYPE, setAudioMuted } from '../base/media';
7
+import { getLocalParticipant, raiseHand } from '../base/participants';
7 8
 import { MiddlewareRegistry } from '../base/redux';
8 9
 import { playSound, registerSound, unregisterSound } from '../base/sounds';
9 10
 import {
10 11
     hideNotification,
11 12
     showNotification
12 13
 } from '../notifications';
14
+import { isForceMuted } from '../participants-pane/functions';
13 15
 
14 16
 import { setCurrentNotificationUid } from './actions';
15 17
 import { TALK_WHILE_MUTED_SOUND_ID } from './constants';
@@ -41,10 +43,13 @@ MiddlewareRegistry.register(store => next => action => {
41 43
             });
42 44
         conference.on(
43 45
             JitsiConferenceEvents.TALK_WHILE_MUTED, async () => {
46
+                const state = getState();
47
+                const local = getLocalParticipant(state);
48
+                const forceMuted = isForceMuted(local, MEDIA_TYPE.AUDIO, state);
44 49
                 const notification = await dispatch(showNotification({
45 50
                     titleKey: 'toolbar.talkWhileMutedPopup',
46
-                    customActionNameKey: 'notify.unmute',
47
-                    customActionHandler: () => dispatch(setAudioMuted(false))
51
+                    customActionNameKey: forceMuted ? 'notify.raiseHandAction' : 'notify.unmute',
52
+                    customActionHandler: () => dispatch(forceMuted ? raiseHand(true) : setAudioMuted(false))
48 53
                 }));
49 54
 
50 55
                 const { soundsTalkWhileMuted } = getState()['features/base/settings'];

+ 11
- 0
react/features/toolbox/functions.web.js View File

@@ -80,3 +80,14 @@ export function isVideoSettingsButtonDisabled(state: Object) {
80 80
 export function isVideoMuteButtonDisabled(state: Object) {
81 81
     return !hasAvailableDevices(state, 'videoInput');
82 82
 }
83
+
84
+/**
85
+ * If an overflow drawer should be displayed or not.
86
+ * This is usually done for mobile devices or on narrow screens.
87
+ *
88
+ * @param {Object} state - The state from the Redux store.
89
+ * @returns {boolean}
90
+ */
91
+export function showOverflowDrawer(state: Object) {
92
+    return state['features/toolbox'].overflowDrawer;
93
+}

+ 20
- 1
react/features/video-menu/components/AbstractGrantModeratorDialog.js View File

@@ -6,7 +6,7 @@ import {
6 6
     createRemoteVideoMenuButtonEvent,
7 7
     sendAnalytics
8 8
 } from '../../analytics';
9
-import { grantModerator } from '../../base/participants';
9
+import { getParticipantById, grantModerator } from '../../base/participants';
10 10
 
11 11
 type Props = {
12 12
 
@@ -20,6 +20,11 @@ type Props = {
20 20
      */
21 21
     participantID: string,
22 22
 
23
+    /**
24
+     * The name of the remote participant to be granted moderator rights.
25
+     */
26
+    participantName: string,
27
+
23 28
     /**
24 29
      * Function to translate i18n labels.
25 30
      */
@@ -64,3 +69,17 @@ export default class AbstractGrantModeratorDialog
64 69
         return true;
65 70
     }
66 71
 }
72
+
73
+/**
74
+ * Maps (parts of) the Redux state to the associated {@code AbstractMuteEveryoneDialog}'s props.
75
+ *
76
+ * @param {Object} state - The redux state.
77
+ * @param {Object} ownProps - The properties explicitly passed to the component.
78
+ * @returns {Props}
79
+ */
80
+export function abstractMapStateToProps(state: Object, ownProps: Props) {
81
+
82
+    return {
83
+        participantName: getParticipantById(state, ownProps.participantID).name
84
+    };
85
+}

+ 2
- 4
react/features/video-menu/components/AbstractMuteButton.js View File

@@ -4,13 +4,11 @@ import {
4 4
     createRemoteVideoMenuButtonEvent,
5 5
     sendAnalytics
6 6
 } from '../../analytics';
7
-import { openDialog } from '../../base/dialog';
8 7
 import { IconMicDisabled } from '../../base/icons';
9 8
 import { MEDIA_TYPE } from '../../base/media';
10 9
 import { AbstractButton, type AbstractButtonProps } from '../../base/toolbox/components';
11 10
 import { isRemoteTrackMuted } from '../../base/tracks';
12
-
13
-import { MuteRemoteParticipantDialog } from '.';
11
+import { muteRemote } from '../actions.any';
14 12
 
15 13
 export type Props = AbstractButtonProps & {
16 14
 
@@ -61,7 +59,7 @@ export default class AbstractMuteButton extends AbstractButton<Props, *> {
61 59
                 'participant_id': participantID
62 60
             }));
63 61
 
64
-        dispatch(openDialog(MuteRemoteParticipantDialog, { participantID }));
62
+        dispatch(muteRemote(participantID, MEDIA_TYPE.AUDIO));
65 63
     }
66 64
 
67 65
     /**

+ 41
- 4
react/features/video-menu/components/AbstractMuteEveryoneDialog.js View File

@@ -2,6 +2,8 @@
2 2
 
3 3
 import React from 'react';
4 4
 
5
+import { requestDisableAudioModeration, requestEnableAudioModeration } from '../../av-moderation/actions';
6
+import { isEnabledFromState } from '../../av-moderation/functions';
5 7
 import { Dialog } from '../../base/dialog';
6 8
 import { MEDIA_TYPE } from '../../base/media';
7 9
 import { getLocalParticipant, getParticipantDisplayName } from '../../base/participants';
@@ -19,7 +21,14 @@ export type Props = AbstractProps & {
19 21
 
20 22
     content: string,
21 23
     exclude: Array<string>,
22
-    title: string
24
+    title: string,
25
+    showAdvancedModerationToggle: boolean,
26
+    isAudioModerationEnabled: boolean
27
+};
28
+
29
+type State = {
30
+    audioModerationEnabled: boolean,
31
+    content: string
23 32
 };
24 33
 
25 34
 /**
@@ -29,12 +38,33 @@ export type Props = AbstractProps & {
29 38
  *
30 39
  * @extends AbstractMuteRemoteParticipantDialog
31 40
  */
32
-export default class AbstractMuteEveryoneDialog<P: Props> extends AbstractMuteRemoteParticipantDialog<P> {
41
+export default class AbstractMuteEveryoneDialog<P: Props> extends AbstractMuteRemoteParticipantDialog<P, State> {
33 42
     static defaultProps = {
34 43
         exclude: [],
35 44
         muteLocal: false
36 45
     };
37 46
 
47
+    /**
48
+     * Initializes a new {@code AbstractMuteRemoteParticipantDialog} instance.
49
+     *
50
+     * @param {Object} props - The read-only properties with which the new
51
+     * instance is to be initialized.
52
+     */
53
+    constructor(props: P) {
54
+        super(props);
55
+
56
+        this.state = {
57
+            audioModerationEnabled: props.isAudioModerationEnabled,
58
+            content: props.content || props.t(props.isAudioModerationEnabled
59
+                ? 'dialog.muteEveryoneDialogModerationOn' : 'dialog.muteEveryoneDialog'
60
+            )
61
+        };
62
+
63
+        // Bind event handlers so they are only bound once per instance.
64
+        this._onSubmit = this._onSubmit.bind(this);
65
+        this._onToggleModeration = this._onToggleModeration.bind(this);
66
+    }
67
+
38 68
     /**
39 69
      * Implements React's {@link Component#render()}.
40 70
      *
@@ -59,6 +89,8 @@ export default class AbstractMuteEveryoneDialog<P: Props> extends AbstractMuteRe
59 89
 
60 90
     _onSubmit: () => boolean;
61 91
 
92
+    _onToggleModeration: () => void;
93
+
62 94
     /**
63 95
      * Callback to be invoked when the value of this dialog is submitted.
64 96
      *
@@ -71,6 +103,11 @@ export default class AbstractMuteEveryoneDialog<P: Props> extends AbstractMuteRe
71 103
         } = this.props;
72 104
 
73 105
         dispatch(muteAllParticipants(exclude, MEDIA_TYPE.AUDIO));
106
+        if (this.state.audioModerationEnabled) {
107
+            dispatch(requestEnableAudioModeration());
108
+        } else {
109
+            dispatch(requestDisableAudioModeration());
110
+        }
74 111
 
75 112
         return true;
76 113
     }
@@ -97,7 +134,7 @@ export function abstractMapStateToProps(state: Object, ownProps: Props) {
97 134
         content: t('dialog.muteEveryoneElseDialog'),
98 135
         title: t('dialog.muteEveryoneElseTitle', { whom })
99 136
     } : {
100
-        content: t('dialog.muteEveryoneDialog'),
101
-        title: t('dialog.muteEveryoneTitle')
137
+        title: t('dialog.muteEveryoneTitle'),
138
+        isAudioModerationEnabled: isEnabledFromState(MEDIA_TYPE.AUDIO, state)
102 139
     };
103 140
 }

+ 44
- 5
react/features/video-menu/components/AbstractMuteEveryonesVideoDialog.js View File

@@ -2,6 +2,8 @@
2 2
 
3 3
 import React from 'react';
4 4
 
5
+import { requestDisableVideoModeration, requestEnableVideoModeration } from '../../av-moderation/actions';
6
+import { isEnabledFromState } from '../../av-moderation/functions';
5 7
 import { Dialog } from '../../base/dialog';
6 8
 import { MEDIA_TYPE } from '../../base/media';
7 9
 import { getLocalParticipant, getParticipantDisplayName } from '../../base/participants';
@@ -19,7 +21,14 @@ export type Props = AbstractProps & {
19 21
 
20 22
     content: string,
21 23
     exclude: Array<string>,
22
-    title: string
24
+    title: string,
25
+    showAdvancedModerationToggle: boolean,
26
+    isVideoModerationEnabled: boolean
27
+};
28
+
29
+type State = {
30
+    moderationEnabled: boolean;
31
+    content: string;
23 32
 };
24 33
 
25 34
 /**
@@ -29,12 +38,34 @@ export type Props = AbstractProps & {
29 38
  *
30 39
  * @extends AbstractMuteRemoteParticipantsVideoDialog
31 40
  */
32
-export default class AbstractMuteEveryonesVideoDialog<P: Props> extends AbstractMuteRemoteParticipantsVideoDialog<P> {
41
+export default class AbstractMuteEveryonesVideoDialog<P: Props>
42
+    extends AbstractMuteRemoteParticipantsVideoDialog<P, State> {
33 43
     static defaultProps = {
34 44
         exclude: [],
35 45
         muteLocal: false
36 46
     };
37 47
 
48
+    /**
49
+     * Initializes a new {@code AbstractMuteRemoteParticipantsVideoDialog} instance.
50
+     *
51
+     * @param {Object} props - The read-only properties with which the new
52
+     * instance is to be initialized.
53
+     */
54
+    constructor(props: P) {
55
+        super(props);
56
+
57
+        this.state = {
58
+            moderationEnabled: props.isVideoModerationEnabled,
59
+            content: props.content || props.t(props.isVideoModerationEnabled
60
+                ? 'dialog.muteEveryonesVideoDialogModerationOn' : 'dialog.muteEveryonesVideoDialog'
61
+            )
62
+        };
63
+
64
+        // Bind event handlers so they are only bound once per instance.
65
+        this._onSubmit = this._onSubmit.bind(this);
66
+        this._onToggleModeration = this._onToggleModeration.bind(this);
67
+    }
68
+
38 69
     /**
39 70
      * Implements React's {@link Component#render()}.
40 71
      *
@@ -59,6 +90,8 @@ export default class AbstractMuteEveryonesVideoDialog<P: Props> extends Abstract
59 90
 
60 91
     _onSubmit: () => boolean;
61 92
 
93
+    _onToggleModeration: () => void;
94
+
62 95
     /**
63 96
      * Callback to be invoked when the value of this dialog is submitted.
64 97
      *
@@ -71,6 +104,11 @@ export default class AbstractMuteEveryonesVideoDialog<P: Props> extends Abstract
71 104
         } = this.props;
72 105
 
73 106
         dispatch(muteAllParticipants(exclude, MEDIA_TYPE.VIDEO));
107
+        if (this.state.moderationEnabled) {
108
+            dispatch(requestEnableVideoModeration());
109
+        } else {
110
+            dispatch(requestDisableVideoModeration());
111
+        }
74 112
 
75 113
         return true;
76 114
     }
@@ -84,7 +122,8 @@ export default class AbstractMuteEveryonesVideoDialog<P: Props> extends Abstract
84 122
  * @returns {Props}
85 123
  */
86 124
 export function abstractMapStateToProps(state: Object, ownProps: Props) {
87
-    const { exclude, t } = ownProps;
125
+    const { exclude = [], t } = ownProps;
126
+    const isVideoModerationEnabled = isEnabledFromState(MEDIA_TYPE.VIDEO, state);
88 127
 
89 128
     const whom = exclude
90 129
         // eslint-disable-next-line no-confusing-arrow
@@ -97,7 +136,7 @@ export function abstractMapStateToProps(state: Object, ownProps: Props) {
97 136
         content: t('dialog.muteEveryoneElsesVideoDialog'),
98 137
         title: t('dialog.muteEveryoneElsesVideoTitle', { whom })
99 138
     } : {
100
-        content: t('dialog.muteEveryonesVideoDialog'),
101
-        title: t('dialog.muteEveryonesVideoTitle')
139
+        title: t('dialog.muteEveryonesVideoTitle'),
140
+        isVideoModerationEnabled
102 141
     };
103 142
 }

+ 2
- 2
react/features/video-menu/components/AbstractMuteRemoteParticipantDialog.js View File

@@ -32,8 +32,8 @@ export type Props = {
32 32
  *
33 33
  * @extends Component
34 34
  */
35
-export default class AbstractMuteRemoteParticipantDialog<P:Props = Props>
36
-    extends Component<P> {
35
+export default class AbstractMuteRemoteParticipantDialog<P:Props = Props, State=void>
36
+    extends Component<P, State> {
37 37
     /**
38 38
      * Initializes a new {@code AbstractMuteRemoteParticipantDialog} instance.
39 39
      *

+ 2
- 2
react/features/video-menu/components/AbstractMuteRemoteParticipantsVideoDialog.js View File

@@ -32,8 +32,8 @@ export type Props = {
32 32
  *
33 33
  * @extends Component
34 34
  */
35
-export default class AbstractMuteRemoteParticipantsVideoDialog<P:Props = Props>
36
-    extends Component<P> {
35
+export default class AbstractMuteRemoteParticipantsVideoDialog<P:Props = Props, State=void>
36
+    extends Component<P, State> {
37 37
     /**
38 38
      * Initializes a new {@code AbstractMuteRemoteParticipantsVideoDialog} instance.
39 39
      *

+ 0
- 32
react/features/video-menu/components/native/MuteRemoteParticipantDialog.js View File

@@ -1,32 +0,0 @@
1
-// @flow
2
-
3
-import React from 'react';
4
-
5
-import { ConfirmDialog } from '../../../base/dialog';
6
-import { translate } from '../../../base/i18n';
7
-import { connect } from '../../../base/redux';
8
-import AbstractMuteRemoteParticipantDialog
9
-    from '../AbstractMuteRemoteParticipantDialog';
10
-
11
-/**
12
- * Dialog to confirm a remote participant mute action.
13
- */
14
-class MuteRemoteParticipantDialog extends AbstractMuteRemoteParticipantDialog {
15
-    /**
16
-     * Implements React's {@link Component#render()}.
17
-     *
18
-     * @inheritdoc
19
-     * @returns {ReactElement}
20
-     */
21
-    render() {
22
-        return (
23
-            <ConfirmDialog
24
-                contentKey = 'dialog.muteParticipantDialog'
25
-                onSubmit = { this._onSubmit } />
26
-        );
27
-    }
28
-
29
-    _onSubmit: () => boolean;
30
-}
31
-
32
-export default translate(connect()(MuteRemoteParticipantDialog));

+ 0
- 1
react/features/video-menu/components/native/index.js View File

@@ -5,7 +5,6 @@ export { default as GrantModeratorDialog } from './GrantModeratorDialog';
5 5
 export { default as KickRemoteParticipantDialog } from './KickRemoteParticipantDialog';
6 6
 export { default as MuteEveryoneDialog } from './MuteEveryoneDialog';
7 7
 export { default as MuteEveryonesVideoDialog } from './MuteEveryonesVideoDialog';
8
-export { default as MuteRemoteParticipantDialog } from './MuteRemoteParticipantDialog';
9 8
 export { default as MuteRemoteParticipantsVideoDialog } from './MuteRemoteParticipantsVideoDialog';
10 9
 export { default as RemoteVideoMenu } from './RemoteVideoMenu';
11 10
 export { default as SharedVideoMenu } from './SharedVideoMenu';

+ 3
- 3
react/features/video-menu/components/web/GrantModeratorDialog.js View File

@@ -6,7 +6,7 @@ import { Dialog } from '../../../base/dialog';
6 6
 import { translate } from '../../../base/i18n';
7 7
 import { connect } from '../../../base/redux';
8 8
 import AbstractGrantModeratorDialog
9
-    from '../AbstractGrantModeratorDialog';
9
+, { abstractMapStateToProps } from '../AbstractGrantModeratorDialog';
10 10
 
11 11
 /**
12 12
  * Dialog to confirm a grant moderator action.
@@ -26,7 +26,7 @@ class GrantModeratorDialog extends AbstractGrantModeratorDialog {
26 26
                 titleKey = 'dialog.grantModeratorTitle'
27 27
                 width = 'small'>
28 28
                 <div>
29
-                    { this.props.t('dialog.grantModeratorDialog') }
29
+                    { this.props.t('dialog.grantModeratorDialog', { participantName: this.props.participantName }) }
30 30
                 </div>
31 31
             </Dialog>
32 32
         );
@@ -35,4 +35,4 @@ class GrantModeratorDialog extends AbstractGrantModeratorDialog {
35 35
     _onSubmit: () => boolean;
36 36
 }
37 37
 
38
-export default translate(connect()(GrantModeratorDialog));
38
+export default translate(connect(abstractMapStateToProps)(GrantModeratorDialog));

+ 36
- 3
react/features/video-menu/components/web/MuteEveryoneDialog.js View File

@@ -4,8 +4,10 @@ import React from 'react';
4 4
 
5 5
 import { Dialog } from '../../../base/dialog';
6 6
 import { translate } from '../../../base/i18n';
7
+import { Switch } from '../../../base/react';
7 8
 import { connect } from '../../../base/redux';
8
-import AbstractMuteEveryoneDialog, { abstractMapStateToProps, type Props } from '../AbstractMuteEveryoneDialog';
9
+import AbstractMuteEveryoneDialog, { abstractMapStateToProps, type Props }
10
+    from '../AbstractMuteEveryoneDialog';
9 11
 
10 12
 /**
11 13
  * A React Component with the contents for a dialog that asks for confirmation
@@ -14,6 +16,23 @@ import AbstractMuteEveryoneDialog, { abstractMapStateToProps, type Props } from
14 16
  * @extends AbstractMuteEveryoneDialog
15 17
  */
16 18
 class MuteEveryoneDialog extends AbstractMuteEveryoneDialog<Props> {
19
+
20
+    /**
21
+     * Toggles advanced moderation switch.
22
+     *
23
+     * @returns {void}
24
+     */
25
+    _onToggleModeration() {
26
+        this.setState(state => {
27
+            return {
28
+                audioModerationEnabled: !state.audioModerationEnabled,
29
+                content: this.props.t(state.audioModerationEnabled
30
+                    ? 'dialog.muteEveryoneDialog' : 'dialog.muteEveryoneDialogModerationOn'
31
+                )
32
+            };
33
+        });
34
+    }
35
+
17 36
     /**
18 37
      * Implements React's {@link Component#render()}.
19 38
      *
@@ -27,8 +46,22 @@ class MuteEveryoneDialog extends AbstractMuteEveryoneDialog<Props> {
27 46
                 onSubmit = { this._onSubmit }
28 47
                 titleString = { this.props.title }
29 48
                 width = 'small'>
30
-                <div>
31
-                    { this.props.content }
49
+                <div className = 'mute-dialog'>
50
+                    { this.state.content }
51
+                    {this.props.exclude.length === 0 && (
52
+                        <>
53
+                            <div className = 'separator-line' />
54
+                            <div className = 'control-row'>
55
+                                <label htmlFor = 'moderation-switch'>
56
+                                    {this.props.t('dialog.moderationAudioLabel')}
57
+                                </label>
58
+                                <Switch
59
+                                    id = 'moderation-switch'
60
+                                    onValueChange = { this._onToggleModeration }
61
+                                    value = { !this.state.audioModerationEnabled } />
62
+                            </div>
63
+                        </>
64
+                    )}
32 65
                 </div>
33 66
             </Dialog>
34 67
         );

+ 34
- 2
react/features/video-menu/components/web/MuteEveryonesVideoDialog.js View File

@@ -4,6 +4,7 @@ import React from 'react';
4 4
 
5 5
 import { Dialog } from '../../../base/dialog';
6 6
 import { translate } from '../../../base/i18n';
7
+import { Switch } from '../../../base/react';
7 8
 import { connect } from '../../../base/redux';
8 9
 import AbstractMuteEveryonesVideoDialog, { abstractMapStateToProps, type Props }
9 10
     from '../AbstractMuteEveryonesVideoDialog';
@@ -15,6 +16,23 @@ import AbstractMuteEveryonesVideoDialog, { abstractMapStateToProps, type Props }
15 16
  * @extends AbstractMuteEveryonesVideoDialog
16 17
  */
17 18
 class MuteEveryonesVideoDialog extends AbstractMuteEveryonesVideoDialog<Props> {
19
+
20
+    /**
21
+     * Toggles advanced moderation switch.
22
+     *
23
+     * @returns {void}
24
+     */
25
+    _onToggleModeration() {
26
+        this.setState(state => {
27
+            return {
28
+                moderationEnabled: !state.moderationEnabled,
29
+                content: this.props.t(state.moderationEnabled
30
+                    ? 'dialog.muteEveryonesVideoDialog' : 'dialog.muteEveryonesVideoDialogModerationOn'
31
+                )
32
+            };
33
+        });
34
+    }
35
+
18 36
     /**
19 37
      * Implements React's {@link Component#render()}.
20 38
      *
@@ -28,8 +46,22 @@ class MuteEveryonesVideoDialog extends AbstractMuteEveryonesVideoDialog<Props> {
28 46
                 onSubmit = { this._onSubmit }
29 47
                 titleString = { this.props.title }
30 48
                 width = 'small'>
31
-                <div>
32
-                    { this.props.content }
49
+                <div className = 'mute-dialog'>
50
+                    {this.state.content}
51
+                    {this.props.exclude.length === 0 && (
52
+                        <>
53
+                            <div className = 'separator-line' />
54
+                            <div className = 'control-row'>
55
+                                <label htmlFor = 'moderation-switch'>
56
+                                    {this.props.t('dialog.moderationVideoLabel')}
57
+                                </label>
58
+                                <Switch
59
+                                    id = 'moderation-switch'
60
+                                    onValueChange = { this._onToggleModeration }
61
+                                    value = { !this.state.moderationEnabled } />
62
+                            </div>
63
+                        </>
64
+                    )}
33 65
                 </div>
34 66
             </Dialog>
35 67
         );

+ 0
- 41
react/features/video-menu/components/web/MuteRemoteParticipantDialog.js View File

@@ -1,41 +0,0 @@
1
-/* @flow */
2
-
3
-import React from 'react';
4
-
5
-import { Dialog } from '../../../base/dialog';
6
-import { translate } from '../../../base/i18n';
7
-import { connect } from '../../../base/redux';
8
-import AbstractMuteRemoteParticipantDialog
9
-    from '../AbstractMuteRemoteParticipantDialog';
10
-
11
-/**
12
- * A React Component with the contents for a dialog that asks for confirmation
13
- * from the user before muting a remote participant.
14
- *
15
- * @extends Component
16
- */
17
-class MuteRemoteParticipantDialog extends AbstractMuteRemoteParticipantDialog {
18
-    /**
19
-     * Implements React's {@link Component#render()}.
20
-     *
21
-     * @inheritdoc
22
-     * @returns {ReactElement}
23
-     */
24
-    render() {
25
-        return (
26
-            <Dialog
27
-                okKey = 'dialog.muteParticipantButton'
28
-                onSubmit = { this._onSubmit }
29
-                titleKey = 'dialog.muteParticipantTitle'
30
-                width = 'small'>
31
-                <div>
32
-                    { this.props.t('dialog.muteParticipantBody') }
33
-                </div>
34
-            </Dialog>
35
-        );
36
-    }
37
-
38
-    _onSubmit: () => boolean;
39
-}
40
-
41
-export default translate(connect()(MuteRemoteParticipantDialog));

+ 0
- 1
react/features/video-menu/components/web/index.js View File

@@ -11,7 +11,6 @@ export { default as MuteEveryoneDialog } from './MuteEveryoneDialog';
11 11
 export { default as MuteEveryonesVideoDialog } from './MuteEveryonesVideoDialog';
12 12
 export { default as MuteEveryoneElseButton } from './MuteEveryoneElseButton';
13 13
 export { default as MuteEveryoneElsesVideoButton } from './MuteEveryoneElsesVideoButton';
14
-export { default as MuteRemoteParticipantDialog } from './MuteRemoteParticipantDialog';
15 14
 export { default as MuteRemoteParticipantsVideoDialog } from './MuteRemoteParticipantsVideoDialog';
16 15
 export { default as PrivateMessageMenuButton } from './PrivateMessageMenuButton';
17 16
 export { REMOTE_CONTROL_MENU_STATES, default as RemoteControlButton } from './RemoteControlButton';

Loading…
Cancel
Save