view 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
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 sat_frontends.quick_frontend.quick_contact_list import QuickContactList
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.ui.Image import Image
from pyjamas import Window
from pyjamas import DOM
from __pyjamas__ import doc

from sat_frontends.tools import jid
from constants import Const as C
import base_widget
import panels
import html_tools
import chat


def buildPresenceStyle(presence, base_style=None):
    """Return the CSS classname to be used for displaying the given presence information.
    @param presence (str): presence is a value in ('', 'chat', 'away', 'dnd', 'xa')
    @param base_style (str): base classname
    @return: str
    """
    if not base_style:
        base_style = "contactLabel"
    return '%s-%s' % (base_style, presence or 'connected')


def setPresenceStyle(widget, presence, base_style=None):
    """
    Set the CSS style of a contact's element according to its presence.

    @param widget (Widget): the UI element of the contact
    @param presence (str): a value in ("", "chat", "away", "dnd", "xa").
    @param base_style (str): the base name of the style to apply
    """
    if not hasattr(widget, 'presence_style'):
        widget.presence_style = None
    style = buildPresenceStyle(presence, base_style)
    if style == widget.presence_style:
        return
    if widget.presence_style is not None:
        widget.removeStyleName(widget.presence_style)
    widget.addStyleName(style)
    widget.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, {'item': self.group})


class ContactLabel(HTML):
    def __init__(self, jid, name=None):
        HTML.__init__(self)
        self.name = name or str(jid)
        self.waiting = False
        self.refresh()
        self.setStyleName('contactLabel')

    def refresh(self):
        if self.waiting:
            wait_html = "<b>(*)</b>&nbsp;"
        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.refresh()


class ContactMenuBar(base_widget.WidgetMenuBar):

    def onBrowserEvent(self, event):
        base_widget.WidgetMenuBar.onBrowserEvent(self, event)
        event.stopPropagation()  # prevent opening the chat dialog

    @classmethod
    def getCategoryHTML(cls, menu_name_i18n, type_):
        return '<img src="%s"/>' % C.DEFAULT_AVATAR

    def setUrl(self, url):
        """Set the URL of the contact avatar."""
        self.items[0].setHTML('<img src="%s" />' % url)


class ContactBox(VerticalPanel, ClickHandler, base_widget.DragLabel):

    def __init__(self, host, jid_, name=None, click_listener=None, handle_menu=None):
        VerticalPanel.__init__(self, StyleName='contactBox', VerticalAlignment='middle')
        base_widget.DragLabel.__init__(self, jid_, "CONTACT")
        self.host = host
        self.jid = jid_
        self.label = ContactLabel(jid_, name)
        self.avatar = ContactMenuBar(self, host) if handle_menu else Image()
        # self.updateAvatar(host.getAvatar(jid_)) # FIXME
        self.add(self.avatar)
        self.add(self.label)
        if click_listener:
            ClickHandler.__init__(self)
            self.addClickListener(self)
            self.click_listener = click_listener

    def addMenus(self, menu_bar):
        menu_bar.addCachedMenus(C.MENU_ROSTER_JID_CONTEXT, {'jid': self.jid})
        menu_bar.addCachedMenus(C.MENU_JID_CONTEXT, {'jid': self.jid})

    def setMessageWaiting(self, waiting):
        """Show a visual indicator if message are waiting

        @param waiting: True if message are waiting"""
        self.label.setMessageWaiting(waiting)

    def updateAvatar(self, url):
        """Update the avatar.

        @param url (str): image url
        """
        self.avatar.setUrl(url)

    def onClick(self, sender):
        self.click_listener(self.jid)


class GroupPanel(VerticalPanel):

    def __init__(self, parent):
        VerticalPanel.__init__(self)
        self.setStyleName('groupPanel')
        self._parent = parent
        self._groups = set()

    def add(self, group):
        if group in self._groups:
            log.warning("trying to add an already existing group")
            return
        _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)
        self._groups.add(group)

    def remove(self, group):
        for wid in self:
            if isinstance(wid, GroupLabel) and wid.group == group:
                VerticalPanel.remove(self, wid)
                self._groups.remove(group)
                return
        log.warning("Trying to remove a non existent group")

    def getGroupBox(self, group):
        """get the widget of a group

        @param group (str): the group
        @return: GroupLabel instance if present, else None"""
        for wid in self:
            if isinstance(wid, GroupLabel) and wid.group == group:
                return wid
        return None

    def getGroups(self):
        return self._groups


