Skip to content

Commit

Permalink
feat(av-moderation) Ask to Unmute and remove from Whitelist (jitsi#10043
Browse files Browse the repository at this point in the history
)

* 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 (jitsi#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

jitsi/lib-jitsi-meet@3b8baa9...0646bc3

Co-authored-by: Дамян Минков <damencho@jitsi.org>
  • Loading branch information
2 people authored and pull[bot] committed Sep 28, 2021
1 parent 751c5df commit c828608
Show file tree
Hide file tree
Showing 12 changed files with 242 additions and 28 deletions.
6 changes: 3 additions & 3 deletions lang/main.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down Expand Up @@ -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",
Expand Down
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
21 changes: 21 additions & 0 deletions react/features/av-moderation/actionTypes.js
Original file line number Diff line number Diff line change
Expand Up @@ -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.
*
Expand All @@ -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.
Expand Down
85 changes: 81 additions & 4 deletions react/features/av-moderation/actions.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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';

Expand All @@ -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.
*
Expand Down Expand Up @@ -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.
*
Expand Down Expand Up @@ -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
};
}

17 changes: 16 additions & 1 deletion react/features/av-moderation/middleware.js
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,9 @@ import {
enableModeration,
localParticipantApproved,
participantApproved,
participantPendingAudio
participantPendingAudio,
localParticipantRejected,
participantRejected
} from './actions';
import {
ASKED_TO_UNMUTE_SOUND_ID, AUDIO_MODERATION_NOTIFICATION_ID,
Expand Down Expand Up @@ -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));
});
Expand All @@ -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));
});
}
});
40 changes: 39 additions & 1 deletion react/features/av-moderation/reducer.js
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand Down Expand Up @@ -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;

Expand Down Expand Up @@ -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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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();

Expand Down
7 changes: 6 additions & 1 deletion react/features/video-menu/actions.any.js
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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));
}
});
};
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import { Component } from 'react';

import { rejectParticipantVideo } from '../../av-moderation/actions';
import { MEDIA_TYPE } from '../../base/media';
import { muteRemote } from '../actions';

Expand Down Expand Up @@ -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;
}
Expand Down
Loading

0 comments on commit c828608

Please sign in to comment.