Keyword-based Tweet Filtering for Threads

As long as any section of a tweet—such as the main content, hashtags, or username—matches a keyword, the entire tweet will be hidden. Supports adding, listing, and individually deleting keywords. Supports quick blocking, listing, and individually deleting blocked users. Menu can be switched between Chinese and English.

このスクリプトの質問や評価の投稿はこちら通報はこちらへお寄せください。
// ==UserScript==
// @name         Keyword-based Tweet Filtering for Threads
// @name:zh-TW   Threads 關鍵字過濾推文
// @name:zh-CN   Threads 关键字过滤推文
// @namespace    http://tampermonkey.net/
// @version      4.0
// @description  As long as any section of a tweet—such as the main content, hashtags, or username—matches a keyword, the entire tweet will be hidden. Supports adding, listing, and individually deleting keywords. Supports quick blocking, listing, and individually deleting blocked users. Menu can be switched between Chinese and English.
// @description:zh-TW 只要推文主體、標籤、用戶名等任一區塊命中關鍵字,整則推文一起隱藏。支援關鍵字新增、清單、單獨刪除。支援快速封鎖、清單、單獨刪除。中英菜單切換。
// @description:zh-CN 只要推文主体、标签、用户名等任一区块命中关键字,整则推文一起隐藏。支援关键字新增、清单、单独删除。支援快速封锁、清单、单独删除。中英菜单切换。
// @author       chatgpt
// @match        https://www.threads.net/*
// @match        https://www.threads.com/*
// @grant        GM_registerMenuCommand
// @grant        GM_setValue
// @grant        GM_getValue
// @license MIT
// ==/UserScript==

