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 篡改猴,才能安装此脚本。

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

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

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

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

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

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

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

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

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

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