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