class BaseContactsPanel(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, handle_click=False, handle_menu=False):
        VerticalPanel.__init__(self)
        self.host = host
        self.contacts = []
        self.click_listener = None
        self.handle_menu = handle_menu

        if handle_click:
            def cb(contact_jid):
                host.widgets.getOrCreateWidget(chat.Chat, contact_jid, type_=C.CHAT_ONE2ONE, profile=C.PROF_KEY_NONE)
            self.click_listener = cb

    def add(self, jid_, name=None):
        """Add a contact to the list.

        @param jid_ (jid.JID): jid_ of the contact
        @param name (str): optional name of the contact
        """
        assert isinstance(jid_, jid.JID)
        if jid_ in self.contacts:
            return
        index = 0
        for contact_ in self.contacts:
            if contact_ > jid_:
                break
            index += 1
        self.contacts.insert(index, jid_)
        box = ContactBox(self.host, jid_, name, self.click_listener, self.handle_menu)
        VerticalPanel.insert(self, box, index)

    def remove(self, jid_):
        box = self.getContactBox(jid_)
        if not box:
            return
        VerticalPanel.remove(self, box)
        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 getContactBox(self, contact_jid):
        """get the widget of a contact

        @param contact_jid (jid.JID): the contact
        @return: ContactBox instance if present, else None"""
        for wid in self:
            if isinstance(wid, ContactBox) and wid.jid == contact_jid:
                return wid
        return None

    def updateAvatar(self, jid_, url):
        """Update the avatar of the given contact

        @param jid_ (jid.JID): contact jid
        @param url (str): image url
        """
        try:
            self.getContactBox(jid_).updateAvatar(url)
        except TypeError:
            pass


class ContactsPanel(BaseContactsPanel):
    """The contact list that is displayed on the left side."""

    def __init__(self, host):
        BaseContactsPanel.__init__(self, host, handle_click=True, handle_menu=True)

    def setState(self, jid_, type_, state):
        """Change the appearance of the contact, according to the state
        @param jid_ (jid.JID): jid.JID which need to change state
        @param type_ (str): one of "availability", "messageWaiting"
        @param state:
            - for messageWaiting type:
                True if message are waiting
            - for availability type:
                C.PRESENCE_UNAVAILABLE or None if not connected, else presence like RFC6121 #4.7.2.1"""
        assert type_ in ('availability', 'messageWaiting')
        contact_box = self.getContactBox(jid_)
        if not contact_box:
            log.warning("No contact box found for {}".format(jid_))
        else:
            if type_ == 'availability':
                if state is None:
                    state = C.PRESENCE_UNAVAILABLE
                setPresenceStyle(contact_box.label, state)
            elif type_ == 'messageWaiting':
                contact_box.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, {'item': None})


