build fix: Only depend on Gee 0.8.3, since 0.8.4 isn't released yet
[platform/upstream/folks.git] / folks / individual.vala
index 79630e3..7fb026d 100644 (file)
@@ -1,5 +1,6 @@
 /*
  * Copyright (C) 2010 Collabora Ltd.
+ * Copyright (C) 2011 Philip Withnall
  *
  * This library is free software: you can redistribute it and/or modify
  * it under the terms of the GNU Lesser General Public License as published by
@@ -16,6 +17,7 @@
  *
  * Authors:
  *       Travis Reitter <travis.reitter@collabora.co.uk>
+ *       Philip Withnall <philip@tecnocode.co.uk>
  */
 
 using Gee;
@@ -60,7 +62,24 @@ public enum Folks.TrustLevel
 
 /**
  * A physical person, aggregated from the various {@link Persona}s the person
- * might have, such as their different IM addresses or vCard entries.
+ * might have, such as their different IM addresses or vCard entries. An
+ * individual must always contain at least one {@link Persona}.
+ *
+ * When choosing the values of single-valued properties (such as
+ * {@link Individual.alias} and {@link Individual.avatar}; but not multi-valued
+ * properties such as {@link Individual.groups} and
+ * {@link Individual.im_addresses}) from the {@link Persona}s in the
+ * individual to present as the values of those properties of the individual,
+ * it is guaranteed that if the individual contains a persona from the primary
+ * persona store (see {@link IndividualAggregator.primary_store}), its property
+ * values will be chosen above all others. This means that any changes to
+ * property values made through folks (which are normally written to the primary
+ * store) will always be used by {@link Folks.Individual}s.
+ *
+ * No further guarantees are made about the order of preference used for
+ * choosing which property values to use for the {@link Folks.Individual}, other
+ * than that the order may vary between properties, but is guaranteed to be
+ * stable for a given property.
  */
 public class Folks.Individual : Object,
     AliasDetails,
@@ -71,6 +90,7 @@ public class Folks.Individual : Object,
     GenderDetails,
     GroupDetails,
     ImDetails,
+    InteractionDetails,
     LocalIdDetails,
     NameDetails,
     NoteDetails,
