diff --git a/react/features/base/conference/middleware.any.ts b/react/features/base/conference/middleware.any.ts index d1f0aea6b6..975b7291f1 100644 --- a/react/features/base/conference/middleware.any.ts +++ b/react/features/base/conference/middleware.any.ts @@ -16,6 +16,7 @@ import { removeLobbyChatParticipant } from '../../chat/actions.any'; import { openDisplayNamePrompt } from '../../display-name/actions'; import { showErrorNotification } from '../../notifications/actions'; import { NOTIFICATION_TIMEOUT_TYPE } from '../../notifications/constants'; +import { hasDisplayName } from '../../prejoin/utils'; import { stopLocalVideoRecording } from '../../recording/actions.any'; import LocalRecordingManager from '../../recording/components/Recording/LocalRecordingManager'; import { iAmVisitor } from '../../visitors/functions'; @@ -276,7 +277,9 @@ function _conferenceJoined({ dispatch, getState }: IStore, next: Function, actio if (requireDisplayName && !getLocalParticipant(getState)?.name && !conference.isHidden()) { - dispatch(openDisplayNamePrompt(undefined)); + dispatch(openDisplayNamePrompt({ + validateInput: hasDisplayName + })); } return result; diff --git a/react/features/base/dialog/components/native/InputDialog.tsx b/react/features/base/dialog/components/native/InputDialog.tsx index 06d9db8ec6..78388d42fd 100644 --- a/react/features/base/dialog/components/native/InputDialog.tsx +++ b/react/features/base/dialog/components/native/InputDialog.tsx @@ -20,6 +20,11 @@ interface IProps extends AbstractProps, WithTranslation { */ descriptionKey?: string; + /** + * Whether to display the cancel button. + */ + disableCancel?: boolean; + /** * An optional initial value to initiate the field with. */ @@ -52,6 +57,11 @@ interface IState extends AbstractState { * The current value of the field. */ fieldValue?: string; + + /** + * The result of the input validation. + */ + isValid: boolean; } /** @@ -68,6 +78,7 @@ class InputDialog extends AbstractDialog { this.state = { fieldValue: props.initialValue, + isValid: props.validateInput ? props.validateInput(props.initialValue) : true, submitting: false }; @@ -115,10 +126,11 @@ class InputDialog extends AbstractDialog { ) } - + onPress = { this._onCancel } />} @@ -132,10 +144,14 @@ class InputDialog extends AbstractDialog { * @returns {void} */ _onChangeText(fieldValue: string) { - if (this.props.validateInput && !this.props.validateInput(fieldValue)) { + if (this.props.validateInput) { + this.setState({ + isValid: this.props.validateInput(fieldValue), + fieldValue + }); + return; } - this.setState({ fieldValue }); diff --git a/react/features/base/ui/components/web/BaseDialog.tsx b/react/features/base/ui/components/web/BaseDialog.tsx index 47d86d2da8..16dedd749c 100644 --- a/react/features/base/ui/components/web/BaseDialog.tsx +++ b/react/features/base/ui/components/web/BaseDialog.tsx @@ -136,6 +136,7 @@ export interface IProps { description?: string; disableBackdropClose?: boolean; disableEnter?: boolean; + disableEscape?: boolean; onClose?: () => void; size?: 'large' | 'medium'; submit?: () => void; @@ -150,6 +151,7 @@ const BaseDialog = ({ description, disableBackdropClose, disableEnter, + disableEscape, onClose, size = 'medium', submit, @@ -166,7 +168,7 @@ const BaseDialog = ({ }, [ disableBackdropClose, onClose ]); const handleKeyDown = useCallback((e: KeyboardEvent) => { - if (e.key === 'Escape') { + if (e.key === 'Escape' && !disableEscape) { onClose?.(); } if (e.key === 'Enter' && !disableEnter) { diff --git a/react/features/base/ui/components/web/Dialog.tsx b/react/features/base/ui/components/web/Dialog.tsx index 61e9808a1b..59c96d59dd 100644 --- a/react/features/base/ui/components/web/Dialog.tsx +++ b/react/features/base/ui/components/web/Dialog.tsx @@ -92,6 +92,7 @@ const Dialog = ({ disableBackdropClose, hideCloseButton, disableEnter, + disableEscape, ok = { translationKey: 'dialog.Ok' }, onCancel, onSubmit, @@ -125,6 +126,7 @@ const Dialog = ({ description = { description } disableBackdropClose = { disableBackdropClose } disableEnter = { disableEnter } + disableEscape = { disableEscape } onClose = { onClose } size = { size } submit = { submit } @@ -170,7 +172,7 @@ const Dialog = ({ id = 'modal-dialog-ok-button' isSubmit = { true } labelKey = { ok.translationKey } - onClick = { submit } />} + { ...(!ok.disabled && { onClick: submit }) } />} ); diff --git a/react/features/display-name/actions.ts b/react/features/display-name/actions.ts index c307b0fbe6..7880e9e22f 100644 --- a/react/features/display-name/actions.ts +++ b/react/features/display-name/actions.ts @@ -5,12 +5,17 @@ import { DisplayNamePrompt } from './components'; /** * Signals to open a dialog with the {@code DisplayNamePrompt} component. * - * @param {?Function} onPostSubmit - The function to invoke after a successful - * submit of the dialog. + * @param {Object} params - Map containing the callbacks to be executed in the prompt: + * - onPostSubmit - The function to invoke after a successful submit of the dialog. + * - validateInput - The function to invoke after a change in the display name value. * @returns {Object} */ -export function openDisplayNamePrompt(onPostSubmit?: Function) { +export function openDisplayNamePrompt({ onPostSubmit, validateInput }: { + onPostSubmit?: Function; + validateInput?: Function; +}) { return openDialog(DisplayNamePrompt, { - onPostSubmit + onPostSubmit, + validateInput }); } diff --git a/react/features/display-name/components/native/DisplayNamePrompt.tsx b/react/features/display-name/components/native/DisplayNamePrompt.tsx index bf3a28354f..49c1f3ecee 100644 --- a/react/features/display-name/components/native/DisplayNamePrompt.tsx +++ b/react/features/display-name/components/native/DisplayNamePrompt.tsx @@ -33,8 +33,10 @@ class DisplayNamePrompt extends Component { return ( + titleKey = 'dialog.displayNameRequired' + validateInput = { this.props.validateInput } /> ); } } diff --git a/react/features/display-name/components/web/DisplayNamePrompt.tsx b/react/features/display-name/components/web/DisplayNamePrompt.tsx index b5b3632fcd..11d0d684b1 100644 --- a/react/features/display-name/components/web/DisplayNamePrompt.tsx +++ b/react/features/display-name/components/web/DisplayNamePrompt.tsx @@ -7,6 +7,8 @@ import Input from '../../../base/ui/components/web/Input'; import { onSetDisplayName } from '../../functions'; import { IProps } from '../../types'; +const INITIAL_DISPLAY_NAME = ''; + /** * The type of the React {@code Component} props of {@link DisplayNamePrompt}. */ @@ -16,6 +18,11 @@ interface IState { * The name to show in the display name text field. */ displayName: string; + + /** + * The result of the input validation. + */ + isValid: boolean; } /** @@ -37,7 +44,8 @@ class DisplayNamePrompt extends Component { super(props); this.state = { - displayName: '' + displayName: INITIAL_DISPLAY_NAME, + isValid: this.props.validateInput ? this.props.validateInput(INITIAL_DISPLAY_NAME) : true }; // Bind event handlers so they are only bound once for every instance. @@ -53,10 +61,19 @@ class DisplayNamePrompt extends Component { * @returns {ReactElement} */ render() { + const disableCloseDialog = Boolean(this.props.validateInput); + return ( { * @returns {void} */ _onDisplayNameChange(value: string) { + if (this.props.validateInput) { + this.setState({ + isValid: this.props.validateInput(value), + displayName: value + }); + + return; + } this.setState({ displayName: value }); diff --git a/react/features/display-name/types.ts b/react/features/display-name/types.ts index 8f6ad22531..223fc2788b 100644 --- a/react/features/display-name/types.ts +++ b/react/features/display-name/types.ts @@ -17,4 +17,9 @@ export interface IProps extends WithTranslation { * Function to be invoked after a successful display name change. */ onPostSubmit?: Function; + + /** + * Function to be invoked after a display name change. + */ + validateInput?: Function; } diff --git a/react/features/prejoin/components/native/Prejoin.tsx b/react/features/prejoin/components/native/Prejoin.tsx index 63a09c9950..d0b35945f3 100644 --- a/react/features/prejoin/components/native/Prejoin.tsx +++ b/react/features/prejoin/components/native/Prejoin.tsx @@ -1,5 +1,5 @@ import { useIsFocused } from '@react-navigation/native'; -import React, { useCallback, useEffect, useLayoutEffect, useState } from 'react'; +import React, { useCallback, useEffect, useLayoutEffect, useMemo, useState } from 'react'; import { useTranslation } from 'react-i18next'; import { BackHandler, @@ -16,6 +16,7 @@ import { appNavigate } from '../../../app/actions.native'; import { IReduxState } from '../../../app/types'; import { setAudioOnly } from '../../../base/audio-only/actions'; import { getConferenceName } from '../../../base/conference/functions'; +import { isNameReadOnly } from '../../../base/config/functions.any'; import { connect } from '../../../base/connection/actions.native'; import { PREJOIN_PAGE_HIDE_DISPLAY_NAME } from '../../../base/flags/constants'; import { getFeatureFlag } from '../../../base/flags/functions'; @@ -28,6 +29,7 @@ import { updateSettings } from '../../../base/settings/actions'; import Button from '../../../base/ui/components/native/Button'; import Input from '../../../base/ui/components/native/Input'; import { BUTTON_TYPES } from '../../../base/ui/constants.native'; +import { openDisplayNamePrompt } from '../../../display-name/actions'; import BrandingImageBackground from '../../../dynamic-branding/components/native/BrandingImageBackground'; import LargeVideo from '../../../large-video/components/LargeVideo.native'; import HeaderNavigationButton from '../../../mobile/navigation/components/HeaderNavigationButton'; @@ -37,6 +39,7 @@ import AudioMuteButton from '../../../toolbox/components/native/AudioMuteButton' import VideoMuteButton from '../../../toolbox/components/native/VideoMuteButton'; import { isDisplayNameRequired } from '../../functions'; import { IPrejoinProps } from '../../types'; +import { hasDisplayName } from '../../utils'; import { preJoinStyles as styles } from './styles'; @@ -52,10 +55,19 @@ const Prejoin: React.FC = ({ navigation }: IPrejoinProps) => { const isDisplayNameMandatory = useSelector((state: IReduxState) => isDisplayNameRequired(state)); const isDisplayNameVisible = useSelector((state: IReduxState) => !getFeatureFlag(state, PREJOIN_PAGE_HIDE_DISPLAY_NAME, false)); + const isDisplayNameReadonly = !useSelector(isNameReadOnly); const roomName = useSelector((state: IReduxState) => getConferenceName(state)); const participantName = localParticipant?.name; const [ displayName, setDisplayName ] = useState(participantName || ''); + const isDisplayNameMissing = useMemo( + () => !displayName && isDisplayNameMandatory, [ displayName, isDisplayNameMandatory ]); + const showDisplayNameError = useMemo( + () => !isDisplayNameReadonly && isDisplayNameMissing && isDisplayNameVisible, + [ isDisplayNameMissing, isDisplayNameReadonly, isDisplayNameVisible ]); + const showDisplayNameInput = useMemo( + () => isDisplayNameVisible && (displayName || !isDisplayNameReadonly), + [ displayName, isDisplayNameReadonly, isDisplayNameVisible ]); const [ isJoining, setIsJoining ] = useState(false); const onChangeDisplayName = useCallback(event => { @@ -73,9 +85,20 @@ const Prejoin: React.FC = ({ navigation }: IPrejoinProps) => { navigateRoot(screen.conference.root); }, [ dispatch ]); + const maybeJoin = useCallback(() => { + if (isDisplayNameMissing) { + dispatch(openDisplayNamePrompt({ + onPostSubmit: onJoin, + validateInput: hasDisplayName + })); + } else { + onJoin(); + } + }, [ dispatch, hasDisplayName, isDisplayNameMissing, onJoin ]); + const onJoinLowBandwidth = useCallback(() => { dispatch(setAudioOnly(true)); - onJoin(); + maybeJoin(); }, [ dispatch ]); const goBack = useCallback(() => { @@ -85,7 +108,6 @@ const Prejoin: React.FC = ({ navigation }: IPrejoinProps) => { }, [ dispatch ]); const { PRIMARY, TERTIARY } = BUTTON_TYPES; - const joinButtonDisabled = !displayName && isDisplayNameMandatory; useEffect(() => { BackHandler.addEventListener('hardwareBackPress', goBack); @@ -158,25 +180,30 @@ const Prejoin: React.FC = ({ navigation }: IPrejoinProps) => { styles = { styles.buttonStylesBorderless } /> { - isDisplayNameVisible - && } + {showDisplayNameError && ( + }> + }>{t('prejoin.errorMissingName')} + )}