Facebook Comment Sorter

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

目前為 2025-04-23 提交的版本,檢視 最新版本

您需要先安裝使用者腳本管理器擴展,如 TampermonkeyGreasemonkeyViolentmonkey 之後才能安裝該腳本。

You will need to install an extension such as Tampermonkey to install this script.

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyUserscripts 後才能安裝該腳本。

你需要先安裝一款使用者腳本管理器擴展,比如 Tampermonkey,才能安裝此腳本

您需要先安裝使用者腳本管理器擴充功能後才能安裝該腳本。

(我已經安裝了使用者腳本管理器,讓我安裝!)

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

(我已經安裝了使用者樣式管理器,讓我安裝!)

// ==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      1.1
// @license      MIT
// @icon         https://www.google.com/s2/favicons?sz=64&domain=facebook.com
// @run-at       document-start
// @grant        none
// ==/UserScript==

(function() {
    'use strict';

    // CHANGE THIS VALUE TO "newest" IF YOU WANT NEWEST COMMENTS INSTEAD
    const sortPreference = "all"; // Options: "newest", "all"

    const processedUrls = new Set();
    const processedButtons = new WeakSet();

    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',
            'наиболее релевантные', 'популярные',
            '最相关', '最热门',
            '最も関連性の高い', '人気',
            'الأكثر صلة', 'الأكثر شعبية',
            'सबसे उपयुक्त', 'सबसे लोकप्रिय'
        ]
    };

    // Specifically block these filter buttons that cause popups
    const blockListTexts = [
        'post filters',
        'filter posts'
    ];

    function shouldSkipButton(button) {
        if (!button || !button.textContent) return true;

        const text = button.textContent.toLowerCase().trim();

        // Skip buttons that explicitly match our blocklist
        if (blockListTexts.some(blockText => text === blockText)) {
            return true;
        }

        // Skip buttons inside filter dialogs
        const parentDialog = button.closest('[role="dialog"]');
        if (parentDialog && parentDialog.textContent &&
            parentDialog.textContent.toLowerCase().includes('post filter')) {
            return true;
        }

        // Skip buttons with specific parents that indicate profile filters
        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() {
        // Look for comment sort buttons
        const potentialButtons = document.querySelectorAll('div[role="button"], span[role="button"]');

        for (const button of potentialButtons) {
            // Skip already processed buttons or null buttons
            if (!button || processedButtons.has(button)) continue;

            // Check if button should be skipped (profile filters, etc.)
            if (shouldSkipButton(button)) {
                processedButtons.add(button);
                continue;
            }

            const text = button.textContent.toLowerCase().trim();

            // Process only if it looks like a comment sort button
            if (sortButtonTexts.default.some(sortText => text.includes(sortText))) {
                try {
                    processedButtons.add(button);
                    button.click();

                    setTimeout(() => {
                        const menuItems = document.querySelectorAll('[role="menuitem"], [role="menuitemradio"], [role="radio"]');
                        const targetTexts = sortPreference === "newest" ? sortButtonTexts.newest : sortButtonTexts.all;

                        if (menuItems.length === 0) {
                            processedButtons.delete(button);
                            return;
                        }

                        let found = false;
                        let targetItem = null;

                        // First try exact matches
                        for (const item of menuItems) {
                            if (!item.textContent) continue;
                            const itemText = item.textContent.toLowerCase().trim();
                            if (targetTexts.some(target => itemText === target)) {
                                targetItem = item;
                                found = true;
                                break;
                            }
                        }

                        // Then try partial matches
                        if (!found) {
                            for (const item of menuItems) {
                                if (!item.textContent) continue;
                                const itemText = item.textContent.toLowerCase().trim();
                                if (targetTexts.some(target => itemText.includes(target))) {
                                    targetItem = item;
                                    found = true;
                                    break;
                                }
                            }
                        }

                        // Fallback to position-based selection as last resort
                        if (!found) {
                            if (sortPreference === "newest" && menuItems.length >= 2) {
                                targetItem = menuItems[1]; // Usually second option
                                found = true;
                            } else if (sortPreference === "all" && menuItems.length >= 3) {
                                targetItem = menuItems[2]; // Usually third option
                                found = true;
                            } else if (menuItems.length >= 1) {
                                targetItem = menuItems[menuItems.length - 1]; // Last option
                                found = true;
                            }
                        }

                        if (found && targetItem) {
                            targetItem.click();
                        } else {
                            processedButtons.delete(button);
                        }
                    }, 500);
                } catch (error) {
                    processedButtons.delete(button);
                }
            }
        }
    }

    function setupRequestIntercepts() {
        const paramMappings = {
            "newest": {
                'feedback_filter': 'stream',
                'order_by': 'time',
                'comment_order': 'chronological',
                'filter': 'stream',
                'comment_filter': 'stream'
            },
            "all": {
                'feedback_filter': 'all',
                'order_by': 'ranked',
                'comment_order': 'ranked_threaded',
                'filter': 'all',
                'comment_filter': 'all'
            }
        };
        const params = paramMappings[sortPreference];

        // Intercept XMLHttpRequest
        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);
                    return originalOpen.apply(this, [method, modifiedUrl]);
                }
            }
            return originalOpen.apply(this, arguments);
        };

        // Intercept fetch
        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);
                        return originalFetch.call(this, modifiedUrl, init);
                    }
                }
                return originalFetch.apply(this, arguments);
            };
        }
    }

    function initialize() {
        setupRequestIntercepts();

        // Initial run with short delay to let the page load
        setTimeout(findAndClickSortButtons, 2000);

        // Run periodically to catch new comments or UI changes
        setInterval(findAndClickSortButtons, 5000);

        // Monitor URL changes to reset and reapply on navigation
        let lastUrl = location.href;
        new MutationObserver(() => {
            if (location.href !== lastUrl) {
                lastUrl = location.href;
                setTimeout(findAndClickSortButtons, 2000);
                processedUrls.clear();
            }
        }).observe(document, {subtree: true, childList: true});
    }

    // Initialize script when DOM is ready
    if (document.readyState === 'loading') {
        document.addEventListener('DOMContentLoaded', initialize);
    } else {
        initialize();
    }
})();