# HG changeset patch
# Node ID 70b8dbea17f3b288c63b9e2f0a8afbd6e50c9e78
# Parent  886b982d5d85ee486d1e988f69a72ce9de3dae8e
diff --git a/pidgin/meson.build b/pidgin/meson.build
--- a/pidgin/meson.build
+++ b/pidgin/meson.build
@@ -32,6 +32,7 @@
 	'pidginiconname.c',
 	'pidgininfopane.c',
 	'pidginkeypad.c',
+	'pidginmessage.c',
 	'pidginnotifiable.c',
 	'pidginnotification.c',
 	'pidginnotificationaddcontact.c',
@@ -83,6 +84,7 @@
 	'pidginiconname.h',
 	'pidgininfopane.h',
 	'pidginkeypad.h',
+	'pidginmessage.h',
 	'pidginnotifiable.h',
 	'pidginnotification.h',
 	'pidginnotificationaddcontact.h',
diff --git a/pidgin/pidginconversation.c b/pidgin/pidginconversation.c
--- a/pidgin/pidginconversation.c
+++ b/pidgin/pidginconversation.c
@@ -25,10 +25,10 @@
 #include <purple.h>
 
 #include "pidginautoadjustment.h"
-#include "pidgincolor.h"
 #include "pidgincontactinfomenu.h"
 #include "pidginconversation.h"
 #include "pidgininfopane.h"
+#include "pidginmessage.h"
 
 #define PIDGIN_CONVERSATION_DATA ("pidgin-conversation")
 
@@ -73,40 +73,6 @@
 	}
 }
 
-/**
- * pidgin_conversation_set_tooltip_for_timestamp: (skip)
- * @tooltip: The tooltip to update.
- * @timestamp: The timestamp to set.
- *
- * Updates @tooltip to display @timestamp. This is meant to be called from
- * a GtkWidget::query-tooltip signal and its return value should be returned
- * from that handler.
- *
- * Returns: %TRUE if a tooltip was set, otherwise %FALSE.
- *
- * Since: 3.0
- */
-static gboolean
-pidgin_conversation_set_tooltip_for_timestamp(GtkTooltip *tooltip,
-                                              GDateTime *timestamp)
-{
-	GDateTime *local = NULL;
-	char *text = NULL;
-
-	if(timestamp == NULL) {
-		return FALSE;
-	}
-
-	local = g_date_time_to_local(timestamp);
-	text = g_date_time_format(local, "%c");
-	g_clear_pointer(&local, g_date_time_unref);
-
-	gtk_tooltip_set_text(tooltip, text);
-	g_clear_pointer(&text, g_free);
-
-	return TRUE;
-}
-
 /* This is used to call g_markup_escape_text for the topic before displaying it
  * in its normal label and the tool tip for that label.
  */
@@ -251,50 +217,6 @@
 	}
 }
 
-static PangoAttrList *
-pidgin_conversation_get_author_attributes(G_GNUC_UNUSED GObject *self,
-                                          PurpleMessage *message,
-                                          G_GNUC_UNUSED gpointer data)
-{
-	GdkRGBA rgba;
-	PangoAttrList *attrs = NULL;
-	gboolean color_valid = FALSE;
-	const char *custom_color = NULL;
-
-	if(!PURPLE_IS_MESSAGE(message)) {
-		return NULL;
-	}
-
-	custom_color = purple_message_get_author_name_color(message);
-	if(!purple_strempty(custom_color)) {
-		color_valid = gdk_rgba_parse(&rgba, custom_color);
-	}
-
-	if(!color_valid) {
-		PurpleContactInfo *author = NULL;
-		const char *name_for_display = NULL;
-
-		author = purple_message_get_author(message);
-		name_for_display = purple_contact_info_get_name_for_display(author);
-
-		pidgin_color_calculate_for_text(name_for_display, &rgba);
-		color_valid = TRUE;
-	}
-
-	attrs = pango_attr_list_new();
-
-	if(color_valid) {
-		PangoAttribute *attr = NULL;
-
-		attr = pango_attr_foreground_new(0xFFFF * rgba.red,
-		                                 0xFFFF * rgba.green,
-		                                 0xFFFF * rgba.blue);
-		pango_attr_list_insert(attrs, attr);
-	}
-
-	return attrs;
-}
-
 static char *
 pidgin_converation_get_timestamp_string(G_GNUC_UNUSED GObject *self,
                                         PurpleMessage *message,
@@ -321,87 +243,6 @@
 	return NULL;
 }
 
