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