changeset 29842:4eb6ac47f01f

propagate from branch 'im.pidgin.cpw.malu.ft_thumbnails' (head 98d8db26c6d12378d0113a97c8b3f989233a5cbc) to branch 'im.pidgin.pidgin' (head 024731c3cb9e8c0e0479d91f68aeaf7fae9a978b)
author Marcus Lundblad <ml@update.uu.se>
date Wed, 28 Apr 2010 19:27:33 +0000
parents 65c7e53d4bdb (current diff) f3a573191e43 (diff)
children e96c7113d32b 1f5c86f79eeb
files
diffstat 16 files changed, 501 insertions(+), 42 deletions(-) [+]
line wrap: on
line diff
--- a/ChangeLog	Mon Apr 26 17:17:44 2010 +0000
+++ b/ChangeLog	Wed Apr 28 19:27:33 2010 +0000
@@ -50,7 +50,9 @@
 	* The 'Message Timestamp Formats' plugin allows forcing 12-hour
 	  timestamps.  (Jonathan Maltz)
 	* Fix pastes from Chrome (rich-text pastes and probably URLs
-	  having garbage appended to them)
+	  having garbage appended to them).
+	* Show file transfer thumbnails for images on supporting protocols
+	  (currently only supported on MSN).
 
 	Bonjour:
 	* Added support for IPv6. (Thanks to T_X for testing)
@@ -76,6 +78,7 @@
 	MSN:
 	* Support for version 9 of the MSN protocol has been removed.  This
 	  version is no longer supported on the servers.
+	* Support file transfer thumbnails (previews) for images.
 
 	XMPP:
 	* Direct messages to a specific resource only upon receipt of a message
--- a/ChangeLog.API	Mon Apr 26 17:17:44 2010 +0000
+++ b/ChangeLog.API	Wed Apr 28 19:27:33 2010 +0000
@@ -33,6 +33,12 @@
 		* purple_socket_speaks_ipv4
 		* purple_unescape_text
 		* purple_uuid_random
+		* purple_xfer_get_thumbnail
+		* purple_xfer_get_thumbnail_mimetype
+		* purple_xfer_set_thumbnail
+		* purple_xfer_prepare_thumbnail
+		* purple_request_action_with_icon
+		* purple_request_action_varg_with_icon
 		* media_caps to the PurpleBuddy struct
 		* buddy-caps-changed blist signal
 		* ui-caps-changed media manager signal
--- a/libpurple/ft.c	Mon Apr 26 17:17:44 2010 +0000
+++ b/libpurple/ft.c	Wed Apr 28 19:27:33 2010 +0000
@@ -60,6 +60,10 @@
 
 	/* TODO: Should really use a PurpleCircBuffer for this. */
 	GByteArray *buffer;
+
+	gpointer thumbnail_data;		/**< thumbnail image */
+	gsize thumbnail_size;
+	gchar *thumbnail_mimetype;
 } PurpleXferPrivData;
 
 static int purple_xfer_choose_file(PurpleXfer *xfer);
@@ -72,6 +76,10 @@
 	if (priv->buffer)
 		g_byte_array_free(priv->buffer, TRUE);
 
+	g_free(priv->thumbnail_data);
+
+	g_free(priv->thumbnail_mimetype);
+
 	g_free(priv);
 }
 
@@ -266,15 +274,21 @@
 	}
 }
 
-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;
+	gconstpointer thumbnail_data;
+	gsize size;
 
 	g_return_if_fail(xfer != NULL);
 	g_return_if_fail(message != NULL);
 
+	thumbnail_data = purple_xfer_get_thumbnail(xfer, &size);
+
 	conv = purple_find_conversation_with_account(PURPLE_CONV_TYPE_IM, xfer->who,
 											   purple_xfer_get_account(xfer));
 
@@ -286,10 +300,39 @@
 	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;
+		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 publically? */
+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;
@@ -448,6 +491,8 @@
 {
 	char *buf, *size_buf;
 	size_t size;
+	gconstpointer thumb;
+	gsize thumb_size;
 
 	/* If we have already accepted the request, ask the destination file
 	   name directly */
@@ -473,13 +518,19 @@
 			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 ((thumb = purple_xfer_get_thumbnail(xfer, &thumb_size))) {
+			purple_request_accept_cancel_with_icon(xfer, NULL, buf, NULL,
+				PURPLE_DEFAULT_ACTION_NONE, xfer->account, xfer->who, NULL,
+				thumb, thumb_size, 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);
@@ -547,10 +598,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);
@@ -1580,6 +1633,52 @@
 		ui_ops->update_progress(xfer, purple_xfer_get_progress(xfer));
 }
 
