summaryrefslogtreecommitdiff
path: root/addons/mail/static/src/js/mail_thread.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_thread.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_thread.js')
-rw-r--r--addons/mail/static/src/js/mail_thread.js430
1 files changed, 430 insertions, 0 deletions
diff --git a/addons/mail/static/src/js/mail_thread.js b/addons/mail/static/src/js/mail_thread.js
new file mode 100644
index 00000000000..b40e1df5ced
--- /dev/null
+++ b/addons/mail/static/src/js/mail_thread.js
@@ -0,0 +1,430 @@
+odoo.define('mail.thread', function (require) {
+"use strict";
+
+var core = require('web.core');
+var data = require('web.data');
+var time = require('web.time');
+var Widget = require('web.Widget');
+var session = require('web.session');
+var web_client = require('web.web_client');
+var mail_utils = require('mail.utils');
+
+var _t = core._t;
+var QWeb = core.qweb;
+
+
+var LIMIT_MESSAGE = 20;
+
+/**
+ * Widget : Input textbox to post messages
+ *
+ * Input with 2 buttons to manage attachment and emoji
+ * - Attachment : upload selected attachment (one at a time) and display them below the textbox. Prevent
+ posting message while uploading
+ * - Emoji : popover exposing emoji list, and append emoji shortcode to the textbox when click on emoji image
+ * It triggers 'message_sent' event with the posted message, when it is sent to the server.
+ */
+var MailComposeMessage = Widget.extend({
+ template: 'mail.ComposeMessage',
+ events: {
+ "keydown .o_mail_compose_message_input": "on_keydown",
+ "change input.oe_form_binary_file": "on_attachment_change",
+ "click .o_mail_compose_message_attachment_list .o_mail_attachment_delete": "on_attachment_delete",
+ "click .o_mail_compose_message_button_attachment": 'on_click_attachment',
+ "click .o_mail_compose_message_button_send": 'message_send',
+ },
+ /**
+ * Contructor
+ * @param {Object} options : the options of the MailComposeMessage
+ * @param {Object[]} [options.emoji_list] : the list of emoji
+ * @param {Object} options.context : the context. It should contains the default_res_id' (id of the current document).
+ * @param {chat|mail} options.display_mode : the 'chat' mode will display an input, when the 'mail' mode will display a texarea.
+ */
+ init: function(parent, dataset, options){
+ this._super.apply(this, arguments);
+ this.thread_dataset = dataset;
+ this.options = _.defaults(options || {}, {
+ 'emoji_list': {},
+ 'context': {},
+ 'display_mode': 'mail',
+ });
+ this.emoji_list = this.options.emoji_list;
+ this.context = this.options.context;
+ // attachment handeling
+ this.AttachmentDataSet = new data.DataSetSearch(this, 'ir.attachment', this.context);
+ this.fileupload_id = _.uniqueId('o_mail_chat_fileupload');
+ this.set('attachment_ids', []);
+ },
+ start: function(){
+ var self = this;
+ this.$input = this.$('.o_mail_compose_message_input');
+ this.$input.focus();
+ this.$attachment_button = this.$(".o_mail_compose_message_button_attachment");
+ // attachments
+ $(window).on(this.fileupload_id, this.on_attachment_loaded);
+ this.on("change:attachment_ids", this, this.attachment_render);
+ // emoji
+ self.$('.o_mail_compose_message_button_emoji').popover({
+ placement: 'top',
+ content: function(){
+ if(!self.$emoji){ // lazy rendering
+ self.$emoji = $(QWeb.render('mail.ComposeMessage.emoji', {'widget': self}));
+ self.$emoji.find('.o_mail_compose_message_emoji_img').on('click', self, self.on_click_emoji_img);
+ }
+ return self.$emoji;
+ },
+ html: true,
+ container: '.o_mail_compose_message_emoji',
+ trigger: 'focus',
+ });
+ return this._super();
+ },
+ // events
+ on_click_attachment: function(event){
+ event.preventDefault();
+ this.$('input.oe_form_binary_file').click();
+ },
+ on_click_emoji_img: function(event){
+ this.$input.val(this.$input.val() + " " + $(event.currentTarget).data('emoji')+ " ");
+ this.$input.focus();
+ },
+ on_keydown: function(event){
+ if(event.which === 13 && this.options.display_mode === 'chat') {
+ this.message_send();
+ }
+ },
+ message_send: function(){
+ var $input = this.$('.o_mail_compose_message_input');
+ var mes = mail_utils.get_text2html($input.val());
+ if (! mes.trim() && this.do_check_attachment_upload()) {
+ return;
+ }
+ $input.val("");
+ this.message_post(mes, _.pluck(this.get('attachment_ids'), 'id'));
+ },
+ /**
+ * Sent the message to the server to create a mail.message, using 'message_post' method
+ * @param {String} body : the core of the message (html, or plaintext)
+ * @param {Number[]} attachment_ids : attachment_ids linked to the message
+ * @param {Object} kwargs : other parameters (in a dictionnary)
+ * @returns {Deferred} resolved when the message is posted, and returning the new message id
+ */
+ message_post: function(body, attachment_ids, kwargs){
+ var self = this;
+ var values = _.defaults(kwargs || {}, {
+ 'body': body,
+ 'attachment_ids': attachment_ids || [],
+ 'message_type': 'comment',
+ 'content_subtype': 'html',
+ 'partner_ids': [],
+ 'subtype': 'mail.mt_comment',
+ });
+ return this.thread_dataset._model.call('message_post', [this.context.default_res_id], values).then(function(message_id){
+ self.clean_attachments(); // empty attachment list
+ self.trigger('message_sent', message_id);
+ return message_id;
+ });
+ },
+ // attachment business
+ on_attachment_change: function(event){
+ var $target = $(event.target);
+ if ($target.val() !== '') {
+ var filename = $target.val().replace(/.*[\\\/]/,'');
+ // if the files exits for this answer, delete the file before upload
+ var attachments = [];
+ for (var i in this.get('attachment_ids')) {
+ if ((this.get('attachment_ids')[i].filename || this.get('attachment_ids')[i].name) == filename) {
+ if (this.get('attachment_ids')[i].upload) {
+ return false;
+ }
+ this.AttachmentDataSet.unlink([this.get('attachment_ids')[i].id]);
+ } else {
+ attachments.push(this.get('attachment_ids')[i]);
+ }
+ }
+ // submit filename
+ this.$('form.oe_form_binary_form').submit();
+ this.$attachment_button.prop('disabled', true);
+
+ attachments.push({
+ 'id': 0,
+ 'name': filename,
+ 'filename': filename,
+ 'url': '',
+ 'upload': true
+ });
+ this.set('attachment_ids', attachments);
+ }
+ },
+ on_attachment_loaded: function(event, result){
+ var attachment_ids = [];
+ if (result.error || !result.id ) {
+ this.do_warn(result.error);
+ attachment_ids = _.filter(this.get('attachment_ids'), function (val) { return !val.upload; });
+ }else{
+ _.each(this.get('attachment_ids'), function(a){
+ if (a.filename == result.filename && a.upload) {
+ attachment_ids.push({
+ 'id': result.id,
+ 'name': result.name || result.filename,
+ 'filename': result.filename,
+ 'url': session.url('/web/binary/saveas', {model: 'ir.attachment', field: 'datas', filename_field: 'name', 'id': result.id}),
+ });
+ }else{
+ attachment_ids.push(a);
+ }
+ });
+ }
+ this.set('attachment_ids', attachment_ids);
+
+ // TODO JEM : understand the 2 lines below ....
+ var $input = this.$('input.oe_form_binary_file');
+ $input.after($input.clone(true)).remove();
+
+ this.$attachment_button.prop('disabled', false);
+ },
+ on_attachment_delete: function(event){
+ event.stopPropagation();
+ var self = this;
+ var attachment_id = $(event.target).data("id");
+ if (attachment_id) {
+ var attachments = [];
+ _.each(this.get('attachment_ids'), function(attachment){
+ if (attachment_id != attachment.id) {
+ attachments.push(attachment);
+ } else {
+ self.AttachmentDataSet.unlink([attachment_id]);
+ }
+ });
+ this.set('attachment_ids', attachments);
+ }
+ },
+ do_check_attachment_upload: function () {
+ if (_.find(this.get('attachment_ids'), function (file) {return file.upload;})) {
+ this.do_warn(_t("Uploading error"), _t("Please, wait while the file is uploading."));
+ return false;
+ }
+ return true;
+ },
+ clean_attachments: function(){
+ this.set('attachment_ids', []);
+ },
+ // ui
+ attachment_render: function(){
+ this.$('.o_mail_compose_message_attachment_list').html(QWeb.render('mail.ComposeMessage.attachments', {'widget': this}));
+ },
+ focus: function(){
+ this.$input.focus();
+ }
+});
+
+
+/**
+ * Mail Thread Mixin : Messages Managment
+ *
+ * Load, Fetch, Display mail.message
+ * This is a mixin since it will be inherit by a form_common.AbstractField and a Wigdet (Client Action)
+ *
+ * @mixin
+ **/
+var MailThreadMixin = {
+ /**
+ * Constructor of the 'mail_thread' mixin
+ * @param {Boolean} [options.display_log_button] : display of the 'log a note' button
+ * @param {Boolean} [options.display_document_link] : display document link on message
+ * @param {Boolean} [options.display_needaction_button] : display the needaction 'check' button on messages
+ * @param {Object[]} [options.internal_subtypes]: list of internal subtype (for employee only)
+ * @param {Object[]} [options.emoji_list] : list of emoji
+ */
+ init: function(){
+ this.MessageDatasetSearch = new data.DataSetSearch(this, 'mail.message');
+ this.set('messages', []);
+ this.emoji_substitution = {};
+ // options
+ this.options = _.defaults(this.options || {}, {
+ 'display_log_button': false,
+ 'display_document_link': false,
+ 'display_needaction_button': false,
+ 'internal_subtypes': [],
+ 'emoji_list': [],
+ });
+ },
+ start: function(){
+ this.on("change:messages", this, this.message_render);
+ },
+ // Common Actions (They should be bind on the implementing widget, the 'events' dict)
+ /**
+ * Generic redirect action : redirect to the form view if the
+ * click node contains 'data-oe-model' and 'data-oe-id'.
+ */
+ on_click_redirect: function(event){
+ event.preventDefault();
+ var res_id = $(event.target).data('oe-id');
+ var res_model = $(event.target).data('oe-model');
+ web_client.action_manager.do_push_state({
+ 'model': res_model,
+ 'id': res_id,
+ 'title': this.record_name,
+ });
+ this.do_action({
+ type:'ir.actions.act_window',
+ view_type: 'form',
+ view_mode: 'form',
+ res_model: res_model,
+ views: [[false, 'form']],
+ res_id: res_id,
+ }, {
+ 'on_reverse_breadcrumb': this.on_reverse_breadcrumb,
+ });
+ },
+ /**
+ * Will be called when clicking on the Breadcrumb (mainly of client action)
+ * Need to be override
+ */
+ on_reverse_breadcrumb: function(){
+
+ },
+ /**
+ * Toggle a message as 'starred' (or not), by toggling the 'o_mail_message_starred' css class.
+ * Trigger when clicking on selector '.o_mail_thread_message_star'.
+ */
+ on_message_star: function(event){
+ var $source = this.$(event.currentTarget);
+ var mid = $source.data('message-id');
+ var is_starred = !$source.hasClass('o_mail_message_starred');
+ return this.MessageDatasetSearch.call('set_message_starred', [[mid], is_starred]).then(function(res){
+ $source.toggleClass('o_mail_message_starred');
+ return mid;
+ });
+ },
+ on_message_needaction: function(event){
+ var $source = this.$(event.currentTarget);
+ var mid = $source.data('message-id');
+ return this.MessageDatasetSearch.call('set_message_done', [[mid]]).then(function(res){
+ $source.remove();
+ return mid;
+ });
+ },
+ // Message functions
+ /**
+ * Fetch given message
+ * @param {Number[]} message_ids : list of mail.message identifiers to fetch
+ * @returns {Deferred} resolved when the messages are loaded
+ */
+ message_format: function(message_ids){
+ return this.MessageDatasetSearch._model.call('message_format', [message_ids]);
+ },
+ /**
+ * Fetch mail.message in the format defined server side
+ * @param {Array} domain : Odoo Domain of the message to fetch
+ * @param {Number} limit : the limit of messages to fetch
+ * @returns {Deferred} resolved when the messages are loaded
+ */
+ message_fetch: function(domain, limit){
+ domain = domain || this.get_message_domain();
+ limit = limit || LIMIT_MESSAGE;
+ return this.MessageDatasetSearch._model.call('message_fetch', [domain], {limit: limit});
+ },
+ _message_replace: function(messages){
+ this.set('messages', this._message_order(messages));
+ },
+ /**
+ * Order the message in a particular order, to be defined by implementation
+ * @param {Object[]} messages : messages to order
+ */
+ _message_order: function(messages){
+ return _.sortBy(messages, 'date');
+ },
+ /**
+ * Insert messages in the current list
+ * @param {Object[]} messages : list of mail.message (formatted server side)
+ */
+ message_insert: function(raw_messages){
+ var current_messages = this.get('messages');
+ current_messages = current_messages.concat(this._message_preprocess(raw_messages));
+ this._message_replace(current_messages);
+ },
+ /**
+ * Load history of message, with the 'history' domain, and insert them in the current list
+ * @returns {Deferred} resolved when the messages are fetched and returning the raw
+ * messages (formatted server side)
+ */
+ message_load_history: function(){
+ var self = this;
+ return this.message_fetch(this.get_message_domain_history()).then(function(raw_messages){
+ self.message_insert(raw_messages);
+ return raw_messages
+ });
+ },
+ /**
+ * Load messages according to the current domain, and set that new list as current messages,
+ * erasing the old ones.
+ * @returns {Deferred} resolved when the messages are fetched and returning the raw
+ * messages (formatted server side)
+ */
+ message_load_new: function(){
+ var self = this;
+ return this.message_fetch(this.get_message_domain()).then(function(raw_messages){
+ self._message_replace(self._message_preprocess(raw_messages));
+ return raw_messages
+ });
+ },
+ /**
+ * Preprocess the list of messages before rendering, add 'computed' field (is_needaction,
+ * is_starred, ...), and apply image shortcode to the message body.
+ * @param {Object[]} messages : list of raw mail.message (formatted server side)
+ * @returns list of messages. It can be sorted, grouped, ...
+ */
+ _message_preprocess: function(messages){
+ var self = this;
+ _.each(messages, function(m){
+ m.is_needaction = _.contains(m.needaction_partner_ids, session.partner_id);
+ m.is_starred = _.contains(m.starred_partner_ids, session.partner_id);
+ m.date = moment(time.str_to_datetime(m.date)).format('YYYY-MM-DD HH:mm:ss'); // set the date in the correct browser user timezone
+ if(m.body){
+ m.body = mail_utils.shortcode_apply(m.body, self.emoji_substitution);
+ }
+ _.each(m.attachment_ids, function(a){
+ a.url = mail_utils.get_attachment_url(session, m.id, a.id);
+ });
+ });
+ return _.sortBy(messages, 'date');
+ },
+ /**
+ * Take the current messages, render them, and insert the rendering in the DOM.
+ * This is triggered when the mesasge list change.
+ * Must be redefined, since it depends on the complete DOM widget
+ */
+ message_render: function(){
+
+ },
+ // Message Domains
+ /**
+ * Return the current domain to fetch the message of the thread. this should
+ * be redifined by the thread implementation.
+ */
+ get_message_domain: function(){
+ return [];
+ },
+ get_message_domain_history: function(){
+ return this.get_message_domain().concat([['id', '<', _.min(_.pluck(this.get('messages'), 'id'))]]);
+ },
+ // Others
+ /**
+ * Set the list of emoji to be substituted in message body
+ * @param {Object[]} emoji_list : list of emoji Object
+ */
+ emoji_set_substitution: function(emoji_list){
+ this.emoji_substitution = mail_utils.shortcode_substitution(emoji_list);
+ },
+};
+
+
+return {
+ MailComposeMessage: MailComposeMessage,
+ MailThreadMixin: MailThreadMixin,
+ LIMIT_MESSAGE: LIMIT_MESSAGE,
+}
+
+
+});