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:
parent
42d926eef3
commit
23bb824731
|
@ -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);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
|
|
|
@ -6,7 +6,6 @@
|
|||
min-width: 75px;
|
||||
text-align: left;
|
||||
padding: 0px;
|
||||
width: 180px;
|
||||
white-space: nowrap;
|
||||
|
||||
&__item {
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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));
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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));
|
||||
}
|
||||
});
|
||||
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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 |
|
@ -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 |
|
@ -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())
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
@ -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 }) => {
|
||||
|
|
|
@ -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<any>) => {
|
||||
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<any>) => {
|
||||
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<string>} exclude - Array of participant IDs to not mute.
|
||||
* @param {MEDIA_TYPE} mediaType - The media type to mute.
|
||||
* @returns {Function}
|
||||
*/
|
||||
export function muteAllParticipants(exclude: Array<string>) {
|
||||
export function muteAllParticipants(exclude: Array<string>, mediaType: MEDIA_TYPE) {
|
||||
return (dispatch: Dispatch<any>, getState: Function) => {
|
||||
const state = getState();
|
||||
const localId = getLocalParticipant(state).id;
|
||||
|
@ -75,7 +97,7 @@ export function muteAllParticipants(exclude: Array<string>) {
|
|||
/* 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 */
|
||||
};
|
||||
|
|
|
@ -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<P: Props> extends AbstractMuteRe
|
|||
exclude
|
||||
} = this.props;
|
||||
|
||||
dispatch(muteAllParticipants(exclude));
|
||||
dispatch(muteAllParticipants(exclude, MEDIA_TYPE.AUDIO));
|
||||
|
||||
return true;
|
||||
}
|
||||
|
|
|
@ -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 ] }));
|
||||
}
|
||||
}
|
|
@ -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')
|
||||
};
|
||||
}
|
|
@ -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<P:Props = Props>
|
|||
_onSubmit() {
|
||||
const { dispatch, participantID } = this.props;
|
||||
|
||||
dispatch(muteRemote(participantID));
|
||||
dispatch(muteRemote(participantID, MEDIA_TYPE.AUDIO));
|
||||
|
||||
return true;
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
};
|
||||
}
|
|
@ -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));
|
|
@ -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));
|
|
@ -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));
|
|
@ -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));
|
|
@ -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<Props> {
|
|||
const {
|
||||
_disableKick,
|
||||
_disableRemoteMute,
|
||||
_isAudioMuted,
|
||||
_isModerator,
|
||||
dispatch,
|
||||
initialVolumeValue,
|
||||
|
@ -166,7 +160,6 @@ class RemoteVideoMenuTriggerButton extends Component<Props> {
|
|||
if (!_disableRemoteMute) {
|
||||
buttons.push(
|
||||
<MuteButton
|
||||
isAudioMuted = { _isAudioMuted }
|
||||
key = 'mute'
|
||||
participantID = { participantID } />
|
||||
);
|
||||
|
@ -175,6 +168,16 @@ class RemoteVideoMenuTriggerButton extends Component<Props> {
|
|||
key = 'mute-others'
|
||||
participantID = { participantID } />
|
||||
);
|
||||
buttons.push(
|
||||
<MuteVideoButton
|
||||
key = 'mute-video'
|
||||
participantID = { participantID } />
|
||||
);
|
||||
buttons.push(
|
||||
<MuteEveryoneElsesVideoButton
|
||||
key = 'mute-others-video'
|
||||
participantID = { participantID } />
|
||||
);
|
||||
}
|
||||
|
||||
buttons.push(
|
||||
|
@ -247,7 +250,6 @@ class RemoteVideoMenuTriggerButton extends Component<Props> {
|
|||
*/
|
||||
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),
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -125,7 +125,7 @@ class AudioMuteButton extends AbstractAudioMuteButton<Props, *> {
|
|||
* @returns {void}
|
||||
*/
|
||||
_setAudioMuted(audioMuted: boolean) {
|
||||
this.props.dispatch(muteLocal(audioMuted));
|
||||
this.props.dispatch(muteLocal(audioMuted, MEDIA_TYPE.AUDIO));
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -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));
|
|
@ -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<Props, State> {
|
|||
&& <MuteEveryoneButton
|
||||
key = 'mute-everyone'
|
||||
showLabel = { true } />,
|
||||
this._shouldShowButton('mute-video-everyone')
|
||||
&& <MuteEveryonesVideoButton
|
||||
key = 'mute-video-everyone'
|
||||
showLabel = { true } />,
|
||||
this._shouldShowButton('stats')
|
||||
&& <OverflowMenuItem
|
||||
accessibilityLabel = { t('toolbar.accessibilityLabel.speakerStats') }
|
||||
|
|
Loading…
Reference in New Issue