changeset 202:2bc6cf004e61

browser, server: comments handling: - banner in unibar show a specific message when the message will be a comment - comments are inserted in a subpanel, in chronological order
author Goffi <goffi@goffi.org>
date Thu, 20 Jun 2013 12:16:46 +0200 (2013-06-20)
parents aa76793da353
children 5fdea93b2541
files browser_side/panels.py libervia.py libervia.tac public/libervia.css
diffstat 4 files changed, 172 insertions(+), 56 deletions(-) [+]
line wrap: on
line diff
--- a/browser_side/panels.py	Sun Apr 07 22:33:55 2013 +0200
+++ b/browser_side/panels.py	Thu Jun 20 12:16:46 2013 +0200
@@ -64,7 +64,7 @@
     def __init__(self, host):
         TextArea.__init__(self)
         #AutoCompleteTextBox.__init__(self)
-        self.__size = (0,0) 
+        self.__size = (0,0)
         self._popup = None
         self._timer = Timer(notify=self._timeCb)
         self.host = host
@@ -84,7 +84,7 @@
             self.getCompletionItems().completions.remove(key)
         except KeyError:
             print "WARNING: trying to remove an unknown key"
-            
+
 
     def showWarning(self, target_data):
         target_hook, _type, msg = target_data
@@ -115,7 +115,7 @@
             self._popup.addStyleName(style)
 
         left = 0
-        top  = 0 #max(0, self.getAbsoluteTop() - contents.getOffsetHeight() - 2)   
+        top  = 0 #max(0, self.getAbsoluteTop() - contents.getOffsetHeight() - 2)
         self._popup.setPopupPosition(left, top)
         self._popup.show()
 
@@ -132,7 +132,7 @@
             - target_type: one of PUBLIC, GROUP, ONE2ONE, STATUS, MISC
             - msg: HTML message which will appear in the privacy warning banner """
         target = self._selected_cache
-        
+
         def getSelectedOrStatus():
             if target:
                 _type, msg = target.getWarningData()
@@ -147,7 +147,7 @@
         elif txt.startswith('@@: '):
             _type = "PUBLIC"
             msg = MicroblogPanel.warning_msg_public
-            target_hook = (txt[4:], None) 
+            target_hook = (txt[4:], None)
         elif txt.startswith('@'):
             _end = txt.find(': ')
             if _end == -1:
@@ -165,7 +165,7 @@
         else:
             print "ERROR: Unknown target"
             target_hook, _type, msg = getSelectedOrStatus()
-        
+
         return (target_hook, _type, msg)
 
     def onBrowserEvent(self, event):
@@ -216,7 +216,7 @@
         self._selected_cache = selected
 
     """def complete(self):
-        
+
         #self.visible=False #XXX: self.visible is not unset in pyjamas when ENTER is pressed and a completion is done
         #XXX: fixed directly on pyjamas, if the patch is accepted, no need to walk around this
         return AutoCompleteTextBox.complete(self)"""
@@ -226,10 +226,24 @@
 
     def __init__(self, data):
         self.id = data['id']
+        self.type = data.get('type','main_item')
         self.content = data['content']
         self.author = data['author']
         self.timestamp = float(data.get('timestamp',0)) #XXX: int doesn't work here
-        
+        self.comments = data.get('comments', False)
+        if self.comments:
+            try:
+                self.comments_hash = (data['comments_service'], data['comments_node'])
+                self.comments_service = data['comments_service']
+                self.comments_node = data['comments_node']
+            except KeyError:
+                print "Warning: can't manage comment [%s], some keys are missing in microblog data (%s)" % (data["comments"], data.keys())
+                self.comments = False
+        if set(("service","node")).issubset(data.keys()):
+            self.service = data["service"]
+            self.node = data["node"]
+            self.hash = (self.service, self.node)
+
 class MicroblogEntry(SimplePanel, ClickHandler):
 
     def __init__(self, blog_panel, mblog_entry):
@@ -239,6 +253,7 @@
         self.author = mblog_entry.author
         self.timestamp = mblog_entry.timestamp
         _datetime = datetime.fromtimestamp(mblog_entry.timestamp)
+        self.comments = mblog_entry.comments
 
         self.panel = HTMLPanel("""
             <div class='mb_entry_header'><span class='mb_entry_author'>%(author)s</span> on <span class='mb_entry_timestamp'>%(timestamp)s</span></div>
@@ -261,10 +276,10 @@
         """Change the avatar of the entry
         @param new_avatar: path to the new image"""
         self.avatar.setUrl(new_avatar)
-    
+
     def onClick(self, sender):
         print "microblog entry selected (author=%s)" % self.author
