1 openerp.mail = function(session) {
2 var _t = session.web._t,
5 var mail = session.mail = {};
7 openerp_mail_followers(session, mail); // import mail_followers.js
10 * ------------------------------------------------------------
12 * ------------------------------------------------------------
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.
19 session.web.FormView = session.web.FormView.extend({
20 do_action: function(action, on_close) {
21 if (action.res_model == 'mail.compose.message') {
23 /* hack for stop context propagation of wrong value
24 * delete this hack when a global method to clean context is create
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' &&
38 key!='active_model' &&
39 key!='edi_web_url_view' &&
41 action.context[key]=null;
45 $('.openerp .oe_mail_wall_threads .oe_mail_thread button.oe_mail_wall_button_fetch').click();
48 return this._super(action, on_close);
54 * ------------------------------------------------------------
56 * ------------------------------------------------------------
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],
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 || '');
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'];
77 /** Replaces some expressions
78 * - :name - shortcut to an image
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);
94 /* replace textarea text into html text
97 get_text2html: function(text){
99 .replace(/[\n\r]/g,'<br/>')
100 .replace(/((?:https?|ftp):\/\/[\S]+)/g,'<a href="$1">$1</a> ')
106 * ------------------------------------------------------------
107 * ComposeMessage widget
108 * ------------------------------------------------------------
110 * This widget handles the display of a form to compose a new message.
111 * This form is a mail.compose.message form_view.
114 mail.ThreadComposeMessage = session.web.Widget.extend({
115 template: 'mail.compose_message',
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]
125 init: function (parent, options) {
128 this.attachment_ids = [];
130 this.context = options.context || {};
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;
141 this.attachment_ids = [];
142 this.options.thread.show_attachment_delete = true;
143 this.options.thread.show_attachment_link = true;
145 this.parent_thread= parent.messages!= undefined ? parent : false;
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);
154 this.display_attachments();
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);
162 /* upload the file on the server, add in the attachments list and reload display
164 display_attachments: function(){
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 );
170 this.list_attachment.replaceWith( render );
172 this.list_attachment = this.$("ul.oe_mail_msg_attachments");
174 // event: delete an attachment
175 this.$el.on('click', '.oe_mail_attachment_delete', self.on_attachment_delete);
177 on_attachment_change: function (event) {
178 event.stopPropagation();
180 var $target = $(event.target);
181 if ($target.val() !== '') {
183 var filename = $target.val().replace(/.*[\\\/]/,'');
185 // if the files exits for this answer, delete the file before upload
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){
192 this.ds_attachment.unlink([this.attachment_ids[i].id]);
194 attachments.push(this.attachment_ids[i]);
197 this.attachment_ids = attachments;
200 //session.web.blockUI();
201 self.$('form.oe_form_binary_form').submit();
202 //self.submit_ajax_attachment();
204 this.$(".oe_attachment_file").hide();
206 this.attachment_ids.push({
209 'filename': filename,
213 this.display_attachments();
217 submit_ajax_attachment: function(){
219 var $form = self.$('form.oe_form_binary_form');
220 var filename = this.$('input.oe_form_binary_file').val().replace(/.*[\\\/]/,'');
223 var fomdata = new FormData();
224 $.each($form.find('input'), function(i, field) {
226 if($field.attr('type')!='file'){
227 fomdata.append($field.attr('name'), $field.val());
229 fomdata.append($field.attr('name'), field.files[0]);
233 var progress=function(event) {
234 self.$("span[name='"+filename+"'] div:lt("+Math.floor(event.loaded / event.total*5)+")").show();
238 url: $form.attr("action"),
244 enctype: 'multipart/form-data',
247 myXhr = $.ajaxSettings.xhr();
249 // for handling the progress of the upload
250 myXhr.upload.addEventListener('progress', progress, false);
252 myXhr.addEventListener('progress', progress, false);
255 success: function(data){
256 $iframe=$('<iframe style="display:none;"/>').html(data);
257 $iframe.appendTo(self.$el);
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]={
270 'filename': result.filename,
271 'url': mail.ChatterUtils.get_attachment_url(this.session, result)
275 this.display_attachments();
277 var $input = this.$('input.oe_form_binary_file');
278 $input.after($input.clone(true)).remove();
279 this.$(".oe_attachment_file").show();
281 /* unlink the file on the server and reload display
283 on_attachment_delete: function (event) {
284 event.stopPropagation();
285 var attachment_id=$(event.target).data("id");
288 for(var i in this.attachment_ids){
289 if(attachment_id!=this.attachment_ids[i].id){
290 attachments.push(this.attachment_ids[i]);
293 this.ds_attachment.unlink([attachment_id]);
296 this.attachment_ids = attachments;
297 this.display_attachments();
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
305 // set_free_attachments: function(){
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])
317 // self.display_attachments();
321 bind_events: function() {
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()} );
331 on_compose_fullmail: function(){
333 for(var i in this.attachment_ids){
334 attachments.push(this.attachment_ids[i].id);
337 type: 'ir.actions.act_window',
338 res_model: 'mail.compose.message',
341 action_from: 'mail.ThreadComposeMessage',
342 views: [[false, 'form']],
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
354 this.do_action(action);
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){
368 /*post a message and fetch the message*/
369 on_message_post: function (body) {
372 var comment_node = this.$('textarea');
373 var body = comment_node.val();
374 comment_node.val('');
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>'));
383 attachments.push(this.attachment_ids[i].id);
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),
393 this.context.default_parent_id,
395 ).then(this.parent_thread.proxy('switch_new_message'));
396 this.attachment_ids=[];
404 * ------------------------------------------------------------
405 * Thread Message Expandable Widget
406 * ------------------------------------------------------------
408 * This widget handles the display the expandable message in a thread.
410 * - - visible message
412 * - - visible message
413 * - - visible message
416 mail.ThreadExpandable = session.web.Widget.extend({
417 template: 'mail.thread.expandable',
419 init: function(parent, options) {
421 this.domain = options.domain || [];
422 this.context = _.extend({
423 default_model: 'mail.thread',
425 default_parent_id: false }, options.context || {});
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';
432 // record options and data
433 this.parent_thread= parent.messages!= undefined ? parent : options.options.thread._parents[0] ;
438 this._super.apply(this, arguments);
443 * Bind events in the widget. Each event is slightly described
444 * in the function. */
445 bind_events: function() {
447 // event: click on 'Vote' button
448 this.$el.on('click', 'a.oe_mail_fetch_more', self.on_expandable);
451 /*The selected thread and all childs (messages/thread) became read
452 * @param {object} mouse envent
454 on_expandable: function (event) {
455 if(event)event.stopPropagation();
456 this.parent_thread.message_fetch(false, this.domain, this.context);
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
474 * - - sub message (parent_id = root message)
476 * - - - - sub sub message (parent id = sub thread)
477 * - - sub message (parent_id = root message)
480 mail.ThreadMessage = session.web.Widget.extend({
481 template: 'mail.thread.message',
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]
502 init: function(parent, options) {
506 var param = options.parameters;
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;
523 this.vote_user_ids = param.vote_user_ids || [];
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 || [];
530 // record domain and context
531 this.domain = options.domain || [];
532 this.context = _.extend({
533 default_model: 'mail.thread',
535 default_parent_id: false }, options.context || {});
539 'thread' : options.options.thread,
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,
550 // record options and data
551 this.parent_thread= parent.messages!= undefined ? parent : options.options.thread._parents[0];
555 this.formating_data();
558 this.ds_notification = new session.web.DataSetSearch(this, 'mail.notification');
559 this.ds_message = new session.web.DataSetSearch(this, 'mail.message');
562 formating_data: function(){
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');
570 this.avatar = mail.ChatterUtils.get_image(this.session, 'res.partner', 'image_small', this.author_id[0]);
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);
579 this._super.apply(this, arguments);
581 this.$el.hide().fadeIn(750);
583 this.create_thread();
587 * Bind events in the widget. Each event is slightly described
588 * in the function. */
589 bind_events: function() {
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();
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);
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);
609 on_message_reply:function(event){
610 event.stopPropagation();
611 this.thread.on_compose_message();
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',
626 create_thread: function(){
631 var param = _.extend(self, {'parent_id': self.id});
633 self.thread = new mail.Thread(self, {
634 'domain': self.domain,
636 'default_model': self.model,
637 'default_res_id': self.res_id,
638 'default_parent_id': self.id
641 'thread' : self.options.thread,
642 'message' : self.options.message
647 /*insert thread in parent message*/
648 self.thread.appendTo(self.$el.find('div.oe_thread_placeholder'));
651 animated_destroy: function(options) {
654 if(options && options.fadeTime) {
655 self.$el.fadeOut(options.fadeTime, function(){
663 on_message_delete: function (event) {
664 event.stopPropagation();
665 if (! confirm(_t("Do you really want to delete this message?"))) { return false; }
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();
675 /*The selected thread and all childs (messages/thread) became read
676 * @param {object} mouse envent
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() );
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");
688 this.ds_notification.call('set_message_read', [ids,false]);
689 this.$el.removeClass("oe_mail_read").addClass("oe_mail_unread");
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
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);
711 if(this.id==options.id)
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);
724 /* get all child message/thread id linked
726 get_child_ids: function(){
728 if(arguments[0]) res.push(this.id);
730 res = res.concat( this.thread.get_child_ids(true) );
736 on_vote: function (event) {
737 event.stopPropagation();
739 return this.ds_message.call('vote_toggle', [[self.id]]).pipe(function(vote){
742 if (!self.has_voted) {
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]);
748 self.vote_user_ids=votes;
751 self.vote_user_ids.push([self.session.uid, 'You']);
758 // Render vote Display template.
759 display_vote: function () {
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);
768 * ------------------------------------------------------------
770 * ------------------------------------------------------------
772 * This widget handles the display of a thread of messages. The
775 * - - sub message (parent_id = root message)
777 * - - - - sub sub message (parent id = sub thread)
778 * - - sub message (parent_id = root message)
781 mail.Thread = session.web.Widget.extend({
782 template: 'mail.thread',
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
804 init: function(parent, options) {
806 this.domain = options.domain || [];
807 this.context = _.extend({
808 default_model: 'mail.thread',
810 default_parent_id: false }, options.context || {});
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] )
824 'message' : options.options.message
827 // record options and data
828 this.parent_linked_message= parent.thread!= undefined ? parent : false ;
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]);
846 this.ds_thread = new session.web.DataSetSearch(this, this.context.default_model);
847 this.ds_message = new session.web.DataSetSearch(this, 'mail.message');
851 // TDE TODO: check for deferred, not sure it is correct
852 this._super.apply(this, arguments);
854 this.list_ul=this.$('ul.oe_mail_thread_display:first');
855 this.more_msg=this.$(">.oe_mail_msg_more_message:first");
857 this.display_user_avatar();
858 var display_done = compose_done = false;
860 this.instantiate_ComposeMessage();
864 if(this.options.thread._parents[0]==this){
865 this.on_first_thread();
868 return display_done && compose_done;
871 instantiate_ComposeMessage: function() {
872 // add message composition form view
873 this.ComposeMessage = new mail.ThreadComposeMessage(this,{
874 'context': this.context,
876 'show_attachment_delete': true,
878 this.ComposeMessage.appendTo(this.$(".oe_mail_thread_action:first"));
881 /* this method is runing for first parent thread
883 on_first_thread: function(){
885 // fetch and display message, using message_ids if set
886 this.message_fetch();
888 $(document).scroll( self.on_scroll );
889 window.setTimeout( self.on_scroll, 500 );
891 $(session.web.qweb.render('mail.wall_no_message', {})).appendTo(this.$('ul.oe_mail_thread_display'));
893 if(this.options.thread.show_header_compose){
894 this.ComposeMessage.$el.show();
895 //this.ComposeMessage.set_free_attachments();
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();
902 this.$el.prepend(button_fetch);
903 this.$el.addClass("oe_mail_wall_first_thread");
906 /* When the expandable object is visible on screen (with scrolling)
907 * then the on_expandable function is launch
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();
915 /* bottom of the screen */
916 var bottom = $(window).scrollTop()+$(window).height()+100;
917 if(bottom - pos.top > 0){
918 last.on_expandable();
925 * Bind events in the widget. Each event is slightly described
926 * in the function. */
927 bind_events: function() {
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(); });
933 /* get all child message/thread id linked
935 get_child_ids: function(){
937 for(var i in this.messages){
938 if(this.messages[i].thread){
939 res = res.concat( this.messages[i].get_child_ids(true) );
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
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);
961 if(this.id==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});
973 //if option default_return_top_thread, return the top if no found thread
974 if(options.default_return_top_thread){
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
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);
993 /* this function is launch when a user click on "Reply" button
995 on_compose_message: function(){
996 this.ComposeMessage.$el.toggle();
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
1007 message_fetch: function (initial_mode, replace_domain, replace_context, ids) {
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);
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() );
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'));
1023 /* create record object and linked him
1025 create_message_object: function (message) {
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);
1037 self.messages.push( self.insert_message(message) );
1041 /** Displays a message or an expandable message */
1042 insert_message: function (message) {
1045 this.$("li.oe_wall_no_message").remove();
1047 if(message.type=='expandable'){
1048 var message = new mail.ThreadExpandable(self, {
1049 'domain': message.domain,
1051 'default_model': message.model,
1052 'default_res_id': message.res_id,
1053 'default_parent_id': message.id },
1054 'parameters': message
1057 var message = new mail.ThreadMessage(self, {
1058 'domain': message.domain,
1060 'default_model': message.model,
1061 'default_res_id': message.res_id,
1062 'default_parent_id': message.id },
1064 'thread': self.options.thread,
1065 'message': self.options.message
1067 'parameters': message
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);
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];
1087 var sort = self.options.thread.thread_level==0 || (self.options.thread.display_on_flat && self.options.thread.thread_level<=1);
1091 message.insertBefore(parent_older.$el);
1093 message.insertAfter(parent_older.$el);
1096 else if(parent_newer){
1098 message.insertAfter(parent_newer.$el);
1100 message.insertBefore(parent_newer.$el);
1105 message.prependTo(thread.list_ul);
1107 message.appendTo(thread.list_ul);
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);
1118 /* Send the records to his parent thread */
1119 switch_new_message: function(records) {
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 );
1132 * ------------------------------------------------------------
1133 * mail_thread Widget
1134 * ------------------------------------------------------------
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.
1140 session.web.form.widgets.add('mail_thread', 'openerp.mail.RecordThread');
1141 mail.RecordThread = session.web.form.AbstractField.extend({
1142 template: 'mail.record_thread',
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};
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();
1157 _check_visibility: function() {
1158 this.$el.toggle(this.view.get("actual_mode") !== "create");
1162 * Reinitialize the widget field and Display the threads
1163 * @param {Object} new_context: context of the refresh
1165 set_value: function() {
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();
1173 _.extend(this.options.context, {
1174 default_res_id: this.view.datarecord.id,
1175 default_model: this.view.model });
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());
1184 this.thread.destroy();
1186 this.thread = new mail.Thread(self, {
1188 'context': this.options.context,
1191 'show_header_compose': show_header_compose,
1192 'use_composer': show_header_compose,
1193 'display_on_flat':true
1196 'show_dd_delete': true
1202 return this.thread.appendTo( this.$('.oe_mail_wall_threads:first') );
1208 * ------------------------------------------------------------
1210 * ------------------------------------------------------------
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.
1216 session.web.client_actions.add('mail.wall', 'session.mail.Wall');
1217 mail.Wall = session.web.Widget.extend({
1218 template: 'mail.wall',
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
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');
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']);
1244 return (searchview_ready && thread_displayed);
1248 * Load the mail.message search view
1249 * @param {Object} defaults ??
1250 * @param {Boolean} hidden some kind of trick we do not care here
1252 load_searchview: function (defaults, hidden) {
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);
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
1267 do_searchview_search: function(domains, contexts, groupbys) {
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();
1283 * Display the threads
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'] : {}),
1291 'use_composer': true,
1292 'show_header_compose': false,
1296 'show_dd_hide': true,
1297 'show_dd_delete': true,
1303 return this.thread.appendTo( this.$('.oe_mail_wall_threads:first') );
1307 bind_events: function(){
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(); });