/* eslint-disable react/jsx-no-bind */ import React, { useRef, useState } from 'react'; import { useTranslation } from 'react-i18next'; import { connect } 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 { withPixelLineHeight } from '../../../base/styles/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 { joinConference as joinConferenceAction, joinConferenceWithoutAudio as joinConferenceWithoutAudioAction, setJoinByPhoneDialogVisiblity as setJoinByPhoneDialogVisiblityAction } from '../../actions.web'; import { isDeviceStatusVisible, isDisplayNameRequired, isJoinByPhoneButtonVisible, isJoinByPhoneDialogVisible, isPrejoinDisplayNameVisible } from '../../functions'; 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. */ deviceStatusVisible: boolean; /** * If join by phone button should be visible. */ hasJoinByPhoneButton: 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 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: { ...withPixelLineHeight(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%', ...withPixelLineHeight(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)}` } }; }); const Prejoin = ({ canEditDisplayName, deviceStatusVisible, hasJoinByPhoneButton, joinConference, joinConferenceWithoutAudio, joiningInProgress, name, participantId, prejoinConfig, readOnlyName, setJoinByPhoneDialogVisiblity, showCameraPreview, showDialog, showErrorOnJoin, showUnsafeRoomWarning, unsafeRoomConsent, updateSettings: dispatchUpdateSettings, videoTrack }: IProps) => { const showDisplayNameField = useRef(canEditDisplayName || showErrorOnJoin); const [ showJoinByPhoneButtons, setShowJoinByPhoneButtons ] = useState(false); const { classes } = useStyles(); const { t } = useTranslation(); /** * Handler for the join button. * * @param {Object} e - The synthetic event. * @returns {void} */ const onJoinButtonClick = () => { if (showErrorOnJoin) { return; } 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(); 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: 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') { 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 (
{showDisplayNameField.current ? ( ) : (
{name}
)} {showErrorOnJoin &&
{t('prejoin.errorMissingName')}
}
{extraButtonsToRender.map(({ key, ...rest }) => (
} onPopoverClose = { onDropdownClose } position = 'bottom' trigger = 'click' visible = { showJoinByPhoneButtons }> {t('prejoin.joinMeeting')}
{showDialog && ( )}
); }; /** * 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 { enableInsecureRoomNameWarning = false } = state['features/base/config']; const { unsafeRoomConsent } = state['features/base/premeeting']; return { canEditDisplayName: isPrejoinDisplayNameVisible(state), deviceStatusVisible: isDeviceStatusVisible(state), hasJoinByPhoneButton: isJoinByPhoneButtonVisible(state), joiningInProgress, name, participantId, prejoinConfig: state['features/base/config'].prejoinConfig, readOnlyName: isNameReadOnly(state), showCameraPreview: !isVideoMutedByUser(state), showDialog: isJoinByPhoneDialogVisible(state), showErrorOnJoin, showUnsafeRoomWarning: isInsecureRoomName(room) && enableInsecureRoomNameWarning, unsafeRoomConsent, videoTrack: getLocalJitsiVideoTrack(state) }; } const mapDispatchToProps = { joinConferenceWithoutAudio: joinConferenceWithoutAudioAction, joinConference: joinConferenceAction, setJoinByPhoneDialogVisiblity: setJoinByPhoneDialogVisiblityAction, updateSettings }; export default connect(mapStateToProps, mapDispatchToProps)(Prejoin);