NOTICE: By continued use of this site you understand and agree to the binding Terms of Service and Privacy Policy.
// ==UserScript== // @name 4chan Thread Follower // @license MIT // @version 0.15.0 // @description Follow threads on 4chan. Adds checkboxes to the bottom of threads to auto-scroll to the bottom and to follow links to new threads. // @author leafscriptbro // @match https://boards.4chan.org/* // @match https://boards.4channel.org/* // @icon https://www.google.com/s2/favicons?sz=64&domain=4chan.org // @downloadURL https://openuserjs.org/install/leafscriptbro/4chan_Thread_Follower.user.js // @updateURL https://openuserjs.org/meta/leafscriptbro/4chan_Thread_Follower.meta.js // @homepage https://openuserjs.org/scripts/leafscriptbro/4chan_Thread_Follower // @grant none // ==/UserScript== 'use strict'; // Global settings let progName = "4chan Thread Follower"; let settingsName = "4chan-thread-follower-settings"; let openInNewTab = true; let autoScrollCheckboxId = "auto-scroll-to-bottom-checkbox"; let autoScrollCheckboxLabel = "Auto-scroll to bottom"; let autoScrollSettingName = "auto-scroll-to-bottom"; let autoScrollBottomOffset = 190; let autoScrollUpdateInterval = 500; let followLinksCheckboxId = "follow-new-general-links-checkbox"; let followLinksCheckboxLabel = "Follow new general links"; let followLinksSettingName = "follow-new-general-links"; let newGeneralThreadPattern = /\b(migrate|fresh(ly)? baked|fresh (th|b)read|new|next (th|b)read|get in(to)?)\b/i; let followLinksUpdateInterval = 1000; let minFollowReplyNumber = 150; let minFollowLinks = 2; // Performance/persistence optimization - get settings one time on page load let settings = getSettings(); // Global functions function getBottomScrollPosition() { return document.body.scrollHeight - window.innerHeight - autoScrollBottomOffset; } function getAutoScrollCheckbox() { return document.getElementById(autoScrollCheckboxId); } function getFollowLinksCheckbox() { return document.getElementById(followLinksCheckboxId); } function getSettings() { let settings = localStorage[settingsName] || "{}"; let parsedSettings = {}; try { parsedSettings = JSON.parse(settings); } catch (error) { console.error(progName + ": error parsing settings object"); console.error(error); } return parsedSettings; } function getPageSettings(href = window.location.href) { return getSettings()[href] || {}; } function setPageSettings(href = window.location.href, newSettings = {}) { settings[href] = newSettings; localStorage[settingsName] = JSON.stringify(settings); } function setPageSetting(href, settingName, settingValue) { let pageSettings = getPageSettings(href); pageSettings[settingName] = settingValue; settings[href] = pageSettings; localStorage[settingsName] = JSON.stringify(settings); } // Disable following links to avoid being unable to access the page after changing URL function resetFollowLinksSetting() { setPageSetting(window.location.href, followLinksSettingName, false); if (followLinksCheckbox) { followLinksCheckbox.checked = false; } } // Get the bottom nav links let navLinksBotElements = document.getElementsByClassName("navLinksBot"); if (navLinksBotElements.length === 0) { console.warn(progName + ": warning: cannot find bottom nav links, aborting"); return; } if (navLinksBotElements.length > 1) { console.warn(progName + ": warning: more than one bottom nav links found, using the first one"); } let navLinksBot = navLinksBotElements[0]; // Add our checkboxes to the bottom page navLinksBot.innerHTML += "<span> [ <label><input type=\"checkbox\" id=\"" + autoScrollCheckboxId + "\" title=\"" + autoScrollCheckboxLabel + "\"> " + autoScrollCheckboxLabel + "</label> ] </span>"; navLinksBot.innerHTML += "<span> [ <label><input type=\"checkbox\" id=\"" + followLinksCheckboxId + "\" title=\"" + followLinksCheckboxLabel + "\"> " + followLinksCheckboxLabel + "</label> ] </span>"; // Get initial settings let initialSettings = getPageSettings(); // Get auto-scroll checkbox let autoScrollCheckbox = getAutoScrollCheckbox(); if (!autoScrollCheckbox) { console.warn(progName + ": warning: can't find the \"" + autoScrollCheckboxLabel + "\" checkbox"); } // Get follow links checkbox let followLinksCheckbox = getFollowLinksCheckbox(); if (!followLinksCheckbox) { console.warn(progName + ": warning: can't find the \"" + followLinksCheckboxLabel + "\" checkbox"); } // Set initial checked values if (autoScrollCheckbox && initialSettings[autoScrollSettingName]) { autoScrollCheckbox.checked = true; } if (followLinksCheckbox && initialSettings[followLinksSettingName]) { followLinksCheckbox.checked = true; } // Set on change functions if (autoScrollCheckbox) { autoScrollCheckbox.addEventListener("change", function onChangeAutoScrollCheckbox() { setPageSetting(window.location.href, autoScrollSettingName, autoScrollCheckbox.checked); if (autoScrollCheckbox.checked) { window.scrollTo(0, getBottomScrollPosition()); } }); } if (followLinksCheckbox) { followLinksCheckbox.addEventListener("change", function onChangeFollowLinksCheckbox() { setPageSetting(window.location.href, followLinksSettingName, followLinksCheckbox.checked); }); } let isAutoScrollPaused = false; let targetHref = null; // Set global scroll event listeners document.addEventListener("scroll", function globalScrollEventListener() { isAutoScrollPaused = window.scrollY < getBottomScrollPosition(); }); document.addEventListener("click", function globalMouseMoveEventListener() { // Navigate to target href if it failed before in a click callback to get around browser restrictions if (targetHref) { window.open(targetHref, "_blank").focus(); resetFollowLinksSetting(); targetHref = null; } }); // Set auto-scroll update interval setInterval(function autoScrollIntervalCallback() { if (!isAutoScrollPaused && autoScrollCheckbox && autoScrollCheckbox.checked) { // Scroll to bottom let currentScroll = window.scrollY; let newScroll = getBottomScrollPosition(); if (currentScroll < newScroll) { window.scrollTo(0, newScroll); } } }, autoScrollUpdateInterval); // Set follow links update interval let didWarnOnUrlParseError = false; setInterval(function followLinksAutoScrollInterval() { if (followLinksCheckbox && followLinksCheckbox.checked && !targetHref) { // Get board name from URL let pathMatch = window.location.pathname.match("/([^/]+)/thread/[0-9]+"); if (!pathMatch) { if (!didWarnOnUrlParseError) { console.warn(progName + ": warning: can't parse current board from URL pathname"); didWarnOnUrlParseError = true; } return; } let currentBoard = pathMatch[1]; // Get the contents of all replies let replyMessageElements = document.querySelectorAll(".replyContainer .postMessage"); // Get only the replies after the minimum number replyMessageElements = Array.from(replyMessageElements).slice(minFollowReplyNumber); for (let replyMessageElement of replyMessageElements) { // Get all cross-thread links in the reply let crossThreadLinks = replyMessageElement.querySelectorAll("[href*=\"/" + currentBoard + "/thread/\"][class=\"quotelink\"]"); if (crossThreadLinks.length >= minFollowLinks) { // Get the href of the first link let firstCrossThreadLinkHref = crossThreadLinks[0].href; // Check for multiple unique links let hasMultipleUniqueLinks = false; for (let crossThreadLink of crossThreadLinks) { if (crossThreadLink.href != firstCrossThreadLinkHref) { hasMultipleUniqueLinks = true; break; } } if (!hasMultipleUniqueLinks && replyMessageElement.textContent.match(newGeneralThreadPattern)) { // Set new page target // Persist settings over to new page setPageSettings(firstCrossThreadLinkHref, getPageSettings()); // Navigate to new URL let didChangeUrl = false; if (openInNewTab) { let newWindow = window.open(firstCrossThreadLinkHref, "_blank"); if (newWindow) { newWindow.focus(); didChangeUrl = true; } else { targetHref = firstCrossThreadLinkHref; } } else { window.location.href = firstCrossThreadLinkHref; didChangeUrl = true; } if (didChangeUrl) { resetFollowLinksSetting(); } } } } } }, followLinksUpdateInterval);