Infinite Scroll Script for Kiwifarms

  • 🐕 I am attempting to get the site runnning as fast as possible. If you are experiencing slow page load times, please report it.

PoonBrosAreValid

kiwifarms.net
Joined
Sep 15, 2023
I just wrote this script tonight and thought it would be useful to others. It adds infinite scrolling to the farms.

Features:
  1. Scroll infinitely through long threads
  2. Page numbers will be added and the URL will be updated so it remembers the page you are on
  3. Works on Desktop and Mobile, Firefox and Chrome
  4. Confirmed working on GreaseMonkey and TamperMonkey (Violent Monkey doesn't work because it doesn't allow window access)

Screenshot_20240818-235424.png
Screenshot_20240818-235432.png

JavaScript:
// ==UserScript==
// @name         Kiwifarms Infinite Scroll
// @namespace    http://tampermonkey.net/
// @version      2025-04-18
// @description  Infinite scroll for your viewing pleasure! (for the new April 2025 version of the site)
// @author       PoonBrosAreValid
// @match        https://kiwifarms.st/threads/*
// @match        https://kiwifarms.st/conversations/*
// @icon         https://kiwifarms.st/styles/custom/logos/kiwi_square.og.png
// @grant        none
// ==/UserScript==

const kiwi = {
    async getPostsForPage(url) {
        const res = await fetch(url);
        if (res.status === 203) {
            throw new Error(`Reload to solve KiwiFlare...`);
        } else if (res.status !== 200) {
            throw new Error(`Page responded error ${res.status}`);
        }

        const rawText = await res.text();

        const dummyElement = document.createElement("div");
        dummyElement.innerHTML = rawText;
        const postSelector = this.getPostSelector(this.pageType);
        const posts = dummyElement.querySelectorAll(postSelector);
        if (posts.length === 0) {
            throw new Error("No posts found!");
        }
        this.setNewlyLoaded(posts, true);

        return posts;
    },

    getPostSelector(pageType) {
        switch (pageType) {
            case "threads":
                return ".block-body .message.message--post";
            case "conversations":
                return ".block-body .message.message--conversationMessage";
            default:
                throw new Error("This page type is not supported!");
        }
    },

    urlForPage(pageNumber) {
        const current = new URL(location.href);
        const chunks = current.pathname.split("/").slice(1, 3);
        const basePath = chunks.join("/");
        return `${current.origin}/${basePath}/page-${pageNumber}`;
    },

    setNewlyLoaded(posts, isNew) {
        const name = "newly-loaded-post";
        for (const post of posts) {
            post.classList[isNew ? "add" : "remove"](name);
        }
    },

    get pageType() {
        const current = new URL(location.href);
        const pieces = current.pathname.split("/");
        // This shouldn't happen, unless I change which pages are allowed
        if (pieces.length <= 1) return "home";
        return pieces[1];
    },

    get xf() {
        // For tampermonkey
        if (window && window.XF) return window.XF;
        // For greasemonkey
        if (unsafeWindow && unsafeWindow.XF) return unsafeWindow.XF;

        throw new Error(
            "Can't get XF (used to tell XenForo new posts were loaded)! Your userscript manager probably rejected access"
        );
    },

    onNewPageLoad() {
        // Tell xenforo a new page has been loaded
        // This activates all the JS (reaction menu, quoting, etc)
        this.xf.onPageLoad();
        // Actives the image viewer 
        this.xf.activate(document.querySelector('.block-body'))
    },
};

const ui = {
    autismEmoji: "/styles/custom/emoji/Autistic.svg",

    dumbEmoji: "/styles/custom/emoji/Dumb.svg",

    buildBanner() {
        const label = document.createElement("article");
        label.classList.add("message", "message--post");
        label.style.padding = "15px";
        return label;
    },

    buildEmoji(src) {
        const icon = document.createElement("img");
        icon.src = src;
        icon.style.width = "32px";
        icon.style.height = "32px";
        return icon;
    },

    addPageNumber(body, pageNumber, pageUrl) {
        const label = this.buildBanner();
        const link = document.createElement("a");
        link.innerText = `Page ${pageNumber}`;
        link.href = pageUrl;
        label.append(link);
        body.append(label);
    },

    getLoadingText() {
        const label = this.buildBanner();
        const icon = this.buildEmoji(this.autismEmoji);

        label.innerText = `Please be patient, I have autism...`;
        label.append(icon);

        return label;
    },

    getErrorText(error) {
        const label = this.buildBanner();
        label.style.backgroundColor = "#660000";
        const icon = this.buildEmoji(this.dumbEmoji);

        label.innerText = error.toString();
        label.append(icon);

        return label;
    },

    setLowerPageIndicator(indicator, currentPage, pageUrl) {
        indicator.href = pageUrl;
        indicator.innerText = currentPage;
    },

    setUrl(url) {
        history.pushState({}, "", url);
    },

    addToTopButton() {
        const btn = document.createElement("button");
        btn.classList.add("label", "label--primary");
        btn.style.position = "fixed";
        btn.style.bottom = "5px";
        btn.style.right = "5px";
        btn.innerText = "Go to Top";

        btn.onclick = () => window.scrollTo(0, 0);

        document.body.append(btn);
    },
};

