changeset 716:3b91225b457a

server + browser side: blogging refactoring (draft), huge commit sorry: /!\ everything is not working yet, group blog is not working for now - adaptation to backend changes - frontend commons part of blog have been moved to QuickFrontend - (editors) button "WYSIWYG edition" renamed to "preview" - (editors) Shift + [ENTER] is now used to send a text message, [ENTER] to finish a ligne normally - (editors) fixed modifiers handling - global simplification, resulting of the refactoring - with backend refactoring, we are now using PEP again, XEP-0277 compatibility is restored \o/
author Goffi <goffi@goffi.org>
date Sun, 16 Aug 2015 01:51:12 +0200
parents b2465423c76e
children 29b84af2ff7b
files src/browser/libervia_main.py src/browser/sat_browser/blog.py src/browser/sat_browser/contact_list.py src/browser/sat_browser/editor_widget.py src/browser/sat_browser/json.py src/browser/sat_browser/richtext.py src/server/server.py
diffstat 7 files changed, 675 insertions(+), 823 deletions(-) [+]
line wrap: on
line diff
--- a/src/browser/libervia_main.py	Tue Jul 28 22:22:10 2015 +0200
+++ b/src/browser/libervia_main.py	Sun Aug 16 01:51:12 2015 +0200
@@ -38,7 +38,7 @@
 from sat.core.i18n import _
 
 from pyjamas.ui.RootPanel import RootPanel
-from pyjamas.ui.HTML import HTML
+# from pyjamas.ui.HTML import HTML
 from pyjamas.ui.KeyboardListener import KEY_ESCAPE
 from pyjamas.Timer import Timer
 from pyjamas import Window, DOM
@@ -69,7 +69,7 @@
 unicode = str  # FIXME: pyjamas workaround
 
 
-MAX_MBLOG_CACHE = 500  # Max microblog entries kept in memories
+# MAX_MBLOG_CACHE = 500  # Max microblog entries kept in memories # FIXME
 
 
 class SatWebFrontend(InputHistory, QuickApp):
@@ -357,19 +357,25 @@
     def profilePlugged(self, dummy):
         self._profile_plugged = True
         QuickApp.profilePlugged(self, C.PROF_KEY_NONE)
+        # XXX: as contact_list.update() is slow and it's called a lot of time
+        #      during profile plugging, we prevent it before it's plugged
+        #      and do all at once now
+        contact_list = self.contact_list
+        contact_list.update()
 
-        microblog_widget = self.displayWidget(blog.MicroblogPanel, ())
-        self.setSelected(microblog_widget)
+
+        blog_widget = self.displayWidget(blog.Blog, ())
+        self.setSelected(blog_widget)
 
         # we fill the panels already here
-        for wid in self.widgets.getWidgets(blog.MicroblogPanel):
-            if wid.accept_all():
-                self.bridge.getMassiveMblogs('ALL', (), None, profile=C.PROF_KEY_NONE, callback=wid.massiveInsert)
-            else:
-                self.bridge.getMassiveMblogs('GROUP', list(wid.accepted_groups), None, profile=C.PROF_KEY_NONE, callback=wid.massiveInsert)
+        # for wid in self.widgets.getWidgets(blog.MicroblogPanel):
+        #     if wid.accept_all():
+        #         self.bridge.getMassiveMblogs('ALL', (), None, profile=C.PROF_KEY_NONE, callback=wid.massiveInsert)
+        #     else:
+        #         self.bridge.getMassiveMblogs('GROUP', list(wid.accepted_groups), None, profile=C.PROF_KEY_NONE, callback=wid.massiveInsert)
 
         #we ask for our own microblogs:
-        self.loadOurMainEntries()
+        # self.loadOurMainEntries()
 
         def gotDefaultMUC(default_muc):
             self.default_muc = default_muc
@@ -438,6 +444,7 @@
             ui = xmlui.create(self, xml_data=data['xmlui'])
             ui.show()
         elif "public_blog" in data:
+            # FIXME: remove this, this is not generic !
             # TODO: use the bare instead of node when all blogs can be retrieved
             node = jid.JID(data['public_blog']).node
             # FIXME: "/blog/{}" won't work with unicode nodes
@@ -461,126 +468,94 @@
             data = {}
         self.bridge.launchAction(callback_id, data, profile=profile, callback=self._actionCb, errback=self._actionEb)
 
-    def _ownBlogsFills(self, mblogs, mblog_panel=None):
-        """Put our own microblogs in cache, then fill the panels with them.
+    # def _ownBlogsFills(self, mblogs, mblog_panel=None):
+    #     """Put our own microblogs in cache, then fill the panels with them.
 
-        @param mblogs (dict): dictionary mapping a publisher JID to blogs data.
-        @param mblog_panel (MicroblogPanel): the panel to fill, or all if None.
-        """
-        cache = []
-        for publisher in mblogs:
-            for mblog in mblogs[publisher][0]:
-                if 'content' not in mblog:
-                    log.warning(u"No content found in microblog [%s]" % mblog)
-                    continue
-                if 'groups' in mblog:
-                    _groups = set(mblog['groups'].split() if mblog['groups'] else [])
-                else:
-                    _groups = None
-                mblog_entry = blog.MicroblogItem(mblog)
-                cache.append((_groups, mblog_entry))
+    #     @param mblogs (dict): dictionary mapping a publisher JID to blogs data.
+    #     @param mblog_panel (MicroblogPanel): the panel to fill, or all if None.
+    #     """
+    #     cache = []
+    #     for publisher in mblogs:
+    #         for mblog in mblogs[publisher][0]:
+    #             if 'content' not in mblog:
+    #                 log.warning(u"No content found in microblog [%s]" % mblog)
+    #                 continue
+    #             if 'groups' in mblog:
+    #                 _groups = set(mblog['groups'].split() if mblog['groups'] else [])
+    #             else:
+    #                 _groups = None
+    #             mblog_entry = blog.MicroblogItem(mblog)
+    #             cache.append((_groups, mblog_entry))
 
-        self.mblog_cache.extend(cache)
-        if len(self.mblog_cache) > MAX_MBLOG_CACHE:
-            del self.mblog_cache[0:len(self.mblog_cache - MAX_MBLOG_CACHE)]
+    #     self.mblog_cache.extend(cache)
+    #     if len(self.mblog_cache) > MAX_MBLOG_CACHE:
+    #         del self.mblog_cache[0:len(self.mblog_cache - MAX_MBLOG_CACHE)]
 
-        widget_list = [mblog_panel] if mblog_panel else self.widgets.getWidgets(blog.MicroblogPanel)
+    #     widget_list = [mblog_panel] if mblog_panel else self.widgets.getWidgets(blog.MicroblogPanel)
 
-        for wid in widget_list:
-            self.fillMicroblogPanel(wid, cache)
+    #     for wid in widget_list:
+    #         self.fillMicroblogPanel(wid, cache)
 
-        # FIXME
+    #     # FIXME
 
-        if self.initialised:
-            return
-        self.initialised = True  # initialisation phase is finished here
-        for event_data in self.init_cache:  # so we have to send all the cached events
-            self.personalEventHandler(*event_data)
-        del self.init_cache
+    #     if self.initialised:
+    #         return
+    #     self.initialised = True  # initialisation phase is finished here
+    #     for event_data in self.init_cache:  # so we have to send all the cached events
+    #         self.personalEventHandler(*event_data)
+    #     del self.init_cache
 
-    def loadOurMainEntries(self, index=0, mblog_panel=None):
-        """Load a page of our own blogs from the cache or ask them to the
-        backend. Then fill the panels with them.
+    # def loadOurMainEntries(self, index=0, mblog_panel=None):
+    #     """Load a page of our own blogs from the cache or ask them to the
+    #     backend. Then fill the panels with them.
 
-        @param index (int): starting index of the blog page to retrieve.
-        @param mblog_panel (MicroblogPanel): the panel to fill, or all if None.
-        """
-        delta = index - self.next_rsm_index
-        if delta < 0:
-            assert mblog_panel is not None
-            self.fillMicroblogPanel(mblog_panel, self.mblog_cache[index:index + C.RSM_MAX_ITEMS])
-            return
+    #     @param index (int): starting index of the blog page to retrieve.
+    #     @param mblog_panel (MicroblogPanel): the panel to fill, or all if None.
+    #     """
+    #     delta = index - self.next_rsm_index
+    #     if delta < 0:
+    #         assert mblog_panel is not None
+    #         self.fillMicroblogPanel(mblog_panel, self.mblog_cache[index:index + C.RSM_MAX_ITEMS])
+    #         return
 
-        def cb(result):
-            self._ownBlogsFills(result, mblog_panel)
+    #     def cb(result):
+    #         self._ownBlogsFills(result, mblog_panel)
 
-        rsm = {'max_': str(delta + C.RSM_MAX_ITEMS), 'index': str(self.next_rsm_index)}
-        self.bridge.getMassiveMblogs('JID', [unicode(self.whoami.bare)], rsm, callback=cb, profile=C.PROF_KEY_NONE)
-        self.next_rsm_index = index + C.RSM_MAX_ITEMS
+    #     rsm = {'max_': str(delta + C.RSM_MAX_ITEMS), 'index': str(self.next_rsm_index)}
+    #     self.bridge.getMassiveMblogs('JID', [unicode(self.whoami.bare)], rsm, callback=cb, profile=C.PROF_KEY_NONE)
+    #     self.next_rsm_index = index + C.RSM_MAX_ITEMS
 
     ## Signals callbacks ##
 
-    def personalEventHandler(self, sender, event_type, data):
-        # FIXME: move some code from here to QuickApp
-        if not self.initialised:
-            self.init_cache.append((sender, event_type, data))
-            return
-        sender = jid.JID(sender).bare
-        if event_type == "MICROBLOG":
-            if not 'content' in data:
-                log.warning("No content found in microblog data")
-                return
-            if 'groups' in data:
-                _groups = set(data['groups'].split() if data['groups'] else [])
-            else:
-                _groups = None
-            mblog_entry = blog.MicroblogItem(data)
+    # def personalEventHandler(self, sender, event_type, data):
+        # elif event_type == 'MICROBLOG_DELETE':
+        #     for wid in self.widgets.getWidgets(blog.MicroblogPanel):
+        #         wid.removeEntry(data['type'], data['id'])
 
-            for wid in self.widgets.getWidgets(blog.MicroblogPanel):
-                wid.addEntryIfAccepted(sender, _groups, mblog_entry)
+        #     if sender == self.whoami.bare and data['type'] == 'main_item':
+        #         for index in xrange(0, len(self.mblog_cache)):
+        #             entry = self.mblog_cache[index]
+        #             if entry[1].id == data['id']:
+        #                 self.mblog_cache.remove(entry)
+        #                 break
+
+    # def fillMicroblogPanel(self, mblog_panel, mblogs):
+    #     """Fill a microblog panel with entries in cache
 
