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

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Greasemonkey 油猴子Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Userscripts ,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==UserScript==
// @name         Add Mute User Button to Bluesky Posts Menu
// @namespace    http://tampermonkey.net/
// @version      0.9
// @description  Add a "MUTE USER" button to each post's options menu on Bluesky and capture parent divs on button click
// @author       JouySandbox, Trevusimon
// @match        https://bsky.app/*
// @grant        none
// @license GNU GPLv3
// ==/UserScript==

(function () {
  'use strict';

  let hostApi = 'https://russula.us-west.host.bsky.network'
  let token = null;
  let profileFetched = new Map(); // Set to track fetched profiles
  let DidPlcTarget = null;

  // Function to get the token directly from localStorage
  function getTokenFromLocalStorage() {
    const storedData = localStorage.getItem('BSKY_STORAGE');
    if (storedData) {
      try {
        const localStorageData = JSON.parse(storedData);
        token = localStorageData.session.currentAccount.accessJwt;
      }
      catch (error) {
        console.error('Failed to parse session data', error);
      }
    }
  }

  // Function to extract did:plc from URL
  function extractDidPlc(url) {
    const match = url.match(/did:plc:[^/]+/);
    return match ? match[0] : null;
  }

  function removeButton(buttonLabel) {
    // Find the button by aria-label
    document.querySelectorAll(`[aria-label="${buttonLabel}"]`).forEach(button => {
      // Remove the found button
      button.remove();
    });
  }

  // Function to add the "MUTE USER" button in the menu before "Mute thread"
  function addMuteButton() {
    document.querySelectorAll('[data-testid="postDropdownMuteThreadBtn"]')
    .forEach(muteThreadBtn => {
      if (!muteThreadBtn.parentNode.querySelector('.mute-user-button') && !profileFetched.get(
        DidPlcTarget)) {
        // Create the MUTE USER button div with the same style as the menu
        let muteButton = document.createElement('div');
        muteButton.setAttribute('aria-label', 'Mute user');
        muteButton.setAttribute('role', 'menuitem');
        muteButton.setAttribute('tabindex', '-1');
        muteButton.className = 'css-175oi2r r-1loqt21 r-1otgn73 mute-user-button';
        muteButton.style.flexDirection = 'row';
        muteButton.style.alignItems = 'center';
        muteButton.style.gap = '16px';
        muteButton.style.padding = '8px 10px';
        muteButton.style.borderRadius = '4px';
        muteButton.style.minHeight = '32px';
        muteButton.style.outline = '0px';
        muteButton.style.cursor = 'pointer';
        muteButton.style.transition = 'background-color 0.2s ease';

        // Add hover (highlight) on mouse over
        muteButton.onmouseover = function () {
          muteButton.style.backgroundColor = 'rgba(29, 161, 242, 0.1)';
        };
        muteButton.onmouseout = function () {
          muteButton.style.backgroundColor = '';
        };

        // Add the text "Mute User"
        let buttonText = document.createElement('div');
        buttonText.className = 'css-146c3p1';
        buttonText.style.fontSize = '14px';
        buttonText.style.letterSpacing = '0.25px';
        buttonText.style.color = 'rgb(215, 221, 228)';
        buttonText.style.flex = '1 1 0%';
        buttonText.style.fontWeight = '600';
        buttonText.style.lineHeight = '14px';
        buttonText.textContent = 'Mute User';

        // Add the emoji
        let buttonIcon = document.createElement('div');
        buttonIcon.className = 'css-175oi2r';
        buttonIcon.style.marginRight = '-2px';
        buttonIcon.style.marginLeft = '12px';
        buttonIcon.textContent = '☠️';  // Adding the emoji here

        // Add the button to the DOM before "Mute thread"
        muteButton.appendChild(buttonText);
        muteButton.appendChild(buttonIcon);
        muteThreadBtn.parentNode.insertBefore(muteButton, muteThreadBtn);

        // Function when clicking the MUTE USER button
        muteButton.onclick = () => {
          let post = muteThreadBtn.closest('.post-class'); // Adjust selector to find the post
          // let userId = post.getAttribute('data-user-id'); // Adjust to get the user ID correctly
          muteUser(DidPlcTarget);
        };
      }
    });
  }

  function addUnmuteButton() {
    document.querySelectorAll('[data-testid="postDropdownMuteThreadBtn"]')
    .forEach(muteThreadBtn => {
      if (!muteThreadBtn.parentNode.querySelector('.unmute-user-button') && profileFetched.get(DidPlcTarget)) {
        // Create the UNMUTE USER button div with the same style as the menu
        let unmuteButton = document.createElement('div');
        unmuteButton.setAttribute('aria-label', 'Unmute User');
        unmuteButton.setAttribute('role', 'menuitem');
        unmuteButton.setAttribute('tabindex', '-1');
        unmuteButton.className = 'css-175oi2r r-1loqt21 r-1otgn73 unmute-user-button';
        unmuteButton.style.flexDirection = 'row';
        unmuteButton.style.alignItems = 'center';
        unmuteButton.style.gap = '16px';
        unmuteButton.style.padding = '8px 10px';
        unmuteButton.style.borderRadius = '4px';
        unmuteButton.style.minHeight = '32px';
        unmuteButton.style.outline = '0px';
        unmuteButton.style.cursor = 'pointer';
        unmuteButton.style.transition = 'background-color 0.2s ease';

        // Add hover (highlight) on mouse over
        unmuteButton.onmouseover = function () {
          unmuteButton.style.backgroundColor = 'rgba(29, 161, 242, 0.1)';
        };
        unmuteButton.onmouseout = function () {
          unmuteButton.style.backgroundColor = '';
        };

        // Add the text "Unmute User"
        let buttonText = document.createElement('div');
        buttonText.className = 'css-146c3p1';
        buttonText.style.fontSize = '14px';
        buttonText.style.letterSpacing = '0.25px';
        buttonText.style.color = 'rgb(215, 221, 228)';
        buttonText.style.flex = '1 1 0%';
        buttonText.style.fontWeight = '600';
        buttonText.style.lineHeight = '14px';
        buttonText.textContent = 'Unmute User';

        // Add the emoji
        let buttonIcon = document.createElement('div');
        buttonIcon.className = 'css-175oi2r';
        buttonIcon.style.marginRight = '-2px';
        buttonIcon.style.marginLeft = '12px';
        buttonIcon.textContent = '😃';  // Adding the emoji here

        // Add the button to the DOM before "Mute thread"
        unmuteButton.appendChild(buttonText);
        unmuteButton.appendChild(buttonIcon);
        muteThreadBtn.parentNode.insertBefore(unmuteButton, muteThreadBtn);

        // Function when clicking the UNMUTE USER button
        unmuteButton.onclick = () => {
          let post = muteThreadBtn.closest('.post-class'); // Adjust selector to find the post
          unmuteUser(DidPlcTarget);
        };
      }
    });
  }

  // Function to mute the user
  async function muteUser(userId) {
    if (!token) {
      alert('Failed to get authorization token');
      return;
    }

    try {
      let response = await fetch(
        `${hostApi}/xrpc/app.bsky.graph.muteActor`,
        {
          method: 'POST',
          headers: {
            'Content-Type': 'application/json',
            'Authorization': `Bearer ${token}`
          },
          body: JSON.stringify({
            actor: userId // Send the userId as "actor"
          })
        });

      if (response.ok) {
        getUserProfile(userId);
        // Update mute state in profileFetched
        profileFetched.set(userId, true);  // After muting, the user is muted

        // Remove the Mute button and add the Unmute button
        removeButton('Mute User');
        addUnmuteButton();
        alert('User muted successfully ☠️☠️☠️');
      } else {
        alert('Failed to mute user');
      }
    }
    catch (error) {
      console.error('Error muting user:', error);
    }
  }

  // Function to unmute the user
  async function unmuteUser(userId) {
    if (!token) {
      alert('Failed to get authorization token');
      return;
    }

    try {
      let response = await fetch(
        `${hostApi}/xrpc/app.bsky.graph.unmuteActor`,
        {
          method: 'POST',
          headers: {
            'Content-Type': 'application/json',
            'Authorization': `Bearer ${token}`
          },
          body: JSON.stringify({
            actor: userId // Send the userId as "actor"
          })
        });

      if (response.ok) {
        await getUserProfile(userId);
        profileFetched.set(userId, false);
        removeButton('Unmute User');
        addMuteButton();
        alert('User unmuted successfully 😃😃😃');
      } else {
        alert('Failed to unmute user');
      }
    }
    catch (error) {
      console.error('Error unmuting user:', error);
    }
  }

  // Função para obter o perfil do usuário
  async function getUserProfile(didPlc) {
    if (!token) {
      alert('Failed to get authorization token');
      return;
    }

    try {
      const encodedDidPlc = encodeURIComponent(didPlc);
      const response = await fetch(`${hostApi}/xrpc/app.bsky.actor.getProfile?actor=${encodedDidPlc}`,
        {
          method: 'GET',
          headers: {
            'Authorization': `Bearer ${token}`
          }
        });

      if (response.ok) {
        return await response.json();
      } else {
        console.error('Failed to fetch user profile');
      }
    }
    catch (error) {
      console.error('Error fetching user profile:', error);
    }
  }

  // Function to add listeners to the dropdown buttons.
  function addDropdownButtonListeners() {
    document.querySelectorAll('[data-testid="postDropdownBtn"]').forEach(button => {
      button.addEventListener('click', async (event) => {
        // Capture the div where the button was clicked
        const divWhereButtonHasBeenClicked = button.closest('div');

        // Capture all parent divs
        const parentDivs = [];
        let selectedElement = divWhereButtonHasBeenClicked;

        while (selectedElement) {
          parentDivs.push(selectedElement);
          selectedElement = selectedElement.parentElement;
        }

        // Show the divs where the button was clicked and its parents
        //console.log('Clicked Div:', divWhereButtonHasBeenClicked);
        // console.log('Parent Divs:', parentDivs);

        // Get the HTML content of the 6th parent div
        const targetDivHtml = parentDivs[5].innerHTML;
        // console.log('Div alvo HTML:', targetDivHtml);

        // Extract did:plc from the HTML content
        const didPlc = extractDidPlc(targetDivHtml);
        // console.log('did:plc:', didPlc);

        // Get the user profile and check if the user is muted
        if (didPlc && !profileFetched.has(didPlc)) {
          let user = await getUserProfile(didPlc);
          profileFetched.set(didPlc, user.viewer.muted);
          if (user.viewer.muted) {
            addUnmuteButton();
          } else {
            addMuteButton();
          }
          DidPlcTarget = didPlc;
        } else if (didPlc && profileFetched.get(didPlc)) {
          DidPlcTarget = didPlc;
          addUnmuteButton();
        } else if (didPlc) {
          DidPlcTarget = didPlc;
          addMuteButton();
        }
      });
    });
  }

  // Capture the token from localStorage
  getTokenFromLocalStorage();

  // Add the MUTE USER button periodically
  setInterval(addMuteButton, 2000);  // Ajuste o intervalo conforme necessário
  setInterval(addUnmuteButton, 2000);  // Ajuste o intervalo conforme necessário

  // Add listeners to the dropdown buttons periodically
  setInterval(addDropdownButtonListeners, 2000);  // Ajuste o intervalo conforme necessário
})();