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:
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.
Features:
- Scroll infinitely through long threads
- Page numbers will be added and the URL will be updated so it remembers the page you are on
- Works on Desktop and Mobile, Firefox and Chrome
- Confirmed working on GreaseMonkey and TamperMonkey (Violent Monkey doesn't work because it doesn't allow window access)
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: