Mercurial > libervia-web
diff 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 |
line wrap: on
line diff
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/browser/sat_browser/contact.py Mon Jun 09 22:15:26 2014 +0200 @@ -0,0 +1,419 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# Libervia: a Salut à Toi frontend +# Copyright (C) 2011, 2012, 2013, 2014 Jérôme Poisson <goffi@goffi.org> + +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. + +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. + +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. + +import pyjd # this is dummy in pyjs +from sat.core.log import getLogger +log = getLogger(__name__) +from pyjamas.ui.SimplePanel import SimplePanel +from pyjamas.ui.ScrollPanel import ScrollPanel +from pyjamas.ui.VerticalPanel import VerticalPanel +from pyjamas.ui.ClickListener import ClickHandler +from pyjamas.ui.Label import Label +from pyjamas.ui.HTML import HTML +from pyjamas import Window +from pyjamas import DOM +from __pyjamas__ import doc + +from jid import JID + +import base_panels +import base_widget +import panels +import html_tools + + +def setPresenceStyle(element, presence, base_style="contact"): + """ + Set the CSS style of a contact's element according to its presence. + @param item: the UI element of the contact + @param presence: a value in ("", "chat", "away", "dnd", "xa"). + @param base_style: the base name of the style to apply + """ + if not hasattr(element, 'presence_style'): + element.presence_style = None + style = '%s-%s' % (base_style, presence or 'connected') + if style == element.presence_style: + return + if element.presence_style is not None: + element.removeStyleName(element.presence_style) + element.addStyleName(style) + element.presence_style = style + + +class GroupLabel(base_widget.DragLabel, Label, ClickHandler): + def __init__(self, host, group): + self.group = group + self.host = host + Label.__init__(self, group) # , Element=DOM.createElement('div') + self.setStyleName('group') + base_widget.DragLabel.__init__(self, group, "GROUP") + ClickHandler.__init__(self) + self.addClickListener(self) + + def onClick(self, sender): + self.host.getOrCreateLiberviaWidget(panels.MicroblogPanel, self.group) + + +class ContactLabel(base_widget.DragLabel, HTML, ClickHandler): + def __init__(self, host, jid, name=None, handleClick=True): + HTML.__init__(self) + self.host = host + self.name = name or jid + self.waiting = False + self.jid = jid + self._fill() + self.setStyleName('contact') + base_widget.DragLabel.__init__(self, jid, "CONTACT") + if handleClick: + ClickHandler.__init__(self) + self.addClickListener(self) + + def _fill(self): + if self.waiting: + _wait_html = "<b>(*)</b> " + self.setHTML("%(wait)s%(name)s" % {'wait': _wait_html, + 'name': html_tools.html_sanitize(self.name)}) + + def setMessageWaiting(self, waiting): + """Show a visual indicator if message are waiting + @param waiting: True if message are waiting""" + self.waiting = waiting + self._fill() + + def onClick(self, sender): + self.host.getOrCreateLiberviaWidget(panels.ChatPanel, self.jid) + + +class GroupList(VerticalPanel): + + def __init__(self, parent): + VerticalPanel.__init__(self) + self.setStyleName('groupList') + self._parent = parent + + def add(self, group): + _item = GroupLabel(self._parent.host, group) + _item.addMouseListener(self._parent) + DOM.setStyleAttribute(_item.getElement(), "cursor", "pointer") + index = 0 + for group_ in [child.group for child in self.getChildren()]: + if group_ > group: + break + index += 1 + VerticalPanel.insert(self, _item, index) + + def remove(self, group): + for wid in self: + if isinstance(wid, GroupLabel) and wid.group == group: + VerticalPanel.remove(self, wid) + + +class GenericContactList(VerticalPanel): + """Class that can be used to represent a contact list, but not necessarily + the one that is displayed on the left side. Special features like popup menu + panel or changing the contact states must be done in a sub-class.""" + + def __init__(self, host, handleClick=False): + VerticalPanel.__init__(self) + self.host = host + self.contacts = [] + self.handleClick = handleClick + + def add(self, jid, name=None, item_cb=None): + if jid in self.contacts: + return + index = 0 + for contact_ in self.contacts: + if contact_ > jid: + break + index += 1 + self.contacts.insert(index, jid) + _item = ContactLabel(self.host, jid, name, handleClick=self.handleClick) + DOM.setStyleAttribute(_item.getElement(), "cursor", "pointer") + VerticalPanel.insert(self, _item, index) + if item_cb is not None: + item_cb(_item) + + def remove(self, jid): + wid = self.getContactLabel(jid) + if not wid: + return + VerticalPanel.remove(self, wid) + self.contacts.remove(jid) + + def isContactPresent(self, contact_jid): + """Return True if a contact is present in the panel""" + return contact_jid in self.contacts + + def getContacts(self): + return self.contacts + + def getContactLabel(self, contact_jid): + """get contactList widget of a contact + @return: ContactLabel item if present, else None""" + for wid in self: + if isinstance(wid, ContactLabel) and wid.jid == contact_jid: + return wid + return None + + +class ContactList(GenericContactList): + """The contact list that is displayed on the left side.""" + + def __init__(self, host): + GenericContactList.__init__(self, host, handleClick=True) + self.menu_entries = {"blog": {"title": "Public blog..."}} + self.context_menu = base_panels.PopupMenuPanel(entries=self.menu_entries, + hide=self.contextMenuHide, + callback=self.contextMenuCallback, + vertical=False, style={"selected": "menu-selected"}) + + def contextMenuHide(self, sender, key): + """Return True if the item for that sender should be hidden.""" + # TODO: enable the blogs of users that are on another server + return JID(sender.jid).domain != self.host._defaultDomain + + def contextMenuCallback(self, sender, key): + if key == "blog": + # TODO: use the bare when all blogs can be retrieved + node = JID(sender.jid).node + web_panel = panels.WebPanel(self.host, "/blog/%s" % node) + self.host.addTab("%s's blog" % node, web_panel) + else: + sender.onClick(sender) + + def add(self, jid_s, name=None): + """Add a contact + + @param jid_s (str): JID as unicode + @param name (str): nickname + """ + def item_cb(item): + self.context_menu.registerRightClickSender(item) + GenericContactList.add(self, jid_s, name, item_cb) + + def setState(self, jid, type_, state): + """Change the appearance of the contact, according to the state + @param jid: jid which need to change state + @param type_: one of availability, messageWaiting + @param state: + - for messageWaiting type: + True if message are waiting + - for availability type: + 'unavailable' if not connected, else presence like RFC6121 #4.7.2.1""" + _item = self.getContactLabel(jid) + if _item: + if type_ == 'availability': + setPresenceStyle(_item, state) + elif type_ == 'messageWaiting': + _item.setMessageWaiting(state) + + +class ContactTitleLabel(base_widget.DragLabel, Label, ClickHandler): + def __init__(self, host, text): + Label.__init__(self, text) # , Element=DOM.createElement('div') + self.host = host + self.setStyleName('contactTitle') + base_widget.DragLabel.__init__(self, text, "CONTACT_TITLE") + ClickHandler.__init__(self) + self.addClickListener(self) + + def onClick(self, sender): + self.host.getOrCreateLiberviaWidget(panels.MicroblogPanel, None) + + +class ContactPanel(SimplePanel): + """Manage the contacts and groups""" + + def __init__(self, host): + SimplePanel.__init__(self) + + self.scroll_panel = ScrollPanel() + + self.host = host + self.groups = {} + self.connected = {} # jid connected as key and their status + + self.vPanel = VerticalPanel() + _title = ContactTitleLabel(host, 'Contacts') + DOM.setStyleAttribute(_title.getElement(), "cursor", "pointer") + + self._contact_list = ContactList(host) + self._contact_list.setStyleName('contactList') + self._groupList = GroupList(self) + self._groupList.setStyleName('groupList') + + self.vPanel.add(_title) + self.vPanel.add(self._groupList) + self.vPanel.add(self._contact_list) + self.scroll_panel.add(self.vPanel) + self.add(self.scroll_panel) + self.setStyleName('contactBox') + Window.addWindowResizeListener(self) + + def onWindowResized(self, width, height): + contact_panel_elt = self.getElement() + classname = 'widgetsPanel' if isinstance(self.getParent().getParent(), panels.UniBoxPanel) else'gwt-TabBar' + _elts = doc().getElementsByClassName(classname) + if not _elts.length: + log.error("no element of class %s found, it should exist !" % classname) + tab_bar_h = height + else: + tab_bar_h = DOM.getAbsoluteTop(_elts.item(0)) or height # getAbsoluteTop can be 0 if tabBar is hidden + + ideal_height = tab_bar_h - DOM.getAbsoluteTop(contact_panel_elt) - 5 + self.scroll_panel.setHeight("%s%s" % (ideal_height, "px")) + + def updateContact(self, jid_s, attributes, groups): + """Add a contact to the panel if it doesn't exist, update it else + @param jid_s: jid userhost as unicode + @param attributes: cf SàT Bridge API's newContact + @param groups: list of groups""" + _current_groups = self.getContactGroups(jid_s) + _new_groups = set(groups) + _key = "@%s: " + + for group in _current_groups.difference(_new_groups): + # We remove the contact from the groups where he isn't anymore + self.groups[group].remove(jid_s) + if not self.groups[group]: + # The group is now empty, we must remove it + del self.groups[group] + self._groupList.remove(group) + if self.host.uni_box: + self.host.uni_box.removeKey(_key % group) + + for group in _new_groups.difference(_current_groups): + # We add the contact to the groups he joined + if not group in self.groups.keys(): + self.groups[group] = set() + self._groupList.add(group) + if self.host.uni_box: + self.host.uni_box.addKey(_key % group) + self.groups[group].add(jid_s) + + # We add the contact to contact list, it will check if contact already exists + self._contact_list.add(jid_s) + + def removeContact(self, jid): + """Remove contacts from groups where he is and contact list""" + self.updateContact(jid, {}, []) # we remove contact from every group + self._contact_list.remove(jid) + + def setConnected(self, jid, resource, availability, priority, statuses): + """Set connection status + @param jid: JID userhost as unicode + """ + if availability == 'unavailable': + if jid in self.connected: + if resource in self.connected[jid]: + del self.connected[jid][resource] + if not self.connected[jid]: + del self.connected[jid] + else: + if not jid in self.connected: + self.connected[jid] = {} + self.connected[jid][resource] = (availability, priority, statuses) + + # check if the contact is connected with another resource, use the one with highest priority + if jid in self.connected: + max_resource = max_priority = None + for tmp_resource in self.connected[jid]: + if max_priority is None or self.connected[jid][tmp_resource][1] >= max_priority: + max_resource = tmp_resource + max_priority = self.connected[jid][tmp_resource][1] + if availability == "unavailable": # do not check the priority here, because 'unavailable' has a dummy one + priority = max_priority + availability = self.connected[jid][max_resource][0] + if jid not in self.connected or priority >= max_priority: + # case 1: jid not in self.connected means all resources are disconnected, update with 'unavailable' + # case 2: update (or confirm) with the values of the resource which takes precedence + self._contact_list.setState(jid, "availability", availability) + + # update the connected contacts chooser live + if hasattr(self.host, "room_contacts_chooser") and self.host.room_contacts_chooser is not None: + self.host.room_contacts_chooser.resetContacts() + + def setContactMessageWaiting(self, jid, waiting): + """Show an visual indicator that contact has send a message + @param jid: jid of the contact + @param waiting: True if message are waiting""" + self._contact_list.setState(jid, "messageWaiting", waiting) + + def getConnected(self, filter_muc=False): + """return a list of all jid (bare jid) connected + @param filter_muc: if True, remove the groups from the list + """ + contacts = self.connected.keys() + contacts.sort() + return contacts if not filter_muc else list(set(contacts).intersection(set(self.getContacts()))) + + def getContactGroups(self, contact_jid_s): + """Get groups where contact is + @param group: string of single group, or list of string + @param contact_jid_s: jid to test, as unicode + """ + result = set() + for group in self.groups: + if self.isContactInGroup(group, contact_jid_s): + result.add(group) + return result + + def isContactInGroup(self, group, contact_jid): + """Test if the contact_jid is in the group + @param group: string of single group, or list of string + @param contact_jid: jid to test + @return: True if contact_jid is in on of the groups""" + if group in self.groups and contact_jid in self.groups[group]: + return True + return False + + def isContactInRoster(self, contact_jid): + """Test if the contact is in our roster list""" + for _contact_label in self._contact_list: + if contact_jid == _contact_label.jid: + return True + return False + + def getContacts(self): + return self._contact_list.getContacts() + + def getGroups(self): + return self.groups.keys() + + def onMouseMove(self, sender, x, y): + pass + + def onMouseDown(self, sender, x, y): + pass + + def onMouseUp(self, sender, x, y): + pass + + def onMouseEnter(self, sender): + if isinstance(sender, GroupLabel): + for contact in self._contact_list: + if contact.jid in self.groups[sender.group]: + contact.addStyleName("selected") + + def onMouseLeave(self, sender): + if isinstance(sender, GroupLabel): + for contact in self._contact_list: + if contact.jid in self.groups[sender.group]: + contact.removeStyleName("selected")