tpf-persona.vala 31.9 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

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

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

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

64 65
  private LoadableIcon? _avatar = null;

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

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

92 93
  private string _full_name = ""; /* must never be null */

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

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

  /**
   * {@inheritDoc}
   *
Travis Reitter's avatar
Travis Reitter committed
184
   * @since 0.6.4
185 186 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
   */
  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 */
    }

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

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

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

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

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

263 264
  private string _alias = ""; /* must never be null */

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

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

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

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

297 298
  private bool _is_favourite = false;

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

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

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

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

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

340 341 342 343
      this._is_favourite = is_favourite;
      this.notify_property ("is-favourite");
    }

344 345 346 347
  private HashSet<EmailFieldDetails> _email_addresses =
      new HashSet<EmailFieldDetails> (
          (GLib.HashFunc) EmailFieldDetails.hash,
          (GLib.EqualFunc) EmailFieldDetails.equal);
348 349 350 351 352
  private Set<EmailFieldDetails> _email_addresses_ro;

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

374 375 376 377 378
  private HashMultiMap<string, ImFieldDetails> _im_addresses =
      new HashMultiMap<string, ImFieldDetails> (null, null,
          (GLib.HashFunc) ImFieldDetails.hash,
          (GLib.EqualFunc) ImFieldDetails.equal);

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

391 392 393
  private HashSet<string> _groups = new HashSet<string> ();
  private Set<string> _groups_ro;

394
  /**
395 396
   * A mapping of group ID to whether the contact is a member.
   *
397
   * See {@link Folks.GroupDetails.groups}.
398
   */
399
  [CCode (notify = false)]
400
  public Set<string> groups
401
    {
402
      get { return this._groups_ro; }
403
      set { this.change_groups.begin (value); }
404 405
    }

406
  /**
407 408
   * Add or remove the Persona from the specified group.
   *
409
   * See {@link Folks.GroupDetails.change_group}.
410
   */
411
  public async void change_group (string group, bool is_member)
412
      throws GLib.Error
413
    {
414
      if (this._groups.contains (group) != is_member)
415
        {
416
          yield this._change_group_membership (group, is_member);
417
        }
418 419

      /* The change will be notified when we receive changes from the store. */
420 421
    }

422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444
  private async void _change_group_membership (string group,
      bool is_member) throws Folks.PropertyError
    {
      try
        {
          if (is_member)
            {
              yield this.contact.add_to_group_async (group);
            }
          else
            {
              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);
        }
    }

  /* Note: Only ever called as a result of signals from Telepathy. */
445
  private bool _change_group (string group, bool is_member)
446
    {
447
      var changed = false;
448 449 450

      if (is_member)
        {
451
          changed = this._groups.add (group);
452 453
        }
      else
454 455 456
        {
          changed = this._groups.remove (group);
        }
457

458
      if (changed == true)
459 460 461 462
        {
          this.group_changed (group, is_member);
          this.notify_property ("groups");
        }
463

464
      return changed;
465 466
    }

467 468 469
  private void _contact_groups_changed (string[] added, string[] removed)
    {
      foreach (var group in added)
470 471 472
        {
          this._change_group (group, true);
        }
473 474

      foreach (var group in removed)
475 476 477
        {
          this._change_group (group, false);
        }
478 479
    }

480 481 482
  /**
   * {@inheritDoc}
   *
Raul Gutierrez Segales's avatar
Raul Gutierrez Segales committed
483
   * @since 0.6.2
484 485 486 487 488 489
   */
  public async void change_groups (Set<string> groups) throws PropertyError
    {
      foreach (var group1 in groups)
        {
          if (this._groups.contains (group1) == false)
490
            yield this._change_group_membership (group1, true);
491 492 493 494 495
        }

      foreach (var group2 in this._groups)
        {
          if (groups.contains (group2) == false)
496
            yield this._change_group_membership (group2, false);
497 498
        }

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

502 503 504 505 506 507
  /* 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)
    {
508 509
      debug ("TpContact %p destroyed; setting ._contact = null in Persona %p",
          obj, this);
510 511 512 513
      this._contact = null;
      this.notify_property ("contact");
    }

514 515
  /**
   * The Telepathy contact represented by this persona.
516 517 518 519 520 521
   *
   * 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).
522
   */
523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544
  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;
        }
    }
545

546 547 548 549
  private HashSet<PhoneFieldDetails> _phone_numbers =
      new HashSet<PhoneFieldDetails> (
          (GLib.HashFunc) PhoneFieldDetails.hash,
          (GLib.EqualFunc) PhoneFieldDetails.equal);
