feat(E2EE) add initial SAS verification UI
This commit is contained in:
parent
1139311809
commit
4c9bfe3d4d
|
@ -147,6 +147,7 @@
|
|||
"bridgeCount": "Server count: ",
|
||||
"codecs": "Codecs (A/V): ",
|
||||
"connectedTo": "Connected to:",
|
||||
"e2eeVerified": "E2EE verified:",
|
||||
"framerate": "Frame rate:",
|
||||
"less": "Show less",
|
||||
"localaddress": "Local address:",
|
||||
|
@ -408,6 +409,10 @@
|
|||
"user": "User",
|
||||
"userIdentifier": "User identifier",
|
||||
"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",
|
||||
"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.",
|
||||
|
@ -1297,6 +1302,7 @@
|
|||
"show": "Show on stage",
|
||||
"showSelfView": "Show self view",
|
||||
"unpinFromStage": "Unpin",
|
||||
"verify": "Verify participant",
|
||||
"videoMuted": "Camera disabled",
|
||||
"videomute": "Participant has stopped the camera"
|
||||
},
|
||||
|
|
|
@ -59,6 +59,7 @@ export interface IJitsiConference {
|
|||
grantOwner: Function;
|
||||
isAVModerationSupported: Function;
|
||||
isCallstatsEnabled: Function;
|
||||
isE2EEEnabled: Function;
|
||||
isEndConferenceSupported: Function;
|
||||
isLobbySupported: Function;
|
||||
isSIPCallingSupported: Function;
|
||||
|
@ -89,6 +90,7 @@ export interface IJitsiConference {
|
|||
setReceiverConstraints: Function;
|
||||
setSenderVideoConstraint: Function;
|
||||
setSubject: Function;
|
||||
startVerification: Function;
|
||||
}
|
||||
|
||||
export interface IConferenceState {
|
||||
|
|
|
@ -4,6 +4,7 @@ import WaitForOwnerDialog from '../../authentication/components/web/WaitForOwner
|
|||
import ChatPrivacyDialog from '../../chat/components/web/ChatPrivacyDialog';
|
||||
import DesktopPicker from '../../desktop-picker/components/DesktopPicker';
|
||||
import DisplayNamePrompt from '../../display-name/components/web/DisplayNamePrompt';
|
||||
import ParticipantVerificationDialog from '../../e2ee/components/ParticipantVerificationDialog';
|
||||
import EmbedMeetingDialog from '../../embed-meeting/components/EmbedMeetingDialog';
|
||||
// @ts-ignore
|
||||
import FeedbackDialog from '../../feedback/components/FeedbackDialog.web';
|
||||
|
@ -49,7 +50,7 @@ const NEW_DIALOG_LIST = [ KeyboardShortcutsDialog, ChatPrivacyDialog, DisplayNam
|
|||
SharedVideoDialog, SpeakerStats, LanguageSelectorDialog, MuteEveryoneDialog, MuteEveryonesVideoDialog,
|
||||
GrantModeratorDialog, KickRemoteParticipantDialog, MuteRemoteParticipantsVideoDialog, VideoQualityDialog,
|
||||
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.
|
||||
const isNewDialog = (component: any) => NEW_DIALOG_LIST.some(comp => comp === component);
|
||||
|
|
|
@ -13,6 +13,8 @@ export interface IParticipant {
|
|||
dominantSpeaker?: boolean;
|
||||
e2eeEnabled?: boolean;
|
||||
e2eeSupported?: boolean;
|
||||
e2eeVerificationAvailable?: boolean;
|
||||
e2eeVerified?: boolean;
|
||||
email?: string;
|
||||
fakeParticipant?: FakeParticipant;
|
||||
features?: {
|
||||
|
|
|
@ -189,6 +189,7 @@ class ConnectionIndicatorContent extends AbstractConnectionIndicator<Props, Stat
|
|||
codec = { codec }
|
||||
connectionSummary = { this._getConnectionStatusTip() }
|
||||
disableShowMoreStats = { this.props._disableShowMoreStats }
|
||||
e2eeVerified = { this.props._isE2EEVerified }
|
||||
enableSaveLogs = { this.props._enableSaveLogs }
|
||||
framerate = { framerate }
|
||||
isLocalVideo = { this.props._isLocalVideo }
|
||||
|
@ -328,6 +329,7 @@ export function _mapStateToProps(state: Object, ownProps: Props) {
|
|||
_disableShowMoreStats: state['features/base/config'].disableShowMoreStats,
|
||||
_isConnectionStatusInactive,
|
||||
_isConnectionStatusInterrupted,
|
||||
_isE2EEVerified: participant?.e2eeVerified,
|
||||
_isVirtualScreenshareParticipant: isScreenShareParticipant(participant),
|
||||
_isLocalVideo: participant?.local,
|
||||
_region: participant?.region,
|
||||
|
|
|
@ -73,6 +73,11 @@ interface IProps extends WithTranslation {
|
|||
*/
|
||||
disableShowMoreStats: boolean;
|
||||
|
||||
/**
|
||||
* Whether or not the participant was verified.
|
||||
*/
|
||||
e2eeVerified: boolean;
|
||||
|
||||
/**
|
||||
* 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
|
||||
|
@ -726,6 +756,7 @@ class ConnectionStatsTable extends Component<IProps> {
|
|||
{ this._renderResolution() }
|
||||
{ this._renderFrameRate() }
|
||||
{ this._renderCodecs() }
|
||||
{ this._renderE2EEVerified() }
|
||||
</tbody>
|
||||
</table>
|
||||
);
|
||||
|
|
|
@ -43,3 +43,7 @@ export const SET_MAX_MODE = 'SET_MAX_MODE';
|
|||
* }
|
||||
*/
|
||||
export const SET_MEDIA_ENCRYPTION_KEY = 'SET_MEDIA_ENCRYPTION_KEY';
|
||||
|
||||
export const START_VERIFICATION = 'START_VERIFICATION';
|
||||
|
||||
export const PARTICIPANT_VERIFIED = 'PARTICIPANT_VERIFIED';
|
||||
|
|
|
@ -1,8 +1,10 @@
|
|||
import {
|
||||
PARTICIPANT_VERIFIED,
|
||||
SET_EVERYONE_ENABLED_E2EE,
|
||||
SET_EVERYONE_SUPPORT_E2EE,
|
||||
SET_MAX_MODE,
|
||||
SET_MEDIA_ENCRYPTION_KEY,
|
||||
START_VERIFICATION,
|
||||
TOGGLE_E2EE } from './actionTypes';
|
||||
|
||||
/**
|
||||
|
@ -80,3 +82,38 @@ export function setMediaEncryptionKey(keyInfo: Object) {
|
|||
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
|
||||
};
|
||||
}
|
||||
|
|
|
@ -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)));
|
|
@ -1,7 +1,9 @@
|
|||
import { IReduxState } from '../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 { 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;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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);
|
||||
}
|
||||
|
|
|
@ -4,6 +4,8 @@ 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, PARTICIPANT_UPDATED } from '../base/participants/actionTypes';
|
||||
import { participantUpdated } from '../base/participants/actions';
|
||||
import {
|
||||
|
@ -17,13 +19,15 @@ import MiddlewareRegistry from '../base/redux/MiddlewareRegistry';
|
|||
import StateListenerRegistry from '../base/redux/StateListenerRegistry';
|
||||
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 ParticipantVerificationDialog from './components/ParticipantVerificationDialog';
|
||||
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';
|
||||
|
||||
|
||||
/**
|
||||
* Middleware that captures actions related to E2EE.
|
||||
*
|
||||
|
@ -239,6 +243,18 @@ MiddlewareRegistry.register(({ dispatch, getState }) => next => action => {
|
|||
|
||||
break;
|
||||
}
|
||||
|
||||
case PARTICIPANT_VERIFIED: {
|
||||
const { isVerified, pId } = action;
|
||||
|
||||
conference?.markParticipantVerified(pId, isVerified);
|
||||
break;
|
||||
}
|
||||
|
||||
case START_VERIFICATION: {
|
||||
conference?.startVerification(action.pId);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return next(action);
|
||||
|
@ -254,6 +270,29 @@ StateListenerRegistry.register(
|
|||
if (previousConference) {
|
||||
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
|
||||
}));
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
|
|
|
@ -20,6 +20,10 @@ export interface IE2EEState {
|
|||
maxMode: string;
|
||||
}
|
||||
|
||||
export interface ISas {
|
||||
emoji: Array<string>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Reduces the Redux actions of the feature features/e2ee.
|
||||
*/
|
||||
|
|
|
@ -18,6 +18,7 @@ import { isParticipantAudioMuted } from '../../../base/tracks/functions';
|
|||
import ContextMenu from '../../../base/ui/components/web/ContextMenu';
|
||||
import ContextMenuItemGroup from '../../../base/ui/components/web/ContextMenuItemGroup';
|
||||
import { getBreakoutRooms, getCurrentRoomId, isInBreakoutRoom } from '../../../breakout-rooms/functions';
|
||||
import { displayVerification } from '../../../e2ee/functions';
|
||||
import { setVolume } from '../../../filmstrip/actions.web';
|
||||
import { isStageFilmstripAvailable } from '../../../filmstrip/functions.web';
|
||||
import { isForceMuted } from '../../../participants-pane/functions';
|
||||
|
@ -29,6 +30,7 @@ import { showOverflowDrawer } from '../../../toolbox/functions.web';
|
|||
import { REMOTE_CONTROL_MENU_STATES } from './RemoteControlButton';
|
||||
// @ts-ignore
|
||||
import SendToRoomButton from './SendToRoomButton';
|
||||
import VerifyParticipantButton from './VerifyParticipantButton';
|
||||
|
||||
import {
|
||||
AskToUnmuteButton,
|
||||
|
@ -150,6 +152,7 @@ const ParticipantContextMenu = ({
|
|||
const isBreakoutRoom = useSelector(isInBreakoutRoom);
|
||||
const isModerationSupported = useSelector((state: IReduxState) => isAvModerationSupported()(state));
|
||||
const stageFilmstrip = useSelector(isStageFilmstripAvailable);
|
||||
const shouldDisplayVerification = useSelector((state: IReduxState) => displayVerification(state, participant?.id));
|
||||
|
||||
const _currentRoomId = useSelector(getCurrentRoomId);
|
||||
const _rooms: Array<{ id: string; }> = Object.values(useSelector(getBreakoutRooms));
|
||||
|
@ -223,6 +226,15 @@ const ParticipantContextMenu = ({
|
|||
participantID = { _getCurrentParticipantId() } />
|
||||
);
|
||||
}
|
||||
|
||||
if (shouldDisplayVerification) {
|
||||
buttons2.push(
|
||||
<VerifyParticipantButton
|
||||
key = 'verify'
|
||||
participantID = { _getCurrentParticipantId() } />
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
if (stageFilmstrip) {
|
||||
|
|
|
@ -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)));
|
Loading…
Reference in New Issue