changeset 15143:b81e4e44b509

[gaim-migrate @ 17929] User Info and Tooltips now use the GaimNotifyUserInfo object and methods defined in notify.h. GaimNotifyUserInfo objects encapsulate a list of GaimNotifyUserInfoEntry objects, each of which may have a label, a value, and be specified to be a section header. This moves the burden of UI generation of user information from the various prpls to the UI. The UI can choose how to display the information rather than being fenced into a particular HTML formatting. Consistency across the prpls' information presentation is now enforced, as well. gaim_notify_user_info_get_text_with_newline() generates text in the: <b>label</b>: value <b>label</b>: value format as was passed by convention from prpls in the past. committer: Tailor Script <tailor@pidgin.im>
author Evan Schoenberg <evan.s@dreskin.net>
date Sun, 10 Dec 2006 02:53:09 +0000
parents 4b319b19aa24
children 94adee1cba70
files libgaim/notify.c libgaim/notify.h libgaim/protocols/gg/gg.c libgaim/protocols/irc/msgs.c libgaim/protocols/jabber/buddy.c libgaim/protocols/jabber/jabber.c libgaim/protocols/msn/msn.c libgaim/protocols/novell/novell.c libgaim/protocols/oscar/oscar.c libgaim/protocols/oscar/oscarcommon.h libgaim/protocols/qq/buddy_info.c libgaim/protocols/qq/qq.c libgaim/protocols/sametime/sametime.c libgaim/protocols/yahoo/yahoo.c libgaim/protocols/yahoo/yahoo.h libgaim/protocols/yahoo/yahoo_profile.c libgaim/protocols/zephyr/zephyr.c libgaim/prpl.h libgaim/util.c libgaim/util.h
diffstat 20 files changed, 848 insertions(+), 540 deletions(-) [+]
line wrap: on
line diff
--- a/libgaim/notify.c	Sat Dec 09 20:20:17 2006 +0000
+++ b/libgaim/notify.c	Sun Dec 10 02:53:09 2006 +0000
@@ -36,6 +36,21 @@
 	gpointer cb_user_data;
 } GaimNotifyInfo;
 
+/**
+ * Definition of a user info entry
+ */
+struct _GaimNotifyUserInfoEntry
+{
+	char *label;
+	char *value;
+	gboolean is_header;
+};
+
+struct _GaimNotifyUserInfo
+{
+	GList *user_info_entries;
+};
+
 void *
 gaim_notify_message(void *handle, GaimNotifyMsgType type,
 					const char *title, const char *primary,
@@ -416,7 +431,7 @@
 
 void *
 gaim_notify_userinfo(GaimConnection *gc, const char *who,
-						   const char *text, GaimNotifyCloseCallback cb, gpointer user_data)
+						   GaimNotifyUserInfo *user_info, GaimNotifyCloseCallback cb, gpointer user_data)
 {
 	GaimNotifyUiOps *ops;
 
@@ -426,21 +441,18 @@
 
 	if (ops != NULL && ops->notify_userinfo != NULL) {
 		GaimNotifyInfo *info;
-		char *infotext = g_strdup(text);
 
 		info            = g_new0(GaimNotifyInfo, 1);
 		info->type      = GAIM_NOTIFY_USERINFO;
 		info->handle    = gc;
 
 		gaim_signal_emit(gaim_notify_get_handle(), "displaying-userinfo",
-						 gaim_connection_get_account(gc), who, &infotext);
+						 gaim_connection_get_account(gc), who, user_info);
 
-		info->ui_handle = ops->notify_userinfo(gc, who, infotext);
+		info->ui_handle = ops->notify_userinfo(gc, who, user_info);
 		info->cb = cb;
 		info->cb_user_data = user_data;
 
-		g_free(infotext);
-
 		if (info->ui_handle != NULL) {
 			handles = g_list_append(handles, info);
 
@@ -463,6 +475,158 @@
 	return NULL;
 }
 
+static GaimNotifyUserInfoEntry *
+gaim_notify_user_info_entry_new(const char *label, const char *value)
+{
+	GaimNotifyUserInfoEntry *user_info_entry;
+	
+	user_info_entry = g_new0(GaimNotifyUserInfoEntry, 1);
+	user_info_entry->label = g_strdup(label);
+	user_info_entry->value = g_strdup(value);
+	user_info_entry->is_header = FALSE;
+
+	return user_info_entry;
+}
+
+static void
+gaim_notify_user_info_entry_destroy(GaimNotifyUserInfoEntry *user_info_entry)
+{
+	g_return_if_fail(user_info_entry != NULL);
+	
+	g_free(user_info_entry->label);
+	g_free(user_info_entry->value);	
+	g_free(user_info_entry);
+}
+
+GaimNotifyUserInfo *
+gaim_notify_user_info_new()
+{
+	GaimNotifyUserInfo *user_info;
+	
+	user_info = g_new0(GaimNotifyUserInfo, 1);
+	user_info->user_info_entries = NULL;
+	
+	return user_info;
+}
+
+void
+gaim_notify_user_info_destroy(GaimNotifyUserInfo *user_info)
+{
+	GList *l;
+
+	for (l = user_info->user_info_entries; l != NULL; l = l->next) {
+		GaimNotifyUserInfoEntry *user_info_entry = l->data;
+		
+		gaim_notify_user_info_entry_destroy(user_info_entry);
+	}
+	
+	g_list_free(user_info->user_info_entries);
+}
+
+GList *
+gaim_notify_user_info_get_entries(GaimNotifyUserInfo *user_info)
+{
+	g_return_val_if_fail(user_info != NULL, NULL);
+
+	return user_info->user_info_entries;
+}
+
+char *
+gaim_notify_user_info_get_text_with_newline(GaimNotifyUserInfo *user_info, const char *newline)
+{
+	GList *l;
+	GString *text;
+	
+	text = g_string_new("");
+
+	for (l = user_info->user_info_entries; l != NULL; l = l->next) {
+		GaimNotifyUserInfoEntry *user_info_entry = l->data;
+		if (user_info_entry->is_header)
+			g_string_append(text, newline);
+
+		if (user_info_entry->label)
+			g_string_append_printf(text, "<b>%s</b>", user_info_entry->label);
+		if (user_info_entry->label && user_info_entry->value)
+			g_string_append(text, ": ");
+		if (user_info_entry->value)
+			g_string_append(text, user_info_entry->value);			
+
+		if (user_info_entry->is_header)
+			g_string_append(text, newline);
+
+		if (l->next)
+			g_string_append(text, newline);
+	}
+
+	return g_string_free(text, FALSE);
+}
+
+
+gchar *
+gaim_notify_user_info_entry_get_label(GaimNotifyUserInfoEntry *user_info_entry)
+{
+	g_return_val_if_fail(user_info_entry != NULL, NULL);
+
+	return user_info_entry->label;
+}
+
+gchar *
+gaim_notify_user_info_entry_get_value(GaimNotifyUserInfoEntry *user_info_entry)
+{
+	g_return_val_if_fail(user_info_entry != NULL, NULL);
+	
+	return user_info_entry->value;
+}
+
+void
+gaim_notify_user_info_add_pair(GaimNotifyUserInfo *user_info, const char *label, const char *value)
+{
+	GaimNotifyUserInfoEntry *entry;
+	
+	entry = gaim_notify_user_info_entry_new(label, value);
+	user_info->user_info_entries = g_list_append(user_info->user_info_entries, entry);
+}
+
+void
+gaim_notify_user_info_prepend_pair(GaimNotifyUserInfo *user_info, const char *label, const char *value)
+{
+	GaimNotifyUserInfoEntry *entry;
+
+	entry = gaim_notify_user_info_entry_new(label, value);
+	user_info->user_info_entries = g_list_prepend(user_info->user_info_entries, entry);
+}
+
+void
+gaim_notify_user_info_add_section_header(GaimNotifyUserInfo *user_info, const char *label)
+{
+	GaimNotifyUserInfoEntry *entry;
+	
+	entry = gaim_notify_user_info_entry_new(label, NULL);
+	entry->is_header = TRUE;
+
+	user_info->user_info_entries = g_list_append(user_info->user_info_entries, entry);
+}
+
+/**
+ * Remove the last item which was added to user_info
+ * This is helpful for removing a section header if the section was empty.
+ */
+void
+gaim_notify_user_info_remove_last_item(GaimNotifyUserInfo *user_info)
+{
+	user_info->user_info_entries = g_list_remove(user_info->user_info_entries,
+										 g_list_last(user_info->user_info_entries)->data);
+}
+
+void
+gaim_notify_user_info_add_section_break(GaimNotifyUserInfo *user_info)
+{
+	/* This is for future expansion; section breaks should be marked as such so the UI
+	* can format them differently if desired.
+	*/	
+	gaim_notify_user_info_add_pair(user_info, NULL, "<HR>");
+}
+
 void *
 gaim_notify_uri(void *handle, const char *uri)
 {
--- a/libgaim/notify.h	Sat Dec 09 20:20:17 2006 +0000
+++ b/libgaim/notify.h	Sun Dec 10 02:53:09 2006 +0000
@@ -29,9 +29,11 @@
 #include <glib-object.h>
 #include <glib.h>
 
+typedef struct _GaimNotifyUserInfoEntry	GaimNotifyUserInfoEntry;
+typedef struct _GaimNotifyUserInfo	GaimNotifyUserInfo;
+
 #include "connection.h"
 
-
 /**
  * Notification close callbacks.
  */
@@ -154,7 +156,7 @@
 	                                      void *data);
 
 	void *(*notify_userinfo)(GaimConnection *gc, const char *who,
-	                         const char *text);
+	                         GaimNotifyUserInfo *user_info);
 
 	void *(*notify_uri)(const char *uri);
 
@@ -403,20 +405,113 @@
  * The text is essentially a stripped-down format of HTML, the same that
  * IMs may send.
  *
- * @param gc	    The GaimConnection handle associated with the information.
- * @param who	    The username associated with the information.
- * @param text      The formatted text.
- * @param cb        The callback to call when the user closes
- *                  the notification.
- * @param user_data The data to pass to the callback.
+ * @param gc		         The GaimConnection handle associated with the information.
+ * @param who				 The username associated with the information.
+ * @param user_info          The GaimNotifyUserInfo which contains the information
+ * @param cb                 The callback to call when the user closes
+ *                           the notification.
+ * @param user_data          The data to pass to the callback.
  *
  * @return A UI-specific handle.
  */
 void *gaim_notify_userinfo(GaimConnection *gc, const char *who,
-						   const char *text, GaimNotifyCloseCallback cb,
+						   GaimNotifyUserInfo *user_info, GaimNotifyCloseCallback cb,
 						   gpointer user_data);
 
 /**
+ * Create a new GaimNotifyUserInfo which is suitable for passing to gaim_notify_userinfo()
+ *
+ * @return A new GaimNotifyUserInfo, which the caller must destroy when done
+ */
+GaimNotifyUserInfo *gaim_notify_user_info_new();
+
+/**
+ * Destroy a GaimNotifyUserInfo
+ *
+ * @param user_info          The GaimNotifyUserInfo
+ */
+void gaim_notify_user_info_destroy(GaimNotifyUserInfo *user_info);
+
+/**
+ * Retrieve the array of GaimNotifyUserInfoEntry objects from a GaimNotifyUserInfo
+ *
+ * @param user_info          The GaimNotifyUserInfo
+ *
+ * @result                   A GList of GaimNotifyUserInfoEntry objects
+ */
+GList *gaim_notify_user_info_get_entries(GaimNotifyUserInfo *user_info);
+
+/**
+ * Create a textual representation of a GaimNotifyUserInfo, separating entries with newline
+ *
+ * @param user_info          The GaimNotifyUserInfo
+ * @param newline            The separation character
+ */
+char *gaim_notify_user_info_get_text_with_newline(GaimNotifyUserInfo *user_info, const char *newline);
+
+/**
+ * Add a label/value pair to a GaimNotifyUserInfo object.  GaimNotifyUserInfo keeps track of the order in which pairs are added.
+ *
+ * @param user_info          The GaimNotifyUserInfo
+ * @param label              A label, which for example might be displayed by a UI with a colon after it ("Status:"). Do not include a colon.
+ *                           If NULL, value will be displayed without a label.
+ * @param value              The value, which might be displayed by a UI after the label.
+ *                           If NULL, label will still be displayed; the UI should then treat label as independent
+ *                           and not include a colon if it would otherwise.
+ */
+void gaim_notify_user_info_add_pair(GaimNotifyUserInfo *user_info, const char *label, const char *value);
+
+/**
+ * Prepend a label/value pair to a GaimNotifyUserInfo object
+ *
+ * @param user_info          The GaimNotifyUserInfo
+ * @param label              A label, which for example might be displayed by a UI with a colon after it ("Status:"). Do not include a colon.
+ *                           If NULL, value will be displayed without a label.
+ * @param value              The value, which might be displayed by a UI after the label.
+ *                           If NULL, label will still be displayed; the UI should then treat label as independent
+ *                           and not include a colon if it would otherwise.
+ */
+void gaim_notify_user_info_prepend_pair(GaimNotifyUserInfo *user_info, const char *label, const char *value);
+
+/**
+ * Add a section break.  A UI might display this as a horizontal line.
+ *
+ * @param user_info          The GaimNotifyUserInfo
+ */
+void gaim_notify_user_info_add_section_break(GaimNotifyUserInfo *user_info);
+
+/**
+ * Add a section header.  A UI might display this in a different font from other text.
+ *
+ * @param user_info          The GaimNotifyUserInfo
+ * @param label              The name of the section
+ */
+void gaim_notify_user_info_add_section_header(GaimNotifyUserInfo *user_info, const char *label);
+
+/**
+ * Remove the last item which was added to a GaimNotifyUserInfo. This could be used to remove a section header which is not needed.
+ */
+void gaim_notify_user_info_remove_last_item(GaimNotifyUserInfo *user_info);
+
+/**
+ * Get the label for a GaimNotifyUserInfoEntry
+ *
+ * @param user_info_entry     The GaimNotifyUserInfoEntry
+ *
+ * @result                    The label
+ */
+gchar *gaim_notify_user_info_entry_get_label(GaimNotifyUserInfoEntry *user_info_entry);
+
+/**
+ * Get the value for a GaimNotifyUserInfoEntry
+ *
+ * @param user_info_entry     The GaimNotifyUserInfoEntry
+ *
+ * @result                    The value
+ */
+gchar *gaim_notify_user_info_entry_get_value(GaimNotifyUserInfoEntry *user_info_entry);
+
+/**
  * Opens a URI or somehow presents it to the user.
  *
  * @param handle The plugin or connection handle.
--- a/libgaim/protocols/gg/gg.c	Sat Dec 09 20:20:17 2006 +0000
+++ b/libgaim/protocols/gg/gg.c	Sun Dec 10 02:53:09 2006 +0000
@@ -1013,56 +1013,47 @@
 static void ggp_pubdir_handle_info(GaimConnection *gc, gg_pubdir50_t req,
 				   GGPSearchForm *form)
 {
-	GString *text;
+	GaimNotifyUserInfo *user_info;
 	char *val, *who;
 
-	text = g_string_new("");
+	user_info = gaim_notify_user_info_new();
 
 	val = ggp_search_get_result(req, 0, GG_PUBDIR50_STATUS);
 	/* XXX: Use of ggp_str_to_uin() is an ugly hack! */
-	g_string_append_printf(text, "<b>%s:</b> %s<br/>",
-	                       _("Status"), ggp_status_by_id(ggp_str_to_uin(val)));
+	gaim_notify_user_info_add_pair(user_info, _("Status"), ggp_status_by_id(ggp_str_to_uin(val)));
 	g_free(val);
 
 	who = ggp_search_get_result(req, 0, GG_PUBDIR50_UIN);
-	g_string_append_printf(text, "<b>%s:</b> %s<br/>",
-	                       _("UIN"), who);
+	gaim_notify_user_info_add_pair(user_info, _("UIN"), who);
 
 	val = ggp_search_get_result(req, 0, GG_PUBDIR50_FIRSTNAME);
-	g_string_append_printf(text, "<b>%s:</b> %s<br/>",
-	                       _("First Name"), val);
+	gaim_notify_user_info_add_pair(user_info, _("First Name"), val);
 	g_free(val);
 
 	val = ggp_search_get_result(req, 0, GG_PUBDIR50_NICKNAME);
-	g_string_append_printf(text, "<b>%s:</b> %s<br/>",
-	                       _("Nickname"), val);
+	gaim_notify_user_info_add_pair(user_info, _("Nickname"), val);
 	g_free(val);
 
 	val = ggp_search_get_result(req, 0, GG_PUBDIR50_CITY);
-	g_string_append_printf(text, "<b>%s:</b> %s<br/>",
-	                       _("City"), val);
+	gaim_notify_user_info_add_pair(user_info, _("City"), val);
 	g_free(val);
 
 	val = ggp_search_get_result(req, 0, GG_PUBDIR50_BIRTHYEAR);
-	if (strncmp(val, "0", 1) == 0) {
-		g_free(val);
-		val = g_strdup("");
+	if (strncmp(val, "0", 1)) {
+		gaim_notify_user_info_add_pair(user_info, _("Birth Year"), val);
 	}
-	g_string_append_printf(text, "<b>%s:</b> %s<br/>",
-			       _("Birth Year"), val);
 	g_free(val);
 
 	val = ggp_buddy_get_name(gc, ggp_str_to_uin(who));
 	g_free(who);
 	who = val;
 
-	val = gaim_strdup_withhtml(text->str);
+/*	val = gaim_strdup_withhtml(text->str); */
 
-	gaim_notify_userinfo(gc, who, val, ggp_sr_close_cb, form);
-
-	g_string_free(text, TRUE);
+	gaim_notify_userinfo(gc, who, user_info, ggp_sr_close_cb, form);
 	g_free(val);
 	g_free(who);
+	gaim_notify_user_info_destroy(user_info);
 }
 /* }}} */
 
@@ -1558,11 +1549,11 @@
 }
 /* }}} */
 
-/* static void ggp_tooltip_text(GaimBuddy *b, GString *str, gboolean full) {{{ */
-static void ggp_tooltip_text(GaimBuddy *b, GString *str, gboolean full)
+/* static void ggp_tooltip_text(GaimBuddy *b, GaimNotifyUserInfo *user_info, gboolean full) {{{ */
+static void ggp_tooltip_text(GaimBuddy *b, GaimNotifyUserInfo *user_info, gboolean full)
 {
 	GaimStatus *status;
-	char *text;
+	char *text, *tmp;
 	const char *msg, *name;
 
 	g_return_if_fail(b != NULL);
@@ -1574,16 +1565,15 @@
 	if (msg != NULL) {
 		text = g_markup_escape_text(msg, -1);
 		if (GAIM_BUDDY_IS_ONLINE(b)) {
-			g_string_append_printf(str, "\n<b>%s:</b> %s: %s",
-				 	       _("Status"), name, text);
+			tmp = g_strdup_printf("%s: %s", name, text);
+			gaim_notify_user_info_add_pair(user_info, _("Status"), tmp);
+			g_free(tmp);
 		} else {
-			g_string_append_printf(str, "\n<b>%s:</b>: %s",
-					       _("Message"), text);
+			gaim_notify_user_info_add_pair(user_info, _("Message"), text);
 		}
 		g_free(text);
-	} else if (GAIM_BUDDY_IS_ONLINE(b)) {
-		g_string_append_printf(str, "\n<b>%s:</b> %s",
-				       _("Status"), name);
+	} else {
+		gaim_notify_user_info_add_pair(user_info, _("Status"), name);
 	}
 }
 /* }}} */
--- a/libgaim/protocols/irc/msgs.c	Sat Dec 09 20:20:17 2006 +0000
+++ b/libgaim/protocols/irc/msgs.c	Sun Dec 10 02:53:09 2006 +0000
@@ -199,7 +199,8 @@
 {
 	GaimConnection *gc;
 	GString *info;
-	char *str, *tmp;
+	char *str, *tmp, *tmp2;
+	GaimNotifyUserInfo *user_info;
 
 	if (!irc->whois.nick) {
 		gaim_debug(GAIM_DEBUG_WARNING, "irc", "Unexpected End of WHOIS for %s\n", args[1]);
@@ -210,55 +211,59 @@
 		return;
 	}
 
-	info = g_string_new("");
-	tmp = g_markup_escape_text(args[1], -1);
-	g_string_append_printf(info, _("<b>%s:</b> %s"), _("Nick"), tmp);
+	user_info = gaim_notify_user_info_new();
+
+	tmp2 = g_markup_escape_text(args[1], -1);
+	tmp = g_strdup_printf("%s%s%s", tmp2,
+				(irc->whois.ircop ? _(" <i>(ircop)</i>") : ""),
+				(irc->whois.identified ? _(" <i>(identified)</i>") : ""));
+	gaim_notify_user_info_add_pair(user_info, _("Nick"), tmp);
+	g_free(tmp2);
 	g_free(tmp);
-	g_string_append_printf(info, "%s%s<br>",
-			       irc->whois.ircop ? _(" <i>(ircop)</i>") : "",
-			       irc->whois.identified ? _(" <i>(identified)</i>") : "");
+
 	if (irc->whois.away) {
 		tmp = g_markup_escape_text(irc->whois.away, strlen(irc->whois.away));
 		g_free(irc->whois.away);
-		g_string_append_printf(info, _("<b>%s:</b> %s<br>"), _("Away"), tmp);
+		gaim_notify_user_info_add_pair(user_info, _("Away"), tmp);
 		g_free(tmp);
 	}
 	if (irc->whois.userhost) {
 		tmp = g_markup_escape_text(irc->whois.name, strlen(irc->whois.name));
 		g_free(irc->whois.name);
-		g_string_append_printf(info, _("<b>%s:</b> %s<br>"), _("Username"), irc->whois.userhost);
-		g_string_append_printf(info, _("<b>%s:</b> %s<br>"), _("Real name"), tmp);
+		gaim_notify_user_info_add_pair(user_info, _("Username"), irc->whois.userhost);
+		gaim_notify_user_info_add_pair(user_info, _("Real name"), tmp);
 		g_free(irc->whois.userhost);
 		g_free(tmp);
 	}
 	if (irc->whois.server) {
-		g_string_append_printf(info, _("<b>%s:</b> %s"), _("Server"), irc->whois.server);
-		g_string_append_printf(info, " (%s)<br>", irc->whois.serverinfo);
+		tmp = g_strdup_printf("%s (%s)", irc->whois.server, irc->whois.serverinfo);
+		gaim_notify_user_info_add_pair(user_info, _("Server"), tmp);
+		g_free(tmp);
 		g_free(irc->whois.server);
 		g_free(irc->whois.serverinfo);
 	}
 	if (irc->whois.channels) {
-		g_string_append_printf(info, _("<b>%s:</b> %s<br>"), _("Currently on"), irc->whois.channels);
+		gaim_notify_user_info_add_pair(user_info, _("Currently on"), irc->whois.channels);
 		g_free(irc->whois.channels);
 	}
 	if (irc->whois.idle) {
 		gchar *timex = gaim_str_seconds_to_string(irc->whois.idle);
-		g_string_append_printf(info, _("<b>Idle for:</b> %s<br>"), timex);
+		gaim_notify_user_info_add_pair(user_info, _("Idle for"), timex);
 		g_free(timex);
-		g_string_append_printf(info, _("<b>%s:</b> %s"), _("Online since"),
-		                       gaim_date_format_full(localtime(&irc->whois.signon)));
+		gaim_notify_user_info_add_pair(user_info,
+														_("Online since"), gaim_date_format_full(localtime(&irc->whois.signon)));
 	}
 	if (!strcmp(irc->whois.nick, "Paco-Paco")) {
-		g_string_append_printf(info, _("<br><b>Defining adjective:</b> Glorious<br>"));
+		gaim_notify_user_info_add_pair(user_info,
+																   _("<b>Defining adjective:</b>"), _("Glorious"));
 	}
 
 	gc = gaim_account_get_connection(irc->account);
-	str = g_string_free(info, FALSE);
 
-	gaim_notify_userinfo(gc, irc->whois.nick, str, NULL, NULL);
+	gaim_notify_userinfo(gc, irc->whois.nick, user_info, NULL, NULL);
+	gaim_notify_user_info_destroy(user_info);
 
 	g_free(irc->whois.nick);
-	g_free(str);
 	memset(&irc->whois, 0, sizeof(irc->whois));
 }
 
--- a/libgaim/protocols/jabber/buddy.c	Sat Dec 09 20:20:17 2006 +0000
+++ b/libgaim/protocols/jabber/buddy.c	Sun Dec 10 02:53:09 2006 +0000
@@ -602,17 +602,17 @@
 
 static void jabber_buddy_info_show_if_ready(JabberBuddyInfo *jbi)
 {
-	GString *info_text;
-	char *resource_name;
+	char *resource_name, *tmp;
 	JabberBuddyResource *jbr;
 	JabberBuddyInfoResource *jbir = NULL;
 	GList *resources;
+	GaimNotifyUserInfo *user_info;
 
 	/* not yet */
 	if(jbi->ids)
 		return;
 
-	info_text = g_string_new("");
+	user_info = gaim_notify_user_info_new();
 	resource_name = jabber_get_resource(jbi->jid);
 
 	if(resource_name) {
@@ -622,29 +622,29 @@
 			char *purdy = NULL;
 			if(jbr->status)
 				purdy = gaim_strdup_withhtml(jbr->status);
-			g_string_append_printf(info_text, "<b>%s:</b> %s%s%s<br/>",
-					_("Status"), jabber_buddy_state_get_name(jbr->state),
-					purdy ? ": " : "",
-					purdy ? purdy : "");
-			if(purdy)
-				g_free(purdy);
+			tmp = g_strdup_printf("%s%s%s", jabber_buddy_state_get_name(jbr->state),
+							(purdy ? ": " : ""),
+							(purdy ? purdy : ""));
+			gaim_notify_user_info_add_pair(user_info, _("Status"), tmp);
+			g_free(tmp);
+			g_free(purdy);
 		} else {
-			g_string_append_printf(info_text, "<b>%s:</b> %s<br/>",
-					_("Status"), _("Unknown"));
+			gaim_notify_user_info_add_pair(user_info, _("Status"), _("Unknown"));
 		}
 		if(jbir) {
 			if(jbir->idle_seconds > 0) {
-				g_string_append_printf(info_text, "<b>%s:</b> %s<br/>",
-						_("Idle"), gaim_str_seconds_to_string(jbir->idle_seconds));
+				gaim_notify_user_info_add_pair(user_info, _("Idle"), gaim_str_seconds_to_string(jbir->idle_seconds));
 			}
 		}
 		if(jbr && jbr->client.name) {
-			g_string_append_printf(info_text, "<b>%s:</b> %s %s<br/>",
-					_("Client:"), jbr->client.name,
-					jbr->client.version ? jbr->client.version : "");
+			tmp = g_strdup_printf("%s%s%s", jbr->client.name,
+								  (jbr->client.version ? " " : ""),
+								  (jbr->client.version ? jbr->client.version : ""));
+			gaim_notify_user_info_add_pair(user_info, _("Client"), tmp);
+			g_free(tmp);
+
 			if(jbr->client.os) {
-				g_string_append_printf(info_text, "<b>%s:</b> %s<br/>",
-						_("Operating System"), jbr->client.os);
+				gaim_notify_user_info_add_pair(user_info, _("Operating System"), jbr->client.os);
 			}
 		}
 	} else {
@@ -654,54 +654,57 @@
 			if(jbr->status)
 				purdy = gaim_strdup_withhtml(jbr->status);
 			if(jbr->name)
-				g_string_append_printf(info_text, "<b>%s:</b> %s<br/>",
-						_("Resource"), jbr->name);
-			g_string_append_printf(info_text, "<b>%s:</b> %d<br/>",
-					_("Priority"), jbr->priority);
-			g_string_append_printf(info_text, "<b>%s:</b> %s%s%s<br/>",
-					_("Status"), jabber_buddy_state_get_name(jbr->state),
-					purdy ? ": " : "",
-					purdy ? purdy : "");
-			if(purdy)
-				g_free(purdy);
+				gaim_notify_user_info_add_pair(user_info, _("Resource"), jbr->name);
+			tmp = g_strdup_printf("%d", jbr->priority);
+			gaim_notify_user_info_add_pair(user_info, _("Priority"), tmp);
+			g_free(tmp);
+
+			tmp = g_strdup_printf("%s%s%s", jabber_buddy_state_get_name(jbr->state),
+								  (purdy ? ": " : ""),
+								  (purdy ? purdy : ""));
+			gaim_notify_user_info_add_pair(user_info, _("Status"), tmp);
+			g_free(tmp);
+			g_free(purdy);
 
 			if(jbr->name)
 				jbir = g_hash_table_lookup(jbi->resources, jbr->name);
 
 			if(jbir) {
 				if(jbir->idle_seconds > 0) {
-					g_string_append_printf(info_text, "<b>%s:</b> %s<br/>",
-							_("Idle"), gaim_str_seconds_to_string(jbir->idle_seconds));
+					gaim_notify_user_info_add_pair(user_info, _("Idle"), gaim_str_seconds_to_string(jbir->idle_seconds));
 				}
 			}
-			if(jbr->client.name) {
-				g_string_append_printf(info_text, "<b>%s:</b> %s %s<br/>",
-						_("Client"), jbr->client.name,
-						jbr->client.version ? jbr->client.version : "");
+			if(jbr && jbr->client.name) {
+				tmp = g_strdup_printf("%s%s%s", jbr->client.name,
+									  (jbr->client.version ? " " : ""),
+									  (jbr->client.version ? jbr->client.version : ""));
+				gaim_notify_user_info_add_pair(user_info,
+											   _("Client"), tmp);
+				g_free(tmp);
+				
 				if(jbr->client.os) {
-					g_string_append_printf(info_text, "<b>%s:</b> %s<br/>",
-							_("Operating System"), jbr->client.os);
+					gaim_notify_user_info_add_pair(user_info, _("Operating System"), jbr->client.os);
 				}
 			}
-
-			g_string_append_printf(info_text, "<br/>");
 		}
 	}
 
 	g_free(resource_name);
 
-	if (jbi->vcard_text != NULL)
-		info_text = g_string_append(info_text, jbi->vcard_text);
+	if (jbi->vcard_text != NULL) {
+		gaim_notify_user_info_add_section_break(user_info);
+		/* Should this have some sort of label? */
+		gaim_notify_user_info_add_pair(user_info, NULL, jbi->vcard_text);
+	}
 
-	gaim_notify_userinfo(jbi->js->gc, jbi->jid, info_text->str, NULL, NULL);
+	gaim_notify_userinfo(jbi->js->gc, jbi->jid, user_info, NULL, NULL);
+	gaim_notify_user_info_destroy(user_info);
 
 	while(jbi->vcard_imgids) {
 		gaim_imgstore_unref(GPOINTER_TO_INT(jbi->vcard_imgids->data));
 		jbi->vcard_imgids = g_slist_delete_link(jbi->vcard_imgids, jbi->vcard_imgids);
 	}
 
