diff --git a/lang/main.json b/lang/main.json index 56bfdf7de..0826f7640 100644 --- a/lang/main.json +++ b/lang/main.json @@ -213,7 +213,9 @@ "done": "Done", "e2eeDescription": "End-to-End Encryption is currently EXPERIMENTAL. Please keep in mind that turning on end-to-end encryption will effectively disable server-side provided services such as: phone participation. Also keep in mind that the meeting will only work for people joining from browsers with support for insertable streams.", "e2eeLabel": "Enable End-to-End Encryption", + "e2eeDisabledDueToMaxModeDescription": "Cannot enable End-to-End Encryption due to large number of participants in the conference.", "e2eeWarning": "WARNING: Not all participants in this meeting seem to have support for End-to-End encryption. If you enable it they won't be able to see nor hear you.", + "e2eeWillDisableDueToMaxModeDescription": "WARNING: End-to-End Encryption will be automatically disabled if more participants join the conference.", "enterDisplayName": "Enter your name here", "embedMeeting": "Embed meeting", "error": "Error", diff --git a/react/features/base/participants/middleware.js b/react/features/base/participants/middleware.js index 3e897a3c2..f567cfdf3 100644 --- a/react/features/base/participants/middleware.js +++ b/react/features/base/participants/middleware.js @@ -5,6 +5,7 @@ import { batch } from 'react-redux'; import UIEvents from '../../../../service/UI/UIEvents'; import { approveParticipant } from '../../av-moderation/actions'; import { toggleE2EE } from '../../e2ee/actions'; +import { MAX_MODE } from '../../e2ee/constants'; import { NOTIFICATION_TIMEOUT, showNotification } from '../../notifications'; import { isForceMuted } from '../../participants-pane/functions'; import { CALLING, INVITED } from '../../presence-status'; @@ -327,16 +328,20 @@ StateListenerRegistry.register( /** * Handles a E2EE enabled status update. * - * @param {Function} dispatch - The Redux dispatch function. + * @param {Store} store - The redux store. * @param {Object} conference - The conference for which we got an update. * @param {string} participantId - The ID of the participant from which we got an update. * @param {boolean} newValue - The new value of the E2EE enabled status. * @returns {void} */ -function _e2eeUpdated({ dispatch }, conference, participantId, newValue) { +function _e2eeUpdated({ getState, dispatch }, conference, participantId, newValue) { const e2eeEnabled = newValue === 'true'; - dispatch(toggleE2EE(e2eeEnabled)); + const { maxMode } = getState()['features/e2ee']; + + if (maxMode !== MAX_MODE.THRESHOLD_EXCEEDED || !e2eeEnabled) { + dispatch(toggleE2EE(e2eeEnabled)); + } dispatch(participantUpdated({ conference, diff --git a/react/features/e2ee/actionTypes.js b/react/features/e2ee/actionTypes.js index 14fcdaa6d..6def85a50 100644 --- a/react/features/e2ee/actionTypes.js +++ b/react/features/e2ee/actionTypes.js @@ -25,3 +25,12 @@ export const SET_EVERYONE_ENABLED_E2EE = 'SET_EVERYONE_ENABLED_E2EE'; * } */ export const SET_EVERYONE_SUPPORT_E2EE = 'SET_EVERYONE_SUPPORT_E2EE'; + +/** + * The type of the action which signals to set new value E2EE maxMode. + * + * { + * type: SET_MAX_MODE + * } + */ +export const SET_MAX_MODE = 'SET_MAX_MODE'; diff --git a/react/features/e2ee/actions.js b/react/features/e2ee/actions.js index a9b737559..1171b5a34 100644 --- a/react/features/e2ee/actions.js +++ b/react/features/e2ee/actions.js @@ -1,6 +1,6 @@ // @flow -import { SET_EVERYONE_ENABLED_E2EE, SET_EVERYONE_SUPPORT_E2EE, TOGGLE_E2EE } from './actionTypes'; +import { SET_EVERYONE_ENABLED_E2EE, SET_EVERYONE_SUPPORT_E2EE, SET_MAX_MODE, TOGGLE_E2EE } from './actionTypes'; /** * Dispatches an action to enable / disable E2EE. @@ -46,3 +46,16 @@ export function setEveryoneSupportE2EE(everyoneSupportE2EE: boolean) { everyoneSupportE2EE }; } + +/** + * Dispatches an action to set E2EE maxMode. + * + * @param {string} maxMode - The new value. + * @returns {Object} + */ +export function setE2EEMaxMode(maxMode: string) { + return { + type: SET_MAX_MODE, + maxMode + }; +} diff --git a/react/features/e2ee/components/E2EESection.js b/react/features/e2ee/components/E2EESection.js index 8d6c53cb8..bbc902140 100644 --- a/react/features/e2ee/components/E2EESection.js +++ b/react/features/e2ee/components/E2EESection.js @@ -8,17 +8,23 @@ import { translate } from '../../base/i18n'; import { Switch } from '../../base/react'; import { connect } from '../../base/redux'; import { toggleE2EE } from '../actions'; +import { MAX_MODE } from '../constants'; import { doesEveryoneSupportE2EE } from '../functions'; type Props = { + /** + * The resource for the description, computed based on the maxMode and whether the switch is toggled or not. + */ + _descriptionResource: string, + /** * Custom e2ee labels. */ _e2eeLabels: Object, /** - * Whether E2EE is currently enabled or not. + * Whether the switch is currently enabled or not. */ _enabled: boolean, @@ -27,6 +33,11 @@ type Props = { */ _everyoneSupportE2EE: boolean, + /** + * Whether E2EE is currently enabled or not. + */ + _toggled: boolean, + /** * The redux {@code dispatch} function. */ @@ -43,7 +54,7 @@ type State = { /** * True if the switch is toggled on. */ - enabled: boolean + toggled: boolean }; /** @@ -59,10 +70,10 @@ class E2EESection extends Component { * @inheritdoc */ static getDerivedStateFromProps(props: Props, state: Object) { - if (props._enabled !== state.enabled) { + if (props._toggled !== state.toggled) { return { - enabled: props._enabled + toggled: props._toggled }; } @@ -78,7 +89,7 @@ class E2EESection extends Component { super(props); this.state = { - enabled: false + toggled: false }; // Bind event handlers so they are only bound once for every instance. @@ -92,9 +103,9 @@ class E2EESection extends Component { * @returns {ReactElement} */ render() { - const { _e2eeLabels, _everyoneSupportE2EE, t } = this.props; - const { enabled } = this.state; - const description = _e2eeLabels?.description || t('dialog.e2eeDescription'); + const { _descriptionResource, _enabled, _e2eeLabels, _everyoneSupportE2EE, t } = this.props; + const { toggled } = this.state; + const description = _e2eeLabels?.description || t(_descriptionResource); const label = _e2eeLabels?.label || t('dialog.e2eeLabel'); const warning = _e2eeLabels?.warning || t('dialog.e2eeWarning'); @@ -113,9 +124,10 @@ class E2EESection extends Component { { label } + value = { toggled } /> ); @@ -130,10 +142,10 @@ class E2EESection extends Component { * @returns {void} */ _onToggle() { - const newValue = !this.state.enabled; + const newValue = !this.state.toggled; this.setState({ - enabled: newValue + toggled: newValue }); sendAnalytics(createE2EEEvent(`enabled.${String(newValue)}`)); @@ -149,12 +161,28 @@ class E2EESection extends Component { * @returns {Props} */ function mapStateToProps(state) { - const { enabled } = state['features/e2ee']; + const { enabled: e2eeEnabled, maxMode } = state['features/e2ee']; const { e2eeLabels } = state['features/base/config']; + let descriptionResource = ''; + + if (e2eeLabels) { + // When e2eeLabels are present, the descriptionResouse is ignored. + descriptionResource = undefined; + } else if (maxMode === MAX_MODE.THRESHOLD_EXCEEDED) { + descriptionResource = 'dialog.e2eeDisabledDueToMaxModeDescription'; + } else if (maxMode === MAX_MODE.ENABLED) { + descriptionResource = e2eeEnabled + ? 'dialog.e2eeWillDisableDueToMaxModeDescription' : 'dialog.e2eeDisabledDueToMaxModeDescription'; + } else { + descriptionResource = 'dialog.e2eeDescription'; + } + return { + _descriptionResource: descriptionResource, _e2eeLabels: e2eeLabels, - _enabled: enabled, + _enabled: maxMode === MAX_MODE.DISABLED || e2eeEnabled, + _toggled: e2eeEnabled, _everyoneSupportE2EE: doesEveryoneSupportE2EE(state) }; } diff --git a/react/features/e2ee/constants.js b/react/features/e2ee/constants.js index ad7ae39ca..f09cdc268 100644 --- a/react/features/e2ee/constants.js +++ b/react/features/e2ee/constants.js @@ -13,3 +13,43 @@ export const E2EE_OFF_SOUND_ID = 'E2EE_OFF_SOUND'; * @type {string} */ export const E2EE_ON_SOUND_ID = 'E2EE_ON_SOUND'; + +/** + * The number of participants after which e2ee maxMode is set to MAX_MODE.ENABLED. + * + * @type {integer} + */ +export const MAX_MODE_LIMIT = 20; + +/** + * If the number of participants is greater then MAX_MODE_LIMIT + MAX_MODE_THRESHOLD + * e2ee maxMode is set to MAX_MODE.THRESHOLD_EXCEEDED. + * + * @type {integer} + */ +export const MAX_MODE_THRESHOLD = 5; + +export const MAX_MODE = { + /** + * Mode for which the e2ee can be enabled or disabled. + * If e2ee is enabled, e2ee section is enabled with a warning text. + * If e2ee is disabled, e2ee section is disabled with a warning text. + * + * @type {string} + */ + ENABLED: 'max-mode-enabled', + + /** + * Mode for which the e2ee and the e2ee section are automatically disabled. + * + * @type {string} + */ + THRESHOLD_EXCEEDED: 'max-mode-threshold-exceeded', + + /** + * The default e2ee maxMode, e2ee can be enabled/disabled, e2ee section is enabled. + * + * @type {string} + */ + DISABLED: 'max-mode-disabled' +}; diff --git a/react/features/e2ee/functions.js b/react/features/e2ee/functions.js index 03eb975c2..1257733fc 100644 --- a/react/features/e2ee/functions.js +++ b/react/features/e2ee/functions.js @@ -2,6 +2,8 @@ import { getParticipantCount } from '../base/participants/functions'; import { toState } from '../base/redux'; +import { MAX_MODE_LIMIT, MAX_MODE_THRESHOLD } from './constants'; + /** * Gets the value of a specific React {@code Component} prop of the currently * mounted {@link App}. @@ -27,3 +29,29 @@ export function doesEveryoneSupportE2EE(stateful) { return everyoneSupportE2EE; } + +/** + * Returns true is the number of participants is larger than {@code MAX_MODE_LIMIT}. + * + * @param {Function|Object} stateful - The redux store or {@code getState} + * function. + * @returns {boolean}. + */ +export function isMaxModeReached(stateful) { + const participantCount = getParticipantCount(toState(stateful)); + + return participantCount >= MAX_MODE_LIMIT; +} + +/** + * Returns true is the number of participants is larger than {@code MAX_MODE_LIMIT + MAX_MODE_THREHOLD}. + * + * @param {Function|Object} stateful - The redux store or {@code getState} + * function. + * @returns {boolean}. + */ +export function isMaxModeThresholdReached(stateful) { + const participantCount = getParticipantCount(toState(stateful)); + + return participantCount >= MAX_MODE_LIMIT + MAX_MODE_THRESHOLD; +} diff --git a/react/features/e2ee/middleware.js b/react/features/e2ee/middleware.js index 065e573c0..4590365fd 100644 --- a/react/features/e2ee/middleware.js +++ b/react/features/e2ee/middleware.js @@ -3,7 +3,7 @@ import { batch } from 'react-redux'; import { APP_WILL_MOUNT, APP_WILL_UNMOUNT } from '../base/app'; -import { getCurrentConference } from '../base/conference'; +import { CONFERENCE_JOINED, getCurrentConference } from '../base/conference'; import { getLocalParticipant, getParticipantById, @@ -18,8 +18,9 @@ import { MiddlewareRegistry, StateListenerRegistry } from '../base/redux'; import { playSound, registerSound, unregisterSound } from '../base/sounds'; import { TOGGLE_E2EE } from './actionTypes'; -import { setEveryoneEnabledE2EE, setEveryoneSupportE2EE, toggleE2EE } from './actions'; -import { E2EE_OFF_SOUND_ID, E2EE_ON_SOUND_ID } from './constants'; +import { setE2EEMaxMode, setEveryoneEnabledE2EE, setEveryoneSupportE2EE, toggleE2EE } from './actions'; +import { E2EE_OFF_SOUND_ID, E2EE_ON_SOUND_ID, MAX_MODE } from './constants'; +import { isMaxModeReached, isMaxModeThresholdReached } from './functions'; import logger from './logger'; import { E2EE_OFF_SOUND_FILE, E2EE_ON_SOUND_FILE } from './sounds'; @@ -46,6 +47,11 @@ MiddlewareRegistry.register(({ dispatch, getState }) => next => action => { dispatch(unregisterSound(E2EE_ON_SOUND_ID)); break; + case CONFERENCE_JOINED: + _updateMaxMode(dispatch, getState); + + break; + case PARTICIPANT_UPDATED: { const { id, e2eeEnabled, e2eeSupported } = action.participant; const oldParticipant = getParticipantById(getState(), id); @@ -88,7 +94,7 @@ MiddlewareRegistry.register(({ dispatch, getState }) => next => action => { const result = next(action); const { e2eeEnabled, e2eeSupported, local } = action.participant; const { everyoneEnabledE2EE } = getState()['features/e2ee']; - const participantCount = getParticipantCount(getState()); + const participantCount = getParticipantCount(getState); // the initial values if (participantCount === 1) { @@ -116,6 +122,8 @@ MiddlewareRegistry.register(({ dispatch, getState }) => next => action => { dispatch(setEveryoneSupportE2EE(false)); } + _updateMaxMode(dispatch, getState); + return result; } @@ -165,6 +173,8 @@ MiddlewareRegistry.register(({ dispatch, getState }) => next => action => { }); } + _updateMaxMode(dispatch, getState); + return result; } @@ -207,3 +217,24 @@ StateListenerRegistry.register( dispatch(toggleE2EE(false)); } }); + +/** + * Sets the maxMode based on the number of participants in the conference. + * + * @param { Dispatch} dispatch - The redux dispatch function. + * @param {Function|Object} getState - The {@code getState} function. + * @private + * @returns {void} + */ +function _updateMaxMode(dispatch, getState) { + const state = getState(); + + if (isMaxModeThresholdReached(state)) { + dispatch(setE2EEMaxMode(MAX_MODE.THRESHOLD_EXCEEDED)); + dispatch(toggleE2EE(false)); + } else if (isMaxModeReached(state)) { + dispatch(setE2EEMaxMode(MAX_MODE.ENABLED)); + } else { + dispatch(setE2EEMaxMode(MAX_MODE.DISABLED)); + } +} diff --git a/react/features/e2ee/reducer.js b/react/features/e2ee/reducer.js index abb587947..ed757de90 100644 --- a/react/features/e2ee/reducer.js +++ b/react/features/e2ee/reducer.js @@ -5,11 +5,14 @@ import { ReducerRegistry } from '../base/redux'; import { SET_EVERYONE_ENABLED_E2EE, SET_EVERYONE_SUPPORT_E2EE, + SET_MAX_MODE, TOGGLE_E2EE } from './actionTypes'; +import { MAX_MODE } from './constants'; const DEFAULT_STATE = { - enabled: false + enabled: false, + maxMode: MAX_MODE.DISABLED }; /** @@ -33,6 +36,13 @@ ReducerRegistry.register('features/e2ee', (state = DEFAULT_STATE, action) => { everyoneSupportE2EE: action.everyoneSupportE2EE }; + case SET_MAX_MODE: { + return { + ...state, + maxMode: action.maxMode + }; + } + default: return state; }