Mercurial > libervia-web
comparison browser/sat_browser/blog.py @ 1124:28e3eb3bb217
files reorganisation and installation rework:
- files have been reorganised to follow other SàT projects and usual Python organisation (no more "/src" directory)
- VERSION file is now used, as for other SàT projects
- replace the overcomplicated setup.py be a more sane one. Pyjamas part is not compiled anymore by setup.py, it must be done separatly
- removed check for data_dir if it's empty
- installation tested working in virtual env
- libervia launching script is now in bin/libervia
author | Goffi <goffi@goffi.org> |
---|---|
date | Sat, 25 Aug 2018 17:59:48 +0200 |
parents | src/browser/sat_browser/blog.py@f2170536ba23 |
children | 2af117bfe6cc |
comparison
equal
deleted
inserted
replaced
1123:63a4b8fe9782 | 1124:28e3eb3bb217 |
---|---|
1 #!/usr/bin/python | |
2 # -*- coding: utf-8 -*- | |
3 | |
4 # Libervia: a Salut à Toi frontend | |
5 # Copyright (C) 2011-2018 Jérôme Poisson <goffi@goffi.org> | |
6 | |
7 # This program is free software: you can redistribute it and/or modify | |
8 # it under the terms of the GNU Affero General Public License as published by | |
9 # the Free Software Foundation, either version 3 of the License, or | |
10 # (at your option) any later version. | |
11 | |
12 # This program is distributed in the hope that it will be useful, | |
13 # but WITHOUT ANY WARRANTY; without even the implied warranty of | |
14 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | |
15 # GNU Affero General Public License for more details. | |
16 | |
17 # You should have received a copy of the GNU Affero General Public License | |
18 # along with this program. If not, see <http://www.gnu.org/licenses/>. | |
19 | |
20 import pyjd # this is dummy in pyjs | |
21 from sat.core.log import getLogger | |
22 log = getLogger(__name__) | |
23 from sat.tools.common import data_format | |
24 from sat.core.i18n import _ #, D_ | |
25 | |
26 from pyjamas.ui.SimplePanel import SimplePanel | |
27 from pyjamas.ui.VerticalPanel import VerticalPanel | |
28 from pyjamas.ui.ScrollPanel import ScrollPanel | |
29 from pyjamas.ui.HorizontalPanel import HorizontalPanel | |
30 from pyjamas.ui.Label import Label | |
31 from pyjamas.ui.HTML import HTML | |
32 from pyjamas.ui.Image import Image | |
33 from pyjamas.ui.ClickListener import ClickHandler | |
34 from pyjamas.ui.FlowPanel import FlowPanel | |
35 from pyjamas.ui import KeyboardListener as keyb | |
36 from pyjamas.ui.KeyboardListener import KeyboardHandler | |
37 from pyjamas.ui.FocusListener import FocusHandler | |
38 from pyjamas.ui.MouseListener import MouseHandler | |
39 from pyjamas.Timer import Timer | |
40 | |
41 from datetime import datetime | |
42 | |
43 import html_tools | |
44 import dialog | |
45 import richtext | |
46 import editor_widget | |
47 import libervia_widget | |
48 from constants import Const as C | |
49 from sat_frontends.quick_frontend import quick_widgets | |
50 from sat_frontends.quick_frontend import quick_blog | |
51 | |
52 unicode = str # XXX: pyjamas doesn't manage unicode | |
53 ENTRY_RICH = (C.ENTRY_MODE_RICH, C.ENTRY_MODE_XHTML) | |
54 | |
55 | |
56 class Entry(quick_blog.Entry, VerticalPanel, ClickHandler, FocusHandler, KeyboardHandler): | |
57 """Graphical representation of a quick_blog.Item""" | |
58 | |
59 def __init__(self, manager, item_data=None, comments_data=None, service=None, node=None): | |
60 quick_blog.Entry.__init__(self, manager, item_data, comments_data, service, node) | |
61 | |
62 VerticalPanel.__init__(self) | |
63 | |
64 self.panel = FlowPanel() | |
65 self.panel.setStyleName('mb_entry') | |
66 | |
67 self.header = HorizontalPanel(StyleName='mb_entry_header') | |
68 self.panel.add(self.header) | |
69 | |
70 self.entry_actions = VerticalPanel() | |
71 self.entry_actions.setStyleName('mb_entry_actions') | |
72 self.panel.add(self.entry_actions) | |
73 | |
74 entry_avatar = SimplePanel() | |
75 entry_avatar.setStyleName('mb_entry_avatar') | |
76 author_jid = self.author_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 | |
79 entry_avatar.add(self.avatar) | |
80 self.panel.add(entry_avatar) | |
81 | |
82 self.entry_dialog = VerticalPanel() | |
83 self.entry_dialog.setStyleName('mb_entry_dialog') | |
84 self.panel.add(self.entry_dialog) | |
85 | |
86 self.comments_panel = None | |
87 self._current_comment = None | |
88 | |
89 self.add(self.panel) | |
90 ClickHandler.__init__(self) | |
91 self.addClickListener(self) | |
92 | |
93 self.refresh() | |
94 self.displayed = False # True when entry is added to parent | |
95 if comments_data: | |
96 self.addComments(comments_data) | |
97 | |
98 def refresh(self): | |
99 self.comment_label = None | |
100 self.update_label = None | |
101 self.delete_label = None | |
102 self.header.clear() | |
103 self.entry_dialog.clear() | |
104 self.entry_actions.clear() | |
105 self._setHeader() | |
106 self._setBubble() | |
107 self._setIcons() | |
108 | |
109 def _setHeader(self): | |
110 """Set the entry header.""" | |
111 if not self.new: | |
112 author = html_tools.html_sanitize(unicode(self.item.author)) | |
113 author_jid = html_tools.html_sanitize(unicode(self.item.author_jid)) | |
114 if author_jid and not self.item.author_verified: | |
115 author_jid += u' <span style="color:red; font-weight: bold;">⚠</span>' | |
116 if author: | |
117 author += " <%s>" % author_jid | |
118 elif author_jid: | |
119 author = author_jid | |
120 else: | |
121 author = _("<unknown author>") | |
122 | |
123 update_text = u" — ✍ " + "<span class='mb_entry_timestamp'>%s</span>" % datetime.fromtimestamp(self.item.updated) | |
124 self.header.add(HTML("""<span class='mb_entry_header_info'> | |
125 <span class='mb_entry_author'>%(author)s</span> on | |
126 <span class='mb_entry_timestamp'>%(published)s</span>%(updated)s | |
127 </span>""" % {'author': author, | |
128 'published': datetime.fromtimestamp(self.item.published) if self.item.published is not None else '', | |
129 'updated': update_text if self.item.published != self.item.updated else '' | |
130 })) | |
131 if self.item.comments: | |
132 self.show_comments_link = HTML('') | |
133 self.header.add(self.show_comments_link) | |
134 | |
135 def _setBubble(self): | |
136 """Set the bubble displaying the initial content.""" | |
137 content = {'text': self.item.content_xhtml if self.item.content_xhtml else self.item.content or '', | |
138 'title': self.item.title_xhtml if self.item.title_xhtml else self.item.title or ''} | |
139 data_format.iter2dict('tag', self.item.tags, content) | |
140 | |
141 if self.mode == C.ENTRY_MODE_TEXT: | |
142 # assume raw text message have no title | |
143 self.bubble = editor_widget.LightTextEditor(content, modifiedCb=self._modifiedCb, afterEditCb=self._afterEditCb, options={'no_xhtml': True}) | |
144 elif self.mode in ENTRY_RICH: | |
145 content['syntax'] = C.SYNTAX_XHTML | |
146 if self.new: | |
147 options = [] | |
148 elif self.item.author_jid == self.blog.host.whoami.bare: | |
149 options = ['update_msg'] | |
150 else: | |
151 options = ['read_only'] | |
152 self.bubble = richtext.RichTextEditor(self.blog.host, content, modifiedCb=self._modifiedCb, afterEditCb=self._afterEditCb, options=options) | |
153 else: | |
154 log.error("Bad entry mode: %s" % self.mode) | |
155 self.bubble.addStyleName("bubble") | |
156 self.entry_dialog.add(self.bubble) | |
157 self.bubble.addEditListener(self._showWarning) # FIXME: remove edit listeners | |
158 self.setEditable(self.editable) | |
159 | |
160 def _setIcons(self): | |
161 """Set the entry icons (delete, update, comment)""" | |
162 if self.new: | |
163 return | |
164 | |
165 def addIcon(label, title): | |
166 label = Label(label) | |
167 label.setTitle(title) | |
168 label.addClickListener(self) | |
169 self.entry_actions.add(label) | |
170 return label | |
171 | |
172 if self.item.comments: | |
173 self.comment_label = addIcon(u"↶", "Comment this message") | |
174 self.comment_label.setStyleName('mb_entry_action_larger') | |
175 else: | |
176 self.comment_label = None | |
177 is_publisher = self.item.author_jid == self.blog.host.whoami.bare | |
178 if is_publisher: | |
179 self.update_label = addIcon(u"✍", "Edit this message") | |
180 # TODO: add delete button if we are the owner of the node | |
181 self.delete_label = addIcon(u"✗", "Delete this message") | |
182 else: | |
183 self.update_label = self.delete_label = None | |
184 | |
185 def _createCommentsPanel(self): | |
186 """Create the panel if it doesn't exists""" | |
187 if self.comments_panel is None: | |
188 self.comments_panel = VerticalPanel() | |
189 self.comments_panel.setStyleName('microblogPanel') | |
190 self.comments_panel.addStyleName('subPanel') | |
191 self.add(self.comments_panel) | |
192 | |
193 def setEditable(self, editable=True): | |
194 """Toggle the bubble between display and edit mode. | |
195 | |
196 @param editable (bool) | |
197 """ | |
198 self.editable = editable | |
199 self.bubble.edit(self.editable) | |
200 self.updateIconsAndButtons() | |
201 | |
202 def updateIconsAndButtons(self): | |
203 """Set the visibility of the icons and the button to switch between blog and microblog.""" | |
204 try: | |
205 self.bubble_commands.removeFromParent() | |
206 except (AttributeError, TypeError): | |
207 pass | |
208 if self.editable: | |
209 if self.mode == C.ENTRY_MODE_TEXT: | |
210 html = _(u'<a style="color: blue;">switch to blog</a>') | |
211 title = _(u'compose a rich text message with a title - suitable for writing articles') | |
212 else: | |
213 html = _(u'<a style="color: blue;">switch to microblog</a>') | |
214 title = _(u'compose a short message without title - suitable for sharing news') | |
215 toggle_syntax_button = HTML(html, Title=title) | |
216 toggle_syntax_button.addClickListener(self.toggleContentSyntax) | |
217 toggle_syntax_button.addStyleName('mb_entry_toggle_syntax') | |
218 toggle_syntax_button.setStyleAttribute('top', '-20px') # XXX: need to force CSS | |
219 toggle_syntax_button.setStyleAttribute('left', '-20px') | |
220 | |
221 self.bubble_commands = HorizontalPanel(Width="100%") | |
222 | |
223 if self.mode == C.ENTRY_MODE_TEXT: | |
224 publish_button = HTML(_(u'<a style="color: blue;">shift + enter to publish</a>'), Title=_(u"... or click here")) | |
225 publish_button.addStyleName('mb_entry_publish_button') | |
226 publish_button.addClickListener(lambda dummy: self.bubble.edit(False)) | |
227 publish_button.setStyleAttribute('top', '-20px') # XXX: need to force CSS | |
228 publish_button.setStyleAttribute('left', '20px') | |
229 self.bubble_commands.add(publish_button) | |
230 | |
231 self.bubble_commands.add(toggle_syntax_button) | |
232 self.entry_dialog.add(self.bubble_commands) | |
233 | |
234 # hide these icons while editing | |
235 try: | |
236 self.delete_label.setVisible(not self.editable) | |
237 except (TypeError, AttributeError): | |
238 pass | |
239 try: | |
240 self.update_label.setVisible(not self.editable) | |
241 except (TypeError, AttributeError): | |
242 pass | |
243 try: | |
244 self.comment_label.setVisible(not self.editable) | |
245 except (TypeError, AttributeError): | |
246 pass | |
247 | |
248 def onClick(self, sender): | |
249 | |
250 if sender == self: | |
251 self.blog.setSelectedEntry(self) | |
252 elif sender == self.delete_label: | |
253 self._onRetractClick() | |
254 elif sender == self.update_label: | |
255 self.setEditable(True) | |
256 elif sender == self.comment_label: | |
257 self._onCommentClick() | |
258 # elif sender == self.show_comments_link: | |
259 # self._blog_panel.loadAllCommentsForEntry(self) | |
260 | |
261 def _modifiedCb(self, content): | |
262 """Send the new content to the backend | |
263 | |
264 @return: False to restore the original content if a deletion has been cancelled | |
265 """ | |
266 if not content['text']: # previous content has been emptied | |
267 if not self.new: | |
268 self._onRetractClick() | |
269 return False | |
270 | |
271 self.item.content = self.item.content_rich = self.item.content_xhtml = None | |
272 self.item.title = self.item.title_rich = self.item.title_xhtml = None | |
273 | |
274 if self.mode in ENTRY_RICH: | |
275 # TODO: if the user change his parameters after the message edition started, | |
276 # the message syntax could be different then the current syntax: pass the | |
277 # message syntax in mb_data for the frontend to use it instead of current syntax. | |
278 self.item.content_rich = content['text'] # XXX: this also works if the syntax is XHTML | |
279 self.item.title = content['title'] | |
280 self.item.tags = list(data_format.dict2iter('tag', content)) | |
281 else: | |
282 self.item.content = content['text'] | |
283 | |
284 self.send() | |
285 | |
286 return True | |
287 | |
288 def _afterEditCb(self, content): | |
289 """Post edition treatments | |
290 | |
291 Remove the entry if it was an empty one (used for creating a new blog post). | |
292 Data for the actual new blog post will be received from the bridge | |
293 @param content(dict): edited content | |
294 """ | |
295 if self.new: | |
296 if self.level == 0: | |
297 # we have a main item, we keep the edit entry | |
298 self.reset(None) | |
299 # FIXME: would be better to reset bubble | |
300 # but bubble.setContent() doesn't seem to work | |
301 self.bubble.removeFromParent() | |
302 self._setBubble() | |
303 else: | |
304 # we don't keep edit entries for comments | |
305 self.delete() | |
306 else: | |
307 self.editable = False | |
308 self.updateIconsAndButtons() | |
309 | |
310 def _showWarning(self, sender, keycode, modifiers): | |
311 if keycode == keyb.KEY_ENTER & keyb.MODIFIER_SHIFT: # FIXME: fix edit_listeners, it's dirty (we have to check keycode/modifiers twice !) | |
312 self.blog.host.showWarning(None, None) | |
313 else: | |
314 # self.blog.host.showWarning(*self.blog.getWarningData(self.type == 'comment')) | |
315 self.blog.host.showWarning(*self.blog.getWarningData(False)) # FIXME: comments are not yet reimplemented | |
316 | |
317 def _onRetractClick(self): | |
318 """Ask confirmation then retract current entry.""" | |
319 assert not self.new | |
320 | |
321 def confirm_cb(answer): | |
322 if answer: | |
323 self.retract() | |
324 | |
325 entry_type = _("message") if self.level == 0 else _("comment") | |
326 and_comments = _(" All comments will be also deleted!") if self.item.comments else "" | |
327 text = _("Do you really want to delete this {entry_type}?{and_comments}").format( | |
328 entry_type=entry_type, and_comments=and_comments) | |
329 dialog.ConfirmDialog(confirm_cb, text=text).show() | |
330 | |
331 def _onCommentClick(self): | |
332 """Add an empty entry for a new comment""" | |
333 if self._current_comment is None: | |
334 if not self.item.comments_service or not self.item.comments_node: | |
335 log.warning("Invalid service and node for comments, can't create a comment") | |
336 self._current_comment = self.addEntry(editable=True, service=self.item.comments_service, node=self.item.comments_node, edit_entry=True) | |
337 self.blog.setSelectedEntry(self._current_comment, True) | |
338 self._current_comment.bubble.setFocus(True) # FIXME: should be done elsewhere (automatically)? | |
339 | |
340 def _changeMode(self, original_content, text): | |
341 self.mode = C.ENTRY_MODE_RICH if self.mode == C.ENTRY_MODE_TEXT else C.ENTRY_MODE_TEXT | |
342 if self.mode in ENTRY_RICH and not text: | |
343 text = ' ' # something different than empty string is needed to initialize the rich text editor | |
344 self.item.content = text | |
345 if self.mode in ENTRY_RICH: | |
346 self.item.content_rich = text # XXX: this also works if the syntax is XHTML | |
347 self.bubble.setDisplayContent() # needed in case the edition is aborted, to not end with an empty bubble | |
348 else: | |
349 self.item.content_xhtml = '' | |
350 self.bubble.removeFromParent() | |
351 self._setBubble() | |
352 self.bubble.setOriginalContent(original_content) | |
353 | |
354 def toggleContentSyntax(self): | |
355 """Toggle the editor between raw and rich text""" | |
356 original_content = self.bubble.getOriginalContent() | |
357 rich = self.mode in ENTRY_RICH | |
358 if rich: | |
359 original_content['syntax'] = C.SYNTAX_XHTML | |
360 | |
361 text = self.bubble.getContent()['text'] | |
362 | |
363 if not text.strip(): | |
364 self._changeMode(original_content,'') | |
365 else: | |
366 if rich: | |
367 def confirm_cb(answer): | |
368 if answer: | |
369 self.blog.host.bridge.syntaxConvert(text, C.SYNTAX_CURRENT, C.SYNTAX_TEXT, profile=None, | |
370 callback=lambda converted: self._changeMode(original_content, converted)) | |
371 dialog.ConfirmDialog(confirm_cb, text=_("Do you really want to lose the title and text formatting?")).show() | |
372 else: | |
373 self.blog.host.bridge.syntaxConvert(text, C.SYNTAX_TEXT, C.SYNTAX_XHTML, profile=None, | |
374 callback=lambda converted: self._changeMode(original_content, converted)) | |
375 | |
376 def update(self, entry=None): | |
377 """Update comments""" | |
378 self._createCommentsPanel() | |
379 self.entries.sort(key=lambda entry: entry.item.published) | |
380 # we put edit_entry at the end | |
381 edit_entry = [] if self.edit_entry is None else [self.edit_entry] | |
382 for idx, entry in enumerate(self.entries + edit_entry): | |
383 if not entry.displayed: | |
384 self.comments_panel.insert(entry, idx) | |
385 entry.displayed = True | |
386 | |
387 def delete(self): | |
388 quick_blog.Entry.delete(self) | |
389 | |
390 # _current comment is specific to libervia, we remove it | |
391 if isinstance(self.manager, Entry): | |
392 self.manager._current_comment = None | |
393 | |
394 # now we remove the pyjamas widgets | |
395 parent = self.parent | |
396 assert isinstance(parent, VerticalPanel) | |
397 self.removeFromParent() | |
398 if not parent.children: | |
399 # the vpanel is empty, we remove it | |
400 parent.removeFromParent() | |
401 try: | |
402 if self.manager.comments_panel == parent: | |
403 self.manager.comments_panel = None | |
404 except AttributeError: | |
405 assert isinstance(self.manager, quick_blog.QuickBlog) | |
406 | |
407 | |
408 class Blog(quick_blog.QuickBlog, libervia_widget.LiberviaWidget, MouseHandler): | |
409 """Panel used to show microblog""" | |
410 warning_msg_public = "This message will be <b>PUBLIC</b> and everybody will be able to see it, even people you don't know" | |
411 warning_msg_group = "This message will be published for all the people of the following groups: <span class='warningTarget'>%s</span>" | |
412 | |
413 def __init__(self, host, targets, profiles=None): | |
414 quick_blog.QuickBlog.__init__(self, host, targets, C.PROF_KEY_NONE) | |
415 title = ", ".join(targets) if targets else "Blog" | |
416 libervia_widget.LiberviaWidget.__init__(self, host, title, selectable=True) | |
417 MouseHandler.__init__(self) | |
418 self.vpanel = VerticalPanel() | |
419 self.vpanel.setStyleName('microblogPanel') | |
420 self.setWidget(self.vpanel) | |
421 if ((self._targets_type == C.ALL and self.host.mblog_available) or | |
422 (self._targets_type == C.GROUP and self.host.groupblog_available)): | |
423 self.addEntry(editable=True, edit_entry=True) | |
424 | |
425 self.getAll() | |
426 | |
427 # self.footer = HTML('', StyleName='microblogPanel_footer') | |
428 # self.footer.waiting = False | |
429 # self.footer.addClickListener(self) | |
430 # self.footer.addMouseListener(self) | |
431 # self.vpanel.add(self.footer) | |
432 # self.next_rsm_index = 0 | |
433 | |
434 def __str__(self): | |
435 return u"Blog Widget [targets: {}, profile: {}]".format(", ".join(self.targets) if self.targets else "meta blog", self.profile) | |
436 | |
437 def update(self): | |
438 self.entries.sort(key=lambda entry: entry.item.published, reverse=True) | |
439 | |
440 start_idx = 0 | |
441 if self.edit_entry is not None: | |
442 start_idx = 1 | |
443 if not self.edit_entry.displayed: | |
444 self.vpanel.insert(self.edit_entry, 0) | |
445 self.edit_entry.displayed = True | |
446 | |
447 # XXX: enumerate is buggued in pyjamas (start is not used) | |
448 # we have to use idx | |
449 idx = start_idx | |
450 for entry in self.entries: | |
451 if not entry.displayed: | |
452 self.vpanel.insert(entry, idx) | |
453 entry.displayed = True | |
454 idx += 1 | |
455 | |
456 # def onDelete(self): | |
457 # quick_widgets.QuickWidget.onDelete(self) | |
458 # self.host.removeListener('avatar', self.avatarListener) | |
459 | |
460 # def onAvatarUpdate(self, jid_, hash_, profile): | |
461 # """Called on avatar update events | |
462 | |
463 # @param jid_: jid of the entity with updated avatar | |
464 # @param hash_: hash of the avatar | |
465 # @param profile: %(doc_profile)s | |
466 # """ | |
467 # whoami = self.host.profiles[self.profile].whoami | |
468 # if self.isJidAccepted(jid_) or jid_.bare == whoami.bare: | |
469 # self.updateValue('avatar', jid_, hash_) | |
470 | |
471 @staticmethod | |
472 def onGroupDrop(host, targets): | |
473 """Create a microblog panel for one, several or all contact groups. | |
474 | |
475 @param host (SatWebFrontend): the SatWebFrontend instance | |
476 @param targets (tuple(unicode)): tuple of groups (empty for "all groups") | |
477 @return: the created MicroblogPanel | |
478 """ | |
479 # XXX: pyjamas doesn't support use of cls directly | |
480 widget = host.displayWidget(Blog, targets, dropped=True) | |
481 return widget | |
482 | |
483 # @property | |
484 # def accepted_groups(self): | |
485 # """Return a set of the accepted groups""" | |
486 # return set().union(*self.targets) | |
487 | |
488 def getWarningData(self, comment): | |
489 """ | |
490 @param comment: set to True if the composed message is a comment | |
491 @return: a couple (type, msg) for calling self.host.showWarning""" | |
492 if comment: | |
493 return ("PUBLIC", "This is a <span class='warningTarget'>comment</span> and keep the initial post visibility, so it is potentialy public") | |
494 elif self._targets_type == C.ALL: | |
495 # we have a meta MicroblogPanel, we publish publicly | |
496 return ("PUBLIC", self.warning_msg_public) | |
497 else: | |
498 # FIXME: manage several groups | |
499 return (self._targets_type, self.warning_msg_group % ' '.join(self.targets)) | |
500 | |
501 def ensureVisible(self, entry): | |
502 """Scroll to an entry to ensure its visibility | |
503 | |
504 @param entry (MicroblogEntry): the entry | |
505 """ | |
506 current = entry | |
507 while True: | |
508 parent = current.getParent() | |
509 if parent is None: | |
510 log.warning("Can't find any parent ScrollPanel") | |
511 return | |
512 elif isinstance(parent, ScrollPanel): | |
513 parent.ensureVisible(entry) | |
514 return | |
515 else: | |
516 current = parent | |
517 | |
518 def setSelectedEntry(self, entry, ensure_visible=False): | |
519 """Select an entry. | |
520 | |
521 @param entry (MicroblogEntry): the entry to select | |
522 @param ensure_visible (boolean): if True, also scroll to the entry | |
523 """ | |
524 if ensure_visible: | |
525 self.ensureVisible(entry) | |
526 | |
527 entry.addStyleName('selected_entry') # blink the clicked entry | |
528 clicked_entry = entry # entry may be None when the timer is done | |
529 Timer(500, lambda timer: clicked_entry.removeStyleName('selected_entry')) | |
530 | |
531 # def updateValue(self, type_, jid_, value): | |
532 # """Update a jid value in entries | |
533 | |
534 # @param type_: one of 'avatar', 'nick' | |
535 # @param jid_(jid.JID): jid concerned | |
536 # @param value: new value""" | |
537 # assert isinstance(jid_, jid.JID) # FIXME: temporary | |
538 # def updateVPanel(vpanel): | |
539 # avatar_url = self.host.getAvatarURL(jid_) | |
540 # for child in vpanel.children: | |
541 # if isinstance(child, MicroblogEntry) and child.author == jid_: | |
542 # child.updateAvatar(avatar_url) | |
543 # elif isinstance(child, VerticalPanel): | |
544 # updateVPanel(child) | |
545 # if type_ == 'avatar': | |
546 # updateVPanel(self.vpanel) | |
547 | |
548 # def onClick(self, sender): | |
549 # if sender == self.footer: | |
550 # self.loadMoreMainEntries() | |
551 | |
552 # def onMouseEnter(self, sender): | |
553 # if sender == self.footer: | |
554 # self.loadMoreMainEntries() | |
555 | |
556 | |
557 libervia_widget.LiberviaWidget.addDropKey("GROUP", lambda host, item: Blog.onGroupDrop(host, (item,))) | |
558 libervia_widget.LiberviaWidget.addDropKey("CONTACT_TITLE", lambda host, item: Blog.onGroupDrop(host, ())) | |
559 quick_blog.registerClass("ENTRY", Entry) | |
560 quick_widgets.register(quick_blog.QuickBlog, Blog) |