changeset 29821:bf0cbb79d629

propagate from branch 'im.pidgin.pidgin' (head 01b93cf5c82386bcea9bfacb2bd35a7d73dd612c) to branch 'im.pidgin.cpw.malu.ft_thumbnails' (head 2ed1751127a572260300b706a2d29f760a0c9406)
author Marcus Lundblad <ml@update.uu.se>
date Sun, 14 Mar 2010 21:20:14 +0000
parents 611832fe4ee2 (current diff) 0fe92a64771d (diff)
children 25a53c299713
files
diffstat 13 files changed, 428 insertions(+), 43 deletions(-) [+]
line wrap: on
line diff
--- a/libpurple/ft.c	Sun Mar 14 19:18:05 2010 +0000
+++ b/libpurple/ft.c	Sun Mar 14 21:20:14 2010 +0000
@@ -178,6 +178,7 @@
 	g_free(xfer->local_filename);
 
 	g_hash_table_remove(xfers_data, xfer);
+	g_free(xfer->thumbnail_data);
 
 	PURPLE_DBUS_UNREGISTER_POINTER(xfer);
 	xfers = g_list_remove(xfers, xfer);
@@ -264,11 +265,14 @@
 	}
 }
 
-void purple_xfer_conversation_write(PurpleXfer *xfer, char *message, gboolean is_error)
+static void 
+purple_xfer_conversation_write_internal(PurpleXfer *xfer,
+	const char *message, gboolean is_error, gboolean print_thumbnail)
 {
 	PurpleConversation *conv = NULL;
 	PurpleMessageFlags flags = PURPLE_MESSAGE_SYSTEM;
 	char *escaped;
+	const gpointer *thumbnail_data = purple_xfer_get_thumbnail_data(xfer);
 
 	g_return_if_fail(xfer != NULL);
 	g_return_if_fail(message != NULL);
@@ -284,10 +288,40 @@
 	if (is_error)
 		flags |= PURPLE_MESSAGE_ERROR;
 
-	purple_conversation_write(conv, NULL, escaped, flags, time(NULL));
+	if (print_thumbnail && thumbnail_data) {
+		gchar *message_with_img;
+		gsize size = purple_xfer_get_thumbnail_size(xfer);
+		gpointer data = g_memdup(thumbnail_data, size); 
+		int id = purple_imgstore_add_with_id(data, size, NULL);
+
+		message_with_img = 
+			g_strdup_printf("<img id='%d'/> %s", id, escaped);
+		purple_conversation_write(conv, NULL, message_with_img, flags, 
+			time(NULL));
+		purple_imgstore_unref_by_id(id);
+		g_free(message_with_img);
+	} else {
+		purple_conversation_write(conv, NULL, escaped, flags, time(NULL));
+	}
 	g_free(escaped);
 }
 
+void
+purple_xfer_conversation_write(PurpleXfer *xfer, gchar *message,
+	gboolean is_error)
+{
+	purple_xfer_conversation_write_internal(xfer, message, is_error, FALSE);
+}
+
+/* maybe this one should be exported puplically? */
+static void
+purple_xfer_conversation_write_with_thumbnail(PurpleXfer *xfer,
+	const gchar *message)
+{
+	purple_xfer_conversation_write_internal(xfer, message, FALSE, TRUE);
+}
+
+
 static void purple_xfer_show_file_error(PurpleXfer *xfer, const char *filename)
 {
 	int err = errno;
@@ -471,13 +505,20 @@
 			serv_got_im(purple_account_get_connection(xfer->account),
 								 xfer->who, xfer->message, 0, time(NULL));
 
-		purple_request_accept_cancel(xfer, NULL, buf, NULL,
-								  PURPLE_DEFAULT_ACTION_NONE,
-								  xfer->account, xfer->who, NULL,
-								  xfer,
-								  G_CALLBACK(purple_xfer_choose_file),
-								  G_CALLBACK(cancel_recv_cb));
-
+		if (purple_xfer_get_thumbnail_data(xfer)) {
+			purple_request_accept_cancel_with_icon(xfer, NULL, buf, NULL,
+				PURPLE_DEFAULT_ACTION_NONE, xfer->account, xfer->who, NULL,
+				purple_xfer_get_thumbnail_data(xfer),
+				purple_xfer_get_thumbnail_size(xfer), xfer, 
+				G_CALLBACK(purple_xfer_choose_file),
+				G_CALLBACK(cancel_recv_cb));
+		} else {
+			purple_request_accept_cancel(xfer, NULL, buf, NULL,
+				PURPLE_DEFAULT_ACTION_NONE, xfer->account, xfer->who, NULL,
+				xfer, G_CALLBACK(purple_xfer_choose_file),
+				G_CALLBACK(cancel_recv_cb));
+		}
+			
 		g_free(buf);
 	} else
 		purple_xfer_choose_file(xfer);
