1 /* This file is part of Cawbird, a Gtk+ linux Twitter client forked from Corebird.
2 * Copyright (C) 2017 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 #include "CbTweetModel.h"
19
20 static void cb_tweet_model_iface_init (GListModelInterface *iface);
21
22 G_DEFINE_TYPE_WITH_CODE (CbTweetModel, cb_tweet_model, G_TYPE_OBJECT,
23 G_IMPLEMENT_INTERFACE (G_TYPE_LIST_MODEL, cb_tweet_model_iface_init));
24
25 static GType
cb_tweet_model_get_item_type(GListModel * model)26 cb_tweet_model_get_item_type (GListModel *model)
27 {
28 return CB_TYPE_TWEET;
29 }
30
31 static guint
cb_tweet_model_get_n_items(GListModel * model)32 cb_tweet_model_get_n_items (GListModel *model)
33 {
34 CbTweetModel *self = CB_TWEET_MODEL (model);
35
36 return self->tweets->len;
37 }
38
39 static gpointer
cb_tweet_model_get_item(GListModel * model,guint index)40 cb_tweet_model_get_item (GListModel *model,
41 guint index)
42 {
43 CbTweetModel *self = CB_TWEET_MODEL (model);
44 CbTweet *tweet;
45
46 g_assert (index < self->tweets->len);
47
48 tweet = g_ptr_array_index (self->tweets, index);
49
50 return g_object_ref (tweet);
51 }
52
53 static void
cb_tweet_model_iface_init(GListModelInterface * iface)54 cb_tweet_model_iface_init (GListModelInterface *iface)
55 {
56 iface->get_item_type = cb_tweet_model_get_item_type;
57 iface->get_n_items = cb_tweet_model_get_n_items;
58 iface->get_item = cb_tweet_model_get_item;
59 }
60
61 static inline void
emit_items_changed(CbTweetModel * self,guint position,guint removed,guint added)62 emit_items_changed (CbTweetModel *self,
63 guint position,
64 guint removed,
65 guint added)
66 {
67 g_list_model_items_changed (G_LIST_MODEL (self), position, removed, added);
68 }
69
70 static void
cb_tweet_model_init(CbTweetModel * self)71 cb_tweet_model_init (CbTweetModel *self)
72 {
73 self->tweets = g_ptr_array_new_with_free_func (g_object_unref);
74 self->hidden_tweets = g_ptr_array_new_with_free_func (g_object_unref);
75 self->thread_mode = FALSE;
76 self->min_id = G_MAXINT64;
77 self->max_id = G_MININT64;
78 }
79
80 static void
cb_tweet_model_finalize(GObject * object)81 cb_tweet_model_finalize (GObject *object)
82 {
83 CbTweetModel *self = CB_TWEET_MODEL (object);
84
85 g_ptr_array_unref (self->tweets);
86 g_ptr_array_unref (self->hidden_tweets);
87
88 G_OBJECT_CLASS (cb_tweet_model_parent_class)->finalize (object);
89 }
90
91 static void
cb_tweet_model_class_init(CbTweetModelClass * klass)92 cb_tweet_model_class_init (CbTweetModelClass *klass)
93 {
94 GObjectClass *object_class = G_OBJECT_CLASS (klass);
95
96 object_class->finalize = cb_tweet_model_finalize;
97 }
98
99 CbTweetModel *
cb_tweet_model_new(void)100 cb_tweet_model_new (void)
101 {
102 return CB_TWEET_MODEL (g_object_new (CB_TYPE_TWEET_MODEL, NULL));
103 }
104
105 static inline void
update_min_max_id(CbTweetModel * self,gint64 old_id)106 update_min_max_id (CbTweetModel *self,
107 gint64 old_id)
108 {
109 int i;
110 #ifdef DEBUG
111 /* This should be called *after* the
112 * tweet has been removed from self->tweets! */
113 for (i = 0; i < self->tweets->len; i ++)
114 {
115 CbTweet *t = g_ptr_array_index (self->tweets, i);
116
117 g_assert (t->id != old_id);
118 }
119 #endif
120
121 if (old_id == self->max_id)
122 {
123 if (self->tweets->len > 0)
124 {
125 CbTweet *t = g_ptr_array_index (self->tweets, self->thread_mode ? self->tweets->len - 1 : 0);
126
127 self->max_id = t->id;
128 // Don't remove newer (higher ID) hidden tweets in case we unhide them later
129 }
130 else
131 {
132 self->max_id = G_MININT64;
133 g_ptr_array_remove_range (self->hidden_tweets, 0, self->hidden_tweets->len);
134 }
135 }
136
137 if (old_id == self->min_id)
138 {
139 if (self->tweets->len > 0)
140 {
141 CbTweet *t = g_ptr_array_index (self->tweets, self->thread_mode ? 0 : self->tweets->len - 1);
142
143 self->min_id = t->id;
144 /* We just removed the tweet with the min_id, so now remove all hidden tweets
145 * with an id lower than the new min_id */
146 for (i = 0; i < self->hidden_tweets->len; i ++)
147 {
148 CbTweet *t = g_ptr_array_index (self->hidden_tweets, i);
149
150 if (t->id < self->min_id)
151 {
152 g_ptr_array_remove_index (self->hidden_tweets, i);
153 i --;
154 }
155 }
156 }
157 else
158 {
159 self->min_id = G_MAXINT64;
160 g_ptr_array_remove_range (self->hidden_tweets, 0, self->hidden_tweets->len);
161 }
162 }
163 }
164
165 int
cb_tweet_model_index_of(CbTweetModel * self,gint64 id)166 cb_tweet_model_index_of (CbTweetModel *self,
167 gint64 id)
168 {
169 int i;
170 g_return_val_if_fail (CB_IS_TWEET_MODEL (self), FALSE);
171
172 for (i = 0; i < self->tweets->len; i ++)
173 {
174 CbTweet *tweet = g_ptr_array_index (self->tweets, i);
175
176 if (tweet->id == id)
177 return i;
178 }
179
180 return -1;
181 }
182
183 int
cb_tweet_model_index_of_retweet(CbTweetModel * self,gint64 id)184 cb_tweet_model_index_of_retweet (CbTweetModel *self,
185 gint64 id)
186 {
187 int i;
188 g_return_val_if_fail (CB_IS_TWEET_MODEL (self), FALSE);
189
190 for (i = 0; i < self->tweets->len; i ++)
191 {
192 CbTweet *tweet = g_ptr_array_index (self->tweets, i);
193
194 if (tweet->retweeted_tweet != NULL && tweet->retweeted_tweet->id == id)
195 return i;
196 }
197
198 return -1;
199 }
200
201 static void
remove_tweet_at_pos(CbTweetModel * self,guint index)202 remove_tweet_at_pos (CbTweetModel *self,
203 guint index)
204 {
205 g_assert (index < self->tweets->len);
206 CbTweet *tweet = g_ptr_array_index (self->tweets, index);
207 gint64 id = tweet->id;
208
209 g_ptr_array_remove_index (self->tweets, index);
210 tweet = NULL; /* We just unreffed it, so potentially freed */
211
212 update_min_max_id (self, id);
213 emit_items_changed (self, index, 1, 0);
214 }
215
216 static inline void
insert_sorted(CbTweetModel * self,CbTweet * tweet)217 insert_sorted (CbTweetModel *self,
218 CbTweet *tweet)
219 {
220 int insert_pos = -1;
221 gint64 id = self->thread_mode && tweet->retweeted_tweet != NULL ? tweet->retweeted_tweet->id : tweet->id;
222
223 if (id > self->max_id)
224 {
225 insert_pos = self->thread_mode ? self->tweets->len : 0;
226 }
227 else if (id < self->min_id)
228 {
229 insert_pos = self->thread_mode ? 0 : self->tweets->len;
230 }
231 else
232 {
233 /* This case should be relatively rare in real life since
234 * we only ever add tweets at the top or bottom of a list */
235 int i;
236 CbTweet *next = g_ptr_array_index (self->tweets, 0);
237
238 for (i = 1; i < self->tweets->len; i ++)
239 {
240 CbTweet *cur = next;
241 next = g_ptr_array_index (self->tweets, i);
242
243 gint64 older_id, newer_id, cur_id;
244
245 if (self->thread_mode) {
246 cur_id = cur->retweeted_tweet != NULL ? cur->retweeted_tweet->id : cur->id;
247 older_id = cur_id;
248 newer_id = next->retweeted_tweet != NULL ? next->retweeted_tweet->id : next->id;
249 } else {
250 cur_id = cur->id;
251 older_id = next->id;
252 newer_id = cur_id;
253 }
254
255 if (newer_id > id && older_id < id)
256 {
257 insert_pos = i;
258 break;
259 }
260 else if (cur_id == id)
261 {
262 // We found a duplicate! Could be caused by injecting the user's own tweet,
263 // so ignore it
264 break;
265 }
266 }
267 }
268
269 if (insert_pos == -1)
270 {
271 /* This can happen if the same tweet gets inserted into an empty model twice.
272 * Generally, we'd like to ignore double insertions, at least right now I can't
273 * think of a good use case for it. (2017-06-13) */
274 return;
275 }
276
277 g_object_ref (tweet);
278 g_ptr_array_insert (self->tweets, insert_pos, tweet);
279
280 emit_items_changed (self, insert_pos, 0, 1);
281
282 if (id > self->max_id)
283 self->max_id = id;
284
285 if (id < self->min_id)
286 self->min_id = id;
287 }
288
289 static void
hide_tweet_internal(CbTweetModel * self,guint index)290 hide_tweet_internal (CbTweetModel *self,
291 guint index)
292 {
293 CbTweet *tweet = g_ptr_array_index (self->tweets, index);
294 gint64 id = tweet->id;
295
296 g_object_ref (tweet);
297 g_ptr_array_remove_index (self->tweets, index);
298 g_object_ref (tweet); /* Have to ref manually */
299 g_ptr_array_add (self->hidden_tweets, tweet);
300 g_object_unref (tweet);
301
302 update_min_max_id (self, id);
303 }
304
305 static void
show_tweet_internal(CbTweetModel * self,guint index)306 show_tweet_internal (CbTweetModel *self,
307 guint index)
308 {
309 CbTweet *tweet = g_ptr_array_index (self->hidden_tweets, index);
310
311 g_object_ref (tweet);
312 g_ptr_array_remove_index (self->hidden_tweets, index);
313 insert_sorted (self, tweet);
314 g_object_unref (tweet);
315 }
316
317 gboolean
cb_tweet_model_contains_id(CbTweetModel * self,gint64 id)318 cb_tweet_model_contains_id (CbTweetModel *self,
319 gint64 id)
320 {
321 return cb_tweet_model_index_of (self, id) != -1;
322 }
323
324 void
cb_tweet_model_clear(CbTweetModel * self)325 cb_tweet_model_clear (CbTweetModel *self)
326 {
327 int l;
328 g_return_if_fail (CB_IS_TWEET_MODEL (self));
329
330 l = self->tweets->len;
331 g_ptr_array_remove_range (self->tweets, 0, l);
332 g_ptr_array_remove_range (self->hidden_tweets, 0, self->hidden_tweets->len);
333
334 self->min_id = G_MAXINT64;
335 self->max_id = G_MININT64;
336
337 emit_items_changed (self, 0, l, 0);
338 }
339
340 void
cb_tweet_model_set_thread_mode(CbTweetModel * self,gboolean thread_mode)341 cb_tweet_model_set_thread_mode (CbTweetModel *self, gboolean thread_mode)
342 {
343 g_return_if_fail (self->min_id == G_MAXINT64);
344 g_return_if_fail (self->max_id == G_MININT64);
345
346 self->thread_mode = thread_mode;
347 }
348
349 CbTweet *
cb_tweet_model_get_for_id(CbTweetModel * self,gint64 id,int diff)350 cb_tweet_model_get_for_id (CbTweetModel *self,
351 gint64 id,
352 int diff)
353 {
354 int i;
355 g_return_val_if_fail (CB_IS_TWEET_MODEL (self), NULL);
356
357 for (i = 0; i < self->tweets->len; i ++)
358 {
359 CbTweet *tweet = g_ptr_array_index (self->tweets, i);
360
361 if (tweet->id == id)
362 {
363 if (i + diff < self->tweets->len && i + diff >= 0)
364 return g_ptr_array_index (self->tweets, i + diff);
365
366 return NULL;
367 }
368 }
369
370 return NULL;
371 }
372
373 gboolean
cb_tweet_model_delete_id(CbTweetModel * self,gint64 id,gboolean * seen)374 cb_tweet_model_delete_id (CbTweetModel *self,
375 gint64 id,
376 gboolean *seen)
377 {
378 int i;
379 g_return_val_if_fail (CB_IS_TWEET_MODEL (self), FALSE);
380
381 for (i = 0; i < self->tweets->len; i ++)
382 {
383 CbTweet *tweet = g_ptr_array_index (self->tweets, i);
384
385 if (tweet->id == id)
386 {
387 *seen = tweet->seen;
388
389 g_assert (!cb_tweet_is_hidden (tweet));
390
391 cb_tweet_set_flag (tweet, CB_TWEET_STATE_DELETED);
392
393 return TRUE;
394 }
395 else if (cb_tweet_is_flag_set (tweet, CB_TWEET_STATE_RETWEETED) &&
396 tweet->my_retweet == id)
397 {
398 cb_tweet_unset_flag (tweet, CB_TWEET_STATE_RETWEETED);
399 }
400 }
401
402 *seen = FALSE;
403 return FALSE;
404 }
405
406 void
cb_tweet_model_remove_tweet(CbTweetModel * self,CbTweet * tweet)407 cb_tweet_model_remove_tweet (CbTweetModel *self,
408 CbTweet *tweet)
409 {
410 int i;
411 g_return_if_fail (CB_IS_TWEET_MODEL (self));
412 g_return_if_fail (CB_IS_TWEET (tweet));
413
414 #ifdef DEBUG
415 if (!cb_tweet_is_hidden (tweet))
416 g_assert (cb_tweet_model_contains_id (self, tweet->id));
417 #endif
418
419 if (cb_tweet_is_hidden (tweet))
420 {
421 for (i = 0; i < self->hidden_tweets->len; i ++)
422 {
423 CbTweet *t = g_ptr_array_index (self->hidden_tweets, i);
424 if (t == tweet)
425 {
426 g_ptr_array_remove_index (self->hidden_tweets, i);
427 break;
428 }
429 }
430 }
431 else
432 {
433 int pos = -1;
434
435 for (i = 0; i < self->tweets->len; i ++)
436 {
437 CbTweet *t = g_ptr_array_index (self->tweets, i);
438
439 if (t == tweet)
440 {
441 pos = i;
442 break;
443 }
444 }
445
446 g_assert (pos != -1);
447
448 remove_tweet_at_pos (self, pos);
449 }
450 }
451
452 void
cb_tweet_model_toggle_flag_on_user_tweets(CbTweetModel * self,gint64 user_id,CbTweetState flag,gboolean active)453 cb_tweet_model_toggle_flag_on_user_tweets (CbTweetModel *self,
454 gint64 user_id,
455 CbTweetState flag,
456 gboolean active)
457 {
458 int i;
459 g_return_if_fail (CB_IS_TWEET_MODEL (self));
460
461 for (i = 0; i < self->tweets->len; i ++)
462 {
463 CbTweet *t = g_ptr_array_index (self->tweets, i);
464
465 if (cb_tweet_get_user_id (t) == user_id)
466 {
467 if (active)
468 {
469 if (cb_tweet_model_set_tweet_flag (self, t, flag))
470 i --;
471 }
472 else
473 {
474 if (cb_tweet_model_unset_tweet_flag (self, t, flag))
475 i --;
476 }
477 }
478 }
479
480 /* Aaaand now the same thing for hidden tweets */
481 for (i = 0; i < self->hidden_tweets->len; i ++)
482 {
483 CbTweet *t = g_ptr_array_index (self->hidden_tweets, i);
484
485 if (cb_tweet_get_user_id (t) == user_id)
486 {
487 if (active)
488 {
489 if (cb_tweet_model_set_tweet_flag (self, t, flag))
490 i --;
491 }
492 else
493 {
494 if (cb_tweet_model_unset_tweet_flag (self, t, flag))
495 i --;
496 }
497 }
498 }
499 }
500
501 void
cb_tweet_model_toggle_flag_on_user_retweets(CbTweetModel * self,gint64 user_id,CbTweetState flag,gboolean active)502 cb_tweet_model_toggle_flag_on_user_retweets (CbTweetModel *self,
503 gint64 user_id,
504 CbTweetState flag,
505 gboolean active)
506 {
507 int i;
508 g_return_if_fail (CB_IS_TWEET_MODEL (self));
509
510 for (i = 0; i < self->tweets->len; i ++)
511 {
512 CbTweet *t = g_ptr_array_index (self->tweets, i);
513
514 if (t->retweeted_tweet != NULL &&
515 t->source_tweet.author.id == user_id)
516 {
517 if (active)
518 {
519 if (cb_tweet_model_set_tweet_flag (self, t, flag))
520 i --;
521 }
522 else
523 {
524 if (cb_tweet_model_unset_tweet_flag (self, t, flag))
525 i --;
526 }
527 }
528 }
529
530 /* Aaaand now the same thing for hidden tweets */
531 for (i = 0; i < self->hidden_tweets->len; i ++)
532 {
533 CbTweet *t = g_ptr_array_index (self->hidden_tweets, i);
534
535 if (t->retweeted_tweet != NULL &&
536 t->source_tweet.author.id == user_id)
537 {
538 if (active)
539 {
540 if (cb_tweet_model_set_tweet_flag (self, t, flag))
541 i --;
542 }
543 else
544 {
545 if (cb_tweet_model_unset_tweet_flag (self, t, flag))
546 i --;
547 }
548 }
549 }
550 }
551
552 gboolean
cb_tweet_model_set_tweet_flag(CbTweetModel * self,CbTweet * tweet,CbTweetState flag)553 cb_tweet_model_set_tweet_flag (CbTweetModel *self,
554 CbTweet *tweet,
555 CbTweetState flag)
556 {
557 int i;
558
559 if (cb_tweet_is_hidden (tweet))
560 {
561 #ifdef DEBUG
562 gboolean found = FALSE;
563 /* Should be in hidden_tweets now, hu? */
564 for (i = 0; self->hidden_tweets->len; i ++)
565 {
566 CbTweet *t = g_ptr_array_index (self->hidden_tweets, i);
567
568 if (t == tweet)
569 {
570 found = TRUE;
571 break;
572 }
573 }
574
575 g_assert (found);
576 #endif
577
578 cb_tweet_set_flag (tweet, flag);
579 }
580 else
581 {
582 #ifdef DEBUG
583 /* Now it should be is self->tweets */
584 gboolean found = FALSE;
585 /* Should be in hidden_tweets now, hu? */
586 for (i = 0; self->tweets->len; i ++)
587 {
588 CbTweet *t = g_ptr_array_index (self->tweets, i);
589
590 if (t == tweet)
591 {
592 found = TRUE;
593 break;
594 }
595 }
596
597 g_assert (found);
598 #endif
599
600 cb_tweet_set_flag (tweet, flag);
601 if (cb_tweet_is_hidden (tweet))
602 {
603 /* Could be hidden now. */
604 for (i = 0; i < self->tweets->len; i ++)
605 {
606 CbTweet *t = g_ptr_array_index (self->tweets, i);
607
608 if (t == tweet)
609 {
610 hide_tweet_internal (self, i);
611 emit_items_changed (self, i, 1, 0);
612 break;
613 }
614 }
615
616 return TRUE;
617 }
618 }
619
620 return FALSE;
621 }
622
623 gboolean
cb_tweet_model_unset_tweet_flag(CbTweetModel * self,CbTweet * tweet,CbTweetState flag)624 cb_tweet_model_unset_tweet_flag (CbTweetModel *self,
625 CbTweet *tweet,
626 CbTweetState flag)
627 {
628 int i;
629
630 if (cb_tweet_is_hidden (tweet))
631 {
632 #ifdef DEBUG
633 gboolean found = FALSE;
634 /* Should be in hidden_tweets now, hu? */
635 for (i = 0; self->hidden_tweets->len; i ++)
636 {
637 CbTweet *t = g_ptr_array_index (self->hidden_tweets, i);
638
639 if (t == tweet)
640 {
641 found = TRUE;
642 break;
643 }
644 }
645
646 g_assert (found);
647 #endif
648
649 cb_tweet_unset_flag (tweet, flag);
650 if (!cb_tweet_is_hidden (tweet))
651 {
652 /* Tweet not hidden anymore. Move to self->tweets
653 * and emit items-changed. */
654 for (i = 0; i < self->hidden_tweets->len; i ++)
655 {
656 CbTweet *t = g_ptr_array_index (self->hidden_tweets, i);
657 if (t == tweet)
658 {
659 show_tweet_internal (self, i);
660 return TRUE;
661 }
662 }
663 }
664 }
665 else
666 {
667 #ifdef DEBUG
668 /* Now it should be is self->tweets */
669 gboolean found = FALSE;
670 /* Should be in hidden_tweets now, hu? */
671 for (i = 0; self->tweets->len; i ++)
672 {
673 CbTweet *t = g_ptr_array_index (self->tweets, i);
674
675 if (t == tweet)
676 {
677 found = TRUE;
678 break;
679 }
680 }
681
682 g_assert (found);
683 #endif
684
685 cb_tweet_unset_flag (tweet, flag);
686 }
687
688 return FALSE;
689 }
690
691 void
cb_tweet_model_add(CbTweetModel * self,CbTweet * tweet)692 cb_tweet_model_add (CbTweetModel *self,
693 CbTweet *tweet)
694 {
695
696 g_return_if_fail (CB_IS_TWEET_MODEL (self));
697 g_return_if_fail (CB_IS_TWEET (tweet));
698
699 if (cb_tweet_is_hidden (tweet))
700 {
701 g_object_ref (tweet);
702 g_ptr_array_add (self->hidden_tweets, tweet);
703 }
704 else
705 {
706 insert_sorted (self, tweet);
707 }
708 }
709
710 void
cb_tweet_model_remove_oldest_n_visible(CbTweetModel * self,guint amount)711 cb_tweet_model_remove_oldest_n_visible (CbTweetModel *self,
712 guint amount)
713 {
714 int size_before;
715 int start;
716
717 if (amount < 1) {
718 return;
719 }
720
721 g_return_if_fail (CB_IS_TWEET_MODEL (self));
722
723 size_before = self->tweets->len;
724
725 if (amount > size_before) {
726 amount = size_before;
727 }
728
729 if (self->thread_mode) {
730 start = 0;
731 }
732 else {
733 start = size_before - amount;
734 }
735
736 g_ptr_array_remove_range (self->tweets,
737 start,
738 amount);
739 update_min_max_id (self, self->min_id);
740 emit_items_changed (self, start, amount, 0);
741 }
742
743 void
cb_tweet_model_remove_tweets_later_than(CbTweetModel * self,gint64 id)744 cb_tweet_model_remove_tweets_later_than (CbTweetModel *self,
745 gint64 id)
746 {
747 g_return_if_fail (CB_IS_TWEET_MODEL (self));
748
749 if (self->tweets->len == 0)
750 return;
751
752 if (self->thread_mode) {
753 for (guint i = self->tweets->len; i > 0; i--) {
754 CbTweet *cur = g_ptr_array_index (self->tweets, i - 1);
755
756 if (cur->id < id)
757 break;
758
759 remove_tweet_at_pos (self, i - 1);
760 }
761 }
762 else {
763 while (self->tweets->len > 0) {
764 CbTweet *first = g_ptr_array_index (self->tweets, 0);
765
766 if (first->id < id)
767 break;
768
769 remove_tweet_at_pos (self, 0);
770 }
771 }
772 }
773