feat(av-moderation) Ask to Unmute and remove from Whitelist (#10043)
* 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
3b8baa9d3b...0646bc3403
Co-authored-by: Дамян Минков <damencho@jitsi.org>
This commit is contained in:
parent
e3ac52908a
commit
ace53c880b
|
@ -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",
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
@ -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));
|
||||
});
|
||||
}
|
||||
});
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -63,15 +63,12 @@ export default function ParticipantQuickAction({
|
|||
</QuickActionButton>
|
||||
);
|
||||
}
|
||||
case QUICK_ACTION_BUTTON.ASK_TO_UNMUTE: {
|
||||
default: {
|
||||
return (
|
||||
<AskToUnmuteButton
|
||||
askUnmuteText = { askUnmuteText }
|
||||
participantID = { participantID } />
|
||||
);
|
||||
}
|
||||
default: {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
|
||||
|
|
|
@ -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<string>, mediaType: MEDIA_TYP
|
|||
}
|
||||
|
||||
dispatch(muteRemote(id, mediaType));
|
||||
if (mediaType === MEDIA_TYPE.AUDIO) {
|
||||
dispatch(rejectParticipantAudio(id));
|
||||
} else {
|
||||
dispatch(rejectParticipantVideo(id));
|
||||
}
|
||||
});
|
||||
};
|
||||
}
|
||||
|
|
|
@ -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<P:Props = Props,
|
|||
const { dispatch, participantID } = this.props;
|
||||
|
||||
dispatch(muteRemote(participantID, MEDIA_TYPE.VIDEO));
|
||||
dispatch(rejectParticipantVideo(participantID));
|
||||
|
||||
return true;
|
||||
}
|
||||
|
|
|
@ -13,6 +13,17 @@ end
|
|||
|
||||
module:log('info', 'Starting av_moderation for %s', muc_component_host);
|
||||
|
||||
-- Returns the index of the given element in the table
|
||||
-- @param table in which to look
|
||||
-- @param elem the element for which to find the index
|
||||
function get_index_in_table(table, elem)
|
||||
for index, value in pairs(table) do
|
||||
if value == elem then
|
||||
return index
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- Sends a json-message to the destination jid
|
||||
-- @param to_jid the destination jid
|
||||
-- @param json_message the message content to send
|
||||
|
@ -46,20 +57,22 @@ function notify_occupants_enable(jid, enable, room, actorJid, mediaType)
|
|||
end
|
||||
end
|
||||
|
||||
-- Notifies about a jid added to the whitelist. Notifies all moderators and admin and the jid itself
|
||||
-- Notifies about a change to the whitelist. Notifies all moderators and admin and the jid itself
|
||||
-- @param jid the jid to notify about the change
|
||||
-- @param moderators whether to notify all moderators in the room
|
||||
-- @param room the room where to send it
|
||||
-- @param mediaType used only when a participant is approved (not sent to moderators)
|
||||
function notify_whitelist_change(jid, moderators, room, mediaType)
|
||||
-- @param removed whether the jid is removed or added
|
||||
function notify_whitelist_change(jid, moderators, room, mediaType, removed)
|
||||
local body_json = {};
|
||||
body_json.type = 'av_moderation';
|
||||
body_json.room = internal_room_jid_match_rewrite(room.jid);
|
||||
body_json.whitelists = room.av_moderation;
|
||||
body_json.removed = removed;
|
||||
body_json.mediaType = mediaType;
|
||||
local moderators_body_json_str = json.encode(body_json);
|
||||
body_json.whitelists = nil;
|
||||
body_json.approved = true; -- we want to send to participants only that they were approved to unmute
|
||||
body_json.mediaType = mediaType;
|
||||
local participant_body_json_str = json.encode(body_json);
|
||||
|
||||
for _, occupant in room:each_occupant() do
|
||||
|
@ -77,6 +90,22 @@ function notify_whitelist_change(jid, moderators, room, mediaType)
|
|||
end
|
||||
end
|
||||
|
||||
-- Notifies jid that is approved. This is a moderator to jid message to ask to unmute,
|
||||
-- @param jid the jid to notify about the change
|
||||
-- @param from the jid that triggered this
|
||||
-- @param room the room where to send it
|
||||
-- @param mediaType the mediaType it was approved for
|
||||
function notify_jid_approved(jid, from, room, mediaType)
|
||||
local body_json = {};
|
||||
body_json.type = 'av_moderation';
|
||||
body_json.room = internal_room_jid_match_rewrite(room.jid);
|
||||
body_json.approved = true; -- we want to send to participants only that they were approved to unmute
|
||||
body_json.mediaType = mediaType;
|
||||
body_json.from = from;
|
||||
|
||||
send_json_message(jid, json.encode(body_json));
|
||||
end
|
||||
|
||||
-- receives messages from clients to the component sending A/V moderation enable/disable commands or adding
|
||||
-- jids to the whitelist
|
||||
function on_message(event)
|
||||
|
@ -166,7 +195,7 @@ function on_message(event)
|
|||
-- send message to all occupants
|
||||
notify_occupants_enable(nil, enabled, room, occupant.nick, mediaType);
|
||||
return true;
|
||||
elseif moderation_command.attr.jidToWhitelist and room.av_moderation then
|
||||
elseif moderation_command.attr.jidToWhitelist then
|
||||
local occupant_jid = moderation_command.attr.jidToWhitelist;
|
||||
-- check if jid is in the room, if so add it to whitelist
|
||||
-- inform all moderators and admins and the jid
|
||||
|
@ -176,16 +205,44 @@ function on_message(event)
|
|||
return false;
|
||||
end
|
||||
|
||||
local whitelist = room.av_moderation[mediaType];
|
||||
if not whitelist then
|
||||
whitelist = {};
|
||||
room.av_moderation[mediaType] = whitelist;
|
||||
if room.av_moderation then
|
||||
local whitelist = room.av_moderation[mediaType];
|
||||
if not whitelist then
|
||||
whitelist = {};
|
||||
room.av_moderation[mediaType] = whitelist;
|
||||
end
|
||||
table.insert(whitelist, occupant_jid);
|
||||
|
||||
notify_whitelist_change(occupant_to_add.jid, true, room, mediaType, false);
|
||||
|
||||
return true;
|
||||
else
|
||||
-- this is a moderator asking the jid to unmute without enabling av moderation
|
||||
-- let's just send the event
|
||||
notify_jid_approved(occupant_to_add.jid, occupant.nick, room, mediaType);
|
||||
end
|
||||
elseif moderation_command.attr.jidToBlacklist then
|
||||
local occupant_jid = moderation_command.attr.jidToBlacklist;
|
||||
-- check if jid is in the room, if so remove it from the whitelist
|
||||
-- inform all moderators and admins
|
||||
local occupant_to_remove = room:get_occupant_by_nick(room_jid_match_rewrite(occupant_jid));
|
||||
if not occupant_to_remove then
|
||||
module:log('warn', 'No occupant %s found for %s', occupant_jid, room.jid);
|
||||
return false;
|
||||
end
|
||||
table.insert(whitelist, occupant_jid);
|
||||
|
||||
notify_whitelist_change(occupant_to_add.jid, true, room, mediaType);
|
||||
if room.av_moderation then
|
||||
local whitelist = room.av_moderation[mediaType];
|
||||
if whitelist then
|
||||
local index = get_index_in_table(whitelist, occupant_jid)
|
||||
if(index) then
|
||||
table.remove(whitelist, index);
|
||||
notify_whitelist_change(occupant_to_remove.jid, true, room, mediaType, true);
|
||||
end
|
||||
end
|
||||
|
||||
return true;
|
||||
return true;
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
|
|
Loading…
Reference in New Issue