feat(recording) refactor consent dialog (#15985)

* feat(recording) refactor consent dialog

Offer 2 choices and add a configurable "learn more" link.

* hide dialog and display link conditionally

* native changes

---------

Co-authored-by: Mihaela Dumitru <mihdmt@gmail.com>
This commit is contained in:
Saúl Ibarra Corretgé
2025-05-06 14:02:39 +02:00
committed by GitHub
parent b123d140fa
commit 5c0c3c2e0d
9 changed files with 134 additions and 15 deletions

View File

@@ -403,6 +403,8 @@ var config = {
// // requireConsent: true,
// // If true consent will be skipped for users who are already in the meeting.
// // skipConsentInMeeting: true,
// // Link for the recording consent dialog's "Learn more" link.
// // consentLearnMoreLink: 'https://jitsi.org/meet/consent',
// },
// recordingService: {

View File

@@ -272,7 +272,8 @@
"Remove": "Remove",
"Share": "Share",
"Submit": "Submit",
"Understand": "I understand",
"Understand": "I understand, keep me muted for now",
"UnderstandAndUnmute": "I understand, please unmute me",
"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",
@@ -309,6 +310,7 @@
"conferenceReloadMsg": "We're trying to fix this. Reconnecting in {{seconds}} sec…",
"conferenceReloadTitle": "Unfortunately, something went wrong.",
"confirm": "Confirm",
"confirmBack": "Back",
"confirmNo": "No",
"confirmYes": "Yes",
"connectError": "Oops! Something went wrong and we couldn't connect to the conference.",
@@ -346,6 +348,7 @@
"kickParticipantTitle": "Kick this participant?",
"kickSystemTitle": "Ouch! You were kicked out of the meeting",
"kickTitle": "Ouch! {{participantDisplayName}} kicked you out of the meeting",
"learnMore": "learn more",
"linkMeeting": "Link meeting",
"linkMeetingTitle": "Link meeting to Salesforce",
"liveStreaming": "Live Streaming",
@@ -403,7 +406,9 @@
"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.",
"recordingInProgressDescription": "This meeting is being recorded and analyzed by AI{{learnMore}}. Your audio and video have been muted. If you choose to unmute, you consent to being recorded.",
"recordingInProgressDescriptionFirstHalf": "This meeting is being recorded and analyzed by AI",
"recordingInProgressDescriptionSecondHalf": ". 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!",

View File

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

View File

@@ -40,6 +40,7 @@ export default class AbstractDialog<P extends IProps, S extends IState = IState>
super(props);
// Bind event handlers so they are only bound once per instance.
this._onBack = this._onBack.bind(this);
this._onCancel = this._onCancel.bind(this);
this._onSubmit = this._onSubmit.bind(this);
this._onSubmitFulfilled = this._onSubmitFulfilled.bind(this);
@@ -75,6 +76,14 @@ export default class AbstractDialog<P extends IProps, S extends IState = IState>
return this.props.dispatch(hideDialog());
}
_onBack() {
const { backDisabled = false, onBack } = this.props;
if (!backDisabled && (!onBack || onBack())) {
this._hide();
}
}
/**
* Dispatches a redux action to hide this dialog when it's canceled.
*

View File

@@ -16,6 +16,11 @@ import styles from './styles';
*/
interface IProps extends AbstractProps, WithTranslation {
/**
* The i18n key of the text label for the back button.
*/
backLabel?: string;
/**
* The i18n key of the text label for the cancel button.
*/
@@ -36,6 +41,11 @@ interface IProps extends AbstractProps, WithTranslation {
*/
descriptionKey?: string | { key: string; params: string; };
/**
* Whether the back button is hidden.
*/
isBackHidden?: Boolean;
/**
* Whether the cancel button is hidden.
*/
@@ -55,6 +65,11 @@ interface IProps extends AbstractProps, WithTranslation {
* Dialog title.
*/
title?: string;
/**
* Renders buttons vertically.
*/
verticalButtons?: boolean;
}
/**
@@ -102,14 +117,17 @@ class ConfirmDialog extends AbstractDialog<IProps> {
*/
override render() {
const {
backLabel,
cancelLabel,
children,
confirmLabel,
isBackHidden = true,
isCancelHidden,
isConfirmDestructive,
isConfirmHidden,
t,
title
title,
verticalButtons
} = this.props;
const dialogButtonStyle
@@ -119,6 +137,7 @@ class ConfirmDialog extends AbstractDialog<IProps> {
return (
<Dialog.Container
coverScreen = { false }
verticalButtons = { verticalButtons }
visible = { true }>
{
title && <Dialog.Title>
@@ -127,6 +146,12 @@ class ConfirmDialog extends AbstractDialog<IProps> {
}
{ this._renderDescription() }
{ children }
{
!isBackHidden && <Dialog.Button
label = { t(backLabel || 'dialog.confirmBack') }
onPress = { this._onBack }
style = { styles.dialogButton } />
}
{
!isCancelHidden && <Dialog.Button
label = { t(cancelLabel || 'dialog.confirmNo') }

View File

@@ -2,6 +2,16 @@ import { ReactNode } from 'react';
export type DialogProps = {
/**
* Whether back button is disabled. Enabled by default.
*/
backDisabled?: boolean;
/**
* Optional i18n key to change the back button title.
*/
backKey?: string;
/**
* Whether cancel button is disabled. Enabled by default.
*/
@@ -27,6 +37,11 @@ export type DialogProps = {
*/
okKey?: string;
/**
* The handler for onBack event.
*/
onBack?: Function;
/**
* The handler for onCancel event.
*/

View File

@@ -1,8 +1,13 @@
import React, { useCallback } from 'react';
import { useDispatch } from 'react-redux';
import { useDispatch, useSelector } from 'react-redux';
import { useTranslation } from 'react-i18next';
import Dialog from 'react-native-dialog';
import ConfirmDialog from '../../../../base/dialog/components/native/ConfirmDialog';
import { setAudioUnmutePermissions, setVideoUnmutePermissions } from '../../../../base/media/actions';
import Link from '../../../../base/react/components/native/Link';
import { setAudioMuted, setAudioUnmutePermissions, setVideoMuted, setVideoUnmutePermissions } from '../../../../base/media/actions';
import { IReduxState } from '../../../../app/types';
import styles from '../styles.native';
/**
* Component that renders the dialog for explicit consent for recordings.
@@ -11,6 +16,10 @@ import { setAudioUnmutePermissions, setVideoUnmutePermissions } from '../../../.
*/
export default function RecordingConsentDialog() {
const dispatch = useDispatch();
const { t } = useTranslation();
const { recordings } = useSelector((state: IReduxState) => state['features/base/config']);
const { consentLearnMoreLink } = recordings ?? {};
const consent = useCallback(() => {
dispatch(setAudioUnmutePermissions(false, true));
@@ -19,12 +28,36 @@ export default function RecordingConsentDialog() {
return true;
}, []);
const consentAndUnmute = useCallback(() => {
dispatch(setAudioUnmutePermissions(false, true));
dispatch(setVideoUnmutePermissions(false, true));
dispatch(setAudioMuted(false));
dispatch(setVideoMuted(false));
return true;
}, []);
return (
<ConfirmDialog
backLabel = { 'dialog.UnderstandAndUnmute' }
confirmLabel = { 'dialog.Understand' }
descriptionKey = { 'dialog.recordingInProgressDescription' }
isBackHidden = { false }
isCancelHidden = { true }
onBack = { consentAndUnmute }
onSubmit = { consent }
title = { 'dialog.recordingInProgressTitle' } />
title = { 'dialog.recordingInProgressTitle' }
verticalButtons = { true }>
<Dialog.Description>
{t('dialog.recordingInProgressDescriptionFirstHalf')}
{consentLearnMoreLink && (
<Link
style = { styles.learnMoreLink }
url = { consentLearnMoreLink }>
{`(${t('dialog.learnMore')})`}
</Link>
)}
{t('dialog.recordingInProgressDescriptionSecondHalf')}
</Dialog.Description>
</ConfirmDialog>
);
}

View File

@@ -94,8 +94,11 @@ export default {
highlightDialogButtonsSpace: {
height: 16,
width: '100%'
},
learnMoreLink: {
color: BaseTheme.palette.link01,
fontWeight: 'bold'
}
};
/**

View File

@@ -1,9 +1,17 @@
import React, { useCallback } from 'react';
import { useTranslation } from 'react-i18next';
import { useDispatch } from 'react-redux';
import { batch, useDispatch, useSelector } from 'react-redux';
import { setAudioUnmutePermissions, setVideoUnmutePermissions } from '../../../../base/media/actions';
import { IReduxState } from '../../../../app/types';
import { translateToHTML } from '../../../../base/i18n/functions';
import {
setAudioMuted,
setAudioUnmutePermissions,
setVideoMuted,
setVideoUnmutePermissions
} from '../../../../base/media/actions';
import Dialog from '../../../../base/ui/components/web/Dialog';
import { hideDialog } from '../../../../base/dialog/actions';
/**
* Component that renders the dialog for explicit consent for recordings.
@@ -13,14 +21,34 @@ import Dialog from '../../../../base/ui/components/web/Dialog';
export default function RecordingConsentDialog() {
const { t } = useTranslation();
const dispatch = useDispatch();
const { recordings } = useSelector((state: IReduxState) => state['features/base/config']);
const { consentLearnMoreLink } = recordings ?? {};
const learnMore = ` (<a href="${consentLearnMoreLink}" target="_blank" rel="noopener noreferrer">${t('dialog.learnMore')}</a>)`;
const consent = useCallback(() => {
dispatch(setAudioUnmutePermissions(false, true));
dispatch(setVideoUnmutePermissions(false, true));
batch(() => {
dispatch(setAudioUnmutePermissions(false, true));
dispatch(setVideoUnmutePermissions(false, true));
});
}, []);
const consentAndUnmute = useCallback(() => {
batch(() => {
dispatch(setAudioUnmutePermissions(false, true));
dispatch(setVideoUnmutePermissions(false, true));
dispatch(setAudioMuted(false));
dispatch(setVideoMuted(false));
dispatch(hideDialog());
});
}, []);
return (
<Dialog
back = {{
hidden: false,
onClick: consentAndUnmute,
translationKey: 'dialog.UnderstandAndUnmute'
}}
cancel = {{ hidden: true }}
disableBackdropClose = { true }
disableEscape = { true }
@@ -28,9 +56,7 @@ export default function RecordingConsentDialog() {
ok = {{ translationKey: 'dialog.Understand' }}
onSubmit = { consent }
titleKey = 'dialog.recordingInProgressTitle'>
<div>
{t('dialog.recordingInProgressDescription')}
</div>
{ translateToHTML(t, 'dialog.recordingInProgressDescription', { learnMore }) }
</Dialog>
);
}