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