// This Javascript program will never make more than this many Twitter API requests var totalApiRequests = 25; // Version of data stored in localStorage; increment to force a cache reset var dbVersion = 3; // Archive of fetched tweets; cached in localStorage // It's a simple array of minimalist tweet objects, sorted in date order var tweetDb = []; // User we're operating on var user = null; function switchTab(tab) { $.each(["text", "map", "pictures", "urls"], function (i, t) { $("div#tab-" + t).css("display", "none"); }); $("div#tab-" + tab).css("display", ""); if (tab != "noname") { window.location.hash="#" + tab; } } function notifyDbUpdated() { $("#tweetCount").text("" + tweetDb.length); } // Main entry for the page function tweetchive(username) { checkDbVersion(); // Handle no username if ("" === username) { switchTab("noname"); return; } // Handle tabs; default tab is map if (window.location.hash && window.location.hash != "") { switchTab(window.location.hash.slice(1)); } else { switchTab("map"); } // Set the global variable for the user user = username; // Display the username $("input#query").val(username); // Set up the map createMap(); // Get the local keys out of localStorage before anyone modifies it tweetDb = getTweetDb(user); // Fire off asynchronous requests for more tweets // As a side effect, this may draw on the map fetchTweets(); // Show all the local tweets we have showAllLocal(); } // Start a process to grab tweets. Should be asynchronous function fetchTweets() { var latest = newestTweet(); if (!latest) { fetchAllTweets(null); } else { fetchTweetsSince(latest); } } // Call the given URL with jQuery AJAX options function callUrl(url, options, tries) { // Global limit totalApiRequests -= 1; if (totalApiRequests < 0) { return; } // Time out a specific request after N tries if (typeof tries == 'undefined') { tries = 3; } tries -= 1; if (tries < 0) { return; } console.log("Fetch (total " + totalApiRequests + " tries " + tries + ")", url) options.timeout = 5000; options.error = function(jqXHR, textStatus, errorThrown) { console.log("JSONP error", jqXHR, textStatus, errorThrown); // On error try again return callUrl(url, options, tries); } $.ajax(url, options); } // Get all the tweets. This will involve successive calls function fetchAllTweets(maxId) { var url = 'https://api.twitter.com/1/statuses/user_timeline.json?screen_name=' + user + '&include_entities=true&count=200&trim_user=true'; if (maxId) { url += "&max_id=" + maxId; } callUrl(url, { dataType: 'jsonp', success: storeAndGetTweets, }); } // Get only the tweets since this tweet // FIXME doesn't handle more than 200 tweets function fetchTweetsSince(tweet) { var url = 'https://api.twitter.com/1/statuses/user_timeline.json?screen_name=' + user + '&include_entities=true&count=200&trim_user=true' url += "&since_id=" + tweet.i; callUrl(url, { dataType: 'jsonp', success: storeTweets, }); } // Store tweets from an AJAX call, then fetch more up to maxId function storeAndGetTweets(tweets, textStatus, jqXHR) { console.log("Response received:", textStatus, tweets.length, " tweets returned."); store(tweets); // We always get at least 1 tweet because of the fencepost condition on max_id // If we got more than 1 we can expect there's more, so let's ask again! if (tweets.length > 1) { var maxId = tweets[tweets.length-1].id_str; fetchAllTweets(maxId); } } // Store tweets from an AJAX call function storeTweets(tweets, textStatus, jqXHR) { console.log("Response received:", textStatus, tweets.length, " tweets returned."); store(tweets); } // Return the newest tweet we have stored away function newestTweet() { if (tweetDb.length == 0) { return null; } else { return tweetDb[0]; } } // Store the provided tweets in localStorage // Not particularly threadsafe; two simultaneous stores will fail. function store(fullTweets) { // Clone the database into a hash for operation var tweets = {}; $.each(tweetDb, function(i, tweet) { tweets[tweet.i] = tweet; }); // Append all the new tweets $.each(fullTweets, function(i, fullTweet) { var ts = moment(fullTweet.created_at, "ddd MMM DD HH:mm:ss Z YYYY").valueOf(); // Extract the stuff we actually care about in a tweet var tweet = { i: fullTweet.id_str, t: fullTweet.text, d: ts } // Extract geotags if (fullTweet.coordinates) { tweet.c = fullTweet.coordinates.coordinates; } // Extract images // FIXME: handle multiple images, other kinds of media if (fullTweet.entities && fullTweet.entities.media) { tweet.p = fullTweet.entities.media[0].media_url; } // Extract URLs if (fullTweet.entities && fullTweet.entities.urls && fullTweet.entities.urls.length > 0) { tweet.u = []; $.each(fullTweet.entities.urls, function(i, u) { tweet.u.push([u.expanded_url, u.indices[0], u.indices[1]]); }); } // Show any tweet that's new in the collection if (!(tweet.i in tweets)) { showTweet(tweet); } // And store the tweet in the temporary database tweets[tweet.i] = tweet; }); // Get the database back as an array, sort it var newDb = []; $.each(tweets, function(i, tweet) { newDb.push(tweet); }); // Sort the new db, then swap it in to tweetDb newDb.sort(function(a, b) { return b.d - a.d; }); tweetDb = newDb; // Persist the db. // FIXME: only need to do this once even if we're fetching 3500 tweets. storeTweetDb(user); // Notify about the update notifyDbUpdated(); } // Show all the tweets stored in the local storage archive // This process is synchronous but should be fast, not involving the network function showAllLocal(localKeys) { $.each(tweetDb, function(i, tweet) { showTweet(tweet); }); } // Show a single tweet on the map function showTweet(tweet) { showText(tweet); if (tweet.p) { showPicture(tweet); } if (tweet.c) { showOnMap(tweet); } if (tweet.u) { showUrls(tweet); } } //// Text formatting functions // Return some HTML representing the tweet function formatTweet(tweet) { // Swizzle the tweet text to have links var tweetText = tweet.t; if (tweet.u) { for (var i = tweet.u.length - 1; i >= 0; i--) { var url = tweet.u[i][0]; var s = tweet.u[i][1]; var e = tweet.u[i][2]; var newText = (tweetText.slice(0, s) + '' + tweetText.slice(s, e) + '' + tweetText.slice(e, tweetText.length)); tweetText = newText; } } // Now build up the full tweet display var display = []; display.push('
');
$.each(tweet.u, function(i, u) {
$("div#tab-urls").append('' + u[0] + '' + "
");
});
$("div#tab-urls").append('