/**
* 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,
GenderDetails,
GroupDetails,
ImDetails,
+ InteractionDetails,
LocalIdDetails,
NameDetails,
NoteDetails,
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, ImFieldDetails> _im_addresses;
- private HashMultiMap<string, WebServiceFieldDetails> _web_service_addresses;
- private string _nickname = "";
/**
* The trust level of the Individual.
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 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.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`.
+ * 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
*/
* 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}
*/
/**
* {@inheritDoc}
*
- * @since UNRELEASED
+ * @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)
{
return;
debug ("Setting alias of individual '%s' to '%s'…", this.id, alias);
PropertyError? persona_error = null;
- var alias_changed = false;
+ var prop_changed = false;
/* 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 && p.store.is_writeable == true &&
- "alias" in p.writeable_properties)
+ var _a = p as AliasDetails;
+ if (_a == null)
+ {
+ continue;
+ }
+ var a = (!) _a;
+
+ if ("alias" in p.writeable_properties)
{
try
{
yield a.change_alias (alias);
debug (" written to writeable persona '%s'", p.uid);
- alias_changed = true;
+ prop_changed = true;
}
catch (PropertyError e)
{
}
}
- /* Failure? */
- if (alias_changed == false)
+ /* Failure? Changing the property failed on every suitable persona found
+ * (and potentially zero suitable personas were found). */
+ if (prop_changed == false)
{
- assert (persona_error != null);
+ if (persona_error == null)
+ {
+ persona_error = new PropertyError.NOT_WRITEABLE (
+ _("Failed to change property ‘%s’: No suitable personas were found."),
+ "alias");
+ }
+
throw persona_error;
}
-
- /* Update our copy of the alias. */
- this._alias = alias;
- this.notify_property ("alias");
}
private StructuredName? _structured_name = null;
set { this.change_full_name.begin (value); } /* not writeable */
}
+ private string _nickname = "";
+
/**
* {@inheritDoc}
*/
/**
* {@inheritDoc}
*
- * @since UNRELEASED
+ * @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. */
+
// Normalise null values to the empty string
if (nickname == null)
{
debug ("Setting nickname of individual '%s' to '%s'…", this.id, nickname);
PropertyError? persona_error = null;
- var nickname_changed = false;
+ var prop_changed = false;
/* 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 && p.store.is_writeable == true &&
- "nickname" in p.writeable_properties)
+ var _n = p as NameDetails;
+ if (_n == null)
+ {
+ continue;
+ }
+ var n = (!) _n;
+
+ if ("nickname" in p.writeable_properties)
{
try
{
yield n.change_nickname (nickname);
debug (" written to writeable persona '%s'", p.uid);
- nickname_changed = true;
+ prop_changed = true;
}
catch (PropertyError e)
{
}
}
- /* Failure? */
- if (nickname_changed == false)
+ /* Failure? Changing the property failed on every suitable persona found
+ * (and potentially zero suitable personas were found). */
+ if (prop_changed == false)
{
- assert (persona_error != null);
+ if (persona_error == null)
+ {
+ persona_error = new PropertyError.NOT_WRITEABLE (
+ _("Failed to change property ‘%s’: No suitable personas were found."),
+ "nickname");
+ }
+
throw persona_error;
}
-
- /* Update our copy of the nickname. */
- this._nickname = nickname;
- this.notify_property ("nickname");
}
private Gender _gender = Gender.UNSPECIFIED;
set { this.change_gender.begin (value); } /* not writeable */
}
- private HashSet<UrlFieldDetails> _urls;
- private Set<UrlFieldDetails> _urls_ro;
+ private HashSet<UrlFieldDetails>? _urls = null;
+ private Set<UrlFieldDetails>? _urls_ro = null;
/**
* {@inheritDoc}
*/
+ [CCode (notify = false)]
public Set<UrlFieldDetails> urls
{
- get { return this._urls_ro; }
- private set
+ get
{
- this._urls.clear ();
- foreach (var url_fd in value)
- this._urls.add (url_fd);
+ this._update_urls (true, false, false);
+ return this._urls_ro;
}
+ set { this.change_urls.begin (value); } /* not writeable */
}
- private HashSet<PhoneFieldDetails> _phone_numbers;
- private Set<PhoneFieldDetails> _phone_numbers_ro;
+ private HashSet<PhoneFieldDetails>? _phone_numbers = null;
+ private Set<PhoneFieldDetails>? _phone_numbers_ro = null;
/**
* {@inheritDoc}
[CCode (notify = false)]
public Set<PhoneFieldDetails> phone_numbers
{
- get { return this._phone_numbers_ro; }
+ get
+ {
+ this._update_phone_numbers (true, false, false);
+ return this._phone_numbers_ro;
+ }
set { this.change_phone_numbers.begin (value); } /* not writeable */
}
- private HashSet<EmailFieldDetails> _email_addresses;
- private Set<EmailFieldDetails> _email_addresses_ro;
+ private HashSet<EmailFieldDetails>? _email_addresses = null;
+ private Set<EmailFieldDetails>? _email_addresses_ro = null;
/**
* {@inheritDoc}
[CCode (notify = false)]
public Set<EmailFieldDetails> email_addresses
{
- get { return this._email_addresses_ro; }
+ get
+ {
+ this._update_email_addresses (true, false, false);
+ return this._email_addresses_ro;
+ }
set { this.change_email_addresses.begin (value); } /* not writeable */
}
- private HashSet<RoleFieldDetails> _roles;
- private Set<RoleFieldDetails> _roles_ro;
+ private HashSet<RoleFieldDetails>? _roles = null;
+ private Set<RoleFieldDetails>? _roles_ro = null;
/**
* {@inheritDoc}
[CCode (notify = false)]
public Set<RoleFieldDetails> roles
{
- get { return this._roles_ro; }
+ get
+ {
+ 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; }
+ get
+ {
+ this._update_local_ids (true, false, false);
+ return this._local_ids_ro;
+ }
set { this.change_local_ids.begin (value); } /* not writeable */
}
set { this.change_calendar_event_id.begin (value); } /* not writeable */
}
- private HashSet<NoteFieldDetails> _notes;
- private Set<NoteFieldDetails> _notes_ro;
+ private HashSet<NoteFieldDetails>? _notes = null;
+ private Set<NoteFieldDetails>? _notes_ro = null;
/**
* {@inheritDoc}
[CCode (notify = false)]
public Set<NoteFieldDetails> notes
{
- get { return this._notes_ro; }
+ get
+ {
+ this._update_notes (true, false, false);
+ return this._notes_ro;
+ }
set { this.change_notes.begin (value); } /* not writeable */
}
- private HashSet<PostalAddressFieldDetails> _postal_addresses;
- private Set<PostalAddressFieldDetails> _postal_addresses_ro;
+ private HashSet<PostalAddressFieldDetails>? _postal_addresses = null;
+ private Set<PostalAddressFieldDetails>? _postal_addresses_ro = null;
/**
* {@inheritDoc}
[CCode (notify = false)]
public Set<PostalAddressFieldDetails> postal_addresses
{
- get { return this._postal_addresses_ro; }
+ get
+ {
+ 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)]
/**
* {@inheritDoc}
*
- * @since UNRELEASED
+ * @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)
{
return;
is_favourite ? "TRUE" : "FALSE");
PropertyError? persona_error = null;
- var is_favourite_changed = false;
+ var prop_changed = false;
/* Try to write it to only the Personas which have "is-favourite" as a
* writeable property.
* is one property which is harmless to propagate. */
foreach (var p in this._persona_set)
{
- var a = p as FavouriteDetails;
- if (a != null && "is-favourite" in p.writeable_properties)
+ var _a = p as FavouriteDetails;
+ if (_a == null)
+ {
+ continue;
+ }
+ var a = (!) _a;
+
+ if ("is-favourite" in p.writeable_properties)
{
try
{
yield a.change_is_favourite (is_favourite);
debug (" written to persona '%s'", p.uid);
- is_favourite_changed = true;
+ prop_changed = true;
}
catch (PropertyError e)
{
}
}
- /* Failure? */
- if (is_favourite_changed == false)
+ /* Failure? Changing the property failed on every suitable persona found
+ * (and potentially zero suitable personas were found). */
+ if (prop_changed == false)
{
- assert (persona_error != null);
+ 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;
}
-
- /* Update our copy of the property. */
- this._is_favourite = is_favourite;
- this.notify_property ("is-favourite");
}
+ 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;
- set
+ /* 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)
{
- foreach (var p in this._persona_set)
+ if (persona_error == null)
{
- if (p is GroupDetails && ((Persona) p).store.is_writeable == true)
- ((GroupDetails) p).groups = value;
+ persona_error = new PropertyError.NOT_WRITEABLE (
+ _("Failed to change property ‘%s’: No suitable personas were found."),
+ "groups");
}
- this._update_groups ();
+
+ throw persona_error;
}
}
+ private HashMultiMap<string, ImFieldDetails>? _im_addresses = null;
+
/**
* {@inheritDoc}
*/
[CCode (notify = false)]
public MultiMap<string, ImFieldDetails> im_addresses
{
- get { return this._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 { return this._web_service_addresses; }
- private set {}
+ 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;
+
+ public DateTime? last_im_interaction_datetime
+ {
+ get
+ {
+ if (this._last_im_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_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;
+ }
+ }
+ }
+ }
+ return this._last_im_interaction_datetime;
+ }
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public uint call_interaction_count
+ {
+ 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}
+ */
+ private DateTime? _last_call_interaction_datetime = null;
+
+ public DateTime? last_call_interaction_datetime
+ {
+ 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.?).
private void _persona_group_changed_cb (string group, bool is_member)
{
- this._update_groups ();
+ this._update_groups (false);
}
private void _notify_gender_cb ()
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 ()
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.
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)
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)
{
+ Object (personas: personas);
+
debug ("Creating new Individual with %u Personas: %p",
- (personas != null ? personas.size : 0), this);
-
- this._im_addresses = new HashMultiMap<string, ImFieldDetails> (
- null, null, ImFieldDetails.hash, (EqualFunc) ImFieldDetails.equal);
- this._web_service_addresses =
- new HashMultiMap<string, WebServiceFieldDetails> (
- null, null,
- (GLib.HashFunc) WebServiceFieldDetails.hash,
- (GLib.EqualFunc) WebServiceFieldDetails.equal);
- this._persona_set =
- new HashSet<Persona> (direct_hash, direct_equal);
+ 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<UrlFieldDetails> (
- (GLib.HashFunc) UrlFieldDetails.hash,
- (GLib.EqualFunc) UrlFieldDetails.equal);
- this._urls_ro = this._urls.read_only_view;
- this._phone_numbers = new HashSet<PhoneFieldDetails> (
- (GLib.HashFunc) PhoneFieldDetails.hash,
- (GLib.EqualFunc) PhoneFieldDetails.equal);
- this._phone_numbers_ro = this._phone_numbers.read_only_view;
- this._email_addresses = new HashSet<EmailFieldDetails> (
- (GLib.HashFunc) EmailFieldDetails.hash,
- (GLib.EqualFunc) EmailFieldDetails.equal);
- this._email_addresses_ro = this._email_addresses.read_only_view;
- this._roles = new HashSet<RoleFieldDetails> (
- (GLib.HashFunc) RoleFieldDetails.hash,
- (GLib.EqualFunc) RoleFieldDetails.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<PostalAddressFieldDetails> (
- (GLib.HashFunc) PostalAddressFieldDetails.hash,
- (GLib.EqualFunc) PostalAddressFieldDetails.equal);
- this._postal_addresses_ro = this._postal_addresses.read_only_view;
- this._notes = new HashSet<NoteFieldDetails> (
- (GLib.HashFunc) NoteFieldDetails.hash,
- (GLib.EqualFunc) NoteFieldDetails.equal);
- this._notes_ro = this._notes.read_only_view;
-
- this.personas = personas;
}
~Individual ()
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;
_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)
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
- * back-end to optimize it (like Telepathy will for MembersChanged for the
- * groups channel list) */
- foreach (var p in this._persona_set)
+ /*
+ * 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) =>
{
- if (p is GroupDetails)
+ 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)
{
- var persona = (GroupDetails) p;
+ return (a_is_set ? 1 : 0) - (b_is_set ? 1 : 0);
+ }
- foreach (var group in persona.groups)
- {
- new_groups.add (group);
- }
+ 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);
}
- }
- foreach (var group in new_groups)
+ /* 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)
{
- if (!this._groups.contains (group))
+ /* We only care about personas implementing the given interface. */
+ if (p.get_type ().is_a (interface_type))
{
- this._groups.add (group);
- foreach (var g in this._groups)
+ if (candidate_p == null ||
+ primary_compare_func (p, (!) candidate_p) > 0)
{
- debug (" %s", g);
+ candidate_p = p;
}
-
- this.group_changed (group, true);
}
}
- /* buffer the removals, so we don't remove while iterating */
- var removes = new GLib.List<string> ();
- foreach (var group in this._groups)
- {
- if (!new_groups.contains (group))
- removes.prepend (group);
- }
-
- removes.foreach ((l) =>
- {
- unowned string group = (string) l;
- this._groups.remove (group);
- this.group_changed (group, false);
- });
+ /* 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);
}
- private void _update_presence ()
- {
- var presence_message = "";
- var presence_status = "";
- var presence_type = Folks.PresenceType.UNSET;
+ /* 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 ();
- /* Choose the most available presence from our personas */
- foreach (var p in this._persona_set)
+ /* 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 ())
{
- if (p is PresenceDetails)
+ /* Notify and return. */
+ if (create_if_not_exist == false)
{
- unowned PresenceDetails presence = (PresenceDetails) p;
-
- if (PresenceDetails.typecmp (presence.presence_type,
- presence_type) > 0)
+ if (emit_notification)
{
- presence_type = presence.presence_type;
- presence_message = presence.presence_message;
- presence_status = presence.presence_status;
+ this.notify_property (prop_name);
}
+ return;
}
- }
-
- 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;
+ /* Lazily instantiate the set of IM addresses. */
+ create_collection ();
+ created = true;
+ }
- if (this.presence_status != presence_status)
- this.presence_status = presence_status;
+ /* 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_is_favourite ()
+ private void _update_groups (bool create_if_not_exist, bool emit_notification = true, bool force_update = true)
{
- var favourite = false;
-
- debug ("Running _update_is_favourite() on '%s'", this.id);
-
- foreach (var p in this._persona_set)
+ /* 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 (favourite == false && p is FavouriteDetails)
+ if (emit_notification)
{
- favourite = ((FavouriteDetails) p).is_favourite;
- if (favourite == true)
- break;
+ this.notify_property ("groups");
}
+ return;
}
- /* 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)
+ /* Lazily instantiate the set of groups. */
+ else if (this._groups == null)
{
- this._is_favourite = favourite;
- this.notify_property ("is-favourite");
+ this._groups = new HashSet<string> ();
+ this._groups_ro = this._groups.read_only_view;
+ created = true;
}
- }
- private void _update_alias ()
- {
- string alias = null;
- var alias_is_display_id = false;
+ /* Don't touch existing content in get(). */
+ if (!created && !force_update)
+ return;
- debug ("Updating alias for individual '%s'", this.id);
+ var new_groups = new HashSet<string> ();
- /* 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. */
+ /* 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)
{
- if (p is AliasDetails && p.store.is_writeable == true)
+ if (p is GroupDetails)
{
- var a = (AliasDetails) p;
+ var persona = (GroupDetails) p;
- if (a.alias != null && a.alias.strip () != "")
+ foreach (var group in persona.groups)
{
- alias = a.alias;
- break;
+ new_groups.add (group);
}
}
}
- debug (" got alias '%s' from writeable personas", alias);
+ foreach (var group in new_groups)
+ {
+ if (this._groups.add (group) && emit_notification)
+ {
+ this.group_changed (group, true);
+ }
+ }
- /* 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)
+ /* buffer the removals, so we don't remove while iterating */
+ var removes = new GLib.List<string> ();
+ foreach (var group in this._groups)
{
- foreach (var p in this._persona_set)
+ if (!new_groups.contains (group))
+ removes.prepend (group);
+ }
+
+ removes.foreach ((l) =>
+ {
+ unowned string group = (string) l;
+ this._groups.remove (group);
+ if (emit_notification)
{
- if (p is AliasDetails)
- {
- var a = (AliasDetails) p;
+ this.group_changed (group, false);
+ }
+ });
+ }
- if (a.alias == null || a.alias.strip () == "")
- continue;
+ private void _update_presence ()
+ {
+ 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;
- 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;
- }
- }
+ return PresenceDetails.typecmp (a_presence, b_presence);
+ }, "presence", (p) =>
+ {
+ var presence_message = ""; /* must not be null */
+ var presence_status = ""; /* must not be null */
+ var presence_type = Folks.PresenceType.UNSET;
+
+ if (p != null)
+ {
+ presence_type = ((PresenceDetails) p).presence_type;
+ presence_message = ((PresenceDetails) p).presence_message;
+ presence_status = ((PresenceDetails) p).presence_status;
}
- }
- debug (" got alias '%s' from non-writeable personas", alias);
+ /* 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 ();
+ }
+ });
+ }
- if (alias == null)
+ private void _update_is_favourite ()
+ {
+ this._update_single_valued_property (typeof (FavouriteDetails), (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)
+ return true;
+ }, (a, b) =>
+ {
+ var a_is_favourite = ((FavouriteDetails) a).is_favourite;
+ var b_is_favourite = ((FavouriteDetails) b).is_favourite;
+
+ return ((a_is_favourite == true) ? 1 : 0) -
+ ((b_is_favourite == true) ? 1 : 0);
+ }, "is-favourite", (p) =>
+ {
+ var favourite = false;
+
+ if (p != null)
+ {
+ favourite = ((FavouriteDetails) p).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)
{
- alias = persona.display_id;
- debug ("No aliases available for individual; using display ID " +
- "instead: %s", alias);
- break;
+ 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 alias back
- * down to all our Personas, even if it's a fallback display ID or
- * something else undesirable. */
- if (this._alias != alias)
+ private void _update_alias ()
+ {
+ this._update_single_valued_property (typeof (AliasDetails), (p) =>
{
- debug ("Changing alias of individual '%s' from '%s' to '%s'.",
- this.id, this._alias, alias);
- this._alias = alias;
- this.notify_property ("alias");
- }
+ var alias = ((AliasDetails) p).alias;
+ return_val_if_fail (alias != null, false);
+
+ return (alias.strip () != ""); /* empty aliases are unset */
+ }, (a, b) =>
+ {
+ var a_alias = ((AliasDetails) a).alias;
+ var b_alias = ((AliasDetails) b).alias;
+
+ return_val_if_fail (a_alias != null, 0);
+ return_val_if_fail (b_alias != null, 0);
+
+ var a_is_empty = (a_alias.strip () == "") ? 1 : 0;
+ var b_is_empty = (b_alias.strip () == "") ? 1 : 0;
+
+ /* 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;
+
+ return (b_is_empty + b_is_display_id) -
+ (a_is_empty + a_is_display_id);
+ }, "alias", (p) =>
+ {
+ string alias = ""; /* must not be null */
+
+ if (p != null)
+ {
+ 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)
+ {
+ this._alias = alias;
+ this.notify_property ("alias");
+ }
+ });
}
private void _update_avatar ()
{
- LoadableIcon? 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 == null || !this._avatar.equal (avatar))
- {
- this._avatar = avatar;
- this.notify_property ("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 ()
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);
+ /* We only care about personas implementing the given interface. */
+ var web_service_details = persona as WebServiceDetails;
+ if (web_service_details != null)
+ {
+ 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);
+ }
+ }
+ }
+ }
- foreach (var ws_fd in cur_addresses)
- this._web_service_addresses.set (cur_web_service, ws_fd);
+ 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;
}
- }
- }
- this.notify_property ("web-service-addresses");
+
+ return false;
+ }, emit_notification, force_update);
}
private void _connect_to_persona (Persona persona)
((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;
- this.notify_property ("structured-name");
- return;
- }
+ name = null;
}
}
- }
- if (name_found == false && this._structured_name != null)
- {
- this._structured_name = null;
- this.notify_property ("structured-name");
- }
+ 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;
- this.notify_property ("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) =>
{
- var name_details = persona as NameDetails;
- if (name_details != null)
+ /* Can't compare two set names. */
+ return 0;
+ }, "nickname", (p) =>
+ {
+ 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,
this._persona_group_changed_cb);
}
+ /* 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);
+ }
+
/* 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).
* aggregator's rewritten, it would be nice to fix this. */
if (persona.individual == this)
{
- persona.individual = replacement_individual;
+ /* 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)
+ {
+ persona.individual = replacement_individual;
+ }
+ else
+ {
+ persona.individual = null;
+ }
}
}
private void _update_gender ()
{
- Gender new_gender = Gender.UNSPECIFIED;
-
- foreach (var persona in this._persona_set)
+ this._update_single_valued_property (typeof (GenderDetails), (p) =>
+ {
+ return ((GenderDetails) p).gender != Gender.UNSPECIFIED;
+ }, (a, b) =>
{
- var gender_details = persona as GenderDetails;
- if (gender_details != null)
+ /* 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;
+
+ if (p != null)
{
- var new_value = gender_details.gender;
- if (new_value != Gender.UNSPECIFIED)
- {
- new_gender = new_value;
- break;
- }
+ new_gender = ((GenderDetails) p).gender;
}
- }
- if (new_gender != this.gender)
- {
- this._gender = new_gender;
- this.notify_property ("gender");
- }
+ if (new_gender != this.gender)
+ {
+ this._gender = new_gender;
+ this.notify_property ("gender");
+ }
+ });
}
- private void _update_urls ()
+ private void _update_urls (bool create_if_not_exist, bool emit_notification = true, bool force_update = true)
{
- /* 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 UrlFieldDetails> (
- null, null, (GLib.EqualFunc) UrlFieldDetails.equal);
-
- this._urls.clear ();
-
- foreach (var persona in this._persona_set)
- {
- var url_details = persona as UrlDetails;
- if (url_details != null)
+ this._update_multi_valued_property ("urls", create_if_not_exist,
+ () => { return this._urls == null; },
+ () =>
{
- foreach (var url_fd in url_details.urls)
+ 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)
{
- if (url_fd.value == null)
- continue;
-
- var existing = urls_set.get (url_fd.value);
- if (existing != null)
- existing.extend_parameters (url_fd.parameters);
- else
+ /* 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_url_fd = new UrlFieldDetails (url_fd.value);
- new_url_fd.extend_parameters (url_fd.parameters);
- urls_set.set (url_fd.value, new_url_fd);
- this._urls.add (new_url_fd);
+ 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. */
- var phone_numbers_set =
- new HashMap<unowned string, unowned PhoneFieldDetails> (
- null, null, (GLib.EqualFunc) PhoneFieldDetails.equal);
-
- 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 phone_fd in phone_details.phone_numbers)
- {
- if (phone_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 (phone_fd.value);
- if (existing != null)
- existing.extend_parameters (phone_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 PhoneFieldDetails (phone_fd.value);
- new_fd.extend_parameters (phone_fd.parameters);
- phone_numbers_set.set (phone_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 EmailFieldDetails> (
- null, null, (GLib.EqualFunc) EmailFieldDetails.equal);
-
- 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 email_fd in email_details.email_addresses)
- {
- if (email_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 (email_fd.value);
- if (existing != null)
- existing.extend_parameters (email_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_email_fd = new EmailFieldDetails (email_fd.value,
- email_fd.parameters);
- emails_set.set (email_fd.value, new_email_fd);
- this._email_addresses.add (new_email_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 role_fd 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)
+ {
+ var role_details = persona as RoleDetails;
+ if (role_details != null)
+ {
+ foreach (var role_fd in ((!) role_details).roles)
+ {
+ new_roles.add (role_fd);
+ }
+ }
+ }
+
+ if (!Utils.set_afd_equal (new_roles, this._roles))
{
- this._roles.add (role_fd);
+ this._roles = new_roles;
+ this._roles_ro = new_roles.read_only_view;
+ return true;
}
- }
- }
- this.notify_property ("roles");
+ 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; },
+ () =>
+ {
+ this._local_ids = new HashSet<string> ();
+ this._local_ids_ro = this._local_ids.read_only_view;
+ },
+ () =>
{
- foreach (var id in local_ids_details.local_ids)
+ var new_local_ids = new HashSet<string> ();
+
+ foreach (var persona in this._persona_set)
+ {
+ 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);
+ }
+ }
+ }
+
+ if (new_local_ids.size != this._local_ids.size ||
+ !new_local_ids.contains_all (this._local_ids))
{
- this._local_ids.add (id);
+ this._local_ids = new_local_ids;
+ this._local_ids_ro = new_local_ids.read_only_view;
+ return true;
}
- }
- }
- this.notify_property ("local-ids");
+ 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 pafd in address_details.postal_addresses)
- this._postal_addresses.add (pafd);
- }
- }
+ 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);
- this.notify_property ("postal-addresses");
+ 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;
+ }
+
+ 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 bday_owner = persona as BirthdayDetails;
- if (bday_owner != null)
+ 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) =>
+ {
+ 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;
- this.freeze_notify ();
- this.notify_property ("birthday");
- this.notify_property ("calendar-event-id");
- this.thaw_notify ();
- }
- 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 ();
- }
+ 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,
* 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))
{
}
/* 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)
}
this._disconnect_from_persona (p, replacement_individual);
- iter.remove ();
}
}
+ foreach (var p in removed)
+ {
+ this._persona_set.remove (p);
+ }
+
this._emit_personas_changed (added, removed);
/* Update this.is_user */
*
* 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.
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.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.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 */
{
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;
+ }
}