tpf-persona.vala 42.7 KB
Newer Older
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
/*
 * Copyright (C) 2010 Collabora Ltd.
 *
 * This library is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Lesser General Public License as published by
 * the Free Software Foundation, either version 2.1 of the License, or
 * (at your option) any later version.
 *
 * This library is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public License
 * along with this library.  If not, see <http://www.gnu.org/licenses/>.
 *
 * Authors:
 *       Travis Reitter <travis.reitter@collabora.co.uk>
 */

21
using Gee;
22
using GLib;
23
using TelepathyGLib;
24
using Folks;
25
#if HAVE_ZEITGEIST
26
using Zeitgeist;
27
#endif
28

29 30 31
/**
 * A persona subclass which represents a single instant messaging contact from
 * Telepathy.
32 33 34
 *
 * There is a one-to-one correspondence between {@link Tpf.Persona}s and
 * {@link TelepathyGLib.Contact}s, although at any time the
35
 * {@link Tpf.Persona.contact} property of a persona may be ``null`` if the
36 37
 * contact's Telepathy connection isn't available (e.g. due to being offline).
 * In this case, the persona's properties persist from a local cache.
38
 */
39
public class Tpf.Persona : Folks.Persona,
40
    AliasDetails,
41
    AvatarDetails,
42
    BirthdayDetails,
43
    EmailDetails,
44
    FavouriteDetails,
45
    GroupDetails,
46
    InteractionDetails,
Travis Reitter's avatar
Travis Reitter committed
47
    ImDetails,
48
    NameDetails,
49
    PhoneDetails,
50 51
    PresenceDetails,
    UrlDetails
52
{
53
  private const string[] _linkable_properties = { "im-addresses" };
54
  private string[] _writeable_properties = null;
55

56 57 58 59
  /* Whether we've finished being constructed; this is used to prevent
   * unnecessary trips to the Telepathy service to tell it about properties
   * being set which are actually just being set from data it's just given us.
   */
60
  private bool _is_constructed = false;
61

62 63 64 65 66 67 68 69
  /**
   * Whether the Persona is in the user's contact list.
   *
   * This will be true for most {@link Folks.Persona}s, but may not be true for
   * personas where {@link Folks.Persona.is_user} is true. If it's false in
   * this case, it means that the persona has been retrieved from the Telepathy
   * connection, but has not been added to the user's contact list.
   *
70
   * @since 0.3.5
71 72 73
   */
  public bool is_in_contact_list { get; set; }

74 75
  private LoadableIcon? _avatar = null;

76
  /**
77 78
   * An avatar for the Persona.
   *
79
   * See {@link Folks.AvatarDetails.avatar}.
80
   *
Travis Reitter's avatar
Travis Reitter committed
81
   * @since 0.6.0
82
   */
83 84 85 86 87 88
  [CCode (notify = false)]
  public LoadableIcon? avatar
    {
      get { return this._avatar; }
      set { this.change_avatar.begin (value); } /* not writeable */
    }
89

90 91 92
  /**
   * {@inheritDoc}
   *
Travis Reitter's avatar
Travis Reitter committed
93
   * @since 0.6.4
94 95 96 97 98 99 100 101
   */
  [CCode (notify = false)]
  public StructuredName? structured_name
    {
      get { return null; }
      set { this.change_structured_name.begin (value); } /* not writeable */
    }

102 103
  private string _full_name = ""; /* must never be null */

104 105 106
  /**
   * {@inheritDoc}
   *
Travis Reitter's avatar
Travis Reitter committed
107
   * @since 0.6.4
108 109 110 111 112 113 114 115 116 117 118
   */
  [CCode (notify = false)]
  public string full_name
    {
      get { return this._full_name; }
      set { this.change_full_name.begin (value); }
    }

  /**
   * {@inheritDoc}
   *
Travis Reitter's avatar
Travis Reitter committed
119
   * @since 0.6.4
120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154
   */
  public async void change_full_name (string full_name) throws PropertyError
    {
      var tpf_store = this.store as Tpf.PersonaStore;

      if (full_name == this._full_name)
        return;

      if (this._is_constructed)
        {
          try
            {
              yield tpf_store.change_user_full_name (this, full_name);
            }
          catch (PersonaStoreError.INVALID_ARGUMENT e1)
            {
              throw new PropertyError.NOT_WRITEABLE (e1.message);
            }
          catch (PersonaStoreError.STORE_OFFLINE e2)
            {
              throw new PropertyError.UNKNOWN_ERROR (e2.message);
            }
          catch (PersonaStoreError e3)
            {
              throw new PropertyError.UNKNOWN_ERROR (e3.message);
            }
        }

      /* the change will be notified when we receive changes to
       * contact.contact_info */
    }

  /**
   * {@inheritDoc}
   *
Travis Reitter's avatar
Travis Reitter committed
155
   * @since 0.6.4
156 157 158 159 160 161 162 163
   */
  [CCode (notify = false)]
  public string nickname
    {
      get { return ""; }
      set { this.change_nickname.begin (value); } /* not writeable */
    }

164 165 166 167 168
  /**
   * {@inheritDoc}
   *
   * ContactInfo has no equivalent field, so this is unsupported.
   *
Travis Reitter's avatar
Travis Reitter committed
169
   * @since 0.6.4
170 171 172 173 174 175 176 177 178 179 180 181
   */
  [CCode (notify = false)]
  public string? calendar_event_id
    {
      get { return null; } /* unsupported */
      set { this.change_calendar_event_id.begin (value); } /* not writeable */
    }

  private DateTime? _birthday = null;
  /**
   * {@inheritDoc}
   *
Travis Reitter's avatar
Travis Reitter committed
182
   * @since 0.6.4
183 184 185 186 187 188 189 190 191 192 193
   */
  [CCode (notify = false)]
  public DateTime? birthday
    {
      get { return this._birthday; }
      set { this.change_birthday.begin (value); }
    }

  /**
   * {@inheritDoc}
   *
Travis Reitter's avatar
Travis Reitter committed
194
   * @since 0.6.4
195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229
   */
  public async void change_birthday (DateTime? birthday) throws PropertyError
    {
      var tpf_store = this.store as Tpf.PersonaStore;

      if (birthday != null && this._birthday != null &&
          birthday.equal (this._birthday))
        {
          return;
        }

      if (this._is_constructed)
        {
          try
            {
              yield tpf_store.change_user_birthday (this, birthday);
            }
          catch (PersonaStoreError.INVALID_ARGUMENT e1)
            {
              throw new PropertyError.NOT_WRITEABLE (e1.message);
            }
          catch (PersonaStoreError.STORE_OFFLINE e2)
            {
              throw new PropertyError.UNKNOWN_ERROR (e2.message);
            }
          catch (PersonaStoreError e3)
            {
              throw new PropertyError.UNKNOWN_ERROR (e3.message);
            }
        }

      /* the change will be notified when we receive changes to
       * contact.contact_info */
    }

230
  /**
231 232
   * The Persona's presence type.
   *
233
   * See {@link Folks.PresenceDetails.presence_type}.
234
   */
235
  public Folks.PresenceType presence_type { get; set; }
236

237 238 239 240 241
  /**
   * The Persona's presence status.
   *
   * See {@link Folks.PresenceDetails.presence_status}.
   *
Travis Reitter's avatar
Travis Reitter committed
242
   * @since 0.6.0
243
   */
244
  public string presence_status { get; set; }
245

246
  /**
247 248
   * The Persona's presence message.
   *
249
   * See {@link Folks.PresenceDetails.presence_message}.
250
   */
251
  public string presence_message { get; set; }
Travis Reitter's avatar
Travis Reitter committed
252

253 254 255 256 257 258 259
  /**
   * The names of the Persona's linkable properties.
   *
   * See {@link Folks.Persona.linkable_properties}.
   */
  public override string[] linkable_properties
    {
260
      get { return Tpf.Persona._linkable_properties; }
261 262
    }

263 264 265
  /**
   * {@inheritDoc}
   *
Travis Reitter's avatar
Travis Reitter committed
266
   * @since 0.6.0
267 268 269
   */
  public override string[] writeable_properties
    {
270
      get { return this._writeable_properties; }
271 272
    }

273 274
  private string _alias = ""; /* must never be null */

275
  /**
276 277
   * An alias for the Persona.
   *
278
   * See {@link Folks.AliasDetails.alias}.
279
   */
280
  [CCode (notify = false)]
281 282 283
  public string alias
    {
      get { return this._alias; }
284 285
      set { this.change_alias.begin (value); }
    }
286

287 288 289
  /**
   * {@inheritDoc}
   *
Raul Gutierrez Segales's avatar
Raul Gutierrez Segales committed
290
   * @since 0.6.2
291 292 293 294
   */
  public async void change_alias (string alias) throws PropertyError
    {
      if (this._alias == alias)
295
        {
296 297
          return;
        }
298

299 300 301
      if (this._is_constructed)
        {
          yield ((Tpf.PersonaStore) this.store).change_alias (this, alias);
302
        }
303

304
      /* The change will be notified when we receive changes from the store. */
305 306
    }

307 308
  private bool _is_favourite = false;

309
  /**
310 311
   * Whether this Persona is a user-defined favourite.
   *
312
   * See {@link Folks.FavouriteDetails.is_favourite}.
313
   */
314
  [CCode (notify = false)]
315
  public bool is_favourite
Philip Withnall's avatar
Philip Withnall committed
316 317
    {
      get { return this._is_favourite; }
318 319
      set { this.change_is_favourite.begin (value); }
    }
Philip Withnall's avatar
Philip Withnall committed
320

321 322 323
  /**
   * {@inheritDoc}
   *
Raul Gutierrez Segales's avatar
Raul Gutierrez Segales committed
324
   * @since 0.6.2
325 326 327 328
   */
  public async void change_is_favourite (bool is_favourite) throws PropertyError
    {
      if (this._is_favourite == is_favourite)
Philip Withnall's avatar
Philip Withnall committed
329
        {
330 331
          return;
        }
Philip Withnall's avatar
Philip Withnall committed
332

333 334 335 336
      if (this._is_constructed)
        {
          yield ((Tpf.PersonaStore) this.store).change_is_favourite (this,
              is_favourite);
Philip Withnall's avatar
Philip Withnall committed
337
        }
338

339
      /* The change will be notified when we receive changes from the store. */
Philip Withnall's avatar
Philip Withnall committed
340 341
    }

342
  /* Note: Only ever called by Tpf.PersonaStore. */
343 344
  internal void _set_is_favourite (bool is_favourite)
    {
345 346 347 348 349
      if (this._is_favourite == is_favourite)
        {
          return;
        }

350 351
      this._is_favourite = is_favourite;
      this.notify_property ("is-favourite");
352 353 354

      /* Mark the cache as needing to be updated. */
      ((Tpf.PersonaStore) this.store)._set_cache_needs_update ();
355 356
    }

357 358
  private HashSet<EmailFieldDetails>? _email_addresses = null;
  private Set<EmailFieldDetails>? _email_addresses_ro = null;
359 360 361 362

  /**
   * {@inheritDoc}
   *
Travis Reitter's avatar
Travis Reitter committed
363
   * @since 0.6.4
364 365 366 367
   */
  [CCode (notify = false)]
  public Set<EmailFieldDetails> email_addresses
    {
368 369
      get
        {
370
          this._contact_notify_contact_info (true, false);
371 372
          return this._email_addresses_ro;
        }
373 374 375 376 377 378
      set { this.change_email_addresses.begin (value); }
    }

  /**
   * {@inheritDoc}
   *
Travis Reitter's avatar
Travis Reitter committed
379
   * @since 0.6.4
380 381 382 383 384 385 386 387
   */
  public async void change_email_addresses (
      Set<EmailFieldDetails> email_addresses) throws PropertyError
    {
      yield this._change_details<EmailFieldDetails> (email_addresses,
          this._email_addresses, "email");
    }

388 389
  /* NOTE: Other properties support lazy initialisation, but im-addresses
   * doesn't as it's a linkable property, so always has to be loaded anyway. */
390 391
  private HashMultiMap<string, ImFieldDetails> _im_addresses =
      new HashMultiMap<string, ImFieldDetails> (null, null,
392 393
          (GLib.HashFunc) ImFieldDetails.hash,
          (GLib.EqualFunc) ImFieldDetails.equal);
394

395
  /**
396
   * A mapping of IM protocol to an (unordered) set of IM addresses.
397
   *
Travis Reitter's avatar
Travis Reitter committed
398
   * See {@link Folks.ImDetails.im_addresses}.
399
   */
400
  [CCode (notify = false)]
401
  public MultiMap<string, ImFieldDetails> im_addresses
402 403
    {
      get { return this._im_addresses; }
404
      set { this.change_im_addresses.begin (value); }
405 406
    }

407 408 409 410 411 412 413
  private uint _im_interaction_count = 0;

  /**
   * A counter for IM interactions (send/receive message) with the persona.
   *
   * See {@link Folks.InteractionDetails.im_interaction_count}
   *
Travis Reitter's avatar
Travis Reitter committed
414
   * @since 0.7.1
415 416 417 418 419 420 421 422 423 424 425 426 427 428
   */
  public uint im_interaction_count
    {
      get { return this._im_interaction_count; }
    }

  internal DateTime? _last_im_interaction_datetime = null;

  /**
   * The latest datetime for IM interactions (send/receive message) with the
   * persona.
   *
   * See {@link Folks.InteractionDetails.last_im_interaction_datetime}
   *
Travis Reitter's avatar
Travis Reitter committed
429
   * @since 0.7.1
430 431 432 433 434 435 436 437 438 439 440 441 442
   */
  public DateTime? last_im_interaction_datetime
    {
      get { return this._last_im_interaction_datetime; }
    }

  private uint _call_interaction_count = 0;

  /**
   * A counter for call interactions (only successful calls) with the persona.
   *
   * See {@link Folks.InteractionDetails.call_interaction_count}
   *
Travis Reitter's avatar
Travis Reitter committed
443
   * @since 0.7.1
444 445 446 447 448 449 450 451 452 453 454 455 456 457
   */
  public uint call_interaction_count
    {
      get { return this._call_interaction_count; }
    }

  internal DateTime? _last_call_interaction_datetime = null;

  /**
   * The latest datetime for call interactions (only successful calls) with the
   * persona.
   *
   * See {@link Folks.InteractionDetails.last_call_interaction_datetime}
   *
Travis Reitter's avatar
Travis Reitter committed
458
   * @since 0.7.1
459 460 461 462 463 464
   */
  public DateTime? last_call_interaction_datetime
    {
      get { return this._last_call_interaction_datetime; }
    }

465 466 467
  private HashSet<string> _groups = new HashSet<string> ();
  private Set<string> _groups_ro;

468
  /**
469
   * A set group IDs for the groups the contact is a member of.
470
   *
471
   * See {@link Folks.GroupDetails.groups}.
472
   */
473
  [CCode (notify = false)]
474
  public Set<string> groups
475
    {
476
      get { return this._groups_ro; }
477
      set { this.change_groups.begin (value); }
478 479
    }

480
  /**
481 482
   * Add or remove the Persona from the specified group.
   *
483
   * See {@link Folks.GroupDetails.change_group}.
484 485 486
   *
   * @throws Folks.PropertyError.UNKNOWN_ERROR if changing group membership
   * failed
487
   */
488
  public async void change_group (string group, bool is_member)
489 490
      throws GLib.Error
    {
491 492 493 494 495 496 497 498 499 500 501 502 503 504
      /* Ensure we have a strong ref to the contact for the duration of the
       * operation. */
      var contact = (Contact?) this._contact.get ();

      if (contact == null)
        {
          /* The Tpf.Persona is being served out of the cache. */
          throw new PropertyError.UNAVAILABLE (
              _("Failed to change group membership: %s"),
              /* Translators: "account" refers to an instant messaging
               * account. */
              _("Account is offline."));
        }

505 506
      try
        {
507
          if (is_member && !this._groups.contains (group))
508
            {
509
              yield contact.add_to_group_async (group);
510
            }
511
          else if (!is_member && this._groups.contains (group))
512
            {
513
              yield contact.remove_from_group_async (group);
514 515 516 517 518 519 520 521
            }
        }
      catch (GLib.Error e)
        {
          /* Translators: the parameter is an error message. */
          throw new PropertyError.UNKNOWN_ERROR (
              _("Failed to change group membership: %s"), e.message);
        }
522 523

      /* The change will be notified when we receive changes from the store. */
524 525 526
    }

  /* Note: Only ever called as a result of signals from Telepathy. */
527
  private void _contact_groups_changed (string[] added, string[] removed)
528
    {
529
      var changed = false;
530

531
      foreach (var group in added)
532
        {
533 534 535 536 537
          if (this._groups.add (group) == true)
            {
              changed = true;
              this.group_changed (group, true);
            }
538
        }
539 540

      foreach (var group in removed)
541
        {
542 543 544 545 546
          if (this._groups.remove (group) == true)
            {
              changed = true;
              this.group_changed (group, false);
            }
547
        }
548

549
      /* Notify if anything changed. */
550
      if (changed == true)
551 552
        {
          this.notify_property ("groups");
553 554 555

          /* Mark the cache as needing to be updated. */
          ((Tpf.PersonaStore) this.store)._set_cache_needs_update ();
556
        }
557 558
    }

559 560 561
  /**
   * {@inheritDoc}
   *
Raul Gutierrez Segales's avatar
Raul Gutierrez Segales committed
562
   * @since 0.6.2
563 564 565
   */
  public async void change_groups (Set<string> groups) throws PropertyError
    {
566 567 568 569 570 571 572 573 574 575 576 577
      var contact = (Contact?) this._contact.get ();

      if (contact == null)
        {
          /* The Tpf.Persona is being served out of the cache. */
          throw new PropertyError.UNAVAILABLE (
              _("Failed to change group membership: %s"),
              /* Translators: "account" refers to an instant messaging
               * account. */
              _("Account is offline."));
        }

578
      try
579
        {
580
          yield contact.set_contact_groups_async (groups.to_array ());
581
        }
582
      catch (GLib.Error e)
583
        {
584 585 586
          /* Translators: the parameter is an error message. */
          throw new PropertyError.UNKNOWN_ERROR (
              _("Failed to change group membership: %s"), e.message);
587 588
        }

589
      /* The change will be notified when we receive changes from the store. */
590 591
    }

592
  /* This has to be weak since, in general, we can't force any TpContacts to
593 594 595 596 597 598 599 600 601
   * remain alive if we want to solve bgo#665376.
   * As per bgo#680335, we have to use a WeakRef rather than a
   * ‘weak Contact?’ to avoid races when clearing the pointer. We still have
   * to use a weak ref. notifier as well, though, in order to be able to emit
   * a property change notification for ::contact.
   *
   * FIXME: Once bgo#554344 is fixed, _contact could be changed back to
   * being a 'weak Contact?', assuming Vala implements weak references using
   * GWeakRef. */
602
  private GLib.WeakRef _contact = GLib.WeakRef (null);
603 604 605

  private void _contact_weak_notify_cb (Object obj)
    {
606 607
      debug ("TpContact %p destroyed; setting ._contact = null in Persona %p",
          obj, this);
608
      /* _contact is cleared automatically as it's a WeakRef. */
609 610 611
      this.notify_property ("contact");
    }

612 613
  /**
   * The Telepathy contact represented by this persona.
614
   *
615
   * Note that this may be ``null`` if the {@link PersonaStore} providing this
616 617 618 619
   * {@link Persona} isn't currently available (e.g. due to not being connected
   * to the network). In this case, most other properties of the {@link Persona}
   * are being retrieved from a cache and may not be current (though there's no
   * way to tell this).
620
   */
621 622 623 624
  public Contact? contact
    {
      get
        {
625 626 627 628 629 630 631 632
          /* FIXME: This property should be changed to transfer its reference
           * when the API is next broken. This is necessary because the
           * TpfPersona doesn't hold a strong ref to the TpContact, so any
           * pointer which is returned might be invalidated before reaching the
           * caller. Probably not a problem in practice since folks won't be
           * run multi-threaded. */
          Contact? contact = (Contact?) this._contact.get ();
          if (contact == null)
633 634 635 636
            {
              return null;
            }

637 638 639
          /* FIXME: I'm so very, very sorry. This is to cause Vala to forget
           * we have a strong ref on 'contact' and not transfer it out. */
          return (Contact) ((void*) contact);
640 641 642 643 644 645 646 647 648
        }

      construct
        {
          if (value != null)
            {
              value.weak_ref (this._contact_weak_notify_cb);
            }

649
          this._contact.set (value);
650 651
        }
    }
652

653 654
  private HashSet<PhoneFieldDetails>? _phone_numbers = null;
  private Set<PhoneFieldDetails>? _phone_numbers_ro = null;
655 656 657 658

  /**
   * {@inheritDoc}
   *
Travis Reitter's avatar
Travis Reitter committed
659
   * @since 0.6.4
660 661 662 663
   */
  [CCode (notify = false)]
  public Set<PhoneFieldDetails> phone_numbers
    {
664 665
      get
        {
666
          this._contact_notify_contact_info (true, false);
667 668
          return this._phone_numbers_ro;
        }
669 670 671
      set { this.change_phone_numbers.begin (value); }
    }

672 673 674
  /**
   * {@inheritDoc}
   *
Travis Reitter's avatar
Travis Reitter committed
675
   * @since 0.6.4
676 677 678 679 680 681 682 683
   */
  public async void change_phone_numbers (
      Set<PhoneFieldDetails> phone_numbers) throws PropertyError
    {
      yield this._change_details<PhoneFieldDetails> (phone_numbers,
          this._phone_numbers, "tel");
    }

684 685
  private HashSet<UrlFieldDetails>? _urls = null;
  private Set<UrlFieldDetails>? _urls_ro = null;
686 687 688 689

  /**
   * {@inheritDoc}
   *
Travis Reitter's avatar
Travis Reitter committed
690
   * @since 0.6.4
691 692 693 694
   */
  [CCode (notify = false)]
  public Set<UrlFieldDetails> urls
    {
695 696
      get
        {
697
          this._contact_notify_contact_info (true, false);
698 699
          return this._urls_ro;
        }
700 701 702 703 704 705
      set { this.change_urls.begin (value); }
    }

  /**
   * {@inheritDoc}
   *
Travis Reitter's avatar
Travis Reitter committed
706
   * @since 0.6.4
707 708 709 710 711 712 713
   */
  public async void change_urls (Set<UrlFieldDetails> urls) throws PropertyError
    {
      yield this._change_details<UrlFieldDetails> (urls,
          this._urls, "url");
    }

714 715
  private async void _change_details<T> (
      Set<AbstractFieldDetails<string>> details,
716
      Set<AbstractFieldDetails<string>>? member_set,
717 718 719 720 721
      string field_name)
        throws PropertyError
    {
      var tpf_store = this.store as Tpf.PersonaStore;

722 723
      if (member_set != null &&
          Folks.Internal.equal_sets<T> (details, member_set))
724 725 726 727 728 729 730 731 732 733 734 735 736 737 738 739 740 741 742 743 744 745 746 747 748 749 750 751
        {
          return;
        }

      if (this._is_constructed)
        {
          try
            {
              yield tpf_store._change_user_details (this, details, field_name);
            }
          catch (PersonaStoreError.INVALID_ARGUMENT e1)
            {
              throw new PropertyError.NOT_WRITEABLE (e1.message);
            }
          catch (PersonaStoreError.STORE_OFFLINE e2)
            {
              throw new PropertyError.UNKNOWN_ERROR (e2.message);
            }
          catch (PersonaStoreError e3)
            {
              throw new PropertyError.UNKNOWN_ERROR (e3.message);
            }
        }

      /* the change will be notified when we receive changes to
       * contact.contact_info */
    }

752 753 754
  /**
   * Create a new persona.
   *
755 756
   * Create a new persona for the {@link PersonaStore} ``store``, representing
   * the Telepathy contact given by ``contact``.
757 758 759
   *
   * @param contact the Telepathy contact being represented by the persona
   * @param store the persona store to place the persona in
760
   */
761
  public Persona (Contact contact, PersonaStore store)
762
    {
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);
767

768
      Object (contact: contact,
769
              display_id: id,
Travis Reitter's avatar
Travis Reitter committed
770
              /* FIXME: This IID format should be moved out to the ImDetails
771 772 773 774
               * interface along with the code in
               * Kf.Persona.linkable_property_to_links(), but that depends on
               * bgo#624842 being fixed. */
              iid: account.get_protocol () + ":" + id,
Travis Reitter's avatar
Travis Reitter committed
775
              uid: uid,
776
              store: store,
777
              is_user: contact.handle == connection.self_handle);
778

779 780 781 782 783 784 785 786 787 788
      debug ("Created new Tpf.Persona '%s' for service-specific UID '%s': %p",
          uid, id, this);
    }

  construct
    {
      this._groups_ro = this._groups.read_only_view;

      /* Contact can be null if we've been created from the cache. All the code
       * below this point is for non-cached personas. */
789 790 791
      var contact = (Contact?) this._contact.get ();

      if (contact == null)
792 793 794
        {
          return;
        }
795

796
      /* Set our alias. */
797
      this._alias = contact.get_alias ();
798

799
      contact.notify["alias"].connect ((s, p) =>
800
          {
801 802 803
            var c = (Contact?) this._contact.get ();
            assert (c != null); /* should never be called while cached */

804
            /* Tp guarantees that aliases are always non-null. */
805
            assert (c.alias != null);
806

807
            if (this._alias != c.alias)
808
              {
809
                this._alias = c.alias;
810
                this.notify_property ("alias");
811 812 813

                /* Mark the cache as needing to be updated. */
                ((Tpf.PersonaStore) this.store)._set_cache_needs_update ();
814 815 816
              }
          });

817
      /* Set our single IM address */
818
      var connection = contact.connection;
819
      var account = connection.get_account ();
820

821 822
      try
        {
823
          var im_addr = ImDetails.normalise_im_address (this.display_id,
824 825 826
              account.get_protocol ());
          var im_fd = new ImFieldDetails (im_addr);
          this._im_addresses.set (account.get_protocol (), im_fd);
827
        }
Travis Reitter's avatar
Travis Reitter committed
828
      catch (ImDetailsError e)
829 830 831 832
        {
          /* This should never happen…but if it does, warn of it and continue */
          warning (e.message);
        }
833

834
      contact.notify["avatar-file"].connect ((s, p) =>
835
        {
836
          this._contact_notify_avatar ();
837
        });
838
      this._contact_notify_avatar ();
839

840
      contact.notify["presence-message"].connect ((s, p) =>
841
        {
842
          this._contact_notify_presence_message ();
843
        });
844
      contact.notify["presence-type"].connect ((s, p) =>
845
        {
846
          this._contact_notify_presence_type ();
847
        });
848
      contact.notify["presence-status"].connect ((s, p) =>
849 850 851
        {
          this._contact_notify_presence_status ();
        });
852 853
      this._contact_notify_presence_message ();
      this._contact_notify_presence_type ();
854
      this._contact_notify_presence_status ();
855

856
      contact.notify["contact-info"].connect ((s, p) =>
857
        {
858
          this._contact_notify_contact_info (false);
859
        });
860
      this._contact_notify_contact_info (false);
861

862
      contact.contact_groups_changed.connect ((added, removed) =>
863 864 865
        {
          this._contact_groups_changed (added, removed);
        });
866
      this._contact_groups_changed (contact.get_contact_groups (), {});
867

868 869
      var tpf_store = this.store as Tpf.PersonaStore;

870 871
      if (this.is_user)
        {
872 873 874 875
          tpf_store.notify["supported-fields"].connect ((s, p) =>
            {
              this._update_writeable_properties ();
            });
876
        }
877 878 879 880 881 882 883

      tpf_store.notify["always-writeable-properties"].connect ((s, p) =>
        {
          this._update_writeable_properties ();
        });

      this._update_writeable_properties ();
884 885
    }

