init
Some checks failed
Close stale issues and PRs / stale (push) Has been cancelled

This commit is contained in:
2025-09-02 14:49:16 +08:00
commit 38ba663466
2885 changed files with 391107 additions and 0 deletions

View 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));
};
}

View File

@@ -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
})
};
}

View File

@@ -0,0 +1,2 @@
// @ts-ignore
export { default as SecurityDialog } from './native/SecurityDialog';

View File

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

View File

@@ -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));

View File

@@ -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));

View File

@@ -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
}
};

View File

@@ -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'>&nbsp;</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>
);
}

View File

@@ -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;

View File

@@ -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>
);
}

View File

@@ -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));

View 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);
}

View 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;
}
}