@@ -81,24 +101,19 @@ public class Folks.Individual : Object,
     UrlDetails,
     WebServiceDetails
 {
-  private bool _is_favourite;
-  private string _alias;
-  private HashSet<string> _groups;
-  private Set<string> _groups_ro;
   /* Stores the Personas contained in this Individual. */
-  private HashSet<Persona> _persona_set;
+  private HashSet<Persona> _persona_set =
+      new HashSet<Persona> ();
   /* Read-only view of the above set */
   private Set<Persona> _persona_set_ro;
   /* Mapping from PersonaStore -> number of Personas from that store contained
    * in this Individual. There shouldn't be any entries with a number < 1.
    * This is used for working out when to disconnect from store signals. */
-  private HashMap<PersonaStore, uint> _stores;
+  private HashMap<unowned PersonaStore, uint> _stores =
+      new HashMap<unowned PersonaStore, uint> (null, null);
   /* The number of Personas in this Individual which have
    * Persona.is_user == true. Iff this is > 0, Individual.is_user == true. */
   private uint _persona_user_count = 0;
-  private HashMultiMap<string, string> _im_addresses;
-  private HashMultiMap<string, string> _web_service_addresses;
-  private string _nickname = "";
 
   /**
    * The trust level of the Individual.
@@ -113,49 +128,137 @@ public class Folks.Individual : Object,
    */
   public TrustLevel trust_level { get; private set; }
 
+  private LoadableIcon? _avatar = null;
+
   /**
    * {@inheritDoc}
+   *
+   * @since 0.6.0
+   */
+  [CCode (notify = false)]
+  public LoadableIcon? avatar
+    {
+      get { return this._avatar; }
+      set { this.change_avatar.begin (value); } /* not writeable */
+    }
+
+  /*
+   * Change the individual's avatar.
+   *
+   * It's preferred to call this rather than setting {@link Individual.avatar}
+   * directly, as this method gives error notification and will only return once
+   * the avatar has been written to the relevant backing stores (or the
+   * operation's failed).
+   *
+   * Setting this property is only guaranteed to succeed (and be written to
+   * the backing store) if
+   * {@link IndividualAggregator.ensure_individual_property_writeable} has been
+   * called successfully on the individual for the property name ``avatar``.
+   *
+   * @param avatar the new avatar (or ``null`` to unset the avatar)
+   * @throws PropertyError if setting the avatar failed
+   * @since 0.6.3
    */
-  public File avatar { get; private set; }
+  public async void change_avatar (LoadableIcon? avatar) throws PropertyError
+    {
+      /* FIXME: Once https://bugzilla.gnome.org/show_bug.cgi?id=604827 is fixed,
+       * this should be rewritten to use async delegates passed to a generic
+       * _change_single_valued_property() method. */
+      if ((this._avatar != null && ((!) this._avatar).equal (avatar)) ||
+          (this._avatar == null && avatar == null))
+        {
+          return;
+        }
+
+      debug ("Setting avatar of individual '%s' to '%p'…", this.id, avatar);
+
+      PropertyError? persona_error = null;
+      var prop_changed = false;
+
+      /* Try to write it to only the writeable Personas which have the
+       * "avatar" property as writeable. */
+      foreach (var p in this._persona_set)
+        {
+          var _a = p as AvatarDetails;
+          if (_a == null)
+            {
+              continue;
+            }
+          var a = (!) _a;
+
+          if ("avatar" in p.writeable_properties)
+            {
+              try
+                {
+                  yield a.change_avatar (avatar);
+                  debug ("    written to writeable persona '%s'", p.uid);
+                  prop_changed = true;
+                }
+              catch (PropertyError e)
+                {
+                  /* Store the first error so we can throw it if setting the
+                   * avatar fails on every other persona. */
+                  if (persona_error == null)
+                    {
+                      persona_error = e;
+                    }
+                }
+            }
+        }
+
+      /* Failure? Changing the property failed on every suitable persona found
+       * (and potentially zero suitable personas were found). */
+      if (prop_changed == false)
+        {
+          if (persona_error == null)
+            {
+              persona_error = new PropertyError.NOT_WRITEABLE (
+                  _("Failed to change property ‘%s’: No suitable personas were found."),
+                  "avatar");
+            }
+
+          throw persona_error;
+        }
+    }
 
   /**
    * {@inheritDoc}
    */
-  public Folks.PresenceType presence_type { get; private set; }
+  public Folks.PresenceType presence_type { get; set; }
 
   /**
    * {@inheritDoc}
    *
-   * @since 0.5.UNRELEASED
+   * @since 0.6.0
    */
-  public string presence_status { get; private set; }
+  public string presence_status { get; set; }
 
   /**
    * {@inheritDoc}
    */
-  public string presence_message { get; private set; }
+  public string presence_message { get; set; }
 
   /**
    * Whether the Individual is the user.
    *
-   * Iff the Individual represents the user (the person who owns the
-   * account in the backend for each {@link Persona} in the Individual)
-   * this is `true`.
+   * Iff the Individual represents the user – the person who owns the
+   * account in the backend for each {@link Persona} in the Individual –
+   * this is ``true``.
    *
    * It is //not// guaranteed that every {@link Persona} in the Individual has
    * its {@link Persona.is_user} set to the same value as the Individual. For
    * example, the user could own two Telepathy accounts, and have added the
    * other account as a contact in each account. The accounts will expose a
    * {@link Persona} for the user (which will have {@link Persona.is_user} set
-   * to `true`) //and// a {@link Persona} for the contact for the other account
-   * (which will have {@link Persona.is_user} set to `false`).
+   * to ``true``) //and// a {@link Persona} for the contact for the other
+   * account (which will have {@link Persona.is_user} set to ``false``).
    *
-   * It is guaranteed that iff this property is set to `true` on an Individual,
-   * there will be at least one {@link Persona} in the Individual with its
-   * {@link Persona.is_user} set to `true`.
+   * It is guaranteed that iff this property is set to ``true`` on an
+   * Individual, there will be at least one {@link Persona} in the Individual
+   * with its {@link Persona.is_user} set to ``true``.
    *
    * It is guaranteed that there will only ever be one Individual with this
-   * property set to `true`.
+   * property set to ``true``.
    *
    * @since 0.3.0
    */
@@ -187,332 +290,660 @@ public class Folks.Individual : Object,
    * should unreference it and remove it from their UI.
    *
    * @param replacement_individual the individual which has replaced this one
-   * due to linking, or `null` if this individual was removed for another reason
+   * due to linking, or ``null`` if this individual was removed for another
+   * reason
    * @since 0.1.13
    */
   public signal void removed (Individual? replacement_individual);
 
+  private string _alias = "";
+
   /**
    * {@inheritDoc}
    */
+  [CCode (notify = false)]
   public string alias
     {
       get { return this._alias; }
+      set { this.change_alias.begin (value); }
+    }
 
-      set
+  /**
+   * {@inheritDoc}
+   *
+   * @since 0.6.2
+   */
+  public async void change_alias (string alias) throws PropertyError
+    {
+      /* FIXME: Once https://bugzilla.gnome.org/show_bug.cgi?id=604827 is fixed,
+       * this should be rewritten to use async delegates passed to a generic
+       * _change_single_valued_property() method. */
+      if (this._alias == alias)
         {
-          if (this._alias == value)
-            return;
+          return;
+        }
 
-          this._alias = value;
+      debug ("Setting alias of individual '%s' to '%s'…", this.id, alias);
 
-          debug ("Setting alias of individual '%s' to '%s'…", this.id, value);
+      PropertyError? persona_error = null;
+      var prop_changed = false;
 
-          /* First, try to write it to only the writeable Personas… */
-          var alias_changed = false;
-          foreach (var p in this._persona_set)
+      /* Try to write it to only the writeable Personas which have "alias"
+       * as a writeable property. */
+      foreach (var p in this._persona_set)
+        {
+          var _a = p as AliasDetails;
+          if (_a == null)
             {
-              if (p is AliasDetails &&
-                  ((Persona) p).store.is_writeable == true)
-                {
-                  debug ("    written to writeable persona '%s'",
-                      ((Persona) p).uid);
-                  ((AliasDetails) p).alias = value;
-                  alias_changed = true;
-                }
+              continue;
             }
+          var a = (!) _a;
 
-          /* …but if there are no writeable Personas, we have to fall back to
-           * writing it to every Persona. */
-          if (alias_changed == false)
+          if ("alias" in p.writeable_properties)
             {
-              foreach (var p in this._persona_set)
+              try
+                {
+                  yield a.change_alias (alias);
+                  debug ("    written to writeable persona '%s'", p.uid);
+                  prop_changed = true;
+                }
+              catch (PropertyError e)
                 {
-                  if (p is AliasDetails)
+                  /* Store the first error so we can throw it if setting the
+                   * alias fails on every other persona. */
+                  if (persona_error == null)
                     {
-                      debug ("    written to non-writeable persona '%s'",
-                          ((Persona) p).uid);
-                      ((AliasDetails) p).alias = value;
+                      persona_error = e;
                     }
                 }
             }
         }
+
+      /* Failure? Changing the property failed on every suitable persona found
+       * (and potentially zero suitable personas were found). */
+      if (prop_changed == false)
+        {
+          if (persona_error == null)
+            {
+              persona_error = new PropertyError.NOT_WRITEABLE (
+                  _("Failed to change property ‘%s’: No suitable personas were found."),
+                  "alias");
+            }
+
+          throw persona_error;
+        }
     }
 
+  private StructuredName? _structured_name = null;
+
   /**
    * {@inheritDoc}
    */
-  public StructuredName structured_name { get; private set; }
+  [CCode (notify = false)]
+  public StructuredName? structured_name
+    {
+      get { return this._structured_name; }
+      set { this.change_structured_name.begin (value); } /* not writeable */
+    }
+
+  private string _full_name = "";
 
   /**
    * {@inheritDoc}
    */
-  public string full_name { get; private set; }
+  [CCode (notify = false)]
+  public string full_name
+    {
+      get { return this._full_name; }
+      set { this.change_full_name.begin (value); } /* not writeable */
+    }
+
+  private string _nickname = "";
 
   /**
    * {@inheritDoc}
    */
+  [CCode (notify = false)]
   public string nickname
     {
       get { return this._nickname; }
+      set { this.change_nickname.begin (value); }
+    }
+
+  /**
+   * {@inheritDoc}
+   *
+   * @since 0.6.2
+   */
+  public async void change_nickname (string nickname) throws PropertyError
+    {
+      /* FIXME: Once https://bugzilla.gnome.org/show_bug.cgi?id=604827 is fixed,
+       * this should be rewritten to use async delegates passed to a generic
+       * _change_single_valued_property() method. */
 
-      set
+      // Normalise null values to the empty string
+      if (nickname == null)
         {
-          // Normalise null values to the empty string
-          if (value == null)
-            value = "";
+          nickname = "";
+        }
 
-          if (this._nickname == value)
-            return;
+      if (this._nickname == nickname)
+        {
+          return;
+        }
 
-          this._nickname = value;
+      debug ("Setting nickname of individual '%s' to '%s'…", this.id, nickname);
 
-          debug ("Setting nickname of individual '%s' to '%s'…", this.id,
-              value);
+      PropertyError? persona_error = null;
+      var prop_changed = false;
 
-          /* First, try to write it to only the writeable Personas… */
-          var nickname_changed = false;
-          foreach (var p in this._persona_set)
+      /* Try to write it to only the writeable Personas which have "nickname"
+       * as a writeable property. */
+      foreach (var p in this._persona_set)
+        {
+          var _n = p as NameDetails;
+          if (_n == null)
             {
-              if (p is NameDetails &&
-                  ((Persona) p).store.is_writeable == true)
-                {
-                  debug ("    written to writeable persona '%s'",
-                      ((Persona) p).uid);
-                  ((NameDetails) p).nickname = value;
-                  nickname_changed = true;
-                }
+              continue;
             }
+          var n = (!) _n;
 
-          /* …but if there are no writeable Personas, we have to fall back to
-           * writing it to every Persona. */
-          if (nickname_changed == false)
+          if ("nickname" in p.writeable_properties)
             {
-              foreach (var p in this._persona_set)
+              try
                 {
-                  if (p is NameDetails)
+                  yield n.change_nickname (nickname);
+                  debug ("    written to writeable persona '%s'", p.uid);
+                  prop_changed = true;
+                }
+              catch (PropertyError e)
+                {
+                  /* Store the first error so we can throw it if setting the
+                   * nickname fails on every other persona. */
+                  if (persona_error == null)
                     {
-                      debug ("    written to non-writeable persona '%s'",
-                          ((Persona) p).uid);
-                      ((NameDetails) p).nickname = value;
+                      persona_error = e;
                     }
                 }
             }
         }
+
+      /* Failure? Changing the property failed on every suitable persona found
+       * (and potentially zero suitable personas were found). */
+      if (prop_changed == false)
+        {
+          if (persona_error == null)
+            {
+              persona_error = new PropertyError.NOT_WRITEABLE (
+                  _("Failed to change property ‘%s’: No suitable personas were found."),
+                  "nickname");
+            }
+
+          throw persona_error;
+        }
     }
 
-  private Gender _gender;
+  private Gender _gender = Gender.UNSPECIFIED;
   /**
    * {@inheritDoc}
    */
+  [CCode (notify = false)]
   public Gender gender
     {
       get { return this._gender; }
-      private set
-        {
-          this._gender = value;
-          this.notify_property ("gender");
-        }
+      set { this.change_gender.begin (value); } /* not writeable */
     }
 
-  private HashSet<FieldDetails> _urls;
-  private Set<FieldDetails> _urls_ro;
+  private HashSet<UrlFieldDetails>? _urls = null;
+  private Set<UrlFieldDetails>? _urls_ro = null;
 
   /**
    * {@inheritDoc}
    */
-  public Set<FieldDetails> urls
+  [CCode (notify = false)]
+  public Set<UrlFieldDetails> urls
     {
-      get { return this._urls_ro; }
-      private set
+      get
         {
-          this._urls.clear ();
-          foreach (var ps in value)
-            this._urls.add (ps);
+          this._update_urls (true, false, false);
+          return this._urls_ro;
         }
+      set { this.change_urls.begin (value); } /* not writeable */
     }
 
-  private HashSet<FieldDetails> _phone_numbers;
-  private Set<FieldDetails> _phone_numbers_ro;
+  private HashSet<PhoneFieldDetails>? _phone_numbers = null;
+  private Set<PhoneFieldDetails>? _phone_numbers_ro = null;
 
   /**
    * {@inheritDoc}
    */
-  public Set<FieldDetails> phone_numbers
+  [CCode (notify = false)]
+  public Set<PhoneFieldDetails> phone_numbers
     {
-      get { return this._phone_numbers_ro; }
-      private set
+      get
         {
-          this._phone_numbers.clear ();
-          foreach (var fd in value)
-            this._phone_numbers.add (fd);
+          this._update_phone_numbers (true, false, false);
+          return this._phone_numbers_ro;
         }
+      set { this.change_phone_numbers.begin (value); } /* not writeable */
     }
 
-  private HashSet<FieldDetails> _email_addresses;
-  private Set<FieldDetails> _email_addresses_ro;
+  private HashSet<EmailFieldDetails>? _email_addresses = null;
+  private Set<EmailFieldDetails>? _email_addresses_ro = null;
 
   /**
    * {@inheritDoc}
    */
-  public Set<FieldDetails> email_addresses
+  [CCode (notify = false)]
+  public Set<EmailFieldDetails> email_addresses
     {
-      get { return this._email_addresses_ro; }
-      private set
+      get
         {
-          this._email_addresses.clear ();
-          foreach (var fd in value)
-            this._email_addresses.add (fd);
+          this._update_email_addresses (true, false, false);
+          return this._email_addresses_ro;
         }
+      set { this.change_email_addresses.begin (value); } /* not writeable */
     }
 
-  private HashSet<Role> _roles;
-  private Set<Role> _roles_ro;
+  private HashSet<RoleFieldDetails>? _roles = null;
+  private Set<RoleFieldDetails>? _roles_ro = null;
 
   /**
    * {@inheritDoc}
    */
-  public Set<Role> roles
+  [CCode (notify = false)]
+  public Set<RoleFieldDetails> roles
     {
-      get { return this._roles_ro; }
-      private set
+      get
         {
-          this._roles.clear ();
-          foreach (var role in value)
-            this._roles.add (role);
-          this.notify_property ("roles");
+          this._update_roles (true, false, false);
+          return this._roles_ro;
         }
+      set { this.change_roles.begin (value); } /* not writeable */
     }
 
-  private HashSet<string> _local_ids;
-  private Set<string> _local_ids_ro;
+  private HashSet<string>? _local_ids = null;
+  private Set<string>? _local_ids_ro = null;
 
   /**
    * {@inheritDoc}
    */
+  [CCode (notify = false)]
   public Set<string> local_ids
     {
-      get { return this._local_ids_ro; }
-      private set
+      get
         {
-          this._local_ids.clear ();
-          foreach (var id in value)
-            this._local_ids.add (id);
-          this.notify_property ("local-ids");
+          this._update_local_ids (true, false, false);
+          return this._local_ids_ro;
         }
+      set { this.change_local_ids.begin (value); } /* not writeable */
+    }
+
+  private DateTime? _birthday = null;
+
+  /**
+   * {@inheritDoc}
+   */
+  [CCode (notify = false)]
+  public DateTime? birthday
+    {
+      get { return this._birthday; }
+      set { this.change_birthday.begin (value); } /* not writeable */
     }
 
-  public DateTime birthday { get; set; }
+  private string? _calendar_event_id = null;
 
-  public string calendar_event_id { get; set; }
+  /**
+   * {@inheritDoc}
+   */
+  [CCode (notify = false)]
+  public string? calendar_event_id
+    {
+      get { return this._calendar_event_id; }
+      set { this.change_calendar_event_id.begin (value); } /* not writeable */
+    }
 
-  private HashSet<Note> _notes;
-  private Set<Note> _notes_ro;
+  private HashSet<NoteFieldDetails>? _notes = null;
+  private Set<NoteFieldDetails>? _notes_ro = null;
 
   /**
    * {@inheritDoc}
    */
-  public Set<Note> notes
+  [CCode (notify = false)]
+  public Set<NoteFieldDetails> notes
     {
-      get { return this._notes_ro; }
-      private set
+      get
         {
-          this._notes.clear ();
-          foreach (var note in value)
-            this._notes.add (note);
-          this.notify_property ("notes");
+          this._update_notes (true, false, false);
+          return this._notes_ro;
         }
+      set { this.change_notes.begin (value); } /* not writeable */
     }
 
-  private HashSet<PostalAddress> _postal_addresses;
-  private Set<PostalAddress> _postal_addresses_ro;
+  private HashSet<PostalAddressFieldDetails>? _postal_addresses = null;
+  private Set<PostalAddressFieldDetails>? _postal_addresses_ro = null;
 
   /**
    * {@inheritDoc}
    */
-  public Set<PostalAddress> postal_addresses
+  [CCode (notify = false)]
+  public Set<PostalAddressFieldDetails> postal_addresses
     {
-      get { return this._postal_addresses_ro; }
-      private set
+      get
         {
-          this._postal_addresses.clear ();
-          foreach (PostalAddress pa in value)
-            this._postal_addresses.add (pa);
+          this._update_postal_addresses (true, false, false);
+          return this._postal_addresses_ro;
         }
+      set { this.change_postal_addresses.begin (value); } /* not writeable */
     }
 
+  private bool _is_favourite = false;
+
   /**
    * Whether this Individual is a user-defined favourite.
    *
-   * This property is `true` if any of this Individual's {@link Persona}s are
+   * This property is ``true`` if any of this Individual's {@link Persona}s are
    * favourites).
    */
+  [CCode (notify = false)]
   public bool is_favourite
     {
       get { return this._is_favourite; }
+      set { this.change_is_favourite.begin (value); }
+    }
 
-      set
+  /**
+   * {@inheritDoc}
+   *
+   * @since 0.6.2
+   */
+  public async void change_is_favourite (bool is_favourite) throws PropertyError
+    {
+      /* FIXME: Once https://bugzilla.gnome.org/show_bug.cgi?id=604827 is fixed,
+       * this should be rewritten to use async delegates passed to a generic
+       * _change_single_valued_property() method. */
+      if (this._is_favourite == is_favourite)
         {
-          if (this._is_favourite == value)
-            return;
+          return;
+        }
+
+      debug ("Setting '%s' favourite status to %s…", this.id,
+        is_favourite ? "TRUE" : "FALSE");
 
-          debug ("Setting '%s' favourite status to %s", this.id,
-              value ? "TRUE" : "FALSE");
+      PropertyError? persona_error = null;
+      var prop_changed = false;
+
+      /* Try to write it to only the Personas which have "is-favourite" as a
+       * writeable property.
+       *
+       * NOTE: We don't check whether the persona's store is writeable, as we
+       * want is-favourite status to propagate to all stores, if possible. This
+       * is one property which is harmless to propagate. */
+      foreach (var p in this._persona_set)
+        {
+          var _a = p as FavouriteDetails;
+          if (_a == null)
+            {
+              continue;
+            }
+          var a = (!) _a;
 
-          this._is_favourite = value;
-          foreach (var p in this._persona_set)
+          if ("is-favourite" in p.writeable_properties)
             {
-              if (p is FavouriteDetails)
+              try
+                {
+                  yield a.change_is_favourite (is_favourite);
+                  debug ("    written to persona '%s'", p.uid);
+                  prop_changed = true;
+                }
+              catch (PropertyError e)
                 {
-                  SignalHandler.block_by_func (p,
-                      (void*) this._notify_is_favourite_cb, this);
-                  ((FavouriteDetails) p).is_favourite = value;
-                  SignalHandler.unblock_by_func (p,
-                      (void*) this._notify_is_favourite_cb, this);
+                  /* Store the first error so we can throw it if setting the
+                   * property fails on every other persona. */
+                  if (persona_error == null)
+                    {
+                      persona_error = e;
+                    }
                 }
             }
         }
+
+      /* Failure? Changing the property failed on every suitable persona found
+       * (and potentially zero suitable personas were found). */
+      if (prop_changed == false)
+        {
+          if (persona_error == null)
+            {
+              persona_error = new PropertyError.NOT_WRITEABLE (
+                  _("Failed to change property ‘%s’: No suitable personas were found."),
+                  "is-favourite");
+            }
+
+          throw persona_error;
+        }
     }
 
+  private HashSet<string>? _groups = null;
+  private Set<string>? _groups_ro = null;
+
   /**
    * {@inheritDoc}
    */
+  [CCode (notify = false)]
   public Set<string> groups
     {
-      get { return this._groups_ro; }
+      get
+        {
+          this._update_groups (true, false, false);
+          return this._groups_ro;
+        }
+      set { this.change_groups.begin (value); }
+    }
+
+  /**
+   * {@inheritDoc}
+   *
+   * @since 0.6.2
+   */
+  public async void change_groups (Set<string> groups) throws PropertyError
+    {
+      /* FIXME: Once https://bugzilla.gnome.org/show_bug.cgi?id=604827 is fixed,
+       * this should be rewritten to use async delegates passed to a generic
+       * _change_single_valued_property() method. */
+      debug ("Setting '%s' groups…", this.id);
+
+      PropertyError? persona_error = null;
+      var prop_changed = false;
+
+      /* Try to write it to only the Personas which have "groups" as a
+       * writeable property. */
+      foreach (var p in this._persona_set)
+        {
+          var _g = p as GroupDetails;
+          if (_g == null)
+            {
+              continue;
+            }
+          var g = (!) _g;
+
+          if ("groups" in p.writeable_properties)
+            {
+              try
+                {
+                  yield g.change_groups (groups);
+                  debug ("    written to persona '%s'", p.uid);
+                  prop_changed = true;
+                }
+              catch (PropertyError e)
+                {
+                  /* Store the first error so we can throw it if setting the
+                   * property fails on every other persona. */
+                  if (persona_error == null)
+                    {
+                      persona_error = e;
+                    }
+                }
+            }
+        }
+
+      /* Failure? Changing the property failed on every suitable persona found
+       * (and potentially zero suitable personas were found). */
+      if (prop_changed == false)
+        {
+          if (persona_error == null)
+            {
+              persona_error = new PropertyError.NOT_WRITEABLE (
+                  _("Failed to change property ‘%s’: No suitable personas were found."),
+                  "groups");
+            }
+
+          throw persona_error;
+        }
+    }
+
+  private HashMultiMap<string, ImFieldDetails>? _im_addresses = null;
+
+  /**
+   * {@inheritDoc}
+   */
+  [CCode (notify = false)]
+  public MultiMap<string, ImFieldDetails> im_addresses
+    {
+      get
+        {
+          this._update_im_addresses (true, false, false);
+          return this._im_addresses;
+        }
+      set { this.change_im_addresses.begin (value); } /* not writeable */
+    }
+
+  private HashMultiMap<string, WebServiceFieldDetails>? _web_service_addresses =
+      null;
+
+  /**
+   * {@inheritDoc}
+   */
+  [CCode (notify = false)]
+  public MultiMap<string, WebServiceFieldDetails> web_service_addresses
+    {
+      get
+        {
+          this._update_web_service_addresses (true, false, false);
+          return this._web_service_addresses;
+        }
+      /* Not writeable: */
+      set { this.change_web_service_addresses.begin (value); }
+    }
+
+  /**
+   * {@inheritDoc}
+   */
+  public uint im_interaction_count
+    {
+      get
+        {
+          uint counter = 0;
+          /* Iterate over all personas and sum up their IM interaction counts*/
+          foreach (var persona in this._persona_set)
+            {
+              var my_interaction_details = persona as InteractionDetails;
+              if (my_interaction_details != null)
+                {
+                  counter = counter + my_interaction_details.im_interaction_count;
+                }
+            }
+          return counter;
+        }
+    }
+
+  /**
+   * {@inheritDoc}
+   */
+  private DateTime? _last_im_interaction_datetime = null;
 
-      set
+  public DateTime? last_im_interaction_datetime
+    {
+      get
         {
-          foreach (var p in this._persona_set)
+          if (this._last_im_interaction_datetime == null)
             {
-              if (p is GroupDetails && ((Persona) p).store.is_writeable == true)
-                ((GroupDetails) p).groups = value;
+              /* Iterate over all personas and get the latest IM interaction datetime */
+              foreach (var persona in this._persona_set)
+                {
+                  var my_interaction_details = persona as InteractionDetails;
+                  if (my_interaction_details != null &&
+                      my_interaction_details.last_im_interaction_datetime != null)
+                    {
+                      DateTime interaction_datetime = my_interaction_details.last_im_interaction_datetime;
+                      if (this._last_im_interaction_datetime == null ||
+                          interaction_datetime.compare (this._last_im_interaction_datetime) == 1)
+                        {
+                          this._last_im_interaction_datetime = my_interaction_details.last_im_interaction_datetime;
+                        }
+                    }
+                }
             }
-          this._update_groups ();
+          return this._last_im_interaction_datetime;
         }
     }
 
   /**
    * {@inheritDoc}
    */
-  public MultiMap<string, string> im_addresses
+  public uint call_interaction_count
     {
-      get { return this._im_addresses; }
-      private set {}
+      get
+        {
+          uint counter = 0;
+          /* Iterate over all personas and sum up their call interaction counts*/
+          foreach (var persona in this._persona_set)
+            {
+              var my_interaction_details = persona as InteractionDetails;
+              if (my_interaction_details != null)
+                {
+                  counter = counter + my_interaction_details.call_interaction_count;
+                }
+            }
+          return counter;
+        }
     }
 
   /**
    * {@inheritDoc}
    */
-  public MultiMap<string, string> web_service_addresses
+  private DateTime? _last_call_interaction_datetime = null;
+
+  public DateTime? last_call_interaction_datetime
     {
-      get { return this._web_service_addresses; }
-      private set {}
+      get
+        {
+          if (this._last_call_interaction_datetime == null)
+            {
+              /* Iterate over all personas and get the latest IM interaction datetime */
+              foreach (var persona in this._persona_set)
+                {
+                  var my_interaction_details = persona as InteractionDetails;
+                  if (my_interaction_details != null &&
+                      my_interaction_details.last_call_interaction_datetime != null)
+                    {
+                      var interaction_datetime = my_interaction_details.last_call_interaction_datetime;
+                      if (this._last_call_interaction_datetime == null ||
+                          interaction_datetime.compare (this._last_call_interaction_datetime) > 1)
+                        {
+                          this._last_call_interaction_datetime = my_interaction_details.last_call_interaction_datetime;
+                        }
+                    }
+                }
+            }
+          return this._last_call_interaction_datetime;
+        }
     }
 
   /**
    * The set of {@link Persona}s encapsulated by this Individual.
    *
+   * There must always be at least one Persona in this set.
+   *
    * No order is specified over the set of personas, as such an order may be
    * different across each of the properties implemented by the personas (e.g.
    * should they be ordered by presence, name, star sign, etc.?).
@@ -574,7 +1005,7 @@ public class Folks.Individual : Object,
 
   private void _persona_group_changed_cb (string group, bool is_member)
     {
-      this._update_groups ();
+      this._update_groups (false);
     }
 
   private void _notify_gender_cb ()
@@ -584,27 +1015,27 @@ public class Folks.Individual : Object,
 
   private void _notify_urls_cb ()
     {
-      this._update_urls ();
+      this._update_urls (false);
     }
 
   private void _notify_phone_numbers_cb ()
     {
-      this._update_phone_numbers ();
+      this._update_phone_numbers (false);
     }
 
   private void _notify_postal_addresses_cb ()
     {
-      this._update_postal_addresses ();
+      this._update_postal_addresses (false);
     }
 
   private void _notify_email_addresses_cb ()
     {
-      this._update_email_addresses ();
+      this._update_email_addresses (false);
     }
 
   private void _notify_roles_cb ()
     {
-      this._update_roles ();
+      this._update_roles (false);
     }
 
   private void _notify_birthday_cb ()
@@ -614,19 +1045,19 @@ public class Folks.Individual : Object,
 
   private void _notify_notes_cb ()
     {
-      this._update_notes ();
+      this._update_notes (false);
     }
 
   private void _notify_local_ids_cb ()
     {
-      this._update_local_ids ();
+      this._update_local_ids (false);
     }
 
   /**
    * Add or remove the Individual from the specified group.
    *
-   * If `is_member` is `true`, the Individual will be added to the `group`. If
-   * it is `false`, they will be removed from the `group`.
+   * If ``is_member`` is ``true``, the Individual will be added to the
+   * ``group``. If it is ``false``, they will be removed from the ``group``.
    *
    * The group membership change will propagate to every {@link Persona} in
    * the Individual.
@@ -654,12 +1085,12 @@ public class Folks.Individual : Object,
 
   private void _notify_im_addresses_cb (Object obj, ParamSpec ps)
     {
-      this._update_im_addresses ();
+      this._update_im_addresses (false);
     }
 
   private void _notify_web_service_addresses_cb (Object obj, ParamSpec ps)
     {
-      this._update_web_service_addresses ();
+      this._update_web_service_addresses (false);
     }
 
   private void _notify_is_favourite_cb (Object obj, ParamSpec ps)
@@ -667,46 +1098,73 @@ public class Folks.Individual : Object,
       this._update_is_favourite ();
     }
 
+  private void _notify_im_interaction_count_cb (Object obj, ParamSpec ps)
+    {
+      /**
+       * The property is pull rather than push. This function is called in
+       * response to personas emitting a similar notification.
+       */
+      this.notify_property ("im-interaction-count");
+    }
+
+  private void _notify_call_interaction_count_cb (Object obj, ParamSpec ps)
+    {
+      /**
+       * The property is pull rather than push. This function is called in
+       * response to personas emitting a similar notification.
+       */
+      this.notify_property ("call-interaction-count");
+    }
+
+  private void _notify_last_im_interaction_datetime_cb (Object obj, ParamSpec ps)
+    {
+      /**
+       * The property is pull rather than push. This function is called in
+       * response to personas emitting a similar notification.
+       */
+      this._last_im_interaction_datetime = null;
+      this.notify_property ("last-im-interaction-datetime");
+    }
+
+  private void _notify_last_call_interaction_datetime_cb (Object obj, ParamSpec ps)
+    {
+      /**
+       * The property is pull rather than push. This function is called in
+       * response to personas emitting a similar notification.
+       */
+      this._last_call_interaction_datetime = null;
+      this.notify_property ("last-call-interaction-datetime");
+    }
+
   /**
    * Create a new Individual.
    *
    * The Individual can optionally be seeded with the {@link Persona}s in
-   * `personas`. Otherwise, it will have to have personas added using the
+   * ``personas``. Otherwise, it will have to have personas added using the
    * {@link Folks.Individual.personas} property after construction.
    *
    * @param personas a list of {@link Persona}s to initialise the
-   * {@link Individual} with, or `null`
+   * {@link Folks.Individual} with, or ``null``
    * @return a new Individual
    *
    * @since 0.5.1
    */
   public Individual (Set<Persona>? personas)
     {
-      this._im_addresses = new HashMultiMap<string, string> ();
-      this._web_service_addresses = new HashMultiMap<string, string> ();
-      this._persona_set =
-          new HashSet<Persona> (direct_hash, direct_equal);
+      Object (personas: personas);
+
+      debug ("Creating new Individual with %u Personas: %p",
+          this._persona_set.size, this);
+    }
+
+  construct
+    {
       this._persona_set_ro = this._persona_set.read_only_view;
-      this._stores = new HashMap<PersonaStore, uint> (null, null);
-      this._gender = Gender.UNSPECIFIED;
-      this._urls = new HashSet<FieldDetails> ();
-      this._urls_ro = this._urls.read_only_view;
-      this._phone_numbers = new HashSet<FieldDetails> ();
-      this._phone_numbers_ro = this._phone_numbers.read_only_view;
-      this._email_addresses = new HashSet<FieldDetails> ();
-      this._email_addresses_ro = this._email_addresses.read_only_view;
-      this._roles = new HashSet<Role>
-          ((GLib.HashFunc) Role.hash, (GLib.EqualFunc) Role.equal);
-      this._roles_ro = this._roles.read_only_view;
-      this._local_ids = new HashSet<string> ();
-      this._local_ids_ro = this._local_ids.read_only_view;
-      this._postal_addresses = new HashSet<PostalAddress> ();
-      this._postal_addresses_ro = this._postal_addresses.read_only_view;
-      this._notes = new HashSet<Note>
-          ((GLib.HashFunc) Note.hash, (GLib.EqualFunc) Note.equal);
-      this._notes_ro = this._notes.read_only_view;
-
-      this.personas = personas;
+    }
+
+  ~Individual ()
+    {
+      debug ("Destroying Individual '%s': %p", this.id, this);
     }
 
   /* Emit the personas-changed signal, turning null parameters into empty sets
@@ -718,8 +1176,8 @@ public class Folks.Individual : Object,
       var _added = added;
       var _removed = removed;
 
-      if ((added == null || added.size == 0) &&
-          (removed == null || removed.size == 0))
+      if ((added == null || ((!) added).size == 0) &&
+          (removed == null || ((!) removed).size == 0))
         {
           /* Emitting it with no added or removed personas is pointless */
           return;
