// ==UserScript== // @name FB Mobile - Clean my feeds // @namespace Violentmonkey Scripts // @version 1.03 // @description Removes Sponsored and Suggested posts from Facebook mobile chromium/react version // @license GNU General Public License v3.0 // @author https://github.com/webdevsk // @match https://m.facebook.com/* // @match https://www.facebook.com/* // @match https://touch.facebook.com/* // @icon data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABHNCSVQICAgIfAhkiAAAAAlwSFlzAAAAbwAAAG8B8aLcQwAAABl0RVh0U29mdHdhcmUAd3d3Lmlua3NjYXBlLm9yZ5vuPBoAAAHZSURBVDiNnZFLSFRxFMa/c1/jjIzYpGEjxFQUCC5a9BKJIAtRzEXEFaJFZXRrIQMtk3a1lWo3iwqkTS0kZyGCA4VNFNEmWwU9MIoiscZp7jzuvf9zWogXogS9Z3fO4fv4feeQiCBKjY8M9Nca3lUtkhqAUnwNoPcUheC63b+z5qm3nmelIxGwkMMir+/MzJSNzYodZ7/ZolKXADoDAJsmSJXahpXiXxPThdlIBlCSFUh+rd1wBNvuttLu1sOGae7zYjy4Nt8QgXpoXbzf9/HVYNfi3O+KK5XP5V3rEti2rde3pHvyuVtFAMB8/JjWJLlEU0M7nlnE0e1fjGVqPgVg4b8E0rHnHoSeDY1mx/CCUiIyiVZdQ8YE7bVgdpCWCqrj6xIQ0Rtm/qlB3okXywHoDJcxAnWa0OPtpb8M8nPP06V6tVD3/Mqj2zcOApjA0/g5AU6HYl7llcAANP4WHnH6SfEQ65hPJuJdvh8cuDs165y8nO1bqiZb4KoyVhhYVoDLqxEDAwT+EBqwwAGwm4jQmmyGF/g3Y3pi+MLU2U9UCjKUwCga/BUmAT8CiDIAnRfCyI8LxSNCeABgh1uro+zWlq7YQ9v++WXe7GWDziu/bcS0+AQGvr8EgD/aK7uaswjePgAAAABJRU5ErkJggg== // @grant GM_getValue // @grant GM_setValue // @grant GM_addStyle // @run-at document-end // @downloadURL https://update.greasyfork.icu/scripts/479868/FB%20Mobile%20-%20Clean%20my%20feeds.user.js // @updateURL https://update.greasyfork.icu/scripts/479868/FB%20Mobile%20-%20Clean%20my%20feeds.meta.js // ==/UserScript== // src/config.ts var devMode = false; var bodyId = "app-body"; var screenRootSelector = "#screen-root"; var routeNodeSelector = "[data-screen-id]:first-child"; var postContainerSelector = "[data-pull-to-refresh-action-id]"; var possibleTargetsSelectorInPost = "span.f2:not(.a), span.f5, [style^='margin-top:9px; height:21px'] > .native-text"; var navBarSelector = "[role='tablist']"; var runScriptOn = ["feed"]; var showPlaceholder = true; var theme = { textColor: "ffffff", iconColor: "e4e6eb", bgClassName: "bg-fallback", iconBgClassName: "icon-bg-fallback" }; // src/lib/block-counter.ts class BlockCounter { static instance = null; elm = null; whitelisted = 0; blacklisted = 0; constructor() {} static getInstance() { if (!BlockCounter.instance) { BlockCounter.instance = new BlockCounter; } return BlockCounter.instance; } register() { if (!this.elm) { this.elm = document.createElement("div"); this.elm.id = "block-counter"; document.body.appendChild(this.elm); if (devMode) console.log("block counter register successful"); } else if (!document.body.contains(this.elm)) { document.body.appendChild(this.elm); if (devMode) console.log("block counter register successful"); } this.render(); return () => this.destroy(); } render() { if (this.elm) { this.elm.innerHTML = `
Whitelisted: ${this.whitelisted}
Blacklisted: ${this.blacklisted}
`; } } destroy() { if (this.elm && document.body.contains(this.elm)) { this.elm.remove(); } } increaseWhite() { this.whitelisted += 1; this.render(); } increaseBlack() { this.blacklisted += 1; this.render(); } } // src/lib/get-current-page.ts var getCurrentPage = () => { return document.querySelector(`${navBarSelector} > [aria-selected="true"]`)?.getAttribute("aria-label")?.split(",")[0] ?? "unknown"; }; // src/lib/make-navbar-sticky.ts var makeNavbarSticky = () => { const navbar = document.querySelector(navBarSelector); const screenRoot = document.querySelector(screenRootSelector); navbar.classList.add(theme.bgClassName); Object.assign(navbar.style, { position: "sticky", top: "-1px", zIndex: "1", insetInline: "0" }); Object.assign(screenRoot.style, { overflow: "visible" }); }; // src/lib/menu-buttons-injector.ts class MenuButtonsInjector { static instance = null; buttonsInjected = false; buttonElements = []; constructor() {} static getInstance() { if (!MenuButtonsInjector.instance) { MenuButtonsInjector.instance = new MenuButtonsInjector; } return MenuButtonsInjector.instance; } inject() { if (this.buttonsInjected) { console.warn("Menu buttons already injected"); return () => { this.destroy(); }; } this.injectButtons(); this.buttonsInjected = true; if (devMode) console.log("Menu buttons injected successfully"); return () => { this.destroy(); }; } setupTabBarStyles() { const tabBarEle = document.querySelector('[role="tablist"]'); if (tabBarEle) { tabBarEle.style.position = "sticky"; tabBarEle.style.zIndex = "1"; tabBarEle.style.top = "0"; } } createButton(id, imgSrc) { const button = document.createElement("div"); button.id = id; button.className = "customBtns"; button.innerHTML = `${placeHolderMessage}: ${author} (${filter})
`; element.appendChild(overlay); } else { element.dataset.actualHeight = "0"; element.dataset.forceHide = "true"; element.style.height = "0rem"; const { previousElementSibling: prevElm } = element; if (!(prevElm && prevElm instanceof HTMLElement) || prevElm.dataset.actualHeight !== "1") return; prevElm.style.marginTop = "0px"; prevElm.style.height = "0px"; prevElm.dataset.actualHeight = "0"; } for (const image of element.querySelectorAll("img")) { image.dataset.src = image.src; image.removeAttribute("src"); image.dataset.nulled = "true"; } }; // src/lib/spinner.ts class Spinner { static instance = null; elm = null; isVisible = false; constructor() {} static getInstance() { if (!Spinner.instance) { Spinner.instance = new Spinner; } return Spinner.instance; } register() { if (!this.elm) { this.elm = document.createElement("div"); this.elm.id = "spinner"; this.elm.innerHTML = ``; document.body.appendChild(this.elm); if (devMode) console.log("Spinner register successful"); } else if (!document.body.contains(this.elm)) { document.body.appendChild(this.elm); if (devMode) console.log("Spinner register successful"); } return () => this.destroy(); } destroy() { if (this.elm && document.body.contains(this.elm)) { this.elm.remove(); } } show() { this.isVisible = true; if (this.elm) { this.elm.style.display = "block"; } } hide() { this.isVisible = false; if (this.elm) { this.elm.style.display = "none"; } } } // src/lib/whitelisted-filters-storage.ts class WhitelistedFiltersStorage { storageKey = "whitelisted-filters"; defaultValue = []; cache = []; static instance = null; listeners = new Set; notifyListeners(newValue) { for (const listener of this.listeners) listener(newValue); } constructor() { this.cache = GM_getValue(this.storageKey, this.defaultValue); } static getInstance() { if (!WhitelistedFiltersStorage.instance) { WhitelistedFiltersStorage.instance = new WhitelistedFiltersStorage; } return WhitelistedFiltersStorage.instance; } get() { return this.cache; } set(value) { if (!Array.isArray(value) || !value.every((val) => typeof val === "string")) { console.error("Invalid value set for whitelisted filters", value); return; } if (devMode) console.log("Set new filters", value); GM_setValue(this.storageKey, value); this.cache = value; this.notifyListeners(value); } onChange(cb) { this.listeners.add(cb); return () => { this.listeners.delete(cb); }; } } // src/lib/run-feeds-cleaner.ts var runFeedsCleaner = () => { if (devMode) console.log("navigator.languages", navigator.languages); const root = document.querySelector(postContainerSelector); const whitelistedStorageInstance = WhitelistedFiltersStorage.getInstance(); const allFilters = Object.keys(filtersDatabase); const whitelistedFilters = whitelistedStorageInstance.get(); const activeFilters = []; const setActiveFilters = (whitelistedFilters2) => { activeFilters.length = 0; activeFilters.push(...new Set(allFilters.flatMap((filter) => whitelistedFilters2.includes(filter) ? [] : getOwnLangFilters(filtersDatabase[filter].keywordsDB)).filter((d) => d))); }; setActiveFilters(whitelistedFilters); const unsubscribeFeedsChangeEvent = whitelistedStorageInstance.onChange(setActiveFilters); const sponsoredFilters = getOwnLangFilters(filtersDatabase.sponsored.keywordsDB); const placeHolderMessage = getOwnLangFilters(keywordsPerLanguage.placeholderMessage)[0]; const checkElement = (element) => { if (element.dataset.purged === "true") return; let flagged = false; let matchedfilter; let reason; for (const span of element.querySelectorAll(possibleTargetsSelectorInPost)) { let done = false; for (const filter of activeFilters) { if (!span.textContent?.includes(filter)) continue; flagged = true; matchedfilter = filterTitlePerKeywordIndex.get(filter); reason = span.innerHTML; if (devMode) console.log(`Flagged post containing: "${reason}" with filter: "${matchedfilter}"`); done = true; break; } if (done) break; } if (!flagged) { BlockCounter.getInstance().increaseWhite(); return; } BlockCounter.getInstance().increaseBlack(); purgeElement({ element, reason, author: element.querySelector("span.f2")?.innerHTML ?? "", placeHolderMessage, sponsoredFilters, filter: matchedfilter }); }; if (devMode) console.log("Initial checking"); for (const element of root.querySelectorAll("[data-tracking-duration-id]")) { checkElement(element); } if (devMode) console.log("Initial checking done"); if (devMode) console.log("Mutation observer setup for new posts"); const observer = new MutationObserver((mutations) => { for (const mutation of mutations) { if (mutation.addedNodes.length === 0) continue; if (devMode) console.log("Checking posts count:", mutation.addedNodes.length); Spinner.getInstance().show(); for (const element of mutation.addedNodes) { if (!(element instanceof HTMLElement) || element.nodeType !== Node.ELEMENT_NODE) continue; if (!element.hasAttribute("data-tracking-duration-id")) continue; checkElement(element); } Spinner.getInstance().hide(); } }); observer.observe(root, { childList: true }); if (devMode) console.log("Mutation observer setup for new posts done on", root); return () => { observer.disconnect(); unsubscribeFeedsChangeEvent(); if (devMode) console.log("Mutation observer for posts disconnected"); }; }; // src/lib/settings-menu-injector.ts var closeMenuIcon = "\uDB85\uDE73"; class SettingsMenuInjector { static instance; ctrl; overlayId = "settingsOverlay"; constructor() { this.ctrl = new AbortController; } static getInstance() { if (!SettingsMenuInjector.instance) { SettingsMenuInjector.instance = new SettingsMenuInjector; } return SettingsMenuInjector.instance; } generateSettingsOverlay() { return `