diff frontends/src/quick_frontend/quick_contact_list.py @ 1367:f71a0fc26886

merged branch frontends_multi_profiles
author Goffi <goffi@goffi.org>
date Wed, 18 Mar 2015 10:52:28 +0100
parents ec9e58357a07
children 017270e6eea4
line wrap: on
line diff
--- a/frontends/src/quick_frontend/quick_contact_list.py	Thu Feb 05 11:59:26 2015 +0100
+++ b/frontends/src/quick_frontend/quick_contact_list.py	Wed Mar 18 10:52:28 2015 +0100
@@ -20,41 +20,237 @@
 from sat.core.i18n import _
 from sat.core.log import getLogger
 log = getLogger(__name__)
+from sat_frontends.quick_frontend.quick_widgets import QuickWidget
+from sat_frontends.quick_frontend.constants import Const as C
+from sat_frontends.tools import jid
 
 
-class QuickContactList(object):
+try:
+    # FIXME: to be removed when an acceptable solution is here
+    unicode('') # XXX: unicode doesn't exist in pyjamas
+except (TypeError, AttributeError): # Error raised is not the same depending on pyjsbuild options
+    # XXX: pyjamas' max doesn't support key argument, so we implement it ourself
+    pyjamas_max = max
+    def max(iterable, key):
+        iter_cpy = list(iterable)
+        iter_cpy.sort(key=key)
+        return pyjamas_max(iter_cpy)
+
+
+class QuickContactList(QuickWidget):
     """This class manage the visual representation of contacts"""
 
-    def __init__(self):
+    def __init__(self, host, profile):
         log.debug(_("Contact List init"))
+        super(QuickContactList, self).__init__(host, profile, profile)
+        # bare jids as keys, resources are used in data
         self._cache = {}
-        self.specials={}
+
+        # special entities (groupchat, gateways, etc), bare jids
+        self._specials = set()
+        # extras are specials with full jids (e.g.: private MUC conversation)
+        self._special_extras = set()
+
+        # group data contain jids in groups and misc frontend data
+        self._groups = {} # groups to group data map
+
+        # contacts in roster (bare jids)
+        self._roster = set()
+
+        # entities with an alert (usually a waiting message), full jid
+        self._alerts = set()
+
+        # selected entities, full jid
+        self._selected = set()
+
+        # we keep our own jid
+        self.whoami = host.profiles[profile].whoami
+
+        # options
+        self.show_disconnected = False
+        self.show_empty_groups = True
+        self.show_resources = False
+        self.show_status = False
+        # TODO: this may lead to two successive UI refresh and needs an optimization
+        self.host.bridge.asyncGetParamA(C.SHOW_EMPTY_GROUPS, "General", profile_key=profile, callback=self._showEmptyGroups)
+        self.host.bridge.asyncGetParamA(C.SHOW_OFFLINE_CONTACTS, "General", profile_key=profile, callback=self._showOfflineContacts)
+
+        # FIXME: workaround for a pyjamas issue: calling hash on a class method always return a different value if that method is defined directly within the class (with the "def" keyword)
+        self.presenceListener = self.onPresenceUpdate
+        self.host.addListener('presence', self.presenceListener, [profile])
+        self.nickListener = self.onNickUpdate
+        self.host.addListener('nick', self.nickListener, [profile])
+
+    def __contains__(self, entity):
+        """Check if entity is in contact list
+
+        @param entity (jid.JID): jid of the entity (resource is not ignored, use bare jid if needed)
+        """
+        if entity.resource:
+            try:
+                return entity.resource in self.getCache(entity.bare, C.CONTACT_RESOURCES)
+            except KeyError:
+                return False
+        return entity in self._cache
 
-    def update_jid(self, jid):
-        """Update the jid in the list when something changed"""
+    @property
+    def roster_entities(self):
+        """Return all the bare JIDs of the roster entities.
+
+        @return: set(jid.JID)
+        """
+        return self._roster
+
+    @property
+    def roster_entities_connected(self):
+        """Return all the bare JIDs of the roster entities that are connected.
+
+        @return: set(jid.JID)
+        """
+        return set([entity for entity in self._roster if self.getCache(entity, C.PRESENCE_SHOW) is not None])
+
+    @property
+    def roster_entities_by_group(self):
+        """Return a dictionary binding the roster groups to their entities bare
+        JIDs. This also includes the empty group (None key).
+
+        @return: dict{unicode: set(jid.JID)}
+        """
+        return {group: self._groups[group]['jids'] for group in self._groups}
+
+    @property
+    def roster_groups_by_entity(self):
+        """Return a dictionary binding the entities bare JIDs to their roster
+        groups. The empty group is filtered out.
+
+        @return: dict{jid.JID: set(unicode)}
+        """
+        result = {}
+        for group, data in self._groups.iteritems():
+            if group is None:
+                continue
+            for entity in data['jids']:
+                result.setdefault(entity, set()).add(group)
+        return result
+
+    def fill(self):
+        """Get all contacts from backend, and fill the widget"""
+        def gotContacts(contacts):
+            for contact in contacts:
+                self.host.newContactHandler(*contact, profile=self.profile)
+
+        self.host.bridge.getContacts(self.profile, callback=gotContacts)
+
+    def update(self):
+        """Update the display when something changed"""
         raise NotImplementedError
 