-	g_string_free(info_text, TRUE);
-
 	if (jbi->timeout_handle > 0)
 		gaim_timeout_remove(jbi->timeout_handle);
 
--- a/libgaim/protocols/jabber/jabber.c	Sat Dec 09 20:20:17 2006 +0000
+++ b/libgaim/protocols/jabber/jabber.c	Sun Dec 10 02:53:09 2006 +0000
@@ -1127,7 +1127,7 @@
 	return ret;
 }
 
-static void jabber_tooltip_text(GaimBuddy *b, GString *str, gboolean full)
+static void jabber_tooltip_text(GaimBuddy *b, GaimNotifyUserInfo *user_info, gboolean full)
 {
 	JabberBuddy *jb;
 
@@ -1160,12 +1160,14 @@
 				else
 					sub = _("None");
 			}
-			g_string_append_printf(str, "\n<b>%s:</b> %s", _("Subscription"), sub);
+			
+			gaim_notify_user_info_add_pair(user_info, _("Subscription"), sub);
 		}
 
 		for(l=jb->resources; l; l = l->next) {
 			char *text = NULL;
 			char *res = NULL;
+			char *label, *value;
 			const char *state;
 
 			jbr = l->data;
@@ -1188,20 +1190,23 @@
 				text = NULL;
 			}
 
-			g_string_append_printf(str, "\n<b>%s%s:</b> %s%s%s",
-					_("Status"),
-					res ? res : "",
-					state,
-					text ? ": " : "",
-					text ? text : "");
+			label = g_strdup_printf("%s%s",
+							_("Status"), (res ? res : ""));
+			value = g_strdup_printf("%s%s%s",
+							state,
+							(text ? ": " : ""),
+							(text ? text : ""));
 
+			gaim_notify_user_info_add_pair(user_info, label, value);
+
+			g_free(label);
+			g_free(value);
 			g_free(text);
 			g_free(res);
 		}
 
 		if(!GAIM_BUDDY_IS_ONLINE(b) && jb->error_msg) {
-			g_string_append_printf(str, "\n<b>%s:</b> %s",
-					_("Error"), jb->error_msg);
+			gaim_notify_user_info_add_pair(user_info, _("Error"), jb->error_msg);
 		}
 	}
 }
--- a/libgaim/protocols/msn/msn.c	Sat Dec 09 20:20:17 2006 +0000
+++ b/libgaim/protocols/msn/msn.c	Sun Dec 10 02:53:09 2006 +0000
@@ -67,10 +67,8 @@
 	MsnGetInfoData *info_data;
 	char *stripped;
 	char *url_buffer;
-	GString *s;
+	GaimNotifyUserInfo *user_info;
 	char *photo_url_text;
-	char *tooltip_text;
-	const char *title;
 
 } MsnGetInfoStepTwoData;
 
@@ -533,7 +531,7 @@
 }
 
 static void
-msn_tooltip_text(GaimBuddy *buddy, GString *str, gboolean full)
+msn_tooltip_text(GaimBuddy *buddy, GaimNotifyUserInfo *user_info, gboolean full)
 {
 	MsnUser *user;
 	GaimPresence *presence = gaim_buddy_get_presence(buddy);
@@ -541,27 +539,27 @@
 
 	user = buddy->proto_data;
 
+	
 	if (gaim_presence_is_online(presence))
 	{
-		g_string_append_printf(str, _("\n<b>%s:</b> %s"), _("Status"),
-							   gaim_presence_is_idle(presence) ?
-							   _("Idle") : gaim_status_get_name(status));
+		gaim_notify_user_info_add_pair(user_info, _("Status"),
+									   (gaim_presence_is_idle(presence) ? _("Idle") : gaim_status_get_name(status)));
 	}
-
+	
 	if (full && user)
 	{
-		g_string_append_printf(str, _("\n<b>%s:</b> %s"), _("Has you"),
-							   (user->list_op & (1 << MSN_LIST_RL)) ?
-							   _("Yes") : _("No"));
+		gaim_notify_user_info_add_pair(user_info, _("Has you"),
+									   ((user->list_op & (1 << MSN_LIST_RL)) ? _("Yes") : _("No")));
+	}
 
 	/* XXX: This is being shown in non-full tooltips because the
 	 * XXX: blocked icon overlay isn't always accurate for MSN.
 	 * XXX: This can die as soon as gaim_privacy_check() knows that
 	 * XXX: this prpl always honors both the allow and deny lists. */
 	if (user)
-		g_string_append_printf(str, _("\n<b>%s:</b> %s"), _("Blocked"),
-							   (user->list_op & (1 << MSN_LIST_BL)) ?
-							   _("Yes") : _("No"));
+	{
+		gaim_notify_user_info_add_pair(user_info, _("Blocked"),
+									   ((user->list_op & (1 << MSN_LIST_BL)) ? _("Yes") : _("No")));
 	}
 }
 
@@ -1341,47 +1339,44 @@
 	}
 }
 
-static char *
-msn_tooltip_info_text(MsnGetInfoData *info_data)
+/**
+ * Extract info text from info_data and add it to user_info
+ */
+static gboolean
+msn_tooltip_extract_info_text(GaimNotifyUserInfo *user_info, MsnGetInfoData *info_data)
 {
-	GString *s;
 	GaimBuddy *b;
 
-	s = g_string_sized_new(80); /* wild guess */
-
 	b = gaim_find_buddy(gaim_connection_get_account(info_data->gc),
 						info_data->name);
 
 	if (b)
 	{
-		GString *str = g_string_new("");
 		char *tmp;
 
 		if (b->alias && b->alias[0])
 		{
 			char *aliastext = g_markup_escape_text(b->alias, -1);
-			g_string_append_printf(s, _("<b>Alias:</b> %s<br>"), aliastext);
+			gaim_notify_user_info_add_pair(user_info, _("Alias"), aliastext);
 			g_free(aliastext);
 		}
 
 		if (b->server_alias)
 		{
 			char *nicktext = g_markup_escape_text(b->server_alias, -1);
-			g_string_append_printf(s, _("<b>%s:</b> "), _("Nickname"));
-			g_string_append_printf(s, "<font sml=\"msn\">%s</font><br>",
-					nicktext);
+			tmp = g_strdup_printf("<font sml=\"msn\">%s</font><br>", nicktext);
+			gaim_notify_user_info_add_pair(user_info, _("Nickname"), tmp);
+			g_free(tmp);
 			g_free(nicktext);
 		}
 
-		msn_tooltip_text(b, str, TRUE);
-		tmp = gaim_strreplace((*str->str == '\n' ? str->str + 1 : str->str),
-							  "\n", "<br>");
-		g_string_free(str, TRUE);
-		g_string_append_printf(s, "%s<br>", tmp);
-		g_free(tmp);
+		/* Add the tooltip information */
+		msn_tooltip_text(b, user_info, TRUE);
+
+		return TRUE;
 	}
 
-	return g_string_free(s, FALSE);
+	return FALSE;
 }
 
 #if PHOTO_SUPPORT
@@ -1419,7 +1414,7 @@
 #endif
 
 #define MSN_GOT_INFO_GET_FIELD(a, b) \
-	found = gaim_markup_extract_info_field(stripped, stripped_len, s, \
+	found = gaim_markup_extract_info_field(stripped, stripped_len, user_info, \
 			"\n" a ":", 0, "\n", 0, "Undisclosed", b, 0, NULL, NULL); \
 	if (found) \
 		sect_info = TRUE;
@@ -1429,17 +1424,15 @@
 		const gchar *url_text, size_t len, const gchar *error_message)
 {
 	MsnGetInfoData *info_data = (MsnGetInfoData *)data;
-	char *stripped, *p, *q;
-	char buf[1024];
-	char *tooltip_text;
+	GaimNotifyUserInfo *user_info;
+	char *stripped, *p, *q, *tmp;
 	char *user_url = NULL;
 	gboolean found;
+	gboolean has_tooltip_text = FALSE;
 	gboolean has_info = FALSE;
 	gboolean sect_info = FALSE;
-	const char* title = NULL;
+	gboolean has_contact_info = FALSE;
 	char *url_buffer;
-	char *personal = NULL;
-	char *business = NULL;
 	GString *s, *s2;
 	int stripped_len;
 #if PHOTO_SUPPORT
@@ -1458,17 +1451,18 @@
 		return;
 	}
 