@@ -733,7 +1191,9 @@ public class Folks.Individual : Object,
           _removed = new HashSet<Persona> ();
         }
 
-      this.personas_changed (_added.read_only_view, _removed.read_only_view);
+      // We've now guaranteed that both _added and _removed are non-null.
+      this.personas_changed (((!) _added).read_only_view,
+          ((!) _removed).read_only_view);
     }
 
   private void _store_removed_cb (PersonaStore store)
@@ -777,42 +1237,277 @@ public class Folks.Individual : Object,
 
   private void _update_fields ()
     {
-      this._update_groups ();
+      this._update_groups (false);
       this._update_presence ();
       this._update_is_favourite ();
       this._update_avatar ();
       this._update_alias ();
       this._update_trust_level ();
-      this._update_im_addresses ();
-      this._update_web_service_addresses ();
+      this._update_im_addresses (false);
+      this._update_web_service_addresses (false);
       this._update_structured_name ();
       this._update_full_name ();
       this._update_nickname ();
       this._update_gender ();
-      this._update_urls ();
-      this._update_phone_numbers ();
-      this._update_email_addresses ();
-      this._update_roles ();
+      this._update_urls (false);
+      this._update_phone_numbers (false);
+      this._update_email_addresses (false);
+      this._update_roles (false);
       this._update_birthday ();
-      this._update_notes ();
-      this._update_postal_addresses ();
-      this._update_local_ids ();
+      this._update_notes (false);
+      this._update_postal_addresses (false);
+      this._update_local_ids (false);
     }
 
