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

27 28 29 30
/**
 * A persona subclass which represents a single instant messaging contact from
 * Telepathy.
 */
31
public class Tpf.Persona : Folks.Persona,
32
    AliasDetails,
33
    AvatarDetails,
34
    BirthdayDetails,
35
    EmailDetails,
36
    FavouriteDetails,
37
    GroupDetails,
38
    InteractionDetails,
Travis Reitter's avatar
Travis Reitter committed
39
    ImDetails,
40
    NameDetails,
41
    PhoneDetails,
42 43
    PresenceDetails,
    UrlDetails
44
{
45
  private const string[] _linkable_properties = { "im-addresses" };
46
  private string[] _writeable_properties = null;
47

48 49 50 51
  /* 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.
   */
52
  private bool _is_constructed = false;
53

54 55 56 57 58 59 60 61
  /**
   * 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.
   *
62
   * @since 0.3.5
63 64 65
   */
  public bool is_in_contact_list { get; set; }

66 67
  private LoadableIcon? _avatar = null;

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

82 83 84
  /**
   * {@inheritDoc}
   *
Travis Reitter's avatar
Travis Reitter committed
85
   * @since 0.6.4
86 87 88 89 90 91 92 93
   */
  [CCode (notify = false)]
  public StructuredName? structured_name
    {
      get { return null; }
      set { this.change_structured_name.begin (value); } /* not writeable */
    }

94 95
  private string _full_name = ""; /* must never be null */

96 97 98
  /**
   * {@inheritDoc}
   *
Travis Reitter's avatar
Travis Reitter committed
99
   * @since 0.6.4
100 101 102 103 104 105 106 107 108 109 110
   */
  [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
111
   * @since 0.6.4
112 113 114 115 116 117 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
   */
  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
147
   * @since 0.6.4
148 149 150 151 152 153 154 155
   */
  [CCode (notify = false)]
  public string nickname
    {
      get { return ""; }
      set { this.change_nickname.begin (value); } /* not writeable */
    }

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

  /**
   * {@inheritDoc}
   *
Travis Reitter's avatar
Travis Reitter committed
186
   * @since 0.6.4
187 188 189 190 191 192 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
   */
  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 */
    }

222
  /**
223 224
   * The Persona's presence type.
   *
225
   * See {@link Folks.PresenceDetails.presence_type}.
226
   */
227
  public Folks.PresenceType presence_type { get; set; }
228

229 230 231 232 233
  /**
   * The Persona's presence status.
   *
   * See {@link Folks.PresenceDetails.presence_status}.
   *
Travis Reitter's avatar
Travis Reitter committed
234
   * @since 0.6.0
235
   */
236
  public string presence_status { get; set; }
237

238
  /**
239 240
   * The Persona's presence message.
   *
241
   * See {@link Folks.PresenceDetails.presence_message}.
242
   */
243
  public string presence_message { get; set; }
Travis Reitter's avatar
Travis Reitter committed
244

245 246 247 248 249 250 251 252 253 254
  /**
   * The names of the Persona's linkable properties.
   *
   * See {@link Folks.Persona.linkable_properties}.
   */
  public override string[] linkable_properties
    {
      get { return this._linkable_properties; }
    }

255 256 257
  /**
   * {@inheritDoc}
   *
Travis Reitter's avatar
Travis Reitter committed
258
   * @since 0.6.0
259 260 261
   */
  public override string[] writeable_properties
    {
262
      get { return this._writeable_properties; }
263 264
    }

265 266
  private string _alias = ""; /* must never be null */

267
  /**
268 269
   * An alias for the Persona.
   *
270
   * See {@link Folks.AliasDetails.alias}.
271
   */
272
  [CCode (notify = false)]
273 274 275
  public string alias
    {
      get { return this._alias; }
276 277
      set { this.change_alias.begin (value); }
    }
278

279 280 281
  /**
   * {@inheritDoc}
   *
Raul Gutierrez Segales's avatar
Raul Gutierrez Segales committed
282
   * @since 0.6.2
283 284 285 286
   */
  public async void change_alias (string alias) throws PropertyError
    {
      if (this._alias == alias)
287
        {
288 289
          return;
        }
290

291 292 293
      if (this._is_constructed)
        {
          yield ((Tpf.PersonaStore) this.store).change_alias (this, alias);
294
        }
295

296
      /* The change will be notified when we receive changes from the store. */
297 298
    }

299 300
  private bool _is_favourite = false;

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

313 314 315
  /**
   * {@inheritDoc}
   *
Raul Gutierrez Segales's avatar
Raul Gutierrez Segales committed
316
   * @since 0.6.2
317 318 319 320
   */
  public async void change_is_favourite (bool is_favourite) throws PropertyError
    {
      if (this._is_favourite == is_favourite)
Philip Withnall's avatar
Philip Withnall committed
321
        {
322 323
          return;
        }
Philip Withnall's avatar
Philip Withnall committed
324

325 326 327 328
      if (this._is_constructed)
        {
          yield ((Tpf.PersonaStore) this.store).change_is_favourite (this,
              is_favourite);
Philip Withnall's avatar
Philip Withnall committed
329
        }
330

331
      /* The change will be notified when we receive changes from the store. */
Philip Withnall's avatar
Philip Withnall committed
332 333
    }

334
  /* Note: Only ever called by Tpf.PersonaStore. */
335 336
  internal void _set_is_favourite (bool is_favourite)
    {
337 338 339 340 341
      if (this._is_favourite == is_favourite)
        {
          return;
        }

342 343
      this._is_favourite = is_favourite;
      this.notify_property ("is-favourite");
344 345 346

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

349 350
  private HashSet<EmailFieldDetails> _email_addresses =
      new HashSet<EmailFieldDetails> (
Jeremy Whiting's avatar
Jeremy Whiting committed
351 352
           AbstractFieldDetails<string>.hash_static,
           AbstractFieldDetails<string>.equal_static);
353 354 355 356 357
  private Set<EmailFieldDetails> _email_addresses_ro;

  /**
   * {@inheritDoc}
   *
Travis Reitter's avatar
Travis Reitter committed
358
   * @since 0.6.4
359 360 361 362 363 364 365 366 367 368 369
   */
  [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
370
   * @since 0.6.4
371 372 373 374 375 376 377 378
   */
  public async void change_email_addresses (
      Set<EmailFieldDetails> email_addresses) throws PropertyError
    {
      yield this._change_details<EmailFieldDetails> (email_addresses,
          this._email_addresses, "email");
    }

379 380
  private HashMultiMap<string, ImFieldDetails> _im_addresses =
      new HashMultiMap<string, ImFieldDetails> (null, null,
Jeremy Whiting's avatar
Jeremy Whiting committed
381 382
           AbstractFieldDetails<string>.hash_static,
           AbstractFieldDetails<string>.equal_static);
383

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

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

454 455 456
  private HashSet<string> _groups = new HashSet<string> ();
  private Set<string> _groups_ro;

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

469
  /**
470 471
   * Add or remove the Persona from the specified group.
   *
472
   * See {@link Folks.GroupDetails.change_group}.
473 474 475
   *
   * @throws Folks.PropertyError.UNKNOWN_ERROR if changing group membership
   * failed
476
   */
477
  public async void change_group (string group, bool is_member)
478 479 480 481
      throws GLib.Error
    {
      try
        {
482
          if (is_member && !this._groups.contains (group))
483 484 485
            {
              yield this.contact.add_to_group_async (group);
            }
486
          else if (!is_member && this._groups.contains (group))
487 488 489 490 491 492 493 494 495 496
            {
              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);
        }
497 498

      /* The change will be notified when we receive changes from the store. */
499 500 501
    }

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

506
      foreach (var group in added)
507
        {
508 509 510 511 512
          if (this._groups.add (group) == true)
            {
              changed = true;
              this.group_changed (group, true);
            }
513
        }
514 515

      foreach (var group in removed)
516
        {
517 518 519 520 521
          if (this._groups.remove (group) == true)
            {
              changed = true;
              this.group_changed (group, false);
            }
522
        }
523

524
      /* Notify if anything changed. */
525
      if (changed == true)
526 527
        {
          this.notify_property ("groups");
528 529 530

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

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

552
      /* The change will be notified when we receive changes from the store. */
553 554
    }

555 556 557 558 559 560
  /* 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)
    {
561 562
      debug ("TpContact %p destroyed; setting ._contact = null in Persona %p",
          obj, this);
563 564 565 566
      this._contact = null;
      this.notify_property ("contact");
    }

567 568
  /**
   * The Telepathy contact represented by this persona.
569 570 571 572 573 574
   *
   * 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).
575
   */
576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597
  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;
        }
    }
598

599 600
  private HashSet<PhoneFieldDetails> _phone_numbers =
      new HashSet<PhoneFieldDetails> (
Jeremy Whiting's avatar
Jeremy Whiting committed
601 602
           AbstractFieldDetails<string>.hash_static,
           AbstractFieldDetails<string>.equal_static);
603 604 605 606 607
  private Set<PhoneFieldDetails> _phone_numbers_ro;

  /**
   * {@inheritDoc}
   *
Travis Reitter's avatar
Travis Reitter committed
608
   * @since 0.6.4
609 610 611 612 613 614 615 616
   */
  [CCode (notify = false)]
  public Set<PhoneFieldDetails> phone_numbers
    {
      get { return this._phone_numbers_ro; }
      set { this.change_phone_numbers.begin (value); }
    }

617 618 619
  /**
   * {@inheritDoc}
   *
Travis Reitter's avatar
Travis Reitter committed
620
   * @since 0.6.4
621 622 623 624 625 626 627 628
   */
  public async void change_phone_numbers (
      Set<PhoneFieldDetails> phone_numbers) throws PropertyError
    {
      yield this._change_details<PhoneFieldDetails> (phone_numbers,
          this._phone_numbers, "tel");
    }

629
  private HashSet<UrlFieldDetails> _urls = new HashSet<UrlFieldDetails> (
Jeremy Whiting's avatar
Jeremy Whiting committed
630 631
       AbstractFieldDetails<string>.hash_static,
       AbstractFieldDetails<string>.equal_static);
632 633 634 635 636
  private Set<UrlFieldDetails> _urls_ro;

  /**
   * {@inheritDoc}
   *
Travis Reitter's avatar
Travis Reitter committed
637
   * @since 0.6.4
638 639 640 641 642 643 644 645 646 647 648
   */
  [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
649
   * @since 0.6.4
650 651 652 653 654 655 656
   */
  public async void change_urls (Set<UrlFieldDetails> urls) throws PropertyError
    {
      yield this._change_details<UrlFieldDetails> (urls,
          this._urls, "url");
    }

657 658 659 660 661 662 663 664
  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;

665
      if (Folks.Internal.equal_sets<T> (details, member_set))
666 667 668 669 670 671 672 673 674 675 676 677 678 679 680 681 682 683 684 685 686 687 688 689 690 691 692 693
        {
          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 */
    }

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

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

721 722 723 724 725 726 727 728 729 730 731 732 733 734 735 736 737
      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;
        }
738

739 740
      /* Set our alias. */
      this._alias = this.contact.get_alias ();
741

742
      this.contact.notify["alias"].connect ((s, p) =>
743
          {
744 745 746
            /* Tp guarantees that aliases are always non-null. */
            assert (this.contact.alias != null);

747
            if (this._alias != this.contact.alias)
748
              {
749
                this._alias = this.contact.alias;
750
                this.notify_property ("alias");
751 752 753

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

757
      /* Set our single IM address */
758
      var connection = this.contact.connection;
759
      var account = connection.get_account ();
760

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

774
      this.contact.notify["avatar-file"].connect ((s, p) =>
775
        {
776
          this._contact_notify_avatar ();
777
        });
778
      this._contact_notify_avatar ();
779

780
      this.contact.notify["presence-message"].connect ((s, p) =>
781
        {
782
          this._contact_notify_presence_message ();
783
        });
784
      this.contact.notify["presence-type"].connect ((s, p) =>
785
        {
786
          this._contact_notify_presence_type ();
787
        });
788
      this.contact.notify["presence-status"].connect ((s, p) =>
789 790 791
        {
          this._contact_notify_presence_status ();
        });
792 793
      this._contact_notify_presence_message ();
      this._contact_notify_presence_type ();
794
      this._contact_notify_presence_status ();
795

796
      this.contact.notify["contact-info"].connect ((s, p) =>
797
        {
798
          this._contact_notify_contact_info ();
799
        });
800
      this._contact_notify_contact_info ();
801

802 803 804 805 806
      this.contact.contact_groups_changed.connect ((added, removed) =>
        {
          this._contact_groups_changed (added, removed);
        });
      this._contact_groups_changed (this.contact.get_contact_groups (), {});
807

808 809
      var tpf_store = this.store as Tpf.PersonaStore;

810 811
      if (this.is_user)
        {
812 813 814 815
          tpf_store.notify["supported-fields"].connect ((s, p) =>
            {
              this._update_writeable_properties ();
            });
816
        }
817 818 819 820 821 822 823

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

      this._update_writeable_properties ();
824 825
    }

826 827 828 829 830 831
  /* Called after all construction-time properties have been set. */
  public override void constructed ()
    {
      this._is_constructed = true;
    }

832
  private void _update_writeable_properties ()
833 834
    {
      var tpf_store = this.store as Tpf.PersonaStore;
835 836 837 838 839 840 841 842 843 844 845 846 847 848 849
      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";
        }
850 851
    }

852
  private void _contact_notify_contact_info ()
853
    {
854
      var changed = false;
855
      var new_birthday_str = "";
856
      var new_full_name = "";
857
      var new_email_addresses = new HashSet<EmailFieldDetails> (
Jeremy Whiting's avatar
Jeremy Whiting committed
858 859
          AbstractFieldDetails<string>.hash_static,
          AbstractFieldDetails<string>.equal_static);
860
      var new_phone_numbers = new HashSet<PhoneFieldDetails> (
Jeremy Whiting's avatar
Jeremy Whiting committed
861 862
           AbstractFieldDetails<string>.hash_static,
           AbstractFieldDetails<string>.equal_static);
863
      var new_urls = new HashSet<UrlFieldDetails> (
Jeremy Whiting's avatar
Jeremy Whiting committed
864 865
           AbstractFieldDetails<string>.hash_static,
           AbstractFieldDetails<string>.equal_static);
866 867 868 869

      var contact_info = this.contact.get_contact_info ();
      foreach (var info in contact_info)
        {
870
          if (info.field_name == "") {}
871 872
          else if (info.field_name == "bday")
            {
873
              new_birthday_str = info.field_value[0] ?? "";
874
            }
875 876 877 878
          else if (info.field_name == "email")
            {
              foreach (var email_addr in info.field_value)
                {
879 880 881 882 883 884
                  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);
                    }
885 886
                }
            }
887 888 889
          else if (info.field_name == "fn")
            {
              new_full_name = info.field_value[0];
890 891
              if (new_full_name == null)
                new_full_name = "";
892 893
            }
          else if (info.field_name == "tel")
894
            {
895 896
              foreach (var phone_num in info.field_value)
                {
897 898 899 900 901 902
                  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);
                    }
903
                }
904
            }
905 906 907 908
          else if (info.field_name == "url")
            {
              foreach (var url in info.field_value)
                {
909 910 911 912 913 914
                  if (url != "")
                    {
                      var parameters = this._afd_params_from_strv (info.parameters);
                      var url_fd = new UrlFieldDetails (url, parameters);
                      new_urls.add (url_fd);
                    }
915 916
                }
            }
917 918
        }

919 920 921
      if (new_birthday_str != "")
        {
          var timeval = TimeVal ();
922
          if (timeval.from_iso8601 (new_birthday_str))
923
            {
924 925 926 927 928 929 930
              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");
931
                  changed = true;
932 933 934 935
                }
            }
          else
            {
936
              debug ("Failed to parse new birthday string '%s'",
937
                  new_birthday_str);
938 939 940 941 942 943 944 945
            }
        }
      else
        {
          if (this._birthday != null)
            {
              this._birthday = null;
              this.notify_property ("birthday");
946
              changed = true;