550 551 552 553 554
  private Set<PhoneFieldDetails> _phone_numbers_ro;

  /**
   * {@inheritDoc}
   *
Travis Reitter's avatar
Travis Reitter committed
555
   * @since 0.6.4
556 557 558 559 560 561 562 563
   */
  [CCode (notify = false)]
  public Set<PhoneFieldDetails> phone_numbers
    {
      get { return this._phone_numbers_ro; }
      set { this.change_phone_numbers.begin (value); }
    }

564 565 566
  /**
   * {@inheritDoc}
   *
Travis Reitter's avatar
Travis Reitter committed
567
   * @since 0.6.4
568 569 570 571 572 573 574 575
   */
  public async void change_phone_numbers (
      Set<PhoneFieldDetails> phone_numbers) throws PropertyError
    {
      yield this._change_details<PhoneFieldDetails> (phone_numbers,
          this._phone_numbers, "tel");
    }

576 577 578
  private HashSet<UrlFieldDetails> _urls = new HashSet<UrlFieldDetails> (
      (GLib.HashFunc) UrlFieldDetails.hash,
      (GLib.EqualFunc) UrlFieldDetails.equal);
579 580 581 582 583
  private Set<UrlFieldDetails> _urls_ro;

  /**
   * {@inheritDoc}
   *
Travis Reitter's avatar
Travis Reitter committed
584
   * @since 0.6.4
585 586 587 588 589 590 591 592 593 594 595
   */
  [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
596
   * @since 0.6.4
597 598 599 600 601 602 603
   */
  public async void change_urls (Set<UrlFieldDetails> urls) throws PropertyError
    {
      yield this._change_details<UrlFieldDetails> (urls,
          this._urls, "url");
    }

604 605 606 607 608 609 610 611
  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;

612
      if (Folks.Internal.equal_sets<T> (details, member_set))
613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635 636 637 638 639 640
        {
          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 */
    }

641 642 643 644 645
  /**
   * Create a new persona.
   *
   * Create a new persona for the {@link PersonaStore} `store`, representing
   * the Telepathy contact given by `contact`.
646 647 648
   *
   * @param contact the Telepathy contact being represented by the persona
   * @param store the persona store to place the persona in
649
   */
650
  public Persona (Contact contact, PersonaStore store)
651
    {
652
      unowned string id = contact.get_identifier ();
653
      var connection = contact.connection;
654
      var account = connection.get_account ();
655
      var uid = this.build_uid (store.type_id, store.id, id);
656

657
      Object (contact: contact,
658
              display_id: id,
Travis Reitter's avatar
Travis Reitter committed
659
              /* FIXME: This IID format should be moved out to the ImDetails
660 661 662 663
               * 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
664
              uid: uid,
665
              store: store,
666
              is_user: contact.handle == connection.self_handle);
667

668 669 670 671 672 673 674 675 676 677 678 679 680 681 682 683 684
      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;
        }
685

686 687
      /* Set our alias. */
      this._alias = this.contact.get_alias ();
688

689
      this.contact.notify["alias"].connect ((s, p) =>
690
          {
691 692 693
            /* Tp guarantees that aliases are always non-null. */
            assert (this.contact.alias != null);

694
            if (this._alias != this.contact.alias)
695
              {
696
                this._alias = this.contact.alias;
697 698 699 700
                this.notify_property ("alias");
              }
          });

701
      /* Set our single IM address */
702
      var connection = this.contact.connection;
703
      var account = connection.get_account ();
704

705 706
      try
        {
707
          var im_addr = ImDetails.normalise_im_address (this.display_id,
708 709 710
              account.get_protocol ());
          var im_fd = new ImFieldDetails (im_addr);
          this._im_addresses.set (account.get_protocol (), im_fd);
711
        }
Travis Reitter's avatar
Travis Reitter committed
712
      catch (ImDetailsError e)
713 714 715 716
        {
          /* This should never happen…but if it does, warn of it and continue */
          warning (e.message);
        }
717

718
      this.contact.notify["avatar-file"].connect ((s, p) =>
719
        {
720
          this._contact_notify_avatar ();
721
        });
722
      this._contact_notify_avatar ();
723

724
      this.contact.notify["presence-message"].connect ((s, p) =>
725
        {
726
          this._contact_notify_presence_message ();
727
        });
728
      this.contact.notify["presence-type"].connect ((s, p) =>
729
        {
730
          this._contact_notify_presence_type ();
731
        });
732
      this.contact.notify["presence-status"].connect ((s, p) =>
733 734 735
        {
          this._contact_notify_presence_status ();
        });
736 737
      this._contact_notify_presence_message ();
      this._contact_notify_presence_type ();
738
      this._contact_notify_presence_status ();
739

740
      this.contact.notify["contact-info"].connect ((s, p) =>
741
        {
742
          this._contact_notify_contact_info ();
743
        });
744
      this._contact_notify_contact_info ();
745

746 747 748 749 750
      this.contact.contact_groups_changed.connect ((added, removed) =>
        {
          this._contact_groups_changed (added, removed);
        });
      this._contact_groups_changed (this.contact.get_contact_groups (), {});
751

752 753
      var tpf_store = this.store as Tpf.PersonaStore;

754 755
      if (this.is_user)
        {
756 757 758 759
          tpf_store.notify["supported-fields"].connect ((s, p) =>
            {
              this._update_writeable_properties ();
            });
760
        }
761 762 763 764 765 766 767

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

      this._update_writeable_properties ();
768 769
    }

770 771 772 773 774 775
  /* Called after all construction-time properties have been set. */
  public override void constructed ()
    {
      this._is_constructed = true;
    }

776
  private void _update_writeable_properties ()
777 778
    {
      var tpf_store = this.store as Tpf.PersonaStore;
779 780 781 782 783 784 785 786 787 788 789 790 791 792 793
      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";
        }
794 795
    }

796
  private void _contact_notify_contact_info ()
797
    {
798
      var new_birthday_str = "";
799
      var new_full_name = "";
800 801 802
      var new_email_addresses = new HashSet<EmailFieldDetails> (
          (GLib.HashFunc) EmailFieldDetails.hash,
          (GLib.EqualFunc) EmailFieldDetails.equal);
803 804 805
      var new_phone_numbers = new HashSet<PhoneFieldDetails> (
          (GLib.HashFunc) PhoneFieldDetails.hash,
          (GLib.EqualFunc) PhoneFieldDetails.equal);
806 807 808
      var new_urls = new HashSet<UrlFieldDetails> (
          (GLib.HashFunc) UrlFieldDetails.hash,
          (GLib.EqualFunc) UrlFieldDetails.equal);
809 810 811 812

      var contact_info = this.contact.get_contact_info ();
      foreach (var info in contact_info)
        {
813
          if (info.field_name == "") {}
814 815
          else if (info.field_name == "bday")
            {
816
              new_birthday_str = info.field_value[0] ?? "";
817
            }
818 819 820 821 822 823 824 825 826
          else if (info.field_name == "email")
            {
              foreach (var email_addr in info.field_value)
                {
                  var parameters = this._afd_params_from_strv (info.parameters);
                  var email_fd = new EmailFieldDetails (email_addr, parameters);
                  new_email_addresses.add (email_fd);
                }
            }
827 828 829
          else if (info.field_name == "fn")
            {
              new_full_name = info.field_value[0];
830 831
              if (new_full_name == null)
                new_full_name = "";
832 833
            }
          else if (info.field_name == "tel")
834
            {
835 836 837 838 839 840
              foreach (var phone_num in info.field_value)
                {
                  var parameters = this._afd_params_from_strv (info.parameters);
                  var phone_fd = new PhoneFieldDetails (phone_num, parameters);
                  new_phone_numbers.add (phone_fd);
                }
841
            }
842 843 844 845 846 847 848 849 850
          else if (info.field_name == "url")
            {
              foreach (var url in info.field_value)
                {
                  var parameters = this._afd_params_from_strv (info.parameters);
                  var url_fd = new UrlFieldDetails (url, parameters);
                  new_urls.add (url_fd);
                }
            }
851 852
        }

853 854 855
      if (new_birthday_str != "")
        {
          var timeval = TimeVal ();
856
          if (timeval.from_iso8601 (new_birthday_str))
857
            {
858 859 860 861 862 863 864 865 866 867 868 869 870
              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");
                }
            }
          else
            {
              warning ("Failed to parse new birthday string '%s'",
                  new_birthday_str);
871 872 873 874 875 876 877 878 879 880 881
            }
        }
      else
        {
          if (this._birthday != null)
            {
              this._birthday = null;
              this.notify_property ("birthday");
            }
        }

882 883 884 885 886 887 888 889
      if (!Folks.Internal.equal_sets<EmailFieldDetails> (new_email_addresses,
              this._email_addresses))
        {
          this._email_addresses = new_email_addresses;
          this._email_addresses_ro = new_email_addresses.read_only_view;
          this.notify_property ("email-addresses");
        }

890 891 892 893 894 895
      if (new_full_name != this._full_name)
        {
          this._full_name = new_full_name;
          this.notify_property ("full-name");
        }

896
      if (!Folks.Internal.equal_sets<PhoneFieldDetails> (new_phone_numbers,
897 898 899 900 901 902
              this._phone_numbers))
        {
          this._phone_numbers = new_phone_numbers;
          this._phone_numbers_ro = new_phone_numbers.read_only_view;
          this.notify_property ("phone-numbers");
        }
903 904 905 906 907 908 909

      if (!Folks.Internal.equal_sets<UrlFieldDetails> (new_urls, this._urls))
        {
          this._urls = new_urls;
          this._urls_ro = new_urls.read_only_view;
          this.notify_property ("urls");
        }
910 911 912 913 914 915 916 917 918 919 920 921 922 923 924 925 926 927 928 929 930 931 932
    }

  private MultiMap<string, string> _afd_params_from_strv (string[] parameters)
    {
      var retval = new HashMultiMap<string, string> ();

      foreach (var entry in parameters)
        {
          var tokens = entry.split ("=", 2);
          if (tokens.length == 2)
            {
              retval.set (tokens[0], tokens[1]);
            }
          else
            {
              warning ("Failed to parse vCard parameter from string '%s'",
                  entry);
            }
        }

      return retval;
    }

