2 * Copyright (C) 2010 Collabora Ltd.
3 * Copyright (C) 2011 Philip Withnall
5 * This library is free software: you can redistribute it and/or modify
6 * it under the terms of the GNU Lesser General Public License as published by
7 * the Free Software Foundation, either version 2.1 of the License, or
8 * (at your option) any later version.
10 * This library is distributed in the hope that it will be useful,
11 * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 * GNU Lesser General Public License for more details.
15 * You should have received a copy of the GNU Lesser General Public License
16 * along with this library. If not, see <http://www.gnu.org/licenses/>.
19 * Travis Reitter <travis.reitter@collabora.co.uk>
20 * Philip Withnall <philip@tecnocode.co.uk>
27 * Trust level for an {@link Individual} for use in the UI.
31 public enum Folks.TrustLevel
34 * The {@link Individual}'s {@link Persona}s aren't trusted at all.
36 * This is the trust level for an {@link Individual} which contains one or
37 * more {@link Persona}s which cannot be guaranteed to be the same
38 * {@link Persona}s as were originally linked together.
40 * For example, an {@link Individual} containing a link-local XMPP
41 * {@link Persona} would have this trust level, since someone else could
42 * easily spoof the link-local XMPP {@link Persona}'s identity.
49 * The {@link Individual}'s {@link Persona}s are trusted.
51 * This trust level is for {@link Individual}s where it can be guaranteed
52 * that all the {@link Persona}s are the same ones as when they were
53 * originally linked together.
55 * Note that this doesn't guarantee that the user who behind each
56 * {@link Persona} is who they claim to be.
64 * A physical person, aggregated from the various {@link Persona}s the person
65 * might have, such as their different IM addresses or vCard entries.
67 * When choosing the values of single-valued properties (such as
68 * {@link Individual.alias} and {@link Individual.avatar}; but not multi-valued
69 * properties such as {@link Individual.groups} and
70 * {@link Individual.im_addresses}) from the {@link Persona}s in the
71 * individual to present as the values of those properties of the individual,
72 * it is guaranteed that if the individual contains a persona from the primary
73 * persona store (see {@link IndividualAggregator.primary_store}), its property
74 * values will be chosen above all others. This means that any changes to
75 * property values made through folks (which are normally written to the primary
76 * store) will always be used by {@link Individual}s.
78 * No further guarantees are made about the order of preference used for
79 * choosing which property values to use for the {@link Individual}, other than
80 * that the order may vary between properties, but is guaranteed to be stable
81 * for a given property.
83 public class Folks.Individual : Object,
102 /* Stores the Personas contained in this Individual. */
103 private HashSet<Persona> _persona_set =
104 new HashSet<Persona> (direct_hash, direct_equal);
105 /* Read-only view of the above set */
106 private Set<Persona> _persona_set_ro;
107 /* Mapping from PersonaStore -> number of Personas from that store contained
108 * in this Individual. There shouldn't be any entries with a number < 1.
109 * This is used for working out when to disconnect from store signals. */
110 private HashMap<PersonaStore, uint> _stores =
111 new HashMap<PersonaStore, uint> (null, null);
112 /* The number of Personas in this Individual which have
113 * Persona.is_user == true. Iff this is > 0, Individual.is_user == true. */
114 private uint _persona_user_count = 0;
117 * The trust level of the Individual.
119 * This specifies how far the Individual can be trusted to be who it claims
120 * to be. See the descriptions for the elements of {@link TrustLevel}.
122 * Clients should ''not'' allow linking of Individuals who have a trust level
123 * of {@link TrustLevel.NONE}.
127 public TrustLevel trust_level { get; private set; }
129 private LoadableIcon? _avatar = null;
136 [CCode (notify = false)]
137 public LoadableIcon? avatar
139 get { return this._avatar; }
140 set { this.change_avatar.begin (value); } /* not writeable */
144 * Change the individual's avatar.
146 * It's preferred to call this rather than setting {@link Individual.avatar}
147 * directly, as this method gives error notification and will only return once
148 * the avatar has been written to the relevant backing stores (or the
149 * operation's failed).
151 * Setting this property is only guaranteed to succeed (and be written to
152 * the backing store) if
153 * {@link IndividualAggregator.ensure_individual_property_writeable} has been
154 * called successfully on the individual for the property name `avatar`.
156 * @param avatar the new avatar (or `null` to unset the avatar)
157 * @throws PropertyError if setting the avatar failed
160 public async void change_avatar (LoadableIcon? avatar) throws PropertyError
162 if ((this._avatar != null && this._avatar.equal (avatar)) ||
163 (this._avatar == null && avatar == null))
168 debug ("Setting avatar of individual '%s' to '%p'…", this.id, avatar);
170 PropertyError? persona_error = null;
171 var avatar_changed = false;
173 /* Try to write it to only the writeable Personas which have the
174 * "avatar" property as writeable. */
175 foreach (var p in this._persona_set)
177 var a = p as AvatarDetails;
178 if (a != null && "avatar" in p.writeable_properties)
182 yield a.change_avatar (avatar);
183 debug (" written to writeable persona '%s'", p.uid);
184 avatar_changed = true;
186 catch (PropertyError e)
188 /* Store the first error so we can throw it if setting the
189 * avatar fails on every other persona. */
190 if (persona_error == null)
199 if (avatar_changed == false)
201 assert (persona_error != null);
209 public Folks.PresenceType presence_type { get; private set; }
216 public string presence_status { get; private set; }
221 public string presence_message { get; private set; }
224 * Whether the Individual is the user.
226 * Iff the Individual represents the user – the person who owns the
227 * account in the backend for each {@link Persona} in the Individual –
230 * It is //not// guaranteed that every {@link Persona} in the Individual has
231 * its {@link Persona.is_user} set to the same value as the Individual. For
232 * example, the user could own two Telepathy accounts, and have added the
233 * other account as a contact in each account. The accounts will expose a
234 * {@link Persona} for the user (which will have {@link Persona.is_user} set
235 * to `true`) //and// a {@link Persona} for the contact for the other account
236 * (which will have {@link Persona.is_user} set to `false`).
238 * It is guaranteed that iff this property is set to `true` on an Individual,
239 * there will be at least one {@link Persona} in the Individual with its
240 * {@link Persona.is_user} set to `true`.
242 * It is guaranteed that there will only ever be one Individual with this
243 * property set to `true`.
247 public bool is_user { get; private set; }
250 * A unique identifier for the Individual.
252 * This uniquely identifies the Individual, and persists across
253 * {@link IndividualAggregator} instances. It may not persist across linking
254 * the Individual with other Individuals.
256 * This is an opaque string and has no structure.
258 * If an identifier is required which will be used for a long-lived link
259 * between different stored data, it may be more desirable to use the
260 * {@link Persona.uid} of the most relevant {@link Persona} in the Individual
261 * instead. For example, if storing references to Individuals who are tagged
262 * in a photo, it may be safer to store the UID of the Persona whose backend
263 * provided the photo (e.g. Facebook).
265 public string id { get; private set; }
268 * Emitted when the last of the Individual's {@link Persona}s has been
271 * At this point, the Individual is invalid, so any client referencing it
272 * should unreference it and remove it from their UI.
274 * @param replacement_individual the individual which has replaced this one
275 * due to linking, or `null` if this individual was removed for another reason
278 public signal void removed (Individual? replacement_individual);
280 private string _alias = "";
285 [CCode (notify = false)]
288 get { return this._alias; }
289 set { this.change_alias.begin (value); }
297 public async void change_alias (string alias) throws PropertyError
299 if (this._alias == alias)
304 debug ("Setting alias of individual '%s' to '%s'…", this.id, alias);
306 PropertyError? persona_error = null;
307 var alias_changed = false;
309 /* Try to write it to only the writeable Personas which have "alias"
310 * as a writeable property. */
311 foreach (var p in this._persona_set)
313 var a = p as AliasDetails;
314 if (a != null && "alias" in p.writeable_properties)
318 yield a.change_alias (alias);
319 debug (" written to writeable persona '%s'", p.uid);
320 alias_changed = true;
322 catch (PropertyError e)
324 /* Store the first error so we can throw it if setting the
325 * alias fails on every other persona. */
326 if (persona_error == null)
335 if (alias_changed == false)
337 assert (persona_error != null);
341 /* Update our copy of the alias. */
343 this.notify_property ("alias");
346 private StructuredName? _structured_name = null;
351 [CCode (notify = false)]
352 public StructuredName? structured_name
354 get { return this._structured_name; }
355 set { this.change_structured_name.begin (value); } /* not writeable */
358 private string _full_name = "";
363 [CCode (notify = false)]
364 public string full_name
366 get { return this._full_name; }
367 set { this.change_full_name.begin (value); } /* not writeable */
370 private string _nickname = "";
375 [CCode (notify = false)]
376 public string nickname
378 get { return this._nickname; }
379 set { this.change_nickname.begin (value); }
387 public async void change_nickname (string nickname) throws PropertyError
389 // Normalise null values to the empty string
390 if (nickname == null)
395 if (this._nickname == nickname)
400 debug ("Setting nickname of individual '%s' to '%s'…", this.id, nickname);
402 PropertyError? persona_error = null;
403 var nickname_changed = false;
405 /* Try to write it to only the writeable Personas which have "nickname"
406 * as a writeable property. */
407 foreach (var p in this._persona_set)
409 var n = p as NameDetails;
410 if (n != null && "nickname" in p.writeable_properties)
414 yield n.change_nickname (nickname);
415 debug (" written to writeable persona '%s'", p.uid);
416 nickname_changed = true;
418 catch (PropertyError e)
420 /* Store the first error so we can throw it if setting the
421 * nickname fails on every other persona. */
422 if (persona_error == null)
431 if (nickname_changed == false)
433 assert (persona_error != null);
437 /* Update our copy of the nickname. */
438 this._nickname = nickname;
439 this.notify_property ("nickname");
442 private Gender _gender = Gender.UNSPECIFIED;
446 [CCode (notify = false)]
449 get { return this._gender; }
450 set { this.change_gender.begin (value); } /* not writeable */
453 private HashSet<UrlFieldDetails> _urls = new HashSet<UrlFieldDetails> (
454 (GLib.HashFunc) UrlFieldDetails.hash,
455 (GLib.EqualFunc) UrlFieldDetails.equal);
456 private Set<UrlFieldDetails> _urls_ro;
461 [CCode (notify = false)]
462 public Set<UrlFieldDetails> urls
464 get { return this._urls_ro; }
465 set { this.change_urls.begin (value); } /* not writeable */
468 private HashSet<PhoneFieldDetails> _phone_numbers =
469 new HashSet<PhoneFieldDetails> (
470 (GLib.HashFunc) PhoneFieldDetails.hash,
471 (GLib.EqualFunc) PhoneFieldDetails.equal);
472 private Set<PhoneFieldDetails> _phone_numbers_ro;
477 [CCode (notify = false)]
478 public Set<PhoneFieldDetails> phone_numbers
480 get { return this._phone_numbers_ro; }
481 set { this.change_phone_numbers.begin (value); } /* not writeable */
484 private HashSet<EmailFieldDetails> _email_addresses =
485 new HashSet<EmailFieldDetails> (
486 (GLib.HashFunc) EmailFieldDetails.hash,
487 (GLib.EqualFunc) EmailFieldDetails.equal);
488 private Set<EmailFieldDetails> _email_addresses_ro;
493 [CCode (notify = false)]
494 public Set<EmailFieldDetails> email_addresses
496 get { return this._email_addresses_ro; }
497 set { this.change_email_addresses.begin (value); } /* not writeable */
500 private HashSet<RoleFieldDetails> _roles = new HashSet<RoleFieldDetails> (
501 (GLib.HashFunc) RoleFieldDetails.hash,
502 (GLib.EqualFunc) RoleFieldDetails.equal);
503 private Set<RoleFieldDetails> _roles_ro;
508 [CCode (notify = false)]
509 public Set<RoleFieldDetails> roles
511 get { return this._roles_ro; }
512 set { this.change_roles.begin (value); } /* not writeable */
515 private HashSet<string> _local_ids = new HashSet<string> ();
516 private Set<string> _local_ids_ro;
521 [CCode (notify = false)]
522 public Set<string> local_ids
524 get { return this._local_ids_ro; }
525 set { this.change_local_ids.begin (value); } /* not writeable */
528 private DateTime? _birthday = null;
533 [CCode (notify = false)]
534 public DateTime? birthday
536 get { return this._birthday; }
537 set { this.change_birthday.begin (value); } /* not writeable */
540 private string? _calendar_event_id = null;
545 [CCode (notify = false)]
546 public string? calendar_event_id
548 get { return this._calendar_event_id; }
549 set { this.change_calendar_event_id.begin (value); } /* not writeable */
552 private HashSet<NoteFieldDetails> _notes = new HashSet<NoteFieldDetails> (
553 (GLib.HashFunc) NoteFieldDetails.hash,
554 (GLib.EqualFunc) NoteFieldDetails.equal);
555 private Set<NoteFieldDetails> _notes_ro;
560 [CCode (notify = false)]
561 public Set<NoteFieldDetails> notes
563 get { return this._notes_ro; }
564 set { this.change_notes.begin (value); } /* not writeable */
567 private HashSet<PostalAddressFieldDetails> _postal_addresses =
568 new HashSet<PostalAddressFieldDetails> (
569 (GLib.HashFunc) PostalAddressFieldDetails.hash,
570 (GLib.EqualFunc) PostalAddressFieldDetails.equal);
571 private Set<PostalAddressFieldDetails> _postal_addresses_ro;
576 [CCode (notify = false)]
577 public Set<PostalAddressFieldDetails> postal_addresses
579 get { return this._postal_addresses_ro; }
580 set { this.change_postal_addresses.begin (value); } /* not writeable */
583 private bool _is_favourite = false;
586 * Whether this Individual is a user-defined favourite.
588 * This property is `true` if any of this Individual's {@link Persona}s are
591 [CCode (notify = false)]
592 public bool is_favourite
594 get { return this._is_favourite; }
595 set { this.change_is_favourite.begin (value); }
603 public async void change_is_favourite (bool is_favourite) throws PropertyError
605 if (this._is_favourite == is_favourite)
610 debug ("Setting '%s' favourite status to %s…", this.id,
611 is_favourite ? "TRUE" : "FALSE");
613 PropertyError? persona_error = null;
614 var is_favourite_changed = false;
616 /* Try to write it to only the Personas which have "is-favourite" as a
617 * writeable property.
619 * NOTE: We don't check whether the persona's store is writeable, as we
620 * want is-favourite status to propagate to all stores, if possible. This
621 * is one property which is harmless to propagate. */
622 foreach (var p in this._persona_set)
624 var a = p as FavouriteDetails;
625 if (a != null && "is-favourite" in p.writeable_properties)
629 yield a.change_is_favourite (is_favourite);
630 debug (" written to persona '%s'", p.uid);
631 is_favourite_changed = true;
633 catch (PropertyError e)
635 /* Store the first error so we can throw it if setting the
636 * property fails on every other persona. */
637 if (persona_error == null)
646 if (is_favourite_changed == false)
648 assert (persona_error != null);
652 /* Update our copy of the property. */
653 this._is_favourite = is_favourite;
654 this.notify_property ("is-favourite");
657 private HashSet<string> _groups = new HashSet<string> ();
658 private Set<string> _groups_ro;
663 [CCode (notify = false)]
664 public Set<string> groups
666 get { return this._groups_ro; }
667 set { this.change_groups.begin (value); }
675 public async void change_groups (Set<string> groups) throws PropertyError
677 debug ("Setting '%s' groups…", this.id);
679 PropertyError? persona_error = null;
680 var groups_changed = false;
682 /* Try to write it to only the Personas which have "groups" as a
683 * writeable property. */
684 foreach (var p in this._persona_set)
686 var g = p as GroupDetails;
687 if (g != null && "groups" in p.writeable_properties)
691 yield g.change_groups (groups);
692 debug (" written to persona '%s'", p.uid);
693 groups_changed = true;
695 catch (PropertyError e)
697 /* Store the first error so we can throw it if setting the
698 * property fails on every other persona. */
699 if (persona_error == null)
708 if (groups_changed == false)
710 assert (persona_error != null);
714 /* Update our copy of the property. */
715 this._update_groups ();
718 private HashMultiMap<string, ImFieldDetails> _im_addresses =
719 new HashMultiMap<string, ImFieldDetails> (
720 null, null, ImFieldDetails.hash, (EqualFunc) ImFieldDetails.equal);
725 [CCode (notify = false)]
726 public MultiMap<string, ImFieldDetails> im_addresses
728 get { return this._im_addresses; }
729 set { this.change_im_addresses.begin (value); } /* not writeable */
732 private HashMultiMap<string, WebServiceFieldDetails> _web_service_addresses =
733 new HashMultiMap<string, WebServiceFieldDetails> (null, null,
734 (GLib.HashFunc) WebServiceFieldDetails.hash,
735 (GLib.EqualFunc) WebServiceFieldDetails.equal);
740 [CCode (notify = false)]
741 public MultiMap<string, WebServiceFieldDetails> web_service_addresses
743 get { return this._web_service_addresses; }
745 set { this.change_web_service_addresses.begin (value); }
749 * The set of {@link Persona}s encapsulated by this Individual.
751 * No order is specified over the set of personas, as such an order may be
752 * different across each of the properties implemented by the personas (e.g.
753 * should they be ordered by presence, name, star sign, etc.?).
755 * Changing the set of personas may cause updates to the aggregated properties
756 * provided by the Individual, resulting in property notifications for them.
758 * Changing the set of personas will not cause permanent linking/unlinking of
759 * the added/removed personas to/from this Individual. To do that, call
760 * {@link IndividualAggregator.link_personas} or
761 * {@link IndividualAggregator.unlink_individual}, which will ensure the link
762 * changes are written to the appropriate backend.
766 public Set<Persona> personas
768 get { return this._persona_set_ro; }
769 set { this._set_personas (value, null); }
773 * Emitted when one or more {@link Persona}s are added to or removed from
774 * the Individual. As the parameters are (unordered) sets, the orders of their
775 * elements are undefined.
777 * @param added a set of {@link Persona}s which have been added
778 * @param removed a set of {@link Persona}s which have been removed
782 public signal void personas_changed (Set<Persona> added,
783 Set<Persona> removed);
785 private void _notify_alias_cb (Object obj, ParamSpec ps)
787 this._update_alias ();
790 private void _notify_avatar_cb (Object obj, ParamSpec ps)
792 this._update_avatar ();
795 private void _notify_full_name_cb ()
797 this._update_full_name ();
800 private void _notify_structured_name_cb ()
802 this._update_structured_name ();
805 private void _notify_nickname_cb ()
807 this._update_nickname ();
810 private void _persona_group_changed_cb (string group, bool is_member)
812 this._update_groups ();
815 private void _notify_gender_cb ()
817 this._update_gender ();
820 private void _notify_urls_cb ()
822 this._update_urls ();
825 private void _notify_phone_numbers_cb ()
827 this._update_phone_numbers ();
830 private void _notify_postal_addresses_cb ()
832 this._update_postal_addresses ();
835 private void _notify_email_addresses_cb ()
837 this._update_email_addresses ();
840 private void _notify_roles_cb ()
842 this._update_roles ();
845 private void _notify_birthday_cb ()
847 this._update_birthday ();
850 private void _notify_notes_cb ()
852 this._update_notes ();
855 private void _notify_local_ids_cb ()
857 this._update_local_ids ();
861 * Add or remove the Individual from the specified group.
863 * If `is_member` is `true`, the Individual will be added to the `group`. If
864 * it is `false`, they will be removed from the `group`.
866 * The group membership change will propagate to every {@link Persona} in
869 * @param group a freeform group identifier
870 * @param is_member whether the Individual should be a member of the group
873 public async void change_group (string group, bool is_member)
875 foreach (var p in this._persona_set)
877 if (p is GroupDetails)
878 ((GroupDetails) p).change_group.begin (group, is_member);
881 /* don't notify, since it hasn't happened in the persona backing stores
882 * yet; react to that directly */
885 private void _notify_presence_cb (Object obj, ParamSpec ps)
887 this._update_presence ();
890 private void _notify_im_addresses_cb (Object obj, ParamSpec ps)
892 this._update_im_addresses ();
895 private void _notify_web_service_addresses_cb (Object obj, ParamSpec ps)
897 this._update_web_service_addresses ();
900 private void _notify_is_favourite_cb (Object obj, ParamSpec ps)
902 this._update_is_favourite ();
906 * Create a new Individual.
908 * The Individual can optionally be seeded with the {@link Persona}s in
909 * `personas`. Otherwise, it will have to have personas added using the
910 * {@link Folks.Individual.personas} property after construction.
912 * @param personas a list of {@link Persona}s to initialise the
913 * {@link Individual} with, or `null`
914 * @return a new Individual
918 public Individual (Set<Persona>? personas)
920 Object (personas: personas);
925 debug ("Creating new Individual with %u Personas: %p",
926 (this.personas != null ? this.personas.size : 0), this);
928 this._persona_set_ro = this._persona_set.read_only_view;
929 this._urls_ro = this._urls.read_only_view;
930 this._phone_numbers_ro = this._phone_numbers.read_only_view;
931 this._email_addresses_ro = this._email_addresses.read_only_view;
932 this._roles_ro = this._roles.read_only_view;
933 this._local_ids_ro = this._local_ids.read_only_view;
934 this._postal_addresses_ro = this._postal_addresses.read_only_view;
935 this._notes_ro = this._notes.read_only_view;
936 this._groups_ro = this._groups.read_only_view;
941 debug ("Destroying Individual '%s': %p", this.id, this);
944 /* Emit the personas-changed signal, turning null parameters into empty sets
945 * and ensuring that the signal is emitted with read-only views of the sets
946 * so that signal handlers can't modify the sets. */
947 private void _emit_personas_changed (Set<Persona>? added,
948 Set<Persona>? removed)
951 var _removed = removed;
953 if ((added == null || added.size == 0) &&
954 (removed == null || removed.size == 0))
956 /* Emitting it with no added or removed personas is pointless */
959 else if (added == null)
961 _added = new HashSet<Persona> ();
963 else if (removed == null)
965 _removed = new HashSet<Persona> ();
968 this.personas_changed (_added.read_only_view, _removed.read_only_view);
971 private void _store_removed_cb (PersonaStore store)
973 var remaining_personas = new HashSet<Persona> ();
975 /* Build a set of the remaining personas (those which weren't in the
977 foreach (var persona in this._persona_set)
979 if (persona.store != store)
981 remaining_personas.add (persona);
985 this._set_personas (remaining_personas, null);
988 private void _store_personas_changed_cb (PersonaStore store,
990 Set<Persona> removed,
993 GroupDetails.ChangeReason reason)
995 var remaining_personas = new HashSet<Persona> ();
997 /* Build a set of the remaining personas (those which aren't in the
998 * set of removed personas). */
999 foreach (var persona in this._persona_set)
1001 if (!removed.contains (persona))
1003 remaining_personas.add (persona);
1007 this._set_personas (remaining_personas, null);
1010 private void _update_fields ()
1012 this._update_groups ();
1013 this._update_presence ();
1014 this._update_is_favourite ();
1015 this._update_avatar ();
1016 this._update_alias ();
1017 this._update_trust_level ();
1018 this._update_im_addresses ();
1019 this._update_web_service_addresses ();
1020 this._update_structured_name ();
1021 this._update_full_name ();
1022 this._update_nickname ();
1023 this._update_gender ();
1024 this._update_urls ();
1025 this._update_phone_numbers ();
1026 this._update_email_addresses ();
1027 this._update_roles ();
1028 this._update_birthday ();
1029 this._update_notes ();
1030 this._update_postal_addresses ();
1031 this._update_local_ids ();
1034 /* Delegate to update the value of a property on this individual from the
1035 * given chosen persona. The chosen_persona may be null, in which case we have
1036 * to set a default value.
1038 * Used in _update_single_valued_property(), below. */
1039 private delegate void SingleValuedPropertySetter (Persona? chosen_persona);
1041 /* Delegate to filter a persona based on whether a given property is set.
1043 * Used in _update_single_valued_property(), below. */
1044 private delegate bool PropertyFilter (Persona persona);
1047 * Update a single-valued property from the values in the personas.
1049 * Single-valued properties are ones such as {@link Individual.alias} or
1050 * {@link Individual.gender} — as opposed to multi-valued ones (which are
1051 * generally sets) such as {@link Individual.im_addresses} or
1052 * {@link Individual.groups}.
1054 * This function uses the given comparison function to order the personas in
1055 * this individual, with the highest-positioned persona (the “greatest”
1056 * persona in the total order) finally being passed to the setter function to
1057 * use in updating the individual's value for the given property. i.e. If
1058 * `compare_func(a, b)` is called and returns > 0, persona `a` will be passed
1061 * At a level above `compare_func`, the function always prefers personas from
1062 * the primary store (see {@link IndividualAggregator.primary_store}) over
1063 * those which aren't.
1065 * Note that if a suitable persona isn't found in the individual (if, for
1066 * example, no personas in the individual implement the desired interface),
1067 * `null` will be passed to `setter`, which should then set the individual's
1068 * property to a default value.
1070 * @param interface_type the type of interface which all personas under
1071 * consideration must implement ({@link Persona} to select all personas)
1072 * @param compare_func comparison function to order personas for selection
1073 * @param prop_name name of the property being set, as used in
1074 * {@link Persona.writeable_properties}
1075 * @param setter function to update the individual with the chosen value
1078 private void _update_single_valued_property (Type interface_type,
1079 PropertyFilter filter_func,
1080 CompareFunc<Persona> compare_func, string prop_name,
1081 SingleValuedPropertySetter setter)
1083 CompareDataFunc<Persona> primary_compare_func = (a, b) =>
1088 /* Always prefer values which are set over those which aren't. */
1089 var a_is_set = filter_func (a);
1090 var b_is_set = filter_func (b);
1092 if (a_is_set != b_is_set)
1094 return (a_is_set ? 1 : 0) - (b_is_set ? 1 : 0);
1097 var a_is_primary = a.store.is_primary_store;
1098 var b_is_primary = b.store.is_primary_store;
1100 if (a_is_primary != b_is_primary)
1102 return (a_is_primary ? 1 : 0) - (b_is_primary ? 1 : 0);
1105 /* If both personas have the same is-primary value, prefer personas
1106 * which have the given property as writeable over those which
1108 var a_is_writeable = (prop_name in a.writeable_properties);
1109 var b_is_writeable = (prop_name in b.writeable_properties);
1111 if (a_is_writeable != b_is_writeable)
1113 return (a_is_writeable ? 1 : 0) - (b_is_writeable ? 1 : 0);
1116 /* If both personas have the same writeability for this property, fall
1117 * back to the given comparison function. If the comparison function
1118 * gives them an equal order, we use the personas' UIDs to ensure that
1119 * we end up with a total order over all personas in the individual
1120 * (otherwise we might end up with unstable property values). */
1121 var order = compare_func (a, b);
1125 order = strcmp (a.uid, b.uid);
1131 Persona? candidate_p = null;
1133 foreach (var p in this._persona_set)
1135 /* We only care about personas implementing the given interface. */
1136 if (p.get_type ().is_a (interface_type))
1138 if (candidate_p == null ||
1139 primary_compare_func (p, candidate_p) > 0)
1146 /* Update the property with the values from the best candidate persona we
1147 * found. Note that it's possible for candidate_p to be null if (e.g.)
1148 * none of this._persona_set implemented the interface. */
1149 setter (candidate_p);
1152 private void _update_groups ()
1154 var new_groups = new HashSet<string> ();
1156 /* FIXME: this should partition the personas by store (maybe we should
1157 * keep that mapping in general in this class), and execute
1158 * "groups-changed" on the store (with the set of personas), to allow the
1159 * back-end to optimize it (like Telepathy will for MembersChanged for the
1160 * groups channel list) */
1161 foreach (var p in this._persona_set)
1163 if (p is GroupDetails)
1165 var persona = (GroupDetails) p;
1167 foreach (var group in persona.groups)
1169 new_groups.add (group);
1174 foreach (var group in new_groups)
1176 if (!this._groups.contains (group))
1178 this._groups.add (group);
1179 foreach (var g in this._groups)
1184 this.group_changed (group, true);
1188 /* buffer the removals, so we don't remove while iterating */
1189 var removes = new GLib.List<string> ();
1190 foreach (var group in this._groups)
1192 if (!new_groups.contains (group))
1193 removes.prepend (group);
1196 removes.foreach ((l) =>
1198 unowned string group = (string) l;
1199 this._groups.remove (group);
1200 this.group_changed (group, false);
1204 private void _update_presence ()
1206 this._update_single_valued_property (typeof (PresenceDetails), (p) =>
1208 return (p as PresenceDetails).presence_type != PresenceType.UNSET;
1211 var a_presence = (a as PresenceDetails).presence_type;
1212 var b_presence = (b as PresenceDetails).presence_type;
1214 return PresenceDetails.typecmp (a_presence, b_presence);
1215 }, "presence", (p) =>
1217 var presence_message = ""; /* must not be null */
1218 var presence_status = ""; /* must not be null */
1219 var presence_type = Folks.PresenceType.UNSET;
1223 presence_type = (p as PresenceDetails).presence_type;
1224 presence_message = (p as PresenceDetails).presence_message;
1225 presence_status = (p as PresenceDetails).presence_status;
1228 /* Only notify if any of the values have changed. */
1229 if (this.presence_type != presence_type ||
1230 this.presence_message != presence_message ||
1231 this.presence_status != presence_status)
1233 this.freeze_notify ();
1234 this.presence_message = presence_message;
1235 this.presence_type = presence_type;
1236 this.presence_status = presence_status;
1237 this.thaw_notify ();
1242 private void _update_is_favourite ()
1244 this._update_single_valued_property (typeof (FavouriteDetails), (p) =>
1249 var a_is_favourite = (a as FavouriteDetails).is_favourite;
1250 var b_is_favourite = (b as FavouriteDetails).is_favourite;
1252 return ((a_is_favourite == true) ? 1 : 0) -
1253 ((b_is_favourite == true) ? 1 : 0);
1254 }, "is-favourite", (p) =>
1256 var favourite = false;
1260 favourite = (p as FavouriteDetails).is_favourite;
1263 /* Only notify if the value has changed. We have to set the private
1264 * member and notify manually, or we'd end up propagating the new
1265 * favourite status back down to all our Personas. */
1266 if (this._is_favourite != favourite)
1268 this._is_favourite = favourite;
1269 this.notify_property ("is-favourite");
1274 private void _update_alias ()
1276 this._update_single_valued_property (typeof (AliasDetails), (p) =>
1278 var alias = (p as AliasDetails).alias;
1279 assert (alias != null);
1281 return (alias.strip () != ""); /* empty aliases are unset */
1284 var a_alias = (a as AliasDetails).alias;
1285 var b_alias = (b as AliasDetails).alias;
1287 assert (a_alias != null);
1288 assert (b_alias != null);
1290 var a_is_empty = (a_alias.strip () == "") ? 1 : 0;
1291 var b_is_empty = (b_alias.strip () == "") ? 1 : 0;
1293 /* We prefer to not have an alias which is the same as the Persona's
1294 * display-id, since having such an alias implies that it's the
1295 * default. However, we prefer using such an alias to using the
1296 * Persona's UID, which is our ultimate fallback (below). */
1297 var a_is_display_id = (a_alias == a.display_id) ? 1 : 0;
1298 var b_is_display_id = (b_alias == b.display_id) ? 1 : 0;
1300 return (b_is_empty + b_is_display_id) -
1301 (a_is_empty + a_is_display_id);
1304 string alias = ""; /* must not be null */
1308 alias = (p as AliasDetails).alias.strip ();
1311 /* Only notify if the value has changed. We have to set the private
1312 * member and notify manually, or we'd end up propagating the new
1313 * alias back down to all our Personas, even if it's a fallback
1314 * display ID or something else undesirable. */
1315 if (this._alias != alias)
1317 this._alias = alias;
1318 this.notify_property ("alias");
1323 private void _update_avatar ()
1325 this._update_single_valued_property (typeof (AvatarDetails), (p) =>
1327 return (p as AvatarDetails).avatar != null;
1330 /* We can't compare two set avatars efficiently. See: bgo#652721. */
1334 LoadableIcon? avatar = null;
1338 avatar = (p as AvatarDetails).avatar;
1341 /* only notify if the value has changed */
1342 if ((this._avatar == null && avatar != null) ||
1343 (this._avatar != null &&
1344 (avatar == null || !this._avatar.equal (avatar))))
1346 this._avatar = avatar;
1347 this.notify_property ("avatar");
1352 private void _update_trust_level ()
1354 var trust_level = TrustLevel.PERSONAS;
1356 foreach (var p in this._persona_set)
1358 if (p.is_user == false &&
1359 p.store.trust_level == PersonaStoreTrust.NONE)
1360 trust_level = TrustLevel.NONE;
1363 /* Only notify if the value has changed */
1364 if (this.trust_level != trust_level)
1365 this.trust_level = trust_level;
1368 private void _update_im_addresses ()
1370 /* populate the IM addresses as the union of our Personas' addresses */
1371 this._im_addresses.clear ();
1373 foreach (var persona in this._persona_set)
1375 if (persona is ImDetails)
1377 var im_details = (ImDetails) persona;
1378 foreach (var cur_protocol in im_details.im_addresses.get_keys ())
1381 im_details.im_addresses.get (cur_protocol);
1383 foreach (var address in cur_addresses)
1385 this._im_addresses.set (cur_protocol, address);
1390 this.notify_property ("im-addresses");
1393 private void _update_web_service_addresses ()
1395 /* populate the web service addresses as the union of our Personas' addresses */
1396 this._web_service_addresses.clear ();
1398 foreach (var persona in this.personas)
1400 if (persona is WebServiceDetails)
1402 var web_service_details = (WebServiceDetails) persona;
1403 foreach (var cur_web_service in
1404 web_service_details.web_service_addresses.get_keys ())
1407 web_service_details.web_service_addresses.get (
1410 foreach (var ws_fd in cur_addresses)
1411 this._web_service_addresses.set (cur_web_service, ws_fd);
1415 this.notify_property ("web-service-addresses");
1418 private void _connect_to_persona (Persona persona)
1420 persona.individual = this;
1422 persona.notify["alias"].connect (this._notify_alias_cb);
1423 persona.notify["avatar"].connect (this._notify_avatar_cb);
1424 persona.notify["presence-message"].connect (this._notify_presence_cb);
1425 persona.notify["presence-type"].connect (this._notify_presence_cb);
1426 persona.notify["im-addresses"].connect (this._notify_im_addresses_cb);
1427 persona.notify["web-service-addresses"].connect
1428 (this._notify_web_service_addresses_cb);
1429 persona.notify["is-favourite"].connect (this._notify_is_favourite_cb);
1430 persona.notify["structured-name"].connect (
1431 this._notify_structured_name_cb);
1432 persona.notify["full-name"].connect (this._notify_full_name_cb);
1433 persona.notify["nickname"].connect (this._notify_nickname_cb);
1434 persona.notify["gender"].connect (this._notify_gender_cb);
1435 persona.notify["urls"].connect (this._notify_urls_cb);
1436 persona.notify["phone-numbers"].connect (this._notify_phone_numbers_cb);
1437 persona.notify["email-addresses"].connect (
1438 this._notify_email_addresses_cb);
1439 persona.notify["roles"].connect (this._notify_roles_cb);
1440 persona.notify["birthday"].connect (this._notify_birthday_cb);
1441 persona.notify["notes"].connect (this._notify_notes_cb);
1442 persona.notify["postal-addresses"].connect
1443 (this._notify_postal_addresses_cb);
1444 persona.notify["local-ids"].connect
1445 (this._notify_local_ids_cb);
1448 if (persona is GroupDetails)
1450 ((GroupDetails) persona).group_changed.connect (
1451 this._persona_group_changed_cb);
1455 private void _update_structured_name ()
1457 this._update_single_valued_property (typeof (NameDetails), (p) =>
1459 var name = (p as NameDetails).structured_name;
1460 return (name != null && !name.is_empty ());
1463 /* Can't compare two set names. */
1465 }, "structured-name", (p) =>
1467 StructuredName? name = null;
1471 name = (p as NameDetails).structured_name;
1473 if (name != null && name.is_empty ())
1479 if ((this._structured_name == null && name != null) ||
1480 (this._structured_name != null &&
1481 (name == null || !this._structured_name.equal (name))))
1483 this._structured_name = name;
1484 this.notify_property ("structured-name");
1489 private void _update_full_name ()
1491 this._update_single_valued_property (typeof (NameDetails), (p) =>
1493 var name = (p as NameDetails).full_name;
1494 assert (name != null);
1496 return (name.strip () != ""); /* empty names are unset */
1499 /* Can't compare two set names. */
1501 }, "full-name", (p) =>
1503 string new_full_name = ""; /* must not be null */
1507 new_full_name = (p as NameDetails).full_name.strip ();
1510 if (new_full_name != this._full_name)
1512 this._full_name = new_full_name;
1513 this.notify_property ("full-name");
1518 private void _update_nickname ()
1520 this._update_single_valued_property (typeof (NameDetails), (p) =>
1522 var nickname = (p as NameDetails).nickname;
1523 assert (nickname != null);
1525 return (nickname.strip () != ""); /* empty names are unset */
1528 /* Can't compare two set names. */
1530 }, "nickname", (p) =>
1532 string new_nickname = ""; /* must not be null */
1536 new_nickname = (p as NameDetails).nickname.strip ();
1539 if (new_nickname != this._nickname)
1541 this._nickname = new_nickname;
1542 this.notify_property ("nickname");
1547 private void _disconnect_from_persona (Persona persona,
1548 Individual? replacement_individual)
1550 persona.notify["alias"].disconnect (this._notify_alias_cb);
1551 persona.notify["avatar"].disconnect (this._notify_avatar_cb);
1552 persona.notify["presence-message"].disconnect (
1553 this._notify_presence_cb);
1554 persona.notify["presence-type"].disconnect (this._notify_presence_cb);
1555 persona.notify["im-addresses"].disconnect (
1556 this._notify_im_addresses_cb);
1557 persona.notify["web-service-addresses"].disconnect (
1558 this._notify_web_service_addresses_cb);
1559 persona.notify["is-favourite"].disconnect (
1560 this._notify_is_favourite_cb);
1561 persona.notify["structured-name"].disconnect (
1562 this._notify_structured_name_cb);
1563 persona.notify["full-name"].disconnect (this._notify_full_name_cb);
1564 persona.notify["nickname"].disconnect (this._notify_nickname_cb);
1565 persona.notify["gender"].disconnect (this._notify_gender_cb);
1566 persona.notify["urls"].disconnect (this._notify_urls_cb);
1567 persona.notify["phone-numbers"].disconnect (
1568 this._notify_phone_numbers_cb);
1569 persona.notify["email-addresses"].disconnect (
1570 this._notify_email_addresses_cb);
1571 persona.notify["roles"].disconnect (this._notify_roles_cb);
1572 persona.notify["birthday"].disconnect (this._notify_birthday_cb);
1573 persona.notify["notes"].disconnect (this._notify_notes_cb);
1574 persona.notify["postal-addresses"].disconnect
1575 (this._notify_postal_addresses_cb);
1576 persona.notify["local-ids"].disconnect (this._notify_local_ids_cb);
1579 if (persona is GroupDetails)
1581 ((GroupDetails) persona).group_changed.disconnect (
1582 this._persona_group_changed_cb);
1585 /* Don't update the individual if the persona's been added to the new one
1586 * already (and thus the new individual has already changed
1587 * persona.individual).
1589 * FIXME: Ideally, we'd assert that a persona can't be added to a new
1590 * individual before it's removed from the old one. However, this
1591 * currently isn't possible due to the way the aggregator works. When the
1592 * aggregator's rewritten, it would be nice to fix this. */
1593 if (persona.individual == this)
1595 /* It may be the case that the persona's being removed from the
1596 * individual (i.e. the replacement individual is non-null, but
1597 * doesn't contain this persona). In this case, we need to set the
1598 * persona's individual to null. */
1599 if (replacement_individual != null &&
1600 persona in replacement_individual.personas)
1602 persona.individual = replacement_individual;
1606 persona.individual = null;
1611 private void _update_gender ()
1613 this._update_single_valued_property (typeof (GenderDetails), (p) =>
1615 return (p as GenderDetails).gender != Gender.UNSPECIFIED;
1618 /* It would be sexist to rank one gender over another.
1619 * Besides, how often will we see two personas in the same individual
1620 * which have different genders? */
1624 var new_gender = Gender.UNSPECIFIED;
1628 new_gender = (p as GenderDetails).gender;
1631 if (new_gender != this.gender)
1633 this._gender = new_gender;
1634 this.notify_property ("gender");
1639 private void _update_urls ()
1641 /* Populate the URLs as the union of our Personas' URLs.
1642 * If the same URL exists multiple times we merge the parameters. */
1643 var urls_set = new HashMap<unowned string, unowned UrlFieldDetails> (
1644 null, null, (GLib.EqualFunc) UrlFieldDetails.equal);
1646 this._urls.clear ();
1648 foreach (var persona in this._persona_set)
1650 var url_details = persona as UrlDetails;
1651 if (url_details != null)
1653 foreach (var url_fd in url_details.urls)
1655 if (url_fd.value == null)
1658 var existing = urls_set.get (url_fd.value);
1659 if (existing != null)
1660 existing.extend_parameters (url_fd.parameters);
1663 var new_url_fd = new UrlFieldDetails (url_fd.value);
1664 new_url_fd.extend_parameters (url_fd.parameters);
1665 urls_set.set (url_fd.value, new_url_fd);
1666 this._urls.add (new_url_fd);
1672 this.notify_property ("urls");
1675 private void _update_phone_numbers ()
1677 /* Populate the phone numbers as the union of our Personas' numbers
1678 * If the same number exists multiple times we merge the parameters. */
1679 var phone_numbers_set =
1680 new HashMap<unowned string, unowned PhoneFieldDetails> (
1681 null, null, (GLib.EqualFunc) PhoneFieldDetails.equal);
1683 this._phone_numbers.clear ();
1685 foreach (var persona in this._persona_set)
1687 var phone_details = persona as PhoneDetails;
1688 if (phone_details != null)
1690 foreach (var phone_fd in phone_details.phone_numbers)
1692 if (phone_fd.value == null)
1695 var existing = phone_numbers_set.get (phone_fd.value);
1696 if (existing != null)
1697 existing.extend_parameters (phone_fd.parameters);
1700 var new_fd = new PhoneFieldDetails (phone_fd.value);
1701 new_fd.extend_parameters (phone_fd.parameters);
1702 phone_numbers_set.set (phone_fd.value, new_fd);
1703 this._phone_numbers.add (new_fd);
1709 this.notify_property ("phone-numbers");
1712 private void _update_email_addresses ()
1714 /* Populate the email addresses as the union of our Personas' addresses.
1715 * If the same address exists multiple times we merge the parameters. */
1716 var emails_set = new HashMap<unowned string, unowned EmailFieldDetails> (
1717 null, null, (GLib.EqualFunc) EmailFieldDetails.equal);
1719 this._email_addresses.clear ();
1721 foreach (var persona in this._persona_set)
1723 var email_details = persona as EmailDetails;
1724 if (email_details != null)
1726 foreach (var email_fd in email_details.email_addresses)
1728 if (email_fd.value == null)
1731 var existing = emails_set.get (email_fd.value);
1732 if (existing != null)
1733 existing.extend_parameters (email_fd.parameters);
1736 var new_email_fd = new EmailFieldDetails (email_fd.value,
1737 email_fd.parameters);
1738 emails_set.set (email_fd.value, new_email_fd);
1739 this._email_addresses.add (new_email_fd);
1745 this.notify_property ("email-addresses");
1748 private void _update_roles ()
1750 this._roles.clear ();
1752 foreach (var persona in this._persona_set)
1754 var role_details = persona as RoleDetails;
1755 if (role_details != null)
1757 foreach (var role_fd in role_details.roles)
1759 this._roles.add (role_fd);
1764 this.notify_property ("roles");
1767 private void _update_local_ids ()
1769 this._local_ids.clear ();
1771 foreach (var persona in this._persona_set)
1773 var local_ids_details = persona as LocalIdDetails;
1774 if (local_ids_details != null)
1776 foreach (var id in local_ids_details.local_ids)
1778 this._local_ids.add (id);
1783 this.notify_property ("local-ids");
1786 private void _update_postal_addresses ()
1788 this._postal_addresses.clear ();
1790 /* FIXME: Detect duplicates somehow? */
1791 foreach (var persona in this._persona_set)
1793 var address_details = persona as PostalAddressDetails;
1794 if (address_details != null)
1796 foreach (var pafd in address_details.postal_addresses)
1797 this._postal_addresses.add (pafd);
1801 this.notify_property ("postal-addresses");
1804 private void _update_birthday ()
1806 this._update_single_valued_property (typeof (BirthdayDetails), (p) =>
1808 var details = (p as BirthdayDetails);
1809 return details.birthday != null && details.calendar_event_id != null;
1812 var a_birthday = (a as BirthdayDetails).birthday;
1813 var b_birthday = (b as BirthdayDetails).birthday;
1814 var a_event_id = (a as BirthdayDetails).calendar_event_id;
1815 var b_event_id = (b as BirthdayDetails).calendar_event_id;
1817 var a_birthday_is_set = (a_birthday != null) ? 1 : 0;
1818 var b_birthday_is_set = (b_birthday != null) ? 1 : 0;
1820 /* We consider the empty string as “set” because it's an opaque ID. */
1821 var a_event_id_is_set = (a_event_id != null) ? 1 : 0;
1822 var b_event_id_is_set = (b_event_id != null) ? 1 : 0;
1824 /* Prefer personas which have both properties set over those who have
1825 * only one set. We don't consider the case where the birthdays from
1826 * different personas don't match, because that's just scary. */
1827 return (a_birthday_is_set + a_event_id_is_set) -
1828 (b_birthday_is_set + b_event_id_is_set);
1829 }, "birthday", (p) =>
1831 unowned DateTime? bday = null;
1832 unowned string? calendar_event_id = null;
1836 bday = (p as BirthdayDetails).birthday;
1837 calendar_event_id = (p as BirthdayDetails).calendar_event_id;
1840 if ((this._birthday == null && bday != null) ||
1841 (this._birthday != null &&
1842 (bday == null || !this._birthday.equal (bday))) ||
1843 (this._calendar_event_id != calendar_event_id))
1845 this._birthday = bday;
1846 this._calendar_event_id = calendar_event_id;
1848 this.freeze_notify ();
1849 this.notify_property ("birthday");
1850 this.notify_property ("calendar-event-id");
1851 this.thaw_notify ();
1856 private void _update_notes ()
1858 this._notes.clear ();
1860 foreach (var persona in this._persona_set)
1862 var note_details = persona as NoteDetails;
1863 if (note_details != null)
1865 foreach (var n in note_details.notes)
1867 this._notes.add (n);
1872 this.notify_property ("notes");
1875 private void _set_personas (Set<Persona>? personas,
1876 Individual? replacement_individual)
1878 assert (replacement_individual == null || replacement_individual != this);
1880 var added = new HashSet<Persona> ();
1881 var removed = new HashSet<Persona> ();
1883 /* Determine which Personas have been added. If personas == null, we
1884 * assume it's an empty set. */
1885 if (personas != null)
1887 foreach (var p in personas)
1889 if (!this._persona_set.contains (p))
1891 /* Keep track of how many Personas are users */
1893 this._persona_user_count++;
1897 this._persona_set.add (p);
1898 this._connect_to_persona (p);
1900 /* Increment the Persona count for this PersonaStore */
1901 var store = p.store;
1902 var num_from_store = this._stores.get (store);
1903 if (num_from_store == 0)
1905 this._stores.set (store, num_from_store + 1);
1909 this._stores.set (store, 1);
1911 store.removed.connect (this._store_removed_cb);
1912 store.personas_changed.connect (
1913 this._store_personas_changed_cb);
1919 /* Determine which Personas have been removed */
1920 var iter = this._persona_set.iterator ();
1921 while (iter.next ())
1923 var p = iter.get ();
1925 if (personas == null || !personas.contains (p))
1927 /* Keep track of how many Personas are users */
1929 this._persona_user_count--;
1933 /* Decrement the Persona count for this PersonaStore */
1934 var store = p.store;
1935 var num_from_store = this._stores.get (store);
1936 if (num_from_store > 1)
1938 this._stores.set (store, num_from_store - 1);
1942 store.removed.disconnect (this._store_removed_cb);
1943 store.personas_changed.disconnect (
1944 this._store_personas_changed_cb);
1946 this._stores.unset (store);
1949 this._disconnect_from_persona (p, replacement_individual);
1954 this._emit_personas_changed (added, removed);
1956 /* Update this.is_user */
1957 var new_is_user = (this._persona_user_count > 0) ? true : false;
1958 if (new_is_user != this.is_user)
1959 this.is_user = new_is_user;
1961 /* If all the Personas have been removed, remove the Individual */
1962 if (this._persona_set.size < 1)
1964 this.removed (replacement_individual);
1968 /* Update the ID. We choose the most interesting Persona in the
1969 * Individual and hash their UID. This is guaranteed to be globally
1970 * unique, and may not change (for one of the two Individuals) if we link
1971 * two Individuals together, which is nice though we can't rely on this
1974 * This method of constructing an ID ensures that it'll be unique and
1975 * stable for a given Individual once the IndividualAggregator reaches
1976 * a quiescent state after startup. It guarantees that the ID will be
1977 * the same every time folks is used, until the Individual is linked
1978 * or unlinked to another Individual.
1980 * We choose the most interesting Persona by ranking all the Personas
1981 * in the Individual by:
1982 * 1. store.is-primary-store
1983 * 2. store.trust-level
1984 * 3. store.id (alphabetically)
1986 * Note that this heuristic shouldn't be changed without careful thought,
1987 * since stored references to IDs may be broken by the change.
1989 if (this._persona_set.size > 0)
1991 Persona? chosen_persona = null;
1993 foreach (var persona in this._persona_set)
1995 if (chosen_persona == null ||
1996 (chosen_persona.store.is_primary_store == false &&
1997 persona.store.is_primary_store == true) ||
1998 (chosen_persona.store.is_primary_store ==
1999 persona.store.is_primary_store &&
2000 chosen_persona.store.trust_level >
2001 persona.store.trust_level) ||
2002 (chosen_persona.store.is_primary_store ==
2003 persona.store.is_primary_store &&
2004 chosen_persona.store.trust_level ==
2005 persona.store.trust_level &&
2006 chosen_persona.store.id > persona.store.id)
2009 chosen_persona = persona;
2013 // Hash the chosen persona's UID
2014 this.id = Checksum.compute_for_string (ChecksumType.SHA1,
2015 chosen_persona.uid);
2018 /* Update our aggregated fields and notify the changes */
2019 this._update_fields ();
2022 internal void replace (Individual replacement_individual)
2024 this._set_personas (null, replacement_individual);