-            if sender == self.whoami.bare:
-                found = False
-                for index in xrange(0, len(self.mblog_cache)):
-                    entry = self.mblog_cache[index]
-                    if entry[1].id == mblog_entry.id:
-                        # replace existing entry
-                        self.mblog_cache.remove(entry)
-                        self.mblog_cache.insert(index, (_groups, mblog_entry))
-                        found = True
-                        break
-                if not found:
-                    self.mblog_cache.append((_groups, mblog_entry))
-                    if len(self.mblog_cache) > MAX_MBLOG_CACHE:
-                        del self.mblog_cache[0:len(self.mblog_cache - MAX_MBLOG_CACHE)]
-        elif event_type == 'MICROBLOG_DELETE':
-            for wid in self.widgets.getWidgets(blog.MicroblogPanel):
-                wid.removeEntry(data['type'], data['id'])
+    #     @param mblog_panel: MicroblogPanel instance
+    #     """
+    #     #XXX: only our own entries are cached
+    #     for cache_entry in mblogs:
+    #         _groups, mblog_entry = cache_entry
+    #         mblog_panel.addEntryIfAccepted(self.whoami.bare, *cache_entry)
 
-            if sender == self.whoami.bare and data['type'] == 'main_item':
-                for index in xrange(0, len(self.mblog_cache)):
-                    entry = self.mblog_cache[index]
-                    if entry[1].id == data['id']:
-                        self.mblog_cache.remove(entry)
-                        break
-
-    def fillMicroblogPanel(self, mblog_panel, mblogs):
-        """Fill a microblog panel with entries in cache
-
-        @param mblog_panel: MicroblogPanel instance
-        """
-        #XXX: only our own entries are cached
-        for cache_entry in mblogs:
-            _groups, mblog_entry = cache_entry
-            mblog_panel.addEntryIfAccepted(self.whoami.bare, *cache_entry)
-
-    def getEntityMBlog(self, entity):
-        # FIXME: call this after a contact has been added to roster
-        log.info(u"geting mblog for entity [%s]" % (entity,))
-        for lib_wid in self.libervia_widgets:
-            if isinstance(lib_wid, blog.MicroblogPanel):
-                if lib_wid.isJidAccepted(entity):
-                    self.bridge.call('getMassiveMblogs', lib_wid.massiveInsert, 'JID', [unicode(entity)])
+    # def getEntityMBlog(self, entity):
+    #     # FIXME: call this after a contact has been added to roster
+    #     log.info(u"geting mblog for entity [%s]" % (entity,))
+    #     for lib_wid in self.libervia_widgets:
+    #         if isinstance(lib_wid, blog.MicroblogPanel):
+    #             if lib_wid.isJidAccepted(entity):
+    #                 self.bridge.call('getMassiveMblogs', lib_wid.massiveInsert, 'JID', [unicode(entity)])
 
     def displayWidget(self, class_, target, dropped=False, new_tab=None, *args, **kwargs):
         """Get or create a LiberviaWidget and select it. When the user dropped
--- a/src/browser/sat_browser/blog.py	Tue Jul 28 22:22:10 2015 +0200
+++ b/src/browser/sat_browser/blog.py	Sun Aug 16 01:51:12 2015 +0200
@@ -21,23 +21,24 @@
 from sat.core.log import getLogger
 log = getLogger(__name__)
 
-from sat.core.i18n import _, D_
+from sat.core.i18n import _ #, D_
 
 from pyjamas.ui.SimplePanel import SimplePanel
 from pyjamas.ui.VerticalPanel import VerticalPanel
+from pyjamas.ui.ScrollPanel import ScrollPanel
 from pyjamas.ui.HorizontalPanel import HorizontalPanel
 from pyjamas.ui.Label import Label
 from pyjamas.ui.HTML import HTML
 from pyjamas.ui.Image import Image
 from pyjamas.ui.ClickListener import ClickHandler
 from pyjamas.ui.FlowPanel import FlowPanel
-from pyjamas.ui.KeyboardListener import KEY_ENTER, KeyboardHandler
+from pyjamas.ui import KeyboardListener as keyb
+from pyjamas.ui.KeyboardListener import KeyboardHandler
 from pyjamas.ui.FocusListener import FocusHandler
 from pyjamas.ui.MouseListener import MouseHandler
 from pyjamas.Timer import Timer
 
 from datetime import datetime
-from time import time
 
 import html_tools
 import dialog
@@ -46,50 +47,19 @@
 import libervia_widget
 from constants import Const as C
 from sat_frontends.quick_frontend import quick_widgets
-from sat_frontends.tools import jid
-
+from sat_frontends.quick_frontend import quick_blog
 
 unicode = str # XXX: pyjamas doesn't manage unicode
+ENTRY_RICH = (C.ENTRY_MODE_RICH, C.ENTRY_MODE_XHTML)
 
 
-class MicroblogItem():
-    # XXX: should be moved in a separated module
+class Entry(quick_blog.Entry, VerticalPanel, ClickHandler, FocusHandler, KeyboardHandler):
+    """Graphical representation of a quick_blog.Item"""
 
-    def __init__(self, data):
-        self.id = data['id']
-        self.type = data.get('type', 'main_item')
-        self.empty = data.get('new', False)
-        self.title = data.get('title', '')
-        self.title_xhtml = data.get('title_xhtml', '')
-        self.content = data.get('content', '')
-        self.content_xhtml = data.get('content_xhtml', '')
-        self.author = jid.JID(data['author'])
-        self.updated = float(data.get('updated', 0))  # XXX: int doesn't work here
-        self.published = float(data.get('published', self.updated))  # XXX: int doesn't work here
-        self.service = data.get('service', '')
-        self.node = data.get('node', '')
-        self.comments = data.get('comments', False)
-        self.comments_service = data.get('comments_service', '')
-        self.comments_node = data.get('comments_node', '')
-
+    def __init__(self, manager, item_data=None, comments_data=None, service=None, node=None):
+        quick_blog.Entry.__init__(self, manager, item_data, comments_data, service, node)
 
-class MicroblogEntry(SimplePanel, ClickHandler, FocusHandler, KeyboardHandler):
-
-    def __init__(self, blog_panel, data):
-        """
-        @param blog_panel: the parent panel
-        @param data: dict containing the blog item data, or a MicroblogItem instance.
-        """
-        self._base_item = data if isinstance(data, MicroblogItem) else MicroblogItem(data)
-        for attr in ['id', 'type', 'empty', 'title', 'title_xhtml', 'content', 'content_xhtml',
-                     'author', 'updated', 'published', 'comments', 'service', 'node',
-                     'comments_service', 'comments_node']:
-            getter = lambda attr: lambda inst: getattr(inst._base_item, attr)
-            setter = lambda attr: lambda inst, value: setattr(inst._base_item, attr, value)
-            setattr(MicroblogEntry, attr, property(getter(attr), setter(attr)))
-
-        SimplePanel.__init__(self)
-        self._blog_panel = blog_panel
+        VerticalPanel.__init__(self)
 
         self.panel = FlowPanel()
         self.panel.setStyleName('mb_entry')
@@ -103,8 +73,9 @@
 
         entry_avatar = SimplePanel()
         entry_avatar.setStyleName('mb_entry_avatar')
-        assert isinstance(self.author, jid.JID) # FIXME: temporary
-        self.avatar = Image(self._blog_panel.host.getAvatarURL(self.author)) # FIXME: self.author should be initially a jid.JID
+        author_jid = self.author_jid
+        self.avatar = Image(self.blog.host.getAvatarURL(author_jid) if author_jid is not None else C.DEFAULT_AVATAR_URL)
+        # TODO: show a warning icon if author is not validated
         entry_avatar.add(self.avatar)
         self.panel.add(entry_avatar)
 
@@ -112,76 +83,69 @@
         self.entry_dialog.setStyleName('mb_entry_dialog')
         self.panel.add(self.entry_dialog)
 
+        self.comments_panel = None
+        self._current_comment = None
+
         self.add(self.panel)
         ClickHandler.__init__(self)
         self.addClickListener(self)
 
-        self.__pub_data = (self.service, self.node, self.id)
-        self.__setContent()
+        self.refresh()
+        self.displayed = False # True when entry is added to parent
+        if comments_data:
+            self.addComments(comments_data)
 
-    def __setContent(self):
-        """Actually set the entry content (header, icons, bubble...)"""
-        self.delete_label = self.update_label = self.comment_label = None
-        self.bubble = self._current_comment = None
-        self.__setHeader()
-        self.__setBubble()
-        self.__setIcons()
+    def refresh(self):
+        self.header.clear()
+        self.entry_dialog.clear()
+        self.entry_actions.clear()
+        self._setHeader()
+        self._setBubble()
+        self._setIcons()
 
-    def __setHeader(self):
+    def _setHeader(self):
         """Set the entry header."""
-        if self.empty:
-            return
-        update_text = u" — ✍ " + "<span class='mb_entry_timestamp'>%s</span>" % datetime.fromtimestamp(self.updated)
-        self.header.add(HTML("""<span class='mb_entry_header_info'>
-                                  <span class='mb_entry_author'>%(author)s</span> on
-                                  <span class='mb_entry_timestamp'>%(published)s</span>%(updated)s
-                                </span>""" % {'author': html_tools.html_sanitize(unicode(self.author)),
-                                              'published': datetime.fromtimestamp(self.published),
-                                              'updated': update_text if self.published != self.updated else ''
-                                              }))
-        if self.comments:
-            self.comments_count = self.hidden_count = 0
-            self.show_comments_link = HTML('')
-            self.header.add(self.show_comments_link)
-
-    def updateHeader(self, comments_count=None, hidden_count=None, inc=None):
-        """Update the header.
+        if not self.new:
+            update_text = u" — ✍ " + "<span class='mb_entry_timestamp'>%s</span>" % datetime.fromtimestamp(self.item.updated)
+            self.header.add(HTML("""<span class='mb_entry_header_info'>
+                                      <span class='mb_entry_author'>%(author)s</span> on
+                                      <span class='mb_entry_timestamp'>%(published)s</span>%(updated)s
+                                    </span>""" % {'author': html_tools.html_sanitize(unicode(self.item.author)),
+                                                  'published': datetime.fromtimestamp(self.item.published) if self.item.published is not None else '',
+                                                  'updated': update_text if self.item.published != self.item.updated else ''
+                                                  }))
+            if self.item.comments:
+                self.show_comments_link = HTML('')
+                self.header.add(self.show_comments_link)
 