-    def getCache(self, jid, name):
+    def getCache(self, entity, name=None):
+        """Return a cache value for a contact
+
+        @param entity(entity.entity): entity of the contact from who we want data (resource is used if given)
+            if a resource specific information is requested:
+                - if no resource is given (bare jid), the main resource is used, according to priority
+                - if resource is given, it is used
+        @param name(unicode): name the data to get, or None to get everything
+        @return: full cache if no name is given, or value of "name", or None
+        """
         try:
-            jid_cache = self._cache[jid.bare]
-            if name == 'status': #XXX: we get the first status for 'status' key
-                return jid_cache['statuses'].get('default','')
-            return jid_cache[name]
-        except (KeyError, IndexError):
+            cache = self._cache[entity.bare]
+        except KeyError:
+            self.setContact(entity)
+            cache = self._cache[entity.bare]
+
+        if name is None:
+            return cache
+        try:
+            if name in ('status', C.PRESENCE_STATUSES, C.PRESENCE_PRIORITY, C.PRESENCE_SHOW):
+                # these data are related to the resource
+                if not entity.resource:
+                    main_resource = cache[C.CONTACT_MAIN_RESOURCE]
+                    cache = cache[C.CONTACT_RESOURCES][main_resource]
+                else:
+                    cache = cache[C.CONTACT_RESOURCES][entity.resource]
+
+                if name == 'status': #XXX: we get the first status for 'status' key
+                    # TODO: manage main language for statuses
+                    return cache[C.PRESENCE_STATUSES].get('default','')
+
+            return cache[name]
+        except KeyError:
             return None
 
-    def setCache(self, jid, name, value):
-        jid_cache = self._cache.setdefault(jid.bare, {})
-        jid_cache[name] = value
+    def setCache(self, entity, name, value):
+        """Set or update value for one data in cache
+
+        @param entity(JID): entity to update
+        @param name(unicode): value to set or update
+        """
+        self.setContact(entity, None, {name: value})
+
+    def getFullJid(self, entity):
+        """Get full jid from a bare jid
+
+        @param entity(jid.JID): must be a bare jid
+        @return (jid.JID): bare jid + main resource
+        @raise ValueError: the entity is not bare
+        """
+        if entity.resource:
+            raise ValueError("getFullJid must be used with a bare jid")
+        main_resource = self.getCache(entity, C.CONTACT_MAIN_RESOURCE)
+        return jid.JID(u"{}/{}".format(entity, main_resource))
+
+
+    def setGroupData(self, group, name, value):
+        """Register a data for a group
+
+        @param group: a valid (existing) group name
+        @param name: name of the data (can't be "jids")
+        @param value: value to set
+        """
+        assert name is not 'jids'
+        self._groups[group][name] = value
 
-    def __contains__(self, jid):
-        raise NotImplementedError
+    def getGroupData(self, group, name=None):
+        """Return value associated to group data
+
+        @param group: a valid (existing) group name
+        @param name: name of the data or None to get the whole dict
+        @return: registered value
+        """
+        if name is None:
+            return self._groups[group]
+        return self._groups[group][name]
+
+    def setSpecial(self, entity, special_type):
+        """Set special flag on an entity
+
+        @param entity(jid.JID): jid of the special entity
+        @param special_type: one of special type (e.g. C.CONTACT_SPECIAL_GROUP) or None to remove special flag
+        """
+        assert special_type in C.CONTACT_SPECIAL_ALLOWED + (None,)
+        self.setCache(entity, C.CONTACT_SPECIAL, special_type)
+
+    def getSpecials(self, special_type=None):
+        """Return all the bare JIDs of the special roster entities of the type
+        specified by special_type. If special_type is None, return all specials.
+
+        @param special_type: one of special type (e.g. C.CONTACT_SPECIAL_GROUP) or None to return all specials.
+        @return: set(jid.JID)
+        """
+        if special_type is None:
+            return self._specials
+        return set([entity for entity in self._specials if self.getCache(entity, C.CONTACT_SPECIAL) == special_type])
 
     def clearContacts(self):
         """Clear all the contact list"""