-  private void _update_groups ()
-    {
-      var new_groups = new HashSet<string> ();
+  /* Delegate to update the value of a property on this individual from the
+   * given chosen persona. The chosen_persona may be null, in which case we have
+   * to set a default value.
+   *
+   * Used in _update_single_valued_property(), below. */
+  private delegate void SingleValuedPropertySetter (Persona? chosen_persona);
 
-      /* this._groups is null during initial construction */
-      if (this._groups == null)
-        {
-          this._groups = new HashSet<string> ();
-          this._groups_ro = this._groups.read_only_view;
-        }
+  /* Delegate to filter a persona based on whether a given property is set.
+   *
+   * Used in _update_single_valued_property(), below. */
+  private delegate bool PropertyFilter (Persona persona);
 
-      /* FIXME: this should partition the personas by store (maybe we should
-       * keep that mapping in general in this class), and execute
-       * "groups-changed" on the store (with the set of personas), to allow the
+  /*
+   * Update a single-valued property from the values in the personas.
+   *
+   * Single-valued properties are ones such as {@link Individual.alias} or
+   * {@link Individual.gender} — as opposed to multi-valued ones (which are
+   * generally sets) such as {@link Individual.im_addresses} or
+   * {@link Individual.groups}.
+   *
+   * This function uses the given comparison function to order the personas in
+   * this individual, with the highest-positioned persona (the “greatest”
+   * persona in the total order) finally being passed to the setter function to
+   * use in updating the individual's value for the given property. i.e. If
+   * ``compare_func(a, b)`` is called and returns > 0, persona ``a`` will be
+   * passed to the setter.
+   *
+   * At a level above ``compare_func``, the function always prefers personas
+   * from the primary store (see {@link IndividualAggregator.primary_store})
+   * over those which aren't.
+   *
+   * Note that if a suitable persona isn't found in the individual (if, for
+   * example, no personas in the individual implement the desired interface),
+   * ``null`` will be passed to ``setter``, which should then set the
+   * individual's property to a default value.
+   *
+   * @param interface_type the type of interface which all personas under
+   * consideration must implement ({@link Persona} to select all personas)
+   * @param compare_func comparison function to order personas for selection
+   * @param prop_name name of the property being set, as used in
+   * {@link Persona.writeable_properties}
+   * @param setter function to update the individual with the chosen value
+   * @since 0.6.2
+   */
+  private void _update_single_valued_property (Type interface_type,
+      PropertyFilter filter_func,
+      CompareFunc<Persona> compare_func, string prop_name,
+      SingleValuedPropertySetter setter)
+    {
+      CompareDataFunc<Persona> primary_compare_func = (a, b) =>
+        {
+          return_val_if_fail (a != null, 0);
+          return_val_if_fail (b != null, 0);
+
+          /* Always prefer values which are set over those which aren't. */
+          var a_is_set = filter_func (a);
+          var b_is_set = filter_func (b);
+
+          if (a_is_set != b_is_set)
+            {
+              return (a_is_set ? 1 : 0) - (b_is_set ? 1 : 0);
+            }
+
+          var a_is_primary = a.store.is_primary_store;
+          var b_is_primary = b.store.is_primary_store;
+
+          if (a_is_primary != b_is_primary)
+            {
+              return (a_is_primary ? 1 : 0) - (b_is_primary ? 1 : 0);
+            }
+
+          /* If both personas have the same is-primary value, prefer personas
+           * which have the given property as writeable over those which
+           * don't. */
+          var a_is_writeable = (prop_name in a.writeable_properties);
+          var b_is_writeable = (prop_name in b.writeable_properties);
+
+          if (a_is_writeable != b_is_writeable)
+            {
+              return (a_is_writeable ? 1 : 0) - (b_is_writeable ? 1 : 0);
+            }
+
+          /* If both personas have the same writeability for this property, fall
+           * back to the given comparison function. If the comparison function
+           * gives them an equal order, we use the personas' UIDs to ensure that
+           * we end up with a total order over all personas in the individual
+           * (otherwise we might end up with unstable property values). */
+          var order = compare_func (a, b);
+
+          if (order == 0)
+            {
+              order = strcmp (a.uid, b.uid);
+            }
+
+          return order;
+        };
+
+      Persona? candidate_p = null;
+
+      foreach (var p in this._persona_set)
+        {
+          /* We only care about personas implementing the given interface. */
+          if (p.get_type ().is_a (interface_type))
+            {
+              if (candidate_p == null ||
+                  primary_compare_func (p, (!) candidate_p) > 0)
+                {
+                  candidate_p = p;
+                }
+            }
+        }
+
+      /* Update the property with the values from the best candidate persona we
+       * found. Note that it's possible for candidate_p to be null if (e.g.)
+       * none of this._persona_set implemented the interface. */
+      setter (candidate_p);
+    }
+
+  /* Delegate to add the values of a property from all personas to the
+   * collection of values for that property in this individual.
+   *
+   * Used in _update_multi_valued_property(), below. */
+  private delegate bool MultiValuedPropertySetter ();
+
+  /* Delegate to get whether a multi-valued property in this Individual has not
+   * been initialised yet (and is thus still null).
+   *
+   * Used in _update_multi_valued_property(), below. */
+  private delegate bool PropertyIsNull ();
+
+  /* Delegate to create a new empty collection for a multi-valued property in
+   * this Individual and assign it to the property.
+   *
+   * Used in _update_multi_valued_property(), below. */
+  private delegate void CollectionCreator ();
+
+  /*
+   * Update a multi-valued property from the values in the personas.
+   *
+   * Multi-valued properties are ones such as {@link Individual.notes} or
+   * {@link Individual.email_addresses} which have multiple values taken as the
+   * union of the values listed by the personas for those properties.
+   *
+   * This function handles lazy instantiation of the multi-valued property. If
+   * ``create_if_not_exist`` is ``true``, the property is guaranteed to be
+   * created (by ``create_collection``) and set to a non-``null`` value before
+   * this function returns.
+   *
+   * If ``create_if_not_exist`` is ``false``, however, the property may not be
+   * instantiated if it hasn't already been accessed through its property
+   * getter. In this case, a change notification will be emitted for the
+   * property and this function will return immediately.
+   *
+   * If ``force_update`` is ``true``, then existing values get updated (if
+   * the current value is different) or created (according to the
+   * ``create_if_not_exist`` value). Otherwise the function only ensures
+   * that there is a value (if ``create_if_not_exist`` is set) and leaves
+   * existing values unchanged.
+   *
+   * If the property value is to be instantiated, or already has been
+   * instantiated, its value is updated by ``setter`` from the values of the
+   * property in the individual's personas.
+   *
+   * @param prop_name name of the property being set, as used in
+   * {@link Persona.writeable_properties}
+   * @param create_if_not_exist ``true`` to ensure the property is non-null;
+   * ``false`` otherwise
+   * @param prop_is_null function returning ``true`` iff the property is
+   * currently ``null``
+   * @param create_collection function creating a new collection/container for
+   * the property values and assigning it to the property (and updating the
+   * property's read-only view as necessary)
+   * @param setter function which adds the values from the individual's
+   * personas' values for the property to the individual's value for the
+   * property; it returns ``true`` if the property value has changed
+   * @since 0.7.4
+   */
+  private void _update_multi_valued_property (string prop_name,
+      bool create_if_not_exist, PropertyIsNull prop_is_null,
+      CollectionCreator create_collection, MultiValuedPropertySetter setter,
+      bool emit_notification = true,
+      bool force_update = true)
+    {
+      /* If the set of values doesn't exist, and we're not meant to lazily
+       * create it, then simply emit a notification (since the set might've
+       * changed — we can't be sure, but emitting is a safe over-estimate) and
+       * return. */
+      bool created = false;
+      if (prop_is_null ())
+        {
+          /* Notify and return. */
+          if (create_if_not_exist == false)
+            {
+              if (emit_notification)
+                {
+                  this.notify_property (prop_name);
+                }
+              return;
+            }
+
+          /* Lazily instantiate the set of IM addresses. */
+          create_collection ();
+          created = true;
+        }
+
+      /* Re-populate the collection as the union of the values in the
+       * individual's personas. Do this when an empty property was just
+       * created or we were asked to explicitly (usually because the caller
+       * knows that the current value is out-dated).
+       */
+      if ((created || force_update) && setter () == true && emit_notification)
+        {
+          this.notify_property (prop_name);
+        }
+    }
+
+  private void _update_groups (bool create_if_not_exist, bool emit_notification = true, bool force_update = true)
+    {
+      /* If the set of groups doesn't exist, and we're not meant to lazily
+       * create it, then simply emit a notification (since the set might've
+       * changed — we can't be sure, but emitting is a safe over-estimate) and
+       * return. */
+      bool created = false;
+      if (this._groups == null && create_if_not_exist == false)
+        {
+          if (emit_notification)
+            {
+              this.notify_property ("groups");
+            }
+          return;
+        }
+
+      /* Lazily instantiate the set of groups. */
+      else if (this._groups == null)
+        {
+          this._groups = new HashSet<string> ();
+          this._groups_ro = this._groups.read_only_view;
+          created = true;
+        }
+
+      /* Don't touch existing content in get(). */
+      if (!created && !force_update)
+         return;
+
+      var new_groups = new HashSet<string> ();
+
+      /* FIXME: this should partition the personas by store (maybe we should
+       * keep that mapping in general in this class), and execute
+       * "groups-changed" on the store (with the set of personas), to allow the
        * back-end to optimize it (like Telepathy will for MembersChanged for the
        * groups channel list) */
       foreach (var p in this._persona_set)
@@ -830,14 +1525,8 @@ public class Folks.Individual : Object,
 
       foreach (var group in new_groups)
         {
-          if (!this._groups.contains (group))
+          if (this._groups.add (group) && emit_notification)
             {
-              this._groups.add (group);
-              foreach (var g in this._groups)
-                {
-                  debug ("   %s", g);
-                }
-
               this.group_changed (group, true);
             }
         }
@@ -854,180 +1543,159 @@ public class Folks.Individual : Object,
         {
           unowned string group = (string) l;
           this._groups.remove (group);
-          this.group_changed (group, false);
+          if (emit_notification)
+            {
+              this.group_changed (group, false);
+            }
         });
     }
 
   private void _update_presence ()
     {
-      var presence_message = "";
-      var presence_status = "";
-      var presence_type = Folks.PresenceType.UNSET;
+      this._update_single_valued_property (typeof (PresenceDetails), (p) =>
+        {
+          return ((PresenceDetails) p).presence_type != PresenceType.UNSET;
+        }, (a, b) =>
+        {
+          var a_presence = ((PresenceDetails) a).presence_type;
+          var b_presence = ((PresenceDetails) b).presence_type;
 
-      /* Choose the most available presence from our personas */
-      foreach (var p in this._persona_set)
+          return PresenceDetails.typecmp (a_presence, b_presence);
+        }, "presence", (p) =>
         {
-          if (p is PresenceDetails)
-            {
-              unowned PresenceDetails presence = (PresenceDetails) p;
+          var presence_message = ""; /* must not be null */
+          var presence_status = ""; /* must not be null */
+          var presence_type = Folks.PresenceType.UNSET;
 
-              if (PresenceDetails.typecmp (presence.presence_type,
-                  presence_type) > 0)
-                {
-                  presence_type = presence.presence_type;
-                  presence_message = presence.presence_message;
-                  presence_status = presence.presence_status;
-                }
+          if (p != null)
+            {
+              presence_type = ((PresenceDetails) p).presence_type;
+              presence_message = ((PresenceDetails) p).presence_message;
+              presence_status = ((PresenceDetails) p).presence_status;
             }
-        }
 
-      if (presence_message == null)
-        presence_message = "";
-      if (presence_status == null)
-        presence_status = "";
-
-      /* only notify if the value has changed */
-      if (this.presence_message != presence_message)
-        this.presence_message = presence_message;
-
-      if (this.presence_type != presence_type)
-        this.presence_type = presence_type;
-
-      if (this.presence_status != presence_status)
-        this.presence_status = presence_status;
+          /* Only notify if any of the values have changed. */
+          if (this.presence_type != presence_type ||
+              this.presence_message != presence_message ||
+              this.presence_status != presence_status)
+            {
+              this.freeze_notify ();
+              this.presence_message = presence_message;
+              this.presence_type = presence_type;
+              this.presence_status = presence_status;
+              this.thaw_notify ();
+            }
+        });
     }
 
   private void _update_is_favourite ()
     {
-      var favourite = false;
-
-      debug ("Running _update_is_favourite() on '%s'", this.id);
+      this._update_single_valued_property (typeof (FavouriteDetails), (p) =>
+        {
+          return true;
+        }, (a, b) =>
+        {
+          var a_is_favourite = ((FavouriteDetails) a).is_favourite;
+          var b_is_favourite = ((FavouriteDetails) b).is_favourite;
 
-      foreach (var p in this._persona_set)
+          return ((a_is_favourite == true) ? 1 : 0) -
+                 ((b_is_favourite == true) ? 1 : 0);
+        }, "is-favourite", (p) =>
         {
-          if (favourite == false && p is FavouriteDetails)
+          var favourite = false;
+
+          if (p != null)
             {
               favourite = ((FavouriteDetails) p).is_favourite;
-              if (favourite == true)
-                break;
             }
-        }
 
-      /* Only notify if the value has changed. We have to set the private member
-       * and notify manually, or we'd end up propagating the new favourite
-       * status back down to all our Personas. */
-      if (this._is_favourite != favourite)
-        {
-          this._is_favourite = favourite;
-          this.notify_property ("is-favourite");
-        }
+          /* Only notify if the value has changed. We have to set the private
+           * member and notify manually, or we'd end up propagating the new
+           * favourite status back down to all our Personas. */
+          if (this._is_favourite != favourite)
+            {
+              this._is_favourite = favourite;
+              this.notify_property ("is-favourite");
+            }
+        });
     }
 
   private void _update_alias ()
     {
-      string alias = null;
-      var alias_is_display_id = false;
-
-      debug ("Updating alias for individual '%s'", this.id);
-
-      /* Search for an alias from a writeable Persona, and use it as our first
-       * choice if it's non-empty, since that's where the user-set alias is
-       * stored. */
-      foreach (var p in this._persona_set)
+      this._update_single_valued_property (typeof (AliasDetails), (p) =>
         {
-          if (p is AliasDetails && p.store.is_writeable == true)
-            {
-              var a = (AliasDetails) p;
+          var alias = ((AliasDetails) p).alias;
+          return_val_if_fail (alias != null, false);
 
-              if (a.alias != null && a.alias.strip () != "")
-                {
-                  alias = a.alias;
-                  break;
-                }
-            }
-        }
-
-      debug ("    got alias '%s' from writeable personas", alias);
-
-      /* Since we can't find a non-empty alias from a writeable backend, try
-       * the aliases from other personas. Use a non-empty alias which isn't
-       * equal to the persona's display ID as our preference. If we can't find
-       * one of those, fall back to one which is equal to the display ID. */
-      if (alias == null)
+          return (alias.strip () != ""); /* empty aliases are unset */
+        }, (a, b) =>
         {
-          foreach (var p in this._persona_set)
-            {
-              if (p is AliasDetails)
-                {
-                  var a = (AliasDetails) p;
+          var a_alias = ((AliasDetails) a).alias;
+          var b_alias = ((AliasDetails) b).alias;
 
-                  if (a.alias == null || a.alias.strip () == "")
-                    continue;
+          return_val_if_fail (a_alias != null, 0);
+          return_val_if_fail (b_alias != null, 0);
 
-                  if (alias == null || alias_is_display_id == true)
-                    {
-                      /* We prefer to not have an alias which is the same as the
-                       * Persona's display-id, since having such an alias
-                       * implies that it's the default. However, we prefer using
-                       * such an alias to using the Persona's UID, which is our
-                       * ultimate fallback (below). */
-                      alias = a.alias;
-
-                      if (a.alias == p.display_id)
-                        alias_is_display_id = true;
-                      else if (alias != null)
-                        break;
-                    }
-                }
-            }
-        }
+          var a_is_empty = (a_alias.strip () == "") ? 1 : 0;
+          var b_is_empty = (b_alias.strip () == "") ? 1 : 0;
 
-      debug ("    got alias '%s' from non-writeable personas", alias);
+          /* We prefer to not have an alias which is the same as the Persona's
+           * display-id, since having such an alias implies that it's the
+           * default. However, we prefer using such an alias to using the
+           * Persona's UID, which is our ultimate fallback (below). */
+          var a_is_display_id = (a_alias == a.display_id) ? 1 : 0;
+          var b_is_display_id = (b_alias == b.display_id) ? 1 : 0;
 
-      if (alias == null)
+          return (b_is_empty + b_is_display_id) -
+                 (a_is_empty + a_is_display_id);
+        }, "alias", (p) =>
         {
-          /* We have to pick a display ID, since none of the personas have an
-           * alias available. Pick the display ID from the first persona in the
-           * list. */
-          foreach (var persona in this._persona_set)
+          string alias = ""; /* must not be null */
+
+          if (p != null)
             {
-              alias = persona.display_id;
-              debug ("No aliases available for individual; using display ID " +
-                  "instead: %s", alias);
-              break;
+              alias = ((AliasDetails) p).alias.strip ();
             }
-        }
 