-        @param comments_count (int): total number of comments.
-        @param hidden_count (int): number of hidden comments.
-        @param inc (int): number to increment the total number of comments with.
-        """
-        if comments_count is not None:
-            self.comments_count = comments_count
-        if hidden_count is not None:
-            self.hidden_count = hidden_count
-        if inc is not None:
-            self.comments_count += inc
+    def _setBubble(self):
+        """Set the bubble displaying the initial content."""
+        content = {'text': self.item.content_xhtml if self.item.content_xhtml else self.item.content or '',
+                   'title': self.item.title_xhtml if self.item.title_xhtml else self.item.title or ''}
 
-        if self.hidden_count > 0:
-            comments = D_('comments') if self.hidden_count > 1 else D_('comment')
-            text = D_("<a>show %(count)d previous %(comments)s</a>") % {'count': self.hidden_count,
-                                                                        'comments': comments}
-            if self not in self.show_comments_link._clickListeners:
-                self.show_comments_link.addClickListener(self)
+        if self.mode == C.ENTRY_MODE_TEXT:
+            # assume raw text message have no title
+            self.bubble = editor_widget.LightTextEditor(content, modifiedCb=self._modifiedCb, afterEditCb=self._afterEditCb, options={'no_xhtml': True})
+        elif self.mode in ENTRY_RICH:
+            content['syntax'] = C.SYNTAX_XHTML
+            if self.new:
+                options = []
+            elif self.item.author_jid == self.blog.host.whoami.bare:
+                options = ['update_msg']
+            else:
+                options = ['read_only']
+            self.bubble = richtext.RichTextEditor(self.blog.host, content, modifiedCb=self._modifiedCb, afterEditCb=self._afterEditCb, options=options)
         else:
-            if self.comments_count > 1:
-                text = "%(count)d %(comments)s" % {'count': self.comments_count,
-                                                   'comments': D_('comments')}
-            elif self.comments_count == 1:
-                text = D_('1 comment')
-            else:
-                text = ''
-            try:
-                self.show_comments_link.removeClickListener(self)
-            except ValueError:
-                pass
+            log.error("Bad entry mode: %s" % self.mode)
+        self.bubble.addStyleName("bubble")
+        self._showSyntaxSwitchButton(False)
+        self.entry_dialog.add(self.bubble)
+        self.bubble.addEditListener(self._showWarning) # FIXME: remove edit listeners
+        self._setEditable()
 
-        self.show_comments_link.setHTML("""<span class='mb_entry_comments'>%(text)s</span></div>""" % {'text': text})
-
-    def __setIcons(self):
+    def _setIcons(self):
         """Set the entry icons (delete, update, comment)"""
-        if self.empty:
+        if self.new:
             return
 
         def addIcon(label, title):
@@ -191,269 +155,291 @@
             self.entry_actions.add(label)
             return label
 
-        if self.comments:
+        if self.item.comments:
             self.comment_label = addIcon(u"↶", "Comment this message")
             self.comment_label.setStyleName('mb_entry_action_larger')
-        is_publisher = self.author == self._blog_panel.host.whoami.bare
+        is_publisher = self.item.author_jid == self.blog.host.whoami.bare
         if is_publisher:
             self.update_label = addIcon(u"✍", "Edit this message")
-        if is_publisher or unicode(self.node).endswith(unicode(self._blog_panel.host.whoami.bare)):
+            # TODO: add delete button if we are the owner of the node
             self.delete_label = addIcon(u"✗", "Delete this message")
 
-    def updateAvatar(self, new_avatar):
-        """Change the avatar of the entry
-        @param new_avatar: path to the new image"""
-        self.avatar.setUrl(new_avatar)
-
-    def onClick(self, sender):
-        if sender == self:
-            self._blog_panel.setSelectedEntry(self)
-        elif sender == self.delete_label:
-            self._delete()
-        elif sender == self.update_label:
-            self.edit(True)
-        elif sender == self.comment_label:
-            self._comment()
-        elif sender == self.show_comments_link:
-            self._blog_panel.loadAllCommentsForEntry(self)
+    def _createCommentsPanel(self):
+        """Create the panel if it doesn't exists"""
+        if self.comments_panel is None:
+            self.comments_panel = VerticalPanel()
+            self.comments_panel.setStyleName('microblogPanel')
+            self.comments_panel.addStyleName('subPanel')
+            self.add(self.comments_panel)
 
-    def __modifiedCb(self, content):
-        """Send the new content to the backend
-        @return: False to restore the original content if a deletion has been cancelled
-        """
-        if not content['text']:  # previous content has been emptied
-            self._delete(True)
-            return False
-        extra = {'published': unicode(self.published)}
-        if isinstance(self.bubble, richtext.RichTextEditor):
-            # TODO: if the user change his parameters after the message edition started,
-            # the message syntax could be different then the current syntax: pass the
-            # message syntax in extra for the frontend to use it instead of current syntax.
-            extra.update({'content_rich': content['text'], 'title': content['title']})
-        if self.empty:
-            if self.type == 'main_item':
-                self._blog_panel.host.bridge.call('sendMblog', None, None, tuple(self._blog_panel.accepted_groups), content['text'], extra)
+    def _setEditable(self):
+        self.bubble.edit(self.editable)
+        if self.editable:
+            self._showSyntaxSwitchButton()
+
+    def setEditable(self, editable=True):
+        self.editable = editable
+        self._setEditable()
+
+    def _showSyntaxSwitchButton(self, show=True):
+        if show:
+            if self.mode == C.ENTRY_MODE_TEXT:
+                html = '<a style="color: blue;">rich text</a>'
             else:
-                self._blog_panel.host.bridge.call('sendMblogComment', None, self._parent_entry.comments, content['text'], extra)
+                html = '<a style="color: blue;">raw text</a>'
+            self.toggle_syntax_button = HTML(html)
+            self.toggle_syntax_button.addClickListener(self.toggleContentSyntax)
+            self.toggle_syntax_button.addStyleName('mb_entry_toggle_syntax')
+            self.entry_dialog.add(self.toggle_syntax_button)
+            self.toggle_syntax_button.setStyleAttribute('top', '-20px')  # XXX: need to force CSS
+            self.toggle_syntax_button.setStyleAttribute('left', '-20px')
         else:
-            self._blog_panel.host.bridge.call('updateMblog', None, self.__pub_data, self.comments, content['text'], extra)
-        return True
+            try:
+                self.toggle_syntax_button.removeFromParent()
+            except (AttributeError, TypeError):
+                pass
 
-    def __afterEditCb(self, content):
-        """Remove the entry if it was an empty one (used for creating a new blog post).
-        Data for the actual new blog post will be received from the bridge"""
-        if self.empty:
-            self._blog_panel.removeEntry(self.type, self.id, update_header=False)
-            if self.type == 'main_item':  # restore the "New message" button
-                self._blog_panel.addNewMessageEntry()
-            else:  # allow to create a new comment
-                self._parent_entry._current_comment = None
-        self.entry_dialog.setWidth('auto')
-        try:
-            self.toggle_syntax_button.removeFromParent()
-        except (AttributeError, TypeError):
-            pass
 
-    def __setBubble(self, edit=False):
-        """Set the bubble displaying the initial content."""
-        content = {'text': self.content_xhtml if self.content_xhtml else self.content,
-                   'title': self.title_xhtml if self.title_xhtml else self.title}
-        if self.content_xhtml:
-            content.update({'syntax': C.SYNTAX_XHTML})
-            if self.author != self._blog_panel.host.whoami.bare:
-                options = ['read_only']
-            else:
-                options = [] if self.empty else ['update_msg']
-            self.bubble = richtext.RichTextEditor(self._blog_panel.host, content, self.__modifiedCb, self.__afterEditCb, options)
-        else:  # assume raw text message have no title
-            self.bubble = editor_widget.LightTextEditor(content, self.__modifiedCb, self.__afterEditCb, options={'no_xhtml': True})
-        self.bubble.addStyleName("bubble")
+    def edit(self, edit=True):
+        """Toggle the bubble between display and edit mode"""
         try:
             self.toggle_syntax_button.removeFromParent()
         except (AttributeError, TypeError):
             pass
-        self.entry_dialog.add(self.bubble)
-        self.edit(edit)
-        self.bubble.addEditListener(self.__showWarning)
+        self.bubble.edit(edit)
+        if edit:
+            if self.mode in ENTRY_RICH:
+                # image = '<a class="richTextIcon">A</a>'
+                html = '<a style="color: blue;">raw text</a>'
+                # title = _('Switch to raw text edition')
+            else:
+                # image = '<img src="media/icons/tango/actions/32/format-text-italic.png" class="richTextIcon"/>'
+                html = '<a style="color: blue;">rich text</a>'
+                # title = _('Switch to rich text edition')
+            self.toggle_syntax_button = HTML(html)
+            self.toggle_syntax_button.addClickListener(self.toggleContentSyntax)
+            self.toggle_syntax_button.addStyleName('mb_entry_toggle_syntax')
+            self.entry_dialog.add(self.toggle_syntax_button)
+            self.toggle_syntax_button.setStyleAttribute('top', '-20px')  # XXX: need to force CSS
+            self.toggle_syntax_button.setStyleAttribute('left', '-20px')
+
+    # def updateAvatar(self, new_avatar):
+    #     """Change the avatar of the entry
+    #     @param new_avatar: path to the new image"""
+    #     self.avatar.setUrl(new_avatar)
+
+    def onClick(self, sender):
+        if sender == self:
+            self.blog.setSelectedEntry(self)
+        elif sender == self.delete_label:
+            self._onRetractClick()
+        elif sender == self.update_label:
+            self.edit(True)
+        elif sender == self.comment_label:
+            self._onCommentClick()
+        # elif sender == self.show_comments_link:
+        #     self._blog_panel.loadAllCommentsForEntry(self)
+
+    def _modifiedCb(self, content):
+        """Send the new content to the backend
+
+        @return: False to restore the original content if a deletion has been cancelled
+        """
+        if not content['text']:  # previous content has been emptied
+            return False
+
+        self.item.content = self.item.content_rich = self.item.content_xhtml = None
+        self.item.title = self.item.title_rich = self.item.title_xhtml = None
 
-    def __showWarning(self, sender, keycode):
-        if keycode == KEY_ENTER:
-            self._blog_panel.host.showWarning(None, None)
+        if self.mode in ENTRY_RICH:
+            # TODO: if the user change his parameters after the message edition started,
+            # the message syntax could be different then the current syntax: pass the
+            # message syntax in mb_data for the frontend to use it instead of current syntax.
+            self.item.content_rich = content['text']
+            self.item.title = content['title']
         else:
-            self._blog_panel.host.showWarning(*self._blog_panel.getWarningData(self.type == 'comment'))
+            self.item.content = content['text']
+
+        self.send()
+
+        return True
+
+    def _afterEditCb(self, content):
+        """Post edition treatments
 
-    def _delete(self, empty=False):
-        """Ask confirmation for deletion.
-        @return: False if the deletion has been cancelled."""
+        Remove the entry if it was an empty one (used for creating a new blog post).
+        Data for the actual new blog post will be received from the bridge
+        @param content(dict): edited content
+        """
+        if self.new:
+            if self.level == 0:
+                # we have a main item, we keep the edit entry
+                self.reset(None)
+                # FIXME: would be better to reset bubble
+                # but bubble.setContent() doesn't seem to work
+                self.bubble.removeFromParent()
+                self._setBubble()
+            else:
+                # we don't keep edit entries for comments
+                self.delete()
+        else:
+            self._showSyntaxSwitchButton(False)
+
+    def _showWarning(self, sender, keycode, modifiers):
+        if keycode == keyb.KEY_ENTER & keyb.MODIFIER_SHIFT: # FIXME: fix edit_listeners, it's dirty (we have to check keycode/modifiers twice !)
+            self.blog.host.showWarning(None, None)
+        else:
+            # self.blog.host.showWarning(*self.blog.getWarningData(self.type == 'comment'))
+            self.blog.host.showWarning(*self.blog.getWarningData(False)) # FIXME: comments are not yet reimplemented
+
+    def _onRetractClick(self):
+        """Ask confirmation then retract current entry."""
+        assert not self.new
+
         def confirm_cb(answer):
             if answer:
