# HG changeset patch
# Node ID 238f1cf81e4d73b6bd3bd4cc5210f35754d1da38
# Parent  93996e9119a26c90dc292e346f74ce7e86757250
diff --git a/libpurple/meson.build b/libpurple/meson.build
--- a/libpurple/meson.build
+++ b/libpurple/meson.build
@@ -14,6 +14,7 @@
 	'purpleaccountusersplit.c',
 	'purpleaddcontactrequest.c',
 	'purpleattachment.c',
+	'purpleattachments.c',
 	'purpleauthorizationrequest.c',
 	'purpleavatar.c',
 	'purplebadge.c',
@@ -108,6 +109,8 @@
 	'purpleaccountoption.h',
 	'purpleaccountusersplit.h',
 	'purpleaddcontactrequest.h',
+	'purpleattachment.h',
+	'purpleattachments.h',
 	'purpleauthorizationrequest.h',
 	'purpleavatar.h',
 	'purplebadge.h',
@@ -136,7 +139,6 @@
 	'purplehistoryadapter.h',
 	'purplehistorymanager.h',
 	'purpleidlemanager.h',
-	'purpleattachment.h',
 	'purplekeyvaluepair.h',
 	'purplemarkup.h',
 	'purplemenu.h',
diff --git a/libpurple/purpleattachments.c b/libpurple/purpleattachments.c
new file mode 100644
--- /dev/null
+++ b/libpurple/purpleattachments.c
@@ -0,0 +1,271 @@
+/*
+ * Purple - Internet Messaging Library
+ * Copyright (C) Pidgin Developers <devel@pidgin.im>
+ *
+ * Purple is the legal property of its developers, whose names are too numerous
+ * to list here. Please refer to the COPYRIGHT file distributed with this
+ * source distribution.
+ *
+ * This library is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License as published by the Free
+ * Software Foundation; either version 2 of the License, or (at your option)
+ * any later version.
+ *
+ * This library is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
+ * more details.
+ *
+ * You should have received a copy of the GNU General Public License along with
+ * this library; if not, see <https://www.gnu.org/licenses/>.
+ */
+
+#include <gio/gio.h>
+
+#include "purpleattachments.h"
+
+struct _PurpleAttachments {
+	GObject parent;
+
+	GPtrArray *attachments;
+};
+
+enum {
+	PROP_0,
+	PROP_ITEM_TYPE,
+	PROP_N_ITEMS,
+	N_PROPERTIES,
+};
+static GParamSpec *properties[N_PROPERTIES] = {NULL, };
+
+/******************************************************************************
+ * Helpers
+ *****************************************************************************/
+static gboolean
+purple_attachments_equal(gconstpointer a, gconstpointer b) {
+	PurpleAttachment *attachment1 = (gpointer)a;
+	PurpleAttachment *attachment2 = (gpointer)b;
+
+	return purple_attachment_equal(attachment1, attachment2);
+}
+
+/******************************************************************************
+ * GListModel Implementation
+ *****************************************************************************/
+static GType
+purple_attachments_get_item_type(G_GNUC_UNUSED GListModel *model) {
+	return PURPLE_TYPE_ATTACHMENT;
+}
+
+static guint
+purple_attachments_get_n_items(GListModel *model) {
+	PurpleAttachments *attachments = PURPLE_ATTACHMENTS(model);
+
+	return attachments->attachments->len;
+}
+
+static gpointer
+purple_attachments_get_item(GListModel *model, guint position) {
+	PurpleAttachments *attachments = PURPLE_ATTACHMENTS(model);
+	PurpleAttachment *attachment = NULL;
+
+	if(position < attachments->attachments->len) {
+		attachment = g_ptr_array_index(attachments->attachments, position);
+
+		if(PURPLE_IS_ATTACHMENT(attachment)) {
+			g_object_ref(attachment);
+		}
+	}
+
+	return attachment;
+}
+
+static void
+purple_attachments_list_model_iface_init(GListModelInterface *iface) {
+	iface->get_item_type = purple_attachments_get_item_type;
+	iface->get_n_items = purple_attachments_get_n_items;
+	iface->get_item = purple_attachments_get_item;
+}
+
+/******************************************************************************
+ * GObject Implementation
+ *****************************************************************************/
+G_DEFINE_FINAL_TYPE_WITH_CODE(PurpleAttachments,
+                              purple_attachments,
+                              G_TYPE_OBJECT,
+                              G_IMPLEMENT_INTERFACE(G_TYPE_LIST_MODEL,
+                                                    purple_attachments_list_model_iface_init))
+
+static void
+purple_attachments_finalize(GObject *obj) {
+	PurpleAttachments *attachments = PURPLE_ATTACHMENTS(obj);
+
+	g_clear_pointer(&attachments->attachments, g_ptr_array_unref);
+
+	G_OBJECT_CLASS(purple_attachments_parent_class)->finalize(obj);
+}
+
+static void
+purple_attachments_get_property(GObject *obj, guint param_id, GValue *value,
+                                GParamSpec *pspec)
+{
+	GListModel *model = G_LIST_MODEL(obj);
+
+	switch(param_id) {
+	case PROP_ITEM_TYPE:
+		g_value_set_gtype(value, g_list_model_get_item_type(model));
+		break;
+	case PROP_N_ITEMS:
+		g_value_set_uint(value, g_list_model_get_n_items(model));
+		break;
+	default:
+		G_OBJECT_WARN_INVALID_PROPERTY_ID(obj, param_id, pspec);
+		break;
+	}
+}
+
+static void
+purple_attachments_init(PurpleAttachments *attachments) {
+	attachments->attachments = g_ptr_array_new_full(0, g_object_unref);
+}
+
+static void
+purple_attachments_class_init(PurpleAttachmentsClass *klass) {
+	GObjectClass *obj_class = G_OBJECT_CLASS(klass);
+
+	obj_class->finalize = purple_attachments_finalize;
+	obj_class->get_property = purple_attachments_get_property;
+
+	/**
+	 * PurpleAttachments:item-type:
+	 *
+	 * The type of items. See [vfunc@Gio.ListModel.get_item_type].
+	 *
+	 * Since: 3.0
+	 */
+	properties[PROP_ITEM_TYPE] = g_param_spec_gtype(
+		"item-type", NULL, NULL,
+		PURPLE_TYPE_ATTACHMENT,
+		G_PARAM_READABLE | G_PARAM_STATIC_STRINGS);
+
+	/**
+	 * PurpleAttachments:n-items:
+	 *
+	 * The number of items. See [vfunc@Gio.ListModel.get_n_items].
+	 *
+	 * Since: 3.0
+	 */
+	properties[PROP_N_ITEMS] = g_param_spec_uint(
+		"n-items", NULL, NULL,
+		0, G_MAXUINT, 0,
+		G_PARAM_READABLE | G_PARAM_STATIC_STRINGS);
+
+	g_object_class_install_properties(obj_class, N_PROPERTIES, properties);
+}
+
+/******************************************************************************
+ * Public API
+ *****************************************************************************/
+gboolean
+purple_attachments_add_attachment(PurpleAttachments *attachments,
+                                  PurpleAttachment *attachment)
+{
+	GObject *obj = NULL;
+	gboolean found = FALSE;
+	guint len = 0;
+
+	g_return_val_if_fail(PURPLE_IS_ATTACHMENTS(attachments), FALSE);
+	g_return_val_if_fail(PURPLE_IS_ATTACHMENT(attachment), FALSE);
+
+	found = g_ptr_array_find_with_equal_func(attachments->attachments,
+	                                         attachment,
+	                                         purple_attachments_equal,
+	                                         NULL);
+
+	if(found) {
+		return FALSE;
+	}
+
+	/* Store length before adding to make the math easier to understand. */
+	len = attachments->attachments->len;
+
+	g_ptr_array_add(attachments->attachments, g_object_ref(attachment));
+
+	obj = G_OBJECT(attachments);
+	g_object_freeze_notify(obj);
+	g_list_model_items_changed(G_LIST_MODEL(attachments), len, 0, 1);
+	g_object_notify_by_pspec(obj, properties[PROP_N_ITEMS]);
+	g_object_thaw_notify(obj);
+
+	return TRUE;
+}
+
+PurpleAttachment *
+purple_attachments_find_with_id(PurpleAttachments *attachments, guint64 id) {
+	g_return_val_if_fail(PURPLE_IS_ATTACHMENTS(attachments), NULL);
+
+	for(guint i = 0; i < attachments->attachments->len; i++) {
+		PurpleAttachment *attachment = NULL;
+
+		attachment = g_ptr_array_index(attachments->attachments, i);
+
+		if(purple_attachment_get_id(attachment) == id) {
+			return attachment;
+		}
+	}
+
+	return NULL;
+}
+
+PurpleAttachments *
+purple_attachments_new(void) {
+	return g_object_new(PURPLE_TYPE_ATTACHMENTS, NULL);
+}
+
+gboolean
+purple_attachments_remove_attachment(PurpleAttachments *attachments,
+                                     PurpleAttachment *attachment)
+{
+	GObject *obj = NULL;
+	gboolean found = FALSE;
+	guint index = 0;
+
+	g_return_val_if_fail(PURPLE_IS_ATTACHMENTS(attachments), FALSE);
+	g_return_val_if_fail(PURPLE_IS_ATTACHMENT(attachment), FALSE);
+
+	found = g_ptr_array_find_with_equal_func(attachments->attachments,
+	                                         attachment,
+	                                         purple_attachments_equal,
+	                                         &index);
+	if(!found) {
+		return FALSE;
+	}
+
+	g_ptr_array_remove_index(attachments->attachments, index);
+
+	obj = G_OBJECT(attachments);
+	g_object_freeze_notify(obj);
+	g_list_model_items_changed(G_LIST_MODEL(attachments), index, 1, 0);
+	g_object_notify_by_pspec(obj, properties[PROP_N_ITEMS]);
+	g_object_thaw_notify(obj);
+
+	return TRUE;
+}
+
+void
+purple_attachments_remove_all(PurpleAttachments *attachments) {
+	guint removed = 0;
+
+	g_return_if_fail(PURPLE_IS_ATTACHMENTS(attachments));
+
+	removed = attachments->attachments->len;
+	g_ptr_array_remove_range(attachments->attachments, 0, removed);
+
+	if(removed > 0) {
+		GObject *obj = G_OBJECT(attachments);
+		g_object_freeze_notify(obj);
+		g_list_model_items_changed(G_LIST_MODEL(attachments), 0, removed, 0);
+		g_object_notify_by_pspec(obj, properties[PROP_N_ITEMS]);
+		g_object_thaw_notify(obj);
+	}
+}
diff --git a/libpurple/purpleattachments.h b/libpurple/purpleattachments.h
new file mode 100644
--- /dev/null
+++ b/libpurple/purpleattachments.h
@@ -0,0 +1,121 @@
+/*
+ * Purple - Internet Messaging Library
+ * Copyright (C) Pidgin Developers <devel@pidgin.im>
+ *
+ * Purple is the legal property of its developers, whose names are too numerous
+ * to list here. Please refer to the COPYRIGHT file distributed with this
+ * source distribution.
+ *
+ * This library is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License as published by the Free
+ * Software Foundation; either version 2 of the License, or (at your option)
+ * any later version.
+ *
+ * This library is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
+ * more details.
+ *
+ * You should have received a copy of the GNU General Public License along with
+ * this library; if not, see <https://www.gnu.org/licenses/>.
+ */
+
+#if !defined(PURPLE_GLOBAL_HEADER_INSIDE) && !defined(PURPLE_COMPILATION)
+# error "only <purple.h> may be included directly"
+#endif
+
+#ifndef PURPLE_ATTACHMENTS_H
+#define PURPLE_ATTACHMENTS_H
+
+#include <glib.h>
+#include <gio/gio.h>
+
+#include "purpleattachment.h"
+#include "purpleversion.h"
+
+G_BEGIN_DECLS
+
+#define PURPLE_TYPE_ATTACHMENTS (purple_attachments_get_type())
+
+/**
+ * PurpleAttachments:
+ *
+ * A collection of [class@Attachment]'s.
+ *
+ * This implements [iface@Gio.ListModel] and should be accessed via that API.
+ *
+ * Since: 3.0
+ */
+PURPLE_AVAILABLE_IN_3_0
+G_DECLARE_FINAL_TYPE(PurpleAttachments, purple_attachments, PURPLE, ATTACHMENTS,
+                     GObject)
+
+/**
+ * purple_attachments_add_attachment:
+ * @attachment: (transfer none): the attachment to add
+ *
+ * Adds an attachment to the collection.
+ *
+ * No sorting will be done, but if @attachment is already in the collection it
+ * will not be added again.
+ *
+ * Returns: true if the item was added; otherwise false.
+ *
+ * Since: 3.0
+ */
+PURPLE_AVAILABLE_IN_3_0
+gboolean purple_attachments_add_attachment(PurpleAttachments *attachments, PurpleAttachment *attachment);
+
+/**
+ * purple_attachments_find_with_id:
+ * @id: the id
+ *
+ * Looks for an attachment matching the given id.
+ *
+ * Returns: (transfer none) (nullable): The attachment if found.
+ *
+ * Since: 3.0
+ */
+PURPLE_AVAILABLE_IN_3_0
+PurpleAttachment *purple_attachments_find_with_id(PurpleAttachments *attachments, guint64 id);
+
+/**
+ * purple_attachments_new:
+ *
+ * Creates a new instance.
+ *
+ * Returns: (transfer full): The new instance.
+ *
+ * Since: 3.0
+ */
+PURPLE_AVAILABLE_IN_3_0
+PurpleAttachments *purple_attachments_new(void);
+
+/**
+ * purple_attachments_remove_attachment:
+ * @attachment: (transfer none): the attachment to remove
+ *
+ * Removes an attachment from the collection.
+ *
+ * If @attachment is not in the collection it will be ignored.
+ *
+ * Returns: true if the item was removed; otherwise false.
+ *
+ * Since: 3.0
+ */
+PURPLE_AVAILABLE_IN_3_0
+gboolean purple_attachments_remove_attachment(PurpleAttachments *attachments, PurpleAttachment *attachment);
+
+/**
+ * purple_attachments_remove_all:
+ *
+ * Removes all attachments from the collection.
+ *
+ * Since: 3.0
+ */
+PURPLE_AVAILABLE_IN_3_0
+void purple_attachments_remove_all(PurpleAttachments *attachments);
+
+G_END_DECLS
+
+#endif /* PURPLE_ATTACHMENTS_H */
diff --git a/libpurple/tests/meson.build b/libpurple/tests/meson.build
--- a/libpurple/tests/meson.build
+++ b/libpurple/tests/meson.build
@@ -3,6 +3,7 @@
     'account_option',
     'account_manager',
     'attachment',
