summaryrefslogtreecommitdiff
path: root/addons/mail/static/src/js/models/threads
diff options
context:
space:
mode:
Diffstat (limited to 'addons/mail/static/src/js/models/threads')
-rw-r--r--addons/mail/static/src/js/models/threads/channel.js442
-rw-r--r--addons/mail/static/src/js/models/threads/dm.js94
-rw-r--r--addons/mail/static/src/js/models/threads/document_thread.js317
-rw-r--r--addons/mail/static/src/js/models/threads/mailbox.js213
-rw-r--r--addons/mail/static/src/js/models/threads/thread.js446
-rw-r--r--addons/mail/static/src/js/models/threads/thread_with_cache.js204
6 files changed, 1716 insertions, 0 deletions
diff --git a/addons/mail/static/src/js/models/threads/channel.js b/addons/mail/static/src/js/models/threads/channel.js
new file mode 100644
index 00000000000..e63cab2f907
--- /dev/null
+++ b/addons/mail/static/src/js/models/threads/channel.js
@@ -0,0 +1,442 @@
+odoo.define('mail.model.Channel', function (require) {
+"use strict";
+
+var ThreadWithCache = require('mail.model.ThreadWithCache');
+var mailUtils = require('mail.utils');
+
+var time = require('web.time');
+
+/**
+ * This class represent channels in JS. In this context, the word channel
+ * has the same meaning of channel on the server, meaning that direct messages
+ * (DM) and livechats are also channels.
+ *
+ * Any piece of code in JS that make use of channels must ideally interact with
+ * such objects, instead of direct data from the server.
+ */
+var Channel = ThreadWithCache.extend({
+ /**
+ * @override
+ * @param {string} [data.anonymous_name]
+ * @param {string} data.channel_type
+ * @param {boolean} data.group_based_subscription
+ * @param {boolean} [data.is_minimized=false]
+ * @param {boolean} data.is_moderator whether the current user is
+ * moderator of this channel.
+ * @param {string} [data.last_message_date] date in server-format
+ * @param {boolean} [data.mass_mailing]
+ * @param {integer} [data.message_unread_counter]
+ * @param {string} [data.public] either 'public' or 'private'
+ * @param {string} data.state
+ * @param {string} [data.uuid]
+ * @param {Object} options
+ * @param {boolean} [options.autoswitch=true]
+ * @param {Object[]} commands
+ */
+ init: function (parent, data, options, commands) {
+ var self = this;
+ this._super.apply(this, arguments);
+
+ // If set, autoswitch channel on joining this channel in discuss
+ // the default behaviour is to autoswitch on join.
+ // exception: receiving channel or chat session notifications
+ //
+ this._autoswitch = 'autoswitch' in options ? options.autoswitch : true;
+ this._chat = undefined; // FIXME: could be dropped when livechat and DM are moved out of this class
+ this._commands = undefined;
+ this._detached = data.is_minimized;
+ this._directPartnerID = undefined;
+ this._folded = data.state === 'folded';
+ // if set: hide 'Leave channel' button
+ this._groupBasedSubscription = data.group_based_subscription;
+ this._isModerator = data.is_moderator;
+ this._lastMessageDate = undefined;
+ this._massMailing = data.mass_mailing;
+ // Deferred that is resolved on fetched members of this channel.
+ this._membersDef = undefined;
+ this._moderation = data.is_moderation;
+ // number of messages that are 'needaction', which is equivalent to the
+ // number of messages in this channel that are in inbox.
+ this._needactionCounter = data.message_needaction_counter || 0;
+ this._serverType = data.channel_type;
+ this._status = undefined;
+ this._throttleFetchSeen = _.throttle(this._fetchSeen.bind(this), 3000);
+ // unique identifier for this channel, which is required for some rpc
+ this._uuid = data.uuid;
+
+ // * list of commands available for this channel
+ this._commands = _.filter(commands, function (command) {
+ return !command.channel_types ||
+ _.contains(command.channel_types, self._serverType);
+ });
+ if (this._type === 'channel') {
+ this._type = data.public !== 'private' ? 'public' : 'private';
+ }
+ if ('anonymous_name' in data) {
+ this._name = data.anonymous_name;
+ }
+ if (data.last_message_date) {
+ this._lastMessageDate = moment(time.str_to_datetime(data.last_message_date));
+ }
+ this._chat = !this.getType().match(/^(public|private)$/);
+ if (data.message_unread_counter) {
+ this._unreadCounter = data.message_unread_counter;
+ }
+ },
+
+ //--------------------------------------------------------------------------
+ // Public
+ //--------------------------------------------------------------------------
+
+ /**
+ * Close the channel
+ *
+ * This operation is executed server-side and the chat window will be
+ * folded in all potential tabs from all browsers.
+ *
+ * @override
+ */
+ close: function () {
+ this._super.apply(this, arguments);
+ this._rpc({
+ model: 'mail.channel',
+ method: 'channel_fold',
+ kwargs: { uuid: this.getUUID(), state: 'closed' },
+ }, { shadow: true });
+ },
+ /**
+ * Decrement the needaction counter of the channel
+ * Floor value at 0.
+ *
+ * @param {integer} [num=1] the amount to decrement at most
+ */
+ decrementNeedactionCounter: function (num) {
+ num = _.isNumber(num) ? num : 1;
+ this._needactionCounter = Math.max(this._needactionCounter - 1, 0);
+ },
+ /**
+ * Open the chat window for this channel
+ * (in all potential tabs from all browsers)
+ *
+ * @pverride
+ */
+ detach: function () {
+ this._super.apply(this, arguments);
+ this._rpc({
+ model: 'mail.channel',
+ method: 'channel_minimize',
+ args: [this.getUUID(), true],
+ }, {
+ shadow: true,
+ });
+ },
+ /**
+ * Force fetch members of the channel
+ *
+ * @returns {$.Promise<Object[]>} resolved with list of channel listeners
+ */
+ forceFetchMembers: function () {
+ this._membersDef = undefined;
+ return this.getMentionPartnerSuggestions();
+ },
+ /**
+ * Folds/Minimize the channel
+ * (in all potential tabs from all browsers)
+ *
+ * @override
+ * @param {boolean} folded
+ */
+ fold: function (folded) {
+ this._super.apply(this, arguments);
+ var args = {
+ uuid: this.getUUID(),
+ };
+ if (_.isBoolean(folded)) {
+ args.state = folded ? 'folded' : 'open';
+ }
+ this._rpc({
+ model: 'mail.channel',
+ method: 'channel_fold',
+ kwargs: args,
+ }, {shadow: true});
+ },
+ /**
+ * Show the list of available commands on this channel (e.g. '/help')
+ *
+ * @override
+ * @returns {Array} list of commands
+ */
+ getCommands: function () {
+ return this._commands;
+ },
+ /**
+ * @returns {mail.model.Message|undefined} last message of channel, if any
+ */
+ getLastMessage: function () {
+ return _.last(this._cache['[]'].messages);
+ },
+ /**
+ * Get listeners of a channel
+ *
+ * @returns {$.Promise<Object[]>} resolved with list of channel listeners
+ */
+ getMentionPartnerSuggestions: function () {
+ if (!this._membersDef) {
+ this._membersDef = this._rpc({
+ model: 'mail.channel',
+ method: 'channel_fetch_listeners',
+ args: [this.getUUID()],
+ }, {
+ shadow: true
+ })
+ .then(function (members) {
+ return members;
+ });
+ }
+ return this._membersDef;
+ },
+ /**
+ * @returns {integer}
+ */
+ getNeedactionCounter: function () {
+ return this._needactionCounter;
+ },
+ /**
+ * @override
+ */
+ getPreview: function () {
+ var result = this._super.apply(this, arguments);
+ if (!this.isChat()) {
+ result.imageSRC = '/web/image/mail.channel/' + this.getID() + '/image_small';
+ }
+ var lastMessage = this.getLastMessage();
+ return _.extend(result, {
+ author: lastMessage ? lastMessage.getDisplayedAuthor() : '',
+ body: lastMessage ? mailUtils.parseAndTransform(lastMessage.getBody(), mailUtils.inline) : '',
+ date: lastMessage ? lastMessage.getDate() : moment(),
+ isAuthor: this.hasMessages() && this.getLastMessage().isAuthor(),
+ });
+ },
+ /**
+ * Returns the title to display in thread window's headers.
+ * For channels, the title is prefixed with "#".
+ *
+ * @override
+ * @returns {string|Object} the name of the thread by default (see getName)
+ */
+ getTitle: function () {
+ return "#" + this._super.apply(this, arguments);
+ },
+ /**
+ * Get the UUID of the channel.
+ * This is a string that uniquely links this channel to a server channel.
+ *
+ * @returns {string} uuid of this channel
+ */
+ getUUID: function () {
+ return this._uuid;
+ },
+ /**
+ * Increment the needaction counter of this channel by 1 unit
+ */
+ incrementNeedactionCounter: function () {
+ this._needactionCounter++;
+ },
+ /**
+ * States whether the channel should be auto-selected on creation
+ *
+ * Note that this is not of the responsibility of the channel
+ * (see mail.model.Thread for more information)
+ *
+ * @override
+ * @returns {boolean}
+ */
+ isAutoswitch: function () {
+ return this._autoswitch;
+ },
+ /**
+ * All instances of this class are chanels
+ *
+ * @override
+ * @returns {boolean}
+ */
+ isChannel: function () {
+ return true;
+ },
+ /**
+ * States whether this channel is a chat or not.
+ * These types of channels are chat:
+ * - direct messages (DM)
+ * - livechat
+ *
+ * @override
+ * @returns {boolean}
+ */
+ isChat: function () {
+ return this._chat;
+ },
+ /**
+ * States whether the channel auto-subscribes some users in a group
+ *
+ * @returns {boolean}
+ */
+ isGroupBasedSubscription: function () {
+ return this._groupBasedSubscription;
+ },
+ /**
+ * States whether the channel is moderated or not.
+ *
+ * @override
+ * @returns {boolean}
+ */
+ isModerated: function () {
+ return this._moderation;
+ },
+ /**
+ * States whether the current user is moderator of this channel.
+ *
+ * @returns {boolean}
+ */
+ isModerator: function () {
+ return this._isModerator;
+ },
+ /**
+ * Marks this channel as read.
+ * The last seen message will be the last message.
+ * Resolved with the last seen message, only for non-mailbox channels
+ *
+ * @override
+ * @returns {$.Promise<integer|undefined>} resolved with last message ID
+ * seen in the channel, and when the channel has been marked as seen on
+ * the server.
+ */
+ markAsRead: function () {
+ if (this._unreadCounter > 0) {
+ this.resetUnreadCounter();
+ return this._throttleFetchSeen();
+ }
+ return this._super.apply(this, arguments);
+ },
+ /**
+ * Prepare and send a message to the server on this channel
+ *
+ * @override
+ * @param {Object} data data related to the new message
+ * @returns {$.Promise<Object>} 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;
+ return this._super.apply(this, arguments).then(function (messageData) {
+ _.extend(messageData, {
+ message_type: 'comment',
+ subtype: 'mail.mt_comment',
+ command: data.command,
+ });
+ return self._rpc({
+ model: 'mail.channel',
+ method: data.command ? 'execute_command' : 'message_post',
+ args: [self._id],
+ kwargs: messageData,
+ }).then(function () {
+ return messageData;
+ });
+ });
+ },
+ /**
+ * Unsubscribes from channel
+ *
+ * @returns {$.Promise} resolve when unsubscribed
+ */
+ unsubscribe: function () {
+ if (_.contains(['public', 'private'], this.getType())) {
+ // unfollow channel
+ return this._rpc({
+ model: 'mail.channel',
+ method: 'action_unfollow',
+ args: [[this._id]],
+ });
+ } else {
+ // unpin livechat
+ return this._rpc({
+ model: 'mail.channel',
+ method: 'channel_pin',
+ args: [this.getUUID(), false],
+ });
+ }
+ },
+ /**
+ * Updates the internal state of the channel, and reflects the changes in
+ * the UI.
+ *
+ * Called by {mail.Manager.Notification} on receiving a channel window
+ * notification. A notification is received after a client-side action
+ * that changes the state of the channel window visually, so that cross tab
+ * channels are updated similarly.
+ *
+ * TODO: This method should be dropped if the state of windows are
+ * completely handled client-side, which should be the case in the near
+ * future.
+ *
+ * @param {Object} params
+ * @param {boolean} params.detached
+ * @param {boolean} params.folded
+ */
+ updateWindowState: function (params) {
+ this._folded = 'folded' in params ? params.folded : this._folded;
+ this._detached = 'detached' in params ? params.detached : this._detached;
+ this._warnUpdatedWindowState();
+ },
+ /**
+ * Reset the needaction counter to 0.
+ */
+ resetNeedactionCounter: function () {
+ this._needactionCounter = 0;
+ },
+
+ //--------------------------------------------------------------------------
+ // Private
+ //--------------------------------------------------------------------------
+
+ /**
+ * @private
+ * @returns {$.Promise<integer>} resolved with ID of last seen message
+ */
+ _fetchSeen: function () {
+ var self = this;
+ return this._rpc({
+ model: 'mail.channel',
+ method: 'channel_seen',
+ args: [[this._id]],
+ }, {
+ shadow: true
+ }).then(function (lastSeenMessageID) {
+ self._lastSeenMessageID = lastSeenMessageID;
+ return lastSeenMessageID;
+ });
+ },
+ /**
+ * Get the domain to fetch all the messages in the current channel
+ *
+ * Note that with moderation, even though some messages are not really
+ * linked to a channel ('channel_ids'), we should nonetheless display them.
+ * These messages are fetched from their associated document
+ * ('mail.channel' with provided channel ID), and messages that are pending
+ * moderation.
+ *
+ * @override
+ * @private
+ * @returns {Array}
+ */
+ _getThreadDomain: function () {
+ return ['|', '&', '&',
+ ['model', '=', 'mail.channel'],
+ ['res_id', 'in', [this._id]],
+ ['need_moderation', '=', true],
+ ['channel_ids', 'in', [this._id]]];
+ },
+});
+
+return Channel;
+
+});
diff --git a/addons/mail/static/src/js/models/threads/dm.js b/addons/mail/static/src/js/models/threads/dm.js
new file mode 100644
index 00000000000..cd08dd7526d
--- /dev/null
+++ b/addons/mail/static/src/js/models/threads/dm.js
@@ -0,0 +1,94 @@
+odoo.define('mail.model.DM', function (require) {
+"use strict";
+
+var Channel = require('mail.model.Channel');
+
+/**
+ *
+ * Any piece of code in JS that make use of DMs must ideally interact with
+ * such objects, instead of direct data from the server.
+ */
+var DM = Channel.extend({
+ /**
+ * @override
+ * @param {Object} data
+ * @param {Object} data.direct_partner
+ * @param {integer} data.direct_partner[0].id
+ * @param {string} data.direct_partner[0].im_status
+ * @param {string} data.direct_partner[0].name
+ */
+ init: function (parent, data) {
+ this._super.apply(this, arguments);
+
+ this._type = 'dm';
+ this._name = data.direct_partner[0].name;
+ this._directPartnerID = data.direct_partner[0].id;
+ this._status = data.direct_partner[0].im_status;
+ },
+
+ //--------------------------------------------------------------------------
+ // Public
+ //--------------------------------------------------------------------------
+
+ /**
+ * Get the direct partner ID linked to the DM, i.e. the partner ID of the
+ * user at the other end of the DM conversation. All DM do have a direct
+ * partner iD.
+ *
+ * @returns {integer}
+ */
+ getDirectPartnerID: function () {
+ return this._directPartnerID;
+ },
+ /**
+ * @override
+ */
+ getPreview: function () {
+ var result = this._super.apply(this, arguments);
+ result.imageSRC = '/web/image/res.partner/' + this.getDirectPartnerID() + '/image_small';
+ return result;
+ },
+ /**
+ * @returns {string}
+ */
+ getStatus: function () {
+ return this._status;
+ },
+ /**
+ * DM are chat
+ *
+ * @override
+ * @returns {boolean}
+ */
+ isChat: function () {
+ return true;
+ },
+ /**
+ * @param {string} newStatus
+ */
+ setStatus: function (newStatus) {
+ this._status = newStatus;
+ },
+ /**
+ * Unpin this DM
+ *
+ * @override
+ * @return {$.Promise} resolve when unpinned
+ */
+ unsubscribe: function () {
+ return this._rpc({
+ model: 'mail.channel',
+ method: 'channel_pin',
+ args: [this.getUUID(), false],
+ });
+ },
+
+ //--------------------------------------------------------------------------
+ // Private
+ //--------------------------------------------------------------------------
+
+});
+
+return DM;
+
+});
diff --git a/addons/mail/static/src/js/models/threads/document_thread.js b/addons/mail/static/src/js/models/threads/document_thread.js
new file mode 100644
index 00000000000..92cc26e845b
--- /dev/null
+++ b/addons/mail/static/src/js/models/threads/document_thread.js
@@ -0,0 +1,317 @@
+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());
+ }
+ },
+ /**
+ * Overrides to store the thread's state in the LocalStorage, so that it is
+ * shared between tabs, and restored on F5.
+ *
+ * @override
+ */
+ close: function () {
+ this._super.apply(this, arguments);
+ this.call('mail_service', 'updateDocumentThreadState', this._id, {
+ name: this.getName(),
+ windowState: 'closed',
+ });
+ },
+ /**
+ * Overrides to store the thread's state in the LocalStorage, so that it is
+ * shared between tabs, and restored on F5.
+ *
+ * @override
+ */
+ detach: function () {
+ this._super.apply(this, arguments);
+ var windowState = this._folded ? 'folded' : 'open';
+ this.call('mail_service', 'updateDocumentThreadState', this._id, {
+ name: this.getName(),
+ windowState: windowState,
+ });
+ },
+ /**
+ * Overrides to store the thread's state in the LocalStorage, so that it is
+ * shared between tabs, and restored on F5.
+ *
+ * @override
+ */
+ fold: function () {
+ this._super.apply(this, arguments);
+ var windowState = this._folded ? 'folded' : 'open';
+ this.call('mail_service', 'updateDocumentThreadState', this._id, {
+ name: this.getName(),
+ windowState: windowState,
+ });
+ },
+ /**
+ * 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<mail.model.Message[]>}
+ */
+ 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<Object>} 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 || "mail.mt_comment",
+ 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;
+
+});
diff --git a/addons/mail/static/src/js/models/threads/mailbox.js b/addons/mail/static/src/js/models/threads/mailbox.js
new file mode 100644
index 00000000000..bcb5ae609b7
--- /dev/null
+++ b/addons/mail/static/src/js/models/threads/mailbox.js
@@ -0,0 +1,213 @@
+odoo.define('mail.model.Mailbox', function (require) {
+"use strict";
+
+var ThreadWithCache = require('mail.model.ThreadWithCache');
+
+var core = require('web.core');
+
+var _t = core._t;
+
+/**
+ * Mailboxes, called "static channels" in earlier version of Odoo, are threads
+ * that are not represented on the server. We can see them as "threads that are
+ * not conversations". For instance, the well-known "Inbox" folder contains a
+ * list of messages, but the inbox does not represent a conversation: Inbox is
+ * modeled as a mailbox.
+ */
+var Mailbox = ThreadWithCache.extend({
+
+ /**
+ * @override
+ * @param {Object} data
+ * @param {string} data.id the ID of the mailbox, without the 'mailbox_'
+ * prefix.
+ * @param {integer} [data.mailboxCounter=0] the initial mailbox counter of
+ * this mailbox.
+ */
+ init: function (parent, data) {
+ // ID or mailboxes are always prefixed with 'mailbox_'
+ data.id = 'mailbox_' + data.id;
+ data.type = 'mailbox';
+ this._super.apply(this, arguments);
+ this._mailboxCounter = data.mailboxCounter || 0;
+ },
+
+ //--------------------------------------------------------------------------
+ // Public
+ //--------------------------------------------------------------------------
+
+ /**
+ * Decrement the counter of this mailbox. It floors to 0.
+ *
+ * @param {integer} [num=1] how many, at most, the counter is decremented
+ */
+ decrementMailboxCounter: function (num) {
+ num = _.isNumber(num) ? num : 1;
+ this._mailboxCounter = Math.max(this._mailboxCounter - num, 0);
+ },
+ /**
+ * Get the local messages of the mailbox (by local messages, we mean
+ * messages that have already been fetched from the server).
+ *
+ * It is possible to filter on local messages that are specific to a
+ * document with the `options` parameter.
+ *
+ * @param {Object} [options]
+ * @param {string} [options.documentModel] model of the document that the
+ * local messages of inbox must be linked to.
+ * @param {integer} [options.documentID] ID of the document that the local
+ * messages of inbox must be linked to.
+ */
+ getLocalMessages: function (options) {
+ var localMessages = this._cache['[]'].messages;
+ if (!options) {
+ return localMessages;
+ }
+ if (options.documentModel && options.documentID) {
+ return _.filter(localMessages, function (localMessage) {
+ return localMessage.getDocumentModel() === options.documentModel &&
+ localMessage.getDocumentID() === options.documentID;
+ });
+ }
+ },
+ /**
+ * Get the mailbox counter of this mailbox.
+ *
+ * @returns {integer}
+ */
+ getMailboxCounter: function () {
+ return this._mailboxCounter;
+ },
+ /**
+ * Get the preview of the messages from the inbox mailbox.
+ * If there are multiple messages associated to a similar document or
+ * channel, only keep the lastest message of this document/channel in the
+ * previews.
+ *
+ * @returns {$.Promise<Object[]>}
+ */
+ getMessagePreviews: function () {
+ var self = this;
+
+ return this.getMessages().then(function (messages) {
+ // pick only last message of chatter
+ // items = list of objects
+ // { unreadCounter: integer, message: mail.model.Message }
+ var items = [];
+ _.each(messages, function (message) {
+ var unreadCounter = 1;
+ var similarItem = _.find(items, function (item) {
+ return self._areMessagesFromSameDocumentThread(item.message, message) ||
+ self._areMessagesFromSameChannel(item.message, message);
+ });
+ if (similarItem) {
+ unreadCounter = similarItem.unreadCounter + 1;
+ var index = _.findIndex(items, similarItem);
+ items[index] = {
+ unreadCounter: unreadCounter,
+ message: message,
+ };
+ } else {
+ items.push({
+ unreadCounter: unreadCounter,
+ message: message,
+ });
+ }
+ });
+ return _.map(items, function (item) {
+ return _.extend(item.message.getPreview(), {
+ unreadCounter: item.unreadCounter,
+ });
+ });
+ });
+ },
+ /**
+ * Increment the counter of this mailbox
+ *
+ * @param {integer} [num=1] how many, the counter is incremented
+ */
+ incrementMailboxCounter: function (num) {
+ num = _.isNumber(num) ? num : 1;
+ this._mailboxCounter = Math.max(this._mailboxCounter + num, 0);
+ },
+ /**
+ * Marks all messages from the mailbox as read. At the moment, this method
+ * makes only sense for 'Inbox'.
+ *
+ * @param {Array} domain
+ * @return {$.Promise} resolved when all messages have been marked as read
+ * on the server
+ */
+ markAllMessagesAsRead: function (domain) {
+ if (this._id === 'mailbox_inbox' && this.getMailboxCounter() > 0) {
+ return this._rpc({
+ model: 'mail.message',
+ method: 'mark_all_as_read',
+ kwargs: {
+ channel_ids: [],
+ domain: domain,
+ },
+ });
+ }
+ return $.when();
+ },
+
+ //--------------------------------------------------------------------------
+ // Private
+ //--------------------------------------------------------------------------
+
+ /**
+ * Tell whether the two messages originate from the same channel.
+ * If any one of them does not originate from a channel, returns false.
+ *
+ * @private
+ * @param {mail.model.Message} message1
+ * @param {mail.model.Message} message2
+ * @returns {boolean}
+ */
+ _areMessagesFromSameChannel: function (message1, message2) {
+ return message1.originatesFromChannel() &&
+ message2.originatesFromChannel() &&
+ message1.getOriginChannelID() === message2.getOriginChannelID();
+ },
+ /**
+ * Tell whether the two messages are linked to the same document thread.
+ * If any one of them is not linked to a document, returns false.
+ *
+ * @private
+ * @param {mail.model.Message} message1
+ * @param {mail.model.Message} message2
+ * @returns {boolean}
+ */
+ _areMessagesFromSameDocumentThread: function (message1, message2) {
+ return message1.isLinkedToDocumentThread() &&
+ message2.isLinkedToDocumentThread() &&
+ message1.getDocumentModel() === message2.getDocumentModel() &&
+ message1.getDocumentID() === message2.getDocumentID();
+ },
+ /**
+ * Get the domain to fetch all the messages in the current mailbox
+ *
+ * @override
+ * @private
+ * @returns {Array}
+ * @throws on missing domain for the provided mailbox (mailboxes should
+ * always have a domain on messages to fetch)
+ */
+ _getThreadDomain: function () {
+ if (this._id === 'mailbox_inbox') {
+ return [['needaction', '=', true]];
+ } else if (this._id === 'mailbox_starred') {
+ return [['starred', '=', true]];
+ } else if (this._id === 'mailbox_moderation') {
+ return [['need_moderation', '=', true]];
+ } else {
+ throw _t(_.str("Missing domain for mailbox with ID '%s'", this._id));
+ }
+ },
+
+});
+
+return Mailbox;
+
+});
diff --git a/addons/mail/static/src/js/models/threads/thread.js b/addons/mail/static/src/js/models/threads/thread.js
new file mode 100644
index 00000000000..de2c6628d67
--- /dev/null
+++ b/addons/mail/static/src/js/models/threads/thread.js
@@ -0,0 +1,446 @@
+odoo.define('mail.model.Thread', function (require) {
+"use strict";
+
+var emojis = require('mail.emojis');
+var mailUtils = require('mail.utils');
+
+var Class = require('web.Class');
+var Mixins = require('web.mixins');
+var ServicesMixin = require('web.ServicesMixin');
+
+/**
+ * This is the super class modeling threads in JS.
+ * Such threads are stored in the mail manager, and any piece of JS code whose
+ * logic relies on threads must ideally interact with such objects.
+ *
+ * In particular, channels and mailboxes are two different kinds of threads.
+ */
+var Thread = Class.extend(Mixins.EventDispatcherMixin, ServicesMixin, {
+ /**
+ * @param {mail.Manager} parent
+ * @param {Object} data
+ * @param {string} [data.channel_type]
+ * @param {integer|string} data.id
+ * @param {string} data.name
+ * @param {string} [data.type]
+ */
+ init: function (parent, data) {
+ Mixins.EventDispatcherMixin.init.call(this, arguments);
+ this.setParent(parent);
+
+ this._id = data.id;
+
+ // threads are not detached by default
+ this._detached = false;
+ // threads are unfolded by default
+ this._folded = false;
+ // if this._massMailing is set, display subject on messages, use
+ // extended composer and show "Send by messages by email" on discuss
+ // sidebar
+ this._massMailing = false;
+ this._name = data.name;
+ // on 1st request to getPreview, fetch data if incomplete. Otherwise it
+ // means that there is no message in this channel.
+ this._previewed = false;
+ this._type = data.type || data.channel_type;
+ // any new message that has not been read yet
+ this._unreadCounter = 0;
+ // max number of fetched messages from the server
+ this._FETCH_LIMIT = 30;
+ },
+
+ //--------------------------------------------------------------------------
+ // Public
+ //--------------------------------------------------------------------------
+
+ /**
+ * Add a message to this thread.
+ *
+ * @abstract
+ * @param {mail.model.Message} message
+ */
+ addMessage: function (message) {},
+ /**
+ * Updates the _detached state of the thread. Must be overriden to reflect
+ * the new state in the interface.
+ */
+ close: function () {
+ this._detached = false;
+ this._folded = false;
+ this._warnUpdatedWindowState();
+ },
+ /**
+ * Updates the _detached state of the thread. Must be overriden to reflect
+ * the new state in the interface.
+ *
+ * @param {Object} [options={}]
+ * @param {boolean} [options.keepFoldState=false] if set, keep the fold state
+ * of this thread. Otherwise unfold it while detaching it.
+ * @param {boolean} [options.passively=false] if set, if the thread window
+ * will be created passively.
+ */
+ detach: function (options) {
+ options = options || {};
+ this._detached = true;
+ this._folded = options.keepFoldState ? this._folded : false;
+ this._warnUpdatedWindowState({
+ passively: options.passively,
+ });
+ },
+ /**
+ * Updates the _folded state of the thread. Must be overriden to reflect
+ * the new state in the interface.
+ */
+ fold: function (folded) {
+ this._detached = true;
+ this._folded = folded;
+ this._warnUpdatedWindowState();
+ },
+ /**
+ * Get the list of available commands for the thread.
+ * By default, threads do not have any available command.
+ *
+ * @returns {Array}
+ */
+ getCommands: function () {
+ return [];
+ },
+ /**
+ * Get the ID of the thread
+ *
+ * @returns {integer|string} string for mailboxes (e.g. 'mailbox_inbox')
+ */
+ getID: function () {
+ return this._id;
+ },
+ /**
+ * Get the listeners of the thread.
+ * By default, a thread has not listener.
+ *
+ * @abstract
+ * @returns {$.Promise<Object[]>}
+ */
+ getMentionPartnerSuggestions: function () {
+ return $.when([]);
+ },
+ /**
+ * Get the list of messages in this thread.
+ * By default, a thread has no messages.
+ *
+ * Note that this method only returns some messages, as we do not want to
+ * fetch all messages of a thread at once, just to read the last message.
+ *
+ * As a result, this method fetches only some messages of the thread,
+ * starting from the last message in the thread. At most, it fetches
+ * `this.LIMIT` number of messages at a time.
+ *
+ * @abstract
+ * @returns {$.Promise<mail.model.Message[]>}
+ */
+ getMessages: function () {
+ return $.when([]);
+ },
+ /**
+ * Get the name of this thread
+ *
+ * @returns {string|Object} object for lazy translated names with _lt
+ */
+ getName: function () {
+ return this._name;
+ },
+ /**
+ * Returns the information required to render the preview of this channel.
+ *
+ * @returns {Object} a valid object for the rendering of previews
+ * (@see mail.Preview template)
+ */
+ getPreview: function () {
+ return {
+ id: this.getID(),
+ imageSRC: '/mail/static/src/img/smiley/avatar.jpg',
+ isChat: this.isChat(),
+ status: this.getStatus(),
+ title: this.getName(),
+ unreadCounter: this.getUnreadCounter(),
+ };
+ },
+ /**
+ * Get the status of the thread (e.g. 'online', 'offline', etc.)
+ *
+ * @returns {string}
+ */
+ getStatus: function () {
+ return this._status;
+ },
+ /**
+ * Returns the title to display in thread window's headers.
+ *
+ * @returns {string|Object} the name of the thread by default (see @getName)
+ */
+ getTitle: function () {
+ return this.getName();
+ },
+ /**
+ * @returns {string}
+ */
+ getType: function () {
+ return this._type;
+ },
+ /**
+ * @returns {integer}
+ */
+ getUnreadCounter: function () {
+ return this._unreadCounter;
+ },
+ /**
+ * State whether this channel has been previewed
+ *
+ * A channel that has been previewed means that it had the necessary data
+ * to display its preview format. A channel needs its meta data and the
+ * last message in order to build its preview format.
+ *
+ * This is useful in order to not fetch preview info on this channel more
+ * than once on channels that have no message at all.
+ *
+ * Any received message updates the last_message, so a channel should
+ * always have all the necessary information to display its preview after
+ * the 1st time.
+ *
+ * @returns {boolean}
+ */
+ hasBeenPreviewed: function () {
+ return this._previewed;
+ },
+ /**
+ * State whether there are unread messages in this thread
+ *
+ * @returns {boolean}
+ */
+ hasUnreadMessages: function () {
+ return this._unreadCounter !== 0;
+ },
+ /**
+ * Increments the needaction counter of this thread
+ * FIXME: this method makes only sense for channels. Not sure, but I think
+ * this method is necessary at this level because of the model
+ * im_support.SupportChannel
+ *
+ * @abstract
+ */
+ incrementNeedactionCounter: function () {},
+ /**
+ * Increment the unread counter of this thread by 1 unit
+ */
+ incrementUnreadCounter: function () {
+ this._unreadCounter++;
+ this._warnNewUnreadCounter();
+ },
+ /**
+ * States whether the thread should be auto-selected on creation
+ *
+ * Note that this is not of the responsibility of the thread: it only
+ * provides guidance for the object that uses threads (e.g. mail.Discuss
+ * must listen on threads and auto-select the thread if autoswitch is set).
+ *
+ * By default, threads are not in autoswitch mode.
+ *
+ * @returns {boolean}
+ */
+ isAutoswitch: function () {
+ return false;
+ },
+ /**
+ * States whether this thread is a channel or not. A thread is a channel if
+ * it is an instance of mail.model.Channel (direct or indirect).
+ * By default, any thread is not a channel
+ *
+ * @returns {boolean}
+ */
+ isChannel: function () {
+ return false;
+ },
+ /**
+ * States whether this thread is a chat or not. In particular, public and
+ * private channels are not chat, but DMs and Livechats are chats. Chats
+ * are threads used for communication between two users.
+ * By default, any thread is not a chat
+ *
+ * @returns {boolean}
+ */
+ isChat: function () {
+ return false;
+ },
+ /**
+ * States whether this thread is detached or not.
+ * A thread that is detached must have a thread window linked to itself.
+ *
+ * @return {boolean}
+ */
+ isDetached: function () {
+ return this._detached;
+ },
+ /**
+ * States whether this thread is folded or not.
+ *
+ * @return {boolean}
+ */
+ isFolded: function () {
+ return this._folded;
+ },
+ /**
+ * States whether the thread is linked to a document
+ * By default, threads are not linked to a document.
+ *
+ * @returns {boolean}
+ */
+ isLinkedToDocument: function () {
+ return false;
+ },
+ /**
+ * States whether this thread has the mass mailing setting active or not.
+ * This is a server-side setting, that determine the type of composer that
+ * is used (basic or extended composer).
+ *
+ * @return {boolean}
+ */
+ isMassMailing: function () {
+ return this._massMailing;
+ },
+ /**
+
+ * States whether the thread is moderated or not.
+ * By default, threads are not moderated.
+ *
+ * @returns {boolean}
+ */
+ isModerated: function () {
+ return false;
+ },
+ /**
+ * States whether the current user is moderator of this thread.
+ * By default, the current user is not moderator of this thread.
+ *
+ * @returns {boolean}
+ */
+ isModerator: function () {
+ return false;
+ },
+ /**
+ * Mark this channel as previewed
+ *
+ * This is useful in order to not fetch preview info on this channel
+ * is the server has no preview in the first place.
+ *
+ * Note: preview fetch is useful only when the channel contains messages
+ * that have not been fetched at all. After that, this channel instance
+ * is updated regularly so that the most up-to-date info are available
+ * to make the preview of this channel.
+ */
+ markAsPreviewed: function () {
+ this._previewed = true;
+ },
+ /**
+ * Mark the thread as read, which resets the unread counter to 0.
+ *
+ * @returns {$.Promise} resolved
+ */
+ markAsRead: function () {
+ if (this._unreadCounter > 0) {
+ this._unreadCounter = 0;
+ this._warnNewUnreadCounter();
+ }
+ return $.when();
+ },
+ /**
+ * Post a message on the thread.
+ *
+ * This method must be completed by concrete threads,
+ * As it currently only pre-process the messages at the moment.
+ *
+ * @abstract
+ * @param {Object} data
+ * @returns {$.Promise<Object>} resolved with the message object to be sent
+ * to the server
+ */
+ postMessage: function (data) {
+ // This message will be received from the mail composer as html content
+ // subtype but the urls will not be linkified. If the mail composer
+ // takes the responsibility to linkify the urls we end up with double
+ // linkification a bit everywhere. Ideally we want to keep the content
+ // as text internally and only make html enrichment at display time but
+ // the current design makes this quite hard to do.
+ var body = mailUtils.parseAndTransform(_.str.trim(data.content), mailUtils.addLink);
+ body = this._generateEmojis(body);
+ var messageData = {
+ partner_ids: data.partner_ids,
+ body: body,
+ attachment_ids: data.attachment_ids,
+ };
+ if ('subject' in data) {
+ messageData.subject = data.subject;
+ }
+ return $.when(messageData);
+ },
+ /**
+ * @private
+ */
+ resetUnreadCounter: function () {
+ this._unreadCounter = 0;
+ this._warnNewUnreadCounter();
+ },
+
+ //--------------------------------------------------------------------------
+ // Private
+ //--------------------------------------------------------------------------
+
+ /**
+ * @abstract
+ * @private
+ * @return {$.Promise}
+ */
+ _fetchMessages: function () {
+ return $.when();
+ },
+ /**
+ * Replace character representations of emojis by their unicode
+ * representation in the provided HTML string. Note that the provided html
+ * string is altered by this function.
+ *
+ * @param {string} htmlString
+ * @returns {string}
+ */
+ _generateEmojis: function (htmlString) {
+ _.each(emojis, function (emoji) {
+ _.each(emoji.sources, function (source) {
+ var escapedSource = String(_.escape(source)).replace(/([.*+?=^!:${}()|[\]/\\])/g, '\\$1');
+ var regexp = new RegExp("(\\s|^)(" + escapedSource + ")(?=\\s|$)", 'g');
+ htmlString = htmlString.replace(regexp, '$1' + emoji.unicode);
+ });
+ });
+ return htmlString;
+ },
+ /**
+ * Warn other mail components that the window state of this thread has
+ * changed (it has become closed, folded, unfolded, detached, etc.)
+ *
+ * @private
+ * @param {Object} [options={}]
+ */
+ _warnUpdatedWindowState: function (options) {
+ options = options || {};
+ this.call('mail_service', 'updateThreadWindow', this.getID(), options);
+ },
+ /**
+ * Warn on the chat bus that the unread counter has been updated
+ *
+ * @private
+ */
+ _warnNewUnreadCounter: function () {
+ this.call('mail_service', 'getMailBus')
+ .trigger('update_thread_unread_counter', this);
+ },
+});
+
+return Thread;
+
+});
diff --git a/addons/mail/static/src/js/models/threads/thread_with_cache.js b/addons/mail/static/src/js/models/threads/thread_with_cache.js
new file mode 100644
index 00000000000..f25a19005f2
--- /dev/null
+++ b/addons/mail/static/src/js/models/threads/thread_with_cache.js
@@ -0,0 +1,204 @@
+odoo.define('mail.model.ThreadWithCache', function (require) {
+"use strict";
+
+var Thread = require('mail.model.Thread');
+
+var session = require('web.session');
+
+/**
+ * This is a version of a thread that is handling a cache.
+ * Basically, this cache is an object indexed by stringified indexes.
+ * These indexes represent the domain of messages that match this domain.
+ *
+ * Any threads that are instances of this Class can be used with a search view,
+ * in order to make searches on messages in a thread.
+ */
+var ThreadWithCache = Thread.extend({
+ /**
+ * @override
+ * @param {integer} [data.seen_message_id=undefined]
+ */
+ init: function (parent, data) {
+ this._super.apply(this, arguments);
+
+ this._cache = {
+ '[]': {
+ allHistoryLoaded: false,
+ loaded: false,
+ messages: [],
+ }
+ };
+ this._lastSeenMessageID = data.seen_message_id;
+ },
+
+ //--------------------------------------------------------------------------
+ // Public
+ //--------------------------------------------------------------------------
+
+ /**
+ * Add the message to this thread
+ *
+ * @override
+ * @private
+ * @param {mail.model.Message} message
+ * @param {Array} domain
+ */
+ addMessage: function (message, domain) {
+ var cache = this._getCache(domain);
+ var index = _.sortedIndex(cache.messages, message, function (msg) {
+ return msg.getID();
+ });
+ if (cache.messages[index] !== message) {
+ cache.messages.splice(index, 0, message);
+ }
+ },
+ /**
+ * Get the last seen message for a given thread
+ *
+ * @return {integer|null} last seen message ID (if any)
+ */
+ getLastSeenMessageID: function () {
+ return this._lastSeenMessageID || null;
+ },
+ /**
+ * @override
+ * @param {Array} [domain]
+ * @param {boolean} [loadMore=false]
+ */
+ getMessages: function (domain, loadMore) {
+ if (loadMore) {
+ return this._fetchMessages(domain, true);
+ }
+ var cache = this._getCache(domain);
+ if (cache.loaded) {
+ return $.when(cache.messages);
+ } else {
+ return this._fetchMessages(domain);
+ }
+ },
+ /**
+ * @private
+ * @return {boolean} true if there is at least a message, false otherwise
+ */
+ hasMessages: function () {
+ return !!(_.last(this._cache['[]'].messages));
+ },
+ /**
+ * Invalidate the caches of the thread
+ */
+ invalidateCaches: function () {
+ this._cache = _.pick(this._cache, '[]');
+ },
+ /**
+ * State whether all messages have been loaded or not
+ *
+ * @param {Array} domain
+ * @return {boolean}
+ */
+ isAllHistoryLoaded: function (domain) {
+ return this._getCache(domain).allHistoryLoaded;
+ },
+ /**
+ * Remove message with ID `messageID` from this thread
+ *
+ * This is equivalent to removing this message from all the caches
+ *
+ * @param {integer} messageID
+ */
+ removeMessage: function (messageID) {
+ _.each(this._cache, function (cache) {
+ cache.messages = _.reject(cache.messages, function (message) {
+ return message.getID() === messageID;
+ });
+ });
+ },
+ /**
+ * Set 'messageID' as the ID of the last message seen in this thread
+ *
+ * @param {integer} messageID
+ */
+ setLastSeenMessageID: function (messageID) {
+ this._lastSeenMessageID = messageID;
+ },
+
+ //--------------------------------------------------------------------------
+ // Private
+ //--------------------------------------------------------------------------
+
+ /**
+ * Gets messages from thread
+ *
+ * @override
+ * @private
+ * @param {Array} [pDomain] filter on the messages of the channel
+ * @param {boolean} [loadMore] Whether it should load more message
+ * @return {$.Promise<mail.model.Message[]>} resolved with list of messages
+ */
+ _fetchMessages: function (pDomain, loadMore) {
+ var self = this;
+
+ var domain = this._getThreadDomain();
+
+ var cache = this._getCache(pDomain);
+ if (pDomain) {
+ domain = domain.concat(pDomain || []);
+ }
+ if (loadMore) {
+ var minMessageID = cache.messages[0].getID();
+ domain = [['id', '<', minMessageID]].concat(domain);
+ }
+ return this._rpc({
+ model: 'mail.message',
+ method: 'message_fetch',
+ args: [domain],
+ kwargs: {
+ limit: this._FETCH_LIMIT,
+ context: session.user_context
+ },
+ }).then(function (messages) {
+ if (!cache.allHistoryLoaded) {
+ cache.allHistoryLoaded = messages.length < self._FETCH_LIMIT;
+ }
+ cache.loaded = true;
+ _.each(messages, function (message) {
+ self.call('mail_service', 'addMessage', message, {
+ silent: true,
+ domain: pDomain,
+ });
+ });
+ cache = self._getCache(pDomain || []);
+ return cache.messages;
+ });
+ },
+ /**
+ * Get thread content from the cache
+ * Useful to get cached messages.
+ *
+ * @private
+ * @param {Array} domain
+ * @return {Object|undefined}
+ */
+ _getCache: function (domain) {
+ var stringifiedDomain = JSON.stringify(domain || []);
+ if (!this._cache[stringifiedDomain]) {
+ this._cache[stringifiedDomain] = {
+ allHistoryLoaded: false,
+ loaded: false,
+ messages: [],
+ };
+ }
+ return this._cache[stringifiedDomain];
+ },
+ /**
+ * Get the domain to fetch all the messages in the current thread
+ *
+ * @abstract
+ * @private
+ * @returns {Array}
+ */
+ _getThreadDomain: function () {},
+});
+
+return ThreadWithCache;
+
+});