view browser_side/contact.py @ 444:b64e528fb524

server side: use of bridge's getReady to be sure that backend is initialised at launch
author Goffi <goffi@goffi.org>
date Thu, 15 May 2014 20:27:52 +0200
parents d52f529a6d42
children
line wrap: on
line source

#!/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 jid import JID
from pyjamas import Window
from pyjamas import DOM

from browser_side.base_panels import PopupMenuPanel
from browser_side.base_widget import DragLabel
from browser_side.panels import ChatPanel, MicroblogPanel, WebPanel, UniBoxPanel
from browser_side.html_tools import html_sanitize
from __pyjamas__ import doc


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")