Bläddra i källkod

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 år sedan
förälder
incheckning
23bb824731
Inget konto är kopplat till bidragsgivarens mejladress
32 ändrade filer med 754 tillägg och 46 borttagningar
  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 Visa fil

@@ -2008,7 +2008,10 @@ export default {
2008 2008
 
2009 2009
         room.on(JitsiConferenceEvents.TRACK_MUTE_CHANGED, (track, participantThatMutedUs) => {
2010 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 Visa fil

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

+ 1
- 1
interface_config.js Visa fil

@@ -206,7 +206,7 @@ var interfaceConfig = {
206 206
         'fodeviceselection', 'hangup', 'profile', 'chat', 'recording',
207 207
         'livestreaming', 'etherpad', 'sharedvideo', 'settings', 'raisehand',
208 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 212
     TOOLBAR_TIMEOUT: 4000,

+ 17
- 0
lang/main-de.json Visa fil

@@ -239,12 +239,19 @@
239 239
         "muteEveryoneElseTitle": "Alle außer {{whom}} stummschalten?",
240 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 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 246
         "muteEveryoneSelf": "sich selbst",
243 247
         "muteEveryoneStartMuted": "Alle beginnen von jetzt an stummgeschaltet",
244 248
         "muteParticipantBody": "Sie können die Stummschaltung anderer Personen nicht aufheben, aber eine Person kann ihre eigene Stummschaltung jederzeit beenden.",
245 249
         "muteParticipantButton": "Stummschalten",
246 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 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 255
         "Ok": "OK",
249 256
         "passwordLabel": "Dieses Meeting wurde gesichert. Bitte geben Sie das $t(lockRoomPasswordUppercase) ein, um dem Meeting beizutreten.",
250 257
         "passwordNotSupported": "Das Festlegen eines Konferenzpassworts wird nicht unterstützt.",
@@ -484,6 +491,8 @@
484 491
         "mutedTitle": "Stummschaltung aktiv!",
485 492
         "mutedRemotelyTitle": "Sie wurden von {{participantDisplayName}} stummgeschaltet!",
486 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 496
         "passwordRemovedRemotely": "$t(lockRoomPasswordUppercase) von einer anderen Person entfernt",
488 497
         "passwordSetRemotely": "$t(lockRoomPasswordUppercase) von einer anderen Person gesetzt",
489 498
         "raisedHand": "{{name}} möchte sprechen.",
@@ -714,12 +723,16 @@
714 723
             "moreOptions": "Menü „Weitere Optionen“",
715 724
             "mute": "„Audio stummschalten“ ein-/ausschalten",
716 725
             "muteEveryone": "Alle stummschalten",
726
+            "muteEveryoneElse": "Alle anderen stummschalten",
727
+            "muteEveryonesVideo": "Alle Kameras ausschalten",
728
+            "muteEveryoneElsesVideo": "Alle anderen Kameras ausschalten",
717 729
             "pip": "Bild-in-Bild-Modus ein-/ausschalten",
718 730
             "privateMessage": "Private Nachricht senden",
719 731
             "profile": "Profil bearbeiten",
720 732
             "raiseHand": "„Melden“ ein-/ausschalten",
721 733
             "recording": "Aufzeichnung ein-/ausschalten",
722 734
             "remoteMute": "Personen stummschalten",
735
+            "remoteVideoMute": "Kamera von dieser Person ausschalten",
723 736
             "security": "Sicherheitsoptionen",
724 737
             "Settings": "Einstellungen ein-/ausschalten",
725 738
             "sharedvideo": "YouTube-Videofreigabe ein-/ausschalten",
@@ -764,6 +777,7 @@
764 777
         "moreOptions": "Weitere Optionen",
765 778
         "mute": "Stummschaltung aktivieren / deaktivieren",
766 779
         "muteEveryone": "Alle stummschalten",
780
+        "muteEveryonesVideo": "Alle Kameras ausschalten",
767 781
         "noAudioSignalTitle": "Es kommt kein Input von Ihrem Mikrofon!",
768 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 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,13 +863,16 @@
849 863
     },
850 864
     "videothumbnail": {
851 865
         "domute": "Stummschalten",
866
+        "domuteVideo": "Kamera ausschalten",
852 867
         "domuteOthers": "Alle anderen stummschalten",
868
+        "domuteVideoOfOthers": "Alle anderen Kameras auschalten",
853 869
         "flip": "Spiegeln",
854 870
         "grantModerator": "Moderationsrechte vergeben",
855 871
         "kick": "Hinauswerfen",
856 872
         "moderator": "Moderation",
857 873
         "mute": "Person ist stumm geschaltet",
858 874
         "muted": "Stummgeschaltet",
875
+        "videoMuted": "Kamera ausgeschaltet",
859 876
         "remoteControl": "Fernsteuerung",
860 877
         "show": "Im Vordergrund anzeigen",
861 878
         "videomute": "Person hat die Kamera angehalten"

+ 17
- 0
lang/main.json Visa fil

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

+ 5
- 2
modules/API/API.js Visa fil

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

+ 2
- 2
package-lock.json Visa fil

@@ -10343,8 +10343,8 @@
10343 10343
       }
10344 10344
     },
10345 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 10348
       "requires": {
10349 10349
         "@jitsi/js-utils": "1.0.2",
10350 10350
         "@jitsi/sdp-interop": "1.0.3",

+ 1
- 1
package.json Visa fil

@@ -56,7 +56,7 @@
56 56
     "jquery-i18next": "1.2.1",
57 57
     "js-md5": "0.6.1",
58 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 60
     "libflacjs": "github:mmig/libflac.js#93d37e7f811f01cf7d8b6a603e38bd3c3810907d",
61 61
     "lodash": "4.17.19",
62 62
     "moment": "2.19.4",

+ 4
- 2
react/features/analytics/AnalyticsEvents.js Visa fil

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

+ 2
- 2
react/features/base/conference/actions.js Visa fil

@@ -149,9 +149,9 @@ function _addConferenceListeners(conference, dispatch) {
149 149
 
150 150
     conference.on(
151 151
         JitsiConferenceEvents.TRACK_MUTE_CHANGED,
152
-        (_, participantThatMutedUs) => {
152
+        (track, participantThatMutedUs) => {
153 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 Visa fil

@@ -68,6 +68,8 @@ export { default as IconMicrophoneEmpty } from './microphone-empty.svg';
68 68
 export { default as IconModerator } from './star.svg';
69 69
 export { default as IconMuteEveryone } from './mute-everyone.svg';
70 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 73
 export { default as IconNotificationJoin } from './navigate_next.svg';
72 74
 export { default as IconOpenInNew } from './open_in_new.svg';
73 75
 export { default as IconOutlook } from './office365.svg';

+ 12
- 0
react/features/base/icons/svg/mute-video-everyone-else.svg Visa fil

@@ -0,0 +1,12 @@
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 Visa fil

@@ -0,0 +1,12 @@
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 Visa fil

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

+ 1
- 1
react/features/base/participants/middleware.js Visa fil

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

+ 2
- 1
react/features/mobile/external-api/middleware.js Visa fil

@@ -26,6 +26,7 @@ import {
26 26
     getURLWithoutParams
27 27
 } from '../../base/connection';
28 28
 import { JitsiConferenceEvents } from '../../base/lib-jitsi-meet';
29
+import { MEDIA_TYPE } from '../../base/media';
29 30
 import { SET_AUDIO_MUTED } from '../../base/media/actionTypes';
30 31
 import { PARTICIPANT_JOINED, PARTICIPANT_LEFT, getParticipants, getParticipantById } from '../../base/participants';
31 32
 import { MiddlewareRegistry, StateListenerRegistry } from '../../base/redux';
@@ -270,7 +271,7 @@ function _registerForNativeEvents(store) {
270 271
     });
271 272
 
272 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 277
     eventEmitter.addListener(ExternalAPI.SEND_ENDPOINT_TEXT_MESSAGE, ({ to, message }) => {

+ 33
- 11
react/features/remote-video-menu/actions.js Visa fil

@@ -6,10 +6,16 @@ import {
6 6
     AUDIO_MUTE,
7 7
     createRemoteMuteConfirmedEvent,
8 8
     createToolbarEvent,
9
-    sendAnalytics
9
+    sendAnalytics,
10
+    VIDEO_MUTE
10 11
 } from '../analytics';
11 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 19
 import {
14 20
     getLocalParticipant,
15 21
     muteRemoteParticipant
@@ -32,17 +38,26 @@ export function hideRemoteVideoMenu() {
32 38
  * Mutes the local participant.
33 39
  *
34 40
  * @param {boolean} enable - Whether to mute or unmute.
41
+ * @param {MEDIA_TYPE} mediaType - The type of the media channel to mute.
35 42
  * @returns {Function}
36 43
  */
37
-export function muteLocal(enable: boolean) {
44
+export function muteLocal(enable: boolean, mediaType: MEDIA_TYPE) {
38 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 57
         // FIXME: The old conference logic as well as the shared video feature
43 58
         // still rely on this event being emitted.
44 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,12 +65,18 @@ export function muteLocal(enable: boolean) {
50 65
  * Mutes the remote participant with the given ID.
51 66
  *
52 67
  * @param {string} participantId - ID of the participant to mute.
68
+ * @param {MEDIA_TYPE} mediaType - The type of the media channel to mute.
53 69
  * @returns {Function}
54 70
  */
55
-export function muteRemote(participantId: string) {
71
+export function muteRemote(participantId: string, mediaType: MEDIA_TYPE) {
56 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,9 +84,10 @@ export function muteRemote(participantId: string) {
63 84
  * Mutes all participants.
64 85
  *
65 86
  * @param {Array<string>} exclude - Array of participant IDs to not mute.
87
+ * @param {MEDIA_TYPE} mediaType - The media type to mute.
66 88
  * @returns {Function}
67 89
  */
68
-export function muteAllParticipants(exclude: Array<string>) {
90
+export function muteAllParticipants(exclude: Array<string>, mediaType: MEDIA_TYPE) {
69 91
     return (dispatch: Dispatch<any>, getState: Function) => {
70 92
         const state = getState();
71 93
         const localId = getLocalParticipant(state).id;
@@ -75,7 +97,7 @@ export function muteAllParticipants(exclude: Array<string>) {
75 97
         /* eslint-disable no-confusing-arrow */
76 98
         participantIds
77 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 101
             .map(dispatch);
80 102
         /* eslint-enable no-confusing-arrow */
81 103
     };

+ 2
- 1
react/features/remote-video-menu/components/AbstractMuteEveryoneDialog.js Visa fil

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

+ 48
- 0
react/features/remote-video-menu/components/AbstractMuteEveryoneElsesVideoButton.js Visa fil

@@ -0,0 +1,48 @@
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 Visa fil

@@ -0,0 +1,103 @@
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 Visa fil

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

+ 65
- 0
react/features/remote-video-menu/components/AbstractMuteRemoteParticipantsVideoDialog.js Visa fil

@@ -0,0 +1,65 @@
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 Visa fil

@@ -0,0 +1,103 @@
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 Visa fil

@@ -0,0 +1,54 @@
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 Visa fil

@@ -0,0 +1,41 @@
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 Visa fil

@@ -0,0 +1,41 @@
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 Visa fil

@@ -0,0 +1,67 @@
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 Visa fil

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

+ 4
- 0
react/features/remote-video-menu/components/web/index.js Visa fil

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

+ 1
- 1
react/features/toolbox/components/AudioMuteButton.js Visa fil

@@ -125,7 +125,7 @@ class AudioMuteButton extends AbstractAudioMuteButton<Props, *> {
125 125
      * @returns {void}
126 126
      */
127 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 Visa fil

@@ -0,0 +1,76 @@
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 Visa fil

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

Laddar…
Avbryt
Spara