fix(prejoin) improve display name handling relative to configs (#13865)

This commit is contained in:
Mihaela Dumitru
2023-09-29 16:17:35 +03:00
committed by GitHub
parent 7bf3e7df1d
commit b546d01c2d
12 changed files with 167 additions and 40 deletions

View File

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

View File

@@ -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<IProps, IState> {
this.state = {
fieldValue: props.initialValue,
isValid: props.validateInput ? props.validateInput(props.initialValue) : true,
submitting: false
};
@@ -115,10 +126,11 @@ class InputDialog extends AbstractDialog<IProps, IState> {
</Dialog.Description>
)
}
<Dialog.Button
{!this.props.disableCancel && <Dialog.Button
label = { t('dialog.Cancel') }
onPress = { this._onCancel } />
onPress = { this._onCancel } />}
<Dialog.Button
disabled = { !this.state.isValid }
label = { t('dialog.Ok') }
onPress = { this._onSubmitValue } />
</Dialog.Container>
@@ -132,10 +144,14 @@ class InputDialog extends AbstractDialog<IProps, IState> {
* @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
});

View File

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

View File

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

View File

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

View File

@@ -33,8 +33,10 @@ class DisplayNamePrompt extends Component<IProps> {
return (
<InputDialog
descriptionKey = 'dialog.enterDisplayName'
disableCancel = { true }
onSubmit = { this._onSetDisplayName }
titleKey = 'dialog.displayNameRequired' />
titleKey = 'dialog.displayNameRequired'
validateInput = { this.props.validateInput } />
);
}
}

View File