-	tooltip_text = msn_tooltip_info_text(info_data);
-	title = _("MSN Profile");
+	user_info = gaim_notify_user_info_new();
+	has_tooltip_text = msn_tooltip_extract_info_text(user_info, info_data);
 
 	if (error_message != NULL || url_text == NULL || strcmp(url_text, "") == 0)
 	{
-		g_snprintf(buf, 1024, "<html><body>%s<b>%s</b></body></html>",
-				tooltip_text, _("Error retrieving profile"));
+		tmp = g_strdup_printf("<b>%s</b>", _("Error retrieving profile"));
+		gaim_notify_user_info_add_pair(user_info, NULL, tmp);
+		g_free(tmp);
 
-		gaim_notify_userinfo(info_data->gc, info_data->name, buf, NULL, NULL);
+		gaim_notify_userinfo(info_data->gc, info_data->name, user_info, NULL, NULL);
+		gaim_notify_user_info_destroy(user_info);
 
-		g_free(tooltip_text);
 		g_free(info_data->name);
 		g_free(info_data);
 		return;
@@ -1528,10 +1522,16 @@
 	/* No we're not. */
 	s = g_string_sized_new(strlen(url_buffer));
 	s2 = g_string_sized_new(strlen(url_buffer));
-
+	
+	/* General section header */
+	if (has_tooltip_text)
+		gaim_notify_user_info_add_section_break(user_info);
+	
+	gaim_notify_user_info_add_section_header(user_info, _("General"));
+	
 	/* Extract their Name and put it in */
-	MSN_GOT_INFO_GET_FIELD("Name", _("Name"))
-
+	MSN_GOT_INFO_GET_FIELD("Name", _("Name"));
+	
 	/* General */
 	MSN_GOT_INFO_GET_FIELD("Nickname", _("Nickname"));
 	MSN_GOT_INFO_GET_FIELD("Age", _("Age"));
@@ -1540,7 +1540,7 @@
 	MSN_GOT_INFO_GET_FIELD("Location", _("Location"));
 
 	/* Extract their Interests and put it in */
-	found = gaim_markup_extract_info_field(stripped, stripped_len, s,
+	found = gaim_markup_extract_info_field(stripped, stripped_len, user_info,
 			"\nInterests\t", 0, " (/default.aspx?page=searchresults", 0,
 			"Undisclosed", _("Hobbies and Interests") /* _("Interests") */,
 			0, NULL, NULL);
@@ -1549,20 +1549,24 @@
 		sect_info = TRUE;
 
 	MSN_GOT_INFO_GET_FIELD("More about me", _("A Little About Me"));
-
+	
 	if (sect_info)
 	{
-		/* trim off the trailing "<br>\n" */
-		g_string_truncate(s, strlen(s->str) - 5);
-		g_string_append_printf(s2, _("%s<b>General</b><br>%s"),
-							   (*tooltip_text) ? "<hr>" : "", s->str);
-		s = g_string_truncate(s, 0);
 		has_info = TRUE;
 		sect_info = FALSE;
 	}
-
-
+    else 
+    {
+		/* Remove the section header */
+		gaim_notify_user_info_remove_last_item(user_info);
+		if (has_tooltip_text)
+			gaim_notify_user_info_remove_last_item(user_info);
+	}
+											   
 	/* Social */
+	gaim_notify_user_info_add_section_break(user_info);
+	gaim_notify_user_info_add_section_header(user_info, _("Social"));
+										   
 	MSN_GOT_INFO_GET_FIELD("Marital status", _("Marital Status"));
 	MSN_GOT_INFO_GET_FIELD("Interested in", _("Interests"));
 	MSN_GOT_INFO_GET_FIELD("Pets", _("Pets"));
@@ -1575,14 +1579,22 @@
 
 	if (sect_info)
 	{
-		g_string_append_printf(s2, _("%s<b>Social</b><br>%s"), has_info ? "<br><hr>" : "", s->str);
-		s = g_string_truncate(s, 0);
 		has_info = TRUE;
 		sect_info = FALSE;
 	}
+    else 
+    {
+		/* Remove the section header */
+		gaim_notify_user_info_remove_last_item(user_info);
+		gaim_notify_user_info_remove_last_item(user_info);
+	}
 
 	/* Contact Info */
 	/* Personal */
+	gaim_notify_user_info_add_section_break(user_info);
+	gaim_notify_user_info_add_section_header(user_info, _("Contact Info"));
+	gaim_notify_user_info_add_section_header(user_info, _("Personal"));
+
 	MSN_GOT_INFO_GET_FIELD("Name", _("Name"));
 	MSN_GOT_INFO_GET_FIELD("Significant other", _("Significant Other"));
 	MSN_GOT_INFO_GET_FIELD("Home phone", _("Home Phone"));
@@ -1598,13 +1610,18 @@
 
 	if (sect_info)
 	{
-		personal = g_strdup_printf(_("<br><b>Personal</b><br>%s"), s->str);
-		s = g_string_truncate(s, 0);
+		has_info = TRUE;
 		sect_info = FALSE;
-		has_info = TRUE;
+		has_contact_info = TRUE;
+	}
+    else 
+    {
+		/* Remove the section header */
+		gaim_notify_user_info_remove_last_item(user_info);
 	}
 
 	/* Business */
+	gaim_notify_user_info_add_section_header(user_info, _("Work"));
 	MSN_GOT_INFO_GET_FIELD("Name", _("Name"));
 	MSN_GOT_INFO_GET_FIELD("Job title", _("Job Title"));
 	MSN_GOT_INFO_GET_FIELD("Company", _("Company"));
@@ -1623,27 +1640,22 @@
 
 	if (sect_info)
 	{
-		business = g_strdup_printf(_("<br><b>Business</b><br>%s"), s->str);
-		s = g_string_truncate(s, 0);
+		has_info = TRUE;
 		sect_info = FALSE;
+		has_contact_info = TRUE;
+	}
+    else 
+    {
+		/* Remove the section header */
+		gaim_notify_user_info_remove_last_item(user_info);
 	}
 
-	if ((personal != NULL) || (business != NULL))
+	if (!has_contact_info)
 	{
-		/* trim off the trailing "<br>\n" */
-		g_string_truncate(s, strlen(s->str) - 5);
-
-		has_info = TRUE;
-		g_string_append_printf(s2, _("<hr><b>Contact Info</b>%s%s"),
-							   personal ? personal : "",
-							   business ? business : "");
+		/* Remove the Contact Info section header */
+		gaim_notify_user_info_remove_last_item(user_info);
 	}
 
-	g_free(personal);
-	g_free(business);
-	g_string_free(s, TRUE);
-	s = s2;
-
 #if 0 /* these probably don't show up any more */
 	/*
 	 * The fields, 'A Little About Me', 'Favorite Things', 'Hobbies
@@ -1768,12 +1780,9 @@
 	/* If we were able to fetch a homepage url earlier, stick it in there */
 	if (user_url != NULL)
 	{
-		g_snprintf(buf, sizeof(buf),
-				   "<b>%s:</b><br><a href=\"%s\">%s</a><br>\n",
-				   _("Homepage"), user_url, user_url);
-
-		g_string_append(s, buf);
-
+		tmp = g_strdup_printf("<a href=\"%s\">%s</a>", user_url, user_url);
+		gaim_notify_user_info_add_pair(user_info, _("Homepage"), tmp);
+		g_free(tmp);
 		g_free(user_url);
 
 		has_info = TRUE;
@@ -1792,25 +1801,22 @@
 		char *p = strstr(url_buffer, "form id=\"SpacesSearch\" name=\"SpacesSearch\"");
 		GaimBuddy *b = gaim_find_buddy
 				(gaim_connection_get_account(info_data->gc), info_data->name);
-		g_string_append_printf(s, "<br><b>%s</b><br>%s<br><br>",
-				_("Error retrieving profile"),
-				((p && b)?
-					_("The user has not created a public profile."):
-				 p? _("MSN reported not being able to find the user's profile. "
-					  "This either means that the user does not exist, "
-					  "or that the user exists "
-					  "but has not created a public profile."):
-					_("Gaim could not find "	/* This should never happen */
-					  "any information in the user's profile. "
-					  "The user most likely does not exist.")));
+		gaim_notify_user_info_add_pair(user_info, _("Error retrieving profile"),
+									   ((p && b) ? _("The user has not created a public profile.") :
+										(p ? _("MSN reported not being able to find the user's profile. "
+											   "This either means that the user does not exist, "
+											   "or that the user exists "
+											   "but has not created a public profile.") :
+										 _("Gaim could not find "	/* This should never happen */
+										   "any information in the user's profile. "
+										   "The user most likely does not exist."))));
 	}
+
 	/* put a link to the actual profile URL */
-	g_string_append_printf(s, _("<hr><b>%s:</b> "), _("Profile URL"));
-	g_string_append_printf(s, "<br><a href=\"%s%s\">%s%s</a><br>",
-			PROFILE_URL, info_data->name, PROFILE_URL, info_data->name);
-
-	/* Finish it off, and show it to them */
-	g_string_append(s, "</body></html>\n");
+	tmp = g_strdup_printf("<a href=\"%s%s\">%s%s</a>",
+					PROFILE_URL, info_data->name, PROFILE_URL, info_data->name);
+	gaim_notify_user_info_add_pair(user_info, _("Profile URL"), tmp);
+	g_free(tmp);									   
 
 #if PHOTO_SUPPORT
 	/* Find the URL to the photo; must be before the marshalling [Bug 994207] */
@@ -1821,10 +1827,8 @@
 	info2_data->info_data = info_data;
 	info2_data->stripped = stripped;
 	info2_data->url_buffer = url_buffer;
-	info2_data->s = s;
+	info2_data->user_info = user_info;
 	info2_data->photo_url_text = photo_url_text;
-	info2_data->tooltip_text = tooltip_text;
-	info2_data->title = title;
 
 	/* Try to put the photo in there too, if there's one */
 	if (photo_url_text)
@@ -1851,9 +1855,8 @@
 	MsnGetInfoData *info_data = info2_data->info_data;
 	char *stripped = info2_data->stripped;
 	char *url_buffer = info2_data->url_buffer;
-	GString *s = info2_data->s;
+	GaimNotifyUserInfo *user_info = info2_data->user_info;
 	char *photo_url_text = info2_data->photo_url_text;
-	char *tooltip_text = info2_data->tooltip_text;
 
 	/* Make sure the connection is still valid if we got here by fetching a photo url */
 	if (url_text && (error_message != NULL ||
@@ -1862,8 +1865,7 @@
 		gaim_debug_warning("msn", "invalid connection. ignoring buddy photo info.\n");
 		g_free(stripped);
 		g_free(url_buffer);
-		g_string_free(s, TRUE);
-		g_free(tooltip_text);
+		g_free(user_info);
 		g_free(info_data->name);
 		g_free(info_data);
 		g_free(photo_url_text);
@@ -1889,20 +1891,17 @@
 			gaim_debug_info("msn", "%s is %d bytes\n", photo_url_text, len);
 			id = gaim_imgstore_add(url_text, len, NULL);
 			g_snprintf(buf, sizeof(buf), "<img id=\"%d\"><br>", id);
-			g_string_prepend(s, buf);
+			gaim_notify_user_info_prepend_pair(user_info, NULL, buf);
 		}
 	}
 
 	/* We continue here from msn_got_info, as if nothing has happened */
 #endif
-
-	g_string_prepend(s, tooltip_text);
-	gaim_notify_userinfo(info_data->gc, info_data->name, s->str, NULL, NULL);
+	gaim_notify_userinfo(info_data->gc, info_data->name, user_info, NULL, NULL);
 
 	g_free(stripped);
 	g_free(url_buffer);
-	g_string_free(s, TRUE);
-	g_free(tooltip_text);
+	gaim_notify_user_info_destroy(user_info);
 	g_free(info_data->name);
 	g_free(info_data);
 #if PHOTO_SUPPORT
--- a/libgaim/protocols/novell/novell.c	Sat Dec 09 20:20:17 2006 +0000
+++ b/libgaim/protocols/novell/novell.c	Sun Dec 10 02:53:09 2006 +0000
@@ -1494,6 +1494,7 @@
 static void
 _show_info(GaimConnection * gc, NMUserRecord * user_record)
 {
+	GaimNotifyUserInfo *user_info =	gaim_notify_user_info_new();
 	GString *info_text;
 	int count, i;
 	NMProperty *property;
@@ -1504,21 +1505,20 @@
 	tag = _("User ID");
 	value = nm_user_record_get_userid(user_record);
 	if (value) {
-		g_string_append_printf(info_text, "<b>%s:</b> %s<br>", tag, value);
+		gaim_notify_user_info_add_pair(user_info, tag, value);
 	}
 
 /*	tag = _("DN");
 	value = nm_user_record_get_dn(user_record);
 	if (value) {
-	g_string_append_printf(info_text, "<b>%s:</b> %s<br>",
-	tag, value);
+		gaim_notify_user_info_add_pair(user_info, tag, value);
 	}
 */
 
 	tag = _("Full name");
 	value = nm_user_record_get_full_name(user_record);
 	if (value) {
-		g_string_append_printf(info_text, "<b>%s:</b> %s<br>", tag, value);
+		gaim_notify_user_info_add_pair(user_info, tag, value);
 	}
 
 	count = nm_user_record_get_property_count(user_record);
@@ -1528,17 +1528,15 @@
 			tag = _map_property_tag(nm_property_get_tag(property));
 			value = nm_property_get_value(property);
 			if (tag && value) {
-				g_string_append_printf(info_text, "<b>%s:</b> %s<br>",
-									   tag, value);
+				gaim_notify_user_info_add_pair(user_info, tag, value);
 			}
 			nm_release_property(property);
 		}
 	}
 
 	gaim_notify_userinfo(gc, nm_user_record_get_userid(user_record), 
-						 info_text->str, NULL, NULL);
-
-	g_string_free(info_text, TRUE);
+						 user_info, NULL, NULL);
+	gaim_notify_user_info_destroy(user_info);
 }
 
 /* Send a join conference, the first item in the parms list is the
@@ -2826,7 +2824,7 @@
 }
 
 static void
-novell_tooltip_text(GaimBuddy * buddy, GString * str, gboolean full)
+novell_tooltip_text(GaimBuddy * buddy, GaimNotifyUserInfo * user_info, gboolean full)
 {
 	NMUserRecord *user_record = NULL;
 	GaimConnection *gc;
@@ -2869,14 +2867,10 @@
 					break;
 			}
 
+			gaim_notify_user_info_add_pair(user_info, _("Status"), status_str);
+			
 			if (text)
-				g_string_append_printf(str, "\n<b>%s:</b> %s"
-										    "\n<b>%s:</b> %s",
-										    _("Status"), status_str,
-										    _("Message"), text);
-			else
-				g_string_append_printf(str, "\n<b>%s:</b> %s",
-										    _("Status"), status_str);
+				gaim_notify_user_info_add_pair(user_info, _("Message"), text);
 		}
 	}
 }
--- a/libgaim/protocols/oscar/oscar.c	Sat Dec 09 20:20:17 2006 +0000
+++ b/libgaim/protocols/oscar/oscar.c	Sun Dec 10 02:53:09 2006 +0000
@@ -705,11 +705,22 @@
 }
 
 static void
-oscar_string_append(GString *str, const char *newline,
-					const char *name, const char *value)
+oscar_user_info_add_pair(GaimNotifyUserInfo *user_info, const char *name, const char *value)
 {
 	if (value && value[0]) {
-		g_string_append_printf(str, "%s<b>%s:</b> %s", newline, name, value);
+		gaim_notify_user_info_add_pair(user_info, name, value);
+	}
+}
+
+static void
+oscar_user_info_convert_and_add_pair(GaimAccount *account, GaimNotifyUserInfo *user_info,
+									 const char *name, const char *value)
+{
+	gchar *utf8;
+	
+	if (value && value[0] && (utf8 = oscar_utf8_try_convert(account, value))) {
+		gaim_notify_user_info_add_pair(user_info, name, utf8);
+		g_free(utf8);
 	}
 }
 
@@ -725,7 +736,19 @@
 	}
 }
 
-static void oscar_string_append_info(GaimConnection *gc, GString *str, const char *newline, GaimBuddy *b, aim_userinfo_t *userinfo)
+static void
+oscar_user_info_convert_and_add(GaimAccount *account, GaimNotifyUserInfo *user_info,
+								const char *name, const char *value)
+{
+	gchar *utf8;
+	
+	if (value && value[0] && (utf8 = oscar_utf8_try_convert(account, value))) {
+		gaim_notify_user_info_add_pair(user_info, name, value);
+		g_free(utf8);
+	}
+}
+
+static void oscar_string_append_info(GaimConnection *gc, GaimNotifyUserInfo *user_info, GaimBuddy *b, aim_userinfo_t *userinfo)
 {
 	OscarData *od;
 	GaimAccount *account;
@@ -738,7 +761,7 @@
 	od = gc->proto_data;
 	account = gaim_connection_get_account(gc);
 
-	if ((str == NULL) || (newline == NULL) || ((b == NULL) && (userinfo == NULL)))
+	if ((user_info == NULL) || ((b == NULL) && (userinfo == NULL)))
 		return;
 
 	if (userinfo == NULL)
@@ -760,17 +783,14 @@
 		if (gaim_presence_is_online(presence)) {
 			if (aim_sn_is_icq(b->name)) {
 				GaimStatus *status = gaim_presence_get_active_status(presence);
-				oscar_string_append(str, newline, _("Status"),
-									gaim_status_get_name(status));
+				oscar_user_info_add_pair(user_info, _("Status"),	gaim_status_get_name(status));
 			}
 		} else {
 			tmp = aim_ssi_itemlist_findparentname(od->ssi.local, b->name);
 			if (aim_ssi_waitingforauth(od->ssi.local, tmp, b->name))
-				oscar_string_append(str, newline, _("Status"),
-									_("Not Authorized"));
+				oscar_user_info_add_pair(user_info, _("Status"),	_("Not Authorized"));
 			else
-				oscar_string_append(str, newline, _("Status"),
-									_("Offline"));
+				oscar_user_info_add_pair(user_info, _("Status"),	_("Offline"));
 		}
 	}
 
@@ -780,14 +800,14 @@
 						(bi->ipaddr & 0x00ff0000) >> 16,
 						(bi->ipaddr & 0x0000ff00) >> 8,
 						(bi->ipaddr & 0x000000ff));
-		oscar_string_append(str, newline, _("IP Address"), tmp);
+		oscar_user_info_add_pair(user_info, _("IP Address"), tmp);
 		g_free(tmp);
 	}
 
 
 	if ((userinfo != NULL) && (userinfo->warnlevel != 0)) {
 		tmp = g_strdup_printf("%d", (int)(userinfo->warnlevel/10.0 + .5));
-		oscar_string_append(str, newline, _("Warning Level"), tmp);
+		oscar_user_info_add_pair(user_info, _("Warning Level"), tmp);
 		g_free(tmp);
 	}
 
@@ -796,7 +816,8 @@
 		if (tmp != NULL) {
 			char *tmp2 = g_markup_escape_text(tmp, strlen(tmp));
 			g_free(tmp);
-			oscar_string_convert_and_append(account, str, newline, _("Buddy Comment"), tmp2);
+
+			oscar_user_info_convert_and_add_pair(account, user_info, _("Buddy Comment"), tmp2);
 			g_free(tmp2);
 		}
 	}
@@ -2581,21 +2602,25 @@
 
 	switch(reason) {
 		case 0x0003: { /* Reply from an ICQ status message request */
-			char *title, *statusmsg, **splitmsg, *dialogmsg;
-
-			title = g_strdup_printf(_("Info for %s"), who);
+			char *statusmsg, **splitmsg;
+			GaimNotifyUserInfo *user_info;
 
 			/* Split at (carriage return/newline)'s, then rejoin later with BRs between. */
 			statusmsg = oscar_icqstatus(state);
 			splitmsg = g_strsplit(msg, "\r\n", 0);
-			dialogmsg = g_strdup_printf(_("<B>UIN:</B> %s<BR><B>Status:</B> %s<HR>%s"), who, statusmsg, g_strjoinv("<BR>", splitmsg));
+			
+			user_info = gaim_notify_user_info_new();
+				
+			gaim_notify_user_info_add_pair(user_info, _("UIN"), who);
+			gaim_notify_user_info_add_pair(user_info, _("Status"), statusmsg);
+			gaim_notify_user_info_add_pair(user_info, NULL, g_strjoinv("<BR>", splitmsg));
+
 			g_free(statusmsg);
 			g_strfreev(splitmsg);
 
