]> git.0d.be Git - empathy.git/blob - libempathy-gtk/empathy-theme-adium.c
remove released flag
[empathy.git] / libempathy-gtk / empathy-theme-adium.c
1 /*
2  * Copyright (C) 2008-2012 Collabora Ltd.
3  * Copyright (C) 2012 Red Hat, Inc.
4  *
5  * This library is free software; you can redistribute it and/or
6  * modify it under the terms of the GNU Lesser General Public
7  * License as published by the Free Software Foundation; either
8  * version 2.1 of the License, or (at your option) any later version.
9  *
10  * This library is distributed in the hope that it will be useful,
11  * but WITHOUT ANY WARRANTY; without even the implied warranty of
12  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
13  * Lesser General Public License for more details.
14  *
15  * You should have received a copy of the GNU Lesser General Public
16  * License along with this library; if not, write to the Free Software
17  * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
18  *
19  * Authors: Xavier Claessens <xclaesse@gmail.com>
20  */
21
22 #include "config.h"
23 #include "empathy-theme-adium.h"
24
25 #include <glib/gi18n-lib.h>
26 #include <tp-account-widgets/tpaw-images.h>
27 #include <tp-account-widgets/tpaw-time.h>
28 #include <tp-account-widgets/tpaw-pixbuf-utils.h>
29 #include <tp-account-widgets/tpaw-utils.h>
30
31 #include "empathy-gsettings.h"
32 #include "empathy-images.h"
33 #include "empathy-plist.h"
34 #include "empathy-smiley-manager.h"
35 #include "empathy-ui-utils.h"
36 #include "empathy-utils.h"
37 #include "empathy-webkit-utils.h"
38
39 #define DEBUG_FLAG EMPATHY_DEBUG_CHAT
40 #include "empathy-debug.h"
41
42 #define BORING_DPI_DEFAULT 96
43
44 /* "Join" consecutive messages with timestamps within five minutes */
45 #define MESSAGE_JOIN_PERIOD 5*60
46
47 struct _EmpathyThemeAdiumPriv
48 {
49   EmpathyAdiumData *data;
50   EmpathySmileyManager *smiley_manager;
51   EmpathyContact *first_contact;
52   EmpathyContact *last_contact;
53   gint64 first_timestamp;
54   gint64 last_timestamp;
55   gboolean first_is_backlog;
56   gboolean last_is_backlog;
57   guint pages_loading;
58   /* Queue of QueuedItem*s containing an EmpathyMessage or string */
59   GQueue message_queue;
60   /* Queue of guint32 of pending message id to remove unread
61    * marker for when we lose focus. */
62   GQueue acked_messages;
63   GtkWidget *inspector_window;
64
65   GSettings *gsettings_chat;
66   GSettings *gsettings_desktop;
67
68   gboolean has_focus;
69   gboolean has_unread_message;
70   gboolean allow_scrolling;
71   gchar *variant;
72   gboolean in_construction;
73   gboolean show_avatars;
74 };
75
76 struct _EmpathyAdiumData
77 {
78   gint ref_count;
79   gchar *path;
80   gchar *basedir;
81   gchar *default_avatar_filename;
82   gchar *default_incoming_avatar_filename;
83   gchar *default_outgoing_avatar_filename;
84   GHashTable *info;
85   guint version;
86   gboolean custom_template;
87   /* gchar* -> gchar* both owned */
88   GHashTable *date_format_cache;
89
90   /* HTML bits */
91   const gchar *template_html;
92   const gchar *content_html;
93   const gchar *in_content_html;
94   const gchar *in_context_html;
95   const gchar *in_nextcontent_html;
96   const gchar *in_nextcontext_html;
97   const gchar *out_content_html;
98   const gchar *out_context_html;
99   const gchar *out_nextcontent_html;
100   const gchar *out_nextcontext_html;
101   const gchar *status_html;
102
103   /* Above html strings are pointers to strings stored in this array.
104    * We do this because of fallbacks, some htmls could be pointing the
105    * same string. */
106   GPtrArray *strings_to_free;
107 };
108
109 static gchar * adium_info_dup_path_for_variant (GHashTable *info,
110     const gchar *variant);
111
112 enum
113 {
114   PROP_0,
115   PROP_ADIUM_DATA,
116   PROP_VARIANT,
117 };
118
119 G_DEFINE_TYPE (EmpathyThemeAdium, empathy_theme_adium,
120        WEBKIT_TYPE_WEB_VIEW)
121
122 enum
123 {
124   QUEUED_EVENT,
125   QUEUED_MESSAGE,
126   QUEUED_EDIT
127 };
128
129 typedef struct
130 {
131   guint type;
132   EmpathyMessage *msg;
133   char *str;
134   gboolean should_highlight;
135 } QueuedItem;
136
137 static QueuedItem *
138 queue_item (GQueue *queue,
139     guint type,
140     EmpathyMessage *msg,
141     const char *str,
142     gboolean should_highlight,
143     gboolean prepend)
144 {
145   QueuedItem *item = g_slice_new0 (QueuedItem);
146
147   item->type = type;
148   if (msg != NULL)
149     item->msg = g_object_ref (msg);
150   item->str = g_strdup (str);
151   item->should_highlight = should_highlight;
152
153   if (prepend)
154     g_queue_push_head (queue, item);
155   else
156     g_queue_push_tail (queue, item);
157
158   return item;
159 }
160
161 static void
162 free_queued_item (QueuedItem *item)
163 {
164   tp_clear_object (&item->msg);
165   g_free (item->str);
166
167   g_slice_free (QueuedItem, item);
168 }
169
170 static gboolean
171 theme_adium_navigation_policy_decision_requested_cb (WebKitWebView *view,
172     WebKitWebFrame *web_frame,
173     WebKitNetworkRequest *request,
174     WebKitWebNavigationAction *action,
175     WebKitWebPolicyDecision *decision,
176     gpointer data)
177 {
178   const gchar *uri;
179
180   /* Only call url_show on clicks */
181   if (webkit_web_navigation_action_get_reason (action) !=
182       WEBKIT_WEB_NAVIGATION_REASON_LINK_CLICKED)
183     {
184       webkit_web_policy_decision_use (decision);
185       return TRUE;
186     }
187
188   uri = webkit_network_request_get_uri (request);
189   empathy_url_show (GTK_WIDGET (view), uri);
190
191   webkit_web_policy_decision_ignore (decision);
192   return TRUE;
193 }
194
195 /* Replace each %@ in format with string passed in args */
196 static gchar *
197 string_with_format (const gchar *format,
198     const gchar *first_string,
199     ...)
200 {
201   va_list args;
202   const gchar *str;
203   GString *result;
204
205   va_start (args, first_string);
206   result = g_string_sized_new (strlen (format));
207   for (str = first_string; str != NULL; str = va_arg (args, const gchar *))
208     {
209       const gchar *next;
210
211       next = strstr (format, "%@");
212       if (next == NULL)
213         break;
214
215       g_string_append_len (result, format, next - format);
216       g_string_append (result, str);
217       format = next + 2;
218     }
219   g_string_append (result, format);
220   va_end (args);
221
222   return g_string_free (result, FALSE);
223 }
224
225 static void
226 theme_adium_load_template (EmpathyThemeAdium *self)
227 {
228   gchar *basedir_uri;
229   gchar *variant_path;
230   gchar *template;
231
232   self->priv->pages_loading++;
233   basedir_uri = g_strconcat ("file://", self->priv->data->basedir, NULL);
234
235   variant_path = adium_info_dup_path_for_variant (self->priv->data->info,
236       self->priv->variant);
237
238   template = string_with_format (self->priv->data->template_html,
239     variant_path, NULL);
240
241   webkit_web_view_load_html_string (WEBKIT_WEB_VIEW (self),
242       template, basedir_uri);
243
244   g_free (basedir_uri);
245   g_free (variant_path);
246   g_free (template);
247 }
248
249 static gchar *
250 theme_adium_parse_body (EmpathyThemeAdium *self,
251   const gchar *text,
252   const gchar *token)
253 {
254   TpawStringParser *parsers;
255   GString *string;
256
257   /* Check if we have to parse smileys */
258   parsers = empathy_webkit_get_string_parser (
259     g_settings_get_boolean (self->priv->gsettings_chat,
260       EMPATHY_PREFS_CHAT_SHOW_SMILEYS));
261
262   /* Parse text and construct string with links and smileys replaced
263    * by html tags. Also escape text to make sure html code is
264    * displayed verbatim. */
265   string = g_string_sized_new (strlen (text));
266
267   /* wrap this in HTML that allows us to find the message for later
268    * editing */
269   if (!tp_str_empty (token))
270     g_string_append_printf (string,
271       "<span id=\"message-token-%s\">",
272       token);
273
274   tpaw_string_parser_substr (text, -1, parsers, string);
275
276   if (!tp_str_empty (token))
277     g_string_append (string, "</span>");
278
279   /* Wrap body in order to make tabs and multiple spaces displayed
280    * properly. See bug #625745. */
281   g_string_prepend (string, "<div style=\"display: inline; "
282                  "white-space: pre-wrap\"'>");
283   g_string_append (string, "</div>");
284
285   return g_string_free (string, FALSE);
286 }
287
288 static void
289 escape_and_append_len (GString *string, const gchar *str, gint len)
290 {
291   while (str != NULL && *str != '\0' && len != 0)
292     {
293       switch (*str)
294         {
295           case '\\':
296             /* \ becomes \\ */
297             g_string_append (string, "\\\\");
298             break;
299           case '\"':
300             /* " becomes \" */
301             g_string_append (string, "\\\"");
302             break;
303           case '\n':
304             /* Remove end of lines */
305             break;
306           default:
307             g_string_append_c (string, *str);
308         }
309
310       str++;
311       len--;
312     }
313 }
314
315 /* If *str starts with match, returns TRUE and move pointer to the end */
316 static gboolean
317 theme_adium_match (const gchar **str,
318     const gchar *match)
319 {
320   gint len;
321
322   len = strlen (match);
323   if (strncmp (*str, match, len) == 0)
324     {
325       *str += len - 1;
326       return TRUE;
327     }
328
329   return FALSE;
330 }
331
332 /* Like theme_adium_match() but also return the X part if match is
333  * like %foo{X}% */
334 static gboolean
335 theme_adium_match_with_format (const gchar **str,
336     const gchar *match,
337     gchar **format)
338 {
339   const gchar *cur = *str;
340   const gchar *end;
341
342   if (!theme_adium_match (&cur, match))
343     return FALSE;
344
345   cur++;
346
347   end = strstr (cur, "}%");
348   if (!end)
349     return FALSE;
350
351   *format = g_strndup (cur , end - cur);
352   *str = end + 1;
353   return TRUE;
354 }
355
356 /* List of colors used by %senderColor%. Copied from
357  * adium/Frameworks/AIUtilities\ Framework/Source/AIColorAdditions.m
358  */
359 static gchar *colors[] = {
360   "aqua", "aquamarine", "blue", "blueviolet", "brown", "burlywood", "cadetblue",
361   "chartreuse", "chocolate", "coral", "cornflowerblue", "crimson", "cyan",
362   "darkblue", "darkcyan", "darkgoldenrod", "darkgreen", "darkgrey", "darkkhaki",
363   "darkmagenta", "darkolivegreen", "darkorange", "darkorchid", "darkred",
364   "darksalmon", "darkseagreen", "darkslateblue", "darkslategrey",
365   "darkturquoise", "darkviolet", "deeppink", "deepskyblue", "dimgrey",
366   "dodgerblue", "firebrick", "forestgreen", "fuchsia", "gold", "goldenrod",
367   "green", "greenyellow", "grey", "hotpink", "indianred", "indigo", "lawngreen",
368   "lightblue", "lightcoral",
369   "lightgreen", "lightgrey", "lightpink", "lightsalmon", "lightseagreen",
370   "lightskyblue", "lightslategrey", "lightsteelblue", "lime", "limegreen",
371   "magenta", "maroon", "mediumaquamarine", "mediumblue", "mediumorchid",
372   "mediumpurple", "mediumseagreen", "mediumslateblue", "mediumspringgreen",
373   "mediumturquoise", "mediumvioletred", "midnightblue", "navy", "olive",
374   "olivedrab", "orange", "orangered", "orchid", "palegreen", "paleturquoise",
375   "palevioletred", "peru", "pink", "plum", "powderblue", "purple", "red",
376   "rosybrown", "royalblue", "saddlebrown", "salmon", "sandybrown", "seagreen",
377   "sienna", "silver", "skyblue", "slateblue", "slategrey", "springgreen",
378   "steelblue", "tan", "teal", "thistle", "tomato", "turquoise", "violet",
379   "yellowgreen",
380 };
381
382 static const gchar *
383 nsdate_to_strftime (EmpathyAdiumData *data, const gchar *nsdate)
384 {
385   /* Convert from NSDateFormatter
386    * (http://www.stepcase.com/blog/2008/12/02/format-string-for-the-iphone-nsdateformatter/)
387    * to strftime supported by g_date_time_format.
388    * FIXME: table is incomplete, doc of g_date_time_format has a table of
389    *        supported tags.
390    * FIXME: g_date_time_format in GLib 2.28 does 0 padding by default, but
391    *        in 2.29.x we have to explictely request padding with %0x */
392   static const gchar *convert_table[] = {
393     "a", "%p", // AM/PM
394     "A", NULL, // 0~86399999 (Millisecond of Day)
395
396     "cccc", "%A", // Sunday/Monday/Tuesday/Wednesday/Thursday/Friday/Saturday
397     "ccc", "%a", // Sun/Mon/Tue/Wed/Thu/Fri/Sat
398     "cc", "%u", // 1~7 (Day of Week)
399     "c", "%u", // 1~7 (Day of Week)
400
401     "dd", "%d", // 1~31 (0 padded Day of Month)
402     "d", "%d", // 1~31 (0 padded Day of Month)
403     "D", "%j", // 1~366 (0 padded Day of Year)
404
405     "e", "%u", // 1~7 (0 padded Day of Week)
406     "EEEE", "%A", // Sunday/Monday/Tuesday/Wednesday/Thursday/Friday/Saturday
407     "EEE", "%a", // Sun/Mon/Tue/Wed/Thu/Fri/Sat
408     "EE", "%a", // Sun/Mon/Tue/Wed/Thu/Fri/Sat
409     "E", "%a", // Sun/Mon/Tue/Wed/Thu/Fri/Sat
410
411     "F", NULL, // 1~5 (0 padded Week of Month, first day of week = Monday)
412
413     "g", NULL, // Julian Day Number (number of days since 4713 BC January 1)
414     "GGGG", NULL, // Before Christ/Anno Domini
415     "GGG", NULL, // BC/AD (Era Designator Abbreviated)
416     "GG", NULL, // BC/AD (Era Designator Abbreviated)
417     "G", NULL, // BC/AD (Era Designator Abbreviated)
418
419     "h", "%I", // 1~12 (0 padded Hour (12hr))
420     "H", "%H", // 0~23 (0 padded Hour (24hr))
421
422     "k", NULL, // 1~24 (0 padded Hour (24hr)
423     "K", NULL, // 0~11 (0 padded Hour (12hr))
424
425     "LLLL", "%B", // January/February/March/April/May/June/July/August/September/October/November/December
426     "LLL", "%b", // Jan/Feb/Mar/Apr/May/Jun/Jul/Aug/Sep/Oct/Nov/Dec
427     "LL", "%m", // 1~12 (0 padded Month)
428     "L", "%m", // 1~12 (0 padded Month)
429
430     "m", "%M", // 0~59 (0 padded Minute)
431     "MMMM", "%B", // January/February/March/April/May/June/July/August/September/October/November/December
432     "MMM", "%b", // Jan/Feb/Mar/Apr/May/Jun/Jul/Aug/Sep/Oct/Nov/Dec
433     "MM", "%m", // 1~12 (0 padded Month)
434     "M", "%m", // 1~12 (0 padded Month)
435
436     "qqqq", NULL, // 1st quarter/2nd quarter/3rd quarter/4th quarter
437     "qqq", NULL, // Q1/Q2/Q3/Q4
438     "qq", NULL, // 1~4 (0 padded Quarter)
439     "q", NULL, // 1~4 (0 padded Quarter)
440     "QQQQ", NULL, // 1st quarter/2nd quarter/3rd quarter/4th quarter
441     "QQQ", NULL, // Q1/Q2/Q3/Q4
442     "QQ", NULL, // 1~4 (0 padded Quarter)
443     "Q", NULL, // 1~4 (0 padded Quarter)
444
445     "s", "%S", // 0~59 (0 padded Second)
446     "S", NULL, // (rounded Sub-Second)
447
448     "u", "%Y", // (0 padded Year)
449
450     "vvvv", "%Z", // (General GMT Timezone Name)
451     "vvv", "%Z", // (General GMT Timezone Abbreviation)
452     "vv", "%Z", // (General GMT Timezone Abbreviation)
453     "v", "%Z", // (General GMT Timezone Abbreviation)
454
455     "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)
456     "W", NULL, // 1~5 (0 padded Week of Month, 1st day of week = Sunday)
457
458     "yyyy", "%Y", // (Full Year)
459     "yyy", "%y", // (2 Digits Year)
460     "yy", "%y", // (2 Digits Year)
461     "y", "%Y", // (Full Year)
462     "YYYY", NULL, // (Full Year, starting from the Sunday of the 1st week of year)
463     "YYY", NULL, // (2 Digits Year, starting from the Sunday of the 1st week of year)
464     "YY", NULL, // (2 Digits Year, starting from the Sunday of the 1st week of year)
465     "Y", NULL, // (Full Year, starting from the Sunday of the 1st week of year)
466
467     "zzzz", NULL, // (Specific GMT Timezone Name)
468     "zzz", NULL, // (Specific GMT Timezone Abbreviation)
469     "zz", NULL, // (Specific GMT Timezone Abbreviation)
470     "z", NULL, // (Specific GMT Timezone Abbreviation)
471     "Z", "%z", // +0000 (RFC 822 Timezone)
472   };
473   const gchar *str;
474   GString *string;
475   guint i, j;
476
477   if (nsdate == NULL)
478     return NULL;
479
480   str = g_hash_table_lookup (data->date_format_cache, nsdate);
481   if (str != NULL) {
482     return str;
483   }
484
485   /* Copy nsdate into string, replacing occurences of NSDateFormatter tags
486    * by corresponding strftime tag. */
487   string = g_string_sized_new (strlen (nsdate));
488   for (i = 0; nsdate[i] != '\0'; i++)
489     {
490       gboolean found = FALSE;
491
492       /* even indexes are NSDateFormatter tag, odd indexes are the
493        * corresponding strftime tag */
494       for (j = 0; j < G_N_ELEMENTS (convert_table); j += 2)
495         {
496           if (g_str_has_prefix (nsdate + i, convert_table[j]))
497             {
498               found = TRUE;
499               break;
500             }
501         }
502
503       if (found)
504         {
505           /* If we don't have a replacement, just ignore that tag */
506           if (convert_table[j + 1] != NULL)
507             g_string_append (string, convert_table[j + 1]);
508
509           i += strlen (convert_table[j]) - 1;
510         }
511       else
512         {
513           g_string_append_c (string, nsdate[i]);
514         }
515     }
516
517   DEBUG ("Date format converted '%s' â†’ '%s'", nsdate, string->str);
518
519   /* The cache takes ownership of string->str */
520   g_hash_table_insert (data->date_format_cache, g_strdup (nsdate), string->str);
521   return g_string_free (string, FALSE);
522 }
523
524 static void
525 theme_adium_add_html (EmpathyThemeAdium *self,
526     const gchar *func,
527     const gchar *html,
528     const gchar *message,
529     const gchar *avatar_filename,
530     const gchar *name,
531     const gchar *contact_id,
532     const gchar *service_name,
533     const gchar *message_classes,
534     gint64 timestamp,
535     gboolean is_backlog,
536     gboolean outgoing,
537     PangoDirection direction)
538 {
539   GBytes *bytes;
540   GString *string;
541   const gchar *cur = NULL;
542   const gchar *js;
543   gchar *script;
544
545   /* Make some search-and-replace in the html code */
546   string = g_string_sized_new (strlen (html) + strlen (message));
547   g_string_append_printf (string, "%s(\"", func);
548
549   for (cur = html; *cur != '\0'; cur++)
550     {
551       const gchar *replace = NULL;
552       gchar *dup_replace = NULL;
553       gchar *format = NULL;
554
555       /* Those are all well known keywords that needs replacement in
556        * html files. Please keep them in the same order than the adium
557        * spec. See http://trac.adium.im/wiki/CreatingMessageStyles */
558       if (theme_adium_match (&cur, "%userIconPath%"))
559         {
560           replace = avatar_filename;
561         }
562       else if (theme_adium_match (&cur, "%senderScreenName%"))
563         {
564           replace = contact_id;
565         }
566       else if (theme_adium_match (&cur, "%sender%"))
567         {
568           replace = name;
569         }
570       else if (theme_adium_match (&cur, "%senderColor%"))
571         {
572           /* A color derived from the user's name.
573            * FIXME: If a colon separated list of HTML colors is at
574            * Incoming/SenderColors.txt it will be used instead of
575            * the default colors.
576            */
577
578           /* Ensure we always use the same color when sending messages
579            * (bgo #658821) */
580           if (outgoing)
581             {
582               replace = "inherit";
583             }
584           else if (contact_id != NULL)
585             {
586               guint hash = g_str_hash (contact_id);
587               replace = colors[hash % G_N_ELEMENTS (colors)];
588             }
589         }
590       else if (theme_adium_match (&cur, "%senderStatusIcon%"))
591         {
592           /* FIXME: The path to the status icon of the sender
593            * (available, away, etc...)
594            */
595         }
596       else if (theme_adium_match (&cur, "%messageDirection%"))
597         {
598           switch (direction)
599             {
600               case PANGO_DIRECTION_LTR:
601               case PANGO_DIRECTION_TTB_LTR:
602               case PANGO_DIRECTION_WEAK_LTR:
603                 replace = "ltr";
604                 break;
605               case PANGO_DIRECTION_RTL:
606               case PANGO_DIRECTION_TTB_RTL:
607               case PANGO_DIRECTION_WEAK_RTL:
608                 replace = "rtl";
609                 break;
610               case PANGO_DIRECTION_NEUTRAL:
611               default:
612                 break;
613             }
614         }
615       else if (theme_adium_match (&cur, "%senderDisplayName%"))
616         {
617           /* FIXME: The serverside (remotely set) name of the
618            * sender, such as an MSN display name.
619            *
620            *  We don't have access to that yet so we use
621            * local alias instead.
622            */
623           replace = name;
624         }
625       else if (theme_adium_match (&cur, "%senderPrefix%"))
626         {
627           /* FIXME: If we supported IRC user mode flags, this
628            * would be replaced with @ if the user is an op, + if
629            * the user has voice, etc. as per
630            * http://hg.adium.im/adium/rev/b586b027de42. But we
631            * don't, so for now we just strip it. */
632         }
633       else if (theme_adium_match_with_format (&cur, "%textbackgroundcolor{",
634             &format))
635         {
636           /* FIXME: This keyword is used to represent the
637            * highlight background color. "X" is the opacity of the
638            * background, ranges from 0 to 1 and can be any decimal
639            * between.
640            */
641         }
642       else if (theme_adium_match (&cur, "%message%"))
643         {
644           replace = message;
645         }
646       else if (theme_adium_match (&cur, "%time%") ||
647            theme_adium_match_with_format (&cur, "%time{", &format))
648         {
649           const gchar *strftime_format;
650
651           strftime_format = nsdate_to_strftime (self->priv->data, format);
652           if (is_backlog)
653             dup_replace = tpaw_time_to_string_local (timestamp,
654               strftime_format ? strftime_format :
655               TPAW_TIME_DATE_FORMAT_DISPLAY_SHORT);
656           else
657             dup_replace = tpaw_time_to_string_local (timestamp,
658               strftime_format ? strftime_format :
659               TPAW_TIME_FORMAT_DISPLAY_SHORT);
660
661           replace = dup_replace;
662         }
663       else if (theme_adium_match (&cur, "%shortTime%"))
664         {
665           dup_replace = tpaw_time_to_string_local (timestamp,
666             TPAW_TIME_FORMAT_DISPLAY_SHORT);
667           replace = dup_replace;
668         }
669       else if (theme_adium_match (&cur, "%service%"))
670         {
671           replace = service_name;
672         }
673       else if (theme_adium_match (&cur, "%variant%"))
674         {
675           /* FIXME: The name of the active message style variant,
676            * with all spaces replaced with an underscore.
677            * A variant named "Alternating Messages - Blue Red"
678            * will become "Alternating_Messages_-_Blue_Red".
679            */
680         }
681       else if (theme_adium_match (&cur, "%userIcons%"))
682         {
683           replace = self->priv->show_avatars ? "showIcons" : "hideIcons";
684         }
685       else if (theme_adium_match (&cur, "%messageClasses%"))
686         {
687           replace = message_classes;
688         }
689       else if (theme_adium_match (&cur, "%status%"))
690         {
691           /* FIXME: A description of the status event. This is
692            * neither in the user's local language nor expected to
693            * be displayed; it may be useful to use a different div
694            * class to present different types of status messages.
695            * The following is a list of some of the more important
696            * status messages; your message style should be able to
697            * handle being shown a status message not in this list,
698            * as even at present the list is incomplete and is
699            * certain to become out of date in the future:
700            *  online
701            *  offline
702            *  away
703            *  away_message
704            *  return_away
705            *  idle
706            *  return_idle
707            *  date_separator
708            *  contact_joined (group chats)
709            *  contact_left
710            *  error
711            *  timed_out
712            *  encryption (all OTR messages use this status)
713            *  purple (all IRC topic and join/part messages use this status)
714            *  fileTransferStarted
715            *  fileTransferCompleted
716            */
717         }
718       else
719         {
720           escape_and_append_len (string, cur, 1);
721           continue;
722         }
723
724       /* Here we have a replacement to make */
725       escape_and_append_len (string, replace, -1);
726
727       g_free (dup_replace);
728       g_free (format);
729     }
730   g_string_append (string, "\")");
731
732   bytes = g_resources_lookup_data ("/org/gnome/Empathy/Chat/empathy-chat.js",
733       G_RESOURCE_LOOKUP_FLAGS_NONE,
734       NULL);
735
736   if (bytes != NULL)
737     {
738       js = (const gchar *) g_bytes_get_data (bytes, NULL);
739       g_string_prepend (string, js);
740       g_bytes_unref (bytes);
741     }
742
743   script = g_string_free (string, FALSE);
744   webkit_web_view_execute_script (WEBKIT_WEB_VIEW (self), script);
745   g_free (script);
746 }
747
748 static void
749 theme_adium_append_event_escaped (EmpathyThemeAdium *self,
750     const gchar *escaped,
751     PangoDirection direction)
752 {
753   theme_adium_add_html (self, "appendMessage",
754       self->priv->data->status_html, escaped, NULL, NULL, NULL,
755       NULL, "event", tpaw_time_get_current (), FALSE, FALSE, direction);
756
757   /* There is no last contact */
758   if (self->priv->last_contact)
759     {
760       g_object_unref (self->priv->last_contact);
761       self->priv->last_contact = NULL;
762     }
763 }
764
765 static void
766 theme_adium_remove_focus_marks (EmpathyThemeAdium *self,
767     WebKitDOMNodeList *nodes)
768 {
769   guint i;
770
771   /* Remove focus and firstFocus class */
772   for (i = 0; i < webkit_dom_node_list_get_length (nodes); i++)
773     {
774       WebKitDOMNode *node = webkit_dom_node_list_item (nodes, i);
775       WebKitDOMHTMLElement *element = WEBKIT_DOM_HTML_ELEMENT (node);
776       gchar *class_name;
777       gchar **classes, **iter;
778       GString *new_class_name;
779       gboolean first = TRUE;
780
781       if (element == NULL)
782         continue;
783
784       class_name = webkit_dom_html_element_get_class_name (element);
785       classes = g_strsplit (class_name, " ", -1);
786       new_class_name = g_string_sized_new (strlen (class_name));
787
788       for (iter = classes; *iter != NULL; iter++)
789         {
790           if (tp_strdiff (*iter, "focus") &&
791               tp_strdiff (*iter, "firstFocus"))
792             {
793               if (!first)
794                 g_string_append_c (new_class_name, ' ');
795
796               g_string_append (new_class_name, *iter);
797               first = FALSE;
798             }
799         }
800
801       webkit_dom_html_element_set_class_name (element, new_class_name->str);
802
803       g_free (class_name);
804       g_strfreev (classes);
805       g_string_free (new_class_name, TRUE);
806     }
807 }
808
809 static void
810 theme_adium_remove_all_focus_marks (EmpathyThemeAdium *self)
811 {
812   WebKitDOMDocument *dom;
813   WebKitDOMNodeList *nodes;
814   GError *error = NULL;
815
816   if (!self->priv->has_unread_message)
817     return;
818
819   self->priv->has_unread_message = FALSE;
820
821   dom = webkit_web_view_get_dom_document (WEBKIT_WEB_VIEW (self));
822   if (dom == NULL)
823     return;
824
825   /* Get all nodes with focus class */
826   nodes = webkit_dom_document_query_selector_all (dom, ".focus", &error);
827
828   if (nodes == NULL)
829     {
830       DEBUG ("Error getting focus nodes: %s",
831         error ? error->message : "No error");
832       g_clear_error (&error);
833       return;
834     }
835
836   theme_adium_remove_focus_marks (self, nodes);
837 }
838
839 enum
840 {
841   ADD_CONSECUTIVE_MSG_SCROLL = 0,
842   ADD_CONSECUTIVE_MSG_NO_SCROLL = 1,
843   ADD_MSG_SCROLL = 2,
844   ADD_MSG_NO_SCROLL = 3
845 };
846
847 /*
848  * theme_adium_add_message:
849  * @self: The #EmpathyThemeAdium used by the view.
850  * @msg: An #EmpathyMessage that is to be added to the view.
851  * @prev_contact: (out): The #EmpathyContact that sent the previous message.
852  * @prev_timestamp: (out): Timestamp of the previous message.
853  * @prev_is_backlog: (out): Whether the previous message was fetched
854  * from the logs.
855  * @should_highlight: Whether the message should be highlighted. eg.,
856  * if it matches the user's username in multi-user chat.
857  * @js_funcs: An array of JavaScript function names
858  *
859  * Shows @msg in the chat view by adding to @self. Addition is defined
860  * by the JavaScript functions listed in @js_funcs. Common examples
861  * are appending new incoming messages or prepending old messages from
862  * the logs.
863  *
864  * @js_funcs should be an array with exactly 4 entries. The entries
865  * should be the names of JavaScript functions that take the raw HTML
866  * that is to be added to the view as an argument and take the following
867  * actions, in this order:
868  * - add a new consecutive message and scroll to it if needed,
869  * - add a new consecutive message and do not scroll,
870  * - add a new non-consecutive message and scroll to it if needed, and
871  * - add a new non-consecutive message and do not scroll
872  *
873  * A message is considered to be consecutive with the previous one if
874  * all the following conditions are met:
875  * - senders are the same contact,
876  * - last message was recieved recently,
877  * - last message and this message both are/aren't backlog, and
878  * - DisableCombineConsecutive is not set in theme's settings
879  */
880 static void
881 theme_adium_add_message (EmpathyThemeAdium *self,
882     EmpathyMessage *msg,
883     EmpathyContact **prev_contact,
884     gint64 *prev_timestamp,
885     gboolean *prev_is_backlog,
886     gboolean should_highlight,
887     const gchar *js_funcs[])
888 {
889   EmpathyContact *sender;
890   TpMessage *tp_msg;
891   TpAccount *account;
892   gchar *body_escaped, *name_escaped;
893   const gchar *name;
894   const gchar *contact_id;
895   EmpathyAvatar *avatar;
896   const gchar *avatar_filename = NULL;
897   gint64 timestamp;
898   const gchar *html = NULL;
899   const gchar *func;
900   const gchar *service_name;
901   GString *message_classes = NULL;
902   gboolean is_backlog;
903   gboolean consecutive;
904   gboolean action;
905   PangoDirection direction;
906
907
908   /* Get information */
909   sender = empathy_message_get_sender (msg);
910   account = empathy_contact_get_account (sender);
911   service_name = tpaw_protocol_name_to_display_name
912     (tp_account_get_protocol_name (account));
913   if (service_name == NULL)
914     service_name = tp_account_get_protocol_name (account);
915   timestamp = empathy_message_get_timestamp (msg);
916   body_escaped = theme_adium_parse_body (self,
917     empathy_message_get_body (msg),
918     empathy_message_get_token (msg));
919   name = empathy_contact_get_logged_alias (sender);
920   contact_id = empathy_contact_get_id (sender);
921   action = (empathy_message_get_tptype (msg) ==
922       TP_CHANNEL_TEXT_MESSAGE_TYPE_ACTION);
923
924   name_escaped = g_markup_escape_text (name, -1);
925
926   /* If this is a /me probably */
927   if (action)
928     {
929       gchar *str;
930
931       if (self->priv->data->version >= 4 || !self->priv->data->custom_template)
932         {
933           str = g_strdup_printf ("<span class='actionMessageUserName'>%s</span>"
934                      "<span class='actionMessageBody'>%s</span>",
935                      name_escaped, body_escaped);
936         }
937       else
938         {
939           str = g_strdup_printf ("*%s*", body_escaped);
940         }
941
942       g_free (body_escaped);
943       body_escaped = str;
944     }
945
946   /* Get the avatar filename, or a fallback */
947   avatar = empathy_contact_get_avatar (sender);
948   if (avatar)
949     avatar_filename = avatar->filename;
950
951   if (!avatar_filename)
952     {
953       if (empathy_contact_is_user (sender))
954         avatar_filename = self->priv->data->default_outgoing_avatar_filename;
955       else
956         avatar_filename = self->priv->data->default_incoming_avatar_filename;
957
958       if (!avatar_filename)
959         {
960           if (!self->priv->data->default_avatar_filename)
961             self->priv->data->default_avatar_filename =
962               tpaw_filename_from_icon_name (TPAW_IMAGE_AVATAR_DEFAULT,
963                        GTK_ICON_SIZE_DIALOG);
964
965           avatar_filename = self->priv->data->default_avatar_filename;
966         }
967     }
968
969   is_backlog = empathy_message_is_backlog (msg);
970   consecutive = empathy_contact_equal (*prev_contact, sender) &&
971     (ABS (timestamp - *prev_timestamp) < MESSAGE_JOIN_PERIOD) &&
972     (is_backlog == *prev_is_backlog) &&
973     !tp_asv_get_boolean (self->priv->data->info,
974              "DisableCombineConsecutive", NULL);
975
976   /* Define message classes */
977   message_classes = g_string_new ("message");
978   if (!self->priv->has_focus && !is_backlog)
979     {
980       if (!self->priv->has_unread_message)
981         {
982           g_string_append (message_classes, " firstFocus");
983           self->priv->has_unread_message = TRUE;
984         }
985       g_string_append (message_classes, " focus");
986     }
987
988   if (is_backlog)
989     g_string_append (message_classes, " history");
990
991   if (consecutive)
992     g_string_append (message_classes, " consecutive");
993
994   if (empathy_contact_is_user (sender))
995     g_string_append (message_classes, " outgoing");
996   else
997     g_string_append (message_classes, " incoming");
998
999   if (should_highlight)
1000     g_string_append (message_classes, " mention");
1001
1002   if (empathy_message_get_tptype (msg) ==
1003       TP_CHANNEL_TEXT_MESSAGE_TYPE_AUTO_REPLY)
1004     g_string_append (message_classes, " autoreply");
1005
1006   if (action)
1007     g_string_append (message_classes, " action");
1008
1009   /* FIXME: other classes:
1010    * status - the message is a status change
1011    * event - the message is a notification of something happening
1012    *         (for example, encryption being turned on)
1013    * %status% - See %status% in theme_adium_add_html ()
1014    */
1015
1016   /* This is slightly a hack, but it's the only way to add
1017    * arbitrary data to messages in the HTML. We add another
1018    * class called "x-empathy-message-id-*" to the message. This
1019    * way, we can remove the unread marker for this specific
1020    * message later. */
1021   tp_msg = empathy_message_get_tp_message (msg);
1022   if (tp_msg != NULL)
1023     {
1024       guint32 id;
1025       gboolean valid;
1026
1027       id = tp_message_get_pending_message_id (tp_msg, &valid);
1028       if (valid)
1029         g_string_append_printf (message_classes,
1030             " x-empathy-message-id-%u", id);
1031     }
1032
1033   /* Define javascript function to use */
1034   if (consecutive)
1035     func = self->priv->allow_scrolling ? js_funcs[ADD_CONSECUTIVE_MSG_SCROLL] :
1036       js_funcs[ADD_CONSECUTIVE_MSG_NO_SCROLL];
1037   else
1038     func = self->priv->allow_scrolling ? js_funcs[ADD_MSG_SCROLL] :
1039       js_funcs[ADD_MSG_NO_SCROLL];
1040
1041   if (empathy_contact_is_user (sender))
1042     {
1043       /* out */
1044       if (is_backlog)
1045         /* context */
1046         html = consecutive ? self->priv->data->out_nextcontext_html :
1047           self->priv->data->out_context_html;
1048       else
1049         /* content */
1050         html = consecutive ? self->priv->data->out_nextcontent_html :
1051           self->priv->data->out_content_html;
1052
1053       /* remove all the unread marks when we are sending a message */
1054       theme_adium_remove_all_focus_marks (self);
1055     }
1056   else
1057     {
1058       /* in */
1059       if (is_backlog)
1060         /* context */
1061         html = consecutive ? self->priv->data->in_nextcontext_html :
1062           self->priv->data->in_context_html;
1063       else
1064         /* content */
1065         html = consecutive ? self->priv->data->in_nextcontent_html :
1066           self->priv->data->in_content_html;
1067     }
1068
1069   direction = pango_find_base_dir (empathy_message_get_body (msg), -1);
1070
1071   theme_adium_add_html (self, func, html, body_escaped,
1072       avatar_filename, name_escaped, contact_id,
1073       service_name, message_classes->str,
1074       timestamp, is_backlog, empathy_contact_is_user (sender), direction);
1075
1076   /* Keep the sender of the last displayed message */
1077   if (*prev_contact)
1078     g_object_unref (*prev_contact);
1079
1080   *prev_contact = g_object_ref (sender);
1081   *prev_timestamp = timestamp;
1082   *prev_is_backlog = is_backlog;
1083
1084   g_free (body_escaped);
1085   g_free (name_escaped);
1086   g_string_free (message_classes, TRUE);
1087 }
1088
1089 void
1090 empathy_theme_adium_append_message (EmpathyThemeAdium *self,
1091     EmpathyMessage *msg,
1092     gboolean should_highlight)
1093 {
1094   const gchar *js_funcs[] = { "appendNextMessage",
1095       "appendNextMessageNoScroll",
1096       "appendMessage",
1097       "appendMessageNoScroll" };
1098
1099   if (self->priv->pages_loading != 0)
1100     {
1101       queue_item (&self->priv->message_queue, QUEUED_MESSAGE, msg, NULL,
1102           should_highlight, FALSE);
1103       return;
1104     }
1105
1106   theme_adium_add_message (self, msg, &self->priv->last_contact,
1107       &self->priv->last_timestamp, &self->priv->last_is_backlog,
1108       should_highlight, js_funcs);
1109 }
1110
1111 void
1112 empathy_theme_adium_append_event (EmpathyThemeAdium *self,
1113     const gchar *str)
1114 {
1115   gchar *str_escaped;
1116   PangoDirection direction;
1117
1118   if (self->priv->pages_loading != 0)
1119     {
1120       queue_item (&self->priv->message_queue, QUEUED_EVENT, NULL, str, FALSE, FALSE);
1121       return;
1122     }
1123
1124   direction = pango_find_base_dir (str, -1);
1125   str_escaped = g_markup_escape_text (str, -1);
1126   theme_adium_append_event_escaped (self, str_escaped, direction);
1127   g_free (str_escaped);
1128 }
1129
1130 void
1131 empathy_theme_adium_append_event_markup (EmpathyThemeAdium *self,
1132     const gchar *markup_text,
1133     const gchar *fallback_text)
1134 {
1135   PangoDirection direction;
1136
1137   direction = pango_find_base_dir (fallback_text, -1);
1138   theme_adium_append_event_escaped (self, markup_text, direction);
1139 }
1140
1141 void
1142 empathy_theme_adium_prepend_message (EmpathyThemeAdium *self,
1143     EmpathyMessage *msg,
1144     gboolean should_highlight)
1145 {
1146   const gchar *js_funcs[] = { "prependPrev",
1147       "prependPrev",
1148       "prepend",
1149       "prepend" };
1150
1151   if (self->priv->pages_loading != 0)
1152     {
1153       queue_item (&self->priv->message_queue, QUEUED_MESSAGE, msg, NULL,
1154           should_highlight, TRUE);
1155       return;
1156     }
1157
1158   theme_adium_add_message (self, msg, &self->priv->first_contact,
1159       &self->priv->first_timestamp, &self->priv->first_is_backlog,
1160       should_highlight, js_funcs);
1161 }
1162
1163 void
1164 empathy_theme_adium_edit_message (EmpathyThemeAdium *self,
1165     EmpathyMessage *message)
1166 {
1167   WebKitDOMDocument *doc;
1168   WebKitDOMElement *span;
1169   gchar *id, *parsed_body;
1170   gchar *tooltip, *timestamp;
1171   GtkIconInfo *icon_info;
1172   GError *error = NULL;
1173
1174   if (self->priv->pages_loading != 0)
1175     {
1176       queue_item (&self->priv->message_queue, QUEUED_EDIT, message, NULL, FALSE, FALSE);
1177       return;
1178     }
1179
1180   id = g_strdup_printf ("message-token-%s",
1181     empathy_message_get_supersedes (message));
1182   /* we don't pass a token here, because doing so will return another
1183    * <span> element, and we don't want nested <span> elements */
1184   parsed_body = theme_adium_parse_body (self,
1185     empathy_message_get_body (message), NULL);
1186
1187   /* find the element */
1188   doc = webkit_web_view_get_dom_document (WEBKIT_WEB_VIEW (self));
1189   span = webkit_dom_document_get_element_by_id (doc, id);
1190
1191   if (span == NULL)
1192     {
1193       DEBUG ("Failed to find id '%s'", id);
1194       goto except;
1195     }
1196
1197   if (!WEBKIT_DOM_IS_HTML_ELEMENT (span))
1198     {
1199       DEBUG ("Not a HTML element");
1200       goto except;
1201     }
1202
1203   /* update the HTML */
1204   webkit_dom_html_element_set_inner_html (WEBKIT_DOM_HTML_ELEMENT (span),
1205     parsed_body, &error);
1206
1207   if (error != NULL)
1208     {
1209       DEBUG ("Error setting new inner-HTML: %s", error->message);
1210       g_error_free (error);
1211       goto except;
1212     }
1213
1214   /* set a tooltip */
1215   timestamp = tpaw_time_to_string_local (
1216     empathy_message_get_timestamp (message),
1217     "%H:%M:%S");
1218   tooltip = g_strdup_printf (_("Message edited at %s"), timestamp);
1219
1220   webkit_dom_html_element_set_title (WEBKIT_DOM_HTML_ELEMENT (span),
1221     tooltip);
1222
1223   g_free (tooltip);
1224   g_free (timestamp);
1225
1226   /* mark this message as edited */
1227   icon_info = gtk_icon_theme_lookup_icon (gtk_icon_theme_get_default (),
1228     EMPATHY_IMAGE_EDIT_MESSAGE, 16, 0);
1229
1230   if (icon_info != NULL)
1231     {
1232       /* set the icon as a background image using CSS
1233        * FIXME: the icon won't update in response to theme changes */
1234       gchar *style = g_strdup_printf (
1235         "background-image:url('%s');"
1236         "background-repeat:no-repeat;"
1237         "background-position:left center;"
1238         "padding-left:19px;", /* 16px icon + 3px padding */
1239         gtk_icon_info_get_filename (icon_info));
1240
1241       webkit_dom_element_set_attribute (span, "style", style, &error);
1242
1243       if (error != NULL)
1244         {
1245           DEBUG ("Error setting element style: %s",
1246             error->message);
1247           g_clear_error (&error);
1248           /* not fatal */
1249         }
1250
1251       g_free (style);
1252       g_object_unref (icon_info);
1253     }
1254
1255   goto finally;
1256
1257 except:
1258   DEBUG ("Could not find message to edit with: %s",
1259     empathy_message_get_body (message));
1260
1261 finally:
1262   g_free (id);
1263   g_free (parsed_body);
1264 }
1265
1266 void
1267 empathy_theme_adium_scroll (EmpathyThemeAdium *self,
1268     gboolean allow_scrolling)
1269 {
1270   self->priv->allow_scrolling = allow_scrolling;
1271
1272   if (allow_scrolling)
1273     empathy_theme_adium_scroll_down (self);
1274 }
1275
1276 void
1277 empathy_theme_adium_scroll_down (EmpathyThemeAdium *self)
1278 {
1279   webkit_web_view_execute_script (WEBKIT_WEB_VIEW (self), "alignChat(true);");
1280 }
1281
1282 gboolean
1283 empathy_theme_adium_get_has_selection (EmpathyThemeAdium *self)
1284 {
1285   return webkit_web_view_has_selection (WEBKIT_WEB_VIEW (self));
1286 }
1287
1288 void
1289 empathy_theme_adium_clear (EmpathyThemeAdium *self)
1290 {
1291   theme_adium_load_template (self);
1292
1293   /* Clear last contact to avoid trying to add a 'joined'
1294    * message when we don't have an insertion point. */
1295   if (self->priv->last_contact)
1296     {
1297       g_object_unref (self->priv->last_contact);
1298       self->priv->last_contact = NULL;
1299     }
1300 }
1301
1302 gboolean
1303 empathy_theme_adium_find_previous (EmpathyThemeAdium *self,
1304     const gchar *search_criteria,
1305     gboolean new_search,
1306     gboolean match_case)
1307 {
1308   /* FIXME: Doesn't respect new_search */
1309   return webkit_web_view_search_text (WEBKIT_WEB_VIEW (self),
1310       search_criteria, match_case, FALSE, TRUE);
1311 }
1312
1313 gboolean
1314 empathy_theme_adium_find_next (EmpathyThemeAdium *self,
1315     const gchar *search_criteria,
1316     gboolean new_search,
1317     gboolean match_case)
1318 {
1319   /* FIXME: Doesn't respect new_search */
1320   return webkit_web_view_search_text (WEBKIT_WEB_VIEW (self),
1321       search_criteria, match_case, TRUE, TRUE);
1322 }
1323
1324 void
1325 empathy_theme_adium_find_abilities (EmpathyThemeAdium *self,
1326     const gchar *search_criteria,
1327     gboolean match_case,
1328     gboolean *can_do_previous,
1329     gboolean *can_do_next)
1330 {
1331   /* FIXME: Does webkit provide an API for that? We have wrap=true in
1332    * find_next and find_previous to work around this problem. */
1333   if (can_do_previous)
1334     *can_do_previous = TRUE;
1335   if (can_do_next)
1336     *can_do_next = TRUE;
1337 }
1338
1339 void
1340 empathy_theme_adium_highlight (EmpathyThemeAdium *self,
1341     const gchar *text,
1342     gboolean match_case)
1343 {
1344   webkit_web_view_unmark_text_matches (WEBKIT_WEB_VIEW (self));
1345   webkit_web_view_mark_text_matches (WEBKIT_WEB_VIEW (self),
1346       text, match_case, 0);
1347   webkit_web_view_set_highlight_text_matches (WEBKIT_WEB_VIEW (self),
1348       TRUE);
1349 }
1350
1351 void
1352 empathy_theme_adium_copy_clipboard (EmpathyThemeAdium *self)
1353 {
1354   webkit_web_view_copy_clipboard (WEBKIT_WEB_VIEW (self));
1355 }
1356
1357 static void
1358 theme_adium_remove_mark_from_message (EmpathyThemeAdium *self,
1359     guint32 id)
1360 {
1361   WebKitDOMDocument *dom;
1362   WebKitDOMNodeList *nodes;
1363   gchar *class;
1364   GError *error = NULL;
1365
1366   dom = webkit_web_view_get_dom_document (WEBKIT_WEB_VIEW (self));
1367   if (dom == NULL)
1368     return;
1369
1370   class = g_strdup_printf (".x-empathy-message-id-%u", id);
1371
1372   /* Get all nodes with focus class */
1373   nodes = webkit_dom_document_query_selector_all (dom, class, &error);
1374   g_free (class);
1375
1376   if (nodes == NULL)
1377     {
1378       DEBUG ("Error getting focus nodes: %s",
1379         error ? error->message : "No error");
1380       g_clear_error (&error);
1381       return;
1382     }
1383
1384   theme_adium_remove_focus_marks (self, nodes);
1385 }
1386
1387 static void
1388 theme_adium_remove_acked_message_unread_mark_foreach (gpointer data,
1389     gpointer user_data)
1390 {
1391   EmpathyThemeAdium *self = user_data;
1392   guint32 id = GPOINTER_TO_UINT (data);
1393
1394   theme_adium_remove_mark_from_message (self, id);
1395 }
1396
1397 void
1398 empathy_theme_adium_focus_toggled (EmpathyThemeAdium *self,
1399     gboolean has_focus)
1400 {
1401   self->priv->has_focus = has_focus;
1402   if (!self->priv->has_focus)
1403     {
1404       /* We've lost focus, so let's make sure all the acked
1405        * messages have lost their unread marker. */
1406       g_queue_foreach (&self->priv->acked_messages,
1407           theme_adium_remove_acked_message_unread_mark_foreach, self);
1408       g_queue_clear (&self->priv->acked_messages);
1409
1410       self->priv->has_unread_message = FALSE;
1411     }
1412 }
1413
1414 void
1415 empathy_theme_adium_message_acknowledged (EmpathyThemeAdium *self,
1416     EmpathyMessage *message)
1417 {
1418   TpMessage *tp_msg;
1419   guint32 id;
1420   gboolean valid;
1421
1422   tp_msg = empathy_message_get_tp_message (message);
1423
1424   if (tp_msg == NULL)
1425     return;
1426
1427   id = tp_message_get_pending_message_id (tp_msg, &valid);
1428   if (!valid)
1429     {
1430       g_warning ("Acknoledged message doesn't have a pending ID");
1431       return;
1432     }
1433
1434   /* We only want to actually remove the unread marker if the
1435    * view doesn't have focus. If we did it all the time we would
1436    * never see the unread markers, ever! So, we'll queue these
1437    * up, and when we lose focus, we'll remove the markers. */
1438   if (self->priv->has_focus)
1439     {
1440       g_queue_push_tail (&self->priv->acked_messages,
1441              GUINT_TO_POINTER (id));
1442       return;
1443     }
1444
1445   theme_adium_remove_mark_from_message (self, id);
1446 }
1447
1448 static gboolean
1449 theme_adium_context_menu_cb (EmpathyThemeAdium *self,
1450     GtkWidget *default_menu,
1451     WebKitHitTestResult *hit_test_result,
1452     gboolean triggered_with_keyboard,
1453     gpointer user_data)
1454 {
1455   GtkWidget *menu;
1456   EmpathyWebKitMenuFlags flags = EMPATHY_WEBKIT_MENU_CLEAR;
1457
1458   if (g_settings_get_boolean (self->priv->gsettings_chat,
1459         EMPATHY_PREFS_CHAT_WEBKIT_DEVELOPER_TOOLS))
1460     flags |= EMPATHY_WEBKIT_MENU_INSPECT;
1461
1462   menu = empathy_webkit_create_context_menu (
1463     WEBKIT_WEB_VIEW (self), hit_test_result, flags);
1464
1465   gtk_widget_show_all (menu);
1466
1467   gtk_menu_popup (GTK_MENU (menu), NULL, NULL, NULL, NULL, 3,
1468       gtk_get_current_event_time ());
1469
1470   return TRUE;
1471 }
1472
1473 void
1474 empathy_theme_adium_set_show_avatars (EmpathyThemeAdium *self,
1475     gboolean show_avatars)
1476 {
1477   self->priv->show_avatars = show_avatars;
1478 }
1479
1480 static void
1481 theme_adium_load_finished_cb (WebKitWebView *view,
1482     WebKitWebFrame *frame,
1483     gpointer user_data)
1484 {
1485   EmpathyThemeAdium *self = EMPATHY_THEME_ADIUM (view);
1486   GList *l;
1487
1488   DEBUG ("Page loaded");
1489   self->priv->pages_loading--;
1490
1491   if (self->priv->pages_loading != 0)
1492     return;
1493
1494   /* Display queued messages */
1495   for (l = self->priv->message_queue.head; l != NULL; l = l->next)
1496     {
1497       QueuedItem *item = l->data;
1498
1499       switch (item->type)
1500         {
1501           case QUEUED_MESSAGE:
1502             empathy_theme_adium_append_message (self, item->msg,
1503               item->should_highlight);
1504             break;
1505
1506           case QUEUED_EDIT:
1507             empathy_theme_adium_edit_message (self, item->msg);
1508             break;
1509
1510           case QUEUED_EVENT:
1511             empathy_theme_adium_append_event (self, item->str);
1512             break;
1513         }
1514
1515       free_queued_item (item);
1516     }
1517
1518   g_queue_clear (&self->priv->message_queue);
1519 }
1520
1521 static void
1522 theme_adium_finalize (GObject *object)
1523 {
1524   EmpathyThemeAdium *self = EMPATHY_THEME_ADIUM (object);
1525
1526   empathy_adium_data_unref (self->priv->data);
1527
1528   g_object_unref (self->priv->gsettings_chat);
1529   g_object_unref (self->priv->gsettings_desktop);
1530
1531   g_free (self->priv->variant);
1532
1533   G_OBJECT_CLASS (empathy_theme_adium_parent_class)->finalize (object);
1534 }
1535
1536 static void
1537 theme_adium_dispose (GObject *object)
1538 {
1539   EmpathyThemeAdium *self = EMPATHY_THEME_ADIUM (object);
1540
1541   if (self->priv->smiley_manager)
1542     {
1543       g_object_unref (self->priv->smiley_manager);
1544       self->priv->smiley_manager = NULL;
1545     }
1546
1547   g_clear_object (&self->priv->first_contact);
1548
1549   if (self->priv->last_contact)
1550     {
1551       g_object_unref (self->priv->last_contact);
1552       self->priv->last_contact = NULL;
1553     }
1554
1555   if (self->priv->inspector_window)
1556     {
1557       gtk_widget_destroy (self->priv->inspector_window);
1558       self->priv->inspector_window = NULL;
1559     }
1560
1561   if (self->priv->acked_messages.length > 0)
1562     {
1563       g_queue_clear (&self->priv->acked_messages);
1564     }
1565
1566   G_OBJECT_CLASS (empathy_theme_adium_parent_class)->dispose (object);
1567 }
1568
1569 static gboolean
1570 theme_adium_inspector_show_window_cb (WebKitWebInspector *inspector,
1571     EmpathyThemeAdium *self)
1572 {
1573   if (self->priv->inspector_window)
1574     {
1575       gtk_widget_show_all (self->priv->inspector_window);
1576     }
1577
1578   return TRUE;
1579 }
1580
1581 static gboolean
1582 theme_adium_inspector_close_window_cb (WebKitWebInspector *inspector,
1583     EmpathyThemeAdium *self)
1584 {
1585   if (self->priv->inspector_window)
1586     {
1587       gtk_widget_hide (self->priv->inspector_window);
1588     }
1589
1590   return TRUE;
1591 }
1592
1593 static WebKitWebView *
1594 theme_adium_inspect_web_view_cb (WebKitWebInspector *inspector,
1595     WebKitWebView *web_view,
1596     EmpathyThemeAdium *self)
1597 {
1598   GtkWidget *scrolled_window;
1599   GtkWidget *inspector_web_view;
1600
1601   if (!self->priv->inspector_window)
1602     {
1603       /* Create main window */
1604       self->priv->inspector_window = gtk_window_new (GTK_WINDOW_TOPLEVEL);
1605
1606       gtk_window_set_default_size (GTK_WINDOW (self->priv->inspector_window),
1607                  800, 600);
1608
1609       g_signal_connect (self->priv->inspector_window, "delete-event",
1610             G_CALLBACK (gtk_widget_hide_on_delete), NULL);
1611
1612       /* Pack a scrolled window */
1613       scrolled_window = gtk_scrolled_window_new (NULL, NULL);
1614
1615       gtk_scrolled_window_set_policy (GTK_SCROLLED_WINDOW (scrolled_window),
1616               GTK_POLICY_AUTOMATIC,
1617               GTK_POLICY_AUTOMATIC);
1618       gtk_container_add (GTK_CONTAINER (self->priv->inspector_window),
1619              scrolled_window);
1620       gtk_widget_show (scrolled_window);
1621
1622       /* Pack a webview in the scrolled window. That webview will be
1623        * used to render the inspector tool. */
1624       inspector_web_view = webkit_web_view_new ();
1625       gtk_container_add (GTK_CONTAINER (scrolled_window),
1626              inspector_web_view);
1627       gtk_widget_show (scrolled_window);
1628
1629       return WEBKIT_WEB_VIEW (inspector_web_view);
1630     }
1631
1632   return NULL;
1633 }
1634
1635 static void
1636 theme_adium_constructed (GObject *object)
1637 {
1638   EmpathyThemeAdium *self = EMPATHY_THEME_ADIUM (object);
1639   const gchar *font_family = NULL;
1640   gint font_size = 0;
1641   WebKitWebView *webkit_view = WEBKIT_WEB_VIEW (object);
1642   WebKitWebInspector *webkit_inspector;
1643
1644   /* Set default settings */
1645   font_family = tp_asv_get_string (self->priv->data->info, "DefaultFontFamily");
1646   font_size = tp_asv_get_int32 (self->priv->data->info, "DefaultFontSize", NULL);
1647
1648   if (font_family && font_size)
1649     {
1650       g_object_set (webkit_web_view_get_settings (webkit_view),
1651         "default-font-family", font_family,
1652         "default-font-size", font_size,
1653         NULL);
1654     }
1655   else
1656     {
1657       empathy_webkit_bind_font_setting (webkit_view,
1658         self->priv->gsettings_desktop,
1659         EMPATHY_PREFS_DESKTOP_INTERFACE_DOCUMENT_FONT_NAME);
1660     }
1661
1662   /* Setup webkit inspector */
1663   webkit_inspector = webkit_web_view_get_inspector (webkit_view);
1664   g_signal_connect (webkit_inspector, "inspect-web-view",
1665       G_CALLBACK (theme_adium_inspect_web_view_cb), object);
1666   g_signal_connect (webkit_inspector, "show-window",
1667       G_CALLBACK (theme_adium_inspector_show_window_cb), object);
1668   g_signal_connect (webkit_inspector, "close-window",
1669       G_CALLBACK (theme_adium_inspector_close_window_cb), object);
1670
1671   /* Load template */
1672   theme_adium_load_template (EMPATHY_THEME_ADIUM (object));
1673
1674   self->priv->in_construction = FALSE;
1675 }
1676
1677 static void
1678 theme_adium_get_property (GObject *object,
1679     guint param_id,
1680     GValue *value,
1681     GParamSpec *pspec)
1682 {
1683   EmpathyThemeAdium *self = EMPATHY_THEME_ADIUM (object);
1684
1685   switch (param_id)
1686     {
1687       case PROP_ADIUM_DATA:
1688         g_value_set_boxed (value, self->priv->data);
1689         break;
1690       case PROP_VARIANT:
1691         g_value_set_string (value, self->priv->variant);
1692         break;
1693       default:
1694         G_OBJECT_WARN_INVALID_PROPERTY_ID (object, param_id, pspec);
1695         break;
1696     };
1697 }
1698
1699 static void
1700 theme_adium_set_property (GObject *object,
1701     guint param_id,
1702     const GValue *value,
1703     GParamSpec *pspec)
1704 {
1705   EmpathyThemeAdium *self = EMPATHY_THEME_ADIUM (object);
1706
1707   switch (param_id)
1708     {
1709       case PROP_ADIUM_DATA:
1710         g_assert (self->priv->data == NULL);
1711         self->priv->data = g_value_dup_boxed (value);
1712         break;
1713       case PROP_VARIANT:
1714         empathy_theme_adium_set_variant (self, g_value_get_string (value));
1715         break;
1716       default:
1717         G_OBJECT_WARN_INVALID_PROPERTY_ID (object, param_id, pspec);
1718         break;
1719     };
1720 }
1721
1722 static void
1723 empathy_theme_adium_class_init (EmpathyThemeAdiumClass *klass)
1724 {
1725   GObjectClass *object_class = G_OBJECT_CLASS (klass);
1726
1727   object_class->finalize = theme_adium_finalize;
1728   object_class->dispose = theme_adium_dispose;
1729   object_class->constructed = theme_adium_constructed;
1730   object_class->get_property = theme_adium_get_property;
1731   object_class->set_property = theme_adium_set_property;
1732
1733   g_object_class_install_property (object_class, PROP_ADIUM_DATA,
1734       g_param_spec_boxed ("adium-data",
1735         "The theme data",
1736         "Data for the adium theme",
1737         EMPATHY_TYPE_ADIUM_DATA,
1738         G_PARAM_CONSTRUCT_ONLY |
1739         G_PARAM_READWRITE |
1740         G_PARAM_STATIC_STRINGS));
1741
1742   g_object_class_install_property (object_class, PROP_VARIANT,
1743       g_param_spec_string ("variant",
1744         "The theme variant",
1745         "Variant name for the theme",
1746         NULL,
1747         G_PARAM_CONSTRUCT |
1748         G_PARAM_READWRITE |
1749         G_PARAM_STATIC_STRINGS));
1750
1751   g_type_class_add_private (object_class, sizeof (EmpathyThemeAdiumPriv));
1752 }
1753
1754 static void
1755 empathy_theme_adium_init (EmpathyThemeAdium *self)
1756 {
1757   self->priv = G_TYPE_INSTANCE_GET_PRIVATE (self,
1758     EMPATHY_TYPE_THEME_ADIUM, EmpathyThemeAdiumPriv);
1759
1760   self->priv->in_construction = TRUE;
1761   g_queue_init (&self->priv->message_queue);
1762   self->priv->allow_scrolling = TRUE;
1763   self->priv->smiley_manager = empathy_smiley_manager_dup_singleton ();
1764
1765   /* Show avatars by default. */
1766   self->priv->show_avatars = TRUE;
1767
1768   g_signal_connect (self, "load-finished",
1769       G_CALLBACK (theme_adium_load_finished_cb), NULL);
1770   g_signal_connect (self, "navigation-policy-decision-requested",
1771         G_CALLBACK (theme_adium_navigation_policy_decision_requested_cb), NULL);
1772   g_signal_connect (self, "context-menu",
1773       G_CALLBACK (theme_adium_context_menu_cb), NULL);
1774
1775   self->priv->gsettings_chat = g_settings_new (EMPATHY_PREFS_CHAT_SCHEMA);
1776   self->priv->gsettings_desktop = g_settings_new (
1777     EMPATHY_PREFS_DESKTOP_INTERFACE_SCHEMA);
1778 }
1779
1780 EmpathyThemeAdium *
1781 empathy_theme_adium_new (EmpathyAdiumData *data,
1782     const gchar *variant)
1783 {
1784   g_return_val_if_fail (data != NULL, NULL);
1785
1786   return g_object_new (EMPATHY_TYPE_THEME_ADIUM,
1787       "adium-data", data,
1788       "variant", variant,
1789       NULL);
1790 }
1791
1792 void
1793 empathy_theme_adium_set_variant (EmpathyThemeAdium *self,
1794     const gchar *variant)
1795 {
1796   gchar *variant_path;
1797   gchar *script;
1798
1799   if (!tp_strdiff (self->priv->variant, variant))
1800     return;
1801
1802   g_free (self->priv->variant);
1803   self->priv->variant = g_strdup (variant);
1804
1805   if (self->priv->in_construction)
1806     return;
1807
1808   DEBUG ("Update view with variant: '%s'", variant);
1809   variant_path = adium_info_dup_path_for_variant (self->priv->data->info,
1810     self->priv->variant);
1811   script = g_strdup_printf ("setStylesheet(\"mainStyle\",\"%s\");",
1812       variant_path);
1813
1814   webkit_web_view_execute_script (WEBKIT_WEB_VIEW (self), script);
1815
1816   g_free (variant_path);
1817   g_free (script);
1818
1819   g_object_notify (G_OBJECT (self), "variant");
1820 }
1821
1822 void
1823 empathy_theme_adium_show_inspector (EmpathyThemeAdium *self)
1824 {
1825   WebKitWebView *web_view = WEBKIT_WEB_VIEW (self);
1826
1827   empathy_webkit_show_inspector (web_view);
1828 }
1829
1830 gboolean
1831 empathy_adium_path_is_valid (const gchar *path)
1832 {
1833   gboolean ret;
1834   gchar *file;
1835   gchar **tmp;
1836   const gchar *dir;
1837
1838   if (path[0] != '/')
1839     return FALSE;
1840
1841   /* The directory has to be *.AdiumMessageStyle per the Adium spec */
1842   tmp = g_strsplit (path, "/", 0);
1843   if (tmp == NULL)
1844     return FALSE;
1845
1846   dir = tmp[g_strv_length (tmp) - 1];
1847
1848   if (!g_str_has_suffix (dir, ".AdiumMessageStyle"))
1849     {
1850       g_strfreev (tmp);
1851       return FALSE;
1852     }
1853
1854   g_strfreev (tmp);
1855
1856   /* The theme is not valid if there is no Info.plist */
1857   file = g_build_filename (path, "Contents", "Info.plist",
1858          NULL);
1859   ret = g_file_test (file, G_FILE_TEST_EXISTS);
1860   g_free (file);
1861
1862   if (!ret)
1863     return FALSE;
1864
1865   /* We ship a default Template.html as fallback if there is any problem
1866    * with the one inside the theme. The only other required file is
1867    * Content.html OR Incoming/Content.html*/
1868   file = g_build_filename (path, "Contents", "Resources", "Content.html",
1869       NULL);
1870   ret = g_file_test (file, G_FILE_TEST_EXISTS);
1871   g_free (file);
1872
1873   if (!ret)
1874     {
1875       file = g_build_filename (path, "Contents", "Resources", "Incoming",
1876              "Content.html", NULL);
1877       ret = g_file_test (file, G_FILE_TEST_EXISTS);
1878       g_free (file);
1879     }
1880
1881   return ret;
1882 }
1883
1884 GHashTable *
1885 empathy_adium_info_new (const gchar *path)
1886 {
1887   gchar *file;
1888   GValue *value;
1889   GHashTable *info = NULL;
1890
1891   g_return_val_if_fail (empathy_adium_path_is_valid (path), NULL);
1892
1893   file = g_build_filename (path, "Contents", "Info.plist", NULL);
1894   value = empathy_plist_parse_from_file (file);
1895   g_free (file);
1896
1897   if (value == NULL)
1898     return NULL;
1899
1900   info = g_value_dup_boxed (value);
1901   tp_g_value_slice_free (value);
1902
1903   /* Insert the theme's path into the hash table,
1904    * keys have to be dupped */
1905   tp_asv_set_string (info, g_strdup ("path"), path);
1906
1907   return info;
1908 }
1909
1910 static guint
1911 adium_info_get_version (GHashTable *info)
1912 {
1913   return tp_asv_get_int32 (info, "MessageViewVersion", NULL);
1914 }
1915
1916 static const gchar *
1917 adium_info_get_no_variant_name (GHashTable *info)
1918 {
1919   const gchar *name = tp_asv_get_string (info, "DisplayNameForNoVariant");
1920   return name ? name : _("Normal");
1921 }
1922
1923 static gchar *
1924 adium_info_dup_path_for_variant (GHashTable *info,
1925     const gchar *variant)
1926 {
1927   guint version = adium_info_get_version (info);
1928   const gchar *no_variant = adium_info_get_no_variant_name (info);
1929   GPtrArray *variants;
1930   guint i;
1931
1932   if (version <= 2 && !tp_strdiff (variant, no_variant))
1933     return g_strdup ("main.css");
1934
1935   variants = empathy_adium_info_get_available_variants (info);
1936   if (variants->len == 0)
1937     return g_strdup ("main.css");
1938
1939   /* Verify the variant exists, fallback to the first one */
1940   for (i = 0; i < variants->len; i++)
1941     {
1942       if (!tp_strdiff (variant, g_ptr_array_index (variants, i)))
1943         break;
1944     }
1945
1946   if (i == variants->len)
1947     {
1948       DEBUG ("Variant %s does not exist", variant);
1949       variant = g_ptr_array_index (variants, 0);
1950     }
1951
1952   return g_strdup_printf ("Variants/%s.css", variant);
1953
1954 }
1955
1956 const gchar *
1957 empathy_adium_info_get_default_variant (GHashTable *info)
1958 {
1959   if (adium_info_get_version (info) <= 2)
1960     return adium_info_get_no_variant_name (info);
1961
1962   return tp_asv_get_string (info, "DefaultVariant");
1963 }
1964
1965 GPtrArray *
1966 empathy_adium_info_get_available_variants (GHashTable *info)
1967 {
1968   GPtrArray *variants;
1969   const gchar *path;
1970   gchar *dirpath;
1971   GDir *dir;
1972
1973   variants = tp_asv_get_boxed (info, "AvailableVariants", G_TYPE_PTR_ARRAY);
1974   if (variants != NULL)
1975     return variants;
1976
1977   variants = g_ptr_array_new_with_free_func (g_free);
1978   tp_asv_take_boxed (info, g_strdup ("AvailableVariants"),
1979     G_TYPE_PTR_ARRAY, variants);
1980
1981   path = tp_asv_get_string (info, "path");
1982   dirpath = g_build_filename (path, "Contents", "Resources", "Variants", NULL);
1983   dir = g_dir_open (dirpath, 0, NULL);
1984   if (dir != NULL)
1985     {
1986       const gchar *name;
1987
1988       for (name = g_dir_read_name (dir);
1989            name != NULL;
1990            name = g_dir_read_name (dir))
1991         {
1992           gchar *display_name;
1993
1994           if (!g_str_has_suffix (name, ".css"))
1995             continue;
1996
1997           display_name = g_strdup (name);
1998           strstr (display_name, ".css")[0] = '\0';
1999           g_ptr_array_add (variants, display_name);
2000         }
2001
2002       g_dir_close (dir);
2003     }
2004   g_free (dirpath);
2005
2006   if (adium_info_get_version (info) <= 2)
2007     g_ptr_array_add (variants,
2008       g_strdup (adium_info_get_no_variant_name (info)));
2009
2010   return variants;
2011 }
2012
2013 GType
2014 empathy_adium_data_get_type (void)
2015 {
2016   static GType type_id = 0;
2017
2018   if (!type_id)
2019     {
2020       type_id = g_boxed_type_register_static ("EmpathyAdiumData",
2021           (GBoxedCopyFunc) empathy_adium_data_ref,
2022           (GBoxedFreeFunc) empathy_adium_data_unref);
2023     }
2024
2025   return type_id;
2026 }
2027
2028 EmpathyAdiumData *
2029 empathy_adium_data_new_with_info (const gchar *path,
2030     GHashTable *info)
2031 {
2032   EmpathyAdiumData *data;
2033   gchar *template_html = NULL;
2034   gchar *footer_html = NULL;
2035   gchar *tmp;
2036
2037   g_return_val_if_fail (empathy_adium_path_is_valid (path), NULL);
2038
2039   data = g_slice_new0 (EmpathyAdiumData);
2040   data->ref_count = 1;
2041   data->path = g_strdup (path);
2042   data->basedir = g_strconcat (path, G_DIR_SEPARATOR_S "Contents"
2043     G_DIR_SEPARATOR_S "Resources" G_DIR_SEPARATOR_S, NULL);
2044   data->info = g_hash_table_ref (info);
2045   data->version = adium_info_get_version (info);
2046   data->strings_to_free = g_ptr_array_new_with_free_func (g_free);
2047   data->date_format_cache = g_hash_table_new_full (g_str_hash,
2048     g_str_equal, g_free, g_free);
2049
2050   DEBUG ("Loading theme at %s", path);
2051
2052 #define LOAD(path, var) \
2053     tmp = g_build_filename (data->basedir, path, NULL); \
2054     g_file_get_contents (tmp, &var, NULL, NULL); \
2055     g_free (tmp); \
2056
2057 #define LOAD_CONST(path, var) \
2058   { \
2059     gchar *content; \
2060     LOAD (path, content); \
2061     if (content != NULL) { \
2062       g_ptr_array_add (data->strings_to_free, content); \
2063     } \
2064     var = content; \
2065   }
2066
2067   /* Load html files */
2068   LOAD_CONST ("Content.html", data->content_html);
2069   LOAD_CONST ("Incoming/Content.html", data->in_content_html);
2070   LOAD_CONST ("Incoming/NextContent.html", data->in_nextcontent_html);
2071   LOAD_CONST ("Incoming/Context.html", data->in_context_html);
2072   LOAD_CONST ("Incoming/NextContext.html", data->in_nextcontext_html);
2073   LOAD_CONST ("Outgoing/Content.html", data->out_content_html);
2074   LOAD_CONST ("Outgoing/NextContent.html", data->out_nextcontent_html);
2075   LOAD_CONST ("Outgoing/Context.html", data->out_context_html);
2076   LOAD_CONST ("Outgoing/NextContext.html", data->out_nextcontext_html);
2077   LOAD_CONST ("Status.html", data->status_html);
2078   LOAD ("Template.html", template_html);
2079   LOAD ("Footer.html", footer_html);
2080
2081 #undef LOAD_CONST
2082 #undef LOAD
2083
2084   /* HTML fallbacks: If we have at least content OR in_content, then
2085    * everything else gets a fallback */
2086
2087 #define FALLBACK(html, fallback) \
2088   if (html == NULL) { \
2089     html = fallback; \
2090   }
2091
2092   /* in_nextcontent -> in_content -> content */
2093   FALLBACK (data->in_content_html,      data->content_html);
2094   FALLBACK (data->in_nextcontent_html,  data->in_content_html);
2095
2096   /* context -> content */
2097   FALLBACK (data->in_context_html,      data->in_content_html);
2098   FALLBACK (data->in_nextcontext_html,  data->in_nextcontent_html);
2099   FALLBACK (data->out_context_html,     data->out_content_html);
2100   FALLBACK (data->out_nextcontext_html, data->out_nextcontent_html);
2101
2102   /* out -> in */
2103   FALLBACK (data->out_content_html,     data->in_content_html);
2104   FALLBACK (data->out_nextcontent_html, data->in_nextcontent_html);
2105   FALLBACK (data->out_context_html,     data->in_context_html);
2106   FALLBACK (data->out_nextcontext_html, data->in_nextcontext_html);
2107
2108   /* status -> in_content */
2109   FALLBACK (data->status_html,          data->in_content_html);
2110
2111 #undef FALLBACK
2112
2113   /* template -> empathy's template */
2114   data->custom_template = (template_html != NULL);
2115   if (template_html == NULL)
2116     {
2117       GError *error = NULL;
2118
2119       tmp = empathy_file_lookup ("Template.html", "data");
2120
2121       if (!g_file_get_contents (tmp, &template_html, NULL, &error)) {
2122         g_warning ("couldn't load Empathy's default theme "
2123           "template: %s", error->message);
2124         g_return_val_if_reached (data);
2125       }
2126
2127       g_free (tmp);
2128     }
2129
2130   /* Default avatar */
2131   tmp = g_build_filename (data->basedir, "Incoming", "buddy_icon.png", NULL);
2132   if (g_file_test (tmp, G_FILE_TEST_EXISTS | G_FILE_TEST_IS_REGULAR))
2133     {
2134       data->default_incoming_avatar_filename = tmp;
2135     }
2136   else
2137     {
2138       g_free (tmp);
2139     }
2140
2141   tmp = g_build_filename (data->basedir, "Outgoing", "buddy_icon.png", NULL);
2142   if (g_file_test (tmp, G_FILE_TEST_EXISTS | G_FILE_TEST_IS_REGULAR))
2143     {
2144       data->default_outgoing_avatar_filename = tmp;
2145     }
2146   else
2147     {
2148       g_free (tmp);
2149     }
2150
2151   /* Old custom templates had only 4 parameters.
2152    * New templates have 5 parameters */
2153   if (data->version <= 2 && data->custom_template)
2154     {
2155       tmp = string_with_format (template_html,
2156         data->basedir,
2157         "%@", /* Leave variant unset */
2158         "", /* The header */
2159         footer_html ? footer_html : "",
2160         NULL);
2161     }
2162   else
2163     {
2164       tmp = string_with_format (template_html,
2165         data->basedir,
2166         data->version <= 2 ? "" : "@import url( \"main.css\" );",
2167         "%@", /* Leave variant unset */
2168         "", /* The header */
2169         footer_html ? footer_html : "",
2170         NULL);
2171     }
2172   g_ptr_array_add (data->strings_to_free, tmp);
2173   data->template_html = tmp;
2174
2175   g_free (template_html);
2176   g_free (footer_html);
2177
2178   return data;
2179 }
2180
2181 EmpathyAdiumData *
2182 empathy_adium_data_new (const gchar *path)
2183 {
2184   EmpathyAdiumData *data;
2185   GHashTable *info;
2186
2187   info = empathy_adium_info_new (path);
2188   data = empathy_adium_data_new_with_info (path, info);
2189   g_hash_table_unref (info);
2190
2191   return data;
2192 }
2193
2194 EmpathyAdiumData *
2195 empathy_adium_data_ref (EmpathyAdiumData *data)
2196 {
2197   g_return_val_if_fail (data != NULL, NULL);
2198
2199   g_atomic_int_inc (&data->ref_count);
2200
2201   return data;
2202 }
2203
2204 void
2205 empathy_adium_data_unref (EmpathyAdiumData *data)
2206 {
2207   g_return_if_fail (data != NULL);
2208
2209   if (g_atomic_int_dec_and_test (&data->ref_count)) {
2210     g_free (data->path);
2211     g_free (data->basedir);
2212     g_free (data->default_avatar_filename);
2213     g_free (data->default_incoming_avatar_filename);
2214     g_free (data->default_outgoing_avatar_filename);
2215     g_hash_table_unref (data->info);
2216     g_ptr_array_unref (data->strings_to_free);
2217     tp_clear_pointer (&data->date_format_cache, g_hash_table_unref);
2218
2219     g_slice_free (EmpathyAdiumData, data);
2220   }
2221 }
2222
2223 GHashTable *
2224 empathy_adium_data_get_info (EmpathyAdiumData *data)
2225 {
2226   g_return_val_if_fail (data != NULL, NULL);
2227
2228   return data->info;
2229 }
2230
2231 const gchar *
2232 empathy_adium_data_get_path (EmpathyAdiumData *data)
2233 {
2234   g_return_val_if_fail (data != NULL, NULL);
2235
2236   return data->path;
2237 }