-static gboolean
-pidgin_conversation_query_tooltip_timestamp_cb(G_GNUC_UNUSED GtkWidget *self,
-                                               G_GNUC_UNUSED gint x,
-                                               G_GNUC_UNUSED gint y,
-                                               G_GNUC_UNUSED gboolean keyboard_mode,
-                                               GtkTooltip *tooltip,
-                                               gpointer data)
-{
-
-	PurpleMessage *message = gtk_list_item_get_item(data);
-	GDateTime *timestamp = NULL;
-
-	if(!PURPLE_IS_MESSAGE(message)) {
-		return FALSE;
-	}
-
-	timestamp = purple_message_get_timestamp(message);
-
-	return pidgin_conversation_set_tooltip_for_timestamp(tooltip, timestamp);
-}
-
-static gboolean
-pidgin_conversation_query_tooltip_edited_cb(G_GNUC_UNUSED GtkWidget *self,
-                                            G_GNUC_UNUSED gint x,
-                                            G_GNUC_UNUSED gint y,
-                                            G_GNUC_UNUSED gboolean keyboard_mode,
-                                            GtkTooltip *tooltip,
-                                            gpointer data)
-{
-	PurpleMessage *message = gtk_list_item_get_item(data);
-	GDateTime *timestamp = NULL;
-
-	if(!PURPLE_IS_MESSAGE(message)) {
-		return FALSE;
-	}
-
-	timestamp = purple_message_get_edited_at(message);
-
-	return pidgin_conversation_set_tooltip_for_timestamp(tooltip, timestamp);
-}
-
-static PangoAttrList *
-pidgin_conversation_get_message_attributes(G_GNUC_UNUSED GObject *self,
-                                           PurpleMessage *message,
-                                           G_GNUC_UNUSED gpointer data)
-{
-	PangoAttrList *attrs = NULL;
-
-	if(!PURPLE_IS_MESSAGE(message)) {
-		return NULL;
-	}
-
-	attrs = pango_attr_list_new();
-
-	if(purple_message_get_action(message)) {
-		PangoAttribute *attr = NULL;
-
-		attr = pango_attr_style_new(PANGO_STYLE_ITALIC);
-
-		pango_attr_list_insert(attrs, attr);
-	}
-
-	return attrs;
-}
-
-
-static char *
-pidgin_conversation_process_message_contents_cb(G_GNUC_UNUSED GObject *self,
-                                                const char *contents,
-                                                G_GNUC_UNUSED gpointer data)
-{
-	char *escaped = NULL;
-	char *linkified = NULL;
-
-	escaped = g_markup_escape_text(contents, -1);
-	linkified = purple_markup_linkify(escaped);
-	g_free(escaped);
-
-	return linkified;
-}
-
 static void
 pidgin_conversation_member_list_context_cb(GtkGestureSingle *self,
                                            G_GNUC_UNUSED gint n_press,
@@ -451,6 +292,42 @@
 	return purple_contact_info_compare(info_a, info_b);
 }
 