-        self._blog_panel.setSelectedEntry(self)
+        self._blog_panel.setSelectedEntry(self if self.comments else None)
 
 
 class MicroblogPanel(base_widget.LiberviaWidget):
@@ -280,6 +295,7 @@
         #DropCell.__init__(self)
         self.accepted_groups = accepted_groups
         self.entries = {}
+        self.comments = {}
         self.selected_entry = None
         self.vpanel = VerticalPanel()
         self.vpanel.setStyleName('microblogPanel')
@@ -306,7 +322,13 @@
         return _new_panel
 
     def getWarningData(self):
-        if not self.accepted_groups:
+        if self.selected_entry:
+            if not self.selected_entry.comments:
+                print ("ERROR: an item without comment is selected")
+                return ("NONE", None)
+            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:
             # we have a meta MicroblogPanel, we publish publicly
             return ("PUBLIC", self.warning_msg_public)
         else:
@@ -315,9 +337,17 @@
             return ("GROUP", self.warning_msg_group %  self.accepted_groups[0])
 
     def onTextEntered(self, text):
-        if not self.accepted_groups:
+        if self.selected_entry:
+            # we are entering a comment
+            comments_node = self.selected_entry.comments
+            if not comments_node:
+                raise Exception("ERROR: comments node is empty")
+            self.host.bridge.call("sendMblogComment", None, comments_node, text)
+        elif not self.accepted_groups:
+            # we are entering a public microblog
             self.host.bridge.call("sendMblog", None, "PUBLIC", None, text)
         else:
+            # we are entering a microblog restricted to a group
             # FIXME: manage several groups
             self.host.bridge.call("sendMblog", None, "GROUP", self.accepted_groups[0], text)
 
@@ -327,7 +357,7 @@
     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: dictionary of microblogs, as the result of getMassiveLastGroupBlogs
@@ -336,32 +366,85 @@
         for publisher in mblogs:
             print "adding blogs for [%s]" % publisher
             for mblog in mblogs[publisher]:
-                if not mblog.has_key('content'):
+                if not "content" in mblog:
                     print ("WARNING: No content found in microblog [%s]", mblog)
                     continue
-                mblog_entry = MicroblogItem(mblog)
-                self.addEntry(mblog_entry)
+                mblog_item = MicroblogItem(mblog)
+                self.addEntry(mblog_item)
 
-    def addEntry(self, mblog_entry):
-        """Add an entry to the panel
-        @param mblog_entry: MicroblogItem instance
+    def mblogsInsert(self, mblogs):
+        """ Insert several microblogs at once
+        @param mblogs: list of microblogs
         """
-        if mblog_entry.id in self.entries:
-            return
-        _entry = MicroblogEntry(self, mblog_entry)
-        self.entries[mblog_entry.id] = _entry
-        
+        for mblog in mblogs:
+            if not "content" in mblog:
+                print ("WARNING: No content found in microblog [%s]", mblog)
+                continue
+            mblog_item = MicroblogItem(mblog)
+            self.addEntry(mblog_item)
+
+    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"""
         # we look for the right index to insert our entry:
-        # we insert the entry above the first entry
+        # if reversed, we insert the entry above the first entry
         # in the past
         idx = 0
-        for child in self.vpanel.children:
+
+        for child in vpanel.children:
             if not isinstance(child, MicroblogEntry):
-                break
-            if child.timestamp < mblog_entry.timestamp:
-                break
+                idx+=1
+                continue
+            if reverse:
+                if child.timestamp < entry.timestamp:
+                    break
+            else:
+                if child.timestamp > entry.timestamp:
+                    break
             idx+=1
-        self.vpanel.insert(_entry,idx)
+
+        vpanel.insert(entry, idx)
+
+    def addEntry(self, mblog_item):
+        """Add an entry to the panel
+        @param mblog_item: MicroblogItem instance
+        """
+        if mblog_item.type == "comment":
+            if not mblog_item.hash in self.comments:
+                # The comments node is not known in this panel
+                return
+            _entry = MicroblogEntry(self, mblog_item)
+            parent = self.comments[mblog_item.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)
+
+            # we want comments to be inserted in chronological order
+            self._chronoInsert(sub_panel, _entry, reverse=False)
+            return
+
+        if mblog_item.id in self.entries:
+            return
+        _entry = MicroblogEntry(self, mblog_item)
+
+        self.entries[mblog_item.id] = _entry
+
+        self._chronoInsert(self.vpanel, _entry)
+
+        if mblog_item.comments:
+            # entry has comments, we keep the comment node as a reference
+            self.comments[mblog_item.comments_hash] = _entry
+            self.host.bridge.call('getMblogComments', self.mblogsInsert, mblog_item.comments_service, mblog_item.comments_node)
 
     def setSelectedEntry(self, entry):
         if self.selected_entry == entry:
