Add a mute button to Bluesky posts, to allow you to quickly mute a user
// ==UserScript==
// @name Add "Mute User" Button to Bluesky Posts
// @namespace plonked
// @description Add a mute button to Bluesky posts, to allow you to quickly mute a user
// @author @plonked.bsky.social
// @match *://bsky.app/*
// @grant none
// @version 0.0.1.20241128204020
// ==/UserScript==
(function() {
'use strict';
const BUTTON_CLASS = 'bsky-mute-btn';
const PROCESSED_CLASS = 'bsky-mute-processed';
const POST_SELECTORS = {
feedItem: '[data-testid^="feedItem-by-"]',
postPage: '[data-testid^="postThreadItem-by-"]',
searchItem: 'div[role="link"][tabindex="0"]'
};
let hostApi = 'https://cordyceps.us-west.host.bsky.network';
let token = null;
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 createMuteButton() {
const button = document.createElement('div');
button.className = `css-175oi2r r-1loqt21 r-1otgn73 ${BUTTON_CLASS}`;
button.setAttribute('role', 'button');
button.setAttribute('tabindex', '0');
button.style.cssText = `
position: absolute;
top: 8px;
right: 8px;
border-radius: 999px;
flex-direction: row;
justify-content: center;
align-items: center;
overflow: hidden;
padding: 5px;
cursor: pointer;
transition: background-color 0.2s ease;
opacity: 0.5;
z-index: 10;
`;
const icon = document.createElement('div');
icon.textContent = '🔇';
icon.style.cssText = `
font-size: 16px;
filter: grayscale(1);
`;
button.appendChild(icon);
button.onmouseover = () => {
button.style.backgroundColor = 'rgba(29, 161, 242, 0.1)';
button.style.opacity = '1';
};
button.onmouseout = () => {
button.style.backgroundColor = '';
button.style.opacity = '0.5';
};
return button;
}
async function muteUser(userId) {
if (!token) {
console.error('Failed to get authorization token');
return false;
}
try {
const response = await fetch(
`${hostApi}/xrpc/app.bsky.graph.muteActor`,
{
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${token}`
},
body: JSON.stringify({ actor: userId })
}
);
return response.ok;
} catch (error) {
console.error('Error muting user:', error);
return false;
}
}
function extractDidPlc(element) {
const html = element.innerHTML;
const match = html.match(/did:plc:[^/"]+/);
return match ? match[0] : null;
}
function findNameInPost(post) {
const testId = post.getAttribute('data-testid');
if (testId) {
const match = testId.match(/(?:feedItem-by-|postThreadItem-by-)([^.]+)/);
if (match) return match[1];
}
const profileLinks = post.querySelectorAll('a[href^="/profile/"]');
for (const link of profileLinks) {
const nameElement = link.querySelector('.css-1jxf684[style*="font-weight: 600"]');
if (nameElement) {
let name = nameElement.textContent.trim();
if (name.startsWith('@')) name = name.slice(1);
if (name.endsWith('.bsky.social')) name = name.replace('.bsky.social', '');
return name;
}
}
return null;
}
function hideAllPostsForUser(didPlc) {
document.querySelectorAll(Object.values(POST_SELECTORS).join(',')).forEach(post => {
if (post.innerHTML.includes(didPlc)) {
post.style.display = 'none';
}
});
}
async function addMuteButton(post) {
if (post.classList.contains(PROCESSED_CLASS)) return;
if (window.getComputedStyle(post).position === 'static') {
post.style.position = 'relative';
}
const didPlc = extractDidPlc(post);
if (!didPlc) return;
const username = findNameInPost(post);
if (!username) return;
const button = createMuteButton();
button.setAttribute('data-did-plc', didPlc);
button.onclick = async (e) => {
e.preventDefault();
e.stopPropagation();
const success = await muteUser(didPlc);
if (success) {
hideAllPostsForUser(didPlc);
}
};
post.appendChild(button);
post.classList.add(PROCESSED_CLASS);
}
function initialize() {
console.log('Initializing Bluesky Direct Mute Button');
getTokenFromLocalStorage();
const observer = new MutationObserver((mutations) => {
if (mutations.some(mutation => mutation.addedNodes.length)) {
const unprocessedPosts = document.querySelectorAll(
Object.values(POST_SELECTORS)
.map(selector => `${selector}:not(.${PROCESSED_CLASS})`)
.join(',')
);
unprocessedPosts.forEach(addMuteButton);
}
});
observer.observe(document.body, { childList: true, subtree: true });
document.querySelectorAll(
Object.values(POST_SELECTORS)
.map(selector => `${selector}:not(.${PROCESSED_CLASS})`)
.join(',')
).forEach(addMuteButton);
}
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', initialize);
} else {
initialize();
}
})();