+static void
+pidgin_conversation_message_setup(G_GNUC_UNUSED GtkSignalListItemFactory *self,
+                                  GObject *object,
+                                  G_GNUC_UNUSED gpointer data)
+{
+	gtk_list_item_set_child(GTK_LIST_ITEM(object), pidgin_message_new(NULL));
+}
+
+static void
+pidgin_conversation_message_bind(G_GNUC_UNUSED GtkSignalListItemFactory *self,
+                                 GObject *object,
+                                 G_GNUC_UNUSED gpointer data)
+{
+	PurpleMessage *purple_message = NULL;
+	GtkListItem *item = GTK_LIST_ITEM(object);
+	GtkWidget *pidgin_message = NULL;
+
+	purple_message = gtk_list_item_get_item(item);
+	pidgin_message = gtk_list_item_get_child(item);
+
+	pidgin_message_set_message(PIDGIN_MESSAGE(pidgin_message), purple_message);
+}
+
+static void
+pidgin_conversation_message_unbind(G_GNUC_UNUSED GtkSignalListItemFactory *self,
+                                   GObject *object,
+                                   G_GNUC_UNUSED gpointer data)
+{
+	GtkListItem *item = GTK_LIST_ITEM(object);
+	GtkWidget *message = NULL;
+
+	message = gtk_list_item_get_child(item);
+
+	pidgin_message_set_message(PIDGIN_MESSAGE(message), NULL);
+}
+
 /******************************************************************************
  * GObject Implementation
  *****************************************************************************/
