Add "Mute User" Button to Bluesky Posts

Add a mute button to Bluesky posts, to allow you to quickly mute a user

  1. // ==UserScript==
  2. // @name Add "Mute User" Button to Bluesky Posts
  3. // @namespace plonked
  4. // @description Add a mute button to Bluesky posts, to allow you to quickly mute a user
  5. // @author @plonked.bsky.social
  6. // @match *://bsky.app/*
  7. // @grant none
  8. // @version 0.0.1.20241128204020
  9. // ==/UserScript==
  10.  
  11. (function() {
  12. 'use strict';
  13.  
  14. const BUTTON_CLASS = 'bsky-mute-btn';
  15. const PROCESSED_CLASS = 'bsky-mute-processed';
  16. const POST_SELECTORS = {
  17. feedItem: '[data-testid^="feedItem-by-"]',
  18. postPage: '[data-testid^="postThreadItem-by-"]',
  19. searchItem: 'div[role="link"][tabindex="0"]'
  20. };
  21.  
  22. let hostApi = 'https://cordyceps.us-west.host.bsky.network';
  23. let token = null;
  24.  
  25. function getTokenFromLocalStorage() {
  26. const storedData = localStorage.getItem('BSKY_STORAGE');
  27. if (storedData) {
  28. try {
  29. const localStorageData = JSON.parse(storedData);
  30. token = localStorageData.session.currentAccount.accessJwt;
  31. } catch (error) {
  32. console.error('Failed to parse session data', error);
  33. }
  34. }
  35. }
  36.  
  37. function createMuteButton() {
  38. const button = document.createElement('div');
  39. button.className = `css-175oi2r r-1loqt21 r-1otgn73 ${BUTTON_CLASS}`;
  40. button.setAttribute('role', 'button');
  41. button.setAttribute('tabindex', '0');
  42. button.style.cssText = `
  43. position: absolute;
  44. top: 8px;
  45. right: 8px;
  46. border-radius: 999px;
  47. flex-direction: row;
  48. justify-content: center;
  49. align-items: center;
  50. overflow: hidden;
  51. padding: 5px;
  52. cursor: pointer;
  53. transition: background-color 0.2s ease;
  54. opacity: 0.5;
  55. z-index: 10;
  56. `;
  57.  
  58. const icon = document.createElement('div');
  59. icon.textContent = '🔇';
  60. icon.style.cssText = `
  61. font-size: 16px;
  62. filter: grayscale(1);
  63. `;
  64.  
  65. button.appendChild(icon);
  66.  
  67. button.onmouseover = () => {
  68. button.style.backgroundColor = 'rgba(29, 161, 242, 0.1)';
  69. button.style.opacity = '1';
  70. };
  71. button.onmouseout = () => {
  72. button.style.backgroundColor = '';
  73. button.style.opacity = '0.5';
  74. };
  75.  
  76. return button;
  77. }
  78.  
  79. async function muteUser(userId) {
  80. if (!token) {
  81. console.error('Failed to get authorization token');
  82. return false;
  83. }
  84.  
  85. try {
  86. const response = await fetch(
  87. `${hostApi}/xrpc/app.bsky.graph.muteActor`,
  88. {
  89. method: 'POST',
  90. headers: {
  91. 'Content-Type': 'application/json',
  92. 'Authorization': `Bearer ${token}`
  93. },
  94. body: JSON.stringify({ actor: userId })
  95. }
  96. );
  97.  
  98. return response.ok;
  99. } catch (error) {
  100. console.error('Error muting user:', error);
  101. return false;
  102. }
  103. }
  104.  
  105. function extractDidPlc(element) {
  106. const html = element.innerHTML;
  107. const match = html.match(/did:plc:[^/"]+/);
  108. return match ? match[0] : null;
  109. }
  110.  
  111. function findNameInPost(post) {
  112. const testId = post.getAttribute('data-testid');
  113. if (testId) {
  114. const match = testId.match(/(?:feedItem-by-|postThreadItem-by-)([^.]+)/);
  115. if (match) return match[1];
  116. }
  117.  
  118. const profileLinks = post.querySelectorAll('a[href^="/profile/"]');
  119. for (const link of profileLinks) {
  120. const nameElement = link.querySelector('.css-1jxf684[style*="font-weight: 600"]');
  121. if (nameElement) {
  122. let name = nameElement.textContent.trim();
  123. if (name.startsWith('@')) name = name.slice(1);
  124. if (name.endsWith('.bsky.social')) name = name.replace('.bsky.social', '');
  125. return name;
  126. }
  127. }
  128.  
  129. return null;
  130. }
  131.  
  132. function hideAllPostsForUser(didPlc) {
  133. document.querySelectorAll(Object.values(POST_SELECTORS).join(',')).forEach(post => {
  134. if (post.innerHTML.includes(didPlc)) {
  135. post.style.display = 'none';
  136. }
  137. });
  138. }
  139.  
  140. async function addMuteButton(post) {
  141. if (post.classList.contains(PROCESSED_CLASS)) return;
  142.  
  143. if (window.getComputedStyle(post).position === 'static') {
  144. post.style.position = 'relative';
  145. }
  146.  
  147. const didPlc = extractDidPlc(post);
  148. if (!didPlc) return;
  149.  
  150. const username = findNameInPost(post);
  151. if (!username) return;
  152.  
  153. const button = createMuteButton();
  154. button.setAttribute('data-did-plc', didPlc);
  155.  
  156. button.onclick = async (e) => {
  157. e.preventDefault();
  158. e.stopPropagation();
  159.  
  160. const success = await muteUser(didPlc);
  161. if (success) {
  162. hideAllPostsForUser(didPlc);
  163. }
  164. };
  165.  
  166. post.appendChild(button);
  167. post.classList.add(PROCESSED_CLASS);
  168. }
  169.  
  170. function initialize() {
  171. console.log('Initializing Bluesky Direct Mute Button');
  172. getTokenFromLocalStorage();
  173.  
  174. const observer = new MutationObserver((mutations) => {
  175. if (mutations.some(mutation => mutation.addedNodes.length)) {
  176. const unprocessedPosts = document.querySelectorAll(
  177. Object.values(POST_SELECTORS)
  178. .map(selector => `${selector}:not(.${PROCESSED_CLASS})`)
  179. .join(',')
  180. );
  181. unprocessedPosts.forEach(addMuteButton);
  182. }
  183. });
  184.  
  185. observer.observe(document.body, { childList: true, subtree: true });
  186.  
  187. document.querySelectorAll(
  188. Object.values(POST_SELECTORS)
  189. .map(selector => `${selector}:not(.${PROCESSED_CLASS})`)
  190. .join(',')
  191. ).forEach(addMuteButton);
  192. }
  193.  
  194. if (document.readyState === 'loading') {
  195. document.addEventListener('DOMContentLoaded', initialize);
  196. } else {
  197. initialize();
  198. }
  199. })();

QingJ © 2025

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