diff src/browser/contact.py @ 449:981ed669d3b3

/!\ reorganize all the file hierarchy, move the code and launching script to src: - browser_side --> src/browser - public --> src/browser_side/public - libervia.py --> src/browser/libervia_main.py - libervia_server --> src/server - libervia_server/libervia.sh --> src/libervia.sh - twisted --> src/twisted - new module src/common - split constants.py in 3 files: - src/common/constants.py - src/browser/constants.py - src/server/constants.py - output --> html (generated by pyjsbuild during the installation) - new option/parameter "data_dir" (-d) to indicates the directory containing html and server_css - setup.py installs libervia to the following paths: - src/common --> <LIB>/libervia/common - src/server --> <LIB>/libervia/server - src/twisted --> <LIB>/twisted - html --> <SHARE>/libervia/html - server_side --> <SHARE>libervia/server_side - LIBERVIA_INSTALL environment variable takes 2 new options with prompt confirmation: - clean: remove previous installation directories - purge: remove building and previous installation directories You may need to update your sat.conf and/or launching script to update the following options/parameters: - ssl_certificate - data_dir
author souliane <souliane@mailoo.org>
date Tue, 20 May 2014 06:41:16 +0200
parents browser_side/contact.py@d52f529a6d42
children 1a0cec9b0f1e
line wrap: on
line diff
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/browser/contact.py	Tue May 20 06:41:16 2014 +0200
@@ -0,0 +1,415 @@
+#!/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
+
+from base_panels import PopupMenuPanel
+from base_widget import DragLabel
+from panels import ChatPanel, MicroblogPanel, WebPanel, UniBoxPanel
+from html_tools import html_sanitize
+
+
+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(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')
+        DragLabel.__init__(self, group, "GROUP")
+        ClickHandler.__init__(self)
+        self.addClickListener(self)
+
+    def onClick(self, sender):
+        self.host.getOrCreateLiberviaWidget(MicroblogPanel, self.group)
+
+
+class ContactLabel(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')
+        DragLabel.__init__(self, jid, "CONTACT")
+        if handleClick:
+            ClickHandler.__init__(self)
+            self.addClickListener(self)
+
+    def _fill(self):
+        if self.waiting:
+            _wait_html = "<b>(*)</b>&nbsp;"
+        self.setHTML("%(wait)s%(name)s" % {'wait': _wait_html,
+                                           'name': 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(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 [group.group for group 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 = 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 = WebPanel(self.host, "/blog/%s" % node)
+            self.host.addTab("%s's blog" % node, web_panel)
+        else:
+            sender.onClick(sender)
+
+    def add(self, jid, name=None):
+        def item_cb(item):
+            self.context_menu.registerRightClickSender(item)
+        GenericContactList.add(self, jid, 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(DragLabel, Label, ClickHandler):
+    def __init__(self, host, text):
+        Label.__init__(self, text) #, Element=DOM.createElement('div')
+        self.host = host
+        self.setStyleName('contactTitle')
+        DragLabel.__init__(self, text, "CONTACT_TITLE")
+        ClickHandler.__init__(self)
+        self.addClickListener(self)
+
+    def onClick(self, sender):
+        self.host.getOrCreateLiberviaWidget(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(), 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, attributes, groups):
+        """Add a contact to the panel if it doesn't exist, update it else
+        @param jid: jid userhost as unicode
+        @attributes: cf SàT Bridge API's newContact
+        @param groups: list of groups"""
+        _current_groups = self.getContactGroups(jid)
+        _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)
+            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)
+
+        # We add the contact to contact list, it will check if contact already exists
+        self._contact_list.add(jid)
+
+    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):
+        """Get groups where contact is
+       @param group: string of single group, or list of string
+       @param contact_jid: jid to test
+        """
+        result = set()
+        for group in self.groups:
+            if self.isContactInGroup(group, contact_jid):
+                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")
+