comparison src/browser/sat_browser/blog.py @ 716:3b91225b457a

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/
author Goffi <goffi@goffi.org>
date Sun, 16 Aug 2015 01:51:12 +0200
parents d75935e2b279
children 0d5889b9313c
comparison
equal deleted inserted replaced
715:b2465423c76e 716:3b91225b457a
19 19
20 import pyjd # this is dummy in pyjs 20 import pyjd # this is dummy in pyjs
21 from sat.core.log import getLogger 21 from sat.core.log import getLogger
22 log = getLogger(__name__) 22 log = getLogger(__name__)
23 23
24 from sat.core.i18n import _, D_ 24 from sat.core.i18n import _ #, D_
25 25
26 from pyjamas.ui.SimplePanel import SimplePanel 26 from pyjamas.ui.SimplePanel import SimplePanel
27 from pyjamas.ui.VerticalPanel import VerticalPanel 27 from pyjamas.ui.VerticalPanel import VerticalPanel
28 from pyjamas.ui.ScrollPanel import ScrollPanel
28 from pyjamas.ui.HorizontalPanel import HorizontalPanel 29 from pyjamas.ui.HorizontalPanel import HorizontalPanel
29 from pyjamas.ui.Label import Label 30 from pyjamas.ui.Label import Label
30 from pyjamas.ui.HTML import HTML 31 from pyjamas.ui.HTML import HTML
31 from pyjamas.ui.Image import Image 32 from pyjamas.ui.Image import Image
32 from pyjamas.ui.ClickListener import ClickHandler 33 from pyjamas.ui.ClickListener import ClickHandler
33 from pyjamas.ui.FlowPanel import FlowPanel 34 from pyjamas.ui.FlowPanel import FlowPanel
34 from pyjamas.ui.KeyboardListener import KEY_ENTER, KeyboardHandler 35 from pyjamas.ui import KeyboardListener as keyb
36 from pyjamas.ui.KeyboardListener import KeyboardHandler
35 from pyjamas.ui.FocusListener import FocusHandler 37 from pyjamas.ui.FocusListener import FocusHandler
36 from pyjamas.ui.MouseListener import MouseHandler 38 from pyjamas.ui.MouseListener import MouseHandler
37 from pyjamas.Timer import Timer 39 from pyjamas.Timer import Timer
38 40
39 from datetime import datetime 41 from datetime import datetime
40 from time import time
41 42
42 import html_tools 43 import html_tools
43 import dialog 44 import dialog
44 import richtext 45 import richtext
45 import editor_widget 46 import editor_widget
46 import libervia_widget 47 import libervia_widget
47 from constants import Const as C 48 from constants import Const as C
48 from sat_frontends.quick_frontend import quick_widgets 49 from sat_frontends.quick_frontend import quick_widgets
49 from sat_frontends.tools import jid 50 from sat_frontends.quick_frontend import quick_blog
50
51 51
52 unicode = str # XXX: pyjamas doesn't manage unicode 52 unicode = str # XXX: pyjamas doesn't manage unicode
53 53 ENTRY_RICH = (C.ENTRY_MODE_RICH, C.ENTRY_MODE_XHTML)
54 54
55 class MicroblogItem(): 55
56 # XXX: should be moved in a separated module 56 class Entry(quick_blog.Entry, VerticalPanel, ClickHandler, FocusHandler, KeyboardHandler):
57 57 """Graphical representation of a quick_blog.Item"""
58 def __init__(self, data): 58
59 self.id = data['id'] 59 def __init__(self, manager, item_data=None, comments_data=None, service=None, node=None):
60 self.type = data.get('type', 'main_item') 60 quick_blog.Entry.__init__(self, manager, item_data, comments_data, service, node)
61 self.empty = data.get('new', False) 61
62 self.title = data.get('title', '') 62 VerticalPanel.__init__(self)
63 self.title_xhtml = data.get('title_xhtml', '')
64 self.content = data.get('content', '')
65 self.content_xhtml = data.get('content_xhtml', '')
66 self.author = jid.JID(data['author'])
67 self.updated = float(data.get('updated', 0)) # XXX: int doesn't work here
68 self.published = float(data.get('published', self.updated)) # XXX: int doesn't work here
69 self.service = data.get('service', '')
70 self.node = data.get('node', '')
71 self.comments = data.get('comments', False)
72 self.comments_service = data.get('comments_service', '')
73 self.comments_node = data.get('comments_node', '')
74
75
76 class MicroblogEntry(SimplePanel, ClickHandler, FocusHandler, KeyboardHandler):
77
78 def __init__(self, blog_panel, data):
79 """
80 @param blog_panel: the parent panel
81 @param data: dict containing the blog item data, or a MicroblogItem instance.
82 """
83 self._base_item = data if isinstance(data, MicroblogItem) else MicroblogItem(data)
84 for attr in ['id', 'type', 'empty', 'title', 'title_xhtml', 'content', 'content_xhtml',
85 'author', 'updated', 'published', 'comments', 'service', 'node',
86 'comments_service', 'comments_node']:
87 getter = lambda attr: lambda inst: getattr(inst._base_item, attr)
88 setter = lambda attr: lambda inst, value: setattr(inst._base_item, attr, value)
89 setattr(MicroblogEntry, attr, property(getter(attr), setter(attr)))
90
91 SimplePanel.__init__(self)
92 self._blog_panel = blog_panel
93 63
94 self.panel = FlowPanel() 64 self.panel = FlowPanel()
95 self.panel.setStyleName('mb_entry') 65 self.panel.setStyleName('mb_entry')
96 66
97 self.header = HorizontalPanel(StyleName='mb_entry_header') 67 self.header = HorizontalPanel(StyleName='mb_entry_header')
101 self.entry_actions.setStyleName('mb_entry_actions') 71 self.entry_actions.setStyleName('mb_entry_actions')
102 self.panel.add(self.entry_actions) 72 self.panel.add(self.entry_actions)
103 73
104 entry_avatar = SimplePanel() 74 entry_avatar = SimplePanel()
105 entry_avatar.setStyleName('mb_entry_avatar') 75 entry_avatar.setStyleName('mb_entry_avatar')
106 assert isinstance(self.author, jid.JID) # FIXME: temporary 76 author_jid = self.author_jid
107 self.avatar = Image(self._blog_panel.host.getAvatarURL(self.author)) # FIXME: self.author should be initially a jid.JID 77 self.avatar = Image(self.blog.host.getAvatarURL(author_jid) if author_jid is not None else C.DEFAULT_AVATAR_URL)
78 # TODO: show a warning icon if author is not validated
108 entry_avatar.add(self.avatar) 79 entry_avatar.add(self.avatar)
109 self.panel.add(entry_avatar) 80 self.panel.add(entry_avatar)
110 81
111 self.entry_dialog = VerticalPanel() 82 self.entry_dialog = VerticalPanel()
112 self.entry_dialog.setStyleName('mb_entry_dialog') 83 self.entry_dialog.setStyleName('mb_entry_dialog')
113 self.panel.add(self.entry_dialog) 84 self.panel.add(self.entry_dialog)
114 85
86 self.comments_panel = None
87 self._current_comment = None
88
115 self.add(self.panel) 89 self.add(self.panel)
116 ClickHandler.__init__(self) 90 ClickHandler.__init__(self)
117 self.addClickListener(self) 91 self.addClickListener(self)
118 92
119 self.__pub_data = (self.service, self.node, self.id) 93 self.refresh()
120 self.__setContent() 94 self.displayed = False # True when entry is added to parent
121 95 if comments_data:
122 def __setContent(self): 96 self.addComments(comments_data)
123 """Actually set the entry content (header, icons, bubble...)""" 97
124 self.delete_label = self.update_label = self.comment_label = None 98 def refresh(self):
125 self.bubble = self._current_comment = None 99 self.header.clear()
126 self.__setHeader() 100 self.entry_dialog.clear()
127 self.__setBubble() 101 self.entry_actions.clear()
128 self.__setIcons() 102 self._setHeader()
129 103 self._setBubble()
130 def __setHeader(self): 104 self._setIcons()
105
106 def _setHeader(self):
131 """Set the entry header.""" 107 """Set the entry header."""
132 if self.empty: 108 if not self.new:
133 return 109 update_text = u" — ✍ " + "<span class='mb_entry_timestamp'>%s</span>" % datetime.fromtimestamp(self.item.updated)
134 update_text = u" — ✍ " + "<span class='mb_entry_timestamp'>%s</span>" % datetime.fromtimestamp(self.updated) 110 self.header.add(HTML("""<span class='mb_entry_header_info'>
135 self.header.add(HTML("""<span class='mb_entry_header_info'> 111 <span class='mb_entry_author'>%(author)s</span> on
136 <span class='mb_entry_author'>%(author)s</span> on 112 <span class='mb_entry_timestamp'>%(published)s</span>%(updated)s
137 <span class='mb_entry_timestamp'>%(published)s</span>%(updated)s 113 </span>""" % {'author': html_tools.html_sanitize(unicode(self.item.author)),
138 </span>""" % {'author': html_tools.html_sanitize(unicode(self.author)), 114 'published': datetime.fromtimestamp(self.item.published) if self.item.published is not None else '',
139 'published': datetime.fromtimestamp(self.published), 115 'updated': update_text if self.item.published != self.item.updated else ''
140 'updated': update_text if self.published != self.updated else '' 116 }))
141 })) 117 if self.item.comments:
142 if self.comments: 118 self.show_comments_link = HTML('')
143 self.comments_count = self.hidden_count = 0 119 self.header.add(self.show_comments_link)
144 self.show_comments_link = HTML('') 120
145 self.header.add(self.show_comments_link) 121 def _setBubble(self):
146 122 """Set the bubble displaying the initial content."""
147 def updateHeader(self, comments_count=None, hidden_count=None, inc=None): 123 content = {'text': self.item.content_xhtml if self.item.content_xhtml else self.item.content or '',
148 """Update the header. 124 'title': self.item.title_xhtml if self.item.title_xhtml else self.item.title or ''}
149 125
150 @param comments_count (int): total number of comments. 126 if self.mode == C.ENTRY_MODE_TEXT:
151 @param hidden_count (int): number of hidden comments. 127 # assume raw text message have no title
152 @param inc (int): number to increment the total number of comments with. 128 self.bubble = editor_widget.LightTextEditor(content, modifiedCb=self._modifiedCb, afterEditCb=self._afterEditCb, options={'no_xhtml': True})
153 """ 129 elif self.mode in ENTRY_RICH:
154 if comments_count is not None: 130 content['syntax'] = C.SYNTAX_XHTML
155 self.comments_count = comments_count 131 if self.new:
156 if hidden_count is not None: 132 options = []
157 self.hidden_count = hidden_count 133 elif self.item.author_jid == self.blog.host.whoami.bare:
158 if inc is not None: 134 options = ['update_msg']
159 self.comments_count += inc
160
161 if self.hidden_count > 0:
162 comments = D_('comments') if self.hidden_count > 1 else D_('comment')
163 text = D_("<a>show %(count)d previous %(comments)s</a>") % {'count': self.hidden_count,
164 'comments': comments}
165 if self not in self.show_comments_link._clickListeners:
166 self.show_comments_link.addClickListener(self)
167 else:
168 if self.comments_count > 1:
169 text = "%(count)d %(comments)s" % {'count': self.comments_count,
170 'comments': D_('comments')}
171 elif self.comments_count == 1:
172 text = D_('1 comment')
173 else: 135 else:
174 text = '' 136 options = ['read_only']
175 try: 137 self.bubble = richtext.RichTextEditor(self.blog.host, content, modifiedCb=self._modifiedCb, afterEditCb=self._afterEditCb, options=options)
176 self.show_comments_link.removeClickListener(self) 138 else:
177 except ValueError: 139 log.error("Bad entry mode: %s" % self.mode)
178 pass 140 self.bubble.addStyleName("bubble")
179 141 self._showSyntaxSwitchButton(False)
180 self.show_comments_link.setHTML("""<span class='mb_entry_comments'>%(text)s</span></div>""" % {'text': text}) 142 self.entry_dialog.add(self.bubble)
181 143 self.bubble.addEditListener(self._showWarning) # FIXME: remove edit listeners
182 def __setIcons(self): 144 self._setEditable()
145
146 def _setIcons(self):
183 """Set the entry icons (delete, update, comment)""" 147 """Set the entry icons (delete, update, comment)"""
184 if self.empty: 148 if self.new:
185 return 149 return
186 150
187 def addIcon(label, title): 151 def addIcon(label, title):
188 label = Label(label) 152 label = Label(label)
189 label.setTitle(title) 153 label.setTitle(title)
190 label.addClickListener(self) 154 label.addClickListener(self)
191 self.entry_actions.add(label) 155 self.entry_actions.add(label)
192 return label 156 return label
193 157
194 if self.comments: 158 if self.item.comments:
195 self.comment_label = addIcon(u"↶", "Comment this message") 159 self.comment_label = addIcon(u"↶", "Comment this message")
196 self.comment_label.setStyleName('mb_entry_action_larger') 160 self.comment_label.setStyleName('mb_entry_action_larger')
197 is_publisher = self.author == self._blog_panel.host.whoami.bare 161 is_publisher = self.item.author_jid == self.blog.host.whoami.bare
198 if is_publisher: 162 if is_publisher:
199 self.update_label = addIcon(u"✍", "Edit this message") 163 self.update_label = addIcon(u"✍", "Edit this message")
200 if is_publisher or unicode(self.node).endswith(unicode(self._blog_panel.host.whoami.bare)): 164 # TODO: add delete button if we are the owner of the node
201 self.delete_label = addIcon(u"✗", "Delete this message") 165 self.delete_label = addIcon(u"✗", "Delete this message")
202 166
203 def updateAvatar(self, new_avatar): 167 def _createCommentsPanel(self):
204 """Change the avatar of the entry 168 """Create the panel if it doesn't exists"""
205 @param new_avatar: path to the new image""" 169 if self.comments_panel is None:
206 self.avatar.setUrl(new_avatar) 170 self.comments_panel = VerticalPanel()
207 171 self.comments_panel.setStyleName('microblogPanel')
208 def onClick(self, sender): 172 self.comments_panel.addStyleName('subPanel')
209 if sender == self: 173 self.add(self.comments_panel)
210 self._blog_panel.setSelectedEntry(self) 174
211 elif sender == self.delete_label: 175 def _setEditable(self):
212 self._delete() 176 self.bubble.edit(self.editable)
213 elif sender == self.update_label: 177 if self.editable:
214 self.edit(True) 178 self._showSyntaxSwitchButton()
215 elif sender == self.comment_label: 179
216 self._comment() 180 def setEditable(self, editable=True):
217 elif sender == self.show_comments_link: 181 self.editable = editable
218 self._blog_panel.loadAllCommentsForEntry(self) 182 self._setEditable()
219 183
220 def __modifiedCb(self, content): 184 def _showSyntaxSwitchButton(self, show=True):
221 """Send the new content to the backend 185 if show:
222 @return: False to restore the original content if a deletion has been cancelled 186 if self.mode == C.ENTRY_MODE_TEXT:
223 """ 187 html = '<a style="color: blue;">rich text</a>'
224 if not content['text']: # previous content has been emptied
225 self._delete(True)
226 return False
227 extra = {'published': unicode(self.published)}
228 if isinstance(self.bubble, richtext.RichTextEditor):
229 # TODO: if the user change his parameters after the message edition started,
230 # the message syntax could be different then the current syntax: pass the
231 # message syntax in extra for the frontend to use it instead of current syntax.
232 extra.update({'content_rich': content['text'], 'title': content['title']})
233 if self.empty:
234 if self.type == 'main_item':
235 self._blog_panel.host.bridge.call('sendMblog', None, None, tuple(self._blog_panel.accepted_groups), content['text'], extra)
236 else: 188 else:
237 self._blog_panel.host.bridge.call('sendMblogComment', None, self._parent_entry.comments, content['text'], extra) 189 html = '<a style="color: blue;">raw text</a>'
238 else: 190 self.toggle_syntax_button = HTML(html)
239 self._blog_panel.host.bridge.call('updateMblog', None, self.__pub_data, self.comments, content['text'], extra) 191 self.toggle_syntax_button.addClickListener(self.toggleContentSyntax)
240 return True 192 self.toggle_syntax_button.addStyleName('mb_entry_toggle_syntax')
241 193 self.entry_dialog.add(self.toggle_syntax_button)
242 def __afterEditCb(self, content): 194 self.toggle_syntax_button.setStyleAttribute('top', '-20px') # XXX: need to force CSS
243 """Remove the entry if it was an empty one (used for creating a new blog post). 195 self.toggle_syntax_button.setStyleAttribute('left', '-20px')
244 Data for the actual new blog post will be received from the bridge""" 196 else:
245 if self.empty: 197 try:
246 self._blog_panel.removeEntry(self.type, self.id, update_header=False) 198 self.toggle_syntax_button.removeFromParent()
247 if self.type == 'main_item': # restore the "New message" button 199 except (AttributeError, TypeError):
248 self._blog_panel.addNewMessageEntry() 200 pass
249 else: # allow to create a new comment 201
250 self._parent_entry._current_comment = None 202
251 self.entry_dialog.setWidth('auto') 203 def edit(self, edit=True):
204 """Toggle the bubble between display and edit mode"""
252 try: 205 try:
253 self.toggle_syntax_button.removeFromParent() 206 self.toggle_syntax_button.removeFromParent()
254 except (AttributeError, TypeError): 207 except (AttributeError, TypeError):
255 pass 208 pass
256 209 self.bubble.edit(edit)
257 def __setBubble(self, edit=False): 210 if edit:
258 """Set the bubble displaying the initial content.""" 211 if self.mode in ENTRY_RICH:
259 content = {'text': self.content_xhtml if self.content_xhtml else self.content, 212 # image = '<a class="richTextIcon">A</a>'
260 'title': self.title_xhtml if self.title_xhtml else self.title} 213 html = '<a style="color: blue;">raw text</a>'
261 if self.content_xhtml: 214 # title = _('Switch to raw text edition')
262 content.update({'syntax': C.SYNTAX_XHTML})
263 if self.author != self._blog_panel.host.whoami.bare:
264 options = ['read_only']
265 else: 215 else:
266 options = [] if self.empty else ['update_msg'] 216 # image = '<img src="media/icons/tango/actions/32/format-text-italic.png" class="richTextIcon"/>'
267 self.bubble = richtext.RichTextEditor(self._blog_panel.host, content, self.__modifiedCb, self.__afterEditCb, options) 217 html = '<a style="color: blue;">rich text</a>'
268 else: # assume raw text message have no title 218 # title = _('Switch to rich text edition')
269 self.bubble = editor_widget.LightTextEditor(content, self.__modifiedCb, self.__afterEditCb, options={'no_xhtml': True}) 219 self.toggle_syntax_button = HTML(html)
270 self.bubble.addStyleName("bubble") 220 self.toggle_syntax_button.addClickListener(self.toggleContentSyntax)
271 try: 221 self.toggle_syntax_button.addStyleName('mb_entry_toggle_syntax')
272 self.toggle_syntax_button.removeFromParent() 222 self.entry_dialog.add(self.toggle_syntax_button)
273 except (AttributeError, TypeError): 223 self.toggle_syntax_button.setStyleAttribute('top', '-20px') # XXX: need to force CSS
274 pass 224 self.toggle_syntax_button.setStyleAttribute('left', '-20px')
275 self.entry_dialog.add(self.bubble) 225
276 self.edit(edit) 226 # def updateAvatar(self, new_avatar):
277 self.bubble.addEditListener(self.__showWarning) 227 # """Change the avatar of the entry
278 228 # @param new_avatar: path to the new image"""
279 def __showWarning(self, sender, keycode): 229 # self.avatar.setUrl(new_avatar)
280 if keycode == KEY_ENTER: 230
281 self._blog_panel.host.showWarning(None, None) 231 def onClick(self, sender):
282 else: 232 if sender == self:
283 self._blog_panel.host.showWarning(*self._blog_panel.getWarningData(self.type == 'comment')) 233 self.blog.setSelectedEntry(self)
284 234 elif sender == self.delete_label:
285 def _delete(self, empty=False): 235 self._onRetractClick()
286 """Ask confirmation for deletion. 236 elif sender == self.update_label:
287 @return: False if the deletion has been cancelled.""" 237 self.edit(True)
238 elif sender == self.comment_label:
239 self._onCommentClick()
240 # elif sender == self.show_comments_link:
241 # self._blog_panel.loadAllCommentsForEntry(self)
242
243 def _modifiedCb(self, content):
244 """Send the new content to the backend
245
246 @return: False to restore the original content if a deletion has been cancelled
247 """
248 if not content['text']: # previous content has been emptied
249 return False
250
251 self.item.content = self.item.content_rich = self.item.content_xhtml = None
252 self.item.title = self.item.title_rich = self.item.title_xhtml = None
253
254 if self.mode in ENTRY_RICH:
255 # TODO: if the user change his parameters after the message edition started,
256 # the message syntax could be different then the current syntax: pass the
257 # message syntax in mb_data for the frontend to use it instead of current syntax.
258 self.item.content_rich = content['text']
259 self.item.title = content['title']
260 else:
261 self.item.content = content['text']
262
263 self.send()
264
265 return True
266
267 def _afterEditCb(self, content):
268 """Post edition treatments
269
270 Remove the entry if it was an empty one (used for creating a new blog post).
271 Data for the actual new blog post will be received from the bridge
272 @param content(dict): edited content
273 """
274 if self.new:
275 if self.level == 0:
276 # we have a main item, we keep the edit entry
277 self.reset(None)
278 # FIXME: would be better to reset bubble
279 # but bubble.setContent() doesn't seem to work
280 self.bubble.removeFromParent()
281 self._setBubble()
282 else:
283 # we don't keep edit entries for comments
284 self.delete()
285 else:
286 self._showSyntaxSwitchButton(False)
287
288 def _showWarning(self, sender, keycode, modifiers):
289 if keycode == keyb.KEY_ENTER & keyb.MODIFIER_SHIFT: # FIXME: fix edit_listeners, it's dirty (we have to check keycode/modifiers twice !)
290 self.blog.host.showWarning(None, None)
291 else:
292 # self.blog.host.showWarning(*self.blog.getWarningData(self.type == 'comment'))
293 self.blog.host.showWarning(*self.blog.getWarningData(False)) # FIXME: comments are not yet reimplemented
294
295 def _onRetractClick(self):
296 """Ask confirmation then retract current entry."""
297 assert not self.new
298
288 def confirm_cb(answer): 299 def confirm_cb(answer):
289 if answer: 300 if answer:
290 self._blog_panel.host.bridge.call('deleteMblog', None, self.__pub_data, self.comments) 301 self.retract()
291 else: # restore the text if it has been emptied during the edition 302
292 self.bubble.setContent(self.bubble._original_content) 303 entry_type = _("message") if self.level == 0 else _("comment")
293 304 and_comments = _(" All comments will be also deleted!") if self.item.comments else ""
294 if self.empty: 305 text = _("Do you really want to delete this {entry_type}?{and_comments}").format(
295 text = _("New ") + (_("message") if self.comments else _("comment")) + _(" without body has been cancelled.") 306 entry_type=entry_type, and_comments=and_comments)
296 dialog.InfoDialog(_("Information"), text).show()
297 return
298 text = ""
299 if empty:
300 text = (_("Message") if self.comments else _("Comment")) + _(" body has been emptied.<br/>")
301 target = _('message and all its comments') if self.comments else _('comment')
302 text += _("Do you really want to delete this %s?") % target
303 dialog.ConfirmDialog(confirm_cb, text=text).show() 307 dialog.ConfirmDialog(confirm_cb, text=text).show()
304 308
305 def _comment(self): 309 def _onCommentClick(self):
306 """Add an empty entry for a new comment""" 310 """Add an empty entry for a new comment"""
307 if self._current_comment: 311 if self._current_comment is None:
308 self._current_comment.bubble.setFocus(True) 312 if not self.item.comments_service or not self.item.comments_node:
309 self._blog_panel.setSelectedEntry(self._current_comment, True) 313 log.warning("Invalid service and node for comments, can pcreate a comment")
310 return 314 self._current_comment = self.addEntry(editable=True, service=self.item.comments_service, node=self.item.comments_node)
311 data = {'id': unicode(time()), 315 self.blog.setSelectedEntry(self._current_comment, True)
312 'new': True, 316
313 'type': 'comment', 317 def _changeMode(self, original_content, text):
314 'author': unicode(self._blog_panel.host.whoami.bare), 318 self.mode = C.ENTRY_MODE_RICH if self.mode == C.ENTRY_MODE_TEXT else C.ENTRY_MODE_TEXT
315 'service': self.comments_service, 319 if self.mode in ENTRY_RICH and not text:
316 'node': self.comments_node 320 text = ' ' # something different than empty string is needed to initialize the rich text editor
317 } 321 self.item.content = text
318 entry = self._blog_panel.addEntry(data, update_header=False) 322 self.content_title = self.content_title_xhtml = ''
319 if entry is None: 323 self.bubble.removeFromParent()
320 log.info("The entry of id %s can not be commented" % self.id) 324 self._setBubble()
321 return 325 self.bubble.setOriginalContent(original_content)
322 entry._parent_entry = self 326 if self.mode in ENTRY_RICH:
323 self._current_comment = entry 327 self.item.content_xhtml = text
324 self.edit(True, entry) 328 self.bubble.setDisplayContent() # needed in case the edition is aborted, to not end with an empty bubble
325 self._blog_panel.setSelectedEntry(entry, True) 329 else:
326 330 self.item.content_xhtml = ''
327 def edit(self, edit, entry=None):
328 """Toggle the bubble between display and edit mode
329 @edit: boolean value
330 @entry: MicroblogEntry instance, or None to use self
331 """
332 if entry is None:
333 entry = self
334 try:
335 entry.toggle_syntax_button.removeFromParent()
336 except (AttributeError, TypeError):
337 pass
338 entry.bubble.edit(edit)
339 if edit:
340 if isinstance(entry.bubble, richtext.RichTextEditor):
341 image = '<a class="richTextIcon">A</a>'
342 html = '<a style="color: blue;">raw text</a>'
343 title = _('Switch to raw text edition')
344 else:
345 image = '<img src="media/icons/tango/actions/32/format-text-italic.png" class="richTextIcon"/>'
346 html = '<a style="color: blue;">rich text</a>'
347 title = _('Switch to rich text edition')
348 entry.toggle_syntax_button = HTML(html)
349 entry.toggle_syntax_button.addClickListener(entry.toggleContentSyntax)
350 entry.toggle_syntax_button.addStyleName('mb_entry_toggle_syntax')
351 entry.entry_dialog.add(entry.toggle_syntax_button)
352 entry.toggle_syntax_button.setStyleAttribute('top', '-20px') # XXX: need to force CSS
353 entry.toggle_syntax_button.setStyleAttribute('left', '-20px')
354 331
355 def toggleContentSyntax(self): 332 def toggleContentSyntax(self):
356 """Toggle the editor between raw and rich text""" 333 """Toggle the editor between raw and rich text"""
357 original_content = self.bubble.getOriginalContent() 334 original_content = self.bubble.getOriginalContent()
358 rich = not isinstance(self.bubble, richtext.RichTextEditor) 335 rich = self.mode in ENTRY_RICH
359 if rich: 336 if rich:
360 original_content['syntax'] = C.SYNTAX_XHTML 337 original_content['syntax'] = C.SYNTAX_XHTML
361 338
362 def setBubble(text): 339 text = self.bubble.getContent()['text']
363 self.content = text 340
364 self.content_xhtml = text if rich else '' 341 if not text.strip():
365 self.content_title = self.content_title_xhtml = '' 342 self._changeMode(original_content,'')
366 self.bubble.removeFromParent() 343 else:
367 self.__setBubble(True)
368 self.bubble.setOriginalContent(original_content)
369 if rich: 344 if rich:
370 self.bubble.setDisplayContent() # needed in case the edition is aborted, to not end with an empty bubble 345 def confirm_cb(answer):
371 346 if answer:
372 text = self.bubble.getContent()['text'] 347 self.blog.host.bridge.syntaxConvert(text, C.SYNTAX_CURRENT, C.SYNTAX_TEXT, profile=None,
373 if not text: 348 callback=lambda converted: self._changeMode(original_content, converted))
374 setBubble(' ') # something different than empty string is needed to initialize the rich text editor 349 dialog.ConfirmDialog(confirm_cb, text=_("Do you really want to lose the title and text formatting?")).show()
375 return 350 else:
376 if not rich: 351 self.blog.host.bridge.syntaxConvert(text, C.SYNTAX_TEXT, C.SYNTAX_XHTML, profile=None,
377 def confirm_cb(answer): 352 callback=lambda converted: self._changeMode(original_content, converted))
378 if answer: 353
379 self._blog_panel.host.bridge.call('syntaxConvert', setBubble, text, C.SYNTAX_CURRENT, C.SYNTAX_TEXT) 354 def update(self, entry=None):
380 dialog.ConfirmDialog(confirm_cb, text=_("Do you really want to lose the title and text formatting?")).show() 355 """Update comments"""
381 else: 356 self._createCommentsPanel()
382 self._blog_panel.host.bridge.call('syntaxConvert', setBubble, text, C.SYNTAX_TEXT, C.SYNTAX_XHTML) 357 self.entries.sort(key=lambda entry: entry.item.published, reverse=True)
383 358 idx = 0
384 359 for entry in self.entries:
385 class MicroblogPanel(quick_widgets.QuickWidget, libervia_widget.LiberviaWidget, MouseHandler): 360 if not entry.displayed:
361 self.comments_panel.insert(entry, idx)
362 entry.displayed = True
363 idx += 1
364
365 def delete(self):
366 quick_blog.Entry.delete(self)
367
368 # _current comment is specific to libervia, we remove it
369 if isinstance(self.manager, Entry):
370 self.manager._current_comment = None
371
372 # now we remove the pyjamas widgets
373 parent = self.parent
374 assert isinstance(parent, VerticalPanel)
375 self.removeFromParent()
376 if not parent.children:
377 # the vpanel is empty, we remove it
378 parent.removeFromParent()
379 try:
380 if self.manager.comments_panel == parent:
381 self.manager.comments_panel = None
382 except AttributeError:
383 assert isinstance(self.manager, quick_blog.QuickBlog)
384
385
386 class Blog(quick_blog.QuickBlog, libervia_widget.LiberviaWidget, MouseHandler):
387 """Panel used to show microblog"""
386 warning_msg_public = "This message will be <b>PUBLIC</b> and everybody will be able to see it, even people you don't know" 388 warning_msg_public = "This message will be <b>PUBLIC</b> and everybody will be able to see it, even people you don't know"
387 warning_msg_group = "This message will be published for all the people of the following groups: <span class='warningTarget'>%s</span>" 389 warning_msg_group = "This message will be published for all the people of the following groups: <span class='warningTarget'>%s</span>"
388 # FIXME: all the generic parts must be moved to quick_frontends
389 390
390 def __init__(self, host, targets, profiles=None): 391 def __init__(self, host, targets, profiles=None):
391 """Panel used to show microblog 392 quick_blog.QuickBlog.__init__(self, host, targets, C.PROF_KEY_NONE)
392 393 title = ", ".join(targets) if targets else "Blog"
393 @param targets (tuple(unicode)): contact groups displayed in this panel. 394 libervia_widget.LiberviaWidget.__init__(self, host, title, selectable=True)
394 If empty, show all microblogs from all contacts.
395 """
396 # do not mix self.targets (set of tuple of unicode) and self.accepted_groups (set of unicode)
397 quick_widgets.QuickWidget.__init__(self, host, targets, C.PROF_KEY_NONE)
398 libervia_widget.LiberviaWidget.__init__(self, host, ", ".join(self.accepted_groups), selectable=True)
399 MouseHandler.__init__(self) 395 MouseHandler.__init__(self)
400 self.entries = {}
401 self.comments = {}
402 self.vpanel = VerticalPanel() 396 self.vpanel = VerticalPanel()
403 self.vpanel.setStyleName('microblogPanel') 397 self.vpanel.setStyleName('microblogPanel')
404 self.setWidget(self.vpanel) 398 self.setWidget(self.vpanel)
405 self.addNewMessageEntry() 399 self.addEntry(editable=True, first=True)
406 400
407 self.footer = HTML('', StyleName='microblogPanel_footer') 401 self.getAll()
408 self.footer.waiting = False 402
409 self.footer.addClickListener(self) 403 # self.footer = HTML('', StyleName='microblogPanel_footer')
410 self.footer.addMouseListener(self) 404 # self.footer.waiting = False
411 self.vpanel.add(self.footer) 405 # self.footer.addClickListener(self)
412 self.next_rsm_index = 0 406 # self.footer.addMouseListener(self)
413 407 # self.vpanel.add(self.footer)
414 # 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) 408 # self.next_rsm_index = 0
415 self.avatarListener = self.onAvatarUpdate
416 host.addListener('avatar', self.avatarListener, [C.PROF_KEY_NONE])
417 409
418 def __str__(self): 410 def __str__(self):
419 return u"Blog Widget [target: {}, profile: {}]".format(', '.join(self.accepted_groups), self.profile) 411 return u"Blog Widget [targets: {}, profile: {}]".format(", ".join(self.targets) if self.targets else "meta blog", self.profile)
420 412
421 def onDelete(self): 413 def update(self):
422 quick_widgets.QuickWidget.onDelete(self) 414 self.entries.sort(key=lambda entry: entry.item.published, reverse=True)
423 self.host.removeListener('avatar', self.avatarListener) 415
424 416 idx = 0
425 def onAvatarUpdate(self, jid_, hash_, profile): 417 if self._first_entry is not None:
426 """Called on avatar update events 418 idx += 1
427 419 if not self._first_entry.displayed:
428 @param jid_: jid of the entity with updated avatar 420 self.vpanel.insert(self._first_entry, 0)
429 @param hash_: hash of the avatar 421 self._first_entry.displayed = True
430 @param profile: %(doc_profile)s 422
431 """ 423 for entry in self.entries:
432 whoami = self.host.profiles[self.profile].whoami 424 if not entry.displayed:
433 if self.isJidAccepted(jid_) or jid_.bare == whoami.bare: 425 self.vpanel.insert(entry, idx)
434 self.updateValue('avatar', jid_, hash_) 426 entry.displayed = True
435 427 idx += 1
436 def addNewMessageEntry(self): 428
437 """Add an empty entry for writing a new message if needed.""" 429 # def onDelete(self):
438 if self.getNewMainEntry(): 430 # quick_widgets.QuickWidget.onDelete(self)
439 return # there's already one 431 # self.host.removeListener('avatar', self.avatarListener)
440 data = {'id': unicode(time()), 432
441 'new': True, 433 # def onAvatarUpdate(self, jid_, hash_, profile):
442 'author': unicode(self.host.whoami.bare), 434 # """Called on avatar update events
443 } 435
444 entry = self.addEntry(data, update_header=False) 436 # @param jid_: jid of the entity with updated avatar
445 entry.edit(True) 437 # @param hash_: hash of the avatar
446 438 # @param profile: %(doc_profile)s
447 def getNewMainEntry(self): 439 # """
448 """Get the new entry being edited, or None if it doesn't exists. 440 # whoami = self.host.profiles[self.profile].whoami
449 441 # if self.isJidAccepted(jid_) or jid_.bare == whoami.bare:
450 @return (MicroblogEntry): the new entry being edited. 442 # self.updateValue('avatar', jid_, hash_)
451 """
452 if len(self.vpanel.children) < 2:
453 return None # there's only the footer
454 first = self.vpanel.children[0]
455 assert(first.type == 'main_item')
456 return first if first.empty else None
457 443
458 @staticmethod 444 @staticmethod
459 def onGroupDrop(host, targets): 445 def onGroupDrop(host, targets):
460 """Create a microblog panel for one, several or all contact groups. 446 """Create a microblog panel for one, several or all contact groups.
461 447
462 @param host (SatWebFrontend): the SatWebFrontend instance 448 @param host (SatWebFrontend): the SatWebFrontend instance
463 @param targets (tuple(unicode)): tuple of groups (empty for "all groups") 449 @param targets (tuple(unicode)): tuple of groups (empty for "all groups")
464 @return: the created MicroblogPanel 450 @return: the created MicroblogPanel
465 """ 451 """
466 # XXX: pyjamas doesn't support use of cls directly 452 # XXX: pyjamas doesn't support use of cls directly
467 widget = host.displayWidget(MicroblogPanel, targets, dropped=True) 453 widget = host.displayWidget(Blog, targets, dropped=True)
468 widget.loadMoreMainEntries()
469 return widget 454 return widget
470 455
471 @property 456 # @property
472 def accepted_groups(self): 457 # def accepted_groups(self):
473 """Return a set of the accepted groups""" 458 # """Return a set of the accepted groups"""
474 return set().union(*self.targets) 459 # return set().union(*self.targets)
475
476 def loadAllCommentsForEntry(self, main_entry):
477 """Load all the comments for the given main entry.
478
479 @param main_entry (MicroblogEntry): main entry having comments.
480 """
481 index = str(main_entry.comments_count - main_entry.hidden_count)
482 rsm = {'max_': str(main_entry.hidden_count), 'index': index}
483 self.host.bridge.call('getMblogComments', self.mblogsInsert, main_entry.comments_service, main_entry.comments_node, rsm)
484
485 def loadMoreMainEntries(self):
486 if self.footer.waiting:
487 return
488 self.footer.waiting = True
489 self.footer.setHTML("loading...")
490
491 self.host.loadOurMainEntries(self.next_rsm_index, self)
492
493 type_ = 'ALL' if self.accepted_groups == [] else 'GROUP'
494 rsm = {'max_': str(C.RSM_MAX_ITEMS), 'index': str(self.next_rsm_index)}
495 self.host.bridge.getMassiveMblogs(type_, list(self.accepted_groups), rsm, profile=C.PROF_KEY_NONE, callback=self.massiveInsert)
496 460
497 def getWarningData(self, comment): 461 def getWarningData(self, comment):
498 """ 462 """
499 @param comment: set to True if the composed message is a comment 463 @param comment: set to True if the composed message is a comment
500 @return: a couple (type, msg) for calling self.host.showWarning""" 464 @return: a couple (type, msg) for calling self.host.showWarning"""
501 if comment: 465 if comment:
502 return ("PUBLIC", "This is a <span class='warningTarget'>comment</span> and keep the initial post visibility, so it is potentialy public") 466 return ("PUBLIC", "This is a <span class='warningTarget'>comment</span> and keep the initial post visibility, so it is potentialy public")
503 elif not self.accepted_groups: 467 elif self._targets_type == C.ALL:
504 # we have a meta MicroblogPanel, we publish publicly 468 # we have a meta MicroblogPanel, we publish publicly
505 return ("PUBLIC", self.warning_msg_public) 469 return ("PUBLIC", self.warning_msg_public)
506 else: 470 else:
507 # FIXME: manage several groups 471 # FIXME: manage several groups
508 return ("GROUP", self.warning_msg_group % ' '.join(self.accepted_groups)) 472 return (self._targets_type, self.warning_msg_group % ' '.join(self.targets))
509
510 def onTextEntered(self, text):
511 if not self.accepted_groups:
512 # we are entering a public microblog
513 self.bridge.call("sendMblog", None, "PUBLIC", (), text, {})
514 else:
515 self.bridge.call("sendMblog", None, "GROUP", tuple(self.accepted_groups), text, {})
516
517 def accept_all(self):
518 return not self.accepted_groups # we accept every microblog only if we are not filtering by groups
519
520 def getEntries(self):
521 """Ask all the entries for the currenly accepted groups,
522 and fill the panel"""
523
524 def massiveInsert(self, mblogs):
525 """Insert several microblogs at once
526
527 @param mblogs (dict): dictionary mapping a publisher to microblogs data:
528 - key: publisher (str)
529 - value: couple (list[dict], dict) with:
530 - list of microblogs data
531 - RSM response data
532 """
533 count_pub = len(mblogs)
534 count_msg = sum([len(value) for value in mblogs.values()])
535 log.debug(u"massive insertion of {count_msg} blogs for {count_pub} contacts".format(count_msg=count_msg, count_pub=count_pub))
536 for publisher in mblogs:
537 log.debug(u"adding {count} blogs for [{publisher}]".format(count=len(mblogs[publisher]), publisher=publisher))
538 self.mblogsInsert(mblogs[publisher])
539 self.next_rsm_index += C.RSM_MAX_ITEMS
540 self.footer.waiting = False
541 self.footer.setHTML('show older messages')
542
543 def mblogsInsert(self, mblogs):
544 """ Insert several microblogs from the same node at once.
545
546 @param mblogs (list): couple (list[dict], dict) with:
547 - list of microblogs data
548 - RSM response data
549 """
550 mblogs, rsm = mblogs
551
552 for mblog in mblogs:
553 if "content" not in mblog:
554 log.warning(u"No content found in microblog [%s]" % mblog)
555 continue
556 self.addEntry(mblog, update_header=False)
557
558 hashes = set([(entry['service'], entry['node']) for entry in mblogs if entry['type'] == 'comment'])
559 assert(len(hashes) < 2) # ensure the blogs come from the same node
560 if len(hashes) == 1:
561 main_entry = self.comments[hashes.pop()]
562 try:
563 count = int(rsm['count'])
564 hidden = count - (int(rsm['index']) + len(mblogs))
565 main_entry.updateHeader(count, hidden)
566 except KeyError: # target pubsub server doesn't support RSM
567 pass
568
569 def _chronoInsert(self, vpanel, entry, reverse=True):
570 """ Insert an entry in chronological order
571 @param vpanel: VerticalPanel instance
572 @param entry: MicroblogEntry
573 @param reverse: more recent entry on top if True, chronological order else"""
574 # XXX: for now we can't use "published" timestamp because the entries
575 # are retrieved using the "updated" field. We don't want new items
576 # inserted with RSM to be inserted "randomly" in the panel, they
577 # should be added at the bottom of the list.
578 assert(isinstance(reverse, bool))
579 if entry.empty:
580 entry.updated = time()
581 # we look for the right index to insert our entry:
582 # if reversed, we insert the entry above the first entry
583 # in the past
584 idx = 0
585
586 for child in vpanel.children[0:-1]: # ignore the footer
587 if not isinstance(child, MicroblogEntry):
588 idx += 1
589 continue
590 condition_to_stop = child.empty or (child.updated > entry.updated)
591 if condition_to_stop != reverse: # != is XOR
592 break
593 idx += 1
594
595 vpanel.insert(entry, idx)
596
597 def addEntryIfAccepted(self, sender, groups, mblog_entry):
598 """Check if an entry can go in MicroblogPanel and add to it
599
600 @param sender(jid.JID): jid of the entry sender
601 @param groups: groups which can receive this entry
602 @param mblog_entry: panels.MicroblogItem instance
603 """
604 assert isinstance(sender, jid.JID) # FIXME temporary
605 if (mblog_entry.type == "comment"
606 or self.isJidAccepted(sender)
607 or (groups is None and sender == self.host.profiles[self.profile].whoami.bare)
608 or (groups and groups.intersection(self.accepted_groups))):
609 self.addEntry(mblog_entry)
610
611 def addEntry(self, data, update_header=True):
612 """Add an entry to the panel
613
614 @param data (dict): dict containing the item data
615 @param update_header (bool): update or not the main comment header
616 @return: the added MicroblogEntry instance, or None
617 """
618 _entry = MicroblogEntry(self, data)
619 if _entry.type == "comment":
620 comments_hash = (_entry.service, _entry.node)
621 if comments_hash not in self.comments:
622 # The comments node is not known in this panel
623 return None
624 parent = self.comments[comments_hash]
625 parent_idx = self.vpanel.getWidgetIndex(parent)
626 # we find or create the panel where the comment must be inserted
627 try:
628 sub_panel = self.vpanel.getWidget(parent_idx + 1)
629 except IndexError:
630 sub_panel = None
631 if not sub_panel or not isinstance(sub_panel, VerticalPanel):
632 sub_panel = VerticalPanel()
633 sub_panel.setStyleName('microblogPanel')
634 sub_panel.addStyleName('subPanel')
635 self.vpanel.insert(sub_panel, parent_idx + 1)
636
637 for idx in xrange(0, len(sub_panel.getChildren())):
638 comment = sub_panel.getIndexedChild(idx)
639 if comment.id == _entry.id:
640 # update an existing comment
641 sub_panel.remove(comment)
642 sub_panel.insert(_entry, idx)
643 return _entry
644 # we want comments to be inserted in chronological order
645 self._chronoInsert(sub_panel, _entry, reverse=False)
646 if update_header:
647 parent.updateHeader(inc=+1)
648 return _entry
649
650 if _entry.comments:
651 # entry has comments, we keep the comments service/node as a reference
652 comments_hash = (_entry.comments_service, _entry.comments_node)
653 self.comments[comments_hash] = _entry
654 self.host.bridge.call('getMblogComments', self.mblogsInsert, _entry.comments_service, _entry.comments_node)
655
656 if _entry.id in self.entries: # update
657 old_entry = self.entries[_entry.id]
658 idx = self.vpanel.getWidgetIndex(old_entry)
659 counts = (old_entry.comments_count, old_entry.hidden_count)
660 self.vpanel.remove(old_entry)
661 self.vpanel.insert(_entry, idx)
662 _entry.updateHeader(*counts)
663 else: # new entry
664 self._chronoInsert(self.vpanel, _entry)
665 if _entry.comments:
666 self.host.bridge.call('getMblogComments', self.mblogsInsert, _entry.comments_service, _entry.comments_node)
667
668 self.entries[_entry.id] = _entry
669
670 return _entry
671
672 def removeEntry(self, type_, id_, update_header=True):
673 """Remove an entry from the panel
674
675 @param type_ (str): entry type ('main_item' or 'comment')
676 @param id_ (str): entry id
677 @param update_header (bool): update or not the main comment header
678 """
679 for child in self.vpanel.getChildren():
680 if isinstance(child, MicroblogEntry) and type_ == 'main_item':
681 if child.id == id_:
682 main_idx = self.vpanel.getWidgetIndex(child)
683 try:
684 sub_panel = self.vpanel.getWidget(main_idx + 1)
685 if isinstance(sub_panel, VerticalPanel):
686 sub_panel.removeFromParent()
687 except IndexError:
688 pass
689 child.removeFromParent()
690 break
691 elif isinstance(child, VerticalPanel) and type_ == 'comment':
692 for comment in child.getChildren():
693 if comment.id == id_:
694 if update_header:
695 hash_ = (comment.service, comment.node)
696 self.comments[hash_].updateHeader(inc=-1)
697 comment.removeFromParent()
698 break
699 473
700 def ensureVisible(self, entry): 474 def ensureVisible(self, entry):
701 """Scroll to an entry to ensure its visibility 475 """Scroll to an entry to ensure its visibility
702 476
703 @param entry (MicroblogEntry): the entry 477 @param entry (MicroblogEntry): the entry
704 """ 478 """
705 try: 479 current = entry
706 self.vpanel.getParent().ensureVisible(entry) # scroll to the clicked entry 480 while True:
707 except AttributeError: 481 parent = current.getParent()
708 log.warning("FIXME: MicroblogPanel.vpanel should be wrapped in a ScrollPanel!") 482 if parent is None:
483 log.warning("Can't find any parent ScrollPanel")
484 return
485 elif isinstance(parent, ScrollPanel):
486 parent.ensureVisible(current)
487 return
488 else:
489 current = parent
709 490
710 def setSelectedEntry(self, entry, ensure_visible=False): 491 def setSelectedEntry(self, entry, ensure_visible=False):
711 """Select an entry. 492 """Select an entry.
712 493
713 @param entry (MicroblogEntry): the entry to select 494 @param entry (MicroblogEntry): the entry to select
718 499
719 entry.addStyleName('selected_entry') # blink the clicked entry 500 entry.addStyleName('selected_entry') # blink the clicked entry
720 clicked_entry = entry # entry may be None when the timer is done 501 clicked_entry = entry # entry may be None when the timer is done
721 Timer(500, lambda timer: clicked_entry.removeStyleName('selected_entry')) 502 Timer(500, lambda timer: clicked_entry.removeStyleName('selected_entry'))
722 503
723 def updateValue(self, type_, jid_, value): 504 # def updateValue(self, type_, jid_, value):
724 """Update a jid value in entries 505 # """Update a jid value in entries
725 506
726 @param type_: one of 'avatar', 'nick' 507 # @param type_: one of 'avatar', 'nick'
727 @param jid_(jid.JID): jid concerned 508 # @param jid_(jid.JID): jid concerned
728 @param value: new value""" 509 # @param value: new value"""
729 assert isinstance(jid_, jid.JID) # FIXME: temporary 510 # assert isinstance(jid_, jid.JID) # FIXME: temporary
730 def updateVPanel(vpanel): 511 # def updateVPanel(vpanel):
731 avatar_url = self.host.getAvatarURL(jid_) 512 # avatar_url = self.host.getAvatarURL(jid_)
732 for child in vpanel.children: 513 # for child in vpanel.children:
733 if isinstance(child, MicroblogEntry) and child.author == jid_: 514 # if isinstance(child, MicroblogEntry) and child.author == jid_:
734 child.updateAvatar(avatar_url) 515 # child.updateAvatar(avatar_url)
735 elif isinstance(child, VerticalPanel): 516 # elif isinstance(child, VerticalPanel):
736 updateVPanel(child) 517 # updateVPanel(child)
737 if type_ == 'avatar': 518 # if type_ == 'avatar':
738 updateVPanel(self.vpanel) 519 # updateVPanel(self.vpanel)
739 520
740 def addAcceptedGroups(self, groups): 521 # def onClick(self, sender):
741 """Add one or more group(s) which can be displayed in this panel. 522 # if sender == self.footer:
742 523 # self.loadMoreMainEntries()
743 @param groups (tuple(unicode)): tuple of groups to add 524
744 """ 525 # def onMouseEnter(self, sender):
745 # FIXME: update the widget's hash in QuickApp._widgets[MicroblogPanel] 526 # if sender == self.footer:
746 self.targets.update(groups) 527 # self.loadMoreMainEntries()
747 528
748 def isJidAccepted(self, jid_): 529
749 """Tell if a jid is actepted and must be shown in this panel 530 # libervia_widget.LiberviaWidget.addDropKey("GROUP", lambda host, item: MicroblogPanel.onGroupDrop(host, (item,)))
750 531 #
751 @param jid_(jid.JID): jid to check 532 # # Needed for the drop keys to not be mixed between meta panel and panel for "Contacts" group
752 @return: True if the jid is accepted 533 libervia_widget.LiberviaWidget.addDropKey("CONTACT_TITLE", lambda host, item: Blog.onGroupDrop(host, ()))
753 """ 534 quick_blog.registerClass("ENTRY", Entry)
754 assert isinstance(jid_, jid.JID) # FIXME temporary 535 quick_widgets.register(quick_blog.QuickBlog, Blog)
755 if self.accept_all():
756 return True
757 for group in self.accepted_groups:
758 if self.host.contact_lists[self.profile].isEntityInGroup(jid_, group):
759 return True
760 return False
761
762 def onClick(self, sender):
763 if sender == self.footer:
764 self.loadMoreMainEntries()
765
766 def onMouseEnter(self, sender):
767 if sender == self.footer:
768 self.loadMoreMainEntries()
769
770
771 libervia_widget.LiberviaWidget.addDropKey("GROUP", lambda host, item: MicroblogPanel.onGroupDrop(host, (item,)))
772
773 # Needed for the drop keys to not be mixed between meta panel and panel for "Contacts" group
774 libervia_widget.LiberviaWidget.addDropKey("CONTACT_TITLE", lambda host, item: MicroblogPanel.onGroupDrop(host, ()))