Phase 1 of the Notifications API
* Created PurpleNotification with unit tests.
* Created PurpleNotificationManager with unit tests.
Testing Done:
Ran the unit tests and ran Pidgin in the devenv.
Bugs closed: PIDGIN-17633
Reviewed at https://reviews.imfreedom.org/r/1502/
--- a/libpurple/core.c Fri Jun 10 20:42:36 2022 -0500
+++ b/libpurple/core.c Wed Jun 15 00:32:22 2022 -0500
@@ -137,6 +137,8 @@
+ purple_notification_manager_startup(); purple_protocol_manager_startup();
@@ -244,6 +246,7 @@
purple_protocol_manager_shutdown();
+ purple_notification_manager_shutdown(); purple_history_manager_shutdown();
/* Everything after util_uninit cannot try to write things to the
--- a/libpurple/meson.build Fri Jun 10 20:42:36 2022 -0500
+++ b/libpurple/meson.build Wed Jun 15 00:32:22 2022 -0500
@@ -61,6 +61,8 @@
'purplenoopcredentialprovider.c',
+ 'purplenotification.c', + 'purplenotificationmanager.c', @@ -158,6 +160,8 @@
'purplenoopcredentialprovider.h',
+ 'purplenotification.h', + 'purplenotificationmanager.h', @@ -248,6 +252,7 @@
'purpleimconversation.h',
+ 'purplenotification.h', --- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/libpurple/purplenotification.c Wed Jun 15 00:32:22 2022 -0500
@@ -0,0 +1,544 @@
+ * 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 "purplenotification.h" +#include "purpleenums.h" +struct _PurpleNotification { + PurpleNotificationType type; + PurpleAccount *account; + GDateTime *created_timestamp; + GDestroyNotify data_destroy_func; + PROP_CREATED_TIMESTAMP, + PROP_DATA_DESTROY_FUNC, +static GParamSpec *properties[N_PROPERTIES] = {NULL, }; +G_DEFINE_TYPE(PurpleNotification, purple_notification, G_TYPE_OBJECT) +/****************************************************************************** + *****************************************************************************/ +purple_notification_set_id(PurpleNotification *notification, const gchar *id) { + g_return_if_fail(PURPLE_IS_NOTIFICATION(notification)); + notification->id = g_uuid_string_random(); + notification->id = g_strdup(id); + g_object_notify_by_pspec(G_OBJECT(notification), properties[PROP_ID]); +purple_notification_set_notification_type(PurpleNotification *notification, + PurpleNotificationType type) + g_return_if_fail(PURPLE_IS_NOTIFICATION(notification)); + notification->type = type; + g_object_notify_by_pspec(G_OBJECT(notification), properties[PROP_TYPE]); +purple_notification_set_account(PurpleNotification *notification, + PurpleAccount *account) + g_return_if_fail(PURPLE_IS_NOTIFICATION(notification)); + if(g_set_object(¬ification->account, account)) { + g_object_notify_by_pspec(G_OBJECT(notification), + properties[PROP_ACCOUNT]); +purple_notification_set_data(PurpleNotification *notification, gpointer data) { + g_return_if_fail(PURPLE_IS_NOTIFICATION(notification)); + notification->data = data; + g_object_notify_by_pspec(G_OBJECT(notification), properties[PROP_DATA]); +purple_notification_set_data_destroy_func(PurpleNotification *notification, + GDestroyNotify data_destroy_func) + g_return_if_fail(PURPLE_IS_NOTIFICATION(notification)); + notification->data_destroy_func = data_destroy_func; + g_object_notify_by_pspec(G_OBJECT(notification), + properties[PROP_DATA_DESTROY_FUNC]); +/****************************************************************************** + * GObject Implementation + *****************************************************************************/ +purple_notification_get_property(GObject *obj, guint param_id, GValue *value, + PurpleNotification *notification = PURPLE_NOTIFICATION(obj); + g_value_set_string(value, + purple_notification_get_id(notification)); + g_value_set_enum(value, + purple_notification_get_notification_type(notification)); + g_value_set_object(value, + purple_notification_get_account(notification)); + case PROP_CREATED_TIMESTAMP: + g_value_set_boxed(value, + purple_notification_get_created_timestamp(notification)); + g_value_set_string(value, + purple_notification_get_title(notification)); + g_value_set_string(value, + purple_notification_get_icon_name(notification)); + g_value_set_boolean(value, + purple_notification_get_read(notification)); + g_value_set_boolean(value, + purple_notification_get_interactive(notification)); + g_value_set_pointer(value, + purple_notification_get_data(notification)); + G_OBJECT_WARN_INVALID_PROPERTY_ID(obj, param_id, pspec); +purple_notification_set_property(GObject *obj, guint param_id, + const GValue *value, GParamSpec *pspec) + PurpleNotification *notification = PURPLE_NOTIFICATION(obj); + purple_notification_set_id(notification, + g_value_get_string(value)); + purple_notification_set_notification_type(notification, + g_value_get_enum(value)); + purple_notification_set_account(notification, + g_value_get_object(value)); + case PROP_CREATED_TIMESTAMP: + purple_notification_set_created_timestamp(notification, + g_value_get_boxed(value)); + purple_notification_set_title(notification, + g_value_get_string(value)); + purple_notification_set_icon_name(notification, + g_value_get_string(value)); + purple_notification_set_read(notification, + g_value_get_boolean(value)); + purple_notification_set_interactive(notification, + g_value_get_boolean(value)); + purple_notification_set_data(notification, + g_value_get_pointer(value)); + case PROP_DATA_DESTROY_FUNC: + purple_notification_set_data_destroy_func(notification, + g_value_get_pointer(value)); + G_OBJECT_WARN_INVALID_PROPERTY_ID(obj, param_id, pspec); +purple_notification_finalize(GObject *obj) { + PurpleNotification *notification = PURPLE_NOTIFICATION(obj); + g_clear_pointer(¬ification->id, g_free); + g_clear_object(¬ification->account); + g_clear_pointer(¬ification->created_timestamp, g_date_time_unref); + g_clear_pointer(¬ification->title, g_free); + g_clear_pointer(¬ification->icon_name, g_free); + if(notification->data_destroy_func != NULL) { + notification->data_destroy_func(notification->data); + G_OBJECT_CLASS(purple_notification_parent_class)->finalize(obj); +purple_notification_init(PurpleNotification *notification) { + purple_notification_set_id(notification, NULL); + if(notification->created_timestamp == NULL) { + purple_notification_set_created_timestamp(notification, NULL); +purple_notification_class_init(PurpleNotificationClass *klass) { + GObjectClass *obj_class = G_OBJECT_CLASS(klass); + obj_class->get_property = purple_notification_get_property; + obj_class->set_property = purple_notification_set_property; + obj_class->finalize = purple_notification_finalize; + * PurpleNotification::id: + * The ID of the notification. Used for things that need to address it. + * This is auto populated at creation time. + properties[PROP_ID] = g_param_spec_string( + "The identifier of the notification.", + G_PARAM_READABLE | G_PARAM_STATIC_STRINGS + * PurpleNotification::type: + * The [enum@NotificationType] of this notification. + properties[PROP_TYPE] = g_param_spec_enum( + "The type of notification.", + PURPLE_TYPE_NOTIFICATION_TYPE, + PURPLE_NOTIFICATION_TYPE_UNKNOWN, + G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS); + * PurpleNotification::account: + * An optional [class@Account] that this notification is for. + properties[PROP_ACCOUNT] = g_param_spec_object( + "The optional account that this notification is for.", + G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS); + * PurpleNotification::created-timestamp: + * The creation time of this notification. This always represented as UTC + * internally, and will be set to UTC now by default. + properties[PROP_CREATED_TIMESTAMP] = g_param_spec_boxed( + "created-timestamp", "created-timestamp", + "The timestamp when this notification was created.", + G_PARAM_READWRITE | G_PARAM_CONSTRUCT | G_PARAM_STATIC_STRINGS); + * PurpleNotification::title: + * An optional title for this notification. A user interface may or may not + * choose to use this when displaying the notification. Regardless, this + * should be a translated string. + properties[PROP_TITLE] = g_param_spec_string( + "The title for the notification.", + G_PARAM_READWRITE | G_PARAM_CONSTRUCT | G_PARAM_STATIC_STRINGS); + * PurpleNotification::icon-name: + * The icon-name in the icon theme to use for the notification. A user + * interface may or may not choose to use this when display the + properties[PROP_ICON_NAME] = g_param_spec_string( + "icon-name", "icon-name", + "The icon name for the notification.", + G_PARAM_READWRITE | G_PARAM_CONSTRUCT | G_PARAM_STATIC_STRINGS); + * PurpleNotification::read: + * Whether or not the notification has been read. + properties[PROP_READ] = g_param_spec_boolean( + "Whether or not the notification has been read.", + G_PARAM_READWRITE | G_PARAM_CONSTRUCT | G_PARAM_STATIC_STRINGS); + * PurpleNotification::interactive: + * Whether or not the notification can be interacted with. + properties[PROP_INTERACTIVE] = g_param_spec_boolean( + "interactive", "interactive", + "Whether or not the notification can be interacted with.", + G_PARAM_READWRITE | G_PARAM_CONSTRUCT | G_PARAM_STATIC_STRINGS); + * PurpleNotification::data: + * Data specific to the [enum@NotificationType] for the notification. + properties[PROP_DATA] = g_param_spec_pointer( + "The type specific data for the notification.", + G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS); + * PurpleNotification::data-destroy-func: + * A [func@GLib.DestroyFunc] to call to free + * [property@PurpleNotification:data]. + properties[PROP_DATA_DESTROY_FUNC] = g_param_spec_pointer( + "data-destroy-func", "data-destroy-func", + "The destroy function to clean up the data property.", + G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS); + g_object_class_install_properties(obj_class, N_PROPERTIES, properties); +/****************************************************************************** + *****************************************************************************/ +purple_notification_new(PurpleNotificationType type, PurpleAccount *account, + gpointer data, GDestroyNotify data_destroy_func) + return g_object_new(PURPLE_TYPE_NOTIFICATION, + "data-destroy-func", data_destroy_func, +purple_notification_get_id(PurpleNotification *notification) { + g_return_val_if_fail(PURPLE_IS_NOTIFICATION(notification), NULL); + return notification->id; +purple_notification_get_notification_type(PurpleNotification *notification) { + g_return_val_if_fail(PURPLE_IS_NOTIFICATION(notification), + PURPLE_NOTIFICATION_TYPE_UNKNOWN); + return notification->type; +purple_notification_get_account(PurpleNotification *notification) { + g_return_val_if_fail(PURPLE_IS_NOTIFICATION(notification), NULL); + return notification->account; +purple_notification_get_created_timestamp(PurpleNotification *notification) { + g_return_val_if_fail(PURPLE_IS_NOTIFICATION(notification), NULL); + return notification->created_timestamp; +purple_notification_set_created_timestamp(PurpleNotification *notification, + g_return_if_fail(PURPLE_IS_NOTIFICATION(notification)); + g_clear_pointer(¬ification->created_timestamp, g_date_time_unref); + if(timestamp == NULL) { + notification->created_timestamp = g_date_time_new_now_utc(); + notification->created_timestamp = g_date_time_to_utc(timestamp); + g_object_notify_by_pspec(G_OBJECT(notification), + properties[PROP_CREATED_TIMESTAMP]); +purple_notification_get_title(PurpleNotification *notification) { + g_return_val_if_fail(PURPLE_IS_NOTIFICATION(notification), NULL); + return notification->title; +purple_notification_set_title(PurpleNotification *notification, + g_return_if_fail(PURPLE_IS_NOTIFICATION(notification)); + g_free(notification->title); + notification->title = g_strdup(title); + g_object_notify_by_pspec(G_OBJECT(notification), properties[PROP_TITLE]); +purple_notification_get_icon_name(PurpleNotification *notification) { + g_return_val_if_fail(PURPLE_IS_NOTIFICATION(notification), NULL); + return notification->icon_name; +purple_notification_set_icon_name(PurpleNotification *notification, + const gchar *icon_name) + g_return_if_fail(PURPLE_IS_NOTIFICATION(notification)); + g_free(notification->icon_name); + notification->icon_name = g_strdup(icon_name); + g_object_notify_by_pspec(G_OBJECT(notification), + properties[PROP_ICON_NAME]); +purple_notification_get_read(PurpleNotification *notification) { + g_return_val_if_fail(PURPLE_IS_NOTIFICATION(notification), FALSE); + return notification->read; +purple_notification_set_read(PurpleNotification *notification, gboolean read) { + g_return_if_fail(PURPLE_IS_NOTIFICATION(notification)); + if(notification->read != read) { + notification->read = read; + g_object_notify_by_pspec(G_OBJECT(notification), + properties[PROP_READ]); +purple_notification_get_interactive(PurpleNotification *notification) { + g_return_val_if_fail(PURPLE_IS_NOTIFICATION(notification), FALSE); + return notification->interactive; +purple_notification_set_interactive(PurpleNotification *notification, + g_return_if_fail(PURPLE_IS_NOTIFICATION(notification)); + if(notification->interactive != interactive) { + notification->interactive = interactive; + g_object_notify_by_pspec(G_OBJECT(notification), + properties[PROP_INTERACTIVE]); +purple_notification_get_data(PurpleNotification *notification) { + g_return_val_if_fail(PURPLE_IS_NOTIFICATION(notification), NULL); + return notification->data; --- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/libpurple/purplenotification.h Wed Jun 15 00:32:22 2022 -0500
@@ -0,0 +1,248 @@
+ * 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/>. +#if !defined(PURPLE_GLOBAL_HEADER_INSIDE) && !defined(PURPLE_COMPILATION) +# error "only <purple.h> may be included directly" +#ifndef PURPLE_NOTIFICATION_H +#define PURPLE_NOTIFICATION_H +#include <glib-object.h> + * PurpleNotificationType: + PURPLE_NOTIFICATION_TYPE_UNKNOWN, + PURPLE_NOTIFICATION_TYPE_GENERIC, + PURPLE_NOTIFICATION_TYPE_CONNECTION_ERROR, + PURPLE_NOTIFICATION_TYPE_CONTACT_AUTHORIZATION, + PURPLE_NOTIFICATION_TYPE_FILE_TRANSFER, + PURPLE_NOTIFICATION_TYPE_CHAT_INVITE, + PURPLE_NOTIFICATION_TYPE_MENTION, + PURPLE_NOTIFICATION_TYPE_REACTION, +} PurpleNotificationType; + * An object that represents a notification. +#define PURPLE_TYPE_NOTIFICATION (purple_notification_get_type()) +G_DECLARE_FINAL_TYPE(PurpleNotification, purple_notification, PURPLE, + * purple_notification_new: + * @type: The [enum@NotificationType] of the notification. + * @account: (nullable): The [class@Account] that created the notification if + * @data: The data for the notification. + * @data_destroy_func: A GDestroyNotify to call to free @data. + * Creates a new notification with the given properties. @account is optional. + * Once the notification is prepared, it should be added to a + * [class@NotificationManager] to be presented to the user. + * Returns: (transfer full): The new notification. +PurpleNotification *purple_notification_new(PurpleNotificationType type, PurpleAccount *account, gpointer data, GDestroyNotify data_destroy_func); + * purple_notification_get_id: + * @notification: The instance. + * Gets the identifier of @notification. + * Returns: The identifier of @notification. +const gchar *purple_notification_get_id(PurpleNotification *notification); + * purple_notification_get_notification_type: + * @notification: The instance. + * Gets the [enum@NotificationType] of @notification. + * Returns: The type of @notification. +PurpleNotificationType purple_notification_get_notification_type(PurpleNotification *notification); + * purple_notification_get_account: + * @notification: The instance. + * Gets the [class@Account] of @notification. + * Returns: (transfer none): The account of @notification. +PurpleAccount *purple_notification_get_account(PurpleNotification *notification); + * purple_notification_get_created_timestamp: + * @notification: The instance. + * Gets the created time of @notification. + * Returns: (transfer none): The creation time of @notification. +GDateTime *purple_notification_get_created_timestamp(PurpleNotification *notification); + * purple_notification_set_created_timestamp: + * @notification: The instance. + * @timestamp: (transfer none): The new timestamp. + * Sets the created timestamp of @notification to @timestamp. + * Timestamp is internally converted to UTC so you don't need to do that ahead +void purple_notification_set_created_timestamp(PurpleNotification *notification, GDateTime *timestamp); + * purple_notification_get_title: + * @notification: The instance. + * Gets the title of @notification. + * Returns: The title of @notification. +const gchar *purple_notification_get_title(PurpleNotification *notification); + * purple_notification_set_title: + * @notification: The instance. + * @title: (nullable): The new title. + * Sets the title of @notification to @title. +void purple_notification_set_title(PurpleNotification *notification, const gchar *title); + * purple_notification_get_icon_name: + * @notification: The instance. + * Gets the named icon for @notification. + * Returns: The named icon for @notification. +const gchar *purple_notification_get_icon_name(PurpleNotification *notification); + * purple_notification_set_icon_name: + * @notification: The instance. + * @icon_name: (nullable): The icon name. + * Sets the named icon for @notification to @icon_name. +void purple_notification_set_icon_name(PurpleNotification *notification, const gchar *icon_name); + * purple_notification_get_read: + * @notification: The instance. + * Gets whether or not @notification has been read. + * Returns: %TRUE if @notification has been read, %FALSE otherwise. +gboolean purple_notification_get_read(PurpleNotification *notification); + * purple_notification_set_read: + * @notification: The instance. + * @read: Whether or not the notification has been read. + * Sets @notification's read state to @read. +void purple_notification_set_read(PurpleNotification *notification, gboolean read); + * purple_notification_get_interactive: + * @notification: The instance. + * Gets whether or not @notification can be interacted with. + * Returns: %TRUE if @notification can be interacted with, %FALSE otherwise. +gboolean purple_notification_get_interactive(PurpleNotification *notification); + * purple_notification_set_interactive: + * @notification: The instance. + * @interactive: Whether or not the notification can be interacted with. + * Sets @notification's interactive state to @interactive. +void purple_notification_set_interactive(PurpleNotification *notification, gboolean interactive); + * purple_notification_get_data: + * @notification: The instance. + * Gets the data that @notification was created with. + * Returns: (transfer none): The data for @notification. +gpointer purple_notification_get_data(PurpleNotification *notification); +#endif /* PURPLE_NOTIFICATION */ --- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/libpurple/purplenotificationmanager.c Wed Jun 15 00:32:22 2022 -0500
@@ -0,0 +1,372 @@
+ * 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 <glib/gi18n-lib.h> +#include "purplenotificationmanager.h" +#include "purpleprivate.h" +static GParamSpec *properties[N_PROPERTIES] = { NULL, }; +static guint signals[N_SIGNALS] = { 0, }; +struct _PurpleNotificationManager { + GHashTable *notifications; +G_DEFINE_TYPE(PurpleNotificationManager, purple_notification_manager, +static PurpleNotificationManager *default_manager = NULL; +/****************************************************************************** + *****************************************************************************/ +purple_notification_manager_set_unread_count(PurpleNotificationManager *manager, + if(manager->unread_count != unread_count) { + manager->unread_count = unread_count; + g_object_notify_by_pspec(G_OBJECT(manager), + properties[PROP_UNREAD_COUNT]); +purple_notification_manager_increment_unread_count(PurpleNotificationManager *manager) + if(manager->unread_count < G_MAXUINT) { + purple_notification_manager_set_unread_count(manager, + manager->unread_count + 1); +purple_notification_manager_decrement_unread_count(PurpleNotificationManager *manager) + if(manager->unread_count > 0) { + purple_notification_manager_set_unread_count(manager, + manager->unread_count - 1); +/****************************************************************************** + *****************************************************************************/ +purple_notification_manager_notify_cb(GObject *obj, + G_GNUC_UNUSED GParamSpec *pspec, + PurpleNotification *notification = PURPLE_NOTIFICATION(obj); + PurpleNotificationManager *manager = data; + /* This function is called after the property is changed. So we need to + * get the new value to determine how the state changed. + if(purple_notification_get_read(notification)) { + purple_notification_manager_decrement_unread_count(manager); + signal_id = signals[SIG_READ]; + purple_notification_manager_increment_unread_count(manager); + signal_id = signals[SIG_UNREAD]; + g_signal_emit(manager, signal_id, 0, notification); +/****************************************************************************** + * GObject Implementation + *****************************************************************************/ +purple_notification_manager_get_property(GObject *obj, guint param_id, + GValue *value, GParamSpec *pspec) + PurpleNotificationManager *manager = PURPLE_NOTIFICATION_MANAGER(obj); + case PROP_UNREAD_COUNT: + g_value_set_uint(value, + purple_notification_manager_get_unread_count(manager)); + G_OBJECT_WARN_INVALID_PROPERTY_ID(obj, param_id, pspec); +purple_notification_manager_finalize(GObject *obj) { + PurpleNotificationManager *manager = NULL; + manager = PURPLE_NOTIFICATION_MANAGER(obj); + g_clear_pointer(&manager->notifications, g_hash_table_destroy); + G_OBJECT_CLASS(purple_notification_manager_parent_class)->finalize(obj); +purple_notification_manager_init(PurpleNotificationManager *manager) { + manager->notifications = g_hash_table_new_full(g_str_hash, g_str_equal, +purple_notification_manager_class_init(PurpleNotificationManagerClass *klass) { + GObjectClass *obj_class = G_OBJECT_CLASS(klass); + obj_class->get_property = purple_notification_manager_get_property; + obj_class->finalize = purple_notification_manager_finalize; + * PurpleNotificationManager:unread-count: + * The number of unread notifications in the manager. + properties[PROP_UNREAD_COUNT] = g_param_spec_uint( + "unread-count", "unread-count", + "The number of unread messages in the manager.", + G_PARAM_READABLE | G_PARAM_STATIC_STRINGS); + g_object_class_install_properties(obj_class, N_PROPERTIES, properties); + * PurpleNotificationManager::added: + * @manager: The instance. + * @notification: The [class@Notification] that was added. + * Emitted after @notification has been added to @manager. + signals[SIG_ADDED] = g_signal_new_class_handler( + G_OBJECT_CLASS_TYPE(klass), + PURPLE_TYPE_NOTIFICATION); + * PurpleNotificationManager::removed: + * @manager: The instance. + * @notification: The [class@Notification] that was removed. + * Emitted after @notification has been removed from @manager. + signals[SIG_REMOVED] = g_signal_new_class_handler( + G_OBJECT_CLASS_TYPE(klass), + PURPLE_TYPE_NOTIFICATION); + * PurpleNotificationManager::read: + * @manager: The instance. + * @notification: The [class@Notification]. + * Emitted after @notification has been marked as read. + signals[SIG_READ] = g_signal_new_class_handler( + G_OBJECT_CLASS_TYPE(klass), + PURPLE_TYPE_NOTIFICATION); + * PurpleNotificationManager::unread: + * @manager: The instance. + * @notification: The [class@Notification]. + * Emitted after @notification has been marked as unread. + signals[SIG_UNREAD] = g_signal_new_class_handler( + G_OBJECT_CLASS_TYPE(klass), + PURPLE_TYPE_NOTIFICATION); +/****************************************************************************** + *****************************************************************************/ +purple_notification_manager_startup(void) { + if(default_manager == NULL) { + default_manager = g_object_new(PURPLE_TYPE_NOTIFICATION_MANAGER, NULL); +purple_notification_manager_shutdown(void) { + g_clear_object(&default_manager); +/****************************************************************************** + *****************************************************************************/ +PurpleNotificationManager * +purple_notification_manager_get_default(void) { + return default_manager; +purple_notification_manager_add(PurpleNotificationManager *manager, + PurpleNotification *notification) + const gchar *id = NULL; + g_return_if_fail(PURPLE_IS_NOTIFICATION_MANAGER(manager)); + g_return_if_fail(PURPLE_IS_NOTIFICATION(notification)); + id = purple_notification_get_id(notification); + if(g_hash_table_lookup(manager->notifications, (gpointer)id) != NULL) { + g_warning("double add detected for notification %s", id); + g_hash_table_insert(manager->notifications, (gpointer)id, notification); + /* Connect to the notify signal for the read property only so we can + * propagate out changes for any notification. + g_signal_connect_object(notification, "notify::read", + G_CALLBACK(purple_notification_manager_notify_cb), + /* If the notification is not read, we need to increment the unread count. + if(!purple_notification_get_read(notification)) { + purple_notification_manager_increment_unread_count(manager); + g_signal_emit(G_OBJECT(manager), signals[SIG_ADDED], 0, notification); +purple_notification_manager_remove(PurpleNotificationManager *manager, + g_return_val_if_fail(PURPLE_IS_NOTIFICATION_MANAGER(manager), FALSE); + g_return_val_if_fail(id != NULL, FALSE); + data = g_hash_table_lookup(manager->notifications, id); + if(PURPLE_IS_NOTIFICATION(data)) { + /* Reference the notification so we can emit the signal after it's been + * removed from the hash table. + g_object_ref(G_OBJECT(data)); + if(g_hash_table_remove(manager->notifications, id)) { + g_signal_emit(G_OBJECT(manager), signals[SIG_REMOVED], 0, + /* Remove the notify signal handler for the read state incase someone + * else added a reference to the notification which would then mess + * with our unread count accounting. + g_signal_handlers_disconnect_by_func(data, + G_CALLBACK(purple_notification_manager_notify_cb), + /* If the notification is not read, we need to decrement the unread + if(!purple_notification_get_read(PURPLE_NOTIFICATION(data))) { + purple_notification_manager_decrement_unread_count(manager); + g_object_unref(G_OBJECT(data)); +purple_notification_manager_get_unread_count(PurpleNotificationManager *manager) { + g_return_val_if_fail(PURPLE_IS_NOTIFICATION_MANAGER(manager), 0); + return manager->unread_count; --- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/libpurple/purplenotificationmanager.h Wed Jun 15 00:32:22 2022 -0500
@@ -0,0 +1,97 @@
+ * 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 <purple.h> may be included directly" +#ifndef PURPLE_NOTIFICATION_MANAGER_H +#define PURPLE_NOTIFICATION_MANAGER_H +#include <glib-object.h> +#include <purplenotification.h> +#define PURPLE_TYPE_NOTIFICATION_MANAGER (purple_notification_manager_get_type()) +G_DECLARE_FINAL_TYPE(PurpleNotificationManager, purple_notification_manager, + PURPLE, NOTIFICATION_MANAGER, GObject) + * PurpleNotificationManager: + * Purple Notification Manager manages all notifications between protocols and + * plugins and how the user interface interacts with them. + * purple_notification_manager_get_default: + * Gets the default [class@NotificationManager] instance. + * Returns: (transfer none): The default instance. +PurpleNotificationManager *purple_notification_manager_get_default(void); + * purple_notification_manager_add: + * @manager: The instance. + * @notification: (transfer full): The [class@Notification] to add. + * Adds @notification into @manager. +void purple_notification_manager_add(PurpleNotificationManager *manager, PurpleNotification *notification); + * purple_notification_manager_remove: + * @manager: The instance. + * @id: The identifier of the notification to remove. + * Removes @notification from @manager. + * Returns: %TRUE if @notification was successfully removed from @manager, +gboolean purple_notification_manager_remove(PurpleNotificationManager *manager, const gchar *id); + * purple_notification_manager_get_unread_count: + * @manager: The instance. + * Gets the number of currently unread notifications. + * Returns: The number of unread notifications. +guint purple_notification_manager_get_unread_count(PurpleNotificationManager *manager); +#endif /* PURPLE_NOTIFICATION_MANAGER_H */ --- a/libpurple/purpleprivate.h Fri Jun 10 20:42:36 2022 -0500
+++ b/libpurple/purpleprivate.h Wed Jun 15 00:32:22 2022 -0500
@@ -321,6 +321,24 @@
void purple_history_manager_shutdown(void);
+ * purple_notification_manager_startup: + * Starts up the notification manager by creating the default instance. +void purple_notification_manager_startup(void); + * purple_notification_manager_shutdown: + * Shuts down the notification manager by destroying the default instance. +void purple_notification_manager_shutdown(void); * purple_whiteboard_manager_startup:
* Starts up the whiteboard manager by creating the default instance.
--- a/libpurple/tests/meson.build Fri Jun 10 20:42:36 2022 -0500
+++ b/libpurple/tests/meson.build Wed Jun 15 00:32:22 2022 -0500
@@ -10,6 +10,8 @@
+ 'notification_manager', --- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/libpurple/tests/test_notification.c Wed Jun 15 00:32:22 2022 -0500
@@ -0,0 +1,163 @@
+ * 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/>. +/****************************************************************************** + *****************************************************************************/ +test_purple_notification_destory_data_callback(gpointer data) { + gboolean *called = data; +/****************************************************************************** + *****************************************************************************/ +test_purple_notification_new(void) { + PurpleAccount *account1 = NULL, *account2 = NULL; + PurpleNotification *notification = NULL; + PurpleNotificationType type = PURPLE_NOTIFICATION_TYPE_UNKNOWN; + GDateTime *created_timestamp = NULL; + const gchar *id = NULL; + account1 = purple_account_new("test", "test"); + notification = purple_notification_new(PURPLE_NOTIFICATION_TYPE_GENERIC, + /* Make sure we got a valid notification. */ + g_assert_true(PURPLE_IS_NOTIFICATION(notification)); + type = purple_notification_get_notification_type(notification); + g_assert_cmpint(PURPLE_NOTIFICATION_TYPE_GENERIC, ==, type); + /* Verify the account is set properly. */ + account2 = purple_notification_get_account(notification); + g_assert_nonnull(account2); + g_assert_true(account1 == account2); + /* Make sure that the id was generated. */ + id = purple_notification_get_id(notification); + /* Make sure that the created-timestamp was set. */ + created_timestamp = purple_notification_get_created_timestamp(notification); + g_assert_nonnull(created_timestamp); + /* Unref it to destory it. */ + g_clear_object(¬ification); + /* Clean up the account. */ + g_clear_object(&account1); +test_purple_notification_destory_data_func(void) { + PurpleNotification *notification = NULL; + gboolean called = FALSE; + /* Create the notification. */ + notification = purple_notification_new(PURPLE_NOTIFICATION_TYPE_GENERIC, + test_purple_notification_destory_data_callback); + g_assert_true(PURPLE_IS_NOTIFICATION(notification)); + /* Unref it to force the destory callback to be called. */ + g_clear_object(¬ification); + /* Make sure the callback was called. */ +test_purple_notification_properties(void) { + PurpleNotification *notification = NULL; + GDateTime *ts1 = NULL, *ts2 = NULL; + notification = purple_notification_new(PURPLE_NOTIFICATION_TYPE_GENERIC, + g_assert_true(PURPLE_IS_NOTIFICATION(notification)); + /* Set the timestamp to current utc and verify it was set properly. */ + ts1 = g_date_time_new_now_utc(); + purple_notification_set_created_timestamp(notification, ts1); + ts2 = purple_notification_get_created_timestamp(notification); + g_assert_true(g_date_time_equal(ts1, ts2)); + g_date_time_unref(ts1); + /* Set the title and verify it was set properly. */ + purple_notification_set_title(notification, "title"); + g_assert_true(purple_strequal(purple_notification_get_title(notification), + /* Set the title and verify it was set properly. */ + purple_notification_set_icon_name(notification, "icon-name"); + g_assert_true(purple_strequal(purple_notification_get_icon_name(notification), + /* Set the read state and verify it. */ + purple_notification_set_read(notification, TRUE); + g_assert_true(purple_notification_get_read(notification)); + purple_notification_set_read(notification, FALSE); + g_assert_false(purple_notification_get_read(notification)); + /* Set the interactive state and verify it. */ + purple_notification_set_interactive(notification, TRUE); + g_assert_true(purple_notification_get_interactive(notification)); + purple_notification_set_interactive(notification, FALSE); + g_assert_false(purple_notification_get_interactive(notification)); + g_clear_object(¬ification); +/****************************************************************************** + *****************************************************************************/ +main(gint argc, gchar *argv[]) { + g_test_init(&argc, &argv, NULL); + g_test_add_func("/notification/new", + test_purple_notification_new); + g_test_add_func("/notification/destory-data-func", + test_purple_notification_destory_data_func); + g_test_add_func("/notification/properties", + test_purple_notification_properties); --- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/libpurple/tests/test_notification_manager.c Wed Jun 15 00:32:22 2022 -0500
@@ -0,0 +1,261 @@
+ * 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/>. +/****************************************************************************** + *****************************************************************************/ +test_purple_notification_manager_increment_cb(G_GNUC_UNUSED PurpleNotificationManager *manager, + G_GNUC_UNUSED PurpleNotification *notification, +test_purple_notification_manager_unread_count_cb(G_GNUC_UNUSED GObject *obj, + G_GNUC_UNUSED GParamSpec *pspec, +/****************************************************************************** + *****************************************************************************/ +test_purple_notification_manager_get_default(void) { + PurpleNotificationManager *manager1 = NULL, *manager2 = NULL; + manager1 = purple_notification_manager_get_default(); + g_assert_true(PURPLE_IS_NOTIFICATION_MANAGER(manager1)); + manager2 = purple_notification_manager_get_default(); + g_assert_true(PURPLE_IS_NOTIFICATION_MANAGER(manager2)); + g_assert_true(manager1 == manager2); +test_purple_notification_manager_add_remove(void) { + PurpleNotificationManager *manager = NULL; + PurpleNotification *notification = NULL; + gint added_called = 0, removed_called = 0; + gboolean removed = FALSE; + const gchar *id = NULL; + guint unread_count = 0; + manager = g_object_new(PURPLE_TYPE_NOTIFICATION_MANAGER, NULL); + g_assert_true(PURPLE_IS_NOTIFICATION_MANAGER(manager)); + /* Wire up our signals. */ + g_signal_connect(manager, "added", + G_CALLBACK(test_purple_notification_manager_increment_cb), + g_signal_connect(manager, "removed", + G_CALLBACK(test_purple_notification_manager_increment_cb), + /* Create the notification and store it's id. */ + notification = purple_notification_new(PURPLE_NOTIFICATION_TYPE_GENERIC, + id = purple_notification_get_id(notification); + /* Add the notification to the manager. */ + purple_notification_manager_add(manager, notification); + /* Make sure the added signal was called. */ + g_assert_cmpint(added_called, ==, 1); + /* Verify that the unread count is 1. */ + unread_count = purple_notification_manager_get_unread_count(manager); + g_assert_cmpint(unread_count, ==, 1); + /* Remove the notification. */ + removed = purple_notification_manager_remove(manager, id); + g_assert_true(removed); + g_assert_cmpint(removed_called, ==, 1); + /* Verify that the unread count is now 0. */ + unread_count = purple_notification_manager_get_unread_count(manager); + g_assert_cmpint(unread_count, ==, 0); + /* Clean up the manager. */ + g_clear_object(&manager); +test_purple_notification_manager_double_add(void) { + if(g_test_subprocess()) { + PurpleNotificationManager *manager = NULL; + PurpleNotification *notification = NULL; + manager = g_object_new(PURPLE_TYPE_NOTIFICATION_MANAGER, NULL); + notification = purple_notification_new(PURPLE_NOTIFICATION_TYPE_GENERIC, + purple_notification_manager_add(manager, notification); + purple_notification_manager_add(manager, notification); + /* This will never get called as the double add outputs a g_warning() + * that causes the test to fail. This is left to avoid a false postive + g_clear_object(&manager); + g_test_trap_subprocess(NULL, 0, 0); + g_test_trap_assert_stderr("*Purple-WARNING*double add detected for notification*"); +test_purple_notification_manager_double_remove(void) { + PurpleNotificationManager *manager = NULL; + PurpleNotification *notification = NULL; + const gchar *id = NULL; + gint removed_called = 0; + manager = g_object_new(PURPLE_TYPE_NOTIFICATION_MANAGER, NULL); + g_signal_connect(manager, "removed", + G_CALLBACK(test_purple_notification_manager_increment_cb), + notification = purple_notification_new(PURPLE_NOTIFICATION_TYPE_GENERIC, + /* Add an additional reference because the manager takes one and the id + * belongs to the notification. So without this, the first remove frees + * the id which would cause an invalid read. + g_object_ref(notification); + id = purple_notification_get_id(notification); + purple_notification_manager_add(manager, notification); + g_assert_true(purple_notification_manager_remove(manager, id)); + g_assert_false(purple_notification_manager_remove(manager, id)); + g_assert_cmpint(removed_called, ==, 1); + g_clear_object(¬ification); + g_clear_object(&manager); +test_purple_notification_manager_read_propagation(void) { + PurpleNotificationManager *manager = NULL; + PurpleNotification *notification = NULL; + gint read_called = 0, unread_called = 0, unread_count_called = 0; + /* Create the manager. */ + manager = g_object_new(PURPLE_TYPE_NOTIFICATION_MANAGER, NULL); + g_signal_connect(manager, "read", + G_CALLBACK(test_purple_notification_manager_increment_cb), + g_signal_connect(manager, "unread", + G_CALLBACK(test_purple_notification_manager_increment_cb), + g_signal_connect(manager, "notify::unread-count", + G_CALLBACK(test_purple_notification_manager_unread_count_cb), + /* Create the notification and add a reference to it before we give our + * original refernce to the manager. + notification = purple_notification_new(PURPLE_NOTIFICATION_TYPE_GENERIC, + g_object_ref(notification); + purple_notification_manager_add(manager, notification); + /* Verify that the read and unread signals were not yet emitted. */ + g_assert_cmpint(read_called, ==, 0); + g_assert_cmpint(unread_called, ==, 0); + /* Verify that the unread_count property changed. */ + g_assert_cmpint(unread_count_called, ==, 1); + /* Now mark the notification as read. */ + purple_notification_set_read(notification, TRUE); + g_assert_cmpint(read_called, ==, 1); + g_assert_cmpint(unread_called, ==, 0); + g_assert_cmpint(unread_count_called, ==, 2); + /* Now mark the notification as unread. */ + purple_notification_set_read(notification, FALSE); + g_assert_cmpint(read_called, ==, 1); + g_assert_cmpint(unread_called, ==, 1); + g_assert_cmpint(unread_count_called, ==, 3); + g_clear_object(¬ification); + g_clear_object(&manager); +/****************************************************************************** + *****************************************************************************/ +main(gint argc, gchar *argv[]) { + GMainLoop *loop = NULL; + g_test_init(&argc, &argv, NULL); + loop = g_main_loop_new(NULL, FALSE); + g_test_add_func("/notification-manager/get-default", + test_purple_notification_manager_get_default); + g_test_add_func("/notification-manager/add-remove", + test_purple_notification_manager_add_remove); + g_test_add_func("/notification-manager/double-add", + test_purple_notification_manager_double_add); + g_test_add_func("/notification-manager/double-remove", + test_purple_notification_manager_double_remove); + g_test_add_func("/notification-manager/read-propagation", + test_purple_notification_manager_read_propagation); + g_main_loop_unref(loop); --- a/po/POTFILES.in Fri Jun 10 20:42:36 2022 -0500
+++ b/po/POTFILES.in Wed Jun 15 00:32:22 2022 -0500
@@ -258,6 +258,8 @@
libpurple/purplemessage.c
libpurple/purplenoopcredentialprovider.c
+libpurple/purplenotification.c +libpurple/purplenotificationmanager.c libpurple/purpleoptions.c
libpurple/purpleplugininfo.c