-			gaim_notify_userinfo(gc, who, dialogmsg, NULL, NULL);
-
-			g_free(title);
-			g_free(dialogmsg);
+			gaim_notify_userinfo(gc, who, user_info, NULL, NULL);
+			gaim_notify_user_info_destroy(user_info);
+
 		} break;
 
 		default: {
@@ -2766,7 +2791,7 @@
 static int gaim_parse_userinfo(OscarData *od, FlapConnection *conn, FlapFrame *fr, ...) {
 	GaimConnection *gc = od->gc;
 	GaimAccount *account = gaim_connection_get_account(gc);
-	GString *str;
+	GaimNotifyUserInfo *user_info;
 	gchar *tmp = NULL, *info_utf8 = NULL, *away_utf8 = NULL;
 	va_list ap;
 	aim_userinfo_t *userinfo;
@@ -2775,33 +2800,36 @@
 	userinfo = va_arg(ap, aim_userinfo_t *);
 	va_end(ap);
 
-	str = g_string_new("");
-	g_string_append_printf(str, "<b>%s:</b> %s", _("Screen Name"), userinfo->sn);
-	g_string_append_printf(str, "\n<br><b>%s</b>: %d%%", _("Warning Level"), (int)((userinfo->warnlevel/10.0) + 0.5));
+	user_info = gaim_notify_user_info_new();
+	gaim_notify_user_info_add_pair(user_info, _("Screen Name"), userinfo->sn);
+
+	tmp = g_strdup_printf("%d", (int)((userinfo->warnlevel/10.0) + 0.5));
+	gaim_notify_user_info_add_pair(user_info, _("Warning Level"), tmp);
+	g_free(tmp);
 
 	if (userinfo->present & AIM_USERINFO_PRESENT_ONLINESINCE) {
 		time_t t = userinfo->onlinesince - od->timeoffset;
-		oscar_string_append(str, "\n<br>", _("Online Since"), gaim_date_format_full(localtime(&t)));
+		oscar_user_info_add_pair(user_info, _("Online Since"), gaim_date_format_full(localtime(&t)));
 	}
 
 	if (userinfo->present & AIM_USERINFO_PRESENT_MEMBERSINCE) {
 		time_t t = userinfo->membersince - od->timeoffset;
-		oscar_string_append(str, "\n<br>", _("Member Since"), gaim_date_format_full(localtime(&t)));
+		oscar_user_info_add_pair(user_info, _("Member Since"), gaim_date_format_full(localtime(&t)));
 	}
 
 	if (userinfo->capabilities != 0) {
 		tmp = oscar_caps_to_string(userinfo->capabilities);
-		oscar_string_append(str, "\n<br>", _("Capabilities"), tmp);
+		oscar_user_info_add_pair(user_info, _("Capabilities"), tmp);
 		g_free(tmp);
 	}
 
 	if (userinfo->present & AIM_USERINFO_PRESENT_IDLE) {
 		tmp = gaim_str_seconds_to_string(userinfo->idletime*60);
-		oscar_string_append(str, "\n<br>", _("Idle"), tmp);
+		oscar_user_info_add_pair(user_info, _("Idle"), tmp);
 		g_free(tmp);
 	}
 
-	oscar_string_append_info(gc, str, "\n<br>", NULL, userinfo);
+	oscar_string_append_info(gc, user_info, NULL, userinfo);
 
 	/* Available message */
 	if ((userinfo->status != NULL) && !(userinfo->flags & AIM_FLAG_AWAY))
@@ -2809,7 +2837,7 @@
 		if (userinfo->status[0] != '\0')
 			tmp = oscar_encoding_to_utf8(userinfo->status_encoding,
 											 userinfo->status, userinfo->status_len);
-		oscar_string_convert_and_append(account, str, "\n<br>", _("Available Message"), tmp);
+		oscar_user_info_add_pair(user_info, _("Available Message"), tmp);
 		g_free(tmp);
 	}
 
@@ -2819,7 +2847,9 @@
 		away_utf8 = oscar_encoding_to_utf8(tmp, userinfo->away, userinfo->away_len);
 		g_free(tmp);
 		if (away_utf8 != NULL) {
-			g_string_append_printf(str, "\n<hr>%s", away_utf8);
+			tmp = gaim_str_sub_away_formatters(away_utf8, gaim_account_get_username(account));
+			oscar_user_info_add_pair(user_info, NULL, tmp);
+			g_free(tmp);
 			g_free(away_utf8);
 		}
 	}
@@ -2830,16 +2860,15 @@
 		info_utf8 = oscar_encoding_to_utf8(tmp, userinfo->info, userinfo->info_len);
 		g_free(tmp);
 		if (info_utf8 != NULL) {
-			g_string_append_printf(str, "\n<hr>%s", info_utf8);
+			tmp = gaim_str_sub_away_formatters(info_utf8, gaim_account_get_username(account));
+			oscar_user_info_add_pair(user_info, _("Profile"), tmp);
+			g_free(tmp);
 			g_free(info_utf8);
 		}
 	}
 
-	tmp = gaim_str_sub_away_formatters(str->str, gaim_account_get_username(account));
-	g_string_free(str, TRUE);
-	gaim_str_strip_char(tmp, '\r');
-	gaim_notify_userinfo(gc, userinfo->sn, tmp, NULL, NULL);
-	g_free(tmp);
+	gaim_notify_userinfo(gc, userinfo->sn, user_info, NULL, NULL);
+	gaim_notify_user_info_destroy(user_info);
 
 	return 1;
 }
@@ -3581,8 +3610,10 @@
 	GaimBuddy *buddy;
 	struct buddyinfo *bi;
 	gchar who[16];
-	GString *str;
+	GaimNotifyUserInfo *user_info;
+	GString *tmp;
 	gchar *utf8;
+	gchar *buf;
 	const gchar *alias;
 	va_list ap;
 	struct aim_icq_info *info;
@@ -3597,7 +3628,8 @@
 	if (!info->uin)
 		return 0;
 
-	str = g_string_sized_new(100);
+	user_info = gaim_notify_user_info_new();
+		
 	g_snprintf(who, sizeof(who), "%u", info->uin);
 	buddy = gaim_find_buddy(gaim_connection_get_account(gc), who);
 	if (buddy != NULL)
@@ -3605,35 +3637,41 @@
 	else
 		bi = NULL;
 
-	g_string_append_printf(str, "<b>%s:</b> %s", _("UIN"), who);
-	oscar_string_convert_and_append(account, str, "\n<br>", _("Nick"), info->nick);
+	gaim_notify_user_info_add_pair(user_info, _("UIN"), who);
+	oscar_user_info_convert_and_add(account, user_info, _("Nick"), info->nick);
 	if ((bi != NULL) && (bi->ipaddr != 0)) {
 		char *tstr =  g_strdup_printf("%hhu.%hhu.%hhu.%hhu",
 						(bi->ipaddr & 0xff000000) >> 24,
 						(bi->ipaddr & 0x00ff0000) >> 16,
 						(bi->ipaddr & 0x0000ff00) >> 8,
 						(bi->ipaddr & 0x000000ff));
-		oscar_string_append(str, "\n<br>", _("IP Address"), tstr);
+		gaim_notify_user_info_add_pair(user_info, _("IP Address"), tstr);
 		g_free(tstr);
 	}
-	oscar_string_convert_and_append(account, str, "\n<br>", _("First Name"), info->first);
-	oscar_string_convert_and_append(account, str, "\n<br>", _("Last Name"), info->last);
+	oscar_user_info_convert_and_add(account, user_info, _("First Name"), info->first);
+	oscar_user_info_convert_and_add(account, user_info, _("Last Name"), info->last);
 	if (info->email && info->email[0] && (utf8 = oscar_utf8_try_convert(gc->account, info->email))) {
-		g_string_append_printf(str, "\n<br><b>%s:</b> <a href=\"mailto:%s\">%s</a>", _("E-Mail Address"), utf8, utf8);
+		buf = g_strdup_printf("<a href=\"mailto:%s\">%s</a>", utf8, utf8);
+		gaim_notify_user_info_add_pair(user_info, _("E-Mail Address"), buf);
+		g_free(buf);
 		g_free(utf8);
 	}
 	if (info->numaddresses && info->email2) {
 		int i;
 		for (i = 0; i < info->numaddresses; i++) {
 			if (info->email2[i] && info->email2[i][0] && (utf8 = oscar_utf8_try_convert(gc->account, info->email2[i]))) {
-				g_string_append_printf(str, "\n<br><b>%s:</b> <a href=\"mailto:%s\">%s</a>", _("E-Mail Address"), utf8, utf8);
+				buf = g_strdup_printf("<a href=\"mailto:%s\">%s</a>", utf8, utf8);
+				gaim_notify_user_info_add_pair(user_info, _("E-Mail Address"), buf);
+				g_free(buf);
 				g_free(utf8);
 			}
 		}
 	}
-	oscar_string_convert_and_append(account, str, "\n<br>", _("Mobile Phone"), info->mobile);
+	oscar_user_info_convert_and_add(account, user_info, _("Mobile Phone"), info->mobile);
+
 	if (info->gender != 0)
-		oscar_string_append(str, "\n<br>", _("Gender"), info->gender == 1 ? _("Female") : _("Male"));
+		gaim_notify_user_info_add_pair(user_info, _("Gender"), (info->gender == 1 ? _("Female") : _("Male")));
+
 	if ((info->birthyear > 1900) && (info->birthmonth > 0) && (info->birthday > 0)) {
 		/* Initialize the struct properly or strftime() will crash
 		 * under some conditions (e.g. Debian sarge w/ LANG=en_HK). */
@@ -3649,57 +3687,68 @@
 		 * feel free to remove it.  --rlaager */
 		mktime(tm);
 
-		oscar_string_append(str, "\n<br>", _("Birthday"),
-				    gaim_date_format_short(tm));
+		oscar_user_info_convert_and_add(account, user_info, _("Birthday"), gaim_date_format_short(tm));
 	}
 	if ((info->age > 0) && (info->age < 255)) {
 		char age[5];
 		snprintf(age, sizeof(age), "%hhd", info->age);
-		oscar_string_append(str, "\n<br>", _("Age"), age);
+		gaim_notify_user_info_add_pair(user_info,
+													_("Age"), age);
 	}
 	if (info->personalwebpage && info->personalwebpage[0] && (utf8 = oscar_utf8_try_convert(gc->account, info->personalwebpage))) {
-		g_string_append_printf(str, "\n<br><b>%s:</b> <a href=\"%s\">%s</a>", _("Personal Web Page"), utf8, utf8);
-		g_free(utf8);
-	}
-	if (info->info && info->info[0] && (utf8 = oscar_utf8_try_convert(gc->account, info->info))) {
-		g_string_append_printf(str, "<hr><b>%s:</b><br>%s", _("Additional Information"), utf8);
+		buf = g_strdup_printf("<a href=\"%s\">%s</a>", utf8, utf8);
+		gaim_notify_user_info_add_pair(user_info, _("Personal Web Page"), buf);
+		g_free(buf);
 		g_free(utf8);
 	}
-	g_string_append_printf(str, "<hr>");
+	
+	oscar_user_info_convert_and_add(account, user_info, _("Additional Information"), info->info);
+
+/*	g_string_append_printf(str, "<hr>"); */
+
 	if ((info->homeaddr && (info->homeaddr[0])) || (info->homecity && info->homecity[0]) || (info->homestate && info->homestate[0]) || (info->homezip && info->homezip[0])) {
-		g_string_append_printf(str, "<b>%s:</b>", _("Home Address"));
-		oscar_string_convert_and_append(account, str, "\n<br>", _("Address"), info->homeaddr);
-		oscar_string_convert_and_append(account, str, "\n<br>", _("City"), info->homecity);
-		oscar_string_convert_and_append(account, str, "\n<br>", _("State"), info->homestate);
-		oscar_string_convert_and_append(account, str, "\n<br>", _("Zip Code"), info->homezip);
-		g_string_append_printf(str, "\n<hr>");
+		tmp = g_string_sized_new(100);
+		oscar_string_convert_and_append(account, tmp, "\n<br>", _("Address"), info->homeaddr);
+		oscar_string_convert_and_append(account, tmp, "\n<br>", _("City"), info->homecity);
+		oscar_string_convert_and_append(account, tmp, "\n<br>", _("State"), info->homestate);
+		oscar_string_convert_and_append(account, tmp, "\n<br>", _("Zip Code"), info->homezip);
+		
+		gaim_notify_user_info_add_pair(user_info, _("Home Address"), tmp->str);
+		
+		g_string_free(tmp, TRUE);
 	}
 	if ((info->workaddr && info->workaddr[0]) || (info->workcity && info->workcity[0]) || (info->workstate && info->workstate[0]) || (info->workzip && info->workzip[0])) {
-		g_string_append_printf(str, "<b>%s:</b>", _("Work Address"));
-		oscar_string_convert_and_append(account, str, "\n<br>", _("Address"), info->workaddr);
-		oscar_string_convert_and_append(account, str, "\n<br>", _("City"), info->workcity);
-		oscar_string_convert_and_append(account, str, "\n<br>", _("State"), info->workstate);
-		oscar_string_convert_and_append(account, str, "\n<br>", _("Zip Code"), info->workzip);
-		g_string_append_printf(str, "\n<hr>");
+		tmp = g_string_sized_new(100);
+
+		oscar_string_convert_and_append(account, tmp, "\n<br>", _("Address"), info->workaddr);
+		oscar_string_convert_and_append(account, tmp, "\n<br>", _("City"), info->workcity);
+		oscar_string_convert_and_append(account, tmp, "\n<br>", _("State"), info->workstate);
+		oscar_string_convert_and_append(account, tmp, "\n<br>", _("Zip Code"), info->workzip);
+
+		gaim_notify_user_info_add_pair(user_info, _("Work Address"), tmp->str);
+		
+		g_string_free(tmp, TRUE);
 	}
 	if ((info->workcompany && info->workcompany[0]) || (info->workdivision && info->workdivision[0]) || (info->workposition && info->workposition[0]) || (info->workwebpage && info->workwebpage[0])) {
-		g_string_append_printf(str, "<b>%s:</b>", _("Work Information"));
-		oscar_string_convert_and_append(account, str, "\n<br>", _("Company"), info->workcompany);
-		oscar_string_convert_and_append(account, str, "\n<br>", _("Division"), info->workdivision);
-		oscar_string_convert_and_append(account, str, "\n<br>", _("Position"), info->workposition);
+		tmp = g_string_sized_new(100);
+		
+		oscar_string_convert_and_append(account, tmp, "\n<br>", _("Company"), info->workcompany);
+		oscar_string_convert_and_append(account, tmp, "\n<br>", _("Division"), info->workdivision);
+		oscar_string_convert_and_append(account, tmp, "\n<br>", _("Position"), info->workposition);
 		if (info->workwebpage && info->workwebpage[0] && (utf8 = oscar_utf8_try_convert(gc->account, info->workwebpage))) {
-			g_string_append_printf(str, "\n<br><b>%s:</b> <a href=\"%s\">%s</a>", _("Web Page"), utf8, utf8);
+			g_string_append_printf(tmp, "\n<br><b>%s:</b> <a href=\"%s\">%s</a>", _("Web Page"), utf8, utf8);
 			g_free(utf8);
 		}
-		g_string_append_printf(str, "\n<hr>");
+		gaim_notify_user_info_add_pair(user_info, _("Work Information"), tmp->str);
+		g_string_free(tmp, TRUE);
 	}
 
 	if (buddy != NULL)
 		alias = gaim_buddy_get_alias(buddy);
 	else
 		alias = who;
-	gaim_notify_userinfo(gc, who, str->str, NULL, NULL);
-	g_string_free(str, TRUE);
+	gaim_notify_userinfo(gc, who, user_info, NULL, NULL);
+	gaim_notify_user_info_destroy(user_info);
 
 	return 1;
 }
@@ -5361,7 +5410,7 @@
 	*ne = emblems[3];
 }
 
-void oscar_tooltip_text(GaimBuddy *b, GString *str, gboolean full) {
+void oscar_tooltip_text(GaimBuddy *b, GaimNotifyUserInfo *user_info, gboolean full) {
 	GaimConnection *gc = b->account->gc;
 	OscarData *od = gc->proto_data;
 	aim_userinfo_t *userinfo = aim_locate_finduserinfo(od, b->name);
@@ -5372,7 +5421,7 @@
 		const char *message;
 
 		if (full)
-			oscar_string_append_info(gc, str, "\n", b, userinfo);
+			oscar_string_append_info(gc, user_info, b, userinfo);
 
 		presence = gaim_buddy_get_presence(b);
 		status = gaim_presence_get_active_status(presence);
@@ -5385,7 +5434,7 @@
 				/* Available status messages are plain text */
 				gchar *tmp;
 				tmp = g_markup_escape_text(message, -1);
-				g_string_append_printf(str, "\n<b>%s:</b> %s", _("Message"), tmp);
+				gaim_notify_user_info_add_pair(user_info, _("Message"), tmp);
 				g_free(tmp);
 			}
 		}
@@ -5400,12 +5449,12 @@
 				g_free(tmp2);
 				tmp2 = gaim_str_sub_away_formatters(tmp1, gaim_account_get_username(gaim_connection_get_account(gc)));
 				g_free(tmp1);
