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. An
66 * individual must always contain at least one {@link Persona}.
68 * When choosing the values of single-valued properties (such as
69 * {@link Individual.alias} and {@link Individual.avatar}; but not multi-valued
70 * properties such as {@link Individual.groups} and
71 * {@link Individual.im_addresses}) from the {@link Persona}s in the
72 * individual to present as the values of those properties of the individual,
73 * it is guaranteed that if the individual contains a persona from the primary
74 * persona store (see {@link IndividualAggregator.primary_store}), its property
75 * values will be chosen above all others. This means that any changes to
76 * property values made through folks (which are normally written to the primary
77 * store) will always be used by {@link Folks.Individual}s.
79 * No further guarantees are made about the order of preference used for
80 * choosing which property values to use for the {@link Folks.Individual}, other
81 * than that the order may vary between properties, but is guaranteed to be
82 * stable for a given property.
84 public class Folks.Individual : Object,
104 /* Stores the Personas contained in this Individual. */
105 private HashSet<Persona> _persona_set =
106 new HashSet<Persona> (direct_hash, direct_equal);
107 /* Read-only view of the above set */
108 private Set<Persona> _persona_set_ro;
109 /* Mapping from PersonaStore -> number of Personas from that store contained
110 * in this Individual. There shouldn't be any entries with a number < 1.
111 * This is used for working out when to disconnect from store signals. */
112 private HashMap<unowned PersonaStore, uint> _stores =
113 new HashMap<unowned PersonaStore, uint> (null, null);
114 /* The number of Personas in this Individual which have
115 * Persona.is_user == true. Iff this is > 0, Individual.is_user == true. */
116 private uint _persona_user_count = 0;
119 * The trust level of the Individual.
121 * This specifies how far the Individual can be trusted to be who it claims
122 * to be. See the descriptions for the elements of {@link TrustLevel}.
124 * Clients should ''not'' allow linking of Individuals who have a trust level
125 * of {@link TrustLevel.NONE}.
129 public TrustLevel trust_level { get; private set; }
131 private LoadableIcon? _avatar = null;
138 [CCode (notify = false)]
139 public LoadableIcon? avatar
141 get { return this._avatar; }
142 set { this.change_avatar.begin (value); } /* not writeable */
146 * Change the individual's avatar.
148 * It's preferred to call this rather than setting {@link Individual.avatar}
149 * directly, as this method gives error notification and will only return once
150 * the avatar has been written to the relevant backing stores (or the
151 * operation's failed).
153 * Setting this property is only guaranteed to succeed (and be written to
154 * the backing store) if
155 * {@link IndividualAggregator.ensure_individual_property_writeable} has been
156 * called successfully on the individual for the property name ``avatar``.
158 * @param avatar the new avatar (or ``null`` to unset the avatar)
159 * @throws PropertyError if setting the avatar failed
162 public async void change_avatar (LoadableIcon? avatar) throws PropertyError
164 /* FIXME: Once https://bugzilla.gnome.org/show_bug.cgi?id=604827 is fixed,
165 * this should be rewritten to use async delegates passed to a generic
166 * _change_single_valued_property() method. */
167 if ((this._avatar != null && ((!) this._avatar).equal (avatar)) ||
168 (this._avatar == null && avatar == null))
173 debug ("Setting avatar of individual '%s' to '%p'…", this.id, avatar);
175 PropertyError? persona_error = null;
176 var prop_changed = false;
178 /* Try to write it to only the writeable Personas which have the
179 * "avatar" property as writeable. */
180 foreach (var p in this._persona_set)
182 var _a = p as AvatarDetails;
189 if ("avatar" in p.writeable_properties)
193 yield a.change_avatar (avatar);
194 debug (" written to writeable persona '%s'", p.uid);
197 catch (PropertyError e)
199 /* Store the first error so we can throw it if setting the
200 * avatar fails on every other persona. */
201 if (persona_error == null)
209 /* Failure? Changing the property failed on every suitable persona found
210 * (and potentially zero suitable personas were found). */
211 if (prop_changed == false)
213 if (persona_error == null)
215 persona_error = new PropertyError.NOT_WRITEABLE (
216 _("Failed to change property ‘%s’: No suitable personas were found."),
227 public Folks.PresenceType presence_type { get; set; }
234 public string presence_status { get; set; }
239 public string presence_message { get; set; }
242 * Whether the Individual is the user.
244 * Iff the Individual represents the user – the person who owns the
245 * account in the backend for each {@link Persona} in the Individual –
248 * It is //not// guaranteed that every {@link Persona} in the Individual has
249 * its {@link Persona.is_user} set to the same value as the Individual. For
250 * example, the user could own two Telepathy accounts, and have added the
251 * other account as a contact in each account. The accounts will expose a
252 * {@link Persona} for the user (which will have {@link Persona.is_user} set
253 * to ``true``) //and// a {@link Persona} for the contact for the other
254 * account (which will have {@link Persona.is_user} set to ``false``).
256 * It is guaranteed that iff this property is set to ``true`` on an
257 * Individual, there will be at least one {@link Persona} in the Individual
258 * with its {@link Persona.is_user} set to ``true``.
260 * It is guaranteed that there will only ever be one Individual with this
261 * property set to ``true``.
265 public bool is_user { get; private set; }
268 * A unique identifier for the Individual.
270 * This uniquely identifies the Individual, and persists across
271 * {@link IndividualAggregator} instances. It may not persist across linking
272 * the Individual with other Individuals.
274 * This is an opaque string and has no structure.
276 * If an identifier is required which will be used for a long-lived link
277 * between different stored data, it may be more desirable to use the
278 * {@link Persona.uid} of the most relevant {@link Persona} in the Individual
279 * instead. For example, if storing references to Individuals who are tagged
280 * in a photo, it may be safer to store the UID of the Persona whose backend
281 * provided the photo (e.g. Facebook).
283 public string id { get; private set; }
286 * Emitted when the last of the Individual's {@link Persona}s has been
289 * At this point, the Individual is invalid, so any client referencing it
290 * should unreference it and remove it from their UI.
292 * @param replacement_individual the individual which has replaced this one
293 * due to linking, or ``null`` if this individual was removed for another
297 public signal void removed (Individual? replacement_individual);
299 private string _alias = "";
304 [CCode (notify = false)]
307 get { return this._alias; }
308 set { this.change_alias.begin (value); }
316 public async void change_alias (string alias) throws PropertyError
318 /* FIXME: Once https://bugzilla.gnome.org/show_bug.cgi?id=604827 is fixed,
319 * this should be rewritten to use async delegates passed to a generic
320 * _change_single_valued_property() method. */
321 if (this._alias == alias)
326 debug ("Setting alias of individual '%s' to '%s'…", this.id, alias);
328 PropertyError? persona_error = null;
329 var prop_changed = false;
331 /* Try to write it to only the writeable Personas which have "alias"
332 * as a writeable property. */
333 foreach (var p in this._persona_set)
335 var _a = p as AliasDetails;
342 if ("alias" in p.writeable_properties)
346 yield a.change_alias (alias);
347 debug (" written to writeable persona '%s'", p.uid);
350 catch (PropertyError e)
352 /* Store the first error so we can throw it if setting the
353 * alias fails on every other persona. */
354 if (persona_error == null)
362 /* Failure? Changing the property failed on every suitable persona found
363 * (and potentially zero suitable personas were found). */
364 if (prop_changed == false)
366 if (persona_error == null)
368 persona_error = new PropertyError.NOT_WRITEABLE (
369 _("Failed to change property ‘%s’: No suitable personas were found."),
377 private StructuredName? _structured_name = null;
382 [CCode (notify = false)]
383 public StructuredName? structured_name
385 get { return this._structured_name; }
386 set { this.change_structured_name.begin (value); } /* not writeable */
389 private string _full_name = "";
394 [CCode (notify = false)]
395 public string full_name
397 get { return this._full_name; }
398 set { this.change_full_name.begin (value); } /* not writeable */
401 private string _nickname = "";
406 [CCode (notify = false)]
407 public string nickname
409 get { return this._nickname; }
410 set { this.change_nickname.begin (value); }
418 public async void change_nickname (string nickname) throws PropertyError
420 /* FIXME: Once https://bugzilla.gnome.org/show_bug.cgi?id=604827 is fixed,
421 * this should be rewritten to use async delegates passed to a generic
422 * _change_single_valued_property() method. */
424 // Normalise null values to the empty string
425 if (nickname == null)
430 if (this._nickname == nickname)
435 debug ("Setting nickname of individual '%s' to '%s'…", this.id, nickname);
437 PropertyError? persona_error = null;
438 var prop_changed = false;
440 /* Try to write it to only the writeable Personas which have "nickname"
441 * as a writeable property. */
442 foreach (var p in this._persona_set)
444 var _n = p as NameDetails;
451 if ("nickname" in p.writeable_properties)
455 yield n.change_nickname (nickname);
456 debug (" written to writeable persona '%s'", p.uid);
459 catch (PropertyError e)
461 /* Store the first error so we can throw it if setting the
462 * nickname fails on every other persona. */
463 if (persona_error == null)
471 /* Failure? Changing the property failed on every suitable persona found
472 * (and potentially zero suitable personas were found). */
473 if (prop_changed == false)
475 if (persona_error == null)
477 persona_error = new PropertyError.NOT_WRITEABLE (
478 _("Failed to change property ‘%s’: No suitable personas were found."),
486 private Gender _gender = Gender.UNSPECIFIED;
490 [CCode (notify = false)]
493 get { return this._gender; }
494 set { this.change_gender.begin (value); } /* not writeable */
497 private HashSet<UrlFieldDetails>? _urls = null;
498 private Set<UrlFieldDetails>? _urls_ro = null;
503 [CCode (notify = false)]
504 public Set<UrlFieldDetails> urls
508 this._update_urls (true, false, false);
509 return this._urls_ro;
511 set { this.change_urls.begin (value); } /* not writeable */
514 private HashSet<PhoneFieldDetails>? _phone_numbers = null;
515 private Set<PhoneFieldDetails>? _phone_numbers_ro = null;
520 [CCode (notify = false)]
521 public Set<PhoneFieldDetails> phone_numbers
525 this._update_phone_numbers (true, false, false);
526 return this._phone_numbers_ro;
528 set { this.change_phone_numbers.begin (value); } /* not writeable */
531 private HashSet<EmailFieldDetails>? _email_addresses = null;
532 private Set<EmailFieldDetails>? _email_addresses_ro = null;
537 [CCode (notify = false)]
538 public Set<EmailFieldDetails> email_addresses
542 this._update_email_addresses (true, false, false);
543 return this._email_addresses_ro;
545 set { this.change_email_addresses.begin (value); } /* not writeable */
548 private HashSet<RoleFieldDetails>? _roles = null;
549 private Set<RoleFieldDetails>? _roles_ro = null;
554 [CCode (notify = false)]
555 public Set<RoleFieldDetails> roles
559 this._update_roles (true, false, false);
560 return this._roles_ro;
562 set { this.change_roles.begin (value); } /* not writeable */
565 private HashSet<string>? _local_ids = null;
566 private Set<string>? _local_ids_ro = null;
571 [CCode (notify = false)]
572 public Set<string> local_ids
576 this._update_local_ids (true, false, false);
577 return this._local_ids_ro;
579 set { this.change_local_ids.begin (value); } /* not writeable */
582 private DateTime? _birthday = null;
587 [CCode (notify = false)]
588 public DateTime? birthday
590 get { return this._birthday; }
591 set { this.change_birthday.begin (value); } /* not writeable */
594 private string? _calendar_event_id = null;
599 [CCode (notify = false)]
600 public string? calendar_event_id
602 get { return this._calendar_event_id; }
603 set { this.change_calendar_event_id.begin (value); } /* not writeable */
606 private HashSet<NoteFieldDetails>? _notes = null;
607 private Set<NoteFieldDetails>? _notes_ro = null;
612 [CCode (notify = false)]
613 public Set<NoteFieldDetails> notes
617 this._update_notes (true, false, false);
618 return this._notes_ro;
620 set { this.change_notes.begin (value); } /* not writeable */
623 private HashSet<PostalAddressFieldDetails>? _postal_addresses = null;
624 private Set<PostalAddressFieldDetails>? _postal_addresses_ro = null;
629 [CCode (notify = false)]
630 public Set<PostalAddressFieldDetails> postal_addresses
634 this._update_postal_addresses (true, false, false);
635 return this._postal_addresses_ro;
637 set { this.change_postal_addresses.begin (value); } /* not writeable */
640 private bool _is_favourite = false;
643 * Whether this Individual is a user-defined favourite.
645 * This property is ``true`` if any of this Individual's {@link Persona}s are
648 [CCode (notify = false)]
649 public bool is_favourite
651 get { return this._is_favourite; }
652 set { this.change_is_favourite.begin (value); }
660 public async void change_is_favourite (bool is_favourite) throws PropertyError
662 /* FIXME: Once https://bugzilla.gnome.org/show_bug.cgi?id=604827 is fixed,
663 * this should be rewritten to use async delegates passed to a generic
664 * _change_single_valued_property() method. */
665 if (this._is_favourite == is_favourite)
670 debug ("Setting '%s' favourite status to %s…", this.id,
671 is_favourite ? "TRUE" : "FALSE");
673 PropertyError? persona_error = null;
674 var prop_changed = false;
676 /* Try to write it to only the Personas which have "is-favourite" as a
677 * writeable property.
679 * NOTE: We don't check whether the persona's store is writeable, as we
680 * want is-favourite status to propagate to all stores, if possible. This
681 * is one property which is harmless to propagate. */
682 foreach (var p in this._persona_set)
684 var _a = p as FavouriteDetails;
691 if ("is-favourite" in p.writeable_properties)
695 yield a.change_is_favourite (is_favourite);
696 debug (" written to persona '%s'", p.uid);
699 catch (PropertyError e)
701 /* Store the first error so we can throw it if setting the
702 * property fails on every other persona. */
703 if (persona_error == null)
711 /* Failure? Changing the property failed on every suitable persona found
712 * (and potentially zero suitable personas were found). */
713 if (prop_changed == false)
715 if (persona_error == null)
717 persona_error = new PropertyError.NOT_WRITEABLE (
718 _("Failed to change property ‘%s’: No suitable personas were found."),
726 private HashSet<string>? _groups = null;
727 private Set<string>? _groups_ro = null;
732 [CCode (notify = false)]
733 public Set<string> groups
737 this._update_groups (true, false, false);
738 return this._groups_ro;
740 set { this.change_groups.begin (value); }
748 public async void change_groups (Set<string> groups) throws PropertyError
750 /* FIXME: Once https://bugzilla.gnome.org/show_bug.cgi?id=604827 is fixed,
751 * this should be rewritten to use async delegates passed to a generic
752 * _change_single_valued_property() method. */
753 debug ("Setting '%s' groups…", this.id);
755 PropertyError? persona_error = null;
756 var prop_changed = false;
758 /* Try to write it to only the Personas which have "groups" as a
759 * writeable property. */
760 foreach (var p in this._persona_set)
762 var _g = p as GroupDetails;
769 if ("groups" in p.writeable_properties)
773 yield g.change_groups (groups);
774 debug (" written to persona '%s'", p.uid);
777 catch (PropertyError e)
779 /* Store the first error so we can throw it if setting the
780 * property fails on every other persona. */
781 if (persona_error == null)
789 /* Failure? Changing the property failed on every suitable persona found
790 * (and potentially zero suitable personas were found). */
791 if (prop_changed == false)
793 if (persona_error == null)
795 persona_error = new PropertyError.NOT_WRITEABLE (
796 _("Failed to change property ‘%s’: No suitable personas were found."),
804 private HashMultiMap<string, ImFieldDetails>? _im_addresses = null;
809 [CCode (notify = false)]
810 public MultiMap<string, ImFieldDetails> im_addresses
814 this._update_im_addresses (true, false, false);
815 return this._im_addresses;
817 set { this.change_im_addresses.begin (value); } /* not writeable */
820 private HashMultiMap<string, WebServiceFieldDetails>? _web_service_addresses =
826 [CCode (notify = false)]
827 public MultiMap<string, WebServiceFieldDetails> web_service_addresses
831 this._update_web_service_addresses (true, false, false);
832 return this._web_service_addresses;
835 set { this.change_web_service_addresses.begin (value); }
841 public uint im_interaction_count
846 /* Iterate over all personas and sum up their IM interaction counts*/
847 foreach (var persona in this._persona_set)
849 var my_interaction_details = persona as InteractionDetails;
850 if (my_interaction_details != null)
852 counter = counter + my_interaction_details.im_interaction_count;
862 private DateTime? _last_im_interaction_datetime = null;
864 public DateTime? last_im_interaction_datetime
868 if (this._last_im_interaction_datetime == null)
870 /* Iterate over all personas and get the latest IM interaction datetime */
871 foreach (var persona in this._persona_set)
873 var my_interaction_details = persona as InteractionDetails;
874 if (my_interaction_details != null &&
875 my_interaction_details.last_im_interaction_datetime != null)
877 DateTime interaction_datetime = my_interaction_details.last_im_interaction_datetime;
878 if (this._last_im_interaction_datetime == null ||
879 interaction_datetime.compare (this._last_im_interaction_datetime) == 1)
881 this._last_im_interaction_datetime = my_interaction_details.last_im_interaction_datetime;
886 return this._last_im_interaction_datetime;
893 public uint call_interaction_count
898 /* Iterate over all personas and sum up their call interaction counts*/
899 foreach (var persona in this._persona_set)
901 var my_interaction_details = persona as InteractionDetails;
902 if (my_interaction_details != null)
904 counter = counter + my_interaction_details.call_interaction_count;
914 private DateTime? _last_call_interaction_datetime = null;
916 public DateTime? last_call_interaction_datetime
920 if (this._last_call_interaction_datetime == null)
922 /* Iterate over all personas and get the latest IM interaction datetime */
923 foreach (var persona in this._persona_set)
925 var my_interaction_details = persona as InteractionDetails;
926 if (my_interaction_details != null &&
927 my_interaction_details.last_call_interaction_datetime != null)
929 var interaction_datetime = my_interaction_details.last_call_interaction_datetime;
930 if (this._last_call_interaction_datetime == null ||
931 interaction_datetime.compare (this._last_call_interaction_datetime) > 1)
933 this._last_call_interaction_datetime = my_interaction_details.last_call_interaction_datetime;
938 return this._last_call_interaction_datetime;
943 * The set of {@link Persona}s encapsulated by this Individual.
945 * There must always be at least one Persona in this set.
947 * No order is specified over the set of personas, as such an order may be
948 * different across each of the properties implemented by the personas (e.g.
949 * should they be ordered by presence, name, star sign, etc.?).
951 * Changing the set of personas may cause updates to the aggregated properties
952 * provided by the Individual, resulting in property notifications for them.
954 * Changing the set of personas will not cause permanent linking/unlinking of
955 * the added/removed personas to/from this Individual. To do that, call
956 * {@link IndividualAggregator.link_personas} or
957 * {@link IndividualAggregator.unlink_individual}, which will ensure the link
958 * changes are written to the appropriate backend.
962 public Set<Persona> personas
964 get { return this._persona_set_ro; }
965 set { this._set_personas (value, null); }
969 * Emitted when one or more {@link Persona}s are added to or removed from
970 * the Individual. As the parameters are (unordered) sets, the orders of their
971 * elements are undefined.
973 * @param added a set of {@link Persona}s which have been added
974 * @param removed a set of {@link Persona}s which have been removed
978 public signal void personas_changed (Set<Persona> added,
979 Set<Persona> removed);
981 private void _notify_alias_cb (Object obj, ParamSpec ps)
983 this._update_alias ();
986 private void _notify_avatar_cb (Object obj, ParamSpec ps)
988 this._update_avatar ();
991 private void _notify_full_name_cb ()
993 this._update_full_name ();
996 private void _notify_structured_name_cb ()
998 this._update_structured_name ();
1001 private void _notify_nickname_cb ()
1003 this._update_nickname ();
1006 private void _persona_group_changed_cb (string group, bool is_member)
1008 this._update_groups (false);
1011 private void _notify_gender_cb ()
1013 this._update_gender ();
1016 private void _notify_urls_cb ()
1018 this._update_urls (false);
1021 private void _notify_phone_numbers_cb ()
1023 this._update_phone_numbers (false);
1026 private void _notify_postal_addresses_cb ()
1028 this._update_postal_addresses (false);
1031 private void _notify_email_addresses_cb ()
1033 this._update_email_addresses (false);
1036 private void _notify_roles_cb ()
1038 this._update_roles (false);
1041 private void _notify_birthday_cb ()
1043 this._update_birthday ();
1046 private void _notify_notes_cb ()
1048 this._update_notes (false);
1051 private void _notify_local_ids_cb ()
1053 this._update_local_ids (false);
1057 * Add or remove the Individual from the specified group.
1059 * If ``is_member`` is ``true``, the Individual will be added to the
1060 * ``group``. If it is ``false``, they will be removed from the ``group``.
1062 * The group membership change will propagate to every {@link Persona} in
1065 * @param group a freeform group identifier
1066 * @param is_member whether the Individual should be a member of the group
1069 public async void change_group (string group, bool is_member)
1071 foreach (var p in this._persona_set)
1073 if (p is GroupDetails)
1074 ((GroupDetails) p).change_group.begin (group, is_member);
1077 /* don't notify, since it hasn't happened in the persona backing stores
1078 * yet; react to that directly */
1081 private void _notify_presence_cb (Object obj, ParamSpec ps)
1083 this._update_presence ();
1086 private void _notify_im_addresses_cb (Object obj, ParamSpec ps)
1088 this._update_im_addresses (false);
1091 private void _notify_web_service_addresses_cb (Object obj, ParamSpec ps)
1093 this._update_web_service_addresses (false);
1096 private void _notify_is_favourite_cb (Object obj, ParamSpec ps)
1098 this._update_is_favourite ();
1101 private void _notify_im_interaction_count_cb (Object obj, ParamSpec ps)
1104 * The property is pull rather than push. This function is called in
1105 * response to personas emitting a similar notification.
1107 this.notify_property ("im-interaction-count");
1110 private void _notify_call_interaction_count_cb (Object obj, ParamSpec ps)
1113 * The property is pull rather than push. This function is called in
1114 * response to personas emitting a similar notification.
1116 this.notify_property ("call-interaction-count");
1119 private void _notify_last_im_interaction_datetime_cb (Object obj, ParamSpec ps)
1122 * The property is pull rather than push. This function is called in
1123 * response to personas emitting a similar notification.
1125 this._last_im_interaction_datetime = null;
1126 this.notify_property ("last-im-interaction-datetime");
1129 private void _notify_last_call_interaction_datetime_cb (Object obj, ParamSpec ps)
1132 * The property is pull rather than push. This function is called in
1133 * response to personas emitting a similar notification.
1135 this._last_call_interaction_datetime = null;
1136 this.notify_property ("last-call-interaction-datetime");
1140 * Create a new Individual.
1142 * The Individual can optionally be seeded with the {@link Persona}s in
1143 * ``personas``. Otherwise, it will have to have personas added using the
1144 * {@link Folks.Individual.personas} property after construction.
1146 * @param personas a list of {@link Persona}s to initialise the
1147 * {@link Folks.Individual} with, or ``null``
1148 * @return a new Individual
1152 public Individual (Set<Persona>? personas)
1154 Object (personas: personas);
1156 debug ("Creating new Individual with %u Personas: %p",
1157 this._persona_set.size, this);
1162 this._persona_set_ro = this._persona_set.read_only_view;
1167 debug ("Destroying Individual '%s': %p", this.id, this);
1170 /* Emit the personas-changed signal, turning null parameters into empty sets
1171 * and ensuring that the signal is emitted with read-only views of the sets
1172 * so that signal handlers can't modify the sets. */
1173 private void _emit_personas_changed (Set<Persona>? added,
1174 Set<Persona>? removed)
1177 var _removed = removed;
1179 if ((added == null || ((!) added).size == 0) &&
1180 (removed == null || ((!) removed).size == 0))
1182 /* Emitting it with no added or removed personas is pointless */
1185 else if (added == null)
1187 _added = new HashSet<Persona> ();
1189 else if (removed == null)
1191 _removed = new HashSet<Persona> ();
1194 // We've now guaranteed that both _added and _removed are non-null.
1195 this.personas_changed (((!) _added).read_only_view,
1196 ((!) _removed).read_only_view);
1199 private void _store_removed_cb (PersonaStore store)
1201 var remaining_personas = new HashSet<Persona> ();
1203 /* Build a set of the remaining personas (those which weren't in the
1205 foreach (var persona in this._persona_set)
1207 if (persona.store != store)
1209 remaining_personas.add (persona);
1213 this._set_personas (remaining_personas, null);
1216 private void _store_personas_changed_cb (PersonaStore store,
1218 Set<Persona> removed,
1221 GroupDetails.ChangeReason reason)
1223 var remaining_personas = new HashSet<Persona> ();
1225 /* Build a set of the remaining personas (those which aren't in the
1226 * set of removed personas). */
1227 foreach (var persona in this._persona_set)
1229 if (!removed.contains (persona))
1231 remaining_personas.add (persona);
1235 this._set_personas (remaining_personas, null);
1238 private void _update_fields ()
1240 this._update_groups (false);
1241 this._update_presence ();
1242 this._update_is_favourite ();
1243 this._update_avatar ();
1244 this._update_alias ();
1245 this._update_trust_level ();
1246 this._update_im_addresses (false);
1247 this._update_web_service_addresses (false);
1248 this._update_structured_name ();
1249 this._update_full_name ();
1250 this._update_nickname ();
1251 this._update_gender ();
1252 this._update_urls (false);
1253 this._update_phone_numbers (false);
1254 this._update_email_addresses (false);
1255 this._update_roles (false);
1256 this._update_birthday ();
1257 this._update_notes (false);
1258 this._update_postal_addresses (false);
1259 this._update_local_ids (false);
1262 /* Delegate to update the value of a property on this individual from the
1263 * given chosen persona. The chosen_persona may be null, in which case we have
1264 * to set a default value.
1266 * Used in _update_single_valued_property(), below. */
1267 private delegate void SingleValuedPropertySetter (Persona? chosen_persona);
1269 /* Delegate to filter a persona based on whether a given property is set.
1271 * Used in _update_single_valued_property(), below. */
1272 private delegate bool PropertyFilter (Persona persona);
1275 * Update a single-valued property from the values in the personas.
1277 * Single-valued properties are ones such as {@link Individual.alias} or
1278 * {@link Individual.gender} — as opposed to multi-valued ones (which are
1279 * generally sets) such as {@link Individual.im_addresses} or
1280 * {@link Individual.groups}.
1282 * This function uses the given comparison function to order the personas in
1283 * this individual, with the highest-positioned persona (the “greatest”
1284 * persona in the total order) finally being passed to the setter function to
1285 * use in updating the individual's value for the given property. i.e. If
1286 * ``compare_func(a, b)`` is called and returns > 0, persona ``a`` will be
1287 * passed to the setter.
1289 * At a level above ``compare_func``, the function always prefers personas
1290 * from the primary store (see {@link IndividualAggregator.primary_store})
1291 * over those which aren't.
1293 * Note that if a suitable persona isn't found in the individual (if, for
1294 * example, no personas in the individual implement the desired interface),
1295 * ``null`` will be passed to ``setter``, which should then set the
1296 * individual's property to a default value.
1298 * @param interface_type the type of interface which all personas under
1299 * consideration must implement ({@link Persona} to select all personas)
1300 * @param compare_func comparison function to order personas for selection
1301 * @param prop_name name of the property being set, as used in
1302 * {@link Persona.writeable_properties}
1303 * @param setter function to update the individual with the chosen value
1306 private void _update_single_valued_property (Type interface_type,
1307 PropertyFilter filter_func,
1308 CompareFunc<Persona> compare_func, string prop_name,
1309 SingleValuedPropertySetter setter)
1311 CompareDataFunc<Persona> primary_compare_func = (a, b) =>
1313 return_val_if_fail (a != null, 0);
1314 return_val_if_fail (b != null, 0);
1316 /* Always prefer values which are set over those which aren't. */
1317 var a_is_set = filter_func (a);
1318 var b_is_set = filter_func (b);
1320 if (a_is_set != b_is_set)
1322 return (a_is_set ? 1 : 0) - (b_is_set ? 1 : 0);
1325 var a_is_primary = a.store.is_primary_store;
1326 var b_is_primary = b.store.is_primary_store;
1328 if (a_is_primary != b_is_primary)
1330 return (a_is_primary ? 1 : 0) - (b_is_primary ? 1 : 0);
1333 /* If both personas have the same is-primary value, prefer personas
1334 * which have the given property as writeable over those which
1336 var a_is_writeable = (prop_name in a.writeable_properties);
1337 var b_is_writeable = (prop_name in b.writeable_properties);
1339 if (a_is_writeable != b_is_writeable)
1341 return (a_is_writeable ? 1 : 0) - (b_is_writeable ? 1 : 0);
1344 /* If both personas have the same writeability for this property, fall
1345 * back to the given comparison function. If the comparison function
1346 * gives them an equal order, we use the personas' UIDs to ensure that
1347 * we end up with a total order over all personas in the individual
1348 * (otherwise we might end up with unstable property values). */
1349 var order = compare_func (a, b);
1353 order = strcmp (a.uid, b.uid);
1359 Persona? candidate_p = null;
1361 foreach (var p in this._persona_set)
1363 /* We only care about personas implementing the given interface. */
1364 if (p.get_type ().is_a (interface_type))
1366 if (candidate_p == null ||
1367 primary_compare_func (p, (!) candidate_p) > 0)
1374 /* Update the property with the values from the best candidate persona we
1375 * found. Note that it's possible for candidate_p to be null if (e.g.)
1376 * none of this._persona_set implemented the interface. */
1377 setter (candidate_p);
1380 /* Delegate to add the values of a property from all personas to the
1381 * collection of values for that property in this individual.
1383 * Used in _update_multi_valued_property(), below. */
1384 private delegate bool MultiValuedPropertySetter ();
1386 /* Delegate to get whether a multi-valued property in this Individual has not
1387 * been initialised yet (and is thus still null).
1389 * Used in _update_multi_valued_property(), below. */
1390 private delegate bool PropertyIsNull ();
1392 /* Delegate to create a new empty collection for a multi-valued property in
1393 * this Individual and assign it to the property.
1395 * Used in _update_multi_valued_property(), below. */
1396 private delegate void CollectionCreator ();
1399 * Update a multi-valued property from the values in the personas.
1401 * Multi-valued properties are ones such as {@link Individual.notes} or
1402 * {@link Individual.email_addresses} which have multiple values taken as the
1403 * union of the values listed by the personas for those properties.
1405 * This function handles lazy instantiation of the multi-valued property. If
1406 * ``create_if_not_exist`` is ``true``, the property is guaranteed to be
1407 * created (by ``create_collection``) and set to a non-``null`` value before
1408 * this function returns.
1410 * If ``create_if_not_exist`` is ``false``, however, the property may not be
1411 * instantiated if it hasn't already been accessed through its property
1412 * getter. In this case, a change notification will be emitted for the
1413 * property and this function will return immediately.
1415 * If ``force_update`` is ``true``, then existing values get updated (if
1416 * the current value is different) or created (according to the
1417 * ``create_if_not_exist`` value). Otherwise the function only ensures
1418 * that there is a value (if ``create_if_not_exist`` is set) and leaves
1419 * existing values unchanged.
1421 * If the property value is to be instantiated, or already has been
1422 * instantiated, its value is updated by ``setter`` from the values of the
1423 * property in the individual's personas.
1425 * @param prop_name name of the property being set, as used in
1426 * {@link Persona.writeable_properties}
1427 * @param create_if_not_exist ``true`` to ensure the property is non-null;
1428 * ``false`` otherwise
1429 * @param prop_is_null function returning ``true`` iff the property is
1430 * currently ``null``
1431 * @param create_collection function creating a new collection/container for
1432 * the property values and assigning it to the property (and updating the
1433 * property's read-only view as necessary)
1434 * @param setter function which adds the values from the individual's
1435 * personas' values for the property to the individual's value for the
1436 * property; it returns ``true`` if the property value has changed
1439 private void _update_multi_valued_property (string prop_name,
1440 bool create_if_not_exist, PropertyIsNull prop_is_null,
1441 CollectionCreator create_collection, MultiValuedPropertySetter setter,
1442 bool emit_notification = true,
1443 bool force_update = true)
1445 /* If the set of values doesn't exist, and we're not meant to lazily
1446 * create it, then simply emit a notification (since the set might've
1447 * changed — we can't be sure, but emitting is a safe over-estimate) and
1449 bool created = false;
1450 if (prop_is_null ())
1452 /* Notify and return. */
1453 if (create_if_not_exist == false)
1455 if (emit_notification)
1457 this.notify_property (prop_name);
1462 /* Lazily instantiate the set of IM addresses. */
1463 create_collection ();
1467 /* Re-populate the collection as the union of the values in the
1468 * individual's personas. Do this when an empty property was just
1469 * created or we were asked to explicitly (usually because the caller
1470 * knows that the current value is out-dated).
1472 if ((created || force_update) && setter () == true && emit_notification)
1474 this.notify_property (prop_name);
1478 private void _update_groups (bool create_if_not_exist, bool emit_notification = true, bool force_update = true)
1480 /* If the set of groups doesn't exist, and we're not meant to lazily
1481 * create it, then simply emit a notification (since the set might've
1482 * changed — we can't be sure, but emitting is a safe over-estimate) and
1484 bool created = false;
1485 if (this._groups == null && create_if_not_exist == false)
1487 if (emit_notification)
1489 this.notify_property ("groups");
1494 /* Lazily instantiate the set of groups. */
1495 else if (this._groups == null)
1497 this._groups = new HashSet<string> ();
1498 this._groups_ro = this._groups.read_only_view;
1502 /* Don't touch existing content in get(). */
1503 if (!created && !force_update)
1506 var new_groups = new HashSet<string> ();
1508 /* FIXME: this should partition the personas by store (maybe we should
1509 * keep that mapping in general in this class), and execute
1510 * "groups-changed" on the store (with the set of personas), to allow the
1511 * back-end to optimize it (like Telepathy will for MembersChanged for the
1512 * groups channel list) */
1513 foreach (var p in this._persona_set)
1515 if (p is GroupDetails)
1517 var persona = (GroupDetails) p;
1519 foreach (var group in persona.groups)
1521 new_groups.add (group);
1526 foreach (var group in new_groups)
1528 if (this._groups.add (group) && emit_notification)
1530 this.group_changed (group, true);
1534 /* buffer the removals, so we don't remove while iterating */
1535 var removes = new GLib.List<string> ();
1536 foreach (var group in this._groups)
1538 if (!new_groups.contains (group))
1539 removes.prepend (group);
1542 removes.foreach ((l) =>
1544 unowned string group = (string) l;
1545 this._groups.remove (group);
1546 if (emit_notification)
1548 this.group_changed (group, false);
1553 private void _update_presence ()
1555 this._update_single_valued_property (typeof (PresenceDetails), (p) =>
1557 return ((PresenceDetails) p).presence_type != PresenceType.UNSET;
1560 var a_presence = ((PresenceDetails) a).presence_type;
1561 var b_presence = ((PresenceDetails) b).presence_type;
1563 return PresenceDetails.typecmp (a_presence, b_presence);
1564 }, "presence", (p) =>
1566 var presence_message = ""; /* must not be null */
1567 var presence_status = ""; /* must not be null */
1568 var presence_type = Folks.PresenceType.UNSET;
1572 presence_type = ((PresenceDetails) p).presence_type;
1573 presence_message = ((PresenceDetails) p).presence_message;
1574 presence_status = ((PresenceDetails) p).presence_status;
1577 /* Only notify if any of the values have changed. */
1578 if (this.presence_type != presence_type ||
1579 this.presence_message != presence_message ||
1580 this.presence_status != presence_status)
1582 this.freeze_notify ();
1583 this.presence_message = presence_message;
1584 this.presence_type = presence_type;
1585 this.presence_status = presence_status;
1586 this.thaw_notify ();
1591 private void _update_is_favourite ()
1593 this._update_single_valued_property (typeof (FavouriteDetails), (p) =>
1598 var a_is_favourite = ((FavouriteDetails) a).is_favourite;
1599 var b_is_favourite = ((FavouriteDetails) b).is_favourite;
1601 return ((a_is_favourite == true) ? 1 : 0) -
1602 ((b_is_favourite == true) ? 1 : 0);
1603 }, "is-favourite", (p) =>
1605 var favourite = false;
1609 favourite = ((FavouriteDetails) p).is_favourite;
1612 /* Only notify if the value has changed. We have to set the private
1613 * member and notify manually, or we'd end up propagating the new
1614 * favourite status back down to all our Personas. */
1615 if (this._is_favourite != favourite)
1617 this._is_favourite = favourite;
1618 this.notify_property ("is-favourite");
1623 private void _update_alias ()
1625 this._update_single_valued_property (typeof (AliasDetails), (p) =>
1627 var alias = ((AliasDetails) p).alias;
1628 return_val_if_fail (alias != null, false);
1630 return (alias.strip () != ""); /* empty aliases are unset */
1633 var a_alias = ((AliasDetails) a).alias;
1634 var b_alias = ((AliasDetails) b).alias;
1636 return_val_if_fail (a_alias != null, 0);
1637 return_val_if_fail (b_alias != null, 0);
1639 var a_is_empty = (a_alias.strip () == "") ? 1 : 0;
1640 var b_is_empty = (b_alias.strip () == "") ? 1 : 0;
1642 /* We prefer to not have an alias which is the same as the Persona's
1643 * display-id, since having such an alias implies that it's the
1644 * default. However, we prefer using such an alias to using the
1645 * Persona's UID, which is our ultimate fallback (below). */
1646 var a_is_display_id = (a_alias == a.display_id) ? 1 : 0;
1647 var b_is_display_id = (b_alias == b.display_id) ? 1 : 0;
1649 return (b_is_empty + b_is_display_id) -
1650 (a_is_empty + a_is_display_id);
1653 string alias = ""; /* must not be null */
1657 alias = ((AliasDetails) p).alias.strip ();
1660 /* Only notify if the value has changed. We have to set the private
1661 * member and notify manually, or we'd end up propagating the new
1662 * alias back down to all our Personas, even if it's a fallback
1663 * display ID or something else undesirable. */
1664 if (this._alias != alias)
1666 this._alias = alias;
1667 this.notify_property ("alias");
1672 private void _update_avatar ()
1674 this._update_single_valued_property (typeof (AvatarDetails), (p) =>
1676 return ((AvatarDetails) p).avatar != null;
1679 /* We can't compare two set avatars efficiently. See: bgo#652721. */
1683 LoadableIcon? avatar = null;
1687 avatar = ((AvatarDetails) p).avatar;
1690 /* only notify if the value has changed */
1691 if ((this._avatar == null && avatar != null) ||
1692 (this._avatar != null &&
1693 (avatar == null || !((!) this._avatar).equal (avatar))))
1695 this._avatar = avatar;
1696 this.notify_property ("avatar");
1701 private void _update_trust_level ()
1703 var trust_level = TrustLevel.PERSONAS;
1705 foreach (var p in this._persona_set)
1707 if (p.is_user == false &&
1708 p.store.trust_level == PersonaStoreTrust.NONE)
1709 trust_level = TrustLevel.NONE;
1712 /* Only notify if the value has changed */
1713 if (this.trust_level != trust_level)
1714 this.trust_level = trust_level;
1717 private void _update_im_addresses (bool create_if_not_exist, bool emit_notification = true, bool force_update = true)
1719 this._update_multi_valued_property ("im-addresses",
1720 create_if_not_exist, () => { return this._im_addresses == null; },
1723 this._im_addresses = new HashMultiMap<string, ImFieldDetails> (
1724 null, null, ImFieldDetails.hash,
1725 (EqualFunc) ImFieldDetails.equal);
1729 var new_im_addresses = new HashMultiMap<string, ImFieldDetails> (
1730 null, null, (GLib.HashFunc) ImFieldDetails.hash,
1731 (GLib.EqualFunc) ImFieldDetails.equal);
1733 foreach (var persona in this._persona_set)
1735 /* We only care about personas implementing the given interface. */
1736 var im_details = persona as ImDetails;
1737 if (im_details != null)
1739 foreach (var cur_protocol in
1740 im_details.im_addresses.get_keys ())
1743 im_details.im_addresses.get (cur_protocol);
1745 foreach (var address in cur_addresses)
1747 new_im_addresses.set (cur_protocol, address);
1753 if (!Utils.multi_map_str_afd_equal (new_im_addresses,
1754 this._im_addresses))
1756 this._im_addresses = new_im_addresses;
1761 }, emit_notification, force_update);
1764 private void _update_web_service_addresses (bool create_if_not_exist, bool emit_notification = true, bool force_update = true)
1766 this._update_multi_valued_property ("web-service-addresses",
1767 create_if_not_exist,
1768 () => { return this._web_service_addresses == null; },
1771 this._web_service_addresses =
1772 new HashMultiMap<string, WebServiceFieldDetails> (null, null,
1773 (GLib.HashFunc) WebServiceFieldDetails.hash,
1774 (GLib.EqualFunc) WebServiceFieldDetails.equal);
1778 var new_web_service_addresses =
1779 new HashMultiMap<string, WebServiceFieldDetails> (null, null,
1780 (GLib.HashFunc) WebServiceFieldDetails.hash,
1781 (GLib.EqualFunc) WebServiceFieldDetails.equal);
1783 foreach (var persona in this._persona_set)
1785 /* We only care about personas implementing the given interface. */
1786 var web_service_details = persona as WebServiceDetails;
1787 if (web_service_details != null)
1789 foreach (var cur_web_service in
1790 web_service_details.web_service_addresses.get_keys ())
1793 web_service_details.web_service_addresses.get (
1796 foreach (var ws_fd in cur_addresses)
1798 new_web_service_addresses.set (cur_web_service,
1805 if (!Utils.multi_map_str_afd_equal (new_web_service_addresses,
1806 this._web_service_addresses))
1808 this._web_service_addresses = new_web_service_addresses;
1813 }, emit_notification, force_update);
1816 private void _connect_to_persona (Persona persona)
1818 persona.individual = this;
1820 persona.notify["alias"].connect (this._notify_alias_cb);
1821 persona.notify["avatar"].connect (this._notify_avatar_cb);
1822 persona.notify["presence-message"].connect (this._notify_presence_cb);
1823 persona.notify["presence-type"].connect (this._notify_presence_cb);
1824 persona.notify["im-addresses"].connect (this._notify_im_addresses_cb);
1825 persona.notify["web-service-addresses"].connect
1826 (this._notify_web_service_addresses_cb);
1827 persona.notify["is-favourite"].connect (this._notify_is_favourite_cb);
1828 persona.notify["structured-name"].connect (
1829 this._notify_structured_name_cb);
1830 persona.notify["full-name"].connect (this._notify_full_name_cb);
1831 persona.notify["nickname"].connect (this._notify_nickname_cb);
1832 persona.notify["gender"].connect (this._notify_gender_cb);
1833 persona.notify["urls"].connect (this._notify_urls_cb);
1834 persona.notify["phone-numbers"].connect (this._notify_phone_numbers_cb);
1835 persona.notify["email-addresses"].connect (
1836 this._notify_email_addresses_cb);
1837 persona.notify["roles"].connect (this._notify_roles_cb);
1838 persona.notify["birthday"].connect (this._notify_birthday_cb);
1839 persona.notify["notes"].connect (this._notify_notes_cb);
1840 persona.notify["postal-addresses"].connect
1841 (this._notify_postal_addresses_cb);
1842 persona.notify["local-ids"].connect
1843 (this._notify_local_ids_cb);
1846 if (persona is GroupDetails)
1848 ((GroupDetails) persona).group_changed.connect (
1849 this._persona_group_changed_cb);
1851 /* Subscribe to the interactions signal for the persona */
1852 var p_interaction_details = persona as InteractionDetails;
1853 if (p_interaction_details != null)
1855 persona.notify["im-interaction-count"].connect (this._notify_im_interaction_count_cb);
1856 persona.notify["call-interaction-count"].connect (this._notify_call_interaction_count_cb);
1857 persona.notify["last-im-interaction-datetime"].connect (this._notify_last_im_interaction_datetime_cb);
1858 persona.notify["last-call-interaction-datetime"].connect (this._notify_last_call_interaction_datetime_cb);
1862 private void _update_structured_name ()
1864 this._update_single_valued_property (typeof (NameDetails), (p) =>
1866 var name = ((NameDetails) p).structured_name;
1867 return (name != null && !((!) name).is_empty ());
1870 /* Can't compare two set names. */
1872 }, "structured-name", (p) =>
1874 StructuredName? name = null;
1878 name = ((NameDetails) p).structured_name;
1880 if (name != null && ((!) name).is_empty ())
1886 if ((this._structured_name == null && name != null) ||
1887 (this._structured_name != null &&
1888 (name == null || !((!) this._structured_name).equal ((!) name))))
1890 this._structured_name = name;
1891 this.notify_property ("structured-name");
1896 private void _update_full_name ()
1898 this._update_single_valued_property (typeof (NameDetails), (p) =>
1900 var name = ((NameDetails) p).full_name;
1901 return_val_if_fail (name != null, false);
1903 return (name.strip () != ""); /* empty names are unset */
1906 /* Can't compare two set names. */
1908 }, "full-name", (p) =>
1910 string new_full_name = ""; /* must not be null */
1914 new_full_name = ((NameDetails) p).full_name.strip ();
1917 if (new_full_name != this._full_name)
1919 this._full_name = new_full_name;
1920 this.notify_property ("full-name");
1925 private void _update_nickname ()
1927 this._update_single_valued_property (typeof (NameDetails), (p) =>
1929 var nickname = ((NameDetails) p).nickname;
1930 return_val_if_fail (nickname != null, false);
1932 return (nickname.strip () != ""); /* empty names are unset */
1935 /* Can't compare two set names. */
1937 }, "nickname", (p) =>
1939 string new_nickname = ""; /* must not be null */
1943 new_nickname = ((NameDetails) p).nickname.strip ();
1946 if (new_nickname != this._nickname)
1948 this._nickname = new_nickname;
1949 this.notify_property ("nickname");
1954 private void _disconnect_from_persona (Persona persona,
1955 Individual? replacement_individual)
1957 persona.notify["alias"].disconnect (this._notify_alias_cb);
1958 persona.notify["avatar"].disconnect (this._notify_avatar_cb);
1959 persona.notify["presence-message"].disconnect (
1960 this._notify_presence_cb);
1961 persona.notify["presence-type"].disconnect (this._notify_presence_cb);
1962 persona.notify["im-addresses"].disconnect (
1963 this._notify_im_addresses_cb);
1964 persona.notify["web-service-addresses"].disconnect (
1965 this._notify_web_service_addresses_cb);
1966 persona.notify["is-favourite"].disconnect (
1967 this._notify_is_favourite_cb);
1968 persona.notify["structured-name"].disconnect (
1969 this._notify_structured_name_cb);
1970 persona.notify["full-name"].disconnect (this._notify_full_name_cb);
1971 persona.notify["nickname"].disconnect (this._notify_nickname_cb);
1972 persona.notify["gender"].disconnect (this._notify_gender_cb);
1973 persona.notify["urls"].disconnect (this._notify_urls_cb);
1974 persona.notify["phone-numbers"].disconnect (
1975 this._notify_phone_numbers_cb);
1976 persona.notify["email-addresses"].disconnect (
1977 this._notify_email_addresses_cb);
1978 persona.notify["roles"].disconnect (this._notify_roles_cb);
1979 persona.notify["birthday"].disconnect (this._notify_birthday_cb);
1980 persona.notify["notes"].disconnect (this._notify_notes_cb);
1981 persona.notify["postal-addresses"].disconnect
1982 (this._notify_postal_addresses_cb);
1983 persona.notify["local-ids"].disconnect (this._notify_local_ids_cb);
1986 if (persona is GroupDetails)
1988 ((GroupDetails) persona).group_changed.disconnect (
1989 this._persona_group_changed_cb);
1992 /* Unsubscribe from the interactions signal for the persona */
1993 var p_interaction_details = persona as InteractionDetails;
1994 if (p_interaction_details != null)
1996 persona.notify["im-interaction-count"].disconnect (this._notify_im_interaction_count_cb);
1997 persona.notify["call-interaction-count"].disconnect (this._notify_call_interaction_count_cb);
1998 persona.notify["last-im-interaction-datetime"].disconnect (this._notify_last_im_interaction_datetime_cb);
1999 persona.notify["last-call-interaction-datetime"].disconnect (this._notify_last_call_interaction_datetime_cb);
2002 /* Don't update the individual if the persona's been added to the new one
2003 * already (and thus the new individual has already changed
2004 * persona.individual).
2006 * FIXME: Ideally, we'd assert that a persona can't be added to a new
2007 * individual before it's removed from the old one. However, this
2008 * currently isn't possible due to the way the aggregator works. When the
2009 * aggregator's rewritten, it would be nice to fix this. */
2010 if (persona.individual == this)
2012 /* It may be the case that the persona's being removed from the
2013 * individual (i.e. the replacement individual is non-null, but
2014 * doesn't contain this persona). In this case, we need to set the
2015 * persona's individual to null. */
2016 if (replacement_individual != null &&
2017 persona in ((!) replacement_individual).personas)
2019 persona.individual = replacement_individual;
2023 persona.individual = null;
2028 private void _update_gender ()
2030 this._update_single_valued_property (typeof (GenderDetails), (p) =>
2032 return ((GenderDetails) p).gender != Gender.UNSPECIFIED;
2035 /* It would be sexist to rank one gender over another.
2036 * Besides, how often will we see two personas in the same individual
2037 * which have different genders? */
2041 var new_gender = Gender.UNSPECIFIED;
2045 new_gender = ((GenderDetails) p).gender;
2048 if (new_gender != this.gender)
2050 this._gender = new_gender;
2051 this.notify_property ("gender");
2056 private void _update_urls (bool create_if_not_exist, bool emit_notification = true, bool force_update = true)
2058 this._update_multi_valued_property ("urls", create_if_not_exist,
2059 () => { return this._urls == null; },
2062 this._urls = new HashSet<UrlFieldDetails> (
2063 (GLib.HashFunc) UrlFieldDetails.hash,
2064 (GLib.EqualFunc) UrlFieldDetails.equal);
2065 this._urls_ro = this._urls.read_only_view;
2069 var new_urls = new HashSet<UrlFieldDetails> (
2070 (GLib.HashFunc) UrlFieldDetails.hash,
2071 (GLib.EqualFunc) UrlFieldDetails.equal);
2072 var urls_set = new HashMap<string, UrlFieldDetails> (null, null,
2073 (GLib.EqualFunc) UrlFieldDetails.equal);
2075 foreach (var persona in this._persona_set)
2077 /* We only care about personas implementing the given
2078 * interface. If the same URL exists multiple times we merge
2079 * the parameters. */
2080 var url_details = persona as UrlDetails;
2081 if (url_details != null)
2083 foreach (var url_fd in ((!) url_details).urls)
2085 var existing = urls_set.get (url_fd.value);
2086 if (existing != null)
2088 existing.extend_parameters (url_fd.parameters);
2093 new UrlFieldDetails (url_fd.value);
2094 new_url_fd.extend_parameters (url_fd.parameters);
2095 urls_set.set (new_url_fd.value, new_url_fd);
2096 new_urls.add (new_url_fd);
2102 if (!Utils.set_afd_equal (new_urls, this._urls))
2104 this._urls = new_urls;
2105 this._urls_ro = new_urls.read_only_view;
2110 }, emit_notification, force_update);
2113 private void _update_phone_numbers (bool create_if_not_exist, bool emit_notification = true, bool force_update = true)
2115 this._update_multi_valued_property ("phone-numbers", create_if_not_exist,
2116 () => { return this._phone_numbers == null; },
2119 this._phone_numbers = new HashSet<PhoneFieldDetails> (
2120 (GLib.HashFunc) PhoneFieldDetails.hash,
2121 (GLib.EqualFunc) PhoneFieldDetails.equal);
2122 this._phone_numbers_ro = this._phone_numbers.read_only_view;
2126 var new_phone_numbers = new HashSet<PhoneFieldDetails> (
2127 (GLib.HashFunc) PhoneFieldDetails.hash,
2128 (GLib.EqualFunc) PhoneFieldDetails.equal);
2129 var phone_numbers_set = new HashMap<string, PhoneFieldDetails> (
2130 null, null, (GLib.EqualFunc) PhoneFieldDetails.equal);
2132 foreach (var persona in this._persona_set)
2134 /* We only care about personas implementing the given
2135 * interface. If the same phone number exists multiple times
2136 * we merge the parameters. */
2137 var phone_details = persona as PhoneDetails;
2138 if (phone_details != null)
2140 foreach (var phone_fd in ((!) phone_details).phone_numbers)
2142 var existing = phone_numbers_set.get (phone_fd.value);
2143 if (existing != null)
2145 existing.extend_parameters (phone_fd.parameters);
2150 new PhoneFieldDetails (phone_fd.value);
2151 new_fd.extend_parameters (phone_fd.parameters);
2152 phone_numbers_set.set (new_fd.value, new_fd);
2153 new_phone_numbers.add (new_fd);
2159 if (!Utils.set_afd_equal (new_phone_numbers, this._phone_numbers))
2161 this._phone_numbers = new_phone_numbers;
2162 this._phone_numbers_ro = new_phone_numbers.read_only_view;
2167 }, emit_notification, force_update);
2170 private void _update_email_addresses (bool create_if_not_exist, bool emit_notification = true, bool force_update = true)
2172 this._update_multi_valued_property ("email-addresses",
2173 create_if_not_exist, () => { return this._email_addresses == null; },
2176 this._email_addresses = new HashSet<EmailFieldDetails> (
2177 (GLib.HashFunc) EmailFieldDetails.hash,
2178 (GLib.EqualFunc) EmailFieldDetails.equal);
2179 this._email_addresses_ro = this._email_addresses.read_only_view;
2183 var new_email_addresses = new HashSet<EmailFieldDetails> (
2184 (GLib.HashFunc) EmailFieldDetails.hash,
2185 (GLib.EqualFunc) EmailFieldDetails.equal);
2186 var emails_set = new HashMap<string, EmailFieldDetails> (
2187 null, null, (GLib.EqualFunc) EmailFieldDetails.equal);
2189 foreach (var persona in this._persona_set)
2191 /* We only care about personas implementing the given
2192 * interface. If the same e-mail address exists multiple times
2193 * we merge the parameters. */
2194 var email_details = persona as EmailDetails;
2195 if (email_details != null)
2197 foreach (var email_fd in ((!) email_details).email_addresses)
2199 var existing = emails_set.get (email_fd.value);
2200 if (existing != null)
2202 existing.extend_parameters (email_fd.parameters);
2207 new EmailFieldDetails (email_fd.value,
2208 email_fd.parameters);
2209 emails_set.set (new_email_fd.value, new_email_fd);
2210 new_email_addresses.add (new_email_fd);
2216 if (!Utils.set_afd_equal (new_email_addresses,
2217 this._email_addresses))
2219 this._email_addresses = new_email_addresses;
2220 this._email_addresses_ro = new_email_addresses.read_only_view;
2225 }, emit_notification, force_update);
2228 private void _update_roles (bool create_if_not_exist, bool emit_notification = true, bool force_update = true)
2230 this._update_multi_valued_property ("roles", create_if_not_exist,
2231 () => { return this._roles == null; },
2234 this._roles = new HashSet<RoleFieldDetails> (
2235 (GLib.HashFunc) RoleFieldDetails.hash,
2236 (GLib.EqualFunc) RoleFieldDetails.equal);
2237 this._roles_ro = this._roles.read_only_view;
2241 var new_roles = new HashSet<RoleFieldDetails> (
2242 (GLib.HashFunc) RoleFieldDetails.hash,
2243 (GLib.EqualFunc) RoleFieldDetails.equal);
2245 foreach (var persona in this._persona_set)
2247 var role_details = persona as RoleDetails;
2248 if (role_details != null)
2250 foreach (var role_fd in ((!) role_details).roles)
2252 new_roles.add (role_fd);
2257 if (!Utils.set_afd_equal (new_roles, this._roles))
2259 this._roles = new_roles;
2260 this._roles_ro = new_roles.read_only_view;
2265 }, emit_notification, force_update);
2268 private void _update_local_ids (bool create_if_not_exist, bool emit_notification = true, bool force_update = true)
2270 this._update_multi_valued_property ("local-ids", create_if_not_exist,
2271 () => { return this._local_ids == null; },
2274 this._local_ids = new HashSet<string> ();
2275 this._local_ids_ro = this._local_ids.read_only_view;
2279 var new_local_ids = new HashSet<string> ();
2281 foreach (var persona in this._persona_set)
2283 var local_id_details = persona as LocalIdDetails;
2284 if (local_id_details != null)
2286 foreach (var id in ((!) local_id_details).local_ids)
2288 new_local_ids.add (id);
2293 if (new_local_ids.size != this._local_ids.size ||
2294 !new_local_ids.contains_all (this._local_ids))
2296 this._local_ids = new_local_ids;
2297 this._local_ids_ro = new_local_ids.read_only_view;
2302 }, emit_notification, force_update);
2305 private void _update_postal_addresses (bool create_if_not_exist, bool emit_notification = true, bool force_update = true)
2307 /* FIXME: Detect duplicates somehow? */
2308 this._update_multi_valued_property ("postal-addresses",
2309 create_if_not_exist, () => { return this._postal_addresses == null; },
2312 this._postal_addresses = new HashSet<PostalAddressFieldDetails> (
2313 (GLib.HashFunc) PostalAddressFieldDetails.hash,
2314 (GLib.EqualFunc) PostalAddressFieldDetails.equal);
2315 this._postal_addresses_ro = this._postal_addresses.read_only_view;
2319 var new_postal_addresses =
2320 new HashSet<PostalAddressFieldDetails> (
2321 (GLib.HashFunc) PostalAddressFieldDetails.hash,
2322 (GLib.EqualFunc) PostalAddressFieldDetails.equal);
2324 foreach (var persona in this._persona_set)
2326 var postal_address_details = persona as PostalAddressDetails;
2327 if (postal_address_details != null)
2329 foreach (var pafd in
2330 ((!) postal_address_details).postal_addresses)
2332 new_postal_addresses.add (pafd);
2337 if (!Utils.set_afd_equal (new_postal_addresses,
2338 this._postal_addresses))
2340 this._postal_addresses = new_postal_addresses;
2341 this._postal_addresses_ro =
2342 new_postal_addresses.read_only_view;
2347 }, emit_notification, force_update);
2350 private void _update_birthday ()
2352 this._update_single_valued_property (typeof (BirthdayDetails), (p) =>
2354 var details = ((BirthdayDetails) p);
2355 return details.birthday != null && details.calendar_event_id != null;
2358 var a_birthday = ((BirthdayDetails) a).birthday;
2359 var b_birthday = ((BirthdayDetails) b).birthday;
2360 var a_event_id = ((BirthdayDetails) a).calendar_event_id;
2361 var b_event_id = ((BirthdayDetails) b).calendar_event_id;
2363 var a_birthday_is_set = (a_birthday != null) ? 1 : 0;
2364 var b_birthday_is_set = (b_birthday != null) ? 1 : 0;
2366 /* We consider the empty string as “set” because it's an opaque ID. */
2367 var a_event_id_is_set = (a_event_id != null) ? 1 : 0;
2368 var b_event_id_is_set = (b_event_id != null) ? 1 : 0;
2370 /* Prefer personas which have both properties set over those who have
2371 * only one set. We don't consider the case where the birthdays from
2372 * different personas don't match, because that's just scary. */
2373 return (a_birthday_is_set + a_event_id_is_set) -
2374 (b_birthday_is_set + b_event_id_is_set);
2375 }, "birthday", (p) =>
2377 unowned DateTime? bday = null;
2378 unowned string? calendar_event_id = null;
2382 bday = ((BirthdayDetails) p).birthday;
2383 calendar_event_id = ((BirthdayDetails) p).calendar_event_id;
2386 if ((this._birthday == null && bday != null) ||
2387 (this._birthday != null &&
2388 (bday == null || !((!) this._birthday).equal ((!) bday))) ||
2389 (this._calendar_event_id != calendar_event_id))
2391 this._birthday = bday;
2392 this._calendar_event_id = calendar_event_id;
2394 this.freeze_notify ();
2395 this.notify_property ("birthday");
2396 this.notify_property ("calendar-event-id");
2397 this.thaw_notify ();
2402 private void _update_notes (bool create_if_not_exist, bool emit_notification = true, bool force_update = true)
2404 this._update_multi_valued_property ("notes", create_if_not_exist,
2405 () => { return this._notes == null; },
2408 this._notes = new HashSet<NoteFieldDetails> (
2409 (GLib.HashFunc) NoteFieldDetails.hash,
2410 (GLib.EqualFunc) NoteFieldDetails.equal);
2411 this._notes_ro = this._notes.read_only_view;
2415 var new_notes = new HashSet<NoteFieldDetails> (
2416 (GLib.HashFunc) NoteFieldDetails.hash,
2417 (GLib.EqualFunc) NoteFieldDetails.equal);
2419 foreach (var persona in this._persona_set)
2421 var note_details = persona as NoteDetails;
2422 if (note_details != null)
2424 foreach (var n in ((!) note_details).notes)
2431 if (!Utils.set_afd_equal (new_notes, this._notes))
2433 this._notes = new_notes;
2434 this._notes_ro = new_notes.read_only_view;
2439 }, emit_notification, force_update);
2442 private void _set_personas (Set<Persona>? personas,
2443 Individual? replacement_individual)
2445 assert (replacement_individual == null || replacement_individual != this);
2447 var added = new HashSet<Persona> ();
2448 var removed = new HashSet<Persona> ();
2450 /* Determine which Personas have been added. If personas == null, we
2451 * assume it's an empty set. */
2452 if (personas != null)
2454 foreach (var p in (!) personas)
2456 if (!this._persona_set.contains (p))
2458 /* Keep track of how many Personas are users */
2460 this._persona_user_count++;
2464 this._persona_set.add (p);
2465 this._connect_to_persona (p);
2467 /* Increment the Persona count for this PersonaStore */
2468 var store = p.store;
2469 var num_from_store = this._stores.get (store);
2470 if (num_from_store == 0)
2472 this._stores.set (store, num_from_store + 1);
2476 this._stores.set (store, 1);
2478 store.removed.connect (this._store_removed_cb);
2479 store.personas_changed.connect (
2480 this._store_personas_changed_cb);
2486 /* Determine which Personas have been removed */
2487 foreach (var p in this._persona_set)
2489 if (personas == null || !((!) personas).contains (p))
2491 /* Keep track of how many Personas are users */
2493 this._persona_user_count--;
2497 /* Decrement the Persona count for this PersonaStore */
2498 var store = p.store;
2499 var num_from_store = this._stores.get (store);
2500 if (num_from_store > 1)
2502 this._stores.set (store, num_from_store - 1);
2506 store.removed.disconnect (this._store_removed_cb);
2507 store.personas_changed.disconnect (
2508 this._store_personas_changed_cb);
2510 this._stores.unset (store);
2513 this._disconnect_from_persona (p, replacement_individual);
2517 foreach (var p in removed)
2519 this._persona_set.remove (p);
2522 this._emit_personas_changed (added, removed);
2524 /* Update this.is_user */
2525 var new_is_user = (this._persona_user_count > 0) ? true : false;
2526 if (new_is_user != this.is_user)
2527 this.is_user = new_is_user;
2529 /* If all the Personas have been removed, remove the Individual */
2530 if (this._persona_set.size < 1)
2532 this.removed (replacement_individual);
2536 /* Update the ID. We choose the most interesting Persona in the
2537 * Individual and hash their UID. This is guaranteed to be globally
2538 * unique, and may not change (for one of the two Individuals) if we link
2539 * two Individuals together, which is nice though we can't rely on this
2542 * This method of constructing an ID ensures that it'll be unique and
2543 * stable for a given Individual once the IndividualAggregator reaches
2544 * a quiescent state after startup. It guarantees that the ID will be
2545 * the same every time folks is used, until the Individual is linked
2546 * or unlinked to another Individual.
2548 * We choose the most interesting Persona by ranking all the Personas
2549 * in the Individual by:
2550 * 1. store.is-primary-store
2551 * 2. store.trust-level
2552 * 3. store.id (alphabetically)
2553 * 4. persona.uid (alphabetically)
2555 * Note that this heuristic shouldn't be changed without careful thought,
2556 * since stored references to IDs may be broken by the change.
2558 if (this._persona_set.size > 0)
2560 Persona? chosen_persona = null;
2562 foreach (var persona in this._persona_set)
2564 if (chosen_persona == null)
2566 chosen_persona = persona;
2570 var _chosen_persona = (!) chosen_persona;
2572 if ((_chosen_persona.store.is_primary_store == false &&
2573 persona.store.is_primary_store == true) ||
2574 (_chosen_persona.store.is_primary_store ==
2575 persona.store.is_primary_store &&
2576 _chosen_persona.store.trust_level >
2577 persona.store.trust_level) ||
2578 (_chosen_persona.store.is_primary_store ==
2579 persona.store.is_primary_store &&
2580 _chosen_persona.store.trust_level ==
2581 persona.store.trust_level &&
2582 _chosen_persona.store.id > persona.store.id) ||
2583 (_chosen_persona.store.is_primary_store ==
2584 persona.store.is_primary_store &&
2585 _chosen_persona.store.trust_level ==
2586 persona.store.trust_level &&
2587 _chosen_persona.store.id == persona.store.id &&
2588 _chosen_persona.uid > persona.uid)
2591 chosen_persona = persona;
2595 /* Hash the chosen persona's UID. We can guarantee chosen_persona is
2596 * non-null here because it's at least set to the first element of
2597 * this._persona_set, which we've checked is non-empty. */
2598 this.id = Checksum.compute_for_string (ChecksumType.SHA1,
2599 ((!) chosen_persona).uid);
2602 /* Update our aggregated fields and notify the changes */
2603 this._update_fields ();
2606 internal void replace (Individual replacement_individual)
2608 this._set_personas (null, replacement_individual);
2612 * Anti-linked with a persona?
2614 * Check whether this individual is anti-linked to {@link Persona} ``p`` at
2615 * all. If so, ``true`` will be returned — ``false`` will be returned
2618 * Note that this will check for anti-links in either direction, since
2619 * anti-links are not necessarily symmetric.
2621 * @param p persona to check for anti-links with
2622 * @return ``true`` if this individual is anti-linked with persona ``p``;
2627 public bool has_anti_link_with_persona (Persona p)
2629 var al = p as AntiLinkable;
2631 foreach (var persona in this._persona_set)
2633 var pl = persona as AntiLinkable;
2635 if ((al != null && ((!) al).has_anti_link_with_persona (persona)) ||
2636 (pl != null && ((!) pl).has_anti_link_with_persona (p)))
2646 * Anti-linked with an individual?
2648 * Check whether this individual is anti-linked to any of the {@link Persona}s
2649 * in {@link Folks.Individual} ``i``. If so, ``true`` will be returned —
2650 * ``false`` will be returned otherwise.
2652 * Note that this will check for anti-links in either direction, since
2653 * anti-links are not necessarily symmetric.
2655 * @param i individual to check for anti-links with
2656 * @return ``true`` if this individual is anti-linked with individual ``i``;
2657 * ``false`` otherwise
2660 public bool has_anti_link_with_individual (Individual i)
2662 foreach (var p in i.personas)
2664 if (this.has_anti_link_with_persona (p) == true)