# HG changeset patch # User Goffi # Date 1439682672 -7200 # Node ID 3b91225b457aca821aa014bcfc6a2c206f5d241b # Parent b2465423c76e2f62bd82a9000735606cd0e87cc6 server + browser side: blogging refactoring (draft), huge commit sorry: /!\ everything is not working yet, group blog is not working for now - adaptation to backend changes - frontend commons part of blog have been moved to QuickFrontend - (editors) button "WYSIWYG edition" renamed to "preview" - (editors) Shift + [ENTER] is now used to send a text message, [ENTER] to finish a ligne normally - (editors) fixed modifiers handling - global simplification, resulting of the refactoring - with backend refactoring, we are now using PEP again, XEP-0277 compatibility is restored \o/ diff -r b2465423c76e -r 3b91225b457a src/browser/libervia_main.py --- a/src/browser/libervia_main.py Tue Jul 28 22:22:10 2015 +0200 +++ b/src/browser/libervia_main.py Sun Aug 16 01:51:12 2015 +0200 @@ -38,7 +38,7 @@ from sat.core.i18n import _ from pyjamas.ui.RootPanel import RootPanel -from pyjamas.ui.HTML import HTML +# from pyjamas.ui.HTML import HTML from pyjamas.ui.KeyboardListener import KEY_ESCAPE from pyjamas.Timer import Timer from pyjamas import Window, DOM @@ -69,7 +69,7 @@ unicode = str # FIXME: pyjamas workaround -MAX_MBLOG_CACHE = 500 # Max microblog entries kept in memories +# MAX_MBLOG_CACHE = 500 # Max microblog entries kept in memories # FIXME class SatWebFrontend(InputHistory, QuickApp): @@ -357,19 +357,25 @@ def profilePlugged(self, dummy): self._profile_plugged = True QuickApp.profilePlugged(self, C.PROF_KEY_NONE) + # XXX: as contact_list.update() is slow and it's called a lot of time + # during profile plugging, we prevent it before it's plugged + # and do all at once now + contact_list = self.contact_list + contact_list.update() - microblog_widget = self.displayWidget(blog.MicroblogPanel, ()) - self.setSelected(microblog_widget) + + blog_widget = self.displayWidget(blog.Blog, ()) + self.setSelected(blog_widget) # we fill the panels already here - for wid in self.widgets.getWidgets(blog.MicroblogPanel): - if wid.accept_all(): - self.bridge.getMassiveMblogs('ALL', (), None, profile=C.PROF_KEY_NONE, callback=wid.massiveInsert) - else: - self.bridge.getMassiveMblogs('GROUP', list(wid.accepted_groups), None, profile=C.PROF_KEY_NONE, callback=wid.massiveInsert) + # for wid in self.widgets.getWidgets(blog.MicroblogPanel): + # if wid.accept_all(): + # self.bridge.getMassiveMblogs('ALL', (), None, profile=C.PROF_KEY_NONE, callback=wid.massiveInsert) + # else: + # self.bridge.getMassiveMblogs('GROUP', list(wid.accepted_groups), None, profile=C.PROF_KEY_NONE, callback=wid.massiveInsert) #we ask for our own microblogs: - self.loadOurMainEntries() + # self.loadOurMainEntries() def gotDefaultMUC(default_muc): self.default_muc = default_muc @@ -438,6 +444,7 @@ ui = xmlui.create(self, xml_data=data['xmlui']) ui.show() elif "public_blog" in data: + # FIXME: remove this, this is not generic ! # TODO: use the bare instead of node when all blogs can be retrieved node = jid.JID(data['public_blog']).node # FIXME: "/blog/{}" won't work with unicode nodes @@ -461,126 +468,94 @@ data = {} self.bridge.launchAction(callback_id, data, profile=profile, callback=self._actionCb, errback=self._actionEb) - def _ownBlogsFills(self, mblogs, mblog_panel=None): - """Put our own microblogs in cache, then fill the panels with them. + # def _ownBlogsFills(self, mblogs, mblog_panel=None): + # """Put our own microblogs in cache, then fill the panels with them. - @param mblogs (dict): dictionary mapping a publisher JID to blogs data. - @param mblog_panel (MicroblogPanel): the panel to fill, or all if None. - """ - cache = [] - for publisher in mblogs: - for mblog in mblogs[publisher][0]: - if 'content' not in mblog: - log.warning(u"No content found in microblog [%s]" % mblog) - continue - if 'groups' in mblog: - _groups = set(mblog['groups'].split() if mblog['groups'] else []) - else: - _groups = None - mblog_entry = blog.MicroblogItem(mblog) - cache.append((_groups, mblog_entry)) + # @param mblogs (dict): dictionary mapping a publisher JID to blogs data. + # @param mblog_panel (MicroblogPanel): the panel to fill, or all if None. + # """ + # cache = [] + # for publisher in mblogs: + # for mblog in mblogs[publisher][0]: + # if 'content' not in mblog: + # log.warning(u"No content found in microblog [%s]" % mblog) + # continue + # if 'groups' in mblog: + # _groups = set(mblog['groups'].split() if mblog['groups'] else []) + # else: + # _groups = None + # mblog_entry = blog.MicroblogItem(mblog) + # cache.append((_groups, mblog_entry)) - self.mblog_cache.extend(cache) - if len(self.mblog_cache) > MAX_MBLOG_CACHE: - del self.mblog_cache[0:len(self.mblog_cache - MAX_MBLOG_CACHE)] + # self.mblog_cache.extend(cache) + # if len(self.mblog_cache) > MAX_MBLOG_CACHE: + # del self.mblog_cache[0:len(self.mblog_cache - MAX_MBLOG_CACHE)] - widget_list = [mblog_panel] if mblog_panel else self.widgets.getWidgets(blog.MicroblogPanel) + # widget_list = [mblog_panel] if mblog_panel else self.widgets.getWidgets(blog.MicroblogPanel) - for wid in widget_list: - self.fillMicroblogPanel(wid, cache) + # for wid in widget_list: + # self.fillMicroblogPanel(wid, cache) - # FIXME + # # FIXME - if self.initialised: - return - self.initialised = True # initialisation phase is finished here - for event_data in self.init_cache: # so we have to send all the cached events - self.personalEventHandler(*event_data) - del self.init_cache + # if self.initialised: + # return + # self.initialised = True # initialisation phase is finished here + # for event_data in self.init_cache: # so we have to send all the cached events + # self.personalEventHandler(*event_data) + # del self.init_cache - def loadOurMainEntries(self, index=0, mblog_panel=None): - """Load a page of our own blogs from the cache or ask them to the - backend. Then fill the panels with them. + # def loadOurMainEntries(self, index=0, mblog_panel=None): + # """Load a page of our own blogs from the cache or ask them to the + # backend. Then fill the panels with them. - @param index (int): starting index of the blog page to retrieve. - @param mblog_panel (MicroblogPanel): the panel to fill, or all if None. - """ - delta = index - self.next_rsm_index - if delta < 0: - assert mblog_panel is not None - self.fillMicroblogPanel(mblog_panel, self.mblog_cache[index:index + C.RSM_MAX_ITEMS]) - return + # @param index (int): starting index of the blog page to retrieve. + # @param mblog_panel (MicroblogPanel): the panel to fill, or all if None. + # """ + # delta = index - self.next_rsm_index + # if delta < 0: + # assert mblog_panel is not None + # self.fillMicroblogPanel(mblog_panel, self.mblog_cache[index:index + C.RSM_MAX_ITEMS]) + # return - def cb(result): - self._ownBlogsFills(result, mblog_panel) + # def cb(result): + # self._ownBlogsFills(result, mblog_panel) - rsm = {'max_': str(delta + C.RSM_MAX_ITEMS), 'index': str(self.next_rsm_index)} - self.bridge.getMassiveMblogs('JID', [unicode(self.whoami.bare)], rsm, callback=cb, profile=C.PROF_KEY_NONE) - self.next_rsm_index = index + C.RSM_MAX_ITEMS + # rsm = {'max_': str(delta + C.RSM_MAX_ITEMS), 'index': str(self.next_rsm_index)} + # self.bridge.getMassiveMblogs('JID', [unicode(self.whoami.bare)], rsm, callback=cb, profile=C.PROF_KEY_NONE) + # self.next_rsm_index = index + C.RSM_MAX_ITEMS ## Signals callbacks ## - def personalEventHandler(self, sender, event_type, data): - # FIXME: move some code from here to QuickApp - if not self.initialised: - self.init_cache.append((sender, event_type, data)) - return - sender = jid.JID(sender).bare - if event_type == "MICROBLOG": - if not 'content' in data: - log.warning("No content found in microblog data") - return - if 'groups' in data: - _groups = set(data['groups'].split() if data['groups'] else []) - else: - _groups = None - mblog_entry = blog.MicroblogItem(data) + # def personalEventHandler(self, sender, event_type, data): + # elif event_type == 'MICROBLOG_DELETE': + # for wid in self.widgets.getWidgets(blog.MicroblogPanel): + # wid.removeEntry(data['type'], data['id']) - for wid in self.widgets.getWidgets(blog.MicroblogPanel): - wid.addEntryIfAccepted(sender, _groups, mblog_entry) + # if sender == self.whoami.bare and data['type'] == 'main_item': + # for index in xrange(0, len(self.mblog_cache)): + # entry = self.mblog_cache[index] + # if entry[1].id == data['id']: + # self.mblog_cache.remove(entry) + # break + + # def fillMicroblogPanel(self, mblog_panel, mblogs): + # """Fill a microblog panel with entries in cache - if sender == self.whoami.bare: - found = False - for index in xrange(0, len(self.mblog_cache)): - entry = self.mblog_cache[index] - if entry[1].id == mblog_entry.id: - # replace existing entry - self.mblog_cache.remove(entry) - self.mblog_cache.insert(index, (_groups, mblog_entry)) - found = True - break - if not found: - self.mblog_cache.append((_groups, mblog_entry)) - if len(self.mblog_cache) > MAX_MBLOG_CACHE: - del self.mblog_cache[0:len(self.mblog_cache - MAX_MBLOG_CACHE)] - elif event_type == 'MICROBLOG_DELETE': - for wid in self.widgets.getWidgets(blog.MicroblogPanel): - wid.removeEntry(data['type'], data['id']) + # @param mblog_panel: MicroblogPanel instance + # """ + # #XXX: only our own entries are cached + # for cache_entry in mblogs: + # _groups, mblog_entry = cache_entry + # mblog_panel.addEntryIfAccepted(self.whoami.bare, *cache_entry) - if sender == self.whoami.bare and data['type'] == 'main_item': - for index in xrange(0, len(self.mblog_cache)): - entry = self.mblog_cache[index] - if entry[1].id == data['id']: - self.mblog_cache.remove(entry) - break - - def fillMicroblogPanel(self, mblog_panel, mblogs): - """Fill a microblog panel with entries in cache - - @param mblog_panel: MicroblogPanel instance - """ - #XXX: only our own entries are cached - for cache_entry in mblogs: - _groups, mblog_entry = cache_entry - mblog_panel.addEntryIfAccepted(self.whoami.bare, *cache_entry) - - def getEntityMBlog(self, entity): - # FIXME: call this after a contact has been added to roster - log.info(u"geting mblog for entity [%s]" % (entity,)) - for lib_wid in self.libervia_widgets: - if isinstance(lib_wid, blog.MicroblogPanel): - if lib_wid.isJidAccepted(entity): - self.bridge.call('getMassiveMblogs', lib_wid.massiveInsert, 'JID', [unicode(entity)]) + # def getEntityMBlog(self, entity): + # # FIXME: call this after a contact has been added to roster + # log.info(u"geting mblog for entity [%s]" % (entity,)) + # for lib_wid in self.libervia_widgets: + # if isinstance(lib_wid, blog.MicroblogPanel): + # if lib_wid.isJidAccepted(entity): + # self.bridge.call('getMassiveMblogs', lib_wid.massiveInsert, 'JID', [unicode(entity)]) def displayWidget(self, class_, target, dropped=False, new_tab=None, *args, **kwargs): """Get or create a LiberviaWidget and select it. When the user dropped diff -r b2465423c76e -r 3b91225b457a src/browser/sat_browser/blog.py --- a/src/browser/sat_browser/blog.py Tue Jul 28 22:22:10 2015 +0200 +++ b/src/browser/sat_browser/blog.py Sun Aug 16 01:51:12 2015 +0200 @@ -21,23 +21,24 @@ from sat.core.log import getLogger log = getLogger(__name__) -from sat.core.i18n import _, D_ +from sat.core.i18n import _ #, D_ from pyjamas.ui.SimplePanel import SimplePanel from pyjamas.ui.VerticalPanel import VerticalPanel +from pyjamas.ui.ScrollPanel import ScrollPanel from pyjamas.ui.HorizontalPanel import HorizontalPanel from pyjamas.ui.Label import Label from pyjamas.ui.HTML import HTML from pyjamas.ui.Image import Image from pyjamas.ui.ClickListener import ClickHandler from pyjamas.ui.FlowPanel import FlowPanel -from pyjamas.ui.KeyboardListener import KEY_ENTER, KeyboardHandler +from pyjamas.ui import KeyboardListener as keyb +from pyjamas.ui.KeyboardListener import KeyboardHandler from pyjamas.ui.FocusListener import FocusHandler from pyjamas.ui.MouseListener import MouseHandler from pyjamas.Timer import Timer from datetime import datetime -from time import time import html_tools import dialog @@ -46,50 +47,19 @@ import libervia_widget from constants import Const as C from sat_frontends.quick_frontend import quick_widgets -from sat_frontends.tools import jid - +from sat_frontends.quick_frontend import quick_blog unicode = str # XXX: pyjamas doesn't manage unicode +ENTRY_RICH = (C.ENTRY_MODE_RICH, C.ENTRY_MODE_XHTML) -class MicroblogItem(): - # XXX: should be moved in a separated module +class Entry(quick_blog.Entry, VerticalPanel, ClickHandler, FocusHandler, KeyboardHandler): + """Graphical representation of a quick_blog.Item""" - def __init__(self, data): - self.id = data['id'] - self.type = data.get('type', 'main_item') - self.empty = data.get('new', False) - self.title = data.get('title', '') - self.title_xhtml = data.get('title_xhtml', '') - self.content = data.get('content', '') - self.content_xhtml = data.get('content_xhtml', '') - self.author = jid.JID(data['author']) - self.updated = float(data.get('updated', 0)) # XXX: int doesn't work here - self.published = float(data.get('published', self.updated)) # XXX: int doesn't work here - self.service = data.get('service', '') - self.node = data.get('node', '') - self.comments = data.get('comments', False) - self.comments_service = data.get('comments_service', '') - self.comments_node = data.get('comments_node', '') - + def __init__(self, manager, item_data=None, comments_data=None, service=None, node=None): + quick_blog.Entry.__init__(self, manager, item_data, comments_data, service, node) -class MicroblogEntry(SimplePanel, ClickHandler, FocusHandler, KeyboardHandler): - - def __init__(self, blog_panel, data): - """ - @param blog_panel: the parent panel - @param data: dict containing the blog item data, or a MicroblogItem instance. - """ - self._base_item = data if isinstance(data, MicroblogItem) else MicroblogItem(data) - for attr in ['id', 'type', 'empty', 'title', 'title_xhtml', 'content', 'content_xhtml', - 'author', 'updated', 'published', 'comments', 'service', 'node', - 'comments_service', 'comments_node']: - getter = lambda attr: lambda inst: getattr(inst._base_item, attr) - setter = lambda attr: lambda inst, value: setattr(inst._base_item, attr, value) - setattr(MicroblogEntry, attr, property(getter(attr), setter(attr))) - - SimplePanel.__init__(self) - self._blog_panel = blog_panel + VerticalPanel.__init__(self) self.panel = FlowPanel() self.panel.setStyleName('mb_entry') @@ -103,8 +73,9 @@ entry_avatar = SimplePanel() entry_avatar.setStyleName('mb_entry_avatar') - assert isinstance(self.author, jid.JID) # FIXME: temporary - self.avatar = Image(self._blog_panel.host.getAvatarURL(self.author)) # FIXME: self.author should be initially a jid.JID + author_jid = self.author_jid + self.avatar = Image(self.blog.host.getAvatarURL(author_jid) if author_jid is not None else C.DEFAULT_AVATAR_URL) + # TODO: show a warning icon if author is not validated entry_avatar.add(self.avatar) self.panel.add(entry_avatar) @@ -112,76 +83,69 @@ self.entry_dialog.setStyleName('mb_entry_dialog') self.panel.add(self.entry_dialog) + self.comments_panel = None + self._current_comment = None + self.add(self.panel) ClickHandler.__init__(self) self.addClickListener(self) - self.__pub_data = (self.service, self.node, self.id) - self.__setContent() + self.refresh() + self.displayed = False # True when entry is added to parent + if comments_data: + self.addComments(comments_data) - def __setContent(self): - """Actually set the entry content (header, icons, bubble...)""" - self.delete_label = self.update_label = self.comment_label = None - self.bubble = self._current_comment = None - self.__setHeader() - self.__setBubble() - self.__setIcons() + def refresh(self): + self.header.clear() + self.entry_dialog.clear() + self.entry_actions.clear() + self._setHeader() + self._setBubble() + self._setIcons() - def __setHeader(self): + def _setHeader(self): """Set the entry header.""" - if self.empty: - return - update_text = u" — ✍ " + "%s" % datetime.fromtimestamp(self.updated) - self.header.add(HTML(""" - on - %(updated)s - """ % {'author': html_tools.html_sanitize(unicode(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. + if not self.new: + update_text = u" — ✍ " + "%s" % datetime.fromtimestamp(self.item.updated) + self.header.add(HTML(""" + on + %(updated)s + """ % {'author': html_tools.html_sanitize(unicode(self.item.author)), + 'published': datetime.fromtimestamp(self.item.published) if self.item.published is not None else '', + 'updated': update_text if self.item.published != self.item.updated else '' + })) + if self.item.comments: + self.show_comments_link = HTML('') + self.header.add(self.show_comments_link) - @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 + def _setBubble(self): + """Set the bubble displaying the initial content.""" + content = {'text': self.item.content_xhtml if self.item.content_xhtml else self.item.content or '', + 'title': self.item.title_xhtml if self.item.title_xhtml else self.item.title or ''} - if self.hidden_count > 0: - comments = D_('comments') if self.hidden_count > 1 else D_('comment') - text = D_("show %(count)d previous %(comments)s") % {'count': self.hidden_count, - 'comments': comments} - if self not in self.show_comments_link._clickListeners: - self.show_comments_link.addClickListener(self) + if self.mode == C.ENTRY_MODE_TEXT: + # assume raw text message have no title + self.bubble = editor_widget.LightTextEditor(content, modifiedCb=self._modifiedCb, afterEditCb=self._afterEditCb, options={'no_xhtml': True}) + elif self.mode in ENTRY_RICH: + content['syntax'] = C.SYNTAX_XHTML + if self.new: + options = [] + elif self.item.author_jid == self.blog.host.whoami.bare: + options = ['update_msg'] + else: + options = ['read_only'] + self.bubble = richtext.RichTextEditor(self.blog.host, content, modifiedCb=self._modifiedCb, afterEditCb=self._afterEditCb, options=options) 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 + log.error("Bad entry mode: %s" % self.mode) + self.bubble.addStyleName("bubble") + self._showSyntaxSwitchButton(False) + self.entry_dialog.add(self.bubble) + self.bubble.addEditListener(self._showWarning) # FIXME: remove edit listeners + self._setEditable() - self.show_comments_link.setHTML("""%(text)s""" % {'text': text}) - - def __setIcons(self): + def _setIcons(self): """Set the entry icons (delete, update, comment)""" - if self.empty: + if self.new: return def addIcon(label, title): @@ -191,269 +155,291 @@ self.entry_actions.add(label) return label - if self.comments: + if self.item.comments: self.comment_label = addIcon(u"↶", "Comment this message") self.comment_label.setStyleName('mb_entry_action_larger') - is_publisher = self.author == self._blog_panel.host.whoami.bare + is_publisher = self.item.author_jid == self.blog.host.whoami.bare if is_publisher: self.update_label = addIcon(u"✍", "Edit this message") - if is_publisher or unicode(self.node).endswith(unicode(self._blog_panel.host.whoami.bare)): + # TODO: add delete button if we are the owner of the node self.delete_label = addIcon(u"✗", "Delete this message") - def updateAvatar(self, new_avatar): - """Change the avatar of the entry - @param new_avatar: path to the new image""" - self.avatar.setUrl(new_avatar) - - def onClick(self, sender): - if sender == self: - self._blog_panel.setSelectedEntry(self) - elif sender == self.delete_label: - self._delete() - elif sender == self.update_label: - self.edit(True) - elif sender == self.comment_label: - self._comment() - elif sender == self.show_comments_link: - self._blog_panel.loadAllCommentsForEntry(self) + def _createCommentsPanel(self): + """Create the panel if it doesn't exists""" + if self.comments_panel is None: + self.comments_panel = VerticalPanel() + self.comments_panel.setStyleName('microblogPanel') + self.comments_panel.addStyleName('subPanel') + self.add(self.comments_panel) - def __modifiedCb(self, content): - """Send the new content to the backend - @return: False to restore the original content if a deletion has been cancelled - """ - if not content['text']: # previous content has been emptied - self._delete(True) - return False - extra = {'published': unicode(self.published)} - if isinstance(self.bubble, richtext.RichTextEditor): - # TODO: if the user change his parameters after the message edition started, - # the message syntax could be different then the current syntax: pass the - # message syntax in extra for the frontend to use it instead of current syntax. - extra.update({'content_rich': content['text'], 'title': content['title']}) - if self.empty: - if self.type == 'main_item': - self._blog_panel.host.bridge.call('sendMblog', None, None, tuple(self._blog_panel.accepted_groups), content['text'], extra) + def _setEditable(self): + self.bubble.edit(self.editable) + if self.editable: + self._showSyntaxSwitchButton() + + def setEditable(self, editable=True): + self.editable = editable + self._setEditable() + + def _showSyntaxSwitchButton(self, show=True): + if show: + if self.mode == C.ENTRY_MODE_TEXT: + html = 'rich text' else: - self._blog_panel.host.bridge.call('sendMblogComment', None, self._parent_entry.comments, content['text'], extra) + html = 'raw text' + self.toggle_syntax_button = HTML(html) + self.toggle_syntax_button.addClickListener(self.toggleContentSyntax) + self.toggle_syntax_button.addStyleName('mb_entry_toggle_syntax') + self.entry_dialog.add(self.toggle_syntax_button) + self.toggle_syntax_button.setStyleAttribute('top', '-20px') # XXX: need to force CSS + self.toggle_syntax_button.setStyleAttribute('left', '-20px') else: - self._blog_panel.host.bridge.call('updateMblog', None, self.__pub_data, self.comments, content['text'], extra) - return True + try: + self.toggle_syntax_button.removeFromParent() + except (AttributeError, TypeError): + pass - def __afterEditCb(self, content): - """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, update_header=False) - if self.type == 'main_item': # restore the "New message" button - self._blog_panel.addNewMessageEntry() - else: # allow to create a new comment - self._parent_entry._current_comment = None - self.entry_dialog.setWidth('auto') - try: - self.toggle_syntax_button.removeFromParent() - except (AttributeError, TypeError): - pass - def __setBubble(self, edit=False): - """Set the bubble displaying the initial content.""" - content = {'text': self.content_xhtml if self.content_xhtml else self.content, - 'title': self.title_xhtml if self.title_xhtml else self.title} - if self.content_xhtml: - content.update({'syntax': C.SYNTAX_XHTML}) - if self.author != self._blog_panel.host.whoami.bare: - options = ['read_only'] - else: - options = [] if self.empty else ['update_msg'] - self.bubble = richtext.RichTextEditor(self._blog_panel.host, content, self.__modifiedCb, self.__afterEditCb, options) - else: # assume raw text message have no title - self.bubble = editor_widget.LightTextEditor(content, self.__modifiedCb, self.__afterEditCb, options={'no_xhtml': True}) - self.bubble.addStyleName("bubble") + def edit(self, edit=True): + """Toggle the bubble between display and edit mode""" try: self.toggle_syntax_button.removeFromParent() except (AttributeError, TypeError): pass - self.entry_dialog.add(self.bubble) - self.edit(edit) - self.bubble.addEditListener(self.__showWarning) + self.bubble.edit(edit) + if edit: + if self.mode in ENTRY_RICH: + # image = 'A' + html = 'raw text' + # title = _('Switch to raw text edition') + else: + # image = '' + html = 'rich text' + # title = _('Switch to rich text edition') + self.toggle_syntax_button = HTML(html) + self.toggle_syntax_button.addClickListener(self.toggleContentSyntax) + self.toggle_syntax_button.addStyleName('mb_entry_toggle_syntax') + self.entry_dialog.add(self.toggle_syntax_button) + self.toggle_syntax_button.setStyleAttribute('top', '-20px') # XXX: need to force CSS + self.toggle_syntax_button.setStyleAttribute('left', '-20px') + + # def updateAvatar(self, new_avatar): + # """Change the avatar of the entry + # @param new_avatar: path to the new image""" + # self.avatar.setUrl(new_avatar) + + def onClick(self, sender): + if sender == self: + self.blog.setSelectedEntry(self) + elif sender == self.delete_label: + self._onRetractClick() + elif sender == self.update_label: + self.edit(True) + elif sender == self.comment_label: + self._onCommentClick() + # elif sender == self.show_comments_link: + # self._blog_panel.loadAllCommentsForEntry(self) + + def _modifiedCb(self, content): + """Send the new content to the backend + + @return: False to restore the original content if a deletion has been cancelled + """ + if not content['text']: # previous content has been emptied + return False + + self.item.content = self.item.content_rich = self.item.content_xhtml = None + self.item.title = self.item.title_rich = self.item.title_xhtml = None - def __showWarning(self, sender, keycode): - if keycode == KEY_ENTER: - self._blog_panel.host.showWarning(None, None) + if self.mode in ENTRY_RICH: + # TODO: if the user change his parameters after the message edition started, + # the message syntax could be different then the current syntax: pass the + # message syntax in mb_data for the frontend to use it instead of current syntax. + self.item.content_rich = content['text'] + self.item.title = content['title'] else: - self._blog_panel.host.showWarning(*self._blog_panel.getWarningData(self.type == 'comment')) + self.item.content = content['text'] + + self.send() + + return True + + def _afterEditCb(self, content): + """Post edition treatments - def _delete(self, empty=False): - """Ask confirmation for deletion. - @return: False if the deletion has been cancelled.""" + 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 + @param content(dict): edited content + """ + if self.new: + if self.level == 0: + # we have a main item, we keep the edit entry + self.reset(None) + # FIXME: would be better to reset bubble + # but bubble.setContent() doesn't seem to work + self.bubble.removeFromParent() + self._setBubble() + else: + # we don't keep edit entries for comments + self.delete() + else: + self._showSyntaxSwitchButton(False) + + def _showWarning(self, sender, keycode, modifiers): + if keycode == keyb.KEY_ENTER & keyb.MODIFIER_SHIFT: # FIXME: fix edit_listeners, it's dirty (we have to check keycode/modifiers twice !) + self.blog.host.showWarning(None, None) + else: + # self.blog.host.showWarning(*self.blog.getWarningData(self.type == 'comment')) + self.blog.host.showWarning(*self.blog.getWarningData(False)) # FIXME: comments are not yet reimplemented + + def _onRetractClick(self): + """Ask confirmation then retract current entry.""" + assert not self.new + def confirm_cb(answer): if answer: - self._blog_panel.host.bridge.call('deleteMblog', None, self.__pub_data, self.comments) - else: # restore the text if it has been emptied during the edition - self.bubble.setContent(self.bubble._original_content) + self.retract() - if self.empty: - text = _("New ") + (_("message") if self.comments else _("comment")) + _(" without body has been cancelled.") - dialog.InfoDialog(_("Information"), text).show() - return - text = "" - if empty: - text = (_("Message") if self.comments else _("Comment")) + _(" body has been emptied.
") - target = _('message and all its comments') if self.comments else _('comment') - text += _("Do you really want to delete this %s?") % target + entry_type = _("message") if self.level == 0 else _("comment") + and_comments = _(" All comments will be also deleted!") if self.item.comments else "" + text = _("Do you really want to delete this {entry_type}?{and_comments}").format( + entry_type=entry_type, and_comments=and_comments) dialog.ConfirmDialog(confirm_cb, text=text).show() - def _comment(self): + def _onCommentClick(self): """Add an empty entry for a new comment""" - if self._current_comment: - self._current_comment.bubble.setFocus(True) - self._blog_panel.setSelectedEntry(self._current_comment, True) - return - data = {'id': unicode(time()), - 'new': True, - 'type': 'comment', - 'author': unicode(self._blog_panel.host.whoami.bare), - 'service': self.comments_service, - 'node': self.comments_node - } - 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 - entry._parent_entry = self - self._current_comment = entry - self.edit(True, entry) - self._blog_panel.setSelectedEntry(entry, True) + if self._current_comment is None: + if not self.item.comments_service or not self.item.comments_node: + log.warning("Invalid service and node for comments, can pcreate a comment") + self._current_comment = self.addEntry(editable=True, service=self.item.comments_service, node=self.item.comments_node) + self.blog.setSelectedEntry(self._current_comment, True) - def edit(self, edit, entry=None): - """Toggle the bubble between display and edit mode - @edit: boolean value - @entry: MicroblogEntry instance, or None to use self - """ - if entry is None: - entry = self - try: - entry.toggle_syntax_button.removeFromParent() - except (AttributeError, TypeError): - pass - entry.bubble.edit(edit) - if edit: - if isinstance(entry.bubble, richtext.RichTextEditor): - image = 'A' - html = 'raw text' - title = _('Switch to raw text edition') - else: - image = '' - html = 'rich text' - title = _('Switch to rich text edition') - entry.toggle_syntax_button = HTML(html) - entry.toggle_syntax_button.addClickListener(entry.toggleContentSyntax) - entry.toggle_syntax_button.addStyleName('mb_entry_toggle_syntax') - entry.entry_dialog.add(entry.toggle_syntax_button) - entry.toggle_syntax_button.setStyleAttribute('top', '-20px') # XXX: need to force CSS - entry.toggle_syntax_button.setStyleAttribute('left', '-20px') + def _changeMode(self, original_content, text): + self.mode = C.ENTRY_MODE_RICH if self.mode == C.ENTRY_MODE_TEXT else C.ENTRY_MODE_TEXT + if self.mode in ENTRY_RICH and not text: + text = ' ' # something different than empty string is needed to initialize the rich text editor + self.item.content = text + self.content_title = self.content_title_xhtml = '' + self.bubble.removeFromParent() + self._setBubble() + self.bubble.setOriginalContent(original_content) + if self.mode in ENTRY_RICH: + self.item.content_xhtml = text + self.bubble.setDisplayContent() # needed in case the edition is aborted, to not end with an empty bubble + else: + self.item.content_xhtml = '' def toggleContentSyntax(self): """Toggle the editor between raw and rich text""" original_content = self.bubble.getOriginalContent() - rich = not isinstance(self.bubble, richtext.RichTextEditor) + rich = self.mode in ENTRY_RICH if rich: original_content['syntax'] = C.SYNTAX_XHTML - def setBubble(text): - self.content = text - self.content_xhtml = text if rich else '' - self.content_title = self.content_title_xhtml = '' - self.bubble.removeFromParent() - self.__setBubble(True) - self.bubble.setOriginalContent(original_content) + text = self.bubble.getContent()['text'] + + if not text.strip(): + self._changeMode(original_content,'') + else: if rich: - self.bubble.setDisplayContent() # needed in case the edition is aborted, to not end with an empty bubble + def confirm_cb(answer): + if answer: + self.blog.host.bridge.syntaxConvert(text, C.SYNTAX_CURRENT, C.SYNTAX_TEXT, profile=None, + callback=lambda converted: self._changeMode(original_content, converted)) + dialog.ConfirmDialog(confirm_cb, text=_("Do you really want to lose the title and text formatting?")).show() + else: + self.blog.host.bridge.syntaxConvert(text, C.SYNTAX_TEXT, C.SYNTAX_XHTML, profile=None, + callback=lambda converted: self._changeMode(original_content, converted)) - text = self.bubble.getContent()['text'] - if not text: - setBubble(' ') # something different than empty string is needed to initialize the rich text editor - return - if not rich: - def confirm_cb(answer): - if answer: - self._blog_panel.host.bridge.call('syntaxConvert', setBubble, text, C.SYNTAX_CURRENT, C.SYNTAX_TEXT) - dialog.ConfirmDialog(confirm_cb, text=_("Do you really want to lose the title and text formatting?")).show() - else: - self._blog_panel.host.bridge.call('syntaxConvert', setBubble, text, C.SYNTAX_TEXT, C.SYNTAX_XHTML) + def update(self, entry=None): + """Update comments""" + self._createCommentsPanel() + self.entries.sort(key=lambda entry: entry.item.published, reverse=True) + idx = 0 + for entry in self.entries: + if not entry.displayed: + self.comments_panel.insert(entry, idx) + entry.displayed = True + idx += 1 + + def delete(self): + quick_blog.Entry.delete(self) + + # _current comment is specific to libervia, we remove it + if isinstance(self.manager, Entry): + self.manager._current_comment = None + + # now we remove the pyjamas widgets + parent = self.parent + assert isinstance(parent, VerticalPanel) + self.removeFromParent() + if not parent.children: + # the vpanel is empty, we remove it + parent.removeFromParent() + try: + if self.manager.comments_panel == parent: + self.manager.comments_panel = None + except AttributeError: + assert isinstance(self.manager, quick_blog.QuickBlog) -class MicroblogPanel(quick_widgets.QuickWidget, libervia_widget.LiberviaWidget, MouseHandler): +class Blog(quick_blog.QuickBlog, libervia_widget.LiberviaWidget, MouseHandler): + """Panel used to show microblog""" warning_msg_public = "This message will be PUBLIC 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 following groups: %s" - # FIXME: all the generic parts must be moved to quick_frontends def __init__(self, host, targets, profiles=None): - """Panel used to show microblog - - @param targets (tuple(unicode)): contact groups displayed in this panel. - If empty, show all microblogs from all contacts. - """ - # do not mix self.targets (set of tuple of unicode) and self.accepted_groups (set of unicode) - quick_widgets.QuickWidget.__init__(self, host, targets, C.PROF_KEY_NONE) - libervia_widget.LiberviaWidget.__init__(self, host, ", ".join(self.accepted_groups), selectable=True) + quick_blog.QuickBlog.__init__(self, host, targets, C.PROF_KEY_NONE) + title = ", ".join(targets) if targets else "Blog" + libervia_widget.LiberviaWidget.__init__(self, host, title, selectable=True) MouseHandler.__init__(self) - self.entries = {} - self.comments = {} self.vpanel = VerticalPanel() self.vpanel.setStyleName('microblogPanel') self.setWidget(self.vpanel) - self.addNewMessageEntry() + self.addEntry(editable=True, first=True) + + self.getAll() - 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 - - # FIXME: workaround for a pyjamas issue: calling hash on a class method always return a different value if that method is defined directly within the class (with the "def" keyword) - self.avatarListener = self.onAvatarUpdate - host.addListener('avatar', self.avatarListener, [C.PROF_KEY_NONE]) + # 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 __str__(self): - return u"Blog Widget [target: {}, profile: {}]".format(', '.join(self.accepted_groups), self.profile) + return u"Blog Widget [targets: {}, profile: {}]".format(", ".join(self.targets) if self.targets else "meta blog", self.profile) - def onDelete(self): - quick_widgets.QuickWidget.onDelete(self) - self.host.removeListener('avatar', self.avatarListener) + def update(self): + self.entries.sort(key=lambda entry: entry.item.published, reverse=True) - def onAvatarUpdate(self, jid_, hash_, profile): - """Called on avatar update events - - @param jid_: jid of the entity with updated avatar - @param hash_: hash of the avatar - @param profile: %(doc_profile)s - """ - whoami = self.host.profiles[self.profile].whoami - if self.isJidAccepted(jid_) or jid_.bare == whoami.bare: - self.updateValue('avatar', jid_, hash_) + idx = 0 + if self._first_entry is not None: + idx += 1 + if not self._first_entry.displayed: + self.vpanel.insert(self._first_entry, 0) + self._first_entry.displayed = True - def addNewMessageEntry(self): - """Add an empty entry for writing a new message if needed.""" - if self.getNewMainEntry(): - return # there's already one - data = {'id': unicode(time()), - 'new': True, - 'author': unicode(self.host.whoami.bare), - } - entry = self.addEntry(data, update_header=False) - entry.edit(True) + for entry in self.entries: + if not entry.displayed: + self.vpanel.insert(entry, idx) + entry.displayed = True + idx += 1 + + # def onDelete(self): + # quick_widgets.QuickWidget.onDelete(self) + # self.host.removeListener('avatar', self.avatarListener) - def getNewMainEntry(self): - """Get the new entry being edited, or None if it doesn't exists. + # def onAvatarUpdate(self, jid_, hash_, profile): + # """Called on avatar update events - @return (MicroblogEntry): the new entry being edited. - """ - 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 + # @param jid_: jid of the entity with updated avatar + # @param hash_: hash of the avatar + # @param profile: %(doc_profile)s + # """ + # whoami = self.host.profiles[self.profile].whoami + # if self.isJidAccepted(jid_) or jid_.bare == whoami.bare: + # self.updateValue('avatar', jid_, hash_) @staticmethod def onGroupDrop(host, targets): @@ -464,35 +450,13 @@ @return: the created MicroblogPanel """ # XXX: pyjamas doesn't support use of cls directly - widget = host.displayWidget(MicroblogPanel, targets, dropped=True) - widget.loadMoreMainEntries() + widget = host.displayWidget(Blog, targets, dropped=True) return widget - @property - def accepted_groups(self): - """Return a set of the accepted groups""" - return set().union(*self.targets) - - 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.getMassiveMblogs(type_, list(self.accepted_groups), rsm, profile=C.PROF_KEY_NONE, callback=self.massiveInsert) + # @property + # def accepted_groups(self): + # """Return a set of the accepted groups""" + # return set().union(*self.targets) def getWarningData(self, comment): """ @@ -500,212 +464,29 @@ @return: a couple (type, msg) for calling self.host.showWarning""" if comment: return ("PUBLIC", "This is a comment and keep the initial post visibility, so it is potentialy public") - elif not self.accepted_groups: + elif self._targets_type == C.ALL: # we have a meta MicroblogPanel, we publish publicly return ("PUBLIC", self.warning_msg_public) else: # FIXME: manage several groups - return ("GROUP", self.warning_msg_group % ' '.join(self.accepted_groups)) - - def onTextEntered(self, text): - if not self.accepted_groups: - # we are entering a public microblog - self.bridge.call("sendMblog", None, "PUBLIC", (), text, {}) - else: - self.bridge.call("sendMblog", None, "GROUP", tuple(self.accepted_groups), text, {}) - - def accept_all(self): - return not self.accepted_groups # we accept every microblog only if we are not filtering by groups - - 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 (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_pub = len(mblogs) - count_msg = sum([len(value) for value in mblogs.values()]) - log.debug(u"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(u"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 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 "content" not in mblog: - log.warning(u"No content found in microblog [%s]" % mblog) - continue - 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()] - try: - count = int(rsm['count']) - hidden = count - (int(rsm['index']) + len(mblogs)) - main_entry.updateHeader(count, hidden) - except KeyError: # target pubsub server doesn't support RSM - pass - - 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.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[0:-1]: # ignore the footer - if not isinstance(child, MicroblogEntry): - idx += 1 - continue - 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 addEntryIfAccepted(self, sender, groups, mblog_entry): - """Check if an entry can go in MicroblogPanel and add to it - - @param sender(jid.JID): jid of the entry sender - @param groups: groups which can receive this entry - @param mblog_entry: panels.MicroblogItem instance - """ - assert isinstance(sender, jid.JID) # FIXME temporary - if (mblog_entry.type == "comment" - or self.isJidAccepted(sender) - or (groups is None and sender == self.host.profiles[self.profile].whoami.bare) - or (groups and groups.intersection(self.accepted_groups))): - self.addEntry(mblog_entry) - - def addEntry(self, data, update_header=True): - """Add an entry to the panel - - @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 comments_hash not in self.comments: - # The comments node is not known in this panel - return None - parent = self.comments[comments_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) - - for idx in xrange(0, len(sub_panel.getChildren())): - comment = sub_panel.getIndexedChild(idx) - if comment.id == _entry.id: - # update an existing comment - sub_panel.remove(comment) - sub_panel.insert(_entry, idx) - 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.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_, update_header=True): - """Remove an entry from the panel - - @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': - if child.id == id_: - main_idx = self.vpanel.getWidgetIndex(child) - try: - sub_panel = self.vpanel.getWidget(main_idx + 1) - if isinstance(sub_panel, VerticalPanel): - sub_panel.removeFromParent() - except IndexError: - pass - child.removeFromParent() - break - 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() - break + return (self._targets_type, self.warning_msg_group % ' '.join(self.targets)) def ensureVisible(self, entry): """Scroll to an entry to ensure its visibility @param entry (MicroblogEntry): the entry """ - try: - self.vpanel.getParent().ensureVisible(entry) # scroll to the clicked entry - except AttributeError: - log.warning("FIXME: MicroblogPanel.vpanel should be wrapped in a ScrollPanel!") + current = entry + while True: + parent = current.getParent() + if parent is None: + log.warning("Can't find any parent ScrollPanel") + return + elif isinstance(parent, ScrollPanel): + parent.ensureVisible(current) + return + else: + current = parent def setSelectedEntry(self, entry, ensure_visible=False): """Select an entry. @@ -720,55 +501,35 @@ clicked_entry = entry # entry may be None when the timer is done Timer(500, lambda timer: clicked_entry.removeStyleName('selected_entry')) - def updateValue(self, type_, jid_, value): - """Update a jid value in entries - - @param type_: one of 'avatar', 'nick' - @param jid_(jid.JID): jid concerned - @param value: new value""" - assert isinstance(jid_, jid.JID) # FIXME: temporary - def updateVPanel(vpanel): - avatar_url = self.host.getAvatarURL(jid_) - for child in vpanel.children: - if isinstance(child, MicroblogEntry) and child.author == jid_: - child.updateAvatar(avatar_url) - elif isinstance(child, VerticalPanel): - updateVPanel(child) - if type_ == 'avatar': - updateVPanel(self.vpanel) - - def addAcceptedGroups(self, groups): - """Add one or more group(s) which can be displayed in this panel. + # def updateValue(self, type_, jid_, value): + # """Update a jid value in entries - @param groups (tuple(unicode)): tuple of groups to add - """ - # FIXME: update the widget's hash in QuickApp._widgets[MicroblogPanel] - self.targets.update(groups) - - def isJidAccepted(self, jid_): - """Tell if a jid is actepted and must be shown in this panel + # @param type_: one of 'avatar', 'nick' + # @param jid_(jid.JID): jid concerned + # @param value: new value""" + # assert isinstance(jid_, jid.JID) # FIXME: temporary + # def updateVPanel(vpanel): + # avatar_url = self.host.getAvatarURL(jid_) + # for child in vpanel.children: + # if isinstance(child, MicroblogEntry) and child.author == jid_: + # child.updateAvatar(avatar_url) + # elif isinstance(child, VerticalPanel): + # updateVPanel(child) + # if type_ == 'avatar': + # updateVPanel(self.vpanel) - @param jid_(jid.JID): jid to check - @return: True if the jid is accepted - """ - assert isinstance(jid_, jid.JID) # FIXME temporary - if self.accept_all(): - return True - for group in self.accepted_groups: - if self.host.contact_lists[self.profile].isEntityInGroup(jid_, group): - return True - return False + # def onClick(self, sender): + # if sender == self.footer: + # self.loadMoreMainEntries() - def onClick(self, sender): - if sender == self.footer: - self.loadMoreMainEntries() - - def onMouseEnter(self, sender): - if sender == self.footer: - self.loadMoreMainEntries() + # def onMouseEnter(self, sender): + # if sender == self.footer: + # self.loadMoreMainEntries() -libervia_widget.LiberviaWidget.addDropKey("GROUP", lambda host, item: MicroblogPanel.onGroupDrop(host, (item,))) - -# Needed for the drop keys to not be mixed between meta panel and panel for "Contacts" group -libervia_widget.LiberviaWidget.addDropKey("CONTACT_TITLE", lambda host, item: MicroblogPanel.onGroupDrop(host, ())) +# libervia_widget.LiberviaWidget.addDropKey("GROUP", lambda host, item: MicroblogPanel.onGroupDrop(host, (item,))) +# +# # Needed for the drop keys to not be mixed between meta panel and panel for "Contacts" group +libervia_widget.LiberviaWidget.addDropKey("CONTACT_TITLE", lambda host, item: Blog.onGroupDrop(host, ())) +quick_blog.registerClass("ENTRY", Entry) +quick_widgets.register(quick_blog.QuickBlog, Blog) diff -r b2465423c76e -r 3b91225b457a src/browser/sat_browser/contact_list.py --- a/src/browser/sat_browser/contact_list.py Tue Jul 28 22:22:10 2015 +0200 +++ b/src/browser/sat_browser/contact_list.py Sun Aug 16 01:51:12 2015 +0200 @@ -155,6 +155,10 @@ self.host.removeListener('avatar', self.avatarListener) def update(self): + # XXX: as update is slow, we avoid many updates on profile plugs + # and do them all at once at the end + if not self.host._profile_plugged: + return ### GROUPS ### _keys = self._groups.keys() try: diff -r b2465423c76e -r 3b91225b457a src/browser/sat_browser/editor_widget.py --- a/src/browser/sat_browser/editor_widget.py Tue Jul 28 22:22:10 2015 +0200 +++ b/src/browser/sat_browser/editor_widget.py Sun Aug 16 01:51:12 2015 +0200 @@ -24,7 +24,7 @@ from pyjamas.ui.HTML import HTML from pyjamas.ui.SimplePanel import SimplePanel from pyjamas.ui.TextArea import TextArea -from pyjamas.ui.KeyboardListener import KEY_ENTER, KEY_SHIFT, KEY_UP, KEY_DOWN, KeyboardHandler +from pyjamas.ui import KeyboardListener as keyb from pyjamas.ui.FocusListener import FocusHandler from pyjamas.ui.ClickListener import ClickHandler from pyjamas.ui.MouseListener import MouseHandler @@ -60,15 +60,15 @@ self.setText(text) Timer(5, lambda timer: self.setCursorPos(len(text))) - if keycode == KEY_ENTER: + if keycode == keyb.KEY_ENTER: if _txt: self.host.selected_widget.onTextEntered(_txt) self.host._updateInputHistory(_txt) # FIXME: why using a global variable ? self.setText('') sender.cancelKey() - elif keycode == KEY_UP: + elif keycode == keyb.KEY_UP: self.host._updateInputHistory(_txt, -1, history_cb) - elif keycode == KEY_DOWN: + elif keycode == keyb.KEY_DOWN: self.host._updateInputHistory(_txt, +1, history_cb) else: self._onComposing() @@ -99,15 +99,15 @@ @param content: dict with at least a 'text' key @param strproc: method to be applied on strings to clean the content @param modifiedCb: method to be called when the text has been modified. - If this method returns: - - True: the modification will be saved and afterEditCb called; - - False: the modification won't be saved and afterEditCb called; - - None: the modification won't be saved and afterEditCb not called. + This method can return: + - True: the modification will be saved and afterEditCb called; + - False: the modification won't be saved and afterEditCb called; + - None: the modification won't be saved and afterEditCb not called. @param afterEditCb: method to be called when the edition is done """ if content is None: content = {'text': ''} - assert('text' in content) + assert 'text' in content if strproc is None: def strproc(text): try: @@ -115,21 +115,23 @@ except (TypeError, AttributeError): return text self.strproc = strproc - self.__modifiedCb = modifiedCb + self._modifiedCb = modifiedCb self._afterEditCb = afterEditCb self.initialized = False self.edit_listeners = [] self.setContent(content) def setContent(self, content=None): - """Set the editable content. The displayed content, which is set from the child class, could differ. - @param content: dict with at least a 'text' key + """Set the editable content. + The displayed content, which is set from the child class, could differ. + + @param content (dict): content data, need at least a 'text' key """ if content is None: content = {'text': ''} elif not isinstance(content, dict): content = {'text': content} - assert('text' in content) + assert 'text' in content self._original_content = {} for key in content: self._original_content[key] = self.strproc(content[key]) @@ -150,7 +152,7 @@ def getOriginalContent(self): """ - @return the original content before modification (dict) + @return (dict): the original content before modification (i.e. content given in __init__) """ return self._original_content @@ -193,8 +195,8 @@ if abort: self._afterEditCb(content) return - if self.__modifiedCb and self.modified(content): - result = self.__modifiedCb(content) # e.g.: send a message or update something + if self._modifiedCb and self.modified(content): + result = self._modifiedCb(content) # e.g.: send a message or update something if result is not None: if self._afterEditCb: self._afterEditCb(content) # e.g.: restore the display mode @@ -220,7 +222,7 @@ self.edit_listeners.append(listener) -class SimpleTextEditor(BaseTextEditor, FocusHandler, KeyboardHandler, ClickHandler): +class SimpleTextEditor(BaseTextEditor, FocusHandler, keyb.KeyboardHandler, ClickHandler): """Base class for manage a simple text editor.""" def __init__(self, content=None, modifiedCb=None, afterEditCb=None, options=None): @@ -228,12 +230,12 @@ @param content @param modifiedCb @param afterEditCb - @param options: dict with the following value: - - no_xhtml: set to True to clean any xhtml content. - - enhance_display: if True, the display text will be enhanced with strings.addURLToText - - listen_keyboard: set to True to terminate the edition with or . - - listen_focus: set to True to terminate the edition when the focus is lost. - - listen_click: set to True to start the edition when you click on the widget. + @param options (dict): can have the following value: + - no_xhtml: set to True to clean any xhtml content. + - enhance_display: if True, the display text will be enhanced with strings.addURLToText + - listen_keyboard: set to True to terminate the edition with or . + - listen_focus: set to True to terminate the edition when the focus is lost. + - listen_click: set to True to start the edition when you click on the widget. """ self.options = {'no_xhtml': False, 'enhance_display': True, @@ -243,12 +245,11 @@ } if options: self.options.update(options) - self.__shift_down = False if self.options['listen_focus']: FocusHandler.__init__(self) if self.options['listen_click']: ClickHandler.__init__(self) - KeyboardHandler.__init__(self) + keyb.KeyboardHandler.__init__(self) strproc = lambda text: html_tools.html_sanitize(html_tools.html_strip(text)) if self.options['no_xhtml'] else html_tools.html_strip(text) BaseTextEditor.__init__(self, content, strproc, modifiedCb, afterEditCb) self.textarea = self.display = None @@ -295,21 +296,14 @@ def onKeyDown(self, sender, keycode, modifiers): for listener in self.edit_listeners: - listener(self.textarea, keycode) + listener(self.textarea, keycode, modifiers) # FIXME: edit_listeners must either be removed, or send an action instead of keycode/modifiers if not self.options['listen_keyboard']: return - if keycode == KEY_SHIFT or self.__shift_down: # allow input a new line with + - self.__shift_down = True - return - if keycode == KEY_ENTER: # finish the edition + if keycode == keyb.KEY_ENTER and modifiers & keyb.MODIFIER_SHIFT: self.textarea.setFocus(False) if not self.options['listen_focus']: self.edit(False) - def onKeyUp(self, sender, keycode, modifiers): - if keycode == KEY_SHIFT: - self.__shift_down = False - def onLostFocus(self, sender): """Finish the edition when focus is lost""" if self.options['listen_focus']: @@ -325,10 +319,10 @@ FocusHandler.onBrowserEvent(self, event) if self.options['listen_click']: ClickHandler.onBrowserEvent(self, event) - KeyboardHandler.onBrowserEvent(self, event) + keyb.KeyboardHandler.onBrowserEvent(self, event) -class HTMLTextEditor(SimpleTextEditor, HTML, FocusHandler, KeyboardHandler): +class HTMLTextEditor(SimpleTextEditor, HTML, FocusHandler, keyb.KeyboardHandler): """Manage a simple text editor with the HTML 5 "contenteditable" property.""" def __init__(self, content=None, modifiedCb=None, afterEditCb=None, options=None): @@ -353,7 +347,7 @@ self.getElement().blur() -class LightTextEditor(SimpleTextEditor, SimplePanel, FocusHandler, KeyboardHandler): +class LightTextEditor(SimpleTextEditor, SimplePanel, FocusHandler, keyb.KeyboardHandler): """Manage a simple text editor with a TextArea for editing, HTML for display.""" def __init__(self, content=None, modifiedCb=None, afterEditCb=None, options=None): diff -r b2465423c76e -r 3b91225b457a src/browser/sat_browser/json.py --- a/src/browser/sat_browser/json.py Tue Jul 28 22:22:10 2015 +0200 +++ b/src/browser/sat_browser/json.py Sun Aug 16 01:51:12 2015 +0200 @@ -170,11 +170,13 @@ class BridgeCall(LiberviaJsonProxy): def __init__(self): LiberviaJsonProxy.__init__(self, "/json_api", - ["getContacts", "addContact", "sendMessage", "sendMblog", "sendMblogComment", - "getMblogs", "getMassiveMblogs", "getMblogComments", + ["getContacts", "addContact", "sendMessage", + "psDeleteNode", "psRetractItem", "psRetractItems", + "mbSend", "mbRetract", "mbGetLast", "mbGetFromMany", "mbGetFromManyRTResult", + "mbGetFromManyWithComments", "mbGetFromManyWithCommentsRTResult", "getHistory", "getPresenceStatuses", "joinMUC", "mucLeave", "getRoomsJoined", "getRoomsSubjects", "inviteMUC", "launchTarotGame", "getTarotCardsPaths", "tarotGameReady", - "tarotGamePlayCards", "launchRadioCollective", "getMblogs", "getMblogsWithComments", + "tarotGamePlayCards", "launchRadioCollective", "getWaitingSub", "subscription", "delContact", "updateContact", "getCard", "getEntityData", "getParamsUI", "asyncGetParamA", "setParam", "launchAction", "disconnect", "chatStateComposing", "getNewAccountDomain", "confirmationAnswer", diff -r b2465423c76e -r 3b91225b457a src/browser/sat_browser/richtext.py --- a/src/browser/sat_browser/richtext.py Tue Jul 28 22:22:10 2015 +0200 +++ b/src/browser/sat_browser/richtext.py Sun Aug 16 01:51:12 2015 +0200 @@ -19,6 +19,8 @@ from sat_frontends.tools import composition from sat.core.i18n import _ +from sat.core.log import getLogger +log = getLogger(__name__) from pyjamas.ui.TextArea import TextArea from pyjamas.ui.Button import Button @@ -157,7 +159,7 @@ self.toolbar.addStyleName(self.style['toolbar']) for key in composition.RICH_SYNTAXES[syntax].keys(): self.addToolbarButton(syntax, key) - self.wysiwyg_button = CheckBox(_('WYSIWYG edition')) + self.wysiwyg_button = CheckBox(_('preview')) wysiywgCb = lambda sender: self.setWysiwyg(sender.getChecked()) self.wysiwyg_button.addClickListener(wysiywgCb) self.toolbar.add(self.wysiwyg_button) @@ -436,6 +438,7 @@ def __syncToUniBox(self, recipients=None, emptyText=False): """Synchronize to unibox if a maximum of one recipient is set. @return True if the sync could be done, False otherwise""" + # FIXME: To be removed (unibox doesn't exist anymore) if not self.host.uni_box: return setText = lambda: self.host.uni_box.setText("" if emptyText else self.getContent()['text']) @@ -536,4 +539,4 @@ def onKeyDown(self, sender=None, keycode=None, modifiers=None): for listener in self._parent.edit_listeners: - listener(self, keycode) + listener(self, keycode, modifiers) # FIXME: edit_listeners must either be removed, or send an action instead of keycode/modifiers diff -r b2465423c76e -r 3b91225b457a src/server/server.py --- a/src/server/server.py Tue Jul 28 22:22:10 2015 +0200 +++ b/src/server/server.py Sun Aug 16 01:51:12 2015 +0200 @@ -150,7 +150,8 @@ self.sat_host = sat_host def asyncBridgeCall(self, method_name, *args, **kwargs): - """Call an asynchrone bridge method and return a deferred + """Call an asynchronous bridge method and return a deferred + @param method_name: name of the method as a unicode @return: a deferred which trigger the result @@ -258,108 +259,220 @@ profile = ISATSession(self.session).profile return self.asyncBridgeCall("sendMessage", to_jid, msg, subject, type_, options, profile) - def jsonrpc_sendMblog(self, type_, dest, text, extra={}): - """ Send microblog message - @param type_ (unicode): one of "PUBLIC", "GROUP" - @param dest (tuple(unicode)): recipient groups (ignored for "PUBLIC") - @param text (unicode): microblog's text - """ - profile = ISATSession(self.session).profile - extra['allow_comments'] = 'True' - - if not type_: # auto-detect - type_ = "PUBLIC" if dest == [] else "GROUP" + ## PubSub ## - if type_ in ("PUBLIC", "GROUP") and text: - if type_ == "PUBLIC": - #This text if for the public microblog - log.debug("sending public blog") - return self.sat_host.bridge.sendGroupBlog("PUBLIC", (), text, extra, profile) - else: - log.debug("sending group blog") - dest = dest if isinstance(dest, list) else [dest] - return self.sat_host.bridge.sendGroupBlog("GROUP", dest, text, extra, profile) - else: - raise Exception("Invalid data") + def jsonrpc_psDeleteNode(self, service, node): + """Delete a whole node - def jsonrpc_deleteMblog(self, pub_data, comments): - """Delete a microblog node - @param pub_data: a tuple (service, comment node identifier, item identifier) - @param comments: comments node identifier (for main item) or False + @param service (unicode): service jid + @param node (unicode): node to delete """ profile = ISATSession(self.session).profile - return self.sat_host.bridge.deleteGroupBlog(pub_data, comments if comments else '', profile) + return self.asyncBridgeCall("psDeleteNode", service, node, profile) + + # def jsonrpc_psRetractItem(self, service, node, item, notify): + # """Delete a whole node + + # @param service (unicode): service jid + # @param node (unicode): node to delete + # @param items (iterable): id of item to retract + # @param notify (bool): True if notification is required + # """ + # profile = ISATSession(self.session).profile + # return self.asyncBridgeCall("psRetractItem", service, node, item, notify, profile) + + # def jsonrpc_psRetractItems(self, service, node, items, notify): + # """Delete a whole node - def jsonrpc_updateMblog(self, pub_data, comments, message, extra={}): - """Modify a microblog node - @param pub_data: a tuple (service, comment node identifier, item identifier) - @param comments: comments node identifier (for main item) or False - @param message: new message - @param extra: dict which option name as key, which can be: - - allow_comments: True to accept an other level of comments, False else (default: False) - - rich: if present, contain rich text in currently selected syntax - """ - profile = ISATSession(self.session).profile - if comments: - extra['allow_comments'] = 'True' - return self.sat_host.bridge.updateGroupBlog(pub_data, comments if comments else '', message, extra, profile) + # @param service (unicode): service jid + # @param node (unicode): node to delete + # @param items (iterable): ids of items to retract + # @param notify (bool): True if notification is required + # """ + # profile = ISATSession(self.session).profile + # return self.asyncBridgeCall("psRetractItems", service, node, items, notify, profile) - def jsonrpc_sendMblogComment(self, node, text, extra={}): - """ Send microblog message - @param node: url of the comments node - @param text: comment + ## microblogging ## + + def jsonrpc_mbSend(self, service, node, mb_data): + """Send microblog data + + @param service (unicode): service jid or empty string to use profile's microblog + @param node (unicode): publishing node, or empty string to use microblog node + @param mb_data(dict): microblog data + @return: a deferred """ profile = ISATSession(self.session).profile - if node and text: - return self.sat_host.bridge.sendGroupBlogComment(node, text, extra, profile) - else: - raise Exception("Invalid data") + return self.asyncBridgeCall("mbSend", service, node, mb_data, profile) + + def jsonrpc_mbRetract(self, service, node, items): + """Delete a whole node - def jsonrpc_getMblogs(self, publisher_jid, item_ids, max_items=C.RSM_MAX_ITEMS): - """Get specified microblogs posted by a contact - @param publisher_jid: jid of the publisher - @param item_ids: list of microblogs items IDs - @return list of microblog data (dict)""" + @param service (unicode): service jid, empty string for PEP + @param node (unicode): node to delete, empty string for default node + @param items (iterable): ids of items to retract + """ profile = ISATSession(self.session).profile - d = self.asyncBridgeCall("getGroupBlogs", publisher_jid, item_ids, {'max_': unicode(max_items)}, False, profile) - return d + return self.asyncBridgeCall("mbRetract", service, node, items, profile) + + def jsonrpc_mbGetLast(self, service_jid, node, max_items, extra): + """Get last microblogs from publisher_jid - def jsonrpc_getMblogsWithComments(self, publisher_jid, item_ids, max_comments=C.RSM_MAX_COMMENTS): - """Get specified microblogs posted by a contact and their comments - @param publisher_jid: jid of the publisher - @param item_ids: list of microblogs items IDs - @return list of couple (microblog data, list of microblog data)""" + @param service_jid (unicode): pubsub service, usually publisher jid + @param node(unicode): mblogs node, or empty string to get the defaut one + @param max_items (int): maximum number of item to get or C.NO_LIMIT to get everything + @param rsm (dict): TODO + @return: a deferred couple with the list of items and metadatas. + """ profile = ISATSession(self.session).profile - d = self.asyncBridgeCall("getGroupBlogsWithComments", publisher_jid, item_ids, {}, max_comments, profile) - return d + return self.asyncBridgeCall("mbGetLast", service_jid, node, max_items, extra, profile) - def jsonrpc_getMassiveMblogs(self, publishers_type, publishers, rsm=None): - """Get lasts microblogs posted by several contacts at once + def jsonrpc_mbGetFromMany(self, publishers_type, publishers, max_items, extra): + """Get many blog nodes at once @param publishers_type (unicode): one of "ALL", "GROUP", "JID" @param publishers (tuple(unicode)): tuple of publishers (empty list for all, list of groups or list of jids) - @param rsm (dict): TODO - @return: dict{unicode: list[dict]) - key: publisher's jid - value: list of microblog data (dict) + @param max_items (int): maximum number of item to get or C.NO_LIMIT to get everything + @param extra (dict): TODO + @return (str): RT Deferred session id + """ + profile = ISATSession(self.session).profile + return self.sat_host.bridge.mbGetFromMany(publishers_type, publishers, max_items, extra, profile) + + def jsonrpc_mbGetFromManyRTResult(self, rt_session): + """Get results from RealTime mbGetFromMany session + + @param rt_session (str): RT Deferred session id + """ + profile = ISATSession(self.session).profile + return self.asyncBridgeCall("mbGetFromManyRTResult", rt_session, profile) + + def jsonrpc_mbGetFromManyWithComments(self, publishers_type, publishers, max_items, max_comments, rsm_dict, rsm_comments_dict): + """Helper method to get the microblogs and their comments in one shot + + @param publishers_type (str): type of the list of publishers (one of "GROUP" or "JID" or "ALL") + @param publishers (list): list of publishers, according to publishers_type (list of groups or list of jids) + @param max_items (int): optional limit on the number of retrieved items. + @param max_comments (int): maximum number of comments to retrieve + @param rsm_dict (dict): RSM data for initial items only + @param rsm_comments_dict (dict): RSM data for comments only + @param profile_key: profile key + @return (str): RT Deferred session id + """ + profile = ISATSession(self.session).profile + return self.sat_host.bridge.mbGetFromManyWithComments(publishers_type, publishers, max_items, max_comments, rsm_dict, rsm_comments_dict, profile) + + def jsonrpc_mbGetFromManyWithCommentsRTResult(self, rt_session): + """Get results from RealTime mbGetFromManyWithComments session + + @param rt_session (str): RT Deferred session id """ profile = ISATSession(self.session).profile - if rsm is None: - rsm = {'max_': unicode(C.RSM_MAX_ITEMS)} - d = self.asyncBridgeCall("getMassiveGroupBlogs", publishers_type, publishers, rsm, profile) - self.sat_host.bridge.massiveSubscribeGroupBlogs(publishers_type, publishers, profile) - return d + return self.asyncBridgeCall("mbGetFromManyWithCommentsRTResult", rt_session, profile) + + + # def jsonrpc_sendMblog(self, type_, dest, text, extra={}): + # """ Send microblog message + # @param type_ (unicode): one of "PUBLIC", "GROUP" + # @param dest (tuple(unicode)): recipient groups (ignored for "PUBLIC") + # @param text (unicode): microblog's text + # """ + # profile = ISATSession(self.session).profile + # extra['allow_comments'] = 'True' + + # if not type_: # auto-detect + # type_ = "PUBLIC" if dest == [] else "GROUP" + + # if type_ in ("PUBLIC", "GROUP") and text: + # if type_ == "PUBLIC": + # #This text if for the public microblog + # log.debug("sending public blog") + # return self.sat_host.bridge.sendGroupBlog("PUBLIC", (), text, extra, profile) + # else: + # log.debug("sending group blog") + # dest = dest if isinstance(dest, list) else [dest] + # return self.sat_host.bridge.sendGroupBlog("GROUP", dest, text, extra, profile) + # else: + # raise Exception("Invalid data") + + # def jsonrpc_deleteMblog(self, pub_data, comments): + # """Delete a microblog node + # @param pub_data: a tuple (service, comment node identifier, item identifier) + # @param comments: comments node identifier (for main item) or False + # """ + # profile = ISATSession(self.session).profile + # return self.sat_host.bridge.deleteGroupBlog(pub_data, comments if comments else '', profile) + + # def jsonrpc_updateMblog(self, pub_data, comments, message, extra={}): + # """Modify a microblog node + # @param pub_data: a tuple (service, comment node identifier, item identifier) + # @param comments: comments node identifier (for main item) or False + # @param message: new message + # @param extra: dict which option name as key, which can be: + # - allow_comments: True to accept an other level of comments, False else (default: False) + # - rich: if present, contain rich text in currently selected syntax + # """ + # profile = ISATSession(self.session).profile + # if comments: + # extra['allow_comments'] = 'True' + # return self.sat_host.bridge.updateGroupBlog(pub_data, comments if comments else '', message, extra, profile) - def jsonrpc_getMblogComments(self, service, node, rsm=None): - """Get all comments of given node - @param service: jid of the service hosting the node - @param node: comments node - """ - profile = ISATSession(self.session).profile - if rsm is None: - rsm = {'max_': unicode(C.RSM_MAX_COMMENTS)} - d = self.asyncBridgeCall("getGroupBlogComments", service, node, rsm, profile) - return d + # def jsonrpc_sendMblogComment(self, node, text, extra={}): + # """ 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, extra, profile) + # else: + # raise Exception("Invalid data") + + # def jsonrpc_getMblogs(self, publisher_jid, item_ids, max_items=C.RSM_MAX_ITEMS): + # """Get specified microblogs posted by a contact + # @param publisher_jid: jid of the publisher + # @param item_ids: list of microblogs items IDs + # @return list of microblog data (dict)""" + # profile = ISATSession(self.session).profile + # d = self.asyncBridgeCall("getGroupBlogs", publisher_jid, item_ids, {'max_': unicode(max_items)}, False, profile) + # return d + + # def jsonrpc_getMblogsWithComments(self, publisher_jid, item_ids, max_comments=C.RSM_MAX_COMMENTS): + # """Get specified microblogs posted by a contact and their comments + # @param publisher_jid: jid of the publisher + # @param item_ids: list of microblogs items IDs + # @return list of couple (microblog data, list of microblog data)""" + # profile = ISATSession(self.session).profile + # d = self.asyncBridgeCall("getGroupBlogsWithComments", publisher_jid, item_ids, {}, max_comments, profile) + # return d + + # def jsonrpc_getMassiveMblogs(self, publishers_type, publishers, rsm=None): + # """Get lasts microblogs posted by several contacts at once + + # @param publishers_type (unicode): one of "ALL", "GROUP", "JID" + # @param publishers (tuple(unicode)): tuple of publishers (empty list for all, list of groups or list of jids) + # @param rsm (dict): TODO + # @return: dict{unicode: list[dict]) + # key: publisher's jid + # value: list of microblog data (dict) + # """ + # profile = ISATSession(self.session).profile + # if rsm is None: + # rsm = {'max_': unicode(C.RSM_MAX_ITEMS)} + # d = self.asyncBridgeCall("getMassiveGroupBlogs", publishers_type, publishers, rsm, profile) + # self.sat_host.bridge.massiveSubscribeGroupBlogs(publishers_type, publishers, profile) + # return d + + # def jsonrpc_getMblogComments(self, service, node, rsm=None): + # """Get all comments of given node + # @param service: jid of the service hosting the node + # @param node: comments node + # """ + # profile = ISATSession(self.session).profile + # if rsm is None: + # rsm = {'max_': unicode(C.RSM_MAX_COMMENTS)} + # d = self.asyncBridgeCall("getGroupBlogComments", service, node, rsm, profile) + # return d def jsonrpc_getPresenceStatuses(self): """Get Presence information for connected contacts""" @@ -1099,7 +1212,7 @@ for signal_name in ['presenceUpdate', 'newMessage', 'subscribe', 'contactDeleted', 'newContact', 'entityDataUpdated', 'askConfirmation', 'newAlert', 'paramUpdate']: self.bridge.register(signal_name, self.signal_handler.getGenericCb(signal_name)) #plugins - for signal_name in ['personalEvent', 'roomJoined', 'roomUserJoined', 'roomUserLeft', 'tarotGameStarted', 'tarotGameNew', 'tarotGameChooseContrat', + for signal_name in ['psEvent', 'roomJoined', 'roomUserJoined', 'roomUserLeft', 'tarotGameStarted', 'tarotGameNew', 'tarotGameChooseContrat', 'tarotGameShowCards', 'tarotGameInvalidCards', 'tarotGameCardsPlayed', 'tarotGameYourTurn', 'tarotGameScore', 'tarotGamePlayers', 'radiocolStarted', 'radiocolPreload', 'radiocolPlay', 'radiocolNoUpload', 'radiocolUploadOk', 'radiocolSongRejected', 'radiocolPlayers', 'roomLeft', 'roomUserChangedNick', 'chatStateReceived']: