// ==UserScript==
// @name Facebook Login Wall Remover
// @name:en Facebook Login Wall Remover
// @name:zh-TW Facebook 登入牆移除器
// @name:ja Facebook ログインウォールリムーバー
// @namespace https://greasyfork.org/en/users/1467948-stonedkhajiit
// @version 0.1.1
// @description [Desktop Site | Guest Mode Only] Removes login popups/banners, fixes page jumps, and opens media in a new tab to prevent deadlocks. Disables itself when logged in.
// @description:en [Desktop Site | Guest Mode Only] Removes login popups and banners, prevents page jumps, and automatically opens media in a new tab to prevent page deadlocks. Automatically disables itself when a logged-in state is detected.
// @description:zh-TW 【桌面版網頁|未登入專用】移除登入提示與橫幅、解決頁面跳轉,並自動在新分頁開啟媒體以從根源上防止頁面死鎖。偵測到登入狀態時將自動停用。
// @description:ja 【デスクトップサイト|未ログイン専用】ログインポップアップとバナーを削除し、ページのジャンプを修正します。デッドロックを防ぐためにメディアを新しいタブで自動的に開き、ログイン状態では自動的に無効になります。
// @author StonedKhajiit
// @match *://*.facebook.com/*
// @icon https://www.facebook.com/favicon.ico
// @grant GM_notification
// @grant GM_getValue
// @grant GM_setValue
// @grant GM_registerMenuCommand
// @grant GM_unregisterMenuCommand
// @run-at document-start
// @license MIT
// ==/UserScript==
(function() {
'use strict';
// --- Configuration ---
const CONFIG = {
LOG_PREFIX: `[FB Login Wall Remover]`,
THROTTLE_DELAY: 250,
PROCESSED_MARKER: 'gm-processed',
SCROLL_RESTORER: {
CORRECTION_DURATION: 250, // ms: How long to force the scroll position.
CORRECTION_FREQUENCY: 16, // ms: How often to force the scroll position (16ms ~ 60fps).
WATCHER_FREQUENCY: 150, // ms: How often to check for modal changes.
MODAL_GRACE_PERIOD: 300, // ms: Time to wait before considering a modal "closed" to ignore transitional states.
},
SELECTORS: {
// Main component selectors
POST_CONTAINER: 'div[role="article"]',
MODAL_CONTAINER: 'div.__fb-light-mode',
DIALOG: '[role="dialog"]',
LOGIN_FORM: 'form#login_form, form[id="login_popup_cta_form"]',
// Media links that trigger modals
MEDIA_LINK: `
a[href*="/photo"], a[href*="fbid="],
a[href*="/videos/"], a[href*="/watch/"],
a[href*="/reel/"]
`.trim().replace(/\s+/g, ' '),
// A robust, multi-language selector for close buttons
CLOSE_BUTTON: `
[aria-label="Close"][role="button"],
[aria-label="關閉"][role="button"],
[aria-label="閉じる"][role="button"],
[aria-label="Cerrar"][role="button"],
[aria-label="Fermer"][role="button"],
[aria-label="Schließen"][role="button"],
[aria-label="Fechar"][role="button"],
[aria-label="Chiudi"][role="button"],
[aria-label="Sluiten"][role="button"],
[aria-label="Закрыть"][role="button"],
[aria-label="Kapat"][role="button"],
[aria-label="Zamknij"][role="button"],
[aria-label="Tutup"][role="button"],
[aria-label="Đóng"][role="button"],
[aria-label="ปิด"][role="button"],
[aria-label="Zatvori"][role="button"],
[aria-label="Zavrieť"][role="button"],
[aria-label="Zavřít"][role="button"],
[aria-label="Bezárás"][role="button"],
div[role="button"]:has(i[data-visualcompletion="css-img"])
`.trim().replace(/\s+/g, ' '),
},
};
// --- Internationalization ---
const STRINGS = {
en: {
notificationTitle: 'Notice',
notificationDeadlock: 'A non-closable login modal was hidden. The page is now locked and won\'t load new content.\n\nPro-Tip: To view media without locking your feed, open it in a new tab (middle-click). Please reload this page.',
autoOpenMediaInNewTab: 'Auto-open media in new tab (prevents deadlock)',
showDeadlockNotification: 'Show deadlock notification',
hideUselessElements: 'Hide useless UI elements (for guest)',
},
'zh-TW': {
notificationTitle: '注意',
notificationDeadlock: '一個無關閉按鈕的登入提示已被隱藏,頁面現已鎖定且無法載入新內容。\n\n提示:為避免動態牆被鎖定,建議以新分頁(滑鼠中鍵)開啟圖片或影片。請重新整理此頁面以繼續。',
autoOpenMediaInNewTab: '在新分頁開啟媒體 (防死鎖)',
showDeadlockNotification: '顯示頁面死鎖通知',
hideUselessElements: '隱藏訪客模式下的多餘介面',
},
ja: {
notificationTitle: '通知',
notificationDeadlock: '閉じるボタンのないログインモーダルを非表示にしました。ページがロックされ、新しいコンテンツは読み込まれません。\n\nヒント:フィードをロックせずにメディアを表示するには、新しいタブで開く(マウスの中央ボタンでクリック)ことをお勧めします。このページをリロードしてください。',
autoOpenMediaInNewTab: 'メディアを新しいタブで開く (デッドロック防止)',
showDeadlockNotification: 'デッドロック通知を表示',
hideUselessElements: '不要なUI要素を非表示にする(ゲスト用)',
},
};
/**
* Determines the best language for UI strings based on browser settings.
* @returns {object} The string object for the detected language.
*/
function getStrings() {
const lang = navigator.language.toLowerCase();
if (lang.startsWith('ja')) return STRINGS.ja;
if (lang.startsWith('zh')) return STRINGS['zh-TW'];
return STRINGS.en;
}
/**
* Checks if the user is logged into Facebook.
* @returns {boolean} True if logged in, false otherwise.
*/
function isLoggedIn() {
// Presence of user-specific links indicates a logged-in state.
if (document.querySelector('a[href="/friends/"]') || document.querySelector('a[href*="/watch/"]')) return true;
// Presence of login forms indicates a logged-out state.
if (document.querySelector('form#login_form') || document.querySelector('a[href*="/login/"]')) return false;
// Default to not logged in if unsure.
return false;
}
// --- Settings Manager ---
const settings = {};
const SettingsManager = {
registeredCommands: [],
definitions: [
{ key: 'autoOpenMediaInNewTab', label: 'Auto-open media in new tab (prevents deadlock)', type: 'toggle', defaultValue: true },
{ key: 'hideUselessElements', label: 'Hide useless UI elements (for guest)', type: 'toggle', defaultValue: true },
{ key: 'showDeadlockNotification', label: 'Show deadlock notification', type: 'toggle', defaultValue: true },
],
init(T) {
this.definitions.forEach(def => {
settings[def.key] = GM_getValue(def.key, def.defaultValue);
});
this.renderMenu(T);
},
renderMenu(T) {
this.registeredCommands.forEach(id => GM_unregisterMenuCommand(id));
this.registeredCommands = [];
this.definitions.forEach(def => {
const status = settings[def.key] ? '✅' : '❌';
const label = `${status} ${T[def.key] || def.label}`;
this.registeredCommands.push(GM_registerMenuCommand(label, () => this.updateSetting(def.key, !settings[def.key], T)));
});
},
updateSetting(key, value, T) {
GM_setValue(key, value);
settings[key] = value;
this.renderMenu(T);
// Inform user that a reload is needed for style changes to take effect.
if (key === 'hideUselessElements') {
alert('Please reload the page for this setting to take full effect.');
}
},
};
/**
* Intercepts and blocks scroll event listeners to prevent the page from locking.
* This aggressive strategy is applied at the earliest possible stage.
*/
function setupInterceptors() {
const originalAddEventListener = EventTarget.prototype.addEventListener;
Object.defineProperty(EventTarget.prototype, 'addEventListener', {
configurable: true,
enumerable: true,
get: () =>
function(type, listener, options) {
if (type === 'scroll') return;
return originalAddEventListener.call(this, type, listener, options);
},
});
}
/**
* Injects CSS rules to hide static UI elements for better performance.
* @param {object} currentSettings - The current user settings object.
*/
function injectHidingStyles(currentSettings) {
const rules = [];
// General login/register banner at the bottom.
rules.push(`div[data-nosnippet]:has(a[href*="/login/"]):has(a[href*="/reg/"])`);
// Top login banner, based on user settings.
if (currentSettings.hideUselessElements) {
rules.push(`div[role="banner"]:has(${CONFIG.SELECTORS.LOGIN_FORM})`);
}
if (rules.length === 0) return;
const css = `${rules.join(',\n')} { display: none !important; }`;
const styleElement = document.createElement('style');
styleElement.textContent = css.trim();
document.head.appendChild(styleElement);
console.log(`${CONFIG.LOG_PREFIX} [StyleInjector] Injected ${rules.length} hiding rule(s).`);
}
/**
* Counteracts page position jumps caused by opening media modals.
* Facebook forcibly scrolls the background page to the top when a modal is opened.
* This module records the scroll position just before the modal opens and restores
* it after the modal is closed.
* @returns {{init: function}} The scroll restorer instance.
*/
function createScrollRestorer() {
let restoreY = null;
let watcherInterval = null;
let correctionInterval = null;
const stopWatcher = () => {
if (!watcherInterval) return;
clearInterval(watcherInterval);
watcherInterval = null;
};
const forceScrollCorrection = () => {
if (restoreY === null) return;
if (correctionInterval) clearInterval(correctionInterval);
const { CORRECTION_DURATION, CORRECTION_FREQUENCY } = CONFIG.SCROLL_RESTORER;
const startTime = Date.now();
const initialRestoreY = restoreY;
correctionInterval = setInterval(() => {
window.scrollTo({ top: initialRestoreY, behavior: 'instant' });
if (Date.now() - startTime > CORRECTION_DURATION) {
clearInterval(correctionInterval);
correctionInterval = null;
restoreY = null;
}
}, CORRECTION_FREQUENCY);
};
const startWatcher = () => {
stopWatcher();
let isContentModalDetected = false;
let modalFirstSeenTime = null;
watcherInterval = setInterval(() => {
const modal = document.querySelector(CONFIG.SELECTORS.DIALOG);
if (!isContentModalDetected && modal && !modal.querySelector(CONFIG.SELECTORS.LOGIN_FORM)) {
isContentModalDetected = true;
modalFirstSeenTime = Date.now();
} else if (isContentModalDetected && !modal) {
if (Date.now() - modalFirstSeenTime > CONFIG.SCROLL_RESTORER.MODAL_GRACE_PERIOD) {
stopWatcher();
forceScrollCorrection();
}
}
}, CONFIG.SCROLL_RESTORER.WATCHER_FREQUENCY);
};
const handleCloseClick = e => {
const closeButton = e.target.closest(CONFIG.SELECTORS.CLOSE_BUTTON);
if (closeButton && restoreY !== null && watcherInterval) {
stopWatcher();
forceScrollCorrection();
}
};
const recordClick = e => {
if (watcherInterval || correctionInterval) return;
const postContainer = e.target.closest(CONFIG.SELECTORS.POST_CONTAINER);
const externalLink = e.target.closest('a[target="_blank"]');
if (postContainer && !externalLink) {
restoreY = window.scrollY;
// Delay watcher start to avoid race conditions with Facebook's event handling.
// This ensures it runs after Facebook's own 'click' handlers have finished.
setTimeout(startWatcher, 0);
}
};
return {
init: () => {
document.body.addEventListener('click', recordClick, true);
document.body.addEventListener('click', handleCloseClick, true);
console.log(`${CONFIG.LOG_PREFIX} [ScrollRestorer] Activated.`);
},
};
}
/**
* Proactively opens media links in a new tab to prevent page deadlocks
* caused by non-closable login modals.
*/
function createLinkInterceptor() {
const handleClick = event => {
if (!settings.autoOpenMediaInNewTab || event.button !== 0) return;
const linkElement = event.target.closest(CONFIG.SELECTORS.MEDIA_LINK);
if (linkElement && linkElement.closest(CONFIG.SELECTORS.POST_CONTAINER)) {
console.log(`${CONFIG.LOG_PREFIX} [LinkInterceptor] High-risk media link clicked. Opening in new tab.`);
event.preventDefault();
event.stopPropagation();
window.open(linkElement.href, '_blank');
}
};
return {
init: () => {
document.body.addEventListener('click', handleClick, true);
console.log(`${CONFIG.LOG_PREFIX} [LinkInterceptor] Activated.`);
},
};
}
/**
* Finds and removes/modifies dynamic UI elements using a MutationObserver.
* @param {object} T - The internationalized strings object.
* @returns {{init: function}} The DOM cleaner instance.
*/
function createDOMCleaner(T) {
// Multi-language keywords for identifying action buttons.
const PRIMARY_KEYWORDS = ["Like", "讚", "いいね!", "Me gusta", "J'aime", "Gefällt mir", "Curtir", "Mi piace", "Vind ik leuk", "Нравится", "Beğen", "Lubię to!", "Suka", "Thích", "ถูกใจ", "Sviđa mi se", "Páči sa mi to", "To se mi líbí", "Tetszik"];
const SECONDARY_KEYWORDS = ["Comment", "留言", "コメントする", "Comentar", "Commenter", "Kommentieren", "Comentar", "Commento", "Reageren", "Комментировать", "Yorum Yap", "Skomentuj", "Komentari", "Bình luận", "แสดงความคิดเห็น", "Komentiraj", "Komentovať", "Okmentovat", "Hozzászólás"];
const FINGERPRINTS = [
// Handles the cookie consent dialog.
{
selector: `${CONFIG.SELECTORS.DIALOG}:has([href*="/policies/cookies/"]) [role="button"][tabindex="0"]`,
action: 'click',
},
// Handles all login modals, closable or not.
{
selector: `${CONFIG.SELECTORS.DIALOG}:has(${CONFIG.SELECTORS.LOGIN_FORM})`,
action: 'handle_login_modal',
},
// Fixes the sticky header that can sometimes appear.
{
selector: `div[style*="top:"][style*="z-index"]:has(div[role="tablist"])`,
action: 'make_static',
setting: 'hideUselessElements',
},
];
/**
* Hides the main interaction toolbar ("Like", "Comment", etc.).
* It locates the toolbar by finding buttons via their innerText, finds their
* smallest common ancestor, and hides that ancestor's parent to avoid leaving
* an empty container. This method is resilient to CSS class changes.
*/
const hideInteractionToolbars = () => {
const processedToolbars = new Set();
// Scan only within post containers that haven't been fully processed for performance.
const unprocessedPosts = document.querySelectorAll(`${CONFIG.SELECTORS.POST_CONTAINER}:not([data-gm-toolbar-processed])`);
unprocessedPosts.forEach(post => {
const allSpans = post.querySelectorAll('span');
let foundToolbarForThisPost = false;
for (const span of allSpans) {
const text = span.innerText.trim();
if (PRIMARY_KEYWORDS.includes(text)) {
let currentParent = span.parentElement;
let depth = 0;
while (currentParent && depth < 10) {
// Check if the parent also contains a secondary keyword.
const hasSecondary = Array.from(currentParent.querySelectorAll('span')).some(otherSpan => SECONDARY_KEYWORDS.includes(otherSpan.innerText.trim()));
// Ensure it's a multi-button container and not the entire post.
if (hasSecondary && currentParent.querySelectorAll('div[role="button"]').length > 1 && currentParent.getAttribute('role') !== 'article') {
const toolbar = currentParent;
// The actual container to hide is the parent of the toolbar.
const targetContainer = toolbar.parentElement;
if (targetContainer && !processedToolbars.has(targetContainer)) {
processedToolbars.add(targetContainer);
targetContainer.style.display = 'none';
post.dataset.gmToolbarProcessed = 'true';
foundToolbarForThisPost = true;
break; // Exit the while loop for this span.
}
}
currentParent = currentParent.parentElement;
depth++;
}
}
if (foundToolbarForThisPost) break; // Exit the for...of loop for this post.
}
});
};
const showNotification = (title, text) => {
if (!settings.showDeadlockNotification) return;
GM_notification({ title, text, timeout: 8000, silent: true });
};
const runEngine = () => {
for (const fingerprint of FINGERPRINTS) {
// Skip rules disabled by user settings.
if (fingerprint.setting && !settings[fingerprint.setting]) {
continue;
}
document.querySelectorAll(fingerprint.selector).forEach(element => {
if (element.dataset.gmProcessed) return;
element.dataset.gmProcessed = 'true';
switch (fingerprint.action) {
case 'click':
element.click();
break;
case 'make_static':
element.style.position = 'static';
break;
case 'handle_login_modal':
{
const closeButton = element.querySelector(CONFIG.SELECTORS.CLOSE_BUTTON);
if (closeButton) {
closeButton.click();
} else {
// Handle non-closable modals.
const allContainers = document.querySelectorAll(CONFIG.SELECTORS.MODAL_CONTAINER);
const parentContainer = Array.from(allContainers).find(c => c.contains(element));
if (parentContainer) {
parentContainer.style.display = 'none';
console.warn(`${CONFIG.LOG_PREFIX} Non-closable modal hidden. Page deadlocked.`);
showNotification(T.notificationTitle, T.notificationDeadlock);
}
}
break;
}
}
});
}
if (settings.hideUselessElements) {
hideInteractionToolbars();
}
};
const throttle = (func, delay) => {
let timeoutId = null;
return (...args) => {
if (timeoutId === null) {
timeoutId = setTimeout(() => {
func.apply(this, args);
timeoutId = null;
}, delay);
}
};
};
return {
init: () => {
const throttledEngine = throttle(runEngine, CONFIG.THROTTLE_DELAY);
throttledEngine();
const observer = new MutationObserver(throttledEngine);
observer.observe(document.documentElement, { childList: true, subtree: true });
},
};
}
// --- Script Execution ---
if (isLoggedIn()) {
console.log(`${CONFIG.LOG_PREFIX} Logged-in state detected. Script terminated.`);
return;
}
console.log(`${CONFIG.LOG_PREFIX} Logged-out state detected. Script active.`);
setupInterceptors();
window.addEventListener('DOMContentLoaded', () => {
const T = getStrings();
SettingsManager.init(T);
// Inject CSS hiding rules based on initial settings.
injectHidingStyles(settings);
const scrollRestorer = createScrollRestorer();
const domCleaner = createDOMCleaner(T);
const linkInterceptor = createLinkInterceptor();
scrollRestorer.init();
domCleaner.init();
linkInterceptor.init();
});
})();