Hacker News Story Rank Change Indicator

Indicate the new stories and stories moving up/down on the front page

// ==UserScript==
// @name         Hacker News Story Rank Change Indicator
// @namespace    http://tampermonkey.net/
// @version      2024-09-07_15-42
// @description  Indicate the new stories and stories moving up/down on the front page
// @author       SMUsamaShah
// @match        https://news.ycombinator.com/
// @match        https://news.ycombinator.com/news
// @match        https://news.ycombinator.com/news?p=*
// @match        https://news.ycombinator.com/?p=*
// @icon         https://www.google.com/s2/favicons?sz=64&domain=ycombinator.com
// @grant        none
// @license      MIT
// ==/UserScript==

(function() {
    'use strict';

    const KEY_LAST_CLEAR = 'hackernews-rank-notifier-clear-time';
    const KEY_RANK_STORE = 'hackernews-rank-notifier';
    const KEY_COMMENT_STORE = 'hackernews-rank-notifier-comments';
    const MAX_STORIES = 3000;
    const MAX_NEW_COMMENTS_FOR_MAX_COLOR = 50;
    const HIDE_SEEN_AND_UNCHANGED_STORIES = false;
    const HIDE_STORIES_WITH_FEWER_NEW_COMMENTS = false;
    const HIDE_STORIES_WITH_FEWER_NEW_COMMENTS_THRESHOLD = 2;

    const oldRanks = JSON.parse(localStorage.getItem(KEY_RANK_STORE)) || {};
    const oldComments = JSON.parse(localStorage.getItem(KEY_COMMENT_STORE)) || {};

    function getAllStoryElements() { return Array.from(document.getElementsByClassName('athing')); }
    function getTitleElement(story) { return story.querySelector('.title a'); }
    function getId(story) { return story.id; }
    function getRank(story) { return parseInt(story.querySelector('span.rank').innerText.slice(0, -1)); }
    function clamp(v, min, max) { return Math.max(min, Math.min(v, max)); }
    
    function sortAndDiscardOldRecords(ranks, comments, keepRecordsUpto) {
        const topStoryIds = Object.keys(ranks).sort((a, b) => b - a).slice(0, keepRecordsUpto); 
        const newRanks = {};
        const newComments = {};
        topStoryIds.forEach(id => {
            newRanks[id] = ranks[id];
            newComments[id] = comments[id];
        });
        localStorage.setItem(KEY_RANK_STORE, JSON.stringify(newRanks));
        localStorage.setItem(KEY_COMMENT_STORE, JSON.stringify(newComments));
    }

    function getCommentsElement(story) { return story.nextSibling.querySelector('.subline>a:last-child'); }
    function getCommentCount(story) {
        let commentsElement = getCommentsElement(story);
        if (!commentsElement) return 0;
        let commentCountText = commentsElement.innerText.match(/([0-9]*) comment/);
        return (commentCountText) ? parseInt(commentCountText[1]) : 0;
    }
    function hideStory(story) {
        story.style.display = 'none';
        story.nextElementSibling.style.display = 'none';
        story.nextElementSibling.nextElementSibling.style.display = 'none';
    }
    function updateTitle(story, rankChange) {
        let title = getTitleElement(story);
        if (rankChange == undefined) {
            title.innerHTML = "<b style='color:green'>(NEW) </b>" + title.innerHTML;
        } else {
            title.textContent = '(' + (rankChange > 0 ? '+' : '-') + Math.abs(rankChange) + ') ' + title.textContent;
        }
    }
    function updateCommentCount(story, commentChange) {
        let commentsElement = getCommentsElement(story);
        if (commentsElement) {
            let bright = clamp(120 - commentChange * 120 / MAX_NEW_COMMENTS_FOR_MAX_COLOR, 0, 120);
            const commentColor = "rgb(255, " + bright + "," + bright + ")";
            commentsElement.innerHTML = '<span style="color:' + commentColor + '">(' + (commentChange > 0 ? '+' : '-') + Math.abs(commentChange) + ') </span>' + commentsElement.innerHTML;
        }
    }

    const stories = getAllStoryElements();

    stories.forEach(function(story) {
        const id = getId(story);
        const rank = getRank(story);
        const commentCount = getCommentCount(story);

        if (id in oldRanks) {
            const rankChange = oldRanks[id] - rank;
            const commentChange = commentCount - (oldComments[id] || 0);
            
            if (rankChange !== 0) {
                updateTitle(story, rankChange);
            }
            if (commentChange !== 0) {
                updateCommentCount(story, commentChange);
            }
            if (HIDE_SEEN_AND_UNCHANGED_STORIES && commentChange === 0) {
                hideStory(story);
            }
            if (HIDE_STORIES_WITH_FEWER_NEW_COMMENTS && commentChange < HIDE_STORIES_WITH_FEWER_NEW_COMMENTS_THRESHOLD) {
                hideStory(story);
            }
        } 
        else {
            updateTitle(story);
        }
        oldRanks[id] = rank;
        oldComments[id] = commentCount;
    });

    const topStoryIds = sortAndDiscardOldRecords(oldRanks, oldComments, MAX_STORIES);
})();