pidgin/pidgin

Phase 1 of the Notifications API

23 months ago, Gary Kramlich
d563b345a096
Parents aaff9cefb423
Children 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/
--- 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