Add "Mute User" Button to Bluesky Posts

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

// ==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();
    }
})();