Wed, 15 Jun 2022 00:32:22 -0500
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_cmds_init(); purple_protocol_manager_startup(); @@ -244,6 +246,7 @@ purple_protocol_manager_shutdown(); purple_cmds_uninit(); + 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 @@ 'purplemenu.c', 'purplemessage.c', 'purplenoopcredentialprovider.c', + 'purplenotification.c', + 'purplenotificationmanager.c', 'purpleoptions.c', 'purplepath.c', 'purpleplugininfo.c', @@ -158,6 +160,8 @@ 'purplemenu.h', 'purplemessage.h', 'purplenoopcredentialprovider.h', + 'purplenotification.h', + 'purplenotificationmanager.h', 'purpleoptions.h', 'purplepath.h', 'purpleplugininfo.h', @@ -248,6 +252,7 @@ 'purpleconversation.h', 'purpleimconversation.h', 'purplemessage.h', + 'purplenotification.h', 'purpleplugininfo.h', 'purpleprotocol.h', 'purpleproxyinfo.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 { + GObject parent; + + gchar *id; + PurpleNotificationType type; + PurpleAccount *account; + + GDateTime *created_timestamp; + gchar *title; + gchar *icon_name; + gboolean read; + gboolean interactive; + + gpointer data; + GDestroyNotify data_destroy_func; +}; + +enum { + PROP_0, + PROP_ID, + PROP_TYPE, + PROP_ACCOUNT, + PROP_CREATED_TIMESTAMP, + PROP_TITLE, + PROP_ICON_NAME, + PROP_READ, + PROP_INTERACTIVE, + PROP_DATA, + PROP_DATA_DESTROY_FUNC, + N_PROPERTIES, +}; +static GParamSpec *properties[N_PROPERTIES] = {NULL, }; + +G_DEFINE_TYPE(PurpleNotification, purple_notification, G_TYPE_OBJECT) + +/****************************************************************************** + * Helpers + *****************************************************************************/ +static void +purple_notification_set_id(PurpleNotification *notification, const gchar *id) { + g_return_if_fail(PURPLE_IS_NOTIFICATION(notification)); + + if(id == NULL) { + notification->id = g_uuid_string_random(); + } else { + notification->id = g_strdup(id); + } + + g_object_notify_by_pspec(G_OBJECT(notification), properties[PROP_ID]); +} + +static void +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]); +} + +static void +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]); + } +} + +static void +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]); +} + +static void +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 + *****************************************************************************/ +static void +purple_notification_get_property(GObject *obj, guint param_id, GValue *value, + GParamSpec *pspec) +{ + PurpleNotification *notification = PURPLE_NOTIFICATION(obj); + + switch(param_id) { + case PROP_ID: + g_value_set_string(value, + purple_notification_get_id(notification)); + break; + case PROP_TYPE: + g_value_set_enum(value, + purple_notification_get_notification_type(notification)); + break; + case PROP_ACCOUNT: + g_value_set_object(value, + purple_notification_get_account(notification)); + break; + case PROP_CREATED_TIMESTAMP: + g_value_set_boxed(value, + purple_notification_get_created_timestamp(notification)); + break; + case PROP_TITLE: + g_value_set_string(value, + purple_notification_get_title(notification)); + break; + case PROP_ICON_NAME: + g_value_set_string(value, + purple_notification_get_icon_name(notification)); + break; + case PROP_READ: + g_value_set_boolean(value, + purple_notification_get_read(notification)); + break; + case PROP_INTERACTIVE: + g_value_set_boolean(value, + purple_notification_get_interactive(notification)); + break; + case PROP_DATA: + g_value_set_pointer(value, + purple_notification_get_data(notification)); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID(obj, param_id, pspec); + break; + } +} + +static void +purple_notification_set_property(GObject *obj, guint param_id, + const GValue *value, GParamSpec *pspec) +{ + PurpleNotification *notification = PURPLE_NOTIFICATION(obj); + + switch(param_id) { + case PROP_ID: + purple_notification_set_id(notification, + g_value_get_string(value)); + break; + case PROP_TYPE: + purple_notification_set_notification_type(notification, + g_value_get_enum(value)); + break; + case PROP_ACCOUNT: + purple_notification_set_account(notification, + g_value_get_object(value)); + break; + case PROP_CREATED_TIMESTAMP: + purple_notification_set_created_timestamp(notification, + g_value_get_boxed(value)); + break; + case PROP_TITLE: + purple_notification_set_title(notification, + g_value_get_string(value)); + break; + case PROP_ICON_NAME: + purple_notification_set_icon_name(notification, + g_value_get_string(value)); + break; + case PROP_READ: + purple_notification_set_read(notification, + g_value_get_boolean(value)); + break; + case PROP_INTERACTIVE: + purple_notification_set_interactive(notification, + g_value_get_boolean(value)); + break; + case PROP_DATA: + purple_notification_set_data(notification, + g_value_get_pointer(value)); + break; + case PROP_DATA_DESTROY_FUNC: + purple_notification_set_data_destroy_func(notification, + g_value_get_pointer(value)); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID(obj, param_id, pspec); + break; + } +} + +static void +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); +} + +static void +purple_notification_init(PurpleNotification *notification) { + purple_notification_set_id(notification, NULL); + + if(notification->created_timestamp == NULL) { + purple_notification_set_created_timestamp(notification, NULL); + } +} + +static void +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. + * + * Since: 3.0.0 + */ + properties[PROP_ID] = g_param_spec_string( + "id", "id", + "The identifier of the notification.", + NULL, + G_PARAM_READABLE | G_PARAM_STATIC_STRINGS + ); + + /** + * PurpleNotification::type: + * + * The [enum@NotificationType] of this notification. + * + * Since: 3.0.0 + */ + properties[PROP_TYPE] = g_param_spec_enum( + "type", "type", + "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. + * + * Since: 3.0.0 + */ + properties[PROP_ACCOUNT] = g_param_spec_object( + "account", "account", + "The optional account that this notification is for.", + PURPLE_TYPE_ACCOUNT, + 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. + * + * Since: 3.0.0 + */ + properties[PROP_CREATED_TIMESTAMP] = g_param_spec_boxed( + "created-timestamp", "created-timestamp", + "The timestamp when this notification was created.", + G_TYPE_DATE_TIME, + 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. + * + * Since: 3.0.0 + */ + properties[PROP_TITLE] = g_param_spec_string( + "title", "title", + "The title for the notification.", + NULL, + 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 + * notification. + * + * Since: 3.0.0 + */ + properties[PROP_ICON_NAME] = g_param_spec_string( + "icon-name", "icon-name", + "The icon name for the notification.", + NULL, + G_PARAM_READWRITE | G_PARAM_CONSTRUCT | G_PARAM_STATIC_STRINGS); + + /** + * PurpleNotification::read: + * + * Whether or not the notification has been read. + * + * Since: 3.0.0 + */ + properties[PROP_READ] = g_param_spec_boolean( + "read", "read", + "Whether or not the notification has been read.", + FALSE, + G_PARAM_READWRITE | G_PARAM_CONSTRUCT | G_PARAM_STATIC_STRINGS); + + /** + * PurpleNotification::interactive: + * + * Whether or not the notification can be interacted with. + * + * Since: 3.0.0 + */ + properties[PROP_INTERACTIVE] = g_param_spec_boolean( + "interactive", "interactive", + "Whether or not the notification can be interacted with.", + FALSE, + G_PARAM_READWRITE | G_PARAM_CONSTRUCT | G_PARAM_STATIC_STRINGS); + + /** + * PurpleNotification::data: + * + * Data specific to the [enum@NotificationType] for the notification. + * + * Since: 3.0.0 + */ + properties[PROP_DATA] = g_param_spec_pointer( + "data", "data", + "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]. + * + * Since: 3.0.0 + */ + 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); +} + +/****************************************************************************** + * Public API + *****************************************************************************/ +PurpleNotification * +purple_notification_new(PurpleNotificationType type, PurpleAccount *account, + gpointer data, GDestroyNotify data_destroy_func) +{ + return g_object_new(PURPLE_TYPE_NOTIFICATION, + "type", type, + "account", account, + "data", data, + "data-destroy-func", data_destroy_func, + NULL); +} + +const gchar * +purple_notification_get_id(PurpleNotification *notification) { + g_return_val_if_fail(PURPLE_IS_NOTIFICATION(notification), NULL); + + return notification->id; +} + +PurpleNotificationType +purple_notification_get_notification_type(PurpleNotification *notification) { + g_return_val_if_fail(PURPLE_IS_NOTIFICATION(notification), + PURPLE_NOTIFICATION_TYPE_UNKNOWN); + + return notification->type; +} + +PurpleAccount * +purple_notification_get_account(PurpleNotification *notification) { + g_return_val_if_fail(PURPLE_IS_NOTIFICATION(notification), NULL); + + return notification->account; +} + +GDateTime * +purple_notification_get_created_timestamp(PurpleNotification *notification) { + g_return_val_if_fail(PURPLE_IS_NOTIFICATION(notification), NULL); + + return notification->created_timestamp; +} + +void +purple_notification_set_created_timestamp(PurpleNotification *notification, + GDateTime *timestamp) +{ + 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(); + } else { + notification->created_timestamp = g_date_time_to_utc(timestamp); + } + + g_object_notify_by_pspec(G_OBJECT(notification), + properties[PROP_CREATED_TIMESTAMP]); +} + +const gchar * +purple_notification_get_title(PurpleNotification *notification) { + g_return_val_if_fail(PURPLE_IS_NOTIFICATION(notification), NULL); + + return notification->title; +} + +void +purple_notification_set_title(PurpleNotification *notification, + const gchar *title) +{ + 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]); +} + +const gchar * +purple_notification_get_icon_name(PurpleNotification *notification) { + g_return_val_if_fail(PURPLE_IS_NOTIFICATION(notification), NULL); + + return notification->icon_name; +} + +void +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]); +} + +gboolean +purple_notification_get_read(PurpleNotification *notification) { + g_return_val_if_fail(PURPLE_IS_NOTIFICATION(notification), FALSE); + + return notification->read; +} + +void +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]); + } +} + +gboolean +purple_notification_get_interactive(PurpleNotification *notification) { + g_return_val_if_fail(PURPLE_IS_NOTIFICATION(notification), FALSE); + + return notification->interactive; +} + +void +purple_notification_set_interactive(PurpleNotification *notification, + gboolean interactive) +{ + 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]); + } +} + +gpointer +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" +#endif + +#ifndef PURPLE_NOTIFICATION_H +#define PURPLE_NOTIFICATION_H + +#include <glib.h> +#include <glib-object.h> + +#include "account.h" + +G_BEGIN_DECLS + +/** + * PurpleNotificationType: + * + * Since: 3.0.0. + */ +typedef enum { + 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; + +/** + * PurpleNotification: + * + * An object that represents a notification. + * + * Since: 3.0.0 + */ + +#define PURPLE_TYPE_NOTIFICATION (purple_notification_get_type()) +G_DECLARE_FINAL_TYPE(PurpleNotification, purple_notification, PURPLE, + NOTIFICATION, GObject) + +/** + * purple_notification_new: + * @type: The [enum@NotificationType] of the notification. + * @account: (nullable): The [class@Account] that created the notification if + * applicable. + * @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. + * + * Since: 3.0.0 + */ +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. + * + * Since: 3.0.0 + */ +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. + * + * Since: 3.0.0 + */ +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. + * + * Since: 3.0.0 + */ +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. + * + * Since: 3.0.0 + */ +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 + * of time. + * + * Since: 3.0.0 + */ +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. + * + * Since: 3.0.0 + */ +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. + * + * Since: 3.0.0 + */ +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. + * + * Since: 3.0.0 + */ +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. + * + * Since: 3.0.0 + */ +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. + * + * Since: 3.0.0 + */ +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. + * + * Since: 3.0.0 + */ +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. + * + * Since: 3.0.0 + */ +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. + * + * Since: 3.0.0 + */ +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. + * + * Since: 3.0.0 + */ +gpointer purple_notification_get_data(PurpleNotification *notification); + +G_END_DECLS + +#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" + +enum { + PROP_ZERO, + PROP_UNREAD_COUNT, + N_PROPERTIES, +}; +static GParamSpec *properties[N_PROPERTIES] = { NULL, }; + +enum { + SIG_ADDED, + SIG_REMOVED, + SIG_READ, + SIG_UNREAD, + N_SIGNALS, +}; +static guint signals[N_SIGNALS] = { 0, }; + +struct _PurpleNotificationManager { + GObject parent; + + GHashTable *notifications; + + guint unread_count; +}; + +G_DEFINE_TYPE(PurpleNotificationManager, purple_notification_manager, + G_TYPE_OBJECT); + +static PurpleNotificationManager *default_manager = NULL; + +/****************************************************************************** + * Helpers + *****************************************************************************/ +static void +purple_notification_manager_set_unread_count(PurpleNotificationManager *manager, + guint unread_count) +{ + if(manager->unread_count != unread_count) { + manager->unread_count = unread_count; + + g_object_notify_by_pspec(G_OBJECT(manager), + properties[PROP_UNREAD_COUNT]); + } +} + +static inline void +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); + } +} + +static inline void +purple_notification_manager_decrement_unread_count(PurpleNotificationManager *manager) +{ + if(manager->unread_count > 0) { + purple_notification_manager_set_unread_count(manager, + manager->unread_count - 1); + } +} + +/****************************************************************************** + * Callbacks + *****************************************************************************/ +static void +purple_notification_manager_notify_cb(GObject *obj, + G_GNUC_UNUSED GParamSpec *pspec, + gpointer data) +{ + PurpleNotification *notification = PURPLE_NOTIFICATION(obj); + PurpleNotificationManager *manager = data; + guint signal_id = 0; + + /* 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]; + } else { + purple_notification_manager_increment_unread_count(manager); + + signal_id = signals[SIG_UNREAD]; + } + + g_signal_emit(manager, signal_id, 0, notification); +} + +/****************************************************************************** + * GObject Implementation + *****************************************************************************/ +static void +purple_notification_manager_get_property(GObject *obj, guint param_id, + GValue *value, GParamSpec *pspec) +{ + PurpleNotificationManager *manager = PURPLE_NOTIFICATION_MANAGER(obj); + + switch(param_id) { + case PROP_UNREAD_COUNT: + g_value_set_uint(value, + purple_notification_manager_get_unread_count(manager)); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID(obj, param_id, pspec); + break; + } +} + +static void +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); +} + +static void +purple_notification_manager_init(PurpleNotificationManager *manager) { + manager->notifications = g_hash_table_new_full(g_str_hash, g_str_equal, + NULL, g_object_unref); +} + +static void +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; + + /* Properties */ + + /** + * PurpleNotificationManager:unread-count: + * + * The number of unread notifications in the manager. + * + * Since: 3.0.0 + */ + properties[PROP_UNREAD_COUNT] = g_param_spec_uint( + "unread-count", "unread-count", + "The number of unread messages in the manager.", + 0, G_MAXUINT, 0, + G_PARAM_READABLE | G_PARAM_STATIC_STRINGS); + + g_object_class_install_properties(obj_class, N_PROPERTIES, properties); + + /* Signals */ + + /** + * PurpleNotificationManager::added: + * @manager: The instance. + * @notification: The [class@Notification] that was added. + * + * Emitted after @notification has been added to @manager. + * + * Since: 3.0.0 + */ + signals[SIG_ADDED] = g_signal_new_class_handler( + "added", + G_OBJECT_CLASS_TYPE(klass), + G_SIGNAL_RUN_LAST, + NULL, + NULL, + NULL, + NULL, + G_TYPE_NONE, + 1, + PURPLE_TYPE_NOTIFICATION); + + /** + * PurpleNotificationManager::removed: + * @manager: The instance. + * @notification: The [class@Notification] that was removed. + * + * Emitted after @notification has been removed from @manager. + * + * Since: 3.0.0 + */ + signals[SIG_REMOVED] = g_signal_new_class_handler( + "removed", + G_OBJECT_CLASS_TYPE(klass), + G_SIGNAL_RUN_LAST, + NULL, + NULL, + NULL, + NULL, + G_TYPE_NONE, + 1, + PURPLE_TYPE_NOTIFICATION); + + /** + * PurpleNotificationManager::read: + * @manager: The instance. + * @notification: The [class@Notification]. + * + * Emitted after @notification has been marked as read. + * + * Since: 3.0.0 + */ + signals[SIG_READ] = g_signal_new_class_handler( + "read", + G_OBJECT_CLASS_TYPE(klass), + G_SIGNAL_RUN_LAST, + NULL, + NULL, + NULL, + NULL, + G_TYPE_NONE, + 1, + PURPLE_TYPE_NOTIFICATION); + + /** + * PurpleNotificationManager::unread: + * @manager: The instance. + * @notification: The [class@Notification]. + * + * Emitted after @notification has been marked as unread. + * + * Since: 3.0.0 + */ + signals[SIG_UNREAD] = g_signal_new_class_handler( + "unread", + G_OBJECT_CLASS_TYPE(klass), + G_SIGNAL_RUN_LAST, + NULL, + NULL, + NULL, + NULL, + G_TYPE_NONE, + 1, + PURPLE_TYPE_NOTIFICATION); +} + +/****************************************************************************** + * Private API + *****************************************************************************/ +void +purple_notification_manager_startup(void) { + if(default_manager == NULL) { + default_manager = g_object_new(PURPLE_TYPE_NOTIFICATION_MANAGER, NULL); + } +} + +void +purple_notification_manager_shutdown(void) { + g_clear_object(&default_manager); +} + +/****************************************************************************** + * Public API + *****************************************************************************/ +PurpleNotificationManager * +purple_notification_manager_get_default(void) { + return default_manager; +} + +void +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); + + return; + } + + 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), + manager, 0); + + /* 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); +} + +gboolean +purple_notification_manager_remove(PurpleNotificationManager *manager, + const gchar *id) +{ + gpointer data = NULL; + gboolean ret = FALSE; + + 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, + data); + + ret = TRUE; + } + + /* 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), + manager); + + /* If the notification is not read, we need to decrement the unread + * count. + */ + if(!purple_notification_get_read(PURPLE_NOTIFICATION(data))) { + purple_notification_manager_decrement_unread_count(manager); + } + + g_object_unref(G_OBJECT(data)); + } + + return ret; +} + +guint +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" +#endif + +#ifndef PURPLE_NOTIFICATION_MANAGER_H +#define PURPLE_NOTIFICATION_MANAGER_H + +#include <glib.h> +#include <glib-object.h> + +#include "account.h" +#include <purplenotification.h> + +G_BEGIN_DECLS + +#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. + * + * Since: 3.0.0 + */ + +/** + * purple_notification_manager_get_default: + * + * Gets the default [class@NotificationManager] instance. + * + * Returns: (transfer none): The default instance. + * + * Since: 3.0.0 + */ +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. + * + * Since: 3.0.0 + */ +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, + * %FALSE otherwise. + * + * Since: 3.0.0 + */ +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. + * + * Since: 3.0.0 + */ +guint purple_notification_manager_get_unread_count(PurpleNotificationManager *manager); + +G_END_DECLS + +#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. + * + * Since: 3.0.0 + */ +void purple_notification_manager_startup(void); + +/** + * purple_notification_manager_shutdown: + * + * Shuts down the notification manager by destroying the default instance. + * + * Since: 3.0.0 + */ +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 @@ 'keyvaluepair', 'markup', 'menu', + 'notification', + 'notification_manager', 'protocol_action', 'protocol_xfer', 'purplepath',
--- /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/>. + */ + +#include <glib.h> + +#include <purple.h> + +#include "test_ui.h" + +/****************************************************************************** + * Helpers + *****************************************************************************/ +static void +test_purple_notification_destory_data_callback(gpointer data) { + gboolean *called = data; + + *called = TRUE; +} + +/****************************************************************************** + * Tests + *****************************************************************************/ +static void +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, + account1, + NULL, + NULL); + + /* Make sure we got a valid notification. */ + g_assert_true(PURPLE_IS_NOTIFICATION(notification)); + + /* Check the type. */ + 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); + g_assert_nonnull(id); + + /* 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); +} + +static void +test_purple_notification_destory_data_func(void) { + PurpleNotification *notification = NULL; + gboolean called = FALSE; + + /* Create the notification. */ + notification = purple_notification_new(PURPLE_NOTIFICATION_TYPE_GENERIC, + NULL, + &called, + test_purple_notification_destory_data_callback); + + /* Sanity check. */ + 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. */ + g_assert_true(called); +} + +static void +test_purple_notification_properties(void) { + PurpleNotification *notification = NULL; + GDateTime *ts1 = NULL, *ts2 = NULL; + + notification = purple_notification_new(PURPLE_NOTIFICATION_TYPE_GENERIC, + NULL, + NULL, + NULL); + + 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), + "title")); + + /* 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), + "icon-name")); + + /* 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)); + + /* Cleanup. */ + g_clear_object(¬ification); +} + +/****************************************************************************** + * Main + *****************************************************************************/ +gint +main(gint argc, gchar *argv[]) { + g_test_init(&argc, &argv, NULL); + + test_ui_purple_init(); + + 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); + + return g_test_run(); +}
--- /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/>. + */ + +#include <glib.h> + +#include <purple.h> + +#include "test_ui.h" + +/****************************************************************************** + * Callbacks + *****************************************************************************/ +static void +test_purple_notification_manager_increment_cb(G_GNUC_UNUSED PurpleNotificationManager *manager, + G_GNUC_UNUSED PurpleNotification *notification, + gpointer data) +{ + gint *called = data; + + *called = *called + 1; +} + +static void +test_purple_notification_manager_unread_count_cb(G_GNUC_UNUSED GObject *obj, + G_GNUC_UNUSED GParamSpec *pspec, + gpointer data) +{ + gint *called = data; + + *called = *called + 1; +} + +/****************************************************************************** + * Tests + *****************************************************************************/ +static void +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); +} + +static void +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), + &added_called); + g_signal_connect(manager, "removed", + G_CALLBACK(test_purple_notification_manager_increment_cb), + &removed_called); + + /* Create the notification and store it's id. */ + notification = purple_notification_new(PURPLE_NOTIFICATION_TYPE_GENERIC, + NULL, NULL, NULL); + 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); +} + +static void +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, + NULL, NULL, NULL); + + 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 + * in static analysis. + */ + g_clear_object(&manager); + } + + g_test_trap_subprocess(NULL, 0, 0); + g_test_trap_assert_stderr("*Purple-WARNING*double add detected for notification*"); +} + +static void +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), + &removed_called); + + notification = purple_notification_new(PURPLE_NOTIFICATION_TYPE_GENERIC, + NULL, NULL, NULL); + /* 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); +} + +static void +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), + &read_called); + g_signal_connect(manager, "unread", + G_CALLBACK(test_purple_notification_manager_increment_cb), + &unread_called); + g_signal_connect(manager, "notify::unread-count", + G_CALLBACK(test_purple_notification_manager_unread_count_cb), + &unread_count_called); + + /* 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, + NULL, + NULL, + NULL); + + 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); + + /* Cleanup. */ + g_clear_object(¬ification); + g_clear_object(&manager); +} + +/****************************************************************************** + * Main + *****************************************************************************/ +gint +main(gint argc, gchar *argv[]) { + GMainLoop *loop = NULL; + gint ret = 0; + + g_test_init(&argc, &argv, NULL); + + test_ui_purple_init(); + + 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); + + ret = g_test_run(); + + g_main_loop_unref(loop); + + return ret; +}
--- 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/purplemarkup.c libpurple/purplemessage.c libpurple/purplenoopcredentialprovider.c +libpurple/purplenotification.c +libpurple/purplenotificationmanager.c libpurple/purpleoptions.c libpurple/purplepath.c libpurple/purpleplugininfo.c