pidgin/pidgin

Create PurplePerson.

19 months ago, Gary Kramlich
c22448f50d5d
Parents fb13b6986266
Children 7ab7d79ac0c5
Create PurplePerson.

This automatically manages the sorting of contacts and will report the highest
ranked one as the priority contact.

Testing Done:
Ran the unit tests

Bugs closed: PIDGIN-17668

Reviewed at https://reviews.imfreedom.org/r/1838/
--- a/libpurple/meson.build Tue Sep 27 02:36:20 2022 -0500
+++ b/libpurple/meson.build Tue Sep 27 02:44:13 2022 -0500
@@ -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',
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/libpurple/purpleperson.c Tue Sep 27 02:44:13 2022 -0500
@@ -0,0 +1,471 @@
+/*
+ * 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);
+
+ /* Ref the contact to avoid a use-after free. */
+ g_object_ref(contact);
+
+ /* g_ptr_array_remove calls g_object_unref because we passed it in as a
+ * GDestroyNotify.
+ */
+ removed = g_ptr_array_remove(person->contacts, contact);
+
+ if(removed) {
+ PurplePresence *presence = purple_contact_get_presence(contact);
+
+ /* Disconnect our signal handlers. */
+ g_signal_handlers_disconnect_by_func(contact,
+ purple_person_contact_notify_cb,
+ person);
+
+ if(PURPLE_IS_PRESENCE(presence)) {
+ g_signal_handlers_disconnect_by_func(presence,
+ purple_person_presence_notify_cb,
+ person);
+ }
+
+ purple_person_sort_contacts(person);
+ }
+
+ /* Remove our reference. */
+ g_object_unref(contact);
+
+ 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);
+}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/libpurple/purpleperson.h Tue Sep 27 02:44:13 2022 -0500
@@ -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 */
--- a/libpurple/tests/meson.build Tue Sep 27 02:36:20 2022 -0500
+++ b/libpurple/tests/meson.build Tue Sep 27 02:44:13 2022 -0500
@@ -13,6 +13,7 @@
'menu',
'notification',
'notification_manager',
+ 'person',
'protocol_action',
'protocol_xfer',
'purplepath',
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/libpurple/tests/test_person.c Tue Sep 27 02:44:13 2022 -0500
@@ -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