@@ -545,10 +586,12 @@
 		{
 			gchar* message = NULL;
 			PurpleBuddy *buddy = purple_find_buddy(xfer->account, xfer->who);
+
 			message = g_strdup_printf(_("%s is offering to send file %s"),
 				buddy ? purple_buddy_get_alias(buddy) : xfer->who, purple_xfer_get_filename(xfer));
-			purple_xfer_conversation_write(xfer, message, FALSE);
+			purple_xfer_conversation_write_with_thumbnail(xfer, message);
 			g_free(message);
+
 			/* Ask for a filename to save to if it's not already given by a plugin */
 			if (xfer->local_filename == NULL)
 				purple_xfer_ask_recv(xfer);
@@ -1575,6 +1618,35 @@
 		ui_ops->update_progress(xfer, purple_xfer_get_progress(xfer));
 }
 
+const void *
+purple_xfer_get_thumbnail_data(const PurpleXfer *xfer)
+{
+	return xfer->thumbnail_data;
+}
+
+gsize
+purple_xfer_get_thumbnail_size(const PurpleXfer *xfer)
+{
+	return xfer->thumbnail_size;
+}
+
+void
+purple_xfer_set_thumbnail(PurpleXfer *xfer, gconstpointer thumbnail,
+	gsize size)
+{
+	if (thumbnail && size > 0) {
+		xfer->thumbnail_data = g_memdup(thumbnail, size);
+		xfer->thumbnail_size = size;
+	}
+}
+
+void
+purple_xfer_prepare_thumbnail(PurpleXfer *xfer)
+{
+	if (xfer->ui_ops->add_thumbnail) {
+		xfer->ui_ops->add_thumbnail(xfer);
+	}
+}
 
 /**************************************************************************
  * File Transfer Subsystem API
--- a/libpurple/ft.h	Sun Mar 14 19:18:05 2010 +0000
+++ b/libpurple/ft.h	Sun Mar 14 21:20:14 2010 +0000
@@ -120,7 +120,12 @@
 	 */
 	void (*data_not_sent)(PurpleXfer *xfer, const guchar *buffer, gsize size);
 
-	void (*_purple_reserved1)(void);
+	/**
+	 * Op to create a thumbnail image for a file transfer
+	 *
+	 * @param xfer   The file transfer structure
+	 */
+	void (*add_thumbnail)(PurpleXfer *xfer);
 } PurpleXferUiOps;
 
 /**
@@ -181,6 +186,9 @@
 	void *ui_data;                    /**< UI-specific data.       */
 
 	void *data;                       /**< prpl-specific data.     */
+
+	gpointer thumbnail_data;		/**< thumbnail image */
+	gsize thumbnail_size;
 };
 
 #ifdef __cplusplus
@@ -687,6 +695,42 @@
  */
 void purple_xfer_prpl_ready(PurpleXfer *xfer);
 
+/**
+ * Gets the thumbnail data for a transfer
+ *
+ * @param xfer The file transfer to get the thumbnail for
+ * @return The thumbnail data, or NULL if there is no thumbnail
+ */
+const void *purple_xfer_get_thumbnail_data(const PurpleXfer *xfer);
+
+/**
+ * Gets the thumbnail size for a transfer
+ *
+ * @param xfer The file transfer to get the thumbnail size for
+ * @return The size, in bytes of the file transfer's thumbnail
+ */
+gsize purple_xfer_get_thumbnail_size(const PurpleXfer *xfer);
+
+
+/**
+ * Sets the thumbnail data for a transfer
+ *
+ * @param xfer The file transfer to set the data for
+ * @param thumbnail A pointer to the thumbnail data, this will be copied
+ * @param size The size in bytes of the passed in thumbnail data
+ */
+void purple_xfer_set_thumbnail(PurpleXfer *xfer, gconstpointer thumbnail,
+	gsize size);
+
+/**
+ * Prepare a thumbnail for a transfer (if the UI supports it)
+ * will be no-op in case the UI doesn't implement thumbnail creation
+ *
+ * @param xfer The file transfer to create a thumbnail for
+ */
+void purple_xfer_prepare_thumbnail(PurpleXfer *xfer);
+
+
 /*@}*/
 
 /**************************************************************************/
