2 * Copyright (C) 2010 Collabora Ltd.
4 * This library is free software: you can redistribute it and/or modify
5 * it under the terms of the GNU Lesser General Public License as published by
6 * the Free Software Foundation, either version 2.1 of the License, or
7 * (at your option) any later version.
9 * This library is distributed in the hope that it will be useful,
10 * but WITHOUT ANY WARRANTY; without even the implied warranty of
11 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 * GNU Lesser General Public License for more details.
14 * You should have received a copy of the GNU Lesser General Public License
15 * along with this library. If not, see <http://www.gnu.org/licenses/>.
18 * Travis Reitter <travis.reitter@collabora.co.uk>
25 * Trust level for an {@link Individual} for use in the UI.
29 public enum Folks.TrustLevel
32 * The {@link Individual}'s {@link Persona}s aren't trusted at all.
34 * This is the trust level for an {@link Individual} which contains one or
35 * more {@link Persona}s which cannot be guaranteed to be the same
36 * {@link Persona}s as were originally linked together.
38 * For example, an {@link Individual} containing a link-local XMPP
39 * {@link Persona} would have this trust level, since someone else could
40 * easily spoof the link-local XMPP {@link Persona}'s identity.
47 * The {@link Individual}'s {@link Persona}s are trusted.
49 * This trust level is for {@link Individual}s where it can be guaranteed
50 * that all the {@link Persona}s are the same ones as when they were
51 * originally linked together.
53 * Note that this doesn't guarantee that the user who behind each
54 * {@link Persona} is who they claim to be.
62 * A physical person, aggregated from the various {@link Persona}s the person
63 * might have, such as their different IM addresses or vCard entries.
65 public class Folks.Individual : Object,
82 private bool _is_favourite;
83 private string _alias;
84 private HashTable<string, bool> _groups;
85 /* These two data structures should store exactly the same set of Personas:
86 * the Personas contained in this Individual. The HashSet is used for fast
87 * lookups, whereas the List is used for iteration.
88 * The Individual's references to its Personas are kept by the HashSet;
89 * since the List contains the same set of Personas, it doesn't need an
90 * extra reference (and due to bgo#624249, this is a good thing). */
91 private GLib.List<unowned Persona> _persona_list;
92 private HashSet<Persona> _persona_set;
93 /* Mapping from PersonaStore -> number of Personas from that store contained
94 * in this Individual. There shouldn't be any entries with a number < 1.
95 * This is used for working out when to disconnect from store signals. */
96 private HashMap<PersonaStore, uint> _stores;
97 /* The number of Personas in this Individual which have
98 * Persona.is_user == true. Iff this is > 0, Individual.is_user == true. */
99 private uint _persona_user_count = 0;
100 private HashTable<string, LinkedHashSet<string>> _im_addresses;
103 * The trust level of the Individual.
105 * This specifies how far the Individual can be trusted to be who it claims
106 * to be. See the descriptions for the elements of {@link TrustLevel}.
108 * Clients should ''not'' allow linking of Individuals who have a trust level
109 * of {@link TrustLevel.NONE}.
113 public TrustLevel trust_level { get; private set; }
118 public File avatar { get; private set; }
123 public Folks.PresenceType presence_type { get; private set; }
128 public string presence_message { get; private set; }
131 * Whether the Individual is the user.
133 * Iff the Individual represents the user (the person who owns the
134 * account in the backend for each {@link Persona} in the Individual)
137 * It is //not// guaranteed that every {@link Persona} in the Individual has
138 * its {@link Persona.is_user} set to the same value as the Individual. For
139 * example, the user could own two Telepathy accounts, and have added the
140 * other account as a contact in each account. The accounts will expose a
141 * {@link Persona} for the user (which will have {@link Persona.is_user} set
142 * to `true`) //and// a {@link Persona} for the contact for the other account
143 * (which will have {@link Persona.is_user} set to `false`).
145 * It is guaranteed that iff this property is set to `true` on an Individual,
146 * there will be at least one {@link Persona} in the Individual with its
147 * {@link Persona.is_user} set to `true`.
149 * It is guaranteed that there will only ever be one Individual with this
150 * property set to `true`.
154 public bool is_user { get; private set; }
157 * A unique identifier for the Individual.
159 * This uniquely identifies the Individual, and persists across
160 * {@link IndividualAggregator} instances.
162 * FIXME: Will this.id actually be the persistent ID for storage?
164 public string id { get; private set; }
167 * Emitted when the last of the Individual's {@link Persona}s has been
170 * At this point, the Individual is invalid, so any client referencing it
171 * should unreference it and remove it from their UI.
173 * @param replacement_individual the individual which has replaced this one
174 * due to linking, or `null` if this individual was removed for another reason
177 public signal void removed (Individual? replacement_individual);
184 get { return this._alias; }
188 if (this._alias == value)
193 debug ("Setting alias of individual '%s' to '%s'…", this.id, value);
195 /* First, try to write it to only the writeable Personas… */
196 var alias_changed = false;
197 this._persona_list.foreach ((p) =>
199 if (p is AliasDetails &&
200 ((Persona) p).store.is_writeable == true)
202 debug (" written to writeable persona '%s'",
204 ((AliasDetails) p).alias = value;
205 alias_changed = true;
209 /* …but if there are no writeable Personas, we have to fall back to
210 * writing it to every Persona. */
211 if (alias_changed == false)
213 this._persona_list.foreach ((p) =>
215 if (p is AliasDetails)
217 debug (" written to non-writeable persona '%s'",
219 ((AliasDetails) p).alias = value;
229 public StructuredName structured_name { get; private set; }
234 public string full_name { get; private set; }
236 private string _nickname;
240 public string nickname { get { return this._nickname; } }
242 private Gender _gender;
248 get { return this._gender; }
251 this._gender = value;
252 this.notify_property ("gender");
256 private GLib.List<FieldDetails> _urls;
260 public GLib.List<FieldDetails> urls
262 get { return this._urls; }
265 this._urls = new GLib.List<FieldDetails> ();
266 foreach (unowned FieldDetails ps in value)
267 this._urls.prepend (ps);
268 this._urls.reverse ();
272 private GLib.List<FieldDetails> _phone_numbers;
276 public GLib.List<FieldDetails> phone_numbers
278 get { return this._phone_numbers; }
281 this._phone_numbers = new GLib.List<FieldDetails> ();
282 foreach (unowned FieldDetails fd in value)
283 this._phone_numbers.prepend (fd);
284 this._phone_numbers.reverse ();
288 private GLib.List<FieldDetails> _email_addresses;
292 public GLib.List<FieldDetails> email_addresses
294 get { return this._email_addresses; }
297 this._email_addresses = new GLib.List<FieldDetails> ();
298 foreach (unowned FieldDetails fd in value)
299 this._email_addresses.prepend (fd);
300 this._email_addresses.reverse ();
304 private HashSet<Role> _roles;
308 public HashSet<Role> roles
310 get { return this._roles; }
314 this.notify_property ("roles");
318 public DateTime birthday { get; set; }
320 public string calendar_event_id { get; set; }
322 private HashSet<Note> _notes;
326 public HashSet<Note> notes
328 get { return this._notes; }
332 this.notify_property ("notes");
336 private GLib.List<PostalAddress> _postal_addresses;
340 public GLib.List<PostalAddress> postal_addresses
342 get { return this._postal_addresses; }
345 this._postal_addresses = new GLib.List<PostalAddress> ();
346 foreach (PostalAddress pa in value)
347 this._postal_addresses.prepend (pa);
348 this._postal_addresses.reverse ();
353 * Whether this Individual is a user-defined favourite.
355 * This property is `true` if any of this Individual's {@link Persona}s are
358 public bool is_favourite
360 get { return this._is_favourite; }
364 if (this._is_favourite == value)
367 debug ("Setting '%s' favourite status to %s", this.id,
368 value ? "TRUE" : "FALSE");
370 this._is_favourite = value;
371 this._persona_list.foreach ((p) =>
373 if (p is Favouritable)
375 SignalHandler.block_by_func (p,
376 (void*) this._notify_is_favourite_cb, this);
377 ((Favouritable) p).is_favourite = value;
378 SignalHandler.unblock_by_func (p,
379 (void*) this._notify_is_favourite_cb, this);
388 public HashTable<string, bool> groups
390 get { return this._groups; }
394 this._groups = value;
395 this._persona_list.foreach ((p) =>
397 if (p is Groupable && ((Persona) p).store.is_writeable == true)
398 ((Groupable) p).groups = value;
406 public HashTable<string, LinkedHashSet<string>> im_addresses
408 get { return this._im_addresses; }
413 * The set of {@link Persona}s encapsulated by this Individual.
415 * Changing the set of personas may cause updates to the aggregated properties
416 * provided by the Individual, resulting in property notifications for them.
418 * Changing the set of personas will not cause permanent linking/unlinking of
419 * the added/removed personas to/from this Individual. To do that, call
420 * {@link IndividualAggregator.link_personas} or
421 * {@link IndividualAggregator.unlink_individual}, which will ensure the link
422 * changes are written to the appropriate backend.
424 public GLib.List<Persona> personas
426 get { return this._persona_list; }
427 set { this._set_personas (value, null); }
431 * Emitted when one or more {@link Persona}s are added to or removed from
434 * @param added a list of {@link Persona}s which have been added
435 * @param removed a list of {@link Persona}s which have been removed
439 public signal void personas_changed (GLib.List<Persona>? added,
440 GLib.List<Persona>? removed);
442 private void _notify_alias_cb (Object obj, ParamSpec ps)
444 this._update_alias ();
447 private void _notify_avatar_cb (Object obj, ParamSpec ps)
449 this._update_avatar ();
452 private void _notify_full_name_cb ()
454 this._update_full_name ();
457 private void _notify_structured_name_cb ()
459 this._update_structured_name ();
462 private void _notify_nickname_cb ()
464 this._update_nickname ();
467 private void _persona_group_changed_cb (string group, bool is_member)
469 this._update_groups ();
472 private void _notify_gender_cb ()
474 this._update_gender ();
477 private void _notify_urls_cb ()
479 this._update_urls ();
482 private void _notify_phone_numbers_cb ()
484 this._update_phone_numbers ();
487 private void _notify_postal_addresses_cb ()
489 this._update_postal_addresses ();
492 private void _notify_email_addresses_cb ()
494 this._update_email_addresses ();
497 private void _notify_roles_cb ()
499 this._update_roles ();
502 private void _notify_birthday_cb ()
504 this._update_birthday ();
507 private void _notify_notes_cb ()
509 this._update_notes ();
513 * Add or remove the Individual from the specified group.
515 * If `is_member` is `true`, the Individual will be added to the `group`. If
516 * it is `false`, they will be removed from the `group`.
518 * The group membership change will propagate to every {@link Persona} in
521 * @param group a freeform group identifier
522 * @param is_member whether the Individual should be a member of the group
525 public async void change_group (string group, bool is_member)
527 this._persona_list.foreach ((p) =>
530 ((Groupable) p).change_group.begin (group, is_member);
533 /* don't notify, since it hasn't happened in the persona backing stores
534 * yet; react to that directly */
537 private void _notify_presence_cb (Object obj, ParamSpec ps)
539 this._update_presence ();
542 private void _notify_im_addresses_cb (Object obj, ParamSpec ps)
544 this._update_im_addresses ();
547 private void _notify_is_favourite_cb (Object obj, ParamSpec ps)
549 this._update_is_favourite ();
553 * Create a new Individual.
555 * The Individual can optionally be seeded with the {@link Persona}s in
556 * `personas`. Otherwise, it will have to have personas added using the
557 * {@link Folks.Individual.personas} property after construction.
559 * @param personas a list of {@link Persona}s to initialise the
560 * {@link Individual} with, or `null`
561 * @return a new Individual
563 public Individual (GLib.List<Persona>? personas)
566 new HashTable<string, LinkedHashSet<string>> (str_hash, str_equal);
567 this._persona_set = new HashSet<Persona> (null, null);
568 this._stores = new HashMap<PersonaStore, uint> (null, null);
569 this._gender = Gender.UNSPECIFIED;
570 this.personas = personas;
573 private void _store_removed_cb (PersonaStore store)
575 GLib.List<Persona> removed_personas = null;
576 var iter = this._persona_set.iterator ();
579 var persona = iter.get ();
581 removed_personas.prepend (persona);
582 this._persona_list.remove (persona);
586 if (removed_personas != null)
587 this.personas_changed (null, removed_personas);
590 this._stores.unset (store);
592 if (this._persona_set.size < 1)
598 this._update_fields ();
601 private void _store_personas_changed_cb (PersonaStore store,
602 GLib.List<Persona>? added,
603 GLib.List<Persona>? removed,
606 Groupable.ChangeReason reason)
608 GLib.List<Persona> removed_personas = null;
609 removed.foreach ((data) =>
611 var p = (Persona) data;
613 if (this._persona_set.remove (p))
615 removed_personas.prepend (p);
616 this._persona_list.remove (p);
620 if (removed_personas != null)
621 this.personas_changed (null, removed_personas);
623 if (this._persona_set.size < 1)
629 this._update_fields ();
632 private void _update_fields ()
634 this._update_groups ();
635 this._update_presence ();
636 this._update_is_favourite ();
637 this._update_avatar ();
638 this._update_alias ();
639 this._update_trust_level ();
640 this._update_im_addresses ();
641 this._update_structured_name ();
642 this._update_full_name ();
643 this._update_nickname ();
644 this._update_gender ();
645 this._update_urls ();
646 this._update_phone_numbers ();
647 this._update_email_addresses ();
648 this._update_roles ();
649 this._update_birthday ();
650 this._update_notes ();
651 this._update_postal_addresses ();
654 private void _update_groups ()
656 var new_groups = new HashTable<string, bool> (str_hash, str_equal);
658 /* this._groups is null during initial construction */
659 if (this._groups == null)
660 this._groups = new HashTable<string, bool> (str_hash, str_equal);
662 /* FIXME: this should partition the personas by store (maybe we should
663 * keep that mapping in general in this class), and execute
664 * "groups-changed" on the store (with the set of personas), to allow the
665 * back-end to optimize it (like Telepathy will for MembersChanged for the
666 * groups channel list) */
667 this._persona_list.foreach ((p) =>
671 var persona = (Groupable) p;
673 persona.groups.foreach ((k, v) =>
675 new_groups.insert ((string) k, true);
680 new_groups.foreach ((k, v) =>
682 unowned string group = (string) k;
683 if (this._groups.lookup (group) != true)
685 this._groups.insert (group, true);
686 this._groups.foreach ((k, v) =>
688 unowned string g = (string) k;
692 this.group_changed (group, true);
696 /* buffer the removals, so we don't remove while iterating */
697 var removes = new GLib.List<string> ();
698 this._groups.foreach ((k, v) =>
700 unowned string group = (string) k;
701 if (new_groups.lookup (group) != true)
702 removes.prepend (group);
705 removes.foreach ((l) =>
707 unowned string group = (string) l;
708 this._groups.remove (group);
709 this.group_changed (group, false);
713 private void _update_presence ()
715 var presence_message = "";
716 var presence_type = Folks.PresenceType.UNSET;
718 /* Choose the most available presence from our personas */
719 this._persona_list.foreach ((p) =>
721 if (p is PresenceOwner)
723 unowned PresenceOwner presence = (PresenceOwner) p;
725 if (PresenceOwner.typecmp (presence.presence_type,
728 presence_type = presence.presence_type;
729 presence_message = presence.presence_message;
734 if (presence_message == null)
735 presence_message = "";
737 /* only notify if the value has changed */
738 if (this.presence_message != presence_message)
739 this.presence_message = presence_message;
741 if (this.presence_type != presence_type)
742 this.presence_type = presence_type;
745 private void _update_is_favourite ()
747 var favourite = false;
749 debug ("Running _update_is_favourite() on '%s'", this.id);
751 this._persona_list.foreach ((p) =>
753 if (favourite == false && p is Favouritable)
755 favourite = ((Favouritable) p).is_favourite;
756 if (favourite == true)
761 /* Only notify if the value has changed. We have to set the private member
762 * and notify manually, or we'd end up propagating the new favourite
763 * status back down to all our Personas. */
764 if (this._is_favourite != favourite)
766 this._is_favourite = favourite;
767 this.notify_property ("is-favourite");
771 private void _update_alias ()
774 var alias_is_display_id = false;
776 debug ("Updating alias for individual '%s'", this.id);
778 /* Search for an alias from a writeable Persona, and use it as our first
779 * choice if it's non-empty, since that's where the user-set alias is
781 foreach (var p in this._persona_list)
783 if (p is AliasDetails && p.store.is_writeable == true)
785 var a = (AliasDetails) p;
787 if (a.alias != null && a.alias.strip () != "")
795 debug (" got alias '%s' from writeable personas", alias);
797 /* Since we can't find a non-empty alias from a writeable backend, try
798 * the aliases from other personas. Use a non-empty alias which isn't
799 * equal to the persona's display ID as our preference. If we can't find
800 * one of those, fall back to one which is equal to the display ID. */
803 foreach (var p in this._persona_list)
805 if (p is AliasDetails)
807 var a = (AliasDetails) p;
809 if (a.alias == null || a.alias.strip () == "")
812 if (alias == null || alias_is_display_id == true)
814 /* We prefer to not have an alias which is the same as the
815 * Persona's display-id, since having such an alias
816 * implies that it's the default. However, we prefer using
817 * such an alias to using the Persona's UID, which is our
818 * ultimate fallback (below). */
821 if (a.alias == p.display_id)
822 alias_is_display_id = true;
823 else if (alias != null)
830 debug (" got alias '%s' from non-writeable personas", alias);
834 /* We have to pick a display ID, since none of the personas have an
835 * alias available. Pick the display ID from the first persona in the
837 alias = this._persona_list.data.display_id;
838 debug ("No aliases available for individual; using display ID " +
839 "instead: %s", alias);
842 /* Only notify if the value has changed. We have to set the private member
843 * and notify manually, or we'd end up propagating the new alias back
844 * down to all our Personas, even if it's a fallback display ID or
845 * something else undesirable. */
846 if (this._alias != alias)
848 debug ("Changing alias of individual '%s' from '%s' to '%s'.",
849 this.id, this._alias, alias);
851 this.notify_property ("alias");
855 private void _update_avatar ()
859 this._persona_list.foreach ((p) =>
861 if (avatar == null && p is AvatarOwner)
863 avatar = ((AvatarOwner) p).avatar;
868 /* only notify if the value has changed */
869 if (this.avatar != avatar)
870 this.avatar = avatar;
873 private void _update_trust_level ()
875 var trust_level = TrustLevel.PERSONAS;
877 foreach (var p in this._persona_list)
879 if (p.is_user == false &&
880 p.store.trust_level == PersonaStoreTrust.NONE)
881 trust_level = TrustLevel.NONE;
884 /* Only notify if the value has changed */
885 if (this.trust_level != trust_level)
886 this.trust_level = trust_level;
889 private void _update_im_addresses ()
891 /* populate the IM addresses as the union of our Personas' addresses */
892 foreach (var persona in this.personas)
894 if (persona is IMable)
896 var imable = (IMable) persona;
897 imable.im_addresses.foreach ((k, v) =>
899 var cur_protocol = (string) k;
900 var cur_addresses = (LinkedHashSet<string>) v;
901 var im_set = this._im_addresses.lookup (cur_protocol);
905 im_set = new LinkedHashSet<string> ();
906 this._im_addresses.insert (cur_protocol, im_set);
909 im_set.add_all (cur_addresses);
913 this.notify_property ("im-addresses");
916 private void _connect_to_persona (Persona persona)
918 persona.notify["alias"].connect (this._notify_alias_cb);
919 persona.notify["avatar"].connect (this._notify_avatar_cb);
920 persona.notify["presence-message"].connect (this._notify_presence_cb);
921 persona.notify["presence-type"].connect (this._notify_presence_cb);
922 persona.notify["im-addresses"].connect (this._notify_im_addresses_cb);
923 persona.notify["is-favourite"].connect (this._notify_is_favourite_cb);
924 persona.notify["structured-name"].connect (
925 this._notify_structured_name_cb);
926 persona.notify["full-name"].connect (this._notify_full_name_cb);
927 persona.notify["nickname"].connect (this._notify_nickname_cb);
928 persona.notify["gender"].connect (this._notify_gender_cb);
929 persona.notify["urls"].connect (this._notify_urls_cb);
930 persona.notify["phone-numbers"].connect (this._notify_phone_numbers_cb);
931 persona.notify["email-addresses"].connect (
932 this._notify_email_addresses_cb);
933 persona.notify["roles"].connect (this._notify_roles_cb);
934 persona.notify["birthday"].connect (this._notify_birthday_cb);
935 persona.notify["notes"].connect (this._notify_notes_cb);
936 persona.notify["postal-addresses"].connect
937 (this._notify_postal_addresses_cb);
939 if (persona is Groupable)
941 ((Groupable) persona).group_changed.connect (
942 this._persona_group_changed_cb);
946 private void _update_structured_name ()
948 foreach (var persona in this._persona_list)
950 var name_owner = persona as NameOwner;
951 if (name_owner != null)
953 var new_value = name_owner.structured_name;
954 if (new_value != null)
956 if (new_value != this.structured_name)
957 this.structured_name = new_value;
965 private void _update_full_name ()
967 foreach (var persona in this._persona_list)
969 var name_owner = persona as NameOwner;
970 if (name_owner != null)
972 var new_value = name_owner.full_name;
973 if (new_value != null)
975 if (new_value != this.full_name)
976 this.full_name = new_value;
984 private void _update_nickname ()
986 foreach (var persona in this._persona_list)
988 var name_owner = persona as NameOwner;
989 if (name_owner != null)
991 var new_value = name_owner.nickname;
992 if (new_value != null)
994 if (new_value != this._nickname)
996 this._nickname = new_value;
997 this.notify_property ("nickname");
1006 private void _disconnect_from_persona (Persona persona)
1008 persona.notify["alias"].disconnect (this._notify_alias_cb);
1009 persona.notify["avatar"].disconnect (this._notify_avatar_cb);
1010 persona.notify["presence-message"].disconnect (
1011 this._notify_presence_cb);
1012 persona.notify["presence-type"].disconnect (this._notify_presence_cb);
1013 persona.notify["im-addresses"].disconnect (
1014 this._notify_im_addresses_cb);
1015 persona.notify["is-favourite"].disconnect (
1016 this._notify_is_favourite_cb);
1017 persona.notify["structured-name"].disconnect (
1018 this._notify_structured_name_cb);
1019 persona.notify["full-name"].disconnect (this._notify_full_name_cb);
1020 persona.notify["nickname"].disconnect (this._notify_nickname_cb);
1021 persona.notify["gender"].disconnect (this._notify_gender_cb);
1022 persona.notify["urls"].disconnect (this._notify_urls_cb);
1023 persona.notify["phone-numbers"].disconnect (
1024 this._notify_phone_numbers_cb);
1025 persona.notify["email-addresses"].disconnect (
1026 this._notify_email_addresses_cb);
1027 persona.notify["roles"].disconnect (this._notify_roles_cb);
1028 persona.notify["birthday"].disconnect (this._notify_birthday_cb);
1029 persona.notify["notes"].disconnect (this._notify_notes_cb);
1030 persona.notify["postal-addresses"].disconnect
1031 (this._notify_postal_addresses_cb);
1034 if (persona is Groupable)
1036 ((Groupable) persona).group_changed.disconnect (
1037 this._persona_group_changed_cb);
1041 private void _update_gender ()
1043 foreach (var persona in this._persona_list)
1045 var gender_owner = persona as GenderOwner;
1046 if (gender_owner != null)
1048 var new_value = gender_owner.gender;
1049 if (new_value != Gender.UNSPECIFIED)
1051 if (new_value != this.gender)
1052 this.gender = new_value;
1059 private void _update_urls ()
1061 /* Populate the URLs as the union of our Personas' URLs.
1062 * If the same URL exist multiple times we merge the parameters. */
1063 var urls_set = new HashTable<unowned string, unowned FieldDetails> (
1064 str_hash, str_equal);
1065 var urls = new GLib.List<FieldDetails> ();
1067 foreach (var persona in this._persona_list)
1069 var urlable = persona as Urlable;
1070 if (urlable != null)
1072 foreach (unowned FieldDetails ps in urlable.urls)
1074 if (ps.value == null)
1077 var existing = urls_set.lookup (ps.value);
1078 if (existing != null)
1079 existing.extend_parameters (ps.parameters);
1082 var new_ps = new FieldDetails (ps.value);
1083 new_ps.extend_parameters (ps.parameters);
1084 urls_set.insert (ps.value, new_ps);
1085 urls.prepend ((owned) new_ps);
1090 /* Set the private member directly to avoid iterating this list again */
1092 this._urls = (owned) urls;
1094 this.notify_property ("urls");
1097 private void _update_phone_numbers ()
1099 /* Populate the phone numberss as the union of our Personas' numbers
1100 * If the same number exist multiple times we merge the parameters. */
1101 /* FIXME: We should handle phone numbers better, just string comparison
1103 var phone_numbers_set =
1104 new HashTable<unowned string, unowned FieldDetails> (
1105 str_hash, str_equal);
1106 foreach (var persona in this._persona_list)
1108 var phoneable = persona as Phoneable;
1109 if (phoneable != null)
1111 foreach (unowned FieldDetails fd in phoneable.phone_numbers)
1113 if (fd.value == null)
1116 var existing = phone_numbers_set.lookup (fd.value);
1117 if (existing != null)
1118 existing.extend_parameters (fd.parameters);
1120 phone_numbers_set.insert (fd.value, fd);
1124 this._phone_numbers = phone_numbers_set.get_values ();
1126 this.notify_property ("phone-numbers");
1129 private void _update_email_addresses ()
1131 /* Populate the email addresses as the union of our Personas' addresses.
1132 * If the same URL exist multiple times we merge the parameters. */
1133 var emails_set = new HashTable<unowned string, unowned FieldDetails> (
1134 str_hash, str_equal);
1135 foreach (var persona in this._persona_list)
1137 var emailable = persona as Emailable;
1138 if (emailable != null)
1140 foreach (unowned FieldDetails fd in emailable.email_addresses)
1142 if (fd.value == null)
1145 var existing = emails_set.lookup (fd.value);
1146 if (existing != null)
1147 existing.extend_parameters (fd.parameters);
1149 emails_set.insert (fd.value, fd);
1153 this._email_addresses = emails_set.get_values ();
1155 this.notify_property ("email-addresses");
1158 private void _update_roles ()
1160 HashSet<Role> roles = new HashSet<Role>
1161 ((GLib.HashFunc) Role.hash, (GLib.EqualFunc) Role.equal);
1163 foreach (var persona in this._persona_list)
1165 var role_owner = persona as RoleOwner;
1166 if (role_owner != null)
1168 foreach (var r in role_owner.roles)
1170 if (roles.contains (r) == false)
1178 this._roles = (owned) roles;
1179 this.notify_property ("roles");
1182 private void _update_postal_addresses ()
1184 this._postal_addresses = new GLib.List<PostalAddress> ();
1185 /* FIXME: Detect duplicates somehow? */
1186 foreach (var persona in this._persona_list)
1188 var address_owner = persona as PostalAddressOwner;
1189 if (address_owner != null)
1191 foreach (unowned PostalAddress pa in address_owner.postal_addresses)
1192 this._postal_addresses.append (pa);
1195 this._postal_addresses.reverse ();
1197 this.notify_property ("postal-addresses");
1200 private void _update_birthday ()
1202 unowned DateTime bday = null;
1203 unowned string calendar_event_id = "";
1205 foreach (var persona in this._persona_list)
1207 var bday_owner = persona as BirthdayOwner;
1208 if (bday_owner != null)
1210 if (bday_owner.birthday != null)
1212 if (this.birthday == null ||
1213 bday_owner.birthday.compare (this.birthday) != 0)
1215 bday = bday_owner.birthday;
1216 calendar_event_id = bday_owner.calendar_event_id;
1223 if (this.birthday != null && bday == null)
1225 this.birthday = null;
1226 this.calendar_event_id = null;
1228 else if (bday != null)
1230 this.birthday = bday;
1231 this.calendar_event_id = calendar_event_id;
1235 private void _update_notes ()
1237 HashSet<Note> notes = new HashSet<Note>
1238 ((GLib.HashFunc) Note.hash, (GLib.EqualFunc) Note.equal);
1240 foreach (var persona in this._persona_list)
1242 var note_owner = persona as NoteOwner;
1243 if (note_owner != null)
1245 foreach (var n in note_owner.notes)
1252 this._notes = (owned) notes;
1253 this.notify_property ("notes");
1256 private void _set_personas (GLib.List<Persona>? persona_list,
1257 Individual? replacement_individual)
1259 var persona_set = new HashSet<Persona> (null, null);
1260 GLib.List<Persona> added = null;
1261 GLib.List<Persona> removed = null;
1263 /* Determine which Personas have been added */
1264 foreach (var p in persona_list)
1266 if (!this._persona_set.contains (p))
1268 /* Keep track of how many Personas are users */
1270 this._persona_user_count++;
1274 this._persona_set.add (p);
1275 this._connect_to_persona (p);
1277 /* Increment the Persona count for this PersonaStore */
1278 var store = p.store;
1279 var num_from_store = this._stores.get (store);
1280 if (num_from_store == 0)
1282 this._stores.set (store, num_from_store + 1);
1286 this._stores.set (store, 1);
1288 store.removed.connect (this._store_removed_cb);
1289 store.personas_changed.connect (
1290 this._store_personas_changed_cb);
1294 persona_set.add (p);
1297 /* Determine which Personas have been removed */
1298 foreach (var p in this._persona_list)
1300 if (!persona_set.contains (p))
1302 /* Keep track of how many Personas are users */
1304 this._persona_user_count--;
1306 removed.prepend (p);
1308 /* Decrement the Persona count for this PersonaStore */
1309 var store = p.store;
1310 var num_from_store = this._stores.get (store);
1311 if (num_from_store > 1)
1313 this._stores.set (store, num_from_store - 1);
1317 store.removed.disconnect (this._store_removed_cb);
1318 store.personas_changed.disconnect (
1319 this._store_personas_changed_cb);
1321 this._stores.unset (store);
1324 this._disconnect_from_persona (p);
1325 this._persona_set.remove (p);
1329 /* Update the Persona list. We just copy the list given to us to save
1330 * repeated insertions/removals and also to ensure we retain the ordering
1331 * of the Personas we were given. */
1332 this._persona_list = persona_list.copy ();
1334 this.personas_changed (added, removed);
1336 /* Update this.is_user */
1337 var new_is_user = (this._persona_user_count > 0) ? true : false;
1338 if (new_is_user != this.is_user)
1339 this.is_user = new_is_user;
1341 /* If all the Personas have been removed, remove the Individual */
1342 if (this._persona_set.size < 1)
1344 this.removed (replacement_individual);
1348 /* TODO: Base this upon our ID in permanent storage, once we have that. */
1349 if (this.id == null && this._persona_list.data != null)
1350 this.id = this._persona_list.data.uid;
1352 /* Update our aggregated fields and notify the changes */
1353 this._update_fields ();
1356 internal void replace (Individual replacement_individual)
1358 this._set_personas (null, replacement_individual);