-> [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|$|[a-z]*>)"), ' '+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.
+
+
+
+ Activity
+
+
+
+
+
+
+
+
+
+
+
+ To: Followers of
+
+
+
+
+ this document
+
+
+
+
+
+
+
+
+ ( )
+ (no email address)
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ New message
+
+
+ Log an internal note
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Follow
+
+ Unfollow
+
+ Following
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Add Followers
+
+
+ Add Channels
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/addons/mail/static/src/xml/client_action.xml b/addons/mail/static/src/xml/client_action.xml
new file mode 100644
index 00000000000..e4e80250b92
--- /dev/null
+++ b/addons/mail/static/src/xml/client_action.xml
@@ -0,0 +1,101 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Invite
+ Detach
+
+
+
+
+
+
+
diff --git a/addons/mail/static/src/xml/composer.xml b/addons/mail/static/src/xml/composer.xml
new file mode 100644
index 00000000000..39e0b5afdc2
--- /dev/null
+++ b/addons/mail/static/src/xml/composer.xml
@@ -0,0 +1,57 @@
+
+
+
+
+
+
+
+
+
+
+
+ /web/binary/upload_attachment
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Mention(s):
+
+
+
+
+
+
diff --git a/addons/mail/static/src/xml/mail_chat_backend.xml b/addons/mail/static/src/xml/mail_chat_backend.xml
deleted file mode 100644
index 253cd1e95aa..00000000000
--- a/addons/mail/static/src/xml/mail_chat_backend.xml
+++ /dev/null
@@ -1,139 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- Invite
- Minimize
-
-
-
-
diff --git a/addons/mail/static/src/xml/mail_chat_common.xml b/addons/mail/static/src/xml/mail_chat_common.xml
deleted file mode 100644
index 7bbbc3f94d7..00000000000
--- a/addons/mail/static/src/xml/mail_chat_common.xml
+++ /dev/null
@@ -1,93 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- -
-
-
- -
-
-
- -
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/addons/mail/static/src/xml/mail_chatter.xml b/addons/mail/static/src/xml/mail_chatter.xml
deleted file mode 100644
index 860633d6096..00000000000
--- a/addons/mail/static/src/xml/mail_chatter.xml
+++ /dev/null
@@ -1,98 +0,0 @@
-
-
-
-
-
-
-
-
- Log an internal note which will not be sent to followers.
- Note that users reading the document can read internal notes.
-
-
-
-
Activity
-
-
-
-
-
-
-
-
-
-
-
-
-
- To: Followers of
-
-
-
-
- this document
-
-
-
-
-
-
-
-
- ( )
- (no email address)
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- New message
-
-
- Log an internal note
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- No Messages.
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/addons/mail/static/src/xml/mail_followers.xml b/addons/mail/static/src/xml/mail_followers.xml
deleted file mode 100644
index c25d3954bb1..00000000000
--- a/addons/mail/static/src/xml/mail_followers.xml
+++ /dev/null
@@ -1,80 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- Follow
- Unfollow
- Following
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- Add followers
-
-
- Add Channels
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/addons/mail/static/src/xml/mail_thread.xml b/addons/mail/static/src/xml/mail_thread.xml
deleted file mode 100644
index 1323cb9c21e..00000000000
--- a/addons/mail/static/src/xml/mail_thread.xml
+++ /dev/null
@@ -1,196 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- Send
-
-
-
-
-
-
-
-
-
-
-
-
- /web/binary/upload_attachment
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- Mention(s):
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- Note by
-
-
-
-
-
-
-
-
-
-
-
-
-
- -
-
-
- on
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- -------- Show more messages --------
-
-
-
-
-
-
-
-
diff --git a/addons/mail/static/src/xml/systray.xml b/addons/mail/static/src/xml/systray.xml
new file mode 100644
index 00000000000..5f355d88969
--- /dev/null
+++ b/addons/mail/static/src/xml/systray.xml
@@ -0,0 +1,20 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/addons/mail/static/src/xml/thread.xml b/addons/mail/static/src/xml/thread.xml
new file mode 100644
index 00000000000..28717bc70a3
--- /dev/null
+++ b/addons/mail/static/src/xml/thread.xml
@@ -0,0 +1,122 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Note by
+
+
+
+
+
+
+
+
+
+ -
+
+
+ on
+
+
+
+
+
+
Subject:
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Uploading
+
+
+
+
+
+
+
+ -------- Show more messages --------
+
+
+
+
diff --git a/addons/mail/views/mail_templates.xml b/addons/mail/views/mail_templates.xml
index dfe1e8befc4..8afb474e9f2 100644
--- a/addons/mail/views/mail_templates.xml
+++ b/addons/mail/views/mail_templates.xml
@@ -3,24 +3,24 @@
-
-
+
+
+
+
+
+
+
+
+
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
diff --git a/addons/mail_tip/static/src/js/mail_tip.js b/addons/mail_tip/static/src/js/mail_tip.js
index fa9151b79d4..60fd0613192 100644
--- a/addons/mail_tip/static/src/js/mail_tip.js
+++ b/addons/mail_tip/static/src/js/mail_tip.js
@@ -1,9 +1,9 @@
odoo.define('mail_tip.mail_tip', function (require) {
"use strict";
-var mail = require('mail.chatter');
+var Chatter = require('mail.Chatter');
-mail.ChatterMailThread.include({
+Chatter.include({
render_value: function() {
var self = this;
this._super.apply(this, arguments).done(function() {
diff --git a/addons/web/static/src/js/boot.js b/addons/web/static/src/js/boot.js
index 80adf9874ca..c63acb01bdf 100644
--- a/addons/web/static/src/js/boot.js
+++ b/addons/web/static/src/js/boot.js
@@ -262,7 +262,7 @@
log_when_loaded();
}
});
- }, 100);
+ }, 1000);
};
$(log_when_loaded);
diff --git a/addons/web/static/src/js/framework/ajax.js b/addons/web/static/src/js/framework/ajax.js
index d6619449a0f..869e0214ad9 100644
--- a/addons/web/static/src/js/framework/ajax.js
+++ b/addons/web/static/src/js/framework/ajax.js
@@ -15,7 +15,9 @@ function genericJsonRpc (fct_name, params, fct) {
var xhr = fct(data);
var result = xhr.pipe(function(result) {
if (result.error !== undefined) {
- console.error("Server application error", JSON.stringify(result.error));
+ if (result.error.data.arguments[0] !== "bus.Bus not available in test mode") {
+ console.error("Server application error", JSON.stringify(result.error));
+ }
return $.Deferred().reject("server", result.error);
} else {
return result.result;
diff --git a/addons/web/static/src/less/enterprise_compatibility.less b/addons/web/static/src/less/enterprise_compatibility.less
index b1cabee1a0c..7dc4238f684 100644
--- a/addons/web/static/src/less/enterprise_compatibility.less
+++ b/addons/web/static/src/less/enterprise_compatibility.less
@@ -8,27 +8,8 @@
// The problem is that we have two web clients in parallel, and every
// addons should behave properly with both.
-@odoo-brand-primary: #337ab7;
-@odoo-brand-optional: @odoo-brand-primary;
-@odoo-brand-secondary: #8f8f8f;
-@odoo-brand-lightsecondary: #e2e2e0;
-@odoo-list-grey: @odoo-brand-lightsecondary; // FIXME: listview thead background color
-@odoo-webclient-background-color: #F0EEEE;
-@odoo-view-background-color: white;
-@odoo-shadow-color: #303030;
@brand-primary: @odoo-brand-primary;
-@odoo-font-size-base: 13px;
-@odoo-font-size-base-touch: 15px;
-@odoo-main-text-color: #555555;
-@odoo-main-color-muted: #a8a8a8;
-
-@odoo-form-group-cols: 12;
-@odoo-horizontal-padding: 16px;
-
-@screen-xs-min: 475px;
-@screen-xxs-max: (@screen-xs-min - 1);
-
.oe-view-manager {
background-color: @odoo-webclient-background-color !important;
}
diff --git a/addons/web/static/src/less/variables.less b/addons/web/static/src/less/variables.less
new file mode 100644
index 00000000000..1984696aafb
--- /dev/null
+++ b/addons/web/static/src/less/variables.less
@@ -0,0 +1,19 @@
+@odoo-brand-primary: #337ab7;
+@odoo-brand-optional: @odoo-brand-primary;
+@odoo-brand-secondary: #8f8f8f;
+@odoo-brand-lightsecondary: #e2e2e0;
+@odoo-list-grey: @odoo-brand-lightsecondary; // FIXME: listview thead background color
+@odoo-webclient-background-color: #F0EEEE;
+@odoo-view-background-color: white;
+@odoo-shadow-color: #303030;
+
+@odoo-font-size-base: 13px;
+@odoo-font-size-base-touch: 15px;
+@odoo-main-text-color: #555555;
+@odoo-main-color-muted: #a8a8a8;
+
+@odoo-form-group-cols: 12;
+@odoo-horizontal-padding: 16px;
+
+@screen-xs-min: 475px;
+@screen-xxs-max: (@screen-xs-min - 1);
diff --git a/addons/web/views/webclient_templates.xml b/addons/web/views/webclient_templates.xml
index 03723cf9a24..2f066dc7c30 100644
--- a/addons/web/views/webclient_templates.xml
+++ b/addons/web/views/webclient_templates.xml
@@ -101,6 +101,7 @@
+
diff --git a/addons/website/views/website_templates.xml b/addons/website/views/website_templates.xml
index b56d76e8e53..969b6f8a8ca 100644
--- a/addons/website/views/website_templates.xml
+++ b/addons/website/views/website_templates.xml
@@ -22,6 +22,9 @@
+
+
+
diff --git a/addons/website_livechat/views/website_livechat.xml b/addons/website_livechat/views/website_livechat.xml
index 02a406dbceb..702482bb707 100644
--- a/addons/website_livechat/views/website_livechat.xml
+++ b/addons/website_livechat/views/website_livechat.xml
@@ -9,11 +9,12 @@
-
-
+
+
-
-
+
+
+
diff --git a/openerp/modules/registry.py b/openerp/modules/registry.py
index 96d9e0a3687..b2319725291 100644
--- a/openerp/modules/registry.py
+++ b/openerp/modules/registry.py
@@ -226,6 +226,10 @@ class Registry(Mapping):
r, c)
return r, c
+ def in_test_mode(self):
+ """ Test whether the registry is in 'test' mode. """
+ return self.test_cr is not None
+
def enter_test_mode(self):
""" Enter the 'test' mode, where one cursor serves several requests. """
assert self.test_cr is None