-                self._blog_panel.host.bridge.call('deleteMblog', None, self.__pub_data, self.comments)
-            else:  # restore the text if it has been emptied during the edition
-                self.bubble.setContent(self.bubble._original_content)
+                self.retract()
 
-        if self.empty:
-            text = _("New ") + (_("message") if self.comments else _("comment")) + _(" without body has been cancelled.")
-            dialog.InfoDialog(_("Information"), text).show()
-            return
-        text = ""
-        if empty:
-            text = (_("Message") if self.comments else _("Comment")) + _(" body has been emptied.<br/>")
-        target = _('message and all its comments') if self.comments else _('comment')
-        text += _("Do you really want to delete this %s?") % target
+        entry_type = _("message") if self.level == 0 else _("comment")
+        and_comments = _(" All comments will be also deleted!") if self.item.comments else ""
+        text = _("Do you really want to delete this {entry_type}?{and_comments}").format(
+                entry_type=entry_type, and_comments=and_comments)
         dialog.ConfirmDialog(confirm_cb, text=text).show()
 
-    def _comment(self):
+    def _onCommentClick(self):
         """Add an empty entry for a new comment"""
-        if self._current_comment:
-            self._current_comment.bubble.setFocus(True)
-            self._blog_panel.setSelectedEntry(self._current_comment, True)
-            return
-        data = {'id': unicode(time()),
-                'new': True,
-                'type': 'comment',
-                'author': unicode(self._blog_panel.host.whoami.bare),
-                'service': self.comments_service,
-                'node': self.comments_node
-                }
-        entry = self._blog_panel.addEntry(data, update_header=False)
-        if entry is None:
-            log.info("The entry of id %s can not be commented" % self.id)
-            return
-        entry._parent_entry = self
-        self._current_comment = entry
-        self.edit(True, entry)
-        self._blog_panel.setSelectedEntry(entry, True)
+        if self._current_comment is None:
+            if not self.item.comments_service or not self.item.comments_node:
+                log.warning("Invalid service and node for comments, can pcreate a comment")
+            self._current_comment = self.addEntry(editable=True, service=self.item.comments_service, node=self.item.comments_node)
+        self.blog.setSelectedEntry(self._current_comment, True)
 
-    def edit(self, edit, entry=None):
-        """Toggle the bubble between display and edit mode
-        @edit: boolean value
-        @entry: MicroblogEntry instance, or None to use self
-        """
-        if entry is None:
-            entry = self
-        try:
-            entry.toggle_syntax_button.removeFromParent()
-        except (AttributeError, TypeError):
-            pass
-        entry.bubble.edit(edit)
-        if edit:
-            if isinstance(entry.bubble, richtext.RichTextEditor):
-                image = '<a class="richTextIcon">A</a>'
-                html = '<a style="color: blue;">raw text</a>'
-                title = _('Switch to raw text edition')
-            else:
-                image = '<img src="media/icons/tango/actions/32/format-text-italic.png" class="richTextIcon"/>'
-                html = '<a style="color: blue;">rich text</a>'
-                title = _('Switch to rich text edition')
-            entry.toggle_syntax_button = HTML(html)
-            entry.toggle_syntax_button.addClickListener(entry.toggleContentSyntax)
-            entry.toggle_syntax_button.addStyleName('mb_entry_toggle_syntax')
-            entry.entry_dialog.add(entry.toggle_syntax_button)
-            entry.toggle_syntax_button.setStyleAttribute('top', '-20px')  # XXX: need to force CSS
-            entry.toggle_syntax_button.setStyleAttribute('left', '-20px')
+    def _changeMode(self, original_content, text):
+        self.mode = C.ENTRY_MODE_RICH if self.mode == C.ENTRY_MODE_TEXT else C.ENTRY_MODE_TEXT
+        if self.mode in ENTRY_RICH and not text:
+            text = ' ' # something different than empty string is needed to initialize the rich text editor
+        self.item.content = text
+        self.content_title = self.content_title_xhtml = ''
+        self.bubble.removeFromParent()
+        self._setBubble()
+        self.bubble.setOriginalContent(original_content)
+        if self.mode in ENTRY_RICH:
+            self.item.content_xhtml = text
+            self.bubble.setDisplayContent()  # needed in case the edition is aborted, to not end with an empty bubble
+        else:
+            self.item.content_xhtml = ''
 
     def toggleContentSyntax(self):
         """Toggle the editor between raw and rich text"""
         original_content = self.bubble.getOriginalContent()
-        rich = not isinstance(self.bubble, richtext.RichTextEditor)
+        rich = self.mode in ENTRY_RICH
         if rich:
             original_content['syntax'] = C.SYNTAX_XHTML
 
-        def setBubble(text):
-            self.content = text
-            self.content_xhtml = text if rich else ''
-            self.content_title = self.content_title_xhtml = ''
-            self.bubble.removeFromParent()
-            self.__setBubble(True)
-            self.bubble.setOriginalContent(original_content)
+        text = self.bubble.getContent()['text']
+
+        if not text.strip():
+            self._changeMode(original_content,'')
+        else:
             if rich:
-                self.bubble.setDisplayContent()  # needed in case the edition is aborted, to not end with an empty bubble
+                def confirm_cb(answer):
+                    if answer:
+                        self.blog.host.bridge.syntaxConvert(text, C.SYNTAX_CURRENT, C.SYNTAX_TEXT, profile=None,
+                                                            callback=lambda converted: self._changeMode(original_content, converted))
+                dialog.ConfirmDialog(confirm_cb, text=_("Do you really want to lose the title and text formatting?")).show()
+            else:
+                self.blog.host.bridge.syntaxConvert(text, C.SYNTAX_TEXT, C.SYNTAX_XHTML, profile=None,
+                                                    callback=lambda converted: self._changeMode(original_content, converted))
 
-        text = self.bubble.getContent()['text']
-        if not text:
-            setBubble(' ')  # something different than empty string is needed to initialize the rich text editor
-            return
-        if not rich:
-            def confirm_cb(answer):
-                if answer:
-                    self._blog_panel.host.bridge.call('syntaxConvert', setBubble, text, C.SYNTAX_CURRENT, C.SYNTAX_TEXT)
-            dialog.ConfirmDialog(confirm_cb, text=_("Do you really want to lose the title and text formatting?")).show()
-        else:
-            self._blog_panel.host.bridge.call('syntaxConvert', setBubble, text, C.SYNTAX_TEXT, C.SYNTAX_XHTML)
+    def update(self, entry=None):
+        """Update comments"""
+        self._createCommentsPanel()
+        self.entries.sort(key=lambda entry: entry.item.published, reverse=True)
+        idx = 0
+        for entry in self.entries:
+            if not entry.displayed:
+                self.comments_panel.insert(entry, idx)
+                entry.displayed = True
+                idx += 1
+
+    def delete(self):
+        quick_blog.Entry.delete(self)
+
+        # _current comment is specific to libervia, we remove it
+        if isinstance(self.manager, Entry):
+            self.manager._current_comment = None
+
+        # now we remove the pyjamas widgets
+        parent = self.parent
+        assert isinstance(parent, VerticalPanel)
+        self.removeFromParent()
+        if not parent.children:
+            # the vpanel is empty, we remove it
+            parent.removeFromParent()
+            try:
+                if self.manager.comments_panel == parent:
+                    self.manager.comments_panel = None
+            except AttributeError:
+                assert isinstance(self.manager, quick_blog.QuickBlog)
 
 
-class MicroblogPanel(quick_widgets.QuickWidget, libervia_widget.LiberviaWidget, MouseHandler):
+class Blog(quick_blog.QuickBlog, libervia_widget.LiberviaWidget, MouseHandler):
+    """Panel used to show microblog"""
     warning_msg_public = "This message will be <b>PUBLIC</b> and everybody will be able to see it, even people you don't know"
     warning_msg_group = "This message will be published for all the people of the following groups: <span class='warningTarget'>%s</span>"
-    # FIXME: all the generic parts must be moved to quick_frontends
 
     def __init__(self, host, targets, profiles=None):
-        """Panel used to show microblog
-
-        @param targets (tuple(unicode)): contact groups displayed in this panel.
-            If empty, show all microblogs from all contacts.
-        """
-        # do not mix self.targets (set of tuple of unicode) and self.accepted_groups (set of unicode)
-        quick_widgets.QuickWidget.__init__(self, host, targets, C.PROF_KEY_NONE)
-        libervia_widget.LiberviaWidget.__init__(self, host, ", ".join(self.accepted_groups), selectable=True)
+        quick_blog.QuickBlog.__init__(self, host, targets, C.PROF_KEY_NONE)
+        title = ", ".join(targets) if targets else "Blog"
+        libervia_widget.LiberviaWidget.__init__(self, host, title, selectable=True)
         MouseHandler.__init__(self)
-        self.entries = {}
-        self.comments = {}
         self.vpanel = VerticalPanel()
         self.vpanel.setStyleName('microblogPanel')
         self.setWidget(self.vpanel)
-        self.addNewMessageEntry()
+        self.addEntry(editable=True, first=True)
+
+        self.getAll()
 
-        self.footer = HTML('', StyleName='microblogPanel_footer')
-        self.footer.waiting = False
-        self.footer.addClickListener(self)
-        self.footer.addMouseListener(self)
-        self.vpanel.add(self.footer)
-        self.next_rsm_index = 0
-
-        # FIXME: workaround for a pyjamas issue: calling hash on a class method always return a different value if that method is defined directly within the class (with the "def" keyword)
-        self.avatarListener = self.onAvatarUpdate
-        host.addListener('avatar', self.avatarListener, [C.PROF_KEY_NONE])
+        # self.footer = HTML('', StyleName='microblogPanel_footer')
+        # self.footer.waiting = False
+        # self.footer.addClickListener(self)
+        # self.footer.addMouseListener(self)
+        # self.vpanel.add(self.footer)
+        # self.next_rsm_index = 0
 
     def __str__(self):
-        return u"Blog Widget [target: {}, profile: {}]".format(', '.join(self.accepted_groups), self.profile)
+        return u"Blog Widget [targets: {}, profile: {}]".format(", ".join(self.targets) if self.targets else "meta blog", self.profile)
 
-    def onDelete(self):
-        quick_widgets.QuickWidget.onDelete(self)
-        self.host.removeListener('avatar', self.avatarListener)
+    def update(self):
+        self.entries.sort(key=lambda entry: entry.item.published, reverse=True)
 
-    def onAvatarUpdate(self, jid_, hash_, profile):
-        """Called on avatar update events
-
-        @param jid_: jid of the entity with updated avatar
-        @param hash_: hash of the avatar
-        @param profile: %(doc_profile)s
-        """
-        whoami = self.host.profiles[self.profile].whoami
-        if self.isJidAccepted(jid_) or jid_.bare == whoami.bare:
-            self.updateValue('avatar', jid_, hash_)
+        idx = 0
+        if self._first_entry is not None:
+            idx += 1
+            if not self._first_entry.displayed:
+                self.vpanel.insert(self._first_entry, 0)
+                self._first_entry.displayed = True
 
