diff src/browser/sat_browser/panels.py @ 586:3eb3a2c0c011

browser and server side: uses RSM (XEP-0059)
author souliane <souliane@mailoo.org>
date Fri, 28 Nov 2014 00:31:27 +0100
parents bade589dbd5a
children
line wrap: on
line diff
--- a/src/browser/sat_browser/panels.py	Thu Oct 23 16:56:36 2014 +0200
+++ b/src/browser/sat_browser/panels.py	Fri Nov 28 00:31:27 2014 +0100
@@ -23,7 +23,7 @@
 
 from sat_frontends.tools.strings import addURLToText
 from sat_frontends.tools.games import SYMBOLS
-from sat.core.i18n import _
+from sat.core.i18n import _, D_
 
 from pyjamas.ui.SimplePanel import SimplePanel
 from pyjamas.ui.AbsolutePanel import AbsolutePanel
@@ -406,7 +406,7 @@
         self.panel = FlowPanel()
         self.panel.setStyleName('mb_entry')
 
-        self.header = HTMLPanel('')
+        self.header = HorizontalPanel(StyleName='mb_entry_header')
         self.panel.add(self.header)
 
         self.entry_actions = VerticalPanel()
@@ -442,18 +442,56 @@
         self.__setIcons()
 
     def __setHeader(self):
-        """Set the entry header"""
+        """Set the entry header."""
         if self.empty:
             return
         update_text = u" — ✍ " + "<span class='mb_entry_timestamp'>%s</span>" % datetime.fromtimestamp(self.updated)
-        self.header.setHTML("""<div class='mb_entry_header'>
-                                   <span class='mb_entry_author'>%(author)s</span> on
-                                   <span class='mb_entry_timestamp'>%(published)s</span>%(updated)s
-                               </div>""" % {'author': html_tools.html_sanitize(self.author),
-                                            'published': datetime.fromtimestamp(self.published),
-                                            'updated': update_text if self.published != self.updated else ''
-                                            }
-                            )
+        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(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.
+
+        @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
+
+        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)
+        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
+
+        self.show_comments_link.setHTML("""<span class='mb_entry_comments'>%(text)s</span></div>""" % {'text': text})
 
     def __setIcons(self):
         """Set the entry icons (delete, update, comment)"""
@@ -490,6 +528,8 @@
             self.edit(True)
         elif sender == self.comment_label:
             self._comment()
+        elif sender == self.show_comments_link:
+            self._blog_panel.loadAllCommentsForEntry(self)
 
     def __modifiedCb(self, content):
         """Send the new content to the backend
@@ -517,7 +557,7 @@
         """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)
+            self._blog_panel.removeEntry(self.type, self.id, update_header=False)
             if self.type == 'main_item':  # restore the "New message" button
                 self._blog_panel.refresh()
             else:  # allow to create a new comment
@@ -589,7 +629,7 @@
                 'service': self.comments_service,
                 'node': self.comments_node
                 }
-        entry = self._blog_panel.addEntry(data)
+        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
@@ -662,7 +702,7 @@
             self._blog_panel.host.bridge.call('syntaxConvert', setBubble, text, C.SYNTAX_TEXT, C.SYNTAX_XHTML)
 
 
-class MicroblogPanel(base_widget.LiberviaWidget):
+class MicroblogPanel(base_widget.LiberviaWidget, MouseHandler):
     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 group <span class='warningTarget'>%s</span>"
 
@@ -671,6 +711,7 @@
         @param accepted_groups: groups displayed in this panel, if empty, show all microblogs from all contacts
         """
         base_widget.LiberviaWidget.__init__(self, host, ", ".join(accepted_groups), selectable=True)
+        MouseHandler.__init__(self)
         self.setAcceptedGroup(accepted_groups)
         self.host = host
         self.entries = {}
@@ -679,6 +720,12 @@
         self.vpanel = VerticalPanel()
         self.vpanel.setStyleName('microblogPanel')
         self.setWidget(self.vpanel)
+        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 refresh(self):
         """Refresh the display of this widget. If the unibox is disabled,
