home *** CD-ROM | disk | FTP | other *** search
/ Maximum CD 2011 February / maximum-cd-2011-02.iso / DiscContents / digsby_setup85.exe / lib / plugins / twitter / res / timeline.js < prev    next >
Encoding:
Text File  |  2010-11-17  |  19.7 KB  |  670 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 = (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 = item.id > maxId ? 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 (notifyNow)
  163.             callEach(this.listeners, 'finish', opts && opts.scroll);
  164.  
  165.         if (!opts.ignoreIds && self.source && (self.source === source || source === undefined))
  166.             self.source.updateMinMax(maxId);
  167.     },
  168.     
  169.     refreshUnreadCount: function () {
  170.         var count = 0;
  171.         var items = this.items;
  172.         for (var i = items.length-1; i >= 0; --i) {
  173.             if (!items[i].read)
  174.                 count++;
  175.         }
  176.         this.unreadCount = count;
  177.     },
  178.  
  179.     trim: function() {
  180.         var self = this;
  181.         if (this.items.length > this.limit) {
  182.             var numToDelete = this.items.length - this.limit;
  183.             $.each(this.items, function (i, item) {
  184.                 if (i >= numToDelete)
  185.                     return false;
  186.  
  187.                 delete self.all[item.id];
  188.                 self.removeItemListener(item);
  189.                 if (!item.read)
  190.                     self.unreadCount -= 1;
  191.             });
  192.             
  193.             var deletedItems = self.items.slice(0, numToDelete);
  194.             console.log(this + ' trimmed ' + deletedItems.length + ' items');
  195.             self.items = self.items.slice(numToDelete);
  196.         }
  197.     },
  198.  
  199.     addItemListener: function (item) {
  200.         if (!('feeds' in item)) item.feeds = {};
  201.         item.feeds[this.feedId] = this;
  202.     },
  203.  
  204.     removeItemListener: function (item) {
  205.         if (item.feeds !== undefined)
  206.             delete item.feeds[this.feedId];
  207.     },
  208.  
  209.     addFeedListener: function (listener) { this.listeners.push(listener); },
  210.  
  211.     removeFeedListener: function (listener) {
  212.         this.listeners = $.grep(this.listeners, function (item, i) {
  213.             return listener !== item;
  214.         });
  215.     }
  216. });
  217.  
  218. function TwitterTimelineFeedModel(tweets) {
  219.     TwitterFeedModel.call(this, tweets);
  220. }
  221.  
  222. TwitterTimelineFeedModel.inheritsFrom(TwitterFeedModel, {
  223.     name: 'timeline',
  224.     serialize: function() {
  225.         return {
  226.             count: this.unreadCount, 
  227.             label: "Timeline", 
  228.             name: 'timeline', 
  229.             type: 'timeline'
  230.         }; 
  231.     },
  232.     sourceURL: 'statuses/home_timeline.json',
  233.  
  234.     addMerge: function(name, mergeFunc) {
  235.         this.merges[name] = mergeFunc;
  236.     },
  237.  
  238.     removeMerge: function(name) {
  239.         if (name in this.merges) {
  240.             delete this.merges[name];
  241.             return this.pruneItems();
  242.         } else
  243.             console.warn('removeMerge(' + name + ') called, but not in this.merges');
  244.  
  245.         return 0;
  246.     },
  247.  
  248.     pruneItems: function() {
  249.         var self = this;
  250.         var oldCount = this.items.length;
  251.         this.items = $.grep(this.items, function(i) { return self.has(i); });
  252.         var delta = oldCount-this.items.length;
  253.         console.log('pruneItems removed ' + delta);
  254.         return delta;
  255.     },
  256.  
  257.     addFilterIds: function(ids) {
  258.         var filterIds = this.filterUserIds;
  259.         $.extend(filterIds, set(ids));
  260.  
  261.         // filter out existing items
  262.         this.items = $.grep(this.items, function (i) {
  263.             return !(i.user in filterIds);
  264.         });
  265.  
  266.         this.refreshUnreadCount();
  267.     },
  268.  
  269.     feedAdded: function(account, feed) {
  270.         if (feed.type === 'group') {
  271.             if (feed.filter)
  272.                 this.addFilterIds(feed.ids);
  273.         } else if (feed.type === 'search') {
  274.             if (feed.merge) {
  275.                 var q = feed.query;
  276.                 this.addMerge(q, function tweetHasQuery(t) {
  277.                     return t.search === q && !account.isMention(t);
  278.                 });
  279.             }
  280.         }
  281.     },
  282.  
  283.     feedDeleted: function(feed, customFeeds) {
  284.         var self = this;
  285.         console.log('feedDeleted: ' + feed + ' ' + feed.type);
  286.         if (feed.type === 'group') {
  287.             if (feed.filter) {
  288.                 // clear the id map and re-add all the ids from remaining feeds
  289.                 this.filterUserIds = {};
  290.                 $.each(customFeeds, function (i, customFeed) {
  291.                     if (customFeed.name !== feed.name && customFeed.filter)
  292.                         self.addFilterIds(customFeed.userIds);
  293.                 });
  294.             }
  295.         } else if (feed.type === 'search') {
  296.             if (feed.merge)
  297.                 return this.removeMerge(feed.query);
  298.         } else
  299.             console.warn('feedDeleted got unknown type: ' + feed.type);
  300.     },
  301.  
  302.     // Returns true if the tweets should be added to this feed.
  303.     has: function(item, account) {
  304.         // filter out any tweets with ids in this.filterUserIds
  305.         if (item.user in this.filterUserIds) return false;
  306.  
  307.         // include directs
  308.         if (item.sender_id) {
  309.             if (isDirectInvite(account.selfUser.id, item, account.invite_message))
  310.                 return false; // leave out direct message invites
  311.  
  312.             return true;
  313.         }
  314.  
  315.         // include all non search tweets
  316.         if (!item.search)
  317.             return true;
  318.         else {
  319.             // see if its a merged search
  320.             var foundMerge = false;
  321.             $.each(this.merges, function (name, merge) {
  322.                 if (merge(item)) {
  323.                     foundMerge = true;
  324.                     return false; // stop .each
  325.                 }
  326.             });
  327.  
  328.             return foundMerge;
  329.         }
  330.  
  331.         return true;
  332.     }
  333. });
  334.  
  335. function isDirectInvite(selfId, item, message) {
  336.     return item.sender_id === selfId && item.text === message;
  337. }
  338.  
  339. function TwitterMentionFeedModel(tweets) {
  340.     TwitterFeedModel.call(this, tweets);
  341. }
  342.  
  343. TwitterMentionFeedModel.inheritsFrom(TwitterFeedModel, {
  344.     sourceURL: 'statuses/mentions.json',
  345.     alwaysMentions: true,
  346.     serialize: function() { return {count: this.unreadCount, label: "Mentions", name: 'mentions', type: 'mentions'}; },
  347.     name: 'mentions',
  348.     has: function(item) { return item.mention; }
  349. });
  350.  
  351. function TwitterGroupFeedModel(tweets, feedOpts) {
  352.     TwitterFeedModel.call(this, tweets);
  353.  
  354.     assert(feedOpts.name);
  355.     assert(feedOpts.type === 'group');
  356.  
  357.     this.name = feedOpts.name;
  358.     this.groupName = feedOpts.groupName;
  359.     this.userIds = feedOpts.ids;
  360.     this.filter = feedOpts.filter;
  361.     this.popups = feedOpts.popups;
  362.     this.noCount = feedOpts.noCount;
  363.  
  364.     // for quick lookup
  365.     this.userIdsSet = set(this.userIds);
  366. }
  367.  
  368. TwitterGroupFeedModel.inheritsFrom(TwitterFeedModel, {
  369.     addsToUnreadCount: function() { return !this.noCount; },
  370.  
  371.     serialize: function() {
  372.         return {
  373.             count: this.unreadCount, 
  374.             filter: this.filter,
  375.             ids: this.userIds,
  376.             label: this.groupName, 
  377.             name: this.name,
  378.             groupName: this.groupName,
  379.             popups: this.popups,
  380.             type: 'group',
  381.             noCount: this.noCount
  382.         };
  383.     },
  384.  
  385.     updateOptions: function(info) {
  386.         var changed = false;
  387.  
  388.         assert(info.ids);
  389.         assert(info.groupName);
  390.  
  391.         this.groupName = info.groupName;
  392.  
  393.         if (this.filter != info.filter) {
  394.             this.filter = info.filter;
  395.             changed = true;
  396.         }
  397.  
  398.         var oldSet = this.userIdsSet;
  399.         this.userIdsSet = set(info.ids);
  400.         if (!setsEqual(oldSet, this.userIdsSet)) {
  401.             this.userIds = info.ids;
  402.             changed = true;
  403.         }
  404.  
  405.  
  406.         this.popups = info.popups;
  407.  
  408.         return changed;
  409.     },
  410.  
  411.     has: function(item) {
  412.         return item.user in this.userIdsSet;
  413.     }
  414. });
  415.  
  416. function TwitterSearchFeedModel(tweets, feedOpts) {
  417.     TwitterFeedModel.call(this, tweets);
  418.  
  419.     assert(feedOpts.type === 'search');
  420.  
  421.     this.name = feedOpts.name;
  422.     this.query = feedOpts.query;
  423.     this.merge = feedOpts.merge;
  424.     this.popups = feedOpts.popups;
  425.     this.title = feedOpts.title;
  426.     this.save = feedOpts.save || false;
  427.     this.noCount = feedOpts.noCount;
  428.  
  429.     this.sourceURL = 'http://search.twitter.com/search.json';
  430.     this.sourceData = {q: this.query};
  431. }
  432.  
  433. TwitterSearchFeedModel.inheritsFrom(TwitterFeedModel, {
  434.     addsToUnreadCount: function() {
  435.         // only contribute to unread count when saved
  436.         return this.save && !this.noCount;
  437.     },
  438.  
  439.     has: function(item) { return item.search === this.query; },
  440.  
  441.     serialize: function() {
  442.         return {count: this.unreadCount, 
  443.                 label: this.title || this.query,
  444.                 name: this.name,
  445.                 title: this.title,
  446.                 type: 'search',
  447.                 query: this.query,
  448.                 merge: this.merge,
  449.                 popups: this.popups,
  450.                 save: this.save,
  451.                 noCount: this.noCount};
  452.     },
  453.  
  454.     updateOptions: function(feed) {
  455.         var self = this, changed = false;
  456.  
  457.         if (feed.merge !== this.merge) { 
  458.             this.merge = feed.merge;
  459.             changed = true;
  460.         }
  461.  
  462.         if (feed.query !== this.query) {
  463.             this.source.searchQuery = this.query = feed.query;
  464.             changed = true;
  465.             this.updateSourceNow();
  466.         }
  467.  
  468.         console.warn('search updateOptions:\n' + JSON.stringify(feed));
  469.  
  470.         this.popups = feed.popups;
  471.         this.title = feed.title;
  472.         this.save = feed.save || false;
  473.  
  474.         return changed;
  475.     },
  476.  
  477.     makeSource: function(account) {
  478.         var source = new TwitterTimelineSearchSource(account, this.query);
  479.         this.source = source;
  480.         source.feed = this;
  481.         return source;
  482.     }
  483. });
  484.  
  485. function TwitterDirectsModel(tweets, feedOpts) {
  486.     TwitterFeedModel.call(this, tweets);
  487. }
  488.  
  489. TwitterDirectsModel.inheritsFrom(TwitterFeedModel, {
  490.     name: 'directs',
  491.     serialize: function() {
  492.         return {
  493.             count: this.unreadCount,
  494.             label: 'Directs',
  495.             name: 'directs',
  496.             type: 'directs'
  497.         };
  498.     },
  499.  
  500.     has: function (item, account) {
  501.         return item.sender_id && !isDirectInvite(account.selfUser.id, item, account.invite_message);
  502.     },
  503.  
  504.     makeSource: function(account) {
  505.         var source = new TwitterTimelineDirectSource(account);
  506.         this.source = source;
  507.         source.feed = this;
  508.         return source;
  509.     }
  510. });
  511.  
  512. /**
  513.  * Collects tweets that are favorited.
  514.  */
  515. function TwitterFavoritesFeedModel(tweets) { TwitterFeedModel.call(this, tweets); }
  516. TwitterFavoritesFeedModel.inheritsFrom(TwitterFeedModel, {
  517.     has: function(item) { return item.favorited ? true : false; },
  518.     name: 'favorites',
  519.     sourceURL: 'favorites.json',
  520.     limitById: false,
  521.     autoUpdates: false,
  522.     displayAllAsUnread: true,
  523.     scrollToBottom: true,
  524.     makeSource: function(account) {
  525.         var self = this,
  526.             source = TwitterFeedModel.prototype.makeSource.call(this, account);
  527.  
  528.         source.onUpdate = function (tweets) {
  529.             // mark any tweets cached as favorite but not returned in the favorites feed update
  530.             // as non-favorited.
  531.             var ids = set($.map(tweets, function (t) { return t.id; }));
  532.             var toUnfavorite = [];
  533.             $.each(source.account.tweets, function (id, tweet) {
  534.                 if (tweet.favorited && !(id in ids)) {
  535.                     toUnfavorite.push(id);
  536.                     tweet.favorited = 0;
  537.                 }
  538.             });
  539.  
  540.             if (toUnfavorite.length) {
  541.                 console.log('unfavoriting cached tweets: ' + toUnfavorite.join(', '));
  542.                 source.account.cacheFavorited(toUnfavorite, false);
  543.                 account.refreshFeedItems(self);
  544.             }
  545.         };
  546.  
  547.         return source;
  548.     }
  549. });
  550.  
  551. /**
  552.  * Collects tweets that have a certain user id.
  553.  *
  554.  * TODO: just use the Group model above?
  555.  */
  556. function TwitterUserFeedModel(tweets, feedDesc) {
  557.     if (!tweets)
  558.         return;
  559.     TwitterFeedModel.call(this, tweets);
  560.     if (feedDesc.name)
  561.         this.name = feedDesc.name;
  562.     this.userScreenName = feedDesc.screen_name;
  563.     assert(this.userScreenName);
  564.     this.sourceURL = 'statuses/user_timeline.json';
  565.     this.sourceData = {screen_name: this.userScreenName};
  566. }
  567.  
  568. TwitterUserFeedModel.inheritsFrom(TwitterFeedModel, {
  569.     has: function(item, account) {
  570.         var user = account.users[item.user];
  571.         return user && user.screen_name.toLowerCase() === this.userScreenName.toLowerCase();
  572.     },
  573.     autoUpdates: false,
  574.     scrollToBottom: true,
  575.     displayAllAsUnread: true,
  576.     addsToUnreadCount: function() { return false; },
  577.     serialize: function() {
  578.         return {
  579.             name: this.name,
  580.             screen_name: this.userScreenName,
  581.             label: this.userScreenName,
  582.             type: 'user'
  583.         };
  584.     }
  585. });
  586.  
  587. function TwitterHistoryFeedModel(tweets, feedDesc) {
  588.     console.warn('TwitterHistoryFeedModel');
  589.     console.warn(' tweets: ' + tweets);
  590.     console.warn(' feedDesc: ' + JSON.stringify(feedDesc));
  591.  
  592.     TwitterUserFeedModel.call(this, tweets, feedDesc);
  593. }
  594.  
  595. TwitterHistoryFeedModel.inheritsFrom(TwitterUserFeedModel, {
  596.     serialize: undefined,
  597.     name: 'history'
  598. });
  599.  
  600. /**
  601.  * as a hack, the first time friends_timeline gets checked, if there are
  602.  * no self tweets in it, we go get user_timeline as well, so that we know
  603.  * your last self tweet for sure.
  604.  */
  605. function selfUpdate(tweets, done) {
  606.     if (this._didExtraUpdate) return done(tweets);
  607.     this._didExtraUpdate = true;
  608.     var self = this, account = this.account;
  609.  
  610.     var found;
  611.     $.each(tweets, function (i, tweet) {
  612.         if (account.isSelfTweet(tweet)) {
  613.             found = tweet;
  614.             return false;
  615.         }
  616.     });
  617.  
  618.     if (found)
  619.         return guard(function() { 
  620.             account.possibleSelfTweets(tweets);
  621.             done(tweets);
  622.         });
  623.  
  624.     // TODO: actually use feeds.history here.
  625.     var url = account.apiRoot + 'statuses/user_timeline.json';
  626.     var maxId = account.feeds.history.source.maxId;
  627.     if (maxId)
  628.         url = urlQuery(url, {since_id: maxId});
  629.  
  630.     function error(_error) {
  631.         console.error('error retrieving user_timeline');
  632.         done(tweets);
  633.     }
  634.  
  635.     function _success(data, status) {
  636.         function _done(selfTweets) {
  637.             console.log('extra update got ' + data.length + ' self tweets');
  638.             arrayExtend(tweets, selfTweets);
  639.             tweets.sort(tweetsByTime);
  640.             account.possibleSelfTweets(tweets);
  641.             done(tweets);
  642.         }
  643.         self.ajaxSuccess(data, status, _done, error);
  644.     }
  645.  
  646.     account.urlRequest(url, _success, error);
  647. }
  648.  
  649. function createFeed(allTweets, feedDesc) {
  650.     var feedTypes = {timeline: TwitterTimelineFeedModel,
  651.                      mentions: TwitterMentionFeedModel,
  652.                      directs: TwitterDirectsModel,
  653.                      group:  TwitterGroupFeedModel,
  654.                      search: TwitterSearchFeedModel,
  655.                      user: TwitterUserFeedModel};
  656.  
  657.     var feedType = feedTypes[feedDesc.type];
  658.     var feed = new feedType(allTweets, feedDesc);
  659.  
  660.     if (feedDesc.type === 'timeline')
  661.         feed.extraUpdate = selfUpdate;
  662.  
  663.     return feed;
  664. }
  665.  
  666. function hasSearchQuery(q) {
  667.     return function tweetHasQuery(t) { return t.search === q; };
  668. }
  669.  
  670.