# HG changeset patch
# Node ID eb4660cc43da
# Parent  e8302a55fddb
diff --git a/libpurple/data/im.pidgin.Purple.PresenceManager.gschema.xml b/libpurple/data/im.pidgin.Purple.PresenceManager.gschema.xml
--- a/libpurple/data/im.pidgin.Purple.PresenceManager.gschema.xml
+++ b/libpurple/data/im.pidgin.Purple.PresenceManager.gschema.xml
@@ -59,4 +59,27 @@
       </description>
     </key>
   </schema>
+
+  <schema path="/purple/presence-manager/" id="im.pidgin.Purple.PresenceManager">
+    <key name="active" type="s">
+      <default>""</default>
+      <summary>The id of the active presence</summary>
+      <description>
+        The name of the active presence that the libpurple user is currently
+        using.
+
+        The presence itself should be have a path of `/presences/%name%` inside
+        the im.pidgin.Purple.PresenceManager settings.
+      </description>
+    </key>
+
+    <key name="presences" type="as">
+      <default>[]</default>
+      <summary>A list of the ids of all saved presences</summary>
+      <description>
+        The list is used to find all of the saved presences as they are
+        relocatable settings.
+      </description>
+    </key>
+  </schema>
 </schemalist>
diff --git a/libpurple/meson.build b/libpurple/meson.build
--- a/libpurple/meson.build
+++ b/libpurple/meson.build
@@ -75,6 +75,7 @@
 	'purpleperson.c',
 	'purpleplugininfo.c',
 	'purplepresence.c',
+	'purplepresencemanager.c',
 	'purpleprotocol.c',
 	'purpleprotocolactions.c',
 	'purpleprotocolchat.c',
@@ -200,6 +201,7 @@
 	'purpleperson.h',
 	'purpleplugininfo.h',
 	'purplepresence.h',
+	'purplepresencemanager.h',
 	'purpleprotocol.h',
 	'purpleprotocolactions.h',
 	'purpleprotocolchat.h',
@@ -242,6 +244,11 @@
 	'xmlnode.h',
 ]
 
