Creddit

Adds post authors to items in Reddit feed (on new Reddit)

// ==UserScript==
// @name         Creddit
// @namespace    github.com/JasonAMelancon
// @version      2025-09-03
// @description  Adds post authors to items in Reddit feed (on new Reddit)
// @author       Jason Melancon
// @license      GNU AGPLv3
// @match        http*://www.reddit.com/*
// @icon         data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==
// @grant        none
// ==/UserScript==

(function() {
    'use strict';

//-- script begins here --//

const DEBUG = false;
// The msg is in a lambda expression body so that string interpolation only happens if we
// actually print the message (lazy evaluation).
function debugLog(lambdifiedMsg) {
    if (DEBUG) console.log(lambdifiedMsg());
}

const scriptName = GM_info.script.name;

//== This is the main part of the script. ==//

function runScript() {
    const CREDITED = "is-credited"; // my custom attribute for the post element
    const POST = "shreddit-post"; // Reddit's custom element name
    const FEED = "shreddit-feed"; // Reddit's custom element name

    const feeds = document.getElementsByTagName(FEED);
    if (feeds.length > 1) { // I have no idea whether this is or will ever be necessary
        console.log(`[${scriptName}] Multiple Reddit feed nodes present`);
    }
    const feed = feeds[0];

    debugLog(() => `[${scriptName}] ${feed.querySelectorAll("article").length} initial articles`);

    // Get the first few articles in the feed when the page loads, and add the author.
    feed.querySelectorAll("article").forEach(article => {
        if (!isCredited(article)) {
            creditAuthor(article);
            markCredited(article);
        }
    });

    // Watch the page for new articles that appear when scrolling.
    const dynamicScroll = new MutationObserver(mutations => {
        debugLog(() => `[${scriptName}] ${mutations.length} new mutation objects`);
        for (let mutation of mutations) {
            const newArticleArray = Array.from(mutation.addedNodes).filter(node => node.nodeName === "ARTICLE");
            debugLog(() => `[${scriptName}] ${newArticleArray.length} new articles`);
            if (newArticleArray.length == 0) continue;
            // Add the author to the new articles as they appear.
            newArticleArray.forEach(article => {
                if (!isCredited(article)) {
                    creditAuthor(article);
                    markCredited(article);
                }
            });
        }
    });
    dynamicScroll.observe(feed, { childList: true, subtree: false, attributes: false, characterData: false });

    // Put the author of a single article on the top line, next to the subreddit and post age.
    function creditAuthor(article) {
        const post = article.querySelector(POST);
        const creditBar = post.querySelector("[id*='credit-bar']");
        const separator = creditBar.querySelector(".created-separator");
        creditBar.appendChild(separator.cloneNode(/*deep = */true));
        const byLineClass = separator.nextElementSibling.getAttribute("class");
        const byLine = document.createElement("span");
        creditBar.appendChild(byLine).setAttribute("class", byLineClass);
        const author = post.getAttribute("author");
        if (author === "[deleted]") {
            byLine.innerHTML = `by ${author}`;
        } else {
            byLine.innerHTML = `by <a href="/u/${author}">${author}</a>`;
        }
    }

    // When scrolling down far enough, Reddit unloads posts from the top of the page,
    // presumably to save memory. In general, Reddit unloads posts you scroll away from
    // and loads or reloads posts you scroll toward. Without checking to make sure the
    // post hasn't already been credited, this can cause this script to credit the post
    // multiple times when the MutationObserver notices a credited post reappear in the
    // feed.
    //
    // Therefore, check first.
    function isCredited(article) {
        const post = article.querySelector(POST);
        debugLog(() => `[${scriptName}] credited check: ${post.hasAttribute(CREDITED)}`);
        return post.hasAttribute(CREDITED);
    }

    // Marks a post as already credited.
    function markCredited(article) {
        const post = article.querySelector(POST);
        post.setAttribute(CREDITED, "");
    }
}

//== This is the part of the script that deals with the PITA way Reddit does scripted navigation. ==//

// Specify known titles for feed pages, because the script should work even when the user
// types in a URL that *isn't* a feed page, and then uses the site's own navigation interface
// to go to one.
const redditFeedTitles = {
    home : "Reddit - The heart of the internet",
    popular : "r/popular"
};

// Detect feed page on initial load by checking the path. This script is primarily designed to run
// on the home page, since that's where the feed is, but on further reflection, it can also run on
// r/popular, because that's basically a feed too; it's not really a subreddit.
if ([ "/", "/r/popular/" ].includes(window.location.pathname)) {
    // limited future-proofing in case Reddit changes their title
    if (window.location.pathname == "/") {
        redditFeedTitles.home = document.title;
    } else {
        redditFeedTitles.popular = document.title;
    }

    runScript();
}

// Run the script when using the nav bar (fake AJAX navigation) to get to the feed.
new MutationObserver(() => {
    if (Object.values(redditFeedTitles).includes(document.title)) {
        runScript();
    }
}).observe(document.querySelector("title"), { childList: true });

//-- script ends here --//

})();