/*
* 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
* the Free Software Foundation, either version 2.1 of the License, or
* (at your option) any later version.
*
* This library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this library. If not, see .
*
* Authors:
* Travis Reitter
* Philip Withnall
*/
using Gee;
using GLib;
/**
* Trust level for an {@link Individual} for use in the UI.
*
* @since 0.1.15
*/
public enum Folks.TrustLevel
{
/**
* The {@link Individual}'s {@link Persona}s aren't trusted at all.
*
* This is the trust level for an {@link Individual} which contains one or
* more {@link Persona}s which cannot be guaranteed to be the same
* {@link Persona}s as were originally linked together.
*
* For example, an {@link Individual} containing a link-local XMPP
* {@link Persona} would have this trust level, since someone else could
* easily spoof the link-local XMPP {@link Persona}'s identity.
*
* @since 0.1.15
*/
NONE,
/**
* The {@link Individual}'s {@link Persona}s are trusted.
*
* This trust level is for {@link Individual}s where it can be guaranteed
* that all the {@link Persona}s are the same ones as when they were
* originally linked together.
*
* Note that this doesn't guarantee that the user who behind each
* {@link Persona} is who they claim to be.
*
* @since 0.1.15
*/
PERSONAS
}
/**
* A physical person, aggregated from the various {@link Persona}s the person
* might have, such as their different IM addresses or vCard entries.
*
* 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 Individual}s.
*
* No further guarantees are made about the order of preference used for
* choosing which property values to use for the {@link 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,
AvatarDetails,
BirthdayDetails,
EmailDetails,
FavouriteDetails,
GenderDetails,
GroupDetails,
ImDetails,
LocalIdDetails,
NameDetails,
NoteDetails,
PresenceDetails,
PhoneDetails,
PostalAddressDetails,
RoleDetails,
UrlDetails,
WebServiceDetails
{
/* Stores the Personas contained in this Individual. */
private HashSet _persona_set =
new HashSet (direct_hash, direct_equal);
/* Read-only view of the above set */
private Set _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 _stores =
new HashMap (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;
/**
* The trust level of the Individual.
*
* This specifies how far the Individual can be trusted to be who it claims
* to be. See the descriptions for the elements of {@link TrustLevel}.
*
* Clients should ''not'' allow linking of Individuals who have a trust level
* of {@link TrustLevel.NONE}.
*
* @since 0.1.15
*/
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 async void change_avatar (LoadableIcon? avatar) throws PropertyError
{
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 avatar_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);
avatar_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? */
if (avatar_changed == false)
{
assert (persona_error != null);
throw persona_error;
}
}
/**
* {@inheritDoc}
*/
public Folks.PresenceType presence_type { get; set; }
/**
* {@inheritDoc}
*
* @since 0.6.0
*/
public string presence_status { get; set; }
/**
* {@inheritDoc}
*/
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`.
*
* 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`).
*
* 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`.
*
* @since 0.3.0
*/
public bool is_user { get; private set; }
/**
* A unique identifier for the Individual.
*
* This uniquely identifies the Individual, and persists across
* {@link IndividualAggregator} instances. It may not persist across linking
* the Individual with other Individuals.
*
* This is an opaque string and has no structure.
*
* If an identifier is required which will be used for a long-lived link
* between different stored data, it may be more desirable to use the
* {@link Persona.uid} of the most relevant {@link Persona} in the Individual
* instead. For example, if storing references to Individuals who are tagged
* in a photo, it may be safer to store the UID of the Persona whose backend
* provided the photo (e.g. Facebook).
*/
public string id { get; private set; }
/**
* Emitted when the last of the Individual's {@link Persona}s has been
* removed.
*
* At this point, the Individual is invalid, so any client referencing it
* 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
* @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); }
}
/**
* {@inheritDoc}
*
* @since 0.6.2
*/
public async void change_alias (string alias) throws PropertyError
{
if (this._alias == alias)
{
return;
}
debug ("Setting alias of individual '%s' to '%s'…", this.id, alias);
PropertyError? persona_error = null;
var alias_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)
{
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;
}
catch (PropertyError e)
{
/* Store the first error so we can throw it if setting the
* alias fails on every other persona. */
if (persona_error == null)
{
persona_error = e;
}
}
}
}
/* Failure? */
if (alias_changed == false)
{
assert (persona_error != null);
throw persona_error;
}
/* Update our copy of the alias. */
this._alias = alias;
this.notify_property ("alias");
}
private StructuredName? _structured_name = null;
/**
* {@inheritDoc}
*/
[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}
*/
[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
{
// Normalise null values to the empty string
if (nickname == null)
{
nickname = "";
}
if (this._nickname == nickname)
{
return;
}
debug ("Setting nickname of individual '%s' to '%s'…", this.id, nickname);
PropertyError? persona_error = null;
var nickname_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)
{
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;
}
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)
{
persona_error = e;
}
}
}
}
/* Failure? */
if (nickname_changed == false)
{
assert (persona_error != null);
throw persona_error;
}
/* Update our copy of the nickname. */
this._nickname = nickname;
this.notify_property ("nickname");
}
private Gender _gender = Gender.UNSPECIFIED;
/**
* {@inheritDoc}
*/
[CCode (notify = false)]
public Gender gender
{
get { return this._gender; }
set { this.change_gender.begin (value); } /* not writeable */
}
private HashSet _urls = new HashSet (
(GLib.HashFunc) UrlFieldDetails.hash,
(GLib.EqualFunc) UrlFieldDetails.equal);
private Set _urls_ro;
/**
* {@inheritDoc}
*/
[CCode (notify = false)]
public Set urls
{
get { return this._urls_ro; }
set { this.change_urls.begin (value); } /* not writeable */
}
private HashSet _phone_numbers =
new HashSet (
(GLib.HashFunc) PhoneFieldDetails.hash,
(GLib.EqualFunc) PhoneFieldDetails.equal);
private Set _phone_numbers_ro;
/**
* {@inheritDoc}
*/
[CCode (notify = false)]
public Set phone_numbers
{
get { return this._phone_numbers_ro; }
set { this.change_phone_numbers.begin (value); } /* not writeable */
}
private HashSet _email_addresses =
new HashSet (
(GLib.HashFunc) EmailFieldDetails.hash,
(GLib.EqualFunc) EmailFieldDetails.equal);
private Set _email_addresses_ro;
/**
* {@inheritDoc}
*/
[CCode (notify = false)]
public Set email_addresses
{
get { return this._email_addresses_ro; }
set { this.change_email_addresses.begin (value); } /* not writeable */
}
private HashSet _roles = new HashSet (
(GLib.HashFunc) RoleFieldDetails.hash,
(GLib.EqualFunc) RoleFieldDetails.equal);
private Set _roles_ro;
/**
* {@inheritDoc}
*/
[CCode (notify = false)]
public Set roles
{
get { return this._roles_ro; }
set { this.change_roles.begin (value); } /* not writeable */
}
private HashSet _local_ids = new HashSet ();
private Set _local_ids_ro;
/**
* {@inheritDoc}
*/
[CCode (notify = false)]
public Set local_ids
{
get { 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 */
}
private string? _calendar_event_id = null;
/**
* {@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 _notes = new HashSet (
(GLib.HashFunc) NoteFieldDetails.hash,
(GLib.EqualFunc) NoteFieldDetails.equal);
private Set _notes_ro;
/**
* {@inheritDoc}
*/
[CCode (notify = false)]
public Set notes
{
get { return this._notes_ro; }
set { this.change_notes.begin (value); } /* not writeable */
}
private HashSet _postal_addresses =
new HashSet (
(GLib.HashFunc) PostalAddressFieldDetails.hash,
(GLib.EqualFunc) PostalAddressFieldDetails.equal);
private Set _postal_addresses_ro;
/**
* {@inheritDoc}
*/
[CCode (notify = false)]
public Set postal_addresses
{
get { 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
* favourites).
*/
[CCode (notify = false)]
public bool is_favourite
{
get { return this._is_favourite; }
set { this.change_is_favourite.begin (value); }
}
/**
* {@inheritDoc}
*
* @since 0.6.2
*/
public async void change_is_favourite (bool is_favourite) throws PropertyError
{
if (this._is_favourite == is_favourite)
{
return;
}
debug ("Setting '%s' favourite status to %s…", this.id,
is_favourite ? "TRUE" : "FALSE");
PropertyError? persona_error = null;
var is_favourite_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;
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;
}
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? */
if (is_favourite_changed == false)
{
assert (persona_error != null);
throw persona_error;
}
/* Update our copy of the property. */
this._is_favourite = is_favourite;
this.notify_property ("is-favourite");
}
private HashSet _groups = new HashSet ();
private Set _groups_ro;
/**
* {@inheritDoc}
*/
[CCode (notify = false)]
public Set groups
{
get { return this._groups_ro; }
set { this.change_groups.begin (value); }
}
/**
* {@inheritDoc}
*
* @since 0.6.2
*/
public async void change_groups (Set groups) throws PropertyError
{
debug ("Setting '%s' groups…", this.id);
PropertyError? persona_error = null;
var groups_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);
groups_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? */
if (groups_changed == false)
{
assert (persona_error != null);
throw persona_error;
}
/* Update our copy of the property. */
this._update_groups ();
}
private HashMultiMap _im_addresses =
new HashMultiMap (
null, null, ImFieldDetails.hash, (EqualFunc) ImFieldDetails.equal);
/**
* {@inheritDoc}
*/
[CCode (notify = false)]
public MultiMap im_addresses
{
get { return this._im_addresses; }
set { this.change_im_addresses.begin (value); } /* not writeable */
}
private HashMultiMap _web_service_addresses =
new HashMultiMap (null, null,
(GLib.HashFunc) WebServiceFieldDetails.hash,
(GLib.EqualFunc) WebServiceFieldDetails.equal);
/**
* {@inheritDoc}
*/
[CCode (notify = false)]
public MultiMap web_service_addresses
{
get { return this._web_service_addresses; }
/* Not writeable: */
set { this.change_web_service_addresses.begin (value); }
}
/**
* The set of {@link Persona}s encapsulated by this Individual.
*
* 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.?).
*
* Changing the set of personas may cause updates to the aggregated properties
* provided by the Individual, resulting in property notifications for them.
*
* Changing the set of personas will not cause permanent linking/unlinking of
* the added/removed personas to/from this Individual. To do that, call
* {@link IndividualAggregator.link_personas} or
* {@link IndividualAggregator.unlink_individual}, which will ensure the link
* changes are written to the appropriate backend.
*
* @since 0.5.1
*/
public Set personas
{
get { return this._persona_set_ro; }
set { this._set_personas (value, null); }
}
/**
* Emitted when one or more {@link Persona}s are added to or removed from
* the Individual. As the parameters are (unordered) sets, the orders of their
* elements are undefined.
*
* @param added a set of {@link Persona}s which have been added
* @param removed a set of {@link Persona}s which have been removed
*
* @since 0.5.1
*/
public signal void personas_changed (Set added,
Set removed);
private void _notify_alias_cb (Object obj, ParamSpec ps)
{
this._update_alias ();
}
private void _notify_avatar_cb (Object obj, ParamSpec ps)
{
this._update_avatar ();
}
private void _notify_full_name_cb ()
{
this._update_full_name ();
}
private void _notify_structured_name_cb ()
{
this._update_structured_name ();
}
private void _notify_nickname_cb ()
{
this._update_nickname ();
}
private void _persona_group_changed_cb (string group, bool is_member)
{
this._update_groups ();
}
private void _notify_gender_cb ()
{
this._update_gender ();
}
private void _notify_urls_cb ()
{
this._update_urls ();
}
private void _notify_phone_numbers_cb ()
{
this._update_phone_numbers ();
}
private void _notify_postal_addresses_cb ()
{
this._update_postal_addresses ();
}
private void _notify_email_addresses_cb ()
{
this._update_email_addresses ();
}
private void _notify_roles_cb ()
{
this._update_roles ();
}
private void _notify_birthday_cb ()
{
this._update_birthday ();
}
private void _notify_notes_cb ()
{
this._update_notes ();
}
private void _notify_local_ids_cb ()
{
this._update_local_ids ();
}
/**
* 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`.
*
* The group membership change will propagate to every {@link Persona} in
* the Individual.
*
* @param group a freeform group identifier
* @param is_member whether the Individual should be a member of the group
* @since 0.1.11
*/
public async void change_group (string group, bool is_member)
{
foreach (var p in this._persona_set)
{
if (p is GroupDetails)
((GroupDetails) p).change_group.begin (group, is_member);
}
/* don't notify, since it hasn't happened in the persona backing stores
* yet; react to that directly */
}
private void _notify_presence_cb (Object obj, ParamSpec ps)
{
this._update_presence ();
}
private void _notify_im_addresses_cb (Object obj, ParamSpec ps)
{
this._update_im_addresses ();
}
private void _notify_web_service_addresses_cb (Object obj, ParamSpec ps)
{
this._update_web_service_addresses ();
}
private void _notify_is_favourite_cb (Object obj, ParamSpec ps)
{
this._update_is_favourite ();
}
/**
* 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
* {@link Folks.Individual.personas} property after construction.
*
* @param personas a list of {@link Persona}s to initialise the
* {@link Individual} with, or `null`
* @return a new Individual
*
* @since 0.5.1
*/
public Individual (Set? personas)
{
Object (personas: personas);
}
construct
{
debug ("Creating new Individual with %u Personas: %p",
this._persona_set.size, this);
this._persona_set_ro = this._persona_set.read_only_view;
this._urls_ro = this._urls.read_only_view;
this._phone_numbers_ro = this._phone_numbers.read_only_view;
this._email_addresses_ro = this._email_addresses.read_only_view;
this._roles_ro = this._roles.read_only_view;
this._local_ids_ro = this._local_ids.read_only_view;
this._postal_addresses_ro = this._postal_addresses.read_only_view;
this._notes_ro = this._notes.read_only_view;
this._groups_ro = this._groups.read_only_view;
}
~Individual ()
{
debug ("Destroying Individual '%s': %p", this.id, this);
}
/* Emit the personas-changed signal, turning null parameters into empty sets
* and ensuring that the signal is emitted with read-only views of the sets
* so that signal handlers can't modify the sets. */
private void _emit_personas_changed (Set? added,
Set? removed)
{
var _added = added;
var _removed = removed;
if ((added == null || ((!) added).size == 0) &&
(removed == null || ((!) removed).size == 0))
{
/* Emitting it with no added or removed personas is pointless */
return;
}
else if (added == null)
{
_added = new HashSet ();
}
else if (removed == null)
{
_removed = new HashSet ();
}
// 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)
{
var remaining_personas = new HashSet ();
/* Build a set of the remaining personas (those which weren't in the
* removed store. */
foreach (var persona in this._persona_set)
{
if (persona.store != store)
{
remaining_personas.add (persona);
}
}
this._set_personas (remaining_personas, null);
}
private void _store_personas_changed_cb (PersonaStore store,
Set added,
Set removed,
string? message,
Persona? actor,
GroupDetails.ChangeReason reason)
{
var remaining_personas = new HashSet ();
/* Build a set of the remaining personas (those which aren't in the
* set of removed personas). */
foreach (var persona in this._persona_set)
{
if (!removed.contains (persona))
{
remaining_personas.add (persona);
}
}
this._set_personas (remaining_personas, null);
}
private void _update_fields ()
{
this._update_groups ();
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_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_birthday ();
this._update_notes ();
this._update_postal_addresses ();
this._update_local_ids ();
}
/* 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);
/* 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);
/*
* 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 compare_func, string prop_name,
SingleValuedPropertySetter setter)
{
CompareDataFunc primary_compare_func = (a, b) =>
{
assert (a != null);
assert (b != null);
/* 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);
}
private void _update_groups ()
{
var new_groups = new HashSet ();
/* 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 GroupDetails)
{
var persona = (GroupDetails) p;
foreach (var group in persona.groups)
{
new_groups.add (group);
}
}
}
foreach (var group in new_groups)
{
if (!this._groups.contains (group))
{
this._groups.add (group);
foreach (var g in this._groups)
{
debug (" %s", g);
}
this.group_changed (group, true);
}
}
/* buffer the removals, so we don't remove while iterating */
var removes = new GLib.List ();
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);
});
}
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;
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;
}
/* 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 ()
{
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;
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)
{
this._is_favourite = favourite;
this.notify_property ("is-favourite");
}
});
}
private void _update_alias ()
{
this._update_single_valued_property (typeof (AliasDetails), (p) =>
{
var alias = ((AliasDetails) p).alias;
assert (alias != null);
return (alias.strip () != ""); /* empty aliases are unset */
}, (a, b) =>
{
var a_alias = ((AliasDetails) a).alias;
var b_alias = ((AliasDetails) b).alias;
assert (a_alias != null);
assert (b_alias != null);
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 ()
{
this._update_single_valued_property (typeof (AvatarDetails), (p) =>
{
return ((AvatarDetails) p).avatar != null;
}, (a, b) =>
{
/* 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;
}
/* 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 ()
{
var trust_level = TrustLevel.PERSONAS;
foreach (var p in this._persona_set)
{
if (p.is_user == false &&
p.store.trust_level == PersonaStoreTrust.NONE)
trust_level = TrustLevel.NONE;
}
/* Only notify if the value has changed */
if (this.trust_level != trust_level)
this.trust_level = trust_level;
}
private void _update_im_addresses ()
{
/* 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)
{
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);
foreach (var address in cur_addresses)
{
this._im_addresses.set (cur_protocol, address);
}
}
}
}
this.notify_property ("im-addresses");
}
private void _update_web_service_addresses ()
{
/* 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)
{
var web_service_details = (WebServiceDetails) persona;
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)
this._web_service_addresses.set (cur_web_service, ws_fd);
}
}
}
this.notify_property ("web-service-addresses");
}
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);
persona.notify["presence-type"].connect (this._notify_presence_cb);
persona.notify["im-addresses"].connect (this._notify_im_addresses_cb);
persona.notify["web-service-addresses"].connect
(this._notify_web_service_addresses_cb);
persona.notify["is-favourite"].connect (this._notify_is_favourite_cb);
persona.notify["structured-name"].connect (
this._notify_structured_name_cb);
persona.notify["full-name"].connect (this._notify_full_name_cb);
persona.notify["nickname"].connect (this._notify_nickname_cb);
persona.notify["gender"].connect (this._notify_gender_cb);
persona.notify["urls"].connect (this._notify_urls_cb);
persona.notify["phone-numbers"].connect (this._notify_phone_numbers_cb);
persona.notify["email-addresses"].connect (
this._notify_email_addresses_cb);
persona.notify["roles"].connect (this._notify_roles_cb);
persona.notify["birthday"].connect (this._notify_birthday_cb);
persona.notify["notes"].connect (this._notify_notes_cb);
persona.notify["postal-addresses"].connect
(this._notify_postal_addresses_cb);
persona.notify["local-ids"].connect
(this._notify_local_ids_cb);
if (persona is GroupDetails)
{
((GroupDetails) persona).group_changed.connect (
this._persona_group_changed_cb);
}
}
private void _update_structured_name ()
{
this._update_single_valued_property (typeof (NameDetails), (p) =>
{
var name = ((NameDetails) p).structured_name;
return (name != null && !((!) name).is_empty ());
}, (a, b) =>
{
/* Can't compare two set names. */
return 0;
}, "structured-name", (p) =>
{
StructuredName? name = null;
if (p != null)
{
name = ((NameDetails) p).structured_name;
if (name != null && ((!) name).is_empty ())
{
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 ()
{
this._update_single_valued_property (typeof (NameDetails), (p) =>
{
var name = ((NameDetails) p).full_name;
assert (name != null);
return (name.strip () != ""); /* empty names are unset */
}, (a, b) =>
{
/* Can't compare two set names. */
return 0;
}, "full-name", (p) =>
{
string new_full_name = ""; /* must not be null */
if (p != null)
{
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");
}
});
}
private void _update_nickname ()
{
this._update_single_valued_property (typeof (NameDetails), (p) =>
{
var nickname = ((NameDetails) p).nickname;
assert (nickname != null);
return (nickname.strip () != ""); /* empty names are unset */
}, (a, b) =>
{
/* Can't compare two set names. */
return 0;
}, "nickname", (p) =>
{
string new_nickname = ""; /* must not be null */
if (p != null)
{
new_nickname = ((NameDetails) p).nickname.strip ();
}
if (new_nickname != this._nickname)
{
this._nickname = new_nickname;
this.notify_property ("nickname");
}
});
}
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);
persona.notify["presence-message"].disconnect (
this._notify_presence_cb);
persona.notify["presence-type"].disconnect (this._notify_presence_cb);
persona.notify["im-addresses"].disconnect (
this._notify_im_addresses_cb);
persona.notify["web-service-addresses"].disconnect (
this._notify_web_service_addresses_cb);
persona.notify["is-favourite"].disconnect (
this._notify_is_favourite_cb);
persona.notify["structured-name"].disconnect (
this._notify_structured_name_cb);
persona.notify["full-name"].disconnect (this._notify_full_name_cb);
persona.notify["nickname"].disconnect (this._notify_nickname_cb);
persona.notify["gender"].disconnect (this._notify_gender_cb);
persona.notify["urls"].disconnect (this._notify_urls_cb);
persona.notify["phone-numbers"].disconnect (
this._notify_phone_numbers_cb);
persona.notify["email-addresses"].disconnect (
this._notify_email_addresses_cb);
persona.notify["roles"].disconnect (this._notify_roles_cb);
persona.notify["birthday"].disconnect (this._notify_birthday_cb);
persona.notify["notes"].disconnect (this._notify_notes_cb);
persona.notify["postal-addresses"].disconnect
(this._notify_postal_addresses_cb);
persona.notify["local-ids"].disconnect (this._notify_local_ids_cb);
if (persona is GroupDetails)
{
((GroupDetails) persona).group_changed.disconnect (
this._persona_group_changed_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).
*
* 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)
{
/* 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 ()
{
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;
if (p != null)
{
new_gender = ((GenderDetails) p).gender;
}
if (new_gender != this.gender)
{
this._gender = new_gender;
this.notify_property ("gender");
}
});
}
private void _update_urls ()
{
/* 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 (
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)
{
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 (url_fd.value, new_url_fd);
this._urls.add (new_url_fd);
}
}
}
}
this.notify_property ("urls");
}
private void _update_phone_numbers ()
{
/* 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 (
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)
{
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 (phone_fd.value, new_fd);
this._phone_numbers.add (new_fd);
}
}
}
}
this.notify_property ("phone-numbers");
}
private void _update_email_addresses ()
{
/* 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 (
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)
{
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 (email_fd.value, new_email_fd);
this._email_addresses.add (new_email_fd);
}
}
}
}
this.notify_property ("email-addresses");
}
private void _update_roles ()
{
this._roles.clear ();
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)
{
this._roles.add (role_fd);
}
}
}
this.notify_property ("roles");
}
private void _update_local_ids ()
{
this._local_ids.clear ();
foreach (var persona in this._persona_set)
{
var local_ids_details = persona as LocalIdDetails;
if (local_ids_details != null)
{
foreach (var id in ((!) local_ids_details).local_ids)
{
this._local_ids.add (id);
}
}
}
this.notify_property ("local-ids");
}
private void _update_postal_addresses ()
{
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)
{
foreach (var pafd in ((!) address_details).postal_addresses)
this._postal_addresses.add (pafd);
}
}
this.notify_property ("postal-addresses");
}
private void _update_birthday ()
{
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) =>
{
unowned DateTime? bday = null;
unowned string? calendar_event_id = null;
if (p != null)
{
bday = ((BirthdayDetails) p).birthday;
calendar_event_id = ((BirthdayDetails) p).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 ()
{
this._notes.clear ();
foreach (var persona in this._persona_set)
{
var note_details = persona as NoteDetails;
if (note_details != null)
{
foreach (var n in ((!) note_details).notes)
{
this._notes.add (n);
}
}
}
this.notify_property ("notes");
}
private void _set_personas (Set? personas,
Individual? replacement_individual)
{
assert (replacement_individual == null || replacement_individual != this);
var added = new HashSet ();
var removed = new HashSet ();
/* Determine which Personas have been added. If personas == null, we
* assume it's an empty set. */
if (personas != null)
{
foreach (var p in (!) personas)
{
if (!this._persona_set.contains (p))
{
/* Keep track of how many Personas are users */
if (p.is_user)
this._persona_user_count++;
added.add (p);
this._persona_set.add (p);
this._connect_to_persona (p);
/* Increment the Persona count for this PersonaStore */
var store = p.store;
var num_from_store = this._stores.get (store);
if (num_from_store == 0)
{
this._stores.set (store, num_from_store + 1);
}
else
{
this._stores.set (store, 1);
store.removed.connect (this._store_removed_cb);
store.personas_changed.connect (
this._store_personas_changed_cb);
}
}
}
}
/* Determine which Personas have been removed */
var iter = this._persona_set.iterator ();
while (iter.next ())
{
var p = iter.get ();
if (personas == null || !((!) personas).contains (p))
{
/* Keep track of how many Personas are users */
if (p.is_user)
this._persona_user_count--;
removed.add (p);
/* Decrement the Persona count for this PersonaStore */
var store = p.store;
var num_from_store = this._stores.get (store);
if (num_from_store > 1)
{
this._stores.set (store, num_from_store - 1);
}
else
{
store.removed.disconnect (this._store_removed_cb);
store.personas_changed.disconnect (
this._store_personas_changed_cb);
this._stores.unset (store);
}
this._disconnect_from_persona (p, replacement_individual);
iter.remove ();
}
}
this._emit_personas_changed (added, removed);
/* Update this.is_user */
var new_is_user = (this._persona_user_count > 0) ? true : false;
if (new_is_user != this.is_user)
this.is_user = new_is_user;
/* If all the Personas have been removed, remove the Individual */
if (this._persona_set.size < 1)
{
this.removed (replacement_individual);
return;
}
/* Update the ID. We choose the most interesting Persona in the
* Individual and hash their UID. This is guaranteed to be globally
* unique, and may not change (for one of the two Individuals) if we link
* two Individuals together, which is nice though we can't rely on this
* behaviour.
*
* This method of constructing an ID ensures that it'll be unique and
* stable for a given Individual once the IndividualAggregator reaches
* a quiescent state after startup. It guarantees that the ID will be
* the same every time folks is used, until the Individual is linked
* or unlinked to another Individual.
*
* We choose the most interesting Persona by ranking all the Personas
* in the Individual by:
* 1. store.is-primary-store
* 2. store.trust-level
* 3. store.id (alphabetically)
*
* Note that this heuristic shouldn't be changed without careful thought,
* since stored references to IDs may be broken by the change.
*/
if (this._persona_set.size > 0)
{
Persona? chosen_persona = null;
foreach (var persona in this._persona_set)
{
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_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 = persona;
}
}
/* 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);
}
/* Update our aggregated fields and notify the changes */
this._update_fields ();
}
internal void replace (Individual replacement_individual)
{
this._set_personas (null, replacement_individual);
}
}