comparison src/browser/sat_browser/contact.py @ 467:97c72fe4a5f2

browser_side: import fixes: - moved browser modules in a sat_browser packages, to avoid import conflicts with std lib (e.g. logging), and let pyjsbuild work normaly - refactored bad import practices: classes are most of time not imported directly, module is imported instead.
author Goffi <goffi@goffi.org>
date Mon, 09 Jun 2014 22:15:26 +0200
parents src/browser/contact.py@36f27d1e64b2
children c21ea1fe3593
comparison
equal deleted inserted replaced
466:01880aa8ea2d 467:97c72fe4a5f2
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 pyjamas.ui.SimplePanel import SimplePanel
24 from pyjamas.ui.ScrollPanel import ScrollPanel
25 from pyjamas.ui.VerticalPanel import VerticalPanel
26 from pyjamas.ui.ClickListener import ClickHandler
27 from pyjamas.ui.Label import Label
28 from pyjamas.ui.HTML import HTML
29 from pyjamas import Window
30 from pyjamas import DOM
31 from __pyjamas__ import doc
32
33 from jid import JID
34
35 import base_panels
36 import base_widget
37 import panels
38 import html_tools
39
40
41 def setPresenceStyle(element, presence, base_style="contact"):
42 """
43 Set the CSS style of a contact's element according to its presence.
44 @param item: the UI element of the contact
45 @param presence: a value in ("", "chat", "away", "dnd", "xa").
46 @param base_style: the base name of the style to apply
47 """
48 if not hasattr(element, 'presence_style'):
49 element.presence_style = None
50 style = '%s-%s' % (base_style, presence or 'connected')
51 if style == element.presence_style:
52 return
53 if element.presence_style is not None:
54 element.removeStyleName(element.presence_style)
55 element.addStyleName(style)
56 element.presence_style = style
57
58
59 class GroupLabel(base_widget.DragLabel, Label, ClickHandler):
60 def __init__(self, host, group):
61 self.group = group
62 self.host = host
63 Label.__init__(self, group) # , Element=DOM.createElement('div')
64 self.setStyleName('group')
65 base_widget.DragLabel.__init__(self, group, "GROUP")
66 ClickHandler.__init__(self)
67 self.addClickListener(self)
68
69 def onClick(self, sender):
70 self.host.getOrCreateLiberviaWidget(panels.MicroblogPanel, self.group)
71
72
73 class ContactLabel(base_widget.DragLabel, HTML, ClickHandler):
74 def __init__(self, host, jid, name=None, handleClick=True):
75 HTML.__init__(self)
76 self.host = host
77 self.name = name or jid
78 self.waiting = False
79 self.jid = jid
80 self._fill()
81 self.setStyleName('contact')
82 base_widget.DragLabel.__init__(self, jid, "CONTACT")
83 if handleClick:
84 ClickHandler.__init__(self)
85 self.addClickListener(self)
86
87 def _fill(self):
88 if self.waiting:
89 _wait_html = "<b>(*)</b>&nbsp;"
90 self.setHTML("%(wait)s%(name)s" % {'wait': _wait_html,
91 'name': html_tools.html_sanitize(self.name)})
92
93 def setMessageWaiting(self, waiting):
94 """Show a visual indicator if message are waiting
95 @param waiting: True if message are waiting"""
96 self.waiting = waiting
97 self._fill()
98
99 def onClick(self, sender):
100 self.host.getOrCreateLiberviaWidget(panels.ChatPanel, self.jid)
101
102
103 class GroupList(VerticalPanel):
104
105 def __init__(self, parent):
106 VerticalPanel.__init__(self)
107 self.setStyleName('groupList')
108 self._parent = parent
109
110 def add(self, group):
111 _item = GroupLabel(self._parent.host, group)
112 _item.addMouseListener(self._parent)
113 DOM.setStyleAttribute(_item.getElement(), "cursor", "pointer")
114 index = 0
115 for group_ in [child.group for child in self.getChildren()]:
116 if group_ > group:
117 break
118 index += 1
119 VerticalPanel.insert(self, _item, index)
120
121 def remove(self, group):
122 for wid in self:
123 if isinstance(wid, GroupLabel) and wid.group == group:
124 VerticalPanel.remove(self, wid)
125
126
127 class GenericContactList(VerticalPanel):
128 """Class that can be used to represent a contact list, but not necessarily
129 the one that is displayed on the left side. Special features like popup menu
130 panel or changing the contact states must be done in a sub-class."""
131
132 def __init__(self, host, handleClick=False):
133 VerticalPanel.__init__(self)
134 self.host = host
135 self.contacts = []
136 self.handleClick = handleClick
137
138 def add(self, jid, name=None, item_cb=None):
139 if jid in self.contacts:
140 return
141 index = 0
142 for contact_ in self.contacts:
143 if contact_ > jid:
144 break
145 index += 1
146 self.contacts.insert(index, jid)
147 _item = ContactLabel(self.host, jid, name, handleClick=self.handleClick)
148 DOM.setStyleAttribute(_item.getElement(), "cursor", "pointer")
149 VerticalPanel.insert(self, _item, index)
150 if item_cb is not None:
151 item_cb(_item)
152
153 def remove(self, jid):
154 wid = self.getContactLabel(jid)
155 if not wid:
156 return
157 VerticalPanel.remove(self, wid)
158 self.contacts.remove(jid)
159
160 def isContactPresent(self, contact_jid):
161 """Return True if a contact is present in the panel"""
162 return contact_jid in self.contacts
163
164 def getContacts(self):
165 return self.contacts
166
167 def getContactLabel(self, contact_jid):
168 """get contactList widget of a contact
169 @return: ContactLabel item if present, else None"""
170 for wid in self:
171 if isinstance(wid, ContactLabel) and wid.jid == contact_jid:
172 return wid
173 return None
174
175
176 class ContactList(GenericContactList):
177 """The contact list that is displayed on the left side."""
178
179 def __init__(self, host):
180 GenericContactList.__init__(self, host, handleClick=True)
181 self.menu_entries = {"blog": {"title": "Public blog..."}}
182 self.context_menu = base_panels.PopupMenuPanel(entries=self.menu_entries,
183 hide=self.contextMenuHide,
184 callback=self.contextMenuCallback,
185 vertical=False, style={"selected": "menu-selected"})
186
187 def contextMenuHide(self, sender, key):
188 """Return True if the item for that sender should be hidden."""
189 # TODO: enable the blogs of users that are on another server
190 return JID(sender.jid).domain != self.host._defaultDomain
191
192 def contextMenuCallback(self, sender, key):
193 if key == "blog":
194 # TODO: use the bare when all blogs can be retrieved
195 node = JID(sender.jid).node
196 web_panel = panels.WebPanel(self.host, "/blog/%s" % node)
197 self.host.addTab("%s's blog" % node, web_panel)
198 else:
199 sender.onClick(sender)
200
201 def add(self, jid_s, name=None):
202 """Add a contact
203
204 @param jid_s (str): JID as unicode
205 @param name (str): nickname
206 """
207 def item_cb(item):
208 self.context_menu.registerRightClickSender(item)
209 GenericContactList.add(self, jid_s, name, item_cb)
210
211 def setState(self, jid, type_, state):
212 """Change the appearance of the contact, according to the state
213 @param jid: jid which need to change state
214 @param type_: one of availability, messageWaiting
215 @param state:
216 - for messageWaiting type:
217 True if message are waiting
218 - for availability type:
219 'unavailable' if not connected, else presence like RFC6121 #4.7.2.1"""
220 _item = self.getContactLabel(jid)
221 if _item:
222 if type_ == 'availability':
223 setPresenceStyle(_item, state)
224 elif type_ == 'messageWaiting':
225 _item.setMessageWaiting(state)
226
227
228 class ContactTitleLabel(base_widget.DragLabel, Label, ClickHandler):
229 def __init__(self, host, text):
230 Label.__init__(self, text) # , Element=DOM.createElement('div')
231 self.host = host
232 self.setStyleName('contactTitle')
233 base_widget.DragLabel.__init__(self, text, "CONTACT_TITLE")
234 ClickHandler.__init__(self)
235 self.addClickListener(self)
236
237 def onClick(self, sender):
238 self.host.getOrCreateLiberviaWidget(panels.MicroblogPanel, None)
239
240
241 class ContactPanel(SimplePanel):
242 """Manage the contacts and groups"""
243
244 def __init__(self, host):
245 SimplePanel.__init__(self)
246
247 self.scroll_panel = ScrollPanel()
248
249 self.host = host
250 self.groups = {}
251 self.connected = {} # jid connected as key and their status
252
253 self.vPanel = VerticalPanel()
254 _title = ContactTitleLabel(host, 'Contacts')
255 DOM.setStyleAttribute(_title.getElement(), "cursor", "pointer")
256
257 self._contact_list = ContactList(host)
258 self._contact_list.setStyleName('contactList')
259 self._groupList = GroupList(self)
260 self._groupList.setStyleName('groupList')
261
262 self.vPanel.add(_title)
263 self.vPanel.add(self._groupList)
264 self.vPanel.add(self._contact_list)
265 self.scroll_panel.add(self.vPanel)
266 self.add(self.scroll_panel)
267 self.setStyleName('contactBox')
268 Window.addWindowResizeListener(self)
269
270 def onWindowResized(self, width, height):
271 contact_panel_elt = self.getElement()
272 classname = 'widgetsPanel' if isinstance(self.getParent().getParent(), panels.UniBoxPanel) else'gwt-TabBar'
273 _elts = doc().getElementsByClassName(classname)
274 if not _elts.length:
275 log.error("no element of class %s found, it should exist !" % classname)
276 tab_bar_h = height
277 else:
278 tab_bar_h = DOM.getAbsoluteTop(_elts.item(0)) or height # getAbsoluteTop can be 0 if tabBar is hidden
279
280 ideal_height = tab_bar_h - DOM.getAbsoluteTop(contact_panel_elt) - 5
281 self.scroll_panel.setHeight("%s%s" % (ideal_height, "px"))
282
283 def updateContact(self, jid_s, attributes, groups):
284 """Add a contact to the panel if it doesn't exist, update it else
285 @param jid_s: jid userhost as unicode
286 @param attributes: cf SàT Bridge API's newContact
287 @param groups: list of groups"""
288 _current_groups = self.getContactGroups(jid_s)
289 _new_groups = set(groups)
290 _key = "@%s: "
291
292 for group in _current_groups.difference(_new_groups):
293 # We remove the contact from the groups where he isn't anymore
294 self.groups[group].remove(jid_s)
295 if not self.groups[group]:
296 # The group is now empty, we must remove it
297 del self.groups[group]
298 self._groupList.remove(group)
299 if self.host.uni_box:
300 self.host.uni_box.removeKey(_key % group)
301
302 for group in _new_groups.difference(_current_groups):
303 # We add the contact to the groups he joined
304 if not group in self.groups.keys():
305 self.groups[group] = set()
306 self._groupList.add(group)
307 if self.host.uni_box:
308 self.host.uni_box.addKey(_key % group)
309 self.groups[group].add(jid_s)
310
311 # We add the contact to contact list, it will check if contact already exists
312 self._contact_list.add(jid_s)
313
314 def removeContact(self, jid):
315 """Remove contacts from groups where he is and contact list"""
316 self.updateContact(jid, {}, []) # we remove contact from every group
317 self._contact_list.remove(jid)
318
319 def setConnected(self, jid, resource, availability, priority, statuses):
320 """Set connection status
321 @param jid: JID userhost as unicode
322 """
323 if availability == 'unavailable':
324 if jid in self.connected:
325 if resource in self.connected[jid]:
326 del self.connected[jid][resource]
327 if not self.connected[jid]:
328 del self.connected[jid]
329 else:
330 if not jid in self.connected:
331 self.connected[jid] = {}
332 self.connected[jid][resource] = (availability, priority, statuses)
333
334 # check if the contact is connected with another resource, use the one with highest priority
335 if jid in self.connected:
336 max_resource = max_priority = None
337 for tmp_resource in self.connected[jid]:
338 if max_priority is None or self.connected[jid][tmp_resource][1] >= max_priority:
339 max_resource = tmp_resource
340 max_priority = self.connected[jid][tmp_resource][1]
341 if availability == "unavailable": # do not check the priority here, because 'unavailable' has a dummy one
342 priority = max_priority
343 availability = self.connected[jid][max_resource][0]
344 if jid not in self.connected or priority >= max_priority:
345 # case 1: jid not in self.connected means all resources are disconnected, update with 'unavailable'
346 # case 2: update (or confirm) with the values of the resource which takes precedence
347 self._contact_list.setState(jid, "availability", availability)
348
349 # update the connected contacts chooser live
350 if hasattr(self.host, "room_contacts_chooser") and self.host.room_contacts_chooser is not None:
351 self.host.room_contacts_chooser.resetContacts()
352
353 def setContactMessageWaiting(self, jid, waiting):
354 """Show an visual indicator that contact has send a message
355 @param jid: jid of the contact
356 @param waiting: True if message are waiting"""
357 self._contact_list.setState(jid, "messageWaiting", waiting)
358
359 def getConnected(self, filter_muc=False):
360 """return a list of all jid (bare jid) connected
361 @param filter_muc: if True, remove the groups from the list
362 """
363 contacts = self.connected.keys()
364 contacts.sort()
365 return contacts if not filter_muc else list(set(contacts).intersection(set(self.getContacts())))
366
367 def getContactGroups(self, contact_jid_s):
368 """Get groups where contact is
369 @param group: string of single group, or list of string
370 @param contact_jid_s: jid to test, as unicode
371 """
372 result = set()
373 for group in self.groups:
374 if self.isContactInGroup(group, contact_jid_s):
375 result.add(group)
376 return result
377
378 def isContactInGroup(self, group, contact_jid):
379 """Test if the contact_jid is in the group
380 @param group: string of single group, or list of string
381 @param contact_jid: jid to test
382 @return: True if contact_jid is in on of the groups"""
383 if group in self.groups and contact_jid in self.groups[group]:
384 return True
385 return False
386
387 def isContactInRoster(self, contact_jid):
388 """Test if the contact is in our roster list"""
389 for _contact_label in self._contact_list:
390 if contact_jid == _contact_label.jid:
391 return True
392 return False
393
394 def getContacts(self):
395 return self._contact_list.getContacts()
396
397 def getGroups(self):
398 return self.groups.keys()
399
400 def onMouseMove(self, sender, x, y):
401 pass
402
403 def onMouseDown(self, sender, x, y):
404 pass
405
406 def onMouseUp(self, sender, x, y):
407 pass
408
409 def onMouseEnter(self, sender):
410 if isinstance(sender, GroupLabel):
411 for contact in self._contact_list:
412 if contact.jid in self.groups[sender.group]:
413 contact.addStyleName("selected")
414
415 def onMouseLeave(self, sender):
416 if isinstance(sender, GroupLabel):
417 for contact in self._contact_list:
418 if contact.jid in self.groups[sender.group]:
419 contact.removeStyleName("selected")