@@ -557,19 +434,15 @@
 	gtk_widget_class_bind_template_callback(widget_class,
 	                                        pidgin_conversation_input_key_pressed_cb);
 	gtk_widget_class_bind_template_callback(widget_class,
-	                                        pidgin_conversation_get_author_attributes);
-	gtk_widget_class_bind_template_callback(widget_class,
 	                                        pidgin_converation_get_timestamp_string);
 	gtk_widget_class_bind_template_callback(widget_class,
-	                                        pidgin_conversation_query_tooltip_timestamp_cb);
+	                                        pidgin_conversation_member_list_context_cb);
 	gtk_widget_class_bind_template_callback(widget_class,
-	                                        pidgin_conversation_query_tooltip_edited_cb);
+	                                        pidgin_conversation_message_setup);
 	gtk_widget_class_bind_template_callback(widget_class,
-	                                        pidgin_conversation_get_message_attributes);
+	                                        pidgin_conversation_message_bind);
 	gtk_widget_class_bind_template_callback(widget_class,
-	                                        pidgin_conversation_process_message_contents_cb);
-	gtk_widget_class_bind_template_callback(widget_class,
-	                                        pidgin_conversation_member_list_context_cb);
+	                                        pidgin_conversation_message_unbind);
 }
 
 /******************************************************************************
diff --git a/pidgin/pidginmessage.c b/pidgin/pidginmessage.c
new file mode 100644
--- /dev/null
+++ b/pidgin/pidginmessage.c
@@ -0,0 +1,354 @@
+/*
+ * 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 "pidginmessage.h"
+
+#include "pidgincolor.h"
+
+struct _PidginMessage {
+	GtkBox parent;
+
+	PurpleMessage *message;
+
+	GtkWidget *avatar;
+	GtkWidget *author;
+	GtkWidget *edited;
+	GtkWidget *timestamp;
+};
+
+enum {
+	PROP_0,
+	PROP_MESSAGE,
+	N_PROPERTIES,
+};
+static GParamSpec *properties[N_PROPERTIES] = {NULL, };
+
+/******************************************************************************
+ * Helpers
+ *****************************************************************************/
+
+/**
+ * pidgin_message_set_tooltip_for_timestamp: (skip)
+ * @tooltip: The tooltip to update.
+ * @timestamp: The timestamp to set.
+ *
+ * Updates @tooltip to display @timestamp. This is meant to be called from
+ * a GtkWidget::query-tooltip signal and its return value should be returned
+ * from that handler.
+ *
+ * Returns: %TRUE if a tooltip was set, otherwise %FALSE.
+ *
+ * Since: 3.0
+ */
+static gboolean
+pidgin_message_set_tooltip_for_timestamp(GtkTooltip *tooltip,
+                                         GDateTime *timestamp)
+{
+	GDateTime *local = NULL;
+	char *text = NULL;
+
+	if(timestamp == NULL) {
+		return FALSE;
+	}
+
+	local = g_date_time_to_local(timestamp);
+	text = g_date_time_format(local, "%c");
+	g_clear_pointer(&local, g_date_time_unref);
+
+	gtk_tooltip_set_text(tooltip, text);
+	g_clear_pointer(&text, g_free);
+
+	return TRUE;
+}
+
+/******************************************************************************
+ * Callbacks
+ *****************************************************************************/
+static char *
+pidgin_message_get_author(G_GNUC_UNUSED GObject *self, PurpleMessage *message,
+                          G_GNUC_UNUSED gpointer data)
+{
+	if(PURPLE_IS_MESSAGE(message)) {
+		PurpleContactInfo *author = NULL;
+
+		author = purple_message_get_author(message);
+
+		if(PURPLE_IS_CONTACT_INFO(author)) {
+			return g_strdup(purple_contact_info_get_name_for_display(author));
+		}
+	}
+
+	return g_strdup("");
+}
+
+static PangoAttrList *
+pidgin_message_get_author_attributes(G_GNUC_UNUSED GObject *self,
+                                     PurpleMessage *message,
+                                     G_GNUC_UNUSED gpointer data)
+{
+	GdkRGBA rgba;
+	PangoAttrList *attrs = NULL;
+	gboolean color_valid = FALSE;
+	const char *custom_color = NULL;
+
+	if(!PURPLE_IS_MESSAGE(message)) {
+		return NULL;
+	}
+
+	custom_color = purple_message_get_author_name_color(message);
+	if(!purple_strempty(custom_color)) {
+		color_valid = gdk_rgba_parse(&rgba, custom_color);
+	}
+
+	if(!color_valid) {
+		PurpleContactInfo *author = NULL;
+		const char *name_for_display = NULL;
+
+		author = purple_message_get_author(message);
+		name_for_display = purple_contact_info_get_name_for_display(author);
+
+		pidgin_color_calculate_for_text(name_for_display, &rgba);
+		color_valid = TRUE;
+	}
+
+	attrs = pango_attr_list_new();
+
+	if(color_valid) {
+		PangoAttribute *attr = NULL;
+
+		attr = pango_attr_foreground_new(0xFFFF * rgba.red,
+		                                 0xFFFF * rgba.green,
+		                                 0xFFFF * rgba.blue);
+		pango_attr_list_insert(attrs, attr);
+	}
+
+	return attrs;
+}
+
+static PangoAttrList *
+pidgin_message_get_message_attributes(G_GNUC_UNUSED GObject *self,
+                                      PurpleMessage *message,
+                                      G_GNUC_UNUSED gpointer data)
+{
+	PangoAttrList *attrs = NULL;
+
+	if(!PURPLE_IS_MESSAGE(message)) {
+		return NULL;
+	}
+
+	attrs = pango_attr_list_new();
+
+	if(purple_message_get_action(message)) {
+		PangoAttribute *attr = NULL;
+
+		attr = pango_attr_style_new(PANGO_STYLE_ITALIC);
+
+		pango_attr_list_insert(attrs, attr);
+	}
+
+	return attrs;
+}
+
+static char *
+pidgin_message_process_message_contents_cb(G_GNUC_UNUSED GObject *self,
+                                           const char *contents,
+                                           G_GNUC_UNUSED gpointer data)
+{
+	char *escaped = NULL;
+	char *linkified = NULL;
+
+	escaped = g_markup_escape_text(contents, -1);
+	linkified = purple_markup_linkify(escaped);
+	g_free(escaped);
+
+	return linkified;
+}
+
+static gboolean
+pidgin_message_query_tooltip_edited_cb(G_GNUC_UNUSED GtkWidget *self,
+                                       G_GNUC_UNUSED gint x,
+                                       G_GNUC_UNUSED gint y,
+                                       G_GNUC_UNUSED gboolean keyboard_mode,
+                                       GtkTooltip *tooltip,
+                                       gpointer data)
+{
+	PurpleMessage *message = gtk_list_item_get_item(data);
+	GDateTime *timestamp = NULL;
+
+	if(!PURPLE_IS_MESSAGE(message)) {
+		return FALSE;
+	}
+
+	timestamp = purple_message_get_edited_at(message);
+
+	return pidgin_message_set_tooltip_for_timestamp(tooltip, timestamp);
+}
+
+static gboolean
+pidgin_message_query_tooltip_timestamp_cb(G_GNUC_UNUSED GtkWidget *self,
+                                          G_GNUC_UNUSED gint x,
+                                          G_GNUC_UNUSED gint y,
+                                          G_GNUC_UNUSED gboolean keyboard_mode,
+                                          GtkTooltip *tooltip,
+                                          gpointer data)
+{
+
+	PidginMessage *message = data;
+	GDateTime *timestamp = NULL;
+
+	if(!PURPLE_IS_MESSAGE(message->message)) {
+		return FALSE;
+	}
+
+	timestamp = purple_message_get_timestamp(message->message);
+
+	return pidgin_message_set_tooltip_for_timestamp(tooltip, timestamp);
+}
+
+/******************************************************************************
+ * GObject Implementation
+ *****************************************************************************/
+G_DEFINE_FINAL_TYPE(PidginMessage, pidgin_message, GTK_TYPE_BOX)
+
+static void
+pidgin_message_finalize(GObject *obj) {
+	PidginMessage *message = PIDGIN_MESSAGE(obj);
+
+	g_clear_object(&message->message);
+
+	G_OBJECT_CLASS(pidgin_message_parent_class)->finalize(obj);
+}
+
+static void
+pidgin_message_get_property(GObject *obj, guint param_id, GValue *value,
+                            GParamSpec *pspec)
+{
+	PidginMessage *message = PIDGIN_MESSAGE(obj);
+
+	switch(param_id) {
+	case PROP_MESSAGE:
+		g_value_set_object(value, pidgin_message_get_message(message));
+		break;
+	default:
+		G_OBJECT_WARN_INVALID_PROPERTY_ID(obj, param_id, pspec);
+		break;
+	}
+}
+
+static void
+pidgin_message_set_property(GObject *obj, guint param_id, const GValue *value,
+                            GParamSpec *pspec)
+{
+	PidginMessage *message = PIDGIN_MESSAGE(obj);
+
+	switch(param_id) {
+	case PROP_MESSAGE:
+		pidgin_message_set_message(message, g_value_get_object(value));
+		break;
+	default:
+		G_OBJECT_WARN_INVALID_PROPERTY_ID(obj, param_id, pspec);
+		break;
+	}
+}
+
+static void
+pidgin_message_init(PidginMessage *message) {
+	gtk_widget_init_template(GTK_WIDGET(message));
+}
+
+static void
+pidgin_message_class_init(PidginMessageClass *klass) {
+	GObjectClass *obj_class = G_OBJECT_CLASS(klass);
+	GtkWidgetClass *widget_class = GTK_WIDGET_CLASS(klass);
+
+	obj_class->finalize = pidgin_message_finalize;
+	obj_class->get_property = pidgin_message_get_property;
+	obj_class->set_property = pidgin_message_set_property;
+
+	/**
+	 * PidginMessage:message:
+	 *
+	 * The [class@Purple.Message] to be displayed.
+	 *
+	 * Since: 3.0
+	 */
+	properties[PROP_MESSAGE] = g_param_spec_object(
+		"message", NULL, NULL,
+		PURPLE_TYPE_MESSAGE,
+		G_PARAM_READWRITE | 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/Pidgin3/Conversations/message.ui"
+	);
+
+	gtk_widget_class_bind_template_callback(widget_class,
+	                                        pidgin_message_get_author);
+	gtk_widget_class_bind_template_callback(widget_class,
+	                                        pidgin_message_get_author_attributes);
+	gtk_widget_class_bind_template_callback(widget_class,
+	                                        pidgin_message_get_message_attributes);
+	gtk_widget_class_bind_template_callback(widget_class,
+	                                        pidgin_message_process_message_contents_cb);
+	gtk_widget_class_bind_template_callback(widget_class,
+	                                        pidgin_message_query_tooltip_edited_cb);
+	gtk_widget_class_bind_template_callback(widget_class,
+	                                        pidgin_message_query_tooltip_timestamp_cb);
+
+	gtk_widget_class_bind_template_child(widget_class, PidginMessage, avatar);
+	gtk_widget_class_bind_template_child(widget_class, PidginMessage, author);
+	gtk_widget_class_bind_template_child(widget_class, PidginMessage, edited);
+	gtk_widget_class_bind_template_child(widget_class, PidginMessage,
+	                                     timestamp);
+}
+
+/******************************************************************************
+ * Public API
+ *****************************************************************************/
+GtkWidget *
+pidgin_message_new(PurpleMessage *message) {
+	return g_object_new(
+		PIDGIN_TYPE_MESSAGE,
+		"message", message,
+		NULL);
+}
+
+PurpleMessage *
+pidgin_message_get_message(PidginMessage *message) {
+	g_return_val_if_fail(PIDGIN_IS_MESSAGE(message), NULL);
+
+	return message->message;
+}
+
+void
+pidgin_message_set_message(PidginMessage *pidgin_message,
+                           PurpleMessage *purple_message)
+{
+	g_return_if_fail(PIDGIN_IS_MESSAGE(pidgin_message));
+
+	if(g_set_object(&pidgin_message->message, purple_message)) {
+		g_object_notify_by_pspec(G_OBJECT(pidgin_message),
+		                         properties[PROP_MESSAGE]);
+	}
+}
diff --git a/pidgin/pidginmessage.h b/pidgin/pidginmessage.h
new file mode 100644
--- /dev/null
+++ b/pidgin/pidginmessage.h
@@ -0,0 +1,91 @@
+/*
+ * 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_MESSAGE_H
+#define PIDGIN_MESSAGE_H
+
+#include <glib.h>
+
+#include <gtk/gtk.h>
+
+#include <purple.h>
+
+#include "pidginversion.h"
+
+G_BEGIN_DECLS
+
+/**
+ * PidginMessage:
+ *
+ * #PidginMessage is a widget that displays [class@Purple.Message]'s.
+ *
+ * Since: 3.0
+ */
+
+#define PIDGIN_TYPE_MESSAGE (pidgin_message_get_type())
+
+PIDGIN_AVAILABLE_IN_3_0
+G_DECLARE_FINAL_TYPE(PidginMessage, pidgin_message, PIDGIN, MESSAGE, GtkBox)
+
+/**
+ * pidgin_message_new:
+ * @message: (nullable): the message to display
+ *
+ * Creates a new widget that will display @message.
+ *
+ * Returns: (transfer full): The new instance.
+ *
+ * Since: 3.0
+ */
+PIDGIN_AVAILABLE_IN_3_0
+GtkWidget *pidgin_message_new(PurpleMessage *message);
+
+/**
+ * pidgin_message_get_message:
+ *
+ * Gets the [class@Purple.Message] that is being displayed.
+ *
+ * Returns: (transfer none) (nullable): The message.
+ *
+ * Since: 3.0
+ */
+PIDGIN_AVAILABLE_IN_3_0
+PurpleMessage *pidgin_message_get_message(PidginMessage *message);
+
+/**
+ * pidgin_message_set_message:
+ * @purple_message: (nullable): the new message to display
+ *
+ * Sets the message of to be displayed to @message.
+ *
+ * Since: 3.0
+ */
+PIDGIN_AVAILABLE_IN_3_0
+void pidgin_message_set_message(PidginMessage *pidgin_message, PurpleMessage *purple_message);
+
+G_END_DECLS
+
+#endif /* PIDGIN_MESSAGE_H */
diff --git a/pidgin/resources/Conversations/conversation.ui b/pidgin/resources/Conversations/conversation.ui
--- a/pidgin/resources/Conversations/conversation.ui
+++ b/pidgin/resources/Conversations/conversation.ui
@@ -79,8 +79,10 @@
                 <property name="hexpand">1</property>
                 <property name="vexpand">1</property>
                 <property name="factory">
