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

+ 1
- 0
css/main.scss View File

98
 @import 'country-picker';
98
 @import 'country-picker';
99
 @import 'modals/invite/invite_more';
99
 @import 'modals/invite/invite_more';
100
 @import 'modals/security/security';
100
 @import 'modals/security/security';
101
+@import 'modals/mute/mute-dialog';
101
 @import 'e2ee';
102
 @import 'e2ee';
102
 @import 'responsive';
103
 @import 'responsive';
103
 @import 'drawer';
104
 @import 'drawer';

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

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
         "embedMeeting": "Embed meeting",
216
         "embedMeeting": "Embed meeting",
217
         "error": "Error",
217
         "error": "Error",
218
         "gracefulShutdown": "Our service is currently down for maintenance. Please try again later.",
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
         "hideShareAudioHelper": "Don't show this dialog again",
221
         "hideShareAudioHelper": "Don't show this dialog again",
222
         "IamHost": "I am the host",
222
         "IamHost": "I am the host",
223
         "incorrectRoomLockPassword": "Incorrect password",
223
         "incorrectRoomLockPassword": "Incorrect password",
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.",
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
         "micTimeoutError": "Could not start audio source. Timeout occured!",
248
         "micTimeoutError": "Could not start audio source. Timeout occured!",
249
         "micUnknownError": "Cannot use microphone for an unknown reason.",
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
         "muteEveryoneElseDialog": "Once muted, you won't be able to unmute them, but they can unmute themselves at any time.",
252
         "muteEveryoneElseDialog": "Once muted, you won't be able to unmute them, but they can unmute themselves at any time.",
251
         "muteEveryoneElseTitle": "Mute everyone except {{whom}}?",
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
         "muteEveryoneTitle": "Mute everyone?",
256
         "muteEveryoneTitle": "Mute everyone?",
254
         "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.",
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
         "muteEveryonesVideoDialogOk": "Disable",
261
         "muteEveryonesVideoDialogOk": "Disable",
258
-        "muteEveryonesVideoTitle": "Disable everyone's camera?",
262
+        "muteEveryonesVideoTitle": "Stop everyone's video?",
259
         "muteEveryoneSelf": "yourself",
263
         "muteEveryoneSelf": "yourself",
260
         "muteEveryoneStartMuted": "Everyone starts muted from now on",
264
         "muteEveryoneStartMuted": "Everyone starts muted from now on",
261
         "muteParticipantBody": "You won't be able to unmute them, but they can unmute themselves at any time.",
265
         "muteParticipantBody": "You won't be able to unmute them, but they can unmute themselves at any time.",
263
         "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.",
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
         "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.",
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
         "muteParticipantTitle": "Mute this participant?",
269
         "muteParticipantTitle": "Mute this participant?",
266
-        "muteParticipantsVideoButton": "Disable camera",
270
+        "muteParticipantsVideoButton": "Stop camera",
267
         "muteParticipantsVideoTitle": "Disable camera of this participant?",
271
         "muteParticipantsVideoTitle": "Disable camera of this participant?",
268
         "muteParticipantsVideoBody": "You won't be able to turn the camera back on, but they can turn it back on at any time.",
272
         "muteParticipantsVideoBody": "You won't be able to turn the camera back on, but they can turn it back on at any time.",
269
         "noDropboxToken": "No valid Dropbox token",
273
         "noDropboxToken": "No valid Dropbox token",
542
     "lockRoomPasswordUppercase": "Password",
546
     "lockRoomPasswordUppercase": "Password",
543
     "me": "me",
547
     "me": "me",
544
     "notify": {
548
     "notify": {
549
+        "allowAction": "Allow",
550
+        "allowedUnmute": "You can unmute your microphone, start your camera or share your screen.",
545
         "connectedOneMember": "{{name}} joined the meeting",
551
         "connectedOneMember": "{{name}} joined the meeting",
546
         "connectedThreePlusMembers": "{{name}} and many others joined the meeting",
552
         "connectedThreePlusMembers": "{{name}} and many others joined the meeting",
547
         "connectedTwoMembers": "{{first}} and {{second}} joined the meeting",
553
         "connectedTwoMembers": "{{first}} and {{second}} joined the meeting",
548
         "disconnected": "disconnected",
554
         "disconnected": "disconnected",
549
         "focus": "Conference focus",
555
         "focus": "Conference focus",
550
         "focusFail": "{{component}} not available - retry in {{ms}} sec",
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
         "invitedOneMember": "{{name}} has been invited",
558
         "invitedOneMember": "{{name}} has been invited",
554
         "invitedThreePlusMembers": "{{name}} and {{count}} others have been invited",
559
         "invitedThreePlusMembers": "{{name}} and {{count}} others have been invited",
555
         "invitedTwoMembers": "{{first}} and {{second}} have been invited",
560
         "invitedTwoMembers": "{{first}} and {{second}} have been invited",
556
         "kickParticipant": "{{kicked}} was kicked by {{kicker}}",
561
         "kickParticipant": "{{kicked}} was kicked by {{kicker}}",
557
         "me": "Me",
562
         "me": "Me",
558
-        "moderator": "Moderator rights granted!",
563
+        "moderator": "You're now a moderator",
559
         "muted": "You have started the conversation muted.",
564
         "muted": "You have started the conversation muted.",
560
         "mutedTitle": "You're muted!",
565
         "mutedTitle": "You're muted!",
561
-        "mutedRemotelyTitle": "You have been muted by {{participantDisplayName}}!",
566
+        "mutedRemotelyTitle": "You've been muted by the moderator",
562
         "mutedRemotelyDescription": "You can always unmute when you're ready to speak. Mute back when you're done to keep noise away from the meeting.",
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
         "videoMutedRemotelyDescription": "You can always turn it on again.",
569
         "videoMutedRemotelyDescription": "You can always turn it on again.",
565
         "passwordRemovedRemotely": "$t(lockRoomPasswordUppercase) removed by another participant",
570
         "passwordRemovedRemotely": "$t(lockRoomPasswordUppercase) removed by another participant",
566
         "passwordSetRemotely": "$t(lockRoomPasswordUppercase) set by another participant",
571
         "passwordSetRemotely": "$t(lockRoomPasswordUppercase) set by another participant",
567
-        "raisedHand": "{{name}} would like to speak.",
572
+        "raisedHand": "Would like to speak.",
568
         "screenShareNoAudio": " Share audio box was not checked in the window selection screen.",
573
         "screenShareNoAudio": " Share audio box was not checked in the window selection screen.",
569
         "screenShareNoAudioTitle": "Couldn't share system audio!",
574
         "screenShareNoAudioTitle": "Couldn't share system audio!",
570
         "somebody": "Somebody",
575
         "somebody": "Somebody",
580
         "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 ",
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
         "oldElectronClientDescription2": "latest build",
586
         "oldElectronClientDescription2": "latest build",
582
         "oldElectronClientDescription3": " now!",
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
         "moderationRequestFromModerator": "The host would like you to unmute",
594
         "moderationRequestFromModerator": "The host would like you to unmute",
590
         "moderationRequestFromParticipant": "Wants to speak",
595
         "moderationRequestFromParticipant": "Wants to speak",
591
         "moderationStartedTitle": "Moderation started",
596
         "moderationStartedTitle": "Moderation started",
605
         },
610
         },
606
         "actions": {
611
         "actions": {
607
             "allow": "Allow attendees to:",
612
             "allow": "Allow attendees to:",
613
+            "audioModeration": "Unmute themselves",
608
             "blockEveryoneMicCamera": "Block everyone's mic and camera",
614
             "blockEveryoneMicCamera": "Block everyone's mic and camera",
609
             "invite": "Invite Someone",
615
             "invite": "Invite Someone",
610
             "askUnmute": "Ask to unmute",
616
             "askUnmute": "Ask to unmute",
611
             "mute": "Mute",
617
             "mute": "Mute",
612
             "muteAll": "Mute all",
618
             "muteAll": "Mute all",
613
             "muteEveryoneElse": "Mute everyone else",
619
             "muteEveryoneElse": "Mute everyone else",
614
-            "startModeration": "Unmute themselves or start video",
615
             "stopEveryonesVideo": "Stop everyone's video",
620
             "stopEveryonesVideo": "Stop everyone's video",
616
             "stopVideo": "Stop video",
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
     "passwordSetRemotely": "Set by another participant",
626
     "passwordSetRemotely": "Set by another participant",
868
             "embedMeeting": "Embed meeting",
874
             "embedMeeting": "Embed meeting",
869
             "feedback": "Leave feedback",
875
             "feedback": "Leave feedback",
870
             "fullScreen": "Toggle full screen",
876
             "fullScreen": "Toggle full screen",
871
-            "grantModerator": "Grant Moderator",
877
+            "grantModerator": "Grant Moderator Rights",
872
             "hangup": "Leave the meeting",
878
             "hangup": "Leave the meeting",
873
             "help": "Help",
879
             "help": "Help",
874
             "invite": "Invite people",
880
             "invite": "Invite people",
1054
         "domuteOthers": "Mute everyone else",
1060
         "domuteOthers": "Mute everyone else",
1055
         "domuteVideoOfOthers": "Disable camera of everyone else",
1061
         "domuteVideoOfOthers": "Disable camera of everyone else",
1056
         "flip": "Flip",
1062
         "flip": "Flip",
1057
-        "grantModerator": "Grant Moderator",
1063
+        "grantModerator": "Grant Moderator Rights",
1058
         "kick": "Kick out",
1064
         "kick": "Kick out",
1059
         "moderator": "Moderator",
1065
         "moderator": "Moderator",
1060
         "mute": "Participant is muted",
1066
         "mute": "Participant is muted",

+ 1
- 0
package.json View File

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

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

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
  * The type of (redux) action which signals that the local participant had been approved.
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
     LOCAL_PARTICIPANT_MODERATION_NOTIFICATION,
11
     LOCAL_PARTICIPANT_MODERATION_NOTIFICATION,
12
     PARTICIPANT_APPROVED,
12
     PARTICIPANT_APPROVED,
13
     PARTICIPANT_PENDING_AUDIO,
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
 } from './actionTypes';
18
 } from './actionTypes';