-      /* Only notify if the value has changed. We have to set the private member
-       * and notify manually, or we'd end up propagating the new alias back
-       * down to all our Personas, even if it's a fallback display ID or
-       * something else undesirable. */
-      if (this._alias != alias)
-        {
-          debug ("Changing alias of individual '%s' from '%s' to '%s'.",
-              this.id, this._alias, alias);
-          this._alias = alias;
-          this.notify_property ("alias");
-        }
+          /* Only notify if the value has changed. We have to set the private
+           * member and notify manually, or we'd end up propagating the new
+           * alias back down to all our Personas, even if it's a fallback
+           * display ID or something else undesirable. */
+          if (this._alias != alias)
+            {
+              this._alias = alias;
+              this.notify_property ("alias");
+            }
+        });
     }
 
   private void _update_avatar ()
     {
-      File avatar = null;
-
-      foreach (var p in this._persona_set)
+      this._update_single_valued_property (typeof (AvatarDetails), (p) =>
+        {
+          return ((AvatarDetails) p).avatar != null;
+        }, (a, b) =>
         {
-          if (p is AvatarDetails)
+          /* We can't compare two set avatars efficiently. See: bgo#652721. */
+          return 0;
+        }, "avatar", (p) =>
+        {
+          LoadableIcon? avatar = null;
+
+          if (p != null)
             {
               avatar = ((AvatarDetails) p).avatar;
-              if (avatar != null)
-                break;
             }
-        }
 
-      /* only notify if the value has changed */
-      if (this.avatar != avatar)
-        this.avatar = avatar;
+          /* only notify if the value has changed */
+          if ((this._avatar == null && avatar != null) ||
+              (this._avatar != null &&
+               (avatar == null || !((!) this._avatar).equal (avatar))))
+            {
+              this._avatar = avatar;
+              this.notify_property ("avatar");
+            }
+        });
     }
 
   private void _update_trust_level ()
@@ -1046,61 +1714,115 @@ public class Folks.Individual : Object,
         this.trust_level = trust_level;
     }
 
