[MERGE]: Merged with lp:openobject-addons
authorAtul Patel (OpenERP) <atp@tinyerp.com>
Thu, 6 Sep 2012 11:34:41 +0000 (17:04 +0530)
committerAtul Patel (OpenERP) <atp@tinyerp.com>
Thu, 6 Sep 2012 11:34:41 +0000 (17:04 +0530)
bzr revid: atp@tinyerp.com-20120906113441-sutt7m5zv1wa0814

1  2 
addons/mail/__init__.py
addons/mail/mail_message.py
addons/mail/mail_thread.py
addons/mail/static/src/css/mail.css
addons/mail/static/src/js/mail.js
addons/mail/static/src/xml/mail.xml

  ##############################################################################
  
  import mail_alias
- import mail_message
  import mail_followers
+ import mail_message
+ import mail_mail
  import mail_thread
  import mail_group
 +import mail_vote
- import ir_needaction
  import res_partner
  import res_users
  import report
@@@ -207,59 -104,152 +104,173 @@@ class mail_message(osv.Model)
                          ('notification', 'System notification'),
                          ], 'Type',
              help="Message type: email for email message, notification for system "\
-                   "message, comment for other messages such as user replies"),
-         'partner_id': fields.many2one('res.partner', 'Related partner',
-             help="Deprecated field. Use partner_ids instead."),
-         'partner_ids': fields.many2many('res.partner',
-             'mail_message_res_partner_rel',
-             'message_id', 'partner_id', 'Destination partners',
-             help="When sending emails through the social network composition wizard"\
-                  "you may choose to send a copy of the mail to partners."),
-         'user_id': fields.many2one('res.users', 'Related User', readonly=1),
+                  "message, comment for other messages such as user replies"),
+         'author_id': fields.many2one('res.partner', 'Author', required=True),
+         'partner_ids': fields.many2many('res.partner', 'mail_notification', 'message_id', 'partner_id', 'Recipients'),
          'attachment_ids': fields.many2many('ir.attachment', 'message_attachment_rel',
              'message_id', 'attachment_id', 'Attachments'),
-         'mail_server_id': fields.many2one('ir.mail_server', 'Outgoing mail server', readonly=1),
-         'state': fields.selection([
-                         ('outgoing', 'Outgoing'),
-                         ('sent', 'Sent'),
-                         ('received', 'Received'),
-                         ('exception', 'Delivery Failed'),
-                         ('cancel', 'Cancelled'),
-                         ], 'Status', readonly=True),
-         'auto_delete': fields.boolean('Auto Delete',
-             help="Permanently delete this email after sending it, to save space"),
-         'original': fields.binary('Original', readonly=1,
-             help="Original version of the message, as it was sent on the network"),
-         'parent_id': fields.many2one('mail.message', 'Parent Message',
-             select=True, ondelete='set null',
-             help="Parent message, used for displaying as threads with hierarchy"),
+         'parent_id': fields.many2one('mail.message', 'Parent Message', select=True, ondelete='set null', help="Initial thread message."),
          'child_ids': fields.one2many('mail.message', 'parent_id', 'Child Messages'),
-         'vote_ids': fields.one2many('mail.vote', 'msg_id', 'Votes'),
+         'model': fields.char('Related Document Model', size=128, select=1),
+         'res_id': fields.integer('Related Document ID', select=1),
+         'record_name': fields.function(_get_record_name, type='string',
+             string='Message Record Name',
+             help="Name get of the related document."),
+         'notification_ids': fields.one2many('mail.notification', 'message_id', 'Notifications'),
+         'subject': fields.char('Subject'),
+         'date': fields.datetime('Date'),
+         'message_id': fields.char('Message-Id', help='Message unique identifier', select=1, readonly=1),
+         'body': fields.html('Contents', help='Automatically sanitized HTML contents'),
+         'unread': fields.function(_get_unread, fnct_search=_search_unread,
+             type='boolean', string='Unread',
+             help='Functional field to search for unread messages linked to uid'),
++        'vote_ids': fields.one2many('mail.vote', 'msg_id', 'Votes'),            
++
      }
-         
+     def _needaction_domain_get(self, cr, uid, context=None):
+         if self._needaction:
+             return [('unread', '=', True)]
+         return []
+     def _get_default_author(self, cr, uid, context=None):
+         return self.pool.get('res.users').browse(cr, uid, uid, context=context).partner_id.id
      _defaults = {
          'type': 'email',
-         'state': 'received',
+         'date': lambda *a: fields.datetime.now(),
+         'author_id': lambda self, cr, uid, ctx={}: self._get_default_author(cr, uid, ctx),
+         'body': '',
      }
 +    
-     
 +    #---------------------------------------------------
 +    #Mail Vote system (Like or Unlike comments
 +    #-----------------------------------------------------
 +    def vote_toggle(self, cr, uid, ids, context=None):
 +        '''
 +        Toggles when Comment is liked or unlike.
 +        create vote entries if current user like comment..
 +        '''
 +        vote_pool = self.pool.get('mail.vote')
 +        new_vote_id = False
 +        for message in self.browse(cr, uid, ids, context):
 +            voters_ids = [x.id for x in message.vote_ids if x.user_id.id == uid]
 +            if not voters_ids:
 +                new_vote_id =  vote_pool.create(cr, uid, {'msg_id': message.id, 'user_id': uid}, context=context)
-             else:
++            else:    
 +                vote_pool.unlink(cr, uid, voters_ids, context=context)
-         return True
++        return True                
++
+     #------------------------------------------------------
+     # Message loading for web interface
+     #------------------------------------------------------
+     def _message_dict_get(self, cr, uid, msg, context=None):
+         """ Return a dict representation of the message browse record. """
+         attachment_ids = self.pool.get('ir.attachment').name_get(cr, uid, [x.id for x in msg.attachment_ids], context=context)
+         author_id = self.pool.get('res.partner').name_get(cr, uid, [msg.author_id.id], context=context)[0]
+         author_user_id = self.pool.get('res.users').name_get(cr, uid, [msg.author_id.user_ids[0].id], context=context)[0]
+         partner_ids = self.pool.get('res.partner').name_get(cr, uid, [x.id for x in msg.partner_ids], context=context)
+         return {
+             'id': msg.id,
+             'type': msg.type,
+             'attachment_ids': attachment_ids,
+             'body': msg.body,
+             'model': msg.model,
+             'res_id': msg.res_id,
+             'record_name': msg.record_name,
+             'subject': msg.subject,
+             'date': msg.date,
+             'author_id': author_id,
+             'author_user_id': author_user_id,
+             'partner_ids': partner_ids,
+             'child_ids': [],
+         }
+     def message_read_tree_flatten(self, cr, uid, messages, current_level, level, context=None):
+         """ Given a tree with several roots of following structure :
+             [   {'id': 1, 'child_ids': [
+                     {'id': 11, 'child_ids': [...] },],
+                 {...}   ]
+             Flatten it to have a maximum number of levels, 0 being flat and
+             sort messages in a level according to a key of the messages.
+             Perform the flattening at leafs if above the maximum depth, then get
+             back in the tree.
+             :param context: ``sort_key``: key for sorting (id by default)
+             :param context: ``sort_reverse``: reverser order for sorting (True by default)
+         """
+         def _flatten(msg_dict):
+             """ from    {'id': x, 'child_ids': [{child1}, {child2}]}
+                 get     [{'id': x, 'child_ids': []}, {child1}, {child2}]
+             """
+             child_ids = msg_dict.pop('child_ids', [])
+             msg_dict['child_ids'] = []
+             return [msg_dict] + child_ids
+             # return sorted([msg_dict] + child_ids, key=itemgetter('id'), reverse=True)
+         context = context or {}
+         # Depth-first flattening
+         for message in messages:
+             if message.get('type') == 'expandable':
+                 continue
+             message['child_ids'] = self.message_read_tree_flatten(cr, uid, message['child_ids'], current_level + 1, level, context=context)
+         # Flatten if above maximum depth
+         if current_level < level:
+             return_list = messages
+         else:
+             return_list = []
+             for message in messages:
+                 for flat_message in _flatten(message):
+                     return_list.append(flat_message)
+         return sorted(return_list, key=itemgetter(context.get('sort_key', 'id')), reverse=context.get('sort_reverse', True))
+     def message_read(self, cr, uid, ids=False, domain=[], thread_level=0, limit=None, context=None):
+         """ If IDs are provided, fetch these records. Otherwise use the domain
+             to fetch the matching records.
+             After having fetched the records provided by IDs, it will fetch the
+             parents to have well-formed threads.
+             :return list: list of trees of messages
+         """
+         limit = limit or self._message_read_limit
+         context = context or {}
+         if not ids:
+             ids = self.search(cr, uid, domain, context=context, limit=limit)
+         messages = self.browse(cr, uid, ids, context=context)
+         result = []
+         tree = {} # key: ID, value: record
+         for msg in messages:
+             if len(result) < (limit - 1):
+                 record = self._message_dict_get(cr, uid, msg, context=context)
+                 if thread_level and msg.parent_id:
+                     while msg.parent_id:
+                         if msg.parent_id.id in tree:
+                             record_parent = tree[msg.parent_id.id]
+                         else:
+                             record_parent = self._message_dict_get(cr, uid, msg.parent_id, context=context)
+                             if msg.parent_id.parent_id:
+                                 tree[msg.parent_id.id] = record_parent
+                         if record['id'] not in [x['id'] for x in record_parent['child_ids']]:
+                             record_parent['child_ids'].append(record)
+                         record = record_parent
+                         msg = msg.parent_id
+                 if msg.id not in tree:
+                     result.append(record)
+                     tree[msg.id] = record
+             else:
+                 result.append({
+                     'type': 'expandable',
+                     'domain': [('id', '<=', msg.id)] + domain,
+                     'context': context,
+                     'thread_level': thread_level,  # should be improve accodting to level of records
+                     'id': -1,
+                 })
+                 break
+         # Flatten the result
+         if thread_level > 0:
+             result = self.message_read_tree_flatten(cr, uid, result, 0, thread_level, context=context)
+         return result
  
      #------------------------------------------------------
      # Email api
