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; });