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..69a613c1fddf4f72ea84d058eef5236c71ef2ec7 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,9 @@ class AudioControls extends PureComponent { intl, shortcuts, isVoiceUser, + inputStream, + isViewer, + isPresenter, } = this.props; let joinIcon = 'audio_off'; @@ -74,29 +78,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 {...{ inputStream, isViewer, isPresenter }} /> : 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 +118,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..d55c0d4e8e29d15319ca72fb3e7f34a1d8f34ff7 100755 --- a/bigbluebutton-html5/imports/ui/components/audio/audio-controls/container.jsx +++ b/bigbluebutton-html5/imports/ui/components/audio/audio-controls/container.jsx @@ -5,10 +5,14 @@ import AudioManager from '/imports/ui/services/audio-manager'; import { makeCall } from '/imports/ui/services/api'; import lockContextContainer from '/imports/ui/components/lock-viewers/context/container'; import logger from '/imports/startup/client/logger'; +import Auth from '/imports/ui/services/auth'; +import Users from '/imports/api/users'; import AudioControls from './component'; import AudioModalContainer from '../audio-modal/container'; import Service from '../service'; +const ROLE_VIEWER = Meteor.settings.public.user.role_viewer; + const AudioControlsContainer = props => <AudioControls {...props} />; const processToggleMuteFromOutside = (e) => { @@ -54,16 +58,30 @@ const { joinListenOnly, } = Service; -export default lockContextContainer(withModalMounter(withTracker(({ mountModal, userLocks }) => ({ - processToggleMuteFromOutside: arg => processToggleMuteFromOutside(arg), - showMute: isConnected() && !isListenOnly() && !isEchoTest() && !userLocks.userMic, - muted: isConnected() && !isListenOnly() && isMuted(), - inAudio: isConnected() && !isEchoTest(), - listenOnly: isConnected() && isListenOnly(), - disable: isConnecting() || isHangingUp() || !Meteor.status().connected, - talking: isTalking() && !isMuted(), - isVoiceUser: isVoiceUser(), - handleToggleMuteMicrophone: () => toggleMuteMicrophone(), - handleJoinAudio: () => (isConnected() ? joinListenOnly() : mountModal(<AudioModalContainer />)), - handleLeaveAudio, -}))(AudioControlsContainer))); +export default lockContextContainer(withModalMounter(withTracker(({ mountModal, userLocks }) => { + const currentUser = Users.findOne({ meetingId: Auth.meetingID, userId: Auth.userID }, { + fields: { + role: 1, + presenter: 1, + }, + }); + const isViewer = currentUser.role === ROLE_VIEWER; + const isPresenter = currentUser.presenter; + + return ({ + processToggleMuteFromOutside: arg => processToggleMuteFromOutside(arg), + showMute: isConnected() && !isListenOnly() && !isEchoTest() && !userLocks.userMic, + muted: isConnected() && !isListenOnly() && isMuted(), + inAudio: isConnected() && !isEchoTest(), + listenOnly: isConnected() && isListenOnly(), + disable: isConnecting() || isHangingUp() || !Meteor.status().connected, + talking: isTalking() && !isMuted(), + isVoiceUser: isVoiceUser(), + handleToggleMuteMicrophone: () => toggleMuteMicrophone(), + handleJoinAudio: () => (isConnected() ? joinListenOnly() : mountModal(<AudioModalContainer />)), + handleLeaveAudio, + inputStream: AudioManager.inputStream, + isViewer, + isPresenter, + }); +})(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..88d03e90e055a64df7a03c0e6ea15ee98b797aeb --- /dev/null +++ b/bigbluebutton-html5/imports/ui/components/muted-alert/component.jsx @@ -0,0 +1,84 @@ +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 cx from 'classnames'; +import { styles } from './styles'; + +const MUTE_ALERT_CONFIG = Meteor.settings.public.app.mutedAlert; + +const propTypes = { + inputStream: PropTypes.object.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 { inputStream } = this.props; + const { interval, threshold, duration } = MUTE_ALERT_CONFIG; + this.speechEvents = hark(inputStream, { interval, threshold }); + 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 }, + ), duration); + } + }); + } + + componentWillUnmount() { + this._isMounted = false; + if (this.speechEvents) this.speechEvents.stop(); + this.resetTimer(); + } + + resetTimer() { + if (this.timer) clearTimeout(this.timer); + this.timer = null; + } + + render() { + const { enabled } = MUTE_ALERT_CONFIG; + if (!enabled) return null; + const { isViewer, isPresenter } = this.props; + const { visible } = this.state; + const style = {}; + style[styles.alignForMod] = !isViewer || isPresenter; + + return visible ? ( + <div className={cx(styles.muteWarning, style)}> + <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..f1076a2cb4204fea2603579e717a6f5d5a7c0f32 --- /dev/null +++ b/bigbluebutton-html5/imports/ui/components/muted-alert/styles.scss @@ -0,0 +1,28 @@ +@import "../../stylesheets/variables/_all"; + +.muteWarning { + position: absolute; + color: var(--color-white); + background-color: var(--color-tip-bg); + text-align: center; + line-height: 1; + font-size: var(--font-size-xl); + padding: var(--md-padding-x); + 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);; + } +} + +.alignForMod { + left: 52.25%; +} diff --git a/bigbluebutton-html5/imports/ui/stylesheets/variables/palette.scss b/bigbluebutton-html5/imports/ui/stylesheets/variables/palette.scss index 288d3a11fc9c312f7beeb9a597101b353af8f95b..4edfd06d5d23937fc2e0c4cf3106f6c8336891ff 100644 --- a/bigbluebutton-html5/imports/ui/stylesheets/variables/palette.scss +++ b/bigbluebutton-html5/imports/ui/stylesheets/variables/palette.scss @@ -30,4 +30,5 @@ --color-gray-label: var(--color-gray); --color-transparent: #ff000000; + --color-tip-bg: #333333; } diff --git a/bigbluebutton-html5/imports/ui/stylesheets/variables/typography.scss b/bigbluebutton-html5/imports/ui/stylesheets/variables/typography.scss index 266cd387c3dc199427044e2aa3599f28448f7b4f..bf83f8da395fa33535242c4244189bc263016e41 100644 --- a/bigbluebutton-html5/imports/ui/stylesheets/variables/typography.scss +++ b/bigbluebutton-html5/imports/ui/stylesheets/variables/typography.scss @@ -6,7 +6,7 @@ --font-family-base: var(--font-family-sans-serif); --font-size-base: 1rem; - --font-size-xl: 1.5rem; + --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 49055c31f55e2cd834f5ac93881e9d58b7013db6..5ea8c1f511e5049ad54348de480293df55c882dc 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.19", diff --git a/bigbluebutton-html5/private/config/settings.yml b/bigbluebutton-html5/private/config/settings.yml index cd182ac81fb0df170ed4e8074f6846ed3c267f20..839ef9abeaa926c6b7ded73fcb6743e321351fa5 100755 --- a/bigbluebutton-html5/private/config/settings.yml +++ b/bigbluebutton-html5/private/config/settings.yml @@ -27,6 +27,11 @@ public: ipv4FallbackDomain: "" allowLogout: true allowFullscreen: true + mutedAlert: + enabled: true + interval: 200 + threshold: -50 + duration: 4000 remainingTimeThreshold: 30 remainingTimeAlertThreshold: 1 defaultSettings: diff --git a/bigbluebutton-html5/private/locales/en.json b/bigbluebutton-html5/private/locales/en.json index 000f5750a0c003b369b8729cd4c411d9b5505ded..cc6ff15f39ec68c0c3ddd42ca91f5c63d6e96889 100755 --- a/bigbluebutton-html5/private/locales/en.json +++ b/bigbluebutton-html5/private/locales/en.json @@ -253,6 +253,7 @@ "app.connectingMessage": "Connecting ...", "app.waitingMessage": "Disconnected. Trying to reconnect in {0} seconds ...", "app.retryNow": "Retry now", + "app.muteWarning.label": "Click {0} to unmute yourself.", "app.navBar.settingsDropdown.optionsLabel": "Options", "app.navBar.settingsDropdown.fullscreenLabel": "Make fullscreen", "app.navBar.settingsDropdown.settingsLabel": "Settings",