From a9989e110776da53e40e26df222a3a6c3f1e1ce0 Mon Sep 17 00:00:00 2001 From: Vitor Mateus Date: Mon, 14 Jan 2019 11:23:35 -0200 Subject: [PATCH 1/3] Fix chat alert (push and audio) #6430 --- .../server/handlers/validateAuthToken.js | 1 + .../api/users/server/modifiers/addUser.js | 1 - .../chat/alert/audio-alert/component.jsx | 14 +- .../ui/components/chat/alert/component.jsx | 297 ++++++++---------- .../ui/components/chat/alert/container.jsx | 14 +- .../chat/alert/push-alert/component.jsx | 21 +- .../imports/ui/components/chat/component.jsx | 2 + .../imports/ui/components/chat/container.jsx | 2 +- .../imports/ui/components/chat/service.js | 88 +++--- .../user-list/chat-list-item/component.jsx | 14 +- .../ui/components/user-list/component.jsx | 60 ++-- .../ui/components/user-list/container.jsx | 4 +- .../ui/components/user-list/service.js | 80 ++--- .../user-list/user-list-content/component.jsx | 6 +- .../user-messages/component.jsx | 55 ++-- .../user-dropdown/component.jsx | 2 +- .../imports/ui/services/user/mapUser.js | 2 +- 17 files changed, 324 insertions(+), 339 deletions(-) diff --git a/bigbluebutton-html5/imports/api/users/server/handlers/validateAuthToken.js b/bigbluebutton-html5/imports/api/users/server/handlers/validateAuthToken.js index 9fdd768003..1150da0792 100644 --- a/bigbluebutton-html5/imports/api/users/server/handlers/validateAuthToken.js +++ b/bigbluebutton-html5/imports/api/users/server/handlers/validateAuthToken.js @@ -40,6 +40,7 @@ export default function handleValidateAuthToken({ body }, meetingId) { $set: { validated: valid, approved: !waitForApproval, + loginTime: Date.now(), }, }; diff --git a/bigbluebutton-html5/imports/api/users/server/modifiers/addUser.js b/bigbluebutton-html5/imports/api/users/server/modifiers/addUser.js index 67f3bd1d7c..86882d05ef 100755 --- a/bigbluebutton-html5/imports/api/users/server/modifiers/addUser.js +++ b/bigbluebutton-html5/imports/api/users/server/modifiers/addUser.js @@ -78,7 +78,6 @@ export default function addUser(meetingId, user) { roles: [ROLE_VIEWER.toLowerCase()], sortName: user.name.trim().toLowerCase(), color, - logTime: Date.now(), }, flat(user), ), diff --git a/bigbluebutton-html5/imports/ui/components/chat/alert/audio-alert/component.jsx b/bigbluebutton-html5/imports/ui/components/chat/alert/audio-alert/component.jsx index fad03755af..1cf0f5c948 100644 --- a/bigbluebutton-html5/imports/ui/components/chat/alert/audio-alert/component.jsx +++ b/bigbluebutton-html5/imports/ui/components/chat/alert/audio-alert/component.jsx @@ -1,41 +1,33 @@ import React from 'react'; -import _ from 'lodash'; import PropTypes from 'prop-types'; const propTypes = { play: PropTypes.bool.isRequired, - count: PropTypes.number.isRequired, }; class ChatAudioAlert extends React.Component { constructor(props) { super(props); this.audio = new Audio(`${Meteor.settings.public.app.basename}/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); + this.componentDidUpdate = this.playAudio; } playAudio() { - if (!this.props.play) return; - + const { play } = this.props; + if (!play) return; this.audio.play(); } diff --git a/bigbluebutton-html5/imports/ui/components/chat/alert/component.jsx b/bigbluebutton-html5/imports/ui/components/chat/alert/component.jsx index 2c9408a392..1ada36c3a4 100755 --- a/bigbluebutton-html5/imports/ui/components/chat/alert/component.jsx +++ b/bigbluebutton-html5/imports/ui/components/chat/alert/component.jsx @@ -10,9 +10,10 @@ import Service from '../service'; import { styles } from '../styles'; const propTypes = { - disableNotify: PropTypes.bool.isRequired, - openChats: PropTypes.arrayOf(PropTypes.object).isRequired, - disableAudio: PropTypes.bool.isRequired, + pushAlertDisabled: PropTypes.bool.isRequired, + activeChats: PropTypes.arrayOf(PropTypes.object).isRequired, + audioAlertDisabled: PropTypes.bool.isRequired, + joinTimestamp: PropTypes.number.isRequired, }; const intlMessages = defineMessages({ @@ -34,51 +35,103 @@ const intlMessages = defineMessages({ }, }); -const PUBLIC_KEY = 'public'; -const PRIVATE_KEY = 'private'; +const ALERT_INTERVAL = 5000; // 5 seconds +const ALERT_DURATION = 4000; // 4 seconds class ChatAlert extends PureComponent { constructor(props) { super(props); this.state = { - notified: Service.getNotified(PRIVATE_KEY), - publicNotified: Service.getNotified(PUBLIC_KEY), + lastAlertTimestamp: props.joinTimestamp, + lastAlertTimestampByChat: {}, + pendingNotificationsByChat: {}, }; } - componentWillReceiveProps(nextProps) { + componentDidUpdate(prevProps) { const { - openChats, - disableNotify, + pushAlertDisabled, + activeChats, + joinTimestamp, } = this.props; - if (nextProps.disableNotify === false && disableNotify === true) { - const loadMessages = {}; - openChats - .forEach((c) => { - loadMessages[c.id] = c.unreadCounter; - }); - this.setState({ notified: loadMessages }); + const { + lastAlertTimestamp, + lastAlertTimestampByChat, + pendingNotificationsByChat, + } = this.state; + // Avoid alerting messages received before enabling alerts + if (prevProps.pushAlertDisabled && !pushAlertDisabled) { + const lastMessageTimestamp = Service.getLastMessageTimestampFromChatList(activeChats); + this.setLastAlertTimestamp(lastMessageTimestamp); return; } - const notifiedToClear = {}; - openChats - .filter(c => c.unreadCounter === 0) - .forEach((c) => { - notifiedToClear[c.id] = 0; + + // Keep track of messages that was not alerted yet + const unalertedMessagesByChatId = {}; + + activeChats + .filter(chat => chat.id !== Session.get('idChatOpen')) + .filter(chat => chat.unreadCounter > 0) + .forEach((chat) => { + const chatId = (chat.id === 'public') ? 'MAIN-PUBLIC-GROUP-CHAT' : chat.id; + const thisChatUnreadMessages = UnreadMessages.getUnreadMessages(chatId); + + unalertedMessagesByChatId[chatId] = thisChatUnreadMessages.filter((msg) => { + const messageChatId = (msg.chatId === 'MAIN-PUBLIC-GROUP-CHAT') ? msg.chatId : msg.sender; + const retorno = (msg + && msg.timestamp > lastAlertTimestamp + && msg.timestamp > joinTimestamp + && msg.timestamp > (lastAlertTimestampByChat[messageChatId] || 0) + ); + return retorno; + }); + + if (!unalertedMessagesByChatId[chatId].length) delete unalertedMessagesByChatId[chatId]; }); - this.setState(({ notified }) => ({ - notified: { - ...notified, - ...notifiedToClear, - }, - }), () => { - Service.setNotified(PRIVATE_KEY, this.state.notified); + const lastUnalertedMessageTimestampByChat = {}; + Object.keys(unalertedMessagesByChatId).forEach((chatId) => { + lastUnalertedMessageTimestampByChat[chatId] = unalertedMessagesByChatId[chatId] + .reduce(Service.maxTimestampReducer, 0); }); + + // Keep track of chats that need to be alerted now (considering alert interval) + const chatsWithPendingAlerts = Object.keys(lastUnalertedMessageTimestampByChat) + .filter(chatId => lastUnalertedMessageTimestampByChat[chatId] + > ((lastAlertTimestampByChat[chatId] || 0) + ALERT_INTERVAL) + && !(chatId in pendingNotificationsByChat)); + + if (!chatsWithPendingAlerts.length) return; + + const newPendingNotificationsByChat = Object.assign({}, + ...chatsWithPendingAlerts.map(chatId => ({ [chatId]: unalertedMessagesByChatId[chatId] }))); + + // Mark messages as alerted + const newLastAlertTimestampByChat = { ...lastAlertTimestampByChat }; + + chatsWithPendingAlerts.forEach( + (chatId) => { + newLastAlertTimestampByChat[chatId] = lastUnalertedMessageTimestampByChat[chatId]; + }, + ); + + this.setChatMessagesState(newPendingNotificationsByChat, newLastAlertTimestampByChat); } + setLastAlertTimestamp(newLastAlertTimestamp) { + const { lastAlertTimestamp } = this.state; + if (newLastAlertTimestamp > 0 && lastAlertTimestamp !== newLastAlertTimestamp) { + this.setState({ lastAlertTimestamp: newLastAlertTimestamp }); + } + } + + setChatMessagesState(pendingNotificationsByChat, lastAlertTimestampByChat) { + this.setState({ pendingNotificationsByChat, lastAlertTimestampByChat }); + } + + mapContentText(message) { const { intl, @@ -86,12 +139,13 @@ class ChatAlert extends PureComponent { const contentMessage = message .map((content) => { if (content.text === 'PUBLIC_CHAT_CLEAR') return intl.formatMessage(intlMessages.publicChatClear); - /* this code is to remove html tags that come in the server's messangens */ + /* this code is to remove html tags that come in the server's messages */ const tempDiv = document.createElement('div'); tempDiv.innerHTML = content.text; const textWithoutTag = tempDiv.innerText; return textWithoutTag; }); + return contentMessage; } @@ -102,155 +156,64 @@ class ChatAlert extends PureComponent {
{ this.mapContentText(message) - .reduce((acc, text) => [...acc, (
), text], []).splice(1) + .reduce((acc, text) => [...acc, (
), text], []) }
); } - notifyPrivateChat() { - const { - disableNotify, - openChats, - intl, - } = this.props; - - if (disableNotify) return; - - const hasUnread = ({ unreadCounter }) => unreadCounter > 0; - const isNotNotified = ({ id, unreadCounter }) => unreadCounter !== this.state.notified[id]; - const isPrivate = ({ id }) => id !== PUBLIC_KEY; - const thisChatClosed = ({ id }) => !Session.equals('idChatOpen', id); - - const chatsNotify = openChats - .filter(hasUnread) - .filter(isNotNotified) - .filter(isPrivate) - .filter(thisChatClosed) - .map(({ - id, - name, - unreadCounter, - ...rest - }) => ({ - ...rest, - name, - unreadCounter, - id, - message: intl.formatMessage(intlMessages.appToastChatPrivate), - })); - - return ( - - { - chatsNotify.map(({ id, message, name }) => { - const getChatmessages = UnreadMessages.getUnreadMessages(id) - .filter(({ fromTime, fromUserId }) => fromTime > (this.state.notified[fromUserId] || 0)); - - const reduceMessages = Service - .reduceAndMapGroupMessages(getChatmessages); - - if (!reduceMessages.length) return null; - - const flatMessages = _.flatten(reduceMessages - .map(msg => this.createMessage(name, msg.content))); - const limitingMessages = flatMessages; - - return ({message}} - onOpen={() => { - this.setState(({ notified }) => ({ - notified: { - ...notified, - [id]: new Date().getTime(), - }, - }), () => { - Service.setNotified(PRIVATE_KEY, this.state.notified); - }); - }} - />); - }) - } - - ); - } - - notifyPublicChat() { - const { - publicUserId, - intl, - disableNotify, - } = this.props; - - const publicUnread = UnreadMessages.getUnreadMessages(publicUserId); - const publicUnreadReduced = Service.reduceAndMapGroupMessages(publicUnread); - - if (disableNotify) return; - if (!Service.hasUnreadMessages(publicUserId)) return; - if (Session.equals('idChatOpen', PUBLIC_KEY)) return; - - const checkIfBeenNotified = ({ sender, time }) => - time > (this.state.publicNotified[sender.id] || 0); - - const chatsNotify = publicUnreadReduced - .map(msg => ({ - ...msg, - sender: { - name: msg.sender ? msg.sender.name : intl.formatMessage(intlMessages.appToastChatSystem), - ...msg.sender, - }, - })) - .filter(checkIfBeenNotified); - return ( - - { - chatsNotify.map(({ sender, time, content }) => - ( - { intl.formatMessage(intlMessages.appToastChatPublic) } - - } - content={this.createMessage(sender.name, content)} - onOpen={() => { - this.setState(({ notified, publicNotified }) => ({ - ...notified, - publicNotified: { - ...publicNotified, - [sender.id]: time, - }, - }), () => { - Service.setNotified(PUBLIC_KEY, this.state.publicNotified); - }); - }} - />)) - } - - ); - } - render() { const { - openChats, - disableAudio, + audioAlertDisabled, + pushAlertDisabled, + intl, } = this.props; - const unreadMessagesCount = openChats - .map(chat => chat.unreadCounter) - .reduce((a, b) => a + b, 0); - const shouldPlayAudio = !disableAudio && unreadMessagesCount > 0; + const { + pendingNotificationsByChat, + } = this.state; + + const shouldPlay = Object.keys(pendingNotificationsByChat).length > 0; return ( - - { this.notifyPublicChat() } - { this.notifyPrivateChat() } + {!audioAlertDisabled ? : null} + { + !pushAlertDisabled + ? Object.keys(pendingNotificationsByChat) + .map((chatId) => { + // Only display the latest group of messages (up to 5 messages) + const reducedMessage = Service + .reduceAndMapGroupMessages(pendingNotificationsByChat[chatId].slice(-5)).pop(); + + if (!reducedMessage) return null; + + const content = this + .createMessage(reducedMessage.sender.name, reducedMessage.content); + + return ( + {intl.formatMessage(intlMessages.appToastChatPublic)} + : {intl.formatMessage(intlMessages.appToastChatPrivate)} + } + onOpen={ + () => { + let pendingNotifications = pendingNotificationsByChat; + delete pendingNotifications[chatId]; + pendingNotifications = { ...pendingNotifications }; + this.setState({ pendingNotificationsByChat: pendingNotifications }); + }} + alertDuration={ALERT_DURATION} + /> + ); + }) + : null + } ); } diff --git a/bigbluebutton-html5/imports/ui/components/chat/alert/container.jsx b/bigbluebutton-html5/imports/ui/components/chat/alert/container.jsx index bddf44f912..35ba003d35 100755 --- a/bigbluebutton-html5/imports/ui/components/chat/alert/container.jsx +++ b/bigbluebutton-html5/imports/ui/components/chat/alert/container.jsx @@ -3,6 +3,9 @@ import { withTracker } from 'meteor/react-meteor-data'; import UserListService from '/imports/ui/components/user-list/service'; import Settings from '/imports/ui/services/settings'; import ChatAlert from './component'; +import ChatService from '/imports/ui/components/chat/service.js'; +import Auth from '/imports/ui/services/auth'; +import Users from '/imports/api/users'; const ChatAlertContainer = props => ( @@ -10,12 +13,13 @@ const ChatAlertContainer = props => ( export default withTracker(() => { const AppSettings = Settings.application; - const openChats = UserListService.getOpenChats(); - + const activeChats = UserListService.getActiveChats(); + const loginTime = Users.findOne({ userId: Auth.userID }).loginTime; return { - disableAudio: !AppSettings.chatAudioAlerts, - disableNotify: !AppSettings.chatPushAlerts, - openChats, + audioAlertDisabled: !AppSettings.chatAudioAlerts, + pushAlertDisabled: !AppSettings.chatPushAlerts, + activeChats, publicUserId: Meteor.settings.public.chat.public_group_id, + joinTimestamp: loginTime, }; })(memo(ChatAlertContainer)); diff --git a/bigbluebutton-html5/imports/ui/components/chat/alert/push-alert/component.jsx b/bigbluebutton-html5/imports/ui/components/chat/alert/push-alert/component.jsx index 562f7a0db1..96aa4184bd 100755 --- a/bigbluebutton-html5/imports/ui/components/chat/alert/push-alert/component.jsx +++ b/bigbluebutton-html5/imports/ui/components/chat/alert/push-alert/component.jsx @@ -4,37 +4,37 @@ import _ from 'lodash'; import injectNotify from '/imports/ui/components/toast/inject-notify/component'; import { Session } from 'meteor/session'; -const ALERT_INTERVAL = 2000; // 2 seconds -const ALERT_LIFETIME = 4000; // 4 seconds const propTypes = { notify: PropTypes.func.isRequired, onOpen: PropTypes.func.isRequired, chatId: PropTypes.string.isRequired, - message: PropTypes.node.isRequired, + title: PropTypes.node.isRequired, content: PropTypes.node.isRequired, + alertDuration: PropTypes.number.isRequired, }; class ChatPushAlert extends PureComponent { - static link(message, chatId) { + static link(title, chatId) { return (
{ Session.set('openPanel', 'chat'); Session.set('idChatOpen', chatId); }} > - { message } + {title}
); } constructor(props) { super(props); - this.showNotify = _.debounce(this.showNotify.bind(this), ALERT_INTERVAL); + this.showNotify = this.showNotify.bind(this); this.componentDidMount = this.showNotify; this.componentDidUpdate = this.showNotify; @@ -45,15 +45,16 @@ class ChatPushAlert extends PureComponent { notify, onOpen, chatId, - message, + title, content, + alertDuration, } = this.props; return notify( - ChatPushAlert.link(message, chatId), + ChatPushAlert.link(title, chatId), 'info', 'chat', - { onOpen, autoClose: ALERT_LIFETIME }, + { onOpen, autoClose: alertDuration }, ChatPushAlert.link(content, chatId), true, ); diff --git a/bigbluebutton-html5/imports/ui/components/chat/component.jsx b/bigbluebutton-html5/imports/ui/components/chat/component.jsx index 1554d7f32f..2d01504907 100644 --- a/bigbluebutton-html5/imports/ui/components/chat/component.jsx +++ b/bigbluebutton-html5/imports/ui/components/chat/component.jsx @@ -56,6 +56,7 @@ const Chat = (props) => { >