From cd34f6de727d5b3858420cff3c9d3c5995c4e75c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alexandre=20K=C3=BChn?= Date: Tue, 12 Jun 2018 15:46:46 +0200 Subject: [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. --- .../mail/static/src/js/models/document_thread.js | 276 +++++++++++++++++++++ 1 file changed, 276 insertions(+) create mode 100644 addons/mail/static/src/js/models/document_thread.js (limited to 'addons/mail/static/src/js/models/document_thread.js') diff --git a/addons/mail/static/src/js/models/document_thread.js b/addons/mail/static/src/js/models/document_thread.js new file mode 100644 index 00000000000..b9671107d9d --- /dev/null +++ b/addons/mail/static/src/js/models/document_thread.js @@ -0,0 +1,276 @@ +odoo.define('mail.model.DocumentThread', function (require) { +"use strict"; + +var Thread = require('mail.model.Thread'); + +var session = require('web.session'); + +/** + * These threads represent threads that are directly attached to documents. + * It is sometimes called the "chatter", but technically this is just the + * part of the chatter containing the thread, as the chatter also contains + * the activity and listener fields. + * + * Note that there are still some hacks in the handling of document threads, + * due to document threads are instantiated from fetched messages, either from + * mail.ThreadField or by fetching messages in Inbox. + * + * Also, because messages are not sent on the longpoll bus for this kind of + * threads, it stores the list of message IDs that it contains, which should + * be up-to-date whenever someone wants to get the messages in the document + * thread. + */ +var DocumentThread = Thread.extend({ + + /** + * @override + * @param {string} [data.messagesIDs] the list of message ids linked + * to the document (if not given, they will be fetched before fetching the + * messages) + * @param {string} data.model + * @param {integer} data.resID + */ + init: function (parent, data) { + data.id = data.resModel + '_' + data.resID; + data.type = 'document_thread'; + + this._super.apply(this, arguments); + + // ID of the related document + this._documentID = data.resID; + // model of the related document + this._documentModel = data.resModel; + // used to handle history of messages + this._messageIDs = data.messageIDs || []; + // list of loaded messages + this._messages = []; + + // if the messageIDs haven't been given, fetch them before fetching the + // messages + this._mustFetchMessageIDs = !_.isArray(data.messageIDs); + }, + + //-------------------------------------------------------------------------- + // Public + //-------------------------------------------------------------------------- + + /** + * Add this message to this document thread. + * + * @override + * @param {mail.model.Message} message + */ + addMessage: function (message) { + // update internal list of messages + this._messages.push(message); + this._messages = _.sortBy(this._messages, function (msg) { + return msg.getID(); + }); + // update message ids associated to this document thread + if (!_.contains(this._messageIDs, message.getID())) { + this._messageIDs.push(message.getID()); + } + }, + /** + * Get the model name of the document that is linked to this document thread + * + * @returns {string} + */ + getDocumentModel: function () { + return this._documentModel; + }, + /** + * Get the ID of the document that is linked to this document thread + * + * @returns {integer} + */ + getDocumentID: function () { + return this._documentID; + }, + /** + * Get he list of message IDs that this document threads contain. + * + * @returns {integer[]} + */ + getMessageIDs: function () { + return this._messageIDs; + }, + /** + * + * Get messages of the document thread + * + * @override + * @param {Object} options + * @param {boolean} [options.forceFetch] if true, fetch anyway, as user + * clicked on 'load more'. + * @returns {$.Promise} + */ + getMessages: function (options) { + var self = this; + return this._fetchMessages(options).then(function () { + self.call('mail_service', 'markMessagesAsRead', self._messageIDs); + return self._messages; + }); + }, + /** + * States whether the thread is linked to a document + * Document thread's are always linked to a document. + * + * @override + * @returns {boolean} + */ + isLinkedToDocument: function () { + return true; + }, + /** + * Overrides this method so that all the messages of this document thread + * are marked as read on the server. + * + * @override + * @returns {$.Promise} resolved when messages have been marked as read on + * the server. + */ + markAsRead: function () { + this._super.apply(this, arguments); + return this.call('mail_service', 'markMessagesAsRead', this._messageIDs); + }, + /** + * Post message for document thread + * + * @override + * @param {Object} data data related to the new message + * @return {$.Promise} resolved when the message has been sent to + * the server, with the object message that has been sent to the server. + */ + postMessage: function (data) { + var self = this; + var resModel = this.getDocumentModel(); + var resID = this.getDocumentID(); + return this._super.apply(this, arguments) + .then(function (messageData) { + _.extend(messageData, { + context: data.context, + message_type: data.message_type, + subtype: data.subtype, + subtype_id: data.subtype_id, + }); + return self._rpc({ + model: resModel, + method: 'message_post', + args: [resID], + kwargs: messageData, + }) + .then(function (messageID) { + return self._rpc({ + model: 'mail.message', + method: 'message_format', + args: [[messageID]], + }) + .then(function (messages) { + messages[0].model = resModel; + messages[0].res_id = resID; + self.call('mail_service', 'addMessage', messages[0]); + return messages[0]; + }); + }); + }); + }, + /** + * Set list of message IDs of this document thread + * + * Useful in order to handle message history of the document thread, + * in particular to fetch messages when necessary and/or display 'load more'. + * + * @param {integer[]} messageIDs + */ + setMessageIDs: function (messageIDs) { + this._mustFetchMessageIDs = false; + this._messageIDs = messageIDs; + }, + /** + * Set the name of this document thread + * + * This is useful if the name of the document related to the document thread + * has changed + * + * @param {string} newName + */ + setName: function (newName) { + this._name = newName; + }, + + //-------------------------------------------------------------------------- + // Private + //-------------------------------------------------------------------------- + + /** + * Get most up to date messageIDs + * + * @private + * @returns {$.Promise} resolved when message IDs have been fetched and set + * in the model + */ + _fetchMessageIDs: function () { + var self = this; + var resID = this.getDocumentID(); + if (!resID || !this._mustFetchMessageIDs) { + return $.when(); + } + return this._rpc({ + model: this.getDocumentModel(), + method: 'read', + args: [[resID], ['message_ids']], + }).then(function (result) { + self.setMessageIDs(result[0].message_ids); + }); + }, + /** + * @override + * @private + * @param {Object} options + * @param {boolean} [options.forceFetch] + * @returns {$.Promise} resolved when messages have been fetched + document + * thread has updated messages + */ + _fetchMessages: function (options) { + var self = this; + return this._fetchMessageIDs().then(function () { + var messageIDs = self._messageIDs; + var loadedMessages = _.filter(self._messages, function (message) { + return _.contains(messageIDs, message.getID()); + }); + var loadedMessageIDs = _.map(loadedMessages, function (message) { + return message.getID(); + }); + + options = options || {}; + var shouldFetch = _.difference( + messageIDs.slice(0, self._FETCH_LIMIT), + loadedMessageIDs + ).length > 0; + if (options.forceFetch || shouldFetch) { + var idsToLoad = _.difference(messageIDs, loadedMessageIDs) + .slice(0, self._FETCH_LIMIT); + return self._rpc({ + model: 'mail.message', + method: 'message_format', + args: [idsToLoad], + context: session.user_context, + }) + .then(function (messagesData) { + _.each(messagesData, function (messageData) { + self.call('mail_service', 'addMessage', messageData, { silent: true }); + }); + }); + } else { + return $.when(); + } + + }); + }, + +}); + +return DocumentThread; + +}); -- cgit v1.2.3