-        self.specials.clear()
+        self.unselectAll()
+        self._cache.clear()
+        self._groups.clear()
+        self._specials.clear()
+        self.update()
 
-    def replace(self, jid, groups=None, attributes=None):
+    def setContact(self, entity, groups=None, attributes=None, in_roster=False):
         """Add a contact to the list if doesn't exist, else update it.
 
         This method can be called with groups=None for the purpose of updating
@@ -64,49 +260,212 @@
 
         None value for 'groups' has a different meaning than [None] which is for the default group.
 
-        @param jid (JID)
+        @param entity (jid.JID): entity to add or replace
         @param groups (list): list of groups or None to ignore the groups membership.
-        @param attributes (dict)
+        @param attributes (dict): attibutes of the added jid or to update
+        @param in_roster (bool): True if contact is from roster
         """
-        if attributes and 'name' in attributes:
-            self.setCache(jid, 'name', attributes['name'])
+        if attributes is None:
+            attributes = {}
+
+        entity_bare = entity.bare
+
+        if in_roster:
+            self._roster.add(entity_bare)
+
+        cache = self._cache.setdefault(entity_bare, {C.CONTACT_RESOURCES: {}})
+
+        assert not C.CONTACT_DATA_FORBIDDEN.intersection(attributes) # we don't want forbidden data in attributes
 
-    def remove(self, jid):
-        """remove a contact from the list"""
-        try:
-            del self.specials[jid.bare]
-        except KeyError:
-            pass
+        # we set groups and fill self._groups accordingly
+        if groups is not None:
+            if not groups:
+                groups = [None]  # [None] is the default group
+            if C.CONTACT_GROUPS in cache:
+                # XXX: don't use set(cache[C.CONTACT_GROUPS]).difference(groups) because it won't work in Pyjamas if None is in cache[C.CONTACT_GROUPS]
+                for group in [group for group in cache[C.CONTACT_GROUPS] if group not in groups]:
+                    self._groups[group]['jids'].remove(entity_bare)
+            cache[C.CONTACT_GROUPS] = groups
+            for group in groups:
+                self._groups.setdefault(group, {}).setdefault('jids', set()).add(entity_bare)
+
+        # special entities management
+        if C.CONTACT_SPECIAL in attributes:
+            if attributes[C.CONTACT_SPECIAL] is None:
+                del attributes[C.CONTACT_SPECIAL]
+                self._specials.remove(entity_bare)
+            else:
+                self._specials.add(entity_bare)
+
+        # now the attribute we keep in cache
+        for attribute, value in attributes.iteritems():
+            cache[attribute] = value
+
+        # we can update the display
+        self.update()
+
+    def getContacts(self):
+        """Return contacts currently selected
+
+        @return (set): set of selected entities"""
+        return self._selected
+
+    def entityToShow(self, entity, check_resource=False):
+        """Tell if the contact should be showed or hidden.
 
-    def add(self, jid, param_groups=None):
-        """add a contact to the list"""
-        raise NotImplementedError
+        @param entity (jid.JID): jid of the contact
+        @param check_resource (bool): True if resource must be significant
+        @return (bool): True if that contact should be showed in the list
+        """
+        show = self.getCache(entity, C.PRESENCE_SHOW)
 
-    def getSpecial(self, jid):
-        """Return special type of jid, or None if it's not special"""
-        return self.specials.get(jid.bare)
+        if check_resource:
+            alerts = self._alerts
+            selected = self._selected
+        else:
+            alerts = {alert.bare for alert in self._alerts}
+            selected = {selected.bare for selected in self._selected}
+        return ((show is not None and show != "unavailable")
+                or self.show_disconnected
+                or entity in alerts
+                or entity in selected)
+
+    def anyEntityToShow(self, entities, check_resources=False):
+        """Tell if in a list of entities, at least one should be shown
+
+        @param entities (list[jid.JID]): list of jids
+        @param check_resources (bool): True if resources must be significant
+        @return: bool
+        """
+        for entity in entities:
+            if self.entityToShow(entity, check_resources):
+                return True
+        return False
+
+    def isEntityInGroup(self, entity, group):
+        """Tell if an entity is in a roster group
 
-    def setSpecial(self, jid, _type, show=False):
-        """Set entity as a special
-        @param jid: jid of the entity
-        @param _type: special type (e.g.: "MUC")
-        @param show: True to display the dialog to chat with this entity
+        @param entity(jid.JID): jid of the entity
+        @param group(unicode): group to check
+        @return (bool): True if the entity is in the group
+        """
+        return entity in self.getGroupData(group, "jids")
+
+    def remove(self, entity):
+        """remove a contact from the list
+
+        @param entity(jid.JID): jid of the entity to remove (bare jid is used)
         """
-        self.specials[jid.bare] = _type
+        entity_bare = entity.bare
+        try:
+            groups = self._cache[entity_bare].get(C.CONTACT_GROUPS, set())
+        except KeyError:
+            log.warning(_("Trying to delete an unknow entity [{}]").format(entity))
+        del self._cache[entity_bare]
+        for group in groups:
+            self._groups[group]['jids'].remove(entity_bare)
+        for set_ in (self._selected, self._alerts, self._specials, self._special_extras):
+            to_remove = set()
+            for set_entity in set_:
+                if set_entity.bare == entity.bare:
+                    to_remove.add(set_entity)
+            set_.difference_update(to_remove)
+        self.update()
 
-    def updatePresence(self, jid, show, priority, statuses):
+    def onPresenceUpdate(self, entity, show, priority, statuses, profile):
         """Update entity's presence status
