diff options
Diffstat (limited to 'addons/mail/static/src/js/services/chat_manager.js')
| -rw-r--r-- | addons/mail/static/src/js/services/chat_manager.js | 1751 |
1 files changed, 1751 insertions, 0 deletions
diff --git a/addons/mail/static/src/js/services/chat_manager.js b/addons/mail/static/src/js/services/chat_manager.js new file mode 100644 index 00000000000..b7c46f2d4c1 --- /dev/null +++ b/addons/mail/static/src/js/services/chat_manager.js @@ -0,0 +1,1751 @@ +odoo.define('mail.ChatManager', function (require) { +"use strict"; + +var utils = require('mail.utils'); + +var AbstractService = require('web.AbstractService'); +var Bus = require('web.Bus'); +var config = require('web.config'); +var core = require('web.core'); +var session = require('web.session'); +var time = require('web.time'); +var web_client = require('web.web_client'); + +var _t = core._t; +var _lt = core._lt; + +var LIMIT = 30; // max number of fetched messages from the server +var PREVIEW_MSG_MAX_SIZE = 350; // optimal for native english speakers +var ODOOBOT_ID = "ODOOBOT"; // default author_id for messages + +/** + * This service handles everything about chat channels and messages. + * + * There are basically two points of entry: + * + * 1. Calling a public method by means of 'this.call' + * 2. Receiving events on busBus (e.g. 'notification') + */ +var ChatManager = AbstractService.extend({ + name: 'chat_manager', + dependencies: ['ajax', 'bus_service'], + /** + * @override + */ + init: function () { + var self = this; + this._super.apply(this, arguments); + + this.messages = []; + this.channels = []; + this.channelsPreviewDef; + this.channelDefs = {}; + this.unreadConversationCounter = 0; + this.emojis = []; + this.emojiSubstitutions = {}; + this.emojiUnicodes = {}; + this.needactionCounter = 0; + this.starredCounter = 0; + this.mentionPartnerSuggestions = []; + this.cannedResponses = []; + this.commands = []; + this.discussMenuID; + this.globalUnreadCounter = 0; + this.pinnedDmPartners = []; // partner_ids we have a pinned DM with + this.discussOpen = false; + + this.chatBus = new Bus(); + this.busBus = this.call('bus_service', 'getBus'); + + this.chatBus.on('discuss_open', null, function (open) { + self.discussOpen = open; + }); + + this.busBus.on('notification', this, this._onNotification); + this.busBus.on('window_focus', this, this._onWindowFocus); + + // TODO create private fonction on prototype + this.channelSeen = _.throttle(function (channel) { + return self._rpc({ + model: 'mail.channel', + method: 'channel_seen', + args: [[channel.id]], + }, { + shadow: true + }); + }, 3000); + + this._isReady = this._initMessaging(); + + this._addChannel({ + id: "channel_inbox", + name: _lt("Inbox"), + type: "static", + }, { displayNeedactions: true }); + + this._addChannel({ + id: "channel_starred", + name: _lt("Starred"), + type: "static" + }); + }, + + //-------------------------------------------------------------------------- + // Public + //-------------------------------------------------------------------------- + + /** + * Closes the chat window of a corresponding channel. + * This operation is executed server-side and the chat window will be + * folded in all potential tabs from all browsers. + * + * @param {integer|string} channelID + * @return {$.Promise} + */ + closeChatSession: function (channelID) { + var channel = this.getChannel(channelID); + return this._rpc({ + model: 'mail.channel', + method: 'channel_fold', + kwargs: {uuid : channel.uuid, state : 'closed'}, + }, {shadow: true}); + }, + /** + * Creates a channel + * + * @param {integer|string} name id of partner (in case of dm) or name + * @param {string} type ['dm', 'public', 'private'] + * @return {$.Promise} + */ + createChannel: function (name, type) { + var method = type === "dm" ? "channel_get" : "channel_create"; + var args = type === "dm" ? [[name]] : [name, type]; + var context = _.extend({isMobile: config.device.isMobile}, session.user_context); + return this._rpc({ + model: 'mail.channel', + method: method, + args: args, + kwargs: {context: context}, + }) + .then(this._addChannel.bind(this)); + }, + /** + * Open the chat window for a given channel + * (in all potential tabs from all browsers) + * + * @param {integer} channelID + * @return {$.Promise} + */ + detachChannel: function (channelID) { + var channel = this.getChannel(channelID); + return this._rpc({ + model: 'mail.channel', + method: 'channel_minimize', + args: [channel.uuid, true], + }, { + shadow: true, + }); + }, + /** + * Folds/Minimize the chat window + * (in all potential tabs from all browsers) + * + * @param {integer} channelID + * @param {boolean} folded + * @return {$.Promise} + */ + foldChannel: function (channelID, folded) { + var args = { + uuid: this.getChannel(channelID).uuid, + }; + if (_.isBoolean(folded)) { + args.state = folded ? 'folded' : 'open'; + } + return this._rpc({ + model: 'mail.channel', + method: 'channel_fold', + kwargs: args, + }, {shadow: true}); + }, + /** + * Returns the list of canned responses + * A canned response is a pre-formatted text that is triggered with + * some keystrokes, such as with ':'. + * + * @return {Array} array of Objects (mail.shortcode) + */ + getCannedResponses: function () { + return this.cannedResponses; + }, + /** + * Returns a channel corresponding to the given id. + * + * @param {string|integer} id e.g. 'channel_inbox', 'channel_starred' + * @return {Object|undefined} channel + */ + getChannel: function (id) { + return _.findWhere(this.channels, {id: id}); + }, + /** + * Returns a list of channels + * + * @return {Object[]} + */ + getChannels: function () { + return _.clone(this.channels); + }, + /** + * Returns the content that will be shown in the mail navbar dropdown + * + * @param {Object[]} channels + * @return {$.Promise<Array>} resolved with array of preview msgs + */ + getChannelsPreview: function (channels) { + var self = this; + var channelsPreview = _.map(channels, function (channel) { + var info; + if (channel.channel_ids && _.contains(channel.channel_ids,"channel_inbox")) { + // map inbox(mail_message) data with existing channel/chat template + info = _.pick(channel, + 'id', 'body', 'avatar_src', 'res_id', 'model', 'module_icon', + 'subject','date', 'record_name', 'status', 'displayed_author', + 'email_from', 'unread_counter'); + info.last_message = { + body: info.body, + date: info.date, + displayed_author: info.displayed_author || info.email_from, + }; + info.name = info.record_name || info.subject || info.displayed_author; + info.image_src = info.module_icon || info.avatar_src; + info.message_id = info.id; + info.id = 'channel_inbox'; + return info; + } + info = _.pick(channel, 'id', 'is_chat', 'name', 'status', 'unread_counter'); + info.last_message = channel.last_message || _.last(channel.cache['[]'].messages); + if (!info.is_chat) { + info.image_src = '/web/image/mail.channel/'+channel.id+'/image_small'; + } else if (channel.direct_partner_id) { + info.image_src = '/web/image/res.partner/'+channel.direct_partner_id+'/image_small'; + } else { + info.image_src = '/mail/static/src/img/smiley/avatar.jpg'; + } + return info; + }); + var missingChannels = _.where(channelsPreview, {last_message: undefined}); + if (!this.channelsPreviewDef) { + if (missingChannels.length) { + var missingChannelIDs = _.pluck(missingChannels, 'id'); + this.channelsPreviewDef = this._rpc({ + model: 'mail.channel', + method: 'channel_fetch_preview', + args: [missingChannelIDs], + }, { + shadow: true, + }); + } else { + this.channelsPreviewDef = $.when(); + } + } + return this.channelsPreviewDef.then(function (channels) { + _.each(missingChannels, function (channelPreview) { + var channel = _.findWhere(channels, {id: channelPreview.id}); + if (channel) { + channelPreview.last_message = self._addMessage(channel.last_message); + } + }); + // sort channels: 1. unread, 2. chat, 3. date of last msg + channelsPreview.sort(function (c1, c2) { + return Math.min(1, c2.unread_counter) - Math.min(1, c1.unread_counter) || + c2.is_chat - c1.is_chat || + !!c2.last_message - !!c1.last_message || + (c2.last_message && c2.last_message.date.diff(c1.last_message.date)); + }); + + // generate last message preview (inline message body and compute date to display) + _.each(channelsPreview, function (channel) { + if (channel.last_message) { + channel.last_message_preview = utils.parse_and_transform(channel.last_message.body, utils.inline); + channel.last_message_date = channel.last_message.date.fromNow(); + } + }); + return channelsPreview; + }); + }, + /** + * @return {web.Bus} the chat bus + */ + getChatBus: function () { + return this.chatBus; + }, + /** + * Show the list of available commands next to a message (e.g. star) + * + * @param {Object} channel + * @return {Array} + */ + getCommands: function (channel) { + return _.filter(this.commands, function (command) { + return !command.channel_types || _.contains(command.channel_types, channel.server_type); + }); + }, + /** + * Returns the record id of ir.ui.menu for Discuss + * + * @return {integer} record id + */ + getDiscussMenuID: function () { + return this.discussMenuID; + }, + /** + * Gets direct message channel + * + * @param {integer} partnerID + * @return {Object|undefined} channel + */ + getDmFromPartnerID: function (partnerID) { + return _.findWhere(this.channels, {direct_partner_id: partnerID}); + }, + /** + * Returns list of emojis Objects + * + * @return {Object[]} + * ['id', 'source', 'unicode_source', 'substitution', 'description'] + */ + getEmojis: function () { + return this.emojis; + }, + /** + * Get the last seen message for a given channel + * + * @param {Object} channel + * @return {Object} Message Object + */ + getLastSeenMessage: function (channel) { + if (channel.last_seen_message_id) { + var messages = channel.cache['[]'].messages; + var msg = _.findWhere(messages, {id: channel.last_seen_message_id}); + if (msg) { + var i = _.sortedIndex(messages, msg, 'id') + 1; + while (i < messages.length && + (messages[i].is_author || messages[i].is_system_notification)) { + msg = messages[i]; + i++; + } + return msg; + } + } + }, + /** + * Get all listeners of a channel. + * + * @param {Object} channel + * @return {$.Promise<Array<Object[]>>|Array<Object[]>} Two cases: + * + * 1. 'Channel' provided + * => Promise resolved with a list containing a list of members + * (list is cached in channel.membersDeferred) + * 2. No 'channel' provided + * => list containing a list of members (cached by chat_manager) + */ + getMentionPartnerSuggestions: function (channel) { + var self = this; + if (!channel) { + return this.mentionPartnerSuggestions; + } + if (!channel.membersDeferred) { + channel.membersDeferred = this._rpc({ + model: 'mail.channel', + method: 'channel_fetch_listeners', + args: [channel.uuid], + }, { + shadow: true + }) + .then(function (members) { + var suggestions = []; + _.each(self.mentionPartnerSuggestions, function (partners) { + suggestions.push(_.filter(partners, function (partner) { + return !_.findWhere(members, { id: partner.id }); + })); + }); + + return [suggestions]; + }); + } + return channel.membersDeferred; + }, + /** + * Gets message from its id + * + * @param {integer} msgID + * @return {Object|undefined} Message Object + */ + getMessage: function (msgID) { + return _.findWhere(this.messages, {id: msgID}); + }, + /** + * Gets messages from channel or ids or record (model and res_id): + * + * 1. From channel if we have 'channelID' in options + * 2. From ids if we have 'ids' in options + * 3. From model if we have 'model' and 'res_id' in options + * + * Rule of precedence: + * + * 'channelID' < 'ids' < 'model' and 'res_id' + * + * If we have none of the cases above, we return an empty list. + * + * @param {Object} [options] + * @param {integer|string} [options.channelID] + * @param {Array} [options.domain] + * @param {integer[]} [options.ids] + * @param {boolean} [options.load_more] + * @param {string} [options.model] + * @param {integer} [options.res_id] + * @return {$.Promise<Object[]>} list of messages + */ + getMessages: function (options) { + var channel; + var self = this; + + if ('channelID' in options && options.load_more) { + // get channel messages, force load_more + channel = this.getChannel(options.channelID); + return this._fetchFromChannel(channel, {domain: options.domain || {}, loadMore: true}); + } + if ('channelID' in options) { + // channel message, check in cache first + channel = this.getChannel(options.channelID); + var channelCache = this._getChannelCache(channel, options.domain); + if (channelCache.loaded) { + return $.when(channelCache.messages); + } else { + return this._fetchFromChannel(channel, {domain: options.domain}); + } + } + if ('ids' in options) { + // get messages from their ids (chatter is the main use case) + return this._fetchDocumentMessages(options.ids, options).then(function (result) { + self.markAsRead(options.ids); + return result; + }); + } + if ('model' in options && 'res_id' in options) { + // get messages for a chatter, when it doesn't know the ids (use + // case is when using the full composer) + var domain = [['model', '=', options.model], ['res_id', '=', options.res_id]]; + return this._rpc({ + model: 'mail.message', + method: 'message_fetch', + args: [domain], + kwargs: {limit: LIMIT}, + }) + .then(function (msgs) { + return _.map(msgs, self._addMessage.bind(self)); + }); + } + return $.when([]); + }, + /** + * Returns the number of messages received from followed channels + * + all messages where the current user is notified. + * + * @return {integer} + */ + getNeedactionCounter: function () { + return this.needactionCounter; + }, + /** + * Gets the number of starred message + * + * @return {integer} + */ + getStarredCounter: function () { + return this.starredCounter; + }, + /** + * Gets the number of conversation which contains unread messages + * + * @return {integer} + */ + getUnreadConversationCounter: function () { + return this.unreadConversationCounter; + }, + /** + * States whether all messages have been loaded or not + * + * @param {Object} channel + * @param {Array} domain + * @return {boolean} + */ + isAllHistoryLoaded: function (channel, domain) { + return this._getChannelCache(channel, domain).all_history_loaded; + }, + /** + * @return {$.Promise} + */ + isReady: function () { + return this._isReady; + }, + /** + * join an existing channel + * See @createChannel to join a new channel + * + * @param {integer} channelID + * @param {Object} [options] + * @return {$.Promise<Object>} resolved with channel object + */ + joinChannel: function (channelID, options) { + var self = this; + if (channelID in this.channelDefs) { + // prevents concurrent calls to channel_join_and_get_info + return this.channelDefs[channelID]; + } + var channel = this.getChannel(channelID); + if (channel) { + // channel already joined + this.channelDefs[channelID] = $.when(channel); + } else { + this.channelDefs[channelID] = this._rpc({ + model: 'mail.channel', + method: 'channel_join_and_get_info', + args: [[channelID]], + }) + .then(function (result) { + return self._addChannel(result, options); + }); + } + return this.channelDefs[channelID]; + }, + /** + * Marks all messages from a channel as read + * + * @param {Object} channel + * @param {Array} domain + * @return {$.Promise} + */ + markAllAsRead: function (channel, domain) { + if ((channel.id === "channel_inbox" && this.needactionCounter) || + (channel && channel.needaction_counter)) { + return this._rpc({ + model: 'mail.message', + method: 'mark_all_as_read', + kwargs: { + channel_ids: channel.id !== "channel_inbox" ? [channel.id] : [], + domain: domain, + }, + }); + } + return $.when(); + }, + /** + * Mark messages as read + * + * @param {Array} msgIDs list of messages ids + * @return {$.Promise} + */ + markAsRead: function (msgIDs) { + var self = this; + var ids = _.filter(msgIDs, function (id) { + var message = _.findWhere(self.messages, {id: id}); + // If too many messages, not all are fetched, and some might not be found + return !message || message.is_needaction; + }); + if (ids.length) { + return this._rpc({ + model: 'mail.message', + method: 'set_message_done', + args: [ids], + }); + } else { + return $.when(); + } + }, + /** + * Marks a channel as seen. + * The seen message will be the last message. + * Resolved with the last seen message, only for non-static channels + * + * @param {Object} channel + * @return {$.Promise<integer|undefined>} last message id seen in the channel + */ + markChannelAsSeen: function (channel) { + if (channel.unread_counter > 0 && channel.type !== 'static') { + this._updateChannelUnreadCounter(channel, 0); + return this.channelSeen(channel); + } + return $.when(); + }, + /** + * Opens the chat window in discuss. + * + * @param {integer} partnerID + * @return {$.Promise<Object>} resolved with the dm channel + */ + openAndDetachDm: function (partnerID) { + return this._rpc({ + model: 'mail.channel', + method: 'channel_get_and_minimize', + args: [[partnerID]], + }) + .then(this._addChannel.bind(this)); + }, + /** + * Open the channel: + * + * 1. If discuss is opened, asks discuss to open the channel + * 2. Otherwise, asks the chat_window_manager to detach the channel + * + * @param {Object} channel + */ + openChannel: function (channel) { + this.chatBus.trigger(this.discussOpen ? 'open_channel' : 'detach_channel', channel); + }, + /** + * Prepares and sends a message to the server: + * + * 1. Either the message is posted on a channel + * 2. Or the message is posted in a model's record (chatter) + * + * Rule of precedence: + * + * 'channelID' < 'model' & 'res_id' + * + * If options as none of these parameters, do nothing and return + * a promise no resolved item. + * + * @param {Object} data data related to the new message + * @param {Object} options + * @param {string|integer} [options.channelID] + * @param {string} [options.model] + * @param {integer} [options.res_id] + * @return {$.Promise} + */ + postMessage: function (data, options) { + var self = this; + options = options || {}; + + // 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 = utils.parse_and_transform(_.str.trim(data.content), utils.add_link); + + var msg = { + partner_ids: data.partner_ids, + body: body, + attachment_ids: data.attachment_ids, + }; + + // Replace emojis by their unicode character + _.each(_.keys(this.emojiUnicodes), function (key) { + var escapedKey = String(key).replace(/([.*+?=^!:${}()|[\]/\\])/g, '\\$1'); + var regexp = new RegExp("(\\s|^)(" + escapedKey + ")(?=\\s|$)", "g"); + msg.body = msg.body.replace(regexp, "$1" + self.emojiUnicodes[key]); + }); + if ('subject' in data) { + msg.subject = data.subject; + } + if ('channelID' in options) { + // post a message in a channel or execute a command + return this._rpc({ + model: 'mail.channel', + method: data.command ? 'execute_command' : 'message_post', + args: [options.channelID], + kwargs: _.extend(msg, { + message_type: 'comment', + content_subtype: 'html', + subtype: 'mail.mt_comment', + command: data.command, + }), + }); + } + if ('model' in options && 'res_id' in options) { + // post a message in a chatter + _.extend(msg, { + content_subtype: data.content_subtype, + context: data.context, + message_type: data.message_type, + subtype: data.subtype, + subtype_id: data.subtype_id, + }); + + return this._rpc({ + model: options.model, + method: 'message_post', + args: [options.res_id], + kwargs: msg, + }) + .then(function (msgID) { + return self._rpc({ + model: 'mail.message', + method: 'message_format', + args: [[msgID]], + }) + .then(function (msgs) { + msgs[0].model = options.model; + msgs[0].res_id = options.res_id; + self._addMessage(msgs[0]); + }); + }); + } + return $.when(); + }, + /** + * Special redirection handling for given model and id + * + * If the model is res.partner, and there is a user associated with this + * partner which isn't the current user, open the DM with this user. + * Otherwhise, open the record's form view, if this is not the current user's. + * + * @param {string} resModel model to open + * @param {integer} resID record to open + * @param {function} [dmRedirectionCallback] only used if 'res.partner' + */ + redirect: function (resModel, resID, dmRedirectionCallback) { + var self = this; + var redirectToDocument = function (resModel, resID, viewID) { + web_client.do_action({ + type:'ir.actions.act_window', + view_type: 'form', + view_mode: 'form', + res_model: resModel, + views: [[viewID || false, 'form']], + res_id: resID, + }); + }; + if (resModel === 'res.partner') { + var domain = [['partner_id', '=', resID]]; + this._rpc({ + model: 'res.users', + method: 'search', + args: [domain], + }) + .then(function (userIDs) { + if (userIDs.length && userIDs[0] !== session.uid && dmRedirectionCallback) { + self.createChannel(resID, 'dm').then(dmRedirectionCallback); + } else { + redirectToDocument(resModel, resID); + } + }); + } else { + this._rpc({ + model: resModel, + method: 'get_formview_id', + args: [[resID], session.user_context], + }) + .then(function (viewID) { + redirectToDocument(resModel, resID, viewID); + }); + } + }, + /** + * Removes all messages from the current model except 'needaction'. + * We want to keep it in inbox. + * + * @param {string} model + */ + removeChatterMessages: function (model) { + this.messages = _.reject(this.messages, function (message) { + return (!message.channel_ids || message.channel_ids.length === 0) && message.model === model; + }); + }, + /** + * Search among prefetched partners, using the string 'searchVal' + * + * @param {string} searchVal + * @param {integer} limit max number of found partners in the response + * @return {$.Promise<Object[]>} list of found partners (matching 'searchVal') + */ + searchPartner: function (searchVal, limit) { + var def = $.Deferred(); + var values = []; + // search among prefetched partners + var searchRegexp = new RegExp(_.str.escapeRegExp(utils.unaccent(searchVal)), 'i'); + _.each(this.mentionPartnerSuggestions, function (partners) { + if (values.length < limit) { + values = values.concat(_.filter(partners, function (partner) { + return session.partner_id !== partner.id && searchRegexp.test(partner.name); + })).splice(0, limit); + } + }); + if (!values.length) { + // extend the research to all users + def = this._rpc({ + model: 'res.partner', + method: 'im_search', + args: [searchVal, limit || 20], + }, { + shadow: true, + }); + } else { + def = $.when(values); + } + return def.then(function (values) { + var autocompleteData = _.map(values, function (value) { + return { id: value.id, value: value.name, label: value.name }; + }); + return _.sortBy(autocompleteData, 'label'); + }); + }, + /** + * Stars or unstars message + * + * @param {integer} msgID + * @return {$.Promise} + */ + toggleStarStatus: function (msgID) { + return this._rpc({ + model: 'mail.message', + method: 'toggle_message_starred', + args: [[msgID]], + }); + }, + /** + * Unstars all messages from all channels + * + * @return {$.Promise} + */ + unstarAll: function () { + return this._rpc({ + model: 'mail.message', + method: 'unstar_all', + args: [[]] + }); + }, + /** + * Unsubscribes from channel + * + * @param {Object} channel + * @param {integer|string} [channel.id] mandatory if channel is of type 'public' or 'private' + * @param {string} [channel.type] + * @param {string} [channel.uuid] mandatory if channel is not of type 'public' or 'private' + * @return {$.Promise} + */ + unsubscribe: function (channel) { + if (_.contains(['public', 'private'], channel.type)) { + return this._rpc({ + model: 'mail.channel', + method: 'action_unfollow', + args: [[channel.id]], + }); + } else { + return this._rpc({ + model: 'mail.channel', + method: 'channel_pin', + args: [channel.uuid, false], + }); + } + }, + + //-------------------------------------------------------------------------- + // Private + //-------------------------------------------------------------------------- + + /** + * Adds a channel and returns it. + * Simply returns the channel if already exists. + * + * @private + * @param {Object} data + * @param {string|integer} data.id id of channel or 'channel_inbox', 'channel_starred', ... + * @param {string|Object} data.name name of channel, e.g. 'general' + * @param {string} data.type type of channel, e.g. 'static' + * @param {string} [data.state] e.g. 'open', 'folded' + * @param {Object|integer} [options=undefined] + * @param {boolean} [options.silent] + * @return {Object} the newly or already exisiting channel + */ + _addChannel: function (data, options) { + options = typeof options === "object" ? options : {}; + var channel = this.getChannel(data.id); + if (channel) { + if (channel.is_folded !== (data.state === "folded")) { + channel.is_folded = (data.state === "folded"); + this.call('chat_window_manager', 'toggleFoldChat', channel); + } + } else { + channel = this._makeChannel(data, options); + this.channels.push(channel); + if (data.last_message) { + channel.last_message = this._addMessage(data.last_message); + } + // In case of a static channel (Inbox, Starred), the name is translated thanks to _lt + // (lazy translate). In this case, channel.name is an object, not a string. + this.channels = _.sortBy(this.channels, function (channel) { + return _.isString(channel.name) ? channel.name.toLowerCase() : ''; + }); + if (!options.silent) { + this.chatBus.trigger("new_channel", channel); + } + if (channel.is_detached) { + this.call('chat_window_manager', 'openChat', channel); + } + } + return channel; + }, + /** + * Adds a channel to a message + * Usefull when you mark a message as 'to do'. + * The message will be available in 'Starred' channel. + * + * @private + * @param {Object} message + * @param {string} channelID + */ + _addChannelToMessage: function (message, channelID) { + if (!message.channel_ids) { + message.channel_ids = []; + } + message.channel_ids.push(channelID); + message.channel_ids = _.uniq(message.channel_ids); + }, + /** + * Creates a new message + * + * @private + * @param {Object} data message data + * @param {integer} data.id + * @param {Object} [options] + * @param {Array} [options.domain] + * @param {boolean} [options.increment_unread] whether we should increment + * the unread_counter of channel. + * @param {boolean} [options.silent] whether it should inform in the chatBus + * of the newly created message. + * @return {Object} message object + */ + _addMessage: function (data, options) { + var self = this; + options = options || {}; + var msg = _.findWhere(this.messages, { id: data.id }); + + if (!msg) { + msg = this._makeMessage(data); + // Keep the array ordered by id when inserting the new message + this.messages.splice(_.sortedIndex(this.messages, msg, 'id'), 0, msg); + _.each(msg.channel_ids, function (channelID) { + var channel = self.getChannel(channelID); + if (channel) { + // update the channel's last message (displayed in the channel + // preview, in mobile) + if (!channel.last_message || msg.id > channel.last_message.id) { + channel.last_message = msg; + } + self._addToCache(msg, []); + if (options.domain && options.domain !== []) { + self._addToCache(msg, options.domain); + } + if (channel.hidden) { + channel.hidden = false; + self.chatBus.trigger('new_channel', channel); + } + if (channel.type !== 'static' && !msg.is_author && !msg.is_system_notification) { + if (options.increment_unread) { + self._updateChannelUnreadCounter(channel, channel.unread_counter+1); + } + if (channel.is_chat && options.show_notification) { + if (!self.discussOpen && !config.device.isMobile) { + // automatically open chat window + self.call('chat_window_manager', 'openChat', channel, { passively: true }); + } + var query = {is_displayed: false}; + self.chatBus.trigger('anyone_listening', channel, query); + self._notifyIncomingMessage(msg, query); + } + } + } + }); + if (!options.silent) { + this.chatBus.trigger('new_message', msg); + } + } else if (options.domain && options.domain !== []) { + this._addToCache(msg, options.domain); + } + return msg; + }, + /** + * Stores message to channelCache. + * + * @private + * @param {Object} message + * @param {Array} domain + */ + _addToCache: function (message, domain) { + var self = this; + _.each(message.channel_ids, function (channelID) { + var channel = self.getChannel(channelID); + if (channel) { + var channelCache = self._getChannelCache(channel, domain); + var index = _.sortedIndex(channelCache.messages, message, 'id'); + if (channelCache.messages[index] !== message) { + channelCache.messages.splice(index, 0, message); + } + } + }); + }, + /** + * Gets messages from their ids + * This method is used when the chatter linked to a record need to be loaded. + * + * @private + * @param {Array} msgIDs message ids to load + * @param {Object} options + * @return {$.Promise<Object[]>} resolved with fetched messages + */ + _fetchDocumentMessages : function (msgIDs, options) { + var self = this; + var loadedMsgs = _.filter(this.messages, function (message) { + return _.contains(msgIDs, message.id); + }); + var loadedMsgIDs = _.pluck(loadedMsgs, 'id'); + + options = options || {}; + if (options.forceFetch || _.difference(msgIDs.slice(0, LIMIT), loadedMsgIDs).length) { + var idsToLoad = _.difference(msgIDs, loadedMsgIDs).slice(0, LIMIT); + return this._rpc({ + model: 'mail.message', + method: 'message_format', + args: [idsToLoad], + context: session.user_context, + }) + .then(function (msgs) { + var processedMsgs = []; + _.each(msgs, function (msg) { + processedMsgs.push(self._addMessage(msg, {silent: true})); + }); + return _.sortBy(loadedMsgs.concat(processedMsgs), function (msg) { + return msg.id; + }); + }); + } else { + return $.when(loadedMsgs); + } + }, + /** + * Gets messages from channel + * + * @private + * @param {Object} channel + * @param {integer|string} channel.id string for static channels, e.g. 'channel_inbox' + * @param {Object} [option={}] + * @param {Array} [options.domain] filter on the messages of the channel + * @param {boolean} [options.load_more] Whether it should load more message + * @return {$.Promise<Object[]>} resolved with list of messages + */ + _fetchFromChannel: function (channel, options) { + var self = this; + options = options || {}; + var domain = + (channel.id === "channel_inbox") ? [['needaction', '=', true]] : + (channel.id === "channel_starred") ? [['starred', '=', true]] : + [['channel_ids', 'in', channel.id]]; + var cache = this._getChannelCache(channel, options.domain); + + if (options.domain) { + domain = domain.concat(options.domain || []); + } + if (options.loadMore) { + var minMessageID = cache.messages[0].id; + domain = [['id', '<', minMessageID]].concat(domain); + } + + return this._rpc({ + model: 'mail.message', + method: 'message_fetch', + args: [domain], + kwargs: {limit: LIMIT, context: session.user_context}, + }) + .then(function (msgs) { + if (!cache.all_history_loaded) { + cache.all_history_loaded = msgs.length < LIMIT; + } + cache.loaded = true; + + _.each(msgs, function (msg) { + self._addMessage(msg, { + channel_id: channel.id, + silent: true, + domain: options.domain, + }); + }); + var channelCache = self._getChannelCache(channel, options.domain || []); + return channelCache.messages; + }); + }, + /** + * Gets channel content from the cache. + * Usefull to get cached messages from a channel. + * + * @private + * @param {Object} channel + * @param {Object[]} channel.cache + * @param {Array} domain + * @return {Object|undefined} + */ + _getChannelCache: function (channel, domain) { + var stringifiedDomain = JSON.stringify(domain || []); + if (!channel.cache[stringifiedDomain]) { + channel.cache[stringifiedDomain] = { + all_history_loaded: false, + loaded: false, + messages: [], + }; + } + return channel.cache[stringifiedDomain]; + }, + /** + * @private + * @returns {$.Promise} + */ + _initMessaging: function () { + var self = this; + return session.is_bound.then(function (){ + var context = _.extend({isMobile: config.device.isMobile}, session.user_context); + return self._rpc({ + route: '/mail/init_messaging', + params: {context: context}, + }); + }).then(function (result) { + _.each(result.channel_slots, function (channels) { + _.each(channels, self._addChannel.bind(self)); + }); + self.needactionCounter = result.needaction_inbox_counter || 0; + self.starredCounter = result.starred_counter || 0; + self.commands = _.map(result.commands, function (command) { + return _.extend({ id: command.name }, command); + }); + self.mentionPartnerSuggestions = result.mention_partner_suggestions; + self.discussMenuID = result.menu_id; + + // Shortcodes: canned responses and emojis + _.each(result.shortcodes, function (s) { + if (s.shortcode_type === 'text') { + self.cannedResponses.push(_.pick(s, ['id', 'source', 'substitution'])); + } else { + self.emojis.push( + _.pick(s, ['id', 'source', 'unicode_source', 'substitution', 'description']) + ); + self.emojiSubstitutions[_.escape(s.source)] = s.substitution; + if (s.unicode_source) { + self.emojiSubstitutions[_.escape(s.unicode_source)] = s.substitution; + self.emojiUnicodes[_.escape(s.source)] = s.unicode_source; + } + } + }); + self.busBus.start_polling(); + }); + }, + /** + * Clear cache of a channel + * + * @private + * @param {Array} channelIDs must be int or string + */ + _invalidateCaches: function (channelIDs) { + var self = this; + _.each(channelIDs, function (channelID) { + var channel = self.getChannel(channelID); + if (channel) { + channel.cache = { '[]': channel.cache['[]']}; + } + }); + }, + /** + * Creates channel object + * + * @private + * @param {Object} data + * @param {string} [data.anonymous_name] + * @param {string} data.channel_type + * @param {Object[]} [data.direct_partner] + * @param {boolean} [data.group_based_subscription] + * @param {integer|string} data.id + * @param {boolean} data.is_minimized + * @param {string} [data.last_message_date] + * @param {boolean} data.mass_mailing + * @param {integer} [data.message_needaction_counter] + * @param {integer} [data.message_unread_counter] + * @param {string} data.name + * @param {string} [data.public] + * @param {integer} data.seen_message_id + * @param {string} [data.state] + * @param {string} [data.type] + * @param {string} channel.uuid + * @param {Object} options + * @param {boolean} [options.autoswitch] + * @param {boolean} options.displayNeedactions + * @param {boolean} [options.hidden] + * @return {Object} channel + */ + _makeChannel: function (data, options) { + var channel = { + id: data.id, + name: data.name, + server_type: data.channel_type, + type: data.type || data.channel_type, + all_history_loaded: false, + uuid: data.uuid, + is_detached: data.is_minimized, + is_folded: data.state === "folded", + autoswitch: 'autoswitch' in options ? options.autoswitch : true, + hidden: options.hidden, + display_needactions: options.displayNeedactions, + mass_mailing: data.mass_mailing, + group_based_subscription: data.group_based_subscription, + needaction_counter: data.message_needaction_counter || 0, + unread_counter: 0, + last_seen_message_id: data.seen_message_id, + cache: {'[]': { + all_history_loaded: false, + loaded: false, + messages: [], + }}, + }; + if (channel.type === "channel") { + channel.type = data.public !== "private" ? "public" : "private"; + } + if (_.size(data.direct_partner) > 0) { + channel.type = "dm"; + channel.name = data.direct_partner[0].name; + channel.direct_partner_id = data.direct_partner[0].id; + channel.status = data.direct_partner[0].im_status; + this.pinnedDmPartners.push(channel.direct_partner_id); + this.busBus.update_option('bus_presence_partner_ids', this.pinnedDmPartners); + } else if ('anonymous_name' in data) { + channel.name = data.anonymous_name; + } + if (data.last_message_date) { + channel.last_message_date = moment(time.str_to_datetime(data.last_message_date)); + } + channel.is_chat = !channel.type.match(/^(public|private|static)$/); + if (data.message_unread_counter) { + this._updateChannelUnreadCounter(channel, data.message_unread_counter); + } + return channel; + }, + /** + * Creates message object + * + * @private + * @param {Object} data + * @param {integer[]} [data.attachment_ids=[]] + * @param {integer[]} [data.author_id] + * @param {string} [data.body=""] + * @param {Array} data.channel_ids contains integers and strings + * @param {string} data.customer_email_status + * @param {string} data.date + * @param {string} data.email_from + * @param {integer} data.id + * @param {string} [data.info] + * @param {boolean} data.is_discussion + * @param {boolean} data.is_note + * @param {string} data.message_type + * @param {string} [data.model] + * @param {boolean} data.module_icon src url of the module icon + * @param {string} data.record_name + * @param {integer} data.res_id + * @param {string} data.subject + * @param {string} data.subtype_description + * @param {integer[]} data.tracking_value_ids + * @return {Object} message + */ + _makeMessage: function (data) { + var self = this; + var msg = { + id: data.id, + author_id: data.author_id, + body: data.body || "", + date: moment(time.str_to_datetime(data.date)), + message_type: data.message_type, + subtype_description: data.subtype_description, + is_author: data.author_id && data.author_id[0] === session.partner_id, + is_note: data.is_note, + is_discussion: data.is_discussion, + is_system_notification: (data.message_type === 'notification' && data.model === 'mail.channel') + || data.info === 'transient_message', + attachment_ids: data.attachment_ids || [], + subject: data.subject, + email_from: data.email_from, + customer_email_status: data.customer_email_status, + customer_email_data: data.customer_email_data, + record_name: data.record_name, + tracking_value_ids: data.tracking_value_ids, + channel_ids: data.channel_ids, + model: data.model, + res_id: data.res_id, + url: session.url("/mail/view?message_id=" + data.id), + module_icon:data.module_icon, + }; + + _.each(_.keys(this.emojiSubstitutions), function (key) { + var escapedKey = String(key).replace(/([.*+?=^!:${}()|[\]/\\])/g, '\\$1'); + var regexp = new RegExp("(?:^|\\s|<[a-z]*>)(" + escapedKey + ")(?=\\s|$|</[a-z]*>)", "g"); + msg.body = msg.body.replace(regexp, ' <span class="o_mail_emoji">'+self.emojiSubstitutions[key]+'</span> '); + }); + + function propertyDescr(channel) { + return { + enumerable: true, + get: function () { + return _.contains(msg.channel_ids, channel); + }, + set: function (bool) { + if (bool) { + self._addChannelToMessage(msg, channel); + } else { + msg.channel_ids = _.without(msg.channel_ids, channel); + } + } + }; + } + + Object.defineProperties(msg, { + is_starred: propertyDescr("channel_starred"), + is_needaction: propertyDescr("channel_inbox"), + }); + + if (_.contains(data.needaction_partner_ids, session.partner_id)) { + msg.is_needaction = true; + } + if (_.contains(data.starred_partner_ids, session.partner_id)) { + msg.is_starred = true; + } + if (msg.model === 'mail.channel') { + var realChannels = _.without(msg.channel_ids, 'channel_inbox', 'channel_starred'); + var origin = realChannels.length === 1 ? realChannels[0] : undefined; + var channel = origin && this.getChannel(origin); + if (channel) { + msg.origin_id = origin; + msg.origin_name = channel.name; + } + } + + // Compute displayed author name or email + if ((!msg.author_id || !msg.author_id[0]) && msg.email_from) { + msg.mailto = msg.email_from; + } else { + msg.displayed_author = (msg.author_id === ODOOBOT_ID) && "OdooBot" || + msg.author_id && msg.author_id[1] || + msg.email_from || _t('Anonymous'); + } + + // Don't redirect on author clicked of self-posted or OdooBot messages + msg.author_redirect = !msg.is_author && msg.author_id !== ODOOBOT_ID; + + // Compute the avatar_url + if (msg.author_id === ODOOBOT_ID) { + msg.avatar_src = "/mail/static/src/img/odoo_o.png"; + } else if (msg.author_id && msg.author_id[0]) { + msg.avatar_src = "/web/image/res.partner/" + msg.author_id[0] + "/image_small"; + } else if (msg.message_type === 'email') { + msg.avatar_src = "/mail/static/src/img/email_icon.png"; + } else { + msg.avatar_src = "/mail/static/src/img/smiley/avatar.jpg"; + } + + // add anchor tags to urls + msg.body = utils.parse_and_transform(msg.body, utils.add_link); + + // Compute url of attachments + _.each(msg.attachment_ids, function (a) { + a.url = '/web/content/' + a.id + '?download=true'; + }); + + // format date to the local only once by message + // can not be done in preprocess, since it alter the original value + if (msg.tracking_value_ids && msg.tracking_value_ids.length) { + var format; + _.each(msg.tracking_value_ids, function (f) { + if (f.field_type === 'datetime') { + format = 'LLL'; + if (f.old_value) { + f.old_value = moment.utc(f.old_value).local().format(format); + } + if (f.new_value) { + f.new_value = moment.utc(f.new_value).local().format(format); + } + } else if (f.field_type === 'date') { + format = 'LL'; + if (f.old_value) { + f.old_value = moment(f.old_value).local().format(format); + } + if (f.new_value) { + f.new_value = moment(f.new_value).local().format(format); + } + } + }); + } + + return msg; + }, + /** + * @private + * @param {Object} data key, value to decide activity created or deleted + */ + _manageActivityUpdateNotification: function (data) { + this.chatBus.trigger('activity_updated', data); + }, + /** + * @private + * @param {Object} message + * @param {Array} message.channel_ids list of integers and strings, + * where strings for static channels, e.g. 'channel_inbox'. + */ + _manageChannelNotification: function (message) { + var self = this; + var def; + var channelAlreadyInCache = true; + if (message.channel_ids.length === 1) { + channelAlreadyInCache = !!this.getChannel(message.channel_ids[0]); + def = this.joinChannel(message.channel_ids[0], {autoswitch: false}); + } else { + def = $.when(); + } + def.then(function () { + // don't increment unread if channel wasn't in cache yet as + // its unread counter has just been fetched + self._addMessage(message, { + show_notification: true, + increment_unread: channelAlreadyInCache + }); + self._invalidateCaches(message.channel_ids); + }); + }, + /** + * @private + * @param {Object} data + * @param {integer|string} data.id string for static channels, e.g. 'channel_inbox' + * @param {integer} [data.last_message_id] mandatory if 'id' refers to an + * existing channel. + */ + _manageChannelSeenNotification: function (data) { + var channel = this.getChannel(data.id); + if (channel) { + channel.last_seen_message_id = data.last_message_id; + if (channel.unread_counter) { + this._updateChannelUnreadCounter(channel, 0); + } + } + }, + /** + * Controls the session of the chat window + * + * @private + * @param {Object} chatSession + * @param {string} chatSession.channel_type + * @param {string|integer} chatSession.id string for static channels, e.g. 'channel_inbox' + * @param {string} chatSession.info + * @param {boolean} chatSession.is_minimized + * @param {string} chatSession.name + * @param {string} chatSession.state + */ + _manageChatSessionNotification: function (chatSession) { + var channel; + if ((chatSession.channel_type === "channel") && (chatSession.state === "open")) { + this._addChannel(chatSession, {autoswitch: false}); + if (!chatSession.is_minimized && chatSession.info !== 'creation') { + web_client.do_notify(_t("Invitation"), _t("You have been invited to: ") + chatSession.name); + } + } + // partner specific change (open a detached window for example) + if ((chatSession.state === "open") || (chatSession.state === "folded")) { + channel = chatSession.is_minimized && this.getChannel(chatSession.id); + if (channel) { + channel.is_detached = true; + channel.is_folded = (chatSession.state === "folded"); + this.call('chat_window_manager', 'openChat', channel); + } + } else if (chatSession.state === "closed") { + channel = this.getChannel(chatSession.id); + if (channel) { + channel.is_detached = false; + this.call('chat_window_manager', 'closeChat', channel, {keep_open_if_unread: true}); + } + } + }, + /** + * Updates channel_inbox when a message has marked as read. + * + * @private + * @param {Object} data + * @param {integer[]} [data.channel_ids] + * @param {integer[]} [data.message_ids] + * @param {string} [data.type] + */ + _manageMarkAsReadNotification: function (data) { + var self = this; + _.each(data.message_ids, function (msgID) { + var message = _.findWhere(self.messages, { id: msgID }); + if (message) { + self._invalidateCaches(message.channel_ids); + self._removeMessageFromChannel("channel_inbox", message); + self.chatBus.trigger('update_message', message, data.type); + } + }); + if (data.channel_ids) { + _.each(data.channel_ids, function (channelID) { + var channel = self.getChannel(channelID); + if (channel) { + channel.needaction_counter = Math.max(channel.needaction_counter - data.message_ids.length, 0); + } + }); + } else { // if no channel_ids specified, this is a 'mark all read' in the inbox + _.each(this.channels, function (channel) { + channel.needaction_counter = 0; + }); + } + this.needactionCounter = Math.max(this.needactionCounter - data.message_ids.length, 0); + this.chatBus.trigger('update_needaction', this.needactionCounter); + }, + /** + * @private + * @param {Object} message + * @param {integer[]} message.channel_ids + */ + _manageNeedactionNotification: function (message) { + var self = this; + message = this._addMessage(message, { + channel_id: 'channel_inbox', + increment_unread: true, + show_notification: true, + }); + this._invalidateCaches(message.channel_ids); + if (message.channel_ids.length !== 0) { + this.needactionCounter++; + } + _.each(message.channel_ids, function (channelID) { + var channel = self.getChannel(channelID); + if (channel) { + channel.needaction_counter++; + } + }); + this.chatBus.trigger('update_needaction', this.needactionCounter); + }, + /** + * @private + * @param {Object} data structure depending on the type + * @param {integer} data.id + */ + _managePartnerNotification: function (data) { + if (data.info === "unsubscribe") { + var channel = this.getChannel(data.id); + if (channel) { + var msg; + if (_.contains(['public', 'private'], channel.type)) { + msg = _.str.sprintf(_t('You unsubscribed from <b>%s</b>.'), channel.name); + } else { + msg = _.str.sprintf(_t('You unpinned your conversation with <b>%s</b>.'), channel.name); + } + this._removeChannel(channel); + this.chatBus.trigger("unsubscribe_from_channel", data.id); + web_client.do_notify(_("Unsubscribed"), msg); + } + } else if (data.type === 'toggle_star') { + this._manageToggleStarNotification(data); + } else if (data.type === 'mark_as_read') { + this._manageMarkAsReadNotification(data); + } else if (data.info === 'channel_seen') { + this._manageChannelSeenNotification(data); + } else if (data.info === 'transient_message') { + this._manageTransientMessageNotification(data); + } else if (data.type === 'activity_updated') { + this._manageActivityUpdateNotification(data); + } else { + this._manageChatSessionNotification(data); + } + }, + /** + * @private + * @param {Object} data partner infos + * @param {integer} data.id + * @param {string} data.im_status + */ + _managePresenceNotification: function (data) { + var dm = this.getDmFromPartnerID(data.id); + if (dm) { + dm.status = data.im_status; + this.chatBus.trigger('update_dm_presence', dm); + } + }, + /** + * @private + * @param {Object} data + * @param {integer[]} data.message_ids + * @param {boolean} data.starred + * @param {string} data.type + */ + _manageToggleStarNotification: function (data) { + var self = this; + _.each(data.message_ids, function (msgID) { + var message = _.findWhere(self.messages, { id: msgID }); + if (message) { + self._invalidateCaches(message.channel_ids); + message.is_starred = data.starred; + if (!message.is_starred) { + self._removeMessageFromChannel("channel_starred", message); + self.starredCounter--; + } else { + self._addToCache(message, []); + var channelStarred = self.getChannel('channel_starred'); + channelStarred.cache = _.pick(channelStarred.cache, "[]"); + self.starredCounter++; + } + self.chatBus.trigger('update_message', message); + } + }); + this.chatBus.trigger('update_starred', this.starredCounter); + }, + /** + * @private + * @param {Object} data + * @param {string} data.author_id + */ + _manageTransientMessageNotification: function (data) { + var lastMessage = _.last(this.messages); + data.id = (lastMessage ? lastMessage.id : 0) + 0.01; + data.author_id = data.author_id || ODOOBOT_ID; + this._addMessage(data); + }, + /** + * shows a popup to notify a new received message. + * This will also rename the odoo tab browser if + * the user is not in it. + * + * @private + * @param {Object} msg message received + * @param {Array} msg.author_id contains [integer, string] + * @param {string} msg.body + * @param {Object} options + * @param {boolean} options.is_displayed + */ + _notifyIncomingMessage: function (msg, options) { + if (this.busBus.is_odoo_focused() && options.is_displayed) { + // no need to notify + return; + } + var title = _t('New message'); + if (msg.author_id[1]) { + title = _.escape(msg.author_id[1]); + } + var content = utils.parse_and_transform(msg.body, utils.strip_html) + .substr(0, PREVIEW_MSG_MAX_SIZE); + + if (!this.busBus.is_odoo_focused()) { + this.globalUnreadCounter++; + var tabTitle = _.str.sprintf(_t("%d Messages"), this.globalUnreadCounter); + web_client.set_title_part("_chat", tabTitle); + } + + this._sendNotification(web_client, title, content); + }, + /** + * Removes channel + * + * @private + * @param {Object} [channel] + * @param {integer} [channel.direct_partner_id] mandatory if type is 'dm' + * @param {integer|string} channel.id string for static channels, e.g. 'channel_inbox' + * @param {string} [channel.type] + */ + _removeChannel: function (channel) { + if (!channel) { return; } + if (channel.type === 'dm') { + var index = this.pinnedDmPartners.indexOf(channel.direct_partner_id); + if (index > -1) { + this.pinnedDmPartners.splice(index, 1); + this.busBus.update_option('bus_presence_partner_ids', this.pinnedDmPartners); + } + } + this.channels = _.without(this.channels, channel); + delete this.channelDefs[channel.id]; + }, + /** + * Removes a message from a channel. + * it will also remove cached message from the channel + * + * @private + * @param {integer|string} channelID string for static channels, e.g. 'channel_inbox' + * @param {Object} message + */ + _removeMessageFromChannel: function (channelID, message) { + message.channel_ids = _.without(message.channel_ids, channelID); + var channel = _.findWhere(this.channels, { id: channelID }); + _.each(channel.cache, function (cache) { + cache.messages = _.without(cache.messages, message); + }); + }, + /** + * Increments or decrements unreadConversationCounter + * + * @private + * @param {Object} channel + * @param {integer} counter + */ + _updateChannelUnreadCounter: function (channel, counter) { + if (channel.unread_counter > 0 && counter === 0) { + this.unreadConversationCounter = Math.max(0, this.unreadConversationCounter-1); + } else if (channel.unread_counter === 0 && counter > 0) { + this.unreadConversationCounter++; + } + channel.unread_counter = counter; + this.chatBus.trigger("update_channel_unread_counter", channel); + }, + + //-------------------------------------------------------------------------- + // Handlers + //-------------------------------------------------------------------------- + + /** + * Notification handlers + * Sometimes, the web client receives unsubscribe notification and an extra + * notification on that channel. This is then followed by an attempt to + * rejoin the channel that we just left. The next few lines remove the + * extra notification to prevent that situation to occur. + * + * @private + * @param {Array} notifications + */ + _onNotification: function (notifications) { + var self = this; + var unsubscribedNotif = _.find(notifications, function (notif) { + return notif[1].info === "unsubscribe"; + }); + if (unsubscribedNotif) { + notifications = _.reject(notifications, function (notif) { + return notif[0][1] === "mail.channel" && notif[0][2] === unsubscribedNotif[1].id; + }); + } + _.each(notifications, function (notification) { + var model = notification[0][1]; + if (model === 'ir.needaction') { + // new message in the inbox + self._manageNeedactionNotification(notification[1]); + } else if (model === 'mail.channel') { + // new message in a channel + self._manageChannelNotification(notification[1]); + } else if (model === 'res.partner') { + // channel joined/left, message marked as read/(un)starred, chat open/closed + self._managePartnerNotification(notification[1]); + } else if (model === 'bus.presence') { + // update presence of users + self._managePresenceNotification(notification[1]); + } + }); + }, + /** + * Global unread counter and notifications + * + * @private + */ + _onWindowFocus: function () { + this.globalUnreadCounter = 0; + web_client.set_title_part("_chat"); + }, + +}); + +return ChatManager; + +}); |
