From 0ceeff515c7e5cd1b744d2e144f4610437c0762f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?G=C3=A9ry=20Debongnie?= Date: Tue, 10 Mar 2015 08:42:01 +0100 Subject: [PATCH] [REF] mail: update to the new module system --- addons/mail/static/src/js/announcement.js | 120 +- addons/mail/static/src/js/mail.js | 3976 ++++++++--------- addons/mail/static/src/js/mail_followers.js | 734 +-- addons/mail/static/src/js/mail_utils.js | 120 + .../static/src/js/many2many_tags_email.js | 21 +- addons/mail/static/src/js/suggestions.js | 141 +- addons/mail/views/mail.xml | 1 + 7 files changed, 2582 insertions(+), 2531 deletions(-) create mode 100644 addons/mail/static/src/js/mail_utils.js diff --git a/addons/mail/static/src/js/announcement.js b/addons/mail/static/src/js/announcement.js index d85e8c9b5af..100bd1ea0b1 100644 --- a/addons/mail/static/src/js/announcement.js +++ b/addons/mail/static/src/js/announcement.js @@ -1,58 +1,68 @@ -openerp_announcement = function(instance) { - instance.web.WebClient.include({ - show_application: function() { - return $.when(this._super.apply(this, arguments)).then(this.proxy('show_annoucement_bar')); - }, - _ab_location: function(dbuuid) { - return _.str.sprintf('https://services.openerp.com/openerp-enterprise/ab/css/%s.css', dbuuid); - }, - show_annoucement_bar: function() { - if (this.session.get_cookie('ab') === 'c') { +odoo.define(['web.core', 'web.Model', 'web.session', 'web.WebClient'], function (require) { +"use strict"; + +var core = require('web.core'); +var Model = require('web.Model'); +var session = require('web.session'); +var WebClient = require('web.WebClient'); + +var QWeb = core.qweb; + +WebClient.include({ + show_application: function() { + return $.when(this._super.apply(this, arguments)).then(this.proxy('show_announcement_bar')); + }, + _ab_location: function(dbuuid) { + return _.str.sprintf('https://services.openerp.com/openerp-enterprise/ab/css/%s.css', dbuuid); + }, + show_announcement_bar: function() { + if (session.get_cookie('ab') === 'c') { + return; + } + var self = this; + var config_parameter = new Model('ir.config_parameter'); + $(QWeb.render('WebClient.announcement_bar')).prependTo($('body')); + var $bar = $('#announcement_bar_table'); + + return config_parameter.call('get_param', ['database.uuid', false]).then(function(dbuuid) { + if (!dbuuid) { return; } - var self = this; - var config_parameter = new instance.web.Model('ir.config_parameter'); - $(openerp.qweb.render('WebClient.announcement_bar')).prependTo($('body')); - var $bar = $('#announcement_bar_table'); - - return config_parameter.call('get_param', ['database.uuid', false]).then(function(dbuuid) { - if (!dbuuid) { - return; - } - var $link = $bar.find('.url a'); - $link.attr('href', _.str.sprintf('%s/%s', $link.attr('href'), dbuuid)); - var $css = $('').attr({ - rel : 'stylesheet', - type: 'text/css', - media: 'screen', - href: self._ab_location(dbuuid) - }); - $css.on('load', function() { - var close = function() { - var ttl = 7*24*60*60; - self.session.set_cookie('ab', 'c', ttl); - $bar.slideUp(); - setTimeout(function () { - $('.openerp_webclient_container').css('height', 'calc(100% - 34px)'); - }, 400); - }; - /* - We need a timeout of at least 350ms because the announcement bar appears - with an animation of 350ms and the computed height might be wrong if we don't wait - */ - setTimeout(function(){ - var height = $('#announcement_bar_table').outerHeight() - + $('#oe_main_menu_navbar').outerHeight(); - $('.openerp_webclient_container').css('height', 'calc(100% - ' + height + 'px)'); - $bar.find('.close').on('click', close); - self.trigger('ab_loaded', $bar); - }, 400) - }); - - $('head').append($css); - }).fail(function(result, ev){ - ev.preventDefault(); + var $link = $bar.find('.url a'); + $link.attr('href', _.str.sprintf('%s/%s', $link.attr('href'), dbuuid)); + var $css = $('').attr({ + rel : 'stylesheet', + type: 'text/css', + media: 'screen', + href: self._ab_location(dbuuid) }); - } - }); -}; + $css.on('load', function() { + var close = function() { + var ttl = 7*24*60*60; + session.set_cookie('ab', 'c', ttl); + $bar.slideUp(); + setTimeout(function () { + $('.openerp_webclient_container').css('height', 'calc(100% - 34px)'); + }, 400); + }; + /* + We need a timeout of at least 350ms because the announcement bar appears + with an animation of 350ms and the computed height might be wrong if we don't wait + */ + setTimeout(function(){ + var height = $('#announcement_bar_table').outerHeight() + + $('#oe_main_menu_navbar').outerHeight(); + $('.openerp_webclient_container').css('height', 'calc(100% - ' + height + 'px)'); + $bar.find('.close').on('click', close); + self.trigger('ab_loaded', $bar); + }, 400); + }); + + $('head').append($css); + }).fail(function(result, ev){ + ev.preventDefault(); + }); + } +}); + +}); diff --git a/addons/mail/static/src/js/mail.js b/addons/mail/static/src/js/mail.js index 86fd68b7d49..0abc9f189bf 100644 --- a/addons/mail/static/src/js/mail.js +++ b/addons/mail/static/src/js/mail.js @@ -1,133 +1,32 @@ -openerp.mail = function (session) { - var _t = session.web._t, - _lt = session.web._lt; +odoo.define('mail.mail', ['mail.utils', 'web.core', 'web.data', 'web.form_common', 'web.pyeval', 'web.SearchView', 'web.session', 'web.SystrayMenu', 'web.time', 'web.utils', 'web.web_client', 'web.Widget'], function (require) { +"use strict"; - var mail = session.mail; +var mail_utils = require('mail.utils'); +var core = require('web.core'); +var data = require('web.data'); +var form_common = require('web.form_common'); +var pyeval = require('web.pyeval'); +var SearchView = require('web.SearchView'); +var real_session = require('web.session'); +var SystrayMenu = require('web.SystrayMenu'); +var time = require('web.time'); +var utils = require('web.utils'); +var web_client = require('web.web_client'); +var Widget = require('web.Widget'); - openerp_mail_followers(session, mail); // import mail_followers.js - openerp_FieldMany2ManyTagsEmail(session); // import manyy2many_tags_email.js - openerp_announcement(session); +var _t = core._t; +var qweb = core.qweb; - /** - * ------------------------------------------------------------ - * 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 - */ - - mail.ChatterUtils = { - - /** parse text to find email: Tagada -> [Tagada, address@mail.fr] or False */ - 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]; - }, - - /* Get an image in /web/binary/image?... */ - get_image: function (session, model, field, id, resize) { - var r = resize ? encodeURIComponent(resize) : ''; - id = id || ''; - return session.url('/web/binary/image', {model: model, field: field, id: id, resize: r}); - }, - - /* Get the url of an attachment {'id': id} */ - get_attachment_url: function (session, message_id, attachment_id) { - return session.url('/mail/download_attachment', { - 'model': 'mail.message', - 'id': message_id, - 'method': 'download_attachment', - 'attachment_id': attachment_id - }); - }, - - /** - * Replaces some expressions - * - :name - shortcut to an image - */ - do_replace_expressions: function (string) { - var icon_list = ['al', 'pinky'] - /* special shortcut: :name, try to find an icon if in list */ - var regex_login = new RegExp(/(^|\s):((\w)*)/g); - var regex_res = regex_login.exec(string); - while (regex_res != null) { - var icon_name = regex_res[2]; - if (_.include(icon_list, icon_name)) - string = string.replace(regex_res[0], regex_res[1] + '' + icon_name + ''); - regex_res = regex_login.exec(string); - } - return string; - }, - - /** - * Replaces textarea text into html text (add

, ) - * TDE note : should be done server-side, in Python -> use mail.compose.message ? - */ - get_text2html: function (text) { - return text - .replace(/((?:https?|ftp):\/\/[\S]+)/g,'$1 ') - .replace(/[\n\r]/g,'
') - }, - - /* Returns the complete domain with "&" - * TDE note: please add some comments to explain how/why - */ - expand_domain: function (domain) { - var new_domain = []; - var nb_and = -1; - // TDE note: smarted code maybe ? - for ( var k = domain.length-1; k >= 0 ; k-- ) { - if ( typeof domain[k] != 'array' && typeof domain[k] != 'object' ) { - nb_and -= 2; - continue; - } - nb_and += 1; - } - - for (var k = 0; k < nb_and ; k++) { - domain.unshift('&'); - } - - return domain; - }, - - // inserts zero width space between each letter of a string so that - // the word will correctly wrap in html boxes smaller than the text - breakword: function(str){ - var out = ''; - if (!str) { - return str; - } - for(var i = 0, len = str.length; i < len; i++){ - out += _.str.escapeHTML(str[i]) + '​'; - } - return out; - }, - }; - - - /** - * ------------------------------------------------------------ - * MessageCommon - * ------------------------------------------------------------ - * - * Common base for expandables, chatter messages and composer. It manages - * the various variables common to those models. - */ - - mail.MessageCommon = session.web.Widget.extend({ +/** + * ------------------------------------------------------------ + * MessageCommon + * ------------------------------------------------------------ + * + * Common base for expandables, chatter messages and composer. It manages + * the various variables common to those models. + */ +var MessageCommon = Widget.extend({ /** * ------------------------------------------------------------ * FIXME: this comment was moved as is from the ThreadMessage Init as @@ -146,1935 +45,390 @@ openerp.mail = function (session) { * - - - - sub sub message (parent id = sub thread) * - - sub message (parent_id = root message) * - - - sub thread - */ - - init: function (parent, datasets, options) { - this._super(parent, options); - - // record options - this.options = datasets.options || options || {}; - // record domain and context - this.domain = datasets.domain || options.domain || []; - this.context = _.extend({ - default_model: false, - default_res_id: 0, - default_parent_id: false }, options.context || {}); - - // data of this message - this.id = datasets.id || false, - this.last_id = this.id, - this.model = datasets.model || this.context.default_model || false, - this.res_id = datasets.res_id || this.context.default_res_id || false, - this.parent_id = datasets.parent_id || false, - this.type = datasets.type || false, - this.subtype = datasets.subtype || false, - this.is_author = datasets.is_author || false, - this.author_avatar = datasets.author_avatar || false, - this.is_private = datasets.is_private || false, - this.subject = datasets.subject || false, - this.name = datasets.name || false, - this.record_name = datasets.record_name || false, - this.body = datasets.body || '', - this.body_short = datasets.body_short || '', - this.vote_nb = datasets.vote_nb || 0, - this.has_voted = datasets.has_voted || false, - this.is_favorite = datasets.is_favorite || false, - this.thread_level = datasets.thread_level || 0, - this.to_read = datasets.to_read || false, - this.author_id = datasets.author_id || false, - this.attachment_ids = datasets.attachment_ids || [], - this.partner_ids = datasets.partner_ids || []; - this.date = datasets.date; - this.user_pid = datasets.user_pid || false; - this.format_data(); - - // update record_name: Partner profile - if (this.model == 'res.partner') { - this.record_name = 'Partner Profile of ' + this.record_name; - } - else if (this.model == 'hr.employee') { - this.record_name = 'News from ' + this.record_name; - } - // record options and data - this.show_record_name = this.options.show_record_name && this.record_name && !this.thread_level; - this.options.show_read = false; - this.options.show_unread = false; - if (this.options.show_read_unread_button) { - if (this.options.read_action == 'read') this.options.show_read = true; - else if (this.options.read_action == 'unread') this.options.show_unread = true; - else { - this.options.show_read = this.to_read; - this.options.show_unread = !this.to_read; - } - this.options.rerender = true; - this.options.toggle_read = true; - } - this.parent_thread = typeof parent.on_message_detroy == 'function' ? parent : this.options.root_thread; - this.thread = false; - }, - - /* Convert date, timerelative and avatar in displayable data. */ - format_data: function () { - - //formating and add some fields for render - this.date = this.date ? session.web.str_to_datetime(this.date) : false; - this.display_date = moment(new Date(this.date)).format('ddd MMM DD YYYY LT'); - if (this.date && new Date().getTime()-this.date.getTime() < 7*24*60*60*1000) { - this.timerelative = $.timeago(this.date); - } - if (this.author_avatar) { - this.avatar = "data:image/png;base64," + this.author_avatar; - } else if (this.type == 'email' && (!this.author_id || !this.author_id[0])) { - this.avatar = ('/mail/static/src/img/email_icon.png'); - } else if (this.author_id && this.template != 'mail.compose_message') { - this.avatar = mail.ChatterUtils.get_image(this.session, 'res.partner', 'image_small', this.author_id[0]); - } else { - this.avatar = mail.ChatterUtils.get_image(this.session, 'res.users', 'image_small', this.session.uid); - } - if (this.author_id && this.author_id[1]) { - var parsed_email = mail.ChatterUtils.parse_email(this.author_id[1]); - this.author_id.push(parsed_email[0], parsed_email[1]); - } - if (this.partner_ids && this.partner_ids.length > 3) { - this.extra_partners_nbr = this.partner_ids.length - 3; - this.extra_partners_str = '' - var extra_partners = this.partner_ids.slice(3); - for (var key in extra_partners) { - this.extra_partners_str += extra_partners[key][1]; - } - } - }, - - - /* upload the file on the server, add in the attachments list and reload display - */ - display_attachments: function () { - for (var l in this.attachment_ids) { - var attach = this.attachment_ids[l]; - if (!attach.formating) { - attach.url = mail.ChatterUtils.get_attachment_url(this.session, this.id, attach.id); - attach.name = mail.ChatterUtils.breakword(attach.name || attach.filename); - attach.formating = true; - } - } - this.$(".oe_msg_attachment_list").html( session.web.qweb.render('mail.thread.message.attachments', {'widget': this}) ); - }, - - /* return the link to resized image - */ - attachments_resize_image: function (id, resize) { - return mail.ChatterUtils.get_image(this.session, 'ir.attachment', 'datas', id, resize); - }, - - /* get all child message id linked. - * @return array of id - */ - get_child_ids: function () { - return _.map(this.get_childs(), function (val) { return val.id; }); - }, - - /* get all child message linked. - * @return array of message object - */ - get_childs: function (nb_thread_level) { - var res=[]; - if (arguments[1] && this.id) res.push(this); - if ((isNaN(nb_thread_level) || nb_thread_level>0) && this.thread) { - _(this.thread.messages).each(function (val, key) { - res = res.concat( val.get_childs((isNaN(nb_thread_level) ? undefined : nb_thread_level-1), true) ); - }); - } - return res; - }, - - /** - * search a message in all thread and child thread. - * This method return an object message. - * @param {object}{int} option.id - * @param {object}{string} option.model - * @param {object}{boolean} option._go_thread_wall - * private for check the top thread - * @return thread object - */ - browse_message: function (options) { - // goto the wall thread for launch browse - if (!options._go_thread_wall) { - options._go_thread_wall = true; - for (var i in this.options.root_thread.messages) { - var res=this.options.root_thread.messages[i].browse_message(options); - if (res) return res; - } - } - - if (this.id==options.id) - return this; - - for (var i in this.thread.messages) { - if (this.thread.messages[i].thread) { - var res=this.thread.messages[i].browse_message(options); - if (res) return res; - } - } - - return false; - }, - - /** - * call on_message_delete on his parent thread - */ - destroy: function () { - - this._super(); - this.parent_thread.on_message_detroy(this); - - } - - }); - - /** - * ------------------------------------------------------------ - * ComposeMessage widget - * ------------------------------------------------------------ - * - * This widget handles the display of a form to compose a new message. - * This form is a mail.compose.message form_view. - * On first time : display a compact textarea that is not the compose form. - * When the user focuses the textarea, the compose message is instantiated. */ - mail.ThreadComposeMessage = mail.MessageCommon.extend({ - template: 'mail.compose_message', + init: function (parent, datasets, options) { + this._super(parent, options); - /** - * @param {Object} parent parent - * @param {Object} [options] - * @param {Object} [context] context passed to the - * mail.compose.message DataSetSearch. Please refer to this model - * for more details about fields and default values. - * @param {Object} recipients = [ - { - 'email_address': [str], - 'partner_id': False/[int], - 'name': [str], - 'full_name': name, - }, - { ... }, - ] - */ + // record options + this.options = datasets.options || options || {}; + // record domain and context + this.domain = datasets.domain || options.domain || []; + this.context = _.extend({ + default_model: false, + default_res_id: 0, + default_parent_id: false }, options.context || {}); - init: function (parent, datasets, options) { - this._super(parent, datasets, options); - this.show_compact_message = false; - this.show_delete_attachment = true; - this.is_log = false; - this.recipients = []; - this.recipient_ids = []; - session.web.bus.on('clear_uncommitted_changes', this, function(e) { - if (this.show_composer && !e.isDefaultPrevented()){ - if (!confirm(_t("You are currently composing a message, your message will be discarded.\n\nAre you sure you want to leave this page ?"))) { - e.preventDefault(); - } - else{ - this.on_cancel(); - } - } - }); - }, - - start: function () { - this._super.apply(this, arguments); - - this.ds_attachment = new session.web.DataSetSearch(this, 'ir.attachment'); - this.fileupload_id = _.uniqueId('oe_fileupload_temp'); - $(window).on(this.fileupload_id, this.on_attachment_loaded); - - this.display_attachments(); - this.bind_events(); - }, - - /* when a user click on the upload button, send file read on_attachment_loaded - */ - on_attachment_change: function (event) { - event.stopPropagation(); - var self = this; - 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.attachment_ids) { - if ((this.attachment_ids[i].filename || this.attachment_ids[i].name) == filename) { - if (this.attachment_ids[i].upload) { - return false; - } - this.ds_attachment.unlink([this.attachment_ids[i].id]); - } else { - attachments.push(this.attachment_ids[i]); - } - } - this.attachment_ids = attachments; - - // submit file - this.$('form.oe_form_binary_form').submit(); - - this.$(".oe_attachment_file").hide(); - - this.attachment_ids.push({ - 'id': 0, - 'name': filename, - 'filename': filename, - 'url': '', - 'upload': true - }); - this.display_attachments(); - } - }, - - /* when the file is uploaded - */ - on_attachment_loaded: function (event, result) { - - if (result.erorr || !result.id ) { - this.do_warn( session.web.qweb.render('mail.error_upload'), result.error); - this.attachment_ids = _.filter(this.attachment_ids, function (val) { return !val.upload; }); - } else { - for (var i in this.attachment_ids) { - if (this.attachment_ids[i].filename == result.filename && this.attachment_ids[i].upload) { - this.attachment_ids[i]={ - 'id': result.id, - 'name': result.name, - 'filename': result.filename, - 'url': mail.ChatterUtils.get_attachment_url(this.session, this.id, result.id) - }; - } - } - } - this.display_attachments(); - var $input = this.$('input.oe_form_binary_file'); - $input.after($input.clone(true)).remove(); - this.$(".oe_attachment_file").show(); - }, - - /* unlink the file on the server and reload display - */ - on_attachment_delete: function (event) { - event.stopPropagation(); - var attachment_id=$(event.target).data("id"); - if (attachment_id) { - var attachments=[]; - for (var i in this.attachment_ids) { - if (attachment_id!=this.attachment_ids[i].id) { - attachments.push(this.attachment_ids[i]); - } - else { - this.ds_attachment.unlink([attachment_id]); - } - } - this.attachment_ids = attachments; - this.display_attachments(); - } - }, - - bind_events: function () { - var self = this; - this.$('.oe_compact_inbox').on('click', self.on_toggle_quick_composer); - this.$('.oe_compose_post').on('click', self.on_toggle_quick_composer); - this.$('.oe_compose_log').on('click', self.on_toggle_quick_composer); - this.$('input.oe_form_binary_file').on('change', _.bind( this.on_attachment_change, this)); - this.$('.oe_cancel').on('click', _.bind( this.on_cancel, this)); - this.$('.oe_post').on('click', self.on_message_post); - this.$('.oe_full').on('click', _.bind( this.on_compose_fullmail, this, this.id ? 'reply' : 'comment')); - /* stack for don't close the compose form if the user click on a button */ - this.$('.oe_msg_left, .oe_msg_center').on('mousedown', _.bind( function () { this.stay_open = true; }, this)); - this.$('.oe_msg_left, .oe_msg_content').on('mouseup', _.bind( function () { this.$('textarea').focus(); }, this)); - var ev_stay = {}; - ev_stay.mouseup = ev_stay.keydown = ev_stay.focus = function () { self.stay_open = false; }; - this.$('textarea').on(ev_stay); - this.$('textarea').autosize(); - - // auto close - this.$('textarea').on('blur', self.on_toggle_quick_composer); - - // event: delete child attachments off the oe_msg_attachment_list box - this.$(".oe_msg_attachment_list").on('click', '.oe_delete', this.on_attachment_delete); - - this.$(".oe_recipients").on('change', 'input', this.on_checked_recipient); - }, - - on_compose_fullmail: function (default_composition_mode) { - var self = this; - if(!this.do_check_attachment_upload()) { - return false; - } - var recipient_done = $.Deferred(); - if (this.is_log) { - recipient_done.resolve([]); - } + // data of this message + this.id = datasets.id || false; + this.last_id = this.id; + this.model = datasets.model || this.context.default_model || false; + this.res_id = datasets.res_id || this.context.default_res_id || false; + this.parent_id = datasets.parent_id || false; + this.type = datasets.type || false; + this.subtype = datasets.subtype || false; + this.is_author = datasets.is_author || false; + this.author_avatar = datasets.author_avatar || false; + this.is_private = datasets.is_private || false; + this.subject = datasets.subject || false; + this.name = datasets.name || false; + this.record_name = datasets.record_name || false; + this.body = datasets.body || ''; + this.body_short = datasets.body_short || ''; + this.vote_nb = datasets.vote_nb || 0; + this.has_voted = datasets.has_voted || false; + this.is_favorite = datasets.is_favorite || false; + this.thread_level = datasets.thread_level || 0; + this.to_read = datasets.to_read || false; + this.author_id = datasets.author_id || false; + this.attachment_ids = datasets.attachment_ids || []; + this.partner_ids = datasets.partner_ids || []; + this.date = datasets.date; + this.user_pid = datasets.user_pid || false; + this.format_data(); + // update record_name: Partner profile + if (this.model == 'res.partner') { + this.record_name = 'Partner Profile of ' + this.record_name; + } + else if (this.model == 'hr.employee') { + this.record_name = 'News from ' + this.record_name; + } + // record options and data + this.show_record_name = this.options.show_record_name && this.record_name && !this.thread_level; + this.options.show_read = false; + this.options.show_unread = false; + if (this.options.show_read_unread_button) { + if (this.options.read_action == 'read') this.options.show_read = true; + else if (this.options.read_action == 'unread') this.options.show_unread = true; else { - recipient_done = this.check_recipient_partners(); + this.options.show_read = this.to_read; + this.options.show_unread = !this.to_read; } - $.when(recipient_done).done(function (partner_ids) { - var context = { - 'default_parent_id': self.id, - 'default_body': mail.ChatterUtils.get_text2html(self.$el ? (self.$el.find('textarea:not(.oe_compact)').val() || '') : ''), - 'default_attachment_ids': _.map(self.attachment_ids, function (file) {return file.id;}), - 'default_partner_ids': partner_ids, - 'default_is_log': self.is_log, - 'mail_post_autofollow': true, - 'mail_post_autofollow_partner_ids': partner_ids, - 'is_private': self.is_private - }; - if (default_composition_mode != 'reply' && 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; + this.options.rerender = true; + this.options.toggle_read = true; + } + this.parent_thread = typeof parent.on_message_detroy == 'function' ? parent : this.options.root_thread; + this.thread = false; + }, + + /* Convert date, timerelative and avatar in displayable data. */ + format_data: function () { + + //formating and add some fields for render + this.date = this.date ? time.str_to_datetime(this.date) : false; + this.display_date = moment(new Date(this.date)).format('ddd MMM DD YYYY LT'); + if (this.date && new Date().getTime()-this.date.getTime() < 7*24*60*60*1000) { + this.timerelative = $.timeago(this.date); + } + if (this.author_avatar) { + this.avatar = "data:image/png;base64," + this.author_avatar; + } else if (this.type == 'email' && (!this.author_id || !this.author_id[0])) { + this.avatar = ('/mail/static/src/img/email_icon.png'); + } else if (this.author_id && this.template != 'mail.compose_message') { + this.avatar = mail_utils.get_image(this.session, 'res.partner', 'image_small', this.author_id[0]); + } else { + this.avatar = mail_utils.get_image(real_session, 'res.users', 'image_small', real_session.uid); + } + if (this.author_id && this.author_id[1]) { + var parsed_email = mail_utils.parse_email(this.author_id[1]); + this.author_id.push(parsed_email[0], parsed_email[1]); + } + if (this.partner_ids && this.partner_ids.length > 3) { + this.extra_partners_nbr = this.partner_ids.length - 3; + this.extra_partners_str = ''; + var extra_partners = this.partner_ids.slice(3); + for (var key in extra_partners) { + this.extra_partners_str += extra_partners[key][1]; + } + } + }, + + + /* upload the file on the server, add in the attachments list and reload display + */ + display_attachments: function () { + for (var l in this.attachment_ids) { + var attach = this.attachment_ids[l]; + if (!attach.formating) { + attach.url = mail_utils.get_attachment_url(real_session, this.id, attach.id); + attach.name = mail_utils.breakword(attach.name || attach.filename); + attach.formating = true; + } + } + this.$(".oe_msg_attachment_list").html( qweb.render('mail.thread.message.attachments', {'widget': this}) ); + }, + + /* return the link to resized image + */ + attachments_resize_image: function (id, resize) { + return mail_utils.get_image(real_session, 'ir.attachment', 'datas', id, resize); + }, + + /* get all child message id linked. + * @return array of id + */ + get_child_ids: function () { + return _.map(this.get_childs(), function (val) { return val.id; }); + }, + + /* get all child message linked. + * @return array of message object + */ + get_childs: function (nb_thread_level) { + var res=[]; + if (arguments[1] && this.id) res.push(this); + if ((isNaN(nb_thread_level) || nb_thread_level>0) && this.thread) { + _(this.thread.messages).each(function (val, key) { + res = res.concat( val.get_childs((isNaN(nb_thread_level) ? undefined : nb_thread_level-1), true) ); + }); + } + return res; + }, + + /** + * search a message in all thread and child thread. + * This method return an object message. + * @param {object}{int} option.id + * @param {object}{string} option.model + * @param {object}{boolean} option._go_thread_wall + * private for check the top thread + * @return thread object + */ + browse_message: function (options) { + var i, res; + // goto the wall thread for launch browse + if (!options._go_thread_wall) { + options._go_thread_wall = true; + for (i in this.options.root_thread.messages) { + res=this.options.root_thread.messages[i].browse_message(options); + if (res) return res; + } + } + + if (this.id === options.id) + return this; + + for (i in this.thread.messages) { + if (this.thread.messages[i].thread) { + res=this.thread.messages[i].browse_message(options); + if (res) return res; + } + } + + return false; + }, + + /** + * call on_message_delete on his parent thread + */ + destroy: function () { + + this._super(); + this.parent_thread.on_message_detroy(this); + + } + +}); + +/** + * ------------------------------------------------------------ + * ComposeMessage widget + * ------------------------------------------------------------ + * + * This widget handles the display of a form to compose a new message. + * This form is a mail.compose.message form_view. + * On first time : display a compact textarea that is not the compose form. + * When the user focuses the textarea, the compose message is instantiated. + */ + +var ThreadComposeMessage = MessageCommon.extend({ + template: 'mail.compose_message', + + /** + * @param {Object} parent parent + * @param {Object} [options] + * @param {Object} [context] context passed to the + * mail.compose.message DataSetSearch. Please refer to this model + * for more details about fields and default values. + * @param {Object} recipients = [ + { + 'email_address': [str], + 'partner_id': False/[int], + 'name': [str], + 'full_name': name, + }, + { ... }, + ] + */ + + init: function (parent, datasets, options) { + this._super(parent, datasets, options); + this.show_compact_message = false; + this.show_delete_attachment = true; + this.is_log = false; + this.recipients = []; + this.recipient_ids = []; + core.bus.on('clear_uncommitted_changes', this, function(e) { + if (this.show_composer && !e.isDefaultPrevented()){ + if (!confirm(_t("You are currently composing a message, your message will be discarded.\n\nAre you sure you want to leave this page ?"))) { + e.preventDefault(); } + else{ + this.on_cancel(); + } + } + }); + }, - var action = { - type: 'ir.actions.act_window', - res_model: 'mail.compose.message', - view_mode: 'form', - view_type: 'form', - views: [[false, 'form']], - target: 'new', - context: context, - }; + start: function () { + this._super.apply(this, arguments); - self.do_action(action); - self.on_cancel(); + this.ds_attachment = new data.DataSetSearch(this, 'ir.attachment'); + this.fileupload_id = _.uniqueId('oe_fileupload_temp'); + $(window).on(this.fileupload_id, this.on_attachment_loaded); + + this.display_attachments(); + this.bind_events(); + }, + + /* when a user click on the upload button, send file read on_attachment_loaded + */ + on_attachment_change: function (event) { + event.stopPropagation(); + 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.attachment_ids) { + if ((this.attachment_ids[i].filename || this.attachment_ids[i].name) == filename) { + if (this.attachment_ids[i].upload) { + return false; + } + this.ds_attachment.unlink([this.attachment_ids[i].id]); + } else { + attachments.push(this.attachment_ids[i]); + } + } + this.attachment_ids = attachments; + + // submit file + this.$('form.oe_form_binary_form').submit(); + + this.$(".oe_attachment_file").hide(); + + this.attachment_ids.push({ + 'id': 0, + 'name': filename, + 'filename': filename, + 'url': '', + 'upload': true }); - - }, - - reinit: function() { - var $render = $( session.web.qweb.render('mail.compose_message', {'widget': this}) ); - - $render.insertAfter(this.$el.last()); - this.$el.remove(); - this.$el = $render; - this.display_attachments(); - this.bind_events(); - }, + } + }, + + /* when the file is uploaded + */ + on_attachment_loaded: function (event, result) { - on_cancel: function (event) { - if (event) event.stopPropagation(); - this.attachment_ids=[]; - this.stay_open = false; - this.show_composer = false; - this.reinit(); - }, - - /* return true if all file are complete else return false and make an alert */ - do_check_attachment_upload: function () { - if (_.find(this.attachment_ids, function (file) {return file.upload;})) { - this.do_warn(session.web.qweb.render('mail.error_upload'), session.web.qweb.render('mail.error_upload_please_wait')); - return false; - } else { - return true; + if (result.erorr || !result.id ) { + this.do_warn( qweb.render('mail.error_upload'), result.error); + this.attachment_ids = _.filter(this.attachment_ids, function (val) { return !val.upload; }); + } else { + for (var i in this.attachment_ids) { + if (this.attachment_ids[i].filename == result.filename && this.attachment_ids[i].upload) { + this.attachment_ids[i]={ + 'id': result.id, + 'name': result.name, + 'filename': result.filename, + 'url': mail_utils.get_attachment_url(this.session, this.id, result.id) + }; + } } - }, + } + this.display_attachments(); + var $input = this.$('input.oe_form_binary_file'); + $input.after($input.clone(true)).remove(); + this.$(".oe_attachment_file").show(); + }, - check_recipient_partners: function () { - var self = this; - var check_done = $.Deferred(); - var recipients = _.filter(this.recipients, 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.parent_thread.ds_thread._model.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.ChatterUtils.parse_email(partner_name); - - var pop = new session.web.form.FormOpenPopup(this); - pop.show_element( - 'res.partner', - partner_id, - { 'force_email': true, - 'ref': "compound_context", - 'default_name': parsed_email[0], - 'default_email': parsed_email[1], - }, { - title: _t("Please complete partner's informations"), - } - ); - 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.parent_thread.ds_thread._model.call('message_partner_info_from_emails', [self.context.default_res_id, new_names_to_find, true]); - } - else { - find_done.resolve([]); - } - $.when(find_done).pipe(function (result) { - var recipient_popups = result.concat(recipients_to_check); - _.each(recipient_popups, function (partner_info) { - if (partner_info.partner_id && _.indexOf(partner_info.partner_id, recipient_ids_to_remove) == -1) { - recipient_ids.push(partner_info.partner_id); - } - }); - }).pipe(function () { - check_done.resolve(recipient_ids); - }); - }); - }); - - return check_done; - }, - - on_message_post: function (event) { - var self = this; - if (self.flag_post) { - return; - } - if (this.do_check_attachment_upload() && (this.attachment_ids.length || this.$('textarea').val().match(/\S+/))) { - self.flag_post = true; - if (this.is_log) { - this.do_send_message_post([], this.is_log); + /* unlink the file on the server and reload display + */ + on_attachment_delete: function (event) { + event.stopPropagation(); + var attachment_id=$(event.target).data("id"); + if (attachment_id) { + var attachments=[]; + for (var i in this.attachment_ids) { + if (attachment_id!=this.attachment_ids[i].id) { + attachments.push(this.attachment_ids[i]); } else { - this.check_recipient_partners().done(function (partner_ids) { - self.do_send_message_post(partner_ids, self.is_log); - }); + this.ds_attachment.unlink([attachment_id]); } } - }, - - /* do post a message and fetch the message */ - do_send_message_post: function (partner_ids, log) { - var self = this; - var values = { - 'body': this.$('textarea').val(), - 'subject': false, - 'parent_id': this.context.default_parent_id, - 'attachment_ids': _.map(this.attachment_ids, function (file) {return file.id;}), - 'partner_ids': partner_ids, - 'context': _.extend(this.parent_thread.context, { - 'mail_post_autofollow': true, - 'mail_post_autofollow_partner_ids': partner_ids, - }), - 'type': 'comment', - 'content_subtype': 'plaintext', - }; - if (log) { - values['subtype'] = false; - } - else { - values['subtype'] = 'mail.mt_comment'; - } - this.parent_thread.ds_thread._model.call('message_post', [this.context.default_res_id], values).done(function (message_id) { - var thread = self.parent_thread; - var root = thread == self.options.root_thread; - if (self.options.display_indented_thread < self.thread_level && thread.parent_message) { - var thread = thread.parent_message.parent_thread; - } - // create object and attach to the thread object - thread.message_fetch([["id", "=", message_id]], false, [message_id], function (arg, data) { - var message = thread.create_message_object( data.slice(-1)[0] ); - // insert the message on dom - thread.insert_message( message, root ? undefined : self.$el, root ); - }); - self.on_cancel(); - self.flag_post = false; - }); - }, - - /* Quick composer: toggle minimal / expanded mode - * - toggle minimal (one-liner) / expanded (textarea, buttons) mode - * - when going into expanded mode: - * - call `message_get_suggested_recipients` to have a list of partners to add - * - compute email_from list (list of unknown email_from to propose to create partners) - */ - on_toggle_quick_composer: function (event) { - var self = this; - var $input = $(event.target); - this.compute_emails_from(); - var email_addresses = _.pluck(this.recipients, 'email_address'); - var suggested_partners = $.Deferred(); - - // if clicked: call for suggested recipients - if (event.type == 'click') { - this.is_log = $input.hasClass('oe_compose_log'); - suggested_partners = this.parent_thread.ds_thread.call('message_get_suggested_recipients', [[this.context.default_res_id], this.context]).done(function (additional_recipients) { - var thread_recipients = additional_recipients[self.context.default_res_id]; - _.each(thread_recipients, function (recipient) { - var parsed_email = mail.ChatterUtils.parse_email(recipient[1]); - if (_.indexOf(email_addresses, parsed_email[1]) == -1) { - self.recipients.push({ - 'checked': true, - 'partner_id': recipient[0], - 'full_name': recipient[1], - 'name': parsed_email[0], - 'email_address': parsed_email[1], - 'reason': recipient[2], - }) - } - }); - }); - } - else { - suggested_partners.resolve({}); - } - - // when call for suggested partners finished: re-render the widget - $.when(suggested_partners).pipe(function (additional_recipients) { - if ((!self.stay_open || (event && event.type == 'click')) && (!self.show_composer || !self.$('textarea:not(.oe_compact)').val().match(/\S+/) && !self.attachment_ids.length)) { - self.show_composer = !self.show_composer || self.stay_open; - self.reinit(); - } - if (!self.stay_open && self.show_composer && (!event || event.type != 'blur')) { - self.$('textarea:not(.oe_compact):first').focus(); - } - }); - - return suggested_partners; - }, - - do_hide_compact: function () { - this.show_compact_message = false; - if (!this.show_composer) { - this.reinit(); - } - }, - - do_show_compact: function () { - this.show_compact_message = true; - if (!this.show_composer) { - this.reinit(); - } - }, - - /** Compute the list of unknown email_from the the given thread - * TDE FIXME: seems odd to delegate to the composer - * TDE TODO: please de-obfuscate and comment your code */ - compute_emails_from: function () { - var self = this; - var messages = []; - - if (this.parent_thread.parent_message) { - // go to the parented message - var message = this.parent_thread.parent_message; - var parent_message = message.parent_id ? message.parent_thread.parent_message : message; - if(parent_message){ - var messages = [parent_message].concat(parent_message.get_childs()); - } - } else if (this.options.emails_from_on_composer) { - // get all wall messages if is not a mail.Wall - _.each(this.options.root_thread.messages, function (msg) {messages.push(msg); messages.concat(msg.get_childs());}); - } - - _.each(messages, function (thread) { - if (thread.author_id && !thread.author_id[0] && - !_.find(self.recipients, function (recipient) {return recipient.email_address == thread.author_id[3];}) && - _.some([thread.author_id[1], thread.author_id[2], thread.author_id[3]])) { - self.recipients.push({ 'full_name': thread.author_id[1], - 'name': thread.author_id[2], - 'email_address': thread.author_id[3], - 'partner_id': false, - 'checked': true, - 'reason': 'Incoming email author' - }); - } - }); - return self.recipients; - }, - - on_checked_recipient: function (event) { - var $input = $(event.target); - var full_name = $input.attr("data"); - _.each(this.recipients, function (recipient) { - if (recipient.full_name == full_name) { - recipient.checked = $input.is(":checked"); - } - }); - }, - }); - - /** - * ------------------------------------------------------------ - * Thread Message Expandable Widget - * ------------------------------------------------------------ - * - * This widget handles the display the expandable message in a thread. - * - thread - * - - visible message - * - - expandable - * - - visible message - * - - visible message - * - - expandable - */ - mail.ThreadExpandable = mail.MessageCommon.extend({ - template: 'mail.thread.expandable', - - init: function (parent, datasets, options) { - this._super(parent, datasets, options); - this.type = 'expandable'; - this.max_limit = datasets.max_limit; - this.nb_messages = datasets.nb_messages; - this.flag_used = false; - }, - - start: function () { - this._super.apply(this, arguments); - this.bind_events(); - }, - - reinit: function () { - var $render = $(session.web.qweb.render('mail.thread.expandable', {'widget': this})); - this.$el.replaceWith( $render ); - this.$el = $render; - this.bind_events(); - }, - - /** - * Bind events in the widget. Each event is slightly described - * in the function. */ - bind_events: function () { - this.$('.oe_msg_more_message').on('click', this.on_expandable); - }, - - animated_destroy: function (fadeTime) { - var self=this; - this.$el.fadeOut(fadeTime, function () { - self.destroy(); - }); - }, - - /*The selected thread and all childs (messages/thread) became read - * @param {object} mouse envent - */ - on_expandable: function (event) { - if (event)event.stopPropagation(); - if (this.flag_used) { - return false - } - this.flag_used = true; - - var self = this; - - // read messages - self.parent_thread.message_fetch(this.domain, this.context, false, function (arg, data) { - self.id = false; - // insert the message on dom after this message - var element = self.$el; - if (self.thread_level === 0){ - element = element.parent(); - } - self.parent_thread.switch_new_message(data, element); - self.animated_destroy(200); - }); - - return false; - }, - - }); - - mail.ThreadMessage = mail.MessageCommon.extend({ - template: 'mail.thread.message', - - start: function () { - this._super.apply(this, arguments); - this.bind_events(); - if(this.thread_level < this.options.display_indented_thread) { - this.create_thread(); - } + this.attachment_ids = attachments; this.display_attachments(); - - this.ds_notification = new session.web.DataSetSearch(this, 'mail.notification'); - this.ds_message = new session.web.DataSetSearch(this, 'mail.message'); - }, - - /** - * Bind events in the widget. Each event is slightly described - * in the function. */ - bind_events: function () { - var self = this; - // header icons bindings - this.$('.oe_read').on('click', this.on_message_read); - this.$('.oe_unread').on('click', this.on_message_unread); - this.$('.oe_msg_delete').on('click', this.on_message_delete); - this.$('.oe_reply').on('click', this.on_message_reply); - this.$('.oe_star').on('click', this.on_star); - this.$('.oe_msg_vote').on('click', this.on_vote); - this.$('.oe_mail_vote_count').on('mouseenter', this.on_hover); - this.$('.oe_mail_expand').on('click', this.on_expand); - this.$('.oe_mail_reduce').on('click', this.on_expand); - this.$('.oe_mail_action_model').on('click', this.on_record_clicked); - this.$('.oe_mail_action_author').on('click', this.on_record_author_clicked); - }, - on_hover : function(event){ - var self = this; - var voter = ""; - var limit = 10; - event.stopPropagation(); - var $target = $(event.target).hasClass("fa-thumbs-o-up") ? $(event.target).parent() : $(event.target); - //Note: We can set data-content attr on target element once we fetch data so that next time when one moves mouse on element it saves call - //But if there is new like comes then we'll not have new likes in popover in that case - if ($target.data('liker-list')) - { - voter = $target.data('liker-list'); - self.bindTooltipTo($target, voter); - $target.tooltip('hide').tooltip('show'); - $(".tooltip").on("mouseleave", function () { - $(this).remove(); - }); - }else{ - this.ds_message.call('get_likers_list', [this.id, limit]) - .done(function (data) { - _.each(data, function(people, index) { - voter = voter + people.substring(0,1).toUpperCase() + people.substring(1); - if(index != data.length-1) { - voter = voter + "
"; - } - }); - $target.data('liker-list', voter); - self.bindTooltipTo($target, voter); - $target.tooltip('hide').tooltip('show'); - $(".tooltip").on("mouseleave", function () { - $(this).remove(); - }); - }); - } - return true; - }, - bindTooltipTo: function($el, value) { - $el.tooltip({ - 'title': value, - 'placement': 'top', - 'container': this.el, - 'html': true, - 'trigger': 'manual', - 'animation': false - }).on("mouseleave", function () { - setTimeout(function () { - if (!$(".tooltip:hover").length) { - $el.tooltip("hide"); - } - },100); - }); - }, - on_record_clicked: function (event) { - event.preventDefault(); - var self = this; - var state = { - 'model': this.model, - 'id': this.res_id, - 'title': this.record_name - }; - session.webclient.action_manager.do_push_state(state); - this.context.params = { - model: this.model, - res_id: this.res_id, - }; - this.thread.ds_thread.call("message_redirect_action", {context: this.context}).then(function(action){ - self.do_action(action); - }); - }, - - on_record_author_clicked: function (event) { - event.preventDefault(); - var partner_id = $(event.target).data('partner'); - var state = { - 'model': 'res.partner', - 'id': partner_id, - 'title': this.record_name - }; - session.webclient.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); - }, - - /* Call the on_compose_message on the thread of this message. */ - on_message_reply:function (event) { - event.stopPropagation(); - this.create_thread(); - this.thread.on_compose_message(event); - return false; - }, - - on_expand: function (event) { - event.stopPropagation(); - this.$('.oe_msg_body:first > .oe_msg_body_short:first').toggle(); - this.$('.oe_msg_body:first > .oe_msg_body_long:first').toggle(); - return false; - }, - - /** - * Instantiate the thread object of this message. - * Each message have only one thread. - */ - create_thread: function () { - if (this.thread) { - return false; - } - /*create thread*/ - this.thread = new mail.Thread(this, this, { - 'domain': this.domain, - 'context':{ - 'default_model': this.model, - 'default_res_id': this.res_id, - 'default_parent_id': this.id - }, - 'options': this.options - } - ); - /*insert thread in parent message*/ - this.thread.insertAfter(this.$el); - }, - - /** - * Fade out the message and his child thread. - * Then this object is destroyed. - */ - animated_destroy: function (fadeTime) { - var self=this; - this.$el.fadeOut(fadeTime, function () { - self.parent_thread.message_to_expandable(self); - }); - if (this.thread) { - this.thread.$el.fadeOut(fadeTime); - } - }, - - /** - * Wait a confirmation for delete the message on the DB. - * Make an animate destroy - */ - on_message_delete: function (event) { - event.stopPropagation(); - if (! confirm(_t("Do you really want to delete this message?"))) { return false; } - - this.animated_destroy(150); - // delete this message and his childs - var ids = [this.id].concat( this.get_child_ids() ); - this.ds_message.unlink(ids); - return false; - }, - - /* Check if the message must be destroy and detroy it or check for re render widget - * @param {callback} apply function - */ - check_for_rerender: function () { - var self = this; - - var messages = [this].concat(this.get_childs()); - var message_ids = _.map(messages, function (msg) { return msg.id;}); - var domain = mail.ChatterUtils.expand_domain( this.options.root_thread.domain ) - .concat([["id", "in", message_ids ]]); - - return this.parent_thread.ds_message.call('message_read', [undefined, domain, [], !!this.parent_thread.options.display_indented_thread, this.context, this.parent_thread.id]) - .then( function (records) { - // remove message not loaded - _.map(messages, function (msg) { - if(!_.find(records, function (record) { return record.id == msg.id; })) { - msg.animated_destroy(150); - } else { - msg.renderElement(); - msg.start(); - } - self.options.root_thread.MailWidget.do_reload_menu_emails(); - }); - - }); - }, - - on_message_read: function (event) { - event.stopPropagation(); - this.on_message_read_unread(true); - return false; - }, - - on_message_unread: function (event) { - event.stopPropagation(); - this.on_message_read_unread(false); - return false; - }, - - /* Set the selected thread and all childs as read or unread, based on - * read parameter. - * @param {boolean} read_value - */ - on_message_read_unread: function (read_value) { - var self = this; - var messages = [this].concat(this.get_childs()); - - // inside the inbox, when the user mark a message as read/done, don't apply this value - // for the stared/favorite message - if (this.options.view_inbox && read_value) { - var messages = _.filter(messages, function (val) { return !val.is_favorite && val.id; }); - if (!messages.length) { - this.check_for_rerender(); - return false; - } - } - var message_ids = _.map(messages, function (val) { return val.id; }); - - this.ds_message.call('set_message_read', [message_ids, read_value, true, this.context]) - .then(function () { - // apply modification - _.each(messages, function (msg) { - msg.to_read = !read_value; - if (msg.options.toggle_read) { - msg.options.show_read = msg.to_read; - msg.options.show_unread = !msg.to_read; - } - }); - // check if the message must be display, destroy or rerender - self.check_for_rerender(); - }); - return false; - }, - - /** - * add or remove a vote for a message and display the result - */ - on_vote: function (event) { - event.stopPropagation(); - this.ds_message.call('vote_toggle', [[this.id]]) - .then( - _.bind(function (vote) { - this.has_voted = vote; - this.vote_nb += this.has_voted ? 1 : -1; - this.display_vote(); - }, this)); - return false; - }, - - /** - * Display the render of this message's vote - */ - display_vote: function () { - var vote_element = session.web.qweb.render('mail.thread.message.vote', {'widget': this}); - this.$(".oe_msg_footer:first .oe_mail_vote_count").remove(); - this.$(".oe_msg_footer:first .oe_msg_vote").replaceWith(vote_element); - this.$('.oe_msg_vote').on('click', this.on_vote); - this.$('.oe_mail_vote_count').on('mouseenter', this.on_hover); - }, - - /** - * add or remove a favorite (or starred) for a message and change class on the DOM - */ - on_star: function (event) { - event.stopPropagation(); - var self=this; - var button = self.$('.oe_star:first'); - - this.ds_message.call('set_message_starred', [[self.id], !self.is_favorite, true]) - .then(function (star) { - self.is_favorite=star; - if (self.is_favorite) { - button.addClass('oe_starred'); - } else { - button.removeClass('oe_starred'); - } - - if (self.options.view_inbox && self.is_favorite) { - self.on_message_read_unread(true); - } else { - self.check_for_rerender(); - } - }); - return false; - }, - - }); - - /** - * ------------------------------------------------------------ - * Thread Widget - * ------------------------------------------------------------ - * - * This widget handles the display of a thread of messages. The - * thread view: - * - root thread - * - - sub message (parent_id = root message) - * - - - sub thread - * - - - - sub sub message (parent id = sub thread) - * - - sub message (parent_id = root message) - * - - - sub thread - */ - mail.Thread = session.web.Widget.extend({ - template: 'mail.thread', - - /** - * @param {Object} parent parent - * @param {Array} [domain] - * @param {Object} [context] context of the thread. It should - contain at least default_model, default_res_id. Please refer to - the ComposeMessage widget for more information about it. - * @param {Object} [options] - * @param {Object} [message] read about mail.ThreadMessage object - * @param {Object} [thread] - * @param {int} [display_indented_thread] number thread level to indented threads. - * other are on flat mode - * @param {Array} [parents] liked with the parents thread - * use with browse, fetch... [O]= top parent - */ - init: function (parent, datasets, options) { - var self = this; - this._super(parent, options); - this.MailWidget = parent instanceof mail.Widget ? parent : false; - this.domain = options.domain || []; - this.context = _.extend(options.context || {}); - - this.options = options.options; - this.options.root_thread = (options.options.root_thread != undefined ? options.options.root_thread : this); - this.options.show_compose_message = this.options.show_compose_message && !this.thread_level; - - // record options and data - this.parent_message= parent.thread!= undefined ? parent : false ; - - // data of this thread - this.id = datasets.id || false; - this.last_id = datasets.last_id || false; - this.parent_id = datasets.parent_id || false; - this.is_private = datasets.is_private || false; - this.author_id = datasets.author_id || false; - this.thread_level = (datasets.thread_level+1) || 0; - datasets.partner_ids = datasets.partner_ids || []; - if (datasets.author_id && !_.contains(_.flatten(datasets.partner_ids),datasets.author_id[0]) && datasets.author_id[0]) { - datasets.partner_ids.push(datasets.author_id); - } - this.user_pid = datasets.user_pid || false; - this.partner_ids = datasets.partner_ids; - this.messages = []; - this.options.flat_mode = (this.options.display_indented_thread - this.thread_level > 0); - - // object compose message - this.compose_message = false; - - this.ds_thread = new session.web.DataSetSearch(this, this.context.default_model || 'mail.thread'); - this.ds_message = new session.web.DataSetSearch(this, 'mail.message'); - this.render_mutex = new $.Mutex(); - }, - - start: function () { - this._super.apply(this, arguments); - this.bind_events(); - return $.when(); - }, - - /* instantiate the compose message object and insert this on the DOM. - * The compose message is display in compact form. - */ - instantiate_compose_message: function () { - // add message composition form view - if (!this.compose_message) { - this.compose_message = new mail.ThreadComposeMessage(this, this, { - 'context': this.options.compose_as_todo && !this.thread_level ? _.extend(this.context, { 'default_starred': true }) : this.context, - 'options': this.options, - }); - if (!this.thread_level || this.thread_level > this.options.display_indented_thread) { - this.compose_message.insertBefore(this.$el); - } else { - this.compose_message.prependTo(this.$el); - } - } - }, - - /* When the expandable object is visible on screen (with scrolling) - * then the on_expandable function is launch - */ - on_scroll: function () { - var expandables = - _.each( _.filter(this.messages, function (val) {return val.max_limit && !val.parent_id;}), function (val) { - var pos = val.$el.position(); - if (pos.top) { - /* bottom of the screen */ - var bottom = $(window).scrollTop()+$(window).height()+200; - if (bottom > pos.top) { - val.on_expandable(); - } - } - }); - }, - - /** - * Bind events in the widget. Each event is slightly described - * in the function. */ - bind_events: function () { - var self = this; - self.$('.oe_mail_list_recipients .oe_more').on('click', self.on_show_recipients); - self.$('.oe_mail_compose_textarea .oe_more_hidden').on('click', self.on_hide_recipients); - }, - - /** - *show all the partner list of this parent message - */ - on_show_recipients: function () { - var p=$(this).parent(); - p.find('.oe_more_hidden, .oe_hidden').show(); - p.find('.oe_more').hide(); - return false; - }, - - /** - *hide a part of the partner list of this parent message - */ - on_hide_recipients: function () { - var p=$(this).parent(); - p.find('.oe_more_hidden, .oe_hidden').hide(); - p.find('.oe_more').show(); - return false; - }, - - /* get all child message/thread id linked. - * @return array of id - */ - get_child_ids: function () { - return _.map(this.get_childs(), function (val) { return val.id; }); - }, - - /* get all child message/thread linked. - * @param {int} nb_thread_level, number of traversed thread level for this search - * @return array of thread object - */ - get_childs: function (nb_thread_level) { - var res=[]; - if (arguments[1]) res.push(this); - if (isNaN(nb_thread_level) || nb_thread_level>0) { - _(this.messages).each(function (val, key) { - if (val.thread) { - res = res.concat( val.thread.get_childs((isNaN(nb_thread_level) ? undefined : nb_thread_level-1), true) ); - } - }); - } - return res; - }, - - /** - *search a thread in all thread and child thread. - * This method return an object thread. - * @param {object}{int} option.id - * @param {object}{string} option.model - * @param {object}{boolean} option._go_thread_wall - * private for check the top thread - * @param {object}{boolean} option.default_return_top_thread - * return the top thread (wall) if no thread found - * @return thread object - */ - browse_thread: function (options) { - // goto the wall thread for launch browse - if (!options._go_thread_wall) { - options._go_thread_wall = true; - return this.options.root_thread.browse_thread(options); - } - - if (this.id == options.id) { - return this; - } - - if (options.id) { - for (var i in this.messages) { - if (this.messages[i].thread) { - var res = this.messages[i].thread.browse_thread({'id':options.id, '_go_thread_wall':true}); - if (res) return res; - } - } - } - - //if option default_return_top_thread, return the top if no found thread - if (options.default_return_top_thread) { - return this; - } - - return false; - }, - - /** - *search a message in all thread and child thread. - * This method return an object message. - * @param {object}{int} option.id - * @param {object}{string} option.model - * @param {object}{boolean} option._go_thread_wall - * private for check the top thread - * @return message object - */ - browse_message: function (options) { - if (this.options.root_thread.messages[0]) - return this.options.root_thread.messages[0].browse_message(options); - }, - - /** - *If compose_message doesn't exist, instantiate the compose message. - * Call the on_toggle_quick_composer method to allow the user to write his message. - * (Is call when a user click on "Reply" button) - */ - on_compose_message: function (event) { - this.instantiate_compose_message(); - this.compose_message.on_toggle_quick_composer(event); - return false; - }, - - /** - *display the message "there are no message" on the thread - */ - no_message: function () { - var no_message = $(session.web.qweb.render('mail.wall_no_message', {})); - if (this.options.help) { - no_message.html(this.options.help); - } - if (!this.$el.find(".oe_view_nocontent").length) - { - no_message.appendTo(this.$el); - } - }, - - /** - *make a request to read the message (calling RPC to "message_read"). - * The result of this method is send to the switch message for sending ach message to - * his parented object thread. - * @param {Array} replace_domain: added to this.domain - * @param {Object} replace_context: added to this.context - * @param {Array} ids read (if the are some ids, the method don't use the domain) - */ - message_fetch: function (replace_domain, replace_context, ids, callback) { - return this.ds_message.call('message_read', [ - // ids force to read - ids === false ? undefined : ids && ids.slice(0, this.options.fetch_limit), - // domain + additional - (replace_domain ? replace_domain : this.domain), - // ids allready loaded - (this.id ? [this.id].concat( this.get_child_ids() ) : this.get_child_ids()), - // option for sending in flat mode by server - this.options.flat_mode, - // context + additional - (replace_context ? replace_context : this.context), - // parent_id - this.context.default_parent_id || undefined, - this.options.fetch_limit, - ]).done(callback ? _.bind(callback, this, arguments) : this.proxy('switch_new_message') - ).done(this.proxy('message_fetch_set_read')); - }, - - message_fetch_set_read: function (message_list) { - if (! this.context.mail_read_set_read) return; - var self = this; - this.render_mutex.exec(function() { - msg_ids = _.pluck(message_list, 'id'); - return self.ds_message.call('set_message_read', [msg_ids, true, false, self.context]) - .then(function (nb_read) { - if (nb_read) { - self.options.root_thread.MailWidget.do_reload_menu_emails(); - } - }); - }); - }, - - /** - *create the message object and attached on this thread. - * When the message object is create, this method call insert_message for, - * displaying this message on the DOM. - * @param : {object} data from calling RPC to "message_read" - */ - create_message_object: function (data) { - var self = this; - - data.thread_level = self.thread_level || 0; - data.options = _.extend(data.options || {}, self.options); - - if (data.type=='expandable') { - var message = new mail.ThreadExpandable(self, data, {'context':{ - 'default_model': data.model || self.context.default_model, - 'default_res_id': data.res_id || self.context.default_res_id, - 'default_parent_id': self.id, - }}); - } else { - data.record_name= (data.record_name != '' && data.record_name) || (self.parent_message && self.parent_message.record_name); - var message = new mail.ThreadMessage(self, data, {'context':{ - 'default_model': data.model, - 'default_res_id': data.res_id, - 'default_parent_id': data.id, - }}); - } - - // check if the message is already create - for (var i in self.messages) { - if (message.id && self.messages[i] && self.messages[i].id == message.id) { - self.messages[i].destroy(); - } - } - self.messages.push( message ); - - return message; - }, - - /** - *insert the message on the DOM. - * All message (and expandable message) are sorted. The method get the - * older and newer message to insert the message (before, after). - * If there are no older or newer, the message is prepend or append to - * the thread (parent object or on root thread for flat view). - * The sort is define by the thread_level (O for newer on top). - * @param : {object} ThreadMessage object - */ - insert_message: function (message, dom_insert_after, prepend) { - var self=this; - if (this.options.show_compact_message > this.thread_level) { - this.instantiate_compose_message(); - this.compose_message.do_show_compact(); - } - - this.$('.oe_view_nocontent').remove(); - if (dom_insert_after && dom_insert_after.parent()[0] == self.$el[0]) { - message.insertAfter(dom_insert_after); - } else if (prepend) { - message.prependTo(self.$el); - } else { - message.appendTo(self.$el); - } - message.$el.hide().fadeIn(500); - - return message - }, - - /** - *get the parent thread of the messages. - * Each message is send to his parent object (or parent thread flat mode) for creating the object message. - * @param : {Array} datas from calling RPC to "message_read" - */ - switch_new_message: function (records, dom_insert_after) { - var self=this; - var dom_insert_after = typeof dom_insert_after == 'object' ? dom_insert_after : false; - _(records).each(function (record) { - var thread = self.browse_thread({ - 'id': record.parent_id, - 'default_return_top_thread':true - }); - // create object and attach to the thread object - var message = thread.create_message_object( record ); - // insert the message on dom - thread.insert_message( message, dom_insert_after); - }); - if (!records.length && this.options.root_thread == this) { - this.no_message(); - } - }, - - /** - * this method is call when the widget of a message or an expandable message is destroy - * in this thread. The this.messages array is filter to remove this message - */ - on_message_detroy: function (message) { - - this.messages = _.filter(this.messages, function (val) { return !val.isDestroyed(); }); - if (this.options.root_thread == this && !this.messages.length) { - this.no_message(); - } - return false; - - }, - - /** - * Convert a destroyed message into a expandable message - */ - message_to_expandable: function (message) { - - if (!this.thread_level || message.isDestroyed()) { - message.destroy(); - return false; - } - - var messages = _.sortBy( this.messages, function (val) { return val.id; }); - var it = _.indexOf( messages, message ); - - var msg_up = message.$el.prev('.oe_msg'); - var msg_down = message.$el.next('.oe_msg'); - var msg_up = msg_up.hasClass('oe_msg_expandable') ? _.find( this.messages, function (val) { return val.$el[0] == msg_up[0]; }) : false; - var msg_down = msg_down.hasClass('oe_msg_expandable') ? _.find( this.messages, function (val) { return val.$el[0] == msg_down[0]; }) : false; - - var message_dom = [ ["id", "=", message.id] ]; - - if ( msg_up && msg_up.type == "expandable" && msg_down && msg_down.type == "expandable") { - // concat two expandable message and add this message to this dom - msg_up.domain = mail.ChatterUtils.expand_domain( msg_up.domain ); - msg_down.domain = mail.ChatterUtils.expand_domain( msg_down.domain ); - - msg_down.domain = ['|','|'].concat( msg_up.domain ).concat( message_dom ).concat( msg_down.domain ); - - if ( !msg_down.max_limit ) { - msg_down.nb_messages += 1 + msg_up.nb_messages; - } - - msg_up.$el.remove(); - msg_up.destroy(); - - msg_down.reinit(); - - } else if ( msg_up && msg_up.type == "expandable") { - // concat preview expandable message and this message to this dom - msg_up.domain = mail.ChatterUtils.expand_domain( msg_up.domain ); - msg_up.domain = ['|'].concat( msg_up.domain ).concat( message_dom ); - - msg_up.nb_messages++; - - msg_up.reinit(); - - } else if ( msg_down && msg_down.type == "expandable") { - // concat next expandable message and this message to this dom - msg_down.domain = mail.ChatterUtils.expand_domain( msg_down.domain ); - msg_down.domain = ['|'].concat( msg_down.domain ).concat( message_dom ); - - // it's maybe a message expandable for the max limit read message - if ( !msg_down.max_limit ) { - msg_down.nb_messages++; - } - - msg_down.reinit(); - - } else { - // create a expandable message - var expandable = new mail.ThreadExpandable(this, { - 'model': message.model, - 'parent_id': message.parent_id, - 'nb_messages': 1, - 'thread_level': message.thread_level, - 'parent_id': message.parent_id, - 'domain': message_dom, - 'options': message.options, - }, { - 'context':{ - 'default_model': message.model || this.context.default_model, - 'default_res_id': message.res_id || this.context.default_res_id, - 'default_parent_id': this.id, - } - }); - - // add object on array and DOM - this.messages.push(expandable); - expandable.insertAfter(message.$el); - } - - // destroy message - message.destroy(); - - return true; - }, - }); - - /** - * ------------------------------------------------------------ - * mail : root Widget - * ------------------------------------------------------------ - * - * This widget handles the display of messages with thread options. Its main - * use is to receive a context and a domain, and to delegate the message - * fetching and displaying to the Thread widget. - */ - session.web.client_actions.add('mail.Widget', 'session.mail.Widget'); - mail.Widget = session.web.Widget.extend({ - template: 'mail.Root', - - /** - * @param {Object} parent parent - * @param {Array} [domain] - * @param {Object} [context] context of the thread. It should - * contain at least default_model, default_res_id. Please refer to - * the compose_message widget for more information about it. - * @param {Object} [options] - *... @param {Number} [truncate_limit=250] number of character to - * display before having a "show more" link; note that the text - * will not be truncated if it does not have 110% of the parameter - *... @param {Boolean} [show_record_name] display the name and link for do action - *... @param {boolean} [show_reply_button] display the reply button - *... @param {boolean} [show_read_unread_button] display the read/unread button - *... @param {int} [display_indented_thread] number thread level to indented threads. - * other are on flat mode - *... @param {Boolean} [show_compose_message] allow to display the composer - *... @param {Boolean} [show_compact_message] display the compact message on the thread - * when the user clic on this compact mode, the composer is open - *... @param {Array} [message_ids] List of ids to fetch by the root thread. - * When you use this option, the domain is not used for the fetch root. - * @param {String} [no_message] Message to display when there are no message - * @param {Boolean} [show_link] Display partner (authors, followers...) on link or not - * @param {Boolean} [compose_as_todo] The root composer mark automatically the message as todo - * @param {Boolean} [readonly] Read only mode, hide all action buttons and composer - */ - init: function (parent, action) { - this._super(parent, action); - var self = this; - this.action = _.clone(action); - this.domain = this.action.domain || this.action.params.domain || []; - this.context = this.action.context || this.action.params.context || {}; - - this.action.params = _.extend({ - 'display_indented_thread' : -1, - 'show_reply_button' : false, - 'show_read_unread_button' : false, - 'truncate_limit' : 250, - 'show_record_name' : false, - 'show_compose_message' : false, - 'show_compact_message' : false, - 'compose_placeholder': false, - 'show_link': true, - 'view_inbox': false, - 'message_ids': undefined, - 'compose_as_todo' : false, - 'readonly' : false, - 'emails_from_on_composer': true, - 'fetch_limit': 30 // limit of chatter messages - }, this.action.params); - - this.action.params.help = this.action.help || false; - }, - - start: function (options) { - this._super.apply(this, arguments); - this.message_render(); - }, - - /** - * create an object "related_menu" - * contains the menu widget and the sub menu related of this wall - */ - do_reload_menu_emails: function () { - var menu = session.webclient.menu; - if (!menu || !menu.current_menu) { - return $.when(); - } - return menu.rpc("/web/menu/load_needaction", {'menu_ids': [menu.current_menu]}).done(function(r) { - menu.on_needaction_loaded(r); - }).then(function () { - menu.trigger("need_action_reloaded"); - }); - }, - - /** - *Create the root thread and display this object in the DOM. - * Call the no_message method then c all the message_fetch method - * of this root thread to display the messages. - */ - message_render: function (search) { - - this.thread = new mail.Thread(this, {}, { - 'domain' : this.domain, - 'context' : this.context, - 'options': this.action.params, - }); - - this.thread.appendTo( this.$el ); - - if (this.action.params.show_compose_message) { - this.thread.instantiate_compose_message(); - this.thread.compose_message.do_show_compact(); - } - - this.thread.message_fetch(null, null, this.action.params.message_ids); - - }, - - }); - - - /** - * ------------------------------------------------------------ - * mail_thread Widget - * ------------------------------------------------------------ - * - * This widget handles the display of messages on a document. Its main - * use is to receive a context and a domain, and to delegate the message - * fetching and displaying to the Thread widget. - * Use Help on the field to display a custom "no message loaded" - */ - session.web.form.widgets.add('mail_thread', 'openerp.mail.RecordThread'); - mail.RecordThread = session.web.form.AbstractField.extend({ - template: 'mail.record_thread', - - init: function (parent, node) { - this._super.apply(this, arguments); - this.ParentViewManager = parent; - this.node = _.clone(node); - this.node.params = _.extend({ - 'display_indented_thread': -1, - 'show_reply_button': false, - 'show_read_unread_button': true, - 'read_action': 'unread', - 'show_record_name': false, - 'show_compact_message': 1, - 'display_log_button' : true, - }, this.node.params); - if (this.node.attrs.placeholder) { - this.node.params.compose_placeholder = this.node.attrs.placeholder; - } - if (this.node.attrs.readonly) { - this.node.params.readonly = this.node.attrs.readonly; - } - if ('display_log_button' in this.options) { - this.node.params.display_log_button = this.options.display_log_button; - } - this.domain = (this.node.params && this.node.params.domain) || (this.field && this.field.domain) || []; - - if (!this.ParentViewManager.is_action_enabled('edit')) { - this.node.params.show_link = false; - } - }, - - start: function () { - this._super.apply(this, arguments); - // NB: check the actual_mode property on view to know if the view is in create mode anymore - this.view.on("change:actual_mode", this, this._check_visibility); - this._check_visibility(); - }, - - _check_visibility: function () { - this.$el.toggle(this.view.get("actual_mode") !== "create"); - }, - - render_value: function () { - var self = this; - - if (! this.view.datarecord.id || session.web.BufferedDataSet.virtual_id_regex.test(this.view.datarecord.id)) { - this.$('oe_mail_thread').hide(); - return; - } - - this.node.params = _.extend(this.node.params, { - 'message_ids': this.get_value(), - 'show_compose_message': this.view.is_action_enabled('edit'), - }); - this.node.context = { - 'mail_read_set_read': true, // set messages as read in Chatter - 'default_res_id': this.view.datarecord.id || false, - 'default_model': this.view.model || false, - }; - - if (this.root) { - $('').insertAfter(this.root.$el); - this.root.destroy(); - } - // create and render Thread widget - this.root = new mail.Widget(this, _.extend(this.node, { - 'domain' : (this.domain || []).concat([['model', '=', this.view.model], ['res_id', '=', this.view.datarecord.id]]), - })); - - return this.root.replace(this.$('.oe_mail-placeholder')); - }, - }); - - - /** - * ------------------------------------------------------------ - * Aside Widget - * ------------------------------------------------------------ - * - * This widget handles the display of a sidebar on the Wall. Its main use - * is to display group and employees suggestion (if hr is installed). - */ - mail.WallSidebar = session.web.Widget.extend({ - template: 'mail.wall.sidebar', - }); - - - /** - * ------------------------------------------------------------ - * Wall Widget - * ------------------------------------------------------------ - * - * This widget handles the display of messages on a Wall. Its main - * use is to receive a context and a domain, and to delegate the message - * fetching and displaying to the Thread widget. - */ - - session.web.client_actions.add('mail.wall', 'session.mail.Wall'); - mail.Wall = session.web.Widget.extend({ - template: 'mail.wall', - - /** - * @param {Object} parent parent - * @param {Object} [options] - * @param {Array} [options.domain] domain on the Wall - * @param {Object} [options.context] context, is an object. It should - * contain default_model, default_res_id, to give it to the threads. - */ - init: function (parent, action) { - this._super(parent, action); - this.ActionManager = parent; - - this.action = _.clone(action); - this.domain = this.action.params.domain || this.action.domain || []; - this.context = _.extend(this.action.params.context || {}, this.action.context || {}); - - // filter some parameters that we will propagate as search_default - this.defaults = {}; - for (var key in this.action.context.params) { - if (_.indexOf(['model', 'res_id'], key) == -1) { - continue; - } - this.context['search_default_' + key] = this.action.context.params[key]; - } - for (var key in this.context) { - if (key.match(/^search_default_/)) { - this.defaults[key.replace(/^search_default_/, '')] = this.context[key]; - } - } - this.action.params = _.extend({ - 'display_indented_thread': 1, - 'show_reply_button': true, - 'show_read_unread_button': true, - 'show_compose_message': true, - 'show_record_name': true, - 'show_compact_message': this.action.params.view_mailbox ? false : 1, - 'view_inbox': false, - 'emails_from_on_composer': false, - 'fetch_limit': 30 // allow inbox to load all children messages - }, this.action.params); - }, - - start: function () { - this._super.apply(this); - this.bind_events(); - var searchview_loaded = this.load_searchview(this.defaults); - if (! this.searchview.has_defaults) { - this.message_render(); - } - // render sidebar - var wall_sidebar = new mail.WallSidebar(this); - wall_sidebar.appendTo(this.$el.find('.oe_mail_wall_aside')); - }, - - /** - * Load the mail.message search view - * @param {Object} defaults ?? - */ - load_searchview: function (defaults) { - var self = this, - ds_msg = new session.web.DataSetSearch(this, 'mail.message'), - options = { $buttons: this.$('.oe-search-options') }; - this.searchview = new session.web.SearchView(this, ds_msg, false, defaults || {}, options); - this.searchview.on('search_data', this, this.do_searchview_search); - this.searchview.appendTo(this.$('.oe-view-manager-search-view')).then(function () { - self.searchview.toggle_visibility(true); - }); - if (this.searchview.has_defaults) { - this.searchview.ready.then(this.searchview.do_search); - } - return this.searchview - }, - - /** - * Get the domains, contexts and groupbys in parameter from search - * view, then render the filtered threads. - * @param {Array} domains - * @param {Array} contexts - * @param {Array} groupbys - */ - do_searchview_search: function (domains, contexts, groupbys) { - var self = this; - session.web.pyeval.eval_domains_and_contexts({ - domains: domains || [], - contexts: contexts || [], - group_by_seq: groupbys || [] - }).then(function (results) { - if(self.root) { - $('').insertAfter(self.root.$el); - self.root.destroy(); - } - return self.message_render(results); - }); - }, - - /** - * Create the root thread widget and display this object in the DOM - */ - message_render: function (search) { - var domain = this.domain.concat(search && search['domain'] ? search['domain'] : []); - var context = _.extend(this.context, search && search['context'] ? search['context'] : {}); - - this.root = new mail.Widget(this, _.extend(this.action, { - 'domain' : domain, - 'context' : context, - })); - return this.root.replace(this.$('.oe_mail-placeholder')); - }, - - bind_events: function () { - var self=this; - this.$(".oe_write_full").click(function (event) { - event.stopPropagation(); - var action = { - name: _t('Compose Email'), - type: 'ir.actions.act_window', - res_model: 'mail.compose.message', - view_mode: 'form', - view_type: 'form', - action_from: 'mail.ThreadComposeMessage', - views: [[false, 'form']], - target: 'new', - context: { - }, - }; - session.client.action_manager.do_action(action); - }); - this.$(".oe_write_onwall").click(function (event) { self.root.thread.on_compose_message(event); }); } - }); + }, - /** - * ------------------------------------------------------------ - * Sub-widgets loading - * ------------------------------------------------------------ - * - * Load here widgets that could depend on widgets defined in mail.js - */ + bind_events: function () { + var self = this; + this.$('.oe_compact_inbox').on('click', self.on_toggle_quick_composer); + this.$('.oe_compose_post').on('click', self.on_toggle_quick_composer); + this.$('.oe_compose_log').on('click', self.on_toggle_quick_composer); + this.$('input.oe_form_binary_file').on('change', _.bind( this.on_attachment_change, this)); + this.$('.oe_cancel').on('click', _.bind( this.on_cancel, this)); + this.$('.oe_post').on('click', self.on_message_post); + this.$('.oe_full').on('click', _.bind( this.on_compose_fullmail, this, this.id ? 'reply' : 'comment')); + /* stack for don't close the compose form if the user click on a button */ + this.$('.oe_msg_left, .oe_msg_center').on('mousedown', _.bind( function () { this.stay_open = true; }, this)); + this.$('.oe_msg_left, .oe_msg_content').on('mouseup', _.bind( function () { this.$('textarea').focus(); }, this)); + var ev_stay = {}; + ev_stay.mouseup = ev_stay.keydown = ev_stay.focus = function () { self.stay_open = false; }; + this.$('textarea').on(ev_stay); + this.$('textarea').autosize(); - openerp.mail.suggestions(session, mail); // import suggestion.js (suggestion widget) + // auto close + this.$('textarea').on('blur', self.on_toggle_quick_composer); + + // event: delete child attachments off the oe_msg_attachment_list box + this.$(".oe_msg_attachment_list").on('click', '.oe_delete', this.on_attachment_delete); + + this.$(".oe_recipients").on('change', 'input', this.on_checked_recipient); + }, + + on_compose_fullmail: function (default_composition_mode) { + var self = this; + if(!this.do_check_attachment_upload()) { + return false; + } + var recipient_done = $.Deferred(); + if (this.is_log) { + recipient_done.resolve([]); + } + else { + recipient_done = this.check_recipient_partners(); + } + $.when(recipient_done).done(function (partner_ids) { + var context = { + 'default_parent_id': self.id, + 'default_body': mail_utils.get_text2html(self.$el ? (self.$el.find('textarea:not(.oe_compact)').val() || '') : ''), + 'default_attachment_ids': _.map(self.attachment_ids, function (file) {return file.id;}), + 'default_partner_ids': partner_ids, + 'default_is_log': self.is_log, + 'mail_post_autofollow': true, + 'mail_post_autofollow_partner_ids': partner_ids, + 'is_private': self.is_private + }; + if (default_composition_mode != 'reply' && 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; + } - /** - * ------------------------------------------------------------ - * UserMenu - * ------------------------------------------------------------ - * - * Add a link on the top user bar for write a full mail - */ - session.web.ComposeMessageTopButton = session.web.Widget.extend({ - template:'mail.compose_message_top_button', - events: { - "click": "on_compose_message", - }, - on_compose_message: function (ev) { - ev.preventDefault(); var action = { type: 'ir.actions.act_window', res_model: 'mail.compose.message', @@ -2082,12 +436,1558 @@ openerp.mail = function (session) { view_type: 'form', views: [[false, 'form']], target: 'new', - context: {}, + context: context, }; - session.client.action_manager.do_action(action); - }, - }); - // Put the ComposeMessageTopButton widget in the systray menu - session.web.SystrayItems.push(session.web.ComposeMessageTopButton); + self.do_action(action); + self.on_cancel(); + }); + + }, + + reinit: function() { + var $render = $( qweb.render('mail.compose_message', {'widget': this}) ); + + $render.insertAfter(this.$el.last()); + this.$el.remove(); + this.$el = $render; + + this.display_attachments(); + this.bind_events(); + }, + + on_cancel: function (event) { + if (event) event.stopPropagation(); + this.attachment_ids=[]; + this.stay_open = false; + this.show_composer = false; + this.reinit(); + }, + + /* return true if all file are complete else return false and make an alert */ + do_check_attachment_upload: function () { + if (_.find(this.attachment_ids, function (file) {return file.upload;})) { + this.do_warn(qweb.render('mail.error_upload'), qweb.render('mail.error_upload_please_wait')); + return false; + } else { + return true; + } + }, + + check_recipient_partners: function () { + var self = this; + var check_done = $.Deferred(); + var recipients = _.filter(this.recipients, 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.parent_thread.ds_thread._model.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.FormOpenPopup(this); + pop.show_element( + 'res.partner', + partner_id, + { 'force_email': true, + 'ref': "compound_context", + 'default_name': parsed_email[0], + 'default_email': parsed_email[1], + }, { + title: _t("Please complete partner's informations"), + } + ); + 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.parent_thread.ds_thread._model.call('message_partner_info_from_emails', [self.context.default_res_id, new_names_to_find, true]); + } + else { + find_done.resolve([]); + } + $.when(find_done).pipe(function (result) { + var recipient_popups = result.concat(recipients_to_check); + _.each(recipient_popups, function (partner_info) { + if (partner_info.partner_id && _.indexOf(partner_info.partner_id, recipient_ids_to_remove) == -1) { + recipient_ids.push(partner_info.partner_id); + } + }); + }).pipe(function () { + check_done.resolve(recipient_ids); + }); + }); + }); + + return check_done; + }, + + on_message_post: function () { + var self = this; + if (self.flag_post) { + return; + } + if (this.do_check_attachment_upload() && (this.attachment_ids.length || this.$('textarea').val().match(/\S+/))) { + self.flag_post = true; + if (this.is_log) { + this.do_send_message_post([], this.is_log); + } + else { + this.check_recipient_partners().done(function (partner_ids) { + self.do_send_message_post(partner_ids, self.is_log); + }); + } + } + }, + + /* do post a message and fetch the message */ + do_send_message_post: function (partner_ids, log) { + var self = this; + var values = { + 'body': this.$('textarea').val(), + 'subject': false, + 'parent_id': this.context.default_parent_id, + 'attachment_ids': _.map(this.attachment_ids, function (file) {return file.id;}), + 'partner_ids': partner_ids, + 'context': _.extend(this.parent_thread.context, { + 'mail_post_autofollow': true, + 'mail_post_autofollow_partner_ids': partner_ids, + }), + 'type': 'comment', + 'content_subtype': 'plaintext', + }; + if (log) { + values.subtype = false; + } + else { + values.subtype = 'mail.mt_comment'; + } + this.parent_thread.ds_thread._model.call('message_post', [this.context.default_res_id], values).done(function (message_id) { + var thread = self.parent_thread; + var root = thread == self.options.root_thread; + if (self.options.display_indented_thread < self.thread_level && thread.parent_message) { + thread = thread.parent_message.parent_thread; + } + // create object and attach to the thread object + thread.message_fetch([["id", "=", message_id]], false, [message_id], function (arg, data) { + var message = thread.create_message_object( data.slice(-1)[0] ); + // insert the message on dom + thread.insert_message( message, root ? undefined : self.$el, root ); + }); + self.on_cancel(); + self.flag_post = false; + }); + }, + + /* Quick composer: toggle minimal / expanded mode + * - toggle minimal (one-liner) / expanded (textarea, buttons) mode + * - when going into expanded mode: + * - call `message_get_suggested_recipients` to have a list of partners to add + * - compute email_from list (list of unknown email_from to propose to create partners) + */ + on_toggle_quick_composer: function (event) { + var self = this; + var $input = $(event.target); + this.compute_emails_from(); + var email_addresses = _.pluck(this.recipients, 'email_address'); + var suggested_partners = $.Deferred(); + + // if clicked: call for suggested recipients + if (event.type == 'click') { + this.is_log = $input.hasClass('oe_compose_log'); + suggested_partners = this.parent_thread.ds_thread.call('message_get_suggested_recipients', [[this.context.default_res_id], this.context]).done(function (additional_recipients) { + var thread_recipients = additional_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.recipients.push({ + 'checked': true, + 'partner_id': recipient[0], + 'full_name': recipient[1], + 'name': parsed_email[0], + 'email_address': parsed_email[1], + 'reason': recipient[2], + }); + } + }); + }); + } + else { + suggested_partners.resolve({}); + } + + // when call for suggested partners finished: re-render the widget + $.when(suggested_partners).pipe(function () { + if ((!self.stay_open || (event && event.type == 'click')) && (!self.show_composer || !self.$('textarea:not(.oe_compact)').val().match(/\S+/) && !self.attachment_ids.length)) { + self.show_composer = !self.show_composer || self.stay_open; + self.reinit(); + } + if (!self.stay_open && self.show_composer && (!event || event.type != 'blur')) { + self.$('textarea:not(.oe_compact):first').focus(); + } + }); + + return suggested_partners; + + }, + + do_hide_compact: function () { + this.show_compact_message = false; + if (!this.show_composer) { + this.reinit(); + } + }, + + do_show_compact: function () { + this.show_compact_message = true; + if (!this.show_composer) { + this.reinit(); + } + }, + + /** Compute the list of unknown email_from the the given thread + * TDE FIXME: seems odd to delegate to the composer + * TDE TODO: please de-obfuscate and comment your code */ + compute_emails_from: function () { + var self = this; + var messages = []; + + if (this.parent_thread.parent_message) { + // go to the parented message + var message = this.parent_thread.parent_message; + var parent_message = message.parent_id ? message.parent_thread.parent_message : message; + if(parent_message){ + messages = [parent_message].concat(parent_message.get_childs()); + } + } else if (this.options.emails_from_on_composer) { + // get all wall messages if is not a mail.Wall + _.each(this.options.root_thread.messages, function (msg) {messages.push(msg); messages.concat(msg.get_childs());}); + } + + _.each(messages, function (thread) { + if (thread.author_id && !thread.author_id[0] && + !_.find(self.recipients, function (recipient) {return recipient.email_address == thread.author_id[3];}) && + _.some([thread.author_id[1], thread.author_id[2], thread.author_id[3]])) { + self.recipients.push({ 'full_name': thread.author_id[1], + 'name': thread.author_id[2], + 'email_address': thread.author_id[3], + 'partner_id': false, + 'checked': true, + 'reason': 'Incoming email author' + }); + } + }); + return self.recipients; + }, + + on_checked_recipient: function (event) { + var $input = $(event.target); + var full_name = $input.attr("data"); + _.each(this.recipients, function (recipient) { + if (recipient.full_name == full_name) { + recipient.checked = $input.is(":checked"); + } + }); + }, +}); + +/** + * ------------------------------------------------------------ + * Thread Message Expandable Widget + * ------------------------------------------------------------ + * + * This widget handles the display the expandable message in a thread. + * - thread + * - - visible message + * - - expandable + * - - visible message + * - - visible message + * - - expandable + */ +var ThreadExpandable = MessageCommon.extend({ + template: 'mail.thread.expandable', + + init: function (parent, datasets, options) { + this._super(parent, datasets, options); + this.type = 'expandable'; + this.max_limit = datasets.max_limit; + this.nb_messages = datasets.nb_messages; + this.flag_used = false; + }, + + start: function () { + this._super.apply(this, arguments); + this.bind_events(); + }, + + reinit: function () { + var $render = $(qweb.render('mail.thread.expandable', {'widget': this})); + this.$el.replaceWith( $render ); + this.$el = $render; + this.bind_events(); + }, + + /** + * Bind events in the widget. Each event is slightly described + * in the function. */ + bind_events: function () { + this.$('.oe_msg_more_message').on('click', this.on_expandable); + }, + + animated_destroy: function (fadeTime) { + var self=this; + this.$el.fadeOut(fadeTime, function () { + self.destroy(); + }); + }, + + /*The selected thread and all childs (messages/thread) became read + * @param {object} mouse envent + */ + on_expandable: function (event) { + var self = this; + if (event)event.stopPropagation(); + if (this.flag_used) { + return false; + } + this.flag_used = true; + + // read messages + self.parent_thread.message_fetch(this.domain, this.context, false, function (arg, data) { + self.id = false; + // insert the message on dom after this message + var element = self.$el; + if (self.thread_level === 0){ + element = element.parent(); + } + self.parent_thread.switch_new_message(data, element); + self.animated_destroy(200); + }); + + return false; + }, + +}); + +var ThreadMessage = MessageCommon.extend({ + template: 'mail.thread.message', + + start: function () { + this._super.apply(this, arguments); + this.bind_events(); + if(this.thread_level < this.options.display_indented_thread) { + this.create_thread(); + } + this.display_attachments(); + + this.ds_notification = new data.DataSetSearch(this, 'mail.notification'); + this.ds_message = new data.DataSetSearch(this, 'mail.message'); + }, + + /** + * Bind events in the widget. Each event is slightly described + * in the function. */ + bind_events: function () { + // header icons bindings + this.$('.oe_read').on('click', this.on_message_read); + this.$('.oe_unread').on('click', this.on_message_unread); + this.$('.oe_msg_delete').on('click', this.on_message_delete); + this.$('.oe_reply').on('click', this.on_message_reply); + this.$('.oe_star').on('click', this.on_star); + this.$('.oe_msg_vote').on('click', this.on_vote); + this.$('.oe_mail_vote_count').on('mouseenter', this.on_hover); + this.$('.oe_mail_expand').on('click', this.on_expand); + this.$('.oe_mail_reduce').on('click', this.on_expand); + this.$('.oe_mail_action_model').on('click', this.on_record_clicked); + this.$('.oe_mail_action_author').on('click', this.on_record_author_clicked); + }, + on_hover : function(event){ + var self = this; + var voter = ""; + var limit = 10; + event.stopPropagation(); + var $target = $(event.target).hasClass("fa-thumbs-o-up") ? $(event.target).parent() : $(event.target); + //Note: We can set data-content attr on target element once we fetch data so that next time when one moves mouse on element it saves call + //But if there is new like comes then we'll not have new likes in popover in that case + if ($target.data('liker-list')) + { + voter = $target.data('liker-list'); + self.bindTooltipTo($target, voter); + $target.tooltip('hide').tooltip('show'); + $(".tooltip").on("mouseleave", function () { + $(this).remove(); + }); + }else{ + this.ds_message.call('get_likers_list', [this.id, limit]) + .done(function (data) { + _.each(data, function(people, index) { + voter = voter + people.substring(0,1).toUpperCase() + people.substring(1); + if(index != data.length-1) { + voter = voter + "
"; + } + }); + $target.data('liker-list', voter); + self.bindTooltipTo($target, voter); + $target.tooltip('hide').tooltip('show'); + $(".tooltip").on("mouseleave", function () { + $(this).remove(); + }); + }); + } + return true; + }, + bindTooltipTo: function($el, value) { + $el.tooltip({ + 'title': value, + 'placement': 'top', + 'container': this.el, + 'html': true, + 'trigger': 'manual', + 'animation': false + }).on("mouseleave", function () { + setTimeout(function () { + if (!$(".tooltip:hover").length) { + $el.tooltip("hide"); + } + },100); + }); + }, + on_record_clicked: function (event) { + event.preventDefault(); + var self = this; + var state = { + 'model': this.model, + 'id': this.res_id, + 'title': this.record_name + }; + web_client.action_manager.do_push_state(state); + this.context.params = { + model: this.model, + res_id: this.res_id, + }; + this.thread.ds_thread.call("message_redirect_action", {context: this.context}).then(function(action){ + self.do_action(action); + }); + }, + + on_record_author_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); + }, + + /* Call the on_compose_message on the thread of this message. */ + on_message_reply:function (event) { + event.stopPropagation(); + this.create_thread(); + this.thread.on_compose_message(event); + return false; + }, + + on_expand: function (event) { + event.stopPropagation(); + this.$('.oe_msg_body:first > .oe_msg_body_short:first').toggle(); + this.$('.oe_msg_body:first > .oe_msg_body_long:first').toggle(); + return false; + }, + + /** + * Instantiate the thread object of this message. + * Each message have only one thread. + */ + create_thread: function () { + if (this.thread) { + return false; + } + /*create thread*/ + this.thread = new Thread(this, this, { + 'domain': this.domain, + 'context':{ + 'default_model': this.model, + 'default_res_id': this.res_id, + 'default_parent_id': this.id + }, + 'options': this.options + } + ); + /*insert thread in parent message*/ + this.thread.insertAfter(this.$el); + }, + + /** + * Fade out the message and his child thread. + * Then this object is destroyed. + */ + animated_destroy: function (fadeTime) { + var self=this; + this.$el.fadeOut(fadeTime, function () { + self.parent_thread.message_to_expandable(self); + }); + if (this.thread) { + this.thread.$el.fadeOut(fadeTime); + } + }, + + /** + * Wait a confirmation for delete the message on the DB. + * Make an animate destroy + */ + on_message_delete: function (event) { + event.stopPropagation(); + if (! confirm(_t("Do you really want to delete this message?"))) { return false; } + + this.animated_destroy(150); + // delete this message and his childs + var ids = [this.id].concat( this.get_child_ids() ); + this.ds_message.unlink(ids); + return false; + }, + + /* Check if the message must be destroy and detroy it or check for re render widget + * @param {callback} apply function + */ + check_for_rerender: function () { + var self = this; + + var messages = [this].concat(this.get_childs()); + var message_ids = _.map(messages, function (msg) { return msg.id;}); + var domain = mail_utils.expand_domain( this.options.root_thread.domain ) + .concat([["id", "in", message_ids ]]); + + return this.parent_thread.ds_message.call('message_read', [undefined, domain, [], !!this.parent_thread.options.display_indented_thread, this.context, this.parent_thread.id]) + .then( function (records) { + // remove message not loaded + _.map(messages, function (msg) { + if(!_.find(records, function (record) { return record.id == msg.id; })) { + msg.animated_destroy(150); + } else { + msg.renderElement(); + msg.start(); + } + self.options.root_thread.MailWidget.do_reload_menu_emails(); + }); + + }); + }, + + on_message_read: function (event) { + event.stopPropagation(); + this.on_message_read_unread(true); + return false; + }, + + on_message_unread: function (event) { + event.stopPropagation(); + this.on_message_read_unread(false); + return false; + }, + + /* Set the selected thread and all childs as read or unread, based on + * read parameter. + * @param {boolean} read_value + */ + on_message_read_unread: function (read_value) { + var self = this; + var messages = [this].concat(this.get_childs()); + + // inside the inbox, when the user mark a message as read/done, don't apply this value + // for the stared/favorite message + if (this.options.view_inbox && read_value) { + messages = _.filter(messages, function (val) { return !val.is_favorite && val.id; }); + if (!messages.length) { + this.check_for_rerender(); + return false; + } + } + var message_ids = _.map(messages, function (val) { return val.id; }); + + this.ds_message.call('set_message_read', [message_ids, read_value, true, this.context]) + .then(function () { + // apply modification + _.each(messages, function (msg) { + msg.to_read = !read_value; + if (msg.options.toggle_read) { + msg.options.show_read = msg.to_read; + msg.options.show_unread = !msg.to_read; + } + }); + // check if the message must be display, destroy or rerender + self.check_for_rerender(); + }); + return false; + }, + + /** + * add or remove a vote for a message and display the result + */ + on_vote: function (event) { + event.stopPropagation(); + this.ds_message.call('vote_toggle', [[this.id]]) + .then( + _.bind(function (vote) { + this.has_voted = vote; + this.vote_nb += this.has_voted ? 1 : -1; + this.display_vote(); + }, this)); + return false; + }, + + /** + * Display the render of this message's vote + */ + display_vote: function () { + var vote_element = qweb.render('mail.thread.message.vote', {'widget': this}); + this.$(".oe_msg_footer:first .oe_mail_vote_count").remove(); + this.$(".oe_msg_footer:first .oe_msg_vote").replaceWith(vote_element); + this.$('.oe_msg_vote').on('click', this.on_vote); + this.$('.oe_mail_vote_count').on('mouseenter', this.on_hover); + }, + + /** + * add or remove a favorite (or starred) for a message and change class on the DOM + */ + on_star: function (event) { + event.stopPropagation(); + var self=this; + var button = self.$('.oe_star:first'); + + this.ds_message.call('set_message_starred', [[self.id], !self.is_favorite, true]) + .then(function (star) { + self.is_favorite=star; + if (self.is_favorite) { + button.addClass('oe_starred'); + } else { + button.removeClass('oe_starred'); + } + + if (self.options.view_inbox && self.is_favorite) { + self.on_message_read_unread(true); + } else { + self.check_for_rerender(); + } + }); + return false; + }, + +}); + +/** + * ------------------------------------------------------------ + * Thread Widget + * ------------------------------------------------------------ + * + * This widget handles the display of a thread of messages. The + * thread view: + * - root thread + * - - sub message (parent_id = root message) + * - - - sub thread + * - - - - sub sub message (parent id = sub thread) + * - - sub message (parent_id = root message) + * - - - sub thread + */ +var Thread = Widget.extend({ + template: 'mail.thread', + + /** + * @param {Object} parent parent + * @param {Array} [domain] + * @param {Object} [context] context of the thread. It should + contain at least default_model, default_res_id. Please refer to + the ComposeMessage widget for more information about it. + * @param {Object} [options] + * @param {Object} [message] read about mail.ThreadMessage object + * @param {Object} [thread] + * @param {int} [display_indented_thread] number thread level to indented threads. + * other are on flat mode + * @param {Array} [parents] liked with the parents thread + * use with browse, fetch... [O]= top parent + */ + init: function (parent, datasets, options) { + this._super(parent, options); + this.MailWidget = parent instanceof MailWidget ? parent : false; + this.domain = options.domain || []; + this.context = _.extend(options.context || {}); + + this.options = options.options; + this.options.root_thread = (options.options.root_thread !== undefined ? options.options.root_thread : this); + this.options.show_compose_message = this.options.show_compose_message && !this.thread_level; + + // record options and data + this.parent_message= parent.thread !== undefined ? parent : false ; + + // data of this thread + this.id = datasets.id || false; + this.last_id = datasets.last_id || false; + this.parent_id = datasets.parent_id || false; + this.is_private = datasets.is_private || false; + this.author_id = datasets.author_id || false; + this.thread_level = (datasets.thread_level+1) || 0; + datasets.partner_ids = datasets.partner_ids || []; + if (datasets.author_id && !_.contains(_.flatten(datasets.partner_ids),datasets.author_id[0]) && datasets.author_id[0]) { + datasets.partner_ids.push(datasets.author_id); + } + this.user_pid = datasets.user_pid || false; + this.partner_ids = datasets.partner_ids; + this.messages = []; + this.options.flat_mode = (this.options.display_indented_thread - this.thread_level > 0); + + // object compose message + this.compose_message = false; + + this.ds_thread = new data.DataSetSearch(this, this.context.default_model || 'mail.thread'); + this.ds_message = new data.DataSetSearch(this, 'mail.message'); + this.render_mutex = new utils.Mutex(); + }, + + start: function () { + this._super.apply(this, arguments); + this.bind_events(); + return $.when(); + }, + + /* instantiate the compose message object and insert this on the DOM. + * The compose message is display in compact form. + */ + instantiate_compose_message: function () { + // add message composition form view + if (!this.compose_message) { + this.compose_message = new ThreadComposeMessage(this, this, { + 'context': this.options.compose_as_todo && !this.thread_level ? _.extend(this.context, { 'default_starred': true }) : this.context, + 'options': this.options, + }); + if (!this.thread_level || this.thread_level > this.options.display_indented_thread) { + this.compose_message.insertBefore(this.$el); + } else { + this.compose_message.prependTo(this.$el); + } + } + }, + + /* When the expandable object is visible on screen (with scrolling) + * then the on_expandable function is launch + */ + on_scroll: function () { + _.each( _.filter(this.messages, function (val) {return val.max_limit && !val.parent_id;}), function (val) { + var pos = val.$el.position(); + if (pos.top) { + /* bottom of the screen */ + var bottom = $(window).scrollTop()+$(window).height()+200; + if (bottom > pos.top) { + val.on_expandable(); + } + } + }); + }, + + /** + * Bind events in the widget. Each event is slightly described + * in the function. */ + bind_events: function () { + var self = this; + self.$('.oe_mail_list_recipients .oe_more').on('click', self.on_show_recipients); + self.$('.oe_mail_compose_textarea .oe_more_hidden').on('click', self.on_hide_recipients); + }, + + /** + *show all the partner list of this parent message + */ + on_show_recipients: function () { + var p=$(this).parent(); + p.find('.oe_more_hidden, .oe_hidden').show(); + p.find('.oe_more').hide(); + return false; + }, + + /** + *hide a part of the partner list of this parent message + */ + on_hide_recipients: function () { + var p=$(this).parent(); + p.find('.oe_more_hidden, .oe_hidden').hide(); + p.find('.oe_more').show(); + return false; + }, + + /* get all child message/thread id linked. + * @return array of id + */ + get_child_ids: function () { + return _.map(this.get_childs(), function (val) { return val.id; }); + }, + + /* get all child message/thread linked. + * @param {int} nb_thread_level, number of traversed thread level for this search + * @return array of thread object + */ + get_childs: function (nb_thread_level) { + var res=[]; + if (arguments[1]) res.push(this); + if (isNaN(nb_thread_level) || nb_thread_level>0) { + _(this.messages).each(function (val) { + if (val.thread) { + res = res.concat( val.thread.get_childs((isNaN(nb_thread_level) ? undefined : nb_thread_level-1), true) ); + } + }); + } + return res; + }, + + /** + *search a thread in all thread and child thread. + * This method return an object thread. + * @param {object}{int} option.id + * @param {object}{string} option.model + * @param {object}{boolean} option._go_thread_wall + * private for check the top thread + * @param {object}{boolean} option.default_return_top_thread + * return the top thread (wall) if no thread found + * @return thread object + */ + browse_thread: function (options) { + // goto the wall thread for launch browse + if (!options._go_thread_wall) { + options._go_thread_wall = true; + return this.options.root_thread.browse_thread(options); + } + + if (this.id == options.id) { + return this; + } + + if (options.id) { + for (var i in this.messages) { + if (this.messages[i].thread) { + var res = this.messages[i].thread.browse_thread({'id':options.id, '_go_thread_wall':true}); + if (res) return res; + } + } + } + + //if option default_return_top_thread, return the top if no found thread + if (options.default_return_top_thread) { + return this; + } + + return false; + }, + + /** + *search a message in all thread and child thread. + * This method return an object message. + * @param {object}{int} option.id + * @param {object}{string} option.model + * @param {object}{boolean} option._go_thread_wall + * private for check the top thread + * @return message object + */ + browse_message: function (options) { + if (this.options.root_thread.messages[0]) + return this.options.root_thread.messages[0].browse_message(options); + }, + + /** + *If compose_message doesn't exist, instantiate the compose message. + * Call the on_toggle_quick_composer method to allow the user to write his message. + * (Is call when a user click on "Reply" button) + */ + on_compose_message: function (event) { + this.instantiate_compose_message(); + this.compose_message.on_toggle_quick_composer(event); + return false; + }, + + /** + *display the message "there are no message" on the thread + */ + no_message: function () { + var no_message = $(qweb.render('mail.wall_no_message', {})); + if (this.options.help) { + no_message.html(this.options.help); + } + if (!this.$el.find(".oe_view_nocontent").length) + { + no_message.appendTo(this.$el); + } + }, + + /** + *make a request to read the message (calling RPC to "message_read"). + * The result of this method is send to the switch message for sending ach message to + * his parented object thread. + * @param {Array} replace_domain: added to this.domain + * @param {Object} replace_context: added to this.context + * @param {Array} ids read (if the are some ids, the method don't use the domain) + */ + message_fetch: function (replace_domain, replace_context, ids, callback) { + return this.ds_message.call('message_read', [ + // ids force to read + ids === false ? undefined : ids && ids.slice(0, this.options.fetch_limit), + // domain + additional + (replace_domain ? replace_domain : this.domain), + // ids allready loaded + (this.id ? [this.id].concat( this.get_child_ids() ) : this.get_child_ids()), + // option for sending in flat mode by server + this.options.flat_mode, + // context + additional + (replace_context ? replace_context : this.context), + // parent_id + this.context.default_parent_id || undefined, + this.options.fetch_limit, + ]).done(callback ? _.bind(callback, this, arguments) : this.proxy('switch_new_message') + ).done(this.proxy('message_fetch_set_read')); + }, + + message_fetch_set_read: function (message_list) { + if (! this.context.mail_read_set_read) return; + var self = this; + this.render_mutex.exec(function() { + var msg_ids = _.pluck(message_list, 'id'); + return self.ds_message.call('set_message_read', [msg_ids, true, false, self.context]) + .then(function (nb_read) { + if (nb_read) { + self.options.root_thread.MailWidget.do_reload_menu_emails(); + } + }); + }); + }, + + /** + *create the message object and attached on this thread. + * When the message object is create, this method call insert_message for, + * displaying this message on the DOM. + * @param : {object} data from calling RPC to "message_read" + */ + create_message_object: function (data) { + var self = this; + var message; + + data.thread_level = self.thread_level || 0; + data.options = _.extend(data.options || {}, self.options); + + if (data.type=='expandable') { + message = new ThreadExpandable(self, data, {'context':{ + 'default_model': data.model || self.context.default_model, + 'default_res_id': data.res_id || self.context.default_res_id, + 'default_parent_id': self.id, + }}); + } else { + data.record_name= (data.record_name !== '' && data.record_name) || (self.parent_message && self.parent_message.record_name); + message = new ThreadMessage(self, data, {'context':{ + 'default_model': data.model, + 'default_res_id': data.res_id, + 'default_parent_id': data.id, + }}); + } + + // check if the message is already create + for (var i in self.messages) { + if (message.id && self.messages[i] && self.messages[i].id === message.id) { + self.messages[i].destroy(); + } + } + self.messages.push( message ); + + return message; + }, + + /** + *insert the message on the DOM. + * All message (and expandable message) are sorted. The method get the + * older and newer message to insert the message (before, after). + * If there are no older or newer, the message is prepend or append to + * the thread (parent object or on root thread for flat view). + * The sort is define by the thread_level (O for newer on top). + * @param : {object} ThreadMessage object + */ + insert_message: function (message, dom_insert_after, prepend) { + var self=this; + if (this.options.show_compact_message > this.thread_level) { + this.instantiate_compose_message(); + this.compose_message.do_show_compact(); + } + + this.$('.oe_view_nocontent').remove(); + if (dom_insert_after && dom_insert_after.parent()[0] == self.$el[0]) { + message.insertAfter(dom_insert_after); + } else if (prepend) { + message.prependTo(self.$el); + } else { + message.appendTo(self.$el); + } + message.$el.hide().fadeIn(500); + + return message; + }, + + /** + *get the parent thread of the messages. + * Each message is send to his parent object (or parent thread flat mode) for creating the object message. + * @param : {Array} datas from calling RPC to "message_read" + */ + switch_new_message: function (records, dom_insert_after) { + var self = this; + dom_insert_after = typeof dom_insert_after == 'object' ? dom_insert_after : false; + _(records).each(function (record) { + var thread = self.browse_thread({ + 'id': record.parent_id, + 'default_return_top_thread':true + }); + // create object and attach to the thread object + var message = thread.create_message_object( record ); + // insert the message on dom + thread.insert_message( message, dom_insert_after); + }); + if (!records.length && this.options.root_thread == this) { + this.no_message(); + } + }, + + /** + * this method is call when the widget of a message or an expandable message is destroy + * in this thread. The this.messages array is filter to remove this message + */ + on_message_detroy: function () { + + this.messages = _.filter(this.messages, function (val) { return !val.isDestroyed(); }); + if (this.options.root_thread == this && !this.messages.length) { + this.no_message(); + } + return false; + + }, + + /** + * Convert a destroyed message into a expandable message + */ + message_to_expandable: function (message) { + + if (!this.thread_level || message.isDestroyed()) { + message.destroy(); + return false; + } + + var messages = _.sortBy( this.messages, function (val) { return val.id; }); + + var msg_up = message.$el.prev('.oe_msg'); + var msg_down = message.$el.next('.oe_msg'); + msg_up = msg_up.hasClass('oe_msg_expandable') ? _.find( this.messages, function (val) { return val.$el[0] == msg_up[0]; }) : false; + msg_down = msg_down.hasClass('oe_msg_expandable') ? _.find( this.messages, function (val) { return val.$el[0] == msg_down[0]; }) : false; + + var message_dom = [ ["id", "=", message.id] ]; + + if ( msg_up && msg_up.type == "expandable" && msg_down && msg_down.type == "expandable") { + // concat two expandable message and add this message to this dom + msg_up.domain = mail_utils.expand_domain( msg_up.domain ); + msg_down.domain = mail_utils.expand_domain( msg_down.domain ); + + msg_down.domain = ['|','|'].concat( msg_up.domain ).concat( message_dom ).concat( msg_down.domain ); + + if ( !msg_down.max_limit ) { + msg_down.nb_messages += 1 + msg_up.nb_messages; + } + + msg_up.$el.remove(); + msg_up.destroy(); + + msg_down.reinit(); + + } else if ( msg_up && msg_up.type == "expandable") { + // concat preview expandable message and this message to this dom + msg_up.domain = mail_utils.expand_domain( msg_up.domain ); + msg_up.domain = ['|'].concat( msg_up.domain ).concat( message_dom ); + + msg_up.nb_messages++; + + msg_up.reinit(); + + } else if ( msg_down && msg_down.type == "expandable") { + // concat next expandable message and this message to this dom + msg_down.domain = mail_utils.expand_domain( msg_down.domain ); + msg_down.domain = ['|'].concat( msg_down.domain ).concat( message_dom ); + + // it's maybe a message expandable for the max limit read message + if ( !msg_down.max_limit ) { + msg_down.nb_messages++; + } + + msg_down.reinit(); + + } else { + // create a expandable message + var expandable = new ThreadExpandable(this, { + 'model': message.model, + 'parent_id': message.parent_id, + 'nb_messages': 1, + 'thread_level': message.thread_level, + 'domain': message_dom, + 'options': message.options, + }, { + 'context':{ + 'default_model': message.model || this.context.default_model, + 'default_res_id': message.res_id || this.context.default_res_id, + 'default_parent_id': this.id, + } + }); + + // add object on array and DOM + this.messages.push(expandable); + expandable.insertAfter(message.$el); + } + + // destroy message + message.destroy(); + + return true; + }, +}); +/** + * ------------------------------------------------------------ + * mail : root Widget + * ------------------------------------------------------------ + * + * This widget handles the display of messages with thread options. Its main + * use is to receive a context and a domain, and to delegate the message + * fetching and displaying to the Thread widget. + */ +var MailWidget = Widget.extend({ + template: 'mail.Root', + + /** + * @param {Object} parent parent + * @param {Array} [domain] + * @param {Object} [context] context of the thread. It should + * contain at least default_model, default_res_id. Please refer to + * the compose_message widget for more information about it. + * @param {Object} [options] + *... @param {Number} [truncate_limit=250] number of character to + * display before having a "show more" link; note that the text + * will not be truncated if it does not have 110% of the parameter + *... @param {Boolean} [show_record_name] display the name and link for do action + *... @param {boolean} [show_reply_button] display the reply button + *... @param {boolean} [show_read_unread_button] display the read/unread button + *... @param {int} [display_indented_thread] number thread level to indented threads. + * other are on flat mode + *... @param {Boolean} [show_compose_message] allow to display the composer + *... @param {Boolean} [show_compact_message] display the compact message on the thread + * when the user clic on this compact mode, the composer is open + *... @param {Array} [message_ids] List of ids to fetch by the root thread. + * When you use this option, the domain is not used for the fetch root. + * @param {String} [no_message] Message to display when there are no message + * @param {Boolean} [show_link] Display partner (authors, followers...) on link or not + * @param {Boolean} [compose_as_todo] The root composer mark automatically the message as todo + * @param {Boolean} [readonly] Read only mode, hide all action buttons and composer + */ + init: function (parent, action) { + this._super(parent, action); + this.action = _.clone(action); + this.domain = this.action.domain || this.action.params.domain || []; + this.context = this.action.context || this.action.params.context || {}; + + this.action.params = _.extend({ + 'display_indented_thread' : -1, + 'show_reply_button' : false, + 'show_read_unread_button' : false, + 'truncate_limit' : 250, + 'show_record_name' : false, + 'show_compose_message' : false, + 'show_compact_message' : false, + 'compose_placeholder': false, + 'show_link': true, + 'view_inbox': false, + 'message_ids': undefined, + 'compose_as_todo' : false, + 'readonly' : false, + 'emails_from_on_composer': true, + 'fetch_limit': 30 // limit of chatter messages + }, this.action.params); + + this.action.params.help = this.action.help || false; + }, + + start: function () { + this._super.apply(this, arguments); + this.message_render(); + }, + + /** + * create an object "related_menu" + * contains the menu widget and the sub menu related of this wall + */ + do_reload_menu_emails: function () { + var menu = web_client.menu; + if (!menu || !menu.current_menu) { + return $.when(); + } + return menu.rpc("/web/menu/load_needaction", {'menu_ids': [menu.current_menu]}).done(function(r) { + menu.on_needaction_loaded(r); + }).then(function () { + menu.trigger("need_action_reloaded"); + }); + }, + + /** + *Create the root thread and display this object in the DOM. + * Call the no_message method then c all the message_fetch method + * of this root thread to display the messages. + */ + message_render: function () { + + this.thread = new Thread(this, {}, { + 'domain' : this.domain, + 'context' : this.context, + 'options': this.action.params, + }); + + this.thread.appendTo( this.$el ); + + if (this.action.params.show_compose_message) { + this.thread.instantiate_compose_message(); + this.thread.compose_message.do_show_compact(); + } + + this.thread.message_fetch(null, null, this.action.params.message_ids); + + }, + +}); +core.action_registry.add('mail.Widget', MailWidget); + + +/** + * ------------------------------------------------------------ + * mail_thread Widget + * ------------------------------------------------------------ + * + * This widget handles the display of messages on a document. Its main + * use is to receive a context and a domain, and to delegate the message + * fetching and displaying to the Thread widget. + * Use Help on the field to display a custom "no message loaded" + */ +var RecordThread = form_common.AbstractField.extend({ + template: 'mail.record_thread', + + init: function (parent, node) { + this._super.apply(this, arguments); + this.ParentViewManager = parent; + this.node = _.clone(node); + this.node.params = _.extend({ + 'display_indented_thread': -1, + 'show_reply_button': false, + 'show_read_unread_button': true, + 'read_action': 'unread', + 'show_record_name': false, + 'show_compact_message': 1, + 'display_log_button' : true, + }, this.node.params); + if (this.node.attrs.placeholder) { + this.node.params.compose_placeholder = this.node.attrs.placeholder; + } + if (this.node.attrs.readonly) { + this.node.params.readonly = this.node.attrs.readonly; + } + if ('display_log_button' in this.options) { + this.node.params.display_log_button = this.options.display_log_button; + } + this.domain = (this.node.params && this.node.params.domain) || (this.field && this.field.domain) || []; + + if (!this.ParentViewManager.is_action_enabled('edit')) { + this.node.params.show_link = false; + } + }, + + start: function () { + this._super.apply(this, arguments); + // NB: check the actual_mode property on view to know if the view is in create mode anymore + this.view.on("change:actual_mode", this, this._check_visibility); + this._check_visibility(); + }, + + _check_visibility: function () { + this.$el.toggle(this.view.get("actual_mode") !== "create"); + }, + + render_value: function () { + if (! this.view.datarecord.id || data.BufferedDataSet.virtual_id_regex.test(this.view.datarecord.id)) { + this.$('oe_mail_thread').hide(); + return; + } + + this.node.params = _.extend(this.node.params, { + 'message_ids': this.get_value(), + 'show_compose_message': this.view.is_action_enabled('edit'), + }); + this.node.context = { + 'mail_read_set_read': true, // set messages as read in Chatter + 'default_res_id': this.view.datarecord.id || false, + 'default_model': this.view.model || false, + }; + + if (this.root) { + $('').insertAfter(this.root.$el); + this.root.destroy(); + } + // create and render Thread widget + this.root = new MailWidget(this, _.extend(this.node, { + 'domain' : (this.domain || []).concat([['model', '=', this.view.model], ['res_id', '=', this.view.datarecord.id]]), + })); + + return this.root.replace(this.$('.oe_mail-placeholder')); + }, +}); +core.form_widget_registry.add('mail_thread', RecordThread); + +/** + * ------------------------------------------------------------ + * Aside Widget + * ------------------------------------------------------------ + * + * This widget handles the display of a sidebar on the Wall. Its main use + * is to display group and employees suggestion (if hr is installed). + */ +var WallSidebar = Widget.extend({ + template: 'mail.wall.sidebar', +}); + + + +/** + * ------------------------------------------------------------ + * Wall Widget + * ------------------------------------------------------------ + * + * This widget handles the display of messages on a Wall. Its main + * use is to receive a context and a domain, and to delegate the message + * fetching and displaying to the Thread widget. + */ + + +var MailWall = Widget.extend({ + template: 'mail.wall', + + /** + * @param {Object} parent parent + * @param {Object} [options] + * @param {Array} [options.domain] domain on the Wall + * @param {Object} [options.context] context, is an object. It should + * contain default_model, default_res_id, to give it to the threads. + */ + init: function (parent, action) { + var key; + this._super(parent, action); + this.ActionManager = parent; + + this.action = _.clone(action); + this.domain = this.action.params.domain || this.action.domain || []; + this.context = _.extend(this.action.params.context || {}, this.action.context || {}); + + // filter some parameters that we will propagate as search_default + this.defaults = {}; + for (key in this.action.context.params) { + if (_.indexOf(['model', 'res_id'], key) == -1) { + continue; + } + this.context['search_default_' + key] = this.action.context.params[key]; + } + for (key in this.context) { + if (key.match(/^search_default_/)) { + this.defaults[key.replace(/^search_default_/, '')] = this.context[key]; + } + } + this.action.params = _.extend({ + 'display_indented_thread': 1, + 'show_reply_button': true, + 'show_read_unread_button': true, + 'show_compose_message': true, + 'show_record_name': true, + 'show_compact_message': this.action.params.view_mailbox ? false : 1, + 'view_inbox': false, + 'emails_from_on_composer': false, + 'fetch_limit': 30 // allow inbox to load all children messages + }, this.action.params); + }, + + start: function () { + this._super.apply(this); + this.bind_events(); + this.load_searchview(this.defaults); + if (! this.searchview.has_defaults) { + this.message_render(); + } + // render sidebar + var wall_sidebar = new WallSidebar(this); + wall_sidebar.appendTo(this.$el.find('.oe_mail_wall_aside')); + }, + + /** + * Load the mail.message search view + * @param {Object} defaults ?? + */ + load_searchview: function (defaults) { + var self = this, + ds_msg = new data.DataSetSearch(this, 'mail.message'), + options = { $buttons: this.$('.oe-search-options') }; + this.searchview = new SearchView(this, ds_msg, false, defaults || {}, options); + this.searchview.on('search_data', this, this.do_searchview_search); + this.searchview.appendTo(this.$('.oe-view-manager-search-view')).then(function () { + self.searchview.toggle_visibility(true); + }); + if (this.searchview.has_defaults) { + this.searchview.ready.then(this.searchview.do_search); + } + return this.searchview; + }, + + /** + * Get the domains, contexts and groupbys in parameter from search + * view, then render the filtered threads. + * @param {Array} domains + * @param {Array} contexts + * @param {Array} groupbys + */ + do_searchview_search: function (domains, contexts, groupbys) { + var self = this; + pyeval.eval_domains_and_contexts({ + domains: domains || [], + contexts: contexts || [], + group_by_seq: groupbys || [] + }).then(function (results) { + if(self.root) { + $('').insertAfter(self.root.$el); + self.root.destroy(); + } + return self.message_render(results); + }); + }, + + /** + * Create the root thread widget and display this object in the DOM + */ + message_render: function (search) { + var domain = this.domain.concat(search && search.domain ? search.domain : []); + var context = _.extend(this.context, search && search.context ? search.context : {}); + + this.root = new MailWidget(this, _.extend(this.action, { + 'domain' : domain, + 'context' : context, + })); + return this.root.replace(this.$('.oe_mail-placeholder')); + }, + + bind_events: function () { + var self=this; + this.$(".oe_write_full").click(function (event) { + event.stopPropagation(); + var action = { + name: _t('Compose Email'), + type: 'ir.actions.act_window', + res_model: 'mail.compose.message', + view_mode: 'form', + view_type: 'form', + action_from: 'mail.ThreadComposeMessage', + views: [[false, 'form']], + target: 'new', + context: { + }, + }; + web_client.action_manager.do_action(action); + }); + this.$(".oe_write_onwall").click(function (event) { self.root.thread.on_compose_message(event); }); + } +}); +core.action_registry.add('mail.wall', MailWall); + +/** + * ------------------------------------------------------------ + * Sub-widgets loading + * ------------------------------------------------------------ + * + * Load here widgets that could depend on widgets defined in mail.js + */ + +// openerp.mail.suggestions(session, mail); // import suggestion.js (suggestion widget) + +/** + * ------------------------------------------------------------ + * UserMenu + * ------------------------------------------------------------ + * + * Add a link on the top user bar for write a full mail + */ +var ComposeMessageTopButton = Widget.extend({ + template:'mail.compose_message_top_button', + events: { + "click": "on_compose_message", + }, + on_compose_message: function (ev) { + ev.preventDefault(); + var action = { + type: 'ir.actions.act_window', + res_model: 'mail.compose.message', + view_mode: 'form', + view_type: 'form', + views: [[false, 'form']], + target: 'new', + context: {}, + }; + web_client.action_manager.do_action(action); + }, +}); + +// Put the ComposeMessageTopButton widget in the systray menu +SystrayMenu.Items.push(ComposeMessageTopButton); + +return { + Thread: Thread, + RecordThread: RecordThread, + WallSidebar: WallSidebar, + Wall: MailWall, }; + +}); diff --git a/addons/mail/static/src/js/mail_followers.js b/addons/mail/static/src/js/mail_followers.js index 78b80269fc3..3d6d6d1ed16 100644 --- a/addons/mail/static/src/js/mail_followers.js +++ b/addons/mail/static/src/js/mail_followers.js @@ -1,380 +1,388 @@ -openerp_mail_followers = function(session, mail) { - var _t = session.web._t, - _lt = session.web._lt; +odoo.define('mail.mail_followers', ['mail.utils', 'web.core', 'web.data', 'web.Dialog', 'web.form_common', 'web.session', 'web.web_client'], function (require) { +"use strict"; - var mail_followers = session.mail_followers = {}; +var mail_utils = require('mail.utils'); +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'); - /** - * ------------------------------------------------------------ - * 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 _t = core._t; +var qweb = core.qweb; - /* Add the widget to registry */ - session.web.form.widgets.add('mail_followers', 'openerp.mail_followers.Followers'); +/** + * ------------------------------------------------------------ + * 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. + */ - mail_followers.Followers = session.web.form.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.displayed_limit = this.node.attrs.displayed_nb || 10; - this.displayed_nb = this.displayed_limit; - this.ds_model = new session.web.DataSetSearch(this, this.view.model); - this.ds_users = new session.web.DataSetSearch(this, 'res.users'); +var Followers = form_common.AbstractField.extend({ + template: 'mail.followers', - this.value = []; - this.followers = []; - - this.view_is_editable = this.__parentedParent.is_action_enabled('edit'); - }, + init: function() { + this._super.apply(this, arguments); + this.image = this.node.attrs.image || 'image_small'; + this.comment = this.node.attrs.help || false; + this.displayed_limit = this.node.attrs.displayed_nb || 10; + this.displayed_nb = this.displayed_limit; + this.ds_model = new data.DataSetSearch(this, this.view.model); + this.ds_users = new data.DataSetSearch(this, 'res.users'); - 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(); - 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.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.$('.oe_follower').on('click', function (event) { - if($(this).hasClass('oe_notfollow')) - self.do_follow(); - else - self.do_unfollow(); - }); - // event: click on a subtype, that (un)subscribe for this subtype - this.$el.on('click', '.oe_subtype_list input', function(event) { - self.do_update_subscription(event); - var $list = self.$('.oe_subtype_list'); - if(!$list.hasClass('open')) { - $list.addClass('open'); - } - if(self.$('.oe_subtype_list ul')[0].children.length < 1) { - $list.removeClass('open'); - } - event.stopPropagation(); - }); - // event: click on 'invite' button, that opens the invite wizard - this.$('.oe_invite').on('click', self.on_invite_follower); - // event: click on 'edit_subtype(pencil)' button to edit subscription - this.$el.on('click', '.oe_edit_subtype', self.on_edit_subtype); - this.$el.on('click', '.oe_remove_follower', self.on_remove_follower); - this.$el.on('click', '.oe_show_more', self.on_show_more_followers); - 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('id'); - $('div.oe_edit_actions').remove(); - self.$dialog = new session.web.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); - this.parents('.modal').modal('hide'); - }}, - { text: _t("Cancel"), click: function() { this.parents('.modal').modal('hide'); }} - ], - }, "

").open(); - return self.fetch_subtypes(user_pid); - }, - - on_invite_follower: function (event) { - 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, - }, - } - this.do_action(action, { - on_close: function() { - self.read_value(); - }, - }); - }, - - on_show_more_followers: function (event) { - this.displayed_nb += this.displayed_limit; - this.display_followers(false); - }, - - on_remove_follower: function (event) { - var partner_id = $(event.target).data('id'); - var name = $(event.target).parent().find("a").html(); - if (confirm(_.str.sprintf(_t("Warning! \n %s won't be notified of any email or discussion on this document. Do you really want to remove him from the followers ?"), name))) { - var context = new session.web.CompoundContext(this.build_context(), {}); - return this.ds_model.call('message_unsubscribe', [[this.view.datarecord.id], [partner_id], context]) - .then(this.proxy('read_value')); - } - }, - - 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 - }; - session.webclient.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; - this.displayed_nb = this.displayed_limit; - 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 this.ds_model.call('read_followers_data', [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 (error, event) { - var self = this; - event.preventDefault(); - return this.ds_users.call('read', [[this.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 followers'); - }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 () { - var self = this; - var node_user_list = this.$('.oe_follower_list').empty(); - this.$('.oe_follower_title').html(this._format_followers(this.value.length)); - }, - - /** Display the followers */ - display_followers: function (records) { - var self = this; - this.message_is_follower = false; - this.followers = records || this.followers; - // clean and display title - var node_user_list = this.$('.oe_follower_list').empty(); - this.$('.oe_follower_title').html(this._format_followers(this.followers.length)); - self.message_is_follower = _.indexOf(this.followers.map(function (rec) { return rec[2]['is_uid']}), true) != -1; - // truncate number of displayed followers - var truncated = this.followers.slice(0, this.displayed_nb); - _(truncated).each(function (record) { - partner = { - 'id': record[0], - 'name': record[1], - 'is_uid': record[2]['is_uid'], - 'is_editable': record[2]['is_editable'], - 'avatar_url': mail.ChatterUtils.get_image(self.session, 'res.partner', 'image_small', record[0]), - } - $(session.web.qweb.render('mail.followers.partner', {'record': partner, 'widget': self})).appendTo(node_user_list); - // On mouse-enter it will show the edit_subtype pencil. - if (partner.is_editable) { - self.$('.oe_follower_list').on('mouseenter mouseleave', function(e) { - self.$('.oe_edit_subtype').toggleClass('oe_hidden', e.type == 'mouseleave'); - self.$('.oe_follower_list').find('.oe_partner').toggleClass('oe_partner_name', e.type == 'mouseenter'); - }); - } - }); - // FVA note: be sure it is correctly translated - if (truncated.length < this.followers.length) { - $(session.web.qweb.render('mail.followers.show_more', {'number': (this.followers.length - truncated.length)} )).appendTo(node_user_list); - } - }, - - display_buttons: function () { - if (this.message_is_follower) { - this.$('button.oe_follower').removeClass('oe_notfollow').addClass('oe_following'); - } - else { - this.$('button.oe_follower').removeClass('oe_following').addClass('oe_notfollow'); - } - - if (this.view.is_action_enabled('edit')) - this.$('span.oe_mail_invite_wrapper').hide(); - else - this.$('span.oe_mail_invite_wrapper').show(); - }, - - /** Fetch subtypes, only if current user is follower */ - fetch_subtypes: function (user_pid) { - var self = this; - var dialog = false; - if (user_pid) { - dialog = true; - } else { - var subtype_list_ul = this.$('.oe_subtype_list ul').empty(); - if (! this.message_is_follower) { - this.$('.oe_subtype_list > .dropdown-toggle').attr('disabled', true); - return; - } - else { - this.$('.oe_subtype_list > .dropdown-toggle').attr('disabled', false); - } - } - var id = this.view.datarecord.id; - this.ds_model.call('message_get_subscription_data', [[id], user_pid, new session.web.CompoundContext(this.build_context(), {})]) - .then(function (data) {self.display_subtypes(data, id, dialog);}); - }, - - /** Display subtypes: {'name': default, followed} */ - display_subtypes:function (data, id, dialog) { - var self = this; - if (dialog) { - var $list = self.$dialog.$el; - } - else { - var $list = this.$('.oe_subtype_list ul'); - } - var records = data[id].message_subtype_data; - this.records_length = $.map(records, function(value, index) { return index; }).length; - if (this.records_length > 1) { self.display_followers(); } - var old_model = ''; - _(records).each(function (record, record_name) { - if (old_model != record.parent_model){ - if (old_model != ''){ - var index = $($list).find('.oe_subtype').length; - $($($list).find('.oe_subtype')[index-1]).addClass('subtype-border'); - } - old_model = record.parent_model; - } - record.name = record_name; - record.followed = record.followed || undefined; - $(session.web.qweb.render('mail.followers.subtype', {'record': record, 'dialog': dialog})).appendTo($list); - }); - }, - - do_follow: function () { - var context = new session.web.CompoundContext(this.build_context(), {}); - this.$('.oe_subtype_list > .dropdown-toggle').attr('disabled', false); - this.ds_model.call('message_subscribe_users', [[this.view.datarecord.id], [this.session.uid], undefined, context]) - .then(this.proxy('read_value')); - - _.each(this.$('.oe_subtype_list input'), function (record) { - $(record).attr('checked', 'checked'); - }); - }, + this.value = []; + this.followers = []; - do_unfollow: function (user_pid) { - var self = this; - if (confirm(_t("Warning! \nYou won't be notified of any email or discussion on this document. Do you really want to unfollow this document ?"))) { - _(this.$('.oe_msg_subtype_check')).each(function (record) { - $(record).attr('checked',false); + 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(); + 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.message_is_follower = false; + this.display_buttons(); + }, + + bind_events: function() { + var self = this; + // event: click on '(Un)Follow' button, that toggles the follow for uid + this.$('.oe_follower').on('click', function (event) { + if($(this).hasClass('oe_notfollow')) + self.do_follow(); + else + self.do_unfollow(); + }); + // event: click on a subtype, that (un)subscribe for this subtype + this.$el.on('click', '.oe_subtype_list input', function(event) { + self.do_update_subscription(event); + var $list = self.$('.oe_subtype_list'); + if(!$list.hasClass('open')) { + $list.addClass('open'); + } + if(self.$('.oe_subtype_list ul')[0].children.length < 1) { + $list.removeClass('open'); + } + event.stopPropagation(); + }); + // event: click on 'invite' button, that opens the invite wizard + this.$('.oe_invite').on('click', self.on_invite_follower); + // event: click on 'edit_subtype(pencil)' button to edit subscription + this.$el.on('click', '.oe_edit_subtype', self.on_edit_subtype); + this.$el.on('click', '.oe_remove_follower', self.on_remove_follower); + this.$el.on('click', '.oe_show_more', self.on_show_more_followers); + 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('id'); + $('div.oe_edit_actions').remove(); + 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); + this.parents('.modal').modal('hide'); + }}, + { text: _t("Cancel"), click: function() { this.parents('.modal').modal('hide'); }} + ], + }, "
").open(); + return self.fetch_subtypes(user_pid); + }, + + on_invite_follower: function (event) { + 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, + }, + }; + this.do_action(action, { + on_close: function() { + self.read_value(); + }, + }); + }, + + on_show_more_followers: function (event) { + this.displayed_nb += this.displayed_limit; + this.display_followers(false); + }, + + on_remove_follower: function (event) { + var partner_id = $(event.target).data('id'); + var name = $(event.target).parent().find("a").html(); + if (confirm(_.str.sprintf(_t("Warning! \n %s won't be notified of any email or discussion on this document. Do you really want to remove him from the followers ?"), name))) { + var context = new data.CompoundContext(this.build_context(), {}); + return this.ds_model.call('message_unsubscribe', [[this.view.datarecord.id], [partner_id], context]) + .then(this.proxy('read_value')); + } + }, + + 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; + this.displayed_nb = this.displayed_limit; + 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 this.ds_model.call('read_followers_data', [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 (error, event) { + var self = this; + event.preventDefault(); + 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 followers'); + }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.$('.oe_follower_list').empty(); + this.$('.oe_follower_title').html(this._format_followers(this.value.length)); + }, + + /** Display the followers */ + display_followers: function (records) { + var self = this; + this.message_is_follower = false; + this.followers = records || this.followers; + // clean and display title + var node_user_list = this.$('.oe_follower_list').empty(); + this.$('.oe_follower_title').html(this._format_followers(this.followers.length)); + self.message_is_follower = _.indexOf(this.followers.map(function (rec) { return rec[2]['is_uid'];}), true) != -1; + // truncate number of displayed followers + var truncated = this.followers.slice(0, this.displayed_nb); + _(truncated).each(function (record) { + var partner = { + 'id': record[0], + 'name': record[1], + 'is_uid': record[2]['is_uid'], + 'is_editable': record[2]['is_editable'], + 'avatar_url': mail_utils.get_image(session, 'res.partner', 'image_small', record[0]), + }; + $(qweb.render('mail.followers.partner', {'record': partner, 'widget': self})).appendTo(node_user_list); + // On mouse-enter it will show the edit_subtype pencil. + if (partner.is_editable) { + self.$('.oe_follower_list').on('mouseenter mouseleave', function(e) { + self.$('.oe_edit_subtype').toggleClass('oe_hidden', e.type == 'mouseleave'); + self.$('.oe_follower_list').find('.oe_partner').toggleClass('oe_partner_name', e.type == 'mouseenter'); }); - var action_unsubscribe = 'message_unsubscribe_users'; - this.$('.oe_subtype_list > .dropdown-toggle').attr('disabled', true); - var follower_ids = [this.session.uid]; - if (user_pid) { - action_unsubscribe = 'message_unsubscribe'; - follower_ids = [user_pid]; } - var context = new session.web.CompoundContext(this.build_context(), {}); - return this.ds_model.call(action_unsubscribe, [[this.view.datarecord.id], follower_ids, context]) - .then(this.proxy('read_value')); - } - return false; - }, + }); + // FVA note: be sure it is correctly translated + if (truncated.length < this.followers.length) { + $(qweb.render('mail.followers.show_more', {'number': (this.followers.length - truncated.length)} )).appendTo(node_user_list); + } + }, - do_update_subscription: function (event, user_pid) { - var self = this; + display_buttons: function () { + if (this.message_is_follower) { + this.$('button.oe_follower').removeClass('oe_notfollow').addClass('oe_following'); + } + else { + this.$('button.oe_follower').removeClass('oe_following').addClass('oe_notfollow'); + } - var action_subscribe = 'message_subscribe_users'; - var follower_ids = [this.session.uid]; - var oe_action = this.$('.oe_actions input[type="checkbox"]'); - if (user_pid) { - action_subscribe = 'message_subscribe'; - follower_ids = [user_pid]; - oe_action = $('.oe_edit_actions input[type="checkbox"]'); + if (this.view.is_action_enabled('edit')) + this.$('span.oe_mail_invite_wrapper').hide(); + else + this.$('span.oe_mail_invite_wrapper').show(); + }, + + /** Fetch subtypes, only if current user is follower */ + fetch_subtypes: function (user_pid) { + var self = this; + var dialog = false; + if (user_pid) { + dialog = true; + } else { + this.$('.oe_subtype_list ul').empty(); + if (! this.message_is_follower) { + this.$('.oe_subtype_list > .dropdown-toggle').attr('disabled', true); + return; } - var checklist = new Array(); - _(oe_action).each(function (record) { - if ($(record).is(':checked')) { - checklist.push(parseInt($(record).data('id'))); + else { + this.$('.oe_subtype_list > .dropdown-toggle').attr('disabled', false); + } + } + var id = this.view.datarecord.id; + this.ds_model.call('message_get_subscription_data', [[id], user_pid, new data.CompoundContext(this.build_context(), {})]) + .then(function (data) {self.display_subtypes(data, id, dialog);}); + }, + + /** Display subtypes: {'name': default, followed} */ + display_subtypes:function (data, id, dialog) { + var self = this; + var $list; + if (dialog) { + $list = self.$dialog.$el; + } + else { + $list = this.$('.oe_subtype_list ul'); + } + var records = data[id].message_subtype_data; + this.records_length = $.map(records, function(value, index) { return index; }).length; + if (this.records_length > 1) { self.display_followers(); } + var old_model = ''; + _(records).each(function (record, record_name) { + if (old_model != record.parent_model){ + if (old_model !== ''){ + var index = $($list).find('.oe_subtype').length; + $($($list).find('.oe_subtype')[index-1]).addClass('subtype-border'); } + old_model = record.parent_model; + } + record.name = record_name; + 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.$('.oe_subtype_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.$('.oe_subtype_list input'), function (record) { + $(record).attr('checked', 'checked'); + }); + }, + + do_unfollow: function (user_pid) { + if (confirm(_t("Warning! \nYou won't be notified of any email or discussion on this document. Do you really want to unfollow this document ?"))) { + _(this.$('.oe_msg_subtype_check')).each(function (record) { + $(record).attr('checked',false); }); + var action_unsubscribe = 'message_unsubscribe_users'; + this.$('.oe_subtype_list > .dropdown-toggle').attr('disabled', true); + var follower_ids = [session.uid]; + if (user_pid) { + action_unsubscribe = 'message_unsubscribe'; + follower_ids = [user_pid]; + } + var context = new data.CompoundContext(this.build_context(), {}); + return this.ds_model.call(action_unsubscribe, [[this.view.datarecord.id], follower_ids, context]) + .then(this.proxy('read_value')); + } + return false; + }, - if (!checklist.length) { - if (!this.do_unfollow(user_pid)) { - $(event.target).attr("checked", "checked"); - } else { - self.$('.oe_subtype_list ul').empty(); - } - } else { - var context = new session.web.CompoundContext(this.build_context(), {}); - return this.ds_model.call(action_subscribe, [[this.view.datarecord.id], follower_ids, checklist, context]) - .then(this.proxy('read_value')); + do_update_subscription: function (event, user_pid) { + var self = this; + + var action_subscribe = 'message_subscribe_users'; + var follower_ids = [session.uid]; + var oe_action = this.$('.oe_actions input[type="checkbox"]'); + if (user_pid) { + action_subscribe = 'message_subscribe'; + follower_ids = [user_pid]; + oe_action = $('.oe_edit_actions input[type="checkbox"]'); + } + var checklist = []; + _(oe_action).each(function (record) { + if ($(record).is(':checked')) { + checklist.push(parseInt($(record).data('id'))); } - }, - }); -}; + }); + + if (!checklist.length) { + if (!this.do_unfollow(user_pid)) { + $(event.target).attr("checked", "checked"); + } else { + self.$('.oe_subtype_list ul').empty(); + } + } else { + var context = new data.CompoundContext(this.build_context(), {}); + return this.ds_model.call(action_subscribe, [[this.view.datarecord.id], follower_ids, checklist, context]) + .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_utils.js b/addons/mail/static/src/js/mail_utils.js new file mode 100644 index 00000000000..0634e240340 --- /dev/null +++ b/addons/mail/static/src/js/mail_utils.js @@ -0,0 +1,120 @@ +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]; +} + +/* Get an image in /web/binary/image?... */ +function get_image(session, model, field, id, resize) { + var r = resize ? encodeURIComponent(resize) : ''; + id = id || ''; + return session.url('/web/binary/image', {model: model, field: field, id: id, resize: r}); +} + +/* Get the url of an attachment {'id': id} */ +function get_attachment_url(session, message_id, attachment_id) { + return session.url('/mail/download_attachment', { + 'model': 'mail.message', + 'id': message_id, + 'method': 'download_attachment', + 'attachment_id': attachment_id + }); +} + +/** + * Replaces some expressions + * - :name - shortcut to an image + */ +function do_replace_expressions(string) { + var icon_list = ['al', 'pinky']; + /* special shortcut: :name, try to find an icon if in list */ + var regex_login = new RegExp(/(^|\s):((\w)*)/g); + var regex_res = regex_login.exec(string); + while (regex_res !== null) { + var icon_name = regex_res[2]; + if (_.include(icon_list, icon_name)) + string = string.replace(regex_res[0], regex_res[1] + '' + icon_name + ''); + regex_res = regex_login.exec(string); + } + return string; +} + +/** + * 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,'
'); +} + +/* Returns the complete domain with "&" + * TDE note: please add some comments to explain how/why + */ +function expand_domain(domain) { + var nb_and = -1; + var k; + // TDE note: smarted code maybe ? + for (k = domain.length-1; k >= 0 ; k-- ) { + if (typeof domain[k] != 'object' ) { + nb_and -= 2; + continue; + } + nb_and += 1; + } + + for (k = 0; k < nb_and ; k++) { + domain.unshift('&'); + } + + return domain; +} + +// inserts zero width space between each letter of a string so that +// the word will correctly wrap in html boxes smaller than the text +function breakword(str){ + var out = ''; + if (!str) { + return str; + } + for(var i = 0, len = str.length; i < len; i++){ + out += _.str.escapeHTML(str[i]) + '​'; + } + return out; +} + +return { + parse_email: parse_email, + get_image: get_image, + get_attachment_url: get_attachment_url, + do_replace_expressions: do_replace_expressions, + get_text2html: get_text2html, + expand_domain: expand_domain, + breakword: breakword, +}; + +}); diff --git a/addons/mail/static/src/js/many2many_tags_email.js b/addons/mail/static/src/js/many2many_tags_email.js index 0b2b33f4872..9a96e2ef78e 100644 --- a/addons/mail/static/src/js/many2many_tags_email.js +++ b/addons/mail/static/src/js/many2many_tags_email.js @@ -1,12 +1,19 @@ -openerp_FieldMany2ManyTagsEmail = function(instance) { -var _t = instance.web._t; +odoo.define('mail.many2manytags', ['web.core', 'web.form_common', 'web.form_relational', 'web.Model'], function (require) { +"use strict"; + +var core = require('web.core'); +var form_common = require('web.form_common'); +var form_relational = require('web.form_relational'); +var Model = require('web.Model'); + +var _t = core._t; /** * Extend of FieldMany2ManyTags widget method. * When the user add a partner and the partner don't have an email, open a popup to purpose to add an email. * The user can choose to add an email or cancel and close the popup. */ -instance.web.form.FieldMany2ManyTagsEmail = instance.web.form.FieldMany2ManyTags.extend({ +var FieldMany2ManyTagsEmail = form_relational.FieldMany2ManyTags.extend({ start: function() { this.values = []; @@ -41,7 +48,7 @@ instance.web.form.FieldMany2ManyTagsEmail = instance.web.form.FieldMany2ManyTags _check_email_popup: function (ids) { var self = this; - new instance.web.Model('res.partner').call("search", [[ + new Model('res.partner').call("search", [[ ["id", "in", ids], ["email", "=", false], ["notify_email", "=", 'always'] ]], @@ -54,7 +61,7 @@ instance.web.form.FieldMany2ManyTagsEmail = instance.web.form.FieldMany2ManyTags // unvalid partner _.each(record_ids, function (id) { - var pop = new instance.web.form.FormOpenPopup(self); + var pop = new form_common.FormOpenPopup(self); pop.show_element( 'res.partner', id, @@ -81,6 +88,6 @@ instance.web.form.FieldMany2ManyTagsEmail = instance.web.form.FieldMany2ManyTags /** * Registry of form fields */ -instance.web.form.widgets.add('many2many_tags_email', 'instance.web.form.FieldMany2ManyTagsEmail'); +core.form_widget_registry.add('many2many_tags_email', FieldMany2ManyTagsEmail); -}; +}); diff --git a/addons/mail/static/src/js/suggestions.js b/addons/mail/static/src/js/suggestions.js index 47a5bcf625c..b0ba8027fdb 100644 --- a/addons/mail/static/src/js/suggestions.js +++ b/addons/mail/static/src/js/suggestions.js @@ -1,78 +1,83 @@ -openerp.mail.suggestions = function(session, mail) { - var _t = session.web._t; - var QWeb = session.web.qweb; +odoo.define(['mail.mail', 'web.core', 'web.data', 'web.session', 'web.Widget'], function (require) { - var suggestions = session.suggestions = {}; - var removed_suggested_group = session.suggestions.removed_suggested_group = []; +var mail = require('mail.mail'); +var core = require('web.core'); +var data = require('web.data'); +var real_session = require('web.session'); +var Widget = require('web.Widget'); - suggestions.Groups = session.web.Widget.extend({ - events: { - 'click .oe_suggestion_remove.oe_suggestion_group': 'stop_group_suggestion', - 'click .oe_suggestion_remove_item.oe_suggestion_group': 'remove_group_suggestion', - 'click .oe_suggestion_join': 'join_group', - }, +var QWeb = core.qweb; - init: function () { - this._super.apply(this, arguments); - this.mail_group = new session.web.DataSetSearch(this, 'mail.group'); - this.res_users = new session.web.DataSetSearch(this, 'res.users'); - this.groups = []; - }, +var removed_suggested_group = []; - start: function () { - this._super.apply(this, arguments); - return this.fetch_suggested_groups(); - }, +var Groups = Widget.extend({ + events: { + 'click .oe_suggestion_remove.oe_suggestion_group': 'stop_group_suggestion', + 'click .oe_suggestion_remove_item.oe_suggestion_group': 'remove_group_suggestion', + 'click .oe_suggestion_join': 'join_group', + }, - fetch_suggested_groups: function () { - var self = this; - var group = self.mail_group.call('get_suggested_thread', {'removed_suggested_threads': removed_suggested_group}).then(function (res) { - _(res).each(function (result) { - result['image']=self.session.url('/web/binary/image', {model: 'mail.group', field: 'image_small', id: result.id}); - }); - self.groups = res; + init: function () { + this._super.apply(this, arguments); + this.mail_group = new data.DataSetSearch(this, 'mail.group'); + this.res_users = new data.DataSetSearch(this, 'res.users'); + this.groups = []; + }, + + start: function () { + this._super.apply(this, arguments); + return this.fetch_suggested_groups(); + }, + + fetch_suggested_groups: function () { + var self = this; + var group = self.mail_group.call('get_suggested_thread', {'removed_suggested_threads': removed_suggested_group}).then(function (res) { + _(res).each(function (result) { + result.image = real_session.url('/web/binary/image', {model: 'mail.group', field: 'image_small', id: result.id}); }); - return $.when(group).then(this.proxy('display_suggested_groups')); - }, + self.groups = res; + }); + return $.when(group).then(this.proxy('display_suggested_groups')); + }, - display_suggested_groups: function () { - var suggested_groups = this.$('.oe_sidebar_suggestion.oe_suggestion_group'); - if (suggested_groups) { - suggested_groups.empty(); - } - if (this.groups.length === 0) { - return this.$el.empty(); - } - return this.$el.empty().html(QWeb.render('mail.suggestions.groups', {'widget': this})); - }, - - join_group: function (event) { - var self = this; - var group_id = parseInt($(event.currentTarget).attr('id'), 10); - return this.mail_group.call('message_subscribe_users', [[group_id], [this.session.uid]]).then(function(res) { - self.fetch_suggested_groups(); - }); - }, - - remove_group_suggestion: function (event) { - removed_suggested_group.push($(event.currentTarget).attr('id')); - return this.fetch_suggested_groups(); - }, - - stop_group_suggestion: function (event) { - var self = this; - return this.res_users.call('stop_showing_groups_suggestions', [this.session.uid]).then(function(res) { - self.$(".oe_sidebar_suggestion.oe_suggestion_group").hide(); - }); + display_suggested_groups: function () { + var suggested_groups = this.$('.oe_sidebar_suggestion.oe_suggestion_group'); + if (suggested_groups) { + suggested_groups.empty(); } - }); + if (this.groups.length === 0) { + return this.$el.empty(); + } + return this.$el.empty().html(QWeb.render('mail.suggestions.groups', {'widget': this})); + }, - session.mail.WallSidebar.include({ - start: function () { - this._super.apply(this, arguments); - var sug_groups = new suggestions.Groups(this); - return sug_groups.appendTo(this.$('.oe_suggestions_groups')); - }, - }); + join_group: function (event) { + var self = this; + var group_id = parseInt($(event.currentTarget).attr('id'), 10); + return this.mail_group.call('message_subscribe_users', [[group_id], [this.session.uid]]).then(function(res) { + self.fetch_suggested_groups(); + }); + }, -}; + remove_group_suggestion: function (event) { + removed_suggested_group.push($(event.currentTarget).attr('id')); + return this.fetch_suggested_groups(); + }, + + stop_group_suggestion: function (event) { + var self = this; + return this.res_users.call('stop_showing_groups_suggestions', [this.session.uid]).then(function(res) { + self.$(".oe_sidebar_suggestion.oe_suggestion_group").hide(); + }); + } +}); + +mail.WallSidebar.include({ + start: function () { + this._super.apply(this, arguments); + var sug_groups = new Groups(this); + return sug_groups.appendTo(this.$('.oe_suggestions_groups')); + }, +}); + +}); diff --git a/addons/mail/views/mail.xml b/addons/mail/views/mail.xml index c2ba922ab2d..a8ddba4c93a 100644 --- a/addons/mail/views/mail.xml +++ b/addons/mail/views/mail.xml @@ -7,6 +7,7 @@ +