Add Mute User Button to Bluesky Posts Menu

Add a "MUTE USER" button to each post's options menu on Bluesky and capture parent divs on button click

  1. // ==UserScript==
  2. // @name Add Mute User Button to Bluesky Posts Menu
  3. // @namespace http://tampermonkey.net/
  4. // @version 0.9
  5. // @description Add a "MUTE USER" button to each post's options menu on Bluesky and capture parent divs on button click
  6. // @author JouySandbox, Trevusimon
  7. // @match https://bsky.app/*
  8. // @grant none
  9. // @license GNU GPLv3
  10. // ==/UserScript==
  11.  
  12. (function () {
  13. 'use strict';
  14.  
  15. let hostApi = 'https://russula.us-west.host.bsky.network'
  16. let token = null;
  17. let profileFetched = new Map(); // Set to track fetched profiles
  18. let DidPlcTarget = null;
  19.  
  20. // Function to get the token directly from localStorage
  21. function getTokenFromLocalStorage() {
  22. const storedData = localStorage.getItem('BSKY_STORAGE');
  23. if (storedData) {
  24. try {
  25. const localStorageData = JSON.parse(storedData);
  26. token = localStorageData.session.currentAccount.accessJwt;
  27. }
  28. catch (error) {
  29. console.error('Failed to parse session data', error);
  30. }
  31. }
  32. }
  33.  
  34. // Function to extract did:plc from URL
  35. function extractDidPlc(url) {
  36. const match = url.match(/did:plc:[^/]+/);
  37. return match ? match[0] : null;
  38. }
  39.  
  40. function removeButton(buttonLabel) {
  41. // Find the button by aria-label
  42. document.querySelectorAll(`[aria-label="${buttonLabel}"]`).forEach(button => {
  43. // Remove the found button
  44. button.remove();
  45. });
  46. }
  47.  
  48. // Function to add the "MUTE USER" button in the menu before "Mute thread"
  49. function addMuteButton() {
  50. document.querySelectorAll('[data-testid="postDropdownMuteThreadBtn"]')
  51. .forEach(muteThreadBtn => {
  52. if (!muteThreadBtn.parentNode.querySelector('.mute-user-button') && !profileFetched.get(
  53. DidPlcTarget)) {
  54. // Create the MUTE USER button div with the same style as the menu
  55. let muteButton = document.createElement('div');
  56. muteButton.setAttribute('aria-label', 'Mute user');
  57. muteButton.setAttribute('role', 'menuitem');
  58. muteButton.setAttribute('tabindex', '-1');
  59. muteButton.className = 'css-175oi2r r-1loqt21 r-1otgn73 mute-user-button';
  60. muteButton.style.flexDirection = 'row';
  61. muteButton.style.alignItems = 'center';
  62. muteButton.style.gap = '16px';
  63. muteButton.style.padding = '8px 10px';
  64. muteButton.style.borderRadius = '4px';
  65. muteButton.style.minHeight = '32px';
  66. muteButton.style.outline = '0px';
  67. muteButton.style.cursor = 'pointer';
  68. muteButton.style.transition = 'background-color 0.2s ease';
  69.  
  70. // Add hover (highlight) on mouse over
  71. muteButton.onmouseover = function () {
  72. muteButton.style.backgroundColor = 'rgba(29, 161, 242, 0.1)';
  73. };
  74. muteButton.onmouseout = function () {
  75. muteButton.style.backgroundColor = '';
  76. };
  77.  
  78. // Add the text "Mute User"
  79. let buttonText = document.createElement('div');
  80. buttonText.className = 'css-146c3p1';
  81. buttonText.style.fontSize = '14px';
  82. buttonText.style.letterSpacing = '0.25px';
  83. buttonText.style.color = 'rgb(215, 221, 228)';
  84. buttonText.style.flex = '1 1 0%';
  85. buttonText.style.fontWeight = '600';
  86. buttonText.style.lineHeight = '14px';
  87. buttonText.textContent = 'Mute User';
  88.  
  89. // Add the emoji
  90. let buttonIcon = document.createElement('div');
  91. buttonIcon.className = 'css-175oi2r';
  92. buttonIcon.style.marginRight = '-2px';
  93. buttonIcon.style.marginLeft = '12px';
  94. buttonIcon.textContent = '☠️'; // Adding the emoji here
  95.  
  96. // Add the button to the DOM before "Mute thread"
  97. muteButton.appendChild(buttonText);
  98. muteButton.appendChild(buttonIcon);
  99. muteThreadBtn.parentNode.insertBefore(muteButton, muteThreadBtn);
  100.  
  101. // Function when clicking the MUTE USER button
  102. muteButton.onclick = () => {
  103. let post = muteThreadBtn.closest('.post-class'); // Adjust selector to find the post
  104. // let userId = post.getAttribute('data-user-id'); // Adjust to get the user ID correctly
  105. muteUser(DidPlcTarget);
  106. };
  107. }
  108. });
  109. }
  110.  
  111. function addUnmuteButton() {
  112. document.querySelectorAll('[data-testid="postDropdownMuteThreadBtn"]')
  113. .forEach(muteThreadBtn => {
  114. if (!muteThreadBtn.parentNode.querySelector('.unmute-user-button') && profileFetched.get(DidPlcTarget)) {
  115. // Create the UNMUTE USER button div with the same style as the menu
  116. let unmuteButton = document.createElement('div');
  117. unmuteButton.setAttribute('aria-label', 'Unmute User');
  118. unmuteButton.setAttribute('role', 'menuitem');
  119. unmuteButton.setAttribute('tabindex', '-1');
  120. unmuteButton.className = 'css-175oi2r r-1loqt21 r-1otgn73 unmute-user-button';
  121. unmuteButton.style.flexDirection = 'row';
  122. unmuteButton.style.alignItems = 'center';
  123. unmuteButton.style.gap = '16px';
  124. unmuteButton.style.padding = '8px 10px';
  125. unmuteButton.style.borderRadius = '4px';
  126. unmuteButton.style.minHeight = '32px';
  127. unmuteButton.style.outline = '0px';
  128. unmuteButton.style.cursor = 'pointer';
  129. unmuteButton.style.transition = 'background-color 0.2s ease';
  130.  
  131. // Add hover (highlight) on mouse over
  132. unmuteButton.onmouseover = function () {
  133. unmuteButton.style.backgroundColor = 'rgba(29, 161, 242, 0.1)';
  134. };
  135. unmuteButton.onmouseout = function () {
  136. unmuteButton.style.backgroundColor = '';
  137. };
  138.  
  139. // Add the text "Unmute User"
  140. let buttonText = document.createElement('div');
  141. buttonText.className = 'css-146c3p1';
  142. buttonText.style.fontSize = '14px';
  143. buttonText.style.letterSpacing = '0.25px';
  144. buttonText.style.color = 'rgb(215, 221, 228)';
  145. buttonText.style.flex = '1 1 0%';
  146. buttonText.style.fontWeight = '600';
  147. buttonText.style.lineHeight = '14px';
  148. buttonText.textContent = 'Unmute User';
  149.  
  150. // Add the emoji
  151. let buttonIcon = document.createElement('div');
  152. buttonIcon.className = 'css-175oi2r';
  153. buttonIcon.style.marginRight = '-2px';
  154. buttonIcon.style.marginLeft = '12px';
  155. buttonIcon.textContent = '😃'; // Adding the emoji here
  156.  
  157. // Add the button to the DOM before "Mute thread"
  158. unmuteButton.appendChild(buttonText);
  159. unmuteButton.appendChild(buttonIcon);
  160. muteThreadBtn.parentNode.insertBefore(unmuteButton, muteThreadBtn);
  161.  
  162. // Function when clicking the UNMUTE USER button
  163. unmuteButton.onclick = () => {
  164. let post = muteThreadBtn.closest('.post-class'); // Adjust selector to find the post
  165. unmuteUser(DidPlcTarget);
  166. };
  167. }
  168. });
  169. }
  170.  
  171. // Function to mute the user
  172. async function muteUser(userId) {
  173. if (!token) {
  174. alert('Failed to get authorization token');
  175. return;
  176. }
  177.  
  178. try {
  179. let response = await fetch(
  180. `${hostApi}/xrpc/app.bsky.graph.muteActor`,
  181. {
  182. method: 'POST',
  183. headers: {
  184. 'Content-Type': 'application/json',
  185. 'Authorization': `Bearer ${token}`
  186. },
  187. body: JSON.stringify({
  188. actor: userId // Send the userId as "actor"
  189. })
  190. });
  191.  
  192. if (response.ok) {
  193. getUserProfile(userId);
  194. // Update mute state in profileFetched
  195. profileFetched.set(userId, true); // After muting, the user is muted
  196.  
  197. // Remove the Mute button and add the Unmute button
  198. removeButton('Mute User');
  199. addUnmuteButton();
  200. alert('User muted successfully ☠️☠️☠️');
  201. } else {
  202. alert('Failed to mute user');
  203. }
  204. }
  205. catch (error) {
  206. console.error('Error muting user:', error);
  207. }
  208. }
  209.  
  210. // Function to unmute the user
  211. async function unmuteUser(userId) {
  212. if (!token) {
  213. alert('Failed to get authorization token');
  214. return;
  215. }
  216.  
  217. try {
  218. let response = await fetch(
  219. `${hostApi}/xrpc/app.bsky.graph.unmuteActor`,
  220. {
  221. method: 'POST',
  222. headers: {
  223. 'Content-Type': 'application/json',
  224. 'Authorization': `Bearer ${token}`
  225. },
  226. body: JSON.stringify({
  227. actor: userId // Send the userId as "actor"
  228. })
  229. });
  230.  
  231. if (response.ok) {
  232. await getUserProfile(userId);
  233. profileFetched.set(userId, false);
  234. removeButton('Unmute User');
  235. addMuteButton();
  236. alert('User unmuted successfully 😃😃😃');
  237. } else {
  238. alert('Failed to unmute user');
  239. }
  240. }
  241. catch (error) {
  242. console.error('Error unmuting user:', error);
  243. }
  244. }
  245.  
  246. // Função para obter o perfil do usuário
  247. async function getUserProfile(didPlc) {
  248. if (!token) {
  249. alert('Failed to get authorization token');
  250. return;
  251. }
  252.  
  253. try {
  254. const encodedDidPlc = encodeURIComponent(didPlc);
  255. const response = await fetch(`${hostApi}/xrpc/app.bsky.actor.getProfile?actor=${encodedDidPlc}`,
  256. {
  257. method: 'GET',
  258. headers: {
  259. 'Authorization': `Bearer ${token}`
  260. }
  261. });
  262.  
  263. if (response.ok) {
  264. return await response.json();
  265. } else {
  266. console.error('Failed to fetch user profile');
  267. }
  268. }
  269. catch (error) {
  270. console.error('Error fetching user profile:', error);
  271. }
  272. }
  273.  
  274. // Function to add listeners to the dropdown buttons.
  275. function addDropdownButtonListeners() {
  276. document.querySelectorAll('[data-testid="postDropdownBtn"]').forEach(button => {
  277. button.addEventListener('click', async (event) => {
  278. // Capture the div where the button was clicked
  279. const divWhereButtonHasBeenClicked = button.closest('div');
  280.  
  281. // Capture all parent divs
  282. const parentDivs = [];
  283. let selectedElement = divWhereButtonHasBeenClicked;
  284.  
  285. while (selectedElement) {
  286. parentDivs.push(selectedElement);
  287. selectedElement = selectedElement.parentElement;
  288. }
  289.  
  290. // Show the divs where the button was clicked and its parents
  291. //console.log('Clicked Div:', divWhereButtonHasBeenClicked);
  292. // console.log('Parent Divs:', parentDivs);
  293.  
  294. // Get the HTML content of the 6th parent div
  295. const targetDivHtml = parentDivs[5].innerHTML;
  296. // console.log('Div alvo HTML:', targetDivHtml);
  297.  
  298. // Extract did:plc from the HTML content
  299. const didPlc = extractDidPlc(targetDivHtml);
  300. // console.log('did:plc:', didPlc);
  301.  
  302. // Get the user profile and check if the user is muted
  303. if (didPlc && !profileFetched.has(didPlc)) {
  304. let user = await getUserProfile(didPlc);
  305. profileFetched.set(didPlc, user.viewer.muted);
  306. if (user.viewer.muted) {
  307. addUnmuteButton();
  308. } else {
  309. addMuteButton();
  310. }
  311. DidPlcTarget = didPlc;
  312. } else if (didPlc && profileFetched.get(didPlc)) {
  313. DidPlcTarget = didPlc;
  314. addUnmuteButton();
  315. } else if (didPlc) {
  316. DidPlcTarget = didPlc;
  317. addMuteButton();
  318. }
  319. });
  320. });
  321. }
  322.  
  323. // Capture the token from localStorage
  324. getTokenFromLocalStorage();
  325.  
  326. // Add the MUTE USER button periodically
  327. setInterval(addMuteButton, 2000); // Ajuste o intervalo conforme necessário
  328. setInterval(addUnmuteButton, 2000); // Ajuste o intervalo conforme necessário
  329.  
  330. // Add listeners to the dropdown buttons periodically
  331. setInterval(addDropdownButtonListeners, 2000); // Ajuste o intervalo conforme necessário
  332. })();