+gconstpointer
+purple_xfer_get_thumbnail(const PurpleXfer *xfer, gsize *len)
+{
+	PurpleXferPrivData *priv = g_hash_table_lookup(xfers_data, xfer);
+
+	if (len)
+		*len = priv->thumbnail_size;
+
+	return priv->thumbnail_data;
+}
+
+const gchar *
+purple_xfer_get_thumbnail_mimetype(const PurpleXfer *xfer)
+{
+	PurpleXferPrivData *priv = g_hash_table_lookup(xfers_data, xfer);
+
+	return priv->thumbnail_mimetype;
+}
+
+void
+purple_xfer_set_thumbnail(PurpleXfer *xfer, gconstpointer thumbnail,
+	gsize size, const gchar *mimetype)
+{
+	PurpleXferPrivData *priv = g_hash_table_lookup(xfers_data, xfer);
+
+	g_free(priv->thumbnail_data);
+	g_free(priv->thumbnail_mimetype);
+
+	if (thumbnail && size > 0) {
+		priv->thumbnail_data = g_memdup(thumbnail, size);
+		priv->thumbnail_size = size;
+		priv->thumbnail_mimetype = g_strdup(mimetype);
+	} else {
+		priv->thumbnail_data = NULL;
+		priv->thumbnail_size = 0;
+		priv->thumbnail_mimetype = NULL;
+	}
+}
+
+void
+purple_xfer_prepare_thumbnail(PurpleXfer *xfer, const gchar *formats)
+{
+	if (xfer->ui_ops->add_thumbnail) {
+		xfer->ui_ops->add_thumbnail(xfer, formats);
+	}
+}
 
 /**************************************************************************
  * File Transfer Subsystem API
--- a/libpurple/ft.h	Mon Apr 26 17:17:44 2010 +0000
+++ b/libpurple/ft.h	Wed Apr 28 19:27:33 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, const gchar *formats);
 } PurpleXferUiOps;
 
 /**
@@ -687,6 +692,51 @@
  */
 void purple_xfer_prpl_ready(PurpleXfer *xfer);
 
+/**
+ * Gets the thumbnail data for a transfer
+ *
+ * @param xfer The file transfer to get the thumbnail for
+ * @param len  If not @c NULL, the length of the thumbnail data returned
+ *             will be set in the location pointed to by this.
+ * @return The thumbnail data, or NULL if there is no thumbnail
+ * @since 2.7.0
+ */
+gconstpointer purple_xfer_get_thumbnail(const PurpleXfer *xfer, gsize *len);
+
+/**
+ * Gets the mimetype of the thumbnail preview for a transfer
+ *
+ * @param xfer The file transfer to get the mimetype for
+ * @return The mimetype of the thumbnail, or @c NULL if not thumbnail is set
+ * @since 2.7.0
+ */
+const gchar *purple_xfer_get_thumbnail_mimetype(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
+ * @param mimetype The mimetype of the generated thumbnail
+ * @since 2.7.0
+ */
+void purple_xfer_set_thumbnail(PurpleXfer *xfer, gconstpointer thumbnail,
+	gsize size, const gchar *mimetype);
+
+/**
+ * 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
+ * @param formats A comma-separated list of mimetypes for image formats
+ *	 	  the protocols can use for thumbnails.
+ * @since 2.7.0
+ */
+void purple_xfer_prepare_thumbnail(PurpleXfer *xfer, const gchar *formats);
+
+
 /*@}*/
 
 /**************************************************************************/
--- a/libpurple/protocols/jabber/data.c	Mon Apr 26 17:17:44 2010 +0000
+++ b/libpurple/protocols/jabber/data.c	Wed Apr 28 19:27:33 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_hash(rawdata, size, "sha1");
@@ -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 @@
 	return data;
 }
 
+void
+jabber_data_destroy(JabberData *data)
+{
+	jabber_data_delete(data);
+}
+
 const char *
 jabber_data_get_cid(const JabberData *data)
 {
@@ -289,14 +296,14 @@
 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);
 }
 
@@ -305,7 +312,7 @@
     const gchar *cid)
 {
 	const JabberData *data = g_hash_table_lookup(remote_data_by_cid, 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);
 
 	if (data == NULL) {
 		gchar *jid_cid =
@@ -323,9 +330,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);
 }
@@ -371,6 +379,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	Mon Apr 26 17:17:44 2010 +0000
+++ b/libpurple/protocols/jabber/data.h	Wed Apr 28 19:27:33 2010 +0000
@@ -34,6 +34,7 @@
 	char *type;
 	gsize size;
 	gpointer data;
+	gboolean ephemeral;
 } JabberData;
 
 typedef void (JabberDataRequestCallback)(JabberData *data, gchar *alt,
@@ -42,12 +43,16 @@
 
 /* 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	Mon Apr 26 17:17:44 2010 +0000
+++ b/libpurple/protocols/jabber/message.c	Wed Apr 28 19:27:33 2010 +0000
@@ -963,7 +963,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	Mon Apr 26 17:17:44 2010 +0000
+++ b/libpurple/protocols/jabber/namespaces.h	Wed Apr 28 19:27:33 2010 +0000
@@ -89,6 +89,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	Mon Apr 26 17:17:44 2010 +0000
+++ b/libpurple/protocols/jabber/si.c	Wed Apr 28 19:27:33 2010 +0000
@@ -32,6 +32,7 @@
 #include "notify.h"
 
 #include "buddy.h"
+#include "data.h"
 #include "disco.h"
 #include "jabber.h"
 #include "ibb.h"
@@ -39,6 +40,7 @@
 #include "si.h"
 
 #define STREAMHOST_CONNECT_TIMEOUT 15
+#define ENABLE_FT_THUMBNAILS 0
 
 typedef struct _JabberSIXfer {
 	JabberStream *js;
@@ -1246,9 +1248,14 @@
 	JabberIq *iq;
 	xmlnode *si, *file, *feature, *x, *field, *option, *value;
 	char buf[32];
+#if ENABLE_FT_THUMBNAILS
+	gconstpointer thumb;
+	gsize thumb_size;
 
+	purple_xfer_prepare_thumbnail(xfer, "jpeg,png");
+#endif
 	xfer->filename = g_path_get_basename(xfer->local_filename);
-
+	
 	iq = jabber_iq_new(jsx->js, JABBER_IQ_SET);
 	xmlnode_set_attrib(iq->node, "to", xfer->who);
 	si = xmlnode_new_child(iq->node, "si");
@@ -1266,6 +1273,23 @@
 	xmlnode_set_attrib(file, "size", buf);
 	/* maybe later we'll do hash and date attribs */
 
+#if ENABLE_FT_THUMBNAILS
+	/* add thumbnail, if appropriate */
+	if ((thumb = purple_xfer_get_thumbnail(xfer, &thumb_size))) {
+		const gchar *mimetype = purple_xfer_get_thumbnail_mimetype(xfer);
+		JabberData *thumbnail_data =
+			jabber_data_create_from_data(thumb, thumb_size,
+				mimetype, 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", mimetype);
+		/* cache data */
+		jabber_data_associate_local(thumbnail_data, NULL);
+	}
+#endif
+						  
 	feature = xmlnode_new_child(si, "feature");
 	xmlnode_set_namespace(feature, "http://jabber.org/protocol/feature-neg");
 	x = xmlnode_new_child(feature, "x");
@@ -1644,12 +1668,29 @@
 		purple_xfer_request(xfer);
 }
 
+#if ENABLE_FT_THUMBNAILS
+static void
+jabber_si_thumbnail_cb(JabberData *data, gchar *alt, gpointer userdata)
+{
+	PurpleXfer *xfer = (PurpleXfer *) userdata;
+
+	if (data) {
+		purple_xfer_set_thumbnail(xfer, jabber_data_get_data(data),
+			jabber_data_get_size(data), jabber_data_get_type(data));
+		/* data is ephemeral, get rid of now (the xfer re-owned the thumbnail */
+		jabber_data_destroy(data);
+	}
+
+	purple_xfer_request(xfer);
+}
+#endif
+
 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;
 
@@ -1731,10 +1772,26 @@
 	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);
 
