Merge commit 'upstream/0.2.85' into experimental
[debian/pino.git] / src / status_delegate.vala
1 using Gtk;
2 using Cairo;
3
4 /** Separate class for status */
5 public class StatusDelegate : EventBox {
6         
7         private Status? status = null;
8         private AStream? stream = null;
9         
10         private VBox vb_main;
11         private BgBox hb_main;
12         
13         private Avatar avatar;
14         private Label nick;
15         private Label date;
16         private WrapLabel content;
17         private ReplyLabel re_label;
18         private bool already_expanded = false;
19         private ConversationView? con_view = null;
20         private VBox vb_right;
21         
22         private SmartTimer timer;
23         
24         private Gdk.Pixbuf? rt_pixbuf = null;
25         private const double MAX_RGB = (double) uint16.MAX;
26         
27         private string date_string = "<small><span foreground='#888'><b>%s</b></span></small>";
28         
29         private Regex nicks;
30         private Regex tags;
31         private Regex groups;
32         private Regex urls;
33         private Regex clear_notice;
34         
35         public StatusDelegate(Status status, AStream stream) {
36                 nicks = new Regex("(^|\\s|['\"+&!/\\(-])@([A-Za-z0-9_]+)");
37                 tags = new Regex("(^|\\s|['\"+&!/\\(-])#([A-Za-z0-9_.-\\p{Latin}\\p{Greek}]+)");
38                 groups = new Regex("(^|\\s|['\"+&!/\\(-])!([A-Za-z0-9_]+)"); //for identi.ca groups
39                 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
40                 
41                 // characters must be cleared to know direction of text
42                 clear_notice = new Regex("[: \n\t\r♻♺]+|@[^ ]+");
43                 
44                 rt_pixbuf = new Gdk.Pixbuf.from_file(Config.RT_PATH);
45                 
46                 this.status = status;
47                 this.stream = stream;
48                 
49                 set_events(Gdk.EventMask.BUTTON_RELEASE_MASK);
50                 set_events(Gdk.EventMask.BUTTON_PRESS_MASK);
51                 button_release_event.connect(on_click);
52                 button_press_event.connect(on_double_click);
53                 
54                 vb_main = new VBox(false, 0);
55                 
56                 hb_main = new BgBox(false, 0);
57                 hb_main.fresh = status.fresh;
58                 hb_main.favorited = status.favorited;
59                 
60                 //update background if fresh status changed
61                 status.notify["fresh"].connect((s, p) => {
62                         hb_main.fresh = ((Status) s).fresh;
63                 });
64                 
65                 status.notify["favorited"].connect((s, p) => {
66                         hb_main.favorited = ((Status) s).favorited;
67                 });
68                 
69                 VBox vb_avatar = new VBox(false, 0);
70                 vb_right = new VBox(false, 0);
71                 HBox hb_header = new HBox(false, 0);
72                 
73                 //check if retweet
74                 string av_url = "";
75                 if(status.retweet == null)
76                         av_url = status.user.pic;
77                 else
78                         av_url = status.retweet.user.pic;
79                 
80                 avatar = new Avatar.from_url(av_url, 48);
81                 vb_avatar.pack_start(avatar, false, false, 4);
82                 
83                 //avatar.load_pic();
84                 
85                 //header
86                 nick = new Label(null);
87                 string user_name = "";
88                 if(status.retweet == null)
89                         user_name = status.user.name;
90                 else
91                         user_name = status.retweet.user.name;
92                 
93                 nick.set_markup("<b>%s</b>".printf(user_name));
94                 date = new Label(null);
95                 date.set_markup(date_string.printf(time_to_human_delta(status.created)));
96                 
97                 timer = new SmartTimer(60);
98                 timer.timeout.connect(update_date);
99                 
100                 hb_header.pack_start(nick, false, false, 0);
101                 hb_header.pack_end(date, false, false, 0);
102                 
103                 //content
104                 content = new WrapLabel();
105                 content.set_markup_plus(format_content(status.content));
106                 content.link_activated.connect(uri_route);
107                 main_window.set_focus.connect(unfocused);
108                 
109                 vb_right.pack_start(hb_header, false, false, 4);
110                 vb_right.pack_start(content, false, false, 0);
111                 
112                 if(status.retweet != null) {
113                         HBox re_box = new HBox(false, 0);
114                         Label re_label = new Label(null);
115                         re_label.set_markup("<small><b><span foreground='#888'>%s </span>%s</b></small>".printf(_("retweeted by"), status.user.name));
116                         
117                         Avatar re_avatar = new Avatar.from_url(status.user.pic, 18);
118                         
119                         re_box.pack_start(re_avatar, false, false, 0);
120                         re_box.pack_start(re_label, false, false, 4);
121                         vb_right.pack_start(re_box, false, false, 4);
122                 } else {
123                         if(status.reply != null) {
124                                 /*HBox re_box = new HBox(false, 0);
125                                 
126                                 Image re_img = new Image.from_file(Config.CONVERSATION_PATH);
127                                 Label re_label = new Label(null);
128                                 re_label.set_markup("<small><b><span foreground='#888'>in reply to </span>%s</b></small>".printf(status.reply.name));
129                                 
130                                 re_box.pack_start(re_img, false, false, 0);
131                                 re_box.pack_start(re_label, false, false, 4);*/
132                                 
133                                 re_label = new ReplyLabel(status.reply.name);
134                                 re_label.set_tooltip(_("Show conversation"));
135                                 
136                                 re_label.clicked.connect(re_label_clicked);
137                                 
138                                 vb_right.pack_start(re_label, false, false, 4);
139                                 
140                                 status.new_reply.connect(add_new_reply);
141                                 
142                                 status.end_reply.connect(end_reply);
143                                 
144                         } else {
145                                 HBox spacer = new HBox(false, 0);
146                                 spacer.set_size_request(1, 4);
147                                 vb_right.pack_start(spacer, false, false, 0);
148                         }
149                 }
150                 
151                 hb_main.pack_start(vb_avatar, false, false, 4);
152                 hb_main.pack_start(vb_right, true, true, 4);
153                 
154                 vb_main.pack_start(hb_main, true, true, 0);
155                 
156                 if(status.own) {
157                         hb_main.reorder_child(vb_right, 0);
158                         
159                         hb_header.remove(nick);
160                         hb_header.remove(date);
161                         hb_header.pack_start(date, false, false, 0);
162                         hb_header.pack_end(nick, false, false, 0);
163                 }
164                 
165                 add(vb_main);
166                 
167                 //set bg color
168                 Gdk.Color color = Gdk.Color();
169                 Gdk.Color.parse("white", out color);
170                 
171                 modify_bg(StateType.NORMAL, color);
172         }
173         
174         private void re_label_clicked() {
175                 re_label.set_sensitive(false);
176                 re_label.start();
177                 
178                 /*
179                 if(already_expanded) {
180                         if(con_view.visible) {
181                                 con_view.hide_all();
182                                 re_label.set_tooltip(tooltip_show);
183                         } else {
184                                 con_view.show_all();
185                                 re_label.set_tooltip(tooltip_hide);
186                         }
187                         
188                         return;
189                 }*/
190                 
191                 stream.account.get_conversation(status);
192                 
193                 //already_expanded = true;
194         }
195         
196         private void add_new_reply(Status nstatus) { //new reply received
197                 debug(nstatus.id);
198                 
199                 if(con_view == null) {
200                         debug("ok");
201                         con_view = new ConversationView();
202                         vb_main.pack_start(con_view, false, false, 0);
203                         con_view.show_all();
204                 }
205                 
206                 con_view.add_delegate(new StatusDelegate(nstatus, stream));
207         }
208         
209         private void end_reply() {
210                 re_label.stop();
211                 re_label.set_tooltip("");
212         }
213         
214         private void update_date() {
215                 date.set_markup(date_string.printf(time_to_human_delta(status.created)));
216         }
217         
218         /** Any click makes it not fresh */
219         private bool on_click(Gdk.EventButton event) {
220                 switch(event.button) {
221                 case 1: //left click
222                         if(!status.fresh)
223                                 return true;
224                         
225                         status.fresh = false;
226                         debug("ok");
227                         return true;
228                 
229                 case 3: //context menu
230                         stream.account.context_menu(stream, status);
231                         return true;
232                 
233                 default:
234                         return false;
235                 }
236         }
237         
238         private bool on_double_click(Gdk.EventButton event) {
239                 if(event.type != Gdk.EventType.2BUTTON_PRESS)
240                         return false;
241                 
242                 debug("double click");
243                 content.set_selectable(true);
244                 main_window.set_focus(content);
245                 
246                 return true;
247         }
248         
249         private void unfocused(Widget? widget) {
250                 if(widget == null || widget == content)
251                         return;
252                 
253                 content.set_selectable(false);
254         }
255         
256         private void uri_route(string prot, string uri) {
257                 switch(prot) {
258                 case "search":
259                         stream.account.go_hashtag(uri);
260                         break;
261                 
262                 case "userinfo":
263                         debug("not implemented");
264                         break;
265                 
266                 default:
267                         GLib.Pid pid;
268                         GLib.Process.spawn_async(".", {"/usr/bin/xdg-open", prot + "://" + uri}, null,
269                                 GLib.SpawnFlags.STDOUT_TO_DEV_NULL, null, out pid);
270                         break;
271                 }
272         }
273         
274         /** Here we draw some things like retweet indicator and others */
275         public override bool expose_event(Gdk.EventExpose event) {
276                 if(status == null || status.retweet == null)
277                         return base.expose_event(event);
278                 
279                 Context ctx = Gdk.cairo_create(this.window);
280                 
281                 bool answer = base.expose_event(event);
282                 
283                 if(rt_pixbuf != null) {
284                         Gdk.Rectangle big_rect = {0, 0 , 48, 48};
285                         Gdk.cairo_rectangle(ctx, big_rect);
286                         Gdk.cairo_set_source_pixbuf(ctx, rt_pixbuf, big_rect.x,
287                                 big_rect.y);
288                         
289                         ctx.fill();
290                 }
291                 
292                 return false;
293         }
294         
295         /** Convert status time to human readable string */
296         private string time_to_human_delta(string created, bool is_search = false) {
297                 string currentLocale = GLib.Intl.setlocale(GLib.LocaleCategory.TIME, null);
298                 GLib.Intl.setlocale(GLib.LocaleCategory.TIME, "C");
299                 
300                 int delta = TimeParser.time_to_diff(created, is_search);
301                 
302                 if(delta < 30)
303                         return _("a few seconds ago");
304                 if(delta < 120) {
305                         //timer.set_interval(120);
306                         return _("1 minute ago");
307                 }
308                 if(delta < 3600) {
309                         timer.set_interval(300);
310                         return _("%i minutes ago").printf(delta / 60);
311                 }
312                 if(delta < 7200) {
313                         timer.set_interval(3600);
314                         return _("about 1 hour ago");
315                 }
316                 if(delta < 86400) {
317                         timer.set_interval(3600);
318                         return _("about %i hours ago").printf(delta / 3600);
319                 }
320                 
321                 timer.set_interval(0);
322                 
323                 GLib.Intl.setlocale(GLib.LocaleCategory.TIME, currentLocale);
324                 
325                 return TimeUtils.str_to_time(created).format("%k:%M %b %d %Y");
326         }
327         
328         /** Performaing to show in markup context */
329         private string strip_tags_plus(owned string content) {
330                 //content = content.replace("\\", "&#92;");
331                 content = Markup.escape_text(content);
332                 //content = content.replace("<", "&lt;");
333                 //content = content.replace(">", "&gt;");
334                 
335                 return content;
336         }
337         
338         private string format_content(owned string data) {
339                 data = strip_tags_plus(data);
340                 
341                 string tmp = data;
342                 
343                 int pos = 0;
344                 while(true) {
345                         //url cutting
346                         MatchInfo match_info;
347                         bool bingo = urls.match_all_full(tmp, -1, pos, GLib.RegexMatchFlags.NEWLINE_ANY, out match_info);
348                         if(bingo) {
349                                 foreach(string s in match_info.fetch_all()) {
350                                         if(s.length > 30) {
351                                                 data = data.replace(s, """<a href="%s" title="%s">%s...</a>""".printf(s, s, s.substring(0, 30)));
352                                         } else {
353                                                 data = data.replace(s, """<a href="%s">%s</a>""".printf(s, s));
354                                         }
355                                         
356                                         match_info.fetch_pos(0, null, out pos);
357                                         break;
358                                 }
359                         } else break;
360                 }
361                 debug(visual_style.fg_color);
362                 data = nicks.replace(data, -1, 0, "\\1<b><a href='userinfo://\\2'><span foreground='=fg-color='>@\\2</span></a></b>");
363                 debug(data);
364                 //data.printf("foreground='#ccc'");
365                 data = tags.replace(data, -1, 0, "\\1<b><a href='search://\\2'>#\\2</a></b>");
366                 
367                 data = groups.replace(data, -1, 0, "\\1<b>!<a href='http://identi.ca/group/\\2'>\\2</a></b>");
368                 data = data.replace("=fg-color=", visual_style.fg_color);
369                 return data;
370         }
371 }