1/* 2 * jQuery LiveTwitter 1.7.3 3 * - Live updating Twitter plugin for jQuery 4 * 5 * Copyright (c) 2009-2011 Inge Jørgensen (elektronaut.no) 6 * Licensed under the MIT license (MIT-LICENSE.txt) 7 * 8 * $Date: 2011/10/28$ 9 */ 10 11/*jslint browser: true, devel: true, onevar: false, immed: false, regexp: false, indent: 2 */ 12/*global window: false, jQuery: false */ 13 14/* 15 * Usage example: 16 * $("#twitterSearch").liveTwitter('bacon', {limit: 10, rate: 15000}); 17 */ 18 19 20(function ($) { 21 22 // Extend jQuery with a reverse function if it isn't already defined 23 if (!$.fn.reverse) { 24 $.fn.reverse = function () { 25 return this.pushStack(this.get().reverse(), arguments); 26 }; 27 } 28 29 $.fn.liveTwitter = function (query, options, callback) { 30 var domNode = this; 31 $(this).each(function () { 32 var settings = {}; 33 34 // Does this.twitter already exist? Let's just change the settings. 35 if (this.twitter) { 36 settings = $.extend(this.twitter.settings, options); 37 this.twitter.settings = settings; 38 if (query) { 39 this.twitter.query = query; 40 } 41 if (this.twitter.interval) { 42 this.twitter.refresh(); 43 } 44 if (callback) { 45 this.twitter.callback = callback; 46 } 47 48 // ..if not, let's initialize. 49 } else { 50 51 // These are the default settings. 52 settings = $.extend({ 53 mode: 'search', // Mode, valid options are: 'search', 'user_timeline', 'list', 'home_timeline' 54 rate: 15000, // Refresh rate in ms 55 limit: 10, // Limit number of results 56 imageSize: 24, // Size of image in pixels 57 refresh: true, 58 timeLinks: true, 59 replies: true, 60 retweets: false, 61 service: false, 62 localization: { 63 seconds: 'seconds ago', 64 minute: 'a minute ago', 65 minutes: 'minutes ago', 66 hour: 'an hour ago', 67 hours: 'hours ago', 68 day: 'a day ago', 69 days: 'days ago' 70 } 71 }, options); 72 // showAuthor should default to true unless mode is 'user_timeline'. 73 if (typeof settings.showAuthor === "undefined") { 74 settings.showAuthor = (settings.mode === 'user_timeline') ? false : true; 75 } 76 77 // Set up a dummy function for the Twitter API callback. 78 if (!window.twitter_callback) { 79 window.twitter_callback = function () { 80 return true; 81 }; 82 } 83 84 this.twitter = { 85 settings: settings, 86 query: query, 87 interval: false, 88 container: this, 89 lastTimeStamp: 0, 90 callback: callback, 91 92 // Convert the time stamp to a more human readable format 93 relativeTime: function (timeString) { 94 var parsedDate = Date.parse(timeString); 95 var delta = (Date.parse(Date()) - parsedDate) / 1000; 96 var r = ''; 97 if (delta < 60) { 98 r = delta + " " + settings.localization.seconds; 99 } else if (delta < 120) { 100 r = settings.localization.minute; 101 } else if (delta < (45 * 60)) { 102 r = (parseInt(delta / 60, 10)).toString() + " " + settings.localization.minutes; 103 } else if (delta < (90 * 60)) { 104 r = settings.localization.hour; 105 } else if (delta < (24 * 60 * 60)) { 106 r = '' + (parseInt(delta / 3600, 10)).toString() + " " + settings.localization.hours; 107 } else if (delta < (48 * 60 * 60)) { 108 r = settings.localization.day; 109 } else { 110 r = (parseInt(delta / 86400, 10)).toString() + " " + settings.localization.days; 111 } 112 return r; 113 }, 114 115 // Update the relative timestamps 116 updateTimestamps: function () { 117 var twitter = this; 118 $(twitter.container).find('span.time').each(function () { 119 var time_element = twitter.settings.timeLinks ? $(this).find('a') : $(this); 120 time_element.html(twitter.relativeTime(this.timeStamp)); 121 }); 122 }, 123 124 apiURL: function () { 125 var params = {}; 126 127 var protocol = (window.location.protocol === 'https:') ? 'https:' : 'http:'; 128 var baseURL = 'api.twitter.com/1/'; 129 var endpoint = ''; 130 131 // Override for Twitter-compatible APIs like Status.net 132 if (this.settings.service) { 133 baseURL = this.settings.service + '/api/'; 134 } 135 136 // Search mode 137 if (this.settings.mode === 'search') { 138 baseURL = (this.settings.service) ? this.settings.service + '/api/' : 'search.twitter.com/'; 139 endpoint = 'search'; 140 params = { 141 q: (this.query && this.query !== '') ? this.query : null, 142 geocode: this.settings.geocode, 143 lang: this.settings.lang, 144 rpp: (this.settings.rpp) ? this.settings.rpp : this.settings.limit 145 }; 146 147 // User/home timeline mode 148 } else if (this.settings.mode === 'user_timeline' || this.settings.mode === 'home_timeline') { 149 endpoint = 'statuses/' + this.settings.mode + '/' + encodeURIComponent(this.query); 150 params = { 151 count: this.settings.limit, 152 include_rts: (this.settings.mode === 'user_timeline' && this.settings.retweets) ? '1' : null, 153 exclude_replies: (!this.settings.replies) ? '1' : null 154 }; 155 156 // Favorites mode 157 } else if (this.settings.mode === 'favorites') { 158 endpoint = 'favorites'; 159 params = { 160 id: encodeURIComponent(this.query) 161 }; 162 163 // List mode 164 } else if (this.settings.mode === 'list') { 165 endpoint = encodeURIComponent(this.query.user) + 166 '/lists/' + 167 encodeURIComponent(this.query.list) + 168 '/statuses'; 169 params = { 170 per_page: this.settings.limit 171 }; 172 } 173 174 // Construct the query string 175 var queryString = []; 176 for (var param in params) { 177 if (params.hasOwnProperty(param) && typeof params[param] !== 'undefined' && params[param] !== null) { 178 queryString[queryString.length] = param + '=' + encodeURIComponent(params[param]); 179 } 180 } 181 queryString = queryString.join("&"); 182 183 // Return the full URL 184 return protocol + '//' + baseURL + endpoint + '.json?' + queryString + '&callback=?'; 185 }, 186 187 // The different APIs will format the results slightly different, 188 // so this method normalizes the tweet object. 189 parseTweet: function (json) { 190 var tweet = { 191 id: (json.id_str) ? json.id_str : json.id, 192 text: json.text, 193 created_at: json.created_at 194 }; 195 196 // Search/regular API differences 197 if (this.settings.mode === 'search') { 198 tweet = $.extend(tweet, { 199 screen_name: json.from_user, 200 profile_image_url: json.profile_image_url 201 }); 202 } else { 203 tweet = $.extend(tweet, { 204 screen_name: json.user.screen_name, 205 profile_image_url: json.user.profile_image_url, 206 created_at: json.created_at.replace(/^(\w+)\s(\w+)\s(\d+)(.*)(\s\d+)$/, "$1, $3 $2$5$4") // Fix for IE 207 }); 208 } 209 210 // Twitter/Status.net 211 if (this.settings.service) { 212 tweet = $.extend(tweet, { 213 url: 'http://' + this.settings.service + '/notice/' + tweet.id, 214 profile_url: 'http://' + this.settings.service + '/' + json.from_user 215 }); 216 if (window.location.protocol === 'https:') { 217 tweet.profile_image_url = tweet.profile_image_url.replace('http:', 'https:'); 218 } 219 220 } else { 221 tweet = $.extend(tweet, { 222 url: 'http://twitter.com/#!/' + tweet.screen_name + '/status/' + tweet.id, 223 profile_url: 'http://twitter.com/#!/' + tweet.screen_name 224 }); 225 // Someday, Twitter will add HTTPS support to twimg.com, but until then 226 // we have to rewrite the profile image urls to the old Amazon S3 urls. 227 if (window.location.protocol === 'https:') { 228 var matches = tweet.profile_image_url.match(/http[s]?:\/\/a[0-9]\.twimg\.com\/(\w+)\/(\w+)\/(.*?)\.(\w+)/i); 229 if (matches) { 230 tweet.profile_image_url = "https://s3.amazonaws.com/twitter_production/" + matches[1] + "/" + matches[2] + "/" + matches[3] + "." + matches[4]; 231 } else { 232 // Failsafe, if profile image url does not match the pattern above 233 // then, at least, change the protocol to HTTPS. 234 // The image may not load, but at least the page stays secure. 235 tweet.profile_image_url = tweet.profile_image_url.replace('http:', 'https:'); 236 } 237 } 238 } 239 240 return tweet; 241 }, 242 243 // Parses the tweet body, linking URLs, #hashtags and @usernames. 244 parseText: function (text) { 245 // URLs 246 text = text.replace(/[A-Za-z]+:\/\/[A-Za-z0-9-_]+\.[A-Za-z0-9-_:%&\?\/.=]+/g, function (m) { 247 return '<a href="' + m + '" rel="external">' + m + '</a>'; 248 }); 249 250 // Twitter 251 if (!this.settings.service) { 252 // @usernames 253 text = text.replace(/@[A-Za-z0-9_]+/g, function (u) { 254 return '<a href="http://twitter.com/#!/' + u.replace(/^@/, '') + '" rel="external">' + u + '</a>'; 255 }); 256 // #hashtags 257 text = text.replace(/#[A-Za-z0-9_\-]+/g, function (u) { 258 return '<a href="http://twitter.com/#!/search?q=' + u.replace(/^#/, '%23') + '" rel="external">' + u + '</a>'; 259 }); 260 261 // Other APIs 262 } else { 263 text = text.replace(/@[A-Za-z0-9_]+/g, function (u) { 264 return '<a href="http://' + settings.service + '/' + u.replace(/^@/, '') + '" rel="external">' + u + '</a>'; 265 }); 266 text = text.replace(/#[A-Za-z0-9_\-]+/g, function (u) { 267 return '<a href="http://' + settings.service + '/search/notice?q?' + u.replace(/^#/, '%23') + '" rel="external">' + u + '</a>'; 268 }); 269 } 270 271 return text; 272 }, 273 274 // Renders a tweet to HTML 275 renderTweet: function (tweet) { 276 var html = '<div class="tweet tweet-' + tweet.id + '">'; 277 278 if (this.settings.showAuthor) { 279 html += '<img width="' + this.settings.imageSize + '" height="' + this.settings.imageSize + '" src="' + tweet.profile_image_url + '" />'; 280 html += '<p class="text"><span class="username"><a href="' + tweet.profile_url + '" rel="external">' + tweet.screen_name + '</a>:</span> '; 281 } else { 282 html += '<p class="text"> '; 283 } 284 285 html += this.parseText(tweet.text); 286 287 if (this.settings.timeLinks) { 288 html += ' <span class="time">'; 289 html += '<a href="' + tweet.url + '" rel="external">'; 290 html += this.relativeTime(tweet.created_at); 291 html += '</a></span>'; 292 } else { 293 html += ' <span class="time">' + this.relativeTime(tweet.created_at) + '</span>'; 294 } 295 296 html += '</p></div>'; 297 298 return html; 299 }, 300 301 // Handle reloading 302 refresh: function (initialize) { 303 var twitter = this; 304 if (twitter.settings.refresh || initialize) { 305 306 $.getJSON(twitter.apiURL(), function (json) { 307 var newTweets = 0; 308 309 // The search and regular APIs differ somewhat 310 var results = (twitter.settings.mode === 'search') ? json.results : json; 311 312 $(results).reverse().each(function () { 313 var tweet = twitter.parseTweet(this); 314 315 // Check if tweets should be filtered 316 if (!twitter.settings.filter || twitter.settings.filter(this)) { 317 // Check if this is actually a new tweet 318 if (Date.parse(tweet.created_at) > twitter.lastTimeStamp) { 319 320 // Insert the HTML 321 $(twitter.container).prepend(twitter.renderTweet(tweet)); 322 323 // Make a note of the timestamp on the first span 324 // so we can update it later. 325 $(twitter.container).find('span.time:first').each(function () { 326 this.timeStamp = tweet.created_at; 327 }); 328 329 // Fade in new tweets unless this is the first load. 330 if (!initialize) { 331 $(twitter.container).find('.tweet-' + tweet.id).hide().fadeIn(); 332 } 333 334 // Remember the last timestamp for the next refresh. 335 twitter.lastTimeStamp = Date.parse(tweet.created_at); 336 337 newTweets += 1; 338 } 339 } 340 }); 341 342 // Did we get any new tweets? 343 if (newTweets > 0) { 344 // Remove old entries exceeding the limit 345 $(twitter.container).find('div.tweet:gt(' + (twitter.settings.limit - 1) + ')').remove(); 346 347 // Run callback 348 if (twitter.callback) { 349 twitter.callback(domNode, newTweets); 350 } 351 352 // Trigger event 353 $(domNode).trigger('tweets'); 354 } 355 }); 356 } 357 }, 358 359 // Start refreshing 360 start: function () { 361 var twitter = this; 362 if (!this.interval) { 363 this.interval = setInterval(function () { 364 twitter.refresh(); 365 }, twitter.settings.rate); 366 this.refresh(true); 367 } 368 }, 369 370 // Stop refreshing 371 stop: function () { 372 if (this.interval) { 373 clearInterval(this.interval); 374 this.interval = false;
375 } 376 }, 377 378 // Clear all tweets 379 clear: function () { 380 $(this.container).find('div.tweet').remove(); 381 this.lastTimeStamp = null; 382 } 383 }; 384 385 var twitter = this.twitter; 386 387 // Update the timestamps in realtime 388 this.timeInterval = setInterval(function () { 389 twitter.updateTimestamps(); 390 }, 5000); 391 392 this.twitter.start(); 393 } 394 }); 395 return this; 396 }; 397})(jQuery);
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.