+    'attachments',
     'authorization_request',
     'badge',
     'badge_manager',
diff --git a/libpurple/tests/test_attachments.c b/libpurple/tests/test_attachments.c
new file mode 100644
--- /dev/null
+++ b/libpurple/tests/test_attachments.c
@@ -0,0 +1,207 @@
+/*
+ * Purple - Internet Messaging Library
+ * Copyright (C) Pidgin Developers <devel@pidgin.im>
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2 of the License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, see <https://www.gnu.org/licenses/>.
+ */
+
+#include <glib.h>
+
+#include <purple.h>
+
+/******************************************************************************
+ * Callbacks
+ *****************************************************************************/
+static void
+test_purple_attachments_items_changed(GListModel *model,
+                                      G_GNUC_UNUSED guint position,
+                                      G_GNUC_UNUSED guint removed,
+                                      G_GNUC_UNUSED guint added,
+                                      gpointer data)
+{
+	guint *counter = data;
+
+	g_assert_true(PURPLE_IS_ATTACHMENTS(model));
+
+	*counter = *counter + 1;
+}
+
+static void
+test_purple_attachments_notify(GObject *self,
+                               G_GNUC_UNUSED GParamSpec *pspec,
+                               gpointer data)
+{
+	guint *counter = data;
+
+	g_assert_true(PURPLE_IS_ATTACHMENTS(self));
+
+	*counter = *counter + 1;
+}
+
+/******************************************************************************
+ * Tests
+ *****************************************************************************/
+static void
+test_purple_attachments_properties(void) {
+	PurpleAttachments *attachments = NULL;
+	GType item_type = G_TYPE_INVALID;
+	guint n_items = 0;
+
+	attachments = g_object_new(
+		PURPLE_TYPE_ATTACHMENTS,
+		NULL);
+
+	g_object_get(
+		attachments,
+		"item-type", &item_type,
+		"n-items", &n_items,
+		NULL);
+
+	g_assert_cmpuint(item_type, ==, PURPLE_TYPE_ATTACHMENT);
+	g_assert_cmpuint(n_items, ==, 0);
+
+	g_assert_finalize_object(attachments);
+}
+
+static void
+test_purple_attachments_double_add(void) {
+	PurpleAttachment *attachment = NULL;
+	PurpleAttachments *attachments = NULL;
+	gboolean success = FALSE;
+	guint items_changed_counter = 0;
+	guint n_items_counter = 0;
+
+	attachments = purple_attachments_new();
+	g_signal_connect(attachments, "items-changed",
+	                 G_CALLBACK(test_purple_attachments_items_changed),
+	                 &items_changed_counter);
+	g_signal_connect(attachments, "notify::n-items",
+	                 G_CALLBACK(test_purple_attachments_notify),
+	                 &n_items_counter);
+
+	attachment = purple_attachment_new(1337lu, "text/plain");
+
+	/* The first add. */
+	items_changed_counter = 0;
+	n_items_counter = 0;
+	success = purple_attachments_add_attachment(attachments, attachment);
+	g_assert_true(success);
+	g_assert_cmpuint(items_changed_counter, ==, 1);
+	g_assert_cmpuint(n_items_counter, ==, 1);
+
+	/* The double add. */
+	items_changed_counter = 0;
+	n_items_counter = 0;
+	success = purple_attachments_add_attachment(attachments, attachment);
+	g_assert_false(success);
+	g_assert_cmpuint(items_changed_counter, ==, 0);
+	g_assert_cmpuint(n_items_counter, ==, 0);
+
+	g_assert_finalize_object(attachments);
+	g_assert_finalize_object(attachment);
+}
+
+static void
+test_purple_attachments_double_remove(void) {
+	PurpleAttachment *attachment = NULL;
+	PurpleAttachments *attachments = NULL;
+	gboolean success = FALSE;
+	guint items_changed_counter = 0;
+	guint n_items_counter = 0;
+
+	attachments = purple_attachments_new();
+	g_signal_connect(attachments, "items-changed",
+	                 G_CALLBACK(test_purple_attachments_items_changed),
+	                 &items_changed_counter);
+	g_signal_connect(attachments, "notify::n-items",
+	                 G_CALLBACK(test_purple_attachments_notify),
+	                 &n_items_counter);
+
+	attachment = purple_attachment_new(1337lu, "text/plain");
+
+	/* The first add. */
+	items_changed_counter = 0;
+	n_items_counter = 0;
+	success = purple_attachments_add_attachment(attachments, attachment);
+	g_assert_true(success);
+	g_assert_cmpuint(items_changed_counter, ==, 1);
+	g_assert_cmpuint(n_items_counter, ==, 1);
+
+	/* The first remove. */
+	items_changed_counter = 0;
+	n_items_counter = 0;
+	success = purple_attachments_remove_attachment(attachments, attachment);
+	g_assert_true(success);
+	g_assert_cmpuint(items_changed_counter, ==, 1);
+	g_assert_cmpuint(n_items_counter, ==, 1);
+
+	/* The second remove. */
+	items_changed_counter = 0;
+	n_items_counter = 0;
+	success = purple_attachments_remove_attachment(attachments, attachment);
+	g_assert_false(success);
+	g_assert_cmpuint(items_changed_counter, ==, 0);
+	g_assert_cmpuint(n_items_counter, ==, 0);
+
+	g_assert_finalize_object(attachments);
+	g_assert_finalize_object(attachment);
+}
+
+static void
+test_purple_attachments_find_with_id(void) {
+	PurpleAttachment *attachment = NULL;
+	PurpleAttachment *found = NULL;
+	PurpleAttachments *attachments = NULL;
+
+	attachments = purple_attachments_new();
+
+	/* Search on the empty collection. */
+	found = purple_attachments_find_with_id(attachments, 1337lu);
+	g_assert_null(found);
+
+	/* Create an attachment and add it. */
+	attachment = purple_attachment_new(1337lu, "text/plain");
+	purple_attachments_add_attachment(attachments, attachment);
+
+	found = purple_attachments_find_with_id(attachments, 1337lu);
+	g_assert_true(PURPLE_IS_ATTACHMENT(found));
+	g_assert_true(found == attachment);
+
+	/* Search for an attachment that doesn't exist. */
+	found = purple_attachments_find_with_id(attachments, 42lu);
+	g_assert_null(found);
+
+	g_assert_finalize_object(attachments);
+	g_assert_finalize_object(attachment);
+}
+
+/******************************************************************************
+ * Main
+ *****************************************************************************/
+int
+main(int argc, char *argv[]) {
+	g_test_init(&argc, &argv, NULL);
+	g_test_set_nonfatal_assertions();
+
+	g_test_add_func("/attachments/properties",
+	                test_purple_attachments_properties);
+	g_test_add_func("/attachments/double-add",
+	                test_purple_attachments_double_add);
+	g_test_add_func("/attachments/double-remove",
+	                test_purple_attachments_double_remove);
+	g_test_add_func("/attachments/find-with-id",
+	                test_purple_attachments_find_with_id);
+
+	return g_test_run();
+}
