tpf-persona.vala 31.3 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 const string[] _always_writeable_properties =
45 46 47 48 49
    {
      "alias",
      "is-favourite",
      "groups"
    };
50
  private string[] _writeable_properties = null;
51

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

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

70 71
  private LoadableIcon? _avatar = null;

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

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

98 99
  private string _full_name = ""; /* must never be null */

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

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

  /**
   * {@inheritDoc}
   *
Travis Reitter's avatar
Travis Reitter committed
190
   * @since 0.6.4
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 222 223 224 225
   */
  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 */
    }

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

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

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

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

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

          return this._always_writeable_properties;
        }
273 274
    }

275 276
  private string _alias = ""; /* must never be null */

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

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

301 302 303
      if (this._is_constructed)
        {
          yield ((Tpf.PersonaStore) this.store).change_alias (this, alias);
304
        }
305 306 307

      this._alias = alias;
      this.notify_property ("alias");
308 309
    }

310 311
  private bool _is_favourite = false;

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

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

336 337 338 339
      if (this._is_constructed)
        {
          yield ((Tpf.PersonaStore) this.store).change_is_favourite (this,
              is_favourite);
Philip Withnall's avatar
Philip Withnall committed
340
        }
341 342 343

      this._is_favourite = is_favourite;
      this.notify_property ("is-favourite");
Philip Withnall's avatar
Philip Withnall committed
344 345
    }

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

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

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

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

393 394 395
  private HashSet<string> _groups = new HashSet<string> ();
  private Set<string> _groups_ro;

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

408
  /**
409 410
   * Add or remove the Persona from the specified group.
   *
411
   * See {@link Folks.GroupDetails.change_group}.
412
   */
413
  public async void change_group (string group, bool is_member)
414
    {
415
      if (this._change_group (group, is_member))
416
        {
417
          Tpf.PersonaStore store = (Tpf.PersonaStore) this.store;
418
          yield store._change_group_membership (this, group, is_member);
419 420 421 422
        }
    }

  private bool _change_group (string group, bool is_member)
423
    {
424
      var changed = false;
425 426 427

      if (is_member)
        {
428
          changed = this._groups.add (group);
429 430
        }
      else
431 432 433
        {
          changed = this._groups.remove (group);
        }
434

435 436 437
      if (changed == true)
        this.group_changed (group, is_member);

438
      return changed;
439 440
    }

441 442 443
  /**
   * {@inheritDoc}
   *
Raul Gutierrez Segales's avatar
Raul Gutierrez Segales committed
444
   * @since 0.6.2
445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464
   */
  public async void change_groups (Set<string> groups) throws PropertyError
    {
      Tpf.PersonaStore store = (Tpf.PersonaStore) this.store;

      foreach (var group1 in groups)
        {
          if (this._groups.contains (group1) == false)
            yield store._change_group_membership (this, group1, true);
        }

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

      this.notify_property ("groups");
    }

465 466 467 468 469 470
  /* 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)
    {
471 472
      debug ("TpContact %p destroyed; setting ._contact = null in Persona %p",
          obj, this);
473 474 475 476
      this._contact = null;
      this.notify_property ("contact");
    }

477 478
  /**
   * The Telepathy contact represented by this persona.
479 480 481 482 483 484
   *
   * 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).
485
   */
486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507
  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;
        }
    }
508

509 510 511 512
  private HashSet<PhoneFieldDetails> _phone_numbers =
      new HashSet<PhoneFieldDetails> (
          (GLib.HashFunc) PhoneFieldDetails.hash,
          (GLib.EqualFunc) PhoneFieldDetails.equal);
513 514 515 516 517
  private Set<PhoneFieldDetails> _phone_numbers_ro;

  /**
   * {@inheritDoc}
   *
Travis Reitter's avatar
Travis Reitter committed
518
   * @since 0.6.4
519 520 521 522 523 524 525 526
   */
  [CCode (notify = false)]
  public Set<PhoneFieldDetails> phone_numbers
    {
      get { return this._phone_numbers_ro; }
      set { this.change_phone_numbers.begin (value); }
    }

