Mercurial > libervia-web
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):