-                  <object class="GtkBuilderListItemFactory">
-                    <property name="resource">/im/pidgin/Pidgin3/Conversations/message.ui</property>
+                  <object class="GtkSignalListItemFactory">
+                    <signal name="setup" handler="pidgin_conversation_message_setup"/>
+                    <signal name="bind" handler="pidgin_conversation_message_bind"/>
+                    <signal name="unbind" handler="pidgin_conversation_message_unbind"/>
                   </object>
                 </property>
                 <property name="model">
diff --git a/pidgin/resources/Conversations/message.ui b/pidgin/resources/Conversations/message.ui
--- a/pidgin/resources/Conversations/message.ui
+++ b/pidgin/resources/Conversations/message.ui
@@ -18,97 +18,91 @@
 -->
 <interface>
   <requires lib="gtk" version="4.0"/>
-  <template class="GtkListItem">
-    <property name="child">
+  <template class="PidginMessage">
+    <property name="orientation">horizontal</property>
+    <child>
+      <object class="GtkEventControllerMotion" id="motion_controller"/>
+    </child>
+    <child>
+      <object class="GtkImage" id="avatar">
+        <property name="icon-name">image-missing</property>
+        <property name="visible">0</property>
+      </object>
+    </child>
+    <child>
       <object class="GtkBox">
