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