diff options
| author | Alexandre Kühn <aku@odoo.com> | 2018-06-12 15:46:46 +0200 |
|---|---|---|
| committer | Alexandre Kühn <aku@odoo.com> | 2018-07-02 15:06:54 +0200 |
| commit | cd34f6de727d5b3858420cff3c9d3c5995c4e75c (patch) | |
| tree | 8d25861f98a038b71728355375d634a5c178290e /addons/mail/static/src/js/services/mail_notification_manager.js | |
| parent | ee9c1f52c316c83775969a3c9038e7e9f560f7e7 (diff) | |
[REF] mail: JS mail refactoring
----------------------
Summary
----------------------
The purpose of this commit is to improve the JS code of the `mail` module.
It applies the new coding guidelines and makes some changes on the design
of some modules, such as the old ChatManager module.
Here is a short summary of the changes that have been made:
1. New coding guidelines
- snake_case to camelCase
- prefix private attributes and methods with '_'
- jsdoc on most methods
- one class per module
2. Rename/Merge some classes
- 'chat manager' becomes 'mail manager' (internal) and 'mail service' (external)
- 'chat window manager' is now included in mail manager
- 'thread' widget now named 'thread widget'
3. New model abstraction for mail objects:
- modules 'mail.model.*'
- modeling:
0..1 0..1 * *
ThreadWindow <------> Thread <-------> Message
/ \
/ \
Thread With Cache Document Thread
/ | \
/ | \
Mailbox Channel Support Channel
|
|
DM
- Thread: the superclass of threads.
- ThreadWindow: the window component of a thread.
- Message: mail objects representing messages.
- Thread With Cache: threads that can be used with search view (Discuss app compatible).
- Document Thread: represents the thread part of a chatter.
- Mailbox: represents what was previously called 'static' channel, e.g. 'Inbox'.
- Channel: mail objects representing channels, including livechat.
- DM: special kind of Channel for 1:1 communication in the backend.
- Support Channel: special channel for im_support module.
This new modeling approach let us easily add features on all threads, such as the
possibility to put any thread in a small window.
----------------------
Known issues
----------------------
[Already Present in Master]
1. When the Discuss app is in the background with 'Inbox' as the selected
Thread, when clicking on a document thread preview in the messaging menu
of the systray, the rainbow man appears.
2. When a document with the chatter is in the background, when receiving an
inbox notification from this document thread, the document thread is
automatically marked as read, which removes the notification right away.
3. Sometimes, opening a DM window from the "blank" thread window does not work.
4. Reply-to feature on Inbox is not working: no message is sent in the document
thread.
5. On the first login of admin user with demo data, the inbox counter is wrong
(it displays 6, instead of 3).
Explanation after investigation:
> On page load, it fetches the correct number of Inbox messages (3),
but the server notifies of 3 needaction messages right away,
so it wrongly assumes these are new needaction messages.
> Not possible for web client to detect that these messages should not
increment the Inbox counter while keeping same API.
6. Notifications for new document thread messages only work when the user sets
'handle with Odoo' for the Notification Management in the preferences.
> due to notifications on the longpoll bus for document thread
messages that come from needaction notifications.
> requires server-side changes to send notification on the longpoll
bus to mentionned user.
[New]
7. When receiving a message on a unjoined channel, thread window flickers
('open' > 'close' > 'open')
Explanation after investigation:
> JS logic:
a) On auto-join, ask server to join the channel and get channel infos.
b) The info tells the channel is not detached, but JS code makes decision
to detach it, and tells server the channel is now detached.
c) From (a), server notifies on longpoll bus the state of channel, which is
not detached. The web client thinks that the window state of the channel
has been changed somewhere else, and the channel is now closed.
d) From (b), server notifies on longpoll bus the state of channel, which is
detached. The web client opens the thread window of this channel.
> The flicker didn't occur before refactoring because the web client was only
updating the model of channel when it receives the longpoll notification.
> Server behaviour on 1st longpoll notification is necessary for cross-tab
synchronization for channel window state.
> New design implies that model and view should be synchronized, hence the
issue now.
> Solution: remove server-side thread window synchronization and replace with
client-side synchronization.
----------------------
Hacks
----------------------
The module `im_livechat` now uses mail objects that are compatible with Message
and Window objects:
Modeling for messages:
AbstractMessage
/ \
/ \
LivechatMessage Message
- AbstractMessage: message compatible with the thread widget.
- LivechatMessage: message used by im_livechat.
- Message: message used with the mail manager.
Modeling for thread windows:
AbstractThreadWindow
/ \
/ \
LivechatWindow ThreadWindow
- AbstractThreadWindow: behaviour share between all types of thread windows.
- LivechatWindow: window used by im_livechat.
- ThreadWindow: window used with mail_manager.
The reason for these hacks are twofold:
1. Use the thread widget in the frontend and livechat external lib bundles.
2. Do not have a dependency with the mail manager in the frontend and external
lib bundles.
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.js | 484 |
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; + +}); |
