# HG changeset patch # User souliane # Date 1388359999 -3600 # Node ID b4781a350483e178edcb4b73e150bc6fbda586dd # Parent 3135ff840b8e5efc3733c4da76d0f269ac321cf3 browser_side: display a "New message" button and add a "comment" icon for main entries in MicroblogPanel when the unibox is disabled diff -r 3135ff840b8e -r b4781a350483 browser_side/panels.py --- a/browser_side/panels.py Mon Dec 30 00:30:45 2013 +0100 +++ b/browser_side/panels.py Mon Dec 30 00:33:19 2013 +0100 @@ -345,8 +345,9 @@ def __init__(self, data): self.id = data['id'] self.type = data.get('type', 'main_item') - self.content = data['content'] - self.xhtml = data.get('xhtml') + self.empty = data.get('new', False) + self.content = data.get('content', '') + self.xhtml = data.get('xhtml', '') self.author = data['author'] self.updated = float(data.get('updated', 0)) # XXX: int doesn't work here try: @@ -354,99 +355,110 @@ except KeyError: self.published = self.updated self.comments = data.get('comments', False) - if self.comments: + if self.empty and self.type == 'main_item': + self.service = self.node = None + self.hash = (self.service, self.node) + else: try: - self.comments_hash = (data['comments_service'], data['comments_node']) - self.comments_service = data['comments_service'] - self.comments_node = data['comments_node'] + self.service = data['comments_service'] if self.comments else data['service'] + self.node = data['comments_node'] if self.comments else data['node'] + self.hash = (self.service, self.node) except KeyError: - print "Warning: can't manage comment [%s], some keys are missing in microblog data (%s)" % (data["comments"], data.keys()) + logging.error("Warning: can't manage item [%s] some keys are missing in microblog data (%s)" % (self.id, data.keys())) self.comments = False - if set(("service", "node")).issubset(data.keys()): - # comment item - self.service = data["service"] - self.node = data["node"] - else: - # main item - try: - self.service = data['comments_service'] - self.node = data['comments_node'] - except KeyError: - logging.error("Main item %s is missing its comments information!" % self.id) - self.hash = (self.service, self.node) class MicroblogEntry(SimplePanel, ClickHandler, FocusHandler, KeyboardHandler): - def __init__(self, blog_panel, mblog_entry): + def __init__(self, blog_panel, data={}): + """ + @param blog_panel: the parent panel + @param data: dict containing the blog item data, or a MicroblogItem instance. + """ + if isinstance(data, MicroblogItem): + self._base_item = data + else: + self._base_item = MicroblogItem(data) SimplePanel.__init__(self) self._blog_panel = blog_panel - self.entry = mblog_entry - self.author = mblog_entry.author - self.updated = mblog_entry.updated - self.published = mblog_entry.published - self.comments = mblog_entry.comments - self.pub_data = (mblog_entry.hash[0], mblog_entry.hash[1], mblog_entry.id) - - self.editable_content = [mblog_entry.xhtml, const.SYNTAX_XHTML] if mblog_entry.xhtml else [mblog_entry.content, None] - self.panel = FlowPanel() self.panel.setStyleName('mb_entry') - update_text = u" — ✍ " + "%s" % datetime.fromtimestamp(self.updated) - header = HTMLPanel("""
- on - %(updated)s -
""" % {'author': html_sanitize(self.author), - 'published': datetime.fromtimestamp(self.published), - 'updated': update_text if self.published != self.updated else '' - } - ) - - - self.panel.add(header) + self.header = HTMLPanel('') + self.panel.add(self.header) - if self.author == blog_panel.host.whoami.bare: - entry_delete_update = VerticalPanel() - entry_delete_update.setStyleName('mb_entry_delete_update') - self.delete_label = Label(u"✗") - self.delete_label.setTitle("Delete this message") - self.update_label = Label(u"✍") - self.update_label.setTitle("Edit this message") - entry_delete_update.add(self.delete_label) - entry_delete_update.add(self.update_label) - self.delete_label.addClickListener(self) - self.update_label.addClickListener(self) - self.panel.add(entry_delete_update) - else: - self.update_label = self.delete_label = None + self.entry_actions = VerticalPanel() + self.entry_actions.setStyleName('mb_entry_actions') + self.panel.add(self.entry_actions) entry_avatar = SimplePanel() entry_avatar.setStyleName('mb_entry_avatar') - self.avatar = Image(blog_panel.host.getAvatar(self.author)) + self.avatar = Image(self._blog_panel.host.getAvatar(self.author)) entry_avatar.add(self.avatar) self.panel.add(entry_avatar) self.entry_dialog = SimplePanel() self.entry_dialog.setStyleName('mb_entry_dialog') - body = addURLToText(html_sanitize(mblog_entry.content)) if not mblog_entry.xhtml else mblog_entry.xhtml - self.bubble = HTML(body) - self.bubble.setStyleName("bubble") - self.entry_dialog.add(self.bubble) self.panel.add(self.entry_dialog) - self.editbox = None self.add(self.panel) ClickHandler.__init__(self) self.addClickListener(self) - def onWindowResized(self, width=None, height=None): - """The listener is active when the text is being modified""" - left = self.avatar.getAbsoluteLeft() + self.avatar.getOffsetWidth() - right = self.delete_label.getAbsoluteLeft() - ideal_width = right - left - 60 - self.entry_dialog.setWidth("%spx" % ideal_width) + self.pub_data = (self.hash[0], self.hash[1], self.id) + self._setContent() + + def __getattr__(self, name): + """This allows to directly use the attributes of MicroblogItem""" + if hasattr(self, name): + return self.__dict__[name] + else: + return getattr(self._base_item, name) + + def _setContent(self): + """Actually set the entry content (header, icons, bubble...)""" + self.delete_label = self.update_label = self.comment_label = None + self.bubble = self.editbox = None + self._setHeader() + if self.empty: + self.editable_content = ['', const.SYNTAX_XHTML] + else: + self.editable_content = [self.xhtml, const.SYNTAX_XHTML] if self.xhtml else [self.content, None] + self.setEntryDialog() + self._setIcons() + + def _setHeader(self): + """Set the entry header""" + if self.empty: + return + update_text = u" — ✍ " + "%s" % datetime.fromtimestamp(self.updated) + self.header.setHTML("""
+ on + %(updated)s +
""" % {'author': html_sanitize(self.author), + 'published': datetime.fromtimestamp(self.published), + 'updated': update_text if self.published != self.updated else '' + } + ) + + def _setIcons(self): + """Set the entry icons (delete, update, comment)""" + if self.empty: + return + + def addIcon(label, title): + label = Label(label) + label.setTitle(title) + label.addClickListener(self) + self.entry_actions.add(label) + return label + + if self.author == self._blog_panel.host.whoami.bare: + self.delete_label = addIcon(u"✗", "Delete this message") + self.update_label = addIcon(u"✍", "Edit this message") + if self.comments: + self.comment_label = addIcon(u"↶", "Comment this message") def updateAvatar(self, new_avatar): """Change the avatar of the entry @@ -456,82 +468,112 @@ def onClick(self, sender): if sender == self: self._blog_panel.setSelectedEntry(self if self.comments else None) - elif sender == self.update_label: - self._update() elif sender == self.delete_label: self._delete() + elif sender == self.update_label: + self.setEntryDialog(edit=True) + elif sender == self.comment_label: + self._comment() def onKeyUp(self, sender, keycode, modifiers): """Update is done when ENTER key is pressed within the raw editbox""" if sender != self.editbox or not self.editbox.getVisible(): return if keycode == KEY_ENTER: - self.updateContent() + self._updateContent() def onLostFocus(self, sender): """Update is done when the focus leaves the raw editbox""" if sender != self.editbox or not self.editbox.getVisible(): return - self.updateContent() + self._updateContent() - def updateContent(self, cancel=False): - """Send the new content to the backend""" + def _updateContent(self, cancel=False): + """Send the new content to the backend, remove the entry if it was + an empty one (used for creating a new blog post)""" if not self.editbox or not self.editbox.getVisible(): return - Window.removeWindowResizeListener(self) self.entry_dialog.setWidth("auto") self.entry_dialog.remove(self.edit_panel) self.entry_dialog.add(self.bubble) new_text = self.editbox.getText().strip() self.edit_panel = self.editbox = None + + def removeNewEntry(): + if self.empty: + self._blog_panel.removeEntry(self.type, self.id) + if self.type == 'main_item': + self._blog_panel.setUniBox(enable=False) + if cancel or new_text == self.editable_content[0] or new_text == "": + removeNewEntry() return self.editable_content[0] = new_text extra = {'published': str(self.published)} - if self.entry.xhtml: + if self.empty or self.xhtml: extra.update({'rich': new_text}) - self._blog_panel.host.bridge.call('updateMblog', None, self.pub_data, self.comments, new_text, extra) + if self.empty: + if self.type == 'main_item': + self._blog_panel.host.bridge.call('sendMblog', None, None, self._blog_panel.accepted_groups, new_text, extra) + else: + self._blog_panel.host.bridge.call('sendMblogComment', None, self._parent_entry.comments, new_text, extra) + else: + self._blog_panel.host.bridge.call('updateMblog', None, self.pub_data, self.comments, new_text, extra) + removeNewEntry() - def _update(self): - """Change the bubble to an editbox""" - if self.editbox and self.editbox.getVisible(): - return + def setEntryDialog(self, edit=False): + """Set the bubble or the editor + @param edit: set to True to display the editor""" + if edit: + if self.empty or self.xhtml: + if not self.editbox: + + def cb(result): + self._updateContent(result == richtext.CANCEL) + + options = ('no_recipient', 'no_sync_unibox', 'no_style', 'update_msg', 'no_close') + editor = richtext.RichTextEditor(self._blog_panel.host, self.panel, cb, options=options) + editor.setWidth('100%') + self.editbox = editor.textarea - def setOriginalText(text, container): - text = text.strip() - container.original_text = text - self.editbox.setWidth('100%') - self.editbox.setText(text) - panel = SimplePanel() - panel.add(container) - panel.setStyleName("bubble") - panel.addStyleName('bubble-editbox') - Window.addWindowResizeListener(self) - self.onWindowResized() + editor.setVisible(True) # needed to build the toolbar + if self.editable_content[0]: + self._blog_panel.host.bridge.call('syntaxConvert', lambda d: self._setOriginalText(d, editor), + self.editable_content[0], self.editable_content[1]) + else: + self._setOriginalText("", editor) + else: + if not self.editbox: + self.editbox = TextArea() + self.editbox.addFocusListener(self) + self.editbox.addKeyboardListener(self) + self._setOriginalText(self.editable_content[0], self.editbox) + else: + if not self.bubble: + self.bubble = HTML() + self.bubble.setStyleName("bubble") + + self.bubble.setHTML(addURLToText(html_sanitize(self.content)) if not self.xhtml else self.xhtml) + self.entry_dialog.add(self.bubble) + + def _setOriginalText(self, text, container): + """Set the original text to be modified in the editor""" + text = text.strip() + container.original_text = text + self.editbox.setWidth('100%') + self.editbox.setText(text) + panel = SimplePanel() + panel.add(container) + panel.setStyleName("bubble") + panel.addStyleName('bubble-editbox') + if self.bubble: self.entry_dialog.remove(self.bubble) - self.entry_dialog.add(panel) - self.editbox.setFocus(True) + self.entry_dialog.add(panel) + self.editbox.setFocus(True) + if text: self.editbox.setSelectionRange(len(text), 0) - self.edit_panel = panel - self.editable_content = [text, container.format if isinstance(container, richtext.RichTextEditor) else None] - - if self.entry.xhtml: - options = ('no_recipient', 'no_sync_unibox', 'no_style', 'update_msg', 'no_close') - - def cb(result): - self.updateContent(result == richtext.CANCEL) - - editor = richtext.RichTextEditor(self._blog_panel.host, self.panel, cb, options=options) - editor.setWidth('100%') - editor.setVisible(True) # needed to build the toolbar - self.editbox = editor.textarea - self._blog_panel.host.bridge.call('syntaxConvert', lambda d: setOriginalText(d, editor), - self.editable_content[0], self.editable_content[1]) - else: - self.editbox = TextArea() - self.editbox.addFocusListener(self) - self.editbox.addKeyboardListener(self) - setOriginalText(self.editable_content[0], self.editbox) + self.edit_panel = panel + self.editable_content = [text, container.format if isinstance(container, richtext.RichTextEditor) else None] def _delete(self): """Ask confirmation for deletion""" @@ -543,6 +585,19 @@ _dialog = ConfirmDialog(confirm_cb, text="Do you really want to delete this %s?" % target) _dialog.show() + def _comment(self): + """Add an empty entry for a new comment""" + data = {'id': str(time()), + 'new': True, + 'type': 'comment', + 'author': self._blog_panel.host.whoami.bare, + 'service': self.service, + 'node': self.node + } + entry = self._blog_panel.addEntry(data) + entry._parent_entry = self + entry.setEntryDialog(edit=True) + class MicroblogPanel(base_widget.LiberviaWidget): warning_msg_public = "This message will be PUBLIC and everybody will be able to see it, even people you don't know" @@ -554,12 +609,34 @@ """ base_widget.LiberviaWidget.__init__(self, host, ", ".join(accepted_groups), selectable=True) self.setAcceptedGroup(accepted_groups) + self.host = host self.entries = {} self.comments = {} self.selected_entry = None self.vpanel = VerticalPanel() self.vpanel.setStyleName('microblogPanel') self.setWidget(self.vpanel) + self.setUniBox(self.host.uni_box) + + def setUniBox(self, enable=False): + """Enable or disable the unibox. If it is disabled, + display the 'New message' button on top of the panel""" + if enable: + return + if hasattr(self, 'new_button'): + self.new_button.setVisible(True) + else: + def addBox(): + self.new_button.setVisible(False) + data = {'id': str(time()), + 'new': True, + 'author': self.host.whoami.bare, + } + entry = self.addEntry(data) + entry.setEntryDialog(edit=True) + self.new_button = Button("New message", listener=addBox) + self.new_button.setStyleName("microblogNewButton") + self.vpanel.insert(self.new_button, 0) @classmethod def registerClass(cls): @@ -651,8 +728,7 @@ if not "content" in mblog: print ("WARNING: No content found in microblog [%s]", mblog) continue - mblog_item = MicroblogItem(mblog) - self.addEntry(mblog_item) + self.addEntry(mblog) def mblogsInsert(self, mblogs): """ Insert several microblogs at once @@ -662,14 +738,15 @@ if not "content" in mblog: print ("WARNING: No content found in microblog [%s]", mblog) continue - mblog_item = MicroblogItem(mblog) - self.addEntry(mblog_item) + self.addEntry(mblog) 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""" + if entry.empty: + entry.published = time() # we look for the right index to insert our entry: # if reversed, we insert the entry above the first entry # in the past @@ -689,16 +766,17 @@ vpanel.insert(entry, idx) - def addEntry(self, mblog_item): + def addEntry(self, data): """Add an entry to the panel - @param mblog_item: MicroblogItem instance + @param data: dict containing the item data + @return: the added entry, or None """ - if mblog_item.type == "comment": - if not mblog_item.hash in self.comments: + _entry = MicroblogEntry(self, data) + if _entry.type == "comment": + if not _entry.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] + return None + parent = self.comments[_entry.hash] parent_idx = self.vpanel.getWidgetIndex(parent) # we find or create the panel where the comment must be inserted try: @@ -712,29 +790,29 @@ self.vpanel.insert(sub_panel, parent_idx + 1) for idx in xrange(0, len(sub_panel.getChildren())): comment = sub_panel.getIndexedChild(idx) - if comment.pub_data[2] == mblog_item.id: + if comment.pub_data[2] == _entry.id: # update an existing comment sub_panel.remove(comment) sub_panel.insert(_entry, idx) - return + return _entry # we want comments to be inserted in chronological order self._chronoInsert(sub_panel, _entry, reverse=False) - return + return _entry - update = mblog_item.id in self.entries - _entry = MicroblogEntry(self, mblog_item) - if update: - idx = self.vpanel.getWidgetIndex(self.entries[mblog_item.id]) - self.vpanel.remove(self.entries[mblog_item.id]) + if _entry.id in self.entries: # update + idx = self.vpanel.getWidgetIndex(self.entries[_entry.id]) + self.vpanel.remove(self.entries[_entry.id]) self.vpanel.insert(_entry, idx) - else: + else: # new entry self._chronoInsert(self.vpanel, _entry) - self.entries[mblog_item.id] = _entry + self.entries[_entry.id] = _entry - if mblog_item.comments: + if _entry.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) + self.comments[_entry.hash] = _entry + self.host.bridge.call('getMblogComments', self.mblogsInsert, _entry.service, _entry.node) + + return _entry def removeEntry(self, type_, id_): """Remove an entry from the panel @@ -743,7 +821,6 @@ """ for child in self.vpanel.getChildren(): if isinstance(child, MicroblogEntry) and type_ == 'main_item': - print child.pub_data if child.pub_data[2] == id_: main_idx = self.vpanel.getWidgetIndex(child) try: diff -r 3135ff840b8e -r b4781a350483 libervia.py --- a/libervia.py Mon Dec 30 00:30:45 2013 +0100 +++ b/libervia.py Mon Dec 30 00:33:19 2013 +0100 @@ -174,13 +174,9 @@ self.room_list = [] # list of rooms self.mblog_cache = [] # used to keep our own blog entries in memory, to show them in new mblog panel self.avatars_cache = {} # keep track of jid's avatar hash (key=jid, value=file) - #self.discuss_panel.addWidget(panels.EmptyPanel(self)) - self.discuss_panel.addWidget(panels.MicroblogPanel(self, [])) - #self.discuss_panel.addWidget(panels.EmptyPanel(self)) self._register_box = None RootPanel().add(self.panel) DOM.addEventPreview(self) - self.resize() self._register = RegisterCall() self._register.call('isRegistered', self._isRegisteredCB) self.initialised = False @@ -328,7 +324,13 @@ self._defaultDomain = "libervia.org" self.bridge.call("getNewAccountDomain", (domain_cb, domain_eb)) - self.bridge.call('asyncGetParamA', self._setUniBox, Const.ENABLE_UNIBOX_PARAM, Const.ENABLE_UNIBOX_KEY) + + def unibox_cb(enable): + self._setUniBox(enable) + self.discuss_panel.addWidget(panels.MicroblogPanel(self, [])) + self.resize() # resize after all the UI elements have been attached + + self.bridge.call('asyncGetParamA', unibox_cb, Const.ENABLE_UNIBOX_PARAM, Const.ENABLE_UNIBOX_KEY) def _tryAutoConnect(self): """This method retrieve the eventual URL parameters to auto-connect the user.""" diff -r 3135ff840b8e -r b4781a350483 public/libervia.css --- a/public/libervia.css Mon Dec 30 00:30:45 2013 +0100 +++ b/public/libervia.css Mon Dec 30 00:33:19 2013 +0100 @@ -69,6 +69,10 @@ .bubble p { margin: 0.4em 0em; } + +.bubble img { + max-width: 100%; +} /* styles for displaying rich text - END */ blockquote, q { quotes: none; } @@ -804,6 +808,10 @@ width: 100%; } +.microblogNewButton { + width: 100%; +} + .subPanel { } @@ -874,16 +882,20 @@ } .bubble-editbox { - width: auto; + width: 100%; +} + +.bubble-editbox textarea{ + width: 100%; } .mb_entry_timestamp { font-style: italic; } -.mb_entry_delete_update { +.mb_entry_actions { float: right; - padding: 0px 5px; + margin: 5px; cursor: pointer; font-size: larger; } diff -r 3135ff840b8e -r b4781a350483 server_css/blog.css --- a/server_css/blog.css Mon Dec 30 00:30:45 2013 +0100 +++ b/server_css/blog.css Mon Dec 30 00:33:19 2013 +0100 @@ -55,3 +55,7 @@ h1, h2, h3, h4, h5, h6 { border-bottom: 1px solid rgb(170, 170, 170); } + +img { + max-width: 100%; +}