886 887 888 889 890 891
  /* Called after all construction-time properties have been set. */
  public override void constructed ()
    {
      this._is_constructed = true;
    }

892
  private void _update_writeable_properties ()
893 894
    {
      var tpf_store = this.store as Tpf.PersonaStore;
895 896 897 898 899 900 901 902 903 904 905 906 907 908 909
      this._writeable_properties = this.store.always_writeable_properties;

      if (this.is_user)
        {
          if ("bday" in tpf_store.supported_fields)
            this._writeable_properties += "birthday";
          if ("email" in tpf_store.supported_fields)
            this._writeable_properties += "email-addresses";
          if ("fn" in tpf_store.supported_fields)
            this._writeable_properties += "full-name";
          if ("tel" in tpf_store.supported_fields)
            this._writeable_properties += "phone-numbers";
          if ("url" in tpf_store.supported_fields)
            this._writeable_properties += "urls";
        }
910 911
    }

912
  private void _contact_notify_contact_info (bool create_if_not_exists, bool emit_notification = true)
913 914 915 916 917 918 919 920 921 922 923 924 925 926 927 928 929 930
    {
      assert ((
          (this._email_addresses == null) &&
          (this._phone_numbers == null) &&
          (this._urls == null)
        ) || (
          (this._email_addresses != null) &&
          (this._phone_numbers != null) &&
          (this._urls != null)
        ));

      /* See the comments in Folks.Individual about the lazy instantiation
       * strategy for URIs, etc.
       *
       * It's necessary to notify for all three properties here, as this
       * function is called identically for all of them. */
      if (this._urls == null && create_if_not_exists == false)
        {
931 932 933 934 935 936
          if (emit_notification)
            {
              this.notify_property ("email-addresses");
              this.notify_property ("phone-numbers");
              this.notify_property ("urls");
            }
937 938 939 940 941 942 943 944 945 946 947 948 949 950 951 952 953 954 955 956
          return;
        }
      else if (this._urls == null)
        {
          this._urls = new HashSet<UrlFieldDetails>