@@ -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<IProps, IState> {
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<IProps, IState> {
* @returns {ReactElement}
*/
render() {
const disableCloseDialog = Boolean(this.props.validateInput);
return (
<Dialog
cancel = {{ translationKey: 'dialog.Cancel' }}
ok = {{ translationKey: 'dialog.Ok' }}
cancel = {{ hidden: true }}
disableBackdropClose = { disableCloseDialog }
disableEnter = { !this.state.isValid }
disableEscape = { disableCloseDialog }
hideCloseButton = { disableCloseDialog }
ok = {{
disabled: !this.state.isValid,
translationKey: 'dialog.Ok'
}}
onSubmit = { this._onSubmit }
titleKey = 'dialog.displayNameRequired'>
<Input
@@ -80,6 +97,14 @@ class DisplayNamePrompt extends Component<IProps, IState> {
* @returns {void}
*/
_onDisplayNameChange(value: string) {
if (this.props.validateInput) {
this.setState({
isValid: this.props.validateInput(value),
displayName: value
});
return;
}
this.setState({
displayName: value
});

View File

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

View File

@@ -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<IPrejoinProps> = ({ 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<IPrejoinProps> = ({ 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<IPrejoinProps> = ({ 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<IPrejoinProps> = ({ navigation }: IPrejoinProps) => {
styles = { styles.buttonStylesBorderless } />
</View>
{
isDisplayNameVisible
&& <Input
showDisplayNameInput && <Input
customStyles = {{ input: styles.customInput }}
disabled = { isDisplayNameReadonly }
error = { showDisplayNameError }
onChange = { onChangeDisplayName }
placeholder = { t('dialog.enterDisplayName') }
value = { displayName } />
}
{showDisplayNameError && (
<View style = { styles.errorContainer as StyleProp<TextStyle> }>
<Text style = { styles.error as StyleProp<TextStyle> }>{t('prejoin.errorMissingName')}</Text>
</View>)}
<Button
accessibilityLabel = 'prejoin.joinMeeting'
disabled = { joinButtonDisabled }
disabled = { showDisplayNameError }
labelKey = 'prejoin.joinMeeting'
onClick = { isJoining ? undefined : onJoin }
onClick = { isJoining ? undefined : maybeJoin }
style = { styles.joinButton }
type = { PRIMARY } />
<Button
accessibilityLabel = 'prejoin.joinMeetingInLowBandwidthMode'
disabled = { joinButtonDisabled }
disabled = { showDisplayNameError }
labelKey = 'prejoin.joinMeetingInLowBandwidthMode'
onClick = { onJoinLowBandwidth }
onClick = { isJoining ? undefined : onJoinLowBandwidth }
style = { styles.joinButton }
type = { TERTIARY } />
</View>

View File

@@ -83,6 +83,23 @@ export const preJoinStyles = {
width: 352
},
errorContainer: {
backgroundColor: BaseTheme.palette.actionDanger,
borderBottomRightRadius: BaseTheme.shape.borderRadius,
borderBottomLeftRadius: BaseTheme.shape.borderRadius,
boxSizing: 'border-box',
marginTop: -BaseTheme.spacing[2],
overflow: 'visible',
wordBreak: 'normal',
width: 352
},
error: {
padding: BaseTheme.spacing[1],
color: BaseTheme.palette.text01,
textAlign: 'center'
},
preJoinRoomName: {
...BaseTheme.typography.heading5,
color: BaseTheme.palette.text01,

View File

@@ -1,7 +1,7 @@
/* eslint-disable react/jsx-no-bind */
import React, { useRef, useState } from 'react';
import React, { useMemo, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { connect } from 'react-redux';
import { connect, useDispatch } from 'react-redux';
import { makeStyles } from 'tss-react/mui';
import { IReduxState } from '../../../app/types';
@@ -21,6 +21,7 @@ import Button from '../../../base/ui/components/web/Button';
import Input from '../../../base/ui/components/web/Input';
import { BUTTON_TYPES } from '../../../base/ui/constants.any';
import isInsecureRoomName from '../../../base/util/isInsecureRoomName';
import { openDisplayNamePrompt } from '../../../display-name/actions';
import { isUnsafeRoomWarningEnabled } from '../../../prejoin/functions';
import {
joinConference as joinConferenceAction,
@@ -34,16 +35,12 @@ import {
isJoinByPhoneDialogVisible,
isPrejoinDisplayNameVisible
} from '../../functions';
import { hasDisplayName } from '../../utils';
import JoinByPhoneDialog from './dialogs/JoinByPhoneDialog';
interface IProps {
/**
* Indicates whether the display name is editable.
*/
canEditDisplayName: boolean;
/**
* Flag signaling if the device status is visible or not.
*/
@@ -54,6 +51,11 @@ interface IProps {
*/
hasJoinByPhoneButton: boolean;
/**
* Flag signaling if the display name is visible or not.
*/
isDisplayNameVisible: boolean;
/**
* Joins the current meeting.
*/
@@ -193,9 +195,9 @@ const useStyles = makeStyles()(theme => {
});
const Prejoin = ({
canEditDisplayName,
deviceStatusVisible,
hasJoinByPhoneButton,
isDisplayNameVisible,
joinConference,
joinConferenceWithoutAudio,
joiningInProgress,
@@ -212,10 +214,16 @@ const Prejoin = ({
updateSettings: dispatchUpdateSettings,
videoTrack
}: IProps) => {
const showDisplayNameField = useRef(canEditDisplayName || showErrorOnJoin);
const showDisplayNameField = useMemo(
() => isDisplayNameVisible && !readOnlyName,
[ isDisplayNameVisible, readOnlyName ]);
const showErrorOnField = useMemo(
() => showDisplayNameField && showErrorOnJoin,
[ showDisplayNameField, showErrorOnJoin ]);
const [ showJoinByPhoneButtons, setShowJoinByPhoneButtons ] = useState(false);
const { classes } = useStyles();
const { t } = useTranslation();
const dispatch = useDispatch();
/**
* Handler for the join button.
@@ -225,6 +233,11 @@ const Prejoin = ({
*/
const onJoinButtonClick = () => {
if (showErrorOnJoin) {
dispatch(openDisplayNamePrompt({
onPostSubmit: joinConference,
validateInput: hasDisplayName
}));
return;
}
joinConference();
@@ -374,12 +387,12 @@ const Prejoin = ({
<div
className = { classes.inputContainer }
data-testid = 'prejoin.screen'>
{showDisplayNameField.current ? (<Input
{showDisplayNameField ? (<Input
accessibilityLabel = { t('dialog.enterDisplayName') }
autoComplete = { 'name' }
autoFocus = { true }
className = { classes.input }
error = { showErrorOnJoin }
error = { showErrorOnField }
id = 'premeeting-name-input'
onChange = { setName }
onKeyPress = { showUnsafeRoomWarning && !unsafeRoomConsent ? undefined : onInputKeyPress }
@@ -393,11 +406,11 @@ const Prejoin = ({
displayName = { name }
participantId = { participantId }
size = { 72 } />
<div className = { classes.avatarName }>{name}</div>
{isDisplayNameVisible && <div className = { classes.avatarName }>{name}</div>}
</div>
)}
{showErrorOnJoin && <div
{showErrorOnField && <div
className = { classes.error }
data-testid = 'prejoin.errorMessage'>{t('prejoin.errorMissingName')}</div>}
@@ -406,7 +419,7 @@ const Prejoin = ({
content = { hasExtraJoinButtons && <div className = { classes.dropdownButtons }>
{extraButtonsToRender.map(({ key, ...rest }) => (
<Button
disabled = { joiningInProgress }
disabled = { joiningInProgress || showErrorOnField }
fullWidth = { true }
key = { key }
type = { BUTTON_TYPES.SECONDARY }
@@ -422,7 +435,9 @@ const Prejoin = ({
ariaDropDownLabel = { t('prejoin.joinWithoutAudio') }
ariaLabel = { t('prejoin.joinMeeting') }
ariaPressed = { showJoinByPhoneButtons }
disabled = { joiningInProgress || (showUnsafeRoomWarning && !unsafeRoomConsent) }
disabled = { joiningInProgress
|| (showUnsafeRoomWarning && !unsafeRoomConsent)
|| showErrorOnField }
hasOptions = { hasExtraJoinButtons }
onClick = { onJoinButtonClick }
onOptionsClick = { onOptionsClick }
@@ -460,9 +475,9 @@ function mapStateToProps(state: IReduxState) {
const { unsafeRoomConsent } = state['features/base/premeeting'];
return {
canEditDisplayName: isPrejoinDisplayNameVisible(state),
deviceStatusVisible: isDeviceStatusVisible(state),
hasJoinByPhoneButton: isJoinByPhoneButtonVisible(state),
isDisplayNameVisible: isPrejoinDisplayNameVisible(state),
joiningInProgress,
name,
participantId,

View File

@@ -795,3 +795,11 @@ export function getCountryFromDialCodeText(text: string) {
|| null
);
}
/**
* Returns whether the display name is present.
*
* @param {string} value - The display name.
* @returns {boolean}
*/
export const hasDisplayName = (value: string): boolean => Boolean(value) && value.trim() !== '';