- // ==UserScript==
- // @name Facebook cleaner
- // @namespace https://lukaszmical.pl/
- // @version 0.2.1
- // @description This script hides sponsored posts (ads) on Facebook, making your feed cleaner and free from distractions.
- // @author Łukasz Micał
- // @match https://*.facebook.com/*
- // @grant GM_registerMenuCommand
- // @grant GM_unregisterMenuCommand
- // @grant GM_setValue
- // @grant GM_getValue
- // @grant window.onurlchange
- // @icon https://www.google.com/s2/favicons?sz=64&domain=facebook.com
- // ==/UserScript==
-
- // css:apps/facebook-cleaner/src/style/style-debug.css
- const style_debug_default =
- '[data-fcc="@"]{position:relative;}[data-fcc="@"]:before{display:block;content:attr(data-fcc-reason);position:absolute;top:0;left:50%;border-radius:16px;padding:8px;background-color:var(--card-background);transform:translateX(-50%);color:var(--primary-text);z-index:999;border:1px solid var(--primary-button-background)}';
-
- // css:apps/facebook-cleaner/src/style/style.css
- const style_default =
- '[data-fcc="@"]{display:none !important;}[data-fcc="@"][data-fcc-type="@"]{display:block !important;max-height:24px;overflow:hidden;margin-bottom:16px;padding-top:24px;border-radius:16px;box-sizing:border-box;position:relative;background-color:var(--card-background);}[data-fcc="@"][data-fcc-type="@"] *{max-height:2px;}[data-fcc="@"][data-fcc-type="@"]:before{display:block;content:attr(title);position:absolute;top:4px;left:50%;transform:translateX(-50%);color:var(--primary-text);}';
-
- // libs/share/src/ui/GlobalStyle.ts
- const GlobalStyle = class {
- static addStyle(key, styles) {
- const style =
- document.getElementById(key) ||
- (function () {
- const style2 = document.createElement('style');
- style2.id = key;
- document.head.appendChild(style2);
- return style2;
- })();
- style.textContent = styles;
- }
- };
-
- // libs/share/src/utils/urlChangeEvent.ts
- function activateUrlChangeEvents() {
- if (!window.onurlchange) {
- const dispatchUrlChangeEvent = function () {
- window.dispatchEvent(new CustomEvent('urlchange'));
- };
- window.addEventListener('popstate', dispatchUrlChangeEvent);
- const originalPushState = history.pushState;
- history.pushState = function (...args) {
- originalPushState.apply(this, args);
- dispatchUrlChangeEvent();
- };
- const originalReplaceState = history.replaceState;
- history.replaceState = function (...args) {
- originalReplaceState.apply(this, args);
- dispatchUrlChangeEvent();
- };
- }
- }
-
- // libs/share/src/ui/Observer.ts
- const Observer = class {
- start(element, callback, options) {
- this.stop();
- this.observer = new MutationObserver(callback);
- this.observer.observe(
- element,
- options || {
- attributeOldValue: true,
- attributes: true,
- characterData: true,
- characterDataOldValue: true,
- childList: true,
- subtree: true,
- }
- );
- }
-
- stop() {
- if (this.observer) {
- this.observer.disconnect();
- }
- }
- };
-
- // apps/facebook-cleaner/src/dictionary/locales.ts
- const locales = {
- id: {
- hiddenPost: 'Postingan tersembunyi',
- feed: 'Postingan Kabar Beranda',
- follow: 'Ikuti',
- join: 'Gabung',
- reels: 'Reels',
- sponsored: 'Bersponsor',
- },
- cs: {
- hiddenPost: 'Skryt\xE9 p\u0159\xEDsp\u011Bvky',
- feed: 'P\u0159\xEDsp\u011Bvku v kan\xE1lu vybran\xFDch p\u0159\xEDsp\u011Bvk\u016F',
- follow: 'Sledovat',
- join: 'P\u0159idat se',
- reels: 'Reels',
- sponsored: 'Sponzorov\xE1no',
- },
- de: {
- hiddenPost: 'Versteckte Beitr\xE4ge',
- feed: 'News Feed-Beitr\xE4ge',
- follow: 'Folgen',
- join: 'Beitreten',
- reels: 'Reels',
- sponsored: 'Anzeige',
- },
- en: {
- hiddenPost: 'Hidden posts',
- feed: 'News Feed posts',
- follow: 'Follow',
- join: 'Join',
- reels: 'Reels',
- sponsored: 'Sponsored',
- },
- es: {
- hiddenPost: 'Publicaciones ocultas',
- feed: 'Publicaciones de la secci\xF3n de noticias',
- follow: 'Seguir',
- join: 'Unirte',
- reels: 'Reels',
- sponsored: 'Publicidad',
- },
- fr: {
- hiddenPost: 'Messages masqu\xE9s',
- feed: 'Nouvelles publications du fil d\u2019actualit\xE9',
- follow: 'Suivre',
- join: 'Rejoindre',
- reels: 'Reels',
- sponsored: 'Sponsoris\xE9',
- },
- it: {
- hiddenPost: 'Post nascosti',
- feed: 'Post della sezione Notizie',
- follow: 'Segui',
- join: 'Iscriviti',
- reels: 'Reels',
- sponsored: 'Sponsorizzato',
- },
- pl: {
- hiddenPost: 'Ukryte posty',
- feed: 'Posty w Aktualno\u015Bciach',
- follow: 'Obserwuj',
- join: 'Do\u0142\u0105cz',
- reels: 'Rolki',
- sponsored: 'Sponsorowane',
- },
- pt: {
- hiddenPost: 'Postagens ocultas',
- feed: 'Publica\xE7\xF5es do Feed de Not\xEDcias',
- follow: 'Seguir',
- join: 'Participar',
- reels: 'Reels',
- sponsored: 'Patrocinado',
- },
- sk: {
- hiddenPost: 'Skryt\xE9 pr\xEDspevky',
- feed: 'Pr\xEDspevky v Novink\xE1ch',
- follow: 'Sledova\u0165',
- join: 'Prida\u0165 sa',
- reels: 'Reels',
- sponsored: 'Sponzorovan\xE9',
- },
- sl: {
- hiddenPost: 'Skrite objave',
- feed: 'Objave v viru novic',
- follow: 'Sledi',
- join: 'Pridru\u017Ei se',
- reels: 'Interaktivni videi',
- sponsored: 'Sponzorirano',
- },
- szl: {
- hiddenPost: 'Skryte posty',
- feed: 'Posty w Aktualno\u015Bciach',
- follow: 'Obserwuj',
- join: 'Do\u0142\u0105cz',
- reels: 'Rolki',
- sponsored: 'Szp\u014Dnzorowane',
- },
- tr: {
- hiddenPost: 'Gizli g\xF6nderiler',
- feed: 'Haber Kayna\u011F\u0131 g\xF6nderileri',
- follow: 'Takip Et',
- join: 'Kat\u0131l',
- reels: 'Reels',
- sponsored: 'Sponsorlu',
- },
- uk: {
- hiddenPost:
- '\u041F\u0440\u0438\u0445\u043E\u0432\u0430\u043D\u0456 \u043F\u043E\u0441\u0442\u0438/Prykhovani posty',
- feed: '\u0414\u043E\u043F\u0438\u0441\u0438 \u0437\u0456 \u0441\u0442\u0440\u0456\u0447\u043A\u0438 \u043D\u043E\u0432\u0438\u043D',
- follow: '\u0421\u0442\u0435\u0436\u0438\u0442\u0438',
- join: '\u041F\u0440\u0438\u0454\u0434\u043D\u0430\u0442\u0438\u0441\u044F',
- reels: '\u0412\u0456\u0434\u0435\u043E Reels',
- sponsored: '\u0420\u0435\u043A\u043B\u0430\u043C\u0430',
- },
- 'zh-Hans': {
- hiddenPost: '\u9690\u85CF\u5E16\u5B50',
- feed: '\u52A8\u6001\u6D88\u606F\u5E16\u5B50',
- follow: '\u5173\u6CE8',
- join: '\u52A0\u5165',
- reels: 'Reels',
- sponsored: '\u8D5E\u52A9\u5185\u5BB9',
- },
- 'zh-Hant': {
- hiddenPost: '\u96B1\u85CF\u8CBC\u6587',
- feed: '\u52D5\u614B\u6D88\u606F\u5E16\u5B50',
- follow: '\u8FFD\u8E64',
- join: '\u52A0\u5165',
- reels: 'Reels',
- sponsored: '\u8D0A\u52A9',
- },
- };
- const languages = Object.keys(locales);
-
- // apps/facebook-cleaner/src/dictionary/Dictionary.ts
- const Dictionary = class {
- constructor() {
- this.lang = this.detectLanguage();
- this.dictionary = this.getDictionary();
- }
-
- getFeedLabel() {
- return this.dictionary.feed;
- }
-
- getFollowLabel() {
- return this.dictionary.follow;
- }
-
- getJoinLabel() {
- return this.dictionary.join;
- }
-
- getReelsLabel() {
- return this.dictionary.reels;
- }
-
- getSponsoredLabel() {
- return this.dictionary.sponsored;
- }
-
- hiddenPostLabel(count) {
- return `${this.dictionary.hiddenPost} (${count})`;
- }
-
- detectLanguage() {
- const pageLang = window.document.documentElement.lang;
- if (pageLang && languages.includes(pageLang)) {
- return pageLang;
- }
- return void 0;
- }
-
- getDictionary() {
- if (this.lang) {
- return locales[this.lang];
- }
- return void 0;
- }
- };
-
- // apps/facebook-cleaner/src/services/ElementDetector.ts
- const ElementDetector = class {
- constructor() {
- this.dictionary = new Dictionary();
- }
-
- getElement(root, query, text) {
- return this.getElements(root, query, text)[0];
- }
-
- getElements(root, query, text) {
- return [...root.querySelectorAll(query)].filter((element) => {
- if (!text) {
- return true;
- }
- return element.textContent.includes(text);
- });
- }
-
- getFeedElement() {
- const [feedHeader] = this.getElements(
- document,
- 'h3.html-h3',
- this.dictionary.getFeedLabel()
- );
- if (!feedHeader) {
- return void 0;
- }
- return feedHeader.parentElement.lastElementChild;
- }
- };
-
- // apps/facebook-cleaner/src/services/UserSettings.ts
- const settingsMenuLabels = {
- ['fcc-hide-reels' /* HideReels */]: 'reels',
- ['fcc-hide-sponsored' /* HideSponsored */]: 'sponsored posts',
- ['fcc-hide-suggested-groups' /* HideSuggestedGroups */]: 'suggested groups',
- ['fcc-hide-suggested-profiles' /* HideSuggestedProfiles */]:
- 'suggested profiles',
- };
- const UserSettings = class {
- constructor() {
- this.setting = {
- ['fcc-hide-reels' /* HideReels */]: true,
- ['fcc-hide-sponsored' /* HideSponsored */]: true,
- ['fcc-hide-suggested-groups' /* HideSuggestedGroups */]: true,
- ['fcc-hide-suggested-profiles' /* HideSuggestedProfiles */]: true,
- };
- this.setting = this.readSettings();
- this.updateMenu();
- }
-
- getSettings() {
- return { ...this.setting };
- }
-
- readSettings() {
- return Object.fromEntries(
- Object.entries(this.setting).map(([key, defaultValue]) => [
- key,
- GM_getValue(key, defaultValue),
- ])
- );
- }
-
- setSettingValue(id, value) {
- this.setting[id] = value;
- GM_setValue(id, value);
- this.updateMenu();
- }
-
- settingLabel(id) {
- return [
- this.setting[id] ? 'Show' : 'Hide',
- settingsMenuLabels[id],
- 'in feed news',
- ].join(' ');
- }
-
- updateMenu() {
- Object.keys(this.setting).forEach((id) => GM_unregisterMenuCommand(id));
- Object.entries(this.setting).forEach(([id, value]) =>
- GM_registerMenuCommand(
- this.settingLabel(id),
- () => this.setSettingValue(id, !value),
- {
- id,
- autoClose: true,
- }
- )
- );
- }
- };
-
- // apps/facebook-cleaner/src/services/BannedPost.ts
- const BannedPost = class {
- constructor() {
- this.detector = new ElementDetector();
- this.dictionary = new Dictionary();
- }
-
- filter(posts, settings) {
- return posts.filter((post) => {
- if (post.dataset.fcc) {
- return true;
- }
- const query = '[data-ad-rendering-role="profile_name"] [role="button"]';
- if (
- settings['fcc-hide-suggested-profiles' /* HideSuggestedProfiles */] &&
- this.detector.getElement(post, query, this.dictionary.getFollowLabel())
- ) {
- post.dataset.fccReason = 'follow' /* Follow */;
- return true;
- }
- if (
- settings['fcc-hide-suggested-groups' /* HideSuggestedGroups */] &&
- this.detector.getElement(post, query, this.dictionary.getJoinLabel())
- ) {
- post.dataset.fccReason = 'join' /* Join */;
- return true;
- }
- if (
- settings['fcc-hide-reels' /* HideReels */] &&
- this.detector.getElement(
- post,
- '[role="button"]',
- this.dictionary.getReelsLabel()
- )
- ) {
- post.dataset.fccReason = 'reels' /* Reels */;
- return true;
- }
- if (
- settings['fcc-hide-sponsored' /* HideSponsored */] &&
- this.detector.getElement(post, 'a[href*="ads/about"]')
- ) {
- post.dataset.fccReason = 'sponsored-link' /* SponsoredLink */;
- return true;
- }
- if (
- settings['fcc-hide-sponsored' /* HideSponsored */] &&
- this.detector.getElement(
- post,
- 'a[attributionsrc] [aria-labelledby]',
- this.dictionary.getSponsoredLabel()
- )
- ) {
- post.dataset.fccReason = 'sponsored-label' /* SponsoredLabel */;
- return true;
- }
- const items = this.detector.getElements(
- post,
- 'a[attributionsrc] [aria-labelledby]'
- );
- if (
- settings['fcc-hide-sponsored' /* HideSponsored */] &&
- items.some(this.isSponsoredElement.bind(this))
- ) {
- post.dataset.fccReason =
- 'sponsored-hidden-label' /* SponsoredHiddenLabel */;
- return true;
- }
- return false;
- });
- }
-
- hide(post) {
- post.dataset.fcc = '@';
- post.dataset.fccType = '';
- }
-
- isSponsoredElement(element) {
- const sponsoredLabel = this.dictionary.getSponsoredLabel();
- const items = [...element.firstElementChild.children].filter((i) =>
- sponsoredLabel.includes(i.innerText)
- );
- if (items.length < sponsoredLabel.length) {
- return false;
- }
- const elementLabel = items
- .map((item) => {
- const styles = getComputedStyle(item);
- return {
- isVisible: styles.position !== 'absolute',
- order: Number(styles.order),
- text: item.innerText,
- };
- })
- .filter((item) => item.isVisible)
- .sort((a, b) => a.order - b.order)
- .map((item) => item.text)
- .join('');
- return elementLabel.includes(sponsoredLabel);
- }
-
- showHiddenPostGroups() {
- const hiddenPosts = document.querySelectorAll('[data-fcc="@"]');
- const hiddenPostsCount = (post) => {
- const nextPost = post.nextElementSibling;
- if (nextPost && nextPost.dataset.fcc) {
- return 1 + hiddenPostsCount(nextPost);
- }
- return 0;
- };
- [...hiddenPosts].forEach((post) => {
- const prevPost = post.previousElementSibling;
- if (!prevPost || (prevPost && !prevPost.dataset.fcc)) {
- const count = 1 + hiddenPostsCount(post);
- post.dataset.fccType = '@';
- post.title = this.dictionary.hiddenPostLabel(count);
- }
- });
- }
- };
-
- // apps/facebook-cleaner/src/services/FeedCleaner.ts
- const FeedCleaner = class {
- constructor() {
- this.bannedPostDetector = new BannedPost();
- this.settings = new UserSettings();
- }
-
- cleanFeed(feedElement) {
- const posts = [...feedElement.children];
- const bannedPosts = this.bannedPostDetector.filter(
- posts,
- this.settings.getSettings()
- );
- bannedPosts.forEach((post) => {
- this.bannedPostDetector.hide(post);
- });
- this.bannedPostDetector.showHiddenPostGroups();
- }
- };
-
- // apps/facebook-cleaner/src/services/FacebookCleaner.ts
- const FacebookCleaner = class {
- constructor() {
- this.detector = new ElementDetector();
- this.feedCleaner = new FeedCleaner();
- this.feedElement = void 0;
- this.observer = new Observer();
- this.initEvents();
- }
-
- run() {
- this.initFeedElement();
- this.initObserver();
- this.feedListUpdated();
- }
-
- feedListUpdated() {
- if (this.isValidElement(this.feedElement)) {
- this.feedCleaner.cleanFeed(this.feedElement);
- }
- }
-
- initEvents() {
- window.addEventListener('urlchange', () => {
- this.run();
- window.setTimeout(this.run.bind(this), 2e3);
- window.setTimeout(this.run.bind(this), 5e3);
- });
- window.setInterval(this.run.bind(this), 20 * 1e3);
- }
-
- initFeedElement() {
- if (!this.isValidElement(this.feedElement)) {
- this.feedElement = this.detector.getFeedElement();
- }
- }
-
- initObserver() {
- if (!this.isValidElement(this.feedElement)) {
- return this.observer.stop();
- }
- if (!this.feedElement.dataset.fccReady) {
- this.feedElement.dataset.fccReady = '1';
- this.observer.start(this.feedElement, this.feedListUpdated.bind(this), {
- childList: true,
- subtree: true,
- });
- }
- }
-
- isValidElement(element) {
- return element && element.isConnected;
- }
- };
-
- // apps/facebook-cleaner/src/main.ts
- activateUrlChangeEvents();
- const isDebug = false;
- GlobalStyle.addStyle(
- 'fcc-style',
- isDebug ? style_debug_default : style_default
- );
- const service = new FacebookCleaner();
- service.run();