Browse Source

feat: Added mute video moderation feature (#8630)

* Added mute video feature

* Fixed export

* Fixed some issues

* Added remote video mute notification

* Fixed import

* Fixed conference event handling

* Fixed some linting issues

* Fixed more linter errors

* turn screenshare off on remote video mute

* Fix linter issue

* translations added for mute video feature

* Added video mute button to interface config

* Updated lib-jitsi-meet

* Fix copy paste error

Co-authored-by: nurjinn jafar <nurjin.jafar@nordeck.net>
j8
Steffen Kolmer 4 years ago
parent
commit
23bb824731
No account linked to committer's email address
32 changed files with 754 additions and 46 deletions
  1. 4
    1
      conference.js
  2. 0
    1
      css/_popup_menu.scss
  3. 1
    1
      interface_config.js
  4. 17
    0
      lang/main-de.json
  5. 17
    0
      lang/main.json
  6. 5
    2
      modules/API/API.js
  7. 2
    2
      package-lock.json
  8. 1
    1
      package.json
  9. 4
    2
      react/features/analytics/AnalyticsEvents.js
  10. 2
    2
      react/features/base/conference/actions.js
  11. 2
    0
      react/features/base/icons/svg/index.js
  12. 12
    0
      react/features/base/icons/svg/mute-video-everyone-else.svg
  13. 12
    0
      react/features/base/icons/svg/mute-video-everyone.svg
  14. 15
    7
      react/features/base/participants/actions.js
  15. 1
    1
      react/features/base/participants/middleware.js
  16. 2
    1
      react/features/mobile/external-api/middleware.js
  17. 33
    11
      react/features/remote-video-menu/actions.js
  18. 2
    1
      react/features/remote-video-menu/components/AbstractMuteEveryoneDialog.js
  19. 48
    0
      react/features/remote-video-menu/components/AbstractMuteEveryoneElsesVideoButton.js
  20. 103
    0
      react/features/remote-video-menu/components/AbstractMuteEveryonesVideoDialog.js
  21. 2
    1
      react/features/remote-video-menu/components/AbstractMuteRemoteParticipantDialog.js
  22. 65
    0
      react/features/remote-video-menu/components/AbstractMuteRemoteParticipantsVideoDialog.js
  23. 103
    0
      react/features/remote-video-menu/components/AbstractMuteVideoButton.js
  24. 54
    0
      react/features/remote-video-menu/components/web/MuteEveryoneElsesVideoButton.js
  25. 41
    0
      react/features/remote-video-menu/components/web/MuteEveryonesVideoDialog.js
  26. 41
    0
      react/features/remote-video-menu/components/web/MuteRemoteParticipantsVideoDialog.js
  27. 67
    0
      react/features/remote-video-menu/components/web/MuteVideoButton.js
  28. 12
    11
      react/features/remote-video-menu/components/web/RemoteVideoMenuTriggerButton.js
  29. 4
    0
      react/features/remote-video-menu/components/web/index.js
  30. 1
    1
      react/features/toolbox/components/AudioMuteButton.js
  31. 76
    0
      react/features/toolbox/components/MuteEveryonesVideoButton.js
  32. 5
    0
      react/features/toolbox/components/web/Toolbox.js

+ 4
- 1
conference.js View File

2008
 
2008
 
2009
         room.on(JitsiConferenceEvents.TRACK_MUTE_CHANGED, (track, participantThatMutedUs) => {
2009
         room.on(JitsiConferenceEvents.TRACK_MUTE_CHANGED, (track, participantThatMutedUs) => {
2010
             if (participantThatMutedUs) {
2010
             if (participantThatMutedUs) {
2011
-                APP.store.dispatch(participantMutedUs(participantThatMutedUs));
2011
+                APP.store.dispatch(participantMutedUs(participantThatMutedUs, track));
2012
+                if (this.isSharingScreen && track.isVideoTrack()) {
2013
+                    this._turnScreenSharingOff(false);
2014
+                }
2012
             }
2015
             }
2013
         });
2016
         });
2014
 
2017
 

+ 0
- 1
css/_popup_menu.scss View File

6
     min-width: 75px;
6
     min-width: 75px;
7
     text-align: left;
7
     text-align: left;
8
     padding: 0px;
8
     padding: 0px;
9
-    width: 180px;
10
     white-space: nowrap;
9
     white-space: nowrap;
11
 
10
 
12
     &__item {
11
     &__item {

+ 1
- 1
interface_config.js View File

206
         'fodeviceselection', 'hangup', 'profile', 'chat', 'recording',
206
         'fodeviceselection', 'hangup', 'profile', 'chat', 'recording',
207
         'livestreaming', 'etherpad', 'sharedvideo', 'settings', 'raisehand',
207
         'livestreaming', 'etherpad', 'sharedvideo', 'settings', 'raisehand',
208
         'videoquality', 'filmstrip', 'invite', 'feedback', 'stats', 'shortcuts',
208
         'videoquality', 'filmstrip', 'invite', 'feedback', 'stats', 'shortcuts',
209
-        'tileview', 'videobackgroundblur', 'download', 'help', 'mute-everyone', 'security'
209
+        'tileview', 'videobackgroundblur', 'download', 'help', 'mute-everyone', 'mute-video-everyone', 'security'
210
     ],
210
     ],
211
 
211
 
212
     TOOLBAR_TIMEOUT: 4000,
212
     TOOLBAR_TIMEOUT: 4000,

+ 17
- 0
lang/main-de.json View File

239
         "muteEveryoneElseTitle": "Alle außer {{whom}} stummschalten?",
239
         "muteEveryoneElseTitle": "Alle außer {{whom}} stummschalten?",
240
         "muteEveryoneDialog": "Wollen Sie wirklich alle stummschalten? Sie können deren Stummschaltung nicht mehr beenden, aber sie können ihre Stummschaltung jederzeit selbst beenden.",
240
         "muteEveryoneDialog": "Wollen Sie wirklich alle stummschalten? Sie können deren Stummschaltung nicht mehr beenden, aber sie können ihre Stummschaltung jederzeit selbst beenden.",
241
         "muteEveryoneTitle": "Alle stummschalten?",
241
         "muteEveryoneTitle": "Alle stummschalten?",
242
+        "muteEveryoneElsesVideoDialog": "Sobald die Kamera deaktiviert ist, können Sie sie nicht wieder aktivieren, die Teilnehmer können dies aber jederzeit wieder ändern.",
243
+        "muteEveryoneElsesVideoTitle": "Die Kamera von allen außer {{whom}} ausschalten?",
244
+        "muteEveryonesVideoDialog": "Sind Sie sicher, dass Sie die Kamera von allen Teilnehmern deaktivieren möchten? Sie können sie nicht wieder aktivieren, die Teilnehmer können dies aber jederzeit wieder ändern.",
245
+        "muteEveryonesVideoTitle": "Die Kamera von allen anderen ausschalten?",
242
         "muteEveryoneSelf": "sich selbst",
246
         "muteEveryoneSelf": "sich selbst",
243
         "muteEveryoneStartMuted": "Alle beginnen von jetzt an stummgeschaltet",
247
         "muteEveryoneStartMuted": "Alle beginnen von jetzt an stummgeschaltet",
244
         "muteParticipantBody": "Sie können die Stummschaltung anderer Personen nicht aufheben, aber eine Person kann ihre eigene Stummschaltung jederzeit beenden.",
248
         "muteParticipantBody": "Sie können die Stummschaltung anderer Personen nicht aufheben, aber eine Person kann ihre eigene Stummschaltung jederzeit beenden.",
245
         "muteParticipantButton": "Stummschalten",
249
         "muteParticipantButton": "Stummschalten",
246
         "muteParticipantDialog": "Wollen Sie diese Person wirklich stummschalten? Sie können die Stummschaltung nicht wieder aufheben, die Person kann dies aber jederzeit selbst tun.",
250
         "muteParticipantDialog": "Wollen Sie diese Person wirklich stummschalten? Sie können die Stummschaltung nicht wieder aufheben, die Person kann dies aber jederzeit selbst tun.",
247
         "muteParticipantTitle": "Person stummschalten?",
251
         "muteParticipantTitle": "Person stummschalten?",
252
+        "muteParticipantsVideoButton": "Kamera ausschalten",
253
+        "muteParticipantsVideoTitle": "Die Kamera von dieser Person ausschalten?",
254
+        "muteParticipantsVideoBody": "Sie können die Kamera nicht wieder aktivieren, die Teilnehmer können dies aber jederzeit wieder ändern.",
248
         "Ok": "OK",
255
         "Ok": "OK",
249
         "passwordLabel": "Dieses Meeting wurde gesichert. Bitte geben Sie das $t(lockRoomPasswordUppercase) ein, um dem Meeting beizutreten.",
256
         "passwordLabel": "Dieses Meeting wurde gesichert. Bitte geben Sie das $t(lockRoomPasswordUppercase) ein, um dem Meeting beizutreten.",
250
         "passwordNotSupported": "Das Festlegen eines Konferenzpassworts wird nicht unterstützt.",
257
         "passwordNotSupported": "Das Festlegen eines Konferenzpassworts wird nicht unterstützt.",
484
         "mutedTitle": "Stummschaltung aktiv!",
491
         "mutedTitle": "Stummschaltung aktiv!",
485
         "mutedRemotelyTitle": "Sie wurden von {{participantDisplayName}} stummgeschaltet!",
492
         "mutedRemotelyTitle": "Sie wurden von {{participantDisplayName}} stummgeschaltet!",
486
         "mutedRemotelyDescription": "Sie können jederzeit die Stummschaltung aufheben, wenn Sie bereit sind zu sprechen. Wenn Sie fertig sind, können Sie sich wieder stummschalten, um Geräusche vom Meeting fernzuhalten.",
493
         "mutedRemotelyDescription": "Sie können jederzeit die Stummschaltung aufheben, wenn Sie bereit sind zu sprechen. Wenn Sie fertig sind, können Sie sich wieder stummschalten, um Geräusche vom Meeting fernzuhalten.",
494
+        "videoMutedRemotelyTitle": "Ihre Kamera wurde von {{participantDisplayName}} ausgeschaltet!",
495
+        "videoMutedRemotelyDescription": "Sie können sie jederzeit wieder einschalten.",
487
         "passwordRemovedRemotely": "$t(lockRoomPasswordUppercase) von einer anderen Person entfernt",
496
         "passwordRemovedRemotely": "$t(lockRoomPasswordUppercase) von einer anderen Person entfernt",
488
         "passwordSetRemotely": "$t(lockRoomPasswordUppercase) von einer anderen Person gesetzt",
497
         "passwordSetRemotely": "$t(lockRoomPasswordUppercase) von einer anderen Person gesetzt",
489
         "raisedHand": "{{name}} möchte sprechen.",
498
         "raisedHand": "{{name}} möchte sprechen.",
714
             "moreOptions": "Menü „Weitere Optionen“",
723
             "moreOptions": "Menü „Weitere Optionen“",
715
             "mute": "„Audio stummschalten“ ein-/ausschalten",
724
             "mute": "„Audio stummschalten“ ein-/ausschalten",
716
             "muteEveryone": "Alle stummschalten",
725
             "muteEveryone": "Alle stummschalten",
726
+            "muteEveryoneElse": "Alle anderen stummschalten",
727
+            "muteEveryonesVideo": "Alle Kameras ausschalten",
728
+            "muteEveryoneElsesVideo": "Alle anderen Kameras ausschalten",
717
             "pip": "Bild-in-Bild-Modus ein-/ausschalten",
729
             "pip": "Bild-in-Bild-Modus ein-/ausschalten",
718
             "privateMessage": "Private Nachricht senden",
730
             "privateMessage": "Private Nachricht senden",
719
             "profile": "Profil bearbeiten",
731
             "profile": "Profil bearbeiten",
720
             "raiseHand": "„Melden“ ein-/ausschalten",
732
             "raiseHand": "„Melden“ ein-/ausschalten",
721
             "recording": "Aufzeichnung ein-/ausschalten",
733
             "recording": "Aufzeichnung ein-/ausschalten",
722
             "remoteMute": "Personen stummschalten",
734
             "remoteMute": "Personen stummschalten",
735
+            "remoteVideoMute": "Kamera von dieser Person ausschalten",
723
             "security": "Sicherheitsoptionen",
736
             "security": "Sicherheitsoptionen",
724
             "Settings": "Einstellungen ein-/ausschalten",
737
             "Settings": "Einstellungen ein-/ausschalten",
725
             "sharedvideo": "YouTube-Videofreigabe ein-/ausschalten",
738
             "sharedvideo": "YouTube-Videofreigabe ein-/ausschalten",
764
         "moreOptions": "Weitere Optionen",
777
         "moreOptions": "Weitere Optionen",
765
         "mute": "Stummschaltung aktivieren / deaktivieren",
778
         "mute": "Stummschaltung aktivieren / deaktivieren",
766
         "muteEveryone": "Alle stummschalten",
779
         "muteEveryone": "Alle stummschalten",
780
+        "muteEveryonesVideo": "Alle Kameras ausschalten",
767
         "noAudioSignalTitle": "Es kommt kein Input von Ihrem Mikrofon!",
781
         "noAudioSignalTitle": "Es kommt kein Input von Ihrem Mikrofon!",
768
         "noAudioSignalDesc": "Wenn Sie das Gerät nicht absichtlich über die Systemeinstellungen oder die Hardware stumm geschaltet haben, sollten Sie einen Wechsel des Geräts in Erwägung ziehen.",
782
         "noAudioSignalDesc": "Wenn Sie das Gerät nicht absichtlich über die Systemeinstellungen oder die Hardware stumm geschaltet haben, sollten Sie einen Wechsel des Geräts in Erwägung ziehen.",
769
         "noAudioSignalDescSuggestion": "Wenn Sie das Gerät nicht absichtlich über die Systemeinstellungen oder die Hardware stummgeschaltet haben, sollten Sie einen Wechsel auf das vorgeschlagene Gerät in Erwägung ziehen.",
783
         "noAudioSignalDescSuggestion": "Wenn Sie das Gerät nicht absichtlich über die Systemeinstellungen oder die Hardware stummgeschaltet haben, sollten Sie einen Wechsel auf das vorgeschlagene Gerät in Erwägung ziehen.",
849
     },
863
     },
850
     "videothumbnail": {
864
     "videothumbnail": {
851
         "domute": "Stummschalten",
865
         "domute": "Stummschalten",
866
+        "domuteVideo": "Kamera ausschalten",
852
         "domuteOthers": "Alle anderen stummschalten",
867
         "domuteOthers": "Alle anderen stummschalten",
868
+        "domuteVideoOfOthers": "Alle anderen Kameras auschalten",
853
         "flip": "Spiegeln",
869
         "flip": "Spiegeln",
854
         "grantModerator": "Moderationsrechte vergeben",
870
         "grantModerator": "Moderationsrechte vergeben",
855
         "kick": "Hinauswerfen",
871
         "kick": "Hinauswerfen",
856
         "moderator": "Moderation",
872
         "moderator": "Moderation",
857
         "mute": "Person ist stumm geschaltet",
873
         "mute": "Person ist stumm geschaltet",
858
         "muted": "Stummgeschaltet",
874
         "muted": "Stummgeschaltet",
875
+        "videoMuted": "Kamera ausgeschaltet",
859
         "remoteControl": "Fernsteuerung",
876
         "remoteControl": "Fernsteuerung",
860
         "show": "Im Vordergrund anzeigen",
877
         "show": "Im Vordergrund anzeigen",
861
         "videomute": "Person hat die Kamera angehalten"
878
         "videomute": "Person hat die Kamera angehalten"

+ 17
- 0
lang/main.json View File

241
         "muteEveryoneElseTitle": "Mute everyone except {{whom}}?",
241
         "muteEveryoneElseTitle": "Mute everyone except {{whom}}?",
242
         "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.",
242
         "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.",
243
         "muteEveryoneTitle": "Mute everyone?",
243
         "muteEveryoneTitle": "Mute everyone?",
244
+        "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.",
245
+        "muteEveryoneElsesVideoTitle": "Disable everyone's camera except {{whom}}?",
246
+        "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.",
247
+        "muteEveryonesVideoTitle": "Disable everyone's camera?",
244
         "muteEveryoneSelf": "yourself",
248
         "muteEveryoneSelf": "yourself",
245
         "muteEveryoneStartMuted": "Everyone starts muted from now on",
249
         "muteEveryoneStartMuted": "Everyone starts muted from now on",
246
         "muteParticipantBody": "You won't be able to unmute them, but they can unmute themselves at any time.",
250
         "muteParticipantBody": "You won't be able to unmute them, but they can unmute themselves at any time.",
247
         "muteParticipantButton": "Mute",
251
         "muteParticipantButton": "Mute",
248
         "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.",
252
         "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.",
249
         "muteParticipantTitle": "Mute this participant?",
253
         "muteParticipantTitle": "Mute this participant?",
254
+        "muteParticipantsVideoButton": "Disable camera",
255
+        "muteParticipantsVideoTitle": "Disable camera of this participant?",
256
+        "muteParticipantsVideoBody": "You won't be able to turn the camera back on, but they can turn it back on at any time.",
250
         "Ok": "OK",
257
         "Ok": "OK",
251
         "passwordLabel": "The meeting has been locked by a participant. Please enter the $t(lockRoomPassword) to join.",
258
         "passwordLabel": "The meeting has been locked by a participant. Please enter the $t(lockRoomPassword) to join.",
252
         "passwordNotSupported": "Setting a meeting $t(lockRoomPassword) is not supported.",
259
         "passwordNotSupported": "Setting a meeting $t(lockRoomPassword) is not supported.",
484
         "mutedTitle": "You're muted!",
491
         "mutedTitle": "You're muted!",
485
         "mutedRemotelyTitle": "You have been muted by {{participantDisplayName}}!",
492
         "mutedRemotelyTitle": "You have been muted by {{participantDisplayName}}!",
486
         "mutedRemotelyDescription": "You can always unmute when you're ready to speak. Mute back when you're done to keep noise away from the meeting.",
493
         "mutedRemotelyDescription": "You can always unmute when you're ready to speak. Mute back when you're done to keep noise away from the meeting.",
494
+        "videoMutedRemotelyTitle": "Your camera has been disabled by {{participantDisplayName}}!",
495
+        "videoMutedRemotelyDescription": "You can always turn it on again.",
487
         "passwordRemovedRemotely": "$t(lockRoomPasswordUppercase) removed by another participant",
496
         "passwordRemovedRemotely": "$t(lockRoomPasswordUppercase) removed by another participant",
488
         "passwordSetRemotely": "$t(lockRoomPasswordUppercase) set by another participant",
497
         "passwordSetRemotely": "$t(lockRoomPasswordUppercase) set by another participant",
489
         "raisedHand": "{{name}} would like to speak.",
498
         "raisedHand": "{{name}} would like to speak.",
716
             "moreOptions": "Show more options",
725
             "moreOptions": "Show more options",
717
             "mute": "Toggle mute audio",
726
             "mute": "Toggle mute audio",
718
             "muteEveryone": "Mute everyone",
727
             "muteEveryone": "Mute everyone",
728
+            "muteEveryoneElse": "Mute everyone else",
729
+            "muteEveryonesVideo": "Disable everyone's camera",
730
+            "muteEveryoneElsesVideo": "Disable everyone else's camera",
719
             "pip": "Toggle Picture-in-Picture mode",
731
             "pip": "Toggle Picture-in-Picture mode",
720
             "privateMessage": "Send private message",
732
             "privateMessage": "Send private message",
721
             "profile": "Edit your profile",
733
             "profile": "Edit your profile",
722
             "raiseHand": "Toggle raise hand",
734
             "raiseHand": "Toggle raise hand",
723
             "recording": "Toggle recording",
735
             "recording": "Toggle recording",
724
             "remoteMute": "Mute participant",
736
             "remoteMute": "Mute participant",
737
+            "remoteVideoMute": "Disable camera of participant",
725
             "security": "Security options",
738
             "security": "Security options",
726
             "Settings": "Toggle settings",
739
             "Settings": "Toggle settings",
727
             "sharedvideo": "Toggle Youtube video sharing",
740
             "sharedvideo": "Toggle Youtube video sharing",
766
         "moreOptions": "More options",
779
         "moreOptions": "More options",
767
         "mute": "Mute / Unmute",
780
         "mute": "Mute / Unmute",
768
         "muteEveryone": "Mute everyone",
781
         "muteEveryone": "Mute everyone",
782
+        "muteEveryonesVideo": "Disable everyone's camera",
769
         "noAudioSignalTitle": "There is no input coming from your mic!",
783
         "noAudioSignalTitle": "There is no input coming from your mic!",
770
         "noAudioSignalDesc": "If you did not purposely mute it from system settings or hardware, consider switching the device.",
784
         "noAudioSignalDesc": "If you did not purposely mute it from system settings or hardware, consider switching the device.",
771
         "noAudioSignalDescSuggestion": "If you did not purposely mute it from system settings or hardware, consider switching to the suggested device.",
785
         "noAudioSignalDescSuggestion": "If you did not purposely mute it from system settings or hardware, consider switching to the suggested device.",
850
     "videothumbnail": {
864
     "videothumbnail": {
851
         "connectionInfo": "Connection Info",
865
         "connectionInfo": "Connection Info",
852
         "domute": "Mute",
866
         "domute": "Mute",
867
+        "domuteVideo": "Disable camera",
853
         "domuteOthers": "Mute everyone else",
868
         "domuteOthers": "Mute everyone else",
869
+        "domuteVideoOfOthers": "Disable camera of everyone else",
854
         "flip": "Flip",
870
         "flip": "Flip",
855
         "grantModerator": "Grant Moderator",
871
         "grantModerator": "Grant Moderator",
856
         "kick": "Kick out",
872
         "kick": "Kick out",
857
         "moderator": "Moderator",
873
         "moderator": "Moderator",
858
         "mute": "Participant is muted",
874
         "mute": "Participant is muted",
859
         "muted": "Muted",
875
         "muted": "Muted",
876
+        "videoMuted": "Camera disabled",
860
         "remoteControl": "Start / Stop remote control",
877
         "remoteControl": "Start / Stop remote control",
861
         "show": "Show on stage",
878
         "show": "Show on stage",
862
         "videomute": "Participant has stopped the camera"
879
         "videomute": "Participant has stopped the camera"

+ 5
- 2
modules/API/API.js View File

14
 } from '../../react/features/base/conference';
14
 } from '../../react/features/base/conference';
15
 import { parseJWTFromURLParams } from '../../react/features/base/jwt';
15
 import { parseJWTFromURLParams } from '../../react/features/base/jwt';
16
 import JitsiMeetJS, { JitsiRecordingConstants } from '../../react/features/base/lib-jitsi-meet';
16
 import JitsiMeetJS, { JitsiRecordingConstants } from '../../react/features/base/lib-jitsi-meet';
17
+import { MEDIA_TYPE } from '../../react/features/base/media';
17
 import { pinParticipant, getParticipantById, kickParticipant } from '../../react/features/base/participants';
18
 import { pinParticipant, getParticipantById, kickParticipant } from '../../react/features/base/participants';
18
 import { setPrivateMessageRecipient } from '../../react/features/chat/actions';
19
 import { setPrivateMessageRecipient } from '../../react/features/chat/actions';
19
 import { openChat } from '../../react/features/chat/actions.web';
20
 import { openChat } from '../../react/features/chat/actions.web';
79
             sendAnalytics(createApiEvent('display.name.changed'));
80
             sendAnalytics(createApiEvent('display.name.changed'));
80
             APP.conference.changeLocalDisplayName(displayName);
81
             APP.conference.changeLocalDisplayName(displayName);
81
         },
82
         },