-    def addNewMessageEntry(self):
-        """Add an empty entry for writing a new message if needed."""
-        if self.getNewMainEntry():
-            return  # there's already one
-        data = {'id': unicode(time()),
-                'new': True,
-                'author': unicode(self.host.whoami.bare),
-                }
-        entry = self.addEntry(data, update_header=False)
-        entry.edit(True)
+        for entry in self.entries:
+            if not entry.displayed:
+                self.vpanel.insert(entry, idx)
+                entry.displayed = True
+                idx += 1
+
+    # def onDelete(self):
+    #     quick_widgets.QuickWidget.onDelete(self)
+    #     self.host.removeListener('avatar', self.avatarListener)
 
-    def getNewMainEntry(self):
-        """Get the new entry being edited, or None if it doesn't exists.
+    # def onAvatarUpdate(self, jid_, hash_, profile):
+    #     """Called on avatar update events
 
-        @return (MicroblogEntry): the new entry being edited.
-        """
-        if len(self.vpanel.children) < 2:
-            return None  # there's only the footer
-        first = self.vpanel.children[0]
-        assert(first.type == 'main_item')
-        return first if first.empty else None
+    #     @param jid_: jid of the entity with updated avatar
+    #     @param hash_: hash of the avatar
+    #     @param profile: %(doc_profile)s
+    #     """
+    #     whoami = self.host.profiles[self.profile].whoami
+    #     if self.isJidAccepted(jid_) or jid_.bare == whoami.bare:
+    #         self.updateValue('avatar', jid_, hash_)
 
     @staticmethod
     def onGroupDrop(host, targets):
@@ -464,35 +450,13 @@
         @return: the created MicroblogPanel
         """
         # XXX: pyjamas doesn't support use of cls directly
-        widget = host.displayWidget(MicroblogPanel, targets, dropped=True)
-        widget.loadMoreMainEntries()
+        widget = host.displayWidget(Blog, targets, dropped=True)
         return widget
 
-    @property
-    def accepted_groups(self):
-        """Return a set of the accepted groups"""
-        return set().union(*self.targets)
-
-    def loadAllCommentsForEntry(self, main_entry):
-        """Load all the comments for the given main entry.
-
-        @param main_entry (MicroblogEntry): main entry having comments.
-        """
-        index = str(main_entry.comments_count - main_entry.hidden_count)
-        rsm = {'max_': str(main_entry.hidden_count), 'index': index}
-        self.host.bridge.call('getMblogComments', self.mblogsInsert, main_entry.comments_service, main_entry.comments_node, rsm)
-
-    def loadMoreMainEntries(self):
-        if self.footer.waiting:
-            return
-        self.footer.waiting = True
-        self.footer.setHTML("loading...")
-
-        self.host.loadOurMainEntries(self.next_rsm_index, self)
-
-        type_ = 'ALL' if self.accepted_groups == [] else 'GROUP'
-        rsm = {'max_': str(C.RSM_MAX_ITEMS), 'index': str(self.next_rsm_index)}
-        self.host.bridge.getMassiveMblogs(type_, list(self.accepted_groups), rsm, profile=C.PROF_KEY_NONE, callback=self.massiveInsert)
+    # @property
+    # def accepted_groups(self):
+    #     """Return a set of the accepted groups"""
+    #     return set().union(*self.targets)
 
     def getWarningData(self, comment):
         """
@@ -500,212 +464,29 @@
         @return: a couple (type, msg) for calling self.host.showWarning"""
         if comment:
             return ("PUBLIC", "This is a <span class='warningTarget'>comment</span> and keep the initial post visibility, so it is potentialy public")
-        elif not self.accepted_groups:
+        elif self._targets_type == C.ALL:
             # we have a meta MicroblogPanel, we publish publicly
             return ("PUBLIC", self.warning_msg_public)
         else:
             # FIXME: manage several groups
-            return ("GROUP", self.warning_msg_group % ' '.join(self.accepted_groups))
-
-    def onTextEntered(self, text):
-        if not self.accepted_groups:
-            # we are entering a public microblog
-            self.bridge.call("sendMblog", None, "PUBLIC", (), text, {})
-        else:
-            self.bridge.call("sendMblog", None, "GROUP", tuple(self.accepted_groups), text, {})
-
-    def accept_all(self):
-        return not self.accepted_groups  # we accept every microblog only if we are not filtering by groups
-
-    def getEntries(self):
-        """Ask all the entries for the currenly accepted groups,
-        and fill the panel"""
-
-    def massiveInsert(self, mblogs):
-        """Insert several microblogs at once
-
-        @param mblogs (dict): dictionary mapping a publisher to microblogs data:
-            - key: publisher (str)
-            - value: couple (list[dict], dict) with:
-                - list of microblogs data
-                - RSM response data
-        """
-        count_pub = len(mblogs)
-        count_msg = sum([len(value) for value in mblogs.values()])
-        log.debug(u"massive insertion of {count_msg} blogs for {count_pub} contacts".format(count_msg=count_msg, count_pub=count_pub))
-        for publisher in mblogs:
-            log.debug(u"adding {count} blogs for [{publisher}]".format(count=len(mblogs[publisher]), publisher=publisher))
-            self.mblogsInsert(mblogs[publisher])
-        self.next_rsm_index += C.RSM_MAX_ITEMS
-        self.footer.waiting = False
-        self.footer.setHTML('show older messages')
-
-    def mblogsInsert(self, mblogs):
-        """ Insert several microblogs from the same node at once.
-
-        @param mblogs (list): couple (list[dict], dict) with:
-            - list of microblogs data
-            - RSM response data
-        """
-        mblogs, rsm = mblogs
-
-        for mblog in mblogs:
-            if "content" not in mblog:
-                log.warning(u"No content found in microblog [%s]" % mblog)
-                continue
-            self.addEntry(mblog, update_header=False)
-
-        hashes = set([(entry['service'], entry['node']) for entry in mblogs if entry['type'] == 'comment'])
-        assert(len(hashes) < 2)  # ensure the blogs come from the same node
-        if len(hashes) == 1:
-            main_entry = self.comments[hashes.pop()]
-            try:
-                count = int(rsm['count'])
-                hidden = count - (int(rsm['index']) + len(mblogs))
-                main_entry.updateHeader(count, hidden)
-            except KeyError:  # target pubsub server doesn't support RSM
-                pass
-
-    def _chronoInsert(self, vpanel, entry, reverse=True):
-        """ Insert an entry in chronological order
-        @param vpanel: VerticalPanel instance
-        @param entry: MicroblogEntry
-        @param reverse: more recent entry on top if True, chronological order else"""
-        # XXX: for now we can't use "published" timestamp because the entries
-        # are retrieved using the "updated" field. We don't want new items
-        # inserted with RSM to be inserted "randomly" in the panel, they
-        # should be added at the bottom of the list.
-        assert(isinstance(reverse, bool))
-        if entry.empty:
-            entry.updated = time()
-        # we look for the right index to insert our entry:
-        # if reversed, we insert the entry above the first entry
-        # in the past
-        idx = 0
-
-        for child in vpanel.children[0:-1]:  # ignore the footer
-            if not isinstance(child, MicroblogEntry):
-                idx += 1
-                continue
-            condition_to_stop = child.empty or (child.updated > entry.updated)
-            if condition_to_stop != reverse:  # != is XOR
-                break
-            idx += 1
-
-        vpanel.insert(entry, idx)
-
-    def addEntryIfAccepted(self, sender, groups, mblog_entry):
-        """Check if an entry can go in MicroblogPanel and add to it
-
-        @param sender(jid.JID): jid of the entry sender
-        @param groups: groups which can receive this entry
-        @param mblog_entry: panels.MicroblogItem instance
-        """
-        assert isinstance(sender, jid.JID) # FIXME temporary
-        if (mblog_entry.type == "comment"
-            or self.isJidAccepted(sender)
-            or (groups is None and sender == self.host.profiles[self.profile].whoami.bare)
-            or (groups and groups.intersection(self.accepted_groups))):
-            self.addEntry(mblog_entry)
-
-    def addEntry(self, data, update_header=True):
-        """Add an entry to the panel
-
-        @param data (dict): dict containing the item data
-        @param update_header (bool): update or not the main comment header
-        @return: the added MicroblogEntry instance, or None
-        """
-        _entry = MicroblogEntry(self, data)
-        if _entry.type == "comment":
-            comments_hash = (_entry.service, _entry.node)
-            if comments_hash not in self.comments:
-                # The comments node is not known in this panel
-                return None
-            parent = self.comments[comments_hash]
-            parent_idx = self.vpanel.getWidgetIndex(parent)
-            # we find or create the panel where the comment must be inserted
-            try:
-                sub_panel = self.vpanel.getWidget(parent_idx + 1)
-            except IndexError:
-                sub_panel = None
-            if not sub_panel or not isinstance(sub_panel, VerticalPanel):
-                sub_panel = VerticalPanel()
-                sub_panel.setStyleName('microblogPanel')
-                sub_panel.addStyleName('subPanel')
-                self.vpanel.insert(sub_panel, parent_idx + 1)
-
-            for idx in xrange(0, len(sub_panel.getChildren())):
-                comment = sub_panel.getIndexedChild(idx)
-                if comment.id == _entry.id:
-                    # update an existing comment
-                    sub_panel.remove(comment)
-                    sub_panel.insert(_entry, idx)
-                    return _entry
-            # we want comments to be inserted in chronological order
-            self._chronoInsert(sub_panel, _entry, reverse=False)
-            if update_header:
-                parent.updateHeader(inc=+1)
-            return _entry
-
-        if _entry.comments:
-            # entry has comments, we keep the comments service/node as a reference
-            comments_hash = (_entry.comments_service, _entry.comments_node)
-            self.comments[comments_hash] = _entry
-            self.host.bridge.call('getMblogComments', self.mblogsInsert, _entry.comments_service, _entry.comments_node)
-
-        if _entry.id in self.entries:  # update
-            old_entry = self.entries[_entry.id]
-            idx = self.vpanel.getWidgetIndex(old_entry)
-            counts = (old_entry.comments_count, old_entry.hidden_count)
-            self.vpanel.remove(old_entry)
-            self.vpanel.insert(_entry, idx)
-            _entry.updateHeader(*counts)
-        else:  # new entry
-            self._chronoInsert(self.vpanel, _entry)
-            if _entry.comments:
-                self.host.bridge.call('getMblogComments', self.mblogsInsert, _entry.comments_service, _entry.comments_node)
-
-        self.entries[_entry.id] = _entry
-
-        return _entry
-
-    def removeEntry(self, type_, id_, update_header=True):
-        """Remove an entry from the panel
-
-        @param type_ (str): entry type ('main_item' or 'comment')
-        @param id_ (str): entry id
-        @param update_header (bool): update or not the main comment header
-        """
-        for child in self.vpanel.getChildren():
-            if isinstance(child, MicroblogEntry) and type_ == 'main_item':
-                if child.id == id_:
-                    main_idx = self.vpanel.getWidgetIndex(child)
-                    try:
-                        sub_panel = self.vpanel.getWidget(main_idx + 1)
-                        if isinstance(sub_panel, VerticalPanel):
-                            sub_panel.removeFromParent()
-                    except IndexError:
-                        pass
-                    child.removeFromParent()
-                    break
-            elif isinstance(child, VerticalPanel) and type_ == 'comment':
-                for comment in child.getChildren():
-                    if comment.id == id_:
-                        if update_header:
-                            hash_ = (comment.service, comment.node)
-                            self.comments[hash_].updateHeader(inc=-1)
-                        comment.removeFromParent()
-                        break
+            return (self._targets_type, self.warning_msg_group % ' '.join(self.targets))
 
     def ensureVisible(self, entry):
         """Scroll to an entry to ensure its visibility
 
         @param entry (MicroblogEntry): the entry
         """
-        try:
-            self.vpanel.getParent().ensureVisible(entry)  # scroll to the clicked entry
-        except AttributeError:
-            log.warning("FIXME: MicroblogPanel.vpanel should be wrapped in a ScrollPanel!")
+        current = entry
+        while True:
+            parent = current.getParent()
+            if parent is None:
+                log.warning("Can't find any parent ScrollPanel")
+                return
+            elif isinstance(parent, ScrollPanel):
+                parent.ensureVisible(current)
+                return
+            else:
+                current = parent
 
     def setSelectedEntry(self, entry, ensure_visible=False):
         """Select an entry.
