feat(E2EE) add initial SAS verification UI

This commit is contained in:
tmoldovan8x8 2022-12-06 19:29:33 +02:00 committed by GitHub
parent 1139311809
commit 4c9bfe3d4d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
14 changed files with 442 additions and 3 deletions

View File

@ -147,6 +147,7 @@
"bridgeCount": "Server count: ", "bridgeCount": "Server count: ",
"codecs": "Codecs (A/V): ", "codecs": "Codecs (A/V): ",
"connectedTo": "Connected to:", "connectedTo": "Connected to:",
"e2eeVerified": "E2EE verified:",
"framerate": "Frame rate:", "framerate": "Frame rate:",
"less": "Show less", "less": "Show less",
"localaddress": "Local address:", "localaddress": "Local address:",
@ -408,6 +409,10 @@
"user": "User", "user": "User",
"userIdentifier": "User identifier", "userIdentifier": "User identifier",
"userPassword": "User password", "userPassword": "User password",
"verifyParticipantConfirm": "They match",
"verifyParticipantDismiss": "They do not match",
"verifyParticipantQuestion": "EXPERIMENTAL: Ask participant {{participantName}} if they see the same content, in the same order.",
"verifyParticipantTitle": "User verification",
"videoLink": "Video link", "videoLink": "Video link",
"viewUpgradeOptions": "View upgrade options", "viewUpgradeOptions": "View upgrade options",
"viewUpgradeOptionsContent": "To get unlimited access to premium features like recording, transcriptions, RTMP Streaming & more, you'll need to upgrade your plan.", "viewUpgradeOptionsContent": "To get unlimited access to premium features like recording, transcriptions, RTMP Streaming & more, you'll need to upgrade your plan.",
@ -1297,6 +1302,7 @@
"show": "Show on stage", "show": "Show on stage",
"showSelfView": "Show self view", "showSelfView": "Show self view",
"unpinFromStage": "Unpin", "unpinFromStage": "Unpin",
"verify": "Verify participant",
"videoMuted": "Camera disabled", "videoMuted": "Camera disabled",
"videomute": "Participant has stopped the camera" "videomute": "Participant has stopped the camera"
}, },

View File

