Threads.net Quick Block

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

Voor het installeren van scripts heb je een extensie nodig, zoals Tampermonkey, Greasemonkey of Violentmonkey.

Voor het installeren van scripts heb je een extensie nodig, zoals Tampermonkey of Violentmonkey.

Voor het installeren van scripts heb je een extensie nodig, zoals Tampermonkey of Violentmonkey.

Voor het installeren van scripts heb je een extensie nodig, zoals Tampermonkey of Userscripts.

Voor het installeren van scripts heb je een extensie nodig, zoals {tampermonkey_link:Tampermonkey}.

Voor het installeren van scripts heb je een gebruikersscriptbeheerder nodig.

(Ik heb al een user script manager, laat me het downloaden!)

Voor het installeren van gebruikersstijlen heb je een extensie nodig, zoals {stylus_link:Stylus}.

Voor het installeren van gebruikersstijlen heb je een extensie nodig, zoals {stylus_link:Stylus}.

Voor het installeren van gebruikersstijlen heb je een extensie nodig, zoals {stylus_link:Stylus}.

Voor het installeren van gebruikersstijlen heb je een gebruikersstijlbeheerder nodig.

Voor het installeren van gebruikersstijlen heb je een gebruikersstijlbeheerder nodig.

Voor het installeren van gebruikersstijlen heb je een gebruikersstijlbeheerder nodig.

(Ik heb al een beheerder - laat me doorgaan met de installatie!)

// ==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();
})();