@@@ -160,102 -173,29 +173,29 @@@ class mail_thread(osv.AbstractModel)
                   "be inserted in kanban views."),
      }
  
-     _defaults = {
-         'message_state': True,
-     }
      #------------------------------------------------------
-     # Automatic subscription when creating/reading
+     # Automatic subscription when creating
      #------------------------------------------------------
 -
 +    
      def create(self, cr, uid, vals, context=None):
-         """ Override of create to subscribe :
-             - the writer
-             - followers given by the monitored fields
-         """
+         """ Override to subscribe the current user. """
          thread_id = super(mail_thread, self).create(cr, uid, vals, context=context)
-         followers_command = self.message_get_automatic_followers(cr, uid, thread_id, vals, fetch_missing=False, context=context)
-         if followers_command:
-             self.write(cr, uid, [thread_id], {'message_follower_ids': followers_command}, context=context)
+         self.message_subscribe_users(cr, uid, [thread_id], [uid], context=context)
          return thread_id
  
-     def write(self, cr, uid, ids, vals, context=None):
-         """ Override of write to subscribe :
-             - the writer
-             - followers given by the monitored fields
-         """
-         if isinstance(ids, (int, long)):
-             ids = [ids]
-         for id in ids:
-             # copy original vals because we are going to modify it
-             specific_vals = dict(vals)
-             # we modify followers: do not subscribe the uid
-             if specific_vals.get('message_follower_ids'):
-                 followers_command = self.message_get_automatic_followers(cr, uid, id, specific_vals, add_uid=False, context=context)
-                 specific_vals['message_follower_ids'] += followers_command
-             else:
-                 followers_command = self.message_get_automatic_followers(cr, uid, id, specific_vals, context=context)
-                 specific_vals['message_follower_ids'] = followers_command
-             write_res = super(mail_thread, self).write(cr, uid, ids, specific_vals, context=context)
-         return True
      def unlink(self, cr, uid, ids, context=None):
-         """Override unlink, to automatically delete messages
-            that are linked with res_model and res_id, not through
-            a foreign key with a 'cascade' ondelete attribute.
-            Notifications will be deleted with messages
-         """
+         """ Override unlink to delete messages and followers. This cannot be
+             cascaded, because link is done through (res_model, res_id). """
          msg_obj = self.pool.get('mail.message')
+         fol_obj = self.pool.get('mail.followers')
          # delete messages and notifications
