Skip to content

Instantly share code, notes, and snippets.

@badp
Created November 26, 2010 00:11
Show Gist options
  • Select an option

  • Save badp/716098 to your computer and use it in GitHub Desktop.

Select an option

Save badp/716098 to your computer and use it in GitHub Desktop.
This causes Chrome dev to crash.
// ==UserScript==
// @name TwitterUnread
// @namespace http://henrik.heimbuerger.de/greasemonkey/
// @description [alpha01] ...
// @include http://twitter.com/
// @include http://twitter.com/#
// @include http://www.twitter.com/
// @include http://www.twitter.com/#
// @include https://twitter.com/
// @include https://twitter.com/#
// @include https://www.twitter.com/
// @include https://www.twitter.com/#
// ==/UserScript==
// TODO:
// � better visual style for read tweets
// � fallback for enhancing if the events all occur before the script can be loaded
// � trigger initialization from an actual twttr pointcut instead of polling
// � give the 'mark read' button its own icon
// � only process the Home timeline, support switching between the streams, etc.
// � store read state in the cloud
// � support for Opera and Chrome
// wait for jQuery and twttr.app
function waiter() {
if(unsafeWindow.jQuery && unsafeWindow.twttr
&& unsafeWindow.twttr.app
&& unsafeWindow.twttr.app.currentPage()
&& unsafeWindow.twttr.app.currentPage().streamManager
&& unsafeWindow.twttr.app.currentPage().streamManager.streams
&& unsafeWindow.twttr.app.currentPage().streamManager.streams.current) {
console.debug('ready');
script(unsafeWindow.jQuery, unsafeWindow.twttr);
} else {
console.debug('waiting')
setTimeout(waiter, 100);
}
}
waiter();
function script($, twttr) {
console.debug('start');
/*
* Helper function for adding CSS to the current page
*/
function addGlobalStyle(css) {
var head, style;
head = document.getElementsByTagName('head')[0];
if (!head) { return; }
style = document.createElement('style');
style.type = 'text/css';
style.innerHTML = css;
head.appendChild(style);
}
function addGlobalScript(code) {
var head, script;
head = document.getElementsByTagName('head')[0];
if (!head) { return; }
script = document.createElement('script');
script.type = 'text/javascript';
script.innerHTML = code;
head.appendChild(script);
}
addGlobalStyle('.tweet-actions .mark-read-action span i{background-position:0 0;margin-right:0;margin-left:3px;}.tweet-actions .mark-read-action:hover span i{background-position:-16px 0;}');
/*
* jQuery.ScrollTo - Easy element scrolling using jQuery.
* Copyright (c) 2007-2009 Ariel Flesler - aflesler(at)gmail(dot)com | http://flesler.blogspot.com
* Dual licensed under MIT and GPL.
* Date: 5/25/2009
* @author Ariel Flesler
* @version 1.4.2
*
* http://flesler.blogspot.com/2007/10/jqueryscrollto.html
*/
console.debug('initializing $.scrollTo');
addGlobalScript("(function(d){var k=d.scrollTo=function(a,i,e){d(window).scrollTo(a,i,e)};k.defaults={axis:'xy',duration:parseFloat(d.fn.jquery)>=1.3?0:1};k.window=function(a){return d(window)._scrollable()};d.fn._scrollable=function(){return this.map(function(){var a=this,i=!a.nodeName||d.inArray(a.nodeName.toLowerCase(),['iframe','#document','html','body'])!=-1;if(!i)return a;var e=(a.contentWindow||a).document||a.ownerDocument||a;return d.browser.safari||e.compatMode=='BackCompat'?e.body:e.documentElement})};d.fn.scrollTo=function(n,j,b){if(typeof j=='object'){b=j;j=0}if(typeof b=='function')b={onAfter:b};if(n=='max')n=9e9;b=d.extend({},k.defaults,b);j=j||b.speed||b.duration;b.queue=b.queue&&b.axis.length>1;if(b.queue)j/=2;b.offset=p(b.offset);b.over=p(b.over);return this._scrollable().each(function(){var q=this,r=d(q),f=n,s,g={},u=r.is('html,body');switch(typeof f){case'number':case'string':if(/^([+-]=)?\d+(\.\d+)?(px|%)?$/.test(f)){f=p(f);break}f=d(f,this);case'object':if(f.is||f.style)s=(f=d(f)).offset()}d.each(b.axis.split(''),function(a,i){var e=i=='x'?'Left':'Top',h=e.toLowerCase(),c='scroll'+e,l=q[c],m=k.max(q,i);if(s){g[c]=s[h]+(u?0:l-r.offset()[h]);if(b.margin){g[c]-=parseInt(f.css('margin'+e))||0;g[c]-=parseInt(f.css('border'+e+'Width'))||0}g[c]+=b.offset[h]||0;if(b.over[h])g[c]+=f[i=='x'?'width':'height']()*b.over[h]}else{var o=f[h];g[c]=o.slice&&o.slice(-1)=='%'?parseFloat(o)/100*m:o}if(/^\d+$/.test(g[c]))g[c]=g[c]<=0?0:Math.min(g[c],m);if(!a&&b.queue){if(l!=g[c])t(b.onAfterFirst);delete g[c]}});t(b.onAfter);function t(a){r.animate(g,j,b.easing,a&&function(){a.call(this,n,b)})}}).end()};k.max=function(a,i){var e=i=='x'?'Width':'Height',h='scroll'+e;if(!d(a).is('html,body'))return a[h]-d(a)[e.toLowerCase()]();var c='client'+e,l=a.ownerDocument.documentElement,m=a.ownerDocument.body;return Math.max(l[h],m[h])-Math.min(l[c],m[c])};function p(a){return typeof a=='object'?a:{top:a,left:a}}})(jQuery)");
/*
* jQuery AOP - jQuery plugin to add features of aspect-oriented programming (AOP) to jQuery.
* http://jquery-aop.googlecode.com/
*
* Licensed under the MIT license:
* http://www.opensource.org/licenses/mit-license.php
*
* Version: 1.3
*
* Cross-frame type detection based on Daniel Steigerwald's code (http://daniel.steigerwald.cz)
* http://gist.github.com/204554
*/
console.debug('initializing $.aop');
addGlobalScript("(function(){var j=1;var k=2;var l=3;var m=4;var n=5;var o=6;var p=true;var q='arguments';var r='undefined';var s=(function(){var toString=Object.prototype.toString,toStrings={},nodeTypes={1:'element',3:'textnode',9:'document',11:'fragment'},types='Arguments Array Boolean Date Document Element Error Fragment Function NodeList Null Number Object RegExp String TextNode Undefined Window'.split(' ');for(var i=types.length;i--;){var b=types[i],constructor=window[b];if(constructor){try{toStrings[toString.call(new constructor)]=b.toLowerCase()}catch(e){}}}return function(a){return a==null&&(a===undefined?r:'null')||a.nodeType&&nodeTypes[a.nodeType]||typeof a.length=='number'&&(a.callee&&q||a.alert&&'window'||a.item&&'nodelist')||toStrings[toString.call(a)]}})();var t=function(a){return s(a)=='function'};var u=function(b,c,d){var f=b[c];if(d.type!=o&&!t(f)){var oldObject=f;f=function(){var a=arguments.length>0?q+'[0]':'';for(var i=1;i<arguments.length;i++){a+=','+q+'['+i+']'}return eval('oldObject('+a+');')}}var h;if(d.type==j||d.type==k||d.type==l)h=function(){var a,exceptionThrown=null;try{a=f.apply(this,arguments)}catch(e){exceptionThrown=e}if(d.type==j)if(exceptionThrown==null)a=d.value.apply(this,[a,c]);else throw exceptionThrown;else if(d.type==k&&exceptionThrown!=null)a=d.value.apply(this,[exceptionThrown,c]);else if(d.type==l)a=d.value.apply(this,[a,exceptionThrown,c]);return a};else if(d.type==m)h=function(){d.value.apply(this,[arguments,c]);return f.apply(this,arguments)};else if(d.type==o)h=function(){return d.value.apply(this,arguments)};else if(d.type==n){h=function(){var a={object:this,args:Array.prototype.slice.call(arguments)};return d.value.apply(a.object,[{arguments:a.args,method:c,proceed:function(){return f.apply(a.object,a.args)}}])}}h.unweave=function(){b[c]=f;pointcut=b=h=f=null};b[c]=h;return h};var v=function(a,b,c){var d=[];for(var f in a){var g=null;try{g=a[f]}catch(e){}if(g!=null&&f.match(b.method)&&t(g))d[d.length]={source:a,method:f,advice:c}}return d};var w=function(a,b){var c=typeof(a.target.prototype)!=r?a.target.prototype:a.target;var d=[];if(b.type!=o&&typeof(c[a.method])==r){var e=v(a.target,a,b);if(e.length==0)e=v(c,a,b);for(var i in e)d[d.length]=u(e[i].source,e[i].method,e[i].advice)}else{d[0]=u(c,a.method,b)}return p?d:d[0]};$.aop={after:function(a,b){return w(a,{type:j,value:b})},afterThrow:function(a,b){return w(a,{type:k,value:b})},afterFinally:function(a,b){return w(a,{type:l,value:b})},before:function(a,b){return w(a,{type:m,value:b})},around:function(a,b){return w(a,{type:n,value:b})},introduction:function(a,b){return w(a,{type:o,value:b})},setup:function(a){p=a.regexMatch}}})()");
console.debug('plugin initialization complete');
var LOCAL_STORAGE_KEY_LAST_READ_TWEET_ID = 'lastReadTweetID';
var TWITTER_ATTRIBUTE_TWEET_ID = 'data-item-id';
var MAX_FETCH_MORE = 3;
var hasScrolled = false;
var isCurrentlyEnhancing = false;
function getLastReadTweetID() {
console.debug('retrieving last read tweet ID: ' + localStorage.getItem(LOCAL_STORAGE_KEY_LAST_READ_TWEET_ID));
return localStorage.getItem(LOCAL_STORAGE_KEY_LAST_READ_TWEET_ID);
}
function setLastReadTweet(tweetID) {
console.debug('setting last read tweet ID to: ' + tweetID);
localStorage.setItem(LOCAL_STORAGE_KEY_LAST_READ_TWEET_ID, tweetID);
}
function executeWithLastReadTweet(callback, fetchCycleCounter) {
if(!fetchCycleCounter) fetchCycleCounter = 0;
console.debug('Searching for last read tweet, currently on iteration ' + fetchCycleCounter + '/' + MAX_FETCH_MORE);
foundTweet = $('div.stream-item[data-item-id="' + getLastReadTweetID() + '"]').get(0);
if(foundTweet) {
console.debug('found: ' + foundTweet);
callback($(foundTweet));
} else if(!foundTweet && fetchCycleCounter < MAX_FETCH_MORE) {
console.debug('fetching more...');
twttr.app.currentPage().streamManager.getMoreOldItems(function() {executeWithLastReadTweet(callback, fetchCycleCounter+1);});
} else {
console.debug('giving up search');
}
}
function onMarkReadActionClick() {
var tweet = $(this).parents('div.stream-item');
var tweetID = tweet.attr(TWITTER_ATTRIBUTE_TWEET_ID);
console.log('marking as read until ' + tweetID);
setLastReadTweet(tweetID);
blurOutReadTweets(tweet);
}
function onJumpHomeClick() {
executeWithLastReadTweet(function(lastReadTweet) {
scrollToTweet(lastReadTweet.next());
});
}
function blurOutReadTweets(lastReadTweet) {
if(lastReadTweet) {
console.log('blurring from ' + lastReadTweet.attr(TWITTER_ATTRIBUTE_TWEET_ID));
lastReadTweet.nextAll().andSelf().css('opacity', '0.3');
// in case the user is trying to 'mark unread'
lastReadTweet.prevAll().css('opacity', '1.0');
} else {
alert('last read tweet not found');
}
}
function scrollToTweet(tweet) {
if(tweet) {
console.log('scrolling to ' + tweet.attr(TWITTER_ATTRIBUTE_TWEET_ID));
$.scrollTo(tweet, {
axis: 'y', // no horizontal scrolling please
duration: 1500, // scrolling should be visible to give the user feedback on the current situation
easing: 'swing', // just makes it look a bit nicer
offset: -window.innerHeight // we want that tweet to be at the *bottom* of the screen
});
}
}
function enhanceWithAction(result) {
if(!isCurrentlyEnhancing) {
isCurrentlyEnhancing = true;
console.group('enhancing');
// add the new actions
$('span.tweet-actions:not(.enhanced)')
// make sure they are only enhanced once
.addClass('enhanced')
// add the 'mark read until here' action
.append('<a class="mark-read-action" href="#"><span><i></i><b>Mark read until here</b></span></a>')
// add the click handlers
.children('a.mark-read-action').click(onMarkReadActionClick);
console.groupEnd();
executeWithLastReadTweet(function(lastReadTweet) {
// blur out read tweets
blurOutReadTweets(lastReadTweet);
// on the first load, also scroll to the last unread tweet (plus one, so we see where the list of unread tweets ends)
if(!hasScrolled) {
scrollToTweet(lastReadTweet.next());
hasScrolled = true;
}
});
isCurrentlyEnhancing = false;
}
return result;
}
function initHooks() {
// hook into the tweet loading
$.aop.after({target: twttr.app.currentPage().streamManager.streams.current, method: 'onShowOldItems'}, enhanceWithAction);
$.aop.after({target: twttr.app.currentPage().streamManager.streams.current, method: 'onShowNewItems'}, enhanceWithAction);
// the logo is used to just jump to your current reading state
$('#logo').unbind('click').click(onJumpHomeClick);
}
initHooks();
}
@badp
Copy link
Copy Markdown
Author

badp commented Nov 26, 2010

This userscript developed by Henrik Heimbuerger. Latest version.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment