# HG changeset patch
# Node ID 731cc5a4ae8c
# Parent  c2a9da21b8c0
diff --git a/doc/reference/libpurple/libpurple-docs.xml b/doc/reference/libpurple/libpurple-docs.xml
--- a/doc/reference/libpurple/libpurple-docs.xml
+++ b/doc/reference/libpurple/libpurple-docs.xml
@@ -80,6 +80,7 @@
       <xi:include href="xml/purpleplugininfo.xml" />
       <xi:include href="xml/purplepresence.xml" />
       <xi:include href="xml/purplewhiteboard.xml" />
+      <xi:include href="xml/purplewhiteboardmanager.xml" />
       <xi:include href="xml/purplewhiteboardops.xml" />
       <xi:include href="xml/purplewhiteboarduiops.xml" />
       <xi:include href="xml/purpleuiinfo.xml" />
diff --git a/libpurple/core.c b/libpurple/core.c
--- a/libpurple/core.c
+++ b/libpurple/core.c
@@ -170,6 +170,7 @@
 	purple_notify_init();
 	purple_conversations_init();
 	purple_conversation_manager_startup();
+	purple_whiteboard_manager_startup();
 	purple_blist_init();
 	purple_log_init();
 	purple_network_init();
@@ -219,6 +220,7 @@
 
 	/* Save .xml files, remove signals, etc. */
 	purple_idle_uninit();
+	purple_whiteboard_manager_shutdown();
 	purple_conversation_manager_shutdown();
 	purple_conversations_uninit();
 	purple_blist_uninit();
diff --git a/libpurple/meson.build b/libpurple/meson.build
--- a/libpurple/meson.build
+++ b/libpurple/meson.build
@@ -73,6 +73,7 @@
 	'purpleprotocolserver.c',
 	'purpleuiinfo.c',
 	'purplewhiteboard.c',
+	'purplewhiteboardmanager.c',
 	'purplewhiteboarduiops.c',
 	'queuedoutputstream.c',
 	'request.c',
@@ -164,6 +165,7 @@
 	'purpleprotocolserver.h',
 	'purpleuiinfo.h',
 	'purplewhiteboard.h',
+	'purplewhiteboardmanager.h',
 	'purplewhiteboardops.h',
 	'purplewhiteboarduiops.h',
 	'queuedoutputstream.h',