19
+import { isEnabledFromState } from './functions';
17
 
20
 
18
 /**
21
 /**
19
  * Action used by moderator to approve audio and video for a participant.
22
  * Action used by moderator to approve audio and video for a participant.
22
  * @returns {void}
25
  * @returns {void}
23
  */
26
  */
24
 export const approveParticipant = (id: string) => (dispatch: Function, getState: Function) => {
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
 };
97
 };
90
 
98
 
91
 /**
99
 /**
92
- * Requests disable of audio and video moderation.
100
+ * Requests disable of audio moderation.
93
  *
101
  *
94
  * @returns {{
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
     return {
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
  * @returns {{
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
     return {
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
-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
 import { MEDIA_TYPE } from '../base/media';
6
 import { MEDIA_TYPE } from '../base/media';
7
 import {
7
 import {
8
     getLocalParticipant,
8
     getLocalParticipant,
9
-    getParticipantDisplayName,
10
     getRemoteParticipants,
9
     getRemoteParticipants,
11
     isLocalParticipantModerator,
10
     isLocalParticipantModerator,
12
     isParticipantModerator,
11
     isParticipantModerator,
16
 import { MiddlewareRegistry, StateListenerRegistry } from '../base/redux';
15
 import { MiddlewareRegistry, StateListenerRegistry } from '../base/redux';
17
 import {
16
 import {
18
     hideNotification,
17
     hideNotification,
19
-    NOTIFICATION_TIMEOUT,
20
     showNotification
18
     showNotification
21
 } from '../notifications';
19
 } from '../notifications';
20
+import { muteLocal } from '../video-menu/actions.any';
22
 
21
 
23
 import {
22
 import {
24
-    DISABLE_MODERATION,
25
-    ENABLE_MODERATION,
26
     LOCAL_PARTICIPANT_MODERATION_NOTIFICATION,
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
 } from './actionTypes';
28
 } from './actionTypes';
30
 import {
29
 import {
31
     disableModeration,
30
     disableModeration,
47
 const CS_MODERATION_NOTIFICATION_ID = 'video-moderation';
46
 const CS_MODERATION_NOTIFICATION_ID = 'video-moderation';
48
 
47
 
49
 MiddlewareRegistry.register(({ dispatch, getState }) => next => action => {
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
     switch (type) {
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
     case LOCAL_PARTICIPANT_MODERATION_NOTIFICATION: {
53
     case LOCAL_PARTICIPANT_MODERATION_NOTIFICATION: {
74
         let descriptionKey;
54
         let descriptionKey;
75
         let titleKey;
55
         let titleKey;
78
         switch (action.mediaType) {
58
         switch (action.mediaType) {
79
         case MEDIA_TYPE.AUDIO: {
59
         case MEDIA_TYPE.AUDIO: {
80
             titleKey = 'notify.moderationInEffectTitle';
60
             titleKey = 'notify.moderationInEffectTitle';
81
-            descriptionKey = 'notify.moderationInEffectDescription';
82
             uid = AUDIO_MODERATION_NOTIFICATION_ID;
61
             uid = AUDIO_MODERATION_NOTIFICATION_ID;
83
             break;
62
             break;
84
         }
63
         }
85
         case MEDIA_TYPE.VIDEO: {
64
         case MEDIA_TYPE.VIDEO: {
86
             titleKey = 'notify.moderationInEffectVideoTitle';
65
             titleKey = 'notify.moderationInEffectVideoTitle';
87
-            descriptionKey = 'notify.moderationInEffectVideoDescription';
88
             uid = VIDEO_MODERATION_NOTIFICATION_ID;
66
             uid = VIDEO_MODERATION_NOTIFICATION_ID;
89
             break;
67
             break;
90
         }
68
         }
91
         case MEDIA_TYPE.PRESENTER: {
69
         case MEDIA_TYPE.PRESENTER: {
92
             titleKey = 'notify.moderationInEffectCSTitle';
70
             titleKey = 'notify.moderationInEffectCSTitle';
93
-            descriptionKey = 'notify.moderationInEffectCSDescription';
94
             uid = CS_MODERATION_NOTIFICATION_ID;
71
             uid = CS_MODERATION_NOTIFICATION_ID;
95
             break;
72
             break;
96
         }
73
         }
110
 
87
 
111
         break;
88
         break;
112
     }
89
     }
113
-    case REQUEST_DISABLE_MODERATION: {
114
-        const { conference } = getConferenceState(getState());
115
-
90
+    case REQUEST_DISABLE_AUDIO_MODERATION: {
116
         conference.disableAVModeration(MEDIA_TYPE.AUDIO);
91
         conference.disableAVModeration(MEDIA_TYPE.AUDIO);
92
+        break;
93
+    }
94
+    case REQUEST_DISABLE_VIDEO_MODERATION: {
117
         conference.disableAVModeration(MEDIA_TYPE.VIDEO);
95
         conference.disableAVModeration(MEDIA_TYPE.VIDEO);
118
         break;
96
         break;
119
     }
97
     }
120
-    case REQUEST_ENABLE_MODERATION: {
121
-        const { conference } = getConferenceState(getState());
122
-
98
+    case REQUEST_ENABLE_AUDIO_MODERATION: {
123
         conference.enableAVModeration(MEDIA_TYPE.AUDIO);
99
         conference.enableAVModeration(MEDIA_TYPE.AUDIO);
100
+        break;
101
+    }
102
+    case REQUEST_ENABLE_VIDEO_MODERATION: {
124
         conference.enableAVModeration(MEDIA_TYPE.VIDEO);
103
         conference.enableAVModeration(MEDIA_TYPE.VIDEO);
125
         break;
104
         break;
126
     }
105
     }
174
 
153
 
175
                 // Audio & video moderation are both enabled at the same time.
154
                 // Audio & video moderation are both enabled at the same time.
176
                 // Avoid displaying 2 different notifications.
155
                 // Avoid displaying 2 different notifications.
177
-                if (mediaType === MEDIA_TYPE.VIDEO) {
156
+                if (mediaType === MEDIA_TYPE.AUDIO) {
178
                     dispatch(showNotification({
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
-<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
 <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" />
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
 </svg>
3
 </svg>

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

8
     sendAnalytics
8
     sendAnalytics
9
 } from '../../analytics';
9
 } from '../../analytics';
10
 import { APP_STATE_CHANGED } from '../../mobile/background';
10
 import { APP_STATE_CHANGED } from '../../mobile/background';
11
+import { isForceMuted } from '../../participants-pane/functions';
11
 import { SET_AUDIO_ONLY, setAudioOnly } from '../audio-only';
12
 import { SET_AUDIO_ONLY, setAudioOnly } from '../audio-only';
12
 import { isRoomValid, SET_ROOM } from '../conference';
13
 import { isRoomValid, SET_ROOM } from '../conference';
14
+import { getLocalParticipant } from '../participants';
13
 import { MiddlewareRegistry } from '../redux';
15
 import { MiddlewareRegistry } from '../redux';
14
 import { getPropertyValue } from '../settings';
16
 import { getPropertyValue } from '../settings';
15
 import { isLocalVideoTrackDesktop, setTrackMuted, TRACK_ADDED } from '../tracks';
17
 import { isLocalVideoTrackDesktop, setTrackMuted, TRACK_ADDED } from '../tracks';
16
 
18
 
19
+import { SET_AUDIO_MUTED, SET_VIDEO_MUTED } from './actionTypes';
17
 import { setAudioMuted, setCameraFacingMode, setVideoMuted } from './actions';
20
 import { setAudioMuted, setCameraFacingMode, setVideoMuted } from './actions';
18
 import {
21
 import {
19
     CAMERA_FACING_MODE,
22
     CAMERA_FACING_MODE,
55
 
58
 
56
         return result;
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
     return next(action);
83
     return next(action);

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

180
  * }
180
  * }
181
  */
181
  */
182
 export const LOCAL_PARTICIPANT_RAISE_HAND = 'LOCAL_PARTICIPANT_RAISE_HAND';
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
     PARTICIPANT_LEFT,
15
     PARTICIPANT_LEFT,
16
     PARTICIPANT_UPDATED,
16
     PARTICIPANT_UPDATED,
17
     PIN_PARTICIPANT,
17
     PIN_PARTICIPANT,
18
-    SET_LOADABLE_AVATAR_URL
18
+    SET_LOADABLE_AVATAR_URL,
19
+    RAISE_HAND_UPDATED
19
 } from './actionTypes';
20
 } from './actionTypes';
