Facebook Comment Sorter

Forces Facebook comments to show "All Comments" or "Newest" instead of "Most Relevant"

当前为 2025-06-01 提交的版本,查看 最新版本

// ==UserScript==
// @name         Facebook Comment Sorter
// @namespace    CustomScripts
// @description  Forces Facebook comments to show "All Comments" or "Newest" instead of "Most Relevant"
// @author       areen-c
// @homepage     https://github.com/areen-c
// @match        *://*.facebook.com/*
// @version      2.0
// @license      MIT
// @icon         https://www.google.com/s2/favicons?sz=64&domain=facebook.com
// @run-at       document-start
// @grant        none
// ==/UserScript==

(function() {
    'use strict';

    // CONFIGURATION
    // Change this to "newest" if you want newest comments first, or "all" for all comments
    const sortPreference = "all"; // Options: "newest", "all"
    
    // Enable debug logging (set to false to disable console logs)
    const DEBUG = true;
    
    const processedUrls = new Set();
    const processedButtons = new WeakSet();
    
    const log = (msg, data) => {
        if (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 = 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 (sortPreference === "newest" && menuItems.length >= 2) {
                                targetItem = menuItems[1];
                                found = true;
                                log('Using position fallback for Newest: index 1');
                            } else if (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[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 (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 (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) {
                    }
                }
                
                return originalFetch.apply(this, arguments);
            };
        }
    }

    function initialize() {
        log('Initializing Facebook Comment Sorter with preference:', sortPreference);
        
        setupRequestIntercepts();

        setTimeout(findAndClickSortButtons, 2000);

        setInterval(findAndClickSortButtons, 5000);

        let lastUrl = location.href;
        new MutationObserver(() => {
            if (location.href !== lastUrl) {
                lastUrl = location.href;
                log('URL changed, resetting and retrying');
                setTimeout(findAndClickSortButtons, 2000);
                processedUrls.clear();
            }
        }).observe(document, {subtree: true, childList: true});
    }

    if (document.readyState === 'loading') {
        document.addEventListener('DOMContentLoaded', initialize);
    } else {
        initialize();
    }
})();