diff 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 diff
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/browser/sat_browser/contact_list.py	Sat Jan 24 01:45:39 2015 +0100
@@ -0,0 +1,590 @@
+#!/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())