1/*  This file is part of Cawbird, a Gtk+ linux Twitter client forked from Corebird.
2 *  Copyright (C) 2013 Timm Bäder (Corebird)
3 *
4 *  Cawbird is free software: you can redistribute it and/or modify
5 *  it under the terms of the GNU General Public License as published by
6 *  the Free Software Foundation, either version 3 of the License, or
7 *  (at your option) any later version.
8 *
9 *  Cawbird is distributed in the hope that it will be useful,
10 *  but WITHOUT ANY WARRANTY; without even the implied warranty of
11 *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12 *  GNU General Public License for more details.
13 *
14 *  You should have received a copy of the GNU General Public License
15 *  along with cawbird.  If not, see <http://www.gnu.org/licenses/>.
16 */
17
18class DMListEntry : Gtk.ListBoxRow, Cb.TwitterItem {
19  private Gtk.Grid grid;
20  private AvatarWidget avatar_image;
21  private Gtk.CheckButton delete_checkbutton;
22  private Gtk.Label text_label;
23  private Gtk.Label screen_name_label;
24  private TextButton name_button;
25  private Gtk.Label time_delta_label;
26  private MediaButton media_button;
27  private string _text;
28  private Json.Object? _message_data;
29  private Cb.Media? _media;
30  private Cb.TextEntity[] _entities;
31
32  public bool is_checked {
33    get {
34      return delete_checkbutton.active;
35    }
36    set {
37      set_checked(value);
38    }
39  }
40
41  public signal void avatar_clicked();
42
43  public string text {
44    set {
45      _text = value;
46      if (_message_data == null) {
47        text_label.label = value;
48        text_label.visible = value != null && value != "";
49      }
50    }
51  }
52
53  public string screen_name {
54    set {
55      screen_name_label.label = "@" + value;
56      screen_name_label.tooltip_text = "@" + value;
57    }
58  }
59
60  public new string name {
61    set {
62      name_button.set_text (value);
63      name_button.tooltip_text = value;
64    }
65  }
66
67  public Cb.Media media {
68    get { return _media; }
69    set {
70      _media = value;
71      if (_media != null) {
72        if (media_button == null) {
73          media_button = new MediaButton(media);
74          grid.attach (media_button, 1, 2, 3, 1);
75          media_button.clicked.connect(media_clicked_cb);
76          media_button.show();
77        }
78        else {
79          media_button.media = _media;
80        }
81      }
82    }
83  }
84
85  // A property would be nice, but Cb.TextEntity isn't a GLib.Object so we can't
86  public void set_entities(Cb.TextEntity[] value) {
87    _entities = value;
88    set_dm_text();
89  }
90
91  public Json.Object? message_data {
92    get { return _message_data; }
93    set {
94      _message_data = value;
95      set_dm_text();
96    }
97  }
98
99  public Cairo.Surface avatar {
100    set { avatar_image.surface = value; }
101  }
102
103  public bool seen {
104    get { return true; }
105    set {}
106  }
107
108  private GLib.TimeSpan last_timediff;
109  public int64 timestamp;
110  public int64 id;
111  public int64 user_id;
112  public unowned MainWindow main_window;
113
114  public DMListEntry () {
115    this.set_activatable (false);
116    this.get_style_context ().add_class ("tweet");
117
118    grid = new Gtk.Grid ();
119    grid.margin = 6;
120    grid.show ();
121    this.add (grid);
122
123    this.avatar_image = new AvatarWidget ();
124    avatar_image.size = 48;
125    avatar_image.set_valign (Gtk.Align.START);
126    avatar_image.show();
127    delete_checkbutton = new Gtk.CheckButton();
128    delete_checkbutton.halign = Gtk.Align.CENTER;
129    delete_checkbutton.valign = Gtk.Align.CENTER;
130    delete_checkbutton.button_release_event.connect(avatar_button_release_cb);
131    delete_checkbutton.enter_notify_event.connect(Utils.set_pointer_on_mouseover);
132    delete_checkbutton.leave_notify_event.connect(Utils.set_pointer_on_mouseover);
133    var avatar_overlay = new Gtk.Overlay();
134    avatar_overlay.add(avatar_image);
135    avatar_overlay.add_overlay(delete_checkbutton);
136    avatar_overlay.show();
137    var event_box = new Gtk.EventBox();
138    event_box.margin = 4;
139    event_box.margin_end = 12;
140    event_box.valign = Gtk.Align.START;
141    event_box.add(avatar_overlay);
142    event_box.button_release_event.connect(avatar_button_release_cb);
143    event_box.enter_notify_event.connect(Utils.set_pointer_on_mouseover);
144    event_box.leave_notify_event.connect(Utils.set_pointer_on_mouseover);
145    event_box.show();
146    grid.attach (event_box, 0, 0, 1, 3);
147
148    this.name_button = new TextButton ();
149    name_button.set_valign (Gtk.Align.BASELINE);
150    name_button.show ();
151    grid.attach (name_button, 1, 0, 1, 1);
152
153    this.screen_name_label = new Gtk.Label (null);
154    screen_name_label.set_margin_start (6);
155    screen_name_label.set_margin_end (6);
156    screen_name_label.set_valign (Gtk.Align.BASELINE);
157    screen_name_label.get_style_context ().add_class ("dim-label");
158    screen_name_label.show ();
159    grid.attach (screen_name_label, 2, 0, 1, 1);
160
161    this.time_delta_label = new Gtk.Label (null);
162    time_delta_label.set_halign (Gtk.Align.END);
163    time_delta_label.set_valign (Gtk.Align.BASELINE);
164    time_delta_label.set_hexpand (true);
165    time_delta_label.show ();
166    time_delta_label.get_style_context ().add_class ("dim-label");
167    grid.attach (time_delta_label, 3, 0, 1, 1);
168
169    this.text_label = new Gtk.Label (null);
170    text_label.set_margin_top (6);
171    text_label.set_margin_end (6);
172    text_label.set_margin_bottom (6);
173    text_label.set_hexpand (true);
174    text_label.set_vexpand (true);
175    text_label.set_xalign (0.0f);
176    text_label.set_line_wrap (true);
177    text_label.set_line_wrap_mode (Pango.WrapMode.WORD_CHAR);
178    text_label.set_use_markup (true);
179    text_label.set_use_markup (true);
180    text_label.set_selectable (true);
181    text_label.show ();
182    text_label.activate_link.connect((uri) => {
183      this.grab_focus ();
184      return TweetUtils.activate_link (uri, main_window);
185    });
186    grid.attach (text_label, 1, 1, 3, 1);
187
188    name_button.clicked.connect (() => {
189      var bundle = new Cb.Bundle ();
190      bundle.put_int64 (ProfilePage.KEY_USER_ID, user_id);
191      bundle.put_string (ProfilePage.KEY_SCREEN_NAME, screen_name_label.label.substring (1));
192      main_window.main_widget.switch_page (Page.PROFILE, bundle);
193    });
194
195    this._entities = new Cb.TextEntity[0];
196
197    this.key_release_event.connect(key_released_cb);
198    Settings.get ().changed["text-transform-flags"].connect(set_dm_text);
199    Settings.get ().changed["tweet-scale"].connect (set_dm_text_scale);
200
201    set_dm_text_scale();
202    this.show ();
203  }
204
205  ~DMListEntry() {
206    Settings.get ().changed["text-transform-flags"].disconnect(set_dm_text);
207  }
208
209  [GtkCallback]
210  private bool key_released_cb (Gdk.EventKey evt) {
211#if DEBUG
212    switch(evt.keyval) {
213      case Gdk.Key.k:
214        if (_message_data != null) {
215          var gen = new Json.Generator();
216          var node = new Json.Node(Json.NodeType.OBJECT);
217          node.set_object(_message_data);
218          gen.set_root(node);
219          gen.set_pretty(true);
220          string json = gen.to_data(null);
221          stderr.printf(json + "\n");
222        }
223        else {
224          stderr.printf("Old format DM - no JSON\n");
225        }
226        return Gdk.EVENT_STOP;
227    }
228#endif
229    return Gdk.EVENT_PROPAGATE;
230  }
231
232  private bool avatar_button_release_cb(Gtk.Widget widget, Gdk.EventButton event) {
233    if (event.button == Gdk.BUTTON_PRIMARY) {
234      set_checked(!is_checked);
235      avatar_clicked();
236      return Gdk.EVENT_STOP;
237    }
238    return Gdk.EVENT_PROPAGATE;
239
240  }
241
242  private void set_dm_text() {
243    if (_message_data != null) {
244      var msg = _message_data.get_string_member ("text");
245      // Force removing media links, because they don't actually work for DMs
246      var flags = Settings.get_text_transform_flags () | Cb.TransformFlags.REMOVE_MEDIA_LINKS;
247      msg = Cb.TextTransform.text (msg, _entities, flags, 0, 0);
248      text_label.label = msg;
249      text_label.visible = msg != "";
250    }
251  }
252
253  private void set_dm_text_scale () {
254    var scale = Settings.get_tweet_scale();
255    var new_attribs = new Pango.AttrList();
256    var scale_attr = Pango.attr_scale_new(scale);
257    new_attribs.insert((owned)scale_attr);
258    text_label.set_attributes(new_attribs);
259  }
260
261  public void load_avatar (string avatar_url) {
262    string url = avatar_url;
263    if (this.get_scale_factor () >= 2)
264      url = url.replace ("_normal", "_bigger");
265
266    Twitter.get ().get_avatar.begin (user_id, url, avatar_image, 48 * this.get_scale_factor ());
267  }
268
269  public int update_time_delta (GLib.DateTime? now = null) {
270    GLib.DateTime cur_time;
271    if (now == null)
272      cur_time = new GLib.DateTime.now_local ();
273    else
274      cur_time = now;
275
276    GLib.DateTime then = new GLib.DateTime.from_unix_local (timestamp);
277    time_delta_label.label = Utils.get_time_delta (then, cur_time);
278    return (int)(cur_time.difference (then) / 1000.0 / 1000.0);
279  }
280
281  public int64 get_sort_factor () {
282    return timestamp;
283  }
284
285  public int64 get_timestamp () {
286    return timestamp;
287  }
288
289  public GLib.TimeSpan get_last_set_timediff () {
290    return this.last_timediff;
291  }
292
293  public void set_last_set_timediff (GLib.TimeSpan span) {
294    this.last_timediff = span;
295  }
296
297  private void media_clicked_cb(MediaButton button, double px, double py) {
298    TweetUtils.handle_media_click ({media}, this.main_window, 0);
299  }
300
301  private void set_checked(bool checked) {
302    if (checked) {
303      avatar_image.opacity = 0.5;
304      delete_checkbutton.active = true;
305      delete_checkbutton.show();
306    }
307    else {
308      avatar_image.opacity = 1;
309      delete_checkbutton.active = false;
310      delete_checkbutton.hide();
311    }
312  }
313}
314
315
316