+purple_private_headers = [
+	'purpleprivate.h',
+	'purplesavedpresenceprivate.h',
+]
+
 purple_request_headers = [
 	'request/purplerequestfield.h',
 	'request/purplerequestfieldaccount.h',
diff --git a/libpurple/purplepresencemanager.h b/libpurple/purplepresencemanager.h
new file mode 100644
--- /dev/null
+++ b/libpurple/purplepresencemanager.h
@@ -0,0 +1,140 @@
+/*
+ * 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 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_PRESENCE_MANAGER_H
+#define PURPLE_PRESENCE_MANAGER_H
+
+#include <glib.h>
+#include <glib-object.h>
+
+#include "purplesavedpresence.h"
+
+G_BEGIN_DECLS
+
+#define PURPLE_TYPE_PRESENCE_MANAGER purple_presence_manager_get_type()
+G_DECLARE_FINAL_TYPE(PurplePresenceManager, purple_presence_manager, PURPLE,
+                     PRESENCE_MANAGER, GObject)
+
+/**
+ * PurplePresenceManager:
+ *
+ * A manager for [class@SavedPresence]'s.
+ *
+ * Since: 3.0.0
+ */
+
+/**
+ * purple_presence_manager_new:
+ * @filename: (nullable): An optional filename where settings will be stored.
+ *
+ * Creates a new presence manager instance.
+ *
+ * If @filename is %NULL, settings will not be stored to disk.
+ *
+ * Returns: (transfer full): The new instance.
+ *
+ * Since: 3.0.0
+ */
+PurplePresenceManager *purple_presence_manager_new(const char *filename);
+
+/**
+ * purple_presence_manager_get_filename:
+ * @manager: The instance.
+ *
+ * Gets the file name that @manager is using for storage.
+ *
+ * Returns: (nullable): The filename where settings are being saved to disk or
+ *          %NULL.
+ *
+ * Since: 3.0.0
+ */
+const char *purple_presence_manager_get_filename(PurplePresenceManager *manager);
+
+/**
+ * purple_presence_manager_get_active:
+ * @manager: The instance.
+ *
+ * Gets the active [class@SavedPresence] from @manager.
+ *
+ * Returns: (transfer none) (nullable): The active presence if there is one,
+ *          otherwise %NULL.
+ *
+ * Since: 3.0.0
+ */
+PurpleSavedPresence *purple_presence_manager_get_active(PurplePresenceManager *manager);
+
+/**
+ * purple_presence_manager_set_active:
+ * @manager: The instance.
+ * @id: The id of the presence to make active.
+ *
+ * Sets the active presence to the [class@SavedPresence] with a
+ * [property@SavedPresence:id] of @id.
+ *
+ * If @manager doesn't know of a presence with an id of @id, %FALSE will be
+ * returned to indicate failure.
+ *
+ * Returns: %TRUE if a presence was found with an id of @id and made active,
+ *          otherwise %FALSE.
+ *
+ * Since: 3.0.0
+ */
+gboolean purple_presence_manager_set_active(PurplePresenceManager *manager, const char *id);
+
+/**
+ * purple_presence_manager_create:
+ * @manager: The instance.
+ *
+ * Creates a new [class@SavedPresence] that is automatically added to @manager.
+ *
+ * This is the only way to add a [class@SavedPresence] to @manager. This means
+ * that user interfaces will need to call this before displaying an editor to
+ * create a [class@SavedPresence].
+ *
+ * Returns: (transfer full): The new [class@SavedPresence].
+ *
+ * Since: 3.0.0
+ */
+PurpleSavedPresence *purple_presence_manager_create(PurplePresenceManager *manager);
+
+/**
+ * purple_presence_manager_remove:
+ * @manager: The instance.
+ * @id: The id of the [class@SavedPresence].
+ *
+ * Attempts to remove the [class@SavedPresence] identified by @id from
+ * @manager.
+ *
+ * Returns: %TRUE if a [class@SavedPresence] was found with @id and removed,
+ *          otherwise %FALSE.
+ *
+ * Since: 3.0.0
+ */
+gboolean purple_presence_manager_remove(PurplePresenceManager *manager, const char *id);
+
+G_END_DECLS
+
+#endif /* PURPLE_PRESENCE_MANAGER_H */
diff --git a/libpurple/purplepresencemanager.c b/libpurple/purplepresencemanager.c
new file mode 100644
--- /dev/null
+++ b/libpurple/purplepresencemanager.c
@@ -0,0 +1,516 @@
+/*
+ * 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 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/>.
+ */
+#define G_SETTINGS_ENABLE_BACKEND
+#include <gio/gsettingsbackend.h>
+
+#include "purplepresencemanager.h"
+#include "purplesavedpresenceprivate.h"
+#include "util.h"
+
+#define MANAGER_SCHEMA_ID "im.pidgin.Purple.PresenceManager"
+#define PRESENCE_SCHEMA_ID "im.pidgin.Purple.SavedPresence"
+#define MANAGER_PATH "/purple/presence-manager"
+
+enum {
+	PROP_0,
+	PROP_FILENAME,
+	PROP_ACTIVE,
+	N_PROPERTIES,
+};
+static GParamSpec *properties[N_PROPERTIES] = {NULL, };
+
+enum {
+	SIG_ADDED,
+	SIG_REMOVED,
+	N_SIGNALS,
+};
+static guint signals[N_SIGNALS] = {};
+
+struct _PurplePresenceManager {
+	GObject parent;
+
+	char *filename;
+	GSettingsBackend *backend;
+	GSettings *settings;
+
+	PurpleSavedPresence *active;
+	GPtrArray *presences;
+};
+
+/******************************************************************************
+ * Helpers
+ *****************************************************************************/
+static inline char *
+purple_presence_manager_path_for_presence(const char *id) {
+	return g_strdup_printf("%s/presences/%s/", MANAGER_PATH, id);
+}
+
+static gboolean
+purple_presence_manager_id_equal(gconstpointer a, gconstpointer b) {
+	PurpleSavedPresence *presence_a = PURPLE_SAVED_PRESENCE((gpointer)a);
+	PurpleSavedPresence *presence_b = PURPLE_SAVED_PRESENCE((gpointer)b);
+	const char *id_a = NULL;
+	const char *id_b = NULL;
+
+	id_a = purple_saved_presence_get_id(presence_a);
+	id_b = purple_saved_presence_get_id(presence_b);
+
+	return purple_strequal(id_a, id_b);
+}
+
+static PurpleSavedPresence *
+purple_presence_manager_find_with_id(PurplePresenceManager *manager,
+                                     const char *id, guint *_index)
+{
+	PurpleSavedPresence *needle = NULL;
+	PurpleSavedPresence *ret = NULL;
+	gboolean found = FALSE;
+	guint index = 0;
+
+	g_return_val_if_fail(PURPLE_IS_PRESENCE_MANAGER(manager), NULL);
+	g_return_val_if_fail(!purple_strempty(id), NULL);
+
+	needle = g_object_new(PURPLE_TYPE_SAVED_PRESENCE, "id", id, NULL);
+
+	found = g_ptr_array_find_with_equal_func(manager->presences, needle,
+	                                         purple_presence_manager_id_equal,
+	                                         &index);
+
+	if(found) {
+		ret = g_ptr_array_index(manager->presences, index);
+		if(_index) {
+			*_index = index;
+		}
+	}
+
+	g_clear_object(&needle);
+
+	return ret;
+}
+
+static void
+purple_presence_manager_update_presences_setting(PurplePresenceManager *manager)
+{
+	GStrvBuilder *builder = NULL;
+	GStrv presences = NULL;
+
+	builder = g_strv_builder_new();
+	for(guint i = 0; i < manager->presences->len; i++) {
+		PurpleSavedPresence *presence = NULL;
+		const char *id = NULL;
+
+		presence = g_ptr_array_index(manager->presences, i);
+		id = purple_saved_presence_get_id(presence);
+		g_strv_builder_add(builder, id);
+	}
+
+	presences = g_strv_builder_end(builder);
+	g_settings_set_strv(manager->settings, "presences",
+	                    (const char * const *)presences);
+	g_strfreev(presences);
+	g_strv_builder_unref(builder);
+}
+
+static inline void
+purple_presence_manager_add(PurplePresenceManager *manager,
+                            PurpleSavedPresence *presence)
+{
+	g_return_if_fail(PURPLE_IS_PRESENCE_MANAGER(manager));
+	g_return_if_fail(PURPLE_IS_SAVED_PRESENCE(presence));
+
+	g_ptr_array_add(manager->presences, g_object_ref(presence));
+	purple_presence_manager_update_presences_setting(manager);
+
+	g_signal_emit(manager, signals[SIG_ADDED], 0, presence);
+	g_list_model_items_changed(G_LIST_MODEL(manager),
+	                           manager->presences->len - 1, 0, 1);
+}
+
+static void
+purple_presence_manager_set_filename(PurplePresenceManager *manager,
+                                     const char *filename)
+{
+	g_return_if_fail(PURPLE_IS_PRESENCE_MANAGER(manager));
+
+	if(!purple_strequal(filename, manager->filename)) {
+		g_free(manager->filename);
+		manager->filename = g_strdup(filename);
+
+		g_object_notify_by_pspec(G_OBJECT(manager), properties[PROP_FILENAME]);
+	}
+}
+
+static void
+purple_presence_manager_load_saved_presences(PurplePresenceManager *manager) {
+	GStrv ids = NULL;
+
+	ids = g_settings_get_strv(manager->settings, "presences");
+	for(int i = 0; ids[i] != NULL; i++) {
+		PurpleSavedPresence *presence = NULL;
+		GSettings *settings = NULL;
+		char *path = NULL;
+
+		path = purple_presence_manager_path_for_presence(ids[i]);
+		settings = g_settings_new_with_backend_and_path(PRESENCE_SCHEMA_ID,
+		                                                manager->backend,
+		                                                path);
+		g_clear_pointer(&path, g_free);
+
+		presence = g_object_new(
+			PURPLE_TYPE_SAVED_PRESENCE,
+			"id", ids[i],
+			"settings", settings,
+			NULL);
+
+		purple_presence_manager_add(manager, presence);
+
+		g_clear_object(&presence);
+	}
+
+	g_strfreev(ids);
+}
+
+/******************************************************************************
+ * GListModel Implementation
+ *****************************************************************************/
+static GType
+purple_presence_manager_get_item_type(G_GNUC_UNUSED GListModel *list) {
+	return PURPLE_TYPE_SAVED_PRESENCE;
+}
+
+static guint
+purple_presence_manager_get_n_items(GListModel *list) {
+	PurplePresenceManager *manager = PURPLE_PRESENCE_MANAGER(list);
+
+	return manager->presences->len;
+}
+
+static gpointer
+purple_presence_manager_get_item(GListModel *list, guint position) {
+	PurplePresenceManager *manager = PURPLE_PRESENCE_MANAGER(list);
+	PurpleSavedPresence *presence = NULL;
+
+	if(position < manager->presences->len) {
+		presence = g_ptr_array_index(manager->presences, position);
+		g_object_ref(presence);
+	}
+
+	return presence;
+}
+
+static void
+purple_presence_manager_list_model_init(GListModelInterface *iface) {
+	iface->get_item_type = purple_presence_manager_get_item_type;
+	iface->get_n_items = purple_presence_manager_get_n_items;
+	iface->get_item = purple_presence_manager_get_item;
+}
+
+/******************************************************************************
+ * GObject Implementation
+ *****************************************************************************/
+G_DEFINE_TYPE_EXTENDED(PurplePresenceManager, purple_presence_manager,
+                       G_TYPE_OBJECT, G_TYPE_FLAG_FINAL,
+                       G_IMPLEMENT_INTERFACE(G_TYPE_LIST_MODEL,
+                                             purple_presence_manager_list_model_init))
+
+static void
+purple_presence_manager_constructed(GObject *obj) {
+	PurplePresenceManager *manager = PURPLE_PRESENCE_MANAGER(obj);
+	char *active_id = NULL;
+
+	G_OBJECT_CLASS(purple_presence_manager_parent_class)->constructed(obj);
+
+	if(manager->filename == NULL) {
+		manager->backend = g_memory_settings_backend_new();
+	} else {
+		manager->backend = g_keyfile_settings_backend_new(manager->filename,
+		                                                  "/", NULL);
+	}
+
+	manager->settings = g_settings_new_with_backend(MANAGER_SCHEMA_ID,
+	                                                manager->backend);
+
+	purple_presence_manager_load_saved_presences(manager);
+
+	active_id = g_settings_get_string(manager->settings, "active");
+	if(!purple_strempty(active_id)) {
+		if(!purple_presence_manager_set_active(manager, active_id)) {
+			g_warning("failed to set the active saved presence to %s",
+			          active_id);
+		}
+	}
+	g_clear_pointer(&active_id, g_free);
+}
+
+static void
+purple_presence_manager_finalize(GObject *obj) {
+	PurplePresenceManager *manager = PURPLE_PRESENCE_MANAGER(obj);
+
+	g_clear_pointer(&manager->filename, g_free);
+
+	g_clear_object(&manager->active);
+	if(manager->presences != NULL) {
+		g_ptr_array_free(manager->presences, TRUE);
+		manager->presences = NULL;
+	}
+
+	g_clear_object(&manager->backend);
+	g_clear_object(&manager->settings);
+
+	G_OBJECT_CLASS(purple_presence_manager_parent_class)->finalize(obj);
+}
+
+static void
+purple_presence_manager_get_property(GObject *obj, guint param_id,
+                                     GValue *value, GParamSpec *pspec)
+{
+	PurplePresenceManager *manager = PURPLE_PRESENCE_MANAGER(obj);
+
+	switch(param_id) {
+		case PROP_FILENAME:
+			g_value_set_string(value,
+			                   purple_presence_manager_get_filename(manager));
+			break;
+		case PROP_ACTIVE:
+			g_value_set_object(value,
+			                   purple_presence_manager_get_active(manager));
+			break;
+		default:
+			G_OBJECT_WARN_INVALID_PROPERTY_ID(obj, param_id, pspec);
+			break;
+	}
+}
+
+static void
+purple_presence_manager_set_property(GObject *obj, guint param_id,
+                                     const GValue *value, GParamSpec *pspec)
+{
+	PurplePresenceManager *manager = PURPLE_PRESENCE_MANAGER(obj);
+
+	switch(param_id) {
+		case PROP_FILENAME:
+			purple_presence_manager_set_filename(manager,
+			                                     g_value_get_string(value));
+			break;
+		default:
+			G_OBJECT_WARN_INVALID_PROPERTY_ID(obj, param_id, pspec);
+			break;
+	}
+}
+
+static void
+purple_presence_manager_init(PurplePresenceManager *manager) {
+	manager->presences = g_ptr_array_new_full(0,
+	                                          (GDestroyNotify)g_object_unref);
+}
+
+static void
+purple_presence_manager_class_init(PurplePresenceManagerClass *klass) {
+	GObjectClass *obj_class = G_OBJECT_CLASS(klass);
+
+	obj_class->constructed = purple_presence_manager_constructed;
+	obj_class->finalize = purple_presence_manager_finalize;
+	obj_class->get_property = purple_presence_manager_get_property;
+	obj_class->set_property = purple_presence_manager_set_property;
+
+	/**
+	 * PurplePresenceManager:filename:
+	 *
+	 * The filename where settings should be stored. If this is %NULL settings
+	 * will not be saved to disk.
+	 *
+	 * Since: 3.0.0
+	 */
+	properties[PROP_FILENAME] = g_param_spec_string(
+		"filename", "filename",
+		"The name of the file where settings should be stored.",
+		NULL,
+		G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS);
+
+	/**
+	 * PurplePresenceManager:active-presence:
+	 *
+	 * The [class@SavedPresence] that is currently active.
+	 *
+	 * Since: 3.0.0
+	 */
+	properties[PROP_ACTIVE] = g_param_spec_object(
+		"active-presence", "active-presence",
+		"The active presence.",
+		PURPLE_TYPE_SAVED_PRESENCE,
+		G_PARAM_READABLE | G_PARAM_STATIC_STRINGS);
+
+	g_object_class_install_properties(obj_class, N_PROPERTIES, properties);
+
+	/**
+	 * PurplePresenceManager::added:
+	 * @self: The instance.
+	 * @presence: The presence that was added.
+	 *
+	 * Emitted when a [class@SavedPresence] is successfully added to the
+	 * 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_SAVED_PRESENCE);
+
+	/**
+	 * PurplePresenceManager::removed:
+	 * @self: The instance.
+	 * @presence: The presence that was removed.
+	 *
+	 * Emitted when a [class@SavedPresence] is successfully removed from the
+	 * 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_SAVED_PRESENCE);
+}
+
+/******************************************************************************
+ * Public API
+ *****************************************************************************/
+PurplePresenceManager *
+purple_presence_manager_new(const char *filename) {
+	return g_object_new(PURPLE_TYPE_PRESENCE_MANAGER,
+	                    "filename", filename,
+	                    NULL);
+}
+
+const char *
+purple_presence_manager_get_filename(PurplePresenceManager *manager) {
+	g_return_val_if_fail(PURPLE_IS_PRESENCE_MANAGER(manager), NULL);
+
+	return manager->filename;
+}
+
+PurpleSavedPresence *
+purple_presence_manager_get_active(PurplePresenceManager *manager) {
+	g_return_val_if_fail(PURPLE_IS_PRESENCE_MANAGER(manager), NULL);
+
+	return manager->active;
+}
+
+gboolean
+purple_presence_manager_set_active(PurplePresenceManager *manager,
+                                   const char *id)
+{
+	PurpleSavedPresence *presence = NULL;
+
+	g_return_val_if_fail(PURPLE_IS_PRESENCE_MANAGER(manager), FALSE);
+
+	if(id != NULL) {
+		presence = purple_presence_manager_find_with_id(manager, id, NULL);
+		if(!PURPLE_IS_SAVED_PRESENCE(presence)) {
+			return FALSE;
+		}
+	}
+
+	if(g_set_object(&manager->active, presence)) {
+		g_object_notify_by_pspec(G_OBJECT(manager), properties[PROP_ACTIVE]);
+
+		g_settings_set_string(manager->settings, "active", id);
+	}
+
+	return TRUE;
+}
+
+PurpleSavedPresence *
+purple_presence_manager_create(PurplePresenceManager *manager) {
+	PurpleSavedPresence *presence = NULL;
+	GSettings *settings = NULL;
+	char *id = NULL;
+	char *path = NULL;
+
+	g_return_val_if_fail(PURPLE_IS_PRESENCE_MANAGER(manager), FALSE);
+
+	id = g_uuid_string_random();
+	path = purple_presence_manager_path_for_presence(id);
+	settings = g_settings_new_with_backend_and_path(PRESENCE_SCHEMA_ID,
+	                                                manager->backend, path);
+	g_free(path);
+
+	presence = g_object_new(
+		PURPLE_TYPE_SAVED_PRESENCE,
+		"id", id,
+		"settings", settings,
+		NULL);
+
+	g_clear_pointer(&id, g_free);
+	g_clear_object(&settings);
+
+	purple_presence_manager_add(manager, presence);
+
+	return presence;
+}
+
+gboolean
+purple_presence_manager_remove(PurplePresenceManager *manager, const char *id)
+{
+	PurpleSavedPresence *presence = NULL;
+	guint index;
+
+	g_return_val_if_fail(PURPLE_IS_PRESENCE_MANAGER(manager), FALSE);
+	g_return_val_if_fail(!purple_strempty(id), FALSE);
+
+	presence = purple_presence_manager_find_with_id(manager, id, &index);
+	if(!PURPLE_IS_SAVED_PRESENCE(presence)) {
+		return FALSE;
+	}
+
+	/* Add a reference to the presence so we can emit the signal that it was
+	 * removed.
+	 */
+	g_object_ref(presence);
+
+	g_ptr_array_remove_index(manager->presences, index);
+	purple_presence_manager_update_presences_setting(manager);
+	purple_saved_presence_reset(presence);
+
+	g_signal_emit(manager, signals[SIG_REMOVED], 0, presence);
+	g_list_model_items_changed(G_LIST_MODEL(manager), index, 1, 0);
+
+	/* Unref the presence as we are now done with it. */
+	g_object_unref(presence);
+
+	return TRUE;
+}
diff --git a/libpurple/purplesavedpresence.c b/libpurple/purplesavedpresence.c
--- a/libpurple/purplesavedpresence.c
+++ b/libpurple/purplesavedpresence.c
@@ -24,6 +24,7 @@
 
 #include "purpleenums.h"
 #include "purplesavedpresence.h"
+#include "purplesavedpresenceprivate.h"
 #include "util.h"
 
 struct _PurpleSavedPresence {
@@ -198,6 +199,15 @@
 }
 
 static void
+purple_saved_presence_dispose(GObject *obj) {
+	PurpleSavedPresence *presence = PURPLE_SAVED_PRESENCE(obj);
+
+	g_clear_object(&presence->settings);
+
+	G_OBJECT_CLASS(purple_saved_presence_parent_class)->dispose(obj);
+}
+
+static void
 purple_saved_presence_set_property(GObject *obj, guint param_id,
                                    const GValue *value, GParamSpec *pspec)
 {
@@ -307,6 +317,7 @@
 	GObjectClass *obj_class = G_OBJECT_CLASS(klass);
 
 	obj_class->constructed = purple_saved_presence_constructed;
+	obj_class->dispose = purple_saved_presence_dispose;
 	obj_class->finalize = purple_saved_presence_finalize;
 	obj_class->get_property = purple_saved_presence_get_property;
 	obj_class->set_property = purple_saved_presence_set_property;
@@ -426,6 +437,27 @@
 }
 
 /******************************************************************************
+ * Private API
+ *****************************************************************************/
+void
+purple_saved_presence_reset(PurpleSavedPresence *presence) {
+	GSettingsSchema *schema = NULL;
+	GStrv keys = NULL;
+
+	g_return_if_fail(PURPLE_IS_SAVED_PRESENCE(presence));
+
+	g_object_get(presence->settings, "settings-schema", &schema, NULL);
+
+	keys = g_settings_schema_list_keys(schema);
+	for(int i = 0; keys[i] != NULL; i++) {
+		g_settings_reset(presence->settings, keys[i]);
+	}
+	g_strfreev(keys);
+
+	g_clear_pointer(&schema, g_settings_schema_unref);
+}
+
+/******************************************************************************
  * Public API
  *****************************************************************************/
 GDateTime *
diff --git a/libpurple/purplesavedpresenceprivate.h b/libpurple/purplesavedpresenceprivate.h
new file mode 100644
--- /dev/null
+++ b/libpurple/purplesavedpresenceprivate.h
@@ -0,0 +1,56 @@
+/*
+ * 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 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/>.
+ */
+
+#ifndef PURPLE_COMPILATION
+# error "purplesavedpresenceprivate.h may only be include by libpurple"
+#endif
+
+#ifndef PURPLE_SAVED_PRESENCE_PRIVATE_H
+#define PURPLE_SAVED_PRESENCE_PRIVATE_H
+
+#include <glib.h>
+
+#include "purplesavedpresence.h"
+
+G_BEGIN_DECLS
+
+/**
+ * purple_saved_presence_reset: (skip)
+ * @presence: The instance.
+ *
+ * Resets all of the settings of @presence to their default values.
+ *
+ * As of at least glib 2.78.0, the GSettings API doesn't have a way to remove a
+ * relocatable settings object. We use relocatable settings to store each saved
+ * presence which means we can't delete the section from the GKeyFile.
+ *
+ * To minimize the amount of disk space wasted by this issue, this function
+ * will default all of the keys in the settings object for @presence so that
+ * the values will be removed from the GKeyFile.
+ *
+ * Since: 3.0.0
+ */
+G_GNUC_INTERNAL void purple_saved_presence_reset(PurpleSavedPresence *presence);
+
+G_END_DECLS
+
+#endif /* PURPLE_SAVED_PRESENCE_PRIVATE_H */
diff --git a/libpurple/tests/meson.build b/libpurple/tests/meson.build
--- a/libpurple/tests/meson.build
+++ b/libpurple/tests/meson.build
@@ -25,6 +25,7 @@
     'notification_manager',
     'person',
     'presence',
+    'presence_manager',
     'protocol',
     'protocol_action',
     'protocol_contacts',
@@ -63,7 +64,8 @@
 foreach prog : PROGS
     e = executable(f'test_@prog@', f'test_@prog@.c',
                    c_args : [
-                       '-DTEST_DATA_DIR="@0@/data"'.format(meson.current_source_dir())
+                       '-DTEST_DATA_DIR="@0@/data"'.format(meson.current_source_dir()),
+                       '-DTEST_CACHE_DIR="@0@/cache"'.format(meson.current_build_dir()),
                    ],
                    dependencies : [libpurple_dep, glib],
                    link_with: test_ui,
diff --git a/libpurple/tests/test_presence_manager.c b/libpurple/tests/test_presence_manager.c
new file mode 100644
--- /dev/null
+++ b/libpurple/tests/test_presence_manager.c
@@ -0,0 +1,189 @@
+/*
+ * 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 <glib/gstdio.h>
+
+#include <purple.h>
+
+/******************************************************************************
+ * Callbacks
+ *****************************************************************************/
+static void
+test_purple_presence_manager_add_remove_counter(G_GNUC_UNUSED PurplePresenceManager *manager,
+                                                G_GNUC_UNUSED PurpleSavedPresence *presence,
+                                                gpointer data)
+{
+	guint *counter = data;
+
+	*counter = *counter + 1;
+}
+
+static void
+test_purple_presence_manager_list_changed_counter(G_GNUC_UNUSED GListModel *list,
+                                                  G_GNUC_UNUSED guint position,
+                                                  G_GNUC_UNUSED guint removed,
+                                                  G_GNUC_UNUSED guint added,
+                                                  gpointer data)
+{
+	guint *counter = data;
+
+	*counter = *counter + 1;
+}
+
+/******************************************************************************
+ * Tests
+ *****************************************************************************/
+static void
+test_purple_presence_manager_new(void) {
+	PurplePresenceManager *manager = NULL;
+
+	manager = purple_presence_manager_new(NULL);
+	g_assert_true(PURPLE_IS_PRESENCE_MANAGER(manager));
+
+	g_clear_object(&manager);
+}
+
+static void
+test_purple_presence_manager_add_remove(void) {
+	PurplePresenceManager *manager = NULL;
+	PurpleSavedPresence *presence = NULL;
+	gboolean success = FALSE;
+	guint len = 0;
+	guint added = 0;
+	guint removed = 0;
+	guint changed = 0;
+	const char *id = NULL;
+
+	manager = purple_presence_manager_new(NULL);
+	g_signal_connect(manager, "added",
+	                 G_CALLBACK(test_purple_presence_manager_add_remove_counter),
+	                 &added);
+	g_signal_connect(manager, "removed",
+	                 G_CALLBACK(test_purple_presence_manager_add_remove_counter),
+	                 &removed);
+	g_signal_connect(manager, "items-changed",
+	                 G_CALLBACK(test_purple_presence_manager_list_changed_counter),
+	                 &changed);
+
+	len = g_list_model_get_n_items(G_LIST_MODEL(manager));
+	g_assert_cmpuint(len, ==, 0);
+
+	presence = purple_presence_manager_create(manager);
+	g_assert_true(PURPLE_IS_SAVED_PRESENCE(presence));
+	purple_saved_presence_set_name(presence, "test presence");
+	g_assert_cmpuint(added, ==, 1);
+	g_assert_cmpuint(removed, ==, 0);
+	g_assert_cmpuint(changed, ==, 1);
+
+	id = purple_saved_presence_get_id(presence);
+	g_assert_nonnull(id);
+
+	len = g_list_model_get_n_items(G_LIST_MODEL(manager));
+	g_assert_cmpuint(len, ==, 1);
+
+	success = purple_presence_manager_remove(manager, id);
+	g_assert_true(success);
+	g_assert_cmpuint(added, ==, 1);
+	g_assert_cmpuint(removed, ==, 1);
+	g_assert_cmpuint(changed, ==, 2);
+
+	len = g_list_model_get_n_items(G_LIST_MODEL(manager));
+	g_assert_cmpuint(len, ==, 0);
+
+	g_clear_object(&presence);
+	g_clear_object(&manager);
+}
+
+static void
+test_purple_presence_manager_persistence(void) {
+	PurplePresenceManager *manager = NULL;
+	PurpleSavedPresence *presence = NULL;
+	char *filename = NULL;
+	char *old_id = NULL;
+	const char *id = NULL;
+	const char *id1 = NULL;
+	gboolean ret = FALSE;
+
+	filename = g_build_filename(TEST_CACHE_DIR,
+	                            "presence_manager_persistence.ini",
+	                            NULL);
+
+	/* Remove the file if it exists so we can start from a known state. */
+	g_remove(filename);
+
+	/* Create the manager, add a presence, and make it active. */
+	manager = purple_presence_manager_new(filename);
+	g_assert_true(PURPLE_IS_PRESENCE_MANAGER(manager));
+
+	presence = purple_presence_manager_create(manager);
+	g_assert_true(PURPLE_IS_SAVED_PRESENCE(presence));
+
+	/* Save the id of the presence as we need to use it later. */
+	id = purple_saved_presence_get_id(presence);
+	old_id = g_strdup(id);
+
+	/* Make the presence active. */
+	ret = purple_presence_manager_set_active(manager, id);
+	g_assert_true(ret);
+	g_clear_object(&presence);
+
+	presence = purple_presence_manager_get_active(manager);
+	g_assert_true(PURPLE_IS_SAVED_PRESENCE(presence));
+
+	id1 = purple_saved_presence_get_id(presence);
+	g_assert_cmpstr(id1, ==, id);
+
+	/* Clean everything up. The last presence was transfer none so we just need
+	 * to null it out.
+	 */
+	presence = NULL;
+	g_clear_object(&manager);
+
+	/* Now create the manager again and verify that everything was restored. */
+	manager = purple_presence_manager_new(filename);
+	g_assert_true(PURPLE_IS_PRESENCE_MANAGER(manager));
+
+	presence = purple_presence_manager_get_active(manager);
+	g_assert_true(PURPLE_IS_SAVED_PRESENCE(presence));
+
+	id = purple_saved_presence_get_id(presence);
+	g_assert_cmpstr(id, ==, old_id);
+	g_clear_pointer(&old_id, g_free);
+
+	/* Cleanup. */
+	g_clear_object(&manager);
+	g_clear_pointer(&filename, g_free);
+}
+
+/******************************************************************************
+ * Main
+ *****************************************************************************/
+gint
+main(gint argc, gchar *argv[]) {
+	g_test_init(&argc, &argv, NULL);
+
+	g_test_add_func("/presence-manager/new",
+	                test_purple_presence_manager_new);
+	g_test_add_func("/presence-manager/add-remove",
+	                test_purple_presence_manager_add_remove);
+	g_test_add_func("/presence-manager/persistence",
+	                test_purple_presence_manager_persistence);
+
+	return g_test_run();
+}
