Greasy Fork is available in English.

Threads.net Quick Block

Adds a button to a threads profile page that automates the multi click process of blocking someone.

// ==UserScript==
// @name         Threads.net Quick Block
// @namespace    http://timberjustinlake.example.com/
// @version      2024-02-24
// @description  Adds a button to a threads profile page that automates the multi click process of blocking someone.
// @author       Timber Justinlake
// @match        https://www.threads.net/*
// @icon         https://www.google.com/s2/favicons?sz=64&domain=threads.net
// @grant        none
// @license      MIT
// ==/UserScript==
'use strict';
/**
 * Main body, adds quick block button to profile pages if user not already blocked
 */
async function initQuickBlock() {
    if (!/\/@[^\/]*$/.test(window.location.pathname)) {
        // console.debug(`[tbq] not on profile page`);
        return ;
    }

    // console.debug('[tbq] Initializing...');
    if (document.getElementById('tj-quick-block')) {
        // console.debug('[tbq] Already Initialized');
        return;
    }

    let controlButton;
    try {
        controlButton = await findProfileControls();
    } catch(err) {
        return // console.debug(`[tbq] Aborting. (${err})`);
    }

    // console.debug('[tbq] found for mention/follow button, adding quick block button');
    const quickBlockButton = controlButton.cloneNode(true);
    quickBlockButton.querySelector('div').innerText = 'QB';
    quickBlockButton.id = 'tj-quick-block';
    quickBlockButton.style.backgroundColor = 'red';
    quickBlockButton.addEventListener('click', blockUser);
    // console.debug('[tbq] appending quickblock');
    controlButton.parentNode.appendChild(quickBlockButton);
}

/**
 * Watches for React initiated page changes and if it detects you're on a user profile
 */
function watchForPageChange() {
    function debounce(func) {
        let timer;

        return (...args) => {
            clearTimeout(timer);
            timer = setTimeout(() => func.apply(this, args), 300);
        };
    }

    // debounce to wait for page to settle before checking
    const addQuickBlock = debounce((mutations, me) => { initQuickBlock(); });

    // set up the mutation observer
    const observer = new MutationObserver(addQuickBlock);

    // to keep things performant we're selecting a single element to watch mutations for that is known to mutate on page change
    const elToWatch = document.querySelector('header').previousElementSibling;

    // start observing
    observer.observe(elToWatch, {
        childList: true,
        attributes: true,
        subtree: false
    });
}

/**
 * Waits for element on selector to exist, checks if element contains text if provided
 * @returns {Promise<HTMLElement>} element matching selector
 */
function waitForElm(selector, text) {
    // console.debug(`[tbq] setting up mutationobserver for element: ${selector}, ${text}`);
    return new Promise((resolve, reject) => {
        let tid;
        function getEl() {
            // console.debug(`[tbq] checking for element: ${selector}, ${text}`);
            const els = [...document.querySelectorAll(selector)];

            if (!text) return el[0];

            return els.filter(el => el.innerText.toLowerCase() === text.toLowerCase())[0];
        }

        const el = getEl();
        if (el) {
            // console.debug(`[tbq] already found element; ${selector}, ${text}`);
            return resolve([null, el]);
        }

        const observer = new MutationObserver(mutations => {
            const el = getEl();

            if (el) {
                // console.debug(`[tbq] Found element, disconnecting mutationobserver for element: ${selector}, ${text}`);
                clearTimeout(tid);
                observer.disconnect();
                resolve([null, el]);
            }
        });

        // console.debug(`[tbq] setting up mutation observer for ${selector}, ${text}`);
        // If you get "parameter 1 is not of type 'Node'" error, see https://stackoverflow.com/a/77855838/492336
        tid = setTimeout(() => {
            observer.disconnect();
            resolve([new Error('[tbq] Timed out looking for element')]);
        }, 500);
        observer.observe(document.body, {
            childList: true,
            subtree: true
        });
    });
}

/**
 * React requires you to focus -> click -> blur elements to emulate a click event
 */
function clickButton(el) {
    el.focus();
    el.click();
    el.blur();
}

/**
 * Relevant buttons for blocking are behind "dialogs" that popup so this will wait for the element to be present.
 * @param {string} the button label to find.
 */
async function clickDialogItem(waitForLabel, clickLabel) {
    const selector = '[role="dialog"] [role="button"]'

    const [elErr, el] = await waitForElm(selector, waitForLabel);
    if (elErr) throw elErr;

    const button = [...document.querySelectorAll(selector)].filter(el => el.innerText === (clickLabel || waitForLabel))[0]
    clickButton(button);
}

/**
 * Sequence to automate blocking user when QB button is clicked.
 */
async function blockUser() {
    try {
        // console.debug(`[tbq] blocking user...`);
        const svg = [...document.querySelectorAll('[aria-label="More"]')].filter(el => !el.closest('header'))[0];
        const moreButton = svg.closest('[role="button"]');
        clickButton(moreButton);

        await clickDialogItem('Block');
        // Cancel button is best way to distinguish between More menu and Block dialog
        await clickDialogItem('Cancel', 'Block');

        const quickBlockButton = document.getElementById('tj-quick-block');
        quickBlockButton.removeEventListener('click', blockUser);
        quickBlockButton.style.backgroundColor = '#3d0000';
        quickBlockButton.style.cursor = 'not-allowed';
        quickBlockButton.title = 'Already blocked';
    } catch(err) {
        // console.debug(`[tbq] was unable to block user: ${err}`);
    }
}

/**
 * Finds the appropriate location to place the quick block button
 */
async function findProfileControls() {
    // console.debug(`[tbq] checking if already blocked...`);
    const [_ub, unblockButton] = await waitForElm('[role="button"]', 'Unblock');
    if (unblockButton) throw new Error('Already blocked');

    // console.debug('[tbq] waiting for mention button...');
    const [_mb, mentionButton] = await waitForElm('[role="button"]', 'Mention');
    if (mentionButton) return mentionButton;

    // console.debug(`[tbq] User doesn't allow non-follers to metion so find the follow button`);

    const [followErr, followButton] = await waitForElm('[role="button"]', 'Follow');
    if (followErr) throw followErr;

    return followButton;
}

(async function() {
    'use strict';
    initQuickBlock();
    watchForPageChange();
})();