From ace53c880bebeeefc6c08eb8228499010cde20e9 Mon Sep 17 00:00:00 2001 From: robertpin Date: Tue, 28 Sep 2021 19:11:13 +0300 Subject: [PATCH] feat(av-moderation) Ask to Unmute and remove from Whitelist (#10043) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat(av-moderation) Ask to Unmute and remove from Whitelist Make Ask to Unmute work without moderation Add remove from moderation whitelist functionality * chore(deps) lib-jitsi-meet@latest * feat(av-moderation) Remove from moderation whitelist functionality (#1729) * fix(chore) corrected typo in log message * fix(e2ee) replace nullish coalescing with or * fix(e2ee) restore initial key when RATCHET_WINDOW_SIZE reached https://github.com/jitsi/lib-jitsi-meet/compare/3b8baa9d3be2839510abaa954357d0b0ab023649...0646bc3403807dbf1370c88f028d9e0a16bcab1a Co-authored-by: Дамян Минков --- lang/main.json | 6 +- package-lock.json | 4 +- package.json | 2 +- react/features/av-moderation/actionTypes.js | 21 +++++ react/features/av-moderation/actions.js | 85 ++++++++++++++++++- react/features/av-moderation/middleware.js | 17 +++- react/features/av-moderation/reducer.js | 40 ++++++++- .../components/ParticipantQuickAction.js | 5 +- .../components/web/MeetingParticipants.js | 2 + react/features/video-menu/actions.any.js | 7 +- ...stractMuteRemoteParticipantsVideoDialog.js | 2 + .../mod_av_moderation_component.lua | 79 ++++++++++++++--- 12 files changed, 242 insertions(+), 28 deletions(-) diff --git a/lang/main.json b/lang/main.json index 638911ac7..87f49a9e0 100644 --- a/lang/main.json +++ b/lang/main.json @@ -271,7 +271,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.", "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?", - "muteParticipantsVideoButton": "Stop camera", + "muteParticipantsVideoButton": "Stop video", "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.", "noDropboxToken": "No valid Dropbox token", @@ -897,8 +897,8 @@ "mute": "Mute / Unmute", "muteEveryone": "Mute everyone", "muteEveryoneElse": "Mute everyone else", - "muteEveryonesVideo": "Disable everyone's camera", - "muteEveryoneElsesVideo": "Disable everyone else's camera", + "muteEveryonesVideo": "Disable everyone's video", + "muteEveryoneElsesVideo": "Disable everyone else's video", "participants": "Participants", "pip": "Toggle Picture-in-Picture mode", "privateMessage": "Send private message", diff --git a/package-lock.json b/package-lock.json index 5c9ad79c0..cbbde2f26 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11117,8 +11117,8 @@ } }, "lib-jitsi-meet": { - "version": "github:jitsi/lib-jitsi-meet#3b8baa9d3be2839510abaa954357d0b0ab023649", - "from": "github:jitsi/lib-jitsi-meet#3b8baa9d3be2839510abaa954357d0b0ab023649", + "version": "github:jitsi/lib-jitsi-meet#0646bc3403807dbf1370c88f028d9e0a16bcab1a", + "from": "github:jitsi/lib-jitsi-meet#0646bc3403807dbf1370c88f028d9e0a16bcab1a", "requires": { "@jitsi/js-utils": "1.0.2", "@jitsi/sdp-interop": "github:jitsi/sdp-interop#4669790bb9020cc8f10c1d1f3823c26b08497547", diff --git a/package.json b/package.json index ebd5ee55a..80f8f60e4 100644 --- a/package.json +++ b/package.json @@ -59,7 +59,7 @@ "jquery-i18next": "1.2.1", "js-md5": "0.6.1", "jwt-decode": "2.2.0", - "lib-jitsi-meet": "github:jitsi/lib-jitsi-meet#3b8baa9d3be2839510abaa954357d0b0ab023649", + "lib-jitsi-meet": "github:jitsi/lib-jitsi-meet#0646bc3403807dbf1370c88f028d9e0a16bcab1a", "libflacjs": "github:mmig/libflac.js#93d37e7f811f01cf7d8b6a603e38bd3c3810907d", "lodash": "4.17.21", "moment": "2.29.1", diff --git a/react/features/av-moderation/actionTypes.js b/react/features/av-moderation/actionTypes.js index 3163cfc5e..d529acdb1 100644 --- a/react/features/av-moderation/actionTypes.js +++ b/react/features/av-moderation/actionTypes.js @@ -74,6 +74,16 @@ export const REQUEST_ENABLE_VIDEO_MODERATION = 'REQUEST_ENABLE_VIDEO_MODERATION' */ export const LOCAL_PARTICIPANT_APPROVED = 'LOCAL_PARTICIPANT_APPROVED'; +/** + * The type of (redux) action which signals that the local participant had been blocked. + * + * { + * type: LOCAL_PARTICIPANT_REJECTED, + * mediaType: MediaType + * } + */ +export const LOCAL_PARTICIPANT_REJECTED = 'LOCAL_PARTICIPANT_REJECTED'; + /** * The type of (redux) action which signals to show notification to the local participant. * @@ -94,6 +104,17 @@ export const LOCAL_PARTICIPANT_MODERATION_NOTIFICATION = 'LOCAL_PARTICIPANT_MODE */ export const PARTICIPANT_APPROVED = 'PARTICIPANT_APPROVED'; +/** + * The type of (redux) action which signals that a participant was blocked for a media type. + * + * { + * type: PARTICIPANT_REJECTED, + * mediaType: MediaType + * participantId: String + * } + */ +export const PARTICIPANT_REJECTED = 'PARTICIPANT_REJECTED'; + /** * The type of (redux) action which signals that a participant asked to have its audio umuted. diff --git a/react/features/av-moderation/actions.js b/react/features/av-moderation/actions.js index 3455c6a59..fc820fadc 100644 --- a/react/features/av-moderation/actions.js +++ b/react/features/av-moderation/actions.js @@ -2,7 +2,7 @@ import { getConferenceState } from '../base/conference'; import { MEDIA_TYPE, type MediaType } from '../base/media/constants'; -import { getParticipantById } from '../base/participants'; +import { getParticipantById, isParticipantModerator } from '../base/participants'; import { isForceMuted } from '../participants-pane/functions'; import { @@ -16,7 +16,9 @@ import { REQUEST_DISABLE_AUDIO_MODERATION, REQUEST_ENABLE_AUDIO_MODERATION, REQUEST_DISABLE_VIDEO_MODERATION, - REQUEST_ENABLE_VIDEO_MODERATION + REQUEST_ENABLE_VIDEO_MODERATION, + LOCAL_PARTICIPANT_REJECTED, + PARTICIPANT_REJECTED } from './actionTypes'; import { isEnabledFromState } from './functions'; @@ -33,15 +35,57 @@ export const approveParticipant = (id: string) => (dispatch: Function, getState: const isAudioForceMuted = isForceMuted(participant, MEDIA_TYPE.AUDIO, state); const isVideoForceMuted = isForceMuted(participant, MEDIA_TYPE.VIDEO, state); + const isAudioModerationOn = isEnabledFromState(MEDIA_TYPE.AUDIO, state); + const isVideoModerationOn = isEnabledFromState(MEDIA_TYPE.VIDEO, state); - if (isEnabledFromState(MEDIA_TYPE.AUDIO, state) && isAudioForceMuted) { + if (!(isAudioModerationOn || isVideoModerationOn) || (isAudioModerationOn && isAudioForceMuted)) { conference.avModerationApprove(MEDIA_TYPE.AUDIO, id); } - if (isEnabledFromState(MEDIA_TYPE.VIDEO, state) && isVideoForceMuted) { + if (isVideoModerationOn && isVideoForceMuted) { conference.avModerationApprove(MEDIA_TYPE.VIDEO, id); } }; +/** + * Action used by moderator to reject audio for a participant. + * + * @param {staring} id - The id of the participant to be rejected. + * @returns {void} + */ +export const rejectParticipantAudio = (id: string) => (dispatch: Function, getState: Function) => { + const state = getState(); + const { conference } = getConferenceState(state); + const audioModeration = isEnabledFromState(MEDIA_TYPE.AUDIO, state); + + const participant = getParticipantById(state, id); + const isAudioForceMuted = isForceMuted(participant, MEDIA_TYPE.AUDIO, state); + const isModerator = isParticipantModerator(participant); + + if (audioModeration && !isAudioForceMuted && !isModerator) { + conference.avModerationReject(MEDIA_TYPE.AUDIO, id); + } +}; + +/** + * Action used by moderator to reject video for a participant. + * + * @param {staring} id - The id of the participant to be rejected. + * @returns {void} + */ +export const rejectParticipantVideo = (id: string) => (dispatch: Function, getState: Function) => { + const state = getState(); + const { conference } = getConferenceState(state); + const videoModeration = isEnabledFromState(MEDIA_TYPE.VIDEO, state); + + const participant = getParticipantById(state, id); + const isVideoForceMuted = isForceMuted(participant, MEDIA_TYPE.VIDEO, state); + const isModerator = isParticipantModerator(participant); + + if (videoModeration && !isVideoForceMuted && !isModerator) { + conference.avModerationReject(MEDIA_TYPE.VIDEO, id); + } +}; + /** * Audio or video moderation is disabled. * @@ -169,6 +213,21 @@ export const localParticipantApproved = (mediaType: MediaType) => { }; }; +/** + * Local participant was blocked to be able to unmute audio and video. + * + * @param {MediaType} mediaType - The media type to disable. + * @returns {{ + * type: LOCAL_PARTICIPANT_REJECTED + * }} + */ +export const localParticipantRejected = (mediaType: MediaType) => { + return { + type: LOCAL_PARTICIPANT_REJECTED, + mediaType + }; +}; + /** * Shows notification when A/V moderation is enabled and local participant is still not approved. * @@ -211,3 +270,21 @@ export function participantApproved(id: string, mediaType: MediaType) { mediaType }; } + +/** + * A participant was blocked to unmute for a mediaType. + * + * @param {string} id - The id of the approved participant. + * @param {MediaType} mediaType - The media type which was approved. + * @returns {{ + * type: PARTICIPANT_REJECTED, + * }} + */ +export function participantRejected(id: string, mediaType: MediaType) { + return { + type: PARTICIPANT_REJECTED, + id, + mediaType + }; +} + diff --git a/react/features/av-moderation/middleware.js b/react/features/av-moderation/middleware.js index e1721e185..2f0234f6b 100644 --- a/react/features/av-moderation/middleware.js +++ b/react/features/av-moderation/middleware.js @@ -35,7 +35,9 @@ import { enableModeration, localParticipantApproved, participantApproved, - participantPendingAudio + participantPendingAudio, + localParticipantRejected, + participantRejected } from './actions'; import { ASKED_TO_UNMUTE_SOUND_ID, AUDIO_MODERATION_NOTIFICATION_ID, @@ -176,6 +178,10 @@ StateListenerRegistry.register( } }); + conference.on(JitsiConferenceEvents.AV_MODERATION_REJECTED, ({ mediaType }) => { + dispatch(localParticipantRejected(mediaType)); + }); + conference.on(JitsiConferenceEvents.AV_MODERATION_CHANGED, ({ enabled, mediaType, actor }) => { enabled ? dispatch(enableModeration(mediaType, actor)) : dispatch(disableModeration(mediaType, actor)); }); @@ -194,5 +200,14 @@ StateListenerRegistry.register( dispatch(dismissPendingParticipant(id, mediaType)); }); }); + + // this is received by moderators + conference.on( + JitsiConferenceEvents.AV_MODERATION_PARTICIPANT_REJECTED, + ({ participant, mediaType }) => { + const { _id: id } = participant; + + dispatch(participantRejected(id, mediaType)); + }); } }); diff --git a/react/features/av-moderation/reducer.js b/react/features/av-moderation/reducer.js index cec82726c..973874408 100644 --- a/react/features/av-moderation/reducer.js +++ b/react/features/av-moderation/reducer.js @@ -13,8 +13,10 @@ import { DISMISS_PENDING_PARTICIPANT, ENABLE_MODERATION, LOCAL_PARTICIPANT_APPROVED, + LOCAL_PARTICIPANT_REJECTED, PARTICIPANT_APPROVED, - PARTICIPANT_PENDING_AUDIO + PARTICIPANT_PENDING_AUDIO, + PARTICIPANT_REJECTED } from './actionTypes'; import { MEDIA_TYPE_TO_PENDING_STORE_KEY } from './constants'; @@ -105,6 +107,16 @@ ReducerRegistry.register('features/av-moderation', (state = initialState, action }; } + case LOCAL_PARTICIPANT_REJECTED: { + const newState = action.mediaType === MEDIA_TYPE.AUDIO + ? { audioUnmuteApproved: false } : { videoUnmuteApproved: false }; + + return { + ...state, + ...newState + }; + } + case PARTICIPANT_PENDING_AUDIO: { const { participant } = action; @@ -228,6 +240,32 @@ ReducerRegistry.register('features/av-moderation', (state = initialState, action return state; } + case PARTICIPANT_REJECTED: { + const { mediaType, id } = action; + + if (mediaType === MEDIA_TYPE.AUDIO) { + return { + ...state, + audioWhitelist: { + ...state.audioWhitelist, + [id]: false + } + }; + } + + if (mediaType === MEDIA_TYPE.VIDEO) { + return { + ...state, + videoWhitelist: { + ...state.videoWhitelist, + [id]: false + } + }; + } + + return state; + } + } return state; diff --git a/react/features/participants-pane/components/ParticipantQuickAction.js b/react/features/participants-pane/components/ParticipantQuickAction.js index 1c5990a4a..7d957fdd0 100644 --- a/react/features/participants-pane/components/ParticipantQuickAction.js +++ b/react/features/participants-pane/components/ParticipantQuickAction.js @@ -63,15 +63,12 @@ export default function ParticipantQuickAction({ ); } - case QUICK_ACTION_BUTTON.ASK_TO_UNMUTE: { + default: { return ( ); } - default: { - return null; - } } } diff --git a/react/features/participants-pane/components/web/MeetingParticipants.js b/react/features/participants-pane/components/web/MeetingParticipants.js index b9d330e9c..b8d2eeed0 100644 --- a/react/features/participants-pane/components/web/MeetingParticipants.js +++ b/react/features/participants-pane/components/web/MeetingParticipants.js @@ -4,6 +4,7 @@ import React, { useCallback, useRef, useState } from 'react'; import { useTranslation } from 'react-i18next'; import { useDispatch } from 'react-redux'; +import { rejectParticipantAudio } from '../../../av-moderation/actions'; import { isToolbarButtonEnabled } from '../../../base/config/functions.web'; import { MEDIA_TYPE } from '../../../base/media'; import { @@ -104,6 +105,7 @@ function MeetingParticipants({ participantsCount, showInviteButton, overflowDraw const muteAudio = useCallback(id => () => { dispatch(muteRemote(id, MEDIA_TYPE.AUDIO)); + dispatch(rejectParticipantAudio(id)); }, [ dispatch ]); const [ drawerParticipant, closeDrawer, openDrawerForParticipant ] = useParticipantDrawer(); diff --git a/react/features/video-menu/actions.any.js b/react/features/video-menu/actions.any.js index a0c4aa0f9..27fe4dfe2 100644 --- a/react/features/video-menu/actions.any.js +++ b/react/features/video-menu/actions.any.js @@ -10,7 +10,7 @@ import { sendAnalytics, VIDEO_MUTE } from '../analytics'; -import { showModeratedNotification } from '../av-moderation/actions'; +import { rejectParticipantAudio, rejectParticipantVideo, showModeratedNotification } from '../av-moderation/actions'; import { shouldShowModeratedNotification } from '../av-moderation/functions'; import { MEDIA_TYPE, @@ -112,6 +112,11 @@ export function muteAllParticipants(exclude: Array, mediaType: MEDIA_TYP } dispatch(muteRemote(id, mediaType)); + if (mediaType === MEDIA_TYPE.AUDIO) { + dispatch(rejectParticipantAudio(id)); + } else { + dispatch(rejectParticipantVideo(id)); + } }); }; } diff --git a/react/features/video-menu/components/AbstractMuteRemoteParticipantsVideoDialog.js b/react/features/video-menu/components/AbstractMuteRemoteParticipantsVideoDialog.js index 7218477b3..40b6189fc 100644 --- a/react/features/video-menu/components/AbstractMuteRemoteParticipantsVideoDialog.js +++ b/react/features/video-menu/components/AbstractMuteRemoteParticipantsVideoDialog.js @@ -2,6 +2,7 @@ import { Component } from 'react'; +import { rejectParticipantVideo } from '../../av-moderation/actions'; import { MEDIA_TYPE } from '../../base/media'; import { muteRemote } from '../actions'; @@ -59,6 +60,7 @@ export default class AbstractMuteRemoteParticipantsVideoDialog