--- a/libpurple/protocols/jabber/data.c	Sun Mar 14 19:18:05 2010 +0000
+++ b/libpurple/protocols/jabber/data.c	Sun Mar 14 21:20:14 2010 +0000
@@ -36,7 +36,7 @@
 
 JabberData *
 jabber_data_create_from_data(gconstpointer rawdata, gsize size, const char *type,
-	JabberStream *js)
+	gboolean ephemeral, JabberStream *js)
 {
 	JabberData *data = g_new0(JabberData, 1);
 	gchar *checksum = jabber_calculate_data_sha1sum(rawdata, size);
@@ -48,6 +48,7 @@
 	data->cid = g_strdup(cid);
 	data->type = g_strdup(type);
 	data->size = size;
+	data->ephemeral = ephemeral;
 
 	data->data = g_memdup(rawdata, size);
 
@@ -110,6 +111,12 @@
 	g_free(data);
 }
 
+void
+jabber_data_destroy(JabberData *data)
+{
+	jabber_data_delete(data);
+}
+
 const char *
 jabber_data_get_cid(const JabberData *data)
 {
@@ -179,21 +186,21 @@
 const JabberData *
 jabber_data_find_local_by_alt(const gchar *alt)
 {
-	purple_debug_info("jabber", "looking up local smiley with alt = %s\n", alt);
+	purple_debug_info("jabber", "looking up local data object with alt = %s\n", alt);
 	return g_hash_table_lookup(local_data_by_alt, alt);
 }
 
 const JabberData *
 jabber_data_find_local_by_cid(const gchar *cid)
 {
-	purple_debug_info("jabber", "lookup local smiley with cid = %s\n", cid);
+	purple_debug_info("jabber", "lookup local data object with cid = %s\n", cid);
 	return g_hash_table_lookup(local_data_by_cid, cid);
 }
 
 const JabberData *
 jabber_data_find_remote_by_cid(const gchar *cid)
 {
-	purple_debug_info("jabber", "lookup remote smiley with cid = %s\n", cid);
+	purple_debug_info("jabber", "lookup remote data object with cid = %s\n", cid);
 
 	return g_hash_table_lookup(remote_data_by_cid, cid);
 }
@@ -201,9 +208,10 @@
 void
 jabber_data_associate_local(JabberData *data, const gchar *alt)
 {
-	purple_debug_info("jabber", "associating local smiley\n alt = %s, cid = %s\n",
-		alt, jabber_data_get_cid(data));
-	g_hash_table_insert(local_data_by_alt, g_strdup(alt), data);
+	purple_debug_info("jabber", "associating local data object\n alt = %s, cid = %s\n",
+		alt , jabber_data_get_cid(data));
+	if (alt)
+		g_hash_table_insert(local_data_by_alt, g_strdup(alt), data);
 	g_hash_table_insert(local_data_by_cid, g_strdup(jabber_data_get_cid(data)),
 		data);
 }
@@ -211,7 +219,7 @@
 void
 jabber_data_associate_remote(JabberData *data)
 {
-	purple_debug_info("jabber", "associating remote smiley, cid = %s\n",
+	purple_debug_info("jabber", "associating remote data object, cid = %s\n",
 		jabber_data_get_cid(data));
 	g_hash_table_insert(remote_data_by_cid, g_strdup(jabber_data_get_cid(data)),
 		data);
@@ -240,6 +248,11 @@
 		xmlnode_set_attrib(result->node, "id", id);
 		xmlnode_insert_child(result->node,
 							 jabber_data_get_xml_definition(data));
+		/* if the data object is temporary, destroy it and remove the references
+		 to it */
+		if (data->ephemeral) {
+			g_hash_table_remove(local_data_by_cid, cid);
+		}
 	}
 	jabber_iq_send(result);
 }
--- a/libpurple/protocols/jabber/data.h	Sun Mar 14 19:18:05 2010 +0000
+++ b/libpurple/protocols/jabber/data.h	Sun Mar 14 21:20:14 2010 +0000
@@ -33,16 +33,21 @@
 	char *type;
 	gsize size;
 	gpointer data;
+	gboolean ephemeral;
 } JabberData;
 
 /* creates a JabberData instance from raw data */
 JabberData *jabber_data_create_from_data(gconstpointer data, gsize size,
-										 const char *type, JabberStream *js);
+	const char *type, gboolean ephemeral, JabberStream *js);
 
 /* create a JabberData instance from an XML "data" element (as defined by
   XEP 0231 */
 JabberData *jabber_data_create_from_xml(xmlnode *tag);
 
