Rename NameOwner -> NameDetails
[platform/upstream/folks.git] / folks / individual.vala
1 /*
2  * Copyright (C) 2010 Collabora Ltd.
3  *
4  * This library is free software: you can redistribute it and/or modify
5  * it under the terms of the GNU Lesser General Public License as published by
6  * the Free Software Foundation, either version 2.1 of the License, or
7  * (at your option) any later version.
8  *
9  * This library is distributed in the hope that it will be useful,
10  * but WITHOUT ANY WARRANTY; without even the implied warranty of
11  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12  * GNU Lesser General Public License for more details.
13  *
14  * You should have received a copy of the GNU Lesser General Public License
15  * along with this library.  If not, see <http://www.gnu.org/licenses/>.
16  *
17  * Authors:
18  *       Travis Reitter <travis.reitter@collabora.co.uk>
19  */
20
21 using Gee;
22 using GLib;
23
24 /**
25  * Trust level for an {@link Individual} for use in the UI.
26  *
27  * @since 0.1.15
28  */
29 public enum Folks.TrustLevel
30 {
31   /**
32    * The {@link Individual}'s {@link Persona}s aren't trusted at all.
33    *
34    * This is the trust level for an {@link Individual} which contains one or
35    * more {@link Persona}s which cannot be guaranteed to be the same
36    * {@link Persona}s as were originally linked together.
37    *
38    * For example, an {@link Individual} containing a link-local XMPP
39    * {@link Persona} would have this trust level, since someone else could
40    * easily spoof the link-local XMPP {@link Persona}'s identity.
41    *
42    * @since 0.1.15
43    */
44   NONE,
45
46   /**
47    * The {@link Individual}'s {@link Persona}s are trusted.
48    *
49    * This trust level is for {@link Individual}s where it can be guaranteed
50    * that all the {@link Persona}s are the same ones as when they were
51    * originally linked together.
52    *
53    * Note that this doesn't guarantee that the user who behind each
54    * {@link Persona} is who they claim to be.
55    *
56    * @since 0.1.15
57    */
58   PERSONAS
59 }
60
61 /**
62  * A physical person, aggregated from the various {@link Persona}s the person
63  * might have, such as their different IM addresses or vCard entries.
64  */
65 public class Folks.Individual : Object,
66     AliasDetails,
67     AvatarDetails,
68     BirthdayDetails,
69     EmailDetails,
70     FavouriteDetails,
71     GenderDetails,
72     GroupDetails,
73     ImDetails,
74     NameDetails,
75     NoteOwner,
76     PresenceOwner,
77     Phoneable,
78     PostalAddressOwner,
79     RoleOwner,
80     Urlable
81 {
82   private bool _is_favourite;
83   private string _alias;
84   private HashTable<string, bool> _groups;
85   /* These two data structures should store exactly the same set of Personas:
86    * the Personas contained in this Individual. The HashSet is used for fast
87    * lookups, whereas the List is used for iteration.
88    * The Individual's references to its Personas are kept by the HashSet;
89    * since the List contains the same set of Personas, it doesn't need an
90    * extra reference (and due to bgo#624249, this is a good thing). */
91   private GLib.List<unowned Persona> _persona_list;
92   private HashSet<Persona> _persona_set;
93   /* Mapping from PersonaStore -> number of Personas from that store contained
94    * in this Individual. There shouldn't be any entries with a number < 1.
95    * This is used for working out when to disconnect from store signals. */
96   private HashMap<PersonaStore, uint> _stores;
97   /* The number of Personas in this Individual which have
98    * Persona.is_user == true. Iff this is > 0, Individual.is_user == true. */
99   private uint _persona_user_count = 0;
100   private HashTable<string, LinkedHashSet<string>> _im_addresses;
101
102   /**
103    * The trust level of the Individual.
104    *
105    * This specifies how far the Individual can be trusted to be who it claims
106    * to be. See the descriptions for the elements of {@link TrustLevel}.
107    *
108    * Clients should ''not'' allow linking of Individuals who have a trust level
109    * of {@link TrustLevel.NONE}.
110    *
111    * @since 0.1.15
112    */
113   public TrustLevel trust_level { get; private set; }
114
115   /**
116    * {@inheritDoc}
117    */
118   public File avatar { get; private set; }
119
120   /**
121    * {@inheritDoc}
122    */
123   public Folks.PresenceType presence_type { get; private set; }
124
125   /**
126    * {@inheritDoc}
127    */
128   public string presence_message { get; private set; }
129
130   /**
131    * Whether the Individual is the user.
132    *
133    * Iff the Individual represents the user (the person who owns the
134    * account in the backend for each {@link Persona} in the Individual)
135    * this is `true`.
136    *
137    * It is //not// guaranteed that every {@link Persona} in the Individual has
138    * its {@link Persona.is_user} set to the same value as the Individual. For
139    * example, the user could own two Telepathy accounts, and have added the
140    * other account as a contact in each account. The accounts will expose a
141    * {@link Persona} for the user (which will have {@link Persona.is_user} set
142    * to `true`) //and// a {@link Persona} for the contact for the other account
143    * (which will have {@link Persona.is_user} set to `false`).
144    *
145    * It is guaranteed that iff this property is set to `true` on an Individual,
146    * there will be at least one {@link Persona} in the Individual with its
147    * {@link Persona.is_user} set to `true`.
148    *
149    * It is guaranteed that there will only ever be one Individual with this
150    * property set to `true`.
151    *
152    * @since 0.3.0
153    */
154   public bool is_user { get; private set; }
155
156   /**
157    * A unique identifier for the Individual.
158    *
159    * This uniquely identifies the Individual, and persists across
160    * {@link IndividualAggregator} instances.
161    *
162    * FIXME: Will this.id actually be the persistent ID for storage?
163    */
164   public string id { get; private set; }
165
166   /**
167    * Emitted when the last of the Individual's {@link Persona}s has been
168    * removed.
169    *
170    * At this point, the Individual is invalid, so any client referencing it
171    * should unreference it and remove it from their UI.
172    *
173    * @param replacement_individual the individual which has replaced this one
174    * due to linking, or `null` if this individual was removed for another reason
175    * @since 0.1.13
176    */
177   public signal void removed (Individual? replacement_individual);
178
179   /**
180    * {@inheritDoc}
181    */
182   public string alias
183     {
184       get { return this._alias; }
185
186       set
187         {
188           if (this._alias == value)
189             return;
190
191           this._alias = value;
192
193           debug ("Setting alias of individual '%s' to '%s'…", this.id, value);
194
195           /* First, try to write it to only the writeable Personas… */
196           var alias_changed = false;
197           this._persona_list.foreach ((p) =>
198             {
199               if (p is AliasDetails &&
200                   ((Persona) p).store.is_writeable == true)
201                 {
202                   debug ("    written to writeable persona '%s'",
203                       ((Persona) p).uid);
204                   ((AliasDetails) p).alias = value;
205                   alias_changed = true;
206                 }
207             });
208
209           /* …but if there are no writeable Personas, we have to fall back to
210            * writing it to every Persona. */
211           if (alias_changed == false)
212             {
213               this._persona_list.foreach ((p) =>
214                 {
215                   if (p is AliasDetails)
216                     {
217                       debug ("    written to non-writeable persona '%s'",
218                           ((Persona) p).uid);
219                       ((AliasDetails) p).alias = value;
220                     }
221                 });
222             }
223         }
224     }
225
226   /**
227    * {@inheritDoc}
228    */
229   public StructuredName structured_name { get; private set; }
230
231   /**
232    * {@inheritDoc}
233    */
234   public string full_name { get; private set; }
235
236   private string _nickname;
237   /**
238    * {@inheritDoc}
239    */
240   public string nickname { get { return this._nickname; } }
241
242   private Gender _gender;
243   /**
244    * {@inheritDoc}
245    */
246   public Gender gender
247     {
248       get { return this._gender; }
249       private set
250         {
251           this._gender = value;
252           this.notify_property ("gender");
253         }
254     }
255
256   private GLib.List<FieldDetails> _urls;
257   /**
258    * {@inheritDoc}
259    */
260   public GLib.List<FieldDetails> urls
261     {
262       get { return this._urls; }
263       private set
264         {
265           this._urls = new GLib.List<FieldDetails> ();
266           foreach (unowned FieldDetails ps in value)
267             this._urls.prepend (ps);
268           this._urls.reverse ();
269         }
270     }
271
272   private GLib.List<FieldDetails> _phone_numbers;
273   /**
274    * {@inheritDoc}
275    */
276   public GLib.List<FieldDetails> phone_numbers
277     {
278       get { return this._phone_numbers; }
279       private set
280         {
281           this._phone_numbers = new GLib.List<FieldDetails> ();
282           foreach (unowned FieldDetails fd in value)
283             this._phone_numbers.prepend (fd);
284           this._phone_numbers.reverse ();
285         }
286     }
287
288   private GLib.List<FieldDetails> _email_addresses;
289   /**
290    * {@inheritDoc}
291    */
292   public GLib.List<FieldDetails> email_addresses
293     {
294       get { return this._email_addresses; }
295       private set
296         {
297           this._email_addresses = new GLib.List<FieldDetails> ();
298           foreach (unowned FieldDetails fd in value)
299             this._email_addresses.prepend (fd);
300           this._email_addresses.reverse ();
301         }
302     }
303
304   private HashSet<Role> _roles;
305   /**
306    * {@inheritDoc}
307    */
308   public HashSet<Role> roles
309     {
310       get { return this._roles; }
311       private set
312         {
313           this._roles = value;
314           this.notify_property ("roles");
315         }
316     }
317
318   public DateTime birthday { get; set; }
319
320   public string calendar_event_id { get; set; }
321
322   private HashSet<Note> _notes;
323   /**
324    * {@inheritDoc}
325    */
326   public HashSet<Note> notes
327     {
328       get { return this._notes; }
329       private set
330         {
331           this._notes = value;
332           this.notify_property ("notes");
333         }
334     }
335
336   private GLib.List<PostalAddress> _postal_addresses;
337   /**
338    * {@inheritDoc}
339    */
340   public GLib.List<PostalAddress> postal_addresses
341     {
342       get { return this._postal_addresses; }
343       private set
344         {
345           this._postal_addresses = new GLib.List<PostalAddress> ();
346           foreach (PostalAddress pa in value)
347             this._postal_addresses.prepend (pa);
348           this._postal_addresses.reverse ();
349         }
350     }
351
352   /**
353    * Whether this Individual is a user-defined favourite.
354    *
355    * This property is `true` if any of this Individual's {@link Persona}s are
356    * favourites).
357    */
358   public bool is_favourite
359     {
360       get { return this._is_favourite; }
361
362       set
363         {
364           if (this._is_favourite == value)
365             return;
366
367           debug ("Setting '%s' favourite status to %s", this.id,
368               value ? "TRUE" : "FALSE");
369
370           this._is_favourite = value;
371           this._persona_list.foreach ((p) =>
372             {
373               if (p is FavouriteDetails)
374                 {
375                   SignalHandler.block_by_func (p,
376                       (void*) this._notify_is_favourite_cb, this);
377                   ((FavouriteDetails) p).is_favourite = value;
378                   SignalHandler.unblock_by_func (p,
379                       (void*) this._notify_is_favourite_cb, this);
380                 }
381             });
382         }
383     }
384
385   /**
386    * {@inheritDoc}
387    */
388   public HashTable<string, bool> groups
389     {
390       get { return this._groups; }
391
392       set
393         {
394           this._groups = value;
395           this._persona_list.foreach ((p) =>
396             {
397               if (p is GroupDetails && ((Persona) p).store.is_writeable == true)
398                 ((GroupDetails) p).groups = value;
399             });
400         }
401     }
402
403   /**
404    * {@inheritDoc}
405    */
406   public HashTable<string, LinkedHashSet<string>> im_addresses
407     {
408       get { return this._im_addresses; }
409       private set {}
410     }
411
412   /**
413    * The set of {@link Persona}s encapsulated by this Individual.
414    *
415    * Changing the set of personas may cause updates to the aggregated properties
416    * provided by the Individual, resulting in property notifications for them.
417    *
418    * Changing the set of personas will not cause permanent linking/unlinking of
419    * the added/removed personas to/from this Individual. To do that, call
420    * {@link IndividualAggregator.link_personas} or
421    * {@link IndividualAggregator.unlink_individual}, which will ensure the link
422    * changes are written to the appropriate backend.
423    */
424   public GLib.List<Persona> personas
425     {
426       get { return this._persona_list; }
427       set { this._set_personas (value, null); }
428     }
429
430   /**
431    * Emitted when one or more {@link Persona}s are added to or removed from
432    * the Individual.
433    *
434    * @param added a list of {@link Persona}s which have been added
435    * @param removed a list of {@link Persona}s which have been removed
436    *
437    * @since 0.1.15
438    */
439   public signal void personas_changed (GLib.List<Persona>? added,
440       GLib.List<Persona>? removed);
441
442   private void _notify_alias_cb (Object obj, ParamSpec ps)
443     {
444       this._update_alias ();
445     }
446
447   private void _notify_avatar_cb (Object obj, ParamSpec ps)
448     {
449       this._update_avatar ();
450     }
451
452   private void _notify_full_name_cb ()
453     {
454       this._update_full_name ();
455     }
456
457   private void _notify_structured_name_cb ()
458     {
459       this._update_structured_name ();
460     }
461
462   private void _notify_nickname_cb ()
463     {
464       this._update_nickname ();
465     }
466
467   private void _persona_group_changed_cb (string group, bool is_member)
468     {
469       this._update_groups ();
470     }
471
472   private void _notify_gender_cb ()
473     {
474       this._update_gender ();
475     }
476
477   private void _notify_urls_cb ()
478     {
479       this._update_urls ();
480     }
481
482   private void _notify_phone_numbers_cb ()
483     {
484       this._update_phone_numbers ();
485     }
486
487   private void _notify_postal_addresses_cb ()
488     {
489       this._update_postal_addresses ();
490     }
491
492   private void _notify_email_addresses_cb ()
493     {
494       this._update_email_addresses ();
495     }
496
497   private void _notify_roles_cb ()
498     {
499       this._update_roles ();
500     }
501
502   private void _notify_birthday_cb ()
503     {
504       this._update_birthday ();
505     }
506
507   private void _notify_notes_cb ()
508     {
509       this._update_notes ();
510     }
511
512   /**
513    * Add or remove the Individual from the specified group.
514    *
515    * If `is_member` is `true`, the Individual will be added to the `group`. If
516    * it is `false`, they will be removed from the `group`.
517    *
518    * The group membership change will propagate to every {@link Persona} in
519    * the Individual.
520    *
521    * @param group a freeform group identifier
522    * @param is_member whether the Individual should be a member of the group
523    * @since 0.1.11
524    */
525   public async void change_group (string group, bool is_member)
526     {
527       this._persona_list.foreach ((p) =>
528         {
529           if (p is GroupDetails)
530             ((GroupDetails) p).change_group.begin (group, is_member);
531         });
532
533       /* don't notify, since it hasn't happened in the persona backing stores
534        * yet; react to that directly */
535     }
536
537   private void _notify_presence_cb (Object obj, ParamSpec ps)
538     {
539       this._update_presence ();
540     }
541
542   private void _notify_im_addresses_cb (Object obj, ParamSpec ps)
543     {
544       this._update_im_addresses ();
545     }
546
547   private void _notify_is_favourite_cb (Object obj, ParamSpec ps)
548     {
549       this._update_is_favourite ();
550     }
551
552   /**
553    * Create a new Individual.
554    *
555    * The Individual can optionally be seeded with the {@link Persona}s in
556    * `personas`. Otherwise, it will have to have personas added using the
557    * {@link Folks.Individual.personas} property after construction.
558    *
559    * @param personas a list of {@link Persona}s to initialise the
560    * {@link Individual} with, or `null`
561    * @return a new Individual
562    */
563   public Individual (GLib.List<Persona>? personas)
564     {
565       this._im_addresses =
566           new HashTable<string, LinkedHashSet<string>> (str_hash, str_equal);
567       this._persona_set = new HashSet<Persona> (null, null);
568       this._stores = new HashMap<PersonaStore, uint> (null, null);
569       this._gender = Gender.UNSPECIFIED;
570       this.personas = personas;
571     }
572
573   private void _store_removed_cb (PersonaStore store)
574     {
575       GLib.List<Persona> removed_personas = null;
576       var iter = this._persona_set.iterator ();
577       while (iter.next ())
578         {
579           var persona = iter.get ();
580
581           removed_personas.prepend (persona);
582           this._persona_list.remove (persona);
583           iter.remove ();
584         }
585
586       if (removed_personas != null)
587         this.personas_changed (null, removed_personas);
588
589       if (store != null)
590         this._stores.unset (store);
591
592       if (this._persona_set.size < 1)
593         {
594           this.removed (null);
595           return;
596         }
597
598       this._update_fields ();
599     }
600
601   private void _store_personas_changed_cb (PersonaStore store,
602       GLib.List<Persona>? added,
603       GLib.List<Persona>? removed,
604       string? message,
605       Persona? actor,
606       GroupDetails.ChangeReason reason)
607     {
608       GLib.List<Persona> removed_personas = null;
609       removed.foreach ((data) =>
610         {
611           var p = (Persona) data;
612
613           if (this._persona_set.remove (p))
614             {
615               removed_personas.prepend (p);
616               this._persona_list.remove (p);
617             }
618         });
619
620       if (removed_personas != null)
621         this.personas_changed (null, removed_personas);
622
623       if (this._persona_set.size < 1)
624         {
625           this.removed (null);
626           return;
627         }
628
629       this._update_fields ();
630     }
631
632   private void _update_fields ()
633     {
634       this._update_groups ();
635       this._update_presence ();
636       this._update_is_favourite ();
637       this._update_avatar ();
638       this._update_alias ();
639       this._update_trust_level ();
640       this._update_im_addresses ();
641       this._update_structured_name ();
642       this._update_full_name ();
643       this._update_nickname ();
644       this._update_gender ();
645       this._update_urls ();
646       this._update_phone_numbers ();
647       this._update_email_addresses ();
648       this._update_roles ();
649       this._update_birthday ();
650       this._update_notes ();
651       this._update_postal_addresses ();
652     }
653
654   private void _update_groups ()
655     {
656       var new_groups = new HashTable<string, bool> (str_hash, str_equal);
657
658       /* this._groups is null during initial construction */
659       if (this._groups == null)
660         this._groups = new HashTable<string, bool> (str_hash, str_equal);
661
662       /* FIXME: this should partition the personas by store (maybe we should
663        * keep that mapping in general in this class), and execute
664        * "groups-changed" on the store (with the set of personas), to allow the
665        * back-end to optimize it (like Telepathy will for MembersChanged for the
666        * groups channel list) */
667       this._persona_list.foreach ((p) =>
668         {
669           if (p is GroupDetails)
670             {
671               var persona = (GroupDetails) p;
672
673               persona.groups.foreach ((k, v) =>
674                 {
675                   new_groups.insert ((string) k, true);
676                 });
677             }
678         });
679
680       new_groups.foreach ((k, v) =>
681         {
682           unowned string group = (string) k;
683           if (this._groups.lookup (group) != true)
684             {
685               this._groups.insert (group, true);
686               this._groups.foreach ((k, v) =>
687                 {
688                   unowned string g = (string) k;
689                   debug ("   %s", g);
690                 });
691
692               this.group_changed (group, true);
693             }
694         });
695
696       /* buffer the removals, so we don't remove while iterating */
697       var removes = new GLib.List<string> ();
698       this._groups.foreach ((k, v) =>
699         {
700           unowned string group = (string) k;
701           if (new_groups.lookup (group) != true)
702             removes.prepend (group);
703         });
704
705       removes.foreach ((l) =>
706         {
707           unowned string group = (string) l;
708           this._groups.remove (group);
709           this.group_changed (group, false);
710         });
711     }
712
713   private void _update_presence ()
714     {
715       var presence_message = "";
716       var presence_type = Folks.PresenceType.UNSET;
717
718       /* Choose the most available presence from our personas */
719       this._persona_list.foreach ((p) =>
720         {
721           if (p is PresenceOwner)
722             {
723               unowned PresenceOwner presence = (PresenceOwner) p;
724
725               if (PresenceOwner.typecmp (presence.presence_type,
726                   presence_type) > 0)
727                 {
728                   presence_type = presence.presence_type;
729                   presence_message = presence.presence_message;
730                 }
731             }
732         });
733
734       if (presence_message == null)
735         presence_message = "";
736
737       /* only notify if the value has changed */
738       if (this.presence_message != presence_message)
739         this.presence_message = presence_message;
740
741       if (this.presence_type != presence_type)
742         this.presence_type = presence_type;
743     }
744
745   private void _update_is_favourite ()
746     {
747       var favourite = false;
748
749       debug ("Running _update_is_favourite() on '%s'", this.id);
750
751       this._persona_list.foreach ((p) =>
752         {
753           if (favourite == false && p is FavouriteDetails)
754             {
755               favourite = ((FavouriteDetails) p).is_favourite;
756               if (favourite == true)
757                 return;
758             }
759         });
760
761       /* Only notify if the value has changed. We have to set the private member
762        * and notify manually, or we'd end up propagating the new favourite
763        * status back down to all our Personas. */
764       if (this._is_favourite != favourite)
765         {
766           this._is_favourite = favourite;
767           this.notify_property ("is-favourite");
768         }
769     }
770
771   private void _update_alias ()
772     {
773       string alias = null;
774       var alias_is_display_id = false;
775
776       debug ("Updating alias for individual '%s'", this.id);
777
778       /* Search for an alias from a writeable Persona, and use it as our first
779        * choice if it's non-empty, since that's where the user-set alias is
780        * stored. */
781       foreach (var p in this._persona_list)
782         {
783           if (p is AliasDetails && p.store.is_writeable == true)
784             {
785               var a = (AliasDetails) p;
786
787               if (a.alias != null && a.alias.strip () != "")
788                 {
789                   alias = a.alias;
790                   break;
791                 }
792             }
793         }
794
795       debug ("    got alias '%s' from writeable personas", alias);
796
797       /* Since we can't find a non-empty alias from a writeable backend, try
798        * the aliases from other personas. Use a non-empty alias which isn't
799        * equal to the persona's display ID as our preference. If we can't find
800        * one of those, fall back to one which is equal to the display ID. */
801       if (alias == null)
802         {
803           foreach (var p in this._persona_list)
804             {
805               if (p is AliasDetails)
806                 {
807                   var a = (AliasDetails) p;
808
809                   if (a.alias == null || a.alias.strip () == "")
810                     continue;
811
812                   if (alias == null || alias_is_display_id == true)
813                     {
814                       /* We prefer to not have an alias which is the same as the
815                        * Persona's display-id, since having such an alias
816                        * implies that it's the default. However, we prefer using
817                        * such an alias to using the Persona's UID, which is our
818                        * ultimate fallback (below). */
819                       alias = a.alias;
820
821                       if (a.alias == p.display_id)
822                         alias_is_display_id = true;
823                       else if (alias != null)
824                         break;
825                     }
826                 }
827             }
828         }
829
830       debug ("    got alias '%s' from non-writeable personas", alias);
831
832       if (alias == null)
833         {
834           /* We have to pick a display ID, since none of the personas have an
835            * alias available. Pick the display ID from the first persona in the
836            * list. */
837           alias = this._persona_list.data.display_id;
838           debug ("No aliases available for individual; using display ID " +
839               "instead: %s", alias);
840         }
841
842       /* Only notify if the value has changed. We have to set the private member
843        * and notify manually, or we'd end up propagating the new alias back
844        * down to all our Personas, even if it's a fallback display ID or
845        * something else undesirable. */
846       if (this._alias != alias)
847         {
848           debug ("Changing alias of individual '%s' from '%s' to '%s'.",
849               this.id, this._alias, alias);
850           this._alias = alias;
851           this.notify_property ("alias");
852         }
853     }
854
855   private void _update_avatar ()
856     {
857       File avatar = null;
858
859       this._persona_list.foreach ((p) =>
860         {
861           if (avatar == null && p is AvatarDetails)
862             {
863               avatar = ((AvatarDetails) p).avatar;
864               return;
865             }
866         });
867
868       /* only notify if the value has changed */
869       if (this.avatar != avatar)
870         this.avatar = avatar;
871     }
872
873   private void _update_trust_level ()
874     {
875       var trust_level = TrustLevel.PERSONAS;
876
877       foreach (var p in this._persona_list)
878         {
879           if (p.is_user == false &&
880               p.store.trust_level == PersonaStoreTrust.NONE)
881             trust_level = TrustLevel.NONE;
882         }
883
884       /* Only notify if the value has changed */
885       if (this.trust_level != trust_level)
886         this.trust_level = trust_level;
887     }
888
889   private void _update_im_addresses ()
890     {
891       /* populate the IM addresses as the union of our Personas' addresses */
892       foreach (var persona in this.personas)
893         {
894           if (persona is ImDetails)
895             {
896               var im_details = (ImDetails) persona;
897               im_details.im_addresses.foreach ((k, v) =>
898                 {
899                   var cur_protocol = (string) k;
900                   var cur_addresses = (LinkedHashSet<string>) v;
901                   var im_set = this._im_addresses.lookup (cur_protocol);
902
903                   if (im_set == null)
904                     {
905                       im_set = new LinkedHashSet<string> ();
906                       this._im_addresses.insert (cur_protocol, im_set);
907                     }
908
909                   im_set.add_all (cur_addresses);
910                 });
911             }
912         }
913       this.notify_property ("im-addresses");
914     }
915
916   private void _connect_to_persona (Persona persona)
917     {
918       persona.notify["alias"].connect (this._notify_alias_cb);
919       persona.notify["avatar"].connect (this._notify_avatar_cb);
920       persona.notify["presence-message"].connect (this._notify_presence_cb);
921       persona.notify["presence-type"].connect (this._notify_presence_cb);
922       persona.notify["im-addresses"].connect (this._notify_im_addresses_cb);
923       persona.notify["is-favourite"].connect (this._notify_is_favourite_cb);
924       persona.notify["structured-name"].connect (
925           this._notify_structured_name_cb);
926       persona.notify["full-name"].connect (this._notify_full_name_cb);
927       persona.notify["nickname"].connect (this._notify_nickname_cb);
928       persona.notify["gender"].connect (this._notify_gender_cb);
929       persona.notify["urls"].connect (this._notify_urls_cb);
930       persona.notify["phone-numbers"].connect (this._notify_phone_numbers_cb);
931       persona.notify["email-addresses"].connect (
932           this._notify_email_addresses_cb);
933       persona.notify["roles"].connect (this._notify_roles_cb);
934       persona.notify["birthday"].connect (this._notify_birthday_cb);
935       persona.notify["notes"].connect (this._notify_notes_cb);
936       persona.notify["postal-addresses"].connect
937           (this._notify_postal_addresses_cb);
938
939       if (persona is GroupDetails)
940         {
941           ((GroupDetails) persona).group_changed.connect (
942               this._persona_group_changed_cb);
943         }
944     }
945
946   private void _update_structured_name ()
947     {
948       foreach (var persona in this._persona_list)
949         {
950           var name_details = persona as NameDetails;
951           if (name_details != null)
952             {
953               var new_value = name_details.structured_name;
954               if (new_value != null)
955                 {
956                   if (new_value != this.structured_name)
957                     this.structured_name = new_value;
958
959                   break;
960                 }
961             }
962         }
963     }
964
965   private void _update_full_name ()
966     {
967       foreach (var persona in this._persona_list)
968         {
969           var name_details = persona as NameDetails;
970           if (name_details != null)
971             {
972               var new_value = name_details.full_name;
973               if (new_value != null)
974                 {
975                   if (new_value != this.full_name)
976                     this.full_name = new_value;
977
978                   break;
979                 }
980             }
981         }
982     }
983
984   private void _update_nickname ()
985     {
986       foreach (var persona in this._persona_list)
987         {
988           var name_details = persona as NameDetails;
989           if (name_details != null)
990             {
991               var new_value = name_details.nickname;
992               if (new_value != null)
993                 {
994                   if (new_value != this._nickname)
995                     {
996                       this._nickname = new_value;
997                       this.notify_property ("nickname");
998                     }
999
1000                   break;
1001                 }
1002             }
1003         }
1004     }
1005
1006   private void _disconnect_from_persona (Persona persona)
1007     {
1008       persona.notify["alias"].disconnect (this._notify_alias_cb);
1009       persona.notify["avatar"].disconnect (this._notify_avatar_cb);
1010       persona.notify["presence-message"].disconnect (
1011           this._notify_presence_cb);
1012       persona.notify["presence-type"].disconnect (this._notify_presence_cb);
1013       persona.notify["im-addresses"].disconnect (
1014           this._notify_im_addresses_cb);
1015       persona.notify["is-favourite"].disconnect (
1016           this._notify_is_favourite_cb);
1017       persona.notify["structured-name"].disconnect (
1018           this._notify_structured_name_cb);
1019       persona.notify["full-name"].disconnect (this._notify_full_name_cb);
1020       persona.notify["nickname"].disconnect (this._notify_nickname_cb);
1021       persona.notify["gender"].disconnect (this._notify_gender_cb);
1022       persona.notify["urls"].disconnect (this._notify_urls_cb);
1023       persona.notify["phone-numbers"].disconnect (
1024           this._notify_phone_numbers_cb);
1025       persona.notify["email-addresses"].disconnect (
1026           this._notify_email_addresses_cb);
1027       persona.notify["roles"].disconnect (this._notify_roles_cb);
1028       persona.notify["birthday"].disconnect (this._notify_birthday_cb);
1029       persona.notify["notes"].disconnect (this._notify_notes_cb);
1030       persona.notify["postal-addresses"].disconnect
1031           (this._notify_postal_addresses_cb);
1032
1033
1034       if (persona is GroupDetails)
1035         {
1036           ((GroupDetails) persona).group_changed.disconnect (
1037               this._persona_group_changed_cb);
1038         }
1039     }
1040
1041   private void _update_gender ()
1042     {
1043       foreach (var persona in this._persona_list)
1044         {
1045           var gender_details = persona as GenderDetails;
1046           if (gender_details != null)
1047             {
1048               var new_value = gender_details.gender;
1049               if (new_value != Gender.UNSPECIFIED)
1050                 {
1051                   if (new_value != this.gender)
1052                     this.gender = new_value;
1053                   break;
1054                 }
1055             }
1056         }
1057     }
1058
1059   private void _update_urls ()
1060     {
1061       /* Populate the URLs as the union of our Personas' URLs.
1062        * If the same URL exist multiple times we merge the parameters. */
1063       var urls_set = new HashTable<unowned string, unowned FieldDetails> (
1064           str_hash, str_equal);
1065       var urls = new GLib.List<FieldDetails> ();
1066
1067       foreach (var persona in this._persona_list)
1068         {
1069           var urlable = persona as Urlable;
1070           if (urlable != null)
1071             {
1072               foreach (unowned FieldDetails ps in urlable.urls)
1073                 {
1074                   if (ps.value == null)
1075                     continue;
1076
1077                   var existing = urls_set.lookup (ps.value);
1078                   if (existing != null)
1079                     existing.extend_parameters (ps.parameters);
1080                   else
1081                     {
1082                       var new_ps = new FieldDetails (ps.value);
1083                       new_ps.extend_parameters (ps.parameters);
1084                       urls_set.insert (ps.value, new_ps);
1085                       urls.prepend ((owned) new_ps);
1086                     }
1087                 }
1088             }
1089         }
1090       /* Set the private member directly to avoid iterating this list again */
1091       urls.reverse ();
1092       this._urls = (owned) urls;
1093
1094       this.notify_property ("urls");
1095     }
1096
1097   private void _update_phone_numbers ()
1098     {
1099       /* Populate the phone numberss as the union of our Personas' numbers
1100        * If the same number exist multiple times we merge the parameters. */
1101       /* FIXME: We should handle phone numbers better, just string comparison
1102          doesn't work. */
1103       var phone_numbers_set =
1104           new HashTable<unowned string, unowned FieldDetails> (
1105               str_hash, str_equal);
1106       foreach (var persona in this._persona_list)
1107         {
1108           var phoneable = persona as Phoneable;
1109           if (phoneable != null)
1110             {
1111               foreach (unowned FieldDetails fd in phoneable.phone_numbers)
1112                 {
1113                   if (fd.value == null)
1114                     continue;
1115
1116                   var existing = phone_numbers_set.lookup (fd.value);
1117                   if (existing != null)
1118                     existing.extend_parameters (fd.parameters);
1119                   else
1120                     phone_numbers_set.insert (fd.value, fd);
1121                 }
1122             }
1123         }
1124       this._phone_numbers = phone_numbers_set.get_values ();
1125
1126       this.notify_property ("phone-numbers");
1127     }
1128
1129   private void _update_email_addresses ()
1130     {
1131       /* Populate the email addresses as the union of our Personas' addresses.
1132        * If the same URL exist multiple times we merge the parameters. */
1133       var emails_set = new HashTable<unowned string, unowned FieldDetails> (
1134           str_hash, str_equal);
1135       foreach (var persona in this._persona_list)
1136         {
1137           var email_details = persona as EmailDetails;
1138           if (email_details != null)
1139             {
1140               foreach (unowned FieldDetails fd in email_details.email_addresses)
1141                 {
1142                   if (fd.value == null)
1143                     continue;
1144
1145                   var existing = emails_set.lookup (fd.value);
1146                   if (existing != null)
1147                     existing.extend_parameters (fd.parameters);
1148                   else
1149                     emails_set.insert (fd.value, fd);
1150                 }
1151             }
1152         }
1153       this._email_addresses = emails_set.get_values ();
1154
1155       this.notify_property ("email-addresses");
1156     }
1157
1158   private void _update_roles ()
1159     {
1160       HashSet<Role> roles = new HashSet<Role>
1161           ((GLib.HashFunc) Role.hash, (GLib.EqualFunc) Role.equal);
1162
1163       foreach (var persona in this._persona_list)
1164         {
1165           var role_owner = persona as RoleOwner;
1166           if (role_owner != null)
1167             {
1168               foreach (var r in role_owner.roles)
1169                 {
1170                   if (roles.contains (r) == false)
1171                     {
1172                       roles.add (r);
1173                     }
1174                 }
1175             }
1176         }
1177
1178       this._roles = (owned) roles;
1179       this.notify_property ("roles");
1180     }
1181
1182   private void _update_postal_addresses ()
1183     {
1184       this._postal_addresses = new GLib.List<PostalAddress> ();
1185       /* FIXME: Detect duplicates somehow? */
1186       foreach (var persona in this._persona_list)
1187         {
1188           var address_owner = persona as PostalAddressOwner;
1189           if (address_owner != null)
1190             {
1191               foreach (unowned PostalAddress pa in address_owner.postal_addresses)
1192                 this._postal_addresses.append (pa);
1193             }
1194         }
1195       this._postal_addresses.reverse ();
1196
1197       this.notify_property ("postal-addresses");
1198     }
1199
1200   private void _update_birthday ()
1201     {
1202       unowned DateTime bday = null;
1203       unowned string calendar_event_id = "";
1204
1205       foreach (var persona in this._persona_list)
1206         {
1207           var bday_owner = persona as BirthdayDetails;
1208           if (bday_owner != null)
1209             {
1210               if (bday_owner.birthday != null)
1211                 {
1212                   if (this.birthday == null ||
1213                       bday_owner.birthday.compare (this.birthday) != 0)
1214                     {
1215                       bday = bday_owner.birthday;
1216                       calendar_event_id = bday_owner.calendar_event_id;
1217                       break;
1218                     }
1219                 }
1220             }
1221         }
1222
1223       if (this.birthday != null && bday == null)
1224         {
1225           this.birthday = null;
1226           this.calendar_event_id = null;
1227         }
1228       else if (bday != null)
1229         {
1230           this.birthday = bday;
1231           this.calendar_event_id = calendar_event_id;
1232         }
1233     }
1234
1235   private void _update_notes ()
1236     {
1237       HashSet<Note> notes = new HashSet<Note>
1238           ((GLib.HashFunc) Note.hash, (GLib.EqualFunc) Note.equal);
1239
1240       foreach (var persona in this._persona_list)
1241         {
1242           var note_owner = persona as NoteOwner;
1243           if (note_owner != null)
1244             {
1245               foreach (var n in note_owner.notes)
1246                 {
1247                   notes.add (n);
1248                 }
1249             }
1250         }
1251
1252       this._notes = (owned) notes;
1253       this.notify_property ("notes");
1254     }
1255
1256   private void _set_personas (GLib.List<Persona>? persona_list,
1257       Individual? replacement_individual)
1258     {
1259       var persona_set = new HashSet<Persona> (null, null);
1260       GLib.List<Persona> added = null;
1261       GLib.List<Persona> removed = null;
1262
1263       /* Determine which Personas have been added */
1264       foreach (var p in persona_list)
1265         {
1266           if (!this._persona_set.contains (p))
1267             {
1268               /* Keep track of how many Personas are users */
1269               if (p.is_user)
1270                 this._persona_user_count++;
1271
1272               added.prepend (p);
1273
1274               this._persona_set.add (p);
1275               this._connect_to_persona (p);
1276
1277               /* Increment the Persona count for this PersonaStore */
1278               var store = p.store;
1279               var num_from_store = this._stores.get (store);
1280               if (num_from_store == 0)
1281                 {
1282                   this._stores.set (store, num_from_store + 1);
1283                 }
1284               else
1285                 {
1286                   this._stores.set (store, 1);
1287
1288                   store.removed.connect (this._store_removed_cb);
1289                   store.personas_changed.connect (
1290                       this._store_personas_changed_cb);
1291                 }
1292             }
1293
1294           persona_set.add (p);
1295         }
1296
1297       /* Determine which Personas have been removed */
1298       foreach (var p in this._persona_list)
1299         {
1300           if (!persona_set.contains (p))
1301             {
1302               /* Keep track of how many Personas are users */
1303               if (p.is_user)
1304                 this._persona_user_count--;
1305
1306               removed.prepend (p);
1307
1308               /* Decrement the Persona count for this PersonaStore */
1309               var store = p.store;
1310               var num_from_store = this._stores.get (store);
1311               if (num_from_store > 1)
1312                 {
1313                   this._stores.set (store, num_from_store - 1);
1314                 }
1315               else
1316                 {
1317                   store.removed.disconnect (this._store_removed_cb);
1318                   store.personas_changed.disconnect (
1319                       this._store_personas_changed_cb);
1320
1321                   this._stores.unset (store);
1322                 }
1323
1324               this._disconnect_from_persona (p);
1325               this._persona_set.remove (p);
1326             }
1327         }
1328
1329       /* Update the Persona list. We just copy the list given to us to save
1330        * repeated insertions/removals and also to ensure we retain the ordering
1331        * of the Personas we were given. */
1332       this._persona_list = persona_list.copy ();
1333
1334       this.personas_changed (added, removed);
1335
1336       /* Update this.is_user */
1337       var new_is_user = (this._persona_user_count > 0) ? true : false;
1338       if (new_is_user != this.is_user)
1339         this.is_user = new_is_user;
1340
1341       /* If all the Personas have been removed, remove the Individual */
1342       if (this._persona_set.size < 1)
1343         {
1344           this.removed (replacement_individual);
1345           return;
1346         }
1347
1348       /* TODO: Base this upon our ID in permanent storage, once we have that. */
1349       if (this.id == null && this._persona_list.data != null)
1350         this.id = this._persona_list.data.uid;
1351
1352       /* Update our aggregated fields and notify the changes */
1353       this._update_fields ();
1354     }
1355
1356   internal void replace (Individual replacement_individual)
1357     {
1358       this._set_personas (null, replacement_individual);
1359     }
1360 }