From dc81e5f66433be9a5610a969e972f2bb74e6c2a1 Mon Sep 17 00:00:00 2001 From: Aaron Bohy Date: Thu, 11 Feb 2016 10:43:44 +0100 Subject: [PATCH] [REF] mail: code refactoring - extended_chat_window feature in its own files: as it is only available in the backend, we don't want it in the frontend assets (which contain the 'basic' chat_window) - move followers feature in its own files - creation of the 'mail.utils' module in utils.js - creation of systray.less to define the style of the mail item and dropdown in the navbar - use of chat_manager's search_partner function in PartnerInviteDialog - some code linting --- addons/mail/__openerp__.py | 2 + addons/mail/static/src/js/chat_manager.js | 89 +--- addons/mail/static/src/js/chatter.js | 434 +----------------- addons/mail/static/src/js/client_action.js | 7 +- addons/mail/static/src/js/composer.js | 25 +- .../static/src/js/extended_chat_window.js | 30 ++ addons/mail/static/src/js/followers.js | 411 +++++++++++++++++ addons/mail/static/src/js/systray.js | 174 +++---- addons/mail/static/src/js/utils.js | 137 ++++++ addons/mail/static/src/js/window_manager.js | 31 -- addons/mail/static/src/less/chat_window.less | 21 - addons/mail/static/src/less/chatter.less | 91 ---- .../mail/static/src/less/client_action.less | 122 +---- .../static/src/less/extended_chat_window.less | 19 + addons/mail/static/src/less/followers.less | 90 ++++ addons/mail/static/src/less/systray.less | 119 +++++ addons/mail/static/src/xml/chat_window.xml | 10 - addons/mail/static/src/xml/chatter.xml | 89 ---- .../static/src/xml/extended_chat_window.xml | 13 + addons/mail/static/src/xml/followers.xml | 90 ++++ addons/mail/views/mail_templates.xml | 6 + 21 files changed, 1021 insertions(+), 989 deletions(-) create mode 100644 addons/mail/static/src/js/extended_chat_window.js create mode 100644 addons/mail/static/src/js/followers.js create mode 100644 addons/mail/static/src/js/utils.js create mode 100644 addons/mail/static/src/less/extended_chat_window.less create mode 100644 addons/mail/static/src/less/followers.less create mode 100644 addons/mail/static/src/less/systray.less create mode 100644 addons/mail/static/src/xml/extended_chat_window.xml create mode 100644 addons/mail/static/src/xml/followers.xml diff --git a/addons/mail/__openerp__.py b/addons/mail/__openerp__.py index 8af27a4f011..ea32babb318 100644 --- a/addons/mail/__openerp__.py +++ b/addons/mail/__openerp__.py @@ -61,9 +61,11 @@ Main Features 'static/src/xml/client_action.xml', 'static/src/xml/composer.xml', 'static/src/xml/chatter.xml', + 'static/src/xml/followers.xml', 'static/src/xml/systray.xml', 'static/src/xml/thread.xml', 'static/src/xml/chat_window.xml', + 'static/src/xml/extended_chat_window.xml', 'static/src/xml/announcement.xml', ], } diff --git a/addons/mail/static/src/js/chat_manager.js b/addons/mail/static/src/js/chat_manager.js index fcc7cffd5d1..856b4def3fd 100644 --- a/addons/mail/static/src/js/chat_manager.js +++ b/addons/mail/static/src/js/chat_manager.js @@ -2,6 +2,7 @@ odoo.define('mail.chat_manager', function (require) { "use strict"; var bus = require('bus.bus').bus; +var utils = require('mail.utils'); var config = require('web.config'); var core = require('web.core'); var data = require('web.data'); @@ -36,36 +37,13 @@ var global_unread_counter = 0; var pinned_dm_partners = []; // partner_ids we have a pinned DM with var client_action_open = false; -// Utils: Window focus/unfocus, beep, tab title, parsing html strings +// Global unread counter and notifications //---------------------------------------------------------------------------------- -var beep = (function () { - if (typeof(Audio) === "undefined") { - return function () {}; - } - var audio = new Audio(); - var ext = audio.canPlayType("audio/ogg; codecs=vorbis") ? ".ogg" : ".mp3"; - audio.src = session.url("/mail/static/src/audio/ting" + ext); - return function () { audio.play(); }; -})(); - bus.on("window_focus", null, function() { global_unread_counter = 0; web_client.set_title_part("_chat"); }); -// to do: move this to mail.utils -function send_native_notification(title, content) { - var notification = new Notification(title, {body: content, icon: "/mail/static/src/img/odoo_o.png"}); - notification.onclick = function (e) { - window.focus(); - if (this.cancel) { - this.cancel(); - } else if (this.close) { - this.close(); - } - }; -} - function notify_incoming_message (msg, options) { if (bus.is_odoo_focused() && options.is_displayed) { // no need to notify @@ -75,7 +53,7 @@ function notify_incoming_message (msg, options) { if (msg.author_id[1]) { title = _.escape(msg.author_id[1]); } - var content = parse_and_transform(msg.body, strip_html).substr(0, preview_msg_max_size); + var content = utils.parse_and_transform(msg.body, utils.strip_html).substr(0, preview_msg_max_size); if (!bus.is_odoo_focused()) { global_unread_counter++; @@ -83,60 +61,7 @@ function notify_incoming_message (msg, options) { web_client.set_title_part("_chat", tab_title); } - if (Notification && Notification.permission === "granted") { - if (bus.is_master) { - send_native_notification(title, content); - } - } else { - web_client.do_notify(title, content); - if (bus.is_master) { - beep(); - } - } -} - -function parse_and_transform(html_string, transform_function) { - var open_token = "OPEN" + Date.now(); - var string = html_string.replace(/</g, open_token); - var children = $('
').html(string).contents(); - return _parse_and_transform(children, transform_function) - .replace(new RegExp(open_token, "g"), "<"); -} - -function _parse_and_transform(nodes, transform_function) { - return _.map(nodes, function (node) { - return transform_function(node, function () { - return _parse_and_transform(node.childNodes, transform_function); - }); - }).join(""); -} - -// suggested regexp (gruber url matching regexp, adapted to js, see https://gist.github.com/gruber/8891611) -var url_regexp = /\b((?:https?:\/\/|www\d{0,3}[.]|[a-z0-9.\-]+[.][a-z]{2,4}\/)(?:[^\s()<>]+|\(([^\s()<>]+|(\([^\s()<>]+\)))*\))+(?:\(([^\s()<>]+|(\([^\s()<>]+\)))*\)|[^\s`!()\[\]{};:'".,<>?«»“”‘’]))/gi; -function add_link (node, transform_children) { - if (node.nodeType === 3) { // text node - return node.data.replace(url_regexp, function (url) { - var href = (!/^(f|ht)tps?:\/\//i.test(url)) ? "http://" + url : url; - return '' + url + ''; - }); - } - if (node.tagName === "A") return node.outerHTML; - node.innerHTML = transform_children(); - return node.outerHTML; -} - -function strip_html (node, transform_children) { - if (node.nodeType === 3) return node.data; // text node - if (node.tagName === "BR") return "\n"; - return transform_children(); -} - -function inline (node, transform_children) { - if (node.nodeType === 3) return node.data; - if (node.tagName === "BR") return " "; - if (node.tagName.match(/^(A|P|DIV|PRE|BLOCKQUOTE)$/)) return transform_children(); - node.innerHTML = transform_children(); - return node.outerHTML; + utils.send_notification(title, content); } // Message and channel manipulation helpers @@ -274,7 +199,7 @@ function make_message (data) { } // add anchor tags to urls - msg.body = parse_and_transform(msg.body, add_link); + msg.body = utils.parse_and_transform(msg.body, utils.add_link); // Compute url of attachments _.each(msg.attachment_ids, function(a) { @@ -1000,7 +925,7 @@ var chat_manager = { }); }, get_message_body_preview: function (message_body) { - return parse_and_transform(message_body, inline); + return utils.parse_and_transform(message_body, utils.inline); }, search_partner: function (search_val, limit) { @@ -1016,8 +941,6 @@ var chat_manager = { return values; }); }, - - send_native_notification: send_native_notification, }; chat_manager.bus.on('client_action_open', null, function (open) { diff --git a/addons/mail/static/src/js/chatter.js b/addons/mail/static/src/js/chatter.js index c9dbe66863c..6fc6e09b1ed 100644 --- a/addons/mail/static/src/js/chatter.js +++ b/addons/mail/static/src/js/chatter.js @@ -4,411 +4,13 @@ odoo.define('mail.Chatter', function (require) { var chat_manager = require('mail.chat_manager'); var composer = require('mail.composer'); var ChatThread = require('mail.ChatThread'); +var utils = require('mail.utils'); -var ajax = require('web.ajax'); var config = require('web.config'); 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 _t = core._t; -var qweb = core.qweb; - -// ----------------------------------------------------------------------------- -// Followers Widget ('mail_followers' widget) -// -// Since it is displayed on a form view, it extends 'AbstractField' widget. -// -// Note: the followers widget is moved inside the chatter by the chatter itself -// for layout purposes. -// ----------------------------------------------------------------------------- -var Followers = form_common.AbstractField.extend({ - template: 'mail.Followers', - - init: function () { - this._super.apply(this, arguments); - - this.image = this.node.attrs.image || 'image_small'; - this.comment = this.node.attrs.help || false; - this.ds_model = new data.DataSetSearch(this, this.view.model); - - this.value = []; - this.followers = []; - this.followers_fetched = $.Deferred(); - this.data_subtype = {}; - - this.view_is_editable = this.__parentedParent.is_action_enabled('edit'); - }, - - start: function () { - // use actual_mode property on view to know if the view is in create mode anymore - this.view.on("change:actual_mode", this, this.on_check_visibility_mode); - this.on_check_visibility_mode(); - this.reinit(); - this.bind_events(); - return this._super(); - }, - - on_check_visibility_mode: function () { - this.set({"force_invisible": this.view.get("actual_mode") === "create"}); - }, - - set_value: function (_value) { - this.value = _value; - this._super(_value); - }, - - reinit: function () { - this.data_subtype = {}; - this.message_is_follower = undefined; - this.display_buttons(); - }, - - bind_events: function () { - var self = this; - - // event: click on '(Un)Follow' button, that toggles the follow for uid - this.$el.on('click', '.o_followers_follow_button', function () { - if ($(this).hasClass('o_followers_notfollow')) { - self.do_follow(); - } else { - self.do_unfollow({user_ids: [session.uid]}); - } - }); - - // event: click on a subtype, that (un)subscribe for this subtype - this.$el.on('click', '.o_subtypes_list input', function(event) { - event.stopPropagation(); - self.do_update_subscription(event); - var $list = self.$('.o_subtypes_list'); - if (!$list.hasClass('open')) { - $list.addClass('open'); - } - if (self.$('.o_subtypes_list ul')[0].children.length < 1) { - $list.removeClass('open'); - } - }); - - // event: click on 'invite' button, that opens the invite wizard - this.$el.on('click', '.o_add_follower', function(event) { - event.preventDefault(); - self.on_invite_follower(false); - }); - this.$el.on('click', '.o_add_follower_channel', function(event) { - event.preventDefault(); - self.on_invite_follower(true); - }); - - // event: click on 'edit_subtype(pencil)' button to edit subscription - this.$el.on('click', '.o_edit_subtype', self.on_edit_subtype); - this.$el.on('click', '.o_remove_follower', self.on_remove_follower); - this.$el.on('click', '.o_mail_redirect', self.on_click_redirect); - }, - - on_edit_subtype: function (event) { - var self = this; - var $currentTarget = $(event.currentTarget); - var follower_id = $currentTarget.data('follower-id'); // id of model mail_follower - var res_id = $currentTarget.data('oe-id'); // id of model res_partner or mail_channel - var is_channel = $currentTarget.data('oe-model') === 'mail.channel'; - self.dialog = new Dialog(this, { - size: 'medium', - title: _t('Edit Subscription of ') + $currentTarget.siblings('a').text(), - buttons: [ - { - text: _t("Apply"), - classes: 'btn-primary', - click: function () { - self.do_update_subscription(event, res_id, is_channel); - }, - close: true - }, - { - text: _t("Cancel"), - close: true, - }, - ], - }).open(); - return self.fetch_subtypes(follower_id).then(function (data) { - self.display_subtypes(data, true, is_channel); - }); - }, - - on_invite_follower: function (channel_only) { - var self = this; - var action = { - type: 'ir.actions.act_window', - res_model: 'mail.wizard.invite', - view_mode: 'form', - view_type: 'form', - views: [[false, 'form']], - name: _t('Invite Follower'), - target: 'new', - context: { - 'default_res_model': this.view.dataset.model, - 'default_res_id': this.view.datarecord.id, - 'mail_invite_follower_channel_only': channel_only, - }, - }; - this.do_action(action, { - on_close: function () { - self.read_value(); - }, - }); - }, - - on_remove_follower: function (event) { - var res_model = $(event.target).parent().find('a').data('oe-model'); - var res_id = $(event.target).parent().find('a').data('oe-id'); - if (res_model === 'res.partner') { - return this.do_unfollow({partner_ids: [res_id]}); - } else { - return this.do_unfollow({channel_ids: [res_id]}); - } - }, - - on_click_redirect: function (event) { - event.preventDefault(); - var res_id = $(event.target).data('oe-id'); - var res_model = $(event.target).data('oe-model'); - this.trigger('redirect', res_model, res_id); - }, - - read_value: function () { - var self = this; - return this.ds_model.read_ids([this.view.datarecord.id], ['message_follower_ids']) - .then(function (results) { - self.value = results[0].message_follower_ids; - self.render_value(); - }); - }, - - render_value: function () { - var self = this; - this.reinit(); - this.value = this.value || []; - return this.fetch_followers().then(function (results) { - self.display_followers(results.followers); - if (results.subtypes) { // current user is follower - self.display_subtypes(results.subtypes); - } - self.display_buttons(); - }).fail(this.display_generic.bind(this)); - }, - - fetch_followers: function () { - return ajax.jsonRpc('/mail/read_followers', 'call', { - follower_ids: this.value, - res_model: this.view.model, - res_id: this.view.datarecord.id, - }); - }, - - /** Fetch subtypes of the given follower - * @param {int} [follower_id] the id of the follower - * @param {string} [follower_model] 'res.partner' or 'mail.channel' - */ - fetch_subtypes: function (follower_id) { - return ajax.jsonRpc('/mail/read_subscription_data', 'call', { - res_model: this.view.model, - follower_id: follower_id, - }); - }, - - _format_followers: function (count){ - var str = ''; - if(count <= 0){ - str = _t('No follower'); - }else if(count === 1){ - str = _t('One follower'); - }else{ - str = ''+count+' '+_t('followers'); - } - return str; - }, - - /** Read on res.partner failed: only display the number of followers */ - display_generic: function () { - this.$('.o_followers_actions').hide(); - this.$('.o_followers_list').hide(); - this.$('.o_followers_title_box > button').prop('disabled', true); - this.$('.o_followers_count').html(this._format_followers(this.value.length)); - }, - - /** Display the followers */ - display_followers: function (records) { - var self = this; - this.followers = records || this.followers; - this.trigger('followers_update', this.followers); - - // clean and display title - var $followers_list = this.$('.o_followers_list').empty(); - this.$('.o_followers_count').html(this._format_followers(this.followers.length)); - var user_follower = _.filter(this.followers, function (rec) { return rec.is_uid;}); - this.message_is_follower = user_follower.length >= 1; - this.follower_id = this.message_is_follower ? user_follower[0].id : undefined; - - // render the dropdown content - $(qweb.render('mail.Followers.add_more', {'widget': self})).appendTo($followers_list); - var $follower_li; - _(this.followers).each(function (record) { - $follower_li = $(qweb.render('mail.Followers.partner', { - 'record': _.extend(record, {'avatar_url': '/web/image/' + record.res_model + '/' + record.res_id + '/image_small'}), - 'widget': self}) - ); - $follower_li.appendTo($followers_list); - - // On mouse-enter it will show the edit_subtype pencil. - if (record.is_editable) { - $follower_li.on('mouseenter mouseleave', function(e) { - $(e.currentTarget).find('.o_edit_subtype').toggleClass('hide', e.type === 'mouseleave'); - }); - } - }); - }, - - display_buttons: function () { - if (this.message_is_follower) { - this.$('button.o_followers_follow_button').removeClass('o_followers_notfollow').addClass('o_followers_following'); - this.$('.o_subtypes_list > .dropdown-toggle').attr('disabled', false); - } else { - this.$('button.o_followers_follow_button').removeClass('o_followers_following').addClass('o_followers_notfollow'); - this.$('.o_subtypes_list > .dropdown-toggle').attr('disabled', true); - } - }, - - /** Display subtypes: {'name': default, followed} */ - display_subtypes:function (data, dialog, display_warning) { - var old_parent_model; - var $list; - if (dialog) { - $list = $('