@@ -378,10 +461,14 @@
         @param type: one of 'avatar', 'nick'
         @param jid: jid concerned
         @param value: new value"""
+        def updateVPanel(vpanel):
+            for child in vpanel.children:
+                if isinstance(child, MicroblogEntry) and child.author == jid:
+                    child.updateAvatar(value)
+                elif isinstance(child, VerticalPanel):
+                    updateVPanel(child)
         if type=='avatar':
-            for entry in self.entries.values():
-                if entry.author == jid:
-                    entry.updateAvatar(value)
+            updateVPanel(self.vpanel)
 
     def setAcceptedGroup(self, group):
         """Set the group which can be displayed in this panel
@@ -466,7 +553,7 @@
             self.remove(self.occupants_list[nick])
         except KeyError:
             print "ERROR: trying to remove an unexisting nick"
-    
+
     def clear(self):
         self.occupants_list.clear()
         AbsolutePanel.clear(self)
@@ -543,12 +630,12 @@
         base_widget.LiberviaWidget.onQuit(self)
         if self.type == 'group':
             self.host.bridge.call('mucLeave', None, self.target.bare)
-        
+
 
     def setUserNick(self, nick):
         """Set the nick of the user, usefull for e.g. change the color of the user"""
         self.nick = nick
-   
+
     def setPresents(self, nicks):
         """Set the users presents in this room
         @param occupants: list of nicks (string)"""
@@ -571,7 +658,7 @@
                 timestamp, from_jid, to_jid, message, mess_type = line
                 self.printMessage(from_jid, message, timestamp)
         self.host.bridge.call('getHistory', getHistoryCB, self.host.whoami.bare, self.target.bare, size, True)
-   
+
     def printInfo(self, msg, type='normal'):
         """Print general info
         @param msg: message to print
@@ -587,19 +674,19 @@
         else:
             _wid.setStyleName('chatTextInfo')
         self.content.add(_wid)
-        
+
 
     def printMessage(self, from_jid, msg, timestamp=None):
         """Print message in chat window. Must be implemented by child class"""
         _jid=JID(from_jid)
-        nick = _jid.node if self.type=='one2one' else _jid.resource 
+        nick = _jid.node if self.type=='one2one' else _jid.resource
         mymess = _jid.resource == self.nick if self.type == "group" else  _jid.bare == self.host.whoami.bare #mymess = True if message comes from local user
         if msg.startswith('/me '):
             self.printInfo('* %s %s' % (nick, msg[4:]),type='me')
             return
         self.content.add(ChatText(timestamp, nick, mymess, msg))
         self.content_scroll.scrollToBottom()
-    
+
     def startGame(self, game_type, referee, players):
         """Configure the chat window to start a game"""
         if game_type=="Tarot":
@@ -612,22 +699,22 @@
             #XXX: We can have double panel if we join quickly enough to have the group chat start signal
             #     on invitation + the one triggered on room join
             if hasattr(self, "radiocol_panel"):
-                return 
+                return
             self.radiocol_panel = RadioColPanel(self, referee, self.nick)
             self.vpanel.insert(self.radiocol_panel, 0)
             self.vpanel.setCellHeight(self.radiocol_panel, self.radiocol_panel.getHeight())
-    
+
     def getGame(self, game_type):
         """Return class managing the game type"""
         #TODO: check that the game is launched, and manage errors
         if game_type=="Tarot":
-            return self.tarot_panel 
+            return self.tarot_panel
         elif game_type=="RadioCol":
-            return self.radiocol_panel 
+            return self.radiocol_panel
 
 class WebPanel(base_widget.LiberviaWidget):
     """ (mini)browser like widget """
-    
+
     def __init__(self, host, url=None):
         """
         @param host: SatWebFrontend instance
@@ -658,7 +745,7 @@
 
 class ContactTabPanel(HorizontalPanel):
     """ TabPanel with a contacts list which can be hidden """
-    
+
     def __init__(self, host, locked = False):
         self.host=host
         HorizontalPanel.__init__(self)
@@ -691,10 +778,10 @@
         #unibox
         unibox_panel = UniBoxPanel(host)
         self.host.setUniBox(unibox_panel.unibox)
-        
+
         #status bar
         status = host.status_panel
-        
+
         #contacts
         _contacts = VerticalPanel()
         contacts_switch = Button('<<', self._contactsSwitch)
@@ -719,7 +806,7 @@
         _hpanel.add(_contacts)
         _hpanel.add(self.tab_panel)
         self.add(_hpanel)
-        
+
         self.setWidth("100%")
         Window.addWindowResizeListener(self)
 
