Gender Filter

Filter chats.gr users by male, female, and other, with buttons. Counter numbers update when you reload. Made using Chatgpt for Tampermonkey Legacy.

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

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

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

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

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

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

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

Advertisement:

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

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

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

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

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

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

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

Advertisement:

// ==UserScript==
// @name         Gender Filter
// @namespace    tampermonkey
// @version      1.0
// @description  Filter chats.gr users by male, female, and other, with buttons. Counter numbers update when you reload. Made using Chatgpt for Tampermonkey Legacy.
// @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';

  const ITEM_SELECTOR = '.gprivate.user_item';
  const HEADER_SELECTOR = '#chat_head';
  const NATIVE_BUTTON_SELECTOR = ':scope > .head_option';

  const FILTER_CONTAINER_ID = 'tm-gender-filter-container';
  const STYLE_ID = 'tm-gender-filter-styles';
  const HIDDEN_CLASS = 'tm-gender-filter-hidden';

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

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

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

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

  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 injectStyles() {
    if (document.getElementById(STYLE_ID)) return;

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

    style.textContent = `
      .${HIDDEN_CLASS} {
        display: none !important;
      }

      #${FILTER_CONTAINER_ID} {
        display: none !important;
        float: left !important;

        width: 190px !important;
        height: 43px !important;

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

        box-sizing: border-box !important;
        overflow: hidden !important;

        background: rgb(55, 151, 233) !important;
        border-radius: 0 !important;
      }

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

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

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

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

      #${FILTER_CONTAINER_ID} .tm-filter-cell {
        display: table-cell !important;
        vertical-align: middle !important;

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

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

      #${FILTER_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;
      }

      #${FILTER_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;
      }

      #${FILTER_CONTAINER_ID} .tm-gender-button.tm-disabled {
        background: rgba(20, 20, 20, 0.35) !important;
        color: rgba(255, 255, 255, 0.85) !important;

        opacity: 0.65 !important;
        text-decoration: line-through !important;
      }

      #${FILTER_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;
      }

      #${FILTER_CONTAINER_ID} .tm-gender-count {
        margin-left: 1px !important;
        font-family: Arial, sans-serif !important;
      }

      @media (max-width: 480px) {
        #${FILTER_CONTAINER_ID} {
          width: 180px !important;
          padding-left: 3px !important;
          padding-right: 3px !important;
        }

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

        #${FILTER_CONTAINER_ID} .tm-gender-button {
          padding-left: 2px !important;
          padding-right: 2px !important;
          font-size: 10px !important;
        }

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

    document.documentElement.appendChild(style);
  }

  function createFilterContainer() {
    const existing = document.getElementById(
      FILTER_CONTAINER_ID
    );

    if (existing) return existing;

    const header = document.querySelector(HEADER_SELECTOR);

    if (!header) return null;

    const nativeButtons = header.querySelectorAll(
      NATIVE_BUTTON_SELECTOR
    );

    if (nativeButtons.length < 2) return null;

    const container = document.createElement('div');
    container.id = FILTER_CONTAINER_ID;
    container.className = 'head_option';

    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';

    for (const categoryName of Object.keys(categories)) {
      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';

      const count = document.createElement('span');
      count.className = 'tm-gender-count';

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

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

        categories[categoryName].enabled =
          !categories[categoryName].enabled;

        filterProfiles();
      });

      inner.appendChild(button);
    }

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

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

    return container;
  }

  function countInitialProfiles() {
    if (totalsCounted) return;

    const items = document.querySelectorAll(ITEM_SELECTOR);

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

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

    for (const item of items) {
      categories[getProfileCategory(item)].count++;
    }

    totalsCounted = true;
  }

  function updateButtons() {
    const container = document.getElementById(
      FILTER_CONTAINER_ID
    );

    if (!container) return;

    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;
      }

      button.classList.toggle(
        'tm-disabled',
        !category.enabled
      );

      button.setAttribute(
        'aria-pressed',
        String(category.enabled)
      );
    }
  }

  function filterProfiles() {
    filterScheduled = false;

    createFilterContainer();
    countInitialProfiles();

    for (const item of document.querySelectorAll(ITEM_SELECTOR)) {
      const category = categories[
        getProfileCategory(item)
      ];

      /*
       * Only toggle this script's own class.
       *
       * This preserves display:none set by the username-hiding
       * script or by chats.gr itself.
       */
      item.classList.toggle(
        HIDDEN_CLASS,
        !category.enabled
      );
    }

    updateButtons();
    updateFilterVisibility();
  }

  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() {
    for (const item of document.querySelectorAll(ITEM_SELECTOR)) {
      const listContainer =
        item.parentElement || item;

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

    return false;
  }

  function updateFilterVisibility() {
    const container = createFilterContainer();

    if (!container) return;

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

  function scheduleFilter() {
    if (filterScheduled) return;

    filterScheduled = true;
    requestAnimationFrame(filterProfiles);
  }

  function mutationContainsProfiles(mutation) {
    for (const node of mutation.addedNodes) {
      if (!(node instanceof Element)) continue;
      if (node.id === FILTER_CONTAINER_ID) continue;

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

    return false;
  }

  function observePage() {
    if (observer) return;

    observer = new MutationObserver(mutations => {
      let profilesChanged = false;

      for (const mutation of mutations) {
        if (
          mutation.type === 'childList' &&
          mutationContainsProfiles(mutation)
        ) {
          profilesChanged = true;
          break;
        }
      }

      if (!document.getElementById(FILTER_CONTAINER_ID)) {
        createFilterContainer();
        updateButtons();
      }

      if (profilesChanged) {
        scheduleFilter();
      } else {
        updateFilterVisibility();
      }
    });

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

    document.addEventListener(
      'click',
      () => requestAnimationFrame(
        updateFilterVisibility
      ),
      true
    );
  }

  function init() {
    injectStyles();
    createFilterContainer();
    filterProfiles();
    observePage();
  }

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