20
 import {
21
 import {
21
     DISCO_REMOTE_CONTROL_FEATURE
22
     DISCO_REMOTE_CONTROL_FEATURE
465
  * @returns {Promise}
466
  * @returns {Promise}
466
  */
467
  */
467
 export function participantMutedUs(participant, track) {
468
 export function participantMutedUs(participant, track) {
468
-    return (dispatch, getState) => {
469
+    return dispatch => {
469
         if (!participant) {
470
         if (!participant) {
470
             return;
471
             return;
471
         }
472
         }
473
         const isAudio = track.isAudioTrack();
474
         const isAudio = track.isAudioTrack();
474
 
475
 
475
         dispatch(showNotification({
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
         enabled
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
 export function getSortedParticipants(stateful: Object | Function) {
456
 export function getSortedParticipants(stateful: Object | Function) {
457
     const localParticipant = getLocalParticipant(stateful);
457
     const localParticipant = getLocalParticipant(stateful);
458
     const remoteParticipants = getRemoteParticipants(stateful);
458
     const remoteParticipants = getRemoteParticipants(stateful);
459
+    const raisedHandParticipantIds = getRaiseHandsQueue(stateful);
459
 
460
 
460
     const items = [];
461
     const items = [];
461
     const dominantSpeaker = getDominantSpeakerParticipant(stateful);
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
     remoteParticipants.forEach(p => {
473
     remoteParticipants.forEach(p => {
464
-        if (p !== dominantSpeaker) {
474
+        if (p !== dominantSpeaker && !raisedHandParticipantIds.find(id => p.id === id)) {
465
             items.push(p);
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
     items.sort((a, b) =>
483
     items.sort((a, b) =>
470
         getParticipantDisplayName(stateful, a.id).localeCompare(getParticipantDisplayName(stateful, b.id))
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
     if (dominantSpeaker && dominantSpeaker !== localParticipant) {
489
     if (dominantSpeaker && dominantSpeaker !== localParticipant) {
476
         items.unshift(dominantSpeaker);
490
         items.unshift(dominantSpeaker);
492
 
506
 
493
     return participantIds;
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
 import { batch } from 'react-redux';
3
 import { batch } from 'react-redux';
4
 
4
 
5
 import UIEvents from '../../../../service/UI/UIEvents';
5
 import UIEvents from '../../../../service/UI/UIEvents';
6
+import { approveParticipant } from '../../av-moderation/actions';
6
 import { toggleE2EE } from '../../e2ee/actions';
7
 import { toggleE2EE } from '../../e2ee/actions';
7
 import { NOTIFICATION_TIMEOUT, showNotification } from '../../notifications';
8
 import { NOTIFICATION_TIMEOUT, showNotification } from '../../notifications';
9
+import { isForceMuted } from '../../participants-pane/functions';
8
 import { CALLING, INVITED } from '../../presence-status';
10
 import { CALLING, INVITED } from '../../presence-status';
9
 import { RAISE_HAND_SOUND_ID } from '../../reactions/constants';
11
 import { RAISE_HAND_SOUND_ID } from '../../reactions/constants';
10
 import { APP_WILL_MOUNT, APP_WILL_UNMOUNT } from '../app';
12
 import { APP_WILL_MOUNT, APP_WILL_UNMOUNT } from '../app';
15
 } from '../conference';
17
 } from '../conference';
16
 import { getDisableRemoveRaisedHandOnFocus } from '../config/functions.any';
18
 import { getDisableRemoveRaisedHandOnFocus } from '../config/functions.any';
17
 import { JitsiConferenceEvents } from '../lib-jitsi-meet';
19
 import { JitsiConferenceEvents } from '../lib-jitsi-meet';
20
+import { MEDIA_TYPE } from '../media';
18
 import { MiddlewareRegistry, StateListenerRegistry } from '../redux';
21
 import { MiddlewareRegistry, StateListenerRegistry } from '../redux';
19
 import { playSound, registerSound, unregisterSound } from '../sounds';
22
 import { playSound, registerSound, unregisterSound } from '../sounds';
20
 
23
 
27
     PARTICIPANT_DISPLAY_NAME_CHANGED,
30
     PARTICIPANT_DISPLAY_NAME_CHANGED,
28
     PARTICIPANT_JOINED,
31
     PARTICIPANT_JOINED,
29
     PARTICIPANT_LEFT,
32
     PARTICIPANT_LEFT,
30
-    PARTICIPANT_UPDATED
33
+    PARTICIPANT_UPDATED,
34
+    RAISE_HAND_UPDATED
31
 } from './actionTypes';
35
 } from './actionTypes';
32
 import {
36
 import {
33
     localParticipantIdChanged,
37
     localParticipantIdChanged,
35
     localParticipantLeft,
39
     localParticipantLeft,
36
     participantLeft,
40
     participantLeft,
37
     participantUpdated,
41
     participantUpdated,
42
+    raiseHandUpdateQueue,
38
     setLoadableAvatarUrl
43
     setLoadableAvatarUrl
39
 } from './actions';
44
 } from './actions';
40
 import {
45
 import {
48
     getParticipantById,
53
     getParticipantById,
49
     getParticipantCount,
54
     getParticipantCount,
50
     getParticipantDisplayName,
55
     getParticipantDisplayName,
51
-    getRemoteParticipants
56
+    getRaiseHandsQueue,
57
+    getRemoteParticipants,
58
+    isLocalParticipantModerator
52
 } from './functions';
59
 } from './functions';
53
 import { PARTICIPANT_JOINED_FILE, PARTICIPANT_LEFT_FILE } from './sounds';
60
 import { PARTICIPANT_JOINED_FILE, PARTICIPANT_LEFT_FILE } from './sounds';
54
 
61
 
122
         const { enabled } = action;
129
         const { enabled } = action;
123
         const localId = getLocalParticipant(store.getState())?.id;
130
         const localId = getLocalParticipant(store.getState())?.id;
124
 
131
 
132
+        store.dispatch(raiseHandUpdateQueue({
133
+            id: localId,
134
+            raisedHand: enabled
135
+        }));
136
+
125
         store.dispatch(participantUpdated({
137
         store.dispatch(participantUpdated({
126
             // XXX Only the local participant is allowed to update without
138
             // XXX Only the local participant is allowed to update without
127
             // stating the JitsiConference instance (i.e. participant property
139
             // stating the JitsiConference instance (i.e. participant property
162
         break;
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
     case PARTICIPANT_JOINED: {
192
     case PARTICIPANT_JOINED: {
166
         _maybePlaySounds(store, action);
193
         _maybePlaySounds(store, action);
167
 
194
 
424
     // Send an external update of the local participant's raised hand state
451
     // Send an external update of the local participant's raised hand state
425
     // if a new raised hand state is defined in the action.
452
     // if a new raised hand state is defined in the action.
426
     if (typeof raisedHand !== 'undefined') {
453
     if (typeof raisedHand !== 'undefined') {
454
+
427
         if (local) {
455
         if (local) {
428
             const { conference } = getState()['features/base/conference'];
456
             const { conference } = getState()['features/base/conference'];
429
 
457
 
476
  */
504
  */
477
 function _raiseHandUpdated({ dispatch, getState }, conference, participantId, newValue) {
505
 function _raiseHandUpdated({ dispatch, getState }, conference, participantId, newValue) {
478
     const raisedHand = newValue === 'true';
506
     const raisedHand = newValue === 'true';
507
+    const state = getState();
479
 
508
 
480
     dispatch(participantUpdated({
509
     dispatch(participantUpdated({
481
         conference,
510
         conference,
483
         raisedHand
512
         raisedHand
484
     }));
513
     }));
485
 
514
 
515
+    dispatch(raiseHandUpdateQueue({
516
+        id: participantId,
517
+        raisedHand
518
+    }));
519
+
486
     if (typeof APP !== 'undefined') {
520
     if (typeof APP !== 'undefined') {
487
         APP.API.notifyRaiseHandUpdated(participantId, raisedHand);
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
     if (raisedHand) {
538
     if (raisedHand) {
491
         dispatch(showNotification({
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
         dispatch(playSound(RAISE_HAND_SOUND_ID));
546
         dispatch(playSound(RAISE_HAND_SOUND_ID));
498
     }
547
     }
499
 }
548
 }

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

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

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

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

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

3
 import type { Dispatch } from 'redux';
3
 import type { Dispatch } from 'redux';
4
 
4
 
5
 import VideoLayout from '../../../modules/UI/videolayout/VideoLayout';
5
 import VideoLayout from '../../../modules/UI/videolayout/VideoLayout';
6
+import { getParticipantById } from '../base/participants/functions';
6
 
7
 
7
 import { OPEN_CHAT } from './actionTypes';
8
 import { OPEN_CHAT } from './actionTypes';
8
 import { closeChat } from './actions.any';
9
 import { closeChat } from './actions.any';
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
  * Toggles display of the chat panel.
53
  * Toggles display of the chat panel.
32
  *
54
  *

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

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

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

45
  * }
45
  * }
46
  */
46
  */
47
 export const SET_NOTIFICATIONS_ENABLED = 'SET_NOTIFICATIONS_ENABLED';
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
 import {
9
 import {
10
     CLEAR_NOTIFICATIONS,
10
     CLEAR_NOTIFICATIONS,
11
     HIDE_NOTIFICATION,
11
     HIDE_NOTIFICATION,
12
+    HIDE_RAISE_HAND_NOTIFICATIONS,
12
     SET_NOTIFICATIONS_ENABLED,
13
     SET_NOTIFICATIONS_ENABLED,
13
     SHOW_NOTIFICATION
14
     SHOW_NOTIFICATION
14
 } from './actionTypes';
15
 } from './actionTypes';
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
  * Stops notifications from being displayed.
66
  * Stops notifications from being displayed.
53
  *
67
  *

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

7
     PARTICIPANT_ROLE,
7
     PARTICIPANT_ROLE,
8
     PARTICIPANT_UPDATED,
8
     PARTICIPANT_UPDATED,
9
     getParticipantById,
9
     getParticipantById,
10
-    getParticipantDisplayName
10
+    getParticipantDisplayName,
11
+    getLocalParticipant
11
 } from '../base/participants';
12
 } from '../base/participants';
12
 import { MiddlewareRegistry, StateListenerRegistry } from '../base/redux';
13
 import { MiddlewareRegistry, StateListenerRegistry } from '../base/redux';
14
+import { PARTICIPANTS_PANE_OPEN } from '../participants-pane/actionTypes';
13
 
15
 
14
 import {
16
 import {
15
     clearNotifications,
17
     clearNotifications,
18
+    hideRaiseHandNotifications,
16
     showNotification,
19
     showNotification,
17
     showParticipantJoinedNotification
20
     showParticipantJoinedNotification
18
 } from './actions';
21
 } from './actions';
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
         return result;
48
         return result;
62
     }
49
     }
63
     case PARTICIPANT_LEFT: {
50
     case PARTICIPANT_LEFT: {
82
         return next(action);
69
         return next(action);
83
     }
70
     }
84
     case PARTICIPANT_UPDATED: {
71
     case PARTICIPANT_UPDATED: {
85
-        if (typeof interfaceConfig === 'undefined' || interfaceConfig.DISABLE_FOCUS_INDICATOR) {
72
+        if (typeof interfaceConfig === 'undefined') {
86
             // Do not show the notification for mobile and also when the focus indicator is disabled.
73
             // Do not show the notification for mobile and also when the focus indicator is disabled.
87
             return next(action);
74
             return next(action);
88
         }
75
         }
89
 
76
 
90
         const { id, role } = action.participant;
77
         const { id, role } = action.participant;
91
         const state = store.getState();
78
         const state = store.getState();
79
+        const localParticipant = getLocalParticipant(state);
80
+
81
+        if (localParticipant.id !== id) {
82
+            return next(action);
83
+        }
84
+
92
         const oldParticipant = getParticipantById(state, id);
85
         const oldParticipant = getParticipantById(state, id);
93
         const oldRole = oldParticipant?.role;
86
         const oldRole = oldParticipant?.role;
94
 
87
 
95
         if (oldRole && oldRole !== role && role === PARTICIPANT_ROLE.MODERATOR) {
88
         if (oldRole && oldRole !== role && role === PARTICIPANT_ROLE.MODERATOR) {
96
-            const displayName = getParticipantDisplayName(state, id);
97
 
89
 
98
             store.dispatch(showNotification({
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
             NOTIFICATION_TIMEOUT));
93
             NOTIFICATION_TIMEOUT));
105
         }
94
         }
106
 
95
 
107
         return next(action);
96
         return next(action);
108
     }
97
     }
98
+    case PARTICIPANTS_PANE_OPEN: {
99
+        store.dispatch(hideRaiseHandNotifications());
100
+        break;
101
+    }
109
     }
102
     }
110
 
103
 
111
     return next(action);
104
     return next(action);

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

5
 import {
5
 import {
6
     CLEAR_NOTIFICATIONS,
6
     CLEAR_NOTIFICATIONS,
7
     HIDE_NOTIFICATION,
7
     HIDE_NOTIFICATION,
8
+    HIDE_RAISE_HAND_NOTIFICATIONS,
8
     SET_NOTIFICATIONS_ENABLED,
9
     SET_NOTIFICATIONS_ENABLED,
9
     SHOW_NOTIFICATION
10
     SHOW_NOTIFICATION
10
 } from './actionTypes';
11
 } from './actionTypes';
43
                     notification => notification.uid !== action.uid)
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
         case SET_NOTIFICATIONS_ENABLED:
55
         case SET_NOTIFICATIONS_ENABLED:
47
             return {
56
             return {
48
                 ...state,
57
                 ...state,

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

1
 // @flow
1
 // @flow
2
 
2
 
3
 import { makeStyles } from '@material-ui/core/styles';
3
 import { makeStyles } from '@material-ui/core/styles';
4
+import clsx from 'clsx';
4
 import React, { useCallback } from 'react';
5
 import React, { useCallback } from 'react';
5
 import { useTranslation } from 'react-i18next';
6
 import { useTranslation } from 'react-i18next';
6
 import { useDispatch, useSelector } from 'react-redux';
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
 import {
15
 import {
10
     isEnabled as isAvModerationEnabled,
16
     isEnabled as isAvModerationEnabled,
11
     isSupported as isAvModerationSupported
17
     isSupported as isAvModerationSupported
13
 import { openDialog } from '../../base/dialog';
19
 import { openDialog } from '../../base/dialog';
14
 import { Icon, IconCheck, IconVideoOff } from '../../base/icons';
20
 import { Icon, IconCheck, IconVideoOff } from '../../base/icons';
15
 import { MEDIA_TYPE } from '../../base/media';
21
 import { MEDIA_TYPE } from '../../base/media';
16
-import { getLocalParticipant } from '../../base/participants';
22
+import {
23
+    getParticipantCount,
24
+    isEveryoneModerator
25
+} from '../../base/participants';
17
 import { MuteEveryonesVideoDialog } from '../../video-menu/components';
26
 import { MuteEveryonesVideoDialog } from '../../video-menu/components';
18
 
27
 
19
 import {
28
 import {
33
             transform: 'translateY(-100%)',
42
             transform: 'translateY(-100%)',
34
             width: '283px'
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
         text: {
56
         text: {
37
             color: '#C2C2C2',
57
             color: '#C2C2C2',
38
             padding: '10px 16px 10px 52px'
58
             padding: '10px 16px 10px 52px'
45
 
65
 
46
 type Props = {
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
     const dispatch = useDispatch();
80
     const dispatch = useDispatch();
56
     const isModerationSupported = useSelector(isAvModerationSupported());
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
     const { t } = useTranslation();
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
     const classes = useStyles();
97
     const classes = useStyles();
66
 
98
 
67
     const muteAllVideo = useCallback(
99
     const muteAllVideo = useCallback(
68
-        () => dispatch(openDialog(MuteEveryonesVideoDialog, { exclude: [ id ] })), [ dispatch ]);
100
+        () => dispatch(openDialog(MuteEveryonesVideoDialog)), [ dispatch ]);
69
 
101
 
70
     return (
102
     return (
71
         <ContextMenu
103
         <ContextMenu
72
-            className = { classes.contextMenu }
104
+            className = { clsx(classes.contextMenu, inDrawer && clsx(classes.drawer)) }
73
             onMouseLeave = { onMouseLeave }>
105
             onMouseLeave = { onMouseLeave }>
74
             <ContextMenuItemGroup>
106
             <ContextMenuItemGroup>
75
                 <ContextMenuItem
107
                 <ContextMenuItem
81
                     <span>{ t('participantsPane.actions.stopEveryonesVideo') }</span>
113
                     <span>{ t('participantsPane.actions.stopEveryonesVideo') }</span>
82
                 </ContextMenuItem>
114
                 </ContextMenuItem>
83
             </ContextMenuItemGroup>
115
             </ContextMenuItemGroup>
84
-            { isModerationSupported ? (
116
+            {isModerationSupported && (participantCount === 1 || !allModerators) ? (
85
                 <ContextMenuItemGroup>
117
                 <ContextMenuItemGroup>
86
                     <div className = { classes.text }>
118
                     <div className = { classes.text }>
87
                         {t('participantsPane.actions.allow')}
119
                         {t('participantsPane.actions.allow')}
88
                     </div>
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
                         <ContextMenuItem
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
                             <span className = { classes.paddedAction }>
143
                             <span className = { classes.paddedAction }>
94
-                                { t('participantsPane.actions.startModeration') }
144
+                                {t('participantsPane.actions.videoModeration')}
95
                             </span>
145
                             </span>
96
                         </ContextMenuItem>
146
                         </ContextMenuItem>
97
                     ) : (
147
                     ) : (
98
                         <ContextMenuItem
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
                             <Icon
151
                             <Icon
102
                                 size = { 20 }
152
                                 size = { 20 }
103
                                 src = { IconCheck } />
153
                                 src = { IconCheck } />
104
-                            <span>{ t('participantsPane.actions.startModeration') }</span>
154
+                            <span>{t('participantsPane.actions.videoModeration')}</span>
105
                         </ContextMenuItem>
155
                         </ContextMenuItem>
106
                     )}
156
                     )}
107
                 </ContextMenuItemGroup>
157
                 </ContextMenuItemGroup>

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

1
 // @flow
1
 // @flow
2
 
2
 
3
-import React, { useCallback } from 'react';
3
+import React from 'react';
4
 import { useTranslation } from 'react-i18next';
4
 import { useTranslation } from 'react-i18next';
5
-import { useDispatch } from 'react-redux';
6
 
5
 
7
-import { approveKnockingParticipant, rejectKnockingParticipant } from '../../../lobby/actions';
8
 import { ACTION_TRIGGER, MEDIA_STATE } from '../../constants';
6
 import { ACTION_TRIGGER, MEDIA_STATE } from '../../constants';
7
+import { useLobbyActions } from '../../hooks';
9
 
8
 
10
 import ParticipantItem from './ParticipantItem';
9
 import ParticipantItem from './ParticipantItem';
11
 import { ParticipantActionButton } from './styled';
10
 import { ParticipantActionButton } from './styled';
12
 
11
 
13
 type Props = {
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
      * Participant reference
25
      * Participant reference
17
      */
26
      */
18
     participant: Object
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
     const { t } = useTranslation();
37
     const { t } = useTranslation();
26
 
38
 
27
     return (
39
     return (
30
             audioMediaState = { MEDIA_STATE.NONE }
42
             audioMediaState = { MEDIA_STATE.NONE }
31
             displayName = { p.name }
43
             displayName = { p.name }
32
             local = { p.local }
44
             local = { p.local }
33
-            participantID = { p.id }
45
+            openDrawerForParticipant = { openDrawerForParticipant }
46
+            overflowDrawer = { overflowDrawer }
47
+            participantID = { id }
34
             raisedHand = { p.raisedHand }
48
             raisedHand = { p.raisedHand }
35
-            videoMuteState = { MEDIA_STATE.NONE }
49
+            videoMediaState = { MEDIA_STATE.NONE }
36
             youText = { t('chat.you') }>
50
             youText = { t('chat.you') }>
37
-            <ParticipantActionButton
38
-                onClick = { reject }>
39
-                {t('lobby.reject')}
40
-            </ParticipantActionButton>
41
             <ParticipantActionButton
51
             <ParticipantActionButton
42
                 onClick = { admit }
52
                 onClick = { admit }
43
                 primary = { true }>
53
                 primary = { true }>

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

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
-// @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

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
 // @flow
1
 // @flow
2
-
2
+import { withStyles } from '@material-ui/core/styles';
3
 import React, { Component } from 'react';
3
 import React, { Component } from 'react';
4
 
4
 
5
+import { Avatar } from '../../../base/avatar';
5
 import { isToolbarButtonEnabled } from '../../../base/config/functions.web';
6
 import { isToolbarButtonEnabled } from '../../../base/config/functions.web';
6
 import { openDialog } from '../../../base/dialog';
7
 import { openDialog } from '../../../base/dialog';
7
 import { translate } from '../../../base/i18n';
8
 import { translate } from '../../../base/i18n';
21
     isParticipantModerator
22
     isParticipantModerator
22
 } from '../../../base/participants';
23
 } from '../../../base/participants';
23
 import { connect } from '../../../base/redux';
24
 import { connect } from '../../../base/redux';
25
+import { withPixelLineHeight } from '../../../base/styles/functions.web';
24
 import { isParticipantAudioMuted, isParticipantVideoMuted } from '../../../base/tracks';
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
 import { GrantModeratorDialog, KickRemoteParticipantDialog, MuteEveryoneDialog } from '../../../video-menu';
30
 import { GrantModeratorDialog, KickRemoteParticipantDialog, MuteEveryoneDialog } from '../../../video-menu';
31
+import { VolumeSlider } from '../../../video-menu/components/web';
28
 import MuteRemoteParticipantsVideoDialog from '../../../video-menu/components/web/MuteRemoteParticipantsVideoDialog';
32
 import MuteRemoteParticipantsVideoDialog from '../../../video-menu/components/web/MuteRemoteParticipantsVideoDialog';
29
 import { getComputedOuterHeight } from '../../functions';
33
 import { getComputedOuterHeight } from '../../functions';
30
 
34
 
73
      */
77
      */
74
     _participant: Object,
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
      * The dispatch function from redux.
97
      * The dispatch function from redux.
78
      */
98
      */
79
     dispatch: Function,
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
      * Callback used to open a confirmation dialog for audio muting.
108
      * Callback used to open a confirmation dialog for audio muting.
83
      */
109
      */
108
      */
134
      */
109
     participantID: string,
135
     participantID: string,
110
 
136
 
137
+    /**
138
+     * True if an overflow drawer should be displayed.
139
+     */
140
+    overflowDrawer: boolean,
141
+
142
+
111
     /**
143
     /**
112
      * The translate function.
144
      * The translate function.
113
      */
145
      */
122
     isHidden: boolean
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
  * Implements the MeetingParticipantContextMenu component.
177
  * Implements the MeetingParticipantContextMenu component.
127
  */
178
  */
146
 
197
 
147
         this._containerRef = React.createRef();
198
         this._containerRef = React.createRef();
148
 
199
 
200
+        this._getCurrentParticipantId = this._getCurrentParticipantId.bind(this);
149
         this._onGrantModerator = this._onGrantModerator.bind(this);
201
         this._onGrantModerator = this._onGrantModerator.bind(this);
150
         this._onKick = this._onKick.bind(this);
202
         this._onKick = this._onKick.bind(this);
151
         this._onMuteEveryoneElse = this._onMuteEveryoneElse.bind(this);
203
         this._onMuteEveryoneElse = this._onMuteEveryoneElse.bind(this);
152
         this._onMuteVideo = this._onMuteVideo.bind(this);
204
         this._onMuteVideo = this._onMuteVideo.bind(this);
153
         this._onSendPrivateMessage = this._onSendPrivateMessage.bind(this);
205
         this._onSendPrivateMessage = this._onSendPrivateMessage.bind(this);
154
-        this._onStopSharedVideo = this._onStopSharedVideo.bind(this);
155
         this._position = this._position.bind(this);
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
     _onGrantModerator: () => void;
223
     _onGrantModerator: () => void;
163
      * @returns {void}
228
      * @returns {void}
164
      */
229
      */
165
     _onGrantModerator() {
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
      * @returns {void}
241
      * @returns {void}
179
      */
242
      */
180
     _onKick() {
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
     _onStopSharedVideo() {
256
     _onStopSharedVideo() {
196
         const { dispatch } = this.props;
257
         const { dispatch } = this.props;
197
 
258
 
198
-        dispatch(stopSharedVideo());
259
+        dispatch(this._onStopSharedVideo());
199
     }
260
     }
200
 
261
 
201
     _onMuteEveryoneElse: () => void;
262
     _onMuteEveryoneElse: () => void;
206
      * @returns {void}
267
      * @returns {void}
207
      */
268
      */
208
     _onMuteEveryoneElse() {
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
      * @returns {void}
280
      * @returns {void}
222
      */
281
      */
223
     _onMuteVideo() {
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
      * @returns {void}
293
      * @returns {void}
237
      */
294
      */
238
     _onSendPrivateMessage() {
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
     _position: () => void;
302
     _position: () => void;
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
      * Implements React Component's componentDidMount.
347
      * Implements React Component's componentDidMount.
275
      *
348
      *
306
             _isParticipantAudioMuted,
379
             _isParticipantAudioMuted,
307
             _localVideoOwner,
380
             _localVideoOwner,
308
             _participant,
381
             _participant,
382
+            _volume = 1,
383
+            classes,
384
+            closeDrawer,
385
+            drawerParticipant,
309
             onEnter,
386
             onEnter,
310
             onLeave,
387
             onLeave,
311
             onSelect,
388
             onSelect,
389
+            overflowDrawer,
312
             muteAudio,
390
             muteAudio,
313
             t
391
             t
314
         } = this.props;
392
         } = this.props;
317
             return null;
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
                             <ContextMenuItemGroup>
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
                             </ContextMenuItemGroup>
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
  * @returns {Props}
518
  * @returns {Props}
415
  */
519
  */
416
 function _mapStateToProps(state, ownProps): Object {
520
 function _mapStateToProps(state, ownProps): Object {
417
-    const { participantID } = ownProps;
521
+    const { participantID, overflowDrawer, drawerParticipant } = ownProps;
418
     const { ownerId } = state['features/shared-video'];
522
     const { ownerId } = state['features/shared-video'];
419
     const localParticipantId = getLocalParticipant(state).id;
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
     const _isLocalModerator = isLocalParticipantModerator(state);
528
     const _isLocalModerator = isLocalParticipantModerator(state);
423
     const _isChatButtonEnabled = isToolbarButtonEnabled('chat', state);
529
     const _isChatButtonEnabled = isToolbarButtonEnabled('chat', state);
425
     const _isParticipantAudioMuted = isParticipantAudioMuted(participant, state);
531
     const _isParticipantAudioMuted = isParticipantAudioMuted(participant, state);
426
     const _isParticipantModerator = isParticipantModerator(participant);
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
     return {
538
     return {
429
         _isLocalModerator,
539
         _isLocalModerator,
430
         _isChatButtonEnabled,
540
         _isChatButtonEnabled,
432
         _isParticipantVideoMuted,
542
         _isParticipantVideoMuted,
433
         _isParticipantAudioMuted,
543
         _isParticipantAudioMuted,
434
         _localVideoOwner: Boolean(ownerId === localParticipantId),
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
 } from '../../../base/participants';
9
 } from '../../../base/participants';
10
 import { connect } from '../../../base/redux';
10
 import { connect } from '../../../base/redux';
11
 import { isParticipantAudioMuted, isParticipantVideoMuted } from '../../../base/tracks';
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
 import ParticipantQuickAction from '../ParticipantQuickAction';
18
 import ParticipantQuickAction from '../ParticipantQuickAction';
15
 
19
 
16
 import ParticipantItem from './ParticipantItem';
20
 import ParticipantItem from './ParticipantItem';
24
     _audioMediaState: MediaState,
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
      * True if the participant is the local participant.
42
      * True if the participant is the local participant.
38
      */
43
      */
39
-    _local: boolean,
44
+    _local: Boolean,
40
 
45
 
41
     /**
46
     /**
42
      * Shared video local participant owner.
47
      * Shared video local participant owner.
96
      */
101
      */
97
     onLeave: Function,
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
      * The aria-label for the ellipsis action.
116
      * The aria-label for the ellipsis action.
101
      */
117
      */
120
  */
136
  */
121
 function MeetingParticipantItem({
137
 function MeetingParticipantItem({
122
     _audioMediaState,
138
     _audioMediaState,
139
+    _videoMediaState,
123
     _displayName,
140
     _displayName,
124
-    _isVideoMuted,
125
-    _localVideoOwner,
126
     _local,
141
     _local,
142
+    _localVideoOwner,
127
     _participant,
143
     _participant,
128
     _participantID,
144
     _participantID,
129
     _quickActionButtonType,
145
     _quickActionButtonType,
130
     _raisedHand,
146
     _raisedHand,
131
     askUnmuteText,
147
     askUnmuteText,
132
     isHighlighted,
148
     isHighlighted,
133
-    onContextMenu,
134
-    onLeave,
135
     muteAudio,
149
     muteAudio,
136
     muteParticipantButtonText,
150
     muteParticipantButtonText,
151
+    onContextMenu,
152
+    onLeave,
153
+    openDrawerForParticipant,
154
+    overflowDrawer,
137
     participantActionEllipsisLabel,
155
     participantActionEllipsisLabel,
138
     youText
156
     youText
139
 }: Props) {
157
 }: Props) {
145
             isHighlighted = { isHighlighted }
163
             isHighlighted = { isHighlighted }
146
             local = { _local }
164
             local = { _local }
147
             onLeave = { onLeave }
165
             onLeave = { onLeave }
166
+            openDrawerForParticipant = { openDrawerForParticipant }
167
+            overflowDrawer = { overflowDrawer }
148
             participantID = { _participantID }
168
             participantID = { _participantID }
149
             raisedHand = { _raisedHand }
169
             raisedHand = { _raisedHand }
150
-            videoMuteState = { _isVideoMuted ? MEDIA_STATE.MUTED : MEDIA_STATE.UNMUTED }
170
+            videoMediaState = { _videoMediaState }
151
             youText = { youText }>
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
                     <ParticipantActionEllipsis
181
                     <ParticipantActionEllipsis
170
                         aria-label = { participantActionEllipsisLabel }
182
                         aria-label = { participantActionEllipsisLabel }
171
                         onClick = { onContextMenu } />
183
                         onClick = { onContextMenu } />
172
-                )
184
+                 </>
173
             }
185
             }
186
+
187
+            {!overflowDrawer && _localVideoOwner && _participant.isFakeParticipant && (
188
+                <ParticipantActionEllipsis
189
+                    aria-label = { participantActionEllipsisLabel }
190
+                    onClick = { onContextMenu } />
191
+            )}
174
         </ParticipantItem>
192
         </ParticipantItem>
175
     );
193
     );
176
 }
194
 }
193
     const _isAudioMuted = isParticipantAudioMuted(participant, state);
211
     const _isAudioMuted = isParticipantAudioMuted(participant, state);
194
     const _isVideoMuted = isParticipantVideoMuted(participant, state);
212
     const _isVideoMuted = isParticipantVideoMuted(participant, state);
195
     const _audioMediaState = getParticipantAudioMediaState(participant, _isAudioMuted, state);
213
     const _audioMediaState = getParticipantAudioMediaState(participant, _isAudioMuted, state);
214
+    const _videoMediaState = getParticipantVideoMediaState(participant, _isVideoMuted, state);
196
     const _quickActionButtonType = getQuickActionButtonType(participant, _isAudioMuted, state);
215
     const _quickActionButtonType = getQuickActionButtonType(participant, _isAudioMuted, state);
197
 
216
 
198
     return {
217
     return {
199
         _audioMediaState,
218
         _audioMediaState,
219
+        _videoMediaState,
200
         _displayName: getParticipantDisplayName(state, participant?.id),
220
         _displayName: getParticipantDisplayName(state, participant?.id),
201
-        _isAudioMuted,
202
-        _isVideoMuted,
203
         _local: Boolean(participant?.local),
221
         _local: Boolean(participant?.local),
204
         _localVideoOwner: Boolean(ownerId === localParticipantId),
222
         _localVideoOwner: Boolean(ownerId === localParticipantId),
205
         _participant: participant,
223
         _participant: participant,

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

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
 import { useDispatch } from 'react-redux';
5
 import { useDispatch } from 'react-redux';
6
 
6
 
7
 import { isToolbarButtonEnabled } from '../../../base/config/functions.web';
7
 import { isToolbarButtonEnabled } from '../../../base/config/functions.web';
8
-import { openDialog } from '../../../base/dialog';
8
+import { MEDIA_TYPE } from '../../../base/media';
9
 import {
9
 import {
10
     getParticipantCountWithFake,
10
     getParticipantCountWithFake,
11
     getSortedParticipantIds
11
     getSortedParticipantIds
12
 } from '../../../base/participants';
12
 } from '../../../base/participants';
13
 import { connect } from '../../../base/redux';
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
 import { findStyledAncestor, shouldRenderInviteButton } from '../../functions';
16
 import { findStyledAncestor, shouldRenderInviteButton } from '../../functions';
17
+import { useParticipantDrawer } from '../../hooks';
16
 
18
 
17
 import { InviteButton } from './InviteButton';
19
 import { InviteButton } from './InviteButton';
18
 import MeetingParticipantContextMenu from './MeetingParticipantContextMenu';
20
 import MeetingParticipantContextMenu from './MeetingParticipantContextMenu';
19
-import MeetingParticipantItem from './MeetingParticipantItem';
21
+import MeetingParticipantItems from './MeetingParticipantItems';
20
 import { Heading, ParticipantContainer } from './styled';
22
 import { Heading, ParticipantContainer } from './styled';
21
 
23
 
22
 type NullProto = {
24
 type NullProto = {
23
-  [key: string]: any,
24
-  __proto__: null
25
+    [key: string]: any,
26
+    __proto__: null
25
 };
27
 };
26
 
28
 
27
 type RaiseContext = NullProto | {|
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
 const initialState = Object.freeze(Object.create(null));
42
 const initialState = Object.freeze(Object.create(null));
49
  *
51
  *
50
  * @returns {ReactNode} - The component.
52
  * @returns {ReactNode} - The component.
51
  */
53
  */
52
-function MeetingParticipantList({ participantsCount, showInviteButton, sortedParticipantIds = [] }) {
54
+function MeetingParticipants({ participantsCount, showInviteButton, overflowDrawer, sortedParticipantIds = [] }) {
53
     const dispatch = useDispatch();
55
     const dispatch = useDispatch();
54
     const isMouseOverMenu = useRef(false);
56
     const isMouseOverMenu = useRef(false);
55
 
57
 
56
-    const [ raiseContext, setRaiseContext ] = useState<RaiseContext>(initialState);
58
+    const [ raiseContext, setRaiseContext ] = useState < RaiseContext >(initialState);
57
     const { t } = useTranslation();
59
     const { t } = useTranslation();
58
 
60
 
59
     const lowerMenu = useCallback(() => {
61
     const lowerMenu = useCallback(() => {
101
     }, [ lowerMenu ]);
103
     }, [ lowerMenu ]);
102
 
104
 
103
     const muteAudio = useCallback(id => () => {
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
     // FIXME:
110
     // FIXME:
108
     // It seems that useTranslation is not very scallable. Unmount 500 components that have the useTranslation hook is
111
     // It seems that useTranslation is not very scallable. Unmount 500 components that have the useTranslation hook is
115
     const askUnmuteText = t('participantsPane.actions.askUnmute');
118
     const askUnmuteText = t('participantsPane.actions.askUnmute');
116
     const muteParticipantButtonText = t('dialog.muteParticipantButton');
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
     return (
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
 
167
 
164
     const showInviteButton = shouldRenderInviteButton(state) && isToolbarButtonEnabled('invite', state);
168
     const showInviteButton = shouldRenderInviteButton(state) && isToolbarButtonEnabled('invite', state);
165
 
169
 
170
+    const overflowDrawer = showOverflowDrawer(state);
171
+
166
     return {
172
     return {
167
         sortedParticipantIds,
173
         sortedParticipantIds,
168
         participantsCount,
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
 // @flow
1
 // @flow
2
 
2
 
3
-import React, { type Node } from 'react';
3
+import React, { type Node, useCallback } from 'react';
4
 
4
 
5
 import { Avatar } from '../../../base/avatar';
5
 import { Avatar } from '../../../base/avatar';
6
 import {
6
 import {
61
     /**
61
     /**
62
      * True if the participant is local.
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
      * Callback for when the mouse leaves this component
72
      * Callback for when the mouse leaves this component
68
      */
73
      */
69
     onLeave?: Function,
74
     onLeave?: Function,
70
 
75
 
76
+    /**
77
+     * If an overflow drawer can be opened.
78
+     */
79
+    overflowDrawer?: boolean,
80
+
71
     /**
81
     /**
72
      * The ID of the participant.
82
      * The ID of the participant.
73
      */
83
      */
81
     /**
91
     /**
82
      * Media state for video
92
      * Media state for video
83
      */
93
      */
84
-    videoMuteState: MediaState,
94
+    videoMediaState: MediaState,
85
 
95
 
86
     /**
96
     /**
87
      * The translated "you" text.
97
      * The translated "you" text.
101
     onLeave,
111
     onLeave,
102
     actionsTrigger = ACTION_TRIGGER.HOVER,
112
     actionsTrigger = ACTION_TRIGGER.HOVER,
103
     audioMediaState = MEDIA_STATE.NONE,
113
     audioMediaState = MEDIA_STATE.NONE,
104
-    videoMuteState = MEDIA_STATE.NONE,
114
+    videoMediaState = MEDIA_STATE.NONE,
105
     displayName,
115
     displayName,
106
     participantID,
116
     participantID,
107
     local,
117
     local,
118
+    openDrawerForParticipant,
119
+    overflowDrawer,
108
     raisedHand,
120
     raisedHand,
109
     youText
121
     youText
110
 }: Props) {
122
 }: Props) {
111
     const ParticipantActions = Actions[actionsTrigger];
123
     const ParticipantActions = Actions[actionsTrigger];
124
+    const onClick = useCallback(
125
+        () => openDrawerForParticipant({
126
+            participantID,
127
+            displayName
128
+        }));
112
 
129
 
113
     return (
130
     return (
114
         <ParticipantContainer
131
         <ParticipantContainer
115
             id = { `participant-item-${participantID}` }
132
             id = { `participant-item-${participantID}` }
116
             isHighlighted = { isHighlighted }
133
             isHighlighted = { isHighlighted }
117
             local = { local }
134
             local = { local }
135
+            onClick = { !local && overflowDrawer ? onClick : undefined }
118
             onMouseLeave = { onLeave }
136
             onMouseLeave = { onLeave }
119
             trigger = { actionsTrigger }>
137
             trigger = { actionsTrigger }>
120
             <Avatar
138
             <Avatar
131
                 { !local && <ParticipantActions children = { children } /> }
149
                 { !local && <ParticipantActions children = { children } /> }
132
                 <ParticipantStates>
150
                 <ParticipantStates>
133
                     { raisedHand && <RaisedHandIndicator /> }
151
                     { raisedHand && <RaisedHandIndicator /> }
134
-                    { VideoStateIcons[videoMuteState] }
152
+                    { VideoStateIcons[videoMediaState] }
135
                     { AudioStateIcons[audioMediaState] }
153
                     { AudioStateIcons[audioMediaState] }
136
                 </ParticipantStates>
154
                 </ParticipantStates>
137
             </ParticipantContent>
155
             </ParticipantContent>

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

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

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

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

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

4
 import { Icon, IconHorizontalPoints } from '../../../base/icons';
4
 import { Icon, IconHorizontalPoints } from '../../../base/icons';
5
 import { ACTION_TRIGGER } from '../../constants';
5
 import { ACTION_TRIGGER } from '../../constants';
6
 
6
 
7
+const MD_BREAKPOINT = '580px';
8
+
7
 export const ignoredChildClassName = 'ignore-child';
9
 export const ignoredChildClassName = 'ignore-child';
8
 
10
 
9
 export const AntiCollapse = styled.br`
11
 export const AntiCollapse = styled.br`
89
     size: 20
91
     size: 20
90
 })`
92
 })`
91
   & > svg {
93
   & > svg {
92
-    fill: #a4b8d1;
94
+    fill: #ffffff;
93
   }
95
   }
94
 `;
96
 `;
95
 
97
 
162
   height: 40px;
164
   height: 40px;
163
   font-size: 15px;
165
   font-size: 15px;
164
   padding: 0 16px;
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
 export const FooterEllipsisButton = styled(FooterButton).attrs({
175
 export const FooterEllipsisButton = styled(FooterButton).attrs({
188
   font-size: 15px;
196
   font-size: 15px;
189
   line-height: 24px;
197
   line-height: 24px;
190
   margin: 8px 0 ${props => props.theme.panePadding}px;
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
 export const ParticipantActionButton = styled(Button)`
205
 export const ParticipantActionButton = styled(Button)`
275
   padding-left: ${props => props.theme.panePadding}px;
287
   padding-left: ${props => props.theme.panePadding}px;
276
   position: relative;
288
   position: relative;
277
 
289
 
290
+  @media (max-width: ${MD_BREAKPOINT}) {
291
+    font-size: 16px;
292
+    height: 64px;
293
+  }
294
+
278
   &:hover {
295
   &:hover {
279
     ${ParticipantStates} {
296
     ${ParticipantStates} {
280
       ${props => !props.local && 'display: none'};
297
       ${props => !props.local && 'display: none'};
293
     & ${ParticipantContent} {
310
     & ${ParticipantContent} {
294
       box-shadow: none;
311
       box-shadow: none;
295
     }
312
     }
313
+
314
+    & ${ParticipantStates} {
315
+      display: none;
316
+    }
296
   ${props => !props.isHighlighted && '}'}
317
   ${props => !props.isHighlighted && '}'}
297
 `;
318
 `;
298
 
319
 
306
   & > *:not(:last-child) {
327
   & > *:not(:last-child) {
307
     margin-right: 8px;
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
 export const ParticipantName = styled.div`
337
 export const ParticipantName = styled.div`

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

94
 export const VideoStateIcons = {
94
 export const VideoStateIcons = {
95
     [MEDIA_STATE.FORCE_MUTED]: (
95
     [MEDIA_STATE.FORCE_MUTED]: (
96
         <Icon
96
         <Icon
97
+            color = '#E04757'
97
             size = { 16 }
98
             size = { 16 }
98
             src = { IconCameraEmptyDisabled } />
99
             src = { IconCameraEmptyDisabled } />
99
     ),
100
     ),

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

71
  * @param {Object} participant - The participant.
71
  * @param {Object} participant - The participant.
72
  * @param {boolean} muted - The mute state of the participant.
72
  * @param {boolean} muted - The mute state of the participant.
73
  * @param {Object} state - The redux state.
73
  * @param {Object} state - The redux state.
74
+ * @param {boolean} ignoreDominantSpeaker - Whether to ignore the dominant speaker state.
74
  * @returns {MediaState}
75
  * @returns {MediaState}
75
  */
76
  */
76
 export function getParticipantAudioMediaState(participant: Object, muted: Boolean, state: Object) {
77
 export function getParticipantAudioMediaState(participant: Object, muted: Boolean, state: Object) {
77
     const dominantSpeaker = getDominantSpeakerParticipant(state);
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
     if (participant === dominantSpeaker) {
88
     if (participant === dominantSpeaker) {
80
         return MEDIA_STATE.DOMINANT_SPEAKER;
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
     if (muted) {
105
     if (muted) {
84
-        if (isForceMuted(participant, MEDIA_TYPE.AUDIO, state)) {
106
+        if (isForceMuted(participant, MEDIA_TYPE.VIDEO, state)) {
85
             return MEDIA_STATE.FORCE_MUTED;
107
             return MEDIA_STATE.FORCE_MUTED;
86
         }
108
         }
87
 
109
 
144
 export function getQuickActionButtonType(participant: Object, isAudioMuted: Boolean, state: Object) {
166
 export function getQuickActionButtonType(participant: Object, isAudioMuted: Boolean, state: Object) {
145
     // handled only by moderators
167
     // handled only by moderators
146
     if (isLocalParticipantModerator(state)) {
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
             return QUICK_ACTION_BUTTON.ASK_TO_UNMUTE;
170
             return QUICK_ACTION_BUTTON.ASK_TO_UNMUTE;
149
         }
171
         }
150
         if (!isAudioMuted) {
172
         if (!isAudioMuted) {

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

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
 import { APP_WILL_MOUNT, APP_WILL_UNMOUNT } from '../base/app';
3
 import { APP_WILL_MOUNT, APP_WILL_UNMOUNT } from '../base/app';
4
 import { CONFERENCE_JOINED } from '../base/conference';
4
 import { CONFERENCE_JOINED } from '../base/conference';
5
 import { JitsiConferenceEvents } from '../base/lib-jitsi-meet';
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
 import { MiddlewareRegistry } from '../base/redux';
8
 import { MiddlewareRegistry } from '../base/redux';
8
 import { playSound, registerSound, unregisterSound } from '../base/sounds';
9
 import { playSound, registerSound, unregisterSound } from '../base/sounds';
9
 import {
10
 import {
10
     hideNotification,
11
     hideNotification,
11
     showNotification
12
     showNotification
12
 } from '../notifications';
13
 } from '../notifications';
14
+import { isForceMuted } from '../participants-pane/functions';
13
 
15
 
14
 import { setCurrentNotificationUid } from './actions';
16
 import { setCurrentNotificationUid } from './actions';
15
 import { TALK_WHILE_MUTED_SOUND_ID } from './constants';
17
 import { TALK_WHILE_MUTED_SOUND_ID } from './constants';
41
             });
43
             });
42
         conference.on(
44
         conference.on(
43
             JitsiConferenceEvents.TALK_WHILE_MUTED, async () => {
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
                 const notification = await dispatch(showNotification({
49
                 const notification = await dispatch(showNotification({
45
                     titleKey: 'toolbar.talkWhileMutedPopup',
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
                 const { soundsTalkWhileMuted } = getState()['features/base/settings'];
55
                 const { soundsTalkWhileMuted } = getState()['features/base/settings'];

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

80
 export function isVideoMuteButtonDisabled(state: Object) {
80
 export function isVideoMuteButtonDisabled(state: Object) {
81
     return !hasAvailableDevices(state, 'videoInput');
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
     createRemoteVideoMenuButtonEvent,
6
     createRemoteVideoMenuButtonEvent,
7
     sendAnalytics
7
     sendAnalytics
8
 } from '../../analytics';
8
 } from '../../analytics';
9
-import { grantModerator } from '../../base/participants';
9
+import { getParticipantById, grantModerator } from '../../base/participants';
10
 
10
 
11
 type Props = {
11
 type Props = {
12
 
12
 
20
      */
20
      */
21
     participantID: string,
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
      * Function to translate i18n labels.
29
      * Function to translate i18n labels.
25
      */
30
      */
64
         return true;
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
     createRemoteVideoMenuButtonEvent,
4
     createRemoteVideoMenuButtonEvent,
5
     sendAnalytics
5
     sendAnalytics
6
 } from '../../analytics';
6
 } from '../../analytics';
7
-import { openDialog } from '../../base/dialog';
8
 import { IconMicDisabled } from '../../base/icons';
7
 import { IconMicDisabled } from '../../base/icons';
9
 import { MEDIA_TYPE } from '../../base/media';
8
 import { MEDIA_TYPE } from '../../base/media';
10
 import { AbstractButton, type AbstractButtonProps } from '../../base/toolbox/components';
9
 import { AbstractButton, type AbstractButtonProps } from '../../base/toolbox/components';
11
 import { isRemoteTrackMuted } from '../../base/tracks';
10
 import { isRemoteTrackMuted } from '../../base/tracks';
12
-
13
-import { MuteRemoteParticipantDialog } from '.';
11
+import { muteRemote } from '../actions.any';
14
 
12
 
15
 export type Props = AbstractButtonProps & {
13
 export type Props = AbstractButtonProps & {
16
 
14
 
61
                 'participant_id': participantID
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
 
2
 
3
 import React from 'react';
3
 import React from 'react';
4
 
4
 
5
+import { requestDisableAudioModeration, requestEnableAudioModeration } from '../../av-moderation/actions';
6
+import { isEnabledFromState } from '../../av-moderation/functions';
5
 import { Dialog } from '../../base/dialog';
7
 import { Dialog } from '../../base/dialog';
6
 import { MEDIA_TYPE } from '../../base/media';
8
 import { MEDIA_TYPE } from '../../base/media';
7
 import { getLocalParticipant, getParticipantDisplayName } from '../../base/participants';
9
 import { getLocalParticipant, getParticipantDisplayName } from '../../base/participants';
19
 
21
 
20
     content: string,
22
     content: string,
21
     exclude: Array<string>,
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
  *
38
  *
30
  * @extends AbstractMuteRemoteParticipantDialog
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
     static defaultProps = {
42
     static defaultProps = {
34
         exclude: [],
43
         exclude: [],
35
         muteLocal: false
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
      * Implements React's {@link Component#render()}.
69
      * Implements React's {@link Component#render()}.
40
      *
70
      *
59
 
89
 
60
     _onSubmit: () => boolean;
90
     _onSubmit: () => boolean;
61
 
91
 
92
+    _onToggleModeration: () => void;
93
+
62
     /**
94
     /**
63
      * Callback to be invoked when the value of this dialog is submitted.
95
      * Callback to be invoked when the value of this dialog is submitted.
64
      *
96
      *
71
         } = this.props;
103
         } = this.props;
72
 
104
 
73
         dispatch(muteAllParticipants(exclude, MEDIA_TYPE.AUDIO));
105
         dispatch(muteAllParticipants(exclude, MEDIA_TYPE.AUDIO));
106
+        if (this.state.audioModerationEnabled) {
107
+            dispatch(requestEnableAudioModeration());
108
+        } else {
109
+            dispatch(requestDisableAudioModeration());
110
+        }
74
 
111
 
75
         return true;
112
         return true;
76
     }
113
     }
97
         content: t('dialog.muteEveryoneElseDialog'),
134
         content: t('dialog.muteEveryoneElseDialog'),
98
         title: t('dialog.muteEveryoneElseTitle', { whom })
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
 
2
 
3
 import React from 'react';
3
 import React from 'react';
4
 
4
 
5
+import { requestDisableVideoModeration, requestEnableVideoModeration } from '../../av-moderation/actions';
6
+import { isEnabledFromState } from '../../av-moderation/functions';
5
 import { Dialog } from '../../base/dialog';
7
 import { Dialog } from '../../base/dialog';
6
 import { MEDIA_TYPE } from '../../base/media';
8
 import { MEDIA_TYPE } from '../../base/media';
7
 import { getLocalParticipant, getParticipantDisplayName } from '../../base/participants';
9
 import { getLocalParticipant, getParticipantDisplayName } from '../../base/participants';
19
 
21
 
20
     content: string,
22
     content: string,
21
     exclude: Array<string>,
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
  *
38
  *
30
  * @extends AbstractMuteRemoteParticipantsVideoDialog
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
     static defaultProps = {
43
     static defaultProps = {
34
         exclude: [],
44
         exclude: [],
35
         muteLocal: false
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
      * Implements React's {@link Component#render()}.
70
      * Implements React's {@link Component#render()}.
40
      *
71
      *
59
 
90
 
60
     _onSubmit: () => boolean;
91
     _onSubmit: () => boolean;
61
 
92
 
93
+    _onToggleModeration: () => void;
94
+
62
     /**
95
     /**
63
      * Callback to be invoked when the value of this dialog is submitted.
96
      * Callback to be invoked when the value of this dialog is submitted.
64
      *
97
      *
71
         } = this.props;
104
         } = this.props;
72
 
105
 
73
         dispatch(muteAllParticipants(exclude, MEDIA_TYPE.VIDEO));
106
         dispatch(muteAllParticipants(exclude, MEDIA_TYPE.VIDEO));
107
+        if (this.state.moderationEnabled) {
108
+            dispatch(requestEnableVideoModeration());
109
+        } else {
110
+            dispatch(requestDisableVideoModeration());
111
+        }
74
 
112
 
75
         return true;
113
         return true;
76
     }
114
     }
84
  * @returns {Props}
122
  * @returns {Props}
85
  */
123
  */
86
 export function abstractMapStateToProps(state: Object, ownProps: Props) {
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
     const whom = exclude
128
     const whom = exclude
90
         // eslint-disable-next-line no-confusing-arrow
129
         // eslint-disable-next-line no-confusing-arrow
97
         content: t('dialog.muteEveryoneElsesVideoDialog'),
136
         content: t('dialog.muteEveryoneElsesVideoDialog'),
98
         title: t('dialog.muteEveryoneElsesVideoTitle', { whom })
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
  *
32
  *
33
  * @extends Component
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
      * Initializes a new {@code AbstractMuteRemoteParticipantDialog} instance.
38
      * Initializes a new {@code AbstractMuteRemoteParticipantDialog} instance.
39
      *
39
      *

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

32
  *
32
  *
33
  * @extends Component
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
      * Initializes a new {@code AbstractMuteRemoteParticipantsVideoDialog} instance.
38
      * Initializes a new {@code AbstractMuteRemoteParticipantsVideoDialog} instance.
39
      *
39
      *

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

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
 export { default as KickRemoteParticipantDialog } from './KickRemoteParticipantDialog';
5
 export { default as KickRemoteParticipantDialog } from './KickRemoteParticipantDialog';
6
 export { default as MuteEveryoneDialog } from './MuteEveryoneDialog';
6
 export { default as MuteEveryoneDialog } from './MuteEveryoneDialog';
7
 export { default as MuteEveryonesVideoDialog } from './MuteEveryonesVideoDialog';
7
 export { default as MuteEveryonesVideoDialog } from './MuteEveryonesVideoDialog';
8
-export { default as MuteRemoteParticipantDialog } from './MuteRemoteParticipantDialog';
9
 export { default as MuteRemoteParticipantsVideoDialog } from './MuteRemoteParticipantsVideoDialog';
8
 export { default as MuteRemoteParticipantsVideoDialog } from './MuteRemoteParticipantsVideoDialog';
10
 export { default as RemoteVideoMenu } from './RemoteVideoMenu';
9
 export { default as RemoteVideoMenu } from './RemoteVideoMenu';
11
 export { default as SharedVideoMenu } from './SharedVideoMenu';
10
 export { default as SharedVideoMenu } from './SharedVideoMenu';

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

6
 import { translate } from '../../../base/i18n';
6
 import { translate } from '../../../base/i18n';
7
 import { connect } from '../../../base/redux';
7
 import { connect } from '../../../base/redux';
8
 import AbstractGrantModeratorDialog
8
 import AbstractGrantModeratorDialog
9
-    from '../AbstractGrantModeratorDialog';
9
+, { abstractMapStateToProps } from '../AbstractGrantModeratorDialog';
10
 
10
 
11
 /**
11
 /**
12
  * Dialog to confirm a grant moderator action.
12
  * Dialog to confirm a grant moderator action.
26
                 titleKey = 'dialog.grantModeratorTitle'
26
                 titleKey = 'dialog.grantModeratorTitle'
27
                 width = 'small'>
27
                 width = 'small'>
28
                 <div>
28
                 <div>
29
-                    { this.props.t('dialog.grantModeratorDialog') }
29
+                    { this.props.t('dialog.grantModeratorDialog', { participantName: this.props.participantName }) }
30
                 </div>
30
                 </div>
31
             </Dialog>
31
             </Dialog>
32
         );
32
         );
35
     _onSubmit: () => boolean;
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
 
4
 
5
 import { Dialog } from '../../../base/dialog';
5
 import { Dialog } from '../../../base/dialog';
6
 import { translate } from '../../../base/i18n';
6
 import { translate } from '../../../base/i18n';
7
+import { Switch } from '../../../base/react';
7
 import { connect } from '../../../base/redux';
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
  * A React Component with the contents for a dialog that asks for confirmation
13
  * A React Component with the contents for a dialog that asks for confirmation
14
  * @extends AbstractMuteEveryoneDialog
16
  * @extends AbstractMuteEveryoneDialog
15
  */
17
  */
16
 class MuteEveryoneDialog extends AbstractMuteEveryoneDialog<Props> {
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
      * Implements React's {@link Component#render()}.
37
      * Implements React's {@link Component#render()}.
19
      *
38
      *
27
                 onSubmit = { this._onSubmit }
46
                 onSubmit = { this._onSubmit }
28
                 titleString = { this.props.title }
47
                 titleString = { this.props.title }
29
                 width = 'small'>
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
                 </div>
65
                 </div>
33
             </Dialog>
66
             </Dialog>
34
         );
67
         );

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

4
 
4
 
5
 import { Dialog } from '../../../base/dialog';
5
 import { Dialog } from '../../../base/dialog';
6
 import { translate } from '../../../base/i18n';
6
 import { translate } from '../../../base/i18n';
7
+import { Switch } from '../../../base/react';
7
 import { connect } from '../../../base/redux';
8
 import { connect } from '../../../base/redux';
8
 import AbstractMuteEveryonesVideoDialog, { abstractMapStateToProps, type Props }
9
 import AbstractMuteEveryonesVideoDialog, { abstractMapStateToProps, type Props }
9
     from '../AbstractMuteEveryonesVideoDialog';
10
     from '../AbstractMuteEveryonesVideoDialog';
15
  * @extends AbstractMuteEveryonesVideoDialog
16
  * @extends AbstractMuteEveryonesVideoDialog
16
  */
17
  */
17
 class MuteEveryonesVideoDialog extends AbstractMuteEveryonesVideoDialog<Props> {
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
      * Implements React's {@link Component#render()}.
37
      * Implements React's {@link Component#render()}.
20
      *
38
      *
28
                 onSubmit = { this._onSubmit }
46
                 onSubmit = { this._onSubmit }
29
                 titleString = { this.props.title }
47
                 titleString = { this.props.title }
30
                 width = 'small'>
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
                 </div>
65
                 </div>
34
             </Dialog>
66
             </Dialog>
35
         );
67
         );

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

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
 export { default as MuteEveryonesVideoDialog } from './MuteEveryonesVideoDialog';
11
 export { default as MuteEveryonesVideoDialog } from './MuteEveryonesVideoDialog';
12
 export { default as MuteEveryoneElseButton } from './MuteEveryoneElseButton';
12
 export { default as MuteEveryoneElseButton } from './MuteEveryoneElseButton';
13
 export { default as MuteEveryoneElsesVideoButton } from './MuteEveryoneElsesVideoButton';
13
 export { default as MuteEveryoneElsesVideoButton } from './MuteEveryoneElsesVideoButton';
14
-export { default as MuteRemoteParticipantDialog } from './MuteRemoteParticipantDialog';
15
 export { default as MuteRemoteParticipantsVideoDialog } from './MuteRemoteParticipantsVideoDialog';
14
 export { default as MuteRemoteParticipantsVideoDialog } from './MuteRemoteParticipantsVideoDialog';
16
 export { default as PrivateMessageMenuButton } from './PrivateMessageMenuButton';
15
 export { default as PrivateMessageMenuButton } from './PrivateMessageMenuButton';
17
 export { REMOTE_CONTROL_MENU_STATES, default as RemoteControlButton } from './RemoteControlButton';
16
 export { REMOTE_CONTROL_MENU_STATES, default as RemoteControlButton } from './RemoteControlButton';

Loading…
Cancel
Save