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