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;
184 if ("avatar" in p.writeable_properties)
188 yield a.change_avatar (avatar);
189 debug (" written to writeable persona '%s'", p.uid);
190 avatar_changed = true;
192 catch (PropertyError e)
194 /* Store the first error so we can throw it if setting the
195 * avatar fails on every other persona. */
196 if (persona_error == null)
205 if (avatar_changed == false)
207 assert (persona_error != null);
215 public Folks.PresenceType presence_type { get; set; }
222 public string presence_status { get; set; }
227 public string presence_message { get; set; }
230 * Whether the Individual is the user.
232 * Iff the Individual represents the user – the person who owns the
233 * account in the backend for each {@link Persona} in the Individual –
236 * It is //not// guaranteed that every {@link Persona} in the Individual has
237 * its {@link Persona.is_user} set to the same value as the Individual. For
238 * example, the user could own two Telepathy accounts, and have added the
239 * other account as a contact in each account. The accounts will expose a
240 * {@link Persona} for the user (which will have {@link Persona.is_user} set
241 * to `true`) //and// a {@link Persona} for the contact for the other account
242 * (which will have {@link Persona.is_user} set to `false`).
244 * It is guaranteed that iff this property is set to `true` on an Individual,
245 * there will be at least one {@link Persona} in the Individual with its
246 * {@link Persona.is_user} set to `true`.
248 * It is guaranteed that there will only ever be one Individual with this
249 * property set to `true`.
253 public bool is_user { get; private set; }
256 * A unique identifier for the Individual.
258 * This uniquely identifies the Individual, and persists across
259 * {@link IndividualAggregator} instances. It may not persist across linking
260 * the Individual with other Individuals.
262 * This is an opaque string and has no structure.
264 * If an identifier is required which will be used for a long-lived link
265 * between different stored data, it may be more desirable to use the
266 * {@link Persona.uid} of the most relevant {@link Persona} in the Individual
267 * instead. For example, if storing references to Individuals who are tagged
268 * in a photo, it may be safer to store the UID of the Persona whose backend
269 * provided the photo (e.g. Facebook).
271 public string id { get; private set; }
274 * Emitted when the last of the Individual's {@link Persona}s has been
277 * At this point, the Individual is invalid, so any client referencing it
278 * should unreference it and remove it from their UI.
280 * @param replacement_individual the individual which has replaced this one
281 * due to linking, or `null` if this individual was removed for another reason
284 public signal void removed (Individual? replacement_individual);
286 private string _alias = "";
291 [CCode (notify = false)]
294 get { return this._alias; }
295 set { this.change_alias.begin (value); }
303 public async void change_alias (string alias) throws PropertyError
305 if (this._alias == alias)
310 debug ("Setting alias of individual '%s' to '%s'…", this.id, alias);
312 PropertyError? persona_error = null;
313 var alias_changed = false;
315 /* Try to write it to only the writeable Personas which have "alias"
316 * as a writeable property. */
317 foreach (var p in this._persona_set)
319 var _a = p as AliasDetails;
326 if ("alias" in p.writeable_properties)
330 yield a.change_alias (alias);
331 debug (" written to writeable persona '%s'", p.uid);
332 alias_changed = true;
334 catch (PropertyError e)
336 /* Store the first error so we can throw it if setting the
337 * alias fails on every other persona. */
338 if (persona_error == null)
347 if (alias_changed == false)
349 assert (persona_error != null);
353 /* Update our copy of the alias. */
355 this.notify_property ("alias");
358 private StructuredName? _structured_name = null;
363 [CCode (notify = false)]
364 public StructuredName? structured_name
366 get { return this._structured_name; }
367 set { this.change_structured_name.begin (value); } /* not writeable */
370 private string _full_name = "";
375 [CCode (notify = false)]
376 public string full_name
378 get { return this._full_name; }
379 set { this.change_full_name.begin (value); } /* not writeable */
382 private string _nickname = "";
387 [CCode (notify = false)]
388 public string nickname
390 get { return this._nickname; }
391 set { this.change_nickname.begin (value); }
399 public async void change_nickname (string nickname) throws PropertyError
401 // Normalise null values to the empty string
402 if (nickname == null)
407 if (this._nickname == nickname)
412 debug ("Setting nickname of individual '%s' to '%s'…", this.id, nickname);
414 PropertyError? persona_error = null;
415 var nickname_changed = false;
417 /* Try to write it to only the writeable Personas which have "nickname"
418 * as a writeable property. */
419 foreach (var p in this._persona_set)
421 var _n = p as NameDetails;
428 if ("nickname" in p.writeable_properties)
432 yield n.change_nickname (nickname);
433 debug (" written to writeable persona '%s'", p.uid);
434 nickname_changed = true;
436 catch (PropertyError e)
438 /* Store the first error so we can throw it if setting the
439 * nickname fails on every other persona. */
440 if (persona_error == null)
449 if (nickname_changed == false)
451 assert (persona_error != null);
455 /* Update our copy of the nickname. */
456 this._nickname = nickname;
457 this.notify_property ("nickname");
460 private Gender _gender = Gender.UNSPECIFIED;
464 [CCode (notify = false)]
467 get { return this._gender; }
468 set { this.change_gender.begin (value); } /* not writeable */
471 private HashSet<UrlFieldDetails> _urls = new HashSet<UrlFieldDetails> (
472 (GLib.HashFunc) UrlFieldDetails.hash,
473 (GLib.EqualFunc) UrlFieldDetails.equal);
474 private Set<UrlFieldDetails> _urls_ro;
479 [CCode (notify = false)]
480 public Set<UrlFieldDetails> urls
482 get { return this._urls_ro; }
483 set { this.change_urls.begin (value); } /* not writeable */
486 private HashSet<PhoneFieldDetails> _phone_numbers =
487 new HashSet<PhoneFieldDetails> (
488 (GLib.HashFunc) PhoneFieldDetails.hash,
489 (GLib.EqualFunc) PhoneFieldDetails.equal);
490 private Set<PhoneFieldDetails> _phone_numbers_ro;
495 [CCode (notify = false)]
496 public Set<PhoneFieldDetails> phone_numbers
498 get { return this._phone_numbers_ro; }
499 set { this.change_phone_numbers.begin (value); } /* not writeable */
502 private HashSet<EmailFieldDetails> _email_addresses =
503 new HashSet<EmailFieldDetails> (
504 (GLib.HashFunc) EmailFieldDetails.hash,
505 (GLib.EqualFunc) EmailFieldDetails.equal);
506 private Set<EmailFieldDetails> _email_addresses_ro;
511 [CCode (notify = false)]
512 public Set<EmailFieldDetails> email_addresses
514 get { return this._email_addresses_ro; }
515 set { this.change_email_addresses.begin (value); } /* not writeable */
518 private HashSet<RoleFieldDetails> _roles = new HashSet<RoleFieldDetails> (
519 (GLib.HashFunc) RoleFieldDetails.hash,
520 (GLib.EqualFunc) RoleFieldDetails.equal);
521 private Set<RoleFieldDetails> _roles_ro;
526 [CCode (notify = false)]
527 public Set<RoleFieldDetails> roles
529 get { return this._roles_ro; }
530 set { this.change_roles.begin (value); } /* not writeable */
533 private HashSet<string> _local_ids = new HashSet<string> ();
534 private Set<string> _local_ids_ro;
539 [CCode (notify = false)]
540 public Set<string> local_ids
542 get { return this._local_ids_ro; }
543 set { this.change_local_ids.begin (value); } /* not writeable */
546 private DateTime? _birthday = null;
551 [CCode (notify = false)]
552 public DateTime? birthday
554 get { return this._birthday; }
555 set { this.change_birthday.begin (value); } /* not writeable */
558 private string? _calendar_event_id = null;
563 [CCode (notify = false)]
564 public string? calendar_event_id
566 get { return this._calendar_event_id; }
567 set { this.change_calendar_event_id.begin (value); } /* not writeable */
570 private HashSet<NoteFieldDetails> _notes = new HashSet<NoteFieldDetails> (
571 (GLib.HashFunc) NoteFieldDetails.hash,
572 (GLib.EqualFunc) NoteFieldDetails.equal);
573 private Set<NoteFieldDetails> _notes_ro;
578 [CCode (notify = false)]
579 public Set<NoteFieldDetails> notes
581 get { return this._notes_ro; }
582 set { this.change_notes.begin (value); } /* not writeable */
585 private HashSet<PostalAddressFieldDetails> _postal_addresses =
586 new HashSet<PostalAddressFieldDetails> (
587 (GLib.HashFunc) PostalAddressFieldDetails.hash,
588 (GLib.EqualFunc) PostalAddressFieldDetails.equal);
589 private Set<PostalAddressFieldDetails> _postal_addresses_ro;
594 [CCode (notify = false)]
595 public Set<PostalAddressFieldDetails> postal_addresses
597 get { return this._postal_addresses_ro; }
598 set { this.change_postal_addresses.begin (value); } /* not writeable */
601 private bool _is_favourite = false;
604 * Whether this Individual is a user-defined favourite.
606 * This property is `true` if any of this Individual's {@link Persona}s are
609 [CCode (notify = false)]
610 public bool is_favourite
612 get { return this._is_favourite; }
613 set { this.change_is_favourite.begin (value); }
621 public async void change_is_favourite (bool is_favourite) throws PropertyError
623 if (this._is_favourite == is_favourite)
628 debug ("Setting '%s' favourite status to %s…", this.id,
629 is_favourite ? "TRUE" : "FALSE");
631 PropertyError? persona_error = null;
632 var is_favourite_changed = false;
634 /* Try to write it to only the Personas which have "is-favourite" as a
635 * writeable property.
637 * NOTE: We don't check whether the persona's store is writeable, as we
638 * want is-favourite status to propagate to all stores, if possible. This
639 * is one property which is harmless to propagate. */
640 foreach (var p in this._persona_set)
642 var _a = p as FavouriteDetails;
649 if ("is-favourite" in p.writeable_properties)
653 yield a.change_is_favourite (is_favourite);
654 debug (" written to persona '%s'", p.uid);
655 is_favourite_changed = true;
657 catch (PropertyError e)
659 /* Store the first error so we can throw it if setting the
660 * property fails on every other persona. */
661 if (persona_error == null)
670 if (is_favourite_changed == false)
672 assert (persona_error != null);
676 /* Update our copy of the property. */
677 this._is_favourite = is_favourite;
678 this.notify_property ("is-favourite");
681 private HashSet<string> _groups = new HashSet<string> ();
682 private Set<string> _groups_ro;
687 [CCode (notify = false)]
688 public Set<string> groups
690 get { return this._groups_ro; }
691 set { this.change_groups.begin (value); }
699 public async void change_groups (Set<string> groups) throws PropertyError
701 debug ("Setting '%s' groups…", this.id);
703 PropertyError? persona_error = null;
704 var groups_changed = false;
706 /* Try to write it to only the Personas which have "groups" as a
707 * writeable property. */
708 foreach (var p in this._persona_set)
710 var _g = p as GroupDetails;
717 if ("groups" in p.writeable_properties)
721 yield g.change_groups (groups);
722 debug (" written to persona '%s'", p.uid);
723 groups_changed = true;
725 catch (PropertyError e)
727 /* Store the first error so we can throw it if setting the
728 * property fails on every other persona. */
729 if (persona_error == null)
738 if (groups_changed == false)
740 assert (persona_error != null);
744 /* Update our copy of the property. */
745 this._update_groups ();
748 private HashMultiMap<string, ImFieldDetails> _im_addresses =
749 new HashMultiMap<string, ImFieldDetails> (
750 null, null, ImFieldDetails.hash, (EqualFunc) ImFieldDetails.equal);
755 [CCode (notify = false)]
756 public MultiMap<string, ImFieldDetails> im_addresses
758 get { return this._im_addresses; }
759 set { this.change_im_addresses.begin (value); } /* not writeable */
762 private HashMultiMap<string, WebServiceFieldDetails> _web_service_addresses =
763 new HashMultiMap<string, WebServiceFieldDetails> (null, null,
764 (GLib.HashFunc) WebServiceFieldDetails.hash,
765 (GLib.EqualFunc) WebServiceFieldDetails.equal);
770 [CCode (notify = false)]
771 public MultiMap<string, WebServiceFieldDetails> web_service_addresses
773 get { return this._web_service_addresses; }
775 set { this.change_web_service_addresses.begin (value); }
779 * The set of {@link Persona}s encapsulated by this Individual.
781 * No order is specified over the set of personas, as such an order may be
782 * different across each of the properties implemented by the personas (e.g.
783 * should they be ordered by presence, name, star sign, etc.?).
785 * Changing the set of personas may cause updates to the aggregated properties
786 * provided by the Individual, resulting in property notifications for them.
788 * Changing the set of personas will not cause permanent linking/unlinking of
789 * the added/removed personas to/from this Individual. To do that, call
790 * {@link IndividualAggregator.link_personas} or
791 * {@link IndividualAggregator.unlink_individual}, which will ensure the link
792 * changes are written to the appropriate backend.
796 public Set<Persona> personas
798 get { return this._persona_set_ro; }
799 set { this._set_personas (value, null); }
803 * Emitted when one or more {@link Persona}s are added to or removed from
804 * the Individual. As the parameters are (unordered) sets, the orders of their
805 * elements are undefined.
807 * @param added a set of {@link Persona}s which have been added
808 * @param removed a set of {@link Persona}s which have been removed
812 public signal void personas_changed (Set<Persona> added,
813 Set<Persona> removed);
815 private void _notify_alias_cb (Object obj, ParamSpec ps)
817 this._update_alias ();
820 private void _notify_avatar_cb (Object obj, ParamSpec ps)
822 this._update_avatar ();
825 private void _notify_full_name_cb ()
827 this._update_full_name ();
830 private void _notify_structured_name_cb ()
832 this._update_structured_name ();
835 private void _notify_nickname_cb ()
837 this._update_nickname ();
840 private void _persona_group_changed_cb (string group, bool is_member)
842 this._update_groups ();
845 private void _notify_gender_cb ()
847 this._update_gender ();
850 private void _notify_urls_cb ()
852 this._update_urls ();
855 private void _notify_phone_numbers_cb ()
857 this._update_phone_numbers ();
860 private void _notify_postal_addresses_cb ()
862 this._update_postal_addresses ();
865 private void _notify_email_addresses_cb ()
867 this._update_email_addresses ();
870 private void _notify_roles_cb ()
872 this._update_roles ();
875 private void _notify_birthday_cb ()
877 this._update_birthday ();
880 private void _notify_notes_cb ()
882 this._update_notes ();
885 private void _notify_local_ids_cb ()
887 this._update_local_ids ();
891 * Add or remove the Individual from the specified group.
893 * If `is_member` is `true`, the Individual will be added to the `group`. If
894 * it is `false`, they will be removed from the `group`.
896 * The group membership change will propagate to every {@link Persona} in
899 * @param group a freeform group identifier
900 * @param is_member whether the Individual should be a member of the group
903 public async void change_group (string group, bool is_member)
905 foreach (var p in this._persona_set)
907 if (p is GroupDetails)
908 ((GroupDetails) p).change_group.begin (group, is_member);
911 /* don't notify, since it hasn't happened in the persona backing stores
912 * yet; react to that directly */
915 private void _notify_presence_cb (Object obj, ParamSpec ps)
917 this._update_presence ();
920 private void _notify_im_addresses_cb (Object obj, ParamSpec ps)
922 this._update_im_addresses ();
925 private void _notify_web_service_addresses_cb (Object obj, ParamSpec ps)
927 this._update_web_service_addresses ();
930 private void _notify_is_favourite_cb (Object obj, ParamSpec ps)
932 this._update_is_favourite ();
936 * Create a new Individual.
938 * The Individual can optionally be seeded with the {@link Persona}s in
939 * `personas`. Otherwise, it will have to have personas added using the
940 * {@link Folks.Individual.personas} property after construction.
942 * @param personas a list of {@link Persona}s to initialise the
943 * {@link Individual} with, or `null`
944 * @return a new Individual
948 public Individual (Set<Persona>? personas)
950 Object (personas: personas);
955 debug ("Creating new Individual with %u Personas: %p",
956 this._persona_set.size, this);
958 this._persona_set_ro = this._persona_set.read_only_view;
959 this._urls_ro = this._urls.read_only_view;
960 this._phone_numbers_ro = this._phone_numbers.read_only_view;
961 this._email_addresses_ro = this._email_addresses.read_only_view;
962 this._roles_ro = this._roles.read_only_view;
963 this._local_ids_ro = this._local_ids.read_only_view;
964 this._postal_addresses_ro = this._postal_addresses.read_only_view;
965 this._notes_ro = this._notes.read_only_view;
966 this._groups_ro = this._groups.read_only_view;
971 debug ("Destroying Individual '%s': %p", this.id, this);
974 /* Emit the personas-changed signal, turning null parameters into empty sets
975 * and ensuring that the signal is emitted with read-only views of the sets
976 * so that signal handlers can't modify the sets. */
977 private void _emit_personas_changed (Set<Persona>? added,
978 Set<Persona>? removed)
981 var _removed = removed;
983 if ((added == null || ((!) added).size == 0) &&
984 (removed == null || ((!) removed).size == 0))
986 /* Emitting it with no added or removed personas is pointless */
989 else if (added == null)
991 _added = new HashSet<Persona> ();
993 else if (removed == null)
995 _removed = new HashSet<Persona> ();
998 // We've now guaranteed that both _added and _removed are non-null.
999 this.personas_changed (((!) _added).read_only_view,
1000 ((!) _removed).read_only_view);
1003 private void _store_removed_cb (PersonaStore store)
1005 var remaining_personas = new HashSet<Persona> ();
1007 /* Build a set of the remaining personas (those which weren't in the
1009 foreach (var persona in this._persona_set)
1011 if (persona.store != store)
1013 remaining_personas.add (persona);
1017 this._set_personas (remaining_personas, null);
1020 private void _store_personas_changed_cb (PersonaStore store,
1022 Set<Persona> removed,
1025 GroupDetails.ChangeReason reason)
1027 var remaining_personas = new HashSet<Persona> ();
1029 /* Build a set of the remaining personas (those which aren't in the
1030 * set of removed personas). */
1031 foreach (var persona in this._persona_set)
1033 if (!removed.contains (persona))
1035 remaining_personas.add (persona);
1039 this._set_personas (remaining_personas, null);
1042 private void _update_fields ()
1044 this._update_groups ();
1045 this._update_presence ();
1046 this._update_is_favourite ();
1047 this._update_avatar ();
1048 this._update_alias ();
1049 this._update_trust_level ();
1050 this._update_im_addresses ();
1051 this._update_web_service_addresses ();
1052 this._update_structured_name ();
1053 this._update_full_name ();
1054 this._update_nickname ();
1055 this._update_gender ();
1056 this._update_urls ();
1057 this._update_phone_numbers ();
1058 this._update_email_addresses ();
1059 this._update_roles ();
1060 this._update_birthday ();
1061 this._update_notes ();
1062 this._update_postal_addresses ();
1063 this._update_local_ids ();
1066 /* Delegate to update the value of a property on this individual from the
1067 * given chosen persona. The chosen_persona may be null, in which case we have
1068 * to set a default value.
1070 * Used in _update_single_valued_property(), below. */
1071 private delegate void SingleValuedPropertySetter (Persona? chosen_persona);
1073 /* Delegate to filter a persona based on whether a given property is set.
1075 * Used in _update_single_valued_property(), below. */
1076 private delegate bool PropertyFilter (Persona persona);
1079 * Update a single-valued property from the values in the personas.
1081 * Single-valued properties are ones such as {@link Individual.alias} or
1082 * {@link Individual.gender} — as opposed to multi-valued ones (which are
1083 * generally sets) such as {@link Individual.im_addresses} or
1084 * {@link Individual.groups}.
1086 * This function uses the given comparison function to order the personas in
1087 * this individual, with the highest-positioned persona (the “greatest”
1088 * persona in the total order) finally being passed to the setter function to
1089 * use in updating the individual's value for the given property. i.e. If
1090 * `compare_func(a, b)` is called and returns > 0, persona `a` will be passed
1093 * At a level above `compare_func`, the function always prefers personas from
1094 * the primary store (see {@link IndividualAggregator.primary_store}) over
1095 * those which aren't.
1097 * Note that if a suitable persona isn't found in the individual (if, for
1098 * example, no personas in the individual implement the desired interface),
1099 * `null` will be passed to `setter`, which should then set the individual's
1100 * property to a default value.
1102 * @param interface_type the type of interface which all personas under
1103 * consideration must implement ({@link Persona} to select all personas)
1104 * @param compare_func comparison function to order personas for selection
1105 * @param prop_name name of the property being set, as used in
1106 * {@link Persona.writeable_properties}
1107 * @param setter function to update the individual with the chosen value
1110 private void _update_single_valued_property (Type interface_type,
1111 PropertyFilter filter_func,
1112 CompareFunc<Persona> compare_func, string prop_name,
1113 SingleValuedPropertySetter setter)
1115 CompareDataFunc<Persona> primary_compare_func = (a, b) =>
1120 /* Always prefer values which are set over those which aren't. */
1121 var a_is_set = filter_func (a);
1122 var b_is_set = filter_func (b);
1124 if (a_is_set != b_is_set)
1126 return (a_is_set ? 1 : 0) - (b_is_set ? 1 : 0);
1129 var a_is_primary = a.store.is_primary_store;
1130 var b_is_primary = b.store.is_primary_store;
1132 if (a_is_primary != b_is_primary)
1134 return (a_is_primary ? 1 : 0) - (b_is_primary ? 1 : 0);
1137 /* If both personas have the same is-primary value, prefer personas
1138 * which have the given property as writeable over those which
1140 var a_is_writeable = (prop_name in a.writeable_properties);
1141 var b_is_writeable = (prop_name in b.writeable_properties);
1143 if (a_is_writeable != b_is_writeable)
1145 return (a_is_writeable ? 1 : 0) - (b_is_writeable ? 1 : 0);
1148 /* If both personas have the same writeability for this property, fall
1149 * back to the given comparison function. If the comparison function
1150 * gives them an equal order, we use the personas' UIDs to ensure that
1151 * we end up with a total order over all personas in the individual
1152 * (otherwise we might end up with unstable property values). */
1153 var order = compare_func (a, b);
1157 order = strcmp (a.uid, b.uid);
1163 Persona? candidate_p = null;
1165 foreach (var p in this._persona_set)
1167 /* We only care about personas implementing the given interface. */
1168 if (p.get_type ().is_a (interface_type))
1170 if (candidate_p == null ||
1171 primary_compare_func (p, (!) candidate_p) > 0)
1178 /* Update the property with the values from the best candidate persona we
1179 * found. Note that it's possible for candidate_p to be null if (e.g.)
1180 * none of this._persona_set implemented the interface. */
1181 setter (candidate_p);
1184 private void _update_groups ()
1186 var new_groups = new HashSet<string> ();
1188 /* FIXME: this should partition the personas by store (maybe we should
1189 * keep that mapping in general in this class), and execute
1190 * "groups-changed" on the store (with the set of personas), to allow the
1191 * back-end to optimize it (like Telepathy will for MembersChanged for the
1192 * groups channel list) */
1193 foreach (var p in this._persona_set)
1195 if (p is GroupDetails)
1197 var persona = (GroupDetails) p;
1199 foreach (var group in persona.groups)
1201 new_groups.add (group);
1206 foreach (var group in new_groups)
1208 if (!this._groups.contains (group))
1210 this._groups.add (group);
1211 foreach (var g in this._groups)
1216 this.group_changed (group, true);
1220 /* buffer the removals, so we don't remove while iterating */
1221 var removes = new GLib.List<string> ();
1222 foreach (var group in this._groups)
1224 if (!new_groups.contains (group))
1225 removes.prepend (group);
1228 removes.foreach ((l) =>
1230 unowned string group = (string) l;
1231 this._groups.remove (group);
1232 this.group_changed (group, false);
1236 private void _update_presence ()
1238 this._update_single_valued_property (typeof (PresenceDetails), (p) =>
1240 return ((PresenceDetails) p).presence_type != PresenceType.UNSET;
1243 var a_presence = ((PresenceDetails) a).presence_type;
1244 var b_presence = ((PresenceDetails) b).presence_type;
1246 return PresenceDetails.typecmp (a_presence, b_presence);
1247 }, "presence", (p) =>
1249 var presence_message = ""; /* must not be null */
1250 var presence_status = ""; /* must not be null */
1251 var presence_type = Folks.PresenceType.UNSET;
1255 presence_type = ((PresenceDetails) p).presence_type;
1256 presence_message = ((PresenceDetails) p).presence_message;
1257 presence_status = ((PresenceDetails) p).presence_status;
1260 /* Only notify if any of the values have changed. */
1261 if (this.presence_type != presence_type ||
1262 this.presence_message != presence_message ||
1263 this.presence_status != presence_status)
1265 this.freeze_notify ();
1266 this.presence_message = presence_message;
1267 this.presence_type = presence_type;
1268 this.presence_status = presence_status;
1269 this.thaw_notify ();
1274 private void _update_is_favourite ()
1276 this._update_single_valued_property (typeof (FavouriteDetails), (p) =>
1281 var a_is_favourite = ((FavouriteDetails) a).is_favourite;
1282 var b_is_favourite = ((FavouriteDetails) b).is_favourite;
1284 return ((a_is_favourite == true) ? 1 : 0) -
1285 ((b_is_favourite == true) ? 1 : 0);
1286 }, "is-favourite", (p) =>
1288 var favourite = false;
1292 favourite = ((FavouriteDetails) p).is_favourite;
1295 /* Only notify if the value has changed. We have to set the private
1296 * member and notify manually, or we'd end up propagating the new
1297 * favourite status back down to all our Personas. */
1298 if (this._is_favourite != favourite)
1300 this._is_favourite = favourite;
1301 this.notify_property ("is-favourite");
1306 private void _update_alias ()
1308 this._update_single_valued_property (typeof (AliasDetails), (p) =>
1310 var alias = ((AliasDetails) p).alias;
1311 assert (alias != null);
1313 return (alias.strip () != ""); /* empty aliases are unset */
1316 var a_alias = ((AliasDetails) a).alias;
1317 var b_alias = ((AliasDetails) b).alias;
1319 assert (a_alias != null);
1320 assert (b_alias != null);
1322 var a_is_empty = (a_alias.strip () == "") ? 1 : 0;
1323 var b_is_empty = (b_alias.strip () == "") ? 1 : 0;
1325 /* We prefer to not have an alias which is the same as the Persona's
1326 * display-id, since having such an alias implies that it's the
1327 * default. However, we prefer using such an alias to using the
1328 * Persona's UID, which is our ultimate fallback (below). */
1329 var a_is_display_id = (a_alias == a.display_id) ? 1 : 0;
1330 var b_is_display_id = (b_alias == b.display_id) ? 1 : 0;
1332 return (b_is_empty + b_is_display_id) -
1333 (a_is_empty + a_is_display_id);
1336 string alias = ""; /* must not be null */
1340 alias = ((AliasDetails) p).alias.strip ();
1343 /* Only notify if the value has changed. We have to set the private
1344 * member and notify manually, or we'd end up propagating the new
1345 * alias back down to all our Personas, even if it's a fallback
1346 * display ID or something else undesirable. */
1347 if (this._alias != alias)
1349 this._alias = alias;
1350 this.notify_property ("alias");
1355 private void _update_avatar ()
1357 this._update_single_valued_property (typeof (AvatarDetails), (p) =>
1359 return ((AvatarDetails) p).avatar != null;
1362 /* We can't compare two set avatars efficiently. See: bgo#652721. */
1366 LoadableIcon? avatar = null;
1370 avatar = ((AvatarDetails) p).avatar;
1373 /* only notify if the value has changed */
1374 if ((this._avatar == null && avatar != null) ||
1375 (this._avatar != null &&
1376 (avatar == null || !((!) this._avatar).equal (avatar))))
1378 this._avatar = avatar;
1379 this.notify_property ("avatar");
1384 private void _update_trust_level ()
1386 var trust_level = TrustLevel.PERSONAS;
1388 foreach (var p in this._persona_set)
1390 if (p.is_user == false &&
1391 p.store.trust_level == PersonaStoreTrust.NONE)
1392 trust_level = TrustLevel.NONE;
1395 /* Only notify if the value has changed */
1396 if (this.trust_level != trust_level)
1397 this.trust_level = trust_level;
1400 private void _update_im_addresses ()
1402 /* populate the IM addresses as the union of our Personas' addresses */
1403 this._im_addresses.clear ();
1405 foreach (var persona in this._persona_set)
1407 if (persona is ImDetails)
1409 var im_details = (ImDetails) persona;
1410 foreach (var cur_protocol in im_details.im_addresses.get_keys ())
1413 im_details.im_addresses.get (cur_protocol);
1415 foreach (var address in cur_addresses)
1417 this._im_addresses.set (cur_protocol, address);
1422 this.notify_property ("im-addresses");
1425 private void _update_web_service_addresses ()
1427 /* populate the web service addresses as the union of our Personas' addresses */
1428 this._web_service_addresses.clear ();
1430 foreach (var persona in this.personas)
1432 if (persona is WebServiceDetails)
1434 var web_service_details = (WebServiceDetails) persona;
1435 foreach (var cur_web_service in
1436 web_service_details.web_service_addresses.get_keys ())
1439 web_service_details.web_service_addresses.get (
1442 foreach (var ws_fd in cur_addresses)
1443 this._web_service_addresses.set (cur_web_service, ws_fd);
1447 this.notify_property ("web-service-addresses");
1450 private void _connect_to_persona (Persona persona)
1452 persona.individual = this;
1454 persona.notify["alias"].connect (this._notify_alias_cb);
1455 persona.notify["avatar"].connect (this._notify_avatar_cb);
1456 persona.notify["presence-message"].connect (this._notify_presence_cb);
1457 persona.notify["presence-type"].connect (this._notify_presence_cb);
1458 persona.notify["im-addresses"].connect (this._notify_im_addresses_cb);
1459 persona.notify["web-service-addresses"].connect
1460 (this._notify_web_service_addresses_cb);
1461 persona.notify["is-favourite"].connect (this._notify_is_favourite_cb);
1462 persona.notify["structured-name"].connect (
1463 this._notify_structured_name_cb);
1464 persona.notify["full-name"].connect (this._notify_full_name_cb);
1465 persona.notify["nickname"].connect (this._notify_nickname_cb);
1466 persona.notify["gender"].connect (this._notify_gender_cb);
1467 persona.notify["urls"].connect (this._notify_urls_cb);
1468 persona.notify["phone-numbers"].connect (this._notify_phone_numbers_cb);
1469 persona.notify["email-addresses"].connect (
1470 this._notify_email_addresses_cb);
1471 persona.notify["roles"].connect (this._notify_roles_cb);
1472 persona.notify["birthday"].connect (this._notify_birthday_cb);
1473 persona.notify["notes"].connect (this._notify_notes_cb);
1474 persona.notify["postal-addresses"].connect
1475 (this._notify_postal_addresses_cb);
1476 persona.notify["local-ids"].connect
1477 (this._notify_local_ids_cb);
1480 if (persona is GroupDetails)
1482 ((GroupDetails) persona).group_changed.connect (
1483 this._persona_group_changed_cb);
1487 private void _update_structured_name ()
1489 this._update_single_valued_property (typeof (NameDetails), (p) =>
1491 var name = ((NameDetails) p).structured_name;
1492 return (name != null && !((!) name).is_empty ());
1495 /* Can't compare two set names. */
1497 }, "structured-name", (p) =>
1499 StructuredName? name = null;
1503 name = ((NameDetails) p).structured_name;
1505 if (name != null && ((!) name).is_empty ())
1511 if ((this._structured_name == null && name != null) ||
1512 (this._structured_name != null &&
1513 (name == null || !((!) this._structured_name).equal ((!) name))))
1515 this._structured_name = name;
1516 this.notify_property ("structured-name");
1521 private void _update_full_name ()
1523 this._update_single_valued_property (typeof (NameDetails), (p) =>
1525 var name = ((NameDetails) p).full_name;
1526 assert (name != null);
1528 return (name.strip () != ""); /* empty names are unset */
1531 /* Can't compare two set names. */
1533 }, "full-name", (p) =>
1535 string new_full_name = ""; /* must not be null */
1539 new_full_name = ((NameDetails) p).full_name.strip ();
1542 if (new_full_name != this._full_name)
1544 this._full_name = new_full_name;
1545 this.notify_property ("full-name");
1550 private void _update_nickname ()
1552 this._update_single_valued_property (typeof (NameDetails), (p) =>
1554 var nickname = ((NameDetails) p).nickname;
1555 assert (nickname != null);
1557 return (nickname.strip () != ""); /* empty names are unset */
1560 /* Can't compare two set names. */
1562 }, "nickname", (p) =>
1564 string new_nickname = ""; /* must not be null */
1568 new_nickname = ((NameDetails) p).nickname.strip ();
1571 if (new_nickname != this._nickname)
1573 this._nickname = new_nickname;
1574 this.notify_property ("nickname");
1579 private void _disconnect_from_persona (Persona persona,
1580 Individual? replacement_individual)
1582 persona.notify["alias"].disconnect (this._notify_alias_cb);
1583 persona.notify["avatar"].disconnect (this._notify_avatar_cb);
1584 persona.notify["presence-message"].disconnect (
1585 this._notify_presence_cb);
1586 persona.notify["presence-type"].disconnect (this._notify_presence_cb);
1587 persona.notify["im-addresses"].disconnect (
1588 this._notify_im_addresses_cb);
1589 persona.notify["web-service-addresses"].disconnect (
1590 this._notify_web_service_addresses_cb);
1591 persona.notify["is-favourite"].disconnect (
1592 this._notify_is_favourite_cb);
1593 persona.notify["structured-name"].disconnect (
1594 this._notify_structured_name_cb);
1595 persona.notify["full-name"].disconnect (this._notify_full_name_cb);
1596 persona.notify["nickname"].disconnect (this._notify_nickname_cb);
1597 persona.notify["gender"].disconnect (this._notify_gender_cb);
1598 persona.notify["urls"].disconnect (this._notify_urls_cb);
1599 persona.notify["phone-numbers"].disconnect (
1600 this._notify_phone_numbers_cb);
1601 persona.notify["email-addresses"].disconnect (
1602 this._notify_email_addresses_cb);
1603 persona.notify["roles"].disconnect (this._notify_roles_cb);
1604 persona.notify["birthday"].disconnect (this._notify_birthday_cb);
1605 persona.notify["notes"].disconnect (this._notify_notes_cb);
1606 persona.notify["postal-addresses"].disconnect
1607 (this._notify_postal_addresses_cb);
1608 persona.notify["local-ids"].disconnect (this._notify_local_ids_cb);
1611 if (persona is GroupDetails)
1613 ((GroupDetails) persona).group_changed.disconnect (
1614 this._persona_group_changed_cb);
1617 /* Don't update the individual if the persona's been added to the new one
1618 * already (and thus the new individual has already changed
1619 * persona.individual).
1621 * FIXME: Ideally, we'd assert that a persona can't be added to a new
1622 * individual before it's removed from the old one. However, this
1623 * currently isn't possible due to the way the aggregator works. When the
1624 * aggregator's rewritten, it would be nice to fix this. */
1625 if (persona.individual == this)
1627 /* It may be the case that the persona's being removed from the
1628 * individual (i.e. the replacement individual is non-null, but
1629 * doesn't contain this persona). In this case, we need to set the
1630 * persona's individual to null. */
1631 if (replacement_individual != null &&
1632 persona in ((!) replacement_individual).personas)
1634 persona.individual = replacement_individual;
1638 persona.individual = null;
1643 private void _update_gender ()
1645 this._update_single_valued_property (typeof (GenderDetails), (p) =>
1647 return ((GenderDetails) p).gender != Gender.UNSPECIFIED;
1650 /* It would be sexist to rank one gender over another.
1651 * Besides, how often will we see two personas in the same individual
1652 * which have different genders? */
1656 var new_gender = Gender.UNSPECIFIED;
1660 new_gender = ((GenderDetails) p).gender;
1663 if (new_gender != this.gender)
1665 this._gender = new_gender;
1666 this.notify_property ("gender");
1671 private void _update_urls ()
1673 /* Populate the URLs as the union of our Personas' URLs.
1674 * If the same URL exists multiple times we merge the parameters. */
1675 var urls_set = new HashMap<unowned string, unowned UrlFieldDetails> (
1676 null, null, (GLib.EqualFunc) UrlFieldDetails.equal);
1678 this._urls.clear ();
1680 foreach (var persona in this._persona_set)
1682 var url_details = persona as UrlDetails;
1683 if (url_details != null)
1685 foreach (var url_fd in ((!) url_details).urls)
1687 var existing = urls_set.get (url_fd.value);
1688 if (existing != null)
1689 existing.extend_parameters (url_fd.parameters);
1692 var new_url_fd = new UrlFieldDetails (url_fd.value);
1693 new_url_fd.extend_parameters (url_fd.parameters);
1694 urls_set.set (url_fd.value, new_url_fd);
1695 this._urls.add (new_url_fd);
1701 this.notify_property ("urls");
1704 private void _update_phone_numbers ()
1706 /* Populate the phone numbers as the union of our Personas' numbers
1707 * If the same number exists multiple times we merge the parameters. */
1708 var phone_numbers_set =
1709 new HashMap<unowned string, unowned PhoneFieldDetails> (
1710 null, null, (GLib.EqualFunc) PhoneFieldDetails.equal);
1712 this._phone_numbers.clear ();
1714 foreach (var persona in this._persona_set)
1716 var phone_details = persona as PhoneDetails;
1717 if (phone_details != null)
1719 foreach (var phone_fd in ((!) phone_details).phone_numbers)
1721 var existing = phone_numbers_set.get (phone_fd.value);
1722 if (existing != null)
1723 existing.extend_parameters (phone_fd.parameters);
1726 var new_fd = new PhoneFieldDetails (phone_fd.value);
1727 new_fd.extend_parameters (phone_fd.parameters);
1728 phone_numbers_set.set (phone_fd.value, new_fd);
1729 this._phone_numbers.add (new_fd);
1735 this.notify_property ("phone-numbers");
1738 private void _update_email_addresses ()
1740 /* Populate the email addresses as the union of our Personas' addresses.
1741 * If the same address exists multiple times we merge the parameters. */
1742 var emails_set = new HashMap<unowned string, unowned EmailFieldDetails> (
1743 null, null, (GLib.EqualFunc) EmailFieldDetails.equal);
1745 this._email_addresses.clear ();
1747 foreach (var persona in this._persona_set)
1749 var email_details = persona as EmailDetails;
1750 if (email_details != null)
1752 foreach (var email_fd in ((!) email_details).email_addresses)
1754 var existing = emails_set.get (email_fd.value);
1755 if (existing != null)
1756 existing.extend_parameters (email_fd.parameters);
1759 var new_email_fd = new EmailFieldDetails (email_fd.value,
1760 email_fd.parameters);
1761 emails_set.set (email_fd.value, new_email_fd);
1762 this._email_addresses.add (new_email_fd);
1768 this.notify_property ("email-addresses");
1771 private void _update_roles ()
1773 this._roles.clear ();
1775 foreach (var persona in this._persona_set)
1777 var role_details = persona as RoleDetails;
1778 if (role_details != null)
1780 foreach (var role_fd in ((!) role_details).roles)
1782 this._roles.add (role_fd);
1787 this.notify_property ("roles");
1790 private void _update_local_ids ()
1792 this._local_ids.clear ();
1794 foreach (var persona in this._persona_set)
1796 var local_ids_details = persona as LocalIdDetails;
1797 if (local_ids_details != null)
1799 foreach (var id in ((!) local_ids_details).local_ids)
1801 this._local_ids.add (id);
1806 this.notify_property ("local-ids");
1809 private void _update_postal_addresses ()
1811 this._postal_addresses.clear ();
1813 /* FIXME: Detect duplicates somehow? */
1814 foreach (var persona in this._persona_set)
1816 var address_details = persona as PostalAddressDetails;
1817 if (address_details != null)
1819 foreach (var pafd in ((!) address_details).postal_addresses)
1820 this._postal_addresses.add (pafd);
1824 this.notify_property ("postal-addresses");
1827 private void _update_birthday ()
1829 this._update_single_valued_property (typeof (BirthdayDetails), (p) =>
1831 var details = ((BirthdayDetails) p);
1832 return details.birthday != null && details.calendar_event_id != null;
1835 var a_birthday = ((BirthdayDetails) a).birthday;
1836 var b_birthday = ((BirthdayDetails) b).birthday;
1837 var a_event_id = ((BirthdayDetails) a).calendar_event_id;
1838 var b_event_id = ((BirthdayDetails) b).calendar_event_id;
1840 var a_birthday_is_set = (a_birthday != null) ? 1 : 0;
1841 var b_birthday_is_set = (b_birthday != null) ? 1 : 0;
1843 /* We consider the empty string as “set” because it's an opaque ID. */
1844 var a_event_id_is_set = (a_event_id != null) ? 1 : 0;
1845 var b_event_id_is_set = (b_event_id != null) ? 1 : 0;
1847 /* Prefer personas which have both properties set over those who have
1848 * only one set. We don't consider the case where the birthdays from
1849 * different personas don't match, because that's just scary. */
1850 return (a_birthday_is_set + a_event_id_is_set) -
1851 (b_birthday_is_set + b_event_id_is_set);
1852 }, "birthday", (p) =>
1854 unowned DateTime? bday = null;
1855 unowned string? calendar_event_id = null;
1859 bday = ((BirthdayDetails) p).birthday;
1860 calendar_event_id = ((BirthdayDetails) p).calendar_event_id;
1863 if ((this._birthday == null && bday != null) ||
1864 (this._birthday != null &&
1865 (bday == null || !((!) this._birthday).equal ((!) bday))) ||
1866 (this._calendar_event_id != calendar_event_id))
1868 this._birthday = bday;
1869 this._calendar_event_id = calendar_event_id;
1871 this.freeze_notify ();
1872 this.notify_property ("birthday");
1873 this.notify_property ("calendar-event-id");
1874 this.thaw_notify ();
1879 private void _update_notes ()
1881 this._notes.clear ();
1883 foreach (var persona in this._persona_set)
1885 var note_details = persona as NoteDetails;
1886 if (note_details != null)
1888 foreach (var n in ((!) note_details).notes)
1890 this._notes.add (n);
1895 this.notify_property ("notes");
1898 private void _set_personas (Set<Persona>? personas,
1899 Individual? replacement_individual)
1901 assert (replacement_individual == null || replacement_individual != this);
1903 var added = new HashSet<Persona> ();
1904 var removed = new HashSet<Persona> ();
1906 /* Determine which Personas have been added. If personas == null, we
1907 * assume it's an empty set. */
1908 if (personas != null)
1910 foreach (var p in (!) personas)
1912 if (!this._persona_set.contains (p))
1914 /* Keep track of how many Personas are users */
1916 this._persona_user_count++;
1920 this._persona_set.add (p);
1921 this._connect_to_persona (p);
1923 /* Increment the Persona count for this PersonaStore */
1924 var store = p.store;
1925 var num_from_store = this._stores.get (store);
1926 if (num_from_store == 0)
1928 this._stores.set (store, num_from_store + 1);
1932 this._stores.set (store, 1);
1934 store.removed.connect (this._store_removed_cb);
1935 store.personas_changed.connect (
1936 this._store_personas_changed_cb);
1942 /* Determine which Personas have been removed */
1943 var iter = this._persona_set.iterator ();
1944 while (iter.next ())
1946 var p = iter.get ();
1948 if (personas == null || !((!) personas).contains (p))
1950 /* Keep track of how many Personas are users */
1952 this._persona_user_count--;
1956 /* Decrement the Persona count for this PersonaStore */
1957 var store = p.store;
1958 var num_from_store = this._stores.get (store);
1959 if (num_from_store > 1)
1961 this._stores.set (store, num_from_store - 1);
1965 store.removed.disconnect (this._store_removed_cb);
1966 store.personas_changed.disconnect (
1967 this._store_personas_changed_cb);
1969 this._stores.unset (store);
1972 this._disconnect_from_persona (p, replacement_individual);
1977 this._emit_personas_changed (added, removed);
1979 /* Update this.is_user */
1980 var new_is_user = (this._persona_user_count > 0) ? true : false;
1981 if (new_is_user != this.is_user)
1982 this.is_user = new_is_user;
1984 /* If all the Personas have been removed, remove the Individual */
1985 if (this._persona_set.size < 1)
1987 this.removed (replacement_individual);
1991 /* Update the ID. We choose the most interesting Persona in the
1992 * Individual and hash their UID. This is guaranteed to be globally
1993 * unique, and may not change (for one of the two Individuals) if we link
1994 * two Individuals together, which is nice though we can't rely on this
1997 * This method of constructing an ID ensures that it'll be unique and
1998 * stable for a given Individual once the IndividualAggregator reaches
1999 * a quiescent state after startup. It guarantees that the ID will be
2000 * the same every time folks is used, until the Individual is linked
2001 * or unlinked to another Individual.
2003 * We choose the most interesting Persona by ranking all the Personas
2004 * in the Individual by:
2005 * 1. store.is-primary-store
2006 * 2. store.trust-level
2007 * 3. store.id (alphabetically)
2009 * Note that this heuristic shouldn't be changed without careful thought,
2010 * since stored references to IDs may be broken by the change.
2012 if (this._persona_set.size > 0)
2014 Persona? chosen_persona = null;
2016 foreach (var persona in this._persona_set)
2018 if (chosen_persona == null)
2020 chosen_persona = persona;
2024 var _chosen_persona = (!) chosen_persona;
2026 if ((_chosen_persona.store.is_primary_store == false &&
2027 persona.store.is_primary_store == true) ||
2028 (_chosen_persona.store.is_primary_store ==
2029 persona.store.is_primary_store &&
2030 _chosen_persona.store.trust_level >
2031 persona.store.trust_level) ||
2032 (_chosen_persona.store.is_primary_store ==
2033 persona.store.is_primary_store &&
2034 _chosen_persona.store.trust_level ==
2035 persona.store.trust_level &&
2036 _chosen_persona.store.id > persona.store.id)
2039 chosen_persona = persona;
2043 /* Hash the chosen persona's UID. We can guarantee chosen_persona is
2044 * non-null here because it's at least set to the first element of
2045 * this._persona_set, which we've checked is non-empty. */
2046 this.id = Checksum.compute_for_string (ChecksumType.SHA1,
2047 ((!) chosen_persona).uid);
2050 /* Update our aggregated fields and notify the changes */
2051 this._update_fields ();
2054 internal void replace (Individual replacement_individual)
2056 this._set_personas (null, replacement_individual);