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