Twitter - Replies Context

By noriaki Last update Dec 25, 2007 — Installed 113 times. Daily Installs: 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 1, 0

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

// ==UserScript==
// @name           Twitter - Replies Context
// @namespace      http://blog.fulltext-search.biz/
// @description    Twitter - Viewer Replies Context: Usage: click replies-side image.
// @license        The MIT License
// @version        0.1.5
// @released       2007-12-17 14:00:00
// @updated        2007-12-21 23:00:00
// @conpatible     Greasemonkey
// @include        http://twitter.com/*
// @exclude        http://twitter.com/*/friends
// @exclude        http://twitter.com/account/*
// @exclude        http://twitter.com/badges/
// @exclude        http://twitter.com/devices
// @exclude        http://twitter.com/direct_messages
// @exclude        http://twitter.com/followers
// @exclude        http://twitter.com/friends
// @exclude        http://twitter.com/help/*
// @exclude        http://twitter.com/invitations
// @exclude        http://twitter.com/tos
// ==/UserScript==

// XPath
var hasRepliesContentExp = [
    'self::node()/descendant::*[local-name() = "span" or local-name() = "SPAN"]',
    '[contains(concat(" ",@class," "), " entry-title ")',
    ' and ',
    'contains(concat(" ",@class," "), " entry-content ")',
    ' and ',
    'count(descendant::*[(local-name() = "a" or local-name() = "A")',
    ' and ',
    'starts-with(@href, "/")',
    '])>0]'
].join('');
var metaExp = [
    'self::node()/following-sibling::*[(local-name() = "span" or local-name() = "SPAN")',
    ' and ',
    'contains(concat(" ",@class," "), " meta ")',
    ' and ',
    'contains(concat(" ",@class," "), " entry-meta ")]'
].join('');
var replyLinkExp = [
    'self::node()/descendant::*[(local-name() = "a" or local-name() = "A")',
    ' and ',
    'starts-with(@href, "/")]'
].join('');
var publishedExp = [
    'self::node()/descendant::*[(local-name() = "abbr" or local-name() = "ABBR")',
    ' and ',
    'contains(concat(" ",@class," "), " published ")]'
].join('');

// Application Class
// -- Controller
function RepliesContext() {};
RepliesContext.Controller = function() {};
RepliesContext.Controller.getUpdates = function(obj) {
    GM_xmlhttpRequest({
        method: 'GET',
        url: 'http://twitter.com/statuses/user_timeline/' + obj.name + '.json',
        onload: function(res) {
            var div = new RepliesContext.View.context();
            var updates = eval('('+res.responseText+')');
            if(updates.error) {
                RepliesContext.View.error(updates, obj);
                obj.icon.src = obj.icon_src;
                return;
            }
            //var ctxs = [];
            for(var i=j=0,len=updates.length; i<len && j<3; i++) {
                if(obj.published > (new Date(updates[i].created_at))) {
                    //ctxs.push(updates[i]);
                    div.contexts.push(updates[i]);
                    j++;
                }
            }
            div.parentTD = obj.parentTD;
            if(div.parent = obj.parent)
                div.parent.children.push(div);
            if(div.contexts.length > 0) {
                div.success();
            } else {
                div.contexts = updates[0];
                div.nonexistence_contexts();
            }
            //RepliesContext.View.contexts(ctxs, obj);
            obj.icon.src = obj.icon_src;
        },
        onerror: function(res) { GM_log(res.status + ':' + res.responseText); }
    });
};
RepliesContext.Controller.insert = function(context, parent) {
    context = context.length ? context[0] : context;
    var spans = $x(hasRepliesContentExp, context);
    spans.forEach(function(span) {
        var meta = $x(metaExp, span)[0];
        var replies = $x(replyLinkExp, span);
        replies.forEach(function(reply) {
            var icon = RepliesContext.View.icon();
            icon.addEventListener('click', function(event) {
                var link = new Object();
                link.icon = this;
                link.icon_src = this.src;
                this.src = RepliesContext.View.loading();
                link.name = reply.textContent;
                link.published = parseDate($x(publishedExp, meta)[0].title);
                link.meta = meta;
                link.parent = parent;
                link.parentTD = span.parentNode.parentNode;
                link.dimension = { x: event.pageX, y: event.pageY };
                RepliesContext.Controller.getUpdates(link);
            }, false);
            reply.parentNode.insertBefore(icon, reply.nextSibling);
        });
    });
};
// -- View
RepliesContext.View = function() {};
RepliesContext.View.icon = function() {
    var icon = document.createElement('img');
    icon.src = [
        'data:image/gif;base64,',
        'R0lGODlhDgAOANU8AH2Un0Ws4Z+wuTWk3XWNmSWb2Uuv4rG/x6vh+nPE7Gi/6ZPV9Fd0gmi+6keu',
        '4YqfqoSZpJSnsmuFklGy5IufqSGZ2GF9ikhod1ZygExreoqfqWaBjUet4YSapSSc2Smd2ofP8Z+w',
        'uktqeYfP8kWs4BuW11Ky45SosYCAgFOz5JTV9JPV9Wi+6XLE7ByW1oSapIufqjaj3FO05SWc2ZSn',
        'sYjP8YfO8Sie2nHD7IbO8RGR1D09Pf///wAAAAAAAAAAACH5BAEAADwALAAAAAAOAA4AAAaAQJ6Q',
        't9sNj8cdjGAsIomaw4OxsyWMSQKFOUKAUtjhjpHZJWotTpjYKaJ2Msd6dxJEADtccYDdQQQHAjs5',
        'CzksATNNADQheSoIKzgGHzpFEgAvOw2EOQokBZU7GBYbOyYNOAoTMS6VRBciOwMBBgEDFTquREU7',
        'BTceJblrYrxOPEEAOw=='
    ].join('');
    icon.className = 'rc-icon';
    icon.alt = icon.title = '縺薙�Reply縺ョ譁�ц繧定ヲ九k';
    return icon;
};
RepliesContext.View.loading = function() {
    return  [
        'data:image/gif;base64,',
        'R0lGODlhEAAQAPIAAP///weq9sPq/IXU+geq9gAAAAAAAAAAACH/C05FVFNDQVBFMi4wAwEAAAAh',
        '/hpDcmVhdGVkIHdpdGggYWpheGxvYWQuaW5mbwAh+QQJCgAAACwAAAAAEAAQAAADGwi6MjRiSenI',
        'm9hqPOvljAOBZGmeaKqubOu6CQAh+QQJCgAAACwAAAAAEAAQAAADHAi63A5ikCEek2TalftWmPZF',
        'U/WdaKqubOu+bwIAIfkECQoAAAAsAAAAABAAEAAAAxwIutz+UIlBhoiKkorB/p3GYVN1dWiqrmzr',
        'vmkCACH5BAkKAAAALAAAAAAQABAAAAMbCLrc/jDKycQgQ8xL8OzgBg6ThWlUqq5s604JACH5BAkK',
        'AAAALAAAAAAQABAAAAMbCLrc/jDKSautYpAhpibbBI7eOEzZ1l1s6yoJACH5BAkKAAAALAAAAAAQ',
        'ABAAAAMaCLrc/jDKSau9OOspBhnC5BHfRJ7iOXAe2CQAIfkECQoAAAAsAAAAABAAEAAAAxoIutz+',
        'MMpJ6xSDDDEz0dMnduJwZZulrmzbJAAh+QQJCgAAACwAAAAAEAAQAAADGwi63P4wRjHIEBJUYjP/',
        '2dZJlIVlaKqubOuyCQAh+QQJCgAAACwAAAAAEAAQAAADHAi63A5ikCEek2TalftWmPZFU/WdaKqu',
        'bOu+bwIAOwAAAAAAAAAAAA=='
    ].join('');
};
RepliesContext.View.context = function() { this.initialize.apply(this, arguments); };
RepliesContext.View.context.prototype = {
    initialize: function() {
        this.element = $N('div', { class: 'rc-contexts'    });
        this.contexts = [];
        this.children = [];
        this.parent = null;
        this.icon = null;
    },

    success: function() {
        var tbody = document.createElement('tbody');
        var table = $N('table', {
            class: 'doing',
            cellspacing: 0,
            style: {
                border: '3px solid #e0e0e0'
            }
        }, [
            tbody
        ]);
        this.contexts.forEach(function(ctx) {
            var source = document.createElement('span');
            source.innerHTML = ctx.source;
            var text = $N('span', { class: 'entry-title entry-content' });
            text.innerHTML = replaceLink(ctx.text);
            var row = $N('tr', { class: 'hentry' }, [
                $N('td', { class: 'thumb vcard author' }, [
                    $N('a', {
                        class: 'url',
                        href: 'http://twitter.com/' + ctx.user.screen_name
                    }, [
                        $N('img', {
                            class: 'photo fn',
                            src: ctx.user.profile_image_url
                        })
                    ])
                ]),
                $N('td', { class: 'content' }, [
                    $N('strong', {}, [
                        $N('a', {
                            title: ctx.user.name,
                            href: 'http://twitter.com/' + ctx.user.screen_name
                        }, ctx.user.screen_name)
                    ]),
                    text,
                    $N('span', { class: 'meta entry-meta' }, [
                        $N('a', {
                            class: 'entry-date',
                            rel: 'bookmark',
                            href: 'http://twitter.com/' + ctx.user.screen_name + '/statuses/' + ctx.id
                        }, [
                            $N('abbr', {
                                class: 'published',
                                title: ctx.created_at
                            }, (new Date(ctx.created_at)).toString())
                        ]),
                        'from',
                        source
                    ])
                ])
            ]);
            tbody.appendChild(row);
        });
        this.element.appendChild(table);
        this.setPosition();
        this.render();
        this.addCloseButton();
        this.parentTD.style.backgroundColor = '#F9F6BA';
        RepliesContext.Controller.insert(this.element, this);
    },

    nonexistence_contexts: function() {
        var ctx = this.contexts;
        this.element.appendChild(
            $N('h4', {}, 'Sorry, this reply was so old that you could not reach.')
        );
        this.element.appendChild(
            $N('p', {}, [
                '@',
                $N('a', {
                    title: ctx.user.name,
                    href: 'http://twitter.com/' + ctx.user.screen_name
                }, ctx.user.screen_name)
            ])
        );
        this.addCloseButton();
        this.setPosition();
        this.render();
    },

    nonexistence_user: function() {
        //TODO
    },

    protected: function() {
        //TODO
    },

    addCloseButton: function() {
        var close = $N('a', {
            title: 'Close',
            href: 'javascript:void(0);',
            class: 'rc-close'
        }, 'x');
        var self = this;
        close.addEventListener('click', function(e) {
            self.parentTD.style.backgroundColor = '#fff';
            self.remove_child();
            this.parentNode.parentNode.removeChild(this.parentNode);
        }, false);
        this.element.appendChild(close);
    },

    setPosition: function() {
        var elm = elementPosition(this.parentTD);
        with(this.element.style) {
            left = elm.__x + 35 + 'px';
            top = elm.__y + elm.offsetHeight - 5 + 'px';
            width = elm.offsetWidth + 'px';
        };
    },

    render: function() {
        document.getElementById('content').appendChild(this.element);
    },

    remove_child: function() {
        var children = this.children;
        if(children.length == 0) return;
        children.forEach(function(child) {
            child.remove_child();
            if(child.element && child.element.parentNode)
                child.element.parentNode.removeChild(child.element);
        });
        if(this.parent) {
            var self = this;
            var tmp = [];
            this.parent.children.forEach(function(child) {
                if(child != self) tmp.push(child);
            });
            this.parent.children = tmp;
        }
    }
};
RepliesContext.View.error = function(res, obj) {
    var parent_div = obj.meta.parentNode.parentNode.parentNode.parentNode;
    var div = $N('div', {
        class: 'rc-contexts',
        style: {
            left: parent_div.offsetLeft + 35 + 'px',
            top: obj.dimension.y + 30 + 'px',
            width: parent_div.offsetWidth + 'px',
            zIndex: '10'
        }
    }, [
        $N('h4', {}, res.error),
        $N('p', {}, [
            '@',
            $N('a', {
                href: 'http://twitter.com/' + obj.name
            }, obj.name)
        ])
    ]);
    var close = $N('a', {
        title: 'Close',
        href: 'javascript:void(0);',
        class: 'rc-close'
    }, 'x');
    close.addEventListener('click', function(e) {
        this.parentNode.parentNode.removeChild(this.parentNode);
    }, false);
    div.appendChild(close);
    $x('//div[@class="tab"]')[0].appendChild(div);
};

// Style
GM_addStyle(<><![CDATA[
    .rc-icon {
        cursor: pointer;
        margin-left: 0.2em;
        vertical-align: top;
    }
    .rc-contexts {
        position: absolute;
        background-color: #fff;
        border: #3a3 1px solid;
        z-index: 3;
    }
    .rc-contexts > h4 {
        padding-left: 3px;
    }
    .rc-close {
        background-color: #f88;
        border: 1px solid #fff;
        color: #fff;
        font-weight: bold;
        line-height: 1;
        padding: 0.18em 0.25em;
        position: absolute;
        right: 4px;
        text-decoration: none;
        top: 4px;
    }
]]></>);

// Main
var i=4;
function addFilter() {
    if(window.AutoPagerize && window.AutoPagerize.addFilter) {
        window.AutoPagerize.addFilter(RepliesContext.Controller.insert);
    } else if(i-- > 0) {
        setTimeout(arguments.callee, 500);
    }
}
RepliesContext.Controller.insert(document);
addFilter();

// Utility
function $x(xpath, context) {
    context = context || document;
    var res = document.evaluate(xpath, context, null, XPathResult.UNORDERED_NODE_ITERATOR_TYPE, null);
    for(var i, nodes = [] ; i=res.iterateNext() ; nodes.push(i));
    return nodes;
}

function $N(name, attr, childs) {
    var ret = document.createElement(name);
    for(k in attr) {
        if(!attr.hasOwnProperty(k)) continue;
        if(k == "class") {
            ret.className = attr[k];
        } else if(k == "style" && typeof(attr[k]) == "object") {
            for(j in attr[k]) ret.style[j] = attr[k][j];
        } else {
            ret.setAttribute(k, attr[k]);
        }
    }
    switch(typeof childs) {
    case "string": {
        ret.appendChild(document.createTextNode(childs));
        break;
    }
    case "object": {
        for(var i=0,len=childs.length; i<len; i++) {
            var child = childs[i];
            if(typeof child == "string") {
                ret.appendChild(document.createTextNode(child));
            } else {
                ret.appendChild(child);
            }
        }
        break;
    }
    }
    return ret;
}

function elementPosition(elem) {
    var targetElem = elem;
    for(elem.__x = elem.__y = 0; targetElem && targetElem.id != 'container'; targetElem = targetElem.offsetParent) {
        elem.__x += targetElem.offsetLeft;
        elem.__y += targetElem.offsetTop;
    }
    //var body = document.getElementsByTagName('body')[0];
    //elem.__x += 2 * (parseInt(window.getComputedStyle(body, "").getPropertyValue("border-left-width")) || 0);
    //elem.__y += 2 * (parseInt(window.getComputedStyle(body, "").getPropertyValue("border-top-width")) || 0);
    return elem;
}

function replaceLink(text) {
    text = text.replace(/@([a-zA-Z0-9_]+)/g, "@<a href=\"/$1\">$1</a>");
    text = text.replace(/(http[s]?:\/\/[;/?:@&=+\$,A-Za-z0-9\-_.!~*'()%]+)/g, "<a href=\"$1\">$1</a>") //']));
    return text;
}

/*
function escapeHTML(text) {
    var ESCAPE = {
        '&': '&amp;',
        '<': '&lt;',
        '>': '&gt;',
        '"': '&quot;',
        "'": '&#039;'
    }
    var result = '', source = text, match;
    while(source.length > 0) {
        if(match = source.match(/[&<>"']/)) {  //'"]/)) {
            result += source.slice(0, match.index);
            result += ESCAPE[match[0]];
            source = source.slice(match.index + match[0].length);
        } else {
            result += source, source = '';
        }
    }
    return result;
}
*/

function timeDifference() {
    var dd = new Date();
    return( ( dd.getHours() - dd.getUTCHours() ) % 24 );
}

// based on the W3CDTF2Date
// http://www.kawa.net/works/js/date/w3cdtf.html
// Date/W3CDTF.js -- W3C Date and Time Formats
/* COPYRIGHT AND LICENSE

Copyright (c) 2005-2006 Yusuke Kawasaki. All rights reserved.
This program is free software; you can redistribute it and/or
modify it under the Artistic license. Or whatever license I choose,
which I will do instead of keeping this documentation like it is.
*/

Date.W3CDTF = function ( dtf ) {
    var dd = new Date();
    dd.setW3CDTF = Date.W3CDTF.prototype.setW3CDTF;
    dd.getW3CDTF = Date.W3CDTF.prototype.getW3CDTF;
    if ( dtf ) dd.setW3CDTF( dtf );
    return dd;
};

Date.W3CDTF.VERSION = "0.04";

Date.W3CDTF.prototype.setW3CDTF = function( dtf ) {
    var sp = dtf.split( /[^0-9]/ );

    // invalid format
    if ( sp.length < 6 || sp.length > 8 ) return;

    // invalid time zone
    if ( sp.length == 7 ) {
        if ( dtf.charAt( dtf.length-1 ) != "Z" ) return;
    }

    // to numeric
    for( var i=0; i<sp.length; i++ ) sp[i] = sp[i]-0;

    // invalid range
    if ( sp[0] < 1970 ||                // year
         sp[1] < 1 || sp[1] > 12 ||     // month
         sp[2] < 1 || sp[2] > 31 ||     // day
         sp[3] < 0 || sp[3] > 23 ||     // hour
         sp[4] < 0 || sp[4] > 59 ||     // min
         sp[5] < 0 || sp[5] > 60 ) {    // sec
        return;                         // invalid date
    }

    // get UTC milli-second
    var msec = Date.UTC( sp[0], sp[1]-1, sp[2], sp[3], sp[4], sp[5] );

    // time zene offset
    if ( sp.length == 8 ) {
        if ( dtf.indexOf("+") < 0 ) sp[6] *= -1;
        if ( sp[6] < -12 || sp[6] > 13 ) return;    // time zone offset hour
        if ( sp[7] < 0 || sp[7] > 59 ) return;      // time zone offset min
        msec -= (sp[6]*60+sp[7]) * 60000;
    }

    // set by milli-second;
    return this.setTime( msec );
};

Date.W3CDTF.prototype.getW3CDTF = function() {
    var year = this.getFullYear();
    var mon  = this.getMonth()+1;
    var day  = this.getDate();
    var hour = this.getHours();
    var min  = this.getMinutes();
    var sec  = this.getSeconds();

    // time zone
    var tzos = this.getTimezoneOffset();
    var tzpm = ( tzos > 0 ) ? "-" : "+";
    if ( tzos < 0 ) tzos *= -1;
    var tzhour = tzos / 60;
    var tzmin  = tzos % 60;

    // sprintf( "%02d", ... )
    if ( mon  < 10 ) mon  = "0"+mon;
    if ( day  < 10 ) day  = "0"+day;
    if ( hour < 10 ) hour = "0"+hour;
    if ( min  < 10 ) min  = "0"+min;
    if ( sec  < 10 ) sec  = "0"+sec;
    if ( tzhour < 10 ) tzhour = "0"+tzhour;
    if ( tzmin  < 10 ) tzmin  = "0"+tzmin;
    var dtf = year+"-"+mon+"-"+day+"T"+hour+":"+min+":"+sec+tzpm+tzhour+":"+tzmin;
    return dtf;
};

function parseDate(string) {
    var date = new Date(string);
    if(date == 'Invalid Date') date = new Date.W3CDTF(string);
    return date;
}

// Auto Update Checking
function UpdateChecker() {};
UpdateChecker.prototype = {
    script_name: 'Twitter - Replies Context',
    script_url: 'http://blog.fulltext-search.biz/files/twitterrepliescontext.user.js',
    current_version: '0.1.5',
    more_info_url: 'http://blog.fulltext-search.biz/pages/twitter-replies-context',

    remote_version: null,

    // Render update information in HTML
    render_update_info: function() {
        var self = this;
        var new_version = $N('div', {
            class: 'yellow-box',
            id: 'gm_update_alert'
        }, [
            $N('h3', {}, 'Your Greasemonkey UserScript Update Available'),
            $N('p', {}, [
                [
                    'There is an update available for "',
                    this.script_name,
                    '".'
                ].join(''),
                $N('br',{}),
                [
                    'You are currently running version ',
                    this.current_version,
                    '. The newest version is ',
                    this.remote_version,
                    '.'
                ].join('')]),
            $N('h4', {}, 'What do your want to do?')
        ]);

        var update_link = $N('a', {
            id: 'gm_update_alert_link',
            href: this.script_url
        }, [
            'Update to version ',
            this.remote_version
        ].join(''));
        update_link.addEventListener('click', function(e) {
            var u = document.getElementById('gm_update_alert');
            u.parentNode.removeChild(u);
        }, false);

        var nonupdate_link = $N('a', { href: 'javascript:void(0);' },
            'Don\'t remind me again until tomorrow');
        nonupdate_link.addEventListener('click', function(e) {
            GM_setValue('last_check_day', self.days_since_start());
            var u = document.getElementById('gm_update_alert');
            u.parentNode.removeChild(u);
        }, false);

        new_version.appendChild(update_link);
        new_version.appendChild(nonupdate_link);

        if(this.more_info_url) {
            new_version.appendChild(
                $N('a', {
                    href: this.more_info_url,
                }, 'Show Update Info')
            );
        }

        var after_sibling = document.getElementById('timeline');
        after_sibling.parentNode.insertBefore(new_version, after_sibling);
    },

    add_update_info_style: function() {
        GM_addStyle(<><![CDATA[
            /* style(like CSS) for update information */
            #gm_update_alert p {
                margin: 0pt;
            }

            #gm_update_alert a:link {
                text-decoration: underline;
            }

            #gm_update_alert > a:link {
                margin: 0.5em 1em 0pt 1em;
            }

            #gm_update_alert h4 + a:link {
                font-weight: bold;
            }
        ]]></>);
    },

    // Check script update remote
    check_update: function() {
        if(!this.has_need_for_check) return;
        var user_script = this;
        GM_xmlhttpRequest({
            method: 'GET',
            url: this.script_url,
            onload: function(res) {
                user_script.remote_version = user_script.check_version(res.responseText);
                if(user_script.remote_version && user_script.remote_version > user_script.current_version) {
                    user_script.add_update_info_style();
                    user_script.render_update_info();
                } else {
                    GM_setValue('last_check_day', user_script.days_since_start());
                }
            },
            onerror: function(res) { GM_log(res.status + ':' + res.responseText); }
        });
    },

    // Check the necessity for update: [Boolean]
    // return [true] if necessary
    has_need_for_check: function() {
        var last_check_day = GM_getValue('last_check_day');
        var current_day = this.days_since_start();
        if(typeof last_check_day == 'undefined' || current_day > last_check_day) {
            return true;
        } else {
            return false;
        }
    },

    // Check version in remote script file: [String]
    check_version: function(string) {
        if(/\/\/\s?@version\s+([\d.]+)/.test(string)) {
            return RegExp.$1;
        } else {
            return null;
        }
    },

    days_since_start: function() {
        var DAYS_IN_MONTH = [31,59,90,120,151,181,212,243,273,304,334,365];
        var now = new Date();
        return(now.getYear() * 365 + DAYS_IN_MONTH[now.getMonth()] + now.getDate());
    }
};

var user_script = new UpdateChecker();
user_script.check_update();