-  private void _update_im_addresses ()
+  private void _update_im_addresses (bool create_if_not_exist, bool emit_notification = true, bool force_update = true)
     {
-      /* populate the IM addresses as the union of our Personas' addresses */
-      this._im_addresses.clear ();
-
-      foreach (var persona in this._persona_set)
-        {
-          if (persona is ImDetails)
+      this._update_multi_valued_property ("im-addresses",
+          create_if_not_exist, () => { return this._im_addresses == null; },
+          () =>
             {
-              var im_details = (ImDetails) persona;
-              foreach (var cur_protocol in im_details.im_addresses.get_keys ())
-                {
-                  var cur_addresses =
-                      im_details.im_addresses.get (cur_protocol);
+              this._im_addresses = new HashMultiMap<string, ImFieldDetails> (
+                  null, null,
+                  (Gee.HashDataFunc) AbstractFieldDetails<string>.hash_static,
+                  (Gee.EqualDataFunc) AbstractFieldDetails<string>.equal_static);
+            },
+          () =>
+            {
+              var new_im_addresses = new HashMultiMap<string, ImFieldDetails> (
+                  null, null,
+                  (Gee.HashDataFunc) AbstractFieldDetails<string>.hash_static,
+                  (Gee.EqualDataFunc) AbstractFieldDetails<string>.equal_static);
 
-                  foreach (var address in cur_addresses)
+              foreach (var persona in this._persona_set)
+                {
+                  /* We only care about personas implementing the given interface. */
+                  var im_details = persona as ImDetails;
+                  if (im_details != null)
                     {
-                      this._im_addresses.set (cur_protocol, address);
+                      foreach (var cur_protocol in
+                          im_details.im_addresses.get_keys ())
+                        {
+                          var cur_addresses =
+                              im_details.im_addresses.get (cur_protocol);
+
+                          foreach (var address in cur_addresses)
+                            {
+                              new_im_addresses.set (cur_protocol, address);
+                            }
+                        }
                     }
                 }
-            }
-        }
-      this.notify_property ("im-addresses");
+
+              if (!Utils.multi_map_str_afd_equal (new_im_addresses,
+                  this._im_addresses))
+                {
+                  this._im_addresses = new_im_addresses;
+                  return true;
+                }
+
+              return false;
+            }, emit_notification, force_update);
     }
 
-  private void _update_web_service_addresses ()
+  private void _update_web_service_addresses (bool create_if_not_exist, bool emit_notification = true, bool force_update = true)
     {
-      /* populate the web service addresses as the union of our Personas' addresses */
-      this._web_service_addresses.clear ();
-
-      foreach (var persona in this.personas)
-        {
-          if (persona is WebServiceDetails)
+      this._update_multi_valued_property ("web-service-addresses",
+          create_if_not_exist,
+          () => { return this._web_service_addresses == null; },
+          () =>
             {
-              var web_service_details = (WebServiceDetails) persona;
-              foreach (var cur_web_service in
-                  web_service_details.web_service_addresses.get_keys ())
+              this._web_service_addresses =
+                  new HashMultiMap<string, WebServiceFieldDetails> (null, null,
+                      (Gee.HashDataFunc)
+                          AbstractFieldDetails<string>.hash_static,
+                      (Gee.EqualDataFunc)
+                          AbstractFieldDetails<string>.equal_static);
+            },
+          () =>
+            {
+              var new_web_service_addresses =
+                  new HashMultiMap<string, WebServiceFieldDetails> (null, null,
+                      (Gee.HashDataFunc)
+                          AbstractFieldDetails<string>.hash_static,
+                      (Gee.EqualDataFunc)
+                          AbstractFieldDetails<string>.equal_static);
+
+              foreach (var persona in this._persona_set)
                 {
-                  var cur_addresses =
-                      web_service_details.web_service_addresses.get (
-                          cur_web_service);
-
-                  foreach (var address in cur_addresses)
+                  /* We only care about personas implementing the given interface. */
+                  var web_service_details = persona as WebServiceDetails;
+                  if (web_service_details != null)
                     {
-                      this._web_service_addresses.set (cur_web_service,
-                          address);
+                      foreach (var cur_web_service in
+                          web_service_details.web_service_addresses.get_keys ())
+                        {
+                          var cur_addresses =
+                              web_service_details.web_service_addresses.get (
+                                  cur_web_service);
+
+                          foreach (var ws_fd in cur_addresses)
+                            {
+                              new_web_service_addresses.set (cur_web_service,
+                                  ws_fd);
+                            }
+                        }
                     }
                 }
-            }
-        }
-      this.notify_property ("web-service-addresses");
+
+              if (!Utils.multi_map_str_afd_equal (new_web_service_addresses,
+                  this._web_service_addresses))
+                {
+                  this._web_service_addresses = new_web_service_addresses;
+                  return true;
+                }
+
+              return false;
+            }, emit_notification, force_update);
     }
 
   private void _connect_to_persona (Persona persona)
     {
+      persona.individual = this;
+
       persona.notify["alias"].connect (this._notify_alias_cb);
       persona.notify["avatar"].connect (this._notify_avatar_cb);
       persona.notify["presence-message"].connect (this._notify_presence_cb);
@@ -1132,83 +1854,111 @@ public class Folks.Individual : Object,
           ((GroupDetails) persona).group_changed.connect (
               this._persona_group_changed_cb);
         }
+      /* Subscribe to the interactions signal for the persona */
+      var p_interaction_details = persona as InteractionDetails;
+      if (p_interaction_details != null)
+        {
+          persona.notify["im-interaction-count"].connect (this._notify_im_interaction_count_cb);
+          persona.notify["call-interaction-count"].connect (this._notify_call_interaction_count_cb);
+          persona.notify["last-im-interaction-datetime"].connect (this._notify_last_im_interaction_datetime_cb);
+          persona.notify["last-call-interaction-datetime"].connect (this._notify_last_call_interaction_datetime_cb);
+        }
     }
 
   private void _update_structured_name ()
     {
-      bool name_found = false;
-
-      foreach (var persona in this._persona_set)
+      this._update_single_valued_property (typeof (NameDetails), (p) =>
+        {
+          var name = ((NameDetails) p).structured_name;
+          return (name != null && !((!) name).is_empty ());
+        }, (a, b) =>
         {
-          var name_details = persona as NameDetails;
-          if (name_details != null)
+          /* Can't compare two set names. */
+          return 0;
+        }, "structured-name", (p) =>
+        {
+          StructuredName? name = null;
+
+          if (p != null)
             {
-              var new_value = name_details.structured_name;
-              if (new_value != null && !new_value.is_empty ())
+              name = ((NameDetails) p).structured_name;
+
+              if (name != null && ((!) name).is_empty ())
                 {
-                  name_found = true;
-                  if (this.structured_name == null ||
-                      !this.structured_name.equal (new_value))
-                    {
-                      this.structured_name = new_value;
-                      return;
-                    }
+                  name = null;
                 }
             }
-        }
 
-      if (name_found == false)
-        this.structured_name = null;
+          if ((this._structured_name == null && name != null) ||
+              (this._structured_name != null &&
+               (name == null || !((!) this._structured_name).equal ((!) name))))
+            {
+              this._structured_name = name;
+              this.notify_property ("structured-name");
+            }
+        });
     }
 
   private void _update_full_name ()
     {
-      string? new_full_name = null;
+      this._update_single_valued_property (typeof (NameDetails), (p) =>
+        {
+          var name = ((NameDetails) p).full_name;
+          return_val_if_fail (name != null, false);
 
-      foreach (var persona in this._persona_set)
+          return (name.strip () != ""); /* empty names are unset */
+        }, (a, b) =>
+        {
+          /* Can't compare two set names. */
+          return 0;
+        }, "full-name", (p) =>
         {
-          var name_details = persona as NameDetails;
-          if (name_details != null)
+          string new_full_name = ""; /* must not be null */
+
+          if (p != null)
             {
-              var new_value = name_details.full_name;
-              if (new_value != null && new_value != "")
-                {
-                  new_full_name = new_value;
-                  break;
-                }
+              new_full_name = ((NameDetails) p).full_name.strip ();
             }
-        }
 
-      if (new_full_name != this.full_name)
-        this.full_name = new_full_name;
+          if (new_full_name != this._full_name)
+            {
+              this._full_name = new_full_name;
+              this.notify_property ("full-name");
+            }
+        });
     }
 
   private void _update_nickname ()
     {
-      string new_nickname = "";
+      this._update_single_valued_property (typeof (NameDetails), (p) =>
+        {
+          var nickname = ((NameDetails) p).nickname;
+          return_val_if_fail (nickname != null, false);
 
-      foreach (var persona in this._persona_set)
+          return (nickname.strip () != ""); /* empty names are unset */
+        }, (a, b) =>
+        {
+          /* Can't compare two set names. */
+          return 0;
+        }, "nickname", (p) =>
         {
-          var name_details = persona as NameDetails;
-          if (name_details != null)
+          string new_nickname = ""; /* must not be null */
+
+          if (p != null)
             {
-              var new_value = name_details.nickname;
-              if (new_value != null && new_value != "")
-                {
-                  new_nickname = new_value;
-                  break;
-                }
+              new_nickname = ((NameDetails) p).nickname.strip ();
             }
-        }
 
-      if (new_nickname != this._nickname)
-        {
-          this._nickname = new_nickname;
-          this.notify_property ("nickname");
-        }
+          if (new_nickname != this._nickname)
+            {
+              this._nickname = new_nickname;
+              this.notify_property ("nickname");
+            }
+        });
     }
 
-  private void _disconnect_from_persona (Persona persona)
+  private void _disconnect_from_persona (Persona persona,
+      Individual? replacement_individual)
     {
       persona.notify["alias"].disconnect (this._notify_alias_cb);
       persona.notify["avatar"].disconnect (this._notify_avatar_cb);
@@ -1244,251 +1994,463 @@ public class Folks.Individual : Object,
           ((GroupDetails) persona).group_changed.disconnect (
               this._persona_group_changed_cb);
         }
-    }
 
-  private void _update_gender ()
-    {
-      Gender new_gender = Gender.UNSPECIFIED;
+      /* Unsubscribe from the interactions signal for the persona */
+      var p_interaction_details = persona as InteractionDetails;
+      if (p_interaction_details != null)
+        {
+          persona.notify["im-interaction-count"].disconnect (this._notify_im_interaction_count_cb);
+          persona.notify["call-interaction-count"].disconnect (this._notify_call_interaction_count_cb);
+          persona.notify["last-im-interaction-datetime"].disconnect (this._notify_last_im_interaction_datetime_cb);
+          persona.notify["last-call-interaction-datetime"].disconnect (this._notify_last_call_interaction_datetime_cb);
+        }
 
-      foreach (var persona in this._persona_set)
+      /* Don't update the individual if the persona's been added to the new one
+       * already (and thus the new individual has already changed
+       * persona.individual).
+       *
+       * FIXME: Ideally, we'd assert that a persona can't be added to a new
+       * individual before it's removed from the old one. However, this
+       * currently isn't possible due to the way the aggregator works. When the
+       * aggregator's rewritten, it would be nice to fix this. */
+      if (persona.individual == this)
         {
-          var gender_details = persona as GenderDetails;
-          if (gender_details != null)
+          /* It may be the case that the persona's being removed from the
+           * individual (i.e. the replacement individual is non-null, but
+           * doesn't contain this persona). In this case, we need to set the
+           * persona's individual to null. */
+          if (replacement_individual != null &&
+              persona in ((!) replacement_individual).personas)
             {
-              var new_value = gender_details.gender;
-              if (new_value != Gender.UNSPECIFIED)
-                {
-                  new_gender = new_value;
-                  break;
-                }
+              persona.individual = replacement_individual;
+            }
+          else
+            {
+              persona.individual = null;
             }
         }
-
-      if (new_gender != this.gender)
-        this.gender = new_gender;
     }
 
-  private void _update_urls ()
+  private void _update_gender ()
     {
-      /* Populate the URLs as the union of our Personas' URLs.
-       * If the same URL exists multiple times we merge the parameters. */
-      var urls_set = new HashMap<unowned string, unowned FieldDetails> ();
+      this._update_single_valued_property (typeof (GenderDetails), (p) =>
+        {
+          return ((GenderDetails) p).gender != Gender.UNSPECIFIED;
+        }, (a, b) =>
+        {
+          /* It would be sexist to rank one gender over another.
+           * Besides, how often will we see two personas in the same individual
+           * which have different genders? */
+          return 0;
+        }, "gender", (p) =>
+        {
+          var new_gender = Gender.UNSPECIFIED;
 
-      this._urls.clear ();
+          if (p != null)
+            {
+              new_gender = ((GenderDetails) p).gender;
+            }
 
-      foreach (var persona in this._persona_set)
-        {
-          var url_details = persona as UrlDetails;
-          if (url_details != null)
+          if (new_gender != this.gender)
             {
-              foreach (var ps in url_details.urls)
-                {
-                  if (ps.value == null)
-                    continue;
+              this._gender = new_gender;
+              this.notify_property ("gender");
+            }
+        });
+    }
 
-                  var existing = urls_set.get (ps.value);
-                  if (existing != null)
-                    existing.extend_parameters (ps.parameters);
-                  else
+  private void _update_urls (bool create_if_not_exist, bool emit_notification = true, bool force_update = true)
+    {
+      this._update_multi_valued_property ("urls", create_if_not_exist,
+          () => { return this._urls == null; },
+          () =>
+            {
+              this._urls = new HashSet<UrlFieldDetails> (
+                  AbstractFieldDetails<string>.hash_static,
+                  AbstractFieldDetails<string>.equal_static);
+              this._urls_ro = this._urls.read_only_view;
+            },
+          () =>
+            {
+              var new_urls = new HashSet<UrlFieldDetails> (
+                  AbstractFieldDetails<string>.hash_static,
+                  AbstractFieldDetails<string>.equal_static);
+              var urls_set = new HashMap<unowned string,
+                  unowned UrlFieldDetails> (
+                    null, null,  AbstractFieldDetails<string>.equal_static);
+
+              foreach (var persona in this._persona_set)
+                {
+                  /* We only care about personas implementing the given
+                   * interface. If the same URL exists multiple times we merge
+                   * the parameters. */
+                  var url_details = persona as UrlDetails;
+                  if (url_details != null)
                     {
-                      var new_ps = new FieldDetails (ps.value);
-                      new_ps.extend_parameters (ps.parameters);
-                      urls_set.set (ps.value, new_ps);
-                      this._urls.add (new_ps);
+                      foreach (var url_fd in ((!) url_details).urls)
+                        {
+                          var existing = urls_set.get (url_fd.value);
+                          if (existing != null)
+                            {
+                              existing.extend_parameters (url_fd.parameters);
+                            }
+                          else
+                            {
+                              var new_url_fd =
+                                  new UrlFieldDetails (url_fd.value);
+                              new_url_fd.extend_parameters (url_fd.parameters);
+                              urls_set.set (new_url_fd.value, new_url_fd);
+                              new_urls.add (new_url_fd);
+                            }
+                        }
                     }
                 }
-            }
-        }
 
-      this.notify_property ("urls");
+              if (!Utils.set_afd_equal (new_urls, this._urls))
+                {
+                  this._urls = new_urls;
+                  this._urls_ro = new_urls.read_only_view;
+                  return true;
+                }
+
+              return false;
+            }, emit_notification, force_update);
     }
 
-  private void _update_phone_numbers ()
+  private void _update_phone_numbers (bool create_if_not_exist, bool emit_notification = true, bool force_update = true)
     {
-      /* Populate the phone numbers as the union of our Personas' numbers
-       * If the same number exists multiple times we merge the parameters. */
-      /* FIXME: We should handle phone numbers better, just string comparison
-         doesn't work. */
-      var phone_numbers_set =
-          new HashMap<unowned string, unowned FieldDetails> ();
-
-      this._phone_numbers.clear ();
-
-      foreach (var persona in this._persona_set)
-        {
-          var phone_details = persona as PhoneDetails;
-          if (phone_details != null)
+      this._update_multi_valued_property ("phone-numbers", create_if_not_exist,
+          () => { return this._phone_numbers == null; },
+          () =>
             {
-              foreach (var fd in phone_details.phone_numbers)
-                {
-                  if (fd.value == null)
-                    continue;
+              this._phone_numbers = new HashSet<PhoneFieldDetails> (
+                  AbstractFieldDetails<string>.hash_static,
+                  AbstractFieldDetails<string>.equal_static);
+              this._phone_numbers_ro = this._phone_numbers.read_only_view;
+            },
+          () =>
+            {
+              var new_phone_numbers = new HashSet<PhoneFieldDetails> (
+                  AbstractFieldDetails<string>.hash_static,
+                  AbstractFieldDetails<string>.equal_static);
+              var phone_numbers_set = new HashMap<string, PhoneFieldDetails> (
+                  null, null, AbstractFieldDetails<string>.equal_static);
 
-                  var existing = phone_numbers_set.get (fd.value);
-                  if (existing != null)
-                    existing.extend_parameters (fd.parameters);
-                  else
+              foreach (var persona in this._persona_set)
+                {
+                  /* We only care about personas implementing the given
+                   * interface. If the same phone number exists multiple times
+                   * we merge the parameters. */
+                  var phone_details = persona as PhoneDetails;
+                  if (phone_details != null)
                     {
-                      var new_fd = new FieldDetails (fd.value);
-                      new_fd.extend_parameters (fd.parameters);
-                      phone_numbers_set.set (fd.value, new_fd);
-                      this._phone_numbers.add (new_fd);
+                      foreach (var phone_fd in ((!) phone_details).phone_numbers)
+                        {
+                          var existing = phone_numbers_set.get (phone_fd.value);
+                          if (existing != null)
+                            {
+                              existing.extend_parameters (phone_fd.parameters);
+                            }
+                          else
+                            {
+                              var new_fd =
+                                  new PhoneFieldDetails (phone_fd.value);
+                              new_fd.extend_parameters (phone_fd.parameters);
+                              phone_numbers_set.set (new_fd.value, new_fd);
+                              new_phone_numbers.add (new_fd);
+                            }
+                        }
                     }
                 }
-            }
-        }
 
-      this.notify_property ("phone-numbers");
+              if (!Utils.set_afd_equal (new_phone_numbers, this._phone_numbers))
+                {
+                  this._phone_numbers = new_phone_numbers;
+                  this._phone_numbers_ro = new_phone_numbers.read_only_view;
+                  return true;
+                }
+
+              return false;
+            }, emit_notification, force_update);
     }
 
-  private void _update_email_addresses ()
+  private void _update_email_addresses (bool create_if_not_exist, bool emit_notification = true, bool force_update = true)
     {
-      /* Populate the email addresses as the union of our Personas' addresses.
-       * If the same address exists multiple times we merge the parameters. */
-      var emails_set = new HashMap<unowned string, unowned FieldDetails> ();
-
-      this._email_addresses.clear ();
-
-      foreach (var persona in this._persona_set)
-        {
-          var email_details = persona as EmailDetails;
-          if (email_details != null)
+      this._update_multi_valued_property ("email-addresses",
+          create_if_not_exist, () => { return this._email_addresses == null; },
+          () =>
             {
-              foreach (var fd in email_details.email_addresses)
-                {
-                  if (fd.value == null)
-                    continue;
+              this._email_addresses = new HashSet<EmailFieldDetails> (
+                  AbstractFieldDetails<string>.hash_static,
+                  AbstractFieldDetails<string>.equal_static);
+              this._email_addresses_ro = this._email_addresses.read_only_view;
+            },
+          () =>
+            {
+              var new_email_addresses = new HashSet<EmailFieldDetails> (
+                  AbstractFieldDetails<string>.hash_static,
+                  AbstractFieldDetails<string>.equal_static);
+              var emails_set = new HashMap<string, EmailFieldDetails> (
+                  null, null, AbstractFieldDetails<string>.equal_static);
 
-                  var existing = emails_set.get (fd.value);
-                  if (existing != null)
-                    existing.extend_parameters (fd.parameters);
-                  else
+              foreach (var persona in this._persona_set)
+                {
+                  /* We only care about personas implementing the given
+                   * interface. If the same e-mail address exists multiple times
+                   * we merge the parameters. */
+                  var email_details = persona as EmailDetails;
+                  if (email_details != null)
                     {
-                      var new_fd = new FieldDetails (fd.value);
-                      new_fd.extend_parameters (fd.parameters);
-                      emails_set.set (fd.value, new_fd);
-                      this._email_addresses.add (new_fd);
+                      foreach (var email_fd in ((!) email_details).email_addresses)
+                        {
+                          var existing = emails_set.get (email_fd.value);
+                          if (existing != null)
+                            {
+                              existing.extend_parameters (email_fd.parameters);
+                            }
+                          else
+                            {
+                              var new_email_fd =
+                                  new EmailFieldDetails (email_fd.value,
+                                      email_fd.parameters);
+                              emails_set.set (new_email_fd.value, new_email_fd);
+                              new_email_addresses.add (new_email_fd);
+                            }
+                        }
                     }
                 }
-            }
-        }
 
-      this.notify_property ("email-addresses");
+              if (!Utils.set_afd_equal (new_email_addresses,
+                  this._email_addresses))
+                {
+                  this._email_addresses = new_email_addresses;
+                  this._email_addresses_ro = new_email_addresses.read_only_view;
+                  return true;
+                }
+
+              return false;
+            }, emit_notification, force_update);
     }
 
-  private void _update_roles ()
+  private void _update_roles (bool create_if_not_exist, bool emit_notification = true, bool force_update = true)
     {
-      this._roles.clear ();
-
-      foreach (var persona in this._persona_set)
-        {
-          var role_details = persona as RoleDetails;
-          if (role_details != null)
+      this._update_multi_valued_property ("roles", create_if_not_exist,
+          () => { return this._roles == null; },
+          () =>
+            {
+              this._roles = new HashSet<RoleFieldDetails> (
+                  AbstractFieldDetails<Role>.hash_static,
+                  AbstractFieldDetails<Role>.equal_static);
+              this._roles_ro = this._roles.read_only_view;
+            },
+          () =>
             {
-              foreach (var r in role_details.roles)
+              var new_roles = new HashSet<RoleFieldDetails> (
+                  AbstractFieldDetails<Role>.hash_static,
+                  AbstractFieldDetails<Role>.equal_static);
+
+              foreach (var persona in this._persona_set)
                 {
-                  this._roles.add (r);
+                  var role_details = persona as RoleDetails;
+                  if (role_details != null)
+                    {
+                      foreach (var role_fd in ((!) role_details).roles)
+                        {
+                          new_roles.add (role_fd);
+                        }
+                    }
                 }
-            }
-        }
 
-      this.notify_property ("roles");
+              if (!Utils.set_afd_equal (new_roles, this._roles))
+                {
+                  this._roles = new_roles;
+                  this._roles_ro = new_roles.read_only_view;
+                  return true;
+                }
+
+              return false;
+            }, emit_notification, force_update);
     }
 
-  private void _update_local_ids ()
+  private void _update_local_ids (bool create_if_not_exist, bool emit_notification = true, bool force_update = true)
     {
-      this._local_ids.clear ();
-
-      foreach (var persona in this._persona_set)
-        {
-          var local_ids_details = persona as LocalIdDetails;
-          if (local_ids_details != null)
+      this._update_multi_valued_property ("local-ids", create_if_not_exist,
+          () => { return this._local_ids == null; },
+          () =>
             {
-              foreach (var id in local_ids_details.local_ids)
+              this._local_ids = new HashSet<string> ();
+              this._local_ids_ro = this._local_ids.read_only_view;
+            },
+          () =>
+            {
+              var new_local_ids = new HashSet<string> ();
+
+              foreach (var persona in this._persona_set)
                 {
-                  this._local_ids.add (id);
+                  var local_id_details = persona as LocalIdDetails;
+                  if (local_id_details != null)
+                    {
+                      foreach (var id in ((!) local_id_details).local_ids)
+                        {
+                          new_local_ids.add (id);
+                        }
+                    }
                 }
-            }
-        }
 
-      this.notify_property ("local-ids");
+              if (new_local_ids.size != this._local_ids.size ||
+                  !new_local_ids.contains_all (this._local_ids))
+                {
+                  this._local_ids = new_local_ids;
+                  this._local_ids_ro = new_local_ids.read_only_view;
+                  return true;
+                }
+
+              return false;
+            }, emit_notification, force_update);
     }
 
