summaryrefslogtreecommitdiff
path: root/addons/mail/static/src/js
diff options
context:
space:
mode:
authorAaron Bohy <aab@odoo.com>2017-12-21 12:29:50 +0100
committerAlexandre Kühn <alex.e.kuhn@gmail.com>2018-02-01 14:51:58 +0100
commit02ec09cb1c3e2d7bc7968f40c18f2208d7f3f498 (patch)
tree10d91136786b13666e78cfb94b12f1609d6f883d /addons/mail/static/src/js
parent0cd656b5af38051df3f7d6319e99705150bb1cd8 (diff)
[REF][IMP] *: mail js refactoring
This commit improves the JS code of the mail module, which comes maily from the new coding guidelines and the addition of "JS services". JS services are important objects that do not fit well in the component tree, such as chat_manager or ajax. The benefits of JS services are improved readability of the code, reduced coupling, and more testable modules. In particular, discuss was hard to Summary of the changes: - ClientAction has been renamed into Discuss - Clear instantiation of chatManager - New coding guidelines in most mail modules - JS Services can interact with each other - Chat Manager and Window Manager are services - Window Manager renamed to Chat Window Manager - bus.bus is now encapsulated in Bus Service (a service) - The test infrastructure has been tweaked with JS Services - Chat Mixin has been removed A future improvement would be to translate some 'trigger' into 'trigger_up'.
Diffstat (limited to 'addons/mail/static/src/js')
-rw-r--r--addons/mail/static/src/js/chat_manager.js1275
-rw-r--r--addons/mail/static/src/js/chat_mixin.js130
-rw-r--r--addons/mail/static/src/js/chat_window.js4
-rw-r--r--addons/mail/static/src/js/chatter.js5
-rw-r--r--addons/mail/static/src/js/chatter_composer.js1
-rw-r--r--addons/mail/static/src/js/composer.js15
-rw-r--r--addons/mail/static/src/js/discuss.js (renamed from addons/mail/static/src/js/client_action.js)305
-rw-r--r--addons/mail/static/src/js/discuss_mobile.js (renamed from addons/mail/static/src/js/client_action_mobile.js)38
-rw-r--r--addons/mail/static/src/js/extended_chat_window.js34
-rw-r--r--addons/mail/static/src/js/services/chat_manager.js1751
-rw-r--r--addons/mail/static/src/js/services/chat_window_manager.js532
-rw-r--r--addons/mail/static/src/js/systray.js43
-rw-r--r--addons/mail/static/src/js/thread.js16
-rw-r--r--addons/mail/static/src/js/thread_field.js27
-rw-r--r--addons/mail/static/src/js/utils.js43
-rw-r--r--addons/mail/static/src/js/window_manager.js400
16 files changed, 2537 insertions, 2082 deletions
diff --git a/addons/mail/static/src/js/chat_manager.js b/addons/mail/static/src/js/chat_manager.js
deleted file mode 100644
index 35d16915341..00000000000
--- a/addons/mail/static/src/js/chat_manager.js
+++ /dev/null
@@ -1,1275 +0,0 @@
-odoo.define('mail.chat_manager', function (require) {
-"use strict";
-
-var bus = require('bus.bus').bus;
-var utils = require('mail.utils');
-var config = require('web.config');
-var Bus = require('web.Bus');
-var core = require('web.core');
-var session = require('web.session');
-var time = require('web.time');
-var web_client = require('web.web_client');
-var Class = require('web.Class');
-var Mixins = require('web.mixins');
-var ServicesMixin = require('web.ServicesMixin');
-
-var _t = core._t;
-var _lt = core._lt;
-var LIMIT = 25;
-var preview_msg_max_size = 350; // optimal for native english speakers
-var ODOOBOT_ID = "ODOOBOT";
-
-// Private model
-//----------------------------------------------------------------------------------
-var messages = [];
-var channels = [];
-var channels_preview_def;
-var channel_defs = {};
-var chat_unread_counter = 0;
-var unread_conversation_counter = 0;
-var emojis = [];
-var emoji_substitutions = {};
-var emoji_unicodes = {};
-var needaction_counter = 0;
-var starred_counter = 0;
-var mention_partner_suggestions = [];
-var canned_responses = [];
-var commands = [];
-var discuss_menu_id;
-var global_unread_counter = 0;
-var pinned_dm_partners = []; // partner_ids we have a pinned DM with
-var client_action_open = false;
-
-// Global unread counter and notifications
-//----------------------------------------------------------------------------------
-bus.on("window_focus", null, function() {
- global_unread_counter = 0;
- web_client.set_title_part("_chat");
-});
-
-function notify_incoming_message (msg, options) {
- if (bus.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 (!bus.is_odoo_focused()) {
- global_unread_counter++;
- var tab_title = _.str.sprintf(_t("%d Messages"), global_unread_counter);
- web_client.set_title_part("_chat", tab_title);
- }
-
- utils.send_notification(web_client, title, content);
-}
-
-// Message and channel manipulation helpers
-//----------------------------------------------------------------------------------
-
-// options: channel_id, silent
-function add_message (data, options) {
- options = options || {};
- var msg = _.findWhere(messages, { id: data.id });
-
- if (!msg) {
- msg = chat_manager.make_message(data);
- // Keep the array ordered by id when inserting the new message
- messages.splice(_.sortedIndex(messages, msg, 'id'), 0, msg);
- _.each(msg.channel_ids, function (channel_id) {
- var channel = chat_manager.get_channel(channel_id);
- 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;
- }
- add_to_cache(msg, []);
- if (options.domain && options.domain !== []) {
- add_to_cache(msg, options.domain);
- }
- if (channel.hidden) {
- channel.hidden = false;
- chat_manager.bus.trigger('new_channel', channel);
- }
- if (channel.type !== 'static' && !msg.is_author && !msg.is_system_notification) {
- if (options.increment_unread) {
- update_channel_unread_counter(channel, channel.unread_counter+1);
- }
- if (channel.is_chat && options.show_notification) {
- if (!client_action_open && !config.device.isMobile) {
- // automatically open chat window
- chat_manager.bus.trigger('open_chat', channel, { passively: true });
- }
- var query = {is_displayed: false};
- chat_manager.bus.trigger('anyone_listening', channel, query);
- notify_incoming_message(msg, query);
- }
- }
- }
- });
- if (!options.silent) {
- chat_manager.bus.trigger('new_message', msg);
- }
- } else if (options.domain && options.domain !== []) {
- add_to_cache(msg, options.domain);
- }
- return msg;
-}
-
-function make_message (data) {
- 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(emoji_substitutions), function (key) {
- var escaped_key = String(key).replace(/([.*+?=^!:${}()|[\]\/\\])/g, '\\$1');
- var regexp = new RegExp("(?:^|\\s|<[a-z]*>)(" + escaped_key + ")(?=\\s|$|</[a-z]*>)", "g");
- msg.body = msg.body.replace(regexp, ' <span class="o_mail_emoji">'+emoji_substitutions[key]+'</span> ');
- });
-
- function property_descr(channel) {
- return {
- enumerable: true,
- get: function () {
- return _.contains(msg.channel_ids, channel);
- },
- set: function (bool) {
- if (bool) {
- add_channel_to_message(msg, channel);
- } else {
- msg.channel_ids = _.without(msg.channel_ids, channel);
- }
- }
- };
- }
-
- Object.defineProperties(msg, {
- is_starred: property_descr("channel_starred"),
- is_needaction: property_descr("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 real_channels = _.without(msg.channel_ids, 'channel_inbox', 'channel_starred');
- var origin = real_channels.length === 1 ? real_channels[0] : undefined;
- var channel = origin && chat_manager.get_channel(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) {
- _.each(msg.tracking_value_ids, function(f) {
- if (f.field_type === 'datetime') {
- var 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') {
- var 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;
-}
-
-function add_channel_to_message (message, channel_id) {
- message.channel_ids.push(channel_id);
- message.channel_ids = _.uniq(message.channel_ids);
-}
-
-function add_channel (data, options) {
- options = typeof options === "object" ? options : {};
- var channel = chat_manager.get_channel(data.id);
- if (channel) {
- if (channel.is_folded !== (data.state === "folded")) {
- channel.is_folded = (data.state === "folded");
- chat_manager.bus.trigger("channel_toggle_fold", channel);
- }
- } else {
- channel = chat_manager.make_channel(data, options);
- channels.push(channel);
- if (data.last_message) {
- channel.last_message = add_message(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.
- channels = _.sortBy(channels, function (channel) { return _.isString(channel.name) ? channel.name.toLowerCase() : '' });
- if (!options.silent) {
- chat_manager.bus.trigger("new_channel", channel);
- }
- if (channel.is_detached) {
- chat_manager.bus.trigger("open_chat", channel);
- }
- }
- return channel;
-}
-
-function make_channel (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.display_needactions,
- 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;
- pinned_dm_partners.push(channel.direct_partner_id);
- bus.update_option('bus_presence_partner_ids', pinned_dm_partners);
- } 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) {
- update_channel_unread_counter(channel, data.message_unread_counter);
- }
- return channel;
-}
-
-function remove_channel (channel) {
- if (!channel) { return; }
- if (channel.type === 'dm') {
- var index = pinned_dm_partners.indexOf(channel.direct_partner_id);
- if (index > -1) {
- pinned_dm_partners.splice(index, 1);
- bus.update_option('bus_presence_partner_ids', pinned_dm_partners);
- }
- }
- channels = _.without(channels, channel);
- delete channel_defs[channel.id];
-}
-
-function get_channel_cache (channel, domain) {
- var stringified_domain = JSON.stringify(domain || []);
- if (!channel.cache[stringified_domain]) {
- channel.cache[stringified_domain] = {
- all_history_loaded: false,
- loaded: false,
- messages: [],
- };
- }
- return channel.cache[stringified_domain];
-}
-
-function invalidate_caches(channel_ids) {
- _.each(channel_ids, function (channel_id) {
- var channel = chat_manager.get_channel(channel_id);
- if (channel) {
- channel.cache = { '[]': channel.cache['[]']};
- }
- });
-}
-
-function add_to_cache(message, domain) {
- _.each(message.channel_ids, function (channel_id) {
- var channel = chat_manager.get_channel(channel_id);
- if (channel) {
- var channel_cache = get_channel_cache(channel, domain);
- var index = _.sortedIndex(channel_cache.messages, message, 'id');
- if (channel_cache.messages[index] !== message) {
- channel_cache.messages.splice(index, 0, message);
- }
- }
- });
-}
-
-function remove_message_from_channel (channel_id, message) {
- message.channel_ids = _.without(message.channel_ids, channel_id);
- var channel = _.findWhere(channels, { id: channel_id });
- _.each(channel.cache, function (cache) {
- cache.messages = _.without(cache.messages, message);
- });
-}
-
-function update_channel_unread_counter (channel, counter) {
- if (channel.unread_counter > 0 && counter === 0) {
- unread_conversation_counter = Math.max(0, unread_conversation_counter-1);
- } else if (channel.unread_counter === 0 && counter > 0) {
- unread_conversation_counter++;
- }
- if (channel.is_chat) {
- chat_unread_counter = Math.max(0, chat_unread_counter - channel.unread_counter + counter);
- }
- channel.unread_counter = counter;
- chat_manager.bus.trigger("update_channel_unread_counter", channel);
-}
-
-// Notification handlers
-// ---------------------------------------------------------------------------------
-function on_notification (notifications) {
- // 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.
- var unsubscribed_notif = _.find(notifications, function (notif) {
- return notif[1].info === "unsubscribe";
- });
- if (unsubscribed_notif) {
- notifications = _.reject(notifications, function (notif) {
- return notif[0][1] === "mail.channel" && notif[0][2] === unsubscribed_notif[1].id;
- });
- }
- _.each(notifications, function (notification) {
- var model = notification[0][1];
- if (model === 'ir.needaction') {
- // new message in the inbox
- on_needaction_notification(notification[1]);
- } else if (model === 'mail.channel') {
- // new message in a channel
- on_channel_notification(notification[1]);
- } else if (model === 'res.partner') {
- // channel joined/left, message marked as read/(un)starred, chat open/closed
- on_partner_notification(notification[1]);
- } else if (model === 'bus.presence') {
- // update presence of users
- on_presence_notification(notification[1]);
- }
- });
-}
-
-function on_needaction_notification (message) {
- message = add_message(message, {
- channel_id: 'channel_inbox',
- show_notification: true,
- increment_unread: true,
- });
- invalidate_caches(message.channel_ids);
- if (message.channel_ids.length !== 0) {
- needaction_counter++;
- }
- _.each(message.channel_ids, function (channel_id) {
- var channel = chat_manager.get_channel(channel_id);
- if (channel) {
- channel.needaction_counter++;
- }
- });
- chat_manager.bus.trigger('update_needaction', needaction_counter);
-}
-
-function on_channel_notification (message) {
- var def;
- var channel_already_in_cache = true;
- if (message.channel_ids.length === 1) {
- channel_already_in_cache = !!chat_manager.get_channel(message.channel_ids[0]);
- def = chat_manager.join_channel(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
- add_message(message, { show_notification: true, increment_unread: channel_already_in_cache });
- invalidate_caches(message.channel_ids);
- });
-}
-
-function on_partner_notification (data) {
- if (data.info === "unsubscribe") {
- var channel = chat_manager.get_channel(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);
- }
- remove_channel(channel);
- chat_manager.bus.trigger("unsubscribe_from_channel", data.id);
- web_client.do_notify(_("Unsubscribed"), msg);
- }
- } else if (data.type === 'toggle_star') {
- on_toggle_star_notification(data);
- } else if (data.type === 'mark_as_read') {
- on_mark_as_read_notification(data);
- } else if (data.type === 'mark_as_unread') {
- on_mark_as_unread_notification(data);
- } else if (data.info === 'channel_seen') {
- on_channel_seen_notification(data);
- } else if (data.info === 'transient_message') {
- on_transient_message_notification(data);
- } else if (data.type === 'activity_updated') {
- onActivityUpdateNodification(data);
- } else {
- on_chat_session_notification(data);
- }
-}
-
-function on_toggle_star_notification (data) {
- _.each(data.message_ids, function (msg_id) {
- var message = _.findWhere(messages, { id: msg_id });
- if (message) {
- invalidate_caches(message.channel_ids);
- message.is_starred = data.starred;
- if (!message.is_starred) {
- remove_message_from_channel("channel_starred", message);
- starred_counter--;
- } else {
- add_to_cache(message, []);
- var channel_starred = chat_manager.get_channel('channel_starred');
- channel_starred.cache = _.pick(channel_starred.cache, "[]");
- starred_counter++;
- }
- chat_manager.bus.trigger('update_message', message);
- }
- });
- chat_manager.bus.trigger('update_starred', starred_counter);
-}
-
-function on_mark_as_read_notification (data) {
- _.each(data.message_ids, function (msg_id) {
- var message = _.findWhere(messages, { id: msg_id });
- if (message) {
- invalidate_caches(message.channel_ids);
- remove_message_from_channel("channel_inbox", message);
- chat_manager.bus.trigger('update_message', message, data.type);
- }
- });
- if (data.channel_ids) {
- _.each(data.channel_ids, function (channel_id) {
- var channel = chat_manager.get_channel(channel_id);
- 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(channels, function (channel) {
- channel.needaction_counter = 0;
- });
- }
- needaction_counter = Math.max(needaction_counter - data.message_ids.length, 0);
- chat_manager.bus.trigger('update_needaction', needaction_counter);
-}
-
-function on_mark_as_unread_notification (data) {
- _.each(data.message_ids, function (message_id) {
- var message = _.findWhere(messages, { id: message_id });
- if (message) {
- invalidate_caches(message.channel_ids);
- add_channel_to_message(message, 'channel_inbox');
- add_to_cache(message, []);
- }
- });
- var channel_inbox = chat_manager.get_channel('channel_inbox');
- channel_inbox.cache = _.pick(channel_inbox.cache, "[]");
-
- _.each(data.channel_ids, function (channel_id) {
- var channel = chat_manager.get_channel(channel_id);
- if (channel) {
- channel.needaction_counter += data.message_ids.length;
- }
- });
- needaction_counter += data.message_ids.length;
- chat_manager.bus.trigger('update_needaction', needaction_counter);
-}
-
-function on_channel_seen_notification (data) {
- var channel = chat_manager.get_channel(data.id);
- if (channel) {
- channel.last_seen_message_id = data.last_message_id;
- if (channel.unread_counter) {
- update_channel_unread_counter(channel, 0);
- }
- }
-}
-
-function on_chat_session_notification (chat_session) {
- var channel;
- if ((chat_session.channel_type === "channel") && (chat_session.state === "open")) {
- add_channel(chat_session, {autoswitch: false});
- if (!chat_session.is_minimized && chat_session.info !== 'creation') {
- web_client.do_notify(_t("Invitation"), _t("You have been invited to: ") + chat_session.name);
- }
- }
- // partner specific change (open a detached window for example)
- if ((chat_session.state === "open") || (chat_session.state === "folded")) {
- channel = chat_session.is_minimized && chat_manager.get_channel(chat_session.id);
- if (channel) {
- channel.is_detached = true;
- channel.is_folded = (chat_session.state === "folded");
- chat_manager.bus.trigger("open_chat", channel);
- }
- } else if (chat_session.state === "closed") {
- channel = chat_manager.get_channel(chat_session.id);
- if (channel) {
- channel.is_detached = false;
- chat_manager.bus.trigger("close_chat", channel, {keep_open_if_unread: true});
- }
- }
-}
-
-function on_presence_notification (data) {
- var dm = chat_manager.get_dm_from_partner_id(data.id);
- if (dm) {
- dm.status = data.im_status;
- chat_manager.bus.trigger('update_dm_presence', dm);
- }
-}
-
-function on_transient_message_notification (data) {
- var last_message = _.last(messages);
- data.id = (last_message ? last_message.id : 0) + 0.01;
- data.author_id = data.author_id || ODOOBOT_ID;
- add_message(data);
-}
-
-function onActivityUpdateNodification (data) {
- chat_manager.bus.trigger('activity_updated', data);
-}
-// Public interface
-//----------------------------------------------------------------------------------
-var ChatManager = Class.extend(Mixins.EventDispatcherMixin, ServicesMixin, {
- // these two functions are exposed for extensibility purposes and shouldn't be called by other modules
- make_message: make_message,
- make_channel: make_channel,
-
- init: function (parent) {
- var self = this;
- Mixins.EventDispatcherMixin.init.call(this);
- this.setParent(parent);
-
- this.bus = new Bus();
- this.bus.on('client_action_open', null, function (open) {
- client_action_open = open;
- });
-
- bus.on('notification', null, on_notification);
-
- this.channel_seen = _.throttle(function (channel) {
- return self._rpc({
- model: 'mail.channel',
- method: 'channel_seen',
- args: [[channel.id]],
- }, {
- shadow: true
- });
- }, 3000);
- },
-
- start: function () {
- this.is_ready = session.is_bound.then(function(){
- var context = _.extend({isMobile: config.device.isMobile}, session.user_context);
- return session.rpc('/mail/client_action', {context: context});
- }).then(this._onMailClientAction.bind(this));
-
- add_channel({
- id: "channel_inbox",
- name: _lt("Inbox"),
- type: "static",
- }, { display_needactions: true });
-
- add_channel({
- id: "channel_starred",
- name: _lt("Starred"),
- type: "static"
- });
- },
-
- _onMailClientAction: function (result) {
- _.each(result.channel_slots, function (channels) {
- _.each(channels, add_channel);
- });
- needaction_counter = result.needaction_inbox_counter;
- starred_counter = result.starred_counter;
- commands = _.map(result.commands, function (command) {
- return _.extend({ id: command.name }, command);
- });
- mention_partner_suggestions = result.mention_partner_suggestions;
- discuss_menu_id = result.menu_id;
-
- // Shortcodes: canned responses and emojis
- _.each(result.shortcodes, function (s) {
- if (s.shortcode_type === 'text') {
- canned_responses.push(_.pick(s, ['id', 'source', 'substitution']));
- } else {
- emojis.push(_.pick(s, ['id', 'source', 'unicode_source', 'substitution', 'description']));
- emoji_substitutions[_.escape(s.source)] = s.substitution;
- if (s.unicode_source) {
- emoji_substitutions[_.escape(s.unicode_source)] = s.substitution;
- emoji_unicodes[_.escape(s.source)] = s.unicode_source;
- }
- }
- });
- bus.start_polling();
- },
-
- // options: domain, load_more
- _fetchFromChannel: function (channel, options) {
- options = options || {};
- var domain =
- (channel.id === "channel_inbox") ? [['needaction', '=', true]] :
- (channel.id === "channel_starred") ? [['starred', '=', true]] :
- [['channel_ids', 'in', channel.id]];
- var cache = get_channel_cache(channel, options.domain);
-
- if (options.domain) {
- domain = domain.concat(options.domain || []);
- }
- if (options.load_more) {
- var min_message_id = cache.messages[0].id;
- domain = [['id', '<', min_message_id]].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) {
- add_message(msg, {channel_id: channel.id, silent: true, domain: options.domain});
- });
- var channel_cache = get_channel_cache(channel, options.domain || []);
- return channel_cache.messages;
- });
- },
- // options: force_fetch
- _fetchDocumentMessages : function (ids, options) {
- var loaded_msgs = _.filter(messages, function (message) {
- return _.contains(ids, message.id);
- });
- var loaded_msg_ids = _.pluck(loaded_msgs, 'id');
-
- options = options || {};
- if (options.force_fetch || _.difference(ids.slice(0, LIMIT), loaded_msg_ids).length) {
- var ids_to_load = _.difference(ids, loaded_msg_ids).slice(0, LIMIT);
-
- return this._rpc({
- model: 'mail.message',
- method: 'message_format',
- args: [ids_to_load],
- context: session.user_context,
- })
- .then(function (msgs) {
- var processed_msgs = [];
- _.each(msgs, function (msg) {
- processed_msgs.push(add_message(msg, {silent: true}));
- });
- return _.sortBy(loaded_msgs.concat(processed_msgs), function (msg) {
- return msg.id;
- });
- });
- } else {
- return $.when(loaded_msgs);
- }
- },
-
- post_message: 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(emoji_unicodes), function (key) {
- var escaped_key = String(key).replace(/([.*+?=^!:${}()|[\]\/\\])/g, '\\$1');
- var regexp = new RegExp("(\\s|^)(" + escaped_key + ")(?=\\s|$)", "g");
- msg.body = msg.body.replace(regexp, "$1" + emoji_unicodes[key]);
- });
-
- if ('subject' in data) {
- msg.subject = data.subject;
- }
- if ('channel_id' 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.channel_id],
- 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 (msg_id) {
- return self._rpc({
- model: 'mail.message',
- method: 'message_format',
- args: [msg_id],
- })
- .then(function (msgs) {
- msgs[0].model = options.model;
- msgs[0].res_id = options.res_id;
- add_message(msgs[0]);
- });
- });
- }
- },
-
- get_message: function (id) {
- return _.findWhere(messages, {id: id});
- },
- get_messages: function (options) {
- var channel;
-
- if ('channel_id' in options && options.load_more) {
- // get channel messages, force load_more
- channel = this.get_channel(options.channel_id);
- return this._fetchFromChannel(channel, {domain: options.domain || {}, load_more: true});
- }
- if ('channel_id' in options) {
- // channel message, check in cache first
- channel = this.get_channel(options.channel_id);
- var channel_cache = get_channel_cache(channel, options.domain);
- if (channel_cache.loaded) {
- return $.when(channel_cache.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) {
- chat_manager.mark_as_read(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]];
- this._rpc({
- model: 'mail.message',
- method: 'message_fetch',
- args: [domain],
- kwargs: {limit: 30},
- })
- .then(function (msgs) {
- return _.map(msgs, add_message);
- });
- }
- },
- toggle_star_status: function (message_id) {
- return this._rpc({
- model: 'mail.message',
- method: 'toggle_message_starred',
- args: [[message_id]],
- });
- },
- unstar_all: function () {
- return this._rpc({
- model: 'mail.message',
- method: 'unstar_all',
- args: [[]]
- });
- },
- mark_as_read: function (message_ids) {
- var ids = _.filter(message_ids, function (id) {
- var message = _.findWhere(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();
- }
- },
- mark_all_as_read: function (channel, domain) {
- if ((channel.id === "channel_inbox" && needaction_counter) || (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();
- },
- undo_mark_as_read: function (message_ids, channel) {
- return this._rpc({
- model: 'mail.message',
- method: 'mark_as_unread',
- args: [message_ids, [channel.id]],
- });
- },
- mark_channel_as_seen: function (channel) {
- if (channel.unread_counter > 0 && channel.type !== 'static') {
- update_channel_unread_counter(channel, 0);
- this.channel_seen(channel);
- }
- },
-
- get_channels: function () {
- return _.clone(channels);
- },
-
- get_channel: function (id) {
- return _.findWhere(channels, {id: id});
- },
-
- get_dm_from_partner_id: function (partner_id) {
- return _.findWhere(channels, {direct_partner_id: partner_id});
- },
-
- all_history_loaded: function (channel, domain) {
- return get_channel_cache(channel, domain).all_history_loaded;
- },
-
- get_mention_partner_suggestions: function (channel) {
- if (!channel) {
- return mention_partner_suggestions;
- }
- if (!channel.members_deferred) {
- channel.members_deferred = this._rpc({
- model: 'mail.channel',
- method: 'channel_fetch_listeners',
- args: [channel.uuid],
- }, {
- shadow: true
- })
- .then(function (members) {
- var suggestions = [];
- _.each(mention_partner_suggestions, function (partners) {
- suggestions.push(_.filter(partners, function (partner) {
- return !_.findWhere(members, { id: partner.id });
- }));
- });
-
- return [members];
- });
- }
- return channel.members_deferred;
- },
-
- get_commands: function (channel) {
- return _.filter(commands, function (command) {
- return !command.channel_types || _.contains(command.channel_types, channel.server_type);
- });
- },
- get_canned_responses: function () {
- return canned_responses;
- },
-
- get_emojis: function() {
- return emojis;
- },
-
- get_needaction_counter: function () {
- return needaction_counter;
- },
- get_starred_counter: function () {
- return starred_counter;
- },
- get_chat_unread_counter: function () {
- return chat_unread_counter;
- },
- get_unread_conversation_counter: function () {
- return unread_conversation_counter;
- },
-
- get_last_seen_message: 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_discuss_menu_id: function () {
- return discuss_menu_id;
- },
-
- detach_channel: function (channel) {
- return this._rpc({
- model: 'mail.channel',
- method: 'channel_minimize',
- args: [channel.uuid, true],
- }, {
- shadow: true,
- });
- },
- remove_chatter_messages: function (model) {
- messages = _.reject(messages, function (message) {
- return message.channel_ids.length === 0 && message.model === model;
- });
- },
-
- create_channel: 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(add_channel);
- },
- join_channel: function (channel_id, options) {
- if (channel_id in channel_defs) {
- // prevents concurrent calls to channel_join_and_get_info
- return channel_defs[channel_id];
- }
- var channel = this.get_channel(channel_id);
- if (channel) {
- // channel already joined
- channel_defs[channel_id] = $.when(channel);
- } else {
- channel_defs[channel_id] = this._rpc({
- model: 'mail.channel',
- method: 'channel_join_and_get_info',
- args: [[channel_id]],
- })
- .then(function (result) {
- return add_channel(result, options);
- });
- }
- return channel_defs[channel_id];
- },
- open_and_detach_dm: function (partner_id) {
- return this._rpc({
- model: 'mail.channel',
- method: 'channel_get_and_minimize',
- args: [[partner_id]],
- })
- .then(add_channel);
- },
- open_channel: function (channel) {
- chat_manager.bus.trigger(client_action_open ? 'open_channel' : 'detach_channel', channel);
- },
-
- 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],
- });
- }
- },
- close_chat_session: function (channel_id) {
- var channel = this.get_channel(channel_id);
- this._rpc({
- model: 'mail.channel',
- method: 'channel_fold',
- kwargs: {uuid : channel.uuid, state : 'closed'},
- }, {shadow: true});
- },
- fold_channel: function (channel_id, folded) {
- var args = {
- uuid: this.get_channel(channel_id).uuid,
- };
- if (_.isBoolean(folded)) {
- args.state = folded ? 'folded' : 'open';
- }
- return this._rpc({
- model: 'mail.channel',
- method: 'channel_fold',
- kwargs: args,
- }, {shadow: true});
- },
- /**
- * 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.
- */
- redirect: function (res_model, res_id, dm_redirection_callback) {
- var self = this;
- var redirect_to_document = function (res_model, res_id, view_id) {
- web_client.do_action({
- type:'ir.actions.act_window',
- view_type: 'form',
- view_mode: 'form',
- res_model: res_model,
- views: [[view_id || false, 'form']],
- res_id: res_id,
- });
- };
- if (res_model === "res.partner") {
- var domain = [["partner_id", "=", res_id]];
- this._rpc({
- model: 'res.users',
- method: 'search',
- args: [domain],
- })
- .then(function (user_ids) {
- if (user_ids.length && user_ids[0] !== session.uid && dm_redirection_callback) {
- self.create_channel(res_id, 'dm').then(dm_redirection_callback);
- } else {
- redirect_to_document(res_model, res_id);
- }
- });
- } else {
- this._rpc({
- model: res_model,
- method: 'get_formview_id',
- args: [[res_id], session.user_context],
- })
- .then(function (view_id) {
- redirect_to_document(res_model, res_id, view_id);
- });
- }
- },
-
- get_channels_preview: function (channels) {
- var channels_preview = _.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 missing_channels = _.where(channels_preview, {last_message: undefined});
- if (!channels_preview_def) {
- if (missing_channels.length) {
- var missing_channel_ids = _.pluck(missing_channels, 'id');
- channels_preview_def = this._rpc({
- model: 'mail.channel',
- method: 'channel_fetch_preview',
- args: [missing_channel_ids],
- }, {
- shadow: true,
- });
- } else {
- channels_preview_def = $.when();
- }
- }
- return channels_preview_def.then(function (channels) {
- _.each(missing_channels, function (channel_preview) {
- var channel = _.findWhere(channels, {id: channel_preview.id});
- if (channel) {
- channel_preview.last_message = add_message(channel.last_message);
- }
- });
- // sort channels: 1. unread, 2. chat, 3. date of last msg
- channels_preview.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(channels_preview, function (channel) {
- if (channel.last_message) {
- channel.last_message_preview = chat_manager.get_message_body_preview(channel.last_message.body);
- channel.last_message_date = channel.last_message.date.fromNow();
- }
- });
- return channels_preview;
- });
- },
- get_message_body_preview: function (message_body) {
- return utils.parse_and_transform(message_body, utils.inline);
- },
-
- search_partner: function (search_val, limit) {
- var def = $.Deferred();
- var values = [];
- // search among prefetched partners
- var search_regexp = new RegExp(_.str.escapeRegExp(utils.unaccent(search_val)), 'i');
- _.each(mention_partner_suggestions, function (partners) {
- if (values.length < limit) {
- values = values.concat(_.filter(partners, function (partner) {
- return session.partner_id !== partner.id && search_regexp.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: [search_val, limit || 20],
- }, {
- shadow: true,
- });
- } else {
- def = $.when(values);
- }
- return def.then(function (values) {
- var autocomplete_data = _.map(values, function (value) {
- return { id: value.id, value: value.name, label: value.name };
- });
- return _.sortBy(autocomplete_data, 'label');
- });
- },
-});
-
-var CallService = Class.extend(Mixins.EventDispatcherMixin, Mixins.ServiceProvider, {
- init: function () {
- Mixins.ServiceProvider.init.call(this);
- Mixins.EventDispatcherMixin.init.call(this);
- },
-});
-var chat_manager = new ChatManager(new CallService());
-chat_manager.start();
-
-return chat_manager;
-
-});
diff --git a/addons/mail/static/src/js/chat_mixin.js b/addons/mail/static/src/js/chat_mixin.js
deleted file mode 100644
index ec1b06a805e..00000000000
--- a/addons/mail/static/src/js/chat_mixin.js
+++ /dev/null
@@ -1,130 +0,0 @@
-odoo.define('mail.chat_mixin', function() {
-"use strict";
-
-
-var ChatMixin = {
- // return a deferred that resolves when chat manager is ready
- _chatReady: function() {
- var result;
- this.trigger_up('chat_manager_ready', {
- callback: function(def) { result = def;}
- });
- return $.when(result);
- },
- _getBus: function() {
- var result;
- this.trigger_up('get_bus', {
- callback: function(bus) { result = bus;}
- });
- return result;
- },
- _getCannedResponses: function() {
- var result;
- this.trigger_up('get_canned_responses', {
- callback: function(responses) { result = responses;}
- });
- return result;
- },
- _getEmojis: function() {
- var result;
- this.trigger_up('get_emojis', {
- callback: function(emojis) { result = emojis;}
- });
- return result;
- },
- _getMentionPartnerSuggestions: function(channel_id) {
- var result;
- this.trigger_up('get_mention_partner_suggestions', {
- channel_id: channel_id,
- callback: function(res) { result = res; },
- });
- return result;
- },
- _getMessages: function(options) {
- var result;
- this.trigger_up('get_messages', {
- options: options,
- callback: function(messages) { result = messages;}
- });
- return result ? result : $.when([]);
- },
- _joinChannel: function(channel_id) {
- var result;
- this.trigger_up('join_channel', {
- channel_id: channel_id,
- callback: function(def) { result = def; },
- });
- return $.when(result);
- },
- _postMessage: function(message, options) {
- var result;
- this.trigger_up('post_message', {
- message: message,
- options: options,
- callback: function(def) { result = def;}
- });
- return $.when(result);
- },
- _removeChatterMessages: function(model) {
- this.trigger_up('remove_chatter_messages', {
- model: model,
- });
- },
- _toggleStarStatus: function(message_id) {
- this.trigger_up('toggle_star_status', {
- message_id: message_id,
- });
- },
-};
-
-return ChatMixin;
-
-});
-
-odoo.define('mail.chat_service', function (require) {
-"use strict";
-
-var chat_manager = require('mail.chat_manager');
-var web_client = require('web.web_client');
-
-web_client.on('get_emojis', web_client, function(event) {
- event.data.callback(chat_manager.get_emojis());
-});
-
-web_client.on('get_canned_responses', web_client, function(event) {
- event.data.callback(chat_manager.get_canned_responses());
-});
-
-web_client.on('chat_manager_ready', web_client, function(event) {
- event.data.callback(chat_manager.is_ready);
-});
-
-web_client.on('get_messages', web_client, function(event) {
- event.data.callback(chat_manager.get_messages(event.data.options));
-});
-
-web_client.on('post_message', web_client, function(event) {
- event.data.callback(chat_manager.post_message(event.data.message, event.data.options));
-});
-
-web_client.on('get_bus', web_client, function(event) {
- event.data.callback(chat_manager.bus);
-});
-
-web_client.on('remove_chatter_messages', web_client, function(event) {
- chat_manager.remove_chatter_messages(event.data.model);
-});
-
-web_client.on('toggle_star_status', web_client, function(event) {
- chat_manager.toggle_star_status(event.data.message_id);
-});
-
-web_client.on('join_channel', web_client, function(event) {
- event.data.callback(chat_manager.join_channel(event.data.channel_id));
-});
-
-web_client.on('get_mention_partner_suggestions', web_client, function(event) {
- event.data.callback(chat_manager.get_mention_partner_suggestions(event.data.channel_id));
-});
-
-}); \ No newline at end of file
diff --git a/addons/mail/static/src/js/chat_window.js b/addons/mail/static/src/js/chat_window.js
index c8cfecd193f..9f09eb390b0 100644
--- a/addons/mail/static/src/js/chat_window.js
+++ b/addons/mail/static/src/js/chat_window.js
@@ -140,7 +140,9 @@ return Widget.extend({
};
this.$input.val('');
if (content) {
- this.trigger('post_message', message, this.channel_id);
+ this.trigger('post_message', message, {
+ channelID: this.channel_id,
+ });
}
}
},
diff --git a/addons/mail/static/src/js/chatter.js b/addons/mail/static/src/js/chatter.js
index 649a7381f7c..dfc041325d9 100644
--- a/addons/mail/static/src/js/chatter.js
+++ b/addons/mail/static/src/js/chatter.js
@@ -2,7 +2,6 @@ odoo.define('mail.Chatter', function (require) {
"use strict";
var Activity = require('mail.Activity');
-var chat_mixin = require('mail.chat_mixin');
var ChatterComposer = require('mail.ChatterComposer');
var Followers = require('mail.Followers');
var ThreadField = require('mail.ThreadField');
@@ -22,7 +21,7 @@ var QWeb = core.qweb;
// with each other.
// It synchronizes the rendering of those widgets (as they may be asynchronous), to limitate
// the flickering when switching between records
-var Chatter = Widget.extend(chat_mixin, {
+var Chatter = Widget.extend({
template: 'mail.Chatter',
custom_events: {
reload_mail_fields: '_onReloadMailFields',
@@ -52,7 +51,7 @@ var Chatter = Widget.extend(chat_mixin, {
// mention: get the prefetched partners and use them as mention suggestions
// if there is a follower widget, the followers will be added to the
// suggestions as well once fetched
- this.mentionPartnerSuggestions = this._getMentionPartnerSuggestions();
+ this.mentionPartnerSuggestions = this.call('chat_manager', 'getMentionPartnerSuggestions');
this.mentionSuggestions = this.mentionPartnerSuggestions;
this.fields = {};
diff --git a/addons/mail/static/src/js/chatter_composer.js b/addons/mail/static/src/js/chatter_composer.js
index fe23bc528fe..0a8088d87ae 100644
--- a/addons/mail/static/src/js/chatter_composer.js
+++ b/addons/mail/static/src/js/chatter_composer.js
@@ -1,7 +1,6 @@
odoo.define('mail.ChatterComposer', function (require) {
"use strict";
-// var chat_manager = require('mail.chat_manager');
var composer = require('mail.composer');
var utils = require('mail.utils');
diff --git a/addons/mail/static/src/js/composer.js b/addons/mail/static/src/js/composer.js
index 2e2aaa66f74..06675ec3902 100644
--- a/addons/mail/static/src/js/composer.js
+++ b/addons/mail/static/src/js/composer.js
@@ -1,7 +1,6 @@
odoo.define('mail.composer', function (require) {
"use strict";
-var chat_mixin = require('mail.chat_mixin');
var DocumentViewer = require('mail.DocumentViewer');
var utils = require('mail.utils');
@@ -385,7 +384,7 @@ var MentionManager = Widget.extend({
});
-var BasicComposer = Widget.extend(chat_mixin, {
+var BasicComposer = Widget.extend({
template: "mail.ChatComposer",
events: {
"keydown .o_composer_input": "_onKeydown",
@@ -482,7 +481,7 @@ var BasicComposer = Widget.extend(chat_mixin, {
self.trigger('input_focused');
});
this.$input.html(this.options.default_body);
- this.$input.css('min-height', this.options.input_min_height)
+ this.$input.css('min-height', this.options.input_min_height);
// Attachments
$(window).on(this.fileupload_id, this.on_attachment_loaded);
@@ -728,7 +727,7 @@ var BasicComposer = Widget.extend(chat_mixin, {
},
// remove mention when user try to edit or remove it.
_removeMention: function (event) {
- if (window.getSelection().anchorNode.parentNode.tagName == 'A') {
+ if (window.getSelection().anchorNode.parentNode.tagName === 'A') {
document.getElementById(window.getSelection().anchorNode.parentNode.id).remove();
}
},
@@ -790,8 +789,8 @@ var BasicComposer = Widget.extend(chat_mixin, {
var self = this;
var def = $.Deferred();
clearTimeout(this.canned_timeout);
- this.canned_timeout = setTimeout(function() {
- var canned_responses = self._getCannedResponses();
+ this.canned_timeout = setTimeout(function () {
+ var canned_responses = self.call('chat_manager', 'getCannedResponses');
var matches = fuzzy.filter(utils.unaccent(search), _.pluck(canned_responses, 'source'));
var indexes = _.pluck(matches.slice(0, self.options.mention_fetch_limit), 'index');
def.resolve(_.map(indexes, function (i) {
@@ -876,7 +875,7 @@ var BasicComposer = Widget.extend(chat_mixin, {
_onEmojiButtonClick: function () {
if (!this.$emojisContainer) { // lazy rendering
this.$emojisContainer = $(QWeb.render('mail.ChatComposer.emojis', {
- emojis: this._getEmojis(),
+ emojis: this.call('chat_manager', 'getEmojis'),
}));
}
if (this.$emojisContainer.parent().length) {
@@ -938,7 +937,7 @@ var BasicComposer = Widget.extend(chat_mixin, {
*/
_onEmojiImageClick: function (ev) {
this.$input.html(this.$input.html() + " " + $(ev.currentTarget).data('emoji') + " ");
- this._placeCaretAtEnd(this.$input[0])
+ this._placeCaretAtEnd(this.$input[0]);
this._hideEmojis();
},
});
diff --git a/addons/mail/static/src/js/client_action.js b/addons/mail/static/src/js/discuss.js
index d2299259486..4de7e637e8b 100644
--- a/addons/mail/static/src/js/client_action.js
+++ b/addons/mail/static/src/js/discuss.js
@@ -1,10 +1,8 @@
-odoo.define('mail.chat_client_action', function (require) {
+odoo.define('mail.chat_discuss', function (require) {
"use strict";
-var chat_manager = require('mail.chat_manager');
var ChatThread = require('mail.ChatThread');
var composer = require('mail.composer');
-var utils = require('mail.utils');
var config = require('web.config');
var ControlPanelMixin = require('web.ControlPanelMixin');
@@ -33,9 +31,11 @@ var PartnerInviteDialog = Dialog.extend({
/**
* @override
+ * @param {integer|string} channelID id of the channel,
+ * a string for static channels (e.g. 'channel_inbox').
*/
- init: function (parent, title, channel_id) {
- this.channel_id = channel_id;
+ init: function (parent, title, channelID) {
+ this.channelID = channelID;
this._super(parent, {
title: title,
@@ -51,7 +51,8 @@ var PartnerInviteDialog = Dialog.extend({
/**
* @override
*/
- start: function (){
+ start: function () {
+ var self = this;
this.$input = this.$('.o_mail_chat_partner_invite_input');
this.$input.select2({
width: '100%',
@@ -62,7 +63,7 @@ var PartnerInviteDialog = Dialog.extend({
return $('<span>').text(item.text).prepend(status);
},
query: function (query) {
- chat_manager.search_partner(query.term, 20).then(function (partners) {
+ self.call('chat_manager', 'searchPartner', query.term, 20).then(function (partners) {
query.callback({
results: _.map(partners, function (partner) {
return _.extend(partner, { text: partner.label });
@@ -80,7 +81,7 @@ var PartnerInviteDialog = Dialog.extend({
/**
* @private
- * @returns {Deferred}
+ * @returns {$.Promise}
*/
_addChannel: function () {
var self = this;
@@ -89,22 +90,23 @@ var PartnerInviteDialog = Dialog.extend({
return this._rpc({
model: 'mail.channel',
method: 'channel_invite',
- args: [this.channel_id],
+ args: [this.channelID],
kwargs: {partner_ids: _.pluck(data, 'id')},
}).then(function () {
var names = _.escape(_.pluck(data, 'text').join(', '));
var notification = _.str.sprintf(_t('You added <b>%s</b> to the conversation.'), names);
self.do_notify(_t('New people'), notification);
- // Clear the members_deferred to fetch again the partner
- // when get_mention_partner_suggestions from the chat_manager is triggered
- delete chat_manager.get_channel(self.channel_id).members_deferred;
+ // Clear the membersDeferred to fetch again the partner
+ // when getMentionPartnerSuggestions from the chatManager is triggered
+ var channel = this.call('chat_manager', 'getChannel', self.channelID);
+ delete channel.membersDeferred;
});
}
},
});
-var ChatAction = Widget.extend(ControlPanelMixin, {
- template: 'mail.client_action',
+var Discuss = Widget.extend(ControlPanelMixin, {
+ template: 'mail.discuss',
custom_events: {
search: '_onSearch',
},
@@ -129,7 +131,7 @@ var ChatAction = Widget.extend(ControlPanelMixin, {
this.domain = [];
this.action = action;
this.options = options || {};
- this.channels_scrolltop = {};
+ this.channelsScrolltop = {};
this.throttledUpdateChannels = _.throttle(this._updateChannels.bind(this), 100, { leading: false });
this.notification_bar = (window.Notification && window.Notification.permission === "default");
this.selected_message = null;
@@ -144,13 +146,14 @@ var ChatAction = Widget.extend(ControlPanelMixin, {
*/
willStart: function () {
var self = this;
- var view_id = this.action && this.action.search_view_id && this.action.search_view_id[0];
+ var viewID = this.action && this.action.search_view_id && this.action.search_view_id[0];
var def = this
- .loadFieldView(this.dataset, view_id, 'search')
+ .loadFieldView(this.dataset, viewID, 'search')
.then(function (fields_view) {
self.fields_view = fields_view;
});
- return $.when(this._super(), chat_manager.is_ready, def);
+ var chatReady = this.call('chat_manager', 'isReady');
+ return $.when(this._super(), chatReady, def);
},
/**
* @override
@@ -158,30 +161,31 @@ var ChatAction = Widget.extend(ControlPanelMixin, {
start: function () {
var self = this;
- var default_channel = chat_manager.get_channel(this.defaultChannelID) ||
- chat_manager.get_channel('channel_inbox');
- this.basic_composer = new composer.BasicComposer(this, {mention_partners_restricted: true});
- this.extended_composer = new composer.ExtendedComposer(this, {mention_partners_restricted: true});
- this.basic_composer.on('post_message', this, this._onPostMessage);
- this.basic_composer.on('input_focused', this, this._onComposerFocused);
- this.extended_composer.on('post_message', this, this._onPostMessage);
- this.extended_composer.on('input_focused', this, this._onComposerFocused);
+ var defaultChannel = this.call('chat_manager', 'getChannel', this.defaultChannelID) ||
+ this.call('chat_manager', 'getChannel', 'channel_inbox');
+
+ this.basicComposer = new composer.BasicComposer(this, {mention_partners_restricted: true});
+ this.extendedComposer = new composer.ExtendedComposer(this, {mention_partners_restricted: true});
+ this.basicComposer.on('post_message', this, this._onPostMessage);
+ this.basicComposer.on('input_focused', this, this._onComposerFocused);
+ this.extendedComposer.on('post_message', this, this._onPostMessage);
+ this.extendedComposer.on('input_focused', this, this._onComposerFocused);
var defs = [];
defs.push(this._renderButtons());
defs.push(this._renderThread());
- defs.push(this.basic_composer.appendTo(this.$('.o_mail_chat_content')));
- defs.push(this.extended_composer.appendTo(this.$('.o_mail_chat_content')));
+ defs.push(this.basicComposer.appendTo(this.$('.o_mail_chat_content')));
+ defs.push(this.extendedComposer.appendTo(this.$('.o_mail_chat_content')));
defs.push(this._renderSearchView());
return $.when.apply($, defs)
- .then(this._setChannel.bind(this, default_channel))
+ .then(this._setChannel.bind(this, defaultChannel))
.then(this._updateChannels.bind(this))
.then(function () {
self._startListening();
self.thread.$el.on("scroll", null, _.debounce(function () {
if (self.thread.is_at_bottom()) {
- chat_manager.mark_channel_as_seen(self.channel);
+ self.call('chat_manager', 'markChannelAsSeen', self.channel);
}
}, 100));
});
@@ -210,17 +214,19 @@ var ChatAction = Widget.extend(ControlPanelMixin, {
* @override
*/
on_attach_callback: function () {
- chat_manager.bus.trigger('client_action_open', true);
+ var chatBus = this.call('chat_manager', 'getChatBus');
+ chatBus.trigger('discuss_open', true);
if (this.channel) {
- this.thread.scroll_to({offset: this.channels_scrolltop[this.channel.id]});
+ this.thread.scroll_to({offset: this.channelsScrolltop[this.channel.id]});
}
},
/**
* @override
*/
on_detach_callback: function () {
- chat_manager.bus.trigger('client_action_open', false);
- this.channels_scrolltop[this.channel.id] = this.thread.get_scrolltop();
+ var chatBus = this.call('chat_manager', 'getChatBus');
+ chatBus.trigger('discuss_open', false);
+ this.channelsScrolltop[this.channel.id] = this.thread.get_scrolltop();
},
//--------------------------------------------------------------------------
@@ -229,14 +235,17 @@ var ChatAction = Widget.extend(ControlPanelMixin, {
/**
* @private
- * @returns {Deferred}
+ * @returns {$.Promise}
*/
_fetchAndRenderThread: function () {
var self = this;
- return chat_manager.get_messages({channel_id: this.channel.id, domain: this.domain}).then(function (result) {
- self.thread.render(result, self._getThreadRenderingOptions(result));
- self._updateButtonStatus(result.length === 0);
- });
+ return this.call('chat_manager', 'getMessages', {
+ channelID: this.channel.id,
+ domain: this.domain
+ }).then(function (messages) {
+ self.thread.render(messages, self._getThreadRenderingOptions(messages));
+ self._updateButtonStatus(messages.length === 0);
+ });
},
/**
* @private
@@ -250,13 +259,13 @@ var ChatAction = Widget.extend(ControlPanelMixin, {
if (!this.unread_counter) {
this.messages_separator_position = false; // no unread message -> don't display separator
} else {
- var msg = chat_manager.get_last_seen_message(this.channel);
+ var msg = this.call('chat_manager', 'getLastSeenMessage', this.channel);
this.messages_separator_position = msg ? msg.id : 'top';
}
}
return {
channel_id: this.channel.id,
- display_load_more: !chat_manager.all_history_loaded(this.channel, this.domain),
+ display_load_more: !this.call('chat_manager', 'isAllHistoryLoaded', this.channel, this.domain),
display_needactions: this.channel.display_needactions,
messages_separator_position: this.messages_separator_position,
squash_close_messages: this.channel.type !== 'static' && !this.channel.mass_mailing,
@@ -268,22 +277,27 @@ var ChatAction = Widget.extend(ControlPanelMixin, {
};
},
/**
+ * Load more messages for the current thread
+ *
* @private
- * @returns {Deferred}
+ * @returns {$.Promise}
*/
_loadMoreMessages: function () {
var self = this;
- var oldest_msg_id = this.$('.o_thread_message').first().data('messageId');
- var oldest_msg_selector = '.o_thread_message[data-message-id="' + oldest_msg_id + '"]';
- var offset = -dom.getPosition(document.querySelector(oldest_msg_selector)).top;
- return chat_manager
- .get_messages({channel_id: this.channel.id, domain: this.domain, load_more: true})
- .then(function (result) {
+ var oldestMsgID = this.$('.o_thread_message').first().data('messageId');
+ var oldestMsgSelector = '.o_thread_message[data-message-id="' + oldestMsgID + '"]';
+ var offset = -dom.getPosition(document.querySelector(oldestMsgSelector)).top;
+ return this.call('chat_manager', 'getMessages', {
+ channelID: this.channel.id,
+ domain: this.domain,
+ loadMore: true
+ })
+ .then(function (messages) {
if (self.messages_separator_position === 'top') {
self.messages_separator_position = undefined; // reset value to re-compute separator position
}
- self.thread.render(result, self._getThreadRenderingOptions(result));
- offset += dom.getPosition(document.querySelector(oldest_msg_selector)).top;
+ self.thread.render(messages, self._getThreadRenderingOptions(messages));
+ offset += dom.getPosition(document.querySelector(oldestMsgSelector)).top;
self.thread.scroll_to({offset: offset});
});
},
@@ -313,9 +327,9 @@ var ChatAction = Widget.extend(ControlPanelMixin, {
select: function (event, ui) {
if (self.last_search_val) {
if (ui.item.value === '_create') {
- chat_manager.create_channel(self.last_search_val, "public");
+ self.call('chat_manager', 'createChannel', self.last_search_val, "public");
} else {
- chat_manager.join_channel(ui.item.id);
+ self.call('chat_manager', 'joinChannel', ui.item.id);
}
}
},
@@ -327,23 +341,23 @@ var ChatAction = Widget.extend(ControlPanelMixin, {
} else if (type === 'private') {
$input.on('keyup', this, function (event) {
var name = _.escape($(event.target).val());
- if(event.which === $.ui.keyCode.ENTER && name) {
- chat_manager.create_channel(name, "private");
+ if (event.which === $.ui.keyCode.ENTER && name) {
+ self.call('chat_manager', 'createChannel', name, "private");
}
});
} else if (type === 'dm') {
$input.autocomplete({
source: function (request, response) {
self.last_search_val = _.escape(request.term);
- chat_manager.search_partner(self.last_search_val, 10).done(response);
+ self.call('chat_manager', 'searchPartner', self.last_search_val, 10).done(response);
},
select: function (event, ui) {
var partner_id = ui.item.id;
- var dm = chat_manager.get_dm_from_partner_id(partner_id);
+ var dm = self.call('chat_manager', 'getDmFromPartnerID', partner_id);
if (dm) {
self._setChannel(dm);
} else {
- chat_manager.create_channel(partner_id, "dm");
+ self.call('chat_manager', 'createChannel', partner_id, "dm");
}
// clear the input
$(this).val('');
@@ -418,20 +432,21 @@ var ChatAction = Widget.extend(ControlPanelMixin, {
* @returns {Deferred}
*/
_renderThread: function () {
+ var self = this;
this.thread = new ChatThread(this, {display_help: true});
- this.thread.on('redirect', this, function (res_model, res_id) {
- chat_manager.redirect(res_model, res_id, this._setChannel.bind(this));
+ this.thread.on('redirect', this, function (resModel, resID) {
+ self.call('chat_manager', 'redirect', resModel, resID, self._setChannel.bind(self));
});
- this.thread.on('redirect_to_channel', this, function (channel_id) {
- chat_manager.join_channel(channel_id).then(this._setChannel.bind(this));
+ this.thread.on('redirect_to_channel', this, function (channelID) {
+ self.call('chat_manager', 'joinChannel', channelID).then(this._setChannel.bind(this));
});
this.thread.on('load_more_messages', this, this._loadMoreMessages);
- this.thread.on('mark_as_read', this, function (message_id) {
- chat_manager.mark_as_read([message_id]);
+ this.thread.on('mark_as_read', this, function (messageID) {
+ self.call('chat_manager', 'markAsRead', [messageID]);
});
- this.thread.on('toggle_star_status', this, function (message_id) {
- chat_manager.toggle_star_status(message_id);
+ this.thread.on('toggle_star_status', this, function (messageID) {
+ self.call('chat_manager', 'toggleStarStatus', messageID);
});
this.thread.on('select_message', this, this._selectMessage);
this.thread.on('unselect_message', this, this._unselectMessage);
@@ -444,12 +459,12 @@ var ChatAction = Widget.extend(ControlPanelMixin, {
* @private
*/
_restoreChannelState: function () {
- var $new_messages_separator = this.$('.o_thread_new_messages_separator');
- if ($new_messages_separator.length) {
- this.thread.$el.scrollTo($new_messages_separator);
+ var $newMessagesSeparator = this.$('.o_thread_new_messages_separator');
+ if ($newMessagesSeparator.length) {
+ this.thread.$el.scrollTo($newMessagesSeparator);
} else {
- var new_channel_scrolltop = this.channels_scrolltop[this.channel.id];
- this.thread.scroll_to({offset: new_channel_scrolltop});
+ var newChannelScrolltop = this.channelsScrolltop[this.channel.id];
+ this.thread.scroll_to({offset: newChannelScrolltop});
}
this._restoreComposerState(this.channel);
},
@@ -461,7 +476,7 @@ var ChatAction = Widget.extend(ControlPanelMixin, {
if (channel.type === 'static') {
return; // no composer in static channels
}
- var composer = channel.mass_mailing ? this.extended_composer : this.basic_composer;
+ var composer = channel.mass_mailing ? this.extendedComposer : this.basicComposer;
var composerState = this.composerStates[channel.uuid];
if (composerState) {
composer.setState(composerState);
@@ -469,14 +484,14 @@ var ChatAction = Widget.extend(ControlPanelMixin, {
},
/**
* @private
- * @param {string} search_val
- * @returns {Deferred<Array>}
+ * @param {string} searchVal
+ * @returns {$.Promise<Array>}
*/
- _searchChannel: function (search_val){
+ _searchChannel: function (searchVal){
return this._rpc({
model: 'mail.channel',
method: 'channel_search_to_join',
- args: [search_val]
+ args: [searchVal]
})
.then(function (result){
var values = [];
@@ -492,27 +507,27 @@ var ChatAction = Widget.extend(ControlPanelMixin, {
},
/**
* @private
- * @param {integer} message_id
+ * @param {integer} messageID
*/
- _selectMessage: function (message_id) {
+ _selectMessage: function (messageID) {
this.$el.addClass('o_mail_selection_mode');
- var message = chat_manager.get_message(message_id);
+ var message = this._getMessage(messageID);
this.selected_message = message;
var subject = "Re: " + message.record_name;
- this.extended_composer.set_subject(subject);
+ this.extendedComposer.set_subject(subject);
if (this.channel.type !== 'static') {
- this.basic_composer.do_hide();
+ this.basicComposer.do_hide();
}
- this.extended_composer.do_show();
+ this.extendedComposer.do_show();
- this.thread.scroll_to({id: message_id, duration: 200, only_if_necessary: true});
- this.extended_composer.focus('body');
+ this.thread.scroll_to({id: messageID, duration: 200, only_if_necessary: true});
+ this.extendedComposer.focus('body');
},
/**
* @private
* @param {Object} channel
- * @returns {Deferred}
+ * @returns {$.Promise}
*/
_setChannel: function (channel) {
var self = this;
@@ -534,9 +549,8 @@ var ChatAction = Widget.extend(ControlPanelMixin, {
return this._fetchAndRenderThread().then(function () {
// Mark channel's messages as read and clear needactions
if (channel.type !== 'static') {
- chat_manager.mark_channel_as_seen(channel);
+ self.call('chat_manager', 'markChannelAsSeen', channel);
}
-
// Restore scroll position and composer of the new current channel
self._restoreChannelState();
@@ -556,25 +570,26 @@ var ChatAction = Widget.extend(ControlPanelMixin, {
});
},
/**
- * Binds handlers on chat_manager events
+ * Binds handlers on chatManager events
*
* @private
*/
_startListening: function () {
- chat_manager.bus.on('open_channel', this, this._setChannel);
- chat_manager.bus.on('new_message', this, this._onNewMessage);
- chat_manager.bus.on('update_message', this, this._onMessageUpdated);
- chat_manager.bus.on('new_channel', this, this._onNewChannel);
- chat_manager.bus.on('anyone_listening', this, function (channel, query) {
+ var chatBus = this.call('chat_manager', 'getChatBus');
+ chatBus.on('open_channel', this, this._setChannel);
+ chatBus.on('new_message', this, this._onNewMessage);
+ chatBus.on('update_message', this, this._onMessageUpdated);
+ chatBus.on('new_channel', this, this._onNewChannel);
+ chatBus.on('anyone_listening', this, function (channel, query) {
query.is_displayed = query.is_displayed ||
(channel.id === this.channel.id && this.thread.is_at_bottom());
});
- chat_manager.bus.on('unsubscribe_from_channel', this, this._onChannelLeft);
- chat_manager.bus.on('update_needaction', this, this.throttledUpdateChannels);
- chat_manager.bus.on('update_starred', this, this.throttledUpdateChannels);
- chat_manager.bus.on('update_channel_unread_counter', this, this.throttledUpdateChannels);
- chat_manager.bus.on('update_dm_presence', this, this.throttledUpdateChannels);
- chat_manager.bus.on('activity_updated', this, this.throttledUpdateChannels);
+ chatBus.on('unsubscribe_from_channel', this, this._onChannelLeft);
+ chatBus.on('update_needaction', this, this.throttledUpdateChannels);
+ chatBus.on('update_starred', this, this.throttledUpdateChannels);
+ chatBus.on('update_channel_unread_counter', this, this.throttledUpdateChannels);
+ chatBus.on('update_dm_presence', this, this.throttledUpdateChannels);
+ chatBus.on('activity_updated', this, this.throttledUpdateChannels);
},
/**
* Stores the scroll position and composer state of the current channel
@@ -583,7 +598,7 @@ var ChatAction = Widget.extend(ControlPanelMixin, {
*/
_storeChannelState: function () {
if (this.channel) {
- this.channels_scrolltop[this.channel.id] = this.thread.get_scrolltop();
+ this.channelsScrolltop[this.channel.id] = this.thread.get_scrolltop();
this._storeComposerState(this.channel);
}
},
@@ -595,7 +610,7 @@ var ChatAction = Widget.extend(ControlPanelMixin, {
if (channel.type === 'static') {
return; // no composer in static channels
}
- var composer = channel.mass_mailing ? this.extended_composer : this.basic_composer;
+ var composer = channel.mass_mailing ? this.extendedComposer : this.basicComposer;
this.composerStates[channel.uuid] = composer.getState();
composer.clear_composer();
},
@@ -603,11 +618,11 @@ var ChatAction = Widget.extend(ControlPanelMixin, {
* @private
*/
_unselectMessage: function () {
- this.basic_composer.do_toggle(this.channel.type !== 'static' && !this.channel.mass_mailing);
- this.extended_composer.do_toggle(this.channel.type !== 'static' && this.channel.mass_mailing);
+ this.basicComposer.do_toggle(this.channel.type !== 'static' && !this.channel.mass_mailing);
+ this.extendedComposer.do_toggle(this.channel.type !== 'static' && this.channel.mass_mailing);
if (!config.device.touch) {
- var composer = this.channel.mass_mailing ? this.extended_composer : this.basic_composer;
+ var composer = this.channel.mass_mailing ? this.extendedComposer : this.basicComposer;
composer.focus();
}
this.$el.removeClass('o_mail_selection_mode');
@@ -623,11 +638,11 @@ var ChatAction = Widget.extend(ControlPanelMixin, {
var self = this;
var $sidebar = this._renderSidebar({
active_channel_id: this.channel ? this.channel.id: undefined,
- channels: chat_manager.get_channels(),
- needaction_counter: chat_manager.get_needaction_counter(),
- starred_counter: chat_manager.get_starred_counter(),
+ channels: this.call('chat_manager', 'getChannels'),
+ needaction_counter: this.call('chat_manager', 'getNeedactionCounter'),
+ starred_counter: this.call('chat_manager', 'getStarredCounter'),
});
- this.$(".o_mail_chat_sidebar").html($sidebar.contents());
+ self.$(".o_mail_chat_sidebar").html($sidebar.contents());
_.each(['dm', 'public', 'private'], function (type) {
var $input = self.$('.o_mail_add_channel[data-type=' + type + '] input');
self._prepareAddChannelInput($input, type);
@@ -729,27 +744,29 @@ var ChatAction = Widget.extend(ControlPanelMixin, {
*/
_onChannelClicked: function (event) {
event.preventDefault();
- var channel_id = $(event.currentTarget).data('channel-id');
- this._setChannel(chat_manager.get_channel(channel_id));
+ var channelID = $(event.currentTarget).data('channel-id');
+ var channel = this.call('chat_manager', 'getChannel', channelID);
+ this._setChannel(channel);
},
/**
* @private
- * @param {integer || string} channel_id
+ * @param {integer|string} channelID
*/
- _onChannelLeft: function (channel_id) {
- if (this.channel.id === channel_id) {
- this._setChannel(chat_manager.get_channel("channel_inbox"));
+ _onChannelLeft: function (channelID) {
+ if (this.channel.id === channelID) {
+ var channel = this.call('chat_manager', 'getChannel', 'channel_inbox');
+ this._setChannel(channel);
}
this._updateChannels();
- delete this.channels_scrolltop[channel_id];
+ delete this.channelsScrolltop[channelID];
},
/**
* @private
*/
_onComposerFocused: function () {
- var composer = this.channel.mass_mailing ? this.extended_composer : this.basic_composer;
- var commands = chat_manager.get_commands(this.channel);
- var partners = chat_manager.get_mention_partner_suggestions(this.channel);
+ var composer = this.channel.mass_mailing ? this.extendedComposer : this.basicComposer;
+ var commands = this.call('chat_manager', 'getCommands', this.channel);
+ var partners = this.call('chat_manager', 'getMentionPartnerSuggestions', this.channel);
composer.mention_set_enabled_commands(commands);
composer.mention_set_prefetched_partners(partners);
},
@@ -757,7 +774,7 @@ var ChatAction = Widget.extend(ControlPanelMixin, {
* @private
*/
_onMarkAllReadClicked: function () {
- chat_manager.mark_all_as_read(this.channel, this.domain);
+ this.call('chat_manager', 'markAllAsRead', this.channel, this.domain);
},
/**
* @private
@@ -766,16 +783,20 @@ var ChatAction = Widget.extend(ControlPanelMixin, {
*/
_onMessageUpdated: function (message, type) {
var self = this;
- var current_channel_id = this.channel.id;
- if ((current_channel_id === "channel_starred" && !message.is_starred) ||
- (current_channel_id === "channel_inbox" && !message.is_needaction)) {
- chat_manager.get_messages({channel_id: this.channel.id, domain: this.domain}).then(function (messages) {
- var options = self._getThreadRenderingOptions(messages);
- self.thread.remove_message_and_render(message.id, messages, options).then(function () {
- self._updateButtonStatus(messages.length === 0, type);
+ var currentChannelID = this.channel.id;
+ if ((currentChannelID === "channel_starred" && !message.is_starred) ||
+ (currentChannelID === "channel_inbox" && !message.is_needaction)) {
+ this.call('chat_manager', 'getMessages', {
+ channelID: this.channel.id,
+ domain: this.domain
+ }).then(function (messages) {
+ var options = self._getThreadRenderingOptions(messages);
+ self.thread.remove_message_and_render(message.id, messages, options).then(function () {
+ self._updateButtonStatus(messages.length === 0, type);
+
+ });
});
- });
- } else if (_.contains(message.channel_ids, current_channel_id)) {
+ } else if (_.contains(message.channel_ids, currentChannelID)) {
this._fetchAndRenderThread();
}
},
@@ -797,9 +818,8 @@ var ChatAction = Widget.extend(ControlPanelMixin, {
var self = this;
if (_.contains(message.channel_ids, this.channel.id)) {
if (this.channel.type !== 'static' && this.thread.is_at_bottom()) {
- chat_manager.mark_channel_as_seen(this.channel);
+ this.call('chat_manager', 'markChannelAsSeen', this.channel);
}
-
var should_scroll = this.thread.is_at_bottom();
this._fetchAndRenderThread().then(function () {
if (should_scroll) {
@@ -810,7 +830,7 @@ var ChatAction = Widget.extend(ControlPanelMixin, {
// Re-render sidebar to indicate that there is a new message in the corresponding channels
this._updateChannels();
// Dump scroll position of channels in which the new message arrived
- this.channels_scrolltop = _.omit(this.channels_scrolltop, message.channel_ids);
+ this.channelsScrolltop = _.omit(this.channelsScrolltop, message.channel_ids);
},
/**
* @private
@@ -818,18 +838,14 @@ var ChatAction = Widget.extend(ControlPanelMixin, {
*/
_onPostMessage: function (message) {
var self = this;
- var options = this.selected_message ? {} : {channel_id: this.channel.id};
+ var options = this.selected_message ? {} : {channelID: this.channel.id};
if (this.selected_message) {
message.subtype = this.selected_message.is_note ? 'mail.mt_note': 'mail.mt_comment';
message.subtype_id = false;
message.message_type = 'comment';
message.content_subtype = 'html';
-
- options.model = this.selected_message.model;
- options.res_id = this.selected_message.res_id;
}
- chat_manager
- .post_message(message, options)
+ this.call('chat_manager', 'postMessage', message, options)
.then(function () {
if (self.selected_message) {
self._renderSnackbar('mail.chat.MessageSentSnackbar', {record_name: self.selected_message.record_name}, 5000);
@@ -890,10 +906,10 @@ var ChatAction = Widget.extend(ControlPanelMixin, {
if (def) {
def.then(function (value) {
if (value === 'denied') {
- utils.send_notification(self, _t('Permission denied'),
+ self._sendNotification(self, _t('Permission denied'),
_t('Odoo will not have the permission to send native notifications on this device.'));
} else {
- utils.send_notification(self, _t('Permission granted'),
+ self._sendNotification(self, _t('Permission granted'),
_t('Odoo has now the permission to send you native notifications on this device.'));
}
});
@@ -936,25 +952,26 @@ var ChatAction = Widget.extend(ControlPanelMixin, {
*/
_onUnpinChannel: function (event) {
event.stopPropagation();
- var channel_id = $(event.target).data("channel-id");
- chat_manager.unsubscribe(chat_manager.get_channel(channel_id));
+ var channelID = $(event.target).data("channel-id");
+ var channel = this.call('chat_manager', 'getChannel', channelID);
+ this.call('chat_manager', 'unsubscribe', channel);
},
/**
* @private
*/
_onUnstarAllClicked: function () {
- chat_manager.unstar_all();
+ this.call('chat_manager', 'unstarAll');
},
/**
* @private
*/
_onUnsubscribeButtonClicked: function () {
- chat_manager.unsubscribe(this.channel);
+ this._unsubscribe(this.channel);
},
});
-core.action_registry.add('mail.chat.instant_messaging', ChatAction);
+core.action_registry.add('mail.chat.instant_messaging', Discuss);
-return ChatAction;
+return Discuss;
});
diff --git a/addons/mail/static/src/js/client_action_mobile.js b/addons/mail/static/src/js/discuss_mobile.js
index ecabb94e1ff..52622b0ce32 100644
--- a/addons/mail/static/src/js/client_action_mobile.js
+++ b/addons/mail/static/src/js/discuss_mobile.js
@@ -1,12 +1,10 @@
-odoo.define('mail.chat_client_action_mobile', function (require) {
+odoo.define('mail.chat_discuss_mobile', function (require) {
"use strict";
-var ChatAction = require('mail.chat_client_action');
-var chat_manager = require('mail.chat_manager');
+var Discuss = require('mail.chat_discuss');
var config = require('web.config');
var core = require('web.core');
-var session = require('web.session');
var QWeb = core.qweb;
@@ -14,10 +12,10 @@ if (!config.device.isMobile) {
return;
}
-ChatAction.include({
- template: 'mail.client_action_mobile',
+Discuss.include({
+ template: 'mail.discuss_mobile',
need_control_panel: false, // in mobile, we use a custom control panel
- events: _.extend(ChatAction.prototype.events, {
+ events: _.extend(Discuss.prototype.events, {
'click .o_mail_mobile_tab': '_onMobileTabClicked',
'click .o_channel_inbox_item': '_onMobileInboxButtonClicked',
'click .o_mail_channel_preview': '_onMobileChannelClicked',
@@ -103,10 +101,11 @@ ChatAction.include({
/**
* @override
* @private
+ * @param {Object} channel
*/
_setChannel: function (channel) {
if (channel.type !== 'static') {
- chat_manager.detach_channel(channel);
+ this.call('chat_manager', 'detachChannel', channel.id);
} else {
this._super.apply(this, arguments);
}
@@ -170,8 +169,9 @@ ChatAction.include({
if (inInbox) {
def = this._fetchAndRenderThread();
} else {
- var channels = _.where(chat_manager.get_channels(), {type: type});
- def = chat_manager.get_channels_preview(channels);
+ var allChannels = this.call('chat_manager', 'getChannels');
+ var channels = _.where(allChannels, {type: type});
+ def = this.call('chat_manager', 'getChannelsPreview', channels);
}
return $.when(def).then(function (channelsPreview) {
// update content
@@ -179,19 +179,15 @@ ChatAction.include({
if (!previouslyInInbox) {
self.$('.o_mail_chat_tab_pane').remove();
self.$mainContent.append(self.thread.$el);
- self.$mainContent.append(self.extended_composer.$el);
+ self.$mainContent.append(self.extendedComposer.$el);
}
self._restoreChannelState();
} else {
self.thread.$el.detach();
- self.extended_composer.$el.detach();
+ self.extendedComposer.$el.detach();
var $content = $(QWeb.render("mail.chat.MobileTabPane", {
channels: channelsPreview,
- get_message_body_preview: chat_manager.get_message_body_preview,
- moment: moment,
- partner_id: session.partner_id,
type: type,
- widget: self,
}));
self._prepareAddChannelInput($content.find('.o_mail_add_channel input'), type);
self.$mainContent.html($content);
@@ -252,7 +248,8 @@ ChatAction.include({
* @param {MouseEvent}
*/
_onMobileInboxButtonClicked: function (event) {
- this._setChannel(chat_manager.get_channel($(event.currentTarget).data('type')));
+ var channel = this.call('chat_manager', 'getChannel', $(event.currentTarget).data('type'));
+ this._setChannel(channel);
this._updateContent(this.channel.id);
},
/**
@@ -264,7 +261,8 @@ ChatAction.include({
_onMobileTabClicked: function (event) {
var type = $(event.currentTarget).data('type');
if (type === 'channel_inbox') {
- this._setChannel(chat_manager.get_channel('channel_inbox'));
+ var channel = this.call('chat_manager', 'getChannel', 'channel_inbox');
+ this._setChannel(channel);
}
this._updateContent(type);
},
@@ -275,8 +273,8 @@ ChatAction.include({
* @param {MouseEvent}
*/
_onMobileChannelClicked: function (event) {
- var channelId = $(event.currentTarget).data("channel_id");
- chat_manager.detach_channel(chat_manager.get_channel(channelId));
+ var channelID = $(event.currentTarget).data("channel_id");
+ this.call('chat_manager', 'detachChannel', channelID);
},
});
diff --git a/addons/mail/static/src/js/extended_chat_window.js b/addons/mail/static/src/js/extended_chat_window.js
index 2163c28dd92..a8aa5673b81 100644
--- a/addons/mail/static/src/js/extended_chat_window.js
+++ b/addons/mail/static/src/js/extended_chat_window.js
@@ -1,9 +1,6 @@
odoo.define('mail.ExtendedChatWindow', function (require) {
"use strict";
-var core = require('web.core');
-
-var chat_manager = require('mail.chat_manager');
var ChatWindow = require('mail.ChatWindow');
var composer = require('mail.composer');
@@ -19,27 +16,29 @@ return ChatWindow.extend({
this.$el.addClass('o_thread_less');
this.$('.o_chat_search_input input')
.autocomplete({
- source: function(request, response) {
- chat_manager.search_partner(request.term, 10).done(response);
+ source: function (request, response) {
+ self.call('chat_manager', 'searchPartner', request.term, 10).done(response);
},
- select: function(event, ui) {
+ select: function (event, ui) {
self.trigger('open_dm_session', ui.item.id);
},
})
.focus();
} else if (!self.options.input_less) {
- var basic_composer = new composer.BasicComposer(self, {mention_partners_restricted: true, isMini: true});
- basic_composer.on('post_message', self, function (message) {
- this.trigger('post_message', message, this.channel_id);
+ var basicComposer = new composer.BasicComposer(self, {mention_partners_restricted: true, isMini: true});
+ basicComposer.on('post_message', self, function (message) {
+ self.call('chat_manager', 'postMessage', message, {
+ channelID: self.channel_id,
+ });
});
- basic_composer.once('input_focused', self, function () {
- var channel = chat_manager.get_channel(this.channel_id);
- var commands = chat_manager.get_commands(channel);
- var partners = chat_manager.get_mention_partner_suggestions(channel);
- basic_composer.mention_set_enabled_commands(commands);
- basic_composer.mention_set_prefetched_partners(partners);
+ basicComposer.once('input_focused', self, function () {
+ var channel = self.call('chat_manager', 'getChannel', self.channel_id);
+ var commands = self.call('chat_manager', 'getCommands', channel);
+ var partners = self.call('chat_manager', 'getMentionPartnerSuggestions', channel);
+ basicComposer.mention_set_enabled_commands(commands);
+ basicComposer.mention_set_prefetched_partners(partners);
});
- def = basic_composer.replace(self.$('.o_chat_composer'));
+ def = basicComposer.replace(self.$('.o_chat_composer'));
}
return $.when(this._super(), def);
},
@@ -49,7 +48,8 @@ return ChatWindow.extend({
event.stopPropagation();
},
on_reverse_breadcrumb: function () {
- chat_manager.bus.trigger('client_action_open', false);
+ var chatBus = this.call('chat_manager', 'getChatBus');
+ chatBus.trigger('discuss_open', false);
},
on_click_expand: _.debounce(function (event) {
event.preventDefault();
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;
+
+});
diff --git a/addons/mail/static/src/js/services/chat_window_manager.js b/addons/mail/static/src/js/services/chat_window_manager.js
new file mode 100644
index 00000000000..f82da369ecf
--- /dev/null
+++ b/addons/mail/static/src/js/services/chat_window_manager.js
@@ -0,0 +1,532 @@
+odoo.define('mail.ChatWindowManager', function (require) {
+"use strict";
+
+var ExtendedChatWindow = require('mail.ExtendedChatWindow');
+
+var AbstractService = require('web.AbstractService');
+var config = require('web.config');
+var core = require('web.core');
+var utils = require('web.utils');
+var web_client = require('web.web_client');
+
+var _t = core._t;
+var QWeb = core.qweb;
+
+//----------------------------------------------------------------
+var CHAT_WINDOW_WIDTH = 325 + 5; // 5 pixels between windows
+
+/**
+ * This service handles chat channels that are displayed in "chat window" mode
+ */
+var ChatWindowManager = AbstractService.extend({
+ name: 'chat_window_manager',
+ dependencies: ['chat_manager'],
+
+ /**
+ * @override
+ */
+ init: function () {
+ var self = this;
+
+ this._super.apply(this, arguments);
+
+ this.chatSessions = [];
+ this.newChatSession;
+ this.displayState = {
+ chatWindowsHidden: false, // chat windows aren't displayed when the client action is open
+ hiddenSessions: [],
+ hiddenUnreadCounter: 0, // total number of unread msgs in hidden chat windows
+ nbSlots: 0,
+ spaceLeft: 0,
+ windowsDropdownIsOpen: false, // used to keep dropdown open when closing chat windows
+ };
+ var chatReady = this.call('chat_manager', 'isReady');
+ chatReady.then(function () {
+ var channels = self.call('chat_manager', 'getChannels');
+ _.each(channels, function (channel) {
+ if (channel.is_detached) {
+ self.openChat(channel);
+ }
+ });
+ });
+
+ var chatBus = this.call('chat_manager', 'getChatBus');
+ chatBus.on('update_message', this, this._onUpdateMessage);
+ chatBus.on('new_message', this, this._onNewMessage);
+ chatBus.on('anyone_listening', this, this._onAnyoneListening);
+ chatBus.on('unsubscribe_from_channel', this, this._onUnsubscribeFromChannel);
+ chatBus.on('update_channel_unread_counter', this, this._onUpdateChannelUnreadCounter);
+ chatBus.on('update_dm_presence', this, this._onUpdateDmPresence);
+ chatBus.on('detach_channel', this, this._onDetachChannel);
+ chatBus.on('discuss_open', this, this._onDiscussOpen);
+
+ core.bus.on('resize', this, _.debounce(this._repositionWindows.bind(this), 100));
+ },
+
+ //--------------------------------------------------------------------------
+ // Public
+ //--------------------------------------------------------------------------
+
+ /**
+ * @param {Object} chatSession
+ * @param {integer} chatSession.id
+ * @param {Object} options
+ */
+ closeChat: function (chatSession, options) {
+ var session = _.find(this.chatSessions, {id: chatSession.id});
+ if (session) {
+ this._closeChat(session, options);
+ }
+ },
+ /**
+ * @param {Object} session
+ * @param {integer} session.id
+ * @param {boolean} [session.is_chat] true for dm? => server_type="chat" & type="dm"...
+ * @param {boolean} [session.mass_mailing]
+ * @param {string} [session.status] e.g. 'offline'
+ * @param {Object} [options]
+ * @param {boolean} [options.passively] if set to true, open the chat window
+ * without focusing the input and marking messages as read if it is not
+ * open yet, and do nothing otherwise
+ */
+ openChat: function (session, options) {
+ var self = this;
+ if (!session) {
+ this._openChatWithoutSession();
+ return;
+ }
+ options = options || {};
+ var chatSession = _.findWhere(this.chatSessions, {id: session.id});
+ if (!chatSession) {
+ var prefix = !session.is_chat ? "#" : "";
+ var windowOptions = {
+ autofocus: !options.passively,
+ input_less: session.mass_mailing,
+ status: session.status,
+ };
+ chatSession = {
+ id: session.id,
+ uuid: session.uuid,
+ name: session.name,
+ keep_unread: options.passively, // don't automatically mark unread messages as seen
+ window: new ExtendedChatWindow(web_client, session.id, prefix + session.name, session.is_folded, session.unread_counter, windowOptions),
+ };
+ chatSession.window.on("close_chat_session", null, function () {
+ self._closeChat(chatSession);
+ self.call('chat_manager', 'closeChatSession', chatSession.id);
+ });
+ chatSession.window.on("toggle_star_status", null, function (messageID) {
+ self.call('chat_manager', 'toggleStarStatus', messageID);
+ });
+
+ chatSession.window.on("fold_channel", null, function (channelID, folded) {
+ self.call('chat_manager', 'foldChannel', channelID, folded);
+ });
+
+ chatSession.window.on("post_message", null, function (message, channelID) {
+ self.call('chat_manager', 'postMessage', message, {
+ channelID: channelID
+ }).then(function () {
+ chatSession.window.thread.scroll_to();
+ });
+ });
+ chatSession.window.on("redirect", null, function (resModel, resID) {
+ self.call('chat_manager', 'redirect', resModel, resID, self.openChat.bind(self));
+ });
+ chatSession.window.on("redirect_to_channel", null, function (channelID) {
+ var session = _.findWhere(self.chatSessions, {id: channelID});
+ if (!session) {
+ self.call('chat_manager', 'joinChannel', channelID).then(function (channel) {
+ self.call('chat_manager', 'detachChannel', channelID);
+ });
+ } else {
+ session.window.toggle_fold(false);
+ }
+ });
+
+ var removeNewChat = false;
+ if (options.passively) {
+ this.chatSessions.push(chatSession); // simply insert the window to the left
+ } else if (this.newChatSession && this.newChatSession.partner_id && this.newChatSession.partner_id === session.direct_partner_id) {
+ // the window takes the place of the 'newChatSession' window
+ this.chatSessions[_.indexOf(this.chatSessions, this.newChatSession)] = chatSession;
+ removeNewChat = true;
+ } else {
+ this._addChatSession(chatSession); // add session such that window is visible
+ }
+
+ chatSession.window.appendTo($('body'))
+ .then(function () {
+ self._repositionWindows({remove_new_chat: removeNewChat});
+ return self.call('chat_manager', 'getMessages', {channelID: chatSession.id});
+ }).then(function (messages) {
+ chatSession.window.render(messages);
+ chatSession.window.thread.scroll_to();
+ setTimeout(function () {
+ chatSession.window.thread.$el.on("scroll", null, _.debounce(function () {
+ if (!chatSession.keep_unread && chatSession.window.thread.is_at_bottom()) {
+ self.call('chat_manager', 'markChannelAsSeen', session);
+ }
+ }, 100));
+ }, 0); // setTimeout to prevent to execute handler on first scroll_to, which is asynchronous
+ if (options.passively) {
+ // mark first unread messages as seen when focusing the window, then on scroll to bottom as usual
+ chatSession.window.$('.o_mail_thread, .o_chat_composer').one('click', function () {
+ self.call('chat_manager', 'markChannelAsSeen', session);
+ });
+ } else if (!self.displayState.chatWindowsHidden && !session.is_folded) {
+ self.call('chat_manager', 'markChannelAsSeen', session);
+ }
+ });
+ } else if (!options.passively) {
+ if (chatSession.window.is_hidden) {
+ this._makeSessionVisible(chatSession);
+ } else if (session.is_folded !== chatSession.window.folded) {
+ chatSession.window.toggle_fold(session.is_folded);
+ }
+ }
+ },
+ /**
+ * Called when unfolding the chat window
+ *
+ * @param {Object} channel
+ * @param {integer} channel.id
+ * @param {boolean} [channel.is_folded]
+ */
+ toggleFoldChat: function (channel) {
+ var session = _.find(this.chatSessions, {id: channel.id});
+ if (session) {
+ session.window.toggle_fold(channel.is_folded);
+ }
+ },
+
+ //--------------------------------------------------------------------------
+ // Private
+ //--------------------------------------------------------------------------
+
+ /**
+ * Add chatSession such that it will be the left-most visible window
+ *
+ * @private
+ * @param {Object} chatSession
+ */
+ _addChatSession: function (chatSession) {
+ this._computeAvailableSlots(this.chatSessions.length+1);
+ this.chatSessions.splice(this.displayState.nbSlots-1, 0, chatSession);
+ },
+ /**
+ * @private
+ * @param {Object} chatSession
+ * @param {boolean} [chatSession.keep_unread]
+ * @param {Object} chatSession.window
+ * @param {Object} [options]
+ * @param {boolean} [options.keep_open_if_unread]
+ */
+ _closeChat: function (chatSession, options) {
+ if (options && options.keep_open_if_unread && chatSession.keep_unread) {
+ return;
+ }
+ this.chatSessions = _.without(this.chatSessions, chatSession);
+ chatSession.window.destroy();
+ this._repositionWindows();
+ },
+ /**
+ * @private
+ */
+ _closeNewChat: function () {
+ this.chatSessions = _.without(this.chatSessions, this.newChatSession);
+ this._repositionWindows({remove_new_chat: true});
+ },
+ /**
+ * @private
+ * @param {integer} nbWindows
+ */
+ _computeAvailableSlots: function (nbWindows) {
+ if (config.device.isMobile) {
+ this.displayState.nbSlots = 1; // one chat window full screen in mobile
+ return;
+ }
+ var width = window.innerWidth;
+ var nbSlots = Math.floor(width/CHAT_WINDOW_WIDTH);
+ var spaceLeft = width - (Math.min(nbSlots, nbWindows)*CHAT_WINDOW_WIDTH);
+ if (nbSlots < nbWindows && spaceLeft < 50) {
+ nbSlots--; // leave at least 50px for the hidden windows dropdown button
+ spaceLeft += CHAT_WINDOW_WIDTH;
+ }
+ this.displayState.nbSlots = nbSlots;
+ this.displayState.spaceLeft = spaceLeft;
+ },
+ /**
+ * @private
+ */
+ _destroyNewChat: function () {
+ this.newChatSession.window.destroy();
+ this.newChatSession = undefined;
+ },
+ /**
+ * @private
+ * @param {Object} session
+ * @param {Widget} session.window
+ */
+ _makeSessionVisible: function (session) {
+ utils.swap(this.chatSessions, session, this.chatSessions[this.displayState.nbSlots-1]);
+ this._repositionWindows();
+ session.window.toggle_fold(false);
+ },
+ /**
+ * @private
+ */
+ _openChatWithoutSession: function () {
+ var self = this;
+ if (!this.newChatSession) {
+ this.newChatSession = {
+ id: '_open',
+ window: new ExtendedChatWindow(web_client, undefined, _t('New message'), false, 0, {thread_less: true}),
+ };
+ this.newChatSession.window.on("close_chat_session", null, this._closeNewChat.bind(this));
+ this.newChatSession.window.on('open_dm_session', null, function (partner_id) {
+ self.newChatSession.partner_id = partner_id;
+ var dm = self.call('chat_manager', 'getDmFromPartnerID', partner_id);
+ if (!dm) {
+ self.call('chat_manager', 'openAndDetachDm', partner_id);
+ } else {
+ var dmSession = _.findWhere(self.chatSessions, {id: dm.id});
+ if (!dmSession) {
+ self.call('chat_manager', 'detachChannel', dm.id);
+ } else {
+ self._closeChat(dmSession);
+ dm.is_folded = false;
+ self.openChat(dm);
+ }
+ }
+ });
+ this._addChatSession(this.newChatSession);
+ this.newChatSession.window.appendTo($('body')).then(this._repositionWindows.bind(this));
+ } else {
+ if (this.newChatSession.window.is_hidden) {
+ this._makeSessionVisible(this.newChatSession);
+ } else if (this.newChatSession.window.folded) {
+ this.newChatSession.window.toggle_fold(false);
+ }
+ }
+ },
+ /**
+ * @private
+ * @param {Object} message
+ * @param {integer[]} message.channel_ids
+ * @param {boolean} scrollBottom
+ */
+ _updateSessions: function (message, scrollBottom) {
+ var self = this;
+ _.each(this.chatSessions, function (session) {
+ if (_.contains(message.channel_ids, session.id)) {
+ var messageVisible = !self.displayState.chatWindowsHidden && !session.window.folded &&
+ !session.window.is_hidden && session.window.thread.is_at_bottom();
+ if (messageVisible && !session.keep_unread) {
+ var channel = self.call('chat_manager', 'getChannel', session.id);
+ self.call('chat_manager', 'markChannelAsSeen', channel);
+ }
+ self.call('chat_manager', 'getMessages', {channelID: session.id}).then(function (messages) {
+ session.window.render(messages);
+ if (scrollBottom && messageVisible) {
+ session.window.thread.scroll_to();
+ }
+ });
+ }
+ });
+ },
+ /**
+ * @private
+ */
+ _renderHiddenSessionsDropdown: function () {
+ var $dropdown = $(QWeb.render("mail.ChatWindowsDropdown", {
+ sessions: this.displayState.hiddenSessions,
+ open: this.displayState.windowsDropdownIsOpen,
+ unread_counter: this.displayState.hiddenUnreadCounter,
+ widget: {isMobile: config.device.isMobile},
+ }));
+ return $dropdown;
+ },
+ /**
+ * @private
+ */
+ _repositionHiddenSessionsDropdown: function () {
+ // Unfold dropdown to the left if there is enough place
+ var $dropdownUL = this.displayState.$hiddenWindowsDropdown.children('ul');
+ if (this.displayState.spaceLeft > $dropdownUL.width() + 10) {
+ $dropdownUL.addClass('dropdown-menu-right');
+ }
+ },
+ /**
+ * @private
+ * @param {Object} [options]
+ * @param {boolean} [options.remove_new_chat]
+ */
+ _repositionWindows: function (options) {
+ var self = this;
+ if (options && options.remove_new_chat) {
+ this._destroyNewChat();
+ }
+ if (this.displayState.chatWindowsHidden) {
+ return;
+ }
+ this._computeAvailableSlots(this.chatSessions.length);
+ var hiddenSessions = [];
+ var hiddenUnreadCounter = 0;
+ var nbSlots = this.displayState.nbSlots;
+ _.each(this.chatSessions, function (session, index) {
+ if (index < nbSlots) {
+ session.window.$el.css({right: CHAT_WINDOW_WIDTH*index, bottom: 0});
+ session.window.do_show();
+ } else {
+ hiddenSessions.push(session);
+ hiddenUnreadCounter += session.window.unread_msgs;
+ session.window.do_hide();
+ }
+ });
+ this.displayState.hiddenSessions = hiddenSessions;
+ this.displayState.hiddenUnreadCounter = hiddenUnreadCounter;
+
+ if (this.displayState.$hidden_windows_dropdown) {
+ this.displayState.$hidden_windows_dropdown.remove();
+ }
+ if (hiddenSessions.length) {
+ this.displayState.$hiddenWindowsDropdown = this._renderHiddenSessionsDropdown();
+ var $hiddenWindowsDropdown = this.displayState.$hiddenWindowsDropdown;
+ $hiddenWindowsDropdown.css({right: CHAT_WINDOW_WIDTH * nbSlots, bottom: 0})
+ .appendTo($('body'));
+ this._repositionHiddenSessionsDropdown();
+ this.displayState.windowsDropdownIsOpen = false;
+
+ $hiddenWindowsDropdown.on('click', '.o_chat_header', function (event) {
+ var sessionID = $(event.currentTarget).data('session-id');
+ var session = _.findWhere(hiddenSessions, {id: sessionID});
+ if (session) {
+ self._makeSessionVisible(session);
+ }
+ });
+ $hiddenWindowsDropdown.on('click', '.o_chat_window_close', function (event) {
+ var sessionID = $(event.currentTarget).closest('.o_chat_header').data('session-id');
+ var session = _.findWhere(hiddenSessions, {id: sessionID});
+ if (session) {
+ session.window.on_click_close(event);
+ self.displayState.windowsDropdownIsOpen = true; // keep the dropdown open
+ }
+ });
+ }
+ },
+
+ //--------------------------------------------------------------------------
+ // Handlers
+ //--------------------------------------------------------------------------
+
+ /**
+ * @private
+ * @param {Object} channel
+ * @param {integer|string} channel.id
+ * @param {Object} query
+ */
+ _onAnyoneListening: function (channel, query) {
+ _.each(this.chatSessions, function (session) {
+ if (channel.id === session.id && session.window.thread.is_at_bottom() && !session.window.is_hidden) {
+ query.is_displayed = true;
+ }
+ });
+ },
+ /**
+ * @private
+ * @param {Object} channel
+ * @param {integer} channel.id
+ */
+ _onDetachChannel: function (channel) {
+ var chatSession = _.findWhere(this.chatSessions, {id: channel.id});
+ if (!chatSession || chatSession.window.folded) {
+ this.call('chat_manager', 'detachChannel', channel.id);
+ } else if (chatSession.window.is_hidden) {
+ this._makeSessionVisible(chatSession);
+ } else {
+ chatSession.window.focus_input();
+ }
+ },
+ /**
+ * @private
+ * @param {boolean} open
+ */
+ _onDiscussOpen: function (open) {
+ this.displayState.chatWindowsHidden = open;
+ if (open) {
+ $('body').addClass('o_no_chat_window');
+ } else {
+ $('body').removeClass('o_no_chat_window');
+ this._repositionWindows();
+ }
+ },
+ /**
+ * @private
+ * @param {Object} message
+ */
+ _onNewMessage: function (message) {
+ this._updateSessions(message, true);
+ },
+ /**
+ * @private
+ * @param {Object} channel
+ * @param {integer} channel.unread_counter
+ */
+ _onUpdateChannelUnreadCounter: function (channel) {
+ var self = this;
+ this.displayState.hiddenUnreadCounter = 0;
+ _.each(self.chatSessions, function (session) {
+ if (channel.id === session.id) {
+ session.window.update_unread(channel.unread_counter);
+ if (channel.unread_counter === 0) {
+ session.keep_unread = false;
+ }
+ }
+ if (session.window.is_hidden) {
+ self.displayState.hiddenUnreadCounter += session.window.unread_msgs;
+ }
+ });
+ if (self.displayState.$hidden_windows_dropdown) {
+ self.displayState.$hidden_windows_dropdown.html(self.renderHiddenSessionsDropdown().html());
+ self._repositionHiddenSessionsDropdown();
+ }
+ },
+ /**
+ * @private
+ * @param {Object} channel
+ * @param {string|integer} channel.id
+ * @param {string} channel.status e.g. 'offline'
+ */
+ _onUpdateDmPresence: function (channel) {
+ _.each(this.chatSessions, function (session) {
+ if (channel.id === session.id) {
+ session.window.update_status(channel.status);
+ }
+ });
+ },
+ /**
+ * @private
+ * @param {Object} message
+ */
+ _onUpdateMessage: function (message) {
+ this._updateSessions(message, false);
+ },
+ /**
+ * @private
+ * @param {string|integer} channelID
+ */
+ _onUnsubscribeFromChannel: function (channelID) {
+ var self = this;
+ _.each(self.chatSessions, function (session) {
+ if (channelID === session.id) {
+ self._closeChat(session);
+ }
+ });
+ },
+
+});
+
+return ChatWindowManager;
+
+});
diff --git a/addons/mail/static/src/js/systray.js b/addons/mail/static/src/js/systray.js
index 2006ae297ef..f2dfdc631ab 100644
--- a/addons/mail/static/src/js/systray.js
+++ b/addons/mail/static/src/js/systray.js
@@ -7,8 +7,6 @@ var session = require('web.session');
var SystrayMenu = require('web.SystrayMenu');
var Widget = require('web.Widget');
-var chat_manager = require('mail.chat_manager');
-
var QWeb = core.qweb;
/**
@@ -35,16 +33,20 @@ var MessagingMenu = Widget.extend({
this.$filter_buttons = this.$('.o_filter_button');
this.$channels_preview = this.$('.o_mail_navbar_dropdown_channels');
this.filter = false;
- chat_manager.bus.on("update_needaction", this, this.update_counter);
- chat_manager.bus.on("update_channel_unread_counter", this, this.update_counter);
- chat_manager.is_ready.then(this.update_counter.bind(this));
+ var chatBus = this.call('chat_manager', 'getChatBus');
+ chatBus.on("update_needaction", this, this.update_counter);
+ chatBus.on("update_channel_unread_counter", this, this.update_counter);
+ var chatReady = this.call('chat_manager', 'isReady');
+ chatReady.then(this.update_counter.bind(this));
return this._super();
},
is_open: function () {
return this.$el.hasClass('open');
},
update_counter: function () {
- var counter = chat_manager.get_needaction_counter() + chat_manager.get_unread_conversation_counter();
+ var needactionCounter = this.call('chat_manager', 'getNeedactionCounter');
+ var unreadConversationCounter = this.call('chat_manager', 'getUnreadConversationCounter');
+ var counter = needactionCounter + unreadConversationCounter;
this.$('.o_notification_counter').text(counter);
this.$el.toggleClass('o_no_notification', !counter);
if (this.is_open()) {
@@ -56,9 +58,10 @@ var MessagingMenu = Widget.extend({
// Display spinner while waiting for channels preview
this.$channels_preview.html(QWeb.render('Spinner'));
-
- chat_manager.is_ready.then(function () {
- var channels = _.filter(chat_manager.get_channels(), function (channel) {
+ var chatReady = this.call('chat_manager', 'isReady');
+ chatReady.then(function () {
+ var allChannels = self.call('chat_manager', 'getChannels');
+ var channels = _.filter(allChannels, function (channel) {
if (self.filter === 'chat') {
return channel.is_chat;
} else if (self.filter === 'channels') {
@@ -67,7 +70,7 @@ var MessagingMenu = Widget.extend({
return channel.type !== 'static';
}
});
- chat_manager.get_messages({channel_id: 'channel_inbox'}).then(function(result) {
+ self.call('chat_manager', 'getMessages', {channelID: 'channel_inbox'}).then(function (result) {
var res = [];
_.each(result, function (message) {
message.unread_counter = 1;
@@ -82,7 +85,7 @@ var MessagingMenu = Widget.extend({
if (self.filter === 'channel_inbox' || !self.filter) {
channels = _.union(channels, res);
}
- chat_manager.get_channels_preview(channels).then(self._render_channels_preview.bind(self));
+ self.call('chat_manager', 'getChannelsPreview', channels).then(self._render_channels_preview.bind(self));
});
});
},
@@ -105,7 +108,7 @@ var MessagingMenu = Widget.extend({
this.update_channels_preview();
},
on_click_new_message: function () {
- chat_manager.bus.trigger('open_chat');
+ this.call('chat_window_manager', 'openChat');
},
// Handlers
@@ -133,13 +136,13 @@ var MessagingMenu = Widget.extend({
this.do_action('mail.mail_channel_action_client_chat', {clear_breadcrumbs: true})
.then(function () {
self.trigger_up('hide_home_menu'); // we cannot 'go back to previous page' otherwise
- core.bus.trigger('change_menu_section', chat_manager.get_discuss_menu_id());
+ core.bus.trigger('change_menu_section', self.call('chat_manager', 'getDiscussMenuID').bind(self));
});
}
} else {
- var channel = chat_manager.get_channel(channelID);
+ var channel = this.call('chat_manager', 'getChannel', channelID);
if (channel) {
- chat_manager.open_channel(channel);
+ self.call('chat_manager', 'openChannel', channel);
}
}
},
@@ -156,8 +159,10 @@ var ActivityMenu = Widget.extend({
},
start: function () {
this.$activities_preview = this.$('.o_mail_navbar_dropdown_channels');
- chat_manager.bus.on("activity_updated", this, this._updateCounter);
- chat_manager.is_ready.then(this._updateCounter.bind(this));
+ var chatBus = this.call('chat_manager', 'getChatBus');
+ chatBus.on("activity_updated", this, this._updateCounter);
+ var chatReady = this.call('chat_manager', 'isReady');
+ chatReady.then(this._updateCounter.bind(this));
this._updateActivityPreview();
return this._super();
},
@@ -168,7 +173,7 @@ var ActivityMenu = Widget.extend({
* Make RPC and get current user's activity details
* @private
*/
- _getActivityData: function(){
+ _getActivityData: function () {
var self = this;
return self._rpc({
@@ -179,7 +184,7 @@ var ActivityMenu = Widget.extend({
},
}).then(function (data) {
self.activities = data;
- self.activityCounter = _.reduce(data, function(total_count, p_data){ return total_count + p_data.total_count; }, 0);
+ self.activityCounter = _.reduce(data, function (total_count, p_data) { return total_count + p_data.total_count; }, 0);
self.$('.o_notification_counter').text(self.activityCounter);
self.$el.toggleClass('o_no_notification', !self.activityCounter);
});
diff --git a/addons/mail/static/src/js/thread.js b/addons/mail/static/src/js/thread.js
index 156fb77c1ad..729d24b6163 100644
--- a/addons/mail/static/src/js/thread.js
+++ b/addons/mail/static/src/js/thread.js
@@ -142,7 +142,7 @@ var Thread = Widget.extend({
this.attachments = _.uniq(_.flatten(_.map(messages, 'attachment_ids')));
- _.each(msgs, function(msg) {
+ _.each(msgs, function (msg) {
var $msg = self.$('.o_thread_message[data-message-id="'+ msg.id +'"]');
$msg.find('.o_mail_timestamp').data('date', msg.date);
@@ -150,7 +150,7 @@ var Thread = Widget.extend({
});
if (!this.update_timestamps_interval) {
- this.update_timestamps_interval = setInterval(function() {
+ this.update_timestamps_interval = setInterval(function () {
self.update_timestamps();
}, 1000*60);
}
@@ -172,17 +172,17 @@ var Thread = Widget.extend({
// nodeType 1: element_node
// nodeType 3: text_node
var $children = $element.contents()
- .filter(function() {
+ .filter(function () {
return this.nodeType === 1 || this.nodeType === 3 && this.nodeValue.trim();
});
- _.each($children, function(child) {
+ _.each($children, function (child) {
var $child = $(child);
// Hide Text nodes if "stopSpelling"
if (child.nodeType === 3 && $child.prevAll("[id*='stopSpelling']").length > 0) {
// Convert Text nodes to Element nodes
- var $child = $('<span>', {
+ $child = $('<span>', {
text: child.textContent,
"data-o-mail-quote": "1",
});
@@ -203,7 +203,7 @@ var Thread = Widget.extend({
}
});
- _.each(groups, function(group) {
+ _.each(groups, function (group) {
// Insert link just before the first node
var $read_more = $('<a>', {
class: "o_mail_read_more",
@@ -213,7 +213,7 @@ var Thread = Widget.extend({
// Toggle All next nodes
var is_read_more = true;
- $read_more.click(function(e) {
+ $read_more.click(function (e) {
e.preventDefault();
is_read_more = !is_read_more;
_.each(group, function ($child) {
@@ -226,7 +226,7 @@ var Thread = Widget.extend({
},
update_timestamps: function () {
var isAtBottom = this.is_at_bottom();
- this.$('.o_mail_timestamp').each(function() {
+ this.$('.o_mail_timestamp').each(function () {
var date = $(this).data('date');
$(this).html(time_from_now(date));
});
diff --git a/addons/mail/static/src/js/thread_field.js b/addons/mail/static/src/js/thread_field.js
index 43d97473d3c..00ea4e2037a 100644
--- a/addons/mail/static/src/js/thread_field.js
+++ b/addons/mail/static/src/js/thread_field.js
@@ -1,7 +1,6 @@
odoo.define('mail.ThreadField', function (require) {
"use strict";
-var chat_mixin = require('mail.chat_mixin');
var ChatThread = require('mail.ChatThread');
var AbstractField = require('web.AbstractField');
@@ -15,14 +14,15 @@ var _t = core._t;
// -----------------------------------------------------------------------------
// 'mail_thread' widget: displays the thread of messages
// -----------------------------------------------------------------------------
-var ThreadField = AbstractField.extend(chat_mixin, {
+var ThreadField = AbstractField.extend({
// inherited
init: function () {
this._super.apply(this, arguments);
this.msgIDs = this.value.res_ids;
},
willStart: function () {
- return this.alive(this._chatReady());
+ var chatReady = this.call('chat_manager', 'isReady');
+ return this.alive(chatReady);
},
start: function () {
var self = this;
@@ -39,7 +39,9 @@ var ThreadField = AbstractField.extend(chat_mixin, {
this.thread.on('load_more_messages', this, this._onLoadMoreMessages);
this.thread.on('redirect', this, this._onRedirect);
this.thread.on('redirect_to_channel', this, this._onRedirectToChannel);
- this.thread.on('toggle_star_status', this, this._toggleStarStatus);
+ this.thread.on('toggle_star_status', this, function (messageID) {
+ self.call('chat_manager', 'toggleStarStatus', messageID);
+ });
var def1 = this.thread.appendTo(this.$el);
var def2 = this._super.apply(this, arguments);
@@ -47,10 +49,9 @@ var ThreadField = AbstractField.extend(chat_mixin, {
return this.alive($.when(def1, def2)).then(function () {
// unwrap the thread to remove an unnecessary level on div
self.setElement(self.thread.$el);
-
- var bus = self._getBus();
- bus.on('new_message', self, self._onNewMessage);
- bus.on('update_message', self, self._onUpdateMessage);
+ var chatBus = self.call('chat_manager', 'getChatBus');
+ chatBus.on('new_message', self, self._onNewMessage);
+ chatBus.on('update_message', self, self._onUpdateMessage);
});
},
_render: function () {
@@ -60,7 +61,7 @@ var ThreadField = AbstractField.extend(chat_mixin, {
return true;
},
destroy: function () {
- this._removeChatterMessages(this.model);
+ this.call('chat_manager', 'removeChatterMessages', this.model);
this._super.apply(this, arguments);
},
_reset: function (record) {
@@ -82,7 +83,7 @@ var ThreadField = AbstractField.extend(chat_mixin, {
postMessage: function (message) {
var self = this;
var options = {model: this.model, res_id: this.res_id};
- return this._postMessage(message, options)
+ return this.call('chat_manager', 'postMessage', message, options)
.then(function () {
if (message.partner_ids.length) {
self.trigger_up('reload_mail_fields', {followers: true});
@@ -123,7 +124,7 @@ var ThreadField = AbstractField.extend(chat_mixin, {
var self = this;
options = options || {};
options.ids = ids;
- var fetch_def = this.dp.add(this._getMessages(options));
+ var fetch_def = this.dp.add(this.call('chat_manager', 'getMessages', options));
return fetch_def.then(function (raw_messages) {
var isCreateMode = false;
if (!self.res_id) {
@@ -147,7 +148,7 @@ var ThreadField = AbstractField.extend(chat_mixin, {
* @returns {Deferred}
*/
_onLoadMoreMessages: function () {
- this._fetchAndRenderThread(this.msgIDs, {force_fetch: true});
+ this._fetchAndRenderThread(this.msgIDs, {forceFetch: true});
},
_onNewMessage: function (message) {
if (message.model === this.model && message.res_id === this.res_id) {
@@ -161,7 +162,7 @@ var ThreadField = AbstractField.extend(chat_mixin, {
},
_onRedirectToChannel: function (channelID) {
var self = this;
- this._joinChannel(channelID).then(function () {
+ this.call('chat_manager', 'joinChannel', channelID).then(function () {
// Execute Discuss client action with 'channel' as default channel
self.do_action('mail.mail_channel_action_client_chat', {active_id: channelID});
});
diff --git a/addons/mail/static/src/js/utils.js b/addons/mail/static/src/js/utils.js
index 49d1705eccd..533fe67f91b 100644
--- a/addons/mail/static/src/js/utils.js
+++ b/addons/mail/static/src/js/utils.js
@@ -1,48 +1,6 @@
odoo.define('mail.utils', function (require) {
"use strict";
-var bus = require('bus.bus').bus;
-
-
-function send_notification(widget, title, content) {
- if (window.Notification && Notification.permission === "granted") {
- if (bus.is_master) {
- _send_native_notification(title, content);
- }
- } else {
- widget.do_notify(title, content);
- if (bus.is_master) {
- _beep(widget);
- }
- }
-}
-function _send_native_notification(title, content) {
- var notification = new Notification(title, {body: content, icon: "/mail/static/src/img/odoo_o.png"});
- notification.onclick = function () {
- window.focus();
- if (this.cancel) {
- this.cancel();
- } else if (this.close) {
- this.close();
- }
- };
-}
-var _beep = (function () {
- if (typeof(Audio) === "undefined") {
- return function () {};
- }
- var audio;
- return function (widget) {
- if (!audio) {
- audio = new Audio();
- var ext = audio.canPlayType("audio/ogg; codecs=vorbis") ? ".ogg" : ".mp3";
- var session = widget.getSession();
- audio.src = session.url("/mail/static/src/audio/ting" + ext);
- }
- audio.play();
- };
-})();
-
function parse_and_transform(html_string, transform_function) {
var open_token = "OPEN" + Date.now();
var string = html_string.replace(/&lt;/g, open_token);
@@ -141,7 +99,6 @@ function unaccent (str) {
}
return {
- send_notification: send_notification,
parse_and_transform: parse_and_transform,
add_link: add_link,
linkify: linkify,
diff --git a/addons/mail/static/src/js/window_manager.js b/addons/mail/static/src/js/window_manager.js
deleted file mode 100644
index 147a3762068..00000000000
--- a/addons/mail/static/src/js/window_manager.js
+++ /dev/null
@@ -1,400 +0,0 @@
-odoo.define('mail.window_manager', function (require) {
-"use strict";
-
-var chat_manager = require('mail.chat_manager');
-var ExtendedChatWindow = require('mail.ExtendedChatWindow');
-
-var config = require('web.config');
-var core = require('web.core');
-var utils = require('web.utils');
-var web_client = require('web.web_client');
-
-var _t = core._t;
-var QWeb = core.qweb;
-
-// chat window management
-//----------------------------------------------------------------
-var CHAT_WINDOW_WIDTH = 325 + 5; // 5 pixels between windows
-var chat_sessions = [];
-var new_chat_session;
-var display_state = {
- chat_windows_hidden: false, // chat windows aren't displayed when the client action is open
- hidden_sessions: [],
- hidden_unread_counter: 0, // total number of unread msgs in hidden chat windows
- nb_slots: 0,
- space_left: 0,
- windows_dropdown_is_open: false, // used to keep dropdown open when closing chat windows
-};
-
-function add_chat_session (chat_session) {
- // adds chat_session such that it will be the left-most visible window
- compute_available_slots(chat_sessions.length+1);
- chat_sessions.splice(display_state.nb_slots-1, 0, chat_session);
-}
-
-// options.passively: if set to true, open the chat window without focusing the
-// input and marking messages as read if it is not open yet, and do nothing
-// otherwise
-function open_chat (session, options) {
- if (!session) {
- open_chat_without_session();
- return;
- }
- options = options || {};
- var chat_session = _.findWhere(chat_sessions, {id: session.id});
- if (!chat_session) {
- var prefix = !session.is_chat ? "#" : "";
- var window_options = {
- autofocus: !options.passively,
- input_less: session.mass_mailing,
- status: session.status,
- };
- chat_session = {
- id: session.id,
- uuid: session.uuid,
- name: session.name,
- keep_unread: options.passively, // don't automatically mark unread messages as seen
- window: new ExtendedChatWindow(web_client, session.id, prefix + session.name, session.is_folded, session.unread_counter, window_options),
- };
- chat_session.window.on("close_chat_session", null, function () {
- close_chat(chat_session);
- chat_manager.close_chat_session(chat_session.id);
- });
- chat_session.window.on("toggle_star_status", null, function (message_id) {
- chat_manager.toggle_star_status(message_id);
- });
-
- chat_session.window.on("fold_channel", null, function (channel_id, folded) {
- chat_manager.fold_channel(channel_id, folded);
- });
-
- chat_session.window.on("post_message", null, function (message, channel_id) {
- chat_manager
- .post_message(message, {channel_id: channel_id})
- .then(function () {
- chat_session.window.thread.scroll_to();
- });
- });
- chat_session.window.on("redirect", null, function (res_model, res_id) {
- chat_manager.redirect(res_model, res_id, open_chat);
- });
- chat_session.window.on("redirect_to_channel", null, function (channel_id) {
- var session = _.findWhere(chat_sessions, {id: channel_id});
- if (!session) {
- chat_manager.join_channel(channel_id).then(function (channel) {
- chat_manager.detach_channel(channel);
- });
- } else {
- session.window.toggle_fold(false);
- }
- });
-
- var remove_new_chat = false;
- if (options.passively) {
- chat_sessions.push(chat_session); // simply insert the window to the left
- } else if (new_chat_session && new_chat_session.partner_id && new_chat_session.partner_id === session.direct_partner_id) {
- // the window takes the place of the 'new_chat_session' window
- chat_sessions[_.indexOf(chat_sessions, new_chat_session)] = chat_session;
- remove_new_chat = true;
- } else {
- add_chat_session(chat_session); // add session such that window is visible
- }
-
- chat_session.window.appendTo($('body'))
- .then(function () {
- reposition_windows({remove_new_chat: remove_new_chat});
- return chat_manager.get_messages({channel_id: chat_session.id});
- }).then(function (messages) {
- chat_session.window.render(messages);
- chat_session.window.thread.scroll_to();
- setTimeout(function () {
- chat_session.window.thread.$el.on("scroll", null, _.debounce(function () {
- if (!chat_session.keep_unread && chat_session.window.thread.is_at_bottom()) {
- chat_manager.mark_channel_as_seen(session);
- }
- }, 100));
- }, 0); // setTimeout to prevent to execute handler on first scroll_to, which is asynchronous
- if (options.passively) {
- // mark first unread messages as seen when focusing the window, then on scroll to bottom as usual
- chat_session.window.$('.o_mail_thread, .o_chat_composer').one('click', function () {
- chat_manager.mark_channel_as_seen(session);
- });
- } else if (!display_state.chat_windows_hidden && !session.is_folded) {
- chat_manager.mark_channel_as_seen(session);
- }
- });
- } else if (!options.passively) {
- if (chat_session.window.is_hidden) {
- make_session_visible(chat_session);
- } else if (session.is_folded !== chat_session.window.folded) {
- chat_session.window.toggle_fold(session.is_folded);
- }
- }
-}
-
-function open_chat_without_session () {
- if (!new_chat_session) {
- new_chat_session = {
- id: '_open',
- window: new ExtendedChatWindow(web_client, undefined, _t('New message'), false, 0, {thread_less: true}),
- };
- new_chat_session.window.on("close_chat_session", null, close_new_chat);
- new_chat_session.window.on('open_dm_session', null, function (partner_id) {
- new_chat_session.partner_id = partner_id;
- var dm = chat_manager.get_dm_from_partner_id(partner_id);
- if (!dm) {
- chat_manager.open_and_detach_dm(partner_id);
- } else {
- var dm_session = _.findWhere(chat_sessions, {id: dm.id});
- if (!dm_session) {
- chat_manager.detach_channel(dm);
- } else {
- close_chat(dm_session);
- dm.is_folded = false;
- open_chat(dm);
- }
- }
- });
- add_chat_session(new_chat_session);
- new_chat_session.window.appendTo($('body')).then(reposition_windows);
- } else {
- if (new_chat_session.window.is_hidden) {
- make_session_visible(new_chat_session);
- } else if (new_chat_session.window.folded) {
- new_chat_session.window.toggle_fold(false);
- }
- }
-}
-
-function close_chat (chat_session, options) {
- if (options && options.keep_open_if_unread && chat_session.keep_unread) {
- return;
- }
- chat_sessions = _.without(chat_sessions, chat_session);
- chat_session.window.destroy();
- reposition_windows();
-}
-
-function close_new_chat () {
- chat_sessions = _.without(chat_sessions, new_chat_session);
- reposition_windows({remove_new_chat: true});
-}
-
-function destroy_new_chat () {
- new_chat_session.window.destroy();
- new_chat_session = undefined;
-}
-
-function toggle_fold_chat (channel) {
- var session = _.find(chat_sessions, {id: channel.id});
- if (session) {
- session.window.toggle_fold(channel.is_folded);
- }
-}
-
-function compute_available_slots (nb_windows) {
- if (config.device.isMobile) {
- display_state.nb_slots = 1; // one chat window full screen in mobile
- return;
- }
- var width = window.innerWidth;
- var nb_slots = Math.floor(width/CHAT_WINDOW_WIDTH);
- var space_left = width - (Math.min(nb_slots, nb_windows)*CHAT_WINDOW_WIDTH);
- if (nb_slots < nb_windows && space_left < 50) {
- nb_slots--; // leave at least 50px for the hidden windows dropdown button
- space_left += CHAT_WINDOW_WIDTH;
- }
- display_state.nb_slots = nb_slots;
- display_state.space_left = space_left;
-}
-
-var reposition_windows = function (options) {
- if (options && options.remove_new_chat) {
- destroy_new_chat();
- }
- if (display_state.chat_windows_hidden) {
- return;
- }
- compute_available_slots(chat_sessions.length);
- var hidden_sessions = [];
- var hidden_unread_counter = 0;
- var nb_slots = display_state.nb_slots;
- _.each(chat_sessions, function (session, index) {
- if (index < nb_slots) {
- session.window.$el.css({right: CHAT_WINDOW_WIDTH*index, bottom: 0});
- session.window.do_show();
- } else {
- hidden_sessions.push(session);
- hidden_unread_counter += session.window.unread_msgs;
- session.window.do_hide();
- }
- });
- display_state.hidden_sessions = hidden_sessions;
- display_state.hidden_unread_counter = hidden_unread_counter;
-
- if (display_state.$hidden_windows_dropdown) {
- display_state.$hidden_windows_dropdown.remove();
- }
- if (hidden_sessions.length) {
- display_state.$hidden_windows_dropdown = render_hidden_sessions_dropdown();
- var $hidden_windows_dropdown = display_state.$hidden_windows_dropdown;
- $hidden_windows_dropdown.css({right: CHAT_WINDOW_WIDTH * nb_slots, bottom: 0})
- .appendTo($('body'));
- reposition_hidden_sessions_dropdown();
- display_state.windows_dropdown_is_open = false;
-
- $hidden_windows_dropdown.on('click', '.o_chat_header', function (event) {
- var session_id = $(event.currentTarget).data('session-id');
- var session = _.findWhere(hidden_sessions, {id: session_id});
- if (session) {
- make_session_visible(session);
- }
- });
- $hidden_windows_dropdown.on('click', '.o_chat_window_close', function (event) {
- var session_id = $(event.currentTarget).closest('.o_chat_header').data('session-id');
- var session = _.findWhere(hidden_sessions, {id: session_id});
- if (session) {
- session.window.on_click_close(event);
- display_state.windows_dropdown_is_open = true; // keep the dropdown open
- }
- });
- }
-};
-
-function make_session_visible (session) {
- utils.swap(chat_sessions, session, chat_sessions[display_state.nb_slots-1]);
- reposition_windows();
- session.window.toggle_fold(false);
-}
-
-function render_hidden_sessions_dropdown () {
- var $dropdown = $(QWeb.render("mail.ChatWindowsDropdown", {
- sessions: display_state.hidden_sessions,
- open: display_state.windows_dropdown_is_open,
- unread_counter: display_state.hidden_unread_counter,
- widget: {isMobile: config.device.isMobile},
- }));
- return $dropdown;
-}
-
-function reposition_hidden_sessions_dropdown () {
- // Unfold dropdown to the left if there is enough place
- var $dropdown_ul = display_state.$hidden_windows_dropdown.children('ul');
- if (display_state.space_left > $dropdown_ul.width() + 10) {
- $dropdown_ul.addClass('dropdown-menu-right');
- }
-}
-
-function update_sessions (message, scrollBottom) {
- _.each(chat_sessions, function (session) {
- if (_.contains(message.channel_ids, session.id)) {
- var message_visible = !display_state.chat_windows_hidden && !session.window.folded &&
- !session.window.is_hidden && session.window.thread.is_at_bottom();
- if (message_visible && !session.keep_unread) {
- chat_manager.mark_channel_as_seen(chat_manager.get_channel(session.id));
- }
- chat_manager.get_messages({channel_id: session.id}).then(function (messages) {
- session.window.render(messages);
- if (scrollBottom && message_visible) {
- session.window.thread.scroll_to();
- }
- });
- }
- });
-}
-
-
-core.bus.on('web_client_ready', null, function () {
- chat_manager.bus.on('open_chat', null, open_chat);
- chat_manager.bus.on('close_chat', null, function (channel, options) {
- var session = _.find(chat_sessions, {id: channel.id});
- if (session) {
- close_chat(session, options);
- }
- });
- chat_manager.bus.on('channel_toggle_fold', null, toggle_fold_chat);
-
- chat_manager.bus.on('new_message', null, function (message) {
- update_sessions(message, true);
- });
-
- chat_manager.bus.on('update_message', null, function (message) {
- update_sessions(message, false);
- });
-
- chat_manager.bus.on('anyone_listening', null, function (channel, query) {
- _.each(chat_sessions, function (session) {
- if (channel.id === session.id && session.window.thread.is_at_bottom() && !session.window.is_hidden) {
- query.is_displayed = true;
- }
- });
- });
-
- chat_manager.bus.on('unsubscribe_from_channel', null, function (channel_id) {
- _.each(chat_sessions, function (session) {
- if (channel_id === session.id) {
- close_chat(session);
- }
- });
- });
-
- chat_manager.bus.on('update_channel_unread_counter', null, function (channel) {
- display_state.hidden_unread_counter = 0;
- _.each(chat_sessions, function (session) {
- if (channel.id === session.id) {
- session.window.update_unread(channel.unread_counter);
- if (channel.unread_counter === 0) {
- session.keep_unread = false;
- }
- }
- if (session.window.is_hidden) {
- display_state.hidden_unread_counter += session.window.unread_msgs;
- }
- });
- if (display_state.$hidden_windows_dropdown) {
- display_state.$hidden_windows_dropdown.html(render_hidden_sessions_dropdown().html());
- reposition_hidden_sessions_dropdown();
- }
- });
-
- chat_manager.bus.on('update_dm_presence', null, function (channel) {
- _.each(chat_sessions, function (session) {
- if (channel.id === session.id) {
- session.window.update_status(channel.status);
- }
- });
- });
-
- chat_manager.is_ready.then(function() {
- _.each(chat_manager.get_channels(), function (channel) {
- if (channel.is_detached) {
- open_chat(channel);
- }
- });
- });
-
- chat_manager.bus.on('detach_channel', null, function (channel) {
- var chat_session = _.findWhere(chat_sessions, {id: channel.id});
- if (!chat_session || chat_session.window.folded) {
- chat_manager.detach_channel(channel);
- } else if (chat_session.window.is_hidden) {
- make_session_visible(chat_session);
- } else {
- chat_session.window.focus_input();
- }
- });
-
- chat_manager.bus.on('client_action_open', null, function (open) {
- display_state.chat_windows_hidden = open;
- if (open) {
- $('body').addClass('o_no_chat_window');
- } else {
- $('body').removeClass('o_no_chat_window');
- reposition_windows();
- }
- });
-
- core.bus.on('resize', null, _.debounce(reposition_windows, 100));
-});
-
-});