Reddit Search - Hide subs/users

Hide search results by subreddit or user

이 스크립트를 설치하려면 Tampermonkey, Greasemonkey 또는 Violentmonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey와 같은 확장 프로그램을 설치해야 합니다.

이 스크립트를 설치하려면 Tampermonkey 또는 Violentmonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey 또는 Userscripts와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 유저 스크립트 관리자 확장 프로그램이 필요합니다.

(이미 유저 스크립트 관리자가 설치되어 있습니다. 설치를 진행합니다!)

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

(이미 유저 스타일 관리자가 설치되어 있습니다. 설치를 진행합니다!)

// ==UserScript==
// @name         Reddit Search - Hide subs/users
// @namespace    Reddit Search - Hide subs/users
// @version      1.0
// @description  Hide search results by subreddit or user
// @match        https://www.reddit.com/search/*
// @icon         https://redditinc.com/hs-fs/hubfs/Reddit%20Inc/Content/Brand%20Page/Reddit_Logo.png?width=200&height=200&name=Reddit_Logo.png
// @grant        GM_getValue
// @grant        GM_setValue
// @grant        GM_addStyle
// @run-at       document-start
// @license      MIT
// ==/UserScript==

(function () {
    'use strict';

    const SUBS_KEY = 'rs_hidden_subs';
    const USERS_KEY = 'rs_hidden_users';

    const hiddenSubs = new Set((GM_getValue(SUBS_KEY, []) || []).map(x => String(x).toLowerCase()));
    const hiddenUsers = new Set((GM_getValue(USERS_KEY, []) || []).map(x => String(x).toLowerCase()));

    GM_addStyle(`
        .rs-x {
            display: inline-flex;
            align-items: center;
            justify-content: center;
            width: 18px;
            height: 18px;
            margin-left: 6px;
            border: 1px solid rgba(255,255,255,.18);
            border-radius: 999px;
            background: rgba(255,255,255,.06);
            color: #ff6b6b;
            font: 700 12px/1 sans-serif;
            cursor: pointer;
            user-select: none;
            vertical-align: middle;
            padding: 0;
            position: relative;
            top: -1px;
        }
        .rs-x:hover { background: rgba(255,255,255,.12); }
        .rs-hidden { display: none !important; }

        .rs-toast-wrap {
            position: fixed;
            right: 16px;
            bottom: 16px;
            z-index: 2147483647;
            display: flex;
            flex-direction: column;
            gap: 8px;
            pointer-events: none;
        }
        .rs-toast {
            display: flex;
            align-items: center;
            gap: 10px;
            max-width: 420px;
            padding: 10px 12px;
            border-radius: 10px;
            background: #1f1f1f;
            color: #fff;
            border: 1px solid rgba(255,255,255,.12);
            box-shadow: 0 10px 30px rgba(0,0,0,.35);
            pointer-events: auto;
        }
        .rs-undo {
            border: 0;
            border-radius: 999px;
            padding: 5px 10px;
            background: #ff4500;
            color: #fff;
            font-weight: 700;
            cursor: pointer;
        }
    `);

    function normSub(s) {
        return String(s || '').trim().replace(/^\/?r\//i, '').toLowerCase();
    }

    function normUser(s) {
        return String(s || '').trim().replace(/^\/?u\//i, '').toLowerCase();
    }

    function save() {
        GM_setValue(SUBS_KEY, [...hiddenSubs]);
        GM_setValue(USERS_KEY, [...hiddenUsers]);
    }

    function stop(e) {
        e.preventDefault();
        e.stopPropagation();
        e.stopImmediatePropagation();
    }

    function parseCtx(el) {
        try {
            return JSON.parse(el.getAttribute('data-faceplate-tracking-context') || '{}');
        } catch {
            return {};
        }
    }

    function toast(msg, undoFn) {
        let wrap = document.querySelector('.rs-toast-wrap');
        if (!wrap) {
            wrap = document.createElement('div');
            wrap.className = 'rs-toast-wrap';
            document.body.appendChild(wrap);
        }

        const t = document.createElement('div');
        t.className = 'rs-toast';

        const span = document.createElement('span');
        span.textContent = msg;

        const btn = document.createElement('button');
        btn.className = 'rs-undo';
        btn.textContent = 'Undo';
        btn.addEventListener('click', (e) => {
            stop(e);
            undoFn();
            t.remove();
        }, true);

        t.append(span, btn);
        wrap.append(t);
        setTimeout(() => t.remove(), 5000);
    }

    function makeX(onClick) {
        const b = document.createElement('button');
        b.className = 'rs-x';
        b.type = 'button';
        b.textContent = '×';
        b.addEventListener('mousedown', stop, true);
        b.addEventListener('click', (e) => {
            stop(e);
            onClick();
        }, true);
        return b;
    }

    function applyHides(root = document) {
        root.querySelectorAll('search-telemetry-tracker[view-events="search/view/post"]').forEach(tracker => {
            const sub = normSub(parseCtx(tracker).subreddit?.name);
            tracker.classList.toggle('rs-hidden', !!sub && hiddenSubs.has(sub));
        });

        root.querySelectorAll('search-telemetry-tracker[view-events="search/view/people"]').forEach(tracker => {
            const user = normUser(parseCtx(tracker).profile?.name);
            tracker.classList.toggle('rs-hidden', !!user && hiddenUsers.has(user));
        });
    }

    function addPostButtons(root = document) {
        root.querySelectorAll('search-telemetry-tracker[view-events="search/view/post"]').forEach(tracker => {
            if (!(tracker instanceof HTMLElement)) return;

            const sub = normSub(parseCtx(tracker).subreddit?.name);
            if (!sub) return;

            const timeEl =
                tracker.querySelector('faceplate-timeago time') ||
                tracker.querySelector('time');

            if (!timeEl) return;
            if (timeEl.parentElement?.querySelector(':scope > .rs-x')) return;

            const btn = makeX(() => {
                hiddenSubs.add(sub);
                save();
                applyHides(document);
                toast(`Hidden r/${sub}`, () => {
                    hiddenSubs.delete(sub);
                    save();
                    applyHides(document);
                });
            });

            timeEl.insertAdjacentElement('afterend', btn);
        });
    }

    function addPeopleButtons(root = document) {
        root.querySelectorAll('search-telemetry-tracker[view-events="search/view/people"]').forEach(tracker => {
            if (!(tracker instanceof HTMLElement)) return;

            const user = normUser(parseCtx(tracker).profile?.name);
            if (!user) return;

            const nameEl =
                tracker.querySelector('[data-testid="search-author"] h3 span[id^="search-results-people-"]') ||
                tracker.querySelector('[data-testid="search-author"] h3 span');

            if (!nameEl) return;
            if (nameEl.parentElement?.querySelector(':scope > .rs-x')) return;

            const btn = makeX(() => {
                hiddenUsers.add(user);
                save();
                applyHides(document);
                toast(`Hidden u/${user}`, () => {
                    hiddenUsers.delete(user);
                    save();
                    applyHides(document);
                });
            });

            nameEl.insertAdjacentElement('afterend', btn);
        });
    }

    function refresh(root = document) {
        addPostButtons(root);
        addPeopleButtons(root);
        applyHides(root);
    }

    let bootTimer = null;
    function bootRefreshBurst() {
        if (bootTimer) clearInterval(bootTimer);

        let runs = 0;
        bootTimer = setInterval(() => {
            refresh(document);
            runs++;
            if (runs >= 40) {
                clearInterval(bootTimer);
                bootTimer = null;
            }
        }, 250);
    }

    const mo = new MutationObserver(muts => {
        let needs = false;
        for (const m of muts) {
            for (const node of m.addedNodes) {
                if (!(node instanceof HTMLElement)) continue;
                needs = true;
                refresh(node);
            }
        }
        if (needs) refresh(document);
    });

    function hookNavigation() {
        const _pushState = history.pushState;
        const _replaceState = history.replaceState;

        history.pushState = function () {
            const r = _pushState.apply(this, arguments);
            setTimeout(() => {
                refresh(document);
                bootRefreshBurst();
            }, 50);
            return r;
        };

        history.replaceState = function () {
            const r = _replaceState.apply(this, arguments);
            setTimeout(() => {
                refresh(document);
                bootRefreshBurst();
            }, 50);
            return r;
        };

        window.addEventListener('popstate', () => {
            setTimeout(() => {
                refresh(document);
                bootRefreshBurst();
            }, 50);
        });
    }

    function init() {
        refresh(document);
        bootRefreshBurst();
        mo.observe(document.documentElement, { childList: true, subtree: true });
        hookNavigation();

        window.addEventListener('load', () => {
            refresh(document);
            bootRefreshBurst();
        });

        document.addEventListener('visibilitychange', () => {
            if (!document.hidden) {
                refresh(document);
            }
        });
    }

    if (document.readyState === 'loading') {
        document.addEventListener('DOMContentLoaded', init, { once: true });
    } else {
        init();
    }
})();