FB Mobile - Clean my feeds

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

  1. // ==UserScript==
  2. // @name FB Mobile - Clean my feeds
  3. // @namespace Violentmonkey Scripts
  4. // @match https://m.facebook.com/*
  5. // @match https://www.facebook.com/*
  6. // @match https://touch.facebook.com/*
  7. // @version 0.42
  8. // @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==
  9. // @run-at document-end
  10. // @author https://github.com/webdevsk
  11. // @description Removes Sponsored and Suggested posts from Facebook mobile chromium/react version
  12. // @license MIT
  13. // @grant GM_addStyle
  14. // ==/UserScript==
  15.  
  16. // Some Things to note here
  17. // This is a React site. Only #screen-root is shipped with the HTML. Everything inside is populated using JS.
  18. // That makes it the perfect element to "observe".
  19. // In order to reduce device memory usage, they remove/compress/disable posts that are far from the current scroll position.
  20. // As they lose their organic Height, facebook uses (2) filler elements to make up for that empty space.
  21. // As posts get constantly added/removed by themselves, you see some jitters while scrolling.
  22. // We are removing posts ourselves. So the jitter happens way more often **SORRY**
  23. // As the posts get removed, the filler elements height need to be adjusted as well. Thats where the jitter happens.
  24. // As filler height goes from say 5000px to 500px in a second when we update it ourselves.
  25. // After scrolling for a while, they just keep spamming suggested posts and ads. So you will often see the "Loading more posts" element.
  26.  
  27. const devMode = false
  28. const showPlaceholder = true
  29.  
  30.  
  31. // Make sure this is the React-Mobile version of facebook
  32. if (document.body.id !== "app-body") {
  33. console.error("ID 'app-body' not found.")
  34. return
  35. }
  36.  
  37. // React root
  38. const root = document.querySelector('#screen-root')
  39. if (!root) {
  40. console.error("screen-root not found")
  41. return
  42. }
  43.  
  44. ////////////////////////////////////////////////////////////////////////////////
  45. //////////////////// Classes ////////////////////////
  46. ////////////////////////////////////////////////////////////////////////////////
  47.  
  48. class Spinner {
  49. constructor() {
  50. this.elm = document.createElement("div")
  51. this.elm.id = "block-counter"
  52. Object.assign(this.elm.style, { position: "fixed", top: "20px", left: "16px", pointerEvents: "none", zIndex: 100 })
  53. this.elm.innerHTML = `<div class="spinner small animated"></div>`
  54. document.body.appendChild(this.elm)
  55. }
  56.  
  57. show() {
  58. this.elm.style.display = "block"
  59. }
  60.  
  61. hide() {
  62. this.elm.style.display = "none"
  63. }
  64. }
  65.  
  66. class BlockCounter {
  67. whitelisted = 0
  68. blacklisted = 0
  69.  
  70. constructor() {
  71. if (!devMode) return
  72. this.elm = document.createElement("div")
  73. document.body.appendChild(this.elm)
  74. 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", })
  75. this.render()
  76. }
  77.  
  78. render() {
  79. if (devMode) this.elm.innerHTML = `
  80. <p>Whitelisted: ${this.whitelisted}</p>
  81. <p>Blacklisted: ${this.blacklisted}</p>
  82. `
  83. }
  84.  
  85. increaseWhite() {
  86. this.whitelisted += 1
  87. this.render()
  88. }
  89.  
  90. increaseBlack() {
  91. this.blacklisted += 1
  92. this.render()
  93. }
  94. }
  95.  
  96. ////////////////////////////////////////////////////////////////////////////////
  97. //////////////////// Initials ////////////////////////
  98. ////////////////////////////////////////////////////////////////////////////////
  99.  
  100. // Show counter on top
  101. const counter = new BlockCounter()
  102.  
  103. // Show spinner while operating
  104. const spinner = new Spinner()
  105.  
  106. // Auto reloads app when idle for 15 minutes
  107. // This is to simulatate to ensure latest data when user comes back to his phone after a while
  108. autoReloadAfterIdle()
  109.  
  110.  
  111. // Some other styles
  112. GM_addStyle(`
  113.  
  114. /* remove install app toast */
  115. div[data-comp-id~="22222"]:has([data-action-id~="32764"]){
  116. display: none !important;
  117. }
  118.  
  119. `)
  120.  
  121. ////////////////////////////////////////////////////////////////////////////////
  122. //////////////////// Labels ////////////////////////
  123. ////////////////////////////////////////////////////////////////////////////////
  124.  
  125. // this version of fb does not update navigator.lang on language change
  126. // navigator.langs contain all of your preset languages. So we need to loop through it
  127. const getLabels = obj => navigator.languages.map(lang => obj[lang]).flat()
  128.  
  129. if (devMode) console.log("navigator.languages", navigator.languages)
  130. // Placeholder Message
  131. const placeholderMsg = getLabels({
  132. 'en-US': 'Removed',
  133. 'en': 'Removed',
  134. 'bn': 'বাতিল'
  135. })[0]
  136. // To be fixed later
  137.  
  138. // Suggested
  139. const suggested = getLabels({
  140. 'en-US': 'Suggested',
  141. 'en': 'Suggested',
  142. 'bn': 'আপনার জন্য প্রস্তাবিত'
  143. })
  144.  
  145. // Sponsored
  146. const sponsored = getLabels({
  147. 'en-US': 'Sponsored',
  148. 'en': 'Sponsored',
  149. 'bn': 'স্পনসর্ড'
  150. })
  151. // Uncategorized
  152. const unCategorized = getLabels({
  153. 'en-US': ['Join', 'Follow'],
  154. 'en': ['Join', 'Follow'],
  155. 'bn': ['ফলো করুন', 'যোগ দিন']
  156. })
  157.  
  158.  
  159.  
  160. //Whatever we wanna do with the convicts
  161. findConvicts((convicts) => {
  162.  
  163. console.table(convicts)
  164. for (const { element, reason, author } of convicts) {
  165. element.tabIndex = "-1"
  166. element.dataset.purged = "true"
  167.  
  168.  
  169. // Sponsored posts get removed in an "out of order" fashion automatically.
  170. // Having placeholder inside them results in a scroll jump
  171. if (showPlaceholder && !(sponsored.includes(reason))) {
  172. element.dataset.actualHeight = "32"
  173. Object.assign(element.style, {
  174. height: "32px",
  175. overflowY: "hidden",
  176. pointerEvents: "none",
  177. position: "relative"
  178. })
  179.  
  180. const overlay = document.createElement("div")
  181. Object.assign(overlay.style, {
  182. position: "absolute",
  183. inset: 0,
  184. background: "#242526",
  185. color: "#e4e6eb",
  186. display: "grid",
  187. pointerEvents: "auto",
  188. placeItems: "center",
  189. paddingInline: ".5rem"
  190. })
  191. overlay.innerHTML = `
  192. <p style="text-overflow: ellipsis; overflow: hidden; white-space: nowrap; width: 100%; text-align: center;">
  193. ${placeholderMsg}: ${author} (${reason})
  194. </p>
  195. `
  196. element.appendChild(overlay)
  197.  
  198. } else {
  199. // Hide elements by resizing to 0px
  200. // Removing from DOM or display:none causes issues loading newer posts
  201. element.dataset.actualHeight = "0"
  202. Object.assign(element.style, {
  203. height: "0px",
  204. overflowY: "hidden",
  205. pointerEvents: "none"
  206. })
  207.  
  208. //Hiding divider element preceding convicted element
  209. const { previousElementSibling: prevElm } = element
  210. if (prevElm.dataset.actualHeight !== "1") continue
  211. prevElm.style.marginTop = "0px"
  212. prevElm.style.height = "0px"
  213. prevElm.dataset.actualHeight = "0"
  214. }
  215.  
  216.  
  217. // Removing image links to restrict downloading unnecessary content
  218. for (const image of element.querySelectorAll("img")) {
  219. image.dataset.src = image.src
  220. //Clearing out src doesn't work as it gets populated again automatically
  221. image.removeAttribute("src")
  222. image.dataset.nulled = true
  223. }
  224. }
  225.  
  226. })
  227.  
  228.  
  229. ////////////////////////////////////////////////////////////////////////////////
  230. //////////////////// function definitions ////////////////////////
  231. ////////////////////////////////////////////////////////////////////////////////
  232.  
  233. function findConvicts(callback) {
  234. const observer = new MutationObserver((mutationList, observer) => {
  235. if (location.pathname !== '/') return
  236. if (devMode) console.time()
  237. spinner.show()
  238. const convicts = []
  239.  
  240. for (const mutation of mutationList) {
  241. if (!(mutation.type === "childList" && mutation.target.matches("[data-type='vscroller']") && mutation.addedNodes.length !== 0)) continue
  242. // console.log(mutation)
  243. // console.table([...mutation.addedNodes].map(item => ({elm:item ,id: item.dataset.trackingDurationId, height: item.dataset.actualHeight})))
  244. for (const element of mutation.addedNodes) {
  245. // Check if element is an actual facebook post
  246. if (!(element.hasAttribute("data-tracking-duration-id"))) continue
  247.  
  248. let suspect = false
  249. let reason
  250. let raw
  251. let author
  252.  
  253. for (const span of element.querySelectorAll("span.f2:not(.a), span.f5")) {
  254. if (![...suggested, ...sponsored, ...unCategorized].some(str => span.textContent.includes(str))) continue
  255. suspect = true
  256. reason = span.innerHTML.split("󰞋")[0]
  257. raw = span.innerHTML
  258. break
  259. }
  260.  
  261. if (suspect) {
  262. author = element.querySelector("span.f2").innerHTML
  263. if (author.includes("Sponsored")) console.log("Author contains Sponsored", element)
  264. }
  265.  
  266. if (suspect) {
  267. convicts.push({
  268. element,
  269. reason,
  270. raw,
  271. id: element.dataset.trackingDurationId,
  272. author
  273. })
  274. counter.increaseBlack()
  275. } else {
  276. counter.increaseWhite()
  277. }
  278.  
  279. }
  280. }
  281.  
  282. if (!!convicts.length) callback(convicts)
  283.  
  284. if (devMode) console.timeEnd()
  285. spinner.hide()
  286. // Set new calculated height to the bottom ".filler" element
  287. // We need to calculate it after all the convicts are taken care of
  288. // *** It seems we dont need it anymore. Completely hiding "Sponsored" posts fixed it for us
  289. // setFillerHeight(mutationList)
  290. })
  291.  
  292. observer.observe(root, {
  293. childList: true,
  294. subtree: true,
  295. })
  296. }
  297.  
  298. // setFillerHeight is omitted
  299. // function setFillerHeight(mutationList) {
  300. // const fillerNode = document.querySelectorAll('.filler')[1]
  301. // if (!fillerNode) return
  302. // let newHeight = 0
  303. // for (const mutation of mutationList) {
  304. // if (!(mutation.type === "childList" && mutation.target.matches("[data-type='vscroller']") && mutation.addedNodes.length !== 0)) continue
  305.  
  306. // newHeight += [...mutation.addedNodes].reduce((accumulator, element) => (
  307. // accumulator += element.classList.contains('displayed') || element.classList.contains('filler') ? 0 : element.clientHeight
  308. // ), 0)
  309. // }
  310. // fillerNode.style.height = newHeight
  311. // }
  312.  
  313.  
  314. function autoReloadAfterIdle(minutes = 15) {
  315. let leaveTime
  316.  
  317. document.addEventListener('visibilitychange', () => {
  318. if (document.hidden) {
  319. leaveTime = new Date()
  320. } else {
  321. let currentTime = new Date()
  322. let timeDiff = (currentTime - leaveTime) / 60000
  323. if (timeDiff > minutes) location.reload()
  324. }
  325. })
  326. }
  327.  
  328.  
  329.  

QingJ © 2025

镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址