+/* destroy a JabberData instance, NOT to be used on data that has been
+	associated, since they get "owned" */
+void jabber_data_destroy(JabberData *data);
+
 const char *jabber_data_get_cid(const JabberData *data);
 const char *jabber_data_get_type(const JabberData *data);
 
--- a/libpurple/protocols/jabber/message.c	Sun Mar 14 19:18:05 2010 +0000
+++ b/libpurple/protocols/jabber/message.c	Sun Mar 14 21:20:14 2010 +0000
@@ -1002,7 +1002,7 @@
 						JabberData *new_data =
 							jabber_data_create_from_data(purple_imgstore_get_data(image),
 								purple_imgstore_get_size(image),
-								jabber_message_get_mimetype_from_ext(ext), js);
+								jabber_message_get_mimetype_from_ext(ext), FALSE, js);
 						purple_debug_info("jabber",
 							"cache local smiley alt = %s, cid = %s\n",
 							shortcut, jabber_data_get_cid(new_data));
--- a/libpurple/protocols/jabber/namespaces.h	Sun Mar 14 19:18:05 2010 +0000
+++ b/libpurple/protocols/jabber/namespaces.h	Sun Mar 14 21:20:14 2010 +0000
@@ -88,6 +88,9 @@
 /* XEP-0237 Roster Versioning */
 #define NS_ROSTER_VERSIONING "urn:xmpp:features:rosterver"
 
+/* XEP-0264 File Transfer Thumbnails (Thumbs) */
+#define NS_THUMBS "urn:xmpp:thumbs:0"
+
 /* Google extensions */
 #define NS_GOOGLE_CAMERA "http://www.google.com/xmpp/protocol/camera/v1"
 #define NS_GOOGLE_VIDEO "http://www.google.com/xmpp/protocol/video/v1"
--- a/libpurple/protocols/jabber/si.c	Sun Mar 14 19:18:05 2010 +0000
+++ b/libpurple/protocols/jabber/si.c	Sun Mar 14 21:20:14 2010 +0000
@@ -32,6 +32,7 @@
 #include "notify.h"
 
 #include "buddy.h"
+#include "data.h"
 #include "disco.h"
 #include "jabber.h"
 #include "ibb.h"
@@ -1245,7 +1246,8 @@
 	char buf[32];
 
 	xfer->filename = g_path_get_basename(xfer->local_filename);
-
+	purple_xfer_prepare_thumbnail(xfer);
+	
 	iq = jabber_iq_new(jsx->js, JABBER_IQ_SET);
 	xmlnode_set_attrib(iq->node, "to", xfer->who);
 	si = xmlnode_new_child(iq->node, "si");
@@ -1263,6 +1265,21 @@
 	xmlnode_set_attrib(file, "size", buf);
 	/* maybe later we'll do hash and date attribs */
 
+	/* add thumbnail, if appropriate */
+	if (purple_xfer_get_thumbnail_data(xfer)) {
+		JabberData *thumbnail_data = 
+			jabber_data_create_from_data(purple_xfer_get_thumbnail_data(xfer),
+				purple_xfer_get_thumbnail_size(xfer), "image/png", TRUE,
+				jsx->js);
+		xmlnode *thumbnail = xmlnode_new_child(file, "thumbnail");
+		xmlnode_set_namespace(thumbnail, NS_THUMBS);
+		xmlnode_set_attrib(thumbnail, "cid", 
+			jabber_data_get_cid(thumbnail_data));
+		xmlnode_set_attrib(thumbnail, "mime-type", "image/png");
+		/* cache data */
+		jabber_data_associate_local(thumbnail_data, NULL);
+	}
+						  
 	feature = xmlnode_new_child(si, "feature");
 	xmlnode_set_namespace(feature, "http://jabber.org/protocol/feature-neg");
 	x = xmlnode_new_child(feature, "x");
@@ -1641,12 +1658,37 @@
 		purple_xfer_request(xfer);
 }
 
