PoonBrosAreValid
kiwifarms.net
- Joined
- Sep 15, 2023
This is a modified version of my Infinite Scroll Script. This one allows you to view all the highlights in a thread easily and quickly!


Use GreaseMonkey or Tampermonkey. Users have complained by Kiwifarms scripts do not run correctly on violent monkey. Be sure to report any bugs or feature requests below!
1. Add the script below to your user script extension of choice.
2. Click the "Switch to highlights view" button in the bottom right corner.
3. You should now be able to see only highlights and infinitely scroll through the important parts of long threads!


Use GreaseMonkey or Tampermonkey. Users have complained by Kiwifarms scripts do not run correctly on violent monkey. Be sure to report any bugs or feature requests below!
1. Add the script below to your user script extension of choice.
2. Click the "Switch to highlights view" button in the bottom right corner.
3. You should now be able to see only highlights and infinitely scroll through the important parts of long threads!
JavaScript:
// ==UserScript==
// @name Kiwifarms Infinte Scroll / Highlight Viewer
// @namespace http://tampermonkey.net/
// @version 2025-03-16
// @description Infinitely scroll through long threads viewing posts and highlights
// @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==
/** How many highlights must show up before a new page shouldn't immediately be loaded */
const LOAD_NEXT_IF_HIGHLIGHT_COUNT = 5
const kiwi = {
async getPostsForPage(url, { mode, currentPage }) {
const isHighlightsOnly = mode === "highlights"
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;
let posts = this.selectPosts(dummyElement);
nextUrl = isHighlightsOnly
? this.urlForNextHighlight(dummyElement)
: kiwi.urlForPage(currentPage);
if (isHighlightsOnly) {
posts = posts.filter(this.isHighlight);
}
if (! nextUrl) {
return { nextUrl: null, posts }
}
if (posts.length === 0) {
throw new Error("No posts found!");
}
this.setNewlyLoaded(posts, true);
return { posts, nextUrl };
},
selectPosts(elem) {
const postSelector = this.getPostSelector(this.pageType);
return [...elem.querySelectorAll(postSelector)];
},
hideNonHighlights() {
// Hide non highlight posts
const posts = kiwi.selectPosts(document);
for (const post of posts) {
if (!kiwi.isHighlight(post)) {
post.style.display = "none";
}
}
},
/** A special case to load more now if there is only a small amount of highlights
* and the lower page selector is still visible
*/
shouldLoadNextHighlightsNow(currentHighlights, lowerPageIndicator) {
return currentHighlights.length < LOAD_NEXT_IF_HIGHLIGHT_COUNT
&& page.isElementVisible(lowerPageIndicator)
},
/**
* @param {Element} post
* @return {bool}
*/
isHighlight(post) {
return post.classList.contains("hb-react-threadHighlight");
},
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}`;
},
urlForNextHighlight(page) {
const elem = page.querySelector(
".block-outer div.buttonGroup > a:nth-child(1)"
);
if (! elem) {
return null
}
return elem.href;
},
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"
);
},
get jQuery() {
// For tampermonkey
if (window.jQuery) return window.jQuery;
if (window.$) return window.$;
// For greasemonkey
if (unsafeWindow.jQuery) return unsafeWindow.jQuery;
if (unsafeWindow.$) return unsafeWindow.$;
throw new Error(
"Can't get JQuery (used to communicate with the image viewer)! Your userscript manager probably rejected access"
);
},
initImageViewer() {
// Tell lightbox to add the image viewer to all newly loaded images
this.jQuery(document).trigger("xf:reinit", ".js-lbImage");
},
onNewPageLoad() {
// Tell xenforo a new page has been loaded
// This activates all the JS (reaction menu, quoting, etc)
this.xf.onPageLoad();
this.initImageViewer();
},
};
const ui = {
autismEmoji: "/styles/custom/emoji/Autistic.svg",
dumbEmoji: "/styles/custom/emoji/Dumb.svg",
informativeEmoji: "/styles/custom/emoji/Informative.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, suffix = "") {
const label = this.buildBanner();
const link = document.createElement("a");
link.innerText = `Page ${pageNumber}${suffix}`;
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;
},
getText(text, emoji) {
const label = this.buildBanner();
const icon = this.buildEmoji(emoji);
label.innerText = text
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, mode) {
const modeUrl = new URL(url)
modeUrl.searchParams.set("mode", mode)
history.pushState({}, "", modeUrl);
},
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.style.cursor = "pointer"
btn.onclick = () => window.scrollTo(0, 0);
document.body.append(btn);
},
addModeToggleBotton() {
const currentMode = page.mode
const nextMode = currentMode === "highlights"
? "posts"
: "highlights"
const url = new URL(location.href)
url.searchParams.set("mode", nextMode)
const btn = document.createElement("button");
btn.classList.add("label", "label--primary");
btn.style.position = "fixed";
btn.style.bottom = "35px";
btn.style.right = "5px";
btn.style.cursor = "pointer"
btn.innerText = `Switch to ${nextMode} View`;
btn.onclick = () => location.href = url.toString();
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);
},
isElementVisible(elem) {
const rect = elem.getBoundingClientRect();
return (
rect.top >= 0 &&
rect.left >= 0 &&
rect.bottom <= (window.innerHeight || document.documentElement.clientHeight) &&
rect.right <= (window.innerWidth || document.documentElement.clientWidth)
);
},
addModeToPageLinks() {
const pageLinks = document.querySelectorAll(".pageNav-page a")
for (const pageLink of pageLinks) {
const modifiedUrl = new URL(pageLink.href, location.origin)
modifiedUrl.searchParams.set("mode", this.mode)
pageLink.href = modifiedUrl
}
},
get currentPage() {
return this.getPageNumber(".pageNav-page--current a");
},
get lastPage() {
return this.getPageNumber(
".pageNavWrapper--full .pageNav-page:last-child a"
);
},
/** Get the script mode from the URL */
get mode() {
const url = new URL(location.href)
return url.searchParams.get("mode") ?? "posts"
},
};
function init() {
"use strict";
const currentMode = page.mode
const indicators = page.getIndicators();
// No pages, no need to keep running
if (!indicators) return;
const state = {
mode: currentMode,
currentPage: page.currentPage,
lastPage: page.lastPage,
lowerPageIndicator: indicators.lowerPageIndicator,
lowerPageSelector: indicators.lowerPageSelector,
postBody: document.querySelector(
".block-body.js-replyNewMessageContainer"
),
isLoading: false,
observer: null,
nextUrl: currentMode === "highlights"
? kiwi.urlForNextHighlight(document)
: kiwi.urlForPage(page.currentPage + 1),
};
ui.addModeToggleBotton();
ui.addToTopButton();
page.addModeToPageLinks();
if (state.mode === "highlights") {
kiwi.hideNonHighlights();
}
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);
const pageSuffix = state.mode === "highlights"
? " (Highlights)"
: ""
ui.addPageNumber(state.postBody, state.currentPage, pageUrl, pageSuffix);
ui.setLowerPageIndicator(
state.lowerPageIndicator,
state.currentPage,
pageUrl
);
const loadingText = ui.getLoadingText();
state.postBody.append(loadingText);
kiwi.getPostsForPage(state.nextUrl, { mode: state.mode, currentPage: state.currentPage })
.then(({ posts, nextUrl }) => {
state.postBody.append(...posts);
state.isLoading = false;
loadingText.remove();
if (! nextUrl) {
const noMoreMessage = ui.getText("No More Posts", ui.informativeEmoji)
state.isLoading = false
state.postBody.append(noMoreMessage)
state.observer.disconnect()
return
}
state.nextUrl = nextUrl;
if (state.mode === "highlights" && kiwi.shouldLoadNextHighlightsNow(posts, state.lowerPageIndicator)) {
getNextPage()
}
ui.setUrl(pageUrl, state.mode);
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();
}
}
init()
Last edited: