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 18public class HomeTimeline : Cb.MessageReceiver, DefaultTimeline { 19 private int64 last_tweet_id = 0; 20 21 protected override string function { 22 get { 23 return "1.1/statuses/home_timeline.json"; 24 } 25 } 26 27 protected override string accessibility_name { 28 get { 29 return _("Home timeline"); 30 } 31 } 32 33 public HomeTimeline(int id, Account account) { 34 base (id); 35 this.account = account; 36 this.tweet_list.account = account; 37 } 38 39 protected override void stream_message_received (Cb.StreamMessageType type, Json.Node root) { 40 if (type == Cb.StreamMessageType.TWEET) { 41 add_tweet (root); 42 } 43 else if (type == Cb.StreamMessageType.TIMELINE_LOADED) { 44 this.preload_is_complete = true; 45 } 46 else if (type == Cb.StreamMessageType.EVENT_UNFOLLOW) { 47 hide_tweets_from (root, Cb.TweetState.HIDDEN_UNFOLLOWED); 48 } 49 else if (type == Cb.StreamMessageType.EVENT_FOLLOW) { 50 show_tweets_from (root, Cb.TweetState.HIDDEN_UNFOLLOWED); 51 load_tweets_from_follow.begin (root); 52 } 53 else { 54 base.stream_message_received (type, root); 55 } 56 } 57 58 protected void add_tweet (Json.Node obj) { 59 GLib.DateTime now = new GLib.DateTime.now_local (); 60 Cb.Tweet t = new Cb.Tweet (); 61 t.load_from_json (obj, this.account.id, now); 62 63 /* We don't use the set_state version from Cb.TweetModel here since 64 we just decide the initial visibility of the tweet */ 65 if (t.retweeted_tweet != null) { 66 if (t.source_tweet.author.id == account.id) { 67 // Don't show our own RTs if we inject them, because Twitter 68 // doesn't provide them in a normal home timeline request. 69 // But we should update the RT status. 70 Utils.set_rt_from_tweet (obj, this.tweet_list.model, this.account); 71 return; 72 } 73 74 t.set_flag (get_rt_flags (t)); 75 } 76 77 TweetUtils.set_tweet_hidden_flags(t, account); 78 79 bool auto_scroll = Settings.auto_scroll_on_new_tweets (); 80 bool is_new_unread = false; 81 82 if (t.id < last_tweet_id && !tweet_list.model.contains_id (t.id)) { 83 int64 age_diff = (int64)(((last_tweet_id >> 22) / 1000) - ((t.id >> 22) / 1000)); 84 debug("Loaded missing tweet %lld (%lld seconds older than %lld)", t.id, age_diff, last_tweet_id); 85 is_new_unread = true; 86 } 87 else if (t.id > last_tweet_id && t.source_tweet.author.id != account.id) { 88 // Keep track of the last ID we saw. 89 // Ignore our own tweets because they get injected and come out of sequence. 90 // This is also the reason we can't just use the model's max_id 91 last_tweet_id = t.id; 92 is_new_unread = true; 93 } 94 // Else we've seen it before, so change nothing 95 96 t.set_seen (t.source_tweet.author.id == account.id || 97 (t.retweeted_tweet != null && t.retweeted_tweet.author.id == account.id) || 98 (this.scrolled_up && 99 main_window.cur_page_id == this.id && 100 auto_scroll) || 101 ! preload_is_complete); 102 103 bool focused = tweet_list.get_first_visible_row () != null && 104 tweet_list.get_first_visible_row ().is_focus; 105 106 bool should_focus = (focused && this.scrolled_up); 107 108 tweet_list.model.add (t); 109 110 if (!t.is_hidden ()) { 111 if (auto_scroll) { 112 base.scroll_up (t); 113 } else if (preload_is_complete) { 114 /* We need to balance even if we don't scroll up, in case 115 auto-scroll-on-new-tweets is disabled */ 116 this.balance_next_upper_change (TOP); 117 } 118 119 if (!t.get_seen () && preload_is_complete && is_new_unread) { 120 this.unread_count ++; 121 } 122 } else { 123 t.set_seen (true); 124 } 125 126 if (should_focus) { 127 tweet_list.get_first_visible_row ().grab_focus (); 128 } 129 130 /* The rest of this function deals with notifications which we certainly 131 don't want to show for invisible tweets */ 132 if (t.is_hidden ()) 133 return; 134 135 // We never show any notifications if auto-scroll-on-new-tweet is enabled 136 // or if it's our tweet or an initial load, or if it's not a new tweet 137 if (t.get_user_id () == account.id || auto_scroll || !preload_is_complete || !is_new_unread) 138 return; 139 140 int stack_size = Settings.get_tweet_stack_count (); 141 142 if (stack_size == 1 && !auto_scroll) { 143 string summary = ""; 144 if (t.retweeted_tweet != null){ 145 summary = _("%s retweeted %s").printf (t.source_tweet.author.user_name, 146 t.retweeted_tweet.author.user_name); 147 } else { 148 summary = _("%s tweeted").printf (t.source_tweet.author.user_name); 149 } 150 string id_suffix = "tweet-%s".printf (t.id.to_string ()); 151 t.notification_id = account.notifications.send (summary, 152 t.get_real_text (), 153 id_suffix); 154 155 } else if(stack_size != 0 && unread_count % stack_size == 0 156 && unread_count > 0) { 157 string summary = ngettext("%d new Tweet!", 158 "%d new Tweets!", unread_count).printf (unread_count); 159 account.notifications.send (summary, ""); 160 } 161 } 162 163 private async void load_tweets_from_follow (Json.Node follow_root) { 164 var follow_id = get_user_id (follow_root); 165 try { 166 var root_array = yield UserUtils.load_user_timeline_by_id (account, follow_id, 200, tweet_list.model.min_id); 167 root_array.foreach_element((array, idx, node) => { add_tweet (node); }); 168 } 169 catch (GLib.Error e) { 170 // If we can't load tweets then oh well, never mind 171 warning(e.message); 172 } 173 } 174 175 public override string get_title () { 176 return "@" + account.screen_name; 177 } 178 179 public override void create_radio_button (Gtk.RadioButton? group) { 180 radio_button = new BadgeRadioButton(group, "cawbird-user-home-symbolic", _("Home")); 181 } 182} 183