-         msg_to_del_ids = msg_obj.search(cr, uid, [('model', '=', self._name), ('res_id', 'in', ids)], context=context)
-         msg_obj.unlink(cr, uid, msg_to_del_ids, context=context)
+         msg_ids = msg_obj.search(cr, uid, [('model', '=', self._name), ('res_id', 'in', ids)], context=context)
+         msg_obj.unlink(cr, uid, msg_ids, context=context)
+         # delete followers
+         fol_ids = fol_obj.search(cr, uid, [('res_model', '=', self._name), ('res_id', 'in', ids)], context=context)
+         fol_obj.unlink(cr, uid, fol_ids, context=context)
          return super(mail_thread, self).unlink(cr, uid, ids, context=context)
  
-     def message_get_automatic_followers(self, cr, uid, id, record_vals, add_uid=True, fetch_missing=False, context=None):
-         """ Return the command for the many2many follower_ids field to manage
-             subscribers. Behavior :
-             - get the monitored fields (ex: ['user_id', 'responsible_id']); those
-               fields should be relationships to res.users (#TODO: res.partner)
-             - if this field is in the record_vals: it means it has been modified
-               thus add its value to the followers
-             - if this fields is not in record_vals, but fetch_missing paramter
-               is set to True: fetch the value in the record (use: at creation
-               for default values, not present in record_vals)
-             - if add_uid: add the current user (for example: writer is subscriber)
-             - generate the command and return it
-             This method has to be used on 1 id, because otherwise it would imply
-             to track which user.id is used for which record.id.
-             :param record_vals: values given to the create method of the new
-                 record, or values updated in a write.
-             :param monitored_fields: a list of fields that are monitored. Those
-                 fields must be many2one fields to the res.users model.
-             :param fetch_missing: is set to True, the method will read the
-                 record to find values that are not present in record_vals.
-             #TODO : UPDATE WHEN MERGING TO PARTNERS
-         """
-         # get monitored fields
-         monitored_fields = self.message_get_monitored_follower_fields(cr, uid, [id], context=context)
-         modified_fields = [field for field in monitored_fields if field in record_vals.iterkeys()]
-         other_fields = [field for field in monitored_fields if field not in record_vals.iterkeys()] if fetch_missing else []
-         # for each monitored field: if in record_vals, it has been modified/added
-         follower_ids = []
-         for field in modified_fields:
-             # do not add 'False'
-             if record_vals.get(fields):
-                 follower_ids.append(record_vals.get(field))
-         # for other fields: read in record if fetch_missing (otherwise list is void)
-         for field in other_fields:
-             record = self.browse(cr, uid, id, context=context)
-             value = getattr(record, field)
-             if value:
-                 follower_ids.append(value)
-         # add uid if asked and not already present
-         if add_uid and uid not in follower_ids:
-             follower_ids.append(uid)
-         return self.message_subscribe_get_command(cr, uid, follower_ids, context=context)
      #------------------------------------------------------
      # mail.message wrappers and tools
      #------------------------------------------------------
Simple merge
@@@ -561,81 -266,11 +266,58 @@@ openerp.mail = function(session) 
              return display_done && compose_done;
          },
  
 +        //Mail vote Functionality...
 +        add_vote_event: function(element){
 +            self = this;
 +            vote_img = element.find('.oe_mail_msg_vote_like');
 +            if (vote_img)
 +                vote_img.click(function(){
 +                    self.subscribe_vote($(this).attr('data-id'));
 +                });
 +            return
 +        },
 +        
 +        find_parent_element: function(name, message_id){
 +            parent_element = false;
 +            _.each($(name), function(element){
 +                if ($(element).attr("data-id") == message_id){
 +                    parent_element = element;
 +                }
 +            });
 +            return parent_element;
 +         },
 +
 +        render_vote: function(message_id){
 +            var self = this;
 +            var mail_vote = new session.web.DataSetSearch(self, 'mail.vote', self.session.context, [['msg_id','=',parseInt(message_id)]]);
 +            mail_vote.read_slice(['user_id']).then(function(result){
 +                vote_count = result.length;
 +                is_vote_liked = false;
 +                _.each(result, function(vote){
 +                    if (self.session.uid == vote.user_id[0]){
 +                        is_vote_liked = true;
 +                    }
 +                });
 +                parent_element = self.find_parent_element(".oe_mail_msg_vote", message_id);
 +                vote_element = session.web.qweb.render('VoteDisplay', {'msg_id': message_id, 'vote_count': vote_count, 'is_vote_liked': is_vote_liked});
 +                $(parent_element).html(vote_element);
 +                self.add_vote_event($(parent_element));
 +            });
 +        },
 +        
 +        subscribe_vote: function(message_id){
 +            var self = this;
 +            this.mail_message = new session.web.DataSet(this, 'mail.message');
 +            return this.mail_message.call('vote_toggle', [[parseInt(message_id)]]).then(function(result){
 +                self.render_vote(message_id);
 +            });
 +        },
-          
-         /**
-          * Override-hack of do_action: automatically reload the chatter.
-          * Normally it should be called only when clicking on 'Post/Send'
-          * in the composition form. */
-         do_action: function(action, on_close) {
-             this.init_comments();
-             if (this.compose_message_widget) {
-                 this.compose_message_widget.reinit(); }
-             return this._super(action, on_close);
-         },
-         instantiate_composition_form: function(mode, email_mode, formatting, msg_id, context) {
-             if (this.compose_message_widget) {
-                 this.compose_message_widget.destroy();
-             }
-             this.compose_message_widget = new mail.ComposeMessage(this, {
-                 'extended_mode': false, 'uid': this.params.uid, 'res_model': this.params.res_model,
-                 'res_id': this.params.res_id, 'mode': mode || 'comment', 'msg_id': msg_id,
-                 'email_mode': email_mode || false, 'formatting': formatting || false,
-                 'context': context || false } );
-             var composition_node = this.$el.find('div.oe_mail_thread_action');
-             composition_node.empty();
-             var compose_done = this.compose_message_widget.appendTo(composition_node);
-             return compose_done;
-         },
 +
+         /** Customize the display
+          * - show_header_compose: show the composition form in the header */
          do_customize_display: function() {
-             if (this.display.show_post_comment) { this.$el.find('div.oe_mail_thread_action').eq(0).show(); }
+             this.display_user_avatar();
+             if (this.display.show_header_compose) { this.$el.find('div.oe_mail_thread_action').eq(0).show(); }
          },
  
          /**
                  if (! confirm(_t("Do you really want to delete this message?"))) { return false; }
                  var msg_id = event.srcElement.dataset.id;
                  if (! msg_id) return false;
-                 var call_defer = self.ds_msg.unlink([parseInt(msg_id)]);
-                 $(event.srcElement).parents('li.oe_mail_thread_msg').eq(0).hide();
-                 if (self.params.thread_level > 0) {
-                     $(event.srcElement).parents('.oe_mail_thread').eq(0).hide();
-                 }
-                 event.preventDefault();
-                 return call_defer;
+                 $(event.srcElement).parents('li.oe_mail_thread_msg').eq(0).remove();
+                 return self.ds_msg.unlink([parseInt(msg_id)]);
              });
              // event: click on 'Hide' in msg side menu
-             this.$el.find('div.oe_mail_thread_display').delegate('a.oe_mail_msg_hide', 'click', function (event) {
-                 if (! confirm(_t("Do you really want to hide this thread ?"))) { return false; }
+             this.$el.on('click', 'a.oe_mail_msg_hide', function (event) {
+                 event.preventDefault();
+                 event.stopPropagation();
                  var msg_id = event.srcElement.dataset.id;
                  if (! msg_id) return false;
-                 var call_defer = self.ds.call('message_remove_pushed_notifications', [[self.params.res_id], [parseInt(msg_id)], true]);
-                 $(event.srcElement).parents('li.oe_mail_thread_msg').eq(0).hide();
-                 if (self.params.thread_level > 0) {
-                     $(event.srcElement).parents('.oe_mail_thread').eq(0).hide();
-                 }
-                 event.preventDefault();
-                 return call_defer;
+                 $(event.srcElement).parents('li.oe_mail_thread_msg').eq(0).remove();
+                 return self.ds_notif.call('set_message_read', [parseInt(msg_id)]);
              });
-             // event: click on "Reply" in msg side menu (email style)
-             this.$el.find('div.oe_mail_thread_display').delegate('a.oe_mail_msg_reply_by_email', 'click', function (event) {
+             // event: click on "Reply by email" in msg side menu (email style)
+             this.$el.on('click', 'a.oe_mail_msg_reply_by_email', function (event) {
+                 event.preventDefault();
+                 event.stopPropagation();
                  var msg_id = event.srcElement.dataset.msg_id;
-                 var email_mode = (event.srcElement.dataset.type == 'email');
-                 var formatting = (event.srcElement.dataset.formatting == 'html');
                  if (! msg_id) return false;
-                 self.instantiate_composition_form('reply', email_mode, formatting, msg_id);
-                 event.preventDefault();
+                 self.compose_message_widget.refresh({
+                     'default_composition_mode': 'reply',
+                     'default_parent_id': parseInt(msg_id),
+                     'default_content_subtype': 'html'} );
              });
          },
-         
-         destroy: function () {
-             this._super.apply(this, arguments);
-         },
-         
-         init_comments: function() {
-             var self = this;
-             this.params.offset = 0;
-             this.comments_structure = {'root_ids': [], 'new_root_ids': [], 'msgs': {}, 'tree_struct': {}, 'model_to_root_ids': {}};
-             this.$el.find('div.oe_mail_thread_display').empty();
-             var domain = this.get_fetch_domain(this.comments_structure);
-             return this.fetch_comments(this.params.limit, this.params.offset, domain).then();
+         /**
+          * Override-hack of do_action: automatically reload the chatter.
+          * Normally it should be called only when clicking on 'Post/Send'
+          * in the composition form. */
+         do_action: function(action, on_close) {
+             this.message_clean();
+             this.message_fetch();
+             if (this.compose_message_widget) {
+                 this.compose_message_widget.refresh({
+                     'default_composition_mode': 'comment',
+                     'default_parent_id': this.options.default_parent_id,
+                     'default_content_subtype': 'plain'} );
+             }
+             return this._super(action, on_close);
          },
