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>
30 * A persona subclass which represents a single instant messaging contact from
33 * There is a one-to-one correspondence between {@link Tpf.Persona}s and
34 * {@link TelepathyGLib.Contact}s, although at any time the
35 * {@link Tpf.Persona.contact} property of a persona may be ``null`` if the
36 * contact's Telepathy connection isn't available (e.g. due to being offline).
37 * In this case, the persona's properties persist from a local cache.
39 public class Tpf.Persona : Folks.Persona,
53 private const string[] _linkable_properties = { "im-addresses" };
54 private string[] _writeable_properties = null;
56 /* Whether we've finished being constructed; this is used to prevent
57 * unnecessary trips to the Telepathy service to tell it about properties
58 * being set which are actually just being set from data it's just given us.
60 private bool _is_constructed = false;
63 * Whether the Persona is in the user's contact list.
65 * This will be true for most {@link Folks.Persona}s, but may not be true for
66 * personas where {@link Folks.Persona.is_user} is true. If it's false in
67 * this case, it means that the persona has been retrieved from the Telepathy
68 * connection, but has not been added to the user's contact list.
72 public bool is_in_contact_list { get; set; }
74 private LoadableIcon? _avatar = null;
77 * An avatar for the Persona.
79 * See {@link Folks.AvatarDetails.avatar}.
83 [CCode (notify = false)]
84 public LoadableIcon? avatar
86 get { return this._avatar; }
87 set { this.change_avatar.begin (value); } /* not writeable */
95 [CCode (notify = false)]
96 public StructuredName? structured_name
99 set { this.change_structured_name.begin (value); } /* not writeable */
102 private string _full_name = ""; /* must never be null */
109 [CCode (notify = false)]
110 public string full_name
112 get { return this._full_name; }
113 set { this.change_full_name.begin (value); }
121 public async void change_full_name (string full_name) throws PropertyError
123 var tpf_store = this.store as Tpf.PersonaStore;
125 if (full_name == this._full_name)
128 if (this._is_constructed)
132 yield tpf_store.change_user_full_name (this, full_name);
134 catch (PersonaStoreError.INVALID_ARGUMENT e1)
136 throw new PropertyError.NOT_WRITEABLE (e1.message);
138 catch (PersonaStoreError.STORE_OFFLINE e2)
140 throw new PropertyError.UNKNOWN_ERROR (e2.message);
142 catch (PersonaStoreError e3)
144 throw new PropertyError.UNKNOWN_ERROR (e3.message);
148 /* the change will be notified when we receive changes to
149 * contact.contact_info */
157 [CCode (notify = false)]
158 public string nickname
161 set { this.change_nickname.begin (value); } /* not writeable */
167 * ContactInfo has no equivalent field, so this is unsupported.
171 [CCode (notify = false)]
172 public string? calendar_event_id
174 get { return null; } /* unsupported */
175 set { this.change_calendar_event_id.begin (value); } /* not writeable */
178 private DateTime? _birthday = null;
184 [CCode (notify = false)]
185 public DateTime? birthday
187 get { return this._birthday; }
188 set { this.change_birthday.begin (value); }
196 public async void change_birthday (DateTime? birthday) throws PropertyError
198 var tpf_store = this.store as Tpf.PersonaStore;
200 if (birthday != null && this._birthday != null &&
201 birthday.equal (this._birthday))
206 if (this._is_constructed)
210 yield tpf_store.change_user_birthday (this, birthday);
212 catch (PersonaStoreError.INVALID_ARGUMENT e1)
214 throw new PropertyError.NOT_WRITEABLE (e1.message);
216 catch (PersonaStoreError.STORE_OFFLINE e2)
218 throw new PropertyError.UNKNOWN_ERROR (e2.message);
220 catch (PersonaStoreError e3)
222 throw new PropertyError.UNKNOWN_ERROR (e3.message);
226 /* the change will be notified when we receive changes to
227 * contact.contact_info */
231 * The Persona's presence type.
233 * See {@link Folks.PresenceDetails.presence_type}.
235 public Folks.PresenceType presence_type { get; set; }
238 * The Persona's presence status.
240 * See {@link Folks.PresenceDetails.presence_status}.
244 public string presence_status { get; set; }
247 * The Persona's presence message.
249 * See {@link Folks.PresenceDetails.presence_message}.
251 public string presence_message { get; set; }
254 * The names of the Persona's linkable properties.
256 * See {@link Folks.Persona.linkable_properties}.
258 public override string[] linkable_properties
260 get { return Tpf.Persona._linkable_properties; }
268 public override string[] writeable_properties
270 get { return this._writeable_properties; }
273 private string _alias = ""; /* must never be null */
276 * An alias for the Persona.
278 * See {@link Folks.AliasDetails.alias}.
280 [CCode (notify = false)]
283 get { return this._alias; }
284 set { this.change_alias.begin (value); }
292 public async void change_alias (string alias) throws PropertyError
294 if (this._alias == alias)
299 if (this._is_constructed)
301 yield ((Tpf.PersonaStore) this.store).change_alias (this, alias);
304 /* The change will be notified when we receive changes from the store. */
307 private bool _is_favourite = false;
310 * Whether this Persona is a user-defined favourite.
312 * See {@link Folks.FavouriteDetails.is_favourite}.
314 [CCode (notify = false)]
315 public bool is_favourite
317 get { return this._is_favourite; }
318 set { this.change_is_favourite.begin (value); }
326 public async void change_is_favourite (bool is_favourite) throws PropertyError
328 if (this._is_favourite == is_favourite)
333 if (this._is_constructed)
335 yield ((Tpf.PersonaStore) this.store).change_is_favourite (this,
339 /* The change will be notified when we receive changes from the store. */
342 /* Note: Only ever called by Tpf.PersonaStore. */
343 internal void _set_is_favourite (bool is_favourite)
345 if (this._is_favourite == is_favourite)
350 this._is_favourite = is_favourite;
351 this.notify_property ("is-favourite");
353 /* Mark the cache as needing to be updated. */
354 ((Tpf.PersonaStore) this.store)._set_cache_needs_update ();
357 private HashSet<EmailFieldDetails>? _email_addresses = null;
358 private Set<EmailFieldDetails>? _email_addresses_ro = null;
365 [CCode (notify = false)]
366 public Set<EmailFieldDetails> email_addresses
370 this._contact_notify_contact_info (true, false);
371 return this._email_addresses_ro;
373 set { this.change_email_addresses.begin (value); }
381 public async void change_email_addresses (
382 Set<EmailFieldDetails> email_addresses) throws PropertyError
384 yield this._change_details<EmailFieldDetails> (email_addresses,
385 this._email_addresses, "email");
388 /* NOTE: Other properties support lazy initialisation, but im-addresses
389 * doesn't as it's a linkable property, so always has to be loaded anyway. */
390 private HashMultiMap<string, ImFieldDetails> _im_addresses =
391 new HashMultiMap<string, ImFieldDetails> (null, null,
392 (Gee.HashDataFunc) AbstractFieldDetails<string>.hash_static,
393 (Gee.EqualDataFunc) AbstractFieldDetails<string>.equal_static);
396 * A mapping of IM protocol to an (unordered) set of IM addresses.
398 * See {@link Folks.ImDetails.im_addresses}.
400 [CCode (notify = false)]
401 public MultiMap<string, ImFieldDetails> im_addresses
403 get { return this._im_addresses; }
404 set { this.change_im_addresses.begin (value); }
407 private uint _im_interaction_count = 0;
410 * A counter for IM interactions (send/receive message) with the persona.
412 * See {@link Folks.InteractionDetails.im_interaction_count}
416 public uint im_interaction_count
418 get { return this._im_interaction_count; }
421 internal DateTime? _last_im_interaction_datetime = null;
424 * The latest datetime for IM interactions (send/receive message) with the
427 * See {@link Folks.InteractionDetails.last_im_interaction_datetime}
431 public DateTime? last_im_interaction_datetime
433 get { return this._last_im_interaction_datetime; }
436 private uint _call_interaction_count = 0;
439 * A counter for call interactions (only successful calls) with the persona.
441 * See {@link Folks.InteractionDetails.call_interaction_count}
445 public uint call_interaction_count
447 get { return this._call_interaction_count; }
450 internal DateTime? _last_call_interaction_datetime = null;
453 * The latest datetime for call interactions (only successful calls) with the
456 * See {@link Folks.InteractionDetails.last_call_interaction_datetime}
460 public DateTime? last_call_interaction_datetime
462 get { return this._last_call_interaction_datetime; }
465 private HashSet<string> _groups = new HashSet<string> ();
466 private Set<string> _groups_ro;
469 * A set group IDs for the groups the contact is a member of.
471 * See {@link Folks.GroupDetails.groups}.
473 [CCode (notify = false)]
474 public Set<string> groups
476 get { return this._groups_ro; }
477 set { this.change_groups.begin (value); }
481 * Add or remove the Persona from the specified group.
483 * See {@link Folks.GroupDetails.change_group}.
485 * @throws Folks.PropertyError.UNKNOWN_ERROR if changing group membership
488 public async void change_group (string group, bool is_member)
491 /* Ensure we have a strong ref to the contact for the duration of the
493 var contact = (Contact?) this._contact.get ();
497 /* The Tpf.Persona is being served out of the cache. */
498 throw new PropertyError.UNAVAILABLE (
499 _("Failed to change group membership: %s"),
500 /* Translators: "account" refers to an instant messaging
502 _("Account is offline."));
507 if (is_member && !this._groups.contains (group))
509 yield contact.add_to_group_async (group);
511 else if (!is_member && this._groups.contains (group))
513 yield contact.remove_from_group_async (group);
518 /* Translators: the parameter is an error message. */
519 throw new PropertyError.UNKNOWN_ERROR (
520 _("Failed to change group membership: %s"), e.message);
523 /* The change will be notified when we receive changes from the store. */
526 /* Note: Only ever called as a result of signals from Telepathy. */
527 private void _contact_groups_changed (string[] added, string[] removed)
531 foreach (var group in added)
533 if (this._groups.add (group) == true)
536 this.group_changed (group, true);
540 foreach (var group in removed)
542 if (this._groups.remove (group) == true)
545 this.group_changed (group, false);
549 /* Notify if anything changed. */
552 this.notify_property ("groups");
554 /* Mark the cache as needing to be updated. */
555 ((Tpf.PersonaStore) this.store)._set_cache_needs_update ();
564 public async void change_groups (Set<string> groups) throws PropertyError
566 var contact = (Contact?) this._contact.get ();
570 /* The Tpf.Persona is being served out of the cache. */
571 throw new PropertyError.UNAVAILABLE (
572 _("Failed to change group membership: %s"),
573 /* Translators: "account" refers to an instant messaging
575 _("Account is offline."));
580 yield contact.set_contact_groups_async (groups.to_array ());
584 /* Translators: the parameter is an error message. */
585 throw new PropertyError.UNKNOWN_ERROR (
586 _("Failed to change group membership: %s"), e.message);
589 /* The change will be notified when we receive changes from the store. */
592 /* This has to be weak since, in general, we can't force any TpContacts to
593 * remain alive if we want to solve bgo#665376.
594 * As per bgo#680335, we have to use a WeakRef rather than a
595 * ‘weak Contact?’ to avoid races when clearing the pointer. We still have
596 * to use a weak ref. notifier as well, though, in order to be able to emit
597 * a property change notification for ::contact.
599 * FIXME: Once bgo#554344 is fixed, _contact could be changed back to
600 * being a 'weak Contact?', assuming Vala implements weak references using
602 private GLib.WeakRef _contact = GLib.WeakRef (null);
604 private void _contact_weak_notify_cb (Object obj)
606 debug ("TpContact %p destroyed; setting ._contact = null in Persona %p",
608 /* _contact is cleared automatically as it's a WeakRef. */
609 this.notify_property ("contact");
613 * The Telepathy contact represented by this persona.
615 * Note that this may be ``null`` if the {@link PersonaStore} providing this
616 * {@link Persona} isn't currently available (e.g. due to not being connected
617 * to the network). In this case, most other properties of the {@link Persona}
618 * are being retrieved from a cache and may not be current (though there's no
621 public Contact? contact
625 /* FIXME: This property should be changed to transfer its reference
626 * when the API is next broken. This is necessary because the
627 * TpfPersona doesn't hold a strong ref to the TpContact, so any
628 * pointer which is returned might be invalidated before reaching the
629 * caller. Probably not a problem in practice since folks won't be
630 * run multi-threaded. */
631 Contact? contact = (Contact?) this._contact.get ();
637 /* FIXME: I'm so very, very sorry. This is to cause Vala to forget
638 * we have a strong ref on 'contact' and not transfer it out. */
639 return (Contact) ((void*) contact);
646 value.weak_ref (this._contact_weak_notify_cb);
649 this._contact.set (value);
653 private HashSet<PhoneFieldDetails>? _phone_numbers = null;
654 private Set<PhoneFieldDetails>? _phone_numbers_ro = null;
661 [CCode (notify = false)]
662 public Set<PhoneFieldDetails> phone_numbers
666 this._contact_notify_contact_info (true, false);
667 return this._phone_numbers_ro;
669 set { this.change_phone_numbers.begin (value); }
677 public async void change_phone_numbers (
678 Set<PhoneFieldDetails> phone_numbers) throws PropertyError
680 yield this._change_details<PhoneFieldDetails> (phone_numbers,
681 this._phone_numbers, "tel");
684 private HashSet<UrlFieldDetails>? _urls = null;
685 private Set<UrlFieldDetails>? _urls_ro = null;
692 [CCode (notify = false)]
693 public Set<UrlFieldDetails> urls
697 this._contact_notify_contact_info (true, false);
698 return this._urls_ro;
700 set { this.change_urls.begin (value); }
708 public async void change_urls (Set<UrlFieldDetails> urls) throws PropertyError
710 yield this._change_details<UrlFieldDetails> (urls,
714 private async void _change_details<T> (
715 Set<AbstractFieldDetails<string>> details,
716 Set<AbstractFieldDetails<string>>? member_set,
720 var tpf_store = this.store as Tpf.PersonaStore;
722 if (member_set != null &&
723 Folks.Internal.equal_sets<T> (details, member_set))
728 if (this._is_constructed)
732 yield tpf_store._change_user_details (this, details, field_name);
734 catch (PersonaStoreError.INVALID_ARGUMENT e1)
736 throw new PropertyError.NOT_WRITEABLE (e1.message);
738 catch (PersonaStoreError.STORE_OFFLINE e2)
740 throw new PropertyError.UNKNOWN_ERROR (e2.message);
742 catch (PersonaStoreError e3)
744 throw new PropertyError.UNKNOWN_ERROR (e3.message);
748 /* the change will be notified when we receive changes to
749 * contact.contact_info */
753 * Create a new persona.
755 * Create a new persona for the {@link PersonaStore} ``store``, representing
756 * the Telepathy contact given by ``contact``.
758 * @param contact the Telepathy contact being represented by the persona
759 * @param store the persona store to place the persona in
761 public Persona (Contact contact, PersonaStore store)
763 unowned string id = contact.get_identifier ();
764 var connection = contact.connection;
765 var account = connection.get_account ();
766 var uid = Folks.Persona.build_uid (store.type_id, store.id, id);
768 Object (contact: contact,
770 /* FIXME: This IID format should be moved out to the ImDetails
771 * interface along with the code in
772 * Kf.Persona.linkable_property_to_links(), but that depends on
773 * bgo#624842 being fixed. */
774 iid: account.get_protocol () + ":" + id,
777 is_user: contact.handle == connection.self_handle);
779 debug ("Created new Tpf.Persona '%s' for service-specific UID '%s': %p",
785 this._groups_ro = this._groups.read_only_view;
787 /* Contact can be null if we've been created from the cache. All the code
788 * below this point is for non-cached personas. */
789 var contact = (Contact?) this._contact.get ();
797 this._alias = contact.get_alias ();
799 contact.notify["alias"].connect ((s, p) =>
801 var c = (Contact?) this._contact.get ();
802 assert (c != null); /* should never be called while cached */
804 /* Tp guarantees that aliases are always non-null. */
805 assert (c.alias != null);
807 if (this._alias != c.alias)
809 this._alias = c.alias;
810 this.notify_property ("alias");
812 /* Mark the cache as needing to be updated. */
813 ((Tpf.PersonaStore) this.store)._set_cache_needs_update ();
817 /* Set our single IM address */
818 var connection = contact.connection;
819 var account = connection.get_account ();
823 var im_addr = ImDetails.normalise_im_address (this.display_id,
824 account.get_protocol ());
825 var im_fd = new ImFieldDetails (im_addr);
826 this._im_addresses.set (account.get_protocol (), im_fd);
828 catch (ImDetailsError e)
830 /* This should never happen…but if it does, warn of it and continue */
834 contact.notify["avatar-file"].connect ((s, p) =>
836 this._contact_notify_avatar ();
838 this._contact_notify_avatar ();
840 contact.notify["presence-message"].connect ((s, p) =>
842 this._contact_notify_presence_message ();
844 contact.notify["presence-type"].connect ((s, p) =>
846 this._contact_notify_presence_type ();
848 contact.notify["presence-status"].connect ((s, p) =>
850 this._contact_notify_presence_status ();
852 this._contact_notify_presence_message ();
853 this._contact_notify_presence_type ();
854 this._contact_notify_presence_status ();
856 contact.notify["contact-info"].connect ((s, p) =>
858 this._contact_notify_contact_info (false);
860 this._contact_notify_contact_info (false);
862 contact.contact_groups_changed.connect ((added, removed) =>
864 this._contact_groups_changed (added, removed);
866 this._contact_groups_changed (contact.get_contact_groups (), {});
868 var tpf_store = this.store as Tpf.PersonaStore;
872 tpf_store.notify["supported-fields"].connect ((s, p) =>
874 this._update_writeable_properties ();
878 tpf_store.notify["always-writeable-properties"].connect ((s, p) =>
880 this._update_writeable_properties ();
883 this._update_writeable_properties ();
886 /* Called after all construction-time properties have been set. */
887 public override void constructed ()
889 this._is_constructed = true;
892 private void _update_writeable_properties ()
894 var tpf_store = this.store as Tpf.PersonaStore;
895 this._writeable_properties = this.store.always_writeable_properties;
899 if ("bday" in tpf_store.supported_fields)
900 this._writeable_properties += "birthday";
901 if ("email" in tpf_store.supported_fields)
902 this._writeable_properties += "email-addresses";
903 if ("fn" in tpf_store.supported_fields)
904 this._writeable_properties += "full-name";
905 if ("tel" in tpf_store.supported_fields)
906 this._writeable_properties += "phone-numbers";
907 if ("url" in tpf_store.supported_fields)
908 this._writeable_properties += "urls";
912 private void _contact_notify_contact_info (bool create_if_not_exists, bool emit_notification = true)
915 (this._email_addresses == null) &&
916 (this._phone_numbers == null) &&
919 (this._email_addresses != null) &&
920 (this._phone_numbers != null) &&
924 /* See the comments in Folks.Individual about the lazy instantiation
925 * strategy for URIs, etc.
927 * It's necessary to notify for all three properties here, as this
928 * function is called identically for all of them. */
929 if (this._urls == null && create_if_not_exists == false)
931 if (emit_notification)
933 this.notify_property ("email-addresses");
934 this.notify_property ("phone-numbers");
935 this.notify_property ("urls");
939 else if (this._urls == null)
941 this._urls = new HashSet<UrlFieldDetails> (
942 AbstractFieldDetails<string>.hash_static,
943 AbstractFieldDetails<string>.equal_static);
944 this._urls_ro = this._urls.read_only_view;
946 this._email_addresses = new HashSet<EmailFieldDetails> (
947 AbstractFieldDetails<string>.hash_static,
948 AbstractFieldDetails<string>.equal_static);
949 this._email_addresses_ro = this._email_addresses.read_only_view;
951 this._phone_numbers = new HashSet<PhoneFieldDetails> (
952 AbstractFieldDetails<string>.hash_static,
953 AbstractFieldDetails<string>.equal_static);
954 this._phone_numbers_ro = this._phone_numbers.read_only_view;
957 var contact = (Contact?) this._contact.get ();
960 /* If operating from the cache, bail out early. */
965 var new_birthday_str = "";
966 var new_full_name = "";
967 var new_email_addresses = new HashSet<EmailFieldDetails> (
968 AbstractFieldDetails<string>.hash_static,
969 AbstractFieldDetails<string>.equal_static);
970 var new_phone_numbers = new HashSet<PhoneFieldDetails> (
971 AbstractFieldDetails<string>.hash_static,
972 AbstractFieldDetails<string>.equal_static);
973 var new_urls = new HashSet<UrlFieldDetails> (
974 AbstractFieldDetails<string>.hash_static,
975 AbstractFieldDetails<string>.equal_static);
977 var contact_info = contact.get_contact_info ();
978 foreach (var info in contact_info)
980 if (info.field_name == "") {}
981 else if (info.field_name == "bday")
983 new_birthday_str = info.field_value[0] ?? "";
985 else if (info.field_name == "email")
987 foreach (var email_addr in info.field_value)
989 if (email_addr != "")
991 var parameters = this._afd_params_from_strv (info.parameters);
992 var email_fd = new EmailFieldDetails (email_addr, parameters);
993 new_email_addresses.add (email_fd);
997 else if (info.field_name == "fn")
999 new_full_name = info.field_value[0];
1000 if (new_full_name == null)
1003 else if (info.field_name == "tel")
1005 foreach (var phone_num in info.field_value)
1007 if (phone_num != "")
1009 var parameters = this._afd_params_from_strv (info.parameters);
1010 var phone_fd = new PhoneFieldDetails (phone_num, parameters);
1011 new_phone_numbers.add (phone_fd);
1015 else if (info.field_name == "url")
1017 foreach (var url in info.field_value)
1021 var parameters = this._afd_params_from_strv (info.parameters);
1022 var url_fd = new UrlFieldDetails (url, parameters);
1023 new_urls.add (url_fd);
1029 if (new_birthday_str != "")
1031 var timeval = TimeVal ();
1032 if (timeval.from_iso8601 (new_birthday_str))
1034 var d = new DateTime.from_timeval_utc (timeval);
1035 if (this._birthday == null ||
1036 (this._birthday != null &&
1037 !this._birthday.equal (d.to_utc ())))
1039 this._birthday = d.to_utc ();
1040 if (emit_notification)
1042 this.notify_property ("birthday");
1049 debug ("Failed to parse new birthday string '%s'",
1055 if (this._birthday != null)
1057 this._birthday = null;
1058 if (emit_notification)
1060 this.notify_property ("birthday");
1066 if (!Folks.Internal.equal_sets<EmailFieldDetails> (new_email_addresses,
1067 this._email_addresses))
1069 this._email_addresses = new_email_addresses;
1070 this._email_addresses_ro = new_email_addresses.read_only_view;
1071 if (emit_notification)
1073 this.notify_property ("email-addresses");
1078 if (new_full_name != this._full_name)
1080 this._full_name = new_full_name;
1081 this.notify_property ("full-name");
1085 if (!Folks.Internal.equal_sets<PhoneFieldDetails> (new_phone_numbers,
1086 this._phone_numbers))
1088 this._phone_numbers = new_phone_numbers;
1089 this._phone_numbers_ro = new_phone_numbers.read_only_view;
1090 if (emit_notification)
1092 this.notify_property ("phone-numbers");
1097 if (!Folks.Internal.equal_sets<UrlFieldDetails> (new_urls, this._urls))
1099 this._urls = new_urls;
1100 this._urls_ro = new_urls.read_only_view;
1101 this.notify_property ("urls");
1105 if (changed == true)
1107 /* Mark the cache as needing to be updated. */
1108 ((Tpf.PersonaStore) this.store)._set_cache_needs_update ();
1112 private MultiMap<string, string> _afd_params_from_strv (string[] parameters)
1114 var retval = new HashMultiMap<string, string> ();
1116 foreach (var entry in parameters)
1118 var tokens = entry.split ("=", 2);
1119 if (tokens.length == 2)
1121 retval.set (tokens[0], tokens[1]);
1125 warning ("Failed to parse vCard parameter from string '%s'",
1134 * Create a new persona for the {@link PersonaStore} ``store``, representing
1135 * a cached contact for which we currently have no Telepathy contact.
1137 * @param store The persona store to place the persona in.
1138 * @param uid The cached UID of the persona.
1139 * @param iid The cached IID of the persona.
1140 * @param im_address The cached IM address of the persona (excluding
1142 * @param protocol The cached protocol of the persona.
1143 * @param groups The cached set of groups the persona is in.
1144 * @param is_favourite Whether the persona is a favourite.
1145 * @param alias The cached alias for the persona.
1146 * @param is_in_contact_list Whether the persona is in the user's contact
1148 * @param is_user Whether the persona is the user.
1149 * @param avatar The icon for the persona's cached avatar, or ``null`` if they
1151 * @param birthday The date/time of birth of the persona, or ``null`` if it's
1153 * @param full_name The persona's full name, or the empty string if it's
1155 * @param email_addresses A set of the persona's e-mail addresses, which may
1156 * be empty (but may not be ``null``).
1157 * @param phone_numbers A set of the persona's phone numbers, which may be
1158 * empty (but may not be ``null``).
1159 * @param urls A set of the persona's URLs, which may be empty (but may not be
1161 * @return A new {@link Tpf.Persona} representing the cached persona.
1165 internal Persona.from_cache (PersonaStore store, string uid, string iid,
1166 string im_address, string protocol, HashSet<string> groups,
1167 bool is_favourite, string alias, bool is_in_contact_list, bool is_user,
1168 LoadableIcon? avatar, DateTime? birthday, string full_name,
1169 HashSet<EmailFieldDetails> email_addresses,
1170 HashSet<PhoneFieldDetails> phone_numbers, HashSet<UrlFieldDetails> urls)
1172 Object (contact: null,
1173 display_id: im_address,
1179 debug ("Created new Tpf.Persona '%s' from cache: %p", uid, this);
1182 var im_fd = new ImFieldDetails (im_address);
1183 this._im_addresses.set (protocol, im_fd);
1186 this._groups = groups;
1187 this._groups_ro = this._groups.read_only_view;
1190 this._email_addresses = email_addresses;
1191 this._email_addresses_ro = this._email_addresses.read_only_view;
1194 this._phone_numbers = phone_numbers;
1195 this._phone_numbers_ro = this._phone_numbers.read_only_view;
1199 this._urls_ro = this._urls.read_only_view;
1204 /* Deal with badly-behaved callers */
1208 if (full_name == null)
1210 /* Deal with badly-behaved callers */
1214 this._alias = alias;
1215 this._is_favourite = is_favourite;
1216 this.is_in_contact_list = is_in_contact_list;
1217 this._birthday = birthday;
1218 this._full_name = full_name;
1221 this._avatar = avatar;
1223 (avatar != null) ? ((FileIcon) avatar).get_file () : null;
1224 ((Tpf.PersonaStore) store)._update_avatar_cache (iid, avatar_file);
1226 // Make the persona appear offline
1227 this.presence_type = PresenceType.OFFLINE;
1228 this.presence_message = "";
1229 this.presence_status = "offline";
1231 this._writeable_properties = {};
1236 debug ("Destroying Tpf.Persona '%s': %p", this.uid, this);
1238 var contact = (Contact?) this._contact.get ();
1239 if (contact != null)
1241 contact.weak_unref (this._contact_weak_notify_cb);
1245 private void _contact_notify_presence_message ()
1247 var contact = (Contact?) this._contact.get ();
1248 assert (contact != null); /* should never be called while cached */
1249 this.presence_message = contact.get_presence_message ();
1252 private void _contact_notify_presence_type ()
1254 var contact = (Contact?) this._contact.get ();
1255 assert (contact != null); /* should never be called while cached */
1256 this.presence_type = Tpf.Persona._folks_presence_type_from_tp (
1257 contact.get_presence_type ());
1260 private void _contact_notify_presence_status ()
1262 var contact = (Contact?) this._contact.get ();
1263 assert (contact != null); /* should never be called while cached */
1264 this.presence_status = contact.get_presence_status ();
1267 private static PresenceType _folks_presence_type_from_tp (
1268 TelepathyGLib.ConnectionPresenceType type)
1272 case TelepathyGLib.ConnectionPresenceType.AVAILABLE:
1273 return PresenceType.AVAILABLE;
1274 case TelepathyGLib.ConnectionPresenceType.AWAY:
1275 return PresenceType.AWAY;
1276 case TelepathyGLib.ConnectionPresenceType.BUSY:
1277 return PresenceType.BUSY;
1278 case TelepathyGLib.ConnectionPresenceType.ERROR:
1279 return PresenceType.ERROR;
1280 case TelepathyGLib.ConnectionPresenceType.EXTENDED_AWAY:
1281 return PresenceType.EXTENDED_AWAY;
1282 case TelepathyGLib.ConnectionPresenceType.HIDDEN:
1283 return PresenceType.HIDDEN;
1284 case TelepathyGLib.ConnectionPresenceType.OFFLINE:
1285 return PresenceType.OFFLINE;
1286 case TelepathyGLib.ConnectionPresenceType.UNKNOWN:
1287 return PresenceType.UNKNOWN;
1288 case TelepathyGLib.ConnectionPresenceType.UNSET:
1289 return PresenceType.UNSET;
1291 return PresenceType.UNKNOWN;
1295 private void _contact_notify_avatar ()
1297 var contact = (Contact?) this._contact.get ();
1298 assert (contact != null); /* should never be called while cached */
1300 var file = contact.avatar_file;
1301 var token = contact.avatar_token;
1303 var from_cache = false;
1305 /* Handle all the different cases of avatars. */
1308 /* Definitely know there's no avatar. */
1312 else if (token != null && file != null)
1314 /* Definitely know there's some avatar, so leave the file alone. */
1319 /* Not sure about the avatar; fall back to any cached avatar. */
1320 file = ((Tpf.PersonaStore) this.store)._query_avatar_cache (this.iid);
1326 icon = new FileIcon (file);
1329 if ((this._avatar == null) != (icon == null) || !this._avatar.equal (icon))
1331 this._avatar = (LoadableIcon) icon;
1332 this.notify_property ("avatar");
1334 if (from_cache == false)
1336 /* Mark the persona cache as needing to be updated. */
1337 ((Tpf.PersonaStore) this.store)._set_cache_needs_update ();
1339 /* Update the avatar cache. */
1340 ((Tpf.PersonaStore) this.store)._update_avatar_cache (this.iid,
1347 * Look up a {@link Tpf.Persona} by its {@link TelepathyGLib.Contact}.
1349 * If the {@link TelepathyGLib.Account} for the contact's
1350 * {@link TelepathyGLib.Connection} is ``null``, or if a
1351 * {@link Tpf.PersonaStore} can't be found for that account, ``null`` will be
1352 * returned. Otherwise, if a {@link Tpf.Persona} already exists for the given
1353 * contact, that will be returned; if one doesn't exist a new one will be
1354 * created and returned. In this case, the {@link Tpf.Persona} will be added
1355 * to the {@link PersonaStore} associated with the account, and will be
1356 * removed when ``contact`` is destroyed.
1358 * @param contact the Telepathy contact of the persona
1359 * @return the persona associated with the contact, or ``null``
1362 public static Persona? dup_for_contact (Contact contact)
1364 var account = contact.connection.get_account ();
1366 debug ("Tpf.Persona.dup_for_contact (%p): got account %p", contact,
1369 /* Account could be null; see the docs for tp_connection_get_account(). */
1370 if (account == null)
1375 var store = PersonaStore.dup_for_account (account);
1376 return store._ensure_persona_for_contact (contact);
1380 internal void _increase_counter (string id, string interaction_type, Event event)
1382 var timestamp = (uint) (event.get_timestamp () / 1000);
1383 var converted_datetime = new DateTime.from_unix_utc (timestamp);
1384 var interpretation = event.get_interpretation ();
1386 /* Only count send/receive for IM interactions */
1387 if (interaction_type == Zeitgeist.NMO_IMMESSAGE &&
1388 (interpretation == Zeitgeist.ZG_SEND_EVENT ||
1389 interpretation == Zeitgeist.ZG_RECEIVE_EVENT))
1391 this._im_interaction_count++;
1392 this.notify_property ("im-interaction-count");
1393 if (this._last_im_interaction_datetime == null ||
1394 this._last_im_interaction_datetime.compare (converted_datetime) == -1)
1396 this._last_im_interaction_datetime = converted_datetime;
1397 this.notify_property ("last-im-interaction-datetime");
1399 debug ("Persona %s IM interaction details changed:\n - count: %u \n - timestamp: %lld\n",
1400 id, this._im_interaction_count, this._last_im_interaction_datetime.format ("%H %M %S - %d %m %y"));
1402 /* Only count successful call for call interactions */
1403 else if (interaction_type == Zeitgeist.NFO_AUDIO &&
1404 interpretation == Zeitgeist.ZG_LEAVE_EVENT)
1406 this._call_interaction_count++;
1407 this.notify_property ("call-interaction-count");
1408 if (this._last_call_interaction_datetime == null ||
1409 this._last_call_interaction_datetime.compare (converted_datetime) == -1)
1411 this._last_call_interaction_datetime = converted_datetime;
1412 this.notify_property ("last-call-interaction-datetime");
1414 debug ("Persona %s Call interaction details changed:\n - count: %u \n - timestamp: %lld\n",
1415 id, this._call_interaction_count, this._last_call_interaction_datetime.format ("%H %M %S - %d %m %y"));
1419 internal void _reset_interaction ()
1421 this._call_interaction_count = 0;
1422 this._im_interaction_count = 0;
1423 this._last_call_interaction_datetime = null;
1424 this._last_im_interaction_datetime = null;