Tue, 27 Sep 2022 02:44:13 -0500
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