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.

Terá de instalar uma extensão como Tampermonkey, Greasemonkey ou Violentmonkey para instalar este script.

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

Terá de instalar uma extensão como Tampermonkey ou Violentmonkey para instalar este script.

Terá de instalar uma extensão como Tampermonkey ou Userscripts para instalar este script.

Terá de instalar uma extensão como Tampermonkey para instalar este script.

Terá de instalar uma extensão de gestão de scripts de utilizador para instalar este script.

(Já tenho um gestor de scripts de utilizador, deixe-me instalá-lo!)

Terá de instalar uma extensão como Stylus para instalar este estilo.

Terá de instalar uma extensão como Stylus para instalar este estilo.

Terá de instalar uma extensão como Stylus para instalar este estilo.

Terá de instalar uma extensão de gestão de estilos de utilizador para instalar este estilo.

Terá de instalar uma extensão de gestão de estilos de utilizador para instalar este estilo.

Terá de instalar uma extensão de gestão de estilos de utilizador para instalar este estilo.

(Já tenho um gestor de estilos de utilizador, deixe-me instalá-lo!)

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