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
18[GtkTemplate (ui = "/uk/co/ibboard/cawbird/ui/list-statuses-page.ui")]
19class ListStatusesPage : ScrollWidget, Cb.MessageReceiver, IPage {
20  public const int KEY_USER_LIST     = 0;
21  public const int KEY_NAME          = 1;
22  public const int KEY_DESCRIPTION   = 2;
23  public const int KEY_CREATOR       = 3;
24  public const int KEY_N_SUBSCRIBERS = 4;
25  public const int KEY_N_MEMBERS     = 5;
26  public const int KEY_CREATED_AT    = 6;
27  public const int KEY_MODE          = 7;
28  public const int KEY_LIST_ID       = 8;
29  public const int KEY_TITLE         = 9;
30
31  public int id                             { get; set; }
32  private unowned MainWindow main_window;
33  public unowned MainWindow window {
34    set {
35      main_window = value;
36      tweet_list.main_window = main_window;
37    }
38  }
39  public unowned Account account;
40  private int64 list_id;
41  private uint tweet_remove_timeout = 0;
42  [GtkChild]
43  private unowned TweetListBox tweet_list;
44  [GtkChild]
45  private unowned Gtk.MenuButton delete_button;
46  [GtkChild]
47  private unowned Gtk.Button edit_button;
48  [GtkChild]
49  private unowned Gtk.Label description_label;
50  [GtkChild]
51  private unowned Gtk.Label title_label;
52  [GtkChild]
53  private unowned Gtk.Label creator_label;
54  [GtkChild]
55  private unowned Gtk.Label subscribers_label;
56  [GtkChild]
57  private unowned Gtk.Label members_label;
58  [GtkChild]
59  private unowned Gtk.Label created_at_label;
60  [GtkChild]
61  private unowned Gtk.Stack title_stack;
62  [GtkChild]
63  private unowned Gtk.Entry title_entry;
64  [GtkChild]
65  private unowned Gtk.Stack description_stack;
66  [GtkChild]
67  private unowned Gtk.Entry description_entry;
68  [GtkChild]
69  private unowned Gtk.Stack delete_stack;
70  [GtkChild]
71  private unowned Gtk.Button cancel_button;
72  [GtkChild]
73  private unowned Gtk.Stack edit_stack;
74  [GtkChild]
75  private unowned Gtk.Button save_button;
76  [GtkChild]
77  private unowned Gtk.Stack mode_stack;
78  [GtkChild]
79  private unowned Gtk.Label mode_label;
80  [GtkChild]
81  private unowned Gtk.ComboBoxText mode_combo_box;
82  [GtkChild]
83  private unowned Gtk.Button refresh_button;
84  private bool loading = false;
85
86
87  public ListStatusesPage (int id, Account account) {
88    this.id = id;
89    this.account = account;
90    this.tweet_list.account = account;
91    this.scrolled_to_end.connect (load_older);
92    this.scrolled_to_start.connect (handle_scrolled_to_start);
93    tweet_list.set_adjustment (this.get_vadjustment ());
94  }
95
96  protected virtual void stream_message_received (Cb.StreamMessageType type, Json.Node root) {
97    if (type == Cb.StreamMessageType.EVENT_BLOCK) {
98      hide_tweets_from (root, Cb.TweetState.HIDDEN_AUTHOR_BLOCKED, Cb.TweetState.HIDDEN_RETWEETER_BLOCKED);
99    } else if (type == Cb.StreamMessageType.EVENT_UNBLOCK) {
100      show_tweets_from (root, Cb.TweetState.HIDDEN_AUTHOR_BLOCKED, Cb.TweetState.HIDDEN_RETWEETER_BLOCKED);
101    } else if (type == Cb.StreamMessageType.EVENT_MUTE) {
102      hide_tweets_from (root, Cb.TweetState.HIDDEN_AUTHOR_MUTED, Cb.TweetState.HIDDEN_RETWEETER_MUTED);
103    } else if (type == Cb.StreamMessageType.EVENT_UNMUTE) {
104      show_tweets_from (root, Cb.TweetState.HIDDEN_AUTHOR_MUTED, Cb.TweetState.HIDDEN_RETWEETER_MUTED);
105    } else if (type == Cb.StreamMessageType.EVENT_HIDE_RTS) {
106      tweet_list.hide_retweets_from (get_user_id (root), Cb.TweetState.HIDDEN_RTS_DISABLED);
107    } else if (type == Cb.StreamMessageType.EVENT_SHOW_RTS) {
108      tweet_list.show_retweets_from (get_user_id (root), Cb.TweetState.HIDDEN_RTS_DISABLED);
109    }
110  }
111
112  private int64 get_user_id (Json.Node root) {
113    return root.get_object ().get_object_member ("target").get_int_member ("id");
114  }
115
116  protected void show_tweets_from (Json.Node root, Cb.TweetState tweet_reason, Cb.TweetState retweet_reason = 0) {
117    if (retweet_reason == 0) {
118      retweet_reason = tweet_reason;
119    }
120    int64 user_id = get_user_id(root);
121    tweet_list.show_tweets_from (user_id, tweet_reason);
122    tweet_list.show_retweets_from (user_id, retweet_reason);
123  }
124
125  protected void hide_tweets_from (Json.Node root, Cb.TweetState tweet_reason, Cb.TweetState retweet_reason = 0) {
126    if (retweet_reason == 0) {
127      retweet_reason = tweet_reason;
128    }
129    int64 user_id = get_user_id(root);
130    tweet_list.hide_tweets_from (user_id, tweet_reason);
131    tweet_list.hide_retweets_from (user_id, retweet_reason);
132  }
133
134  /**
135   * va_list params:
136   *  - int64 list_id - The id of the list to show
137   *  - string name - The lists's name
138   *  - bool user_list - true if the list belongs to the user, false otherwise
139   *  - string description - the lists's description
140   *  - string creator
141   *  - int subscribers_count
142   *  - int memebers_count
143   *  - int64 created_at
144   *  - string mode
145   */
146  public void on_join (int page_id, Cb.Bundle? args) {
147    int64 list_id = args.get_int64 (KEY_LIST_ID);
148    if (list_id == 0) {
149      list_id = this.list_id;
150      return;
151      // Continue
152    }
153
154    string? list_name = args.get_string (KEY_NAME);
155    if (list_name != null) {
156      string list_title = args.get_string (KEY_TITLE);
157      bool user_list = args.get_bool (KEY_USER_LIST);
158      string description = args.get_string (KEY_DESCRIPTION);
159      string creator = args.get_string (KEY_CREATOR);
160      int n_subscribers = args.get_int (KEY_N_SUBSCRIBERS);
161      int n_members = args.get_int (KEY_N_MEMBERS);
162      int64 created_at = args.get_int64 (KEY_CREATED_AT);
163      string mode = args.get_string (KEY_MODE);
164
165      delete_button.sensitive = user_list;
166      edit_button.sensitive = user_list;
167      title_label.label = list_title;
168      description_label.label = description;
169      creator_label.label = creator;
170      members_label.label = "%'d".printf (n_members);
171      subscribers_label.label = "%'d".printf (n_subscribers);
172      created_at_label.label = new GLib.DateTime.from_unix_local (created_at).format ("%x, %X");
173      set_mode_label(mode);
174
175      // TRANSLATORS: "%s" is the user's name for the list - e.g. "Contributors" when looking at https://twitter.com/i/lists/1285277968676331522
176      var accessible_name = _("%s list tweets").printf(list_title);
177      tweet_list.get_accessible().set_name(accessible_name);
178      tweet_list.get_accessible().set_description(accessible_name);
179    }
180
181    debug (@"Showing list with id $list_id");
182    if (list_id == this.list_id) {
183      this.list_id = list_id;
184      load_newer.begin ();
185    } else {
186      this.list_id = list_id;
187      tweet_list.model.clear ();
188      load_newest.begin ();
189    }
190
191  }
192
193  public void on_leave () {}
194
195  private async void load_newest () {
196    loading = true;
197    tweet_list.set_unempty ();
198    uint requested_tweet_count = 25;
199    var call = account.proxy.new_call ();
200    call.set_function ("1.1/lists/statuses.json");
201    call.set_method ("GET");
202    debug ("USING LIST ID %s", list_id.to_string ());
203    call.add_param ("list_id", list_id.to_string ());
204    call.add_param ("count", requested_tweet_count.to_string ());
205    call.add_param ("tweet_mode", "extended");
206    call.add_param ("include_ext_alt_text", "true");
207
208    Json.Node? root = null;
209    try {
210      root = yield Cb.Utils.load_threaded_async (call, null);
211    } catch (GLib.Error e) {
212      if (e.message.down () == "not found") {
213        tweet_list.set_empty ();
214      }
215      warning (e.message);
216      loading = false;
217      return;
218    }
219
220    var root_array = root.get_array ();
221    if (root_array.get_length () == 0) {
222      tweet_list.set_empty ();
223      loading = false;
224      return;
225    }
226    TweetUtils.work_array (root_array,
227                           tweet_list,
228                           account);
229
230    loading = false;
231  }
232
233  private async void load_older () {
234    if (loading)
235      return;
236
237    loading = true;
238    uint requested_tweet_count = 25;
239    var call = account.proxy.new_call ();
240    call.set_function ("1.1/lists/statuses.json");
241    call.set_method ("GET");
242    call.add_param ("list_id", list_id.to_string ());
243    call.add_param ("max_id", (tweet_list.model.min_id -1).to_string ());
244    call.add_param ("count", requested_tweet_count.to_string ());
245    call.add_param ("tweet_mode", "extended");
246    call.add_param ("include_ext_alt_text", "true");
247
248    Json.Node? root = null;
249    try {
250      root = yield Cb.Utils.load_threaded_async (call, null);
251    } catch (GLib.Error e) {
252      warning (e.message);
253      return;
254    }
255
256    var root_array = root.get_array ();
257    TweetUtils.work_array (root_array,
258                           tweet_list,
259                           account);
260    loading = false;
261  }
262
263  private void set_mode_label(string mode) {
264    mode_label.label = mode == "private" ? _("Private") : _("Public");
265  }
266
267  [GtkCallback]
268  private void edit_button_clicked_cb () {
269    title_stack.visible_child = title_entry;
270    description_stack.visible_child = description_entry;
271    delete_stack.visible_child = cancel_button;
272    edit_stack.visible_child = save_button;
273    mode_stack.visible_child = mode_combo_box;
274
275    title_entry.text = title_label.label;
276    description_entry.text = description_label.label;
277    mode_combo_box.active_id = mode_label.label == _("Private") ? "private" : "public";
278  }
279
280  [GtkCallback]
281  private void cancel_button_clicked_cb () {
282    title_stack.visible_child = title_label;
283    description_stack.visible_child = description_label;
284    delete_stack.visible_child = delete_button;
285    edit_stack.visible_child = edit_button;
286    mode_stack.visible_child = mode_label;
287  }
288
289  [GtkCallback]
290  private void save_button_clicked_cb () {
291    // Make everything go back to normal
292    title_label.label = title_entry.get_text();
293    description_label.label = description_entry.text;
294    set_mode_label(mode_combo_box.active_id);
295    cancel_button_clicked_cb ();
296    edit_button.sensitive = false;
297    delete_button.sensitive = false;
298    var call = account.proxy.new_call ();
299    call.set_function ("1.1/lists/update.json");
300    call.set_method ("POST");
301    call.add_param ("list_id", list_id.to_string ());
302    call.add_param ("name", title_label.label);
303    call.add_param ("mode", mode_combo_box.active_id);
304    call.add_param ("description", description_label.label);
305    main_window.set_window_title (this.get_title ());
306
307    call.invoke_async.begin (null, (o, res) => {
308      try {
309        call.invoke_async.end (res);
310      } catch (GLib.Error e) {
311        Utils.show_error_dialog (TweetUtils.failed_request_to_error (call, e), this.main_window);
312      }
313      account.user_stream.inject_tweet(Cb.StreamMessageType.EVENT_LIST_UPDATED, call.get_payload());
314      edit_button.sensitive = true;
315      delete_button.sensitive = true;
316    });
317  }
318
319  [GtkCallback]
320  private void delete_confirmation_item_clicked_cb () {
321    ListUtils.delete_list.begin (account, list_id, (obj, res) => {
322      try {
323        ListUtils.delete_list.end (res);
324        // Go back to the ListsPage and tell it to remove this list
325        var bundle = new Cb.Bundle ();
326        bundle.put_int (ListsPage.KEY_MODE, ListsPage.MODE_DELETE);
327        bundle.put_int64 (ListsPage.KEY_LIST_ID, list_id);
328        main_window.main_widget.switch_page (Page.LISTS, bundle);
329      } catch (GLib.Error e) {
330        Utils.show_error_dialog (e, this.main_window);
331      }
332    });
333  }
334
335  [GtkCallback]
336  private void refresh_button_clicked_cb () {
337    refresh_button.sensitive = false;
338    load_newer.begin (() => {
339      refresh_button.sensitive = true;
340    });
341  }
342
343  [GtkCallback]
344  private void tweet_activated_cb (Gtk.ListBoxRow row) {
345    if (row is TweetListEntry) {
346      var bundle = new Cb.Bundle ();
347      bundle.put_int (TweetInfoPage.KEY_MODE, TweetInfoPage.BY_INSTANCE);
348      bundle.put_object (TweetInfoPage.KEY_TWEET, ((TweetListEntry)row).tweet);
349      main_window.main_widget.switch_page (Page.TWEET_INFO, bundle);
350    } else
351      warning ("row is of unknown type");
352  }
353
354  private async void load_newer () {
355    var call = account.proxy.new_call ();
356    call.set_function ("1.1/lists/statuses.json");
357    call.set_method ("GET");
358    call.add_param ("list_id", list_id.to_string ());
359    call.add_param ("count", "30");
360    call.add_param ("tweet_mode", "extended");
361    call.add_param ("include_ext_alt_text", "true");
362    int64 since_id = tweet_list.model.max_id;
363    if (since_id < 0)
364      since_id = 1;
365
366    call.add_param ("since_id", since_id.to_string ());
367    debug ("Getting statuses since %s for list_id %s",
368           since_id.to_string (), list_id.to_string ());
369
370    Json.Node? root = null;
371    try {
372      root = yield Cb.Utils.load_threaded_async (call, null);
373    } catch (GLib.Error e) {
374      warning (e.message);
375      return;
376    }
377
378    var root_array = root.get_array ();
379    if (root_array.get_length () > 0) {
380      TweetUtils.work_array (root_array,
381                             tweet_list,
382                             account);
383    }
384  }
385
386  protected void handle_scrolled_to_start() {
387    if (tweet_remove_timeout != 0)
388      return;
389
390    if (tweet_list.model.get_n_items () > DefaultTimeline.REST) {
391      tweet_remove_timeout = GLib.Timeout.add (500, () => {
392        if (!scrolled_up) {
393          tweet_remove_timeout = 0;
394          return false;
395        }
396
397        tweet_list.model.remove_oldest_n_visible (tweet_list.model.get_n_items () - DefaultTimeline.REST);
398        tweet_remove_timeout = 0;
399        return GLib.Source.REMOVE;
400      });
401    } else if (tweet_remove_timeout != 0) {
402      GLib.Source.remove (tweet_remove_timeout);
403      tweet_remove_timeout = 0;
404    }
405  }
406
407  public string get_title () {
408    return title_label.label;
409  }
410
411  public void create_radio_button (Gtk.RadioButton? group) {}
412  public Gtk.RadioButton? get_radio_button () {return null;}
413
414  public void rerun_filters () {
415    TweetUtils.rerun_filters(tweet_list, account);
416  }
417}
418