diff --git a/bigbluebutton-html5/imports/ui/components/audio/audio-modal/component.jsx b/bigbluebutton-html5/imports/ui/components/audio/audio-modal/component.jsx index d32f988e43..8afc079537 100755 --- a/bigbluebutton-html5/imports/ui/components/audio/audio-modal/component.jsx +++ b/bigbluebutton-html5/imports/ui/components/audio/audio-modal/component.jsx @@ -132,6 +132,7 @@ class AudioModal extends Component { content: null, hasError: false, errCode: null, + errMessage: null, }; this.handleGoToAudioOptions = this.handleGoToAudioOptions.bind(this); @@ -258,6 +259,7 @@ class AudioModal extends Component { return this.setState({ content: 'help', errCode: MIC_ERROR.NO_SSL, + errMessage: 'NoSSL', }); } @@ -363,6 +365,7 @@ class AudioModal extends Component { this.setState({ content: 'help', errCode: 0, + errMessage: type, disableActions: false, }); break; @@ -370,6 +373,7 @@ class AudioModal extends Component { default: this.setState({ errCode: 0, + errMessage: type, disableActions: false, }); break; @@ -514,16 +518,22 @@ class AudioModal extends Component { localEchoEnabled, showVolumeMeter, notify, + AudioError, } = this.props; + const { MIC_ERROR } = AudioError; const confirmationCallback = !localEchoEnabled ? this.handleRetryGoToEchoTest : this.handleJoinLocalEcho; - const handleGUMFailure = () => { + const handleGUMFailure = (error) => { + const errCode = error?.name === 'NotAllowedError' + ? MIC_ERROR.NO_PERMISSION + : 0 this.setState({ content: 'help', - errCode: 0, + errCode, + errMessage: error?.name || 'GUMFailure', disableActions: false, }); }; @@ -550,18 +560,20 @@ class AudioModal extends Component { } renderHelp() { - const { errCode } = this.state; - const { AudioError } = this.props; + const { errCode, errMessage } = this.state; + const { AudioError, getTroubleshootingLink } = this.props; const audioErr = { ...AudioError, code: errCode, + message: errMessage, }; return ( ); } diff --git a/bigbluebutton-html5/imports/ui/components/audio/audio-modal/container.jsx b/bigbluebutton-html5/imports/ui/components/audio/audio-modal/container.jsx index d61c8e20f8..6e89657df1 100755 --- a/bigbluebutton-html5/imports/ui/components/audio/audio-modal/container.jsx +++ b/bigbluebutton-html5/imports/ui/components/audio/audio-modal/container.jsx @@ -16,6 +16,7 @@ import { } from './service'; import Storage from '/imports/ui/services/storage/session'; import Service from '../service'; +import AudioModalService from '/imports/ui/components/audio/audio-modal/service'; const AudioModalContainer = (props) => ; @@ -98,5 +99,6 @@ export default lockContextContainer(withTracker(({ userLocks, setIsOpen }) => { notify: Service.notify, isRTL, AudioError, + getTroubleshootingLink: AudioModalService.getTroubleshootingLink, }); })(AudioModalContainer)); diff --git a/bigbluebutton-html5/imports/ui/components/audio/audio-modal/service.js b/bigbluebutton-html5/imports/ui/components/audio/audio-modal/service.js index ffb6c85716..b8e1a12de7 100644 --- a/bigbluebutton-html5/imports/ui/components/audio/audio-modal/service.js +++ b/bigbluebutton-html5/imports/ui/components/audio/audio-modal/service.js @@ -3,6 +3,7 @@ import Storage from '/imports/ui/services/storage/session'; const CLIENT_DID_USER_SELECTED_MICROPHONE_KEY = 'clientUserSelectedMicrophone'; const CLIENT_DID_USER_SELECTED_LISTEN_ONLY_KEY = 'clientUserSelectedListenOnly'; +const TROUBLESHOOTING_LINKS = Meteor.settings.public.media.audioTroubleshootingLinks; export const setUserSelectedMicrophone = (value) => ( Storage.setItem(CLIENT_DID_USER_SELECTED_MICROPHONE_KEY, !!value) @@ -78,6 +79,11 @@ export const closeModal = (callback) => { callback(); }; +const getTroubleshootingLink = (errorCode) => { + if (TROUBLESHOOTING_LINKS) return TROUBLESHOOTING_LINKS[errorCode] || TROUBLESHOOTING_LINKS[0]; + return null; +}; + export default { joinMicrophone, closeModal, @@ -85,4 +91,5 @@ export default { leaveEchoTest, didUserSelectedMicrophone, didUserSelectedListenOnly, + getTroubleshootingLink, }; diff --git a/bigbluebutton-html5/imports/ui/components/audio/help/component.jsx b/bigbluebutton-html5/imports/ui/components/audio/help/component.jsx index 142954b0ea..41f07e1ced 100644 --- a/bigbluebutton-html5/imports/ui/components/audio/help/component.jsx +++ b/bigbluebutton-html5/imports/ui/components/audio/help/component.jsx @@ -1,63 +1,171 @@ import React, { Component } from 'react'; import { injectIntl, defineMessages } from 'react-intl'; +import PropTypes from 'prop-types'; import Styled from './styles'; +const propTypes = { + intl: PropTypes.shape({ + formatMessage: PropTypes.func.isRequired, + }).isRequired, + audioErr: PropTypes.shape({ + code: PropTypes.number, + message: PropTypes.string, + MIC_ERROR: PropTypes.shape({ + NO_SSL: PropTypes.number, + MAC_OS_BLOCK: PropTypes.number, + NO_PERMISSION: PropTypes.number, + }), + }).isRequired, + handleBack: PropTypes.func.isRequired, + troubleshootingLink: PropTypes.string, +}; + +const defaultProps = { + troubleshootingLink: '', +}; + const intlMessages = defineMessages({ - descriptionHelp: { - id: 'app.audioModal.helpText', - description: 'Text decription for the audio help', + helpSubtitle: { + id: 'app.audioModal.helpSubtitle', + description: 'Text description for the audio help subtitle', }, - backLabel: { - id: 'app.audio.backLabel', - description: 'audio settings back button label', + helpPermissionStep1: { + id: 'app.audioModal.helpPermissionStep1', + description: 'Text description for the audio permission help step 1', + }, + helpPermissionStep2: { + id: 'app.audioModal.helpPermissionStep2', + description: 'Text description for the audio permission help step 2', + }, + helpPermissionStep3: { + id: 'app.audioModal.helpPermissionStep3', + description: 'Text description for the audio permission help step 3', + }, + retryLabel: { + id: 'app.audio.audioSettings.retryLabel', + description: 'audio settings retry button label', }, noSSL: { id: 'app.audioModal.help.noSSL', - description: 'Text decription for domain not using https', + description: 'Text description for domain not using https', }, macNotAllowed: { id: 'app.audioModal.help.macNotAllowed', - description: 'Text decription for mac needed to enable OS setting', + description: 'Text description for mac needed to enable OS setting', + }, + helpTroubleshoot: { + id: 'app.audioModal.help.troubleshoot', + description: 'Text description for help troubleshoot', + }, + unknownError: { + id: 'app.audioModal.help.unknownError', + description: 'Text description for unknown error', + }, + errorCode: { + id: 'app.audioModal.help.errorCode', + description: 'Text description for error code', }, }); class Help extends Component { + renderNoSSL() { + const { intl } = this.props; + + return ( + + {intl.formatMessage(intlMessages.noSSL)} + + ); + } + + renderMacNotAllowed() { + const { intl } = this.props; + + return ( + + {intl.formatMessage(intlMessages.macNotAllowed)} + + ); + } + + renderPermissionHelp() { + const { intl } = this.props; + return ( + <> + + {intl.formatMessage(intlMessages.helpSubtitle)} + + +
  • {intl.formatMessage(intlMessages.helpPermissionStep1)}
  • +
  • {intl.formatMessage(intlMessages.helpPermissionStep2)}
  • +
  • {intl.formatMessage(intlMessages.helpPermissionStep3)}
  • +
    + + ); + } + + renderGenericErrorHelp() { + const { intl, audioErr } = this.props; + const { code, message } = audioErr; + + return ( + <> + + {intl.formatMessage(intlMessages.helpSubtitle)} + + + {intl.formatMessage(intlMessages.unknownError)} + + + {intl.formatMessage(intlMessages.errorCode, { 0: code, 1: message || 'UnknownError' })} + + + ); + } + + renderHelpMessage() { + const { audioErr } = this.props; + const { MIC_ERROR } = audioErr; + + switch (audioErr.code) { + case MIC_ERROR.NO_SSL: + return this.renderNoSSL(); + case MIC_ERROR.MAC_OS_BLOCK: + return this.renderMacNotAllowed(); + case MIC_ERROR.NO_PERMISSION: + return this.renderPermissionHelp(); + default: + return this.renderGenericErrorHelp(); + } + } + render() { const { intl, handleBack, - audioErr, + troubleshootingLink, } = this.props; - const { code, MIC_ERROR } = audioErr; - - let helpMessage = null; - - switch (code) { - case MIC_ERROR.NO_SSL: - helpMessage = intl.formatMessage(intlMessages.noSSL); - break; - case MIC_ERROR.MAC_OS_BLOCK: - helpMessage = intl.formatMessage(intlMessages.macNotAllowed); - break; - case MIC_ERROR.NO_PERMISSION: - default: - helpMessage = intl.formatMessage(intlMessages.descriptionHelp); - break; - } - return ( - - { helpMessage } - + {this.renderHelpMessage()} + { troubleshootingLink && ( + + + {intl.formatMessage(intlMessages.helpTroubleshoot)} + + + )} - @@ -65,4 +173,7 @@ class Help extends Component { } } +Help.propTypes = propTypes; +Help.defaultProps = defaultProps; + export default injectIntl(Help); diff --git a/bigbluebutton-html5/imports/ui/components/audio/help/styles.js b/bigbluebutton-html5/imports/ui/components/audio/help/styles.js index b19fb065ba..33c560ddb4 100644 --- a/bigbluebutton-html5/imports/ui/components/audio/help/styles.js +++ b/bigbluebutton-html5/imports/ui/components/audio/help/styles.js @@ -1,29 +1,36 @@ import styled from 'styled-components'; import Button from '/imports/ui/components/common/button/component'; import { smallOnly } from '/imports/ui/stylesheets/styled-components/breakpoints'; +import { jumboPaddingY, smPaddingY } from '/imports/ui/stylesheets/styled-components/general'; +import { + fontSizeSmaller, +} from '/imports/ui/stylesheets/styled-components/typography'; +import { + colorLink, +} from '/imports/ui/stylesheets/styled-components/palette'; const Help = styled.span` display: flex; flex-flow: column; - height: 10rem; + min-height: 10rem; `; const Text = styled.div` text-align: center; + justify-content: center; margin-top: auto; margin-bottom: auto; `; const EnterAudio = styled.div` - margin-top: 1.5rem; display: flex; justify-content: flex-end; + margin-top: ${jumboPaddingY}; `; -const BackButton = styled(Button)` +const RetryButton = styled(Button)` margin-right: 0.5rem; margin-left: inherit; - border: none; [dir="rtl"] & { margin-right: inherit; @@ -41,9 +48,32 @@ const BackButton = styled(Button)` } `; +const TroubleshootLink = styled.a` + color: ${colorLink}; +`; + +const UnknownError = styled.label` + font-size: ${fontSizeSmaller}; + justify-content: center; + text-align: center; + margin-top: ${smPaddingY}; + margin-bottom: ${smPaddingY}; +`; + +const PermissionHelpSteps = styled.ul` + text-align: left; + justify-content: center; + li { + margin-bottom: ${smPaddingY}; + } +`; + export default { Help, Text, EnterAudio, - BackButton, + RetryButton, + TroubleshootLink, + UnknownError, + PermissionHelpSteps, }; diff --git a/bigbluebutton-html5/private/config/settings.yml b/bigbluebutton-html5/private/config/settings.yml index b0cfe66ec3..79f3a09b63 100755 --- a/bigbluebutton-html5/private/config/settings.yml +++ b/bigbluebutton-html5/private/config/settings.yml @@ -771,6 +771,15 @@ public: # audio: high # webcam: medium # screenshare: medium + # + # audioTroubleshootingLinks: links to help users troubleshoot audio issues + # If no link is provided, the audio troubleshooting button will not be shown. + # Index is the error code: + # - 7: permission denied error code + # - 0: unknown error + #audioTroubleshootingLinks: + # 7: 'https://link.bigbluebutton.org/perm' + # 0: 'https://link.bigbluebutton.org/unk' stats: enabled: true interval: 10000 diff --git a/bigbluebutton-html5/public/locales/en.json b/bigbluebutton-html5/public/locales/en.json index db7c8b94e2..5e48c7ead1 100755 --- a/bigbluebutton-html5/public/locales/en.json +++ b/bigbluebutton-html5/public/locales/en.json @@ -732,8 +732,14 @@ "app.audioModal.no.arialabel" : "Echo is inaudible", "app.audioModal.echoTestTitle": "This is a private echo test. Speak a few words. Did you hear audio?", "app.audioModal.settingsTitle": "Change your audio settings", - "app.audioModal.helpTitle": "There was an issue with your media devices", - "app.audioModal.helpText": "Did you give permission for access to your microphone? Note that a dialog should appear when you try to join audio, asking for your media device permissions, please accept that in order to join the audio conference. If that is not the case, try changing your microphone permissions in your browser's settings.", + "app.audioModal.helpTitle": "There was an issue with your audio devices", + "app.audioModal.helpSubtitle": "We couldn't enable your microphone", + "app.audioModal.helpPermissionStep1": "When joining a call, accept all requests if prompted to use your microphone.", + "app.audioModal.helpPermissionStep2": "Check browser and device settings to ensure microphone access is allowed.", + "app.audioModal.helpPermissionStep3": "Refresh the page and try again.", + "app.audioModal.help.troubleshoot": "Still having issues? Click here for help.", + "app.audioModal.help.unknownError": "Unknown error. Review your browser and system settings and try again.", + "app.audioModal.help.errorCode": "Error code: {0} - {1}", "app.audioModal.help.noSSL": "This page is unsecured. For microphone access to be allowed the page must be served over HTTPS. Please contact the server administrator.", "app.audioModal.help.macNotAllowed": "It looks like your Mac System Preferences are blocking access to your microphone. Open System Preferences > Security & Privacy > Privacy > Microphone, and verify that the browser you're using is checked.", "app.audioModal.audioDialTitle": "Join using your phone",