feat(av-moderation) Updated Advanced moderation (#9875)
Co-authored-by: Vlad Piersec <vlad.piersec@8x8.com>
This commit is contained in:
parent
f2e2d52cfd
commit
1dc8bfa631
|
@ -29,7 +29,7 @@
|
||||||
margin: 8px 16px 8px 0;
|
margin: 8px 16px 8px 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 375px) {
|
@media (max-width: 580px) {
|
||||||
.participants_pane {
|
.participants_pane {
|
||||||
height: 100vh;
|
height: 100vh;
|
||||||
height: -webkit-fill-available;
|
height: -webkit-fill-available;
|
||||||
|
|
|
@ -98,6 +98,7 @@ $flagsImagePath: "../images/";
|
||||||
@import 'country-picker';
|
@import 'country-picker';
|
||||||
@import 'modals/invite/invite_more';
|
@import 'modals/invite/invite_more';
|
||||||
@import 'modals/security/security';
|
@import 'modals/security/security';
|
||||||
|
@import 'modals/mute/mute-dialog';
|
||||||
@import 'e2ee';
|
@import 'e2ee';
|
||||||
@import 'responsive';
|
@import 'responsive';
|
||||||
@import 'drawer';
|
@import 'drawer';
|
||||||
|
|
|
@ -0,0 +1,19 @@
|
||||||
|
.mute-dialog {
|
||||||
|
.separator-line {
|
||||||
|
margin: 24px 0 24px -20px;
|
||||||
|
padding: 0 20px;
|
||||||
|
width: 100%;
|
||||||
|
height: 1px;
|
||||||
|
background: #5E6D7A;
|
||||||
|
}
|
||||||
|
|
||||||
|
.control-row {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
margin-top: 15px;
|
||||||
|
|
||||||
|
label {
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -216,8 +216,8 @@
|
||||||
"embedMeeting": "Embed meeting",
|
"embedMeeting": "Embed meeting",
|
||||||
"error": "Error",
|
"error": "Error",
|
||||||
"gracefulShutdown": "Our service is currently down for maintenance. Please try again later.",
|
"gracefulShutdown": "Our service is currently down for maintenance. Please try again later.",
|
||||||
"grantModeratorDialog": "Are you sure you want to make this participant a moderator?",
|
"grantModeratorDialog": "Are you sure you want to grant moderator rights to {{participantName}}?",
|
||||||
"grantModeratorTitle": "Grant moderator",
|
"grantModeratorTitle": "Grant moderator rights",
|
||||||
"hideShareAudioHelper": "Don't show this dialog again",
|
"hideShareAudioHelper": "Don't show this dialog again",
|
||||||
"IamHost": "I am the host",
|
"IamHost": "I am the host",
|
||||||
"incorrectRoomLockPassword": "Incorrect password",
|
"incorrectRoomLockPassword": "Incorrect password",
|
||||||
|
@ -247,15 +247,19 @@
|
||||||
"micPermissionDeniedError": "You have not granted permission to use your microphone. You can still join the conference but others won't hear you. Use the camera button in the address bar to fix this.",
|
"micPermissionDeniedError": "You have not granted permission to use your microphone. You can still join the conference but others won't hear you. Use the camera button in the address bar to fix this.",
|
||||||
"micTimeoutError": "Could not start audio source. Timeout occured!",
|
"micTimeoutError": "Could not start audio source. Timeout occured!",
|
||||||
"micUnknownError": "Cannot use microphone for an unknown reason.",
|
"micUnknownError": "Cannot use microphone for an unknown reason.",
|
||||||
|
"moderationAudioLabel": "Allow attendees to unmute themselves",
|
||||||
|
"moderationVideoLabel": "Allow attendees to start their video",
|
||||||
"muteEveryoneElseDialog": "Once muted, you won't be able to unmute them, but they can unmute themselves at any time.",
|
"muteEveryoneElseDialog": "Once muted, you won't be able to unmute them, but they can unmute themselves at any time.",
|
||||||
"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": "The participants can unmute themselves at any time.",
|
||||||
|
"muteEveryoneDialogModerationOn": "The participants can send a request to speak 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.",
|
"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}}?",
|
"muteEveryoneElsesVideoTitle": "Stop everyone's video 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.",
|
"muteEveryonesVideoDialog": "The participants can turn on their video at any time.",
|
||||||
|
"muteEveryonesVideoDialogModerationOn": "The participants can send a request to turn on their video at any time.",
|
||||||
"muteEveryonesVideoDialogOk": "Disable",
|
"muteEveryonesVideoDialogOk": "Disable",
|
||||||
"muteEveryonesVideoTitle": "Disable everyone's camera?",
|
"muteEveryonesVideoTitle": "Stop everyone's video?",
|
||||||
"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.",
|
||||||
|
@ -263,7 +267,7 @@
|
||||||
"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.",
|
||||||
"muteParticipantsVideoDialog": "Are you sure you want to turn off this participant's camera? You won't be able to turn the camera back on, but they can turn it back on at any time.",
|
"muteParticipantsVideoDialog": "Are you sure you want to turn off this participant's camera? You won't be able to turn the camera back on, but they can turn it back on at any time.",
|
||||||
"muteParticipantTitle": "Mute this participant?",
|
"muteParticipantTitle": "Mute this participant?",
|
||||||
"muteParticipantsVideoButton": "Disable camera",
|
"muteParticipantsVideoButton": "Stop camera",
|
||||||
"muteParticipantsVideoTitle": "Disable camera of this participant?",
|
"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.",
|
"muteParticipantsVideoBody": "You won't be able to turn the camera back on, but they can turn it back on at any time.",
|
||||||
"noDropboxToken": "No valid Dropbox token",
|
"noDropboxToken": "No valid Dropbox token",
|
||||||
|
@ -542,29 +546,30 @@
|
||||||
"lockRoomPasswordUppercase": "Password",
|
"lockRoomPasswordUppercase": "Password",
|
||||||
"me": "me",
|
"me": "me",
|
||||||
"notify": {
|
"notify": {
|
||||||
|
"allowAction": "Allow",
|
||||||
|
"allowedUnmute": "You can unmute your microphone, start your camera or share your screen.",
|
||||||
"connectedOneMember": "{{name}} joined the meeting",
|
"connectedOneMember": "{{name}} joined the meeting",
|
||||||
"connectedThreePlusMembers": "{{name}} and many others joined the meeting",
|
"connectedThreePlusMembers": "{{name}} and many others joined the meeting",
|
||||||
"connectedTwoMembers": "{{first}} and {{second}} joined the meeting",
|
"connectedTwoMembers": "{{first}} and {{second}} joined the meeting",
|
||||||
"disconnected": "disconnected",
|
"disconnected": "disconnected",
|
||||||
"focus": "Conference focus",
|
"focus": "Conference focus",
|
||||||
"focusFail": "{{component}} not available - retry in {{ms}} sec",
|
"focusFail": "{{component}} not available - retry in {{ms}} sec",
|
||||||
"grantedTo": "Moderator rights granted to {{to}}!",
|
"hostAskedUnmute": "The moderator would like you to speak",
|
||||||
"hostAskedUnmute": "The host would like you to unmute",
|
|
||||||
"invitedOneMember": "{{name}} has been invited",
|
"invitedOneMember": "{{name}} has been invited",
|
||||||
"invitedThreePlusMembers": "{{name}} and {{count}} others have been invited",
|
"invitedThreePlusMembers": "{{name}} and {{count}} others have been invited",
|
||||||
"invitedTwoMembers": "{{first}} and {{second}} have been invited",
|
"invitedTwoMembers": "{{first}} and {{second}} have been invited",
|
||||||
"kickParticipant": "{{kicked}} was kicked by {{kicker}}",
|
"kickParticipant": "{{kicked}} was kicked by {{kicker}}",
|
||||||
"me": "Me",
|
"me": "Me",
|
||||||
"moderator": "Moderator rights granted!",
|
"moderator": "You're now a moderator",
|
||||||
"muted": "You have started the conversation muted.",
|
"muted": "You have started the conversation muted.",
|
||||||
"mutedTitle": "You're muted!",
|
"mutedTitle": "You're muted!",
|
||||||
"mutedRemotelyTitle": "You have been muted by {{participantDisplayName}}!",
|
"mutedRemotelyTitle": "You've been muted by the moderator",
|
||||||
"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}}!",
|
"videoMutedRemotelyTitle": "Your camera has been turned off by the moderator",
|
||||||
"videoMutedRemotelyDescription": "You can always turn it on again.",
|
"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": "Would like to speak.",
|
||||||
"screenShareNoAudio": " Share audio box was not checked in the window selection screen.",
|
"screenShareNoAudio": " Share audio box was not checked in the window selection screen.",
|
||||||
"screenShareNoAudioTitle": "Couldn't share system audio!",
|
"screenShareNoAudioTitle": "Couldn't share system audio!",
|
||||||
"somebody": "Somebody",
|
"somebody": "Somebody",
|
||||||
|
@ -580,12 +585,12 @@
|
||||||
"oldElectronClientDescription1": "You appear to be using an old version of the Jitsi Meet client which has known security vulnerabilities. Please make sure you update to our ",
|
"oldElectronClientDescription1": "You appear to be using an old version of the Jitsi Meet client which has known security vulnerabilities. Please make sure you update to our ",
|
||||||
"oldElectronClientDescription2": "latest build",
|
"oldElectronClientDescription2": "latest build",
|
||||||
"oldElectronClientDescription3": " now!",
|
"oldElectronClientDescription3": " now!",
|
||||||
"moderationInEffectDescription": "Please raise hand if you want to speak",
|
"moderationInEffectDescription": "Please raise hand if you want to speak.",
|
||||||
"moderationInEffectCSDescription": "Please raise hand if you want to share your video",
|
"moderationInEffectCSDescription": "Please raise hand if you want to share your screen.",
|
||||||
"moderationInEffectVideoDescription": "Please raise your hand if you want your video to be visible",
|
"moderationInEffectVideoDescription": "Please raise your hand if you want to start your camera.",
|
||||||
"moderationInEffectTitle": "The microphone is muted by the moderator",
|
"moderationInEffectTitle": "Your microphone is muted by the moderator",
|
||||||
"moderationInEffectCSTitle": "Content sharing is disabled by moderator",
|
"moderationInEffectCSTitle": "Screen sharing is blocked by the moderator",
|
||||||
"moderationInEffectVideoTitle": "The video is muted by the moderator",
|
"moderationInEffectVideoTitle": "Your camera is blocked by the moderator",
|
||||||
"moderationRequestFromModerator": "The host would like you to unmute",
|
"moderationRequestFromModerator": "The host would like you to unmute",
|
||||||
"moderationRequestFromParticipant": "Wants to speak",
|
"moderationRequestFromParticipant": "Wants to speak",
|
||||||
"moderationStartedTitle": "Moderation started",
|
"moderationStartedTitle": "Moderation started",
|
||||||
|
@ -605,16 +610,17 @@
|
||||||
},
|
},
|
||||||
"actions": {
|
"actions": {
|
||||||
"allow": "Allow attendees to:",
|
"allow": "Allow attendees to:",
|
||||||
|
"audioModeration": "Unmute themselves",
|
||||||
"blockEveryoneMicCamera": "Block everyone's mic and camera",
|
"blockEveryoneMicCamera": "Block everyone's mic and camera",
|
||||||
"invite": "Invite Someone",
|
"invite": "Invite Someone",
|
||||||
"askUnmute": "Ask to unmute",
|
"askUnmute": "Ask to unmute",
|
||||||
"mute": "Mute",
|
"mute": "Mute",
|
||||||
"muteAll": "Mute all",
|
"muteAll": "Mute all",
|
||||||
"muteEveryoneElse": "Mute everyone else",
|
"muteEveryoneElse": "Mute everyone else",
|
||||||
"startModeration": "Unmute themselves or start video",
|
|
||||||
"stopEveryonesVideo": "Stop everyone's video",
|
"stopEveryonesVideo": "Stop everyone's video",
|
||||||
"stopVideo": "Stop video",
|
"stopVideo": "Stop video",
|
||||||
"unblockEveryoneMicCamera": "Unblock everyone's mic and camera"
|
"unblockEveryoneMicCamera": "Unblock everyone's mic and camera",
|
||||||
|
"videoModeration": "Start video"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"passwordSetRemotely": "Set by another participant",
|
"passwordSetRemotely": "Set by another participant",
|
||||||
|
@ -868,7 +874,7 @@
|
||||||
"embedMeeting": "Embed meeting",
|
"embedMeeting": "Embed meeting",
|
||||||
"feedback": "Leave feedback",
|
"feedback": "Leave feedback",
|
||||||
"fullScreen": "Toggle full screen",
|
"fullScreen": "Toggle full screen",
|
||||||
"grantModerator": "Grant Moderator",
|
"grantModerator": "Grant Moderator Rights",
|
||||||
"hangup": "Leave the meeting",
|
"hangup": "Leave the meeting",
|
||||||
"help": "Help",
|
"help": "Help",
|
||||||
"invite": "Invite people",
|
"invite": "Invite people",
|
||||||
|
@ -1054,7 +1060,7 @@
|
||||||
"domuteOthers": "Mute everyone else",
|
"domuteOthers": "Mute everyone else",
|
||||||
"domuteVideoOfOthers": "Disable camera of everyone else",
|
"domuteVideoOfOthers": "Disable camera of everyone else",
|
||||||
"flip": "Flip",
|
"flip": "Flip",
|
||||||
"grantModerator": "Grant Moderator",
|
"grantModerator": "Grant Moderator Rights",
|
||||||
"kick": "Kick out",
|
"kick": "Kick out",
|
||||||
"moderator": "Moderator",
|
"moderator": "Moderator",
|
||||||
"mute": "Participant is muted",
|
"mute": "Participant is muted",
|
||||||
|
|
|
@ -47,6 +47,7 @@
|
||||||
"base64-js": "1.3.1",
|
"base64-js": "1.3.1",
|
||||||
"bc-css-flags": "3.0.0",
|
"bc-css-flags": "3.0.0",
|
||||||
"clipboard-copy": "4.0.1",
|
"clipboard-copy": "4.0.1",
|
||||||
|
"clsx": "1.1.1",
|
||||||
"dropbox": "10.7.0",
|
"dropbox": "10.7.0",
|
||||||
"focus-visible": "5.1.0",
|
"focus-visible": "5.1.0",
|
||||||
"i18n-iso-countries": "6.8.0",
|
"i18n-iso-countries": "6.8.0",
|
||||||
|
|
|
@ -29,22 +29,40 @@ export const ENABLE_MODERATION = 'ENABLE_MODERATION';
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The type of (redux) action which signals that A/V Moderation disable has been requested.
|
* The type of (redux) action which signals that Audio Moderation disable has been requested.
|
||||||
*
|
*
|
||||||
* {
|
* {
|
||||||
* type: REQUEST_DISABLE_MODERATION
|
* type: REQUEST_DISABLE_AUDIO_MODERATION
|
||||||
* }
|
* }
|
||||||
*/
|
*/
|
||||||
export const REQUEST_DISABLE_MODERATION = 'REQUEST_DISABLE_MODERATION';
|
export const REQUEST_DISABLE_AUDIO_MODERATION = 'REQUEST_DISABLE_AUDIO_MODERATION';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The type of (redux) action which signals that A/V Moderation enable has been requested.
|
* The type of (redux) action which signals that Video Moderation disable has been requested.
|
||||||
*
|
*
|
||||||
* {
|
* {
|
||||||
* type: REQUEST_ENABLE_MODERATION
|
* type: REQUEST_DISABLE_VIDEO_MODERATION
|
||||||
* }
|
* }
|
||||||
*/
|
*/
|
||||||
export const REQUEST_ENABLE_MODERATION = 'REQUEST_ENABLE_MODERATION';
|
export const REQUEST_DISABLE_VIDEO_MODERATION = 'REQUEST_DISABLE_VIDEO_MODERATION';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The type of (redux) action which signals that Audio Moderation enable has been requested.
|
||||||
|
*
|
||||||
|
* {
|
||||||
|
* type: REQUEST_ENABLE_AUDIO_MODERATION
|
||||||
|
* }
|
||||||
|
*/
|
||||||
|
export const REQUEST_ENABLE_AUDIO_MODERATION = 'REQUEST_ENABLE_AUDIO_MODERATION';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The type of (redux) action which signals that Video Moderation enable has been requested.
|
||||||
|
*
|
||||||
|
* {
|
||||||
|
* type: REQUEST_ENABLE_VIDEO_MODERATION
|
||||||
|
* }
|
||||||
|
*/
|
||||||
|
export const REQUEST_ENABLE_VIDEO_MODERATION = 'REQUEST_ENABLE_VIDEO_MODERATION';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The type of (redux) action which signals that the local participant had been approved.
|
* The type of (redux) action which signals that the local participant had been approved.
|
||||||
|
|
|
@ -11,9 +11,12 @@ import {
|
||||||
LOCAL_PARTICIPANT_MODERATION_NOTIFICATION,
|
LOCAL_PARTICIPANT_MODERATION_NOTIFICATION,
|
||||||
PARTICIPANT_APPROVED,
|
PARTICIPANT_APPROVED,
|
||||||
PARTICIPANT_PENDING_AUDIO,
|
PARTICIPANT_PENDING_AUDIO,
|
||||||
REQUEST_DISABLE_MODERATION,
|
REQUEST_DISABLE_AUDIO_MODERATION,
|
||||||
REQUEST_ENABLE_MODERATION
|
REQUEST_ENABLE_AUDIO_MODERATION,
|
||||||
|
REQUEST_DISABLE_VIDEO_MODERATION,
|
||||||
|
REQUEST_ENABLE_VIDEO_MODERATION
|
||||||
} from './actionTypes';
|
} from './actionTypes';
|
||||||
|
import { isEnabledFromState } from './functions';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Action used by moderator to approve audio and video for a participant.
|
* Action used by moderator to approve audio and video for a participant.
|
||||||
|
@ -22,10 +25,15 @@ import {
|
||||||
* @returns {void}
|
* @returns {void}
|
||||||
*/
|
*/
|
||||||
export const approveParticipant = (id: string) => (dispatch: Function, getState: Function) => {
|
export const approveParticipant = (id: string) => (dispatch: Function, getState: Function) => {
|
||||||
const { conference } = getConferenceState(getState());
|
const state = getState();
|
||||||
|
const { conference } = getConferenceState(state);
|
||||||
|
|
||||||
conference.avModerationApprove(MEDIA_TYPE.AUDIO, id);
|
if (isEnabledFromState(MEDIA_TYPE.AUDIO, state)) {
|
||||||
conference.avModerationApprove(MEDIA_TYPE.VIDEO, id);
|
conference.avModerationApprove(MEDIA_TYPE.AUDIO, id);
|
||||||
|
}
|
||||||
|
if (isEnabledFromState(MEDIA_TYPE.VIDEO, state)) {
|
||||||
|
conference.avModerationApprove(MEDIA_TYPE.VIDEO, id);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -89,28 +97,54 @@ export const enableModeration = (mediaType: MediaType, actor: Object) => {
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Requests disable of audio and video moderation.
|
* Requests disable of audio moderation.
|
||||||
*
|
*
|
||||||
* @returns {{
|
* @returns {{
|
||||||
* type: REQUEST_DISABLE_MODERATED_AUDIO
|
* type: REQUEST_DISABLE_AUDIO_MODERATION
|
||||||
* }}
|
* }}
|
||||||
*/
|
*/
|
||||||
export const requestDisableModeration = () => {
|
export const requestDisableAudioModeration = () => {
|
||||||
return {
|
return {
|
||||||
type: REQUEST_DISABLE_MODERATION
|
type: REQUEST_DISABLE_AUDIO_MODERATION
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Requests enabled audio & video moderation.
|
* Requests disable of video moderation.
|
||||||
*
|
*
|
||||||
* @returns {{
|
* @returns {{
|
||||||
* type: REQUEST_ENABLE_MODERATED_AUDIO
|
* type: REQUEST_DISABLE_VIDEO_MODERATION
|
||||||
* }}
|
* }}
|
||||||
*/
|
*/
|
||||||
export const requestEnableModeration = () => {
|
export const requestDisableVideoModeration = () => {
|
||||||
return {
|
return {
|
||||||
type: REQUEST_ENABLE_MODERATION
|
type: REQUEST_DISABLE_VIDEO_MODERATION
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Requests enable of audio moderation.
|
||||||
|
*
|
||||||
|
* @returns {{
|
||||||
|
* type: REQUEST_ENABLE_AUDIO_MODERATION
|
||||||
|
* }}
|
||||||
|
*/
|
||||||
|
export const requestEnableAudioModeration = () => {
|
||||||
|
return {
|
||||||
|
type: REQUEST_ENABLE_AUDIO_MODERATION
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Requests enable of video moderation.
|
||||||
|
*
|
||||||
|
* @returns {{
|
||||||
|
* type: REQUEST_ENABLE_VIDEO_MODERATION
|
||||||
|
* }}
|
||||||
|
*/
|
||||||
|
export const requestEnableVideoModeration = () => {
|
||||||
|
return {
|
||||||
|
type: REQUEST_ENABLE_VIDEO_MODERATION
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -1,38 +0,0 @@
|
||||||
import React from 'react';
|
|
||||||
import { useTranslation } from 'react-i18next';
|
|
||||||
import { useSelector } from 'react-redux';
|
|
||||||
|
|
||||||
import NotificationWithParticipants from '../../notifications/components/web/NotificationWithParticipants';
|
|
||||||
import {
|
|
||||||
approveParticipant,
|
|
||||||
dismissPendingAudioParticipant
|
|
||||||
} from '../actions';
|
|
||||||
import { getParticipantsAskingToAudioUnmute } from '../functions';
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Component used to display a list of participants who asked to be unmuted.
|
|
||||||
* This is visible only to moderators.
|
|
||||||
*
|
|
||||||
* @returns {React$Element<'ul'> | null}
|
|
||||||
*/
|
|
||||||
export default function() {
|
|
||||||
const participants = useSelector(getParticipantsAskingToAudioUnmute);
|
|
||||||
const { t } = useTranslation();
|
|
||||||
|
|
||||||
return participants.length
|
|
||||||
? (
|
|
||||||
<>
|
|
||||||
<div className = 'title'>
|
|
||||||
{ t('raisedHand') }
|
|
||||||
</div>
|
|
||||||
<NotificationWithParticipants
|
|
||||||
approveButtonText = { t('notify.unmute') }
|
|
||||||
onApprove = { approveParticipant }
|
|
||||||
onReject = { dismissPendingAudioParticipant }
|
|
||||||
participants = { participants }
|
|
||||||
rejectButtonText = { t('dialog.dismiss') }
|
|
||||||
testIdPrefix = 'avModeration' />
|
|
||||||
</>
|
|
||||||
) : null;
|
|
||||||
}
|
|
|
@ -6,7 +6,6 @@ import { JitsiConferenceEvents } from '../base/lib-jitsi-meet';
|
||||||
import { MEDIA_TYPE } from '../base/media';
|
import { MEDIA_TYPE } from '../base/media';
|
||||||
import {
|
import {
|
||||||
getLocalParticipant,
|
getLocalParticipant,
|
||||||
getParticipantDisplayName,
|
|
||||||
getRemoteParticipants,
|
getRemoteParticipants,
|
||||||
isLocalParticipantModerator,
|
isLocalParticipantModerator,
|
||||||
isParticipantModerator,
|
isParticipantModerator,
|
||||||
|
@ -16,16 +15,16 @@ import {
|
||||||
import { MiddlewareRegistry, StateListenerRegistry } from '../base/redux';
|
import { MiddlewareRegistry, StateListenerRegistry } from '../base/redux';
|
||||||
import {
|
import {
|
||||||
hideNotification,
|
hideNotification,
|
||||||
NOTIFICATION_TIMEOUT,
|
|
||||||
showNotification
|
showNotification
|
||||||
} from '../notifications';
|
} from '../notifications';
|
||||||
|
import { muteLocal } from '../video-menu/actions.any';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
DISABLE_MODERATION,
|
|
||||||
ENABLE_MODERATION,
|
|
||||||
LOCAL_PARTICIPANT_MODERATION_NOTIFICATION,
|
LOCAL_PARTICIPANT_MODERATION_NOTIFICATION,
|
||||||
REQUEST_DISABLE_MODERATION,
|
REQUEST_DISABLE_AUDIO_MODERATION,
|
||||||
REQUEST_ENABLE_MODERATION
|
REQUEST_DISABLE_VIDEO_MODERATION,
|
||||||
|
REQUEST_ENABLE_AUDIO_MODERATION,
|
||||||
|
REQUEST_ENABLE_VIDEO_MODERATION
|
||||||
} from './actionTypes';
|
} from './actionTypes';
|
||||||
import {
|
import {
|
||||||
disableModeration,
|
disableModeration,
|
||||||
|
@ -47,29 +46,10 @@ const AUDIO_MODERATION_NOTIFICATION_ID = 'audio-moderation';
|
||||||
const CS_MODERATION_NOTIFICATION_ID = 'video-moderation';
|
const CS_MODERATION_NOTIFICATION_ID = 'video-moderation';
|
||||||
|
|
||||||
MiddlewareRegistry.register(({ dispatch, getState }) => next => action => {
|
MiddlewareRegistry.register(({ dispatch, getState }) => next => action => {
|
||||||
const { actor, mediaType, type } = action;
|
const { type } = action;
|
||||||
|
const { conference } = getConferenceState(getState());
|
||||||
|
|
||||||
switch (type) {
|
switch (type) {
|
||||||
case DISABLE_MODERATION:
|
|
||||||
case ENABLE_MODERATION: {
|
|
||||||
// Audio & video moderation are both enabled at the same time.
|
|
||||||
// Avoid displaying 2 different notifications.
|
|
||||||
if (mediaType === MEDIA_TYPE.VIDEO) {
|
|
||||||
const titleKey = type === ENABLE_MODERATION
|
|
||||||
? 'notify.moderationStartedTitle'
|
|
||||||
: 'notify.moderationStoppedTitle';
|
|
||||||
|
|
||||||
dispatch(showNotification({
|
|
||||||
descriptionKey: actor ? 'notify.moderationToggleDescription' : undefined,
|
|
||||||
descriptionArguments: actor ? {
|
|
||||||
participantDisplayName: getParticipantDisplayName(getState, actor.getId())
|
|
||||||
} : undefined,
|
|
||||||
titleKey
|
|
||||||
}, NOTIFICATION_TIMEOUT));
|
|
||||||
}
|
|
||||||
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case LOCAL_PARTICIPANT_MODERATION_NOTIFICATION: {
|
case LOCAL_PARTICIPANT_MODERATION_NOTIFICATION: {
|
||||||
let descriptionKey;
|
let descriptionKey;
|
||||||
let titleKey;
|
let titleKey;
|
||||||
|
@ -78,19 +58,16 @@ MiddlewareRegistry.register(({ dispatch, getState }) => next => action => {
|
||||||
switch (action.mediaType) {
|
switch (action.mediaType) {
|
||||||
case MEDIA_TYPE.AUDIO: {
|
case MEDIA_TYPE.AUDIO: {
|
||||||
titleKey = 'notify.moderationInEffectTitle';
|
titleKey = 'notify.moderationInEffectTitle';
|
||||||
descriptionKey = 'notify.moderationInEffectDescription';
|
|
||||||
uid = AUDIO_MODERATION_NOTIFICATION_ID;
|
uid = AUDIO_MODERATION_NOTIFICATION_ID;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case MEDIA_TYPE.VIDEO: {
|
case MEDIA_TYPE.VIDEO: {
|
||||||
titleKey = 'notify.moderationInEffectVideoTitle';
|
titleKey = 'notify.moderationInEffectVideoTitle';
|
||||||
descriptionKey = 'notify.moderationInEffectVideoDescription';
|
|
||||||
uid = VIDEO_MODERATION_NOTIFICATION_ID;
|
uid = VIDEO_MODERATION_NOTIFICATION_ID;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case MEDIA_TYPE.PRESENTER: {
|
case MEDIA_TYPE.PRESENTER: {
|
||||||
titleKey = 'notify.moderationInEffectCSTitle';
|
titleKey = 'notify.moderationInEffectCSTitle';
|
||||||
descriptionKey = 'notify.moderationInEffectCSDescription';
|
|
||||||
uid = CS_MODERATION_NOTIFICATION_ID;
|
uid = CS_MODERATION_NOTIFICATION_ID;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
@ -110,17 +87,19 @@ MiddlewareRegistry.register(({ dispatch, getState }) => next => action => {
|
||||||
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case REQUEST_DISABLE_MODERATION: {
|
case REQUEST_DISABLE_AUDIO_MODERATION: {
|
||||||
const { conference } = getConferenceState(getState());
|
|
||||||
|
|
||||||
conference.disableAVModeration(MEDIA_TYPE.AUDIO);
|
conference.disableAVModeration(MEDIA_TYPE.AUDIO);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case REQUEST_DISABLE_VIDEO_MODERATION: {
|
||||||
conference.disableAVModeration(MEDIA_TYPE.VIDEO);
|
conference.disableAVModeration(MEDIA_TYPE.VIDEO);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case REQUEST_ENABLE_MODERATION: {
|
case REQUEST_ENABLE_AUDIO_MODERATION: {
|
||||||
const { conference } = getConferenceState(getState());
|
|
||||||
|
|
||||||
conference.enableAVModeration(MEDIA_TYPE.AUDIO);
|
conference.enableAVModeration(MEDIA_TYPE.AUDIO);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case REQUEST_ENABLE_VIDEO_MODERATION: {
|
||||||
conference.enableAVModeration(MEDIA_TYPE.VIDEO);
|
conference.enableAVModeration(MEDIA_TYPE.VIDEO);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
@ -174,11 +153,12 @@ StateListenerRegistry.register(
|
||||||
|
|
||||||
// Audio & video moderation are both enabled at the same time.
|
// Audio & video moderation are both enabled at the same time.
|
||||||
// Avoid displaying 2 different notifications.
|
// Avoid displaying 2 different notifications.
|
||||||
if (mediaType === MEDIA_TYPE.VIDEO) {
|
if (mediaType === MEDIA_TYPE.AUDIO) {
|
||||||
dispatch(showNotification({
|
dispatch(showNotification({
|
||||||
titleKey: 'notify.unmute',
|
titleKey: 'notify.hostAskedUnmute',
|
||||||
descriptionKey: 'notify.hostAskedUnmute',
|
sticky: true,
|
||||||
sticky: true
|
customActionNameKey: 'notify.unmute',
|
||||||
|
customActionHandler: () => dispatch(muteLocal(false, MEDIA_TYPE.AUDIO))
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
|
@ -1,3 +1,3 @@
|
||||||
<svg width="22" height="22" viewBox="0 0 22 22" fill="none" xmlns="http://www.w3.org/2000/svg">
|
<svg width="22" height="22" viewBox="0 0 22 22" xmlns="http://www.w3.org/2000/svg">
|
||||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M6.84074 5.49992H6.81762L3.42398 2.10629C3.06002 1.74232 2.47001 1.74223 2.10617 2.10608C1.74232 2.46992 1.74241 3.05993 2.10638 3.42389L4.1824 5.49992H3.66668C2.65415 5.49992 1.83334 6.32073 1.83334 7.33325V14.6666C1.83334 15.6791 2.65415 16.4999 3.66668 16.4999H13.75C14.154 16.4999 14.5274 16.3693 14.8304 16.1479L18.576 19.8936C18.94 20.2575 19.53 20.2576 19.8939 19.8938C20.2577 19.5299 20.2576 18.9399 19.8936 18.5759L15.5833 14.2656V14.2425L13.75 12.4092V12.4323L8.65095 7.33325H8.67407L6.84074 5.49992ZM13.75 9.77398V9.16659V7.33325H11.3093L9.47595 5.49992H13.75C14.7625 5.49992 15.5833 6.32073 15.5833 7.33325V8.11897L18.7952 6.28361C19.2348 6.03243 19.7947 6.18515 20.0459 6.62471C20.125 6.76321 20.1667 6.91998 20.1667 7.0795V14.9203C20.1667 15.2643 19.9772 15.5641 19.6969 15.7209L15.9614 11.9853L18.3333 13.3408V8.65908L15.5833 10.2305V11.6073L13.75 9.77398ZM3.66668 7.33325H6.01574L13.3491 14.6666H3.66668V7.33325Z" />
|
<path fill-rule="evenodd" clip-rule="evenodd" d="M6.84074 5.49992H6.81762L3.42398 2.10629C3.06002 1.74232 2.47001 1.74223 2.10617 2.10608C1.74232 2.46992 1.74241 3.05993 2.10638 3.42389L4.1824 5.49992H3.66668C2.65415 5.49992 1.83334 6.32073 1.83334 7.33325V14.6666C1.83334 15.6791 2.65415 16.4999 3.66668 16.4999H13.75C14.154 16.4999 14.5274 16.3693 14.8304 16.1479L18.576 19.8936C18.94 20.2575 19.53 20.2576 19.8939 19.8938C20.2577 19.5299 20.2576 18.9399 19.8936 18.5759L15.5833 14.2656V14.2425L13.75 12.4092V12.4323L8.65095 7.33325H8.67407L6.84074 5.49992ZM13.75 9.77398V9.16659V7.33325H11.3093L9.47595 5.49992H13.75C14.7625 5.49992 15.5833 6.32073 15.5833 7.33325V8.11897L18.7952 6.28361C19.2348 6.03243 19.7947 6.18515 20.0459 6.62471C20.125 6.76321 20.1667 6.91998 20.1667 7.0795V14.9203C20.1667 15.2643 19.9772 15.5641 19.6969 15.7209L15.9614 11.9853L18.3333 13.3408V8.65908L15.5833 10.2305V11.6073L13.75 9.77398ZM3.66668 7.33325H6.01574L13.3491 14.6666H3.66668V7.33325Z" />
|
||||||
</svg>
|
</svg>
|
||||||
|
|
Before Width: | Height: | Size: 1.1 KiB After Width: | Height: | Size: 1.0 KiB |
|
@ -8,12 +8,15 @@ import {
|
||||||
sendAnalytics
|
sendAnalytics
|
||||||
} from '../../analytics';
|
} from '../../analytics';
|
||||||
import { APP_STATE_CHANGED } from '../../mobile/background';
|
import { APP_STATE_CHANGED } from '../../mobile/background';
|
||||||
|
import { isForceMuted } from '../../participants-pane/functions';
|
||||||
import { SET_AUDIO_ONLY, setAudioOnly } from '../audio-only';
|
import { SET_AUDIO_ONLY, setAudioOnly } from '../audio-only';
|
||||||
import { isRoomValid, SET_ROOM } from '../conference';
|
import { isRoomValid, SET_ROOM } from '../conference';
|
||||||
|
import { getLocalParticipant } from '../participants';
|
||||||
import { MiddlewareRegistry } from '../redux';
|
import { MiddlewareRegistry } from '../redux';
|
||||||
import { getPropertyValue } from '../settings';
|
import { getPropertyValue } from '../settings';
|
||||||
import { isLocalVideoTrackDesktop, setTrackMuted, TRACK_ADDED } from '../tracks';
|
import { isLocalVideoTrackDesktop, setTrackMuted, TRACK_ADDED } from '../tracks';
|
||||||
|
|
||||||
|
import { SET_AUDIO_MUTED, SET_VIDEO_MUTED } from './actionTypes';
|
||||||
import { setAudioMuted, setCameraFacingMode, setVideoMuted } from './actions';
|
import { setAudioMuted, setCameraFacingMode, setVideoMuted } from './actions';
|
||||||
import {
|
import {
|
||||||
CAMERA_FACING_MODE,
|
CAMERA_FACING_MODE,
|
||||||
|
@ -55,6 +58,26 @@ MiddlewareRegistry.register(store => next => action => {
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
case SET_AUDIO_MUTED: {
|
||||||
|
const state = store.getState();
|
||||||
|
const participant = getLocalParticipant(state);
|
||||||
|
|
||||||
|
if (!action.muted && isForceMuted(participant, MEDIA_TYPE.AUDIO, state)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
case SET_VIDEO_MUTED: {
|
||||||
|
const state = store.getState();
|
||||||
|
const participant = getLocalParticipant(state);
|
||||||
|
|
||||||
|
if (!action.muted && isForceMuted(participant, MEDIA_TYPE.VIDEO, state)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return next(action);
|
return next(action);
|
||||||
|
|
|
@ -180,3 +180,15 @@ export const SET_LOADABLE_AVATAR_URL = 'SET_LOADABLE_AVATAR_URL';
|
||||||
* }
|
* }
|
||||||
*/
|
*/
|
||||||
export const LOCAL_PARTICIPANT_RAISE_HAND = 'LOCAL_PARTICIPANT_RAISE_HAND';
|
export const LOCAL_PARTICIPANT_RAISE_HAND = 'LOCAL_PARTICIPANT_RAISE_HAND';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Updates participant in raise hand queue.
|
||||||
|
* {
|
||||||
|
* type: RAISE_HAND_UPDATED,
|
||||||
|
* participant: {
|
||||||
|
* id: string,
|
||||||
|
* raiseHand: boolean
|
||||||
|
* }
|
||||||
|
* }
|
||||||
|
*/
|
||||||
|
export const RAISE_HAND_UPDATED = 'RAISE_HAND_UPDATED';
|
||||||
|
|
|
@ -15,7 +15,8 @@ import {
|
||||||
PARTICIPANT_LEFT,
|
PARTICIPANT_LEFT,
|
||||||
PARTICIPANT_UPDATED,
|
PARTICIPANT_UPDATED,
|
||||||
PIN_PARTICIPANT,
|
PIN_PARTICIPANT,
|
||||||
SET_LOADABLE_AVATAR_URL
|
SET_LOADABLE_AVATAR_URL,
|
||||||
|
RAISE_HAND_UPDATED
|
||||||
} from './actionTypes';
|
} from './actionTypes';
|
||||||
import {
|
import {
|
||||||
DISCO_REMOTE_CONTROL_FEATURE
|
DISCO_REMOTE_CONTROL_FEATURE
|
||||||
|
@ -465,7 +466,7 @@ export function participantUpdated(participant = {}) {
|
||||||
* @returns {Promise}
|
* @returns {Promise}
|
||||||
*/
|
*/
|
||||||
export function participantMutedUs(participant, track) {
|
export function participantMutedUs(participant, track) {
|
||||||
return (dispatch, getState) => {
|
return dispatch => {
|
||||||
if (!participant) {
|
if (!participant) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
@ -473,12 +474,7 @@ export function participantMutedUs(participant, track) {
|
||||||
const isAudio = track.isAudioTrack();
|
const isAudio = track.isAudioTrack();
|
||||||
|
|
||||||
dispatch(showNotification({
|
dispatch(showNotification({
|
||||||
descriptionKey: isAudio ? 'notify.mutedRemotelyDescription' : 'notify.videoMutedRemotelyDescription',
|
titleKey: isAudio ? 'notify.mutedRemotelyTitle' : 'notify.videoMutedRemotelyTitle'
|
||||||
titleKey: isAudio ? 'notify.mutedRemotelyTitle' : 'notify.videoMutedRemotelyTitle',
|
|
||||||
titleArguments: {
|
|
||||||
participantDisplayName:
|
|
||||||
getParticipantDisplayName(getState, participant.getId())
|
|
||||||
}
|
|
||||||
}));
|
}));
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@ -574,3 +570,19 @@ export function raiseHand(enabled) {
|
||||||
enabled
|
enabled
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update raise hand queue of participants.
|
||||||
|
*
|
||||||
|
* @param {Object} participant - Participant that updated raised hand.
|
||||||
|
* @returns {{
|
||||||
|
* type: RAISE_HAND_UPDATED,
|
||||||
|
* participant: Object
|
||||||
|
* }}
|
||||||
|
*/
|
||||||
|
export function raiseHandUpdateQueue(participant) {
|
||||||
|
return {
|
||||||
|
type: RAISE_HAND_UPDATED,
|
||||||
|
participant
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
|
@ -456,21 +456,35 @@ async function _getFirstLoadableAvatarUrl(participant, store) {
|
||||||
export function getSortedParticipants(stateful: Object | Function) {
|
export function getSortedParticipants(stateful: Object | Function) {
|
||||||
const localParticipant = getLocalParticipant(stateful);
|
const localParticipant = getLocalParticipant(stateful);
|
||||||
const remoteParticipants = getRemoteParticipants(stateful);
|
const remoteParticipants = getRemoteParticipants(stateful);
|
||||||
|
const raisedHandParticipantIds = getRaiseHandsQueue(stateful);
|
||||||
|
|
||||||
const items = [];
|
const items = [];
|
||||||
const dominantSpeaker = getDominantSpeakerParticipant(stateful);
|
const dominantSpeaker = getDominantSpeakerParticipant(stateful);
|
||||||
|
const raisedHandParticipants = [];
|
||||||
|
|
||||||
|
raisedHandParticipantIds
|
||||||
|
.map(id => remoteParticipants.get(id) || localParticipant)
|
||||||
|
.forEach(p => {
|
||||||
|
if (p !== dominantSpeaker) {
|
||||||
|
raisedHandParticipants.push(p);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
remoteParticipants.forEach(p => {
|
remoteParticipants.forEach(p => {
|
||||||
if (p !== dominantSpeaker) {
|
if (p !== dominantSpeaker && !raisedHandParticipantIds.find(id => p.id === id)) {
|
||||||
items.push(p);
|
items.push(p);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (!raisedHandParticipantIds.find(id => localParticipant.id === id)) {
|
||||||
|
items.push(localParticipant);
|
||||||
|
}
|
||||||
|
|
||||||
items.sort((a, b) =>
|
items.sort((a, b) =>
|
||||||
getParticipantDisplayName(stateful, a.id).localeCompare(getParticipantDisplayName(stateful, b.id))
|
getParticipantDisplayName(stateful, a.id).localeCompare(getParticipantDisplayName(stateful, b.id))
|
||||||
);
|
);
|
||||||
|
|
||||||
items.unshift(localParticipant);
|
items.unshift(...raisedHandParticipants);
|
||||||
|
|
||||||
if (dominantSpeaker && dominantSpeaker !== localParticipant) {
|
if (dominantSpeaker && dominantSpeaker !== localParticipant) {
|
||||||
items.unshift(dominantSpeaker);
|
items.unshift(dominantSpeaker);
|
||||||
|
@ -492,3 +506,17 @@ export function getSortedParticipantIds(stateful: Object | Function): Array<stri
|
||||||
|
|
||||||
return participantIds;
|
return participantIds;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the participants queue with raised hands.
|
||||||
|
*
|
||||||
|
* @param {(Function|Object)} stateful - The (whole) redux state, or redux's
|
||||||
|
* {@code getState} function to be used to retrieve the state
|
||||||
|
* features/base/participants.
|
||||||
|
* @returns {Array<string>}
|
||||||
|
*/
|
||||||
|
export function getRaiseHandsQueue(stateful: Object | Function): Array<string> {
|
||||||
|
const { raisedHandsQueue } = toState(stateful)['features/base/participants'];
|
||||||
|
|
||||||
|
return raisedHandsQueue;
|
||||||
|
}
|
||||||
|
|
|
@ -3,8 +3,10 @@
|
||||||
import { batch } from 'react-redux';
|
import { batch } from 'react-redux';
|
||||||
|
|
||||||
import UIEvents from '../../../../service/UI/UIEvents';
|
import UIEvents from '../../../../service/UI/UIEvents';
|
||||||
|
import { approveParticipant } from '../../av-moderation/actions';
|
||||||
import { toggleE2EE } from '../../e2ee/actions';
|
import { toggleE2EE } from '../../e2ee/actions';
|
||||||
import { NOTIFICATION_TIMEOUT, showNotification } from '../../notifications';
|
import { NOTIFICATION_TIMEOUT, showNotification } from '../../notifications';
|
||||||
|
import { isForceMuted } from '../../participants-pane/functions';
|
||||||
import { CALLING, INVITED } from '../../presence-status';
|
import { CALLING, INVITED } from '../../presence-status';
|
||||||
import { RAISE_HAND_SOUND_ID } from '../../reactions/constants';
|
import { RAISE_HAND_SOUND_ID } from '../../reactions/constants';
|
||||||
import { APP_WILL_MOUNT, APP_WILL_UNMOUNT } from '../app';
|
import { APP_WILL_MOUNT, APP_WILL_UNMOUNT } from '../app';
|
||||||
|
@ -15,6 +17,7 @@ import {
|
||||||
} from '../conference';
|
} from '../conference';
|
||||||
import { getDisableRemoveRaisedHandOnFocus } from '../config/functions.any';
|
import { getDisableRemoveRaisedHandOnFocus } from '../config/functions.any';
|
||||||
import { JitsiConferenceEvents } from '../lib-jitsi-meet';
|
import { JitsiConferenceEvents } from '../lib-jitsi-meet';
|
||||||
|
import { MEDIA_TYPE } from '../media';
|
||||||
import { MiddlewareRegistry, StateListenerRegistry } from '../redux';
|
import { MiddlewareRegistry, StateListenerRegistry } from '../redux';
|
||||||
import { playSound, registerSound, unregisterSound } from '../sounds';
|
import { playSound, registerSound, unregisterSound } from '../sounds';
|
||||||
|
|
||||||
|
@ -27,7 +30,8 @@ import {
|
||||||
PARTICIPANT_DISPLAY_NAME_CHANGED,
|
PARTICIPANT_DISPLAY_NAME_CHANGED,
|
||||||
PARTICIPANT_JOINED,
|
PARTICIPANT_JOINED,
|
||||||
PARTICIPANT_LEFT,
|
PARTICIPANT_LEFT,
|
||||||
PARTICIPANT_UPDATED
|
PARTICIPANT_UPDATED,
|
||||||
|
RAISE_HAND_UPDATED
|
||||||
} from './actionTypes';
|
} from './actionTypes';
|
||||||
import {
|
import {
|
||||||
localParticipantIdChanged,
|
localParticipantIdChanged,
|
||||||
|
@ -35,6 +39,7 @@ import {
|
||||||
localParticipantLeft,
|
localParticipantLeft,
|
||||||
participantLeft,
|
participantLeft,
|
||||||
participantUpdated,
|
participantUpdated,
|
||||||
|
raiseHandUpdateQueue,
|
||||||
setLoadableAvatarUrl
|
setLoadableAvatarUrl
|
||||||
} from './actions';
|
} from './actions';
|
||||||
import {
|
import {
|
||||||
|
@ -48,7 +53,9 @@ import {
|
||||||
getParticipantById,
|
getParticipantById,
|
||||||
getParticipantCount,
|
getParticipantCount,
|
||||||
getParticipantDisplayName,
|
getParticipantDisplayName,
|
||||||
getRemoteParticipants
|
getRaiseHandsQueue,
|
||||||
|
getRemoteParticipants,
|
||||||
|
isLocalParticipantModerator
|
||||||
} from './functions';
|
} from './functions';
|
||||||
import { PARTICIPANT_JOINED_FILE, PARTICIPANT_LEFT_FILE } from './sounds';
|
import { PARTICIPANT_JOINED_FILE, PARTICIPANT_LEFT_FILE } from './sounds';
|
||||||
|
|
||||||
|
@ -122,6 +129,11 @@ MiddlewareRegistry.register(store => next => action => {
|
||||||
const { enabled } = action;
|
const { enabled } = action;
|
||||||
const localId = getLocalParticipant(store.getState())?.id;
|
const localId = getLocalParticipant(store.getState())?.id;
|
||||||
|
|
||||||
|
store.dispatch(raiseHandUpdateQueue({
|
||||||
|
id: localId,
|
||||||
|
raisedHand: enabled
|
||||||
|
}));
|
||||||
|
|
||||||
store.dispatch(participantUpdated({
|
store.dispatch(participantUpdated({
|
||||||
// XXX Only the local participant is allowed to update without
|
// XXX Only the local participant is allowed to update without
|
||||||
// stating the JitsiConference instance (i.e. participant property
|
// stating the JitsiConference instance (i.e. participant property
|
||||||
|
@ -162,6 +174,21 @@ MiddlewareRegistry.register(store => next => action => {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
case RAISE_HAND_UPDATED: {
|
||||||
|
const { participant } = action;
|
||||||
|
const queue = getRaiseHandsQueue(store.getState());
|
||||||
|
|
||||||
|
if (participant.raisedHand) {
|
||||||
|
queue.push(participant.id);
|
||||||
|
action.queue = queue;
|
||||||
|
} else {
|
||||||
|
const filteredQueue = queue.filter(id => id !== participant.id);
|
||||||
|
|
||||||
|
action.queue = filteredQueue;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
case PARTICIPANT_JOINED: {
|
case PARTICIPANT_JOINED: {
|
||||||
_maybePlaySounds(store, action);
|
_maybePlaySounds(store, action);
|
||||||
|
|
||||||
|
@ -424,6 +451,7 @@ function _participantJoinedOrUpdated(store, next, action) {
|
||||||
// Send an external update of the local participant's raised hand state
|
// Send an external update of the local participant's raised hand state
|
||||||
// if a new raised hand state is defined in the action.
|
// if a new raised hand state is defined in the action.
|
||||||
if (typeof raisedHand !== 'undefined') {
|
if (typeof raisedHand !== 'undefined') {
|
||||||
|
|
||||||
if (local) {
|
if (local) {
|
||||||
const { conference } = getState()['features/base/conference'];
|
const { conference } = getState()['features/base/conference'];
|
||||||
|
|
||||||
|
@ -476,6 +504,7 @@ function _participantJoinedOrUpdated(store, next, action) {
|
||||||
*/
|
*/
|
||||||
function _raiseHandUpdated({ dispatch, getState }, conference, participantId, newValue) {
|
function _raiseHandUpdated({ dispatch, getState }, conference, participantId, newValue) {
|
||||||
const raisedHand = newValue === 'true';
|
const raisedHand = newValue === 'true';
|
||||||
|
const state = getState();
|
||||||
|
|
||||||
dispatch(participantUpdated({
|
dispatch(participantUpdated({
|
||||||
conference,
|
conference,
|
||||||
|
@ -483,17 +512,37 @@ function _raiseHandUpdated({ dispatch, getState }, conference, participantId, ne
|
||||||
raisedHand
|
raisedHand
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
dispatch(raiseHandUpdateQueue({
|
||||||
|
id: participantId,
|
||||||
|
raisedHand
|
||||||
|
}));
|
||||||
|
|
||||||
if (typeof APP !== 'undefined') {
|
if (typeof APP !== 'undefined') {
|
||||||
APP.API.notifyRaiseHandUpdated(participantId, raisedHand);
|
APP.API.notifyRaiseHandUpdated(participantId, raisedHand);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const isModerator = isLocalParticipantModerator(state);
|
||||||
|
const participant = getParticipantById(state, participantId);
|
||||||
|
let shouldDisplayAllowAction = false;
|
||||||
|
|
||||||
|
if (isModerator) {
|
||||||
|
shouldDisplayAllowAction = isForceMuted(participant, MEDIA_TYPE.AUDIO, state)
|
||||||
|
|| isForceMuted(participant, MEDIA_TYPE.VIDEO, state);
|
||||||
|
}
|
||||||
|
|
||||||
|
const action = shouldDisplayAllowAction ? {
|
||||||
|
customActionNameKey: 'notify.allowAction',
|
||||||
|
customActionHandler: () => dispatch(approveParticipant(participantId))
|
||||||
|
} : {};
|
||||||
|
|
||||||
if (raisedHand) {
|
if (raisedHand) {
|
||||||
dispatch(showNotification({
|
dispatch(showNotification({
|
||||||
titleArguments: {
|
titleKey: 'notify.somebody',
|
||||||
name: getParticipantDisplayName(getState, participantId)
|
title: getParticipantDisplayName(state, participantId),
|
||||||
},
|
descriptionKey: 'notify.raisedHand',
|
||||||
titleKey: 'notify.raisedHand'
|
raiseHandNotification: true,
|
||||||
}, NOTIFICATION_TIMEOUT));
|
...action
|
||||||
|
}, NOTIFICATION_TIMEOUT * (shouldDisplayAllowAction ? 2 : 1)));
|
||||||
dispatch(playSound(RAISE_HAND_SOUND_ID));
|
dispatch(playSound(RAISE_HAND_SOUND_ID));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -10,6 +10,7 @@ import {
|
||||||
PARTICIPANT_LEFT,
|
PARTICIPANT_LEFT,
|
||||||
PARTICIPANT_UPDATED,
|
PARTICIPANT_UPDATED,
|
||||||
PIN_PARTICIPANT,
|
PIN_PARTICIPANT,
|
||||||
|
RAISE_HAND_UPDATED,
|
||||||
SET_LOADABLE_AVATAR_URL
|
SET_LOADABLE_AVATAR_URL
|
||||||
} from './actionTypes';
|
} from './actionTypes';
|
||||||
import { LOCAL_PARTICIPANT_DEFAULT_ID, PARTICIPANT_ROLE } from './constants';
|
import { LOCAL_PARTICIPANT_DEFAULT_ID, PARTICIPANT_ROLE } from './constants';
|
||||||
|
@ -63,7 +64,8 @@ const DEFAULT_STATE = {
|
||||||
remote: new Map(),
|
remote: new Map(),
|
||||||
sortedRemoteParticipants: new Map(),
|
sortedRemoteParticipants: new Map(),
|
||||||
sortedRemoteScreenshares: new Map(),
|
sortedRemoteScreenshares: new Map(),
|
||||||
speakersList: new Map()
|
speakersList: new Map(),
|
||||||
|
raisedHandsQueue: []
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -318,6 +320,12 @@ ReducerRegistry.register('features/base/participants', (state = DEFAULT_STATE, a
|
||||||
|
|
||||||
return { ...state };
|
return { ...state };
|
||||||
}
|
}
|
||||||
|
case RAISE_HAND_UPDATED: {
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
raisedHandsQueue: action.queue
|
||||||
|
};
|
||||||
|
}
|
||||||
case SCREEN_SHARE_REMOTE_PARTICIPANTS_UPDATED: {
|
case SCREEN_SHARE_REMOTE_PARTICIPANTS_UPDATED: {
|
||||||
const { participantIds } = action;
|
const { participantIds } = action;
|
||||||
const sortedSharesList = [];
|
const sortedSharesList = [];
|
||||||
|
|
|
@ -14,7 +14,8 @@ import {
|
||||||
SET_VIDEO_MUTED,
|
SET_VIDEO_MUTED,
|
||||||
VIDEO_MUTISM_AUTHORITY,
|
VIDEO_MUTISM_AUTHORITY,
|
||||||
TOGGLE_CAMERA_FACING_MODE,
|
TOGGLE_CAMERA_FACING_MODE,
|
||||||
toggleCameraFacingMode
|
toggleCameraFacingMode,
|
||||||
|
VIDEO_TYPE
|
||||||
} from '../media';
|
} from '../media';
|
||||||
import { MiddlewareRegistry } from '../redux';
|
import { MiddlewareRegistry } from '../redux';
|
||||||
|
|
||||||
|
@ -28,6 +29,7 @@ import {
|
||||||
import {
|
import {
|
||||||
createLocalTracksA,
|
createLocalTracksA,
|
||||||
showNoDataFromSourceVideoError,
|
showNoDataFromSourceVideoError,
|
||||||
|
toggleScreensharing,
|
||||||
trackNoDataFromSourceNotificationInfoChanged
|
trackNoDataFromSourceNotificationInfoChanged
|
||||||
} from './actions';
|
} from './actions';
|
||||||
import {
|
import {
|
||||||
|
@ -137,9 +139,9 @@ MiddlewareRegistry.register(store => next => action => {
|
||||||
|
|
||||||
case TOGGLE_SCREENSHARING:
|
case TOGGLE_SCREENSHARING:
|
||||||
if (typeof APP === 'object') {
|
if (typeof APP === 'object') {
|
||||||
|
|
||||||
// check for A/V Moderation when trying to start screen sharing
|
// check for A/V Moderation when trying to start screen sharing
|
||||||
if (action.enabled && shouldShowModeratedNotification(MEDIA_TYPE.VIDEO, store.getState())) {
|
if ((action.enabled || action.enabled === undefined)
|
||||||
|
&& shouldShowModeratedNotification(MEDIA_TYPE.VIDEO, store.getState())) {
|
||||||
store.dispatch(showModeratedNotification(MEDIA_TYPE.PRESENTER));
|
store.dispatch(showModeratedNotification(MEDIA_TYPE.PRESENTER));
|
||||||
|
|
||||||
return;
|
return;
|
||||||
|
@ -171,8 +173,10 @@ MiddlewareRegistry.register(store => next => action => {
|
||||||
// Do not change the video mute state for local presenter tracks.
|
// Do not change the video mute state for local presenter tracks.
|
||||||
if (jitsiTrack.type === MEDIA_TYPE.PRESENTER) {
|
if (jitsiTrack.type === MEDIA_TYPE.PRESENTER) {
|
||||||
APP.conference.mutePresenter(muted);
|
APP.conference.mutePresenter(muted);
|
||||||
} else if (jitsiTrack.isLocal()) {
|
} else if (jitsiTrack.isLocal() && !(jitsiTrack.videoType === VIDEO_TYPE.DESKTOP)) {
|
||||||
APP.conference.setVideoMuteStatus();
|
APP.conference.setVideoMuteStatus();
|
||||||
|
} else if (jitsiTrack.isLocal() && muted && jitsiTrack.videoType === VIDEO_TYPE.DESKTOP) {
|
||||||
|
store.dispatch(toggleScreensharing(false));
|
||||||
} else {
|
} else {
|
||||||
APP.UI.setVideoMuted(participantID);
|
APP.UI.setVideoMuted(participantID);
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,6 +3,7 @@
|
||||||
import type { Dispatch } from 'redux';
|
import type { Dispatch } from 'redux';
|
||||||
|
|
||||||
import VideoLayout from '../../../modules/UI/videolayout/VideoLayout';
|
import VideoLayout from '../../../modules/UI/videolayout/VideoLayout';
|
||||||
|
import { getParticipantById } from '../base/participants/functions';
|
||||||
|
|
||||||
import { OPEN_CHAT } from './actionTypes';
|
import { OPEN_CHAT } from './actionTypes';
|
||||||
import { closeChat } from './actions.any';
|
import { closeChat } from './actions.any';
|
||||||
|
@ -27,6 +28,27 @@ export function openChat(participant: Object) {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Displays the chat panel for a participant identified by an id.
|
||||||
|
*
|
||||||
|
* @param {string} id - The id of the participant.
|
||||||
|
* @returns {{
|
||||||
|
* participant: Participant,
|
||||||
|
* type: OPEN_CHAT
|
||||||
|
* }}
|
||||||
|
*/
|
||||||
|
export function openChatById(id: string) {
|
||||||
|
return function(dispatch: (Object) => Object, getState: Function) {
|
||||||
|
const participant = getParticipantById(getState(), id);
|
||||||
|
|
||||||
|
return dispatch({
|
||||||
|
participant,
|
||||||
|
type: OPEN_CHAT
|
||||||
|
});
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Toggles display of the chat panel.
|
* Toggles display of the chat panel.
|
||||||
*
|
*
|
||||||
|
|
|
@ -4,7 +4,6 @@ import _ from 'lodash';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
|
||||||
import VideoLayout from '../../../../../modules/UI/videolayout/VideoLayout';
|
import VideoLayout from '../../../../../modules/UI/videolayout/VideoLayout';
|
||||||
import AudioModerationNotifications from '../../../av-moderation/components/AudioModerationNotifications';
|
|
||||||
import { getConferenceNameForTitle } from '../../../base/conference';
|
import { getConferenceNameForTitle } from '../../../base/conference';
|
||||||
import { connect, disconnect } from '../../../base/connection';
|
import { connect, disconnect } from '../../../base/connection';
|
||||||
import { translate } from '../../../base/i18n';
|
import { translate } from '../../../base/i18n';
|
||||||
|
@ -233,7 +232,6 @@ class Conference extends AbstractConference<Props, *> {
|
||||||
{!_isParticipantsPaneVisible
|
{!_isParticipantsPaneVisible
|
||||||
&& <div id = 'notification-participant-list'>
|
&& <div id = 'notification-participant-list'>
|
||||||
<KnockingParticipantList />
|
<KnockingParticipantList />
|
||||||
<AudioModerationNotifications />
|
|
||||||
</div>}
|
</div>}
|
||||||
<Filmstrip />
|
<Filmstrip />
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -45,3 +45,13 @@ export const SHOW_NOTIFICATION = 'SHOW_NOTIFICATION';
|
||||||
* }
|
* }
|
||||||
*/
|
*/
|
||||||
export const SET_NOTIFICATIONS_ENABLED = 'SET_NOTIFICATIONS_ENABLED';
|
export const SET_NOTIFICATIONS_ENABLED = 'SET_NOTIFICATIONS_ENABLED';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The type of (redux) action which signals that raise hand notifications
|
||||||
|
* should be dismissed.
|
||||||
|
*
|
||||||
|
* {
|
||||||
|
* type: HIDE_RAISE_HAND_NOTIFICATIONS
|
||||||
|
* }
|
||||||
|
*/
|
||||||
|
export const HIDE_RAISE_HAND_NOTIFICATIONS = 'HIDE_RAISE_HAND_NOTIFICATIONS';
|
||||||
|
|
|
@ -9,6 +9,7 @@ import { getParticipantCount } from '../base/participants/functions';
|
||||||
import {
|
import {
|
||||||
CLEAR_NOTIFICATIONS,
|
CLEAR_NOTIFICATIONS,
|
||||||
HIDE_NOTIFICATION,
|
HIDE_NOTIFICATION,
|
||||||
|
HIDE_RAISE_HAND_NOTIFICATIONS,
|
||||||
SET_NOTIFICATIONS_ENABLED,
|
SET_NOTIFICATIONS_ENABLED,
|
||||||
SHOW_NOTIFICATION
|
SHOW_NOTIFICATION
|
||||||
} from './actionTypes';
|
} from './actionTypes';
|
||||||
|
@ -48,6 +49,19 @@ export function hideNotification(uid: string) {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Removes the raise hand notifications.
|
||||||
|
*
|
||||||
|
* @returns {{
|
||||||
|
* type: HIDE_RAISE_HAND_NOTIFICATIONS
|
||||||
|
* }}
|
||||||
|
*/
|
||||||
|
export function hideRaiseHandNotifications() {
|
||||||
|
return {
|
||||||
|
type: HIDE_RAISE_HAND_NOTIFICATIONS
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Stops notifications from being displayed.
|
* Stops notifications from being displayed.
|
||||||
*
|
*
|
||||||
|
|
|
@ -7,12 +7,15 @@ import {
|
||||||
PARTICIPANT_ROLE,
|
PARTICIPANT_ROLE,
|
||||||
PARTICIPANT_UPDATED,
|
PARTICIPANT_UPDATED,
|
||||||
getParticipantById,
|
getParticipantById,
|
||||||
getParticipantDisplayName
|
getParticipantDisplayName,
|
||||||
|
getLocalParticipant
|
||||||
} from '../base/participants';
|
} from '../base/participants';
|
||||||
import { MiddlewareRegistry, StateListenerRegistry } from '../base/redux';
|
import { MiddlewareRegistry, StateListenerRegistry } from '../base/redux';
|
||||||
|
import { PARTICIPANTS_PANE_OPEN } from '../participants-pane/actionTypes';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
clearNotifications,
|
clearNotifications,
|
||||||
|
hideRaiseHandNotifications,
|
||||||
showNotification,
|
showNotification,
|
||||||
showParticipantJoinedNotification
|
showParticipantJoinedNotification
|
||||||
} from './actions';
|
} from './actions';
|
||||||
|
@ -42,22 +45,6 @@ MiddlewareRegistry.register(store => next => action => {
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (typeof interfaceConfig === 'object'
|
|
||||||
&& !interfaceConfig.DISABLE_FOCUS_INDICATOR && p.role === PARTICIPANT_ROLE.MODERATOR) {
|
|
||||||
// Do not show the notification for mobile and also when the focus indicator is disabled.
|
|
||||||
const displayName = getParticipantDisplayName(state, p.id);
|
|
||||||
|
|
||||||
if (!p.isReplacing) {
|
|
||||||
dispatch(showNotification({
|
|
||||||
descriptionArguments: { to: displayName || '$t(notify.somebody)' },
|
|
||||||
descriptionKey: 'notify.grantedTo',
|
|
||||||
titleKey: 'notify.somebody',
|
|
||||||
title: displayName
|
|
||||||
},
|
|
||||||
NOTIFICATION_TIMEOUT));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
case PARTICIPANT_LEFT: {
|
case PARTICIPANT_LEFT: {
|
||||||
|
@ -82,30 +69,36 @@ MiddlewareRegistry.register(store => next => action => {
|
||||||
return next(action);
|
return next(action);
|
||||||
}
|
}
|
||||||
case PARTICIPANT_UPDATED: {
|
case PARTICIPANT_UPDATED: {
|
||||||
if (typeof interfaceConfig === 'undefined' || interfaceConfig.DISABLE_FOCUS_INDICATOR) {
|
if (typeof interfaceConfig === 'undefined') {
|
||||||
// Do not show the notification for mobile and also when the focus indicator is disabled.
|
// Do not show the notification for mobile and also when the focus indicator is disabled.
|
||||||
return next(action);
|
return next(action);
|
||||||
}
|
}
|
||||||
|
|
||||||
const { id, role } = action.participant;
|
const { id, role } = action.participant;
|
||||||
const state = store.getState();
|
const state = store.getState();
|
||||||
|
const localParticipant = getLocalParticipant(state);
|
||||||
|
|
||||||
|
if (localParticipant.id !== id) {
|
||||||
|
return next(action);
|
||||||
|
}
|
||||||
|
|
||||||
const oldParticipant = getParticipantById(state, id);
|
const oldParticipant = getParticipantById(state, id);
|
||||||
const oldRole = oldParticipant?.role;
|
const oldRole = oldParticipant?.role;
|
||||||
|
|
||||||
if (oldRole && oldRole !== role && role === PARTICIPANT_ROLE.MODERATOR) {
|
if (oldRole && oldRole !== role && role === PARTICIPANT_ROLE.MODERATOR) {
|
||||||
const displayName = getParticipantDisplayName(state, id);
|
|
||||||
|
|
||||||
store.dispatch(showNotification({
|
store.dispatch(showNotification({
|
||||||
descriptionArguments: { to: displayName || '$t(notify.somebody)' },
|
titleKey: 'notify.moderator'
|
||||||
descriptionKey: 'notify.grantedTo',
|
|
||||||
titleKey: 'notify.somebody',
|
|
||||||
title: displayName
|
|
||||||
},
|
},
|
||||||
NOTIFICATION_TIMEOUT));
|
NOTIFICATION_TIMEOUT));
|
||||||
}
|
}
|
||||||
|
|
||||||
return next(action);
|
return next(action);
|
||||||
}
|
}
|
||||||
|
case PARTICIPANTS_PANE_OPEN: {
|
||||||
|
store.dispatch(hideRaiseHandNotifications());
|
||||||
|
break;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return next(action);
|
return next(action);
|
||||||
|
|
|
@ -5,6 +5,7 @@ import { ReducerRegistry } from '../base/redux';
|
||||||
import {
|
import {
|
||||||
CLEAR_NOTIFICATIONS,
|
CLEAR_NOTIFICATIONS,
|
||||||
HIDE_NOTIFICATION,
|
HIDE_NOTIFICATION,
|
||||||
|
HIDE_RAISE_HAND_NOTIFICATIONS,
|
||||||
SET_NOTIFICATIONS_ENABLED,
|
SET_NOTIFICATIONS_ENABLED,
|
||||||
SHOW_NOTIFICATION
|
SHOW_NOTIFICATION
|
||||||
} from './actionTypes';
|
} from './actionTypes';
|
||||||
|
@ -43,6 +44,14 @@ ReducerRegistry.register('features/notifications',
|
||||||
notification => notification.uid !== action.uid)
|
notification => notification.uid !== action.uid)
|
||||||
};
|
};
|
||||||
|
|
||||||
|
case HIDE_RAISE_HAND_NOTIFICATIONS:
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
notifications: state.notifications.filter(
|
||||||
|
notification => !notification.props.raiseHandNotification
|
||||||
|
)
|
||||||
|
};
|
||||||
|
|
||||||
case SET_NOTIFICATIONS_ENABLED:
|
case SET_NOTIFICATIONS_ENABLED:
|
||||||
return {
|
return {
|
||||||
...state,
|
...state,
|
||||||
|
|
|
@ -1,11 +1,17 @@
|
||||||
// @flow
|
// @flow
|
||||||
|
|
||||||
import { makeStyles } from '@material-ui/core/styles';
|
import { makeStyles } from '@material-ui/core/styles';
|
||||||
|
import clsx from 'clsx';
|
||||||
import React, { useCallback } from 'react';
|
import React, { useCallback } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { useDispatch, useSelector } from 'react-redux';
|
import { useDispatch, useSelector } from 'react-redux';
|
||||||
|
|
||||||
import { requestDisableModeration, requestEnableModeration } from '../../av-moderation/actions';
|
import {
|
||||||
|
requestDisableAudioModeration,
|
||||||
|
requestDisableVideoModeration,
|
||||||
|
requestEnableAudioModeration,
|
||||||
|
requestEnableVideoModeration
|
||||||
|
} from '../../av-moderation/actions';
|
||||||
import {
|
import {
|
||||||
isEnabled as isAvModerationEnabled,
|
isEnabled as isAvModerationEnabled,
|
||||||
isSupported as isAvModerationSupported
|
isSupported as isAvModerationSupported
|
||||||
|
@ -13,7 +19,10 @@ import {
|
||||||
import { openDialog } from '../../base/dialog';
|
import { openDialog } from '../../base/dialog';
|
||||||
import { Icon, IconCheck, IconVideoOff } from '../../base/icons';
|
import { Icon, IconCheck, IconVideoOff } from '../../base/icons';
|
||||||
import { MEDIA_TYPE } from '../../base/media';
|
import { MEDIA_TYPE } from '../../base/media';
|
||||||
import { getLocalParticipant } from '../../base/participants';
|
import {
|
||||||
|
getParticipantCount,
|
||||||
|
isEveryoneModerator
|
||||||
|
} from '../../base/participants';
|
||||||
import { MuteEveryonesVideoDialog } from '../../video-menu/components';
|
import { MuteEveryonesVideoDialog } from '../../video-menu/components';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
|
@ -33,6 +42,17 @@ const useStyles = makeStyles(() => {
|
||||||
transform: 'translateY(-100%)',
|
transform: 'translateY(-100%)',
|
||||||
width: '283px'
|
width: '283px'
|
||||||
},
|
},
|
||||||
|
drawer: {
|
||||||
|
width: '100%',
|
||||||
|
top: 'auto',
|
||||||
|
bottom: 0,
|
||||||
|
transform: 'none',
|
||||||
|
position: 'relative',
|
||||||
|
|
||||||
|
'& > div': {
|
||||||
|
lineHeight: '32px'
|
||||||
|
}
|
||||||
|
},
|
||||||
text: {
|
text: {
|
||||||
color: '#C2C2C2',
|
color: '#C2C2C2',
|
||||||
padding: '10px 16px 10px 52px'
|
padding: '10px 16px 10px 52px'
|
||||||
|
@ -45,31 +65,43 @@ const useStyles = makeStyles(() => {
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Callback for the mouse leaving this item
|
* Whether the menu is displayed inside a drawer.
|
||||||
*/
|
*/
|
||||||
onMouseLeave: Function
|
inDrawer?: boolean,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Callback for the mouse leaving this item.
|
||||||
|
*/
|
||||||
|
onMouseLeave?: Function
|
||||||
};
|
};
|
||||||
|
|
||||||
export const FooterContextMenu = ({ onMouseLeave }: Props) => {
|
export const FooterContextMenu = ({ inDrawer, onMouseLeave }: Props) => {
|
||||||
const dispatch = useDispatch();
|
const dispatch = useDispatch();
|
||||||
const isModerationSupported = useSelector(isAvModerationSupported());
|
const isModerationSupported = useSelector(isAvModerationSupported());
|
||||||
const isModerationEnabled = useSelector(isAvModerationEnabled(MEDIA_TYPE.AUDIO));
|
const allModerators = useSelector(isEveryoneModerator);
|
||||||
const { id } = useSelector(getLocalParticipant);
|
const participantCount = useSelector(getParticipantCount);
|
||||||
|
const isAudioModerationEnabled = useSelector(isAvModerationEnabled(MEDIA_TYPE.AUDIO));
|
||||||
|
const isVideoModerationEnabled = useSelector(isAvModerationEnabled(MEDIA_TYPE.VIDEO));
|
||||||
|
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
const disable = useCallback(() => dispatch(requestDisableModeration()), [ dispatch ]);
|
const disableAudioModeration = useCallback(() => dispatch(requestDisableAudioModeration()), [ dispatch ]);
|
||||||
|
|
||||||
const enable = useCallback(() => dispatch(requestEnableModeration()), [ dispatch ]);
|
const disableVideoModeration = useCallback(() => dispatch(requestDisableVideoModeration()), [ dispatch ]);
|
||||||
|
|
||||||
|
const enableAudioModeration = useCallback(() => dispatch(requestEnableAudioModeration()), [ dispatch ]);
|
||||||
|
|
||||||
|
const enableVideoModeration = useCallback(() => dispatch(requestEnableVideoModeration()), [ dispatch ]);
|
||||||
|
|
||||||
const classes = useStyles();
|
const classes = useStyles();
|
||||||
|
|
||||||
const muteAllVideo = useCallback(
|
const muteAllVideo = useCallback(
|
||||||
() => dispatch(openDialog(MuteEveryonesVideoDialog, { exclude: [ id ] })), [ dispatch ]);
|
() => dispatch(openDialog(MuteEveryonesVideoDialog)), [ dispatch ]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ContextMenu
|
<ContextMenu
|
||||||
className = { classes.contextMenu }
|
className = { clsx(classes.contextMenu, inDrawer && clsx(classes.drawer)) }
|
||||||
onMouseLeave = { onMouseLeave }>
|
onMouseLeave = { onMouseLeave }>
|
||||||
<ContextMenuItemGroup>
|
<ContextMenuItemGroup>
|
||||||
<ContextMenuItem
|
<ContextMenuItem
|
||||||
|
@ -81,27 +113,45 @@ export const FooterContextMenu = ({ onMouseLeave }: Props) => {
|
||||||
<span>{ t('participantsPane.actions.stopEveryonesVideo') }</span>
|
<span>{ t('participantsPane.actions.stopEveryonesVideo') }</span>
|
||||||
</ContextMenuItem>
|
</ContextMenuItem>
|
||||||
</ContextMenuItemGroup>
|
</ContextMenuItemGroup>
|
||||||
{ isModerationSupported ? (
|
{isModerationSupported && (participantCount === 1 || !allModerators) ? (
|
||||||
<ContextMenuItemGroup>
|
<ContextMenuItemGroup>
|
||||||
<div className = { classes.text }>
|
<div className = { classes.text }>
|
||||||
{t('participantsPane.actions.allow')}
|
{t('participantsPane.actions.allow')}
|
||||||
</div>
|
</div>
|
||||||
{ isModerationEnabled ? (
|
{ isAudioModerationEnabled ? (
|
||||||
<ContextMenuItem
|
<ContextMenuItem
|
||||||
id = 'participants-pane-context-menu-stop-moderation'
|
id = 'participants-pane-context-menu-stop-audio-moderation'
|
||||||
onClick = { disable }>
|
onClick = { disableAudioModeration }>
|
||||||
<span className = { classes.paddedAction }>
|
<span className = { classes.paddedAction }>
|
||||||
{ t('participantsPane.actions.startModeration') }
|
{t('participantsPane.actions.audioModeration') }
|
||||||
</span>
|
</span>
|
||||||
</ContextMenuItem>
|
</ContextMenuItem>
|
||||||
) : (
|
) : (
|
||||||
<ContextMenuItem
|
<ContextMenuItem
|
||||||
id = 'participants-pane-context-menu-start-moderation'
|
id = 'participants-pane-context-menu-start-audio-moderation'
|
||||||
onClick = { enable }>
|
onClick = { enableAudioModeration }>
|
||||||
<Icon
|
<Icon
|
||||||
size = { 20 }
|
size = { 20 }
|
||||||
src = { IconCheck } />
|
src = { IconCheck } />
|
||||||
<span>{ t('participantsPane.actions.startModeration') }</span>
|
<span>{t('participantsPane.actions.audioModeration') }</span>
|
||||||
|
</ContextMenuItem>
|
||||||
|
)}
|
||||||
|
{ isVideoModerationEnabled ? (
|
||||||
|
<ContextMenuItem
|
||||||
|
id = 'participants-pane-context-menu-stop-video-moderation'
|
||||||
|
onClick = { disableVideoModeration }>
|
||||||
|
<span className = { classes.paddedAction }>
|
||||||
|
{t('participantsPane.actions.videoModeration')}
|
||||||
|
</span>
|
||||||
|
</ContextMenuItem>
|
||||||
|
) : (
|
||||||
|
<ContextMenuItem
|
||||||
|
id = 'participants-pane-context-menu-start-video-moderation'
|
||||||
|
onClick = { enableVideoModeration }>
|
||||||
|
<Icon
|
||||||
|
size = { 20 }
|
||||||
|
src = { IconCheck } />
|
||||||
|
<span>{t('participantsPane.actions.videoModeration')}</span>
|
||||||
</ContextMenuItem>
|
</ContextMenuItem>
|
||||||
)}
|
)}
|
||||||
</ContextMenuItemGroup>
|
</ContextMenuItemGroup>
|
||||||
|
|
|
@ -1,27 +1,39 @@
|
||||||
// @flow
|
// @flow
|
||||||
|
|
||||||
import React, { useCallback } from 'react';
|
import React from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { useDispatch } from 'react-redux';
|
|
||||||
|
|
||||||
import { approveKnockingParticipant, rejectKnockingParticipant } from '../../../lobby/actions';
|
|
||||||
import { ACTION_TRIGGER, MEDIA_STATE } from '../../constants';
|
import { ACTION_TRIGGER, MEDIA_STATE } from '../../constants';
|
||||||
|
import { useLobbyActions } from '../../hooks';
|
||||||
|
|
||||||
import ParticipantItem from './ParticipantItem';
|
import ParticipantItem from './ParticipantItem';
|
||||||
import { ParticipantActionButton } from './styled';
|
import { ParticipantActionButton } from './styled';
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* If an overflow drawer should be displayed.
|
||||||
|
*/
|
||||||
|
overflowDrawer: boolean,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Callback used to open a drawer with admit/reject actions.
|
||||||
|
*/
|
||||||
|
openDrawerForParticipant: Function,
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Participant reference
|
* Participant reference
|
||||||
*/
|
*/
|
||||||
participant: Object
|
participant: Object
|
||||||
};
|
};
|
||||||
|
|
||||||
export const LobbyParticipantItem = ({ participant: p }: Props) => {
|
export const LobbyParticipantItem = ({
|
||||||
const dispatch = useDispatch();
|
overflowDrawer,
|
||||||
const admit = useCallback(() => dispatch(approveKnockingParticipant(p.id), [ dispatch ]));
|
participant: p,
|
||||||
const reject = useCallback(() => dispatch(rejectKnockingParticipant(p.id), [ dispatch ]));
|
openDrawerForParticipant
|
||||||
|
}: Props) => {
|
||||||
|
const { id } = p;
|
||||||
|
const [ admit ] = useLobbyActions({ participantID: id });
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -30,14 +42,12 @@ export const LobbyParticipantItem = ({ participant: p }: Props) => {
|
||||||
audioMediaState = { MEDIA_STATE.NONE }
|
audioMediaState = { MEDIA_STATE.NONE }
|
||||||
displayName = { p.name }
|
displayName = { p.name }
|
||||||
local = { p.local }
|
local = { p.local }
|
||||||
participantID = { p.id }
|
openDrawerForParticipant = { openDrawerForParticipant }
|
||||||
|
overflowDrawer = { overflowDrawer }
|
||||||
|
participantID = { id }
|
||||||
raisedHand = { p.raisedHand }
|
raisedHand = { p.raisedHand }
|
||||||
videoMuteState = { MEDIA_STATE.NONE }
|
videoMediaState = { MEDIA_STATE.NONE }
|
||||||
youText = { t('chat.you') }>
|
youText = { t('chat.you') }>
|
||||||
<ParticipantActionButton
|
|
||||||
onClick = { reject }>
|
|
||||||
{t('lobby.reject')}
|
|
||||||
</ParticipantActionButton>
|
|
||||||
<ParticipantActionButton
|
<ParticipantActionButton
|
||||||
onClick = { admit }
|
onClick = { admit }
|
||||||
primary = { true }>
|
primary = { true }>
|
||||||
|
|
|
@ -0,0 +1,47 @@
|
||||||
|
// @flow
|
||||||
|
|
||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
import { LobbyParticipantItem } from './LobbyParticipantItem';
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Opens a drawer with actions for a knocking participant.
|
||||||
|
*/
|
||||||
|
openDrawerForParticipant: Function,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* If a drawer with actions should be displayed.
|
||||||
|
*/
|
||||||
|
overflowDrawer: boolean,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* List with the knocking participants.
|
||||||
|
*/
|
||||||
|
participants: Array<Object>
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Component used to display a list of knocking participants.
|
||||||
|
*
|
||||||
|
* @param {Object} props - The props of the component.
|
||||||
|
* @returns {ReactNode}
|
||||||
|
*/
|
||||||
|
function LobbyParticipantItems({ openDrawerForParticipant, overflowDrawer, participants }: Props) {
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
{participants.map(p => (
|
||||||
|
<LobbyParticipantItem
|
||||||
|
key = { p.id }
|
||||||
|
openDrawerForParticipant = { openDrawerForParticipant }
|
||||||
|
overflowDrawer = { overflowDrawer }
|
||||||
|
participant = { p } />)
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Memoize the component in order to avoid rerender on drawer open/close.
|
||||||
|
export default React.memo<Props>(LobbyParticipantItems);
|
|
@ -1,72 +0,0 @@
|
||||||
// @flow
|
|
||||||
|
|
||||||
import { makeStyles } from '@material-ui/core/styles';
|
|
||||||
import React, { useCallback } from 'react';
|
|
||||||
import { useTranslation } from 'react-i18next';
|
|
||||||
import { useSelector, useDispatch } from 'react-redux';
|
|
||||||
|
|
||||||
import { withPixelLineHeight } from '../../../base/styles/functions.web';
|
|
||||||
import { admitMultiple } from '../../../lobby/actions.web';
|
|
||||||
import { getKnockingParticipants, getLobbyEnabled } from '../../../lobby/functions';
|
|
||||||
|
|
||||||
import { LobbyParticipantItem } from './LobbyParticipantItem';
|
|
||||||
|
|
||||||
const useStyles = makeStyles(theme => {
|
|
||||||
return {
|
|
||||||
headingContainer: {
|
|
||||||
alignItems: 'center',
|
|
||||||
display: 'flex',
|
|
||||||
justifyContent: 'space-between'
|
|
||||||
},
|
|
||||||
heading: {
|
|
||||||
...withPixelLineHeight(theme.typography.heading7),
|
|
||||||
color: theme.palette.text02
|
|
||||||
},
|
|
||||||
link: {
|
|
||||||
...withPixelLineHeight(theme.typography.labelBold),
|
|
||||||
color: theme.palette.link01,
|
|
||||||
cursor: 'pointer'
|
|
||||||
}
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
export const LobbyParticipantList = () => {
|
|
||||||
const lobbyEnabled = useSelector(getLobbyEnabled);
|
|
||||||
const participants = useSelector(getKnockingParticipants);
|
|
||||||
|
|
||||||
const { t } = useTranslation();
|
|
||||||
const classes = useStyles();
|
|
||||||
const dispatch = useDispatch();
|
|
||||||
const admitAll = useCallback(() => {
|
|
||||||
dispatch(admitMultiple(participants));
|
|
||||||
}, [ dispatch, participants ]);
|
|
||||||
|
|
||||||
if (!lobbyEnabled || !participants.length) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<div className = { classes.headingContainer }>
|
|
||||||
<div className = { classes.heading }>
|
|
||||||
{t('participantsPane.headings.lobby', { count: participants.length })}
|
|
||||||
</div>
|
|
||||||
{
|
|
||||||
participants.length > 1 && (
|
|
||||||
<div
|
|
||||||
className = { classes.link }
|
|
||||||
onClick = { admitAll }>{t('lobby.admitAll')}</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
{participants.map(p => (
|
|
||||||
<LobbyParticipantItem
|
|
||||||
key = { p.id }
|
|
||||||
participant = { p } />)
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
};
|
|
|
@ -0,0 +1,134 @@
|
||||||
|
// @flow
|
||||||
|
|
||||||
|
import { makeStyles } from '@material-ui/core/styles';
|
||||||
|
import React, { useCallback } from 'react';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import { useSelector, useDispatch } from 'react-redux';
|
||||||
|
|
||||||
|
import { Avatar } from '../../../base/avatar';
|
||||||
|
import { Icon, IconCheck, IconClose } from '../../../base/icons';
|
||||||
|
import { withPixelLineHeight } from '../../../base/styles/functions.web';
|
||||||
|
import { admitMultiple } from '../../../lobby/actions.web';
|
||||||
|
import { getLobbyEnabled, getKnockingParticipants } from '../../../lobby/functions';
|
||||||
|
import { Drawer, DrawerPortal } from '../../../toolbox/components/web';
|
||||||
|
import { showOverflowDrawer } from '../../../toolbox/functions';
|
||||||
|
import { useLobbyActions, useParticipantDrawer } from '../../hooks';
|
||||||
|
|
||||||
|
import LobbyParticipantItems from './LobbyParticipantItems';
|
||||||
|
|
||||||
|
const useStyles = makeStyles(theme => {
|
||||||
|
return {
|
||||||
|
drawerActions: {
|
||||||
|
listStyleType: 'none',
|
||||||
|
margin: 0,
|
||||||
|
padding: 0
|
||||||
|
},
|
||||||
|
drawerItem: {
|
||||||
|
alignItems: 'center',
|
||||||
|
color: theme.palette.text01,
|
||||||
|
display: 'flex',
|
||||||
|
padding: '12px 16px',
|
||||||
|
...withPixelLineHeight(theme.typography.bodyShortRegularLarge),
|
||||||
|
|
||||||
|
'&:first-child': {
|
||||||
|
marginTop: '15px'
|
||||||
|
|
||||||
|
},
|
||||||
|
|
||||||
|
'&:hover': {
|
||||||
|
cursor: 'pointer',
|
||||||
|
background: theme.palette.action02
|
||||||
|
}
|
||||||
|
},
|
||||||
|
icon: {
|
||||||
|
marginRight: 16
|
||||||
|
},
|
||||||
|
headingContainer: {
|
||||||
|
alignItems: 'center',
|
||||||
|
display: 'flex',
|
||||||
|
justifyContent: 'space-between'
|
||||||
|
},
|
||||||
|
heading: {
|
||||||
|
...withPixelLineHeight(theme.typography.heading7),
|
||||||
|
color: theme.palette.text02
|
||||||
|
},
|
||||||
|
link: {
|
||||||
|
...withPixelLineHeight(theme.typography.labelBold),
|
||||||
|
color: theme.palette.link01,
|
||||||
|
cursor: 'pointer'
|
||||||
|
}
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Component used to display a list of participants waiting in the lobby.
|
||||||
|
*
|
||||||
|
* @returns {ReactNode}
|
||||||
|
*/
|
||||||
|
export default function LobbyParticipants() {
|
||||||
|
const lobbyEnabled = useSelector(getLobbyEnabled);
|
||||||
|
const participants = useSelector(getKnockingParticipants);
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const classes = useStyles();
|
||||||
|
const dispatch = useDispatch();
|
||||||
|
const admitAll = useCallback(() => {
|
||||||
|
dispatch(admitMultiple(participants));
|
||||||
|
}, [ dispatch, participants ]);
|
||||||
|
const overflowDrawer = useSelector(showOverflowDrawer);
|
||||||
|
const [ drawerParticipant, closeDrawer, openDrawerForParticipant ] = useParticipantDrawer();
|
||||||
|
const [ admit, reject ] = useLobbyActions(drawerParticipant, closeDrawer);
|
||||||
|
|
||||||
|
if (!lobbyEnabled || !participants.length) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div className = { classes.headingContainer }>
|
||||||
|
<div className = { classes.heading }>
|
||||||
|
{t('participantsPane.headings.lobby', { count: participants.length })}
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
className = { classes.link }
|
||||||
|
onClick = { admitAll }>{t('lobby.admitAll')}</div>
|
||||||
|
</div>
|
||||||
|
<LobbyParticipantItems
|
||||||
|
openDrawerForParticipant = { openDrawerForParticipant }
|
||||||
|
overflowDrawer = { overflowDrawer }
|
||||||
|
participants = { participants } />
|
||||||
|
<DrawerPortal>
|
||||||
|
<Drawer
|
||||||
|
isOpen = { Boolean(drawerParticipant && overflowDrawer) }
|
||||||
|
onClose = { closeDrawer }>
|
||||||
|
<ul className = { classes.drawerActions }>
|
||||||
|
<li className = { classes.drawerItem }>
|
||||||
|
<Avatar
|
||||||
|
className = { classes.icon }
|
||||||
|
participantId = { drawerParticipant && drawerParticipant.participantID }
|
||||||
|
size = { 20 } />
|
||||||
|
<span>{ drawerParticipant && drawerParticipant.displayName }</span>
|
||||||
|
</li>
|
||||||
|
<li
|
||||||
|
className = { classes.drawerItem }
|
||||||
|
onClick = { admit }>
|
||||||
|
<Icon
|
||||||
|
className = { classes.icon }
|
||||||
|
size = { 20 }
|
||||||
|
src = { IconCheck } />
|
||||||
|
<span>{ t('lobby.admit') }</span>
|
||||||
|
</li>
|
||||||
|
<li
|
||||||
|
className = { classes.drawerItem }
|
||||||
|
onClick = { reject }>
|
||||||
|
<Icon
|
||||||
|
className = { classes.icon }
|
||||||
|
size = { 20 }
|
||||||
|
src = { IconClose } />
|
||||||
|
<span>{ t('lobby.reject')}</span>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</Drawer>
|
||||||
|
</DrawerPortal>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
|
@ -1,7 +1,8 @@
|
||||||
// @flow
|
// @flow
|
||||||
|
import { withStyles } from '@material-ui/core/styles';
|
||||||
import React, { Component } from 'react';
|
import React, { Component } from 'react';
|
||||||
|
|
||||||
|
import { Avatar } from '../../../base/avatar';
|
||||||
import { isToolbarButtonEnabled } from '../../../base/config/functions.web';
|
import { isToolbarButtonEnabled } from '../../../base/config/functions.web';
|
||||||
import { openDialog } from '../../../base/dialog';
|
import { openDialog } from '../../../base/dialog';
|
||||||
import { translate } from '../../../base/i18n';
|
import { translate } from '../../../base/i18n';
|
||||||
|
@ -21,10 +22,13 @@ import {
|
||||||
isParticipantModerator
|
isParticipantModerator
|
||||||
} from '../../../base/participants';
|
} from '../../../base/participants';
|
||||||
import { connect } from '../../../base/redux';
|
import { connect } from '../../../base/redux';
|
||||||
|
import { withPixelLineHeight } from '../../../base/styles/functions.web';
|
||||||
import { isParticipantAudioMuted, isParticipantVideoMuted } from '../../../base/tracks';
|
import { isParticipantAudioMuted, isParticipantVideoMuted } from '../../../base/tracks';
|
||||||
import { openChat } from '../../../chat/actions';
|
import { openChatById } from '../../../chat/actions';
|
||||||
import { stopSharedVideo } from '../../../shared-video/actions.any';
|
import { setVolume } from '../../../filmstrip/actions.web';
|
||||||
|
import { Drawer, DrawerPortal } from '../../../toolbox/components/web';
|
||||||
import { GrantModeratorDialog, KickRemoteParticipantDialog, MuteEveryoneDialog } from '../../../video-menu';
|
import { GrantModeratorDialog, KickRemoteParticipantDialog, MuteEveryoneDialog } from '../../../video-menu';
|
||||||
|
import { VolumeSlider } from '../../../video-menu/components/web';
|
||||||
import MuteRemoteParticipantsVideoDialog from '../../../video-menu/components/web/MuteRemoteParticipantsVideoDialog';
|
import MuteRemoteParticipantsVideoDialog from '../../../video-menu/components/web/MuteRemoteParticipantsVideoDialog';
|
||||||
import { getComputedOuterHeight } from '../../functions';
|
import { getComputedOuterHeight } from '../../functions';
|
||||||
|
|
||||||
|
@ -73,11 +77,33 @@ type Props = {
|
||||||
*/
|
*/
|
||||||
_participant: Object,
|
_participant: Object,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A value between 0 and 1 indicating the volume of the participant's
|
||||||
|
* audio element.
|
||||||
|
*/
|
||||||
|
_volume: ?number,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Closes a drawer if open.
|
||||||
|
*/
|
||||||
|
closeDrawer: Function,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An object containing the CSS classes.
|
||||||
|
*/
|
||||||
|
classes?: {[ key: string]: string},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The dispatch function from redux.
|
* The dispatch function from redux.
|
||||||
*/
|
*/
|
||||||
dispatch: Function,
|
dispatch: Function,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The participant for which the drawer is open.
|
||||||
|
* It contains the displayName & participantID.
|
||||||
|
*/
|
||||||
|
drawerParticipant: Object,
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Callback used to open a confirmation dialog for audio muting.
|
* Callback used to open a confirmation dialog for audio muting.
|
||||||
*/
|
*/
|
||||||
|
@ -108,6 +134,12 @@ type Props = {
|
||||||
*/
|
*/
|
||||||
participantID: string,
|
participantID: string,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* True if an overflow drawer should be displayed.
|
||||||
|
*/
|
||||||
|
overflowDrawer: boolean,
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The translate function.
|
* The translate function.
|
||||||
*/
|
*/
|
||||||
|
@ -122,6 +154,25 @@ type State = {
|
||||||
isHidden: boolean
|
isHidden: boolean
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const styles = theme => {
|
||||||
|
return {
|
||||||
|
drawer: {
|
||||||
|
'& > div': {
|
||||||
|
...withPixelLineHeight(theme.typography.bodyShortRegularLarge),
|
||||||
|
lineHeight: '32px',
|
||||||
|
|
||||||
|
'& svg': {
|
||||||
|
fill: theme.palette.icon01
|
||||||
|
}
|
||||||
|
},
|
||||||
|
'&:first-child': {
|
||||||
|
marginTop: 15
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Implements the MeetingParticipantContextMenu component.
|
* Implements the MeetingParticipantContextMenu component.
|
||||||
*/
|
*/
|
||||||
|
@ -146,13 +197,27 @@ class MeetingParticipantContextMenu extends Component<Props, State> {
|
||||||
|
|
||||||
this._containerRef = React.createRef();
|
this._containerRef = React.createRef();
|
||||||
|
|
||||||
|
this._getCurrentParticipantId = this._getCurrentParticipantId.bind(this);
|
||||||
this._onGrantModerator = this._onGrantModerator.bind(this);
|
this._onGrantModerator = this._onGrantModerator.bind(this);
|
||||||
this._onKick = this._onKick.bind(this);
|
this._onKick = this._onKick.bind(this);
|
||||||
this._onMuteEveryoneElse = this._onMuteEveryoneElse.bind(this);
|
this._onMuteEveryoneElse = this._onMuteEveryoneElse.bind(this);
|
||||||
this._onMuteVideo = this._onMuteVideo.bind(this);
|
this._onMuteVideo = this._onMuteVideo.bind(this);
|
||||||
this._onSendPrivateMessage = this._onSendPrivateMessage.bind(this);
|
this._onSendPrivateMessage = this._onSendPrivateMessage.bind(this);
|
||||||
this._onStopSharedVideo = this._onStopSharedVideo.bind(this);
|
|
||||||
this._position = this._position.bind(this);
|
this._position = this._position.bind(this);
|
||||||
|
this._onVolumeChange = this._onVolumeChange.bind(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
_getCurrentParticipantId: () => string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the participant id for the item we want to operate.
|
||||||
|
*
|
||||||
|
* @returns {void}
|
||||||
|
*/
|
||||||
|
_getCurrentParticipantId() {
|
||||||
|
const { _participant, drawerParticipant, overflowDrawer } = this.props;
|
||||||
|
|
||||||
|
return overflowDrawer ? drawerParticipant?.participantID : _participant?.id;
|
||||||
}
|
}
|
||||||
|
|
||||||
_onGrantModerator: () => void;
|
_onGrantModerator: () => void;
|
||||||
|
@ -163,10 +228,8 @@ class MeetingParticipantContextMenu extends Component<Props, State> {
|
||||||
* @returns {void}
|
* @returns {void}
|
||||||
*/
|
*/
|
||||||
_onGrantModerator() {
|
_onGrantModerator() {
|
||||||
const { _participant, dispatch } = this.props;
|
this.props.dispatch(openDialog(GrantModeratorDialog, {
|
||||||
|
participantID: this._getCurrentParticipantId()
|
||||||
dispatch(openDialog(GrantModeratorDialog, {
|
|
||||||
participantID: _participant?.id
|
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -178,10 +241,8 @@ class MeetingParticipantContextMenu extends Component<Props, State> {
|
||||||
* @returns {void}
|
* @returns {void}
|
||||||
*/
|
*/
|
||||||
_onKick() {
|
_onKick() {
|
||||||
const { _participant, dispatch } = this.props;
|
this.props.dispatch(openDialog(KickRemoteParticipantDialog, {
|
||||||
|
participantID: this._getCurrentParticipantId()
|
||||||
dispatch(openDialog(KickRemoteParticipantDialog, {
|
|
||||||
participantID: _participant?.id
|
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -195,7 +256,7 @@ class MeetingParticipantContextMenu extends Component<Props, State> {
|
||||||
_onStopSharedVideo() {
|
_onStopSharedVideo() {
|
||||||
const { dispatch } = this.props;
|
const { dispatch } = this.props;
|
||||||
|
|
||||||
dispatch(stopSharedVideo());
|
dispatch(this._onStopSharedVideo());
|
||||||
}
|
}
|
||||||
|
|
||||||
_onMuteEveryoneElse: () => void;
|
_onMuteEveryoneElse: () => void;
|
||||||
|
@ -206,10 +267,8 @@ class MeetingParticipantContextMenu extends Component<Props, State> {
|
||||||
* @returns {void}
|
* @returns {void}
|
||||||
*/
|
*/
|
||||||
_onMuteEveryoneElse() {
|
_onMuteEveryoneElse() {
|
||||||
const { _participant, dispatch } = this.props;
|
this.props.dispatch(openDialog(MuteEveryoneDialog, {
|
||||||
|
exclude: [ this._getCurrentParticipantId() ]
|
||||||
dispatch(openDialog(MuteEveryoneDialog, {
|
|
||||||
exclude: [ _participant?.id ]
|
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -221,10 +280,8 @@ class MeetingParticipantContextMenu extends Component<Props, State> {
|
||||||
* @returns {void}
|
* @returns {void}
|
||||||
*/
|
*/
|
||||||
_onMuteVideo() {
|
_onMuteVideo() {
|
||||||
const { _participant, dispatch } = this.props;
|
this.props.dispatch(openDialog(MuteRemoteParticipantsVideoDialog, {
|
||||||
|
participantID: this._getCurrentParticipantId()
|
||||||
dispatch(openDialog(MuteRemoteParticipantsVideoDialog, {
|
|
||||||
participantID: _participant?.id
|
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -236,9 +293,10 @@ class MeetingParticipantContextMenu extends Component<Props, State> {
|
||||||
* @returns {void}
|
* @returns {void}
|
||||||
*/
|
*/
|
||||||
_onSendPrivateMessage() {
|
_onSendPrivateMessage() {
|
||||||
const { _participant, dispatch } = this.props;
|
const { closeDrawer, dispatch, overflowDrawer } = this.props;
|
||||||
|
|
||||||
dispatch(openChat(_participant));
|
dispatch(openChatById(this._getCurrentParticipantId()));
|
||||||
|
overflowDrawer && closeDrawer();
|
||||||
}
|
}
|
||||||
|
|
||||||
_position: () => void;
|
_position: () => void;
|
||||||
|
@ -270,6 +328,21 @@ class MeetingParticipantContextMenu extends Component<Props, State> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_onVolumeChange: (number) => void;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handles volume changes.
|
||||||
|
*
|
||||||
|
* @param {number} value - The new value for the volume.
|
||||||
|
* @returns {void}
|
||||||
|
*/
|
||||||
|
_onVolumeChange(value) {
|
||||||
|
const { _participant, dispatch } = this.props;
|
||||||
|
const { id } = _participant;
|
||||||
|
|
||||||
|
dispatch(setVolume(id, value));
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Implements React Component's componentDidMount.
|
* Implements React Component's componentDidMount.
|
||||||
*
|
*
|
||||||
|
@ -306,9 +379,14 @@ class MeetingParticipantContextMenu extends Component<Props, State> {
|
||||||
_isParticipantAudioMuted,
|
_isParticipantAudioMuted,
|
||||||
_localVideoOwner,
|
_localVideoOwner,
|
||||||
_participant,
|
_participant,
|
||||||
|
_volume = 1,
|
||||||
|
classes,
|
||||||
|
closeDrawer,
|
||||||
|
drawerParticipant,
|
||||||
onEnter,
|
onEnter,
|
||||||
onLeave,
|
onLeave,
|
||||||
onSelect,
|
onSelect,
|
||||||
|
overflowDrawer,
|
||||||
muteAudio,
|
muteAudio,
|
||||||
t
|
t
|
||||||
} = this.props;
|
} = this.props;
|
||||||
|
@ -317,90 +395,116 @@ class MeetingParticipantContextMenu extends Component<Props, State> {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
const actions
|
||||||
<ContextMenu
|
= _participant.isFakeParticipant ? (
|
||||||
className = { ignoredChildClassName }
|
<>
|
||||||
innerRef = { this._containerRef }
|
{_localVideoOwner && (
|
||||||
isHidden = { this.state.isHidden }
|
|
||||||
onClick = { onSelect }
|
|
||||||
onMouseEnter = { onEnter }
|
|
||||||
onMouseLeave = { onLeave }>
|
|
||||||
{
|
|
||||||
!_participant.isFakeParticipant && (
|
|
||||||
<>
|
|
||||||
<ContextMenuItemGroup>
|
|
||||||
{
|
|
||||||
_isLocalModerator && (
|
|
||||||
<>
|
|
||||||
{
|
|
||||||
!_isParticipantAudioMuted
|
|
||||||
&& <ContextMenuItem onClick = { muteAudio(_participant) }>
|
|
||||||
<ContextMenuIcon src = { IconMicDisabled } />
|
|
||||||
<span>{t('dialog.muteParticipantButton')}</span>
|
|
||||||
</ContextMenuItem>
|
|
||||||
}
|
|
||||||
|
|
||||||
<ContextMenuItem onClick = { this._onMuteEveryoneElse }>
|
|
||||||
<ContextMenuIcon src = { IconMuteEveryoneElse } />
|
|
||||||
<span>{t('toolbar.accessibilityLabel.muteEveryoneElse')}</span>
|
|
||||||
</ContextMenuItem>
|
|
||||||
</>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
{
|
|
||||||
_isLocalModerator && (
|
|
||||||
_isParticipantVideoMuted || (
|
|
||||||
<ContextMenuItem onClick = { this._onMuteVideo }>
|
|
||||||
<ContextMenuIcon src = { IconVideoOff } />
|
|
||||||
<span>{t('participantsPane.actions.stopVideo')}</span>
|
|
||||||
</ContextMenuItem>
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
</ContextMenuItemGroup>
|
|
||||||
|
|
||||||
<ContextMenuItemGroup>
|
|
||||||
{
|
|
||||||
_isLocalModerator && (
|
|
||||||
<>
|
|
||||||
{
|
|
||||||
!_isParticipantModerator && (
|
|
||||||
<ContextMenuItem onClick = { this._onGrantModerator }>
|
|
||||||
<ContextMenuIcon src = { IconCrown } />
|
|
||||||
<span>{t('toolbar.accessibilityLabel.grantModerator')}</span>
|
|
||||||
</ContextMenuItem>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
<ContextMenuItem onClick = { this._onKick }>
|
|
||||||
<ContextMenuIcon src = { IconCloseCircle } />
|
|
||||||
<span>{ t('videothumbnail.kick') }</span>
|
|
||||||
</ContextMenuItem>
|
|
||||||
</>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
{
|
|
||||||
_isChatButtonEnabled && (
|
|
||||||
<ContextMenuItem onClick = { this._onSendPrivateMessage }>
|
|
||||||
<ContextMenuIcon src = { IconMessage } />
|
|
||||||
<span>{t('toolbar.accessibilityLabel.privateMessage')}</span>
|
|
||||||
</ContextMenuItem>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
</ContextMenuItemGroup>
|
|
||||||
</>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
{
|
|
||||||
_participant.isFakeParticipant && _localVideoOwner && (
|
|
||||||
<ContextMenuItem onClick = { this._onStopSharedVideo }>
|
<ContextMenuItem onClick = { this._onStopSharedVideo }>
|
||||||
<ContextMenuIcon src = { IconShareVideo } />
|
<ContextMenuIcon src = { IconShareVideo } />
|
||||||
<span>{t('toolbar.stopSharedVideo')}</span>
|
<span>{t('toolbar.stopSharedVideo')}</span>
|
||||||
</ContextMenuItem>
|
</ContextMenuItem>
|
||||||
)
|
)}
|
||||||
}
|
</>
|
||||||
</ContextMenu>
|
) : (
|
||||||
|
<>
|
||||||
|
{_isLocalModerator && (
|
||||||
|
<ContextMenuItemGroup>
|
||||||
|
<>
|
||||||
|
{
|
||||||
|
!_isParticipantAudioMuted && overflowDrawer
|
||||||
|
&& <ContextMenuItem onClick = { muteAudio(_participant) }>
|
||||||
|
<ContextMenuIcon src = { IconMicDisabled } />
|
||||||
|
<span>{t('dialog.muteParticipantButton')}</span>
|
||||||
|
</ContextMenuItem>
|
||||||
|
}
|
||||||
|
|
||||||
|
<ContextMenuItem onClick = { this._onMuteEveryoneElse }>
|
||||||
|
<ContextMenuIcon src = { IconMuteEveryoneElse } />
|
||||||
|
<span>{t('toolbar.accessibilityLabel.muteEveryoneElse')}</span>
|
||||||
|
</ContextMenuItem>
|
||||||
|
</>
|
||||||
|
|
||||||
|
{
|
||||||
|
_isParticipantVideoMuted || (
|
||||||
|
<ContextMenuItem onClick = { this._onMuteVideo }>
|
||||||
|
<ContextMenuIcon src = { IconVideoOff } />
|
||||||
|
<span>{t('participantsPane.actions.stopVideo')}</span>
|
||||||
|
</ContextMenuItem>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
</ContextMenuItemGroup>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<ContextMenuItemGroup>
|
||||||
|
{
|
||||||
|
_isLocalModerator && (
|
||||||
|
<>
|
||||||
|
{
|
||||||
|
!_isParticipantModerator && (
|
||||||
|
<ContextMenuItem onClick = { this._onGrantModerator }>
|
||||||
|
<ContextMenuIcon src = { IconCrown } />
|
||||||
|
<span>{t('toolbar.accessibilityLabel.grantModerator')}</span>
|
||||||
|
</ContextMenuItem>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
<ContextMenuItem onClick = { this._onKick }>
|
||||||
|
<ContextMenuIcon src = { IconCloseCircle } />
|
||||||
|
<span>{ t('videothumbnail.kick') }</span>
|
||||||
|
</ContextMenuItem>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
{
|
||||||
|
_isChatButtonEnabled && (
|
||||||
|
<ContextMenuItem onClick = { this._onSendPrivateMessage }>
|
||||||
|
<ContextMenuIcon src = { IconMessage } />
|
||||||
|
<span>{t('toolbar.accessibilityLabel.privateMessage')}</span>
|
||||||
|
</ContextMenuItem>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
</ContextMenuItemGroup>
|
||||||
|
{ overflowDrawer && typeof _volume === 'number' && !isNaN(_volume)
|
||||||
|
&& <ContextMenuItemGroup>
|
||||||
|
<VolumeSlider
|
||||||
|
initialValue = { _volume }
|
||||||
|
key = 'volume-slider'
|
||||||
|
onChange = { this._onVolumeChange } />
|
||||||
|
</ContextMenuItemGroup>
|
||||||
|
}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{ !overflowDrawer
|
||||||
|
&& <ContextMenu
|
||||||
|
className = { ignoredChildClassName }
|
||||||
|
innerRef = { this._containerRef }
|
||||||
|
isHidden = { this.state.isHidden }
|
||||||
|
onClick = { onSelect }
|
||||||
|
onMouseEnter = { onEnter }
|
||||||
|
onMouseLeave = { onLeave }>
|
||||||
|
{ actions }
|
||||||
|
</ContextMenu>}
|
||||||
|
|
||||||
|
<DrawerPortal>
|
||||||
|
<Drawer
|
||||||
|
isOpen = { drawerParticipant && overflowDrawer }
|
||||||
|
onClose = { closeDrawer }>
|
||||||
|
<div className = { classes && classes.drawer }>
|
||||||
|
<ContextMenuItemGroup>
|
||||||
|
<ContextMenuItem>
|
||||||
|
<Avatar
|
||||||
|
participantId = { drawerParticipant && drawerParticipant.participantID }
|
||||||
|
size = { 20 } />
|
||||||
|
<span>{ drawerParticipant && drawerParticipant.displayName }</span>
|
||||||
|
</ContextMenuItem>
|
||||||
|
</ContextMenuItemGroup>
|
||||||
|
{ actions }
|
||||||
|
</div>
|
||||||
|
</Drawer>
|
||||||
|
</DrawerPortal>
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -414,10 +518,12 @@ class MeetingParticipantContextMenu extends Component<Props, State> {
|
||||||
* @returns {Props}
|
* @returns {Props}
|
||||||
*/
|
*/
|
||||||
function _mapStateToProps(state, ownProps): Object {
|
function _mapStateToProps(state, ownProps): Object {
|
||||||
const { participantID } = ownProps;
|
const { participantID, overflowDrawer, drawerParticipant } = ownProps;
|
||||||
const { ownerId } = state['features/shared-video'];
|
const { ownerId } = state['features/shared-video'];
|
||||||
const localParticipantId = getLocalParticipant(state).id;
|
const localParticipantId = getLocalParticipant(state).id;
|
||||||
const participant = getParticipantByIdOrUndefined(state, participantID);
|
|
||||||
|
const participant = getParticipantByIdOrUndefined(state,
|
||||||
|
overflowDrawer ? drawerParticipant?.participantID : participantID);
|
||||||
|
|
||||||
const _isLocalModerator = isLocalParticipantModerator(state);
|
const _isLocalModerator = isLocalParticipantModerator(state);
|
||||||
const _isChatButtonEnabled = isToolbarButtonEnabled('chat', state);
|
const _isChatButtonEnabled = isToolbarButtonEnabled('chat', state);
|
||||||
|
@ -425,6 +531,10 @@ function _mapStateToProps(state, ownProps): Object {
|
||||||
const _isParticipantAudioMuted = isParticipantAudioMuted(participant, state);
|
const _isParticipantAudioMuted = isParticipantAudioMuted(participant, state);
|
||||||
const _isParticipantModerator = isParticipantModerator(participant);
|
const _isParticipantModerator = isParticipantModerator(participant);
|
||||||
|
|
||||||
|
const { participantsVolume } = state['features/filmstrip'];
|
||||||
|
const id = participant?.id;
|
||||||
|
const isLocal = participant?.local ?? true;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
_isLocalModerator,
|
_isLocalModerator,
|
||||||
_isChatButtonEnabled,
|
_isChatButtonEnabled,
|
||||||
|
@ -432,8 +542,9 @@ function _mapStateToProps(state, ownProps): Object {
|
||||||
_isParticipantVideoMuted,
|
_isParticipantVideoMuted,
|
||||||
_isParticipantAudioMuted,
|
_isParticipantAudioMuted,
|
||||||
_localVideoOwner: Boolean(ownerId === localParticipantId),
|
_localVideoOwner: Boolean(ownerId === localParticipantId),
|
||||||
_participant: participant
|
_participant: participant,
|
||||||
|
_volume: isLocal ? undefined : id ? participantsVolume[id] : undefined
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export default translate(connect(_mapStateToProps)(MeetingParticipantContextMenu));
|
export default withStyles(styles)(translate(connect(_mapStateToProps)(MeetingParticipantContextMenu)));
|
||||||
|
|
|
@ -9,8 +9,12 @@ import {
|
||||||
} from '../../../base/participants';
|
} from '../../../base/participants';
|
||||||
import { connect } from '../../../base/redux';
|
import { connect } from '../../../base/redux';
|
||||||
import { isParticipantAudioMuted, isParticipantVideoMuted } from '../../../base/tracks';
|
import { isParticipantAudioMuted, isParticipantVideoMuted } from '../../../base/tracks';
|
||||||
import { ACTION_TRIGGER, MEDIA_STATE, type MediaState } from '../../constants';
|
import { ACTION_TRIGGER, type MediaState } from '../../constants';
|
||||||
import { getParticipantAudioMediaState, getQuickActionButtonType } from '../../functions';
|
import {
|
||||||
|
getParticipantAudioMediaState,
|
||||||
|
getParticipantVideoMediaState,
|
||||||
|
getQuickActionButtonType
|
||||||
|
} from '../../functions';
|
||||||
import ParticipantQuickAction from '../ParticipantQuickAction';
|
import ParticipantQuickAction from '../ParticipantQuickAction';
|
||||||
|
|
||||||
import ParticipantItem from './ParticipantItem';
|
import ParticipantItem from './ParticipantItem';
|
||||||
|
@ -23,20 +27,21 @@ type Props = {
|
||||||
*/
|
*/
|
||||||
_audioMediaState: MediaState,
|
_audioMediaState: MediaState,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Media state for video.
|
||||||
|
*/
|
||||||
|
_videoMediaState: MediaState,
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The display name of the participant.
|
* The display name of the participant.
|
||||||
*/
|
*/
|
||||||
_displayName: string,
|
_displayName: string,
|
||||||
|
|
||||||
/**
|
|
||||||
* True if the participant is video muted.
|
|
||||||
*/
|
|
||||||
_isVideoMuted: boolean,
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* True if the participant is the local participant.
|
* True if the participant is the local participant.
|
||||||
*/
|
*/
|
||||||
_local: boolean,
|
_local: Boolean,
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Shared video local participant owner.
|
* Shared video local participant owner.
|
||||||
|
@ -96,6 +101,17 @@ type Props = {
|
||||||
*/
|
*/
|
||||||
onLeave: Function,
|
onLeave: Function,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Callback used to open an actions drawer for a participant.
|
||||||
|
*/
|
||||||
|
openDrawerForParticipant: Function,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* True if an overflow drawer should be displayed.
|
||||||
|
*/
|
||||||
|
overflowDrawer: boolean,
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The aria-label for the ellipsis action.
|
* The aria-label for the ellipsis action.
|
||||||
*/
|
*/
|
||||||
|
@ -120,20 +136,22 @@ type Props = {
|
||||||
*/
|
*/
|
||||||
function MeetingParticipantItem({
|
function MeetingParticipantItem({
|
||||||
_audioMediaState,
|
_audioMediaState,
|
||||||
|
_videoMediaState,
|
||||||
_displayName,
|
_displayName,
|
||||||
_isVideoMuted,
|
|
||||||
_localVideoOwner,
|
|
||||||
_local,
|
_local,
|
||||||
|
_localVideoOwner,
|
||||||
_participant,
|
_participant,
|
||||||
_participantID,
|
_participantID,
|
||||||
_quickActionButtonType,
|
_quickActionButtonType,
|
||||||
_raisedHand,
|
_raisedHand,
|
||||||
askUnmuteText,
|
askUnmuteText,
|
||||||
isHighlighted,
|
isHighlighted,
|
||||||
onContextMenu,
|
|
||||||
onLeave,
|
|
||||||
muteAudio,
|
muteAudio,
|
||||||
muteParticipantButtonText,
|
muteParticipantButtonText,
|
||||||
|
onContextMenu,
|
||||||
|
onLeave,
|
||||||
|
openDrawerForParticipant,
|
||||||
|
overflowDrawer,
|
||||||
participantActionEllipsisLabel,
|
participantActionEllipsisLabel,
|
||||||
youText
|
youText
|
||||||
}: Props) {
|
}: Props) {
|
||||||
|
@ -145,32 +163,32 @@ function MeetingParticipantItem({
|
||||||
isHighlighted = { isHighlighted }
|
isHighlighted = { isHighlighted }
|
||||||
local = { _local }
|
local = { _local }
|
||||||
onLeave = { onLeave }
|
onLeave = { onLeave }
|
||||||
|
openDrawerForParticipant = { openDrawerForParticipant }
|
||||||
|
overflowDrawer = { overflowDrawer }
|
||||||
participantID = { _participantID }
|
participantID = { _participantID }
|
||||||
raisedHand = { _raisedHand }
|
raisedHand = { _raisedHand }
|
||||||
videoMuteState = { _isVideoMuted ? MEDIA_STATE.MUTED : MEDIA_STATE.UNMUTED }
|
videoMediaState = { _videoMediaState }
|
||||||
youText = { youText }>
|
youText = { youText }>
|
||||||
{
|
|
||||||
!_participant.isFakeParticipant && (
|
{!overflowDrawer && !_participant.isFakeParticipant
|
||||||
<>
|
&& <>
|
||||||
<ParticipantQuickAction
|
<ParticipantQuickAction
|
||||||
askUnmuteText = { askUnmuteText }
|
askUnmuteText = { askUnmuteText }
|
||||||
buttonType = { _quickActionButtonType }
|
buttonType = { _quickActionButtonType }
|
||||||
muteAudio = { muteAudio }
|
muteAudio = { muteAudio }
|
||||||
muteParticipantButtonText = { muteParticipantButtonText }
|
muteParticipantButtonText = { muteParticipantButtonText }
|
||||||
participantID = { _participantID } />
|
participantID = { _participantID } />
|
||||||
<ParticipantActionEllipsis
|
|
||||||
aria-label = { participantActionEllipsisLabel }
|
|
||||||
onClick = { onContextMenu } />
|
|
||||||
</>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
{
|
|
||||||
_participant.isFakeParticipant && _localVideoOwner && (
|
|
||||||
<ParticipantActionEllipsis
|
<ParticipantActionEllipsis
|
||||||
aria-label = { participantActionEllipsisLabel }
|
aria-label = { participantActionEllipsisLabel }
|
||||||
onClick = { onContextMenu } />
|
onClick = { onContextMenu } />
|
||||||
)
|
</>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
{!overflowDrawer && _localVideoOwner && _participant.isFakeParticipant && (
|
||||||
|
<ParticipantActionEllipsis
|
||||||
|
aria-label = { participantActionEllipsisLabel }
|
||||||
|
onClick = { onContextMenu } />
|
||||||
|
)}
|
||||||
</ParticipantItem>
|
</ParticipantItem>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -193,13 +211,13 @@ function _mapStateToProps(state, ownProps): Object {
|
||||||
const _isAudioMuted = isParticipantAudioMuted(participant, state);
|
const _isAudioMuted = isParticipantAudioMuted(participant, state);
|
||||||
const _isVideoMuted = isParticipantVideoMuted(participant, state);
|
const _isVideoMuted = isParticipantVideoMuted(participant, state);
|
||||||
const _audioMediaState = getParticipantAudioMediaState(participant, _isAudioMuted, state);
|
const _audioMediaState = getParticipantAudioMediaState(participant, _isAudioMuted, state);
|
||||||
|
const _videoMediaState = getParticipantVideoMediaState(participant, _isVideoMuted, state);
|
||||||
const _quickActionButtonType = getQuickActionButtonType(participant, _isAudioMuted, state);
|
const _quickActionButtonType = getQuickActionButtonType(participant, _isAudioMuted, state);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
_audioMediaState,
|
_audioMediaState,
|
||||||
|
_videoMediaState,
|
||||||
_displayName: getParticipantDisplayName(state, participant?.id),
|
_displayName: getParticipantDisplayName(state, participant?.id),
|
||||||
_isAudioMuted,
|
|
||||||
_isVideoMuted,
|
|
||||||
_local: Boolean(participant?.local),
|
_local: Boolean(participant?.local),
|
||||||
_localVideoOwner: Boolean(ownerId === localParticipantId),
|
_localVideoOwner: Boolean(ownerId === localParticipantId),
|
||||||
_participant: participant,
|
_participant: participant,
|
||||||
|
|
|
@ -0,0 +1,103 @@
|
||||||
|
// @flow
|
||||||
|
|
||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
import MeetingParticipantItem from './MeetingParticipantItem';
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The translated ask unmute text for the qiuck action buttons.
|
||||||
|
*/
|
||||||
|
askUnmuteText: string,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Callback for the mouse leaving this item
|
||||||
|
*/
|
||||||
|
lowerMenu: Function,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Callback for the activation of this item's context menu
|
||||||
|
*/
|
||||||
|
toggleMenu: Function,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Callback used to open a confirmation dialog for audio muting.
|
||||||
|
*/
|
||||||
|
muteAudio: Function,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The translated text for the mute participant button.
|
||||||
|
*/
|
||||||
|
muteParticipantButtonText: string,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The meeting participants.
|
||||||
|
*/
|
||||||
|
participantIds: Array<string>,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Callback used to open an actions drawer for a participant.
|
||||||
|
*/
|
||||||
|
openDrawerForParticipant: Function,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* True if an overflow drawer should be displayed.
|
||||||
|
*/
|
||||||
|
overflowDrawer: boolean,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The if of the participant for which the context menu should be open.
|
||||||
|
*/
|
||||||
|
raiseContextId?: string,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The aria-label for the ellipsis action.
|
||||||
|
*/
|
||||||
|
participantActionEllipsisLabel: string,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The translated "you" text.
|
||||||
|
*/
|
||||||
|
youText: string
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Component used to display a list of meeting participants.
|
||||||
|
*
|
||||||
|
* @returns {ReactNode}
|
||||||
|
*/
|
||||||
|
function MeetingParticipantItems({
|
||||||
|
askUnmuteText,
|
||||||
|
lowerMenu,
|
||||||
|
toggleMenu,
|
||||||
|
muteAudio,
|
||||||
|
muteParticipantButtonText,
|
||||||
|
participantIds,
|
||||||
|
openDrawerForParticipant,
|
||||||
|
overflowDrawer,
|
||||||
|
raiseContextId,
|
||||||
|
participantActionEllipsisLabel,
|
||||||
|
youText
|
||||||
|
}) {
|
||||||
|
const renderParticipant = id => (
|
||||||
|
<MeetingParticipantItem
|
||||||
|
askUnmuteText = { askUnmuteText }
|
||||||
|
isHighlighted = { raiseContextId === id }
|
||||||
|
key = { id }
|
||||||
|
muteAudio = { muteAudio }
|
||||||
|
muteParticipantButtonText = { muteParticipantButtonText }
|
||||||
|
onContextMenu = { toggleMenu(id) }
|
||||||
|
onLeave = { lowerMenu }
|
||||||
|
openDrawerForParticipant = { openDrawerForParticipant }
|
||||||
|
overflowDrawer = { overflowDrawer }
|
||||||
|
participantActionEllipsisLabel = { participantActionEllipsisLabel }
|
||||||
|
participantID = { id }
|
||||||
|
youText = { youText } />
|
||||||
|
);
|
||||||
|
|
||||||
|
return participantIds.map(renderParticipant);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Memoize the component in order to avoid rerender on drawer open/close.
|
||||||
|
export default React.memo<Props>(MeetingParticipantItems);
|
|
@ -5,36 +5,38 @@ import { useTranslation } from 'react-i18next';
|
||||||
import { useDispatch } from 'react-redux';
|
import { useDispatch } from 'react-redux';
|
||||||
|
|
||||||
import { isToolbarButtonEnabled } from '../../../base/config/functions.web';
|
import { isToolbarButtonEnabled } from '../../../base/config/functions.web';
|
||||||
import { openDialog } from '../../../base/dialog';
|
import { MEDIA_TYPE } from '../../../base/media';
|
||||||
import {
|
import {
|
||||||
getParticipantCountWithFake,
|
getParticipantCountWithFake,
|
||||||
getSortedParticipantIds
|
getSortedParticipantIds
|
||||||
} from '../../../base/participants';
|
} from '../../../base/participants';
|
||||||
import { connect } from '../../../base/redux';
|
import { connect } from '../../../base/redux';
|
||||||
import MuteRemoteParticipantDialog from '../../../video-menu/components/web/MuteRemoteParticipantDialog';
|
import { showOverflowDrawer } from '../../../toolbox/functions';
|
||||||
|
import { muteRemote } from '../../../video-menu/actions.any';
|
||||||
import { findStyledAncestor, shouldRenderInviteButton } from '../../functions';
|
import { findStyledAncestor, shouldRenderInviteButton } from '../../functions';
|
||||||
|
import { useParticipantDrawer } from '../../hooks';
|
||||||
|
|
||||||
import { InviteButton } from './InviteButton';
|
import { InviteButton } from './InviteButton';
|
||||||
import MeetingParticipantContextMenu from './MeetingParticipantContextMenu';
|
import MeetingParticipantContextMenu from './MeetingParticipantContextMenu';
|
||||||
import MeetingParticipantItem from './MeetingParticipantItem';
|
import MeetingParticipantItems from './MeetingParticipantItems';
|
||||||
import { Heading, ParticipantContainer } from './styled';
|
import { Heading, ParticipantContainer } from './styled';
|
||||||
|
|
||||||
type NullProto = {
|
type NullProto = {
|
||||||
[key: string]: any,
|
[key: string]: any,
|
||||||
__proto__: null
|
__proto__: null
|
||||||
};
|
};
|
||||||
|
|
||||||
type RaiseContext = NullProto | {|
|
type RaiseContext = NullProto | {|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Target elements against which positioning calculations are made.
|
* Target elements against which positioning calculations are made.
|
||||||
*/
|
*/
|
||||||
offsetTarget?: HTMLElement,
|
offsetTarget?: HTMLElement,
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The ID of the participant.
|
* The ID of the participant.
|
||||||
*/
|
*/
|
||||||
participantID?: String,
|
participantID ?: string,
|
||||||
|};
|
|};
|
||||||
|
|
||||||
const initialState = Object.freeze(Object.create(null));
|
const initialState = Object.freeze(Object.create(null));
|
||||||
|
@ -49,11 +51,11 @@ const initialState = Object.freeze(Object.create(null));
|
||||||
*
|
*
|
||||||
* @returns {ReactNode} - The component.
|
* @returns {ReactNode} - The component.
|
||||||
*/
|
*/
|
||||||
function MeetingParticipantList({ participantsCount, showInviteButton, sortedParticipantIds = [] }) {
|
function MeetingParticipants({ participantsCount, showInviteButton, overflowDrawer, sortedParticipantIds = [] }) {
|
||||||
const dispatch = useDispatch();
|
const dispatch = useDispatch();
|
||||||
const isMouseOverMenu = useRef(false);
|
const isMouseOverMenu = useRef(false);
|
||||||
|
|
||||||
const [ raiseContext, setRaiseContext ] = useState<RaiseContext>(initialState);
|
const [ raiseContext, setRaiseContext ] = useState < RaiseContext >(initialState);
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
const lowerMenu = useCallback(() => {
|
const lowerMenu = useCallback(() => {
|
||||||
|
@ -101,8 +103,9 @@ function MeetingParticipantList({ participantsCount, showInviteButton, sortedPar
|
||||||
}, [ lowerMenu ]);
|
}, [ lowerMenu ]);
|
||||||
|
|
||||||
const muteAudio = useCallback(id => () => {
|
const muteAudio = useCallback(id => () => {
|
||||||
dispatch(openDialog(MuteRemoteParticipantDialog, { participantID: id }));
|
dispatch(muteRemote(id, MEDIA_TYPE.AUDIO));
|
||||||
});
|
}, [ dispatch ]);
|
||||||
|
const [ drawerParticipant, closeDrawer, openDrawerForParticipant ] = useParticipantDrawer();
|
||||||
|
|
||||||
// FIXME:
|
// FIXME:
|
||||||
// It seems that useTranslation is not very scallable. Unmount 500 components that have the useTranslation hook is
|
// It seems that useTranslation is not very scallable. Unmount 500 components that have the useTranslation hook is
|
||||||
|
@ -115,34 +118,35 @@ function MeetingParticipantList({ participantsCount, showInviteButton, sortedPar
|
||||||
const askUnmuteText = t('participantsPane.actions.askUnmute');
|
const askUnmuteText = t('participantsPane.actions.askUnmute');
|
||||||
const muteParticipantButtonText = t('dialog.muteParticipantButton');
|
const muteParticipantButtonText = t('dialog.muteParticipantButton');
|
||||||
|
|
||||||
const renderParticipant = id => (
|
|
||||||
<MeetingParticipantItem
|
|
||||||
askUnmuteText = { askUnmuteText }
|
|
||||||
isHighlighted = { raiseContext.participantID === id }
|
|
||||||
key = { id }
|
|
||||||
muteAudio = { muteAudio }
|
|
||||||
muteParticipantButtonText = { muteParticipantButtonText }
|
|
||||||
onContextMenu = { toggleMenu(id) }
|
|
||||||
onLeave = { lowerMenu }
|
|
||||||
participantActionEllipsisLabel = { participantActionEllipsisLabel }
|
|
||||||
participantID = { id }
|
|
||||||
youText = { youText } />
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Heading>{t('participantsPane.headings.participantsList', { count: participantsCount })}</Heading>
|
<Heading>{t('participantsPane.headings.participantsList', { count: participantsCount })}</Heading>
|
||||||
{showInviteButton && <InviteButton />}
|
{showInviteButton && <InviteButton />}
|
||||||
<div>
|
<div>
|
||||||
{sortedParticipantIds.map(renderParticipant)}
|
<MeetingParticipantItems
|
||||||
</div>
|
askUnmuteText = { askUnmuteText }
|
||||||
<MeetingParticipantContextMenu
|
lowerMenu = { lowerMenu }
|
||||||
muteAudio = { muteAudio }
|
muteAudio = { muteAudio }
|
||||||
onEnter = { menuEnter }
|
muteParticipantButtonText = { muteParticipantButtonText }
|
||||||
onLeave = { menuLeave }
|
openDrawerForParticipant = { openDrawerForParticipant }
|
||||||
onSelect = { lowerMenu }
|
overflowDrawer = { overflowDrawer }
|
||||||
{ ...raiseContext } />
|
participantActionEllipsisLabel = { participantActionEllipsisLabel }
|
||||||
</>
|
participantIds = { sortedParticipantIds }
|
||||||
|
participantsCount = { participantsCount }
|
||||||
|
raiseContextId = { raiseContext.participantID }
|
||||||
|
toggleMenu = { toggleMenu }
|
||||||
|
youText = { youText } />
|
||||||
|
</div>
|
||||||
|
<MeetingParticipantContextMenu
|
||||||
|
closeDrawer = { closeDrawer }
|
||||||
|
drawerParticipant = { drawerParticipant }
|
||||||
|
muteAudio = { muteAudio }
|
||||||
|
onEnter = { menuEnter }
|
||||||
|
onLeave = { menuLeave }
|
||||||
|
onSelect = { lowerMenu }
|
||||||
|
overflowDrawer = { overflowDrawer }
|
||||||
|
{ ...raiseContext } />
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -163,11 +167,14 @@ function _mapStateToProps(state): Object {
|
||||||
|
|
||||||
const showInviteButton = shouldRenderInviteButton(state) && isToolbarButtonEnabled('invite', state);
|
const showInviteButton = shouldRenderInviteButton(state) && isToolbarButtonEnabled('invite', state);
|
||||||
|
|
||||||
|
const overflowDrawer = showOverflowDrawer(state);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
sortedParticipantIds,
|
sortedParticipantIds,
|
||||||
participantsCount,
|
participantsCount,
|
||||||
showInviteButton
|
showInviteButton,
|
||||||
|
overflowDrawer
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export default connect(_mapStateToProps)(MeetingParticipantList);
|
export default connect(_mapStateToProps)(MeetingParticipants);
|
|
@ -1,6 +1,6 @@
|
||||||
// @flow
|
// @flow
|
||||||
|
|
||||||
import React, { type Node } from 'react';
|
import React, { type Node, useCallback } from 'react';
|
||||||
|
|
||||||
import { Avatar } from '../../../base/avatar';
|
import { Avatar } from '../../../base/avatar';
|
||||||
import {
|
import {
|
||||||
|
@ -61,13 +61,23 @@ type Props = {
|
||||||
/**
|
/**
|
||||||
* True if the participant is local.
|
* True if the participant is local.
|
||||||
*/
|
*/
|
||||||
local: boolean,
|
local: Boolean,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Opens a drawer with participant actions.
|
||||||
|
*/
|
||||||
|
openDrawerForParticipant: Function,
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Callback for when the mouse leaves this component
|
* Callback for when the mouse leaves this component
|
||||||
*/
|
*/
|
||||||
onLeave?: Function,
|
onLeave?: Function,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* If an overflow drawer can be opened.
|
||||||
|
*/
|
||||||
|
overflowDrawer?: boolean,
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The ID of the participant.
|
* The ID of the participant.
|
||||||
*/
|
*/
|
||||||
|
@ -81,7 +91,7 @@ type Props = {
|
||||||
/**
|
/**
|
||||||
* Media state for video
|
* Media state for video
|
||||||
*/
|
*/
|
||||||
videoMuteState: MediaState,
|
videoMediaState: MediaState,
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The translated "you" text.
|
* The translated "you" text.
|
||||||
|
@ -101,20 +111,28 @@ export default function ParticipantItem({
|
||||||
onLeave,
|
onLeave,
|
||||||
actionsTrigger = ACTION_TRIGGER.HOVER,
|
actionsTrigger = ACTION_TRIGGER.HOVER,
|
||||||
audioMediaState = MEDIA_STATE.NONE,
|
audioMediaState = MEDIA_STATE.NONE,
|
||||||
videoMuteState = MEDIA_STATE.NONE,
|
videoMediaState = MEDIA_STATE.NONE,
|
||||||
displayName,
|
displayName,
|
||||||
participantID,
|
participantID,
|
||||||
local,
|
local,
|
||||||
|
openDrawerForParticipant,
|
||||||
|
overflowDrawer,
|
||||||
raisedHand,
|
raisedHand,
|
||||||
youText
|
youText
|
||||||
}: Props) {
|
}: Props) {
|
||||||
const ParticipantActions = Actions[actionsTrigger];
|
const ParticipantActions = Actions[actionsTrigger];
|
||||||
|
const onClick = useCallback(
|
||||||
|
() => openDrawerForParticipant({
|
||||||
|
participantID,
|
||||||
|
displayName
|
||||||
|
}));
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ParticipantContainer
|
<ParticipantContainer
|
||||||
id = { `participant-item-${participantID}` }
|
id = { `participant-item-${participantID}` }
|
||||||
isHighlighted = { isHighlighted }
|
isHighlighted = { isHighlighted }
|
||||||
local = { local }
|
local = { local }
|
||||||
|
onClick = { !local && overflowDrawer ? onClick : undefined }
|
||||||
onMouseLeave = { onLeave }
|
onMouseLeave = { onLeave }
|
||||||
trigger = { actionsTrigger }>
|
trigger = { actionsTrigger }>
|
||||||
<Avatar
|
<Avatar
|
||||||
|
@ -131,7 +149,7 @@ export default function ParticipantItem({
|
||||||
{ !local && <ParticipantActions children = { children } /> }
|
{ !local && <ParticipantActions children = { children } /> }
|
||||||
<ParticipantStates>
|
<ParticipantStates>
|
||||||
{ raisedHand && <RaisedHandIndicator /> }
|
{ raisedHand && <RaisedHandIndicator /> }
|
||||||
{ VideoStateIcons[videoMuteState] }
|
{ VideoStateIcons[videoMediaState] }
|
||||||
{ AudioStateIcons[audioMediaState] }
|
{ AudioStateIcons[audioMediaState] }
|
||||||
</ParticipantStates>
|
</ParticipantStates>
|
||||||
</ParticipantContent>
|
</ParticipantContent>
|
||||||
|
|
|
@ -7,14 +7,16 @@ import { openDialog } from '../../../base/dialog';
|
||||||
import { translate } from '../../../base/i18n';
|
import { translate } from '../../../base/i18n';
|
||||||
import { isLocalParticipantModerator } from '../../../base/participants';
|
import { isLocalParticipantModerator } from '../../../base/participants';
|
||||||
import { connect } from '../../../base/redux';
|
import { connect } from '../../../base/redux';
|
||||||
|
import { Drawer, DrawerPortal } from '../../../toolbox/components/web';
|
||||||
|
import { showOverflowDrawer } from '../../../toolbox/functions';
|
||||||
import { MuteEveryoneDialog } from '../../../video-menu/components/';
|
import { MuteEveryoneDialog } from '../../../video-menu/components/';
|
||||||
import { close } from '../../actions';
|
import { close } from '../../actions';
|
||||||
import { classList, findStyledAncestor, getParticipantsPaneOpen } from '../../functions';
|
import { classList, findStyledAncestor, getParticipantsPaneOpen } from '../../functions';
|
||||||
import theme from '../../theme.json';
|
import theme from '../../theme.json';
|
||||||
import { FooterContextMenu } from '../FooterContextMenu';
|
import { FooterContextMenu } from '../FooterContextMenu';
|
||||||
|
|
||||||
import { LobbyParticipantList } from './LobbyParticipantList';
|
import LobbyParticipants from './LobbyParticipants';
|
||||||
import MeetingParticipantList from './MeetingParticipantList';
|
import MeetingParticipants from './MeetingParticipants';
|
||||||
import {
|
import {
|
||||||
AntiCollapse,
|
AntiCollapse,
|
||||||
Close,
|
Close,
|
||||||
|
@ -31,6 +33,11 @@ import {
|
||||||
*/
|
*/
|
||||||
type Props = {
|
type Props = {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether to display the context menu as a drawer.
|
||||||
|
*/
|
||||||
|
_overflowDrawer: boolean,
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Is the participants pane open.
|
* Is the participants pane open.
|
||||||
*/
|
*/
|
||||||
|
@ -81,6 +88,7 @@ class ParticipantsPane extends Component<Props, State> {
|
||||||
|
|
||||||
// Bind event handlers so they are only bound once per instance.
|
// Bind event handlers so they are only bound once per instance.
|
||||||
this._onClosePane = this._onClosePane.bind(this);
|
this._onClosePane = this._onClosePane.bind(this);
|
||||||
|
this._onDrawerClose = this._onDrawerClose.bind(this);
|
||||||
this._onKeyPress = this._onKeyPress.bind(this);
|
this._onKeyPress = this._onKeyPress.bind(this);
|
||||||
this._onMuteAll = this._onMuteAll.bind(this);
|
this._onMuteAll = this._onMuteAll.bind(this);
|
||||||
this._onToggleContext = this._onToggleContext.bind(this);
|
this._onToggleContext = this._onToggleContext.bind(this);
|
||||||
|
@ -113,10 +121,12 @@ class ParticipantsPane extends Component<Props, State> {
|
||||||
*/
|
*/
|
||||||
render() {
|
render() {
|
||||||
const {
|
const {
|
||||||
|
_overflowDrawer,
|
||||||
_paneOpen,
|
_paneOpen,
|
||||||
_showFooter,
|
_showFooter,
|
||||||
t
|
t
|
||||||
} = this.props;
|
} = this.props;
|
||||||
|
const { contextOpen } = this.state;
|
||||||
|
|
||||||
// when the pane is not open optimize to not
|
// when the pane is not open optimize to not
|
||||||
// execute the MeetingParticipantList render for large list of participants
|
// execute the MeetingParticipantList render for large list of participants
|
||||||
|
@ -137,9 +147,9 @@ class ParticipantsPane extends Component<Props, State> {
|
||||||
tabIndex = { 0 } />
|
tabIndex = { 0 } />
|
||||||
</Header>
|
</Header>
|
||||||
<Container>
|
<Container>
|
||||||
<LobbyParticipantList />
|
<LobbyParticipants />
|
||||||
<AntiCollapse />
|
<AntiCollapse />
|
||||||
<MeetingParticipantList />
|
<MeetingParticipants />
|
||||||
</Container>
|
</Container>
|
||||||
{_showFooter && (
|
{_showFooter && (
|
||||||
<Footer>
|
<Footer>
|
||||||
|
@ -150,12 +160,19 @@ class ParticipantsPane extends Component<Props, State> {
|
||||||
<FooterEllipsisButton
|
<FooterEllipsisButton
|
||||||
id = 'participants-pane-context-menu'
|
id = 'participants-pane-context-menu'
|
||||||
onClick = { this._onToggleContext } />
|
onClick = { this._onToggleContext } />
|
||||||
{this.state.contextOpen
|
{this.state.contextOpen && !_overflowDrawer
|
||||||
&& <FooterContextMenu onMouseLeave = { this._onToggleContext } />}
|
&& <FooterContextMenu onMouseLeave = { this._onToggleContext } />}
|
||||||
</FooterEllipsisContainer>
|
</FooterEllipsisContainer>
|
||||||
</Footer>
|
</Footer>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
<DrawerPortal>
|
||||||
|
<Drawer
|
||||||
|
isOpen = { contextOpen && _overflowDrawer }
|
||||||
|
onClose = { this._onDrawerClose }>
|
||||||
|
<FooterContextMenu inDrawer = { true } />
|
||||||
|
</Drawer>
|
||||||
|
</DrawerPortal>
|
||||||
</div>
|
</div>
|
||||||
</ThemeProvider>
|
</ThemeProvider>
|
||||||
);
|
);
|
||||||
|
@ -173,6 +190,20 @@ class ParticipantsPane extends Component<Props, State> {
|
||||||
this.props.dispatch(close());
|
this.props.dispatch(close());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_onDrawerClose: () => void
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Callback for closing the drawer.
|
||||||
|
*
|
||||||
|
* @private
|
||||||
|
* @returns {void}
|
||||||
|
*/
|
||||||
|
_onDrawerClose() {
|
||||||
|
this.setState({
|
||||||
|
contextOpen: false
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
_onKeyPress: (Object) => void;
|
_onKeyPress: (Object) => void;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -228,6 +259,8 @@ class ParticipantsPane extends Component<Props, State> {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -245,6 +278,7 @@ function _mapStateToProps(state: Object) {
|
||||||
const isPaneOpen = getParticipantsPaneOpen(state);
|
const isPaneOpen = getParticipantsPaneOpen(state);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
_overflowDrawer: showOverflowDrawer(state),
|
||||||
_paneOpen: isPaneOpen,
|
_paneOpen: isPaneOpen,
|
||||||
_showFooter: isPaneOpen && isLocalParticipantModerator(state)
|
_showFooter: isPaneOpen && isLocalParticipantModerator(state)
|
||||||
};
|
};
|
||||||
|
|
|
@ -1,7 +1,5 @@
|
||||||
export * from './InviteButton';
|
export * from './InviteButton';
|
||||||
export * from './LobbyParticipantItem';
|
export * from './LobbyParticipantItem';
|
||||||
export * from './LobbyParticipantList';
|
|
||||||
export * from './MeetingParticipantList';
|
|
||||||
export { default as ParticipantsPane } from './ParticipantsPane';
|
export { default as ParticipantsPane } from './ParticipantsPane';
|
||||||
export * from '../ParticipantsPaneButton';
|
export * from '../ParticipantsPaneButton';
|
||||||
export * from './RaisedHandIndicator';
|
export * from './RaisedHandIndicator';
|
||||||
|
|
|
@ -4,6 +4,8 @@ import styled from 'styled-components';
|
||||||
import { Icon, IconHorizontalPoints } from '../../../base/icons';
|
import { Icon, IconHorizontalPoints } from '../../../base/icons';
|
||||||
import { ACTION_TRIGGER } from '../../constants';
|
import { ACTION_TRIGGER } from '../../constants';
|
||||||
|
|
||||||
|
const MD_BREAKPOINT = '580px';
|
||||||
|
|
||||||
export const ignoredChildClassName = 'ignore-child';
|
export const ignoredChildClassName = 'ignore-child';
|
||||||
|
|
||||||
export const AntiCollapse = styled.br`
|
export const AntiCollapse = styled.br`
|
||||||
|
@ -89,7 +91,7 @@ export const ContextMenuIcon = styled(Icon).attrs({
|
||||||
size: 20
|
size: 20
|
||||||
})`
|
})`
|
||||||
& > svg {
|
& > svg {
|
||||||
fill: #a4b8d1;
|
fill: #ffffff;
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
@ -162,6 +164,12 @@ export const FooterButton = styled(Button)`
|
||||||
height: 40px;
|
height: 40px;
|
||||||
font-size: 15px;
|
font-size: 15px;
|
||||||
padding: 0 16px;
|
padding: 0 16px;
|
||||||
|
|
||||||
|
@media (max-width: ${MD_BREAKPOINT}) {
|
||||||
|
font-size: 16px;
|
||||||
|
height: 48px;
|
||||||
|
min-width: 48px;
|
||||||
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
export const FooterEllipsisButton = styled(FooterButton).attrs({
|
export const FooterEllipsisButton = styled(FooterButton).attrs({
|
||||||
|
@ -188,6 +196,10 @@ export const Heading = styled.div`
|
||||||
font-size: 15px;
|
font-size: 15px;
|
||||||
line-height: 24px;
|
line-height: 24px;
|
||||||
margin: 8px 0 ${props => props.theme.panePadding}px;
|
margin: 8px 0 ${props => props.theme.panePadding}px;
|
||||||
|
|
||||||
|
@media (max-width: ${MD_BREAKPOINT}) {
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
export const ParticipantActionButton = styled(Button)`
|
export const ParticipantActionButton = styled(Button)`
|
||||||
|
@ -275,6 +287,11 @@ export const ParticipantContainer = styled.div`
|
||||||
padding-left: ${props => props.theme.panePadding}px;
|
padding-left: ${props => props.theme.panePadding}px;
|
||||||
position: relative;
|
position: relative;
|
||||||
|
|
||||||
|
@media (max-width: ${MD_BREAKPOINT}) {
|
||||||
|
font-size: 16px;
|
||||||
|
height: 64px;
|
||||||
|
}
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
${ParticipantStates} {
|
${ParticipantStates} {
|
||||||
${props => !props.local && 'display: none'};
|
${props => !props.local && 'display: none'};
|
||||||
|
@ -293,6 +310,10 @@ export const ParticipantContainer = styled.div`
|
||||||
& ${ParticipantContent} {
|
& ${ParticipantContent} {
|
||||||
box-shadow: none;
|
box-shadow: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
& ${ParticipantStates} {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
${props => !props.isHighlighted && '}'}
|
${props => !props.isHighlighted && '}'}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
@ -306,6 +327,11 @@ export const ParticipantInviteButton = styled(Button).attrs({
|
||||||
& > *:not(:last-child) {
|
& > *:not(:last-child) {
|
||||||
margin-right: 8px;
|
margin-right: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@media (max-width: ${MD_BREAKPOINT}) {
|
||||||
|
font-size: 16px;
|
||||||
|
height: 48px;
|
||||||
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
export const ParticipantName = styled.div`
|
export const ParticipantName = styled.div`
|
||||||
|
|
|
@ -94,6 +94,7 @@ export const AudioStateIcons: {[MediaState]: React$Element<any> | null} = {
|
||||||
export const VideoStateIcons = {
|
export const VideoStateIcons = {
|
||||||
[MEDIA_STATE.FORCE_MUTED]: (
|
[MEDIA_STATE.FORCE_MUTED]: (
|
||||||
<Icon
|
<Icon
|
||||||
|
color = '#E04757'
|
||||||
size = { 16 }
|
size = { 16 }
|
||||||
src = { IconCameraEmptyDisabled } />
|
src = { IconCameraEmptyDisabled } />
|
||||||
),
|
),
|
||||||
|
|
|
@ -71,17 +71,39 @@ export function isForceMuted(participant: Object, mediaType: MediaType, state: O
|
||||||
* @param {Object} participant - The participant.
|
* @param {Object} participant - The participant.
|
||||||
* @param {boolean} muted - The mute state of the participant.
|
* @param {boolean} muted - The mute state of the participant.
|
||||||
* @param {Object} state - The redux state.
|
* @param {Object} state - The redux state.
|
||||||
|
* @param {boolean} ignoreDominantSpeaker - Whether to ignore the dominant speaker state.
|
||||||
* @returns {MediaState}
|
* @returns {MediaState}
|
||||||
*/
|
*/
|
||||||
export function getParticipantAudioMediaState(participant: Object, muted: Boolean, state: Object) {
|
export function getParticipantAudioMediaState(participant: Object, muted: Boolean, state: Object) {
|
||||||
const dominantSpeaker = getDominantSpeakerParticipant(state);
|
const dominantSpeaker = getDominantSpeakerParticipant(state);
|
||||||
|
|
||||||
|
if (muted) {
|
||||||
|
if (isForceMuted(participant, MEDIA_TYPE.AUDIO, state)) {
|
||||||
|
return MEDIA_STATE.FORCE_MUTED;
|
||||||
|
}
|
||||||
|
|
||||||
|
return MEDIA_STATE.MUTED;
|
||||||
|
}
|
||||||
|
|
||||||
if (participant === dominantSpeaker) {
|
if (participant === dominantSpeaker) {
|
||||||
return MEDIA_STATE.DOMINANT_SPEAKER;
|
return MEDIA_STATE.DOMINANT_SPEAKER;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return MEDIA_STATE.UNMUTED;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Determines the video media state (the mic icon) for a participant.
|
||||||
|
*
|
||||||
|
* @param {Object} participant - The participant.
|
||||||
|
* @param {boolean} muted - The mute state of the participant.
|
||||||
|
* @param {Object} state - The redux state.
|
||||||
|
* @param {boolean} ignoreDominantSpeaker - Whether to ignore the dominant speaker state.
|
||||||
|
* @returns {MediaState}
|
||||||
|
*/
|
||||||
|
export function getParticipantVideoMediaState(participant: Object, muted: Boolean, state: Object) {
|
||||||
if (muted) {
|
if (muted) {
|
||||||
if (isForceMuted(participant, MEDIA_TYPE.AUDIO, state)) {
|
if (isForceMuted(participant, MEDIA_TYPE.VIDEO, state)) {
|
||||||
return MEDIA_STATE.FORCE_MUTED;
|
return MEDIA_STATE.FORCE_MUTED;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -144,7 +166,7 @@ export const getParticipantsPaneOpen = (state: Object) => Boolean(getState(state
|
||||||
export function getQuickActionButtonType(participant: Object, isAudioMuted: Boolean, state: Object) {
|
export function getQuickActionButtonType(participant: Object, isAudioMuted: Boolean, state: Object) {
|
||||||
// handled only by moderators
|
// handled only by moderators
|
||||||
if (isLocalParticipantModerator(state)) {
|
if (isLocalParticipantModerator(state)) {
|
||||||
if (isForceMuted(participant, MEDIA_TYPE.AUDIO, state)) {
|
if (isForceMuted(participant, MEDIA_TYPE.AUDIO, state) || isForceMuted(participant, MEDIA_TYPE.VIDEO, state)) {
|
||||||
return QUICK_ACTION_BUTTON.ASK_TO_UNMUTE;
|
return QUICK_ACTION_BUTTON.ASK_TO_UNMUTE;
|
||||||
}
|
}
|
||||||
if (!isAudioMuted) {
|
if (!isAudioMuted) {
|
||||||
|
|
|
@ -0,0 +1,46 @@
|
||||||
|
import { useCallback, useState } from 'react';
|
||||||
|
import { useDispatch } from 'react-redux';
|
||||||
|
|
||||||
|
import { approveKnockingParticipant, rejectKnockingParticipant } from '../lobby/actions';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Hook used to create admit/reject lobby actions.
|
||||||
|
*
|
||||||
|
* @param {Object} participant - The participant for which the actions are created.
|
||||||
|
* @param {Function} closeDrawer - Callback for closing the drawer.
|
||||||
|
* @returns {Array<Function>}
|
||||||
|
*/
|
||||||
|
export function useLobbyActions(participant, closeDrawer) {
|
||||||
|
const dispatch = useDispatch();
|
||||||
|
|
||||||
|
return [
|
||||||
|
useCallback(e => {
|
||||||
|
e.stopPropagation();
|
||||||
|
dispatch(approveKnockingParticipant(participant && participant.participantID));
|
||||||
|
closeDrawer && closeDrawer();
|
||||||
|
}, [ dispatch, closeDrawer ]),
|
||||||
|
|
||||||
|
useCallback(() => {
|
||||||
|
dispatch(rejectKnockingParticipant(participant && participant.participantID));
|
||||||
|
closeDrawer && closeDrawer();
|
||||||
|
}, [ dispatch, closeDrawer ])
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Hook used to create actions & state for opening a drawer.
|
||||||
|
*
|
||||||
|
* @returns {Array<any>}
|
||||||
|
*/
|
||||||
|
export function useParticipantDrawer() {
|
||||||
|
const [ drawerParticipant, openDrawerForParticipant ] = useState(null);
|
||||||
|
const closeDrawer = useCallback(() => {
|
||||||
|
openDrawerForParticipant(null);
|
||||||
|
});
|
||||||
|
|
||||||
|
return [
|
||||||
|
drawerParticipant,
|
||||||
|
closeDrawer,
|
||||||
|
openDrawerForParticipant
|
||||||
|
];
|
||||||
|
}
|
|
@ -3,13 +3,15 @@
|
||||||
import { APP_WILL_MOUNT, APP_WILL_UNMOUNT } from '../base/app';
|
import { APP_WILL_MOUNT, APP_WILL_UNMOUNT } from '../base/app';
|
||||||
import { CONFERENCE_JOINED } from '../base/conference';
|
import { CONFERENCE_JOINED } from '../base/conference';
|
||||||
import { JitsiConferenceEvents } from '../base/lib-jitsi-meet';
|
import { JitsiConferenceEvents } from '../base/lib-jitsi-meet';
|
||||||
import { setAudioMuted } from '../base/media';
|
import { MEDIA_TYPE, setAudioMuted } from '../base/media';
|
||||||
|
import { getLocalParticipant, raiseHand } from '../base/participants';
|
||||||
import { MiddlewareRegistry } from '../base/redux';
|
import { MiddlewareRegistry } from '../base/redux';
|
||||||
import { playSound, registerSound, unregisterSound } from '../base/sounds';
|
import { playSound, registerSound, unregisterSound } from '../base/sounds';
|
||||||
import {
|
import {
|
||||||
hideNotification,
|
hideNotification,
|
||||||
showNotification
|
showNotification
|
||||||
} from '../notifications';
|
} from '../notifications';
|
||||||
|
import { isForceMuted } from '../participants-pane/functions';
|
||||||
|
|
||||||
import { setCurrentNotificationUid } from './actions';
|
import { setCurrentNotificationUid } from './actions';
|
||||||
import { TALK_WHILE_MUTED_SOUND_ID } from './constants';
|
import { TALK_WHILE_MUTED_SOUND_ID } from './constants';
|
||||||
|
@ -41,10 +43,13 @@ MiddlewareRegistry.register(store => next => action => {
|
||||||
});
|
});
|
||||||
conference.on(
|
conference.on(
|
||||||
JitsiConferenceEvents.TALK_WHILE_MUTED, async () => {
|
JitsiConferenceEvents.TALK_WHILE_MUTED, async () => {
|
||||||
|
const state = getState();
|
||||||
|
const local = getLocalParticipant(state);
|
||||||
|
const forceMuted = isForceMuted(local, MEDIA_TYPE.AUDIO, state);
|
||||||
const notification = await dispatch(showNotification({
|
const notification = await dispatch(showNotification({
|
||||||
titleKey: 'toolbar.talkWhileMutedPopup',
|
titleKey: 'toolbar.talkWhileMutedPopup',
|
||||||
customActionNameKey: 'notify.unmute',
|
customActionNameKey: forceMuted ? 'notify.raiseHandAction' : 'notify.unmute',
|
||||||
customActionHandler: () => dispatch(setAudioMuted(false))
|
customActionHandler: () => dispatch(forceMuted ? raiseHand(true) : setAudioMuted(false))
|
||||||
}));
|
}));
|
||||||
|
|
||||||
const { soundsTalkWhileMuted } = getState()['features/base/settings'];
|
const { soundsTalkWhileMuted } = getState()['features/base/settings'];
|
||||||
|
|
|
@ -80,3 +80,14 @@ export function isVideoSettingsButtonDisabled(state: Object) {
|
||||||
export function isVideoMuteButtonDisabled(state: Object) {
|
export function isVideoMuteButtonDisabled(state: Object) {
|
||||||
return !hasAvailableDevices(state, 'videoInput');
|
return !hasAvailableDevices(state, 'videoInput');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* If an overflow drawer should be displayed or not.
|
||||||
|
* This is usually done for mobile devices or on narrow screens.
|
||||||
|
*
|
||||||
|
* @param {Object} state - The state from the Redux store.
|
||||||
|
* @returns {boolean}
|
||||||
|
*/
|
||||||
|
export function showOverflowDrawer(state: Object) {
|
||||||
|
return state['features/toolbox'].overflowDrawer;
|
||||||
|
}
|
||||||
|
|
|
@ -6,7 +6,7 @@ import {
|
||||||
createRemoteVideoMenuButtonEvent,
|
createRemoteVideoMenuButtonEvent,
|
||||||
sendAnalytics
|
sendAnalytics
|
||||||
} from '../../analytics';
|
} from '../../analytics';
|
||||||
import { grantModerator } from '../../base/participants';
|
import { getParticipantById, grantModerator } from '../../base/participants';
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
|
|
||||||
|
@ -20,6 +20,11 @@ type Props = {
|
||||||
*/
|
*/
|
||||||
participantID: string,
|
participantID: string,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The name of the remote participant to be granted moderator rights.
|
||||||
|
*/
|
||||||
|
participantName: string,
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Function to translate i18n labels.
|
* Function to translate i18n labels.
|
||||||
*/
|
*/
|
||||||
|
@ -64,3 +69,17 @@ export default class AbstractGrantModeratorDialog
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Maps (parts of) the Redux state to the associated {@code AbstractMuteEveryoneDialog}'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) {
|
||||||
|
|
||||||
|
return {
|
||||||
|
participantName: getParticipantById(state, ownProps.participantID).name
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
|
@ -4,13 +4,11 @@ import {
|
||||||
createRemoteVideoMenuButtonEvent,
|
createRemoteVideoMenuButtonEvent,
|
||||||
sendAnalytics
|
sendAnalytics
|
||||||
} from '../../analytics';
|
} from '../../analytics';
|
||||||
import { openDialog } from '../../base/dialog';
|
|
||||||
import { IconMicDisabled } from '../../base/icons';
|
import { IconMicDisabled } from '../../base/icons';
|
||||||
import { MEDIA_TYPE } from '../../base/media';
|
import { MEDIA_TYPE } from '../../base/media';
|
||||||
import { AbstractButton, type AbstractButtonProps } from '../../base/toolbox/components';
|
import { AbstractButton, type AbstractButtonProps } from '../../base/toolbox/components';
|
||||||
import { isRemoteTrackMuted } from '../../base/tracks';
|
import { isRemoteTrackMuted } from '../../base/tracks';
|
||||||
|
import { muteRemote } from '../actions.any';
|
||||||
import { MuteRemoteParticipantDialog } from '.';
|
|
||||||
|
|
||||||
export type Props = AbstractButtonProps & {
|
export type Props = AbstractButtonProps & {
|
||||||
|
|
||||||
|
@ -61,7 +59,7 @@ export default class AbstractMuteButton extends AbstractButton<Props, *> {
|
||||||
'participant_id': participantID
|
'participant_id': participantID
|
||||||
}));
|
}));
|
||||||
|
|
||||||
dispatch(openDialog(MuteRemoteParticipantDialog, { participantID }));
|
dispatch(muteRemote(participantID, MEDIA_TYPE.AUDIO));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -2,6 +2,8 @@
|
||||||
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
|
||||||
|
import { requestDisableAudioModeration, requestEnableAudioModeration } from '../../av-moderation/actions';
|
||||||
|
import { isEnabledFromState } from '../../av-moderation/functions';
|
||||||
import { Dialog } from '../../base/dialog';
|
import { Dialog } from '../../base/dialog';
|
||||||
import { MEDIA_TYPE } from '../../base/media';
|
import { MEDIA_TYPE } from '../../base/media';
|
||||||
import { getLocalParticipant, getParticipantDisplayName } from '../../base/participants';
|
import { getLocalParticipant, getParticipantDisplayName } from '../../base/participants';
|
||||||
|
@ -19,7 +21,14 @@ export type Props = AbstractProps & {
|
||||||
|
|
||||||
content: string,
|
content: string,
|
||||||
exclude: Array<string>,
|
exclude: Array<string>,
|
||||||
title: string
|
title: string,
|
||||||
|
showAdvancedModerationToggle: boolean,
|
||||||
|
isAudioModerationEnabled: boolean
|
||||||
|
};
|
||||||
|
|
||||||
|
type State = {
|
||||||
|
audioModerationEnabled: boolean,
|
||||||
|
content: string
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -29,12 +38,33 @@ export type Props = AbstractProps & {
|
||||||
*
|
*
|
||||||
* @extends AbstractMuteRemoteParticipantDialog
|
* @extends AbstractMuteRemoteParticipantDialog
|
||||||
*/
|
*/
|
||||||
export default class AbstractMuteEveryoneDialog<P: Props> extends AbstractMuteRemoteParticipantDialog<P> {
|
export default class AbstractMuteEveryoneDialog<P: Props> extends AbstractMuteRemoteParticipantDialog<P, State> {
|
||||||
static defaultProps = {
|
static defaultProps = {
|
||||||
exclude: [],
|
exclude: [],
|
||||||
muteLocal: false
|
muteLocal: false
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initializes a new {@code AbstractMuteRemoteParticipantDialog} instance.
|
||||||
|
*
|
||||||
|
* @param {Object} props - The read-only properties with which the new
|
||||||
|
* instance is to be initialized.
|
||||||
|
*/
|
||||||
|
constructor(props: P) {
|
||||||
|
super(props);
|
||||||
|
|
||||||
|
this.state = {
|
||||||
|
audioModerationEnabled: props.isAudioModerationEnabled,
|
||||||
|
content: props.content || props.t(props.isAudioModerationEnabled
|
||||||
|
? 'dialog.muteEveryoneDialogModerationOn' : 'dialog.muteEveryoneDialog'
|
||||||
|
)
|
||||||
|
};
|
||||||
|
|
||||||
|
// Bind event handlers so they are only bound once per instance.
|
||||||
|
this._onSubmit = this._onSubmit.bind(this);
|
||||||
|
this._onToggleModeration = this._onToggleModeration.bind(this);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Implements React's {@link Component#render()}.
|
* Implements React's {@link Component#render()}.
|
||||||
*
|
*
|
||||||
|
@ -59,6 +89,8 @@ export default class AbstractMuteEveryoneDialog<P: Props> extends AbstractMuteRe
|
||||||
|
|
||||||
_onSubmit: () => boolean;
|
_onSubmit: () => boolean;
|
||||||
|
|
||||||
|
_onToggleModeration: () => void;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Callback to be invoked when the value of this dialog is submitted.
|
* Callback to be invoked when the value of this dialog is submitted.
|
||||||
*
|
*
|
||||||
|
@ -71,6 +103,11 @@ export default class AbstractMuteEveryoneDialog<P: Props> extends AbstractMuteRe
|
||||||
} = this.props;
|
} = this.props;
|
||||||
|
|
||||||
dispatch(muteAllParticipants(exclude, MEDIA_TYPE.AUDIO));
|
dispatch(muteAllParticipants(exclude, MEDIA_TYPE.AUDIO));
|
||||||
|
if (this.state.audioModerationEnabled) {
|
||||||
|
dispatch(requestEnableAudioModeration());
|
||||||
|
} else {
|
||||||
|
dispatch(requestDisableAudioModeration());
|
||||||
|
}
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
@ -97,7 +134,7 @@ export function abstractMapStateToProps(state: Object, ownProps: Props) {
|
||||||
content: t('dialog.muteEveryoneElseDialog'),
|
content: t('dialog.muteEveryoneElseDialog'),
|
||||||
title: t('dialog.muteEveryoneElseTitle', { whom })
|
title: t('dialog.muteEveryoneElseTitle', { whom })
|
||||||
} : {
|
} : {
|
||||||
content: t('dialog.muteEveryoneDialog'),
|
title: t('dialog.muteEveryoneTitle'),
|
||||||
title: t('dialog.muteEveryoneTitle')
|
isAudioModerationEnabled: isEnabledFromState(MEDIA_TYPE.AUDIO, state)
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,6 +2,8 @@
|
||||||
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
|
||||||
|
import { requestDisableVideoModeration, requestEnableVideoModeration } from '../../av-moderation/actions';
|
||||||
|
import { isEnabledFromState } from '../../av-moderation/functions';
|
||||||
import { Dialog } from '../../base/dialog';
|
import { Dialog } from '../../base/dialog';
|
||||||
import { MEDIA_TYPE } from '../../base/media';
|
import { MEDIA_TYPE } from '../../base/media';
|
||||||
import { getLocalParticipant, getParticipantDisplayName } from '../../base/participants';
|
import { getLocalParticipant, getParticipantDisplayName } from '../../base/participants';
|
||||||
|
@ -19,7 +21,14 @@ export type Props = AbstractProps & {
|
||||||
|
|
||||||
content: string,
|
content: string,
|
||||||
exclude: Array<string>,
|
exclude: Array<string>,
|
||||||
title: string
|
title: string,
|
||||||
|
showAdvancedModerationToggle: boolean,
|
||||||
|
isVideoModerationEnabled: boolean
|
||||||
|
};
|
||||||
|
|
||||||
|
type State = {
|
||||||
|
moderationEnabled: boolean;
|
||||||
|
content: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -29,12 +38,34 @@ export type Props = AbstractProps & {
|
||||||
*
|
*
|
||||||
* @extends AbstractMuteRemoteParticipantsVideoDialog
|
* @extends AbstractMuteRemoteParticipantsVideoDialog
|
||||||
*/
|
*/
|
||||||
export default class AbstractMuteEveryonesVideoDialog<P: Props> extends AbstractMuteRemoteParticipantsVideoDialog<P> {
|
export default class AbstractMuteEveryonesVideoDialog<P: Props>
|
||||||
|
extends AbstractMuteRemoteParticipantsVideoDialog<P, State> {
|
||||||
static defaultProps = {
|
static defaultProps = {
|
||||||
exclude: [],
|
exclude: [],
|
||||||
muteLocal: false
|
muteLocal: false
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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);
|
||||||
|
|
||||||
|
this.state = {
|
||||||
|
moderationEnabled: props.isVideoModerationEnabled,
|
||||||
|
content: props.content || props.t(props.isVideoModerationEnabled
|
||||||
|
? 'dialog.muteEveryonesVideoDialogModerationOn' : 'dialog.muteEveryonesVideoDialog'
|
||||||
|
)
|
||||||
|
};
|
||||||
|
|
||||||
|
// Bind event handlers so they are only bound once per instance.
|
||||||
|
this._onSubmit = this._onSubmit.bind(this);
|
||||||
|
this._onToggleModeration = this._onToggleModeration.bind(this);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Implements React's {@link Component#render()}.
|
* Implements React's {@link Component#render()}.
|
||||||
*
|
*
|
||||||
|
@ -59,6 +90,8 @@ export default class AbstractMuteEveryonesVideoDialog<P: Props> extends Abstract
|
||||||
|
|
||||||
_onSubmit: () => boolean;
|
_onSubmit: () => boolean;
|
||||||
|
|
||||||
|
_onToggleModeration: () => void;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Callback to be invoked when the value of this dialog is submitted.
|
* Callback to be invoked when the value of this dialog is submitted.
|
||||||
*
|
*
|
||||||
|
@ -71,6 +104,11 @@ export default class AbstractMuteEveryonesVideoDialog<P: Props> extends Abstract
|
||||||
} = this.props;
|
} = this.props;
|
||||||
|
|
||||||
dispatch(muteAllParticipants(exclude, MEDIA_TYPE.VIDEO));
|
dispatch(muteAllParticipants(exclude, MEDIA_TYPE.VIDEO));
|
||||||
|
if (this.state.moderationEnabled) {
|
||||||
|
dispatch(requestEnableVideoModeration());
|
||||||
|
} else {
|
||||||
|
dispatch(requestDisableVideoModeration());
|
||||||
|
}
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
@ -84,7 +122,8 @@ export default class AbstractMuteEveryonesVideoDialog<P: Props> extends Abstract
|
||||||
* @returns {Props}
|
* @returns {Props}
|
||||||
*/
|
*/
|
||||||
export function abstractMapStateToProps(state: Object, ownProps: Props) {
|
export function abstractMapStateToProps(state: Object, ownProps: Props) {
|
||||||
const { exclude, t } = ownProps;
|
const { exclude = [], t } = ownProps;
|
||||||
|
const isVideoModerationEnabled = isEnabledFromState(MEDIA_TYPE.VIDEO, state);
|
||||||
|
|
||||||
const whom = exclude
|
const whom = exclude
|
||||||
// eslint-disable-next-line no-confusing-arrow
|
// eslint-disable-next-line no-confusing-arrow
|
||||||
|
@ -97,7 +136,7 @@ export function abstractMapStateToProps(state: Object, ownProps: Props) {
|
||||||
content: t('dialog.muteEveryoneElsesVideoDialog'),
|
content: t('dialog.muteEveryoneElsesVideoDialog'),
|
||||||
title: t('dialog.muteEveryoneElsesVideoTitle', { whom })
|
title: t('dialog.muteEveryoneElsesVideoTitle', { whom })
|
||||||
} : {
|
} : {
|
||||||
content: t('dialog.muteEveryonesVideoDialog'),
|
title: t('dialog.muteEveryonesVideoTitle'),
|
||||||
title: t('dialog.muteEveryonesVideoTitle')
|
isVideoModerationEnabled
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
@ -32,8 +32,8 @@ export type Props = {
|
||||||
*
|
*
|
||||||
* @extends Component
|
* @extends Component
|
||||||
*/
|
*/
|
||||||
export default class AbstractMuteRemoteParticipantDialog<P:Props = Props>
|
export default class AbstractMuteRemoteParticipantDialog<P:Props = Props, State=void>
|
||||||
extends Component<P> {
|
extends Component<P, State> {
|
||||||
/**
|
/**
|
||||||
* Initializes a new {@code AbstractMuteRemoteParticipantDialog} instance.
|
* Initializes a new {@code AbstractMuteRemoteParticipantDialog} instance.
|
||||||
*
|
*
|
||||||
|
|
|
@ -32,8 +32,8 @@ export type Props = {
|
||||||
*
|
*
|
||||||
* @extends Component
|
* @extends Component
|
||||||
*/
|
*/
|
||||||
export default class AbstractMuteRemoteParticipantsVideoDialog<P:Props = Props>
|
export default class AbstractMuteRemoteParticipantsVideoDialog<P:Props = Props, State=void>
|
||||||
extends Component<P> {
|
extends Component<P, State> {
|
||||||
/**
|
/**
|
||||||
* Initializes a new {@code AbstractMuteRemoteParticipantsVideoDialog} instance.
|
* Initializes a new {@code AbstractMuteRemoteParticipantsVideoDialog} instance.
|
||||||
*
|
*
|
||||||
|
|
|
@ -1,32 +0,0 @@
|
||||||
// @flow
|
|
||||||
|
|
||||||
import React from 'react';
|
|
||||||
|
|
||||||
import { ConfirmDialog } from '../../../base/dialog';
|
|
||||||
import { translate } from '../../../base/i18n';
|
|
||||||
import { connect } from '../../../base/redux';
|
|
||||||
import AbstractMuteRemoteParticipantDialog
|
|
||||||
from '../AbstractMuteRemoteParticipantDialog';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Dialog to confirm a remote participant mute action.
|
|
||||||
*/
|
|
||||||
class MuteRemoteParticipantDialog extends AbstractMuteRemoteParticipantDialog {
|
|
||||||
/**
|
|
||||||
* Implements React's {@link Component#render()}.
|
|
||||||
*
|
|
||||||
* @inheritdoc
|
|
||||||
* @returns {ReactElement}
|
|
||||||
*/
|
|
||||||
render() {
|
|
||||||
return (
|
|
||||||
<ConfirmDialog
|
|
||||||
contentKey = 'dialog.muteParticipantDialog'
|
|
||||||
onSubmit = { this._onSubmit } />
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
_onSubmit: () => boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
export default translate(connect()(MuteRemoteParticipantDialog));
|
|
|
@ -5,7 +5,6 @@ export { default as GrantModeratorDialog } from './GrantModeratorDialog';
|
||||||
export { default as KickRemoteParticipantDialog } from './KickRemoteParticipantDialog';
|
export { default as KickRemoteParticipantDialog } from './KickRemoteParticipantDialog';
|
||||||
export { default as MuteEveryoneDialog } from './MuteEveryoneDialog';
|
export { default as MuteEveryoneDialog } from './MuteEveryoneDialog';
|
||||||
export { default as MuteEveryonesVideoDialog } from './MuteEveryonesVideoDialog';
|
export { default as MuteEveryonesVideoDialog } from './MuteEveryonesVideoDialog';
|
||||||
export { default as MuteRemoteParticipantDialog } from './MuteRemoteParticipantDialog';
|
|
||||||
export { default as MuteRemoteParticipantsVideoDialog } from './MuteRemoteParticipantsVideoDialog';
|
export { default as MuteRemoteParticipantsVideoDialog } from './MuteRemoteParticipantsVideoDialog';
|
||||||
export { default as RemoteVideoMenu } from './RemoteVideoMenu';
|
export { default as RemoteVideoMenu } from './RemoteVideoMenu';
|
||||||
export { default as SharedVideoMenu } from './SharedVideoMenu';
|
export { default as SharedVideoMenu } from './SharedVideoMenu';
|
||||||
|
|
|
@ -6,7 +6,7 @@ import { Dialog } from '../../../base/dialog';
|
||||||
import { translate } from '../../../base/i18n';
|
import { translate } from '../../../base/i18n';
|
||||||
import { connect } from '../../../base/redux';
|
import { connect } from '../../../base/redux';
|
||||||
import AbstractGrantModeratorDialog
|
import AbstractGrantModeratorDialog
|
||||||
from '../AbstractGrantModeratorDialog';
|
, { abstractMapStateToProps } from '../AbstractGrantModeratorDialog';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Dialog to confirm a grant moderator action.
|
* Dialog to confirm a grant moderator action.
|
||||||
|
@ -26,7 +26,7 @@ class GrantModeratorDialog extends AbstractGrantModeratorDialog {
|
||||||
titleKey = 'dialog.grantModeratorTitle'
|
titleKey = 'dialog.grantModeratorTitle'
|
||||||
width = 'small'>
|
width = 'small'>
|
||||||
<div>
|
<div>
|
||||||
{ this.props.t('dialog.grantModeratorDialog') }
|
{ this.props.t('dialog.grantModeratorDialog', { participantName: this.props.participantName }) }
|
||||||
</div>
|
</div>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
);
|
);
|
||||||
|
@ -35,4 +35,4 @@ class GrantModeratorDialog extends AbstractGrantModeratorDialog {
|
||||||
_onSubmit: () => boolean;
|
_onSubmit: () => boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default translate(connect()(GrantModeratorDialog));
|
export default translate(connect(abstractMapStateToProps)(GrantModeratorDialog));
|
||||||
|
|
|
@ -4,8 +4,10 @@ import React from 'react';
|
||||||
|
|
||||||
import { Dialog } from '../../../base/dialog';
|
import { Dialog } from '../../../base/dialog';
|
||||||
import { translate } from '../../../base/i18n';
|
import { translate } from '../../../base/i18n';
|
||||||
|
import { Switch } from '../../../base/react';
|
||||||
import { connect } from '../../../base/redux';
|
import { connect } from '../../../base/redux';
|
||||||
import AbstractMuteEveryoneDialog, { abstractMapStateToProps, type Props } from '../AbstractMuteEveryoneDialog';
|
import AbstractMuteEveryoneDialog, { abstractMapStateToProps, type Props }
|
||||||
|
from '../AbstractMuteEveryoneDialog';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A React Component with the contents for a dialog that asks for confirmation
|
* A React Component with the contents for a dialog that asks for confirmation
|
||||||
|
@ -14,6 +16,23 @@ import AbstractMuteEveryoneDialog, { abstractMapStateToProps, type Props } from
|
||||||
* @extends AbstractMuteEveryoneDialog
|
* @extends AbstractMuteEveryoneDialog
|
||||||
*/
|
*/
|
||||||
class MuteEveryoneDialog extends AbstractMuteEveryoneDialog<Props> {
|
class MuteEveryoneDialog extends AbstractMuteEveryoneDialog<Props> {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Toggles advanced moderation switch.
|
||||||
|
*
|
||||||
|
* @returns {void}
|
||||||
|
*/
|
||||||
|
_onToggleModeration() {
|
||||||
|
this.setState(state => {
|
||||||
|
return {
|
||||||
|
audioModerationEnabled: !state.audioModerationEnabled,
|
||||||
|
content: this.props.t(state.audioModerationEnabled
|
||||||
|
? 'dialog.muteEveryoneDialog' : 'dialog.muteEveryoneDialogModerationOn'
|
||||||
|
)
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Implements React's {@link Component#render()}.
|
* Implements React's {@link Component#render()}.
|
||||||
*
|
*
|
||||||
|
@ -27,8 +46,22 @@ class MuteEveryoneDialog extends AbstractMuteEveryoneDialog<Props> {
|
||||||
onSubmit = { this._onSubmit }
|
onSubmit = { this._onSubmit }
|
||||||
titleString = { this.props.title }
|
titleString = { this.props.title }
|
||||||
width = 'small'>
|
width = 'small'>
|
||||||
<div>
|
<div className = 'mute-dialog'>
|
||||||
{ this.props.content }
|
{ this.state.content }
|
||||||
|
{this.props.exclude.length === 0 && (
|
||||||
|
<>
|
||||||
|
<div className = 'separator-line' />
|
||||||
|
<div className = 'control-row'>
|
||||||
|
<label htmlFor = 'moderation-switch'>
|
||||||
|
{this.props.t('dialog.moderationAudioLabel')}
|
||||||
|
</label>
|
||||||
|
<Switch
|
||||||
|
id = 'moderation-switch'
|
||||||
|
onValueChange = { this._onToggleModeration }
|
||||||
|
value = { !this.state.audioModerationEnabled } />
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
);
|
);
|
||||||
|
|
|
@ -4,6 +4,7 @@ import React from 'react';
|
||||||
|
|
||||||
import { Dialog } from '../../../base/dialog';
|
import { Dialog } from '../../../base/dialog';
|
||||||
import { translate } from '../../../base/i18n';
|
import { translate } from '../../../base/i18n';
|
||||||
|
import { Switch } from '../../../base/react';
|
||||||
import { connect } from '../../../base/redux';
|
import { connect } from '../../../base/redux';
|
||||||
import AbstractMuteEveryonesVideoDialog, { abstractMapStateToProps, type Props }
|
import AbstractMuteEveryonesVideoDialog, { abstractMapStateToProps, type Props }
|
||||||
from '../AbstractMuteEveryonesVideoDialog';
|
from '../AbstractMuteEveryonesVideoDialog';
|
||||||
|
@ -15,6 +16,23 @@ import AbstractMuteEveryonesVideoDialog, { abstractMapStateToProps, type Props }
|
||||||
* @extends AbstractMuteEveryonesVideoDialog
|
* @extends AbstractMuteEveryonesVideoDialog
|
||||||
*/
|
*/
|
||||||
class MuteEveryonesVideoDialog extends AbstractMuteEveryonesVideoDialog<Props> {
|
class MuteEveryonesVideoDialog extends AbstractMuteEveryonesVideoDialog<Props> {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Toggles advanced moderation switch.
|
||||||
|
*
|
||||||
|
* @returns {void}
|
||||||
|
*/
|
||||||
|
_onToggleModeration() {
|
||||||
|
this.setState(state => {
|
||||||
|
return {
|
||||||
|
moderationEnabled: !state.moderationEnabled,
|
||||||
|
content: this.props.t(state.moderationEnabled
|
||||||
|
? 'dialog.muteEveryonesVideoDialog' : 'dialog.muteEveryonesVideoDialogModerationOn'
|
||||||
|
)
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Implements React's {@link Component#render()}.
|
* Implements React's {@link Component#render()}.
|
||||||
*
|
*
|
||||||
|
@ -28,8 +46,22 @@ class MuteEveryonesVideoDialog extends AbstractMuteEveryonesVideoDialog<Props> {
|
||||||
onSubmit = { this._onSubmit }
|
onSubmit = { this._onSubmit }
|
||||||
titleString = { this.props.title }
|
titleString = { this.props.title }
|
||||||
width = 'small'>
|
width = 'small'>
|
||||||
<div>
|
<div className = 'mute-dialog'>
|
||||||
{ this.props.content }
|
{this.state.content}
|
||||||
|
{this.props.exclude.length === 0 && (
|
||||||
|
<>
|
||||||
|
<div className = 'separator-line' />
|
||||||
|
<div className = 'control-row'>
|
||||||
|
<label htmlFor = 'moderation-switch'>
|
||||||
|
{this.props.t('dialog.moderationVideoLabel')}
|
||||||
|
</label>
|
||||||
|
<Switch
|
||||||
|
id = 'moderation-switch'
|
||||||
|
onValueChange = { this._onToggleModeration }
|
||||||
|
value = { !this.state.moderationEnabled } />
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
);
|
);
|
||||||
|
|
|
@ -1,41 +0,0 @@
|
||||||
/* @flow */
|
|
||||||
|
|
||||||
import React from 'react';
|
|
||||||
|
|
||||||
import { Dialog } from '../../../base/dialog';
|
|
||||||
import { translate } from '../../../base/i18n';
|
|
||||||
import { connect } from '../../../base/redux';
|
|
||||||
import AbstractMuteRemoteParticipantDialog
|
|
||||||
from '../AbstractMuteRemoteParticipantDialog';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A React Component with the contents for a dialog that asks for confirmation
|
|
||||||
* from the user before muting a remote participant.
|
|
||||||
*
|
|
||||||
* @extends Component
|
|
||||||
*/
|
|
||||||
class MuteRemoteParticipantDialog extends AbstractMuteRemoteParticipantDialog {
|
|
||||||
/**
|
|
||||||
* Implements React's {@link Component#render()}.
|
|
||||||
*
|
|
||||||
* @inheritdoc
|
|
||||||
* @returns {ReactElement}
|
|
||||||
*/
|
|
||||||
render() {
|
|
||||||
return (
|
|
||||||
<Dialog
|
|
||||||
okKey = 'dialog.muteParticipantButton'
|
|
||||||
onSubmit = { this._onSubmit }
|
|
||||||
titleKey = 'dialog.muteParticipantTitle'
|
|
||||||
width = 'small'>
|
|
||||||
<div>
|
|
||||||
{ this.props.t('dialog.muteParticipantBody') }
|
|
||||||
</div>
|
|
||||||
</Dialog>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
_onSubmit: () => boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
export default translate(connect()(MuteRemoteParticipantDialog));
|
|
|
@ -11,7 +11,6 @@ export { default as MuteEveryoneDialog } from './MuteEveryoneDialog';
|
||||||
export { default as MuteEveryonesVideoDialog } from './MuteEveryonesVideoDialog';
|
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 MuteEveryoneElsesVideoButton } from './MuteEveryoneElsesVideoButton';
|
||||||
export { default as MuteRemoteParticipantDialog } from './MuteRemoteParticipantDialog';
|
|
||||||
export { default as MuteRemoteParticipantsVideoDialog } from './MuteRemoteParticipantsVideoDialog';
|
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';
|
||||||
|
|
Loading…
Reference in New Issue