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