You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

1302 lines
47 KiB

  1. odoo.define('mail_base.base', function (require) {
  2. "use strict";
  3. var bus = require('bus.bus').bus;
  4. var utils = require('mail.utils');
  5. var config = require('web.config');
  6. var Bus = require('web.Bus');
  7. var core = require('web.core');
  8. var session = require('web.session');
  9. var time = require('web.time');
  10. var web_client = require('web.web_client');
  11. var Class = require('web.Class');
  12. var Mixins = require('web.mixins');
  13. var ServicesMixin = require('web.ServicesMixin');
  14. var _t = core._t;
  15. var _lt = core._lt;
  16. var LIMIT = 25;
  17. var preview_msg_max_size = 350; // optimal for native english speakers
  18. var ODOOBOT_ID = "ODOOBOT";
  19. var chat_manager = require('mail.chat_manager');
  20. // Private model
  21. //----------------------------------------------------------------------------------
  22. var messages = [];
  23. var channels = [];
  24. var channels_preview_def;
  25. var channel_defs = {};
  26. var chat_unread_counter = 0;
  27. var unread_conversation_counter = 0;
  28. var emojis = [];
  29. var emoji_substitutions = {};
  30. var emoji_unicodes = {};
  31. var needaction_counter = 0;
  32. var starred_counter = 0;
  33. var mention_partner_suggestions = [];
  34. var canned_responses = [];
  35. var commands = [];
  36. var discuss_menu_id;
  37. var global_unread_counter = 0;
  38. var pinned_dm_partners = []; // partner_ids we have a pinned DM with
  39. var client_action_open = false;
  40. // Global unread counter and notifications
  41. //----------------------------------------------------------------------------------
  42. bus.on("window_focus", null, function() {
  43. global_unread_counter = 0;
  44. web_client.set_title_part("_chat");
  45. });
  46. chat_manager.notify_incoming_message = function (msg, options) {
  47. if (bus.is_odoo_focused() && options.is_displayed) {
  48. // no need to notify
  49. return;
  50. }
  51. var title = _t('New message');
  52. if (msg.author_id[1]) {
  53. title = _.escape(msg.author_id[1]);
  54. }
  55. var content = utils.parse_and_transform(msg.body, utils.strip_html).substr(0, preview_msg_max_size);
  56. if (!bus.is_odoo_focused()) {
  57. global_unread_counter++;
  58. var tab_title = _.str.sprintf(_t("%d Messages"), global_unread_counter);
  59. web_client.set_title_part("_chat", tab_title);
  60. }
  61. utils.send_notification(web_client, title, content);
  62. }
  63. // Message and channel manipulation helpers
  64. //----------------------------------------------------------------------------------
  65. // options: channel_id, silent
  66. chat_manager.add_message = function (data, options) {
  67. options = options || {};
  68. var msg = _.findWhere(messages, { id: data.id });
  69. if (!msg) {
  70. msg = chat_manager.make_message(data);
  71. // Keep the array ordered by id when inserting the new message
  72. messages.splice(_.sortedIndex(messages, msg, 'id'), 0, msg);
  73. _.each(msg.channel_ids, function (channel_id) {
  74. var channel = chat_manager.get_channel(channel_id);
  75. if (channel) {
  76. // update the channel's last message (displayed in the channel
  77. // preview, in mobile)
  78. if (!channel.last_message || msg.id > channel.last_message.id) {
  79. channel.last_message = msg;
  80. }
  81. chat_manager.add_to_cache(msg, []);
  82. if (options.domain && options.domain !== []) {
  83. chat_manager.add_to_cache(msg, options.domain);
  84. }
  85. if (channel.hidden) {
  86. channel.hidden = false;
  87. chat_manager.bus.trigger('new_channel', channel);
  88. }
  89. if (channel.type !== 'static' && !msg.is_author && !msg.is_system_notification) {
  90. if (options.increment_unread) {
  91. chat_manager.update_channel_unread_counter(channel, channel.unread_counter+1);
  92. }
  93. if (channel.is_chat && options.show_notification) {
  94. if (!client_action_open && !config.device.isMobile) {
  95. // automatically open chat window
  96. chat_manager.bus.trigger('open_chat', channel, { passively: true });
  97. }
  98. var query = {is_displayed: false};
  99. chat_manager.bus.trigger('anyone_listening', channel, query);
  100. chat_manager.notify_incoming_message(msg, query);
  101. }
  102. }
  103. }
  104. });
  105. if (!options.silent) {
  106. chat_manager.bus.trigger('new_message', msg);
  107. }
  108. } else if (options.domain && options.domain !== []) {
  109. chat_manager.add_to_cache(msg, options.domain);
  110. }
  111. return msg;
  112. }
  113. chat_manager.get_channel_array = function(msg){
  114. return [ msg.channel_ids, 'channel_inbox', 'channel_starred' ];
  115. }
  116. chat_manager.get_properties = function(msg){
  117. return {
  118. is_starred: chat_manager.property_descr("channel_starred", msg, chat_manager),
  119. is_needaction: chat_manager.property_descr("channel_inbox", msg, chat_manager)
  120. };
  121. }
  122. chat_manager.property_descr = function (channel, msg, self) {
  123. return {
  124. enumerable: true,
  125. get: function () {
  126. return _.contains(msg.channel_ids, channel);
  127. },
  128. set: function (bool) {
  129. if (bool) {
  130. chat_manager.add_channel_to_message(msg, channel);
  131. } else {
  132. msg.channel_ids = _.without(msg.channel_ids, channel);
  133. }
  134. }
  135. };
  136. }
  137. chat_manager.set_channel_flags = function(data, msg){
  138. if (_.contains(data.needaction_partner_ids, session.partner_id)) {
  139. msg.is_needaction = true;
  140. }
  141. if (_.contains(data.starred_partner_ids, session.partner_id)) {
  142. msg.is_starred = true;
  143. }
  144. return msg;
  145. }
  146. chat_manager.make_message = function (data) {
  147. var msg = {
  148. id: data.id,
  149. author_id: data.author_id,
  150. body: data.body || "",
  151. date: moment(time.str_to_datetime(data.date)),
  152. message_type: data.message_type,
  153. subtype_description: data.subtype_description,
  154. is_author: data.author_id && data.author_id[0] === session.partner_id,
  155. is_note: data.is_note,
  156. is_system_notification: (data.message_type === 'notification' && data.model === 'mail.channel')
  157. || data.info === 'transient_message',
  158. attachment_ids: data.attachment_ids || [],
  159. subject: data.subject,
  160. email_from: data.email_from,
  161. customer_email_status: data.customer_email_status,
  162. customer_email_data: data.customer_email_data,
  163. record_name: data.record_name,
  164. tracking_value_ids: data.tracking_value_ids,
  165. channel_ids: data.channel_ids,
  166. model: data.model,
  167. res_id: data.res_id,
  168. url: session.url("/mail/view?message_id=" + data.id),
  169. module_icon:data.module_icon,
  170. };
  171. _.each(_.keys(emoji_substitutions), function (key) {
  172. var escaped_key = String(key).replace(/([.*+?=^!:${}()|[\]\/\\])/g, '\\$1');
  173. var regexp = new RegExp("(?:^|\\s|<[a-z]*>)(" + escaped_key + ")(?=\\s|$|</[a-z]*>)", "g");
  174. msg.body = msg.body.replace(regexp, ' <span class="o_mail_emoji">'+emoji_substitutions[key]+'</span> ');
  175. });
  176. Object.defineProperties(msg, chat_manager.get_properties(msg));
  177. msg = chat_manager.set_channel_flags(data, msg);
  178. if (msg.model === 'mail.channel') {
  179. var real_channels = _.without(chat_manager.get_channel_array(msg));
  180. var origin = real_channels.length === 1 ? real_channels[0] : undefined;
  181. var channel = origin && chat_manager.get_channel(origin);
  182. if (channel) {
  183. msg.origin_id = origin;
  184. msg.origin_name = channel.name;
  185. }
  186. }
  187. // Compute displayed author name or email
  188. if ((!msg.author_id || !msg.author_id[0]) && msg.email_from) {
  189. msg.mailto = msg.email_from;
  190. } else {
  191. msg.displayed_author = (msg.author_id === ODOOBOT_ID) && "OdooBot" ||
  192. msg.author_id && msg.author_id[1] ||
  193. msg.email_from || _t('Anonymous');
  194. }
  195. // Don't redirect on author clicked of self-posted or OdooBot messages
  196. msg.author_redirect = !msg.is_author && msg.author_id !== ODOOBOT_ID;
  197. // Compute the avatar_url
  198. if (msg.author_id === ODOOBOT_ID) {
  199. msg.avatar_src = "/mail/static/src/img/odoo_o.png";
  200. } else if (msg.author_id && msg.author_id[0]) {
  201. msg.avatar_src = "/web/image/res.partner/" + msg.author_id[0] + "/image_small";
  202. } else if (msg.message_type === 'email') {
  203. msg.avatar_src = "/mail/static/src/img/email_icon.png";
  204. } else {
  205. msg.avatar_src = "/mail/static/src/img/smiley/avatar.jpg";
  206. }
  207. // add anchor tags to urls
  208. msg.body = utils.parse_and_transform(msg.body, utils.add_link);
  209. // Compute url of attachments
  210. _.each(msg.attachment_ids, function(a) {
  211. a.url = '/web/content/' + a.id + '?download=true';
  212. });
  213. // format date to the local only once by message
  214. // can not be done in preprocess, since it alter the original value
  215. if (msg.tracking_value_ids && msg.tracking_value_ids.length) {
  216. _.each(msg.tracking_value_ids, function(f) {
  217. if (f.field_type === 'datetime') {
  218. var format = 'LLL';
  219. if (f.old_value) {
  220. f.old_value = moment.utc(f.old_value).local().format(format);
  221. }
  222. if (f.new_value) {
  223. f.new_value = moment.utc(f.new_value).local().format(format);
  224. }
  225. } else if (f.field_type === 'date') {
  226. var format = 'LL';
  227. if (f.old_value) {
  228. f.old_value = moment(f.old_value).local().format(format);
  229. }
  230. if (f.new_value) {
  231. f.new_value = moment(f.new_value).local().format(format);
  232. }
  233. }
  234. });
  235. }
  236. return msg;
  237. }
  238. chat_manager.add_channel_to_message = function (message, channel_id) {
  239. message.channel_ids.push(channel_id);
  240. message.channel_ids = _.uniq(message.channel_ids);
  241. }
  242. chat_manager.add_channel = function (data, options) {
  243. options = typeof options === "object" ? options : {};
  244. var channel = chat_manager.get_channel(data.id);
  245. if (channel) {
  246. if (channel.is_folded !== (data.state === "folded")) {
  247. channel.is_folded = (data.state === "folded");
  248. chat_manager.bus.trigger("channel_toggle_fold", channel);
  249. }
  250. } else {
  251. channel = chat_manager.make_channel(data, options);
  252. channels.push(channel);
  253. if (data.last_message) {
  254. channel.last_message = chat_manager.add_message(data.last_message);
  255. }
  256. // In case of a static channel (Inbox, Starred), the name is translated thanks to _lt
  257. // (lazy translate). In this case, channel.name is an object, not a string.
  258. channels = _.sortBy(channels, function (channel) { return _.isString(channel.name) ? channel.name.toLowerCase() : '' });
  259. if (!options.silent) {
  260. chat_manager.bus.trigger("new_channel", channel);
  261. }
  262. if (channel.is_detached) {
  263. chat_manager.bus.trigger("open_chat", channel);
  264. }
  265. }
  266. return channel;
  267. }
  268. chat_manager.make_channel = function (data, options) {
  269. var channel = {
  270. id: data.id,
  271. name: data.name,
  272. server_type: data.channel_type,
  273. type: data.type || data.channel_type,
  274. all_history_loaded: false,
  275. uuid: data.uuid,
  276. is_detached: data.is_minimized,
  277. is_folded: data.state === "folded",
  278. autoswitch: 'autoswitch' in options ? options.autoswitch : true,
  279. hidden: options.hidden,
  280. display_needactions: options.display_needactions,
  281. mass_mailing: data.mass_mailing,
  282. group_based_subscription: data.group_based_subscription,
  283. needaction_counter: data.message_needaction_counter || 0,
  284. unread_counter: 0,
  285. last_seen_message_id: data.seen_message_id,
  286. cache: {'[]': {
  287. all_history_loaded: false,
  288. loaded: false,
  289. messages: [],
  290. }},
  291. };
  292. if (channel.type === "channel") {
  293. channel.type = data.public !== "private" ? "public" : "private";
  294. }
  295. if (_.size(data.direct_partner) > 0) {
  296. channel.type = "dm";
  297. channel.name = data.direct_partner[0].name;
  298. channel.direct_partner_id = data.direct_partner[0].id;
  299. channel.status = data.direct_partner[0].im_status;
  300. pinned_dm_partners.push(channel.direct_partner_id);
  301. bus.update_option('bus_presence_partner_ids', pinned_dm_partners);
  302. } else if ('anonymous_name' in data) {
  303. channel.name = data.anonymous_name;
  304. }
  305. if (data.last_message_date) {
  306. channel.last_message_date = moment(time.str_to_datetime(data.last_message_date));
  307. }
  308. channel.is_chat = !channel.type.match(/^(public|private|static)$/);
  309. if (data.message_unread_counter) {
  310. chat_manager.update_channel_unread_counter(channel, data.message_unread_counter);
  311. }
  312. return channel;
  313. }
  314. chat_manager.remove_channel = function (channel) {
  315. if (!channel) { return; }
  316. if (channel.type === 'dm') {
  317. var index = pinned_dm_partners.indexOf(channel.direct_partner_id);
  318. if (index > -1) {
  319. pinned_dm_partners.splice(index, 1);
  320. bus.update_option('bus_presence_partner_ids', pinned_dm_partners);
  321. }
  322. }
  323. channels = _.without(channels, channel);
  324. delete channel_defs[channel.id];
  325. }
  326. chat_manager.get_channel_cache = function (channel, domain) {
  327. var stringified_domain = JSON.stringify(domain || []);
  328. if (!channel.cache[stringified_domain]) {
  329. channel.cache[stringified_domain] = {
  330. all_history_loaded: false,
  331. loaded: false,
  332. messages: [],
  333. };
  334. }
  335. return channel.cache[stringified_domain];
  336. }
  337. chat_manager.invalidate_caches = function (channel_ids) {
  338. _.each(channel_ids, function (channel_id) {
  339. var channel = chat_manager.get_channel(channel_id);
  340. if (channel) {
  341. channel.cache = { '[]': channel.cache['[]']};
  342. }
  343. });
  344. }
  345. chat_manager.add_to_cache = function (message, domain) {
  346. _.each(message.channel_ids, function (channel_id) {
  347. var channel = chat_manager.get_channel(channel_id);
  348. if (channel) {
  349. var channel_cache = chat_manager.get_channel_cache(channel, domain);
  350. var index = _.sortedIndex(channel_cache.messages, message, 'id');
  351. if (channel_cache.messages[index] !== message) {
  352. channel_cache.messages.splice(index, 0, message);
  353. }
  354. }
  355. });
  356. }
  357. chat_manager.remove_message_from_channel = function (channel_id, message) {
  358. message.channel_ids = _.without(message.channel_ids, channel_id);
  359. var channel = _.findWhere(channels, { id: channel_id });
  360. _.each(channel.cache, function (cache) {
  361. cache.messages = _.without(cache.messages, message);
  362. });
  363. }
  364. chat_manager.update_channel_unread_counter = function (channel, counter) {
  365. if (channel.unread_counter > 0 && counter === 0) {
  366. unread_conversation_counter = Math.max(0, unread_conversation_counter-1);
  367. } else if (channel.unread_counter === 0 && counter > 0) {
  368. unread_conversation_counter++;
  369. }
  370. if (channel.is_chat) {
  371. chat_unread_counter = Math.max(0, chat_unread_counter - channel.unread_counter + counter);
  372. }
  373. channel.unread_counter = counter;
  374. chat_manager.bus.trigger("update_channel_unread_counter", channel);
  375. }
  376. // Notification handlers
  377. // ---------------------------------------------------------------------------------
  378. chat_manager.on_notification = function (notifications) {
  379. // sometimes, the web client receives unsubscribe notification and an extra
  380. // notification on that channel. This is then followed by an attempt to
  381. // rejoin the channel that we just left. The next few lines remove the
  382. // extra notification to prevent that situation to occur.
  383. var unsubscribed_notif = _.find(notifications, function (notif) {
  384. return notif[1].info === "unsubscribe";
  385. });
  386. if (unsubscribed_notif) {
  387. notifications = _.reject(notifications, function (notif) {
  388. return notif[0][1] === "mail.channel" && notif[0][2] === unsubscribed_notif[1].id;
  389. });
  390. }
  391. _.each(notifications, function (notification) {
  392. var model = notification[0][1];
  393. if (model === 'ir.needaction') {
  394. // new message in the inbox
  395. chat_manager.on_needaction_notification(notification[1]);
  396. } else if (model === 'mail.channel') {
  397. // new message in a channel
  398. chat_manager.on_channel_notification(notification[1]);
  399. } else if (model === 'res.partner') {
  400. // channel joined/left, message marked as read/(un)starred, chat open/closed
  401. chat_manager.on_partner_notification(notification[1]);
  402. } else if (model === 'bus.presence') {
  403. // update presence of users
  404. chat_manager.on_presence_notification(notification[1]);
  405. }
  406. });
  407. }
  408. chat_manager.on_needaction_notification = function (message) {
  409. message = chat_manager.add_message(message, {
  410. channel_id: 'channel_inbox',
  411. show_notification: true,
  412. increment_unread: true,
  413. });
  414. chat_manager.invalidate_caches(message.channel_ids);
  415. if (message.channel_ids.length !== 0) {
  416. needaction_counter++;
  417. }
  418. _.each(message.channel_ids, function (channel_id) {
  419. var channel = chat_manager.get_channel(channel_id);
  420. if (channel) {
  421. channel.needaction_counter++;
  422. }
  423. });
  424. chat_manager.bus.trigger('update_needaction', needaction_counter);
  425. }
  426. chat_manager.on_channel_notification = function (message) {
  427. var def;
  428. var channel_already_in_cache = true;
  429. if (message.channel_ids.length === 1) {
  430. channel_already_in_cache = !!chat_manager.get_channel(message.channel_ids[0]);
  431. def = chat_manager.join_channel(message.channel_ids[0], {autoswitch: false});
  432. } else {
  433. def = $.when();
  434. }
  435. def.then(function () {
  436. // don't increment unread if channel wasn't in cache yet as its unread counter has just been fetched
  437. chat_manager.add_message(message, { show_notification: true, increment_unread: channel_already_in_cache });
  438. chat_manager.invalidate_caches(message.channel_ids);
  439. });
  440. }
  441. chat_manager.on_partner_notification = function (data) {
  442. if (data.info === "unsubscribe") {
  443. var channel = chat_manager.get_channel(data.id);
  444. if (channel) {
  445. var msg;
  446. if (_.contains(['public', 'private'], channel.type)) {
  447. msg = _.str.sprintf(_t('You unsubscribed from <b>%s</b>.'), channel.name);
  448. } else {
  449. msg = _.str.sprintf(_t('You unpinned your conversation with <b>%s</b>.'), channel.name);
  450. }
  451. chat_manager.remove_channel(channel);
  452. chat_manager.bus.trigger("unsubscribe_from_channel", data.id);
  453. web_client.do_notify(_("Unsubscribed"), msg);
  454. }
  455. } else if (data.type === 'toggle_star') {
  456. chat_manager.on_toggle_star_notification(data);
  457. } else if (data.type === 'mark_as_read') {
  458. chat_manager.on_mark_as_read_notification(data);
  459. } else if (data.type === 'mark_as_unread') {
  460. chat_manager.on_mark_as_unread_notification(data);
  461. } else if (data.info === 'channel_seen') {
  462. chat_manager.on_channel_seen_notification(data);
  463. } else if (data.info === 'transient_message') {
  464. chat_manager.on_transient_message_notification(data);
  465. } else if (data.type === 'activity_updated') {
  466. chat_manager.onActivityUpdateNodification(data);
  467. } else {
  468. chat_manager.on_chat_session_notification(data);
  469. }
  470. }
  471. chat_manager.on_toggle_star_notification = function (data) {
  472. _.each(data.message_ids, function (msg_id) {
  473. var message = _.findWhere(messages, { id: msg_id });
  474. if (message) {
  475. chat_manager.invalidate_caches(message.channel_ids);
  476. message.is_starred = data.starred;
  477. if (!message.is_starred) {
  478. chat_manager.remove_message_from_channel("channel_starred", message);
  479. starred_counter--;
  480. } else {
  481. chat_manager.add_to_cache(message, []);
  482. var channel_starred = chat_manager.get_channel('channel_starred');
  483. channel_starred.cache = _.pick(channel_starred.cache, "[]");
  484. starred_counter++;
  485. }
  486. chat_manager.bus.trigger('update_message', message);
  487. }
  488. });
  489. chat_manager.bus.trigger('update_starred', starred_counter);
  490. }
  491. chat_manager.on_mark_as_read_notification = function (data) {
  492. _.each(data.message_ids, function (msg_id) {
  493. var message = _.findWhere(messages, { id: msg_id });
  494. if (message) {
  495. chat_manager.invalidate_caches(message.channel_ids);
  496. chat_manager.remove_message_from_channel("channel_inbox", message);
  497. chat_manager.bus.trigger('update_message', message, data.type);
  498. }
  499. });
  500. if (data.channel_ids) {
  501. _.each(data.channel_ids, function (channel_id) {
  502. var channel = chat_manager.get_channel(channel_id);
  503. if (channel) {
  504. channel.needaction_counter = Math.max(channel.needaction_counter - data.message_ids.length, 0);
  505. }
  506. });
  507. } else { // if no channel_ids specified, this is a 'mark all read' in the inbox
  508. _.each(channels, function (channel) {
  509. channel.needaction_counter = 0;
  510. });
  511. }
  512. needaction_counter = Math.max(needaction_counter - data.message_ids.length, 0);
  513. chat_manager.bus.trigger('update_needaction', needaction_counter);
  514. }
  515. chat_manager.on_mark_as_unread_notification = function (data) {
  516. _.each(data.message_ids, function (message_id) {
  517. var message = _.findWhere(messages, { id: message_id });
  518. if (message) {
  519. chat_manager.invalidate_caches(message.channel_ids);
  520. chat_manager.add_channel_to_message(message, 'channel_inbox');
  521. chat_manager.add_to_cache(message, []);
  522. }
  523. });
  524. var channel_inbox = chat_manager.get_channel('channel_inbox');
  525. channel_inbox.cache = _.pick(channel_inbox.cache, "[]");
  526. _.each(data.channel_ids, function (channel_id) {
  527. var channel = chat_manager.get_channel(channel_id);
  528. if (channel) {
  529. channel.needaction_counter += data.message_ids.length;
  530. }
  531. });
  532. needaction_counter += data.message_ids.length;
  533. chat_manager.bus.trigger('update_needaction', needaction_counter);
  534. }
  535. chat_manager.on_channel_seen_notification = function (data) {
  536. var channel = chat_manager.get_channel(data.id);
  537. if (channel) {
  538. channel.last_seen_message_id = data.last_message_id;
  539. if (channel.unread_counter) {
  540. chat_manager.update_channel_unread_counter(channel, 0);
  541. }
  542. }
  543. }
  544. chat_manager.on_chat_session_notification = function (chat_session) {
  545. var channel;
  546. if ((chat_session.channel_type === "channel") && (chat_session.state === "open")) {
  547. chat_manager.add_channel(chat_session, {autoswitch: false});
  548. if (!chat_session.is_minimized && chat_session.info !== 'creation') {
  549. web_client.do_notify(_t("Invitation"), _t("You have been invited to: ") + chat_session.name);
  550. }
  551. }
  552. // partner specific change (open a detached window for example)
  553. if ((chat_session.state === "open") || (chat_session.state === "folded")) {
  554. channel = chat_session.is_minimized && chat_manager.get_channel(chat_session.id);
  555. if (channel) {
  556. channel.is_detached = true;
  557. channel.is_folded = (chat_session.state === "folded");
  558. chat_manager.bus.trigger("open_chat", channel);
  559. }
  560. } else if (chat_session.state === "closed") {
  561. channel = chat_manager.get_channel(chat_session.id);
  562. if (channel) {
  563. channel.is_detached = false;
  564. chat_manager.bus.trigger("close_chat", channel, {keep_open_if_unread: true});
  565. }
  566. }
  567. }
  568. chat_manager.on_presence_notification = function (data) {
  569. var dm = chat_manager.get_dm_from_partner_id(data.id);
  570. if (dm) {
  571. dm.status = data.im_status;
  572. chat_manager.bus.trigger('update_dm_presence', dm);
  573. }
  574. }
  575. chat_manager.on_transient_message_notification = function(data) {
  576. var last_message = _.last(messages);
  577. data.id = (last_message ? last_message.id : 0) + 0.01;
  578. data.author_id = data.author_id || ODOOBOT_ID;
  579. chat_manager.add_message(data);
  580. }
  581. chat_manager.onActivityUpdateNodification = function (data) {
  582. chat_manager.bus.trigger('activity_updated', data);
  583. }
  584. // Public interface
  585. //----------------------------------------------------------------------------------
  586. chat_manager.init = function (parent) {
  587. var self = this;
  588. Mixins.EventDispatcherMixin.init.call(this);
  589. this.setParent(parent);
  590. this.bus = new Bus();
  591. this.bus.on('client_action_open', null, function (open) {
  592. client_action_open = open;
  593. });
  594. bus.on('notification', null, chat_manager.on_notification);
  595. this.channel_seen = _.throttle(function (channel) {
  596. return self._rpc({
  597. model: 'mail.channel',
  598. method: 'channel_seen',
  599. args: [[channel.id]],
  600. }, {
  601. shadow: true
  602. });
  603. }, 3000);
  604. }
  605. chat_manager.start = function () {
  606. this.is_ready = session.is_bound.then(function(){
  607. var context = _.extend({isMobile: config.device.isMobile}, session.user_context);
  608. return session.rpc('/mail/client_action', {context: context});
  609. }).then(chat_manager._onMailClientAction.bind(this));
  610. chat_manager.add_channel({
  611. id: "channel_inbox",
  612. name: _lt("Inbox"),
  613. type: "static",
  614. }, { display_needactions: true });
  615. chat_manager.add_channel({
  616. id: "channel_starred",
  617. name: _lt("Starred"),
  618. type: "static"
  619. });
  620. },
  621. chat_manager._onMailClientAction = function (result) {
  622. _.each(result.channel_slots, function (channels) {
  623. _.each(channels, chat_manager.add_channel);
  624. });
  625. needaction_counter = result.needaction_inbox_counter;
  626. starred_counter = result.starred_counter;
  627. commands = _.map(result.commands, function (command) {
  628. return _.extend({ id: command.name }, command);
  629. });
  630. mention_partner_suggestions = result.mention_partner_suggestions;
  631. discuss_menu_id = result.menu_id;
  632. // Shortcodes: canned responses and emojis
  633. _.each(result.shortcodes, function (s) {
  634. if (s.shortcode_type === 'text') {
  635. canned_responses.push(_.pick(s, ['id', 'source', 'substitution']));
  636. } else {
  637. emojis.push(_.pick(s, ['id', 'source', 'unicode_source', 'substitution', 'description']));
  638. emoji_substitutions[_.escape(s.source)] = s.substitution;
  639. if (s.unicode_source) {
  640. emoji_substitutions[_.escape(s.unicode_source)] = s.substitution;
  641. emoji_unicodes[_.escape(s.source)] = s.unicode_source;
  642. }
  643. }
  644. });
  645. bus.start_polling();
  646. }
  647. // options: domain, load_more
  648. chat_manager._fetchFromChannel = function (channel, options) {
  649. options = options || {};
  650. var domain =
  651. (channel.id === "channel_inbox") ? [['needaction', '=', true]] :
  652. (channel.id === "channel_starred") ? [['starred', '=', true]] :
  653. [['channel_ids', 'in', channel.id]];
  654. var cache = chat_manager.get_channel_cache(channel, options.domain);
  655. if (options.domain) {
  656. domain = domain.concat(options.domain || []);
  657. }
  658. if (options.load_more) {
  659. var min_message_id = cache.messages[0].id;
  660. domain = [['id', '<', min_message_id]].concat(domain);
  661. }
  662. return this._rpc({
  663. model: 'mail.message',
  664. method: 'message_fetch',
  665. args: [domain],
  666. kwargs: {limit: LIMIT, context: session.user_context},
  667. })
  668. .then(function (msgs) {
  669. if (!cache.all_history_loaded) {
  670. cache.all_history_loaded = msgs.length < LIMIT;
  671. }
  672. cache.loaded = true;
  673. _.each(msgs, function (msg) {
  674. chat_manager.add_message(msg, {channel_id: channel.id, silent: true, domain: options.domain});
  675. });
  676. var channel_cache = chat_manager.get_channel_cache(channel, options.domain || []);
  677. return channel_cache.messages;
  678. });
  679. }
  680. // options: force_fetch
  681. chat_manager._fetchDocumentMessages = function (ids, options) {
  682. var loaded_msgs = _.filter(messages, function (message) {
  683. return _.contains(ids, message.id);
  684. });
  685. var loaded_msg_ids = _.pluck(loaded_msgs, 'id');
  686. options = options || {};
  687. if (options.force_fetch || _.difference(ids.slice(0, LIMIT), loaded_msg_ids).length) {
  688. var ids_to_load = _.difference(ids, loaded_msg_ids).slice(0, LIMIT);
  689. return this._rpc({
  690. model: 'mail.message',
  691. method: 'message_format',
  692. args: [ids_to_load],
  693. context: session.user_context,
  694. })
  695. .then(function (msgs) {
  696. var processed_msgs = [];
  697. _.each(msgs, function (msg) {
  698. processed_msgs.push(chat_manager.add_message(msg, {silent: true}));
  699. });
  700. return _.sortBy(loaded_msgs.concat(processed_msgs), function (msg) {
  701. return msg.id;
  702. });
  703. });
  704. } else {
  705. return $.when(loaded_msgs);
  706. }
  707. },
  708. chat_manager.post_message = function (data, options) {
  709. var self = this;
  710. options = options || {};
  711. // This message will be received from the mail composer as html content subtype
  712. // but the urls will not be linkified. If the mail composer takes the responsibility
  713. // to linkify the urls we end up with double linkification a bit everywhere.
  714. // Ideally we want to keep the content as text internally and only make html
  715. // enrichment at display time but the current design makes this quite hard to do.
  716. var body = utils.parse_and_transform(_.str.trim(data.content), utils.add_link);
  717. var msg = {
  718. partner_ids: data.partner_ids,
  719. body: body,
  720. attachment_ids: data.attachment_ids,
  721. };
  722. // Replace emojis by their unicode character
  723. _.each(_.keys(emoji_unicodes), function (key) {
  724. var escaped_key = String(key).replace(/([.*+?=^!:${}()|[\]\/\\])/g, '\\$1');
  725. var regexp = new RegExp("(\\s|^)(" + escaped_key + ")(?=\\s|$)", "g");
  726. msg.body = msg.body.replace(regexp, "$1" + emoji_unicodes[key]);
  727. });
  728. if ('subject' in data) {
  729. msg.subject = data.subject;
  730. }
  731. if ('channel_id' in options) {
  732. // post a message in a channel or execute a command
  733. return this._rpc({
  734. model: 'mail.channel',
  735. method: data.command ? 'execute_command' : 'message_post',
  736. args: [options.channel_id],
  737. kwargs: _.extend(msg, {
  738. message_type: 'comment',
  739. content_subtype: 'html',
  740. subtype: 'mail.mt_comment',
  741. command: data.command,
  742. }),
  743. });
  744. }
  745. if ('model' in options && 'res_id' in options) {
  746. // post a message in a chatter
  747. _.extend(msg, {
  748. content_subtype: data.content_subtype,
  749. context: data.context,
  750. message_type: data.message_type,
  751. subtype: data.subtype,
  752. subtype_id: data.subtype_id,
  753. });
  754. return this._rpc({
  755. model: options.model,
  756. method: 'message_post',
  757. args: [options.res_id],
  758. kwargs: msg,
  759. })
  760. .then(function (msg_id) {
  761. return self._rpc({
  762. model: 'mail.message',
  763. method: 'message_format',
  764. args: [msg_id],
  765. })
  766. .then(function (msgs) {
  767. msgs[0].model = options.model;
  768. msgs[0].res_id = options.res_id;
  769. chat_manager.add_message(msgs[0]);
  770. });
  771. });
  772. }
  773. }
  774. chat_manager.get_message = function (id) {
  775. return _.findWhere(messages, {id: id});
  776. }
  777. chat_manager.get_messages = function (options) {
  778. var channel;
  779. if ('channel_id' in options && options.load_more) {
  780. // get channel messages, force load_more
  781. channel = this.get_channel(options.channel_id);
  782. return this._fetchFromChannel(channel, {domain: options.domain || {}, load_more: true});
  783. }
  784. if ('channel_id' in options) {
  785. // channel message, check in cache first
  786. channel = this.get_channel(options.channel_id);
  787. var channel_cache = chat_manager.get_channel_cache(channel, options.domain);
  788. if (channel_cache.loaded) {
  789. return $.when(channel_cache.messages);
  790. } else {
  791. return this._fetchFromChannel(channel, {domain: options.domain});
  792. }
  793. }
  794. if ('ids' in options) {
  795. // get messages from their ids (chatter is the main use case)
  796. return this._fetchDocumentMessages(options.ids, options).then(function(result) {
  797. chat_manager.mark_as_read(options.ids);
  798. return result;
  799. });
  800. }
  801. if ('model' in options && 'res_id' in options) {
  802. // get messages for a chatter, when it doesn't know the ids (use
  803. // case is when using the full composer)
  804. var domain = [['model', '=', options.model], ['res_id', '=', options.res_id]];
  805. this._rpc({
  806. model: 'mail.message',
  807. method: 'message_fetch',
  808. args: [domain],
  809. kwargs: {limit: 30},
  810. })
  811. .then(function (msgs) {
  812. return _.map(msgs, chat_manager.add_message);
  813. });
  814. }
  815. }
  816. chat_manager.toggle_star_status = function (message_id) {
  817. return this._rpc({
  818. model: 'mail.message',
  819. method: 'toggle_message_starred',
  820. args: [[message_id]],
  821. });
  822. }
  823. chat_manager.unstar_all = function () {
  824. return this._rpc({
  825. model: 'mail.message',
  826. method: 'unstar_all',
  827. args: [[]]
  828. });
  829. }
  830. chat_manager.mark_as_read = function (message_ids) {
  831. var ids = _.filter(message_ids, function (id) {
  832. var message = _.findWhere(messages, {id: id});
  833. // If too many messages, not all are fetched, and some might not be found
  834. return !message || message.is_needaction;
  835. });
  836. if (ids.length) {
  837. return this._rpc({
  838. model: 'mail.message',
  839. method: 'set_message_done',
  840. args: [ids],
  841. });
  842. } else {
  843. return $.when();
  844. }
  845. }
  846. chat_manager.mark_all_as_read = function (channel, domain) {
  847. if ((channel.id === "channel_inbox" && needaction_counter) || (channel && channel.needaction_counter)) {
  848. return this._rpc({
  849. model: 'mail.message',
  850. method: 'mark_all_as_read',
  851. kwargs: {channel_ids: channel.id !== "channel_inbox" ? [channel.id] : [], domain: domain},
  852. });
  853. }
  854. return $.when();
  855. }
  856. chat_manager.undo_mark_as_read = function (message_ids, channel) {
  857. return this._rpc({
  858. model: 'mail.message',
  859. method: 'mark_as_unread',
  860. args: [message_ids, [channel.id]],
  861. });
  862. }
  863. chat_manager.mark_channel_as_seen = function (channel) {
  864. if (channel.unread_counter > 0 && channel.type !== 'static') {
  865. chat_manager.update_channel_unread_counter(channel, 0);
  866. this.channel_seen(channel);
  867. }
  868. }
  869. chat_manager.get_channels = function () {
  870. return _.clone(channels);
  871. }
  872. chat_manager.get_channel = function (id) {
  873. return _.findWhere(channels, {id: id});
  874. }
  875. chat_manager.get_dm_from_partner_id = function (partner_id) {
  876. return _.findWhere(channels, {direct_partner_id: partner_id});
  877. }
  878. chat_manager.all_history_loaded = function (channel, domain) {
  879. return chat_manager.get_channel_cache(channel, domain).all_history_loaded;
  880. }
  881. chat_manager.get_mention_partner_suggestions = function (channel) {
  882. if (!channel) {
  883. return mention_partner_suggestions;
  884. }
  885. if (!channel.members_deferred) {
  886. channel.members_deferred = this._rpc({
  887. model: 'mail.channel',
  888. method: 'channel_fetch_listeners',
  889. args: [channel.uuid],
  890. }, {
  891. shadow: true
  892. })
  893. .then(function (members) {
  894. var suggestions = [];
  895. _.each(mention_partner_suggestions, function (partners) {
  896. suggestions.push(_.filter(partners, function (partner) {
  897. return !_.findWhere(members, { id: partner.id });
  898. }));
  899. });
  900. return [members];
  901. });
  902. }
  903. return channel.members_deferred;
  904. }
  905. chat_manager.get_commands = function (channel) {
  906. return _.filter(commands, function (command) {
  907. return !command.channel_types || _.contains(command.channel_types, channel.server_type);
  908. });
  909. }
  910. chat_manager.get_canned_responses = function () {
  911. return canned_responses;
  912. }
  913. chat_manager.get_emojis = function() {
  914. return emojis;
  915. }
  916. chat_manager.get_needaction_counter = function () {
  917. return needaction_counter;
  918. }
  919. chat_manager.get_starred_counter = function () {
  920. return starred_counter;
  921. }
  922. chat_manager.get_chat_unread_counter = function () {
  923. return chat_unread_counter;
  924. }
  925. chat_manager.get_unread_conversation_counter = function () {
  926. return unread_conversation_counter;
  927. }
  928. chat_manager.get_last_seen_message = function (channel) {
  929. if (channel.last_seen_message_id) {
  930. var messages = channel.cache['[]'].messages;
  931. var msg = _.findWhere(messages, {id: channel.last_seen_message_id});
  932. if (msg) {
  933. var i = _.sortedIndex(messages, msg, 'id') + 1;
  934. while (i < messages.length && (messages[i].is_author || messages[i].is_system_notification)) {
  935. msg = messages[i];
  936. i++;
  937. }
  938. return msg;
  939. }
  940. }
  941. }
  942. chat_manager.get_discuss_menu_id = function () {
  943. return discuss_menu_id;
  944. }
  945. chat_manager.detach_channel = function (channel) {
  946. return this._rpc({
  947. model: 'mail.channel',
  948. method: 'channel_minimize',
  949. args: [channel.uuid, true],
  950. }, {
  951. shadow: true,
  952. });
  953. }
  954. chat_manager.remove_chatter_messages = function (model) {
  955. messages = _.reject(messages, function (message) {
  956. return message.channel_ids.length === 0 && message.model === model;
  957. });
  958. }
  959. chat_manager.create_channel = function (name, type) {
  960. var method = type === "dm" ? "channel_get" : "channel_create";
  961. var args = type === "dm" ? [[name]] : [name, type];
  962. var context = _.extend({isMobile: config.device.isMobile}, session.user_context);
  963. return this._rpc({
  964. model: 'mail.channel',
  965. method: method,
  966. args: args,
  967. kwargs: {context: context},
  968. })
  969. .then(chat_manager.add_channel);
  970. }
  971. chat_manager.join_channel = function (channel_id, options) {
  972. if (channel_id in channel_defs) {
  973. // prevents concurrent calls to channel_join_and_get_info
  974. return channel_defs[channel_id];
  975. }
  976. var channel = this.get_channel(channel_id);
  977. if (channel) {
  978. // channel already joined
  979. channel_defs[channel_id] = $.when(channel);
  980. } else {
  981. channel_defs[channel_id] = this._rpc({
  982. model: 'mail.channel',
  983. method: 'channel_join_and_get_info',
  984. args: [[channel_id]],
  985. })
  986. .then(function (result) {
  987. return chat_manager.add_channel(result, options);
  988. });
  989. }
  990. return channel_defs[channel_id];
  991. }
  992. chat_manager.open_and_detach_dm = function (partner_id) {
  993. return this._rpc({
  994. model: 'mail.channel',
  995. method: 'channel_get_and_minimize',
  996. args: [[partner_id]],
  997. })
  998. .then(chat_manager.add_channel);
  999. }
  1000. chat_manager.open_channel = function (channel) {
  1001. chat_manager.bus.trigger(client_action_open ? 'open_channel' : 'detach_channel', channel);
  1002. }
  1003. chat_manager.unsubscribe = function (channel) {
  1004. if (_.contains(['public', 'private'], channel.type)) {
  1005. return this._rpc({
  1006. model: 'mail.channel',
  1007. method: 'action_unfollow',
  1008. args: [[channel.id]],
  1009. });
  1010. } else {
  1011. return this._rpc({
  1012. model: 'mail.channel',
  1013. method: 'channel_pin',
  1014. args: [channel.uuid, false],
  1015. });
  1016. }
  1017. }
  1018. chat_manager.close_chat_session = function (channel_id) {
  1019. var channel = this.get_channel(channel_id);
  1020. this._rpc({
  1021. model: 'mail.channel',
  1022. method: 'channel_fold',
  1023. kwargs: {uuid : channel.uuid, state : 'closed'},
  1024. }, {shadow: true});
  1025. }
  1026. chat_manager.fold_channel = function (channel_id, folded) {
  1027. var args = {
  1028. uuid: this.get_channel(channel_id).uuid,
  1029. };
  1030. if (_.isBoolean(folded)) {
  1031. args.state = folded ? 'folded' : 'open';
  1032. }
  1033. return this._rpc({
  1034. model: 'mail.channel',
  1035. method: 'channel_fold',
  1036. kwargs: args,
  1037. }, {shadow: true});
  1038. }
  1039. /**
  1040. * Special redirection handling for given model and id
  1041. *
  1042. * If the model is res.partner, and there is a user associated with this
  1043. * partner which isn't the current user, open the DM with this user.
  1044. * Otherwhise, open the record's form view, if this is not the current user's.
  1045. */
  1046. chat_manager.redirect = function (res_model, res_id, dm_redirection_callback) {
  1047. var self = this;
  1048. var redirect_to_document = function (res_model, res_id, view_id) {
  1049. web_client.do_action({
  1050. type:'ir.actions.act_window',
  1051. view_type: 'form',
  1052. view_mode: 'form',
  1053. res_model: res_model,
  1054. views: [[view_id || false, 'form']],
  1055. res_id: res_id,
  1056. });
  1057. };
  1058. if (res_model === "res.partner") {
  1059. var domain = [["partner_id", "=", res_id]];
  1060. this._rpc({
  1061. model: 'res.users',
  1062. method: 'search',
  1063. args: [domain],
  1064. })
  1065. .then(function (user_ids) {
  1066. if (user_ids.length && user_ids[0] !== session.uid && dm_redirection_callback) {
  1067. self.create_channel(res_id, 'dm').then(dm_redirection_callback);
  1068. } else {
  1069. redirect_to_document(res_model, res_id);
  1070. }
  1071. });
  1072. } else {
  1073. this._rpc({
  1074. model: res_model,
  1075. method: 'get_formview_id',
  1076. args: [[res_id], session.user_context],
  1077. })
  1078. .then(function (view_id) {
  1079. redirect_to_document(res_model, res_id, view_id);
  1080. });
  1081. }
  1082. }
  1083. chat_manager.get_channels_preview = function (channels) {
  1084. var channels_preview = _.map(channels, function (channel) {
  1085. var info;
  1086. if (channel.channel_ids && _.contains(channel.channel_ids,"channel_inbox")) {
  1087. // map inbox(mail_message) data with existing channel/chat template
  1088. info = _.pick(channel, 'id', 'body', 'avatar_src', 'res_id', 'model', 'module_icon', 'subject','date', 'record_name', 'status', 'displayed_author', 'email_from', 'unread_counter');
  1089. info.last_message = {
  1090. body: info.body,
  1091. date: info.date,
  1092. displayed_author: info.displayed_author || info.email_from,
  1093. };
  1094. info.name = info.record_name || info.subject || info.displayed_author;
  1095. info.image_src = info.module_icon || info.avatar_src;
  1096. info.message_id = info.id;
  1097. info.id = 'channel_inbox';
  1098. return info;
  1099. }
  1100. info = _.pick(channel, 'id', 'is_chat', 'name', 'status', 'unread_counter');
  1101. info.last_message = channel.last_message || _.last(channel.cache['[]'].messages);
  1102. if (!info.is_chat) {
  1103. info.image_src = '/web/image/mail.channel/'+channel.id+'/image_small';
  1104. } else if (channel.direct_partner_id) {
  1105. info.image_src = '/web/image/res.partner/'+channel.direct_partner_id+'/image_small';
  1106. } else {
  1107. info.image_src = '/mail/static/src/img/smiley/avatar.jpg';
  1108. }
  1109. return info;
  1110. });
  1111. var missing_channels = _.where(channels_preview, {last_message: undefined});
  1112. if (!channels_preview_def) {
  1113. if (missing_channels.length) {
  1114. var missing_channel_ids = _.pluck(missing_channels, 'id');
  1115. channels_preview_def = this._rpc({
  1116. model: 'mail.channel',
  1117. method: 'channel_fetch_preview',
  1118. args: [missing_channel_ids],
  1119. }, {
  1120. shadow: true,
  1121. });
  1122. } else {
  1123. channels_preview_def = $.when();
  1124. }
  1125. }
  1126. return channels_preview_def.then(function (channels) {
  1127. _.each(missing_channels, function (channel_preview) {
  1128. var channel = _.findWhere(channels, {id: channel_preview.id});
  1129. if (channel) {
  1130. channel_preview.last_message = chat_manager.add_message(channel.last_message);
  1131. }
  1132. });
  1133. // sort channels: 1. unread, 2. chat, 3. date of last msg
  1134. channels_preview.sort(function (c1, c2) {
  1135. return Math.min(1, c2.unread_counter) - Math.min(1, c1.unread_counter) ||
  1136. c2.is_chat - c1.is_chat ||
  1137. !!c2.last_message - !!c1.last_message ||
  1138. (c2.last_message && c2.last_message.date.diff(c1.last_message.date));
  1139. });
  1140. // generate last message preview (inline message body and compute date to display)
  1141. _.each(channels_preview, function (channel) {
  1142. if (channel.last_message) {
  1143. channel.last_message_preview = chat_manager.get_message_body_preview(channel.last_message.body);
  1144. channel.last_message_date = channel.last_message.date.fromNow();
  1145. }
  1146. });
  1147. return channels_preview;
  1148. });
  1149. },
  1150. chat_manager.get_message_body_preview = function (message_body) {
  1151. return utils.parse_and_transform(message_body, utils.inline);
  1152. }
  1153. chat_manager.search_partner = function (search_val, limit) {
  1154. var def = $.Deferred();
  1155. var values = [];
  1156. // search among prefetched partners
  1157. var search_regexp = new RegExp(_.str.escapeRegExp(utils.unaccent(search_val)), 'i');
  1158. _.each(mention_partner_suggestions, function (partners) {
  1159. if (values.length < limit) {
  1160. values = values.concat(_.filter(partners, function (partner) {
  1161. return session.partner_id !== partner.id && search_regexp.test(partner.name);
  1162. })).splice(0, limit);
  1163. }
  1164. });
  1165. if (!values.length) {
  1166. // extend the research to all users
  1167. def = this._rpc({
  1168. model: 'res.partner',
  1169. method: 'im_search',
  1170. args: [search_val, limit || 20],
  1171. }, {
  1172. shadow: true,
  1173. });
  1174. } else {
  1175. def = $.when(values);
  1176. }
  1177. return def.then(function (values) {
  1178. var autocomplete_data = _.map(values, function (value) {
  1179. return { id: value.id, value: value.name, label: value.name };
  1180. });
  1181. return _.sortBy(autocomplete_data, 'label');
  1182. });
  1183. }
  1184. chat_manager.start();
  1185. bus.off('notification');
  1186. bus.on('notification', null, function () {
  1187. chat_manager.on_notification.apply(chat_manager, arguments);
  1188. });
  1189. return {
  1190. ODOOBOT_ID: ODOOBOT_ID,
  1191. chat_manager: chat_manager,
  1192. };
  1193. });