class ContactList(SimplePanel, QuickContactList):
    """Manage the contacts and groups"""

    def __init__(self, host):
        QuickContactList.__init__(self, host, C.PROF_KEY_NONE)
        SimplePanel.__init__(self)
        self.scroll_panel = ScrollPanel()
        self.vPanel = VerticalPanel()
        _title = ContactTitleLabel(host, 'Contacts')
        DOM.setStyleAttribute(_title.getElement(), "cursor", "pointer")
        self._contacts_panel = ContactsPanel(host)
        self._contacts_panel.setStyleName('contactPanel') # FIXME: style doesn't exists !
        self._group_panel = GroupPanel(self)

        self.vPanel.add(_title)
        self.vPanel.add(self._group_panel)
        self.vPanel.add(self._contacts_panel)
        self.scroll_panel.add(self.vPanel)
        self.add(self.scroll_panel)
        self.setStyleName('contactList')
        Window.addWindowResizeListener(self)

    @property
    def profile(self):
        return C.PROF_KEY_NONE

    def update(self):
        ### GROUPS ###
        _keys = self._groups.keys()
        try:
            # XXX: Pyjamas doesn't do the set casting if None is present
            _keys.remove(None)
        except KeyError:
            pass
        current_groups = set(_keys)
        shown_groups = self._group_panel.getGroups()
        new_groups = current_groups.difference(shown_groups)
        removed_groups = shown_groups.difference(current_groups)
        for group in new_groups:
            self._group_panel.add(group)
        for group in removed_groups:
            self._group_panel.remove(group)

        ### JIDS ###
        current_contacts = set(self._cache.keys())
        shown_contacts = set(self._contacts_panel.getContacts())
        new_contacts = current_contacts.difference(shown_contacts)
        removed_contacts = shown_contacts.difference(current_contacts)

        for contact in new_contacts:
            self._contacts_panel.add(contact)
        for contact in removed_contacts:
            self._contacts_panel.remove(contact)

    def onWindowResized(self, width, height):
        contact_panel_elt = self.getElement()
        # FIXME: still needed ?
        # classname = 'widgetsPanel' if isinstance(self.getParent().getParent(), panels.UniBoxPanel) else 'gwt-TabBar'
        classname = '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._group_panel.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 group not in self.groups.keys():
    #             self.groups[group] = set()
    #             self._group_panel.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._contacts_panel.add(jid_s)
    #     self.updateVisibility([jid_s], self.getContactGroups(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._contacts_panel.remove(jid)

    # def setConnected(self, jid_s, resource, availability, priority, statuses):
    #     """Set connection status
    #     @param jid_s (str): JID userhost as unicode
    #     """
    #     if availability == 'unavailable':
    #         if jid_s in self.connected:
    #             if resource in self.connected[jid_s]:
    #                 del self.connected[jid_s][resource]
    #             if not self.connected[jid_s]:
    #                 del self.connected[jid_s]
    #     else:
    #         if jid_s not in self.connected:
    #             self.connected[jid_s] = {}
    #         self.connected[jid_s][resource] = (availability, priority, statuses)

    #     # check if the contact is connected with another resource, use the one with highest priority
    #     if jid_s in self.connected:
    #         max_resource = max_priority = None
    #         for tmp_resource in self.connected[jid_s]:
    #             if max_priority is None or self.connected[jid_s][tmp_resource][1] >= max_priority:
    #                 max_resource = tmp_resource
    #                 max_priority = self.connected[jid_s][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_s][max_resource][0]
    #     if jid_s 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._contacts_panel.setState(jid_s, "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()

    #     self.updateVisibility([jid_s], self.getContactGroups(jid_s))

    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._contacts_panel.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_box in self._contacts_panel:
            if contact_jid == contact_box.jid:
                return True
        return False

    # def getContacts(self):
    #     return self._contacts_panel.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):
            jids = self.getGroupData(sender.group, "jids")
            for contact in self._contacts_panel:
                if contact.jid in jids:
                    contact.label.addStyleName("selected")

    def onMouseLeave(self, sender):
        if isinstance(sender, GroupLabel):
            jids = self.getGroupData(sender.group, "jids")
            for contact in self._contacts_panel:
                if contact.jid in jids:
                    contact.label.removeStyleName("selected")

    def updateAvatar(self, jid_s, url):
        """Update the avatar of the given contact

        @param jid_s (str): contact jid
        @param url (str): image url
        """
        self._contacts_panel.updateAvatar(jid_s, url)

    def hasVisibleMembers(self, group):
        """Tell if the given group actually has visible members

        @param group (str): the group to check
        @return: boolean
        """
        for jid_ in self.groups[group]:
            if self._contacts_panel.getContactBox(jid_).isVisible():
                return True
        return False

    def offlineContactsToShow(self):
        """Tell if offline contacts should be visible according to the user settings

        @return: boolean
        """
        return self.host.getCachedParam('General', C.SHOW_OFFLINE_CONTACTS) == 'true'

    def emtyGroupsToShow(self):
        """Tell if empty groups should be visible according to the user settings

        @return: boolean
        """
        return self.host.getCachedParam('General', C.SHOW_EMPTY_GROUPS) == 'true'

    def updatePresence(self, entity, show, priority, statuses):
        QuickContactList.updatePresence(self, entity, show, priority, statuses)
        entity_bare = entity.bare
        show = self.getCache(entity_bare, C.PRESENCE_SHOW) # we use cache to have the show nformation of main resource only
        self._contacts_panel.setState(entity_bare, "availability", show)

    # def updateVisibility(self, jids, groups):
    #     """Set the widgets visibility for the given contacts and groups

    #     @param jids (list[str]): list of JID
    #     @param groups (list[str]): list of groups
    #     """
    #     for jid_s in jids:
    #         try:
    #             self._contacts_panel.getContactBox(jid_s).setVisible(jid_s in self.connected or self.offlineContactsToShow())
    #         except TypeError:
    #             log.warning('No box for contact %s: this code line should not be reached' % jid_s)
    #     for group in groups:
    #         try:
    #             self._group_panel.getGroupBox(group).setVisible(self.hasVisibleMembers(group) or self.emtyGroupsToShow())
    #         except TypeError:
    #             log.warning('No box for group %s: this code line should not be reached' % group)

    # def refresh(self):
    #     """Show or hide disconnected contacts and empty groups"""
    #     self.updateVisibility(self._contacts_panel.contacts, self.groups.keys())