+static void
+jabber_si_thumbnail_cb(JabberStream *js, const char *from, JabberIqType type, 
+	const char *id, xmlnode *packet, gpointer data)
+{
+	PurpleXfer *xfer = (PurpleXfer *) data;
+	xmlnode *data_element = xmlnode_get_child(packet, "data");
+	xmlnode *item_not_found = xmlnode_get_child(packet, "item-not-found");
+
+	if (data_element) {
+		JabberData *data = jabber_data_create_from_xml(data_element);
+
+		if (data) {
+			purple_xfer_set_thumbnail(xfer, jabber_data_get_data(data),
+				jabber_data_get_size(data));
+			jabber_data_destroy(data);
+		}
+	} else if (item_not_found) {
+		purple_debug_info("jabber",
+			"Responder didn't recognize requested data\n");
+	} else {
+		purple_debug_error("jabber", "Unknown response to data request\n");
+	}
+	purple_xfer_request(xfer);
+}
+
 void jabber_si_parse(JabberStream *js, const char *from, JabberIqType type,
                      const char *id, xmlnode *si)
 {
 	JabberSIXfer *jsx;
 	PurpleXfer *xfer;
-	xmlnode *file, *feature, *x, *field, *option, *value;
+	xmlnode *file, *feature, *x, *field, *option, *value, *thumbnail;
 	const char *stream_id, *filename, *filesize_c, *profile;
 	size_t filesize = 0;
 
@@ -1728,10 +1770,30 @@
 	purple_xfer_set_request_denied_fnc(xfer, jabber_si_xfer_request_denied);
 	purple_xfer_set_cancel_recv_fnc(xfer, jabber_si_xfer_cancel_recv);
 	purple_xfer_set_end_fnc(xfer, jabber_si_xfer_end);
-
+				   
 	js->file_transfers = g_list_append(js->file_transfers, xfer);
 
-	purple_xfer_request(xfer);
+	/* if there is a thumbnail, we should request it... */
+	if ((thumbnail = xmlnode_get_child_with_namespace(file, "thumbnail",
+		NS_THUMBS))) {
+		const char *cid = xmlnode_get_attrib(thumbnail, "cid");
+		if (cid) {
+			JabberIq *request = 
+				jabber_iq_new(jsx->js, JABBER_IQ_GET);
+
+			purple_debug_info("jabber", "got file thumbnail, request it\n");
+			xmlnode_insert_child(request->node, 
+				jabber_data_get_xml_request(cid));
+			xmlnode_set_attrib(request->node, "to", 
+				purple_xfer_get_remote_user(xfer));
+			jabber_iq_set_callback(request, jabber_si_thumbnail_cb, xfer);
+			jabber_iq_send(request);
+		} else {
+			purple_xfer_request(xfer);
+		}
+	} else {
+		purple_xfer_request(xfer);
+	}
 }
 
 void
--- a/libpurple/protocols/msn/slp.c	Sun Mar 14 19:18:05 2010 +0000
+++ b/libpurple/protocols/msn/slp.c	Sun Mar 14 21:20:14 2010 +0000
@@ -422,6 +422,11 @@
 
 			xfer->data = slpcall;
 
+			if (header->type == 0 && bin_len >= sizeof(MsnFileContext)) {
+				purple_xfer_set_thumbnail(xfer, &header->preview,
+				                          bin_len - sizeof(MsnFileContext));
+			}
+
 			purple_xfer_request(xfer);
 		}
 		g_free(header);