-				g_string_append_printf(str, "\n<b>%s:</b> %s", _("Message"), tmp2);
+				gaim_notify_user_info_add_pair(user_info, _("Away Message"), tmp2);
 				g_free(tmp2);
 			}
 			else
 			{
-				g_string_append_printf(str, "\n<b>%s:</b> %s", _("Message"), _("<i>(retrieving)</i>"));
+				gaim_notify_user_info_add_pair(user_info, _("Away Message"), _("<i>(retrieving)</i>"));
 			}
 		}
 	}
--- a/libgaim/protocols/oscar/oscarcommon.h	Sat Dec 09 20:20:17 2006 +0000
+++ b/libgaim/protocols/oscar/oscarcommon.h	Sun Dec 10 02:53:09 2006 +0000
@@ -28,6 +28,7 @@
 #include "internal.h"
 #include "prpl.h"
 #include "version.h"
+#include "notify.h"
 
 #define OSCAR_DEFAULT_LOGIN_SERVER "login.oscar.aol.com"
 #define OSCAR_DEFAULT_LOGIN_PORT 5190
@@ -48,7 +49,7 @@
 const char *oscar_list_icon_aim(GaimAccount *a, GaimBuddy *b);
 void oscar_list_emblems(GaimBuddy *b, const char **se, const char **sw, const char **nw, const char **ne);
 char *oscar_status_text(GaimBuddy *b);
-void oscar_tooltip_text(GaimBuddy *b, GString *str, gboolean full);
+void oscar_tooltip_text(GaimBuddy *b, GaimNotifyUserInfo *user_info, gboolean full);
 GList *oscar_status_types(GaimAccount *account);
 GList *oscar_blist_node_menu(GaimBlistNode *node);
 GList *oscar_chat_info(GaimConnection *gc);
--- a/libgaim/protocols/qq/buddy_info.c	Sat Dec 09 20:20:17 2006 +0000
+++ b/libgaim/protocols/qq/buddy_info.c	Sun Dec 10 02:53:09 2006 +0000
@@ -153,56 +153,58 @@
 	}
 }
 
-static void append_field_value(GString *info_text, const gchar *field,
+static gboolean append_field_value(GaimNotifyUserInfo *user_info, const gchar *field,
 		const gchar *title, const gchar **choice, gint choice_size)
 {
 	gchar *value = field_value(field, choice, choice_size);
 
 	if (value != NULL) {
-		g_string_append_printf(info_text, "<br /><b>%s:</b> %s", title, value);
+		gaim_notify_user_info_add_pair(user_info, title, value);
 		g_free(value);
+		
+		return TRUE;
 	}
+	
+	return FALSE;
 }
 
-static GString *info_to_str(const contact_info *info)
+static GaimNotifyUserInfo *
+info_to_notify_user_info(const contact_info *info)
 {
-	GString *info_text, *extra_info;
+	GaimNotifyUserInfo *user_info = gaim_notify_user_info_new();
 	const gchar *intro;
-	gint len;
+	gboolean has_extra_info = FALSE;
 
-	info_text = g_string_new("");
-	g_string_append_printf(info_text, "<b>%s</b><br /><br />", QQ_PRIMARY_INFORMATION);
-	g_string_append_printf(info_text, "<b>%s:</b> %s", QQ_NUMBER, info->uid);
-	append_field_value(info_text, info->nick, QQ_NICKNAME, NULL, 0);
-	append_field_value(info_text, info->name, QQ_NAME, NULL, 0);
-	append_field_value(info_text, info->age, QQ_AGE, NULL, 0);
-	append_field_value(info_text, info->gender, QQ_GENDER, genders, QQ_GENDER_SIZE);
-	append_field_value(info_text, info->country, QQ_COUNTRY, NULL, 0);
-	append_field_value(info_text, info->province, QQ_PROVINCE, NULL, 0);
-	append_field_value(info_text, info->city, QQ_CITY, NULL, 0);
+	gaim_notify_user_info_add_pair(user_info, QQ_NUMBER, info->uid);
+
+	append_field_value(user_info, info->nick, QQ_NICKNAME, NULL, 0);
+	append_field_value(user_info, info->name, QQ_NAME, NULL, 0);
+	append_field_value(user_info, info->age, QQ_AGE, NULL, 0);
+	append_field_value(user_info, info->gender, QQ_GENDER, genders, QQ_GENDER_SIZE);
+	append_field_value(user_info, info->country, QQ_COUNTRY, NULL, 0);
+	append_field_value(user_info, info->province, QQ_PROVINCE, NULL, 0);
+	append_field_value(user_info, info->city, QQ_CITY, NULL, 0);
 
-	extra_info = g_string_new("");
-	g_string_append_printf(extra_info, "<br /><br /><b>%s</b><br />", QQ_ADDITIONAL_INFORMATION);
-	len = extra_info->len;
-	append_field_value(extra_info, info->horoscope, QQ_HOROSCOPE, horoscope_names, QQ_HOROSCOPE_SIZE);
-	append_field_value(extra_info, info->occupation, QQ_OCCUPATION, NULL, 0);
-	append_field_value(extra_info, info->zodiac, QQ_ZODIAC, zodiac_names, QQ_ZODIAC_SIZE);
-	append_field_value(extra_info, info->blood, QQ_BLOOD, blood_types, QQ_BLOOD_SIZE);
-	append_field_value(extra_info, info->college, QQ_COLLEGE, NULL, 0);
-	append_field_value(extra_info, info->email, QQ_EMAIL, NULL, 0);
-	append_field_value(extra_info, info->address, QQ_ADDRESS, NULL, 0);
-	append_field_value(extra_info, info->zipcode, QQ_ZIPCODE, NULL, 0);
-	append_field_value(extra_info, info->hp_num, QQ_CELL, NULL, 0);
-	append_field_value(extra_info, info->tel, QQ_TELEPHONE, NULL, 0);
-	append_field_value(extra_info, info->homepage, QQ_HOMEPAGE, NULL, 0);
-	if (len != extra_info->len)
-		g_string_append(info_text, extra_info->str);
-	g_string_free(extra_info, TRUE);
+	gaim_notify_user_info_add_section_header(user_info, QQ_ADDITIONAL_INFORMATION);
+
+	has_extra_info |= append_field_value(user_info, info->horoscope, QQ_HOROSCOPE, horoscope_names, QQ_HOROSCOPE_SIZE);
+	has_extra_info |= append_field_value(user_info, info->occupation, QQ_OCCUPATION, NULL, 0);
+	has_extra_info |= append_field_value(user_info, info->zodiac, QQ_ZODIAC, zodiac_names, QQ_ZODIAC_SIZE);
+	has_extra_info |= append_field_value(user_info, info->blood, QQ_BLOOD, blood_types, QQ_BLOOD_SIZE);
+	has_extra_info |= append_field_value(user_info, info->college, QQ_COLLEGE, NULL, 0);
+	has_extra_info |= append_field_value(user_info, info->email, QQ_EMAIL, NULL, 0);
+	has_extra_info |= append_field_value(user_info, info->address, QQ_ADDRESS, NULL, 0);
+	has_extra_info |= append_field_value(user_info, info->zipcode, QQ_ZIPCODE, NULL, 0);
+	has_extra_info |= append_field_value(user_info, info->hp_num, QQ_CELL, NULL, 0);
+	has_extra_info |= append_field_value(user_info, info->tel, QQ_TELEPHONE, NULL, 0);
+	has_extra_info |= append_field_value(user_info, info->homepage, QQ_HOMEPAGE, NULL, 0);
+
+	if (!has_extra_info)
+		gaim_notify_user_info_remove_last_item(user_info);
 
 	intro = field_value(info->intro, NULL, 0);
 	if (intro) {
-		g_string_append_printf(info_text, "<br /><br /><b>%s</b><br /><br />", QQ_INTRO);
-		g_string_append(info_text, intro);
+		gaim_notify_user_info_add_pair(user_info, QQ_INTRO, intro);
 	}
 
 	/* for debugging */
@@ -227,7 +229,7 @@
 	append_field_value(info_text, info->unknown6, "unknown6", NULL, 0);
 	*/
 
-	return info_text;
+	return user_info;
 }
 
 /* send a packet to get detailed information of uid */
@@ -658,7 +660,7 @@
 	qq_data *qd;
 	contact_info *info;
 	GList *list, *query_list;
-	GString *info_text;
+	GaimNotifyUserInfo *user_info;
 
 	g_return_if_fail(buf != NULL && buf_len != 0);
 
@@ -689,9 +691,9 @@
 			query = (qq_info_query *) query_list->data;
 			if (query->uid == atoi(info->uid)) {
 				if (query->show_window) {
-					info_text = info_to_str(info);
-					gaim_notify_userinfo(gc, info->uid, info_text->str, NULL, NULL);
-					g_string_free(info_text, TRUE);
+					user_info = info_to_notify_user_info(info);
+					gaim_notify_userinfo(gc, info->uid, user_info, NULL, NULL);
+					gaim_notify_user_info_destroy(user_info);
 				} else if (query->modify_info) {
 					create_modify_info_dialogue(gc, info);
 				}
--- a/libgaim/protocols/qq/qq.c	Sat Dec 09 20:20:17 2006 +0000
+++ b/libgaim/protocols/qq/qq.c	Sun Dec 10 02:53:09 2006 +0000
@@ -191,10 +191,11 @@
 
 
 /* a floating text when mouse is on the icon, show connection status here */
-static void _qq_tooltip_text(GaimBuddy *b, GString *tooltip, gboolean full)
+static void _qq_tooltip_text(GaimBuddy *b, GaimNotifyUserInfo *user_info, gboolean full)
 {
 	qq_buddy *q_bud;
 	gchar *ip_str;
+	char *tmp, *tmp2;
 
 	g_return_if_fail(b != NULL);
 
@@ -205,27 +206,40 @@
 	{
 		ip_str = gen_ip_str(q_bud->ip);
 		if (strlen(ip_str) != 0) {
-			g_string_append_printf(tooltip, _("\n<b>%s Address:</b> %s:%d"),
-					(q_bud->comm_flag & QQ_COMM_FLAG_TCP_MODE)
-				       ? "TCP" : "UDP", ip_str, q_bud->port);
+			tmp = g_strdup_printf(_("%s Address"),
+						  ((q_bud->comm_flag & QQ_COMM_FLAG_TCP_MODE) ? "TCP" : "UDP"));
+			tmp2 = g_strdup_printf("%s:%d", ip_str, q_bud->port);
+			gaim_notify_user_info_add_pair(user_info, tmp, tmp2);
+			g_free(tmp2);
+			g_free(tmp);
 		}
 		g_free(ip_str);
-		g_string_append_printf(tooltip, _("\n<b>Age:</b> %d"), q_bud->age);
+
+		tmp = g_strdup_printf("%d", q_bud->age);
+		gaim_notify_user_info_add_pair(user_info, _("Age"), tmp);
+		g_free(tmp);
+
 		switch (q_bud->gender) {
 		case QQ_BUDDY_GENDER_GG:
-			g_string_append(tooltip, _("\n<b>Gender:</b> Male"));
+			gaim_notify_user_info_add_pair(user_info, _("Gender"), _("Male"));
 			break;
 		case QQ_BUDDY_GENDER_MM:
-			g_string_append(tooltip, _("\n<b>Gender:</b> Female"));
+			gaim_notify_user_info_add_pair(user_info, _("Gender"), _("Female"));
 			break;
 		case QQ_BUDDY_GENDER_UNKNOWN:
-			g_string_append(tooltip, _("\n<b>Gender:</b> Unknown"));
+			gaim_notify_user_info_add_pair(user_info, _("Gender"), _("Unknown"));
 			break;
 		default:
-			g_string_append_printf(tooltip, _("\n<b>Gender:</b> ERROR(%d)"), q_bud->gender);
+			tmp = g_strdup_printf("Error (%d)", q_bud->gender);
+			gaim_notify_user_info_add_pair(user_info, _("Gender"), tmp);
+			g_free(tmp);
 		}
-		if (q_bud->level)
-			g_string_append_printf(tooltip, _("\n<b>Level:</b> %d"), q_bud->level);
+
+		if (q_bud->level) {
+			tmp = g_strdup_printf("%d", q_bud->level);
+			gaim_notify_user_info_add_pair(user_info, _("Level"), tmp);
+			g_free(tmp);			
+		}
 		/* For debugging */
 		/*
 		g_string_append_printf(tooltip, "\n<b>Flag:</b> %01x", q_bud->flag1);
--- a/libgaim/protocols/sametime/sametime.c	Sat Dec 09 20:20:17 2006 +0000
+++ b/libgaim/protocols/sametime/sametime.c	Sun Dec 10 02:53:09 2006 +0000
@@ -3250,7 +3250,7 @@
 }
 
 
-static void mw_prpl_tooltip_text(GaimBuddy *b, GString *str, gboolean full) {
+static void mw_prpl_tooltip_text(GaimBuddy *b, GaimNotifyUserInfo *user_info, gboolean full) {
   GaimConnection *gc;
   struct mwGaimPluginData *pd;
   struct mwAwareIdBlock idb = { mwAware_USER, b->name, NULL };
@@ -3267,22 +3267,22 @@
 
   if(message != NULL && gaim_utf8_strcasecmp(status, message)) {
     tmp = g_markup_escape_text(message, -1);
-    g_string_append_printf(str, _("\n<b>%s:</b> %s"), status, tmp);
+	gaim_notify_user_info_add_pair(user_info, status, tmp);
     g_free(tmp);
 
   } else {
-    g_string_append_printf(str, _("\n<b>Status:</b> %s"), status);
+	gaim_notify_user_info_add_pair(user_info, _("Status"), status);
   }
 
   if(full) {
     tmp = user_supports_text(pd->srvc_aware, b->name);
     if(tmp) {
-      g_string_append_printf(str, _("\n<b>Supports:</b> %s"), tmp);
+	  gaim_notify_user_info_add_pair(user_info, _("Supports"), tmp);
       g_free(tmp);
     }
 
     if(buddy_is_external(b)) {
-      g_string_append(str, _("\n<b>External User</b>"));
+	  gaim_notify_user_info_add_pair(user_info, NULL, _("External User"));
     }
   }
 }
@@ -4111,9 +4111,9 @@
   struct mwGaimPluginData *pd;
   GaimAccount *acct;
   GaimBuddy *b;
-  
-  GString *str;
-  const char *tmp;
+  GaimNotifyUserInfo *user_info;
+  char *tmp;
+  const char *tmp2;
 
   g_return_if_fail(who != NULL);
   g_return_if_fail(*who != '\0');
@@ -4122,64 +4122,57 @@
 
   acct = gaim_connection_get_account(gc);
   b = gaim_find_buddy(acct, who);
-
-  str = g_string_new(NULL);
+  user_info = gaim_notify_user_info_new();
 
   if(gaim_str_has_prefix(who, "@E ")) {
-    g_string_append(str, _("<b>External User</b><br>"));
+	gaim_notify_user_info_add_pair(user_info, _("External User"), NULL);
   }
 
-  g_string_append_printf(str, _("<b>User ID:</b> %s<br>"), who);
+  gaim_notify_user_info_add_pair(user_info, _("User ID"), who);
 
   if(b) {
     guint32 type;
 
     if(b->server_alias) {
-      g_string_append_printf(str, _("<b>Full Name:</b> %s<br>"),
-			     b->server_alias);
+		gaim_notify_user_info_add_pair(user_info, _("Full Name"), b->server_alias);
     }
 
     type = gaim_blist_node_get_int((GaimBlistNode *) b, BUDDY_KEY_CLIENT);
     if(type) {
-      g_string_append(str, _("<b>Last Known Client:</b> "));
-
-      tmp = mw_client_name(type);
-      if(tmp) {
-	g_string_append(str, tmp);
-	g_string_append(str, "<br>");
-	
-      } else {
-	g_string_append_printf(str, _("Unknown (0x%04x)<br>"), type);
-      }
+	  tmp = g_strdup(mw_client_name(type));
+	  if (!tmp)
+		tmp = g_strdup_printf(_("Unknown (0x%04x)<br>"), type);
+
+	  gaim_notify_user_info_add_pair(user_info, _("Last Known Client"), tmp);
+		
+	  g_free(tmp);
     }
   }
-
+  
   tmp = user_supports_text(pd->srvc_aware, who);
   if(tmp) {
-    g_string_append_printf(str, _("<b>Supports:</b> %s<br>"), tmp);
-    g_free((char *) tmp);
+	gaim_notify_user_info_add_pair(user_info, _("Supports"), tmp);
+	g_free(tmp);
   }
 
   if(b) {
-    tmp = status_text(b);
-    g_string_append_printf(str, _("<b>Status:</b> %s"), tmp);
-
-    g_string_append(str, "<hr>");
-    
-    tmp = mwServiceAware_getText(pd->srvc_aware, &idb);
-    if(tmp) {
-      tmp = g_markup_escape_text(tmp, -1);
-      g_string_append(str, tmp);
-      g_free((char *) tmp);
+	gaim_notify_user_info_add_pair(user_info, _("Status"), status_text(b));
+
+	/* XXX Is this adding a status message in its own section rather than with the "Status" label? */
+    tmp2 = mwServiceAware_getText(pd->srvc_aware, &idb);
+    if(tmp2) {
+      tmp = g_markup_escape_text(tmp2, -1);
+	  gaim_notify_user_info_add_section_break(user_info);
+	  gaim_notify_user_info_add_pair(user_info, NULL, tmp);
+      g_free(tmp);
     }
   }
 
   /* @todo emit a signal to allow a plugin to override the display of
      this notification, so that it can create its own */
 
-  gaim_notify_userinfo(gc, who, str->str, NULL, NULL);
-
-  g_string_free(str, TRUE);
+  gaim_notify_userinfo(gc, who, user_info, NULL, NULL);
+  gaim_notify_user_info_destroy(user_info);
 }
  
  
--- a/libgaim/protocols/yahoo/yahoo.c	Sat Dec 09 20:20:17 2006 +0000
+++ b/libgaim/protocols/yahoo/yahoo.c	Sun Dec 10 02:53:09 2006 +0000
@@ -2983,7 +2983,7 @@
 	}
 }
 
