Threads.net Quick Block

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

  1. // ==UserScript==
  2. // @name Threads.net Quick Block
  3. // @namespace http://timberjustinlake.example.com/
  4. // @version 2024-02-24
  5. // @description Adds a button to a threads profile page that automates the multi click process of blocking someone.
  6. // @author Timber Justinlake
  7. // @match https://www.threads.net/*
  8. // @icon https://www.google.com/s2/favicons?sz=64&domain=threads.net
  9. // @grant none
  10. // @license MIT
  11. // ==/UserScript==
  12. 'use strict';
  13. /**
  14. * Main body, adds quick block button to profile pages if user not already blocked
  15. */
  16. async function initQuickBlock() {
  17. if (!/\/@[^\/]*$/.test(window.location.pathname)) {
  18. // console.debug(`[tbq] not on profile page`);
  19. return ;
  20. }
  21.  
  22. // console.debug('[tbq] Initializing...');
  23. if (document.getElementById('tj-quick-block')) {
  24. // console.debug('[tbq] Already Initialized');
  25. return;
  26. }
  27.  
  28. let controlButton;
  29. try {
  30. controlButton = await findProfileControls();
  31. } catch(err) {
  32. return // console.debug(`[tbq] Aborting. (${err})`);
  33. }
  34.  
  35. // console.debug('[tbq] found for mention/follow button, adding quick block button');
  36. const quickBlockButton = controlButton.cloneNode(true);
  37. quickBlockButton.querySelector('div').innerText = 'QB';
  38. quickBlockButton.id = 'tj-quick-block';
  39. quickBlockButton.style.backgroundColor = 'red';
  40. quickBlockButton.addEventListener('click', blockUser);
  41. // console.debug('[tbq] appending quickblock');
  42. controlButton.parentNode.appendChild(quickBlockButton);
  43. }
  44.  
  45. /**
  46. * Watches for React initiated page changes and if it detects you're on a user profile
  47. */
  48. function watchForPageChange() {
  49. function debounce(func) {
  50. let timer;
  51.  
  52. return (...args) => {
  53. clearTimeout(timer);
  54. timer = setTimeout(() => func.apply(this, args), 300);
  55. };
  56. }
  57.  
  58. // debounce to wait for page to settle before checking
  59. const addQuickBlock = debounce((mutations, me) => { initQuickBlock(); });
  60.  
  61. // set up the mutation observer
  62. const observer = new MutationObserver(addQuickBlock);
  63.  
  64. // to keep things performant we're selecting a single element to watch mutations for that is known to mutate on page change
  65. const elToWatch = document.querySelector('header').previousElementSibling;
  66.  
  67. // start observing
  68. observer.observe(elToWatch, {
  69. childList: true,
  70. attributes: true,
  71. subtree: false
  72. });
  73. }
  74.  
  75. /**
  76. * Waits for element on selector to exist, checks if element contains text if provided
  77. * @returns {Promise<HTMLElement>} element matching selector
  78. */
  79. function waitForElm(selector, text) {
  80. // console.debug(`[tbq] setting up mutationobserver for element: ${selector}, ${text}`);
  81. return new Promise((resolve, reject) => {
  82. let tid;
  83. function getEl() {
  84. // console.debug(`[tbq] checking for element: ${selector}, ${text}`);
  85. const els = [...document.querySelectorAll(selector)];
  86.  
  87. if (!text) return el[0];
  88.  
  89. return els.filter(el => el.innerText.toLowerCase() === text.toLowerCase())[0];
  90. }
  91.  
  92. const el = getEl();
  93. if (el) {
  94. // console.debug(`[tbq] already found element; ${selector}, ${text}`);
  95. return resolve([null, el]);
  96. }
  97.  
  98. const observer = new MutationObserver(mutations => {
  99. const el = getEl();
  100.  
  101. if (el) {
  102. // console.debug(`[tbq] Found element, disconnecting mutationobserver for element: ${selector}, ${text}`);
  103. clearTimeout(tid);
  104. observer.disconnect();
  105. resolve([null, el]);
  106. }
  107. });
  108.  
  109. // console.debug(`[tbq] setting up mutation observer for ${selector}, ${text}`);
  110. // If you get "parameter 1 is not of type 'Node'" error, see https://stackoverflow.com/a/77855838/492336
  111. tid = setTimeout(() => {
  112. observer.disconnect();
  113. resolve([new Error('[tbq] Timed out looking for element')]);
  114. }, 500);
  115. observer.observe(document.body, {
  116. childList: true,
  117. subtree: true
  118. });
  119. });
  120. }
  121.  
  122. /**
  123. * React requires you to focus -> click -> blur elements to emulate a click event
  124. */
  125. function clickButton(el) {
  126. el.focus();
  127. el.click();
  128. el.blur();
  129. }
  130.  
  131. /**
  132. * Relevant buttons for blocking are behind "dialogs" that popup so this will wait for the element to be present.
  133. * @param {string} the button label to find.
  134. */
  135. async function clickDialogItem(waitForLabel, clickLabel) {
  136. const selector = '[role="dialog"] [role="button"]'
  137.  
  138. const [elErr, el] = await waitForElm(selector, waitForLabel);
  139. if (elErr) throw elErr;
  140.  
  141. const button = [...document.querySelectorAll(selector)].filter(el => el.innerText === (clickLabel || waitForLabel))[0]
  142. clickButton(button);
  143. }
  144.  
  145. /**
  146. * Sequence to automate blocking user when QB button is clicked.
  147. */
  148. async function blockUser() {
  149. try {
  150. // console.debug(`[tbq] blocking user...`);
  151. const svg = [...document.querySelectorAll('[aria-label="More"]')].filter(el => !el.closest('header'))[0];
  152. const moreButton = svg.closest('[role="button"]');
  153. clickButton(moreButton);
  154.  
  155. await clickDialogItem('Block');
  156. // Cancel button is best way to distinguish between More menu and Block dialog
  157. await clickDialogItem('Cancel', 'Block');
  158.  
  159. const quickBlockButton = document.getElementById('tj-quick-block');
  160. quickBlockButton.removeEventListener('click', blockUser);
  161. quickBlockButton.style.backgroundColor = '#3d0000';
  162. quickBlockButton.style.cursor = 'not-allowed';
  163. quickBlockButton.title = 'Already blocked';
  164. } catch(err) {
  165. // console.debug(`[tbq] was unable to block user: ${err}`);
  166. }
  167. }
  168.  
  169. /**
  170. * Finds the appropriate location to place the quick block button
  171. */
  172. async function findProfileControls() {
  173. // console.debug(`[tbq] checking if already blocked...`);
  174. const [_ub, unblockButton] = await waitForElm('[role="button"]', 'Unblock');
  175. if (unblockButton) throw new Error('Already blocked');
  176.  
  177. // console.debug('[tbq] waiting for mention button...');
  178. const [_mb, mentionButton] = await waitForElm('[role="button"]', 'Mention');
  179. if (mentionButton) return mentionButton;
  180.  
  181. // console.debug(`[tbq] User doesn't allow non-follers to metion so find the follow button`);
  182.  
  183. const [followErr, followButton] = await waitForElm('[role="button"]', 'Follow');
  184. if (followErr) throw followErr;
  185.  
  186. return followButton;
  187. }
  188.  
  189. (async function() {
  190. 'use strict';
  191. initQuickBlock();
  192. watchForPageChange();
  193. })();

QingJ © 2025

镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址