Creddit

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

目前為 2025-09-18 提交的版本,檢視 最新版本

您需要先安裝使用者腳本管理器擴展,如 TampermonkeyGreasemonkeyViolentmonkey 之後才能安裝該腳本。

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

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyUserscripts 後才能安裝該腳本。

你需要先安裝一款使用者腳本管理器擴展,比如 Tampermonkey,才能安裝此腳本

您需要先安裝使用者腳本管理器擴充功能後才能安裝該腳本。

(我已經安裝了使用者腳本管理器,讓我安裝!)

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

(我已經安裝了使用者樣式管理器,讓我安裝!)

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

(function() {
    'use strict';

//-- script begins here --//

const DEBUG = true;
// 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;
const FEED = "shreddit-feed"; // Reddit's custom element name
const POST = "shreddit-post"; // Reddit's custom element name
const VIEW_CONTEXT = "view-context"; // Reddit's custom attribute name
const AGGREGATE_FEED = "AggregateFeed"; // Reddit's post element attribute value; indicates need to run script

const CREDITED = "is-credited"; // my custom attribute for the post element

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

function runScript() {
    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];

    const articleCollection = feed.querySelectorAll("article");
    debugLog(() => `[${scriptName}] ${articleCollection.length} initial articles`);
    // Get the first few articles in the feed when the page loads, and add the author.
    articleCollection.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. ==//

// Detect feed page on initial load or subsequent XHR/AJAX load by checking the view-context attribute
// of the first post.
//
// This script can run on any page that's designated by Reddit as an "aggregate feed," meaning a feed
// with posts taken from more than one subreddit. Examples would be r/popular, r/all, /new/, etc.
function runScriptIfNecessary() {
    const isAggregate = document.querySelector(POST)?.getAttribute(VIEW_CONTEXT) == AGGREGATE_FEED;
    debugLog(() => `[${scriptName}] This page ${isAggregate ? "contains" : "does not contain"} an aggregate feed`);

    if (isAggregate) {
        runScript();
    }
}

runScriptIfNecessary();

// Run the script when using the site nav (fake AJAX navigation) to get to the feed. This apparently
// can be bouncy and fire multiple times, and lead to repeated console debug messages, but this can
// be ignored since, before modifying any post's DOM, we always check if we've done it already.
new MutationObserver(() => {
    const title = document.querySelector("title").textContent.trim();
    debugLog(() => `[${scriptName}] Page <title> change detected (indicating navigation to a new page);\n` +
                   `[${scriptName}] <title> is now "${title}"`);

    runScriptIfNecessary();
}).observe(document.querySelector("title"), { childList: true });

//-- script ends here --//

})();