]> git.0d.be Git - empathy.git/blobdiff - libempathy-gtk/empathy-theme-adium.c
individual_view_drag_end: remove the auto scroll
[empathy.git] / libempathy-gtk / empathy-theme-adium.c
index 0a505eb579a924c7a6ce3d85d353baafc771176c..98df46b79a9de43ee019f56544b9a46e0a461e33 100644 (file)
 #include "config.h"
 
 #include <string.h>
-#include <glib/gi18n.h>
+#include <glib/gi18n-lib.h>
 
 #include <webkit/webkit.h>
 #include <telepathy-glib/dbus.h>
 #include <telepathy-glib/util.h>
 
-#include <gconf/gconf-client.h>
 #include <pango/pango.h>
 #include <gdk/gdk.h>
 
+#include <libempathy/empathy-gsettings.h>
 #include <libempathy/empathy-time.h>
 #include <libempathy/empathy-utils.h>
 
 #include "empathy-theme-adium.h"
 #include "empathy-smiley-manager.h"
-#include "empathy-conf.h"
 #include "empathy-ui-utils.h"
 #include "empathy-plist.h"
+#include "empathy-images.h"
+#include "empathy-webkit-utils.h"
 
 #define DEBUG_FLAG EMPATHY_DEBUG_CHAT
 #include <libempathy/empathy-debug.h>
 
 #define GET_PRIV(obj) EMPATHY_GET_PRIV (obj, EmpathyThemeAdium)
 
-/* GConf key containing current value of font */
-#define EMPATHY_GCONF_FONT_KEY_NAME       "/desktop/gnome/interface/font_name"
-#define BORING_DPI_DEFAULT                96
+#define BORING_DPI_DEFAULT 96
 
 /* "Join" consecutive messages with timestamps within five minutes */
 #define MESSAGE_JOIN_PERIOD 5*60