@@ -720,55 +501,35 @@
         clicked_entry = entry  # entry may be None when the timer is done
         Timer(500, lambda timer: clicked_entry.removeStyleName('selected_entry'))
 
-    def updateValue(self, type_, jid_, value):
-        """Update a jid value in entries
-
-        @param type_: one of 'avatar', 'nick'
-        @param jid_(jid.JID): jid concerned
-        @param value: new value"""
-        assert isinstance(jid_, jid.JID) # FIXME: temporary
-        def updateVPanel(vpanel):
-            avatar_url = self.host.getAvatarURL(jid_)
-            for child in vpanel.children:
-                if isinstance(child, MicroblogEntry) and child.author == jid_:
-                    child.updateAvatar(avatar_url)
-                elif isinstance(child, VerticalPanel):
-                    updateVPanel(child)
-        if type_ == 'avatar':
-            updateVPanel(self.vpanel)
-
-    def addAcceptedGroups(self, groups):
-        """Add one or more group(s) which can be displayed in this panel.
+    # def updateValue(self, type_, jid_, value):
+    #     """Update a jid value in entries
 
-        @param groups (tuple(unicode)): tuple of groups to add
-        """
-        # FIXME: update the widget's hash in QuickApp._widgets[MicroblogPanel]
-        self.targets.update(groups)
-
-    def isJidAccepted(self, jid_):
-        """Tell if a jid is actepted and must be shown in this panel
+    #     @param type_: one of 'avatar', 'nick'
+    #     @param jid_(jid.JID): jid concerned
+    #     @param value: new value"""
+    #     assert isinstance(jid_, jid.JID) # FIXME: temporary
+    #     def updateVPanel(vpanel):
+    #         avatar_url = self.host.getAvatarURL(jid_)
+    #         for child in vpanel.children:
+    #             if isinstance(child, MicroblogEntry) and child.author == jid_:
+    #                 child.updateAvatar(avatar_url)
+    #             elif isinstance(child, VerticalPanel):
+    #                 updateVPanel(child)
+    #     if type_ == 'avatar':
+    #         updateVPanel(self.vpanel)
 
-        @param jid_(jid.JID): jid to check
-        @return: True if the jid is accepted
-        """
-        assert isinstance(jid_, jid.JID) # FIXME temporary
-        if self.accept_all():
-            return True
-        for group in self.accepted_groups:
-            if self.host.contact_lists[self.profile].isEntityInGroup(jid_, group):
-                return True
-        return False
+    # def onClick(self, sender):
+    #     if sender == self.footer:
+    #         self.loadMoreMainEntries()
 
-    def onClick(self, sender):
-        if sender == self.footer:
-            self.loadMoreMainEntries()
-
-    def onMouseEnter(self, sender):
-        if sender == self.footer:
-            self.loadMoreMainEntries()
+    # def onMouseEnter(self, sender):
+    #     if sender == self.footer:
+    #         self.loadMoreMainEntries()
 
 
-libervia_widget.LiberviaWidget.addDropKey("GROUP", lambda host, item: MicroblogPanel.onGroupDrop(host, (item,)))
-
-# Needed for the drop keys to not be mixed between meta panel and panel for "Contacts" group
-libervia_widget.LiberviaWidget.addDropKey("CONTACT_TITLE", lambda host, item: MicroblogPanel.onGroupDrop(host, ()))
+# libervia_widget.LiberviaWidget.addDropKey("GROUP", lambda host, item: MicroblogPanel.onGroupDrop(host, (item,)))
+#
+# # Needed for the drop keys to not be mixed between meta panel and panel for "Contacts" group
+libervia_widget.LiberviaWidget.addDropKey("CONTACT_TITLE", lambda host, item: Blog.onGroupDrop(host, ()))
+quick_blog.registerClass("ENTRY", Entry)
+quick_widgets.register(quick_blog.QuickBlog, Blog)
--- a/src/browser/sat_browser/contact_list.py	Tue Jul 28 22:22:10 2015 +0200
+++ b/src/browser/sat_browser/contact_list.py	Sun Aug 16 01:51:12 2015 +0200
@@ -155,6 +155,10 @@
         self.host.removeListener('avatar', self.avatarListener)
 
     def update(self):
+        # XXX: as update is slow, we avoid many updates on profile plugs
+        # and do them all at once at the end
+        if not self.host._profile_plugged:
+            return
         ### GROUPS ###
         _keys = self._groups.keys()
         try:
--- a/src/browser/sat_browser/editor_widget.py	Tue Jul 28 22:22:10 2015 +0200
+++ b/src/browser/sat_browser/editor_widget.py	Sun Aug 16 01:51:12 2015 +0200
@@ -24,7 +24,7 @@
 from pyjamas.ui.HTML import HTML
 from pyjamas.ui.SimplePanel import SimplePanel
 from pyjamas.ui.TextArea import TextArea
-from pyjamas.ui.KeyboardListener import KEY_ENTER, KEY_SHIFT, KEY_UP, KEY_DOWN, KeyboardHandler
+from pyjamas.ui import KeyboardListener as keyb
 from pyjamas.ui.FocusListener import FocusHandler
 from pyjamas.ui.ClickListener import ClickHandler
 from pyjamas.ui.MouseListener import MouseHandler
@@ -60,15 +60,15 @@
             self.setText(text)
             Timer(5, lambda timer: self.setCursorPos(len(text)))
 
-        if keycode == KEY_ENTER:
+        if keycode == keyb.KEY_ENTER:
             if _txt:
                 self.host.selected_widget.onTextEntered(_txt)
                 self.host._updateInputHistory(_txt) # FIXME: why using a global variable ?
             self.setText('')
             sender.cancelKey()
-        elif keycode == KEY_UP:
+        elif keycode == keyb.KEY_UP:
             self.host._updateInputHistory(_txt, -1, history_cb)
-        elif keycode == KEY_DOWN:
+        elif keycode == keyb.KEY_DOWN:
             self.host._updateInputHistory(_txt, +1, history_cb)
         else:
             self._onComposing()
@@ -99,15 +99,15 @@
         @param content: dict with at least a 'text' key
         @param strproc: method to be applied on strings to clean the content
         @param modifiedCb: method to be called when the text has been modified.
-        If this method returns:
-        - True: the modification will be saved and afterEditCb called;
-        - False: the modification won't be saved and afterEditCb called;
-        - None: the modification won't be saved and afterEditCb not called.
+            This method can return:
+                - True: the modification will be saved and afterEditCb called;
+                - False: the modification won't be saved and afterEditCb called;
+                - None: the modification won't be saved and afterEditCb not called.
         @param afterEditCb: method to be called when the edition is done
         """
         if content is None:
             content = {'text': ''}
-        assert('text' in content)
+        assert 'text' in content
         if strproc is None:
             def strproc(text):
                 try:
@@ -115,21 +115,23 @@
                 except (TypeError, AttributeError):
                     return text
         self.strproc = strproc
-        self.__modifiedCb = modifiedCb
+        self._modifiedCb = modifiedCb
         self._afterEditCb = afterEditCb
         self.initialized = False
         self.edit_listeners = []
         self.setContent(content)
 
     def setContent(self, content=None):
-        """Set the editable content. The displayed content, which is set from the child class, could differ.
-        @param content: dict with at least a 'text' key
+        """Set the editable content.
+        The displayed content, which is set from the child class, could differ.
+
+        @param content (dict): content data, need at least a 'text' key
         """
         if content is None:
             content = {'text': ''}
         elif not isinstance(content, dict):
             content = {'text': content}
-        assert('text' in content)
+        assert 'text' in content
         self._original_content = {}
         for key in content:
             self._original_content[key] = self.strproc(content[key])
@@ -150,7 +152,7 @@
 
     def getOriginalContent(self):
         """
-        @return the original content before modification (dict)
+        @return (dict): the original content before modification (i.e. content given in __init__)
         """
         return self._original_content
 
@@ -193,8 +195,8 @@
             if abort:
                 self._afterEditCb(content)
                 return
-            if self.__modifiedCb and self.modified(content):
-                result = self.__modifiedCb(content)  # e.g.: send a message or update something
+            if self._modifiedCb and self.modified(content):
+                result = self._modifiedCb(content)  # e.g.: send a message or update something
                 if result is not None:
                     if self._afterEditCb:
                         self._afterEditCb(content)  # e.g.: restore the display mode
@@ -220,7 +222,7 @@
         self.edit_listeners.append(listener)
 
 
-class SimpleTextEditor(BaseTextEditor, FocusHandler, KeyboardHandler, ClickHandler):
+class SimpleTextEditor(BaseTextEditor, FocusHandler, keyb.KeyboardHandler, ClickHandler):
     """Base class for manage a simple text editor."""
 
     def __init__(self, content=None, modifiedCb=None, afterEditCb=None, options=None):
@@ -228,12 +230,12 @@
         @param content
         @param modifiedCb
         @param afterEditCb
-        @param options: dict with the following value:
-        - no_xhtml: set to True to clean any xhtml content.
-        - enhance_display: if True, the display text will be enhanced with strings.addURLToText
-        - listen_keyboard: set to True to terminate the edition with <enter> or <escape>.
-        - listen_focus: set to True to terminate the edition when the focus is lost.
-        - listen_click: set to True to start the edition when you click on the widget.
+        @param options (dict): can have the following value:
+            - no_xhtml: set to True to clean any xhtml content.
+            - enhance_display: if True, the display text will be enhanced with strings.addURLToText
+            - listen_keyboard: set to True to terminate the edition with <enter> or <escape>.
+            - listen_focus: set to True to terminate the edition when the focus is lost.
+            - listen_click: set to True to start the edition when you click on the widget.
         """
         self.options = {'no_xhtml': False,
                         'enhance_display': True,
@@ -243,12 +245,11 @@
                         }
         if options:
             self.options.update(options)
-        self.__shift_down = False
         if self.options['listen_focus']:
             FocusHandler.__init__(self)
         if self.options['listen_click']:
             ClickHandler.__init__(self)