82
-        'mute-everyone': () => {
83
+        'mute-everyone': mediaType => {
84
+            const muteMediaType = mediaType ? mediaType : MEDIA_TYPE.AUDIO;
85
+
83
             sendAnalytics(createApiEvent('muted-everyone'));
86
             sendAnalytics(createApiEvent('muted-everyone'));
84
             const participants = APP.store.getState()['features/base/participants'];
87
             const participants = APP.store.getState()['features/base/participants'];
85
             const localIds = participants
88
             const localIds = participants
87
                 .filter(participant => participant.role === 'moderator')
90
                 .filter(participant => participant.role === 'moderator')
88
                 .map(participant => participant.id);
91
                 .map(participant => participant.id);
89
 
92
 
90
-            APP.store.dispatch(muteAllParticipants(localIds));
93
+            APP.store.dispatch(muteAllParticipants(localIds, muteMediaType));
91
         },
94
         },
92
         'toggle-lobby': isLobbyEnabled => {
95
         'toggle-lobby': isLobbyEnabled => {
93
             APP.store.dispatch(toggleLobbyMode(isLobbyEnabled));
96
             APP.store.dispatch(toggleLobbyMode(isLobbyEnabled));

+ 2
- 2
package-lock.json View File

10343
       }
10343
       }
10344
     },
10344
     },
