From cb7add2ebd50c1981e35bb12a3e52a69a58bc05f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alexandre=20K=C3=BChn?= Date: Wed, 4 Jul 2018 13:44:49 +0200 Subject: [MOV] mail: move thread models in specific folder for thread models The `model` folder contains several files, so this groups files related to threads directly. --- addons/mail/static/src/js/models/channel.js | 442 -------------------- addons/mail/static/src/js/models/dm.js | 94 ----- .../mail/static/src/js/models/document_thread.js | 317 --------------- addons/mail/static/src/js/models/mailbox.js | 213 ---------- addons/mail/static/src/js/models/thread.js | 446 --------------------- .../mail/static/src/js/models/thread_with_cache.js | 204 ---------- .../mail/static/src/js/models/threads/channel.js | 442 ++++++++++++++++++++ addons/mail/static/src/js/models/threads/dm.js | 94 +++++ .../src/js/models/threads/document_thread.js | 317 +++++++++++++++ .../mail/static/src/js/models/threads/mailbox.js | 213 ++++++++++ addons/mail/static/src/js/models/threads/thread.js | 446 +++++++++++++++++++++ .../src/js/models/threads/thread_with_cache.js | 204 ++++++++++ 12 files changed, 1716 insertions(+), 1716 deletions(-) delete mode 100644 addons/mail/static/src/js/models/channel.js delete mode 100644 addons/mail/static/src/js/models/dm.js delete mode 100644 addons/mail/static/src/js/models/document_thread.js delete mode 100644 addons/mail/static/src/js/models/mailbox.js delete mode 100644 addons/mail/static/src/js/models/thread.js delete mode 100644 addons/mail/static/src/js/models/thread_with_cache.js create mode 100644 addons/mail/static/src/js/models/threads/channel.js create mode 100644 addons/mail/static/src/js/models/threads/dm.js create mode 100644 addons/mail/static/src/js/models/threads/document_thread.js create mode 100644 addons/mail/static/src/js/models/threads/mailbox.js create mode 100644 addons/mail/static/src/js/models/threads/thread.js create mode 100644 addons/mail/static/src/js/models/threads/thread_with_cache.js (limited to 'addons/mail/static/src') diff --git a/addons/mail/static/src/js/models/channel.js b/addons/mail/static/src/js/models/channel.js deleted file mode 100644 index e63cab2f907..00000000000 --- a/addons/mail/static/src/js/models/channel.js +++ /dev/null @@ -1,442 +0,0 @@ -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} 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} 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} 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} 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} 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/dm.js b/addons/mail/static/src/js/models/dm.js deleted file mode 100644 index cd08dd7526d..00000000000 --- a/addons/mail/static/src/js/models/dm.js +++ /dev/null @@ -1,94 +0,0 @@ -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/document_thread.js b/addons/mail/static/src/js/models/document_thread.js deleted file mode 100644 index 92cc26e845b..00000000000 --- a/addons/mail/static/src/js/models/document_thread.js +++ /dev/null @@ -1,317 +0,0 @@ -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} - */ - 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 || "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/mailbox.js b/addons/mail/static/src/js/models/mailbox.js deleted file mode 100644 index bcb5ae609b7..00000000000 --- a/addons/mail/static/src/js/models/mailbox.js +++ /dev/null @@ -1,213 +0,0 @@ -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} - */ - 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/thread.js b/addons/mail/static/src/js/models/thread.js deleted file mode 100644 index de2c6628d67..00000000000 --- a/addons/mail/static/src/js/models/thread.js +++ /dev/null @@ -1,446 +0,0 @@ -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} - */ - 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} - */ - 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} 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/thread_with_cache.js b/addons/mail/static/src/js/models/thread_with_cache.js deleted file mode 100644 index f25a19005f2..00000000000 --- a/addons/mail/static/src/js/models/thread_with_cache.js +++ /dev/null @@ -1,204 +0,0 @@ -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} 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; - -}); 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} 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} 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} 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} 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} 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} + */ + 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 || "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} + */ + 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} + */ + 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} + */ + 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} 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} 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; + +}); -- cgit v1.2.3