527 528 529
  /**
   * {@inheritDoc}
   *
Travis Reitter's avatar
Travis Reitter committed
530
   * @since 0.6.4
531 532 533 534 535 536 537 538
   */
  public async void change_phone_numbers (
      Set<PhoneFieldDetails> phone_numbers) throws PropertyError
    {
      yield this._change_details<PhoneFieldDetails> (phone_numbers,
          this._phone_numbers, "tel");
    }

539 540 541
  private HashSet<UrlFieldDetails> _urls = new HashSet<UrlFieldDetails> (
      (GLib.HashFunc) UrlFieldDetails.hash,
      (GLib.EqualFunc) UrlFieldDetails.equal);
542 543 544 545 546
  private Set<UrlFieldDetails> _urls_ro;

  /**
   * {@inheritDoc}
   *
Travis Reitter's avatar
Travis Reitter committed
547
   * @since 0.6.4
548 549 550 551 552 553 554 555 556 557 558
   */
  [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
559
   * @since 0.6.4
560 561 562 563 564 565 566
   */
  public async void change_urls (Set<UrlFieldDetails> urls) throws PropertyError
    {
      yield this._change_details<UrlFieldDetails> (urls,
          this._urls, "url");
    }

567 568 569 570 571 572 573 574
  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;

575
      if (Folks.Internal.equal_sets<T> (details, member_set))
576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603
        {
          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 */
    }

604 605 606 607 608
  /**
   * Create a new persona.
   *
   * Create a new persona for the {@link PersonaStore} `store`, representing
   * the Telepathy contact given by `contact`.
609 610 611
   *
   * @param contact the Telepathy contact being represented by the persona
   * @param store the persona store to place the persona in
612
   */
613
  public Persona (Contact contact, PersonaStore store)
614
    {
615
      unowned string id = contact.get_identifier ();
616
      var connection = contact.connection;
617
      var account = connection.get_account ();
618
      var uid = this.build_uid (store.type_id, store.id, id);
619

620
      Object (contact: contact,
621
              display_id: id,
Travis Reitter's avatar
Travis Reitter committed
622
              /* FIXME: This IID format should be moved out to the ImDetails
623 624 625 626
               * 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
627
              uid: uid,
628
              store: store,
629
              is_user: contact.handle == connection.self_handle);
630

631 632 633 634 635 636 637 638 639 640 641 642 643 644 645 646 647
      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;
        }
648

649 650
      /* Set our alias. */
      this._alias = this.contact.get_alias ();
651

652
      this.contact.notify["alias"].connect ((s, p) =>
653
          {
654 655 656
            /* Tp guarantees that aliases are always non-null. */
            assert (this.contact.alias != null);

657
            if (this._alias != this.contact.alias)
658
              {
659
                this._alias = this.contact.alias;
660 661 662 663
                this.notify_property ("alias");
              }
          });

664
      /* Set our single IM address */
665
      var connection = this.contact.connection;
666
      var account = connection.get_account ();
667

668 669
      try
        {
670
          var im_addr = ImDetails.normalise_im_address (this.display_id,
671 672 673
              account.get_protocol ());
          var im_fd = new ImFieldDetails (im_addr);
          this._im_addresses.set (account.get_protocol (), im_fd);
674
        }
Travis Reitter's avatar
Travis Reitter committed
675
      catch (ImDetailsError e)
676 677 678 679
        {
          /* This should never happen…but if it does, warn of it and continue */
          warning (e.message);
        }
680

681
      this.contact.notify["avatar-file"].connect ((s, p) =>
682
        {
683
          this._contact_notify_avatar ();
684
        });
685
      this._contact_notify_avatar ();
686

687
      this.contact.notify["presence-message"].connect ((s, p) =>
688
        {
689
          this._contact_notify_presence_message ();
690
        });
691
      this.contact.notify["presence-type"].connect ((s, p) =>
692
        {
693
          this._contact_notify_presence_type ();
694
        });
695
      this.contact.notify["presence-status"].connect ((s, p) =>
696 697 698
        {
          this._contact_notify_presence_status ();
        });
699 700
      this._contact_notify_presence_message ();
      this._contact_notify_presence_type ();
701
      this._contact_notify_presence_status ();
702

703
      this.contact.notify["contact-info"].connect ((s, p) =>
704
        {
705
          this._contact_notify_contact_info ();
706
        });
707
      this._contact_notify_contact_info ();
708

709 710 711 712 713
      ((Tpf.PersonaStore) this.store).group_members_changed.connect (
          (s, group, added, removed) =>
            {
              if (added.find (this) != null)
                this._change_group (group, true);
714

715 716 717
              if (removed.find (this) != null)
                this._change_group (group, false);
            });
718

719 720 721
      ((Tpf.PersonaStore) this.store).group_removed.connect (
          (s, group, error) =>
            {
722 723 724 725 726 727 728
              /* FIXME: Can't use
               * !(error is TelepathyGLib.DBusError.OBJECT_REMOVED) because the
               * GIR bindings don't annotate errors */
              if (error != null &&
                  (error.domain != TelepathyGLib.dbus_errors_quark () ||
                   error.code != TelepathyGLib.DBusError.OBJECT_REMOVED))
                {
729
                  debug ("Group invalidated: %s", error.message);
730
                  this._change_group (group, false);
731
                }
732
            });
733 734 735 736 737 738 739 740 741 742 743 744

      if (this.is_user)
        {
          ((Tpf.PersonaStore) this.store).notify["supported-fields"].connect (
            (s, p) =>
              {
                this._store_notify_supported_fields ();
              });
          this._store_notify_supported_fields ();
        }
    }

745 746 747 748 749 750
  /* Called after all construction-time properties have been set. */
  public override void constructed ()
    {
      this._is_constructed = true;
    }

751 752 753 754 755
  private void _store_notify_supported_fields ()
    {
      var tpf_store = this.store as Tpf.PersonaStore;
      this._writeable_properties = this._always_writeable_properties;

756 757
      if ("bday" in tpf_store.supported_fields)
        this._writeable_properties += "birthday";
758 759
      if ("email" in tpf_store.supported_fields)
        this._writeable_properties += "email-addresses";
760 761 762 763
      if ("fn" in tpf_store.supported_fields)
        this._writeable_properties += "full-name";
      if ("tel" in tpf_store.supported_fields)
        this._writeable_properties += "phone-numbers";
764 765
      if ("url" in tpf_store.supported_fields)
        this._writeable_properties += "urls";
766 767
    }

768
  private void _contact_notify_contact_info ()
769
    {
770
      var new_birthday_str = "";
771
      var new_full_name = "";
772 773 774
      var new_email_addresses = new HashSet<EmailFieldDetails> (
          (GLib.HashFunc) EmailFieldDetails.hash,
          (GLib.EqualFunc) EmailFieldDetails.equal);
775 776 777
      var new_phone_numbers = new HashSet<PhoneFieldDetails> (
          (GLib.HashFunc) PhoneFieldDetails.hash,
          (GLib.EqualFunc) PhoneFieldDetails.equal);
778 779 780
      var new_urls = new HashSet<UrlFieldDetails> (
          (GLib.HashFunc) UrlFieldDetails.hash,
          (GLib.EqualFunc) UrlFieldDetails.equal);
781 782 783 784

      var contact_info = this.contact.get_contact_info ();
      foreach (var info in contact_info)
        {
785
          if (info.field_name == "") {}
786 787
          else if (info.field_name == "bday")
            {
788
              new_birthday_str = info.field_value[0] ?? "";
789
            }
790 791 792 793 794 795 796 797 798
          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);
                }
            }
799 800 801
          else if (info.field_name == "fn")
            {
              new_full_name = info.field_value[0];
802 803
              if (new_full_name == null)
                new_full_name = "";
804 805
            }
          else if (info.field_name == "tel")
806
            {
807 808 809 810 811 812
              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);
                }