-void yahoo_tooltip_text(GaimBuddy *b, GString *str, gboolean full)
+void yahoo_tooltip_text(GaimBuddy *b, GaimNotifyUserInfo *user_info, gboolean full)
 {
 	YahooFriend *f;
 	char *escaped;
@@ -3024,14 +3024,13 @@
 
 	if (status != NULL) {
 		escaped = g_markup_escape_text(status, strlen(status));
-		g_string_append_printf(str, _("\n<b>%s:</b> %s"), _("Status"), escaped);
+		gaim_notify_user_info_add_pair(user_info, _("Status"), escaped);
 		g_free(status);
 		g_free(escaped);
 	}
 
 	if (presence != NULL)
-		g_string_append_printf(str, _("\n<b>%s:</b> %s"),
-				_("Presence"), presence);
+		gaim_notify_user_info_add_pair(user_info, _("Presence"), presence);
 }
 
 static void yahoo_addbuddyfrommenu_cb(GaimBlistNode *node, gpointer data)
--- a/libgaim/protocols/yahoo/yahoo.h	Sat Dec 09 20:20:17 2006 +0000
+++ b/libgaim/protocols/yahoo/yahoo.h	Sun Dec 10 02:53:09 2006 +0000
@@ -199,7 +199,7 @@
 char *yahoo_string_decode(GaimConnection *gc, const char *str, gboolean utf8);
 
 /* previously-static functions, now needed for yahoo_profile.c */
-void yahoo_tooltip_text(GaimBuddy *b, GString *str, gboolean full);
+void yahoo_tooltip_text(GaimBuddy *b, GaimNotifyUserInfo *user_info, gboolean full);
 
 /* yahoo_profile.c */
 void yahoo_get_info(GaimConnection *gc, const char *name);
--- a/libgaim/protocols/yahoo/yahoo_profile.c	Sat Dec 09 20:20:17 2006 +0000
+++ b/libgaim/protocols/yahoo/yahoo_profile.c	Sun Dec 10 02:53:09 2006 +0000
@@ -89,11 +89,10 @@
 
 typedef struct {
 	YahooGetInfoData *info_data;
+	GaimNotifyUserInfo *user_info;
 	char *url_buffer;
-	GString *s;
 	char *photo_url_text;
 	char *profile_url_text;
-	char *tooltip_text;
 	const profile_strings_node_t *strings;
 	const char *last_updated_string;
 	const char *title;
@@ -668,8 +667,7 @@
 	return str;
 }
 
-static char *yahoo_tooltip_info_text(YahooGetInfoData *info_data) {
-	GString *s = g_string_sized_new(80); /* wild guess */
+static void yahoo_extract_user_info_text(GaimNotifyUserInfo *user_info, YahooGetInfoData *info_data) {
 	GaimBuddy *b;
 	YahooFriend *f;
 
@@ -677,37 +675,28 @@
 			info_data->name);
 
 	if (b) {
-		GString *str = g_string_new("");
-		char *tmp;
-
 		if(b->alias && b->alias[0]) {
 			char *aliastext = g_markup_escape_text(b->alias, -1);
-			g_string_append_printf(s, _("<b>Alias:</b> %s<br>"), aliastext);
+			gaim_notify_user_info_add_pair(user_info, _("Alias"), aliastext); 
 			g_free(aliastext);
 		}
 		#if 0
 		if (b->idle > 0) {
 			char *idletime = gaim_str_seconds_to_string(time(NULL) - b->idle);
-			g_string_append_printf(s, _("<b>%s:</b> %s<br>"), _("Idle"),
-					idletime);
+			gaim_notify_user_info_add_pair(user_info, _("Idle"), idletime);
 			g_free(idletime);
 		}
 		#endif
 
-		yahoo_tooltip_text(b, str, TRUE);
-		tmp = gaim_strreplace((*str->str == '\n' ? str->str + 1 : str->str),
-							  "\n", "<br>");
-		g_string_free(str, TRUE);
-		g_string_append_printf(s, "%s<br>", tmp);
-		g_free(tmp);
+		/* Add the normal tooltip pairs */
+		yahoo_tooltip_text(b, user_info, TRUE);
 
 		if ((f = yahoo_friend_find(info_data->gc, b->name))) {
 			const char *ip;
 			if ((ip = yahoo_friend_get_ip(f)))
-				g_string_append_printf(s, _("<b>IP Address:</b> %s<br>"), ip);
+				gaim_notify_user_info_add_pair(user_info, _("IP Address"), ip);
 		}
 	}
-	return g_string_free(s, FALSE);
 }
 
 #if PHOTO_SUPPORT
@@ -752,6 +741,7 @@
 		const gchar *url_text, size_t len, const gchar *error_message)
 {
 	YahooGetInfoData *info_data = (YahooGetInfoData *)user_data;
+	GaimNotifyUserInfo *user_info;
 	char *p;
 	char buf[1024];
 #if PHOTO_SUPPORT
@@ -766,7 +756,7 @@
 	const char *last_updated_string = NULL;
 	char *url_buffer;
 	GString *s;
-	char *tooltip_text = NULL;
+	char *tmp;
 	char *profile_url_text = NULL;
 	int lang, strid;
 	struct yahoo_data *yd;
@@ -779,25 +769,24 @@
 	yd = info_data->gc->proto_data;
 	yd->url_datas = g_slist_remove(yd->url_datas, url_data);
 
+	user_info = gaim_notify_user_info_new();
+
 	title = yd->jp ? _("Yahoo! Japan Profile") :
 					 _("Yahoo! Profile");
 
 	/* Get the tooltip info string */
-	tooltip_text = yahoo_tooltip_info_text(info_data);
+	yahoo_extract_user_info_text(user_info, info_data);
 
 	/* We failed to grab the profile URL.  This is not expected to actually
 	 * happen except under unusual error conditions, as Yahoo is observed
 	 * to send back HTML, with a 200 status code.
 	 */
 	if (error_message != NULL || url_text == NULL || strcmp(url_text, "") == 0) {
-		g_snprintf(buf, 1024, "<html><body>%s<b>%s</b></body></html>",
-				tooltip_text, _("Error retrieving profile"));
-
+		gaim_notify_user_info_add_pair(user_info, _("Error retrieving profile"), NULL);
 		gaim_notify_userinfo(info_data->gc, info_data->name, 
-			buf, NULL, NULL);
-
+			user_info, NULL, NULL);
+		gaim_notify_user_info_destroy(user_info);
 		g_free(profile_url_text);
-		g_free(tooltip_text);
 		g_free(info_data->name);
 		g_free(info_data);
 		return;
@@ -821,20 +810,21 @@
 		p = strstr(url_text, "Adult Content Warning"); /* TITLE element */
 	}
 	if (p) {
-		g_snprintf(buf, 1024, "<html><body>%s<b>%s</b><br><br>"
-					"%s<br><a href=\"%s\">%s</a></body></html>",
-				tooltip_text,
-				_("Sorry, profiles marked as containing adult content "
-				  "are not supported at this time."),
-				_("If you wish to view this profile, "
-				  "you will need to visit this link in your web browser"),
-				profile_url_text, profile_url_text);
+		tmp = g_strdup_printf("<b>%s</b><br><br>"
+							  "%s<br><a href=\"%s\">%s</a>",
+						_("Sorry, profiles marked as containing adult content "
+						"are not supported at this time."),
+						 _("If you wish to view this profile, "
+						"you will need to visit this link in your web browser:"),
+						 profile_url_text, profile_url_text);
+		gaim_notify_user_info_add_pair(user_info, NULL, tmp);		
+		g_free(tmp);
 
 		gaim_notify_userinfo(info_data->gc, info_data->name, 
-				buf, NULL, NULL);
+				user_info, NULL, NULL);
 
 		g_free(profile_url_text);
-		g_free(tooltip_text);
+		gaim_notify_user_info_destroy(user_info);
 		g_free(info_data->name);
 		g_free(info_data);
 		return;
@@ -908,14 +898,13 @@
 	info2_data = g_malloc(sizeof(YahooGetInfoStepTwoData));
 	info2_data->info_data = info_data;
 	info2_data->url_buffer = url_buffer;
-	info2_data->s = s;
 	info2_data->photo_url_text = photo_url_text;
 	info2_data->profile_url_text = profile_url_text;
-	info2_data->tooltip_text = tooltip_text;
 	info2_data->strings = strings;
 	info2_data->last_updated_string = last_updated_string;
 	info2_data->title = title;
 	info2_data->profile_state = profile_state;
+	info2_data->user_info = user_info;
 
 	/* Try to put the photo in there too, if there's one */
 	if (photo_url_text) {
@@ -953,14 +942,14 @@
 	char *stripped;
 	int stripped_len;
 	char *last_updated_utf8_string = NULL;
+	char *tmp;
 
 	/* Unmarshall the saved state */
 	YahooGetInfoData *info_data = info2_data->info_data;
 	char *url_buffer = info2_data->url_buffer;
-	GString *s = info2_data->s;
+	GaimNotifyUserInfo *user_info = info2_data->user_info;
 	char *photo_url_text = info2_data->photo_url_text;
 	char *profile_url_text = info2_data->profile_url_text;
-	char *tooltip_text = info2_data->tooltip_text;
 	const profile_strings_node_t *strings = info2_data->strings;
 	const char *last_updated_string = info2_data->last_updated_string;
 	profile_state_t profile_state = info2_data->profile_state;
@@ -1013,15 +1002,11 @@
 		gaim_debug_misc("yahoo", "after utf8 conversion: stripped = (%s)\n", stripped);
 	}
 