-        <child>
-          <object class="GtkEventControllerMotion" id="motion_controller"/>
-        </child>
-        <child>
-          <object class="GtkImage" id="avatar">
-            <property name="icon-name">image-missing</property>
-            <property name="visible">0</property>
-          </object>
-        </child>
+        <property name="hexpand">1</property>
+        <property name="orientation">vertical</property>
         <child>
           <object class="GtkBox">
-            <property name="hexpand">1</property>
-            <property name="orientation">vertical</property>
+            <property name="spacing">8</property>
             <child>
-              <object class="GtkBox">
-                <property name="spacing">8</property>
-                <child>
-                  <object class="GtkLabel" id="author">
-                    <binding name="label">
-                      <lookup name="name-for-display" type="PurpleContactInfo">
-                        <lookup name="author" type="PurpleMessage">
-                          <lookup name="item">GtkListItem</lookup>
-                        </lookup>
-                      </lookup>
-                    </binding>
-                    <binding name="attributes">
-                      <closure type="PangoAttrList" function="pidgin_conversation_get_author_attributes">
-                        <lookup name="item">GtkListItem</lookup>
-                      </closure>
-                    </binding>
-                  </object>
-                </child>
-                <child>
-                  <object class="GtkLabel" id="timestamp">
-                    <binding name="visible">
-                      <lookup name="contains-pointer">motion_controller</lookup>
-                    </binding>
-                    <binding name="label">
-                      <closure type="gchararray" function="pidgin_converation_get_timestamp_string">
-                        <lookup name="item">GtkListItem</lookup>
-                      </closure>
-                    </binding>
-                    <property name="has-tooltip">1</property>
-                    <signal name="query-tooltip" handler="pidgin_conversation_query_tooltip_timestamp_cb"/>
-                  </object>
-                </child>
-                <child>
-                  <object class="GtkLabel" id="edited">
-                    <binding name="visible">
-                      <lookup name="edited" type="PurpleMessage">
-                        <lookup name="item">GtkListItem</lookup>
-                      </lookup>
-                    </binding>
-                    <property name="label" translatable="yes">(edited)</property>
-                    <property name="has-tooltip">1</property>
-                    <signal name="query-tooltip" handler="pidgin_conversation_query_tooltip_edited_cb"/>
-                  </object>
-                </child>
-              </object>
-            </child>
-            <child>
-              <object class="GtkLabel" id="content">
-                <property name="wrap">1</property>
-                <property name="wrap-mode">word-char</property>
-                <property name="xalign">0</property>
-                <property name="yalign">0</property>
-                <property name="use-markup">1</property>
+              <object class="GtkLabel" id="author">
                 <binding name="label">
