Greasy Fork

Greasy Fork is available in English.

FB Mobile - Clean my feeds

Removes Sponsored and Suggested posts from Facebook mobile chromium/react version

当前为 2025-03-13 提交的版本,查看 最新版本

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Greasemonkey 油猴子Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Userscripts ,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==UserScript==
// @name        FB Mobile - Clean my feeds
// @namespace   Violentmonkey Scripts
// @match       https://m.facebook.com/*
// @match       https://www.facebook.com/*
// @match       https://touch.facebook.com/*
// @version     0.42
// @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==
// @run-at      document-end
// @author      https://github.com/webdevsk
// @description Removes Sponsored and Suggested posts from Facebook mobile chromium/react version
// @license     MIT
// @grant       GM_addStyle
// ==/UserScript==

// Some Things to note here
// This is a React site. Only #screen-root is shipped with the HTML. Everything inside is populated using JS.
// That makes it the perfect element to "observe".
// In order to reduce device memory usage, they remove/compress/disable posts that are far from the current scroll position.
// As they lose their organic Height, facebook uses (2) filler elements to make up for that empty space.
// As posts get constantly added/removed by themselves, you see some jitters while scrolling.
// We are removing posts ourselves. So the jitter happens way more often **SORRY**
// As the posts get removed, the filler elements height need to be adjusted as well. Thats where the jitter happens.
// As filler height goes from say 5000px to 500px in a second when we update it ourselves.
// After scrolling for a while, they just keep spamming suggested posts and ads. So you will often see the "Loading more posts" element.

const devMode = false
const showPlaceholder = true


// Make sure this is the React-Mobile version of facebook
if (document.body.id !== "app-body") {
    console.error("ID 'app-body' not found.")
    return
}

// React root
const root = document.querySelector('#screen-root')
if (!root) {
    console.error("screen-root not found")
    return
}

////////////////////////////////////////////////////////////////////////////////
////////////////////                   Classes           ////////////////////////
////////////////////////////////////////////////////////////////////////////////

class Spinner {
    constructor() {
        this.elm = document.createElement("div")
        this.elm.id = "block-counter"
        Object.assign(this.elm.style, { position: "fixed", top: "20px", left: "16px", pointerEvents: "none", zIndex: 100 })
        this.elm.innerHTML = `<div class="spinner small animated"></div>`
        document.body.appendChild(this.elm)
    }

    show() {
        this.elm.style.display = "block"
    }

    hide() {
        this.elm.style.display = "none"
    }
}

class BlockCounter {
    whitelisted = 0
    blacklisted = 0

    constructor() {
        if (!devMode) return
        this.elm = document.createElement("div")
        document.body.appendChild(this.elm)
        Object.assign(this.elm.style, { position: "fixed", top: 0, right: 0, padding: ".5rem 1rem", background: "#323436", borderRadius: ".2rem", display: "flex", flexFlow: "row wrap", zIndex: 99, color: "#ddd", gap: ".5rem", fontSize: ".8rem", pointerEvents: "none", })
        this.render()
    }

    render() {
        if (devMode) this.elm.innerHTML = `
            <p>Whitelisted: ${this.whitelisted}</p>
            <p>Blacklisted: ${this.blacklisted}</p>
        `
    }

    increaseWhite() {
        this.whitelisted += 1
        this.render()
    }

    increaseBlack() {
        this.blacklisted += 1
        this.render()
    }
}

////////////////////////////////////////////////////////////////////////////////
////////////////////                  Initials          ////////////////////////
////////////////////////////////////////////////////////////////////////////////

// Show counter on top
const counter = new BlockCounter()

// Show spinner while operating
const spinner = new Spinner()

// Auto reloads app when idle for 15 minutes
// This is to simulatate to ensure latest data when user comes back to his phone after a while
autoReloadAfterIdle()


// Some other styles
GM_addStyle(`

    /* remove install app toast */
    div[data-comp-id~="22222"]:has([data-action-id~="32764"]){
      display: none !important;
    }

`)

////////////////////////////////////////////////////////////////////////////////
////////////////////                   Labels           ////////////////////////
////////////////////////////////////////////////////////////////////////////////

// this version of fb does not update navigator.lang on language change
// navigator.langs contain all of your preset languages. So we need to loop through it
const getLabels = obj => navigator.languages.map(lang => obj[lang]).flat()

if (devMode) console.log("navigator.languages", navigator.languages)
// Placeholder Message
const placeholderMsg = getLabels({
    'en-US': 'Removed',
    'en': 'Removed',
    'bn': 'বাতিল'
})[0]
// To be fixed later

// Suggested
const suggested = getLabels({
    'en-US': 'Suggested',
    'en': 'Suggested',
    'bn': 'আপনার জন্য প্রস্তাবিত'
})

// Sponsored
const sponsored = getLabels({
    'en-US': 'Sponsored',
    'en': 'Sponsored',
    'bn': 'স্পনসর্ড'
})
// Uncategorized
const unCategorized = getLabels({
    'en-US': ['Join', 'Follow'],
    'en': ['Join', 'Follow'],
    'bn': ['ফলো করুন', 'যোগ দিন']
})



