您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Forces Facebook comments to show "All Comments" or "Newest" instead of "Most Relevant" + Auto-expand replies
// ==UserScript== // @name Facebook Comment Sorter // @namespace CustomScripts // @description Forces Facebook comments to show "All Comments" or "Newest" instead of "Most Relevant" + Auto-expand replies // @author areen-c // @homepage https://github.com/areen-c // @match *://*.facebook.com/* // @version 2.1 // @license MIT // @icon https://www.google.com/s2/favicons?sz=64&domain=facebook.com // @run-at document-start // @grant none // ==/UserScript== /* CHANGELOG: Version 2.1 (2025-01-06) - NEW: Added automatic comment reply expansion feature - NEW: Configuration option to enable/disable reply expansion - NEW: Smart scroll position preservation during reply expansion - NEW: Viewport-aware processing (only expands visible replies) - IMPROVED: Added debounced scroll handling for better performance - IMPROVED: Better detection of reply buttons across multiple languages Version 2.0 (2025-01-06) - FIXED: Major bug where "All Comments" would incorrectly select "Newest" due to partial text matching - ADDED: Smart text matching that prioritizes exact matches at the beginning of menu items - ADDED: Comprehensive debug logging system with DEBUG flag - ADDED: Better handling of Facebook's combined menu text (title + description) - IMPROVED: Menu item detection logic with multiple fallback strategies - IMPROVED: More robust parameter mappings for Facebook's GraphQL API - IMPROVED: Added support for more API parameters (view_option, sort_by, isInitialFetch) Version 1.2 (2024-12-XX) - Added support for multiple languages - Improved URL interception for XMLHttpRequest and fetch - Added POST body modification for GraphQL requests Version 1.1 (2024-XX-XX) - Added parameter mappings for comment sorting - Fixed menu item selection logic Version 1.0 (2024-XX-XX) - Initial release - Basic comment sorting functionality */ (function() { 'use strict'; // =============== CONFIGURATION =============== // Change these settings to customize the script behavior const CONFIG = { // Comment sort preference: "newest" or "all" sortPreference: "all", // Enable debug logging in console debug: true, // Auto-expand comment replies expandReplies: true, // Delay between processing reply buttons (ms) replyExpandDelay: 1000, // Only expand replies in viewport viewportOnly: true }; const processedUrls = new Set(); const processedButtons = new WeakSet(); const processedReplyButtons = new WeakSet(); const log = (msg, data) => { if (CONFIG.debug) { console.log(`[FB Comment Sorter] ${msg}`, data || ''); } }; const sortButtonTexts = { newest: [ 'newest', 'terbaru', 'most recent', 'recent', 'más recientes', 'reciente', 'plus récents', 'récent', 'neueste', 'aktuellste', 'mais recentes', 'recente', 'più recenti', 'recente', 'nieuwste', 'recent', 'новейшие', 'недавние', '最新', '新的', '最新', '新しい', 'الأحدث', 'حديث', 'नवीनतम', 'हाल का' ], all: [ 'all comments', 'semua komentar', 'all', 'todos los comentarios', 'todos', 'tous les commentaires', 'tous', 'alle kommentare', 'alle', 'todos os comentários', 'todos', 'tutti i commenti', 'tutti', 'alle reacties', 'alle', 'все комментарии', 'все', '所有评论', '全部', 'すべてのコメント', 'すべて', 'كل التعليقات', 'الكل', 'सभी टिप्पणियां', 'सभी' ], default: [ 'most relevant', 'paling relevan', 'relevan', 'most popular', 'komentar teratas', 'oldest', 'más relevantes', 'relevante', 'más populares', 'plus pertinents', 'pertinent', 'plus populaires', 'relevanteste', 'beliebteste', 'mais relevantes', 'relevante', 'mais populares', 'più rilevanti', 'rilevante', 'più popolari', 'meest relevant', 'relevant', 'populairste', 'наиболее релевантные', 'популярные', '最相关', '最热门', '最も関連性の高い', '人気', 'الأكثر صلة', 'الأكثر شعبية', 'सबसे उपयुक्त', ' सबसे लोकप्रिय' ] }; const blockListTexts = [ 'post filters', 'filter posts' ]; function shouldSkipButton(button) { if (!button || !button.textContent) return true; const text = button.textContent.toLowerCase().trim(); if (blockListTexts.some(blockText => text === blockText)) { return true; } const parentDialog = button.closest('[role="dialog"]'); if (parentDialog && parentDialog.textContent && parentDialog.textContent.toLowerCase().includes('post filter')) { return true; } let parent = button.parentElement; for (let i = 0; i < 3 && parent; i++) { if (parent.getAttribute && parent.getAttribute('aria-label') === 'Filters') { return true; } parent = parent.parentElement; } return false; } function findAndClickSortButtons() { const potentialButtons = document.querySelectorAll('div[role="button"], span[role="button"]'); for (const button of potentialButtons) { if (!button || processedButtons.has(button)) continue; if (shouldSkipButton(button)) { processedButtons.add(button); continue; } const text = button.textContent.toLowerCase().trim(); if (sortButtonTexts.default.some(sortText => text.includes(sortText))) { try { processedButtons.add(button); log('Found sort button with text:', text); button.click(); setTimeout(() => { const menuItems = document.querySelectorAll('[role="menuitem"], [role="menuitemradio"], [role="radio"]'); const targetTexts = CONFIG.sortPreference === "newest" ? sortButtonTexts.newest : sortButtonTexts.all; log(`Found ${menuItems.length} menu items`); if (menuItems.length === 0) { log('No menu items found, removing button from processed list'); processedButtons.delete(button); return; } menuItems.forEach((item, index) => { log(`Menu item ${index}:`, item.textContent?.trim()); }); let found = false; let targetItem = null; for (const item of menuItems) { if (!item.textContent) continue; const itemText = item.textContent.toLowerCase().trim(); for (const target of targetTexts) { if (itemText.startsWith(target)) { targetItem = item; found = true; log('Found item starting with target text:', itemText); break; } } if (found) break; } if (!found) { for (const item of menuItems) { if (!item.textContent) continue; const itemText = item.textContent.toLowerCase().trim(); const firstPart = itemText.split(/show|muestra|afficher|zeige|mostra|toon|показать|显示|表示|عرض|दिखाएं/)[0].trim(); for (const target of targetTexts) { if (firstPart === target || firstPart.endsWith(target)) { targetItem = item; found = true; log('Found item with matching title part:', firstPart); break; } } if (found) break; } } if (!found) { log('Using position-based fallback'); if (CONFIG.sortPreference === "newest" && menuItems.length >= 2) { targetItem = menuItems[1]; found = true; log('Using position fallback for Newest: index 1'); } else if (CONFIG.sortPreference === "all" && menuItems.length >= 3) { targetItem = menuItems[2]; found = true; log('Using position fallback for All Comments: index 2'); } } if (found && targetItem) { log('Clicking menu item:', targetItem.textContent?.trim()); targetItem.click(); } else { log('No suitable menu item found'); processedButtons.delete(button); } }, 500); } catch (error) { log('Error processing button:', error); processedButtons.delete(button); } } } } function setupRequestIntercepts() { const paramMappings = { "newest": { 'feedback_filter': 'stream', 'order_by': 'time', 'comment_order': 'chronological', 'filter': 'stream', 'comment_filter': 'stream', 'sort': 'time', 'ranking_setting': 'CHRONOLOGICAL', 'view_option': 'CHRONOLOGICAL', 'sort_by': 'time' }, "all": { 'feedback_filter': 'all', 'order_by': 'all', 'comment_order': 'all', 'filter': 'all', 'comment_filter': 'all', 'sort': 'all', 'ranking_setting': 'ALL', 'view_option': 'ALL', 'sort_by': 'all' } }; const params = paramMappings[CONFIG.sortPreference]; const originalOpen = XMLHttpRequest.prototype.open; XMLHttpRequest.prototype.open = function(method, url) { if (typeof url === 'string' && !processedUrls.has(url)) { if ((url.includes('/api/graphql/') || url.includes('feedback')) && (url.includes('comment') || url.includes('Comment'))) { let modifiedUrl = url; for (const [key, value] of Object.entries(params)) { if (modifiedUrl.includes(`${key}=`)) { modifiedUrl = modifiedUrl.replace(new RegExp(`${key}=([^&]*)`, 'g'), `${key}=${value}`); } else { modifiedUrl += (modifiedUrl.includes('?') ? '&' : '?') + `${key}=${value}`; } } processedUrls.add(modifiedUrl); log('Modified XMLHttpRequest URL:', modifiedUrl); return originalOpen.apply(this, [method, modifiedUrl]); } } return originalOpen.apply(this, arguments); }; if (window.fetch) { const originalFetch = window.fetch; window.fetch = function(resource, init) { if (resource && typeof resource === 'string' && !processedUrls.has(resource)) { if ((resource.includes('/api/graphql/') || resource.includes('feedback')) && (resource.includes('comment') || resource.includes('Comment'))) { let modifiedUrl = resource; for (const [key, value] of Object.entries(params)) { if (modifiedUrl.includes(`${key}=`)) { modifiedUrl = modifiedUrl.replace(new RegExp(`${key}=([^&]*)`, 'g'), `${key}=${value}`); } else { modifiedUrl += (modifiedUrl.includes('?') ? '&' : '?') + `${key}=${value}`; } } processedUrls.add(modifiedUrl); log('Modified fetch URL:', modifiedUrl); return originalFetch.call(this, modifiedUrl, init); } } if (init && init.method === 'POST' && init.body) { try { let bodyStr = init.body; if (typeof bodyStr === 'string' && bodyStr.includes('comment')) { let bodyObj = JSON.parse(bodyStr); if (bodyObj.variables) { const originalVars = JSON.stringify(bodyObj.variables); if (CONFIG.sortPreference === "all") { bodyObj.variables.orderBy = "ALL"; bodyObj.variables.topLevelViewOption = "ALL"; bodyObj.variables.rankingSetting = "ALL"; bodyObj.variables.viewOption = "ALL"; bodyObj.variables.sortBy = "ALL"; bodyObj.variables.useDefaultActor = false; bodyObj.variables.isInitialFetch = false; } else if (CONFIG.sortPreference === "newest") { bodyObj.variables.orderBy = "CHRONOLOGICAL"; bodyObj.variables.topLevelViewOption = "CHRONOLOGICAL"; bodyObj.variables.rankingSetting = "CHRONOLOGICAL"; bodyObj.variables.viewOption = "CHRONOLOGICAL"; bodyObj.variables.sortBy = "TIME"; bodyObj.variables.useDefaultActor = false; bodyObj.variables.isInitialFetch = false; } const modifiedVars = JSON.stringify(bodyObj.variables); if (originalVars !== modifiedVars) { log('Modified POST body variables:', bodyObj.variables); } init.body = JSON.stringify(bodyObj); } } } catch (e) { // If parsing fails, continue normally } } return originalFetch.apply(this, arguments); }; } } function isInViewport(element) { const rect = element.getBoundingClientRect(); return ( rect.top >= 0 && rect.left >= 0 && rect.bottom <= (window.innerHeight || document.documentElement.clientHeight) && rect.right <= (window.innerWidth || document.documentElement.clientWidth) ); } function expandCommentReplies() { if (!CONFIG.expandReplies) return; const initialScrollY = window.scrollY; const replyButtons = Array.from(document.querySelectorAll('div[role="button"], span[role="button"]')).filter(button => { if (!button.textContent || processedReplyButtons.has(button)) return false; const text = button.textContent.toLowerCase().trim(); const isReplyButton = ( (text.includes('view') && text.includes('repl')) || text.includes('replies') || text.includes('show more replies') || (text.includes('lihat') && text.includes('balasan')) || text.includes('balasan lainnya') || text.match(/^\d+\s+(replies|balasan|réponses|respuestas|antworten|risposte|antwoorden|ответов|回复|返信|ردود|उत्तर)/) || text.match(/\d+\s+more\s+(replies|comments)/) || text.match(/\d+\s+(balasan|komentar)\s+lagi/) ); const isHideButton = ( text.includes('hide') || text.includes('sembunyikan') || text.includes('tutup') || text.includes('collapse') ); const isMoreCommentsButton = ( (text.includes('more comments') && !text.includes('replies')) || (text.includes('komentar lainnya') && !text.includes('balasan')) || text.includes('load more comments') || text.includes('lihat komentar lainnya') ); return isReplyButton && !isHideButton && !isMoreCommentsButton; }); log(`Found ${replyButtons.length} reply buttons`); let processedCount = 0; for (const button of replyButtons) { if (CONFIG.viewportOnly && !isInViewport(button)) { continue; } try { processedReplyButtons.add(button); processedCount++; const beforeHeight = document.documentElement.scrollHeight; log('Expanding replies:', button.textContent?.trim()); button.click(); setTimeout(() => { const afterHeight = document.documentElement.scrollHeight; const heightDifference = afterHeight - beforeHeight; if (heightDifference > 50 && window.scrollY !== initialScrollY) { window.scrollTo({ top: initialScrollY, behavior: 'instant' }); } }, 100); if (processedCount >= 1) { break; } } catch (error) { log('Error expanding replies:', error); } } } let scrollTimeout = null; function handleScroll() { if (!CONFIG.expandReplies) return; if (scrollTimeout) clearTimeout(scrollTimeout); scrollTimeout = setTimeout(() => { expandCommentReplies(); }, 500); } function setupReplyExpansionObserver() { if (!CONFIG.expandReplies) return; if (CONFIG.viewportOnly) { window.addEventListener('scroll', handleScroll, { passive: true }); } const observer = new MutationObserver(mutations => { let hasNewComments = false; for (const mutation of mutations) { if (mutation.type === 'childList' && mutation.addedNodes.length) { for (const node of mutation.addedNodes) { if (node.nodeType === Node.ELEMENT_NODE) { const hasCommentContent = node.querySelector?.('[data-testid*="comment"]') || node.querySelector?.('[aria-label*="comment"]') || node.querySelector?.('[role="article"]') || (node.textContent?.includes('replies') || node.textContent?.includes('balasan')); if (hasCommentContent) { hasNewComments = true; break; } } } } if (hasNewComments) break; } if (hasNewComments) { setTimeout(expandCommentReplies, CONFIG.replyExpandDelay); } }); observer.observe(document.body, { childList: true, subtree: true }); } function initialize() { log('Initializing Facebook Comment Sorter v2.1'); log('Configuration:', CONFIG); setupRequestIntercepts(); setTimeout(() => { findAndClickSortButtons(); if (CONFIG.expandReplies) { expandCommentReplies(); } }, 2000); setInterval(findAndClickSortButtons, 5000); if (CONFIG.expandReplies) { setInterval(expandCommentReplies, CONFIG.replyExpandDelay * 3); setupReplyExpansionObserver(); } let lastUrl = location.href; new MutationObserver(() => { if (location.href !== lastUrl) { lastUrl = location.href; log('URL changed, resetting and retrying'); setTimeout(() => { findAndClickSortButtons(); if (CONFIG.expandReplies) { processedReplyButtons = new WeakSet(); expandCommentReplies(); } }, 2000); processedUrls.clear(); } }).observe(document, {subtree: true, childList: true}); } if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', initialize); } else { initialize(); } })();
QingJ © 2025
镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址