933 934 935 936 937 938 939 940 941 942 943 944 945 946 947 948 949 950
  /**
   * Create a new persona for the {@link PersonaStore} `store`, representing
   * a cached contact for which we currently have no Telepathy contact.
   *
   * @param store The persona store to place the persona in.
   * @param uid The cached UID of the persona.
   * @param iid The cached IID of the persona.
   * @param im_address The cached IM address of the persona (excluding
   * protocol).
   * @param protocol The cached protocol of the persona.
   * @param groups The cached set of groups the persona is in.
   * @param is_favourite Whether the persona is a favourite.
   * @param alias The cached alias for the persona.
   * @param is_in_contact_list Whether the persona is in the user's contact
   * list.
   * @param is_user Whether the persona is the user.
   * @param avatar The icon for the persona's cached avatar, or `null` if they
   * have no avatar.
951 952 953 954 955 956 957 958 959 960
   * @param birthday The date/time of birth of the persona, or `null` if it's
   * unknown.
   * @param full_name The persona's full name, or the empty string if it's
   * unknown.
   * @param email_addresses A set of the persona's e-mail addresses, which may
   * be empty (but may not be `null`).
   * @param phone_numbers A set of the persona's phone numbers, which may be
   * empty (but may not be `null`).
   * @param urls A set of the persona's URLs, which may be empty (but may not be
   * `null`).
961 962
   * @return A new {@link Tpf.Persona} representing the cached persona.
   *
Travis Reitter's avatar
Travis Reitter committed
963
   * @since 0.6.0
964 965 966 967
   */
  internal Persona.from_cache (PersonaStore store, string uid, string iid,
      string im_address, string protocol, HashSet<string> groups,
      bool is_favourite, string alias, bool is_in_contact_list, bool is_user,
968 969 970
      LoadableIcon? avatar, DateTime? birthday, string full_name,
      HashSet<EmailFieldDetails> email_addresses,
      HashSet<PhoneFieldDetails> phone_numbers, HashSet<UrlFieldDetails> urls)
971 972 973 974 975 976 977 978
    {
      Object (contact: null,
              display_id: im_address,
              iid: iid,
              uid: uid,
              store: store,
              is_user: is_user);

979
      debug ("Created new Tpf.Persona '%s' from cache: %p", uid, this);
980 981

      // IM addresses
982 983
      var im_fd = new ImFieldDetails (im_address);
      this._im_addresses.set (protocol, im_fd);
984 985 986 987 988

      // Groups
      this._groups = groups;
      this._groups_ro = this._groups.read_only_view;

989 990 991 992 993 994 995 996 997 998 999 1000
      // E-mail addresses
      this._email_addresses = email_addresses;
      this._email_addresses_ro = this._email_addresses.read_only_view;

      // Phone numbers
      this._phone_numbers = phone_numbers;
      this._phone_numbers_ro = this._phone_numbers.read_only_view;

      // URLs
      this._urls = urls;
      this._urls_ro = this._urls.read_only_view;

1001
      // Other properties
1002 1003 1004 1005 1006 1007
      if (alias == null)
        {
          /* Deal with badly-behaved callers */
          alias = "";
        }