# HG changeset patch
# Node ID df0b6d1404de170550906e1cf240310af00ff381
# Parent  28a4480fe7463e4db57daa17a4aa81534678883b
diff --git a/libpurple/meson.build b/libpurple/meson.build
--- a/libpurple/meson.build
+++ b/libpurple/meson.build
@@ -18,6 +18,7 @@
 	'purpleauthorizationrequest.c',
 	'purpleavatar.c',
 	'purplebadge.c',
+	'purplebadges.c',
 	'purplechanneljoindetails.c',
 	'purpleconnectionerrorinfo.c',
 	'purplecontact.c',
@@ -107,6 +108,7 @@
 	'purpleauthorizationrequest.h',
 	'purpleavatar.h',
 	'purplebadge.h',
+	'purplebadges.h',
 	'purplechanneljoindetails.h',
 	'purpleconnectionerrorinfo.h',
 	'purplecontact.h',
diff --git a/libpurple/purplebadges.c b/libpurple/purplebadges.c
new file mode 100644
--- /dev/null
+++ b/libpurple/purplebadges.c
@@ -0,0 +1,236 @@
+/*
+ * 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 "purplebadges.h"
+
+#include "util.h"
+
+struct _PurpleBadges {
+	GObject parent;
+
+	GPtrArray *badges;
+};
+
+enum {
+	PROP_0,
+	PROP_ITEM_TYPE,
+	PROP_N_ITEMS,
+	N_PROPERTIES,
+};
+static GParamSpec *properties[N_PROPERTIES] = {NULL, };
+
+/******************************************************************************
+ * Helpers
+ *****************************************************************************/
+static gboolean
+purple_badges_equal(gconstpointer a, gconstpointer b) {
+	PurpleBadge *badge1 = (gpointer)a;
+	PurpleBadge *badge2 = (gpointer)b;
+
+	return purple_badge_equal(badge1, badge2);
+}
+
+static int
+purple_badges_compare(gconstpointer a, gconstpointer b) {
+	PurpleBadge *badge1 = (gpointer)a;
+	PurpleBadge *badge2 = (gpointer)b;
+
+	return purple_badge_compare(badge1, badge2);
+}
+
+/******************************************************************************
+ * GListModel Implementation
+ *****************************************************************************/
+static GType
+purple_badges_get_item_type(G_GNUC_UNUSED GListModel *model) {
+	return PURPLE_TYPE_BADGE;
+}
+
+static guint
+purple_badges_get_n_items(GListModel *model) {
+	PurpleBadges *badges = PURPLE_BADGES(model);
+
+	return badges->badges->len;
+}
+
+static gpointer
+purple_badges_get_item(GListModel *model, guint position) {
+	PurpleBadges *badges = PURPLE_BADGES(model);
+	PurpleBadge *badge = NULL;
+
+	if(position < badges->badges->len) {
+		badge = g_ptr_array_index(badges->badges, position);
+
+		if(PURPLE_IS_BADGE(badge)) {
+			g_object_ref(badge);
+		}
+	}
+
+	return badge;
+}
+
+static void
+purple_badges_list_model_init(GListModelInterface *iface) {
+	iface->get_item_type = purple_badges_get_item_type;
+	iface->get_n_items = purple_badges_get_n_items;
+	iface->get_item = purple_badges_get_item;
+}
+
+/******************************************************************************
+ * GObject Implementation
+ *****************************************************************************/
+G_DEFINE_FINAL_TYPE_WITH_CODE(PurpleBadges, purple_badges, G_TYPE_OBJECT,
+                              G_IMPLEMENT_INTERFACE(G_TYPE_LIST_MODEL,
+                                                    purple_badges_list_model_init))
+
+static void
+purple_badges_finalize(GObject *obj) {
+	PurpleBadges *badges = PURPLE_BADGES(obj);
+
+	g_clear_pointer(&badges->badges, g_ptr_array_unref);
+
+	G_OBJECT_CLASS(purple_badges_parent_class)->finalize(obj);
+}
+
+static void
+purple_badges_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_badges_init(PurpleBadges *badges) {
+	badges->badges = g_ptr_array_new_full(0, g_object_unref);
+}
+
+static void
+purple_badges_class_init(PurpleBadgesClass *klass) {
+	GObjectClass *obj_class = G_OBJECT_CLASS(klass);
+
+	obj_class->finalize = purple_badges_finalize;
+	obj_class->get_property = purple_badges_get_property;
+
+	/**
+	 * PurpleBadges:item-type:
+	 *
+	 * The type of items. See [iface@Gio.ListModel.get_item_type].
+	 *
+	 * Since: 3.0
+	 */
+	properties[PROP_ITEM_TYPE] = g_param_spec_gtype(
+		"item-type", NULL, NULL,
+		PURPLE_TYPE_BADGE,
+		G_PARAM_READABLE | G_PARAM_STATIC_STRINGS);
+
+	/**
+	 * PurpleBadges:n-items:
+	 *
+	 * The number of items. See [iface@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
+ *****************************************************************************/
+PurpleBadges *
+purple_badges_new(void) {
+	return g_object_new(PURPLE_TYPE_BADGES, NULL);
+}
+
+gboolean
+purple_badges_add_badge(PurpleBadges *badges, PurpleBadge *badge) {
+	gboolean found = FALSE;
+	guint len = 0;
+
+	g_return_val_if_fail(PURPLE_IS_BADGES(badges), FALSE);
+	g_return_val_if_fail(PURPLE_IS_BADGE(badge), FALSE);
+
+	found = g_ptr_array_find_with_equal_func(badges->badges, badge,
+	                                         purple_badges_equal, NULL);
+
+	if(found) {
+		return FALSE;
+	}
+
+	/* Store length before adding to make the math easier to understand. */
+	len = badges->badges->len;
+
+	g_ptr_array_add(badges->badges, g_object_ref(badge));
+	g_ptr_array_sort_values(badges->badges, purple_badges_compare);
+
+	g_list_model_items_changed(G_LIST_MODEL(badges), 0, len, len + 1);
+
+	return TRUE;
+}
+
+gboolean
+purple_badges_remove_badge(PurpleBadges *badges, const char *id) {
+	PurpleBadge *needle = NULL;
+	gboolean found = FALSE;
+	guint index = 0;
+	guint len = 0;
+
+	g_return_val_if_fail(PURPLE_IS_BADGES(badges), FALSE);
+	g_return_val_if_fail(!purple_strempty(id), FALSE);
+
+	/* We only need the id, but the other values can't be null. */
+	needle = purple_badge_new(id, 0, "folder-saved-search-symbolic", "🔍");
+
+	found = g_ptr_array_find_with_equal_func(badges->badges, needle,
+	                                         purple_badges_equal, &index);
+	g_clear_object(&needle);
+
+	if(!found) {
+		return FALSE;
+	}
+
+	len = badges->badges->len;
+
+	g_ptr_array_remove_index(badges->badges, index);
+
+	g_list_model_items_changed(G_LIST_MODEL(badges), 0, len, len - 1);
+
+	return TRUE;
+}
diff --git a/libpurple/purplebadges.h b/libpurple/purplebadges.h
new file mode 100644
--- /dev/null
+++ b/libpurple/purplebadges.h
@@ -0,0 +1,100 @@
+/*
+ * 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_BADGES_H
+#define PURPLE_BADGES_H
+
+#include <glib.h>
+#include <glib-object.h>
+
+#include "purplebadge.h"
+#include "purpleversion.h"
+
+G_BEGIN_DECLS
+
+/**
+ * PurpleBadges:
+ *
+ * #PurpleBadges is a collection of [class@Badge]s sorted by their priority.
+ * This is intended to be used on [class@ConversationMembers] and perhaps
+ * elsewhere to display information like "moderator", "admin", "broadcaster",
+ * etc. Items with the highest priority will be at the start of the collection.
+ *
+ * Since: 3.0
+ */
+
+#define PURPLE_TYPE_BADGES (purple_badges_get_type())
+
+PURPLE_AVAILABLE_IN_3_0
+G_DECLARE_FINAL_TYPE(PurpleBadges, purple_badges, PURPLE, BADGES, GObject)
+
+/**
+ * purple_badges_new:
+ *
+ * Creates a new badges collection.
+ *
+ * Returns: (transfer full): The new instance.
+ *
+ * Since: 3.0
+ */
+PURPLE_AVAILABLE_IN_3_0
+PurpleBadges *purple_badges_new(void);
+
+/**
+ * purple_badges_add_badge:
+ * @badges: The instance.
+ * @badge: The new badge to add.
+ *
+ * Adds @badge to @badges.
+ *
+ * If a badge with the same id of @badge already exists, @badge will not be
+ * added and %FALSE will be returned.
+ *
+ * Returns: %TRUE if @badge was added successfully, otherwise %FALSE if an
+ *          existing badge has the same id.
+ *
+ * Since: 3.0
+ */
+PURPLE_AVAILABLE_IN_3_0
+gboolean purple_badges_add_badge(PurpleBadges *badges, PurpleBadge *badge);
+
+/**
+ * purple_badges_remove_badge:
+ * @badges: The instance.
+ * @id: The id of the badge to remove.
+ *
+ * Removes the badge with an id of @id.
+ *
+ * Returns: %TRUE if a badge was found with an id of @id, otherwise %FALSE.
+ *
+ * Since: 3.0
+ */
+PURPLE_AVAILABLE_IN_3_0
+gboolean purple_badges_remove_badge(PurpleBadges *badges, const char *id);
+
+G_END_DECLS
+
+#endif /* PURPLE_BADGES_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_manager',
     'authorization_request',
     'badge',
+    'badges',
     'channel_join_details',
     'circular_buffer',
     'create_conversation_details',
diff --git a/libpurple/tests/test_badges.c b/libpurple/tests/test_badges.c
new file mode 100644
--- /dev/null
+++ b/libpurple/tests/test_badges.c
@@ -0,0 +1,267 @@
+/*
+ * 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_badges_items_changed(GListModel *model, guint position,
+                                 G_GNUC_UNUSED guint removed,
+                                 G_GNUC_UNUSED guint added, gpointer data)
+{
+	guint *counter = data;
+
+	g_assert_true(PURPLE_IS_BADGES(model));
+
+	/* Due to the auto sort, the list is always completely changed, so position
+	 * should always be zero.
+	 */
+	g_assert_cmpuint(position, ==, 0);
+
+	*counter = *counter + 1;
+}
+
+/******************************************************************************
+ * Tests
+ *****************************************************************************/
+static void
+test_purple_badges_properties(void) {
+	PurpleBadges *badges;
+	GType item_type = G_TYPE_INVALID;
+	guint n_items = 0;
+
+	badges = g_object_new(
+		PURPLE_TYPE_BADGES,
+		NULL);
+
+	g_object_get(
+		badges,
+		"item-type", &item_type,
+		"n-items", &n_items,
+		NULL);
+
+	g_assert_cmpuint(item_type, ==, PURPLE_TYPE_BADGE);
+	g_assert_cmpuint(n_items, ==, 0);
+
+	g_assert_finalize_object(badges);
+}
+
+static void
+test_purple_badges_auto_sort(void) {
+	PurpleBadge *high = NULL;
+	PurpleBadge *medium = NULL;
+	PurpleBadge *low = NULL;
+	PurpleBadge *badge = NULL;
+	PurpleBadges *badges = NULL;
+	gboolean success = FALSE;
+	guint counter = 0;
+	guint n_items = 0;
+
+	/* This test will add badges with 3 different priorities; high, medium, and
+	 * low. medium will be added first, then low, then high. The order will be
+	 * checked after each add a counter will be used to see if the
+	 * "items-changed" signal was called once per addition.
+	 *
+	 * Once that's all good, low will be removed, then high, then medium, and
+	 * the order and "items-changed" signal counter will be checked as well.
+	 */
+
+	badges = purple_badges_new();
+	g_signal_connect(badges, "items-changed",
+	                 G_CALLBACK(test_purple_badges_items_changed), &counter);
+
+	high = purple_badge_new("high", 1000, "icon-name", "+");
+	medium = purple_badge_new("medium", 0, "icon-name", "=");
+	low = purple_badge_new("low", -1000, "icon-name", "-");
+
+	/* Start by adding medium. */
+	counter = 0;
+	success = purple_badges_add_badge(badges, medium);
+	g_assert_true(success);
+	g_assert_cmpuint(counter, ==, 1);
+
+	n_items = g_list_model_get_n_items(G_LIST_MODEL(badges));
+	g_assert_cmpuint(n_items, ==, 1);
+
+	/* Add low. */
+	counter = 0;
+	success = purple_badges_add_badge(badges, low);
+	g_assert_true(success);
+	g_assert_cmpuint(counter, ==, 1);
+
+	n_items = g_list_model_get_n_items(G_LIST_MODEL(badges));
+	g_assert_cmpuint(n_items, ==, 2);
+
+	badge = g_list_model_get_item(G_LIST_MODEL(badges), 0);
+	g_assert_true(badge == medium);
+	g_clear_object(&badge);
+
+	badge = g_list_model_get_item(G_LIST_MODEL(badges), 1);
+	g_assert_true(badge == low);
+	g_clear_object(&badge);
+
+	/* Add High. */
+	counter = 0;
+	success = purple_badges_add_badge(badges, high);
+	g_assert_true(success);
+	g_assert_cmpuint(counter, ==, 1);
+
+	n_items = g_list_model_get_n_items(G_LIST_MODEL(badges));
+	g_assert_cmpuint(n_items, ==, 3);
+
+	badge = g_list_model_get_item(G_LIST_MODEL(badges), 0);
+	g_assert(badge == high);
+	g_clear_object(&badge);
+
+	badge = g_list_model_get_item(G_LIST_MODEL(badges), 1);
+	g_assert(badge == medium);
+	g_clear_object(&badge);
+
+	badge = g_list_model_get_item(G_LIST_MODEL(badges), 2);
+	g_assert(badge == low);
+	g_clear_object(&badge);
+
+	/* Remove low. */
+	counter = 0;
+	success = purple_badges_remove_badge(badges, "low");
+	g_assert_true(success);
+	g_assert_cmpuint(counter, ==, 1);
+
+	n_items = g_list_model_get_n_items(G_LIST_MODEL(badges));
+	g_assert_cmpuint(n_items, ==, 2);
+
+	badge = g_list_model_get_item(G_LIST_MODEL(badges), 0);
+	g_assert_true(badge == high);
+	g_clear_object(&badge);
+
+	badge = g_list_model_get_item(G_LIST_MODEL(badges), 1);
+	g_assert_true(badge == medium);
+	g_clear_object(&badge);
+
+	/* Remove high. */
+	counter = 0;
+	success = purple_badges_remove_badge(badges, "high");
+	g_assert_true(success);
+	g_assert_cmpuint(counter, ==, 1);
+
+	n_items = g_list_model_get_n_items(G_LIST_MODEL(badges));
+	g_assert_cmpuint(n_items, ==, 1);
+
+	badge = g_list_model_get_item(G_LIST_MODEL(badges), 0);
+	g_assert_true(badge == medium);
+	g_clear_object(&badge);
+
+	/* Remove medium. */
+	counter = 0;
+	success = purple_badges_remove_badge(badges, "medium");
+	g_assert_true(success);
+	g_assert_cmpuint(counter, ==, 1);
+
+	n_items = g_list_model_get_n_items(G_LIST_MODEL(badges));
+	g_assert_cmpuint(n_items, ==, 0);
+
+	/* Cleanup. */
+	g_assert_finalize_object(badges);
+	g_assert_finalize_object(high);
+	g_assert_finalize_object(medium);
+	g_assert_finalize_object(low);
+}
+
+static void
+test_purple_badges_double_add(void) {
+	PurpleBadge *badge = NULL;
+	PurpleBadges *badges = NULL;
+	gboolean success = FALSE;
+	guint counter = 0;
+
+	badges = purple_badges_new();
+	g_signal_connect(badges, "items-changed",
+	                 G_CALLBACK(test_purple_badges_items_changed), &counter);
+
+	badge = purple_badge_new("double-add", 0, "icon", "i");
+
+	/* The first add. */
+	counter = 0;
+	success = purple_badges_add_badge(badges, badge);
+	g_assert_true(success);
+	g_assert_cmpuint(counter, ==, 1);
+
+	/* The double add. */
+	counter = 0;
+	success = purple_badges_add_badge(badges, badge);
+	g_assert_false(success);
+	g_assert_cmpuint(counter, ==, 0);
+
+	g_assert_finalize_object(badges);
+	g_assert_finalize_object(badge);
+}
+
+static void
+test_purple_badges_double_remove(void) {
+	PurpleBadge *badge = NULL;
+	PurpleBadges *badges = NULL;
+	gboolean success = FALSE;
+	guint counter = 0;
+
+	badges = purple_badges_new();
+	g_signal_connect(badges, "items-changed",
+	                 G_CALLBACK(test_purple_badges_items_changed), &counter);
+
+	badge = purple_badge_new("double-remove", 0, "icon", "i");
+
+	/* The first add. */
+	counter = 0;
+	success = purple_badges_add_badge(badges, badge);
+	g_assert_true(success);
+	g_assert_cmpuint(counter, ==, 1);
+
+	/* The first remove. */
+	counter = 0;
+	success = purple_badges_remove_badge(badges, "double-remove");
+	g_assert_true(success);
+	g_assert_cmpuint(counter, ==, 1);
+
+	/* The second remove. */
+	counter = 0;
+	success = purple_badges_remove_badge(badges, "double-remove");
+	g_assert_false(success);
+	g_assert_cmpuint(counter, ==, 0);
+
+	g_assert_finalize_object(badges);
+	g_assert_finalize_object(badge);
+}
+
+/******************************************************************************
+ * Main
+ *****************************************************************************/
+int
+main(int argc, char *argv[]) {
+	g_test_init(&argc, &argv, NULL);
+	g_test_set_nonfatal_assertions();
+
+	g_test_add_func("/badges/properties", test_purple_badges_properties);
+	g_test_add_func("/badges/auto-sort", test_purple_badges_auto_sort);
+	g_test_add_func("/badges/double-add", test_purple_badges_double_add);
+	g_test_add_func("/badges/double-remove", test_purple_badges_double_remove);
+
+	return g_test_run();
+}