//Whatever we wanna do with the convicts
findConvicts((convicts) => {

    console.table(convicts)
    for (const { element, reason, author } of convicts) {
        element.tabIndex = "-1"
        element.dataset.purged = "true"


        // Sponsored posts get removed in an "out of order" fashion automatically.
        // Having placeholder inside them results in a  scroll jump
        if (showPlaceholder && !(sponsored.includes(reason))) {
            element.dataset.actualHeight = "32"
            Object.assign(element.style, {
                height: "32px",
                overflowY: "hidden",
                pointerEvents: "none",
                position: "relative"
            })

            const overlay = document.createElement("div")
            Object.assign(overlay.style, {
                position: "absolute",
                inset: 0,
                background: "#242526",
                color: "#e4e6eb",
                display: "grid",
                pointerEvents: "auto",
                placeItems: "center",
                paddingInline: ".5rem"
            })
            overlay.innerHTML = `
                <p style="text-overflow: ellipsis; overflow: hidden; white-space: nowrap; width: 100%; text-align: center;">
                    ${placeholderMsg}: ${author} (${reason})
                </p>
            `
            element.appendChild(overlay)

        } else {
            // Hide elements by resizing to 0px
            // Removing from DOM or display:none causes issues loading newer posts
            element.dataset.actualHeight = "0"
            Object.assign(element.style, {
                height: "0px",
                overflowY: "hidden",
                pointerEvents: "none"
            })

            //Hiding divider element preceding convicted element
            const { previousElementSibling: prevElm } = element
            if (prevElm.dataset.actualHeight !== "1") continue
            prevElm.style.marginTop = "0px"
            prevElm.style.height = "0px"
            prevElm.dataset.actualHeight = "0"
        }


        // Removing image links to restrict downloading unnecessary content
        for (const image of element.querySelectorAll("img")) {
            image.dataset.src = image.src
            //Clearing out src doesn't work as it gets populated again automatically
            image.removeAttribute("src")
            image.dataset.nulled = true
        }
    }

})


////////////////////////////////////////////////////////////////////////////////
////////////////////         function definitions       ////////////////////////
////////////////////////////////////////////////////////////////////////////////

function findConvicts(callback) {
    const observer = new MutationObserver((mutationList, observer) => {
        if (location.pathname !== '/') return
        if (devMode) console.time()
        spinner.show()
        const convicts = []

        for (const mutation of mutationList) {
            if (!(mutation.type === "childList" && mutation.target.matches("[data-type='vscroller']") && mutation.addedNodes.length !== 0)) continue
            // console.log(mutation)
            // console.table([...mutation.addedNodes].map(item => ({elm:item ,id: item.dataset.trackingDurationId, height: item.dataset.actualHeight})))
            for (const element of mutation.addedNodes) {
                // Check if element is an actual facebook post
                if (!(element.hasAttribute("data-tracking-duration-id"))) continue

                let suspect = false
                let reason
                let raw
                let author

                for (const span of element.querySelectorAll("span.f2:not(.a), span.f5")) {
                    if (![...suggested, ...sponsored, ...unCategorized].some(str => span.textContent.includes(str))) continue
                    suspect = true
                    reason = span.innerHTML.split("󰞋")[0]
                    raw = span.innerHTML
                    break
                }

                if (suspect) {
                    author = element.querySelector("span.f2").innerHTML
                    if (author.includes("Sponsored")) console.log("Author contains Sponsored", element)
                }

                if (suspect) {
                    convicts.push({
                        element,
                        reason,
                        raw,
                        id: element.dataset.trackingDurationId,
                        author
                    })
                    counter.increaseBlack()
                } else {
                    counter.increaseWhite()
                }

            }
        }

        if (!!convicts.length) callback(convicts)

        if (devMode) console.timeEnd()
        spinner.hide()
        // Set new calculated height to the bottom ".filler" element
        // We need to calculate it after all the convicts are taken care of
        // *** It seems we dont need it anymore. Completely hiding "Sponsored" posts fixed it for us
        // setFillerHeight(mutationList)
    })

    observer.observe(root, {
        childList: true,
        subtree: true,
    })
}

// setFillerHeight is omitted
// function setFillerHeight(mutationList) {
//     const fillerNode = document.querySelectorAll('.filler')[1]
//     if (!fillerNode) return
//     let newHeight = 0
//     for (const mutation of mutationList) {
//         if (!(mutation.type === "childList" && mutation.target.matches("[data-type='vscroller']") && mutation.addedNodes.length !== 0)) continue

//         newHeight += [...mutation.addedNodes].reduce((accumulator, element) => (
//             accumulator += element.classList.contains('displayed') || element.classList.contains('filler') ? 0 : element.clientHeight
//         ), 0)
//     }
//     fillerNode.style.height = newHeight
// }


function autoReloadAfterIdle(minutes = 15) {
    let leaveTime

    document.addEventListener('visibilitychange', () => {
        if (document.hidden) {
            leaveTime = new Date()
        } else {
            let currentTime = new Date()
            let timeDiff = (currentTime - leaveTime) / 60000
            if (timeDiff > minutes) location.reload()
        }
    })
}