Large

[OBSOLETE] CatchUp

By Richard Gibson Last update May 12, 2010 — Installed 2,988 times.

There are 11 previous versions of this script.

Add Syntax Highlighting (this will take a few seconds, probably freezing your browser while it works)

// ==UserScript==
// @name           CatchUp
// @namespace      http://userscripts.org/people/336
// @description    CatchUp enhances the Twitter homepage to show ALL your friends with ALL their tweets.
// @source         http://userscripts.org/scripts/show/66998
// @identifier     http://userscripts.org/scripts/source/66998.user.js
// @version        0.4.3
// @date           2010-05-12
// @creator        Richard Gibson <@gmail.com>; http://twitter.com/gibson042
// @include        http://twitter.com/*
// @include        https://twitter.com/*
// ==/UserScript==
// 
// **COPYRIGHT NOTICE**
// 
// I, Richard Gibson, hereby establish my original authorship of this
// work, and announce its release into the public domain.  I claim no
// exclusive copyrights to it, and will neither pursue myself (nor
// condone pursuit by others of) punishment, retribution, or forced
// payment for its full or partial reproduction in any form.
// 
// That being said, I would like to receive credit for this work
// whenever it, or any part thereof, is reproduced or incorporated into
// another creation; and would also like compensation whenever revenue
// is derived from such reproduction or inclusion.  At the very least,
// please let me know if you find this work useful or enjoyable, and
// contact me with any comments or criticisms regarding it.
// 
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
// 
// **END COPYRIGHT NOTICE**
//
//
// Changelog:
// 0.4.3 (2010-05-12)
// 	Improved: Exclude replies to the active user from "Missed Replies"
// 	Improved: Skip the prioritize prompt when no followed users are protected
// 	Improved: Instead of "1 days ago", show "x hours ago" when x <= 36
// 0.4.1 (2010-05-04)
// 	Improved: Exclude replies between followed users from "Missed Replies"
// 	Fixed: Missed Replies recovers from JSON parsing errors
// 0.4 (2010-05-02)
// 	New: #fixreplies from the home page!
// 0.3.1 (2010-03-09)
// 	Improved: @others tab includes @mentions
// 0.3 (2010-03-08)
// 	New: Adds an @others tab to profile pages that shows their replies to others
// 	Improved: Cleaner and more portable auto-update code
// 0.2.2 (2010-02-08)
// 	Improved: Prompt user before trying an authenticated request
// 0.2.1 (2010-02-04)
// 	Fixed: secure/authenticated API requests from HTTP
// 0.2 (2010-02-03)
// 	New: supports all Twitter pages (but only automatic for logged-in user's page)
// 0.1 (2010-01-22)
// 	original release

(function(){

// fundamentals (used to build other constants)
var SCRIPT = {
	 name: "CatchUp"
	,namespace: "http://userscripts.org/people/336"
	,source: "http://userscripts.org"      // script homepage/description URL
			+ "/scripts/show/66998"
	,identifier: "http://userscripts.org"  // script URL
			+ "/scripts/source/66998.user.js"
	,version: "0.4.3"                      // version
	,date: "2010-05-12"                    // update date
};
var UPDATE = {
	 lastCheck: 0
	,script: {}
	,getValue: (typeof(GM_getValue)!='undefined' && GM_getValue) || function(n,d){return d;}
	,setValue: (typeof(GM_setValue)!='undefined' && GM_setValue) || function(n,v){return v;}
	,httpRequest: (typeof(GM_xmlhttpRequest)!='undefined' && GM_xmlhttpRequest) || function(){}
	,init: function(objScript) {
		for (var name in objScript) { if (!(name in this)) {
			this[name] = this.script[name] = this.getValue('_UPDATE_' + name, objScript[name]);
		} }
		this.lastCheck = this.getValue('_UPDATE_' + 'lastCheck', 0);
	}
	,check: function(intCheckDays, strUrl, objScript, fnOnNewer, fnIsNewer, blnForce) {
		this.init(objScript);
		var interval = Math.max(parseFloat(intCheckDays) * 24 * 60 * 60 * 1000, 0) || Infinity;
		var diff = (new Date()) - parseInt(this.lastCheck,10);
		if(!blnForce && !this.isNewer(this, objScript, fnIsNewer) && !(diff > interval)){ return false; }
		if (blnForce || (diff > interval)) {
			var t = this;
			return this.httpRequest({method: 'GET', url: strUrl, onload: function(r){
				t.setValue('_UPDATE_' + 'lastCheck', t.lastCheck = '' + (new Date()).getTime());
				t.parse(r.responseText, [intCheckDays, strUrl, objScript, fnOnNewer, fnIsNewer, false]);
			}});
		}
		try{ fnOnNewer(this, objScript); }catch(x){}
	}
	,parse: function(strResponse, arrCheckArgs) {
		var re = /\/\/\s*(?:@(\S+)\s+(.*?)\s*(?:$|\n)|(==\/UserScript==))/gm, match = true, name;
		while (match && (match = re.exec(strResponse))) {
			if(match[3]){ match = null; continue; }
			name = match[1];
			if(name in this.script){ this.setValue('_UPDATE_' + name, this[name] = match[2]); }
			else if(!(name in this)){ this[name] = match[2]; }
		}
		this.check.apply(this, arrCheckArgs || []);
	}
	,isNewer: function(objUpdate, objScript, fnIsNewer) {
		if(!objUpdate){ objUpdate = this; }
		if(!objScript || (objUpdate.date > objScript.date)){ return true; }
		try {
			return fnIsNewer(objUpdate, objScript);
		}
		catch (x) {
			return (!(objUpdate.date < objScript.date) && (objUpdate.version != objScript.version));
		}
	}
};
var CDATA = function(s){ return '<![CDATA[' + s.replace(/\]\]\>/g, ']]]]><![CDATA[>') + ']]>'; };
var URL_BASE = location.href.replace(/^(.*?:[/]*[^/]*).*$/, '$1');

// constants
var ABORT_TEXT = "Whoops! Something went wrong. Please try again!";	// see twitter.js
var BAD_URL = 'http://example.com:0/';	// should always error
var HTML_AVATAR = '<span class="thumb vcard author"><a href="' + URL_BASE + '/%{screen_name}"'
		+ ' class="tweet-url profile-pic url"><img alt="%{name}" class="photo fn"'
		+ ' height="48" width="48" src="%{profile_image_url}"/></a></span>';
var HTML_MORE = '<a href="%{url}" class="round more" id="more" rel="next">more</a>';
var HTML_SCREEN_NAME = '<strong><a href="' + URL_BASE + '/%{screen_name}"'
		+ ' class="tweet-url screen-name">%{screen_name}</a></strong>';
var INPUT_TIMEOUT = 400;
var KEEP_PROPERTIES = {id:0, name:0, screen_name:0, protected:0, profile_image_url:0, status:0};
var MAX_CATCHUP = 30;
var MAX_RETRIES = 3;
var MAX_SIMULTANEOUS_REQUESTS = 4;
var MISSED_PAGE = 30;
var OTHERS_MINIMUM_LENGTH = 15;
var PREF = {
	 FILTER_HOURS: 'FILTER_HOURS'
	,PROTECTED_OUTLINE: 'PROTECTED_OUTLINE_PIXELS'
	,REPLIES_DAYS: 'REPLIES_DAYS'
	,SEEK_PROTECTED: 'SEEK_PROTECTED'
	,SORT: {toString: function(){return 'SORT';}, FOLLOW_SEQUENCE: 0, ALPHABETIC: 1, LAST_UPDATE: 2}
};
var REQUEST_HEADERS = {	// http://twitter.pbworks.com/API-Docs
	 'X-Twitter-Client': 'CatchUp'
	,'X-Twitter-Client-Version': SCRIPT.version
	,'X-Twitter-Client-URL': 'data:text/xml;charset=utf-8,' + encodeURIComponent([
		 '<?xml version="1.0" encoding="utf-8"?>'
		,'<client>'
			,'<name>' + CDATA(SCRIPT.name) + '</name>'
			,'<version>' + CDATA(SCRIPT.version) + '</version>'
			,'<author>'
					,'<name>Richard Gibson</name>\n<email>richard.gibson+twitter'+'@'+'gmail.com</email>'
					,'<twitter>gibson042</twitter>\n</author>'
			,'<url>' + CDATA(SCRIPT.source) + '</url>'
			//,'<twitter></twitter>'
			,'<description>' + CDATA(
				'CatchUp enhances the Twitter homepage to show ALL your friends with ALL their tweets.'
			) + '</description>'
		,'</client>'
	].join('\n'))
};
var RE_SUFFIXED_URL = /^([^#]*?(?:\/(\w+))?)(?:\/|(#(?:.*[\/#])?))([^\/#]+)(?:##([^?]*))?$/;
var SUFFIXED_URL_PROPERTIES = {url: 0, base: 1, user: 2, suffix: 4, params: 5};	//hashPrefix: 3
var T_SEC = 1000, T_MIN = T_SEC * 60, T_HR = T_MIN * 60, T_DAY = T_HR * 24;
var UPDATE_CHECK_DAYS = 2.5;
	// http://apiwiki.twitter.com/Twitter-REST-API-Method%3A-statuses%C2%A0friends
//var URL_FOLLOWING = 'https://api.twitter.com/1/statuses/friends.json?';
var URL_FOLLOWING = {toString: function(/*objParams, ...*/){
	var base = 'https://twitter.com/statuses/friends.json', params = [];
	for (var a = Array.prototype.slice.call(arguments), i = 0; i < a.length; i++) {
		for(var p in a[i]){ params.push(p + (a[i][p] !== undefined ? '=' + a[i][p] : '')); }
	}
	return base + (params.length > 0 ? '?' + params.join('&') : '');
}};
//var URL_STATUSES = 'https://api.twitter.com/1/statuses/user_timeline.json?';
var URL_STATUSES = {
	toString: function(){
		var base = ['https://twitter.com/', ''], params = ['twttr=true'];
		try{ params.concat(g$.param(g$.ajaxSettings.data) || []); }catch(x){}
		for (var a = Array.prototype.slice.call(arguments), i = 0; i < a.length; i++) {
			for (var p in a[i]) {
				if(p == 'screen_name'){ base[1] = a[i][p] || ''; }
				else{ params.push(a[i][p] === undefined ? p : [p, a[i][p]].join('=')); }
			}
		}
		return base.join('') + (params.length > 0 ? '?' + params.join('&') : '');
	}
	,getParams: function(strUrl){
		var params = {screen_name: strUrl.replace(/^(https?:\/\/twitter\.com)?\/|\?.*$/g, '')};
		var reParam = /([^=]+)=?(.*)/;
		for (var q = strUrl.replace(/.*?\?/,'').split('&'), i = 0, match; i < q.length; i++) {
			match = reParam.exec(q[i]) || {};
			if(match[1] && match[1] != 'twttr'){ params[match[1]] = match[2]; }
		}
		return params;
	}
};

// derived constants and localized text
var FOLLOWING_PROTECTED_PROMPT = 'Would you like ' + SCRIPT.name
		+ ' to try getting status updates from protected users (requires authentication)?';
var MISSED_LIMIT_TEXT = SCRIPT.name + ' is limited to ' + MAX_CATCHUP + ' users.';
var MISSED_LIMIT_PROMPT = MISSED_LIMIT_TEXT + ' Would you like to prioritize protected users?';
var IMG_LOADING = [	// http://ajaxload.info/cache/FF/FF/FF/00/00/00/19-1.gif
	 'data:image/gif;base64,R0lGODlhZABkAPQAAP%2F%2F%2FwAAAI6OjmhoaDY2Njw8PFxcXBwcHBI'
	,'SEiwsLFRUVExMTH5%2BfoaGhiQkJAAAAHZ2dkRERAoKCgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA'
	,'ACH%2FC05FVFNDQVBFMi4wAwEAAAAh%2FhpDcmVhdGVkIHdpdGggYWpheGxvYWQuaW5mbwAh%2BQQJBwAAACwAAAAAZABkA'
	,'AAF%2FyAgjmRpnmiqrmzrvnAsz3Rt33iu73zfMgoDw0csAgSEh%2FJBEBifucRymYBaaYzpdHjtuhba5cJLXoHDj3HZBykk'
	,'IpDWAP0YrHsDiV5faB3CB3c8EHuFdisNDlMHTi4NEI2CJwWFewQuAwtBMAIKQZGSJAmVelVGEAaeXKEkEaQSpkUNngYNrCW'
	,'EpIdGj6C3IpSFfb%2BCAwkOCbvEy8zNzs%2FQ0dLT1NUrAgOf1kUMBwjfB8rbOQLe3%2BC24wxCNwPn7wrjEAv0qzMK7%2B'
	,'eX2wb0mzXu8iGIty1TPRvlBKazJgBVnBsN8okbRy6VgoUUM2rcyLGjx48gQ4ocSbKkyZMoJf8JMFCAwAJfKU0gOUDzgAOYH'
	,'iE8XDGAJoKaalAoObHERFESU0oMFbF06YikKQQsiKCJBYGaNR2ocPr0AQCuQ8F6Fdt1rNeuLSBQjRDB3qSfPm1uPYvUbN2j'
	,'TO2izQs171e6J9SuxXjCAFaaQYkC9ku2MWCnYR2rkDqV4IoEWG%2FO5fp3ceS7nuk2Db0YBQS3UVm6xBmztevXsGPLnk27t'
	,'u3buHOvQU3bgIPflscJ4C3D92%2FgFNUWgHPj2G%2BbmhkWWL78xvPjDog%2FazCdOmsXzrF%2FdyYgAvUI7Y7bDF5N%2BQ'
	,'LCM4whM7BxvO77%2BPPr38%2B%2F%2Fw4GbhSw0xMQDKCdJAwkcIx2ggMSsQABENLHzALILDhMERAQ0BKE8IUSwYILPjEAh'
	,'CQ2yMoCClaYmA8NQLhhh5I0oOCCB5rAQI0mGEDiRLfMQhWOI3CXgIYwotBAA%2FaN09KQCVw4m4wEMElAkTEhIWUCSaL0IJ'
	,'PsySZVlC%2F5J%2BaYZJZppgghAAAh%2BQQJBwAAACwAAAAAZABkAAAF%2FyAgjmRpnmiqrmzrvnAsz3Rt33iu73zfMhAIw'
	,'0csAgQDhESCGAiM0NzgsawOolgaQ1ldIobZsAvS7ULE6BW5vDynfUiFsyVgL58rwQLxOCzeKwwHCIQHYCsLbH95Dg%2BOjg'
	,'eAKAKDhIUNLA2JVQt4KhGPoYuSJEmWlgYuSBCYLRKhjwikJQqnlgpFsKGzJAa2hLhEuo6yvCKUv549BcOjxgOVhFdFdbAOy'
	,'sYNCgQK2HDMVAXexuTl5ufo6err7O3kAgKs4%2B48AhEH%2BATz9Dj2%2BP8EWvET0YDBPlX%2FEh7i18CAgm42ICT8l2og'
	,'AAYPFSyU0WAiPjcDtSkwIHCGAAITE%2F%2BUpCeg4EqTKPGptEikpQEGL2nq3Mmzp8%2BfQIMKHUq0qNGjSJO6E8DA4Ryle'
	,'Qw4mOqgk1F4LRo4OEDVwTQUjk48MjGWxC6zD0aEBbBWbdlJBhYsAJlC6lSuDiKoaOuWbdq%2BfMMG%2Fus37eCsCuRaVWG3'
	,'q94UfEUIJlz48GHJsND6VaFJ8UEAWrdS%2FSqWMubNgClP1nz67ebIJQTEnduicdWDZ92aXq17N%2BG1kV2nwEqnqYGnUJM'
	,'rX868ufPn0KNLn069Or%2BN0hksSFCArkWmORgkcJCgvHeWCiIYOB9jAfnx3D%2BfE5A%2BwoKKNSLAh4%2BdXYMI9gEonw'
	,'oKlPeeON8ZAOCgfTc0UB5%2FOiERwQA5xaCJff3xM6B1HHbo4YcghigiNXFBhEVLGc5yEgEJEKBPFBBEUEAE7M0yAIs44le'
	,'TjDNGUKEkBrQopDM%2BNFDAjEf%2BCMiNQhJAWpE8zqjkG%2F8JGcGGIjCQIgoMyOhjOkwNMMCWJTTkInJZNYAlPQYU4KKT'
	,'0xnpopsFTKmUPW8ScOV0N7oJ53TxJAbBmiMWauihiIIYAgAh%2BQQJBwAAACwAAAAAZABkAAAF%2FyAgjmRpnmiqrmzrvnA'
	,'sz3Rt33iu73zv%2F8AZo4BAFBjBpI5xKBYPSKWURnA6CdNszGrVeltc5zcoYDReiXDCBSkQCpDxShA52AuCFoQribMKEoGB'
	,'A3IpdQh2B1h6TQgOfisDgpOQhSMNiYkIZy4CnC0Ek4IFliVMmnYGQAmigWull5mJUT6srRGwJESZrz%2BSrZWwAgSJDp8%2'
	,'FgJOkuaYKwUADCQ4JhMzW19jZ2tvc3d7f4NoCCwgPCAs4AwQODqrhIgIOD%2FPzBzYDDgfsDgrvAAX0AqKjIW0fuzzhJASk'
	,'56CGwXwOaH1bGLBGQX0H31Gch6CGgYf93gGkOJCGgYIh3%2F8JUBjQHg6J%2FgSMlBABob%2BbOHPq3Mmzp8%2BfQIMKHUq'
	,'0qNEUAiBAOHZ0RYN10p41PZGg6jQHNk%2FM07q1BD2vX0l0BdB1rIiKKhgoMMD0BANpVqmpMHv2AVm7I7aa1Yu3bl6%2BYv'
	,'uuUEDYXdq40qqhoHu38d%2Bwfvf2pRjYcYq1a0FNg5vVBGPAfy03lhwa8mjBJxqs7Yzi6WapgemaPh0b9diythnjSAqB9dT'
	,'fwIMLH068uPHjyJMrX84cnIABCwz4Hj4uAYEEeHIOMAAbhjrr1lO%2Bg65gQXcX0a5fL%2FnOwIL3imlAUG%2Fd8DsI7xfA'
	,'lEFH%2FSKcEAywHw3b9dbcgQgmqOByggw26KAIDAxwnnAGEGAhe0AIoEAE0mXzlBsWTojDhhFwmE0bFroR3w8RLNAiLtg8Z'
	,'aGFbfVgwIv2WaOOGzn%2BIIABCqx4TRk1pkXYgMQNUUAERyhnwJIFFNAjcTdGaWJydCxZ03INBFjkg2CGKeaYCYYAACH5BA'
	,'kHAAAALAAAAABkAGQAAAX%2FICCOZGmeaKqubOu%2BcCzPdG3feK7vfO%2F%2FwBnDUCAMBMGkTkA4OA8EpHJKMzyfBqo2V'
	,'kBcEYWtuNW8HsJjoIDReC2e3kPEJRgojulVPeFIGKQrEGYOgCoMBwiJBwx5KQMOkJBZLQILkAuFKQ2IiYqZjQANfA4HkAlt'
	,'dKgtBp2tA6AlDJGzjD8KrZ0KsCSipJCltT63uAiTuyIGsw66asQHn6ACCpEKqj8DrQevxyVr0D4NCgTV3OXm5%2Bjp6uvs7'
	,'e7v6gIQEQkFEDgNCxELwfACBRICBtxGQ1QCPgn6uRsgsOE9GgoQ8inwLV2ChgLRzKCHsI9Cdg4wBkxQw9LBPhTh%2FwG4KH'
	,'IODQYnDz6Ex1DkTCEL6t189w%2BjRhsf%2FQ04WACPyqNIkypdyrSp06dQo0qdSrWqVUcL%2BNER0MAa1AYOHoh9kKCiiEo'
	,'E6nl1emDsWAIrcqYlkDKF2BNjTeQl4bbEXRF%2F%2F47oe8KABLdjg4qAOTcBAcWAH%2BiVLBjA3cqXJQ%2FWbDkzX84oFC'
	,'Aey%2BwEg8Zp136e3Pnz3sitN28mDLsyiQWjxRo7EaFxXRS2W2OmDNqz7NrDY5swkPsB5FC91a6gHRm08OKvYWu3nd1EW8R'
	,'w9XA1q1TAd7Flr76wo1W9%2B%2Ffw48ufT7%2B%2B%2Ffv48%2Bs%2FwXUABPLwCWAAAQRiolQD%2F%2BFDIKRdBOz0TjgK'
	,'kGNDAwsSSJBKEESowHOUEFjEY0lJEyGAegyw4G5HNcAAiS0g2ACL%2B8Uo44w01mjjjTi%2BwMCKMs5TQAQO%2BiCPAQme0'
	,'0AEP%2F4IIw0DZLVAkLA0kGQBBajGQ5MLKIDiMUcmGYGVO0CQZXvnCIAkkFOsYQCH0XQVAwP%2BsRlgVvssadU8%2B6Cp3z'
	,'z66JmfNBFE8EeMKrqZ46GIJqrooi6EAAAh%2BQQJBwAAACwAAAAAZABkAAAF%2FyAgjmRpnmiqrmzrvnAsz3Rt33iu73zv%'
	,'2F0Baw2BoBI88g2N5MCCfNgZz6WBArzEl1dHEeluGw9Sh%2BJpTg%2B1y8GpABGdWQxFZWF0L7nLhEhAOgBFwcScNCYcOCX'
	,'ctAwsRbC5%2FgIGEJwuIh3xADJOdg5UjEQmJowlBYZ2AEKAkeZgFQZypB0asIgyYCatBCakEtiQMBQkFu0GGkwSfwGYQBov'
	,'M0dLT1NXW19jZ2ts%2BAgYKA8s0As6Q3AADBwjrB9AzogkEytwN6uvs4jAQ8fxO2wr3ApqTMYAfgQSatBEIeK8MjQEHIzrU'
	,'BpAhgoEyIkSct62BxQP5YAhoZCDktQEB2%2F%2Bd66ZAQZGVMGPKnEmzps2bOHPq3Mmzp88v5Iz9ZLFAgtGLjCIU8IezqFG'
	,'jDzCagCBPntQSDx6cyKoVa1avX0mEBRB2rAiuXU00eMoWwQoF8grIW2H2rFazX%2FHeTUs2Lde%2BYvmegMCWrVATC%2BRW'
	,'pSsYsN6%2FI%2FLyHYtWL%2BATAwo%2FPVyCatWrgU1IDm3Zst2%2Bk%2FeiEKBZgtsVA5SGY1wXcmTVt2v77aq7cSvNoIe'
	,'OcOo6uPARAhhwPs68ufPn0KNLn069uvXrfQpklSAoRwOT1lhXdgC%2BBQSlEZZb0175QcJ3Sgt039Y%2B6%2BsZDQrI119L'
	,'W%2F26MUQQ33zaSFDfATY0kFh2euewV9l748AkwAGVITidAAA9gACE2HXo4YcghijiiN0YEIEC5e3QAAP9RWOiIxMd0xKK0'
	,'zhSRwRPMNCSAepVYoCNTMnoUopxNDLbEysSuVIDLVLXyALGMSfAAgsosICSP01J5ZXWQUBlj89hSeKYZJZpJoghAAAh%2BQ'
	,'QJBwAAACwAAAAAZABkAAAF%2FyAgjmRpnmiqrmzrvnAsz3Rt33iu73zv%2F0Bag8FoBI%2B8RmKZMCKfNQbTkSAIoNgYZEl'
	,'NOBjZcGtLLUPE6JSg601cXQ3IO60SQAzyF9l7bgkMbQNzdCUCC1UJEWAuAgOCLwYOkpIDhCdbBIiVQFIOB5IHVpYlBpmmC0'
	,'EMk6t9oyIDplUGqZ%2Bek06uAAwEpqJBCqsOs7kjDAYLCoM%2FDQa1ycSEEBCL0NXW19jZ2tvc3d7fPwJDAsoz4hC44AIFB'
	,'%2B0R5TGwvAbw2Q0E7fnvNQIEBbwEqHVj0A5BvgPpYtzj9W%2BTNwUHDR4QqBAgr1bdIBzMlzCGgX8EFtTD1sBTPgQFRv%2'
	,'F6YTAgDzgAJfP5eslDAAMFDTrS3Mmzp8%2BfQIMKHUq0qNGjSJMisYNR6YotCBAE9GPAgE6fEKJqnbiiQYQCYCmaePDgBNm'
	,'yJc6mVUuC7Ai3AOC%2BZWuipAStUQusGFDgawQFK%2BTOjYtWhFvBhwsTnlsWseITDfDibVoCAtivgFUINtxY8VnHiwdz%2'
	,'Fty2MwoBkrVSJtEAbNjAjxeDnu25cOLaoU2sSa236wCrKglvpss5t%2FDHcuEO31z57laxTisniErganQSNldf3869u%2Ff'
	,'v4MOLH0%2B%2BvHk%2FA5YQeISjQfBr6yTIl5%2FSxp2%2F76sNmM9fuwsDESyAHzgJ8DdfbzN4JWCkBBFYd40DBsqXgA0D'
	,'MIhMfsQUGGEENjRQIR4v7Rehfy9gWE18%2FDkEnh0RJELieTDGKOOMNAa1DlkS1Bceap894ICJUNjhCJAyFNAjWahAA8ECT'
	,'Krow5FkIVDNMcgMAwSUzFnCAJMLvHiDBFBKWQ1LLgERAZRJBpVTiQ70eMBQDSigAHSnLYCAj2kCJYCcBjwz3h98EnkUM1ad'
	,'J2iNiCaq6KKLhgAAIfkECQcAAAAsAAAAAGQAZAAABf8gII5kaZ5oqq5s675wLM90bd94ru987%2F%2FAoHAYEywShIWAyKw'
	,'tCMjEokmFCaJQwrLKVTWy0UZ3jCqAC%2BSfoCF%2BNQrIQrvFWEQU87RpQOgbYg0MMAwJDoUEeXoiX2Z9iT0LhgmTU4okEH'
	,'0EZgNCk4WFEZYkX5kEEEJwhoaVoiIGmklDEJOSgq0jDAOnRBBwBba3wcLDxMXGx8jJysvMzUJbzgAGn7s2DQsFEdXLCg4HD'
	,'t6cNhHZ2dDJAuDqhtbkBe%2BPxgze4N8ON%2BTu58jp6%2BA3DPJtU9aNnoM%2FOBrs4wYuAcJoPYBBnEixosWLGDNq3Mix'
	,'o8ePIEOKxGHEjIGFKBj%2FDLyY7oDLA1pYKIgQQcmKBw9O4MxZYmdPnyRwjhAKgOhQoCcWvDyA4IC4FAHtaLvJM2hOo0WvV'
	,'s3K9ehRrVZZeFsKc0UDmnZW%2FjQhFOtOt2C9ingLt%2BuJsU1dolmhwI5NFVjnxhVsl2tdwkgNby0RgSyCpyogqGWbOOvi'
	,'tlvfriVc2LKKli9jjkRhRNPJ0ahTq17NurXr17Bjy55NG0UDBQpOvx6AoHdTiTQgGICsrIFv3wdQvoCwoC9xZAqO%2B34Ow'
	,'0DfBQ%2BVEZDeW4GNOgsWTC4WnTv1QQaAJ2vA9Hhy1wPaN42XWoD1Acpr69%2FPv79%2FZgN8ch5qBUhgoIF7BSMAfAT07T'
	,'DAgRCON8ZtuDWYQwIQHpigKAzgpoCEOGCYoQQJKGidARaaYB12LhAwogShKMhAiqMc8JYDNELwIojJ2EjXAS0UCOGAywxA1'
	,'05EjgBBBAlMZdECR%2BLESmpQRjklagxE%2BYB6oyVwZImtCUDAW6K51mF6%2F6Wp5po2hAAAIfkECQcAAAAsAAAAAGQAZA'
	,'AABf8gII5kaZ5oqq5s675wLM90bd94ru987%2F%2FAoHAYE0AWC4iAyKwNCFDCoEmFCSJRQmRZ7aoaBWi40PCaUc%2Fo9Ow'
	,'TNMqvhiE84LYYg4GSnWpEChEQMQ0MVlgJWnZ8I36AgHBAT4iIa4uMjo9CC5MECZWWAI2Oij4GnaefoEcFBYVCAlCIBK6gIw'
	,'wNpEACCgsGubXAwcLDxMXGx8jJysvMZ7%2FKDAsRC5A1DQO9z8YMCQ4J39UzBhHTCtrDAgXf3gkKNg3S0hHhx9zs3hE3BvL'
	,'mzOnd6xbcYDCuXzMI677RenfOGAR1CxY26yFxosWLGDNq3Mixo8ePIEOKHEmyZDEBAwz%2FGGDQcISAlhMFLHBwwIEDXyyO'
	,'ZFvx4MGJnj5LABU6lETPEUcBJEVa9MQAm1Ad0CshE4mCqUaDZlWqlatXpl9FLB26NGyKCFBr3lyxCwk1nl3F%2BiwLlO7cr'
	,'mPr4r17NqpNAzkXKMCpoqxcs0ftItaaWLFhEk9p2jyAlSrMukTjNs5qOO9hzipkRiVsMgXKwSxLq17NurXr17Bjy55Nu7Zt'
	,'IoRWwizZIMGB3wR2f4FQuVjv38gLCD8hR8HVg78RIEdQnAUD5woqHjMgPfpv7S92Oa8ujAHy8%2BTZ3prYgED331tkp0Mef'
	,'7YbJctv69%2FPv7%2F%2FHOlI0JNyQ%2BxCwHPACOCAmV4S5AfDAAhEKF0qfCyg14BANCChhAc4CAQCFz6mgwIbSggYKCGK'
	,'mAOJJSLgDiggXiiBC9cQ5wJ3LVJ4hoUX5rMCPBIEKcFbPx5QYofAHKAXkissIKSQArGgIYfgsaGAki62JMCTT8J0Wh0cQcC'
	,'lkIK8JuaYEpTpGgMIjIlAlSYNMKaOq6HUpgQIgDkbAxBAAOd%2FgAYqKA0hAAAh%2BQQJBwAAACwAAAAAZABkAAAF%2FyAg'
	,'jmRpnmiqrmzrvnAsz3Rt33iu73zv%2F8CgcChrQAYNotImiBQKi%2BRyCjM4nwOqtmV4Og3bcIpRuDLEaBNDoTjDGg1BWmV'
	,'QGORDA2GfnZusCxFgQg17BAUEUn4jEYGNQwOHhhCLJFYREQpDEIZ7ipUCVgqfQAt7BYOVYkduqq6vsLGys7S1tre4ubq7Uw'
	,'IDBn04DAOUuwJ7CQQReDUMC8%2FFuXrJydE0Bs92uwvUBAnBNM7P4LcK3ufkMxDAvMfnBbw9oQsDzPH3%2BPn6%2B%2Fz9%'
	,'2Fv8AAwocSLCgwYO9IECwh9AEBAcJHCRq0aAOqRMPHmDMaCKjRhIeP47gKIIkyZEeU%2F8IgMiSABc2mlacRAlgJkebGnGi'
	,'zCmyZk8UAxIIHdoqRR02LGaW5AkyZFOfT5c6pamURFCWES%2BaCGWgKIqqN3uGfapzqU%2BxTFEIiChUYo%2BpO0uM3fnzp'
	,'Mm6VUs8jDixoVoIDBj6HUy4sOHDiBMrXsy4sWMSTSRkLCD4ltcZK0M%2BQFB5lgIHEFPNWKB5cq7PDg6AFh0DQem8sVaCBn'
	,'0gQY3XsGExSD0bdI0DryXgks0bYg3SpeHhQj07HQzgIR10lmWAr%2FMYC1wjWDD9sffv4MOLR3j1m5J1l%2F0UkMCevXIgD'
	,'RIcQHCAQHctENrrv55D%2FoH%2FB7ynnn7t2fYDAwD%2BR59zVmEkQCB7BvqgQIIAphdGBA9K4JILcbzQAID0%2FcfgFvk9'
	,'aE0KDyFA34kp%2BAdgBK4MQKCAKEqg4o0sniBAAQBS9goEESQQQY4nJHDjjRGy0EBg%2FRx55GFO3ngYAVFuWBiCRx4w4kE'
	,'NFKBiAVuOJ%2BaYZIoZAgAh%2BQQJBwAAACwAAAAAZABkAAAF%2FyAgjmRpnmiqrmzrvnAsz3Rt33iu73zv%2F8CgcChrMB'
	,'oNotImUCwiiuRyCoNErhEIdduCPJ9arhgleEYWgrHaxIBAGDFkep1iGBhzobUQkdJLDAtOYUENEXx8fn8iBguOBkMNiImLJ'
	,'F6CA0MCBYh9lSMCEAYQikAMnBFwn2MCRquvsLGys7S1tre4ubq7vDqtpL5HvAIGBMYDeTTECgrJtwwEBcYEzjIMzKO7A9PG'
	,'pUUGzN61EMbSBOIxoei0ZdOQvTuhAw3V8Pb3%2BPn6%2B%2Fz9%2Fv8AAwocSBCQo0wFUwhI8KDhgwPrerUSUK8EAYcOD%2'
	,'FCTRCABGhUMMGJ8d6JhSZMlHP%2BmVEkCJQCULkVgVFggQUcCC1QoEOlQQYqYMh%2B8FDrCZEyjRIMWRdoyaZ2bNhOoOmGA'
	,'Z8OcKIAO3bqUpdKjSXk25XqiQdSb60JaJWlCK9OlZLeChetVrtMSm85iTXFRpMafdYfefRsUqEuYg7WWkGTTk4qFGB1EHEa'
	,'vIpuDCTNr3sy5s%2BfPoEOLHk063YCaCZD1mlpjk4TXrwtYjgWh5gLWMiDA3o3wFoQECRwExw2jwG7YCXDlFS58r4wEx187'
	,'wMUgOHDgEWpEiC4h%2Ba281h34pKE7em9b1YUDn7xiwHHZugKdYc%2FCSoIss0vr38%2B%2Fv%2F%2FRTRAQhRIC4AHLAAc'
	,'goCCkAuf50IACDkTYzCcCJLiggvTRAKEDB0TIFh0GXLjgeD4wwGGEESaQIREKiKggiT2YiOKJxI0xgIsIfKgCPS%2BYFWGH'
	,'wq2oiYULHpCfCFZE%2BFELBszoQIN0NEDkATWaIACHB2TpwJEAEGOdaqsIMIACYLKwQJZoHuDcCkZweUsBaCKQJQGfEZBml'
	,'gV8ZkCCceqYWXVpUgOamNEYIOR%2FiCaq6KIAhAAAIfkECQcAAAAsAAAAAGQAZAAABf8gII5kaZ5oqq5s675wLM90bd94ru'
	,'987%2F%2FAoHBIExCPOMhiAUE6ZYLl0vissqJSqnWLGiwUA64Y1WiMfwKGmSgwgM%2BotsKwFhoWkYgBbmIo%2FgxEeXgLf'
	,'CUNfwp1QQp4eoaHakdRelqQl5iZmpucnZ6foKGioz8LCA8IC5akOAcPr68Oq6CzMguwuAWjEBEFC4syDriwEqICvcg2w7ii'
	,'DQXPBRHAMKfLD8bR0RE2t8u6ogzPEU01AsK4ErWdAtMzxxKvBeqs9PX29%2Fj5%2Bvv8%2Ff7%2FAAMKNAEBwryBJAYgkMC'
	,'wEMIUAxhKlOBQn4AB0cKsWDiRYTsRr07AMjGSBDOT10D%2FpgyJkmUXAjAJkEMBoaPEmSRTogTgkue1niGB6hwptAXMAgR8'
	,'qahpU4JGkTpHBI06bGdRlSdV%2BlQRE6aCjU3n9dRatCzVoT%2FNqjCAFCbOExE7VoQ6tqTUtC2jbtW6967eE2wjPFWhUOL'
	,'chzQNIl7MuLHjx5AjS55MubJlGQ3cKDj4kMEBBKARDKZ1ZwDnFQI%2Bhwb9UZMAAglgb6uhcDXor6EUwN49GoYC26AJiFoQ'
	,'u3jvF7Vt4wZloDjstzBS2z7QWtPuBKpseA594LinAQYU37g45%2FTl8%2BjTq19fmUF4yq8PfE5QPQeEAgkKBLpUQL7%2FB'
	,'EJAkMCADiSwHx8NyIeAfH8IHOgDfgUm4MBhY0Dg34V7ACEhgQnMxocACyoon4M9EBfhhJdEcOEBwrkwQAQLeHcCAwNKSEB9'
	,'VRzjHwHmAbCAA0Ci6AIDeCjiGgQ4jjBAkAcAKSNCCgQZ5HKOGQBkk0Bm%2BBgDUjZJYmMGYOmAlpFlRgd7aKap5poyhAAAI'
	,'fkECQcAAAAsAAAAAGQAZAAABf8gII5kaZ5oqq5s675wLM90bd94ru987%2F%2FAoHBIExCPOIHB0EA6ZUqFwmB8WlkCqbR6'
	,'9S0cD8SCy2JMGd3f4cFmO8irRjPdW7TvEaEAYkDTTwh3bRJCEAoLC35%2FJIJ3QgaICwaLJYGND0IDkRCUJHaNBXoDAxBwl'
	,'Gt3EqadRwIFEmwFq6y0tba3uLm6u7y9viYQEQkFpb8%2FAxLJybLGI7MwEMrSA81KEQNzNK%2FSyQnGWQsREZM1CdzJDsYN'
	,'4RHh2TIR5xLev1nt4zbR59TqCuOcNVxxY1btXcABBBIkGPCsmcOHECNKnEixosWLGDNq3MjxCIRiHV0wIIAAQQKAIVX%2FM'
	,'DhQsqQElBUFNFCAjUWBli0dGGSEyUQbn2xKOOI5IigAo0V%2FpmBQIEIBgigg4MS5MynQoz1FBEWKtatVrVuzel2h4GlTfl'
	,'GntnzGFexYrErdckXaiGjbEv6aEltxc%2BqbFHfD2hUr%2BGvXuIfFmmD6NEJVEg1Y4oQJtC3ixDwtZzWqWfGJBksajmhA0'
	,'iTllCk%2BikbNurXr17Bjy55Nu7bt20HkKGCwOiWDBAeC63S4B1vvFAIIBF%2Be4DEuAQsISCdHI%2FLy5ad1QZBeQLrzMs'
	,'sRLFdgDKF0AgUUybB%2B%2FYB6XiO7Sz9%2BQkAE8cEREPh%2By8B5hjbYtxxU6kDQAH3I7XEgnG4MNujggxBGCAVvt2Xhw'
	,'IUK8JfEIX3YYsCFB2CoRwEJJEQAgkM0ANyFLL7HgwElxphdGhCwCKIDLu4QXYwEUEeJAAnc6EACOeowAI8n1TKAjQ74uIIA'
	,'o9Bnn4kRoDgElEEmQIULNWY54wkMjAKSLQq%2BIMCQQwZp5UVdZpnkbBC4OeSXqCXnJpG1qahQc7c1wAADGkoo6KCEFrpCC'
	,'AA7AAAAAAAAAAAA'
].join('');

// global variables
var g$;
var g_;
var garrAjaxInterceptors = [];
var gblnGotFollowing = false;
var garrFollowing = [];
var gmapFollowing = {};
var gelFollowing;
var gintFilterHours = 0;
var gstrLocationNoHash = location.href.replace(/#.*$/, '');
var garrMissed = [];
var gstrPageUser;
var gblnPageUserInUrl;
var gjqOthersTab;
var gblnReload = false;
var gintPromptForProtected = 1;
var gintProtectedOutline = 2;
var gintRepliesDays = 1;
var gintSequence = 0;
var gstrSessionUser;
var gjqSide;
var gintSort = PREF.SORT.FOLLOW_SEQUENCE;
var gobjUnsafeWindow = (function(){try{return unsafeWindow;}
		catch(x){return (function(){return this;})();}})();
var gblnUser404;

// "real" functions
function init () {
	var then = new Date();
	gintFilterHours = getPersistentValue(PREF.FILTER_HOURS, gintFilterHours);
	gintPromptForProtected = getPersistentValue(PREF.SEEK_PROTECTED, gintPromptForProtected);
	gintProtectedOutline = getPersistentValue(PREF.PROTECTED_OUTLINE, gintProtectedOutline);
	gintRepliesDays = getPersistentValue(PREF.REPLIES_DAYS, gintRepliesDays);
	gintSort = getPersistentValue(PREF.SORT, gintSort);
	gstrSessionUser = getSessionUser();
	gstrPageUser = getPageUser();
	gblnPageUserInUrl = (new RegExp(
		'^\\w+:[/]*.*?[/]' + (gstrPageUser || '$.') + '([/]|$)', 'i'
	)).test(gstrLocationNoHash);
	gblnUser404 = !gstrPageUser && /^\w+:[/]*.*?[/].+?([/]|$)/.test(gstrLocationNoHash);

	// user settings
	registerMenuCommand(SCRIPT.name + ": handle protected users", function(){
		setPersistentValue(PREF.SEEK_PROTECTED, gintPromptForProtected =
				(confirm(FOLLOWING_PROTECTED_PROMPT) ? 0 : 1));
	});
	registerMenuCommand(SCRIPT.name + ": protected user outline width", function(){
		var strInput = prompt('Protected users outline width (pixels):', gintProtectedOutline);
		strInput = (strInput !== null) && parseInt(strInput || 0, 10);
		if (strInput || strInput === 0) {
			setPersistentValue(PREF.PROTECTED_OUTLINE, gintProtectedOutline = strInput);
		}
	});

	// add new tabs (must happen early for potential |replacePage|)
	var _obj = {user: gstrPageUser};
	if (!addTab('@others'
		,{title: defer_('Replies by @%{user}', _obj), heading: defer_('Replies by @%{user}', _obj)}
		,ajaxOthers
		,{
			 htmlLabel: ['@', $E('i',{},'others')]
			,htmlTitle: defer_('Replies to others by @%{user}', _obj)
			,precedingTabSelector: '#replies_tab, #profile_tab'
			,onReload: init
		}
	)){ return; }
	if (((gstrPageUser && gstrSessionUser == gstrPageUser) || gblnUser404) && !addMissedTab()) {
		return;
	}

	// gobjUnsafeWindow.jQuery
	executeOnCondition(
		function(){ return (gobjUnsafeWindow.jQuery || false); }	// satisfied/continue
		,function(){
			g$ = gobjUnsafeWindow.jQuery;
			g_ = gobjUnsafeWindow._ || replaceParams;
			gjqSide = g$('#side_base');
			interceptAjax();
		}
		,null
		,50
	);

	// try to show updates under #following list for 1 minute (2 second intervals)
	executeOnCondition(function(){	// #following_list found
		gelFollowing = gelFollowing || document.getElementById('following_list');
		if(gelFollowing){ return true; }	// satisfied
		if(((new Date()) - then) <= T_MIN){ return false; }	// continue
		return null;	// abort
	}, null, showUpdate, T_SEC * 2);

	if (gstrPageUser) {
		// show all followers for the page user
		executeOnCondition(
			function(){	// found #following_list
				gelFollowing = gelFollowing || document.getElementById('following_list');
				return (gelFollowing || false);	// satisfied/continue
			}
			,function(){
				showUpdate();
				if(gstrSessionUser == gstrPageUser){ return activateFollowing(); }
	
				var elActivate = gelFollowing.parentNode.insertBefore(
						$E('button', {style:'display:block;margin:auto;padding:0;font-size:small;'}, 'CatchUp'),
						gelFollowing);
				elActivate.addEventListener('click', function(){
					activateFollowing();
					elActivate.parentNode.removeChild(elActivate);
				}, true);
			}
			,null
			,T_SEC * 2
		);
	}

	// global styles
	addPageStyle(['body[id="@others"] ol.statuses li { padding-left: 0.5em; }',
			'body[id="@others"] ol.statuses span.status-body { margin-left: 0; }'].join('\n'));
}

function ajaxOthers (objUrlParts, fnAjax, objSettings) {
	var fnSuccess = objSettings.success;
	objSettings.url = objUrlParts.base + (objUrlParts.user == gstrPageUser ? '' : '/' + gstrPageUser);
	objSettings.success = function(objData){
		var processed = processOthers(objData, objUrlParts.suffix);
		arguments[0] = processed.data;
		var r = fnSuccess.apply(this, arguments);
		if(processed.listener){ setTimeout(processed.listener,50); }
		return r;
	};
	fnAjax.call(this, objSettings);
}

function processOthers (objData, strSuffix) {
	var countIn = 0, countOut, fnListener = undefined;

	if (objData['#pagination']) {
		objData['#pagination'] = objData['#pagination'].replace(/href="(.*?)"/g,
				'href="$1#' + strSuffix + '"');
	}

	if (objData['#timeline']) {
		var timeline = g$(objData['#timeline']);
		countIn = timeline.find(".hentry").length;
		timeline = timeline.find(".hentry")	// statuses
			.filter(isNotReply)	// non-replies
				.remove()
			.end()	// back to statuses
		.end();	// back to timeline
		countOut = timeline.find(".hentry").length;
		objData['#timeline'] = g$('<div/>').append(timeline).html();
	}

	fnListener = function(){
		updateLocation(strSuffix, true);
		if (countIn > 0
				&& (countOut == 0 || (g$('#timeline').find(".hentry").length) < OTHERS_MINIMUM_LENGTH)) {
			g$('#pagination #more').click();
		}
	};

	return {data: objData, listener: fnListener};
}

function addMissedTab () {
	// ensure a scope-meaningful $
	var $ = g$ || function(v){
		var o = Array.prototype.slice.call(arguments);
		o.css = o.fadeIn = o.fadeOut = function(){return '';};
		o.hover = function(){return this;};
		return o;
	};
	
	// make a picker for gintRepliesDays
		// selectedIndex analog
	var elSelection = $E('span', {style:'display:inline;font:inherit'}, ['']);
		// <select> analog
	var $side = $('#side_base');
	var border = $side.css('border-left-color') || $side.css('border-right-color') || 'black';
	var background = $side.css('background-color') || 'inherit';
	var elPicker = attachEventListener(
		 $E('ul', {'class': 'CatchUp_tabPicker', style:'display:none;position:absolute;top:2em;right:0;'
		 		+ 'z-index:1;line-height:1.3;text-align:left;white-space:nowrap;'
				+ 'border:1px solid ' + border + ';background-color:' + background + ';'})
		,'click'
		,function(evt){
			var el = evt.srcElement || evt.originalTarget || evt.target;
			var value = parseInt(el.getAttribute('value'), 10);
			if (value) {
				setPersistentValue(PREF.REPLIES_DAYS, gintRepliesDays = value);
				elSelection.firstChild.nodeValue = value + ' days$'.slice(0, value == 1 ? -2 : -1);
			}
			try { evt.stopPropagation(); evt.preventDefault(); }catch(x){}
			try {	// refresh the tab if it is already active
				g$(document.evaluate(
					 "ancestor::*[contains(concat(' ',translate(@class,'\t\n','  '),' '), ' active ')][1]"
					,this, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null).singleNodeValue).click();
			}catch(x){}
			return evt.returnValue = false;
		}
		,true
	);
		// <option> analogs
	for (var i = 1, t; i <= 4; i++) {
		t = i + ' days$'.slice(0, i == 1 ? -2 : -1);
		elPicker.appendChild($E('li', {style: 'padding:0;'},
				$E('a', {value: i, style: 'margin-right:1px;padding:1px 1.3em 1px 5px;'}, t)));
		if(gintRepliesDays == i){ elSelection.firstChild.nodeValue = t; }
	}
		// global styles
	addPageStyle('ul.CatchUp_tabPicker li a:not(:hover) { background-color:inherit; }');

	// add the tab
	var added = addTab('*missed'
		,{title: '#fixreplies', heading: defer_('Missed replies')}
		,ajaxMissed
		,{
			 htmlLabel: [
				 $($E('span'
						,{title:'Maximum age of reply', class:'stat_count CatchUp_tabPickerParent',
								style:'position:relative;top:-.5em;height:2em;'
								+ 'font-size:80%;line-height:2.5;text-align:right;overflow:visible;'}
						,elSelection, ' \u25BC', elPicker
				)).hover(function(){$(elPicker).fadeIn('fast');}, function(){$(elPicker).fadeOut();})[0]
				,defer_('Missed')
			]
			,htmlTitle: defer_('See tweets dropped by #fixreplies')
			,hideForOthers: true
			,precedingTabSelector: '#home_tab, #profile_tab'
			,onReload: init
			//,dispatchAction: 'profile'
		}
	);
	if (added && $ !== g$) {
		var then = new Date();
		executeOnCondition(
			function(){	// jQuery
				if(g$){ return true; }	// satisfied
				if(((new Date()) - then) <= T_MIN){ return false; }	// try for 1 minute
				return null;	// abort
			}
			,function(){
				$side = g$('#side_base');
				border = $side.css('border-left-color') || $side.css('border-right-color') || 'black';
				background = $side.css('background-color') || 'inherit';
				g$(elPicker).css('border-color', border).css('background-color', background);
				g$(elPicker.parentNode).hover(function(){g$(elPicker).fadeIn('fast');},
						function(){g$(elPicker).fadeOut();});
			}
			,function(){
				addPageStyle('.CatchUp_tabPickerParent:hover .CatchUp_tabPicker) { '
						+ 'display:block !important; }');
			}
		);
	}
	return added;
}

function ajaxMissed (objUrlParts, fnAjax, objSettings, _tsAbort) {
	// run AJAX pre-callback
	fnAjax({async: false, global: false, url: BAD_URL, beforeSend: objSettings.beforeSend});
	
	// define finisher, and jump into it if this is a "more" request
	var finish = function(){
		// fake an AJAX response with a timeline of up to MISSED_PAGE entries
		var timeline = g$(garrMissed.timelineHtml);
		for (var a = garrMissed.splice(0,MISSED_PAGE), i = 0; i < a.length; i++) {
			timeline.append(a[i].entry);
		}
		var argsSuccess = [{
			 users: {}	// just in case
			,'#pagination': (garrMissed.length == 0 ? '' :
					replaceParams(HTML_MORE, {url: objUrlParts.url.replace(/(##.*)?$/, '##more')}))
			,'#timeline': g$('<div/>').append(timeline).html()	// compiled #timeline
		}, 'success'];
		fnAjax({async: false, global: false, url: BAD_URL, complete: objSettings.complete,
			error: function(){
				if(!argsSuccess){ return; }	// avoid double calls
				try{ objSettings.success.apply(this, argsSuccess); }catch(x){}
				argsSuccess = timeline = finish = undefined;	// free memory
			}
		});
	};
	if(objUrlParts.params == 'more' && garrMissed.timelineHtml){ return finish(); }

	// wait for garrFollowing
	var now = new Date();
	if (!gblnGotFollowing) {
		arguments[3] = arguments[3] || (now.getTime() + 15 * T_SEC);	// _tsAbort
		if(now <= arguments[3]){ return setTimeout(getCallback(arguments), T_SEC); }
		return fnAjax({global: ('global' in objSettings ? objSettings.global : true), url: BAD_URL,
			error: objSettings.error || function(){showNotification(g_(ABORT_TEXT));}
		});
	}

	// clear garrMissed, find tab, determine cutoff time, and make the initial URL queue
	garrMissed = [];
	var $tab = g$("ul.sidebar-menu li.loading"), $text = $tab.children("a"), hasProtected = false;
	var tsAfter = new Date(now.getFullYear(),now.getMonth(),now.getDate()) - gintRepliesDays * T_DAY;
	var queue = garrFollowing.filter(function(user){
		hasProtected = hasProtected || user.protected;
		return user.protected || (user._CatchUp_last_update >= tsAfter);
	});
	var sortFunction = sortFollowing_lastUpdate;
	if (queue.length > MAX_CATCHUP && (hasProtected
			? confirm(MISSED_LIMIT_PROMPT) : alert(MISSED_LIMIT_TEXT) && false)) {
		sortFunction = sortFollowing_lastUpdate_protectedFirst;
	}
	queue.sort(sortFunction);
	queue = queue.slice(0, MAX_CATCHUP);
	for (var i = queue.length - 1; i >= 0; i--) {
		queue[i] = URL_STATUSES.toString({screen_name: queue[i].screen_name});
	}

	// define response processor
	var aborted = false, processed = 0, pending = 0, width = $tab.outerWidth();
	var cssTab = $tab.css('cssText'), cssText = $text.css('cssText'), bgUri = getSolidBitmapUri(
		 	gjqSide.css('background-color').replace(/^$/,'127,127,127').replace(/[^0-9,]/g,'').split(','),
			width, $tab.outerHeight());
	$tab.css('background', [$text.css('background-color') || 'transparent', 'url("'+bgUri+'")',
			'no-repeat', Math.round(width * 0.8/queue.length) + 'px', 0].join(' '));
	$text.css('cssText', cssText.replace(/([^;])\s*$/, '$1;')
			+ 'background-color:transparent !important');
	var process = function(objData){
		if(aborted){ return; }
		var urlNext, strTimeline = objData && objData['#timeline'];

		if (strTimeline) {	// successful response
			// loop over and extract reply statuses
			var user = gmapFollowing[URL_STATUSES.getParams(this.url).screen_name];
			var $entries = g$(strTimeline).find("> .hentry");
			var varEarliest = $entries.filter(":last")
					.find(".status-body > .entry-meta > .entry-date > .timestamp").attr('data');
			$entries.filter(isMissedReply).each(function(){
				var $this = g$(this).removeClass('latest-status'), timestamp = now;
					// get timestamp and abort if it predates tsAfter
				try {
					timestamp = Date.parse(eval('('
						+ $this.find(".status-body > .entry-meta > .entry-date > .timestamp").attr('data')
					+ ')').time)
				}catch(x) {}
				if(timestamp < tsAfter){ return; }
					// add a user avatar image and (if necessary) screen name, and reformat retweet images
				$this.prepend(g$(replaceParams(HTML_AVATAR, user)));
				$this.find(".status-body:not(:has(> strong > .screen-name, > strong > .username))"
						+ " .status-content").prepend(g$(replaceParams(HTML_SCREEN_NAME, user)));
				$this.find(".big-retweet-icon").css({position:'relative', top:'1em', left:'-3.1em',
						zIndex:99, marginRight:'-28px', border:'3px solid white', backgroundColor:'white'});
					// capture the entry
				garrMissed.push({time: timestamp, entry: $this});
			});
			$entries = undefined;
				
			// seek more history if the earliest status postdates tsAfter
			try {
				varEarliest = Date.parse( eval('(' + varEarliest + ')').time );
				urlNext = (varEarliest >= tsAfter) && g$(objData['#pagination'] || '<a/>').attr('href');
			}catch(x){}
			if(urlNext){ queue.push(urlNext); }

			// processing finished; update progress and decrement pending count
			processed += 1;
			pending -= 1;
		}
		else if (objData !== null) {	// error response
			if (this.url.retries >= MAX_RETRIES) {	// retry limit exceeded; alert the user and abort
				aborted = (objData.status >= 400 && objData.responseText) || g_(ABORT_TEXT) || ABORT_TEXT;
				showNotification(aborted);
				return;
			}
			// increment retry count, requeue URL, and decrement pending count
			this.url.retries = (this.url.retries || 0) + 1;
			queue.push(this.url);
			pending -= 1;
		}

		// "atomically" get the next URL and increment pending
		pending += (urlNext = queue.splice(0, 1)).length;

		// show progress
		if (processed > 0) {
			$tab.css('background-position',
					Math.round(width * processed/(processed + pending + queue.length)) + 'px 0');
		}

		if (0 == urlNext.length && 0 == pending) { // no more URLs and none pending; time to finish
			$text.css('cssText', cssText);
			$tab.css('cssText', cssTab);
			garrMissed.sort(function(a,b){return b.time - a.time;});	// sort by descending timestamp
			garrMissed.timelineHtml = strTimeline.replace(/>(.|\n)*$/m, '/>');	// <ol id='timeline'...
			finish();
			setTimeout(function(){updateLocation(objUrlParts.suffix);}, 50);
			process = undefined;	// free memory
		}
		else if (urlNext[0]) {	// retrieve and process the next URL
			getJSON(urlNext[0], arguments.callee, arguments.callee, undefined, T_MIN);
		}
	};

	// show more or initiate processor threads, as appropriate
	for(var i = Math.min(queue.length, MAX_SIMULTANEOUS_REQUESTS) || 1; i > 0; i--){ process(null); }
}

function activateFollowing () {
	getFollowing(gstrPageUser, function(){
		gblnGotFollowing = true;
		addFollowingControls();
		sortFollowing();
		showAllFollowing();
		setInterval(function(){try{
			g$(gelFollowing).find('span.vcard a.url').each(function(){
				if(!this.getAttribute('titlePrototype')){ return; }
				this.setAttribute('title', this.getAttribute('titlePrototype')
						.replace(/%\{timestamp\{{0} ([^}]*)\}/, timeAgo));
			});
		}catch(x){} }, T_MIN);
	});
}

function showUpdate (objUpdate, objScript) {
	if(!arguments.length){ return UPDATE.check(UPDATE_CHECK_DAYS, SCRIPT.identifier, SCRIPT, showUpdate); }
	var title = objUpdate.name + ' ' + objUpdate.version + ', released ' + objUpdate.date;
	var elBefore = gelFollowing || document.body.firstChild;
	elBefore.parentNode.insertBefore(
		$E('div', {style: 'font-weight:bold;font-size:small;text-align:center;'}
			,$E('a', {href: objScript.source, title: title}, objScript.name + ' has been ')
			,$E('a', {href: objScript.identifier, title: title, style: 'color:red;background:#ddd;'},
					'updated!')
		)
		,elBefore
	);
//	catch (x) {
//		if (((new Date()) - objUpdate.lastCheck) < T_HR
//				&& confirm('Update ' + objScript.name + ' to ' + title + '?')) {
//			openInTab(objScript.identifier, objScript.name);
//		}
//	}
}

function sortFollowing () {
	var fnSort = sortFollowing_sequence;
	switch (gintSort) {
		case PREF.SORT.ALPHABETIC: fnSort = sortFollowing_alphabetic; break;
		case PREF.SORT.LAST_UPDATE: fnSort = sortFollowing_lastUpdate; break;
		//case PREF.SORT.FOLLOW_SEQUENCE: fnSort = sortFollowing_sequence; break;
	}
	garrFollowing.sort(fnSort);
}
function sortFollowing_sequence (a, b) {
	return (a._CatchUp_sequence - b._CatchUp_sequence);
}
function sortFollowing_alphabetic (a, b) {
	return (a.screen_name.toLowerCase() > b.screen_name.toLowerCase() ? 1 : -1);
}
function sortFollowing_lastUpdate (a, b) {
	return (b._CatchUp_last_update - a._CatchUp_last_update);
}
function sortFollowing_lastUpdate_protectedFirst (a, b) {
	return (a.protected != b.protected ? ((b.protected && 1) || -1) : sortFollowing_lastUpdate(a, b));
}

function showAllFollowing (blnForceRedraw) {
	if(!gelFollowing){ return; }
	for (var a = gelFollowing.childNodes, i = a.length - 1; i >= 0; i--) {
		if (blnForceRedraw || !a[i]._CatchUp_user) {
			gelFollowing.removeChild(a[i]);
			try{ a[i]._CatchUp_user.el = undefined; }catch(x){}
		}
	}
	var tsCutoff = (gintFilterHours ? (new Date()) - T_HR * gintFilterHours : 0);
	for (var i = 0, objUser, el; i < garrFollowing.length; i++) {
		objUser = garrFollowing[i];
		el = objUser.el;
		if (!el || el.parentNode !== gelFollowing) {
			el = objUser.el = createVCard(objUser);
		}
		el.style.display = (objUser._CatchUp_last_update.getTime() < tsCutoff ? 'none' : '');
		gelFollowing.insertBefore(el, null);
	}
}

function addFollowingControls () {
	if(!gelFollowing){ return; }

	// sort
	var elSort = $E('select', {title: 'sort the people you follow', style: 'font-size:inherit;'});
	var options = {
		 'by follow date': PREF.SORT.FOLLOW_SEQUENCE
		,'alphabetically': PREF.SORT.ALPHABETIC
		,'by last tweet': PREF.SORT.LAST_UPDATE
	}
	for (var label in options) {
		elSort.add($E('option', {value: options[label]}, label), null);
		if(gintSort == options[label]){ elSort.lastChild.selected = true; }
	}
	elSort.addEventListener('change', addFollowingControls_sortChange, true);

	// filter
	elFilter = $E('input', {type: 'text', size: 2, value: gintFilterHours || '#',
			title: "hide people who haven't updated within this time frame",
			style: 'width:1.85em;font-size:inherit;text-align:right;'});
	elFilter.addEventListener('change', addFollowingControls_filterChange, true);
	elFilter.addEventListener('keypress', addFollowingControls_filterKey, true);
	elFilter = $E('span', {}, elFilter, ' hours ');

	// add
	gelFollowing.parentNode.insertBefore(
			$E('div', {style: 'font-size:smaller;text-align:center;'}, elFilter, elSort),
			gelFollowing);
}
function addFollowingControls_sortChange () {
	gintSort = parseInt(this.options[this.selectedIndex].value,10) || 0;
	setPersistentValue(PREF.SORT, gintSort);
	sortFollowing();
	showAllFollowing();
}
function addFollowingControls_filterChange () {
	var value = parseInt(this.value || '0', 10);
	if(isNaN(value)){ return; }
	gintFilterHours = value;
	setPersistentValue(PREF.FILTER_HOURS, gintFilterHours);
	showAllFollowing();
}
function addFollowingControls_filterKey (e) {
	if(e.keyCode == 13){ addFollowingControls_filterChange.call(this, e); }
}

function getFollowing (strScreenName, fnDone) {
		// http://apiwiki.twitter.com/Twitter-REST-API-Method%3A-statuses%C2%A0friends
	var seekProtected = undefined, arrProtected = [], strCursor = '-1';
	var strCursoredUrl = URL_FOLLOWING.toString(
			{screen_name: strScreenName || undefined}, {cursor: ''});

	getJSON(strCursoredUrl + strCursor, function(objResponse){
			// check for (and separate) protected users
		for (var a = (objResponse || {}).users || [], i = a.length - 1; i >= 0; i--) {
			if (a[i].protected && a[i].statuses_count) {	// protected user with statuses
				if (!a[i].status && seekProtected === undefined) {	// authentication required for status
					seekProtected = !gintPromptForProtected || confirm(FOLLOWING_PROTECTED_PROMPT);
					if (seekProtected) {
						// trigger authentication request by seeking protected user data; retry when done
						var fnThis = arguments.callee, intMutex = 1, fnRetry = function(){
							if(!((intMutex--) > 0)){ return (++intMutex); }
							getJSON(strCursoredUrl + strCursor, fnThis);
						};
						return getJSON(URL_FOLLOWING.toString({screen_name: a[i].screen_name, cursor: -1}),
								fnRetry, fnRetry, function(r){if(r && r.readyState==4 && r.status>=400)fnRetry();});
					}
				}
				arrProtected.push(objResponse.users.splice(i, 1)[0]);
			}
		}
			// add non-protected users and, if not finished, request more
		addFollowing(objResponse.users);
		strCursor = objResponse.next_cursor_str;
		if (/[^0 ]/.test(strCursor)) {
			return getJSON(strCursoredUrl + strCursor, arguments.callee);
		}
			// handle protected users and finish
		if (false && arrProtected.length > 0 && seekProtected !== false
				&& (seekProtected || !gintPromptForProtected || confirm(FOLLOWING_PROTECTED_PROMPT))) {
			return getFollowing_protected(arrProtected, fnDone);
		}
		else {
			addFollowing(arrProtected);
			try{ fnDone(); }catch(x){}
		}
	});
}
function getFollowing_protected (arrProtected, fnDone) {
	// manually get the latest tweet from everyone in arrProtected before |addFollowing|
	addFollowing(arrProtected);
	try{ fnDone(); }catch(x){}
}

function addFollowing (arrFollowing) {
	for (var L = arrFollowing.length, i = 0, objIn, objOut; i < L; i++) {
		objIn = arrFollowing[i];
		objOut = {};
		for(var p in KEEP_PROPERTIES){ objOut[p] = objIn[p]; }
		objOut._CatchUp_sequence = gintSequence;
		gintSequence += 1;
		objOut._CatchUp_last_update = new Date(Date.parse((objOut.status || {}).created_at) || 0);
		arrFollowing[i] = gmapFollowing[objOut.screen_name] = objOut;
	}
	garrFollowing.push.apply(garrFollowing, arrFollowing);
}

// Twitter-specific helper functions
function addTab (strUrlSuffix, objTitleAndHeading, fnAjaxInterceptor, objOptions) {
	objOptions = objOptions || {};
	var then = new Date(), match;

	// check if we should replace a user 404 page
	match = RE_SUFFIXED_URL.exec(gstrLocationNoHash) || {};
	if (!gblnReload && gblnUser404 && match[4] == strUrlSuffix
			&& !(objOptions.hideForOthers && match[2] && gstrSessionUser && match[2]!=gstrSessionUser)) {
		// this tab was requested but not shown, so let's replace the page!
		gblnReload = true;
			// let the user know we're working
		try {
			var elWrapper = null, title, re = /(^|\s)wrapper(\s|$)/;
			for (var a = document.getElementsByTagName('div'), i = 0; !elWrapper && i < a.length; i++) {
				if(re.test(a[i].className)){ elWrapper = a[i]; }
			}
			var s = {backgroundImage: ["url('",IMG_LOADING,"')"].join(''), backgroundRepeat: 'no-repeat',
					backgroundPosition: '92.5% center'};
			for(var p in s){ try{elWrapper.style[p]=s[p];}catch(x){} }
			elWrapper.getElementsByTagName('h2')[0].firstChild.nodeValue = 'Please wait...';
		}catch(x){}
			// page replacement
		var fnOnReload = objOptions.onReload || function(){ try{
			var evt = document.createEvent('HTMLEvents');
			evt.initEvent('load', false, false);
			window.dispatchEvent(evt);
		}catch(x){} };
		httpRequest({method: 'GET', url: match[1], headers: REQUEST_HEADERS,
			onload: function(objResponse){
				replacePage(objResponse.responseText, function(){
					executeOnCondition(function(){	// jQuery
						if(gobjUnsafeWindow.jQuery){ return true; }	// satisfied
						if(((new Date()) - then) <= T_SEC * 15){ return false; }	// try for 15 seconds
						return null;	// abort
					}, function(){twitterDocumentReady(fnOnReload);}, fnOnReload, T_SEC / 5);
				});
			}
		});
		return false;
	}

	// try to add the tab
	if (!gblnUser404) {
		match = RE_SUFFIXED_URL.exec(location.href) || {};
		if (gstrPageUser && (!objOptions.hideForOthers || gstrPageUser == gstrSessionUser)) {
			var isCurrentTab = (match[4] == strUrlSuffix
					&& (match[2] || match[3] || gstrPageUser != strUrlSuffix));
			executeOnCondition(
				function(){	// jQuery
					if(g$){ return true; }	// satisfied
					if(((new Date()) - then) <= T_MIN){ return false; }	// try for 1 minute
					return null;	// abort
				}
				,function(){
					addJQueryAjaxInterceptor({urlSuffix: strUrlSuffix, interceptor: fnAjaxInterceptor});
					setTimeout(function(){
						addTab_show(strUrlSuffix, objTitleAndHeading, isCurrentTab, objOptions);
					}, 0);
				}
				,null
				,T_SEC / 5
			);
		}
	}

	return true;
}
function addTab_show (strName, objTitleAndHeading, blnActivate, objOptions) {
	var selPrecedingTab = (objOptions.precedingTabSelector || '#x[id!=x]') +', #primary_nav > *:last';
	var $referenceTab = g$(objOptions.referenceTabSelector || selPrecedingTab);
	if(!$referenceTab.length){ return; }
		// build the new tab's href from $referenceTab
	var href = $referenceTab.children('a').attr('href') || '';	//.children('.in-page-link')
	if(!href){ return setTimeout(getCallback(arguments), 50); }
	href = href.replace(/^(.*?:[\/]*.*?(?=\/|$))?.*$/, '$1/' + gstrPageUser + '/' + strName);

	var tabAttributes = {id: '_' + SCRIPT.name + '_' + strName.replace(/\W/g,'') + '_tab'};
	if(objOptions.htmlTitle){ tabAttributes.title = objOptions.htmlTitle; }
	var $tab = g$($E('li', tabAttributes, $E('a', {href: href, class: 'in-page-link',
			data: '{"dispatch_action":"' + (objOptions.dispatchAction || 'profile') + '"}'},
		objOptions.htmlLabel || strName
	))).insertAfter(g$(selPrecedingTab).eq(0)).children();
	try{ $tab = $tab.isInPageLink(); }catch(x){}
	try{ $tab = $tab.isSidebarTab(); }catch(x){}
	try{ gobjUnsafeWindow.twttr.titles_and_headings[strName] = objTitleAndHeading; }catch(x){}
	if(blnActivate){ setTimeout(function(){$tab.click();},0); }
}

function twitterDocumentReady (fnOnComplete) {
	try {	// see twitter.js:$(document).ready
		(function($, twttr){
		  $().Page();
		  if(twttr) twttr.setDefaultBucket();
		  this.initializeTimeline();
		  $("#pagination #more").isMoreButton();
		  if(twttr) $("body").bind("ajaxSuccess", twttr.setupRetweetTips);
		  if(twttr) twttr.setupRetweetTips();
		  $("span.byline a").tipsy({
		    gravity: "n"
		  });
		  this.initializePage();
		  fnOnComplete();
		}).call(gobjUnsafeWindow, gobjUnsafeWindow.jQuery, gobjUnsafeWindow.twttr);
	}
	catch(x){
		try{ $("#pagination #more").isMoreButton(); }catch(x2){}
		try{ this.initializePage(); }catch(x2){}
		fnOnComplete();
		throw x;
	}
}

function updateLocation (strSuffix, blnInsertUserInUrl, idBody) {
	try {
		var objText = gobjUnsafeWindow.twttr.titles_and_headings[strSuffix];
		for(var p in objText){ if((objText[p] || {}).deferred){ objText[p] += ''; } }	// toString
	}catch(x){}
	try {
		gobjUnsafeWindow.twttr.updateLocation(
			(!blnInsertUserInUrl || gblnPageUserInUrl ? '' : gstrPageUser + '/') + strSuffix
		);
		document.getElementsByTagName('body')[0].id = idBody || strSuffix;
		gobjUnsafeWindow.setTitleAndHeading(strSuffix);
	}catch(x){}
}

function showNotification (str) {
	try {
		(new gobjUnsafeWindow.InfoNotification()).setMessage(str).show();
	}
	catch(x) {
		alert(str);
	}
}

function isReply (idx, elEntry) {
	elEntry = elEntry || this;
	var blnReply = (g$(".entry-meta:last:contains('in reply to')", elEntry).length != 0);
	var elUsername = !blnReply && g$('.entry-content > .username', elEntry)[0];
	var txt = (elUsername && elUsername.previousSibling) || {};
	return blnReply || (txt.nodeType == 3 && /^\s*@$/.test(txt.nodeValue) && !txt.previousSibling);
}
function isNotReply (idx, elEntry) {
	return !isReply(idx, elEntry || this);
}
function isMissedReply (idx, elEntry) {
	elEntry = elEntry || this;
	if(!isReply(idx, elEntry)){ return false; }
	var $atUser = g$('.entry-content > .username', elEntry).eq(0);
	var strAtUser = $atUser.text();
	return ($atUser.length != 0 && strAtUser != gstrSessionUser && !gmapFollowing[strAtUser]);
}

function getPageUser () {
	var strUser;
	try {
		strUser = document.evaluate('head/meta[@name="page-user-screen_name"]/@content',
				document.documentElement, null, XPathResult.STRING_TYPE, null).stringValue;
	}
	catch (x) {
		strUser = location.pathname.substring(1).split('/')[0].replace(/^home$/, '');
	}
	return strUser || getSessionUser();
}
function getSessionUser () {
	try {
		return document.evaluate('head/meta[@name="session-user-screen_name"]/@content',
				document.documentElement, null, XPathResult.STRING_TYPE, null).stringValue;
	}
	catch (x) {
		return (document.getElementById('profile_link') || {href: ''}).href.split('/').pop();
	}
}

// Twitter-inspired helper functions
function createVCard (objUser) {
	var status = objUser.status;
	var title = objUser.name;
	if (objUser._CatchUp_last_update.getTime()) {
		title += ' (%{timestamp ' + objUser._CatchUp_last_update.getTime() + '}): ' + status.text;
	}
	var imgStyle = '';
	if (objUser.protected) {
		try {
			imgStyle = gjqSide.css('background-color').replace(/^$/,'0,0,255').replace(/[^0-9,]/g,'')
					.split(',');
			for(var i = imgStyle.length - 1; i >= 0; i--){ imgStyle[i] = 0xFF - imgStyle[i]; }
		}
		catch(x){}
		imgStyle = ['outline:', gintProtectedOutline, 'px outset rgb(', imgStyle.join(','), ');']
				.join('');
	}
	var el = $E('span', {'class': 'vcard'},
		$E('a', {titlePrototype: title, title: title.replace(/%\{timestamp ([^}]*)\}/, timeAgo),
				rel: 'contact', hreflang: 'en', 'class': 'url', href: '/' + objUser.screen_name},
			$E('img', {width: 24, height: 24, 'class': 'photo fn', style: imgStyle, alt: objUser.name,
					src: objUser.profile_image_url.replace(/_normal(\.[^.]{3,4})?$/, '_mini$1')})
		)
	);
	el._CatchUp_user = objUser;
	return el;
}

function timeAgo (strSubstring, strTimestamp) {
	if(!strTimestamp){ return false; }
	var d = (new Date()) - (new Date(parseInt(strTimestamp,10)));
	if(isNaN(d) || d < 0){ return false; }
	if(d < T_SEC * 90){ return "1 minute ago"; }
	if(d < T_MIN * 45){ return "%{time} minutes ago".replace(/%\{time\}/, Math.round(d / T_MIN)); }
	if(d < T_MIN * 90){ return "1 hour ago"; }
	if(d < T_HR * 36.5){ return "%{time} hours ago".replace(/%\{time\}/, Math.round(d / T_HR)); }
	return "%{time} days ago".replace(/%\{time\}/, Math.round(d / T_DAY));
}

// jQuery helper functions
function defer_ () {	// delayed evaluation of calls to |_|
	var args = Array.prototype.slice.call(arguments);
	return {deferred: true, toString: function(){return g_.apply(null, args);}};
}

function replaceParams (str, obj) {
	for(var p in (obj || {})) {
		str = str.replace(new RegExp("\\%\\{" + p + "\\}", "gi"), obj[p]);
	}
	return str;
}

function interceptAjax () {
	var $ajax = g$.ajax;
	g$.ajax = function(objSettings) {
		var match = RE_SUFFIXED_URL.exec((objSettings || {}).url || ''), objInterceptor = null;
		if (match) {
			for (var suffix = match[4], i = 0; !objInterceptor && i < garrAjaxInterceptors.length; i++) {
				objInterceptor = (suffix == garrAjaxInterceptors[i].urlSuffix) && garrAjaxInterceptors[i];
			}
			if (objInterceptor) {
				for(var p in SUFFIXED_URL_PROPERTIES){ match[p] = match[SUFFIXED_URL_PROPERTIES[p]]; }
				return objInterceptor.interceptor.apply(this,
						[match, $ajax].concat(Array.prototype.slice.call(arguments)));
			}
		}
		return $ajax.apply(this, arguments);
	};
	copyProperties($ajax, g$.ajax);
}

function addJQueryAjaxInterceptor (objInterceptor) {
	garrAjaxInterceptors.push(objInterceptor);
}

// user script helper functions
var setPersistentValue, getPersistentValue, registerMenuCommand = function(){};
try {
	setPersistentValue = GM_setValue;
	getPersistentValue = GM_getValue;
	registerMenuCommand = GM_registerMenuCommand;
}
catch (x) {
	(function(){
		var gobjPersistent = {};
		setPersistentValue = function(strName, varValue) {
			gobjPersistent[strName] = varValue;
			return varValue;
		};
		getPersistentValue = function(strName, varDefault) {
			return (gobjPersistent.hasOwnProperty(strName) ? gobjPersistent[strName] : varDefault);
		};
	})();
}

var addPageStyle;
try {
	addPageStyle = GM_addStyle;
}
catch (x) {
	addPageStyle = function(strCss) {
		try {
			document.documentElement.getElementsByTagName('head')[0].appendChild(
				document.createElement('style')
			).appendChild(document.createTextNode(strCss));
		}catch(x){}
	};
}

function alterPageStyle (reSelector, fnAlterCss, _arrRules) {
	if (arguments.length < 3) {
		for (var a = document.styleSheets, i = 0; i < a.length; i++) {
			try{ alterPageStyle(reSelector, fnAlterCss, a[i].cssRules); }catch(x){}
		}
		return;
	}
	for (var i = 0, objRule; i < _arrRules.length; i++) {
		objRule = _arrRules.item(i);
		try{ alterPageStyle(reSelector, fnAlterCss, objRule.styleSheet.cssRules); }catch(x){}	// @import
		try{ alterPageStyle(reSelector, fnAlterCss, objRule.cssRules); }catch(x){}	// @media
		try{ if(reSelector.test(objRule.selectorText)){ fnAlterCss(objRule); } }catch(x){}
	}
}

var httpRequest;
try {
	httpRequest = GM_xmlhttpRequest;
}
catch (x) {	// adapted from http://jibbering.com/2002/4/httprequest.html
	httpRequest = function(objDetails) {
		var request = null;
		/*@cc_on @*/	// JScript (Internet Explorer) conditional compilation
		/*@if (@_jscript_version >= 5)
		try {
			request = new ActiveXObject("Msxml2.XMLHTTP");
		}
		catch (x) {
			try{ request = new ActiveXObject("Microsoft.XMLHTTP"); }catch(x){}
		}
		@end @*/
		try{ request = request || new XMLHttpRequest(); }catch(x){}

		try {
			request.open(objDetails.method, objDetails.url, true);

			try { for (var h in objDetails.headers) {
				try{ request.setRequestHeader(h, objDetails.headers[h]); }catch(x){}
			} }catch(x){}
			request.onreadystatechange = function(){
				var	details = {readyState: request.readyState,
						responseHeaders: request.getAllResponseHeaders(),
						responseText: request.responseText,
						status: request.status,
						statusText: request.statusText};
				try{ objDetails.onreadystatechange.call(request, details); }catch(x){}
				if(request.readyState != 4){ return; }
				try {
					objDetails[request.status >= 200 && request.status < 400
							? "onload" : "onerror"].call(request, details);
				}catch(x){}
			};

			request.send(objDetails.data || null);
		}catch(x){}

		return request;
	};
}

var openInTab;
try {
	openInTab = GM_openInTab;
}
catch (x) {
	openInTab = function(strUrl, strName) {
		if(strName === undefined){ strName = '_blank'; }
		if(!window.open(strUrl, strName)){ gobjUnsafeWindow.location.assign(strUrl); }
	};
}

// generic helper functions
function replacePage (strHtml, fnOnComplete) {
	var head = {content:'', attributes:{}}, body = {content:'', attributes:{}}, ref = {content:''};
	var reContent = /(<head\b)|(<\/head\b)|(<body\b)|(<\/body\b)/gm, match, idxCapture,
			idxContent, attributes;

	while (match = reContent.exec(strHtml)) {
		for(idxCapture = 1; idxCapture in match && !match[idxCapture]; idxCapture++);
		if (1 == (idxCapture % 2)) {	// open tag
			switch (match[idxCapture]) {
				case '<head': ref = head; break;
				case '<body': ref = body; break;
				default: ref = {content:'', attributes:{}};
			}
			idxContent = strHtml.indexOf('>', match.index) + 1;
			attributes = getAttributesFromHtml(strHtml.substring(match.index, idxContent));
			for(var a in attributes){ ref.attributes[a] = attributes[a]; }
		}
		else {	// close tag
			ref.content += strHtml.substring(idxContent, match.index);
			idxContent = match.index;
		}
	}

	var elHead = document.getElementsByTagName('head')[0]
			|| document.documentElement.appendChild(document.createElement('head'));
	for(var a = elHead.childNodes, i = a.length - 1; i >= 0; i--){ elHead.removeChild(a[i]); }
	for(var a in head.attributes){ elHead.setAttribute(a, head.attributes[a]); }
	elHead.innerHTML = head.content;

	var elBody = document.body || document.getElementsByTagName('body')[0]
			|| document.documentElement.appendChild(document.createElement('body'));
	for(var a in body.attributes){ elBody.setAttribute(a, body.attributes[a]); }
	elBody.innerHTML = body.content;

	setTimeout(function(){
		windowEval(gobjUnsafeWindow, document.getElementsByTagName('script'), fnOnComplete);
	}, 50);
}

function windowEval (objWindow, varSource, fnOnComplete, _idx) {
	if ('string' == (typeof varSource) || varSource instanceof String) {
		(function(objThis, idUnique, intMaxAttempts, intAttempt){
			var meta = (fnOnComplete ? [
				 "document.body.appendChild(document.createElement('span')).id='" + idUnique + "';"
				,"document.getElementById('" + idUnique + "').className='success';"
			] : ['','']);
			this.setTimeout(["document.write=function(){};", meta[0], varSource, meta[1]].join('\n'), 0);
			if(!fnOnComplete){ return; }
			this.setTimeout(function(){
				var el = document.getElementById(idUnique);
				if (!(el && el.className) && intAttempt < intMaxAttempts) {	// try again
					intAttempt += 1;
					return objThis.setTimeout(arguments.callee, 50);
				}
				try{ el.parentNode.removeChild(el); }catch(x){}
				fnOnComplete();	// move on
			}, 50);
		}).call(objWindow, objWindow, windowEval_getUniqueElementId(), 20, 0);
	}
	else {
		if(!('length' in varSource)){ varSource = [varSource]; }
		for (var i = _idx || 0, el; i < varSource.length; i++) {
			el = varSource[i];
			if (el.src) {
				var fnContinue = function(){ windowEval(objWindow, varSource, fnOnComplete, i + 1); };
				return httpRequest({method: 'GET', url: el.src, headers: REQUEST_HEADERS,
					 onload: function(response){ windowEval(objWindow, response.responseText, fnContinue); }
					,onerror: fnContinue
				});
			}
			else {
				windowEval(objWindow, el.textContent || el.text);
			}
		}
		(fnOnComplete || function(){})();
	}
}
function windowEval_getUniqueElementId (intMaxAttempts) {
	var id, attempt = 0;
	do {
		attempt += 1;
		id = ['windowEval', attempt, (new Date()).getTime(), Math.random()].join('_').replace('0.','');
	} while(document.getElementById(id) && !(attempt >= intMaxAttempts))
	return (attempt >= intMaxAttempts ? undefined : id);
}

function getJSON (strUrl, fnCallback, fnOnError, fnOnReadyStateChange, msTimeout) {
	var done = false, headers = {}; for(var p in REQUEST_HEADERS){ headers[p] = REQUEST_HEADERS[p]; }
	try{ headers.Accept = g$.ajaxSettings.accepts.json; }catch(x){}
	headers.Accept = (headers.Accept || 'application/json, text/javascript') + ', *' + '/' + '*';
	var objDetails = {method: 'GET', url: strUrl, headers: headers
		,onload: function(obj){
			if(done){ return; }
			done = true;
			var json = obj.responseText, o;
			try {
				if((typeof JSON) != 'undefined'){ o = JSON.parse(json); }
				else{ o = eval(['(', json, ')'].join('')); }
			}
			catch(x) {
				done = false;
				return this.onerror();
			}
			fnCallback.call(objDetails, o);
		}
		,onerror: function(){
			if(done){ return; }
			done = true;
			if(fnOnError){ fnOnError.apply(objDetails, arguments); }
		}
		,onreadystatechange: (fnOnReadyStateChange ? function(){
			if(!done){ fnOnReadyStateChange.apply(objDetails, arguments); }
		} : undefined)
	};
	var objRequest = httpRequest(objDetails);
	var tsAbort = new Date((new Date()).getTime() + msTimeout);
	if ((new Date()) < tsAbort) {
		var checkAbort = setInterval(function(){
			if (done) {
				clearInterval(checkAbort);
			}
			else if ((new Date()) >= tsAbort) {
				try{ objRequest.abort(); }catch(x){}
				try{ objDetails.onerror({}); }catch(x){ done = true; }
			}
		}, 50);
	}
}

function getSolidBitmapUri (rgb, intWidth, intHeight) {
	var len = intHeight * 4 * Math.ceil((intWidth/8)/4), arrBmp = [
		 'BM'	// BMP header: magic number
		,getSolidBitmapUri_N(4, 0x3A + len)	// BMP header: BMP size
		,getSolidBitmapUri_N(4, 0)	// BMP header: reserved
		,getSolidBitmapUri_N(4, 0x3A)	// BMP header: data offset
		,getSolidBitmapUri_N(4, 40)	// DIB header: Windows V3
		,getSolidBitmapUri_N(-4, intWidth)	// DIB header: width
		,getSolidBitmapUri_N(-4, intHeight)	// DIB header: height
		,getSolidBitmapUri_N(2, 1)	// DIB header: color planes
		,getSolidBitmapUri_N(2, 1)	// DIB header: bits per pixel
		,getSolidBitmapUri_N(4, 0)	// DIB header: compression method
		,getSolidBitmapUri_N(4, len)	// DIB header: data size
		,getSolidBitmapUri_N(-4, 0x0B13)	// DIB header: horizontal pixels/meter
		,getSolidBitmapUri_N(-4, 0x0B13)	// DIB header: vertical pixels/meter
		,getSolidBitmapUri_N(4, 1)	// DIB header: palette size
		,getSolidBitmapUri_N(4, 1)	// DIB header: important colors
		,getSolidBitmapUri_N(4,	// palette: 1x RGB
				((rgb[0] << 16) & 0xFF0000) | ((rgb[1] << 8) & 0xFF00) | (rgb[2] & 0xFF))
		,(new Array(1 + len)).join('\u0000')	// data
	];
	return 'data:image/bmp,' + getSolidBitmapUri_encode(arrBmp.join(''));
}
function getSolidBitmapUri_N (intBytes, intN) {	// signed/unsigned little-endian (LSB first) integer
	var a = [], signed = (intBytes < 0);
	for (var i = Math.abs(intBytes) - 1; i >= 0; i--) {
		a.push((intN & 0xFF) || 0);
		intN = (signed ? intN >> 8 : intN >>> 8);
	}
	return String.fromCharCode.apply(String, a);
}
function getSolidBitmapUri_encode (str) {
	var H = '0123456789ABCDEF';	// hexadecimal characters
	// encode and replace two-byte encodings %C2%80-%C2%BF & %C3%80-%C3%BF (110000bb,10bbbbbb)
	return encodeURIComponent(str).replace(/%C2/g,'').replace(/%C3%(.)/g, function(s,h){
		return '%' + H.charAt(H.indexOf(h) | 0x4);
	});
}

var $E = function createElement (strName, objAttributes, varContent /*, varContent, ...*/) {
	var el = document.createElement(strName);
	try{
		for (var attribute in objAttributes) {
			el.setAttribute(attribute, objAttributes[attribute]);
		}
	}catch(x){}
	if (arguments.length > 3 || (!/^(string|undefined)$/.test(typeof(varContent)) && !(varContent instanceof Array))) {
		varContent = Array.prototype.slice.call(arguments, 2);
	}
	if (varContent instanceof Array) {
		for (var L = varContent.length, i = 0, c; i < L; i++) {
			c = varContent[i];
			el.appendChild(c && typeof(c) == 'object' && 'parentNode' in c
					? c : document.createTextNode(c));
		}
	}
	else if (varContent) {
		el.innerHTML = varContent;
	}
	return el;
}

function attachEventListener (el, strEvent, fnListener, blnUseCapture) {
	try {
		el.addEventListener(strEvent, fnListener, blnUseCapture ? true : false);
	}
	catch (x) { try {
		el.attachEvent('on' + strEvent, fnListener);
	}
	catch (x) {} }
	return el;
}

function executeOnCondition (fnIsSatisfied, fnOnSatisfied, fnOnAbort, intInterval, _fnRetry) {
	switch (fnIsSatisfied() && true) {
		case true:
			if(fnOnSatisfied){ fnOnSatisfied(); }
			break;
		case false:
			setTimeout(_fnRetry || executeOnCondition_getCallback(arguments), intInterval || 50);
			break;
		default:
			if(fnOnAbort){ fnOnAbort(); }
	}
}
function executeOnCondition_getCallback (args) {
	var exits = {1: args[1] || function(){}, 2: args[2] || function(){}};
	var done = function() {
		for(var i = args.length - 1; i >= 0; i--){ delete args[i]; }
		args = exits = done = undefined;
	};
	args[1] = function(){ exits[1].apply(this,arguments); done(); };
	args[2] = function(){ exits[2].apply(this,arguments); done(); };
	args[4] = function(){ executeOnCondition.apply(null, args); };
	return args[4];
}

function getCallback (args, objThis, fn) {
	return function(){ (fn || args.callee).apply(objThis, args); };
}

function copyProperties (from, to) {
	try {
		for (var p in from) {
			try {
				if (from.hasOwnProperty(p)) {
					to[p] = from[p];
				}
			}catch(x){}
		}
	}catch(x){}
	for (var a = ['__proto__', 'prototype', 'constructor'], i = 0, p; p = a[i]; i++) {
		try{ to[p] = from[p]; }catch(x){}
	}
}

function getAttributesFromHtml (strTag) {
	var map = {}, match;
	while (match = reAttribute.exec(strTag)) {
		map[match[1]] = (match[3] !== undefined ? match[3] : match[1]);
	}
	return map;
}
var reAttribute = /\s([^\s=\/]+)(?:\s*=\s*(['"'])?(.*?)\2(?=\s|\/|>|$))?/gm;

attachEventListener(window, 'load', init, true);

})();