☰

chats.gr toolbar

Filter chats.gr users by gender and username.

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

You will need to install an extension such as Tampermonkey or Violentmonkey to install this script.

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

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

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

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

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

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

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

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

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

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

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

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

// ==UserScript==
// @name         chats.gr toolbar
// @namespace    tampermonkey
// @version      2.3
// @description  Filter chats.gr users by gender and username.
// @author       unknownposter025
// @license      MIT
// @match        *://chats.gr/app/*
// @icon         https://www.google.com/s2/favicons?sz=64&domain=chats.gr
// @grant        none
// @run-at       document-idle
// ==/UserScript==

(function () {
    'use strict';

    /* =========================================================
       CONFIG
       ========================================================= */

    const HEADER_SELECTOR = '#chat_head';
    const USER_ITEM_SELECTOR = '.user_item';
    const USERNAME_SELECTOR = 'p.username.user';

    const GENDER_CONTAINER_ID = 'tm-gender-filter-container';
    const SEARCH_CONTAINER_ID = 'tm-word-search-container';
    const SEARCH_INPUT_ID = 'tm-word-search-input';

    const STYLE_ID = 'tm-user-filter-styles';

    const GENDER_HIDDEN_CLASS = 'tm-gender-filter-hidden';
    const SEARCH_HIDDEN_CLASS = 'tm-word-search-hidden';

    const CONTROLS_CLASS = 'tm-user-filter-control';

    const CONTROL_BACKGROUND = '#1077cf';

    const SEARCH_WIDTH = '150px';

    const categories = {
        male: {
            emoji: '♂️',
            className: 'malebg',
            count: 0
        },

        female: {
            emoji: '♀️',
            className: 'femalebg',
            count: 0
        },

        other: {
            emoji: '⚧️',
            className: 'otherbg',
            count: 0
        }
    };


    /* =========================================================
       STATE
       ========================================================= */

    let activeGender = null;

    let observer = null;
    let filterScheduled = false;
    let totalsCounted = false;


    /* =========================================================
       STYLES
       ========================================================= */

    function injectStyles() {
        if (document.getElementById(STYLE_ID)) {
            return;
        }

        const style = document.createElement('style');

        style.id = STYLE_ID;

        style.textContent = `

            /* -------------------------------------------------
               FILTER CLASSES
               ------------------------------------------------- */

            .${GENDER_HIDDEN_CLASS},
            .${SEARCH_HIDDEN_CLASS} {
                display: none !important;
            }


            /* -------------------------------------------------
               SHARED CONTROL PARENTS
               ------------------------------------------------- */

            #${GENDER_CONTAINER_ID},
            #${SEARCH_CONTAINER_ID} {
                float: left !important;

                height: 43px !important;

                margin: 0 !important;
                padding: 0 5px !important;

                box-sizing: border-box !important;

                background: ${CONTROL_BACKGROUND} !important;
                border-radius: 0 !important;

                overflow: hidden !important;
            }


            /* -------------------------------------------------
               GENDER FILTER
               ------------------------------------------------- */

            #${GENDER_CONTAINER_ID} {
                display: none !important;

                width: 230px !important;
            }

            #${GENDER_CONTAINER_ID}.tm-visible {
                display: block !important;
            }

            #${GENDER_CONTAINER_ID} > .btable {
                display: table !important;
                table-layout: fixed !important;

                width: 100% !important;
                height: 43px !important;

                margin: 0 !important;
                padding: 0 !important;
            }

            #${GENDER_CONTAINER_ID} .tm-filter-cell {
                display: table-cell !important;

                width: 100% !important;
                height: 43px !important;

                margin: 0 !important;
                padding: 0 !important;

                vertical-align: middle !important;
            }

            #${GENDER_CONTAINER_ID} .tm-filter-inner {
                display: flex !important;
                align-items: center !important;

                width: 100% !important;
                height: 31px !important;

                gap: 4px !important;

                margin: 0 !important;
                padding: 0 !important;

                box-sizing: border-box !important;

                white-space: nowrap !important;
            }


            /* -------------------------------------------------
               ALL FOUR BUTTONS
               ------------------------------------------------- */

            #${GENDER_CONTAINER_ID} .tm-gender-button {
                appearance: none !important;

                flex: 1 1 0 !important;
                min-width: 0 !important;

                height: 27px !important;

                margin: 0 !important;
                padding: 1px 4px !important;

                border: 1px solid rgba(255, 255, 255, 0.65) !important;
                border-radius: 5px !important;

                background: rgba(255, 255, 255, 0.95) !important;
                color: #111111 !important;

                font-family: Arial, sans-serif !important;
                font-size: 11px !important;
                font-weight: 700 !important;
                line-height: 23px !important;

                text-align: center !important;
                white-space: nowrap !important;

                overflow: hidden !important;

                cursor: pointer !important;
                touch-action: manipulation !important;

                outline: none !important;
                box-shadow: none !important;
            }


            /*
             * Buttons only look pressed while physically
             * being pressed. No persistent active state.
             */
            #${GENDER_CONTAINER_ID}
            .tm-gender-button:active {
                background: rgba(225, 225, 225, 1) !important;
            }


            #${GENDER_CONTAINER_ID} .tm-gender-emoji {
                font-family:
                    "Noto Color Emoji",
                    "Segoe UI Emoji",
                    "Apple Color Emoji",
                    sans-serif !important;

                font-size: 13px !important;
                font-weight: normal !important;
                line-height: 1 !important;

                vertical-align: -1px !important;
            }

            #${GENDER_CONTAINER_ID} .tm-gender-count {
                margin-left: 1px !important;

                font-family: Arial, sans-serif !important;
            }


            /* -------------------------------------------------
               WORD SEARCH
               ------------------------------------------------- */

            #${SEARCH_CONTAINER_ID} {
                display: none !important;

                align-items: center !important;
                justify-content: center !important;

                width: ${SEARCH_WIDTH} !important;
            }

            #${SEARCH_CONTAINER_ID}.tm-visible {
                display: flex !important;
            }

            #${SEARCH_CONTAINER_ID} .tm-word-search-inner {
                display: flex !important;
                align-items: center !important;

                width: 100% !important;
                height: 31px !important;

                margin: 0 !important;
                padding: 0 !important;

                box-sizing: border-box !important;
            }

            #${SEARCH_INPUT_ID} {
                display: block !important;

                width: 100% !important;
                height: 27px !important;

                min-width: 0 !important;

                margin: 0 !important;
                padding: 1px 8px !important;

                box-sizing: border-box !important;

                border: 1px solid rgba(255, 255, 255, 0.65) !important;
                border-radius: 5px !important;

                background: rgba(255, 255, 255, 0.95) !important;
                color: #111111 !important;

                font-family: Arial, sans-serif !important;
                font-size: 11px !important;
                font-weight: 700 !important;
                line-height: 23px !important;

                outline: none !important;
                box-shadow: none !important;
            }

            #${SEARCH_INPUT_ID}::placeholder {
                color: #666666 !important;
                opacity: 1 !important;
            }

            #${SEARCH_INPUT_ID}:focus {
                border-color: rgba(255, 255, 255, 0.95) !important;
            }


            /* -------------------------------------------------
               MOBILE
               ------------------------------------------------- */

            @media (max-width: 480px) {

                #${GENDER_CONTAINER_ID} {
                    width: 220px !important;

                    padding-left: 3px !important;
                    padding-right: 3px !important;
                }

                #${SEARCH_CONTAINER_ID} {
                    width: 170px !important;

                    padding-left: 3px !important;
                    padding-right: 3px !important;
                }

                #${GENDER_CONTAINER_ID} .tm-filter-inner {
                    gap: 3px !important;
                }

                #${GENDER_CONTAINER_ID} .tm-gender-button {
                    flex: 1 1 0 !important;
                    min-width: 0 !important;

                    height: 27px !important;

                    padding-left: 2px !important;
                    padding-right: 2px !important;

                    font-size: 10px !important;
                }

                #${GENDER_CONTAINER_ID} .tm-gender-emoji {
                    font-size: 12px !important;
                }

                #${SEARCH_INPUT_ID} {
                    padding-left: 6px !important;
                    padding-right: 6px !important;

                    font-size: 10px !important;
                }
            }
        `;

        document.documentElement.appendChild(style);
    }


    /* =========================================================
       USER / GENDER HELPERS
       ========================================================= */

    function hasClassCaseInsensitive(element, wantedClass) {
        const target = wantedClass.toLowerCase();

        return Array.from(element.classList).some(
            className =>
                className.toLowerCase() === target
        );
    }


    function getProfileCategory(item) {
        if (
            hasClassCaseInsensitive(
                item,
                categories.male.className
            )
        ) {
            return 'male';
        }

        if (
            hasClassCaseInsensitive(
                item,
                categories.female.className
            )
        ) {
            return 'female';
        }

        if (
            hasClassCaseInsensitive(
                item,
                categories.other.className
            )
        ) {
            return 'other';
        }

        return 'other';
    }


    function getUserItems() {
        return document.querySelectorAll(
            USER_ITEM_SELECTOR
        );
    }


    /* =========================================================
       GENDER COUNTS
       ========================================================= */

    function countInitialProfiles() {
        if (totalsCounted) {
            return;
        }

        const items = getUserItems();

        if (items.length === 0) {
            return;
        }

        for (const category of Object.values(categories)) {
            category.count = 0;
        }

        for (const item of items) {
            const category = getProfileCategory(item);

            categories[category].count++;
        }

        totalsCounted = true;
    }


    function getTotalProfileCount() {
        return Object.values(categories)
            .reduce(
                (total, category) =>
                    total + category.count,
                0
            );
    }


    /* =========================================================
       GENDER FILTER
       ========================================================= */

    function createGenderFilter() {
        const existing =
            document.getElementById(
                GENDER_CONTAINER_ID
            );

        if (existing) {
            return existing;
        }

        const header =
            document.querySelector(
                HEADER_SELECTOR
            );

        if (!header) {
            return null;
        }

        const nativeOptions =
            header.querySelectorAll(
                ':scope > .head_option'
            );

        if (nativeOptions.length < 2) {
            return null;
        }

        const container =
            document.createElement('div');

        container.id =
            GENDER_CONTAINER_ID;

        container.className =
            `head_option ${CONTROLS_CLASS}`;

        const table =
            document.createElement('div');

        table.className =
            'btable';

        const cell =
            document.createElement('div');

        cell.className =
            'bcell_mid tm-filter-cell';

        const inner =
            document.createElement('div');

        inner.className =
            'tm-filter-inner';


        /* -----------------------------------------------------
           THREE GENDER BUTTONS
           ----------------------------------------------------- */

        for (
            const categoryName
            of Object.keys(categories)
        ) {
            const category =
                categories[categoryName];

            const button =
                document.createElement('button');

            button.type = 'button';

            button.className =
                'tm-gender-button';

            button.dataset.category =
                categoryName;

            const emoji =
                document.createElement('span');

            emoji.className =
                'tm-gender-emoji';

            emoji.textContent =
                category.emoji;

            const count =
                document.createElement('span');

            count.className =
                'tm-gender-count';

            count.textContent =
                category.count;

            button.appendChild(emoji);
            button.appendChild(count);


            button.addEventListener(
                'click',
                event => {
                    event.preventDefault();
                    event.stopPropagation();

                    activeGender =
                        categoryName;

                    applyFilters();
                }
            );

            inner.appendChild(button);
        }


        /* -----------------------------------------------------
           RESET / ALL BUTTON
           ----------------------------------------------------- */

        const resetButton =
            document.createElement('button');

        resetButton.type = 'button';

        resetButton.className =
            'tm-gender-button';

        resetButton.dataset.category =
            'reset';

        const resetEmoji =
            document.createElement('span');

        resetEmoji.className =
            'tm-gender-emoji';

        resetEmoji.textContent =
            '✳️';

        const resetCount =
            document.createElement('span');

        resetCount.className =
            'tm-gender-count';

        resetCount.textContent =
            getTotalProfileCount();

        resetButton.appendChild(resetEmoji);
        resetButton.appendChild(resetCount);


        resetButton.addEventListener(
            'click',
            event => {
                event.preventDefault();
                event.stopPropagation();

                activeGender = null;

                applyFilters();
            }
        );

        inner.appendChild(resetButton);


        cell.appendChild(inner);
        table.appendChild(cell);
        container.appendChild(table);


        nativeOptions[0].insertAdjacentElement(
            'afterend',
            container
        );

        return container;
    }


    function updateGenderButtons() {
        const container =
            document.getElementById(
                GENDER_CONTAINER_ID
            );

        if (!container) {
            return;
        }

        /*
         * Update the three gender counts.
         */
        for (
            const [
                categoryName,
                category
            ]
            of Object.entries(categories)
        ) {
            const button =
                container.querySelector(
                    `[data-category="${categoryName}"]`
                );

            if (!button) {
                continue;
            }

            const emoji =
                button.querySelector(
                    '.tm-gender-emoji'
                );

            const count =
                button.querySelector(
                    '.tm-gender-count'
                );

            if (emoji) {
                emoji.textContent =
                    category.emoji;
            }

            if (count) {
                count.textContent =
                    category.count;
            }
        }


        /*
         * Update the ✳️ total count.
         *
         * This is the sum of the same initial counts
         * used by the three gender buttons.
         */
        const resetButton =
            container.querySelector(
                '[data-category="reset"]'
            );

        if (resetButton) {
            const count =
                resetButton.querySelector(
                    '.tm-gender-count'
                );

            if (count) {
                count.textContent =
                    getTotalProfileCount();
            }
        }
    }


    function applyGenderFilter() {
        for (const item of getUserItems()) {
            const category =
                getProfileCategory(item);

            const shouldHide =
                activeGender !== null &&
                category !== activeGender;

            item.classList.toggle(
                GENDER_HIDDEN_CLASS,
                shouldHide
            );
        }

        updateGenderButtons();
    }


    /* =========================================================
       WORD SEARCH
       ========================================================= */

    function createSearchBar() {
        const existing =
            document.getElementById(
                SEARCH_CONTAINER_ID
            );

        if (existing) {
            return existing;
        }

        const header =
            document.querySelector(
                HEADER_SELECTOR
            );

        if (!header) {
            return null;
        }

        const container =
            document.createElement('div');

        container.id =
            SEARCH_CONTAINER_ID;

        container.className =
            `head_option ${CONTROLS_CLASS}`;

        const inner =
            document.createElement('div');

        inner.className =
            'tm-word-search-inner';

        const input =
            document.createElement('input');

        input.id =
            SEARCH_INPUT_ID;

        input.type =
            'text';

        input.autocomplete =
            'off';

        input.spellcheck =
            false;

        input.placeholder =
            'Search usernames...';


        input.addEventListener(
            'input',
            scheduleFilter
        );


        input.addEventListener(
            'keydown',
            event => {
                if (event.key !== 'Escape') {
                    return;
                }

                input.value = '';

                applyFilters();

                input.blur();
            }
        );


        inner.appendChild(input);
        container.appendChild(inner);


        const genderFilter =
            document.getElementById(
                GENDER_CONTAINER_ID
            );

        if (
            genderFilter &&
            genderFilter.parentElement === header
        ) {
            genderFilter.insertAdjacentElement(
                'afterend',
                container
            );
        } else {
            const nativeOptions =
                header.querySelectorAll(
                    ':scope > .head_option'
                );

            if (nativeOptions.length > 0) {
                nativeOptions[0].insertAdjacentElement(
                    'afterend',
                    container
                );
            } else {
                header.appendChild(container);
            }
        }

        return container;
    }


    function applySearchFilter() {
        const input =
            document.getElementById(
                SEARCH_INPUT_ID
            );

        if (!input) {
            return;
        }

        const keyword =
            input.value
                .trim()
                .toLocaleLowerCase();

        for (const item of getUserItems()) {
            const username =
                item.querySelector(
                    USERNAME_SELECTOR
                );

            if (!username) {
                continue;
            }

            const usernameText =
                username.textContent
                    .trim()
                    .toLocaleLowerCase();

            const matches =
                keyword === '' ||
                usernameText.includes(keyword);

            item.classList.toggle(
                SEARCH_HIDDEN_CLASS,
                !matches
            );
        }
    }


    /* =========================================================
       LIST VISIBILITY
       ========================================================= */

    function isElementVisible(element) {
        if (!(element instanceof Element)) {
            return false;
        }

        for (
            let current = element;
            current &&
            current !== document.documentElement;
            current = current.parentElement
        ) {
            const style =
                getComputedStyle(current);

            if (
                style.display === 'none' ||
                style.visibility === 'hidden' ||
                style.visibility === 'collapse'
            ) {
                return false;
            }
        }

        const rect =
            element.getBoundingClientRect();

        return (
            rect.width > 0 &&
            rect.height > 0
        );
    }


    function isUserListVisible() {
        const items =
            getUserItems();

        for (const item of items) {
            const container =
                item.parentElement || item;

            if (
                isElementVisible(container)
            ) {
                return true;
            }
        }

        return false;
    }


    function updateControlVisibility() {
        const gender =
            document.getElementById(
                GENDER_CONTAINER_ID
            );

        const search =
            document.getElementById(
                SEARCH_CONTAINER_ID
            );

        if (!gender || !search) {
            return;
        }

        const visible =
            isUserListVisible();

        gender.classList.toggle(
            'tm-visible',
            visible
        );

        search.classList.toggle(
            'tm-visible',
            visible
        );
    }


    /* =========================================================
       CONTROL POSITION
       ========================================================= */

    function keepControlsTogether() {
        const header =
            document.querySelector(
                HEADER_SELECTOR
            );

        if (!header) {
            return;
        }

        const gender =
            document.getElementById(
                GENDER_CONTAINER_ID
            );

        const search =
            document.getElementById(
                SEARCH_CONTAINER_ID
            );

        if (!gender || !search) {
            return;
        }

        if (
            gender.parentElement !== header ||
            search.parentElement !== header
        ) {
            return;
        }

        if (
            search.previousElementSibling !== gender
        ) {
            gender.insertAdjacentElement(
                'afterend',
                search
            );
        }
    }


    /* =========================================================
       COMBINED FILTERING
       ========================================================= */

    function applyFilters() {
        filterScheduled = false;

        createGenderFilter();
        createSearchBar();

        countInitialProfiles();

        applyGenderFilter();
        applySearchFilter();

        keepControlsTogether();
        updateControlVisibility();
    }


    function scheduleFilter() {
        if (filterScheduled) {
            return;
        }

        filterScheduled = true;

        requestAnimationFrame(
            applyFilters
        );
    }


    /* =========================================================
       MUTATION DETECTION
       ========================================================= */

    function mutationContainsUser(mutation) {
        for (const node of mutation.addedNodes) {
            if (!(node instanceof Element)) {
                continue;
            }

            if (
                node.id === GENDER_CONTAINER_ID ||
                node.id === SEARCH_CONTAINER_ID
            ) {
                continue;
            }

            if (
                node.matches(
                    USER_ITEM_SELECTOR
                ) ||
                node.querySelector(
                    USER_ITEM_SELECTOR
                )
            ) {
                return true;
            }
        }

        return false;
    }


    /* =========================================================
       OBSERVER
       ========================================================= */

    function observePage() {
        if (observer) {
            return;
        }

        observer =
            new MutationObserver(
                mutations => {
                    let usersChanged = false;
                    let controlsMissing = false;

                    for (const mutation of mutations) {
                        if (
                            mutation.type ===
                                'childList' &&
                            mutationContainsUser(
                                mutation
                            )
                        ) {
                            usersChanged = true;
                        }
                    }

                    if (
                        !document.getElementById(
                            GENDER_CONTAINER_ID
                        ) ||
                        !document.getElementById(
                            SEARCH_CONTAINER_ID
                        )
                    ) {
                        controlsMissing = true;
                    }

                    if (controlsMissing) {
                        createGenderFilter();
                        createSearchBar();

                        updateGenderButtons();
                    }

                    if (usersChanged) {
                        scheduleFilter();
                        return;
                    }

                    keepControlsTogether();
                    updateControlVisibility();
                }
            );


        observer.observe(
            document.body,
            {
                childList: true,
                subtree: true,
                attributes: true,
                attributeFilter: [
                    'class',
                    'style',
                    'hidden'
                ]
            }
        );


        document.addEventListener(
            'click',
            () => {
                requestAnimationFrame(() => {
                    keepControlsTogether();
                    updateControlVisibility();
                });
            },
            true
        );
    }


    /* =========================================================
       INITIALIZATION
       ========================================================= */

    function init() {
        injectStyles();

        createGenderFilter();
        createSearchBar();

        applyFilters();

        observePage();
    }


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

})();