Wed, 07 Aug 2024 21:48:53 -0500
Add PurpleMessages
PurpleMessages is a collection of PurpleMessage objects and the conversation
they belong to.
Testing Done:
Ran the unit tests under valgrind and had the turtles check in on things too.
Reviewed at https://reviews.imfreedom.org/r/3362/
--- a/libpurple/meson.build Wed Aug 07 21:47:34 2024 -0500 +++ b/libpurple/meson.build Wed Aug 07 21:48:53 2024 -0500 @@ -40,6 +40,7 @@ 'purplemarkup.c', 'purplemenu.c', 'purplemessage.c', + 'purplemessages.c', 'purplenoopcredentialprovider.c', 'purplenotification.c', 'purplenotificationauthorizationrequest.c', @@ -129,6 +130,7 @@ 'purplemarkup.h', 'purplemenu.h', 'purplemessage.h', + 'purplemessages.h', 'purplenoopcredentialprovider.h', 'purplenotification.h', 'purplenotificationauthorizationrequest.h',
--- a/libpurple/purplemessage.c Wed Aug 07 21:47:34 2024 -0500 +++ b/libpurple/purplemessage.c Wed Aug 07 21:48:53 2024 -0500 @@ -986,3 +986,21 @@ g_hash_table_remove_all(message->attachments); } +int +purple_message_compare_timestamp(PurpleMessage *message1, + PurpleMessage *message2) +{ + if(message1 == message2) { + return 0; + } + + if(message1 == NULL) { + return -1; + } + + if(message2 == NULL) { + return 1; + } + + return birb_date_time_compare(message1->timestamp, message2->timestamp); +}
--- a/libpurple/purplemessage.h Wed Aug 07 21:47:34 2024 -0500 +++ b/libpurple/purplemessage.h Wed Aug 07 21:48:53 2024 -0500 @@ -700,6 +700,21 @@ PURPLE_AVAILABLE_IN_3_0 void purple_message_clear_attachments(PurpleMessage *message); +/** + * purple_message_compare_timestamp: + * @message1: (transfer none) (nullable): The first instance. + * @message2: (transfer none) (nullable): The second instance. + * + * Compare two [class@Message] objects. + * + * Returns: -1, 0 or 1 if the timestamp of @message1 is *less than*, *equal to* + * or *greater than* the timestamp of @message2. + * + * Since: 3.0 + */ +PURPLE_AVAILABLE_IN_3_0 +int purple_message_compare_timestamp(PurpleMessage *message1, PurpleMessage *message2); + G_END_DECLS #endif /* PURPLE_MESSAGE_H */
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/libpurple/purplemessages.c Wed Aug 07 21:48:53 2024 -0500 @@ -0,0 +1,262 @@ +/* + * Purple - Internet Messaging Library + * Copyright (C) Pidgin Developers <devel@pidgin.im> + * + * Purple is the legal property of its developers, whose names are too numerous + * to list here. Please refer to the COPYRIGHT file distributed with this + * source distribution. + * + * This library 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 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 General Public License for + * more details. + * + * You should have received a copy of the GNU General Public License along with + * this library; if not, see <https://www.gnu.org/licenses/>. + */ + +#include "purplemessages.h" + +enum { + PROP_0, + PROP_CONVERSATION, + PROP_ITEM_TYPE, + PROP_N_ITEMS, + N_PROPERTIES, +}; +static GParamSpec *properties[N_PROPERTIES] = {NULL, }; + +struct _PurpleMessages { + GObject parent; + + PurpleConversation *conversation; + + GListStore *model; +}; + +/****************************************************************************** + * Helpers + *****************************************************************************/ +static void +purple_messages_set_conversation(PurpleMessages *messages, + PurpleConversation *conversation) +{ + g_return_if_fail(PURPLE_IS_MESSAGES(messages)); + + if(g_set_object(&messages->conversation, conversation)) { + g_object_notify_by_pspec(G_OBJECT(messages), + properties[PROP_CONVERSATION]); + } +} + +static int +purple_messages_compare(gconstpointer a, gconstpointer b, + G_GNUC_UNUSED gpointer data) +{ + PurpleMessage *message1 = PURPLE_MESSAGE((gpointer)a); + PurpleMessage *message2 = PURPLE_MESSAGE((gpointer)b); + + return purple_message_compare_timestamp(message1, message2); +} + +/****************************************************************************** + * Callbacks + *****************************************************************************/ +static void +purple_messages_items_changed_cb(G_GNUC_UNUSED GListModel *model, + guint position, guint removed, guint added, + gpointer data) +{ + g_list_model_items_changed(data, position, removed, added); +} + +/****************************************************************************** + * GListModel Implementation + *****************************************************************************/ +static GType +purple_messages_get_item_type(GListModel *model) { + PurpleMessages *messages = PURPLE_MESSAGES(model); + + return g_list_model_get_item_type(G_LIST_MODEL(messages->model)); +} + +static guint +purple_messages_get_n_items(GListModel *model) { + PurpleMessages *messages = PURPLE_MESSAGES(model); + + return g_list_model_get_n_items(G_LIST_MODEL(messages->model)); +} + +static gpointer +purple_messages_get_item(GListModel *model, guint position) { + PurpleMessages *messages = PURPLE_MESSAGES(model); + + return g_list_model_get_item(G_LIST_MODEL(messages->model), position); +} + +static void +purple_messages_list_model_iface_init(GListModelInterface *iface) { + iface->get_item_type = purple_messages_get_item_type; + iface->get_n_items = purple_messages_get_n_items; + iface->get_item = purple_messages_get_item; +} + +/****************************************************************************** + * GObject Implementation + *****************************************************************************/ +G_DEFINE_FINAL_TYPE_WITH_CODE( + PurpleMessages, + purple_messages, + G_TYPE_OBJECT, + G_IMPLEMENT_INTERFACE(G_TYPE_LIST_MODEL, + purple_messages_list_model_iface_init)) + +static void +purple_messages_dispose(GObject *obj) { + PurpleMessages *message = PURPLE_MESSAGES(obj); + + g_clear_object(&message->conversation); + + g_list_store_remove_all(message->model); + + G_OBJECT_CLASS(purple_messages_parent_class)->dispose(obj); +} + +static void +purple_messages_finalize(GObject *obj) { + PurpleMessages *message = PURPLE_MESSAGES(obj); + + g_clear_object(&message->model); + + G_OBJECT_CLASS(purple_messages_parent_class)->finalize(obj); +} + +static void +purple_messages_get_property(GObject *obj, guint param_id, GValue *value, + GParamSpec *pspec) +{ + PurpleMessages *messages = PURPLE_MESSAGES(obj); + + switch(param_id) { + case PROP_CONVERSATION: + g_value_set_object(value, purple_messages_get_conversation(messages)); + break; + case PROP_ITEM_TYPE: + g_value_set_gtype(value, + g_list_model_get_item_type(G_LIST_MODEL(messages->model))); + break; + case PROP_N_ITEMS: + g_value_set_uint(value, + g_list_model_get_n_items(G_LIST_MODEL(messages->model))); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID(obj, param_id, pspec); + break; + } +} + +static void +purple_messages_set_property(GObject *obj, guint param_id, const GValue *value, + GParamSpec *pspec) +{ + PurpleMessages *messages = PURPLE_MESSAGES(obj); + + switch(param_id) { + case PROP_CONVERSATION: + purple_messages_set_conversation(messages, g_value_get_object(value)); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID(obj, param_id, pspec); + break; + } +} + +static void +purple_messages_init(PurpleMessages *messages) { + messages->model = g_list_store_new(PURPLE_TYPE_MESSAGE); + g_signal_connect_object(messages->model, "items-changed", + G_CALLBACK(purple_messages_items_changed_cb), + messages, G_CONNECT_DEFAULT); +} + +static void +purple_messages_class_init(PurpleMessagesClass *klass) { + GObjectClass *obj_class = G_OBJECT_CLASS(klass); + + obj_class->dispose = purple_messages_dispose; + obj_class->finalize = purple_messages_finalize; + obj_class->get_property = purple_messages_get_property; + obj_class->set_property = purple_messages_set_property; + + /** + * PurpleMessages:conversation: + * + * The [class@Conversation] that these messages belong to. + * + * Since: 3.0 + */ + properties[PROP_CONVERSATION] = g_param_spec_object( + "conversation", NULL, NULL, + PURPLE_TYPE_CONVERSATION, + G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS); + + /** + * PurpleMessages:item-type: + * + * The type of items. See [iface@Gio.ListModel.get_item_type]. + * + * Since: 3.0 + */ + properties[PROP_ITEM_TYPE] = g_param_spec_gtype( + "item-type", NULL, NULL, + PURPLE_TYPE_MESSAGE, + G_PARAM_READABLE | G_PARAM_STATIC_STRINGS); + + /** + * PurpleMessages:n-items: + * + * The number of items. See [iface@Gio.ListModel.get_n_items]. + * + * Since: 3.0 + */ + properties[PROP_N_ITEMS] = g_param_spec_uint( + "n-items", NULL, NULL, + 0, G_MAXUINT, 0, + G_PARAM_READABLE | G_PARAM_STATIC_STRINGS); + + g_object_class_install_properties(obj_class, N_PROPERTIES, properties); +} + +/****************************************************************************** + * Public API + *****************************************************************************/ +PurpleMessages * +purple_messages_new(PurpleConversation *conversation) { + g_return_val_if_fail(PURPLE_IS_CONVERSATION(conversation), NULL); + + return g_object_new( + PURPLE_TYPE_MESSAGES, + "conversation", conversation, + NULL); +} + +PurpleConversation * +purple_messages_get_conversation(PurpleMessages *messages) { + g_return_val_if_fail(PURPLE_IS_MESSAGES(messages), NULL); + + return messages->conversation; +} + +void +purple_messages_add(PurpleMessages *messages, PurpleMessage *message) { + g_return_if_fail(PURPLE_IS_MESSAGES(messages)); + g_return_if_fail(PURPLE_IS_MESSAGE(message)); + + g_list_store_insert_sorted(messages->model, message, + (GCompareDataFunc)purple_messages_compare, NULL); +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/libpurple/purplemessages.h Wed Aug 07 21:48:53 2024 -0500 @@ -0,0 +1,100 @@ +/* + * Purple - Internet Messaging Library + * Copyright (C) Pidgin Developers <devel@pidgin.im> + * + * Purple is the legal property of its developers, whose names are too numerous + * to list here. Please refer to the COPYRIGHT file distributed with this + * source distribution. + * + * This library 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 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 General Public License for + * more details. + * + * You should have received a copy of the GNU General Public License along with + * this library; if not, see <https://www.gnu.org/licenses/>. + */ + +#if !defined(PURPLE_GLOBAL_HEADER_INSIDE) && !defined(PURPLE_COMPILATION) +# error "only <purple.h> may be included directly" +#endif + +#ifndef PURPLE_MESSAGES_H +#define PURPLE_MESSAGES_H + +#include <glib.h> +#include <gio/gio.h> + +#include "purpleconversation.h" +#include "purplemessage.h" +#include "purpleversion.h" + +G_BEGIN_DECLS + +#define PURPLE_TYPE_MESSAGES (purple_messages_get_type()) + +/** + * PurpleMessages: + * + * A read-only collection of [class@Message]'s and the [class@Conversation] + * that they belong to. + * + * This collection is meant to make it easy to pass around a number of related + * messages. For example getting messages from a server or displaying a few + * messages in a search result. + * + * Since: 3.0 + */ +PURPLE_AVAILABLE_IN_3_0 +G_DECLARE_FINAL_TYPE(PurpleMessages, purple_messages, PURPLE, MESSAGES, + GObject) + +/** + * purple_messages_new: + * @conversation: The conversation. + * + * Creates a new instance for @conversation. + * + * Returns: (transfer full): The new instance. + * + * Since: 3.0 + */ +PURPLE_AVAILABLE_IN_3_0 +PurpleMessages *purple_messages_new(PurpleConversation *conversation); + +/** + * purple_messages_get_conversation: + * @messages: The instance. + * + * Gets the conversation from @messages. + * + * Returns: (transfer none): The conversation. + * + * Since: 3.0 + */ +PURPLE_AVAILABLE_IN_3_0 +PurpleConversation *purple_messages_get_conversation(PurpleMessages *messages); + +/** + * purple_messages_add: + * @messages: The instance. + * @message: The message to add. + * + * Adds @message to @messages. + * + * @message will be sorted inserted according to @message's created timestamp + * and no duplication checking is performed. + * + * Since: 3.0 + */ +PURPLE_AVAILABLE_IN_3_0 +void purple_messages_add(PurpleMessages *messages, PurpleMessage *message); + +G_END_DECLS + +#endif /* PURPLE_MESSAGES_H */
--- a/libpurple/tests/meson.build Wed Aug 07 21:47:34 2024 -0500 +++ b/libpurple/tests/meson.build Wed Aug 07 21:48:53 2024 -0500 @@ -23,6 +23,7 @@ 'markup', 'menu', 'message', + 'messages', 'notification', 'notification_authorization_request', 'notification_manager',
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/libpurple/tests/test_messages.c Wed Aug 07 21:48:53 2024 -0500 @@ -0,0 +1,231 @@ +/* + * 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> + +/****************************************************************************** + * Callbacks + *****************************************************************************/ +static void +test_purple_messages_items_changed_counter(G_GNUC_UNUSED GListModel *model, + G_GNUC_UNUSED guint position, + G_GNUC_UNUSED guint removed, + G_GNUC_UNUSED guint added, + gpointer data) +{ + guint *counter = data; + + *counter = *counter + 1; +} + +/****************************************************************************** + * Tests + *****************************************************************************/ +static void +test_purple_message_new_with_conversation(void) { + PurpleAccount *account = NULL; + PurpleConversation *conversation = NULL; + PurpleMessages *messages = NULL; + + account = purple_account_new("test", "test"); + conversation = g_object_new( + PURPLE_TYPE_CONVERSATION, + "account", account, + NULL); + + messages = purple_messages_new(conversation); + g_assert_true(PURPLE_IS_MESSAGES(messages)); + g_assert_true(G_IS_LIST_MODEL(messages)); + + g_assert_finalize_object(messages); + g_assert_finalize_object(conversation); + g_clear_object(&account); +} + +static void +test_purple_message_new_without_conversation(void) { + if(g_test_subprocess()) { + PurpleMessages *messages = NULL; + + messages = purple_messages_new(NULL); + g_assert_null(messages); + + g_assert_not_reached(); + } + + g_test_trap_subprocess(NULL, 0, 0); + g_test_trap_assert_stderr("*CRITICAL*IS_CONVERSATION*failed*"); +} + +static void +test_purple_messages_properties(void) { + PurpleAccount *account = NULL; + PurpleConversation *conversation1 = NULL; + PurpleConversation *conversation2 = NULL; + PurpleMessages *messages = NULL; + + account = purple_account_new("test", "test"); + conversation1 = g_object_new( + PURPLE_TYPE_CONVERSATION, + "account", account, + NULL); + + messages = g_object_new( + PURPLE_TYPE_MESSAGES, + "conversation", conversation1, + NULL); + + g_object_get( + G_OBJECT(messages), + "conversation", &conversation2, + NULL); + + g_assert_true(conversation2 == conversation1); + g_clear_object(&conversation2); + + g_assert_finalize_object(messages); + g_assert_finalize_object(conversation1); + g_clear_object(&account); +} + +static void +test_purple_messages_add_single(void) { + PurpleAccount *account = NULL; + PurpleConversation *conversation = NULL; + PurpleMessage *message1 = NULL; + PurpleMessage *message2 = NULL; + PurpleMessages *messages = NULL; + guint counter = 0; + + account = purple_account_new("test", "test"); + conversation = g_object_new( + PURPLE_TYPE_CONVERSATION, + "account", account, + NULL); + + messages = purple_messages_new(conversation); + g_signal_connect(messages, "items-changed", + G_CALLBACK(test_purple_messages_items_changed_counter), + &counter); + + message1 = purple_message_new(purple_account_get_contact_info(account), + "test message"); + purple_messages_add(messages, message1); + g_assert_cmpuint(counter, ==, 1); + + message2 = g_list_model_get_item(G_LIST_MODEL(messages), 0); + g_assert_true(PURPLE_IS_MESSAGE(message2)); + g_assert_true(message2 == message1); + g_clear_object(&message2); + + g_assert_finalize_object(messages); + g_assert_finalize_object(message1); + g_assert_finalize_object(conversation); + g_clear_object(&account); +} + +static void +test_purple_messages_add_multiple(void) { + PurpleAccount *account = NULL; + PurpleConversation *conversation = NULL; + PurpleMessage *message = NULL; + PurpleMessage *message1 = NULL; + PurpleMessage *message2 = NULL; + PurpleMessages *messages = NULL; + GDateTime *dt1 = NULL; + GDateTime *dt2 = NULL; + GTimeZone *zone = NULL; + guint counter = 0; + + /* This test adds two messages to the collection, the first one has an + * older timestamp than the first, which lets us test the automatic + * sorting. + */ + + account = purple_account_new("test", "test"); + conversation = g_object_new( + PURPLE_TYPE_CONVERSATION, + "account", account, + NULL); + + messages = purple_messages_new(conversation); + g_signal_connect(messages, "items-changed", + G_CALLBACK(test_purple_messages_items_changed_counter), + &counter); + + zone = g_time_zone_new_utc(); + + message1 = purple_message_new(purple_account_get_contact_info(account), + "second message"); + dt1 = g_date_time_new_from_iso8601("2024-08-07T03:07:33+0000", zone); + purple_message_set_timestamp(message1, dt1); + g_clear_pointer(&dt1, g_date_time_unref); + purple_messages_add(messages, message1); + g_assert_cmpuint(counter, ==, 1); + + message2 = purple_message_new(purple_account_get_contact_info(account), + "first message"); + dt2 = g_date_time_new_from_iso8601("2024-08-07T03:06:33+0000", zone); + purple_message_set_timestamp(message2, dt2); + g_clear_pointer(&dt2, g_date_time_unref); + purple_messages_add(messages, message2); + g_assert_cmpuint(counter, ==, 2); + + /* Make sure that the first item in the list is message2. */ + message = g_list_model_get_item(G_LIST_MODEL(messages), 0); + g_assert_true(PURPLE_IS_MESSAGE(message)); + g_assert_true(message == message2); + g_clear_object(&message); + + /* Make sure that the second item in the list is message1. */ + message = g_list_model_get_item(G_LIST_MODEL(messages), 1); + g_assert_true(PURPLE_IS_MESSAGE(message)); + g_assert_true(message == message1); + g_clear_object(&message); + + g_clear_pointer(&zone, g_time_zone_unref); + g_assert_finalize_object(messages); + g_assert_finalize_object(message1); + g_assert_finalize_object(message2); + g_assert_finalize_object(conversation); + g_clear_object(&account); +} + +/****************************************************************************** + * Main + *****************************************************************************/ +int +main(int argc, char *argv[]) { + g_test_init(&argc, &argv, NULL); + g_test_set_nonfatal_assertions(); + + g_test_add_func("/messages/new/with-conversation", + test_purple_message_new_with_conversation); + g_test_add_func("/messages/new/without-conversation", + test_purple_message_new_without_conversation); + + g_test_add_func("/messages/properties", test_purple_messages_properties); + + g_test_add_func("/messages/add/single", test_purple_messages_add_single); + g_test_add_func("/messages/add/multiple", + test_purple_messages_add_multiple); + + return g_test_run(); +}