comparison sat_frontends/quick_frontend/quick_contact_list.py @ 2617:81b70eeb710f

quick_frontend(contact list): refactored update: update is now called with appropriate constant value (C.UPDATE_ADD, C.UPDATE_DELETE, C.UPDATE_MODIFY and so on) when a widget change visibility according to current options. Before it was linked to cache only (C.UPDATE_ADD was only called when contact was first added to cache). This make widget handling in frontends more easy. Renamed entityToShow to entityVisible, which seems to correspond better. Started reducing lines lenght to 90 chars as a test. May become the new coding style soon.
author Goffi <goffi@goffi.org>
date Sun, 24 Jun 2018 21:59:29 +0200
parents 2e6864b1d577
children 56f94936df1e
comparison
equal deleted inserted replaced
2616:1cc88adb5142 2617:81b70eeb710f
15 # GNU Affero General Public License for more details. 15 # GNU Affero General Public License for more details.
16 16
17 # You should have received a copy of the GNU Affero General Public License 17 # You should have received a copy of the GNU Affero General Public License
18 # along with this program. If not, see <http://www.gnu.org/licenses/>. 18 # along with this program. If not, see <http://www.gnu.org/licenses/>.
19 19
20 """Contact List handling multi profiles at once, should replace quick_contact_list module in the future""" 20 """Contact List handling multi profiles at once,
21 should replace quick_contact_list module in the future"""
21 22
22 from sat.core.i18n import _ 23 from sat.core.i18n import _
23 from sat.core.log import getLogger 24 from sat.core.log import getLogger
24 log = getLogger(__name__) 25 log = getLogger(__name__)
25 from sat.core import exceptions 26 from sat.core import exceptions
30 31
31 32
32 try: 33 try:
33 # FIXME: to be removed when an acceptable solution is here 34 # FIXME: to be removed when an acceptable solution is here
34 unicode('') # XXX: unicode doesn't exist in pyjamas 35 unicode('') # XXX: unicode doesn't exist in pyjamas
35 except (TypeError, AttributeError): # Error raised is not the same depending on pyjsbuild options 36 except (TypeError, AttributeError): # Error raised is not the same depending on
37 # pyjsbuild options
36 # XXX: pyjamas' max doesn't support key argument, so we implement it ourself 38 # XXX: pyjamas' max doesn't support key argument, so we implement it ourself
37 pyjamas_max = max 39 pyjamas_max = max
38 40
39 def max(iterable, key): 41 def max(iterable, key):
40 iter_cpy = list(iterable) 42 iter_cpy = list(iterable)
86 # options 88 # options
87 self.show_disconnected = False 89 self.show_disconnected = False
88 self.show_empty_groups = True 90 self.show_empty_groups = True
89 self.show_resources = False 91 self.show_resources = False
90 self.show_status = False 92 self.show_status = False
91 93 # do we show entities with notifications?
92 self.host.bridge.asyncGetParamA(C.SHOW_EMPTY_GROUPS, "General", profile_key=profile, callback=self._showEmptyGroups) 94 # if True, entities will be show even if they normally would not
93 self.host.bridge.asyncGetParamA(C.SHOW_OFFLINE_CONTACTS, "General", profile_key=profile, callback=self._showOfflineContacts) 95 # (e.g. not in contact list) if they have notifications attached
94 96 self.show_entities_with_notifs = True
95 # 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) 97
98 self.host.bridge.asyncGetParamA(C.SHOW_EMPTY_GROUPS,
99 "General",
100 profile_key=profile,
101 callback=self._showEmptyGroups)
102
103 self.host.bridge.asyncGetParamA(C.SHOW_OFFLINE_CONTACTS,
104 "General",
105 profile_key=profile,
106 callback=self._showOfflineContacts)
107
108 # FIXME: workaround for a pyjamas issue: calling hash on a class method always
109 # return a different value if that method is defined directly within the
110 # class (with the "def" keyword)
96 self.presenceListener = self.onPresenceUpdate 111 self.presenceListener = self.onPresenceUpdate
97 self.host.addListener('presence', self.presenceListener, [self.profile]) 112 self.host.addListener('presence', self.presenceListener, [self.profile])
98 self.nickListener = self.onNickUpdate 113 self.nickListener = self.onNickUpdate
99 self.host.addListener('nick', self.nickListener, [self.profile]) 114 self.host.addListener('nick', self.nickListener, [self.profile])
100 self.notifListener = self.onNotification 115 self.notifListener = self.onNotification
114 129
115 def __contains__(self, entity): 130 def __contains__(self, entity):
116 """Check if entity is in contact list 131 """Check if entity is in contact list
117 132
118 An entity can be in contact list even if not in roster 133 An entity can be in contact list even if not in roster
119 @param entity (jid.JID): jid of the entity (resource is not ignored, use bare jid if needed) 134 @param entity (jid.JID): jid of the entity (resource is not ignored,
135 use bare jid if needed)
120 """ 136 """
121 if entity.resource: 137 if entity.resource:
122 try: 138 try:
123 return entity.resource in self.getCache(entity.bare, C.CONTACT_RESOURCES) 139 return entity.resource in self.getCache(entity.bare, C.CONTACT_RESOURCES)
124 except KeyError: 140 except exceptions.NotFound:
125 return False 141 return False
126 return entity in self._cache 142 return entity in self._cache
127 143
128 @property 144 @property
129 def roster(self): 145 def roster(self):
137 def roster_connected(self): 153 def roster_connected(self):
138 """Return all the bare JIDs of the roster entities that are connected. 154 """Return all the bare JIDs of the roster entities that are connected.
139 155
140 @return (set[jid.JID]) 156 @return (set[jid.JID])
141 """ 157 """
142 return set([entity for entity in self._roster if self.getCache(entity, C.PRESENCE_SHOW) is not None]) 158 return set([entity for entity in self._roster
159 if self.getCache(entity, C.PRESENCE_SHOW) is not None])
143 160
144 @property 161 @property
145 def roster_entities_by_group(self): 162 def roster_entities_by_group(self):
146 """Return a dictionary binding the roster groups to their entities bare JIDs. 163 """Return a dictionary binding the roster groups to their entities bare JIDs.
147 164
184 """Return item representation for all visible entities in cache 201 """Return item representation for all visible entities in cache
185 202
186 entities are not sorted 203 entities are not sorted
187 key: bare jid, value: data 204 key: bare jid, value: data
188 """ 205 """
189 return {jid_:cache for jid_, cache in self._cache.iteritems() if self.entityToShow(jid_)} 206 return {jid_:cache for jid_, cache in self._cache.iteritems()
207 if self.entityVisible(jid_)}
190 208
191 209
192 def getItem(self, entity): 210 def getItem(self, entity):
193 """Return item representation of requested entity 211 """Return item representation of requested entity
194 212
196 @raise (KeyError): entity is unknown 214 @raise (KeyError): entity is unknown
197 """ 215 """
198 return self._cache[entity] 216 return self._cache[entity]
199 217
200 def _gotContacts(self, contacts): 218 def _gotContacts(self, contacts):
201 """Called during filling, add contacts and notice parent that contacts are filled""" 219 """Add contacts and notice parent that contacts are filled
220
221 Called during initial contact list filling
222 @param contacts(tuple): all contacts
223 """
202 for contact in contacts: 224 for contact in contacts:
203 self.host.newContactHandler(*contact, profile=self.profile) 225 self.host.newContactHandler(*contact, profile=self.profile)
204 handler._contactsFilled(self.profile) 226 handler._contactsFilled(self.profile)
205 227
206 def _fill(self): 228 def _fill(self):
212 self.host.bridge.getContacts(self.profile, callback=self._gotContacts) 234 self.host.bridge.getContacts(self.profile, callback=self._gotContacts)
213 235
214 def fill(self): 236 def fill(self):
215 handler.fill(self.profile) 237 handler.fill(self.profile)
216 238
217 def getCache(self, entity, name=None, bare_default=True): 239 def getCache(self, entity, name=None, bare_default=True, create_if_not_found=False):
218 """Return a cache value for a contact 240 """Return a cache value for a contact
219 241
220 @param entity(jid.JID): entity of the contact from who we want data (resource is used if given) 242 @param entity(jid.JID): entity of the contact from who we want data
243 (resource is used if given)
221 if a resource specific information is requested: 244 if a resource specific information is requested:
222 - if no resource is given (bare jid), the main resource is used, according to priority 245 - if no resource is given (bare jid), the main resource is used,
246 according to priority
223 - if resource is given, it is used 247 - if resource is given, it is used
224 @param name(unicode): name the data to get, or None to get everything 248 @param name(unicode): name the data to get, or None to get everything
225 @param bare_default(bool, None): if True and entity is a full jid, the value of bare jid 249 @param bare_default(bool, None): if True and entity is a full jid,
226 will be returned if not value is found for the requested resource. 250 the value of bare jid will be returned if not value is found for
251 the requested resource.
227 If False, None is returned if no value is found for the requested resource. 252 If False, None is returned if no value is found for the requested resource.
228 If None, bare_default will be set to False if entity is in a room, True else 253 If None, bare_default will be set to False if entity is in a room, True else
254 @param create_if_not_found(bool): if True, create contact if it's not found
255 in cache
229 @return: full cache if no name is given, or value of "name", or None 256 @return: full cache if no name is given, or value of "name", or None
257 @raise NotFound: entity not found in cache
230 """ 258 """
231 # FIXME: resource handling need to be reworked 259 # FIXME: resource handling need to be reworked
232 # FIXME: bare_default work for requesting full jid to get bare jid, but not the other way 260 # FIXME: bare_default work for requesting full jid to get bare jid,
233 # e.g.: if we have set an avatar for user@server.tld/resource and we request user@server.tld 261 # but not the other way
262 # e.g.: if we have set an avatar for user@server.tld/resource
263 # and we request user@server.tld
234 # we won't get the avatar set in the resource 264 # we won't get the avatar set in the resource
235 try: 265 try:
236 cache = self._cache[entity.bare] 266 cache = self._cache[entity.bare]
237 except KeyError: 267 except KeyError:
238 self.setContact(entity) 268 if create_if_not_found:
239 cache = self._cache[entity.bare] 269 self.setContact(entity)
270 cache = self._cache[entity.bare]
271 else:
272 raise exceptions.NotFound
240 273
241 if name is None: 274 if name is None:
275 # full cache is requested
242 return cache 276 return cache
243 277
244 if name in ('status', C.PRESENCE_STATUSES, C.PRESENCE_PRIORITY, C.PRESENCE_SHOW): 278 if name in ('status', C.PRESENCE_STATUSES, C.PRESENCE_PRIORITY, C.PRESENCE_SHOW):
245 # these data are related to the resource 279 # these data are related to the resource
246 if not entity.resource: 280 if not entity.resource:
335 def setSpecial(self, entity, special_type): 369 def setSpecial(self, entity, special_type):
336 """Set special flag on an entity 370 """Set special flag on an entity
337 371
338 @param entity(jid.JID): jid of the special entity 372 @param entity(jid.JID): jid of the special entity
339 if the jid is full, will be added to special extras 373 if the jid is full, will be added to special extras
340 @param special_type: one of special type (e.g. C.CONTACT_SPECIAL_GROUP) or None to remove special flag 374 @param special_type: one of special type (e.g. C.CONTACT_SPECIAL_GROUP)
375 or None to remove special flag
341 """ 376 """
342 assert special_type in C.CONTACT_SPECIAL_ALLOWED + (None,) 377 assert special_type in C.CONTACT_SPECIAL_ALLOWED + (None,)
343 self.setCache(entity, C.CONTACT_SPECIAL, special_type) 378 self.setCache(entity, C.CONTACT_SPECIAL, special_type)
344 379
345 def getSpecials(self, special_type=None, bare=False): 380 def getSpecials(self, special_type=None, bare=False):
346 """Return all the bare JIDs of the special roster entities of with given type. 381 """Return all the bare JIDs of the special roster entities of with given type.
347 382
348 @param special_type(unicode, None): if not None, filter by special type (e.g. C.CONTACT_SPECIAL_GROUP) 383 @param special_type(unicode, None): if not None, filter by special type
384 (e.g. C.CONTACT_SPECIAL_GROUP)
349 @param bare(bool): return only bare jids if True 385 @param bare(bool): return only bare jids if True
350 @return (iter[jid.JID]): found special entities 386 @return (iter[jid.JID]): found special entities
351 """ 387 """
352 for entity in self._specials: 388 for entity in self._specials:
353 if bare and entity.resource: 389 if bare and entity.resource:
354 continue 390 continue
355 if special_type is not None and self.getCache(entity, C.CONTACT_SPECIAL) != special_type: 391 if (special_type is not None
392 and self.getCache(entity, C.CONTACT_SPECIAL) != special_type):
356 continue 393 continue
357 yield entity 394 yield entity
358 395
359 def disconnect(self): 396 def disconnect(self):
360 # for now we just clear contacts on disconnect 397 # for now we just clear contacts on disconnect
379 This method can be called with groups=None for the purpose of updating 416 This method can be called with groups=None for the purpose of updating
380 the contact's attributes (e.g. nickname). In that case, the groups 417 the contact's attributes (e.g. nickname). In that case, the groups
381 attribute must not be set to the default group but ignored. If not, 418 attribute must not be set to the default group but ignored. If not,
382 you may move your contact from its actual group(s) to the default one. 419 you may move your contact from its actual group(s) to the default one.
383 420
384 None value for 'groups' has a different meaning than [None] which is for the default group. 421 None value for 'groups' has a different meaning than [None]
422 which is for the default group.
385 423
386 @param entity (jid.JID): entity to add or replace 424 @param entity (jid.JID): entity to add or replace
387 if entity is a full jid, attributes will be cached in for the full jid only 425 if entity is a full jid, attributes will be cached in for the full jid only
388 @param groups (list): list of groups or None to ignore the groups membership. 426 @param groups (list): list of groups or None to ignore the groups membership.
389 @param attributes (dict): attibutes of the added jid or to update 427 @param attributes (dict): attibutes of the added jid or to update
392 """ 430 """
393 if attributes is None: 431 if attributes is None:
394 attributes = {} 432 attributes = {}
395 433
396 entity_bare = entity.bare 434 entity_bare = entity.bare
397 update_type = C.UPDATE_MODIFY if entity_bare in self._cache else C.UPDATE_ADD 435 # we check if the entity is visible before changing anything
436 # this way we know if we need to do an UPDATE_ADD, UPDATE_MODIFY
437 # or an UPDATE_DELETE
438 was_visible = self.entityVisible(entity_bare)
398 439
399 if in_roster: 440 if in_roster:
400 self._roster.add(entity_bare) 441 self._roster.add(entity_bare)
401 442
402 cache = self._cache.setdefault(entity_bare, {C.CONTACT_RESOURCES: {}, 443 cache = self._cache.setdefault(entity_bare, {C.CONTACT_RESOURCES: {},
403 C.CONTACT_MAIN_RESOURCE: None, 444 C.CONTACT_MAIN_RESOURCE: None,
404 C.CONTACT_SELECTED: set()}) 445 C.CONTACT_SELECTED: set()})
405 446
406 assert not C.CONTACT_DATA_FORBIDDEN.intersection(attributes) # we don't want forbidden data in attributes 447 # we don't want forbidden data in attributes
448 assert not C.CONTACT_DATA_FORBIDDEN.intersection(attributes)
407 449
408 # we set groups and fill self._groups accordingly 450 # we set groups and fill self._groups accordingly
409 if groups is not None: 451 if groups is not None:
410 if not groups: 452 if not groups:
411 groups = [None] # [None] is the default group 453 groups = [None] # [None] is the default group
412 if C.CONTACT_GROUPS in cache: 454 if C.CONTACT_GROUPS in cache:
413 # 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] 455 # XXX: don't use set(cache[C.CONTACT_GROUPS]).difference(groups) because
414 for group in [group for group in cache[C.CONTACT_GROUPS] if group not in groups]: 456 # it won't work in Pyjamas if None is in cache[C.CONTACT_GROUPS]
457 for group in [group for group in cache[C.CONTACT_GROUPS]
458 if group not in groups]:
415 self._groups[group]['jids'].remove(entity_bare) 459 self._groups[group]['jids'].remove(entity_bare)
416 cache[C.CONTACT_GROUPS] = groups 460 cache[C.CONTACT_GROUPS] = groups
417 for group in groups: 461 for group in groups:
418 self._groups.setdefault(group, {}).setdefault('jids', set()).add(entity_bare) 462 self._groups.setdefault(group, {}).setdefault('jids', set()).add(
463 entity_bare)
419 464
420 # special entities management 465 # special entities management
421 if C.CONTACT_SPECIAL in attributes: 466 if C.CONTACT_SPECIAL in attributes:
422 if attributes[C.CONTACT_SPECIAL] is None: 467 if attributes[C.CONTACT_SPECIAL] is None:
423 del attributes[C.CONTACT_SPECIAL] 468 del attributes[C.CONTACT_SPECIAL]
426 self._specials.add(entity) 471 self._specials.add(entity)
427 cache[C.CONTACT_MAIN_RESOURCE] = None 472 cache[C.CONTACT_MAIN_RESOURCE] = None
428 473
429 # now the attributes we keep in cache 474 # now the attributes we keep in cache
430 # XXX: if entity is a full jid, we store the value for the resource only 475 # XXX: if entity is a full jid, we store the value for the resource only
431 cache_attr = cache[C.CONTACT_RESOURCES].setdefault(entity.resource, {}) if entity.resource else cache 476 cache_attr = (cache[C.CONTACT_RESOURCES].setdefault(entity.resource, {})
477 if entity.resource else cache)
432 for attribute, value in attributes.iteritems(): 478 for attribute, value in attributes.iteritems():
433 if value is None: 479 if value is None:
434 # XXX: pyjamas hack: we need to use pop instead of del 480 # XXX: pyjamas hack: we need to use pop instead of del
435 try: 481 try:
436 cache_attr[attribute].pop(value) 482 cache_attr[attribute].pop(value)
437 except KeyError: 483 except KeyError:
438 pass 484 pass
439 else: 485 else:
440 if attribute == 'nick' and self.isSpecial(entity, C.CONTACT_SPECIAL_GROUP): 486 if attribute == 'nick' and self.isSpecial(entity, C.CONTACT_SPECIAL_GROUP):
441 # we don't want to keep nick for MUC rooms 487 # we don't want to keep nick for MUC rooms
442 # FIXME: this is here as plugin XEP-0054 can link resource's nick with bare jid 488 # FIXME: this is here as plugin XEP-0054 can link resource's nick
443 # which in the case of MUC set the nick for the whole MUC 489 # with bare jid which in the case of MUC
490 # set the nick for the whole MUC
444 # resulting in bad name displayed in some frontends 491 # resulting in bad name displayed in some frontends
445 continue 492 continue
446 cache_attr[attribute] = value 493 cache_attr[attribute] = value
447 494
448 # we can update the display 495 # we can update the display if needed
449 self.update([entity], update_type, self.profile) 496 if self.entityVisible(entity_bare):
450 497 # if the contact was not visible, we need to add a widget
451 def entityToShow(self, entity, check_resource=False): 498 # else we just update id
499 update_type = C.UPDATE_MODIFY if was_visible else C.UPDATE_ADD
500 self.update([entity], update_type, self.profile)
501 elif was_visible:
502 # the entity was visible and is not anymore, we remove it
503 self.update([entity], C.UPDATE_DELETE, self.profile)
504
505 def entityVisible(self, entity, check_resource=False):
452 """Tell if the contact should be showed or hidden. 506 """Tell if the contact should be showed or hidden.
453 507
454 @param entity (jid.JID): jid of the contact 508 @param entity (jid.JID): jid of the contact
455 @param check_resource (bool): True if resource must be significant 509 @param check_resource (bool): True if resource must be significant
456 @return (bool): True if that contact should be showed in the list 510 @return (bool): True if that contact should be showed in the list
457 """ 511 """
458 show = self.getCache(entity, C.PRESENCE_SHOW) 512 try:
513 show = self.getCache(entity, C.PRESENCE_SHOW)
514 except exceptions.NotFound:
515 return False
459 516
460 if check_resource: 517 if check_resource:
461 selected = self._selected 518 selected = self._selected
462 else: 519 else:
463 selected = {selected.bare for selected in self._selected} 520 selected = {selected.bare for selected in self._selected}
464 return ((show is not None and show != C.PRESENCE_UNAVAILABLE) 521 return ((show is not None and show != C.PRESENCE_UNAVAILABLE)
465 or self.show_disconnected 522 or self.show_disconnected
466 or entity in selected 523 or entity in selected
467 or next(self.host.getNotifs(entity.bare, profile=self.profile), None) 524 or (self.show_entities_with_notifs
525 and next(self.host.getNotifs(entity.bare, profile=self.profile), None))
468 ) 526 )
469 527
470 def anyEntityToShow(self, entities, check_resources=False): 528 def anyEntityVisible(self, entities, check_resources=False):
471 """Tell if in a list of entities, at least one should be shown 529 """Tell if in a list of entities, at least one should be shown
472 530
473 @param entities (list[jid.JID]): list of jids 531 @param entities (list[jid.JID]): list of jids
474 @param check_resources (bool): True if resources must be significant 532 @param check_resources (bool): True if resources must be significant
475 @return (bool): True if a least one entity need to be shown 533 @return (bool): True if a least one entity need to be shown
476 """ 534 """
477 # FIXME: looks inefficient, really needed? 535 # FIXME: looks inefficient, really needed?
478 for entity in entities: 536 for entity in entities:
479 if self.entityToShow(entity, check_resources): 537 if self.entityVisible(entity, check_resources):
480 return True 538 return True
481 return False 539 return False
482 540
483 def isEntityInGroup(self, entity, group): 541 def isEntityInGroup(self, entity, group):
484 """Tell if an entity is in a roster group 542 """Tell if an entity is in a roster group
493 """remove a contact from the list 551 """remove a contact from the list
494 552
495 @param entity(jid.JID): jid of the entity to remove (bare jid is used) 553 @param entity(jid.JID): jid of the entity to remove (bare jid is used)
496 """ 554 """
497 entity_bare = entity.bare 555 entity_bare = entity.bare
556 was_visible = self.entityVisible(entity_bare)
498 try: 557 try:
499 groups = self._cache[entity_bare].get(C.CONTACT_GROUPS, set()) 558 groups = self._cache[entity_bare].get(C.CONTACT_GROUPS, set())
500 except KeyError: 559 except KeyError:
501 log.error(_(u"Trying to delete an unknow entity [{}]").format(entity)) 560 log.error(_(u"Trying to delete an unknow entity [{}]").format(entity))
502 try: 561 try:
505 pass 564 pass
506 del self._cache[entity_bare] 565 del self._cache[entity_bare]
507 for group in groups: 566 for group in groups:
508 self._groups[group]['jids'].remove(entity_bare) 567 self._groups[group]['jids'].remove(entity_bare)
509 if not self._groups[group]['jids']: 568 if not self._groups[group]['jids']:
510 self._groups.pop(group) # FIXME: we use pop because of pyjamas: http://wiki.goffi.org/wiki/Issues_with_Pyjamas/en 569 # FIXME: we use pop because of pyjamas:
570 # http://wiki.goffi.org/wiki/Issues_with_Pyjamas/en
571 self._groups.pop(group)
511 for iterable in (self._selected, self._specials): 572 for iterable in (self._selected, self._specials):
512 to_remove = set() 573 to_remove = set()
513 for set_entity in iterable: 574 for set_entity in iterable:
514 if set_entity.bare == entity.bare: 575 if set_entity.bare == entity.bare:
515 to_remove.add(set_entity) 576 to_remove.add(set_entity)
516 iterable.difference_update(to_remove) 577 iterable.difference_update(to_remove)
517 self.update([entity], C.UPDATE_DELETE, self.profile) 578 if was_visible:
579 self.update([entity], C.UPDATE_DELETE, self.profile)
518 580
519 def onPresenceUpdate(self, entity, show, priority, statuses, profile): 581 def onPresenceUpdate(self, entity, show, priority, statuses, profile):
520 """Update entity's presence status 582 """Update entity's presence status
521 583
522 @param entity(jid.JID): entity updated 584 @param entity(jid.JID): entity updated
523 @param show: availability 585 @param show: availability
524 @parap priority: resource's priority 586 @parap priority: resource's priority
525 @param statuses: dict of statuses 587 @param statuses: dict of statuses
526 @param profile: %(doc_profile)s 588 @param profile: %(doc_profile)s
527 """ 589 """
528 cache = self.getCache(entity) 590 # FIXME: cache modification should be done with setContact
591 # the resources/presence handling logic should be moved there
592 was_visible = self.entityVisible(entity.bare)
593 cache = self.getCache(entity, create_if_not_found=True)
529 if show == C.PRESENCE_UNAVAILABLE: 594 if show == C.PRESENCE_UNAVAILABLE:
530 if not entity.resource: 595 if not entity.resource:
531 cache[C.CONTACT_RESOURCES].clear() 596 cache[C.CONTACT_RESOURCES].clear()
532 cache[C.CONTACT_MAIN_RESOURCE] = None 597 cache[C.CONTACT_MAIN_RESOURCE] = None
533 else: 598 else:
534 try: 599 try:
535 del cache[C.CONTACT_RESOURCES][entity.resource] 600 del cache[C.CONTACT_RESOURCES][entity.resource]
536 except KeyError: 601 except KeyError:
537 log.error(u"Presence unavailable received for an unknown resource [{}]".format(entity)) 602 log.error(u"Presence unavailable received "
603 u"for an unknown resource [{}]".format(entity))
538 if not cache[C.CONTACT_RESOURCES]: 604 if not cache[C.CONTACT_RESOURCES]:
539 cache[C.CONTACT_MAIN_RESOURCE] = None 605 cache[C.CONTACT_MAIN_RESOURCE] = None
540 else: 606 else:
541 if not entity.resource: 607 if not entity.resource:
542 log.warning(_(u"received presence from entity without resource: {}".format(entity))) 608 log.warning(_(u"received presence from entity "
609 u"without resource: {}".format(entity)))
543 resources_data = cache[C.CONTACT_RESOURCES] 610 resources_data = cache[C.CONTACT_RESOURCES]
544 resource_data = resources_data.setdefault(entity.resource, {}) 611 resource_data = resources_data.setdefault(entity.resource, {})
545 resource_data[C.PRESENCE_SHOW] = show 612 resource_data[C.PRESENCE_SHOW] = show
546 resource_data[C.PRESENCE_PRIORITY] = int(priority) 613 resource_data[C.PRESENCE_PRIORITY] = int(priority)
547 resource_data[C.PRESENCE_STATUSES] = statuses 614 resource_data[C.PRESENCE_STATUSES] = statuses
548 615
549 if entity.bare not in self._specials: 616 if entity.bare not in self._specials:
550 # we may have resources with no priority 617 # we may have resources with no priority
551 # (when a cached value is added for a not connected resource) 618 # (when a cached value is added for a not connected resource)
552 priority_resource = max(resources_data, key=lambda res: resources_data[res].get(C.PRESENCE_PRIORITY, -2**32)) 619 priority_resource = max(resources_data,
620 key=lambda res: resources_data[res].get(
621 C.PRESENCE_PRIORITY, -2**32))
553 cache[C.CONTACT_MAIN_RESOURCE] = priority_resource 622 cache[C.CONTACT_MAIN_RESOURCE] = priority_resource
554 self.update([entity], C.UPDATE_MODIFY, self.profile) 623 if self.entityVisible(entity.bare):
624 update_type = C.UPDATE_MODIFY if was_visible else C.UPDATE_ADD
625 self.update([entity], update_type, self.profile)
626 elif was_visible:
627 self.update([entity], C.UPDATE_DELETE, self.profile)
555 628
556 def onNickUpdate(self, entity, new_nick, profile): 629 def onNickUpdate(self, entity, new_nick, profile):
557 """Update entity's nick 630 """Update entity's nick
558 631
559 @param entity(jid.JID): entity updated 632 @param entity(jid.JID): entity updated
560 @param new_nick(unicode): new nick of the entity 633 @param new_nick(unicode): new nick of the entity
561 @param profile: %(doc_profile)s 634 @param profile: %(doc_profile)s
562 """ 635 """
563 assert profile == self.profile 636 assert profile == self.profile
564 self.setCache(entity, 'nick', new_nick) 637 self.setCache(entity, 'nick', new_nick)
565 self.update([entity], C.UPDATE_MODIFY, profile)
566 638
567 def onNotification(self, entity, notif, profile): 639 def onNotification(self, entity, notif, profile):
568 """Update entity with notification 640 """Update entity with notification
569 641
570 @param entity(jid.JID): entity updated 642 @param entity(jid.JID): entity updated
571 @param notif(dict): notification data 643 @param notif(dict): notification data
572 @param profile: %(doc_profile)s 644 @param profile: %(doc_profile)s
573 """ 645 """
574 assert profile == self.profile 646 assert profile == self.profile
575 if entity is not None: 647 if entity is not None and self.entityVisible(entity):
576 self.update([entity], C.UPDATE_MODIFY, profile) 648 self.update([entity], C.UPDATE_MODIFY, profile)
577 649
578 def unselect(self, entity): 650 def unselect(self, entity):
579 """Unselect an entity 651 """Unselect an entity
580 652
614 cache[C.CONTACT_SELECTED].add(entity.resource) 686 cache[C.CONTACT_SELECTED].add(entity.resource)
615 self._selected.add(entity) 687 self._selected.add(entity)
616 self.update([entity], C.UPDATE_SELECTION, profile=self.profile) 688 self.update([entity], C.UPDATE_SELECTION, profile=self.profile)
617 689
618 def showOfflineContacts(self, show): 690 def showOfflineContacts(self, show):
619 """Tell if offline contacts should shown 691 """Tell if offline contacts should be shown
620 692
621 @param show(bool): True if offline contacts should be shown 693 @param show(bool): True if offline contacts should be shown
622 """ 694 """
623 assert isinstance(show, bool) 695 assert isinstance(show, bool)
624 if self.show_disconnected == show: 696 if self.show_disconnected == show:
636 def showResources(self, show): 708 def showResources(self, show):
637 assert isinstance(show, bool) 709 assert isinstance(show, bool)
638 if self.show_resources == show: 710 if self.show_resources == show:
639 return 711 return
640 self.show_resources = show 712 self.show_resources = show
641 self.update(profile=self.profile) 713 self.update(type_=C.UPDATE_STRUCTURE, profile=self.profile)
642 714
643 def plug(self): 715 def plug(self):
644 handler.addProfile(self.profile) 716 handler.addProfile(self.profile)
645 717
646 def unplug(self): 718 def unplug(self):
655 def __init__(self, host): 727 def __init__(self, host):
656 super(QuickContactListHandler, self).__init__() 728 super(QuickContactListHandler, self).__init__()
657 self.host = host 729 self.host = host
658 global handler 730 global handler
659 if handler is not None: 731 if handler is not None:
660 raise exceptions.InternalError(u"QuickContactListHandler must be instanciated only once") 732 raise exceptions.InternalError(u"QuickContactListHandler must be "
733 u"instanciated only once")
661 handler = self 734 handler = self
662 self._clist = {} # key: profile, value: ProfileContactList 735 self._clist = {} # key: profile, value: ProfileContactList
663 self._widgets = set() 736 self._widgets = set()
664 self._update_locked = False # se to True to ignore updates 737 self._update_locked = False # se to True to ignore updates
665 738
668 return self._clist[profile] 741 return self._clist[profile]
669 742
670 def __contains__(self, entity): 743 def __contains__(self, entity):
671 """Check if entity is in contact list 744 """Check if entity is in contact list
672 745
673 @param entity (jid.JID): jid of the entity (resource is not ignored, use bare jid if needed) 746 @param entity (jid.JID): jid of the entity (resource is not ignored,
747 use bare jid if needed)
674 """ 748 """
675 for contact_list in self._clist.itervalues(): 749 for contact_list in self._clist.itervalues():
676 if entity in contact_list: 750 if entity in contact_list:
677 return True 751 return True
678 return False 752 return False
823 def getSpecialExtras(self, special_type=None): 897 def getSpecialExtras(self, special_type=None):
824 """Return special extras with given type 898 """Return special extras with given type
825 899
826 If special_type is None, return all special extras. 900 If special_type is None, return all special extras.
827 901
828 @param special_type(unicode, None): one of special type (e.g. C.CONTACT_SPECIAL_GROUP) 902 @param special_type(unicode, None): one of special type
903 (e.g. C.CONTACT_SPECIAL_GROUP)
829 None to return all special extras. 904 None to return all special extras.
830 @return (set[jid.JID]) 905 @return (set[jid.JID])
831 """ 906 """
832 entities = set() 907 entities = set()
833 for contact_list in self._clist.itervalues(): 908 for contact_list in self._clist.itervalues():
836 911
837 def _contactsFilled(self, profile): 912 def _contactsFilled(self, profile):
838 self._to_fill.remove(profile) 913 self._to_fill.remove(profile)
839 if not self._to_fill: 914 if not self._to_fill:
840 del self._to_fill 915 del self._to_fill
916 # we need a full update when all contacts are filled
841 self.update() 917 self.update()
842 918
843 def fill(self, profile=None): 919 def fill(self, profile=None):
844 """Get all contacts from backend, and fill the widget 920 """Get all contacts from backend, and fill the widget
845 921
862 else: 938 else:
863 to_fill.update(self._clist.items()) 939 to_fill.update(self._clist.items())
864 940
865 remaining = to_fill.difference(filled) 941 remaining = to_fill.difference(filled)
866 if remaining != to_fill: 942 if remaining != to_fill:
867 log.debug(u"Not re-filling already filled contact list(s) for {}".format(u', '.join(to_fill.intersection(filled)))) 943 log.debug(u"Not re-filling already filled contact list(s) for {}".format(
944 u', '.join(to_fill.intersection(filled))))
868 for profile in remaining: 945 for profile in remaining:
869 self._clist[profile]._fill() 946 self._clist[profile]._fill()
870 947
871 def clearContacts(self, keep_cache=False): 948 def clearContacts(self, keep_cache=False):
872 """Clear all the contact list 949 """Clear all the contact list
873 950
874 @param keep_cache: if True, don't reset the cache 951 @param keep_cache: if True, don't reset the cache
875 """ 952 """
876 for contact_list in self._clist.itervalues(): 953 for contact_list in self._clist.itervalues():
877 contact_list.clearContacts(keep_cache) 954 contact_list.clearContacts(keep_cache)
955 # we need a full update
878 self.update() 956 self.update()
879 957
880 def select(self, entity): 958 def select(self, entity):
881 for contact_list in self._clist.itervalues(): 959 for contact_list in self._clist.itervalues():
882 contact_list.select(entity) 960 contact_list.select(entity)
893 @param locked(bool): updates are forbidden if True 971 @param locked(bool): updates are forbidden if True
894 @param do_update(bool): if True, a full update is done after unlocking 972 @param do_update(bool): if True, a full update is done after unlocking
895 if set to False, widget state can be inconsistent, be sure to know 973 if set to False, widget state can be inconsistent, be sure to know
896 what youa re doing! 974 what youa re doing!
897 """ 975 """
898 log.debug(u"Contact lists updates are now {}".format(u"LOCKED" if locked else u"UNLOCKED")) 976 log.debug(u"Contact lists updates are now {}".format(
977 u"LOCKED" if locked else u"UNLOCKED"))
899 self._update_locked = locked 978 self._update_locked = locked
900 if not locked and do_update: 979 if not locked and do_update:
901 self.update() 980 self.update()
902 981
903 def update(self, entities=None, type_=None, profile=None): 982 def update(self, entities=None, type_=None, profile=None):
908 987
909 class QuickContactList(QuickWidget): 988 class QuickContactList(QuickWidget):
910 """This class manage the visual representation of contacts""" 989 """This class manage the visual representation of contacts"""
911 SINGLE=False 990 SINGLE=False
912 PROFILES_MULTIPLE=True 991 PROFILES_MULTIPLE=True
913 PROFILES_ALLOW_NONE=True # Can be linked to no profile (e.g. at the early forntend start) 992 # Can be linked to no profile (e.g. at the early frontend start)
993 PROFILES_ALLOW_NONE=True
914 994
915 def __init__(self, host, profiles): 995 def __init__(self, host, profiles):
916 super(QuickContactList, self).__init__(host, None, profiles) 996 super(QuickContactList, self).__init__(host, None, profiles)
917 997
918 # options 998 # options
935 def items(self): 1015 def items(self):
936 return handler.items 1016 return handler.items
937 1017
938 @property 1018 @property
939 def items_sorted(self): 1019 def items_sorted(self):
940 return handler.items 1020 return handler.items_sorted
941 1021
942 def update(self, entities=None, type_=None, profile=None): 1022 def update(self, entities=None, type_=None, profile=None):
943 """Update the display when something changed 1023 """Update the display when something changed
944 1024
945 @param entities(iterable[jid.JID], None): updated entities, 1025 @param entities(iterable[jid.JID], None): updated entities,
948 - C.UPDATE_DELETE: entity deleted 1028 - C.UPDATE_DELETE: entity deleted
949 - C.UPDATE_MODIFY: entity updated 1029 - C.UPDATE_MODIFY: entity updated
950 - C.UPDATE_ADD: entity added 1030 - C.UPDATE_ADD: entity added
951 - C.UPDATE_SELECTION: selection modified 1031 - C.UPDATE_SELECTION: selection modified
952 or None for undefined update 1032 or None for undefined update
1033 Note that events correspond to addition, modification and deletion
1034 of items on the whole contact list. If the contact is visible or not
1035 has no influence on the type_.
953 @param profile(unicode, None): profile concerned with the update 1036 @param profile(unicode, None): profile concerned with the update
954 None if unknown 1037 None if all profiles need to be updated
955 """ 1038 """
956 raise NotImplementedError 1039 raise NotImplementedError
957 1040
958 def onDelete(self): 1041 def onDelete(self):
959 QuickWidget.onDelete(self) 1042 QuickWidget.onDelete(self)