odoo.define('mail.Chatter', function (require) {
"use strict";
var Activity = require('mail.Activity');
var AttachmentBox = require('mail.AttachmentBox');
var ChatterComposer = require('mail.composer.Chatter');
var Dialog = require('web.Dialog');
var Followers = require('mail.Followers');
var ThreadField = require('mail.ThreadField');
var mailUtils = require('mail.utils');
var concurrency = require('web.concurrency');
var config = require('web.config');
var core = require('web.core');
var Widget = require('web.Widget');
var _t = core._t;
var QWeb = core.qweb;
// The purpose of this widget is to display the chatter area below the form view
//
// It instantiates the optional mail_thread, mail_activity and mail_followers widgets.
// It Ensures that those widgets are appended at the right place, and allows them to communicate
// with each other.
// It synchronizes the rendering of those widgets (as they may be asynchronous), to mitigate
// the flickering when switching between records
var Chatter = Widget.extend({
template: 'mail.Chatter',
custom_events: {
delete_attachment: '_onDeleteAttachment',
discard_record_changes: '_onDiscardRecordChanges',
reload_attachment_box: '_onReloadAttachmentBox',
reload_mail_fields: '_onReloadMailFields',
},
events: {
'click .o_chatter_button_new_message': '_onOpenComposerMessage',
'click .o_chatter_button_log_note': '_onOpenComposerNote',
'click .o_chatter_button_attachment': '_onClickAttachmentButton',
'click .o_chatter_button_schedule_activity': '_onScheduleActivity',
},
supportedFieldTypes: ['one2many'],
/**
* @override
* @param {widget} parent
* @param {Object} record
* @param {Object} mailFields
* @param {string} [mailFields.mail_activity]
* @param {string} [mailFields.mail_followers]
* @param {string} [mailFields.mail_thread]
* @param {Object} options
* @param {string} [options.viewType=record.viewType] current viewType in
* which the chatter is instantiated
*/
init: function (parent, record, mailFields, options) {
this._super.apply(this, arguments);
this._setState(record);
this.attachments = {};
this.fields = {};
this._areAttachmentsLoaded = false;
this._disableAttachmentBox = !!options.disable_attachment_box;
this._dp = new concurrency.DropPrevious();
this._isAttachmentBoxOpen = false;
// mention: get the prefetched partners and use them as mention suggestions
// if there is a follower widget, the followers will be added to the
// suggestions as well once fetched
this._mentionPartnerSuggestions = this.call('mail_service', 'getMentionPartnerSuggestions');
this._mentionSuggestions = this._mentionPartnerSuggestions;
if (mailFields.mail_activity) {
this.fields.activity = new Activity(this, mailFields.mail_activity, record, options);
}
if (mailFields.mail_followers) {
this.fields.followers = new Followers(this, mailFields.mail_followers, record, options);
}
if (mailFields.mail_thread) {
this.fields.thread = new ThreadField(this, mailFields.mail_thread, record, options);
var fieldsInfo = record.fieldsInfo[options.viewType || record.viewType];
var nodeOptions = fieldsInfo[mailFields.mail_thread].options || {};
this.hasLogButton = options.display_log_button || nodeOptions.display_log_button;
this.postRefresh = nodeOptions.post_refresh || 'never';
this.reloadOnUploadAttachment = this.postRefresh === 'always';
}
},
/**
* @override
*/
start: function () {
this._$topbar = this.$('.o_chatter_topbar');
if(!this._disableAttachmentBox) {
this.$('.o_topbar_right_area').append(QWeb.render('mail.chatter.Attachment.Button', {
displayCounter: !!this.fields.thread,
count: this.record.data.message_attachment_count || 0,
}));
}
// render and append the buttons
this._$topbar.prepend(QWeb.render('mail.chatter.Buttons', {
newMessageButton: !!this.fields.thread,
logNoteButton: this.hasLogButton,
scheduleActivityButton: !!this.fields.activity,
isMobile: config.device.isMobile,
}));
// start and append the widgets
var fieldDefs = _.invoke(this.fields, 'appendTo', $('
'));
var def = this._dp.add($.when.apply($, fieldDefs));
this._render(def).then(this._updateMentionSuggestions.bind(this));
return this._super.apply(this, arguments);
},
//--------------------------------------------------------------------------
// Public
//--------------------------------------------------------------------------
/**
* @param {Object} record
* @param {integer} [record.res_id=undefined]
* @param {Object[]} [fieldNames=undefined]
*/
update: function (record, fieldNames) {
var self = this;
// close the composer if we switch to another record as it is record dependent
if (this.record.res_id !== record.res_id) {
this._closeComposer(true);
this._closeAttachments();
this._areAttachmentsLoaded = false;
this.attachments = {};
}
// update the state
this._setState(record);
// detach the thread and activity widgets (temporarily force the height to prevent flickering)
// keep the followers in the DOM as it has a synchronous pre-rendering
this.$el.height(this.$el.height());
if (this.fields.activity) {
this.fields.activity.$el.detach();
}
if (this.fields.thread) {
this.fields.thread.$el.detach();
}
// reset and re-append the widgets (and reset 'height: auto' rule)
// if fieldNames is given, only reset those fields, otherwise reset all fields
var fieldsToReset;
if (fieldNames) {
fieldsToReset = _.filter(this.fields, function (field) {
return _.contains(fieldNames, field.name);
});
} else {
fieldsToReset = this.fields;
}
var fieldDefs = _.invoke(fieldsToReset, 'reset', record);
var def = this._dp.add($.when.apply($, fieldDefs));
this._render(def).then(function () {
self.$el.height('auto');
self._updateMentionSuggestions();
});
this._updateAttachmentCounter();
},
//--------------------------------------------------------------------------
// Private
//--------------------------------------------------------------------------
/**
* @private
*/
_closeAttachments: function () {
if (this.fields.attachments) {
this.$('.o_chatter_button_attachment').removeClass('o_active_attach');
this.fields.attachments.destroy();
this._isAttachmentBoxOpen = false;
}
},
/**
* @private
* @param {boolean} force
*/
_closeComposer: function (force) {
if (this._composer && (this._composer.isEmpty() || force)) {
this.$el.removeClass('o_chatter_composer_active');
this.$('.o_chatter_button_new_message, .o_chatter_button_log_note').removeClass('o_active');
this._composer.do_hide();
this._composer.clearComposer();
}
},
/**
* @private
*/
_disableChatter: function () {
this.$('.btn').prop('disabled', true); // disable buttons
},
/**
* @private
*/
_disableComposer: function () {
this.$(".o_composer_button_send").prop('disabled', true);
},
/**
* Discard changes on the record.
*
* @private
* @returns {$.Deferred} resolved if successfully discarding changes on
* the record, rejected otherwise
*/
_discardChanges: function () {
var def = $.Deferred();
this.trigger_up('discard_changes', {
recordID: this.record.id,
onSuccess: def.resolve.bind(def),
onFailure: def.reject.bind(def),
});
return def;
},
/**
* Discard changes on the record if the message will reload the record
* after posting it
*
* @private
* @param {Object} messageData
* @return {$.Deferred} resolved if no reload or proceed to discard the
* changes on the record, rejected otherwise
*/
_discardOnReload: function (messageData) {
if (this._reloadAfterPost(messageData)) {
return this._discardChanges();
}
return $.when();
},
/**
* @private
*/
_enableChatter: function () {
this.$('.btn').prop('disabled', false); // enable buttons
},
/**
* @private
*/
_enableComposer: function () {
this.$(".o_composer_button_send").prop('disabled', false);
},
/*
* @private
* @return {Deferred}
*/
_fetchAttachments: function () {
var self = this;
var domain = [
['res_id', '=', this.record.res_id],
['res_model', '=', this.record.model],
];
return this._rpc({
model: 'ir.attachment',
method: 'search_read',
domain: domain,
fields: ['id', 'name', 'datas_fname', 'mimetype'],
}).then(function (result) {
self._areAttachmentsLoaded = true;
self.attachments = result;
});
},
/**
* @private
*/
_openAttachmentBox: function () {
if (this.fields.attachments) {
this._closeAttachments();
}
this.fields.attachments = new AttachmentBox(this, this.record, this.attachments);
var $anchor = this.$('.o_chatter_topbar');
if (this._composer) {
var $anchor = this.$('.o_thread_composer');
} else {
var $anchor = this.$('.o_chatter_topbar');
}
this.fields.attachments.insertAfter($anchor);
this.$el.addClass('o_chatter_composer_active');
this.$('.o_chatter_button_attachment').addClass('o_active_attach');
this._isAttachmentBoxOpen = true;
},
/**
* @private
* @param {Object} options
* @param {Object[]} [options.suggested_partners=[]]
* @param {boolean} [options.isLog]
*/
_openComposer: function (options) {
var self = this;
var oldComposer = this._composer;
// create the new composer
this._composer = new ChatterComposer(this, this.record.model, options.suggested_partners || [], {
commandsEnabled: false,
context: this.context,
inputMinHeight: 50,
isLog: options && options.isLog,
recordName: this.recordName,
defaultBody: oldComposer && oldComposer.$input && oldComposer.$input.val(),
defaultMentionSelections: oldComposer && oldComposer.getMentionListenerSelections(),
attachmentIds: (oldComposer && oldComposer.get('attachment_ids')) || [],
});
this._composer.on('input_focused', this, function () {
this._composer.mentionSetPrefetchedPartners(this._mentionSuggestions || []);
});
this._composer.insertAfter(this.$('.o_chatter_topbar')).then(function () {
// destroy existing composer
if (oldComposer) {
oldComposer.destroy();
}
self._composer.focus();
self._composer.on('post_message', self, function (messageData) {
self._discardOnReload(messageData).then(function () {
self._disableComposer();
self.fields.thread.postMessage(messageData).then(function () {
self._closeComposer(true);
if (self._reloadAfterPost(messageData)) {
self.trigger_up('reload');
} else if (messageData.attachment_ids.length) {
self._reloadAttachmentBox();
self.trigger_up('reload', {fieldNames: ['message_attachment_count'], keepChanges: true});
}
}).fail(function () {
self._enableComposer();
});
});
});
self._composer.on('need_refresh', self, self.trigger_up.bind(self, 'reload'));
self._composer.on('close_composer', null, self._closeComposer.bind(self, true));
self.$el.addClass('o_chatter_composer_active');
self.$('.o_chatter_button_new_message, .o_chatter_button_log_note').removeClass('o_active');
self.$('.o_chatter_button_new_message').toggleClass('o_active', !self._composer.options.isLog);
self.$('.o_chatter_button_log_note').toggleClass('o_active', self._composer.options.isLog);
});
},
/**
* State if the record will be reloaded after posting a message.
* Useful to warn the user of unsaved changes if the record is dirty.
*
* @private
* @param {Object} messageData
* @param {Array} [messageData.partner_ids] list of recipients of a message
* @return {boolean} true if record will be reloaded after posting the
* message, false otherwise
*/
_reloadAfterPost: function (messageData) {
return this.postRefresh === 'always' ||
(
this.postRefresh === 'recipients' &&
messageData.partner_ids &&
messageData.partner_ids.length
);
},
/**
* @private
*/
_reloadAttachmentBox: function () {
this._areAttachmentsLoaded = false;
if (this._isAttachmentBoxOpen) {
this._fetchAttachments().then(this._openAttachmentBox.bind(this));
}
if (this.fields.thread) {
this.trigger_up('reload', { fieldNames: ['message_attachment_count'], keepChanges: true });
}
},
/**
* @private
* @param {Deferred} def
* @returns {Deferred}
*/
_render: function (def) {
// the rendering of the chatter is aynchronous: relational data of its fields needs to be
// fetched (in some case, it might be synchronous as they hold an internal cache).
// this function takes a deferred as argument, which is resolved once all fields have
// fetched their data
// this function appends the fields where they should be once the given deferred is resolved
// and if it takes more than 500ms, displays a spinner to indicate that it is loading
var self = this;
var $spinner = $(QWeb.render('Spinner'));
concurrency.rejectAfter(concurrency.delay(500), def).then(function () {
$spinner.appendTo(self.$el);
});
return def.then(function () {
if (self.fields.activity) {
self.fields.activity.$el.appendTo(self.$el);
}
if (self.fields.followers) {
if (self._disableAttachmentBox) {
self.fields.followers.$el.appendTo(self.$('.o_topbar_right_area'));
} else {
self.fields.followers.$el.insertBefore(self.$('.o_chatter_button_attachment'));
}
}
if (self.fields.thread) {
self.fields.thread.$el.appendTo(self.$el);
}
}).always(function () {
// disable widgets in create mode, otherwise enable
self._isCreateMode ? self._disableChatter() : self._enableChatter();
$spinner.remove();
});
},
/**
* @private
* @param {Object} record
* @param {integer} [record.res_id]
* @param {string} [record.model]
* @param {string} record.data.display_name
*/
_setState: function (record) {
this._isCreateMode = !record.res_id;
if (!this.record || this.record.res_id !== record.res_id) {
this.context = {
default_res_id: record.res_id || false,
default_model: record.model || false,
};
// reset the suggested_partners_def to ensure a reload of the
// suggested partners when opening the composer on another record
this.suggested_partners_def = undefined;
}
this.record = record;
this.recordName = record.data.display_name;
},
/**
* @private
*/
_updateAttachmentCounter: function () {
var count = this.record.data.message_attachment_count || 0;
this.$('.o_chatter_attachment_button_count').html(count);
},
/**
* @private
*/
_updateMentionSuggestions: function () {
if (!this.fields.followers) {
return;
}
var self = this;
this._mentionSuggestions = [];
// add the followers to the mention suggestions
var followerSuggestions = [];
var followers = this.fields.followers.getFollowers();
_.each(followers, function (follower) {
if (follower.res_model === 'res.partner') {
followerSuggestions.push({
id: follower.res_id,
name: follower.name,
email: follower.email,
});
}
});
if (followerSuggestions.length) {
this._mentionSuggestions.push(followerSuggestions);
}
// add the partners (followers filtered out) to the mention suggestions
_.each(this._mentionPartnerSuggestions, function (partners) {
self._mentionSuggestions.push(_.filter(partners, function (partner) {
return !_.findWhere(followerSuggestions, { id: partner.id });
}));
});
},
//--------------------------------------------------------------------------
// Handlers
//--------------------------------------------------------------------------
/**
* @private
* @param {OdooEvent} ev
* @param {integer} ev.data.attachmentId
* @param {String} ev.data.attachmentName
*/
_onDeleteAttachment: function (ev) {
ev.stopPropagation();
var self = this;
var options = {
confirm_callback: function () {
self._rpc({
model: 'ir.attachment',
method: 'unlink',
args: [parseInt(ev.data.attachmentId, 10)],
})
.then(function () {
self._reloadAttachmentBox();
if (self.fields.thread) {
self.fields.thread.removeAttachments([ev.data.attachmentId]);
}
self.trigger_up('reload');
});
}
};
var promptText = _.str.sprintf(_t("Do you really want to delete %s?"), _.escape(ev.data.attachmentName));
Dialog.confirm(this, promptText, options);
},
/**
* @private
*/
_onClickAttachmentButton: function () {
if(this._disableAttachmentBox) {
return;
}
if (this._isAttachmentBoxOpen) {
this._closeAttachments();
} else {
var def;
if (!this._areAttachmentsLoaded) {
def = this._fetchAttachments();
}
$.when(def).then(this._openAttachmentBox.bind(this));
}
},
/**
* Discard changes on the record.
* This is notified by the composer, when opening the full-composer.
*
* @private
* @param {OdooEvent} ev
* @param {function} ev.data.proceed callback to tell to proceed
*/
_onDiscardRecordChanges: function (ev) {
this._discardChanges().then(ev.data.proceed);
},
_onOpenComposerMessage: function () {
var self = this;
if (!this.suggested_partners_def) {
this.suggested_partners_def = $.Deferred();
var method = 'message_get_suggested_recipients';
var args = [[this.context.default_res_id], this.context];
this._rpc({model: this.record.model, method: method, args: args})
.then(function (result) {
if (!self.suggested_partners_def) {
return; // widget has been reset (e.g. we just switched to another record)
}
var suggested_partners = [];
var thread_recipients = result[self.context.default_res_id];
_.each(thread_recipients, function (recipient) {
var parsed_email = recipient[1] && mailUtils.parseEmail(recipient[1]);
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],
});
});
self.suggested_partners_def.resolve(suggested_partners);
});
}
this.suggested_partners_def.then(function (suggested_partners) {
self._openComposer({ isLog: false, suggested_partners: suggested_partners });
});
},
/**
* @private
*/
_onOpenComposerNote: function () {
this._openComposer({ isLog: true });
},
/**
* @private
*/
_onReloadAttachmentBox: function () {
if (this.reloadOnUploadAttachment) {
this.trigger_up('reload');
}
this._reloadAttachmentBox();
},
/**
* @private
* @param {OdooEvent} event
* @param {string} event.name
* @param {Object} event.data
* @param {boolean} [event.data.activity]
* @param {boolean} [event.data.followers]
* @param {boolean} [event.data.thread]
*/
_onReloadMailFields: function (event) {
var fieldNames = [];
if (this.fields.activity && event.data.activity) {
fieldNames.push(this.fields.activity.name);
}
if (this.fields.followers && event.data.followers) {
fieldNames.push(this.fields.followers.name);
}
if (this.fields.thread && event.data.thread) {
fieldNames.push(this.fields.thread.name);
}
this.trigger_up('reload', {
fieldNames: fieldNames,
keepChanges: true,
});
},
/**
* @private
*/
_onScheduleActivity: function () {
this.fields.activity.scheduleActivity();
},
});
return Chatter;
});