# HG changeset patch # User Goffi # Date 1371723406 -7200 # Node ID 2bc6cf004e61749bc2e99068f578c79eba965850 # Parent aa76793da3530214be3c73e239f12905965ffe96 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 diff -r aa76793da353 -r 2bc6cf004e61 browser_side/panels.py --- 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("""
on
@@ -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 comment 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) diff -r aa76793da353 -r 2bc6cf004e61 libervia.py --- 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) diff -r aa76793da353 -r 2bc6cf004e61 libervia.tac --- 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 diff -r aa76793da353 -r 2bc6cf004e61 public/libervia.css --- 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; }