# HG changeset patch
# Node ID 036516628336
# Parent  74985c7b824d
diff --git a/libpurple/meson.build b/libpurple/meson.build
--- a/libpurple/meson.build
+++ b/libpurple/meson.build
@@ -68,6 +68,7 @@
 	'purplenotificationmanager.c',
 	'purpleoptions.c',
 	'purplepath.c',
+	'purpleperson.c',
 	'purpleplugininfo.c',
 	'purplepresence.c',
 	'purpleprotocol.c',
@@ -172,6 +173,7 @@
 	'purplenotificationmanager.h',
 	'purpleoptions.h',
 	'purplepath.h',
+	'purpleperson.h',
 	'purpleplugininfo.h',
 	'purplepresence.h',
 	'purpleprotocol.h',
diff --git a/libpurple/purpleperson.h b/libpurple/purpleperson.h
new file mode 100644
--- /dev/null
+++ b/libpurple/purpleperson.h
@@ -0,0 +1,185 @@
+/*
+ * Purple - Internet Messaging Library
+ * Copyright (C) Pidgin Developers <devel@pidgin.im>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program 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 General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, see <https://www.gnu.org/licenses/>.
+ */
+
+#if !defined(PURPLE_GLOBAL_HEADER_INSIDE) && !defined(PURPLE_COMPILATION)
+# error "only <pidgin.h> may be included directly"
+#endif
+
+#ifndef PURPLE_PERSON_H
+#define PURPLE_PERSON_H
+
+#include <glib.h>
+#include <glib-object.h>
+
+#include <gdk-pixbuf/gdk-pixbuf.h>
+
+#include <libpurple/purplecontact.h>
+#include <libpurple/purpletags.h>
+
+G_BEGIN_DECLS
+
+#define PURPLE_TYPE_PERSON (purple_person_get_type())
+G_DECLARE_FINAL_TYPE(PurplePerson, purple_person, PURPLE, PERSON, GObject)
+
+/**
+ * PurplePerson:
+ *
+ * A collection of [class@Purple.Contact] that contains a user selectable custom
+ * avatar and alias.
+ *
+ * Since: 3.0.0
+ */
+
+/**
+ * purple_person_new:
+ *
+ * Creates a new [class@Purple.Person].
+ *
+ * Returns: (transfer full): The new instance.
+ *
+ * Since: 3.0.0
+ */
+PurplePerson *purple_person_new(void);
+
+/**
+ * purple_person_get_id:
+ * @person: The instance.
+ *
+ * Gets the id of @person. This is created internally and is read-only.
+ *
+ * Returns: The id of @person.
+ *
+ * Since: 3.0.0
+ */
+const gchar *purple_person_get_id(PurplePerson *person);
+
+/**
+ * purple_person_get_alias:
+ * @person: The instance.
+ *
+ * Gets the alias of @person that was set by the libpurple user.
+ *
+ * This will be %NULL if no alias is set.
+ *
+ * Returns: (nullable): The alias of @person or %NULL.
+ *
+ * Since: 3.0.0
+ */
+const gchar *purple_person_get_alias(PurplePerson *person);
+
+/**
+ * purple_person_set_alias:
+ * @person: The instance.
+ * @alias: (nullable): The new alias.
+ *
+ * Sets the alias of @person to @alias.
+ *
+ * This should only be called in direct response to a user interaction to set a
+ * custom alias. Protocol plugins should only be setting the alias of
+ * [class@Purple.Contact].
+ *
+ * If @alias is %NULL, then the previous alias is cleared.
+ *
+ * Since: 3.0.0
+ */
+void purple_person_set_alias(PurplePerson *person, const gchar *alias);
+
+/**
+ * purple_person_get_avatar:
+ * @person: The instance.
+ *
+ * Gets the avatar of @person or %NULL if no avatar is set.
+ *
+ * Returns: (transfer none) (nullable): The avatar for @person.
+ *
+ * Since: 3.0.0
+ */
+GdkPixbuf *purple_person_get_avatar(PurplePerson *person);
+
+/**
+ * purple_person_set_avatar:
+ * @person: The instance.
+ * @avatar: (nullable): The new avatar.
+ *
+ * Sets the avatar of @person to @avatar. If @avatar is %NULL then the previous
+ * avatar is cleared.
+ *
+ * This should only be called in direct response to a user interaction to set a
+ * custom avatar. Protocol plugins should only be setting the avatars of
+ * [class@Purple.Contact].
+ *
+ * Since: 3.0.0
+ */
+void purple_person_set_avatar(PurplePerson *person, GdkPixbuf *avatar);
+
+/**
+ * purple_person_get_tags:
+ * @person: The instance.
+ *
+ * Gets the [class@Purple.Tags] instance for @person.
+ *
+ * Returns: (transfer none): The tags for @person.
+ *
+ * Since: 3.0.0
+ */
+PurpleTags *purple_person_get_tags(PurplePerson *person);
+
+/**
+ * purple_person_add_contact:
+ * @person: The instance.
+ * @contact: The contact to add.
+ *
+ * Adds @contact to @person.
+ *
+ * Duplicate contacts are currently allowed, but that may change at a later
+ * time.
+ *
+ * Since: 3.0.0
+ */
+void purple_person_add_contact(PurplePerson *person, PurpleContact *contact);
+
+/**
+ * purple_person_remove_contact:
+ * @person: The instance.
+ * @contact: The contact to remove.
+ *
+ * Removes @contact from @person.
+ *
+ * Returns: %TRUE if @contact was found and removed otherwise %FALSE.
+ *
+ * Since: 3.0.0
+ */
+gboolean purple_person_remove_contact(PurplePerson *person, PurpleContact *contact);
+
+/**
+ * purple_person_get_priority_contact:
+ * @person: The instance.
+ *
+ * Gets the priority contact for @person. A priority contact is the one that is
+ * the most available.
+ *
+ * Returns: (transfer none) (nullable): The priority contact or %NULL if
+ *          @person does not have any contacts.
+ *
+ * Since: 3.0.0
+ */
+PurpleContact *purple_person_get_priority_contact(PurplePerson *person);
+
+G_END_DECLS
+
+#endif /* PURPLE_PERSON_H */
diff --git a/libpurple/purpleperson.c b/libpurple/purpleperson.c
new file mode 100644
--- /dev/null
+++ b/libpurple/purpleperson.c
@@ -0,0 +1,460 @@
+/*
+ * Purple - Internet Messaging Library
+ * Copyright (C) Pidgin Developers <devel@pidgin.im>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program 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 General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, see <https://www.gnu.org/licenses/>.
+ */
+
+#include "purpleperson.h"
+
+struct _PurplePerson {
+	GObject parent;
+
+	gchar *id;
+
+	gchar *alias;
+	GdkPixbuf *avatar;
+	PurpleTags *tags;
+
+	GPtrArray *contacts;
+};
+
+enum {
+	PROP_0,
+	PROP_ID,
+	PROP_ALIAS,
+	PROP_AVATAR,
+	PROP_TAGS,
+	PROP_PRIORITY_CONTACT,
+	N_PROPERTIES
+};
+static GParamSpec *properties[N_PROPERTIES] = {NULL, };
+
+/******************************************************************************
+ * Helpers
+ *****************************************************************************/
+static void
+purple_person_set_id(PurplePerson *person, const gchar *id) {
+	g_return_if_fail(PURPLE_IS_PERSON(person));
+
+	g_free(person->id);
+
+	if(id != NULL) {
+		person->id = g_strdup(id);
+	} else {
+		person->id = g_uuid_string_random();
+	}
+
+	g_object_notify_by_pspec(G_OBJECT(person), properties[PROP_ID]);
+}
+
+static gint
+purple_person_contact_compare(gconstpointer a, gconstpointer b) {
+	PurpleContact *c1 = *(PurpleContact **)a;
+	PurpleContact *c2 = *(PurpleContact **)b;
+	PurplePresence *p1 = NULL;
+	PurplePresence *p2 = NULL;
+
+	p1 = purple_contact_get_presence(c1);
+	p2 = purple_contact_get_presence(c2);
+
+	return purple_presence_compare(p1, p2);
+}
+
+static void
+purple_person_sort_contacts(PurplePerson *person) {
+	PurpleContact *original_priority = NULL;
+	PurpleContact *new_priority = NULL;
+	guint n_items = person->contacts->len;
+
+	if(n_items <= 1) {
+		g_object_notify_by_pspec(G_OBJECT(person),
+		                         properties[PROP_PRIORITY_CONTACT]);
+
+		g_list_model_items_changed(G_LIST_MODEL(person), 0, n_items, n_items);
+
+		return;
+	}
+
+	original_priority = purple_person_get_priority_contact(person);
+
+	g_ptr_array_sort(person->contacts, purple_person_contact_compare);
+
+	/* Tell the list we update our stuff. */
+	g_list_model_items_changed(G_LIST_MODEL(person), 0, n_items, n_items);
+
+	/* See if the priority contact changed. */
+	new_priority = g_ptr_array_index(person->contacts, 0);
+	if(original_priority != new_priority) {
+		g_object_notify_by_pspec(G_OBJECT(person),
+		                         properties[PROP_PRIORITY_CONTACT]);
+	}
+}
+
+/******************************************************************************
+ * Callbacks
+ *****************************************************************************/
+static void
+purple_person_presence_notify_cb(G_GNUC_UNUSED GObject *obj,
+                                 G_GNUC_UNUSED GParamSpec *pspec,
+                                 gpointer data)
+{
+	purple_person_sort_contacts(data);
+}
+
+static void
+purple_person_contact_notify_cb(GObject *obj,
+                                G_GNUC_UNUSED GParamSpec *pspec,
+                                gpointer data)
+{
+	PurpleContact *contact = PURPLE_CONTACT(obj);
+
+	g_signal_connect_object(contact, "notify",
+	                        G_CALLBACK(purple_person_presence_notify_cb),
+	                        data, 0);
+}
+
+/******************************************************************************
+ * GListModel Implementation
+ *****************************************************************************/
+static GType
+purple_person_get_item_type(G_GNUC_UNUSED GListModel *list) {
+	return PURPLE_TYPE_CONTACT;
+}
+
+static guint
+purple_person_get_n_items(GListModel *list) {
+	PurplePerson *person = PURPLE_PERSON(list);
+
+	return person->contacts->len;
+}
+
+static gpointer
+purple_person_get_item(GListModel *list, guint position) {
+	PurplePerson *person = PURPLE_PERSON(list);
+	PurpleContact *contact = NULL;
+
+	if(position < person->contacts->len) {
+		contact = g_ptr_array_index(person->contacts, position);
+		g_object_ref(contact);
+	}
+
+	return contact;
+}
+
+static void
+purple_person_list_model_init(GListModelInterface *iface) {
+	iface->get_item_type = purple_person_get_item_type;
+	iface->get_n_items = purple_person_get_n_items;
+	iface->get_item = purple_person_get_item;
+}
+
+/******************************************************************************
+ * GObject Implementation
+ *****************************************************************************/
+G_DEFINE_TYPE_EXTENDED(PurplePerson, purple_person, G_TYPE_OBJECT, 0,
+                       G_IMPLEMENT_INTERFACE(G_TYPE_LIST_MODEL,
+                                             purple_person_list_model_init))
+
+static void
+purple_person_get_property(GObject *obj, guint param_id, GValue *value,
+                           GParamSpec *pspec)
+{
+	PurplePerson *person = PURPLE_PERSON(obj);
+
+	switch(param_id) {
+		case PROP_ID:
+			g_value_set_string(value, purple_person_get_id(person));
+			break;
+		case PROP_ALIAS:
+			g_value_set_string(value, purple_person_get_alias(person));
+			break;
+		case PROP_AVATAR:
+			g_value_set_object(value, purple_person_get_avatar(person));
+			break;
+		case PROP_TAGS:
+			g_value_set_object(value, purple_person_get_tags(person));
+			break;
+		case PROP_PRIORITY_CONTACT:
+			g_value_set_object(value,
+			                   purple_person_get_priority_contact(person));
+			break;
+		default:
+			G_OBJECT_WARN_INVALID_PROPERTY_ID(obj, param_id, pspec);
+			break;
+	}
+}
+
+static void
+purple_person_set_property(GObject *obj, guint param_id, const GValue *value,
+                            GParamSpec *pspec)
+{
+	PurplePerson *person = PURPLE_PERSON(obj);
+
+	switch(param_id) {
+		case PROP_ID:
+			purple_person_set_id(person, g_value_get_string(value));
+			break;
+		case PROP_ALIAS:
+			purple_person_set_alias(person, g_value_get_string(value));
+			break;
+		case PROP_AVATAR:
+			purple_person_set_avatar(person, g_value_get_object(value));
+			break;
+		default:
+			G_OBJECT_WARN_INVALID_PROPERTY_ID(obj, param_id, pspec);
+			break;
+	}
+}
+
+static void
+purple_person_dispose(GObject *obj) {
+	PurplePerson *person = PURPLE_PERSON(obj);
+
+	g_clear_object(&person->avatar);
+	g_clear_object(&person->tags);
+
+	if(person->contacts != NULL) {
+		g_ptr_array_free(person->contacts, TRUE);
+		person->contacts = NULL;
+	}
+
+	G_OBJECT_CLASS(purple_person_parent_class)->dispose(obj);
+}
+
+static void
+purple_person_finalize(GObject *obj) {
+	PurplePerson *person = PURPLE_PERSON(obj);
+
+	g_clear_pointer(&person->id, g_free);
+	g_clear_pointer(&person->alias, g_free);
+
+	G_OBJECT_CLASS(purple_person_parent_class)->finalize(obj);
+}
+
+static void
+purple_person_constructed(GObject *obj) {
+	PurplePerson *person = NULL;
+
+	G_OBJECT_CLASS(purple_person_parent_class)->constructed(obj);
+
+	person = PURPLE_PERSON(obj);
+	if(person->id == NULL) {
+		purple_person_set_id(person, NULL);
+	}
+}
+
+static void
+purple_person_init(PurplePerson *person) {
+	person->tags = purple_tags_new();
+	person->contacts = g_ptr_array_new_full(0, (GDestroyNotify)g_object_unref);
+}
+
+static void
+purple_person_class_init(PurplePersonClass *klass) {
+	GObjectClass *obj_class = G_OBJECT_CLASS(klass);
+
+	obj_class->get_property = purple_person_get_property;
+	obj_class->set_property = purple_person_set_property;
+	obj_class->constructed = purple_person_constructed;
+	obj_class->dispose = purple_person_dispose;
+	obj_class->finalize = purple_person_finalize;
+
+	/**
+	 * PurplePerson:id:
+	 *
+	 * The protocol specific id for the contact.
+	 *
+	 * Since: 3.0.0
+	 */
+	properties[PROP_ID] = g_param_spec_string(
+		"id", "id",
+		"The id of this contact",
+		NULL,
+		G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS);
+
+	/**
+	 * PurplePerson:alias:
+	 *
+	 * The alias for this person. This is controlled by the libpurple user.
+	 *
+	 * Since: 3.0.0
+	 */
+	properties[PROP_ALIAS] = g_param_spec_string(
+		"alias", "alias",
+		"The alias of this person.",
+		NULL,
+		G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS);
+
+	/**
+	 * PurplePerson:avatar:
+	 *
+	 * The avatar for this person. This is controlled by the libpurple user,
+	 * which they can use to set a custom avatar.
+	 *
+	 * Since: 3.0.0
+	 */
+	properties[PROP_AVATAR] = g_param_spec_object(
+		"avatar", "avatar",
+		"The avatar of this person",
+		GDK_TYPE_PIXBUF,
+		G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS);
+
+	/**
+	 * PurplePerson:tags:
+	 *
+	 * The [class@Purple.Tags] for this person.
+	 *
+	 * Since: 3.0.0
+	 */
+	properties[PROP_TAGS] = g_param_spec_object(
+		"tags", "tags",
+		"The tags for this person",
+		PURPLE_TYPE_TAGS,
+		G_PARAM_READABLE | G_PARAM_STATIC_STRINGS);
+
+	/**
+	 * PurplePerson:priority-contact:
+	 *
+	 * The [class@Purple.Contact] that currently has the highest priority.
+	 *
+	 * This is used by user interfaces to determine which [class@Purple.Contact]
+	 * to use when messaging and so on.
+	 *
+	 * Since: 3.0.0
+	 */
+	properties[PROP_PRIORITY_CONTACT] = g_param_spec_object(
+		"priority-contact", "priority-contact",
+		"The priority contact for the person",
+		PURPLE_TYPE_CONTACT,
+		G_PARAM_READABLE | G_PARAM_STATIC_STRINGS);
+
+	g_object_class_install_properties(obj_class, N_PROPERTIES, properties);
+}
+
+/******************************************************************************
+ * Public API
+ *****************************************************************************/
+PurplePerson *
+purple_person_new(void) {
+	return g_object_new(PURPLE_TYPE_PERSON, NULL);
+}
+
+const gchar *
+purple_person_get_id(PurplePerson *person) {
+	g_return_val_if_fail(PURPLE_IS_PERSON(person), NULL);
+
+	return person->id;
+}
+
+const gchar *
+purple_person_get_alias(PurplePerson *person) {
+	g_return_val_if_fail(PURPLE_IS_PERSON(person), NULL);
+
+	return person->alias;
+}
+
+void
+purple_person_set_alias(PurplePerson *person, const gchar *alias) {
+	g_return_if_fail(PURPLE_IS_PERSON(person));
+
+	g_free(person->alias);
+	person->alias = g_strdup(alias);
+
+	g_object_notify_by_pspec(G_OBJECT(person), properties[PROP_ALIAS]);
+}
+
+GdkPixbuf *
+purple_person_get_avatar(PurplePerson *person) {
+	g_return_val_if_fail(PURPLE_IS_PERSON(person), NULL);
+
+	return person->avatar;
+}
+
+void
+purple_person_set_avatar(PurplePerson *person, GdkPixbuf *avatar) {
+	g_return_if_fail(PURPLE_IS_PERSON(person));
+
+	if(g_set_object(&person->avatar, avatar)) {
+		g_object_notify_by_pspec(G_OBJECT(person), properties[PROP_AVATAR]);
+	}
+}
+
+PurpleTags *
+purple_person_get_tags(PurplePerson *person) {
+	g_return_val_if_fail(PURPLE_IS_PERSON(person), NULL);
+
+	return person->tags;
+}
+
+void
+purple_person_add_contact(PurplePerson *person, PurpleContact *contact) {
+	PurplePresence *presence = NULL;
+
+	g_return_if_fail(PURPLE_IS_PERSON(person));
+	g_return_if_fail(PURPLE_IS_CONTACT(contact));
+
+	g_ptr_array_add(person->contacts, g_object_ref(contact));
+
+	g_signal_connect_object(contact, "notify::presence",
+	                        G_CALLBACK(purple_person_contact_notify_cb),
+	                        person, 0);
+
+	presence = purple_contact_get_presence(contact);
+	if(PURPLE_IS_PRESENCE(presence)) {
+		g_signal_connect_object(presence, "notify",
+		                        G_CALLBACK(purple_person_presence_notify_cb),
+		                        person, 0);
+	}
+
+	purple_person_sort_contacts(person);
+}
+
+gboolean
+purple_person_remove_contact(PurplePerson *person, PurpleContact *contact) {
+	gboolean removed = FALSE;
+
+	g_return_val_if_fail(PURPLE_IS_PERSON(person), FALSE);
+	g_return_val_if_fail(PURPLE_IS_CONTACT(contact), FALSE);
+
+	/* g_ptr_array_remove calls g_object_unref because we passed it in as a
+	 * GDestroyNotify.
+	 */
+	removed = g_ptr_array_remove(person->contacts, contact);
+
+	/* Disconnect our signal handlers. */
+	g_signal_handlers_disconnect_by_func(contact,
+	                                     purple_person_contact_notify_cb,
+	                                     person);
+	g_signal_handlers_disconnect_by_func(contact,
+	                                     purple_person_presence_notify_cb,
+	                                     person);
+
+	if(removed) {
+		purple_person_sort_contacts(person);
+	}
+
+	return removed;
+}
+
+PurpleContact *
+purple_person_get_priority_contact(PurplePerson *person) {
+	g_return_val_if_fail(PURPLE_IS_PERSON(person), NULL);
+
+	if(person->contacts->len == 0) {
+		return NULL;
+	}
+
+	return g_ptr_array_index(person->contacts, 0);
+}
diff --git a/libpurple/tests/meson.build b/libpurple/tests/meson.build
--- a/libpurple/tests/meson.build
+++ b/libpurple/tests/meson.build
@@ -13,6 +13,7 @@
     'menu',
     'notification',
     'notification_manager',
