home *** CD-ROM | disk | FTP | other *** search
/ Maximum CD 2011 October / maximum-cd-2011-10.iso / DiscContents / digsby_setup.exe / lib / plugins / twitter / res / timeline.js < prev    next >
Encoding:
Text File  |  2011-01-03  |  20.1 KB  |  679 lines

  1. var _feedId = 0;
  2. function nextFeedId() { return _feedId++; }
  3.  
  4. function FeedModel() {
  5.     this.feedId = nextFeedId();
  6. }
  7.  
  8. FeedModel.prototype = {
  9.     has: function(item) {
  10.         return false;
  11.     }
  12. };
  13.  
  14. function TwitterFeedModel(tweets) {
  15.     this.tweets = tweets;
  16.     this.listeners = [];
  17.  
  18.     FeedModel.call(this);
  19.     this.filterUserIds = {};
  20.     this.merges = {};
  21.  
  22.     this.all = {};
  23.     this.items = [];
  24.     this.unreadCount = 0;
  25. }
  26.  
  27. TwitterFeedModel.inheritsFrom(FeedModel, {
  28.     limit: 100,
  29.     autoUpdates: true,
  30.  
  31.     // Returns true if the tweet is in this feed.
  32.     hasTweet: function(item) {
  33.         return item.id in this.all;
  34.     },
  35.  
  36.     /**
  37.      * called when changing to this view. should return true if it's time to
  38.      * do a "manual" update.
  39.      */
  40.     shouldUpdateOnView: function () {
  41.         if (!this.autoUpdates && this.source) {
  42.             var now = new Date().getTime();
  43.             return (now - this.source.lastUpdateTime) >= this.manualUpdateFrequency;
  44.         }
  45.         return false;
  46.     },
  47.  
  48.     // frequency to allow updates when switching to views like favorites
  49.     manualUpdateFrequency: 1000 * 60 * 5,
  50.  
  51.     addsToUnreadCount: function() { return true; },
  52.     toString: function() {
  53.         return '<' + this.constructor.name + ' (' + this.unreadCount + '/' + this.items.length + ' unread)>';
  54.     },
  55.  
  56.     makeSource: function(account) {
  57.         if (!this.sourceURL)
  58.             return;
  59.  
  60.         var url = account.apiRoot + this.sourceURL;
  61.         var source = new TwitterTimelineSource(account, url, this.sourceData);
  62.         if (this.limitById !== undefined)
  63.             source.limitById = this.limitById;
  64.         this.source = source;
  65.         source.feed = this;
  66.         return source;
  67.     },
  68.  
  69.     updateSourceNow: function() {
  70.         if (!this.source) return;
  71.         var self = this;
  72.         var done = function() {
  73.             callEach(self.listeners, 'finish', false);
  74.         };
  75.         var obj = {success: done, error: done};
  76.         this.source.update(obj);
  77.     },
  78.  
  79.     markedAsRead: function(item) {
  80.         this.unreadCount -= 1;
  81.         if (this.unreadCount < 0) {
  82.             console.warn('WARNING: unreadCount went below 0');
  83.             this.unreadCount = 0;
  84.         }
  85.     },
  86.  
  87.     add: function(item, search) {
  88.         var isnew = !(item.id in this.all);
  89.         if (isnew) {
  90.             this.all[item.id] = item;
  91.  
  92.             if (search) {
  93.                 var key = function(t) { return t.created_at_ms; };
  94.                 var i = binarySearch(this.items, item, key);
  95.                 arrayExtend(this.items, [item], i);
  96.             } else {
  97.                 this.items.push(item);
  98.             }
  99.  
  100.             this.addItemListener(item);
  101.             if (isnew && !item.read)
  102.                 this.unreadCount += 1;
  103.         } else {
  104.             var myitem = this.all[item.id];
  105.             var wasread = myitem.read;
  106.             for (var k in item)
  107.                 myitem[k] = item[k];
  108.             myitem.read = wasread || item.read;
  109.         }
  110.         return isnew;
  111.     },
  112.  
  113.     removeItem: function(item) {
  114.         if (item.id in this.all) {
  115.             this.removeItemListener(item);
  116.             var found = false;
  117.             this.items = $.grep(this.items, function (i) {
  118.                 found = true;
  119.                 return i.id !== item.id;
  120.             });
  121.             if (found) this.refreshUnreadCount();
  122.         }
  123.     },
  124.  
  125.     removeAllItems: function() {
  126.         var self = this;
  127.         $.each(this.items, function (i, item) {
  128.             self.removeItemListener(item);
  129.         });
  130.         this.unreadCount = 0;
  131.         this.items = [];
  132.         this.all = {};
  133.     },
  134.  
  135.     addSorted: function(items, source, notifyNow, opts) {
  136.         var self = this;
  137.         assert(items);
  138.         var newItems = [];
  139.         var maxCreatedAt = 0;
  140.         var maxId = '0';
  141.  
  142.         if (verbose) console.log('addSorted(' + items.length + ' items) from ' + source);
  143.  
  144.         var search = true;//(items.length && this.items.length !== 0 && 
  145.             //((items[0].created_at_ms < this.items[this.items.length-1].created_at_ms) ||
  146.              //(items[items.length-1].created_at_ms > this.items[0].created_at_ms)));
  147.  
  148.         var account = opts.account;
  149.         $.each(items, function (i, item) {
  150.             if (self.has(item, account)) {
  151.                 var added = self.add(item, search);
  152.                 if (added) {
  153.                     maxId = compareIds(item.id, maxId) > 0 ? item.id : maxId;
  154.                     maxCreatedAt = item.created_at_ms > maxCreatedAt ? item.created_at_ms : maxCreatedAt;
  155.                     newItems.push(item);
  156.                 }
  157.             }
  158.         });
  159.  
  160.         this.trim();
  161.  
  162.         if (0) {
  163.         for (var i = 0; i < self.items.length-1; ++i) {
  164.             var a = self.items[i];
  165.             var b = self.items[i+1];
  166.             assert(a.created_at_ms <= b.created_at_ms,
  167.                    'created_at_ms wrong: ' +a+ ' (' +a.created_at_ms+ ') and ' +b+ ' (' +b.created_at_ms+ ')');
  168.         }
  169.         }
  170.  
  171.         if (notifyNow)
  172.             callEach(this.listeners, 'finish', opts && opts.scroll);
  173.  
  174.         if (!opts.ignoreIds && self.source && (self.source === source || source === undefined))
  175.             self.source.updateMinMax(maxId);
  176.     },
  177.     
  178.     refreshUnreadCount: function () {
  179.         var count = 0;
  180.         var items = this.items;
  181.         for (var i = items.length-1; i >= 0; --i) {
  182.             if (!items[i].read)
  183.                 count++;
  184.         }
  185.         this.unreadCount = count;
  186.     },
  187.  
  188.     trim: function() {
  189.         var self = this;
  190.         if (this.items.length > this.limit) {
  191.             var numToDelete = this.items.length - this.limit;
  192.             $.each(this.items, function (i, item) {
  193.                 if (i >= numToDelete)
  194.                     return false;
  195.  
  196.                 delete self.all[item.id];
  197.                 self.removeItemListener(item);
  198.                 if (!item.read)
  199.                     self.unreadCount -= 1;
  200.             });
  201.             
  202.             var deletedItems = self.items.slice(0, numToDelete);
  203.             console.log(this + ' trimmed ' + deletedItems.length + ' items');
  204.             self.items = self.items.slice(numToDelete);
  205.         }
  206.     },
  207.  
  208.     addItemListener: function (item) {
  209.         if (!('feeds' in item)) item.feeds = {};
  210.         item.feeds[this.feedId] = this;
  211.     },
  212.  
  213.     removeItemListener: function (item) {
  214.         if (item.feeds !== undefined)
  215.             delete item.feeds[this.feedId];
  216.     },
  217.  
  218.     addFeedListener: function (listener) { this.listeners.push(listener); },
  219.  
  220.     removeFeedListener: function (listener) {
  221.         this.listeners = $.grep(this.listeners, function (item, i) {
  222.             return listener !== item;
  223.         });
  224.     }
  225. });
  226.  
  227. function TwitterTimelineFeedModel(tweets) {
  228.     TwitterFeedModel.call(this, tweets);
  229. }
  230.  
  231. TwitterTimelineFeedModel.inheritsFrom(TwitterFeedModel, {
  232.     name: 'timeline',
  233.     serialize: function() {
  234.         return {
  235.             count: this.unreadCount, 
  236.             label: "Timeline", 
  237.             name: 'timeline', 
  238.             type: 'timeline'
  239.         }; 
  240.     },
  241.     sourceURL: 'statuses/home_timeline.json',
  242.  
  243.     addMerge: function(name, mergeFunc) {
  244.         this.merges[name] = mergeFunc;
  245.     },
  246.  
  247.     removeMerge: function(name) {
  248.         if (name in this.merges) {
  249.             delete this.merges[name];
  250.             return this.pruneItems();
  251.         } else
  252.             console.warn('removeMerge(' + name + ') called, but not in this.merges');
  253.  
  254.         return 0;
  255.     },
  256.  
  257.     pruneItems: function() {
  258.         var self = this;
  259.         var oldCount = this.items.length;
  260.         this.items = $.grep(this.items, function(i) { return self.has(i); });
  261.         var delta = oldCount-this.items.length;
  262.         console.log('pruneItems removed ' + delta);
  263.         return delta;
  264.     },
  265.  
  266.     addFilterIds: function(ids) {
  267.         var filterIds = this.filterUserIds;
  268.         $.extend(filterIds, set(ids));
  269.  
  270.         // filter out existing items
  271.         this.items = $.grep(this.items, function (i) {
  272.             return !(i.user in filterIds);
  273.         });
  274.  
  275.         this.refreshUnreadCount();
  276.     },
  277.  
  278.     feedAdded: function(account, feed) {
  279.         if (feed.type === 'group') {
  280.             if (feed.filter)
  281.                 this.addFilterIds(feed.ids);
  282.         } else if (feed.type === 'search') {
  283.             if (feed.merge) {
  284.                 var q = feed.query;
  285.                 this.addMerge(q, function tweetHasQuery(t) {
  286.                     return t.search === q && !account.isMention(t);
  287.                 });
  288.             }
  289.         }
  290.     },
  291.  
  292.     feedDeleted: function(feed, customFeeds) {
  293.         var self = this;
  294.         console.log('feedDeleted: ' + feed + ' ' + feed.type);
  295.         if (feed.type === 'group') {
  296.             if (feed.filter) {
  297.                 // clear the id map and re-add all the ids from remaining feeds
  298.                 this.filterUserIds = {};
  299.                 $.each(customFeeds, function (i, customFeed) {
  300.                     if (customFeed.name !== feed.name && customFeed.filter)
  301.                         self.addFilterIds(customFeed.userIds);
  302.                 });
  303.             }
  304.         } else if (feed.type === 'search') {
  305.             if (feed.merge)
  306.                 return this.removeMerge(feed.query);
  307.         } else
  308.             console.warn('feedDeleted got unknown type: ' + feed.type);
  309.     },
  310.  
  311.     // Returns true if the tweets should be added to this feed.
  312.     has: function(item, account) {
  313.         // filter out any tweets with ids in this.filterUserIds
  314.         if (item.user in this.filterUserIds) return false;
  315.  
  316.         // include directs
  317.         if (item.sender_id && account.selfUser) {
  318.             if (isDirectInvite(account.selfUser.id, item, account.invite_message))
  319.                 return false; // leave out direct message invites
  320.  
  321.             return true;
  322.         }
  323.  
  324.         // include all non search tweets
  325.         if (!item.search)
  326.             return true;
  327.         else {
  328.             // see if its a merged search
  329.             var foundMerge = false;
  330.             $.each(this.merges, function (name, merge) {
  331.                 if (merge(item)) {
  332.                     foundMerge = true;
  333.                     return false; // stop .each
  334.                 }
  335.             });
  336.  
  337.             return foundMerge;
  338.         }
  339.  
  340.         return true;
  341.     }
  342. });
  343.  
  344. function isDirectInvite(selfId, item, message) {
  345.     return item.sender_id === selfId && item.text === message;
  346. }
  347.  
  348. function TwitterMentionFeedModel(tweets) {
  349.     TwitterFeedModel.call(this, tweets);
  350. }
  351.  
  352. TwitterMentionFeedModel.inheritsFrom(TwitterFeedModel, {
  353.     sourceURL: 'statuses/mentions.json',
  354.     alwaysMentions: true,
  355.     serialize: function() { return {count: this.unreadCount, label: "Mentions", name: 'mentions', type: 'mentions'}; },
  356.     name: 'mentions',
  357.     has: function(item) { return item.mention; }
  358. });
  359.  
  360. function TwitterGroupFeedModel(tweets, feedOpts) {
  361.     TwitterFeedModel.call(this, tweets);
  362.  
  363.     assert(feedOpts.name);
  364.     assert(feedOpts.type === 'group');
  365.  
  366.     this.name = feedOpts.name;
  367.     this.groupName = feedOpts.groupName;
  368.     this.userIds = feedOpts.ids;
  369.     this.filter = feedOpts.filter;
  370.     this.popups = feedOpts.popups;
  371.     this.noCount = feedOpts.noCount;
  372.  
  373.     // for quick lookup
  374.     this.userIdsSet = set(this.userIds);
  375. }
  376.  
  377. TwitterGroupFeedModel.inheritsFrom(TwitterFeedModel, {
  378.     addsToUnreadCount: function() { return !this.noCount; },
  379.  
  380.     serialize: function() {
  381.         return {
  382.             count: this.unreadCount, 
  383.             filter: this.filter,
  384.             ids: this.userIds,
  385.             label: this.groupName, 
  386.             name: this.name,
  387.             groupName: this.groupName,
  388.             popups: this.popups,
  389.             type: 'group',
  390.             noCount: this.noCount
  391.         };
  392.     },
  393.  
  394.     updateOptions: function(info) {
  395.         var changed = false;
  396.  
  397.         assert(info.ids);
  398.         assert(info.groupName);
  399.  
  400.         this.groupName = info.groupName;
  401.  
  402.         if (this.filter != info.filter) {
  403.             this.filter = info.filter;
  404.             changed = true;
  405.         }
  406.  
  407.         var oldSet = this.userIdsSet;
  408.         this.userIdsSet = set(info.ids);
  409.         if (!setsEqual(oldSet, this.userIdsSet)) {
  410.             this.userIds = info.ids;
  411.             changed = true;
  412.         }
  413.  
  414.  
  415.         this.popups = info.popups;
  416.  
  417.         return changed;
  418.     },
  419.  
  420.     has: function(item) {
  421.         return item.user in this.userIdsSet;
  422.     }
  423. });
  424.  
  425. function TwitterSearchFeedModel(tweets, feedOpts) {
  426.     TwitterFeedModel.call(this, tweets);
  427.  
  428.     assert(feedOpts.type === 'search');
  429.  
  430.     this.name = feedOpts.name;
  431.     this.query = feedOpts.query;
  432.     this.merge = feedOpts.merge;
  433.     this.popups = feedOpts.popups;
  434.     this.title = feedOpts.title;
  435.     this.save = feedOpts.save || false;
  436.     this.noCount = feedOpts.noCount;
  437.  
  438.     this.sourceURL = 'http://search.twitter.com/search.json';
  439.     this.sourceData = {q: this.query};
  440. }
  441.  
  442. TwitterSearchFeedModel.inheritsFrom(TwitterFeedModel, {
  443.     addsToUnreadCount: function() {
  444.         // only contribute to unread count when saved
  445.         return this.save && !this.noCount;
  446.     },
  447.  
  448.     has: function(item) { return item.search === this.query; },
  449.  
  450.     serialize: function() {
  451.         return {count: this.unreadCount, 
  452.                 label: this.title || this.query,
  453.                 name: this.name,
  454.                 title: this.title,
  455.                 type: 'search',
  456.                 query: this.query,
  457.                 merge: this.merge,
  458.                 popups: this.popups,
  459.                 save: this.save,
  460.                 noCount: this.noCount};
  461.     },
  462.  
  463.     updateOptions: function(feed) {
  464.         var self = this, changed = false;
  465.  
  466.         if (feed.merge !== this.merge) { 
  467.             this.merge = feed.merge;
  468.             changed = true;
  469.         }
  470.  
  471.         if (feed.query !== this.query) {
  472.             this.source.searchQuery = this.query = feed.query;
  473.             changed = true;
  474.             this.updateSourceNow();
  475.         }
  476.  
  477.         console.warn('search updateOptions:\n' + JSON.stringify(feed));
  478.  
  479.         this.popups = feed.popups;
  480.         this.title = feed.title;
  481.         this.save = feed.save || false;
  482.  
  483.         return changed;
  484.     },
  485.  
  486.     makeSource: function(account) {
  487.         var source = new TwitterTimelineSearchSource(account, this.query);
  488.         this.source = source;
  489.         source.feed = this;
  490.         return source;
  491.     }
  492. });
  493.  
  494. function TwitterDirectsModel(tweets, feedOpts) {
  495.     TwitterFeedModel.call(this, tweets);
  496. }
  497.  
  498. TwitterDirectsModel.inheritsFrom(TwitterFeedModel, {
  499.     name: 'directs',
  500.     serialize: function() {
  501.         return {
  502.             count: this.unreadCount,
  503.             label: 'Directs',
  504.             name: 'directs',
  505.             type: 'directs'
  506.         };
  507.     },
  508.  
  509.     has: function (item, account) {
  510.         return item.sender_id && !isDirectInvite(account.selfUser.id, item, account.invite_message);
  511.     },
  512.  
  513.     makeSource: function(account) {
  514.         var source = new TwitterTimelineDirectSource(account);
  515.         this.source = source;
  516.         source.feed = this;
  517.         return source;
  518.     }
  519. });
  520.  
  521. /**
  522.  * Collects tweets that are favorited.
  523.  */
  524. function TwitterFavoritesFeedModel(tweets) { TwitterFeedModel.call(this, tweets); }
  525. TwitterFavoritesFeedModel.inheritsFrom(TwitterFeedModel, {
  526.     has: function(item) { return item.favorited ? true : false; },
  527.     name: 'favorites',
  528.     sourceURL: 'favorites.json',
  529.     limitById: false,
  530.     autoUpdates: false,
  531.     displayAllAsUnread: true,
  532.     scrollToBottom: true,
  533.     makeSource: function(account) {
  534.         var self = this,
  535.             source = TwitterFeedModel.prototype.makeSource.call(this, account);
  536.  
  537.         source.onUpdate = function (tweets) {
  538.             // mark any tweets cached as favorite but not returned in the favorites feed update
  539.             // as non-favorited.
  540.             var ids = set($.map(tweets, function (t) { return t.id; }));
  541.             var toUnfavorite = [];
  542.             $.each(source.account.tweets, function (id, tweet) {
  543.                 if (tweet.favorited && !(id in ids)) {
  544.                     toUnfavorite.push(id);
  545.                     tweet.favorited = 0;
  546.                 }
  547.             });
  548.  
  549.             if (toUnfavorite.length) {
  550.                 console.log('unfavoriting cached tweets: ' + toUnfavorite.join(', '));
  551.                 source.account.cacheFavorited(toUnfavorite, false);
  552.                 account.refreshFeedItems(self);
  553.             }
  554.         };
  555.  
  556.         return source;
  557.     }
  558. });
  559.  
  560. /**
  561.  * Collects tweets that have a certain user id.
  562.  *
  563.  * TODO: just use the Group model above?
  564.  */
  565. function TwitterUserFeedModel(tweets, feedDesc) {
  566.     if (!tweets)
  567.         return;
  568.     TwitterFeedModel.call(this, tweets);
  569.     if (feedDesc.name)
  570.         this.name = feedDesc.name;
  571.     this.userScreenName = feedDesc.screen_name;
  572.     assert(this.userScreenName);
  573.     this.sourceURL = 'statuses/user_timeline.json';
  574.     this.sourceData = {screen_name: this.userScreenName};
  575. }
  576.  
  577. TwitterUserFeedModel.inheritsFrom(TwitterFeedModel, {
  578.     has: function(item, account) {
  579.         var user = account.users[item.user];
  580.         return user && user.screen_name && user.screen_name.toLowerCase() === this.userScreenName.toLowerCase();
  581.     },
  582.     autoUpdates: false,
  583.     scrollToBottom: true,
  584.     displayAllAsUnread: true,
  585.     addsToUnreadCount: function() { return false; },
  586.     serialize: function() {
  587.         return {
  588.             name: this.name,
  589.             screen_name: this.userScreenName,
  590.             label: this.userScreenName,
  591.             type: 'user'
  592.         };
  593.     }
  594. });
  595.  
  596. function TwitterHistoryFeedModel(tweets, feedDesc) {
  597.     console.warn('TwitterHistoryFeedModel');
  598.     console.warn(' tweets: ' + tweets);
  599.     console.warn(' feedDesc: ' + JSON.stringify(feedDesc));
  600.  
  601.     TwitterUserFeedModel.call(this, tweets, feedDesc);
  602. }
  603.  
  604. TwitterHistoryFeedModel.inheritsFrom(TwitterUserFeedModel, {
  605.     serialize: undefined,
  606.     name: 'history'
  607. });
  608.  
  609. /**
  610.  * as a hack, the first time friends_timeline gets checked, if there are
  611.  * no self tweets in it, we go get user_timeline as well, so that we know
  612.  * your last self tweet for sure.
  613.  */
  614. function selfUpdate(tweets, done) {
  615.     if (this._didExtraUpdate) return done(tweets);
  616.     this._didExtraUpdate = true;
  617.     var self = this, account = this.account;
  618.  
  619.     var found;
  620.     $.each(tweets, function (i, tweet) {
  621.         if (account.isSelfTweet(tweet)) {
  622.             found = tweet;
  623.             return false;
  624.         }
  625.     });
  626.  
  627.     if (found)
  628.         return guard(function() { 
  629.             account.possibleSelfTweets(tweets);
  630.             done(tweets);
  631.         });
  632.  
  633.     // TODO: actually use feeds.history here.
  634.     var url = account.apiRoot + 'statuses/user_timeline.json';
  635.     var maxId = account.feeds.history.source.maxId;
  636.     if (maxId && maxId !== '0')
  637.         url = urlQuery(url, {since_id: maxId});
  638.  
  639.     function error(_error) {
  640.         console.error('error retrieving user_timeline');
  641.         done(tweets);
  642.     }
  643.  
  644.     function _success(data, status) {
  645.         function _done(selfTweets) {
  646.             console.log('extra update got ' + data.length + ' self tweets');
  647.             arrayExtend(tweets, selfTweets);
  648.             tweets.sort(tweetsByTime);
  649.             account.possibleSelfTweets(tweets);
  650.             done(tweets);
  651.         }
  652.         self.ajaxSuccess(data, status, _done, error);
  653.     }
  654.  
  655.     account.urlRequest(url, _success, error);
  656. }
  657.  
  658. function createFeed(allTweets, feedDesc) {
  659.     var feedTypes = {timeline: TwitterTimelineFeedModel,
  660.                      mentions: TwitterMentionFeedModel,
  661.                      directs: TwitterDirectsModel,
  662.                      group:  TwitterGroupFeedModel,
  663.                      search: TwitterSearchFeedModel,
  664.                      user: TwitterUserFeedModel};
  665.  
  666.     var feedType = feedTypes[feedDesc.type];
  667.     var feed = new feedType(allTweets, feedDesc);
  668.  
  669.     if (feedDesc.type === 'timeline')
  670.         feed.extraUpdate = selfUpdate;
  671.  
  672.     return feed;
  673. }
  674.  
  675. function hasSearchQuery(q) {
  676.     return function tweetHasQuery(t) { return t.search === q; };
  677. }
  678.  
  679.