baa9354d9b3c8f692a43e402ac7b67f5975d79d0
[odoo/odoo.git] / addons / mail / static / src / js / mail.js
1 openerp.mail = function(session) {
2     var _t = session.web._t,
3        _lt = session.web._lt;
4
5     var mail = session.mail = {};
6
7     openerp_mail_followers(session, mail);        // import mail_followers.js
8
9     /**
10      * ------------------------------------------------------------
11      * FormView
12      * ------------------------------------------------------------
13      * 
14      * Override of formview do_action method, to catch all return action about
15      * mail.compose.message. The purpose is to bind 'Send by e-mail' buttons.
16      */
17
18     session.web.FormView = session.web.FormView.extend({
19         do_action: function(action) {
20             if (action.res_model == 'mail.compose.message') {
21                 /* hack for stop context propagation of wrong value
22                  * delete this hack when a global method to clean context is create
23                  */
24                 var context_keys = ['default_template_id', 'default_composition_mode', 
25                     'default_use_template', 'default_partner_ids', 'default_model',
26                     'default_res_id', 'default_subtype', 'active_id', 'lang',
27                     'bin_raw', 'tz', 'active_model', 'edi_web_url_view', 'active_ids']
28                 for (var key in action.context) {
29                     if (_.indexOf(context_keys, key) == -1) {
30                         action.context[key] = null;
31                     }
32                 }
33                 /* end hack */
34             }
35             return this._super.apply(this, arguments);
36         },
37     });
38
39
40     /**
41      * ------------------------------------------------------------
42      * ChatterUtils
43      * ------------------------------------------------------------
44      * 
45      * This class holds a few tools method for Chatter.
46      * Some regular expressions not used anymore, kept because I want to
47      * - (^|\s)@((\w|@|\.)*): @login@log.log
48      * - (^|\s)\[(\w+).(\w+),(\d)\|*((\w|[@ .,])*)\]: [ir.attachment,3|My Label],
49      *   for internal links
50      */
51
52     mail.ChatterUtils = {
53
54         /** Get an image in /web/binary/image?... */
55         get_image: function(session, model, field, id) {
56             return session.prefix + '/web/binary/image?session_id=' + session.session_id + '&model=' + model + '&field=' + field + '&id=' + (id || '');
57         },
58
59         /** Get the url of an attachment {'id': id} */
60         get_attachment_url: function (session, attachment) {
61             return session.origin + '/web/binary/saveas?session_id=' + session.session_id + '&model=ir.attachment&field=datas&filename_field=datas_fname&id=' + attachment['id'];
62         },
63
64         /** Replaces some expressions
65          * - :name - shortcut to an image
66          */
67         do_replace_expressions: function (string) {
68             var icon_list = ['al', 'pinky']
69             /* special shortcut: :name, try to find an icon if in list */
70             var regex_login = new RegExp(/(^|\s):((\w)*)/g);
71             var regex_res = regex_login.exec(string);
72             while (regex_res != null) {
73                 var icon_name = regex_res[2];
74                 if (_.include(icon_list, icon_name))
75                     string = string.replace(regex_res[0], regex_res[1] + '<img src="/mail/static/src/img/_' + icon_name + '.png" width="22px" height="22px" alt="' + icon_name + '"/>');
76                 regex_res = regex_login.exec(string);
77             }
78             return string;
79         },
80
81         /* replace textarea text into html text
82          * (add <p>, <a>)
83          * TDE note : should not be here, but server-side I think ...
84         */
85         get_text2html: function(text){
86             return text
87                 .replace(/[\n\r]/g,'<br/>')
88                 .replace(/((?:https?|ftp):\/\/[\S]+)/g,'<a href="$1">$1</a> ')
89         }
90     };
91
92
93     /**
94      * ------------------------------------------------------------
95      * ComposeMessage widget
96      * ------------------------------------------------------------
97      * 
98      * This widget handles the display of a form to compose a new message.
99      * This form is a mail.compose.message form_view.
100      */
101     
102     mail.ThreadComposeMessage = session.web.Widget.extend({
103         template: 'mail.compose_message',
104
105         /**
106          * @param {Object} parent parent
107          * @param {Object} [options]
108          *      @param {Object} [context] context passed to the
109          *          mail.compose.message DataSetSearch. Please refer to this model
110          *          for more details about fields and default values.
111          *      @param {Boolean} [show_attachment_delete] 
112          */
113         init: function (parent, options) {
114             var self = this;
115             this._super(parent);
116             this.context = options.context || {};
117
118             this.datasets = {
119                 'attachment_ids' : [],
120                 'id': options.datasets.id,
121                 'model': options.datasets.model,
122                 'res_model': options.datasets.res_model,
123                 'is_private': options.datasets.is_private || false,
124                 'partner_ids': options.datasets.partner_ids || []
125             };
126             this.options={};
127             this.options.thread={};
128             this.options.thread.show_header_compose = options.options.thread.show_header_compose;
129             this.options.thread.display_on_thread = options.options.thread.display_on_thread;
130             this.options.thread.show_attachment_delete = true;
131             this.options.thread.show_attachment_link = true;
132
133             this.parent_thread= parent.messages!= undefined ? parent : false;
134
135             this.ds_attachment = new session.web.DataSetSearch(this, 'ir.attachment');
136
137             this.fileupload_id = _.uniqueId('oe_fileupload_temp');
138             $(window).on(self.fileupload_id, self.on_attachment_loaded);
139         },
140
141         start: function(){
142             this.display_attachments();
143             this.bind_events();
144
145             //load avatar img
146             var user_avatar = mail.ChatterUtils.get_image(this.session, 'res.users', 'image_small', this.session.uid);
147             this.$('img.oe_mail_icon').attr('src', user_avatar);
148         },
149
150         /* upload the file on the server, add in the attachments list and reload display
151          */
152         display_attachments: function(){
153             var self = this;
154             var render = $(session.web.qweb.render('mail.thread.message.attachments', {'widget': self}));
155             if(!this.list_attachment){
156                 this.$('.oe_mail_compose_attachment_list').replaceWith( render );
157             } else {
158                 this.list_attachment.replaceWith( render );
159             }
160             this.list_attachment = this.$("ul.oe_msg_attachments");
161
162             // event: delete an attachment
163             this.$el.on('click', '.oe_mail_attachment_delete', self.on_attachment_delete);
164         },
165         on_attachment_change: function (event) {
166             event.stopPropagation();
167             var self = this;
168             var $target = $(event.target);
169             if ($target.val() !== '') {
170
171                 var filename = $target.val().replace(/.*[\\\/]/,'');
172
173                 // if the files exits for this answer, delete the file before upload
174                 var attachments=[];
175                 for(var i in this.datasets.attachment_ids){
176                     if((this.datasets.attachment_ids[i].filename || this.datasets.attachment_ids[i].name) == filename){
177                         if(this.datasets.attachment_ids[i].upload){
178                             return false;
179                         }
180                         this.ds_attachment.unlink([this.datasets.attachment_ids[i].id]);
181                     } else {
182                         attachments.push(this.datasets.attachment_ids[i]);
183                     }
184                 }
185                 this.datasets.attachment_ids = attachments;
186
187                 // submit file
188                 //session.web.blockUI();
189                 self.$('form.oe_form_binary_form').submit();
190
191                 this.$(".oe_attachment_file").hide();
192
193                 this.datasets.attachment_ids.push({
194                     'id': 0,
195                     'name': filename,
196                     'filename': filename,
197                     'url': '',
198                     'upload': true
199                 });
200                 this.display_attachments();
201             }
202         },
203         
204         on_attachment_loaded: function (event, result) {
205             //session.web.unblockUI();
206             for(var i in this.datasets.attachment_ids){
207                 if(this.datasets.attachment_ids[i].filename == result.filename && this.datasets.attachment_ids[i].upload) {
208                     this.datasets.attachment_ids[i]={
209                         'id': result.id,
210                         'name': result.name,
211                         'filename': result.filename,
212                         'url': mail.ChatterUtils.get_attachment_url(this.session, result)
213                     };
214                 }
215             }
216             this.display_attachments();
217
218             var $input = this.$('input.oe_form_binary_file');
219             $input.after($input.clone(true)).remove();
220             this.$(".oe_attachment_file").show();
221         },
222         /* unlink the file on the server and reload display
223          */
224         on_attachment_delete: function (event) {
225             event.stopPropagation();
226             var attachment_id=$(event.target).data("id");
227             if (attachment_id) {
228                 var attachments=[];
229                 for(var i in this.datasets.attachment_ids){
230                     if(attachment_id!=this.datasets.attachment_ids[i].id){
231                         attachments.push(this.datasets.attachment_ids[i]);
232                     }
233                     else {
234                         this.ds_attachment.unlink([attachment_id]);
235                     }
236                 }
237                 this.datasets.attachment_ids = attachments;
238                 this.display_attachments();
239             }
240         },
241
242         /* to avoid having unsorted file on the server.
243             we will show the users files of the first message post
244             TDE note: unnecessary call to server I think
245          */
246         // set_free_attachments: function(){
247         //     var self=this;
248         //     this.parent_thread.ds_message.call('user_free_attachment').then(function(attachments){
249         //         this.attachment_ids=[];
250         //         for(var i in attachments){
251         //             self.attachment_ids[i]={
252         //                 'id': attachments[i].id,
253         //                 'name': attachments[i].name,
254         //                 'filename': attachments[i].filename,
255         //                 'url': mail.ChatterUtils.get_attachment_url(self.session, attachments[i])
256         //             };
257         //         }
258         //         self.display_attachments();
259         //     });
260         // },
261
262         bind_events: function() {
263             var self = this;
264
265             // set the function called when attachments are added
266             this.$el.on('change', 'input.oe_form_binary_file', self.on_attachment_change );
267             this.$el.on('click', 'a.oe_cancel', self.on_cancel );
268             this.$el.on('click', 'button.oe_post', function(){self.on_message_post()} );
269             this.$el.on('click', 'button.oe_full', function(){self.on_compose_fullmail()} );
270         },
271
272         on_compose_fullmail: function(){
273             var attachments=[];
274             for(var i in this.datasets.attachment_ids){
275                 attachments.push(this.datasets.attachment_ids[i].id);
276             }
277             var partner_ids=[];
278             for(var i in this.datasets.partner_ids){
279                 partner_ids.push(this.datasets.partner_ids[i][0]);
280             }
281             var action = {
282                 type: 'ir.actions.act_window',
283                 res_model: 'mail.compose.message',
284                 view_mode: 'form',
285                 view_type: 'form',
286                 action_from: 'mail.ThreadComposeMessage',
287                 views: [[false, 'form']],
288                 target: 'new',
289                 context: {
290                     'default_model': this.context.default_model,
291                     'default_res_id': this.context.default_res_id,
292                     'default_content_subtype': 'html',
293                     'default_parent_id': this.context.default_parent_id,
294                     'default_body': mail.ChatterUtils.get_text2html(this.$('textarea').val() || ''),
295                     'default_attachment_ids': attachments,
296                     'default_partner_ids': partner_ids
297                 },
298             };
299             this.do_action(action);
300         },
301
302         on_cancel: function(event){
303             if(event) event.stopPropagation();
304             this.$('textarea').val("");
305             this.$('input[data-id]').remove();
306             //this.attachment_ids=[];
307             this.display_attachments();
308             if(!this.options.thread.show_header_compose || !this.options.thread.display_on_thread[0]){
309                 this.$el.hide();
310             }
311         },
312
313         /*post a message and fetch the message*/
314         on_message_post: function (body) {
315             var self = this;
316
317             if (! body) {
318                 var comment_node = this.$('textarea');
319                 var body = comment_node.val();
320                 comment_node.val('');
321             }
322
323             var attachments=[];
324             for(var i in this.datasets.attachment_ids){
325                 if(this.datasets.attachment_ids[i].upload){
326                     session.web.dialog($('<div>' + session.web.qweb.render('CrashManager.warning', {message: 'Please, wait while the file is uploading.'}) + '</div>'));
327                     return false;
328                 }
329                 attachments.push(this.datasets.attachment_ids[i].id);
330             }
331
332             if(body.match(/\S+/)) {
333                 this.parent_thread.ds_thread.call('message_post_api', [
334                         this.context.default_res_id, 
335                         mail.ChatterUtils.get_text2html(body), 
336                         false, 
337                         'comment', 
338                         'mail.mt_comment',
339                         this.context.default_parent_id, 
340                         attachments,
341                         this.parent_thread.context
342                     ]).then(function(records){
343                         self.parent_thread.switch_new_message(records);
344                         self.datasets.attachment_ids=[];
345                         self.on_cancel();
346                     });
347                 return true;
348             }
349         },
350     });
351
352     /** 
353      * ------------------------------------------------------------
354      * Thread Message Expandable Widget
355      * ------------------------------------------------------------
356      *
357      * This widget handles the display the expandable message in a thread.
358      * - thread
359      * - - visible message
360      * - - expandable
361      * - - visible message
362      * - - visible message
363      * - - expandable
364      */
365     mail.ThreadExpandable = session.web.Widget.extend({
366         template: 'mail.thread.expandable',
367
368         init: function(parent, options) {
369             this._super(parent);
370             this.domain = options.domain || [];
371             this.context = _.extend({
372                 default_model: 'mail.thread',
373                 default_res_id: 0,
374                 default_parent_id: false }, options.context || {});
375
376             this.datasets = {
377                 'id' : options.datasets.id || -1,
378                 'model' : options.datasets.model || false,
379                 'parent_id' : options.datasets.parent_id || false,
380                 'nb_messages' : options.datasets.nb_messages || 0,
381                 'type' : 'expandable',
382                 'max_limit' : options.datasets.max_limit || false,
383                 'flag_used' : false,
384             };
385
386             // record options and data
387             this.parent_thread= parent.messages!= undefined ? parent : options.options.thread._parents[0] ;
388         },
389
390         
391         start: function() {
392             this._super.apply(this, arguments);
393             this.bind_events();
394         },
395
396         /**
397          * Bind events in the widget. Each event is slightly described
398          * in the function. */
399         bind_events: function() {
400             var self = this;
401             this.$el.on('click', 'a.oe_mail_fetch_more', self.on_expandable);
402         },
403
404         animated_destroy: function(options) {
405             var self=this;
406             //graphic effects
407             if(options && options.fadeTime) {
408                 self.$el.fadeOut(options.fadeTime, function(){
409                     self.destroy();
410                 });
411             } else {
412                 self.destroy();
413             }
414         },
415
416         /*The selected thread and all childs (messages/thread) became read
417         * @param {object} mouse envent
418         */
419         on_expandable: function (event) {
420             if(event)event.stopPropagation();
421             if(this.datasets.flag_used) {
422                 return false
423             }
424             this.datasets.flag_used = true;
425
426             this.animated_destroy({'fadeTime':300});
427             this.parent_thread.message_fetch(false, this.domain, this.context);
428             return false;
429         },
430     });
431
432     /** 
433      * ------------------------------------------------------------
434      * Thread Message Widget
435      * ------------------------------------------------------------
436      * This widget handles the display of a messages in a thread. 
437      * Displays a record and performs some formatting on the record :
438      * - record.date: formatting according to the user timezone
439      * - record.timerelative: relative time givein by timeago lib
440      * - record.avatar: image url
441      * - record.attachment_ids[].url: url of each attachmentThe
442      * thread view :
443      * - root thread
444      * - - sub message (parent_id = root message)
445      * - - - sub thread
446      * - - - - sub sub message (parent id = sub thread)
447      * - - sub message (parent_id = root message)
448      * - - - sub thread
449      */
450     mail.ThreadMessage = session.web.Widget.extend({
451         template: 'mail.thread.message',
452
453         /**
454          * @param {Object} parent parent
455          * @param {Array} [domain]
456          * @param {Object} [context] context of the thread. It should
457             contain at least default_model, default_res_id. Please refer to
458             the ComposeMessage widget for more information about it.
459          * @param {Object} [options]
460          *      @param {Object} [thread] read obout mail.Thread object
461          *      @param {Object} [message]
462          *          @param {Number} [message_ids=null] ids for message_fetch
463          *          @param {Number} [message_data=null] already formatted message data, 
464          *              for subthreads getting data from their parent
465          *          @param {Number} [truncate_limit=250] number of character to
466          *              display before having a "show more" link; note that the text
467          *              will not be truncated if it does not have 110% of the parameter
468          *          @param {Boolean} [show_record_name]
469          *          @param {Boolean} [show_dd_delete]
470          *          @param {Array [A,B]} [show_reply] display the reply button on the
471          *              message for thread level between A and B. -1 for no begin or no end.
472          *          @param {Array [A,B]} [show_read_unread] display the read/unread button on the
473          *              message for thread level between A and B. -1 for no begin or no end.
474          */
475         init: function(parent, options) {
476             this._super(parent);
477
478             // record datasets
479             var param = options.datasets;
480             this.datasets = _.extend({
481                 'id' : -1,
482                 'model' : false,
483                 'parent_id': false,
484                 'res_id' : false,
485                 'type' : false,
486                 'is_author' : false,
487                 'is_private' : false,
488                 'subject' : false,
489                 'name' : false,
490                 'record_name' : false,
491                 'body' : false,
492                 'vote_user_ids' :[],
493                 'has_voted' : false,
494                 'is_favorite' : false,
495                 'thread_level' : 0,
496                 'to_read' : true,
497                 'author_id' : [],
498                 'attachment_ids' : [],
499             }, param || {});
500             this.datasets._date = param.date;
501
502             // record domain and context
503             this.domain = options.domain || [];
504             this.context = _.extend({
505                 default_model: 'mail.thread',
506                 default_res_id: 0,
507                 default_parent_id: false }, options.context || {});
508
509             // record options
510             this.options={
511                 'thread' : options.options.thread,
512                 'message' : {
513                     'message_ids': options.options.message.message_ids || null,
514                     'message_data': options.options.message.message_data || null,
515                     'show_record_name': options.options.message.show_record_name != undefined ? options.options.message.show_record_name: true,
516                     'show_dd_delete': options.options.message.show_dd_delete || false,
517                     'truncate_limit': options.options.message.truncate_limit || 250,
518                     'show_reply': options.options.message.show_reply || [0,-1],
519                     'show_read_unread': options.options.message.show_read_unread || [0,-1],
520                 }
521             };
522
523             this.datasets.show_reply = this.options.message.show_reply[0]>=0 && 
524                 this.options.message.show_reply[0]<=this.datasets.thread_level &&
525                 (this.options.message.show_reply[1]<0 || this.options.message.show_reply[1]>=this.datasets.thread_level);
526
527             this.datasets.show_read_unread = this.options.message.show_read_unread[0]>=0 && 
528                 this.options.message.show_read_unread[0]<=this.datasets.thread_level &&
529                 (this.options.message.show_read_unread[1]<0 || this.options.message.show_read_unread[1]>=this.datasets.thread_level);
530
531             // record options and data
532             this.parent_thread= parent.messages!= undefined ? parent : options.options.thread._parents[0];
533             this.thread = false;
534
535             if( this.datasets.id > 0 ) {
536                 this.formating_data();
537             }
538
539             this.ds_notification = new session.web.DataSetSearch(this, 'mail.notification');
540             this.ds_message = new session.web.DataSetSearch(this, 'mail.message');
541             this.ds_follow = new session.web.DataSetSearch(this, 'mail.followers');
542         },
543
544         formating_data: function(){
545
546             //formating and add some fields for render
547             this.datasets.date = session.web.format_value(this.datasets._date, {type:"datetime"});
548             this.datasets.timerelative = $.timeago(this.datasets.date);
549             if (this.datasets.type == 'email') {
550                 this.datasets.avatar = ('/mail/static/src/img/email_icon.png');
551             } else {
552                 this.datasets.avatar = mail.ChatterUtils.get_image(this.session, 'res.partner', 'image_small', this.datasets.author_id[0]);
553             }
554             for (var l in this.datasets.attachment_ids) {
555                 var attach = this.datasets.attachment_ids[l];
556                 attach['url'] = mail.ChatterUtils.get_attachment_url(this.session, attach);
557             }
558         },
559         
560         start: function() {
561             this._super.apply(this, arguments);
562             this.expender();
563             this.$el.hide().fadeIn(750);
564             this.bind_events();
565             this.create_thread();
566         },
567
568         /**
569          * Bind events in the widget. Each event is slightly described
570          * in the function. */
571         bind_events: function() {
572             var self = this;
573
574             // event: click on 'Attachment(s)' in msg
575             this.$('a.oe_msg_view_attachments:first').on('click', function (event) {
576                 self.$('.oe_msg_attachments:first').toggle();
577             });
578             // event: click on icone 'Read' in header
579             this.$el.on('click', 'a.oe_read', this.on_message_read_unread);
580             // event: click on icone 'UnRead' in header
581             this.$el.on('click', 'a.oe_unread', this.on_message_read_unread);
582             // event: click on 'Delete' in msg side menu
583             this.$el.on('click', 'a.oe_msg_delete', this.on_message_delete);
584
585             // event: click on 'Reply' in msg
586             this.$el.on('click', 'a.oe_reply', this.on_message_reply);
587             // event: click on 'Vote' button
588             this.$el.on('click', 'button.oe_msg_vote', this.on_vote);
589             // event: click on 'Star' button
590             this.$el.on('click', 'button.oe_mail_starbox', this.on_star);
591         },
592
593         on_message_reply:function(event){
594             event.stopPropagation();
595             this.thread.on_compose_message();
596             return false;
597         },
598
599         expender: function(){
600             this.$('div.oe_msg_body:first').expander({
601                 slicePoint: this.options.truncate_limit,
602                 expandText: 'read more',
603                 userCollapseText: '[^]',
604                 detailClass: 'oe_msg_tail',
605                 moreClass: 'oe_mail_expand',
606                 lessClass: 'oe_mail_reduce',
607                 });
608         },
609
610         create_thread: function(){
611             var self=this;
612             if(this.thread){
613                 return false;
614             }
615
616             /*create thread*/
617             self.thread = new mail.Thread(self, {
618                     'domain': self.domain,
619                     'context':{
620                         'default_model': self.datasets.model,
621                         'default_res_id': self.datasets.res_id,
622                         'default_parent_id': self.datasets.id
623                     },
624                     'options': {
625                         'thread' : self.options.thread,
626                         'message' : self.options.message
627                     },
628                     'datasets': self.datasets
629                 }
630             );
631             /*insert thread in parent message*/
632             self.thread.appendTo(self.$el.find('div.oe_thread_placeholder'));
633         },
634         
635         animated_destroy: function(options) {
636             var self=this;
637             //graphic effects  
638             if(options && options.fadeTime) {
639                 self.$el.fadeOut(options.fadeTime, function(){
640                     self.destroy();
641                 });
642             } else {
643                 self.destroy();
644             }
645         },
646
647         on_message_delete: function (event) {
648             event.stopPropagation();
649             if (! confirm(_t("Do you really want to delete this message?"))) { return false; }
650             
651             this.animated_destroy({fadeTime:250});
652             // delete this message and his childs
653             var ids = [this.datasets.id].concat( this.get_child_ids() );
654             this.ds_message.unlink(ids);
655             this.animated_destroy();
656             return false;
657         },
658
659         /*The selected thread and all childs (messages/thread) became read
660         * @param {object} mouse envent
661         */
662         on_message_read_unread: function (event) {
663             event.stopPropagation();
664             // if this message is read, all childs message display is read
665             var ids = [this.datasets.id].concat( this.get_child_ids() );
666             var read = $(event.srcElement).hasClass("oe_read");
667             this.$el.removeClass("oe_mail_" + (read?"un":"") + "read").addClass("oe_mail_" + (read?"":"un") + "read");
668
669             if( (read && this.options.thread.typeof_thread == 'inbox') ||
670                 (!read && this.options.thread.typeof_thread == 'archives')) {
671                 this.animated_destroy({fadeTime:250});
672             }
673             // TDE note: should have a context here
674             this.ds_notification.call('set_message_read', [ids, read]);
675             return false;
676         },
677
678         /** browse message
679          * @param {object}{int} option.id
680          * @param {object}{string} option.model
681          * @param {object}{boolean} option._go_thread_wall
682          *      private for check the top thread
683          * @return thread object
684          */
685         browse_message: function(options){
686             // goto the wall thread for launch browse
687             if(!options._go_thread_wall) {
688                 options._go_thread_wall = true;
689                 for(var i in this.options.thread._parents[0].messages){
690                     var res=this.options.thread._parents[0].messages[i].browse_message(options);
691                     if(res) return res;
692                 }
693             }
694
695             if(this.datasets.id==options.id)
696                 return this;
697
698             for(var i in this.thread.messages){
699                 if(this.thread.messages[i].thread){
700                     var res=this.thread.messages[i].browse_message(options);
701                     if(res) return res;
702                 }
703             }
704
705             return false;
706         },
707
708         /* get all child message/thread id linked
709         */
710         get_child_ids: function(){
711             var res=[]
712             if(arguments[0]) res.push(this.datasets.id);
713             if(this.thread){
714                 res = res.concat( this.thread.get_child_ids(true) );
715             }
716             return res;
717         },
718
719         on_vote: function (event) {
720             event.stopPropagation();
721             var self=this;
722             return this.ds_message.call('vote_toggle', [[self.datasets.id]]).pipe(function(vote){
723
724                 self.datasets.has_voted=vote;
725                 if (!self.datasets.has_voted) {
726                     var votes=[];
727                     for(var i in self.datasets.vote_user_ids){
728                         if(self.datasets.vote_user_ids[i][0]!=self.datasets.session.uid)
729                             vote.push(self.datasets.vote_user_ids[i]);
730                     }
731                     self.datasets.vote_user_ids=votes;
732                 }
733                 else {
734                     self.datasets.vote_user_ids.push([self.session.uid, 'You']);
735                 }
736                 self.display_vote();
737             });
738             return false;
739         },
740
741         // Render vote Display template.
742         display_vote: function () {
743             var self = this;
744             var vote_element = session.web.qweb.render('mail.thread.message.vote', {'widget': self});
745             self.$(".placeholder-mail-vote:first").empty();
746             self.$(".placeholder-mail-vote:first").html(vote_element);
747         },
748
749         // Stared/unstared + Render star.
750         on_star: function (event) {
751             event.stopPropagation();
752             var self=this;
753             var button = self.$('button.oe_mail_starbox:first');
754             return this.ds_message.call('favorite_toggle', [[self.datasets.id]]).pipe(function(star){
755                 self.datasets.is_favorite=star;
756                 if(self.datasets.is_favorite){
757                     button.addClass('oe_stared');
758                 } else {
759                     button.removeClass('oe_stared');
760                     if( self.options.thread.typeof_thread == 'stared' ) {
761                         self.animated_destroy({fadeTime:250});
762                     }
763                 }
764             });
765             return false;
766         },
767
768     });
769
770     /** 
771      * ------------------------------------------------------------
772      * Thread Widget
773      * ------------------------------------------------------------
774      *
775      * This widget handles the display of a thread of messages. The
776      * thread view:
777      * - root thread
778      * - - sub message (parent_id = root message)
779      * - - - sub thread
780      * - - - - sub sub message (parent id = sub thread)
781      * - - sub message (parent_id = root message)
782      * - - - sub thread
783      */
784     mail.Thread = session.web.Widget.extend({
785         template: 'mail.thread',
786
787         /**
788          * @param {Object} parent parent
789          * @param {Array} [domain]
790          * @param {Object} [context] context of the thread. It should
791             contain at least default_model, default_res_id. Please refer to
792             the ComposeMessage widget for more information about it.
793          * @param {Object} [options]
794          *      @param {Object} [message] read about mail.ThreadMessage object
795          *      @param {Object} [thread]
796          *          @param {Boolean} [use_composer] use the advanced composer, or
797          *              the default basic textarea if not set
798          *          @param {Number} [expandable_number=5] number message show
799          *              for each click on "show more message"
800          *          @param {Number} [expandable_default_number=5] number message show
801          *              on begin before the first click on "show more message"
802          *          @param {Array [A,B]} [display_on_thread] display the threads (hierarchy)
803          *              for the thread level between A and B. -1 for no begin or no end.
804          *              All thread before A are insert in the root thread.
805          *              All thread after B are insert in parent thread on B level.
806          *          @param {Select} [typeof_thread] inbox/archives/stared/sent
807          *              type of thread and option for user application like animate
808          *              destroy for read/unread
809          *          @param {Array} [parents] liked with the parents thread
810          *              use with browse, fetch... [O]= top parent
811          */
812         init: function(parent, options) {
813             this._super(parent);
814             this.domain = options.domain || [];
815             this.context = _.extend({
816                 default_model: 'mail.thread',
817                 default_res_id: 0,
818                 default_parent_id: false }, options.context || {});
819
820             // options
821             this.options={
822                 'thread' : {
823                     'show_header_compose': (options.options.thread.show_header_compose != undefined ? options.options.thread.show_header_compose: false),
824                     'use_composer': options.options.thread.use_composer || false,
825                     'expandable_number': options.options.thread.expandable_number || 5,
826                     'expandable_default_number': options.options.thread.expandable_default_number || 5,
827                     '_expandable_max': options.options.thread.expandable_default_number || 5,
828                     'display_on_thread': options.options.thread.display_on_thread || [0,-1],
829                     'typeof_thread': options.options.thread.typeof_thread || 'inbox',
830                     '_parents': (options.options.thread._parents != undefined ? options.options.thread._parents : []).concat( [this] )
831                 },
832                 'message' : options.options.message
833             };
834
835             // record options and data
836             this.parent_message= parent.thread!= undefined ? parent : false ;
837
838             var param = options.datasets
839             // datasets and internal vars
840             this.datasets = {
841                 'id' : param.id || false,
842                 'model' : param.model || false,
843                 'parent_id' : param.parent_id || false,
844                 'is_private' : param.is_private || false,
845                 'author_id' : param.author_id || false,
846                 'thread_level' : (param.thread_level+1) || 0,
847                 'partner_ids' : []
848             };
849
850             for(var i in param.partner_ids){
851                 if(param.partner_ids[i][0]!=(param.author_id ? param.author_id[0] : -1)){
852                     this.datasets.partner_ids.push(param.partner_ids[i]);
853                 }
854             }
855
856             this.messages = [];
857             this.ComposeMessage = false;
858
859             this.ds_thread = new session.web.DataSetSearch(this, this.context.default_model || 'mail.thread');
860             this.ds_message = new session.web.DataSetSearch(this, 'mail.message');
861         },
862         
863         start: function() {
864             this._super.apply(this, arguments);
865
866             this.list_ul = this.$('ul.oe_mail_thread_display:first');
867             this.more_msg = this.$(">.oe_msg_more_message:first");
868
869             this.display_user_avatar();
870             var display_done = compose_done = false;
871             
872             this.bind_events();
873
874             if(this.options.thread._parents[0]==this){
875                 this.on_root_thread();
876             }
877
878             return display_done && compose_done;
879         },
880
881         instantiate_ComposeMessage: function() {
882             // add message composition form view
883             this.ComposeMessage = new mail.ThreadComposeMessage(this,{
884                 'context': this.context,
885                 'datasets': this.datasets,
886                 'options': this.options,
887                 'show_attachment_delete': true,
888             });
889             this.ComposeMessage.appendTo(this.$(".oe_mail_thread_action:first"));
890         },
891
892         /* this method is runing for first parent thread
893         */
894         on_root_thread: function(){
895             var self=this;
896             // fetch and display message, using message_ids if set
897             this.message_fetch();
898
899             $(document).scroll( self.on_scroll );
900             $(window).resize( self.on_scroll );
901             window.setTimeout( self.on_scroll, 500 );
902
903             $(session.web.qweb.render('mail.wall_no_message', {})).appendTo(this.$('ul.oe_mail_thread_display'));
904
905             this.instantiate_ComposeMessage();
906             this.ComposeMessage.datasets.is_private=true;
907
908             if(this.options.thread.show_header_compose){
909                 this.ComposeMessage.$el.show();
910                 //this.ComposeMessage.set_free_attachments();
911             }
912
913             this.$el.addClass("oe_mail_root_thread");
914         },
915
916         /* When the expandable object is visible on screen (with scrolling)
917          * then the on_expandable function is launch
918         */
919         on_scroll: function(event){
920             if(event)event.stopPropagation();
921             var message = this.messages[0];
922             if(message && message.datasets.type=="expandable" && message.datasets.max_limit){
923                 var pos = message.$el.position();
924                 if(pos.top){
925                     /* bottom of the screen */
926                     var bottom = $(window).scrollTop()+$(window).height()+200;
927                     if(bottom - pos.top > 0){
928                         message.on_expandable();
929                     }
930                 }
931
932             }
933         },
934
935         /**
936          * Bind events in the widget. Each event is slightly described
937          * in the function. */
938         bind_events: function() {
939             var self = this;
940             self.$('.oe_mail_compose_textarea .oe_more').click(function () { var p=$(this).parent(); p.find('.oe_more_hidden, .oe_hidden').show(); p.find('.oe_more').hide(); });
941             self.$('.oe_mail_compose_textarea .oe_more_hidden').click(function () { var p=$(this).parent(); p.find('.oe_more_hidden, .oe_hidden').hide(); p.find('.oe_more').show(); });
942         },
943
944         /* get all child message/thread id linked
945         */
946         get_child_ids: function(){
947             var res=[];
948             _(this.get_childs()).each(function (val, key) { res.push(val.datasets.id); });
949             return res;
950         },
951
952         /* get all child message/thread linked
953         */
954         get_childs: function(nb_thread_level){
955             var res=[];
956             if(arguments[1]) res.push(this);
957             if(isNaN(nb_thread_level) || nb_thread_level>0){
958                 _(this.messages).each(function (val, key) {
959                     if(val.thread){
960                         res = res.concat( val.thread.get_childs((isNaN(nb_thread_level) ? null : nb_thread_level-1), true) ) 
961                     }
962                 });
963             }
964             return res;
965         },
966
967         /** browse thread
968          * @param {object}{int} option.id
969          * @param {object}{string} option.model
970          * @param {object}{boolean} option._go_thread_wall
971          *      private for check the top thread
972          * @param {object}{boolean} option.default_return_top_thread
973          *      return the top thread (wall) if no thread found
974          * @return thread object
975          */
976         browse_thread: function(options){
977             // goto the wall thread for launch browse
978             if(!options._go_thread_wall) {
979                 options._go_thread_wall = true;
980                 return this.options.thread._parents[0].browse_thread(options);
981             }
982
983             if(this.datasets.id==options.id){
984                 return this;
985             }
986
987             if(options.id)
988             for(var i in this.messages){
989                 if(this.messages[i].thread){
990                     var res=this.messages[i].thread.browse_thread({'id':options.id, '_go_thread_wall':true});
991                     if(res) return res;
992                 }
993             }
994
995             //if option default_return_top_thread, return the top if no found thread
996             if(options.default_return_top_thread){
997                 return this;
998             }
999
1000             return false;
1001         },
1002
1003         /** browse message
1004          * @param {object}{int} option.id
1005          * @param {object}{string} option.model
1006          * @param {object}{boolean} option._go_thread_wall
1007          *      private for check the top thread
1008          * @return thread object
1009          */
1010         browse_message: function(options){
1011             if(this.options.thread._parents[0].messages[0])
1012                 return this.options.thread._parents[0].messages[0].browse_message(options);
1013         },
1014
1015         /* this function is launch when a user click on "Reply" button
1016         */
1017         on_compose_message: function(){
1018             if(!this.ComposeMessage){
1019                 this.instantiate_ComposeMessage();
1020             }
1021             this.ComposeMessage.$el.toggle();
1022             return false;
1023         },
1024
1025         /** Fetch messages
1026          * @param {Bool} initial_mode: initial mode: try to use message_data or
1027          *  message_ids, if nothing available perform a message_read; otherwise
1028          *  directly perform a message_read
1029          * @param {Array} replace_domain: added to this.domain
1030          * @param {Object} replace_context: added to this.context
1031          */
1032         message_fetch: function (initial_mode, replace_domain, replace_context, ids, callback) {
1033             var self = this;
1034
1035             // initial mode: try to use message_data or message_ids
1036             if (initial_mode && this.options.thread.message_data) {
1037                 return this.create_message_object(this.options.message_data);
1038             }
1039             // domain and context: options + additional
1040             fetch_domain = replace_domain ? replace_domain : this.domain;
1041             fetch_context = replace_context ? replace_context : this.context;
1042             var message_loaded = [this.datasets.id||0].concat( self.options.thread._parents[0].get_child_ids() );
1043
1044             return this.ds_message.call('message_read', [ids, fetch_domain, message_loaded, fetch_context, this.context.default_parent_id || undefined]
1045                 ).then(this.proxy('switch_new_message'));
1046         },
1047
1048         /* create record object and linked him
1049          */
1050         create_message_object: function (data) {
1051             var self = this;
1052
1053             if(data.type=='expandable'){
1054                 var message = new mail.ThreadExpandable(self, {
1055                     'domain': data.domain,
1056                     'context': {
1057                         'default_model': data.model || self.context.default_model,
1058                         'default_res_id': data.res_id || self.context.default_res_id,
1059                         'default_parent_id': self.datasets.id },
1060                     'datasets': data
1061                 });
1062             } else {
1063                 var message = new mail.ThreadMessage(self, {
1064                     'domain': data.domain,
1065                     'context': {
1066                         'default_model': data.model,
1067                         'default_res_id': data.res_id,
1068                         'default_parent_id': data.id },
1069                     'options':{
1070                         'thread': self.options.thread,
1071                         'message': self.options.message
1072                     },
1073                     'datasets': _.extend(data, {'thread_level': self.datasets.thread_level})
1074                 });
1075                 var data = _.extend(data, {'thread_level': self.datasets.thread_level});
1076             }
1077
1078             // check if the message is already create
1079             for(var i in self.messages){
1080                 if(self.messages[i].datasets.id==message.datasets.id){
1081                     self.messages[i].destroy();
1082                     self.messages[i]=self.insert_message(message);
1083                     return true;
1084                 }
1085             }
1086             self.messages.push( self.insert_message(message) );
1087         },
1088
1089         /** Displays a message or an expandable message  */
1090         insert_message: function (message) {
1091             var self=this;
1092
1093             this.$("li.oe_wall_no_message").remove();
1094
1095             // insert on hierarchy display => insert in self child
1096             var thread_messages = self.messages;
1097             var thread = self;
1098             var flat = false;
1099             var hierarchy = self.options.thread.display_on_thread;
1100             if( hierarchy[0] < 0 ||
1101                 hierarchy[0] > self.datasets.thread_level ||
1102                 (hierarchy[1]>0 && hierarchy[1] < self.datasets.thread_level) ) {
1103
1104                 var flat = true;
1105
1106                 if(hierarchy[0]<0){
1107                 
1108                     // all is in flat mode
1109                     thread =  self.options.thread._parents[0];
1110                     var nb_thread_level = null;
1111                 
1112                 } else if(hierarchy[0] > self.datasets.thread_level) {
1113                  
1114                     // list all childs messages for flat display before the hierarchy
1115                     thread =  self.options.thread._parents[0];
1116                     var nb_thread_level = hierarchy[0];
1117                 
1118                 } else if(hierarchy[1] < self.datasets.thread_level) {
1119                 
1120                     // list all childs messages for flat display after the hierarchy
1121                     thread =  self.options.thread._parents[hierarchy[1]];
1122                     var nb_thread_level = hierarchy[1]>0 ? hierarchy[1]-hierarchy[0] : null;
1123                 } else {
1124
1125                     thread =  self.options.thread._parents[0];
1126                     var nb_thread_level = null;
1127                 }
1128
1129                 var thread_messages = [];
1130                 _(thread.get_childs( nb_thread_level )).each(function (val, key) { thread_messages.push(val.parent_message); });
1131             }
1132
1133
1134             // check older and newer message for insert
1135             var parent_newer = false;
1136             var parent_older = false;
1137             if ( message.datasets.id > 0 ){
1138                 for(var i in thread_messages){
1139                     if(thread_messages[i].datasets.id > message.datasets.id){
1140                         if(!parent_newer || parent_newer.datasets.id>=thread_messages[i].datasets.id)
1141                             parent_newer = thread_messages[i];
1142                     } else if(thread_messages[i].datasets.id>0 && thread_messages[i].datasets.id < message.datasets.id) {
1143                         if(!parent_older || parent_older.id<thread_messages[i].datasets.id)
1144                             parent_older = thread_messages[i];
1145                     }
1146                 }
1147             }
1148
1149             var sort = self.datasets.thread_level==0 || (flat && self.datasets.thread_level>=1);
1150
1151             if(parent_older){
1152                 if(sort){
1153                     message.insertBefore(parent_older.$el);
1154                 } else {
1155                     message.insertAfter(parent_older.$el);
1156                 }
1157             } else if(parent_newer){
1158                 if(sort){
1159                     message.insertAfter(parent_newer.$el);
1160                 } else {
1161                     message.insertBefore(parent_newer.$el);
1162                 }
1163             } else {
1164                 if(sort && message.id > 0){
1165                     message.prependTo(thread.list_ul);
1166                 } else {
1167                     message.appendTo(thread.list_ul);
1168                 }
1169             }
1170
1171             return message
1172         },
1173
1174         display_user_avatar: function () {
1175             var avatar = mail.ChatterUtils.get_image(this.session, 'res.users', 'image_small', this.session.uid);
1176             return this.$('img.oe_mail_icon').attr('src', avatar);
1177         },
1178         
1179         /*  Send the records to his parent thread */
1180         switch_new_message: function(records) {
1181             var self=this;
1182             _(records).each(function(record){
1183                 self.browse_thread({
1184                     'id': record.parent_id, 
1185                     'default_return_top_thread':true
1186                 }).create_message_object( record );
1187             });
1188         },
1189     });
1190
1191
1192     /** 
1193      * ------------------------------------------------------------
1194      * mail_thread Widget
1195      * ------------------------------------------------------------
1196      *
1197      * This widget handles the display of messages on a document. Its main
1198      * use is to receive a context and a domain, and to delegate the message
1199      * fetching and displaying to the Thread widget.
1200      */
1201     session.web.form.widgets.add('mail_thread', 'openerp.mail.RecordThread');
1202     mail.RecordThread = session.web.form.AbstractField.extend({
1203         template: 'mail.record_thread',
1204
1205         init: function() {
1206             this._super.apply(this, arguments);
1207             this.options.domain = this.options.domain || [];
1208             this.options.context = {'default_model': 'mail.thread', 'default_res_id': false};
1209         },
1210
1211         start: function() {
1212             this._super.apply(this, arguments);
1213             // NB: check the actual_mode property on view to know if the view is in create mode anymore
1214             this.view.on("change:actual_mode", this, this._check_visibility);
1215             this._check_visibility();
1216         },
1217
1218         _check_visibility: function() {
1219             this.$el.toggle(this.view.get("actual_mode") !== "create");
1220         },
1221         render_value: function() {
1222             var self = this;
1223             if (! this.view.datarecord.id || session.web.BufferedDataSet.virtual_id_regex.test(this.view.datarecord.id)) {
1224                 this.$('oe_mail_thread').hide();
1225                 return;
1226             }
1227             // update context
1228             _.extend(this.options.context, {
1229                 default_res_id: this.view.datarecord.id,
1230                 default_model: this.view.model,
1231                 default_is_private: false });
1232             // update domain
1233             var domain = this.options.domain.concat([['model', '=', this.view.model], ['res_id', '=', this.view.datarecord.id]]);
1234             // create and render Thread widget
1235             // TDE note: replace message_is_follower by a check in message_follower_ids, as message_is_follower is not used in views anymore
1236             var show_header_compose = this.view.is_action_enabled('edit') ||
1237                 (this.getParent().fields.message_is_follower && this.getParent().fields.message_is_follower.get_value());
1238
1239             if(this.thread){
1240                 this.thread.destroy();
1241             }
1242             this.thread = new mail.Thread(self, {
1243                     'domain': domain,
1244                     'context': this.options.context,
1245                     'options':{
1246                         'thread':{
1247                             'show_header_compose': show_header_compose,
1248                             'use_composer': show_header_compose,
1249                             'display_on_thread':[-1,-1]
1250                         },
1251                         'message':{
1252                             'show_reply': [-1,-1],
1253                             'show_read_unread': [-1,-1],
1254                             'show_dd_delete': false
1255                         }
1256                     },
1257                     'datasets': {},
1258                 }
1259             );
1260             return this.thread.appendTo( this.$('.oe_mail_wall_threads:first') );
1261         },
1262     });
1263
1264
1265     /** 
1266      * ------------------------------------------------------------
1267      * Wall Widget
1268      * ------------------------------------------------------------
1269      *
1270      * This widget handles the display of messages on a Wall. Its main
1271      * use is to receive a context and a domain, and to delegate the message
1272      * fetching and displaying to the Thread widget.
1273      */
1274     session.web.client_actions.add('mail.wall', 'session.mail.Wall');
1275     mail.Wall = session.web.Widget.extend({
1276         template: 'mail.wall',
1277
1278         /**
1279          * @param {Object} parent parent
1280          * @param {Object} [options]
1281          * @param {Array} [options.domain] domain on the Wall
1282          * @param {Object} [options.context] context, is an object. It should
1283          *      contain default_model, default_res_id, to give it to the threads.
1284          * @param {Number} [options.thread_level] number of thread levels to display
1285          *      0 being flat.
1286          */
1287         init: function (parent, options) {
1288             this._super(parent);
1289             this.options = options || {};
1290             this.options.domain = options.domain || [];
1291             this.options.context = options.context || {};
1292             this.search_results = {'domain': [], 'context': {}, 'groupby': {}}
1293             this.ds_msg = new session.web.DataSetSearch(this, 'mail.message');
1294         },
1295
1296         start: function () {
1297             this._super.apply(this, arguments);
1298             var searchview_ready = this.load_searchview({}, false);
1299             var thread_displayed = this.message_render();
1300             this.options.domain = this.options.domain.concat(this.search_results['domain']);
1301             this.bind_events();
1302             return (searchview_ready && thread_displayed);
1303         },
1304
1305         /**
1306          * Load the mail.message search view
1307          * @param {Object} defaults ??
1308          * @param {Boolean} hidden some kind of trick we do not care here
1309          */
1310         load_searchview: function (defaults, hidden) {
1311             var self = this;
1312             this.searchview = new session.web.SearchView(this, this.ds_msg, false, defaults || {}, hidden || false);
1313             return this.searchview.appendTo(this.$('.oe_view_manager_view_search')).then(function () {
1314                 self.searchview.on('search_data', self, self.do_searchview_search);
1315             });
1316         },
1317
1318         /**
1319          * Get the domains, contexts and groupbys in parameter from search
1320          * view, then render the filtered threads.
1321          * @param {Array} domains
1322          * @param {Array} contexts
1323          * @param {Array} groupbys
1324          */
1325         do_searchview_search: function(domains, contexts, groupbys) {
1326             var self = this;
1327             this.rpc('/web/session/eval_domain_and_context', {
1328                 domains: domains || [],
1329                 contexts: contexts || [],
1330                 group_by_seq: groupbys || []
1331             }).then(function (results) {
1332                 self.search_results['context'] = results.context;
1333                 self.search_results['domain'] = results.domain;
1334                 self.thread.destroy();
1335                 return self.message_render();
1336             });
1337         },
1338
1339
1340         /**
1341          * Display the threads
1342           */
1343         message_render: function (search) {
1344             var domain = this.options.domain.concat(this.search_results['domain']);
1345             var context = _.extend(this.options.context, search&&search.search_results['context'] ? search.search_results['context'] : {});
1346             this.thread = new mail.Thread(this, {
1347                     'domain' : domain,
1348                     'context' : context,
1349                     'options': {
1350                         'thread' :{
1351                             'use_composer': true,
1352                             'show_header_compose': false,
1353                             'typeof_thread': context.typeof_thread || 'inbox',
1354                             'display_on_thread': [0,1]
1355                         },
1356                         'message': {
1357                             'show_reply': [0,0],
1358                             'show_read_unread': [0,-1],
1359                             'show_dd_delete': false,
1360                         },
1361                     },
1362                     'datasets': {},
1363                 }
1364             );
1365             return this.thread.appendTo( this.$('.oe_mail_wall_threads:first') );
1366
1367         },
1368
1369         bind_events: function(){
1370             var self=this;
1371             this.$("button.oe_write_full:first").click(function(){ self.thread.ComposeMessage.on_compose_fullmail(); });
1372             this.$("button.oe_write_onwall:first").click(function(){ self.thread.ComposeMessage.$el.toggle(); });
1373         }
1374     });
1375
1376
1377     /**
1378      * ------------------------------------------------------------
1379      * UserMenu
1380      * ------------------------------------------------------------
1381      * 
1382      * Add a link on the top user bar for write a full mail
1383      */
1384     session.web.ComposeMessageTopButton = session.web.Widget.extend({
1385         template:'mail.compose_message.button_top_bar',
1386
1387         init: function (parent, options) {
1388             this._super.apply(this, options);
1389             this.options = this.options || {};
1390             this.options.domain = this.options.domain || [];
1391             this.options.context = {
1392                 'default_model': false,
1393                 'default_res_id': 0,
1394                 'default_content_subtype': 'html',
1395             };
1396         },
1397
1398         start: function(parent, params) {
1399             var self = this;
1400             this.$el.on('click', 'button', self.on_compose_message );
1401             this._super(parent, params);
1402         },
1403
1404         on_compose_message: function(event){
1405             event.stopPropagation();
1406             var action = {
1407                 type: 'ir.actions.act_window',
1408                 res_model: 'mail.compose.message',
1409                 view_mode: 'form',
1410                 view_type: 'form',
1411                 action_from: 'mail.ThreadComposeMessage',
1412                 views: [[false, 'form']],
1413                 target: 'new',
1414                 context: this.options.context,
1415             };
1416             session.client.action_manager.do_action(action);
1417         },
1418
1419     });
1420
1421     session.web.UserMenu = session.web.UserMenu.extend({
1422         start: function(parent, params) {
1423             var render = new session.web.ComposeMessageTopButton();
1424             render.insertAfter(this.$el);
1425             this._super(parent, params);
1426         }
1427     });
1428
1429 };