Mercurial > libervia-backend
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)