@ -59,6 +59,7 @@ export interface IJitsiConference {
grantOwner: Function; grantOwner: Function;
isAVModerationSupported: Function; isAVModerationSupported: Function;
isCallstatsEnabled: Function; isCallstatsEnabled: Function;
isE2EEEnabled: Function;
isEndConferenceSupported: Function; isEndConferenceSupported: Function;
isLobbySupported: Function; isLobbySupported: Function;
isSIPCallingSupported: Function; isSIPCallingSupported: Function;
@ -89,6 +90,7 @@ export interface IJitsiConference {
setReceiverConstraints: Function; setReceiverConstraints: Function;
setSenderVideoConstraint: Function; setSenderVideoConstraint: Function;
setSubject: Function; setSubject: Function;
startVerification: Function;
} }
export interface IConferenceState { export interface IConferenceState {

View File

@ -4,6 +4,7 @@ import WaitForOwnerDialog from '../../authentication/components/web/WaitForOwner
import ChatPrivacyDialog from '../../chat/components/web/ChatPrivacyDialog'; import ChatPrivacyDialog from '../../chat/components/web/ChatPrivacyDialog';
import DesktopPicker from '../../desktop-picker/components/DesktopPicker'; import DesktopPicker from '../../desktop-picker/components/DesktopPicker';
import DisplayNamePrompt from '../../display-name/components/web/DisplayNamePrompt'; import DisplayNamePrompt from '../../display-name/components/web/DisplayNamePrompt';
import ParticipantVerificationDialog from '../../e2ee/components/ParticipantVerificationDialog';
import EmbedMeetingDialog from '../../embed-meeting/components/EmbedMeetingDialog'; import EmbedMeetingDialog from '../../embed-meeting/components/EmbedMeetingDialog';
// @ts-ignore // @ts-ignore
import FeedbackDialog from '../../feedback/components/FeedbackDialog.web'; import FeedbackDialog from '../../feedback/components/FeedbackDialog.web';
@ -49,7 +50,7 @@ const NEW_DIALOG_LIST = [ KeyboardShortcutsDialog, ChatPrivacyDialog, DisplayNam
SharedVideoDialog, SpeakerStats, LanguageSelectorDialog, MuteEveryoneDialog, MuteEveryonesVideoDialog, SharedVideoDialog, SpeakerStats, LanguageSelectorDialog, MuteEveryoneDialog, MuteEveryonesVideoDialog,
GrantModeratorDialog, KickRemoteParticipantDialog, MuteRemoteParticipantsVideoDialog, VideoQualityDialog, GrantModeratorDialog, KickRemoteParticipantDialog, MuteRemoteParticipantsVideoDialog, VideoQualityDialog,
VirtualBackgroundDialog, LoginDialog, WaitForOwnerDialog, DesktopPicker, RemoteControlAuthorizationDialog, VirtualBackgroundDialog, LoginDialog, WaitForOwnerDialog, DesktopPicker, RemoteControlAuthorizationDialog,
LogoutDialog, SalesforceLinkDialog ]; LogoutDialog, SalesforceLinkDialog, ParticipantVerificationDialog ];
// This function is necessary while the transition from @atlaskit dialog to our component is ongoing. // This function is necessary while the transition from @atlaskit dialog to our component is ongoing.
const isNewDialog = (component: any) => NEW_DIALOG_LIST.some(comp => comp === component); const isNewDialog = (component: any) => NEW_DIALOG_LIST.some(comp => comp === component);

View File

@ -13,6 +13,8 @@ export interface IParticipant {
dominantSpeaker?: boolean; dominantSpeaker?: boolean;
e2eeEnabled?: boolean; e2eeEnabled?: boolean;
e2eeSupported?: boolean; e2eeSupported?: boolean;
e2eeVerificationAvailable?: boolean;
e2eeVerified?: boolean;
email?: string; email?: string;
fakeParticipant?: FakeParticipant; fakeParticipant?: FakeParticipant;
features?: { features?: {

View File

@ -189,6 +189,7 @@ class ConnectionIndicatorContent extends AbstractConnectionIndicator<Props, Stat
codec = { codec } codec = { codec }
connectionSummary = { this._getConnectionStatusTip() } connectionSummary = { this._getConnectionStatusTip() }
disableShowMoreStats = { this.props._disableShowMoreStats } disableShowMoreStats = { this.props._disableShowMoreStats }
e2eeVerified = { this.props._isE2EEVerified }
enableSaveLogs = { this.props._enableSaveLogs } enableSaveLogs = { this.props._enableSaveLogs }
framerate = { framerate } framerate = { framerate }
isLocalVideo = { this.props._isLocalVideo } isLocalVideo = { this.props._isLocalVideo }
@ -328,6 +329,7 @@ export function _mapStateToProps(state: Object, ownProps: Props) {
_disableShowMoreStats: state['features/base/config'].disableShowMoreStats, _disableShowMoreStats: state['features/base/config'].disableShowMoreStats,
_isConnectionStatusInactive, _isConnectionStatusInactive,
_isConnectionStatusInterrupted, _isConnectionStatusInterrupted,
_isE2EEVerified: participant?.e2eeVerified,
_isVirtualScreenshareParticipant: isScreenShareParticipant(participant), _isVirtualScreenshareParticipant: isScreenShareParticipant(participant),
_isLocalVideo: participant?.local, _isLocalVideo: participant?.local,
_region: participant?.region, _region: participant?.region,

View File

@ -73,6 +73,11 @@ interface IProps extends WithTranslation {
*/ */
disableShowMoreStats: boolean; disableShowMoreStats: boolean;
/**
* Whether or not the participant was verified.
*/
e2eeVerified: boolean;
/** /**
* Whether or not should display the "Save Logs" link. * Whether or not should display the "Save Logs" link.
*/ */
@ -486,6 +491,31 @@ class ConnectionStatsTable extends Component<IProps> {
); );
} }
/**
* Creates a a table row as a ReactElement for displaying e2ee verication status, if present.
*
* @private
* @returns {ReactElement}
*/
_renderE2EEVerified() {
const { e2eeVerified, t } = this.props;
if (e2eeVerified === undefined) {
return;
}
const status = e2eeVerified ? '\u{2705}' : '\u{274C}';
return (
<tr>
<td>
<span>{ t('connectionindicator.e2eeVerified') }</span>
</td>
<td>{ status }</td>
</tr>
);
}
/** /**
* Creates a table row as a ReactElement for displaying a summary message * Creates a table row as a ReactElement for displaying a summary message
@ -726,6 +756,7 @@ class ConnectionStatsTable extends Component<IProps> {
{ this._renderResolution() } { this._renderResolution() }
{ this._renderFrameRate() } { this._renderFrameRate() }
{ this._renderCodecs() } { this._renderCodecs() }
{ this._renderE2EEVerified() }
</tbody> </tbody>
</table> </table>
); );

View File

@ -43,3 +43,7 @@ export const SET_MAX_MODE = 'SET_MAX_MODE';
* } * }
*/ */
export const SET_MEDIA_ENCRYPTION_KEY = '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';

View File

@ -1,8 +1,10 @@
import { import {
PARTICIPANT_VERIFIED,
SET_EVERYONE_ENABLED_E2EE, SET_EVERYONE_ENABLED_E2EE,
SET_EVERYONE_SUPPORT_E2EE, SET_EVERYONE_SUPPORT_E2EE,
SET_MAX_MODE, SET_MAX_MODE,
SET_MEDIA_ENCRYPTION_KEY, SET_MEDIA_ENCRYPTION_KEY,
START_VERIFICATION,
TOGGLE_E2EE } from './actionTypes'; TOGGLE_E2EE } from './actionTypes';
/** /**
@ -80,3 +82,38 @@ export function setMediaEncryptionKey(keyInfo: Object) {
keyInfo keyInfo
}; };
} }
/**
* Dispatches an action to start participant e2ee verficiation 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 verifcation status.
* @returns {{
* type: PARTICIPANT_VERIFIED,
* pId: string,
* isVerified: boolean
* }}
*/
export function participantVerified(pId: string, isVerified: boolean) {
return {
type: PARTICIPANT_VERIFIED,
pId,
isVerified
};
}

View File

@ -0,0 +1,166 @@
import { withStyles } from '@mui/styles';
import React, { Component } from 'react';
import { WithTranslation } from 'react-i18next';
import { IReduxState, IStore } from '../../app/types';
import { translate } from '../../base/i18n/functions';
import { getParticipantById } from '../../base/participants/functions';
import { connect } from '../../base/redux/functions';
import Dialog from '../../base/ui/components/web/Dialog';
import { participantVerified } from '../actions';
import { ISas } from '../reducer';
interface IProps extends WithTranslation {
classes: any;
decimal: string;
dispatch: IStore['dispatch'];
emoji: string;
pId: string;
participantName: string;
sas: ISas;
}
/**
* Creates the styles for the component.
*
* @param {Object} theme - The current UI theme.
*
* @returns {Object}
*/
const styles = () => {
return {
container: {
display: 'flex',
flexDirection: 'column',
margin: '16px'
},
row: {
alignSelf: 'center',
display: 'flex'
},
item: {
textAlign: 'center',
margin: '16px'
},
emoji: {
fontSize: '40px',
margin: '12px'
}
};
};
/**
* Class for the dialog displayed for E2EE sas verification.
*/
export class ParticipantVerificationDialog extends Component<IProps> {
/**
* Instantiates a new instance.
*
* @inheritdoc
*/
constructor(props: IProps) {
super(props);
this._onConfirmed = this._onConfirmed.bind(this);
this._onDismissed = this._onDismissed.bind(this);
}
/**
* Implements React's {@link Component#render()}.
*
* @inheritdoc
* @returns {ReactElement}
*/
render() {
const { emoji } = this.props.sas;
const { participantName } = this.props;
const { classes, t } = this.props;
return (
<Dialog
cancel = {{ translationKey: 'dialog.verifyParticipantDismiss' }}
ok = {{ translationKey: 'dialog.verifyParticipantConfirm' }}
onCancel = { this._onDismissed }
onSubmit = { this._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>
);
}
/**
* Notifies this ParticipantVerificationDialog that it has been dismissed by cancel.
*
* @private
* @returns {void}
*/
_onDismissed() {
this.props.dispatch(participantVerified(this.props.pId, false));
return true;
}
/**
* Notifies this ParticipantVerificationDialog that it has been dismissed with confirmation.
*
* @private
* @returns {void}
*/
_onConfirmed() {
this.props.dispatch(participantVerified(this.props.pId, true));
return true;
}
}
/**
* 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 translate(connect(_mapStateToProps)(
// @ts-ignore
withStyles(styles)(ParticipantVerificationDialog)));

View File

@ -1,7 +1,9 @@
import { IReduxState } from '../app/types';
import { IStateful } from '../base/app/types'; import { IStateful } from '../base/app/types';
import { getParticipantCount } from '../base/participants/functions'; import { getParticipantById, getParticipantCount } from '../base/participants/functions';
import { toState } from '../base/redux/functions'; import { toState } from '../base/redux/functions';
import { MAX_MODE_LIMIT, MAX_MODE_THRESHOLD } from './constants'; import { MAX_MODE_LIMIT, MAX_MODE_THRESHOLD } from './constants';
/** /**
@ -55,3 +57,19 @@ export function isMaxModeThresholdReached(stateful: IStateful) {
return participantCount >= MAX_MODE_LIMIT + MAX_MODE_THRESHOLD; 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);
}

View File

@ -4,6 +4,8 @@ import { IStore } from '../app/types';
import { APP_WILL_MOUNT, APP_WILL_UNMOUNT } from '../base/app/actionTypes'; import { APP_WILL_MOUNT, APP_WILL_UNMOUNT } from '../base/app/actionTypes';
import { CONFERENCE_JOINED } from '../base/conference/actionTypes'; import { CONFERENCE_JOINED } from '../base/conference/actionTypes';
import { getCurrentConference } from '../base/conference/functions'; import { getCurrentConference } from '../base/conference/functions';
import { openDialog } from '../base/dialog/actions';
import { JitsiConferenceEvents } from '../base/lib-jitsi-meet';
import { PARTICIPANT_JOINED, PARTICIPANT_LEFT, PARTICIPANT_UPDATED } from '../base/participants/actionTypes'; import { PARTICIPANT_JOINED, PARTICIPANT_LEFT, PARTICIPANT_UPDATED } from '../base/participants/actionTypes';
import { participantUpdated } from '../base/participants/actions'; import { participantUpdated } from '../base/participants/actions';
import { import {
@ -17,13 +19,15 @@ import MiddlewareRegistry from '../base/redux/MiddlewareRegistry';
import StateListenerRegistry from '../base/redux/StateListenerRegistry'; import StateListenerRegistry from '../base/redux/StateListenerRegistry';
import { playSound, registerSound, unregisterSound } from '../base/sounds/actions'; import { playSound, registerSound, unregisterSound } from '../base/sounds/actions';
import { SET_MEDIA_ENCRYPTION_KEY, TOGGLE_E2EE } from './actionTypes'; import { PARTICIPANT_VERIFIED, SET_MEDIA_ENCRYPTION_KEY, START_VERIFICATION, TOGGLE_E2EE } from './actionTypes';
import { setE2EEMaxMode, setEveryoneEnabledE2EE, setEveryoneSupportE2EE, toggleE2EE } from './actions'; import { setE2EEMaxMode, setEveryoneEnabledE2EE, setEveryoneSupportE2EE, toggleE2EE } from './actions';
import ParticipantVerificationDialog from './components/ParticipantVerificationDialog';
import { E2EE_OFF_SOUND_ID, E2EE_ON_SOUND_ID, MAX_MODE } from './constants'; import { E2EE_OFF_SOUND_ID, E2EE_ON_SOUND_ID, MAX_MODE } from './constants';
import { isMaxModeReached, isMaxModeThresholdReached } from './functions'; import { isMaxModeReached, isMaxModeThresholdReached } from './functions';
import logger from './logger'; import logger from './logger';
import { E2EE_OFF_SOUND_FILE, E2EE_ON_SOUND_FILE } from './sounds'; import { E2EE_OFF_SOUND_FILE, E2EE_ON_SOUND_FILE } from './sounds';
/** /**
* Middleware that captures actions related to E2EE. * Middleware that captures actions related to E2EE.
* *
@ -239,6 +243,18 @@ MiddlewareRegistry.register(({ dispatch, getState }) => next => action => {
break; break;
} }
case PARTICIPANT_VERIFIED: {
const { isVerified, pId } = action;
conference?.markParticipantVerified(pId, isVerified);
break;
}
case START_VERIFICATION: {
conference?.startVerification(action.pId);
break;
}
} }
return next(action); return next(action);
@ -254,6 +270,29 @@ StateListenerRegistry.register(
if (previousConference) { if (previousConference) {
dispatch(toggleE2EE(false)); dispatch(toggleE2EE(false));
} }
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
}));
});
}); });
/** /**

View File

@ -20,6 +20,10 @@ export interface IE2EEState {
maxMode: string; maxMode: string;
} }
export interface ISas {
emoji: Array<string>;
}
/** /**
* Reduces the Redux actions of the feature features/e2ee. * Reduces the Redux actions of the feature features/e2ee.
*/ */

View File

@ -18,6 +18,7 @@ import { isParticipantAudioMuted } from '../../../base/tracks/functions';
import ContextMenu from '../../../base/ui/components/web/ContextMenu'; import ContextMenu from '../../../base/ui/components/web/ContextMenu';
import ContextMenuItemGroup from '../../../base/ui/components/web/ContextMenuItemGroup'; import ContextMenuItemGroup from '../../../base/ui/components/web/ContextMenuItemGroup';
import { getBreakoutRooms, getCurrentRoomId, isInBreakoutRoom } from '../../../breakout-rooms/functions'; import { getBreakoutRooms, getCurrentRoomId, isInBreakoutRoom } from '../../../breakout-rooms/functions';
import { displayVerification } from '../../../e2ee/functions';
import { setVolume } from '../../../filmstrip/actions.web'; import { setVolume } from '../../../filmstrip/actions.web';
import { isStageFilmstripAvailable } from '../../../filmstrip/functions.web'; import { isStageFilmstripAvailable } from '../../../filmstrip/functions.web';
import { isForceMuted } from '../../../participants-pane/functions'; import { isForceMuted } from '../../../participants-pane/functions';
@ -29,6 +30,7 @@ import { showOverflowDrawer } from '../../../toolbox/functions.web';
import { REMOTE_CONTROL_MENU_STATES } from './RemoteControlButton'; import { REMOTE_CONTROL_MENU_STATES } from './RemoteControlButton';
// @ts-ignore // @ts-ignore
import SendToRoomButton from './SendToRoomButton'; import SendToRoomButton from './SendToRoomButton';
import VerifyParticipantButton from './VerifyParticipantButton';
import { import {
AskToUnmuteButton, AskToUnmuteButton,
@ -150,6 +152,7 @@ const ParticipantContextMenu = ({
const isBreakoutRoom = useSelector(isInBreakoutRoom); const isBreakoutRoom = useSelector(isInBreakoutRoom);
const isModerationSupported = useSelector((state: IReduxState) => isAvModerationSupported()(state)); const isModerationSupported = useSelector((state: IReduxState) => isAvModerationSupported()(state));
const stageFilmstrip = useSelector(isStageFilmstripAvailable); const stageFilmstrip = useSelector(isStageFilmstripAvailable);
const shouldDisplayVerification = useSelector((state: IReduxState) => displayVerification(state, participant?.id));
const _currentRoomId = useSelector(getCurrentRoomId); const _currentRoomId = useSelector(getCurrentRoomId);
const _rooms: Array<{ id: string; }> = Object.values(useSelector(getBreakoutRooms)); const _rooms: Array<{ id: string; }> = Object.values(useSelector(getBreakoutRooms));
@ -223,6 +226,15 @@ const ParticipantContextMenu = ({
participantID = { _getCurrentParticipantId() } /> participantID = { _getCurrentParticipantId() } />
); );
} }
if (shouldDisplayVerification) {
buttons2.push(
<VerifyParticipantButton
key = 'verify'
participantID = { _getCurrentParticipantId() } />
);
}
} }
if (stageFilmstrip) { if (stageFilmstrip) {

View File

@ -0,0 +1,115 @@
/* eslint-disable lines-around-comment */
import { withStyles } from '@mui/styles';
import React, { Component } from 'react';
import { WithTranslation } from 'react-i18next';
import { connect } from 'react-redux';
import { IReduxState } from '../../../app/types';
import { translate } from '../../../base/i18n/functions';
import { IconCheck } from '../../../base/icons/svg';
import ContextMenuItem from '../../../base/ui/components/web/ContextMenuItem';
import { startVerification } from '../../../e2ee/actions';
/**
* The type of the React {@code Component} props of
* {@link VerifyParticipantButton}.
*/
interface IProps extends WithTranslation {
/**
* The redux {@code dispatch} function.
*/
dispatch: Function;
/**
* The ID of the participant that this button is supposed to verified.
*/
participantID: string;
}
const styles = () => {
return {
triggerButton: {
padding: '3px !important',
borderRadius: '4px'
},
contextMenu: {
position: 'relative' as const,
marginTop: 0,
right: 'auto',
marginRight: '4px',
marginBottom: '4px'
}
};
};
/**
* React {@code Component} for displaying an icon associated with opening the
* the {@code VideoMenu}.
*
* @augments {Component}
*/
class VerifyParticipantButton extends Component<IProps> {
/**
* Instantiates a new {@code Component}.
*
* @inheritdoc
*/
constructor(props: IProps) {
super(props);
this._handleClick = this._handleClick.bind(this);
}
/**
* Implements React's {@link Component#render()}.
*
* @inheritdoc
* @returns {ReactElement}
*/
render() {
const { participantID, t } = this.props;
return (
<ContextMenuItem
accessibilityLabel = { t('videothumbnail.verify') }
className = 'verifylink'
icon = { IconCheck }
id = { `verifylink_${participantID}` }
// eslint-disable-next-line react/jsx-handler-names
onClick = { this._handleClick }
text = { t('videothumbnail.verify') } />
);
}
/**
* Handles clicking / pressing the button, and starts the participant verification process.
*
* @private
* @returns {void}
*/
_handleClick() {
const { dispatch, participantID } = this.props;
dispatch(startVerification(participantID));
}
}
/**
* Maps (parts of) the Redux state to the associated {@code RemoteVideoMenuTriggerButton}'s props.
*
* @param {Object} state - The Redux state.
* @param {Object} ownProps - The own props of the component.
* @private
* @returns {IProps}
*/
function _mapStateToProps(state: IReduxState, ownProps: Partial<IProps>) {
const { participantID } = ownProps;
return {
_participantID: participantID
};
}
export default translate(connect(_mapStateToProps)(withStyles(styles)(VerifyParticipantButton)));