Old Reddit Highlight Unread Comments

On topic pages, show "X unread comments (Y total)"; on comment pages, highlight unread comments. Local storage only -- does not work across multiple computers.

You will need to install an extension such as Tampermonkey, Greasemonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey to install this script.

You will need to install an extension such as Tampermonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey or Userscripts to install this script.

You will need to install an extension such as Tampermonkey to install this script.

You will need to install a user script manager extension to install this script.

(I already have a user script manager, let me install it!)

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

(I already have a user style manager, let me install it!)

// ==UserScript==
// @name           Old Reddit Highlight Unread Comments
// @description    On topic pages, show "X unread comments (Y total)"; on comment pages, highlight unread comments. Local storage only -- does not work across multiple computers.
// @author         Xiao
// @namespace      https://greasyfork.org/users/5802
// @match          https://old.reddit.com/*
// @grant          GM_setValue
// @grant          GM_getValue
// @grant          GM_listValues
// @grant          GM_deleteValue
// @version        1.0
// ==/UserScript==

/*
todo
- navegar pelo new pra não ter que usar o teclado
- selecionar os minutos quando eu der espaço passando pelo ':'
Features:
 * Replaces "10 comments" with "5 unread comments (10 total)"
 * Unread comments are highlighted for ease of skimming
 * You can navigate through unread comments with the following hotkeys:
   Alt+Q / Alt+W -or- Ctrl+Up arrow / Ctrl+Down arrow
Mix code from:
https://greasyfork.org/scripts/8937-reddit-unread-comment-helper-fork/
https://greasyfork.org/scripts/8029-reddit-highlight-new-comments/
And a little piece of:
https://addons.mozilla.org/firefox/addon/cozy-reddit/

*/
(function(){

    var DELETE_OLDER_THAN = 1000*60*60*24*30; // Items older than 30 days
    /* Shove an item into local storage */
    function getData(id) {
        var data = GM_getValue(id);
        if (data === undefined || data.substr(0, 1) != "{")
            return null;
        return JSON.parse(data);
    }
    /* Get an item out of local storage */
    function setData(id, data) {
		console.log('unread:');
		console.log(data);
        GM_setValue(id, JSON.stringify(data));
    }
    /* Delete old items out of local storage */
    function deleteOldItems() {
        var data = GM_getValue('_last_clean_time');
        if (data !== null && Date.now() - data < 1000*60*60*24) { // Cleanup every 24 hours
            return;
        }
        var row;
        for (var key in GM_listValues()) {
            data = GM_getValue(key);
            if (data === undefined || data.substr(0, 1) != "{") {
                continue;
            }
            row = JSON.parse(data);
            if (Date.now() - row.t < DELETE_OLDER_THAN) {
                continue;
            }
            GM_deleteValue(key);
        }
        GM_setValue('_last_clean_time', Date.now());
    }
	/* Cozy Reddit */
	 function calculateCommentTop (comment) {
        var curTopPos = 0;
        if (comment.offsetParent) {
            do {
                curTopPos += comment.offsetTop;
            } while (comment = comment.offsetParent);

            return [curTopPos - 10];
        }
    }
    /* Get sorted array of unread comments offsets. */
    function getCommentsOffsetTop() {
        var comments = document.getElementsByClassName('unreadHighlighted');
        var arr = new Array(comments.length);
        for (var i = 0; i < comments.length; i++) {
            arr[i] = calculateCommentTop(comments[i]);
        }
        arr.sort(function(a, b) {
            return a - b;
        });
        return arr;
    }
    /* Jump to the next new comment. */
    function jumpToNextComment() {
        var unread = getCommentsOffsetTop();
        var scrollUnread;
        var scrollTop = window.pageYOffset || document.documentElement.scrollTop;
        for (var i = 0; i < unread.length; i++) {
            scrollUnread = unread[i];
            if (scrollUnread > scrollTop) {
                window.scrollTo(0, scrollUnread);
                break;
            }
        }
    }
    /* Jump to the previous new comment. */
    function jumpToPrevComment() {
        var unread = getCommentsOffsetTop().reverse();
        var scrollUnread;
        var scrollTop = window.pageYOffset || document.documentElement.scrollTop;
        for (var i = 0; i < unread.length; i++) {
            scrollUnread = unread[i];
            if (scrollUnread < scrollTop) {
                window.scrollTo(0, scrollUnread);
                break;
            }
        }
    }

    /*
    Handle a list page, adding "n unread comments" links etc.
    */
    function handleListPage() {
        var snap = document.evaluate("//a" +
                "[contains(concat(' ', normalize-space(@class), ' '), ' comments ')]" +
                "[contains(@href, '/comments')]",
            document, null, XPathResult.UNORDERED_NODE_SNAPSHOT_TYPE, null);

        var b36tid, row, match, comments, seen, newcomments, newlink;
        for(var elm = null, i = 0; (elm = snap.snapshotItem(i)); i++) {
            match = elm.firstChild.nodeValue.match(/(\d+) (comment|comentário)/);
            // No comments; bail early.
            if (!match)
                continue;

            comments = match[1];

            // Alphanumeric base-36 id, like "1lp5".
            b36tid = elm.getAttribute("href").match(/\/comments\/([^\/]+)/)[1];
            row = getData(b36tid);

            seen = row ? row.c : 0;

            newcomments = comments - seen;
            // Can be negative if comments are deleted.
            if (newcomments < 0) newcomments = 0;

            newlink = elm.cloneNode(false);

            var cstring = "unread comment" + (newcomments != 1 ? "s" : "");

            if (newcomments > 0) {
				nc = document.createElement('span');
				nc.style.color = "#333";
				nc.textContent = newcomments + " " + cstring;
				elm.textContent = comments > newcomments ? ' (' + comments + ' total)' : '';
				elm.insertBefore(nc, elm.childNodes[0]);
			}
        }
    }

    /*
    Handle a comments page: highlight new comments, save the ID of the highest
    comment, etc.
    */
    function handleCommentsPage() {
        var url = document.location.href.split("#");
        var frag = url.length > 1 ? url[1] : false;
        var b36tid = url[0].match(/\/comments\/([^\/]+)/)[1];

        var row = getData(b36tid);

        var partida, max_cid = 0, newmax = 0;
        if (row) {
            newmax = max_cid = row.m;
			partida = row.t;
        } else {
			partida = Date.parse(document.getElementById('siteTable').getElementsByClassName('tagline')[0].getElementsByTagName('time')[document.getElementById('siteTable').getElementsByClassName('tagline')[0].getElementsByTagName('time').length - 1].getAttribute('datetime'));
		}

        var comments, i, split, b36cid, cid;
        comments = document.getElementsByClassName('comment');
        for(i=0; i<comments.length; i++) {
            split = comments[i].className.split("_");
            if(split.length == 2)
            {
                b36cid = split[1].split(' ')[0].substr(1);
                cid = parseInt(b36cid, 36);
                if (cid > max_cid) {
					comments[i].getElementsByClassName("entry")[0].parentElement.classList.add('unreadHighlighted');
					comments[i].getElementsByClassName("entry")[0].parentElement.setAttribute('style', HNC.generate_comment_style(Date.parse(comments[i].getElementsByClassName('tagline')[0].getElementsByTagName('time')[comments[i].getElementsByClassName('tagline')[0].getElementsByTagName('time').length - 1].getAttribute('datetime')), partida));
                    if (cid > newmax) {
                        newmax = cid;
                    }
                }
            }
        }

		if (getData(document.location.href.split("#")[0].match(/\/comments\/([^\/]+)/)[1]))
			ui.create_comment_highlighter(getData(document.location.href.split("#")[0].match(/\/comments\/([^\/]+)/)[1]).t, 0);
		else
			ui.create_comment_highlighter(Date.parse(document.getElementById('siteTable').getElementsByClassName('tagline')[0].getElementsByTagName('time')[document.getElementById('siteTable').getElementsByClassName('tagline')[0].getElementsByTagName('time').length - 1].getAttribute('datetime')), 1);

		if (row)
			setTimeout(jumpToNextComment, 250);

        comments = document.getElementsByClassName('comments')[0].innerHTML.match(/\b\d+\b/);
        if (comments && comments[0] > 0) {
            setData(b36tid, {"m": newmax, "c": comments, "t": Date.now()});
        }
    }

	let HNC = {
		highlight: function (since) {
			let comments = document.getElementsByClassName('comment'),
				username
			;

			if (document.body.classList.contains('loggedin')) {
				username = document.getElementsByClassName('user')[0].firstElementChild.textContent;
			}

			for (let comment of comments) {
				/* skip removed or deleted comments */
				if (comment.classList.contains('deleted') || comment.classList.contains('spam')) {
					continue;
				}

				/* skip our own comments */
				let author = comment.getElementsByClassName('author')[0].textContent;
				if (username && username == author) {
					continue;
				}

				/* select original or edited comment time */
				let times = comment.getElementsByClassName('tagline')[0].getElementsByTagName('time'),
					time  = Date.parse(times[times.length - 1].getAttribute('datetime'))
				;

				/* add styles */
				if (time > since) {
					comment.getElementsByClassName("entry")[0].parentElement.classList.add('unreadHighlighted');
					comment.getElementsByClassName("entry")[0].parentElement.setAttribute('style', this.generate_comment_style(time, since));
				}
			}
		},

		reset_highlighting: function () {
			let comments = document.getElementsByClassName('unreadHighlighted');

			for (let i = comments.length; i > 0; i--) {
				let comment = comments[i - 1];
				comment.classList.remove('unreadHighlighted');
				comment.removeAttribute('style');
			}
		},

		generate_comment_style: function (comment_time, since) {
			let style = 'background-color: %color !important;\npadding: 0 5px;';

			style = style.replace(/\s+/g, ' ');
			style = style.replace(/%color/g, this.get_color(Date.now() - comment_time, Date.now() - since));

			return style;
		},

		get_color: function (comment_age, highlighting_since) {
			let time_diff = 1 - comment_age / highlighting_since,
				color_newer = tinycolor('hsl(214, 16, 9').toHsl(), // hsl(210, 16.7%, 9.4%)
				color_older = tinycolor('hsl(214, 16, 9').toHsl()
			;

			let color_final = tinycolor({
				h: color_older.h + (color_newer.h - color_older.h) * time_diff,
				s: color_older.s + (color_newer.s - color_older.s) * time_diff,
				l: color_older.l + (color_newer.l - color_older.l) * time_diff,
			});

			return color_final.toHslString();
		},
	};

	data = {
		comment_highlighter: function () {/*
			<div class="title" style="line-height: 20px;">Highlight comments since:
				<select id="comment-visits">
					<option value="">no highlighting</option>
					<option value="custom">custom</option>
				</select>
				<input id="hnc_custom_visit" type="text" value="00:00" pattern="\d+?:\d+?" style="text-align: center; display: none;" />
			</div>
		*/},

		get: function (name) {
			return this.function_to_string(this[name]);
		},

		/* original authored by lavoiesl, at https://gist.github.com/lavoiesl/5880516*/
		function_to_string: function (func, strip_leading_whitespace) {
			if (strip_leading_whitespace === undefined) {
				strip_leading_whitespace = 1;
			}

			let matches = func.toString().match(/function[\s\w]*?\(\)\s*?\{[\S\s]*?\/\*\!?\s*?\n([\s\S]+?)\s*?\*\/\s*\}/);

			if (!matches) {
				return false;
			}

			if (strip_leading_whitespace) {
				matches[1] = matches[1].replace(/^(\t| {4})/gm, '');
			}

			return matches[1];
		}
	};

	ui = {
		create_comment_highlighter: function (visit, first) {
			/* create element */
			let highlighter = document.createElement('div');

			highlighter.innerHTML = data.get('comment_highlighter');
			highlighter.classList.add('rounded', 'gold-accent', 'comment-visits-box');

			let commentarea      = document.getElementsByClassName('commentarea')[0],
				sitetable        = commentarea.getElementsByClassName('sitetable')[0],
				comment_margin   = window.getComputedStyle(sitetable.firstChild).getPropertyValue('margin-left'),
				gold_highlighter = document.getElementsByClassName('comment-visits-box')[0]
			;

			/* remove default comment highlighter */
			if (gold_highlighter) {
				gold_highlighter.parentNode.removeChild(gold_highlighter);
			}

			/* properly place */
			highlighter.style.setProperty('margin-left', comment_margin);
			commentarea.insertBefore(highlighter, sitetable);

			/* generate visits */
			let select = document.getElementById('comment-visits');
			select.style.textAlignLast = 'center';

			let option = document.createElement('option');
			option.textContent = (Math.floor(Math.floor((Date.now() - visit) / 1000) / 3600) < 10 ? '0' : '') + Math.floor(Math.floor((Date.now() - visit) / 1000) / 3600) + ':' + (Math.floor((Math.floor((Date.now() - visit) / 1000) % 3600) / 60) < 10 ? '0' : '') + Math.floor((Math.floor((Date.now() - visit) / 1000) % 3600) / 60);
			option.value = visit;
			select.appendChild(option);
			select.children[2].setAttribute('selected', '');

			if (!first) {
				let visit0 = Date.parse(document.getElementById('siteTable').getElementsByClassName('tagline')[0].getElementsByTagName('time')[document.getElementById('siteTable').getElementsByClassName('tagline')[0].getElementsByTagName('time').length - 1].getAttribute('datetime'));
				let option2 = document.createElement('option');
				option2.textContent = (Math.floor(Math.floor((Date.now() - visit0) / 1000) / 3600) < 10 ? '0' : '') + Math.floor(Math.floor((Date.now() - visit0) / 1000) / 3600) + ':' + (Math.floor((Math.floor((Date.now() - visit0) / 1000) % 3600) / 60) < 10 ? '0' : '') + Math.floor((Math.floor((Date.now() - visit0) / 1000) % 3600) / 60);
				option2.value = visit0;
				document.getElementById('comment-visits').appendChild(option2);
			}


			// add listeners
			select.addEventListener('change', this.update_highlighting);

			let custom = document.getElementById('hnc_custom_visit');
			custom.style.setProperty('width', (select.getBoundingClientRect().width) + 'px');
			custom.addEventListener('keydown', this.custom_visit_key_monitor);
			custom.addEventListener('blur', this.set_custom_visit);

			this.custom_pos = 0;
		},

		update_highlighting: function (event) {
			/* no highlighting */
			if (event.target.value == '') {
				HNC.reset_highlighting();
			}

			/* custom */
			else if (event.target.value == 'custom') {
				document.getElementById('comment-visits').style.setProperty('display', 'none');
				let custom = document.getElementById('hnc_custom_visit');
				custom.style.removeProperty('display');
				custom.focus();
				custom.setSelectionRange(0, 2);
			}

			/* previous visit */
			else {
				HNC.reset_highlighting();
				HNC.highlight(parseInt(event.target.value, 10));
				jumpToNextComment();
			}
			event.target.blur();
		},

		custom_visit_key_monitor: function (event) {
			if (event.altKey || event.ctrlKey || (event.shiftKey && event.key != 'Tab')) {
				return;
			}
			if (event.key == 'Tab') {

				let match = event.target.value.match(/^(\d+?:)\d+?$/);

				if (match) {
					if (event.shiftKey) {
						ui.custom_pos--;
					}
					else {
						ui.custom_pos++;
					}

					if (ui.custom_pos % 2 == 0) {
						event.target.setSelectionRange(0, match[1].length - 1);
					}
					else {
						event.target.setSelectionRange(match[1].length, match[0].length);
					}

					event.preventDefault();
					event.stopPropagation();
				}
			}
			else if (event.key == 'Enter') {
				event.target.blur();
				event.preventDefault();
				event.stopPropagation();
			}
		},

		set_custom_visit: function (event) {
			let select = document.getElementById('comment-visits'),
				match  = event.target.value.match(/^(\d+?):(\d+?)$/)
			;

			if (match) {
				let option  = document.createElement('option'),
					hours   = parseInt(match[1], 10),
					minutes = parseInt(match[2], 10),
					visit   = Date.now() - (hours * 60 + minutes) * 60 * 1000
				;

				option.value = visit;
				option.textContent = (hours < 10 ? '0' : '') + hours + ':' + (minutes < 10 ? '0' : '') + minutes;

				select.add(option, 2);
				select.selectedIndex = 2;
			}
			else {
				select.selectedIndex = 0;
			}

			let change = new Event('change');
			select.dispatchEvent(change);

			event.target.value = '00:00';
			event.target.style.setProperty('display', 'none');
			select.style.removeProperty('display');
		}
	};

    if (document.location.href.match(/\/comments(\/|\?|#|$)/)) {
        if (document.location.href.match(/\/comments\/[^\/?#]+(\/([^\/?#]+\/?)?)?(\?|#|$)/)) {
            deleteOldItems();
			handleCommentsPage();

            document.addEventListener('keydown', function(e) {
                // Alt+Q
                if (e.keyCode == "Q".charCodeAt(0) && !e.shiftKey && !e.ctrlKey && e.altKey && !e.metaKey) {
                    jumpToPrevComment();
					e.preventDefault();
                    return false;
                }
                // Alt+W
                if (e.keyCode == "W".charCodeAt(0) && !e.shiftKey && !e.ctrlKey && e.altKey && !e.metaKey) {
                    jumpToNextComment();
					e.preventDefault();
                    return false;
                }

                // Ctrl+up
                if (e.keyCode == 38 && !e.shiftKey && e.ctrlKey && !e.altKey && !e.metaKey) {
                    jumpToPrevComment();
					e.preventDefault();
                    return false;
                }
                // Ctrl+down
                if (e.keyCode == 40 && !e.shiftKey && e.ctrlKey && !e.altKey && !e.metaKey) {
                    jumpToNextComment();
					e.preventDefault();
                    return false;
                }
            }, false);
        }
    } else {
        handleListPage();
    }

})();