diff --git a/conference.js b/conference.js index a98d5e610f..d1a0eab11b 100644 --- a/conference.js +++ b/conference.js @@ -2008,7 +2008,10 @@ export default { room.on(JitsiConferenceEvents.TRACK_MUTE_CHANGED, (track, participantThatMutedUs) => { if (participantThatMutedUs) { - APP.store.dispatch(participantMutedUs(participantThatMutedUs)); + APP.store.dispatch(participantMutedUs(participantThatMutedUs, track)); + if (this.isSharingScreen && track.isVideoTrack()) { + this._turnScreenSharingOff(false); + } } }); diff --git a/css/_popup_menu.scss b/css/_popup_menu.scss index 6bb98f2508..bf4ac8a9d9 100644 --- a/css/_popup_menu.scss +++ b/css/_popup_menu.scss @@ -6,7 +6,6 @@ min-width: 75px; text-align: left; padding: 0px; - width: 180px; white-space: nowrap; &__item { diff --git a/interface_config.js b/interface_config.js index 0682982bd5..b5cc4f48f2 100644 --- a/interface_config.js +++ b/interface_config.js @@ -206,7 +206,7 @@ var interfaceConfig = { 'fodeviceselection', 'hangup', 'profile', 'chat', 'recording', 'livestreaming', 'etherpad', 'sharedvideo', 'settings', 'raisehand', 'videoquality', 'filmstrip', 'invite', 'feedback', 'stats', 'shortcuts', - 'tileview', 'videobackgroundblur', 'download', 'help', 'mute-everyone', 'security' + 'tileview', 'videobackgroundblur', 'download', 'help', 'mute-everyone', 'mute-video-everyone', 'security' ], TOOLBAR_TIMEOUT: 4000, diff --git a/lang/main-de.json b/lang/main-de.json index 020b00b2b1..53b90e6819 100644 --- a/lang/main-de.json +++ b/lang/main-de.json @@ -239,12 +239,19 @@ "muteEveryoneElseTitle": "Alle außer {{whom}} stummschalten?", "muteEveryoneDialog": "Wollen Sie wirklich alle stummschalten? Sie können deren Stummschaltung nicht mehr beenden, aber sie können ihre Stummschaltung jederzeit selbst beenden.", "muteEveryoneTitle": "Alle stummschalten?", + "muteEveryoneElsesVideoDialog": "Sobald die Kamera deaktiviert ist, können Sie sie nicht wieder aktivieren, die Teilnehmer können dies aber jederzeit wieder ändern.", + "muteEveryoneElsesVideoTitle": "Die Kamera von allen außer {{whom}} ausschalten?", + "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.", + "muteEveryonesVideoTitle": "Die Kamera von allen anderen ausschalten?", "muteEveryoneSelf": "sich selbst", "muteEveryoneStartMuted": "Alle beginnen von jetzt an stummgeschaltet", "muteParticipantBody": "Sie können die Stummschaltung anderer Personen nicht aufheben, aber eine Person kann ihre eigene Stummschaltung jederzeit beenden.", "muteParticipantButton": "Stummschalten", "muteParticipantDialog": "Wollen Sie diese Person wirklich stummschalten? Sie können die Stummschaltung nicht wieder aufheben, die Person kann dies aber jederzeit selbst tun.", "muteParticipantTitle": "Person stummschalten?", + "muteParticipantsVideoButton": "Kamera ausschalten", + "muteParticipantsVideoTitle": "Die Kamera von dieser Person ausschalten?", + "muteParticipantsVideoBody": "Sie können die Kamera nicht wieder aktivieren, die Teilnehmer können dies aber jederzeit wieder ändern.", "Ok": "OK", "passwordLabel": "Dieses Meeting wurde gesichert. Bitte geben Sie das $t(lockRoomPasswordUppercase) ein, um dem Meeting beizutreten.", "passwordNotSupported": "Das Festlegen eines Konferenzpassworts wird nicht unterstützt.", @@ -484,6 +491,8 @@ "mutedTitle": "Stummschaltung aktiv!", "mutedRemotelyTitle": "Sie wurden von {{participantDisplayName}} stummgeschaltet!", "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.", + "videoMutedRemotelyTitle": "Ihre Kamera wurde von {{participantDisplayName}} ausgeschaltet!", + "videoMutedRemotelyDescription": "Sie können sie jederzeit wieder einschalten.", "passwordRemovedRemotely": "$t(lockRoomPasswordUppercase) von einer anderen Person entfernt", "passwordSetRemotely": "$t(lockRoomPasswordUppercase) von einer anderen Person gesetzt", "raisedHand": "{{name}} möchte sprechen.", @@ -714,12 +723,16 @@ "moreOptions": "Menü „Weitere Optionen“", "mute": "„Audio stummschalten“ ein-/ausschalten", "muteEveryone": "Alle stummschalten", + "muteEveryoneElse": "Alle anderen stummschalten", + "muteEveryonesVideo": "Alle Kameras ausschalten", + "muteEveryoneElsesVideo": "Alle anderen Kameras ausschalten", "pip": "Bild-in-Bild-Modus ein-/ausschalten", "privateMessage": "Private Nachricht senden", "profile": "Profil bearbeiten", "raiseHand": "„Melden“ ein-/ausschalten", "recording": "Aufzeichnung ein-/ausschalten", "remoteMute": "Personen stummschalten", + "remoteVideoMute": "Kamera von dieser Person ausschalten", "security": "Sicherheitsoptionen", "Settings": "Einstellungen ein-/ausschalten", "sharedvideo": "YouTube-Videofreigabe ein-/ausschalten", @@ -764,6 +777,7 @@ "moreOptions": "Weitere Optionen", "mute": "Stummschaltung aktivieren / deaktivieren", "muteEveryone": "Alle stummschalten", + "muteEveryonesVideo": "Alle Kameras ausschalten", "noAudioSignalTitle": "Es kommt kein Input von Ihrem Mikrofon!", "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.", "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 @@ }, "videothumbnail": { "domute": "Stummschalten", + "domuteVideo": "Kamera ausschalten", "domuteOthers": "Alle anderen stummschalten", + "domuteVideoOfOthers": "Alle anderen Kameras auschalten", "flip": "Spiegeln", "grantModerator": "Moderationsrechte vergeben", "kick": "Hinauswerfen", "moderator": "Moderation", "mute": "Person ist stumm geschaltet", "muted": "Stummgeschaltet", + "videoMuted": "Kamera ausgeschaltet", "remoteControl": "Fernsteuerung", "show": "Im Vordergrund anzeigen", "videomute": "Person hat die Kamera angehalten" diff --git a/lang/main.json b/lang/main.json index 661e6dee46..3db8db1c5b 100644 --- a/lang/main.json +++ b/lang/main.json @@ -241,12 +241,19 @@ "muteEveryoneElseTitle": "Mute everyone except {{whom}}?", "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.", "muteEveryoneTitle": "Mute everyone?", + "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.", + "muteEveryoneElsesVideoTitle": "Disable everyone's camera except {{whom}}?", + "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.", + "muteEveryonesVideoTitle": "Disable everyone's camera?", "muteEveryoneSelf": "yourself", "muteEveryoneStartMuted": "Everyone starts muted from now on", "muteParticipantBody": "You won't be able to unmute them, but they can unmute themselves at any time.", "muteParticipantButton": "Mute", "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.", "muteParticipantTitle": "Mute this participant?", + "muteParticipantsVideoButton": "Disable camera", + "muteParticipantsVideoTitle": "Disable camera of this participant?", + "muteParticipantsVideoBody": "You won't be able to turn the camera back on, but they can turn it back on at any time.", "Ok": "OK", "passwordLabel": "The meeting has been locked by a participant. Please enter the $t(lockRoomPassword) to join.", "passwordNotSupported": "Setting a meeting $t(lockRoomPassword) is not supported.", @@ -484,6 +491,8 @@ "mutedTitle": "You're muted!", "mutedRemotelyTitle": "You have been muted by {{participantDisplayName}}!", "mutedRemotelyDescription": "You can always unmute when you're ready to speak. Mute back when you're done to keep noise away from the meeting.", + "videoMutedRemotelyTitle": "Your camera has been disabled by {{participantDisplayName}}!", + "videoMutedRemotelyDescription": "You can always turn it on again.", "passwordRemovedRemotely": "$t(lockRoomPasswordUppercase) removed by another participant", "passwordSetRemotely": "$t(lockRoomPasswordUppercase) set by another participant", "raisedHand": "{{name}} would like to speak.", @@ -716,12 +725,16 @@ "moreOptions": "Show more options", "mute": "Toggle mute audio", "muteEveryone": "Mute everyone", + "muteEveryoneElse": "Mute everyone else", + "muteEveryonesVideo": "Disable everyone's camera", + "muteEveryoneElsesVideo": "Disable everyone else's camera", "pip": "Toggle Picture-in-Picture mode", "privateMessage": "Send private message", "profile": "Edit your profile", "raiseHand": "Toggle raise hand", "recording": "Toggle recording", "remoteMute": "Mute participant", + "remoteVideoMute": "Disable camera of participant", "security": "Security options", "Settings": "Toggle settings", "sharedvideo": "Toggle Youtube video sharing", @@ -766,6 +779,7 @@ "moreOptions": "More options", "mute": "Mute / Unmute", "muteEveryone": "Mute everyone", + "muteEveryonesVideo": "Disable everyone's camera", "noAudioSignalTitle": "There is no input coming from your mic!", "noAudioSignalDesc": "If you did not purposely mute it from system settings or hardware, consider switching the device.", "noAudioSignalDescSuggestion": "If you did not purposely mute it from system settings or hardware, consider switching to the suggested device.", @@ -850,13 +864,16 @@ "videothumbnail": { "connectionInfo": "Connection Info", "domute": "Mute", + "domuteVideo": "Disable camera", "domuteOthers": "Mute everyone else", + "domuteVideoOfOthers": "Disable camera of everyone else", "flip": "Flip", "grantModerator": "Grant Moderator", "kick": "Kick out", "moderator": "Moderator", "mute": "Participant is muted", "muted": "Muted", + "videoMuted": "Camera disabled", "remoteControl": "Start / Stop remote control", "show": "Show on stage", "videomute": "Participant has stopped the camera" diff --git a/modules/API/API.js b/modules/API/API.js index 196fa71eb0..6d46441b69 100644 --- a/modules/API/API.js +++ b/modules/API/API.js @@ -14,6 +14,7 @@ import { } from '../../react/features/base/conference'; import { parseJWTFromURLParams } from '../../react/features/base/jwt'; import JitsiMeetJS, { JitsiRecordingConstants } from '../../react/features/base/lib-jitsi-meet'; +import { MEDIA_TYPE } from '../../react/features/base/media'; import { pinParticipant, getParticipantById, kickParticipant } from '../../react/features/base/participants'; import { setPrivateMessageRecipient } from '../../react/features/chat/actions'; import { openChat } from '../../react/features/chat/actions.web'; @@ -79,7 +80,9 @@ function initCommands() { sendAnalytics(createApiEvent('display.name.changed')); APP.conference.changeLocalDisplayName(displayName); }, - 'mute-everyone': () => { + 'mute-everyone': mediaType => { + const muteMediaType = mediaType ? mediaType : MEDIA_TYPE.AUDIO; + sendAnalytics(createApiEvent('muted-everyone')); const participants = APP.store.getState()['features/base/participants']; const localIds = participants @@ -87,7 +90,7 @@ function initCommands() { .filter(participant => participant.role === 'moderator') .map(participant => participant.id); - APP.store.dispatch(muteAllParticipants(localIds)); + APP.store.dispatch(muteAllParticipants(localIds, muteMediaType)); }, 'toggle-lobby': isLobbyEnabled => { APP.store.dispatch(toggleLobbyMode(isLobbyEnabled)); diff --git a/package-lock.json b/package-lock.json index 718fa6ef5f..0ad6db8a61 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10343,8 +10343,8 @@ } }, "lib-jitsi-meet": { - "version": "github:jitsi/lib-jitsi-meet#6a7b16c33e27481b03e5a37636e72426c0848265", - "from": "github:jitsi/lib-jitsi-meet#6a7b16c33e27481b03e5a37636e72426c0848265", + "version": "github:jitsi/lib-jitsi-meet#9beb47fe5f5ae9caf0b206588e14fa676479ce31", + "from": "github:jitsi/lib-jitsi-meet#9beb47fe5f5ae9caf0b206588e14fa676479ce31", "requires": { "@jitsi/js-utils": "1.0.2", "@jitsi/sdp-interop": "1.0.3", diff --git a/package.json b/package.json index 60bec0b848..ea5ddbf447 100644 --- a/package.json +++ b/package.json @@ -56,7 +56,7 @@ "jquery-i18next": "1.2.1", "js-md5": "0.6.1", "jwt-decode": "2.2.0", - "lib-jitsi-meet": "github:jitsi/lib-jitsi-meet#6a7b16c33e27481b03e5a37636e72426c0848265", + "lib-jitsi-meet": "github:jitsi/lib-jitsi-meet#9beb47fe5f5ae9caf0b206588e14fa676479ce31", "libflacjs": "github:mmig/libflac.js#93d37e7f811f01cf7d8b6a603e38bd3c3810907d", "lodash": "4.17.19", "moment": "2.19.4", diff --git a/react/features/analytics/AnalyticsEvents.js b/react/features/analytics/AnalyticsEvents.js index 54e4bd2cb7..4a30d0c800 100644 --- a/react/features/analytics/AnalyticsEvents.js +++ b/react/features/analytics/AnalyticsEvents.js @@ -504,15 +504,17 @@ export function createRejoinedEvent({ url, lastConferenceDuration, timeSinceLeft * * @param {string} participantId - The ID of the participant that was remotely * muted. + * @param {string} mediaType - The media type of the channel to mute. * @returns {Object} The event in a format suitable for sending via * sendAnalytics. */ -export function createRemoteMuteConfirmedEvent(participantId) { +export function createRemoteMuteConfirmedEvent(participantId, mediaType) { return { action: 'clicked', actionSubject: 'remote.mute.dialog.confirm.button', attributes: { - 'participant_id': participantId + 'participant_id': participantId, + 'media_type': mediaType }, source: 'remote.mute.dialog', type: TYPE_UI diff --git a/react/features/base/conference/actions.js b/react/features/base/conference/actions.js index 3a2e5a863d..4ff69abec2 100644 --- a/react/features/base/conference/actions.js +++ b/react/features/base/conference/actions.js @@ -149,9 +149,9 @@ function _addConferenceListeners(conference, dispatch) { conference.on( JitsiConferenceEvents.TRACK_MUTE_CHANGED, - (_, participantThatMutedUs) => { + (track, participantThatMutedUs) => { if (participantThatMutedUs) { - dispatch(participantMutedUs(participantThatMutedUs)); + dispatch(participantMutedUs(participantThatMutedUs, track)); } }); diff --git a/react/features/base/icons/svg/index.js b/react/features/base/icons/svg/index.js index f8844cc81f..2ca7ed3bb3 100644 --- a/react/features/base/icons/svg/index.js +++ b/react/features/base/icons/svg/index.js @@ -68,6 +68,8 @@ export { default as IconMicrophoneEmpty } from './microphone-empty.svg'; export { default as IconModerator } from './star.svg'; export { default as IconMuteEveryone } from './mute-everyone.svg'; export { default as IconMuteEveryoneElse } from './mute-everyone-else.svg'; +export { default as IconMuteVideoEveryone } from './mute-video-everyone.svg'; +export { default as IconMuteVideoEveryoneElse } from './mute-video-everyone-else.svg'; export { default as IconNotificationJoin } from './navigate_next.svg'; export { default as IconOpenInNew } from './open_in_new.svg'; export { default as IconOutlook } from './office365.svg'; diff --git a/react/features/base/icons/svg/mute-video-everyone-else.svg b/react/features/base/icons/svg/mute-video-everyone-else.svg new file mode 100644 index 0000000000..f0442a3e85 --- /dev/null +++ b/react/features/base/icons/svg/mute-video-everyone-else.svg @@ -0,0 +1,12 @@ + + + + + + + diff --git a/react/features/base/icons/svg/mute-video-everyone.svg b/react/features/base/icons/svg/mute-video-everyone.svg new file mode 100644 index 0000000000..5e9c24076a --- /dev/null +++ b/react/features/base/icons/svg/mute-video-everyone.svg @@ -0,0 +1,12 @@ + + + + + + + diff --git a/react/features/base/participants/actions.js b/react/features/base/participants/actions.js index 5ce8dc70b4..5ba4a6abc2 100644 --- a/react/features/base/participants/actions.js +++ b/react/features/base/participants/actions.js @@ -16,7 +16,9 @@ import { PIN_PARTICIPANT, SET_LOADABLE_AVATAR_URL } from './actionTypes'; -import { DISCO_REMOTE_CONTROL_FEATURE } from './constants'; +import { + DISCO_REMOTE_CONTROL_FEATURE +} from './constants'; import { getLocalParticipant, getNormalizedDisplayName, @@ -192,15 +194,18 @@ export function localParticipantRoleChanged(role) { * Create an action for muting another participant in the conference. * * @param {string} id - Participant's ID. + * @param {MEDIA_TYPE} mediaType - The media to mute. * @returns {{ * type: MUTE_REMOTE_PARTICIPANT, - * id: string + * id: string, + * mediaType: MEDIA_TYPE * }} */ -export function muteRemoteParticipant(id) { +export function muteRemoteParticipant(id, mediaType) { return { type: MUTE_REMOTE_PARTICIPANT, - id + id, + mediaType }; } @@ -450,17 +455,20 @@ export function participantUpdated(participant = {}) { * Action to signal that a participant has muted us. * * @param {JitsiParticipant} participant - Information about participant. + * @param {JitsiLocalTrack} track - Information about the track that has been muted. * @returns {Promise} */ -export function participantMutedUs(participant) { +export function participantMutedUs(participant, track) { return (dispatch, getState) => { if (!participant) { return; } + const isAudio = track.isAudioTrack(); + dispatch(showNotification({ - descriptionKey: 'notify.mutedRemotelyDescription', - titleKey: 'notify.mutedRemotelyTitle', + descriptionKey: isAudio ? 'notify.mutedRemotelyDescription' : 'notify.videoMutedRemotelyDescription', + titleKey: isAudio ? 'notify.mutedRemotelyTitle' : 'notify.videoMutedRemotelyTitle', titleArguments: { participantDisplayName: getParticipantDisplayName(getState, participant.getId()) diff --git a/react/features/base/participants/middleware.js b/react/features/base/participants/middleware.js index 55872c41ac..ff038c64e9 100644 --- a/react/features/base/participants/middleware.js +++ b/react/features/base/participants/middleware.js @@ -112,7 +112,7 @@ MiddlewareRegistry.register(store => next => action => { case MUTE_REMOTE_PARTICIPANT: { const { conference } = store.getState()['features/base/conference']; - conference.muteParticipant(action.id); + conference.muteParticipant(action.id, action.mediaType); break; } diff --git a/react/features/mobile/external-api/middleware.js b/react/features/mobile/external-api/middleware.js index 4e88365010..6dde60cf88 100644 --- a/react/features/mobile/external-api/middleware.js +++ b/react/features/mobile/external-api/middleware.js @@ -26,6 +26,7 @@ import { getURLWithoutParams } from '../../base/connection'; import { JitsiConferenceEvents } from '../../base/lib-jitsi-meet'; +import { MEDIA_TYPE } from '../../base/media'; import { SET_AUDIO_MUTED } from '../../base/media/actionTypes'; import { PARTICIPANT_JOINED, PARTICIPANT_LEFT, getParticipants, getParticipantById } from '../../base/participants'; import { MiddlewareRegistry, StateListenerRegistry } from '../../base/redux'; @@ -270,7 +271,7 @@ function _registerForNativeEvents(store) { }); eventEmitter.addListener(ExternalAPI.SET_AUDIO_MUTED, ({ muted }) => { - dispatch(muteLocal(muted === 'true')); + dispatch(muteLocal(muted === 'true', MEDIA_TYPE.AUDIO)); }); eventEmitter.addListener(ExternalAPI.SEND_ENDPOINT_TEXT_MESSAGE, ({ to, message }) => { diff --git a/react/features/remote-video-menu/actions.js b/react/features/remote-video-menu/actions.js index 8594c0db12..59bfadadf1 100644 --- a/react/features/remote-video-menu/actions.js +++ b/react/features/remote-video-menu/actions.js @@ -6,10 +6,16 @@ import { AUDIO_MUTE, createRemoteMuteConfirmedEvent, createToolbarEvent, - sendAnalytics + sendAnalytics, + VIDEO_MUTE } from '../analytics'; import { hideDialog } from '../base/dialog'; -import { setAudioMuted } from '../base/media'; +import { + MEDIA_TYPE, + setAudioMuted, + setVideoMuted, + VIDEO_MUTISM_AUTHORITY +} from '../base/media'; import { getLocalParticipant, muteRemoteParticipant @@ -32,17 +38,26 @@ export function hideRemoteVideoMenu() { * Mutes the local participant. * * @param {boolean} enable - Whether to mute or unmute. + * @param {MEDIA_TYPE} mediaType - The type of the media channel to mute. * @returns {Function} */ -export function muteLocal(enable: boolean) { +export function muteLocal(enable: boolean, mediaType: MEDIA_TYPE) { return (dispatch: Dispatch) => { - sendAnalytics(createToolbarEvent(AUDIO_MUTE, { enable })); - dispatch(setAudioMuted(enable, /* ensureTrack */ true)); + const isAudio = mediaType === MEDIA_TYPE.AUDIO; + + if (!isAudio && mediaType !== MEDIA_TYPE.VIDEO) { + console.error(`Unsupported media type: ${mediaType}`); + + return; + } + sendAnalytics(createToolbarEvent(isAudio ? AUDIO_MUTE : VIDEO_MUTE, { enable })); + dispatch(isAudio ? setAudioMuted(enable, /* ensureTrack */ true) + : setVideoMuted(enable, mediaType, VIDEO_MUTISM_AUTHORITY.USER, /* ensureTrack */ true)); // FIXME: The old conference logic as well as the shared video feature // still rely on this event being emitted. typeof APP === 'undefined' - || APP.UI.emitEvent(UIEvents.AUDIO_MUTED, enable, true); + || APP.UI.emitEvent(isAudio ? UIEvents.AUDIO_MUTED : UIEvents.VIDEO_MUTED, enable, true); }; } @@ -50,12 +65,18 @@ export function muteLocal(enable: boolean) { * Mutes the remote participant with the given ID. * * @param {string} participantId - ID of the participant to mute. + * @param {MEDIA_TYPE} mediaType - The type of the media channel to mute. * @returns {Function} */ -export function muteRemote(participantId: string) { +export function muteRemote(participantId: string, mediaType: MEDIA_TYPE) { return (dispatch: Dispatch) => { - sendAnalytics(createRemoteMuteConfirmedEvent(participantId)); - dispatch(muteRemoteParticipant(participantId)); + if (mediaType !== MEDIA_TYPE.AUDIO && mediaType !== MEDIA_TYPE.VIDEO) { + console.error(`Unsupported media type: ${mediaType}`); + + return; + } + sendAnalytics(createRemoteMuteConfirmedEvent(participantId, mediaType)); + dispatch(muteRemoteParticipant(participantId, mediaType)); }; } @@ -63,9 +84,10 @@ export function muteRemote(participantId: string) { * Mutes all participants. * * @param {Array} exclude - Array of participant IDs to not mute. + * @param {MEDIA_TYPE} mediaType - The media type to mute. * @returns {Function} */ -export function muteAllParticipants(exclude: Array) { +export function muteAllParticipants(exclude: Array, mediaType: MEDIA_TYPE) { return (dispatch: Dispatch, getState: Function) => { const state = getState(); const localId = getLocalParticipant(state).id; @@ -75,7 +97,7 @@ export function muteAllParticipants(exclude: Array) { /* eslint-disable no-confusing-arrow */ participantIds .filter(id => !exclude.includes(id)) - .map(id => id === localId ? muteLocal(true) : muteRemote(id)) + .map(id => id === localId ? muteLocal(true, mediaType) : muteRemote(id, mediaType)) .map(dispatch); /* eslint-enable no-confusing-arrow */ }; diff --git a/react/features/remote-video-menu/components/AbstractMuteEveryoneDialog.js b/react/features/remote-video-menu/components/AbstractMuteEveryoneDialog.js index 1470dee984..617bb5782d 100644 --- a/react/features/remote-video-menu/components/AbstractMuteEveryoneDialog.js +++ b/react/features/remote-video-menu/components/AbstractMuteEveryoneDialog.js @@ -3,6 +3,7 @@ import React from 'react'; import { Dialog } from '../../base/dialog'; +import { MEDIA_TYPE } from '../../base/media'; import { getLocalParticipant, getParticipantDisplayName } from '../../base/participants'; import { muteAllParticipants } from '../actions'; @@ -69,7 +70,7 @@ export default class AbstractMuteEveryoneDialog extends AbstractMuteRe exclude } = this.props; - dispatch(muteAllParticipants(exclude)); + dispatch(muteAllParticipants(exclude, MEDIA_TYPE.AUDIO)); return true; } diff --git a/react/features/remote-video-menu/components/AbstractMuteEveryoneElsesVideoButton.js b/react/features/remote-video-menu/components/AbstractMuteEveryoneElsesVideoButton.js new file mode 100644 index 0000000000..750f3a6d72 --- /dev/null +++ b/react/features/remote-video-menu/components/AbstractMuteEveryoneElsesVideoButton.js @@ -0,0 +1,48 @@ +// @flow + +import { createToolbarEvent, sendAnalytics } from '../../analytics'; +import { openDialog } from '../../base/dialog'; +import { IconMuteVideoEveryone } from '../../base/icons'; +import { AbstractButton, type AbstractButtonProps } from '../../base/toolbox/components'; + +import { MuteEveryonesVideoDialog } from '.'; + +export type Props = AbstractButtonProps & { + + /** + * The redux {@code dispatch} function. + */ + dispatch: Function, + + /** + * The ID of the participant object that this button is supposed to keep unmuted. + */ + participantID: string, + + /** + * The function to be used to translate i18n labels. + */ + t: Function +}; + +/** + * An abstract remote video menu button which disables the camera of all the other participants. + */ +export default class AbstractMuteEveryoneElsesVideoButton extends AbstractButton { + accessibilityLabel = 'toolbar.accessibilityLabel.muteEveryoneElsesVideo'; + icon = IconMuteVideoEveryone; + label = 'videothumbnail.domuteVideoOfOthers'; + + /** + * Handles clicking / pressing the button, and opens a confirmation dialog. + * + * @private + * @returns {void} + */ + _handleClick() { + const { dispatch, participantID } = this.props; + + sendAnalytics(createToolbarEvent('mute.everyoneelsesvideo.pressed')); + dispatch(openDialog(MuteEveryonesVideoDialog, { exclude: [ participantID ] })); + } +} diff --git a/react/features/remote-video-menu/components/AbstractMuteEveryonesVideoDialog.js b/react/features/remote-video-menu/components/AbstractMuteEveryonesVideoDialog.js new file mode 100644 index 0000000000..20e5aec88f --- /dev/null +++ b/react/features/remote-video-menu/components/AbstractMuteEveryonesVideoDialog.js @@ -0,0 +1,103 @@ +// @flow + +import React from 'react'; + +import { Dialog } from '../../base/dialog'; +import { MEDIA_TYPE } from '../../base/media'; +import { getLocalParticipant, getParticipantDisplayName } from '../../base/participants'; +import { muteAllParticipants } from '../actions'; + +import AbstractMuteRemoteParticipantsVideoDialog, { + type Props as AbstractProps +} from './AbstractMuteRemoteParticipantsVideoDialog'; + +/** + * The type of the React {@code Component} props of + * {@link AbstractMuteEveryonesVideoDialog}. + */ +export type Props = AbstractProps & { + + content: string, + exclude: Array, + title: string +}; + +/** + * + * An abstract Component with the contents for a dialog that asks for confirmation + * from the user before disabling all remote participants cameras. + * + * @extends AbstractMuteRemoteParticipantsVideoDialog + */ +export default class AbstractMuteEveryonesVideoDialog extends AbstractMuteRemoteParticipantsVideoDialog

{ + static defaultProps = { + exclude: [], + muteLocal: false + }; + + /** + * Implements React's {@link Component#render()}. + * + * @inheritdoc + * @returns {ReactElement} + */ + render() { + const { content, title } = this.props; + + return ( +

+
+ { content } +
+
+ ); + } + + _onSubmit: () => boolean; + + /** + * Callback to be invoked when the value of this dialog is submitted. + * + * @returns {boolean} + */ + _onSubmit() { + const { + dispatch, + exclude + } = this.props; + + dispatch(muteAllParticipants(exclude, MEDIA_TYPE.VIDEO)); + + return true; + } +} + +/** + * Maps (parts of) the Redux state to the associated {@code AbstractMuteEveryonesVideoDialog}'s props. + * + * @param {Object} state - The redux state. + * @param {Object} ownProps - The properties explicitly passed to the component. + * @returns {Props} + */ +export function abstractMapStateToProps(state: Object, ownProps: Props) { + const { exclude, t } = ownProps; + + const whom = exclude + // eslint-disable-next-line no-confusing-arrow + .map(id => id === getLocalParticipant(state).id + ? t('dialog.muteEveryoneSelf') + : getParticipantDisplayName(state, id)) + .join(', '); + + return whom.length ? { + content: t('dialog.muteEveryoneElsesVideoDialog'), + title: t('dialog.muteEveryoneElsesVideoTitle', { whom }) + } : { + content: t('dialog.muteEveryonesVideoDialog'), + title: t('dialog.muteEveryonesVideoTitle') + }; +} diff --git a/react/features/remote-video-menu/components/AbstractMuteRemoteParticipantDialog.js b/react/features/remote-video-menu/components/AbstractMuteRemoteParticipantDialog.js index b4e01685f7..b7322a2227 100644 --- a/react/features/remote-video-menu/components/AbstractMuteRemoteParticipantDialog.js +++ b/react/features/remote-video-menu/components/AbstractMuteRemoteParticipantDialog.js @@ -2,6 +2,7 @@ import { Component } from 'react'; +import { MEDIA_TYPE } from '../../base/media'; import { muteRemote } from '../actions'; /** @@ -57,7 +58,7 @@ export default class AbstractMuteRemoteParticipantDialog _onSubmit() { const { dispatch, participantID } = this.props; - dispatch(muteRemote(participantID)); + dispatch(muteRemote(participantID, MEDIA_TYPE.AUDIO)); return true; } diff --git a/react/features/remote-video-menu/components/AbstractMuteRemoteParticipantsVideoDialog.js b/react/features/remote-video-menu/components/AbstractMuteRemoteParticipantsVideoDialog.js new file mode 100644 index 0000000000..d32db1375f --- /dev/null +++ b/react/features/remote-video-menu/components/AbstractMuteRemoteParticipantsVideoDialog.js @@ -0,0 +1,65 @@ +// @flow + +import { Component } from 'react'; + +import { MEDIA_TYPE } from '../../base/media'; +import { muteRemote } from '../actions'; + +/** + * The type of the React {@code Component} props of + * {@link AbstractMuteRemoteParticipantsVideoDialog}. + */ +export type Props = { + + /** + * The Redux dispatch function. + */ + dispatch: Function, + + /** + * The ID of the remote participant to be muted. + */ + participantID: string, + + /** + * Function to translate i18n labels. + */ + t: Function +}; + +/** + * Abstract dialog to confirm a remote participant video ute action. + * + * @extends Component + */ +export default class AbstractMuteRemoteParticipantsVideoDialog + extends Component

{ + /** + * Initializes a new {@code AbstractMuteRemoteParticipantsVideoDialog} instance. + * + * @param {Object} props - The read-only properties with which the new + * instance is to be initialized. + */ + constructor(props: P) { + super(props); + + // Bind event handlers so they are only bound once per instance. + this._onSubmit = this._onSubmit.bind(this); + } + + _onSubmit: () => boolean; + + /** + * Handles the submit button action. + * + * @private + * @returns {boolean} - True (to note that the modal should be closed). + */ + _onSubmit() { + const { dispatch, participantID } = this.props; + + dispatch(muteRemote(participantID, MEDIA_TYPE.VIDEO)); + + return true; + } +} diff --git a/react/features/remote-video-menu/components/AbstractMuteVideoButton.js b/react/features/remote-video-menu/components/AbstractMuteVideoButton.js new file mode 100644 index 0000000000..1b72a23834 --- /dev/null +++ b/react/features/remote-video-menu/components/AbstractMuteVideoButton.js @@ -0,0 +1,103 @@ +// @flow + +import { + createRemoteVideoMenuButtonEvent, + sendAnalytics +} from '../../analytics'; +import { openDialog } from '../../base/dialog'; +import { IconCameraDisabled } from '../../base/icons'; +import { MEDIA_TYPE } from '../../base/media'; +import { AbstractButton, type AbstractButtonProps } from '../../base/toolbox/components'; +import { isRemoteTrackMuted } from '../../base/tracks'; + +import { MuteRemoteParticipantsVideoDialog } from '.'; + +export type Props = AbstractButtonProps & { + + /** + * Boolean to indicate if the video track of the participant is muted or + * not. + */ + _videoTrackMuted: boolean, + + /** + * The redux {@code dispatch} function. + */ + dispatch: Function, + + /** + * The ID of the participant object that this button is supposed to + * mute/unmute. + */ + participantID: string, + + /** + * The function to be used to translate i18n labels. + */ + t: Function +}; + +/** + * An abstract remote video menu button which mutes the remote participant. + */ +export default class AbstractMuteVideoButton extends AbstractButton { + accessibilityLabel = 'toolbar.accessibilityLabel.remoteVideoMute'; + icon = IconCameraDisabled; + label = 'videothumbnail.domuteVideo'; + toggledLabel = 'videothumbnail.videoMuted'; + + /** + * Handles clicking / pressing the button, and mutes the participant. + * + * @private + * @returns {void} + */ + _handleClick() { + const { dispatch, participantID } = this.props; + + sendAnalytics(createRemoteVideoMenuButtonEvent( + 'mute.button', + { + 'participant_id': participantID + })); + + dispatch(openDialog(MuteRemoteParticipantsVideoDialog, { participantID })); + } + + /** + * Renders the item disabled if the participant is muted. + * + * @inheritdoc + */ + _isDisabled() { + return this.props._videoTrackMuted; + } + + /** + * Renders the item toggled if the participant is muted. + * + * @inheritdoc + */ + _isToggled() { + return this.props._videoTrackMuted; + } +} + +/** + * Function that maps parts of Redux state tree into component props. + * + * @param {Object} state - Redux state. + * @param {Object} ownProps - Properties of component. + * @private + * @returns {{ + * _videoTrackMuted: boolean + * }} + */ +export function _mapStateToProps(state: Object, ownProps: Props) { + const tracks = state['features/base/tracks']; + + return { + _videoTrackMuted: isRemoteTrackMuted( + tracks, MEDIA_TYPE.VIDEO, ownProps.participantID) + }; +} diff --git a/react/features/remote-video-menu/components/web/MuteEveryoneElsesVideoButton.js b/react/features/remote-video-menu/components/web/MuteEveryoneElsesVideoButton.js new file mode 100644 index 0000000000..99e4eb1c2c --- /dev/null +++ b/react/features/remote-video-menu/components/web/MuteEveryoneElsesVideoButton.js @@ -0,0 +1,54 @@ +// @flow + +import React from 'react'; + +import { translate } from '../../../base/i18n'; +import { IconMuteVideoEveryoneElse } from '../../../base/icons'; +import { connect } from '../../../base/redux'; +import AbstractMuteEveryoneElsesVideoButton, { + type Props +} from '../AbstractMuteEveryoneElsesVideoButton'; + +import RemoteVideoMenuButton from './RemoteVideoMenuButton'; + +/** + * Implements a React {@link Component} which displays a button for audio muting + * every participant in the conference except the one with the given + * participantID + */ +class MuteEveryoneElsesVideoButton extends AbstractMuteEveryoneElsesVideoButton { + /** + * Instantiates a new {@code Component}. + * + * @inheritdoc + */ + constructor(props: Props) { + super(props); + + this._handleClick = this._handleClick.bind(this); + } + + /** + * Implements React's {@link Component#render()}. + * + * @inheritdoc + * @returns {ReactElement} + */ + render() { + const { participantID, t } = this.props; + + return ( + + ); + } + + _handleClick: () => void; +} + +export default translate(connect()(MuteEveryoneElsesVideoButton)); diff --git a/react/features/remote-video-menu/components/web/MuteEveryonesVideoDialog.js b/react/features/remote-video-menu/components/web/MuteEveryonesVideoDialog.js new file mode 100644 index 0000000000..592c595407 --- /dev/null +++ b/react/features/remote-video-menu/components/web/MuteEveryonesVideoDialog.js @@ -0,0 +1,41 @@ +// @flow + +import React from 'react'; + +import { Dialog } from '../../../base/dialog'; +import { translate } from '../../../base/i18n'; +import { connect } from '../../../base/redux'; +import AbstractMuteEveryonesVideoDialog, { abstractMapStateToProps, type Props } + from '../AbstractMuteEveryonesVideoDialog'; + +/** + * A React Component with the contents for a dialog that asks for confirmation + * from the user before disabling all remote participants cameras. + * + * @extends AbstractMuteEveryonesVideoDialog + */ +class MuteEveryonesVideoDialog extends AbstractMuteEveryonesVideoDialog { + /** + * Implements React's {@link Component#render()}. + * + * @inheritdoc + * @returns {ReactElement} + */ + render() { + return ( +

+
+ { this.props.content } +
+
+ ); + } + + _onSubmit: () => boolean; +} + +export default translate(connect(abstractMapStateToProps)(MuteEveryonesVideoDialog)); diff --git a/react/features/remote-video-menu/components/web/MuteRemoteParticipantsVideoDialog.js b/react/features/remote-video-menu/components/web/MuteRemoteParticipantsVideoDialog.js new file mode 100644 index 0000000000..292e456552 --- /dev/null +++ b/react/features/remote-video-menu/components/web/MuteRemoteParticipantsVideoDialog.js @@ -0,0 +1,41 @@ +/* @flow */ + +import React from 'react'; + +import { Dialog } from '../../../base/dialog'; +import { translate } from '../../../base/i18n'; +import { connect } from '../../../base/redux'; +import AbstractMuteRemoteParticipantsVideoDialog + from '../AbstractMuteRemoteParticipantsVideoDialog'; + +/** + * A React Component with the contents for a dialog that asks for confirmation + * from the user before disabling a remote participants camera. + * + * @extends Component + */ +class MuteRemoteParticipantsVideoDialog extends AbstractMuteRemoteParticipantsVideoDialog { + /** + * Implements React's {@link Component#render()}. + * + * @inheritdoc + * @returns {ReactElement} + */ + render() { + return ( + +
+ { this.props.t('dialog.muteParticipantsVideoBody') } +
+
+ ); + } + + _onSubmit: () => boolean; +} + +export default translate(connect()(MuteRemoteParticipantsVideoDialog)); diff --git a/react/features/remote-video-menu/components/web/MuteVideoButton.js b/react/features/remote-video-menu/components/web/MuteVideoButton.js new file mode 100644 index 0000000000..f6863f5ebb --- /dev/null +++ b/react/features/remote-video-menu/components/web/MuteVideoButton.js @@ -0,0 +1,67 @@ +/* @flow */ + +import React from 'react'; + +import { translate } from '../../../base/i18n'; +import { IconCameraDisabled } from '../../../base/icons'; +import { connect } from '../../../base/redux'; +import AbstractMuteVideoButton, { + _mapStateToProps, + type Props +} from '../AbstractMuteVideoButton'; + +import RemoteVideoMenuButton from './RemoteVideoMenuButton'; + +/** + * Implements a React {@link Component} which displays a button for disabling + * the camera of a participant in the conference. + * + * NOTE: At the time of writing this is a button that doesn't use the + * {@code AbstractButton} base component, but is inherited from the same + * super class ({@code AbstractMuteVideoButton} that extends {@code AbstractButton}) + * for the sake of code sharing between web and mobile. Once web uses the + * {@code AbstractButton} base component, this can be fully removed. + */ +class MuteVideoButton extends AbstractMuteVideoButton { + /** + * Instantiates a new {@code Component}. + * + * @inheritdoc + */ + constructor(props: Props) { + super(props); + + this._handleClick = this._handleClick.bind(this); + } + + /** + * Implements React's {@link Component#render()}. + * + * @inheritdoc + * @returns {ReactElement} + */ + render() { + const { _videoTrackMuted, participantID, t } = this.props; + const muteConfig = _videoTrackMuted ? { + translationKey: 'videothumbnail.videoMuted', + muteClassName: 'mutelink disabled' + } : { + translationKey: 'videothumbnail.domuteVideo', + muteClassName: 'mutelink' + }; + + return ( + + ); + } + + _handleClick: () => void +} + +export default translate(connect(_mapStateToProps)(MuteVideoButton)); diff --git a/react/features/remote-video-menu/components/web/RemoteVideoMenuTriggerButton.js b/react/features/remote-video-menu/components/web/RemoteVideoMenuTriggerButton.js index 4d1e9e3f5a..9d6c9866dd 100644 --- a/react/features/remote-video-menu/components/web/RemoteVideoMenuTriggerButton.js +++ b/react/features/remote-video-menu/components/web/RemoteVideoMenuTriggerButton.js @@ -3,20 +3,20 @@ import React, { Component } from 'react'; import { Icon, IconMenuThumb } from '../../../base/icons'; -import { MEDIA_TYPE } from '../../../base/media'; import { getLocalParticipant, getParticipantById, PARTICIPANT_ROLE } from '../../../base/participants'; import { Popover } from '../../../base/popover'; import { connect } from '../../../base/redux'; -import { isRemoteTrackMuted } from '../../../base/tracks'; import { requestRemoteControl, stopController } from '../../../remote-control'; import { getCurrentLayout, LAYOUTS } from '../../../video-layout'; import MuteEveryoneElseButton from './MuteEveryoneElseButton'; +import MuteEveryoneElsesVideoButton from './MuteEveryoneElsesVideoButton'; import { REMOTE_CONTROL_MENU_STATES } from './RemoteControlButton'; import { GrantModeratorButton, MuteButton, + MuteVideoButton, KickButton, PrivateMessageMenuButton, RemoteControlButton, @@ -43,11 +43,6 @@ type Props = { */ _disableRemoteMute: Boolean, - /** - * Whether or not the participant is currently muted. - */ - _isAudioMuted: boolean, - /** * Whether or not the participant is a conference moderator. */ @@ -151,7 +146,6 @@ class RemoteVideoMenuTriggerButton extends Component { const { _disableKick, _disableRemoteMute, - _isAudioMuted, _isModerator, dispatch, initialVolumeValue, @@ -166,7 +160,6 @@ class RemoteVideoMenuTriggerButton extends Component { if (!_disableRemoteMute) { buttons.push( ); @@ -175,6 +168,16 @@ class RemoteVideoMenuTriggerButton extends Component { key = 'mute-others' participantID = { participantID } /> ); + buttons.push( + + ); + buttons.push( + + ); } buttons.push( @@ -247,7 +250,6 @@ class RemoteVideoMenuTriggerButton extends Component { */ function _mapStateToProps(state, ownProps) { const { participantID } = ownProps; - const tracks = state['features/base/tracks']; const localParticipant = getLocalParticipant(state); const { remoteVideoMenu = {}, disableRemoteMute } = state['features/base/config']; const { disableKick } = remoteVideoMenu; @@ -286,7 +288,6 @@ function _mapStateToProps(state, ownProps) { } return { - _isAudioMuted: isRemoteTrackMuted(tracks, MEDIA_TYPE.AUDIO, participantID) || false, _isModerator: Boolean(localParticipant?.role === PARTICIPANT_ROLE.MODERATOR), _disableKick: Boolean(disableKick), _disableRemoteMute: Boolean(disableRemoteMute), diff --git a/react/features/remote-video-menu/components/web/index.js b/react/features/remote-video-menu/components/web/index.js index 78cd6b8ddc..7a80e58e4b 100644 --- a/react/features/remote-video-menu/components/web/index.js +++ b/react/features/remote-video-menu/components/web/index.js @@ -5,9 +5,13 @@ export { default as GrantModeratorDialog } from './GrantModeratorDialog'; export { default as KickButton } from './KickButton'; export { default as KickRemoteParticipantDialog } from './KickRemoteParticipantDialog'; export { default as MuteButton } from './MuteButton'; +export { default as MuteVideoButton } from './MuteVideoButton'; export { default as MuteEveryoneDialog } from './MuteEveryoneDialog'; +export { default as MuteEveryonesVideoDialog } from './MuteEveryonesVideoDialog'; export { default as MuteEveryoneElseButton } from './MuteEveryoneElseButton'; +export { default as MuteEveryoneElsesVideoButton } from './MuteEveryoneElsesVideoButton'; export { default as MuteRemoteParticipantDialog } from './MuteRemoteParticipantDialog'; +export { default as MuteRemoteParticipantsVideoDialog } from './MuteRemoteParticipantsVideoDialog'; export { default as PrivateMessageMenuButton } from './PrivateMessageMenuButton'; export { REMOTE_CONTROL_MENU_STATES, default as RemoteControlButton } from './RemoteControlButton'; export { default as RemoteVideoMenu } from './RemoteVideoMenu'; diff --git a/react/features/toolbox/components/AudioMuteButton.js b/react/features/toolbox/components/AudioMuteButton.js index 9599ed1e67..c03bf641bf 100644 --- a/react/features/toolbox/components/AudioMuteButton.js +++ b/react/features/toolbox/components/AudioMuteButton.js @@ -125,7 +125,7 @@ class AudioMuteButton extends AbstractAudioMuteButton { * @returns {void} */ _setAudioMuted(audioMuted: boolean) { - this.props.dispatch(muteLocal(audioMuted)); + this.props.dispatch(muteLocal(audioMuted, MEDIA_TYPE.AUDIO)); } /** diff --git a/react/features/toolbox/components/MuteEveryonesVideoButton.js b/react/features/toolbox/components/MuteEveryonesVideoButton.js new file mode 100644 index 0000000000..750485bce1 --- /dev/null +++ b/react/features/toolbox/components/MuteEveryonesVideoButton.js @@ -0,0 +1,76 @@ +// @flow + +import { createToolbarEvent, sendAnalytics } from '../../analytics'; +import { openDialog } from '../../base/dialog'; +import { translate } from '../../base/i18n'; +import { IconMuteVideoEveryone } from '../../base/icons'; +import { getLocalParticipant, PARTICIPANT_ROLE } from '../../base/participants'; +import { connect } from '../../base/redux'; +import { AbstractButton, type AbstractButtonProps } from '../../base/toolbox/components'; +import { MuteEveryonesVideoDialog } from '../../remote-video-menu/components'; + +type Props = AbstractButtonProps & { + + /** + * The Redux dispatch function. + */ + dispatch: Function, + + /* + ** Whether the local participant is a moderator or not. + */ + isModerator: Boolean, + + /** + * The ID of the local participant. + */ + localParticipantId: string +}; + +/** + * Implements a React {@link Component} which displays a button for disabling the camera of + * every participant (except the local one) + */ +class MuteEveryonesVideoButton extends AbstractButton { + accessibilityLabel = 'toolbar.accessibilityLabel.muteEveryonesVideo'; + icon = IconMuteVideoEveryone; + label = 'toolbar.muteEveryonesVideo'; + tooltip = 'toolbar.muteVideoEveryone'; + + /** + * Handles clicking / pressing the button, and opens a confirmation dialog. + * + * @private + * @returns {void} + */ + _handleClick() { + const { dispatch, localParticipantId } = this.props; + + sendAnalytics(createToolbarEvent('mute.everyone.pressed')); + dispatch(openDialog(MuteEveryonesVideoDialog, { + exclude: [ localParticipantId ] + })); + } +} + +/** + * Maps part of the redux state to the component's props. + * + * @param {Object} state - The redux store/state. + * @param {Props} ownProps - The component's own props. + * @returns {Object} + */ +function _mapStateToProps(state: Object, ownProps: Props) { + const localParticipant = getLocalParticipant(state); + const isModerator = localParticipant.role === PARTICIPANT_ROLE.MODERATOR; + const { visible } = ownProps; + const { disableRemoteMute } = state['features/base/config']; + + return { + isModerator, + localParticipantId: localParticipant.id, + visible: visible && isModerator && !disableRemoteMute + }; +} + +export default translate(connect(_mapStateToProps)(MuteEveryonesVideoButton)); diff --git a/react/features/toolbox/components/web/Toolbox.js b/react/features/toolbox/components/web/Toolbox.js index a8c9bbaa6c..9cd2eb7dbe 100644 --- a/react/features/toolbox/components/web/Toolbox.js +++ b/react/features/toolbox/components/web/Toolbox.js @@ -82,6 +82,7 @@ import DownloadButton from '../DownloadButton'; import HangupButton from '../HangupButton'; import HelpButton from '../HelpButton'; import MuteEveryoneButton from '../MuteEveryoneButton'; +import MuteEveryonesVideoButton from '../MuteEveryonesVideoButton'; import AudioSettingsButton from './AudioSettingsButton'; import OverflowMenuButton from './OverflowMenuButton'; @@ -1079,6 +1080,10 @@ class Toolbox extends Component { && , + this._shouldShowButton('mute-video-everyone') + && , this._shouldShowButton('stats') &&