This commit is contained in:
30
react/features/e2ee/actionTypes.ts
Normal file
30
react/features/e2ee/actionTypes.ts
Normal file
@@ -0,0 +1,30 @@
|
||||
/**
|
||||
* The type of the action which signals that E2EE needs to be enabled / disabled.
|
||||
*
|
||||
* {
|
||||
* type: TOGGLE_E2EE
|
||||
* }
|
||||
*/
|
||||
export const TOGGLE_E2EE = 'TOGGLE_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';
|
||||
|
||||
/**
|
||||
* The type of the action which signals to set media encryption key for e2ee.
|
||||
*
|
||||
* {
|
||||
* type: SET_MEDIA_ENCRYPTION_KEY
|
||||
* }
|
||||
*/
|
||||
export const SET_MEDIA_ENCRYPTION_KEY = 'SET_MEDIA_ENCRYPTION_KEY';
|
||||
|
||||
export const START_VERIFICATION = 'START_VERIFICATION';
|
||||
|
||||
export const PARTICIPANT_VERIFIED = 'PARTICIPANT_VERIFIED';
|
||||
85
react/features/e2ee/actions.ts
Normal file
85
react/features/e2ee/actions.ts
Normal file
@@ -0,0 +1,85 @@
|
||||
import {
|
||||
PARTICIPANT_VERIFIED,
|
||||
SET_MAX_MODE,
|
||||
SET_MEDIA_ENCRYPTION_KEY,
|
||||
START_VERIFICATION,
|
||||
TOGGLE_E2EE } from './actionTypes';
|
||||
|
||||
/**
|
||||
* Dispatches an action to enable / disable E2EE.
|
||||
*
|
||||
* @param {boolean} enabled - Whether E2EE is to be enabled or not.
|
||||
* @returns {Object}
|
||||
*/
|
||||
export function toggleE2EE(enabled: boolean) {
|
||||
return {
|
||||
type: TOGGLE_E2EE,
|
||||
enabled
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Dispatches an action to set media encryption key.
|
||||
*
|
||||
* @param {Object} keyInfo - Json containing key information.
|
||||
* @param {string} [keyInfo.encryptionKey] - The exported encryption key.
|
||||
* @param {number} [keyInfo.index] - The index of the encryption key.
|
||||
* @returns {{
|
||||
* type: SET_MEDIA_ENCRYPTION_KEY,
|
||||
* keyInfo: Object
|
||||
* }}
|
||||
*/
|
||||
export function setMediaEncryptionKey(keyInfo: Object) {
|
||||
return {
|
||||
type: SET_MEDIA_ENCRYPTION_KEY,
|
||||
keyInfo
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Dispatches an action to start participant e2ee verification process.
|
||||
*
|
||||
* @param {string} pId - The participant id.
|
||||
* @returns {{
|
||||
* type: START_VERIFICATION,
|
||||
* pId: string
|
||||
* }}
|
||||
*/
|
||||
export function startVerification(pId: string) {
|
||||
return {
|
||||
type: START_VERIFICATION,
|
||||
pId
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Dispatches an action to set participant e2ee verification status.
|
||||
*
|
||||
* @param {string} pId - The participant id.
|
||||
* @param {boolean} isVerified - The verification status.
|
||||
* @returns {{
|
||||
* type: PARTICIPANT_VERIFIED,
|
||||
* pId: string,
|
||||
* isVerified: boolean
|
||||
* }}
|
||||
*/
|
||||
export function participantVerified(pId: string, isVerified: boolean) {
|
||||
return {
|
||||
type: PARTICIPANT_VERIFIED,
|
||||
pId,
|
||||
isVerified
|
||||
};
|
||||
}
|
||||
59
react/features/e2ee/components/E2EELabel.tsx
Normal file
59
react/features/e2ee/components/E2EELabel.tsx
Normal file
@@ -0,0 +1,59 @@
|
||||
import React from 'react';
|
||||
import { WithTranslation } from 'react-i18next';
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
import { IReduxState } from '../../app/types';
|
||||
import { translate } from '../../base/i18n/functions';
|
||||
import { IconE2EE } from '../../base/icons/svg';
|
||||
import Label from '../../base/label/components/web/Label';
|
||||
import { COLORS } from '../../base/label/constants';
|
||||
import Tooltip from '../../base/tooltip/components/Tooltip';
|
||||
|
||||
export interface IProps extends WithTranslation {
|
||||
|
||||
/**
|
||||
* Custom e2ee labels.
|
||||
*/
|
||||
_e2eeLabels?: any;
|
||||
|
||||
/**
|
||||
* True if the label needs to be rendered, false otherwise.
|
||||
*/
|
||||
_showLabel?: boolean;
|
||||
}
|
||||
|
||||
|
||||
const E2EELabel = ({ _e2eeLabels, _showLabel, t }: IProps) => {
|
||||
if (!_showLabel) {
|
||||
return null;
|
||||
}
|
||||
const content = _e2eeLabels?.tooltip || t('e2ee.labelToolTip');
|
||||
|
||||
return (
|
||||
<Tooltip
|
||||
content = { content }
|
||||
position = { 'bottom' }>
|
||||
<Label
|
||||
color = { COLORS.green }
|
||||
icon = { IconE2EE } />
|
||||
</Tooltip>
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Maps (parts of) the redux state to the associated props of this {@code Component}.
|
||||
*
|
||||
* @param {Object} state - The redux state.
|
||||
* @private
|
||||
* @returns {IProps}
|
||||
*/
|
||||
export function _mapStateToProps(state: IReduxState) {
|
||||
const { e2ee = {} } = state['features/base/config'];
|
||||
|
||||
return {
|
||||
_e2eeLabels: e2ee.labels,
|
||||
_showLabel: state['features/base/participants'].numberOfParticipantsDisabledE2EE === 0
|
||||
};
|
||||
}
|
||||
|
||||
export default translate(connect(_mapStateToProps)(E2EELabel));
|
||||
174
react/features/e2ee/components/E2EESection.tsx
Normal file
174
react/features/e2ee/components/E2EESection.tsx
Normal file
@@ -0,0 +1,174 @@
|
||||
import React, { useCallback, useEffect, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { connect } from 'react-redux';
|
||||
import { makeStyles } from 'tss-react/mui';
|
||||
|
||||
import { createE2EEEvent } from '../../analytics/AnalyticsEvents';
|
||||
import { sendAnalytics } from '../../analytics/functions';
|
||||
import { IReduxState, IStore } from '../../app/types';
|
||||
import Switch from '../../base/ui/components/web/Switch';
|
||||
import { toggleE2EE } from '../actions';
|
||||
import { MAX_MODE } from '../constants';
|
||||
import { doesEveryoneSupportE2EE } from '../functions';
|
||||
|
||||
interface IProps {
|
||||
|
||||
/**
|
||||
* The resource for the description, computed based on the maxMode and whether the switch is toggled or not.
|
||||
*/
|
||||
_descriptionResource?: string;
|
||||
|
||||
/**
|
||||
* Custom e2ee labels.
|
||||
*/
|
||||
_e2eeLabels: any;
|
||||
|
||||
/**
|
||||
* Whether the switch is currently enabled or not.
|
||||
*/
|
||||
_enabled: boolean;
|
||||
|
||||
/**
|
||||
* Indicates whether all participants in the conference currently support E2EE.
|
||||
*/
|
||||
_everyoneSupportE2EE: boolean;
|
||||
|
||||
/**
|
||||
* Whether E2EE is currently enabled or not.
|
||||
*/
|
||||
_toggled: boolean;
|
||||
|
||||
/**
|
||||
* The redux {@code dispatch} function.
|
||||
*/
|
||||
dispatch: IStore['dispatch'];
|
||||
}
|
||||
|
||||
const useStyles = makeStyles()(() => {
|
||||
return {
|
||||
e2eeSection: {
|
||||
display: 'flex',
|
||||
flexDirection: 'column'
|
||||
},
|
||||
|
||||
description: {
|
||||
fontSize: '0.875rem',
|
||||
margin: '15px 0'
|
||||
},
|
||||
|
||||
controlRow: {
|
||||
display: 'flex',
|
||||
justifyContent: 'space-between',
|
||||
marginTop: '15px',
|
||||
|
||||
'& label': {
|
||||
fontSize: '0.875rem',
|
||||
fontWeight: 'bold'
|
||||
}
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
/**
|
||||
* Implements a React {@code Component} for displaying a security dialog section with a field
|
||||
* for setting the E2EE key.
|
||||
*
|
||||
* @param {IProps} props - Component's props.
|
||||
* @returns {JSX}
|
||||
*/
|
||||
const E2EESection = ({
|
||||
_descriptionResource,
|
||||
_enabled,
|
||||
_e2eeLabels,
|
||||
_everyoneSupportE2EE,
|
||||
_toggled,
|
||||
dispatch
|
||||
}: IProps) => {
|
||||
const { classes } = useStyles();
|
||||
const { t } = useTranslation();
|
||||
const [ toggled, setToggled ] = useState(_toggled ?? false);
|
||||
|
||||
useEffect(() => {
|
||||
setToggled(_toggled);
|
||||
}, [ _toggled ]);
|
||||
|
||||
/**
|
||||
* Callback to be invoked when the user toggles E2EE on or off.
|
||||
*
|
||||
* @private
|
||||
* @returns {void}
|
||||
*/
|
||||
const _onToggle = useCallback(() => {
|
||||
const newValue = !toggled;
|
||||
|
||||
setToggled(newValue);
|
||||
|
||||
sendAnalytics(createE2EEEvent(`enabled.${String(newValue)}`));
|
||||
dispatch(toggleE2EE(newValue));
|
||||
}, [ toggled ]);
|
||||
|
||||
const description = _e2eeLabels?.description || t(_descriptionResource ?? '');
|
||||
const label = _e2eeLabels?.label || t('dialog.e2eeLabel');
|
||||
const warning = _e2eeLabels?.warning || t('dialog.e2eeWarning');
|
||||
|
||||
return (
|
||||
<div
|
||||
className = { classes.e2eeSection }
|
||||
id = 'e2ee-section'>
|
||||
<p
|
||||
aria-live = 'polite'
|
||||
className = { classes.description }
|
||||
id = 'e2ee-section-description'>
|
||||
{description}
|
||||
{!_everyoneSupportE2EE && <br />}
|
||||
{!_everyoneSupportE2EE && warning}
|
||||
</p>
|
||||
<div className = { classes.controlRow }>
|
||||
<label htmlFor = 'e2ee-section-switch'>
|
||||
{label}
|
||||
</label>
|
||||
<Switch
|
||||
checked = { toggled }
|
||||
disabled = { !_enabled }
|
||||
id = 'e2ee-section-switch'
|
||||
onChange = { _onToggle } />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Maps (parts of) the Redux state to the associated props for this component.
|
||||
*
|
||||
* @param {Object} state - The Redux state.
|
||||
* @private
|
||||
* @returns {IProps}
|
||||
*/
|
||||
function mapStateToProps(state: IReduxState) {
|
||||
const { enabled: e2eeEnabled, maxMode } = state['features/e2ee'];
|
||||
const { e2ee = {} } = state['features/base/config'];
|
||||
|
||||
let descriptionResource: string | undefined = '';
|
||||
|
||||
if (e2ee.labels) {
|
||||
// When e2eeLabels are present, the description resource 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: e2ee.labels,
|
||||
_enabled: maxMode === MAX_MODE.DISABLED || e2eeEnabled,
|
||||
_toggled: e2eeEnabled,
|
||||
_everyoneSupportE2EE: Boolean(doesEveryoneSupportE2EE(state))
|
||||
};
|
||||
}
|
||||
|
||||
export default connect(mapStateToProps)(E2EESection);
|
||||
123
react/features/e2ee/components/ParticipantVerificationDialog.tsx
Normal file
123
react/features/e2ee/components/ParticipantVerificationDialog.tsx
Normal file
@@ -0,0 +1,123 @@
|
||||
import React, { useCallback } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { connect } from 'react-redux';
|
||||
import { makeStyles } from 'tss-react/mui';
|
||||
|
||||
import { IReduxState, IStore } from '../../app/types';
|
||||
import { getParticipantById } from '../../base/participants/functions';
|
||||
import Dialog from '../../base/ui/components/web/Dialog';
|
||||
import { participantVerified } from '../actions';
|
||||
import { ISas } from '../reducer';
|
||||
|
||||
interface IProps {
|
||||
decimal: string;
|
||||
dispatch: IStore['dispatch'];
|
||||
emoji: string;
|
||||
pId: string;
|
||||
participantName?: string;
|
||||
sas: ISas;
|
||||
}
|
||||
|
||||
const useStyles = makeStyles()(() => {
|
||||
return {
|
||||
container: {
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
margin: '16px'
|
||||
},
|
||||
row: {
|
||||
alignSelf: 'center',
|
||||
display: 'flex'
|
||||
},
|
||||
item: {
|
||||
textAlign: 'center',
|
||||
margin: '16px'
|
||||
},
|
||||
emoji: {
|
||||
fontSize: '2.5rem',
|
||||
margin: '12px'
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
const ParticipantVerificationDialog = ({
|
||||
dispatch,
|
||||
participantName,
|
||||
pId,
|
||||
sas
|
||||
}: IProps) => {
|
||||
const { classes } = useStyles();
|
||||
const { t } = useTranslation();
|
||||
|
||||
const _onDismissed = useCallback(() => {
|
||||
dispatch(participantVerified(pId, false));
|
||||
|
||||
return true;
|
||||
}, [ pId ]);
|
||||
|
||||
const _onConfirmed = useCallback(() => {
|
||||
dispatch(participantVerified(pId, true));
|
||||
|
||||
return true;
|
||||
}, [ pId ]);
|
||||
|
||||
const { emoji } = sas;
|
||||
|
||||
return (
|
||||
<Dialog
|
||||
cancel = {{ translationKey: 'dialog.verifyParticipantDismiss' }}
|
||||
ok = {{ translationKey: 'dialog.verifyParticipantConfirm' }}
|
||||
onCancel = { _onDismissed }
|
||||
onSubmit = { _onConfirmed }
|
||||
titleKey = 'dialog.verifyParticipantTitle'>
|
||||
<div>
|
||||
{t('dialog.verifyParticipantQuestion', { participantName })}
|
||||
</div>
|
||||
|
||||
<div className = { classes.container }>
|
||||
<div className = { classes.row }>
|
||||
{/* @ts-ignore */}
|
||||
{emoji.slice(0, 4).map((e: Array<string>) =>
|
||||
(<div
|
||||
className = { classes.item }
|
||||
key = { e.toString() }>
|
||||
<div className = { classes.emoji }>{e[0]}</div>
|
||||
<div>{e[1].charAt(0).toUpperCase() + e[1].slice(1)}</div>
|
||||
</div>))}
|
||||
</div>
|
||||
|
||||
<div className = { classes.row }>
|
||||
{/* @ts-ignore */}
|
||||
{emoji.slice(4, 7).map((e: Array<string>) =>
|
||||
(<div
|
||||
className = { classes.item }
|
||||
key = { e.toString() }>
|
||||
<div className = { classes.emoji }>{e[0]} </div>
|
||||
<div>{e[1].charAt(0).toUpperCase() + e[1].slice(1)}</div>
|
||||
</div>))}
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Maps part of the Redux store to the props of this component.
|
||||
*
|
||||
* @param {IReduxState} state - The Redux state.
|
||||
* @param {IProps} ownProps - The own props of the component.
|
||||
* @returns {IProps}
|
||||
*/
|
||||
export function _mapStateToProps(state: IReduxState, ownProps: IProps) {
|
||||
const participant = getParticipantById(state, ownProps.pId);
|
||||
|
||||
return {
|
||||
sas: ownProps.sas,
|
||||
pId: ownProps.pId,
|
||||
participantName: participant?.name
|
||||
};
|
||||
}
|
||||
|
||||
export default connect(_mapStateToProps)(ParticipantVerificationDialog);
|
||||
53
react/features/e2ee/constants.ts
Normal file
53
react/features/e2ee/constants.ts
Normal file
@@ -0,0 +1,53 @@
|
||||
/**
|
||||
* The identifier of the sound to be played when e2ee is disabled.
|
||||
*
|
||||
* @type {string}
|
||||
*/
|
||||
export const E2EE_OFF_SOUND_ID = 'E2EE_OFF_SOUND';
|
||||
|
||||
/**
|
||||
* The identifier of the sound to be played when e2ee is enabled.
|
||||
*
|
||||
* @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'
|
||||
};
|
||||
120
react/features/e2ee/functions.ts
Normal file
120
react/features/e2ee/functions.ts
Normal file
@@ -0,0 +1,120 @@
|
||||
import i18next from 'i18next';
|
||||
|
||||
import { IReduxState, IStore } from '../app/types';
|
||||
import { IStateful } from '../base/app/types';
|
||||
import { getSoundFileSrc } from '../base/media/functions';
|
||||
import { getParticipantById, getParticipantCount, getParticipantCountWithFake } from '../base/participants/functions';
|
||||
import { toState } from '../base/redux/functions';
|
||||
import { registerSound, unregisterSound } from '../base/sounds/actions';
|
||||
|
||||
import {
|
||||
E2EE_OFF_SOUND_ID,
|
||||
E2EE_ON_SOUND_ID,
|
||||
MAX_MODE_LIMIT,
|
||||
MAX_MODE_THRESHOLD
|
||||
} from './constants';
|
||||
import {
|
||||
E2EE_OFF_SOUND_FILE,
|
||||
E2EE_ON_SOUND_FILE
|
||||
} from './sounds';
|
||||
|
||||
|
||||
/**
|
||||
* Gets the value of a specific React {@code Component} prop of the currently
|
||||
* mounted {@link App}.
|
||||
*
|
||||
* @param {IStateful} stateful - The redux store or {@code getState}
|
||||
* function.
|
||||
* @param {string} propName - The name of the React {@code Component} prop of
|
||||
* the currently mounted {@code App} to get.
|
||||
* @returns {*} The value of the specified React {@code Component} prop of the
|
||||
* currently mounted {@code App}.
|
||||
*/
|
||||
export function doesEveryoneSupportE2EE(stateful: IStateful) {
|
||||
const state = toState(stateful);
|
||||
const { numberOfParticipantsNotSupportingE2EE } = state['features/base/participants'];
|
||||
const { e2eeSupported } = state['features/base/conference'];
|
||||
const participantCount = getParticipantCountWithFake(state);
|
||||
|
||||
if (participantCount === 1) {
|
||||
// This will happen if we are alone.
|
||||
|
||||
return e2eeSupported;
|
||||
}
|
||||
|
||||
return numberOfParticipantsNotSupportingE2EE === 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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: IStateful) {
|
||||
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: IStateful) {
|
||||
const participantCount = getParticipantCount(toState(stateful));
|
||||
|
||||
return participantCount >= MAX_MODE_LIMIT + MAX_MODE_THRESHOLD;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether e2ee is enabled by the backend.
|
||||
*
|
||||
* @param {Object} state - The redux state.
|
||||
* @param {string} pId - The participant id.
|
||||
* @returns {boolean}
|
||||
*/
|
||||
export function displayVerification(state: IReduxState, pId: string) {
|
||||
const { conference } = state['features/base/conference'];
|
||||
const participant = getParticipantById(state, pId);
|
||||
|
||||
return Boolean(conference?.isE2EEEnabled()
|
||||
&& participant?.e2eeVerificationAvailable
|
||||
&& participant?.e2eeVerified === undefined);
|
||||
}
|
||||
|
||||
/**
|
||||
* Unregisters the audio files based on locale.
|
||||
*
|
||||
* @param {Dispatch<any>} dispatch - The redux dispatch function.
|
||||
* @returns {void}
|
||||
*/
|
||||
export function unregisterE2eeAudioFiles(dispatch: IStore['dispatch']) {
|
||||
dispatch(unregisterSound(E2EE_OFF_SOUND_ID));
|
||||
dispatch(unregisterSound(E2EE_ON_SOUND_ID));
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers the audio files based on locale.
|
||||
*
|
||||
* @param {Dispatch<any>} dispatch - The redux dispatch function.
|
||||
* @param {boolean|undefined} shouldUnregister - Whether the sounds should be unregistered.
|
||||
* @returns {void}
|
||||
*/
|
||||
export function registerE2eeAudioFiles(dispatch: IStore['dispatch'], shouldUnregister?: boolean) {
|
||||
const language = i18next.language;
|
||||
|
||||
shouldUnregister && unregisterE2eeAudioFiles(dispatch);
|
||||
|
||||
dispatch(registerSound(
|
||||
E2EE_OFF_SOUND_ID,
|
||||
getSoundFileSrc(E2EE_OFF_SOUND_FILE, language)));
|
||||
|
||||
dispatch(registerSound(
|
||||
E2EE_ON_SOUND_ID,
|
||||
getSoundFileSrc(E2EE_ON_SOUND_FILE, language)));
|
||||
}
|
||||
3
react/features/e2ee/logger.ts
Normal file
3
react/features/e2ee/logger.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
import { getLogger } from '../base/logging/functions';
|
||||
|
||||
export default getLogger('features/e2ee');
|
||||
215
react/features/e2ee/middleware.ts
Normal file
215
react/features/e2ee/middleware.ts
Normal file
@@ -0,0 +1,215 @@
|
||||
import { IStore } from '../app/types';
|
||||
import { APP_WILL_MOUNT, APP_WILL_UNMOUNT } from '../base/app/actionTypes';
|
||||
import { CONFERENCE_JOINED } from '../base/conference/actionTypes';
|
||||
import { getCurrentConference } from '../base/conference/functions';
|
||||
import { openDialog } from '../base/dialog/actions';
|
||||
import { JitsiConferenceEvents } from '../base/lib-jitsi-meet';
|
||||
import { PARTICIPANT_JOINED, PARTICIPANT_LEFT } from '../base/participants/actionTypes';
|
||||
import { participantUpdated } from '../base/participants/actions';
|
||||
import {
|
||||
getLocalParticipant,
|
||||
getParticipantById,
|
||||
isScreenShareParticipant
|
||||
} from '../base/participants/functions';
|
||||
import MiddlewareRegistry from '../base/redux/MiddlewareRegistry';
|
||||
import StateListenerRegistry from '../base/redux/StateListenerRegistry';
|
||||
import { playSound } from '../base/sounds/actions';
|
||||
|
||||
import { PARTICIPANT_VERIFIED, SET_MEDIA_ENCRYPTION_KEY, START_VERIFICATION, TOGGLE_E2EE } from './actionTypes';
|
||||
import { setE2EEMaxMode, toggleE2EE } from './actions';
|
||||
import ParticipantVerificationDialog from './components/ParticipantVerificationDialog';
|
||||
import { E2EE_OFF_SOUND_ID, E2EE_ON_SOUND_ID, MAX_MODE } from './constants';
|
||||
import {
|
||||
isMaxModeReached,
|
||||
isMaxModeThresholdReached,
|
||||
registerE2eeAudioFiles,
|
||||
unregisterE2eeAudioFiles
|
||||
} from './functions';
|
||||
import logger from './logger';
|
||||
|
||||
/**
|
||||
* Middleware that captures actions related to E2EE.
|
||||
*
|
||||
* @param {Store} store - The redux store.
|
||||
* @returns {Function}
|
||||
*/
|
||||
MiddlewareRegistry.register(({ dispatch, getState }) => next => action => {
|
||||
const conference = getCurrentConference(getState);
|
||||
|
||||
switch (action.type) {
|
||||
case APP_WILL_MOUNT:
|
||||
registerE2eeAudioFiles(dispatch);
|
||||
break;
|
||||
|
||||
case APP_WILL_UNMOUNT:
|
||||
unregisterE2eeAudioFiles(dispatch);
|
||||
break;
|
||||
|
||||
case CONFERENCE_JOINED:
|
||||
_updateMaxMode(dispatch, getState);
|
||||
|
||||
break;
|
||||
|
||||
case PARTICIPANT_JOINED: {
|
||||
const result = next(action);
|
||||
|
||||
if (!isScreenShareParticipant(action.participant) && !action.participant.local) {
|
||||
_updateMaxMode(dispatch, getState);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
case PARTICIPANT_LEFT: {
|
||||
const participant = getParticipantById(getState(), action.participant?.id);
|
||||
const result = next(action);
|
||||
|
||||
if (!isScreenShareParticipant(participant)) {
|
||||
_updateMaxMode(dispatch, getState);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
case TOGGLE_E2EE: {
|
||||
if (conference?.isE2EESupported() && conference.isE2EEEnabled() !== action.enabled) {
|
||||
logger.debug(`E2EE will be ${action.enabled ? 'enabled' : 'disabled'}`);
|
||||
conference.toggleE2EE(action.enabled);
|
||||
|
||||
// Broadcast that we enabled / disabled E2EE.
|
||||
const participant = getLocalParticipant(getState);
|
||||
|
||||
dispatch(participantUpdated({
|
||||
e2eeEnabled: action.enabled,
|
||||
id: participant?.id ?? '',
|
||||
local: true
|
||||
}));
|
||||
|
||||
const soundID = action.enabled ? E2EE_ON_SOUND_ID : E2EE_OFF_SOUND_ID;
|
||||
|
||||
dispatch(playSound(soundID));
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
case SET_MEDIA_ENCRYPTION_KEY: {
|
||||
if (conference?.isE2EESupported()) {
|
||||
const { exportedKey, index } = action.keyInfo;
|
||||
|
||||
if (exportedKey) {
|
||||
window.crypto.subtle.importKey(
|
||||
'raw',
|
||||
new Uint8Array(exportedKey),
|
||||
'AES-GCM',
|
||||
false,
|
||||
[ 'encrypt', 'decrypt' ])
|
||||
.then(
|
||||
encryptionKey => {
|
||||
conference.setMediaEncryptionKey({
|
||||
encryptionKey,
|
||||
index
|
||||
});
|
||||
})
|
||||
.catch(error => logger.error('SET_MEDIA_ENCRYPTION_KEY error', error));
|
||||
} else {
|
||||
conference.setMediaEncryptionKey({
|
||||
encryptionKey: false,
|
||||
index
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
case PARTICIPANT_VERIFIED: {
|
||||
const { isVerified, pId } = action;
|
||||
|
||||
conference?.markParticipantVerified(pId, isVerified);
|
||||
break;
|
||||
}
|
||||
|
||||
case START_VERIFICATION: {
|
||||
conference?.startVerification(action.pId);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return next(action);
|
||||
});
|
||||
|
||||
/**
|
||||
* Set up state change listener to perform maintenance tasks when the conference
|
||||
* is left or failed.
|
||||
*/
|
||||
StateListenerRegistry.register(
|
||||
state => getCurrentConference(state),
|
||||
(conference, { dispatch }, previousConference) => {
|
||||
if (previousConference) {
|
||||
dispatch(toggleE2EE(false));
|
||||
}
|
||||
|
||||
if (conference) {
|
||||
conference.on(JitsiConferenceEvents.E2EE_VERIFICATION_AVAILABLE, (pId: string) => {
|
||||
dispatch(participantUpdated({
|
||||
e2eeVerificationAvailable: true,
|
||||
id: pId
|
||||
}));
|
||||
});
|
||||
|
||||
conference.on(JitsiConferenceEvents.E2EE_VERIFICATION_READY, (pId: string, sas: object) => {
|
||||
dispatch(openDialog(ParticipantVerificationDialog, { pId,
|
||||
sas }));
|
||||
});
|
||||
|
||||
conference.on(JitsiConferenceEvents.E2EE_VERIFICATION_COMPLETED,
|
||||
(pId: string, success: boolean, message: string) => {
|
||||
if (message) {
|
||||
logger.warn('E2EE_VERIFICATION_COMPLETED warning', message);
|
||||
}
|
||||
dispatch(participantUpdated({
|
||||
e2eeVerified: success,
|
||||
id: pId
|
||||
}));
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* Sets the maxMode based on the number of participants in the conference.
|
||||
*
|
||||
* @param { Dispatch<any>} dispatch - The redux dispatch function.
|
||||
* @param {Function|Object} getState - The {@code getState} function.
|
||||
* @private
|
||||
* @returns {void}
|
||||
*/
|
||||
function _updateMaxMode(dispatch: IStore['dispatch'], getState: IStore['getState']) {
|
||||
const state = getState();
|
||||
|
||||
const { e2ee = {} } = state['features/base/config'];
|
||||
|
||||
if (e2ee.externallyManagedKey) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { maxMode, enabled } = state['features/e2ee'];
|
||||
const isMaxModeThresholdReachedValue = isMaxModeThresholdReached(state);
|
||||
let newMaxMode: string;
|
||||
|
||||
if (isMaxModeThresholdReachedValue) {
|
||||
newMaxMode = MAX_MODE.THRESHOLD_EXCEEDED;
|
||||
} else if (isMaxModeReached(state)) {
|
||||
newMaxMode = MAX_MODE.ENABLED;
|
||||
} else {
|
||||
newMaxMode = MAX_MODE.DISABLED;
|
||||
}
|
||||
|
||||
if (maxMode !== newMaxMode) {
|
||||
dispatch(setE2EEMaxMode(newMaxMode));
|
||||
}
|
||||
|
||||
if (isMaxModeThresholdReachedValue && !enabled) {
|
||||
dispatch(toggleE2EE(false));
|
||||
}
|
||||
}
|
||||
44
react/features/e2ee/reducer.ts
Normal file
44
react/features/e2ee/reducer.ts
Normal file
@@ -0,0 +1,44 @@
|
||||
import ReducerRegistry from '../base/redux/ReducerRegistry';
|
||||
|
||||
import {
|
||||
SET_MAX_MODE,
|
||||
TOGGLE_E2EE
|
||||
} from './actionTypes';
|
||||
import { MAX_MODE } from './constants';
|
||||
|
||||
const DEFAULT_STATE = {
|
||||
enabled: false,
|
||||
maxMode: MAX_MODE.DISABLED
|
||||
};
|
||||
|
||||
export interface IE2EEState {
|
||||
enabled: boolean;
|
||||
maxMode: string;
|
||||
}
|
||||
|
||||
export interface ISas {
|
||||
emoji: Array<string>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Reduces the Redux actions of the feature features/e2ee.
|
||||
*/
|
||||
ReducerRegistry.register<IE2EEState>('features/e2ee', (state = DEFAULT_STATE, action): IE2EEState => {
|
||||
switch (action.type) {
|
||||
case TOGGLE_E2EE:
|
||||
return {
|
||||
...state,
|
||||
enabled: action.enabled
|
||||
};
|
||||
|
||||
case SET_MAX_MODE: {
|
||||
return {
|
||||
...state,
|
||||
maxMode: action.maxMode
|
||||
};
|
||||
}
|
||||
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
});
|
||||
13
react/features/e2ee/sounds.ts
Normal file
13
react/features/e2ee/sounds.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
/**
|
||||
* The name of the bundled audio file which will be played when e2ee is disabled.
|
||||
*
|
||||
* @type {string}
|
||||
*/
|
||||
export const E2EE_OFF_SOUND_FILE = 'e2eeOff.mp3';
|
||||
|
||||
/**
|
||||
* The name of the bundled audio file which will be played when e2ee is enabled.
|
||||
*
|
||||
* @type {string}
|
||||
*/
|
||||
export const E2EE_ON_SOUND_FILE = 'e2eeOn.mp3';
|
||||
Reference in New Issue
Block a user