-        KeyboardHandler.__init__(self)
+        keyb.KeyboardHandler.__init__(self)
         strproc = lambda text: html_tools.html_sanitize(html_tools.html_strip(text)) if self.options['no_xhtml'] else html_tools.html_strip(text)
         BaseTextEditor.__init__(self, content, strproc, modifiedCb, afterEditCb)
         self.textarea = self.display = None
@@ -295,21 +296,14 @@
 
     def onKeyDown(self, sender, keycode, modifiers):
         for listener in self.edit_listeners:
-            listener(self.textarea, keycode)
+            listener(self.textarea, keycode, modifiers) # FIXME: edit_listeners must either be removed, or send an action instead of keycode/modifiers
         if not self.options['listen_keyboard']:
             return
-        if keycode == KEY_SHIFT or self.__shift_down:  # allow input a new line with <shift> + <enter>
-            self.__shift_down = True
-            return
-        if keycode == KEY_ENTER:  # finish the edition
+        if keycode == keyb.KEY_ENTER and modifiers & keyb.MODIFIER_SHIFT:
             self.textarea.setFocus(False)
             if not self.options['listen_focus']:
                 self.edit(False)
 
-    def onKeyUp(self, sender, keycode, modifiers):
-        if keycode == KEY_SHIFT:
-            self.__shift_down = False
-
     def onLostFocus(self, sender):
         """Finish the edition when focus is lost"""
         if self.options['listen_focus']:
@@ -325,10 +319,10 @@
             FocusHandler.onBrowserEvent(self, event)
         if self.options['listen_click']:
             ClickHandler.onBrowserEvent(self, event)
-        KeyboardHandler.onBrowserEvent(self, event)
+        keyb.KeyboardHandler.onBrowserEvent(self, event)
 
 
-class HTMLTextEditor(SimpleTextEditor, HTML, FocusHandler, KeyboardHandler):
+class HTMLTextEditor(SimpleTextEditor, HTML, FocusHandler, keyb.KeyboardHandler):
     """Manage a simple text editor with the HTML 5 "contenteditable" property."""
 
     def __init__(self, content=None, modifiedCb=None, afterEditCb=None, options=None):
@@ -353,7 +347,7 @@
             self.getElement().blur()
 
 
-class LightTextEditor(SimpleTextEditor, SimplePanel, FocusHandler, KeyboardHandler):
+class LightTextEditor(SimpleTextEditor, SimplePanel, FocusHandler, keyb.KeyboardHandler):
     """Manage a simple text editor with a TextArea for editing, HTML for display."""
 
     def __init__(self, content=None, modifiedCb=None, afterEditCb=None, options=None):
