feat(rn,security) add security dialog

This commit is contained in:
Calinteodor 2021-04-09 15:30:25 +03:00 committed by GitHub
parent 524af5ca67
commit bf3726cb93
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
29 changed files with 726 additions and 415 deletions

View File

@ -226,7 +226,7 @@
"liveStreamingDisabledBecauseOfActiveRecordingTooltip": "Not possible while recording is active", "liveStreamingDisabledBecauseOfActiveRecordingTooltip": "Not possible while recording is active",
"liveStreamingDisabledTooltip": "Start live stream disabled.", "liveStreamingDisabledTooltip": "Start live stream disabled.",
"lockMessage": "Failed to lock the conference.", "lockMessage": "Failed to lock the conference.",
"lockRoom": "Add meeting $t(lockRoomPasswordUppercase)", "lockRoom": "Add meeting $t(lockRoomPassword)",
"lockTitle": "Lock failed", "lockTitle": "Lock failed",
"logoutQuestion": "Are you sure you want to logout and stop the conference?", "logoutQuestion": "Are you sure you want to logout and stop the conference?",
"login": "Login", "login": "Login",
@ -525,7 +525,7 @@
"oldElectronClientDescription2": "latest build", "oldElectronClientDescription2": "latest build",
"oldElectronClientDescription3": " now!" "oldElectronClientDescription3": " now!"
}, },
"passwordSetRemotely": "set by another participant", "passwordSetRemotely": "Set by another participant",
"passwordDigitsOnly": "Up to {{number}} digits", "passwordDigitsOnly": "Up to {{number}} digits",
"poweredby": "powered by", "poweredby": "powered by",
"prejoin": { "prejoin": {

View File

@ -38,7 +38,7 @@ import {
resizeLargeVideo, resizeLargeVideo,
selectParticipantInLargeVideo selectParticipantInLargeVideo
} from '../../react/features/large-video/actions'; } from '../../react/features/large-video/actions';
import { toggleLobbyMode } from '../../react/features/lobby/actions.web'; import { toggleLobbyMode } from '../../react/features/lobby/actions';
import { RECORDING_TYPES } from '../../react/features/recording/constants'; import { RECORDING_TYPES } from '../../react/features/recording/constants';
import { getActiveSession } from '../../react/features/recording/functions'; import { getActiveSession } from '../../react/features/recording/functions';
import { toggleTileView, setTileView } from '../../react/features/video-layout'; import { toggleTileView, setTileView } from '../../react/features/video-layout';

View File

@ -116,6 +116,24 @@ const brandedDialogText = {
textAlign: 'center' textAlign: 'center'
}; };
const brandedDialogLabelStyle = {
color: schemeColor('text'),
flexShrink: 1,
fontSize: MD_FONT_SIZE,
opacity: 0.90
};
const brandedDialogItemContainerStyle = {
alignItems: 'center',
flexDirection: 'row',
height: MD_ITEM_HEIGHT
};
const brandedDialogIconStyle = {
color: schemeColor('icon'),
fontSize: 24
};
export const inputDialog = { export const inputDialog = {
bottomField: { bottomField: {
marginBottom: 0 marginBottom: 0
@ -145,28 +163,22 @@ ColorSchemeRegistry.register('BottomSheet', {
* Style for the {@code Icon} element in a generic item of the menu. * Style for the {@code Icon} element in a generic item of the menu.
*/ */
iconStyle: { iconStyle: {
color: schemeColor('icon'), ...brandedDialogIconStyle
fontSize: 24
}, },
/** /**
* Style for the label in a generic item rendered in the menu. * Style for the label in a generic item rendered in the menu.
*/ */
labelStyle: { labelStyle: {
color: schemeColor('text'), ...brandedDialogLabelStyle,
flexShrink: 1, marginLeft: 32
fontSize: MD_FONT_SIZE,
marginLeft: 32,
opacity: 0.90
}, },
/** /**
* Container style for a generic item rendered in the menu. * Container style for a generic item rendered in the menu.
*/ */
style: { style: {
alignItems: 'center', ...brandedDialogItemContainerStyle
flexDirection: 'row',
height: MD_ITEM_HEIGHT
}, },
/** /**
@ -258,3 +270,28 @@ ColorSchemeRegistry.register('Dialog', {
borderTopWidth: 1 borderTopWidth: 1
} }
}); });
ColorSchemeRegistry.register('SecurityDialog', {
/**
* Field on an input dialog.
*/
field: {
borderBottomWidth: 1,
borderColor: schemeColor('border'),
color: schemeColor('text'),
fontSize: 14,
paddingBottom: 8
},
text: {
color: schemeColor('text'),
fontSize: 14,
marginTop: 8
},
title: {
color: schemeColor('text'),
fontSize: 18,
fontWeight: 'bold'
}
});

View File

@ -0,0 +1,25 @@
// @flow
import { type Dispatch } from 'redux';
import {
getCurrentConference
} from '../base/conference';
/**
* Action to toggle lobby mode on or off.
*
* @param {boolean} enabled - The desired (new) state of the lobby mode.
* @returns {Function}
*/
export function toggleLobbyMode(enabled: boolean) {
return async (dispatch: Dispatch<any>, getState: Function) => {
const conference = getCurrentConference(getState);
if (enabled) {
conference.enableLobby();
} else {
conference.disableLobby();
}
};
}

View File

@ -11,6 +11,7 @@ import {
} from '../base/conference'; } from '../base/conference';
import { hideDialog, openDialog } from '../base/dialog'; import { hideDialog, openDialog } from '../base/dialog';
import { getLocalParticipant } from '../base/participants'; import { getLocalParticipant } from '../base/participants';
export * from './actions.any';
import { import {
KNOCKING_PARTICIPANT_ARRIVED_OR_UPDATED, KNOCKING_PARTICIPANT_ARRIVED_OR_UPDATED,
@ -198,20 +199,3 @@ export function startKnocking() {
}; };
} }
/**
* Action to toggle lobby mode on or off.
*
* @param {boolean} enabled - The desired (new) state of the lobby mode.
* @returns {Function}
*/
export function toggleLobbyMode(enabled: boolean) {
return async (dispatch: Dispatch<any>, getState: Function) => {
const conference = getCurrentConference(getState);
if (enabled) {
conference.enableLobby();
} else {
conference.disableLobby();
}
};
}

View File

@ -1,78 +0,0 @@
// @flow
import { getCurrentConference } from '../../../base/conference';
import { translate } from '../../../base/i18n';
import { IconMeetingUnlocked, IconMeetingLocked } from '../../../base/icons';
import { isLocalParticipantModerator } from '../../../base/participants';
import { connect } from '../../../base/redux';
import AbstractButton, { type Props as AbstractProps } from '../../../base/toolbox/components/AbstractButton';
import { showDisableLobbyModeDialog, showEnableLobbyModeDialog } from '../../actions.native';
type Props = AbstractProps & {
/**
* The Redux Dispatch function.
*/
dispatch: Function,
/**
* True if the lobby mode is currently enabled for this conference.
*/
lobbyEnabled: boolean
};
/**
* Component to render the lobby mode initiator button.
*/
class LobbyModeButton extends AbstractButton<Props, any> {
accessibilityLabel = 'toolbar.accessibilityLabel.lobbyButton';
icon = IconMeetingUnlocked;
label = 'toolbar.lobbyButtonEnable';
toggledLabel = 'toolbar.lobbyButtonDisable'
toggledIcon = IconMeetingLocked;
/**
* Callback for the click event of the button.
*
* @returns {void}
*/
_handleClick() {
const { dispatch } = this.props;
if (this._isToggled()) {
dispatch(showDisableLobbyModeDialog());
} else {
dispatch(showEnableLobbyModeDialog());
}
}
/**
* Function to define the button state.
*
* @returns {boolean}
*/
_isToggled() {
return this.props.lobbyEnabled;
}
}
/**
* Maps part of the Redux store to the props of this component.
*
* @param {Object} state - The Redux state.
* @param {Props} ownProps - The own props of the component.
* @returns {Props}
*/
export function _mapStateToProps(state: Object): $Shape<Props> {
const conference = getCurrentConference(state);
const { lobbyEnabled } = state['features/lobby'];
const { hideLobbyButton } = state['features/base/config'];
const lobbySupported = conference && conference.isLobbySupported();
return {
lobbyEnabled,
visible: lobbySupported && isLocalParticipantModerator(state) && !hideLobbyButton
};
}
export default translate(connect(_mapStateToProps)(LobbyModeButton));

View File

@ -0,0 +1,58 @@
// @flow
import React from 'react';
import { Switch, View } from 'react-native';
import { translate } from '../../../base/i18n';
import { connect } from '../../../base/redux';
import styles, {
ENABLED_THUMB_COLOR,
ENABLED_TRACK_COLOR,
DISABLED_THUMB_COLOR
} from './styles';
/**
* The type of the React {@code Component} props of {@link LobbyModeSwitch}.
*/
type Props = {
/**
* True if the lobby mode is currently enabled for this conference.
*/
lobbyEnabled: boolean,
/**
* Callback to be invoked when handling enable-disable lobby mode switch.
*/
onToggleLobbyMode: Function
};
/**
* Component meant to Enable/Disable lobby mode.
*
* @returns {React$Element<any>}
*/
function LobbyModeSwitch(
{
lobbyEnabled,
onToggleLobbyMode
}: Props) {
return (
<View style = { styles.lobbySwitchContainer }>
<Switch
onValueChange = { onToggleLobbyMode }
style = { styles.lobbySwitchIcon }
thumbColor = {
lobbyEnabled
? ENABLED_THUMB_COLOR
: DISABLED_THUMB_COLOR
}
trackColor = {{ true: ENABLED_TRACK_COLOR }}
value = { lobbyEnabled } />
</View>
);
}
export default translate(connect()(LobbyModeSwitch));

View File

@ -3,5 +3,4 @@
export { default as DisableLobbyModeDialog } from './DisableLobbyModeDialog'; export { default as DisableLobbyModeDialog } from './DisableLobbyModeDialog';
export { default as EnableLobbyModeDialog } from './EnableLobbyModeDialog'; export { default as EnableLobbyModeDialog } from './EnableLobbyModeDialog';
export { default as KnockingParticipantList } from './KnockingParticipantList'; export { default as KnockingParticipantList } from './KnockingParticipantList';
export { default as LobbyModeButton } from './LobbyModeButton';
export { default as LobbyScreen } from './LobbyScreen'; export { default as LobbyScreen } from './LobbyScreen';

View File

@ -4,6 +4,10 @@ import { ColorPalette } from '../../../base/styles';
const SECONDARY_COLOR = '#B8C7E0'; const SECONDARY_COLOR = '#B8C7E0';
export const ENABLED_THUMB_COLOR = ColorPalette.blueHighlight;
export const ENABLED_TRACK_COLOR = ColorPalette.blue;
export const DISABLED_THUMB_COLOR = ColorPalette.darkGrey;
export default { export default {
button: { button: {
alignItems: 'center', alignItems: 'center',
@ -138,5 +142,14 @@ export default {
knockingParticipantListText: { knockingParticipantListText: {
color: 'white' color: 'white'
},
lobbySwitchContainer: {
flexDirection: 'column',
marginTop: 16
},
lobbySwitchIcon: {
alignSelf: 'flex-end'
} }
}; };

View File

@ -12,34 +12,12 @@ import {
setPassword setPassword
} from '../base/conference'; } from '../base/conference';
import { hideDialog, openDialog } from '../base/dialog'; import { hideDialog, openDialog } from '../base/dialog';
import { SecurityDialog } from '../security/components/security-dialog';
import { PasswordRequiredPrompt, RoomLockPrompt } from './components'; import { PasswordRequiredPrompt } from './components';
declare var APP: Object; declare var APP: Object;
/**
* Begins a (user) request to lock a specific conference/room.
*
* @param {JitsiConference|undefined} conference - The JitsiConference to lock
* if specified or undefined if the current JitsiConference is to be locked.
* @returns {Function}
*/
export function beginRoomLockRequest(conference: ?Object) {
return (dispatch: Function, getState: Function) => {
if (typeof conference === 'undefined') {
// eslint-disable-next-line no-param-reassign
conference = getState()['features/base/conference'].conference;
}
if (conference) {
const passwordNumberOfDigits = getState()['features/base/config'].roomPasswordNumberOfDigits;
dispatch(openDialog(RoomLockPrompt, {
conference,
passwordNumberOfDigits }));
}
};
}
/** /**
* Cancels a prompt for a password to join a specific conference/room. * Cancels a prompt for a password to join a specific conference/room.
* *
@ -99,7 +77,7 @@ export function endRoomLockRequest(
= password = password
? dispatch(setPassword(conference, conference.lock, password)) ? dispatch(setPassword(conference, conference.lock, password))
: Promise.resolve(); : Promise.resolve();
const endRoomLockRequest_ = () => dispatch(hideDialog(RoomLockPrompt)); const endRoomLockRequest_ = () => dispatch(hideDialog(SecurityDialog));
setPassword_.then(endRoomLockRequest_, endRoomLockRequest_); setPassword_.then(endRoomLockRequest_, endRoomLockRequest_);
}; };
@ -137,3 +115,5 @@ export function unlockRoom() {
)); ));
}; };
} }

View File

@ -1,101 +0,0 @@
// @flow
import { MEETING_PASSWORD_ENABLED, getFeatureFlag } from '../../base/flags';
import { translate } from '../../base/i18n';
import { IconRoomLock, IconRoomUnlock } from '../../base/icons';
import { isLocalParticipantModerator } from '../../base/participants';
import { connect } from '../../base/redux';
import { AbstractButton, type AbstractButtonProps } from '../../base/toolbox/components';
import { beginRoomLockRequest, unlockRoom } from '../actions';
type Props = AbstractButtonProps & {
/**
* Whether the current local participant is a moderator, therefore is
* allowed to lock or unlock the conference.
*/
_localParticipantModerator: boolean,
/**
* Whether the current conference is locked or not.
*/
_locked: boolean,
/**
* The redux {@code dispatch} function.
*/
dispatch: Function
};
/**
* An implementation of a button for locking / unlocking a room.
*/
class RoomLockButton extends AbstractButton<Props, *> {
accessibilityLabel = 'toolbar.accessibilityLabel.lockRoom';
icon = IconRoomLock;
label = 'dialog.lockRoom';
toggledIcon = IconRoomUnlock;
toggledLabel = 'dialog.unlockRoom';
/**
* Handles clicking / pressing the button.
*
* @override
* @protected
* @returns {void}
*/
_handleClick() {
const { dispatch, _locked } = this.props;
if (_locked) {
dispatch(unlockRoom());
} else {
dispatch(beginRoomLockRequest());
}
}
/**
* Indicates whether this button is disabled or not.
*
* @override
* @protected
* @returns {boolean}
*/
_isDisabled() {
return !this.props._localParticipantModerator;
}
/**
* Indicates whether this button is in toggled state or not.
*
* @override
* @protected
* @returns {boolean}
*/
_isToggled() {
return this.props._locked;
}
}
/**
* Maps (parts of) the redux state to the associated props for the
* {@code RoomLockButton} component.
*
* @param {Object} state - The Redux state.
* @param {Object} ownProps - The properties explicitly passed to the component instance.
* @private
* @returns {Props}
*/
function _mapStateToProps(state, ownProps): Object {
const { conference, locked } = state['features/base/conference'];
const enabled = getFeatureFlag(state, MEETING_PASSWORD_ENABLED, true);
const { visible = enabled } = ownProps;
return {
_localParticipantModerator: Boolean(conference && isLocalParticipantModerator(state)),
_locked: Boolean(conference && locked),
visible
};
}
export default translate(connect(_mapStateToProps)(RoomLockButton));

View File

@ -1,140 +0,0 @@
// @flow
import React, { Component } from 'react';
import type { Dispatch } from 'redux';
import { InputDialog } from '../../base/dialog';
import { connect } from '../../base/redux';
import { endRoomLockRequest } from '../actions';
/**
* The style of the {@link TextInput} rendered by {@code RoomLockPrompt}. As it
* requests the entry of a password, {@code TextInput} automatically correcting
* the entry of the password is a pain to deal with as a user.
*/
const _TEXT_INPUT_PROPS = {
autoCapitalize: 'none',
autoCorrect: false
};
/**
* The type of the React {@code Component} props of {@link RoomLockPrompt}.
*/
type Props = {
/**
* The JitsiConference which requires a password.
*/
conference: Object,
/**
* The number of digits to be used in the password.
*/
passwordNumberOfDigits: ?number,
/**
* Redux store dispatch function.
*/
dispatch: Dispatch<any>
};
/**
* Implements a React Component which prompts the user for a password to lock a
* conference/room.
*/
class RoomLockPrompt extends Component<Props> {
/**
* Initializes a new RoomLockPrompt instance.
*
* @param {Props} props - The read-only properties with which the new
* instance is to be initialized.
*/
constructor(props: Props) {
super(props);
// Bind event handlers so they are only bound once per instance.
this._onCancel = this._onCancel.bind(this);
this._onSubmit = this._onSubmit.bind(this);
this._validateInput = this._validateInput.bind(this);
}
/**
* Implements React's {@link Component#render()}.
*
* @inheritdoc
* @returns {ReactElement}
*/
render() {
let textInputProps = _TEXT_INPUT_PROPS;
if (this.props.passwordNumberOfDigits) {
textInputProps = {
...textInputProps,
keyboardType: 'number-pad',
maxLength: this.props.passwordNumberOfDigits
};
}
return (
<InputDialog
contentKey = 'security.about'
onCancel = { this._onCancel }
onSubmit = { this._onSubmit }
textInputProps = { textInputProps }
validateInput = { this._validateInput } />
);
}
_onCancel: () => boolean;
/**
* Notifies this prompt that it has been dismissed by cancel.
*
* @private
* @returns {boolean} True to hide this dialog/prompt; otherwise, false.
*/
_onCancel() {
// An undefined password is understood to cancel the request to lock the
// conference/room.
return this._onSubmit(undefined);
}
_onSubmit: (?string) => boolean;
/**
* Notifies this prompt that it has been dismissed by submitting a specific
* value.
*
* @param {string|undefined} value - The submitted value.
* @private
* @returns {boolean} False because we do not want to hide this
* dialog/prompt as the hiding will be handled inside endRoomLockRequest
* after setting the password is resolved.
*/
_onSubmit(value: ?string) {
this.props.dispatch(endRoomLockRequest(this.props.conference, value));
return false; // Do not hide.
}
_validateInput: (string) => boolean;
/**
* Verifies input in case only digits are required.
*
* @param {string|undefined} value - The submitted value.
* @private
* @returns {boolean} False when the value is not valid and True otherwise.
*/
_validateInput(value: string) {
// we want only digits, but both number-pad and numeric add ',' and '.' as symbols
if (this.props.passwordNumberOfDigits && value.length > 0 && !/^\d+$/.test(value)) {
return false;
}
return true;
}
}
export default connect()(RoomLockPrompt);

View File

@ -0,0 +1,83 @@
// @flow
import React from 'react';
import { Switch, Text, View } from 'react-native';
import { translate } from '../../base/i18n';
import { connect } from '../../base/redux';
import { LOCKED_REMOTELY } from '../constants';
import styles, {
DISABLED_THUMB_COLOR,
ENABLED_THUMB_COLOR, ENABLED_TRACK_COLOR
} from './styles';
/**
* The type of the React {@code Component} props of {@link RoomLockSwitch}.
*/
type Props = {
/**
* Checks if the room is locked based on defined room lock constants.
*/
locked: string,
/**
* Whether the switch is disabled.
*/
disabled: boolean,
/**
* Callback to be invoked when the user toggles room lock.
*/
onToggleRoomLock: Function,
/**
* Control for room lock.
*/
toggleRoomLock: boolean,
/**
* Invoked to obtain translated strings.
*/
t: Function
};
/**
* Component meant to Add/Remove meeting password.
*
* @returns {React$Element<any>}
*/
function RoomLockSwitch(
{
locked,
disabled,
onToggleRoomLock,
toggleRoomLock,
t
}: Props) {
return (
<View style = { styles.roomLockSwitchContainer }>
<Text>
{
locked === LOCKED_REMOTELY
&& t('passwordSetRemotely')
}
</Text>
<Switch
disabled = { disabled }
onValueChange = { onToggleRoomLock }
thumbColor = {
toggleRoomLock
? ENABLED_THUMB_COLOR
: DISABLED_THUMB_COLOR
}
trackColor = {{ true: ENABLED_TRACK_COLOR }}
value = { toggleRoomLock } />
</View>
);
}
export default translate(connect()(RoomLockSwitch));

View File

@ -1,3 +1 @@
export { default as PasswordRequiredPrompt } from './PasswordRequiredPrompt'; export { default as PasswordRequiredPrompt } from './PasswordRequiredPrompt';
export { default as RoomLockButton } from './RoomLockButton';
export { default as RoomLockPrompt } from './RoomLockPrompt';

View File

@ -0,0 +1,16 @@
// @flow
import { ColorPalette } from '../../base/styles';
export const ENABLED_THUMB_COLOR = ColorPalette.blueHighlight;
export const ENABLED_TRACK_COLOR = ColorPalette.blue;
export const DISABLED_THUMB_COLOR = ColorPalette.darkGrey;
export default {
roomLockSwitchContainer: {
alignItems: 'center',
flexDirection: 'row',
justifyContent: 'space-between',
marginTop: 16
}
};

View File

@ -16,7 +16,7 @@ import {
} from '../notifications'; } from '../notifications';
import { _openPasswordRequiredPrompt } from './actions'; import { _openPasswordRequiredPrompt } from './actions';
import { PasswordRequiredPrompt, RoomLockPrompt } from './components'; import { PasswordRequiredPrompt } from './components';
import { LOCKED_REMOTELY } from './constants'; import { LOCKED_REMOTELY } from './constants';
import logger from './logger'; import logger from './logger';
@ -85,7 +85,6 @@ MiddlewareRegistry.register(store => next => action => {
*/ */
function _conferenceJoined({ dispatch }, next, action) { function _conferenceJoined({ dispatch }, next, action) {
dispatch(hideDialog(PasswordRequiredPrompt)); dispatch(hideDialog(PasswordRequiredPrompt));
dispatch(hideDialog(RoomLockPrompt));
return next(action); return next(action);
} }
@ -116,7 +115,6 @@ function _conferenceFailed({ dispatch }, next, action) {
} }
} else { } else {
dispatch(hideDialog(PasswordRequiredPrompt)); dispatch(hideDialog(PasswordRequiredPrompt));
dispatch(hideDialog(RoomLockPrompt));
} }
return next(action); return next(action);

View File

@ -1,5 +1,7 @@
// @flow // @flow
import type { Dispatch } from 'redux';
import { createToolbarEvent, sendAnalytics } from '../../../analytics'; import { createToolbarEvent, sendAnalytics } from '../../../analytics';
import { translate } from '../../../base/i18n'; import { translate } from '../../../base/i18n';
import { IconSecurityOff, IconSecurityOn } from '../../../base/icons'; import { IconSecurityOff, IconSecurityOn } from '../../../base/icons';
@ -16,10 +18,9 @@ type Props = AbstractButtonProps & {
_locked: boolean, _locked: boolean,
/** /**
* On click handler that opens the security dialog. * The redux {@code dispatch} function.
*/ */
onClick: Function dispatch: Dispatch<any>
}; };
@ -40,7 +41,7 @@ class SecurityDialogButton extends AbstractButton<Props, *> {
*/ */
_handleClick() { _handleClick() {
sendAnalytics(createToolbarEvent('toggle.security', { enable: !this.props._locked })); sendAnalytics(createToolbarEvent('toggle.security', { enable: !this.props._locked }));
this.props.onClick(); this.props.dispatch(toggleSecurityDialog());
} }
/** /**
@ -69,14 +70,4 @@ function mapStateToProps(state: Object) {
}; };
} }
/** export default translate(connect(mapStateToProps)(SecurityDialogButton));
* Maps dispatching of some action to React component props.
*
* @param {Function} dispatch - Redux action dispatcher.
* @returns {Props}
*/
const mapDispatchToProps = {
onClick: () => toggleSecurityDialog()
};
export default translate(connect(mapStateToProps, mapDispatchToProps)(SecurityDialogButton));

View File

@ -0,0 +1 @@
export * from './native';

View File

@ -0,0 +1 @@
export * from './web';

View File

@ -1,4 +1 @@
// @flow export * from './_';
export { default as SecurityDialog } from './SecurityDialog';
export { default as SecurityDialogButton } from './SecurityDialogButton';

View File

@ -0,0 +1,444 @@
// @flow
import React, { PureComponent } from 'react';
import {
KeyboardAvoidingView,
Platform,
Text,
TextInput,
View
} from 'react-native';
import { connect } from 'react-redux';
import type { Dispatch } from 'redux';
import { ColorSchemeRegistry } from '../../../../base/color-scheme';
import {
FIELD_UNDERLINE,
CustomSubmitDialog
} from '../../../../base/dialog';
import { getFeatureFlag, MEETING_PASSWORD_ENABLED } from '../../../../base/flags';
import { translate } from '../../../../base/i18n';
import { isLocalParticipantModerator } from '../../../../base/participants';
import { StyleType } from '../../../../base/styles';
import { toggleLobbyMode } from '../../../../lobby/actions.any';
import LobbyModeSwitch
from '../../../../lobby/components/native/LobbyModeSwitch';
import { LOCKED_LOCALLY } from '../../../../room-lock';
import {
endRoomLockRequest,
unlockRoom
} from '../../../../room-lock/actions';
import RoomLockSwitch from '../../../../room-lock/components/RoomLockSwitch';
/**
* The style of the {@link TextInput} rendered by {@code SecurityDialog}. As it
* requests the entry of a password, {@code TextInput} automatically correcting
* the entry of the password is a pain to deal with as a user.
*/
const _TEXT_INPUT_PROPS = {
autoCapitalize: 'none',
autoCorrect: false
};
/**
* The type of the React {@code Component} props of {@link SecurityDialog}.
*/
type Props = {
/**
* The JitsiConference which requires a password.
*/
_conference: Object,
/**
* The color-schemed stylesheet of the feature.
*/
_dialogStyles: StyleType,
/**
* Whether the local user is the moderator.
*/
_isModerator: boolean,
/**
* State of the lobby mode.
*/
_lobbyEnabled: boolean,
/**
* Whether the lobby mode switch is available or not.
*/
_lobbyModeSwitchVisible: boolean,
/**
* The value for how the conference is locked (or undefined if not locked)
* as defined by room-lock constants.
*/
_locked: string,
/**
* Checks if the conference room is locked or not.
*/
_lockedConference: boolean,
/**
* The current known password for the JitsiConference.
*/
_password: string,
/**
* Number of digits used in the room-lock password.
*/
_passwordNumberOfDigits: number,
/**
* Whether the room lock switch is available or not.
*/
_roomLockSwitchVisible: boolean,
/**
* The color-schemed stylesheet of the security dialog feature.
*/
_securityDialogStyles: StyleType,
/**
* Redux store dispatch function.
*/
dispatch: Dispatch<any>,
/**
* Invoked to obtain translated strings.
*/
t: Function
};
/**
* The type of the React {@code Component} state of {@link SecurityDialog}.
*/
type State = {
/**
* Password added by the participant for room lock.
*/
passwordInputValue: string,
/**
* Shows an input or a message.
*/
showElement: boolean
};
/**
* Component that renders the security options dialog.
*
* @returns {React$Element<any>}
*/
class SecurityDialog extends PureComponent<Props, State> {
/**
* Instantiates a new {@code SecurityDialog}.
*
* @inheritdoc
*/
constructor(props: Props) {
super(props);
this.state = {
passwordInputValue: '',
showElement: props._locked === LOCKED_LOCALLY || false
};
this._onChangeText = this._onChangeText.bind(this);
this._onSubmit = this._onSubmit.bind(this);
this._onToggleLobbyMode = this._onToggleLobbyMode.bind(this);
this._onToggleRoomLock = this._onToggleRoomLock.bind(this);
}
/**
* Implements {@code SecurityDialog.render}.
*
* @inheritdoc
*/
render() {
return (
<CustomSubmitDialog
onSubmit = { this._onSubmit }>
<KeyboardAvoidingView
behavior =
{
Platform.OS === 'ios'
? 'padding' : 'height'
}
enabled = { true }>
{ this._renderLobbyMode() }
{ this._renderRoomLock() }
</KeyboardAvoidingView>
</CustomSubmitDialog>
);
}
/**
* Renders lobby mode.
*
* @returns {ReactElement}
* @private
*/
_renderLobbyMode() {
const {
_lobbyEnabled,
_lobbyModeSwitchVisible,
_securityDialogStyles,
t
} = this.props;
if (!_lobbyModeSwitchVisible) {
return null;
}
return (
<View>
<Text style = { _securityDialogStyles.title } >
{ t('lobby.dialogTitle') }
</Text>
<Text style = { _securityDialogStyles.text } >
{ t('lobby.enableDialogText') }
</Text>
<LobbyModeSwitch
lobbyEnabled = { _lobbyEnabled }
onToggleLobbyMode = { this._onToggleLobbyMode } />
</View>
);
}
/**
* Renders room lock.
*
* @returns {ReactElement}
* @private
*/
_renderRoomLock() {
const {
_isModerator,
_locked,
_lockedConference,
_roomLockSwitchVisible,
_securityDialogStyles,
t
} = this.props;
const { showElement } = this.state;
if (!_roomLockSwitchVisible) {
return null;
}
return (
<View>
<Text style = { _securityDialogStyles.title } >
{ t('dialog.lockRoom') }
</Text>
<Text style = { _securityDialogStyles.text } >
{ t('security.about') }
</Text>
<RoomLockSwitch
disabled = { !_isModerator }
locked = { _locked }
onToggleRoomLock = { this._onToggleRoomLock }
toggleRoomLock = { showElement || _lockedConference } />
{ this._renderRoomLockMessage() }
</View>
);
}
/**
* Renders room lock text input/message.
*
* @returns {ReactElement}
* @private
*/
_renderRoomLockMessage() {
let textInputProps = _TEXT_INPUT_PROPS;
const {
_isModerator,
_locked,
_password,
_passwordNumberOfDigits,
_securityDialogStyles,
t
} = this.props;
const { passwordInputValue, showElement } = this.state;
if (_passwordNumberOfDigits) {
textInputProps = {
...textInputProps,
keyboardType: 'numeric',
maxLength: _passwordNumberOfDigits
};
}
if (!_isModerator) {
return null;
}
if (showElement) {
if (typeof _locked === 'undefined') {
return (
<TextInput
onChangeText = { this._onChangeText }
placeholder = { t('lobby.passwordField') }
style = { _securityDialogStyles.field }
underlineColorAndroid = { FIELD_UNDERLINE }
value = { passwordInputValue }
{ ...textInputProps } />
);
} else if (_locked) {
if (_locked === LOCKED_LOCALLY && typeof _password !== 'undefined') {
return (
<TextInput
onChangeText = { this._onChangeText }
placeholder = { _password }
style = { _securityDialogStyles.field }
underlineColorAndroid = { FIELD_UNDERLINE }
value = { passwordInputValue }
{ ...textInputProps } />
);
}
}
}
}
_onToggleLobbyMode: () => void;
/**
* Handles the enable-disable lobby mode switch.
*
* @private
* @returns {void}
*/
_onToggleLobbyMode() {
const { _lobbyEnabled, dispatch } = this.props;
if (_lobbyEnabled) {
dispatch(toggleLobbyMode(false));
} else {
dispatch(toggleLobbyMode(true));
}
}
_onToggleRoomLock: () => void;
/**
* Callback to be invoked when room lock button is pressed.
*
* @returns {void}
*/
_onToggleRoomLock() {
const { _isModerator, _locked, dispatch } = this.props;
const { showElement } = this.state;
this.setState({
showElement: !showElement
});
if (_locked && _isModerator) {
dispatch(unlockRoom());
this.setState({
showElement: false
});
}
}
/**
* Verifies input in case only digits are required.
*
* @param {string} passwordInputValue - The value of the password
* text input.
* @private
* @returns {boolean} False when the value is not valid and True otherwise.
*/
_validateInputValue(passwordInputValue: string) {
const { _passwordNumberOfDigits } = this.props;
// we want only digits,
// but both number-pad and numeric add ',' and '.' as symbols
if (_passwordNumberOfDigits
&& passwordInputValue.length > 0
&& !/^\d+$/.test(passwordInputValue)) {
return false;
}
return true;
}
_onChangeText: string => void;
/**
* Callback to be invoked when the text in the field changes.
*
* @param {string} passwordInputValue - The value of password input.
* @returns {void}
*/
_onChangeText(passwordInputValue) {
if (!this._validateInputValue(passwordInputValue)) {
return;
}
this.setState({
passwordInputValue
});
}
_onSubmit: () => boolean;
/**
* Submits value typed in text input.
*
* @returns {boolean} False because we do not want to hide this
* dialog/prompt as the hiding will be handled inside endRoomLockRequest
* after setting the password is resolved.
*/
_onSubmit() {
const {
_conference,
dispatch
} = this.props;
const { passwordInputValue } = this.state;
dispatch(endRoomLockRequest(_conference, passwordInputValue));
return false;
}
}
/**
* Maps part of the Redux state to the props of this component.
*
* @param {Object} state - The Redux state.
* @returns {Props}
*/
function _mapStateToProps(state: Object): Object {
const { conference, locked, password } = state['features/base/conference'];
const { hideLobbyButton } = state['features/base/config'];
const { lobbyEnabled } = state['features/lobby'];
const { roomPasswordNumberOfDigits } = state['features/base/config'];
const lobbySupported = conference && conference.isLobbySupported();
const visible = getFeatureFlag(state, MEETING_PASSWORD_ENABLED, true);
return {
_conference: conference,
_dialogStyles: ColorSchemeRegistry.get(state, 'Dialog'),
_isModerator: isLocalParticipantModerator(state),
_lobbyEnabled: lobbyEnabled,
_lobbyModeSwitchVisible:
lobbySupported && isLocalParticipantModerator(state) && !hideLobbyButton,
_locked: locked,
_lockedConference: Boolean(conference && locked),
_password: password,
_passwordNumberOfDigits: roomPasswordNumberOfDigits,
_roomLockSwitchVisible: visible,
_securityDialogStyles: ColorSchemeRegistry.get(state, 'SecurityDialog')
};
}
export default translate(connect(_mapStateToProps)(SecurityDialog));

View File

@ -0,0 +1,4 @@
// @flow
export { default as SecurityDialog } from './SecurityDialog';

View File

@ -2,8 +2,8 @@
import React, { Component } from 'react'; import React, { Component } from 'react';
import { translate } from '../../../base/i18n'; import { translate } from '../../../../base/i18n';
import { LOCKED_LOCALLY } from '../../../room-lock'; import { LOCKED_LOCALLY } from '../../../../room-lock';
/** /**
* The type of the React {@code Component} props of {@link PasswordForm}. * The type of the React {@code Component} props of {@link PasswordForm}.

View File

@ -3,8 +3,8 @@
import React, { useRef } from 'react'; import React, { useRef } from 'react';
import { translate } from '../../../base/i18n'; import { translate } from '../../../../base/i18n';
import { copyText } from '../../../base/util'; import { copyText } from '../../../../base/util';
import PasswordForm from './PasswordForm'; import PasswordForm from './PasswordForm';

View File

@ -2,13 +2,13 @@
import React, { useState, useEffect } from 'react'; import React, { useState, useEffect } from 'react';
import { setPassword as setPass } from '../../../base/conference'; import { setPassword as setPass } from '../../../../base/conference';
import { Dialog } from '../../../base/dialog'; import { Dialog } from '../../../../base/dialog';
import { translate } from '../../../base/i18n'; import { translate } from '../../../../base/i18n';
import { isLocalParticipantModerator } from '../../../base/participants'; import { isLocalParticipantModerator } from '../../../../base/participants';
import { connect } from '../../../base/redux'; import { connect } from '../../../../base/redux';
import { E2EESection } from '../../../e2ee/components'; import { E2EESection } from '../../../../e2ee/components';
import { LobbySection } from '../../../lobby'; import { LobbySection } from '../../../../lobby';
import PasswordSection from './PasswordSection'; import PasswordSection from './PasswordSection';

View File

@ -0,0 +1,3 @@
// @flow
export { default as SecurityDialog } from './SecurityDialog';

View File

@ -11,10 +11,9 @@ import { connect } from '../../../base/redux';
import { StyleType } from '../../../base/styles'; import { StyleType } from '../../../base/styles';
import { SharedDocumentButton } from '../../../etherpad'; import { SharedDocumentButton } from '../../../etherpad';
import { InviteButton } from '../../../invite'; import { InviteButton } from '../../../invite';
import { LobbyModeButton } from '../../../lobby/components/native';
import { AudioRouteButton } from '../../../mobile/audio-mode'; import { AudioRouteButton } from '../../../mobile/audio-mode';
import { LiveStreamButton, RecordButton } from '../../../recording'; import { LiveStreamButton, RecordButton } from '../../../recording';
import { RoomLockButton } from '../../../room-lock'; import SecurityDialogButton from '../../../security/components/security-dialog/SecurityDialogButton';
import { SharedVideoButton } from '../../../shared-video/components'; import { SharedVideoButton } from '../../../shared-video/components';
import { ClosedCaptionButton } from '../../../subtitles'; import { ClosedCaptionButton } from '../../../subtitles';
import { TileViewButton } from '../../../video-layout'; import { TileViewButton } from '../../../video-layout';
@ -140,7 +139,7 @@ class OverflowMenu extends PureComponent<Props, State> {
{!toolbarButtons.has('invite') && <InviteButton { ...buttonProps } />} {!toolbarButtons.has('invite') && <InviteButton { ...buttonProps } />}
<AudioOnlyButton { ...buttonProps } /> <AudioOnlyButton { ...buttonProps } />
{!toolbarButtons.has('raisehand') && <RaiseHandButton { ...buttonProps } />} {!toolbarButtons.has('raisehand') && <RaiseHandButton { ...buttonProps } />}
<LobbyModeButton { ...buttonProps } /> <SecurityDialogButton { ...buttonProps } />
<ScreenSharingButton { ...buttonProps } /> <ScreenSharingButton { ...buttonProps } />
<MoreOptionsButton { ...moreOptionsButtonProps } /> <MoreOptionsButton { ...moreOptionsButtonProps } />
<Collapsible collapsed = { !showMore }> <Collapsible collapsed = { !showMore }>
@ -149,7 +148,6 @@ class OverflowMenu extends PureComponent<Props, State> {
<RecordButton { ...buttonProps } /> <RecordButton { ...buttonProps } />
<LiveStreamButton { ...buttonProps } /> <LiveStreamButton { ...buttonProps } />
<SharedVideoButton { ...buttonProps } /> <SharedVideoButton { ...buttonProps } />
<RoomLockButton { ...buttonProps } />
<ClosedCaptionButton { ...buttonProps } /> <ClosedCaptionButton { ...buttonProps } />
<SharedDocumentButton { ...buttonProps } /> <SharedDocumentButton { ...buttonProps } />
<MuteEveryoneButton { ...buttonProps } /> <MuteEveryoneButton { ...buttonProps } />

View File

@ -47,7 +47,7 @@ import {
LiveStreamButton, LiveStreamButton,
RecordButton RecordButton
} from '../../../recording'; } from '../../../recording';
import { SecurityDialogButton } from '../../../security'; import SecurityDialogButton from '../../../security/components/security-dialog/SecurityDialogButton';
import { import {
SETTINGS_TABS, SETTINGS_TABS,
SettingsButton, SettingsButton,