diff --git a/doc/reference/pidgin/pidgin-docs.xml b/doc/reference/pidgin/pidgin-docs.xml
--- a/doc/reference/pidgin/pidgin-docs.xml
+++ b/doc/reference/pidgin/pidgin-docs.xml
@@ -57,6 +57,7 @@
       <xi:include href="xml/pidginactiongroup.xml" />
       <xi:include href="xml/pidginapplication.xml" />
       <xi:include href="xml/pidginattachment.xml" />
+      <xi:include href="xml/pidginavatar.xml" />
       <xi:include href="xml/pidgincellrendererexpander.xml" />
       <xi:include href="xml/pidginclosebutton.xml" />
       <xi:include href="xml/pidgincontactcompletion.xml" />
diff --git a/libpurple/buddyicon.h b/libpurple/buddyicon.h
--- a/libpurple/buddyicon.h
+++ b/libpurple/buddyicon.h
@@ -25,6 +25,10 @@
 
 #ifndef PURPLE_BUDDYICON_H
 #define PURPLE_BUDDYICON_H
+
+#include <glib.h>
+#include <gio/gio.h>
+
 /**
  * SECTION:buddyicon
  * @section_id: libpurple-buddyicon
@@ -169,6 +173,20 @@
                            size_t len, const char *checksum);
 
 /**
+ * purple_buddy_icon_save_to_filename:
+ * @icon: The #PurpleBuddyIcon instance.
+ * @filename: The filename to write.
+ * @error: (optional): A return address for a #GError.
+ *
+ * Writes the contents of @icon to @filename.
+ *
+ * Returns: %TRUE on success, or %FALSE on error possibly with @error set.
+ *
+ * Since: 3.0.0
+ */
+gboolean purple_buddy_icon_save_to_filename(PurpleBuddyIcon *icon, const gchar *filename, GError **error);
+
+/**
  * purple_buddy_icon_get_account:
  * @icon: The buddy icon.
  *
@@ -213,6 +231,18 @@
 gconstpointer purple_buddy_icon_get_data(const PurpleBuddyIcon *icon, size_t *len);
 
 /**
+ * purple_buddy_icon_get_stream:
+ * @icon: The #PurpleBuddyIcon instance.
+ *
+ * Gets the data of @icon as a #GInputStream.
+ *
+ * Returns: (transfer full): A new #GInputStream.
+ *
+ * Since: 3.0.0
+ */
+GInputStream *purple_buddy_icon_get_stream(PurpleBuddyIcon *icon);
+
+/**
  * purple_buddy_icon_get_extension:
  * @icon: The buddy icon.
  *
diff --git a/libpurple/buddyicon.c b/libpurple/buddyicon.c
--- a/libpurple/buddyicon.c
+++ b/libpurple/buddyicon.c
@@ -477,6 +477,19 @@
 		g_object_unref(old_img);
 }
 
+gboolean
+purple_buddy_icon_save_to_filename(PurpleBuddyIcon *icon,
+                                   const gchar *filename, GError **error)
+{
+	gconstpointer data;
+	size_t len;
+
+	data = purple_buddy_icon_get_data(icon, &len);
+
+	return g_file_set_contents(filename, data, len, error);
+}
+
+
 PurpleAccount *
 purple_buddy_icon_get_account(const PurpleBuddyIcon *icon)
 {
@@ -517,6 +530,18 @@
 	return NULL;
 }
 
+GInputStream *
+purple_buddy_icon_get_stream(PurpleBuddyIcon *icon) {
+	gconstpointer data = NULL;
+	size_t len = 0;
+
+	g_return_val_if_fail(icon != NULL, NULL);
+
+	data = purple_buddy_icon_get_data(icon, &len);
+
+	return g_memory_input_stream_new_from_data(data, (gssize)len, NULL);
+}
+
 const char *
 purple_buddy_icon_get_extension(const PurpleBuddyIcon *icon)
 {
diff --git a/pidgin/glade/pidgin3.xml.in b/pidgin/glade/pidgin3.xml.in
--- a/pidgin/glade/pidgin3.xml.in
+++ b/pidgin/glade/pidgin3.xml.in
@@ -6,6 +6,7 @@
     <glade-widget-class name="PidginAccountStore" generic-name="account_store" title="AccountStore"/>
     <glade-widget-class name="PidginAccountFilterConnected" generic-name="account_filter_connected" title="FilterConnected"/>
     <glade-widget-class name="PidginAccountFilterProtocol" generic-name="account_filter_protocol" title="FilterProtocol"/>
+    <glade-widget-class name="PidginAvatar" generic-name="avatar" title="Avatar"/>
     <glade-widget-class name="PidginCloseButton" generic-name="close-button" title="CloseButton"/>
     <glade-widget-class name="PidginConversationWindow" generic-name="conversation_window" title="ConversationWindow"/>
     <glade-widget-class name="PidginCredentialProviderStore" generic-name="credential_provider_store" title="CredentialProviderStore"/>
@@ -25,6 +26,7 @@
     <glade-widget-class-ref name="PidginAccountStore"/>
     <glade-widget-class-ref name="PidginAccountFilterConnected"/>
     <glade-widget-class-ref name="PidginAccountFilterProtocol"/>
+    <glade-widget-class-ref name="PidginAvatar"/>
     <glade-widget-class-ref name="PidginCloseButton"/>
     <glade-widget-class-ref name="PidginConversationWindow"/>
     <glade-widget-class-ref name="PidginCredentialProviderStore"/>
diff --git a/pidgin/meson.build b/pidgin/meson.build
--- a/pidgin/meson.build
+++ b/pidgin/meson.build
@@ -37,6 +37,7 @@
 	'pidginactiongroup.c',
 	'pidginapplication.c',
 	'pidginattachment.c',
+	'pidginavatar.c',
 	'pidgincellrendererexpander.c',
 	'pidginclosebutton.c',
 	'pidgincontactcompletion.c',
@@ -104,6 +105,7 @@
 	'pidginactiongroup.h',
 	'pidginapplication.h',
 	'pidginattachment.h',
+	'pidginavatar.h',
 	'pidgincellrendererexpander.h',
 	'pidginclosebutton.h',
 	'pidgincontactcompletion.h',
diff --git a/pidgin/pidginavatar.h b/pidgin/pidginavatar.h
new file mode 100644
--- /dev/null
+++ b/pidgin/pidginavatar.h
@@ -0,0 +1,151 @@
+/*
+ * Pidgin - Internet Messenger
+ * Copyright (C) Pidgin Developers <devel@pidgin.im>
+ *
+ * Pidgin 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(PIDGIN_GLOBAL_HEADER_INSIDE) && !defined(PIDGIN_COMPILATION)
+# error "only <pidgin.h> may be included directly"
+#endif
+
+#ifndef PIDGIN_AVATAR_H
+#define PIDGIN_AVATAR_H
+
+/**
+ * SECTION:pidginavatar
+ * @section_id: pidgin-pidginavatar
+ * @short_description: Avatars
+ * @title: Avatar Widget
+ *
+ * #PidginAvatar is a widget that displays avatars for contacts or
+ * conversations.
+ */
+
+#include <glib.h>
+
+#include <gtk/gtk.h>
+
+#include <purple.h>
+
+G_BEGIN_DECLS
+
+/**
+ * PIDGIN_TYPE_AVATAR:
+ *
+ * The standard _get_type macro for #PidginAvatar.
+ */
+
+#define PIDGIN_TYPE_AVATAR (pidgin_avatar_get_type())
+G_DECLARE_FINAL_TYPE(PidginAvatar, pidgin_avatar,
+                     PIDGIN, AVATAR, GtkEventBox)
+
+/**
+ * PidginAvatar:
+ *
+ * #PidginAvatar is an opaque data structure and should only be accessed via
+ * its API.
+ */
+
+/**
+ * pidgin_avatar_new:
+ *
+ * Creates a new #PidginAvatar instance.
+ *
+ * Returns: (transfer full): The new #PidginAvatar instance.
+ *
+ * Since: 3.0.0
+ */
+GtkWidget *pidgin_avatar_new(void);
+
+/**
+ * pidgin_avatar_set_animate:
+ * @avatar: The #PidginAvatar instance.
+ * @animate: Whether or not to animate the avatar.
+ *
+ * Starts or stops animation of @avatar.  If avatar is displaying a
+ * non-animated image, changing this will do nothing unless a new animated
+ * image is set.
+ *
+ * Since: 3.0.0
+ */
+void pidgin_avatar_set_animate(PidginAvatar *avatar, gboolean animate);
+
+/**
+ * pidgin_avatar_get_animate:
+ * @avatar: The #PidginAvatar instance.
+ *
+ * Gets whether or not @avatar should be animated.
+ *
+ * Returns: Whether or not @avatar should be animated.
+ *
+ * Since: 3.0.0
+ */
+gboolean pidgin_avatar_get_animate(PidginAvatar *avatar);
+
+/**
+ * pidgin_avatar_set_buddy:
+ * @avatar: The #PidginAvatar instance.
+ * @buddy: (nullable): The #PurpleBuddy to set or %NULL to unset.
+ *
+ * Sets or unsets the #PurpleBuddy that @avatar should use for display.
+ *
+ * Since: 3.0.0
+ */
+void pidgin_avatar_set_buddy(PidginAvatar *avatar, PurpleBuddy *buddy);
+
+/**
+ * pidgin_avatar_get_buddy:
+ * @avatar: The #PidginAvatar instance.
+ *
+ * Gets the #PurpleBuddy that @avatar is using for display.
+ *
+ * Returns: (transfer none): The #PurpleBuddy that @avatar is using for display.
+ *
+ * Since: 3.0.0
+ */
+PurpleBuddy *pidgin_avatar_get_buddy(PidginAvatar *avatar);
+
+/**
+ * pidgin_avatar_set_conversation:
+ * @avatar: The #PidginAvatar instance.
+ * @conversation: (nullable): The #PurpleConversation to set or %NULL to unset.
+ *
+ * Sets or unsets the #PurpleConversation that @avatar uses to find the
+ * #PurpleBuddy whose icon to display.
+ *
+ * Since: 3.0.0
+ */
+void pidgin_avatar_set_conversation(PidginAvatar *avatar, PurpleConversation *conversation);
+
+/**
+ * pidgin_avatar_get_conversation:
+ * @avatar: The #PidginAvatar instance.
+ *
+ * Gets the #PurpleConversation that @avatar is using for display.
+ *
+ * Returns: (transfer none): The #PurpleConversation that @avatar is using to
+ *          find the #PurpleBuddy whos icon to display.
+ *
+ * Since: 3.0.0
+ */
+PurpleConversation *pidgin_avatar_get_conversation(PidginAvatar *avatar);
+
+G_END_DECLS
+
+#endif /* PIDGIN_AVATAR_H */
diff --git a/pidgin/pidginavatar.c b/pidgin/pidginavatar.c
new file mode 100644
--- /dev/null
+++ b/pidgin/pidginavatar.c
@@ -0,0 +1,626 @@
+/*
+ * Pidgin - Internet Messenger
+ * Copyright (C) Pidgin Developers <devel@pidgin.im>
+ *
+ * Pidgin 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/>.
+ */
+
+#include <glib/gi18n-lib.h>
+
+#include "pidgin/pidginavatar.h"
+
+struct _PidginAvatar {
+	GtkEventBox parent;
+
+	GtkWidget *icon;
+
+	GdkPixbufAnimation *animation;
+	gboolean animate;
+
+	PurpleBuddy *buddy;
+	PurpleConversation *conversation;
+};
+
+enum {
+	PROP_0,
+	PROP_ANIMATE,
+	PROP_BUDDY,
+	PROP_CONVERSATION,
+	N_PROPERTIES,
+};
+static GParamSpec *properties[N_PROPERTIES] = {NULL, };
+
+G_DEFINE_TYPE(PidginAvatar, pidgin_avatar, GTK_TYPE_EVENT_BOX)
+
+/******************************************************************************
+ * Actions
+ *****************************************************************************/
+static void
+pidgin_avatar_animate_toggle(GSimpleAction *action, GVariant *value,
+                             gpointer data)
+{
+	PidginAvatar *avatar = PIDGIN_AVATAR(data);
+	gboolean animate = g_variant_get_boolean(value);
+
+	pidgin_avatar_set_animate(avatar, animate);
+	g_simple_action_set_state(G_SIMPLE_ACTION(action),
+	                          g_variant_new_boolean(animate));
+}
+
+static void
+pidgin_avatar_save_response_cb(GtkNativeDialog *native, gint response,
+                               gpointer data)
+{
+	PidginAvatar *avatar = PIDGIN_AVATAR(data);
+	PurpleBuddyIcon *icon = NULL;
+
+	if(response != GTK_RESPONSE_ACCEPT || !PURPLE_IS_BUDDY(avatar->buddy)) {
+		gtk_native_dialog_destroy(native);
+
+		return;
+	}
+
+	icon = purple_buddy_get_icon(avatar->buddy);
+
+	if(icon != NULL) {
+		GtkFileChooser *chooser = GTK_FILE_CHOOSER(native);
+		gchar *filename = NULL;
+
+		filename = gtk_file_chooser_get_filename(chooser);
+
+		purple_buddy_icon_save_to_filename(icon, filename, NULL);
+
+		g_free(filename);
+	}
+
+	gtk_native_dialog_destroy(native);
+}
+
+static void
+pidgin_avatar_save_cb(GSimpleAction *action, GVariant *parameter,
+                      gpointer data)
+{
+	PidginAvatar *avatar = PIDGIN_AVATAR(data);
+	PurpleAccount *account = NULL;
+	GtkFileChooserNative *native = NULL;
+	GtkFileChooser *chooser = NULL;
+	GtkWindow *window = NULL;
+	const gchar *ext = NULL, *name = NULL;
+	gchar *filename = NULL;
+
+	g_return_if_fail(PURPLE_IS_BUDDY(avatar->buddy));
+
+	ext = purple_buddy_icon_get_extension(purple_buddy_get_icon(avatar->buddy));
+
+	account = purple_buddy_get_account(avatar->buddy);
+	name = purple_buddy_get_name(avatar->buddy);
+	filename = g_strdup_printf("%s.%s", purple_normalize(account, name), ext);
+
+	window = GTK_WINDOW(gtk_widget_get_toplevel(GTK_WIDGET(avatar)));
+	native = gtk_file_chooser_native_new(_("Save Avatar"),
+	                                     window,
+	                                     GTK_FILE_CHOOSER_ACTION_SAVE,
+	                                     _("_Save"),
+	                                     _("_Cancel"));
+	g_signal_connect(G_OBJECT(native), "response",
+	                 G_CALLBACK(pidgin_avatar_save_response_cb), avatar);
+
+	chooser = GTK_FILE_CHOOSER(native);
+
+	gtk_file_chooser_set_do_overwrite_confirmation(chooser, TRUE);
+	gtk_file_chooser_set_filename(chooser, filename);
+	g_free(filename);
+
+	gtk_native_dialog_show(GTK_NATIVE_DIALOG(native));
+}
+
+static void
+pidgin_avatar_set_custom_response_cb(GtkNativeDialog *native, gint response,
+                                     gpointer data)
+{
+	PidginAvatar *avatar = PIDGIN_AVATAR(data);
+	GtkFileChooser *chooser = GTK_FILE_CHOOSER(native);
+	gchar *filename = NULL;
+
+	if(response != GTK_RESPONSE_ACCEPT || !PURPLE_IS_BUDDY(avatar->buddy)) {
+		gtk_native_dialog_destroy(native);
+
+		return;
+	}
+
+	filename = gtk_file_chooser_get_filename(chooser);
+	if(filename != NULL) {
+		PurpleContact *contact = purple_buddy_get_contact(avatar->buddy);
+		PurpleBlistNode *node = PURPLE_BLIST_NODE(contact);
+
+		purple_buddy_icons_node_set_custom_icon_from_file(node, filename);
+	}
+
+	gtk_native_dialog_destroy(native);
+}
+
+static void
+pidgin_avatar_set_custom_cb(GSimpleAction *action, GVariant *parameter,
+                            gpointer data)
+{
+	PidginAvatar *avatar = PIDGIN_AVATAR(data);
+	GtkFileChooserNative *native = NULL;
+	GtkWindow *window = NULL;
+
+	window = GTK_WINDOW(gtk_widget_get_toplevel(GTK_WIDGET(avatar)));
+	native = gtk_file_chooser_native_new(_("Set Custom Avatar"),
+	                                     window,
+	                                     GTK_FILE_CHOOSER_ACTION_OPEN,
+	                                     _("_Set Custom"),
+	                                     _("_Cancel"));
+
+	g_signal_connect(G_OBJECT(native), "response",
+	                 G_CALLBACK(pidgin_avatar_set_custom_response_cb), avatar);
+
+	gtk_native_dialog_show(GTK_NATIVE_DIALOG(native));
+}
+
+static void
+pidgin_avatar_clear_custom_cb(GSimpleAction *action, GVariant *parameter,
+                              gpointer data)
+{
+	PidginAvatar *avatar = PIDGIN_AVATAR(data);
+
+	if(PURPLE_IS_BUDDY(avatar->buddy)) {
+		PurpleContact *contact = purple_buddy_get_contact(avatar->buddy);
+		PurpleBlistNode *node = PURPLE_BLIST_NODE(contact);
+
+		purple_buddy_icons_node_set_custom_icon_from_file(node, NULL);
+	}
+}
+
+static GActionEntry actions[] = {
+	{
+		.name = "animate",
+		.state = "false",
+		.change_state = pidgin_avatar_animate_toggle,
+	}, {
+		.name = "save-avatar",
+		.activate = pidgin_avatar_save_cb,
+	}, {
+		.name = "set-custom-avatar",
+		.activate = pidgin_avatar_set_custom_cb,
+	}, {
+		.name = "clear-custom-avatar",
+		.activate = pidgin_avatar_clear_custom_cb,
+	},
+};
+
+/******************************************************************************
+ * Helpers
+ *****************************************************************************/
+static GdkPixbufAnimation *
+pidgin_avatar_find_buddy_icon(PurpleBuddy *buddy,
+                              PurpleIMConversation *conversation)
+{
+	GdkPixbufAnimation *ret = NULL;
+	GInputStream *stream = NULL;
+	PurpleContact *contact = NULL;
+
+	g_return_val_if_fail(PURPLE_IS_BUDDY(buddy), NULL);
+
+	/* First check if our user has set a custom icon for this buddy. */
+	contact = purple_buddy_get_contact(buddy);
+	if(PURPLE_IS_CONTACT(contact)) {
+		PurpleBlistNode *node = PURPLE_BLIST_NODE(contact);
+		PurpleImage *custom_image = NULL;
+
+		custom_image = purple_buddy_icons_node_find_custom_icon(node);
+		if(PURPLE_IS_IMAGE(custom_image)) {
+			gconstpointer data = purple_image_get_data(custom_image);
+			gsize length = purple_image_get_data_size(custom_image);
+
+			stream = g_memory_input_stream_new_from_data(data, (gssize)length,
+			                                             NULL);
+		}
+	}
+
+	/* If there is no custom icon, fall back to checking if the buddy has an
+	 * icon set.
+	 */
+	if(!G_IS_INPUT_STREAM(stream)) {
+		PurpleBuddyIcon *icon = purple_buddy_get_icon(buddy);
+
+		if(icon != NULL) {
+			stream = purple_buddy_icon_get_stream(icon);
+		}
+	}
+
+	/* Finally if we still don't have icon, we fallback to asking the
+	 * conversation for one.
+	 */
+	if(!G_IS_INPUT_STREAM(stream) && PURPLE_IS_IM_CONVERSATION(conversation)) {
+		PurpleBuddyIcon *icon = purple_im_conversation_get_icon(PURPLE_IM_CONVERSATION(conversation));
+
+		if(icon != NULL) {
+			stream = purple_buddy_icon_get_stream(icon);
+		}
+	}
+
+	if(G_IS_INPUT_STREAM(stream)) {
+		ret = gdk_pixbuf_animation_new_from_stream(stream, NULL, NULL);
+		g_clear_object(&stream);
+	}
+
+	return ret;
+}
+
+static void
+pidgin_avatar_update(PidginAvatar *avatar) {
+	PurpleAccount *account = NULL;
+	GdkPixbufAnimation *animation = NULL;
+
+	if(PURPLE_IS_BUDDY(avatar->buddy)) {
+		animation = pidgin_avatar_find_buddy_icon(avatar->buddy, NULL);
+	} else if(PURPLE_IS_IM_CONVERSATION(avatar->conversation)) {
+		PurpleBuddy *buddy = NULL;
+		const gchar *name = NULL;
+
+		account = purple_conversation_get_account(avatar->conversation);
+
+		name = purple_conversation_get_name(avatar->conversation);
+		buddy = purple_blist_find_buddy(account, name);
+
+		if(PURPLE_IS_BUDDY(buddy)) {
+			animation = pidgin_avatar_find_buddy_icon(buddy,
+		                                              PURPLE_IM_CONVERSATION(avatar->conversation));
+		}
+	}
+
+	g_set_object(&avatar->animation, animation);
+
+	if(GDK_IS_PIXBUF_ANIMATION(avatar->animation)) {
+		if(avatar->animate) {
+			gtk_image_set_from_animation(GTK_IMAGE(avatar->icon),
+			                             avatar->animation);
+		} else {
+			GdkPixbuf *frame = NULL;
+
+			frame = gdk_pixbuf_animation_get_static_image(avatar->animation);
+
+			gtk_image_set_from_pixbuf(GTK_IMAGE(avatar->icon), frame);
+		}
+	} else {
+		gtk_image_clear(GTK_IMAGE(avatar->icon));
+	}
+
+	g_clear_object(&animation);
+}
+
+/******************************************************************************
+ * Callbacks
+ *****************************************************************************/
+static gboolean
+pidgin_avatar_button_press_handler(GtkWidget *widget, GdkEventButton *event,
+                                   gpointer data)
+{
+	PidginAvatar *avatar = PIDGIN_AVATAR(data);
+	GtkBuilder *builder = NULL;
+	GtkWidget *menu = NULL;
+	GMenuModel *model = NULL;
+
+	if(!gdk_event_triggers_context_menu((GdkEvent *)event)) {
+		return FALSE;
+	}
+
+	builder = gtk_builder_new_from_resource("/im/pidgin/Pidgin/Avatar/menu.ui");
+	model = (GMenuModel *)gtk_builder_get_object(builder, "menu");
+
+	menu = gtk_menu_new_from_model(model);
+	gtk_menu_attach_to_widget(GTK_MENU(menu), GTK_WIDGET(avatar), NULL);
+
+	g_clear_object(&builder);
+
+	gtk_widget_show_all(menu);
+
+	gtk_menu_popup_at_pointer(GTK_MENU(menu), (GdkEvent *)event);
+
+	return TRUE;
+}
+
+/*
+ * This function is a callback for when properties change on the buddy we're
+ * tracking.  It should not be reused for the conversation we're tracking
+ * because we have to disconnect old handlers and reuse of this function will
+ * cause issues if a buddy is changed but a conversation is not and vice versa.
+ */
+static void
+pidgin_avatar_buddy_icon_updated(GObject *obj, GParamSpec *pspec, gpointer d) {
+	PidginAvatar *avatar = PIDGIN_AVATAR(d);
+
+	pidgin_avatar_update(avatar);
+}
+
+/*
+ * This function is a callback for when properties change on the conversation
+ * we're tracking.  It should not be reused for the buddy we're tracking
+ * because we have to disconnect old handlers and reuse of this function will
+ * cause issues if a buddy is changed but a conversation is not and vice versa.
+ */
+static void
+pidgin_avatar_conversation_updated(GObject *obj, GParamSpec *pspec, gpointer d)
+{
+	PidginAvatar *avatar = PIDGIN_AVATAR(d);
+
+	pidgin_avatar_update(avatar);
+}
+
+static gboolean
+pidgin_avatar_enter_notify_handler(GtkWidget *widget, GdkEvent *event,
+                                   gpointer data)
+{
+	PidginAvatar *avatar = PIDGIN_AVATAR(widget);
+
+	pidgin_avatar_set_animate(avatar, TRUE);
+
+	return FALSE;
+}
+
+static gboolean
+pidgin_avatar_leave_notify_handler(GtkWidget *widget, GdkEvent *event,
+                                   gpointer user_data)
+{
+	PidginAvatar *avatar = PIDGIN_AVATAR(widget);
+
+	pidgin_avatar_set_animate(avatar, FALSE);
+
+	return FALSE;
+}
+
+/******************************************************************************
+ * GObject Implementation
+ *****************************************************************************/
+static void
+pidgin_avatar_get_property(GObject *obj, guint param_id, GValue *value,
+                           GParamSpec *pspec)
+{
+	PidginAvatar *avatar = PIDGIN_AVATAR(obj);
+
+	switch(param_id) {
+		case PROP_ANIMATE:
+			g_value_set_boolean(value, pidgin_avatar_get_animate(avatar));
+			break;
+		case PROP_BUDDY:
+			g_value_set_object(value, pidgin_avatar_get_buddy(avatar));
+			break;
+		case PROP_CONVERSATION:
+			g_value_set_object(value, pidgin_avatar_get_conversation(avatar));
+			break;
+		default:
+			G_OBJECT_WARN_INVALID_PROPERTY_ID(obj, param_id, pspec);
+			break;
+	}
+}
+
+static void
+pidgin_avatar_set_property(GObject *obj, guint param_id, const GValue *value,
+                           GParamSpec *pspec)
+{
+	PidginAvatar *avatar = PIDGIN_AVATAR(obj);
+
+	switch(param_id) {
+		case PROP_ANIMATE:
+			pidgin_avatar_set_animate(avatar, g_value_get_boolean(value));
+			break;
+		case PROP_BUDDY:
+			pidgin_avatar_set_buddy(avatar, g_value_get_object(value));
+			break;
+		case PROP_CONVERSATION:
+			pidgin_avatar_set_conversation(avatar, g_value_get_object(value));
+			break;
+		default:
+			G_OBJECT_WARN_INVALID_PROPERTY_ID(obj, param_id, pspec);
+			break;
+	}
+}
+
+static void
+pidgin_avatar_dispose(GObject *obj) {
+	PidginAvatar *avatar = PIDGIN_AVATAR(obj);
+
+	pidgin_avatar_set_buddy(avatar, NULL);
+	pidgin_avatar_set_conversation(avatar, NULL);
+
+	g_clear_object(&avatar->animation);
+
+	G_OBJECT_CLASS(pidgin_avatar_parent_class)->dispose(obj);
+}
+
+static void
+pidgin_avatar_init(PidginAvatar *avatar) {
+	GSimpleActionGroup *group = NULL;
+
+	gtk_widget_init_template(GTK_WIDGET(avatar));
+
+	/* For development/design purposes, the avatar defaults to the
+	 * "image-missing" icon.  However, we don't want to display that to users,
+	 * so we clear it during run time.
+	 */
+	gtk_image_clear(GTK_IMAGE(avatar->icon));
+
+	/* Now setup our actions. */
+	group = g_simple_action_group_new();
+	g_action_map_add_action_entries(G_ACTION_MAP(group), actions,
+	                                G_N_ELEMENTS(actions), avatar);
+
+	gtk_widget_insert_action_group(GTK_WIDGET(avatar), "avatar",
+	                               G_ACTION_GROUP(group));
+}
+
+static void
+pidgin_avatar_class_init(PidginAvatarClass *klass) {
+	GObjectClass *obj_class = G_OBJECT_CLASS(klass);
+	GtkWidgetClass *widget_class = GTK_WIDGET_CLASS(klass);
+
+	obj_class->get_property = pidgin_avatar_get_property;
+	obj_class->set_property = pidgin_avatar_set_property;
+	obj_class->dispose = pidgin_avatar_dispose;
+
+	/**
+	 * PidginAvatar::animate:
+	 *
+	 * Whether or not an animated avatar should be animated.
+	 */
+	properties[PROP_ANIMATE] = g_param_spec_boolean(
+		"animate", "animate",
+		"Whether or not to animate an animated avatar",
+		FALSE,
+		G_PARAM_READWRITE | G_PARAM_CONSTRUCT | G_PARAM_STATIC_STRINGS);
+
+	/**
+	 * PidginAvatar::buddy:
+	 *
+	 * The #PurpleBuddy whose avatar will be displayed.
+	 */
+	properties[PROP_BUDDY] = g_param_spec_object(
+		"buddy", "buddy",
+		"The buddy whose avatar to display",
+		PURPLE_TYPE_BUDDY,
+		G_PARAM_READWRITE | G_PARAM_CONSTRUCT | G_PARAM_STATIC_STRINGS);
+
+	/**
+	 * PidginAvatar::conversation:
+	 *
+	 * The #PurpleConversation which will be used to find the correct
+	 * #PurpleBuddy.
+	 */
+	properties[PROP_CONVERSATION] = g_param_spec_object(
+		"conversation", "conversation",
+		"The conversation used to find the correct buddy.",
+		PURPLE_TYPE_CONVERSATION,
+		G_PARAM_READWRITE | G_PARAM_CONSTRUCT | G_PARAM_STATIC_STRINGS);
+
+	g_object_class_install_properties(obj_class, N_PROPERTIES, properties);
+
+	gtk_widget_class_set_template_from_resource(
+	    widget_class,
+	    "/im/pidgin/Pidgin/Avatar/avatar.ui"
+	);
+
+	gtk_widget_class_bind_template_child(widget_class, PidginAvatar, icon);
+
+	gtk_widget_class_bind_template_callback(widget_class,
+	                                        pidgin_avatar_button_press_handler);
+	gtk_widget_class_bind_template_callback(widget_class,
+	                                        pidgin_avatar_enter_notify_handler);
+	gtk_widget_class_bind_template_callback(widget_class,
+	                                        pidgin_avatar_leave_notify_handler);
+}
+
+/******************************************************************************
+ * API
+ *****************************************************************************/
+GtkWidget *
+pidgin_avatar_new(void) {
+	return GTK_WIDGET(g_object_new(PIDGIN_TYPE_AVATAR, NULL));
+}
+
+void
+pidgin_avatar_set_animate(PidginAvatar *avatar, gboolean animate) {
+	g_return_if_fail(PIDGIN_IS_AVATAR(avatar));
+
+	avatar->animate = animate;
+
+	if(GDK_IS_PIXBUF_ANIMATION(avatar->animation)) {
+		if(avatar->animate) {
+			gtk_image_set_from_animation(GTK_IMAGE(avatar->icon),
+			                             avatar->animation);
+		} else {
+			GdkPixbuf *frame = NULL;
+
+			frame = gdk_pixbuf_animation_get_static_image(avatar->animation);
+
+			gtk_image_set_from_pixbuf(GTK_IMAGE(avatar->icon), frame);
+		}
+	}
+}
+
+gboolean
+pidgin_avatar_get_animate(PidginAvatar *avatar) {
+	g_return_val_if_fail(PIDGIN_IS_AVATAR(avatar), FALSE);
+
+	return avatar->animate;
+}
+
+void
+pidgin_avatar_set_buddy(PidginAvatar *avatar, PurpleBuddy *buddy) {
+	g_return_if_fail(PIDGIN_IS_AVATAR(avatar));
+
+	/* Remove our old signal handler. */
+	if(PURPLE_IS_BUDDY(avatar->buddy)) {
+		g_signal_handlers_disconnect_by_func(avatar->buddy,
+		                                     pidgin_avatar_buddy_icon_updated,
+		                                     avatar);
+	}
+
+	if(g_set_object(&avatar->buddy, buddy)) {
+		pidgin_avatar_update(avatar);
+
+		g_object_notify_by_pspec(G_OBJECT(avatar), properties[PROP_BUDDY]);
+	}
+
+	/* Add the notify signal so we can update when the icon changes. */
+	if(PURPLE_IS_BUDDY(avatar->buddy)) {
+		g_signal_connect(G_OBJECT(avatar->buddy), "notify::icon",
+		                 G_CALLBACK(pidgin_avatar_buddy_icon_updated), avatar);
+	}
+}
+
+PurpleBuddy *
+pidgin_avatar_get_buddy(PidginAvatar *avatar) {
+	g_return_val_if_fail(PIDGIN_IS_AVATAR(avatar), NULL);
+
+	return avatar->buddy;
+}
+
+void
+pidgin_avatar_set_conversation(PidginAvatar *avatar,
+                               PurpleConversation *conversation)
+{
+	g_return_if_fail(PIDGIN_IS_AVATAR(avatar));
+
+	/* Remove our old signal handler. */
+	if(PURPLE_IS_CONVERSATION(avatar->conversation)) {
+		g_signal_handlers_disconnect_by_func(avatar->conversation,
+		                                     pidgin_avatar_conversation_updated,
+		                                     avatar);
+	}
+
+	if(g_set_object(&avatar->conversation, conversation)) {
+		g_object_notify_by_pspec(G_OBJECT(avatar),
+		                         properties[PROP_CONVERSATION]);
+	}
+
+	/* Add the notify signal so we can update when the icon changes. */
+	if(PURPLE_IS_CONVERSATION(avatar->conversation)) {
+		g_signal_connect(G_OBJECT(avatar->conversation), "notify",
+		                 G_CALLBACK(pidgin_avatar_conversation_updated), avatar);
+	}
+}
+
+PurpleConversation *
+pidgin_avatar_get_conversation(PidginAvatar *avatar) {
+	g_return_val_if_fail(PIDGIN_IS_AVATAR(avatar), NULL);
+
+	return avatar->conversation;
+}
diff --git a/pidgin/resources/Avatar/avatar.ui b/pidgin/resources/Avatar/avatar.ui
new file mode 100644
--- /dev/null
+++ b/pidgin/resources/Avatar/avatar.ui
@@ -0,0 +1,43 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- Generated with glade 3.38.2 
+
+Pidgin - Internet Messenger
+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/>.
+
+-->
+<interface>
+  <requires lib="gtk+" version="3.20"/>
+  <!-- interface-license-type gplv2 -->
+  <!-- interface-name Pidgin -->
+  <!-- interface-description Internet Messenger -->
+  <!-- interface-copyright Pidgin Developers <devel@pidgin.im> -->
+  <template class="PidginAvatar" parent="GtkEventBox">
+    <property name="visible">True</property>
+    <property name="can-focus">False</property>
+    <property name="events">GDK_POINTER_MOTION_MASK | GDK_ENTER_NOTIFY_MASK | GDK_LEAVE_NOTIFY_MASK | GDK_STRUCTURE_MASK</property>
+    <signal name="button-press-event" handler="pidgin_avatar_button_press_handler" object="PidginAvatar" swapped="no"/>
+    <signal name="enter-notify-event" handler="pidgin_avatar_enter_notify_handler" swapped="no"/>
+    <signal name="leave-notify-event" handler="pidgin_avatar_leave_notify_handler" swapped="no"/>
+    <child>
+      <object class="GtkImage" id="icon">
+        <property name="visible">True</property>
+        <property name="can-focus">False</property>
+        <property name="icon-name">image-missing</property>
+        <property name="icon_size">3</property>
+      </object>
+    </child>
+  </template>
+</interface>
diff --git a/pidgin/resources/Avatar/menu.ui b/pidgin/resources/Avatar/menu.ui
new file mode 100644
--- /dev/null
+++ b/pidgin/resources/Avatar/menu.ui
@@ -0,0 +1,23 @@
+<?xml version="1.0"?>
+<interface>
+  <menu id="menu">
+    <section>
+      <item>
+        <attribute name="label" translatable="yes">Animate</attribute>
+        <attribute name="action">avatar.animate</attribute>
+      </item>
+      <item>
+        <attribute name="label" translatable="yes">Save Avatar As...</attribute>
+        <attribute name="action">avatar.save-avatar</attribute>
+      </item>
+      <item>
+        <attribute name="label" translatable="yes">Set Custom Avatar...</attribute>
+        <attribute name="action">avatar.set-custom-avatar</attribute>
+      </item>
+      <item>
+        <attribute name="label" translatable="yes">Clear Custom Avatar</attribute>
+        <attribute name="action">avatar.clear-custom-avatar</attribute>
+      </item>
+    </section>
+  </menu>
+</interface>
diff --git a/pidgin/resources/pidgin.gresource.xml b/pidgin/resources/pidgin.gresource.xml
--- a/pidgin/resources/pidgin.gresource.xml
+++ b/pidgin/resources/pidgin.gresource.xml
@@ -9,6 +9,8 @@
     <file compressed="true">Accounts/chooser.ui</file>
     <file compressed="true">Accounts/entry.css</file>
     <file compressed="true">Accounts/menu.ui</file>
+    <file compressed="true">Avatar/avatar.ui</file>
+    <file compressed="true">Avatar/menu.ui</file>
     <file compressed="true">BuddyList/window.ui</file>
     <file compressed="true">Conversations/invite_dialog.ui</file>
     <file compressed="true">Conversations/menu.ui</file>
diff --git a/po/POTFILES.in b/po/POTFILES.in
--- a/po/POTFILES.in
+++ b/po/POTFILES.in
@@ -348,6 +348,7 @@
 pidgin/pidginactiongroup.c
 pidgin/pidginapplication.c
 pidgin/pidginattachment.c
+pidgin/pidginavatar.c
 pidgin/pidgin.c
 pidgin/pidgincellrendererexpander.c
 pidgin/pidginclosebutton.c
@@ -395,6 +396,8 @@
 pidgin/resources/Accounts/actionsmenu.ui
 pidgin/resources/Accounts/chooser.ui
 pidgin/resources/Accounts/menu.ui
+pidgin/resources/Avatar/avatar.ui
+pidgin/resources/Avatar/menu.ui
 pidgin/resources/BuddyList/window.ui
 pidgin/resources/closebutton.ui
 pidgin/resources/Conversations/invite_dialog.ui