--- a/src/browser/sat_browser/json.py	Tue Jul 28 22:22:10 2015 +0200
+++ b/src/browser/sat_browser/json.py	Sun Aug 16 01:51:12 2015 +0200
@@ -170,11 +170,13 @@
 class BridgeCall(LiberviaJsonProxy):
     def __init__(self):
         LiberviaJsonProxy.__init__(self, "/json_api",
-                        ["getContacts", "addContact", "sendMessage", "sendMblog", "sendMblogComment",
-                         "getMblogs", "getMassiveMblogs", "getMblogComments",
+                        ["getContacts", "addContact", "sendMessage",
+                         "psDeleteNode", "psRetractItem", "psRetractItems",
+                         "mbSend", "mbRetract", "mbGetLast", "mbGetFromMany", "mbGetFromManyRTResult",
+                         "mbGetFromManyWithComments", "mbGetFromManyWithCommentsRTResult",
                          "getHistory", "getPresenceStatuses", "joinMUC", "mucLeave", "getRoomsJoined",
                          "getRoomsSubjects", "inviteMUC", "launchTarotGame", "getTarotCardsPaths", "tarotGameReady",
-                         "tarotGamePlayCards", "launchRadioCollective", "getMblogs", "getMblogsWithComments",
+                         "tarotGamePlayCards", "launchRadioCollective",
                          "getWaitingSub", "subscription", "delContact", "updateContact", "getCard",
                          "getEntityData", "getParamsUI", "asyncGetParamA", "setParam", "launchAction",
                          "disconnect", "chatStateComposing", "getNewAccountDomain", "confirmationAnswer",
--- a/src/browser/sat_browser/richtext.py	Tue Jul 28 22:22:10 2015 +0200
+++ b/src/browser/sat_browser/richtext.py	Sun Aug 16 01:51:12 2015 +0200
@@ -19,6 +19,8 @@
 
 from sat_frontends.tools import composition
 from sat.core.i18n import _
+from sat.core.log import getLogger
+log = getLogger(__name__)
 
 from pyjamas.ui.TextArea import TextArea
 from pyjamas.ui.Button import Button
@@ -157,7 +159,7 @@
             self.toolbar.addStyleName(self.style['toolbar'])
             for key in composition.RICH_SYNTAXES[syntax].keys():
                 self.addToolbarButton(syntax, key)
-            self.wysiwyg_button = CheckBox(_('WYSIWYG edition'))
+            self.wysiwyg_button = CheckBox(_('preview'))
             wysiywgCb = lambda sender: self.setWysiwyg(sender.getChecked())
             self.wysiwyg_button.addClickListener(wysiywgCb)
             self.toolbar.add(self.wysiwyg_button)
@@ -436,6 +438,7 @@
     def __syncToUniBox(self, recipients=None, emptyText=False):
         """Synchronize to unibox if a maximum of one recipient is set.
         @return True if the sync could be done, False otherwise"""
+        # FIXME: To be removed (unibox doesn't exist anymore)
         if not self.host.uni_box:
             return
         setText = lambda: self.host.uni_box.setText("" if emptyText else self.getContent()['text'])
@@ -536,4 +539,4 @@
 
     def onKeyDown(self, sender=None, keycode=None, modifiers=None):
         for listener in self._parent.edit_listeners:
-            listener(self, keycode)
+            listener(self, keycode, modifiers) # FIXME: edit_listeners must either be removed, or send an action instead of keycode/modifiers
--- a/src/server/server.py	Tue Jul 28 22:22:10 2015 +0200
+++ b/src/server/server.py	Sun Aug 16 01:51:12 2015 +0200
@@ -150,7 +150,8 @@
         self.sat_host = sat_host
 
     def asyncBridgeCall(self, method_name, *args, **kwargs):
-        """Call an asynchrone bridge method and return a deferred
+        """Call an asynchronous bridge method and return a deferred
+
         @param method_name: name of the method as a unicode
         @return: a deferred which trigger the result
 
@@ -258,108 +259,220 @@
         profile = ISATSession(self.session).profile
         return self.asyncBridgeCall("sendMessage", to_jid, msg, subject, type_, options, profile)
 
-    def jsonrpc_sendMblog(self, type_, dest, text, extra={}):
-        """ Send microblog message
-        @param type_ (unicode): one of "PUBLIC", "GROUP"
-        @param dest (tuple(unicode)): recipient groups (ignored for "PUBLIC")
-        @param text (unicode): microblog's text
-        """
-        profile = ISATSession(self.session).profile
-        extra['allow_comments'] = 'True'
-
-        if not type_:  # auto-detect
-            type_ = "PUBLIC" if dest == [] else "GROUP"
+    ## PubSub ##
 
-        if type_ in ("PUBLIC", "GROUP") and text:
-            if type_ == "PUBLIC":
-                #This text if for the public microblog
-                log.debug("sending public blog")
-                return self.sat_host.bridge.sendGroupBlog("PUBLIC", (), text, extra, profile)
-            else:
-                log.debug("sending group blog")
-                dest = dest if isinstance(dest, list) else [dest]
-                return self.sat_host.bridge.sendGroupBlog("GROUP", dest, text, extra, profile)
-        else:
-            raise Exception("Invalid data")
+    def jsonrpc_psDeleteNode(self, service, node):
+        """Delete a whole node
 
-    def jsonrpc_deleteMblog(self, pub_data, comments):
-        """Delete a microblog node
-        @param pub_data: a tuple (service, comment node identifier, item identifier)
-        @param comments: comments node identifier (for main item) or False
+        @param service (unicode): service jid
+        @param node (unicode): node to delete
         """
         profile = ISATSession(self.session).profile
-        return self.sat_host.bridge.deleteGroupBlog(pub_data, comments if comments else '', profile)
+        return self.asyncBridgeCall("psDeleteNode", service, node, profile)
+
+    # def jsonrpc_psRetractItem(self, service, node, item, notify):
+    #     """Delete a whole node
+
+    #     @param service (unicode): service jid
+    #     @param node (unicode): node to delete
+    #     @param items (iterable): id of item to retract
+    #     @param notify (bool): True if notification is required
+    #     """
+    #     profile = ISATSession(self.session).profile
+    #     return self.asyncBridgeCall("psRetractItem", service, node, item, notify, profile)
+
+    # def jsonrpc_psRetractItems(self, service, node, items, notify):
+    #     """Delete a whole node
 
-    def jsonrpc_updateMblog(self, pub_data, comments, message, extra={}):
-        """Modify a microblog node
-        @param pub_data: a tuple (service, comment node identifier, item identifier)
-        @param comments: comments node identifier (for main item) or False
-        @param message: new message
-        @param extra: dict which option name as key, which can be:
-            - allow_comments: True to accept an other level of comments, False else (default: False)
-            - rich: if present, contain rich text in currently selected syntax
-        """
-        profile = ISATSession(self.session).profile
-        if comments:
-            extra['allow_comments'] = 'True'
-        return self.sat_host.bridge.updateGroupBlog(pub_data, comments if comments else '', message, extra, profile)
+    #     @param service (unicode): service jid
+    #     @param node (unicode): node to delete
+    #     @param items (iterable): ids of items to retract
+    #     @param notify (bool): True if notification is required
+    #     """
+    #     profile = ISATSession(self.session).profile
+    #     return self.asyncBridgeCall("psRetractItems", service, node, items, notify, profile)
 
-    def jsonrpc_sendMblogComment(self, node, text, extra={}):
-        """ Send microblog message
-        @param node: url of the comments node
-        @param text: comment
+    ## microblogging ##
+
+    def jsonrpc_mbSend(self, service, node, mb_data):
+        """Send microblog data
+
+        @param service (unicode): service jid or empty string to use profile's microblog
+        @param node (unicode): publishing node, or empty string to use microblog node
+        @param mb_data(dict): microblog data
+        @return: a deferred
         """
         profile = ISATSession(self.session).profile
-        if node and text:
-            return self.sat_host.bridge.sendGroupBlogComment(node, text, extra, profile)
-        else:
-            raise Exception("Invalid data")
+        return self.asyncBridgeCall("mbSend", service, node, mb_data, profile)
+
+    def jsonrpc_mbRetract(self, service, node, items):
+        """Delete a whole node
 
-    def jsonrpc_getMblogs(self, publisher_jid, item_ids, max_items=C.RSM_MAX_ITEMS):
-        """Get specified microblogs posted by a contact
-        @param publisher_jid: jid of the publisher
-        @param item_ids: list of microblogs items IDs
-        @return list of microblog data (dict)"""
+        @param service (unicode): service jid, empty string for PEP
+        @param node (unicode): node to delete, empty string for default node
+        @param items (iterable): ids of items to retract
+        """
         profile = ISATSession(self.session).profile
-        d = self.asyncBridgeCall("getGroupBlogs", publisher_jid, item_ids, {'max_': unicode(max_items)}, False, profile)
-        return d
+        return self.asyncBridgeCall("mbRetract", service, node, items, profile)
+
+    def jsonrpc_mbGetLast(self, service_jid, node, max_items, extra):
+        """Get last microblogs from publisher_jid
 
-    def jsonrpc_getMblogsWithComments(self, publisher_jid, item_ids, max_comments=C.RSM_MAX_COMMENTS):
-        """Get specified microblogs posted by a contact and their comments
-        @param publisher_jid: jid of the publisher
-        @param item_ids: list of microblogs items IDs
-        @return list of couple (microblog data, list of microblog data)"""
+        @param service_jid (unicode): pubsub service, usually publisher jid
+        @param node(unicode): mblogs node, or empty string to get the defaut one
+        @param max_items (int): maximum number of item to get or C.NO_LIMIT to get everything
+        @param rsm (dict): TODO
+        @return: a deferred couple with the list of items and metadatas.
+        """
         profile = ISATSession(self.session).profile
-        d = self.asyncBridgeCall("getGroupBlogsWithComments", publisher_jid, item_ids, {}, max_comments, profile)
-        return d
+        return self.asyncBridgeCall("mbGetLast", service_jid, node, max_items, extra, profile)
 
-    def jsonrpc_getMassiveMblogs(self, publishers_type, publishers, rsm=None):
-        """Get lasts microblogs posted by several contacts at once
+    def jsonrpc_mbGetFromMany(self, publishers_type, publishers, max_items, extra):
+        """Get many blog nodes at once
 
         @param publishers_type (unicode): one of "ALL", "GROUP", "JID"
         @param publishers (tuple(unicode)): tuple of publishers (empty list for all, list of groups or list of jids)
-        @param rsm (dict): TODO
-        @return: dict{unicode: list[dict])
-            key: publisher's jid
-            value: list of microblog data (dict)
+        @param max_items (int): maximum number of item to get or C.NO_LIMIT to get everything
+        @param extra (dict): TODO
+        @return (str): RT Deferred session id
+        """
+        profile = ISATSession(self.session).profile
+        return self.sat_host.bridge.mbGetFromMany(publishers_type, publishers, max_items, extra, profile)
+
+    def jsonrpc_mbGetFromManyRTResult(self, rt_session):
+        """Get results from RealTime mbGetFromMany session
+
+        @param rt_session (str): RT Deferred session id
+        """
+        profile = ISATSession(self.session).profile
+        return self.asyncBridgeCall("mbGetFromManyRTResult", rt_session, profile)
+
+    def jsonrpc_mbGetFromManyWithComments(self, publishers_type, publishers, max_items, max_comments, rsm_dict, rsm_comments_dict):
+        """Helper method to get the microblogs and their comments in one shot
+
+        @param publishers_type (str): type of the list of publishers (one of "GROUP" or "JID" or "ALL")
+        @param publishers (list): list of publishers, according to publishers_type (list of groups or list of jids)
+        @param max_items (int): optional limit on the number of retrieved items.
+        @param max_comments (int): maximum number of comments to retrieve
+        @param rsm_dict (dict): RSM data for initial items only
+        @param rsm_comments_dict (dict): RSM data for comments only
+        @param profile_key: profile key
+        @return (str): RT Deferred session id
+        """
+        profile = ISATSession(self.session).profile
+        return self.sat_host.bridge.mbGetFromManyWithComments(publishers_type, publishers, max_items, max_comments, rsm_dict, rsm_comments_dict, profile)
+
+    def jsonrpc_mbGetFromManyWithCommentsRTResult(self, rt_session):
+        """Get results from RealTime mbGetFromManyWithComments session
+
+        @param rt_session (str): RT Deferred session id
         """
         profile = ISATSession(self.session).profile
-        if rsm is None:
-            rsm = {'max_': unicode(C.RSM_MAX_ITEMS)}
-        d = self.asyncBridgeCall("getMassiveGroupBlogs", publishers_type, publishers, rsm, profile)
-        self.sat_host.bridge.massiveSubscribeGroupBlogs(publishers_type, publishers, profile)
-        return d
+        return self.asyncBridgeCall("mbGetFromManyWithCommentsRTResult", rt_session, profile)
+
+
+    # def jsonrpc_sendMblog(self, type_, dest, text, extra={}):
+    #     """ Send microblog message
+    #     @param type_ (unicode): one of "PUBLIC", "GROUP"
+    #     @param dest (tuple(unicode)): recipient groups (ignored for "PUBLIC")
+    #     @param text (unicode): microblog's text
+    #     """
+    #     profile = ISATSession(self.session).profile
+    #     extra['allow_comments'] = 'True'
+
+    #     if not type_:  # auto-detect
+    #         type_ = "PUBLIC" if dest == [] else "GROUP"
+
+    #     if type_ in ("PUBLIC", "GROUP") and text:
+    #         if type_ == "PUBLIC":
+    #             #This text if for the public microblog
+    #             log.debug("sending public blog")
+    #             return self.sat_host.bridge.sendGroupBlog("PUBLIC", (), text, extra, profile)
+    #         else:
+    #             log.debug("sending group blog")
+    #             dest = dest if isinstance(dest, list) else [dest]
+    #             return self.sat_host.bridge.sendGroupBlog("GROUP", dest, text, extra, profile)
+    #     else:
+    #         raise Exception("Invalid data")
+
+    # def jsonrpc_deleteMblog(self, pub_data, comments):
+    #     """Delete a microblog node
+    #     @param pub_data: a tuple (service, comment node identifier, item identifier)
+    #     @param comments: comments node identifier (for main item) or False
+    #     """
+    #     profile = ISATSession(self.session).profile
+    #     return self.sat_host.bridge.deleteGroupBlog(pub_data, comments if comments else '', profile)
+
+    # def jsonrpc_updateMblog(self, pub_data, comments, message, extra={}):
+    #     """Modify a microblog node
+    #     @param pub_data: a tuple (service, comment node identifier, item identifier)
+    #     @param comments: comments node identifier (for main item) or False
+    #     @param message: new message
+    #     @param extra: dict which option name as key, which can be:
+    #         - allow_comments: True to accept an other level of comments, False else (default: False)
+    #         - rich: if present, contain rich text in currently selected syntax
+    #     """
+    #     profile = ISATSession(self.session).profile
+    #     if comments:
+    #         extra['allow_comments'] = 'True'
+    #     return self.sat_host.bridge.updateGroupBlog(pub_data, comments if comments else '', message, extra, profile)
 
-    def jsonrpc_getMblogComments(self, service, node, rsm=None):
-        """Get all comments of given node
-        @param service: jid of the service hosting the node
-        @param node: comments node
-        """
-        profile = ISATSession(self.session).profile
-        if rsm is None:
-            rsm = {'max_': unicode(C.RSM_MAX_COMMENTS)}
-        d = self.asyncBridgeCall("getGroupBlogComments", service, node, rsm, profile)
-        return d
+    # def jsonrpc_sendMblogComment(self, node, text, extra={}):
+    #     """ Send microblog message
+    #     @param node: url of the comments node
+    #     @param text: comment
+    #     """
+    #     profile = ISATSession(self.session).profile
+    #     if node and text:
+    #         return self.sat_host.bridge.sendGroupBlogComment(node, text, extra, profile)
+    #     else:
+    #         raise Exception("Invalid data")
+
+    # def jsonrpc_getMblogs(self, publisher_jid, item_ids, max_items=C.RSM_MAX_ITEMS):
+    #     """Get specified microblogs posted by a contact
+    #     @param publisher_jid: jid of the publisher
+    #     @param item_ids: list of microblogs items IDs
+    #     @return list of microblog data (dict)"""
+    #     profile = ISATSession(self.session).profile
+    #     d = self.asyncBridgeCall("getGroupBlogs", publisher_jid, item_ids, {'max_': unicode(max_items)}, False, profile)
+    #     return d
+
+    # def jsonrpc_getMblogsWithComments(self, publisher_jid, item_ids, max_comments=C.RSM_MAX_COMMENTS):
+    #     """Get specified microblogs posted by a contact and their comments
+    #     @param publisher_jid: jid of the publisher
+    #     @param item_ids: list of microblogs items IDs
+    #     @return list of couple (microblog data, list of microblog data)"""
+    #     profile = ISATSession(self.session).profile
+    #     d = self.asyncBridgeCall("getGroupBlogsWithComments", publisher_jid, item_ids, {}, max_comments, profile)
+    #     return d
+
+    # def jsonrpc_getMassiveMblogs(self, publishers_type, publishers, rsm=None):
+    #     """Get lasts microblogs posted by several contacts at once
+
+    #     @param publishers_type (unicode): one of "ALL", "GROUP", "JID"
+    #     @param publishers (tuple(unicode)): tuple of publishers (empty list for all, list of groups or list of jids)
+    #     @param rsm (dict): TODO
+    #     @return: dict{unicode: list[dict])
+    #         key: publisher's jid
+    #         value: list of microblog data (dict)
+    #     """
+    #     profile = ISATSession(self.session).profile
+    #     if rsm is None:
+    #         rsm = {'max_': unicode(C.RSM_MAX_ITEMS)}
+    #     d = self.asyncBridgeCall("getMassiveGroupBlogs", publishers_type, publishers, rsm, profile)
+    #     self.sat_host.bridge.massiveSubscribeGroupBlogs(publishers_type, publishers, profile)
+    #     return d
+
+    # def jsonrpc_getMblogComments(self, service, node, rsm=None):
+    #     """Get all comments of given node
+    #     @param service: jid of the service hosting the node
+    #     @param node: comments node
+    #     """
+    #     profile = ISATSession(self.session).profile
+    #     if rsm is None:
+    #         rsm = {'max_': unicode(C.RSM_MAX_COMMENTS)}
+    #     d = self.asyncBridgeCall("getGroupBlogComments", service, node, rsm, profile)
+    #     return d
 
     def jsonrpc_getPresenceStatuses(self):
         """Get Presence information for connected contacts"""
@@ -1099,7 +1212,7 @@
             for signal_name in ['presenceUpdate', 'newMessage', 'subscribe', 'contactDeleted', 'newContact', 'entityDataUpdated', 'askConfirmation', 'newAlert', 'paramUpdate']:
                 self.bridge.register(signal_name, self.signal_handler.getGenericCb(signal_name))
             #plugins
-            for signal_name in ['personalEvent', 'roomJoined', 'roomUserJoined', 'roomUserLeft', 'tarotGameStarted', 'tarotGameNew', 'tarotGameChooseContrat',
+            for signal_name in ['psEvent', 'roomJoined', 'roomUserJoined', 'roomUserLeft', 'tarotGameStarted', 'tarotGameNew', 'tarotGameChooseContrat',
                                 'tarotGameShowCards', 'tarotGameInvalidCards', 'tarotGameCardsPlayed', 'tarotGameYourTurn', 'tarotGameScore', 'tarotGamePlayers',
                                 'radiocolStarted', 'radiocolPreload', 'radiocolPlay', 'radiocolNoUpload', 'radiocolUploadOk', 'radiocolSongRejected', 'radiocolPlayers',
                                 'roomLeft', 'roomUserChangedNick', 'chatStateReceived']: