Reddit visited link remover

hides links you already visited on reddit and offers keyboard navigatin

// ==UserScript==
// @name            Reddit visited link remover
// @version         0.9.5
// @namespace       sxxe@gmx.de
// @description     hides links you already visited on reddit and offers keyboard navigatin
// @include         *.reddit.com/*
// @exclude         *.reddit.com/r/*/comments/*
// @exclude         *.reddit.com/user/*
// @exclude         *.reddit.com/message/*
// @exclude         *.reddit.com/reddits/*
// @exclude         *.reddit.com/prefs/*
// @require         http://ajax.googleapis.com/ajax/libs/jquery/2.1.0/jquery.min.js

// ==/UserScript==

// Keyboard Shortcuts:
//
// - h: Open next unread post in new tab
// - j: Jump to next unread post and mark it as read (+minimize)
// - shift + j: Jump to next post (even minimized)
// - k: Jump to previous unread post and mark it as read (+minimize)
// - shift + k: Jump to previous post (even minimized)
//
// - l: Open comments of active post
// - u: up vote active post
// - m: down vote active post
//

var cached_links_time = 60*60*24*2; // Two days - how long should visited links be saved
var cached_links_count = 1000; // Only save this amount of links (due to performance reasons)
var fade_time = 750; // time it takes until the visited link is completely invisible
var show_footer = true; // show how many links are currently cached at the buttom of reddit.com
var toggle_links_button = true; // show button to show / hide links (hide_links has to be true)
var hide_links = false; // removes links completely (after the next page reload)
var add_manual_hide_link = true; // adds a link to the frontpage to manually hide posts
var activate_popup = false; // activate popup functionality

// ----------------------------------------------------------

// Add config style
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);
}
addGlobalStyle('.rvlr_marker { color: #FFF !important; background-color: #FF4500; border-radius: 50%; text-align: center !important; padding: 2px; }');

if (toggle_links_button && hide_links) {
    $("body").append ('<center><div id="show_links" class="show_links"><button>Toggle links</button></div> <br></center>');
}

if (add_manual_hide_link) {
    $('ul.flat-list.buttons').append('<li><a class="manHideLink" href="#">mark as read</a></li>');
}

var stateMachine;
window.addEventListener ("load", function () {

        stateMachine = new GM_LinkTrack();

        if (show_footer) {
            var numLinks    = stateMachine.GetVisitedLinkCount();
            $("body").append ('<center><p>' + numLinks + ' links cached. (max '+ cached_links_count + ')</p><br></center>');
        }

    },
    false
);

var activeElement;
document.addEventListener("keypress", function(e) {

    if(!e) e=window.event;

    var isShift = e.shiftKey;

    var key = e.which;

    //console.log('key: '+key+' isShift: '+isShift);

    // http://www.w3schools.com/jsref/event_key_which.asp

    if ((key == 106) || (key == 74)) { // j key
        // shrink next unshrinked link
        var element;
        if (isShift){
            element = getElementAfterActive();
        } else {
            element = getCorrectElement();
        }
        //console.log("element: " + element + "e0: " + element[0]);
        if ( typeof element === "undefined" ) {
            nextPage();
        } else if ( typeof element[0] === "undefined" ) {
            nextPage();
        }

        stateMachine.LinkIsNewPub(element);
        setActiveElement(element);

    } else if (key == 104) { // h key
        // pop disable, open link in new tab
        var element = getCorrectElement();
        stateMachine.LinkIsNewPub(element);
        setActiveElement(element);
        openLink(element);

    } else if ((key == 107) || (key == 75)) { // k key
        // open previous link in popup

        var element;
        if (isShift){
            element = getElementBeforeActive();
        } else {
            element = getPrevUnreadLink();
        }

        if(element.length === 0) {
            return 0;
        }

        stateMachine.LinkIsNewPub(element);
        setActiveElement(element);

    } else if (key == 117) { // u key
        // up vote active post
        upVote(getActiveElement());
    } else if (key == 109) { // m key
        // down vote active post
        downVote(getActiveElement());
    }  else if (key == 108) { // l key
        // open comments of active post
        openLinkComments(getActiveElement());
    }

}, true);

function setActiveElement(elm) {
    resetActiveElement();
    activeElement = elm;
    activeElement.addClass('rvlr_active');
    activeElement.closest('div.thing:visible:not(.promoted)').find(".rank").addClass('rvlr_marker');
}

function resetActiveElement() {
    if (activeElement) {
        activeElement.removeClass('rvlr_active');
        activeElement.closest('div.thing:visible:not(.promoted)').find(".rank").removeClass('rvlr_marker');
    };
}

function getActiveElement() {
    return activeElement;
}

function isActiveElement(elm) {
    if (elm.hasClass('rvlr_active')) {
        return true;
    }
    return false;
}

function getCorrectElement() {
    var element;

    var aElement = getActiveElement();
    if (typeof aElement !== "undefined") {
        element = getNextUnreadLink();
    }

    if (typeof element === "undefined") {
        element = getFirstUnreadLink();
    }

    return element;
}


function nextPage() {
    $('a[rel*=next]')[0].click();
}

function getNextUnreadLink() {

    var element = getActiveElement();
    if (typeof element !== "undefined") {
        var elem = element.closest('div.thing:not(.promoted)').nextAll('div.thing:not(.shrinked):not(.promoted)').eq(0).find('a.title');
        return elem;
    }
    return 0;
}

function getPrevUnreadLink() {

    var element = getActiveElement();
    if (typeof element !== "undefined") {
        var elem = element.closest('div.thing:not(.promoted)').prevAll('div.thing:not(.shrinked):not(.promoted)').eq(0).find('a.title');
        console.log(elem);
        return elem;
    }
    return 0;
}

function getFirstUnreadLink() {
    var currentElement;
    $('#siteTable a.title').each(function() {
        if ( ($(this).closest('div.thing').hasClass('promoted') === false) && ($(this).closest('div.thing').hasClass('shrinked') === false) && ($(this).closest('div.thing').is(":hidden") === false) )  {
            currentElement = $(this);
            return false;
        }
    });
    //console.log(currentElement);
    return currentElement;
}

function getLastShrinkedLink() {
    var element;
    $('#siteTable a.title').each(function() {
        if ( ($(this).closest('div.thing').hasClass('promoted') === false) && ($(this).closest('div.thing').hasClass('shrinked') === true) && ($(this).closest('div.thing').is(":hidden") === false) )  {
            element = $(this);
        }
    });
    return element;
}

function getElementBeforeActive() {
    var element;

    element = $('.rvlr_active').closest('div.thing:not(.promoted)').prevAll('div.thing:visible:not(.promoted)').eq(0).find('a.title');

    return element;
}

function getElementAfterActive() {
    var element;

    element = $('.rvlr_active').closest('div.thing:not(.promoted)').nextAll('div.thing:visible:not(.promoted)').eq(0).find('a.title');

    return element;
}

function openLink(elm) {
    var href = elm.attr('href');
    window.open(href, '_blank');
}

function getYoutubeId(url) {
    var regExp = /^.*(youtu.be\/|v\/|u\/\w\/|embed\/|watch\?v=|\&v=)([^#\&\?]*).*/;
    var match = url.match(regExp);

    if (match && match[2].length == 11) {
        return match[2];
    } else {
        return 0;
    }
}

function openLinkComments(elm) {
    var href = elm.closest('div.thing:not(.promoted)').find("a.comments").attr('href');
    //window.location = href;
    window.open(href, '_blank');
}

function upVote(elm) {
    elm.closest('div.thing:not(.promoted)').find("div.up, div.upmod").click();
}

function downVote(elm) {
    elm.closest('div.thing:not(.promoted)').find("div.down, div.downmod").click();
}

function GM_LinkTrack () {
    var visitedLinkArry = [];
    var numVisitedLinks = 0;
    var link_count = 0;
    var current_timestamp = new Date().getTime();

    var sortedLocalStorage = SortLocalStorage();

    // Get visited link-list from storage.
    for (var J = sortedLocalStorage.length - 1;  J >= 0;  --J) {

        var item = sortedLocalStorage[J];

        var four_weeks = cached_links_time*1000;

        // Get saved links
        if (/^Visited_\d+.*/i.test (item) ) {

            var regex = /^Visited_(\d+).*/;
            var old_timestamp = regex.exec(item)[1];

            var regex2 = /^Visited_\d+_(.*)/;
            var value = regex2.exec(item)[1];

            var regex3 = /^(Visited_\d+)_.*/;
            var itemName = regex3.exec(item)[1];

            //console.log(numVisitedLinks + " " + item+ " t: " + old_timestamp + " v: " + value + " n: " + itemName);

            if (value == '#') {
                localStorage.removeItem (itemName);
                break;
            }

            if (link_count >= cached_links_count) {
                localStorage.removeItem (itemName);
                //console.log(numVisitedLinks + " " + value + "t: " + timeConverter(old_timestamp));
                link_count--;
            }
            link_count++;

            // check link age
            if ( (current_timestamp - old_timestamp) < four_weeks ) {
                visitedLinkArry.push (value);
                numVisitedLinks++;

                //console.log(numVisitedLinks + " " + localStorage[itemName] + " t: " +old_timestamp[1]);

                if (hide_links) {
                    $('a[href*="' + value + '"]').closest('div.thing:not(.promoted)').fadeOut(fade_time);
                } else {
                    //$('a[href="' + localStorage[itemName] + '"]').closest('div').fadeOut(fade_time);
                    shrinkLinks($('a[href*="' + value + '"]'));
                }

            } else {
                // too old, remove from storage
                localStorage.removeItem (itemName);
            }
        }
    }

    function SortLocalStorage() {

        var localStorageArray = [];

        if(localStorage.length > 0) {
            for (var i=0; i<localStorage.length; i++){
                localStorageArray[i] = localStorage.key(i)+ "_" +localStorage.getItem(localStorage.key(i));
            }
        }

        return localStorageArray.sort();
    }

    function timeConverter(UNIX_timestamp) {
     var a = new Date(UNIX_timestamp*1000);
     var months = ['Jan','Feb','Mar','Apr','May','Jun','Jul','Aug','Sep','Oct','Nov','Dec'];
         var year = a.getFullYear();
         var month = months[a.getMonth()];
         var date = a.getDate();
         var hour = a.getHours();
         var min = a.getMinutes();
         var sec = a.getSeconds();
         var time = date+','+month+' '+year+' '+hour+':'+min+':'+sec ;
         return time;
    }

    function ShowLinks () {
        for (var J = localStorage.length - 1;  J >= 0;  --J) {
            var itemName    = localStorage.key (J);

            // Get saved links
            if (/^Visited_\d+$/i.test (itemName) ) {
                $('a[href="' + localStorage[itemName] + '"]').closest('div.thing:not(.promoted)').fadeToggle(fade_time);
            }
        }
    }

    this.LinkIsNewPub = function (linkObj) {
        LinkIsNew(linkObj);
    };

    function LinkIsNew (linkObj) {
        var href = linkObj.attr('href');

        href = href.replace(/^(http|https)\:\/\//g, '');       

        if (visitedLinkArry.indexOf (href) == -1) {
            visitedLinkArry.push (href);
        
            var timestamp = new Date().getTime();

            var itemName    = 'Visited_' + timestamp;
            localStorage.setItem (itemName, href);
            numVisitedLinks++;
            
            // Hide links imideately after klicked. Makes it impossible to see commenst afterward.
            //$('a[href="' + href + '"]').closest('div').fadeOut(fade_time);
        }
        centerView(linkObj);
        shrinkLinks(linkObj);
        setActiveElement(linkObj);

        return true;
    }

    function shrinkLinks (linkObj) {

        // Alter the look of clicked links imideately
        var mainLinkElement = linkObj.closest('div.thing:not(.promoted)');

        // Remove elements that are nor needed in the smll view
        mainLinkElement.find('p.tagline').remove();
        mainLinkElement.find('a.thumbnail').css({'opacity': '0', 'width': '0px'});
        mainLinkElement.find('a.thumbnail').hide(fade_time, function () {
            $(this).remove();
        });
        mainLinkElement.find('span.domain').remove();
        mainLinkElement.find('div.expando-button').remove();
        mainLinkElement.find('li a.manHideLink').remove();
        mainLinkElement.find('span.linkflairlabel').remove();

        // Realign elements for the small view
        var animObj = {"queue": false, "duration": fade_time};

        mainLinkElement.find('a.title').animate({'font-size': '9px', 'margin': '0px'}, animObj);
        mainLinkElement.find('p.title').css({'float': 'left', 'margin-left': '10px', 'font-size': '9px', 'font-weight': 'normal', 'margin-top': '4px', 'text-overflow': 'ellipsis', 'max-width': '300px', 'white-space': 'nowrap', 'overflow': 'hidden'});

        mainLinkElement.find('ul.flat-list.buttons').animate({'margin-left': '10px', 'font-size': '9px', 'margin-top': '0px'}, animObj);
        mainLinkElement.find('span.rank').animate({'margin-top': '1px', 'font-size': '10px'}, animObj);

        mainLinkElement.find('div.midcol').css("cssText", "width: 110px !important;"); //jquery hack to apply important
        mainLinkElement.find('div.midcol').css({'margin-top': '0px', 'margin-bottom': '0px', 'height': 'unset'});
        mainLinkElement.find('div.arrow').css({'float': 'left'});

        mainLinkElement.find('div.score').css({'float': 'left'});
        mainLinkElement.find('div.score').animate({'font-size': '10px', 'padding-left': '10px', 'padding-right': '10px', 'margin-top': '3px', 'width': '30px'}, animObj);
        mainLinkElement.find('div.entry').css({'padding': '0px'});

        //main div
        mainLinkElement.css({'padding': '0px'});
        mainLinkElement.css({'padding-left': '10px', 'opacity': '0.4', 'margin': '4px'});
        //mainLinkElement.find('p.tagline').css({'float': 'right'});
        mainLinkElement.addClass('shrinked');

        return true;
    }

    this.GetVisitedLinkCount = function () {
        return numVisitedLinks;
    };

    function centerView(elm) {

        var offsetTop = $(elm).offset().top;
        var newTopPos = offsetTop - $(window).scrollTop();
        var elemLimitPos = $(window).height() / 2;

        //console.log("offsetTop: " + offsetTop);
        //console.log("newTopPos: " + newTopPos);        
        //console.log("elemLimitPos: " + elemLimitPos);

        if ( newTopPos > elemLimitPos ) {
            $('html,body').animate({
                scrollTop: offsetTop - 50
            }, 800);
        }
    }

    $('a.title').click(function() {
        LinkIsNew($(this));
    });
 
    $('#hidesidebar').click(function(event) {
        event.preventDefault();

        $(".side").toggle();
    });

    if (toggle_links_button && hide_links) {
        $('#show_links button').click (function() {
            ShowLinks();
        });
    }

    if (add_manual_hide_link) {
        $('a.manHideLink').click (function(event) {
            event.preventDefault();
            var $titleLink = $(this).closest('div.thing:not(.promoted)').find('a.title');

            LinkIsNew($titleLink);
        });
    }
}