comparison 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
comparison
equal deleted inserted replaced
585:bade589dbd5a 586:3eb3a2c0c011
21 from sat.core.log import getLogger 21 from sat.core.log import getLogger
22 log = getLogger(__name__) 22 log = getLogger(__name__)
23 23
24 from sat_frontends.tools.strings import addURLToText 24 from sat_frontends.tools.strings import addURLToText
25 from sat_frontends.tools.games import SYMBOLS 25 from sat_frontends.tools.games import SYMBOLS
26 from sat.core.i18n import _ 26 from sat.core.i18n import _, D_
27 27
28 from pyjamas.ui.SimplePanel import SimplePanel 28 from pyjamas.ui.SimplePanel import SimplePanel
29 from pyjamas.ui.AbsolutePanel import AbsolutePanel 29 from pyjamas.ui.AbsolutePanel import AbsolutePanel
30 from pyjamas.ui.VerticalPanel import VerticalPanel 30 from pyjamas.ui.VerticalPanel import VerticalPanel
31 from pyjamas.ui.HorizontalPanel import HorizontalPanel 31 from pyjamas.ui.HorizontalPanel import HorizontalPanel
404 self._blog_panel = blog_panel 404 self._blog_panel = blog_panel
405 405
406 self.panel = FlowPanel() 406 self.panel = FlowPanel()
407 self.panel.setStyleName('mb_entry') 407 self.panel.setStyleName('mb_entry')
408 408
409 self.header = HTMLPanel('') 409 self.header = HorizontalPanel(StyleName='mb_entry_header')
410 self.panel.add(self.header) 410 self.panel.add(self.header)
411 411
412 self.entry_actions = VerticalPanel() 412 self.entry_actions = VerticalPanel()
413 self.entry_actions.setStyleName('mb_entry_actions') 413 self.entry_actions.setStyleName('mb_entry_actions')
414 self.panel.add(self.entry_actions) 414 self.panel.add(self.entry_actions)
440 self.__setHeader() 440 self.__setHeader()
441 self.__setBubble() 441 self.__setBubble()
442 self.__setIcons() 442 self.__setIcons()
443 443
444 def __setHeader(self): 444 def __setHeader(self):
445 """Set the entry header""" 445 """Set the entry header."""
446 if self.empty: 446 if self.empty:
447 return 447 return
448 update_text = u" — ✍ " + "<span class='mb_entry_timestamp'>%s</span>" % datetime.fromtimestamp(self.updated) 448 update_text = u" — ✍ " + "<span class='mb_entry_timestamp'>%s</span>" % datetime.fromtimestamp(self.updated)
449 self.header.setHTML("""<div class='mb_entry_header'> 449 self.header.add(HTML("""<span class='mb_entry_header_info'>
450 <span class='mb_entry_author'>%(author)s</span> on 450 <span class='mb_entry_author'>%(author)s</span> on
451 <span class='mb_entry_timestamp'>%(published)s</span>%(updated)s 451 <span class='mb_entry_timestamp'>%(published)s</span>%(updated)s
452 </div>""" % {'author': html_tools.html_sanitize(self.author), 452 </span>""" % {'author': html_tools.html_sanitize(self.author),
453 'published': datetime.fromtimestamp(self.published), 453 'published': datetime.fromtimestamp(self.published),
454 'updated': update_text if self.published != self.updated else '' 454 'updated': update_text if self.published != self.updated else ''
455 } 455 }))
456 ) 456 if self.comments:
457 self.comments_count = self.hidden_count = 0
458 self.show_comments_link = HTML('')
459 self.header.add(self.show_comments_link)
460
461 def updateHeader(self, comments_count=None, hidden_count=None, inc=None):
462 """Update the header.
463
464 @param comments_count (int): total number of comments.
465 @param hidden_count (int): number of hidden comments.
466 @param inc (int): number to increment the total number of comments with.
467 """
468 if comments_count is not None:
469 self.comments_count = comments_count
470 if hidden_count is not None:
471 self.hidden_count = hidden_count
472 if inc is not None:
473 self.comments_count += inc
474
475 if self.hidden_count > 0:
476 comments = D_('comments') if self.hidden_count > 1 else D_('comment')
477 text = D_("<a>show %(count)d previous %(comments)s</a>") % {'count': self.hidden_count,
478 'comments': comments}
479 if self not in self.show_comments_link._clickListeners:
480 self.show_comments_link.addClickListener(self)
481 else:
482 if self.comments_count > 1:
483 text = "%(count)d %(comments)s" % {'count': self.comments_count,
484 'comments': D_('comments')}
485 elif self.comments_count == 1:
486 text = D_('1 comment')
487 else:
488 text = ''
489 try:
490 self.show_comments_link.removeClickListener(self)
491 except ValueError:
492 pass
493
494 self.show_comments_link.setHTML("""<span class='mb_entry_comments'>%(text)s</span></div>""" % {'text': text})
457 495
458 def __setIcons(self): 496 def __setIcons(self):
459 """Set the entry icons (delete, update, comment)""" 497 """Set the entry icons (delete, update, comment)"""
460 if self.empty: 498 if self.empty:
461 return 499 return
488 self._delete() 526 self._delete()
489 elif sender == self.update_label: 527 elif sender == self.update_label:
490 self.edit(True) 528 self.edit(True)
491 elif sender == self.comment_label: 529 elif sender == self.comment_label:
492 self._comment() 530 self._comment()
531 elif sender == self.show_comments_link:
532 self._blog_panel.loadAllCommentsForEntry(self)
493 533
494 def __modifiedCb(self, content): 534 def __modifiedCb(self, content):
495 """Send the new content to the backend 535 """Send the new content to the backend
496 @return: False to restore the original content if a deletion has been cancelled 536 @return: False to restore the original content if a deletion has been cancelled
497 """ 537 """
515 555
516 def __afterEditCb(self, content): 556 def __afterEditCb(self, content):
517 """Remove the entry if it was an empty one (used for creating a new blog post). 557 """Remove the entry if it was an empty one (used for creating a new blog post).
518 Data for the actual new blog post will be received from the bridge""" 558 Data for the actual new blog post will be received from the bridge"""
519 if self.empty: 559 if self.empty:
520 self._blog_panel.removeEntry(self.type, self.id) 560 self._blog_panel.removeEntry(self.type, self.id, update_header=False)
521 if self.type == 'main_item': # restore the "New message" button 561 if self.type == 'main_item': # restore the "New message" button
522 self._blog_panel.refresh() 562 self._blog_panel.refresh()
523 else: # allow to create a new comment 563 else: # allow to create a new comment
524 self._parent_entry._current_comment = None 564 self._parent_entry._current_comment = None
525 self.entry_dialog.setWidth('auto') 565 self.entry_dialog.setWidth('auto')
587 'type': 'comment', 627 'type': 'comment',
588 'author': self._blog_panel.host.whoami.bare, 628 'author': self._blog_panel.host.whoami.bare,
589 'service': self.comments_service, 629 'service': self.comments_service,
590 'node': self.comments_node 630 'node': self.comments_node
591 } 631 }
592 entry = self._blog_panel.addEntry(data) 632 entry = self._blog_panel.addEntry(data, update_header=False)
593 if entry is None: 633 if entry is None:
594 log.info("The entry of id %s can not be commented" % self.id) 634 log.info("The entry of id %s can not be commented" % self.id)
595 return 635 return
596 entry._parent_entry = self 636 entry._parent_entry = self
597 self._current_comment = entry 637 self._current_comment = entry
660 dialog.ConfirmDialog(confirm_cb, text=_("Do you really want to lose the title and text formatting?")).show() 700 dialog.ConfirmDialog(confirm_cb, text=_("Do you really want to lose the title and text formatting?")).show()
661 else: 701 else:
662 self._blog_panel.host.bridge.call('syntaxConvert', setBubble, text, C.SYNTAX_TEXT, C.SYNTAX_XHTML) 702 self._blog_panel.host.bridge.call('syntaxConvert', setBubble, text, C.SYNTAX_TEXT, C.SYNTAX_XHTML)
663 703
664 704
665 class MicroblogPanel(base_widget.LiberviaWidget): 705 class MicroblogPanel(base_widget.LiberviaWidget, MouseHandler):
666 warning_msg_public = "This message will be <b>PUBLIC</b> and everybody will be able to see it, even people you don't know" 706 warning_msg_public = "This message will be <b>PUBLIC</b> and everybody will be able to see it, even people you don't know"
667 warning_msg_group = "This message will be published for all the people of the group <span class='warningTarget'>%s</span>" 707 warning_msg_group = "This message will be published for all the people of the group <span class='warningTarget'>%s</span>"
668 708
669 def __init__(self, host, accepted_groups): 709 def __init__(self, host, accepted_groups):
670 """Panel used to show microblog 710 """Panel used to show microblog
671 @param accepted_groups: groups displayed in this panel, if empty, show all microblogs from all contacts 711 @param accepted_groups: groups displayed in this panel, if empty, show all microblogs from all contacts
672 """ 712 """
673 base_widget.LiberviaWidget.__init__(self, host, ", ".join(accepted_groups), selectable=True) 713 base_widget.LiberviaWidget.__init__(self, host, ", ".join(accepted_groups), selectable=True)
714 MouseHandler.__init__(self)
674 self.setAcceptedGroup(accepted_groups) 715 self.setAcceptedGroup(accepted_groups)
675 self.host = host 716 self.host = host
676 self.entries = {} 717 self.entries = {}
677 self.comments = {} 718 self.comments = {}
678 self.selected_entry = None 719 self.selected_entry = None
679 self.vpanel = VerticalPanel() 720 self.vpanel = VerticalPanel()
680 self.vpanel.setStyleName('microblogPanel') 721 self.vpanel.setStyleName('microblogPanel')
681 self.setWidget(self.vpanel) 722 self.setWidget(self.vpanel)
723 self.footer = HTML('', StyleName='microblogPanel_footer')
724 self.footer.waiting = False
725 self.footer.addClickListener(self)
726 self.footer.addMouseListener(self)
727 self.vpanel.add(self.footer)
728 self.next_rsm_index = 0
682 729
683 def refresh(self): 730 def refresh(self):
684 """Refresh the display of this widget. If the unibox is disabled, 731 """Refresh the display of this widget. If the unibox is disabled,
685 display the 'New message' button or an empty bubble on top of the panel""" 732 display the 'New message' button or an empty bubble on top of the panel"""
686 if hasattr(self, 'new_button'): 733 if hasattr(self, 'new_button'):
692 self.new_button.setVisible(False) 739 self.new_button.setVisible(False)
693 data = {'id': str(time()), 740 data = {'id': str(time()),
694 'new': True, 741 'new': True,
695 'author': self.host.whoami.bare, 742 'author': self.host.whoami.bare,
696 } 743 }
697 entry = self.addEntry(data) 744 entry = self.addEntry(data, update_header=False)
698 entry.edit(True) 745 entry.edit(True)
699 if NEW_MESSAGE_USE_BUTTON: 746 if NEW_MESSAGE_USE_BUTTON:
700 self.new_button = Button("New message", listener=addBox) 747 self.new_button = Button("New message", listener=addBox)
701 self.new_button.setStyleName("microblogNewButton") 748 self.new_button.setStyleName("microblogNewButton")
702 self.vpanel.insert(self.new_button, 0) 749 self.vpanel.insert(self.new_button, 0)
706 def getNewMainEntry(self): 753 def getNewMainEntry(self):
707 """Get the new entry being edited, or None if it doesn't exists. 754 """Get the new entry being edited, or None if it doesn't exists.
708 755
709 @return (MicroblogEntry): the new entry being edited. 756 @return (MicroblogEntry): the new entry being edited.
710 """ 757 """
711 try: 758 if len(self.vpanel.children) < 2:
712 first = self.vpanel.children[0] 759 return None # there's only the footer
713 except IndexError: 760 first = self.vpanel.children[0]
714 return None
715 assert(first.type == 'main_item') 761 assert(first.type == 'main_item')
716 return first if first.empty else None 762 return first if first.empty else None
717 763
718 @classmethod 764 @classmethod
719 def registerClass(cls): 765 def registerClass(cls):
727 @param item: single group as a string, list of groups 773 @param item: single group as a string, list of groups
728 (as an array) or None (for the meta group = "all groups") 774 (as an array) or None (for the meta group = "all groups")
729 @return: the created MicroblogPanel 775 @return: the created MicroblogPanel
730 """ 776 """
731 _items = item if isinstance(item, list) else ([] if item is None else [item]) 777 _items = item if isinstance(item, list) else ([] if item is None else [item])
732 _type = 'ALL' if _items == [] else 'GROUP'
733 # XXX: pyjamas doesn't support use of cls directly 778 # XXX: pyjamas doesn't support use of cls directly
734 _new_panel = MicroblogPanel(host, _items) 779 _new_panel = MicroblogPanel(host, _items)
735 host.FillMicroblogPanel(_new_panel) 780 _new_panel.loadMoreMainEntries()
736 host.bridge.call('getMassiveLastMblogs', _new_panel.massiveInsert, _type, _items, 10)
737 host.setSelected(_new_panel) 781 host.setSelected(_new_panel)
738 _new_panel.refresh() 782 _new_panel.refresh()
739 return _new_panel 783 return _new_panel
740 784
741 @classmethod 785 @classmethod
744 return MicroblogPanel.createPanel(host, None) 788 return MicroblogPanel.createPanel(host, None)
745 789
746 @property 790 @property
747 def accepted_groups(self): 791 def accepted_groups(self):
748 return self._accepted_groups 792 return self._accepted_groups
793
794 def loadAllCommentsForEntry(self, main_entry):
795 """Load all the comments for the given main entry.
796
797 @param main_entry (MicroblogEntry): main entry having comments.
798 """
799 index = str(main_entry.comments_count - main_entry.hidden_count)
800 rsm = {'max': str(main_entry.hidden_count), 'index': index}
801 self.host.bridge.call('getMblogComments', self.mblogsInsert, main_entry.comments_service, main_entry.comments_node, rsm)
802
803 def loadMoreMainEntries(self):
804 if self.footer.waiting:
805 return
806 self.footer.waiting = True
807 self.footer.setHTML("loading...")
808
809 self.host.loadOurMainEntries(self.next_rsm_index, self)
810
811 type_ = 'ALL' if self.accepted_groups == [] else 'GROUP'
812 rsm = {'max': str(C.RSM_MAX_ITEMS), 'index': str(self.next_rsm_index)}
813 self.host.bridge.call('getMassiveMblogs', self.massiveInsert, type_, self.accepted_groups, rsm)
749 814
750 def matchEntity(self, item): 815 def matchEntity(self, item):
751 """ 816 """
752 @param item: single group as a string, list of groups 817 @param item: single group as a string, list of groups
753 (as an array) or None (for the meta group = "all groups") 818 (as an array) or None (for the meta group = "all groups")
800 """Ask all the entries for the currenly accepted groups, 865 """Ask all the entries for the currenly accepted groups,
801 and fill the panel""" 866 and fill the panel"""
802 867
803 def massiveInsert(self, mblogs): 868 def massiveInsert(self, mblogs):
804 """Insert several microblogs at once 869 """Insert several microblogs at once
805 @param mblogs: dictionary of microblogs, as the result of getMassiveLastGroupBlogs 870 @param mblogs (dict): dictionary mapping a publisher to microblogs data:
806 """ 871 - key: publisher (str)
807 count = sum([len(value) for value in mblogs.values()]) 872 - value: couple (list[dict], dict) with:
808 log.debug("Massive insertion of %d microblogs" % count) 873 - list of microblogs data
874 - RSM response data
875 """
876 count_pub = len(mblogs)
877 count_msg = sum([len(value) for value in mblogs.values()])
878 log.debug("massive insertion of {count_msg} blogs for {count_pub} contacts".format(count_msg=count_msg, count_pub=count_pub))
809 for publisher in mblogs: 879 for publisher in mblogs:
810 log.debug("adding blogs for [%s]" % publisher) 880 log.debug("adding {count} blogs for [{publisher}]".format(count=len(mblogs[publisher]), publisher=publisher))
811 for mblog in mblogs[publisher]: 881 self.mblogsInsert(mblogs[publisher])
812 if not "content" in mblog: 882 self.next_rsm_index += C.RSM_MAX_ITEMS
813 log.warning("No content found in microblog [%s]" % mblog) 883 self.footer.waiting = False
814 continue 884 self.footer.setHTML('show older messages')
815 self.addEntry(mblog)
816 885
817 def mblogsInsert(self, mblogs): 886 def mblogsInsert(self, mblogs):
818 """ Insert several microblogs at once 887 """ Insert several microblogs from the same node at once.
819 @param mblogs: list of microblogs 888
820 """ 889 @param mblogs (list): couple (list[dict], dict) with:
890 - list of microblogs data
891 - RSM response data
892 """
893 mblogs, rsm = mblogs
894
821 for mblog in mblogs: 895 for mblog in mblogs:
822 if not "content" in mblog: 896 if "content" not in mblog:
823 log.warning("No content found in microblog [%s]" % mblog) 897 log.warning("No content found in microblog [%s]" % mblog)
824 continue 898 continue
825 self.addEntry(mblog) 899 self.addEntry(mblog, update_header=False)
900
901 hashes = set([(entry['service'], entry['node']) for entry in mblogs if entry['type'] == 'comment'])
902 assert(len(hashes) < 2) # ensure the blogs come from the same node
903 if len(hashes) == 1:
904 main_entry = self.comments[hashes.pop()]
905 count = int(rsm['count'])
906 hidden = count - (int(rsm['index']) + len(mblogs))
907 main_entry.updateHeader(count, hidden)
826 908
827 def _chronoInsert(self, vpanel, entry, reverse=True): 909 def _chronoInsert(self, vpanel, entry, reverse=True):
828 """ Insert an entry in chronological order 910 """ Insert an entry in chronological order
829 @param vpanel: VerticalPanel instance 911 @param vpanel: VerticalPanel instance
830 @param entry: MicroblogEntry 912 @param entry: MicroblogEntry
831 @param reverse: more recent entry on top if True, chronological order else""" 913 @param reverse: more recent entry on top if True, chronological order else"""
914 # XXX: for now we can't use "published" timestamp because the entries
915 # are retrieved using the "updated" field. We don't want new items
916 # inserted with RSM to be inserted "randomly" in the panel, they
917 # should be added at the bottom of the list.
832 assert(isinstance(reverse, bool)) 918 assert(isinstance(reverse, bool))
833 if entry.empty: 919 if entry.empty:
834 entry.published = time() 920 entry.updated = time()
835 # we look for the right index to insert our entry: 921 # we look for the right index to insert our entry:
836 # if reversed, we insert the entry above the first entry 922 # if reversed, we insert the entry above the first entry
837 # in the past 923 # in the past
838 idx = 0 924 idx = 0
839 925
840 for child in vpanel.children: 926 for child in vpanel.children[0:-1]: # ignore the footer
841 if not isinstance(child, MicroblogEntry): 927 if not isinstance(child, MicroblogEntry):
842 idx += 1 928 idx += 1
843 continue 929 continue
844 condition_to_stop = child.empty or (child.published > entry.published) 930 condition_to_stop = child.empty or (child.updated > entry.updated)
845 if condition_to_stop != reverse: # != is XOR 931 if condition_to_stop != reverse: # != is XOR
846 break 932 break
847 idx += 1 933 idx += 1
848 934
849 vpanel.insert(entry, idx) 935 vpanel.insert(entry, idx)
850 936
851 def addEntry(self, data): 937 def addEntry(self, data, update_header=True):
852 """Add an entry to the panel 938 """Add an entry to the panel
853 @param data: dict containing the item data 939
854 @return: the added entry, or None 940 @param data (dict): dict containing the item data
941 @param update_header (bool): update or not the main comment header
942 @return: the added MicroblogEntry instance, or None
855 """ 943 """
856 _entry = MicroblogEntry(self, data) 944 _entry = MicroblogEntry(self, data)
857 if _entry.type == "comment": 945 if _entry.type == "comment":
858 comments_hash = (_entry.service, _entry.node) 946 comments_hash = (_entry.service, _entry.node)
859 if not comments_hash in self.comments: 947 if comments_hash not in self.comments:
860 # The comments node is not known in this panel 948 # The comments node is not known in this panel
861 return None 949 return None
862 parent = self.comments[comments_hash] 950 parent = self.comments[comments_hash]
863 parent_idx = self.vpanel.getWidgetIndex(parent) 951 parent_idx = self.vpanel.getWidgetIndex(parent)
864 # we find or create the panel where the comment must be inserted 952 # we find or create the panel where the comment must be inserted
869 if not sub_panel or not isinstance(sub_panel, VerticalPanel): 957 if not sub_panel or not isinstance(sub_panel, VerticalPanel):
870 sub_panel = VerticalPanel() 958 sub_panel = VerticalPanel()
871 sub_panel.setStyleName('microblogPanel') 959 sub_panel.setStyleName('microblogPanel')
872 sub_panel.addStyleName('subPanel') 960 sub_panel.addStyleName('subPanel')
873 self.vpanel.insert(sub_panel, parent_idx + 1) 961 self.vpanel.insert(sub_panel, parent_idx + 1)
962
874 for idx in xrange(0, len(sub_panel.getChildren())): 963 for idx in xrange(0, len(sub_panel.getChildren())):
875 comment = sub_panel.getIndexedChild(idx) 964 comment = sub_panel.getIndexedChild(idx)
876 if comment.id == _entry.id: 965 if comment.id == _entry.id:
877 # update an existing comment 966 # update an existing comment
878 sub_panel.remove(comment) 967 sub_panel.remove(comment)
879 sub_panel.insert(_entry, idx) 968 sub_panel.insert(_entry, idx)
880 return _entry 969 return _entry
881 # we want comments to be inserted in chronological order 970 # we want comments to be inserted in chronological order
882 self._chronoInsert(sub_panel, _entry, reverse=False) 971 self._chronoInsert(sub_panel, _entry, reverse=False)
972 if update_header:
973 parent.updateHeader(inc=+1)
883 return _entry 974 return _entry
884
885 if _entry.id in self.entries: # update
886 idx = self.vpanel.getWidgetIndex(self.entries[_entry.id])
887 self.vpanel.remove(self.entries[_entry.id])
888 self.vpanel.insert(_entry, idx)
889 else: # new entry
890 self._chronoInsert(self.vpanel, _entry)
891 self.entries[_entry.id] = _entry
892 975
893 if _entry.comments: 976 if _entry.comments:
894 # entry has comments, we keep the comments service/node as a reference 977 # entry has comments, we keep the comments service/node as a reference
895 comments_hash = (_entry.comments_service, _entry.comments_node) 978 comments_hash = (_entry.comments_service, _entry.comments_node)
896 self.comments[comments_hash] = _entry 979 self.comments[comments_hash] = _entry
897 self.host.bridge.call('getMblogComments', self.mblogsInsert, _entry.comments_service, _entry.comments_node) 980
981 if _entry.id in self.entries: # update
982 old_entry = self.entries[_entry.id]
983 idx = self.vpanel.getWidgetIndex(old_entry)
984 counts = (old_entry.comments_count, old_entry.hidden_count)
985 self.vpanel.remove(old_entry)
986 self.vpanel.insert(_entry, idx)
987 _entry.updateHeader(*counts)
988 else: # new entry
989 self._chronoInsert(self.vpanel, _entry)
990 if _entry.comments:
991 self.host.bridge.call('getMblogComments', self.mblogsInsert, _entry.comments_service, _entry.comments_node)
992
993 self.entries[_entry.id] = _entry
898 994
899 return _entry 995 return _entry
900 996
901 def removeEntry(self, type_, id_): 997 def removeEntry(self, type_, id_, update_header=True):
902 """Remove an entry from the panel 998 """Remove an entry from the panel
903 @param type_: entry type ('main_item' or 'comment') 999
904 @param id_: entry id 1000 @param type_ (str): entry type ('main_item' or 'comment')
1001 @param id_ (str): entry id
1002 @param update_header (bool): update or not the main comment header
905 """ 1003 """
906 for child in self.vpanel.getChildren(): 1004 for child in self.vpanel.getChildren():
907 if isinstance(child, MicroblogEntry) and type_ == 'main_item': 1005 if isinstance(child, MicroblogEntry) and type_ == 'main_item':
908 if child.id == id_: 1006 if child.id == id_:
909 main_idx = self.vpanel.getWidgetIndex(child) 1007 main_idx = self.vpanel.getWidgetIndex(child)
917 self.selected_entry = None 1015 self.selected_entry = None
918 break 1016 break
919 elif isinstance(child, VerticalPanel) and type_ == 'comment': 1017 elif isinstance(child, VerticalPanel) and type_ == 'comment':
920 for comment in child.getChildren(): 1018 for comment in child.getChildren():
921 if comment.id == id_: 1019 if comment.id == id_:
1020 if update_header:
1021 hash_ = (comment.service, comment.node)
1022 self.comments[hash_].updateHeader(inc=-1)
922 comment.removeFromParent() 1023 comment.removeFromParent()
923 self.selected_entry = None 1024 self.selected_entry = None
924 break 1025 break
925 1026
926 def ensureVisible(self, entry): 1027 def ensureVisible(self, entry):
995 return True 1096 return True
996 for group in self._accepted_groups: 1097 for group in self._accepted_groups:
997 if self.host.contact_panel.isContactInGroup(group, jid_s): 1098 if self.host.contact_panel.isContactInGroup(group, jid_s):
998 return True 1099 return True
999 return False 1100 return False
1101
1102 def onClick(self, sender):
1103 if sender == self.footer:
1104 self.loadMoreMainEntries()
1105
1106 def onMouseEnter(self, sender):
1107 if sender == self.footer:
1108 self.loadMoreMainEntries()
1000 1109
1001 1110
1002 class StatusPanel(base_panels.HTMLTextEditor): 1111 class StatusPanel(base_panels.HTMLTextEditor):
1003 1112
1004 EMPTY_STATUS = '&lt;click to set a status&gt;' 1113 EMPTY_STATUS = '&lt;click to set a status&gt;'