// ==UserScript==
// @name FB Mobile - Clean my feeds
// @namespace Violentmonkey Scripts
// @match https://m.facebook.com/*
// @match https://www.facebook.com/*
// @version 0.41
// @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.documentElement.classList.contains("ssr")) return
// React root
const root = document.querySelector('#screen-root')
if (!root) 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( img[src*="MpdfZ1mwXmC.png"]){
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.f5")) {
if (![...suggested, ...sponsored].some(str => span.textContent.includes(str))) continue
suspect = true
reason = span.innerHTML.split("")[0]
raw = span.innerHTML
break
}
if (!suspect) {
const span = element.querySelector("span.f2:not(.a)")
if (span && unCategorized.some(str => span.textContent === str)) {
suspect = true
reason = span.textContent
raw = span.textContent
}
}
if (suspect) {
author = element.querySelector("span.f2").innerHTML
if (author.includes("Sponsored")) console.log(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()
}
})
}