diff --git a/bigbluebutton-html5/imports/ui/components/chat/notification/audio-notification/component.jsx b/bigbluebutton-html5/imports/ui/components/chat/notification/audio-notification/component.jsx new file mode 100644 index 0000000000000000000000000000000000000000..5001a439fb4ec38e1a9f12a1d8112260d650a9c7 --- /dev/null +++ b/bigbluebutton-html5/imports/ui/components/chat/notification/audio-notification/component.jsx @@ -0,0 +1,49 @@ +import React from 'react'; +import _ from 'lodash'; +import PropTypes from 'prop-types'; + +const propTypes = { + play: PropTypes.bool.isRequired, + count: PropTypes.number.isRequired, +}; + +class ChatAudioNotification extends React.Component { + constructor(props) { + super(props); + this.audio = new Audio('/html5client/resources/sounds/notify.mp3'); + + this.handleAudioLoaded = this.handleAudioLoaded.bind(this); + this.playAudio = this.playAudio.bind(this); + this.componentDidUpdate = _.debounce(this.playAudio, 2000); + } + + componentDidMount() { + this.audio.addEventListener('loadedmetadata', this.handleAudioLoaded); + } + + shouldComponentUpdate(nextProps) { + return nextProps.count > this.props.count; + } + + componentWillUnmount() { + this.audio.removeEventListener('loadedmetadata', this.handleAudioLoaded); + } + + handleAudioLoaded() { + this.componentDidUpdate = _.debounce(this.playAudio, this.audio.duration * 1000); + } + + playAudio() { + if (!this.props.play) return; + + this.audio.play(); + } + + render() { + return null; + } + +} +ChatAudioNotification.propTypes = propTypes; + +export default ChatAudioNotification; diff --git a/bigbluebutton-html5/imports/ui/components/chat/notification/component.jsx b/bigbluebutton-html5/imports/ui/components/chat/notification/component.jsx new file mode 100644 index 0000000000000000000000000000000000000000..50ebd65438e9ec4ad0a4f8fe143cdc76edb03f21 --- /dev/null +++ b/bigbluebutton-html5/imports/ui/components/chat/notification/component.jsx @@ -0,0 +1,94 @@ +import React, { Component } from 'react'; +import PropTypes from 'prop-types'; +import ChatAudioNotification from './audio-notification/component'; +import ChatPushNotification from './push-notification/component'; + +const propTypes = { + disableNotify: PropTypes.bool.isRequired, + openChats: PropTypes.arrayOf(PropTypes.object).isRequired, + disableAudio: PropTypes.bool.isRequired, +}; + +class ChatNotification extends Component { + constructor(props) { + super(props); + this.state = { notified: {} }; + } + + componentWillReceiveProps(nextProps) { + const { + openChats, + disableNotify, + } = this.props; + + if (nextProps.disableNotify === false && disableNotify === true) { + const loadMessages = {}; + openChats + .forEach((c) => { + loadMessages[c.id] = c.unreadCounter; + }); + this.setState({ notified: loadMessages }); + return; + } + + const notifiedToClear = {}; + openChats + .filter(c => c.unreadCounter === 0) + .forEach((c) => { + notifiedToClear[c.id] = 0; + }); + + this.setState(({ notified }) => ({ + notified: { + ...notified, + ...notifiedToClear, + }, + })); + } + + render() { + const { + disableNotify, + disableAudio, + openChats, + } = this.props; + + const unreadMessagesCount = openChats + .map(chat => chat.unreadCounter) + .reduce((a, b) => a + b, 0); + + const shouldPlayAudio = !disableAudio && unreadMessagesCount > 0; + + const chatsNotify = openChats + .filter(({ id, unreadCounter }) => + unreadCounter > 0 && + unreadCounter !== this.state.notified[id] && + !disableNotify); + + return ( + <span> + <ChatAudioNotification play={shouldPlayAudio} count={unreadMessagesCount} /> + { + chatsNotify.map(({ id, name, unreadCounter }) => + (<ChatPushNotification + key={id} + name={name} + count={unreadCounter} + onOpen={() => { + this.setState(({ notified }) => ({ + notified: { + ...notified, + [id]: unreadCounter, + }, + })); + }} + />), + ) + } + </span> + ); + } +} +ChatNotification.propTypes = propTypes; + +export default ChatNotification; diff --git a/bigbluebutton-html5/imports/ui/components/chat/notification/container.jsx b/bigbluebutton-html5/imports/ui/components/chat/notification/container.jsx index b58274514ba8d3331b25dbf99bbf445e08033e3c..ade6871a4c75c8d840c759296888dd34b61b65b2 100644 --- a/bigbluebutton-html5/imports/ui/components/chat/notification/container.jsx +++ b/bigbluebutton-html5/imports/ui/components/chat/notification/container.jsx @@ -1,46 +1,20 @@ -import React, { Component } from 'react'; -// import PropTypes from 'prop-types'; +import React from 'react'; import { createContainer } from 'meteor/react-meteor-data'; -import _ from 'lodash'; - -import Auth from '/imports/ui/services/auth'; import UserListService from '/imports/ui/components/user-list/service'; import Settings from '/imports/ui/services/settings'; +import ChatNotification from './component'; -class ChatNotificationContainer extends Component { - constructor(props) { - super(props); - - this.audio = new Audio('/html5client/resources/sounds/notify.mp3'); - } - - componentDidUpdate(prevProps) { - if (this.props.unreadMessagesCount < prevProps.unreadMessagesCount) return; - - this.playAudio(); - } - - playAudio() { - if (this.props.disableAudio) return; - - _.debounce(() => this.audio.play(), this.audio.duration * 1000)(); - } - - render() { - return null; - } -} +const ChatNotificationContainer = props => ( + <ChatNotification {...props} /> +); export default createContainer(() => { const AppSettings = Settings.application; - - const unreadMessagesCount = UserListService.getOpenChats() - .map(chat => chat.unreadCounter) - .filter(userID => userID !== Auth.userID) - .reduce((a, b) => a + b, 0); + const openChats = UserListService.getOpenChats(); return { disableAudio: !AppSettings.chatAudioNotifications, - unreadMessagesCount, + disableNotify: !AppSettings.chatPushNotifications, + openChats, }; }, ChatNotificationContainer); diff --git a/bigbluebutton-html5/imports/ui/components/chat/notification/push-notification/component.jsx b/bigbluebutton-html5/imports/ui/components/chat/notification/push-notification/component.jsx new file mode 100644 index 0000000000000000000000000000000000000000..0ee49592563fb0766c5cd8b6ffb10de77fd66619 --- /dev/null +++ b/bigbluebutton-html5/imports/ui/components/chat/notification/push-notification/component.jsx @@ -0,0 +1,60 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import _ from 'lodash'; +import injectNotify from '/imports/ui/components/toast/inject-notify/component'; +import { defineMessages, injectIntl, intlShape } from 'react-intl'; + +const propTypes = { + intl: intlShape.isRequired, + count: PropTypes.number.isRequired, + name: PropTypes.string.isRequired, + notify: PropTypes.func.isRequired, + onOpen: PropTypes.func.isRequired, +}; + +const intlMessages = defineMessages({ + appToastChatSigular: { + id: 'app.toast.chat.singular', + description: 'when entry a message', + }, + appToastChatPlural: { + id: 'app.toast.chat.plural', + description: 'when entry various message', + }, +}); + +class ChatPushNotification extends React.Component { + constructor(props) { + super(props); + this.showNotify = _.debounce(this.showNotify.bind(this), 1000); + + this.componentDidMount = this.showNotify; + this.componentDidUpdate = this.showNotify; + } + + showNotify() { + const { + intl, + count, + name, + notify, + onOpen, + } = this.props; + + const message = intl.formatMessage( + count > 1 ? + intlMessages.appToastChatPlural : + intlMessages.appToastChatSigular, { + 0: count, + 1: name }); + + return notify(message, 'info', 'chat', { onOpen }); + } + + render() { + return null; + } +} +ChatPushNotification.propTypes = propTypes; + +export default injectIntl(injectNotify(ChatPushNotification)); diff --git a/bigbluebutton-html5/imports/ui/components/settings/component.jsx b/bigbluebutton-html5/imports/ui/components/settings/component.jsx index abcd6879bc5ea41851c32027b4571384e336520d..f442ddc4b9df23efc6404951f4162f600aa082ed 100644 --- a/bigbluebutton-html5/imports/ui/components/settings/component.jsx +++ b/bigbluebutton-html5/imports/ui/components/settings/component.jsx @@ -1,13 +1,13 @@ import React, { Component } from 'react'; import Modal from '/imports/ui/components/modal/fullscreen/component'; import { Tab, Tabs, TabList, TabPanel } from 'react-tabs'; -import { defineMessages, injectIntl } from 'react-intl'; +import { defineMessages, injectIntl, intlShape } from 'react-intl'; import ClosedCaptions from '/imports/ui/components/settings/submenus/closed-captions/component'; import Application from '/imports/ui/components/settings/submenus/application/container'; -import Participants from '/imports/ui/components/settings/submenus/participants/component'; import _ from 'lodash'; -import { withModalMounter } from '../modal/service'; +import PropTypes from 'prop-types'; +import { withModalMounter } from '../modal/service'; import Icon from '../icon/component'; import styles from './styles'; @@ -55,6 +55,15 @@ const intlMessages = defineMessages({ }); const propTypes = { + intl: intlShape.isRequired, + video: PropTypes.object.isRequired, + application: PropTypes.object.isRequired, + cc: PropTypes.object.isRequired, + participants: PropTypes.object.isRequired, + updateSettings: PropTypes.func.isRequired, + availableLocales: PropTypes.object.isRequired, + mountModal: PropTypes.func.isRequired, + locales: PropTypes.array.isRequired, }; class Settings extends Component { @@ -110,7 +119,6 @@ class Settings extends Component { renderModalContent() { const { - isModerator, intl, } = this.props; @@ -134,12 +142,12 @@ class Settings extends Component { <Icon iconName="user" className={styles.icon} /> <span id="ccTab">{intl.formatMessage(intlMessages.closecaptionTabLabel)}</span> </Tab> - {/*{ isModerator ?*/} - {/*<Tab className={styles.tabSelector} aria-labelledby="usersTab">*/} - {/*<Icon iconName="user" className={styles.icon} />*/} - {/*<span id="usersTab">{intl.formatMessage(intlMessages.usersTabLabel)}</span>*/} - {/*</Tab>*/} - {/*: null }*/} + {/* { isModerator ?*/} + {/* <Tab className={styles.tabSelector} aria-labelledby="usersTab">*/} + {/* <Icon iconName="user" className={styles.icon} />*/} + {/* <span id="usersTab">{intl.formatMessage(intlMessages.usersTabLabel)}</span>*/} + {/* </Tab>*/} + {/* : null }*/} </TabList> <TabPanel className={styles.tabPanel}> <Application @@ -161,14 +169,14 @@ class Settings extends Component { locales={this.props.locales} /> </TabPanel> - {/*{ isModerator ?*/} - {/*<TabPanel className={styles.tabPanel}>*/} - {/*<Participants*/} - {/*settings={this.state.current.participants}*/} - {/*handleUpdateSettings={this.handleUpdateSettings}*/} - {/*/>*/} - {/*</TabPanel>*/} - {/*: null }*/} + {/* { isModerator ?*/} + {/* <TabPanel className={styles.tabPanel}>*/} + {/* <Participants*/} + {/* settings={this.state.current.participants}*/} + {/* handleUpdateSettings={this.handleUpdateSettings}*/} + {/* />*/} + {/* </TabPanel>*/} + {/* : null }*/} </Tabs> ); } diff --git a/bigbluebutton-html5/imports/ui/components/settings/submenus/application/component.jsx b/bigbluebutton-html5/imports/ui/components/settings/submenus/application/component.jsx index b79dc2f99be3896dfbb40bdd9ff491d26ec91a9c..0c1600336b5c536388e105adf581da1d06c87347 100644 --- a/bigbluebutton-html5/imports/ui/components/settings/submenus/application/component.jsx +++ b/bigbluebutton-html5/imports/ui/components/settings/submenus/application/component.jsx @@ -1,8 +1,5 @@ import React from 'react'; -import Modal from 'react-modal'; -import Icon from '/imports/ui/components/icon/component'; import Button from '/imports/ui/components/button/component'; -import ReactDOM from 'react-dom'; import cx from 'classnames'; import Toggle from '/imports/ui/components/switch/component'; import { defineMessages, injectIntl } from 'react-intl'; @@ -155,7 +152,7 @@ class ApplicationMenu extends BaseMenu { </div> </div> </div> - {/* TODO: Uncomment after the release + <div className={styles.row}> <div className={styles.col}> <div className={styles.formElement} > @@ -175,7 +172,7 @@ class ApplicationMenu extends BaseMenu { /> </div> </div> - </div>*/} + </div> <div className={styles.row}> <div className={styles.col}> <div className={styles.formElement}> @@ -185,29 +182,29 @@ class ApplicationMenu extends BaseMenu { </div> </div> <div className={styles.col}> - <label aria-labelledby="changeLangLabel" className={cx(styles.formElement, styles.pullContentRight)}> - <select - defaultValue={this.formatLocale(this.state.settings.locale)} - className={styles.select} - onChange={this.handleSelectChange.bind(this, 'locale', availableLocales)} - > - <option disabled> - { availableLocales && + <label aria-labelledby="changeLangLabel" className={cx(styles.formElement, styles.pullContentRight)}> + <select + defaultValue={this.formatLocale(this.state.settings.locale)} + className={styles.select} + onChange={this.handleSelectChange.bind(this, 'locale', availableLocales)} + > + <option disabled> + { availableLocales && availableLocales.length ? intl.formatMessage(intlMessages.languageOptionLabel) : intl.formatMessage(intlMessages.noLocaleOptionLabel) } - </option> - {availableLocales ? availableLocales.map((locale, index) => + </option> + {availableLocales ? availableLocales.map((locale, index) => (<option key={index} value={locale.locale}> {locale.name} </option>), ) : null } - </select> - </label> - <div - id="changeLangLabel" - aria-label={intl.formatMessage(intlMessages.ariaLanguageLabel)} - /> + </select> + </label> + <div + id="changeLangLabel" + aria-label={intl.formatMessage(intlMessages.ariaLanguageLabel)} + /> </div> </div> <hr className={styles.separator} /> diff --git a/bigbluebutton-html5/private/locales/en.json b/bigbluebutton-html5/private/locales/en.json index e267a8ca7e33db7f77b4fe04b9d33f4b30c71a67..7c96cade82246baa9db3aa056ac0367b0b3aeb0e 100644 --- a/bigbluebutton-html5/private/locales/en.json +++ b/bigbluebutton-html5/private/locales/en.json @@ -254,6 +254,8 @@ "app.error.403": "Forbidden", "app.error.leaveLabel": "Log in again", "app.guest.waiting": "Waiting for approval to join", + "app.toast.chat.singular":"you have {0} new message in {1}", + "app.toast.chat.plural":"you have {0} new messages in {1}", "app.notification.recordingStart": "This session is now being recorded", "app.notification.recordingStop": "This session is not being recorded anymore" }