-
Notifications
You must be signed in to change notification settings - Fork 1
/
FB Mobile - Clean my feeds.user.js
334 lines (273 loc) · 12.5 KB
/
FB Mobile - Clean my feeds.user.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
// ==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
// @downloadURL https://update.greasyfork.org/scripts/479868/FB%20Mobile%20-%20Clean%20my%20feeds.user.js
// @updateURL https://update.greasyfork.org/scripts/479868/FB%20Mobile%20-%20Clean%20my%20feeds.meta.js
// ==/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()
}
})
}