Mercurial > libervia-web
comparison src/browser/sat_browser/contact_list.py @ 679:a90cc8fc9605
merged branch frontends_multi_profiles
author | Goffi <goffi@goffi.org> |
---|---|
date | Wed, 18 Mar 2015 16:15:18 +0100 |
parents | e489218886d7 |
children | e876f493dccc |
comparison
equal
deleted
inserted
replaced
590:1bffc4c244c3 | 679:a90cc8fc9605 |
---|---|
1 #!/usr/bin/python | |
2 # -*- coding: utf-8 -*- | |
3 | |
4 # Libervia: a Salut à Toi frontend | |
5 # Copyright (C) 2011, 2012, 2013, 2014 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_frontends.quick_frontend.quick_contact_list import QuickContactList | |
24 from pyjamas.ui.SimplePanel import SimplePanel | |
25 from pyjamas.ui.ScrollPanel import ScrollPanel | |
26 from pyjamas.ui.VerticalPanel import VerticalPanel | |
27 from pyjamas.ui.ClickListener import ClickHandler | |
28 from pyjamas.ui.Label import Label | |
29 from pyjamas import Window | |
30 from pyjamas import DOM | |
31 | |
32 from constants import Const as C | |
33 import libervia_widget | |
34 import contact_panel | |
35 import blog | |
36 import chat | |
37 | |
38 unicode = str # XXX: pyjama doesn't manage unicode | |
39 | |
40 | |
41 def buildPresenceStyle(presence, base_style=None): | |
42 """Return the CSS classname to be used for displaying the given presence information. | |
43 | |
44 @param presence (unicode): presence is a value in ('', 'chat', 'away', 'dnd', 'xa') | |
45 @param base_style (unicode): base classname | |
46 @return: unicode | |
47 """ | |
48 if not base_style: | |
49 base_style = "contactLabel" | |
50 return '%s-%s' % (base_style, presence or 'connected') | |
51 | |
52 | |
53 def setPresenceStyle(widget, presence, base_style=None): | |
54 """ | |
55 Set the CSS style of a contact's element according to its presence. | |
56 | |
57 @param widget (Widget): the UI element of the contact | |
58 @param presence (unicode): a value in ("", "chat", "away", "dnd", "xa"). | |
59 @param base_style (unicode): the base name of the style to apply | |
60 """ | |
61 if not hasattr(widget, 'presence_style'): | |
62 widget.presence_style = None | |
63 style = buildPresenceStyle(presence, base_style) | |
64 if style == widget.presence_style: | |
65 return | |
66 if widget.presence_style is not None: | |
67 widget.removeStyleName(widget.presence_style) | |
68 widget.addStyleName(style) | |
69 widget.presence_style = style | |
70 | |
71 | |
72 class GroupLabel(libervia_widget.DragLabel, Label, ClickHandler): | |
73 def __init__(self, host, group): | |
74 """ | |
75 | |
76 @param host (SatWebFrontend) | |
77 @param group (unicode): group name | |
78 """ | |
79 self.group = group | |
80 Label.__init__(self, group) # , Element=DOM.createElement('div') | |
81 self.setStyleName('group') | |
82 libervia_widget.DragLabel.__init__(self, group, "GROUP", host) | |
83 ClickHandler.__init__(self) | |
84 self.addClickListener(self) | |
85 | |
86 def onClick(self, sender): | |
87 self.host.displayWidget(blog.MicroblogPanel, (self.group,)) | |
88 | |
89 | |
90 class GroupPanel(VerticalPanel): | |
91 | |
92 def __init__(self, parent): | |
93 VerticalPanel.__init__(self) | |
94 self.setStyleName('groupPanel') | |
95 self._parent = parent | |
96 self._groups = set() | |
97 | |
98 def add(self, group): | |
99 if group in self._groups: | |
100 log.warning("trying to add an already existing group") | |
101 return | |
102 _item = GroupLabel(self._parent.host, group) | |
103 _item.addMouseListener(self._parent) | |
104 DOM.setStyleAttribute(_item.getElement(), "cursor", "pointer") | |
105 index = 0 | |
106 for group_ in [child.group for child in self.getChildren()]: | |
107 if group_ > group: | |
108 break | |
109 index += 1 | |
110 VerticalPanel.insert(self, _item, index) | |
111 self._groups.add(group) | |
112 | |
113 def remove(self, group): | |
114 for wid in self: | |
115 if isinstance(wid, GroupLabel) and wid.group == group: | |
116 VerticalPanel.remove(self, wid) | |
117 self._groups.remove(group) | |
118 return | |
119 log.warning("Trying to remove a non existent group") | |
120 | |
121 def getGroupBox(self, group): | |
122 """get the widget of a group | |
123 | |
124 @param group (unicode): the group | |
125 @return: GroupLabel instance if present, else None""" | |
126 for wid in self: | |
127 if isinstance(wid, GroupLabel) and wid.group == group: | |
128 return wid | |
129 return None | |
130 | |
131 def getGroups(self): | |
132 return self._groups | |
133 | |
134 | |
135 class ContactsPanel(contact_panel.ContactsPanel): | |
136 """The contact list that is displayed on the left side.""" | |
137 | |
138 def __init__(self, host): | |
139 | |
140 def on_click(contact_jid): | |
141 self.host.displayWidget(chat.Chat, contact_jid, type_=C.CHAT_ONE2ONE) | |
142 | |
143 contact_panel.ContactsPanel.__init__(self, host, contacts_click=on_click, | |
144 contacts_menus=(C.MENU_JID_CONTEXT, C.MENU_ROSTER_JID_CONTEXT)) | |
145 | |
146 def setState(self, jid_, type_, state): | |
147 """Change the appearance of the contact, according to the state | |
148 | |
149 @param jid_ (jid.JID): jid.JID which need to change state | |
150 @param type_ (unicode): one of "availability", "messageWaiting" | |
151 @param state: | |
152 - for messageWaiting type: | |
153 True if message are waiting | |
154 - for availability type: | |
155 C.PRESENCE_UNAVAILABLE or None if not connected, else presence like RFC6121 #4.7.2.1""" | |
156 assert type_ in ('availability', 'messageWaiting') | |
157 contact_box = self.getContactBox(jid_) | |
158 if type_ == 'availability': | |
159 if state is None: | |
160 state = C.PRESENCE_UNAVAILABLE | |
161 setPresenceStyle(contact_box.label, state) | |
162 elif type_ == 'messageWaiting': | |
163 contact_box.setAlert(state) | |
164 | |
165 | |
166 class ContactTitleLabel(libervia_widget.DragLabel, Label, ClickHandler): | |
167 | |
168 def __init__(self, host, text): | |
169 Label.__init__(self, text) # , Element=DOM.createElement('div') | |
170 self.setStyleName('contactTitle') | |
171 libervia_widget.DragLabel.__init__(self, text, "CONTACT_TITLE", host) | |
172 ClickHandler.__init__(self) | |
173 self.addClickListener(self) | |
174 | |
175 def onClick(self, sender): | |
176 self.host.displayWidget(blog.MicroblogPanel, ()) | |
177 | |
178 | |
179 class ContactList(SimplePanel, QuickContactList): | |
180 """Manage the contacts and groups""" | |
181 | |
182 def __init__(self, host): | |
183 QuickContactList.__init__(self, host, C.PROF_KEY_NONE) | |
184 SimplePanel.__init__(self) | |
185 self.host = host | |
186 self.scroll_panel = ScrollPanel() | |
187 self.vPanel = VerticalPanel() | |
188 _title = ContactTitleLabel(host, 'Contacts') | |
189 DOM.setStyleAttribute(_title.getElement(), "cursor", "pointer") | |
190 self._contacts_panel = ContactsPanel(host) | |
191 self._contacts_panel.setStyleName('contactPanel') # FIXME: style doesn't exists ! | |
192 self._group_panel = GroupPanel(self) | |
193 | |
194 self.vPanel.add(_title) | |
195 self.vPanel.add(self._group_panel) | |
196 self.vPanel.add(self._contacts_panel) | |
197 self.scroll_panel.add(self.vPanel) | |
198 self.add(self.scroll_panel) | |
199 self.setStyleName('contactList') | |
200 Window.addWindowResizeListener(self) | |
201 | |
202 # 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) | |
203 self.avatarListener = self.onAvatarUpdate | |
204 host.addListener('avatar', self.avatarListener, [C.PROF_KEY_NONE]) | |
205 | |
206 @property | |
207 def profile(self): | |
208 return C.PROF_KEY_NONE | |
209 | |
210 def onDelete(self): | |
211 QuickContactList.onDelete(self) | |
212 self.host.removeListener('avatar', self.avatarListener) | |
213 | |
214 def update(self): | |
215 ### GROUPS ### | |
216 _keys = self._groups.keys() | |
217 try: | |
218 # XXX: Pyjamas doesn't do the set casting if None is present | |
219 _keys.remove(None) | |
220 except (KeyError, ValueError): # XXX: error raised depend on pyjama's compilation options | |
221 pass | |
222 current_groups = set(_keys) | |
223 shown_groups = self._group_panel.getGroups() | |
224 new_groups = current_groups.difference(shown_groups) | |
225 removed_groups = shown_groups.difference(current_groups) | |
226 for group in new_groups: | |
227 self._group_panel.add(group) | |
228 for group in removed_groups: | |
229 self._group_panel.remove(group) | |
230 | |
231 ### JIDS ### | |
232 to_show = [jid_ for jid_ in self.roster_entities if self.entityToShow(jid_) and jid_ != self.whoami.bare] | |
233 to_show.sort() | |
234 | |
235 self._contacts_panel.setList(to_show) | |
236 | |
237 for jid_ in self._alerts: | |
238 self._contacts_panel.setState(jid_, "messageWaiting", True) | |
239 | |
240 def remove(self, entity): | |
241 # FIXME: SimplePanel and QuickContactList both have a 'remove' method | |
242 QuickContactList.remove(self, entity) | |
243 | |
244 def onWindowResized(self, width, height): | |
245 ideal_height = height - DOM.getAbsoluteTop(self.getElement()) - 5 | |
246 tab_panel = self.host.panel.tab_panel | |
247 if tab_panel.getWidgetCount() > 1: | |
248 ideal_height -= tab_panel.getTabBar().getOffsetHeight() | |
249 self.scroll_panel.setHeight("%s%s" % (ideal_height, "px")) | |
250 | |
251 # def updateContact(self, jid_s, attributes, groups): | |
252 # """Add a contact to the panel if it doesn't exist, update it else | |
253 | |
254 # @param jid_s: jid userhost as unicode | |
255 # @param attributes: cf SàT Bridge API's newContact | |
256 # @param groups: list of groups""" | |
257 # _current_groups = self.getContactGroups(jid_s) | |
258 # _new_groups = set(groups) | |
259 # _key = "@%s: " | |
260 | |
261 # for group in _current_groups.difference(_new_groups): | |
262 # # We remove the contact from the groups where he isn't anymore | |
263 # self.groups[group].remove(jid_s) | |
264 # if not self.groups[group]: | |
265 # # The group is now empty, we must remove it | |
266 # del self.groups[group] | |
267 # self._group_panel.remove(group) | |
268 # if self.host.uni_box: | |
269 # self.host.uni_box.removeKey(_key % group) | |
270 | |
271 # for group in _new_groups.difference(_current_groups): | |
272 # # We add the contact to the groups he joined | |
273 # if group not in self.groups.keys(): | |
274 # self.groups[group] = set() | |
275 # self._group_panel.add(group) | |
276 # if self.host.uni_box: | |
277 # self.host.uni_box.addKey(_key % group) | |
278 # self.groups[group].add(jid_s) | |
279 | |
280 # # We add the contact to contact list, it will check if contact already exists | |
281 # self._contacts_panel.add(jid_s) | |
282 # self.updateVisibility([jid_s], self.getContactGroups(jid_s)) | |
283 | |
284 # def removeContact(self, jid): | |
285 # """Remove contacts from groups where he is and contact list""" | |
286 # self.updateContact(jid, {}, []) # we remove contact from every group | |
287 # self._contacts_panel.remove(jid) | |
288 | |
289 # def setConnected(self, jid_s, resource, availability, priority, statuses): | |
290 # """Set connection status | |
291 # @param jid_s (unicode): JID userhost as unicode | |
292 # """ | |
293 # if availability == 'unavailable': | |
294 # if jid_s in self.connected: | |
295 # if resource in self.connected[jid_s]: | |
296 # del self.connected[jid_s][resource] | |
297 # if not self.connected[jid_s]: | |
298 # del self.connected[jid_s] | |
299 # else: | |
300 # if jid_s not in self.connected: | |
301 # self.connected[jid_s] = {} | |
302 # self.connected[jid_s][resource] = (availability, priority, statuses) | |
303 | |
304 # # check if the contact is connected with another resource, use the one with highest priority | |
305 # if jid_s in self.connected: | |
306 # max_resource = max_priority = None | |
307 # for tmp_resource in self.connected[jid_s]: | |
308 # if max_priority is None or self.connected[jid_s][tmp_resource][1] >= max_priority: | |
309 # max_resource = tmp_resource | |
310 # max_priority = self.connected[jid_s][tmp_resource][1] | |
311 # if availability == "unavailable": # do not check the priority here, because 'unavailable' has a dummy one | |
312 # priority = max_priority | |
313 # availability = self.connected[jid_s][max_resource][0] | |
314 # if jid_s not in self.connected or priority >= max_priority: | |
315 # # case 1: jid not in self.connected means all resources are disconnected, update with 'unavailable' | |
316 # # case 2: update (or confirm) with the values of the resource which takes precedence | |
317 # self._contacts_panel.setState(jid_s, "availability", availability) | |
318 | |
319 # self.updateVisibility([jid_s], self.getContactGroups(jid_s)) | |
320 | |
321 def setContactMessageWaiting(self, jid, waiting): | |
322 """Show a visual indicator that contact has send a message | |
323 | |
324 @param jid: jid of the contact | |
325 @param waiting: True if message are waiting""" | |
326 raise Exception("Should not be there") | |
327 # self._contacts_panel.setState(jid, "messageWaiting", waiting) | |
328 | |
329 # def getConnected(self, filter_muc=False): | |
330 # """return a list of all jid (bare jid) connected | |
331 # @param filter_muc: if True, remove the groups from the list | |
332 # """ | |
333 # contacts = self.connected.keys() | |
334 # contacts.sort() | |
335 # return contacts if not filter_muc else list(set(contacts).intersection(set(self.getContacts()))) | |
336 | |
337 # def getContactGroups(self, contact_jid_s): | |
338 # """Get groups where contact is | |
339 # @param group: string of single group, or list of string | |
340 # @param contact_jid_s: jid to test, as unicode | |
341 # """ | |
342 # result = set() | |
343 # for group in self.groups: | |
344 # if self.isContactInGroup(group, contact_jid_s): | |
345 # result.add(group) | |
346 # return result | |
347 | |
348 # def isContactInGroup(self, group, contact_jid): | |
349 # """Test if the contact_jid is in the group | |
350 # @param group: string of single group, or list of string | |
351 # @param contact_jid: jid to test | |
352 # @return: True if contact_jid is in on of the groups""" | |
353 # if group in self.groups and contact_jid in self.groups[group]: | |
354 # return True | |
355 # return False | |
356 | |
357 def isContactInRoster(self, contact_jid): | |
358 """Test if the contact is in our roster list""" | |
359 for contact_box in self._contacts_panel: | |
360 if contact_jid == contact_box.jid: | |
361 return True | |
362 return False | |
363 | |
364 # def getContacts(self): | |
365 # return self._contacts_panel.getContacts() | |
366 | |
367 def getGroups(self): | |
368 return self.groups.keys() | |
369 | |
370 def onMouseMove(self, sender, x, y): | |
371 pass | |
372 | |
373 def onMouseDown(self, sender, x, y): | |
374 pass | |
375 | |
376 def onMouseUp(self, sender, x, y): | |
377 pass | |
378 | |
379 def onMouseEnter(self, sender): | |
380 if isinstance(sender, GroupLabel): | |
381 jids = self.getGroupData(sender.group, "jids") | |
382 for contact in self._contacts_panel: | |
383 if contact.jid in jids: | |
384 contact.label.addStyleName("selected") | |
385 | |
386 def onMouseLeave(self, sender): | |
387 if isinstance(sender, GroupLabel): | |
388 jids = self.getGroupData(sender.group, "jids") | |
389 for contact in self._contacts_panel: | |
390 if contact.jid in jids: | |
391 contact.label.removeStyleName("selected") | |
392 | |
393 def onAvatarUpdate(self, jid_, hash_, profile): | |
394 """Called on avatar update events | |
395 | |
396 @param jid_: jid of the entity with updated avatar | |
397 @param hash_: hash of the avatar | |
398 @param profile: %(doc_profile)s | |
399 """ | |
400 self._contacts_panel.updateAvatar(jid_, self.host.getAvatarURL(jid_)) | |
401 | |
402 def onNickUpdate(self, jid_, new_nick, profile): | |
403 self._contacts_panel.updateNick(jid_, new_nick) | |
404 | |
405 def hasVisibleMembers(self, group): | |
406 """Tell if the given group actually has visible members | |
407 | |
408 @param group (unicode): the group to check | |
409 @return: boolean | |
410 """ | |
411 raise Exception # FIXME: remove this method | |
412 for jid_ in self.groups[group]: | |
413 if self._contacts_panel.getContactBox(jid_).isVisible(): | |
414 return True | |
415 return False | |
416 | |
417 def offlineContactsToShow(self): | |
418 """Tell if offline contacts should be visible according to the user settings | |
419 | |
420 @return: boolean | |
421 """ | |
422 return C.bool(self.host.getCachedParam('General', C.SHOW_OFFLINE_CONTACTS)) | |
423 | |
424 def emtyGroupsToShow(self): | |
425 """Tell if empty groups should be visible according to the user settings | |
426 | |
427 @return: boolean | |
428 """ | |
429 return C.bool(self.host.getCachedParam('General', C.SHOW_EMPTY_GROUPS)) | |
430 | |
431 def onPresenceUpdate(self, entity, show, priority, statuses, profile): | |
432 QuickContactList.onPresenceUpdate(self, entity, show, priority, statuses, profile) | |
433 entity_bare = entity.bare | |
434 show = self.getCache(entity_bare, C.PRESENCE_SHOW) # we use cache to have the show nformation of main resource only | |
435 self._contacts_panel.setState(entity_bare, "availability", show) | |
436 self.update() # FIXME: should update the list without rebuilding it all | |
437 | |
438 # def updateVisibility(self, jids, groups): | |
439 # """Set the widgets visibility for the given contacts and groups | |
440 | |
441 # @param jids (list[unicode]): list of JID | |
442 # @param groups (list[unicode]): list of groups | |
443 # """ | |
444 # for jid_s in jids: | |
445 # try: | |
446 # self._contacts_panel.getContactBox(jid_s).setVisible(jid_s in self.connected or self.offlineContactsToShow()) | |
447 # except TypeError: | |
448 # log.warning('No box for contact %s: this code line should not be reached' % jid_s) | |
449 # for group in groups: | |
450 # try: | |
451 # self._group_panel.getGroupBox(group).setVisible(self.hasVisibleMembers(group) or self.emtyGroupsToShow()) | |
452 # except TypeError: | |
453 # log.warning('No box for group %s: this code line should not be reached' % group) | |
454 | |
455 # def refresh(self): | |
456 # """Show or hide disconnected contacts and empty groups""" | |
457 # self.updateVisibility(self._contacts_panel.contacts, self.groups.keys()) | |
458 | |
459 | |
460 class JIDList(list): | |
461 """JID-friendly list implementation for Pyjamas""" | |
462 | |
463 def __contains__(self, item): | |
464 """Tells if the list contains the given item. | |
465 | |
466 @param item (object): element to check | |
467 @return: bool | |
468 """ | |
469 # Since our JID doesn't inherit from str/unicode, without this method | |
470 # the test would return True only when the objects references are the | |
471 # same. Tests have shown that the other iterable "set" and "dict" don't | |
472 # need this hack to reproduce the Twisted's behavior. | |
473 for other in self: | |
474 if other == item: | |
475 return True | |
476 return False |