tpf-persona.vala 38 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
using Zeitgeist;
26

27 28 29
/**
 * A persona subclass which represents a single instant messaging contact from
 * Telepathy.
30 31 32 33 34 35
 *
 * There is a one-to-one correspondence between {@link Tpf.Persona}s and
 * {@link TelepathyGLib.Contact}s, although at any time the
 * {@link Tpf.Persona.contact} property of a persona may be `null` if the
 * 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.
36
 */
37
public class Tpf.Persona : Folks.Persona,
38
    AliasDetails,
39
    AvatarDetails,
40
    BirthdayDetails,
41
    EmailDetails,
42
    FavouriteDetails,
43
    GroupDetails,
44
    InteractionDetails,
Travis Reitter's avatar
Travis Reitter committed
45
    ImDetails,
46
    NameDetails,
47
    PhoneDetails,
48 49
    PresenceDetails,
    UrlDetails
50
{
51
  private const string[] _linkable_properties = { "im-addresses" };
52
  private string[] _writeable_properties = null;
53

54 55 56 57
  /* 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.
   */
58
  private bool _is_constructed = false;
59

60 61 62 63 64 65 66 67
  /**
   * 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.
   *
68
   * @since 0.3.5
69 70 71
   */
  public bool is_in_contact_list { get; set; }

72 73
  private LoadableIcon? _avatar = null;

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

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

100 101
  private string _full_name = ""; /* must never be null */

102 103 104
  /**
   * {@inheritDoc}
   *
Travis Reitter's avatar
Travis Reitter committed
105
   * @since 0.6.4
106 107 108 109 110 111 112 113 114 115 116
   */
  [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
117
   * @since 0.6.4
118 119 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
   */
  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
153
   * @since 0.6.4
154 155 156 157 158 159 160 161
   */
  [CCode (notify = false)]
  public string nickname
    {
      get { return ""; }
      set { this.change_nickname.begin (value); } /* not writeable */
    }

162 163 164 165 166
  /**
   * {@inheritDoc}
   *
   * ContactInfo has no equivalent field, so this is unsupported.
   *
Travis Reitter's avatar
Travis Reitter committed
167
   * @since 0.6.4
168 169 170 171 172 173 174 175 176 177 178 179
   */
  [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
180
   * @since 0.6.4
181 182 183 184 185 186 187 188 189 190 191
   */
  [CCode (notify = false)]
  public DateTime? birthday
    {
      get { return this._birthday; }
      set { this.change_birthday.begin (value); }
    }

  /**
   * {@inheritDoc}
   *
Travis Reitter's avatar
Travis Reitter committed
192
   * @since 0.6.4
193 194 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
   */
  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 */
    }

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

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

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

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

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

271 272
  private string _alias = ""; /* must never be null */

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

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

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

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

305 306
  private bool _is_favourite = false;

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

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

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

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

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

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

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

355 356
  private HashSet<EmailFieldDetails> _email_addresses =
      new HashSet<EmailFieldDetails> (
357 358
          (GLib.HashFunc) EmailFieldDetails.hash,
          (GLib.EqualFunc) EmailFieldDetails.equal);
359 360 361 362 363
  private Set<EmailFieldDetails> _email_addresses_ro;

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

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

385 386
  private HashMultiMap<string, ImFieldDetails> _im_addresses =
      new HashMultiMap<string, ImFieldDetails> (null, null,
387 388
          (GLib.HashFunc) ImFieldDetails.hash,
          (GLib.EqualFunc) ImFieldDetails.equal);
389

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

402 403 404 405 406 407 408
  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
409
   * @since 0.7.1
410 411 412 413 414 415 416 417 418 419 420 421 422 423
   */
  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
424
   * @since 0.7.1
425 426 427 428 429 430 431 432 433 434 435 436 437
   */
  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
438
   * @since 0.7.1
439 440 441 442 443 444 445 446 447 448 449 450 451 452
   */
  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
453
   * @since 0.7.1
454 455 456 457 458 459
   */
  public DateTime? last_call_interaction_datetime
    {
      get { return this._last_call_interaction_datetime; }
    }

460 461 462
  private HashSet<string> _groups = new HashSet<string> ();
  private Set<string> _groups_ro;

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

475
  /**
476 477
   * Add or remove the Persona from the specified group.
   *
478
   * See {@link Folks.GroupDetails.change_group}.
479 480 481
   *
   * @throws Folks.PropertyError.UNKNOWN_ERROR if changing group membership
   * failed
482
   */
483
  public async void change_group (string group, bool is_member)
484 485 486 487
      throws GLib.Error
    {
      try
        {
488
          if (is_member && !this._groups.contains (group))
489 490 491
            {
              yield this.contact.add_to_group_async (group);
            }
492
          else if (!is_member && this._groups.contains (group))
493 494 495 496 497 498 499 500 501 502
            {
              yield this.contact.remove_from_group_async (group);
            }
        }
      catch (GLib.Error e)
        {
          /* Translators: the parameter is an error message. */
          throw new PropertyError.UNKNOWN_ERROR (
              _("Failed to change group membership: %s"), e.message);
        }
503 504

      /* The change will be notified when we receive changes from the store. */
505 506 507
    }

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

512
      foreach (var group in added)
513
        {
514 515 516 517 518
          if (this._groups.add (group) == true)
            {
              changed = true;
              this.group_changed (group, true);
            }
519
        }
520 521

      foreach (var group in removed)
522
        {
523 524 525 526 527
          if (this._groups.remove (group) == true)
            {
              changed = true;
              this.group_changed (group, false);
            }
528
        }
529

530
      /* Notify if anything changed. */
531
      if (changed == true)
532 533
        {
          this.notify_property ("groups");
534 535 536

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

540 541 542
  /**
   * {@inheritDoc}
   *
Raul Gutierrez Segales's avatar
Raul Gutierrez Segales committed
543
   * @since 0.6.2
544 545 546
   */
  public async void change_groups (Set<string> groups) throws PropertyError
    {
547
      try
548
        {
549
          yield this.contact.set_contact_groups_async (groups.to_array ());
550
        }
551
      catch (GLib.Error e)
552
        {
553 554 555
          /* Translators: the parameter is an error message. */
          throw new PropertyError.UNKNOWN_ERROR (
              _("Failed to change group membership: %s"), e.message);
556 557
        }

558
      /* The change will be notified when we receive changes from the store. */
559 560
    }

561 562 563 564 565 566
  /* This has to be weak since, in general, we can't force any TpContacts to
   * remain alive if we want to solve bgo#665376. */
  private weak Contact? _contact = null;

  private void _contact_weak_notify_cb (Object obj)
    {
567 568
      debug ("TpContact %p destroyed; setting ._contact = null in Persona %p",
          obj, this);
569 570 571 572
      this._contact = null;
      this.notify_property ("contact");
    }

573 574
  /**
   * The Telepathy contact represented by this persona.
575 576 577 578 579 580
   *
   * Note that this may be `null` if the {@link PersonaStore} providing this
   * {@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).
581
   */
582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603
  public Contact? contact
    {
      get
        {
          if (this._contact == null)
            {
              return null;
            }

          return this._contact;
        }

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

          this._contact = value;
        }
    }
604

605 606
  private HashSet<PhoneFieldDetails> _phone_numbers =
      new HashSet<PhoneFieldDetails> (
607 608
          (GLib.HashFunc) PhoneFieldDetails.hash,
          (GLib.EqualFunc) PhoneFieldDetails.equal);
609 610 611 612 613
  private Set<PhoneFieldDetails> _phone_numbers_ro;

  /**
   * {@inheritDoc}
   *
Travis Reitter's avatar
Travis Reitter committed
614
   * @since 0.6.4
615 616 617 618 619 620 621 622
   */
  [CCode (notify = false)]
  public Set<PhoneFieldDetails> phone_numbers
    {
      get { return this._phone_numbers_ro; }
      set { this.change_phone_numbers.begin (value); }
    }

623 624 625
  /**
   * {@inheritDoc}
   *
Travis Reitter's avatar
Travis Reitter committed
626
   * @since 0.6.4
627 628 629 630 631 632 633 634
   */
  public async void change_phone_numbers (
      Set<PhoneFieldDetails> phone_numbers) throws PropertyError
    {
      yield this._change_details<PhoneFieldDetails> (phone_numbers,
          this._phone_numbers, "tel");
    }

635
  private HashSet<UrlFieldDetails> _urls = new HashSet<UrlFieldDetails> (
636 637
      (GLib.HashFunc) UrlFieldDetails.hash,
      (GLib.EqualFunc) UrlFieldDetails.equal);
638 639 640 641 642
  private Set<UrlFieldDetails> _urls_ro;

  /**
   * {@inheritDoc}
   *
Travis Reitter's avatar
Travis Reitter committed
643
   * @since 0.6.4
644 645 646 647 648 649 650 651 652 653 654
   */
  [CCode (notify = false)]
  public Set<UrlFieldDetails> urls
    {
      get { return this._urls_ro; }
      set { this.change_urls.begin (value); }
    }

  /**
   * {@inheritDoc}
   *
Travis Reitter's avatar
Travis Reitter committed
655
   * @since 0.6.4
656 657 658 659 660 661 662
   */
  public async void change_urls (Set<UrlFieldDetails> urls) throws PropertyError
    {
      yield this._change_details<UrlFieldDetails> (urls,
          this._urls, "url");
    }

663 664 665 666 667 668 669 670
  private async void _change_details<T> (
      Set<AbstractFieldDetails<string>> details,
      Set<AbstractFieldDetails<string>> member_set,
      string field_name)
        throws PropertyError
    {
      var tpf_store = this.store as Tpf.PersonaStore;

671
      if (Folks.Internal.equal_sets<T> (details, member_set))
672 673 674 675 676 677 678 679 680 681 682 683 684 685 686 687 688 689 690 691 692 693 694 695 696 697 698 699
        {
          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 */
    }

700 701 702 703 704
  /**
   * Create a new persona.
   *
   * Create a new persona for the {@link PersonaStore} `store`, representing
   * the Telepathy contact given by `contact`.
705 706 707
   *
   * @param contact the Telepathy contact being represented by the persona
   * @param store the persona store to place the persona in
708
   */
709
  public Persona (Contact contact, PersonaStore store)
710
    {
711
      unowned string id = contact.get_identifier ();
712
      var connection = contact.connection;
713
      var account = connection.get_account ();
714
      var uid = Folks.Persona.build_uid (store.type_id, store.id, id);
715

716
      Object (contact: contact,
717
              display_id: id,
Travis Reitter's avatar
Travis Reitter committed
718
              /* FIXME: This IID format should be moved out to the ImDetails
719 720 721 722
               * 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
723
              uid: uid,
724
              store: store,
725
              is_user: contact.handle == connection.self_handle);
726

727 728 729 730 731 732 733 734 735 736 737 738 739 740 741 742 743
      debug ("Created new Tpf.Persona '%s' for service-specific UID '%s': %p",
          uid, id, this);
    }

  construct
    {
      this._groups_ro = this._groups.read_only_view;
      this._email_addresses_ro = this._email_addresses.read_only_view;
      this._phone_numbers_ro = this._phone_numbers.read_only_view;
      this._urls_ro = this._urls.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. */
      if (this.contact == null)
        {
          return;
        }
744

745 746
      /* Set our alias. */
      this._alias = this.contact.get_alias ();
747

748
      this.contact.notify["alias"].connect ((s, p) =>
749
          {
750 751 752
            /* Tp guarantees that aliases are always non-null. */
            assert (this.contact.alias != null);

753
            if (this._alias != this.contact.alias)
754
              {
755
                this._alias = this.contact.alias;
756
                this.notify_property ("alias");
757 758 759

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

763
      /* Set our single IM address */
764
      var connection = this.contact.connection;
765
      var account = connection.get_account ();
766

767 768
      try
        {
769
          var im_addr = ImDetails.normalise_im_address (this.display_id,
770 771 772
              account.get_protocol ());
          var im_fd = new ImFieldDetails (im_addr);
          this._im_addresses.set (account.get_protocol (), im_fd);
773
        }
Travis Reitter's avatar
Travis Reitter committed
774
      catch (ImDetailsError e)
775 776 777 778
        {
          /* This should never happen…but if it does, warn of it and continue */
          warning (e.message);
        }
779

780
      this.contact.notify["avatar-file"].connect ((s, p) =>
781
        {
782
          this._contact_notify_avatar ();
783
        });
784
      this._contact_notify_avatar ();
785

786
      this.contact.notify["presence-message"].connect ((s, p) =>
787
        {
788
          this._contact_notify_presence_message ();
789
        });
790
      this.contact.notify["presence-type"].connect ((s, p) =>
791
        {
792
          this._contact_notify_presence_type ();
793
        });
794
      this.contact.notify["presence-status"].connect ((s, p) =>
795 796 797
        {
          this._contact_notify_presence_status ();
        });
798 799
      this._contact_notify_presence_message ();
      this._contact_notify_presence_type ();
800
      this._contact_notify_presence_status ();
801

802
      this.contact.notify["contact-info"].connect ((s, p) =>
803
        {
804
          this._contact_notify_contact_info ();
805
        });
806
      this._contact_notify_contact_info ();
807

808 809 810 811 812
      this.contact.contact_groups_changed.connect ((added, removed) =>
        {
          this._contact_groups_changed (added, removed);
        });
      this._contact_groups_changed (this.contact.get_contact_groups (), {});
813

814 815
      var tpf_store = this.store as Tpf.PersonaStore;

816 817
      if (this.is_user)
        {
818 819 820 821
          tpf_store.notify["supported-fields"].connect ((s, p) =>
            {
              this._update_writeable_properties ();
            });
822
        }
823 824 825 826 827 828 829

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

      this._update_writeable_properties ();
830 831
    }

832 833 834 835 836 837
  /* Called after all construction-time properties have been set. */
  public override void constructed ()
    {
      this._is_constructed = true;
    }

838
  private void _update_writeable_properties ()
839 840
    {
      var tpf_store = this.store as Tpf.PersonaStore;
841 842 843 844 845 846 847 848 849 850 851 852 853 854 855
      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";
        }
856 857
    }

858
  private void _contact_notify_contact_info ()
859
    {
860
      var changed = false;
861
      var new_birthday_str = "";
862
      var new_full_name = "";
863
      var new_email_addresses = new HashSet<EmailFieldDetails> (
864 865
          (GLib.HashFunc) EmailFieldDetails.hash,
          (GLib.EqualFunc) EmailFieldDetails.equal);
866
      var new_phone_numbers = new HashSet<PhoneFieldDetails> (
867 868
          (GLib.HashFunc) PhoneFieldDetails.hash,
          (GLib.EqualFunc) PhoneFieldDetails.equal);
869
      var new_urls = new HashSet<UrlFieldDetails> (
870 871
          (GLib.HashFunc) UrlFieldDetails.hash,
          (GLib.EqualFunc) UrlFieldDetails.equal);
872 873 874 875

      var contact_info = this.contact.get_contact_info ();
      foreach (var info in contact_info)
        {
876
          if (info.field_name == "") {}
877 878
          else if (info.field_name == "bday")
            {
879
              new_birthday_str = info.field_value[0] ?? "";
880
            }
881 882 883 884
          else if (info.field_name == "email")
            {
              foreach (var email_addr in info.field_value)
                {
885 886 887 888 889 890
                  if (email_addr != "")
                    {
                      var parameters = this._afd_params_from_strv (info.parameters);
                      var email_fd = new EmailFieldDetails (email_addr, parameters);
                      new_email_addresses.add (email_fd);
                    }
891 892
                }
            }
893 894 895
          else if (info.field_name == "fn")
            {
              new_full_name = info.field_value[0];
896 897
              if (new_full_name == null)
                new_full_name = "";
898 899
            }
          else if (info.field_name == "tel")
900
            {
901 902
              foreach (var phone_num in info.field_value)
                {
903 904 905 906 907 908
                  if (phone_num != "")
                    {
                      var parameters = this._afd_params_from_strv (info.parameters);
                      var phone_fd = new PhoneFieldDetails (phone_num, parameters);
                      new_phone_numbers.add (phone_fd);
                    }
909
                }
910
            }
911 912 913 914
          else if (info.field_name == "url")
            {
              foreach (var url in info.field_value)
                {
915 916 917 918 919 920
                  if (url != "")
                    {
                      var parameters = this._afd_params_from_strv (info.parameters);
                      var url_fd = new UrlFieldDetails (url, parameters);
                      new_urls.add (url_fd);
                    }
921 922
                }
            }
923 924
        }

925 926 927
      if (new_birthday_str != "")
        {
          var timeval = TimeVal ();
928
          if (timeval.from_iso8601 (new_birthday_str))
929
            {
930 931 932 933 934 935 936
              var d = new DateTime.from_timeval_utc (timeval);
              if (this._birthday == null ||
                  (this._birthday != null &&
                    !this._birthday.equal (d.to_utc ())))
                {
                  this._birthday = d.to_utc ();
                  this.notify_property ("birthday");
937
                  changed = true;
938 939 940 941
                }
            }
          else
            {
942
              debug ("Failed to parse new birthday string '%s'"