feat(recordings) add consent dialog (#15673)

This commit is contained in:
Mihaela Dumitru
2025-03-04 10:30:12 +02:00
committed by GitHub
parent fa2a8c5084
commit ebdd9755ba
12 changed files with 151 additions and 6 deletions

View File

@@ -393,6 +393,9 @@ var config = {
// // showPrejoinWarning: true,
// // If true, the notification for recording start will display a link to download the cloud recording.
// // showRecordingLink: true,
// // If true, mutes audio and video when a recording begins and displays a dialog
// // explaining the effect of unmuting.
// // requireConsent: true,
// },
// recordingService: {

View File

@@ -263,6 +263,7 @@
"Remove": "Remove",
"Share": "Share",
"Submit": "Submit",
"Understand": "I understand",
"WaitForHostMsg": "The conference has not yet started because no moderators have yet arrived. If you'd like to become a moderator please log-in. Otherwise, please wait.",
"WaitForHostNoAuthMsg": "The conference has not yet started because no moderators have yet arrived. Please wait.",
"WaitingForHostButton": "Wait for moderator",
@@ -393,6 +394,8 @@
"recentlyUsedObjects": "Your recently used objects",
"recording": "Recording",
"recordingDisabledBecauseOfActiveLiveStreamingTooltip": "Not possible while a live stream is active",
"recordingInProgressDescription": "This meeting is being recorded. Your audio and video have been muted. If you choose to unmute, you consent to being recorded.",
"recordingInProgressTitle": "Recording in progress",
"rejoinNow": "Rejoin now",
"remoteControlAllowedMessage": "{{user}} accepted your remote control request!",
"remoteControlDeniedMessage": "{{user}} rejected your remote control request!",

View File

@@ -542,6 +542,7 @@ export interface IConfig {
recordingSharingUrl?: string;
recordings?: {
recordAudioAndVideo?: boolean;
requireConsent?: boolean;
showPrejoinWarning?: boolean;
showRecordingLink?: boolean;
suggestRecording?: boolean;

View File

@@ -205,7 +205,10 @@ export default [
'remoteVideoMenu',
'roomPasswordNumberOfDigits',
'readOnlyName',
'recordings',
'recordings.recordAudioAndVideo',
'recordings.showPrejoinWarning',
'recordings.showRecordingLink',
'recordings.suggestRecording',
'replaceParticipant',
'resolution',
'screenshotCapture',

View File

@@ -36,6 +36,11 @@ interface IProps extends AbstractProps, WithTranslation {
*/
descriptionKey?: string | { key: string; params: string; };
/**
* Whether the cancel button is hidden.
*/
isCancelHidden?: Boolean;
/**
* Whether or not the nature of the confirm button is destructive.
*/
@@ -100,6 +105,7 @@ class ConfirmDialog extends AbstractDialog<IProps> {
cancelLabel,
children,
confirmLabel,
isCancelHidden,
isConfirmDestructive,
isConfirmHidden,
t,
@@ -121,10 +127,12 @@ class ConfirmDialog extends AbstractDialog<IProps> {
}
{ this._renderDescription() }
{ children }
<Dialog.Button
label = { t(cancelLabel || 'dialog.confirmNo') }
onPress = { this._onCancel }
style = { styles.dialogButton } />
{
!isCancelHidden && <Dialog.Button
label = { t(cancelLabel || 'dialog.confirmNo') }
onPress = { this._onCancel }
style = { styles.dialogButton } />
}
{
!isConfirmHidden && <Dialog.Button
label = { t(confirmLabel || 'dialog.confirmYes') }

View File

@@ -158,6 +158,7 @@ export interface IDynamicBrandingState {
logoImageUrl: string;
muiBrandedTheme?: boolean;
premeetingBackground: string;
requireRecordingConsent?: boolean;
sharedVideoAllowedURLDomains?: Array<string>;
showGiphyIntegration?: boolean;
supportUrl?: string;
@@ -186,6 +187,7 @@ ReducerRegistry.register<IDynamicBrandingState>(STORE_NAME, (state = DEFAULT_STA
premeetingBackground,
sharedVideoAllowedURLDomains,
showGiphyIntegration,
requireRecordingConsent,
supportUrl,
virtualBackgrounds
} = action.value;
@@ -205,6 +207,7 @@ ReducerRegistry.register<IDynamicBrandingState>(STORE_NAME, (state = DEFAULT_STA
premeetingBackground,
sharedVideoAllowedURLDomains,
showGiphyIntegration,
requireRecordingConsent,
supportUrl,
customizationFailed: false,
customizationReady: true,

View File

@@ -1 +1,2 @@
export { default as StartRecordingDialog } from './native/StartRecordingDialog';
export { default as RecordingConsentDialog } from './native/RecordingConsentDialog';

View File

@@ -1 +1,2 @@
export { default as StartRecordingDialog } from './web/StartRecordingDialog';
export { default as RecordingConsentDialog } from './web/RecordingConsentDialog';

View File

@@ -0,0 +1,28 @@
import React, { useCallback } from 'react';
import { useDispatch } from 'react-redux';
import ConfirmDialog from '../../../../base/dialog/components/native/ConfirmDialog';
import { setAudioUnmutePermissions, setVideoUnmutePermissions } from '../../../../base/media/actions';
/**
* Component that renders the dialog for explicit consent for recordings.
*
* @returns {JSX.Element}
*/
export default function RecordingConsentDialog() {
const dispatch = useDispatch();
const consent = useCallback(() => {
dispatch(setAudioUnmutePermissions(false, true));
dispatch(setVideoUnmutePermissions(false, true));
}, []);
return (
<ConfirmDialog
confirmLabel = { 'dialog.Understand' }
descriptionKey = { 'dialog.recordingInProgressDescription' }
isCancelHidden = { true }
onSubmit = { consent }
title = { 'dialog.recordingInProgressTitle' } />
);
}

View File

@@ -0,0 +1,34 @@
import React, { useCallback } from 'react';
import { useTranslation } from 'react-i18next';
import { useDispatch } from 'react-redux';
import { setAudioUnmutePermissions, setVideoUnmutePermissions } from '../../../../base/media/actions';
import Dialog from '../../../../base/ui/components/web/Dialog';
/**
* Component that renders the dialog for explicit consent for recordings.
*
* @returns {JSX.Element}
*/
export default function RecordingConsentDialog() {
const { t } = useTranslation();
const dispatch = useDispatch();
const consent = useCallback(() => {
dispatch(setAudioUnmutePermissions(false, true));
dispatch(setVideoUnmutePermissions(false, true));
}, []);
return (
<Dialog
cancel = {{ hidden: true }}
disableBackdropClose = { true }
ok = {{ translationKey: 'dialog.Understand' }}
onSubmit = { consent }
titleKey = 'dialog.recordingInProgressTitle'>
<div>
{t('dialog.recordingInProgressDescription')}
</div>
</Dialog>
);
}

View File

@@ -437,3 +437,26 @@ export function isLiveStreamingButtonVisible({
}) {
return !isInBreakoutRoom && liveStreamingEnabled && liveStreamingAllowed;
}
/**
* Whether the RecordingConsentDialog should be displayed.
*
* @param {any} recorderSession - The recorder session.
* @param {IReduxState} state - The Redux state.
* @returns {boolean}
*/
export function shouldRequireRecordingConsent(recorderSession: any, state: IReduxState) {
const { requireRecordingConsent } = state['features/dynamic-branding'] || {};
const { requireConsent } = state['features/base/config'].recordings || {};
if (!requireConsent && !requireRecordingConsent) {
return false;
}
if (!recorderSession._initiator
|| recorderSession._statusFromJicofo === JitsiRecordingConstants.status.OFF) {
return false;
}
return recorderSession._initiator !== getLocalParticipant(state)?.id;
}

View File

@@ -1,13 +1,22 @@
import { batch } from 'react-redux';
import { createRecordingEvent } from '../analytics/AnalyticsEvents';
import { sendAnalytics } from '../analytics/functions';
import { IStore } from '../app/types';
import { APP_WILL_MOUNT, APP_WILL_UNMOUNT } from '../base/app/actionTypes';
import { CONFERENCE_JOIN_IN_PROGRESS } from '../base/conference/actionTypes';
import { getCurrentConference } from '../base/conference/functions';
import { openDialog } from '../base/dialog/actions';
import JitsiMeetJS, {
JitsiConferenceEvents,
JitsiRecordingConstants
} from '../base/lib-jitsi-meet';
import {
setAudioMuted,
setAudioUnmutePermissions,
setVideoMuted,
setVideoUnmutePermissions
} from '../base/media/actions';
import { MEDIA_TYPE } from '../base/media/constants';
import { PARTICIPANT_UPDATED } from '../base/participants/actionTypes';
import { updateLocalRecordingStatus } from '../base/participants/actions';
@@ -37,6 +46,7 @@ import {
showStoppedRecordingNotification,
updateRecordingSessionData
} from './actions';
import { RecordingConsentDialog } from './components/Recording';
import LocalRecordingManager from './components/Recording/LocalRecordingManager';
import {
LIVE_STREAMING_OFF_SOUND_ID,
@@ -49,6 +59,7 @@ import {
getResourceId,
getSessionById,
registerRecordingAudioFiles,
shouldRequireRecordingConsent,
unregisterRecordingAudioFiles
} from './functions';
import logger from './logger';
@@ -101,7 +112,11 @@ MiddlewareRegistry.register(({ dispatch, getState }) => next => action => {
(recorderSession: any) => {
if (recorderSession) {
recorderSession.getID() && dispatch(updateRecordingSessionData(recorderSession));
recorderSession.getError() && _showRecordingErrorNotification(recorderSession, dispatch, getState);
if (recorderSession.getError()) {
_showRecordingErrorNotification(recorderSession, dispatch, getState);
} else {
_showExplicitConsentDialog(recorderSession, dispatch, getState);
}
}
return;
@@ -390,3 +405,25 @@ function _showRecordingErrorNotification(session: any, dispatch: IStore['dispatc
APP.API.notifyRecordingStatusChanged(false, mode, error, isRecorderTranscriptionsRunning(getState()));
}
}
/**
* Mutes audio and video and displays the RecordingConsentDialog when the conditions are met.
*
* @param {any} recorderSession - The recording session.
* @param {Function} dispatch - The Redux dispatch function.
* @param {Function} getState - The Redux getState function.
* @returns {void}
*/
function _showExplicitConsentDialog(recorderSession: any, dispatch: IStore['dispatch'], getState: IStore['getState']) {
if (!shouldRequireRecordingConsent(recorderSession, getState())) {
return;
}
batch(() => {
dispatch(setAudioUnmutePermissions(true, true));
dispatch(setVideoUnmutePermissions(true, true));
dispatch(setAudioMuted(true));
dispatch(setVideoMuted(true));
dispatch(openDialog(RecordingConsentDialog));
});
}