]> git.0d.be Git - empathy.git/blob - libempathy-gtk/empathy-theme-adium.c
pixbuf-utils: copy the generic pixbuf-related utily functions from Empathy
[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   js = (const gchar *) g_bytes_get_data (bytes, NULL);
736   g_string_prepend (string, js);
737   g_bytes_unref (bytes);
738
739   script = g_string_free (string, FALSE);
740   webkit_web_view_execute_script (WEBKIT_WEB_VIEW (self), script);
741   g_free (script);
742 }
743
744 static void
745 theme_adium_append_event_escaped (EmpathyThemeAdium *self,
746     const gchar *escaped,
747     PangoDirection direction)
748 {
749   theme_adium_add_html (self, "appendMessage",
750       self->priv->data->status_html, escaped, NULL, NULL, NULL,
751       NULL, "event", tpaw_time_get_current (), FALSE, FALSE, direction);
752
753   /* There is no last contact */
754   if (self->priv->last_contact)
755     {
756       g_object_unref (self->priv->last_contact);
757       self->priv->last_contact = NULL;
758     }
759 }
760
761 static void
762 theme_adium_remove_focus_marks (EmpathyThemeAdium *self,
763     WebKitDOMNodeList *nodes)
764 {
765   guint i;
766
767   /* Remove focus and firstFocus class */
768   for (i = 0; i < webkit_dom_node_list_get_length (nodes); i++)
769     {
770       WebKitDOMNode *node = webkit_dom_node_list_item (nodes, i);
771       WebKitDOMHTMLElement *element = WEBKIT_DOM_HTML_ELEMENT (node);
772       gchar *class_name;
773       gchar **classes, **iter;
774       GString *new_class_name;
775       gboolean first = TRUE;
776
777       if (element == NULL)
778         continue;
779
780       class_name = webkit_dom_html_element_get_class_name (element);
781       classes = g_strsplit (class_name, " ", -1);
782       new_class_name = g_string_sized_new (strlen (class_name));
783
784       for (iter = classes; *iter != NULL; iter++)
785         {
786           if (tp_strdiff (*iter, "focus") &&
787               tp_strdiff (*iter, "firstFocus"))
788             {
789               if (!first)
790                 g_string_append_c (new_class_name, ' ');
791
792               g_string_append (new_class_name, *iter);
793               first = FALSE;
794             }
795         }
796
797       webkit_dom_html_element_set_class_name (element, new_class_name->str);
798
799       g_free (class_name);
800       g_strfreev (classes);
801       g_string_free (new_class_name, TRUE);
802     }
803 }
804
805 static void
806 theme_adium_remove_all_focus_marks (EmpathyThemeAdium *self)
807 {
808   WebKitDOMDocument *dom;
809   WebKitDOMNodeList *nodes;
810   GError *error = NULL;
811
812   if (!self->priv->has_unread_message)
813     return;
814
815   self->priv->has_unread_message = FALSE;
816
817   dom = webkit_web_view_get_dom_document (WEBKIT_WEB_VIEW (self));
818   if (dom == NULL)
819     return;
820
821   /* Get all nodes with focus class */
822   nodes = webkit_dom_document_query_selector_all (dom, ".focus", &error);
823
824   if (nodes == NULL)
825     {
826       DEBUG ("Error getting focus nodes: %s",
827         error ? error->message : "No error");
828       g_clear_error (&error);
829       return;
830     }
831
832   theme_adium_remove_focus_marks (self, nodes);
833 }
834
835 enum
836 {
837   ADD_CONSECUTIVE_MSG_SCROLL = 0,
838   ADD_CONSECUTIVE_MSG_NO_SCROLL = 1,
839   ADD_MSG_SCROLL = 2,
840   ADD_MSG_NO_SCROLL = 3
841 };
842
843 /*
844  * theme_adium_add_message:
845  * @self: The #EmpathyThemeAdium used by the view.
846  * @msg: An #EmpathyMessage that is to be added to the view.
847  * @prev_contact: (out): The #EmpathyContact that sent the previous message.
848  * @prev_timestamp: (out): Timestamp of the previous message.
849  * @prev_is_backlog: (out): Whether the previous message was fetched
850  * from the logs.
851  * @should_highlight: Whether the message should be highlighted. eg.,
852  * if it matches the user's username in multi-user chat.
853  * @js_funcs: An array of JavaScript function names
854  *
855  * Shows @msg in the chat view by adding to @self. Addition is defined
856  * by the JavaScript functions listed in @js_funcs. Common examples
857  * are appending new incoming messages or prepending old messages from
858  * the logs.
859  *
860  * @js_funcs should be an array with exactly 4 entries. The entries
861  * should be the names of JavaScript functions that take the raw HTML
862  * that is to be added to the view as an argument and take the following
863  * actions, in this order:
864  * - add a new consecutive message and scroll to it if needed,
865  * - add a new consecutive message and do not scroll,
866  * - add a new non-consecutive message and scroll to it if needed, and
867  * - add a new non-consecutive message and do not scroll
868  *
869  * A message is considered to be consecutive with the previous one if
870  * all the following conditions are met:
871  * - senders are the same contact,
872  * - last message was recieved recently,
873  * - last message and this message both are/aren't backlog, and
874  * - DisableCombineConsecutive is not set in theme's settings
875  */
876 static void
877 theme_adium_add_message (EmpathyThemeAdium *self,
878     EmpathyMessage *msg,
879     EmpathyContact **prev_contact,
880     gint64 *prev_timestamp,
881     gboolean *prev_is_backlog,
882     gboolean should_highlight,
883     const gchar *js_funcs[])
884 {
885   EmpathyContact *sender;
886   TpMessage *tp_msg;
887   TpAccount *account;
888   gchar *body_escaped, *name_escaped;
889   const gchar *name;
890   const gchar *contact_id;
891   EmpathyAvatar *avatar;
892   const gchar *avatar_filename = NULL;
893   gint64 timestamp;
894   const gchar *html = NULL;
895   const gchar *func;
896   const gchar *service_name;
897   GString *message_classes = NULL;
898   gboolean is_backlog;
899   gboolean consecutive;
900   gboolean action;
901   PangoDirection direction;
902
903
904   /* Get information */
905   sender = empathy_message_get_sender (msg);
906   account = empathy_contact_get_account (sender);
907   service_name = tpaw_protocol_name_to_display_name
908     (tp_account_get_protocol_name (account));
909   if (service_name == NULL)
910     service_name = tp_account_get_protocol_name (account);
911   timestamp = empathy_message_get_timestamp (msg);
912   body_escaped = theme_adium_parse_body (self,
913     empathy_message_get_body (msg),
914     empathy_message_get_token (msg));
915   name = empathy_contact_get_logged_alias (sender);
916   contact_id = empathy_contact_get_id (sender);
917   action = (empathy_message_get_tptype (msg) ==
918       TP_CHANNEL_TEXT_MESSAGE_TYPE_ACTION);
919
920   name_escaped = g_markup_escape_text (name, -1);
921
922   /* If this is a /me probably */
923   if (action)
924     {
925       gchar *str;
926
927       if (self->priv->data->version >= 4 || !self->priv->data->custom_template)
928         {
929           str = g_strdup_printf ("<span class='actionMessageUserName'>%s</span>"
930                      "<span class='actionMessageBody'>%s</span>",
931                      name_escaped, body_escaped);
932         }
933       else
934         {
935           str = g_strdup_printf ("*%s*", body_escaped);
936         }
937
938       g_free (body_escaped);
939       body_escaped = str;
940     }
941
942   /* Get the avatar filename, or a fallback */
943   avatar = empathy_contact_get_avatar (sender);
944   if (avatar)
945     avatar_filename = avatar->filename;
946
947   if (!avatar_filename)
948     {
949       if (empathy_contact_is_user (sender))
950         avatar_filename = self->priv->data->default_outgoing_avatar_filename;
951       else
952         avatar_filename = self->priv->data->default_incoming_avatar_filename;
953
954       if (!avatar_filename)
955         {
956           if (!self->priv->data->default_avatar_filename)
957             self->priv->data->default_avatar_filename =
958               tpaw_filename_from_icon_name (TPAW_IMAGE_AVATAR_DEFAULT,
959                        GTK_ICON_SIZE_DIALOG);
960
961           avatar_filename = self->priv->data->default_avatar_filename;
962         }
963     }
964
965   is_backlog = empathy_message_is_backlog (msg);
966   consecutive = empathy_contact_equal (*prev_contact, sender) &&
967     (ABS (timestamp - *prev_timestamp) < MESSAGE_JOIN_PERIOD) &&
968     (is_backlog == *prev_is_backlog) &&
969     !tp_asv_get_boolean (self->priv->data->info,
970              "DisableCombineConsecutive", NULL);
971
972   /* Define message classes */
973   message_classes = g_string_new ("message");
974   if (!self->priv->has_focus && !is_backlog)
975     {
976       if (!self->priv->has_unread_message)
977         {
978           g_string_append (message_classes, " firstFocus");
979           self->priv->has_unread_message = TRUE;
980         }
981       g_string_append (message_classes, " focus");
982     }
983
984   if (is_backlog)
985     g_string_append (message_classes, " history");
986
987   if (consecutive)
988     g_string_append (message_classes, " consecutive");
989
990   if (empathy_contact_is_user (sender))
991     g_string_append (message_classes, " outgoing");
992   else
993     g_string_append (message_classes, " incoming");
994
995   if (should_highlight)
996     g_string_append (message_classes, " mention");
997
998   if (empathy_message_get_tptype (msg) ==
999       TP_CHANNEL_TEXT_MESSAGE_TYPE_AUTO_REPLY)
1000     g_string_append (message_classes, " autoreply");
1001
1002   if (action)
1003     g_string_append (message_classes, " action");
1004
1005   /* FIXME: other classes:
1006    * status - the message is a status change
1007    * event - the message is a notification of something happening
1008    *         (for example, encryption being turned on)
1009    * %status% - See %status% in theme_adium_add_html ()
1010    */
1011
1012   /* This is slightly a hack, but it's the only way to add
1013    * arbitrary data to messages in the HTML. We add another
1014    * class called "x-empathy-message-id-*" to the message. This
1015    * way, we can remove the unread marker for this specific
1016    * message later. */
1017   tp_msg = empathy_message_get_tp_message (msg);
1018   if (tp_msg != NULL)
1019     {
1020       guint32 id;
1021       gboolean valid;
1022
1023       id = tp_message_get_pending_message_id (tp_msg, &valid);
1024       if (valid)
1025         g_string_append_printf (message_classes,
1026             " x-empathy-message-id-%u", id);
1027     }
1028
1029   /* Define javascript function to use */
1030   if (consecutive)
1031     func = self->priv->allow_scrolling ? js_funcs[ADD_CONSECUTIVE_MSG_SCROLL] :
1032       js_funcs[ADD_CONSECUTIVE_MSG_NO_SCROLL];
1033   else
1034     func = self->priv->allow_scrolling ? js_funcs[ADD_MSG_SCROLL] :
1035       js_funcs[ADD_MSG_NO_SCROLL];
1036
1037   if (empathy_contact_is_user (sender))
1038     {
1039       /* out */
1040       if (is_backlog)
1041         /* context */
1042         html = consecutive ? self->priv->data->out_nextcontext_html :
1043           self->priv->data->out_context_html;
1044       else
1045         /* content */
1046         html = consecutive ? self->priv->data->out_nextcontent_html :
1047           self->priv->data->out_content_html;
1048
1049       /* remove all the unread marks when we are sending a message */
1050       theme_adium_remove_all_focus_marks (self);
1051     }
1052   else
1053     {
1054       /* in */
1055       if (is_backlog)
1056         /* context */
1057         html = consecutive ? self->priv->data->in_nextcontext_html :
1058           self->priv->data->in_context_html;
1059       else
1060         /* content */
1061         html = consecutive ? self->priv->data->in_nextcontent_html :
1062           self->priv->data->in_content_html;
1063     }
1064
1065   direction = pango_find_base_dir (empathy_message_get_body (msg), -1);
1066
1067   theme_adium_add_html (self, func, html, body_escaped,
1068       avatar_filename, name_escaped, contact_id,
1069       service_name, message_classes->str,
1070       timestamp, is_backlog, empathy_contact_is_user (sender), direction);
1071
1072   /* Keep the sender of the last displayed message */
1073   if (*prev_contact)
1074     g_object_unref (*prev_contact);
1075
1076   *prev_contact = g_object_ref (sender);
1077   *prev_timestamp = timestamp;
1078   *prev_is_backlog = is_backlog;
1079
1080   g_free (body_escaped);
1081   g_free (name_escaped);
1082   g_string_free (message_classes, TRUE);
1083 }
1084
1085 void
1086 empathy_theme_adium_append_message (EmpathyThemeAdium *self,
1087     EmpathyMessage *msg,
1088     gboolean should_highlight)
1089 {
1090   const gchar *js_funcs[] = { "appendNextMessage",
1091       "appendNextMessageNoScroll",
1092       "appendMessage",
1093       "appendMessageNoScroll" };
1094
1095   if (self->priv->pages_loading != 0)
1096     {
1097       queue_item (&self->priv->message_queue, QUEUED_MESSAGE, msg, NULL,
1098           should_highlight, FALSE);
1099       return;
1100     }
1101
1102   theme_adium_add_message (self, msg, &self->priv->last_contact,
1103       &self->priv->last_timestamp, &self->priv->last_is_backlog,
1104       should_highlight, js_funcs);
1105 }
1106
1107 void
1108 empathy_theme_adium_append_event (EmpathyThemeAdium *self,
1109     const gchar *str)
1110 {
1111   gchar *str_escaped;
1112   PangoDirection direction;
1113
1114   if (self->priv->pages_loading != 0)
1115     {
1116       queue_item (&self->priv->message_queue, QUEUED_EVENT, NULL, str, FALSE, FALSE);
1117       return;
1118     }
1119
1120   direction = pango_find_base_dir (str, -1);
1121   str_escaped = g_markup_escape_text (str, -1);
1122   theme_adium_append_event_escaped (self, str_escaped, direction);
1123   g_free (str_escaped);
1124 }
1125
1126 void
1127 empathy_theme_adium_append_event_markup (EmpathyThemeAdium *self,
1128     const gchar *markup_text,
1129     const gchar *fallback_text)
1130 {
1131   PangoDirection direction;
1132
1133   direction = pango_find_base_dir (fallback_text, -1);
1134   theme_adium_append_event_escaped (self, markup_text, direction);
1135 }
1136
1137 void
1138 empathy_theme_adium_prepend_message (EmpathyThemeAdium *self,
1139     EmpathyMessage *msg,
1140     gboolean should_highlight)
1141 {
1142   const gchar *js_funcs[] = { "prependPrev",
1143       "prependPrev",
1144       "prepend",
1145       "prepend" };
1146
1147   if (self->priv->pages_loading != 0)
1148     {
1149       queue_item (&self->priv->message_queue, QUEUED_MESSAGE, msg, NULL,
1150           should_highlight, TRUE);
1151       return;
1152     }
1153
1154   theme_adium_add_message (self, msg, &self->priv->first_contact,
1155       &self->priv->first_timestamp, &self->priv->first_is_backlog,
1156       should_highlight, js_funcs);
1157 }
1158
1159 void
1160 empathy_theme_adium_edit_message (EmpathyThemeAdium *self,
1161     EmpathyMessage *message)
1162 {
1163   WebKitDOMDocument *doc;
1164   WebKitDOMElement *span;
1165   gchar *id, *parsed_body;
1166   gchar *tooltip, *timestamp;
1167   GtkIconInfo *icon_info;
1168   GError *error = NULL;
1169
1170   if (self->priv->pages_loading != 0)
1171     {
1172       queue_item (&self->priv->message_queue, QUEUED_EDIT, message, NULL, FALSE, FALSE);
1173       return;
1174     }
1175
1176   id = g_strdup_printf ("message-token-%s",
1177     empathy_message_get_supersedes (message));
1178   /* we don't pass a token here, because doing so will return another
1179    * <span> element, and we don't want nested <span> elements */
1180   parsed_body = theme_adium_parse_body (self,
1181     empathy_message_get_body (message), NULL);
1182
1183   /* find the element */
1184   doc = webkit_web_view_get_dom_document (WEBKIT_WEB_VIEW (self));
1185   span = webkit_dom_document_get_element_by_id (doc, id);
1186
1187   if (span == NULL)
1188     {
1189       DEBUG ("Failed to find id '%s'", id);
1190       goto except;
1191     }
1192
1193   if (!WEBKIT_DOM_IS_HTML_ELEMENT (span))
1194     {
1195       DEBUG ("Not a HTML element");
1196       goto except;
1197     }
1198
1199   /* update the HTML */
1200   webkit_dom_html_element_set_inner_html (WEBKIT_DOM_HTML_ELEMENT (span),
1201     parsed_body, &error);
1202
1203   if (error != NULL)
1204     {
1205       DEBUG ("Error setting new inner-HTML: %s", error->message);
1206       g_error_free (error);
1207       goto except;
1208     }
1209
1210   /* set a tooltip */
1211   timestamp = tpaw_time_to_string_local (
1212     empathy_message_get_timestamp (message),
1213     "%H:%M:%S");
1214   tooltip = g_strdup_printf (_("Message edited at %s"), timestamp);
1215
1216   webkit_dom_html_element_set_title (WEBKIT_DOM_HTML_ELEMENT (span),
1217     tooltip);
1218
1219   g_free (tooltip);
1220   g_free (timestamp);
1221
1222   /* mark this message as edited */
1223   icon_info = gtk_icon_theme_lookup_icon (gtk_icon_theme_get_default (),
1224     EMPATHY_IMAGE_EDIT_MESSAGE, 16, 0);
1225
1226   if (icon_info != NULL)
1227     {
1228       /* set the icon as a background image using CSS
1229        * FIXME: the icon won't update in response to theme changes */
1230       gchar *style = g_strdup_printf (
1231         "background-image:url('%s');"
1232         "background-repeat:no-repeat;"
1233         "background-position:left center;"
1234         "padding-left:19px;", /* 16px icon + 3px padding */
1235         gtk_icon_info_get_filename (icon_info));
1236
1237       webkit_dom_element_set_attribute (span, "style", style, &error);
1238
1239       if (error != NULL)
1240         {
1241           DEBUG ("Error setting element style: %s",
1242             error->message);
1243           g_clear_error (&error);
1244           /* not fatal */
1245         }
1246
1247       g_free (style);
1248       gtk_icon_info_free (icon_info);
1249     }
1250
1251   goto finally;
1252
1253 except:
1254   DEBUG ("Could not find message to edit with: %s",
1255     empathy_message_get_body (message));
1256
1257 finally:
1258   g_free (id);
1259   g_free (parsed_body);
1260 }
1261
1262 void
1263 empathy_theme_adium_scroll (EmpathyThemeAdium *self,
1264     gboolean allow_scrolling)
1265 {
1266   self->priv->allow_scrolling = allow_scrolling;
1267
1268   if (allow_scrolling)
1269     empathy_theme_adium_scroll_down (self);
1270 }
1271
1272 void
1273 empathy_theme_adium_scroll_down (EmpathyThemeAdium *self)
1274 {
1275   webkit_web_view_execute_script (WEBKIT_WEB_VIEW (self), "alignChat(true);");
1276 }
1277
1278 gboolean
1279 empathy_theme_adium_get_has_selection (EmpathyThemeAdium *self)
1280 {
1281   return webkit_web_view_has_selection (WEBKIT_WEB_VIEW (self));
1282 }
1283
1284 void
1285 empathy_theme_adium_clear (EmpathyThemeAdium *self)
1286 {
1287   theme_adium_load_template (self);
1288
1289   /* Clear last contact to avoid trying to add a 'joined'
1290    * message when we don't have an insertion point. */
1291   if (self->priv->last_contact)
1292     {
1293       g_object_unref (self->priv->last_contact);
1294       self->priv->last_contact = NULL;
1295     }
1296 }
1297
1298 gboolean
1299 empathy_theme_adium_find_previous (EmpathyThemeAdium *self,
1300     const gchar *search_criteria,
1301     gboolean new_search,
1302     gboolean match_case)
1303 {
1304   /* FIXME: Doesn't respect new_search */
1305   return webkit_web_view_search_text (WEBKIT_WEB_VIEW (self),
1306       search_criteria, match_case, FALSE, TRUE);
1307 }
1308
1309 gboolean
1310 empathy_theme_adium_find_next (EmpathyThemeAdium *self,
1311     const gchar *search_criteria,
1312     gboolean new_search,
1313     gboolean match_case)
1314 {
1315   /* FIXME: Doesn't respect new_search */
1316   return webkit_web_view_search_text (WEBKIT_WEB_VIEW (self),
1317       search_criteria, match_case, TRUE, TRUE);
1318 }
1319
1320 void
1321 empathy_theme_adium_find_abilities (EmpathyThemeAdium *self,
1322     const gchar *search_criteria,
1323     gboolean match_case,
1324     gboolean *can_do_previous,
1325     gboolean *can_do_next)
1326 {
1327   /* FIXME: Does webkit provide an API for that? We have wrap=true in
1328    * find_next and find_previous to work around this problem. */
1329   if (can_do_previous)
1330     *can_do_previous = TRUE;
1331   if (can_do_next)
1332     *can_do_next = TRUE;
1333 }
1334
1335 void
1336 empathy_theme_adium_highlight (EmpathyThemeAdium *self,
1337     const gchar *text,
1338     gboolean match_case)
1339 {
1340   webkit_web_view_unmark_text_matches (WEBKIT_WEB_VIEW (self));
1341   webkit_web_view_mark_text_matches (WEBKIT_WEB_VIEW (self),
1342       text, match_case, 0);
1343   webkit_web_view_set_highlight_text_matches (WEBKIT_WEB_VIEW (self),
1344       TRUE);
1345 }
1346
1347 void
1348 empathy_theme_adium_copy_clipboard (EmpathyThemeAdium *self)
1349 {
1350   webkit_web_view_copy_clipboard (WEBKIT_WEB_VIEW (self));
1351 }
1352
1353 static void
1354 theme_adium_remove_mark_from_message (EmpathyThemeAdium *self,
1355     guint32 id)
1356 {
1357   WebKitDOMDocument *dom;
1358   WebKitDOMNodeList *nodes;
1359   gchar *class;
1360   GError *error = NULL;
1361
1362   dom = webkit_web_view_get_dom_document (WEBKIT_WEB_VIEW (self));
1363   if (dom == NULL)
1364     return;
1365
1366   class = g_strdup_printf (".x-empathy-message-id-%u", id);
1367
1368   /* Get all nodes with focus class */
1369   nodes = webkit_dom_document_query_selector_all (dom, class, &error);
1370   g_free (class);
1371
1372   if (nodes == NULL)
1373     {
1374       DEBUG ("Error getting focus nodes: %s",
1375         error ? error->message : "No error");
1376       g_clear_error (&error);
1377       return;
1378     }
1379
1380   theme_adium_remove_focus_marks (self, nodes);
1381 }
1382
1383 static void
1384 theme_adium_remove_acked_message_unread_mark_foreach (gpointer data,
1385     gpointer user_data)
1386 {
1387   EmpathyThemeAdium *self = user_data;
1388   guint32 id = GPOINTER_TO_UINT (data);
1389
1390   theme_adium_remove_mark_from_message (self, id);
1391 }
1392
1393 void
1394 empathy_theme_adium_focus_toggled (EmpathyThemeAdium *self,
1395     gboolean has_focus)
1396 {
1397   self->priv->has_focus = has_focus;
1398   if (!self->priv->has_focus)
1399     {
1400       /* We've lost focus, so let's make sure all the acked
1401        * messages have lost their unread marker. */
1402       g_queue_foreach (&self->priv->acked_messages,
1403           theme_adium_remove_acked_message_unread_mark_foreach, self);
1404       g_queue_clear (&self->priv->acked_messages);
1405
1406       self->priv->has_unread_message = FALSE;
1407     }
1408 }
1409
1410 void
1411 empathy_theme_adium_message_acknowledged (EmpathyThemeAdium *self,
1412     EmpathyMessage *message)
1413 {
1414   TpMessage *tp_msg;
1415   guint32 id;
1416   gboolean valid;
1417
1418   tp_msg = empathy_message_get_tp_message (message);
1419
1420   if (tp_msg == NULL)
1421     return;
1422
1423   id = tp_message_get_pending_message_id (tp_msg, &valid);
1424   if (!valid)
1425     {
1426       g_warning ("Acknoledged message doesn't have a pending ID");
1427       return;
1428     }
1429
1430   /* We only want to actually remove the unread marker if the
1431    * view doesn't have focus. If we did it all the time we would
1432    * never see the unread markers, ever! So, we'll queue these
1433    * up, and when we lose focus, we'll remove the markers. */
1434   if (self->priv->has_focus)
1435     {
1436       g_queue_push_tail (&self->priv->acked_messages,
1437              GUINT_TO_POINTER (id));
1438       return;
1439     }
1440
1441   theme_adium_remove_mark_from_message (self, id);
1442 }
1443
1444 static gboolean
1445 theme_adium_context_menu_cb (EmpathyThemeAdium *self,
1446     GtkWidget *default_menu,
1447     WebKitHitTestResult *hit_test_result,
1448     gboolean triggered_with_keyboard,
1449     gpointer user_data)
1450 {
1451   GtkWidget *menu;
1452   EmpathyWebKitMenuFlags flags = EMPATHY_WEBKIT_MENU_CLEAR;
1453
1454   if (g_settings_get_boolean (self->priv->gsettings_chat,
1455         EMPATHY_PREFS_CHAT_WEBKIT_DEVELOPER_TOOLS))
1456     flags |= EMPATHY_WEBKIT_MENU_INSPECT;
1457
1458   menu = empathy_webkit_create_context_menu (
1459     WEBKIT_WEB_VIEW (self), hit_test_result, flags);
1460
1461   gtk_widget_show_all (menu);
1462
1463   gtk_menu_popup (GTK_MENU (menu), NULL, NULL, NULL, NULL, 3,
1464       gtk_get_current_event_time ());
1465
1466   return TRUE;
1467 }
1468
1469 void
1470 empathy_theme_adium_set_show_avatars (EmpathyThemeAdium *self,
1471     gboolean show_avatars)
1472 {
1473   self->priv->show_avatars = show_avatars;
1474 }
1475
1476 static void
1477 theme_adium_load_finished_cb (WebKitWebView *view,
1478     WebKitWebFrame *frame,
1479     gpointer user_data)
1480 {
1481   EmpathyThemeAdium *self = EMPATHY_THEME_ADIUM (view);
1482   GList *l;
1483
1484   DEBUG ("Page loaded");
1485   self->priv->pages_loading--;
1486
1487   if (self->priv->pages_loading != 0)
1488     return;
1489
1490   /* Display queued messages */
1491   for (l = self->priv->message_queue.head; l != NULL; l = l->next)
1492     {
1493       QueuedItem *item = l->data;
1494
1495       switch (item->type)
1496         {
1497           case QUEUED_MESSAGE:
1498             empathy_theme_adium_append_message (self, item->msg,
1499               item->should_highlight);
1500             break;
1501
1502           case QUEUED_EDIT:
1503             empathy_theme_adium_edit_message (self, item->msg);
1504             break;
1505
1506           case QUEUED_EVENT:
1507             empathy_theme_adium_append_event (self, item->str);
1508             break;
1509         }
1510
1511       free_queued_item (item);
1512     }
1513
1514   g_queue_clear (&self->priv->message_queue);
1515 }
1516
1517 static void
1518 theme_adium_finalize (GObject *object)
1519 {
1520   EmpathyThemeAdium *self = EMPATHY_THEME_ADIUM (object);
1521
1522   empathy_adium_data_unref (self->priv->data);
1523
1524   g_object_unref (self->priv->gsettings_chat);
1525   g_object_unref (self->priv->gsettings_desktop);
1526
1527   g_free (self->priv->variant);
1528
1529   G_OBJECT_CLASS (empathy_theme_adium_parent_class)->finalize (object);
1530 }
1531
1532 static void
1533 theme_adium_dispose (GObject *object)
1534 {
1535   EmpathyThemeAdium *self = EMPATHY_THEME_ADIUM (object);
1536
1537   if (self->priv->smiley_manager)
1538     {
1539       g_object_unref (self->priv->smiley_manager);
1540       self->priv->smiley_manager = NULL;
1541     }
1542
1543   g_clear_object (&self->priv->first_contact);
1544
1545   if (self->priv->last_contact)
1546     {
1547       g_object_unref (self->priv->last_contact);
1548       self->priv->last_contact = NULL;
1549     }
1550
1551   if (self->priv->inspector_window)
1552     {
1553       gtk_widget_destroy (self->priv->inspector_window);
1554       self->priv->inspector_window = NULL;
1555     }
1556
1557   if (self->priv->acked_messages.length > 0)
1558     {
1559       g_queue_clear (&self->priv->acked_messages);
1560     }
1561
1562   G_OBJECT_CLASS (empathy_theme_adium_parent_class)->dispose (object);
1563 }
1564
1565 static gboolean
1566 theme_adium_inspector_show_window_cb (WebKitWebInspector *inspector,
1567     EmpathyThemeAdium *self)
1568 {
1569   if (self->priv->inspector_window)
1570     {
1571       gtk_widget_show_all (self->priv->inspector_window);
1572     }
1573
1574   return TRUE;
1575 }
1576
1577 static gboolean
1578 theme_adium_inspector_close_window_cb (WebKitWebInspector *inspector,
1579     EmpathyThemeAdium *self)
1580 {
1581   if (self->priv->inspector_window)
1582     {
1583       gtk_widget_hide (self->priv->inspector_window);
1584     }
1585
1586   return TRUE;
1587 }
1588
1589 static WebKitWebView *
1590 theme_adium_inspect_web_view_cb (WebKitWebInspector *inspector,
1591     WebKitWebView *web_view,
1592     EmpathyThemeAdium *self)
1593 {
1594   GtkWidget *scrolled_window;
1595   GtkWidget *inspector_web_view;
1596
1597   if (!self->priv->inspector_window)
1598     {
1599       /* Create main window */
1600       self->priv->inspector_window = gtk_window_new (GTK_WINDOW_TOPLEVEL);
1601
1602       gtk_window_set_default_size (GTK_WINDOW (self->priv->inspector_window),
1603                  800, 600);
1604
1605       g_signal_connect (self->priv->inspector_window, "delete-event",
1606             G_CALLBACK (gtk_widget_hide_on_delete), NULL);
1607
1608       /* Pack a scrolled window */
1609       scrolled_window = gtk_scrolled_window_new (NULL, NULL);
1610
1611       gtk_scrolled_window_set_policy (GTK_SCROLLED_WINDOW (scrolled_window),
1612               GTK_POLICY_AUTOMATIC,
1613               GTK_POLICY_AUTOMATIC);
1614       gtk_container_add (GTK_CONTAINER (self->priv->inspector_window),
1615              scrolled_window);
1616       gtk_widget_show (scrolled_window);
1617
1618       /* Pack a webview in the scrolled window. That webview will be
1619        * used to render the inspector tool. */
1620       inspector_web_view = webkit_web_view_new ();
1621       gtk_container_add (GTK_CONTAINER (scrolled_window),
1622              inspector_web_view);
1623       gtk_widget_show (scrolled_window);
1624
1625       return WEBKIT_WEB_VIEW (inspector_web_view);
1626     }
1627
1628   return NULL;
1629 }
1630
1631 static void
1632 theme_adium_constructed (GObject *object)
1633 {
1634   EmpathyThemeAdium *self = EMPATHY_THEME_ADIUM (object);
1635   const gchar *font_family = NULL;
1636   gint font_size = 0;
1637   WebKitWebView *webkit_view = WEBKIT_WEB_VIEW (object);
1638   WebKitWebInspector *webkit_inspector;
1639
1640   /* Set default settings */
1641   font_family = tp_asv_get_string (self->priv->data->info, "DefaultFontFamily");
1642   font_size = tp_asv_get_int32 (self->priv->data->info, "DefaultFontSize", NULL);
1643
1644   if (font_family && font_size)
1645     {
1646       g_object_set (webkit_web_view_get_settings (webkit_view),
1647         "default-font-family", font_family,
1648         "default-font-size", font_size,
1649         NULL);
1650     }
1651   else
1652     {
1653       empathy_webkit_bind_font_setting (webkit_view,
1654         self->priv->gsettings_desktop,
1655         EMPATHY_PREFS_DESKTOP_INTERFACE_DOCUMENT_FONT_NAME);
1656     }
1657
1658   /* Setup webkit inspector */
1659   webkit_inspector = webkit_web_view_get_inspector (webkit_view);
1660   g_signal_connect (webkit_inspector, "inspect-web-view",
1661       G_CALLBACK (theme_adium_inspect_web_view_cb), object);
1662   g_signal_connect (webkit_inspector, "show-window",
1663       G_CALLBACK (theme_adium_inspector_show_window_cb), object);
1664   g_signal_connect (webkit_inspector, "close-window",
1665       G_CALLBACK (theme_adium_inspector_close_window_cb), object);
1666
1667   /* Load template */
1668   theme_adium_load_template (EMPATHY_THEME_ADIUM (object));
1669
1670   self->priv->in_construction = FALSE;
1671 }
1672
1673 static void
1674 theme_adium_get_property (GObject *object,
1675     guint param_id,
1676     GValue *value,
1677     GParamSpec *pspec)
1678 {
1679   EmpathyThemeAdium *self = EMPATHY_THEME_ADIUM (object);
1680
1681   switch (param_id)
1682     {
1683       case PROP_ADIUM_DATA:
1684         g_value_set_boxed (value, self->priv->data);
1685         break;
1686       case PROP_VARIANT:
1687         g_value_set_string (value, self->priv->variant);
1688         break;
1689       default:
1690         G_OBJECT_WARN_INVALID_PROPERTY_ID (object, param_id, pspec);
1691         break;
1692     };
1693 }
1694
1695 static void
1696 theme_adium_set_property (GObject *object,
1697     guint param_id,
1698     const GValue *value,
1699     GParamSpec *pspec)
1700 {
1701   EmpathyThemeAdium *self = EMPATHY_THEME_ADIUM (object);
1702
1703   switch (param_id)
1704     {
1705       case PROP_ADIUM_DATA:
1706         g_assert (self->priv->data == NULL);
1707         self->priv->data = g_value_dup_boxed (value);
1708         break;
1709       case PROP_VARIANT:
1710         empathy_theme_adium_set_variant (self, g_value_get_string (value));
1711         break;
1712       default:
1713         G_OBJECT_WARN_INVALID_PROPERTY_ID (object, param_id, pspec);
1714         break;
1715     };
1716 }
1717
1718 static void
1719 empathy_theme_adium_class_init (EmpathyThemeAdiumClass *klass)
1720 {
1721   GObjectClass *object_class = G_OBJECT_CLASS (klass);
1722
1723   object_class->finalize = theme_adium_finalize;
1724   object_class->dispose = theme_adium_dispose;
1725   object_class->constructed = theme_adium_constructed;
1726   object_class->get_property = theme_adium_get_property;
1727   object_class->set_property = theme_adium_set_property;
1728
1729   g_object_class_install_property (object_class, PROP_ADIUM_DATA,
1730       g_param_spec_boxed ("adium-data",
1731         "The theme data",
1732         "Data for the adium theme",
1733         EMPATHY_TYPE_ADIUM_DATA,
1734         G_PARAM_CONSTRUCT_ONLY |
1735         G_PARAM_READWRITE |
1736         G_PARAM_STATIC_STRINGS));
1737
1738   g_object_class_install_property (object_class, PROP_VARIANT,
1739       g_param_spec_string ("variant",
1740         "The theme variant",
1741         "Variant name for the theme",
1742         NULL,
1743         G_PARAM_CONSTRUCT |
1744         G_PARAM_READWRITE |
1745         G_PARAM_STATIC_STRINGS));
1746
1747   g_type_class_add_private (object_class, sizeof (EmpathyThemeAdiumPriv));
1748 }
1749
1750 static void
1751 empathy_theme_adium_init (EmpathyThemeAdium *self)
1752 {
1753   self->priv = G_TYPE_INSTANCE_GET_PRIVATE (self,
1754     EMPATHY_TYPE_THEME_ADIUM, EmpathyThemeAdiumPriv);
1755
1756   self->priv->in_construction = TRUE;
1757   g_queue_init (&self->priv->message_queue);
1758   self->priv->allow_scrolling = TRUE;
1759   self->priv->smiley_manager = empathy_smiley_manager_dup_singleton ();
1760
1761   /* Show avatars by default. */
1762   self->priv->show_avatars = TRUE;
1763
1764   g_signal_connect (self, "load-finished",
1765       G_CALLBACK (theme_adium_load_finished_cb), NULL);
1766   g_signal_connect (self, "navigation-policy-decision-requested",
1767         G_CALLBACK (theme_adium_navigation_policy_decision_requested_cb), NULL);
1768   g_signal_connect (self, "context-menu",
1769       G_CALLBACK (theme_adium_context_menu_cb), NULL);
1770
1771   self->priv->gsettings_chat = g_settings_new (EMPATHY_PREFS_CHAT_SCHEMA);
1772   self->priv->gsettings_desktop = g_settings_new (
1773     EMPATHY_PREFS_DESKTOP_INTERFACE_SCHEMA);
1774 }
1775
1776 EmpathyThemeAdium *
1777 empathy_theme_adium_new (EmpathyAdiumData *data,
1778     const gchar *variant)
1779 {
1780   g_return_val_if_fail (data != NULL, NULL);
1781
1782   return g_object_new (EMPATHY_TYPE_THEME_ADIUM,
1783       "adium-data", data,
1784       "variant", variant,
1785       NULL);
1786 }
1787
1788 void
1789 empathy_theme_adium_set_variant (EmpathyThemeAdium *self,
1790     const gchar *variant)
1791 {
1792   gchar *variant_path;
1793   gchar *script;
1794
1795   if (!tp_strdiff (self->priv->variant, variant))
1796     return;
1797
1798   g_free (self->priv->variant);
1799   self->priv->variant = g_strdup (variant);
1800
1801   if (self->priv->in_construction)
1802     return;
1803
1804   DEBUG ("Update view with variant: '%s'", variant);
1805   variant_path = adium_info_dup_path_for_variant (self->priv->data->info,
1806     self->priv->variant);
1807   script = g_strdup_printf ("setStylesheet(\"mainStyle\",\"%s\");",
1808       variant_path);
1809
1810   webkit_web_view_execute_script (WEBKIT_WEB_VIEW (self), script);
1811
1812   g_free (variant_path);
1813   g_free (script);
1814
1815   g_object_notify (G_OBJECT (self), "variant");
1816 }
1817
1818 void
1819 empathy_theme_adium_show_inspector (EmpathyThemeAdium *self)
1820 {
1821   WebKitWebView *web_view = WEBKIT_WEB_VIEW (self);
1822
1823   empathy_webkit_show_inspector (web_view);
1824 }
1825
1826 gboolean
1827 empathy_adium_path_is_valid (const gchar *path)
1828 {
1829   gboolean ret;
1830   gchar *file;
1831   gchar **tmp;
1832   const gchar *dir;
1833
1834   if (path[0] != '/')
1835     return FALSE;
1836
1837   /* The directory has to be *.AdiumMessageStyle per the Adium spec */
1838   tmp = g_strsplit (path, "/", 0);
1839   if (tmp == NULL)
1840     return FALSE;
1841
1842   dir = tmp[g_strv_length (tmp) - 1];
1843
1844   if (!g_str_has_suffix (dir, ".AdiumMessageStyle"))
1845     {
1846       g_strfreev (tmp);
1847       return FALSE;
1848     }
1849
1850   g_strfreev (tmp);
1851
1852   /* The theme is not valid if there is no Info.plist */
1853   file = g_build_filename (path, "Contents", "Info.plist",
1854          NULL);
1855   ret = g_file_test (file, G_FILE_TEST_EXISTS);
1856   g_free (file);
1857
1858   if (!ret)
1859     return FALSE;
1860
1861   /* We ship a default Template.html as fallback if there is any problem
1862    * with the one inside the theme. The only other required file is
1863    * Content.html OR Incoming/Content.html*/
1864   file = g_build_filename (path, "Contents", "Resources", "Content.html",
1865       NULL);
1866   ret = g_file_test (file, G_FILE_TEST_EXISTS);
1867   g_free (file);
1868
1869   if (!ret)
1870     {
1871       file = g_build_filename (path, "Contents", "Resources", "Incoming",
1872              "Content.html", NULL);
1873       ret = g_file_test (file, G_FILE_TEST_EXISTS);
1874       g_free (file);
1875     }
1876
1877   return ret;
1878 }
1879
1880 GHashTable *
1881 empathy_adium_info_new (const gchar *path)
1882 {
1883   gchar *file;
1884   GValue *value;
1885   GHashTable *info = NULL;
1886
1887   g_return_val_if_fail (empathy_adium_path_is_valid (path), NULL);
1888
1889   file = g_build_filename (path, "Contents", "Info.plist", NULL);
1890   value = empathy_plist_parse_from_file (file);
1891   g_free (file);
1892
1893   if (value == NULL)
1894     return NULL;
1895
1896   info = g_value_dup_boxed (value);
1897   tp_g_value_slice_free (value);
1898
1899   /* Insert the theme's path into the hash table,
1900    * keys have to be dupped */
1901   tp_asv_set_string (info, g_strdup ("path"), path);
1902
1903   return info;
1904 }
1905
1906 static guint
1907 adium_info_get_version (GHashTable *info)
1908 {
1909   return tp_asv_get_int32 (info, "MessageViewVersion", NULL);
1910 }
1911
1912 static const gchar *
1913 adium_info_get_no_variant_name (GHashTable *info)
1914 {
1915   const gchar *name = tp_asv_get_string (info, "DisplayNameForNoVariant");
1916   return name ? name : _("Normal");
1917 }
1918
1919 static gchar *
1920 adium_info_dup_path_for_variant (GHashTable *info,
1921     const gchar *variant)
1922 {
1923   guint version = adium_info_get_version (info);
1924   const gchar *no_variant = adium_info_get_no_variant_name (info);
1925   GPtrArray *variants;
1926   guint i;
1927
1928   if (version <= 2 && !tp_strdiff (variant, no_variant))
1929     return g_strdup ("main.css");
1930
1931   variants = empathy_adium_info_get_available_variants (info);
1932   if (variants->len == 0)
1933     return g_strdup ("main.css");
1934
1935   /* Verify the variant exists, fallback to the first one */
1936   for (i = 0; i < variants->len; i++)
1937     {
1938       if (!tp_strdiff (variant, g_ptr_array_index (variants, i)))
1939         break;
1940     }
1941
1942   if (i == variants->len)
1943     {
1944       DEBUG ("Variant %s does not exist", variant);
1945       variant = g_ptr_array_index (variants, 0);
1946     }
1947
1948   return g_strdup_printf ("Variants/%s.css", variant);
1949
1950 }
1951
1952 const gchar *
1953 empathy_adium_info_get_default_variant (GHashTable *info)
1954 {
1955   if (adium_info_get_version (info) <= 2)
1956     return adium_info_get_no_variant_name (info);
1957
1958   return tp_asv_get_string (info, "DefaultVariant");
1959 }
1960
1961 GPtrArray *
1962 empathy_adium_info_get_available_variants (GHashTable *info)
1963 {
1964   GPtrArray *variants;
1965   const gchar *path;
1966   gchar *dirpath;
1967   GDir *dir;
1968
1969   variants = tp_asv_get_boxed (info, "AvailableVariants", G_TYPE_PTR_ARRAY);
1970   if (variants != NULL)
1971     return variants;
1972
1973   variants = g_ptr_array_new_with_free_func (g_free);
1974   tp_asv_take_boxed (info, g_strdup ("AvailableVariants"),
1975     G_TYPE_PTR_ARRAY, variants);
1976
1977   path = tp_asv_get_string (info, "path");
1978   dirpath = g_build_filename (path, "Contents", "Resources", "Variants", NULL);
1979   dir = g_dir_open (dirpath, 0, NULL);
1980   if (dir != NULL)
1981     {
1982       const gchar *name;
1983
1984       for (name = g_dir_read_name (dir);
1985            name != NULL;
1986            name = g_dir_read_name (dir))
1987         {
1988           gchar *display_name;
1989
1990           if (!g_str_has_suffix (name, ".css"))
1991             continue;
1992
1993           display_name = g_strdup (name);
1994           strstr (display_name, ".css")[0] = '\0';
1995           g_ptr_array_add (variants, display_name);
1996         }
1997
1998       g_dir_close (dir);
1999     }
2000   g_free (dirpath);
2001
2002   if (adium_info_get_version (info) <= 2)
2003     g_ptr_array_add (variants,
2004       g_strdup (adium_info_get_no_variant_name (info)));
2005
2006   return variants;
2007 }
2008
2009 GType
2010 empathy_adium_data_get_type (void)
2011 {
2012   static GType type_id = 0;
2013
2014   if (!type_id)
2015     {
2016       type_id = g_boxed_type_register_static ("EmpathyAdiumData",
2017           (GBoxedCopyFunc) empathy_adium_data_ref,
2018           (GBoxedFreeFunc) empathy_adium_data_unref);
2019     }
2020
2021   return type_id;
2022 }
2023
2024 EmpathyAdiumData *
2025 empathy_adium_data_new_with_info (const gchar *path,
2026     GHashTable *info)
2027 {
2028   EmpathyAdiumData *data;
2029   gchar *template_html = NULL;
2030   gchar *footer_html = NULL;
2031   gchar *tmp;
2032
2033   g_return_val_if_fail (empathy_adium_path_is_valid (path), NULL);
2034
2035   data = g_slice_new0 (EmpathyAdiumData);
2036   data->ref_count = 1;
2037   data->path = g_strdup (path);
2038   data->basedir = g_strconcat (path, G_DIR_SEPARATOR_S "Contents"
2039     G_DIR_SEPARATOR_S "Resources" G_DIR_SEPARATOR_S, NULL);
2040   data->info = g_hash_table_ref (info);
2041   data->version = adium_info_get_version (info);
2042   data->strings_to_free = g_ptr_array_new_with_free_func (g_free);
2043   data->date_format_cache = g_hash_table_new_full (g_str_hash,
2044     g_str_equal, g_free, g_free);
2045
2046   DEBUG ("Loading theme at %s", path);
2047
2048 #define LOAD(path, var) \
2049     tmp = g_build_filename (data->basedir, path, NULL); \
2050     g_file_get_contents (tmp, &var, NULL, NULL); \
2051     g_free (tmp); \
2052
2053 #define LOAD_CONST(path, var) \
2054   { \
2055     gchar *content; \
2056     LOAD (path, content); \
2057     if (content != NULL) { \
2058       g_ptr_array_add (data->strings_to_free, content); \
2059     } \
2060     var = content; \
2061   }
2062
2063   /* Load html files */
2064   LOAD_CONST ("Content.html", data->content_html);
2065   LOAD_CONST ("Incoming/Content.html", data->in_content_html);
2066   LOAD_CONST ("Incoming/NextContent.html", data->in_nextcontent_html);
2067   LOAD_CONST ("Incoming/Context.html", data->in_context_html);
2068   LOAD_CONST ("Incoming/NextContext.html", data->in_nextcontext_html);
2069   LOAD_CONST ("Outgoing/Content.html", data->out_content_html);
2070   LOAD_CONST ("Outgoing/NextContent.html", data->out_nextcontent_html);
2071   LOAD_CONST ("Outgoing/Context.html", data->out_context_html);
2072   LOAD_CONST ("Outgoing/NextContext.html", data->out_nextcontext_html);
2073   LOAD_CONST ("Status.html", data->status_html);
2074   LOAD ("Template.html", template_html);
2075   LOAD ("Footer.html", footer_html);
2076
2077 #undef LOAD_CONST
2078 #undef LOAD
2079
2080   /* HTML fallbacks: If we have at least content OR in_content, then
2081    * everything else gets a fallback */
2082
2083 #define FALLBACK(html, fallback) \
2084   if (html == NULL) { \
2085     html = fallback; \
2086   }
2087
2088   /* in_nextcontent -> in_content -> content */
2089   FALLBACK (data->in_content_html,      data->content_html);
2090   FALLBACK (data->in_nextcontent_html,  data->in_content_html);
2091
2092   /* context -> content */
2093   FALLBACK (data->in_context_html,      data->in_content_html);
2094   FALLBACK (data->in_nextcontext_html,  data->in_nextcontent_html);
2095   FALLBACK (data->out_context_html,     data->out_content_html);
2096   FALLBACK (data->out_nextcontext_html, data->out_nextcontent_html);
2097
2098   /* out -> in */
2099   FALLBACK (data->out_content_html,     data->in_content_html);
2100   FALLBACK (data->out_nextcontent_html, data->in_nextcontent_html);
2101   FALLBACK (data->out_context_html,     data->in_context_html);
2102   FALLBACK (data->out_nextcontext_html, data->in_nextcontext_html);
2103
2104   /* status -> in_content */
2105   FALLBACK (data->status_html,          data->in_content_html);
2106
2107 #undef FALLBACK
2108
2109   /* template -> empathy's template */
2110   data->custom_template = (template_html != NULL);
2111   if (template_html == NULL)
2112     {
2113       GError *error = NULL;
2114
2115       tmp = empathy_file_lookup ("Template.html", "data");
2116
2117       if (!g_file_get_contents (tmp, &template_html, NULL, &error)) {
2118         g_warning ("couldn't load Empathy's default theme "
2119           "template: %s", error->message);
2120         g_return_val_if_reached (data);
2121       }
2122
2123       g_free (tmp);
2124     }
2125
2126   /* Default avatar */
2127   tmp = g_build_filename (data->basedir, "Incoming", "buddy_icon.png", NULL);
2128   if (g_file_test (tmp, G_FILE_TEST_EXISTS | G_FILE_TEST_IS_REGULAR))
2129     {
2130       data->default_incoming_avatar_filename = tmp;
2131     }
2132   else
2133     {
2134       g_free (tmp);
2135     }
2136
2137   tmp = g_build_filename (data->basedir, "Outgoing", "buddy_icon.png", NULL);
2138   if (g_file_test (tmp, G_FILE_TEST_EXISTS | G_FILE_TEST_IS_REGULAR))
2139     {
2140       data->default_outgoing_avatar_filename = tmp;
2141     }
2142   else
2143     {
2144       g_free (tmp);
2145     }
2146
2147   /* Old custom templates had only 4 parameters.
2148    * New templates have 5 parameters */
2149   if (data->version <= 2 && data->custom_template)
2150     {
2151       tmp = string_with_format (template_html,
2152         data->basedir,
2153         "%@", /* Leave variant unset */
2154         "", /* The header */
2155         footer_html ? footer_html : "",
2156         NULL);
2157     }
2158   else
2159     {
2160       tmp = string_with_format (template_html,
2161         data->basedir,
2162         data->version <= 2 ? "" : "@import url( \"main.css\" );",
2163         "%@", /* Leave variant unset */
2164         "", /* The header */
2165         footer_html ? footer_html : "",
2166         NULL);
2167     }
2168   g_ptr_array_add (data->strings_to_free, tmp);
2169   data->template_html = tmp;
2170
2171   g_free (template_html);
2172   g_free (footer_html);
2173
2174   return data;
2175 }
2176
2177 EmpathyAdiumData *
2178 empathy_adium_data_new (const gchar *path)
2179 {
2180   EmpathyAdiumData *data;
2181   GHashTable *info;
2182
2183   info = empathy_adium_info_new (path);
2184   data = empathy_adium_data_new_with_info (path, info);
2185   g_hash_table_unref (info);
2186
2187   return data;
2188 }
2189
2190 EmpathyAdiumData *
2191 empathy_adium_data_ref (EmpathyAdiumData *data)
2192 {
2193   g_return_val_if_fail (data != NULL, NULL);
2194
2195   g_atomic_int_inc (&data->ref_count);
2196
2197   return data;
2198 }
2199
2200 void
2201 empathy_adium_data_unref (EmpathyAdiumData *data)
2202 {
2203   g_return_if_fail (data != NULL);
2204
2205   if (g_atomic_int_dec_and_test (&data->ref_count)) {
2206     g_free (data->path);
2207     g_free (data->basedir);
2208     g_free (data->default_avatar_filename);
2209     g_free (data->default_incoming_avatar_filename);
2210     g_free (data->default_outgoing_avatar_filename);
2211     g_hash_table_unref (data->info);
2212     g_ptr_array_unref (data->strings_to_free);
2213     tp_clear_pointer (&data->date_format_cache, g_hash_table_unref);
2214
2215     g_slice_free (EmpathyAdiumData, data);
2216   }
2217 }
2218
2219 GHashTable *
2220 empathy_adium_data_get_info (EmpathyAdiumData *data)
2221 {
2222   g_return_val_if_fail (data != NULL, NULL);
2223
2224   return data->info;
2225 }
2226
2227 const gchar *
2228 empathy_adium_data_get_path (EmpathyAdiumData *data)
2229 {
2230   g_return_val_if_fail (data != NULL, NULL);
2231
2232   return data->path;
2233 }