--- a/libpurple/protocols/msn/slplink.c	Sun Mar 14 19:18:05 2010 +0000
+++ b/libpurple/protocols/msn/slplink.c	Sun Mar 14 21:20:14 2010 +0000
@@ -662,15 +662,19 @@
 gen_context(PurpleXfer *xfer, const char *file_name, const char *file_path)
 {
 	gsize size = 0;
-	MsnFileContext header;
+	MsnFileContext *header;
 	gchar *u8 = NULL;
 	gchar *ret;
 	gunichar2 *uni = NULL;
 	glong currentChar = 0;
 	glong len = 0;
+	const char *preview;
+	gsize preview_len;
 
 	size = purple_xfer_get_size(xfer);
 
+	purple_xfer_prepare_thumbnail(xfer);
+
 	if (!file_name) {
 		gchar *basename = g_path_get_basename(file_path);
 		u8 = purple_utf8_try_convert(basename);
@@ -686,23 +690,37 @@
 		u8 = NULL;
 	}
 
-	header.length = GUINT32_TO_LE(sizeof(MsnFileContext) - 1);
-	header.version = GUINT32_TO_LE(2); /* V.3 contains additional unnecessary data */
-	header.file_size = GUINT64_TO_LE(size);
-	header.type = GUINT32_TO_LE(1);    /* No file preview */
+	preview = purple_xfer_get_thumbnail_data(xfer);
+	if (preview)
+		preview_len = purple_xfer_get_thumbnail_size(xfer);
+	else
+		preview_len = 0;
+	header = g_malloc(sizeof(MsnFileContext) + preview_len);
+
+	header->length = GUINT32_TO_LE(sizeof(MsnFileContext) - 1);
+	header->version = GUINT32_TO_LE(2); /* V.3 contains additional unnecessary data */
+	header->file_size = GUINT64_TO_LE(size);
+	if (preview)
+		header->type = GUINT32_TO_LE(0);
+	else
+		header->type = GUINT32_TO_LE(1);
 
 	len = MIN(len, MAX_FILE_NAME_LEN);
 	for (currentChar = 0; currentChar < len; currentChar++) {
-		header.file_name[currentChar] = GUINT16_TO_LE(uni[currentChar]);
+		header->file_name[currentChar] = GUINT16_TO_LE(uni[currentChar]);
 	}
-	memset(&header.file_name[currentChar], 0x00, (MAX_FILE_NAME_LEN - currentChar) * 2);
+	memset(&header->file_name[currentChar], 0x00, (MAX_FILE_NAME_LEN - currentChar) * 2);
 
-	memset(&header.unknown1, 0, sizeof(header.unknown1));
-	header.unknown2 = GUINT32_TO_LE(0xffffffff);
-	header.preview[0] = '\0';
+	memset(&header->unknown1, 0, sizeof(header->unknown1));
+	header->unknown2 = GUINT32_TO_LE(0xffffffff);
+	if (preview) {
+		memcpy(&header->preview, preview, preview_len);
+	}
+	header->preview[preview_len] = '\0';
 
 	g_free(uni);
-	ret = purple_base64_encode((const guchar *)&header, sizeof(MsnFileContext));
+	ret = purple_base64_encode((const guchar *)header, sizeof(MsnFileContext) + preview_len);
+	g_free(header);
 	return ret;
 }
 
--- a/libpurple/request.c	Sun Mar 14 19:18:05 2010 +0000
+++ b/libpurple/request.c	Sun Mar 14 21:20:14 2010 +0000
@@ -1317,6 +1317,29 @@
 }
 
 void *
+purple_request_action_with_icon(void *handle, const char *title, 
+					const char *primary,
+					const char *secondary, int default_action,
+					PurpleAccount *account, const char *who, 
+					PurpleConversation *conv, gconstpointer icon_data,
+					gsize icon_size, void *user_data, size_t action_count, ...)
+{
+	void *ui_handle;
+	va_list args;
+
+	g_return_val_if_fail(action_count > 0, NULL);
+
+	va_start(args, action_count);
+	ui_handle = purple_request_action_varg_with_icon(handle, title, primary, 
+		secondary, default_action, account, who, conv, icon_data, icon_size,
+		user_data, action_count, args);
+	va_end(args);
+
+	return ui_handle;
+}
+
+
+void *
 purple_request_action_varg(void *handle, const char *title,
 						 const char *primary, const char *secondary,
 						 int default_action,
@@ -1348,6 +1371,41 @@
 }
 
 void *
+purple_request_action_varg_with_icon(void *handle, const char *title,
+						 const char *primary, const char *secondary,
+						 int default_action,
+						 PurpleAccount *account, const char *who, 
+						 PurpleConversation *conv, gconstpointer icon_data,
+						 gsize icon_size,
+						 void *user_data, size_t action_count, va_list actions)
+{
+	PurpleRequestUiOps *ops;
+
+	g_return_val_if_fail(action_count > 0, NULL);
+
+	ops = purple_request_get_ui_ops();
+
+	if (ops != NULL && ops->request_action != NULL) {
+		PurpleRequestInfo *info;
+
+		info            = g_new0(PurpleRequestInfo, 1);
+		info->type      = PURPLE_REQUEST_ACTION;
+		info->handle    = handle;
+		info->ui_handle = ops->request_action_with_icon(title, primary, secondary,
+											  default_action, account, who, conv,
+											  icon_data, icon_size,
+											  user_data, action_count, actions);
+
+		handles = g_list_append(handles, info);
+
+		return info->ui_handle;
+	}
+
+	return NULL;
+}
+
+
+void *
 purple_request_fields(void *handle, const char *title, const char *primary,
 					const char *secondary, PurpleRequestFields *fields,
 					const char *ok_text, GCallback ok_cb,
--- a/libpurple/request.h	Sun Mar 14 19:18:05 2010 +0000
+++ b/libpurple/request.h	Sun Mar 14 21:20:14 2010 +0000
@@ -237,10 +237,18 @@
 	                        PurpleAccount *account, const char *who,
 	                        PurpleConversation *conv, void *user_data);
 