-        @param jid: entity to update's jid
+
+        @param entity(jid.JID): entity updated
         @param show: availability
         @parap priority: resource's priority
-        @param statuses: dict of statuses"""
-        self.setCache(jid, 'show', show)
-        self.setCache(jid, 'prority', priority)
-        self.setCache(jid, 'statuses', statuses)
-        self.update_jid(jid)
+        @param statuses: dict of statuses
+        @param profile: %(doc_profile)s
+        """
+        cache = self.getCache(entity)
+        if show == C.PRESENCE_UNAVAILABLE:
+            if not entity.resource:
+                cache[C.CONTACT_RESOURCES].clear()
+                cache[C.CONTACT_MAIN_RESOURCE]= None
+            else:
+                try:
+                    del cache[C.CONTACT_RESOURCES][entity.resource]
+                except KeyError:
+                    log.error("Presence unavailable received for an unknown resource [{}]".format(entity))
+                if not cache[C.CONTACT_RESOURCES]:
+                    cache[C.CONTACT_MAIN_RESOURCE] = None
+        else:
+            assert entity.resource
+            resources_data = cache[C.CONTACT_RESOURCES]
+            resource_data = resources_data.setdefault(entity.resource, {})
+            resource_data[C.PRESENCE_SHOW] = show
+            resource_data[C.PRESENCE_PRIORITY] = int(priority)
+            resource_data[C.PRESENCE_STATUSES] = statuses
+
+            priority_resource = max(resources_data, key=lambda res: resources_data[res][C.PRESENCE_PRIORITY])
+            cache[C.CONTACT_MAIN_RESOURCE] = priority_resource
+
+    def onNickUpdate(self, entity, new_nick, profile):
+        """Update entity's nick
+
+        @param entity(jid.JID): entity updated
+        @param new_nick(unicode): new nick of the entity
+        @param profile: %(doc_profile)s
+        """
+        raise NotImplementedError # Must be implemented by frontends
+
+    def unselectAll(self):
+        """Unselect all contacts"""
+        self._selected.clear()
+        self.update()
+
+    def select(self, entity):
+        """Select an entity
+
+        @param entity(jid.JID): entity to select (resource is significant)
+        """
+        log.debug("select %s" % entity)
+        self._selected.add(entity)
+        self.update()
+
+    def setAlert(self, entity):
+        """Set an alert on the entity (usually for a waiting message)
+
+        @param entity(jid.JID): entity which must displayed in alert mode (resource is significant)
+        """
+        self._alerts.add(entity)
+        self.update()
+
+    def _showOfflineContacts(self, show_str):
+        self.showOfflineContacts(C.bool(show_str))
 
     def showOfflineContacts(self, show):
-        pass
+        """Tell if offline contacts should shown
+
+        @param show(bool): True if offline contacts should be shown
+        """
+        assert isinstance(show, bool)
+        if self.show_disconnected == show:
+            return
+        self.show_disconnected = show
+        self.update()
+
+    def _showEmptyGroups(self, show_str):
+        self.showEmptyGroups(C.bool(show_str))
 
     def showEmptyGroups(self, show):
-        pass
+        assert isinstance(show, bool)
+        if self.show_empty_groups == show:
+            return
+        self.show_empty_groups = show
+        self.update()
+
+    def showResources(self, show):
+        show = C.bool(show)
+        if self.show_resources == show:
+            return
+        self.show_resources = show
+        self.update()
+
+    def onDelete(self):
+        QuickWidget.onDelete(self)
+        self.host.removeListener('presence', self.presenceListener)