(function() {
    'use strict';

    // ===== 多語言支援 =====
    const LANGS = {
        zh: {
            addKeyword: '新增關鍵字',
            keywordList: '關鍵字清單/刪除',
            clearKeywords: '清除所有關鍵字',
            blockList: '封鎖名單管理',
            clearBlocks: '清除所有封鎖用戶',
            langSwitch: '語言 中文',
            blockUser: '封鎖用戶',
            confirmBlock: username => `確定要封鎖 @${username} 嗎?\n(此用戶所有推文將被隱藏)`,
            blocked: username => `已封鎖 @${username}!`,
            addKeywordPrompt: '請輸入要新增的關鍵字(可用半形或全形逗號分隔,一次可多個):',
            addedKeyword: '已新增關鍵字!',
            noKeyword: '目前沒有設定任何關鍵字。',
            keywordListMsg: (list) => `目前關鍵字如下:\n${list}\n請輸入要刪除的關鍵字編號(可多個,用逗號分隔),或留空取消:`,
            deletedKeyword: '已刪除指定關鍵字!',
            clearedKeyword: '已清除所有關鍵字!',
            noBlockUser: '目前沒有封鎖任何用戶。',
            blockListMsg: (list) => `目前封鎖用戶如下:\n${list}\n請輸入要解除封鎖的用戶編號(可多個,用逗號分隔),或留空取消:`,
            unblocked: '已解除指定用戶封鎖!',
            clearedBlock: '已清除所有封鎖用戶!',
        },
        en: {
            addKeyword: 'Add Keyword',
            keywordList: 'Keyword List/Delete',
            clearKeywords: 'Clear All Keywords',
            blockList: 'Blocked Users',
            clearBlocks: 'Clear All Blocked Users',
            langSwitch: 'language EN',
            blockUser: 'Block User',
            confirmBlock: username => `Are you sure to block @${username}?\n(All posts from this user will be hidden)`,
            blocked: username => `@${username} has been blocked!`,
            addKeywordPrompt: 'Enter keywords to add (comma or Chinese comma separated, multiple allowed):',
            addedKeyword: 'Keyword(s) added!',
            noKeyword: 'No keywords set.',
            keywordListMsg: (list) => `Current keywords:\n${list}\nEnter the number(s) to delete (comma separated), or leave blank to cancel:`,
            deletedKeyword: 'Selected keyword(s) deleted!',
            clearedKeyword: 'All keywords cleared!',
            noBlockUser: 'No users blocked.',
            blockListMsg: (list) => `Blocked users:\n${list}\nEnter the number(s) to unblock (comma separated), or leave blank to cancel:`,
            unblocked: 'Selected user(s) unblocked!',
            clearedBlock: 'All blocked users cleared!',
        }
    };

    function getLang() {
        return GM_getValue('lang', (navigator.language || '').toLowerCase().startsWith('zh') ? 'zh' : 'en');
    }
    function setLang(lang) {
        GM_setValue('lang', lang);
    }
    function t(key, ...args) {
        const lang = getLang();
        const str = LANGS[lang][key];
        return typeof str === 'function' ? str(...args) : str;
    }

    // 關鍵字相關
    function getKeywords() {
        return GM_getValue('keywords', []);
    }
    function setKeywords(keywords) {
        GM_setValue('keywords', keywords);
    }

    // 封鎖用戶相關
    function getBlockedUsers() {
        return GM_getValue('blockedUsers', []);
    }
    function setBlockedUsers(users) {
        GM_setValue('blockedUsers', users);
    }

    // 取得所有推文主容器
    function getAllPostContainers() {
        return document.querySelectorAll('div[data-pressable-container][class*=" "]');
    }

    // 在推文主容器下,找所有可能含有文字的區塊
    function getAllTextBlocks(container) {
        return container.querySelectorAll('span[dir="auto"]:not([translate="no"]), a[role="link"], span, div');
    }

    // 取得用戶名稱(Threads 通常在 a[href^="/@"] 內)
    function getUsername(container) {
        let a = container.querySelector('a[href^="/@"]');
        if (a) {
            let username = a.getAttribute('href').replace('/', '').replace('@', '');
            return username;
        }
        return null;
    }

    // ====== 這裡是唯一修改的地方 ======
    // 過濾推文(不區分大小寫)
    function filterPosts() {
        let keywords = getKeywords().map(k => k.toLowerCase());
        let blockedUsers = getBlockedUsers();
        let containers = getAllPostContainers();
        containers.forEach(container => {
            let blocks = getAllTextBlocks(container);
            let matched = false;
            // 關鍵字過濾(不區分大小寫)
            blocks.forEach(block => {
                let text = (block.innerText || block.textContent || "").trim().toLowerCase();
                if (text && keywords.some(keyword => keyword && text.includes(keyword))) {
                    matched = true;
                }
            });
            // 封鎖用戶過濾
            let username = getUsername(container);
            if (username && blockedUsers.includes(username)) {
                matched = true;
            }
            if (matched) {
                container.style.display = 'none';
            } else {
                container.style.display = '';
            }
        });
    }
    // ====== 修改結束 ======

    // 插入封鎖用戶按鈕(插在「分享」按鈕右邊)
    function insertBlockButtons() {
        let shareSvgs = document.querySelectorAll('svg[aria-label="分享"], svg[aria-label="Share"]');
        let blockedUsers = getBlockedUsers();

        shareSvgs.forEach(svg => {
            let shareBtnDiv = svg.closest('div[role="button"]');
            if (!shareBtnDiv) return;

            // 找到推文主容器
            let container = shareBtnDiv;
            for (let i = 0; i < 10; i++) {
                if (!container) break;
                if (container.hasAttribute('data-pressable-container')) break;
                container = container.parentElement;
            }
            if (!container || !container.hasAttribute('data-pressable-container')) return;

            // 避免重複插入
            if (container.querySelector('.tm-block-user-btn')) return;

            let username = getUsername(container);
            if (!username) return;

            // 建立封鎖按鈕
            let blockBtn = document.createElement('button');
            blockBtn.className = 'tm-block-user-btn';
            blockBtn.title = t('blockUser');
            blockBtn.style.marginLeft = '8px';
            blockBtn.style.background = 'none';
            blockBtn.style.border = 'none';
            blockBtn.style.cursor = 'pointer';
            blockBtn.style.fontSize = '18px';
            blockBtn.style.color = '#d00';
            blockBtn.textContent = '🚫'; // 這裡改成 textContent

            blockBtn.onclick = function(e) {
                e.stopPropagation();
                if (confirm(t('confirmBlock', username))) {
                    let users = getBlockedUsers();
                    if (!users.includes(username)) {
                        users.push(username);
                        setBlockedUsers(users);
                        alert(t('blocked', username));
                        filterPosts();
                    }
                }
            };

            shareBtnDiv.parentNode.insertBefore(blockBtn, shareBtnDiv.nextSibling);
        });
    }

    // observer 只監控新節點
    const observer = new MutationObserver(mutations => {
        let needFilter = false;
        for (const m of mutations) {
            if (m.addedNodes && m.addedNodes.length > 0) {
                needFilter = true;
                break;
            }
        }
        if (needFilter) {
            filterPosts();
            insertBlockButtons();
        }
    });
    observer.observe(document.body, { childList: true, subtree: true });

    // 初始執行一次
    filterPosts();
    insertBlockButtons();

    // 新增關鍵字
    GM_registerMenuCommand(t('addKeyword'), () => {
        let input = prompt(t('addKeywordPrompt'));
        if (input !== null) {
            let arr = input.split(/,|,/).map(s => s.trim()).filter(Boolean);
            let keywords = getKeywords();
            let newKeywords = [...keywords];
            arr.forEach(k => {
                if (!newKeywords.includes(k)) newKeywords.push(k);
            });
            setKeywords(newKeywords);
            alert(t('addedKeyword'));
            location.reload();
        }
    });

    // 關鍵字清單與單獨刪除
    GM_registerMenuCommand(t('keywordList'), () => {
        let keywords = getKeywords();
        if (keywords.length === 0) {
            alert(t('noKeyword'));
            return;
        }
        let msg = t('keywordListMsg', keywords.map((k, i) => `${i+1}. ${k}`).join('\n'));
        let input = prompt(msg, '');
        if (input !== null && input.trim() !== '') {
            let idxArr = input.split(/,|,/).map(s => parseInt(s.trim(), 10) - 1).filter(i => !isNaN(i) && i >= 0 && i < keywords.length);
            if (idxArr.length > 0) {
                let newKeywords = keywords.filter((k, i) => !idxArr.includes(i));
                setKeywords(newKeywords);
                alert(t('deletedKeyword'));
                location.reload();
            }
        }
    });

    // 清除所有關鍵字
    GM_registerMenuCommand(t('clearKeywords'), () => {
        setKeywords([]);
        alert(t('clearedKeyword'));
        location.reload();
    });

    // 封鎖名單管理
    GM_registerMenuCommand(t('blockList'), () => {
        let users = getBlockedUsers();
        if (users.length === 0) {
            alert(t('noBlockUser'));
            return;
        }
        let msg = t('blockListMsg', users.map((u, i) => `${i+1}. @${u}`).join('\n'));
        let input = prompt(msg, '');
        if (input !== null && input.trim() !== '') {
            let idxArr = input.split(/,|,/).map(s => parseInt(s.trim(), 10) - 1).filter(i => !isNaN(i) && i >= 0 && i < users.length);
            if (idxArr.length > 0) {
                let newUsers = users.filter((u, i) => !idxArr.includes(i));
                setBlockedUsers(newUsers);
                alert(t('unblocked'));
                location.reload();
            }
        }
    });

    // 清除所有封鎖用戶
    GM_registerMenuCommand(t('clearBlocks'), () => {
        setBlockedUsers([]);
        alert(t('clearedBlock'));
        location.reload();
    });

    // ===== 語言切換按鈕(放在最後) =====
    GM_registerMenuCommand(t('langSwitch'), () => {
        let current = getLang();
        let next = current === 'zh' ? 'en' : 'zh';
        setLang(next);
        location.reload();
    });

})();