changeset 312:b4781a350483

browser_side: display a "New message" button and add a "comment" icon for main entries in MicroblogPanel when the unibox is disabled
author souliane <souliane@mailoo.org>
date Mon, 30 Dec 2013 00:33:19 +0100
parents 3135ff840b8e
children 5ad70625867a
files browser_side/panels.py libervia.py public/libervia.css server_css/blog.css
diffstat 4 files changed, 241 insertions(+), 146 deletions(-) [+]
line wrap: on
line diff
--- 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" — ✍ " + "<span class='mb_entry_timestamp'>%s</span>" % datetime.fromtimestamp(self.updated)
-        header = HTMLPanel("""<div class='mb_entry_header'>
-                                  <span class='mb_entry_author'>%(author)s</span> on
-                                  <span class='mb_entry_timestamp'>%(published)s</span>%(updated)s
-                              </div>""" % {'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" — ✍ " + "<span class='mb_entry_timestamp'>%s</span>" % datetime.fromtimestamp(self.updated)
+        self.header.setHTML("""<div class='mb_entry_header'>
+                                   <span class='mb_entry_author'>%(author)s</span> on
+                                   <span class='mb_entry_timestamp'>%(published)s</span>%(updated)s
+                               </div>""" % {'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:
--- 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."""
--- 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;
 }
--- 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%;
+}