Filter chats.gr users by gender and username.
// ==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();
}
})();