This commit is contained in:
@@ -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));
|
||||
Reference in New Issue
Block a user