--- a/libervia.py	Sun Apr 07 22:33:55 2013 +0200
+++ b/libervia.py	Thu Jun 20 12:16:46 2013 +0200
@@ -83,7 +83,7 @@
 class BridgeCall(LiberviaJsonProxy):
     def __init__(self):
         LiberviaJsonProxy.__init__(self, "/json_api",
-                        ["getContacts", "addContact", "sendMessage", "sendMblog", "getLastMblogs", "getMassiveLastMblogs", "getProfileJid", "getHistory", "getPresenceStatus",
+                        ["getContacts", "addContact", "sendMessage", "sendMblog", "sendMblogComment", "getLastMblogs", "getMassiveLastMblogs", "getMblogComments", "getProfileJid", "getHistory", "getPresenceStatus",
                          "joinMUC", "mucLeave", "getRoomsJoined", "launchTarotGame", "getTarotCardsPaths", "tarotGameReady", "tarotGameContratChoosed",
                          "tarotGamePlayCards", "launchRadioCollective", "getWaitingSub", "subscription", "delContact", "updateContact", "getEntityData", "getParamsUI",
                          #"setParam",
@@ -202,7 +202,7 @@
     def getAvatar(self, jid_str):
         """Return avatar of a jid if in cache, else ask for it"""
         def dataReceived(result):
-            if result.has_key('avatar'):
+            if 'avatar' in result:
                 self._entityDataUpdatedCb(jid_str, 'avatar', result['avatar'])
         
         if jid_str not in self.avatars_cache:
@@ -361,10 +361,10 @@
 
     def _personalEventCb(self, sender, event_type, data):
         if event_type == "MICROBLOG":
-            if not data.has_key('content'):
+            if not 'content' in data:
                 print ("WARNING: No content found in microblog data")
                 return
-            if data.has_key('groups'):
+            if 'groups' in data:
                 _groups = set(data['groups'].split() if data['groups'] else [])
             else:
                 _groups=None
@@ -385,7 +385,7 @@
         @param sender: jid of the entry sender
         @param _groups: groups which can receive this entry
         @param mblog_entry: MicroblogItem instance"""
-        if mblog_panel.isJidAccepted(sender) or (_groups == None and self.whoami and sender == self.whoami.bare) \
+        if mblog_entry.type == "comment" or mblog_panel.isJidAccepted(sender) or (_groups == None and self.whoami and sender == self.whoami.bare) \
            or (_groups and _groups.intersection(mblog_panel.accepted_groups)):
             mblog_panel.addEntry(mblog_entry)
 
--- a/libervia.tac	Sun Apr 07 22:33:55 2013 +0200
+++ b/libervia.tac	Thu Jun 20 12:16:46 2013 +0200
@@ -201,10 +201,21 @@
             if _type == "PUBLIC":
                 #This text if for the public microblog
                 print "sending public blog"
-                return self.sat_host.bridge.sendGroupBlog("PUBLIC", [], text, profile)
+                return self.sat_host.bridge.sendGroupBlog("PUBLIC", [], text, {'allow_comments': 'True'}, profile)
             else:
                 print "sending group blog"
-                return self.sat_host.bridge.sendGroupBlog("GROUP", [dest], text, profile)
+                return self.sat_host.bridge.sendGroupBlog("GROUP", [dest], text, {'allow_comments': 'True'}, profile)
+        else:
+            raise Exception("Invalid data")
+
+    def jsonrpc_sendMblogComment(self, node, text):
+        """ 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, profile)
         else:
             raise Exception("Invalid data") 
     
@@ -230,6 +241,17 @@
         self.sat_host.bridge.massiveSubscribeGroupBlogs(publishers_type, publishers_list, profile)
         return d
 
+    def jsonrpc_getMblogComments(self, service, node):
+        """Get all comments of given node
+        @param service: jid of the service hosting the node
+        @param node: comments node
+        """
+        profile = ISATSession(self.session).profile
+        d = defer.Deferred()
+        self.sat_host.bridge.getGroupBlogComments(service, node, profile, callback=d.callback, errback=d.errback)
+        return d
+
+
     def jsonrpc_getPresenceStatus(self):
         """Get Presence information for connected contacts"""
         profile = ISATSession(self.session).profile
--- a/public/libervia.css	Sun Apr 07 22:33:55 2013 +0200
+++ b/public/libervia.css	Thu Jun 20 12:16:46 2013 +0200
@@ -717,6 +717,13 @@
     width: 100%;
 }
 
+.subPanel {
+}
+
+.subpanel .mb_entry {
+    padding-left: 65px;
+}
+
 .mb_entry {
 	min-height: 64px;
 }