@@ -57,12 +56,24 @@ typedef struct {
        EmpathyAdiumData     *data;
        EmpathySmileyManager *smiley_manager;
        EmpathyContact       *last_contact;
-       time_t                last_timestamp;
+       gint64                last_timestamp;
        gboolean              last_is_backlog;
-       gboolean              page_loaded;
-       GList                *message_queue;
-       guint                 notify_enable_webkit_developer_tools_id;
+       guint                 pages_loading;
+       /* Queue of QueuedItem*s containing an EmpathyMessage or string */
+       GQueue                message_queue;
+       /* Queue of guint32 of pending message id to remove unread
+        * marker for when we lose focus. */
+       GQueue                acked_messages;
        GtkWidget            *inspector_window;
+
+       GSettings            *gsettings_chat;
+       GSettings            *gsettings_desktop;
+
+       gboolean              has_focus;
+       gboolean              has_unread_message;
+       gboolean              allow_scrolling;
+       gchar                *variant;
+       gboolean              in_construction;
 } EmpathyThemeAdiumPriv;
 
 struct _EmpathyAdiumData {
@@ -72,33 +83,38 @@ struct _EmpathyAdiumData {
        gchar *default_avatar_filename;
        gchar *default_incoming_avatar_filename;
        gchar *default_outgoing_avatar_filename;
-       gchar *template_html;
-       gchar *in_content_html;
-       gsize  in_content_len;
-       gchar *in_context_html;
-       gsize  in_context_len;
-       gchar *in_nextcontent_html;
-       gsize  in_nextcontent_len;
-       gchar *in_nextcontext_html;
-       gsize  in_nextcontext_len;
-       gchar *out_content_html;
-       gsize  out_content_len;
-       gchar *out_context_html;
-       gsize  out_context_len;
-       gchar *out_nextcontent_html;
-       gsize  out_nextcontent_len;
-       gchar *out_nextcontext_html;
-       gsize  out_nextcontext_len;
-       gchar *status_html;
-       gsize  status_len;
        GHashTable *info;
+       guint version;
+       gboolean custom_template;
+       /* gchar* -> gchar* both owned */
+       GHashTable *date_format_cache;
+
+       /* HTML bits */
+       const gchar *template_html;
+       const gchar *content_html;
+       const gchar *in_content_html;
+       const gchar *in_context_html;
+       const gchar *in_nextcontent_html;
+       const gchar *in_nextcontext_html;
+       const gchar *out_content_html;
+       const gchar *out_context_html;
+       const gchar *out_nextcontent_html;
+       const gchar *out_nextcontext_html;
+       const gchar *status_html;
+
+       /* Above html strings are pointers to strings stored in this array.
+        * We do this because of fallbacks, some htmls could be pointing the
+        * same string. */
+       GPtrArray *strings_to_free;
 };
 
 static void theme_adium_iface_init (EmpathyChatViewIface *iface);
+static gchar * adium_info_dup_path_for_variant (GHashTable *info, const gchar *variant);
 
 enum {
        PROP_0,
        PROP_ADIUM_DATA,
+       PROP_VARIANT,
 };
 
 G_DEFINE_TYPE_WITH_CODE (EmpathyThemeAdium, empathy_theme_adium,
@@ -106,17 +122,58 @@ G_DEFINE_TYPE_WITH_CODE (EmpathyThemeAdium, empathy_theme_adium,
                         G_IMPLEMENT_INTERFACE (EMPATHY_TYPE_CHAT_VIEW,
                                                theme_adium_iface_init));
 
+enum {
+       QUEUED_EVENT,
+       QUEUED_MESSAGE,
+       QUEUED_EDIT
+};
+
+typedef struct {
+       guint type;
+       EmpathyMessage *msg;
+       char *str;
+       gboolean should_highlight;
+} QueuedItem;
+
+static QueuedItem *
+queue_item (GQueue *queue,
+           guint type,
+           EmpathyMessage *msg,
+           const char *str,
+           gboolean should_highlight)
+{
+       QueuedItem *item = g_slice_new0 (QueuedItem);
+
+       item->type = type;
+       if (msg != NULL)
+               item->msg = g_object_ref (msg);
+       item->str = g_strdup (str);
+       item->should_highlight = should_highlight;
+
+       g_queue_push_tail (queue, item);
+
+       return item;
+}
+
+static void
+free_queued_item (QueuedItem *item)
+{
+       tp_clear_object (&item->msg);
+       g_free (item->str);
+
+       g_slice_free (QueuedItem, item);
+}
+
 static void
 theme_adium_update_enable_webkit_developer_tools (EmpathyThemeAdium *theme)
 {
+       EmpathyThemeAdiumPriv *priv = GET_PRIV (theme);
        WebKitWebView  *web_view = WEBKIT_WEB_VIEW (theme);
        gboolean        enable_webkit_developer_tools;
 
-       if (!empathy_conf_get_bool (empathy_conf_get (),
-                                   EMPATHY_PREFS_CHAT_WEBKIT_DEVELOPER_TOOLS,
-                                   &enable_webkit_developer_tools)) {
-               return;
-       }
+       enable_webkit_developer_tools = g_settings_get_boolean (
+                       priv->gsettings_chat,
+                       EMPATHY_PREFS_CHAT_WEBKIT_DEVELOPER_TOOLS);
 
        g_object_set (G_OBJECT (webkit_web_view_get_settings (web_view)),
                      "enable-developer-extras",
@@ -125,7 +182,7 @@ theme_adium_update_enable_webkit_developer_tools (EmpathyThemeAdium *theme)
 }
 
 static void
-theme_adium_notify_enable_webkit_developer_tools_cb (EmpathyConf *conf,
+theme_adium_notify_enable_webkit_developer_tools_cb (GSettings   *gsettings,
                                                     const gchar *key,
                                                     gpointer     user_data)
 {
@@ -158,165 +215,101 @@ theme_adium_navigation_policy_decision_requested_cb (WebKitWebView             *
        return TRUE;
 }
 
-static void
-theme_adium_copy_address_cb (GtkMenuItem *menuitem,
-                            gpointer     user_data)
-{
-       WebKitHitTestResult   *hit_test_result = WEBKIT_HIT_TEST_RESULT (user_data);
-       gchar                 *uri;
-       GtkClipboard          *clipboard;
-
-       g_object_get (G_OBJECT (hit_test_result), "link-uri", &uri, NULL);
-
-       clipboard = gtk_clipboard_get (GDK_SELECTION_CLIPBOARD);
-       gtk_clipboard_set_text (clipboard, uri, -1);
-
-       clipboard = gtk_clipboard_get (GDK_SELECTION_PRIMARY);
-       gtk_clipboard_set_text (clipboard, uri, -1);
-
-       g_free (uri);
-}
-
-static void
-theme_adium_open_address_cb (GtkMenuItem *menuitem,
-                            gpointer     user_data)
+/* Replace each %@ in format with string passed in args */
+static gchar *
+string_with_format (const gchar *format,
+                   const gchar *first_string,
+                   ...)
 {
-       WebKitHitTestResult   *hit_test_result = WEBKIT_HIT_TEST_RESULT (user_data);
-       gchar                 *uri;
-
-       g_object_get (G_OBJECT (hit_test_result), "link-uri", &uri, NULL);
+       va_list args;
+       const gchar *str;
+       GString *result;
 
-       empathy_url_show (GTK_WIDGET (menuitem), uri);
+       va_start (args, first_string);
+       result = g_string_sized_new (strlen (format));
+       for (str = first_string; str != NULL; str = va_arg (args, const gchar *)) {
+               const gchar *next;
 
-       g_free (uri);
-}
-
-static void
-theme_adium_match_newline (const gchar *text,
-                          gssize len,
-                          EmpathyStringReplace replace_func,
-                          EmpathyStringParser *sub_parsers,
-                          gpointer user_data)
-{
-       GString *string = user_data;
-       gint i;
-       gint prev = 0;
-
-       if (len < 0) {
-               len = G_MAXSSIZE;
-       }
-
-       /* Replace \n by <br/> */
-       for (i = 0; i < len && text[i] != '\0'; i++) {
-               if (text[i] == '\n') {
-                       empathy_string_parser_substr (text + prev,
-                                                     i - prev, sub_parsers,
-                                                     user_data);
-                       g_string_append (string, "<br/>");
-                       prev = i + 1;
+               next = strstr (format, "%@");
+               if (next == NULL) {
+                       break;
                }
-       }
-       empathy_string_parser_substr (text + prev, i - prev,
-                                     sub_parsers, user_data);
-}
 
-static void
-theme_adium_replace_link (const gchar *text,
-                         gssize len,
-                         gpointer match_data,
-                         gpointer user_data)
-{
-       GString *string = user_data;
-       gchar *real_url;
-       gchar *escaped;
-
-       real_url = empathy_make_absolute_url_len (text, len);
-
-       /* The thing we are making a link of may contain
-        * characters which need escaping */
-       escaped = g_markup_escape_text (text, len);
-
-       /* Append the link inside <a href=""></a> tag */
-       g_string_append_printf (string, "<a href=\"%s\">%s</a>",
-                               real_url, escaped);
-
-       g_free (real_url);
-       g_free (escaped);
-}
-
-static void
-theme_adium_replace_smiley (const gchar *text,
-                           gssize len,
-                           gpointer match_data,
-                           gpointer user_data)
-{
-       EmpathySmileyHit *hit = match_data;
-       GString *string = user_data;
+               g_string_append_len (result, format, next - format);
+               g_string_append (result, str);
+               format = next + 2;
+       }
+       g_string_append (result, format);
+       va_end (args);
 
-       /* Replace smiley by a <img/> tag */
-       g_string_append_printf (string,
-                               "<img src=\"%s\" alt=\"%.*s\" title=\"%.*s\"/>",
-                               hit->path, (int)len, text, (int)len, text);
+       return g_string_free (result, FALSE);
 }
 
 static void
-theme_adium_replace_escaped (const gchar *text,
-                            gssize len,
-                            gpointer match_data,
-                            gpointer user_data)
+theme_adium_load_template (EmpathyThemeAdium *theme)
 {
-       GString *string = user_data;
-       gchar *escaped;
+       EmpathyThemeAdiumPriv *priv = GET_PRIV (theme);
+       gchar                 *basedir_uri;
+       gchar                 *variant_path;
+       gchar                 *template;
 
-       escaped = g_markup_escape_text (text, len);
-       g_string_append (string, escaped);
-       g_free (escaped);
+       priv->pages_loading++;
+       basedir_uri = g_strconcat ("file://", priv->data->basedir, NULL);
+       variant_path = adium_info_dup_path_for_variant (priv->data->info,
+               priv->variant);
+       template = string_with_format (priv->data->template_html,
+               variant_path, NULL);
+       webkit_web_view_load_html_string (WEBKIT_WEB_VIEW (theme),
+                                         template, basedir_uri);
+       g_free (basedir_uri);
+       g_free (variant_path);
+       g_free (template);
 }
 
-static EmpathyStringParser string_parsers[] = {
-       {empathy_string_match_link, theme_adium_replace_link},
-       {theme_adium_match_newline, NULL},
-       {empathy_string_match_all, theme_adium_replace_escaped},
-       {NULL, NULL}
-};
-
-static EmpathyStringParser string_parsers_with_smiley[] = {
-       {empathy_string_match_link, theme_adium_replace_link},
-       {empathy_string_match_smiley, theme_adium_replace_smiley},
-       {theme_adium_match_newline, NULL},
-       {empathy_string_match_all, theme_adium_replace_escaped},
-       {NULL, NULL}
-};
-
 static gchar *
-theme_adium_parse_body (const gchar *text)
+theme_adium_parse_body (EmpathyThemeAdium *self,
+       const gchar *text,
+       const gchar *token)
 {
+       EmpathyThemeAdiumPriv *priv = GET_PRIV (self);
        EmpathyStringParser *parsers;
        GString *string;
-       gboolean use_smileys;
 
        /* Check if we have to parse smileys */
-       empathy_conf_get_bool (empathy_conf_get (),
-                              EMPATHY_PREFS_CHAT_SHOW_SMILEYS,
-                              &use_smileys);
-       if (use_smileys)
-               parsers = string_parsers_with_smiley;
-       else
-               parsers = string_parsers;
+       parsers = empathy_webkit_get_string_parser (
+               g_settings_get_boolean (priv->gsettings_chat,
+                       EMPATHY_PREFS_CHAT_SHOW_SMILEYS));
 
        /* Parse text and construct string with links and smileys replaced
         * by html tags. Also escape text to make sure html code is
         * displayed verbatim. */
        string = g_string_sized_new (strlen (text));
+
+       /* wrap this in HTML that allows us to find the message for later
+        * editing */
+       if (!tp_str_empty (token))
+               g_string_append_printf (string,
+                       "<span id=\"message-token-%s\">",
+                       token);
+
        empathy_string_parser_substr (text, -1, parsers, string);
 
+       if (!tp_str_empty (token))
+               g_string_append (string, "</span>");
+
+       /* Wrap body in order to make tabs and multiple spaces displayed
+        * properly. See bug #625745. */
+       g_string_prepend (string, "<div style=\"display: inline; "
+                                              "white-space: pre-wrap\"'>");
+       g_string_append (string, "</div>");
+
        return g_string_free (string, FALSE);
 }
 
 static void
 escape_and_append_len (GString *string, const gchar *str, gint len)
 {
-       while (*str != '\0' && len != 0) {
+       while (str != NULL && *str != '\0' && len != 0) {
                switch (*str) {
                case '\\':
                        /* \ becomes \\ */
@@ -338,8 +331,10 @@ escape_and_append_len (GString *string, const gchar *str, gint len)
        }
 }
 
+/* If *str starts with match, returns TRUE and move pointer to the end */
 static gboolean
-theme_adium_match (const gchar **str, const gchar *match)
+theme_adium_match (const gchar **str,
+                  const gchar *match)
 {
        gint len;
 
@@ -352,76 +347,327 @@ theme_adium_match (const gchar **str, const gchar *match)
        return FALSE;
 }
 
+/* Like theme_adium_match() but also return the X part if match is like %foo{X}% */
+static gboolean
+theme_adium_match_with_format (const gchar **str,
+                              const gchar *match,
+                              gchar **format)
+{
+       const gchar *cur = *str;
+       const gchar *end;
+
+       if (!theme_adium_match (&cur, match)) {
+               return FALSE;
+       }
+       cur++;
+
+       end = strstr (cur, "}%");
+       if (!end) {
+               return FALSE;
+       }
+
+       *format = g_strndup (cur , end - cur);
+       *str = end + 1;
+       return TRUE;
+}
+
+/* List of colors used by %senderColor%. Copied from
+ * adium/Frameworks/AIUtilities\ Framework/Source/AIColorAdditions.m
+ */
+static gchar *colors[] = {
+       "aqua", "aquamarine", "blue", "blueviolet", "brown", "burlywood", "cadetblue",
+       "chartreuse", "chocolate", "coral", "cornflowerblue", "crimson", "cyan",
+       "darkblue", "darkcyan", "darkgoldenrod", "darkgreen", "darkgrey", "darkkhaki",
+       "darkmagenta", "darkolivegreen", "darkorange", "darkorchid", "darkred",
+       "darksalmon", "darkseagreen", "darkslateblue", "darkslategrey",
+       "darkturquoise", "darkviolet", "deeppink", "deepskyblue", "dimgrey",
+       "dodgerblue", "firebrick", "forestgreen", "fuchsia", "gold", "goldenrod",
+       "green", "greenyellow", "grey", "hotpink", "indianred", "indigo", "lawngreen",
+       "lightblue", "lightcoral",
+       "lightgreen", "lightgrey", "lightpink", "lightsalmon", "lightseagreen",
+       "lightskyblue", "lightslategrey", "lightsteelblue", "lime", "limegreen",
+       "magenta", "maroon", "mediumaquamarine", "mediumblue", "mediumorchid",
+       "mediumpurple", "mediumseagreen", "mediumslateblue", "mediumspringgreen",
+       "mediumturquoise", "mediumvioletred", "midnightblue", "navy", "olive",
+       "olivedrab", "orange", "orangered", "orchid", "palegreen", "paleturquoise",
+       "palevioletred", "peru", "pink", "plum", "powderblue", "purple", "red",
+       "rosybrown", "royalblue", "saddlebrown", "salmon", "sandybrown", "seagreen",
+       "sienna", "silver", "skyblue", "slateblue", "slategrey", "springgreen",
+       "steelblue", "tan", "teal", "thistle", "tomato", "turquoise", "violet",
+       "yellowgreen",
+};
+
+static const gchar *
+nsdate_to_strftime (EmpathyAdiumData *data, const gchar *nsdate)
+{
+       /* Convert from NSDateFormatter (http://www.stepcase.com/blog/2008/12/02/format-string-for-the-iphone-nsdateformatter/)
+        * to strftime supported by g_date_time_format.
+        * FIXME: table is incomplete, doc of g_date_time_format has a table of
+        *        supported tags.
+        * FIXME: g_date_time_format in GLib 2.28 does 0 padding by default, but
+        *        in 2.29.x we have to explictely request padding with %0x */
+       static const gchar *convert_table[] = {
+               "a", "%p", // AM/PM
+               "A", NULL, // 0~86399999 (Millisecond of Day)
+
+               "cccc", "%A", // Sunday/Monday/Tuesday/Wednesday/Thursday/Friday/Saturday
+               "ccc", "%a", // Sun/Mon/Tue/Wed/Thu/Fri/Sat
+               "cc", "%u", // 1~7 (Day of Week)
+               "c", "%u", // 1~7 (Day of Week)
+
+               "dd", "%d", // 1~31 (0 padded Day of Month)
+               "d", "%d", // 1~31 (0 padded Day of Month)
+               "D", "%j", // 1~366 (0 padded Day of Year)
+
+               "e", "%u", // 1~7 (0 padded Day of Week)
+               "EEEE", "%A", // Sunday/Monday/Tuesday/Wednesday/Thursday/Friday/Saturday
+               "EEE", "%a", // Sun/Mon/Tue/Wed/Thu/Fri/Sat
+               "EE", "%a", // Sun/Mon/Tue/Wed/Thu/Fri/Sat
+               "E", "%a", // Sun/Mon/Tue/Wed/Thu/Fri/Sat
+
+               "F", NULL, // 1~5 (0 padded Week of Month, first day of week = Monday)
+
+               "g", NULL, // Julian Day Number (number of days since 4713 BC January 1)
+               "GGGG", NULL, // Before Christ/Anno Domini
+               "GGG", NULL, // BC/AD (Era Designator Abbreviated)
+               "GG", NULL, // BC/AD (Era Designator Abbreviated)
+               "G", NULL, // BC/AD (Era Designator Abbreviated)
+
+               "h", "%I", // 1~12 (0 padded Hour (12hr))
+               "H", "%H", // 0~23 (0 padded Hour (24hr))
+
+               "k", NULL, // 1~24 (0 padded Hour (24hr)
+               "K", NULL, // 0~11 (0 padded Hour (12hr))
+
+               "LLLL", "%B", // January/February/March/April/May/June/July/August/September/October/November/December
+               "LLL", "%b", // Jan/Feb/Mar/Apr/May/Jun/Jul/Aug/Sep/Oct/Nov/Dec
+               "LL", "%m", // 1~12 (0 padded Month)
+               "L", "%m", // 1~12 (0 padded Month)
+
+               "m", "%M", // 0~59 (0 padded Minute)
+               "MMMM", "%B", // January/February/March/April/May/June/July/August/September/October/November/December
+               "MMM", "%b", // Jan/Feb/Mar/Apr/May/Jun/Jul/Aug/Sep/Oct/Nov/Dec
+               "MM", "%m", // 1~12 (0 padded Month)
+               "M", "%m", // 1~12 (0 padded Month)
+
+               "qqqq", NULL, // 1st quarter/2nd quarter/3rd quarter/4th quarter
+               "qqq", NULL, // Q1/Q2/Q3/Q4
+               "qq", NULL, // 1~4 (0 padded Quarter)
+               "q", NULL, // 1~4 (0 padded Quarter)
+               "QQQQ", NULL, // 1st quarter/2nd quarter/3rd quarter/4th quarter
+               "QQQ", NULL, // Q1/Q2/Q3/Q4
+               "QQ", NULL, // 1~4 (0 padded Quarter)
+               "Q", NULL, // 1~4 (0 padded Quarter)
+
+               "s", "%S", // 0~59 (0 padded Second)
+               "S", NULL, // (rounded Sub-Second)
+
+               "u", "%Y", // (0 padded Year)
+
+               "vvvv", "%Z", // (General GMT Timezone Name)
+               "vvv", "%Z", // (General GMT Timezone Abbreviation)
+               "vv", "%Z", // (General GMT Timezone Abbreviation)
+               "v", "%Z", // (General GMT Timezone Abbreviation)
+
+               "w", "%W", // 1~53 (0 padded Week of Year, 1st day of week = Sunday, NB, 1st week of year starts from the last Sunday of last year)
+               "W", NULL, // 1~5 (0 padded Week of Month, 1st day of week = Sunday)
+
+               "yyyy", "%Y", // (Full Year)
+               "yyy", "%y", // (2 Digits Year)
+               "yy", "%y", // (2 Digits Year)
+               "y", "%Y", // (Full Year)
+               "YYYY", NULL, // (Full Year, starting from the Sunday of the 1st week of year)
+               "YYY", NULL, // (2 Digits Year, starting from the Sunday of the 1st week of year)
+               "YY", NULL, // (2 Digits Year, starting from the Sunday of the 1st week of year)
+               "Y", NULL, // (Full Year, starting from the Sunday of the 1st week of year)
+
+               "zzzz", NULL, // (Specific GMT Timezone Name)
+               "zzz", NULL, // (Specific GMT Timezone Abbreviation)
+               "zz", NULL, // (Specific GMT Timezone Abbreviation)
+               "z", NULL, // (Specific GMT Timezone Abbreviation)
+               "Z", "%z", // +0000 (RFC 822 Timezone)
+       };
+       const gchar *str;
+       GString *string;
+       guint i, j;
+
+       if (nsdate == NULL) {
+               return NULL;
+       }
+
+       str = g_hash_table_lookup (data->date_format_cache, nsdate);
+       if (str != NULL) {
+               return str;
+       }
+
+       /* Copy nsdate into string, replacing occurences of NSDateFormatter tags
+        * by corresponding strftime tag. */
+       string = g_string_sized_new (strlen (nsdate));
+       for (i = 0; nsdate[i] != '\0'; i++) {
+               gboolean found = FALSE;
+
+               /* even indexes are NSDateFormatter tag, odd indexes are the
+                * corresponding strftime tag */
+               for (j = 0; j < G_N_ELEMENTS (convert_table); j += 2) {
+                       if (g_str_has_prefix (nsdate + i, convert_table[j])) {
+                               found = TRUE;
+                               break;
+                       }
+               }
+               if (found) {
+                       /* If we don't have a replacement, just ignore that tag */
+                       if (convert_table[j + 1] != NULL) {
+                               g_string_append (string, convert_table[j + 1]);
+                       }
+                       i += strlen (convert_table[j]) - 1;
+               } else {
+                       g_string_append_c (string, nsdate[i]);
+               }
+       }
+
+       DEBUG ("Date format converted '%s' â†’ '%s'", nsdate, string->str);
+
+       /* The cache takes ownership of string->str */
+       g_hash_table_insert (data->date_format_cache, g_strdup (nsdate), string->str);
+       return g_string_free (string, FALSE);
+}
+
+
 static void
 theme_adium_append_html (EmpathyThemeAdium *theme,
                         const gchar       *func,
-                        const gchar       *html, gsize len,
+                        const gchar       *html,
                         const gchar       *message,
                         const gchar       *avatar_filename,
                         const gchar       *name,
                         const gchar       *contact_id,
                         const gchar       *service_name,
                         const gchar       *message_classes,
-                        time_t             timestamp)
+                        gint64             timestamp,
+                        gboolean           is_backlog,
+                        gboolean           outgoing)
 {
+       EmpathyThemeAdiumPriv *priv = GET_PRIV (theme);
        GString     *string;
        const gchar *cur = NULL;
        gchar       *script;
 
        /* Make some search-and-replace in the html code */
-       string = g_string_sized_new (len + strlen (message));
+       string = g_string_sized_new (strlen (html) + strlen (message));
        g_string_append_printf (string, "%s(\"", func);
        for (cur = html; *cur != '\0'; cur++) {
                const gchar *replace = NULL;
                gchar       *dup_replace = NULL;
+               gchar       *format = NULL;
 
-               if (theme_adium_match (&cur, "%message%")) {
-                       replace = message;
-               } else if (theme_adium_match (&cur, "%messageClasses%")) {
-                       replace = message_classes;
-               } else if (theme_adium_match (&cur, "%userIconPath%")) {
+               /* Those are all well known keywords that needs replacement in
+                * html files. Please keep them in the same order than the adium
+                * spec. See http://trac.adium.im/wiki/CreatingMessageStyles */
+               if (theme_adium_match (&cur, "%userIconPath%")) {
                        replace = avatar_filename;
-               } else if (theme_adium_match (&cur, "%sender%")) {
-                       replace = name;
                } else if (theme_adium_match (&cur, "%senderScreenName%")) {
                        replace = contact_id;
+               } else if (theme_adium_match (&cur, "%sender%")) {
+                       replace = name;
+               } else if (theme_adium_match (&cur, "%senderColor%")) {
+                       /* A color derived from the user's name.
+                        * FIXME: If a colon separated list of HTML colors is at
+                        * Incoming/SenderColors.txt it will be used instead of
+                        * the default colors.
+                        */
+
+                       /* Ensure we always use the same color when sending messages
+                        * (bgo #658821) */
+                       if (outgoing) {
+                               replace = "inherit";
+                       } else if (contact_id != NULL) {
+                               guint hash = g_str_hash (contact_id);
+                               replace = colors[hash % G_N_ELEMENTS (colors)];
+                       }
+               } else if (theme_adium_match (&cur, "%senderStatusIcon%")) {
+                       /* FIXME: The path to the status icon of the sender
+                        * (available, away, etc...)
+                        */
+               } else if (theme_adium_match (&cur, "%messageDirection%")) {
+                       /* FIXME: The text direction of the message
+                        * (either rtl or ltr)
+                        */
                } else if (theme_adium_match (&cur, "%senderDisplayName%")) {
-                       /* %senderDisplayName% -
-                        * "The serverside (remotely set) name of the sender,
-                        *  such as an MSN display name."
+                       /* FIXME: The serverside (remotely set) name of the
+                        * sender, such as an MSN display name.
                         *
-                        * We don't have access to that yet so we use local
-                        * alias instead.*/
+                        *  We don't have access to that yet so we use
+                        * local alias instead.
+                        */
                        replace = name;
-               } else if (theme_adium_match (&cur, "%service%")) {
-                       replace = service_name;
-               } else if (theme_adium_match (&cur, "%shortTime%")) {
-                       dup_replace = empathy_time_to_string_local (timestamp,
-                               EMPATHY_TIME_FORMAT_DISPLAY_SHORT);
-                       replace = dup_replace;
-               } else if (theme_adium_match (&cur, "%time")) {
-                       gchar *format = NULL;
-                       gchar *end;
-                       /* Time can be in 2 formats:
-                        * %time% or %time{strftime format}%
-                        * Extract the time format if provided. */
-                       if (cur[1] == '{') {
-                               cur += 2;
-                               end = strstr (cur, "}%");
-                               if (!end) {
-                                       /* Invalid string */
-                                       continue;
-                               }
-                               format = g_strndup (cur, end - cur);
-                               cur = end + 1;
+               } else if (theme_adium_match_with_format (&cur, "%textbackgroundcolor{", &format)) {
+                       /* FIXME: This keyword is used to represent the
+                        * highlight background color. "X" is the opacity of the
+                        * background, ranges from 0 to 1 and can be any decimal
+                        * between.
+                        */
+               } else if (theme_adium_match (&cur, "%message%")) {
+                       replace = message;
+               } else if (theme_adium_match (&cur, "%time%") ||
+                          theme_adium_match_with_format (&cur, "%time{", &format)) {
+                       const gchar *strftime_format;
+
+                       strftime_format = nsdate_to_strftime (priv->data, format);
+                       if (is_backlog) {
+                               dup_replace = empathy_time_to_string_local (timestamp,
+                                       strftime_format ? strftime_format :
+                                       EMPATHY_TIME_DATE_FORMAT_DISPLAY_SHORT);
                        } else {
-                               cur++;
+                               dup_replace = empathy_time_to_string_local (timestamp,
+                                       strftime_format ? strftime_format :
+                                       EMPATHY_TIME_FORMAT_DISPLAY_SHORT);
                        }
-
+                       replace = dup_replace;
+               } else if (theme_adium_match (&cur, "%shortTime%")) {
                        dup_replace = empathy_time_to_string_local (timestamp,
-                               format ? format : EMPATHY_TIME_FORMAT_DISPLAY_SHORT);
+                               EMPATHY_TIME_FORMAT_DISPLAY_SHORT);
                        replace = dup_replace;
-                       g_free (format);
+               } else if (theme_adium_match (&cur, "%service%")) {
+                       replace = service_name;
+               } else if (theme_adium_match (&cur, "%variant%")) {
+                       /* FIXME: The name of the active message style variant,
+                        * with all spaces replaced with an underscore.
+                        * A variant named "Alternating Messages - Blue Red"
+                        * will become "Alternating_Messages_-_Blue_Red".
+                        */
+               } else if (theme_adium_match (&cur, "%userIcons%")) {
+                       /* FIXME: mus t be "hideIcons" if use preference is set
+                        * to hide avatars */
+                       replace = "showIcons";
+               } else if (theme_adium_match (&cur, "%messageClasses%")) {
+                       replace = message_classes;
+               } else if (theme_adium_match (&cur, "%status%")) {
+                       /* FIXME: A description of the status event. This is
+                        * neither in the user's local language nor expected to
+                        * be displayed; it may be useful to use a different div
+                        * class to present different types of status messages.
+                        * The following is a list of some of the more important
+                        * status messages; your message style should be able to
+                        * handle being shown a status message not in this list,
+                        * as even at present the list is incomplete and is
+                        * certain to become out of date in the future:
+                        *      online
+                        *      offline
+                        *      away
+                        *      away_message
+                        *      return_away
+                        *      idle
+                        *      return_idle
+                        *      date_separator
+                        *      contact_joined (group chats)
+                        *      contact_left
+                        *      error
+                        *      timed_out
+                        *      encryption (all OTR messages use this status)
+                        *      purple (all IRC topic and join/part messages use this status)
+                        *      fileTransferStarted
+                        *      fileTransferCompleted
+                        */
                } else {
                        escape_and_append_len (string, cur, 1);
                        continue;
@@ -429,7 +675,9 @@ theme_adium_append_html (EmpathyThemeAdium *theme,
 
                /* Here we have a replacement to make */
                escape_and_append_len (string, replace, -1);
+
                g_free (dup_replace);
+               g_free (format);
        }
        g_string_append (string, "\")");
 
@@ -445,13 +693,10 @@ theme_adium_append_event_escaped (EmpathyChatView *view,
        EmpathyThemeAdium     *theme = EMPATHY_THEME_ADIUM (view);
        EmpathyThemeAdiumPriv *priv = GET_PRIV (theme);
 
-       if (priv->data->status_html) {
-               theme_adium_append_html (theme, "appendMessage",
-                                        priv->data->status_html,
-                                        priv->data->status_len,
-                                        escaped, NULL, NULL, NULL, NULL,
-                                        "event", empathy_time_get_current ());
-       }
+       theme_adium_append_html (theme, "appendMessage",
+                                priv->data->status_html, escaped, NULL, NULL, NULL,
+                                NULL, "event",
+                                empathy_time_get_current (), FALSE, FALSE);
 
        /* There is no last contact */
        if (priv->last_contact) {
@@ -460,32 +705,103 @@ theme_adium_append_event_escaped (EmpathyChatView *view,
        }
 }
 
+static void
+theme_adium_remove_focus_marks (EmpathyThemeAdium *theme,
+    WebKitDOMNodeList *nodes)
+{
+       guint i;
+
+       /* Remove focus and firstFocus class */
+       for (i = 0; i < webkit_dom_node_list_get_length (nodes); i++) {
+               WebKitDOMNode *node = webkit_dom_node_list_item (nodes, i);
+               WebKitDOMHTMLElement *element = WEBKIT_DOM_HTML_ELEMENT (node);
+               gchar *class_name;
+               gchar **classes, **iter;
+               GString *new_class_name;
+               gboolean first = TRUE;
+
+               if (element == NULL) {
+                       continue;
+               }
+
+               class_name = webkit_dom_html_element_get_class_name (element);
+               classes = g_strsplit (class_name, " ", -1);
+               new_class_name = g_string_sized_new (strlen (class_name));
+               for (iter = classes; *iter != NULL; iter++) {
+                       if (tp_strdiff (*iter, "focus") &&
+                           tp_strdiff (*iter, "firstFocus")) {
+                               if (!first) {
+                                       g_string_append_c (new_class_name, ' ');
+                               }
+                               g_string_append (new_class_name, *iter);
+                               first = FALSE;
+                       }
+               }
+
+               webkit_dom_html_element_set_class_name (element, new_class_name->str);
+
+               g_free (class_name);
+               g_strfreev (classes);
+               g_string_free (new_class_name, TRUE);
+       }
+}
+
+static void
+theme_adium_remove_all_focus_marks (EmpathyThemeAdium *theme)
+{
+       EmpathyThemeAdiumPriv *priv = GET_PRIV (theme);
+       WebKitDOMDocument *dom;
+       WebKitDOMNodeList *nodes;
+       GError *error = NULL;
+
+       if (!priv->has_unread_message)
+               return;
+
+       priv->has_unread_message = FALSE;
+
+       dom = webkit_web_view_get_dom_document (WEBKIT_WEB_VIEW (theme));
+       if (dom == NULL) {
+               return;
+       }
+
+       /* Get all nodes with focus class */
+       nodes = webkit_dom_document_query_selector_all (dom, ".focus", &error);
+       if (nodes == NULL) {
+               DEBUG ("Error getting focus nodes: %s",
+                       error ? error->message : "No error");
+               g_clear_error (&error);
+               return;
+       }
+
+       theme_adium_remove_focus_marks (theme, nodes);
+}
+
 static void
 theme_adium_append_message (EmpathyChatView *view,
-                           EmpathyMessage  *msg)
+                           EmpathyMessage  *msg,
+                           gboolean         should_highlight)
 {
        EmpathyThemeAdium     *theme = EMPATHY_THEME_ADIUM (view);
        EmpathyThemeAdiumPriv *priv = GET_PRIV (theme);
        EmpathyContact        *sender;
+       TpMessage             *tp_msg;
        TpAccount             *account;
-       gchar                 *body_escaped;
-       const gchar           *body;
+       gchar                 *body_escaped, *name_escaped;
        const gchar           *name;
        const gchar           *contact_id;
        EmpathyAvatar         *avatar;
        const gchar           *avatar_filename = NULL;
-       time_t                 timestamp;
-       gchar                 *html = NULL;
-       gsize                  len = 0;
+       gint64                 timestamp;
+       const gchar           *html = NULL;
        const gchar           *func;
        const gchar           *service_name;
        GString               *message_classes = NULL;
        gboolean               is_backlog;
        gboolean               consecutive;
+       gboolean               action;
 
-       if (!priv->page_loaded) {
-               priv->message_queue = g_list_prepend (priv->message_queue,
-                                                     g_object_ref (msg));
+       if (priv->pages_loading != 0) {
+               queue_item (&priv->message_queue, QUEUED_MESSAGE, msg, NULL, should_highlight);
                return;
        }
 
@@ -497,21 +813,28 @@ theme_adium_append_message (EmpathyChatView *view,
        if (service_name == NULL)
                service_name = tp_account_get_protocol (account);
        timestamp = empathy_message_get_timestamp (msg);
-       body = empathy_message_get_body (msg);
-       body_escaped = theme_adium_parse_body (body);
-       name = empathy_contact_get_name (sender);
+       body_escaped = theme_adium_parse_body (theme,
+               empathy_message_get_body (msg),
+               empathy_message_get_token (msg));
+       name = empathy_contact_get_logged_alias (sender);
        contact_id = empathy_contact_get_id (sender);
+       action = (empathy_message_get_tptype (msg) == TP_CHANNEL_TEXT_MESSAGE_TYPE_ACTION);
 
-       /* If this is a /me, append an event */
-       if (empathy_message_get_tptype (msg) == TP_CHANNEL_TEXT_MESSAGE_TYPE_ACTION) {
-               gchar *str;
+       name_escaped = g_markup_escape_text (name, -1);
 
-               str = g_strdup_printf ("%s %s", name, body_escaped);
-               theme_adium_append_event_escaped (view, str);
+       /* If this is a /me probably */
+       if (action) {
+               gchar *str;
 
-               g_free (str);
+               if (priv->data->version >= 4 || !priv->data->custom_template) {
+                       str = g_strdup_printf ("<span class='actionMessageUserName'>%s</span>"
+                                              "<span class='actionMessageBody'>%s</span>",
+                                              name_escaped, body_escaped);
+               } else {
+                       str = g_strdup_printf ("*%s*", body_escaped);
+               }
                g_free (body_escaped);
-               return;
+               body_escaped = str;
        }
 
        /* Get the avatar filename, or a fallback */
@@ -528,7 +851,7 @@ theme_adium_append_message (EmpathyChatView *view,
                if (!avatar_filename) {
                        if (!priv->data->default_avatar_filename) {
                                priv->data->default_avatar_filename =
-                                       empathy_filename_from_icon_name ("stock_person",
+                                       empathy_filename_from_icon_name (EMPATHY_IMAGE_AVATAR_DEFAULT,
                                                                         GTK_ICON_SIZE_DIALOG);
                        }
                        avatar_filename = priv->data->default_avatar_filename;
@@ -549,6 +872,13 @@ theme_adium_append_message (EmpathyChatView *view,
 
        /* Define message classes */
        message_classes = g_string_new ("message");
+       if (!priv->has_focus && !is_backlog) {
+               if (!priv->has_unread_message) {
+                       g_string_append (message_classes, " firstFocus");
+                       priv->has_unread_message = TRUE;
+               }
+               g_string_append (message_classes, " focus");
+       }
        if (is_backlog) {
                g_string_append (message_classes, " history");
        }
@@ -560,84 +890,73 @@ theme_adium_append_message (EmpathyChatView *view,
        } else {
                g_string_append (message_classes, " incoming");
        }
+       if (should_highlight) {
+               g_string_append (message_classes, " mention");
+       }
+       if (empathy_message_get_tptype (msg) == TP_CHANNEL_TEXT_MESSAGE_TYPE_AUTO_REPLY) {
+               g_string_append (message_classes, " autoreply");
+       }
+       if (action) {
+               g_string_append (message_classes, " action");
+       }
+       /* FIXME: other classes:
+        * status - the message is a status change
+        * event - the message is a notification of something happening
+        *         (for example, encryption being turned on)
+        * %status% - See %status% in theme_adium_append_html ()
+        */
+
+       /* This is slightly a hack, but it's the only way to add
+        * arbitrary data to messages in the HTML. We add another
+        * class called "x-empathy-message-id-*" to the message. This
+        * way, we can remove the unread marker for this specific
+        * message later. */
+       tp_msg = empathy_message_get_tp_message (msg);
+       if (tp_msg != NULL) {
+               guint32 id;
+               gboolean valid;
+
+               id = tp_message_get_pending_message_id (tp_msg, &valid);
+               if (valid) {
+                       g_string_append_printf (message_classes,
+                           " x-empathy-message-id-%u", id);
+               }
+       }
 
        /* Define javascript function to use */
        if (consecutive) {
-               func = "appendNextMessage";
+               func = priv->allow_scrolling ? "appendNextMessage" : "appendNextMessageNoScroll";
        } else {
-               func = "appendMessage";
+               func = priv->allow_scrolling ? "appendMessage" : "appendMessageNoScroll";
        }
 
-       /* Outgoing */
        if (empathy_contact_is_user (sender)) {
-               if (consecutive) {
-                       if (is_backlog) {
-                               html = priv->data->out_nextcontext_html;
-                               len = priv->data->out_nextcontext_len;
-                       }
-
-                       /* Not backlog, or fallback if NextContext.html
-                        * is missing */
-                       if (html == NULL) {
-                               html = priv->data->out_nextcontent_html;
-                               len = priv->data->out_nextcontent_len;
-                       }
-               }
-
-               /* Not consecutive, or fallback if NextContext.html and/or
-                * NextContent.html are missing */
-               if (html == NULL) {
-                       if (is_backlog) {
-                               html = priv->data->out_context_html;
-                               len = priv->data->out_context_len;
-                       }
-
-                       if (html == NULL) {
-                               html = priv->data->out_content_html;
-                               len = priv->data->out_content_len;
-                       }
-               }
-       }
-
-       /* Incoming, or fallback if outgoing files are missing */
-       if (html == NULL) {
-               if (consecutive) {
-                       if (is_backlog) {
-                               html = priv->data->in_nextcontext_html;
-                               len = priv->data->in_nextcontext_len;
-                       }
-
-                       /* Note backlog, or fallback if NextContext.html
-                        * is missing */
-                       if (html == NULL) {
-                               html = priv->data->in_nextcontent_html;
-                               len = priv->data->in_nextcontent_len;
-                       }
+               /* out */
+               if (is_backlog) {
+                       /* context */
+                       html = consecutive ? priv->data->out_nextcontext_html : priv->data->out_context_html;
+               } else {
+                       /* content */
+                       html = consecutive ? priv->data->out_nextcontent_html : priv->data->out_content_html;
                }
 
-               /* Not consecutive, or fallback if NextContext.html and/or
-                * NextContent.html are missing */
-               if (html == NULL) {
-                       if (is_backlog) {
-                               html = priv->data->in_context_html;
-                               len = priv->data->in_context_len;
-                       }
-
-                       if (html == NULL) {
-                               html = priv->data->in_content_html;
-                               len = priv->data->in_content_len;
-                       }
+               /* remove all the unread marks when we are sending a message */
+               theme_adium_remove_all_focus_marks (theme);
+       } else {
+               /* in */
+               if (is_backlog) {
+                       /* context */
+                       html = consecutive ? priv->data->in_nextcontext_html : priv->data->in_context_html;
+               } else {
+                       /* content */
+                       html = consecutive ? priv->data->in_nextcontent_html : priv->data->in_content_html;
                }
        }
 
-       if (html != NULL) {
-               theme_adium_append_html (theme, func, html, len, body_escaped,
-                                        avatar_filename, name, contact_id,
-                                        service_name, message_classes->str,
-                                        timestamp);
-       } else {
-               DEBUG ("Couldn't find HTML file for this message");
-       }
+       theme_adium_append_html (theme, func, html, body_escaped,
+                                avatar_filename, name_escaped, contact_id,
+                                service_name, message_classes->str,
+                                timestamp, is_backlog, empathy_contact_is_user (sender));
 
        /* Keep the sender of the last displayed message */
        if (priv->last_contact) {
@@ -648,6 +967,7 @@ theme_adium_append_message (EmpathyChatView *view,
        priv->last_is_backlog = is_backlog;
 
        g_free (body_escaped);
+       g_free (name_escaped);
        g_string_free (message_classes, TRUE);
 }
 
@@ -655,25 +975,141 @@ static void
 theme_adium_append_event (EmpathyChatView *view,
                          const gchar     *str)
 {
+       EmpathyThemeAdiumPriv *priv = GET_PRIV (view);
        gchar *str_escaped;
 
+       if (priv->pages_loading != 0) {
+               queue_item (&priv->message_queue, QUEUED_EVENT, NULL, str, FALSE);
+               return;
+       }
+
        str_escaped = g_markup_escape_text (str, -1);
        theme_adium_append_event_escaped (view, str_escaped);
        g_free (str_escaped);
 }
 
+static void
+theme_adium_append_event_markup (EmpathyChatView *view,
+                                const gchar     *markup_text,
+                                const gchar     *fallback_text)
+{
+       theme_adium_append_event_escaped (view, markup_text);
+}
+
+static void
+theme_adium_edit_message (EmpathyChatView *view,
+                         EmpathyMessage  *message)
+{
+       EmpathyThemeAdiumPriv *priv = GET_PRIV (view);
+       WebKitDOMDocument *doc;
+       WebKitDOMElement *span;
+       gchar *id, *parsed_body;
+       gchar *tooltip, *timestamp;
+       GtkIconInfo *icon_info;
+       GError *error = NULL;
+
+       if (priv->pages_loading != 0) {
+               queue_item (&priv->message_queue, QUEUED_EDIT, message, NULL, FALSE);
+               return;
+       }
+
+       id = g_strdup_printf ("message-token-%s",
+               empathy_message_get_supersedes (message));
+       /* we don't pass a token here, because doing so will return another
+        * <span> element, and we don't want nested <span> elements */
+       parsed_body = theme_adium_parse_body (EMPATHY_THEME_ADIUM (view),
+               empathy_message_get_body (message), NULL);
+
+       /* find the element */
+       doc = webkit_web_view_get_dom_document (WEBKIT_WEB_VIEW (view));
+       span = webkit_dom_document_get_element_by_id (doc, id);
+
+       if (span == NULL) {
+               DEBUG ("Failed to find id '%s'", id);
+               goto except;
+       }
+
+       if (!WEBKIT_DOM_IS_HTML_ELEMENT (span)) {
+               DEBUG ("Not a HTML element");
+               goto except;
+       }
+
+       /* update the HTML */
+       webkit_dom_html_element_set_inner_html (WEBKIT_DOM_HTML_ELEMENT (span),
+               parsed_body, &error);
+
+       if (error != NULL) {
+               DEBUG ("Error setting new inner-HTML: %s", error->message);
+               g_error_free (error);
+               goto except;
+       }
+
+       /* set a tooltip */
+       timestamp = empathy_time_to_string_local (
+               empathy_message_get_timestamp (message),
+               "%H:%M:%S");
+       tooltip = g_strdup_printf (_("Message edited at %s"), timestamp);
+
+       webkit_dom_html_element_set_title (WEBKIT_DOM_HTML_ELEMENT (span),
+               tooltip);
+
+       g_free (tooltip);
+       g_free (timestamp);
+
+       /* mark this message as edited */
+       icon_info = gtk_icon_theme_lookup_icon (gtk_icon_theme_get_default (),
+               EMPATHY_IMAGE_EDIT_MESSAGE, 16, 0);
+
+       if (icon_info != NULL) {
+               /* set the icon as a background image using CSS
+                * FIXME: the icon won't update in response to theme changes */
+               gchar *style = g_strdup_printf (
+                       "background-image:url('%s');"
+                       "background-repeat:no-repeat;"
+                       "background-position:left center;"
+                       "padding-left:19px;", /* 16px icon + 3px padding */
+                       gtk_icon_info_get_filename (icon_info));
+
+               webkit_dom_element_set_attribute (span, "style", style, &error);
+
+               if (error != NULL) {
+                       DEBUG ("Error setting element style: %s",
+                               error->message);
+                       g_clear_error (&error);
+                       /* not fatal */
+               }
+
+               g_free (style);
+               gtk_icon_info_free (icon_info);
+       }
+
+       goto finally;
+
+except:
+       DEBUG ("Could not find message to edit with: %s",
+               empathy_message_get_body (message));
+
+finally:
+       g_free (id);
+       g_free (parsed_body);
+}
+
 static void
 theme_adium_scroll (EmpathyChatView *view,
                    gboolean         allow_scrolling)
 {
-       /* FIXME: Is it possible? I guess we need a js function, but I don't
-        * see any... */
+       EmpathyThemeAdiumPriv *priv = GET_PRIV (view);
+
+       priv->allow_scrolling = allow_scrolling;
+       if (allow_scrolling) {
+               empathy_chat_view_scroll_down (view);
+       }
 }
 
 static void
 theme_adium_scroll_down (EmpathyChatView *view)
 {
-       webkit_web_view_execute_script (WEBKIT_WEB_VIEW (view), "scrollToBottom()");
+       webkit_web_view_execute_script (WEBKIT_WEB_VIEW (view), "alignChat(true);");
 }
 
 static gboolean
@@ -686,14 +1122,8 @@ static void
 theme_adium_clear (EmpathyChatView *view)
 {
        EmpathyThemeAdiumPriv *priv = GET_PRIV (view);
-       gchar *basedir_uri;
 
-       priv->page_loaded = FALSE;
-       basedir_uri = g_strconcat ("file://", priv->data->basedir, NULL);
-       webkit_web_view_load_html_string (WEBKIT_WEB_VIEW (view),
-                                         priv->data->template_html,
-                                         basedir_uri);
-       g_free (basedir_uri);
+       theme_adium_load_template (EMPATHY_THEME_ADIUM (view));
 
        /* Clear last contact to avoid trying to add a 'joined'
         * message when we don't have an insertion point. */
@@ -706,26 +1136,31 @@ theme_adium_clear (EmpathyChatView *view)
 static gboolean
 theme_adium_find_previous (EmpathyChatView *view,
                           const gchar     *search_criteria,
-                          gboolean         new_search)
+                          gboolean         new_search,
+                          gboolean         match_case)
 {
+       /* FIXME: Doesn't respect new_search */
        return webkit_web_view_search_text (WEBKIT_WEB_VIEW (view),
-                                           search_criteria, FALSE,
+                                           search_criteria, match_case,
                                            FALSE, TRUE);
 }
 
 static gboolean
 theme_adium_find_next (EmpathyChatView *view,
                       const gchar     *search_criteria,
-                      gboolean         new_search)
+                      gboolean         new_search,
+                      gboolean         match_case)
 {
+       /* FIXME: Doesn't respect new_search */
        return webkit_web_view_search_text (WEBKIT_WEB_VIEW (view),
-                                           search_criteria, FALSE,
+                                           search_criteria, match_case,
                                            TRUE, TRUE);
 }
 
 static void
 theme_adium_find_abilities (EmpathyChatView *view,
                            const gchar    *search_criteria,
+                            gboolean        match_case,
                            gboolean       *can_do_previous,
                            gboolean       *can_do_next)
 {
@@ -756,87 +1191,97 @@ theme_adium_copy_clipboard (EmpathyChatView *view)
 }
 
 static void
-theme_adium_context_menu_selection_done_cb (GtkMenuShell *menu, gpointer user_data)
+theme_adium_remove_mark_from_message (EmpathyThemeAdium *self,
+                                     guint32 id)
 {
-       WebKitHitTestResult *hit_test_result = WEBKIT_HIT_TEST_RESULT (user_data);
+       WebKitDOMDocument *dom;
+       WebKitDOMNodeList *nodes;
+       gchar *class;
+       GError *error = NULL;
+
+       dom = webkit_web_view_get_dom_document (WEBKIT_WEB_VIEW (self));
+       if (dom == NULL) {
+               return;
+       }
+
+       class = g_strdup_printf (".x-empathy-message-id-%u", id);
+
+       /* Get all nodes with focus class */
+       nodes = webkit_dom_document_query_selector_all (dom, class, &error);
+       g_free (class);
+
+       if (nodes == NULL) {
+               DEBUG ("Error getting focus nodes: %s",
+                       error ? error->message : "No error");
+               g_clear_error (&error);
+               return;
+       }
 
-       g_object_unref (hit_test_result);
+       theme_adium_remove_focus_marks (self, nodes);
 }
 
 static void
-theme_adium_context_menu_for_event (EmpathyThemeAdium *theme, GdkEventButton *event)
+theme_adium_remove_acked_message_unread_mark_foreach (gpointer data,
+                                                     gpointer user_data)
 {
-       WebKitWebView              *view = WEBKIT_WEB_VIEW (theme);
-       WebKitHitTestResult        *hit_test_result;
-       WebKitHitTestResultContext  context;
-       GtkWidget                  *menu;
-       GtkWidget                  *item;
-
-       hit_test_result = webkit_web_view_get_hit_test_result (view, event);
-       g_object_get (G_OBJECT (hit_test_result), "context", &context, NULL);
-
-       /* The menu */
-       menu = gtk_menu_new ();
-
-       /* Select all item */
-       item = gtk_image_menu_item_new_from_stock (GTK_STOCK_SELECT_ALL, NULL);
-       gtk_menu_shell_prepend (GTK_MENU_SHELL (menu), item);
-
-       g_signal_connect_swapped (item, "activate",
-                                 G_CALLBACK (webkit_web_view_select_all),
-                                 view);
-
-       /* Copy menu item */
-       if (webkit_web_view_can_copy_clipboard (view)) {
-               item = gtk_image_menu_item_new_from_stock (GTK_STOCK_COPY, NULL);
-               gtk_menu_shell_prepend (GTK_MENU_SHELL (menu), item);
-
-               g_signal_connect_swapped (item, "activate",
-                                         G_CALLBACK (webkit_web_view_copy_clipboard),
-                                         view);
-       }
-
-       /* Clear menu item */
-       item = gtk_separator_menu_item_new ();
-       gtk_menu_shell_prepend (GTK_MENU_SHELL (menu), item);
-
-       item = gtk_image_menu_item_new_from_stock (GTK_STOCK_CLEAR, NULL);
-       gtk_menu_shell_prepend (GTK_MENU_SHELL (menu), item);
-
-       g_signal_connect_swapped (item, "activate",
-                                 G_CALLBACK (empathy_chat_view_clear),
-                                 view);
-
-       /* We will only add the following menu items if we are
-        * right-clicking a link */
-       if (context & WEBKIT_HIT_TEST_RESULT_CONTEXT_LINK) {
-               /* Separator */
-               item = gtk_separator_menu_item_new ();
-               gtk_menu_shell_prepend (GTK_MENU_SHELL (menu), item);
-
-               /* Copy Link Address menu item */
-               item = gtk_menu_item_new_with_mnemonic (_("_Copy Link Address"));
-               g_signal_connect (item, "activate",
-                                 G_CALLBACK (theme_adium_copy_address_cb),
-                                 hit_test_result);
-               gtk_menu_shell_prepend (GTK_MENU_SHELL (menu), item);
-
-               /* Open Link menu item */
-               item = gtk_menu_item_new_with_mnemonic (_("_Open Link"));
-               g_signal_connect (item, "activate",
-                                 G_CALLBACK (theme_adium_open_address_cb),
-                                 hit_test_result);
-               gtk_menu_shell_prepend (GTK_MENU_SHELL (menu), item);
-       }
-
-       g_signal_connect (GTK_MENU_SHELL (menu), "selection-done",
-                         G_CALLBACK (theme_adium_context_menu_selection_done_cb),
-                         hit_test_result);
-
-       /* Display the menu */
-       gtk_widget_show_all (menu);
-       gtk_menu_popup (GTK_MENU (menu), NULL, NULL, NULL, NULL,
-                       event->button, event->time);
+       EmpathyThemeAdium *self = user_data;
+       guint32 id = GPOINTER_TO_UINT (data);
+
+       theme_adium_remove_mark_from_message (self, id);
+}
+
+static void
+theme_adium_focus_toggled (EmpathyChatView *view,
+                          gboolean         has_focus)
+{
+       EmpathyThemeAdiumPriv *priv = GET_PRIV (view);
+
+       priv->has_focus = has_focus;
+       if (!priv->has_focus) {
+               /* We've lost focus, so let's make sure all the acked
+                * messages have lost their unread marker. */
+               g_queue_foreach (&priv->acked_messages,
+                                theme_adium_remove_acked_message_unread_mark_foreach,
+                                view);
+               g_queue_clear (&priv->acked_messages);
+
+               priv->has_unread_message = FALSE;
+       }
+}
+
+static void
+theme_adium_message_acknowledged (EmpathyChatView *view,
+                                 EmpathyMessage  *message)
+{
+       EmpathyThemeAdium *self = (EmpathyThemeAdium *) view;
+       EmpathyThemeAdiumPriv *priv = GET_PRIV (view);
+       TpMessage *tp_msg;
+       guint32 id;
+       gboolean valid;
+
+       tp_msg = empathy_message_get_tp_message (message);
+
+       if (tp_msg == NULL) {
+               return;
+       }
+
+       id = tp_message_get_pending_message_id (tp_msg, &valid);
+       if (!valid) {
+               g_warning ("Acknoledged message doesn't have a pending ID");
+               return;
+       }
+
+       /* We only want to actually remove the unread marker if the
+        * view doesn't have focus. If we did it all the time we would
+        * never see the unread markers, ever! So, we'll queue these
+        * up, and when we lose focus, we'll remove the markers. */
+       if (priv->has_focus) {
+               g_queue_push_tail (&priv->acked_messages,
+                                  GUINT_TO_POINTER (id));
+               return;
+       }
+
+       theme_adium_remove_mark_from_message (self, id);
 }
 
 static gboolean
@@ -852,7 +1297,9 @@ theme_adium_button_press_event (GtkWidget *widget, GdkEventButton *event)
                 * item ourselves, so we disable our customized menu
                 * if the developer extras are enabled. */
                if (!developer_tools_enabled) {
-                       theme_adium_context_menu_for_event (EMPATHY_THEME_ADIUM (widget), event);
+                       empathy_webkit_context_menu_for_event (
+                               WEBKIT_WEB_VIEW (widget), event,
+                               EMPATHY_WEBKIT_MENU_CLEAR);
                        return TRUE;
                }
        }
@@ -865,6 +1312,8 @@ theme_adium_iface_init (EmpathyChatViewIface *iface)
 {
        iface->append_message = theme_adium_append_message;
        iface->append_event = theme_adium_append_event;
+       iface->append_event_markup = theme_adium_append_event_markup;
+       iface->edit_message = theme_adium_edit_message;
        iface->scroll = theme_adium_scroll;
        iface->scroll_down = theme_adium_scroll_down;
        iface->get_has_selection = theme_adium_get_has_selection;
@@ -874,6 +1323,8 @@ theme_adium_iface_init (EmpathyChatViewIface *iface)
        iface->find_abilities = theme_adium_find_abilities;
        iface->highlight = theme_adium_highlight;
        iface->copy_clipboard = theme_adium_copy_clipboard;
+       iface->focus_toggled = theme_adium_focus_toggled;
+       iface->message_acknowledged = theme_adium_message_acknowledged;
 }
 
 static void
@@ -883,19 +1334,38 @@ theme_adium_load_finished_cb (WebKitWebView  *view,
 {
        EmpathyThemeAdiumPriv *priv = GET_PRIV (view);
        EmpathyChatView       *chat_view = EMPATHY_CHAT_VIEW (view);
+       GList                 *l;
 
        DEBUG ("Page loaded");
-       priv->page_loaded = TRUE;
+       priv->pages_loading--;
+
+       if (priv->pages_loading != 0)
+               return;
 
        /* Display queued messages */
-       priv->message_queue = g_list_reverse (priv->message_queue);
-       while (priv->message_queue) {
-               EmpathyMessage *message = priv->message_queue->data;
+       for (l = priv->message_queue.head; l != NULL; l = l->next) {
+               QueuedItem *item = l->data;
+
+               switch (item->type)
+               {
+                       case QUEUED_MESSAGE:
+                               theme_adium_append_message (chat_view, item->msg,
+                                       item->should_highlight);
+                               break;
+
+                       case QUEUED_EDIT:
+                               theme_adium_edit_message (chat_view, item->msg);
+                               break;
+
+                       case QUEUED_EVENT:
+                               theme_adium_append_event (chat_view, item->str);
+                               break;
+               }
 
-               theme_adium_append_message (chat_view, message);
-               priv->message_queue = g_list_remove (priv->message_queue, message);
-               g_object_unref (message);
+               free_queued_item (item);
        }
+
+       g_queue_clear (&priv->message_queue);
 }
 
 static void
@@ -905,8 +1375,8 @@ theme_adium_finalize (GObject *object)
 
        empathy_adium_data_unref (priv->data);
 
-       empathy_conf_notify_remove (empathy_conf_get (),
-                                   priv->notify_enable_webkit_developer_tools_id);
+       g_object_unref (priv->gsettings_chat);
+       g_object_unref (priv->gsettings_desktop);
 
        G_OBJECT_CLASS (empathy_theme_adium_parent_class)->finalize (object);
 }
@@ -931,6 +1401,10 @@ theme_adium_dispose (GObject *object)
                priv->inspector_window = NULL;
        }
 
+       if (priv->acked_messages.length > 0) {
+               g_queue_clear (&priv->acked_messages);
+       }
+
        G_OBJECT_CLASS (empathy_theme_adium_parent_class)->dispose (object);
 }
 
@@ -999,87 +1473,28 @@ theme_adium_inspect_web_view_cb (WebKitWebInspector *inspector,
        return NULL;
 }
 
-static PangoFontDescription *
-theme_adium_get_default_font (void)
-{
-       GConfClient *gconf_client;
-       PangoFontDescription *pango_fd;
-       gchar *gconf_font_family;
-
-       gconf_client = gconf_client_get_default ();
-       if (gconf_client == NULL) {
-               return NULL;
-       }
-       gconf_font_family = gconf_client_get_string (gconf_client,
-                    EMPATHY_GCONF_FONT_KEY_NAME,
-                    NULL);
-       if (gconf_font_family == NULL) {
-               g_object_unref (gconf_client);
-               return NULL;
-       }
-       pango_fd = pango_font_description_from_string (gconf_font_family);
-       g_free (gconf_font_family);
-       g_object_unref (gconf_client);
-       return pango_fd;
-}
-
-static void
-theme_adium_set_webkit_font (WebKitWebSettings *w_settings,
-                            const gchar *name,
-                            gint size)
-{
-       g_object_set (w_settings, "default-font-family", name, NULL);
-       g_object_set (w_settings, "default-font-size", size, NULL);
-}
-
-static void
-theme_adium_set_default_font (WebKitWebSettings *w_settings)
-{
-       PangoFontDescription *default_font_desc;
-       GdkScreen *current_screen;
-       gdouble dpi = 0;
-       gint pango_font_size = 0;
-
-       default_font_desc = theme_adium_get_default_font ();
-       if (default_font_desc == NULL)
-               return ;
-       pango_font_size = pango_font_description_get_size (default_font_desc)
-               / PANGO_SCALE ;
-       if (pango_font_description_get_size_is_absolute (default_font_desc)) {
-               current_screen = gdk_screen_get_default ();
-               if (current_screen != NULL) {
-                       dpi = gdk_screen_get_resolution (current_screen);
-               } else {
-                       dpi = BORING_DPI_DEFAULT;
-               }
-               pango_font_size = (gint) (pango_font_size / (dpi / 72));
-       }
-       theme_adium_set_webkit_font (w_settings,
-               pango_font_description_get_family (default_font_desc),
-               pango_font_size);
-       pango_font_description_free (default_font_desc);
-}
-
 static void
 theme_adium_constructed (GObject *object)
 {
        EmpathyThemeAdiumPriv *priv = GET_PRIV (object);
-       gchar                 *basedir_uri;
        const gchar           *font_family = NULL;
        gint                   font_size = 0;
        WebKitWebView         *webkit_view = WEBKIT_WEB_VIEW (object);
-       WebKitWebSettings     *webkit_settings;
        WebKitWebInspector    *webkit_inspector;
 
        /* Set default settings */
        font_family = tp_asv_get_string (priv->data->info, "DefaultFontFamily");
        font_size = tp_asv_get_int32 (priv->data->info, "DefaultFontSize", NULL);
-       webkit_settings = webkit_web_view_get_settings (webkit_view);
 
        if (font_family && font_size) {
-               theme_adium_set_webkit_font (webkit_settings, font_family, font_size);
+               g_object_set (webkit_web_view_get_settings (webkit_view),
+                       "default-font-family", font_family,
+                       "default-font-size", font_size,
+                       NULL);
        } else {
-               theme_adium_set_default_font (webkit_settings);
+               empathy_webkit_bind_font_setting (webkit_view,
+                       priv->gsettings_desktop,
+                       EMPATHY_PREFS_DESKTOP_INTERFACE_DOCUMENT_FONT_NAME);
        }
 
        /* Setup webkit inspector */
@@ -1095,11 +1510,9 @@ theme_adium_constructed (GObject *object)
                          object);
 
        /* Load template */
-       basedir_uri = g_strconcat ("file://", priv->data->basedir, NULL);
-       webkit_web_view_load_html_string (WEBKIT_WEB_VIEW (object),
-                                         priv->data->template_html,
-                                         basedir_uri);
-       g_free (basedir_uri);
+       theme_adium_load_template (EMPATHY_THEME_ADIUM (object));
+
+       priv->in_construction = FALSE;
 }
 
 static void
@@ -1114,6 +1527,9 @@ theme_adium_get_property (GObject    *object,
        case PROP_ADIUM_DATA:
                g_value_set_boxed (value, priv->data);
                break;
+       case PROP_VARIANT:
+               g_value_set_string (value, priv->variant);
+               break;
        default:
                G_OBJECT_WARN_INVALID_PROPERTY_ID (object, param_id, pspec);
                break;
@@ -1126,6 +1542,7 @@ theme_adium_set_property (GObject      *object,
                          const GValue *value,
                          GParamSpec   *pspec)
 {
+       EmpathyThemeAdium *theme = EMPATHY_THEME_ADIUM (object);
        EmpathyThemeAdiumPriv *priv = GET_PRIV (object);
 
        switch (param_id) {
@@ -1133,6 +1550,9 @@ theme_adium_set_property (GObject      *object,
                g_assert (priv->data == NULL);
                priv->data = g_value_dup_boxed (value);
                break;
+       case PROP_VARIANT:
+               empathy_theme_adium_set_variant (theme, g_value_get_string (value));
+               break;
        default:
                G_OBJECT_WARN_INVALID_PROPERTY_ID (object, param_id, pspec);
                break;
@@ -1162,6 +1582,15 @@ empathy_theme_adium_class_init (EmpathyThemeAdiumClass *klass)
                                                              G_PARAM_CONSTRUCT_ONLY |
                                                              G_PARAM_READWRITE |
                                                              G_PARAM_STATIC_STRINGS));
+       g_object_class_install_property (object_class,
+                                        PROP_VARIANT,
+                                        g_param_spec_string ("variant",
+                                                             "The theme variant",
+                                                             "Variant name for the theme",
+                                                             NULL,
+                                                             G_PARAM_CONSTRUCT |
+                                                             G_PARAM_READWRITE |
+                                                             G_PARAM_STATIC_STRINGS));
 
        g_type_class_add_private (object_class, sizeof (EmpathyThemeAdiumPriv));
 }
@@ -1174,6 +1603,9 @@ empathy_theme_adium_init (EmpathyThemeAdium *theme)
 
        theme->priv = priv;
 
+       priv->in_construction = TRUE;
+       g_queue_init (&priv->message_queue);
+       priv->allow_scrolling = TRUE;
        priv->smiley_manager = empathy_smiley_manager_dup_singleton ();
 
        g_signal_connect (theme, "load-finished",
@@ -1183,25 +1615,76 @@ empathy_theme_adium_init (EmpathyThemeAdium *theme)
                          G_CALLBACK (theme_adium_navigation_policy_decision_requested_cb),
                          NULL);
 
-       priv->notify_enable_webkit_developer_tools_id =
-               empathy_conf_notify_add (empathy_conf_get (),
-                                        EMPATHY_PREFS_CHAT_WEBKIT_DEVELOPER_TOOLS,
-                                        theme_adium_notify_enable_webkit_developer_tools_cb,
-                                        theme);
+       priv->gsettings_chat = g_settings_new (EMPATHY_PREFS_CHAT_SCHEMA);
+       priv->gsettings_desktop = g_settings_new (
+               EMPATHY_PREFS_DESKTOP_INTERFACE_SCHEMA);
+
+       g_signal_connect (priv->gsettings_chat,
+               "changed::" EMPATHY_PREFS_CHAT_WEBKIT_DEVELOPER_TOOLS,
+               G_CALLBACK (theme_adium_notify_enable_webkit_developer_tools_cb),
+               theme);
 
        theme_adium_update_enable_webkit_developer_tools (theme);
 }
 
 EmpathyThemeAdium *
-empathy_theme_adium_new (EmpathyAdiumData *data)
+empathy_theme_adium_new (EmpathyAdiumData *data,
+                        const gchar *variant)
 {
        g_return_val_if_fail (data != NULL, NULL);
 
        return g_object_new (EMPATHY_TYPE_THEME_ADIUM,
                             "adium-data", data,
+                            "variant", variant,
                             NULL);
 }
 
+void
+empathy_theme_adium_set_variant (EmpathyThemeAdium *theme,
+                                const gchar *variant)
+{
+       EmpathyThemeAdiumPriv *priv = GET_PRIV (theme);
+       gchar *variant_path;
+       gchar *script;
+
+       if (!tp_strdiff (priv->variant, variant)) {
+               return;
+       }
+
+       g_free (priv->variant);
+       priv->variant = g_strdup (variant);
+
+       if (priv->in_construction) {
+               return;
+       }
+
+       DEBUG ("Update view with variant: '%s'", variant);
+       variant_path = adium_info_dup_path_for_variant (priv->data->info,
+               priv->variant);
+       script = g_strdup_printf ("setStylesheet(\"mainStyle\",\"%s\");", variant_path);
+
+       webkit_web_view_execute_script (WEBKIT_WEB_VIEW (theme), script);
+
+       g_free (variant_path);
+       g_free (script);
+
+       g_object_notify (G_OBJECT (theme), "variant");
+}
+
+void
+empathy_theme_adium_show_inspector (EmpathyThemeAdium *theme)
+{
+       WebKitWebView      *web_view = WEBKIT_WEB_VIEW (theme);
+       WebKitWebInspector *inspector;
+
+       g_object_set (G_OBJECT (webkit_web_view_get_settings (web_view)),
+                     "enable-developer-extras", TRUE,
+                     NULL);
+
+       inspector = webkit_web_view_get_inspector (web_view);
+       webkit_web_inspector_show (inspector);
+}
+
 gboolean
 empathy_adium_path_is_valid (const gchar *path)
 {
@@ -1214,18 +1697,24 @@ empathy_adium_path_is_valid (const gchar *path)
        ret = g_file_test (file, G_FILE_TEST_EXISTS);
        g_free (file);
 
-       if (ret == FALSE)
-               return ret;
+       if (!ret)
+               return FALSE;
 
        /* We ship a default Template.html as fallback if there is any problem
         * with the one inside the theme. The only other required file is
-        * Content.html for incoming messages (outgoing fallback to use
-        * incoming). */
-       file = g_build_filename (path, "Contents", "Resources", "Incoming",
-                                "Content.html", NULL);
+        * Content.html OR Incoming/Content.html*/
+       file = g_build_filename (path, "Contents", "Resources", "Content.html",
+                                NULL);
        ret = g_file_test (file, G_FILE_TEST_EXISTS);
        g_free (file);
 
+       if (!ret) {
+               file = g_build_filename (path, "Contents", "Resources", "Incoming",
+                                        "Content.html", NULL);
+               ret = g_file_test (file, G_FILE_TEST_EXISTS);
+               g_free (file);
+       }
+
        return ret;
 }
 
@@ -1255,6 +1744,109 @@ empathy_adium_info_new (const gchar *path)
        return info;
 }
 
+static guint
+adium_info_get_version (GHashTable *info)
+{
+       return tp_asv_get_int32 (info, "MessageViewVersion", NULL);
+}
+
+static const gchar *
+adium_info_get_no_variant_name (GHashTable *info)
+{
+       const gchar *name = tp_asv_get_string (info, "DisplayNameForNoVariant");
+       return name ? name : _("Normal");
+}
+
+static gchar *
+adium_info_dup_path_for_variant (GHashTable *info,
+                                const gchar *variant)
+{
+       guint version = adium_info_get_version (info);
+       const gchar *no_variant = adium_info_get_no_variant_name (info);
+       GPtrArray *variants;
+       guint i;
+
+       if (version <= 2 && !tp_strdiff (variant, no_variant)) {
+               return g_strdup ("main.css");
+       }
+
+       variants = empathy_adium_info_get_available_variants (info);
+       if (variants->len == 0)
+               return g_strdup ("main.css");
+
+       /* Verify the variant exists, fallback to the first one */
+       for (i = 0; i < variants->len; i++) {
+               if (!tp_strdiff (variant, g_ptr_array_index (variants, i))) {
+                       break;
+               }
+       }
+       if (i == variants->len) {
+               DEBUG ("Variant %s does not exist", variant);
+               variant = g_ptr_array_index (variants, 0);
+       }
+
+       return g_strdup_printf ("Variants/%s.css", variant);
+
+}
+
+const gchar *
+empathy_adium_info_get_default_variant (GHashTable *info)
+{
+       if (adium_info_get_version (info) <= 2) {
+               return adium_info_get_no_variant_name (info);
+       }
+
+       return tp_asv_get_string (info, "DefaultVariant");
+}
+
+GPtrArray *
+empathy_adium_info_get_available_variants (GHashTable *info)
+{
+       GPtrArray *variants;
+       const gchar *path;
+       gchar *dirpath;
+       GDir *dir;
+
+       variants = tp_asv_get_boxed (info, "AvailableVariants", G_TYPE_PTR_ARRAY);
+       if (variants != NULL) {
+               return variants;
+       }
+
+       variants = g_ptr_array_new_with_free_func (g_free);
+       tp_asv_take_boxed (info, g_strdup ("AvailableVariants"),
+               G_TYPE_PTR_ARRAY, variants);
+
+       path = tp_asv_get_string (info, "path");
+       dirpath = g_build_filename (path, "Contents", "Resources", "Variants", NULL);
+       dir = g_dir_open (dirpath, 0, NULL);
+       if (dir != NULL) {
+               const gchar *name;
+
+               for (name = g_dir_read_name (dir);
+                    name != NULL;
+                    name = g_dir_read_name (dir)) {
+                       gchar *display_name;
+
+                       if (!g_str_has_suffix (name, ".css")) {
+                               continue;
+                       }
+
+                       display_name = g_strdup (name);
+                       strstr (display_name, ".css")[0] = '\0';
+                       g_ptr_array_add (variants, display_name);
+               }
+               g_dir_close (dir);
+       }
+       g_free (dirpath);
+
+       if (adium_info_get_version (info) <= 2) {
+               g_ptr_array_add (variants,
+                       g_strdup (adium_info_get_no_variant_name (info)));
+       }
+
+       return variants;
+}
+
 GType
 empathy_adium_data_get_type (void)
 {
@@ -1274,16 +1866,9 @@ EmpathyAdiumData  *
 empathy_adium_data_new_with_info (const gchar *path, GHashTable *info)
 {
        EmpathyAdiumData *data;
-       gchar            *file;
        gchar            *template_html = NULL;
-       gsize             template_len;
        gchar            *footer_html = NULL;
-       gsize             footer_len;
-       GString          *string;
-       gchar           **strv = NULL;
-       gchar            *css_path;
-       guint             len = 0;
-       guint             i = 0;
+       gchar            *tmp;
 
        g_return_val_if_fail (empathy_adium_path_is_valid (path), NULL);
 
@@ -1293,121 +1878,127 @@ empathy_adium_data_new_with_info (const gchar *path, GHashTable *info)
        data->basedir = g_strconcat (path, G_DIR_SEPARATOR_S "Contents"
                G_DIR_SEPARATOR_S "Resources" G_DIR_SEPARATOR_S, NULL);
        data->info = g_hash_table_ref (info);
+       data->version = adium_info_get_version (info);
+       data->strings_to_free = g_ptr_array_new_with_free_func (g_free);
+       data->date_format_cache = g_hash_table_new_full (g_str_hash,
+               g_str_equal, g_free, g_free);
+
+       DEBUG ("Loading theme at %s", path);
+
+#define LOAD(path, var) \
+               tmp = g_build_filename (data->basedir, path, NULL); \
+               g_file_get_contents (tmp, &var, NULL, NULL); \
+               g_free (tmp); \
+
+#define LOAD_CONST(path, var) \
+       { \
+               gchar *content; \
+               LOAD (path, content); \
+               if (content != NULL) { \
+                       g_ptr_array_add (data->strings_to_free, content); \
+               } \
+               var = content; \
+       }
 
        /* Load html files */
-       file = g_build_filename (data->basedir, "Incoming", "Content.html", NULL);
-       g_file_get_contents (file, &data->in_content_html, &data->in_content_len, NULL);
-       g_free (file);
-
-       file = g_build_filename (data->basedir, "Incoming", "NextContent.html", NULL);
-       g_file_get_contents (file, &data->in_nextcontent_html, &data->in_nextcontent_len, NULL);
-       g_free (file);
+       LOAD_CONST ("Content.html", data->content_html);
+       LOAD_CONST ("Incoming/Content.html", data->in_content_html);
+       LOAD_CONST ("Incoming/NextContent.html", data->in_nextcontent_html);
+       LOAD_CONST ("Incoming/Context.html", data->in_context_html);
+       LOAD_CONST ("Incoming/NextContext.html", data->in_nextcontext_html);
+       LOAD_CONST ("Outgoing/Content.html", data->out_content_html);
+       LOAD_CONST ("Outgoing/NextContent.html", data->out_nextcontent_html);
+       LOAD_CONST ("Outgoing/Context.html", data->out_context_html);
+       LOAD_CONST ("Outgoing/NextContext.html", data->out_nextcontext_html);
+       LOAD_CONST ("Status.html", data->status_html);
+       LOAD ("Template.html", template_html);
+       LOAD ("Footer.html", footer_html);
+
+#undef LOAD_CONST
+#undef LOAD
+
+       /* HTML fallbacks: If we have at least content OR in_content, then
+        * everything else gets a fallback */
+
+#define FALLBACK(html, fallback) \
+       if (html == NULL) { \
+               html = fallback; \
+       }
 
-       file = g_build_filename (data->basedir, "Incoming", "Context.html", NULL);
-       g_file_get_contents (file, &data->in_context_html, &data->in_context_len, NULL);
-       g_free (file);
+       /* in_nextcontent -> in_content -> content */
+       FALLBACK (data->in_content_html,      data->content_html);
+       FALLBACK (data->in_nextcontent_html,  data->in_content_html);
 
-       file = g_build_filename (data->basedir, "Incoming", "NextContext.html", NULL);
-       g_file_get_contents (file, &data->in_nextcontext_html, &data->in_nextcontext_len, NULL);
-       g_free (file);
+       /* context -> content */
+       FALLBACK (data->in_context_html,      data->in_content_html);
+       FALLBACK (data->in_nextcontext_html,  data->in_nextcontent_html);
+       FALLBACK (data->out_context_html,     data->out_content_html);
+       FALLBACK (data->out_nextcontext_html, data->out_nextcontent_html);
 
-       file = g_build_filename (data->basedir, "Outgoing", "Content.html", NULL);
-       g_file_get_contents (file, &data->out_content_html, &data->out_content_len, NULL);
-       g_free (file);
+       /* out -> in */
+       FALLBACK (data->out_content_html,     data->in_content_html);
+       FALLBACK (data->out_nextcontent_html, data->in_nextcontent_html);
+       FALLBACK (data->out_context_html,     data->in_context_html);
+       FALLBACK (data->out_nextcontext_html, data->in_nextcontext_html);
 
-       file = g_build_filename (data->basedir, "Outgoing", "NextContent.html", NULL);
-       g_file_get_contents (file, &data->out_nextcontent_html, &data->out_nextcontent_len, NULL);
-       g_free (file);
+       /* status -> in_content */
+       FALLBACK (data->status_html,          data->in_content_html);
 
-       file = g_build_filename (data->basedir, "Outgoing", "Context.html", NULL);
-       g_file_get_contents (file, &data->out_context_html, &data->out_context_len, NULL);
-       g_free (file);
+#undef FALLBACK
 
-       file = g_build_filename (data->basedir, "Outgoing", "NextContext.html", NULL);
-       g_file_get_contents (file, &data->out_nextcontext_html, &data->out_nextcontext_len, NULL);
-       g_free (file);
+       /* template -> empathy's template */
+       data->custom_template = (template_html != NULL);
+       if (template_html == NULL) {
+               GError *error = NULL;
 
-       file = g_build_filename (data->basedir, "Status.html", NULL);
-       g_file_get_contents (file, &data->status_html, &data->status_len, NULL);
-       g_free (file);
+               tmp = empathy_file_lookup ("Template.html", "data");
 
-       file = g_build_filename (data->basedir, "Footer.html", NULL);
-       g_file_get_contents (file, &footer_html, &footer_len, NULL);
-       g_free (file);
+               if (!g_file_get_contents (tmp, &template_html, NULL, &error)) {
+                       g_warning ("couldn't load Empathy's default theme "
+                               "template: %s", error->message);
+                       g_return_val_if_reached (data);
+               }
 
-       file = g_build_filename (data->basedir, "Incoming", "buddy_icon.png", NULL);
-       if (g_file_test (file, G_FILE_TEST_EXISTS | G_FILE_TEST_IS_REGULAR)) {
-               data->default_incoming_avatar_filename = file;
-       } else {
-               g_free (file);
+               g_free (tmp);
        }
 
-       file = g_build_filename (data->basedir, "Outgoing", "buddy_icon.png", NULL);
-       if (g_file_test (file, G_FILE_TEST_EXISTS | G_FILE_TEST_IS_REGULAR)) {
-               data->default_outgoing_avatar_filename = file;
+       /* Default avatar */
+       tmp = g_build_filename (data->basedir, "Incoming", "buddy_icon.png", NULL);
+       if (g_file_test (tmp, G_FILE_TEST_EXISTS | G_FILE_TEST_IS_REGULAR)) {
+               data->default_incoming_avatar_filename = tmp;
        } else {
-               g_free (file);
+               g_free (tmp);
        }
-
-       css_path = g_build_filename (data->basedir, "main.css", NULL);
-
-       /* There is 2 formats for Template.html: The old one has 4 parameters,
-        * the new one has 5 parameters. */
-       file = g_build_filename (data->basedir, "Template.html", NULL);
-       if (g_file_get_contents (file, &template_html, &template_len, NULL)) {
-               strv = g_strsplit (template_html, "%@", -1);
-               len = g_strv_length (strv);
+       tmp = g_build_filename (data->basedir, "Outgoing", "buddy_icon.png", NULL);
+       if (g_file_test (tmp, G_FILE_TEST_EXISTS | G_FILE_TEST_IS_REGULAR)) {
+               data->default_outgoing_avatar_filename = tmp;
+       } else {
+               g_free (tmp);
        }
-       g_free (file);
-
-       if (len != 5 && len != 6) {
-               /* Either the theme has no template or it don't have the good
-                * number of parameters. Fallback to use our own template. */
-               g_free (template_html);
-               g_strfreev (strv);
 
-               file = empathy_file_lookup ("Template.html", "data");
-               g_file_get_contents (file, &template_html, &template_len, NULL);
-               g_free (file);
-               strv = g_strsplit (template_html, "%@", -1);
-               len = g_strv_length (strv);
-       }
-
-       /* Replace %@ with the needed information in the template html. */
-       string = g_string_sized_new (template_len);
-       g_string_append (string, strv[i++]);
-       g_string_append (string, data->basedir);
-       g_string_append (string, strv[i++]);
-       if (len == 6) {
-               const gchar *variant;
-
-               /* We include main.css by default */
-               g_string_append_printf (string, "@import url(\"%s\");", css_path);
-               g_string_append (string, strv[i++]);
-               variant = tp_asv_get_string (data->info, "DefaultVariant");
-               if (variant) {
-                       g_string_append (string, "Variants/");
-                       g_string_append (string, variant);
-                       g_string_append (string, ".css");
-               }
+       /* Old custom templates had only 4 parameters.
+        * New templates have 5 parameters */
+       if (data->version <= 2 && data->custom_template) {
+               tmp = string_with_format (template_html,
+                       data->basedir,
+                       "%@", /* Leave variant unset */
+                       "", /* The header */
+                       footer_html ? footer_html : "",
+                       NULL);
        } else {
-               /* FIXME: We should set main.css OR the variant css */
-               g_string_append (string, css_path);
-       }
-       g_string_append (string, strv[i++]);
-       g_string_append (string, ""); /* We don't want header */
-       g_string_append (string, strv[i++]);
-       /* FIXME: We should replace adium %macros% in footer */
-       if (footer_html) {
-               g_string_append (string, footer_html);
+               tmp = string_with_format (template_html,
+                       data->basedir,
+                       data->version <= 2 ? "" : "@import url( \"main.css\" );",
+                       "%@", /* Leave variant unset */
+                       "", /* The header */
+                       footer_html ? footer_html : "",
+                       NULL);
        }
-       g_string_append (string, strv[i++]);
-       data->template_html = g_string_free (string, FALSE);
+       g_ptr_array_add (data->strings_to_free, tmp);
+       data->template_html = tmp;
 
-       g_free (footer_html);
        g_free (template_html);
-       g_free (css_path);
-       g_strfreev (strv);
+       g_free (footer_html);
 
        return data;
 }
@@ -1443,20 +2034,13 @@ empathy_adium_data_unref (EmpathyAdiumData *data)
        if (g_atomic_int_dec_and_test (&data->ref_count)) {
                g_free (data->path);
                g_free (data->basedir);
-               g_free (data->template_html);
-               g_free (data->in_content_html);
-               g_free (data->in_nextcontent_html);
-               g_free (data->in_context_html);
-               g_free (data->in_nextcontext_html);
-               g_free (data->out_content_html);
-               g_free (data->out_nextcontent_html);
-               g_free (data->out_context_html);
-               g_free (data->out_nextcontext_html);
                g_free (data->default_avatar_filename);
                g_free (data->default_incoming_avatar_filename);
                g_free (data->default_outgoing_avatar_filename);
-               g_free (data->status_html);
                g_hash_table_unref (data->info);
+               g_ptr_array_unref (data->strings_to_free);
+               tp_clear_pointer (&data->date_format_cache, g_hash_table_unref);
+
                g_slice_free (EmpathyAdiumData, data);
        }
 }