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