comparison src/browser/sat_browser/contact_list.py @ 589:a5019e62c3e9 frontends_multi_profiles

browser side: big refactoring to base Libervia on QuickFrontend, first draft: /!\ not finished, partially working and highly instable - add collections module with an OrderedDict like class - SatWebFrontend inherit from QuickApp - general sat_frontends tools.jid module is used - bridge/json methods have moved to json module - UniBox is partially removed (should be totally removed before merge to trunk) - Signals are now register with the generic registerSignal method (which is called mainly in QuickFrontend) - the generic getOrCreateWidget method from QuickWidgetsManager is used instead of Libervia's specific methods - all Widget are now based more or less directly on QuickWidget - with the new QuickWidgetsManager.getWidgets method, it's no more necessary to check all widgets which are instance of a particular class - ChatPanel and related moved to chat module - MicroblogPanel and related moved to blog module - global and overcomplicated send method has been disabled: each class should manage its own sending - for consistency with other frontends, former ContactPanel has been renamed to ContactList and vice versa - for the same reason, ChatPanel has been renamed to Chat - for compatibility with QuickFrontend, a fake profile is used in several places, it is set to C.PROF_KEY_NONE (real profile is managed server side for obvious security reasons) - changed default url for web panel to SàT website, and contact address to generic SàT contact address - ContactList is based on QuickContactList, UI changes are done in update method - bride call (now json module) have been greatly improved, in particular call can be done in the same way as for other frontends (bridge.method_name(arg1, arg2, ..., callback=cb, errback=eb). Blocking method must be called like async methods due to javascript architecture - in bridge calls, a callback can now exists without errback - hard reload on BridgeSignals remote error has been disabled, a better option should be implemented - use of constants where that make sens, some style improvments - avatars are temporarily disabled - lot of code disabled, will be fixed or removed before merge - various other changes, check diff for more details server side: manage remote exception on getEntityData, removed getProfileJid call, added getWaitingConf, added getRoomsSubjects
author Goffi <goffi@goffi.org>
date Sat, 24 Jan 2015 01:45:39 +0100
parents src/browser/sat_browser/contact.py@668bb04e9708
children c66f7227848e
comparison
equal deleted inserted replaced
585:bade589dbd5a 589:a5019e62c3e9
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.ui.HTML import HTML
30 from pyjamas.ui.Image import Image
31 from pyjamas import Window
32 from pyjamas import DOM
33 from __pyjamas__ import doc
34
35 from sat_frontends.tools import jid
36 from constants import Const as C
37 import base_widget
38 import panels
39 import html_tools
40 import chat
41
42
43 def buildPresenceStyle(presence, base_style=None):
44 """Return the CSS classname to be used for displaying the given presence information.
45 @param presence (str): presence is a value in ('', 'chat', 'away', 'dnd', 'xa')
46 @param base_style (str): base classname
47 @return: str
48 """
49 if not base_style:
50 base_style = "contactLabel"
51 return '%s-%s' % (base_style, presence or 'connected')
52
53
54 def setPresenceStyle(widget, presence, base_style=None):
55 """
56 Set the CSS style of a contact's element according to its presence.
57
58 @param widget (Widget): the UI element of the contact
59 @param presence (str): a value in ("", "chat", "away", "dnd", "xa").
60 @param base_style (str): the base name of the style to apply
61 """
62 if not hasattr(widget, 'presence_style'):
63 widget.presence_style = None
64 style = buildPresenceStyle(presence, base_style)
65 if style == widget.presence_style:
66 return
67 if widget.presence_style is not None:
68 widget.removeStyleName(widget.presence_style)
69 widget.addStyleName(style)
70 widget.presence_style = style
71
72
73 class GroupLabel(base_widget.DragLabel, Label, ClickHandler):
74 def __init__(self, host, group):
75 self.group = group
76 self.host = host
77 Label.__init__(self, group) # , Element=DOM.createElement('div')
78 self.setStyleName('group')
79 base_widget.DragLabel.__init__(self, group, "GROUP")
80 ClickHandler.__init__(self)
81 self.addClickListener(self)
82
83 def onClick(self, sender):
84 self.host.getOrCreateLiberviaWidget(panels.MicroblogPanel, {'item': self.group})
85
86
87 class ContactLabel(HTML):
88 def __init__(self, jid, name=None):
89 HTML.__init__(self)
90 self.name = name or str(jid)
91 self.waiting = False
92 self.refresh()
93 self.setStyleName('contactLabel')
94
95 def refresh(self):
96 if self.waiting:
97 wait_html = "<b>(*)</b>&nbsp;"
98 self.setHTML("%(wait)s%(name)s" % {'wait': wait_html,
99 'name': html_tools.html_sanitize(self.name)})
100
101 def setMessageWaiting(self, waiting):
102 """Show a visual indicator if message are waiting
103
104 @param waiting: True if message are waiting"""
105 self.waiting = waiting
106 self.refresh()
107
108
109 class ContactMenuBar(base_widget.WidgetMenuBar):
110
111 def onBrowserEvent(self, event):
112 base_widget.WidgetMenuBar.onBrowserEvent(self, event)
113 event.stopPropagation() # prevent opening the chat dialog
114
115 @classmethod
116 def getCategoryHTML(cls, menu_name_i18n, type_):
117 return '<img src="%s"/>' % C.DEFAULT_AVATAR
118
119 def setUrl(self, url):
120 """Set the URL of the contact avatar."""
121 self.items[0].setHTML('<img src="%s" />' % url)
122
123
124 class ContactBox(VerticalPanel, ClickHandler, base_widget.DragLabel):
125
126 def __init__(self, host, jid_, name=None, click_listener=None, handle_menu=None):
127 VerticalPanel.__init__(self, StyleName='contactBox', VerticalAlignment='middle')
128 base_widget.DragLabel.__init__(self, jid_, "CONTACT")
129 self.host = host
130 self.jid = jid_
131 self.label = ContactLabel(jid_, name)
132 self.avatar = ContactMenuBar(self, host) if handle_menu else Image()
133 # self.updateAvatar(host.getAvatar(jid_)) # FIXME
134 self.add(self.avatar)
135 self.add(self.label)
136 if click_listener:
137 ClickHandler.__init__(self)
138 self.addClickListener(self)
139 self.click_listener = click_listener
140
141 def addMenus(self, menu_bar):
142 menu_bar.addCachedMenus(C.MENU_ROSTER_JID_CONTEXT, {'jid': self.jid})
143 menu_bar.addCachedMenus(C.MENU_JID_CONTEXT, {'jid': self.jid})
144
145 def setMessageWaiting(self, waiting):
146 """Show a visual indicator if message are waiting
147
148 @param waiting: True if message are waiting"""
149 self.label.setMessageWaiting(waiting)
150
151 def updateAvatar(self, url):
152 """Update the avatar.
153
154 @param url (str): image url
155 """
156 self.avatar.setUrl(url)
157
158 def onClick(self, sender):
159 self.click_listener(self.jid)
160
161
162 class GroupPanel(VerticalPanel):
163
164 def __init__(self, parent):
165 VerticalPanel.__init__(self)
166 self.setStyleName('groupPanel')
167 self._parent = parent
168 self._groups = set()
169
170 def add(self, group):
171 if group in self._groups:
172 log.warning("trying to add an already existing group")
173 return
174 _item = GroupLabel(self._parent.host, group)
175 _item.addMouseListener(self._parent)
176 DOM.setStyleAttribute(_item.getElement(), "cursor", "pointer")
177 index = 0
178 for group_ in [child.group for child in self.getChildren()]:
179 if group_ > group:
180 break
181 index += 1
182 VerticalPanel.insert(self, _item, index)
183 self._groups.add(group)
184
185 def remove(self, group):
186 for wid in self:
187 if isinstance(wid, GroupLabel) and wid.group == group:
188 VerticalPanel.remove(self, wid)
189 self._groups.remove(group)
190 return
191 log.warning("Trying to remove a non existent group")
192
193 def getGroupBox(self, group):
194 """get the widget of a group
195
196 @param group (str): the group
197 @return: GroupLabel instance if present, else None"""
198 for wid in self:
199 if isinstance(wid, GroupLabel) and wid.group == group:
200 return wid
201 return None
202
203 def getGroups(self):
204 return self._groups
205
206
207 class BaseContactsPanel(VerticalPanel):
208 """Class that can be used to represent a contact list, but not necessarily
209 the one that is displayed on the left side. Special features like popup menu
210 panel or changing the contact states must be done in a sub-class."""
211
212 def __init__(self, host, handle_click=False, handle_menu=False):
213 VerticalPanel.__init__(self)
214 self.host = host
215 self.contacts = []
216 self.click_listener = None
217 self.handle_menu = handle_menu
218
219 if handle_click:
220 def cb(contact_jid):
221 host.widgets.getOrCreateWidget(chat.Chat, contact_jid, type_=C.CHAT_ONE2ONE, profile=C.PROF_KEY_NONE)
222 self.click_listener = cb
223
224 def add(self, jid_, name=None):
225 """Add a contact to the list.
226
227 @param jid_ (jid.JID): jid_ of the contact
228 @param name (str): optional name of the contact
229 """
230 assert isinstance(jid_, jid.JID)
231 if jid_ in self.contacts:
232 return
233 index = 0
234 for contact_ in self.contacts:
235 if contact_ > jid_:
236 break
237 index += 1
238 self.contacts.insert(index, jid_)
239 box = ContactBox(self.host, jid_, name, self.click_listener, self.handle_menu)
240 VerticalPanel.insert(self, box, index)
241
242 def remove(self, jid_):
243 box = self.getContactBox(jid_)
244 if not box:
245 return
246 VerticalPanel.remove(self, box)
247 self.contacts.remove(jid_)
248
249 def isContactPresent(self, contact_jid):
250 """Return True if a contact is present in the panel"""
251 return contact_jid in self.contacts
252
253 def getContacts(self):
254 return self.contacts
255
256 def getContactBox(self, contact_jid):
257 """get the widget of a contact
258
259 @param contact_jid (jid.JID): the contact
260 @return: ContactBox instance if present, else None"""
261 for wid in self:
262 if isinstance(wid, ContactBox) and wid.jid == contact_jid:
263 return wid
264 return None
265
266 def updateAvatar(self, jid_, url):
267 """Update the avatar of the given contact
268
269 @param jid_ (jid.JID): contact jid
270 @param url (str): image url
271 """
272 try:
273 self.getContactBox(jid_).updateAvatar(url)
274 except TypeError:
275 pass
276
277
278 class ContactsPanel(BaseContactsPanel):
279 """The contact list that is displayed on the left side."""
280
281 def __init__(self, host):
282 BaseContactsPanel.__init__(self, host, handle_click=True, handle_menu=True)
283
284 def setState(self, jid_, type_, state):
285 """Change the appearance of the contact, according to the state
286 @param jid_ (jid.JID): jid.JID which need to change state
287 @param type_ (str): one of "availability", "messageWaiting"
288 @param state:
289 - for messageWaiting type:
290 True if message are waiting
291 - for availability type:
292 C.PRESENCE_UNAVAILABLE or None if not connected, else presence like RFC6121 #4.7.2.1"""
293 assert type_ in ('availability', 'messageWaiting')
294 contact_box = self.getContactBox(jid_)
295 if not contact_box:
296 log.warning("No contact box found for {}".format(jid_))
297 else:
298 if type_ == 'availability':
299 if state is None:
300 state = C.PRESENCE_UNAVAILABLE
301 setPresenceStyle(contact_box.label, state)
302 elif type_ == 'messageWaiting':
303 contact_box.setMessageWaiting(state)
304
305
306 class ContactTitleLabel(base_widget.DragLabel, Label, ClickHandler):
307 def __init__(self, host, text):
308 Label.__init__(self, text) # , Element=DOM.createElement('div')
309 self.host = host
310 self.setStyleName('contactTitle')
311 base_widget.DragLabel.__init__(self, text, "CONTACT_TITLE")
312 ClickHandler.__init__(self)
313 self.addClickListener(self)
314
315 def onClick(self, sender):
316 self.host.getOrCreateLiberviaWidget(panels.MicroblogPanel, {'item': None})
317
318
319 class ContactList(SimplePanel, QuickContactList):
320 """Manage the contacts and groups"""
321
322 def __init__(self, host):
323 QuickContactList.__init__(self, host, C.PROF_KEY_NONE)
324 SimplePanel.__init__(self)
325 self.scroll_panel = ScrollPanel()
326 self.vPanel = VerticalPanel()
327 _title = ContactTitleLabel(host, 'Contacts')
328 DOM.setStyleAttribute(_title.getElement(), "cursor", "pointer")
329 self._contacts_panel = ContactsPanel(host)
330 self._contacts_panel.setStyleName('contactPanel') # FIXME: style doesn't exists !
331 self._group_panel = GroupPanel(self)
332
333 self.vPanel.add(_title)
334 self.vPanel.add(self._group_panel)
335 self.vPanel.add(self._contacts_panel)
336 self.scroll_panel.add(self.vPanel)
337 self.add(self.scroll_panel)
338 self.setStyleName('contactList')
339 Window.addWindowResizeListener(self)
340
341 @property
342 def profile(self):
343 return C.PROF_KEY_NONE
344
345 def update(self):
346 ### GROUPS ###
347 _keys = self._groups.keys()
348 try:
349 # XXX: Pyjamas doesn't do the set casting if None is present
350 _keys.remove(None)
351 except KeyError:
352 pass
353 current_groups = set(_keys)
354 shown_groups = self._group_panel.getGroups()
355 new_groups = current_groups.difference(shown_groups)
356 removed_groups = shown_groups.difference(current_groups)
357 for group in new_groups:
358 self._group_panel.add(group)
359 for group in removed_groups:
360 self._group_panel.remove(group)
361
362 ### JIDS ###
363 current_contacts = set(self._cache.keys())
364 shown_contacts = set(self._contacts_panel.getContacts())
365 new_contacts = current_contacts.difference(shown_contacts)
366 removed_contacts = shown_contacts.difference(current_contacts)
367
368 for contact in new_contacts:
369 self._contacts_panel.add(contact)
370 for contact in removed_contacts:
371 self._contacts_panel.remove(contact)
372
373 def onWindowResized(self, width, height):
374 contact_panel_elt = self.getElement()
375 # FIXME: still needed ?
376 # classname = 'widgetsPanel' if isinstance(self.getParent().getParent(), panels.UniBoxPanel) else 'gwt-TabBar'
377 classname = 'gwt-TabBar'
378 _elts = doc().getElementsByClassName(classname)
379 if not _elts.length:
380 log.error("no element of class %s found, it should exist !" % classname)
381 tab_bar_h = height
382 else:
383 tab_bar_h = DOM.getAbsoluteTop(_elts.item(0)) or height # getAbsoluteTop can be 0 if tabBar is hidden
384
385 ideal_height = tab_bar_h - DOM.getAbsoluteTop(contact_panel_elt) - 5
386 self.scroll_panel.setHeight("%s%s" % (ideal_height, "px"))
387
388 # def updateContact(self, jid_s, attributes, groups):
389 # """Add a contact to the panel if it doesn't exist, update it else
390
391 # @param jid_s: jid userhost as unicode
392 # @param attributes: cf SàT Bridge API's newContact
393 # @param groups: list of groups"""
394 # _current_groups = self.getContactGroups(jid_s)
395 # _new_groups = set(groups)
396 # _key = "@%s: "
397
398 # for group in _current_groups.difference(_new_groups):
399 # # We remove the contact from the groups where he isn't anymore
400 # self.groups[group].remove(jid_s)
401 # if not self.groups[group]:
402 # # The group is now empty, we must remove it
403 # del self.groups[group]
404 # self._group_panel.remove(group)
405 # if self.host.uni_box:
406 # self.host.uni_box.removeKey(_key % group)
407
408 # for group in _new_groups.difference(_current_groups):
409 # # We add the contact to the groups he joined
410 # if group not in self.groups.keys():
411 # self.groups[group] = set()
412 # self._group_panel.add(group)
413 # if self.host.uni_box:
414 # self.host.uni_box.addKey(_key % group)
415 # self.groups[group].add(jid_s)
416
417 # # We add the contact to contact list, it will check if contact already exists
418 # self._contacts_panel.add(jid_s)
419 # self.updateVisibility([jid_s], self.getContactGroups(jid_s))
420
421 # def removeContact(self, jid):
422 # """Remove contacts from groups where he is and contact list"""
423 # self.updateContact(jid, {}, []) # we remove contact from every group
424 # self._contacts_panel.remove(jid)
425
426 # def setConnected(self, jid_s, resource, availability, priority, statuses):
427 # """Set connection status
428 # @param jid_s (str): JID userhost as unicode
429 # """
430 # if availability == 'unavailable':
431 # if jid_s in self.connected:
432 # if resource in self.connected[jid_s]:
433 # del self.connected[jid_s][resource]
434 # if not self.connected[jid_s]:
435 # del self.connected[jid_s]
436 # else:
437 # if jid_s not in self.connected:
438 # self.connected[jid_s] = {}
439 # self.connected[jid_s][resource] = (availability, priority, statuses)
440
441 # # check if the contact is connected with another resource, use the one with highest priority
442 # if jid_s in self.connected:
443 # max_resource = max_priority = None
444 # for tmp_resource in self.connected[jid_s]:
445 # if max_priority is None or self.connected[jid_s][tmp_resource][1] >= max_priority:
446 # max_resource = tmp_resource
447 # max_priority = self.connected[jid_s][tmp_resource][1]
448 # if availability == "unavailable": # do not check the priority here, because 'unavailable' has a dummy one
449 # priority = max_priority
450 # availability = self.connected[jid_s][max_resource][0]
451 # if jid_s not in self.connected or priority >= max_priority:
452 # # case 1: jid not in self.connected means all resources are disconnected, update with 'unavailable'
453 # # case 2: update (or confirm) with the values of the resource which takes precedence
454 # self._contacts_panel.setState(jid_s, "availability", availability)
455
456 # # update the connected contacts chooser live
457 # if hasattr(self.host, "room_contacts_chooser") and self.host.room_contacts_chooser is not None:
458 # self.host.room_contacts_chooser.resetContacts()
459
460 # self.updateVisibility([jid_s], self.getContactGroups(jid_s))
461
462 def setContactMessageWaiting(self, jid, waiting):
463 """Show an visual indicator that contact has send a message
464 @param jid: jid of the contact
465 @param waiting: True if message are waiting"""
466 self._contacts_panel.setState(jid, "messageWaiting", waiting)
467
468 # def getConnected(self, filter_muc=False):
469 # """return a list of all jid (bare jid) connected
470 # @param filter_muc: if True, remove the groups from the list
471 # """
472 # contacts = self.connected.keys()
473 # contacts.sort()
474 # return contacts if not filter_muc else list(set(contacts).intersection(set(self.getContacts())))
475
476 # def getContactGroups(self, contact_jid_s):
477 # """Get groups where contact is
478 # @param group: string of single group, or list of string
479 # @param contact_jid_s: jid to test, as unicode
480 # """
481 # result = set()
482 # for group in self.groups:
483 # if self.isContactInGroup(group, contact_jid_s):
484 # result.add(group)
485 # return result
486
487 # def isContactInGroup(self, group, contact_jid):
488 # """Test if the contact_jid is in the group
489 # @param group: string of single group, or list of string
490 # @param contact_jid: jid to test
491 # @return: True if contact_jid is in on of the groups"""
492 # if group in self.groups and contact_jid in self.groups[group]:
493 # return True
494 # return False
495
496 def isContactInRoster(self, contact_jid):
497 """Test if the contact is in our roster list"""
498 for contact_box in self._contacts_panel:
499 if contact_jid == contact_box.jid:
500 return True
501 return False
502
503 # def getContacts(self):
504 # return self._contacts_panel.getContacts()
505
506 def getGroups(self):
507 return self.groups.keys()
508
509 def onMouseMove(self, sender, x, y):
510 pass
511
512 def onMouseDown(self, sender, x, y):
513 pass
514
515 def onMouseUp(self, sender, x, y):
516 pass
517
518 def onMouseEnter(self, sender):
519 if isinstance(sender, GroupLabel):
520 jids = self.getGroupData(sender.group, "jids")
521 for contact in self._contacts_panel:
522 if contact.jid in jids:
523 contact.label.addStyleName("selected")
524
525 def onMouseLeave(self, sender):
526 if isinstance(sender, GroupLabel):
527 jids = self.getGroupData(sender.group, "jids")
528 for contact in self._contacts_panel:
529 if contact.jid in jids:
530 contact.label.removeStyleName("selected")
531
532 def updateAvatar(self, jid_s, url):
533 """Update the avatar of the given contact
534
535 @param jid_s (str): contact jid
536 @param url (str): image url
537 """
538 self._contacts_panel.updateAvatar(jid_s, url)
539
540 def hasVisibleMembers(self, group):
541 """Tell if the given group actually has visible members
542
543 @param group (str): the group to check
544 @return: boolean
545 """
546 for jid_ in self.groups[group]:
547 if self._contacts_panel.getContactBox(jid_).isVisible():
548 return True
549 return False
550
551 def offlineContactsToShow(self):
552 """Tell if offline contacts should be visible according to the user settings
553
554 @return: boolean
555 """
556 return self.host.getCachedParam('General', C.SHOW_OFFLINE_CONTACTS) == 'true'
557
558 def emtyGroupsToShow(self):
559 """Tell if empty groups should be visible according to the user settings
560
561 @return: boolean
562 """
563 return self.host.getCachedParam('General', C.SHOW_EMPTY_GROUPS) == 'true'
564
565 def updatePresence(self, entity, show, priority, statuses):
566 QuickContactList.updatePresence(self, entity, show, priority, statuses)
567 entity_bare = entity.bare
568 show = self.getCache(entity_bare, C.PRESENCE_SHOW) # we use cache to have the show nformation of main resource only
569 self._contacts_panel.setState(entity_bare, "availability", show)
570
571 # def updateVisibility(self, jids, groups):
572 # """Set the widgets visibility for the given contacts and groups
573
574 # @param jids (list[str]): list of JID
575 # @param groups (list[str]): list of groups
576 # """
577 # for jid_s in jids:
578 # try:
579 # self._contacts_panel.getContactBox(jid_s).setVisible(jid_s in self.connected or self.offlineContactsToShow())
580 # except TypeError:
581 # log.warning('No box for contact %s: this code line should not be reached' % jid_s)
582 # for group in groups:
583 # try:
584 # self._group_panel.getGroupBox(group).setVisible(self.hasVisibleMembers(group) or self.emtyGroupsToShow())
585 # except TypeError:
586 # log.warning('No box for group %s: this code line should not be reached' % group)
587
588 # def refresh(self):
589 # """Show or hide disconnected contacts and empty groups"""
590 # self.updateVisibility(self._contacts_panel.contacts, self.groups.keys())