diff --git a/addons/bus/controllers/main.py b/addons/bus/controllers/main.py index 3da386e80b2..050eb684625 100644 --- a/addons/bus/controllers/main.py +++ b/addons/bus/controllers/main.py @@ -1,7 +1,6 @@ # -*- coding: utf-8 -*- import openerp from openerp.http import request - from openerp.addons.bus.models.bus import dispatch @@ -35,4 +34,6 @@ class BusController(openerp.http.Controller): raise Exception("bus.Bus unavailable") if [c for c in channels if not isinstance(c, basestring)]: raise Exception("bus.Bus only string channels are allowed.") + if request.registry.in_test_mode(): + raise openerp.exceptions.UserError("bus.Bus not available in test mode") return self._poll(request.db, channels, last, options) diff --git a/addons/bus/static/src/js/bus.js b/addons/bus/static/src/js/bus.js index 3c828668308..b89f66a7c33 100644 --- a/addons/bus/static/src/js/bus.js +++ b/addons/bus/static/src/js/bus.js @@ -1,4 +1,5 @@ odoo.define('bus.bus', function (require) { +"use strict"; var session = require('web.session'); var Widget = require('web.Widget'); @@ -40,11 +41,15 @@ bus.Bus = Widget.extend({ var self = this; self.activated = true; var data = {'channels': self.channels, 'last': self.last, 'options' : self.options}; - session.rpc('/longpolling/poll', data, {shadow : true}).then(function(result) { + this.current_poll = $.Deferred(); + this.current_poll.then(function (result) { _.each(result, _.bind(self.on_notification, self)); if(!self.stop){ self.poll(); } + }); + session.rpc('/longpolling/poll', data, {shadow : true}).then(function(result) { + self.current_poll.resolve(result); }, function(unused, e) { // no error popup if request is interrupted or fails for any reason e.preventDefault(); @@ -58,6 +63,12 @@ bus.Bus = Widget.extend({ } this.trigger("notification", [notification.channel, notification.message]); }, + restart_poll: function () { + if (this.current_poll) { + this.current_poll.fail(); + } + this.poll(); + }, add_channel: function(channel){ if(!_.contains(this.channels, channel)){ this.channels.push(channel); diff --git a/addons/im_livechat/static/src/js/im_livechat.js b/addons/im_livechat/static/src/js/im_livechat.js index 7ff644f0e0a..40a712e68df 100644 --- a/addons/im_livechat/static/src/js/im_livechat.js +++ b/addons/im_livechat/static/src/js/im_livechat.js @@ -7,11 +7,14 @@ var user_session = require('web.session'); var time = require('web.time'); var utils = require('web.utils'); var Widget = require('web.Widget'); -var mail_chat_common = require('mail.chat_common'); +// var mail_chat_common = require('mail.chat_common'); var _t = core._t; var QWeb = core.qweb; +return { + LivechatButton: Widget, +}; /* * Conversation Patch diff --git a/addons/im_livechat/static/src/js/im_livechat_backend.js b/addons/im_livechat/static/src/js/im_livechat_backend.js index bc970d37e06..646d1b8c7df 100644 --- a/addons/im_livechat/static/src/js/im_livechat_backend.js +++ b/addons/im_livechat/static/src/js/im_livechat_backend.js @@ -1,7 +1,8 @@ odoo.define('im_livechat.chat_backend', function (require) { "use strict"; -var mail_chat_backend = require('mail.chat_backend'); +// var mail_chat_backend = require('mail.chat_backend'); +return; /** diff --git a/addons/im_livechat/static/src/css/im_livechat.css b/addons/im_livechat/static/src/less/im_livechat.less similarity index 100% rename from addons/im_livechat/static/src/css/im_livechat.css rename to addons/im_livechat/static/src/less/im_livechat.less diff --git a/addons/im_livechat/static/src/xml/im_livechat_backend.xml b/addons/im_livechat/static/src/xml/im_livechat_backend.xml index 456e293a093..e371a0b9723 100644 --- a/addons/im_livechat/static/src/xml/im_livechat_backend.xml +++ b/addons/im_livechat/static/src/xml/im_livechat_backend.xml @@ -1,7 +1,7 @@ diff --git a/addons/im_livechat/views/im_livechat_channel_templates.xml b/addons/im_livechat/views/im_livechat_channel_templates.xml index e491635111b..19882bcb34e 100644 --- a/addons/im_livechat/views/im_livechat_channel_templates.xml +++ b/addons/im_livechat/views/im_livechat_channel_templates.xml @@ -125,11 +125,12 @@ - - + + - - + + + diff --git a/addons/mail/__openerp__.py b/addons/mail/__openerp__.py index 58021bb2c9f..aec922b2b4d 100644 --- a/addons/mail/__openerp__.py +++ b/addons/mail/__openerp__.py @@ -58,11 +58,12 @@ Main Features 'installable': True, 'application': True, 'qweb': [ - 'static/src/xml/mail_thread.xml', - 'static/src/xml/mail_chatter.xml', - 'static/src/xml/mail_chat_common.xml', - 'static/src/xml/mail_chat_backend.xml', - 'static/src/xml/mail_followers.xml', + 'static/src/xml/client_action.xml', + 'static/src/xml/composer.xml', + 'static/src/xml/chatter.xml', + 'static/src/xml/systray.xml', + 'static/src/xml/thread.xml', + 'static/src/xml/chat_window.xml', 'static/src/xml/announcement.xml', ], } diff --git a/addons/mail/static/src/js/chat_manager.js b/addons/mail/static/src/js/chat_manager.js new file mode 100644 index 00000000000..a6eb4d7d264 --- /dev/null +++ b/addons/mail/static/src/js/chat_manager.js @@ -0,0 +1,448 @@ +odoo.define('mail.chat_manager', function (require) { +"use strict"; + +var bus = require('bus.bus').bus; +var core = require('web.core'); +var data = require('web.data'); +var Model = require('web.Model'); +var session = require('web.session'); +var web_client = require('web.web_client'); + +var _t = core._t; +var LIMIT = 20; + +var MessageModel = new Model('mail.message', session.context); +var ChannelModel = new Model('mail.channel', session.context); + +// Private model +//---------------------------------------------------------------------------------- +var messages = []; +var channels = []; +var emojis = []; +var emoji_substitutions = {}; +var needaction_counter = 0; + +// 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 = make_message(data, options.channel_id); + // Keep the array ordered by date when inserting the new message + messages.splice(_.sortedIndex(messages, msg, 'date'), 0, msg); + if (options.channel_id) { + var channel = _.findWhere(channels, {id: options.channel_id}); + if (channel.hidden) { + channel.hidden = false; + chat_manager.bus.trigger('new_channel', channel); + } + if ((channel.type === "dm") && (options.show_notification)) { + var query = { is_displayed: false }; + chat_manager.bus.trigger('anyone_listening', channel, query); + if (!query.is_displayed) { + web_client.do_notify(_t('New message'), _t("You received a message from ") + msg.author_id[1]); + } + } + } + if (!options.silent) { + chat_manager.bus.trigger('new_message', msg); + } + } else if (msg && options.channel_id !== undefined) { + add_channel_to_message(msg, options.channel_id); + } + return msg; +} + +function make_message (data, channel_id) { + var msg = { + id: data.id, + author_id: data.author_id, + body: data.body, + date: data.date, + message_type: data.message_type, + is_note: data.is_note, + attachment_ids: data.attachment_ids, + subject: data.subject, + email_from: data.email_from, + record_name: data.record_name, + tracking_value_ids: data.tracking_value_ids, + channel_ids: (channel_id !== undefined ? [channel_id] : []), + model: data.model, + res_id: data.res_id, + }; + + _.each(_.keys(emoji_substitutions), function(key){ + var escaped_key = String(key).replace(/([.*+?=^!:${}()|[\]\/\\])/g, '\\$1'); //] + var regexp = new RegExp("(?:^|\\s|<[a-z]*>)(" + escaped_key + ")(?:\\s|$|)"); + msg.body = msg.body.replace(regexp, ' '+emoji_substitutions[key]+' '); + }); + + 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; + } + return msg; +} + +function add_channel_to_message (message, channel_id) { + message.channel_ids.push(channel_id); + message.channel_ids = _.uniq(message.channel_ids); +} + +function post_channel_message (data) { + return ChannelModel.call('message_post', [data.channel_id], { + message_type: 'comment', + content_subtype: 'html', + partner_ids: data.partner_ids, + body: data.content, + subtype: 'mail.mt_comment', + attachment_ids: data.attachment_ids, + }); +} + +function post_document_message (model_name, res_id, data) { + var values = { + attachment_ids: data.attachment_ids, + body: data.content, + content_subtype: data.content_subtype, + message_type: data.message_type, + partner_ids: data.partner_ids, + subtype: data.subtype, + subtype_id: data.subtype_id, + }; + + var model = new Model(model_name); + return model.call('message_post', [res_id], values).then(function (msg_id) { + return MessageModel.call('message_format', [msg_id]).then(function (msgs) { + msgs[0].model = model_name; + msgs[0].res_id = res_id; + add_message(msgs[0]); + }); + }); +} + +function add_channel (data, options) { + options = typeof options === "object" ? options : {}; + var channel = _.findWhere(channels, {id: 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 = make_channel(data, options); + channels.push(channel); + if (!options.silent) { + chat_manager.bus.trigger("new_channel", channel); + } + if (channel.is_detached) { + chat_manager.bus.trigger("open_chat", channel); + } + } +} + +function make_channel (data, options) { + var channel = { + id: data.id, + name: data.name, + type: data.type || "public", + all_history_loaded: false, + uuid: data.uuid, + is_detached: data.is_minimized, + is_folded: data.state === "folded", + loaded: false, + autoswitch: 'autoswitch' in options ? options.autoswitch : true, + hidden: options.hidden, + }; + if (data.public === "private") { + channel.type = "private"; + } + if ('direct_partner' in data) { + channel.type = "dm"; + channel.name = data.direct_partner[0].name; + channel.status = data.direct_partner[0].im_status; + } + return channel; +} + +// options: domain, load_more +function fetch_from_channel (channel_id, options) { + var domain = + (channel_id === "channel_inbox") ? [['needaction', '=', true]] : + (channel_id === "channel_starred") ? [['starred', '=', true]] : + [['channel_ids', 'in', channel_id]]; + + options = options || {}; + if (options.domain) { + domain = new data.CompoundDomain(domain, options.domain || []); + } + if (options.load_more) { + var min_message_id = _.chain(messages) + .filter(function (msg) { return _.contains(msg.channel_ids, channel_id); }) + .pluck("id") + .min() + .value(); + + domain = new data.CompoundDomain([['id', '<', min_message_id]], domain); + } + + return MessageModel.call('message_fetch', [domain], {limit: LIMIT}).then(function (msgs) { + var channel = _.findWhere(channels, {id: channel_id}); + channel.loaded = true; + if (!channel.all_history_loaded) { + channel.all_history_loaded = msgs.length < LIMIT; + } + + _.each(msgs, function (msg) { + add_message(msg, {channel_id: channel_id, silent: true}); + }); + return _.filter(messages, function (m) { + return _.contains(m.channel_ids, channel_id); + }); + }); +} + +// options: force_fetch +function fetch_document_messages (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 MessageModel.call('message_format', [ids_to_load]).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.date; + }); + }); + } else { + return $.when(loaded_msgs); + } +} + +// Public interface +//---------------------------------------------------------------------------------- +var chat_manager = { + post_message: post_channel_message, + post_message_in_document: post_document_message, + + get_messages: function (options) { + if ('channel_id' in options) { // channel message + var channel = _.findWhere(channels, {id: options.channel_id}); + if (channel.loaded) { + return $.when(_.filter(messages, function (message) { + return _.contains(message.channel_ids, channel.id); + })); + } else { + return fetch_from_channel(options.channel_id); + } + } else { // chatter message + } + }, + fetch: function (channel_id, domain) { + return fetch_from_channel(channel_id, {domain: domain}); + }, + fetch_more: function (channel_id, domain) { + return fetch_from_channel(channel_id, {domain: domain, load_more: true}); + }, + /** + * Fetches chatter messages from their ids + */ + fetch_messages: function (message_ids, options) { + return fetch_document_messages(message_ids, options); + }, + toggle_star_status: function (message_id) { + var msg = _.findWhere(messages, { id: message_id }); + + return MessageModel.call('set_message_starred', [[message_id], !msg.is_starred]).then(function () { + msg.is_starred = !msg.is_starred; + chat_manager.bus.trigger('update_message', msg); + }); + }, + mark_as_read: function (message_id) { + return MessageModel.call('set_message_done', [[message_id]]).then(function () { + var message = _.findWhere(messages, { id: message_id }); + message.channel_ids = _.without(message.channel_ids, "channel_inbox"); + chat_manager.bus.trigger('update_message', message); + needaction_counter = needaction_counter - 1; + chat_manager.bus.trigger('update_needaction', needaction_counter); + }); + }, + + get_channels: function () { + return _.clone(channels); + }, + + get_channel: function (id) { + return _.findWhere(channels, {id: id}) || channels[0]; + }, + + all_history_loaded: function (channel_id) { + return this.get_channel(channel_id).all_history_loaded; + }, + + get_emojis: function() { + return emojis; + }, + + get_needaction_counter: function () { + return needaction_counter; + }, + + detach_channel: function (channel_id) { + var channel = this.get_channel(channel_id); + return ChannelModel.call("channel_minimize", [channel.uuid, true]); + }, + remove_chatter_messages: function (model) { + messages = _.reject(messages, function (message) { + return message.channel_ids.length === 0 && message.model === model; + }); + }, + bus: new core.Bus(), + + create_channel: function (name, type) { + var method = type === "dm" ? "channel_get" : "channel_create"; + var args = type === "dm" ? [[name]] : [name, type]; + + return ChannelModel + .call(method, args) + .then(add_channel) + .then(function () { + bus.restart_poll(); + }); + }, + join_channel: function (channel_id) { + return ChannelModel + .call('channel_join_and_get_info', [[channel_id]]) + .then(add_channel); + }, + + unsubscribe: function (channel_id) { + var def; + var channel = chat_manager.get_channel(channel_id); + if (channel.type === "dm") { + def = ChannelModel.call('channel_pin', [channel.uuid, false]); + } else { + def = ChannelModel.call('action_unfollow', [[channel_id]]); + } + return def.then(function () { + channels = _.without(channels, channel); + }); + }, + close_chat_session: function (channel_id) { + var channel = _.findWhere(channels, {id: channel_id}); + ChannelModel.call("channel_fold", [], {uuid : channel.uuid, state : "closed"}); + }, + fold_channel: function (channel_id) { + var channel = _.findWhere(channels, {id: channel_id}); + return ChannelModel.call("channel_fold", [], {uuid : channel.uuid}).then(function () { + channel.is_folded = !channel.is_folded; + }); + }, +}; + +// Initialization +// --------------------------------------------------------------------------------- +function init () { + add_channel({ + id: "channel_inbox", + name: _t("Inbox"), + type: "static" + }); + + add_channel({ + id: "channel_starred", + name: _t("Starred"), + type: "static" + }); + + var load_channels = session.rpc('/mail/client_action').then(function (result) { + _.each(result.channel_slots.channel_channel, add_channel); + _.each(result.channel_slots.channel_private_group, add_channel); + _.each(result.channel_slots.channel_direct_message, add_channel); + needaction_counter = result.needaction_inbox_counter; + }); + + var load_emojis = session.rpc("/mail/chat_init").then(function (result) { + emojis = result.emoji; + _.each(emojis, function(emoji) { + emoji_substitutions[emoji.source] = emoji.substitution; + }); + }); + + bus.on('notification', null, function (notification) { + var model = notification[0][1]; + if (model === 'mail.channel') { + // new message in a channel + var message = notification[1]; + var channel_id = message.channel_ids[0]; + message = add_message(message, { channel_id: channel_id, show_notification: true} ); + if (_.contains(message.channel_ids, 'channel_inbox')) { + needaction_counter = needaction_counter + 1; + chat_manager.bus.trigger('update_needaction', needaction_counter); + } + } + if (model === 'res.partner') { + var chat_session = notification[1]; + if ((chat_session.channel_type === "channel") && (chat_session.public === "private") && (chat_session.state === "open")) { + add_channel(chat_session, {autoswitch: false}); + if (!chat_session.is_minimized) { + web_client.do_notify(_t("Private Channel"), _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")) { + add_channel(chat_session, {autoswitch: false, silent: true, hidden: true}); + if (chat_session.is_minimized) { + chat_manager.bus.trigger("open_chat", chat_session); + } + } + if (chat_session.state === "closed") { + chat_manager.bus.trigger("close_chat", chat_session); + } + } + }); + + return $.when(load_channels, load_emojis).then(function () { + bus.start_polling(); + return chat_manager; + }); +} + +return init(); + +}); diff --git a/addons/mail/static/src/js/chat_window.js b/addons/mail/static/src/js/chat_window.js new file mode 100644 index 00000000000..c9ca3675054 --- /dev/null +++ b/addons/mail/static/src/js/chat_window.js @@ -0,0 +1,77 @@ +odoo.define('mail.ChatWindow', function (require) { +"use strict"; + +var ChatThread = require('mail.ChatThread'); + +var core = require('web.core'); +var Widget = require('web.Widget'); + +var _t = core._t; + +return Widget.extend({ + template: "mail.ChatWindow", + events: { + "keydown .o_chat_input": "on_keydown", + "click .o_chat_window_close": "on_click_close", + "click .o_chat_title": "on_click_fold", + }, + + init: function (parent, channel_id, title, is_folded) { + this._super(parent); + this.title = title; + this.channel_id = channel_id; + this.placeholder = _t("Say something"); + this.folded = is_folded; + }, + start: function () { + this.$content = this.$('.o_chat_content'); + this.$input = this.$('.o_chat_input input'); + + this.thread = new ChatThread(this, {display_avatar: false}); + this.thread.on('toggle_star_status', this, function (message_id) { + this.trigger('toggle_star_status', message_id); + }); + + this.fold(); + var def = this.thread.appendTo(this.$content); + return $.when(this._super(), def); + }, + render: function (messages) { + this.thread.render(messages, {display_load_more: false}); + }, + scrollBottom: function () { + this.$content.scrollTo('max'); + }, + fold: function () { + this.$el.animate({ + height: this.folded ? "28px" : "333px" + }); + }, + toggle_fold: function () { + this.folded = !this.folded; + this.fold(); + }, + on_keydown: function (event) { + if (event.which === $.ui.keyCode.ENTER) { + var message = { + content: this.$input.val(), + attachment_ids: [], + partner_ids: [], + channel_id: this.channel_id, + }; + this.$input.val(''); + this.trigger('post_message', message); + } + }, + on_click_close: function (event) { + event.stopPropagation(); + event.preventDefault(); + this.trigger("close_chat_session"); + }, + on_click_fold: function () { + this.trigger("fold_channel", this.channel_id); + this.toggle_fold(); + }, +}); + +}); diff --git a/addons/mail/static/src/js/chatter.js b/addons/mail/static/src/js/chatter.js new file mode 100644 index 00000000000..3415789917d --- /dev/null +++ b/addons/mail/static/src/js/chatter.js @@ -0,0 +1,897 @@ +odoo.define('mail.Chatter', function (require) { +"use strict"; + +var chat_manager = require('mail.chat_manager'); +var ChatComposer = require('mail.ChatComposer'); +var ChatThread = require('mail.ChatThread'); + +var ajax = require('web.ajax'); +var core = require('web.core'); +var data = require('web.data'); +var Dialog = require('web.Dialog'); +var form_common = require('web.form_common'); +var session = require('web.session'); +var web_client = require('web.web_client'); + +var _t = core._t; +var qweb = core.qweb; + +// ----------------------------------------------------------------------------- +// Followers Widget ('mail_followers' widget) +// +// Since it is displayed on a form view, it extends 'AbstractField' widget. +// +// Note: the followers widget is moved inside the chatter by the chatter itself +// for layout purposes. +// ----------------------------------------------------------------------------- +var Followers = form_common.AbstractField.extend({ + template: 'mail.Followers', + + init: function () { + this._super.apply(this, arguments); + + this.image = this.node.attrs.image || 'image_small'; + this.comment = this.node.attrs.help || false; + this.ds_model = new data.DataSetSearch(this, this.view.model); + this.ds_users = new data.DataSetSearch(this, 'res.users'); + + this.value = []; + this.followers = []; + this.data_subtype = {}; + + this.view_is_editable = this.__parentedParent.is_action_enabled('edit'); + }, + + start: function () { + // use actual_mode property on view to know if the view is in create mode anymore + this.view.on("change:actual_mode", this, this.on_check_visibility_mode); + this.on_check_visibility_mode(); + this.reinit(); + this.bind_events(); + return this._super(); + }, + + on_check_visibility_mode: function () { + this.set({"force_invisible": this.view.get("actual_mode") === "create"}); + }, + + set_value: function (_value) { + this.value = _value; + this._super(_value); + }, + + reinit: function () { + this.data_subtype = {}; + this.message_is_follower = undefined; + this.display_buttons(); + }, + + bind_events: function () { + var self = this; + + // event: click on '(Un)Follow' button, that toggles the follow for uid + this.$el.on('click', '.o_followers_follow_button', function () { + if ($(this).hasClass('o_followers_notfollow')) { + self.do_follow(); + } else { + self.do_unfollow({user_ids: [session.uid]}); + } + }); + + // event: click on a subtype, that (un)subscribe for this subtype + this.$el.on('click', '.o_subtypes_list input', function(event) { + event.stopPropagation(); + self.do_update_subscription(event); + var $list = self.$('.o_subtypes_list'); + if (!$list.hasClass('open')) { + $list.addClass('open'); + } + if (self.$('.o_subtypes_list ul')[0].children.length < 1) { + $list.removeClass('open'); + } + }); + + // event: click on 'invite' button, that opens the invite wizard + this.$el.on('click', '.o_add_follower', function(event) { + event.preventDefault(); + self.on_invite_follower(false); + }); + this.$el.on('click', '.o_add_follower_channel', function(event) { + event.preventDefault(); + self.on_invite_follower(true); + }); + + // event: click on 'edit_subtype(pencil)' button to edit subscription + this.$el.on('click', '.o_edit_subtype', self.on_edit_subtype); + this.$el.on('click', '.o_remove_follower', self.on_remove_follower); + this.$el.on('click', 'a[data-partner]', self.on_follower_clicked); + }, + + on_edit_subtype: function (event) { + var self = this; + var $currentTarget = $(event.currentTarget); + var follower_id = $currentTarget.data('oe-id'); + var is_channel = $currentTarget.data('oe-model') === 'mail.channel'; + self.dialog = new Dialog(this, { + size: 'small', + title: _t('Edit Subscription of ') + $currentTarget.siblings('a').text(), + buttons: [ + { + text: _t("Apply"), + classes: 'btn-primary', + click: function () { + self.do_update_subscription(event, follower_id, is_channel); + }, + close: true + }, + { + text: _t("Cancel"), + close: true, + }, + ], + }).open(); + return self.fetch_subtypes($currentTarget.data('id')); + }, + + on_invite_follower: function (channel_only) { + var self = this; + var action = { + type: 'ir.actions.act_window', + res_model: 'mail.wizard.invite', + view_mode: 'form', + view_type: 'form', + views: [[false, 'form']], + name: _t('Invite Follower'), + target: 'new', + context: { + 'default_res_model': this.view.dataset.model, + 'default_res_id': this.view.datarecord.id, + 'mail_invite_follower_channel_only': channel_only, + }, + }; + this.do_action(action, { + on_close: function () { + self.read_value(); + }, + }); + }, + + on_remove_follower: function (event) { + var res_model = $(event.target).parent().find('a').data('oe-model'); + var res_id = $(event.target).parent().find('a').data('oe-id'); + if (res_model === 'res.partner') { + return this.do_unfollow({partner_ids: [res_id]}); + } else { + return this.do_unfollow({channel_ids: [res_id]}); + } + }, + + on_follower_clicked: function (event) { + event.preventDefault(); + var partner_id = $(event.target).data('partner'); + var state = { + 'model': 'res.partner', + 'id': partner_id, + 'title': this.record_name + }; + web_client.action_manager.do_push_state(state); + var action = { + type:'ir.actions.act_window', + view_type: 'form', + view_mode: 'form', + res_model: 'res.partner', + views: [[false, 'form']], + res_id: partner_id, + }; + this.do_action(action); + }, + + read_value: function () { + var self = this; + return this.ds_model.read_ids([this.view.datarecord.id], ['message_follower_ids']) + .then(function (results) { + self.value = results[0].message_follower_ids; + self.render_value(); + }); + }, + + render_value: function () { + this.reinit(); + return this.fetch_followers(this.value); + }, + + fetch_followers: function (value_) { + this.value = value_ || []; + return ajax.jsonRpc('/mail/read_followers', 'call', {'follower_ids': this.value}) + .then(this.proxy('display_followers'), this.proxy('fetch_generic')) + .then(this.proxy('display_buttons')) + .then(this.proxy('fetch_subtypes')); + }, + + /** Read on res.partner failed: fall back on a generic case + - fetch current user partner_id (call because no other smart solution currently) FIXME + - then display a generic message about followers */ + fetch_generic: function () { + var self = this; + + return this.ds_users.call('read', [[session.uid], ['partner_id']]) + .then(function (results) { + var pid = results[0].partner_id[0]; + self.message_is_follower = (_.indexOf(self.value, pid) !== -1); + }) + .then(self.proxy('display_generic')); + }, + + _format_followers: function (count){ + var str = ''; + if(count <= 0){ + str = _t('No follower'); + }else if(count === 1){ + str = _t('One follower'); + }else{ + str = ''+count+' '+_t('followers'); + } + return str; + }, + + /* Display generic info about follower, for people not having access to res_partner */ + display_generic: function () { + this.$('.o_followers_list').empty(); + this.$('.o_followers_count').html(this._format_followers(this.value.length)); + }, + + /** Display the followers */ + display_followers: function (records) { + var self = this; + this.followers = records || this.followers; + + // clean and display title + var $followers_list = this.$('.o_followers_list').empty(); + this.$('.o_followers_count').html(this._format_followers(this.followers.length)); + var user_follower = _.filter(this.followers, function (rec) { return rec.is_uid;}); + this.message_is_follower = user_follower.length >= 1; + this.follower_id = this.message_is_follower ? user_follower[0].id : undefined; + + // render the dropdown content + $(qweb.render('mail.Followers.add_more', {'widget': self})).appendTo($followers_list); + var $follower_li; + _(this.followers).each(function (record) { + $follower_li = $(qweb.render('mail.Followers.partner', { + 'record': _.extend(record, {'avatar_url': '/web/image/' + record.res_model + '/' + record.res_id + '/image_small'}), + 'widget': self}) + ); + $follower_li.appendTo($followers_list); + + // On mouse-enter it will show the edit_subtype pencil. + if (record.is_editable) { + $follower_li.on('mouseenter mouseleave', function(e) { + $(e.currentTarget).find('.o_edit_subtype').toggleClass('hide', e.type === 'mouseleave'); + }); + } + }); + }, + + display_buttons: function () { + if (this.message_is_follower) { + this.$('button.o_followers_follow_button').removeClass('o_followers_notfollow').addClass('o_followers_following'); + } else { + this.$('button.o_followers_follow_button').removeClass('o_followers_following').addClass('o_followers_notfollow'); + } + }, + + /** Fetch subtypes, only if current user is follower */ + fetch_subtypes: function (follower_id) { + var self = this; + var dialog = false; + + if (follower_id) { + dialog = true; + } + else { + this.$('.o_subtypes_list ul').empty(); + if (! this.message_is_follower) { + this.$('.o_subtypes_list > .dropdown-toggle').attr('disabled', true); + return; + } + else { + this.$('.o_subtypes_list > .dropdown-toggle').attr('disabled', false); + } + } + if (this.follower_id) { + return ajax.jsonRpc('/mail/read_subscription_data', 'call', { + res_model: this.view.model, + res_id: this.view.datarecord.id, + follower_id: follower_id, + }).then(function (data) { + self.display_subtypes(data, dialog); + }); + } else { + return $.Deferred().resolve(); + } + }, + + /** Display subtypes: {'name': default, followed} */ + display_subtypes:function (data, dialog) { + var self = this; + var old_parent_model; + var $list; + if (dialog) { + $list = this.dialog.$el; + } else { + $list = this.$('.o_subtypes_list ul'); + } + $list.empty(); + + this.data_subtype = data; + this.records_length = $.map(data, function(value, index) { return index; }).length; + + if (this.records_length > 1) { + self.display_followers(); + } + + _.each(data, function (record) { + if (old_parent_model !== record.parent_model && old_parent_model !== undefined) { + $list.append($('
  • ').addClass('divider')); + } + old_parent_model = record.parent_model; + record.followed = record.followed || undefined; + $(qweb.render('mail.Followers.subtype', {'record': record, + 'dialog': dialog})) + .appendTo($list); + }); + }, + + do_follow: function () { + var context = new data.CompoundContext(this.build_context(), {}); + this.$('.o_subtypes_list > .dropdown-toggle').attr('disabled', false); + this.ds_model.call('message_subscribe_users', [[this.view.datarecord.id], + [session.uid], + undefined, + context]) + .then(this.proxy('read_value')); + + _.each(this.$('.o_subtypes_list input'), function (record) { + $(record).attr('checked', 'checked'); + }); + }, + + /** + * Remove users, partners, or channels from the followers + * @param {Array} [ids.user_ids] the user ids + * @param {Array} [ids.partner_ids] the partner ids + * @param {Array} [ids.channel_ids] the channel ids + */ + do_unfollow: function (ids) { + if (confirm(_t("Warning! \n If you remove a follower, he won't be notified of any email or discussion on this document. Do you really want to remove this follower ?"))) { + _(this.$('.o_subtype_checkbox')).each(function (record) { + $(record).attr('checked',false); + }); + + this.$('.o_subtypes_list > .dropdown-toggle').attr('disabled', true); + var context = new data.CompoundContext(this.build_context(), {}); + + if (ids.partner_ids || ids.channel_ids) { + return this.ds_model.call( + 'message_unsubscribe', [ + [this.view.datarecord.id], + ids.partner_ids, + ids.channel_ids, + context] + ).then(this.proxy('read_value')); + } else { + return this.ds_model.call( + 'message_unsubscribe_users', [ + [this.view.datarecord.id], + ids.user_ids, + context] + ).then(this.proxy('read_value')); + } + } + return false; + }, + + do_update_subscription: function (event, follower_id, is_channel) { + var self = this; + var kwargs = {}; + var ids = {}; + var action_subscribe; + var subtypes; + this.data_subtype = {}; + + if (follower_id !== undefined) { + // Subtypes edited from the modal + action_subscribe = 'message_subscribe'; + subtypes = this.dialog.$('input[type="checkbox"]'); + if (is_channel) { + ids.channel_ids = [follower_id]; + } else { + ids.partner_ids = [follower_id]; + } + } else { + action_subscribe = 'message_subscribe_users'; + subtypes = this.$('.o_followers_actions input[type="checkbox"]'); + ids.user_ids = [session.uid]; + } + kwargs = _.extend(kwargs, ids); + + // Get the subtype ids + var checklist = []; + _(subtypes).each(function (record) { + if ($(record).is(':checked')) { + checklist.push(parseInt($(record).data('id'))); + } + }); + kwargs.subtype_ids = checklist; + + // If no more subtype followed, unsubscribe the follower + if (!checklist.length) { + if (!this.do_unfollow(ids)) { + $(event.target).attr("checked", "checked"); + } else { + self.$('.o_subtypes_list ul').empty(); + } + } else { + kwargs.context = new data.CompoundContext(this.build_context(), {}); + return this.ds_model._model.call(action_subscribe, [[this.view.datarecord.id]], kwargs).then(this.proxy('read_value')); + } + }, +}); + +// ----------------------------------------------------------------------------- +// Chat Composer for the Chatter +// +// Extends the basic Composer Widget to add 'suggested partner' layer (open +// popup when suggested partner is selected without email, or other +// informations), and the button to open the full composer wizard. +// ----------------------------------------------------------------------------- +var ChatterComposer = ChatComposer.extend({ + template: 'mail.chatter.ChatComposer', + + init: function (parent, dataset, options) { + this._super(parent, options); + this.thread_dataset = dataset; + this.suggested_partners = []; + this.options = _.defaults(this.options, { + display_mode: 'textarea', + record_name: false, + is_log: false, + internal_subtypes: [], + }); + this.events = _.extend(this.events, { + 'click .o_composer_button_full_composer': 'on_open_full_composer', + }); + }, + + willStart: function () { + if (this.options.is_log) { + return this._super.apply(this, arguments); + } + return $.when(this._super.apply(this, arguments), this.message_get_suggested_recipients()); + }, + + start: function () { + var self = this; + return this._super.apply(this, arguments).then(function () { + autosize(self.$input); + }); + }, + + preprocess_message: function () { + var self = this; + var def = $.Deferred(); + this._super().then(function (message) { + message = _.extend(message, { + subtype_id: false, + subtype: 'mail.mt_comment', + message_type: 'comment', + content_subtype: 'html', + }); + + // Subtype + if (self.options.is_log) { + var subtype_id = parseInt(self.$('.o_chatter_composer_subtype_select').val()); + if (_.indexOf(_.pluck(self.options.internal_subtypes, 'id'), subtype_id) === -1) { + message.subtype = 'mail.mt_note'; + } else { + message.subtype_id = subtype_id; + } + } + + // Partner_ids + if (!self.options.is_log) { + var checked_suggested_partners = self.get_checked_suggested_partners(); + self.check_suggested_partners(checked_suggested_partners).done(function (partner_ids) { + message.partner_ids = (message.partner_ids || []).concat(partner_ids); + // update context + message.context = _.defaults(self.context, { + mail_post_autofollow: true, + mail_post_autofollow_partner_ids: partner_ids, + }); + def.resolve(message); + }); + } else { + def.resolve(message); + } + + }); + + return def; + }, + + /** + * Override keydown event handler to go to next line on ENTER and send message on CTRL+ENTER + */ + on_keydown: function (event) { + if (!event.ctrlKey && (event.keyCode === $.ui.keyCode.ENTER) && !this.get('mention_partners').length) { + return; + } + this._super(event); + }, + + message_get_suggested_recipients: function () { + var self = this; + var email_addresses = _.pluck(this.suggested_partners, 'email_address'); + return this.thread_dataset + .call('message_get_suggested_recipients', [[this.context.default_res_id], this.context]) + .done(function (suggested_recipients) { + var thread_recipients = suggested_recipients[self.context.default_res_id]; + _.each(thread_recipients, function (recipient) { + var parsed_email = parse_email(recipient[1]); + if (_.indexOf(email_addresses, parsed_email[1]) === -1) { + self.suggested_partners.push({ + checked: true, + partner_id: recipient[0], + full_name: recipient[1], + name: parsed_email[0], + email_address: parsed_email[1], + reason: recipient[2], + }); + } + }); + }); + }, + + /** + * Get the list of selected suggested partners + * @returns Array() : list of 'recipient' selected partners (may not be created in db) + **/ + get_checked_suggested_partners: function () { + var self = this; + var checked_partners = []; + this.$('.o_composer_suggested_partners input:checked').each(function() { + var full_name = $(this).data('fullname'); + checked_partners = checked_partners.concat(_.filter(self.suggested_partners, function(item) { + return full_name === item.full_name; + })); + }); + return checked_partners; + }, + + /** + * Check the additionnal partners (not necessary registered partners), and open a popup form view + * for the ones who informations is missing. + * @param Array : list of 'recipient' partners to complete informations or validate + * @returns Deferred resolved with the list of checked suggested partners (real partner) + **/ + check_suggested_partners: function (checked_suggested_partners) { + var self = this; + var check_done = $.Deferred(); + + var recipients = _.filter(checked_suggested_partners, function (recipient) { return recipient.checked; }); + var recipients_to_find = _.filter(recipients, function (recipient) { return (! recipient.partner_id); }); + var names_to_find = _.pluck(recipients_to_find, 'full_name'); + var recipients_to_check = _.filter(recipients, function (recipient) { return (recipient.partner_id && ! recipient.email_address); }); + var recipient_ids = _.pluck(_.filter(recipients, function (recipient) { return recipient.partner_id && recipient.email_address; }), 'partner_id'); + + var names_to_remove = []; + var recipient_ids_to_remove = []; + + // have unknown names -> call message_get_partner_info_from_emails to try to find partner_id + var find_done = $.Deferred(); + if (names_to_find.length > 0) { + find_done = self.thread_dataset.call('message_partner_info_from_emails', [[this.context.default_res_id], names_to_find]); + } else { + find_done.resolve([]); + } + + // for unknown names + incomplete partners -> open popup - cancel = remove from recipients + $.when(find_done).pipe(function (result) { + var emails_deferred = []; + var recipient_popups = result.concat(recipients_to_check); + + _.each(recipient_popups, function (partner_info) { + var deferred = $.Deferred(); + emails_deferred.push(deferred); + + var partner_name = partner_info.full_name; + var partner_id = partner_info.partner_id; + var parsed_email = parse_email(partner_name); + + var dialog = new form_common.FormViewDialog(self, { + res_model: 'res.partner', + res_id: partner_id, + context: { + force_email: true, + ref: "compound_context", + default_name: parsed_email[0], + default_email: parsed_email[1], + }, + title: _t("Please complete partner's informations"), + }).open(); + dialog.on('closed', self, function () { + deferred.resolve(); + }); + dialog.view_form.on('on_button_cancel', self, function () { + names_to_remove.push(partner_name); + if (partner_id) { + recipient_ids_to_remove.push(partner_id); + } + }); + }); + $.when.apply($, emails_deferred).then(function () { + var new_names_to_find = _.difference(names_to_find, names_to_remove); + find_done = $.Deferred(); + if (new_names_to_find.length > 0) { + find_done = self.thread_dataset.call('message_partner_info_from_emails', [[self.context.default_res_id], new_names_to_find, true]); + } else { + find_done.resolve([]); + } + $.when(find_done).pipe(function (result) { + var recipient_popups = result.concat(recipients_to_check); + _.each(recipient_popups, function (partner_info) { + if (partner_info.partner_id && _.indexOf(partner_info.partner_id, recipient_ids_to_remove) === -1) { + recipient_ids.push(partner_info.partner_id); + } + }); + }).pipe(function () { + check_done.resolve(recipient_ids); + }); + }); + }); + return check_done; + }, + + on_open_full_composer: function() { + if (!this.do_check_attachment_upload()){ + return false; + } + + var self = this; + var recipient_done = $.Deferred(); + if (this.options.is_log) { + recipient_done.resolve([]); + } else { + var checked_suggested_partners = this.get_checked_suggested_partners(); + recipient_done = this.check_suggested_partners(checked_suggested_partners); + } + recipient_done.then(function (partner_ids) { + var context = { + default_parent_id: self.id, + default_body: get_text2html(self.$input.val()), + default_attachment_ids: _.pluck(self.get('attachment_ids'), 'id'), + default_partner_ids: partner_ids, + default_is_log: self.options.is_log, + mail_post_autofollow: true, + mail_post_autofollow_partner_ids: partner_ids, + }; + + if (self.context.default_model && self.context.default_res_id) { + context.default_model = self.context.default_model; + context.default_res_id = self.context.default_res_id; + } + + self.do_action({ + type: 'ir.actions.act_window', + res_model: 'mail.compose.message', + view_mode: 'form', + view_type: 'form', + views: [[false, 'form']], + target: 'new', + context: context, + }, { + on_close: function() { + self.trigger('need_refresh'); + }, + }); + }); + } +}); + +// ----------------------------------------------------------------------------- +// Document Chatter ('mail_thread' widget) +// +// Since it is displayed on a form view, it extends 'AbstractField' widget. +// ----------------------------------------------------------------------------- +var Chatter = form_common.AbstractField.extend({ + template: 'mail.Chatter', + + events: { + "click .o_mail_redirect": "on_click_redirect", + "click .o_chatter_button_new_message": "on_open_composer_new_message", + "click .o_chatter_button_log_note": "on_open_composer_log_note", + }, + + init: function () { + this._super.apply(this, arguments); + this.messages = []; + this.model = this.view.dataset.model; + this.res_id = undefined; + this.context = this.options.context || {}; + }, + + start: function () { + var self = this; + + // Move the follower's widget (if any) inside the chatter + var followers = this.field_manager.fields.message_follower_ids; + if (followers) { + this.$('.o_chatter_topbar').append(followers.$el); + } + + this.thread = new ChatThread(this, { + display_order: ChatThread.ORDER.DESC, + display_document_link: false, + }); + this.thread.on('load_more_messages', this, this.load_more_messages); + this.thread.on('toggle_star_status', this, function (message_id) { + chat_manager.toggle_star_status(message_id); + }); + + var def1 = this._super.apply(this, arguments); + var def2 = this.thread.appendTo(this.$el); + + return $.when(def1, def2).then(function () { + chat_manager.bus.on('new_message', self, self.on_new_message); + chat_manager.bus.on('update_message', self, self.on_update_message); + }); + }, + + fetch_and_render_thread: function (ids, options) { + var self = this; + return chat_manager.fetch_messages(ids, options).then(function (raw_messages) { + self.thread.render(raw_messages, {display_load_more: raw_messages.length < ids.length}); + }); + }, + + on_post_message: function (message) { + var self = this; + chat_manager + .post_message_in_document(this.model, this.res_id, message) + .then(function () { + self.close_composer(); + }) + .fail(function () { + // todo: display notification + }); + }, + + /** + * When a message is correctly posted, fetch its data to render it + * @param {Number} message_id : the identifier of the new posted message + * @returns {Deferred} + */ + on_new_message: function (message) { + if (message.model === this.model && message.res_id === this.res_id) { + this.msg_ids.unshift(message.id); + this.fetch_and_render_thread(this.msg_ids); + } + }, + + on_update_message: function (message) { + if (message.model === this.model && message.res_id === this.res_id) { + this.fetch_and_render_thread(this.msg_ids); + } + }, + + on_click_redirect: function (event) { + event.preventDefault(); + this.do_action({ + type:'ir.actions.act_window', + view_type: 'form', + view_mode: 'form', + res_model: $(event.target).data('oe-model'), + views: [[false, 'form']], + res_id: $(event.target).data('oe-id'), + }); + }, + + load_more_messages: function () { + this.fetch_and_render_thread(this.msg_ids, {force_fetch: true}); + }, + + /** + * The value of the field has change (modification or switch to next document). Form view is + * not re-rendered, simply updated. A re-fetch is needed. + * @override + */ + render_value: function () { + // update context + this.context = _.extend({ + default_res_id: this.view.datarecord.id || false, + default_model: this.view.model || false, + }, this.options.context || {}); + this.thread_dataset = this.view.dataset; + this.res_id = this.view.datarecord.id; + this.record_name = this.view.datarecord.display_name; + this.msg_ids = this.get_value() || []; + + // destroy current composer, if any + if (this.composer) { + this.composer.destroy(); + } + + // fetch and render messages of current document + return this.fetch_and_render_thread(this.msg_ids); + }, + // composer toggle + on_open_composer_new_message: function () { + this.open_composer(); + }, + on_open_composer_log_note: function () { + this.open_composer({is_log: true}); + }, + open_composer: function (options) { + var self = this; + var old_composer = this.composer; + // create the new composer + this.composer = new ChatterComposer(this, this.thread_dataset, { + internal_subtypes: this.options.internal_subtypes, + is_log: options && options.is_log, + context: this.context, + record_name: this.record_name, + }); + this.composer.insertBefore(this.$('.o_mail_thread')).then(function () { + // destroy existing composer + if (old_composer) { + old_composer.destroy(); + } + self.composer.on('post_message', self, self.on_post_message); + }); + + }, + close_composer: function () { + if (this.composer.is_empty()) { + this.composer.do_hide(); + } + }, + + destroy: function () { + chat_manager.remove_chatter_messages(this.model); + this._super.apply(this, arguments); + }, + +}); + +// ----------------------------------------------------------------------------- +// Utils +// ----------------------------------------------------------------------------- +/** + * Parses text to find email: Tagada -> [Tagada, address@mail.fr] or False + */ +var parse_email = function (text) { + var result = text.match(/(.*)<(.*@.*)>/); + if (result) { + return [_.str.trim(result[1]), _.str.trim(result[2])]; + } + result = text.match(/(.*@.*)/); + if (result) { + return [_.str.trim(result[1]), _.str.trim(result[1])]; + } + return [text, false]; +}; +/** + * Replaces textarea text into html text (add

    , ) + * TDE note : should be done server-side, in Python -> use mail.compose.message ? + */ +var get_text2html = function (text) { + return text + .replace(/((?:https?|ftp):\/\/[\S]+)/g,'$1 ') + .replace(/[\n\r]/g,'
    '); +}; + +core.form_widget_registry.add('mail_followers', Followers); +core.form_widget_registry.add('mail_thread', Chatter); + +return Chatter; + +}); diff --git a/addons/mail/static/src/js/client_action.js b/addons/mail/static/src/js/client_action.js new file mode 100644 index 00000000000..025b9d88f1d --- /dev/null +++ b/addons/mail/static/src/js/client_action.js @@ -0,0 +1,476 @@ +odoo.define('mail.chat_client_action', function (require) { +"use strict"; + +var chat_manager = require('mail.chat_manager'); +var ChatComposer = require('mail.ChatComposer'); +var ChatThread = require('mail.ChatThread'); + +var ControlPanelMixin = require('web.ControlPanelMixin'); +var core = require('web.core'); +var data = require('web.data'); +var Dialog = require('web.Dialog'); +var framework = require('web.framework'); +var Model = require('web.Model'); + +var pyeval = require('web.pyeval'); +var SearchView = require('web.SearchView'); +var Sidebar = require('web.Sidebar'); +var Widget = require('web.Widget'); + +var QWeb = core.qweb; +var _t = core._t; + +/** + * Widget : Invite People to Channel Dialog + * + * Popup containing a 'many2many_tags' custom input to select multiple partners. + * Search user according to the input, and trigger event when selection is validated. + **/ +var PartnerInviteDialog = Dialog.extend({ + dialog_title: _t('Invite people'), + template: "mail.PartnerInviteDialog", + init: function(parent, title, channel_id){ + this.channel_id = channel_id; + + this._super(parent, { + title: title, + size: "medium", + buttons: [{ + text: _t("Invite"), + close: true, + classes: "btn-primary", + click: _.bind(this.on_click_add, this), + }], + }); + this.PartnersModel = new Model('res.partner'); + }, + start: function(){ + var self = this; + this.$input = this.$('.o_mail_chat_partner_invite_input'); + this.$input.select2({ + width: '100%', + allowClear: true, + multiple: true, + formatResult: function(item){ + var css_class = "fa-circle" + (item.im_status === 'online' ? "" : "-o"); + return $('').addClass(css_class).text(item.text); + }, + query: function (query) { + self.PartnersModel.call('im_search', [query.term, 20]).then(function(result){ + var data = []; + _.each(result, function(partner){ + partner.text = partner.name; + data.push(partner); + }); + query.callback({results: data}); + }); + } + }); + return this._super.apply(this, arguments); + }, + on_click_add: function(){ + var self = this; + var data = this.$input.select2('data'); + if(data.length >= 1){ + var ChannelModel = new Model('mail.channel'); + return ChannelModel.call('channel_invite', [], {ids : [this.channel_id], partner_ids: _.pluck(data, 'id')}) + .then(function(){ + var names = _.pluck(data, 'text').join(', '); + var notification = _.str.sprintf(_t('You added %s to the conversation.'), names); + self.do_notify(_t('New people'), notification); + }); + } + }, +}); + +var ChatAction = Widget.extend(ControlPanelMixin, { + template: 'mail.client_action', + + events: { + "click .o_mail_chat_channel_item": function (event) { + event.preventDefault(); + var channel_id = this.$(event.currentTarget).data('channel-id'); + this.set_channel(channel_id); + }, + "click .o_mail_sidebar_title .o_add": function (event) { + event.preventDefault(); + var type = $(event.target).data("type"); + this.$('.o_mail_add_channel[data-type=' + type + ']') + .show() + .find("input").focus(); + }, + "blur .o_mail_add_channel input": function () { + this.$('.o_mail_add_channel') + .hide(); + }, + "click .o_mail_partner_unpin": function (event) { + event.stopPropagation(); + var self = this; + var channel_id = $(event.target).data("channel-id"); + chat_manager.unsubscribe(channel_id).then(function () { + self.render_sidebar(); + self.set_channel("channel_inbox"); + }); + }, + }, + + on_attach_callback: function () { + this.thread.scroll_to({offset: this.channels_scrolltop[this.channel_id]}); + }, + on_detach_callback: function () { + this.channels_scrolltop[this.channel_id] = this.thread.get_scrolltop(); + }, + + init: function(parent, action) { + this._super.apply(this, arguments); + this.action_manager = parent; + this.domain = []; + this.action = action; + this.channels_scrolltop = {}; + }, + + start: function() { + var self = this; + + // create searchview + var options = { + $buttons: $("

    "), + action: this.action, + disable_groupby: true, + }; + var dataset = new data.DataSetSearch(this, 'mail.message'); + var view_id = (this.action && this.action.search_view_id && this.action.search_view_id[0]) || false; + var default_channel = this.action.context.active_id || this.action.params.default_active_id || 'channel_inbox'; + this.searchview = new SearchView(this, dataset, view_id, {}, options); + this.searchview.on('search_data', this, this.on_search); + + this.sidebar = new Sidebar(this, { + sections: [ + {name: 'action', label: _t('Action')}, + ], + items: { + action: [ + {label: _t('Unsubscribe'), classname: 'o_mail_chat_button_unsubscribe', callback: this.on_click_button_unsubscribe}, + {label: _t('Settings'), classname: 'o_mail_chat_button_settings', callback: this.on_click_button_settings}, + ], + }, + }); + + this.composer = new ChatComposer(this); + this.thread = new ChatThread(this, { + no_content_helper: this.action.help, + }); + + this.$buttons = $(QWeb.render("mail.chat.ControlButtons", {})); + this.$buttons.find('button').css({display:"inline-block"}); + this.$buttons.on('click', '.o_mail_chat_button_invite', this.on_click_button_invite); + this.$buttons.on('click', '.o_mail_chat_button_detach', this.on_click_button_detach); + + this.thread.on('redirect', this, this.on_redirect); + this.thread.on('load_more_messages', this, this.load_more_messages); + this.thread.on('mark_as_read', this, function (message_id) { + chat_manager.mark_as_read(message_id); + }); + this.thread.on('toggle_star_status', this, function (message_id) { + chat_manager.toggle_star_status(message_id); + }); + this.composer.on('post_message', this, this.on_post_message); + + var def1 = this.thread.prependTo(this.$('.o_mail_chat_content')); + var def2 = this.composer.appendTo(this.$('.o_mail_chat_content')); + var def3 = this.searchview.appendTo($("
    ")); + var def4 = this.sidebar.appendTo($("
    ")); + + this.render_sidebar(); + + return $.when(def1, def2, def3, def4) + .then(this.set_channel.bind(this, default_channel)) + .then(function () { + chat_manager.bus.on('new_message', self, self.on_new_message); + chat_manager.bus.on('update_message', self, self.on_update_message); + chat_manager.bus.on('new_channel', self, self.on_new_channel); + chat_manager.bus.on('anyone_listening', self, function (channel, query) { + query.is_displayed = query.is_displayed || channel.id === self.channel_id; + }); + chat_manager.bus.on('update_needaction', self, self.on_update_needaction); + }); + }, + + render_sidebar: function () { + var self = this; + var $sidebar = QWeb.render("mail.chat.Sidebar", { + channels: chat_manager.get_channels(), + needaction_counter: chat_manager.get_needaction_counter() + }); + this.$(".o_mail_chat_sidebar").replaceWith($sidebar); + + this.$('.o_mail_add_channel[data-type=public]').find("input").autocomplete({ + source: function(request, response) { + self.last_search_val = request.term; + self.do_search_channel(request.term).done(function(result){ + result.push({ + 'label': _.str.sprintf(''+_t("Create %s")+'', '"'+self.last_search_val+'"'), + 'value': '_create', + }); + response(result); + }); + }, + select: function(event, ui) { + if (self.last_search_val) { + if (ui.item.value === '_create') { + chat_manager.create_channel(self.last_search_val, "public"); + } else { + chat_manager.join_channel(ui.item.id); + } + } + }, + focus: function(event) { + event.preventDefault(); + }, + html: true, + }); + + this.$('.o_mail_add_channel[data-type=dm]').find("input").autocomplete({ + source: function(request, response) { + self.last_search_val = request.term; + self.do_search_partner(request.term).done(function(result){ + response(result); + }); + }, + select: function(event, ui) { + var partner_id = ui.item.id; + chat_manager.create_channel(partner_id, "dm"); + }, + focus: function(event) { + event.preventDefault(); + }, + html: true, + }); + + this.$('.o_mail_add_channel[data-type=private]').find("input").on('keyup', this, function (event) { + var name = $(event.target).val(); + if(event.which === $.ui.keyCode.ENTER && name) { + chat_manager.create_channel(name, "private"); + } + }); + }, + + do_search_channel: function(search_val){ + var Channel = new Model("mail.channel"); + return Channel.call('channel_search_to_join', [search_val]).then(function(result){ + var values = []; + _.each(result, function(channel){ + values.push(_.extend(channel, { + 'value': channel.name, + 'label': channel.name, + })); + }); + return values; + }); + }, + + do_search_partner: function (search_val) { + var Partner = new Model("res.partner"); + return Partner.call('im_search', [search_val, 20]).then(function(result){ + var values = []; + _.each(result, function(user){ + values.push(_.extend(user, { + 'value': user.name, + 'label': user.name, + })); + }); + return values; + }); + }, + + set_channel: function (channel_id) { + var self = this; + // Store scroll position of previous channel + if (this.channel_id) { + this.channels_scrolltop[this.channel_id] = this.thread.get_scrolltop(); + } + var new_channel_scrolltop = this.channels_scrolltop[channel_id]; + var channel = chat_manager.get_channel(channel_id); + channel_id = channel.id; + this.channel_id = channel_id; + this.set("title", channel.name); + this.$buttons.toggle(channel.type !== "static"); + this.$buttons + .find('.o_mail_chat_button_invite') + .toggle(channel.type !== "dm"); + + this.update_cp(); + this.action.context.active_id = channel_id; + this.action.context.active_ids = [channel_id]; + + this.$('.o_mail_chat_channel_item') + .removeClass('o_active') + .filter('[data-channel-id=' + channel_id + ']') + .removeClass('o_unread_message') + .addClass('o_active'); + + this.$('.o_chat_composer').toggle(channel.type !== 'static'); + this.sidebar.$el.toggle(channel.type !== 'static'); + + return this.fetch_and_render_thread().then(function () { + self.thread.scroll_to({offset: new_channel_scrolltop}); + self.action_manager.do_push_state({ + action: self.action.id, + active_id: self.channel_id, + }); + }); + }, + + fetch_and_render_thread: function () { + var self = this; + return chat_manager.fetch(this.channel_id, this.domain).then(function(result) { + self.thread.render(result, {display_load_more: !chat_manager.all_history_loaded(self.channel_id)}); + }); + }, + + load_more_messages: 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 = -framework.getPosition(document.querySelector(oldest_msg_selector)).top; + return chat_manager + .fetch_more(this.channel_id, this.domain) + .then(function(result) { + self.thread.render(result, {display_load_more: !chat_manager.all_history_loaded(self.channel_id)}); + offset += framework.getPosition(document.querySelector(oldest_msg_selector)).top; + self.thread.scroll_to({offset: offset}); + }); + }, + + update_cp: function () { + this.update_control_panel({ + breadcrumbs: this.action_manager.get_breadcrumbs(), + cp_content: { + $buttons: this.$buttons, + $searchview: this.searchview.$el, + $searchview_buttons: this.searchview.$buttons.contents(), + $sidebar: this.sidebar.$el, + }, + searchview: this.searchview, + }); + }, + + on_search: function (domains) { + var result = pyeval.sync_eval_domains_and_contexts({ + domains: domains + }); + + this.domain = result.domain; + this.fetch_and_render_thread(); + }, + + on_redirect: function (res_model, res_id) { + this.action_manager.do_push_state({ + model: res_model, + id: res_id, + }); + this.do_action({ + type:'ir.actions.act_window', + view_type: 'form', + view_mode: 'form', + res_model: res_model, + views: [[false, 'form']], + res_id: res_id, + }, { + on_reverse_breadcrumb: this.on_reverse_breadcrumb, + }); + }, + on_reverse_breadcrumb: function () { + this.update_cp(); // do not reload the client action, just display it, but a refresh of the control panel is needed. + this.action_manager.do_push_state({ + action: this.action.id, + active_id: this.channel_id, + }); + }, + on_post_message: function (message) { + message.channel_id = this.channel_id; + chat_manager + .post_message(message) + .fail(function () { + // todo: display notification + }); + }, + on_new_message: function (message) { + var self = this; + if (_.contains(message.channel_ids, this.channel_id)) { + this.fetch_and_render_thread().then(function () { + self.thread.scroll_to({id: message.id}); + }); + } + // Indicate in the sidebar that there is a new message in the corresponding channels + _.each(_.without(message.channel_ids, this.channel_id), function (channel_id) { + self.$('.o_mail_chat_channel_item') + .filter('[data-channel-id=' + channel_id + ']') + .addClass('o_unread_message'); + }); + // Dump scroll position of channels in which the new message arrived + this.channels_scrolltop = _.omit(this.channels_scrolltop, message.channel_ids); + }, + on_update_message: function (message) { + var self = this; + if ((this.channel_id === "channel_starred" && !message.is_starred) || + (this.channel_id === "channel_inbox" && !message.is_needaction)) { + chat_manager.fetch(this.channel_id, this.domain).then(function (messages) { + var options = {display_load_more: !chat_manager.all_history_loaded(self.channel_id)}; + self.thread.remove_message_and_render(message.id, messages, options); + }); + } else if (_.contains(message.channel_ids, this.channel_id)) { + this.fetch_and_render_thread(); + } + }, + on_new_channel: function (channel) { + this.render_sidebar(); + if (channel.autoswitch) { + this.set_channel(channel.id); + } + }, + on_update_needaction: function () { + var $needaction_badge = QWeb.render("mail.chat.SidebarNeedaction", { + needaction_counter: chat_manager.get_needaction_counter() + }); + this.$('.o_mail_sidebar_needaction').replaceWith($needaction_badge); + }, + + on_click_button_invite: function () { + var channel = chat_manager.get_channel(this.channel_id); + var title = _.str.sprintf(_t('Invite people to %s'), channel.name); + new PartnerInviteDialog(this, title, this.channel_id).open(); + }, + on_click_button_detach: function () { + chat_manager.detach_channel(this.channel_id); + }, + + on_click_button_unsubscribe: function () { + var self = this; + var channel = chat_manager.get_channel(this.channel_id); + chat_manager + .unsubscribe(channel.id) + .then(this.set_channel.bind(this, "channel_inbox")) + .then(this.render_sidebar.bind(this)) + .then(function () { + var msg = _.str.sprintf(_t('You unsubscribed from %s.'), channel.name); + self.do_notify(_t("Unsubscribed"), msg); + }); + }, + on_click_button_settings: function() { + this.do_action({ + type: 'ir.actions.act_window', + res_model: "mail.channel", + res_id: this.channel_id, + views: [[false, 'form']], + target: 'current' + }, { + on_reverse_breadcrumb: this.on_reverse_breadcrumb, + }); + }, +}); + + +core.action_registry.add('mail.chat.instant_messaging', ChatAction); + +}); diff --git a/addons/mail/static/src/js/composer.js b/addons/mail/static/src/js/composer.js new file mode 100644 index 00000000000..75244319053 --- /dev/null +++ b/addons/mail/static/src/js/composer.js @@ -0,0 +1,486 @@ +odoo.define('mail.ChatComposer', function (require) { +"use strict"; + +var chat_manager = require('mail.chat_manager'); + +var core = require('web.core'); +var data = require('web.data'); +var Model = require('web.Model'); +var session = require('web.session'); +var Widget = require('web.Widget'); + +var QWeb = core.qweb; +var _t = core._t; + +var Composer = Widget.extend({ + template: "mail.ChatComposer", + + events: { + "keydown .o_composer_input": "on_keydown", + "keyup .o_composer_input": "on_keyup", + "change input.o_form_input_file": "on_attachment_change", + "click .o_composer_button_send": "send_message", + "click .o_composer_button_add_attachment": "on_click_add_attachment", + "click .o_attachment_delete": "on_attachment_delete", + "click .o_mention_proposition": "on_click_mention_item", + "mouseover .o_mention_proposition": "on_hover_mention_proposition", + }, + + init: function (parent, options) { + this._super.apply(this, arguments); + this.options = _.defaults(options || {}, { + context: {}, + mention_delimiter: '@', + mention_min_length: 2, + mention_typing_speed: 400, + mention_fetch_limit: 8, + }); + this.context = this.options.context; + + // Attachments + this.AttachmentDataSet = new data.DataSetSearch(this, 'ir.attachment', this.context); + this.fileupload_id = _.uniqueId('o_chat_fileupload'); + this.set('attachment_ids', []); + + // Mention + this.PartnerModel = new Model('res.partner'); + this.set('mention_partners', []); // proposition of not-mention partner matching the mention_word + this.set('mention_selected_partners', []); // contains the mention partners sorted as they appear in the input text + }, + + start: function () { + var self = this; + + this.$attachment_button = this.$(".o_composer_button_add_attachment"); + this.$attachments_list = this.$('.o_composer_attachments_list'); + this.$mention_partner_tags = this.$('.o_composer_mentioned_partners'); + this.$mention_dropdown = this.$('.o_composer_mention_dropdown'); + this.$input = this.$('.o_composer_input'); + this.$input.focus(); + + // Attachments + $(window).on(this.fileupload_id, this.on_attachment_loaded); + this.on("change:attachment_ids", this, this.render_attachments); + + // Emoji + this.$('.o_composer_button_emoji').popover({ + placement: 'top', + content: function() { + if (!self.$emojis) { // lazy rendering + self.$emojis = $(QWeb.render('mail.ChatComposer.emojis', { + emojis: chat_manager.get_emojis(), + })); + self.$emojis.filter('.o_mail_emoji').on('click', self, self.on_click_emoji_img); + } + return self.$emojis; + }, + html: true, + container: '.o_composer_emoji', + trigger: 'focus', + }); + + // Mention + this.on('change:mention_partners', this, this.render_mention_partners); + this.on('change:mention_selected_partners', this, this.render_mention_selected_partners); + return this._super(); + }, + + preprocess_message: function () { + // Return a deferred as this function is extended with asynchronous + // behavior for the chatter composer + return $.when({ + content: this.mention_preprocess_message(this.$input.val()), + attachment_ids: _.pluck(this.get('attachment_ids'), 'id'), + partner_ids: _.pluck(this.get('mention_selected_partners'), 'id'), + }); + }, + + send_message: function () { + if (this.is_empty() || !this.do_check_attachment_upload()) { + return; + } + + var self = this; + this.preprocess_message().then(function (message) { + self.trigger('post_message', message); + + // Empty input, selected partners and attachments + self.$input.val(''); + self.set('mention_selected_partners', []); + self.set('attachment_ids', []); + + self.$input.focus(); + }); + }, + + // Events + on_click_add_attachment: function () { + this.$('input.o_form_input_file').click(); + this.$input.focus(); + }, + + on_click_emoji_img: function(event) { + this.$input.val(this.$input.val() + " " + $(event.currentTarget).data('emoji') + " "); + this.$input.focus(); + }, + + on_keydown: function (event) { + switch(event.which) { + // UP, DOWN: prevent moving cursor if navigation in mention propositions + case $.ui.keyCode.UP: + case $.ui.keyCode.DOWN: + if (this.get('mention_partners').length) { + event.preventDefault(); + } + break; + // BACKSPACE, DELETE: check if need to remove a mention + case $.ui.keyCode.BACKSPACE: + case $.ui.keyCode.DELETE: + this.mention_check_remove(); + break; + // ENTER: submit the message only if the dropdown mention proposition is not displayed + case $.ui.keyCode.ENTER: + event.preventDefault(); + if (!this.get('mention_partners').length) { + this.send_message(); + } + break; + } + }, + + on_keyup: function (event) { + switch(event.which) { + // ESCAPED KEYS: do nothing + case $.ui.keyCode.END: + case $.ui.keyCode.PAGE_UP: + case $.ui.keyCode.PAGE_DOWN: + break; + // ESCAPE: close mention propositions + case $.ui.keyCode.ESCAPE: + this.set('mention_partners', []); + break; + // ENTER, UP, DOWN: check if navigation in mention propositions + case $.ui.keyCode.ENTER: + case $.ui.keyCode.UP: + case $.ui.keyCode.DOWN: + this.mention_proposition_navigation(event.which); + break; + // Otherwise, check if a mention is typed + default: + this.mention_word = this.mention_detect_delimiter(); + if (this.mention_word) { + this.mention_word_changed(); + } else { + this.set('mention_partners', []); // close the dropdown + } + } + }, + + // Attachments + on_attachment_change: function(event) { + var $target = $(event.target); + if ($target.val() !== '') { + var filename = $target.val().replace(/.*[\\\/]/,''); + // if the files exits for this answer, delete the file before upload + var attachments = []; + for (var i in this.get('attachment_ids')) { + if ((this.get('attachment_ids')[i].filename || this.get('attachment_ids')[i].name) === filename) { + if (this.get('attachment_ids')[i].upload) { + return false; + } + this.AttachmentDataSet.unlink([this.get('attachment_ids')[i].id]); + } else { + attachments.push(this.get('attachment_ids')[i]); + } + } + // submit filename + this.$('form.o_form_binary_form').submit(); + this.$attachment_button.prop('disabled', true); + + attachments.push({ + 'id': 0, + 'name': filename, + 'filename': filename, + 'url': '', + 'upload': true, + 'mimetype': '', + }); + this.set('attachment_ids', attachments); + } + }, + on_attachment_loaded: function(event, result) { + var attachment_ids = []; + if (result.error || !result.id ) { + this.do_warn(result.error); + attachment_ids = _.filter(this.get('attachment_ids'), function (val) { return !val.upload; }); + } else { + _.each(this.get('attachment_ids'), function(a) { + if (a.filename === result.filename && a.upload) { + attachment_ids.push({ + 'id': result.id, + 'name': result.name || result.filename, + 'filename': result.filename, + 'mimetype': result.mimetype, + 'url': session.url('/web/content', {'id': result.id, download: true}), + }); + } else { + attachment_ids.push(a); + } + }); + } + this.set('attachment_ids', attachment_ids); + this.$attachment_button.prop('disabled', false); + }, + on_attachment_delete: function(event){ + event.stopPropagation(); + var self = this; + var attachment_id = $(event.target).data("id"); + if (attachment_id) { + var attachments = []; + _.each(this.get('attachment_ids'), function(attachment){ + if (attachment_id !== attachment.id) { + attachments.push(attachment); + } else { + self.AttachmentDataSet.unlink([attachment_id]); + } + }); + this.set('attachment_ids', attachments); + } + }, + do_check_attachment_upload: function () { + if (_.find(this.get('attachment_ids'), function (file) { return file.upload; })) { + this.do_warn(_t("Uploading error"), _t("Please, wait while the file is uploading.")); + return false; + } + return true; + }, + render_attachments: function() { + this.$attachments_list.html(QWeb.render('mail.ChatComposer.Attachments', { + attachments: this.get('attachment_ids'), + })); + }, + + // Mention + on_click_mention_item: function (event) { + event.preventDefault(); + + var text_input = this.$input.val(); + var partner_id = $(event.currentTarget).data('partner-id'); + var selected_partner = _.filter(this.get('mention_partners'), function (p) { + return p.id === partner_id; + })[0]; + + // add the mention partner to the list + var mention_selected_partners = this.get('mention_selected_partners'); + if (mention_selected_partners.length) { // there are already mention partners + // get mention matches (ordered by index in the text) + var matches = this.mention_get_match(text_input); + var index = this.mention_get_index(matches, this.get_selection_positions().start); + mention_selected_partners.splice(index, 0, selected_partner); + mention_selected_partners = _.clone(mention_selected_partners); + } else { // this is the first mentionned partner + mention_selected_partners = mention_selected_partners.concat([selected_partner]); + } + this.set('mention_selected_partners', mention_selected_partners); + + // update input text, and reset dropdown + var cursor_position = this.get_selection_positions().start; + var text_left = text_input.substring(0, cursor_position-(this.mention_word.length+1)); + var text_right = text_input.substring(cursor_position, text_input.length); + var text_input_new = text_left + this.options.mention_delimiter + selected_partner.name + ' ' + text_right; + this.$input.val(text_input_new); + this.set_cursor_position(text_left.length+selected_partner.name.length+2); + this.set('mention_partners', []); + }, + + on_hover_mention_proposition: function (event) { + var $elem = $(event.currentTarget); + this.$('.o_mention_proposition').removeClass('active'); + $elem.addClass('active'); + }, + + mention_proposition_navigation: function (keycode) { + var $active = this.$('.o_mention_proposition.active'); + if (keycode === $.ui.keyCode.ENTER) { // selecting proposition + $active.click(); + } else { // navigation in propositions + var $to; + if (keycode === $.ui.keyCode.DOWN) { + $to = $active.next('.o_mention_proposition:not(.active)'); + } else { + $to = $active.prev('.o_mention_proposition:not(.active)'); + } + if ($to.length) { + $active.removeClass('active'); + $to.addClass('active'); + } + } + }, + + /** + * Return the text attached to the mention delimiter + * @returns {String|false}: the text right after the delimiter or false + */ + mention_detect_delimiter: function () { + var options = this.options; + var delimiter = options.mention_delimiter; + var text_val = this.$input.val(); + var cursor_position = this.get_selection_positions().start; + var left_string = text_val.substring(0, cursor_position); + var search_str = text_val.substring(left_string.lastIndexOf(delimiter) - 1, cursor_position); + + return validate_keyword(search_str); + + function validate_keyword (search_str) { + var pattern = "(^"+delimiter+"|(^\\s"+delimiter+"))"; + var regex_start = new RegExp(pattern, "g"); + search_str = search_str.replace(/^\s\s*|^[\n\r]/g, ''); + if (regex_start.test(search_str) && search_str.length > options.mention_min_length) { + search_str = search_str.replace(pattern, ''); + return search_str.indexOf(' ') < 0 && !/[\r\n]/.test(search_str) ? search_str.replace(delimiter, '') : false; + } + return false; + } + }, + + mention_word_changed: function () { + var self = this; + // start a timeout to fetch partner with the current 'mention word'. The timer avoid to start + // an RPC for each pushed key when the user is typing the partner name. + // The 'mention_typing_speed' option should approach the time for a human to type a letter. + clearTimeout(this.mention_fetch_timer); + this.mention_fetch_timer = setTimeout(function () { + self.mention_fetch_partner(self.mention_word); + }, this.options.mention_typing_speed); + }, + + mention_fetch_partner: function (search_str) { + var self = this; + this.PartnerModel + .query(['id', 'name', 'email']) + .filter([['id', 'not in', _.pluck(this.get('mention_selected_partners'), 'id')], + '|', ['name', 'ilike', search_str], ['email', 'ilike', search_str]]) + .limit(this.options.mention_fetch_limit) + .all().then(function (partners) { + self.set('mention_partners', partners); + }); + }, + + mention_check_remove: function () { + var mention_selected_partners = this.get('mention_selected_partners'); + var partners_to_remove = []; + var selection = this.get_selection_positions(); + var deleted_binf = selection.start; + var deleted_bsup = selection.end; + + var matches = this.mention_get_match(this.$input.val()); + for (var i=0; i%s", partners[i].id, partner_name); + var subtext = message.substring(start_index, end_index).replace(match[0], processed_text); + substrings.push(subtext); + start_index = end_index; + } + substrings.push(message.substring(start_index, message.length)); + return substrings.join(''); + } + return message; + }, + + render_mention_partners: function () { + if (this.get('mention_partners').length) { + this.$mention_dropdown.html(QWeb.render('mail.ChatComposer.MentionMenu', { + partners: this.get('mention_partners'), + })); + this.$mention_dropdown.addClass('open'); + } else { + this.$mention_dropdown.removeClass('open'); + this.$mention_dropdown.empty(); + } + }, + + render_mention_selected_partners: function () { + this.$mention_partner_tags.html(QWeb.render('mail.ChatComposer.MentionTags', { + partners: this.get('mention_selected_partners'), + })); + }, + + /** + * Return the matches (as RexExp.exec does) for the partner mention in the input text + * @param {String} input_text: the text to search matches + * @returns {Object[]} matches in the same format as RexExp.exec() + */ + mention_get_match: function (input_text) { + var self = this; + // create the regex of all mention partner name + var partner_names = _.pluck(this.get('mention_selected_partners'), 'name'); + var escaped_partner_names = _.map(partner_names, function (str) { + return "("+_.str.escapeRegExp(self.options.mention_delimiter+str)+")"; + }); + var regex_str = escaped_partner_names.join('|'); + // extract matches + var result = []; + if(regex_str.length){ + var myRegexp = new RegExp(regex_str, 'g'); + var match = myRegexp.exec(input_text); + while (match !== null) { + result.push(match); + match = myRegexp.exec(input_text); + } + } + return result; + }, + + mention_get_index: function (matches, cursor_position) { + for (var i=0; i'+_t("Create %s")+'', '"'+self.last_search_val+'"'), - 'value': '_create', - }); - } - response(result); - }); - }, - select: function(event, ui) { - self.on_click_item(ui.item); - }, - focus: function(event) { - event.preventDefault(); - }, - html: true, - }); - }, - // ui - _toggle_elements: function(){ - this.$add_more_text.toggle(); - this.$add_more_search_bar.toggle(); - this.$input.val(''); - this.$input.focus(); - }, - on_click_text: function(event){ - event.preventDefault(); - this._toggle_elements(); - }, - // to be redefined - do_search: function(){ - return $.when(); - }, - on_click_item: function(item){ - if(item.value === '_create'){ - if(this.last_search_val){ - this.trigger('item_create', this.last_search_val); - } - }else{ - this.trigger('item_clicked', item); - } - }, -}); - -var PartnerAddMoreSearch = AbstractAddMoreSearch.extend({ - /** - * Do the search call - * @override - */ - do_search: function(search_val){ - var Partner = new Model("res.partner"); - return Partner.call('im_search', [search_val, this.limit]).then(function(result){ - var values = []; - _.each(result, function(user){ - values.push(_.extend(user, { - 'value': user.name, - 'label': user.name, - })); - }); - return values; - }); - }, -}); - -var ChannelAddMoreSearch = AbstractAddMoreSearch.extend({ - /** - * Do the search call - * @override - */ - do_search: function(search_val){ - var Channel = new Model("mail.channel"); - return Channel.call('channel_search_to_join', [search_val]).then(function(result){ - var values = []; - _.each(result, function(channel){ - values.push(_.extend(channel, { - 'value': channel.name, - 'label': channel.name, - })); - }); - return values; - }); - }, -}); - -var PrivateGroupAddMoreSearch = AbstractAddMoreSearch.extend({ - _bind_events: function(){ - // don't call the super to avoid autocomplete - this.$input.on('keyup', this, this.on_keydown); - }, - on_keydown: function(event){ - if(event.which === $.ui.keyCode.ENTER && this.$input.val()){ - this.trigger('item_create', this.$input.val()); - } - }, -}); - - -/** - * Widget : Invite People to Channel Dialog - * - * Popup containing a 'many2many_tags' custom input to select multiple partners. - * Search user according to the input, and trigger event when selection is validated. - **/ -var PartnerInviteDialog = Dialog.extend({ - dialog_title: _t('Invite people'), - template: "mail.chat.PartnerInviteDialog", - init: function(parent, options){ - options = _.defaults(options || {}, { - buttons: [{ - text: _t("Invite"), - close: true, - classes: "btn-primary", - click: _.bind(this.on_click_add, this), - }], - channel: undefined, - }); - this._super.apply(this, arguments); - this.set("partners", []); - this.channel = options.channel; - this.PartnersModel = new Model('res.partner'); - this.ChannelModel = new Model('mail.channel'); - this.limit = 20; - }, - start: function(){ - var self = this; - this.$partner_invite_input = this.$('.o_mail_chat_partner_invite_input'); - this.$partner_invite_input.select2({ - width: '100%', - allowClear: true, - multiple: true, - formatResult: function(item){ - if(item.im_status === 'online'){ - return ' ' + item.text + ''; - } - return ' ' + item.text + ''; - }, - query: function (query) { - self.PartnersModel.call('im_search', [query.term, self.limit]).then(function(result){ - var data = []; - _.each(result, function(partner){ - partner.text = partner.name; - data.push(partner); - }); - query.callback({results: data}); - }); - } - }); - return this._super.apply(this, arguments); - }, - on_click_add: function(){ - var self = this; - var data = this.$partner_invite_input.select2('data'); - if(data.length >= 1){ - return this.ChannelModel.call('channel_invite', [], {"ids" : [this.channel.id], 'partner_ids': _.pluck(data, 'id')}).then(function(){ - var names = _.pluck(data, 'text').join(', '); - self.do_notify(_t('New people'), _.str.sprintf(_t('You added %s to the conversation.'), names)); - }); - } - }, -}); - -/** - * Client Action : Instant Messaging View, inspired by Slack.com - * - * Action replacing the Inbox, and the list of group (mailing list, multiple conversation, rooms, ...) - * Includes real time messages (received and sent), creating group, channel, chat conversation, ... - **/ -var ChatMailThread = Widget.extend(mail_thread.MailThreadMixin, ControlPanelMixin, { - template: 'mail.chat.ChatMailThread', - events: { - "click .o_mail_thread_show_more > button": "message_load_history", - // events from MailThreadMixin - "click .o_mail_redirect": "on_click_redirect", - "click .o_mail_expand": "on_message_body_toggle", - "click .o_mail_thread_message_star": "on_message_star", - // events specific for ChatMailThread - "click .o_mail_chat_channel_item": "on_click_channel", - "click .o_mail_chat_partner_item": "on_click_partner", - "click .o_mail_chat_partner_unpin": "on_click_partner_unpin", - "click .o_mail_thread_message_needaction": "on_message_needaction", - }, - init: function (parent, action) { - this._super.apply(this, arguments); - // attributes - this.action_manager = parent; - this.help_message = action.help || ''; - this.context = action.context; - this.action = action; - this.chatter_needaction_auto = false; - this.options = this.action.params; - // mail thread mixin - mail_thread.MailThreadMixin.init.call(this); - // components : conversation manager and search widget (channel_type + '_search_widget') - this.conv_manager = web_client.mail_conversation_manager; - this.channel_search_widget = new ChannelAddMoreSearch(this, {'label': _t('+ Subscribe'), 'can_create': true}); - this.group_search_widget = new PrivateGroupAddMoreSearch(this, {'label': _t('+ New private group'), 'can_create': true}); - this.partner_search_widget = new PartnerAddMoreSearch(this); - // options (update the default of MailThreadMixin) - this.options = _.extend(this.options, { - 'display_document_link': true, - 'display_needaction_button': true, - 'emoji_list': this.conv_manager.emoji_list, - 'default_username': _t('Anonymous'), - }); - this.emoji_set_substitution(this.conv_manager.emoji_list); - // channel business - this.channels = {}; - this.mapping = {}; // mapping partner_id/channel_id for 'direct message' channel - this.set('current_channel_id', false); - this.set('needaction_inbox_counter', 0); - this.search_domain = []; - // channel slots - this.set('channel_channel', []); - this.set('channel_direct_message', []); - this.set('channel_private_group', []); - this.set('partners', []); - // models - this.ChannelModel = new Model('mail.channel', this.context); - // control panel items - this.control_elements = {}; - }, - willStart: function(){ - var self = this; - return session.rpc('/mail/client_action').then(function(result){ - self.chatter_needaction_auto = result.chatter_needaction_auto; - self.set('needaction_inbox_counter', result.needaction_inbox_counter); - self.set('partners', result.channel_slots.partners); - self.mapping = result.channel_slots.mapping; - self._channel_slot(_.omit(result.channel_slots, 'partners', 'mapping')); - }); - }, - start: function(){ - var self = this; - this._super.apply(this, arguments); - - this.$messages = this.$('.o_mail_chat_messages'); - - mail_thread.MailThreadMixin.start.call(this); - // channel business events - this.on("change:current_channel_id", this, this.channel_change); - this.on("change:channel_channel", this, function(){ - self.channel_render('channel_channel'); - }); - this.on("change:channel_private_group", this, function(){ - self.channel_render('channel_private_group'); - }); - this.on("change:partners", this, this.partner_render); - - // search widget for channel - this.channel_search_widget.insertAfter(this.$('.o_mail_chat_channel_slot_channel_channel')); - this.channel_search_widget.on('item_create', this, function(name){ - self.channel_create(name, 'public'); - }); - this.channel_search_widget.on('item_clicked', this, function(item){ - self.channel_join_and_get_info(item.id).then(function(channel){ - self.channel_apply(channel); - }); - }); - // search widget for direct message - this.partner_search_widget.insertAfter(this.$('.o_mail_chat_channel_slot_partners')); - this.partner_search_widget.on('item_clicked', this, function(item){ - self.channel_get([item.id]); - self.partner_add(item); - }); - // search widget for private group - this.group_search_widget.insertAfter(this.$('.o_mail_chat_channel_slot_channel_private_group')); - this.group_search_widget.on('item_create', this, function(name){ - self.channel_create(name, 'private'); - }); - - // needaction inbox counter - this.on('change:needaction_inbox_counter', this, this.needaction_inbox_change); - - return $.when(this._super.apply(this, arguments), this.cp_render_searchview()).then(function(){ - // render control panel's elements - self.cp_render_elements(); - // apply default channel - var channel_id = self.context.active_id || self.action.params.default_active_id || 'channel_inbox'; - if(!_.isString(channel_id)){ - if(_.contains(_.keys(self.channels), channel_id)){ - self.set('current_channel_id', channel_id); - }else{ - self.channel_info(channel_id).then(function(channel){ - self.channel_apply(channel); - }); - } - }else{ - self.set('current_channel_id', channel_id); - } - // internal communication (bind here to avoid receving message from bus when client action still not totally ready) - internal_bus.on('mail_message_receive', self, self.message_receive); - internal_bus.on('mail_session_receive', self, self.channel_receive); - // needaction : the inbox has the same counter as the needaction top counter - internal_bus.on('mail_needaction_new', this, function(inc){ - self.set('needaction_inbox_counter' , self.get('needaction_inbox_counter') + inc); - }); - internal_bus.on('mail_needaction_done', this, function(dec){ - self.set('needaction_inbox_counter' , self.get('needaction_inbox_counter') - dec); - }); - }); - }, - // event actions - on_click_channel: function(event){ - event.preventDefault(); - var channel_id = this.$(event.currentTarget).data('channel-id'); - this.set('current_channel_id', channel_id); - }, - on_click_partner: function(event){ - if(!this.$(event.target).hasClass('o_mail_chat_channel_unpin')){ - event.preventDefault(); - var partner_id = this.$(event.currentTarget).data('partner-id'); - if(this.mapping[partner_id]){ // don't fetch if channel already in local - this.set('current_channel_id', this.mapping[partner_id]); - }else{ - this.channel_get([partner_id]); - } - } - }, - on_click_partner_unpin: function(event){ - event.preventDefault(); - var self = this; - var $source = this.$(event.currentTarget); - var partner_id = $source.data('partner-id'); - var channel_id = this.mapping[partner_id]; - var channel = this.channels[channel_id]; - this.channel_pin(channel.uuid, false).then(function(){ - self.set('partners', _.filter(self.get('partners'), function(p){ return p.id !== partner_id; })); - self.channel_remove(channel_id); - delete self.mapping[partner_id]; - // if unpin current channel, switch to inbox - if(self.get('current_channel_id') === channel_id){ - self.set('current_channel_id', 'channel_inbox'); - } - }); - }, - on_click_button_minimize: function(event){ - event.preventDefault(); - var current_channel = this.channels[this.get('current_channel_id')]; - if(current_channel){ - var channel_uuid = current_channel.uuid; - return this.ChannelModel.call("channel_minimize", [channel_uuid, true]); - } - return $.Deferred().reject(); - }, - on_click_button_invite: function(){ - new PartnerInviteDialog(this, { - title: _.str.sprintf(_t('Invite people to %s'), this.get_current_channel_name()), - channel: this.channels[this.get('current_channel_id')], - }).open(); - }, - on_click_button_unsubscribe: function(){ - var self = this; - this.ChannelModel.call('action_unfollow', [[this.get('current_channel_id')]]).then(function(){ - var channel = self.channels[self.get('current_channel_id')]; - var slot = self.get_channel_slot(channel); - self.set(slot, _.filter(self.get(slot), function(c){ return c.id !== channel.id; })); - self.do_notify(_t('Unsubscribe'), _.str.sprintf(_t('You unsubscribe from %s.'), self.get_current_channel_name())); - self.set('current_channel_id', 'channel_inbox'); // jump to inbox - }); - }, - on_click_button_settings: function(){ - this.do_action({ - type: 'ir.actions.act_window', - res_model: "mail.channel", - res_id: this.get('current_channel_id'), - views: [[false, 'form']], - target: 'current' - }, { - 'on_reverse_breadcrumb': this.on_reverse_breadcrumb, - }); - }, - on_message_needaction: function(event){ - var self = this; - mail_thread.MailThreadMixin.on_message_needaction.call(this, event).then(function(message_id){ - // decrement the needaction top counter - internal_bus.trigger('mail_needaction_done', 1); - // decrement the channel labels - var treated_messages = _.filter(self.get('messages'), function(m){ return m.id == message_id; }); - if(treated_messages.length){ - self.needaction_decrement(treated_messages[0].channel_ids || []); - } - }); - }, - // control panel - cp_update: function(){ - // toggle cp elements - var current_channel_id = this.get('current_channel_id'); - if(_.isString(current_channel_id)){ - this.control_elements.$buttons.hide(); - this.control_elements.$sidebar.hide(); - }else{ - this.control_elements.$buttons.show(); - if(this.channels[current_channel_id].channel_type === 'chat'){ - this.control_elements.$buttons.find('.o_mail_chat_button_invite').hide(); - this.control_elements.$sidebar.hide(); - } else { - this.control_elements.$buttons.find('.o_mail_chat_button_invite').show(); - this.control_elements.$sidebar.show(); - } - } - - // update control panel - var status = { - breadcrumbs: this.action_manager.get_breadcrumbs(), - cp_content: { - $buttons: this.control_elements.$buttons, - $searchview: this.control_elements.$searchview, - $searchview_buttons: this.control_elements.$searchview_buttons, - $sidebar: this.control_elements.$sidebar, - }, - searchview: this.searchview, - }; - this.update_control_panel(status); - }, - cp_render_elements: function() { - this.control_elements.$buttons = $(QWeb.render("mail.chat.ControlButtons", {})); - this.control_elements.$buttons.on('click', '.o_mail_chat_button_invite', _.bind(this.on_click_button_invite, this)); - this.control_elements.$buttons.on('click', '.o_mail_chat_button_minimize', _.bind(this.on_click_button_minimize, this)); - - var $sidebar_container = $('
    '); - new Sidebar(this, { - sections: [ - {name: 'more', label: _t('More')}, - ], - items: { - more: [ - {label: _t('Unsubscribe'), classname: 'o_mail_chat_button_unsubscribe', callback: this.on_click_button_unsubscribe}, - {label: _t('Settings'), classname: 'o_mail_chat_button_settings', callback: this.on_click_button_settings}, - ], - }, - }).appendTo($sidebar_container); - this.control_elements.$sidebar = $sidebar_container.contents(); - }, - cp_render_searchview: function(){ - var self = this; - var options = { - $buttons: $("
    "), - action: this.action, - disable_groupby: true, - }; - var view_id = (this.action && this.action.search_view_id && this.action.search_view_id[0]) || false; - this.searchview = new SearchView(this, this.MessageDatasetSearch, view_id, {}, options); - - this.searchview.on('search_data', this, this.on_search); - return $.when(this.searchview.appendTo($("
    "))).done(function() { - self.control_elements.$searchview = self.searchview.$el; - self.control_elements.$searchview_buttons = self.searchview.$buttons.contents(); - }); - }, - /** - * Method call when action if done after redirecting (using 'on_click_redirect') - * @override - */ - on_reverse_breadcrumb: function(){ - var current_channel_id = this.get('current_channel_id'); - this.cp_update(); // do not reload the client action, just display it, but a refresh of the control panel is needed. - // push state - this.action_manager.do_push_state({ - action: this.action.id, - active_id: current_channel_id, - }); - }, - // messages search methods - on_search: function(domains, contexts, groupbys){ - var self = this; - return pyeval.eval_domains_and_contexts({ - domains: [[]].concat(domains || []), - contexts: [this.context].concat(contexts || []), - group_by_seq: groupbys || [] - }).done(function(results){ - if (results.error) { - throw new Error(_.str.sprintf(_t("Failed to evaluate search criterions")+": \n%s", JSON.stringify(results.error))); - } - // modify the search domain and do search - self.search_domain = results.domain; - return self.message_load_new(); - }); - }, - // channels - /** - * Set the channel slot - * @param {Object[]} fetch_result : should contains only the slot name (as key) and the list of channel header as value. - */ - _channel_slot: function(fetch_result){ - var self = this; - var channel_slots = _.keys(fetch_result); - _.each(channel_slots, function(slot){ - // update the channel slot - self.set(slot, fetch_result[slot]); - // flatten the result : update the complete channel list - _.each(fetch_result[slot], function(channel){ - self.channels[channel.id] = channel; - }); - }); - }, - /** - * Apply a channel means adding it, and swith to it - * @param {Object} channel : channel header - */ - channel_apply: function(channel){ - this.channel_add(channel); - this.set('current_channel_id', channel.id); - }, - /** - * Add the given channel, or update it if already exists and loaded - * @param {Object} channel : channel header to add - */ - channel_add: function(channel){ - var channel_slot = this.get_channel_slot(channel); - var existing = this.get(channel_slot); - if(_.contains(_.pluck(existing, 'id'), channel.id)){ - // update the old channel - var filtered_channels = _.filter(existing, function(item){ return item.id != channel.id; }); - this.set(channel_slot, filtered_channels.concat([channel])); - }else{ - // simply add the reveiced channel - this.set(channel_slot, existing.concat([channel])); - } - // also update the flatten list - this.channels[channel.id] = channel; - - // update the mapping for 'direct message' channel, and the partner list - if(channel_slot === 'channel_direct_message'){ - var partner = channel.direct_partner[0]; - this.mapping[partner.id] = channel.id; - this.partner_add(partner); - } - }, - channel_remove: function(channel_id){ - var channel = this.channels[channel_id]; - var slot = this.get_channel_slot(channel); - this.set(slot, _.filter(this.get(slot), function(c){ return c.id !== channel_id; })); - delete this.channels[channel_id]; - }, - /** - * Get the channel the current user has with the given partner, and get the channel header - * @param {Number[]} partner_ids : list of res.partner identifier - */ - channel_get: function(partner_ids){ - var self = this; - return this.ChannelModel.call('channel_get', [partner_ids]).then(function(channel){ - self.channel_apply(channel); - }); - }, - /** - * Create a channel with the given name and type, and apply it - * @param {String} channel_name : the name of the channel - * @param {String} privacy : the privacy of the channel (groups, public, ...) - */ - channel_create: function(channel_name, privacy){ - var self = this; - return this.ChannelModel.call('channel_create', [channel_name, privacy]).then(function(channel){ - self.channel_apply(channel); - }); - }, - channel_info: function(channel_id){ - return this.ChannelModel.call('channel_info', [[channel_id]]).then(function(channels){ - return channels[0]; - }); - }, - channel_pin: function(uuid, pinned){ - return this.ChannelModel.call('channel_pin', [uuid, pinned]); - }, - channel_join_and_get_info: function(channel_id){ - return this.ChannelModel.call('channel_join_and_get_info', [[channel_id]]); - }, - channel_change: function(){ - var self = this; - var current_channel_id = this.get('current_channel_id'); - // mail chat compose message (destroy and replace it) - if (this.mail_chat_compose_message) { - this.mail_chat_compose_message.destroy(); - } - this.mail_chat_compose_message = new mail_thread.MailComposeMessage(this, new data.DataSetSearch(this, 'mail.channel', this.context), { - 'emoji_list': this.options.emoji_list, - 'context': _.extend(this.context, { - 'default_res_id': current_channel_id, - }), - 'display_mode': 'chat', - }); - if(_.isString(current_channel_id)){ - this.$('.o_mail_chat_composer_wrapper').hide(); - }else{ - this.$('.o_mail_chat_composer_wrapper').show(); - } - this.mail_chat_compose_message.appendTo(this.$('.o_mail_chat_composer_wrapper')); - this.mail_chat_compose_message.focus(); - - // push state (the action is referred by action_manager, and no reloaded when jumping - // channel, so update context is required) - this.action.context.active_id = current_channel_id; - this.action.context.active_ids = [current_channel_id]; - web_client.action_manager.do_push_state({ - action: this.action.id, - active_id: current_channel_id, - }); - - // update title (to display in the breadcrumbs) according to current channel - this.set('title', this.get_current_channel_name()); - - // update control panel - this.cp_update(); - - // fetch the messages - return this.message_load_new().then(function(){ - // if normal channel, update last message id, and unbold - var def = $.Deferred().resolve(); - if(_.isNumber(current_channel_id)){ - def = self.ChannelModel.call('channel_seen', [[current_channel_id]]); - }else{ - // auto treat needaction from inbox - if(current_channel_id === 'channel_inbox' && self.chatter_needaction_auto){ - def = self.MessageDatasetSearch._model.query(['id', 'channel_ids']).filter([['needaction', '=', true]]).all().then(function(messages){ - // get needaction message ids - var message_ids = _.pluck(messages, 'id'); - if(message_ids){ - // call to unlink - self.MessageDatasetSearch._model.call('set_message_done',[message_ids]).then(function(){ - // decrement channel items in batch - var flatten_channel_ids = _.flatten(_.pluck(messages, 'channel_ids')); - var channel_count = _.countBy(flatten_channel_ids, function(cid) { - return cid; - }); - _.each(_.keys(channel_count), function(cid){ - self.needaction_decrement([cid], channel_count[cid]); - }); - // decrement the needaction top counter - internal_bus.trigger('mail_needaction_done', message_ids.length); - }); - } - }); - } - } - def.then(function(){ - self._toggle_unread_message(current_channel_id, false, true); - self._scroll(); - }); - }); - }, - channel_render: function(channel_slot){ - this.$('.o_mail_chat_channel_slot_' + channel_slot).replaceWith(QWeb.render("mail.chat.ChatMailThread.channels", {'widget': this, 'channel_slot': channel_slot})); - }, - // partners - partner_add: function(partner){ - var partners = _.filter(this.get('partners'), function(p){ return p.id != partner.id; }); - this.set('partners', partners.concat([partner])); - }, - partner_render: function(){ - this.$('.o_mail_chat_channel_slot_partners').replaceWith(QWeb.render("mail.chat.ChatMailThread.partners", {'widget': this})); - }, - // needaction - needaction_increment: function(channel_ids, inc){ - var self = this; - _.each(channel_ids, function(channel_id){ - if(self.channels[channel_id]){ - var count = (self.channels[channel_id].message_needaction_counter || 0) + (inc || 1); - self.channels[channel_id].message_needaction_counter = count; - self.$('.o_mail_chat_needaction[data-channel-id='+channel_id+']').html(count); - self.$('.o_mail_chat_needaction[data-channel-id='+channel_id+']').show(); - } - }); - }, - needaction_decrement: function(channel_ids, dec){ - var self = this; - _.each(channel_ids, function(channel_id){ - if(self.channels[channel_id]){ - var count = (self.channels[channel_id].message_needaction_counter || 0) - (dec || 1); - self.channels[channel_id].message_needaction_counter = count; - self.$('.o_mail_chat_needaction[data-channel-id='+channel_id+']').html(count); - if(count <= 0){ - self.$('.o_mail_chat_needaction[data-channel-id='+channel_id+']').show(); - } - } - }); - }, - needaction_inbox_change: function(){ - var count = this.get('needaction_inbox_counter'); - var $elem = this.$('.o_mail_chat_needaction[data-channel-id="channel_inbox"]'); - $elem.html(count); - if(count > 0){ - $elem.show(); - }else{ - $elem.hide(); - } - }, - // from bus - channel_receive: function(channel){ - this.channel_add(channel); - }, - message_receive: function(message){ - var self = this; - var in_current_channel = _.contains(message.channel_ids, this.get('current_channel_id')); - // if current channel should reveice message, give it to it - if(in_current_channel){ - this.message_insert([message]); - } - // for other message channel, get the channel if not loaded yet, and bolded them - var other_message_channel_ids = _.without(message.channel_ids, this.get('current_channel_id')); - var active_channel_ids = _.map(_.keys(this.channels), function(cid){ - return parseInt(cid); - }); // integer as key of a dict is cast as string in javascript - var channel_to_fetch = _.difference(other_message_channel_ids, active_channel_ids); - // fetch unloaded channels and add it - var def = $.Deferred(); - if(channel_to_fetch.length >= 1){ - def = this.ChannelModel.call("channel_info", [], {"ids" : channel_to_fetch}).then(function(channels){ - _.each(channels, function(channel){ - self.channel_add(channel); - }); - }); - }else{ - def.resolve([]); - } - // bold the channel to indicate unread messages - def.then(function(){ - // bold channel having unread messages - _.each(other_message_channel_ids, function(channel_id){ - self._toggle_unread_message(channel_id, true); - }); - // auto scroll to bottom if the message arrived in the current channel - if(in_current_channel){ - self._scroll(); - } - }); - // if needaction, then increment the label - if(message.needaction_partner_ids && _.contains(message.needaction_partner_ids, session.partner_id)){ - this.needaction_increment(message.channel_ids || []); - } - }, - // utils function - get_channel_slot: function(channel){ - if(channel.channel_type === 'channel'){ - if(channel.public === 'private'){ - return 'channel_private_group'; - } - return 'channel_channel'; - } - if(channel.channel_type === 'chat'){ - return 'channel_direct_message'; - } - }, - get_current_channel_name: function(){ - var current_channel_id = this.get('current_channel_id'); - var current_channel = this.channels[current_channel_id]; - var current_channel_name = current_channel && current_channel.name || _t('Unknown'); - // virtual channel id (for inbox, or starred channel) - if(_.isString(current_channel_id)){ - if(current_channel_id == 'channel_inbox'){ - current_channel_name = _t('Inbox'); - } - if(current_channel_id == 'channel_starred'){ - current_channel_name = _t('Starred'); - } - } - return current_channel_name; - }, - _toggle_unread_message: function(channel_id, add_or_remove, set_as_active){ - var partner_id = false; - var inverse_mapping = _.invert(this.mapping); - // toggle bold channel/partner item - this.$('.o_mail_chat_sidebar .o_mail_chat_channel_item[data-channel-id="'+channel_id+'"]').toggleClass('o_mail_chat_channel_unread', add_or_remove); - if(_.contains(_.keys(inverse_mapping), channel_id.toString())){ - partner_id = parseInt(inverse_mapping[channel_id]); - this.$('.o_mail_chat_sidebar .o_mail_chat_partner_item[data-partner-id="'+partner_id+'"]').toggleClass('o_mail_chat_channel_unread', add_or_remove); - } - // set the channel/partner item as the active one - if(set_as_active){ - this.$('.o_mail_chat_sidebar .o_mail_chat_channel_item, .o_mail_chat_sidebar .o_mail_chat_partner_item').find('a').removeClass('active'); - this.$('.o_mail_chat_sidebar .o_mail_chat_channel_item[data-channel-id="'+channel_id+'"]').find('a').addClass('active'); - if(partner_id){ - this.$('.o_mail_chat_sidebar .o_mail_chat_partner_item[data-partner-id="'+partner_id+'"]').find('a').addClass('active'); - } - } - }, - _scroll: function(){ - var current_channel_id = this.get('current_channel_id'); - var current_channel = this.channels[current_channel_id]; - var last_seen_message; - if(_.isNumber(current_channel_id)){ - if(current_channel.seen_message_id){ - last_seen_message = this.$(".o_mail_thread_message[data-message-id="+current_channel.seen_message_id+"]"); - if(last_seen_message.length){ - this.$messages.scrollTop(last_seen_message.offset().top); - }else{ - this.$messages.scrollTop(0); - } - }else{ - this.$messages.scrollTop(this.$messages[0].scrollHeight); - } - current_channel.seen_message_id = false; - }else{ - this.$messages.scrollTop(this.$messages[0].scrollHeight); - } - }, - /** - * Render the messages - * @override - */ - message_render: function(){ - this.$messages.html(QWeb.render('mail.chat.ChatMailThread.content', {'widget': this})); - }, - /** - * Return the message domain for the current channel - * @override - */ - get_message_domain: function(){ - // default channel domain - var current_channel_id = this.get('current_channel_id'); - var domain = [['channel_ids', 'in', current_channel_id]]; - // virtual channel id (for inbox, or starred channel) - if(_.isString(current_channel_id)){ - if(current_channel_id == 'channel_inbox'){ - domain = [['needaction', '=', true]]; - } - if(current_channel_id == 'channel_starred'){ - domain = [['starred', '=', true]]; - } - } - // add search domain - domain = domain.concat(this.search_domain); - return domain; - }, - /** - * Extend message_load_history to keep scroll position at last seen message - */ - message_load_history: function() { - var self = this; - var oldest_msg_selector = '.o_mail_thread_message[data-message-id="' + this.get('messages')[0].id + '"]'; - var offset = -framework.getPosition(document.querySelector(oldest_msg_selector)).top; - mail_thread.MailThreadMixin.message_load_history.call(this).then(function() { - offset += framework.getPosition(document.querySelector(oldest_msg_selector)).top; - self.$messages.scrollTop(offset); - }); - }, -}); - -core.action_registry.add('mail.chat.instant_messaging', ChatMailThread); - - -return { - ChatMailThread: ChatMailThread, -}; - -}); diff --git a/addons/mail/static/src/js/mail_chat_common.js b/addons/mail/static/src/js/mail_chat_common.js deleted file mode 100644 index dd3b1801b31..00000000000 --- a/addons/mail/static/src/js/mail_chat_common.js +++ /dev/null @@ -1,412 +0,0 @@ -odoo.define('mail.chat_common', function (require) { -"use strict"; - -var bus = require('bus.bus'); -var core = require('web.core'); -var session = require('web.session'); -var time = require('web.time'); -var Widget = require('web.Widget'); -var mail_utils = require('mail.utils'); - -var _t = core._t; -var QWeb = core.qweb; - -var NBR_LIMIT_HISTORY = 20; - - -/** - * Widget handeling the sessions/conversations - * - * Responsible to listen the bus and do the correct action according to the received notifications (fetch - * conversation, add message, ...) - **/ -var ConversationManager = Widget.extend({ - init: function(parent, options) { - this._super(parent); - this.options = _.clone(options) || {}; - _.defaults(this.options, { - placeholder: _t("Say something..."), - default_username: _t("Anonymous"), - load_history: true, - focus: false, - }); - this.emoji_list = []; - // business - this.sessions = {}; - this.bus = bus.bus; - this.bus.on("notification", this, this.on_notification); - // ui - this.set("right_offset", 0); - this.set("bottom_offset", 0); - this.on("change:right_offset", this, this.calc_positions); - this.on("change:bottom_offset", this, this.calc_positions); - - this.set("waiting_messages", 0); - this.on("change:waiting_messages", this, this.window_title_change); - $(window).on("focus", _.bind(this.window_focus, this)); - this.window_title_change(); - // session widgets deferred (for their creation) - this._session_defs = {}; - }, - start: function(){ - var self = this; - return $.when(session.rpc("/mail/chat_init"), this._super.apply(this, arguments)).then(function(result){ - self._setup(result); - }); - }, - _setup: function(init_data){ - this.emoji_list = init_data.emoji; - this.options.emoji = mail_utils.shortcode_substitution(init_data.emoji); - }, - on_notification: function(notification, options) { - var self = this; - var channel = notification[0]; - var message = notification[1]; - var regex_uuid = new RegExp(/(\w{8}(-\w{4}){3}-\w{12}?)/g); - var is_channel_uuid = regex_uuid.test(channel); - - // (dbname, 'res.partner', partner_id) receive only the new mail.channel header - if((Array.isArray(channel) && (channel[1] === 'res.partner')) || is_channel_uuid){ - // activate the received session - if(message.uuid){ - this.session_apply(message, options); - } - } - // (dbname, 'mail.channel', channel_id) receive only the mail.message of the channel - if((Array.isArray(channel) && (channel[1] === 'mail.channel')) || is_channel_uuid){ - // message to display in the chatview - if (message.message_type) { - self.message_receive(message); - } - } - }, - // window focus unfocus beep and title - window_focus: function() { - this.set("waiting_messages", 0); - }, - window_beep: function() { - mail_utils.beep(session); - }, - window_title_change: function() { - if (this.get("waiting_messages") !== 0) { - this.window_beep(); - } - }, - // sessions methods - session_apply: function(session, options){ - // options used by this function : focus (load_history can be usefull) - var self = this; - options = _.defaults(options || {}, { - 'focus': true, - }, this.options); - // create/get the conversation widget - var conv = this.sessions[session.id]; - if (! conv) { - if(session.state !== 'closed'){ - conv = new Conversation(this, session, options); - // store this deferred to apply next action (notifications) when it is resolved. - this._session_defs[session.id] = conv.appendTo($("body")); - this._session_defs[session.id].then(function() { - self.calc_positions(); - }); - conv.on("destroyed", this, _.bind(this.session_delete, this)); - this.sessions[session.id] = conv; - } - }else{ - // update the state only when the 'contruction' deferred is resolved. - this._session_defs[session.id].then(function(){ - conv.set('session', session); - }); - } - return conv; - }, - session_delete: function(channel_id){ - delete this.sessions[channel_id]; - this.calc_positions(); - }, - /** - * Adding a message in all its channels - * /!\ Suppose the channel are already loaded in the ConversationManager /!\ - * @param {Object} message : the received message (formatted server side) - */ - message_receive: function(message){ - var self = this; - _.each(message.channel_ids, function(channel_id){ - self.sessions[channel_id].message_receive(message); - }); - }, - // others - calc_positions: function() { - var self = this; - var current = this.get("right_offset"); - _.each(this.sessions, function(s) { - s.set("bottom_position", self.get("bottom_offset")); - s.set("right_position", current); - current += s.$().outerWidth(true); - }); - }, - destroy: function() { - $(window).off("focus", this.window_focus); - $(window).off("blur", this.window_blur); - return this._super(); - } -}); - -/** - * Widget Conversation - * - * Display the conversation in a 'portable' window, manage sending and receiving message, - * and the fold state of the conversation. - **/ -var Conversation = Widget.extend({ - template: "mail.chat.im.Conversation", - events: { - "keydown input": "keydown", - "click .o_mail_chat_im_window_close": "on_click_close", - "click .o_mail_chat_im_window_header": "on_click_header" - }, - init: function(parent, session, options) { - this._super(parent); - this.options = _.defaults(options || {}, { - 'emoji': {}, - 'default_username': _t('Anonymous'), - 'placeholder': _t('Write something ...'), - 'loading_history': true, - 'focus': true, - }); - this.set("messages", []); - this.set("session", session); - this.set("pending", 0); - this.set("right_position", 0); - this.set("bottom_position", 0); - }, - willStart: function(){ - if(this.options.loading_history){ - return $.when(this.message_history(), this._super.apply(this, arguments)); - } - return this._super.apply(this, arguments); - }, - start: function() { - var self = this; - // ui - self.on("change:right_position", self, self.calc_pos); - self.on("change:bottom_position", self, self.calc_pos); - self.on("change:pending", self, self.message_pending_change); - - self.full_height = self.$().height(); - self.$('.o_mail_chat_im_window_content').on('scroll',function(){ - if($(this).scrollTop() === 0){ - self.message_history(); - } - }); - // business - self.on("change:session", self, self.session_change); - self.on("change:messages", self, self.message_render); - - return self._super.apply(this, arguments).then(function(){ - // place the conversation window correctly on the screen - self.calc_pos(); - // focus if needed - if(self.options.focus){ - self.focus(); - } - // re-apply the session to display the correct state - self.set('session', _.clone(self.get('session'))); - self._go_bottom(); - }); - }, - // ui - show: function(){ - this.$().animate({ - height: this.full_height - }); - this.set("pending", 0); - }, - hide: function(){ - this.$().animate({ - height: this.$(".o_mail_chat_im_window_header").outerHeight() - }); - }, - calc_pos: function() { - this.$().css("right", this.get("right_position")); - this.$().css("bottom", this.get("bottom_position")); - }, - // session - /** - * Set the fold state LOCALLY for the session. This will - * trigger a ui update - * @param {string} state : the fold state ('fold', 'closed' or 'open') - */ - session_fold: function(state){ - var session = this.get('session'); - session.state = state; - this.set('session', session); - }, - /** - * Triggered when the session has changed : it update the header name - * and fold the window (ui) according to its state. - */ - session_change: function(){ - // update conversation name in header - var name = this.get("session").name || _t('Unknown Channel'); - this.$(".o_mail_chat_im_window_header_name").text(name); - this.$(".o_mail_chat_im_window_header_name").attr('title', name); - // update the fold state - if(this.get("session").state){ - if(this.get("session").state === 'closed'){ - this.destroy(); - }else{ - if(this.get("session").state === 'open'){ - this.show(); - }else{ - this.hide(); - } - } - } - }, - // messages - /** - * Load the history of the current session, and insert it into already loaded messages - * @returns {Deferred} resolved with the new loaded messages (Object[]) - */ - message_history: function(){ - var self = this; - if(this.options.loading_history){ - var data = { - uuid: self.get("session").uuid, - limit: NBR_LIMIT_HISTORY, - last_id: _.first(this.get("messages")) ? _.first(this.get("messages")).id : false, - }; - return session.rpc("/mail/chat_history", data).then(function(messages){ - if(messages){ - self.message_insert(messages); - if(messages.length != NBR_LIMIT_HISTORY){ - self.options.loading_history = false; - } - }else{ - self.options.loading_history = false; - } - return messages; - }); - } - return $.Deferred().resolve([]); - }, - message_receive: function(message) { - // don't add duplicated message (this can happen when refreshing and receiving already recieved - // message that are still in the bus queue/buffer). - if(!_.contains(_.pluck(this.get('messages'), 'id'), message.id)){ - // is pending message ? - if (this.get('session').state === 'open') { - this.set("pending", 0); - } else { - if(!this.message_am_i_author(message)){ - this.set("pending", this.get("pending") + 1); - } - } - // insert it - this.message_insert([message]); - this._go_bottom(); - } - }, - message_send: function(message) { - var self = this; - var send_it = function() { - return session.rpc("/mail/chat_post", {uuid: self.get("session").uuid, message_content: message}); - }; - var tries = 0; - send_it().fail(function(error, e) { - e.preventDefault(); - tries += 1; - if (tries < 3) - return send_it(); - }); - }, - /** - * Preporcess the message (add informations in the Object, ...) - * @param {Object[]} messages : server side formatted message (from message_format method) - */ - _message_preprocess: function(raw_messages){ - var self = this; - _.each(raw_messages, function(m){ - m.date = moment(time.str_to_datetime(m.date)).format('YYYY-MM-DD HH:mm:ss'); // set the date in the correct browser user timezone - if(m.body){ - m.body = mail_utils.shortcode_apply(m.body, self.options.emoji); - } - _.each(m.attachment_ids, function(a){ - a.url = '/web/content/' + a.id + '?download=true'; - a.fa_class = mail_utils.attachment_mimetype_icon(a.mimetype); - }); - }); - return raw_messages; - }, - /** - * Insert new raw message into the current ones - * @param {Object[]} messages : server side formatted message (from message_format method) - */ - message_insert: function(raw_messages){ - var messages = this._message_preprocess(raw_messages); - this.set("messages", _.sortBy(this.get("messages").concat(messages), function(m){ return m.id; })); - }, - message_render: function(){ - var message_html = $(QWeb.render("mail.chat.im.Conversation.messages", {"widget": this})); - this.$('.o_mail_chat_im_window_content').html(message_html); - }, - message_am_i_author: function(message){ - return message.author_id && message.author_id[0] === session.partner_id; - }, - message_pending_change: function(){ - if (this.get("pending") === 0) { - this.$(".o_mail_chat_im_window_header_counter").text(""); - } else { - this.$(".o_mail_chat_im_window_header_counter").text("(" + this.get("pending") + ")"); - } - }, - // utils and event - _go_bottom: function() { - if(this.$(".o_mail_chat_im_window_content").length){ - this.$(".o_mail_chat_im_window_content").scrollTop(this.$(".o_mail_chat_im_window_content").get(0).scrollHeight); - } - }, - focus: function() { - this.$(".o_mail_chat_im_window_input").focus(); - }, - keydown: function(e) { - if(e && e.which == 27) { - if(this.$el.prev().find('.o_mail_chat_im_window_input').length > 0){ - this.$el.prev().find('.o_mail_chat_im_window_input').focus(); - }else{ - this.$el.next().find('.o_mail_chat_im_window_input').focus(); - } - e.stopPropagation(); - this.session_fold('closed'); - } - if(e && e.which !== 13) { - return; - } - var mes = this.$("input").val(); - if (! mes.trim()) { - return; - } - this.$("input").val(""); - this.message_send(mes); - }, - on_click_header: function(){ - this.session_fold(); - }, - on_click_close: function(event) { - event.stopPropagation(); // avoid triggering event for 'on_click_header' - this.session_fold('closed'); - }, - destroy: function() { - this.trigger("destroyed", this.get('session').id); - return this._super(); - } -}); - - -return { - Conversation: Conversation, - ConversationManager: ConversationManager, -}; - -}); diff --git a/addons/mail/static/src/js/mail_chatter.js b/addons/mail/static/src/js/mail_chatter.js deleted file mode 100644 index 8fbe8c1ea05..00000000000 --- a/addons/mail/static/src/js/mail_chatter.js +++ /dev/null @@ -1,459 +0,0 @@ -odoo.define('mail.chatter', function (require) { -"use strict"; - -var core = require('web.core'); -var web_client = require('web.web_client'); -var Widget = require('web.Widget'); -var form_common = require('web.form_common'); -var SystrayMenu = require('web.SystrayMenu'); -var mail_utils = require('mail.utils'); -var mail_thread = require('mail.thread'); - -var QWeb = core.qweb; -var _t = core._t; -var internal_bus = core.bus; - - -/** - * Mail Compose Message for Chatter - * - * Extends the basic MailComposeMessage Widget to add 'suggested partner' layer (open popup - * when suggested partner is selected without email, or other informations), and the button - * to open the 'mail.compose.message' wizard. - **/ -var ChatterMailComposeMessage = mail_thread.MailComposeMessage.extend({ - template: 'mail.chatter.ComposeMessage', - init: function(parent, dataset, options){ - this._super.apply(this, arguments); - this.suggested_partners = []; - this.context = options.context || {}; - this.options = _.defaults(options, { - 'record_name': false, - 'is_log': false, - 'internal_subtypes': [], - }); - this.events = _.extend(this.events, { - 'click .o_mail_compose_message_button_full_message': 'on_compose_message', - }); - }, - willStart: function(){ - if(this.options.is_log){ - return this._super.apply(this, arguments); - } - return $.when(this._super.apply(this, arguments), this.message_get_suggested_recipients()); - }, - start: function(){ - this.$input = this.$('.o_mail_compose_message_input'); - autosize(this.$input); - this.$input.focus(); - return this._super.apply(this, arguments); - }, - message_get_suggested_recipients: function(){ - var self = this; - var email_addresses = _.pluck(this.suggested_partners, 'email_address'); - return this.thread_dataset.call('message_get_suggested_recipients', [[this.context.default_res_id], this.context]).done(function (suggested_recipients) { - var thread_recipients = suggested_recipients[self.context.default_res_id]; - _.each(thread_recipients, function (recipient) { - var parsed_email = mail_utils.parse_email(recipient[1]); - if (_.indexOf(email_addresses, parsed_email[1]) == -1) { - self.suggested_partners.push({ - 'checked': true, - 'partner_id': recipient[0], - 'full_name': recipient[1], - 'name': parsed_email[0], - 'email_address': parsed_email[1], - 'reason': recipient[2], - }); - } - }); - }); - }, - /** - * Get the list of selected suggested partner - * @returns Array() : list of 'recipient' selected partner (may not be created in db) - **/ - get_checked_suggested_partners: function(){ - var self = this; - var checked_partners = []; - this.$('.o_mail_chatter_compose_message_suggested_partners input:checked').each(function(){ - var $input = $(this); - var full_name = $input.data('fullname'); - checked_partners = checked_partners.concat(_.filter(self.suggested_partners, function(item){ - return full_name === item.full_name; - })); - }); - return checked_partners; - }, - /** - * Check the additionnal partners (not necessary registered partners), and open a popup form view - * for the ones who informations is missing. - * @param Array : list of 'recipient' partners to complete informations or validate - * @returns Deferred resolved with the list of checked suggested partners (real partner) - **/ - check_suggested_partners: function (checked_suggested_partners) { - var self = this; - var check_done = $.Deferred(); - - var recipients = _.filter(checked_suggested_partners, function (recipient) { return recipient.checked; }); - var recipients_to_find = _.filter(recipients, function (recipient) { return (! recipient.partner_id); }); - var names_to_find = _.pluck(recipients_to_find, 'full_name'); - var recipients_to_check = _.filter(recipients, function (recipient) { return (recipient.partner_id && ! recipient.email_address); }); - var recipient_ids = _.pluck(_.filter(recipients, function (recipient) { return recipient.partner_id && recipient.email_address; }), 'partner_id'); - - var names_to_remove = []; - var recipient_ids_to_remove = []; - - // have unknown names -> call message_get_partner_info_from_emails to try to find partner_id - var find_done = $.Deferred(); - if (names_to_find.length > 0) { - find_done = self.thread_dataset.call('message_partner_info_from_emails', [[this.context.default_res_id], names_to_find]); - } else { - find_done.resolve([]); - } - - // for unknown names + incomplete partners -> open popup - cancel = remove from recipients - $.when(find_done).pipe(function (result) { - var emails_deferred = []; - var recipient_popups = result.concat(recipients_to_check); - - _.each(recipient_popups, function (partner_info) { - var deferred = $.Deferred(); - emails_deferred.push(deferred); - - var partner_name = partner_info.full_name; - var partner_id = partner_info.partner_id; - var parsed_email = mail_utils.parse_email(partner_name); - - var pop = new form_common.FormViewDialog(this, { - res_model: 'res.partner', - res_id: partner_id, - context: { - 'force_email': true, - 'ref': "compound_context", - 'default_name': parsed_email[0], - 'default_email': parsed_email[1], - }, - title: _t("Please complete partner's informations"), - }).open(); - pop.on('closed', self, function () { - deferred.resolve(); - }); - pop.view_form.on('on_button_cancel', self, function () { - names_to_remove.push(partner_name); - if (partner_id) { - recipient_ids_to_remove.push(partner_id); - } - }); - }); - $.when.apply($, emails_deferred).then(function () { - var new_names_to_find = _.difference(names_to_find, names_to_remove); - find_done = $.Deferred(); - if (new_names_to_find.length > 0) { - find_done = self.thread_dataset.call('message_partner_info_from_emails', [[self.context.default_res_id], new_names_to_find, true]); - } else { - find_done.resolve([]); - } - $.when(find_done).pipe(function (result) { - var recipient_popups = result.concat(recipients_to_check); - _.each(recipient_popups, function (partner_info) { - if (partner_info.partner_id && _.indexOf(partner_info.partner_id, recipient_ids_to_remove) == -1) { - recipient_ids.push(partner_info.partner_id); - } - }); - }).pipe(function () { - check_done.resolve(recipient_ids); - }); - }); - }); - return check_done; - }, - message_post: function(body, attachment_ids, values){ - var self = this; - var _super = self._super; - // default values - values = _.defaults(values || {}, { - 'message_type': 'comment', - 'content_subtype': 'html', - 'partner_ids': [], - 'subtype_id': false, - 'subtype': false, - }); - // update subtype - if(this.options.is_log) { - var subtype_id = parseInt(this.$('.o_mail_chatter_compose_message_subtype_select').val()); - if(_.indexOf(_.pluck(this.options.internal_subtypes, 'id'), subtype_id) == -1) { - values.subtype = 'mail.mt_note'; - }else{ - values.subtype_id = subtype_id; - } - }else{ - values.subtype = 'mail.mt_comment'; - } - // update partner_ids - var def = $.when(); - if(this.options.is_log){ - // directly call the super() - def = _.bind(_super, self)(body, attachment_ids, values); - }else{ - // call the super() when all partner popup are treated - var checked_suggested_partners = this.get_checked_suggested_partners(); - this.check_suggested_partners(checked_suggested_partners).done(function(partner_ids){ - // update context - values.partner_ids = partner_ids; - values.context = _.defaults(self.context, { - 'mail_post_autofollow': true, - 'mail_post_autofollow_partner_ids': partner_ids, - }); - def = _.bind(_super, self)(body, attachment_ids, values); - }); - } - return def; - }, - is_empty: function(){ - return this.$input.val() === ''; - }, - on_compose_message: function(){ - var self = this; - if (!this.do_check_attachment_upload()){ - return false; - } - - var recipient_done = $.Deferred(); - if (this.options.is_log){ - recipient_done.resolve([]); - }else{ - var checked_suggested_partners = this.get_checked_suggested_partners(); - recipient_done = this.check_suggested_partners(checked_suggested_partners); - } - recipient_done.then(function(partner_ids){ - var context = { - 'default_parent_id': self.id, - 'default_body': mail_utils.get_text2html(self.$('.o_mail_compose_message_input').val()), - 'default_attachment_ids': _.pluck(self.get('attachment_ids'), 'id'), - 'default_partner_ids': partner_ids, - 'default_is_log': self.options.is_log, - 'mail_post_autofollow': true, - 'mail_post_autofollow_partner_ids': partner_ids, - }; - - if (self.context.default_model && self.context.default_res_id) { - context.default_model = self.context.default_model; - context.default_res_id = self.context.default_res_id; - } - - self.do_action({ - type: 'ir.actions.act_window', - res_model: 'mail.compose.message', - view_mode: 'form', - view_type: 'form', - views: [[false, 'form']], - target: 'new', - context: context, - }, { - on_close: function() { - self.trigger('need_refresh'); - }, - }); - }); - } -}); - - -/** - * Document Chatter ('mail_thread' widget) - * - * Displayed on every document. Since it is displayed on a form view, it extends 'AbstractField' widget, and - * required the behaviour of 'MailThreadMixin' to manage mail.message (using common methods). - * This widget is responsible of the toggle the message composer and the custom message rendering. - **/ -var ChatterMailThread = form_common.AbstractField.extend(mail_thread.MailThreadMixin, { - template: 'mail.chatter.ChatterMailThread', - events: { - "click .o_mail_redirect": "on_click_redirect", - "click .o_mail_thread_message_star": "on_message_star", - "click .o_mail_thread_message_needaction": "on_message_needaction", - "click .o_mail_thread_show_more": "message_load_history", - "click .o_mail_expand": "on_message_body_toggle", - // toggle message composer (!! declaration order is important !!) - "click .o_mail_chatter_button_new_message": "on_open_composer", - "click .o_mail_chatter_button_log_note": "on_open_composer", - "blur .o_mail_compose_message_input": "on_close_composer", - "mousedown .o_mail_chatter_compose_message": function() { - this.stay_open = true; - }, - "focus .o_mail_compose_message_input": function() { - this.stay_open = false; - }, - "keydown .o_mail_compose_message_input": function() { - this.stay_open = false; - }, - }, - init: function (){ - this._super.apply(this, arguments); - mail_thread.MailThreadMixin.init.call(this); - this.message_composer = undefined; - this.record_name = ''; - this.context = this.options.context || {}; - // options from 'message_ids' node in field_view_get - this.options = _.defaults(this.options, { - 'display_log_button': true, - 'display_document_link': false, - 'internal_subtypes': [], - 'emoji_list': [], - }); - this.emoji_set_substitution(this.options.emoji_list); - }, - start: function(){ - mail_thread.MailThreadMixin.start.call(this); - return this._super.apply(this, arguments); - }, - /** - * When a message is correctly posted, fetch its data to render it - * @param {Number} message_id : the identifier of the new posted message - * @returns {Deferred} - */ - on_message_post: function(message_id){ - var self = this; - return this.message_format([message_id]).then(function(messages){ - self.stay_open = false; - self.message_insert(messages); - self.on_close_composer(); - }); - }, - /** - * The value of the field has change (modification or switch to next document). Form view is - * not re-rendered, simply updated. A re-fetch is needed. - * @override - */ - render_value: function(){ - var self = this; - // re-rendering for mail composer message - this.all_messages_loaded = this.get_value().length <= mail_thread.LIMIT_MESSAGE; - this.renderElement(); - // reset dataset - this.ThreadDataset = this.view.dataset; - // update context (require for mail.compose.message wizard) - this.context = _.extend({ - 'mail_read_set_read': true, // set messages as read in Chatter TODO JEM : not used anymore - 'default_res_id': this.view.datarecord.id || false, - 'default_model': this.view.model || false, - }, this.options.context || {}); - // fetch messages of current document - var ids = this.get_value() || []; - return this.message_format(ids.slice(0, mail_thread.LIMIT_MESSAGE)).then(function(raw_messages){ - self._message_replace(self._message_preprocess(raw_messages)); - self.record_name = _.last(raw_messages) ? _.last(raw_messages).record_name : ''; - // set the thread as read - self.ThreadDataset.call('message_set_read', [[self.view.datarecord.id]]).then(function(message_ids){ - // decrement the needaction top counter - internal_bus.trigger('mail_needaction_done', message_ids.length); - }); - }); - }, - // composer toggle - on_open_composer: function(event){ - var self = this; - this.$('.o_mail_chatter_compose_message').show(); - // destroy existing composer - if(this.message_composer){ - this.message_composer.destroy(); - } - // create the new composer - this.message_composer = new ChatterMailComposeMessage(this, this.ThreadDataset, { - 'internal_subtypes': this.options.internal_subtypes, - 'is_log': this.$(event.currentTarget).hasClass('o_mail_chatter_button_log_note'), - 'emoji_list': this.options.emoji_list, - 'context': this.context, - 'record_name': this.record_name, - }); - this.message_composer.appendTo(this.$('.o_mail_chatter_compose_message')); - this.message_composer.on('message_sent', this, this.on_message_post); - this.message_composer.on('need_refresh', this, function(){ - self.message_refresh(); - self.on_close_composer(); - }); - }, - on_close_composer: function(){ - if(this.message_composer && this.message_composer.is_empty() && !this.stay_open){ - this.$('.o_mail_chatter_compose_message').hide(); - } - }, - /** - * Refresh the current message, and set them as current message. Use 'message_fetch', since its - * domain don't use 'ids', so if there is new messages, they'll be fetched. - * @returns {Deferred} - */ - message_refresh: function(){ - return this.message_load_new(); - }, - /** - * Render the messages - * @override - */ - message_render: function(){ - this.$('.o_mail_chatter_messages').html(QWeb.render('mail.chatter.ChatterMailThread.messages', {'widget': this})); - - // display image thumbnail - this.$(".o_mail_chatter_messages .o_image[data-mimetype^='image']").each(function () { - var $img = $(this); - if (/gif|jpe|jpg|png/.test($img.data('mimetype')) && $img.data('src')) { - $img.css('background-image', "url('" + $img.data('src') + "')"); - } - }); - }, - /** - * Order the messages - * @param {Object[]} messages : messages to order - * @override - */ - _message_order: function(messages){ - messages = mail_thread.MailThreadMixin._message_order.call(this, messages); - return messages.reverse(); - }, - /** - * Return the message domain for the current document mail thread - * @override - */ - get_message_domain: function(){ - var res_id = this.ThreadDataset.ids[this.ThreadDataset.index]; - return [['model', '=', this.ThreadDataset.model], ['res_id', '=', res_id]]; - } -}); - -core.form_widget_registry.add('mail_thread', ChatterMailThread); - - - -/** - * Global ComposeMessage Top Button - * - * Add a link on the top user bar to write a full mail. It opens the form view - * of the mail.compose.message (in a modal). - */ -var ComposeMessageTopButton = Widget.extend({ - template:'mail.ComposeMessageTopButton', - events: { - "click": "on_compose_message", - }, - on_compose_message: function (ev) { - ev.preventDefault(); - web_client.action_manager.do_action({ - type: 'ir.actions.act_window', - res_model: 'mail.compose.message', - view_mode: 'form', - view_type: 'form', - views: [[false, 'form']], - target: 'new', - }); - }, -}); - -// Put the ComposeMessageTopButton widget in the systray menu -SystrayMenu.Items.push(ComposeMessageTopButton); - - -return { - ChatterMailThread: ChatterMailThread, -}; - -}); diff --git a/addons/mail/static/src/js/mail_followers.js b/addons/mail/static/src/js/mail_followers.js deleted file mode 100644 index 8e09d1116c4..00000000000 --- a/addons/mail/static/src/js/mail_followers.js +++ /dev/null @@ -1,424 +0,0 @@ -odoo.define('mail.mail_followers', function (require) { -"use strict"; - -var ajax = require('web.ajax'); -var core = require('web.core'); -var data = require('web.data'); -var Dialog = require('web.Dialog'); -var form_common = require('web.form_common'); -var session = require('web.session'); -var web_client = require('web.web_client'); - -var _t = core._t; -var qweb = core.qweb; - -/** - * ------------------------------------------------------------ - * Mail Followers Widget - * ------------------------------------------------------------ - * - * This widget handles the display of a list of records as a vertical - * list, with an image on the left. The widget itself is a floatting - * right-sided box. - * This widget is mainly used to display the followers of records - * in OpenChatter. - */ - - -var Followers = form_common.AbstractField.extend({ - template: 'mail.Followers', - - init: function() { - this._super.apply(this, arguments); - - this.image = this.node.attrs.image || 'image_small'; - this.comment = this.node.attrs.help || false; - this.ds_model = new data.DataSetSearch(this, this.view.model); - this.ds_users = new data.DataSetSearch(this, 'res.users'); - - this.value = []; - this.followers = []; - this.data_subtype = {}; - - this.view_is_editable = this.__parentedParent.is_action_enabled('edit'); - }, - - start: function() { - // use actual_mode property on view to know if the view is in create mode anymore - this.view.on("change:actual_mode", this, this.on_check_visibility_mode); - this.on_check_visibility_mode(); - this.reinit(); - this.bind_events(); - return this._super(); - }, - - on_check_visibility_mode: function () { - this.set({"force_invisible": this.view.get("actual_mode") === "create"}); - }, - - set_value: function(_value) { - this.value = _value; - this._super(_value); - }, - - reinit: function() { - this.data_subtype = {}; - this.message_is_follower = undefined; - this.display_buttons(); - }, - - bind_events: function() { - var self = this; - - // event: click on '(Un)Follow' button, that toggles the follow for uid - this.$('.o_followers_follow_button').on('click', function () { - if($(this).hasClass('o_followers_notfollow')) { - self.do_follow(); - } - else { - self.do_unfollow([session.uid]); - } - }); - - // event: click on a subtype, that (un)subscribe for this subtype - this.$el.on('click', '.o_subtypes_list input', function(event) { - self.do_update_subscription(event); - var $list = self.$('.o_subtypes_list'); - if(!$list.hasClass('open')) { - $list.addClass('open'); - } - if(self.$('.o_subtypes_list ul')[0].children.length < 1) { - $list.removeClass('open'); - } - event.stopPropagation(); - }); - - // event: click on 'invite' button, that opens the invite wizard - this.$el.on('click', '.o_add_follower', function(e){ - e.preventDefault(); - self.on_invite_follower(false); - }); - this.$el.on('click', '.o_add_follower_channel', function(e){ - e.preventDefault(); - self.on_invite_follower(true); - }); - - // event: click on 'edit_subtype(pencil)' button to edit subscription - this.$el.on('click', '.o_edit_subtype', self.on_edit_subtype); - this.$el.on('click', '.o_remove_follower', self.on_remove_follower); - this.$el.on('click', 'a[data-partner]', self.on_follower_clicked); - }, - - on_edit_subtype: function(event) { - var self = this; - var $currentTarget = $(event.currentTarget); - var user_pid = $currentTarget.data('res-id'); - var follower_id = $currentTarget.data('id'); - self.dialog = new Dialog(this, { - size: 'small', - title: _t('Edit Subscription of ') + $currentTarget.siblings('a').text(), - buttons: [ - { - text: _t("Apply"), - click: function() { - self.do_update_subscription(event, user_pid); - }, - close: true - }, - { - text: _t("Cancel"), - close: true, - }, - ], - }).open(); - return self.fetch_subtypes(follower_id); - }, - - on_invite_follower: function (channel_only) { - var self = this; - var action = { - type: 'ir.actions.act_window', - res_model: 'mail.wizard.invite', - view_mode: 'form', - view_type: 'form', - views: [[false, 'form']], - name: _t('Invite Follower'), - target: 'new', - context: { - 'default_res_model': this.view.dataset.model, - 'default_res_id': this.view.datarecord.id, - 'mail_invite_follower_channel_only': channel_only, - }, - }; - this.do_action(action, { - on_close: function() { - self.read_value(); - }, - }); - }, - - on_remove_follower: function (event) { - var res_model = $(event.target).parent().find('a').data('res-model'); - var res_id = $(event.target).parent().find('a').data('res-id'); - if (res_model === 'res.partner') { return this.do_unfollow(undefined, [res_id], undefined); } - else { return this.do_unfollow(undefined, undefined, [res_id]); } - }, - - on_follower_clicked: function (event) { - event.preventDefault(); - var partner_id = $(event.target).data('partner'); - var state = { - 'model': 'res.partner', - 'id': partner_id, - 'title': this.record_name - }; - web_client.action_manager.do_push_state(state); - var action = { - type:'ir.actions.act_window', - view_type: 'form', - view_mode: 'form', - res_model: 'res.partner', - views: [[false, 'form']], - res_id: partner_id, - }; - this.do_action(action); - }, - - read_value: function () { - var self = this; - return this.ds_model.read_ids([this.view.datarecord.id], ['message_follower_ids']) - .then(function (results) { - self.value = results[0].message_follower_ids; - self.render_value(); - }); - }, - - render_value: function () { - this.reinit(); - return this.fetch_followers(this.value); - }, - - fetch_followers: function (value_) { - this.value = value_ || []; - return ajax.jsonRpc('/mail/read_followers', 'call', {'follower_ids': this.value}) - .then(this.proxy('display_followers'), this.proxy('fetch_generic')) - .then(this.proxy('display_buttons')) - .then(this.proxy('fetch_subtypes')); - }, - - /** Read on res.partner failed: fall back on a generic case - - fetch current user partner_id (call because no other smart solution currently) FIXME - - then display a generic message about followers */ - fetch_generic: function () { - var self = this; - - return this.ds_users.call('read', [[session.uid], ['partner_id']]) - .then(function (results) { - var pid = results[0].partner_id[0]; - self.message_is_follower = (_.indexOf(self.value, pid) !== -1); - }) - .then(self.proxy('display_generic')); - }, - - _format_followers: function(count){ - var str = ''; - if(count <= 0){ - str = _t('No follower'); - }else if(count === 1){ - str = _t('One follower'); - }else{ - str = ''+count+' '+_t('followers'); - } - return str; - }, - - /* Display generic info about follower, for people not having access to res_partner */ - display_generic: function () { - this.$('.o_followers_list').empty(); - this.$('.o_followers_count').html(this._format_followers(this.value.length)); - }, - - /** Display the followers */ - display_followers: function (records) { - var self = this; - this.followers = records || this.followers; - - // clean and display title - var $followers_list = this.$('.o_followers_list').empty(); - this.$('.o_followers_count').html(this._format_followers(this.followers.length)); - var user_follower = _.filter(this.followers, function (rec) { return rec.is_uid;}); - this.message_is_follower = user_follower.length >= 1; - this.follower_id = this.message_is_follower ? user_follower[0].id : undefined; - - // render the dropdown content - $(qweb.render('mail.Followers.add_more', {'widget': self})).appendTo($followers_list); - var $follower_li; - _(this.followers).each(function (record) { - $follower_li = $(qweb.render('mail.Followers.partner', { - 'record': _.extend(record, {'avatar_url': '/web/image/' + record.res_model + '/' + record.res_id + '/image_small'}), - 'widget': self}) - ); - $follower_li.appendTo($followers_list); - - // On mouse-enter it will show the edit_subtype pencil. - if (record.is_editable) { - $follower_li.on('mouseenter mouseleave', function(e) { - $(e.currentTarget).find('.o_edit_subtype').toggleClass('hide', e.type === 'mouseleave'); - }); - } - }); - }, - - display_buttons: function () { - if (this.message_is_follower) { - this.$('button.o_followers_follow_button').removeClass('o_followers_notfollow').addClass('o_followers_following'); - } else { - this.$('button.o_followers_follow_button').removeClass('o_followers_following').addClass('o_followers_notfollow'); - } - }, - - /** Fetch subtypes, only if current user is follower */ - fetch_subtypes: function (follower_id) { - var self = this; - var dialog = false; - - if (follower_id) { - dialog = true; - } - else { - this.$('.o_subtypes_list ul').empty(); - if (! this.message_is_follower) { - this.$('.o_subtypes_list > .dropdown-toggle').attr('disabled', true); - return; - } - else { - this.$('.o_subtypes_list > .dropdown-toggle').attr('disabled', false); - } - } - if (this.follower_id) { - return ajax.jsonRpc('/mail/read_subscription_data', 'call', {'res_model': this.view.model, 'res_id': this.view.datarecord.id, 'follower_id': follower_id}) - .then(function (data) { self.display_subtypes(data, dialog); }); - } else { - return $.Deferred().resolve(); - } - }, - - /** Display subtypes: {'name': default, followed} */ - display_subtypes:function (data, dialog) { - var self = this; - var old_parent_model; - var $list; - if (dialog) { - $list = this.dialog.$el; - } else { - $list = this.$('.o_subtypes_list ul'); - } - $list.empty(); - - this.data_subtype = data; - this.records_length = $.map(data, function(value, index) { return index; }).length; - - if (this.records_length > 1) { - self.display_followers(); - } - - _.each(data, function (record) { - if (old_parent_model !== record.parent_model && old_parent_model !== undefined) { - $list.append($('
  • ').addClass('divider')); - } - old_parent_model = record.parent_model; - record.followed = record.followed || undefined; - $(qweb.render('mail.Followers.subtype', {'record': record, - 'dialog': dialog})) - .appendTo($list); - }); - }, - - do_follow: function () { - var context = new data.CompoundContext(this.build_context(), {}); - this.$('.o_subtypes_list > .dropdown-toggle').attr('disabled', false); - this.ds_model.call('message_subscribe_users', [[this.view.datarecord.id], - [session.uid], - undefined, - context]) - .then(this.proxy('read_value')); - - _.each(this.$('.o_subtypes_list input'), function (record) { - $(record).attr('checked', 'checked'); - }); - }, - - do_unfollow: function (user_ids, partner_ids, channel_ids) { - if (confirm(_t("Warning! \n If you remove a follower, he won't be notified of any email or discussion on this document. Do you really want to remove this follower ?"))) { - _(this.$('.o_subtype_checkbox')).each(function (record) { - $(record).attr('checked',false); - }); - - this.$('.o_subtypes_list > .dropdown-toggle').attr('disabled', true); - var context = new data.CompoundContext(this.build_context(), {}); - - if (partner_ids || channel_ids) { - return this.ds_model.call( - 'message_unsubscribe', [ - [this.view.datarecord.id], - partner_ids, - channel_ids, - context] - ).then(this.proxy('read_value')); - } - else { - return this.ds_model.call( - 'message_unsubscribe_users', [ - [this.view.datarecord.id], - user_ids, - context] - ).then(this.proxy('read_value')); - } - } - return false; - }, - - do_update_subscription: function (event, user_pid) { - var self = this; - this.data_subtype = {}; - - var kwargs = {}; - var action_subscribe = 'message_subscribe_users'; - var subtypes = this.$('.o_followers_actions input[type="checkbox"]'); - - // Subtypes edited from the modal - if (user_pid) { - action_subscribe = 'message_subscribe'; - subtypes = this.dialog.$('input[type="checkbox"]'); - kwargs['partner_ids'] = [user_pid]; - }else{ - kwargs['user_ids'] = [session.uid]; - } - - // Get the subtype ids - var checklist = []; - _(subtypes).each(function (record) { - if ($(record).is(':checked')) { - checklist.push(parseInt($(record).data('id'))); - } - }); - kwargs['subtype_ids'] = checklist; - - // If no more subtype followed, unsubscribe the partner. - if (!checklist.length) { - if (!this.do_unfollow(undefined, [user_pid], undefined)) { - $(event.target).attr("checked", "checked"); - } else { - self.$('.o_subtypes_list ul').empty(); - } - } else { - kwargs['context'] = new data.CompoundContext(this.build_context(), {}); - return this.ds_model._model.call(action_subscribe, [[this.view.datarecord.id]], kwargs).then(this.proxy('read_value')); - } - }, -}); - -/* Add the widget to registry */ -core.form_widget_registry.add('mail_followers', Followers); - -}); diff --git a/addons/mail/static/src/js/mail_thread.js b/addons/mail/static/src/js/mail_thread.js deleted file mode 100644 index 6511e6a6583..00000000000 --- a/addons/mail/static/src/js/mail_thread.js +++ /dev/null @@ -1,823 +0,0 @@ -odoo.define('mail.thread', function (require) { -"use strict"; - -var core = require('web.core'); -var data = require('web.data'); -var Model = require('web.Model'); -var time = require('web.time'); -var Widget = require('web.Widget'); -var session = require('web.session'); -var web_client = require('web.web_client'); -var mail_utils = require('mail.utils'); - -var _t = core._t; -var QWeb = core.qweb; - - -var LIMIT_MESSAGE = 20; - -/** - * Widget : Input textbox to post messages - * - * Input with 2 buttons to manage attachment and emoji - * - Attachment : upload selected attachment (one at a time) and display them below the textbox. Prevent - posting message while uploading - * - Emoji : popover exposing emoji list, and append emoji shortcode to the textbox when click on emoji image - * It triggers 'message_sent' event with the posted message, when it is sent to the server. - */ -var MailComposeMessage = Widget.extend({ - template: 'mail.ComposeMessage', - events: { - "keydown .o_mail_compose_message_input": "on_keydown", - "keyup .o_mail_compose_message_input": "on_keyup", - "keypress .o_mail_compose_message_input": "on_keypress", - "change input.o_form_input_file": "on_attachment_change", - "click .o_mail_attachment_delete": "on_attachment_delete", - "click .o_mail_compose_message_button_attachment": 'on_click_attachment', - "click .o_mail_compose_message_button_send": 'message_send', - "click .o_mail_mention_proposition": "on_click_mention_item", - "mouseover .o_mail_mention_proposition": "on_hover_mention_proposition", - }, - /** - * Contructor - * @param {Object} options : the options of the MailComposeMessage - * @param {Object[]} [options.emoji_list] : the list of emoji - * @param {Object} options.context : the context. It should contains the default_res_id' (id of the current document). - * @param {chat|mail} options.display_mode : the 'chat' mode will display an input, when the 'mail' mode will display a texarea. - * @param {Char} [options.mention_delimiter] : the delimiter char to distinguish the beginning of a mention - * @param {Integer} [options.mention_min_length] : min length of the mention word required to start a search on partner - * @param {Integer} [options.mention_typing_speed] : delay before starting a search with the mention word - * @param {Integer} [options.mention_fetch_limit] : limit of partner fetch - */ - init: function(parent, dataset, options){ - this._super.apply(this, arguments); - this.thread_dataset = dataset; - this.options = _.defaults(options || {}, { - 'emoji_list': {}, - 'context': {}, - 'display_mode': 'mail', - 'mention_delimiter': '@', - 'mention_min_length': 2, - 'mention_typing_speed': 400, - 'mention_fetch_limit': 8, - }); - this.emoji_list = this.options.emoji_list; - this.context = this.options.context; - this.input_buffer = ''; // save the value at keydown - // attachment handeling - this.AttachmentDataSet = new data.DataSetSearch(this, 'ir.attachment', this.context); - this.fileupload_id = _.uniqueId('o_mail_chat_fileupload'); - this.set('attachment_ids', []); - // mention - this.PartnerModel = new Model('res.partner'); - this.set('mention_word', false); // word typed after the delimiter - this.set('mention_partners', []); // proposition of not-mention partner matching the mention_word - this.set('mention_selected_partners', []); // contains the mention partners sorted as they appear in the input text - }, - start: function(){ - var self = this; - this.$attachment_button = this.$(".o_mail_compose_message_button_attachment"); - this.$attachments_list = this.$el.filter('.o_mail_attachments_list'); - this.$mention_partner_tags = this.$el.filter('.o_mail_mention_partner_tags'); - this.$mention_dropdown = this.$('.o_mail_mention_dropdown'); - this.$input = this.$('.o_mail_compose_message_input'); - this.$input.focus(); - - // attachments - $(window).on(this.fileupload_id, this.on_attachment_loaded); - this.on("change:attachment_ids", this, this.attachment_render); - - // emoji - self.$('.o_mail_compose_message_button_emoji').popover({ - placement: 'top', - content: function(){ - if(!self.$emoji){ // lazy rendering - self.$emoji = $(QWeb.render('mail.ComposeMessage.emoji', {'widget': self})); - self.$emoji.filter('.o_mail_compose_message_emoji_img').on('click', self, self.on_click_emoji_img); - } - return self.$emoji; - }, - html: true, - container: '.o_mail_compose_message_emoji', - trigger: 'focus', - }); - - // mention - this.on('change:mention_word', this, this.mention_word_change); - this.on('change:mention_partners', this, this.mention_render_partner); - this.on('change:mention_selected_partners', this, this.mention_render_selected_partners); - return this._super(); - }, - // events - on_click_attachment: function(event){ - event.preventDefault(); - this.$('input.o_form_input_file').click(); - }, - on_click_emoji_img: function(event){ - this.$input.val(this.$input.val() + " " + $(event.currentTarget).data('emoji')+ " "); - this.$input.focus(); - }, - on_keydown: function(event){ - // Save the old input - this.input_buffer = this.$input.val(); - // Key Down displatching - switch(event.which) { - // BACKSPACE : check if need to remove a mention - case $.ui.keyCode.BACKSPACE: - this.mention_check_remove(event.which); - break; - // ENTER : submit the message only if the dropdown mention proposition is not displayed - case $.ui.keyCode.ENTER: - if(!this.get('mention_partners').length && this.options.display_mode === 'chat'){ - this.message_send(); - } - break; - // UP, DOWN : when navigate on the dropdown, prevent moving the cursor position in the input text - case $.ui.keyCode.UP: - case $.ui.keyCode.DOWN: - event.preventDefault(); - break; - } - }, - message_send: function(){ - var mes = mail_utils.get_text2html(this.$input.val()); - if (! mes.trim() && this.do_check_attachment_upload()) { - return; - } - this.$input.val(""); - this.message_post(mes, _.pluck(this.get('attachment_ids'), 'id')); - }, - on_keypress: function(event){ - switch(event.which) { - // ENTER : prevent moving cursor position when selecting a mention proposition in dropdown - case $.ui.keyCode.ENTER: - if(this.get('mention_partners').length){ - event.preventDefault(); - } - break; - } - }, - on_keyup: function(event){ - switch(event.which) { - // ESCAPED KEYS : do nothing - case $.ui.keyCode.END: - case $.ui.keyCode.PAGE_UP: - case $.ui.keyCode.PAGE_DOWN: - case $.ui.keyCode.ESCAPE: - // do nothing - break; - // ENTER, UP, DOWN : check if navigation in mention propositions - case $.ui.keyCode.UP: - case $.ui.keyCode.DOWN: - case $.ui.keyCode.ENTER: - this.mention_proposition_navigation(event.which); - break; - // DELETE : check if need to remove a mention - case $.ui.keyCode.DELETE: - this.mention_check_remove(event.which); - break; - // Otherwise, check is a mention is typed - default: - var mention_word = this.mention_detect_delimiter(); - if(mention_word){ - this.set('mention_word', mention_word); - }else{ - this.set('mention_partners', []); // close the dropdown - } - } - }, - /** - * Sent the message to the server to create a mail.message, using 'message_post' method - * @param {String} body : the core of the message (html, or plaintext) - * @param {Number[]} attachment_ids : attachment_ids linked to the message - * @param {Object} kwargs : other parameters (in a dictionnary) - * @returns {Deferred} resolved when the message is posted, and returning the new message id - */ - message_post: function(body, attachment_ids, kwargs){ - var self = this; - var values = _.defaults(kwargs || {}, { - 'message_type': 'comment', - 'content_subtype': 'html', - 'partner_ids': [], - 'subtype': 'mail.mt_comment', - }); - values = _.extend(values, { - 'body': this.mention_preprocess_message(body), - 'attachment_ids': attachment_ids || [], - 'partner_ids': _.pluck(this.get('mention_selected_partners'), 'id').concat(values.partner_ids), - }); - return this.thread_dataset._model.call('message_post', [this.context.default_res_id], values).then(function(message_id){ - self.reset(); // empty attachment, mention partners, ... - self.trigger('message_sent', message_id); - return message_id; - }); - }, - // attachment business - on_attachment_change: function(event){ - var $target = $(event.target); - if ($target.val() !== '') { - var filename = $target.val().replace(/.*[\\\/]/,''); - // if the files exits for this answer, delete the file before upload - var attachments = []; - for (var i in this.get('attachment_ids')) { - if ((this.get('attachment_ids')[i].filename || this.get('attachment_ids')[i].name) == filename) { - if (this.get('attachment_ids')[i].upload) { - return false; - } - this.AttachmentDataSet.unlink([this.get('attachment_ids')[i].id]); - } else { - attachments.push(this.get('attachment_ids')[i]); - } - } - // submit filename - this.$('form.o_form_binary_form').submit(); - this.$attachment_button.prop('disabled', true); - - attachments.push({ - 'id': 0, - 'name': filename, - 'filename': filename, - 'url': '', - 'upload': true, - 'mimetype': '', - }); - this.set('attachment_ids', attachments); - } - }, - on_attachment_loaded: function(event, result){ - var attachment_ids = []; - if (result.error || !result.id ) { - this.do_warn(result.error); - attachment_ids = _.filter(this.get('attachment_ids'), function (val) { return !val.upload; }); - }else{ - _.each(this.get('attachment_ids'), function(a){ - if (a.filename == result.filename && a.upload) { - attachment_ids.push({ - 'id': result.id, - 'name': result.name || result.filename, - 'filename': result.filename, - 'mimetype': result.mimetype, - 'url': session.url('/web/content', {'id': result.id, download: true}), - }); - }else{ - attachment_ids.push(a); - } - }); - } - this.set('attachment_ids', attachment_ids); - - // TODO JEM : understand the 2 lines below .... - var $input = this.$('input.o_form_input_file'); - $input.after($input.clone(true)).remove(); - - this.$attachment_button.prop('disabled', false); - }, - on_attachment_delete: function(event){ - event.stopPropagation(); - var self = this; - var attachment_id = $(event.target).data("id"); - if (attachment_id) { - var attachments = []; - _.each(this.get('attachment_ids'), function(attachment){ - if (attachment_id != attachment.id) { - attachments.push(attachment); - } else { - self.AttachmentDataSet.unlink([attachment_id]); - } - }); - this.set('attachment_ids', attachments); - } - }, - do_check_attachment_upload: function () { - if (_.find(this.get('attachment_ids'), function (file) {return file.upload;})) { - this.do_warn(_t("Uploading error"), _t("Please, wait while the file is uploading.")); - return false; - } - return true; - }, - attachment_render: function(){ - this.$attachments_list.html(QWeb.render('mail.ComposeMessage.attachments', {'widget': this})); - - // display image thumbnail - this.$attachments_list.find(".o_image[data-mimetype^='image']").each(function () { - var $img = $(this); - if (/gif|jpe|jpg|png/.test($img.data('mimetype')) && $img.data('src')) { - $img.css('background-image', "url('" + $img.data('src') + "')"); - } - }); - }, - // Mention - on_click_mention_item: function(event){ - event.preventDefault(); - - var text_input = this.$input.val(); - var partner_id = this.$(event.currentTarget).data('partner-id'); - var selected_partner = _.filter(this.get('mention_partners'), function(p){ - return p.id === partner_id; - })[0]; - - // add the mention partner to the list - var mention_selected_partners = this.get('mention_selected_partners'); - if(mention_selected_partners.length){ // there are already mention partner - // get mention matches (ordered by index in the text) - var matches = this.mention_get_match(text_input); - var index = this.mention_get_index(matches, this.get_cursor_position()); - mention_selected_partners.splice(index, 0, selected_partner); - mention_selected_partners = _.clone(mention_selected_partners); - }else{ // this is the first mentionned partner - mention_selected_partners = mention_selected_partners.concat([selected_partner]); - } - this.set('mention_selected_partners', mention_selected_partners); - - // update input text, and reset dropdown - var mention_word = this.get('mention_word'); - var cursor_position = this.get_cursor_position(); - var text_left = text_input.substring(0, cursor_position-(mention_word.length+1)); - var text_right = text_input.substring(cursor_position, text_input.length); - var text_input_new = text_left + this.options.mention_delimiter + selected_partner.name + ' ' + text_right; - this.$input.val(text_input_new); - this.set_cursor_position(text_left.length+selected_partner.name.length+2); - this.set('mention_partners', []); - }, - on_hover_mention_proposition: function(event){ - var $elem = this.$(event.currentTarget); - this.$('.o_mail_mention_proposition').removeClass('active'); - $elem.addClass('active'); - }, - mention_proposition_navigation: function(keycode){ - var $active = this.$('.o_mail_mention_proposition.active'); - if(keycode === $.ui.keyCode.ENTER){ // selecting proposition - if($active){ - $active.click(); - } - }else{ // navigation in propositions - var $to; - if(keycode === $.ui.keyCode.DOWN){ - $to = $active.next('.o_mail_mention_proposition:not(.active)'); - }else{ - $to = $active.prev('.o_mail_mention_proposition:not(.active)'); - } - if($to.length){ - this.$('.o_mail_mention_proposition').removeClass('active'); - $to.addClass('active'); - } - } - }, - /** - * Return the text attached to the mention delimiter - * @returns {String|false} : the text right after the delimiter or false - */ - mention_detect_delimiter: function(){ - var self = this; - var delimiter = self.options.mention_delimiter; - var validate_keyword = function(search_str){ - var pattern = "(^"+delimiter+"|(^\\s"+delimiter+"))"; - var regex_start = new RegExp(pattern, "g"); - search_str = search_str.replace(/^\s\s*|^[\n\r]/g, ''); - if (regex_start.test(search_str) && search_str.length > self.options.mention_min_length){ - search_str = search_str.replace(pattern, ''); - return search_str.indexOf(' ') < 0 && !/[\r\n]/.test(search_str) ? search_str.replace(delimiter, '') : false; - } - return false; - }; - var text_val = this.$input.val(); - var left_string = text_val.substring(0, this.get_cursor_position()); - var search_str = text_val.substring(left_string.lastIndexOf(delimiter) - 1, this.get_cursor_position()); - return validate_keyword(search_str); - }, - mention_word_change: function(){ - var self = this; - var word = this.get('mention_word'); - if(word){ - // start a timeout to fetch partner with the current 'mention word'. The timer avoid to start - // a RPC for each pushed key when the user is typing the partner name. The 'mention_typing_speed' - // option should approach the time for a human to type a letter. - clearTimeout(this.mention_fetch_timer); - this.mention_fetch_timer = setTimeout(function() { - self.mention_fetch_partner(); - }, this.options.mention_typing_speed); - }else{ - // when deleting mention word, avoid RPC call with last long enough word - clearTimeout(this.mention_fetch_timer); - } - }, - mention_fetch_partner: function(){ - var self = this; - var search_str = this.get('mention_word'); - this.PartnerModel.query(['id', 'name', 'email']) - .filter([['id', 'not in', _.pluck(this.get('mention_selected_partners'), 'id')],'|', ['name', 'ilike', search_str], ['email', 'ilike', search_str]]) - .limit(this.options.mention_fetch_limit) - .all().then(function(partners){ - self.set('mention_partners', partners); - }); - }, - mention_check_remove: function(keycode){ - var mention_selected_partners = this.get('mention_selected_partners'); - var cursor = this.get_cursor_position(); - var input_text = this.$input.val(); - var matches; - switch(keycode) { - // Remove a mention when a character belonging to a mention word is removed from the input text - case $.ui.keyCode.BACKSPACE: - matches = this.mention_get_match(this.$input.val()); - for(var i=0 ; i< matches.length ; i++){ - var m = matches[i]; - if(m.index <= cursor && cursor <= m.index+m[0].length){ - mention_selected_partners.splice(i, 1); - } - } - break; - // Remove a mention if the deleted string overlapse a mention - case $.ui.keyCode.DELETE: - var left_text = input_text.substring(0, cursor); - var right_text = input_text.substring(cursor, input_text.length); - var temp = this.input_buffer.substring(0, this.input_buffer.length - right_text.length); - var deleted_text = temp.substring(left_text.length, temp.length); - - var deleted_binf = left_text.length; - var deleted_bsup = left_text.length + deleted_text.length; - - matches = this.mention_get_match(this.input_buffer); - for(var i=0 ; i< matches.length ; i++){ - var m = matches[i]; - var m1 = m.index; - var m2 = m.index+m[0].length; - if(deleted_binf <= m2 && m1 <= deleted_bsup){ - mention_selected_partners.splice(i, 1); - } - } - break; - } - this.set('mention_selected_partners', _.clone(mention_selected_partners)); - }, - mention_preprocess_message: function(body){ - var partners = this.get('mention_selected_partners'); - if(partners.length){ - var matches = this.mention_get_match(body); - var substrings = []; - var start_index = 0; - for(var i=0; i < matches.length ; i++){ - var match = matches[i]; - var end_index = match.index+match[0].length; - var subtext = body.substring(start_index, end_index); - subtext = subtext.replace(match[0], _.str.sprintf("%s", partners[i].id, match[0])); - substrings.push(subtext); - start_index = end_index; - } - substrings.push(body.substring(start_index, body.length)); - return substrings.join(''); - } - return body; - }, - mention_render_partner: function(){ - this.$mention_dropdown.html(QWeb.render('mail.ComposeMessage.mention_menu', {'widget': this})); - if(this.get('mention_partners').length){ - this.$mention_dropdown.addClass('open'); - }else{ - this.$mention_dropdown.removeClass('open'); - } - }, - mention_render_selected_partners: function(){ - this.$mention_partner_tags.html(QWeb.render('mail.ComposeMessage.mention_tags', {'widget': this})); - }, - /** - * Return the matches (as RexExp.exec do) for the partner mention in the input text - * @param {String} input_text : the text to search matches - * @returns {Object[]} matches in the same format as RexExp.exec() - */ - mention_get_match: function(input_text){ - var self = this; - // create the regex of all mention partner name - var partner_names = _.pluck(this.get('mention_selected_partners'), 'name'); - var escaped_partner_names = _.map(partner_names, function(str){ - return "("+_.str.escapeRegExp(self.options.mention_delimiter+str)+")"; - }); - var regex_str = escaped_partner_names.join('|'); - // extract matches - var result = []; - if(regex_str.length){ - var myRegexp = new RegExp(regex_str, 'g'); - var match = myRegexp.exec(input_text); - while (match !== null) { - result.push(match); - match = myRegexp.exec(input_text); - } - } - return result; - }, - mention_get_index: function(matches, cursor_position){ - for(var i = 0 ; i < matches.length ; i++){ - if(cursor_position < matches[i].index){ - return i; - }else{ - if(i === matches.length-1){ - return i+1; - } - } - } - return 0; - }, - // others - reset: function(){ - this.set('attachment_ids', []); - this.set('mention_partners', []); - this.set('mention_selected_partners', []); - }, - focus: function(){ - this.$input.focus(); - }, - get_cursor_position: function() { - var el = this.$input.get(0); - if(!el){ - return 0; - } - if('selectionStart' in el) { - return el.selectionStart; - } else if('selection' in document) { - var cr = document.selection.createRange(); - return cr.moveStart('character', -el.focus().value.length).text.length - cr.text.length; - } - return 0; - }, - set_cursor_position: function(pos) { - this.$input.each(function(index, elem) { - if (elem.setSelectionRange){ - elem.setSelectionRange(pos, pos); - } - else if (elem.createTextRange){ - elem.createTextRange().collapse(true).moveEnd('character', pos).moveStart('character', pos).select(); - } - }); - }, -}); - - -/** - * Mail Thread Mixin : Messages Managment - * - * Load, Fetch, Display mail.message - * This is a mixin since it will be inherit by a form_common.AbstractField and a Wigdet (Client Action) - * - * @mixin - **/ -var MailThreadMixin = { - /** - * Constructor of the 'mail_thread' mixin - * @param {Boolean} [options.display_log_button] : display of the 'log a note' button - * @param {Boolean} [options.display_document_link] : display document link on message - * @param {Boolean} [options.display_needaction_button] : display the needaction 'check' button on messages - * @param {Object[]} [options.internal_subtypes]: list of internal subtype (for employee only) - * @param {Object[]} [options.emoji_list] : list of emoji - */ - init: function(){ - this.MessageDatasetSearch = new data.DataSetSearch(this, 'mail.message'); - this.set('messages', []); - this.emoji_substitution = {}; - // options - this.options = _.defaults(this.options || {}, { - 'display_log_button': false, - 'display_document_link': false, - 'display_needaction_button': false, - 'internal_subtypes': [], - 'emoji_list': [], - 'default_username': _t('Anonymous'), - }); - }, - start: function(){ - this.on("change:messages", this, this.message_render); - }, - // Common Actions (They should be bind on the implementing widget, the 'events' dict) - /** - * Generic redirect action : redirect to the form view if the - * click node contains 'data-oe-model' and 'data-oe-id'. - */ - on_click_redirect: function(event){ - event.preventDefault(); - var res_id = $(event.target).data('oe-id'); - var res_model = $(event.target).data('oe-model'); - web_client.action_manager.do_push_state({ - 'model': res_model, - 'id': res_id, - 'title': this.record_name, - }); - this.do_action({ - type:'ir.actions.act_window', - view_type: 'form', - view_mode: 'form', - res_model: res_model, - views: [[false, 'form']], - res_id: res_id, - }, { - 'on_reverse_breadcrumb': this.on_reverse_breadcrumb, - }); - }, - /** - * Will be called when clicking on the Breadcrumb (mainly of client action) - * Need to be override - */ - on_reverse_breadcrumb: function(){ - - }, - /** - * Toggle short / long body when present (hide signature / crop long - messages) - */ - on_message_body_toggle: function (event) { - event.preventDefault(); - var $source = this.$(event.currentTarget); - $source.parents('.o_mail_thread_message_core').find('.o_mail_body_short').toggle(); - $source.parents('.o_mail_thread_message_core').find('.o_mail_body_long').toggle(); - }, - /** - * Toggle a message as 'starred' (or not), by toggling the 'o_mail_message_starred' css class. - * Trigger when clicking on selector '.o_mail_thread_message_star'. - */ - on_message_star: function(event){ - var self = this; - var $source = this.$(event.currentTarget); - var mid = $source.data('message-id'); - var is_starred = !$source.hasClass('o_mail_message_starred'); - return this.MessageDatasetSearch.call('set_message_starred', [[mid], is_starred]).then(function(){ - $source.toggleClass('fa-star-o fa-star o_mail_message_starred'); - self._message_star([mid], $source.hasClass('o_mail_message_starred')); - return mid; - }); - }, - /** - * Star a formatted message in local - * @param Int[] message_ids : the identifiers of the message to set as (none-)starred by current user - * @param Boolean star : star or not the given messages - */ - _message_star: function(message_ids, star){ - _.each(this.get('messages'), function(m){ - if(_.contains(message_ids, m.id)){ - if(star){ - m.is_starred = true; - m.starred_partner_ids.push(session.partner_id); - }else{ - m.is_starred = false; - m.starred_partner_ids = _.without(m.starred_partner_ids, session.partner_id); - } - } - }); - }, - on_message_needaction: function(event){ - var self = this; - var $source = this.$(event.currentTarget); - var mid = $source.data('message-id'); - return this.MessageDatasetSearch.call('set_message_done', [[mid]]).then(function(){ - $source.remove(); - self._message_needaction([mid]); - return mid; - }); - }, - /** - * Make formatted messages a needaction - * @param Int[] message_ids : message ids to mark as needaction treated - */ - _message_needaction: function(message_ids, needaction){ - _.each(this.get('messages'), function(m){ - if(_.contains(message_ids, m.id)){ - m.is_needaction = false; - m.needaction_partner_ids = _.without(m.starred_partner_ids, session.partner_id); - } - }); - }, - // Message functions - /** - * Fetch given message - * @param {Number[]} message_ids : list of mail.message identifiers to fetch - * @returns {Deferred} resolved when the messages are loaded - */ - message_format: function(message_ids){ - return this.MessageDatasetSearch._model.call('message_format', [message_ids]); - }, - /** - * Fetch mail.message in the format defined server side - * @param {Array} domain : Odoo Domain of the message to fetch - * @param {Number} limit : the limit of messages to fetch - * @returns {Deferred} resolved when the messages are loaded - */ - message_fetch: function(domain, limit){ - domain = domain || this.get_message_domain(); - limit = limit || LIMIT_MESSAGE; - return this.MessageDatasetSearch._model.call('message_fetch', [domain], {limit: limit}); - }, - _message_replace: function(messages){ - this.set('messages', this._message_order(messages)); - }, - /** - * Order the message in a particular order, to be defined by implementation - * @param {Object[]} messages : messages to order - */ - _message_order: function(messages){ - return _.sortBy(messages, 'date'); - }, - /** - * Insert messages in the current list - * @param {Object[]} messages : list of mail.message (formatted server side) - */ - message_insert: function(raw_messages){ - var current_messages = this.get('messages'); - current_messages = current_messages.concat(this._message_preprocess(raw_messages)); - this._message_replace(current_messages); - }, - /** - * Load history of message, with the 'history' domain, and insert them in the current list - * @returns {Deferred} resolved when the messages are fetched and returning the raw - * messages (formatted server side) - */ - message_load_history: function(){ - var self = this; - return this.message_fetch(this.get_message_domain_history()).then(function(raw_messages){ - if (raw_messages.length < LIMIT_MESSAGE) { - self.all_messages_loaded = true; - } - self.message_insert(raw_messages); - return raw_messages; - }); - }, - /** - * Load messages according to the current domain, and set that new list as current messages, - * erasing the old ones. - * @returns {Deferred} resolved when the messages are fetched and returning the raw - * messages (formatted server side) - */ - message_load_new: function(){ - var self = this; - return this.message_fetch(this.get_message_domain()).then(function(raw_messages){ - self.all_messages_loaded = raw_messages.length < LIMIT_MESSAGE; - self._message_replace(self._message_preprocess(raw_messages)); - return raw_messages; - }); - }, - /** - * Preprocess the list of messages before rendering, add 'computed' field (is_needaction, - * is_starred, ...), and apply image shortcode to the message body. - * @param {Object[]} messages : list of raw mail.message (formatted server side) - * @returns list of messages. It can be sorted, grouped, ... - */ - _message_preprocess: function(messages){ - var self = this; - _.each(messages, function(m){ - m.is_needaction = _.contains(m.needaction_partner_ids, session.partner_id); - m.is_starred = _.contains(m.starred_partner_ids, session.partner_id); - m.date = moment(time.str_to_datetime(m.date)).format('YYYY-MM-DD HH:mm:ss'); // set the date in the correct browser user timezone - if(m.body){ - m.body = mail_utils.shortcode_apply(m.body, self.emoji_substitution); - m.body = self._message_preprocess_mention(m.body); - } - _.each(m.attachment_ids, function(a){ - a.url = '/web/content/' + a.id + '?download=true'; - }); - }); - return _.sortBy(messages, 'date'); - }, - /** - * Transform the mention 'span' tag into a link tag to the partner form - * @param {String} body : the message body (html) to transform - * @returns {String} the transformed body - */ - _message_preprocess_mention: function(body){ - var re = /(.*?)\<\/span>/g; - var subst = "$3"; - return body.replace(re, subst); - }, - /** - * Take the current messages, render them, and insert the rendering in the DOM. - * This is triggered when the mesasge list change. - * Must be redefined, since it depends on the complete DOM widget - */ - message_render: function(){ - - }, - // Message Domains - /** - * Return the current domain to fetch the message of the thread. this should - * be redifined by the thread implementation. - */ - get_message_domain: function(){ - return []; - }, - get_message_domain_history: function(){ - return this.get_message_domain().concat([['id', '<', _.min(_.pluck(this.get('messages'), 'id'))]]); - }, - // Others - /** - * Set the list of emoji to be substituted in message body - * @param {Object[]} emoji_list : list of emoji Object - */ - emoji_set_substitution: function(emoji_list){ - this.emoji_substitution = mail_utils.shortcode_substitution(emoji_list); - }, -}; - - -return { - MailComposeMessage: MailComposeMessage, - MailThreadMixin: MailThreadMixin, - LIMIT_MESSAGE: LIMIT_MESSAGE, -}; - - -}); diff --git a/addons/mail/static/src/js/mail_utils.js b/addons/mail/static/src/js/mail_utils.js deleted file mode 100644 index 36f8fb9081e..00000000000 --- a/addons/mail/static/src/js/mail_utils.js +++ /dev/null @@ -1,134 +0,0 @@ -odoo.define('mail.utils', function () { -"use strict"; - -/** - * ------------------------------------------------------------ - * ChatterUtils - * ------------------------------------------------------------ - * - * This class holds a few tools method for Chatter. - * Some regular expressions not used anymore, kept because I want to - * - (^|\s)@((\w|@|\.)*): @login@log.log - * - (^|\s)\[(\w+).(\w+),(\d)\|*((\w|[@ .,])*)\]: [ir.attachment,3|My Label], - * for internal links - */ - - -/** parse text to find email: Tagada -> [Tagada, address@mail.fr] or False */ -function parse_email(text) { - var result = text.match(/(.*)<(.*@.*)>/); - if (result) { - return [_.str.trim(result[1]), _.str.trim(result[2])]; - } - result = text.match(/(.*@.*)/); - if (result) { - return [_.str.trim(result[1]), _.str.trim(result[1])]; - } - return [text, false]; -} - -/** - * Replaces textarea text into html text (add

    , ) - * TDE note : should be done server-side, in Python -> use mail.compose.message ? - */ -function get_text2html(text) { - return text - .replace(/((?:https?|ftp):\/\/[\S]+)/g,'$1 ') - .replace(/[\n\r]/g,'
    '); -} - -/** - * ------------------------------------------------------------ - * MailChat Utils - * ------------------------------------------------------------ - */ - - /** - * Apply the given shortcode to the 'str' message. - * @param str : the text to substitute (html). - * @param shortcodes : dict where key is the shortcode, and the value is the substitution - */ -function shortcode_apply(str, shortcodes){ - var re_escape = function(str){ - return String(str).replace(/([.*+?=^!:${}()|[\]\/\\])/g, '\\$1'); - }; - _.each(_.keys(shortcodes), function(key){ - str = str.replace( new RegExp("(?:^|\\s|<[a-z]*>)(" + re_escape(key) + ")(?:\\s|$|)"), ' '+shortcodes[key]+' '); - }); - return str; -} - -/** - * Transform the list of emoji (shortcode Object) into a key-array representing - * the substitution to apply - * @param {Ojbect[]} emoji_list : list of emoji object - * @returns {Object} mapping between the code (as key) and the substitution (as value) - */ -function shortcode_substitution(emoji_list){ - var emoji_substitution = {}; - _.each(emoji_list, function(emoji){ - emoji_substitution[emoji.source] = emoji.substitution; - }); - return emoji_substitution; -} - -/** - * Associate a font awesome class to a attachment file type - * @param {string} mimetype : mimetype to analyse - * @returns {string} the fa class associated - */ -function attachment_mimetype_icon(mimetype){ - var mapping = { - 'text/plain': 'fa fa-file-text-o', - 'text/css': 'fa fa-file-code-o', - 'text/javascript': 'fa fa-file-code-o', - 'text/html': 'fa fa-file-code-o', - 'image/jpeg': 'fa fa-file-photo-o', - 'image/png': 'fa fa-file-photo-o', - 'image/gif': 'fa fa-file-photo-o', - 'application/octet-stream': 'fa fa-file-code-o', - 'application/pdf': 'fa fa-file-pdf-o', - 'application/zip': 'fa fa-file-archive-o', - 'application/x-compressed': 'fa fa-file-archive-o', - 'application/msword': 'fa fa-file-word-o', - 'application/vnd.ms-excel': 'fa fa-file-excel-o', - 'application/mspowerpoint': 'fa fa-file-powerpoint-o', - 'application/powerpoint': 'fa fa-file-powerpoint-o', - 'application/vnd.ms-powerpoint': 'fa fa-file-powerpoint-o', - 'audio/mpeg3': 'fa fa-file-audio-o', - 'audio/x-mpeg-3': 'fa fa-file-audio-o', - 'audio/mpeg': 'fa fa-file-audio-o', - 'audio/midi': 'fa fa-file-audio-o', - 'video/msvideo': 'fa fa-file-movie-o', - 'video/mpeg': 'fa fa-file-movie-o', - 'video/quicktime': 'fa fa-file-movie-o', - }; - if(_.contains(_.keys(mapping), mimetype)){ - return mapping[mimetype]; - } - return 'fa fa-file-o'; -} - -/** - * Play the noise 'ting' to notify user - */ -function beep(session){ - if (typeof(Audio) === "undefined") { - return; - } - var audio = new Audio(); - var ext = audio.canPlayType("audio/ogg; codecs=vorbis") ? ".ogg" : ".mp3"; - audio.src = session.url("/mail/static/src/audio/ting") + ext; - audio.play(); -} - -return { - parse_email: parse_email, - get_text2html: get_text2html, - shortcode_apply: shortcode_apply, - shortcode_substitution: shortcode_substitution, - attachment_mimetype_icon: attachment_mimetype_icon, - beep: beep, -}; - -}); diff --git a/addons/mail/static/src/js/systray.js b/addons/mail/static/src/js/systray.js new file mode 100644 index 00000000000..8deb1ac24a0 --- /dev/null +++ b/addons/mail/static/src/js/systray.js @@ -0,0 +1,81 @@ +odoo.define('mail.systray', function (require) { +"use strict"; + +var core = require('web.core'); +var Model = require('web.Model'); +var SystrayMenu = require('web.SystrayMenu'); +var web_client = require('web.web_client'); +var Widget = require('web.Widget'); + +var chat_manager = require('mail.chat_manager'); + +/** + * Widget Top Menu Notification Counter + * + * Counter of notification in the Systray Menu. Need to know if InstantMessagingView is displayed to + * increment (or not) the counter. On click, should redirect to the client action. + **/ +var NotificationTopButton = Widget.extend({ + template:'mail.chat.NotificationTopButton', + events: { + "click": "on_click", + }, + willStart: function () { + var self = this; + var ir_model = new Model("ir.model.data"); + var def1 = ir_model.call("xmlid_to_res_id", ["mail.mail_channel_menu_root_chat"]); + var def2 = ir_model.call("xmlid_to_res_id", ["mail.mail_channel_action_client_chat"]); + return $.when(def1, def2, this._super()).then(function (menu_id, action_id) { + self.discuss_menu_id = menu_id; + self.client_action_id = action_id; + }); + }, + start: function () { + chat_manager.bus.on("update_needaction", this, this.update_counter); + this.update_counter(chat_manager.get_needaction_counter()); + return this._super(); + }, + update_counter: function (counter) { + this.$('.fa-comment').html(counter); + }, + on_click: function (event) { + var self = this; + event.preventDefault(); + this.do_action(this.client_action_id, {clear_breadcrumbs: true}).then(function () { + core.bus.trigger('change_menu_section', self.discuss_menu_id); + }); + }, +}); + +SystrayMenu.Items.push(NotificationTopButton); + + + +/** + * * Global ComposeMessage Top Button + * * + * * Add a link on the top user bar to write a full mail. It opens the form view + * * of the mail.compose.message (in a modal). + * */ +var ComposeMessageTopButton = Widget.extend({ + template:'mail.ComposeMessageTopButton', + events: { + "click": "on_compose_message", + }, + on_compose_message: function (ev) { + ev.preventDefault(); + web_client.action_manager.do_action({ + type: 'ir.actions.act_window', + res_model: 'mail.compose.message', + view_mode: 'form', + view_type: 'form', + views: [[false, 'form']], + target: 'new', + }); + }, +}); + +// Put the ComposeMessageTopButton widget in the systray menu +SystrayMenu.Items.push(ComposeMessageTopButton); + +}); diff --git a/addons/mail/static/src/js/thread.js b/addons/mail/static/src/js/thread.js new file mode 100644 index 00000000000..ffe13dbb7ae --- /dev/null +++ b/addons/mail/static/src/js/thread.js @@ -0,0 +1,127 @@ +odoo.define('mail.ChatThread', function (require) { +"use strict"; + +var core = require('web.core'); +var Widget = require('web.Widget'); + +var QWeb = core.qweb; +var _t = core._t; + +var ORDER = { + ASC: 1, + DESC: -1, +}; + +var Thread = Widget.extend({ + className: 'o_mail_thread', + + events: { + "click .o_mail_redirect": "on_click_redirect", + "click .o_thread_show_more": "on_click_show_more", + "click .o_thread_message_needaction": function (event) { + var message_id = $(event.currentTarget).data('message-id'); + this.trigger("mark_as_read", message_id); + }, + "click .o_thread_message_star": function (event) { + var message_id = $(event.currentTarget).data('message-id'); + this.trigger("toggle_star_status", message_id); + }, + }, + + init: function (parent, options) { + this._super.apply(this, arguments); + this.options = _.defaults(options || {}, { + display_order: ORDER.ASC, + display_needactions: true, + default_username: _t('Anonymous'), + display_document_link: true, + display_avatar: true, + }); + }, + + render: function (messages, options) { + var msgs = _.map(messages, this._preprocess_message.bind(this)); + if (this.options.display_order === ORDER.DESC) { + msgs.reverse(); + } + this.$el.html(QWeb.render('mail.ChatThread', { + messages: msgs, + options: _.extend({}, this.options, options), + ORDER: ORDER, + })); + }, + + on_click_redirect: function (event) { + event.preventDefault(); + var res_id = $(event.target).data('oe-id'); + var res_model = $(event.target).data('oe-model'); + this.trigger('redirect', res_model, res_id); + }, + + on_click_show_more: function () { + this.trigger('load_more_messages'); + }, + + _preprocess_message: function (message) { + var msg = _.extend({}, message); + + // Compute displayed author + if (!msg.author_id || !msg.author_id[1]) { + msg.displayed_author = msg.email_from || this.options.default_username; + } + + // Compute the avatar_url + if (msg.author_id && msg.author_id[0]) { + msg.avatar_src = "/web/image/res.partner/" + msg.author_id[0] + "/image_small"; + } else if (msg.msg_type === 'email') { + msg.avatar_src = "/mail/static/src/img/email_icon.png"; + } else { + msg.avatar_src = "/mail/static/src/img/smiley/avatar.jpg"; + } + + // Compute url of attachments + _.each(msg.attachment_ids, function(a) { + a.url = '/web/content/' + a.id + '?download=true'; + }); + + return msg; + }, + + /** + * Removes a message and re-renders the thread + * @param {int} [message_id] the id of the removed message + * @param {array} [messages] the list of messages to display, without the removed one + * @param {object} [options] options for the thread rendering + */ + remove_message_and_render: function (message_id, messages, options) { + var self = this; + this.$('.o_thread_message[data-message-id=' + message_id + ']').fadeOut({ + done: function () { self.render(messages, options); } + }); + }, + + /** + * Scrolls the thread to a given message or offset if any, to bottom otherwise + * @param {int} [target.id] optional: the id of the message to scroll to + * @param {int} [target.offset] optional: the number of pixels to scroll + */ + scroll_to: function (target) { + target = target || {}; + if (target.id !== undefined) { + this.$el.scrollTo(this.$('.o_thread_message[data-message-id=' + target.id + ']')); + } else if (target.offset !== undefined) { + this.$el.scrollTop(target.offset); + } else { + this.$el.scrollTop(this.el.scrollHeight); + } + }, + get_scrolltop: function () { + return this.$el.scrollTop(); + }, +}); + +Thread.ORDER = ORDER; + +return Thread; + +}); diff --git a/addons/mail/static/src/js/window_manager.js b/addons/mail/static/src/js/window_manager.js new file mode 100644 index 00000000000..5f0f559ba19 --- /dev/null +++ b/addons/mail/static/src/js/window_manager.js @@ -0,0 +1,115 @@ +odoo.define('mail.window_manager', function (require) { +"use strict"; + +var chat_manager = require('mail.chat_manager'); +var ChatWindow = require('mail.ChatWindow'); + +var core = require('web.core'); +var web_client = require('web.web_client'); + + +// chat window management +//---------------------------------------------------------------- +var chat_sessions = []; + +function open_chat (session) { + if (!_.findWhere(chat_sessions, {id: session.id})) { + var chat_session = { + id: session.id, + uuid: session.uuid, + name: session.name, + window: new ChatWindow(web_client, session.id, session.name, session.is_folded), + }; + 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) { + chat_manager.fold_channel(channel_id); + }); + + chat_session.window.on("post_message", null, function (message) { + chat_manager.post_message(message); + }); + + chat_sessions.push(chat_session); + chat_session.window.appendTo($('body')) + .then(reposition_windows) + .then(function () { + return chat_manager.get_messages({channel_id: chat_session.id}); + }).then(function (messages) { + chat_session.window.render(messages); + chat_session.window.scrollBottom(); + }); + } +} + +function close_chat (chat_session) { + var session = _.find(chat_sessions, {id: chat_session.id}); + if (session) { + chat_sessions = _.without(chat_sessions, session); + session.window.destroy(); + reposition_windows(); + } +} + +function toggle_fold_chat (channel) { + var session = _.find(chat_sessions, {id: channel.id}); + if (session) { + session.window.toggle_fold(); + } +} + +function reposition_windows () { + _.each(chat_sessions, function (session, index) { + session.window.$el.css({right: 245 * index, bottom: 0}); + }); +} + +function update_sessions (message, scrollBottom) { + _.each(chat_sessions, function (session) { + if (_.contains(message.channel_ids, session.id)) { + chat_manager.get_messages({channel_id: session.id}).then(function (messages) { + session.window.render(messages); + if (scrollBottom) { + session.window.scrollBottom(); + } + }); + } + }); +} + + +core.bus.on('web_client_ready', null, function () { + chat_manager.bus.on('open_chat', null, open_chat); + chat_manager.bus.on('close_chat', null, close_chat); + 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 (_.contains(channel.id, session.id)) { + query.is_displayed = true; + } + }); + }); + + _.each(chat_manager.get_channels(), function (channel) { + if (channel.is_detached) { + open_chat(channel); + } + }); +}); + +}); diff --git a/addons/mail/static/src/less/chat_window.less b/addons/mail/static/src/less/chat_window.less new file mode 100644 index 00000000000..43fb5c7f6f1 --- /dev/null +++ b/addons/mail/static/src/less/chat_window.less @@ -0,0 +1,50 @@ +@o-chat-window-bg: lighten(lightgray, 13%); + +.o_chat_window { + .o-flex-display(); + .o-flex-flow(column, nowrap); + position: fixed; + width: 240px; + height: 333px; + background-color: @o-chat-window-bg; + border: 1px solid gray; + + .o_chat_header { + .o-flex-display(); + .o-flex(0, 0, auto); + color: white; + background-color: @odoo-brand-primary; + padding: 5px; + + .o_chat_title { + cursor: pointer; + .o-flex(1, 1, auto); + } + + .o_chat_window_close { + color: white; + padding: 3px; + &:hover { + background-color: darken(@odoo-brand-primary, 10%); + } + } + } + + .o_chat_content { + .o-flex(1, 1, auto); + overflow: auto; + padding: 5px; + + .o_mail_thread .o_thread_date_separator { + border-color: lightgray; + .o_thread_date { + background-color: @o-chat-window-bg; + } + } + } + + .o_chat_input > input { + padding: 5px; + width: 100%; + } +} diff --git a/addons/mail/static/src/less/mail_followers.less b/addons/mail/static/src/less/chatter.less similarity index 69% rename from addons/mail/static/src/less/mail_followers.less rename to addons/mail/static/src/less/chatter.less index 557f9b4f286..2e570f6cc86 100644 --- a/addons/mail/static/src/less/mail_followers.less +++ b/addons/mail/static/src/less/chatter.less @@ -1,14 +1,48 @@ -.o_mail_followers { - height: 0px; // to be on the same line as mail composer buttons - float: right; - @media (min-width: @screen-sm-min) { - &.o_form_field { - margin: 0; // override o_form_field's margin-bottom of 5px +.o_chatter { + width: 100%; + + .o_chat_composer { + margin-top: 10px; + + .o_chatter_composer_info { + display: inline-block; + margin-bottom: 5px; + color: @odoo-main-color-muted; + font-style: italic; + } + + .o_chatter_composer_subtypes { + > label { + display: inline-block; // Required for old design compatibility + width: 100px; + } + + .o_chatter_composer_subtype_select { + display: inline-block; + min-width: 150px; + width: auto; + } + } + + .o_composer { + margin-top: 10px; + + .o_composer_input { + width: 100%; + margin-bottom: 5px; + } } } + + .o_mail_thread { + margin-top: 10px; + } +} + +.o_followers { + float: right; @media (max-width: @screen-xs-max) { - height: auto; // to be displayed above the mail composer buttons - float: left; + display: none; // Hide the followers in mobile } .o_followers_actions { @@ -16,7 +50,7 @@ float: right; .o_followers_follow_button { - width: 110px; + width: 120px; .o_follow, .o_followers_unfollow, .o_followers_following { display: none; @@ -49,7 +83,7 @@ float: right; margin-left: 5px; > button { - width: 120px; + width: 140px; } .o_followers_count { @@ -80,7 +114,7 @@ } .o_edit_subtype { .o-position-absolute(6px, 18px); - cursor: pointer; + cursor: pointer; } } diff --git a/addons/mail/static/src/less/client_action.less b/addons/mail/static/src/less/client_action.less new file mode 100644 index 00000000000..53392488146 --- /dev/null +++ b/addons/mail/static/src/less/client_action.less @@ -0,0 +1,190 @@ +// ------------------------------------------------------------------ +// Layout +// ------------------------------------------------------------------ +.o_mail_chat { + .o-position-absolute(0, 0, 0, 0); + height: 100%; + + @media (max-width: @screen-xs-max) { + position: static; + height: auto; + } + + .o-flex-display(); + .o-flex-flow(row, nowrap); + + .o_mail_chat_sidebar { + .o-flex(0, 0, 200px); + overflow: auto; + padding: 8px 0; + + .o_mail_sidebar_title { + .o-webclient-padding(@top: 5px, @bottom: 5px); + .o-flex-display(); + align-items: center; + justify-content: space-between; + h4 { + text-transform: uppercase; + } + .o_add { + padding: 8px; + } + } + .o_mail_chat_channel_item { + width: 100%; + .o-webclient-padding(@top: 5px, @bottom: 5px); + padding-left: 1.5*@odoo-horizontal-padding; // larger padding than titles + > .badge { + margin: 0; + } + .o_mail_partner_unpin { + padding: 2px 8px; + } + &.o_unread_message { + font-weight: bold; + } + } + .o_mail_add_channel { + width: 100%; + .o-webclient-padding(@top: 5px, @bottom: 5px); + padding-left: 1.5*@odoo-horizontal-padding; // larger padding than titles + span { + margin-right: 5px; + } + } + } + + .o_mail_chat_content { + .o-flex(1, 1, 100%); + height: 100%; + .o-flex-display(); + .o-flex-flow(column, nowrap); + @media (max-width: @screen-xs-max) { + height: auto; + } + + .o_mail_thread { + .o-flex(1, 0, 0); + overflow: auto; + .o-webclient-padding(); + + .o_chat_help_message { + padding: 25px 0; + text-align: justify; + } + } + .o_chat_composer { + .o-flex(0, 0, auto); + .o-webclient-padding(@top: 10px, @bottom: 5px); + + .o_composer { + .o-flex-display(); + + .o_composer_input { + .o-flex(1, 1, auto); + padding: 5px; + margin-right: 5px; + border: none; + } + + .o_composer_buttons { + .o-flex(0, 0, auto); + } + } + } + } +} + +// ------------------------------------------------------------------ +// Style +// ------------------------------------------------------------------ +.o_mail_chat { + .o_mail_chat_sidebar { + background-color: @gray; + color: @gray-lighter; + + .o_mail_chat_channel_item { + overflow: hidden; + text-overflow: ellipsis; + cursor: pointer; + &:hover { + background-color: @gray-dark; + } + + > .badge { + color: white; + background-color: @odoo-brand-optional; + } + + &.o_active { + background-color: @gray-dark; + } + } + + .o_mail_add_channel { + display: none; + input { + color: black; + } + } + + .o_mail_chat_channel_unread { + font-weight: bold; + } + + .o_mail_sidebar_title { + h4 { + font-size: 13px; + text-transform: uppercase; + } + .o_add { + cursor: pointer; + &:hover { + background-color: @gray-dark; + } + } + } + + // .o_mail_chat_add_more_search { + // padding: @padding-base-vertical @padding-base-horizontal; + // .o_mail_chat_add_more_text { + // color: @gray-lighter; + // display: inline-block; + // font-style: italic; + // cursor: pointer; + // font-size: @font-size-base; + // } + // .o_mail_chat_add_more_text:hover { + // text-decoration: underline; + // color: white; + // cursor: pointer; + // } + // } + + } + + .o_mail_chat_content { + background-color: white; + + .o_chat_composer { + border-top: 1px solid @odoo-brand-optional; + background-color: @odoo-brand-optional; + + .o_attachment { + .o_attachment_name, .o_attachment_delete { + color: white; + } + } + + .o_composer_mentioned_partners { + color: white; + + > .badge { + color: white; + border: 1px solid white; + background-color: transparent; + } + } + } + } +} diff --git a/addons/mail/static/src/less/composer.less b/addons/mail/static/src/less/composer.less new file mode 100644 index 00000000000..a53ffd34f70 --- /dev/null +++ b/addons/mail/static/src/less/composer.less @@ -0,0 +1,97 @@ +.o_composer_mentioned_partners, .o_composer_attachments_list { + margin-top: 5px; + text-align: justify; +} + +// Emoji size in the popover +.o_mail_emoji { + display: inline-block; + height: 24px; + width: 24px; + + > img { + cursor: pointer; + width: 100%; + } +} + +// Attachment Icons (common for chat thread and chat composer) +// ------------------------------------------------------------------ +@-webkit-keyframes oe_mail_attach_loading_anim { + 0% { background: @odoo-brand-optional } + 50% { background: darken(@odoo-brand-optional, 10%) } + 100% { background: @odoo-brand-optional } +} +@-moz-keyframes oe_mail_attach_loading_anim { + 0% { background: @odoo-brand-optional } + 50% { background: darken(@odoo-brand-optional, 10%) } + 100% { background: @odoo-brand-optional } +} +@-o-keyframes oe_mail_attach_loading_anim { + 0% { background: @odoo-brand-optional } + 50% { background: darken(@odoo-brand-optional, 10%) } + 100% { background: @odoo-brand-optional } +} +@keyframes oe_mail_attach_loading_anim { + 0% { background: @odoo-brand-optional } + 50% { background: darken(@odoo-brand-optional, 10%) } + 100% { background: @odoo-brand-optional } +} + +@o-mail-attachment-image-size: 60px; +.o_attachment { + position: relative; + width: 2*@o-mail-attachment-image-size; + display: inline-block; + height: @o-mail-attachment-image-size; + text-align: center; + margin-bottom: 15px; + + .o_image { + .o-position-absolute(@top: 0, @left: @o-mail-attachment-image-size/2); + width: @o-mail-attachment-image-size; + height: @o-mail-attachment-image-size; + } + + .o_attachment_name { + display: inline-block; + max-width: 100%; + margin-top: @o-mail-attachment-image-size; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + color: @odoo-main-text-color; + } + + .o_attachment_progress_bar { + display: none; + } + .o_attachment_delete { + .o-position-absolute(@top: 0, @right: 0); + color: @odoo-main-text-color; + cursor: pointer; + opacity: 0; + .o-transition(opacity, 0.2s, linear); + } + &.o_attachment_uploading { + .o_attachment_progress_bar { + display: block; + .o-position-absolute(@top: 18px, @left: 16px, @right: 16px); + height: 17px; + line-height: 13px; + color: white; + border-radius: @border-radius-base; + border: solid 1px grey; + box-shadow: 0px 1px 10px @odoo-shadow-color; + .o-animation(@name: oe_mail_attach_loading_anim, @duration: 1s, @timing-function: linear, @iteration-count: infinite); + } + .o_attachment_delete { + display: none; + } + } + + &:hover .o_attachment_delete { + opacity: 1; + .o-transition(opacity, 0.2s, linear); + } +} diff --git a/addons/mail/static/src/less/mail_chat_backend.less b/addons/mail/static/src/less/mail_chat_backend.less deleted file mode 100644 index f93d1ed2448..00000000000 --- a/addons/mail/static/src/less/mail_chat_backend.less +++ /dev/null @@ -1,128 +0,0 @@ - -// ------------------------------------------------------------------ -// Layout -// ------------------------------------------------------------------ -.o_mail_chat { - .o-position-absolute(0, 0, 0, 0); - height: 100%; - - @media (max-width: @screen-xs-max) { - position: static; - height: auto; - } - - .o-flex-display(); - .o-flex-flow(row, nowrap); - - .o_mail_chat_sidebar { - .o-flex(0, 0, 15%); - overflow: auto; - padding: 1%; - } - - .o_mail_chat_content { - .o-flex(0, 0, 85%); - height: 100%; - .o-flex-display(); - .o-flex-flow(column, nowrap); - @media (max-width: @screen-xs-max) { - height: auto; - } - - .o_mail_chat_messages { - .o-flex(1, 0, 0); - overflow: auto; - .o-webclient-padding(); - - .o_mail_chat_help_message { - padding: 25px 0; - text-align: justify; - } - } - - .o_mail_chat_composer_wrapper { - .o-flex(0, 0, auto); - .o-webclient-padding(@top: 10px, @bottom: 5px); - } - } -} - -// ------------------------------------------------------------------ -// Style -// ------------------------------------------------------------------ -.o_mail_chat { - .o_mail_chat_sidebar { - background-color: @gray; - - > .nav-pills { - > li { - > a { - color: @odoo-brand-optional; // community compatibility: override base.css - &:hover, &:focus { - background-color: @gray-dark; - } - &.active { - background-color: @gray-dark; - } - } - - &.o_mail_chat_channel_item, &.o_mail_chat_partner_item { - > a { - color: @gray-lighter; - > .badge { - color: white; - background-color: @odoo-brand-optional; - } - - > i.fa { - padding-top: 3px; - padding-right: 3px; - - &.o_online { - color: @odoo-brand-optional; - } - } - } - } - } - } - - .o_mail_chat_channel_item.o_mail_chat_channel_unread, .o_mail_chat_partner_item.o_mail_chat_channel_unread{ - font-weight: bold; - } - - .o_mail_chat_sidebar_title { - text-transform: uppercase; - color: @gray-lighter; - margin-top: 25px; - .user-select(none); - cursor: default; - } - - .o_mail_chat_add_more_search{ - padding: @padding-base-vertical @padding-base-horizontal; - .o_mail_chat_add_more_text{ - color: @gray-lighter; - display: inline-block; - font-style: italic; - cursor: pointer; - font-size: @font-size-base; - } - .o_mail_chat_add_more_text:hover{ - text-decoration: underline; - color: white; - cursor: pointer; - } - } - - } - - .o_mail_chat_content { - background-color: white; - - .o_mail_chat_composer_wrapper { - border-top: 1px solid @gray-lighter; - background-color: #21b799; - } - } -} diff --git a/addons/mail/static/src/less/mail_chat_common.less b/addons/mail/static/src/less/mail_chat_common.less deleted file mode 100644 index 97119ed506c..00000000000 --- a/addons/mail/static/src/less/mail_chat_common.less +++ /dev/null @@ -1,115 +0,0 @@ - -/* emoji size */ -.o_mail_emoji img{ - height: 20px; -} - -// ------------------------------------------------------------------ -// Chat Window -// ------------------------------------------------------------------ - -@media screen and (max-width:360px) { - .o_mail_chat_im_window { - width: 100%; - margin-right: 0; - } - .o_mail_chat_im_window .o_mail_chat_im_window_content { - width: 100% !important; - } - .o_mail_chat_im_window .o_mail_chat_im_window_input { - width: 100% !important; - } -} - - -.o_mail_chat_im_window { - position: fixed; - overflow: hidden; - margin-bottom: 5px; - margin-right: 6px; - background: rgba(250, 250, 250, 0.95); - border: 1px solid #DEDEDE; - width: 240px; - z-index:500; -} - -/* HEADER */ -.o_mail_chat_im_window .o_mail_chat_im_window_header { - padding: 3px 6px 2px; - cursor: pointer; - background: #DEDEDE; - background: -moz-linear-gradient(#FCFCFC, #DEDEDE); - background: -webkit-gradient(linear, left top, left bottom, from(#FCFCFC), to(#DEDEDE)); - border-radius: 3px 3px 0 0; - -moz-border-radius: 3px 3px 0 0; - -webkit-border-radius: 3px 3px 0 0; -} - -.o_mail_chat_im_window .o_mail_chat_im_window_header_name { - max-width: 75%; - word-wrap: break-word; - display: inline-block; - height: 15px; - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; -} - -.o_mail_chat_im_window .o_mail_chat_im_window_header_buttons{ - float: right; -} - - -/* CONTENT (messages, ...) */ -.o_mail_chat_im_window .o_mail_chat_im_window_content { - height: 280px; - overflow-y: scroll; -} - -.o_mail_chat_im_window .o_mail_chat_im_messages .o_mail_chat_im_window_date_sperator{ - padding: 5px; - padding-right: 10px; - text-align: right; - font-weight: bold; -} - -.o_mail_chat_im_window .o_mail_chat_im_message{ - margin: auto; - margin-bottom: 5px; -} - -.o_mail_chat_im_window .o_mail_chat_im_message .o_mail_chat_im_message_avatar{ - width: 20%; - display: inline-block; - vertical-align: top; -} - -.o_mail_chat_im_window .o_mail_chat_im_message .o_mail_chat_im_message_avatar img{ - width: 80%; - float: right; -} - -.o_mail_chat_im_window .o_mail_chat_im_message .o_mail_chat_im_message_core{ - width: 75%; - display: inline-block; - padding: 5px; - - background: white; - border-radius: 2px; - box-shadow: 0 1px 2px rgba(0, 0, 0, 0.2); - max-width:100%; -} - -.o_mail_chat_im_window .o_mail_chat_im_message .o_mail_chat_im_message_core p{ - margin: 0; -} - -/* FOOTER */ -.o_mail_chat_im_window .o_mail_chat_im_window_footer { - padding:3px; - background: #DEDEDE; -} - -.o_mail_chat_im_window .o_mail_chat_im_window_footer .o_mail_chat_im_window_input { - width: 100%; -} diff --git a/addons/mail/static/src/less/mail_chatter.less b/addons/mail/static/src/less/mail_chatter.less deleted file mode 100644 index 501f74c15a2..00000000000 --- a/addons/mail/static/src/less/mail_chatter.less +++ /dev/null @@ -1,20 +0,0 @@ -.o_mail_chatter { - width: 100%; - - .o_mail_chatter_compose_message { - display: none; - margin-top: 10px; - .o_mail_chatter_compose_message_suggested_partners label { - vertical-align: middle; - display: block; - line-height: 14px; - } - .o_mail_compose_message_buttons > button { - .btn-sm; - } - } - - .o_mail_chatter_messages { - margin-top: 10px; - } -} diff --git a/addons/mail/static/src/less/mail_thread.less b/addons/mail/static/src/less/mail_thread.less deleted file mode 100644 index 3f7b78201fe..00000000000 --- a/addons/mail/static/src/less/mail_thread.less +++ /dev/null @@ -1,194 +0,0 @@ -// ------------------------------------------------------------------ -// Mail Thread 'Mixin' -// ------------------------------------------------------------------ -.o_mail_thread { - .o_mail_thread_date_separator, .o_mail_thread_message { - margin: auto; // override the margin-{left,right}: -15px defined on .row - } - .o_mail_thread_date_separator { - .o_mail_thread_date_wrapper { - border-bottom: solid @gray-lighter 2px; - margin-bottom: 15px; - .o_mail_thread_date_separator_title { - display: table; - position: relative; - top: 10px; - margin: 0 auto; - padding: 0 10px 0 10px; - font-weight: bold; - background: white; - } - } - } - @mail_thread_avatar_size: 50px; - .o_mail_thread_message { - .o-flex-display(); - margin: 10px 0; - .o_mail_thread_message_avatar { - .o-flex(0, 0, @mail_thread_avatar_size); - margin-right: 10px; - @media (max-width: @screen-xs-max) { - .o-flex(0, 0, @mail_thread_avatar_size/2); - } - .o_mail_thread_avatar { - display: block; - margin: 0 auto; // center the avatar - max-width: @mail_thread_avatar_size; - @media (max-width: @screen-xs-max) { - max-width: @mail_thread_avatar_size/2; - } - } - } - .o_mail_thread_message_core { - .o-flex(1, 1, auto); - > p { - margin: 0 0 9px; // Required by the old design to override a general rule on p's - } - .o_mail_body_long { - display: none; - } - } - .o_mail_thread_message_star, .o_mail_thread_message_needaction { - cursor: pointer; - opacity: 0.5; - &:hover { - opacity: 1; - } - } - .o_mail_thread_message_star.o_mail_message_starred { - color: gold; - } - .o_mail_redirect { - cursor: pointer; - } - } - .o_mail_thread_show_more { - text-align: center; - } -} - -// ------------------------------------------------------------------ -// Mail Compose Message -// ------------------------------------------------------------------ -.o_mail_chat_composer_wrapper { - .o_mail_compose_message { // use flex for the layout in the Chat app - .o-flex-display(); - .o_mail_compose_message_input { - .o-flex(1, 1, auto); - } - .o_mail_compose_message_buttons { - .o-flex(0, 0, auto); - } - } -} -.o_mail_compose_message { - width: 100%; - input.o_mail_compose_message_input { - border: none; - padding: 5px; - margin-right: 5px; - } - textarea.o_mail_compose_message_input { - width: 100%; - margin: 5px 0; - } -} -.o_mail_mention_partner_tags { - margin-top: 5px; - text-align: justify; -} -.o_mail_attachments_list .o_mail_chat_im_attachment_files { - margin-top: 5px; - text-align: justify; -} -// Emoji size in the popover -.o_mail_compose_message_emoji { - .o_mail_compose_message_emoji_img > img { - cursor: pointer; - height: 26px; - } -} - -// ------------------------------------------------------------------ -// Attachment Icons (common for mail thread message and compose message) -// ------------------------------------------------------------------ -@o-mail-attachment-image-size: 60px; - -@-webkit-keyframes oe_mail_attach_loading_anim { - 0% { background: @odoo-brand-optional } - 50% { background: darken(@odoo-brand-optional, 10%) } - 100% { background: @odoo-brand-optional } -} -@-moz-keyframes oe_mail_attach_loading_anim { - 0% { background: @odoo-brand-optional } - 50% { background: darken(@odoo-brand-optional, 10%) } - 100% { background: @odoo-brand-optional } -} -@-o-keyframes oe_mail_attach_loading_anim { - 0% { background: @odoo-brand-optional } - 50% { background: darken(@odoo-brand-optional, 10%) } - 100% { background: @odoo-brand-optional } -} -@keyframes oe_mail_attach_loading_anim { - 0% { background: @odoo-brand-optional } - 50% { background: darken(@odoo-brand-optional, 10%) } - 100% { background: @odoo-brand-optional } -} - -.o_mail_attachment { - position: relative; - width: 2*@o-mail-attachment-image-size; - display: inline-block; - height: 50px; - text-align: center; - margin-bottom: 15px; - - .o_image { - .o-position-absolute(@top: 0, @left: @o-mail-attachment-image-size/2); - width: @o-mail-attachment-image-size; - height: @o-mail-attachment-image-size; - } - - .o_mail_attachment_name { - display: inline-block; - max-width: 100%; - margin-top: @o-mail-attachment-image-size; - overflow: hidden; - text-overflow: ellipsis; - color: @odoo-main-text-color; - } - - &.o_mail_uploading { - .o_mail_attachment_progress_bar { - display: block; - .o-position-absolute(@top: 18px, @left: 16px, @right: 16px); - height: 17px; - line-height: 13px; - color: white; - border-radius: @border-radius-base; - border: solid 1px grey; - box-shadow: 0px 1px 10px @odoo-shadow-color; - .o-animation(@name: oe_mail_attach_loading_anim, @duration: 1s, @timing-function: linear, @iteration-count: infinite); - } - .o_mail_attachment_delete { - display: none; - } - } - - .o_mail_attachment_progress_bar { - display: none; - } - - .o_mail_attachment_delete { - .o-position-absolute(@top: 0, @right: 0); - color: @odoo-main-text-color; - cursor: pointer; - opacity: 0; - .o-transition(opacity, 0.2s, linear); - } - - &:hover .o_mail_attachment_delete { - opacity: 1; - .o-transition(opacity, 0.2s, linear); - } -} diff --git a/addons/mail/static/src/less/thread.less b/addons/mail/static/src/less/thread.less new file mode 100644 index 00000000000..61b3c571868 --- /dev/null +++ b/addons/mail/static/src/less/thread.less @@ -0,0 +1,71 @@ +@mail-thread-avatar-size: 50px; + +.o_mail_thread { + .o_thread_show_more { + text-align: center; + } + + .o_thread_date_separator { + margin-bottom: 15px; + border-bottom: solid @gray-lighter 2px; + text-align: center; + + .o_thread_date { + position: relative; + top: 10px; + margin: 0 auto; + padding: 0 10px; + font-weight: bold; + background: white; + } + } + + .o_thread_message { + .o-flex-display(); + margin: 10px 0; + + .o_thread_message_avatar { + .o-flex(0, 0, @mail-thread-avatar-size); + margin-right: 10px; + @media (max-width: @screen-xs-max) { + .o-flex(0, 0, @mail-thread-avatar-size/2); + } + + .o_thread_message_avatar_img { + display: block; + margin: 0 auto; // center the avatar + max-width: @mail-thread-avatar-size; + @media (max-width: @screen-xs-max) { + max-width: @mail-thread-avatar-size/2; + } + } + } + + .o_thread_message_core { + .o-flex(1, 1, auto); + + .o_mail_subject { + font-style: italic; + } + + > p { + margin: 0 0 9px; // Required by the old design to override a general rule on p's + } + + .o_thread_message_star, .o_thread_message_needaction { + cursor: pointer; + opacity: 0.5; + &:hover { + opacity: 1; + } + &.fa-star { + color: gold; + } + } + + .o_mail_redirect { + cursor: pointer; + } + } + } +} diff --git a/addons/mail/static/src/xml/chat_window.xml b/addons/mail/static/src/xml/chat_window.xml new file mode 100644 index 00000000000..e94c51767c2 --- /dev/null +++ b/addons/mail/static/src/xml/chat_window.xml @@ -0,0 +1,24 @@ + + + + + +

    +
    + + + + + + +
    +
    + +
    +
    + +
    +
    + + + diff --git a/addons/mail/static/src/xml/chatter.xml b/addons/mail/static/src/xml/chatter.xml new file mode 100644 index 00000000000..51b0fc78eed --- /dev/null +++ b/addons/mail/static/src/xml/chatter.xml @@ -0,0 +1,165 @@ + + + + + + + + + + + Log an internal note which will not be sent to followers, but which can be read + by users accessing this document. + + +
    + + +
    +
    + + + + + To: Followers of + + + + + this document + + + +
    + +
    + + + () + (no email address) +
    +
    +
    +
    +
    + + + +