+#if ENABLE_FT_THUMBNAILS
+	/* 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) {
+			jabber_data_request(js, cid, purple_xfer_get_remote_user(xfer),
+			    NULL, TRUE, jabber_si_thumbnail_cb, xfer);
+		} else {
+			purple_xfer_request(xfer);
+		}
+	} else {
+		purple_xfer_request(xfer);
+	}
+#else
 	purple_xfer_request(xfer);
+#endif
 }
 
 void
--- a/libpurple/protocols/msn/slp.c	Mon Apr 26 17:17:44 2010 +0000
+++ b/libpurple/protocols/msn/slp.c	Wed Apr 28 19:27:33 2010 +0000
@@ -422,6 +422,12 @@
 
 			xfer->data = slpcall;
 
+			if (header->type == 0 && bin_len >= sizeof(MsnFileContext)) {
+				purple_xfer_set_thumbnail(xfer, &header->preview,
+				                          bin_len - sizeof(MsnFileContext),
+				    					  "image/png");
+			}
+
 			purple_xfer_request(xfer);
 		}
 		g_free(header);
--- a/libpurple/protocols/msn/slplink.c	Mon Apr 26 17:17:44 2010 +0000
+++ b/libpurple/protocols/msn/slplink.c	Wed Apr 28 19:27:33 2010 +0000
@@ -692,15 +692,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, "png");
+
 	if (!file_name) {
 		gchar *basename = g_path_get_basename(file_path);
 		u8 = purple_utf8_try_convert(basename);
@@ -716,23 +720,33 @@
 		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(xfer, &preview_len);
+	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/prpl.h	Mon Apr 26 17:17:44 2010 +0000
+++ b/libpurple/prpl.h	Wed Apr 28 19:27:33 2010 +0000
@@ -52,6 +52,13 @@
 typedef struct _PurpleBuddyIconSpec PurpleBuddyIconSpec;
 
 /**
+ * A description of a file transfer thumbnail specification.
+ * This tells the UI if and what image formats the prpl support for file
+ * transfer thumbnails.
+ */
+typedef struct _PurpleThumbnailSpec PurpleThumbnailSpec;
+
+/**
  * This \#define exists just to make it easier to fill out the buddy icon
  * field in the prpl info struct for protocols that couldn't care less.
  */
--- a/libpurple/request.c	Mon Apr 26 17:17:44 2010 +0000
+++ b/libpurple/request.c	Wed Apr 28 19:27:33 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_with_icon != 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	Mon Apr 26 17:17:44 2010 +0000
+++ b/libpurple/request.h	Wed Apr 28 19:27:33 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,29 @@
 	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
+ * @since 2.7.0
+ */
+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.
+ * @since 2.7.0
+ */
+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 +1508,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	Mon Apr 26 17:17:44 2010 +0000
+++ b/pidgin/gtkft.c	Wed Apr 28 19:27:33 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,67 @@
 		pidgin_xfer_dialog_cancel_xfer(xfer_dialog, xfer);
 }
 
+static void
+pidgin_xfer_add_thumbnail(PurpleXfer *xfer, const gchar *formats)
+{	
+	purple_debug_info("ft", "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 **formats_split = g_strsplit(formats, ",", 0);
+			gchar *buffer = NULL;
+			gsize size;
+			char *option_keys[2] = {NULL, NULL};
+			char *option_values[2] = {NULL, NULL};
+			int i;
+			gchar *format = NULL;
+			
+			for (i = 0; formats_split[i]; i++) {
+				if (purple_strequal(formats_split[i], "jpeg")) {
+					purple_debug_info("ft", "creating JPEG thumbnail\n");
+					option_keys[0] = "quality";
+					option_values[0] = "90";
+					format = "jpeg";
+					break;
+				} else if (purple_strequal(formats_split[i], "png")) {
+					purple_debug_info("ft", "creating PNG thumbnail\n");
+					option_keys[0] = "compression";
+					option_values[0] = "9";
+					format = "png";
+					break;
+				}
+			}
+
+			/* Try the first format given by the PRPL without options */
+			if (format == NULL) {
+				purple_debug_info("ft",
+				    "creating thumbnail of format %s as demanded by PRPL\n",
+				    formats_split[0]);
+				format = formats_split[0];
+			}
+
+			gdk_pixbuf_save_to_bufferv(thumbnail, &buffer, &size, format, 
+				option_keys, option_values, NULL);
+
+			if (buffer) {
+				gchar *mimetype = g_strdup_printf("image/%s", format);				
+				purple_debug_info("ft",
+				                  "created thumbnail of %" G_GSIZE_FORMAT " bytes\n",
+					size);
+				purple_xfer_set_thumbnail(xfer, buffer, size, mimetype);
+				g_free(buffer);
+				g_free(mimetype);
+			}
+			g_object_unref(thumbnail);
+			g_strfreev(formats_split);
+		}
+	}
+}
+
 static PurpleXferUiOps ops =
 {
 	pidgin_xfer_new_xfer,
@@ -1168,7 +1232,7 @@
 	NULL,
 	NULL,
 	NULL,
-	NULL
+	pidgin_xfer_add_thumbnail
 };
 
 /**************************************************************************
--- a/pidgin/gtkrequest.c	Mon Apr 26 17:17:44 2010 +0000
+++ b/pidgin/gtkrequest.c	Wed Apr 28 19:27:33 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