diff --git a/bigbluebutton-html5/imports/ui/components/audio/audio-controls/component.jsx b/bigbluebutton-html5/imports/ui/components/audio/audio-controls/component.jsx index abf8323c274fdc549393a2c6cc7f39069abe916a..a69cda8fe2b07e83f658bc901bd5ae342de30efd 100755 --- a/bigbluebutton-html5/imports/ui/components/audio/audio-controls/component.jsx +++ b/bigbluebutton-html5/imports/ui/components/audio/audio-controls/component.jsx @@ -5,6 +5,7 @@ import { defineMessages, intlShape, injectIntl } from 'react-intl'; import Button from '/imports/ui/components/button/component'; import getFromUserSettings from '/imports/ui/services/users-settings'; import withShortcutHelper from '/imports/ui/components/shortcut-help/service'; +import MutedAlert from '/imports/ui/components/muted-alert/component'; import { styles } from './styles'; const intlMessages = defineMessages({ @@ -63,6 +64,7 @@ class AudioControls extends PureComponent { intl, shortcuts, isVoiceUser, + inputDeviceId, } = this.props; let joinIcon = 'audio_off'; @@ -74,29 +76,32 @@ class AudioControls extends PureComponent { } } + const label = muted ? intl.formatMessage(intlMessages.unmuteAudio) + : intl.formatMessage(intlMessages.muteAudio); + + const toggleMuteBtn = ( + <Button + className={cx(styles.muteToggle, !talking || styles.glow, !muted || styles.btn)} + onClick={handleToggleMuteMicrophone} + disabled={disable} + hideLabel + label={label} + aria-label={label} + color={!muted ? 'primary' : 'default'} + ghost={muted} + icon={muted ? 'mute' : 'unmute'} + size="lg" + circle + accessKey={shortcuts.togglemute} + /> + ); + return ( <span className={styles.container}> - {showMute && isVoiceUser - ? ( - <Button - className={cx(styles.button, !talking || styles.glow, !muted || styles.btn)} - onClick={handleToggleMuteMicrophone} - disabled={disable} - hideLabel - label={muted ? intl.formatMessage(intlMessages.unmuteAudio) - : intl.formatMessage(intlMessages.muteAudio)} - aria-label={muted ? intl.formatMessage(intlMessages.unmuteAudio) - : intl.formatMessage(intlMessages.muteAudio)} - color={!muted ? 'primary' : 'default'} - ghost={muted} - icon={muted ? 'mute' : 'unmute'} - size="lg" - circle - accessKey={shortcuts.togglemute} - /> - ) : null} + {muted ? <MutedAlert inputDeviceId={inputDeviceId} /> : null} + {showMute && isVoiceUser ? toggleMuteBtn : null} <Button - className={cx(styles.button, inAudio || styles.btn)} + className={cx(inAudio || styles.btn)} onClick={inAudio ? handleLeaveAudio : handleJoinAudio} disabled={disable} hideLabel @@ -111,7 +116,8 @@ class AudioControls extends PureComponent { circle accessKey={inAudio ? shortcuts.leaveaudio : shortcuts.joinaudio} /> - </span>); + </span> + ); } } diff --git a/bigbluebutton-html5/imports/ui/components/audio/audio-controls/container.jsx b/bigbluebutton-html5/imports/ui/components/audio/audio-controls/container.jsx index 04161e8635cfaaa187dee9d1e6651fd7f21c69c1..065b9ac50235bfb4d491ad7fe0ea0d0c5a1db3f2 100755 --- a/bigbluebutton-html5/imports/ui/components/audio/audio-controls/container.jsx +++ b/bigbluebutton-html5/imports/ui/components/audio/audio-controls/container.jsx @@ -66,4 +66,5 @@ export default lockContextContainer(withModalMounter(withTracker(({ mountModal, handleToggleMuteMicrophone: () => toggleMuteMicrophone(), handleJoinAudio: () => (isConnected() ? joinListenOnly() : mountModal(<AudioModalContainer />)), handleLeaveAudio, + inputDeviceId: AudioManager.inputDeviceId, }))(AudioControlsContainer))); diff --git a/bigbluebutton-html5/imports/ui/components/audio/audio-controls/styles.scss b/bigbluebutton-html5/imports/ui/components/audio/audio-controls/styles.scss index 702f765146bfd25adb75f2b88c60bb9a0c21877a..49b567556555d5461bf17d3d95c93df3ffbe82db 100755 --- a/bigbluebutton-html5/imports/ui/components/audio/audio-controls/styles.scss +++ b/bigbluebutton-html5/imports/ui/components/audio/audio-controls/styles.scss @@ -5,31 +5,39 @@ display: flex; flex-flow: row; - > * { - margin: 0 var(--sm-padding-x); + .muteToggle { + margin-right: var(--sm-padding-x); + margin-left: 0; @include mq($small-only) { - margin: 0 var(--sm-padding-y); + margin-right: var(--sm-padding-y); } - } - - > :first-child { - margin-left: 0; - margin-right: inherit; [dir="rtl"] & { - margin-left: inherit; + margin-left: var(--sm-padding-x); margin-right: 0; + + @include mq($small-only) { + margin-left: var(--sm-padding-y); + } } } - + > :last-child { - margin-left: inherit; - margin-right: 0; + margin-left: var(--sm-padding-x); + margin-right: 0; + + @include mq($small-only) { + margin-left: var(--sm-padding-y); + } [dir="rtl"] & { margin-left: 0; - margin-right: inherit; + margin-right: var(--sm-padding-x); + + @include mq($small-only) { + margin-right: var(--sm-padding-y); + } } } } diff --git a/bigbluebutton-html5/imports/ui/components/muted-alert/component.jsx b/bigbluebutton-html5/imports/ui/components/muted-alert/component.jsx new file mode 100644 index 0000000000000000000000000000000000000000..e44e5a894f2177cb837ee399e5d384613f5db72a --- /dev/null +++ b/bigbluebutton-html5/imports/ui/components/muted-alert/component.jsx @@ -0,0 +1,81 @@ +import React, { Component } from 'react'; +import PropTypes from 'prop-types'; +import { FormattedMessage } from 'react-intl'; +import hark from 'hark'; +import Icon from '/imports/ui/components/icon/component'; +import { styles } from './styles'; + +const MUTE_WARNING_TIMEOUT = 4000; + +const propTypes = { + inputDeviceId: PropTypes.string.isRequired, +}; + +class MutedAlert extends Component { + constructor(props) { + super(props); + + this.state = { + visible: false, + }; + + this.speechEvents = null; + this.timer = null; + + this.resetTimer = this.resetTimer.bind(this); + } + + componentDidMount() { + this._isMounted = true; + const { inputDeviceId } = this.props; + + navigator.mediaDevices.getUserMedia({ audio: { deviceId: inputDeviceId } }) + .then((stream) => { + this.speechEvents = hark(stream, { interval: 200 }); + + this.speechEvents.on('speaking', () => { + this.resetTimer(); + if (this._isMounted) this.setState({ visible: true }); + }); + + this.speechEvents.on('stopped_speaking', () => { + if (this._isMounted) { + this.timer = setTimeout(() => this.setState( + { visible: false }, + ), MUTE_WARNING_TIMEOUT); + } + }); + }); + } + + componentWillUnmount() { + this._isMounted = false; + if (this.speechEvents) this.speechEvents.stop(); + } + + resetTimer() { + if (this.timer) clearTimeout(this.timer); + this.timer = null; + } + + render() { + const { visible } = this.state; + return visible ? ( + <div className={styles.muteWarning}> + <span> + <FormattedMessage + id="app.muteWarning.label" + description="Warning when someone speaks while muted" + values={{ + 0: <Icon iconName="mute" />, + }} + /> + </span> + </div> + ) : null; + } +} + +MutedAlert.propTypes = propTypes; + +export default MutedAlert; diff --git a/bigbluebutton-html5/imports/ui/components/muted-alert/styles.scss b/bigbluebutton-html5/imports/ui/components/muted-alert/styles.scss new file mode 100644 index 0000000000000000000000000000000000000000..51e22367e3abae3c3b4a17bd4f26907980aec7ee --- /dev/null +++ b/bigbluebutton-html5/imports/ui/components/muted-alert/styles.scss @@ -0,0 +1,24 @@ +@import "../../stylesheets/variables/_all"; + +.muteWarning { + position: absolute; + color: var(--color-white); + background-color: black; + text-align: center; + line-height: 1; + font-size: var(--font-size-xl); + padding: var(--md-padding-y); + border-radius: var(--border-radius); + top: -50%; + left: 50%; + transform: translate(-50%, -50%); + z-index: 5; + + > span { + white-space: nowrap; + } + + @include mq($small-only) { + font-size: var(--font-size-md);; + } + } \ No newline at end of file diff --git a/bigbluebutton-html5/imports/ui/stylesheets/variables/typography.scss b/bigbluebutton-html5/imports/ui/stylesheets/variables/typography.scss index 29270a5be8f5f102063778eff2946a80b2e85bfc..bf83f8da395fa33535242c4244189bc263016e41 100644 --- a/bigbluebutton-html5/imports/ui/stylesheets/variables/typography.scss +++ b/bigbluebutton-html5/imports/ui/stylesheets/variables/typography.scss @@ -6,6 +6,7 @@ --font-family-base: var(--font-family-sans-serif); --font-size-base: 1rem; + --font-size-xl: 1.75rem; --font-size-large: 1.25rem; --font-size-md: 0.95rem; --font-size-small: 0.875rem; diff --git a/bigbluebutton-html5/package.json b/bigbluebutton-html5/package.json index 1e4299b8ba0bc79c36bf129c24cf560688be5b2d..44fe3c8b38f56198acb4dd55db6250302a03a6a1 100755 --- a/bigbluebutton-html5/package.json +++ b/bigbluebutton-html5/package.json @@ -44,6 +44,7 @@ "fastdom": "^1.0.9", "fibers": "^4.0.2", "flat": "~4.1.0", + "hark": "^1.2.3", "immutability-helper": "~2.8.1", "langmap": "0.0.16", "lodash": "^4.17.15", diff --git a/bigbluebutton-html5/private/locales/en.json b/bigbluebutton-html5/private/locales/en.json index c9cf7635b8978927601bd7b25fe16f11e7bc6cdf..4467379a90f971f6cae66bda1384b0677484082f 100755 --- a/bigbluebutton-html5/private/locales/en.json +++ b/bigbluebutton-html5/private/locales/en.json @@ -244,6 +244,7 @@ "app.connectingMessage": "Connecting ...", "app.waitingMessage": "Disconnected. Trying to reconnect in {0} seconds ...", "app.retryNow": "Retry now", + "app.muteWarning.label": "You are muted. Click {0} to unmute yourself", "app.navBar.settingsDropdown.optionsLabel": "Options", "app.navBar.settingsDropdown.fullscreenLabel": "Make fullscreen", "app.navBar.settingsDropdown.settingsLabel": "Settings",