813
            }
814 815 816 817 818 819 820 821 822
          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);
                }
            }
823 824
        }

825 826 827
      if (new_birthday_str != "")
        {
          var timeval = TimeVal ();
828
          if (timeval.from_iso8601 (new_birthday_str))
829
            {
830 831 832 833 834 835 836 837 838 839 840 841 842
              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);
843 844 845 846 847 848 849 850 851 852 853
            }
        }
      else
        {
          if (this._birthday != null)
            {
              this._birthday = null;
              this.notify_property ("birthday");
            }
        }

854 855 856 857 858 859 860 861
      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");
        }

862 863 864 865 866 867
      if (new_full_name != this._full_name)
        {
          this._full_name = new_full_name;
          this.notify_property ("full-name");
        }

868
      if (!Folks.Internal.equal_sets<PhoneFieldDetails> (new_phone_numbers,
869 870 871 872 873 874
              this._phone_numbers))
        {
          this._phone_numbers = new_phone_numbers;
          this._phone_numbers_ro = new_phone_numbers.read_only_view;
          this.notify_property ("phone-numbers");
        }
875 876 877 878 879 880 881

      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");
        }
882 883 884 885 886 887 888 889 890 891 892 893 894 895 896 897 898 899 900 901 902 903 904
    }

  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;
    }

