summaryrefslogtreecommitdiff
path: root/addons/mail/static/src/js/mail_chatter.js
diff options
context:
space:
mode:
authorJérome Maes <jem@openerp.com>2015-08-11 10:17:10 +0200
committerAaron Bohy <aab@odoo.com>2015-09-01 20:16:09 +0200
commit449f25463843767ba41a9a8c2adfdd4ceb2f2e5c (patch)
tree5a45357150f4b6f1d7977badc9c84843f64d8d11 /addons/mail/static/src/js/mail_chatter.js
parenta9a78ee9b6a499c29b284e1612951cc20249502f (diff)
[REF] mail : include real time, and replace Timeline features
This commit add instant messaging features in mail module to make it a real team collaboration tool. Lots of these new features will replace Timeleine view (which is not removed in this commit). Many files have been moved, split or created for a better structure. - My Channels : A user can be member of channel (mail.channel). These are discussion group, but also an aggregate of notification. Setting a channel as document follower, it will receive all the message (of the chatter) in real time, as information stream. - A new Inbox : this aims to replace the timeline view. The user will see its channels grouped by 'slot', and receive the message in real time. Jump to the form view, invite people, create private discussion group, talk directly to another employee, ... are the main features. Your message can still be starred. The 'Inbox' will only contain your needaction. - Needaction concept : a needaction is a message calling you to do something. If you follow a document as a person, the posted message will be 'needaction' for you. It can also be a message where you are mention with a '@my_name' (this feature is not added in this commit). - The chatter doesn't change so much. It still allow you to see the discussion about the document, to star some message, and treat your needaction. - Chat : the only way to have a minified conversation is from the Inbox Client Action. It allow you to keep an eye on a channel when working on document. - Compose Message : it is now common to the Inbox, and the chatter. It offers shortcode substitution, attachments management, and optional subtype. - UI : the chat window has been clean, they have a new look now. To do so, - Chatter code have been completely rewrite, independently of timeline view - Add 'bus' as mail dependency, to insure real time - A mail.message is broadcasted on the bus channel (db, mail.channel, channel_id), and a channel header on (db, res.partner, partner_id) - Extracting mail_thread mixin used for Chatter and ChatMailThread (Inbox client action). This uniformize the programming structure with the server side - Clean CSS style and use LESS instead of CSS - Define a new messafe format (message_format method in mail_message.py), compatible with fetching and broadcasting - Apply guideline coding conventions This commit also break the module im_livechat, since im_chat models don't exists anymore and will be fixed in the next commit. Thanks to Thibault Delavallee (tde) for the long debates and precious advices, Richard Mathot (rim) for the support, Simon Lejeune (sle) for the client action layout, and the Usability Team (apr, lle, bst yti) for the long testing. Conflicts: addons/mail/models/mail_thread.py
Diffstat (limited to 'addons/mail/static/src/js/mail_chatter.js')
-rw-r--r--addons/mail/static/src/js/mail_chatter.js471
1 files changed, 471 insertions, 0 deletions
diff --git a/addons/mail/static/src/js/mail_chatter.js b/addons/mail/static/src/js/mail_chatter.js
new file mode 100644
index 00000000000..7032f864eb4
--- /dev/null
+++ b/addons/mail/static/src/js/mail_chatter.js
@@ -0,0 +1,471 @@
+odoo.define('mail.chatter', function (require) {
+"use strict";
+
+var core = require('web.core');
+var data = require('web.data');
+var session = require('web.session');
+var web_client = require('web.web_client');
+var Model = require('web.Model');
+var Widget = require('web.Widget');
+var form_common = require('web.form_common');
+var SystrayMenu = require('web.SystrayMenu');
+var mail_utils = require('mail.utils');
+var mail_thread = require('mail.thread');
+
+var QWeb = core.qweb;
+var _t = core._t;
+var internal_bus = core.bus;
+
+
+/**
+ * Mail Compose Message for Chatter
+ *
+ * Extends the basic MailComposeMessage Widget to add 'suggested partner' layer (open popup
+ * when suggested partner is selected without email, or other informations), and the button
+ * to open the 'mail.compose.message' wizard.
+ **/
+var ChatterMailComposeMessage = mail_thread.MailComposeMessage.extend({
+ template: 'mail.chatter.ComposeMessage',
+ init: function(parent, dataset, options){
+ this._super.apply(this, arguments);
+ this.suggested_partners = [];
+ this.context = options.context || {};
+ // options
+ this.options = _.defaults(options, {
+ 'record_name': false,
+ 'is_log': false,
+ 'internal_subtypes': [],
+ });
+ },
+ willStart: function(){
+ if(this.options.is_log){
+ return this._super.apply(this, arguments);
+ }
+ return $.when(this._super.apply(this, arguments), this.message_get_suggested_recipients());
+ },
+ start: function(){
+ this.$input = this.$('.o_mail_compose_message_input');
+ this.$input.focus();
+ // add mail compose message full button
+ this.$('.o_mail_compose_message_button_group').append('<button class="btn btn-default o_mail_compose_message_button_full_message" type="button" data-toggle="popover"><i class="fa fa-pencil-square-o"/></button>')
+ this.$('.o_mail_compose_message_button_full_message').on('click', this, this.on_compose_message);
+ return this._super.apply(this, arguments);
+ },
+ message_get_suggested_recipients: function(){
+ var self = this;
+ var email_addresses = _.pluck(this.suggested_partners, 'email_address');
+ return this.thread_dataset.call('message_get_suggested_recipients', [[this.context.default_res_id], this.context]).done(function (suggested_recipients) {
+ var thread_recipients = suggested_recipients[self.context.default_res_id];
+ _.each(thread_recipients, function (recipient) {
+ var parsed_email = mail_utils.parse_email(recipient[1]);
+ if (_.indexOf(email_addresses, parsed_email[1]) == -1) {
+ self.suggested_partners.push({
+ 'checked': true,
+ 'partner_id': recipient[0],
+ 'full_name': recipient[1],
+ 'name': parsed_email[0],
+ 'email_address': parsed_email[1],
+ 'reason': recipient[2],
+ });
+ }
+ });
+ });
+ },
+ /**
+ * Get the list of selected suggested partner
+ * @returns Array() : list of 'recipient' selected partner (may not be created in db)
+ **/
+ get_checked_suggested_partners: function(){
+ var self = this;
+ var checked_partners = [];
+ this.$('.o_mail_chatter_compose_message_suggested_partners input:checked').each(function(index){
+ var $input = $(this);
+ var full_name = $input.data('fullname');
+ checked_partners = checked_partners.concat(_.filter(self.suggested_partners, function(item){
+ return full_name === item.full_name;
+ }));
+ });
+ return checked_partners;
+ },
+ /**
+ * Check the additionnal partners (not necessary registered partners), and open a popup form view
+ * for the ones who informations is missing.
+ * @param Array : list of 'recipient' partners to complete informations or validate
+ * @returns Deferred resolved with the list of checked suggested partners (real partner)
+ **/
+ check_suggested_partners: function (checked_suggested_partners) {
+ var self = this;
+ var check_done = $.Deferred();
+
+ var recipients = _.filter(checked_suggested_partners, function (recipient) { return recipient.checked; });
+ var recipients_to_find = _.filter(recipients, function (recipient) { return (! recipient.partner_id); });
+ var names_to_find = _.pluck(recipients_to_find, 'full_name');
+ var recipients_to_check = _.filter(recipients, function (recipient) { return (recipient.partner_id && ! recipient.email_address); });
+ var recipient_ids = _.pluck(_.filter(recipients, function (recipient) { return recipient.partner_id && recipient.email_address; }), 'partner_id');
+
+ var names_to_remove = [];
+ var recipient_ids_to_remove = [];
+
+ // have unknown names -> call message_get_partner_info_from_emails to try to find partner_id
+ var find_done = $.Deferred();
+ if (names_to_find.length > 0) {
+ find_done = self.thread_dataset.call('message_partner_info_from_emails', [[this.context.default_res_id], names_to_find]);
+ } else {
+ find_done.resolve([]);
+ }
+
+ // for unknown names + incomplete partners -> open popup - cancel = remove from recipients
+ $.when(find_done).pipe(function (result) {
+ var emails_deferred = [];
+ var recipient_popups = result.concat(recipients_to_check);
+
+ _.each(recipient_popups, function (partner_info) {
+ var deferred = $.Deferred();
+ emails_deferred.push(deferred);
+
+ var partner_name = partner_info.full_name;
+ var partner_id = partner_info.partner_id;
+ var parsed_email = mail_utils.parse_email(partner_name);
+
+ var pop = new form_common.FormViewDialog(this, {
+ res_model: 'res.partner',
+ res_id: partner_id,
+ context: {
+ 'force_email': true,
+ 'ref': "compound_context",
+ 'default_name': parsed_email[0],
+ 'default_email': parsed_email[1],
+ },
+ title: _t("Please complete partner's informations"),
+ }).open();
+ pop.on('closed', self, function () {
+ deferred.resolve();
+ });
+ pop.view_form.on('on_button_cancel', self, function () {
+ names_to_remove.push(partner_name);
+ if (partner_id) {
+ recipient_ids_to_remove.push(partner_id);
+ }
+ });
+ });
+ $.when.apply($, emails_deferred).then(function () {
+ var new_names_to_find = _.difference(names_to_find, names_to_remove);
+ find_done = $.Deferred();
+ if (new_names_to_find.length > 0) {
+ find_done = self.thread_dataset.call('message_partner_info_from_emails', [[self.context.default_res_id], new_names_to_find, true]);
+ } else {
+ find_done.resolve([]);
+ }
+ $.when(find_done).pipe(function (result) {
+ var recipient_popups = result.concat(recipients_to_check);
+ _.each(recipient_popups, function (partner_info) {
+ if (partner_info.partner_id && _.indexOf(partner_info.partner_id, recipient_ids_to_remove) == -1) {
+ recipient_ids.push(partner_info.partner_id);
+ }
+ });
+ }).pipe(function () {
+ check_done.resolve(recipient_ids);
+ });
+ });
+ });
+ return check_done;
+ },
+ message_post: function(body, attachment_ids, values){
+ var self = this;
+ var _super = self._super;
+ // default values
+ values = _.defaults(values || {}, {
+ 'message_type': 'comment',
+ 'content_subtype': 'html',
+ 'partner_ids': [],
+ 'subtype_id': false,
+ 'subtype': false,
+ });
+ // update subtype
+ if(this.options.is_log) {
+ var subtype_id = parseInt(this.$('.o_mail_chatter_compose_message_subtype_select').val());
+ if(_.indexOf(_.pluck(this.internal_subtypes, 'id'), subtype_id) == -1) {
+ values['subtype'] = 'mail.mt_note'
+ }else{
+ values['subtype_id'] = subtype_id;
+ }
+ }else{
+ values['subtype'] = 'mail.mt_comment';
+ }
+ // update partner_ids
+ var def = $.when();
+ if(this.options.is_log){
+ // directly call the super()
+ def = _.bind(_super, self)(body, attachment_ids, values);
+ }else{
+ // call the super() when all partner popup are treated
+ var checked_suggested_partners = this.get_checked_suggested_partners();
+ this.check_suggested_partners(checked_suggested_partners).done(function(partner_ids){
+ // update context
+ values['partner_ids'] = partner_ids;
+ values['context'] = _.defaults(self.context, {
+ 'mail_post_autofollow': true,
+ 'mail_post_autofollow_partner_ids': partner_ids,
+ });
+ def = _.bind(_super, self)(body, attachment_ids, values);
+ });
+ }
+ return def
+ },
+ is_empty: function(){
+ return this.$input.val() === '';
+ },
+ on_compose_message: function(event){
+ var self = this;
+ if (!this.do_check_attachment_upload()){
+ return false;
+ }
+
+ var recipient_done = $.Deferred();
+ if (this.options.is_log){
+ recipient_done.resolve([]);
+ }else{
+ var checked_suggested_partners = this.get_checked_suggested_partners();
+ recipient_done = this.check_suggested_partners(checked_suggested_partners);
+ }
+ recipient_done.then(function(partner_ids){
+ var context = {
+ 'default_parent_id': self.id,
+ 'default_body': mail_utils.get_text2html(self.$('.o_mail_compose_message_input').val()),
+ 'default_attachment_ids': _.pluck(self.get('attachment_ids'), 'id'),
+ 'default_partner_ids': partner_ids,
+ 'default_is_log': self.options.is_log,
+ 'mail_post_autofollow': true,
+ 'mail_post_autofollow_partner_ids': partner_ids,
+ };
+
+ if (self.context.default_model && self.context.default_res_id) {
+ context.default_model = self.context.default_model;
+ context.default_res_id = self.context.default_res_id;
+ }
+
+ self.do_action({
+ type: 'ir.actions.act_window',
+ res_model: 'mail.compose.message',
+ view_mode: 'form',
+ view_type: 'form',
+ views: [[false, 'form']],
+ target: 'new',
+ context: context,
+ }, {
+ on_close: function() {
+ self.trigger('need_refresh');
+ },
+ });
+ });
+ }
+});
+
+
+/**
+ * Document Chatter ('mail_thread' widget)
+ *
+ * Displayed on every document. Since it is displayed on a form view, it extends 'AbstractField' widget, and
+ * required the behaviour of 'MailThreadMixin' to manage mail.message (using common methods).
+ * This widget is responsible of the toggle the message composer and the custom message rendering.
+ **/
+var ChatterMailThread = form_common.AbstractField.extend(mail_thread.MailThreadMixin, {
+ template: 'mail.chatter.ChatterMailThread',
+ events: {
+ "click .o_mail_redirect": "on_click_redirect",
+ "click .o_mail_thread_message_star": "on_message_star",
+ "click .o_mail_thread_message_needaction": "on_message_needaction",
+ "click .o_mail_chatter_show_more_message": "on_message_show_more",
+ // toggle message composer (!! declaration order is important !!)
+ "click .o_mail_chatter_button_new_message": "on_open_composer",
+ "click .o_mail_chatter_button_log_note": "on_open_composer",
+ "blur .o_mail_compose_message_input": "on_close_composer",
+ "mousedown .o_mail_chatter_compose_message": function(event) {
+ this.stay_open = true;
+ },
+ "focus .o_mail_compose_message_input": function(event) {
+ this.stay_open = false;
+ },
+ "keydown .o_mail_compose_message_input": function(event) {
+ this.stay_open = false;
+ },
+ },
+ init: function (parent, node){
+ this._super.apply(this, arguments);
+ mail_thread.MailThreadMixin.init.call(this);
+ this.message_composer = undefined;
+ this.record_name = '';
+ this.context = this.options.context || {};
+ // options from 'message_ids' node in field_view_get
+ this.options = _.defaults(this.options, {
+ 'display_log_button': true,
+ 'display_document_link': false,
+ 'internal_subtypes': [],
+ 'emoji_list': [],
+ });
+ this.emoji_set_substitution(this.options.emoji_list);
+ },
+ start: function(){
+ mail_thread.MailThreadMixin.start.call(this);
+ return this._super.apply(this, arguments);
+ },
+ on_message_show_more: function(event){
+ var self = this;
+ this.message_load_history().then(function(messages){
+ if(messages.length < mail_thread.LIMIT_MESSAGE){
+ self.$('.o_mail_chatter_show_more_message').hide();
+ }
+ });
+ },
+ /**
+ * When a message is correctly posted, fetch its data to render it
+ * @param {Number} message_id : the identifier of the new posted message
+ * @returns {Deferred}
+ */
+ on_message_post: function(message_id){
+ var self = this;
+ return this.message_format([message_id]).then(function(messages){
+ self.stay_open = false;
+ self.message_insert(messages);
+ self.on_close_composer();
+ });
+ },
+ /**
+ * The value of the field has change (modification or switch to next document). Form view is
+ * not re-rendered, simply updated. A re-fetch is needed.
+ * @override
+ */
+ render_value: function(){
+ var self = this;
+ // re-rendering for mail composer message
+ this.options.display_show_more_message = this.get_value().length > mail_thread.LIMIT_MESSAGE;
+ this.renderElement();
+ // reset dataset
+ this.ThreadDataset = this.view.dataset;
+ // update context (require for mail.compose.message wizard)
+ this.context = _.extend({
+ 'mail_read_set_read': true, // set messages as read in Chatter TODO JEM : not used anymore
+ 'default_res_id': this.view.datarecord.id || false,
+ 'default_model': this.view.model || false,
+ }, this.options.context || {});
+ // fetch messages of current document
+ this.message_format(this.get_value().slice(0, mail_thread.LIMIT_MESSAGE)).then(function(raw_messages){
+ self._message_replace(self._message_preprocess(raw_messages));
+ self.record_name = _.last(raw_messages) ? _.last(raw_messages).record_name : '';
+ // set the thread as read
+ self.ThreadDataset.call('message_set_read', [[self.view.datarecord.id]]).then(function(message_ids){
+ // decrement the needaction top counter
+ internal_bus.trigger('mail_needaction_done', message_ids.length);
+ });
+ });
+ },
+ // composer toggle
+ on_open_composer: function(event){
+ var self = this;
+ this.$('.o_mail_chatter_compose_buttons').hide();
+ this.$('.o_mail_chatter_compose_message_wrap').show();
+ // destroy existing composer
+ if(this.message_composer){
+ this.message_composer.destroy();
+ }
+ // create the new composer
+ this.message_composer = new ChatterMailComposeMessage(this, this.ThreadDataset, {
+ 'internal_subtypes': this.options.internal_subtypes,
+ 'is_log': this.$(event.currentTarget).hasClass('o_mail_chatter_button_log_note'),
+ 'emoji_list': this.options.emoji_list,
+ 'context': this.context,
+ 'record_name': this.record_name,
+ });
+ this.message_composer.appendTo(this.$('.o_mail_chatter_compose_message_wrap'));
+ this.message_composer.on('message_sent', this, this.on_message_post);
+ this.message_composer.on('need_refresh', this, function(e){
+ self.message_refresh();
+ self.on_close_composer();
+ });
+ },
+ on_close_composer: function(event){
+ if(this.message_composer && this.message_composer.is_empty() && !this.stay_open){
+ this.$('.o_mail_chatter_compose_buttons').show();
+ this.$('.o_mail_chatter_compose_message_wrap').hide();
+ }
+ },
+ /**
+ * Refresh the current message, and set them as current message. Use 'message_fetch', since its
+ * domain don't use 'ids', so if there is new messages, they'll be fetched.
+ * @returns {Deferred}
+ */
+ message_refresh: function(){
+ return this.message_load_new();
+ },
+ /**
+ * Render the messages
+ * @override
+ */
+ message_render: function(){
+ this.$('.o_mail_chatter_messages').html(QWeb.render('mail.chatter.ChatterMailThread.messages', {'widget': this}));
+ },
+ /**
+ * Order the messages
+ * @param {Object[]} messages : messages to order
+ * @override
+ */
+ _message_order: function(messages){
+ var messages = mail_thread.MailThreadMixin._message_order.call(this, messages);
+ return messages.reverse();
+ },
+ /**
+ * Return the message domain for the current document mail thread
+ * @override
+ */
+ get_message_domain: function(){
+ var res_id = this.ThreadDataset.ids[this.ThreadDataset.index];
+ return [['model', '=', this.ThreadDataset.model], ['res_id', '=', res_id]];
+ }
+});
+
+core.form_widget_registry.add('mail_thread', ChatterMailThread);
+
+
+
+/**
+ * Global ComposeMessage Top Button
+ *
+ * Add a link on the top user bar to write a full mail. It opens the form view
+ * of the mail.compose.message (in a modal).
+ */
+var ComposeMessageTopButton = Widget.extend({
+ template:'mail.ComposeMessageTopButton',
+ events: {
+ "click": "on_compose_message",
+ },
+ on_compose_message: function (ev) {
+ ev.preventDefault();
+ var ctx = {};
+ if ($('button.o_timeline_compose_post') && $('button.o_timeline_compose_post').is(":visible") == true &&
+ (this.getParent()).getParent().action_manager.inner_widget.active_view.type == 'form'){
+ ctx = {
+ 'default_res_id': (this.getParent()).getParent().action_manager.inner_widget.active_view.controller.datarecord.id,
+ 'default_model': (this.getParent()).getParent().action_manager.inner_widget.active_view.controller.model,
+ };
+ }
+ web_client.action_manager.do_action({
+ type: 'ir.actions.act_window',
+ res_model: 'mail.compose.message',
+ view_mode: 'form',
+ view_type: 'form',
+ views: [[false, 'form']],
+ target: 'new',
+ context: ctx,
+ });
+ },
+});
+
+// Put the ComposeMessageTopButton widget in the systray menu
+SystrayMenu.Items.push(ComposeMessageTopButton);
+
+
+return {
+ ChatterMailThread: ChatterMailThread,
+};
+
+});