@@ -694,7 +741,7 @@
                         'new': True,
                         'author': self.host.whoami.bare,
                         }
-                entry = self.addEntry(data)
+                entry = self.addEntry(data, update_header=False)
                 entry.edit(True)
             if NEW_MESSAGE_USE_BUTTON:
                 self.new_button = Button("New message", listener=addBox)
@@ -708,10 +755,9 @@
 
         @return (MicroblogEntry): the new entry being edited.
         """
-        try:
-            first = self.vpanel.children[0]
-        except IndexError:
-            return None
+        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
 
@@ -729,11 +775,9 @@
         @return: the created MicroblogPanel
         """
         _items = item if isinstance(item, list) else ([] if item is None else [item])
-        _type = 'ALL' if _items == [] else 'GROUP'
         # XXX: pyjamas doesn't support use of cls directly
         _new_panel = MicroblogPanel(host, _items)
-        host.FillMicroblogPanel(_new_panel)
-        host.bridge.call('getMassiveLastMblogs', _new_panel.massiveInsert, _type, _items, 10)
+        _new_panel.loadMoreMainEntries()
         host.setSelected(_new_panel)
         _new_panel.refresh()
         return _new_panel
@@ -747,6 +791,27 @@
     def accepted_groups(self):
         return self._accepted_groups
 
+    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.call('getMassiveMblogs', self.massiveInsert, type_, self.accepted_groups, rsm)
+
     def matchEntity(self, item):
         """
         @param item: single group as a string, list of groups
@@ -802,61 +867,84 @@
 
     def massiveInsert(self, mblogs):
         """Insert several microblogs at once
-        @param mblogs: dictionary of microblogs, as the result of getMassiveLastGroupBlogs
+        @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 = sum([len(value) for value in mblogs.values()])
-        log.debug("Massive insertion of %d microblogs" % count)
+        count_pub = len(mblogs)
+        count_msg = sum([len(value) for value in mblogs.values()])
+        log.debug("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("adding blogs for [%s]" % publisher)
-            for mblog in mblogs[publisher]:
-                if not "content" in mblog:
-                    log.warning("No content found in microblog [%s]" % mblog)
-                    continue
-                self.addEntry(mblog)
+            log.debug("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 at once
-        @param mblogs: list of microblogs
+        """ 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 not "content" in mblog:
+            if "content" not in mblog:
                 log.warning("No content found in microblog [%s]" % mblog)
                 continue
-            self.addEntry(mblog)
+            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()]
+            count = int(rsm['count'])
+            hidden = count - (int(rsm['index']) + len(mblogs))
+            main_entry.updateHeader(count, hidden)
 
     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.published = time()
+            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:
+        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.published > entry.published)
+            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 addEntry(self, data):
+    def addEntry(self, data, update_header=True):
         """Add an entry to the panel
-        @param data: dict containing the item data
-        @return: the added entry, or None
+
+        @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 not comments_hash in self.comments:
+            if comments_hash not in self.comments:
                 # The comments node is not known in this panel
                 return None
             parent = self.comments[comments_hash]
@@ -871,6 +959,7 @@
                 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:
@@ -880,28 +969,37 @@
                     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.id in self.entries:  # update
-            idx = self.vpanel.getWidgetIndex(self.entries[_entry.id])
-            self.vpanel.remove(self.entries[_entry.id])
-            self.vpanel.insert(_entry, idx)
-        else:  # new entry
-            self._chronoInsert(self.vpanel, _entry)
-        self.entries[_entry.id] = _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_):
+    def removeEntry(self, type_, id_, update_header=True):
         """Remove an entry from the panel
-        @param type_: entry type ('main_item' or 'comment')
-        @param id_: entry id
+
+        @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':
@@ -919,6 +1017,9 @@
             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()
                         self.selected_entry = None
                         break
@@ -998,6 +1099,14 @@
                 return True
         return False
 
+    def onClick(self, sender):
+        if sender == self.footer:
+            self.loadMoreMainEntries()
+
+    def onMouseEnter(self, sender):
+        if sender == self.footer:
+            self.loadMoreMainEntries()
+
 
 class StatusPanel(base_panels.HTMLTextEditor):