const page = {
    getIndicators() {
        const pageNav = document.querySelectorAll(".pageNavWrapper");
        if (pageNav.length === 0) return null;
        const currentPageIndicators = document.querySelectorAll(
            ".pageNav-page--current a"
        );
        if (currentPageIndicators.length === 0) return null;

        return {
            lowerPageSelector: pageNav[1],
            lowerPageIndicator: currentPageIndicators[1],
        };
    },

    getPageNumber(selector, defaultNumber = 0) {
        const elem = document.querySelector(selector);
        if (!elem) return defaultNumber;
        return Number(elem.innerText);
    },

    get currentPage() {
        return this.getPageNumber(".pageNav-page--current a");
    },

    get lastPage() {
        return this.getPageNumber(
            ".pageNavWrapper--full .pageNav-page:last-child a"
        );
    },
};

(function () {
    "use strict";

    const indicators = page.getIndicators();
    // No pages, no need to keep running
    if (!indicators) return;

    const state = {
        currentPage: page.currentPage,
        lastPage: page.lastPage,
        lowerPageIndicator: indicators.lowerPageIndicator,
        lowerPageSelector: indicators.lowerPageSelector,
        postBody: document.querySelector(
            ".block-body.js-replyNewMessageContainer"
        ),
        isLoading: false,
        observer: null,
    };

    ui.addToTopButton();

    function getNextPage() {
        if (state.isLoading) return;
        if (state.currentPage >= state.lastPage) {
            state.observer.disconnect();
            return;
        }

        state.isLoading = true;
        state.currentPage += 1;

        const pageUrl = kiwi.urlForPage(state.currentPage);

        ui.addPageNumber(state.postBody, state.currentPage, pageUrl);
        ui.setLowerPageIndicator(
            state.lowerPageIndicator,
            state.currentPage,
            pageUrl
        );

        const loadingText = ui.getLoadingText();
        state.postBody.append(loadingText);

        kiwi.getPostsForPage(pageUrl)
            .then((posts) => {
                state.postBody.append(...posts);
                state.isLoading = false;
                loadingText.remove();

                ui.setUrl(pageUrl);

                kiwi.onNewPageLoad();

                setTimeout(() => kiwi.setNewlyLoaded(posts, false), 1000);
            })
            .catch((error) => {
                state.isLoading = false;
                loadingText.remove();

                console.error("Kiwi Farms Infinite Scroll", error);

                const errorMessage = ui.getErrorText(error);
                state.postBody.append(errorMessage);

                state.observer.disconnect();
            });
    }

    function startObserver() {
        state.observer = new IntersectionObserver((entries) => {
            for (const entry of entries) {
                if (entry.isIntersecting) {
                    getNextPage();
                }
            }
        });
        state.observer.observe(state.lowerPageSelector);
    }

    if (state.lastPage !== 1) {
        startObserver();
    }
})();

Edit: Fix bug with the reaction menu
Edit: Add the current page to the URL so you can save your progress
Edit: Better error handling and error messages
Edit: Add support for conversations
Edit: April 18, 2025. A site update broke this script! Here is a new version.
 
Last edited:
Oh my fauci thank you kind programmer socks kiwi poster, now my ADHD addled brain can infinitely scroll through any thread just like my heckking wholesome chungus shortform content social media sites!
41271 - SoyBooru.png
 
const chunks = current.pathname.split('/').slice(0, 3)
I don't know why chunks[0] is "" on librewolf, but I added a workaround:
JavaScript:
  function urlForPage(pageNumber) {
    const current = new URL(location.href);
    const chunks = current.pathname.split('/').slice(0, 3)
    if (!chunks[0]) {
      chunks[0] =  current.origin;
    }
    const basePath = chunks.join('/')
    return basePath + `/page-${pageNumber}`
  }

Edit: The reactions menu don't seem to popup with mouse over.
 
I don't know why chunks[0] is "" on librewolf, but I added a workaround:

Edit: The reactions menu don't seem to popup with mouse over.

That's because the pathname only takes the back of the URL (`/threads/infinite-scroll-script-for-kiwifarms.198354`) and drops the origin. Did it not work with the relative url on librewolf?
You're right, looks like it breaks reactions for the next pages (quoting and everything else seems fine). I'll take a look. Fixed now!
 
Last edited:
That's because the pathname only takes the back of the URL (`/threads/infinite-scroll-script-for-kiwifarms.198354`) and drops the origin. Did it not work with the relative url on librewolf?
You're right, looks like it breaks reactions for the next pages (quoting and everything else seems fine). I'll take a look. Fixed now!
No, I saw an error message:
TypeError: /threads/[forum]/page-[number] is not a valid URL.

That clued me in on something going wrong with the splitting.
 
Back