import React, { useEffect, useState } from 'react'; import PropTypes from 'prop-types'; import { defineMessages, injectIntl, FormattedMessage, } from 'react-intl'; import { useMutation } from '@apollo/client'; import Styled from './styles'; import PermissionsOverlay from '../permissions-overlay/component'; import AudioSettings from '../audio-settings/component'; import EchoTest from '../echo-test/component'; import Help from '../help/component'; import AudioDial from '../audio-dial/component'; import AudioAutoplayPrompt from '../autoplay/component'; import { getSettingsSingletonInstance } from '/imports/ui/services/settings'; import usePreviousValue from '/imports/ui/hooks/usePreviousValue'; import { SET_AWAY } from '/imports/ui/components/user-list/user-list-content/user-participants/user-list-participants/user-actions/mutations'; import VideoService from '/imports/ui/components/video-provider/video-provider-graphql/service'; import AudioCaptionsSelectContainer from '../audio-graphql/audio-captions/captions/component'; import useToggleVoice from '/imports/ui/components/audio/audio-graphql/hooks/useToggleVoice'; import { muteAway, } from '/imports/ui/components/audio/audio-graphql/audio-controls/input-stream-live-selector/service'; import Session from '/imports/ui/services/storage/in-memory'; const propTypes = { intl: PropTypes.shape({ formatMessage: PropTypes.func.isRequired, }).isRequired, closeModal: PropTypes.func.isRequired, joinMicrophone: PropTypes.func.isRequired, joinListenOnly: PropTypes.func.isRequired, joinEchoTest: PropTypes.func.isRequired, exitAudio: PropTypes.func.isRequired, leaveEchoTest: PropTypes.func.isRequired, changeInputDevice: PropTypes.func.isRequired, changeOutputDevice: PropTypes.func.isRequired, isEchoTest: PropTypes.bool.isRequired, isConnecting: PropTypes.bool.isRequired, isConnected: PropTypes.bool.isRequired, isUsingAudio: PropTypes.bool.isRequired, isListenOnly: PropTypes.bool.isRequired, inputDeviceId: PropTypes.string, outputDeviceId: PropTypes.string, formattedDialNum: PropTypes.string.isRequired, showPermissionsOvelay: PropTypes.bool.isRequired, listenOnlyMode: PropTypes.bool.isRequired, joinFullAudioImmediately: PropTypes.bool, forceListenOnlyAttendee: PropTypes.bool.isRequired, audioLocked: PropTypes.bool.isRequired, resolve: PropTypes.func, isMobileNative: PropTypes.bool.isRequired, isIE: PropTypes.bool.isRequired, formattedTelVoice: PropTypes.string.isRequired, autoplayBlocked: PropTypes.bool.isRequired, handleAllowAutoplay: PropTypes.func.isRequired, changeInputStream: PropTypes.func.isRequired, localEchoEnabled: PropTypes.bool.isRequired, showVolumeMeter: PropTypes.bool.isRequired, notify: PropTypes.func.isRequired, isRTL: PropTypes.bool.isRequired, priority: PropTypes.string.isRequired, isOpen: PropTypes.bool.isRequired, setIsOpen: PropTypes.func.isRequired, AudioError: PropTypes.shape({ MIC_ERROR: PropTypes.number.isRequired, NO_SSL: PropTypes.number.isRequired, }).isRequired, getTroubleshootingLink: PropTypes.func.isRequired, away: PropTypes.bool, }; const intlMessages = defineMessages({ microphoneLabel: { id: 'app.audioModal.microphoneLabel', description: 'Join mic audio button label', }, listenOnlyLabel: { id: 'app.audioModal.listenOnlyLabel', description: 'Join listen only audio button label', }, listenOnlyDesc: { id: 'app.audioModal.listenOnlyDesc', description: 'Join listen only audio button description', }, microphoneDesc: { id: 'app.audioModal.microphoneDesc', description: 'Join mic audio button description', }, closeLabel: { id: 'app.audioModal.closeLabel', description: 'close audio modal button label', }, audioChoiceLabel: { id: 'app.audioModal.audioChoiceLabel', description: 'Join audio modal title', }, iOSError: { id: 'app.audioModal.iOSBrowser', description: 'Audio/Video Not supported warning', }, iOSErrorDescription: { id: 'app.audioModal.iOSErrorDescription', description: 'Audio/Video not supported description', }, iOSErrorRecommendation: { id: 'app.audioModal.iOSErrorRecommendation', description: 'Audio/Video recommended action', }, echoTestTitle: { id: 'app.audioModal.echoTestTitle', description: 'Title for the echo test', }, settingsTitle: { id: 'app.audioModal.settingsTitle', description: 'Title for the audio modal', }, helpTitle: { id: 'app.audioModal.helpTitle', description: 'Title for the audio help', }, audioDialTitle: { id: 'app.audioModal.audioDialTitle', description: 'Title for the audio dial', }, connecting: { id: 'app.audioModal.connecting', description: 'Message for audio connecting', }, ariaModalTitle: { id: 'app.audioModal.ariaTitle', description: 'aria label for modal title', }, autoplayPromptTitle: { id: 'app.audioModal.autoplayBlockedDesc', description: 'Message for autoplay audio block', }, }); const AudioModal = ({ forceListenOnlyAttendee, joinFullAudioImmediately = false, listenOnlyMode, audioLocked, isUsingAudio, isListenOnly, autoplayBlocked, closeModal, isEchoTest, exitAudio, resolve = null, leaveEchoTest, AudioError, joinEchoTest, isConnecting, localEchoEnabled, joinListenOnly, changeInputStream, joinMicrophone, intl, isMobileNative, formattedDialNum, isRTL, isConnected, inputDeviceId = null, outputDeviceId = null, changeInputDevice, changeOutputDevice, showVolumeMeter, notify, formattedTelVoice, handleAllowAutoplay, showPermissionsOvelay, isIE, isOpen, priority, setIsOpen, getTroubleshootingLink, away = false, }) => { const [content, setContent] = useState(null); const [hasError, setHasError] = useState(false); const [disableActions, setDisableActions] = useState(false); const [errorInfo, setErrorInfo] = useState(null); const [autoplayChecked, setAutoplayChecked] = useState(false); const [setAway] = useMutation(SET_AWAY); const voiceToggle = useToggleVoice(); const prevAutoplayBlocked = usePreviousValue(autoplayBlocked); useEffect(() => { if (prevAutoplayBlocked && !autoplayBlocked) { setAutoplayChecked(true); } }, [autoplayBlocked]); const handleJoinAudioError = (err) => { const { type, errCode, errMessage } = err; switch (type) { case 'MEDIA_ERROR': setContent('help'); setErrorInfo({ errCode, errMessage, }); setDisableActions(false); break; case 'CONNECTION_ERROR': default: setErrorInfo({ errCode, errMessage: type, }); setDisableActions(false); break; } }; const handleGoToLocalEcho = () => { // Simplified echo test: this will return the AudioSettings with: // - withEcho: true // Echo test will be local and done in the AudioSettings view instead of the // old E2E -> yes/no -> join view setContent('settings'); }; const handleGoToEchoTest = () => { const { MIC_ERROR } = AudioError; const noSSL = !window.location.protocol.includes('https'); if (noSSL) { setContent('help'); setErrorInfo({ errCode: MIC_ERROR.NO_SSL, errMessage: 'NoSSL', }); return null; } if (disableActions && isConnecting) return null; if (localEchoEnabled) return handleGoToLocalEcho(); setHasError(false); setDisableActions(true); return joinEchoTest().then(() => { setContent('echoTest'); setDisableActions(true); }).catch((err) => { handleJoinAudioError(err); }); }; const handleGoToAudioOptions = () => { setContent(null); setHasError(true); setDisableActions(false); }; const handleGoToAudioSettings = () => { leaveEchoTest().then(() => { setContent('settings'); }); }; const handleRetryGoToEchoTest = () => { setHasError(false); setContent(null); setErrorInfo(null); return handleGoToEchoTest(); }; const disableAwayMode = () => { if (!away) return; muteAway(false, true, voiceToggle); setAway({ variables: { away: false, }, }); VideoService.setTrackEnabled(true); }; const handleJoinListenOnly = () => { if (disableActions && isConnecting) return null; setDisableActions(true); setHasError(false); setErrorInfo(null); return joinListenOnly().then(() => { setDisableActions(false); disableAwayMode(); }).catch((err) => { handleJoinAudioError(err); }); }; const handleJoinMicrophone = () => { if (disableActions && isConnecting) return; setHasError(false); setDisableActions(true); setErrorInfo(null); joinMicrophone().then(() => { setDisableActions(false); }).catch((err) => { handleJoinAudioError(err); }); }; const handleJoinLocalEcho = (inputStream) => { // Reset the modal to a connecting state - this kind of sucks? // prlanzarin Apr 04 2022 setContent(null); if (inputStream) changeInputStream(inputStream); handleJoinMicrophone(); disableAwayMode(); }; const skipAudioOptions = () => (isConnecting || (forceListenOnlyAttendee && !autoplayChecked)) && !content && !hasError; const renderAudioOptions = () => { const hideMicrophone = forceListenOnlyAttendee || audioLocked; const arrow = isRTL ? '←' : '→'; const dialAudioLabel = `${intl.formatMessage(intlMessages.audioDialTitle)} ${arrow}`; return (
{!hideMicrophone && !isMobileNative && ( <> {intl.formatMessage(intlMessages.microphoneDesc)} )} {listenOnlyMode && ( <> {intl.formatMessage(intlMessages.listenOnlyDesc)} )} {formattedDialNum ? ( { setContent('audioDial'); }} /> ) : null}
); }; const renderEchoTest = () => ( ); const renderAudioSettings = () => { const confirmationCallback = !localEchoEnabled ? handleRetryGoToEchoTest : handleJoinLocalEcho; const handleGUMFailure = (error) => { const code = error?.name === 'NotAllowedError' ? AudioError.MIC_ERROR.NO_PERMISSION : 0; setContent('help'); setErrorInfo({ errCode: code, errMessage: error?.name || 'NotAllowedError', }); setDisableActions(false); }; return ( ); }; const renderHelp = () => { const audioErr = { ...AudioError, code: errorInfo?.errCode, message: errorInfo?.errMessage, }; return ( ); }; const renderAudioDial = () => ( ); const renderAutoplayOverlay = () => ( ); const contents = { echoTest: { title: intlMessages.echoTestTitle, component: renderEchoTest, }, settings: { title: intlMessages.settingsTitle, component: renderAudioSettings, }, help: { title: intlMessages.helpTitle, component: renderHelp, }, audioDial: { title: intlMessages.audioDialTitle, component: renderAudioDial, }, autoplayBlocked: { title: intlMessages.autoplayPromptTitle, component: renderAutoplayOverlay, }, }; const renderContent = () => { const { animations } = getSettingsSingletonInstance().application; if (skipAudioOptions()) { return ( {intl.formatMessage(intlMessages.connecting)} ); } return content ? contents[content].component() : renderAudioOptions(); }; useEffect(() => { if (!isUsingAudio) { if (forceListenOnlyAttendee || audioLocked) { handleJoinListenOnly(); return; } if (joinFullAudioImmediately && !listenOnlyMode) { handleJoinMicrophone(); return; } if (!listenOnlyMode) { handleGoToEchoTest(); } } }, []); useEffect(() => { if (autoplayBlocked) { setContent('autoplayBlocked'); } else if (prevAutoplayBlocked) { closeModal(); } }, [autoplayBlocked]); useEffect(() => () => { if (isEchoTest) { exitAudio(); } if (resolve) resolve(); Session.setItem('audioModalIsOpen', false); }, []); let title = content ? intl.formatMessage(contents[content].title) : intl.formatMessage(intlMessages.audioChoiceLabel); title = !skipAudioOptions() ? title : null; return ( <> {showPermissionsOvelay ? : null} {isIE ? ( Chrome, 1: Firefox, }} /> ) : null} {renderContent()} ); }; AudioModal.propTypes = propTypes; export default injectIntl(AudioModal);