+	/** @see purple_request_action_varg_with_icon(). */
+	void *(*request_action_with_icon)(const char *title, const char *primary,
+	                        const char *secondary, int default_action,
+	                        PurpleAccount *account, const char *who,
+	                        PurpleConversation *conv, 
+							gconstpointer icon_data, gsize icon_size,
+							void *user_data,
+	                        size_t action_count, va_list actions);
+
 	void (*_purple_reserved1)(void);
 	void (*_purple_reserved2)(void);
 	void (*_purple_reserved3)(void);
-	void (*_purple_reserved4)(void);
 } PurpleRequestUiOps;
 
 typedef void (*PurpleRequestInputCb)(void *, const char *);
@@ -1393,6 +1401,27 @@
 	void *user_data, size_t action_count, va_list actions);
 
 /**
+ * Version of purple_request_action() supplying an image for the UI to 
+ * optionally display as an icon in the dialog; see its documentation
+ */
+void *purple_request_action_with_icon(void *handle, const char *title, 
+	const char *primary, const char *secondary, int default_action, 
+	PurpleAccount *account, const char *who, PurpleConversation *conv, 
+	gconstpointer icon_data, gsize icon_size, void *user_data, 
+	size_t action_count, ...);
+
+/**
+ * <tt>va_list</tt> version of purple_request_action_with_icon(); 
+ * see its documentation.
+ */
+void *purple_request_action_varg_with_icon(void *handle, const char *title,
+	const char *primary, const char *secondary, int default_action,
+	PurpleAccount *account, const char *who, PurpleConversation *conv,
+	gconstpointer icon_data, gsize icon_size,
+	void *user_data, size_t action_count, va_list actions);
+
+
+/**
  * Displays groups of fields for the user to fill in.
  *
  * @param handle      The plugin or connection handle.  For some things this
@@ -1477,6 +1506,19 @@
 						_("_Accept"), (accept_cb), _("_Cancel"), (cancel_cb))
 
 /**
+ * A wrapper for purple_request_action_with_icon() that uses Accept and Cancel 
+ * buttons.
+ */
+#define purple_request_accept_cancel_with_icon(handle, title, primary, secondary, \
+								   default_action, account, who, conv, \
+								   icon_data, icon_size, \
+								   user_data, accept_cb, cancel_cb) \
+	purple_request_action_with_icon((handle), (title), (primary), (secondary), \
+						(default_action), account, who, conv, icon_data, icon_size, \
+						(user_data), 2, \
+						_("_Accept"), (accept_cb), _("_Cancel"), (cancel_cb))
+
+/**
  * Displays a file selector request dialog.  Returns the selected filename to
  * the callback.  Can be used for either opening a file or saving a file.
  *
--- a/pidgin/gtkft.c	Sun Mar 14 19:18:05 2010 +0000
+++ b/pidgin/gtkft.c	Sun Mar 14 21:20:14 2010 +0000
@@ -40,6 +40,9 @@
 #define PIDGINXFER(xfer) \
 	(PidginXferUiData *)(xfer)->ui_data
 
+/* the maximum size of files we will try to make a thumbnail for */
+#define PIDGIN_XFER_MAX_SIZE_IMAGE_THUMBNAIL 10 * 1024 * 1024
+
 struct _PidginXferDialog
 {
 	gboolean keep_open;
@@ -1157,6 +1160,36 @@
 		pidgin_xfer_dialog_cancel_xfer(xfer_dialog, xfer);
 }
 
