Created
December 15, 2025 18:55
-
-
Save Astropulse/30a09ae130f9992bfe7b80310132845c to your computer and use it in GitHub Desktop.
Violentmonkey/greasemonkey extension to force 'following' page default on X
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| // ==UserScript== | |
| // @name X Home Default to Following | |
| // @namespace vm.x.following.default | |
| // @version 1.1.0 | |
| // @description On X home, automatically switch to the "Following" tab instead of "For you". | |
| // @match https://x.com/home | |
| // @match https://x.com/home* | |
| // @match https://twitter.com/home | |
| // @match https://twitter.com/home* | |
| // @run-at document-idle | |
| // @grant none | |
| // ==/UserScript== | |
| (function () { | |
| "use strict"; | |
| const TARGET_TAB_TEXT = "following"; | |
| const AVOID_TAB_TEXT = "for you"; | |
| let lastActionAt = 0; | |
| let pending = false; | |
| function nowMs() { | |
| return Date.now(); | |
| } | |
| function normalizeText(s) { | |
| return (s || "") | |
| .replace(/\s+/g, " ") | |
| .trim() | |
| .toLowerCase(); | |
| } | |
| function isElementVisible(el) { | |
| if (!el) return false; | |
| const style = window.getComputedStyle(el); | |
| if (!style) return false; | |
| if (style.display === "none" || style.visibility === "hidden") return false; | |
| const rect = el.getBoundingClientRect(); | |
| return rect.width > 0 && rect.height > 0; | |
| } | |
| function getHomePrimaryColumn() { | |
| // X uses a primary column in the middle. We keep this loose on purpose. | |
| // Try to anchor near the tabs area using role="tablist". | |
| const tablist = document.querySelector('[role="tablist"]'); | |
| if (tablist) return tablist; | |
| return null; | |
| } | |
| function getTabs() { | |
| const root = getHomePrimaryColumn() || document; | |
| const tabs = Array.from(root.querySelectorAll('[role="tab"]')); | |
| // Sometimes tabs are anchors without role=tab but with role=tab on child. | |
| // Keep only unique elements and those with readable text. | |
| return tabs | |
| .filter((t) => normalizeText(t.textContent).length > 0) | |
| .filter((t) => isElementVisible(t)); | |
| } | |
| function findTabByText(wantedLower) { | |
| const tabs = getTabs(); | |
| for (const tab of tabs) { | |
| const txt = normalizeText(tab.textContent); | |
| if (txt === wantedLower) return tab; | |
| } | |
| // Fallback: partial match | |
| for (const tab of tabs) { | |
| const txt = normalizeText(tab.textContent); | |
| if (txt.includes(wantedLower)) return tab; | |
| } | |
| return null; | |
| } | |
| function isSelectedTab(tab) { | |
| if (!tab) return false; | |
| const ariaSelected = tab.getAttribute("aria-selected"); | |
| if (ariaSelected === "true") return true; | |
| // Some builds use data attributes or different patterns. | |
| // If this tab contains an element that looks like "selected", keep it conservative. | |
| const className = (tab.className || "").toString().toLowerCase(); | |
| if (className.includes("selected") || className.includes("active")) return true; | |
| return false; | |
| } | |
| function onHomeRoute() { | |
| const p = location.pathname || ""; | |
| return p === "/home"; | |
| } | |
| function trySwitchToFollowing(reason) { | |
| if (!onHomeRoute()) return; | |
| const t = nowMs(); | |
| if (t - lastActionAt < 1000) return; // throttle | |
| const followingTab = findTabByText(TARGET_TAB_TEXT); | |
| const forYouTab = findTabByText(AVOID_TAB_TEXT); | |
| // If we cannot find the tabs yet, bail and let observers retry. | |
| if (!followingTab) return; | |
| // If Following is already selected, nothing to do. | |
| if (isSelectedTab(followingTab)) return; | |
| // If For you is selected, click Following. | |
| // If we cannot detect selection, still click Following once. | |
| const shouldClick = | |
| (forYouTab && isSelectedTab(forYouTab)) || !isSelectedTab(followingTab); | |
| if (!shouldClick) return; | |
| lastActionAt = t; | |
| // Click in a way that works with React event handlers. | |
| followingTab.dispatchEvent( | |
| new MouseEvent("click", { bubbles: true, cancelable: true, view: window }) | |
| ); | |
| // Some UI states need focus first. | |
| try { | |
| followingTab.focus(); | |
| } catch (e) {} | |
| // If the first click fails due to overlay timing, schedule one retry. | |
| setTimeout(() => { | |
| if (!isSelectedTab(findTabByText(TARGET_TAB_TEXT))) { | |
| const retryTab = findTabByText(TARGET_TAB_TEXT); | |
| if (retryTab) { | |
| retryTab.dispatchEvent( | |
| new MouseEvent("click", { | |
| bubbles: true, | |
| cancelable: true, | |
| view: window, | |
| }) | |
| ); | |
| } | |
| } | |
| }, 700); | |
| } | |
| function scheduleTry(reason) { | |
| if (pending) return; | |
| pending = true; | |
| setTimeout(() => { | |
| pending = false; | |
| trySwitchToFollowing(reason); | |
| }, 50); | |
| } | |
| function hookHistory() { | |
| const origPushState = history.pushState; | |
| const origReplaceState = history.replaceState; | |
| function fire() { | |
| scheduleTry("route"); | |
| } | |
| history.pushState = function () { | |
| const r = origPushState.apply(this, arguments); | |
| fire(); | |
| return r; | |
| }; | |
| history.replaceState = function () { | |
| const r = origReplaceState.apply(this, arguments); | |
| fire(); | |
| return r; | |
| }; | |
| window.addEventListener("popstate", fire, true); | |
| } | |
| function observeDom() { | |
| const obs = new MutationObserver(() => { | |
| scheduleTry("dom"); | |
| }); | |
| obs.observe(document.documentElement, { | |
| childList: true, | |
| subtree: true, | |
| }); | |
| } | |
| // Init | |
| hookHistory(); | |
| observeDom(); | |
| // Initial attempt | |
| scheduleTry("init"); | |
| // Also retry a few times early to catch slow loads. | |
| let tries = 0; | |
| const early = setInterval(() => { | |
| tries += 1; | |
| scheduleTry("early"); | |
| if (tries >= 12) clearInterval(early); | |
| }, 500); | |
| })(); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment