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>
This commit is contained in:
Steffen Kolmer 2021-02-24 22:45:07 +01:00 committed by GitHub
parent 42d926eef3
commit 23bb824731
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
32 changed files with 754 additions and 46 deletions

View File

@ -2008,7 +2008,10 @@ export default {
room.on(JitsiConferenceEvents.TRACK_MUTE_CHANGED, (track, participantThatMutedUs) => { room.on(JitsiConferenceEvents.TRACK_MUTE_CHANGED, (track, participantThatMutedUs) => {
if (participantThatMutedUs) { if (participantThatMutedUs) {
APP.store.dispatch(participantMutedUs(participantThatMutedUs)); APP.store.dispatch(participantMutedUs(participantThatMutedUs, track));
if (this.isSharingScreen && track.isVideoTrack()) {
this._turnScreenSharingOff(false);
}
} }
}); });

View File

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

View File

@ -206,7 +206,7 @@ var interfaceConfig = {
'fodeviceselection', 'hangup', 'profile', 'chat', 'recording', 'fodeviceselection', 'hangup', 'profile', 'chat', 'recording',
'livestreaming', 'etherpad', 'sharedvideo', 'settings', 'raisehand', 'livestreaming', 'etherpad', 'sharedvideo', 'settings', 'raisehand',
'videoquality', 'filmstrip', 'invite', 'feedback', 'stats', 'shortcuts', '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, TOOLBAR_TIMEOUT: 4000,

View File

@ -239,12 +239,19 @@
"muteEveryoneElseTitle": "Alle außer {{whom}} stummschalten?", "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.", "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?", "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", "muteEveryoneSelf": "sich selbst",
"muteEveryoneStartMuted": "Alle beginnen von jetzt an stummgeschaltet", "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.", "muteParticipantBody": "Sie können die Stummschaltung anderer Personen nicht aufheben, aber eine Person kann ihre eigene Stummschaltung jederzeit beenden.",
"muteParticipantButton": "Stummschalten", "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.", "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?", "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", "Ok": "OK",
"passwordLabel": "Dieses Meeting wurde gesichert. Bitte geben Sie das $t(lockRoomPasswordUppercase) ein, um dem Meeting beizutreten.", "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.", "passwordNotSupported": "Das Festlegen eines Konferenzpassworts wird nicht unterstützt.",
@ -484,6 +491,8 @@
"mutedTitle": "Stummschaltung aktiv!", "mutedTitle": "Stummschaltung aktiv!",
"mutedRemotelyTitle": "Sie wurden von {{participantDisplayName}} stummgeschaltet!", "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.", "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", "passwordRemovedRemotely": "$t(lockRoomPasswordUppercase) von einer anderen Person entfernt",
"passwordSetRemotely": "$t(lockRoomPasswordUppercase) von einer anderen Person gesetzt", "passwordSetRemotely": "$t(lockRoomPasswordUppercase) von einer anderen Person gesetzt",
"raisedHand": "{{name}} möchte sprechen.", "raisedHand": "{{name}} möchte sprechen.",
@ -714,12 +723,16 @@
"moreOptions": "Menü „Weitere Optionen“", "moreOptions": "Menü „Weitere Optionen“",
"mute": "„Audio stummschalten“ ein-/ausschalten", "mute": "„Audio stummschalten“ ein-/ausschalten",
"muteEveryone": "Alle stummschalten", "muteEveryone": "Alle stummschalten",
"muteEveryoneElse": "Alle anderen stummschalten",
"muteEveryonesVideo": "Alle Kameras ausschalten",
"muteEveryoneElsesVideo": "Alle anderen Kameras ausschalten",
"pip": "Bild-in-Bild-Modus ein-/ausschalten", "pip": "Bild-in-Bild-Modus ein-/ausschalten",
"privateMessage": "Private Nachricht senden", "privateMessage": "Private Nachricht senden",
"profile": "Profil bearbeiten", "profile": "Profil bearbeiten",
"raiseHand": "„Melden“ ein-/ausschalten", "raiseHand": "„Melden“ ein-/ausschalten",
"recording": "Aufzeichnung ein-/ausschalten", "recording": "Aufzeichnung ein-/ausschalten",
"remoteMute": "Personen stummschalten", "remoteMute": "Personen stummschalten",
"remoteVideoMute": "Kamera von dieser Person ausschalten",
"security": "Sicherheitsoptionen", "security": "Sicherheitsoptionen",
"Settings": "Einstellungen ein-/ausschalten", "Settings": "Einstellungen ein-/ausschalten",
"sharedvideo": "YouTube-Videofreigabe ein-/ausschalten", "sharedvideo": "YouTube-Videofreigabe ein-/ausschalten",
@ -764,6 +777,7 @@
"moreOptions": "Weitere Optionen", "moreOptions": "Weitere Optionen",
"mute": "Stummschaltung aktivieren / deaktivieren", "mute": "Stummschaltung aktivieren / deaktivieren",
"muteEveryone": "Alle stummschalten", "muteEveryone": "Alle stummschalten",
"muteEveryonesVideo": "Alle Kameras ausschalten",
"noAudioSignalTitle": "Es kommt kein Input von Ihrem Mikrofon!", "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.", "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.", "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": { "videothumbnail": {
"domute": "Stummschalten", "domute": "Stummschalten",
"domuteVideo": "Kamera ausschalten",
"domuteOthers": "Alle anderen stummschalten", "domuteOthers": "Alle anderen stummschalten",
"domuteVideoOfOthers": "Alle anderen Kameras auschalten",
"flip": "Spiegeln", "flip": "Spiegeln",
"grantModerator": "Moderationsrechte vergeben", "grantModerator": "Moderationsrechte vergeben",
"kick": "Hinauswerfen", "kick": "Hinauswerfen",
"moderator": "Moderation", "moderator": "Moderation",
"mute": "Person ist stumm geschaltet", "mute": "Person ist stumm geschaltet",
"muted": "Stummgeschaltet", "muted": "Stummgeschaltet",
"videoMuted": "Kamera ausgeschaltet",
"remoteControl": "Fernsteuerung", "remoteControl": "Fernsteuerung",
"show": "Im Vordergrund anzeigen", "show": "Im Vordergrund anzeigen",
"videomute": "Person hat die Kamera angehalten" "videomute": "Person hat die Kamera angehalten"

View File

@ -241,12 +241,19 @@
"muteEveryoneElseTitle": "Mute everyone except {{whom}}?", "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.", "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?", "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", "muteEveryoneSelf": "yourself",
"muteEveryoneStartMuted": "Everyone starts muted from now on", "muteEveryoneStartMuted": "Everyone starts muted from now on",
"muteParticipantBody": "You won't be able to unmute them, but they can unmute themselves at any time.", "muteParticipantBody": "You won't be able to unmute them, but they can unmute themselves at any time.",
"muteParticipantButton": "Mute", "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.", "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?", "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", "Ok": "OK",
"passwordLabel": "The meeting has been locked by a participant. Please enter the $t(lockRoomPassword) to join.", "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.", "passwordNotSupported": "Setting a meeting $t(lockRoomPassword) is not supported.",
@ -484,6 +491,8 @@
"mutedTitle": "You're muted!", "mutedTitle": "You're muted!",
"mutedRemotelyTitle": "You have been muted by {{participantDisplayName}}!", "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.", "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", "passwordRemovedRemotely": "$t(lockRoomPasswordUppercase) removed by another participant",
"passwordSetRemotely": "$t(lockRoomPasswordUppercase) set by another participant", "passwordSetRemotely": "$t(lockRoomPasswordUppercase) set by another participant",
"raisedHand": "{{name}} would like to speak.", "raisedHand": "{{name}} would like to speak.",
@ -716,12 +725,16 @@
"moreOptions": "Show more options", "moreOptions": "Show more options",
"mute": "Toggle mute audio", "mute": "Toggle mute audio",
"muteEveryone": "Mute everyone", "muteEveryone": "Mute everyone",
"muteEveryoneElse": "Mute everyone else",
"muteEveryonesVideo": "Disable everyone's camera",
"muteEveryoneElsesVideo": "Disable everyone else's camera",
"pip": "Toggle Picture-in-Picture mode", "pip": "Toggle Picture-in-Picture mode",
"privateMessage": "Send private message", "privateMessage": "Send private message",
"profile": "Edit your profile", "profile": "Edit your profile",
"raiseHand": "Toggle raise hand", "raiseHand": "Toggle raise hand",
"recording": "Toggle recording", "recording": "Toggle recording",
"remoteMute": "Mute participant", "remoteMute": "Mute participant",
"remoteVideoMute": "Disable camera of participant",
"security": "Security options", "security": "Security options",
"Settings": "Toggle settings", "Settings": "Toggle settings",
"sharedvideo": "Toggle Youtube video sharing", "sharedvideo": "Toggle Youtube video sharing",
@ -766,6 +779,7 @@
"moreOptions": "More options", "moreOptions": "More options",
"mute": "Mute / Unmute", "mute": "Mute / Unmute",
"muteEveryone": "Mute everyone", "muteEveryone": "Mute everyone",
"muteEveryonesVideo": "Disable everyone's camera",
"noAudioSignalTitle": "There is no input coming from your mic!", "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.", "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.", "noAudioSignalDescSuggestion": "If you did not purposely mute it from system settings or hardware, consider switching to the suggested device.",
@ -850,13 +864,16 @@
"videothumbnail": { "videothumbnail": {
"connectionInfo": "Connection Info", "connectionInfo": "Connection Info",
"domute": "Mute", "domute": "Mute",
"domuteVideo": "Disable camera",
"domuteOthers": "Mute everyone else", "domuteOthers": "Mute everyone else",
"domuteVideoOfOthers": "Disable camera of everyone else",
"flip": "Flip", "flip": "Flip",
"grantModerator": "Grant Moderator", "grantModerator": "Grant Moderator",
"kick": "Kick out", "kick": "Kick out",
"moderator": "Moderator", "moderator": "Moderator",
"mute": "Participant is muted", "mute": "Participant is muted",
"muted": "Muted", "muted": "Muted",
"videoMuted": "Camera disabled",
"remoteControl": "Start / Stop remote control", "remoteControl": "Start / Stop remote control",
"show": "Show on stage", "show": "Show on stage",
"videomute": "Participant has stopped the camera" "videomute": "Participant has stopped the camera"

View File

@ -14,6 +14,7 @@ import {
} from '../../react/features/base/conference'; } from '../../react/features/base/conference';
import { parseJWTFromURLParams } from '../../react/features/base/jwt'; import { parseJWTFromURLParams } from '../../react/features/base/jwt';
import JitsiMeetJS, { JitsiRecordingConstants } from '../../react/features/base/lib-jitsi-meet'; 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 { pinParticipant, getParticipantById, kickParticipant } from '../../react/features/base/participants';
import { setPrivateMessageRecipient } from '../../react/features/chat/actions'; import { setPrivateMessageRecipient } from '../../react/features/chat/actions';
import { openChat } from '../../react/features/chat/actions.web'; import { openChat } from '../../react/features/chat/actions.web';
@ -79,7 +80,9 @@ function initCommands() {
sendAnalytics(createApiEvent('display.name.changed')); sendAnalytics(createApiEvent('display.name.changed'));
APP.conference.changeLocalDisplayName(displayName); APP.conference.changeLocalDisplayName(displayName);
}, },
'mute-everyone': () => { 'mute-everyone': mediaType => {
const muteMediaType = mediaType ? mediaType : MEDIA_TYPE.AUDIO;
sendAnalytics(createApiEvent('muted-everyone')); sendAnalytics(createApiEvent('muted-everyone'));
const participants = APP.store.getState()['features/base/participants']; const participants = APP.store.getState()['features/base/participants'];
const localIds = participants const localIds = participants
@ -87,7 +90,7 @@ function initCommands() {
.filter(participant => participant.role === 'moderator') .filter(participant => participant.role === 'moderator')
.map(participant => participant.id); .map(participant => participant.id);
APP.store.dispatch(muteAllParticipants(localIds)); APP.store.dispatch(muteAllParticipants(localIds, muteMediaType));
}, },
'toggle-lobby': isLobbyEnabled => { 'toggle-lobby': isLobbyEnabled => {
APP.store.dispatch(toggleLobbyMode(isLobbyEnabled)); APP.store.dispatch(toggleLobbyMode(isLobbyEnabled));

4
package-lock.json generated
View File

@ -10343,8 +10343,8 @@
} }
}, },
"lib-jitsi-meet": { "lib-jitsi-meet": {
"version": "github:jitsi/lib-jitsi-meet#6a7b16c33e27481b03e5a37636e72426c0848265", "version": "github:jitsi/lib-jitsi-meet#9beb47fe5f5ae9caf0b206588e14fa676479ce31",
"from": "github:jitsi/lib-jitsi-meet#6a7b16c33e27481b03e5a37636e72426c0848265", "from": "github:jitsi/lib-jitsi-meet#9beb47fe5f5ae9caf0b206588e14fa676479ce31",
"requires": { "requires": {
"@jitsi/js-utils": "1.0.2", "@jitsi/js-utils": "1.0.2",
"@jitsi/sdp-interop": "1.0.3", "@jitsi/sdp-interop": "1.0.3",

View File

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

View File

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

View File

@ -149,9 +149,9 @@ function _addConferenceListeners(conference, dispatch) {
conference.on( conference.on(
JitsiConferenceEvents.TRACK_MUTE_CHANGED, JitsiConferenceEvents.TRACK_MUTE_CHANGED,
(_, participantThatMutedUs) => { (track, participantThatMutedUs) => {
if (participantThatMutedUs) { if (participantThatMutedUs) {
dispatch(participantMutedUs(participantThatMutedUs)); dispatch(participantMutedUs(participantThatMutedUs, track));
} }
}); });

View File

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

View File

@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 16.0.3, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<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"
width="24px" height="24px" viewBox="0 0 24 24" enable-background="new 0 0 24 24" xml:space="preserve">
<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
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
l-9.135-9.134h5.045c0.459,0,0.84,0.344,0.84,0.803v2.866L17.584,10.769z"/>
<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
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
l-5.162-5.162h2.851c0.26,0,0.476,0.194,0.476,0.454v1.619L22.852,2.892z"/>
</svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 16.0.3, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<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"
width="24px" height="24px" viewBox="0 0 24 24" enable-background="new 0 0 24 24" xml:space="preserve">
<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
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
l-9.135-9.134h5.045c0.459,0,0.84,0.344,0.84,0.803v2.866L17.584,10.769z"/>
<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
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
l-5.162-5.162h2.851c0.26,0,0.476,0.194,0.476,0.454v1.619L22.852,2.892z"/>
</svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

@ -16,7 +16,9 @@ import {
PIN_PARTICIPANT, PIN_PARTICIPANT,
SET_LOADABLE_AVATAR_URL SET_LOADABLE_AVATAR_URL
} from './actionTypes'; } from './actionTypes';
import { DISCO_REMOTE_CONTROL_FEATURE } from './constants'; import {
DISCO_REMOTE_CONTROL_FEATURE
} from './constants';
import { import {
getLocalParticipant, getLocalParticipant,
getNormalizedDisplayName, getNormalizedDisplayName,
@ -192,15 +194,18 @@ export function localParticipantRoleChanged(role) {
* Create an action for muting another participant in the conference. * Create an action for muting another participant in the conference.
* *
* @param {string} id - Participant's ID. * @param {string} id - Participant's ID.
* @param {MEDIA_TYPE} mediaType - The media to mute.
* @returns {{ * @returns {{
* type: MUTE_REMOTE_PARTICIPANT, * type: MUTE_REMOTE_PARTICIPANT,
* id: string * id: string,
* mediaType: MEDIA_TYPE
* }} * }}
*/ */
export function muteRemoteParticipant(id) { export function muteRemoteParticipant(id, mediaType) {
return { return {
type: MUTE_REMOTE_PARTICIPANT, type: MUTE_REMOTE_PARTICIPANT,
id id,
mediaType
}; };
} }
@ -450,17 +455,20 @@ export function participantUpdated(participant = {}) {
* Action to signal that a participant has muted us. * Action to signal that a participant has muted us.
* *
* @param {JitsiParticipant} participant - Information about participant. * @param {JitsiParticipant} participant - Information about participant.
* @param {JitsiLocalTrack} track - Information about the track that has been muted.
* @returns {Promise} * @returns {Promise}
*/ */
export function participantMutedUs(participant) { export function participantMutedUs(participant, track) {
return (dispatch, getState) => { return (dispatch, getState) => {
if (!participant) { if (!participant) {
return; return;
} }
const isAudio = track.isAudioTrack();
dispatch(showNotification({ dispatch(showNotification({
descriptionKey: 'notify.mutedRemotelyDescription', descriptionKey: isAudio ? 'notify.mutedRemotelyDescription' : 'notify.videoMutedRemotelyDescription',
titleKey: 'notify.mutedRemotelyTitle', titleKey: isAudio ? 'notify.mutedRemotelyTitle' : 'notify.videoMutedRemotelyTitle',
titleArguments: { titleArguments: {
participantDisplayName: participantDisplayName:
getParticipantDisplayName(getState, participant.getId()) getParticipantDisplayName(getState, participant.getId())

View File

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

View File

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

View File

@ -6,10 +6,16 @@ import {
AUDIO_MUTE, AUDIO_MUTE,
createRemoteMuteConfirmedEvent, createRemoteMuteConfirmedEvent,
createToolbarEvent, createToolbarEvent,
sendAnalytics sendAnalytics,
VIDEO_MUTE
} from '../analytics'; } from '../analytics';
import { hideDialog } from '../base/dialog'; import { hideDialog } from '../base/dialog';
import { setAudioMuted } from '../base/media'; import {
MEDIA_TYPE,
setAudioMuted,
setVideoMuted,
VIDEO_MUTISM_AUTHORITY
} from '../base/media';
import { import {
getLocalParticipant, getLocalParticipant,
muteRemoteParticipant muteRemoteParticipant
@ -32,17 +38,26 @@ export function hideRemoteVideoMenu() {
* Mutes the local participant. * Mutes the local participant.
* *
* @param {boolean} enable - Whether to mute or unmute. * @param {boolean} enable - Whether to mute or unmute.
* @param {MEDIA_TYPE} mediaType - The type of the media channel to mute.
* @returns {Function} * @returns {Function}
*/ */
export function muteLocal(enable: boolean) { export function muteLocal(enable: boolean, mediaType: MEDIA_TYPE) {
return (dispatch: Dispatch<any>) => { return (dispatch: Dispatch<any>) => {
sendAnalytics(createToolbarEvent(AUDIO_MUTE, { enable })); const isAudio = mediaType === MEDIA_TYPE.AUDIO;
dispatch(setAudioMuted(enable, /* ensureTrack */ true));
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 // FIXME: The old conference logic as well as the shared video feature
// still rely on this event being emitted. // still rely on this event being emitted.
typeof APP === 'undefined' 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. * Mutes the remote participant with the given ID.
* *
* @param {string} participantId - ID of the participant to mute. * @param {string} participantId - ID of the participant to mute.
* @param {MEDIA_TYPE} mediaType - The type of the media channel to mute.
* @returns {Function} * @returns {Function}
*/ */
export function muteRemote(participantId: string) { export function muteRemote(participantId: string, mediaType: MEDIA_TYPE) {
return (dispatch: Dispatch<any>) => { return (dispatch: Dispatch<any>) => {
sendAnalytics(createRemoteMuteConfirmedEvent(participantId)); if (mediaType !== MEDIA_TYPE.AUDIO && mediaType !== MEDIA_TYPE.VIDEO) {
dispatch(muteRemoteParticipant(participantId)); 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. * Mutes all participants.
* *
* @param {Array<string>} exclude - Array of participant IDs to not mute. * @param {Array<string>} exclude - Array of participant IDs to not mute.
* @param {MEDIA_TYPE} mediaType - The media type to mute.
* @returns {Function} * @returns {Function}
*/ */
export function muteAllParticipants(exclude: Array<string>) { export function muteAllParticipants(exclude: Array<string>, mediaType: MEDIA_TYPE) {
return (dispatch: Dispatch<any>, getState: Function) => { return (dispatch: Dispatch<any>, getState: Function) => {
const state = getState(); const state = getState();
const localId = getLocalParticipant(state).id; const localId = getLocalParticipant(state).id;
@ -75,7 +97,7 @@ export function muteAllParticipants(exclude: Array<string>) {
/* eslint-disable no-confusing-arrow */ /* eslint-disable no-confusing-arrow */
participantIds participantIds
.filter(id => !exclude.includes(id)) .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); .map(dispatch);
/* eslint-enable no-confusing-arrow */ /* eslint-enable no-confusing-arrow */
}; };

View File

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

View File

@ -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<Props, *> {
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 ] }));
}
}

View File

@ -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<string>,
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<P: Props> extends AbstractMuteRemoteParticipantsVideoDialog<P> {
static defaultProps = {
exclude: [],
muteLocal: false
};
/**
* Implements React's {@link Component#render()}.
*
* @inheritdoc
* @returns {ReactElement}
*/
render() {
const { content, title } = this.props;
return (
<Dialog
okKey = 'dialog.muteParticipantsVideoButton'
onSubmit = { this._onSubmit }
titleString = { title }
width = 'small'>
<div>
{ content }
</div>
</Dialog>
);
}
_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')
};
}

View File

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

View File

@ -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<P:Props = Props>
extends Component<P> {
/**
* 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;
}
}

View File

@ -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<Props, *> {
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)
};
}

View File

@ -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 (
<RemoteVideoMenuButton
buttonText = { t('videothumbnail.domuteVideoOfOthers') }
displayClass = { 'mutelink' }
icon = { IconMuteVideoEveryoneElse }
id = { `mutelink_${participantID}` }
// eslint-disable-next-line react/jsx-handler-names
onClick = { this._handleClick } />
);
}
_handleClick: () => void;
}
export default translate(connect()(MuteEveryoneElsesVideoButton));

View File

@ -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<Props> {
/**
* Implements React's {@link Component#render()}.
*
* @inheritdoc
* @returns {ReactElement}
*/
render() {
return (
<Dialog
okKey = 'dialog.muteParticipantsVideoButton'
onSubmit = { this._onSubmit }
titleString = { this.props.title }
width = 'small'>
<div>
{ this.props.content }
</div>
</Dialog>
);
}
_onSubmit: () => boolean;
}
export default translate(connect(abstractMapStateToProps)(MuteEveryonesVideoDialog));

View File

@ -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 (
<Dialog
okKey = 'dialog.muteParticipantsVideoButton'
onSubmit = { this._onSubmit }
titleKey = 'dialog.muteParticipantsVideoTitle'
width = 'small'>
<div>
{ this.props.t('dialog.muteParticipantsVideoBody') }
</div>
</Dialog>
);
}
_onSubmit: () => boolean;
}
export default translate(connect()(MuteRemoteParticipantsVideoDialog));

View File

@ -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 (
<RemoteVideoMenuButton
buttonText = { t(muteConfig.translationKey) }
displayClass = { muteConfig.muteClassName }
icon = { IconCameraDisabled }
id = { `mutelink_${participantID}` }
// eslint-disable-next-line react/jsx-handler-names
onClick = { this._handleClick } />
);
}
_handleClick: () => void
}
export default translate(connect(_mapStateToProps)(MuteVideoButton));

View File

@ -3,20 +3,20 @@
import React, { Component } from 'react'; import React, { Component } from 'react';
import { Icon, IconMenuThumb } from '../../../base/icons'; import { Icon, IconMenuThumb } from '../../../base/icons';
import { MEDIA_TYPE } from '../../../base/media';
import { getLocalParticipant, getParticipantById, PARTICIPANT_ROLE } from '../../../base/participants'; import { getLocalParticipant, getParticipantById, PARTICIPANT_ROLE } from '../../../base/participants';
import { Popover } from '../../../base/popover'; import { Popover } from '../../../base/popover';
import { connect } from '../../../base/redux'; import { connect } from '../../../base/redux';
import { isRemoteTrackMuted } from '../../../base/tracks';
import { requestRemoteControl, stopController } from '../../../remote-control'; import { requestRemoteControl, stopController } from '../../../remote-control';
import { getCurrentLayout, LAYOUTS } from '../../../video-layout'; import { getCurrentLayout, LAYOUTS } from '../../../video-layout';
import MuteEveryoneElseButton from './MuteEveryoneElseButton'; import MuteEveryoneElseButton from './MuteEveryoneElseButton';
import MuteEveryoneElsesVideoButton from './MuteEveryoneElsesVideoButton';
import { REMOTE_CONTROL_MENU_STATES } from './RemoteControlButton'; import { REMOTE_CONTROL_MENU_STATES } from './RemoteControlButton';
import { import {
GrantModeratorButton, GrantModeratorButton,
MuteButton, MuteButton,
MuteVideoButton,
KickButton, KickButton,
PrivateMessageMenuButton, PrivateMessageMenuButton,
RemoteControlButton, RemoteControlButton,
@ -43,11 +43,6 @@ type Props = {
*/ */
_disableRemoteMute: Boolean, _disableRemoteMute: Boolean,
/**
* Whether or not the participant is currently muted.
*/
_isAudioMuted: boolean,
/** /**
* Whether or not the participant is a conference moderator. * Whether or not the participant is a conference moderator.
*/ */
@ -151,7 +146,6 @@ class RemoteVideoMenuTriggerButton extends Component<Props> {
const { const {
_disableKick, _disableKick,
_disableRemoteMute, _disableRemoteMute,
_isAudioMuted,
_isModerator, _isModerator,
dispatch, dispatch,
initialVolumeValue, initialVolumeValue,
@ -166,7 +160,6 @@ class RemoteVideoMenuTriggerButton extends Component<Props> {
if (!_disableRemoteMute) { if (!_disableRemoteMute) {
buttons.push( buttons.push(
<MuteButton <MuteButton
isAudioMuted = { _isAudioMuted }
key = 'mute' key = 'mute'
participantID = { participantID } /> participantID = { participantID } />
); );
@ -175,6 +168,16 @@ class RemoteVideoMenuTriggerButton extends Component<Props> {
key = 'mute-others' key = 'mute-others'
participantID = { participantID } /> participantID = { participantID } />
); );
buttons.push(
<MuteVideoButton
key = 'mute-video'
participantID = { participantID } />
);
buttons.push(
<MuteEveryoneElsesVideoButton
key = 'mute-others-video'
participantID = { participantID } />
);
} }
buttons.push( buttons.push(
@ -247,7 +250,6 @@ class RemoteVideoMenuTriggerButton extends Component<Props> {
*/ */
function _mapStateToProps(state, ownProps) { function _mapStateToProps(state, ownProps) {
const { participantID } = ownProps; const { participantID } = ownProps;
const tracks = state['features/base/tracks'];
const localParticipant = getLocalParticipant(state); const localParticipant = getLocalParticipant(state);
const { remoteVideoMenu = {}, disableRemoteMute } = state['features/base/config']; const { remoteVideoMenu = {}, disableRemoteMute } = state['features/base/config'];
const { disableKick } = remoteVideoMenu; const { disableKick } = remoteVideoMenu;
@ -286,7 +288,6 @@ function _mapStateToProps(state, ownProps) {
} }
return { return {
_isAudioMuted: isRemoteTrackMuted(tracks, MEDIA_TYPE.AUDIO, participantID) || false,
_isModerator: Boolean(localParticipant?.role === PARTICIPANT_ROLE.MODERATOR), _isModerator: Boolean(localParticipant?.role === PARTICIPANT_ROLE.MODERATOR),
_disableKick: Boolean(disableKick), _disableKick: Boolean(disableKick),
_disableRemoteMute: Boolean(disableRemoteMute), _disableRemoteMute: Boolean(disableRemoteMute),

View File

@ -5,9 +5,13 @@ export { default as GrantModeratorDialog } from './GrantModeratorDialog';
export { default as KickButton } from './KickButton'; export { default as KickButton } from './KickButton';
export { default as KickRemoteParticipantDialog } from './KickRemoteParticipantDialog'; export { default as KickRemoteParticipantDialog } from './KickRemoteParticipantDialog';
export { default as MuteButton } from './MuteButton'; export { default as MuteButton } from './MuteButton';
export { default as MuteVideoButton } from './MuteVideoButton';
export { default as MuteEveryoneDialog } from './MuteEveryoneDialog'; export { default as MuteEveryoneDialog } from './MuteEveryoneDialog';
export { default as MuteEveryonesVideoDialog } from './MuteEveryonesVideoDialog';
export { default as MuteEveryoneElseButton } from './MuteEveryoneElseButton'; export { default as MuteEveryoneElseButton } from './MuteEveryoneElseButton';
export { default as MuteEveryoneElsesVideoButton } from './MuteEveryoneElsesVideoButton';
export { default as MuteRemoteParticipantDialog } from './MuteRemoteParticipantDialog'; export { default as MuteRemoteParticipantDialog } from './MuteRemoteParticipantDialog';
export { default as MuteRemoteParticipantsVideoDialog } from './MuteRemoteParticipantsVideoDialog';
export { default as PrivateMessageMenuButton } from './PrivateMessageMenuButton'; export { default as PrivateMessageMenuButton } from './PrivateMessageMenuButton';
export { REMOTE_CONTROL_MENU_STATES, default as RemoteControlButton } from './RemoteControlButton'; export { REMOTE_CONTROL_MENU_STATES, default as RemoteControlButton } from './RemoteControlButton';
export { default as RemoteVideoMenu } from './RemoteVideoMenu'; export { default as RemoteVideoMenu } from './RemoteVideoMenu';

View File

@ -125,7 +125,7 @@ class AudioMuteButton extends AbstractAudioMuteButton<Props, *> {
* @returns {void} * @returns {void}
*/ */
_setAudioMuted(audioMuted: boolean) { _setAudioMuted(audioMuted: boolean) {
this.props.dispatch(muteLocal(audioMuted)); this.props.dispatch(muteLocal(audioMuted, MEDIA_TYPE.AUDIO));
} }
/** /**

View File

@ -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<Props, *> {
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));

View File

@ -82,6 +82,7 @@ import DownloadButton from '../DownloadButton';
import HangupButton from '../HangupButton'; import HangupButton from '../HangupButton';
import HelpButton from '../HelpButton'; import HelpButton from '../HelpButton';
import MuteEveryoneButton from '../MuteEveryoneButton'; import MuteEveryoneButton from '../MuteEveryoneButton';
import MuteEveryonesVideoButton from '../MuteEveryonesVideoButton';
import AudioSettingsButton from './AudioSettingsButton'; import AudioSettingsButton from './AudioSettingsButton';
import OverflowMenuButton from './OverflowMenuButton'; import OverflowMenuButton from './OverflowMenuButton';
@ -1079,6 +1080,10 @@ class Toolbox extends Component<Props, State> {
&& <MuteEveryoneButton && <MuteEveryoneButton
key = 'mute-everyone' key = 'mute-everyone'
showLabel = { true } />, showLabel = { true } />,
this._shouldShowButton('mute-video-everyone')
&& <MuteEveryonesVideoButton
key = 'mute-video-everyone'
showLabel = { true } />,
this._shouldShowButton('stats') this._shouldShowButton('stats')
&& <OverflowMenuItem && <OverflowMenuItem
accessibilityLabel = { t('toolbar.accessibilityLabel.speakerStats') } accessibilityLabel = { t('toolbar.accessibilityLabel.speakerStats') }