905 906 907 908 909 910 911 912 913 914 915 916 917 918 919 920 921 922
  /**
   * 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.
923 924 925 926 927 928 929 930 931 932
   * @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`).
933 934
   * @return A new {@link Tpf.Persona} representing the cached persona.
   *
Travis Reitter's avatar
Travis Reitter committed
935
   * @since 0.6.0
936 937 938 939
   */
  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,
940 941 942
      LoadableIcon? avatar, DateTime? birthday, string full_name,
      HashSet<EmailFieldDetails> email_addresses,
      HashSet<PhoneFieldDetails> phone_numbers, HashSet<UrlFieldDetails> urls)
943 944 945 946 947 948 949 950
    {
      Object (contact: null,
              display_id: im_address,
              iid: iid,
              uid: uid,
              store: store,
              is_user: is_user);

951
      debug ("Created new Tpf.Persona '%s' from cache: %p", uid, this);
952 953

      // IM addresses
954 955
      var im_fd = new ImFieldDetails (im_address);
      this._im_addresses.set (protocol, im_fd);
956 957 958 959 960

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

961 962 963 964 965 966 967 968 969 970 971 972
      // 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;

973
      // Other properties
974 975 976 977 978 979
      if (alias == null)
        {
          /* Deal with badly-behaved callers */
          alias = "";
        }

980 981 982 983 984 985
      if (full_name == null)
        {
          /* Deal with badly-behaved callers */
          full_name = "";
        }

986 987 988
      this._alias = alias;
      this._is_favourite = is_favourite;
      this.is_in_contact_list = is_in_contact_list;
989
      this._avatar = avatar;
990 991
      this._birthday = birthday;
      this._full_name = full_name;
992 993 994 995 996 997

      // Make the persona appear offline
      this.presence_type = PresenceType.OFFLINE;
      this.presence_message = "";
    }

998 999 1000
  ~Persona ()
    {
      debug ("Destroying Tpf.Persona '%s': %p", this.uid, this);
1001 1002 1003 1004 1005

      if (this._contact != null)
        {
          this._contact.weak_unref (this._contact_weak_notify_cb);
        }
1006 1007
    }

1008
  private void _contact_notify_presence_message ()
1009
    {
1010
      this.presence_message = this.contact.get_presence_message ();