Imported Upstream version 0.2.11
[debian/pino.git] / src / template.vala
1 /* template.vala
2  *
3  * Copyright (C) 2009-2010  troorl
4  *
5  * This program is free software: you can redistribute it and/or modify
6  * it under the terms of the GNU Lesser General Public License as published by
7  * the Free Software Foundation, either version 3 of the License, or
8  * (at your option) any later version.
9  *
10  * This program 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
13  * GNU Lesser General Public License for more details.
14  *
15  * You should have received a copy of the GNU Lesser General Public License
16  * along with this program.  If not, see <http://www.gnu.org/licenses/>.
17  *
18  * Author:
19  *      troorl <troorl@gmail.com>
20  */
21
22 using Gee;
23 using GLib;
24 using Auth;
25 using RestAPI;
26 using TimeUtils;
27
28 public class Template : Object {
29         
30         private Prefs prefs;
31         public SystemStyle gtk_style;
32         public Cache cache;
33         
34         private Accounts accounts;
35         private string login;
36         private string status_url;
37         private string search_url;
38         private string nick_url;
39         private string service;
40         
41         private string main_template;
42         private string status_template;
43         private string status_me_template;
44         private string status_direct_template;
45         
46         private Regex nicks;
47         private Regex tags;
48         private Regex groups;
49         private Regex urls;
50         private Regex clear_notice;
51         
52         public signal void emit_for_refresh();
53         
54         public Template(Prefs _prefs, Accounts _accounts, SystemStyle _gtk_style, Cache _cache) {
55                 prefs = _prefs;
56                 accounts = _accounts;
57                 gtk_style = _gtk_style;
58                 cache = _cache;
59                 reload();
60                 
61                 //compile regex
62                 /*
63                 nicks = new Regex("(^|\\s)@([A-Za-z0-9_]+)");
64                 tags = new Regex("((^|\\s)\\#[A-Za-z0-9_\\p{Latin}\\p{Greek}]+)");
65                 groups = new Regex("(^|\\s)!([A-Za-z0-9_]+)"); //for identi.ca groups
66                 urls = new Regex("((http|https|ftp)://([\\S]+))"); //need something better
67                 */
68                 nicks = new Regex("(^|\\s|['\"+&!/\\(-])@([A-Za-z0-9_]+)");
69                 tags = new Regex("(^|\\s|['\"+&!/\\(-])#([A-Za-z0-9_.-\\p{Latin}\\p{Greek}]+)");
70                 groups = new Regex("(^|\\s|['\"+&!/\\(-])!([A-Za-z0-9_]+)"); //for identi.ca groups
71                 urls = new Regex("((https?|ftp)://([A-Za-z0-9+&@#/%?=~_|!:,.;-]*)([A-Za-z0-9+&@#/%=~_|$]))"); // still needs to be improved for urls containing () such as wikipedia's
72                 
73                 // characters must be cleared to know direction of text
74                 clear_notice = new Regex("[: \n\t\r♻♺]+|@[^ ]+");
75                 
76                 prefs.roundedAvatarsChanged.connect(() => emit_for_refresh());
77                 prefs.opacityTweetsChanged.connect(() => emit_for_refresh());
78                 prefs.rtlChanged.connect(() => emit_for_refresh());
79                 prefs.fullNamesChanged.connect(() => emit_for_refresh());
80                 prefs.nativeLinkColorChanged.connect(() => emit_for_refresh());
81                 prefs.fontChanged.connect(() => emit_for_refresh());
82         }
83         
84         private void login_changed() {
85                 var acc = accounts.get_current_account();
86                 
87                 if(acc != null) {
88                         login = acc.login;
89                         service = acc.service;
90                         
91                         switch(service) {
92                                 case "twitter.com":
93                                         status_url = "http://twitter.com/%s/status/%s";
94                                         search_url = "http://twitter.com/#search?q=";
95                                         nick_url = "http://twitter.com/";
96                                         break;
97                                 
98                                 case "identi.ca":
99                                         status_url = "http://identi.ca/notice/%s";
100                                         search_url = "http://identi.ca/tag/";
101                                         nick_url = "http://identi.ca/";
102                                         break;
103                         }
104                 }
105         }
106         
107         public void refresh_gtk_style(SystemStyle _gtk_style) {
108                 gtk_style = _gtk_style;
109                 emit_for_refresh();
110         }
111         
112         private string generate(string content) {
113                 //rounded userpics
114                 string rounded_str = "";
115                 if(prefs.roundedAvatars)
116                         rounded_str = "-webkit-border-radius:5px;";
117                 
118                 var map = new HashMap<string, string>();
119                 map["bg_color"] = gtk_style.bg_color;
120                 map["fg_color"] = gtk_style.fg_color;
121                 map["rounded"] = rounded_str;
122                 map["lt_color"] = gtk_style.lt_color;
123                 map["sl_color"] = gtk_style.sl_color;
124                 map["lg_color"] = gtk_style.lg_color;
125                 map["dr_color"] = gtk_style.dr_color;
126                 map["tweets_opacity"] = prefs.opacityTweets;
127                 map["font_size"] = prefs.deFontSize.to_string();
128                 map["font_size_small"] = (prefs.deFontSize - 1).to_string();
129                 map["fresh_color"] = prefs.freshColor;
130                 
131                 if(prefs.nativeLinkColor)
132                         map["link_color"] = gtk_style.lk_color;
133                 else
134                         map["link_color"] = gtk_style.sl_color;
135                 
136                 map["font_name"] = prefs.deFontName;
137                 map["main_content"] = content;
138                 
139                 return render(main_template, map);
140         }
141         
142         /* just render something in the center of the screen */
143         public string center_render(string content) {
144                 string result = """<table width="100%" height="100%">
145                 <tr>
146                 <td align="center">%s</td>
147                 </tr>
148                 </table>""".printf(content);
149                 
150                 return result;
151         }
152         
153         /* render start screen */
154         public string generate_message(string message) {
155                 string content = "<h2>%s</h2>".printf(message);
156                 
157                 return generate(center_render(content));
158         }
159         
160         /* render user-show form */
161         public string generate_user_show_form() {
162                 string content = """<form method="GET" action="userinfo://">
163                 <input type="text" name="user" /><input type="submit" value="%s" />
164                 </form>""".printf(_("Show user"));
165                 
166                 return generate(center_render(content));
167         }
168         
169         /* render direct inbox */
170         public string generate_direct(Gee.ArrayList<Status> friends, int last_focused) {
171                 //changing locale to C
172                 string currentLocale = GLib.Intl.setlocale(GLib.LocaleCategory.TIME, null);
173                 GLib.Intl.setlocale(GLib.LocaleCategory.TIME, "C");
174                 
175                 login_changed();
176                 
177                 string content = "";
178                 
179                 var now = get_current_time();
180                 
181                 var reply_text = _("Reply");
182                 var delete_text = _("Delete");
183                 var dm_text = _("Direct message");
184                 
185                 //rounded userpics
186                 string rounded_str = "";
187                 if(prefs.roundedAvatars)
188                         rounded_str = "-webkit-border-radius:5px;";
189                 
190                 foreach(Status i in friends) {
191                         string text = strip_tags_plus(i.text);
192                         
193                         //checking for new statuses
194                         var fresh = "old";
195                         if(last_focused > 0 && (int)i.created_at.mktime() > last_focused)
196                                 fresh = "fresh";
197                         
198                         //making human-readable time/date
199                         string time = time_to_human_delta(i.created_at_s, i.created_at);
200                         
201                         var user_avatar = i.user_avatar;
202                         var name = i.user_screen_name;
203                         var screen_name = i.user_screen_name;
204                         
205                         var map = new HashMap<string, string>();
206                         map["avatar"] = cache.get_or_download(user_avatar, Cache.Method.ASYNC, false);
207                         map["fresh"] = fresh;
208                         map["id"] = i.id;
209                         map["screen_name"] = screen_name;
210                         
211                         if(prefs.fullNames)
212                                 map["name"] = name;
213                         else
214                                 map["name"] = screen_name;
215                         
216                         map["time"] = time;
217                         map["content"] = making_links(text);
218                         
219                         if(prefs.rtlSupport && is_rtl(clear_notice.replace(text, -1, 0, "")))
220                                 map["rtl_class"] = "rtl-notice";
221                         else
222                                 map["rtl_class"] = "norm-notice";
223                         
224                         map["delete_text"] = delete_text;
225                         map["dm_text"] = dm_text;
226                         map["delete"] = Config.DELETE_PATH;
227                         map["direct_reply"] = Config.DIRECT_REPLY_PATH;
228                         content += render(status_direct_template, map);
229                 }
230                 
231                 //back to the normal locale
232                 GLib.Intl.setlocale(GLib.LocaleCategory.TIME, currentLocale);
233                 
234                 return generate(content);
235         }
236         
237         /* render timeline, mentions */
238         public string generate_timeline(Gee.ArrayList<Status> friends,
239                 int last_focused, bool with_favorites = false) {
240                 
241                 //changing locale to C
242                 string currentLocale = GLib.Intl.setlocale(GLib.LocaleCategory.TIME, null);
243                 GLib.Intl.setlocale(GLib.LocaleCategory.TIME, "C");
244                 
245                 login_changed();
246                 
247                 string content = "";
248                 
249                 var now = get_current_time();
250                 
251                 var reply_text = _("Reply");
252                 var delete_text = _("Delete");
253                 var retweet_text = _("Retweet");
254                 var dm_text = _("Direct message");
255                 
256                 //rounded userpics
257                 string rounded_str = "";
258                 if(prefs.roundedAvatars)
259                         rounded_str = "-webkit-border-radius:5px;";
260                 
261                 foreach(Status i in friends) {
262                         //warning(i.is_favorite.to_string());
263                         string text = strip_tags_plus(i.text);
264                         
265                         //checking for new statuses
266                         var fresh = "old";
267                         if(last_focused > 0 && (int)i.created_at.mktime() > last_focused)
268                                 fresh = "fresh";
269                         
270                         //making human-readable time/date
271                         string time = time_to_human_delta(i.created_at_s, i.created_at);
272                         
273                         var by_who = "";
274                                 
275                         if(i.to_user != "") { // in reply to
276                                 string to_user = i.to_user;
277                                 if(to_user == login)
278                                         to_user = _("you");
279                                 
280                                 by_who = "<a class='by_who' href='showtree://%s'>%s %s</a>".printf(i.id, _("in reply to"), to_user);
281                         }
282                         
283                         if(i.user_screen_name == login) { //your own status
284                                 var map = new HashMap<string, string>();
285                                 map["avatar"] = cache.get_or_download(i.user_avatar, Cache.Method.ASYNC, false);
286                                 map["me"] = "me";
287                                 map["id"] = i.id;
288                                 map["time"] = time;
289                                 map["by_who"] = by_who;
290                                 
291                                 if(prefs.fullNames)
292                                         map["name"] = i.user_name;
293                                 else
294                                         map["name"] = i.user_screen_name;
295                         
296                                 map["content"] = making_links(strip_tags_plus(text));
297
298                                 if(prefs.rtlSupport && is_rtl(clear_notice.replace(text, -1, 0, "")))
299                                         map["rtl_class"] = "rtl-notice";
300                                 else
301                                         map["rtl_class"] = "norm-notice";
302                                 
303                                 map["delete_text"] = delete_text;
304                                 map["delete"] = Config.DELETE_PATH;
305                                 content += render(status_me_template, map);
306                                 
307                         } else {
308                                 var re_icon = "";
309                                 
310                                 var user_avatar = i.user_avatar;
311                                 var name = i.user_name;
312                                 var screen_name = i.user_screen_name;
313                                 var favorite_text = _("Add to favorite");
314                                 var defavorite_text = _("Remove from favorite");
315                                 
316                                 if(i.is_retweet) {
317                                         re_icon = "<span class='re'>Rt:</span> ";
318                                         by_who = "<a class='by_who' href='userinfo://%s'>by %s</a>".printf(i.user_screen_name, i.user_screen_name);
319                                         name = i.re_user_name;
320                                         screen_name = i.re_user_screen_name;
321                                         user_avatar = i.re_user_avatar;
322                                         text = strip_tags_plus(i.re_text);
323                                 }
324                                 
325                                 var map = new HashMap<string, string>();
326                                 map["avatar"] = cache.get_or_download(user_avatar, Cache.Method.ASYNC, false);
327                                 map["fresh"] = fresh;
328                                 map["id"] = i.id;
329                                 map["re_icon"] = re_icon;
330                                 map["screen_name"] = screen_name;
331                                 
332                                 if(prefs.fullNames)
333                                         map["name"] = name;
334                                 else
335                                         map["name"] = screen_name;
336                         
337                                 map["time"] = time;
338                                 
339                                 if(with_favorites) {
340                                         if(i.is_favorite) {
341                                                 map["favorite"] = """<a class="favorite" href="favorited://%s"><img id="fav_%s" src="%s" />""".printf(i.id, i.id, Config.FAVORITE_PATH);
342                                         } else {
343                                                 map["favorite"] = """<a class="favorite" href="favorited://%s"><img id="fav_%s" src="%s" />""".printf(i.id, i.id, Config.FAVORITE_NO_PATH);
344                                         }
345                                 } else {
346                                         map["favorite"] = "";
347                                 }
348                                 
349                                 map["content"] = making_links(text);
350                                 
351                                 if(prefs.rtlSupport && is_rtl(clear_notice.replace(text, -1, 0, "")))
352                                         map["rtl_class"] = "rtl-notice";
353                                 else
354                                         map["rtl_class"] = "norm-notice";
355                                 
356                                 map["by_who"] = by_who;
357                                 map["retweet_text"] = retweet_text;
358                                 map["reply_text"] = reply_text;
359                                 map["direct_reply"] = Config.DIRECT_REPLY_PATH;
360                                 map["dm_text"] = dm_text;
361                                 map["reply"] = Config.REPLY_PATH;
362                                 map["re_tweet"] = Config.RETWEET_PATH;
363                                 content += render(status_template, map);
364                         }
365                 }
366                 
367                 //back to the normal locale
368                 GLib.Intl.setlocale(GLib.LocaleCategory.TIME, currentLocale);
369                 
370                 return generate(content);
371         }
372         
373         private string render(string text, HashMap<string, string> map) {
374                 string result = text;
375                 
376                 foreach(string key in map.keys) {
377                         var pat = new Regex("{{" + key + "}}");
378                         result = pat.replace(result, -1, 0, map[key]);
379                 }
380                 return result;
381         }
382         
383         private string making_links(string text) {
384                 string result = text;
385                 
386                 //result = urls.replace(text, -1, 0, "<a href='\\0'>\\0</a>");
387                 
388                 //I hate glib regex......
389                 int pos = 0;
390                 while(true) {
391                         //url cutting
392                         MatchInfo match_info;
393                         bool bingo = urls.match_all_full(text, -1, pos, GLib.RegexMatchFlags.NEWLINE_ANY, out match_info);
394                         if(bingo) {
395                                 foreach(string s in match_info.fetch_all()) {
396                                         if(s.length > 30) {
397                                                 result = result.replace(s, "<a href='%s' title='%s'>%s...</a>".printf(s, s, s.substring(0, 30)));
398                                         } else {
399                                                 result = result.replace(s, "<a href='%s'>%s</a>".printf(s, s));
400                                         }
401                                         
402                                         match_info.fetch_pos(0, null, out pos);
403                                         break;
404                                 }
405                         } else break;
406                 }
407                 
408                 result = nicks.replace(result, -1, 0, "\\1@<a class='re_nick' href='userinfo://\\2'>\\2</a>");
409                 //result = tags.replace(result, -1, 0, "<a class='tags' href='%s\\1'>\\1</a>".printf(search_url));
410                 result = tags.replace(result, -1, 0, "\\1#<a class='tags' href='%s\\2'>\\2</a>".printf(search_url));
411                 
412                 if(service == "identi.ca") //for identi.ca only
413                         result = groups.replace(result, -1, 0, "\\1!<a class='tags' href='http://identi.ca/group/\\2'>\\2</a>");
414                 
415                 return result;
416         }
417         
418         private string time_to_human_delta(string created_at_s, Time t) {
419                 int delta = TimeParser.time_to_diff(created_at_s);//(int)(now.mktime() - t.mktime());
420                 
421                 if(delta < 30)
422                         return _("a few seconds ago");
423                 if(delta < 120)
424                         return _("1 minute ago");
425                 if(delta < 3600)
426                         return _("%i minutes ago").printf(delta / 60);
427                 if(delta < 7200)
428                         return _("about 1 hour ago");
429                 if(delta < 86400)
430                         return _("about %i hours ago").printf(delta / 3600);
431                 
432                 return str_to_time(created_at_s).format("%k:%M %b %d %Y");
433         }
434         
435         public void reload() {
436                 //load templates
437                 main_template = load_template(Config.TEMPLATES_PATH + "/main.tpl");
438                 status_template = load_template(Config.TEMPLATES_PATH + "/status.tpl");
439                 status_me_template = load_template(Config.TEMPLATES_PATH + "/status_me.tpl");
440                 status_direct_template = load_template(Config.TEMPLATES_PATH + "/status_direct.tpl");
441         }
442         
443         private string load_template(string path) {
444                 var file = File.new_for_path(path);
445                 
446                 if(!file.query_exists(null)) {
447                         stderr.printf("File '%s' doesn't exist.\n", file.get_path());
448                         //return 1
449                 }
450                 
451                 var in_stream = new DataInputStream (file.read(null));
452                 
453                 string result = "";
454                 string tmp = "";
455                 while((tmp = in_stream.read_line(null, null)) != null)
456                         result += tmp;
457                 tmp = null;
458                 in_stream = null;
459                 return result;
460         }
461         
462         /* Right-to-left languages detection by Behrooz Shabani <everplays@gmail.com> */
463         private bool is_rtl(string inStr){
464                 unichar cc = inStr[0]; // first character code
465                 if(cc>=1536 && cc<=1791) // arabic, persian, ...
466                         return true;
467                 if(cc>=65136 && cc<=65279) // arabic peresent 2
468                         return true;
469                 if(cc>=64336 && cc<=65023) // arabic peresent 1
470                         return true;
471                 if(cc>=1424 && cc<=1535) // hebrew
472                         return true;
473                 if(cc>=64256 && cc<=64335) // hebrew peresent
474                         return true;
475                 if(cc>=1792 && cc<=1871) // Syriac
476                         return true;
477                 if(cc>=1920 && cc<=1983) // Thaana
478                         return true;
479                 if(cc>=1984 && cc<=2047) // NKo
480                         return true;
481                 if(cc>=11568 && cc<=11647) // Tifinagh
482                         return true;
483                 return false;
484         }
485         
486         /* Performaing to show in html context */
487         private string strip_tags_plus(owned string content) {
488                 content = content.replace("\\", "&#92;");
489                 content = content.replace("<", "&lt;");
490                 content = content.replace(">", "&gt;");
491                 
492                 return content;
493         }
494 }