diff --git a/libpurple/protocols/silc/wb.c b/libpurple/protocols/silc/wb.c
--- a/libpurple/protocols/silc/wb.c
+++ b/libpurple/protocols/silc/wb.c
@@ -105,13 +105,24 @@
 PurpleWhiteboard *silcpurple_wb_init(SilcPurple sg, SilcClientEntry client_entry)
 {
 	PurpleWhiteboard *wb;
+	PurpleWhiteboardManager *manager;
 	SilcPurpleWb wbs;
+	gchar *id = NULL;
+
+	manager = purple_whiteboard_manager_get_default();
 
-	wb = purple_whiteboard_get_session(sg->account, client_entry->nickname);
-	if (!wb)
-		wb = purple_whiteboard_new(sg->account, client_entry->nickname, 0);
-	if (!wb)
+	id = g_strdup_printf("silc-client-%s", client_entry->nickname);
+	wb = purple_whiteboard_manager_find(manager, id);
+	if(!PURPLE_IS_WHITEBOARD(wb)) {
+		wb = purple_whiteboard_new(sg->account, id, 0);
+		purple_whiteboard_manager_register(manager, wb, NULL);
+	}
+
+	g_free(id);
+
+	if(!PURPLE_IS_WHITEBOARD(wb)) {
 		return NULL;
+	}
 
 	if (!purple_whiteboard_get_protocol_data(wb)) {
 		wbs = silc_calloc(1, sizeof(*wbs));
@@ -135,14 +146,24 @@
 
 PurpleWhiteboard *silcpurple_wb_init_ch(SilcPurple sg, SilcChannelEntry channel)
 {
+	PurpleWhiteboardManager *manager = NULL;
 	PurpleWhiteboard *wb;
 	SilcPurpleWb wbs;
+	gchar *id;
 
-	wb = purple_whiteboard_get_session(sg->account, channel->channel_name);
-	if (!wb)
-		wb = purple_whiteboard_new(sg->account, channel->channel_name, 0);
-	if (!wb)
+	manager = purple_whiteboard_manager_get_default();
+
+	id = g_strdup_printf("silc-channel-%s", channel->channel_name);
+	wb = purple_whiteboard_manager_find(manager, id);
+	if(!PURPLE_IS_WHITEBOARD(wb)) {
+		wb = purple_whiteboard_new(sg->account, id, 0);
+	}
+
+	g_free(id);
+
+	if(!PURPLE_IS_WHITEBOARD(wb)) {
 		return NULL;
+	}
 
 	if (!purple_whiteboard_get_protocol_data(wb)) {
 		wbs = silc_calloc(1, sizeof(*wbs));
@@ -315,18 +336,19 @@
 			 SilcMessageFlags flags, const unsigned char *message,
 			 SilcUInt32 message_len)
 {
-	SilcPurple sg;
-	PurpleConnection *gc;
 	PurpleWhiteboard *wb;
+	PurpleWhiteboardManager *manager;
+	gchar *id = NULL;
+
+	manager = purple_whiteboard_manager_get_default();
 
-	gc = client->application;
-	sg = purple_connection_get_protocol_data(gc);
+	id = g_strdup_printf("silc-client-%s", sender->nickname);
+	wb = purple_whiteboard_manager_find(manager, id);
+	g_free(id);
 
-	wb = purple_whiteboard_get_session(sg->account, sender->nickname);
-	if (!wb) {
+	if(!PURPLE_IS_WHITEBOARD(wb)) {
 		/* Ask user if they want to open the whiteboard */
-		silcpurple_wb_request(client, message, message_len,
-				    sender, NULL);
+		silcpurple_wb_request(client, message, message_len, sender, NULL);
 		return;
 	}
 
@@ -342,18 +364,20 @@
 			    const unsigned char *message,
 			    SilcUInt32 message_len)
 {
-	SilcPurple sg;
-	PurpleConnection *gc;
 	PurpleWhiteboard *wb;
+	PurpleWhiteboardManager *manager;
+	gchar *id;
+
+	manager = purple_whiteboard_manager_get_default();
 
-	gc = client->application;
-	sg = purple_connection_get_protocol_data(gc);
+	id = g_strdup_printf("silc-channel-%s", channel->channel_name);
+	wb = purple_whiteboard_manager_find(manager, id);
+	g_free(id);
 
-	wb = purple_whiteboard_get_session(sg->account, channel->channel_name);
-	if (!wb) {
+	if(!PURPLE_IS_WHITEBOARD(wb)) {
 		/* Ask user if they want to open the whiteboard */
-		silcpurple_wb_request(client, message, message_len,
-				    sender, channel);
+		silcpurple_wb_request(client, message, message_len, sender, channel);
+
 		return;
 	}
 
@@ -430,9 +454,14 @@
 void silcpurple_wb_end(PurpleWhiteboard *wb)
 {
 	SilcPurpleWb wbs = purple_whiteboard_get_protocol_data(wb);
+	PurpleWhiteboardManager * manager;
+
+	manager = purple_whiteboard_manager_get_default();
 
 	silc_free(wbs);
 	purple_whiteboard_set_protocol_data(wb, NULL);
+
+	purple_whiteboard_manager_unregister(manager, wb, NULL);
 }
 
 void silcpurple_wb_get_dimensions(const PurpleWhiteboard *wb, int *width, int *height)
diff --git a/libpurple/purpleprivate.h b/libpurple/purpleprivate.h
--- a/libpurple/purpleprivate.h
+++ b/libpurple/purpleprivate.h
@@ -255,6 +255,25 @@
  */
 void purple_credential_provider_deactivate(PurpleCredentialProvider *provider);
 
+/**
+ * purple_whiteboard_manager_startup:
+ *
+ * Starts up the whiteboard manager by creating the default instance.
+ *
+ * Since: 3.0.0
+ */
+void purple_whiteboard_manager_startup(void);
+
+/**
+ * purple_whiteboard_manager_shutdown:
+ *
+ * Shuts down the whiteboard manager by destroying the default instance.
+ *
+ * Since: 3.0.0
+ */
+void purple_whiteboard_manager_shutdown(void);
+
+
 G_END_DECLS
 
 #endif /* PURPLE_PRIVATE_H */
diff --git a/libpurple/purplewhiteboard.h b/libpurple/purplewhiteboard.h
--- a/libpurple/purplewhiteboard.h
+++ b/libpurple/purplewhiteboard.h
@@ -94,14 +94,14 @@
 /**
  * purple_whiteboard_new:
  * @account: A #PurpleAccount instance.
- * @who: Who you're drawing with.
+ * @id: The identifier of the whiteboard.
  * @state: The state.
  *
  * Creates a new whiteboard.
  *
  * Returns: (transfer full): The new #PurpleWhiteboard instance.
  */
-PurpleWhiteboard *purple_whiteboard_new(PurpleAccount *account, const gchar *who, gint state);
+PurpleWhiteboard *purple_whiteboard_new(PurpleAccount *account, const gchar *id, gint state);
 
 /**
  * purple_whiteboard_get_account:
@@ -114,14 +114,16 @@
 PurpleAccount *purple_whiteboard_get_account(PurpleWhiteboard *whiteboard);
 
 /**
- * purple_whiteboard_get_who:
+ * purple_whiteboard_get_id:
  * @whiteboard: The #PurpleWhiteboard instance.
  *
- * Gets the name of who you're drawing with.
+ * Gets the id of @whiteboard.
+ *
+ * Returns: The id of @whiteboard.
  *
- * Returns: The name of who you're drawing with.
+ * Since: 3.0.0
  */
-const gchar *purple_whiteboard_get_who(PurpleWhiteboard *whiteboard);
+const gchar *purple_whiteboard_get_id(PurpleWhiteboard *whiteboard);
 
 /**
  * purple_whiteboard_set_state:
@@ -151,18 +153,6 @@
 void purple_whiteboard_start(PurpleWhiteboard *whiteboard);
 
 /**
- * purple_whiteboard_get_session:
- * @account: A #PurpleAccount instance.
- * @who: The name of the user you're drawing with.
- *
- * Finds a whiteboard from @account and @who.
- *
- * Returns: (transfer none): The #PurpleWhiteboard instance if found, otherwise
- *          %NULL.
- */
-PurpleWhiteboard *purple_whiteboard_get_session(PurpleAccount *account, const gchar *who);
-
-/**
  * purple_whiteboard_draw_list_destroy:
  * @draw_list: (element-type gint): The drawing list.
  *
@@ -313,6 +303,20 @@
  */
 gpointer purple_whiteboard_get_protocol_data(PurpleWhiteboard *whiteboard);
 
+/**
+ * purple_whiteboard_equal:
+ * @whiteboard1: The first #PurpleWhiteboard instance to check.
+ * @whiteboard2: The second #PurpleWhiteboard instance to check.
+ *
+ * Checks the id's for @whiteboard1 and @whiteboard2 and return whether or not
+ * they are equal.
+ *
+ * Returns: %TRUE if the id's of @whiteboard1 and @whiteboard2 are equal.
+ *
+ * Since: 3.0.0
+ */
+gboolean purple_whiteboard_equal(PurpleWhiteboard *whiteboard1, PurpleWhiteboard *whiteboard2);
+
 G_END_DECLS
 
 #endif /* PURPLE_WHITEBOARD_H */
diff --git a/libpurple/purplewhiteboard.c b/libpurple/purplewhiteboard.c
--- a/libpurple/purplewhiteboard.c
+++ b/libpurple/purplewhiteboard.c
@@ -27,12 +27,13 @@
 #include "purpleprotocolfactory.h"
 #include "purplewhiteboarduiops.h"
 #include "protocol.h"
+#include "util.h"
 
 typedef struct {
 	int state;
 
 	PurpleAccount *account;
-	char *who;
+	gchar *id;
 
 	/* TODO Remove this and use protocol-specific subclasses. */
 	void *proto_data;
@@ -47,7 +48,7 @@
 	PROP_0,
 	PROP_STATE,
 	PROP_ACCOUNT,
-	PROP_WHO,
+	PROP_ID,
 	PROP_DRAW_LIST,
 	N_PROPERTIES,
 };
@@ -56,11 +57,6 @@
 G_DEFINE_TYPE_WITH_PRIVATE(PurpleWhiteboard, purple_whiteboard, G_TYPE_OBJECT)
 
 /******************************************************************************
- * Globals
- *****************************************************************************/
-static GList *wb_list = NULL;
-
-/******************************************************************************
  * Helpers
  *****************************************************************************/
 static void
@@ -78,15 +74,15 @@
 }
 
 static void
-purple_whiteboard_set_who(PurpleWhiteboard *whiteboard, const gchar *who) {
+purple_whiteboard_set_id(PurpleWhiteboard *whiteboard, const gchar *id) {
 	PurpleWhiteboardPrivate *priv = NULL;
 
 	priv = purple_whiteboard_get_instance_private(whiteboard);
 
-	g_clear_pointer(&priv->who, g_free);
-	priv->who = g_strdup(who);
+	g_free(priv->id);
+	priv->id = g_strdup(id);
 
-	g_object_notify_by_pspec(G_OBJECT(whiteboard), properties[PROP_WHO]);
+	g_object_notify_by_pspec(G_OBJECT(whiteboard), properties[PROP_ID]);
 }
 
 /******************************************************************************
@@ -106,8 +102,8 @@
 			purple_whiteboard_set_account(whiteboard,
 			                              g_value_get_object(value));
 			break;
-		case PROP_WHO:
-			purple_whiteboard_set_who(whiteboard, g_value_get_string(value));
+		case PROP_ID:
+			purple_whiteboard_set_id(whiteboard, g_value_get_string(value));
 			break;
 		case PROP_DRAW_LIST:
 			purple_whiteboard_set_draw_list(whiteboard,
@@ -133,9 +129,9 @@
 			g_value_set_object(value,
 			                   purple_whiteboard_get_account(whiteboard));
 			break;
-		case PROP_WHO:
+		case PROP_ID:
 			g_value_set_string(value,
-			                   purple_whiteboard_get_who(whiteboard));
+			                   purple_whiteboard_get_id(whiteboard));
 			break;
 		case PROP_DRAW_LIST:
 			g_value_set_pointer(value,
@@ -170,8 +166,6 @@
 	if(priv->protocol_ops != NULL && priv->protocol_ops->start != NULL) {
 		priv->protocol_ops->start(whiteboard);
 	}
-
-	wb_list = g_list_append(wb_list, whiteboard);
 }
 
 static void
@@ -186,10 +180,8 @@
 		priv->protocol_ops->end(whiteboard);
 	}
 
-	wb_list = g_list_remove(wb_list, whiteboard);
-
 	g_clear_object(&priv->account);
-	g_clear_pointer(&priv->who, g_free);
+	g_clear_pointer(&priv->id, g_free);
 
 	/* TODO: figure out how we need to clean up the drawlist */
 
@@ -216,9 +208,9 @@
 		"The whiteboard's account.", PURPLE_TYPE_ACCOUNT,
 		G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS);
 
-	properties[PROP_WHO] = g_param_spec_string(
-		"who", "Who",
-		"Who you're drawing with.", NULL,
+	properties[PROP_ID] = g_param_spec_string(
+		"id", "id",
+		"The ID of the whiteboard.", NULL,
 		G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS);
 
 	properties[PROP_DRAW_LIST] = g_param_spec_pointer(
@@ -257,14 +249,14 @@
 }
 
 const gchar *
-purple_whiteboard_get_who(PurpleWhiteboard *whiteboard) {
+purple_whiteboard_get_id(PurpleWhiteboard *whiteboard) {
 	PurpleWhiteboardPrivate *priv = NULL;
 
 	g_return_val_if_fail(PURPLE_IS_WHITEBOARD(whiteboard), NULL);
 
 	priv = purple_whiteboard_get_instance_private(whiteboard);
 
-	return priv->who;
+	return priv->id;
 }
 
 void
@@ -296,24 +288,6 @@
 	purple_whiteboard_ui_ops_create(whiteboard);
 }
 
-PurpleWhiteboard *
-purple_whiteboard_get_session(PurpleAccount *account, const gchar *who) {
-	PurpleWhiteboard *whiteboard = NULL;
-	PurpleWhiteboardPrivate *priv = NULL;
-	GList *l = NULL;
-
-	for(l = wb_list; l != NULL; l = l->next) {
-		whiteboard = PURPLE_WHITEBOARD(l->data);
-		priv = purple_whiteboard_get_instance_private(whiteboard);
-
-		if(priv->account == account && purple_strequal(priv->who, who)) {
-			return whiteboard;
-		}
-	}
-
-	return NULL;
-}
-
 void
 purple_whiteboard_draw_list_destroy(GList *draw_list) {
 	g_list_free(draw_list);
@@ -491,12 +465,12 @@
 }
 
 PurpleWhiteboard *
-purple_whiteboard_new(PurpleAccount *account, const gchar *who, gint state) {
+purple_whiteboard_new(PurpleAccount *account, const gchar *id, gint state) {
 	PurpleWhiteboard *whiteboard = NULL;
 	PurpleProtocol *protocol = NULL;
 
 	g_return_val_if_fail(PURPLE_IS_ACCOUNT(account), NULL);
-	g_return_val_if_fail(who != NULL, NULL);
+	g_return_val_if_fail(id != NULL, NULL);
 
 	protocol = purple_account_get_protocol(account);
 
@@ -504,12 +478,12 @@
 
 	if(PURPLE_IS_PROTOCOL_FACTORY(protocol)) {
 		whiteboard = purple_protocol_factory_whiteboard_new(
-			PURPLE_PROTOCOL_FACTORY(protocol), account, who, state);
+			PURPLE_PROTOCOL_FACTORY(protocol), account, id, state);
 	} else {
 		whiteboard = g_object_new(PURPLE_TYPE_WHITEBOARD,
 			"account", account,
-			"who",     who,
-			"state",   state,
+			"id", id,
+			"state", state,
 			NULL
 		);
 	}
@@ -517,3 +491,20 @@
 	return whiteboard;
 }
 
+gboolean
+purple_whiteboard_equal(PurpleWhiteboard *whiteboard1,
+                        PurpleWhiteboard *whiteboard2)
+{
+	PurpleWhiteboardPrivate *priv1 = NULL, *priv2 = NULL;
+
+	if(whiteboard1 == NULL) {
+		return (whiteboard2 == NULL);
+	} else if(whiteboard2 == NULL) {
+		return FALSE;
+	}
+
+	priv1 = purple_whiteboard_get_instance_private(whiteboard1);
+	priv2 = purple_whiteboard_get_instance_private(whiteboard2);
+
+	return purple_strequal(priv1->id, priv2->id);
+}
diff --git a/libpurple/purplewhiteboardmanager.h b/libpurple/purplewhiteboardmanager.h
new file mode 100644
--- /dev/null
+++ b/libpurple/purplewhiteboardmanager.h
@@ -0,0 +1,193 @@
+/*
+ * 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 <pidgin.h> may be included directly"
+#endif
+
+#ifndef PURPLE_WHITEBOARD_MANAGER_H
+#define PURPLE_WHITEBOARD_MANAGER_H
+
+#include <glib.h>
+#include <glib-object.h>
+
+#include <libpurple/purplewhiteboard.h>
+
+G_BEGIN_DECLS
+
+/**
+ * SECTION:purplewhiteboardmanager
+ * @section_id: libpurple-purplewhiteboardmanager
+ * @title: Purple Whiteboard Manager
+ * @short_description: Management of whiteboards.
+ *
+ * #PurpleWhiteboardManager keeps track of all whiteboards and emits signals
+ * when whiteboards are registered and unregistered.
+ */
+
+/**
+ * PURPLE_WHITEBOARD_MANAGER_DOMAIN:
+ *
+ * A #GError domain for errors from #PurpleWhiteboardManager.
+ *
+ * Since: 3.0.0
+ */
+#define PURPLE_WHITEBOARD_MANAGER_DOMAIN (g_quark_from_static_string("purple-whiteboard-manager"))
+
+/**
+ * PURPLE_TYPE_WHITEBOARD_MANAGER:
+ *
+ * The standard _TYPE_ macro for #PurpleWhiteboardManager.
+ *
+ * Since: 3.0.0
+ */
+#define PURPLE_TYPE_WHITEBOARD_MANAGER (purple_whiteboard_manager_get_type())
+
+/**
+ * purple_whiteboard_manager_get_type:
+ *
+ * The standard _get_type macro for #PurpleWhiteboardManager.
+ *
+ * Since: 3.0.0
+ */
+
+/**
+ * PurpleWhiteboardManager:
+ *
+ * An opaque data structure that manages whiteboards.
+ *
+ * Since: 3.0.0
+ */
+G_DECLARE_DERIVABLE_TYPE(PurpleWhiteboardManager, purple_whiteboard_manager,
+                         PURPLE, WHITEBOARD_MANAGER, GObject)
+
+/**
+ * PurpleWhiteboardManagerClass:
+ * @registered: The default signal handler for when a whiteboard is registered.
+ * @unregistered: The default signal handler for when a whiteboard is
+ *                unregistered.
+ *
+ * The class structure for #PurpleWhiteboardManager.
+ *
+ * Since: 3.0.0
+ */
+struct _PurpleWhiteboardManagerClass {
+	/*< private >*/
+	GObjectClass parent;
+
+	/*< public >*/
+	void (*registered)(PurpleWhiteboardManager *manager, PurpleWhiteboard *whiteboard);
+	void (*unregistered)(PurpleWhiteboardManager *manager, PurpleWhiteboard *whiteboard);
+
+	/*< private >*/
+	gpointer reserved[4];
+};
+
+/**
+ * PurpleWhiteboardManagerForeachFunc:
+ * @whiteboard: The #PurpleWhiteboard instance.
+ * @data: User supplied data.
+ *
+ * A function to be used as a callback with purple_whiteboard_manager_foreach().
+ *
+ * Since: 3.0.0
+ */
+typedef void (*PurpleWhiteboardManagerForeachFunc)(PurpleWhiteboard *whiteboard, gpointer data);
+
+/**
+ * purple_whiteboard_manager_get_default:
+ *
+ * Gets the default #PurpleWhiteboardManager instance.
+ *
+ * Returns: (transfer none): The default #PurpleWhiteboardManager instance.
+ *
+ * Since: 3.0.0
+ */
+PurpleWhiteboardManager *purple_whiteboard_manager_get_default(void);
+
+/**
+ * purple_whiteboard_manager_register:
+ * @manager: The #PurpleWhiteboardManager instance.
+ * @whiteboard: The #PurpleWhiteboard to register.
+ * @error: (out) (optional) (nullable): A return address for a #GError.
+ *
+ * Registers @whiteboard with @manager.
+ *
+ * Returns: %TRUE if @whiteboard was successfully registered with @manager,
+ *          %FALSE otherwise.
+ *
+ * Since: 3.0.0
+ */
+gboolean purple_whiteboard_manager_register(PurpleWhiteboardManager *manager, PurpleWhiteboard *whiteboard, GError **error);
+
+/**
+ * purple_whiteboard_manager_unregister:
+ * @manager: The #PurpleWhiteboardManager instance.
+ * @whiteboard: The #PurpleWhiteboard to unregister.
+ * @error: (out) (optional) (nullable): A return address for a #GError.
+ *
+ * Unregisters @whiteboard from @manager.
+ *
+ * Returns: %TRUE if @whiteboard was successfully unregistered from @manager,
+ *          %FALSE otherwise.
+ *
+ * Since: 3.0.0
+ */
+gboolean purple_whiteboard_manager_unregister(PurpleWhiteboardManager *manager, PurpleWhiteboard *whiteboard, GError **error);
+
+/**
+ * purple_whiteboard_manager_find:
+ * @manager: The #PurpleWhiteboardManager instance.
+ * @id: The id of the #PurpleWhiteboard to find.
+ *
+ * Gets the #PurpleWhiteboard identified by @id if found, otherwise %NULL.
+ *
+ * Returns: (transfer full): The #PurpleWhiteboard identified by @id or %NULL.
+ *
+ * Since: 3.0.0
+ */
+PurpleWhiteboard *purple_whiteboard_manager_find(PurpleWhiteboardManager *manager, const gchar *id);
+
+/**
+ * purple_whiteboard_manager_foreach:
+ * @manager: The #PurpleWhiteboardManager instance.
+ * @func: (scope call): The #PurpleWhiteboardManagerForeachFunc to call.
+ * @data: User data to pass to @func.
+ *
+ * Calls @func for each #PurpleWhiteboard that @manager knows about.
+ *
+ * Since: 3.0.0
+ */
+void purple_whiteboard_manager_foreach(PurpleWhiteboardManager *manager, PurpleWhiteboardManagerForeachFunc func, gpointer data);
+
+/**
+ * purple_whiteboard_manager_get_model:
+ * @manager: The #PurpleWhiteboardManager instance.
+ *
+ * Gets the backing model of @manager as a #GListModel.
+ *
+ * Returns: (transfer none) (element-type PurpleWhiteboard): The #GListModel
+ *          containing all of the #PurpleWhiteboards registered with @manager.
+ *
+ * Since: 3.0.0
+ */
+GListModel *purple_whiteboard_manager_get_model(PurpleWhiteboardManager *manager);
+
+G_END_DECLS
+
+#endif /* PURPLE_WHITEBOARD_MANAGER_H */
diff --git a/libpurple/purplewhiteboardmanager.c b/libpurple/purplewhiteboardmanager.c
new file mode 100644
--- /dev/null
+++ b/libpurple/purplewhiteboardmanager.c
@@ -0,0 +1,269 @@
+/*
+ * 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 "purplewhiteboardmanager.h"
+#include "purpleprivate.h"
+
+enum {
+	SIG_REGISTERED,
+	SIG_UNREGISTERED,
+	N_SIGNALS,
+};
+static guint signals[N_SIGNALS] = {0, };
+
+typedef struct {
+	GListStore *store;
+} PurpleWhiteboardManagerPrivate;
+
+G_DEFINE_TYPE_WITH_PRIVATE(PurpleWhiteboardManager, purple_whiteboard_manager,
+                           G_TYPE_OBJECT);
+
+static PurpleWhiteboardManager *default_manager = NULL;
+
+/******************************************************************************
+ * GObject Implementation
+ *****************************************************************************/
+static void
+purple_whiteboard_manager_finalize(GObject *obj) {
+	PurpleWhiteboardManager *manager = NULL;
+	PurpleWhiteboardManagerPrivate *priv = NULL;
+
+	manager = PURPLE_WHITEBOARD_MANAGER(obj);
+	priv = purple_whiteboard_manager_get_instance_private(manager);
+
+	g_clear_object(&priv->store);
+
+	G_OBJECT_CLASS(purple_whiteboard_manager_parent_class)->finalize(obj);
+}
+
+static void
+purple_whiteboard_manager_init(PurpleWhiteboardManager *manager) {
+	PurpleWhiteboardManagerPrivate *priv = NULL;
+
+	priv = purple_whiteboard_manager_get_instance_private(manager);
+
+	priv->store = g_list_store_new(PURPLE_TYPE_WHITEBOARD);
+}
+
+static void
+purple_whiteboard_manager_class_init(PurpleWhiteboardManagerClass *klass) {
+	GObjectClass *obj_class = G_OBJECT_CLASS(klass);
+
+	obj_class->finalize = purple_whiteboard_manager_finalize;
+
+	/**
+	 * PurpleWhiteboardManager::registered:
+	 * @manager: The #PurpleWhiteboardManager instance.
+	 * @whiteboard: The #PurpleWhiteboard that was registered.
+	 *
+	 * Emitted after @whiteboard has been registered in @manager.
+	 *
+	 * Since: 3.0.0
+	 */
+	signals[SIG_REGISTERED] = g_signal_new(
+		"registered",
+		G_OBJECT_CLASS_TYPE(klass),
+		G_SIGNAL_RUN_LAST,
+		G_STRUCT_OFFSET(PurpleWhiteboardManagerClass, registered),
+		NULL,
+		NULL,
+		NULL,
+		G_TYPE_NONE,
+		1,
+		PURPLE_TYPE_WHITEBOARD);
+
+	/**
+	 * PurpleWhiteboardManager::unregistered:
+	 * @manager: The #PurpleWhiteboardManager instance.
+	 * @whiteboard: The #PurpleWhiteboard that was unregistered.
+	 *
+	 * Emitted after @whiteboard has been unregistered from @manager.
+	 *
+	 * Since: 3.0.0
+	 */
+	signals[SIG_UNREGISTERED] = g_signal_new(
+		"unregistered",
+		G_OBJECT_CLASS_TYPE(klass),
+		G_SIGNAL_RUN_LAST,
+		G_STRUCT_OFFSET(PurpleWhiteboardManagerClass, unregistered),
+		NULL,
+		NULL,
+		NULL,
+		G_TYPE_NONE,
+		1,
+		PURPLE_TYPE_WHITEBOARD);
+}
+
+/******************************************************************************
+ * Private API
+ *****************************************************************************/
+void
+purple_whiteboard_manager_startup(void) {
+	if(default_manager == NULL) {
+		default_manager = g_object_new(PURPLE_TYPE_WHITEBOARD_MANAGER, NULL);
+	}
+}
+
+void
+purple_whiteboard_manager_shutdown(void) {
+	g_clear_object(&default_manager);
+}
+
+/******************************************************************************
+ * Public API
+ *****************************************************************************/
+PurpleWhiteboardManager *
+purple_whiteboard_manager_get_default(void) {
+	return default_manager;
+}
+
+gboolean
+purple_whiteboard_manager_register(PurpleWhiteboardManager *manager,
+                                   PurpleWhiteboard *whiteboard,
+                                   GError **error)
+{
+	PurpleWhiteboardManagerPrivate *priv = NULL;
+	gboolean found = FALSE;
+
+	g_return_val_if_fail(PURPLE_IS_WHITEBOARD_MANAGER(manager), FALSE);
+	g_return_val_if_fail(PURPLE_IS_WHITEBOARD(whiteboard), FALSE);
+
+	priv = purple_whiteboard_manager_get_instance_private(manager);
+
+	found = g_list_store_find_with_equal_func(priv->store, whiteboard,
+	                                          (GEqualFunc)purple_whiteboard_equal,
+	                                          NULL);
+
+	if(found) {
+		g_set_error(error, PURPLE_WHITEBOARD_MANAGER_DOMAIN, 0,
+		            _("whiteboard %s is already registered"),
+		            purple_whiteboard_get_id(whiteboard));
+
+		return FALSE;
+	}
+
+	g_list_store_insert(priv->store, 0, whiteboard);
+
+	g_signal_emit(G_OBJECT(manager), signals[SIG_REGISTERED], 0, whiteboard);
+
+	return TRUE;
+}
+
+gboolean
+purple_whiteboard_manager_unregister(PurpleWhiteboardManager *manager,
+                                     PurpleWhiteboard *whiteboard,
+                                     GError **error)
+{
+	PurpleWhiteboardManagerPrivate *priv = NULL;
+	guint position = 0;
+	gboolean found = FALSE;
+
+	g_return_val_if_fail(PURPLE_IS_WHITEBOARD_MANAGER(manager), FALSE);
+	g_return_val_if_fail(PURPLE_IS_WHITEBOARD(whiteboard), FALSE);
+
+	priv = purple_whiteboard_manager_get_instance_private(manager);
+
+	found = g_list_store_find_with_equal_func(priv->store, whiteboard,
+	                                          (GEqualFunc)purple_whiteboard_equal,
+	                                          &position);
+
+	if(!found) {
+		g_set_error(error, PURPLE_WHITEBOARD_MANAGER_DOMAIN, 0,
+		            _("whiteboard %s is not registered"),
+		            purple_whiteboard_get_id(whiteboard));
+
+		return FALSE;
+	}
+
+	/* Temporarily ref whiteboard so we can pass it along to the signal
+	 * callbacks.
+	 */
+	g_object_ref(G_OBJECT(whiteboard));
+
+	g_list_store_remove(priv->store, position);
+
+	g_signal_emit(G_OBJECT(manager), signals[SIG_UNREGISTERED], 0, whiteboard);
+
+	g_object_unref(G_OBJECT(whiteboard));
+
+	return TRUE;
+}
+
+PurpleWhiteboard *
+purple_whiteboard_manager_find(PurpleWhiteboardManager *manager,
+                               const gchar *id)
+{
+	PurpleWhiteboardManagerPrivate *priv = NULL;
+	guint idx, n;
+
+	g_return_val_if_fail(PURPLE_IS_WHITEBOARD_MANAGER(manager), NULL);
+	g_return_val_if_fail(id != NULL, NULL);
+
+	priv = purple_whiteboard_manager_get_instance_private(manager);
+
+	n = g_list_model_get_n_items(G_LIST_MODEL(priv->store));
+	for(idx = 0; idx < n; idx++) {
+		GObject *obj = NULL;
+
+		obj = g_list_model_get_object(G_LIST_MODEL(priv->store), idx);
+		if(PURPLE_IS_WHITEBOARD(obj)) {
+			PurpleWhiteboard *whiteboard = PURPLE_WHITEBOARD(obj);
+
+			if(purple_strequal(id, purple_whiteboard_get_id(whiteboard))) {
+				return whiteboard;
+			}
+		}
+		g_clear_object(&obj);
+	}
+
+	return NULL;
+}
+
+void
+purple_whiteboard_manager_foreach(PurpleWhiteboardManager *manager,
+                                  PurpleWhiteboardManagerForeachFunc func,
+                                  gpointer data)
+{
+	PurpleWhiteboardManagerPrivate *priv = NULL;
+	guint idx, n;
+
+	g_return_if_fail(PURPLE_IS_WHITEBOARD_MANAGER(manager));
+	g_return_if_fail(func != NULL);
+
+	priv = purple_whiteboard_manager_get_instance_private(manager);
+
+	n = g_list_model_get_n_items(G_LIST_MODEL(priv->store));
+	for(idx = 0; idx < n; idx++) {
+		gpointer item = g_list_model_get_item(G_LIST_MODEL(priv->store), idx);
+		func(item, data);
+		g_object_unref(item);
+	}
+}
+
+GListModel *
+purple_whiteboard_manager_get_model(PurpleWhiteboardManager *manager) {
+	PurpleWhiteboardManagerPrivate *priv = NULL;
+
+	g_return_val_if_fail(PURPLE_IS_WHITEBOARD_MANAGER(manager), NULL);
+
+	priv = purple_whiteboard_manager_get_instance_private(manager);
+
+	return G_LIST_MODEL(priv->store);
+}
diff --git a/libpurple/tests/meson.build b/libpurple/tests/meson.build
--- a/libpurple/tests/meson.build
+++ b/libpurple/tests/meson.build
@@ -13,7 +13,8 @@
     'queued_output_stream',
     'trie',
     'util',
-    'xmlnode'
+    'whiteboard_manager',
+    'xmlnode',
 ]
 
 test_ui = static_library(
diff --git a/libpurple/tests/test_whiteboard_manager.c b/libpurple/tests/test_whiteboard_manager.c
new file mode 100644
--- /dev/null
+++ b/libpurple/tests/test_whiteboard_manager.c
@@ -0,0 +1,54 @@
+/*
+ * 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"
+
+/******************************************************************************
+ * Tests
+ *****************************************************************************/
+static void
+test_purple_whiteboard_manager_get_default(void) {
+	PurpleWhiteboardManager *manager1 = NULL, *manager2 = NULL;
+
+	manager1 = purple_whiteboard_manager_get_default();
+	g_assert_true(PURPLE_IS_WHITEBOARD_MANAGER(manager1));
+
+	manager2 = purple_whiteboard_manager_get_default();
+	g_assert_true(PURPLE_IS_WHITEBOARD_MANAGER(manager2));
+
+	g_assert_true(manager1 == manager2);
+}
+
+/******************************************************************************
+ * Main
+ *****************************************************************************/
+gint
+main(gint argc, gchar *argv[]) {
+	g_test_init(&argc, &argv, NULL);
+
+	test_ui_purple_init();
+
+	g_test_add_func("/whiteboard-manager/get-default",
+	                test_purple_whiteboard_manager_get_default);
+
+	return g_test_run();
+}
diff --git a/meson.build b/meson.build
--- a/meson.build
+++ b/meson.build
@@ -230,7 +230,7 @@
 # #######################################################################
 # # Check for GLib 2.44 (required)
 # #######################################################################
-glib = dependency('glib-2.0', version : '>= 2.52.0')
+glib = dependency('glib-2.0', version : '>= 2.64.0')
 gio = dependency('gio-2.0')
 gobject = dependency('gobject-2.0')
 gthread = dependency('gthread-2.0')
diff --git a/pidgin/gtkwhiteboard.c b/pidgin/gtkwhiteboard.c
--- a/pidgin/gtkwhiteboard.c
+++ b/pidgin/gtkwhiteboard.c
@@ -588,13 +588,13 @@
 	 * their username
 	 */
 	buddy = purple_blist_find_buddy(purple_whiteboard_get_account(wb),
-	                                purple_whiteboard_get_who(wb));
+	                                purple_whiteboard_get_id(wb));
 
 	gtk_window_set_title(GTK_WINDOW(gtkwb),
 	                     buddy != NULL
 	                             ? purple_buddy_get_contact_alias(buddy)
-	                             : purple_whiteboard_get_who(wb));
-	gtk_widget_set_name(GTK_WIDGET(gtkwb), purple_whiteboard_get_who(wb));
+	                             : purple_whiteboard_get_id(wb));
+	gtk_widget_set_name(GTK_WIDGET(gtkwb), purple_whiteboard_get_id(wb));
 
 	gtk_widget_set_size_request(GTK_WIDGET(gtkwb->drawing_area),
 	                            gtkwb->width, gtkwb->height);
diff --git a/po/POTFILES.in b/po/POTFILES.in
--- a/po/POTFILES.in
+++ b/po/POTFILES.in
@@ -268,6 +268,7 @@
 libpurple/purpleprotocolserver.c
 libpurple/purpleuiinfo.c
 libpurple/purplewhiteboard.c
+libpurple/purplewhiteboardmanager.c
 libpurple/purplewhiteboarduiops.c
 libpurple/queuedoutputstream.c
 libpurple/request.c