-                  <!-- this file is used by a GtkItemFactory in
-                      PidginConversation which is why the function name has a
-                      pidgin_conversation prefix.
+                  <!-- The first argument to the closure is automatically a
+                       PidginMessage, but if we don't do the lookup the message
+                       property on it isn't set yet for some reason.
                     -->
-                  <closure type="gchararray" function="pidgin_conversation_process_message_contents_cb">
-                    <lookup name="contents" type="PurpleMessage">
-                      <lookup name="item">GtkListItem</lookup>
-                    </lookup>
+                  <closure type="gchararray" function="pidgin_message_get_author">
+                    <lookup name="message">PidginMessage</lookup>
                   </closure>
                 </binding>
                 <binding name="attributes">
-                  <closure type="PangoAttrList" function="pidgin_conversation_get_message_attributes">
-                    <lookup name="item">GtkListItem</lookup>
+                  <closure type="PangoAttrList" function="pidgin_message_get_author_attributes">
+                    <lookup name="message">PidginMessage</lookup>
                   </closure>
                 </binding>
               </object>
             </child>
+            <child>
+              <object class="GtkLabel" id="timestamp">
+                <binding name="visible">
+                  <lookup name="contains-pointer">motion_controller</lookup>
+                </binding>
+                <property name="has-tooltip">1</property>
+                <signal name="query-tooltip" handler="pidgin_message_query_tooltip_timestamp_cb"/>
+              </object>
+            </child>
+            <child>
+              <object class="GtkLabel" id="edited">
+                <binding name="visible">
+                  <lookup name="edited" type="PurpleMessage">
+                    <lookup name="message">PidginMessage</lookup>
+                  </lookup>
+                </binding>
+                <property name="label" translatable="yes">(edited)</property>
+                <property name="has-tooltip">1</property>
+                <signal name="query-tooltip" handler="pidgin_message_query_tooltip_edited_cb"/>
+              </object>
+            </child>
+          </object>
+        </child>
+        <child>
+          <object class="GtkLabel" id="content">
+            <property name="wrap">1</property>
+            <property name="wrap-mode">word-char</property>
+            <property name="xalign">0</property>
+            <property name="yalign">0</property>
+            <property name="use-markup">1</property>
+            <binding name="label">
+              <!-- this file is used by a GtkItemFactory in
+                  PidginConversation which is why the function name has a
+                  pidgin_conversation prefix.
+                -->
+              <closure type="gchararray" function="pidgin_message_process_message_contents_cb">
+                <lookup name="contents" type="PurpleMessage">
+                  <lookup name="message">PidginMessage</lookup>
+                </lookup>
+              </closure>
+            </binding>
+            <binding name="attributes">
+              <closure type="PangoAttrList" function="pidgin_message_get_message_attributes">
+                <lookup name="message">PidginMessage</lookup>
+              </closure>
+            </binding>
           </object>
         </child>
       </object>
-    </property>
+    </child>
   </template>
 </interface>
