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