[IMP] mail: review and reduce mail.RecordThread
[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_content_subtype', , 'default_subject',
27                     'default_body', 'active_id', 'lang', 'bin_raw', 'tz',
28                     'active_model', 'edi_web_url_view', 'active_ids']
29                 for (var key in action.context) {
30                     if (_.indexOf(context_keys, key) == -1) {
31                         action.context[key] = null;
32                     }
33                 }
34                 /* end hack */
35             }
36             return this._super.apply(this, arguments);
37         },
38     });
39
40
41     /**
42      * ------------------------------------------------------------
43      * ChatterUtils
44      * ------------------------------------------------------------
45      * 
46      * This class holds a few tools method for Chatter.
47      * Some regular expressions not used anymore, kept because I want to
48      * - (^|\s)@((\w|@|\.)*): @login@log.log
49      * - (^|\s)\[(\w+).(\w+),(\d)\|*((\w|[@ .,])*)\]: [ir.attachment,3|My Label],
50      *   for internal links
51      */
52
53     mail.ChatterUtils = {
54
55         /* Get an image in /web/binary/image?... */
56         get_image: function (session, model, field, id) {
57             return session.prefix + '/web/binary/image?session_id=' + session.session_id + '&model=' + model + '&field=' + field + '&id=' + (id || '');
58         },
59
60         /* Get the url of an attachment {'id': id} */
61         get_attachment_url: function (session, attachment) {
62             return session.origin + '/web/binary/saveas?session_id=' + session.session_id + '&model=ir.attachment&field=datas&filename_field=datas_fname&id=' + attachment['id'];
63         },
64
65         /**
66          * Replaces some expressions
67          * - :name - shortcut to an image
68          */
69         do_replace_expressions: function (string) {
70             var icon_list = ['al', 'pinky']
71             /* special shortcut: :name, try to find an icon if in list */
72             var regex_login = new RegExp(/(^|\s):((\w)*)/g);
73             var regex_res = regex_login.exec(string);
74             while (regex_res != null) {
75                 var icon_name = regex_res[2];
76                 if (_.include(icon_list, icon_name))
77                     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 + '"/>');
78                 regex_res = regex_login.exec(string);
79             }
80             return string;
81         },
82
83         /**
84          * Replaces textarea text into html text (add <p>, <a>)
85          * TDE note : should be done server-side, in Python -> use mail.compose.message ?
86          */
87         get_text2html: function (text) {
88             return text
89                 .replace(/[\n\r]/g,'<br/>')
90                 .replace(/((?:https?|ftp):\/\/[\S]+)/g,'<a href="$1">$1</a> ')
91         },
92
93         /* Returns the complete domain with "&" 
94          * TDE note: please add some comments to explain how/why
95          */
96         expand_domain: function (domain) {
97             var new_domain = [];
98             var nb_and = -1;
99             // TDE note: smarted code maybe ?
100             for ( var k = domain.length-1; k >= 0 ; k-- ) {
101                 if ( typeof domain[k] != 'array' && typeof domain[k] != 'object' ) {
102                     nb_and -= 2;
103                     continue;
104                 }
105                 nb_and += 1;
106             }
107
108             for (var k = 0; k < nb_and ; k++) {
109                 domain.unshift('&');
110             }
111
112             return domain;
113         }
114     };
115
116
117     /**
118      * ------------------------------------------------------------
119      * ComposeMessage widget
120      * ------------------------------------------------------------
121      * 
122      * This widget handles the display of a form to compose a new message.
123      * This form is a mail.compose.message form_view.
124      * On first time : display a compact textarea that is not the compose form.
125      * When the user focuses the textarea, the compose message is instantiated.
126      */
127     
128     mail.ThreadComposeMessage = session.web.Widget.extend({
129         template: 'mail.compose_message',
130
131         /**
132          * @param {Object} parent parent
133          * @param {Object} [options]
134          *      @param {Object} [context] context passed to the
135          *          mail.compose.message DataSetSearch. Please refer to this model
136          *          for more details about fields and default values.
137          */
138
139         init: function (parent, datasets, options) {
140             var self = this;
141             this._super(parent);
142             this.context = options.context || {};
143             this.options = options.options;
144
145             this.show_compact_message = false;
146
147             // data of this compose message
148             this.id = datasets.id;
149             this.model = datasets.model;
150             this.res_model = datasets.res_model;
151             this.is_private = datasets.is_private || false;
152             this.partner_ids = datasets.partner_ids || [];
153             this.thread_level = datasets.thread_level;
154
155             this.attachment_ids = [];
156             this.parent_thread= parent.messages!= undefined ? parent : false;
157             this.avatar = mail.ChatterUtils.get_image(this.session, 'res.users', 'image_small', this.session.uid);
158
159             this.ds_attachment = new session.web.DataSetSearch(this, 'ir.attachment');
160             this.show_delete_attachment = true;
161
162             this.fileupload_id = _.uniqueId('oe_fileupload_temp');
163             $(window).on(self.fileupload_id, self.on_attachment_loaded);
164         },
165
166         start: function () {
167             this.display_attachments();
168             this.bind_events();
169         },
170
171         /* upload the file on the server, add in the attachments list and reload display
172          */
173         display_attachments: function () {
174             this.$(".oe_msg_attachment_list").html( 
175                 session.web.qweb.render('mail.thread.message.attachments', {'widget': this}) );
176             // event: delete an attachment
177             this.$(".oe_msg_attachment_list").on('click', '.oe_mail_attachment_delete', this.on_attachment_delete);
178         },
179
180         /* when a user click on the upload button, send file read on_attachment_loaded
181         */
182         on_attachment_change: function (event) {
183             event.stopPropagation();
184             var self = this;
185             var $target = $(event.target);
186             if ($target.val() !== '') {
187
188                 var filename = $target.val().replace(/.*[\\\/]/,'');
189
190                 // if the files exits for this answer, delete the file before upload
191                 var attachments=[];
192                 for (var i in this.attachment_ids) {
193                     if ((this.attachment_ids[i].filename || this.attachment_ids[i].name) == filename) {
194                         if (this.attachment_ids[i].upload) {
195                             return false;
196                         }
197                         this.ds_attachment.unlink([this.attachment_ids[i].id]);
198                     } else {
199                         attachments.push(this.attachment_ids[i]);
200                     }
201                 }
202                 this.attachment_ids = attachments;
203
204                 // submit file
205                 this.$('form.oe_form_binary_form').submit();
206
207                 this.$(".oe_attachment_file").hide();
208
209                 this.attachment_ids.push({
210                     'id': 0,
211                     'name': filename,
212                     'filename': filename,
213                     'url': '',
214                     'upload': true
215                 });
216                 this.display_attachments();
217             }
218         },
219         
220         /* when the file is uploaded 
221         */
222         on_attachment_loaded: function (event, result) {
223             for (var i in this.attachment_ids) {
224                 if (this.attachment_ids[i].filename == result.filename && this.attachment_ids[i].upload) {
225                     this.attachment_ids[i]={
226                         'id': result.id,
227                         'name': result.name,
228                         'filename': result.filename,
229                         'url': mail.ChatterUtils.get_attachment_url(this.session, result)
230                     };
231                 }
232             }
233             this.display_attachments();
234
235             var $input = this.$('input.oe_form_binary_file');
236             $input.after($input.clone(true)).remove();
237             this.$(".oe_attachment_file").show();
238         },
239
240         /* unlink the file on the server and reload display
241          */
242         on_attachment_delete: function (event) {
243             event.stopPropagation();
244             var attachment_id=$(event.target).data("id");
245             if (attachment_id) {
246                 var attachments=[];
247                 for (var i in this.attachment_ids) {
248                     if (attachment_id!=this.attachment_ids[i].id) {
249                         attachments.push(this.attachment_ids[i]);
250                     }
251                     else {
252                         this.ds_attachment.unlink([attachment_id]);
253                     }
254                 }
255                 this.attachment_ids = attachments;
256                 this.display_attachments();
257             }
258         },
259
260         bind_events: function () {
261             var self = this;
262
263             this.$('textarea.oe_compact').on('focus', _.bind( this.on_compose_expandable, this));
264
265             // set the function called when attachments are added
266             this.$el.on('change', 'input.oe_form_binary_file', _.bind( this.on_attachment_change, this) );
267
268             this.$el.on('click', '.oe_cancel', _.bind( this.on_cancel, this) );
269             this.$el.on('click', '.oe_post', _.bind( this.on_message_post, this) );
270             this.$el.on('click', '.oe_full', _.bind( this.on_compose_fullmail, this, 'reply') );
271
272             /* stack for don't close the compose form if the user click on a button */
273             this.$el.on('mousedown', '.oe_msg_footer', _.bind( function () { this.stay_open = true; }, this));
274             this.$('textarea:not(.oe_compact):first').on('focus, mouseup, keydown', _.bind( function () { this.stay_open = false; }, this));
275             this.$('textarea:not(.oe_compact):first').autosize();
276
277             // auto close
278             this.$el.on('blur', 'textarea:not(.oe_compact):first', _.bind( this.on_compose_expandable, this));
279         },
280
281         on_compose_fullmail: function (default_composition_mode) {
282             if (default_composition_mode == 'reply') {
283                 var context = {
284                     'default_composition_mode': default_composition_mode,
285                     'default_parent_id': this.id,
286                     'default_body': mail.ChatterUtils.get_text2html(this.$el ? (this.$el.find('textarea:not(.oe_compact)').val() || '') : ''),
287                     'default_attachment_ids': this.attachment_ids,
288                 };
289             } else {
290                 var context = {
291                     'default_model': this.context.default_model,
292                     'default_res_id': this.context.default_res_id,
293                     'default_content_subtype': 'html',
294                     'default_composition_mode': default_composition_mode,
295                     'default_parent_id': this.id,
296                     'default_body': mail.ChatterUtils.get_text2html(this.$el ? (this.$el.find('textarea:not(.oe_compact)').val() || '') : ''),
297                     'default_attachment_ids': this.attachment_ids,
298                 };
299             }
300             var action = {
301                 type: 'ir.actions.act_window',
302                 res_model: 'mail.compose.message',
303                 view_mode: 'form',
304                 view_type: 'form',
305                 action_from: 'mail.ThreadComposeMessage',
306                 views: [[false, 'form']],
307                 target: 'new',
308                 context: context,
309             };
310
311             this.do_action(action);
312             this.on_cancel();
313         },
314
315         reinit: function() {
316             var $render = $( session.web.qweb.render('mail.compose_message', {'widget': this}) );
317
318             $render.insertAfter(this.$el.last());
319             this.$el.remove();
320             this.$el = $render;
321
322             this.display_attachments();
323             this.bind_events();
324         },
325
326         on_cancel: function (event) {
327             if (event) event.stopPropagation();
328             this.attachment_ids=[];
329             this.stay_open = false;
330             this.show_composer = false;
331             this.reinit();
332         },
333
334         /*post a message and fetch the message*/
335         on_message_post: function (event) {
336             var self = this;
337
338             var comment_node =  this.$('textarea');
339             var body = comment_node.val();
340             comment_node.val('');
341
342             var attachments=[];
343             for (var i in this.attachment_ids) {
344                 if (this.attachment_ids[i].upload) {
345                     session.web.dialog($('<div>' + session.web.qweb.render('CrashManager.warning', {message: 'Please, wait while the file is uploading.'}) + '</div>'));
346                     return false;
347                 }
348                 attachments.push(this.attachment_ids[i].id);
349             }
350
351             if (body.match(/\S+/)) {
352                 //session.web.blockUI();
353                 this.parent_thread.ds_thread.call('message_post_api', [
354                         this.context.default_res_id, 
355                         mail.ChatterUtils.get_text2html(body), 
356                         false, 
357                         this.context.default_parent_id, 
358                         attachments,
359                         this.parent_thread.context
360                     ]).then(function (record) {
361                         var thread = self.parent_thread;
362                         // create object and attach to the thread object
363                         thread.message_fetch(false, false, [record], function (arg, data) {
364                             var message = thread.create_message_object( data[0] );
365                             // insert the message on dom
366                             thread.insert_message( message, self.$el );
367                             if (thread.parent_message) {
368                                 self.$el.remove();
369                                 self.parent_thread.compose_message = null;
370                             } else {
371                                 self.on_cancel();
372                             }
373                         });
374                         //session.web.unblockUI();
375                     });
376                 return true;
377             }
378         },
379
380         /* convert the compact mode into the compose message
381         */
382         on_compose_expandable: function (event) {
383
384             if (!this.stay_open && (!this.show_composer || !this.$('textarea:not(.oe_compact)').val().match(/\S+/))) {
385                 this.show_composer = !this.show_composer || this.stay_open;
386                 this.reinit();
387             }
388             if (!this.stay_open && this.show_composer) {
389                 this.$('textarea:not(.oe_compact):first').focus();
390             }
391             return true;
392         },
393
394         do_hide_compact: function () {
395             this.show_compact_message = false;
396             if (!this.show_composer) {
397                 this.reinit();
398             }
399         },
400
401         do_show_compact: function () {
402             this.show_compact_message = true;
403             if (!this.show_composer) {
404                 this.reinit();
405             }
406         }
407     });
408
409     /**
410      * ------------------------------------------------------------
411      * Thread Message Expandable Widget
412      * ------------------------------------------------------------
413      *
414      * This widget handles the display the expandable message in a thread.
415      * - thread
416      * - - visible message
417      * - - expandable
418      * - - visible message
419      * - - visible message
420      * - - expandable
421      */
422     mail.ThreadExpandable = session.web.Widget.extend({
423         template: 'mail.thread.expandable',
424
425         init: function (parent, datasets, context) {
426             this._super(parent);
427             this.domain = datasets.domain || [];
428             this.options = datasets.options;
429             this.context = _.extend({
430                 default_model: 'mail.thread',
431                 default_res_id: 0,
432                 default_parent_id: false }, context || {});
433
434             // data of this expandable message
435             this.id = datasets.id || -1,
436             this.model = datasets.model || false,
437             this.parent_id = datasets.parent_id || false,
438             this.nb_messages = datasets.nb_messages || 0,
439             this.thread_level = datasets.thread_level || 0,
440             this.type = 'expandable',
441             this.max_limit = this.id < 0 || false,
442             this.flag_used = false,
443             this.parent_thread= parent.messages!= undefined ? parent : this.options.root_thread;
444         },
445
446         
447         start: function () {
448             this._super.apply(this, arguments);
449             this.bind_events();
450         },
451
452         reinit: function () {
453             var $render = $(session.web.qweb.render('mail.thread.expandable', {'widget': this}));
454             this.$el.replaceWith( $render );
455             this.$el = $render;
456             this.bind_events();
457         },
458
459         /**
460          * Bind events in the widget. Each event is slightly described
461          * in the function. */
462         bind_events: function () {
463             this.$el.on('click', 'a.oe_msg_fetch_more', this.on_expandable);
464         },
465
466         animated_destroy: function (fadeTime) {
467             var self=this;
468             this.$el.fadeOut(fadeTime, function () {
469                 self.destroy();
470             });
471         },
472
473         /*The selected thread and all childs (messages/thread) became read
474         * @param {object} mouse envent
475         */
476         on_expandable: function (event) {
477             if (event)event.stopPropagation();
478             if (this.flag_used) {
479                 return false
480             }
481             this.flag_used = true;
482
483             this.animated_destroy(200);
484             this.parent_thread.message_fetch(this.domain, this.context);
485             return false;
486         },
487
488         /**
489          * call on_message_delete on his parent thread
490         */
491         destroy: function () {
492
493             this._super();
494             this.parent_thread.on_message_detroy(this);
495
496         }
497     });
498
499     /**
500      * ------------------------------------------------------------
501      * Thread Message Widget
502      * ------------------------------------------------------------
503      * This widget handles the display of a messages in a thread. 
504      * Displays a record and performs some formatting on the record :
505      * - record.date: formatting according to the user timezone
506      * - record.timerelative: relative time givein by timeago lib
507      * - record.avatar: image url
508      * - record.attachment_ids[].url: url of each attachmentThe
509      * thread view :
510      * - root thread
511      * - - sub message (parent_id = root message)
512      * - - - sub thread
513      * - - - - sub sub message (parent id = sub thread)
514      * - - sub message (parent_id = root message)
515      * - - - sub thread
516      */
517     mail.ThreadMessage = session.web.Widget.extend({
518         template: 'mail.thread.message',
519
520         /**
521          * @param {Object} parent parent
522          * @param {Array} [domain]
523          * @param {Object} [context] context of the thread. It should
524             contain at least default_model, default_res_id. Please refer to
525             the ComposeMessage widget for more information about it.
526          * @param {Object} [options]
527          *      @param {Object} [thread] read obout mail.Thread object
528          *      @param {Object} [message]
529          *          @param {Number} [truncate_limit=250] number of character to
530          *              display before having a "show more" link; note that the text
531          *              will not be truncated if it does not have 110% of the parameter
532          *          @param {Boolean} [show_record_name]
533          *...  @param {boolean} [show_reply_button] display the reply button
534          *...  @param {boolean} [show_read_unread_button] display the read/unread button
535          */
536         init: function (parent, datasets, context) {
537             this._super(parent);
538
539             // record domain and context
540             this.domain = datasets.domain || [];
541             this.context = _.extend({
542                 default_model: 'mail.thread',
543                 default_res_id: 0,
544                 default_parent_id: false }, context || {});
545
546             // record options
547             this.options = datasets.options || {};
548
549             // data of this message
550             this.id = datasets.id ||  -1,
551             this.model = datasets.model ||  false,
552             this.parent_id = datasets.parent_id ||  false,
553             this.res_id = datasets.res_id ||  false,
554             this.type = datasets.type ||  false,
555             this.is_author = datasets.is_author ||  false,
556             this.is_private = datasets.is_private ||  false,
557             this.subject = datasets.subject ||  false,
558             this.name = datasets.name ||  false,
559             this.record_name = datasets.record_name ||  false,
560             this.body = datasets.body ||  false,
561             this.vote_nb = datasets.vote_nb || 0,
562             this.has_voted = datasets.has_voted ||  false,
563             this.is_favorite = datasets.is_favorite ||  false,
564             this.thread_level = datasets.thread_level ||  0,
565             this.to_read = datasets.to_read || false,
566             this.author_id = datasets.author_id ||  [],
567             this.attachment_ids = datasets.attachment_ids ||  [],
568             this._date = datasets.date;
569
570             // record options and data
571             this.parent_thread= parent.messages!= undefined ? parent : this.options.root_thread;
572             this.thread = false;
573
574             if ( this.id > 0 ) {
575                 this.formating_data();
576             }
577
578             this.ds_notification = new session.web.DataSetSearch(this, 'mail.notification');
579             this.ds_message = new session.web.DataSetSearch(this, 'mail.message');
580             this.ds_follow = new session.web.DataSetSearch(this, 'mail.followers');
581         },
582
583         /* Convert date, timerelative and avatar in displayable data. */
584         formating_data: function () {
585
586             //formating and add some fields for render
587             this.date = session.web.format_value(this._date, {type:"datetime"});
588             this.timerelative = $.timeago(this.date);
589             if (this.type == 'email') {
590                 this.avatar = ('/mail/static/src/img/email_icon.png');
591             } else {
592                 this.avatar = mail.ChatterUtils.get_image(this.session, 'res.partner', 'image_small', this.author_id[0]);
593             }
594             for (var l in this.attachment_ids) {
595                 var attach = this.attachment_ids[l];
596                 attach['url'] = mail.ChatterUtils.get_attachment_url(this.session, attach);
597
598                 if ((attach.filename || attach.name).match(/[.](jpg|jpg|gif|png|tif|svg)$/i)) {
599                     attach.is_image = true;
600                     attach['url'] = mail.ChatterUtils.get_image(this.session, 'ir.attachment', 'datas', attach.id); 
601                 }
602             }
603         },
604         
605         start: function () {
606             this._super.apply(this, arguments);
607             this.expender();
608             this.$el.hide().fadeIn(750, function () {$(this).css('display', '');});
609             this.resize_img();
610             this.bind_events();
611             if(this.thread_level < this.options.display_indented_thread) {
612                 this.create_thread();
613             }
614             this.$('.oe_msg_attachments, .oe_msg_images').addClass("oe_hidden");
615         },
616
617         resize_img: function () {
618             var resize = function () {
619                 var h = $(this).height();
620                 var w = $(this).width();
621                 if ( h > 100 || w >100 ) {
622                     var ratio = 100 / (h > w ? h : w);
623                     $(this).attr("width", parseInt( w*ratio )).attr("height", parseInt( h*ratio ));
624                 }
625             };
626             this.$("img").load(resize).each(resize);
627         },
628
629         /**
630          * Bind events in the widget. Each event is slightly described
631          * in the function. */
632         bind_events: function () {
633             var self = this;
634
635             // event: click on 'Attachment(s)' in msg
636             this.$('.oe_mail_msg_view_attachments').on('click', function (event) {
637                 var attach = self.$('.oe_msg_attachments:first, .oe_msg_images:first');
638                 if ( self.$('.oe_msg_attachments:first').hasClass("oe_hidden") ) {
639                     attach.removeClass("oe_hidden");
640                 } else {
641                     attach.addClass("oe_hidden");
642                 }
643                 self.resize_img();
644             });
645             // event: click on icone 'Read' in header
646             this.$el.on('click', '.oe_read', this.on_message_read_unread);
647             // event: click on icone 'UnRead' in header
648             this.$el.on('click', '.oe_unread', this.on_message_read_unread);
649             // event: click on 'Delete' in msg side menu
650             this.$el.on('click', '.oe_msg_delete', this.on_message_delete);
651
652             // event: click on 'Reply' in msg
653             this.$el.on('click', '.oe_reply', this.on_message_reply);
654             // event: click on 'Vote' button
655             this.$el.on('click', '.oe_msg_vote', this.on_vote);
656             // event: click on 'starred/favorite' button
657             this.$el.on('click', '.oe_star', this.on_star);
658         },
659
660         /* Call the on_compose_message on the thread of this message. */
661         on_message_reply:function (event) {
662             event.stopPropagation();
663             this.create_thread();
664             this.thread.on_compose_message();
665             return false;
666         },
667
668         expender: function () {
669             this.$('.oe_msg_body:first').expander({
670                 slicePoint: this.options.truncate_limit,
671                 expandText: 'read more',
672                 userCollapseText: '[^]',
673                 detailClass: 'oe_msg_tail',
674                 moreClass: 'oe_mail_expand',
675                 lessClass: 'oe_mail_reduce',
676                 });
677         },
678
679         /**
680          * Instantiate the thread object of this message.
681          * Each message have only one thread.
682          */
683         create_thread: function () {
684             if (this.thread) {
685                 return false;
686             }
687             /*create thread*/
688             this.thread = new mail.Thread(this, this, {
689                     'domain': this.domain,
690                     'context':{
691                         'default_model': this.model,
692                         'default_res_id': this.res_id,
693                         'default_parent_id': this.id
694                     },
695                     'options': this.options
696                 }
697             );
698             /*insert thread in parent message*/
699             this.thread.insertAfter(this.$el);
700         },
701         
702         /**
703          * Fade out the message and his child thread.
704          * Then this object is destroyed.
705          */
706         animated_destroy: function (fadeTime) {
707             var self=this;
708             this.$el.fadeOut(fadeTime, function () {
709                 self.parent_thread.message_to_expandable(self);
710             });
711             if (this.thread) {
712                 this.thread.$el.fadeOut(fadeTime);
713             }
714         },
715
716         /**
717          * Wait a confirmation for delete the message on the DB.
718          * Make an animate destroy
719          */
720         on_message_delete: function (event) {
721             event.stopPropagation();
722             if (! confirm(_t("Do you really want to delete this message?"))) { return false; }
723             
724             this.animated_destroy(150);
725             // delete this message and his childs
726             var ids = [this.id].concat( this.get_child_ids() );
727             this.ds_message.unlink(ids);
728             return false;
729         },
730
731         /* Check if the message must be destroy and detroy it
732         * @param {callback} apply function
733         */
734         check_for_destroy: function () {
735             var domain = mail.ChatterUtils.expand_domain( this.options.root_thread.domain ).concat([["id", "=", this.id]]);
736             this.parent_thread.ds_message.call('message_read', [undefined, domain, [], 0, this.context, this.parent_thread.id])
737                 .then( _.bind(function (record) { if (!record || !record.length) this.animated_destroy(150); }, this) );
738         },
739
740         /*The selected thread and all childs (messages/thread) became read
741         * @param {object} mouse envent
742         */
743         on_message_read_unread: function (event) {
744             event.stopPropagation();
745             var self=this;
746
747             // if this message is read, all childs message display is read
748             this.ds_notification.call('set_message_read', [ [this.id].concat( this.get_child_ids() ) , this.to_read, this.context]).pipe(function () {
749                 self.$el.removeClass(self.to_read ? 'oe_msg_unread':'oe_msg_read').addClass(self.to_read ? 'oe_msg_read':'oe_msg_unread');
750                 self.to_read = !self.to_read;
751                 // check if the message must be display
752                 self.check_for_destroy();
753             });
754             return false;
755         },
756
757         /**
758          * search a message in all thread and child thread.
759          * This method return an object message.
760          * @param {object}{int} option.id
761          * @param {object}{string} option.model
762          * @param {object}{boolean} option._go_thread_wall
763          *      private for check the top thread
764          * @return thread object
765          */
766         browse_message: function (options) {
767             // goto the wall thread for launch browse
768             if (!options._go_thread_wall) {
769                 options._go_thread_wall = true;
770                 for (var i in this.options.root_thread.messages) {
771                     var res=this.options.root_thread.messages[i].browse_message(options);
772                     if (res) return res;
773                 }
774             }
775
776             if (this.id==options.id)
777                 return this;
778
779             for (var i in this.thread.messages) {
780                 if (this.thread.messages[i].thread) {
781                     var res=this.thread.messages[i].browse_message(options);
782                     if (res) return res;
783                 }
784             }
785
786             return false;
787         },
788
789         /* get all child message id linked.
790          * @return array of id
791         */
792         get_child_ids: function () {
793             var res=[]
794             if (arguments[0]) res.push(this.id);
795             if (this.thread) {
796                 res = res.concat( this.thread.get_child_ids(true) );
797             }
798             return res;
799         },
800
801         /**
802          * add or remove a vote for a message and display the result
803         */
804         on_vote: function (event) {
805             event.stopPropagation();
806             return this.ds_message.call('vote_toggle', [[this.id]]).pipe(
807                 _.bind(function (vote) {
808                     this.has_voted = vote;
809                     this.vote_nb += this.has_voted ? 1 : -1;
810                     this.display_vote();
811                 }, this));
812             return false;
813         },
814
815         /**
816          * Display the render of this message's vote
817         */
818         display_vote: function () {
819             var vote_element = session.web.qweb.render('mail.thread.message.vote', {'widget': this});
820             this.$(".oe_msg_footer:first .oe_mail_vote_count").remove();
821             this.$(".oe_msg_footer:first .oe_msg_vote").replaceWith(vote_element);
822         },
823
824         /**
825          * add or remove a favorite (or starred) for a message and change class on the DOM
826         */
827         on_star: function (event) {
828             event.stopPropagation();
829             var self=this;
830             var button = self.$('.oe_star:first');
831             return this.ds_message.call('favorite_toggle', [[self.id]]).pipe(function (star) {
832                 self.is_favorite=star;
833                 if (self.is_favorite) {
834                     button.addClass('oe_starred');
835                 } else {
836                     button.removeClass('oe_starred');
837                     // check if the message must be display
838                     self.check_for_destroy();
839                 }
840             });
841             return false;
842         },
843
844         /**
845          * call on_message_delete on his parent thread
846         */
847         destroy: function () {
848
849             this._super();
850             this.parent_thread.on_message_detroy(this);
851
852         }
853
854     });
855
856     /**
857          * ------------------------------------------------------------
858      * Thread Widget
859      * ------------------------------------------------------------
860      *
861      * This widget handles the display of a thread of messages. The
862      * thread view:
863      * - root thread
864      * - - sub message (parent_id = root message)
865      * - - - sub thread
866      * - - - - sub sub message (parent id = sub thread)
867      * - - sub message (parent_id = root message)
868      * - - - sub thread
869      */
870     mail.Thread = session.web.Widget.extend({
871         template: 'mail.thread',
872
873         /**
874          * @param {Object} parent parent
875          * @param {Array} [domain]
876          * @param {Object} [context] context of the thread. It should
877             contain at least default_model, default_res_id. Please refer to
878             the ComposeMessage widget for more information about it.
879          * @param {Object} [options]
880          *      @param {Object} [message] read about mail.ThreadMessage object
881          *      @param {Object} [thread]
882          *          @param {int} [display_indented_thread] number thread level to indented threads.
883          *              other are on flat mode
884          *          @param {Array} [parents] liked with the parents thread
885          *              use with browse, fetch... [O]= top parent
886          */
887         init: function (parent, datasets, options) {
888             this._super(parent);
889             this.domain = options.domain || [];
890             this.context = _.extend({
891                 default_model: 'mail.thread',
892                 default_res_id: 0,
893                 default_parent_id: false }, options.context || {});
894
895             this.options = options.options;
896             this.options.root_thread = (options.options.root_thread != undefined ? options.options.root_thread : this);
897
898             // record options and data
899             this.parent_message= parent.thread!= undefined ? parent : false ;
900
901             // data of this thread
902             this.id =  datasets.id || false,
903             this.model =  datasets.model || false,
904             this.parent_id =  datasets.parent_id || false,
905             this.is_private =  datasets.is_private || false,
906             this.author_id =  datasets.author_id || false,
907             this.thread_level =  (datasets.thread_level+1) || 0,
908             this.partner_ids =  _.filter(datasets.partner_ids, function (partner) { return partner[0]!=datasets.author_id[0]; } ) 
909             this.messages = [];
910             this.show_compose_message = this.options.show_compose_message && (this.options.show_reply_button > this.thread_level || !this.thread_level);
911
912             // object compose message
913             this.compose_message = false;
914
915             this.ds_thread = new session.web.DataSetSearch(this, this.context.default_model || 'mail.thread');
916             this.ds_message = new session.web.DataSetSearch(this, 'mail.message');
917         },
918         
919         start: function () {
920             this._super.apply(this, arguments);
921             this.bind_events();
922         },
923
924         /* instantiate the compose message object and insert this on the DOM.
925         * The compose message is display in compact form.
926         */
927         instantiate_compose_message: function () {
928             // add message composition form view
929             if (!this.compose_message) {
930                 this.compose_message = new mail.ThreadComposeMessage(this, this, {
931                     'context': this.context,
932                     'options': this.options,
933                 });
934                 if (!this.thread_level) {
935                     // root view
936                     this.compose_message.insertBefore(this.$el);
937                 } else if (this.thread_level > this.options.display_indented_thread) {
938                     this.compose_message.insertAfter(this.$el);
939                 } else {
940                     this.compose_message.appendTo(this.$el);
941                 }
942             }
943         },
944
945         /* When the expandable object is visible on screen (with scrolling)
946          * then the on_expandable function is launch
947         */
948         on_scroll: function (event) {
949             if (event)event.stopPropagation();
950             $last = this.$('> .oe_msg_expandable:last');
951             if ($last.hasClass('oe_max_limit')) {
952                 var pos = $last.position();
953                 if (pos.top) {
954                     /* bottom of the screen */
955                     var bottom = $(window).scrollTop()+$(window).height()+200;
956                     if (bottom > pos.top) {
957                         $last.find('.oe_msg_fetch_more').click();
958                     }
959                 }
960             }
961         },
962
963         /**
964          * Bind events in the widget. Each event is slightly described
965          * in the function. */
966         bind_events: function () {
967             var self = this;
968             self.$el.on('click', '.oe_mail_list_recipients .oe_more', self.on_show_recipients);
969             self.$el.on('click', '.oe_mail_compose_textarea .oe_more_hidden', self.on_hide_recipients);
970         },
971
972         /**
973          *show all the partner list of this parent message
974         */
975         on_show_recipients: function () {
976             var p=$(this).parent(); 
977             p.find('.oe_more_hidden, .oe_hidden').show(); 
978             p.find('.oe_more').hide(); 
979         },
980
981         /**
982          *hide a part of the partner list of this parent message
983         */
984         on_hide_recipients: function () {
985             var p=$(this).parent(); 
986             p.find('.oe_more_hidden, .oe_hidden').hide(); 
987             p.find('.oe_more').show(); 
988         },
989
990         /* get all child message/thread id linked.
991          * @return array of id
992         */
993         get_child_ids: function () {
994             var res=[];
995             _(this.get_childs()).each(function (val, key) { res.push(val.id); });
996             return res;
997         },
998
999         /* get all child message/thread linked.
1000          * @param {int} nb_thread_level, number of traversed thread level for this search
1001          * @return array of thread object
1002         */
1003         get_childs: function (nb_thread_level) {
1004             var res=[];
1005             if (arguments[1]) res.push(this);
1006             if (isNaN(nb_thread_level) || nb_thread_level>0) {
1007                 _(this.messages).each(function (val, key) {
1008                     if (val.thread) {
1009                         res = res.concat( val.thread.get_childs((isNaN(nb_thread_level) ? undefined : nb_thread_level-1), true) );
1010                     }
1011                 });
1012             }
1013             return res;
1014         },
1015
1016         /**
1017          *search a thread in all thread and child thread.
1018          * This method return an object thread.
1019          * @param {object}{int} option.id
1020          * @param {object}{string} option.model
1021          * @param {object}{boolean} option._go_thread_wall
1022          *      private for check the top thread
1023          * @param {object}{boolean} option.default_return_top_thread
1024          *      return the top thread (wall) if no thread found
1025          * @return thread object
1026          */
1027         browse_thread: function (options) {
1028             // goto the wall thread for launch browse
1029             if (!options._go_thread_wall) {
1030                 options._go_thread_wall = true;
1031                 return this.options.root_thread.browse_thread(options);
1032             }
1033
1034             if (this.id == options.id) {
1035                 return this;
1036             }
1037
1038             if (options.id) {
1039                 for (var i in this.messages) {
1040                     if (this.messages[i].thread) {
1041                         var res = this.messages[i].thread.browse_thread({'id':options.id, '_go_thread_wall':true});
1042                         if (res) return res;
1043                     }
1044                 }
1045             }
1046
1047             //if option default_return_top_thread, return the top if no found thread
1048             if (options.default_return_top_thread) {
1049                 return this;
1050             }
1051
1052             return false;
1053         },
1054
1055         /**
1056          *search a message in all thread and child thread.
1057          * This method return an object message.
1058          * @param {object}{int} option.id
1059          * @param {object}{string} option.model
1060          * @param {object}{boolean} option._go_thread_wall
1061          *      private for check the top thread
1062          * @return message object
1063          */
1064         browse_message: function (options) {
1065             if (this.options.root_thread.messages[0])
1066                 return this.options.root_thread.messages[0].browse_message(options);
1067         },
1068
1069         /**
1070          *If compose_message doesn't exist, instantiate the compose message.
1071         * Call the on_compose_expandable method to allow the user to write his message.
1072         * (Is call when a user click on "Reply" button)
1073         */
1074         on_compose_message: function () {
1075             this.instantiate_compose_message();
1076             this.compose_message.on_compose_expandable();
1077         },
1078
1079         /**
1080          *display the message "there are no message" on the thread
1081         */
1082         no_message: function () {
1083             var no_message = $(session.web.qweb.render('mail.wall_no_message', {}));
1084             if (this.options.no_message) {
1085                 no_message.html(this.options.no_message);
1086             }
1087             no_message.appendTo(this.$el);
1088         },
1089
1090         /**
1091          *make a request to read the message (calling RPC to "message_read").
1092          * The result of this method is send to the switch message for sending ach message to
1093          * his parented object thread.
1094          * @param {Array} replace_domain: added to this.domain
1095          * @param {Object} replace_context: added to this.context
1096          * @param {Array} ids read (if the are some ids, the method don't use the domain)
1097          */
1098         message_fetch: function (replace_domain, replace_context, ids, callback) {
1099             var self = this;
1100
1101             // domain and context: options + additional
1102             fetch_domain = replace_domain ? replace_domain : this.domain;
1103             fetch_context = replace_context ? replace_context : this.context;
1104             var message_loaded_ids = this.id ? [this.id].concat( self.get_child_ids() ) : self.get_child_ids();
1105
1106             // CHM note : option for sending in flat mode by server
1107             var thread_level = this.options.display_indented_thread > this.thread_level ? this.options.display_indented_thread - this.thread_level : 0;
1108
1109             return this.ds_message.call('message_read', [ids, fetch_domain, message_loaded_ids, thread_level, fetch_context, this.context.default_parent_id || undefined])
1110                 .then(callback ? _.bind(callback, this, arguments) : this.proxy('switch_new_message'));
1111         },
1112
1113         /**
1114          *create the message object and attached on this thread.
1115          * When the message object is create, this method call insert_message for,
1116          * displaying this message on the DOM.
1117          * @param : {object} data from calling RPC to "message_read"
1118          */
1119         create_message_object: function (data) {
1120             var self = this;
1121
1122             var data = _.extend(data, {'thread_level': data.thread_level ? data.thread_level : self.thread_level});
1123             data.options = _.extend(self.options, data.options);
1124
1125             if (data.type=='expandable') {
1126                 var message = new mail.ThreadExpandable(self, data, {
1127                     'default_model': data.model || self.context.default_model,
1128                     'default_res_id': data.res_id || self.context.default_res_id,
1129                     'default_parent_id': self.id,
1130                 });
1131             } else {
1132                 var message = new mail.ThreadMessage(self, data, {
1133                     'default_model': data.model,
1134                     'default_res_id': data.res_id,
1135                     'default_parent_id': data.id,
1136                 });
1137             }
1138
1139             // check if the message is already create
1140             for (var i in self.messages) {
1141                 if (self.messages[i] && self.messages[i].id == message.id) {
1142                     self.messages[i].destroy();
1143                 }
1144             }
1145             self.messages.push( message );
1146
1147             return message;
1148         },
1149
1150         /**
1151          *insert the message on the DOM.
1152          * All message (and expandable message) are sorted. The method get the
1153          * older and newer message to insert the message (before, after).
1154          * If there are no older or newer, the message is prepend or append to
1155          * the thread (parent object or on root thread for flat view).
1156          * The sort is define by the thread_level (O for newer on top).
1157          * @param : {object} ThreadMessage object
1158          */
1159         insert_message: function (message, dom_insert_after) {
1160             var self=this;
1161
1162             if (this.show_compose_message && this.options.show_compact_message) {
1163                 this.instantiate_compose_message();
1164                 this.compose_message.do_show_compact();
1165             }
1166
1167             this.$('.oe_wall_no_message').remove();
1168
1169
1170             if (dom_insert_after) {
1171                 message.insertAfter(dom_insert_after);
1172                 return message
1173             } 
1174
1175             // check older and newer message for insertion
1176             var message_newer = false;
1177             var message_older = false;
1178             if (message.id > 0) {
1179                 for (var i in self.messages) {
1180                     if (self.messages[i].id > message.id) {
1181                         if (!message_newer || message_newer.id > self.messages[i].id) {
1182                             message_newer = self.messages[i];
1183                         }
1184                     } else if (self.messages[i].id > 0 && self.messages[i].id < message.id) {
1185                         if (!message_older || message_older.id < self.messages[i].id) {
1186                             message_older = self.messages[i];
1187                         }
1188                     }
1189                 }
1190             }
1191
1192             var sort = (!!self.thread_level || message.id<0);
1193
1194             if (sort) {
1195                 if (message_older) {
1196
1197                     message.insertAfter(message_older.thread ? (message_older.thread.compose_message ? message_older.thread.compose_message.$el : message_older.thread.$el) : message_older.$el);
1198
1199                 } else if (message_newer) {
1200
1201                     message.insertBefore(message_newer.$el);
1202
1203                 } else if (message.id < 0) {
1204
1205                     message.appendTo(self.$el);
1206
1207                 } else {
1208
1209                     message.prependTo(self.$el);
1210                 }
1211             } else {
1212                 if (message_older) {
1213
1214                     message.insertBefore(message_older.$el);
1215
1216                 } else if (message_newer) {
1217
1218                     message.insertAfter(message_newer.thread ? (message_newer.thread.compose_message ? message_newer.thread.compose_message.$el : message_newer.thread.$el) : message_newer.$el );
1219
1220                 } else if (message.id < 0) {
1221
1222                     message.prependTo(self.$el);
1223
1224                 } else {
1225
1226                     message.appendTo(self.$el);
1227
1228                 }
1229             }
1230
1231             return message
1232         },
1233         
1234         /**
1235          *get the parent thread of the messages.
1236          * Each message is send to his parent object (or parent thread flat mode) for creating the object message.
1237          * @param : {Array} datas from calling RPC to "message_read"
1238          */
1239         switch_new_message: function (records) {
1240             var self=this;
1241             _(records).each(function (record) {
1242                 var thread = self.browse_thread({
1243                     'id': record.parent_id, 
1244                     'default_return_top_thread':true
1245                 });
1246                 // create object and attach to the thread object
1247                 var message = thread.create_message_object( record );
1248                 // insert the message on dom
1249                 thread.insert_message( message );
1250             });
1251         },
1252
1253         /**
1254          * this method is call when the widget of a message or an expandable message is destroy
1255          * in this thread. The this.messages array is filter to remove this message
1256          */
1257         on_message_detroy: function (message) {
1258
1259             this.messages = _.filter(this.messages, function (val) { return !val.isDestroyed(); });
1260
1261         },
1262
1263         /**
1264          * Convert a destroyed message into a expandable message
1265          */
1266         message_to_expandable: function (message) {
1267
1268             if (!this.thread_level || message.isDestroyed()) {
1269                 message.destroy();
1270                 return false;
1271             }
1272
1273             var messages = _.sortBy( this.messages, function (val) { return val.id; });
1274             var it = _.indexOf( messages, message );
1275
1276             var msg_up = messages[it-1];
1277             var msg_down = messages[it+1];
1278
1279             var message_dom = [ ["id", "=", message.id] ];
1280
1281             if ( msg_up && msg_up.type == "expandable" && msg_down && msg_down.type == "expandable") {
1282                 // concat two expandable message and add this message to this dom
1283                 msg_up.domain = mail.ChatterUtils.expand_domain( msg_up.domain );
1284                 msg_down.domain = mail.ChatterUtils.expand_domain( msg_down.domain );
1285
1286                 msg_down.domain = ['|','|'].concat( msg_up.domain ).concat( message_dom ).concat( msg_down.domain );
1287
1288                 if ( !msg_down.max_limit ) {
1289                     msg_down.nb_messages += 1 + msg_up.nb_messages;
1290                 }
1291
1292                 msg_up.$el.remove();
1293                 msg_up.destroy();
1294
1295                 msg_down.reinit();
1296
1297             } else if ( msg_up && msg_up.type == "expandable") {
1298                 // concat preview expandable message and this message to this dom
1299                 msg_up.domain = mail.ChatterUtils.expand_domain( msg_up.domain );
1300                 msg_up.domain = ['|'].concat( msg_up.domain ).concat( message_dom );
1301                 
1302                 msg_up.nb_messages++;
1303
1304                 msg_up.reinit();
1305
1306             } else if ( msg_down && msg_down.type == "expandable") {
1307                 // concat next expandable message and this message to this dom
1308                 msg_down.domain = mail.ChatterUtils.expand_domain( msg_down.domain );
1309                 msg_down.domain = ['|'].concat( msg_down.domain ).concat( message_dom );
1310                 
1311                 // it's maybe a message expandable for the max limit read message
1312                 if ( !msg_down.max_limit ) {
1313                     msg_down.nb_messages++;
1314                 }
1315                 
1316                 msg_down.reinit();
1317
1318             } else {
1319                 // create a expandable message
1320                 var expandable = new mail.ThreadExpandable(this, {
1321                     'id': message.id,
1322                     'model': message.model,
1323                     'parent_id': message.parent_id,
1324                     'nb_messages': 1,
1325                     'thread_level': message.thread_level,
1326                     'parent_id': message.parent_id,
1327                     'domain': message_dom,
1328                     'options': message.options,
1329                     }, {
1330                     'default_model': message.model || this.context.default_model,
1331                     'default_res_id': message.res_id || this.context.default_res_id,
1332                     'default_parent_id': this.id,
1333                 });
1334
1335                 // add object on array and DOM
1336                 this.messages.push(expandable);
1337                 expandable.insertAfter(message.$el);
1338             }
1339
1340             // destroy message
1341             message.destroy();
1342
1343             return true;
1344         },
1345     });
1346
1347     /**
1348          * ------------------------------------------------------------
1349      * mail : root Widget
1350      * ------------------------------------------------------------
1351      *
1352      * This widget handles the display of messages with thread options. Its main
1353      * use is to receive a context and a domain, and to delegate the message
1354      * fetching and displaying to the Thread widget.
1355      */
1356     session.web.client_actions.add('mail.Widget', 'session.mail.Widget');
1357     mail.Widget = session.web.Widget.extend({
1358         template: 'mail.Widget',
1359
1360         /**
1361          * @param {Object} parent parent
1362          * @param {Array} [domain]
1363          * @param {Object} [context] context of the thread. It should
1364          *   contain at least default_model, default_res_id. Please refer to
1365          *   the compose_message widget for more information about it.
1366          * @param {Object} [options]
1367          *...  @param {Number} [truncate_limit=250] number of character to
1368          *      display before having a "show more" link; note that the text
1369          *      will not be truncated if it does not have 110% of the parameter
1370          *...  @param {Boolean} [show_record_name] display the name and link for do action
1371          *...  @param {boolean} [show_reply_button] display the reply button
1372          *...  @param {boolean} [show_read_unread_button] display the read/unread button
1373          *...  @param {int} [display_indented_thread] number thread level to indented threads.
1374          *      other are on flat mode
1375          *...  @param {Boolean} [show_compose_message] allow to display the composer
1376          *...  @param {Boolean} [show_compact_message] display the compact message on the thread
1377          *      when the user clic on this compact mode, the composer is open
1378          *...  @param {Array} [message_ids] List of ids to fetch by the root thread.
1379          *      When you use this option, the domain is not used for the fetch root.
1380          *     @param {String} [no_message] Message to display when there are no message
1381          */
1382         init: function (parent, options) {
1383             this._super(parent);
1384             this.domain = options.domain || [];
1385             this.context = options.context || {};
1386             this.search_results = {'domain': [], 'context': {}, 'groupby': {}};
1387
1388             this.options = _.extend({
1389                 'display_indented_thread' : -1,
1390                 'show_reply_button' : false,
1391                 'show_read_unread_button' : false,
1392                 'truncate_limit' : 250,
1393                 'show_record_name' : false,
1394                 'show_compose_message' : false,
1395                 'show_compact_message' : false,
1396                 'message_ids': undefined,
1397                 'no_message': false
1398             }, options);
1399
1400             if (this.display_indented_thread === false) {
1401                 this.display_indented_thread = -1;
1402             }
1403             
1404         },
1405
1406         start: function (options) {
1407             this._super.apply(this, arguments);
1408             this.message_render();
1409             this.bind_events();
1410         },
1411
1412         
1413         /**
1414          *Create the root thread and display this object in the DOM.
1415          * Call the no_message method then c all the message_fetch method 
1416          * of this root thread to display the messages.
1417          */
1418         message_render: function (search) {
1419
1420             this.thread = new mail.Thread(this, {}, {
1421                 'domain' : this.domain,
1422                 'context' : this.context,
1423                 'options': this.options,
1424             });
1425
1426             this.thread.appendTo( this.$el );
1427             this.thread.no_message();
1428             this.thread.message_fetch(null, null, this.options.message_ids);
1429
1430             if (this.options.show_compose_message) {
1431                 this.thread.instantiate_compose_message();
1432                 if (this.options.show_compact_message) {
1433                     this.thread.compose_message.do_show_compact();
1434                 } else {
1435                     this.thread.compose_message.do_hide_compact();
1436                 }
1437             }
1438         },
1439
1440         bind_events: function () {
1441             $(document).scroll( _.bind(this.thread.on_scroll, this.thread) );
1442             $(window).resize( _.bind(this.thread.on_scroll, this.thread) );
1443             this.$el.resize( _.bind(this.thread.on_scroll, this.thread) );
1444             window.setTimeout( _.bind(this.thread.on_scroll, this.thread), 500 );
1445         }
1446     });
1447
1448
1449     /**
1450          * ------------------------------------------------------------
1451      * mail_thread Widget
1452      * ------------------------------------------------------------
1453      *
1454      * This widget handles the display of messages on a document. Its main
1455      * use is to receive a context and a domain, and to delegate the message
1456      * fetching and displaying to the Thread widget.
1457      * Use Help on the field to display a custom "no message loaded"
1458      */
1459     session.web.form.widgets.add('mail_thread', 'openerp.mail.RecordThread');
1460     mail.RecordThread = session.web.form.AbstractField.extend({
1461         template: 'mail.record_thread',
1462
1463         start: function () {
1464             this._super.apply(this, arguments);
1465             // NB: check the actual_mode property on view to know if the view is in create mode anymore
1466             this.view.on("change:actual_mode", this, this._check_visibility);
1467             this._check_visibility();
1468         },
1469
1470         _check_visibility: function () {
1471             this.$el.toggle(this.view.get("actual_mode") !== "create");
1472         },
1473
1474         render_value: function () {
1475             var self = this;
1476             if (! this.view.datarecord.id || session.web.BufferedDataSet.virtual_id_regex.test(this.view.datarecord.id)) {
1477                 this.$('oe_mail_thread').hide();
1478                 return;
1479             }
1480
1481             if (this.root) {
1482                 this.root.destroy();
1483             }
1484             // create and render Thread widget
1485             this.root = new mail.Widget(this, {
1486                 'domain' : (this.options.domain || []).concat([['model', '=', this.view.model], ['res_id', '=', this.view.datarecord.id]]),
1487                 'context' : {
1488                     'default_res_id': this.view.datarecord.id || false,
1489                     'default_model': this.view.model || 'mail.thread',
1490                     'default_is_private': false 
1491                 },
1492                 'display_indented_thread': -1,
1493                 'show_reply_button': false,
1494                 'show_read_unread_button': false,
1495                 'show_compose_message': this.view.is_action_enabled('edit') || (this.getParent().fields.message_is_follower && this.getParent().fields.message_is_follower.get_value()),
1496                 'message_ids': this.getParent().fields.message_ids ? this.getParent().fields.message_ids.get_value() : undefined,
1497                 'show_compact_message': true,
1498                 'no_message': this.node.attrs.help
1499                 }
1500             );
1501
1502             return this.root.replace(this.$('.oe_mail-placeholder'));
1503         },
1504     });
1505
1506
1507     /**
1508          * ------------------------------------------------------------
1509      * Wall Widget
1510      * ------------------------------------------------------------
1511      *
1512      * This widget handles the display of messages on a Wall. Its main
1513      * use is to receive a context and a domain, and to delegate the message
1514      * fetching and displaying to the Thread widget.
1515      */
1516     session.web.client_actions.add('mail.wall', 'session.mail.Wall');
1517     mail.Wall = session.web.Widget.extend({
1518         template: 'mail.wall',
1519
1520         /**
1521          * @param {Object} parent parent
1522          * @param {Object} [options]
1523          * @param {Array} [options.domain] domain on the Wall
1524          * @param {Object} [options.context] context, is an object. It should
1525          *      contain default_model, default_res_id, to give it to the threads.
1526          */
1527         init: function (parent, options) {
1528             this._super(parent);
1529             console.log(arguments);
1530             this.options = options || {};
1531             this.options.domain = options.domain || [];
1532             this.options.context = options.context || {};
1533
1534             this.options.defaults = {};
1535             for (var key in options.context) {
1536                 if(key.match(/^search_default_/)) {
1537                     this.options.defaults[key.replace(/^search_default_/, '')] = options.context[key];
1538                 }
1539             }
1540         },
1541
1542         start: function () {
1543             this._super.apply(this);
1544             this.load_searchview(this.options.defaults, false);
1545             this.bind_events();
1546
1547             if(!this.searchview.has_defaults) {
1548                 this.message_render();
1549             }
1550         },
1551
1552         /**
1553          * Load the mail.message search view
1554          * @param {Object} defaults ??
1555          * @param {Boolean} hidden some kind of trick we do not care here
1556          */
1557         load_searchview: function (defaults, hidden) {
1558             var self = this;
1559             var ds_msg = new session.web.DataSetSearch(self, 'mail.message');
1560             self.searchview = new session.web.SearchView(self, ds_msg, false, defaults || {}, hidden || false);
1561             self.searchview.appendTo(self.$('.oe_view_manager_view_search'))
1562                 .then(function () { self.searchview.on('search_data', self, self.do_searchview_search); });
1563             if (this.searchview.has_defaults) {
1564                 this.searchview.ready.then(this.searchview.do_search);
1565             }
1566             return self.searchview
1567         },
1568
1569         /**
1570          * Get the domains, contexts and groupbys in parameter from search
1571          * view, then render the filtered threads.
1572          * @param {Array} domains
1573          * @param {Array} contexts
1574          * @param {Array} groupbys
1575          */
1576         do_searchview_search: function (domains, contexts, groupbys) {
1577             var self = this;
1578             this.rpc('/web/session/eval_domain_and_context', {
1579                 domains: domains || [],
1580                 contexts: contexts || [],
1581                 group_by_seq: groupbys || []
1582             }).then(function (results) {
1583                 if(self.root) {
1584                     $('<span class="oe_mail-placeholder"/>').insertAfter(self.root.$el);
1585                     self.root.destroy();
1586                 }
1587                 return self.message_render(results);
1588             });
1589         },
1590
1591
1592         /**
1593          *Create the root thread widget and display this object in the DOM
1594           */
1595         message_render: function ( search ) {
1596             var domain = this.options.domain.concat(search && search['domain'] ? search['domain'] : []);
1597             var context = _.extend(this.options.context, search && search['context'] ? search['context'] : {});
1598             this.root = new mail.Widget(this, {
1599                 'domain' : domain,
1600                 'context' : context,
1601                 'display_indented_thread': 1,
1602                 'show_reply_button': true,
1603                 'show_read_unread_button': true,
1604                 'show_compose_message': true,
1605                 'show_compact_message': false,
1606                 }
1607             );
1608
1609             return this.root.replace(this.$('.oe_mail-placeholder'));
1610         },
1611
1612         bind_events: function () {
1613             var self=this;
1614             this.$(".oe_write_full").click(function(){ self.root.thread.compose_message.on_compose_fullmail(); });
1615             this.$(".oe_write_onwall").click(function(){ self.root.thread.on_compose_message(); });
1616         }
1617     });
1618
1619
1620     /**
1621          * ------------------------------------------------------------
1622      * UserMenu
1623      * ------------------------------------------------------------
1624      * 
1625      * Add a link on the top user bar for write a full mail
1626      */
1627     session.web.ComposeMessageTopButton = session.web.Widget.extend({
1628         template:'mail.ComposeMessageTopButton',
1629
1630         start: function () {
1631             this.$el.on('click', 'button', this.on_compose_message );
1632             this._super();
1633         },
1634
1635         on_compose_message: function (event) {
1636             event.stopPropagation();
1637             var action = {
1638                 type: 'ir.actions.act_window',
1639                 res_model: 'mail.compose.message',
1640                 view_mode: 'form',
1641                 view_type: 'form',
1642                 views: [[false, 'form']],
1643                 target: 'new',
1644                 context: { 'default_content_subtype': 'html' },
1645             };
1646             session.client.action_manager.do_action(action);
1647         },
1648     });
1649
1650     session.web.UserMenu = session.web.UserMenu.extend({
1651         start: function () {
1652             var render = new session.web.ComposeMessageTopButton();
1653             render.insertAfter(this.$el);
1654             this._super();
1655         }
1656     });
1657
1658 };