summaryrefslogtreecommitdiff
path: root/addons/mail/static/src/js/services/mail_notification_manager.js
diff options
context:
space:
mode:
Diffstat (limited to 'addons/mail/static/src/js/services/mail_notification_manager.js')
-rw-r--r--addons/mail/static/src/js/services/mail_notification_manager.js484
1 files changed, 484 insertions, 0 deletions
diff --git a/addons/mail/static/src/js/services/mail_notification_manager.js b/addons/mail/static/src/js/services/mail_notification_manager.js
new file mode 100644
index 00000000000..cd0ff9223a0
--- /dev/null
+++ b/addons/mail/static/src/js/services/mail_notification_manager.js
@@ -0,0 +1,484 @@
+odoo.define('mail.Manager.Notification', function (require) {
+"use strict";
+
+/**
+ * Mail Notification Manager
+ *
+ * This part of the mail manager is responsible for receiving notifications on
+ * the longpoll bus, which are data received from the server.
+ */
+var MailManager = require('mail.Manager');
+var MailFailure = require('mail.model.MailFailure');
+
+var core = require('web.core');
+
+var _t = core._t;
+
+MailManager.include({
+
+ start: function () {
+ this._super.apply(this, arguments);
+ this._busBus.on('notification', this, this._onNotification);
+ },
+
+ //--------------------------------------------------------------------------
+ // Private
+ //--------------------------------------------------------------------------
+
+ /**
+ * Remove channel notifications if there is an unsubscribe notification
+ * on this channel.
+ *
+ * @private
+ * @param {Object[]} notifications
+ * @returns {Object[]} notifications filtered of channel notifications
+ * matching unsubscribe notifs
+ */
+ _filterNotificationsOnUnsubscribe: function (notifications) {
+ var unsubscribedNotif = _.find(notifications, function (notif) {
+ return notif[1].info === 'unsubscribe';
+ });
+ if (unsubscribedNotif) {
+ notifications = _.reject(notifications, function (notif) {
+ return notif[0][1] === 'mail.channel' &&
+ notif[0][2] === unsubscribedNotif[1].id;
+ });
+ }
+ return notifications;
+ },
+ /**
+ * Called when a new or updated message is received on a channel
+ *
+ * @private
+ * @param {Object} messageData
+ * @param {Array} messageData.channel_ids list of integers and strings,
+ * where strings for static channels, e.g. 'mailbox_inbox'.
+ */
+ _handleChannelNotification: function (messageData) {
+ var self = this;
+ var def;
+ var channelAlreadyInCache = true;
+ if (messageData.channel_ids.length === 1) {
+ channelAlreadyInCache = !!this.getChannel(messageData.channel_ids[0]);
+ def = this.joinChannel(messageData.channel_ids[0], { autoswitch: false });
+ } else {
+ def = $.when();
+ }
+ def.then(function () {
+ // don't increment unread if channel wasn't in cache yet as
+ // its unread counter has just been fetched
+ self.addMessage(messageData, {
+ showNotification: true,
+ incrementUnread: channelAlreadyInCache
+ });
+ });
+ },
+ /**
+ * On message becoming a need action (pinned to inbox)
+ *
+ * @private
+ * @param {Object} messageData
+ * @param {integer[]} messageData.channel_ids
+ */
+ _handleNeedactionNotification: function (messageData) {
+ var self = this;
+ var inbox = this.getMailbox('inbox');
+ var message = this.addMessage(messageData, {
+ incrementUnread: true,
+ showNotification: true,
+ });
+ inbox.incrementMailboxCounter();
+ _.each(message.getThreadIDs(), function (threadID) {
+ var channel = self.getChannel(threadID);
+ if (channel) {
+ channel.incrementNeedactionCounter();
+ }
+ });
+ this._mailBus.trigger('update_needaction', inbox.getMailboxCounter());
+ },
+ /**
+ * Called when an activity record has been updated on the server
+ *
+ * @private
+ * @param {Object} data key, value to decide activity created or deleted
+ */
+ _handlePartnerActivityUpdateNotification: function (data) {
+ this._mailBus.trigger('activity_updated', data);
+ },
+ /**
+ * Called when a channel has been seen, and the server responses with the
+ * last message seen. Useful in order to track last message seen.
+ *
+ * @private
+ * @param {Object} data
+ * @param {integer} data.id ID of a channel
+ * @param {integer} [data.last_message_id] mandatory if 'id' refers to an
+ * existing channel.
+ */
+ _handlePartnerChannelSeenNotification: function (data) {
+ var channel = this.getChannel(data.id);
+ if (channel) {
+ channel.setLastSeenMessageID(data.last_message_id);
+ if (channel.hasUnreadMessages()) {
+ channel.resetUnreadCounter();
+ }
+ }
+ },
+ /**
+ * Called when receiving a channel state as a partner notification:
+ *
+ * - if it is a new channel, it means we have been invited to this channel
+ * - if it is an existing channel, it means the window state of the channel
+ * may have changed (as this state is stored server-side)
+ *
+ * @private
+ * @param {Object} channelData
+ * @param {integer} channelData.id
+ * @param {boolean} channelData.is_minimized
+ * @param {string} channelData.state
+ */
+ _handlePartnerChannelNotification: function (channelData) {
+ if (
+ (channelData.channel_type === 'channel') &&
+ (channelData.state === 'open')
+ ) {
+ // invited to a new channel
+ this._addChannel(channelData, { autoswitch: false });
+ if (
+ !channelData.is_minimized &&
+ channelData.info !== 'creation'
+ ) {
+ this.do_notify(
+ _t("Invitation"),
+ _t("You have been invited to: ") + channelData.name);
+ }
+ }
+ var channel = this.getChannel(channelData.id);
+ if (channel) {
+ channel.updateWindowState({
+ folded: channelData.state === 'folded' ? true : false,
+ detached: channelData.is_minimized,
+ });
+ }
+ },
+ /**
+ * Add or remove failure when receiving a failure update message
+ *
+ * @private
+ * @param {Object} data
+ * @param {Object[]} data.elements list of mail failure data
+ * @param {string} data.elements[].message_id ID of related message that
+ * has a mail failure.
+ * @param {Array} data.elements[].notifications list of notifications
+ * that is related to a mail failure.
+ * @param {string} data.elements[].notifications[0] sending state of a mail
+ * failure (e.g. 'exception').
+ */
+ _handlePartnerMailFailureNotification: function (datas) {
+ var self = this;
+ _.each(datas.elements, function (data) {
+ var isNewFailure = _.some(data.notifications, function (notif) {
+ return notif[0] === 'exception' || notif[0] === 'bounce';
+ });
+ var matchedFailure = _.find(self._mailFailures, function (failure) {
+ return failure.getMessageID() === data.message_id;
+ });
+ if (matchedFailure) {
+ var index = _.findIndex(self._mailFailures, matchedFailure);
+ if (isNewFailure) {
+ self._mailFailures[index] = new MailFailure(self, data);
+ } else {
+ self._mailFailures.splice(index, 1);
+ }
+ } else if (isNewFailure) {
+ self._mailFailures.push(new MailFailure(self, data));
+ }
+ var message = _.find(self._messages, function (msg) {
+ return msg.getID() === data.message_id;
+ });
+ if (message) {
+ if (isNewFailure) {
+ message.updateCustomerEmailStatus('exception');
+ } else {
+ message.updateCustomerEmailStatus('sent');
+ }
+ self._updateMessageNotificationStatus(data, message);
+ self._mailBus.trigger('update_message', message);
+ }
+ });
+ this._mailBus.trigger('update_needaction', this.needactionCounter);
+ },
+ /**
+ * Updates mailbox_inbox when a message has marked as read.
+ *
+ * @private
+ * @param {Object} data
+ * @param {integer[]} [data.channel_ids]
+ * @param {integer[]} [data.message_ids]
+ * @param {string} [data.type]
+ */
+ _handlePartnerMarkAsReadNotification: function (data) {
+ var self = this;
+ _.each(data.message_ids, function (messageID) {
+ var message = _.find(self._messages, function (msg) {
+ return msg.getID() === messageID;
+ });
+ if (message) {
+ self._removeMessageFromThread('mailbox_inbox', message);
+ self._mailBus.trigger('update_message', message, data.type);
+ }
+ });
+ if (data.channel_ids) {
+ _.each(data.channel_ids, function (channelID) {
+ var channel = self.getChannel(channelID);
+ if (channel) {
+ channel.decrementNeedactionCounter(data.message_ids.length, 0);
+ }
+ });
+ } else {
+ // if no channel_ids specified, this is a 'mark all read' in inbox
+ _.each(this._threads, function (thread) {
+ thread.resetNeedactionCounter();
+ });
+ }
+ var inbox = this.getMailbox('inbox');
+ inbox.decrementMailboxCounter(data.message_ids.length);
+ this._mailBus.trigger('update_needaction', inbox.getMailboxCounter());
+ },
+ /**
+ * On receiving a message made by the current user, on a moderated channel,
+ * which is pending moderation.
+ *
+ * @private
+ * @param {Object} data
+ * @param {Object} data.message server-side data of the message
+ */
+ _handlePartnerMessageAuthorNotification: function (data) {
+ this.addMessage(data.message);
+ },
+ /**
+ * Notification to delete several messages locally
+ * Useful when a pending moderation message has been rejected, so that
+ * this message should not be displayed anymore.
+ *
+ * @private
+ * @param {Object} data
+ * @param {Object[]} [data.message_ids] IDs of messages to delete locally.
+ */
+ _handlePartnerMessageDeletionNotification: function (data) {
+ var self = this;
+ _.each(data.message_ids, function (messageID) {
+ var message = self.getMessage(messageID);
+ if (message) {
+ message.setModerationStatus('rejected');
+ }
+ });
+ },
+ /**
+ * On receiving a message pending moderation, and current user is moderator
+ * of such message.
+ *
+ * @param {Object} data notification data
+ * @param {Object} data.message data of message
+ */
+ _handlePartnerMessageModeratorNotification: function (data) {
+ this.addMessage(data.message);
+ this._mailBus.trigger('update_moderation_counter');
+ },
+ /**
+ * On receiving a notification that is specific to a user
+ *
+ * @private
+ * @param {Object} data structure depending on the type
+ * @param {integer} data.id
+ */
+ _handlePartnerNotification: function (data) {
+ if (data.info === 'unsubscribe') {
+ this._handlePartnerUnsubscribeNotification(data);
+ } else if (data.type === 'toggle_star') {
+ this._handlePartnerToggleStarNotification(data);
+ } else if (data.type === 'mark_as_read') {
+ this._handlePartnerMarkAsReadNotification(data);
+ } else if (data.type === 'moderator') {
+ this._handlePartnerMessageModeratorNotification(data);
+ } else if (data.type === 'author') {
+ this._handlePartnerMessageAuthorNotification(data);
+ } else if (data.type === 'deletion') {
+ this._handlePartnerMessageDeletionNotification(data);
+ } else if (data.info === 'channel_seen') {
+ this._handlePartnerChannelSeenNotification(data);
+ } else if (data.info === 'transient_message') {
+ this._handlePartnerTransientMessageNotification(data);
+ } else if (data.type === 'activity_updated') {
+ this._handlePartnerActivityUpdateNotification(data);
+ } else if (data.type === 'mail_failure') {
+ this._handlePartnerMailFailureNotification(data);
+ } else {
+ this._handlePartnerChannelNotification(data);
+ }
+ },
+ /**
+ * On toggling on or off the star status of one or several messages.
+ * As the information is stored server-side, the web client must adapt
+ * itself from server's data on the messages.
+ *
+ * @private
+ * @param {Object} data
+ * @param {integer[]} data.message_ids IDs of messages that have a change
+ * of their starred status.
+ * @param {boolean} data.starred states whether the messages with id in
+ * `data.message_ids` have become either starred or unstarred
+ */
+ _handlePartnerToggleStarNotification: function (data) {
+ var self = this;
+ var starred = this.getMailbox('starred');
+ _.each(data.message_ids, function (messageID) {
+ var message = _.find(self._messages, function (msg) {
+ return msg.getID() === messageID;
+ });
+ if (message) {
+ message.setStarred(data.starred);
+ if (!message.isStarred()) {
+ self._removeMessageFromThread('mailbox_starred', message);
+ } else {
+ self._addMessageToThreads(message, []);
+ var channelStarred = self.getMailbox('starred');
+ channelStarred.invalidateCaches();
+ }
+ self._mailBus.trigger('update_message', message);
+ }
+ });
+
+ if (data.starred) {
+ // increase starred counter if message is marked as star
+ starred.incrementMailboxCounter(data.message_ids.length);
+ } else {
+ // decrease starred counter if message is remove from starred
+ // if unstar_all then it will set to 0.
+ starred.decrementMailboxCounter(data.message_ids.length);
+ }
+
+ this._mailBus.trigger('update_starred', starred.getMailboxCounter());
+ },
+ /**
+ * On receiving a transient message, i.e. a message which does not come from
+ * a member of the channel. Usually a log message, such as one generated
+ * from a command with ('/').
+ *
+ * @private
+ * @param {Object} data
+ * @param {string} data.author_id
+ */
+ _handlePartnerTransientMessageNotification: function (data) {
+ var lastMessage = _.last(this._messages);
+ data.id = (lastMessage ? lastMessage.getID() : 0) + 0.01;
+ data.author_id = data.author_id || this.getOdoobotID();
+ this.addMessage(data);
+ },
+ /**
+ * On receiving a unsubscribe from channel notification, confirm
+ * unsubscription from channel and adapt screen accordingly.
+ *
+ * @private
+ * @param {Object} data
+ * @param {Object} data.id ID of the unsubscribed channel
+ */
+ _handlePartnerUnsubscribeNotification: function (data) {
+ var channel = this.getChannel(data.id);
+ if (channel) {
+ var message;
+ if (_.contains(['public', 'private'], channel.getType())) {
+ message = _.str.sprintf(
+ _t("You unsubscribed from <b>%s</b>."),
+ channel.getName()
+ );
+ } else {
+ message = _.str.sprintf(
+ _t("You unpinned your conversation with <b>%s</b>."),
+ channel.getName()
+ );
+ }
+ this._removeChannel(channel);
+ this._mailBus.trigger('unsubscribe_from_channel', data.id);
+ this.do_notify(_("Unsubscribed"), message);
+ }
+ },
+ /**
+ * On receiving an update on user status (e.g. becoming 'online', 'offline',
+ * 'idle', etc.).
+ *
+ * @private
+ * @param {Object} data partner infos
+ * @param {integer} data.id
+ * @param {string} data.im_status
+ */
+ _handlePresenceNotification: function (data) {
+ var dm = this.getDmFromPartnerID(data.id);
+ if (dm) {
+ dm.setStatus(data.im_status);
+ this._mailBus.trigger('update_dm_presence', dm);
+ }
+ },
+ /**
+ * Update the message notification status of message based on update_message
+ *
+ * @private
+ * @param {Object} data
+ * @param {Object[]} data.notifications
+ * @param {mail.model.Message} message
+ */
+ _updateMessageNotificationStatus: function (data, message) {
+ _.each(data.notifications, function (notif, id) {
+ var partnerName = notif[1];
+ var notifStatus = notif[0];
+ var res = _.find(message.getCustomerEmailData(), function (entry) {
+ return entry[0] === parseInt(id);
+ });
+ if (res) {
+ res[2] = notifStatus;
+ } else {
+ message.addCustomerEmailData([parseInt(id), partnerName, notifStatus]);
+ }
+ });
+ },
+
+ //--------------------------------------------------------------------------
+ // Handlers
+ //--------------------------------------------------------------------------
+
+ /**
+ * Notification handlers
+ * Sometimes, the web client receives unsubscribe notification and an extra
+ * notification on that channel. This is then followed by an attempt to
+ * rejoin the channel that we just left. The next few lines remove the
+ * extra notification to prevent that situation to occur.
+ *
+ * @private
+ * @param {Array} notifs
+ */
+ _onNotification: function (notifs) {
+ var self = this;
+ notifs = this._filterNotificationsOnUnsubscribe(notifs);
+ _.each(notifs, function (notif) {
+ var model = notif[0][1];
+ if (model === 'ir.needaction') {
+ // new message in the inbox
+ self._handleNeedactionNotification(notif[1]);
+ } else if (model === 'mail.channel') {
+ // new message in a channel
+ self._handleChannelNotification(notif[1]);
+ } else if (model === 'res.partner') {
+ // channel joined/left, message marked as read/(un)starred,
+ // chat open/closed, moderation specific, etc.
+ self._handlePartnerNotification(notif[1]);
+ } else if (model === 'bus.presence') {
+ // update presence of users
+ self._handlePresenceNotification(notif[1]);
+ }
+ });
+ },
+});
+
+return MailManager;
+
+});