-         
-         fetch_comments: function (limit, offset, domain) {
-             var self = this;
-             var defer = this.ds.call('message_read', [[this.params.res_id], (this.params.thread_level > 0), (this.comments_structure['root_ids']),
-                                     (limit+1) || (this.params.limit+1), offset||this.params.offset, domain||undefined ]).then(function (records) {
-                 if (records.length <= self.params.limit) self.display.show_more = false;
-                 // else { self.display.show_more = true; records.pop(); }
-                 // else { self.display.show_more = true; records.splice(0, 1); }
-                 else { self.display.show_more = true; }
-                 self.display_comments(records);
-                 // TODO: move to customize display
-                 if (self.display.show_more == true) self.$el.find('div.oe_mail_thread_more:last').show();
-                 else  self.$el.find('div.oe_mail_thread_more:last').hide();
+         /** Instantiate the composition form, with every parameters in context
+             or in the widget context. */
+         instantiate_composition_form: function(context) {
+             if (this.compose_message_widget) {
+                 this.compose_message_widget.destroy();
+             }
+             this.compose_message_widget = new mail.ComposeMessage(this, {
+                 'context': _.extend(context || {}, this.options.context),
              });
-             
-             return defer;
+             var composition_node = this.$el.find('div.oe_mail_thread_action');
+             composition_node.empty();
+             var compose_done = this.compose_message_widget.appendTo(composition_node);
+             return compose_done;
          },
  
-         display_comments_from_parameters: function (records) {
-             if (records.length > 0 && records.length < (records[0].child_ids.length+1) ) this.display.show_more = true;
-             else this.display.show_more = false;
-             var defer = this.display_comments(records);
-             // TODO: move to customize display
-             if (this.display.show_more == true) $('div.oe_mail_thread_more').eq(-2).show();
-             else $('div.oe_mail_thread_more').eq(-2).hide();
-             return defer;
+         /** Clean the thread */
+         message_clean: function() {
+             this.$el.find('div.oe_mail_thread_display').empty();
          },
-         
-         display_comments: function (records) {
+         /** Fetch messages
+          * @param {Bool} initial_mode: initial mode: try to use message_data or
+          *  message_ids, if nothing available perform a message_read; otherwise
+          *  directly perform a message_read
+          * @param {Array} additional_domain: added to options.domain
+          * @param {Object} additional_context: added to options.context
+          */
+         message_fetch: function (initial_mode, additional_domain, additional_context) {
              var self = this;
-             // sort the records
-             mail.ChatterUtils.records_struct_add_records(this.comments_structure, records, this.params.parent_id);
-             //build attachments download urls and compute time-relative from dates
-             for (var k in records) {
-                 records[k].timerelative = $.timeago(records[k].date);
-                 if (records[k].attachments) {
-                     for (var l in records[k].attachments) {
-                         var url = self.session.origin + '/web/binary/saveas?session_id=' + self.session.session_id + '&model=ir.attachment&field=datas&filename_field=datas_fname&id='+records[k].attachments[l].id;
-                         records[k].attachments[l].url = url;
-                     }
-                 }
+             // domain and context: options + additional
+             fetch_domain = _.flatten([this.options.domain, additional_domain || []], true)
+             fetch_context = _.extend(this.options.context, additional_context || {})
+             // if message_ids is set: try to use it
+             if (initial_mode && this.options.message_data) {
+                 return this.message_display(this.options.message_data);
              }
+             return this.ds_message.call('message_read',
+                 [(initial_mode && this.options.message_ids) || false, fetch_domain, this.options.thread_level, undefined, fetch_context]
+                 ).then(this.proxy('message_display'));
+         },
+         /* Display a list of records
+          * A specific case is done for 'expandable' messages that are messages
+             displayed under a 'show more' button form
+          */
+         message_display: function (records) {
+             var self = this;
+             var _expendable = false;
              _(records).each(function (record) {
 +                //Render Votes.
-                  self.render_vote(record.id);
-                 var sub_msgs = [];
-                 if ((record.parent_id == false || record.parent_id[0] == self.params.parent_id) && self.params.thread_level > 0 ) {
-                     var sub_list = self.comments_structure['tree_struct'][record.id]['direct_childs'];
-                     _(records).each(function (record) {
-                         //if (record.parent_id == false || record.parent_id[0] == self.params.parent_id) return;
-                         if (_.indexOf(sub_list, record.id) != -1) {
-                             sub_msgs.push(record);
-                         }
++                self.render_vote(record.id);
+                 if (record.type == 'expandable') {
+                     _expendable = true;
+                     self.update_fetch_more(true);
+                     self.fetch_more_domain = record.domain;
+                     self.fetch_more_context = record.context;
+                 }
+                 else {
+                     self.display_record(record);
+                     // if (self.options.thread_level >= 0) {
+                     self.thread = new mail.Thread(self, {
+                         'context': {
+                             'default_model': record.model,
+                             'default_id': record.res_id,
+                             'default_parent_id': record.id },
+                         'message_data': record.child_ids, 'thread_level': self.options.thread_level-1,
+                         'show_header_compose': false, 'show_reply': self.options.thread_level > 1,
+                         'show_hide': self.display.show_hide, 'show_delete': self.display.show_delete,
                      });
-                     self.display_comment(record);
-                     self.thread = new mail.Thread(self, {'res_model': self.params.res_model, 'res_id': self.params.res_id, 'uid': self.params.uid,
-                                                             'records': sub_msgs, 'thread_level': (self.params.thread_level-1), 'parent_id': record.id,
-                                                             'is_wall': self.params.is_wall});
                      self.$el.find('li.oe_mail_thread_msg:last').append('<div class="oe_mail_thread_subthread"/>');
                      self.thread.appendTo(self.$el.find('div.oe_mail_thread_subthread:last'));
-                 }
-                 else if (self.params.thread_level == 0) {
-                     self.display_comment(record);
+                     // }
                  }
              });
-             mail.ChatterUtils.records_struct_update_after_display(this.comments_structure);
-             // update offset for "More" buttons
-             if (this.params.thread_level == 0) this.params.offset += records.length;
+             if (! _expendable) {
+                 this.update_fetch_more(false);
+             }
          },
  
-         /** Displays a record, performs text/link formatting */
-         display_comment: function (record) {
-             record.body = mail.ChatterUtils.do_text_nl2br($.trim(record.body), true);
-             // if (record.type == 'email' && record.state == 'received') {
+         /** Displays a record and performs some formatting on the record :
+          * - record.date: formatting according to the user timezone
+          * - record.timerelative: relative time givein by timeago lib
+          * - record.avatar: image url
+          * - record.attachments[].url: url of each attachment
+          * - record.is_author: is the current user the author of the record */
+         display_record: function (record) {
+             // formatting and additional fields
+             record.date = session.web.format_value(record.date, {type:"datetime"});
+             record.timerelative = $.timeago(record.date);
              if (record.type == 'email') {
-                 record.mini_url = ('/mail/static/src/img/email_icon.png');
+                 record.avatar = ('/mail/static/src/img/email_icon.png');
              } else {
-                 record.mini_url = mail.ChatterUtils.get_image(this.session.prefix, this.session.session_id, 'res.users', 'image_small', record.user_id[0]);
+                 record.avatar = mail.ChatterUtils.get_image(this.session.prefix, this.session.session_id, 'res.partner', 'image_small', record.author_id[0]);
              }
-             // body text manipulation
-             if (record.subtype == 'plain') {
-                 record.body = mail.ChatterUtils.do_text_remove_html_tags(record.body);
+             //TDE: FIX
+             if (record.attachments) {
+                 for (var l in record.attachments) {
+                     var url = self.session.origin + '/web/binary/saveas?session_id=' + self.session.session_id + '&model=ir.attachment&field=datas&filename_field=datas_fname&id='+records[k].attachments[l].id;
+                     record.attachments[l].url = url;
+                 }
              }
-             record.body = mail.ChatterUtils.do_replace_expressions(record.body);
-             // format date according to the user timezone
-             record.date = session.web.format_value(record.date, {type:"datetime"});
-             // is the user the author ?
-             record.is_author = mail.ChatterUtils.is_author(this, record.user_id[0]);
-             // render
-             var rendered = session.web.qweb.render('mail.thread.message', {'record': record, 'thread': this, 'params': this.params, 'display': this.display});
-             // expand feature
+             record.is_author = mail.ChatterUtils.is_author(this, record.author_user_id[0]);
+             // render, add the expand feature
+             var rendered = session.web.qweb.render('mail.thread.message', {'record': record, 'thread': this, 'params': this.options, 'display': this.display});
              $(rendered).appendTo(this.$el.children('div.oe_mail_thread_display:first'));
              this.$el.find('div.oe_mail_msg_record_body').expander({
-                 slicePoint: this.params.msg_more_limit,
+                 slicePoint: this.options.msg_more_limit,
                  expandText: 'read more',
                  userCollapseText: '[^]',
                  detailClass: 'oe_mail_msg_tail',
          </div>
      </ul>
  
 +    <!-- Vote system (Like or Unlike -->
 +    <t t-name="VoteDisplay">
 +        <t t-if='vote_count'>
 +             <span class="oe_left oe_mail_vote_string">Votes :<t t-esc="vote_count"/></span>
 +        </t>
 +        <li>
 +        <t t-if="!is_vote_liked">
 +            <button class="oe_mail_msg_vote_like" t-att-data-id="msg_id" title="Click to Vote.">
 +                <span>+1</span>
 +            </button>
 +        </t>
 +        <t t-if="is_vote_liked">
 +            <button class="oe_mail_msg_vote_like" t-att-data-id="msg_id" title="Click to Unvote." style="background:#DC5F59; padding:1px">
 +                <span>-1</span>
 +            </button>
 +        </t>
 +        </li>
 +    </t>
 +
      <!-- default layout -->
      <li t-name="mail.thread.message" class="oe_mail oe_mail_thread_msg">
-         <div t-attf-class="oe_mail_msg_#{record.type}">
-             <img class="oe_mail_icon oe_mail_frame oe_left" t-att-src="record.mini_url"/>
+         <div t-attf-class="oe_mail_msg_#{record.type} oe_semantic_html_override">
+             <img class="oe_mail_icon oe_mail_frame oe_left" t-att-src="record.avatar"/>
              <div class="oe_mail_msg_content">
                  <!-- dropdown menu with message options and actions -->
                  <span class="oe_dropdown_toggle oe_dropdown_arrow">
                      <div class="oe_clear"/>
                      <ul class="oe_mail_msg_footer">
                        <li t-if="record.subject &amp; params.thread_level > 0"><a t-attf-href="#model=#{params.res_model}&amp;id=#{params.res_id}"><t t-raw="record.record_name"/></a></li>
-                       <li><a t-attf-href="#model=res.users&amp;id=#{record.user_id[0]}"><t t-raw="record.user_id[1]"/></a></li>
+                       <li><a t-attf-href="#model=res.partner&amp;id=#{record.author_id[0]}"><t t-raw="record.author_id[1]"/></a></li>
                        <li><span t-att-title="record.date"><t t-raw="record.timerelative"/></span></li>
 +                      <li> <span t-att-data-id="record.id" class="oe_mail_msg_vote"></span> </li>
                        <li t-if="display['show_reply']"><a href="#" class="oe_mail_msg_reply">Reply</a></li>
                        <!-- uncomment when merging vote
                        <li><a href="#">Like</a></li>