Phase 1 of the Notifications API

Wed, 15 Jun 2022 00:32:22 -0500

author
Gary Kramlich <grim@reaperworld.com>
date
Wed, 15 Jun 2022 00:32:22 -0500
changeset 41433
d563b345a096
parent 41432
aaff9cefb423
child 41434
31c7fd8a23f2

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/

libpurple/core.c file | annotate | diff | comparison | revisions
libpurple/meson.build file | annotate | diff | comparison | revisions
libpurple/purplenotification.c file | annotate | diff | comparison | revisions
libpurple/purplenotification.h file | annotate | diff | comparison | revisions
libpurple/purplenotificationmanager.c file | annotate | diff | comparison | revisions
libpurple/purplenotificationmanager.h file | annotate | diff | comparison | revisions
libpurple/purpleprivate.h file | annotate | diff | comparison | revisions
libpurple/tests/meson.build file | annotate | diff | comparison | revisions
libpurple/tests/test_notification.c file | annotate | diff | comparison | revisions
libpurple/tests/test_notification_manager.c file | annotate | diff | comparison | revisions
po/POTFILES.in file | annotate | diff | comparison | revisions
--- 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(&notification->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(&notification->id, g_free);
+	g_clear_object(&notification->account);
+
+	g_clear_pointer(&notification->created_timestamp, g_date_time_unref);
+	g_clear_pointer(&notification->title, g_free);
+	g_clear_pointer(&notification->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(&notification->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(&notification);
+
+	/* 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(&notification);
+
+	/* 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(&notification);
+}
+
+/******************************************************************************
+ * 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(&notification);
+	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(&notification);
+	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

mercurial