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