+    'person',
     'protocol_action',
     'protocol_xfer',
     'purplepath',
diff --git a/libpurple/tests/test_person.c b/libpurple/tests/test_person.c
new file mode 100644
--- /dev/null
+++ b/libpurple/tests/test_person.c
@@ -0,0 +1,387 @@
+/*
+ * Purple - Internet Messaging Library
+ * Copyright (C) Pidgin Developers <devel@pidgin.im>
+ *
+ * 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 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 <https://www.gnu.org/licenses/>.
+ */
+
+#include <glib.h>
+
+#include <purple.h>
+
+#include "test_ui.h"
+
+/******************************************************************************
+ * Callbacks
+ *****************************************************************************/
+static void
+test_purple_person_items_changed_cb(G_GNUC_UNUSED GListModel *model,
+                                    G_GNUC_UNUSED guint position,
+                                    G_GNUC_UNUSED guint removed,
+                                    G_GNUC_UNUSED guint added,
+                                    gpointer data)
+{
+	gboolean *called = data;
+
+	*called = TRUE;
+}
+
+static void
+test_purple_person_notify_cb(G_GNUC_UNUSED GObject *obj,
+                             G_GNUC_UNUSED GParamSpec *pspec,
+                             gpointer data)
+{
+	gboolean *called = data;
+
+	*called = TRUE;
+}
+
+/******************************************************************************
+ * Tests
+ *****************************************************************************/
+static void
+test_purple_person_new(void) {
+	PurplePerson *person = NULL;
+
+	person = purple_person_new();
+
+	g_assert_true(PURPLE_IS_PERSON(person));
+
+	g_clear_object(&person);
+}
+
+static void
+test_purple_person_properties(void) {
+	PurpleContact *person = NULL;
+	PurpleTags *tags = NULL;
+	GdkPixbuf *avatar = NULL;
+	GdkPixbuf *avatar1 = NULL;
+	gchar *id = NULL;
+	gchar *alias = NULL;
+
+	/* Create our avatar for testing. */
+	avatar = gdk_pixbuf_new(GDK_COLORSPACE_RGB, FALSE, 8, 1, 1);
+
+	/* Use g_object_new so we can test setting properties by name. All of them
+	 * call the setter methods, so by doing it this way we exercise more of the
+	 * code.
+	 */
+	person = g_object_new(
+		PURPLE_TYPE_PERSON,
+		"alias", "alias",
+		"avatar", avatar,
+		NULL);
+
+	/* Now use g_object_get to read all of the properties. */
+	g_object_get(person,
+		"id", &id,
+		"alias", &alias,
+		"avatar", &avatar1,
+		"tags", &tags,
+		NULL);
+
+	/* Compare all the things. */
+	g_assert_nonnull(id);
+	g_assert_cmpstr(alias, ==, "alias");
+	g_assert_true(avatar1 == avatar);
+	g_assert_nonnull(tags);
+
+	/* Free/unref all the things. */
+	g_clear_pointer(&id, g_free);
+	g_clear_pointer(&alias, g_free);
+	g_clear_object(&avatar1);
+	g_clear_object(&tags);
+
+	g_clear_object(&avatar);
+	g_clear_object(&person);
+}
+
+static void
+test_purple_person_contacts_single(void) {
+	PurpleAccount *account = NULL;
+	PurpleContact *contact = NULL;
+	PurplePerson *person = NULL;
+	guint n_items = 0;
+	gboolean removed = FALSE;
+	gboolean changed = FALSE;
+
+	account = purple_account_new("test", "test");
+	contact = purple_contact_new(account, "username");
+	person = purple_person_new();
+	g_signal_connect(person, "items-changed",
+	                 G_CALLBACK(test_purple_person_items_changed_cb), &changed);
+
+	n_items = g_list_model_get_n_items(G_LIST_MODEL(person));
+	g_assert_cmpuint(n_items, ==, 0);
+	purple_person_add_contact(person, contact);
+	n_items = g_list_model_get_n_items(G_LIST_MODEL(person));
+	g_assert_cmpuint(n_items, ==, 1);
+	g_assert_true(changed);
+
+	changed = FALSE;
+
+	removed = purple_person_remove_contact(person, contact);
+	g_assert_true(removed);
+	n_items = g_list_model_get_n_items(G_LIST_MODEL(person));
+	g_assert_cmpuint(n_items, ==, 0);
+	g_assert_true(changed);
+
+	g_clear_object(&person);
+	g_clear_object(&account);
+}
+
+static void
+test_purple_person_contacts_multiple(void) {
+	PurpleAccount *account = NULL;
+	PurplePerson *person = NULL;
+	GPtrArray *contacts = NULL;
+	guint n_items = 0;
+	const gint n_contacts = 5;
+	gboolean changed = FALSE;
+
+	account = purple_account_new("test", "test");
+	person = purple_person_new();
+	g_signal_connect(person, "items-changed",
+	                 G_CALLBACK(test_purple_person_items_changed_cb), &changed);
+
+	contacts = g_ptr_array_new_full(n_contacts, g_object_unref);
+	for(gint i = 0; i < n_contacts; i++) {
+		PurpleContact *contact = NULL;
+		gchar *username = NULL;
+
+		changed = FALSE;
+
+		n_items = g_list_model_get_n_items(G_LIST_MODEL(person));
+		g_assert_cmpuint(n_items, ==, i);
+
+		username = g_strdup_printf("username%d", i);
+		contact = purple_contact_new(account, username);
+		g_free(username);
+
+		/* Add the contact to the ptr array so we can remove it below. */
+		g_ptr_array_add(contacts, contact);
+
+		/* Add the contact to the person and make sure that all the magic
+		 * happened.
+		 */
+		purple_person_add_contact(person, contact);
+		n_items = g_list_model_get_n_items(G_LIST_MODEL(person));
+		g_assert_cmpuint(n_items, ==, i + 1);
+		g_assert_true(changed);
+	}
+
+	for(gint i = 0; i < n_contacts; i++) {
+		PurpleContact *contact = contacts->pdata[i];
+		gboolean removed = FALSE;
+
+		changed = FALSE;
+
+		n_items = g_list_model_get_n_items(G_LIST_MODEL(person));
+		g_assert_cmpuint(n_items, ==, n_contacts - i);
+
+		removed = purple_person_remove_contact(person, contact);
+		g_assert_true(removed);
+
+		n_items = g_list_model_get_n_items(G_LIST_MODEL(person));
+		g_assert_cmpuint(n_items, ==, n_contacts - (i + 1));
+
+		g_assert_true(changed);
+	}
+
+	/* Final sanity check that the person has no more contacts. */
+	n_items = g_list_model_get_n_items(G_LIST_MODEL(person));
+	g_assert_cmpuint(n_items, ==, 0);
+
+	g_ptr_array_free(contacts, TRUE);
+
+	g_clear_object(&person);
+	g_clear_object(&account);
+}
+
+static void
+test_purple_person_priority_single(void) {
+	PurpleAccount *account = NULL;
+	PurpleContact *contact = NULL;
+	PurpleContact *priority = NULL;
+	PurplePerson *person = NULL;
+	PurplePresence *presence = NULL;
+	PurpleStatus *status = NULL;
+	PurpleStatusType *status_type = NULL;
+	gboolean called = FALSE;
+
+	account = purple_account_new("test", "test");
+
+	person = purple_person_new();
+	g_signal_connect(person, "notify::priority-contact",
+	                 G_CALLBACK(test_purple_person_notify_cb), &called);
+	priority = purple_person_get_priority_contact(person);
+	g_assert_null(priority);
+
+	/* Build the presence and status for the contact. */
+	presence = g_object_new(PURPLE_TYPE_PRESENCE, NULL);
+	status_type = purple_status_type_new(PURPLE_STATUS_AVAILABLE, "available",
+	                                     "Available", FALSE);
+	status = purple_status_new(status_type, presence);
+	g_object_set(G_OBJECT(presence), "active-status", status, NULL);
+	g_clear_object(&status);
+
+	/* Now create a real contact. */
+	contact = purple_contact_new(account, "username");
+	purple_contact_set_presence(contact, presence);
+	purple_person_add_contact(person, contact);
+
+	g_assert_true(called);
+
+	priority = purple_person_get_priority_contact(person);
+	g_assert_true(priority == contact);
+
+	purple_status_type_destroy(status_type);
+	g_clear_object(&account);
+	g_clear_object(&person);
+	g_clear_object(&contact);
+	g_clear_object(&presence);
+}
+
+static void
+test_purple_person_priority_multiple_with_change(void) {
+	PurpleAccount *account = NULL;
+	PurpleContact *priority = NULL;
+	PurpleContact *first = NULL;
+	PurpleContact *sorted_contact = NULL;
+	PurplePerson *person = NULL;
+	PurplePresence *sorted_presence = NULL;
+	PurpleStatus *status = NULL;
+	PurpleStatusType *available = NULL;
+	PurpleStatusType *offline = NULL;
+	gboolean changed = FALSE;
+	gint n_contacts = 5;
+	guint n_items = 0;
+
+	/* This unit test is a bit complicated, but it adds 5 contacts to a person
+	 * all whose presences are set to offline. After adding all the contacts,
+	 * we verify that the first contact we added is the priority contact. Then
+	 * we flip the active status of the n_contacts - 2 contact to available.
+	 * This should make it the priority contact which we then assert.
+	 */
+
+	account = purple_account_new("test", "test");
+
+	/* Create our status types. */
+	available = purple_status_type_new(PURPLE_STATUS_AVAILABLE, "available",
+	                                   "Available", FALSE);
+	offline = purple_status_type_new(PURPLE_STATUS_OFFLINE, "offline",
+	                                 "Offline", FALSE);
+
+	/* Create the person and connected to the notify signal for the
+	 * priority-contact property.
+	 */
+	person = purple_person_new();
+	g_signal_connect(person, "notify::priority-contact",
+	                 G_CALLBACK(test_purple_person_notify_cb), &changed);
+	priority = purple_person_get_priority_contact(person);
+	g_assert_null(priority);
+
+	/* Create and add all contacts. */
+	for(gint i = 0; i < n_contacts; i++) {
+		PurpleContact *contact = NULL;
+		PurplePresence *presence = NULL;
+		gchar *username = NULL;
+
+		/* Set changed to false as it shouldn't be changed. */
+		changed = FALSE;
+
+		/* Build the presence and status for the contact. */
+		presence = g_object_new(PURPLE_TYPE_PRESENCE, NULL);
+		status = purple_status_new(offline, presence);
+		g_object_set(G_OBJECT(presence), "active-status", status, NULL);
+		g_clear_object(&status);
+
+		/* Now create a real contact. */
+		username = g_strdup_printf("username%d", i + 1);
+		contact = purple_contact_new(account, username);
+		g_free(username);
+		purple_contact_set_presence(contact, presence);
+
+		purple_person_add_contact(person, contact);
+
+		if(i == 0) {
+			first = g_object_ref(contact);
+			g_assert_true(changed);
+		} else {
+			g_assert_false(changed);
+
+			if(i == n_contacts - 2) {
+				sorted_contact = g_object_ref(contact);
+				sorted_presence = g_object_ref(presence);
+			}
+		}
+
+		g_clear_object(&contact);
+		g_clear_object(&presence);
+	}
+
+	n_items = g_list_model_get_n_items(G_LIST_MODEL(person));
+	g_assert_cmpuint(n_items, ==, n_contacts);
+
+	priority = purple_person_get_priority_contact(person);
+	g_assert_true(priority == first);
+	g_clear_object(&first);
+
+	/* Now set the second from the last contact's status to available, and
+	 * verify that that contact is now the priority contact.
+	 */
+	changed = FALSE;
+	status = purple_status_new(available, sorted_presence);
+	g_object_set(G_OBJECT(sorted_presence), "active-status", status, NULL);
+	g_clear_object(&status);
+	g_assert_true(changed);
+	priority = purple_person_get_priority_contact(person);
+	g_assert_true(priority == sorted_contact);
+
+	/* Cleanup. */
+	purple_status_type_destroy(offline);
+	purple_status_type_destroy(available);
+
+	g_clear_object(&sorted_contact);
+	g_clear_object(&sorted_presence);
+
+	g_clear_object(&account);
+	g_clear_object(&person);
+}
+
+/******************************************************************************
+ * Main
+ *****************************************************************************/
+gint
+main(gint argc, gchar *argv[]) {
+	g_test_init(&argc, &argv, NULL);
+
+	test_ui_purple_init();
+
+	g_test_add_func("/person/new",
+	                test_purple_person_new);
+	g_test_add_func("/person/properties",
+	                test_purple_person_properties);
+	g_test_add_func("/person/contacts/single",
+	                test_purple_person_contacts_single);
+	g_test_add_func("/person/contacts/multiple",
+	                test_purple_person_contacts_multiple);
+
+	g_test_add_func("/person/priority/single",
+	                test_purple_person_priority_single);
+	g_test_add_func("/person/priority/multiple-with-change",
+	                test_purple_person_priority_multiple_with_change);
+
+	return g_test_run();
+}
\ No newline at end of file
