e2ee: add UI elements
* Add dialog to set the E2EE key * Use the Redux action / middleware to update the key even when set through the hash parameter * Cleanup URL after processing the key so it's not recorded in browser history
This commit is contained in:
parent
0077ee29c5
commit
cb6fbb0f03
|
@ -113,6 +113,7 @@ import {
|
|||
import { getJitsiMeetGlobalNS } from './react/features/base/util';
|
||||
import { showDesktopPicker } from './react/features/desktop-picker';
|
||||
import { appendSuffix } from './react/features/display-name';
|
||||
import { setE2EEKey } from './react/features/e2ee';
|
||||
import {
|
||||
maybeOpenFeedbackDialog,
|
||||
submitFeedback
|
||||
|
@ -470,11 +471,6 @@ export default {
|
|||
*/
|
||||
localVideo: null,
|
||||
|
||||
/**
|
||||
* The key used for End-To-End Encryption.
|
||||
*/
|
||||
e2eeKey: undefined,
|
||||
|
||||
/**
|
||||
* Creates local media tracks and connects to a room. Will show error
|
||||
* dialogs in case accessing the local microphone and/or camera failed. Will
|
||||
|
@ -1202,11 +1198,14 @@ export default {
|
|||
items[key] = param[1];
|
||||
}
|
||||
|
||||
this.e2eeKey = items.e2eekey;
|
||||
if (typeof items.e2eekey !== undefined) {
|
||||
APP.store.dispatch(setE2EEKey(items.e2eekey));
|
||||
|
||||
logger.debug(`New E2EE key: ${this.e2eeKey}`);
|
||||
// Clean URL in browser history.
|
||||
const cleanUrl = window.location.href.split('#')[0];
|
||||
|
||||
this._room.setE2EEKey(this.e2eeKey);
|
||||
history.replaceState(history.state, document.title, cleanUrl);
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
|
|
|
@ -51,7 +51,8 @@ var interfaceConfig = {
|
|||
'fodeviceselection', 'hangup', 'profile', 'info', 'chat', 'recording',
|
||||
'livestreaming', 'etherpad', 'sharedvideo', 'settings', 'raisehand',
|
||||
'videoquality', 'filmstrip', 'invite', 'feedback', 'stats', 'shortcuts',
|
||||
'tileview', 'videobackgroundblur', 'download', 'help', 'mute-everyone'
|
||||
'tileview', 'videobackgroundblur', 'download', 'help', 'mute-everyone',
|
||||
'e2ee'
|
||||
],
|
||||
|
||||
SETTINGS_SECTIONS: [ 'devices', 'language', 'moderator', 'profile', 'calendar' ],
|
||||
|
|
|
@ -175,6 +175,9 @@
|
|||
"dismiss": "Dismiss",
|
||||
"displayNameRequired": "Hi! What’s your name?",
|
||||
"done": "Done",
|
||||
"e2eeDescription": "<p>End-to-End Encryption is currently <strong>EXPERIMENTAL</strong>. Please see <a href='https://jitsi.org/blog/e2ee/' target='_blank'>this post</a> for details.</p><br/><p>Please keep in mind that turning on end-to-end encryption will effectively disable server-side provided services such as: recording, live streaming and phone participation. Also keep in mind that the meeting will only work for people joining from browsers with support for insertable streams.</p>",
|
||||
"e2eeLabel": "Key",
|
||||
"e2eeTitle": "End-to-End Encryption",
|
||||
"enterDisplayName": "Please enter your name here",
|
||||
"error": "Error",
|
||||
"externalInstallationMsg": "You need to install our desktop sharing extension.",
|
||||
|
@ -595,6 +598,7 @@
|
|||
"chat": "Toggle chat window",
|
||||
"document": "Toggle shared document",
|
||||
"download": "Download our apps",
|
||||
"e2ee": "End-to-End Encryption",
|
||||
"feedback": "Leave feedback",
|
||||
"fullScreen": "Toggle full screen",
|
||||
"hangup": "Leave the call",
|
||||
|
@ -638,6 +642,7 @@
|
|||
"documentClose": "Close shared document",
|
||||
"documentOpen": "Open shared document",
|
||||
"download": "Download our apps",
|
||||
"e2ee": "End-to-End Encryption",
|
||||
"enterFullScreen": "View full screen",
|
||||
"enterTileView": "Enter tile view",
|
||||
"exitFullScreen": "Exit full screen",
|
||||
|
|
|
@ -257,6 +257,20 @@ export function createDeviceChangedEvent(mediaType, deviceType) {
|
|||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates an event indicating that an action related to E2EE occurred.
|
||||
*
|
||||
* @param {string} action - The action which occurred.
|
||||
* @returns {Object} The event in a format suitable for sending via
|
||||
* sendAnalytics.
|
||||
*/
|
||||
export function createE2EEEvent(action) {
|
||||
return {
|
||||
action,
|
||||
actionSubject: 'e2ee'
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates an event which specifies that the feedback dialog has been opened.
|
||||
*
|
||||
|
|
|
@ -31,6 +31,7 @@ import { isRoomValid } from './functions';
|
|||
|
||||
const DEFAULT_STATE = {
|
||||
conference: undefined,
|
||||
e2eeSupported: undefined,
|
||||
joining: undefined,
|
||||
leaving: undefined,
|
||||
locked: undefined,
|
||||
|
@ -175,6 +176,7 @@ function _conferenceFailed(state, { conference, error }) {
|
|||
return assign(state, {
|
||||
authRequired,
|
||||
conference: undefined,
|
||||
e2eeSupported: undefined,
|
||||
error,
|
||||
joining: undefined,
|
||||
leaving: undefined,
|
||||
|
@ -226,6 +228,9 @@ function _conferenceJoined(state, { conference }) {
|
|||
* @type {JitsiConference}
|
||||
*/
|
||||
conference,
|
||||
|
||||
e2eeSupported: conference.isE2EESupported(),
|
||||
|
||||
joining: undefined,
|
||||
leaving: undefined,
|
||||
|
||||
|
|
|
@ -0,0 +1,8 @@
|
|||
/**
|
||||
* The type of the action which signals the E2EE key has changed.
|
||||
*
|
||||
* {
|
||||
* type: SET_E2EE_KEY
|
||||
* }
|
||||
*/
|
||||
export const SET_E2EE_KEY = 'SET_E2EE_KEY';
|
|
@ -0,0 +1,16 @@
|
|||
// @flow
|
||||
|
||||
import { SET_E2EE_KEY } from './actionTypes';
|
||||
|
||||
/**
|
||||
* Dispatches an action to set the E2EE key.
|
||||
*
|
||||
* @param {string|undefined} key - The new key to be used for E2EE.
|
||||
* @returns {Object}
|
||||
*/
|
||||
export function setE2EEKey(key: ?string) {
|
||||
return {
|
||||
type: SET_E2EE_KEY,
|
||||
key
|
||||
};
|
||||
}
|
|
@ -0,0 +1,76 @@
|
|||
// @flow
|
||||
|
||||
import React from 'react';
|
||||
|
||||
import { createE2EEEvent, sendAnalytics } from '../../analytics';
|
||||
import { openDialog } from '../../base/dialog';
|
||||
import { translate } from '../../base/i18n';
|
||||
import { IconRoomUnlock } from '../../base/icons';
|
||||
import { connect } from '../../base/redux';
|
||||
import { AbstractButton, BetaTag } from '../../base/toolbox';
|
||||
import type { AbstractButtonProps } from '../../base/toolbox';
|
||||
|
||||
import E2EEDialog from './E2EEDialog';
|
||||
|
||||
|
||||
type Props = AbstractButtonProps & {
|
||||
|
||||
/**
|
||||
* The redux {@code dispatch} function.
|
||||
*/
|
||||
dispatch: Function
|
||||
|
||||
};
|
||||
|
||||
/**
|
||||
* Button that open a dialog to set the E2EE key.
|
||||
*/
|
||||
class E2EEButton extends AbstractButton<Props, *> {
|
||||
accessibilityLabel = 'toolbar.accessibilityLabel.e2ee';
|
||||
icon = IconRoomUnlock;
|
||||
label = 'toolbar.e2ee';
|
||||
tooltip = 'toolbar.e2ee';
|
||||
|
||||
/**
|
||||
* Helper function to be implemented by subclasses, which returns
|
||||
* a React Element to display (a beta tag) at the end of the button.
|
||||
*
|
||||
* @override
|
||||
* @protected
|
||||
* @returns {ReactElement}
|
||||
*/
|
||||
_getElementAfter() {
|
||||
return <BetaTag />;
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles clicking / pressing the button, and opens the E2EE dialog.
|
||||
*
|
||||
* @protected
|
||||
* @returns {void}
|
||||
*/
|
||||
_handleClick() {
|
||||
sendAnalytics(createE2EEEvent('dialog.open'));
|
||||
this.props.dispatch(openDialog(E2EEDialog));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Maps (parts of) the redux state to the associated props for this component.
|
||||
*
|
||||
* @param {Object} state - The Redux state.
|
||||
* @param {Props} ownProps - The own props of the Component.
|
||||
* @private
|
||||
* @returns {Props}
|
||||
*/
|
||||
export function mapStateToProps(state: Object, ownProps: Props) {
|
||||
const { e2eeSupported } = state['features/base/conference'];
|
||||
const { visible = Boolean(e2eeSupported) } = ownProps;
|
||||
|
||||
return {
|
||||
visible
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
export default translate(connect(mapStateToProps)(E2EEButton));
|
|
@ -0,0 +1,142 @@
|
|||
/* @flow */
|
||||
|
||||
import React, { Component } from 'react';
|
||||
import type { Dispatch } from 'redux';
|
||||
import { FieldTextStateless as TextField } from '@atlaskit/field-text';
|
||||
|
||||
import { createE2EEEvent, sendAnalytics } from '../../analytics';
|
||||
import { Dialog } from '../../base/dialog';
|
||||
import { translate, translateToHTML } from '../../base/i18n';
|
||||
import { connect } from '../../base/redux';
|
||||
|
||||
import { setE2EEKey } from '../actions';
|
||||
|
||||
|
||||
type Props = {
|
||||
|
||||
/**
|
||||
* The current E2EE key.
|
||||
*/
|
||||
_key: string,
|
||||
|
||||
/**
|
||||
* The redux {@code dispatch} function.
|
||||
*/
|
||||
dispatch: Dispatch<any>,
|
||||
|
||||
/**
|
||||
* Invoked to obtain translated strings.
|
||||
*/
|
||||
t: Function
|
||||
};
|
||||
|
||||
type State = {
|
||||
|
||||
/**
|
||||
* The current E2EE key.
|
||||
*/
|
||||
key: string
|
||||
};
|
||||
|
||||
/**
|
||||
* Implements a React {@code Component} for displaying a dialog with a field
|
||||
* for setting the E2EE key.
|
||||
*
|
||||
* @extends Component
|
||||
*/
|
||||
class E2EEDialog extends Component<Props, State> {
|
||||
/**
|
||||
* Initializes a new {@code E2EEDialog } instance.
|
||||
*
|
||||
* @param {Object} props - The read-only properties with which the new
|
||||
* instance is to be initialized.
|
||||
*/
|
||||
constructor(props: Props) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
key: this.props._key
|
||||
};
|
||||
|
||||
// Bind event handlers so they are only bound once for every instance.
|
||||
this._onKeyChange = this._onKeyChange.bind(this);
|
||||
this._onSubmit = this._onSubmit.bind(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements React's {@link Component#render()}.
|
||||
*
|
||||
* @inheritdoc
|
||||
* @returns {ReactElement}
|
||||
*/
|
||||
render() {
|
||||
const { t } = this.props;
|
||||
|
||||
return (
|
||||
<Dialog
|
||||
isModal = { false }
|
||||
onSubmit = { this._onSubmit }
|
||||
titleKey = 'dialog.e2eeTitle'
|
||||
width = 'small'>
|
||||
<div className = 'e2ee-destription'>
|
||||
{ translateToHTML(t, 'dialog.e2eeDescription') }
|
||||
</div>
|
||||
<TextField
|
||||
autoFocus = { true }
|
||||
compact = { true }
|
||||
label = { t('dialog.e2eeLabel') }
|
||||
name = 'e2eeKey'
|
||||
onChange = { this._onKeyChange }
|
||||
shouldFitContainer = { true }
|
||||
type = 'password'
|
||||
value = { this.state.key } />
|
||||
</Dialog>);
|
||||
}
|
||||
|
||||
_onKeyChange: (Object) => void;
|
||||
|
||||
/**
|
||||
* Updates the entered key.
|
||||
*
|
||||
* @param {Object} event - The DOM event triggered from the entered value having changed.
|
||||
* @private
|
||||
* @returns {void}
|
||||
*/
|
||||
_onKeyChange(event) {
|
||||
this.setState({ key: event.target.value.trim() });
|
||||
}
|
||||
|
||||
_onSubmit: () => boolean;
|
||||
|
||||
/**
|
||||
* Dispatches an action to update the E2EE key.
|
||||
*
|
||||
* @private
|
||||
* @returns {boolean}
|
||||
*/
|
||||
_onSubmit() {
|
||||
const { key } = this.state;
|
||||
|
||||
sendAnalytics(createE2EEEvent(`key.${key ? 'set' : 'unset'}`));
|
||||
this.props.dispatch(setE2EEKey(key));
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Maps (parts of) the Redux state to the associated props for this component.
|
||||
*
|
||||
* @param {Object} state - The Redux state.
|
||||
* @private
|
||||
* @returns {Props}
|
||||
*/
|
||||
function mapStateToProps(state) {
|
||||
const { e2eeKey } = state['features/e2ee'];
|
||||
|
||||
return {
|
||||
_key: e2eeKey || ''
|
||||
};
|
||||
}
|
||||
|
||||
export default translate(connect(mapStateToProps)(E2EEDialog));
|
|
@ -0,0 +1,2 @@
|
|||
export { default as E2EEButton } from './E2EEButton';
|
||||
export { default as E2EEDialog } from './E2EEDialog';
|
|
@ -0,0 +1,6 @@
|
|||
export * from './actions';
|
||||
export * from './actionTypes';
|
||||
export * from './components';
|
||||
|
||||
import './middleware';
|
||||
import './reducer';
|
|
@ -0,0 +1,5 @@
|
|||
// @flow
|
||||
|
||||
import { getLogger } from '../base/logging/functions';
|
||||
|
||||
export default getLogger('features/e2ee');
|
|
@ -0,0 +1,43 @@
|
|||
// @flow
|
||||
|
||||
import { getCurrentConference } from '../base/conference';
|
||||
import { MiddlewareRegistry, StateListenerRegistry } from '../base/redux';
|
||||
|
||||
import { SET_E2EE_KEY } from './actionTypes';
|
||||
import { setE2EEKey } from './actions';
|
||||
import logger from './logger';
|
||||
|
||||
/**
|
||||
* Middleware that captures actions related to E2EE.
|
||||
*
|
||||
* @param {Store} store - The redux store.
|
||||
* @returns {Function}
|
||||
*/
|
||||
MiddlewareRegistry.register(({ getState }) => next => action => {
|
||||
switch (action.type) {
|
||||
case SET_E2EE_KEY: {
|
||||
const conference = getCurrentConference(getState);
|
||||
|
||||
if (conference) {
|
||||
logger.debug(`New E2EE key: ${action.key}`);
|
||||
conference.setE2EEKey(action.key);
|
||||
}
|
||||
|
||||
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(setE2EEKey(undefined));
|
||||
}
|
||||
});
|
|
@ -0,0 +1,29 @@
|
|||
// @flow
|
||||
|
||||
import { ReducerRegistry } from '../base/redux';
|
||||
|
||||
import { SET_E2EE_KEY } from './actionTypes';
|
||||
|
||||
const DEFAULT_STATE = {
|
||||
|
||||
/**
|
||||
* E2EE key.
|
||||
*/
|
||||
e2eeKey: undefined
|
||||
};
|
||||
|
||||
/**
|
||||
* Reduces the Redux actions of the feature features/e2ee.
|
||||
*/
|
||||
ReducerRegistry.register('features/e2ee', (state = DEFAULT_STATE, action) => {
|
||||
switch (action.type) {
|
||||
case SET_E2EE_KEY:
|
||||
return {
|
||||
...state,
|
||||
e2eeKey: action.key
|
||||
};
|
||||
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
});
|
|
@ -33,6 +33,7 @@ import { OverflowMenuItem } from '../../../base/toolbox';
|
|||
import { getLocalVideoTrack, toggleScreensharing } from '../../../base/tracks';
|
||||
import { VideoBlurButton } from '../../../blur';
|
||||
import { ChatCounter, toggleChat } from '../../../chat';
|
||||
import { E2EEButton } from '../../../e2ee';
|
||||
import { SharedDocumentButton } from '../../../etherpad';
|
||||
import { openFeedbackDialog } from '../../../feedback';
|
||||
import {
|
||||
|
@ -1012,6 +1013,10 @@ class Toolbox extends Component<Props, State> {
|
|||
key = 'stats'
|
||||
onClick = { this._onToolbarOpenSpeakerStats }
|
||||
text = { t('toolbar.speakerStats') } />,
|
||||
this._shouldShowButton('e2ee')
|
||||
&& <E2EEButton
|
||||
key = 'e2ee'
|
||||
showLabel = { true } />,
|
||||
this._shouldShowButton('feedback')
|
||||
&& _feedbackConfigured
|
||||
&& <OverflowMenuItem
|
||||
|
|
Loading…
Reference in New Issue