-  private void _update_postal_addresses ()
+  private void _update_postal_addresses (bool create_if_not_exist, bool emit_notification = true, bool force_update = true)
     {
-      this._postal_addresses.clear ();
-
       /* FIXME: Detect duplicates somehow? */
-      foreach (var persona in this._persona_set)
-        {
-          var address_details = persona as PostalAddressDetails;
-          if (address_details != null)
+      this._update_multi_valued_property ("postal-addresses",
+          create_if_not_exist, () => { return this._postal_addresses == null; },
+          () =>
             {
-              foreach (var pa in address_details.postal_addresses)
-                this._postal_addresses.add (pa);
-            }
-        }
+              this._postal_addresses = new HashSet<PostalAddressFieldDetails> (
+                  AbstractFieldDetails<PostalAddress>.hash_static,
+                  AbstractFieldDetails<PostalAddress>.equal_static);
+              this._postal_addresses_ro = this._postal_addresses.read_only_view;
+            },
+          () =>
+            {
+              var new_postal_addresses =
+                  new HashSet<PostalAddressFieldDetails> (
+                      AbstractFieldDetails<PostalAddress>.hash_static,
+                      AbstractFieldDetails<PostalAddress>.equal_static);
+
+              foreach (var persona in this._persona_set)
+                {
+                  var postal_address_details = persona as PostalAddressDetails;
+                  if (postal_address_details != null)
+                    {
+                      foreach (var pafd in
+                          ((!) postal_address_details).postal_addresses)
+                        {
+                          new_postal_addresses.add (pafd);
+                        }
+                    }
+                }
+
+              if (!Utils.set_afd_equal (new_postal_addresses,
+                  this._postal_addresses))
+                {
+                  this._postal_addresses = new_postal_addresses;
+                  this._postal_addresses_ro =
+                      new_postal_addresses.read_only_view;
+                  return true;
+                }
 
-      this.notify_property ("postal-addresses");
+              return false;
+            }, emit_notification, force_update);
     }
 
   private void _update_birthday ()
     {
-      unowned DateTime bday = null;
-      unowned string calendar_event_id = "";
-
-      foreach (var persona in this._persona_set)
+      this._update_single_valued_property (typeof (BirthdayDetails), (p) =>
+        {
+          var details = ((BirthdayDetails) p);
+          return details.birthday != null && details.calendar_event_id != null;
+        }, (a, b) =>
+        {
+          var a_birthday = ((BirthdayDetails) a).birthday;
+          var b_birthday = ((BirthdayDetails) b).birthday;
+          var a_event_id = ((BirthdayDetails) a).calendar_event_id;
+          var b_event_id = ((BirthdayDetails) b).calendar_event_id;
+
+          var a_birthday_is_set = (a_birthday != null) ? 1 : 0;
+          var b_birthday_is_set = (b_birthday != null) ? 1 : 0;
+
+          /* We consider the empty string as “set” because it's an opaque ID. */
+          var a_event_id_is_set = (a_event_id != null) ? 1 : 0;
+          var b_event_id_is_set = (b_event_id != null) ? 1 : 0;
+
+          /* Prefer personas which have both properties set over those who have
+           * only one set. We don't consider the case where the birthdays from
+           * different personas don't match, because that's just scary. */
+          return (a_birthday_is_set + a_event_id_is_set) -
+                 (b_birthday_is_set + b_event_id_is_set);
+        }, "birthday", (p) =>
         {
-          var bday_owner = persona as BirthdayDetails;
-          if (bday_owner != null)
+          unowned DateTime? bday = null;
+          unowned string? calendar_event_id = null;
+
+          if (p != null)
             {
-              if (bday_owner.birthday != null)
-                {
-                  if (this.birthday == null ||
-                      bday_owner.birthday.compare (this.birthday) != 0)
-                    {
-                      bday = bday_owner.birthday;
-                      calendar_event_id = bday_owner.calendar_event_id;
-                      break;
-                    }
-                }
+              bday = ((BirthdayDetails) p).birthday;
+              calendar_event_id = ((BirthdayDetails) p).calendar_event_id;
             }
-        }
 
-      if (this.birthday != null && bday == null)
-        {
-          this.birthday = null;
-          this.calendar_event_id = null;
-        }
-      else if (bday != null)
-        {
-          this.birthday = bday;
-          this.calendar_event_id = calendar_event_id;
-        }
+          if ((this._birthday == null && bday != null) ||
+              (this._birthday != null &&
+               (bday == null || !((!) this._birthday).equal ((!) bday))) ||
+              (this._calendar_event_id != calendar_event_id))
+            {
+              this._birthday = bday;
+              this._calendar_event_id = calendar_event_id;
+
+              this.freeze_notify ();
+              this.notify_property ("birthday");
+              this.notify_property ("calendar-event-id");
+              this.thaw_notify ();
+            }
+        });
     }
 
-  private void _update_notes ()
+  private void _update_notes (bool create_if_not_exist, bool emit_notification = true, bool force_update = true)
     {
-      this._notes.clear ();
-
-      foreach (var persona in this._persona_set)
-        {
-          var note_details = persona as NoteDetails;
-          if (note_details != null)
+      this._update_multi_valued_property ("notes", create_if_not_exist,
+          () => { return this._notes == null; },
+          () =>
+            {
+              this._notes = new HashSet<NoteFieldDetails> (
+                  AbstractFieldDetails<string>.hash_static,
+                  AbstractFieldDetails<string>.equal_static);
+              this._notes_ro = this._notes.read_only_view;
+            },
+          () =>
             {
-              foreach (var n in note_details.notes)
+              var new_notes = new HashSet<NoteFieldDetails> (
+                  AbstractFieldDetails<string>.hash_static,
+                  AbstractFieldDetails<string>.equal_static);
+
+              foreach (var persona in this._persona_set)
                 {
-                  this._notes.add (n);
+                  var note_details = persona as NoteDetails;
+                  if (note_details != null)
+                    {
+                      foreach (var n in ((!) note_details).notes)
+                        {
+                          new_notes.add (n);
+                        }
+                    }
+                }
+
+              if (!Utils.set_afd_equal (new_notes, this._notes))
+                {
+                  this._notes = new_notes;
+                  this._notes_ro = new_notes.read_only_view;
+                  return true;
                 }
-            }
-        }
 
-      this.notify_property ("notes");
+              return false;
+            }, emit_notification, force_update);
     }
 
   private void _set_personas (Set<Persona>? personas,
       Individual? replacement_individual)
     {
+      assert (replacement_individual == null || replacement_individual != this);
+
       var added = new HashSet<Persona> ();
       var removed = new HashSet<Persona> ();
 
@@ -1496,7 +2458,7 @@ public class Folks.Individual : Object,
        * assume it's an empty set. */
       if (personas != null)
         {
-          foreach (var p in personas)
+          foreach (var p in (!) personas)
             {
               if (!this._persona_set.contains (p))
                 {
@@ -1529,12 +2491,9 @@ public class Folks.Individual : Object,
         }
 
       /* Determine which Personas have been removed */
-      var iter = this._persona_set.iterator ();
-      while (iter.next ())
+      foreach (var p in this._persona_set)
         {
-          var p = iter.get ();
-
-          if (personas == null || !personas.contains (p))
+          if (personas == null || !((!) personas).contains (p))
             {
               /* Keep track of how many Personas are users */
               if (p.is_user)
@@ -1558,11 +2517,15 @@ public class Folks.Individual : Object,
                   this._stores.unset (store);
                 }
 
-              this._disconnect_from_persona (p);
-              iter.remove ();
+              this._disconnect_from_persona (p, replacement_individual);
             }
         }
 
+      foreach (var p in removed)
+        {
+          this._persona_set.remove (p);
+        }
+
       this._emit_personas_changed (added, removed);
 
       /* Update this.is_user */
@@ -1591,9 +2554,10 @@ public class Folks.Individual : Object,
        *
        * We choose the most interesting Persona by ranking all the Personas
        * in the Individual by:
-       *  1. store.is-writeable
+       *  1. store.is-primary-store
        *  2. store.trust-level
        *  3. store.id (alphabetically)
+       *  4. persona.uid (alphabetically)
        *
        * Note that this heuristic shouldn't be changed without careful thought,
        * since stored references to IDs may be broken by the change.
@@ -1604,27 +2568,42 @@ public class Folks.Individual : Object,
 
           foreach (var persona in this._persona_set)
             {
-              if (chosen_persona == null ||
-                  (chosen_persona.store.is_writeable == false &&
-                      persona.store.is_writeable == true) ||
-                  (chosen_persona.store.is_writeable ==
-                          persona.store.is_writeable &&
-                      chosen_persona.store.trust_level >
+              if (chosen_persona == null)
+                {
+                  chosen_persona = persona;
+                  continue;
+                }
+
+              var _chosen_persona = (!) chosen_persona;
+
+              if ((_chosen_persona.store.is_primary_store == false &&
+                      persona.store.is_primary_store == true) ||
+                  (_chosen_persona.store.is_primary_store ==
+                          persona.store.is_primary_store &&
+                      _chosen_persona.store.trust_level >
                           persona.store.trust_level) ||
-                  (chosen_persona.store.is_writeable ==
-                          persona.store.is_writeable &&
-                      chosen_persona.store.trust_level ==
+                  (_chosen_persona.store.is_primary_store ==
+                          persona.store.is_primary_store &&
+                      _chosen_persona.store.trust_level ==
+                          persona.store.trust_level &&
+                      _chosen_persona.store.id > persona.store.id) ||
+                  (_chosen_persona.store.is_primary_store ==
+                          persona.store.is_primary_store &&
+                      _chosen_persona.store.trust_level ==
                           persona.store.trust_level &&
-                      chosen_persona.store.id > persona.store.id)
+                      _chosen_persona.store.id == persona.store.id &&
+                      _chosen_persona.uid > persona.uid)
                  )
                {
                  chosen_persona = persona;
                }
             }
 
-          // Hash the chosen persona's UID
+          /* Hash the chosen persona's UID. We can guarantee chosen_persona is
+           * non-null here because it's at least set to the first element of
+           * this._persona_set, which we've checked is non-empty. */
           this.id = Checksum.compute_for_string (ChecksumType.SHA1,
-              chosen_persona.uid);
+              ((!) chosen_persona).uid);
         }
 
       /* Update our aggregated fields and notify the changes */
@@ -1635,4 +2614,66 @@ public class Folks.Individual : Object,
     {
       this._set_personas (null, replacement_individual);
     }
+
+  /**
+   * Anti-linked with a persona?
+   *
+   * Check whether this individual is anti-linked to {@link Persona} ``p`` at
+   * all. If so, ``true`` will be returned — ``false`` will be returned
+   * otherwise.
+   *
+   * Note that this will check for anti-links in either direction, since
+   * anti-links are not necessarily symmetric.
+   *
+   * @param p persona to check for anti-links with
+   * @return ``true`` if this individual is anti-linked with persona ``p``;
+   * ``false``
+   * otherwise
+   * @since 0.7.3
+   */
+  public bool has_anti_link_with_persona (Persona p)
+    {
+      var al = p as AntiLinkable;
+
+      foreach (var persona in this._persona_set)
+        {
+          var pl = persona as AntiLinkable;
+
+          if ((al != null && ((!) al).has_anti_link_with_persona (persona)) ||
+              (pl != null && ((!) pl).has_anti_link_with_persona (p)))
+            {
+              return true;
+            }
+        }
+
+      return false;
+    }
+
+  /**
+   * Anti-linked with an individual?
+   *
+   * Check whether this individual is anti-linked to any of the {@link Persona}s
+   * in {@link Folks.Individual} ``i``. If so, ``true`` will be returned —
+   * ``false`` will be returned otherwise.
+   *
+   * Note that this will check for anti-links in either direction, since
+   * anti-links are not necessarily symmetric.
+   *
+   * @param i individual to check for anti-links with
+   * @return ``true`` if this individual is anti-linked with individual ``i``;
+   * ``false`` otherwise
+   * @since 0.7.3
+   */
+  public bool has_anti_link_with_individual (Individual i)
+    {
+      foreach (var p in i.personas)
+        {
+          if (this.has_anti_link_with_persona (p) == true)
+            {
+              return true;
+            }
+        }
+
+      return false;
+    }
 }