-	/* gonna re-use the memory we've already got for url_buffer */
-	/* no we're not */
-	s = g_string_sized_new(strlen(url_buffer));
-
 	if (profile_state == PROFILE_STATE_DEFAULT) {
 #if 0
 	/* extract their Yahoo! ID and put it in. Don't bother marking has_info as
 	 * true, since the Yahoo! ID will always be there */
-	if (!gaim_markup_extract_info_field(stripped, stripped_len, s,
+	if (!gaim_markup_extract_info_field(stripped, stripped_len, user_info,
 			strings->yahoo_id_string, 10, "\n", 0,
 			NULL, _("Yahoo! ID"), 0, NULL, NULL))
 		;
@@ -1039,48 +1024,51 @@
 		} else {
 			gaim_debug_info("yahoo", "%s is %d bytes\n", photo_url_text, len);
 			id = gaim_imgstore_add(url_text, len, NULL);
-			g_string_append_printf(s, "<img id=\"%d\"><br>", id);
+			
+			tmp = g_strdup_printf("<img id=\"%d\"><br>", id);
+			gaim_notify_user_info_add_pair(user_info, NULL, tmp);
+			g_free(tmp);
 		}
 	}
 #endif /* PHOTO_SUPPORT */
 
 	/* extract their Email address and put it in */
-	found |= gaim_markup_extract_info_field(stripped, stripped_len, s,
+	found |= gaim_markup_extract_info_field(stripped, stripped_len, user_info,
 			strings->my_email_string, 1, " ", 0,
 			strings->private_string, _("E-Mail"), 0, NULL, NULL);
 
 	/* extract the Nickname if it exists */
-	found |= gaim_markup_extract_info_field(stripped, stripped_len, s,
+	found |= gaim_markup_extract_info_field(stripped, stripped_len, user_info,
 			"Nickname:", 1, "\n", '\n',
 			NULL, _("Nickname"), 0, NULL, NULL);
 
 	/* extract their RealName and put it in */
-	found |= gaim_markup_extract_info_field(stripped, stripped_len, s,
+	found |= gaim_markup_extract_info_field(stripped, stripped_len, user_info,
 			strings->realname_string, 1, "\n", '\n',
 			NULL, _("Real Name"), 0, NULL, NULL);
 
 	/* extract their Location and put it in */
-	found |= gaim_markup_extract_info_field(stripped, stripped_len, s,
+	found |= gaim_markup_extract_info_field(stripped, stripped_len, user_info,
 			strings->location_string, 2, "\n", '\n',
 			NULL, _("Location"), 0, NULL, NULL);
 
 	/* extract their Age and put it in */
-	found |= gaim_markup_extract_info_field(stripped, stripped_len, s,
+	found |= gaim_markup_extract_info_field(stripped, stripped_len, user_info,
 			strings->age_string, 3, "\n", '\n',
 			NULL, _("Age"), 0, NULL, NULL);
 
 	/* extract their MaritalStatus and put it in */
-	found |= gaim_markup_extract_info_field(stripped, stripped_len, s,
+	found |= gaim_markup_extract_info_field(stripped, stripped_len, user_info,
 			strings->maritalstatus_string, 3, "\n", '\n',
 			strings->no_answer_string, _("Marital Status"), 0, NULL, NULL);
 
 	/* extract their Gender and put it in */
-	found |= gaim_markup_extract_info_field(stripped, stripped_len, s,
+	found |= gaim_markup_extract_info_field(stripped, stripped_len, user_info,
 			strings->gender_string, 3, "\n", '\n',
 			strings->no_answer_string, _("Gender"), 0, NULL, NULL);
 
 	/* extract their Occupation and put it in */
-	found |= gaim_markup_extract_info_field(stripped, stripped_len, s,
+	found |= gaim_markup_extract_info_field(stripped, stripped_len, user_info,
 			strings->occupation_string, 2, "\n", '\n',
 			NULL, _("Occupation"), 0, NULL, NULL);
 
@@ -1093,15 +1081,15 @@
 	 * the "Description" ("Self PR") heading instead of "Links".)
 	 */
 
-	if (!gaim_markup_extract_info_field(stripped, stripped_len, s,
+	if (!gaim_markup_extract_info_field(stripped, stripped_len, user_info,
 			strings->hobbies_string, 1, strings->latest_news_string,
 			'\n', "\n", _("Hobbies"), 0, NULL, NULL))
 	{
-		if (!gaim_markup_extract_info_field(stripped, stripped_len, s,
+		if (!gaim_markup_extract_info_field(stripped, stripped_len, user_info,
 				strings->hobbies_string, 1, strings->favorite_quote_string,
 				'\n', "\n", _("Hobbies"), 0, NULL, NULL))
 		{
-			found |= gaim_markup_extract_info_field(stripped, stripped_len, s,
+			found |= gaim_markup_extract_info_field(stripped, stripped_len, user_info,
 					strings->hobbies_string, 1, strings->links_string,
 					'\n', "\n", _("Hobbies"), 0, NULL, NULL);
 		}
@@ -1111,18 +1099,18 @@
 	else
 		found = TRUE;
 
-	if (!gaim_markup_extract_info_field(stripped, stripped_len, s,
+	if (!gaim_markup_extract_info_field(stripped, stripped_len, user_info,
 			strings->latest_news_string, 1, strings->favorite_quote_string,
 			'\n', "\n", _("Latest News"), 0, NULL, NULL))
 	{
-		found |= gaim_markup_extract_info_field(stripped, stripped_len, s,
+		found |= gaim_markup_extract_info_field(stripped, stripped_len, user_info,
 				strings->latest_news_string, 1, strings->links_string,
 				'\n', "\n", _("Latest News"), 0, NULL, NULL);
 	}
 	else
 		found = TRUE;
 
-	found |= gaim_markup_extract_info_field(stripped, stripped_len, s,
+	found |= gaim_markup_extract_info_field(stripped, stripped_len, user_info,
 			strings->favorite_quote_string, 1, strings->links_string,
 			'\n', "\n", _("Favorite Quote"), 0, NULL, NULL);
 
@@ -1136,7 +1124,7 @@
 			strstr(stripped, strings->no_home_page_specified_string);
 		if(!p)
 		{
-			found |= gaim_markup_extract_info_field(stripped, stripped_len, s,
+			found |= gaim_markup_extract_info_field(stripped, stripped_len, user_info,
 					strings->home_page_string, 1, "\n", 0, NULL,
 					_("Home Page"), 1, NULL, NULL);
 		}
@@ -1151,16 +1139,16 @@
 		strstr(stripped,strings->no_cool_link_specified_string);
 	if (!p)
 	{
-		if (gaim_markup_extract_info_field(stripped, stripped_len, s,
+		if (gaim_markup_extract_info_field(stripped, stripped_len, user_info,
 				strings->cool_link_1_string, 1, "\n", 0, NULL,
 				_("Cool Link 1"), 1, NULL, NULL))
 		{
 			found = TRUE;
-			if (gaim_markup_extract_info_field(stripped, stripped_len, s,
+			if (gaim_markup_extract_info_field(stripped, stripped_len, user_info,
 					strings->cool_link_2_string, 1, "\n", 0, NULL,
 					_("Cool Link 2"), 1, NULL, NULL))
 			{
-				gaim_markup_extract_info_field(stripped, stripped_len, s,
+				gaim_markup_extract_info_field(stripped, stripped_len, user_info,
 						strings->cool_link_3_string, 1, "\n", 0, NULL,
 						_("Cool Link 3"), 1, NULL, NULL);
 			}
@@ -1169,12 +1157,12 @@
 
 	if (last_updated_utf8_string != NULL) {
 		/* see if Member Since is there, and if so, extract it. */
-		found |= gaim_markup_extract_info_field(stripped, stripped_len, s,
+		found |= gaim_markup_extract_info_field(stripped, stripped_len, user_info,
 				"Member Since:", 1, last_updated_utf8_string,
 				'\n', NULL, _("Member Since"), 0, NULL, yahoo_info_date_reformat);
 
 		/* extract the Last Updated date and put it in */
-		found |= gaim_markup_extract_info_field(stripped, stripped_len, s,
+		found |= gaim_markup_extract_info_field(stripped, stripped_len, user_info,
 				last_updated_utf8_string, 1, " ", '\n', NULL,
 				_("Last Update"), 0, NULL, yahoo_info_date_reformat);
 	}
@@ -1182,13 +1170,15 @@
 
 	if(!found)
 	{
-		g_string_append_printf(s, "<br><b>");
-		g_string_append_printf(s, _("User information for %s unavailable"),
+		GString *str = g_string_new("");
+
+		g_string_append_printf(str, "<br><b>");
+		g_string_append_printf(str, _("User information for %s unavailable"),
 				info_data->name);
-		g_string_append_printf(s, "</b><br>");
+		g_string_append_printf(str, "</b><br>");
 
 		if (profile_state == PROFILE_STATE_UNKNOWN_LANGUAGE) {
-			g_string_append_printf(s, "%s<br><br>",
+			g_string_append_printf(str, "%s<br><br>",
 					_("Sorry, this profile seems to be in a language "
 					  "or format that is not supported at this time."));
 
@@ -1204,7 +1194,7 @@
 				 */
 				f = yahoo_friend_find(b->account->gc, b->name);
 			}
-			g_string_append_printf(s, "%s<br><br>",
+			g_string_append_printf(str, "%s<br><br>",
 				f?  _("Could not retrieve the user's profile. "
 					  "This most likely is a temporary server-side problem. "
 					  "Please try again later."):
@@ -1214,36 +1204,30 @@
 					  "profile. If you know that the user exists, "
 					  "please try again later."));
 		} else {
-			g_string_append_printf(s, "%s<br><br>",
+			g_string_append_printf(str, "%s<br><br>",
 					_("The user's profile is empty."));
 		}
+		
+		gaim_notify_user_info_add_pair(user_info, NULL, str->str);
+		g_string_free(str, TRUE);
 	}
 
 	/* put a link to the actual profile URL */
-	g_string_append_printf(s, _("<b>%s:</b> "), _("Profile URL"));
-	g_string_append_printf(s, "<br><a href=\"%s\">%s</a><br>",
-			profile_url_text, profile_url_text);
+	tmp = g_strdup_printf("<a href=\"%s\">%s</a>", profile_url_text, profile_url_text);
+	gaim_notify_user_info_add_pair(user_info, _("Profile URL"), tmp);
+	g_free(tmp);
 
-	/* finish off the html at the end */
-	g_string_append(s, "</body></html>");
 	g_free(stripped);
 
-	/* Put the Yahoo! ID, nickname, idle time, and status message in */
-	g_string_prepend(s, tooltip_text);
-
-	/* finish off the html at the beginning */
-	g_string_prepend(s, "<html><body>");
-
 	/* show it to the user */
 	gaim_notify_userinfo(info_data->gc, info_data->name,
-						  s->str, NULL, NULL);
+						  user_info, NULL, NULL);
+	gaim_notify_user_info_destroy(user_info);
 
 	g_free(last_updated_utf8_string);
 	g_free(url_buffer);
 	g_free(fudged_buffer);
-	g_string_free(s, TRUE);
 	g_free(profile_url_text);
-	g_free(tooltip_text);
 	g_free(info_data->name);
 	g_free(info_data);
 
--- a/libgaim/protocols/zephyr/zephyr.c	Sat Dec 09 20:20:17 2006 +0000
+++ b/libgaim/protocols/zephyr/zephyr.c	Sun Dec 10 02:53:09 2006 +0000
@@ -781,22 +781,27 @@
 			if ((b && pending_zloc(zephyr,b->name)) || pending_zloc(zephyr,user)) {
 				ZLocations_t locs;
 				int one = 1;
-				GString *str = g_string_new("");
+				GaimNotifyUserInfo *user_info = gaim_notify_user_info_new();
+				char *tmp;
 
-				g_string_append_printf(str, _("<b>User:</b> %s<br>"), b ? b->name : user);
+				gaim_notify_user_info_add_pair(user_info, _("User"), (b ? b->name : user));
 				if (b && b->alias)
-					g_string_append_printf(str, _("<b>Alias:</b> %s<br>"), b->alias);
+					gaim_notify_user_info_add_pair(user_info, _("Alias"), b->alias);
+
 				if (!nlocs) {
-					g_string_append_printf(str, _("<br>Hidden or not logged-in"));
+					gaim_notify_user_info_add_pair(user_info, NULL, _("Hidden or not logged-in"));
 				}
 				for (; nlocs > 0; nlocs--) {
 					/* XXX add real error reporting */
+					
 					ZGetLocations(&locs, &one);
-					g_string_append_printf(str, _("<br>At %s since %s"), locs.host, locs.time);
+					tmp = g_strdup_printf(_("<br>At %s since %s"), locs.host, locs.time);	
+					gaim_notify_user_info_add_pair(user_info, _("Location"), tmp);
+					g_free(tmp);
 				}
-				gaim_notify_userinfo(gc, b ? b->name : user, 
-						     str->str, NULL, NULL);
-				g_string_free(str, TRUE);
+				gaim_notify_userinfo(gc, (b ? b->name : user), 
+						     user_info, NULL, NULL);
+				gaim_notify_user_info_destroy(user_info);
 			} else {
 				if (nlocs>0) 
 					gaim_prpl_got_user_status(gc->account, b ? b->name : user, "available", NULL);
@@ -1198,23 +1203,27 @@
 				}
 	
 				if ((b && pending_zloc(zephyr,b->name)) || pending_zloc(zephyr,user) || pending_zloc(zephyr,local_zephyr_normalize(zephyr,user))){
-					GString *str = g_string_new("");
+					GaimNotifyUserInfo *user_info = gaim_notify_user_info_new();
+					char *tmp;
 
-					g_string_append_printf(str, _("<b>User:</b> %s<br>"), b ? b->name : user);
+					gaim_notify_user_info_add_pair(user_info, _("User"), (b ? b->name : user));
+
 					if (b && b->alias)
-						g_string_append_printf(str, _("<b>Alias:</b> %s<br>"), b->alias);
+						gaim_notify_user_info_add_pair(user_info, _("Alias"), b->alias);
 											
 					if (!nlocs) {
-						g_string_append_printf(str, _("<br>Hidden or not logged-in"));
+						gaim_notify_user_info_add_pair(user_info, NULL, _("Hidden or not logged-in"));
 					} else {
-						g_string_append_printf(str, _("<br>At %s since %s"), 
-								       tree_child(tree_child(tree_child(tree_child(locations,2),0),0),2)->contents, 
-								       tree_child(tree_child(tree_child(tree_child(locations,2),0),2),2)->contents);
+						tmp = g_strdup_printf(_("<br>At %s since %s"),
+									  tree_child(tree_child(tree_child(tree_child(locations,2),0),0),2)->contents,
+									  tree_child(tree_child(tree_child(tree_child(locations,2),0),2),2)->contents);
+						gaim_notify_user_info_add_pair(user_info, _("Location"), tmp);
+						g_free(tmp);
 					}
 
 					gaim_notify_userinfo(gc, b ? b->name : user,
-							     str->str, NULL, NULL);
-					g_string_free(str, TRUE);
+							     user_info, NULL, NULL);
+					gaim_notify_user_info_destroy(user_info);
 				} else {
 					if (nlocs>0) 
 						gaim_prpl_got_user_status(gc->account, b ? b->name : user, "available", NULL);
--- a/libgaim/prpl.h	Sat Dec 09 20:20:17 2006 +0000
+++ b/libgaim/prpl.h	Sun Dec 10 02:53:09 2006 +0000
@@ -66,6 +66,7 @@
 #include "blist.h"
 #include "conversation.h"
 #include "ft.h"
+#include "notify.h"
 #include "proxy.h"
 #include "plugin.h"
 #include "roomlist.h"
@@ -190,7 +191,7 @@
 	/**
 	 * Allows the prpl to add text to a buddy's tooltip.
 	 */
-	void (*tooltip_text)(GaimBuddy *buddy, GString *str, gboolean full);
+	void (*tooltip_text)(GaimBuddy *buddy, GaimNotifyUserInfo *user_info, gboolean full);
 
 	/**
 	 * This must be implemented, and must add at least the offline
--- a/libgaim/util.c	Sat Dec 09 20:20:17 2006 +0000
+++ b/libgaim/util.c	Sun Dec 10 02:53:09 2006 +0000
@@ -1061,7 +1061,7 @@
 }
 
 gboolean
-gaim_markup_extract_info_field(const char *str, int len, GString *dest,
+gaim_markup_extract_info_field(const char *str, int len, GaimNotifyUserInfo *user_info,
 							   const char *start_token, int skip,
 							   const char *end_token, char check_value,
 							   const char *no_value_token,
@@ -1072,7 +1072,7 @@
 	const char *p, *q;
 
 	g_return_val_if_fail(str          != NULL, FALSE);
-	g_return_val_if_fail(dest  != NULL, FALSE);
+	g_return_val_if_fail(user_info    != NULL, FALSE);
 	g_return_val_if_fail(start_token  != NULL, FALSE);
 	g_return_val_if_fail(end_token    != NULL, FALSE);
 	g_return_val_if_fail(display_name != NULL, FALSE);
@@ -1110,11 +1110,11 @@
 					  (no_value_token && strncmp(p, no_value_token,
 												 strlen(no_value_token)))))
 	{
-		g_string_append_printf(dest, _("<b>%s:</b> "), display_name);
+		GString *dest = g_string_new("");
 
 		if (is_link)
 		{
-			g_string_append(dest, "<br><a href=\"");
+			g_string_append(dest, "<a href=\"");
 
 			if (link_prefix)
 				g_string_append(dest, link_prefix);
@@ -1147,7 +1147,8 @@
 				g_string_append_len(dest, p, q - p);
 		}
 
-		g_string_append(dest, "<br>\n");
+		gaim_notify_user_info_add_pair(user_info, display_name, dest->str);
+		g_string_free(dest, TRUE);
 
 		return TRUE;
 	}
--- a/libgaim/util.h	Sat Dec 09 20:20:17 2006 +0000
+++ b/libgaim/util.h	Sun Dec 10 02:53:09 2006 +0000
@@ -365,8 +365,8 @@
  *
  * @param str            The string to parse.
  * @param len            The size of str.
- * @param dest           The destination GString to append the new
- *                       field info to.
+ * @param user_info      The destination GaimNotifyUserInfo to which the new
+ *                       field info should be added.
  * @param start_token    The beginning token.
  * @param skip           The number of characters to skip after the
  *                       start token.
@@ -380,7 +380,7 @@
  *
  * @return TRUE if successful, or FALSE otherwise.
  */
-gboolean gaim_markup_extract_info_field(const char *str, int len, GString *dest,
+gboolean gaim_markup_extract_info_field(const char *str, int len, GaimNotifyUserInfo *user_info,
                                         const char *start_token, int skip,
                                         const char *end_token, char check_value,
                                         const char *no_value_token,