+static void
+pidgin_xfer_add_thumbnail(PurpleXfer *xfer)
+{
+	purple_debug_info("pidgin", "creating thumbnail for transfer\n");
+
+	if (purple_xfer_get_size(xfer) <= PIDGIN_XFER_MAX_SIZE_IMAGE_THUMBNAIL) {
+		GdkPixbuf *thumbnail = 
+			gdk_pixbuf_new_from_file_at_size(
+				purple_xfer_get_local_filename(xfer), 128, 128, NULL);
+
+		if (thumbnail) {
+			gchar *buffer = NULL;
+			gsize size;
+			char *option_keys[2] = {"compression", NULL};
+			char *option_values[2] = {"9", NULL};
+			gdk_pixbuf_save_to_bufferv(thumbnail, &buffer, &size, "png", 
+				option_keys, option_values, NULL);
+
+			if (buffer) {
+				purple_debug_info("pidgin",
+				                  "created thumbnail of %" G_GSIZE_FORMAT " bytes\n",
+					size);
+				purple_xfer_set_thumbnail(xfer, buffer, size);
+				g_free(buffer);
+			}
+			g_object_unref(thumbnail);
+		}
+	}
+}
+
 static PurpleXferUiOps ops =
 {
 	pidgin_xfer_new_xfer,
@@ -1168,7 +1201,7 @@
 	NULL,
 	NULL,
 	NULL,
-	NULL
+	pidgin_xfer_add_thumbnail
 };
 
 /**************************************************************************
--- a/pidgin/gtkrequest.c	Sun Mar 14 19:18:05 2010 +0000
+++ b/pidgin/gtkrequest.c	Sun Mar 14 21:20:14 2010 +0000
@@ -26,6 +26,7 @@
 #include "internal.h"
 #include "pidgin.h"
 
+#include "debug.h"
 #include "prefs.h"
 #include "util.h"
 
@@ -592,9 +593,11 @@
 }
 
 static void *
-pidgin_request_action(const char *title, const char *primary,
+pidgin_request_action_with_icon(const char *title, const char *primary,
 						const char *secondary, int default_action,
-					    PurpleAccount *account, const char *who, PurpleConversation *conv,
+					    PurpleAccount *account, const char *who, 
+						PurpleConversation *conv, gconstpointer icon_data,
+						gsize icon_size,
 						void *user_data, size_t action_count, va_list actions)
 {
 	PidginRequestData *data;
@@ -602,7 +605,7 @@
 	GtkWidget *vbox;
 	GtkWidget *hbox;
 	GtkWidget *label;
-	GtkWidget *img;
+	GtkWidget *img = NULL;
 	void **buttons;
 	char *label_text;
 	char *primary_esc, *secondary_esc;
@@ -659,8 +662,25 @@
 	gtk_container_add(GTK_CONTAINER(GTK_DIALOG(dialog)->vbox), hbox);
 
 	/* Dialog icon. */
-	img = gtk_image_new_from_stock(PIDGIN_STOCK_DIALOG_QUESTION,
+	if (icon_data) {
+		GdkPixbufLoader *loader = gdk_pixbuf_loader_new();
+		GdkPixbuf *pixbuf = NULL;
+		if (gdk_pixbuf_loader_write(loader, icon_data, icon_size, NULL)) {
+			pixbuf = gdk_pixbuf_loader_get_pixbuf(loader);
+			if (pixbuf) {
+				img = gtk_image_new_from_pixbuf(pixbuf);
+			}
+		} else {
+			purple_debug_info("pidgin", "failed to parse dialog icon\n");
+		}
+		gdk_pixbuf_loader_close(loader, NULL);
+		g_object_unref(loader);
+	}
+	
+	if (!img) {
+		img = gtk_image_new_from_stock(PIDGIN_STOCK_DIALOG_QUESTION,
 				       gtk_icon_size_from_name(PIDGIN_ICON_SIZE_TANGO_HUGE));
+	}
 	gtk_misc_set_alignment(GTK_MISC(img), 0, 0);
 	gtk_box_pack_start(GTK_BOX(hbox), img, FALSE, FALSE, 0);
 
@@ -712,6 +732,16 @@
 	return data;
 }
 
+static void *
+pidgin_request_action(const char *title, const char *primary,
+						const char *secondary, int default_action,
+					    PurpleAccount *account, const char *who, PurpleConversation *conv,
+						void *user_data, size_t action_count, va_list actions)
+{
+	pidgin_request_action_with_icon(title, primary, secondary, default_action,
+		account, who, conv, NULL, 0, user_data, action_count, actions);
+}
+
 static void
 req_entry_field_changed_cb(GtkWidget *entry, PurpleRequestField *field)
 {
@@ -1699,7 +1729,7 @@
 	pidgin_request_file,
 	pidgin_close_request,
 	pidgin_request_folder,
-	NULL,
+	pidgin_request_action_with_icon,
 	NULL,
 	NULL,
 	NULL