This commit is contained in:
15
react/features/security/actions.ts
Normal file
15
react/features/security/actions.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
import { IStore } from '../app/types';
|
||||
import { toggleDialog } from '../base/dialog/actions';
|
||||
|
||||
import { SecurityDialog } from './components/security-dialog';
|
||||
|
||||
/**
|
||||
* Action that triggers toggle of the security options dialog.
|
||||
*
|
||||
* @returns {Function}
|
||||
*/
|
||||
export function toggleSecurityDialog() {
|
||||
return function(dispatch: IStore['dispatch']) {
|
||||
dispatch(toggleDialog(SecurityDialog));
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,92 @@
|
||||
import { createToolbarEvent } from '../../../analytics/AnalyticsEvents';
|
||||
import { sendAnalytics } from '../../../analytics/functions';
|
||||
import { IReduxState } from '../../../app/types';
|
||||
import { getSecurityUiConfig } from '../../../base/config/functions.any';
|
||||
import { LOBBY_MODE_ENABLED, MEETING_PASSWORD_ENABLED, SECURITY_OPTIONS_ENABLED } from '../../../base/flags/constants';
|
||||
import { getFeatureFlag } from '../../../base/flags/functions';
|
||||
import { IconSecurityOff, IconSecurityOn } from '../../../base/icons/svg';
|
||||
import { isLocalParticipantModerator } from '../../../base/participants/functions';
|
||||
import AbstractButton, { IProps as AbstractButtonProps } from '../../../base/toolbox/components/AbstractButton';
|
||||
import { isSecurityDialogButtonVisible } from '../../functions';
|
||||
|
||||
export interface IProps extends AbstractButtonProps {
|
||||
|
||||
/**
|
||||
* Whether the shared document is being edited or not.
|
||||
*/
|
||||
_locked: boolean;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Implements an {@link AbstractButton} to open the security dialog/screen.
|
||||
*/
|
||||
export default class AbstractSecurityDialogButton<P extends IProps, S>
|
||||
extends AbstractButton<P, S> {
|
||||
override accessibilityLabel = 'toolbar.accessibilityLabel.security';
|
||||
override icon = IconSecurityOff;
|
||||
override label = 'toolbar.security';
|
||||
override toggledIcon = IconSecurityOn;
|
||||
override tooltip = 'toolbar.security';
|
||||
|
||||
/**
|
||||
* Helper function to be implemented by subclasses, which should be used
|
||||
* to handle the security button being clicked / pressed.
|
||||
*
|
||||
* @protected
|
||||
* @returns {void}
|
||||
*/
|
||||
_handleClickSecurityButton() {
|
||||
// To be implemented by subclass.
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles clicking / pressing the button.
|
||||
*
|
||||
* @private
|
||||
* @returns {void}
|
||||
*/
|
||||
override _handleClick() {
|
||||
const { _locked } = this.props;
|
||||
|
||||
sendAnalytics(createToolbarEvent('toggle.security', { enable: !_locked }));
|
||||
this._handleClickSecurityButton();
|
||||
}
|
||||
|
||||
/**
|
||||
* Indicates whether this button is in toggled state or not.
|
||||
*
|
||||
* @override
|
||||
* @returns {boolean}
|
||||
*/
|
||||
override _isToggled() {
|
||||
return this.props._locked;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Maps part of the redux state to the component's props.
|
||||
*
|
||||
* @param {Object} state - The redux store/state.
|
||||
* @returns {IProps}
|
||||
*/
|
||||
export function _mapStateToProps(state: IReduxState) {
|
||||
const { conference } = state['features/base/conference'];
|
||||
const { locked } = state['features/base/conference'];
|
||||
const { lobbyEnabled } = state['features/lobby'];
|
||||
const enabledSecurityOptionsFlag = getFeatureFlag(state, SECURITY_OPTIONS_ENABLED, true);
|
||||
const enabledLobbyModeFlag = getFeatureFlag(state, LOBBY_MODE_ENABLED, true);
|
||||
const enabledMeetingPassFlag = getFeatureFlag(state, MEETING_PASSWORD_ENABLED, true);
|
||||
|
||||
return {
|
||||
_locked: Boolean(locked || lobbyEnabled),
|
||||
visible: isSecurityDialogButtonVisible({
|
||||
conference,
|
||||
securityUIConfig: getSecurityUiConfig(state),
|
||||
isModerator: isLocalParticipantModerator(state),
|
||||
enabledLobbyModeFlag,
|
||||
enabledMeetingPassFlag,
|
||||
enabledSecurityOptionsFlag
|
||||
})
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
// @ts-ignore
|
||||
export { default as SecurityDialog } from './native/SecurityDialog';
|
||||
@@ -0,0 +1 @@
|
||||
export { default as SecurityDialog } from './web/SecurityDialog';
|
||||
@@ -0,0 +1,533 @@
|
||||
import React, { PureComponent } from 'react';
|
||||
import {
|
||||
Text,
|
||||
TextStyle,
|
||||
View,
|
||||
ViewStyle
|
||||
} from 'react-native';
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
import { IReduxState, IStore } from '../../../../app/types';
|
||||
import { IJitsiConference } from '../../../../base/conference/reducer';
|
||||
import { getSecurityUiConfig } from '../../../../base/config/functions.any';
|
||||
import { MEETING_PASSWORD_ENABLED } from '../../../../base/flags/constants';
|
||||
import { getFeatureFlag } from '../../../../base/flags/functions';
|
||||
import { translate } from '../../../../base/i18n/functions';
|
||||
import JitsiScreen from '../../../../base/modal/components/JitsiScreen';
|
||||
import { isLocalParticipantModerator } from '../../../../base/participants/functions';
|
||||
import Button from '../../../../base/ui/components/native/Button';
|
||||
import Input from '../../../../base/ui/components/native/Input';
|
||||
import Switch from '../../../../base/ui/components/native/Switch';
|
||||
import { BUTTON_TYPES } from '../../../../base/ui/constants.native';
|
||||
import { copyText } from '../../../../base/util/copyText.native';
|
||||
import { isInBreakoutRoom } from '../../../../breakout-rooms/functions';
|
||||
import { toggleLobbyMode } from '../../../../lobby/actions.any';
|
||||
import { isEnablingLobbyAllowed } from '../../../../lobby/functions';
|
||||
import {
|
||||
endRoomLockRequest,
|
||||
unlockRoom
|
||||
} from '../../../../room-lock/actions';
|
||||
import { LOCKED_LOCALLY, LOCKED_REMOTELY } from '../../../../room-lock/constants';
|
||||
|
||||
import styles from './styles';
|
||||
|
||||
/**
|
||||
* 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}.
|
||||
*/
|
||||
interface IProps {
|
||||
|
||||
/**
|
||||
* The JitsiConference which requires a password.
|
||||
*/
|
||||
_conference?: IJitsiConference;
|
||||
|
||||
/**
|
||||
* Whether enabling lobby is allowed or not.
|
||||
*/
|
||||
_isEnablingLobbyAllowed: boolean;
|
||||
|
||||
/**
|
||||
* Whether the local user is the moderator.
|
||||
*/
|
||||
_isModerator: boolean;
|
||||
|
||||
/**
|
||||
* Whether lobby mode is enabled or not.
|
||||
*/
|
||||
_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 setting a room password is available or not.
|
||||
*/
|
||||
_roomPasswordControls: boolean;
|
||||
|
||||
/**
|
||||
* Redux store dispatch function.
|
||||
*/
|
||||
dispatch: IStore['dispatch'];
|
||||
|
||||
/**
|
||||
* Invoked to obtain translated strings.
|
||||
*/
|
||||
t: Function;
|
||||
}
|
||||
|
||||
/**
|
||||
* The type of the React {@code Component} state of {@link SecurityDialog}.
|
||||
*/
|
||||
interface IState {
|
||||
|
||||
/**
|
||||
* State of lobby mode.
|
||||
*/
|
||||
lobbyEnabled: boolean;
|
||||
|
||||
/**
|
||||
* 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<IProps, IState> {
|
||||
|
||||
/**
|
||||
* Instantiates a new {@code SecurityDialog}.
|
||||
*
|
||||
* @inheritdoc
|
||||
*/
|
||||
constructor(props: IProps) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
lobbyEnabled: props._lobbyEnabled,
|
||||
passwordInputValue: '',
|
||||
showElement: props._locked === LOCKED_LOCALLY || false
|
||||
};
|
||||
|
||||
this._onChangeText = this._onChangeText.bind(this);
|
||||
this._onCancel = this._onCancel.bind(this);
|
||||
this._onCopy = this._onCopy.bind(this);
|
||||
this._onSubmit = this._onSubmit.bind(this);
|
||||
this._onToggleLobbyMode = this._onToggleLobbyMode.bind(this);
|
||||
this._onAddPassword = this._onAddPassword.bind(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements {@code SecurityDialog.render}.
|
||||
*
|
||||
* @inheritdoc
|
||||
*/
|
||||
override render() {
|
||||
return (
|
||||
<JitsiScreen style = { styles.securityDialogContainer }>
|
||||
{ this._renderLobbyMode() }
|
||||
{ this._renderSetRoomPassword() }
|
||||
</JitsiScreen>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders lobby mode.
|
||||
*
|
||||
* @returns {ReactElement}
|
||||
* @private
|
||||
*/
|
||||
_renderLobbyMode() {
|
||||
const {
|
||||
_isEnablingLobbyAllowed,
|
||||
_lobbyModeSwitchVisible,
|
||||
t
|
||||
} = this.props;
|
||||
|
||||
if (!_lobbyModeSwitchVisible || !_isEnablingLobbyAllowed) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<View style = { styles.lobbyModeContainer }>
|
||||
<View style = { styles.lobbyModeContent } >
|
||||
<Text style = { styles.lobbyModeText }>
|
||||
{ t('lobby.enableDialogText') }
|
||||
</Text>
|
||||
<View style = { styles.lobbyModeSection as ViewStyle }>
|
||||
<Text style = { styles.lobbyModeLabel as TextStyle } >
|
||||
{ t('lobby.toggleLabel') }
|
||||
</Text>
|
||||
<Switch
|
||||
checked = { this.state.lobbyEnabled }
|
||||
onChange = { this._onToggleLobbyMode } />
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders setting the password.
|
||||
*
|
||||
* @returns {ReactElement}
|
||||
* @private
|
||||
*/
|
||||
_renderSetRoomPassword() {
|
||||
const {
|
||||
_isModerator,
|
||||
_locked,
|
||||
_lockedConference,
|
||||
_password,
|
||||
_roomPasswordControls,
|
||||
t
|
||||
} = this.props;
|
||||
const { showElement } = this.state;
|
||||
let setPasswordControls;
|
||||
|
||||
if (!_roomPasswordControls) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (_locked && showElement) {
|
||||
setPasswordControls = (
|
||||
<>
|
||||
<Button
|
||||
accessibilityLabel = 'dialog.Remove'
|
||||
labelKey = 'dialog.Remove'
|
||||
labelStyle = { styles.passwordSetupButtonLabel }
|
||||
onClick = { this._onCancel }
|
||||
type = { BUTTON_TYPES.TERTIARY } />
|
||||
{
|
||||
_password
|
||||
&& <Button
|
||||
accessibilityLabel = 'dialog.copy'
|
||||
labelKey = 'dialog.copy'
|
||||
labelStyle = { styles.passwordSetupButtonLabel }
|
||||
onClick = { this._onCopy }
|
||||
type = { BUTTON_TYPES.TERTIARY } />
|
||||
}
|
||||
</>
|
||||
);
|
||||
} else if (!_lockedConference && showElement) {
|
||||
setPasswordControls = (
|
||||
<>
|
||||
<Button
|
||||
accessibilityLabel = 'dialog.Cancel'
|
||||
labelKey = 'dialog.Cancel'
|
||||
labelStyle = { styles.passwordSetupButtonLabel }
|
||||
onClick = { this._onCancel }
|
||||
type = { BUTTON_TYPES.TERTIARY } />
|
||||
<Button
|
||||
accessibilityLabel = 'dialog.add'
|
||||
labelKey = 'dialog.add'
|
||||
labelStyle = { styles.passwordSetupButtonLabel }
|
||||
onClick = { this._onSubmit }
|
||||
type = { BUTTON_TYPES.TERTIARY } />
|
||||
</>
|
||||
);
|
||||
} else if (!_lockedConference && !showElement) {
|
||||
setPasswordControls = (
|
||||
<Button
|
||||
accessibilityLabel = 'info.addPassword'
|
||||
disabled = { !_isModerator }
|
||||
labelKey = 'info.addPassword'
|
||||
labelStyle = { styles.passwordSetupButtonLabel }
|
||||
onClick = { this._onAddPassword }
|
||||
type = { BUTTON_TYPES.TERTIARY } />
|
||||
);
|
||||
}
|
||||
|
||||
if (_locked === LOCKED_REMOTELY) {
|
||||
if (_isModerator) {
|
||||
setPasswordControls = (
|
||||
<View style = { styles.passwordSetRemotelyContainer as ViewStyle }>
|
||||
<Text style = { styles.passwordSetRemotelyText }>
|
||||
{ t('passwordSetRemotely') }
|
||||
</Text>
|
||||
<Button
|
||||
accessibilityLabel = 'dialog.Remove'
|
||||
labelKey = 'dialog.Remove'
|
||||
labelStyle = { styles.passwordSetupButtonLabel }
|
||||
onClick = { this._onCancel }
|
||||
type = { BUTTON_TYPES.TERTIARY } />
|
||||
</View>
|
||||
);
|
||||
} else {
|
||||
setPasswordControls = (
|
||||
<View style = { styles.passwordSetRemotelyContainer as ViewStyle }>
|
||||
<Text style = { styles.passwordSetRemotelyTextDisabled }>
|
||||
{ t('passwordSetRemotely') }
|
||||
</Text>
|
||||
<Button
|
||||
accessibilityLabel = 'info.addPassword'
|
||||
disabled = { !_isModerator }
|
||||
labelKey = 'info.addPassword'
|
||||
labelStyle = { styles.passwordSetupButtonLabel }
|
||||
onClick = { this._onAddPassword }
|
||||
type = { BUTTON_TYPES.TERTIARY } />
|
||||
</View>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<View
|
||||
style = { styles.passwordContainer } >
|
||||
<Text style = { styles.passwordContainerText }>
|
||||
{ t(_isModerator ? 'security.about' : 'security.aboutReadOnly') }
|
||||
</Text>
|
||||
<View
|
||||
style = {
|
||||
_locked !== LOCKED_REMOTELY
|
||||
&& styles.passwordContainerControls as ViewStyle
|
||||
}>
|
||||
<View>
|
||||
{ this._setRoomPasswordMessage() }
|
||||
</View>
|
||||
{ _isModerator && setPasswordControls }
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders room lock text input/message.
|
||||
*
|
||||
* @returns {ReactElement}
|
||||
* @private
|
||||
*/
|
||||
_setRoomPasswordMessage() {
|
||||
let textInputProps: any = _TEXT_INPUT_PROPS;
|
||||
const {
|
||||
_isModerator,
|
||||
_locked,
|
||||
_password,
|
||||
_passwordNumberOfDigits,
|
||||
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 (
|
||||
<Input
|
||||
accessibilityLabel = { t('info.addPassword') }
|
||||
autoFocus = { true }
|
||||
clearable = { true }
|
||||
customStyles = {{ container: styles.customContainer }}
|
||||
onChange = { this._onChangeText }
|
||||
placeholder = { t('dialog.password') }
|
||||
value = { passwordInputValue }
|
||||
{ ...textInputProps } />
|
||||
);
|
||||
} else if (_locked) {
|
||||
if (_locked === LOCKED_LOCALLY && typeof _password !== 'undefined') {
|
||||
return (
|
||||
<View style = { styles.savedPasswordContainer as ViewStyle }>
|
||||
<Text style = { styles.savedPasswordLabel as TextStyle }>
|
||||
{ t('info.password') }
|
||||
</Text>
|
||||
<Text style = { styles.savedPassword }>
|
||||
{ _password }
|
||||
</Text>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles the enable-disable lobby mode switch.
|
||||
*
|
||||
* @private
|
||||
* @returns {void}
|
||||
*/
|
||||
_onToggleLobbyMode() {
|
||||
const { dispatch } = this.props;
|
||||
const { lobbyEnabled } = this.state;
|
||||
|
||||
this.setState({
|
||||
lobbyEnabled: !lobbyEnabled
|
||||
});
|
||||
|
||||
dispatch(toggleLobbyMode(!lobbyEnabled));
|
||||
}
|
||||
|
||||
/**
|
||||
* Callback to be invoked when add password button is pressed.
|
||||
*
|
||||
* @returns {void}
|
||||
*/
|
||||
_onAddPassword() {
|
||||
const { showElement } = this.state;
|
||||
|
||||
this.setState({
|
||||
showElement: !showElement
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 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;
|
||||
}
|
||||
|
||||
/**
|
||||
* Callback to be invoked when the text in the field changes.
|
||||
*
|
||||
* @param {string} passwordInputValue - The value of password input.
|
||||
* @returns {void}
|
||||
*/
|
||||
_onChangeText(passwordInputValue: string) {
|
||||
if (!this._validateInputValue(passwordInputValue)) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.setState({
|
||||
passwordInputValue
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Cancels value typed in text input.
|
||||
*
|
||||
* @returns {void}
|
||||
*/
|
||||
_onCancel() {
|
||||
this.setState({
|
||||
passwordInputValue: '',
|
||||
showElement: false
|
||||
});
|
||||
|
||||
this.props.dispatch(unlockRoom());
|
||||
}
|
||||
|
||||
/**
|
||||
* Copies room password.
|
||||
*
|
||||
* @returns {void}
|
||||
*/
|
||||
_onCopy() {
|
||||
const { passwordInputValue } = this.state;
|
||||
|
||||
copyText(passwordInputValue);
|
||||
}
|
||||
|
||||
/**
|
||||
* Submits value typed in text input.
|
||||
*
|
||||
* @returns {void}
|
||||
*/
|
||||
_onSubmit() {
|
||||
const {
|
||||
_conference,
|
||||
dispatch
|
||||
} = this.props;
|
||||
const { passwordInputValue } = this.state;
|
||||
|
||||
_conference && dispatch(endRoomLockRequest(_conference, passwordInputValue));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Maps part of the Redux state to the props of this component.
|
||||
*
|
||||
* @param {Object} state - The Redux state.
|
||||
* @returns {IProps}
|
||||
*/
|
||||
function _mapStateToProps(state: IReduxState) {
|
||||
const { conference, locked, password } = state['features/base/conference'];
|
||||
const { disableLobbyPassword, hideLobbyButton } = getSecurityUiConfig(state);
|
||||
const { lobbyEnabled } = state['features/lobby'];
|
||||
const { roomPasswordNumberOfDigits } = state['features/base/config'];
|
||||
const lobbySupported = conference?.isLobbySupported();
|
||||
const visible = getFeatureFlag(state, MEETING_PASSWORD_ENABLED, true);
|
||||
|
||||
return {
|
||||
_conference: conference,
|
||||
_isEnablingLobbyAllowed: isEnablingLobbyAllowed(state),
|
||||
_isModerator: isLocalParticipantModerator(state),
|
||||
_lobbyEnabled: lobbyEnabled,
|
||||
_lobbyModeSwitchVisible:
|
||||
lobbySupported && isLocalParticipantModerator(state) && !hideLobbyButton && !isInBreakoutRoom(state),
|
||||
_locked: locked,
|
||||
_lockedConference: Boolean(conference && locked),
|
||||
_password: password,
|
||||
_passwordNumberOfDigits: roomPasswordNumberOfDigits,
|
||||
_roomPasswordControls: visible && !disableLobbyPassword
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
export default translate(connect(_mapStateToProps)(SecurityDialog));
|
||||
@@ -0,0 +1,27 @@
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
import { translate } from '../../../../base/i18n/functions';
|
||||
import { navigate } from '../../../../mobile/navigation/components/conference/ConferenceNavigationContainerRef';
|
||||
import { screen } from '../../../../mobile/navigation/routes';
|
||||
import AbstractSecurityDialogButton, {
|
||||
IProps as AbstractSecurityDialogButtonProps,
|
||||
_mapStateToProps as _abstractMapStateToProps
|
||||
} from '../AbstractSecurityDialogButton';
|
||||
|
||||
/**
|
||||
* Implements an {@link AbstractSecurityDialogButton} to open the security screen.
|
||||
*/
|
||||
class SecurityDialogButton<P extends AbstractSecurityDialogButtonProps, S> extends AbstractSecurityDialogButton<P, S> {
|
||||
|
||||
/**
|
||||
* Opens / closes the security screen.
|
||||
*
|
||||
* @private
|
||||
* @returns {void}
|
||||
*/
|
||||
_handleClickSecurityButton() {
|
||||
navigate(screen.conference.security);
|
||||
}
|
||||
}
|
||||
|
||||
export default translate(connect(_abstractMapStateToProps)(SecurityDialogButton));
|
||||
@@ -0,0 +1,95 @@
|
||||
import BaseTheme from '../../../../base/ui/components/BaseTheme.native';
|
||||
|
||||
/**
|
||||
* The styles of the feature security.
|
||||
*/
|
||||
export default {
|
||||
|
||||
securityDialogContainer: {
|
||||
backgroundColor: BaseTheme.palette.ui01,
|
||||
flex: 1
|
||||
},
|
||||
|
||||
headerCloseButton: {
|
||||
marginLeft: 12
|
||||
},
|
||||
|
||||
lobbyModeContainer: {
|
||||
borderBottomColor: BaseTheme.palette.ui07,
|
||||
borderBottomWidth: 1,
|
||||
marginTop: BaseTheme.spacing[4]
|
||||
},
|
||||
|
||||
lobbyModeContent: {
|
||||
marginHorizontal: BaseTheme.spacing[3],
|
||||
marginBottom: BaseTheme.spacing[4]
|
||||
},
|
||||
|
||||
lobbyModeText: {
|
||||
color: BaseTheme.palette.text01
|
||||
},
|
||||
|
||||
lobbyModeLabel: {
|
||||
color: BaseTheme.palette.text01,
|
||||
fontWeight: 'bold',
|
||||
marginTop: BaseTheme.spacing[2]
|
||||
},
|
||||
|
||||
lobbyModeSection: {
|
||||
alignItems: 'center',
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'space-between',
|
||||
marginTop: BaseTheme.spacing[1]
|
||||
},
|
||||
|
||||
passwordContainer: {
|
||||
marginHorizontal: BaseTheme.spacing[3],
|
||||
marginTop: BaseTheme.spacing[4]
|
||||
},
|
||||
|
||||
passwordContainerText: {
|
||||
color: BaseTheme.palette.text01
|
||||
},
|
||||
|
||||
passwordContainerControls: {
|
||||
alignItems: 'center',
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'space-between'
|
||||
},
|
||||
|
||||
savedPasswordContainer: {
|
||||
flexDirection: 'row',
|
||||
width: 208
|
||||
},
|
||||
|
||||
savedPasswordLabel: {
|
||||
color: BaseTheme.palette.text01,
|
||||
fontWeight: 'bold'
|
||||
},
|
||||
|
||||
savedPassword: {
|
||||
color: BaseTheme.palette.text01
|
||||
},
|
||||
|
||||
customContainer: {
|
||||
width: 208
|
||||
},
|
||||
|
||||
passwordSetupButtonLabel: {
|
||||
color: BaseTheme.palette.link01
|
||||
},
|
||||
|
||||
passwordSetRemotelyContainer: {
|
||||
alignItems: 'center',
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'space-between'
|
||||
},
|
||||
|
||||
passwordSetRemotelyText: {
|
||||
color: BaseTheme.palette.text01
|
||||
},
|
||||
|
||||
passwordSetRemotelyTextDisabled: {
|
||||
color: BaseTheme.palette.text02
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,115 @@
|
||||
import React, { useCallback, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import Input from '../../../../base/ui/components/web/Input';
|
||||
import { LOCKED_LOCALLY } from '../../../../room-lock/constants';
|
||||
|
||||
/**
|
||||
* The type of the React {@code Component} props of {@link PasswordForm}.
|
||||
*/
|
||||
interface IProps {
|
||||
|
||||
/**
|
||||
* Whether or not to show the password editing field.
|
||||
*/
|
||||
editEnabled: boolean;
|
||||
|
||||
/**
|
||||
* The value for how the conference is locked (or undefined if not locked)
|
||||
* as defined by room-lock constants.
|
||||
*/
|
||||
locked?: string;
|
||||
|
||||
/**
|
||||
* Callback to invoke when the local participant is submitting a password
|
||||
* set request.
|
||||
*/
|
||||
onSubmit: Function;
|
||||
|
||||
/**
|
||||
* The current known password for the JitsiConference.
|
||||
*/
|
||||
password?: string;
|
||||
|
||||
/**
|
||||
* The number of digits to be used in the password.
|
||||
*/
|
||||
passwordNumberOfDigits?: number;
|
||||
|
||||
/**
|
||||
* Whether or not the password should be visible.
|
||||
*/
|
||||
visible: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* React {@code Component} for displaying and editing the conference password.
|
||||
*
|
||||
* @returns {ReactElement}
|
||||
*/
|
||||
export default function PasswordForm({
|
||||
editEnabled,
|
||||
locked,
|
||||
onSubmit,
|
||||
password,
|
||||
passwordNumberOfDigits,
|
||||
visible
|
||||
}: IProps) {
|
||||
const { t } = useTranslation();
|
||||
const [ enteredPassword, setEnteredPassword ] = useState('');
|
||||
const onKeyPress = useCallback(event => {
|
||||
if (event.key === 'Enter') {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
|
||||
onSubmit(enteredPassword);
|
||||
}
|
||||
}, [ onSubmit, enteredPassword ]);
|
||||
|
||||
if (!editEnabled && enteredPassword && enteredPassword !== '') {
|
||||
setEnteredPassword('');
|
||||
}
|
||||
|
||||
const placeHolderText
|
||||
= passwordNumberOfDigits ? t('passwordDigitsOnly', { number: passwordNumberOfDigits }) : t('dialog.password');
|
||||
|
||||
|
||||
return (
|
||||
<div className = 'info-password'>
|
||||
{ locked && <>
|
||||
<span className = 'info-label'>
|
||||
{t('info.password')}
|
||||
</span>
|
||||
<span className = 'spacer'> </span>
|
||||
<span className = 'info-password-field info-value'>
|
||||
{locked === LOCKED_LOCALLY ? (
|
||||
<div className = 'info-password-local'>
|
||||
{ visible ? password : '******' }
|
||||
</div>
|
||||
) : (
|
||||
<div className = 'info-password-remote'>
|
||||
{ t('passwordSetRemotely') }
|
||||
</div>
|
||||
) }
|
||||
</span>
|
||||
</>
|
||||
}
|
||||
{
|
||||
editEnabled && <div
|
||||
className = 'info-password-form'>
|
||||
<Input
|
||||
accessibilityLabel = { t('info.addPassword') }
|
||||
autoFocus = { true }
|
||||
id = 'info-password-input'
|
||||
maxLength = { passwordNumberOfDigits }
|
||||
mode = { passwordNumberOfDigits ? 'numeric' : undefined }
|
||||
onChange = { setEnteredPassword }
|
||||
onKeyPress = { onKeyPress }
|
||||
placeholder = { placeHolderText }
|
||||
type = 'password'
|
||||
value = { enteredPassword } />
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,189 @@
|
||||
/* eslint-disable react/jsx-no-bind */
|
||||
import React, { useCallback, useRef, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
|
||||
import { IReduxState } from '../../../../app/types';
|
||||
import { setPassword } from '../../../../base/conference/actions';
|
||||
import { isLocalParticipantModerator } from '../../../../base/participants/functions';
|
||||
import { copyText } from '../../../../base/util/copyText.web';
|
||||
import { LOCKED_LOCALLY } from '../../../../room-lock/constants';
|
||||
import { NOTIFY_CLICK_MODE } from '../../../../toolbox/types';
|
||||
|
||||
import PasswordForm from './PasswordForm';
|
||||
|
||||
const DIGITS_ONLY = /^\d+$/;
|
||||
const KEY = 'add-passcode';
|
||||
|
||||
/**
|
||||
* Component that handles the password manipulation from the invite dialog.
|
||||
*
|
||||
* @returns {React$Element<any>}
|
||||
*/
|
||||
function PasswordSection() {
|
||||
const { t } = useTranslation();
|
||||
const dispatch = useDispatch();
|
||||
const canEditPassword = useSelector(isLocalParticipantModerator);
|
||||
const passwordNumberOfDigits = useSelector(
|
||||
(state: IReduxState) => state['features/base/config'].roomPasswordNumberOfDigits);
|
||||
const conference = useSelector((state: IReduxState) => state['features/base/conference'].conference);
|
||||
const locked = useSelector((state: IReduxState) => state['features/base/conference'].locked);
|
||||
const password = useSelector((state: IReduxState) => state['features/base/conference'].password);
|
||||
const formRef = useRef<HTMLDivElement>(null);
|
||||
const [ passwordVisible, setPasswordVisible ] = useState(false);
|
||||
const buttonsWithNotifyClick = useSelector(
|
||||
(state: IReduxState) => state['features/toolbox'].buttonsWithNotifyClick);
|
||||
const [ passwordEditEnabled, setPasswordEditEnabled ] = useState(false);
|
||||
|
||||
if (passwordEditEnabled && (password || locked)) {
|
||||
setPasswordEditEnabled(false);
|
||||
}
|
||||
|
||||
const onPasswordSubmit = useCallback((enteredPassword: string) => {
|
||||
if (enteredPassword && passwordNumberOfDigits && !DIGITS_ONLY.test(enteredPassword)) {
|
||||
// Don't set the password.
|
||||
return;
|
||||
}
|
||||
dispatch(setPassword(conference, conference?.lock, enteredPassword));
|
||||
}, [ dispatch, passwordNumberOfDigits, conference?.lock ]);
|
||||
|
||||
const onTogglePasswordEditState = useCallback(() => {
|
||||
if (typeof APP === 'undefined' || !buttonsWithNotifyClick?.size) {
|
||||
setPasswordEditEnabled(!passwordEditEnabled);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
const notifyMode = buttonsWithNotifyClick?.get(KEY);
|
||||
|
||||
if (notifyMode) {
|
||||
APP.API.notifyToolbarButtonClicked(
|
||||
KEY, notifyMode === NOTIFY_CLICK_MODE.PREVENT_AND_NOTIFY
|
||||
);
|
||||
}
|
||||
|
||||
if (!notifyMode || notifyMode === NOTIFY_CLICK_MODE.ONLY_NOTIFY) {
|
||||
setPasswordEditEnabled(!passwordEditEnabled);
|
||||
}
|
||||
}, [ buttonsWithNotifyClick, setPasswordEditEnabled, passwordEditEnabled ]);
|
||||
|
||||
const onPasswordSave = useCallback(() => {
|
||||
if (formRef.current) {
|
||||
// @ts-ignore
|
||||
const { value } = formRef.current.querySelector('div > input');
|
||||
|
||||
if (value) {
|
||||
onPasswordSubmit(value);
|
||||
}
|
||||
}
|
||||
}, [ formRef.current, onPasswordSubmit ]);
|
||||
|
||||
const onPasswordRemove = useCallback(() => {
|
||||
onPasswordSubmit('');
|
||||
}, [ onPasswordSubmit ]);
|
||||
|
||||
|
||||
const onPasswordCopy = useCallback(() => {
|
||||
copyText(password ?? '');
|
||||
}, [ password ]);
|
||||
|
||||
const onPasswordShow = useCallback(() => {
|
||||
setPasswordVisible(true);
|
||||
}, [ setPasswordVisible ]);
|
||||
|
||||
const onPasswordHide = useCallback(() => {
|
||||
setPasswordVisible(false);
|
||||
}, [ setPasswordVisible ]);
|
||||
|
||||
let actions = null;
|
||||
|
||||
if (canEditPassword) {
|
||||
if (passwordEditEnabled) {
|
||||
actions = (
|
||||
<>
|
||||
<button
|
||||
className = 'as-link'
|
||||
onClick = { onTogglePasswordEditState }
|
||||
type = 'button'>
|
||||
{ t('dialog.Cancel') }
|
||||
<span className = 'sr-only'>({ t('dialog.password') })</span>
|
||||
</button>
|
||||
<button
|
||||
className = 'as-link'
|
||||
onClick = { onPasswordSave }
|
||||
type = 'button'>
|
||||
{ t('dialog.add') }
|
||||
<span className = 'sr-only'>({ t('dialog.password') })</span>
|
||||
</button>
|
||||
</>
|
||||
);
|
||||
} else if (locked) {
|
||||
actions = (
|
||||
<>
|
||||
<button
|
||||
className = 'remove-password as-link'
|
||||
onClick = { onPasswordRemove }
|
||||
type = 'button'>
|
||||
{ t('dialog.Remove') }
|
||||
<span className = 'sr-only'>({ t('dialog.password') })</span>
|
||||
</button>
|
||||
{
|
||||
|
||||
// There are cases like lobby and grant moderator when password is not available
|
||||
password ? <>
|
||||
<button
|
||||
className = 'copy-password as-link'
|
||||
onClick = { onPasswordCopy }
|
||||
type = 'button'>
|
||||
{ t('dialog.copy') }
|
||||
<span className = 'sr-only'>({ t('dialog.password') })</span>
|
||||
</button>
|
||||
</> : null
|
||||
}
|
||||
{locked === LOCKED_LOCALLY && (
|
||||
<button
|
||||
className = 'as-link'
|
||||
onClick = { passwordVisible ? onPasswordHide : onPasswordShow }
|
||||
type = 'button'>
|
||||
{t(passwordVisible ? 'dialog.hide' : 'dialog.show')}
|
||||
<span className = 'sr-only'>({ t('dialog.password') })</span>
|
||||
</button>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
} else {
|
||||
actions = (
|
||||
<button
|
||||
className = 'add-password as-link'
|
||||
onClick = { onTogglePasswordEditState }
|
||||
type = 'button'>{ t('info.addPassword') }</button>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className = 'security-dialog password-section'>
|
||||
<p className = 'description'>
|
||||
{ t(canEditPassword ? 'security.about' : 'security.aboutReadOnly') }
|
||||
</p>
|
||||
<div className = 'security-dialog password'>
|
||||
<div
|
||||
className = 'info-dialog info-dialog-column info-dialog-password'
|
||||
ref = { formRef }>
|
||||
<PasswordForm
|
||||
editEnabled = { passwordEditEnabled }
|
||||
locked = { locked }
|
||||
onSubmit = { onPasswordSubmit }
|
||||
password = { password }
|
||||
passwordNumberOfDigits = { passwordNumberOfDigits }
|
||||
visible = { passwordVisible } />
|
||||
</div>
|
||||
<div className = 'security-dialog password-actions'>
|
||||
{ actions }
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default PasswordSection;
|
||||
@@ -0,0 +1,65 @@
|
||||
import React from 'react';
|
||||
import { useSelector } from 'react-redux';
|
||||
|
||||
import { IReduxState } from '../../../../app/types';
|
||||
import { getSecurityUiConfig } from '../../../../base/config/functions.any';
|
||||
import { isLocalParticipantModerator } from '../../../../base/participants/functions';
|
||||
import Dialog from '../../../../base/ui/components/web/Dialog';
|
||||
import { isInBreakoutRoom } from '../../../../breakout-rooms/functions';
|
||||
import E2EESection from '../../../../e2ee/components/E2EESection';
|
||||
import LobbySection from '../../../../lobby/components/web/LobbySection';
|
||||
import { isEnablingLobbyAllowed } from '../../../../lobby/functions';
|
||||
|
||||
import PasswordSection from './PasswordSection';
|
||||
|
||||
export interface INotifyClick {
|
||||
key: string;
|
||||
preventExecution: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Component that renders the security options dialog.
|
||||
*
|
||||
* @returns {React$Element<any>}
|
||||
*/
|
||||
export default function SecurityDialog() {
|
||||
const lobbySupported = useSelector((state: IReduxState) =>
|
||||
state['features/base/conference'].conference?.isLobbySupported());
|
||||
const e2eeSupported = useSelector((state: IReduxState) => state['features/base/conference'].e2eeSupported);
|
||||
const isInBreakout = useSelector(isInBreakoutRoom);
|
||||
const disableLobbyPassword = useSelector((state: IReduxState) => getSecurityUiConfig(state)?.disableLobbyPassword)
|
||||
|| isInBreakout;
|
||||
const isModerator = useSelector(isLocalParticipantModerator);
|
||||
const { hideLobbyButton } = useSelector(getSecurityUiConfig);
|
||||
const _isLobbyVisible = useSelector(isEnablingLobbyAllowed)
|
||||
&& lobbySupported && isModerator && !isInBreakout && !hideLobbyButton;
|
||||
const showE2ee = Boolean(e2eeSupported) && isModerator;
|
||||
|
||||
return (
|
||||
<Dialog
|
||||
cancel = {{ hidden: true }}
|
||||
ok = {{ hidden: true }}
|
||||
titleKey = 'security.title'>
|
||||
<div className = 'security-dialog'>
|
||||
{
|
||||
_isLobbyVisible && <LobbySection />
|
||||
}
|
||||
{
|
||||
!disableLobbyPassword && (
|
||||
<>
|
||||
{ _isLobbyVisible && <div className = 'separator-line' /> }
|
||||
<PasswordSection />
|
||||
</>
|
||||
)
|
||||
}
|
||||
{
|
||||
showE2ee ? <>
|
||||
{ (_isLobbyVisible || !disableLobbyPassword) && <div className = 'separator-line' /> }
|
||||
<E2EESection />
|
||||
</> : null
|
||||
}
|
||||
|
||||
</div>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
import { translate } from '../../../../base/i18n/functions';
|
||||
import { toggleSecurityDialog } from '../../../actions';
|
||||
import AbstractSecurityDialogButton, {
|
||||
IProps as AbstractSecurityDialogButtonProps,
|
||||
_mapStateToProps as _abstractMapStateToProps
|
||||
} from '../AbstractSecurityDialogButton';
|
||||
|
||||
/**
|
||||
* Implements an {@link AbstractSecurityDialogButton} to open the security dialog.
|
||||
*/
|
||||
class SecurityDialogButton<P extends AbstractSecurityDialogButtonProps, S> extends AbstractSecurityDialogButton<P, S> {
|
||||
|
||||
/**
|
||||
* Opens / closes the security dialog.
|
||||
*
|
||||
* @private
|
||||
* @returns {void}
|
||||
*/
|
||||
override _handleClickSecurityButton() {
|
||||
const { dispatch } = this.props;
|
||||
|
||||
dispatch(toggleSecurityDialog());
|
||||
}
|
||||
}
|
||||
|
||||
export default translate(connect(_abstractMapStateToProps)(SecurityDialogButton));
|
||||
28
react/features/security/functions.ts
Normal file
28
react/features/security/functions.ts
Normal file
@@ -0,0 +1,28 @@
|
||||
/**
|
||||
* Returns true if the security dialog button should be visible and false otherwise.
|
||||
*
|
||||
* @param {Object} options - The parameters needed to determine the security dialog button visibility.
|
||||
* @returns {boolean}
|
||||
*/
|
||||
export function isSecurityDialogButtonVisible({
|
||||
conference,
|
||||
securityUIConfig,
|
||||
isModerator,
|
||||
enabledLobbyModeFlag,
|
||||
enabledSecurityOptionsFlag,
|
||||
enabledMeetingPassFlag
|
||||
}: {
|
||||
conference: any;
|
||||
enabledLobbyModeFlag: boolean;
|
||||
enabledMeetingPassFlag: boolean;
|
||||
enabledSecurityOptionsFlag: boolean;
|
||||
isModerator: boolean;
|
||||
securityUIConfig: { hideLobbyButton?: boolean; };
|
||||
}) {
|
||||
const { hideLobbyButton } = securityUIConfig;
|
||||
const lobbySupported = conference?.isLobbySupported();
|
||||
const lobby = lobbySupported && isModerator && !hideLobbyButton;
|
||||
|
||||
|
||||
return enabledSecurityOptionsFlag && ((enabledLobbyModeFlag && lobby) || enabledMeetingPassFlag);
|
||||
}
|
||||
45
react/features/security/hooks.web.ts
Normal file
45
react/features/security/hooks.web.ts
Normal file
@@ -0,0 +1,45 @@
|
||||
import { useSelector } from 'react-redux';
|
||||
|
||||
import { IReduxState } from '../app/types';
|
||||
import { getSecurityUiConfig } from '../base/config/functions.any';
|
||||
import { LOBBY_MODE_ENABLED, MEETING_PASSWORD_ENABLED, SECURITY_OPTIONS_ENABLED } from '../base/flags/constants';
|
||||
import { getFeatureFlag } from '../base/flags/functions';
|
||||
import { isLocalParticipantModerator } from '../base/participants/functions';
|
||||
|
||||
import SecurityDialogButton from './components/security-dialog/web/SecurityDialogButton';
|
||||
import { isSecurityDialogButtonVisible } from './functions';
|
||||
|
||||
const security = {
|
||||
key: 'security',
|
||||
alias: 'info',
|
||||
Content: SecurityDialogButton,
|
||||
group: 2
|
||||
};
|
||||
|
||||
/**
|
||||
* A hook that returns the security dialog button if it is enabled and undefined otherwise.
|
||||
*
|
||||
* @returns {Object | undefined}
|
||||
*/
|
||||
export function useSecurityDialogButton() {
|
||||
const conference = useSelector((state: IReduxState) => state['features/base/conference'].conference);
|
||||
const securityUIConfig = useSelector(getSecurityUiConfig);
|
||||
const isModerator = useSelector(isLocalParticipantModerator);
|
||||
const enabledLobbyModeFlag
|
||||
= useSelector((state: IReduxState) => getFeatureFlag(state, LOBBY_MODE_ENABLED, true));
|
||||
const enabledSecurityOptionsFlag
|
||||
= useSelector((state: IReduxState) => getFeatureFlag(state, SECURITY_OPTIONS_ENABLED, true));
|
||||
const enabledMeetingPassFlag
|
||||
= useSelector((state: IReduxState) => getFeatureFlag(state, MEETING_PASSWORD_ENABLED, true));
|
||||
|
||||
if (isSecurityDialogButtonVisible({
|
||||
conference,
|
||||
securityUIConfig,
|
||||
isModerator,
|
||||
enabledLobbyModeFlag,
|
||||
enabledSecurityOptionsFlag,
|
||||
enabledMeetingPassFlag
|
||||
})) {
|
||||
return security;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user