10345
     "lib-jitsi-meet": {
10345
     "lib-jitsi-meet": {
10346
-      "version": "github:jitsi/lib-jitsi-meet#6a7b16c33e27481b03e5a37636e72426c0848265",
10347
-      "from": "github:jitsi/lib-jitsi-meet#6a7b16c33e27481b03e5a37636e72426c0848265",
10346
+      "version": "github:jitsi/lib-jitsi-meet#9beb47fe5f5ae9caf0b206588e14fa676479ce31",
10347
+      "from": "github:jitsi/lib-jitsi-meet#9beb47fe5f5ae9caf0b206588e14fa676479ce31",
10348
       "requires": {
10348
       "requires": {
10349
         "@jitsi/js-utils": "1.0.2",
10349
         "@jitsi/js-utils": "1.0.2",
10350
         "@jitsi/sdp-interop": "1.0.3",
10350
         "@jitsi/sdp-interop": "1.0.3",

+ 1
- 1
package.json View File

56
     "jquery-i18next": "1.2.1",
56
     "jquery-i18next": "1.2.1",
57
     "js-md5": "0.6.1",
57
     "js-md5": "0.6.1",
58
     "jwt-decode": "2.2.0",
58
     "jwt-decode": "2.2.0",
59
-    "lib-jitsi-meet": "github:jitsi/lib-jitsi-meet#6a7b16c33e27481b03e5a37636e72426c0848265",
59
+    "lib-jitsi-meet": "github:jitsi/lib-jitsi-meet#9beb47fe5f5ae9caf0b206588e14fa676479ce31",
60
     "libflacjs": "github:mmig/libflac.js#93d37e7f811f01cf7d8b6a603e38bd3c3810907d",
60
     "libflacjs": "github:mmig/libflac.js#93d37e7f811f01cf7d8b6a603e38bd3c3810907d",
61
     "lodash": "4.17.19",
61
     "lodash": "4.17.19",
62
     "moment": "2.19.4",
62
     "moment": "2.19.4",

+ 4
- 2
react/features/analytics/AnalyticsEvents.js View File

504
  *
504
  *
505
  * @param {string} participantId - The ID of the participant that was remotely
505
  * @param {string} participantId - The ID of the participant that was remotely
506
  * muted.
506
  * muted.
507
+ * @param {string} mediaType - The media type of the channel to mute.
507
  * @returns {Object} The event in a format suitable for sending via
508
  * @returns {Object} The event in a format suitable for sending via
508
  * sendAnalytics.
509
  * sendAnalytics.
509
  */
510
  */
510
-export function createRemoteMuteConfirmedEvent(participantId) {
511
+export function createRemoteMuteConfirmedEvent(participantId, mediaType) {
511
     return {
512
     return {
512
         action: 'clicked',
513
         action: 'clicked',
513
         actionSubject: 'remote.mute.dialog.confirm.button',
514
         actionSubject: 'remote.mute.dialog.confirm.button',
514
         attributes: {
515
         attributes: {
515
-            'participant_id': participantId
516
+            'participant_id': participantId,
517
+            'media_type': mediaType
516
         },
518
         },
517
         source: 'remote.mute.dialog',
519
         source: 'remote.mute.dialog',
518
         type: TYPE_UI
520
         type: TYPE_UI

+ 2
- 2
react/features/base/conference/actions.js View File

149
 
149
 
150
     conference.on(
150
     conference.on(
151
         JitsiConferenceEvents.TRACK_MUTE_CHANGED,
151
         JitsiConferenceEvents.TRACK_MUTE_CHANGED,
152
-        (_, participantThatMutedUs) => {
152
+        (track, participantThatMutedUs) => {
153
             if (participantThatMutedUs) {
153
             if (participantThatMutedUs) {
154
-                dispatch(participantMutedUs(participantThatMutedUs));
154
+                dispatch(participantMutedUs(participantThatMutedUs, track));
155
             }
155
             }
156
         });
156
         });
157
 
157
 

+ 2
- 0
react/features/base/icons/svg/index.js View File

68
 export { default as IconModerator } from './star.svg';
68
 export { default as IconModerator } from './star.svg';
69
 export { default as IconMuteEveryone } from './mute-everyone.svg';
69
 export { default as IconMuteEveryone } from './mute-everyone.svg';
70
 export { default as IconMuteEveryoneElse } from './mute-everyone-else.svg';
70
 export { default as IconMuteEveryoneElse } from './mute-everyone-else.svg';
71
+export { default as IconMuteVideoEveryone } from './mute-video-everyone.svg';
72
+export { default as IconMuteVideoEveryoneElse } from './mute-video-everyone-else.svg';
71
 export { default as IconNotificationJoin } from './navigate_next.svg';
73
 export { default as IconNotificationJoin } from './navigate_next.svg';
72
 export { default as IconOpenInNew } from './open_in_new.svg';
74
 export { default as IconOpenInNew } from './open_in_new.svg';
73
 export { default as IconOutlook } from './office365.svg';
75
 export { default as IconOutlook } from './office365.svg';

+ 12
- 0
react/features/base/icons/svg/mute-video-everyone-else.svg View File

1
+<?xml version="1.0" encoding="utf-8"?>
2
+<!-- Generator: Adobe Illustrator 16.0.3, SVG Export Plug-In . SVG Version: 6.00 Build 0)  -->
3
+<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
4
+<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
5
+	 width="24px" height="24px" viewBox="0 0 24 24" enable-background="new 0 0 24 24" xml:space="preserve">
6
+<path fill="#FFFFFF" d="M3.136,7.1l14.448,14.447l-1.033,1.032l-2.598-2.599c-0.115,0.077-0.307,0.153-0.459,0.153H3.709
7
+	c-0.458,0-0.803-0.346-0.803-0.804v-8.179c0-0.459,0.344-0.803,0.803-0.803h0.612L2.104,8.131L3.136,7.1z M17.584,10.769v8.714
8
+	l-9.135-9.134h5.045c0.459,0,0.84,0.344,0.84,0.803v2.866L17.584,10.769z"/>
9
+<path fill="#FFFFFF" d="M14.688,0.818l8.164,8.165l-0.584,0.583L20.8,8.098c-0.065,0.043-0.174,0.086-0.26,0.086h-5.528
10
+	c-0.259,0-0.454-0.195-0.454-0.454V3.108c0-0.26,0.195-0.454,0.454-0.454h0.345l-1.253-1.253L14.688,0.818z M22.852,2.892v4.924
11
+	l-5.162-5.162h2.851c0.26,0,0.476,0.194,0.476,0.454v1.619L22.852,2.892z"/>
12
+</svg>

+ 12
- 0
react/features/base/icons/svg/mute-video-everyone.svg View File

1
+<?xml version="1.0" encoding="utf-8"?>
2
+<!-- Generator: Adobe Illustrator 16.0.3, SVG Export Plug-In . SVG Version: 6.00 Build 0)  -->
3
+<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
4
+<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
5
+	 width="24px" height="24px" viewBox="0 0 24 24" enable-background="new 0 0 24 24" xml:space="preserve">
6
+<path fill="#A4B8D1" d="M3.136,7.1l14.448,14.447l-1.033,1.032l-2.598-2.599c-0.115,0.077-0.307,0.153-0.459,0.153H3.709
7
+	c-0.458,0-0.803-0.346-0.803-0.804v-8.179c0-0.459,0.344-0.803,0.803-0.803h0.612L2.104,8.131L3.136,7.1z M17.584,10.769v8.714
8
+	l-9.135-9.134h5.045c0.459,0,0.84,0.344,0.84,0.803v2.866L17.584,10.769z"/>
9
+<path fill="#A4B8D1" d="M14.688,0.818l8.164,8.165l-0.584,0.583L20.8,8.098c-0.065,0.043-0.174,0.086-0.26,0.086h-5.528
10
+	c-0.259,0-0.454-0.195-0.454-0.454V3.108c0-0.26,0.195-0.454,0.454-0.454h0.345l-1.253-1.253L14.688,0.818z M22.852,2.892v4.924
11
+	l-5.162-5.162h2.851c0.26,0,0.476,0.194,0.476,0.454v1.619L22.852,2.892z"/>
12
+</svg>

+ 15
- 7
react/features/base/participants/actions.js View File

16
     PIN_PARTICIPANT,
16
     PIN_PARTICIPANT,
17
     SET_LOADABLE_AVATAR_URL
17
     SET_LOADABLE_AVATAR_URL
18
 } from './actionTypes';
18
 } from './actionTypes';
19
-import { DISCO_REMOTE_CONTROL_FEATURE } from './constants';
19
+import {
20
+    DISCO_REMOTE_CONTROL_FEATURE
21
+} from './constants';
20
 import {
22
 import {
21
     getLocalParticipant,
23
     getLocalParticipant,
22
     getNormalizedDisplayName,
24
     getNormalizedDisplayName,
192
  * Create an action for muting another participant in the conference.
194
  * Create an action for muting another participant in the conference.
193
  *
195
  *
194
  * @param {string} id - Participant's ID.
196
  * @param {string} id - Participant's ID.
197
+ * @param {MEDIA_TYPE} mediaType - The media to mute.
195
  * @returns {{
198
  * @returns {{
196
  *     type: MUTE_REMOTE_PARTICIPANT,
199
  *     type: MUTE_REMOTE_PARTICIPANT,
197
- *     id: string
200
+ *     id: string,
201
+ *     mediaType: MEDIA_TYPE
198
  * }}
202
  * }}
199
  */
203
  */
200
-export function muteRemoteParticipant(id) {
204
+export function muteRemoteParticipant(id, mediaType) {
201
     return {
205
     return {
202
         type: MUTE_REMOTE_PARTICIPANT,
206
         type: MUTE_REMOTE_PARTICIPANT,
203
-        id
207
+        id,
208
+        mediaType
204
     };
209
     };
205
 }
210
 }
206
 
211
 
450
  * Action to signal that a participant has muted us.
455
  * Action to signal that a participant has muted us.
451
  *
456
  *
452
  * @param {JitsiParticipant} participant - Information about participant.
457
  * @param {JitsiParticipant} participant - Information about participant.
458
+ * @param {JitsiLocalTrack} track - Information about the track that has been muted.
453
  * @returns {Promise}
459
  * @returns {Promise}
454
  */
460
  */
455
-export function participantMutedUs(participant) {
461
+export function participantMutedUs(participant, track) {
456
     return (dispatch, getState) => {
462
     return (dispatch, getState) => {
457
         if (!participant) {
463
         if (!participant) {
458
             return;
464
             return;
459
         }
465
         }
460
 
466
 
467
+        const isAudio = track.isAudioTrack();
468
+
461
         dispatch(showNotification({
469
         dispatch(showNotification({
462
-            descriptionKey: 'notify.mutedRemotelyDescription',
463
-            titleKey: 'notify.mutedRemotelyTitle',
470
+            descriptionKey: isAudio ? 'notify.mutedRemotelyDescription' : 'notify.videoMutedRemotelyDescription',
471
+            titleKey: isAudio ? 'notify.mutedRemotelyTitle' : 'notify.videoMutedRemotelyTitle',
464
             titleArguments: {
472
             titleArguments: {
465
                 participantDisplayName:
473
                 participantDisplayName:
466
                     getParticipantDisplayName(getState, participant.getId())
474
                     getParticipantDisplayName(getState, participant.getId())

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

112
     case MUTE_REMOTE_PARTICIPANT: {
112
     case MUTE_REMOTE_PARTICIPANT: {
113
         const { conference } = store.getState()['features/base/conference'];
113
         const { conference } = store.getState()['features/base/conference'];
114
 
114
 
115
-        conference.muteParticipant(action.id);
115
+        conference.muteParticipant(action.id, action.mediaType);
116
         break;
116
         break;
117
     }
117
     }
118
 
118
 

+ 2
- 1
react/features/mobile/external-api/middleware.js View File

26
     getURLWithoutParams
26
     getURLWithoutParams
27
 } from '../../base/connection';
27
 } from '../../base/connection';
28
 import { JitsiConferenceEvents } from '../../base/lib-jitsi-meet';
28
 import { JitsiConferenceEvents } from '../../base/lib-jitsi-meet';
29
+import { MEDIA_TYPE } from '../../base/media';
29
 import { SET_AUDIO_MUTED } from '../../base/media/actionTypes';
30
 import { SET_AUDIO_MUTED } from '../../base/media/actionTypes';
30
 import { PARTICIPANT_JOINED, PARTICIPANT_LEFT, getParticipants, getParticipantById } from '../../base/participants';
31
 import { PARTICIPANT_JOINED, PARTICIPANT_LEFT, getParticipants, getParticipantById } from '../../base/participants';
31
 import { MiddlewareRegistry, StateListenerRegistry } from '../../base/redux';
32
 import { MiddlewareRegistry, StateListenerRegistry } from '../../base/redux';
270
     });
271
     });
271
 
272
 
272
     eventEmitter.addListener(ExternalAPI.SET_AUDIO_MUTED, ({ muted }) => {
273
     eventEmitter.addListener(ExternalAPI.SET_AUDIO_MUTED, ({ muted }) => {
273
-        dispatch(muteLocal(muted === 'true'));
274
+        dispatch(muteLocal(muted === 'true', MEDIA_TYPE.AUDIO));
274
     });
275
     });
275
 
276
 
276
     eventEmitter.addListener(ExternalAPI.SEND_ENDPOINT_TEXT_MESSAGE, ({ to, message }) => {
277
     eventEmitter.addListener(ExternalAPI.SEND_ENDPOINT_TEXT_MESSAGE, ({ to, message }) => {

+ 33
- 11
react/features/remote-video-menu/actions.js View File

6
     AUDIO_MUTE,
6
     AUDIO_MUTE,
7
     createRemoteMuteConfirmedEvent,
7
     createRemoteMuteConfirmedEvent,
8
     createToolbarEvent,
8
     createToolbarEvent,
9
-    sendAnalytics
9
+    sendAnalytics,
10
+    VIDEO_MUTE
10
 } from '../analytics';
11
 } from '../analytics';
11
 import { hideDialog } from '../base/dialog';
12
 import { hideDialog } from '../base/dialog';
12
-import { setAudioMuted } from '../base/media';
13
+import {
14
+    MEDIA_TYPE,
15
+    setAudioMuted,
16
+    setVideoMuted,
17
+    VIDEO_MUTISM_AUTHORITY
18
+} from '../base/media';
13
 import {
19
 import {
14
     getLocalParticipant,
20
     getLocalParticipant,
15
     muteRemoteParticipant
21
     muteRemoteParticipant
32
  * Mutes the local participant.
38
  * Mutes the local participant.
33
  *
39
  *
34
  * @param {boolean} enable - Whether to mute or unmute.
40
  * @param {boolean} enable - Whether to mute or unmute.
41
+ * @param {MEDIA_TYPE} mediaType - The type of the media channel to mute.
35
  * @returns {Function}
42
  * @returns {Function}
36
  */
43
  */
37
-export function muteLocal(enable: boolean) {
44
+export function muteLocal(enable: boolean, mediaType: MEDIA_TYPE) {
38
     return (dispatch: Dispatch<any>) => {
45
     return (dispatch: Dispatch<any>) => {
39
-        sendAnalytics(createToolbarEvent(AUDIO_MUTE, { enable }));
40
-        dispatch(setAudioMuted(enable, /* ensureTrack */ true));
46
+        const isAudio = mediaType === MEDIA_TYPE.AUDIO;
47
+
48
+        if (!isAudio && mediaType !== MEDIA_TYPE.VIDEO) {
49
+            console.error(`Unsupported media type: ${mediaType}`);
50
+
51
+            return;
52
+        }
53
+        sendAnalytics(createToolbarEvent(isAudio ? AUDIO_MUTE : VIDEO_MUTE, { enable }));
54
+        dispatch(isAudio ? setAudioMuted(enable, /* ensureTrack */ true)
55
+            : setVideoMuted(enable, mediaType, VIDEO_MUTISM_AUTHORITY.USER, /* ensureTrack */ true));
41
 
56
 
42
         // FIXME: The old conference logic as well as the shared video feature
57
         // FIXME: The old conference logic as well as the shared video feature
43
         // still rely on this event being emitted.
58
         // still rely on this event being emitted.
44
         typeof APP === 'undefined'
59
         typeof APP === 'undefined'
45
-            || APP.UI.emitEvent(UIEvents.AUDIO_MUTED, enable, true);
60
+            || APP.UI.emitEvent(isAudio ? UIEvents.AUDIO_MUTED : UIEvents.VIDEO_MUTED, enable, true);
46
     };
61
     };
47
 }
62
 }
48
 
63
 
50
  * Mutes the remote participant with the given ID.
65
  * Mutes the remote participant with the given ID.
51
  *
66
  *
52
  * @param {string} participantId - ID of the participant to mute.
67
  * @param {string} participantId - ID of the participant to mute.
68
+ * @param {MEDIA_TYPE} mediaType - The type of the media channel to mute.
53
  * @returns {Function}
69
  * @returns {Function}
54
  */
70
  */
55
-export function muteRemote(participantId: string) {
71
+export function muteRemote(participantId: string, mediaType: MEDIA_TYPE) {
56
     return (dispatch: Dispatch<any>) => {
72
     return (dispatch: Dispatch<any>) => {
57
-        sendAnalytics(createRemoteMuteConfirmedEvent(participantId));
58
-        dispatch(muteRemoteParticipant(participantId));
73
+        if (mediaType !== MEDIA_TYPE.AUDIO && mediaType !== MEDIA_TYPE.VIDEO) {
74
+            console.error(`Unsupported media type: ${mediaType}`);
75
+
76
+            return;
77
+        }
78
+        sendAnalytics(createRemoteMuteConfirmedEvent(participantId, mediaType));
79
+        dispatch(muteRemoteParticipant(participantId, mediaType));
59
     };
80
     };
60
 }
81
 }
61
 
82
 
63
  * Mutes all participants.
84
  * Mutes all participants.
64
  *
85
  *
65
  * @param {Array<string>} exclude - Array of participant IDs to not mute.
86
  * @param {Array<string>} exclude - Array of participant IDs to not mute.
87
+ * @param {MEDIA_TYPE} mediaType - The media type to mute.
66
  * @returns {Function}
88
  * @returns {Function}
67
  */
89
  */
68
-export function muteAllParticipants(exclude: Array<string>) {
90
+export function muteAllParticipants(exclude: Array<string>, mediaType: MEDIA_TYPE) {
69
     return (dispatch: Dispatch<any>, getState: Function) => {
91
     return (dispatch: Dispatch<any>, getState: Function) => {
70
         const state = getState();
92
         const state = getState();
71
         const localId = getLocalParticipant(state).id;
93
         const localId = getLocalParticipant(state).id;
75
         /* eslint-disable no-confusing-arrow */
97
         /* eslint-disable no-confusing-arrow */
76
         participantIds
98
         participantIds
77
             .filter(id => !exclude.includes(id))
99
             .filter(id => !exclude.includes(id))
78
-            .map(id => id === localId ? muteLocal(true) : muteRemote(id))
100
+            .map(id => id === localId ? muteLocal(true, mediaType) : muteRemote(id, mediaType))
79
             .map(dispatch);
101
             .map(dispatch);
80
         /* eslint-enable no-confusing-arrow */
102
         /* eslint-enable no-confusing-arrow */
81
     };
103
     };

+ 2
- 1
react/features/remote-video-menu/components/AbstractMuteEveryoneDialog.js View File

3
 import React from 'react';
3
 import React from 'react';
4
 
4
 
5
 import { Dialog } from '../../base/dialog';
5
 import { Dialog } from '../../base/dialog';
6
+import { MEDIA_TYPE } from '../../base/media';
6
 import { getLocalParticipant, getParticipantDisplayName } from '../../base/participants';
7
 import { getLocalParticipant, getParticipantDisplayName } from '../../base/participants';
7
 import { muteAllParticipants } from '../actions';
8
 import { muteAllParticipants } from '../actions';
8
 
9
 
69
             exclude
70
             exclude
70
         } = this.props;
71
         } = this.props;
71
 
72
 
72
-        dispatch(muteAllParticipants(exclude));
73
+        dispatch(muteAllParticipants(exclude, MEDIA_TYPE.AUDIO));
73
 
74
 
74
         return true;
75
         return true;
75
     }
76
     }

+ 48
- 0
react/features/remote-video-menu/components/AbstractMuteEveryoneElsesVideoButton.js View File

1
+// @flow
2
+
3
+import { createToolbarEvent, sendAnalytics } from '../../analytics';
4
+import { openDialog } from '../../base/dialog';
5
+import { IconMuteVideoEveryone } from '../../base/icons';
6
+import { AbstractButton, type AbstractButtonProps } from '../../base/toolbox/components';
7
+
8
+import { MuteEveryonesVideoDialog } from '.';
9
+
10
+export type Props = AbstractButtonProps & {
11
+
12
+    /**
13
+     * The redux {@code dispatch} function.
14
+     */
15
+    dispatch: Function,
16
+
17
+    /**
18
+     * The ID of the participant object that this button is supposed to keep unmuted.
19
+     */
20
+    participantID: string,
21
+
22
+    /**
23
+     * The function to be used to translate i18n labels.
24
+     */
25
+    t: Function
26
+};
27
+
28
+/**
29
+ * An abstract remote video menu button which disables the camera of all the other participants.
30
+ */
31
+export default class AbstractMuteEveryoneElsesVideoButton extends AbstractButton<Props, *> {
32
+    accessibilityLabel = 'toolbar.accessibilityLabel.muteEveryoneElsesVideo';
33
+    icon = IconMuteVideoEveryone;
34
+    label = 'videothumbnail.domuteVideoOfOthers';
35
+
36
+    /**
37
+     * Handles clicking / pressing the button, and opens a confirmation dialog.
38
+     *
39
+     * @private
40
+     * @returns {void}
41
+     */
42
+    _handleClick() {
43
+        const { dispatch, participantID } = this.props;
44
+
45
+        sendAnalytics(createToolbarEvent('mute.everyoneelsesvideo.pressed'));
46
+        dispatch(openDialog(MuteEveryonesVideoDialog, { exclude: [ participantID ] }));
47
+    }
48
+}

+ 103
- 0
react/features/remote-video-menu/components/AbstractMuteEveryonesVideoDialog.js View File

1
+// @flow
2
+
3
+import React from 'react';
4
+
5
+import { Dialog } from '../../base/dialog';
6
+import { MEDIA_TYPE } from '../../base/media';
7
+import { getLocalParticipant, getParticipantDisplayName } from '../../base/participants';
8
+import { muteAllParticipants } from '../actions';
9
+
10
+import AbstractMuteRemoteParticipantsVideoDialog, {
11
+    type Props as AbstractProps
12
+} from './AbstractMuteRemoteParticipantsVideoDialog';
13
+
14
+/**
15
+ * The type of the React {@code Component} props of
16
+ * {@link AbstractMuteEveryonesVideoDialog}.
17
+ */
18
+export type Props = AbstractProps & {
19
+
20
+    content: string,
21
+    exclude: Array<string>,
22
+    title: string
23
+};
24
+
25
+/**
26
+ *
27
+ * An abstract Component with the contents for a dialog that asks for confirmation
28
+ * from the user before disabling all remote participants cameras.
29
+ *
30
+ * @extends AbstractMuteRemoteParticipantsVideoDialog
31
+ */
32
+export default class AbstractMuteEveryonesVideoDialog<P: Props> extends AbstractMuteRemoteParticipantsVideoDialog<P> {
33
+    static defaultProps = {
34
+        exclude: [],
35
+        muteLocal: false
36
+    };
37
+
38
+    /**
39
+     * Implements React's {@link Component#render()}.
40
+     *
41
+     * @inheritdoc
42
+     * @returns {ReactElement}
43
+     */
44
+    render() {
45
+        const { content, title } = this.props;
46
+
47
+        return (
48
+            <Dialog
49
+                okKey = 'dialog.muteParticipantsVideoButton'
50
+                onSubmit = { this._onSubmit }
51
+                titleString = { title }
52
+                width = 'small'>
53
+                <div>
54
+                    { content }
55
+                </div>
56
+            </Dialog>
57
+        );
58
+    }
59
+
60
+    _onSubmit: () => boolean;
61
+
62
+    /**
63
+     * Callback to be invoked when the value of this dialog is submitted.
64
+     *
65
+     * @returns {boolean}
66
+     */
67
+    _onSubmit() {
68
+        const {
69
+            dispatch,
70
+            exclude
71
+        } = this.props;
72
+
73
+        dispatch(muteAllParticipants(exclude, MEDIA_TYPE.VIDEO));
74
+
75
+        return true;
76
+    }
77
+}
78
+
79
+/**
80
+ * Maps (parts of) the Redux state to the associated {@code AbstractMuteEveryonesVideoDialog}'s props.
81
+ *
82
+ * @param {Object} state - The redux state.
83
+ * @param {Object} ownProps - The properties explicitly passed to the component.
84
+ * @returns {Props}
85
+ */
86
+export function abstractMapStateToProps(state: Object, ownProps: Props) {
87
+    const { exclude, t } = ownProps;
88
+
89
+    const whom = exclude
90
+        // eslint-disable-next-line no-confusing-arrow
91
+        .map(id => id === getLocalParticipant(state).id
92
+            ? t('dialog.muteEveryoneSelf')
93
+            : getParticipantDisplayName(state, id))
94
+        .join(', ');
95
+
96
+    return whom.length ? {
97
+        content: t('dialog.muteEveryoneElsesVideoDialog'),
98
+        title: t('dialog.muteEveryoneElsesVideoTitle', { whom })
99
+    } : {
100
+        content: t('dialog.muteEveryonesVideoDialog'),
101
+        title: t('dialog.muteEveryonesVideoTitle')
102
+    };
103
+}

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

2
 
2
 
3
 import { Component } from 'react';
3
 import { Component } from 'react';
4
 
4
 
5
+import { MEDIA_TYPE } from '../../base/media';
5
 import { muteRemote } from '../actions';
6
 import { muteRemote } from '../actions';
6
 
7
 
7
 /**
8
 /**
57
     _onSubmit() {
58
     _onSubmit() {
58
         const { dispatch, participantID } = this.props;
59
         const { dispatch, participantID } = this.props;
59
 
60
 
60
-        dispatch(muteRemote(participantID));
61
+        dispatch(muteRemote(participantID, MEDIA_TYPE.AUDIO));
61
 
62
 
62
         return true;
63
         return true;
63
     }
64
     }

+ 65
- 0
react/features/remote-video-menu/components/AbstractMuteRemoteParticipantsVideoDialog.js View File

1
+// @flow
2
+
3
+import { Component } from 'react';
4
+
5
+import { MEDIA_TYPE } from '../../base/media';
6
+import { muteRemote } from '../actions';
7
+
8
+/**
9
+ * The type of the React {@code Component} props of
10
+ * {@link AbstractMuteRemoteParticipantsVideoDialog}.
11
+ */
12
+export type Props = {
13
+
14
+    /**
15
+     * The Redux dispatch function.
16
+     */
17
+    dispatch: Function,
18
+
19
+    /**
20
+     * The ID of the remote participant to be muted.
21
+     */
22
+    participantID: string,
23
+
24
+    /**
25
+     * Function to translate i18n labels.
26
+     */
27
+    t: Function
28
+};
29
+
30
+/**
31
+ * Abstract dialog to confirm a remote participant video ute action.
32
+ *
33
+ * @extends Component
34
+ */
35
+export default class AbstractMuteRemoteParticipantsVideoDialog<P:Props = Props>
36
+    extends Component<P> {
37
+    /**
38
+     * Initializes a new {@code AbstractMuteRemoteParticipantsVideoDialog} instance.
39
+     *
40
+     * @param {Object} props - The read-only properties with which the new
41
+     * instance is to be initialized.
42
+     */
43
+    constructor(props: P) {
44
+        super(props);
45
+
46
+        // Bind event handlers so they are only bound once per instance.
47
+        this._onSubmit = this._onSubmit.bind(this);
48
+    }
49
+
50
+    _onSubmit: () => boolean;
51
+
52
+    /**
53
+     * Handles the submit button action.
54
+     *
55
+     * @private
56
+     * @returns {boolean} - True (to note that the modal should be closed).
57
+     */
58
+    _onSubmit() {
59
+        const { dispatch, participantID } = this.props;
60
+
61
+        dispatch(muteRemote(participantID, MEDIA_TYPE.VIDEO));
62
+
63
+        return true;
64
+    }
65
+}

+ 103
- 0
react/features/remote-video-menu/components/AbstractMuteVideoButton.js View File

1
+// @flow
2
+
3
+import {
4
+    createRemoteVideoMenuButtonEvent,
5
+    sendAnalytics
6
+} from '../../analytics';
7
+import { openDialog } from '../../base/dialog';
8
+import { IconCameraDisabled } from '../../base/icons';
9
+import { MEDIA_TYPE } from '../../base/media';
10
+import { AbstractButton, type AbstractButtonProps } from '../../base/toolbox/components';
11
+import { isRemoteTrackMuted } from '../../base/tracks';
12
+
13
+import { MuteRemoteParticipantsVideoDialog } from '.';
14
+
15
+export type Props = AbstractButtonProps & {
16
+
17
+    /**
18
+     * Boolean to indicate if the video track of the participant is muted or
19
+     * not.
20
+     */
21
+    _videoTrackMuted: boolean,
22
+
23
+    /**
24
+     * The redux {@code dispatch} function.
25
+     */
26
+    dispatch: Function,
27
+
28
+    /**
29
+     * The ID of the participant object that this button is supposed to
30
+     * mute/unmute.
31
+     */
32
+    participantID: string,
33
+
34
+    /**
35
+     * The function to be used to translate i18n labels.
36
+     */
37
+    t: Function
38
+};
39
+
40
+/**
41
+ * An abstract remote video menu button which mutes the remote participant.
42
+ */
43
+export default class AbstractMuteVideoButton extends AbstractButton<Props, *> {
44
+    accessibilityLabel = 'toolbar.accessibilityLabel.remoteVideoMute';
45
+    icon = IconCameraDisabled;
46
+    label = 'videothumbnail.domuteVideo';
47
+    toggledLabel = 'videothumbnail.videoMuted';
48
+
49
+    /**
50
+     * Handles clicking / pressing the button, and mutes the participant.
51
+     *
52
+     * @private
53
+     * @returns {void}
54
+     */
55
+    _handleClick() {
56
+        const { dispatch, participantID } = this.props;
57
+
58
+        sendAnalytics(createRemoteVideoMenuButtonEvent(
59
+            'mute.button',
60
+            {
61
+                'participant_id': participantID
62
+            }));
63
+
64
+        dispatch(openDialog(MuteRemoteParticipantsVideoDialog, { participantID }));
65
+    }
66
+
67
+    /**
68
+     * Renders the item disabled if the participant is muted.
69
+     *
70
+     * @inheritdoc
71
+     */
72
+    _isDisabled() {
73
+        return this.props._videoTrackMuted;
74
+    }
75
+
76
+    /**
77
+     * Renders the item toggled if the participant is muted.
78
+     *
79
+     * @inheritdoc
80
+     */
81
+    _isToggled() {
82
+        return this.props._videoTrackMuted;
83
+    }
84
+}
85
+
86
+/**
87
+ * Function that maps parts of Redux state tree into component props.
88
+ *
89
+ * @param {Object} state - Redux state.
90
+ * @param {Object} ownProps - Properties of component.
91
+ * @private
92
+ * @returns {{
93
+ *      _videoTrackMuted: boolean
94
+ *  }}
95
+ */
96
+export function _mapStateToProps(state: Object, ownProps: Props) {
97
+    const tracks = state['features/base/tracks'];
98
+
99
+    return {
100
+        _videoTrackMuted: isRemoteTrackMuted(
101
+            tracks, MEDIA_TYPE.VIDEO, ownProps.participantID)
102
+    };
103
+}

+ 54
- 0
react/features/remote-video-menu/components/web/MuteEveryoneElsesVideoButton.js View File

1
+// @flow
2
+
3
+import React from 'react';
4
+
5
+import { translate } from '../../../base/i18n';
6
+import { IconMuteVideoEveryoneElse } from '../../../base/icons';
7
+import { connect } from '../../../base/redux';
8
+import AbstractMuteEveryoneElsesVideoButton, {
9
+    type Props
10
+} from '../AbstractMuteEveryoneElsesVideoButton';
11
+
12
+import RemoteVideoMenuButton from './RemoteVideoMenuButton';
13
+
14
+/**
15
+ * Implements a React {@link Component} which displays a button for audio muting
16
+ * every participant in the conference except the one with the given
17
+ * participantID
18
+ */
19
+class MuteEveryoneElsesVideoButton extends AbstractMuteEveryoneElsesVideoButton {
20
+    /**
21
+     * Instantiates a new {@code Component}.
22
+     *
23
+     * @inheritdoc
24
+     */
25
+    constructor(props: Props) {
26
+        super(props);
27
+
28
+        this._handleClick = this._handleClick.bind(this);
29
+    }
30
+
31
+    /**
32
+     * Implements React's {@link Component#render()}.
33
+     *
34
+     * @inheritdoc
35
+     * @returns {ReactElement}
36
+     */
37
+    render() {
38
+        const { participantID, t } = this.props;
39
+
40
+        return (
41
+            <RemoteVideoMenuButton
42
+                buttonText = { t('videothumbnail.domuteVideoOfOthers') }
43
+                displayClass = { 'mutelink' }
44
+                icon = { IconMuteVideoEveryoneElse }
45
+                id = { `mutelink_${participantID}` }
46
+                // eslint-disable-next-line react/jsx-handler-names
47
+                onClick = { this._handleClick } />
48
+        );
49
+    }
50
+
51
+    _handleClick: () => void;
52
+}
53
+
54
+export default translate(connect()(MuteEveryoneElsesVideoButton));

+ 41
- 0
react/features/remote-video-menu/components/web/MuteEveryonesVideoDialog.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 AbstractMuteEveryonesVideoDialog, { abstractMapStateToProps, type Props }
9
+    from '../AbstractMuteEveryonesVideoDialog';
10
+
11
+/**
12
+ * A React Component with the contents for a dialog that asks for confirmation
13
+ * from the user before disabling all remote participants cameras.
14
+ *
15
+ * @extends AbstractMuteEveryonesVideoDialog
16
+ */
17
+class MuteEveryonesVideoDialog extends AbstractMuteEveryonesVideoDialog<Props> {
18
+    /**
19
+     * Implements React's {@link Component#render()}.
20
+     *
21
+     * @inheritdoc
22
+     * @returns {ReactElement}
23
+     */
24
+    render() {
25
+        return (
26
+            <Dialog
27
+                okKey = 'dialog.muteParticipantsVideoButton'
28
+                onSubmit = { this._onSubmit }
29
+                titleString = { this.props.title }
30
+                width = 'small'>
31
+                <div>
32
+                    { this.props.content }
33
+                </div>
34
+            </Dialog>
35
+        );
36
+    }
37
+
38
+    _onSubmit: () => boolean;
39
+}
40
+
41
+export default translate(connect(abstractMapStateToProps)(MuteEveryonesVideoDialog));

+ 41
- 0
react/features/remote-video-menu/components/web/MuteRemoteParticipantsVideoDialog.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 AbstractMuteRemoteParticipantsVideoDialog
9
+    from '../AbstractMuteRemoteParticipantsVideoDialog';
10
+
11
+/**
12
+ * A React Component with the contents for a dialog that asks for confirmation
13
+ * from the user before disabling a remote participants camera.
14
+ *
15
+ * @extends Component
16
+ */
17
+class MuteRemoteParticipantsVideoDialog extends AbstractMuteRemoteParticipantsVideoDialog {
18
+    /**
19
+     * Implements React's {@link Component#render()}.
20
+     *
21
+     * @inheritdoc
22
+     * @returns {ReactElement}
23
+     */
24
+    render() {
25
+        return (
26
+            <Dialog
27
+                okKey = 'dialog.muteParticipantsVideoButton'
28
+                onSubmit = { this._onSubmit }
29
+                titleKey = 'dialog.muteParticipantsVideoTitle'
30
+                width = 'small'>
31
+                <div>
32
+                    { this.props.t('dialog.muteParticipantsVideoBody') }
33
+                </div>
34
+            </Dialog>
35
+        );
36
+    }
37
+
38
+    _onSubmit: () => boolean;
39
+}
40
+
41
+export default translate(connect()(MuteRemoteParticipantsVideoDialog));

+ 67
- 0
react/features/remote-video-menu/components/web/MuteVideoButton.js View File

1
+/* @flow */
2
+
3
+import React from 'react';
4
+
5
+import { translate } from '../../../base/i18n';
6
+import { IconCameraDisabled } from '../../../base/icons';
7
+import { connect } from '../../../base/redux';
8
+import AbstractMuteVideoButton, {
9
+    _mapStateToProps,
10
+    type Props
11
+} from '../AbstractMuteVideoButton';
12
+
13
+import RemoteVideoMenuButton from './RemoteVideoMenuButton';
14
+
15
+/**
16
+ * Implements a React {@link Component} which displays a button for disabling
17
+ * the camera of a participant in the conference.
18
+ *
19
+ * NOTE: At the time of writing this is a button that doesn't use the
20
+ * {@code AbstractButton} base component, but is inherited from the same
21
+ * super class ({@code AbstractMuteVideoButton} that extends {@code AbstractButton})
22
+ * for the sake of code sharing between web and mobile. Once web uses the
23
+ * {@code AbstractButton} base component, this can be fully removed.
24
+ */
25
+class MuteVideoButton extends AbstractMuteVideoButton {
26
+    /**
27
+     * Instantiates a new {@code Component}.
28
+     *
29
+     * @inheritdoc
30
+     */
31
+    constructor(props: Props) {
32
+        super(props);
33
+
34
+        this._handleClick = this._handleClick.bind(this);
35
+    }
36
+
37
+    /**
38
+     * Implements React's {@link Component#render()}.
39
+     *
40
+     * @inheritdoc
41
+     * @returns {ReactElement}
42
+     */
43
+    render() {
44
+        const { _videoTrackMuted, participantID, t } = this.props;
45
+        const muteConfig = _videoTrackMuted ? {
46
+            translationKey: 'videothumbnail.videoMuted',
47
+            muteClassName: 'mutelink disabled'
48
+        } : {
49
+            translationKey: 'videothumbnail.domuteVideo',
50
+            muteClassName: 'mutelink'
51
+        };
52
+
53
+        return (
54
+            <RemoteVideoMenuButton
55
+                buttonText = { t(muteConfig.translationKey) }
56
+                displayClass = { muteConfig.muteClassName }
57
+                icon = { IconCameraDisabled }
58
+                id = { `mutelink_${participantID}` }
59
+                // eslint-disable-next-line react/jsx-handler-names
60
+                onClick = { this._handleClick } />
61
+        );
62
+    }
63
+
64
+    _handleClick: () => void
65
+}
66
+
67
+export default translate(connect(_mapStateToProps)(MuteVideoButton));

+ 12
- 11
react/features/remote-video-menu/components/web/RemoteVideoMenuTriggerButton.js View File

3
 import React, { Component } from 'react';
3
 import React, { Component } from 'react';
4
 
4
 
5
 import { Icon, IconMenuThumb } from '../../../base/icons';
5
 import { Icon, IconMenuThumb } from '../../../base/icons';
6
-import { MEDIA_TYPE } from '../../../base/media';
7
 import { getLocalParticipant, getParticipantById, PARTICIPANT_ROLE } from '../../../base/participants';
6
 import { getLocalParticipant, getParticipantById, PARTICIPANT_ROLE } from '../../../base/participants';
8
 import { Popover } from '../../../base/popover';
7
 import { Popover } from '../../../base/popover';
9
 import { connect } from '../../../base/redux';
8
 import { connect } from '../../../base/redux';
10
-import { isRemoteTrackMuted } from '../../../base/tracks';
11
 import { requestRemoteControl, stopController } from '../../../remote-control';
9
 import { requestRemoteControl, stopController } from '../../../remote-control';
12
 import { getCurrentLayout, LAYOUTS } from '../../../video-layout';
10
 import { getCurrentLayout, LAYOUTS } from '../../../video-layout';
13
 
11
 
14
 import MuteEveryoneElseButton from './MuteEveryoneElseButton';
12
 import MuteEveryoneElseButton from './MuteEveryoneElseButton';
13
+import MuteEveryoneElsesVideoButton from './MuteEveryoneElsesVideoButton';
15
 import { REMOTE_CONTROL_MENU_STATES } from './RemoteControlButton';
14
 import { REMOTE_CONTROL_MENU_STATES } from './RemoteControlButton';
16
 
15
 
17
 import {
16
 import {
18
     GrantModeratorButton,
17
     GrantModeratorButton,
19
     MuteButton,
18
     MuteButton,
19
+    MuteVideoButton,
20
     KickButton,
20
     KickButton,
21
     PrivateMessageMenuButton,
21
     PrivateMessageMenuButton,
22
     RemoteControlButton,
22
     RemoteControlButton,
43
      */
43
      */
44
     _disableRemoteMute: Boolean,
44
     _disableRemoteMute: Boolean,
45
 
45
 
46
-    /**
47
-     * Whether or not the participant is currently muted.
48
-     */
49
-    _isAudioMuted: boolean,
50
-
51
     /**
46
     /**
52
      * Whether or not the participant is a conference moderator.
47
      * Whether or not the participant is a conference moderator.
53
      */
48
      */
151
         const {
146
         const {
152
             _disableKick,
147
             _disableKick,
153
             _disableRemoteMute,
148
             _disableRemoteMute,
154
-            _isAudioMuted,
155
             _isModerator,
149
             _isModerator,
156
             dispatch,
150
             dispatch,
157
             initialVolumeValue,
151
             initialVolumeValue,
166
             if (!_disableRemoteMute) {
160
             if (!_disableRemoteMute) {
167
                 buttons.push(
161
                 buttons.push(
168
                     <MuteButton
162
                     <MuteButton
169
-                        isAudioMuted = { _isAudioMuted }
170
                         key = 'mute'
163
                         key = 'mute'
171
                         participantID = { participantID } />
164
                         participantID = { participantID } />
172
                 );
165
                 );
175
                         key = 'mute-others'
168
                         key = 'mute-others'
176
                         participantID = { participantID } />
169
                         participantID = { participantID } />
177
                 );
170
                 );
171
+                buttons.push(
172
+                    <MuteVideoButton
173
+                        key = 'mute-video'
174
+                        participantID = { participantID } />
175
+                );
176
+                buttons.push(
177
+                    <MuteEveryoneElsesVideoButton
178
+                        key = 'mute-others-video'
179
+                        participantID = { participantID } />
180
+                );
178
             }
181
             }
179
 
182
 
180
             buttons.push(
183
             buttons.push(
247
  */
250
  */
248
 function _mapStateToProps(state, ownProps) {
251
 function _mapStateToProps(state, ownProps) {
249
     const { participantID } = ownProps;
252
     const { participantID } = ownProps;
250
-    const tracks = state['features/base/tracks'];
251
     const localParticipant = getLocalParticipant(state);
253
     const localParticipant = getLocalParticipant(state);
252
     const { remoteVideoMenu = {}, disableRemoteMute } = state['features/base/config'];
254
     const { remoteVideoMenu = {}, disableRemoteMute } = state['features/base/config'];
253
     const { disableKick } = remoteVideoMenu;
255
     const { disableKick } = remoteVideoMenu;
286
     }
288
     }
287
 
289
 
288
     return {
290
     return {
289
-        _isAudioMuted: isRemoteTrackMuted(tracks, MEDIA_TYPE.AUDIO, participantID) || false,
290
         _isModerator: Boolean(localParticipant?.role === PARTICIPANT_ROLE.MODERATOR),
291
         _isModerator: Boolean(localParticipant?.role === PARTICIPANT_ROLE.MODERATOR),
291
         _disableKick: Boolean(disableKick),
292
         _disableKick: Boolean(disableKick),
292
         _disableRemoteMute: Boolean(disableRemoteMute),
293
         _disableRemoteMute: Boolean(disableRemoteMute),

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

5
 export { default as KickButton } from './KickButton';
5
 export { default as KickButton } from './KickButton';
6
 export { default as KickRemoteParticipantDialog } from './KickRemoteParticipantDialog';
6
 export { default as KickRemoteParticipantDialog } from './KickRemoteParticipantDialog';
7
 export { default as MuteButton } from './MuteButton';
7
 export { default as MuteButton } from './MuteButton';
8
+export { default as MuteVideoButton } from './MuteVideoButton';
8
 export { default as MuteEveryoneDialog } from './MuteEveryoneDialog';
9
 export { default as MuteEveryoneDialog } from './MuteEveryoneDialog';
10
+export { default as MuteEveryonesVideoDialog } from './MuteEveryonesVideoDialog';
9
 export { default as MuteEveryoneElseButton } from './MuteEveryoneElseButton';
11
 export { default as MuteEveryoneElseButton } from './MuteEveryoneElseButton';
12
+export { default as MuteEveryoneElsesVideoButton } from './MuteEveryoneElsesVideoButton';
10
 export { default as MuteRemoteParticipantDialog } from './MuteRemoteParticipantDialog';
13
 export { default as MuteRemoteParticipantDialog } from './MuteRemoteParticipantDialog';
14
+export { default as MuteRemoteParticipantsVideoDialog } from './MuteRemoteParticipantsVideoDialog';
11
 export { default as PrivateMessageMenuButton } from './PrivateMessageMenuButton';
15
 export { default as PrivateMessageMenuButton } from './PrivateMessageMenuButton';
12
 export { REMOTE_CONTROL_MENU_STATES, default as RemoteControlButton } from './RemoteControlButton';
16
 export { REMOTE_CONTROL_MENU_STATES, default as RemoteControlButton } from './RemoteControlButton';
13
 export { default as RemoteVideoMenu } from './RemoteVideoMenu';
17
 export { default as RemoteVideoMenu } from './RemoteVideoMenu';

+ 1
- 1
react/features/toolbox/components/AudioMuteButton.js View File

125
      * @returns {void}
125
      * @returns {void}
126
      */
126
      */
127
     _setAudioMuted(audioMuted: boolean) {
127
     _setAudioMuted(audioMuted: boolean) {
128
-        this.props.dispatch(muteLocal(audioMuted));
128
+        this.props.dispatch(muteLocal(audioMuted, MEDIA_TYPE.AUDIO));
129
     }
129
     }
130
 
130
 
131
     /**
131
     /**

+ 76
- 0
react/features/toolbox/components/MuteEveryonesVideoButton.js View File

1
+// @flow
2
+
3
+import { createToolbarEvent, sendAnalytics } from '../../analytics';
4
+import { openDialog } from '../../base/dialog';
5
+import { translate } from '../../base/i18n';
6
+import { IconMuteVideoEveryone } from '../../base/icons';
7
+import { getLocalParticipant, PARTICIPANT_ROLE } from '../../base/participants';
8
+import { connect } from '../../base/redux';
9
+import { AbstractButton, type AbstractButtonProps } from '../../base/toolbox/components';
10
+import { MuteEveryonesVideoDialog } from '../../remote-video-menu/components';
11
+
12
+type Props = AbstractButtonProps & {
13
+
14
+    /**
15
+     * The Redux dispatch function.
16
+     */
17
+    dispatch: Function,
18
+
19
+    /*
20
+     ** Whether the local participant is a moderator or not.
21
+     */
22
+    isModerator: Boolean,
23
+
24
+    /**
25
+     * The ID of the local participant.
26
+     */
27
+    localParticipantId: string
28
+};
29
+
30
+/**
31
+ * Implements a React {@link Component} which displays a button for disabling the camera of
32
+ * every participant (except the local one)
33
+ */
34
+class MuteEveryonesVideoButton extends AbstractButton<Props, *> {
35
+    accessibilityLabel = 'toolbar.accessibilityLabel.muteEveryonesVideo';
36
+    icon = IconMuteVideoEveryone;
37
+    label = 'toolbar.muteEveryonesVideo';
38
+    tooltip = 'toolbar.muteVideoEveryone';
39
+
40
+    /**
41
+     * Handles clicking / pressing the button, and opens a confirmation dialog.
42
+     *
43
+     * @private
44
+     * @returns {void}
45
+     */
46
+    _handleClick() {
47
+        const { dispatch, localParticipantId } = this.props;
48
+
49
+        sendAnalytics(createToolbarEvent('mute.everyone.pressed'));
50
+        dispatch(openDialog(MuteEveryonesVideoDialog, {
51
+            exclude: [ localParticipantId ]
52
+        }));
53
+    }
54
+}
55
+
56
+/**
57
+ * Maps part of the redux state to the component's props.
58
+ *
59
+ * @param {Object} state - The redux store/state.
60
+ * @param {Props} ownProps - The component's own props.
61
+ * @returns {Object}
62
+ */
63
+function _mapStateToProps(state: Object, ownProps: Props) {
64
+    const localParticipant = getLocalParticipant(state);
65
+    const isModerator = localParticipant.role === PARTICIPANT_ROLE.MODERATOR;
66
+    const { visible } = ownProps;
67
+    const { disableRemoteMute } = state['features/base/config'];
68
+
69
+    return {
70
+        isModerator,
71
+        localParticipantId: localParticipant.id,
72
+        visible: visible && isModerator && !disableRemoteMute
73
+    };
74
+}
75
+
76
+export default translate(connect(_mapStateToProps)(MuteEveryonesVideoButton));

+ 5
- 0
react/features/toolbox/components/web/Toolbox.js View File

82
 import HangupButton from '../HangupButton';
82
 import HangupButton from '../HangupButton';
83
 import HelpButton from '../HelpButton';
83
 import HelpButton from '../HelpButton';
84
 import MuteEveryoneButton from '../MuteEveryoneButton';
84
 import MuteEveryoneButton from '../MuteEveryoneButton';
85
+import MuteEveryonesVideoButton from '../MuteEveryonesVideoButton';
85
 
86
 
86
 import AudioSettingsButton from './AudioSettingsButton';
87
 import AudioSettingsButton from './AudioSettingsButton';
87
 import OverflowMenuButton from './OverflowMenuButton';
88
 import OverflowMenuButton from './OverflowMenuButton';
1079
                 && <MuteEveryoneButton
1080
                 && <MuteEveryoneButton
1080
                     key = 'mute-everyone'
1081
                     key = 'mute-everyone'
1081
                     showLabel = { true } />,
1082
                     showLabel = { true } />,
1083
+            this._shouldShowButton('mute-video-everyone')
1084
+                && <MuteEveryonesVideoButton
1085
+                    key = 'mute-video-everyone'
1086
+                    showLabel = { true } />,
1082
             this._shouldShowButton('stats')
1087
             this._shouldShowButton('stats')
1083
                 && <OverflowMenuItem
1088
                 && <OverflowMenuItem
1084
                     accessibilityLabel = { t('toolbar.accessibilityLabel.speakerStats') }
1089
                     accessibilityLabel = { t('toolbar.accessibilityLabel.speakerStats') }

Loading…
Cancel
Save