Some checks failed
Close stale issues and PRs / stale (push) Has been cancelled
534 lines
16 KiB
TypeScript
534 lines
16 KiB
TypeScript
/* eslint-disable react/jsx-no-bind */
|
|
import React, { useMemo, useState } from 'react';
|
|
import { useTranslation } from 'react-i18next';
|
|
import { connect, useDispatch } from 'react-redux';
|
|
import { makeStyles } from 'tss-react/mui';
|
|
|
|
import { IReduxState } from '../../../app/types';
|
|
import Avatar from '../../../base/avatar/components/Avatar';
|
|
import { isNameReadOnly } from '../../../base/config/functions.web';
|
|
import { IconArrowDown, IconArrowUp, IconPhoneRinging, IconVolumeOff } from '../../../base/icons/svg';
|
|
import { isVideoMutedByUser } from '../../../base/media/functions';
|
|
import { getLocalParticipant } from '../../../base/participants/functions';
|
|
import Popover from '../../../base/popover/components/Popover.web';
|
|
import ActionButton from '../../../base/premeeting/components/web/ActionButton';
|
|
import PreMeetingScreen from '../../../base/premeeting/components/web/PreMeetingScreen';
|
|
import { updateSettings } from '../../../base/settings/actions';
|
|
import { getDisplayName } from '../../../base/settings/functions.web';
|
|
import { getLocalJitsiVideoTrack } from '../../../base/tracks/functions.web';
|
|
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,
|
|
joinConferenceWithoutAudio as joinConferenceWithoutAudioAction,
|
|
setJoinByPhoneDialogVisiblity as setJoinByPhoneDialogVisiblityAction
|
|
} from '../../actions.web';
|
|
import {
|
|
isDeviceStatusVisible,
|
|
isDisplayNameRequired,
|
|
isJoinByPhoneButtonVisible,
|
|
isJoinByPhoneDialogVisible,
|
|
isPrejoinDisplayNameVisible
|
|
} from '../../functions';
|
|
import logger from '../../logger';
|
|
import { hasDisplayName } from '../../utils';
|
|
|
|
import JoinByPhoneDialog from './dialogs/JoinByPhoneDialog';
|
|
|
|
interface IProps {
|
|
|
|
/**
|
|
* Flag signaling if the device status is visible or not.
|
|
*/
|
|
deviceStatusVisible: boolean;
|
|
|
|
/**
|
|
* If join by phone button should be visible.
|
|
*/
|
|
hasJoinByPhoneButton: boolean;
|
|
|
|
/**
|
|
* Flag signaling if the display name is visible or not.
|
|
*/
|
|
isDisplayNameVisible: boolean;
|
|
|
|
/**
|
|
* Joins the current meeting.
|
|
*/
|
|
joinConference: Function;
|
|
|
|
/**
|
|
* Joins the current meeting without audio.
|
|
*/
|
|
joinConferenceWithoutAudio: Function;
|
|
|
|
/**
|
|
* Whether conference join is in progress.
|
|
*/
|
|
joiningInProgress?: boolean;
|
|
|
|
/**
|
|
* The name of the user that is about to join.
|
|
*/
|
|
name: string;
|
|
|
|
/**
|
|
* Local participant id.
|
|
*/
|
|
participantId?: string;
|
|
|
|
/**
|
|
* The prejoin config.
|
|
*/
|
|
prejoinConfig?: any;
|
|
|
|
/**
|
|
* Whether the name input should be read only or not.
|
|
*/
|
|
readOnlyName: boolean;
|
|
|
|
/**
|
|
* Sets visibility of the 'JoinByPhoneDialog'.
|
|
*/
|
|
setJoinByPhoneDialogVisiblity: Function;
|
|
|
|
/**
|
|
* Flag signaling the visibility of camera preview.
|
|
*/
|
|
showCameraPreview: boolean;
|
|
|
|
/**
|
|
* If 'JoinByPhoneDialog' is visible or not.
|
|
*/
|
|
showDialog: boolean;
|
|
|
|
/**
|
|
* If should show an error when joining without a name.
|
|
*/
|
|
showErrorOnJoin: boolean;
|
|
|
|
/**
|
|
* If the recording warning is visible or not.
|
|
*/
|
|
showRecordingWarning: boolean;
|
|
|
|
/**
|
|
* If should show unsafe room warning when joining.
|
|
*/
|
|
showUnsafeRoomWarning: boolean;
|
|
|
|
/**
|
|
* Whether the user has approved to join a room with unsafe name.
|
|
*/
|
|
unsafeRoomConsent?: boolean;
|
|
|
|
/**
|
|
* Updates settings.
|
|
*/
|
|
updateSettings: Function;
|
|
|
|
/**
|
|
* The JitsiLocalTrack to display.
|
|
*/
|
|
videoTrack?: Object;
|
|
}
|
|
|
|
const useStyles = makeStyles()(theme => {
|
|
return {
|
|
inputContainer: {
|
|
width: '100%'
|
|
},
|
|
|
|
input: {
|
|
width: '100%',
|
|
marginBottom: theme.spacing(3),
|
|
|
|
'& input': {
|
|
textAlign: 'center'
|
|
}
|
|
},
|
|
|
|
avatarContainer: {
|
|
display: 'flex',
|
|
alignItems: 'center',
|
|
flexDirection: 'column'
|
|
},
|
|
|
|
avatar: {
|
|
margin: `${theme.spacing(2)} auto ${theme.spacing(3)}`
|
|
},
|
|
|
|
avatarName: {
|
|
...theme.typography.bodyShortBoldLarge,
|
|
color: theme.palette.text01,
|
|
marginBottom: theme.spacing(5),
|
|
textAlign: 'center'
|
|
},
|
|
|
|
error: {
|
|
backgroundColor: theme.palette.actionDanger,
|
|
color: theme.palette.text01,
|
|
borderRadius: theme.shape.borderRadius,
|
|
width: '100%',
|
|
...theme.typography.labelRegular,
|
|
boxSizing: 'border-box',
|
|
padding: theme.spacing(1),
|
|
textAlign: 'center',
|
|
marginTop: `-${theme.spacing(2)}`,
|
|
marginBottom: theme.spacing(3)
|
|
},
|
|
|
|
dropdownContainer: {
|
|
position: 'relative',
|
|
width: '100%'
|
|
},
|
|
|
|
dropdownButtons: {
|
|
width: '300px',
|
|
padding: '8px 0',
|
|
backgroundColor: theme.palette.action02,
|
|
color: theme.palette.text04,
|
|
borderRadius: theme.shape.borderRadius,
|
|
position: 'relative',
|
|
top: `-${theme.spacing(3)}`,
|
|
|
|
'@media (max-width: 511px)': {
|
|
margin: '0 auto',
|
|
top: 0
|
|
},
|
|
|
|
'@media (max-width: 420px)': {
|
|
top: 0,
|
|
width: 'calc(100% - 32px)'
|
|
}
|
|
}
|
|
};
|
|
});
|
|
|
|
const Prejoin = ({
|
|
deviceStatusVisible,
|
|
hasJoinByPhoneButton,
|
|
isDisplayNameVisible,
|
|
joinConference,
|
|
joinConferenceWithoutAudio,
|
|
joiningInProgress,
|
|
name,
|
|
participantId,
|
|
prejoinConfig,
|
|
readOnlyName,
|
|
setJoinByPhoneDialogVisiblity,
|
|
showCameraPreview,
|
|
showDialog,
|
|
showErrorOnJoin,
|
|
showRecordingWarning,
|
|
showUnsafeRoomWarning,
|
|
unsafeRoomConsent,
|
|
updateSettings: dispatchUpdateSettings,
|
|
videoTrack
|
|
}: IProps) => {
|
|
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.
|
|
*
|
|
* @param {Object} e - The synthetic event.
|
|
* @returns {void}
|
|
*/
|
|
const onJoinButtonClick = () => {
|
|
if (showErrorOnJoin) {
|
|
dispatch(openDisplayNamePrompt({
|
|
onPostSubmit: joinConference,
|
|
validateInput: hasDisplayName
|
|
}));
|
|
|
|
return;
|
|
}
|
|
|
|
logger.info('Prejoin join button clicked.');
|
|
|
|
joinConference();
|
|
};
|
|
|
|
/**
|
|
* Closes the dropdown.
|
|
*
|
|
* @returns {void}
|
|
*/
|
|
const onDropdownClose = () => {
|
|
setShowJoinByPhoneButtons(false);
|
|
};
|
|
|
|
/**
|
|
* Displays the join by phone buttons dropdown.
|
|
*
|
|
* @param {Object} e - The synthetic event.
|
|
* @returns {void}
|
|
*/
|
|
const onOptionsClick = (e?: React.KeyboardEvent | React.MouseEvent | undefined) => {
|
|
e?.stopPropagation();
|
|
|
|
setShowJoinByPhoneButtons(show => !show);
|
|
};
|
|
|
|
/**
|
|
* Sets the guest participant name.
|
|
*
|
|
* @param {string} displayName - Participant name.
|
|
* @returns {void}
|
|
*/
|
|
const setName = (displayName: string) => {
|
|
dispatchUpdateSettings({
|
|
displayName
|
|
});
|
|
};
|
|
|
|
/**
|
|
* Closes the join by phone dialog.
|
|
*
|
|
* @returns {undefined}
|
|
*/
|
|
const closeDialog = () => {
|
|
setJoinByPhoneDialogVisiblity(false);
|
|
};
|
|
|
|
/**
|
|
* Displays the dialog for joining a meeting by phone.
|
|
*
|
|
* @returns {undefined}
|
|
*/
|
|
const doShowDialog = () => {
|
|
setJoinByPhoneDialogVisiblity(true);
|
|
onDropdownClose();
|
|
};
|
|
|
|
/**
|
|
* KeyPress handler for accessibility.
|
|
*
|
|
* @param {Object} e - The key event to handle.
|
|
*
|
|
* @returns {void}
|
|
*/
|
|
const showDialogKeyPress = (e: React.KeyboardEvent) => {
|
|
if (e.key === ' ' || e.key === 'Enter') {
|
|
e.preventDefault();
|
|
doShowDialog();
|
|
}
|
|
};
|
|
|
|
/**
|
|
* KeyPress handler for accessibility.
|
|
*
|
|
* @param {Object} e - The key event to handle.
|
|
*
|
|
* @returns {void}
|
|
*/
|
|
const onJoinConferenceWithoutAudioKeyPress = (e: React.KeyboardEvent) => {
|
|
if (joinConferenceWithoutAudio
|
|
&& (e.key === ' '
|
|
|| e.key === 'Enter')) {
|
|
e.preventDefault();
|
|
logger.info('Prejoin joinConferenceWithoutAudio dispatched on a key pressed.');
|
|
joinConferenceWithoutAudio();
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Gets the list of extra join buttons.
|
|
*
|
|
* @returns {Object} - The list of extra buttons.
|
|
*/
|
|
const getExtraJoinButtons = () => {
|
|
const noAudio = {
|
|
key: 'no-audio',
|
|
testId: 'prejoin.joinWithoutAudio',
|
|
icon: IconVolumeOff,
|
|
label: t('prejoin.joinWithoutAudio'),
|
|
onClick: () => {
|
|
logger.info('Prejoin join conference without audio pressed.');
|
|
joinConferenceWithoutAudio();
|
|
},
|
|
onKeyPress: onJoinConferenceWithoutAudioKeyPress
|
|
};
|
|
|
|
const byPhone = {
|
|
key: 'by-phone',
|
|
testId: 'prejoin.joinByPhone',
|
|
icon: IconPhoneRinging,
|
|
label: t('prejoin.joinAudioByPhone'),
|
|
onClick: doShowDialog,
|
|
onKeyPress: showDialogKeyPress
|
|
};
|
|
|
|
return {
|
|
noAudio,
|
|
byPhone
|
|
};
|
|
};
|
|
|
|
/**
|
|
* Handle keypress on input.
|
|
*
|
|
* @param {KeyboardEvent} e - Keyboard event.
|
|
* @returns {void}
|
|
*/
|
|
const onInputKeyPress = (e: React.KeyboardEvent) => {
|
|
if (e.key === 'Enter') {
|
|
logger.info('Dispatching join conference on Enter key press from the prejoin screen.');
|
|
joinConference();
|
|
}
|
|
};
|
|
|
|
const extraJoinButtons = getExtraJoinButtons();
|
|
let extraButtonsToRender = Object.values(extraJoinButtons).filter((val: any) =>
|
|
!(prejoinConfig?.hideExtraJoinButtons || []).includes(val.key)
|
|
);
|
|
|
|
if (!hasJoinByPhoneButton) {
|
|
extraButtonsToRender = extraButtonsToRender.filter((btn: any) => btn.key !== 'by-phone');
|
|
}
|
|
const hasExtraJoinButtons = Boolean(extraButtonsToRender.length);
|
|
|
|
return (
|
|
<PreMeetingScreen
|
|
showDeviceStatus = { deviceStatusVisible }
|
|
showRecordingWarning = { showRecordingWarning }
|
|
showUnsafeRoomWarning = { showUnsafeRoomWarning }
|
|
title = { t('prejoin.joinMeeting') }
|
|
videoMuted = { !showCameraPreview }
|
|
videoTrack = { videoTrack }>
|
|
<div
|
|
className = { classes.inputContainer }
|
|
data-testid = 'prejoin.screen'>
|
|
{showDisplayNameField ? (<Input
|
|
accessibilityLabel = { t('dialog.enterDisplayName') }
|
|
autoComplete = { 'name' }
|
|
autoFocus = { true }
|
|
className = { classes.input }
|
|
error = { showErrorOnField }
|
|
id = 'premeeting-name-input'
|
|
onChange = { setName }
|
|
onKeyPress = { showUnsafeRoomWarning && !unsafeRoomConsent ? undefined : onInputKeyPress }
|
|
placeholder = { t('dialog.enterDisplayName') }
|
|
readOnly = { readOnlyName }
|
|
value = { name } />
|
|
) : (
|
|
<div className = { classes.avatarContainer }>
|
|
<Avatar
|
|
className = { classes.avatar }
|
|
displayName = { name }
|
|
participantId = { participantId }
|
|
size = { 72 } />
|
|
{isDisplayNameVisible && <div className = { classes.avatarName }>{name}</div>}
|
|
</div>
|
|
)}
|
|
|
|
{showErrorOnField && <div
|
|
className = { classes.error }
|
|
data-testid = 'prejoin.errorMessage'>
|
|
<p aria-live = 'polite' >
|
|
{t('prejoin.errorMissingName')}
|
|
</p>
|
|
</div>}
|
|
|
|
<div className = { classes.dropdownContainer }>
|
|
<Popover
|
|
content = { hasExtraJoinButtons && <div className = { classes.dropdownButtons }>
|
|
{extraButtonsToRender.map(({ key, ...rest }) => (
|
|
<Button
|
|
disabled = { joiningInProgress || showErrorOnField }
|
|
fullWidth = { true }
|
|
key = { key }
|
|
type = { BUTTON_TYPES.SECONDARY }
|
|
{ ...rest } />
|
|
))}
|
|
</div> }
|
|
onPopoverClose = { onDropdownClose }
|
|
position = 'bottom'
|
|
trigger = 'click'
|
|
visible = { showJoinByPhoneButtons }>
|
|
<ActionButton
|
|
OptionsIcon = { showJoinByPhoneButtons ? IconArrowUp : IconArrowDown }
|
|
ariaDropDownLabel = { t('prejoin.joinWithoutAudio') }
|
|
ariaLabel = { t('prejoin.joinMeeting') }
|
|
ariaPressed = { showJoinByPhoneButtons }
|
|
disabled = { joiningInProgress
|
|
|| (showUnsafeRoomWarning && !unsafeRoomConsent)
|
|
|| showErrorOnField }
|
|
hasOptions = { hasExtraJoinButtons }
|
|
onClick = { onJoinButtonClick }
|
|
onOptionsClick = { onOptionsClick }
|
|
role = 'button'
|
|
tabIndex = { 0 }
|
|
testId = 'prejoin.joinMeeting'
|
|
type = 'primary'>
|
|
{t('prejoin.joinMeeting')}
|
|
</ActionButton>
|
|
</Popover>
|
|
</div>
|
|
</div>
|
|
{showDialog && (
|
|
<JoinByPhoneDialog
|
|
joinConferenceWithoutAudio = { joinConferenceWithoutAudio }
|
|
onClose = { closeDialog } />
|
|
)}
|
|
</PreMeetingScreen>
|
|
);
|
|
};
|
|
|
|
|
|
/**
|
|
* Maps (parts of) the redux state to the React {@code Component} props.
|
|
*
|
|
* @param {Object} state - The redux state.
|
|
* @returns {Object}
|
|
*/
|
|
function mapStateToProps(state: IReduxState) {
|
|
const name = getDisplayName(state);
|
|
const showErrorOnJoin = isDisplayNameRequired(state) && !name;
|
|
const { id: participantId } = getLocalParticipant(state) ?? {};
|
|
const { joiningInProgress } = state['features/prejoin'];
|
|
const { room } = state['features/base/conference'];
|
|
const { unsafeRoomConsent } = state['features/base/premeeting'];
|
|
const { showPrejoinWarning: showRecordingWarning } = state['features/base/config'].recordings ?? {};
|
|
|
|
return {
|
|
deviceStatusVisible: isDeviceStatusVisible(state),
|
|
hasJoinByPhoneButton: isJoinByPhoneButtonVisible(state),
|
|
isDisplayNameVisible: isPrejoinDisplayNameVisible(state),
|
|
joiningInProgress,
|
|
name,
|
|
participantId,
|
|
prejoinConfig: state['features/base/config'].prejoinConfig,
|
|
readOnlyName: isNameReadOnly(state),
|
|
showCameraPreview: !isVideoMutedByUser(state),
|
|
showDialog: isJoinByPhoneDialogVisible(state),
|
|
showErrorOnJoin,
|
|
showRecordingWarning: Boolean(showRecordingWarning),
|
|
showUnsafeRoomWarning: isInsecureRoomName(room) && isUnsafeRoomWarningEnabled(state),
|
|
unsafeRoomConsent,
|
|
videoTrack: getLocalJitsiVideoTrack(state)
|
|
};
|
|
}
|
|
|
|
const mapDispatchToProps = {
|
|
joinConferenceWithoutAudio: joinConferenceWithoutAudioAction,
|
|
joinConference: joinConferenceAction,
|
|
setJoinByPhoneDialogVisiblity: setJoinByPhoneDialogVisiblityAction,
|
|
updateSettings
|
|
};
|
|
|
|
export default connect(mapStateToProps, mapDispatchToProps)(Prejoin);
|