Mercurial > libervia-web
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, ())) |