Romeo Additions

Enhances GR, especially for non-PLUS users

Verzia zo dňa 24.11.2024. Pozri najnovšiu verziu.

Na nainštalovanie skriptu si budete musieť nainštalovať rozšírenie, ako napríklad Tampermonkey, Greasemonkey alebo Violentmonkey.

Na nainštalovanie skriptu si budete musieť nainštalovať rozšírenie, ako napríklad Tampermonkey, % alebo Violentmonkey.

Na nainštalovanie skriptu si budete musieť nainštalovať rozšírenie, ako napríklad Tampermonkey, % alebo Violentmonkey.

Na nainštalovanie skriptu si budete musieť nainštalovať rozšírenie, ako napríklad Tampermonkey alebo Userscripts.

Na inštaláciu tohto skriptu je potrebné nainštalovať rozšírenie, ako napríklad Tampermonkey.

Na inštaláciu tohto skriptu je potrebné nainštalovať rozšírenie správcu používateľských skriptov.

(Už mám správcu používateľských skriptov, nechajte ma ho nainštalovať!)

Na inštaláciu tohto štýlu je potrebné nainštalovať rozšírenie, ako napríklad Stylus.

Na inštaláciu tohto štýlu je potrebné nainštalovať rozšírenie, ako napríklad Stylus.

Na inštaláciu tohto štýlu je potrebné nainštalovať rozšírenie, ako napríklad Stylus.

Na inštaláciu tohto štýlu je potrebné nainštalovať rozšírenie správcu používateľských štýlov.

Na inštaláciu tohto štýlu je potrebné nainštalovať rozšírenie správcu používateľských štýlov.

Na inštaláciu tohto štýlu je potrebné nainštalovať rozšírenie správcu používateľských štýlov.

(Už mám správcu používateľských štýlov, nechajte ma ho nainštalovať!)

// ==UserScript==
// @name           Romeo Additions
// @name:de        Romeo Additions
// @namespace      https://greasyfork.org/en/users/723211-ray/
// @version        7.4.1
// @description    Enhances GR, especially for non-PLUS users
// @description:de Verbessert GR, insbesondere für nicht-PLUS-Benutzer
// @author         -Ray-, Djamana
// @match          *://*.romeo.com/*
// @license        MIT
// @grant          none
// @supportURL     https://greasyfork.org/en/scripts/419514-romeo-additions
// ==/UserScript==

const CM2FT = 0.03280839895;
const KG2LBS = 2.20462262185;
const M2MI = 0.0006213712;

function escapeHtml(unsafe)
{
	return unsafe
		.replace(/&/g, "&")
		.replace(/</g, "&lt;")
		.replace(/>/g, "&gt;")
		.replace(/"/g, "&quot;")
		.replace(/'/g, "&#039;");
}

function formatTime(str)
{
	const date = new Date(Date.parse(str));
	const lang = getLang();
	return `${date.toLocaleDateString(lang)} ${date.toLocaleTimeString(lang)}`;
}

function getLang()
{
	return document.documentElement.getAttribute("lang") || "en";
}

function isJson(value)
{
	if (!value || typeof value !== "string")
		return false;

	try
	{
		JSON.parse(value);
		return true;
	}
	catch
	{
		return false;
	}
}

function log()
{
	if (GM_info.script.version === "0.0.0")
	{
		if (arguments.length > 1 && arguments[1])
			arguments[0] += "\n";
		console.log(...arguments);
	}
}

function round(value, maxDigits = 0)
{
	const f = Math.pow(10, maxDigits);
	return Math.round(value * f) / f;
}

// ---- CSS ----

function addCss(css)
{
	let style = document.createElement('style');
	if (style.styleSheet)
		style.styleSheet.cssText = css;
	else
		style.appendChild(document.createTextNode(css));
	return document.head.appendChild(style);
};

function setCssProp(name, value)
{
	document.documentElement.style.setProperty(name, value);
}

// ---- DOM ----

const domHooks = {};

function initDom()
{
	function tagCall(el, callback)
	{
		if (!el.getAttribute("data-ra-hook"))
		{
			el.setAttribute("data-ra-hook", true);
			callback(el);
		}
	}

	const observer = new MutationObserver((mutations, observer) =>
	{
		for (const mutation of mutations)
		{
			for (const el of mutation.addedNodes)
			{
				if (el.nodeType === Node.ELEMENT_NODE)
				{
					for (const [selector, callback] of Object.entries(domHooks))
					{
						if (el.matches(selector))
						{
							// Trigger for element.
							tagCall(el, callback);
						}
						else
						{
							// Trigger for children of attached elements.
							for (const elChild of el.querySelectorAll(selector))
								tagCall(elChild, callback);
						}
					}
				}
			}
		}
	});
	observer.observe(document.body, { subtree: true, childList: true });
}

function addElement(parent, html)
{
	parent.insertAdjacentHTML("beforeend", html);
	return parent.lastChild;
}

function onDom(selector, callback)
{
	// Trigger for existing elements.
	for (const el of document.querySelectorAll(selector))
		callback(el);

	// Add to observer list.
	domHooks[selector] = callback;
}

// ---- Translation ----

const strings =
{
	aboutMe:
	{
		de: "Über mich",
		en: "About Me",
	},
	age:
	{
		de: "Alter",
		en: "Age",
	},
	ageRange:
	{
		de: "Altersspanne",
		en: "Age range",
	},
	ageRangeValue:
	{
		de: "Zwischen $from und $to",
		en: "Between $from and $to",
	},
	analPosition:
	{
		en: "Position",
	},
	analPosition_TOP_ONLY:
	{
		de: "Nur Aktiv",
		en: "Top only",
	},
	analPosition_MORE_TOP:
	{
		de: "Eher Aktiv",
		en: "More top",
	},
	analPosition_VERSATILE:
	{
		de: "Flexibel",
		en: "Versatile",
	},
	analPosition_MORE_BOTTOM:
	{
		de: "Eher Passiv",
		en: "More bottom",
	},
	analPosition_BOTTOM_ONLY:
	{
		de: "Nur Passiv",
		en: "Bottom only",
	},
	analPosition_NO:
	{
		de: "Kein Anal",
		en: "No anal",
	},
	beard:
	{
		de: "Bart",
		en: "Beard",
	},
	beard_DESIGNER_STUBBLE:
	{
		de: "3-Tage-Bart",
		en: "Designer stubble",
	},
	beard_FULL_BEARD:
	{
		de: "Vollbart",
		en: "Full beard",
	},
	beard_GOATEE:
	{
		en: "Goatee",
	},
	beard_MOUSTACHE:
	{
		de: "Schnauzer",
		en: "Moustache",
	},
	beard_NO_BEARD:
	{
		de: "Kein Bart",
		en: "No beard",
	},
	bedAndBreakfast:
	{
		en: "Bed & Breakfast",
	},
	blockUser:
	{
		de: "Benutzer blockieren",
		en: "Block user",
	},
	bmi:
	{
		en: "BMI",
	},
	bmiMildThin:
	{
		de: "Leichtes Untergewicht",
		en: "Mildly Thin",
	},
	bmiModerateThin:
	{
		de: "Mäßiges Untergewicht",
		en: "Moderately Thin",
	},
	bmiNormal:
	{
		de: "Normal",
		en: "Normal",
	},
	bmiObese1:
	{
		de: "Adipositas I",
		en: "Obese Class I",
	},
	bmiObese2:
	{
		de: "Adipositas II",
		en: "Obese Class II",
	},
	bmiObese3:
	{
		de: "Adipositas III",
		en: "Obese Class III",
	},
	bmiPreObese:
	{
		de: "Präadipositas",
		en: "Pre-Obese",
	},
	bmiSevereThin:
	{
		de: "Starkes Untergewicht",
		en: "Severely Thin",
	},
	bodyType:
	{
		de: "Statur",
		en: "Body Type",
	},
	bodyType_ATHLETIC:
	{
		de: "Athletisch",
		en: "Athletic",
	},
	bodyType_AVERAGE:
	{
		de: "Normal",
		en: "Average",
	},
	bodyType_BELLY:
	{
		de: "Bauch",
		en: "Belly",
	},
	bodyType_MUSCULAR:
	{
		de: "Muskulös",
		en: "Muscular",
	},
	bodyType_SLIM:
	{
		de: "Schlank",
		en: "Slim",
	},
	bodyType_STOCKY:
	{
		de: "Stämmig",
		en: "Stocky",
	},
	bodyHair:
	{
		de: "Körperbehaarung",
		en: "Body Hair",
	},
	bodyHair_AVERAGE:
	{
		de: "Mittel behaart",
		en: "Hairy",
	},
	bodyHair_LITTLE:
	{
		de: "Wenig behaart",
		en: "Not very hairy",
	},
	bodyHair_SHAVED:
	{
		de: "Rasiert",
		en: "Shaved",
	},
	bodyHair_SMOOTH:
	{
		de: "Unbehaart",
		en: "Smooth",
	},
	bodyHair_VERY_HAIRY:
	{
		de: "Stark behaart",
		en: "Very hairy",
	},
	clearList:
	{
		de: "Möchtest du wirklich alle Einträge in der Liste entfernen?",
		en: "Do you really want to remove all elements from the list?",
	},
	concision:
	{
		de: "Beschneidung",
		en: "Concision",
	},
	concision_CUT:
	{
		de: "Beschnitten",
		en: "Cut",
	},
	concision_UNCUT:
	{
		de: "Unbeschnitten",
		en: "Uncut",
	},
	customRadius:
	{
		de: "Benutzerdefinierter Radius",
		en: "Custom Radius",
	},
	dick:
	{
		de: "Schwanz",
		en: "Dick",
	},
	dick_S:
	{
		en: "S",
	},
	dick_M:
	{
		en: "M",
	},
	dick_L:
	{
		en: "L",
	},
	dick_XL:
	{
		en: "XL",
	},
	dick_XXL:
	{
		en: "XXL",
	},
	dirty:
	{
		en: "Dirty",
	},
	dirty_NO:
	{
		de: "Kein Dirty",
		en: "No dirty",
	},
	dirty_WS_ONLY:
	{
		de: "Ja, aber nur NS",
		en: "WS only",
	},
	dirty_YES:
	{
		en: "Dirty",
	},
	display:
	{
		de: "Anzeige",
		en: "Display",
	},
	distance:
	{
		de: "Entfernung",
		en: "Distance",
	},
	enhancedFilter:
	{
		de: "Filter mit allen Details",
		en: "Filter with all details",
	},
	enhancedFilterDesc:
	{
		de: "Erlaubt Radar-Ergebnisse nach allen Details zu filtern.",
		en: "Allows to filter radar results by additional details.",
	},
	enhancedTiles:
	{
		de: "Große Kacheln erzwingen",
		en: "Force big grid",
	},
	enhancedTilesDesc:
	{
		de: "Zeigt alle Benutzer in großen Kacheln.",
		en: "Shows all users in big tiles.",
	},
	ethnicity:
	{
		de: "Typ",
		en: "Ethnicity",
	},
	ethnicity_ARAB:
	{
		de: "Araber",
		en: "Arab",
	},
	ethnicity_ASIAN:
	{
		de: "Asiate",
		en: "Asian",
	},
	ethnicity_BLACK:
	{
		de: "Schwarz",
		en: "Black",
	},
	ethnicity_CAUCASIAN:
	{
		de: "Europäer",
		en: "Caucasian",
	},
	ethnicity_INDIAN:
	{
		de: "Inder",
		en: "Indian",
	},
	ethnicity_LATIN:
	{
		de: "Latino",
		en: "Latin",
	},
	ethnicity_MEDITERRANEAN:
	{
		de: "Südländer",
		en: "Mediterranean",
	},
	ethnicity_MIXED:
	{
		en: "Mixed",
	},
	eyeColor:
	{
		de: "Augenfarbe",
		en: "Eye Colour",
	},
	eyeColor_BLUE:
	{
		de: "Blau",
		en: "Blue",
	},
	eyeColor_BROWN:
	{
		de: "Braun",
		en: "Brown",
	},
	eyeColor_GREEN:
	{
		de: "Grün",
		en: "Green",
	},
	eyeColor_GREY:
	{
		de: "Grau",
		en: "Grey",
	},
	eyeColor_OTHER:
	{
		de: "Sonstige",
		en: "Other",
	},
	fetish:
	{
		de: "Fetisch",
		en: "Fetish",
	},
	fetish_BOOTS:
	{
		en: "Boots",
	},
	fetish_CROSSDRESSING:
	{
		de: "Cross-Dressing",
		en: "Cross-dressing",
	},
	fetish_DRAG:
	{
		de: "Dessous",
		en: "Lingerie",
	},
	fetish_FORMAL:
	{
		de: "Anzug",
		en: "Formal dress",
	},
	fetish_JEANS:
	{
		en: "Jeans",
	},
	fetish_LEATHER:
	{
		de: "Leder",
		en: "Leather",
	},
	fetish_LYCRA:
	{
		en: "Lycra",
	},
	fetish_RUBBER:
	{
		en: "Rubber",
	},
	fetish_SKATER:
	{
		en: "Skater",
	},
	fetish_SKINS:
	{
		en: "Skins & Punks",
	},
	fetish_SNEAKERS:
	{
		en: "Sneakers & Socks",
	},
	fetish_SPORTS:
	{
		de: "Sportsgear",
		en: "Sports gear",
	},
	fetish_TECHNO:
	{
		en: "Raver",
	},
	fetish_UNDERWEAR:
	{
		de: "Unterwäsche",
		en: "Underwear",
	},
	fetish_UNIFORM:
	{
		en: "Uniform",
	},
	fetish_WORKER:
	{
		de: "Handwerker",
		en: "Worker",
	},
	filters:
	{
		en: "Filters",
		de: "Filter",
	},
	fisting:
	{
		de: "Fisten",
		en: "Fisting",
	},
	fisting_ACTIVE:
	{
		de: "FF Aktiv",
		en: "FF Active",
	},
	fisting_ACTIVE_PASSIVE:
	{
		de: "FF Flexibel",
		en: "FF Versatile",
	},
	fisting_NO:
	{
		de: "Kein FF",
		en: "No FF",
	},
	fisting_PASSIVE:
	{
		de: "FF Passiv",
		en: "FF Passive",
	},
	fullHeadlines:
	{
		de: "Vollständige Überschriften",
		en: "Full headlines",
	},
	fullHeadlinesDesc:
	{
		de: "Zeigt lange Profilüberschriften vollständig.",
		en: "Shows long profile headlines completely.",
	},
	fullMessages:
	{
		de: "Vollständige Nachrichten",
		en: "Full messages",
	},
	fullMessagesDesc:
	{
		de: "Zeigt Nachrichten ungekürzt in der Nachrichtenliste.",
		en: "Shows messages without truncation in the message list.",
	},
	gender:
	{
		de: "Geschlecht",
		en: "Gender",
	},
	gender_MAN:
	{
		de: "Mann",
		en: "Man",
	},
	gender_TRANS_MAN:
	{
		de: "Transmann",
		en: "Trans man",
	},
	gender_TRANS_WOMAN:
	{
		de: "Transfrau",
		en: "Trans woman",
	},
	gender_NON_BINARY:
	{
		de: "Nicht binär",
		en: "Non-binary",
	},
	gender_OTHER:
	{
		de: "Anderes",
		en: "Other",
	},
	genderOrientation:
	{
		de: "Ich bin",
		en: "I am",
	},
	general:
	{
		de: "Allgemein",
		en: "General",
	},
	hairColor:
	{
		de: "Haarfarbe",
		en: "Hair Colour",
	},
	hairColor_BLACK:
	{
		de: "Schwarz",
		en: "Black",
	},
	hairColor_BLOND:
	{
		en: "Blond",
	},
	hairColor_BROWN:
	{
		de: "Braune Haare",
		en: "Brown",
	},
	hairColor_GREY:
	{
		de: "Grau",
		en: "Grey",
	},
	hairColor_LIGHT_BROWN:
	{
		de: "Dunkelblond",
		en: "Light brown",
	},
	hairColor_OTHER:
	{
		de: "Sonstige",
		en: "Other",
	},
	hairColor_RED:
	{
		de: "Rot",
		en: "Red",
	},
	hairLength:
	{
		de: "Haarlänge",
		en: "Hair Length",
	},
	hairLength_AVERAGE:
	{
		de: "Normal",
		en: "Average",
	},
	hairLength_LONG:
	{
		de: "Lang",
		en: "Long",
	},
	hairLength_PUNK:
	{
		en: "Punk",
	},
	hairLength_SHAVED:
	{
		de: "Rasiert",
		en: "Shaved",
	},
	hairLength_SHORT:
	{
		de: "Kurz",
		en: "Short",
	},
	height:
	{
		de: "Größe",
		en: "Height",
	},
	hiddenUsers:
	{
		de: "Ausgeblendete Benutzer",
		en: "Hidden users",
	},
	hideActivities:
	{
		de: "In Aktivitäten ausblenden",
		en: "Hide in activities",
	},
	hideContacts:
	{
		de: "In Kontakten ausblenden",
		en: "Hide in contacts",
	},
	hideLikes:
	{
		de: "In Bilder-Likes ausblenden",
		en: "Hide in picture likes",
	},
	hideMessages:
	{
		de: "In Chat ausblenden",
		en: "Hide in chat",
	},
	hideUser:
	{
		de: "Benutzer ausblenden",
		en: "Hide user",
	},
	hideVisits:
	{
		de: "In Besuchern ausblenden",
		en: "Hide in visitors",
	},
	interests:
	{
		de: "Interessen",
		en: "Interests",
	},
	interests_ART:
	{
		de: "Kunst",
		en: "Art",
	},
	interests_BOARDGAME:
	{
		de: "Brettspiele",
		en: "Board games",
	},
	interests_CAR:
	{
		de: "Autos",
		en: "Cars",
	},
	interests_COLLECT:
	{
		de: "Sammeln",
		en: "Collecting",
	},
	interests_COMPUTER:
	{
		de: "Computer",
		en: "Computers",
	},
	interests_COOK:
	{
		de: "Kochen",
		en: "Cooking",
	},
	interests_DANCE:
	{
		en: "Dance",
	},
	interests_FILM:
	{
		en: "Film & Video",
	},
	interests_FOTO:
	{
		de: "Fotografie",
		en: "Photography",
	},
	interests_GAME:
	{
		de: "Computerspiele",
		en: "Gaming",
	},
	interests_LITERATURE:
	{
		de: "Literatur",
		en: "Literature",
	},
	interests_MODELING:
	{
		de: "Modellbau",
		en: "Model building",
	},
	interests_MOTORBIKE:
	{
		de: "Motorrad",
		en: "Motorbikes",
	},
	interests_MUSIC:
	{
		de: "Musik",
		en: "Music",
	},
	interests_NATURE:
	{
		de: "Natur",
		en: "Nature",
	},
	interests_POLITICS:
	{
		de: "Politik",
		en: "Politics",
	},
	interests_TV:
	{
		en: "TV",
	},
	languages:
	{
		de: "Sprachen",
		en: "Languages",
	},
	languages_af:
	{
		de: "Afrikaans",
		en: "Afrikaans",
	},
	languages_ar:
	{
		de: "Arabisch",
		en: "Arabic",
	},
	languages_arm:
	{
		de: "Armenisch",
		en: "Armenian",
	},
	languages_az:
	{
		de: "Aserbaidschanisch",
		en: "Azerbaijani",
	},
	languages_be:
	{
		de: "Belarussisch",
		en: "Belarusian",
	},
	languages_bg:
	{
		de: "Bulgarisch",
		en: "Bulgarian",
	},
	languages_bn:
	{
		de: "Bengali",
		en: "Bengali",
	},
	languages_bs:
	{
		de: "Bosnisch",
		en: "Bosnian",
	},
	languages_bur:
	{
		de: "Burmesisch",
		en: "Burmese",
	},
	languages_ca:
	{
		de: "Katalanisch",
		en: "Catalan",
	},
	languages_ceb:
	{
		de: "Cebuano",
		en: "Cebuano",
	},
	languages_cs:
	{
		de: "Tschechisch",
		en: "Czech",
	},
	languages_da:
	{
		de: "Dänisch",
		en: "Danish",
	},
	languages_de:
	{
		de: "Deutsch",
		en: "German",
	},
	languages_el:
	{
		de: "Griechisch",
		en: "Greek",
	},
	languages_en:
	{
		de: "Englisch",
		en: "English",
	},
	languages_eo:
	{
		de: "Esperanto",
		en: "Esperanto",
	},
	languages_es:
	{
		de: "Spanisch",
		en: "Spanish",
	},
	languages_et:
	{
		de: "Estnisch",
		en: "Estonian",
	},
	languages_eu:
	{
		de: "Baskisch",
		en: "Basque",
	},
	languages_fa:
	{
		de: "Persisch",
		en: "Persian",
	},
	languages_fi:
	{
		de: "Finnisch",
		en: "Finnish",
	},
	languages_fr:
	{
		de: "Französisch",
		en: "French",
	},
	languages_frc:
	{
		de: "Kanadisches Französisch",
		en: "Canadian French",
	},
	languages_gd:
	{
		de: "Schottisch-Gälisch",
		en: "Scottish Gaelic",
	},
	languages_gl:
	{
		de: "Galician",
		en: "Galician",
	},
	languages_gsw:
	{
		de: "Schwyzerdütsch",
		en: "Swiss-German",
	},
	languages_hi:
	{
		de: "Hindi",
		en: "Hindi",
	},
	languages_hr:
	{
		de: "Kroatisch",
		en: "Croatian",
	},
	languages_hu:
	{
		de: "Ungarisch",
		en: "Hungarian",
	},
	languages_id:
	{
		de: "Indonesisch",
		en: "Indonesian",
	},
	languages_is:
	{
		de: "Isländisch",
		en: "Icelandic",
	},
	languages_it:
	{
		de: "Italienisch",
		en: "Italian",
	},
	languages_iw:
	{
		de: "Hebräisch",
		en: "Hebrew",
	},
	languages_ja:
	{
		de: "Japanisch",
		en: "Japanese",
	},
	languages_ka:
	{
		de: "Georgisch",
		en: "Georgian",
	},
	languages_kl:
	{
		de: "Grönländisch",
		en: "Greenlandic (Kalaallisut)",
	},
	languages_km:
	{
		de: "Kambodschanisch",
		en: "Cambodian",
	},
	languages_kn:
	{
		de: "Kannada",
		en: "Kannada",
	},
	languages_ko:
	{
		de: "Koreanisch",
		en: "Korean",
	},
	languages_ku:
	{
		de: "Kurdisch",
		en: "Kurdish",
	},
	languages_la:
	{
		de: "Latein",
		en: "Latin",
	},
	languages_lb:
	{
		de: "Luxemburgisch",
		en: "Luxembourgish",
	},
	languages_lo:
	{
		de: "Laotisch",
		en: "Lao",
	},
	languages_lt:
	{
		de: "Litauisch",
		en: "Lithuanian",
	},
	languages_lv:
	{
		de: "Lettisch",
		en: "Latvian",
	},
	languages_mk:
	{
		de: "Mazedonisch",
		en: "Macedonian",
	},
	languages_ml:
	{
		de: "Malayalam",
		en: "Malayalam",
	},
	languages_mr:
	{
		de: "Marathi",
		en: "Marathi",
	},
	languages_ms:
	{
		de: "Malaiisch",
		en: "Malay",
	},
	languages_mt:
	{
		de: "Maltesisch",
		en: "Maltese",
	},
	languages_nl:
	{
		de: "Niederländisch",
		en: "Dutch",
	},
	languages_no:
	{
		de: "Norwegisch",
		en: "Norwegian",
	},
	languages_oc:
	{
		de: "Okzitanisch",
		en: "Occitan",
	},
	languages_pl:
	{
		de: "Polnisch",
		en: "Polish",
	},
	languages_ps:
	{
		de: "Paschtunisch",
		en: "Pashto",
	},
	languages_pt:
	{
		de: "Portugiesisch",
		en: "Portuguese",
	},
	languages_ro:
	{
		de: "Rumänisch",
		en: "Romanian",
	},
	languages_roh:
	{
		de: "Rätoromanisch",
		en: "Romansch",
	},
	languages_ru:
	{
		de: "Russisch",
		en: "Russian",
	},
	languages_sgn:
	{
		de: "Gebärdensprache",
		en: "Sign language",
	},
	languages_sh:
	{
		de: "Serbo-Croatian",
		en: "Serbo-Croatian",
	},
	languages_sk:
	{
		de: "Slowakisch",
		en: "Slovak",
	},
	languages_sl:
	{
		de: "Slowenisch",
		en: "Slovenian",
	},
	languages_sq:
	{
		de: "Albanisch",
		en: "Albanian",
	},
	languages_sr:
	{
		de: "Serbisch",
		en: "Serbian",
	},
	languages_sv:
	{
		de: "Schwedisch",
		en: "Swedish",
	},
	languages_ta:
	{
		de: "Tamil",
		en: "Tamil",
	},
	languages_te:
	{
		de: "Telugu",
		en: "Telugu",
	},
	languages_th:
	{
		de: "Thailändisch",
		en: "Thai",
	},
	languages_tl:
	{
		de: "Tagalog",
		en: "Tagalog",
	},
	languages_tr:
	{
		de: "Türkisch",
		en: "Turkish",
	},
	languages_uk:
	{
		de: "Ukrainisch",
		en: "Ukrainian",
	},
	languages_us:
	{
		de: "US-Englisch",
		en: "US English",
	},
	languages_vi:
	{
		de: "Vietnamesisch",
		en: "Vietnamese",
	},
	languages_wel:
	{
		de: "Walisisch",
		en: "Welsh",
	},
	languages_wen:
	{
		de: "Sorbisch",
		en: "Sorbian",
	},
	languages_zgh:
	{
		de: "Tamazight",
		en: "Tamazight",
	},
	languages_zh:
	{
		de: "Chinesisch",
		en: "Chinese",
	},
	lastLogin:
	{
		de: "Letzter Login",
		en: "Last Login",
	},
	location:
	{
		de: "Ort",
		en: "Location",
	},
	latLong:
	{
		de: "Breitengrad, Längengrad",
		en: "Latitude, Longitude",
	},
	lookingFor:
	{
		de: "Ich suche",
		en: "Looking For",
	},
	lookingForOther:
	{
		de: "Sucht nach",
		en: "They're Looking For",
	},
	maxAge:
	{
		de: "Maximales Alter",
		en: "Maximal age",
	},
	messages:
	{
		de: "Nachrichten",
		en: "Messages",
	},
	metadata:
	{
		de: "Metadaten",
		en: "Metadata",
	},
	minAge:
	{
		de: "Minimales Alter",
		en: "Minimal age",
	},
	myAge:
	{
		de: "Mein Alter",
		en: "My Age",
	},
	myGender:
	{
		de: "Mein Geschlecht",
		en: "My gender",
	},
	myOrientation:
	{
		de: "Meine Orientierung",
		en: "My orientation",
	},
	new:
	{
		de: "Neu",
		en: "New",
	},
	noEntry:
	{
		de: "Keine Angabe",
		en: "No entry",
	},
	onlineStatus:
	{
		en: "Status",
	},
	onlineStatus_DATE:
	{
		en: "Date",
	},
	onlineStatus_OFFLINE:
	{
		en: "Offline",
	},
	onlineStatus_ONLINE:
	{
		en: "Online",
	},
	onlineStatus_SEX:
	{
		en: "Now",
	},
	travelersOnly:
	{
		de: "Nur Reisende",
		en: "Travelers only",
	},
	openTo:
	{
		de: "Offen für",
		en: "Open to",
	},
	openTo_FRIENDSHIP:
	{
		de: "Freunde",
		en: "Friends",
	},
	openTo_RELATIONSHIP:
	{
		de: "Beziehung",
		en: "Relationship",
	},
	openTo_SEXDATES:
	{
		en: "Sex",
	},
	orientation:
	{
		de: "Orientierung",
		en: "Orientation",
	},
	orientation_BISEXUAL:
	{
		de: "Bisexuell",
		en: "Bisexual",
	},
	orientation_GAY:
	{
		en: "Gay",
	},
	orientation_QUEER:
	{
		en: "Queer",
	},
	orientation_OTHER:
	{
		de: "Andere",
		en: "Other",
	},
	orientation_STRAIGHT:
	{
		de: "Hetero",
		en: "Straight",
	},
	other:
	{
		de: "Sonstige",
		en: "Other",
	},
	piercings:
	{
		en: "Piercings",
	},
	piercings_A_FEW:
	{
		de: "Wenige",
		en: "A few",
	},
	piercings_A_LOT:
	{
		de: "Viele",
		en: "A lot",
	},
	piercings_NO:
	{
		de: "Keine Piercings",
		en: "No piercings",
	},
	profileId:
	{
		de: "Profil-ID",
		en: "Profile ID",
	},
	relationship:
	{
		de: "Beziehung",
		en: "Relationship",
	},
	relationship_MARRIED:
	{
		de: "Verheiratet",
		en: "Married",
	},
	relationship_OPEN:
	{
		de: "Offene Partnerschaft",
		en: "Open",
	},
	relationship_PARTNER:
	{
		de: "Verpartnert",
		en: "Partner",
	},
	relationship_SINGLE:
	{
		en: "Single",
	},
	saferSex:
	{
		de: "Safer Sex",
		en: "Safer sex",
	},
	saferSex_ALWAYS:
	{
		en: "Safe",
	},
	saferSex_CONDOM:
	{
		de: "Kondom",
		en: "Condom",
	},
	saferSex_NEEDS_DISCUSSION:
	{
		de: "Nach Absprache",
		en: "Let's talk",
	},
	saferSex_PREP:
	{
		en: "PrEP",
	},
	saferSex_PREP_AND_CONDOM:
	{
		de: "PrEP und Kondom",
		en: "PrEP and condom",
	},
	saferSex_TASP:
	{
		en: "TasP",
	},
	sendEnter:
	{
		de: "Enter sendet Nachricht",
		en: "Enter sends message",
	},
	sendEnterDesc:
	{
		de: "Wenn deaktiviert erzeugt Enter einen Absatz und Strg+Enter sendet die Nachricht.",
		en: "If disabled, Enter creates a new line instead and Ctrl+Enter sends the message.",
	},
	sexual:
	{
		de: "Sexuelles",
		en: "Sexual",
	},
	sm:
	{
		de: "SM",
		en: "S&M",
	},
	sm_NO:
	{
		de: "Kein SM",
		en: "No SM",
	},
	sm_SOFT:
	{
		en: "Soft SM",
	},
	sm_YES:
	{
		en: "SM",
	},
	smoker:
	{
		de: "Raucher",
		en: "Smoker",
	},
	smoker_NO:
	{
		de: "Nein",
		en: "No",
	},
	smoker_SOCIALLY:
	{
		de: "Selten",
		en: "Socially",
	},
	smoker_YES:
	{
		de: "Ja",
		en: "Yes",
	},
	socialSmoker:
	{
		de: "Raucht selten",
		en: "Social Smoker",
	},
	speakingMyLanguage:
	{
		de: "Spricht meine Sprache",
		en: "Speaking my language",
	},
	systemMessages:
	{
		de: "Systemnachrichten",
		en: "System messages",
	},
	systemMessagesDesc:
	{
		de: "Erlaubt Popups wie Standort- oder Fehlermeldungen.",
		en: "Allows popups like GPS or error messages.",
	},
	tattoos:
	{
		en: "Tattoos",
	},
	tattoos_A_FEW:
	{
		de: "Wenige",
		en: "A few",
	},
	tattoos_A_LOT:
	{
		de: "Viele",
		en: "A lot",
	},
	tattoos_NO:
	{
		de: "Keine Tattoos",
		en: "No tattoos",
	},
	tiles:
	{
		de: "Benutzerkacheln",
		en: "User Tiles",
	},
	tileCount:
	{
		de: "Kachelspalten (0 für Standard)",
		en: "Tile columns (0 for default)",
	},
	tileDetails:
	{
		de: "Benutzerkachel-Details",
		en: "User Tile Details",
	},
	typingNotifications:
	{
		de: "Tippbenachrichtigungen",
		en: "Typing notifications",
	},
	typingNotificationsDesc:
	{
		de: "Ob Empfänger die Eingabe einer Nachricht sehen können.",
		en: "Whether receivers can see that a message is being written.",
	},
	viewFullImage:
	{
		de: "Bild anzeigen",
		en: "Preview image",
	},
	viewProfile:
	{
		de: "Profilvorschau anzeigen",
		en: "Preview profile",
	},
	weight:
	{
		de: "Gewicht",
		en: "Weight",
	}
};

function translate(key)
{
	const translations = strings[key];
	return translations
		? translations[getLang()] || translations.en || "%" + key + "%"
		: "%" + key + "%";
}

function translateEnum(name, key)
{
	return translate(`${name}_${key}`);
}

// ---- Settings ----

const settingsNs = "RA_SETTINGS:";
let measurementSystem = "METRIC";
let radarFilter = {};
let tileDetails = new Set();
let tileStyle = null;

function load(name, fallback)
{
	const value = localStorage.getItem(settingsNs + name);
	return value === "false" ? false : value ? value : fallback;
}
function save(name, value)
{
	localStorage.setItem(settingsNs + name, value);
}

function getEnhancedFilter()
{
	return load("enhancedFilter", true);
}
function getEnhancedTiles()
{
	return load("enhancedTiles", true);
}
function getFullHeadlines()
{
	return load("fullHeadlines", true);
}
function getFullMessages()
{
	return load("fullMessages", true);
}
function getHiddenMaxAge()
{
	return load("hiddenMaxAge", 99);
}
function getHiddenMinAge()
{
	return load("hiddenMinAge", 18);
}
function getHiddenUsers()
{
	return new Set(JSON.parse(load("hiddenUsers", `[]`)));
}
function getHideActivities()
{
	return load("hideActivities", true);
}
function getHideContacts()
{
	return load("hideContacts", false);
}
function getHideLikes()
{
	return load("hideLikes", true);
}
function getHideMessages()
{
	return load("hideMessages", false);
}
function getHideVisits()
{
	return load("hideVisits", true);
}
function getRadarFilter()
{
	return JSON.parse(load("radarFilter", `{}`));
}
function getSavedRadarFilter(id)
{
	return getSavedRadarFilters()[id] ?? getRadarFilter();
}
function getSavedRadarFilters()
{
	return JSON.parse(load("savedRadarFilters", "{}"));
}
function getSendEnter()
{
	return load("sendEnter", true);
}
function getSystemMessages()
{
	return load("systemMessages", true);
}
function getTileCount()
{
	return parseInt(load("tileCount", 0));
}
function getTileDetails()
{
	return new Set(JSON.parse(load("tileDetails", `[ "age", "height", "bodyHair", "bodyType", "relationship", "analPosition" ]`)));
}
function getTypingNotifications()
{
	return load("typingNotifications", true);
}
function setEnhancedFilter(value)
{
	save("enhancedFilter", value);
}
function setEnhancedTiles(value)
{
	save("enhancedTiles", value);
}
function setFullHeadlines(value)
{
	setCssProp("--tile-headline-white-space", value ? "unset" : "nowrap");
	save("fullHeadlines", value);
}
function setFullMessages(value)
{
	setCssProp("--message-line-clamp", value ? "unset" : "2");
	save("fullMessages", value);
}
function setHiddenMaxAge(value)
{
	save("hiddenMaxAge", value);
}
function setHiddenMinAge(value)
{
	save("hiddenMinAge", value);
}
function setHideActivities(value)
{
	save("hideActivities", value);
}
function setHideContacts(value)
{
	save("hideContacts", value);
}
function setHideLikes(value)
{
	save("hideLikes", value);
}
function setHideMessages(value)
{
	save("hideMessages", value);
}
function setHideVisits(value)
{
	save("hideVisits", value);
}
function setRadarFilter()
{
	save("radarFilter", JSON.stringify(radarFilter));
}
function setSavedRadarFilter(id, value = null)
{
	const filters = JSON.parse(load("savedRadarFilters", "{}"));
	if (value)
		filters[id] = value;
	else
		delete filters[id];
	save("savedRadarFilters", JSON.stringify(filters));
}
function setSendEnter(value)
{
	save("sendEnter", value);
}
function setSystemMessages(value)
{
	setCssProp("--system-message-visibility", value ? "visible" : "collapse");
	save("systemMessages", value);
}
function setTileCount(value)
{
	if (value)
	{
		setCssProp("--tile-count", value);
		if (!tileStyle)
		{
			tileStyle = addCss(`
			:root
			{
				--tile-count: 0;
				--tile-size: calc(100% / max(1, var(--tile-count)) - 1px);
			}
			/* discover */
			section.js-main-stage > main main > section > ul
			{
				grid-template-columns: repeat(var(--tile-count), 1fr) !important;
			}
			/* radar desktop */
			.search-results__item
			{
				padding-bottom: var(--tile-size) !important;
				width: var(--tile-size) !important;
			}
			/* radar mobile - starts at 768px where .search-results__item turns inline, requiring to adjust .tile */
			@media not screen and (min-width: 768px)
			{
				.tile:not(.js-strip .tile):not(.tile--small)
				{
					width: var(--tile-size) !important;
				}
			}
			/* visitors */
			#cruise main > ul
			{
				grid-template-columns: repeat(var(--tile-count), 1fr);
			}
			`);
		}
	}
	else
	{
		if (tileStyle)
		{
			tileStyle.remove();
			tileStyle = null;
		}
	}
	save("tileCount", value);
}
function setTileDetail(key, visible)
{
	if (visible)
		tileDetails.add(key);
	else
		tileDetails.delete(key);
	save("tileDetails", JSON.stringify(Array.from(tileDetails)));
}
function setTypingNotifications(value)
{
	save("typingNotifications", value);
}
function setUserHidden(username, hide)
{
	let hiddenUsers = getHiddenUsers();
	if (hide)
		hiddenUsers.add(username);
	else
		hiddenUsers.delete(username);
	save("hiddenUsers", JSON.stringify(Array.from(hiddenUsers)));
}

// ---- Fetch/XHR ----

const apiKey = atob("QVM4YnpHSExBOFk5QlhGNzNpRE51UUJIZUVPMFVLamY=");
let sessionId;

function addHook(hooks, func, method, route, callback)
{
	if (!hooks[func])
		hooks[func] = {};
	if (!hooks[func][method])
		hooks[func][method] = {};
	hooks[func][method][route] = callback;
}

function callHook(hooks, func, e)
{
	// Log XHR.
	if (func !== "open")
	{
		const prefix = func === "load" || func === "recv" ? e.status : "<<<";
		if (e.body)
			log(`${prefix} ${e.method} ${e.url}`, e.body);
		else
			log(`${prefix} ${e.method} ${e.url}`);
	}

	// Only handle success for now.
	if (e.status && (e.status < 200 || e.status > 299))
		return false;

	// Extract route.
	const routeStart = e.url.indexOf("/api/");
	if (routeStart === -1)
		return false;
	const routeEnd = e.url.indexOf("?");
	const route = e.url.substring(routeStart + "/api/".length, routeEnd === -1 ? undefined : routeEnd);

	// Route to matching hook.
	if (hooks[func] && hooks[func][e.method])
	{
		for (const [hookRoute, callback] of Object.entries(hooks[func][e.method]))
		{
			e.args = matchRoute(route, hookRoute);
			if (e.args !== undefined)
			{
				log(`🪝 ${func} ${e.method} ${route}`);
				callback(e);
				return true;
			}
		}
	}
	return false;
}

function matchRoute(route, match)
{
	if (route === match)
		return [];
	const routeParts = route.split("/");
	const matchParts = match.split("/");
	if (routeParts.length !== matchParts.length)
	{
		return;
	}
	const args = [];
	for (let i = 0; i < routeParts.length; ++i)
	{
		if (matchParts[i] === "*")
			args.push(routeParts[i]);
		else if (routeParts[i] !== matchParts[i])
			return;
	}
	return args;
}

// Fetch

const fetchHooks = {};

function initFetch()
{
	const realFetch = window.fetch;
	window.fetch = async (resource, options) =>
	{
		// Hook send.
		const e = options
			? { method: options.method, url: resource }
			: { method: resource.method, url: resource.url };
		if (callHook(fetchHooks, "send", e) && e.cancel)
		{
			return;
		}
		resource.method = e.method;
		resource.url = e.url;

		// Hook receive.
		const response = await realFetch(resource, options);
		try
		{
			e.body = await response.clone().json();
		}
		catch
		{
			return response; // not JSON or empty reply
		}
		e.cancel = false;
		e.status = response.status;
		if (callHook(fetchHooks, "recv", e) && e.cancel)
		{
			e.body = null;
			e.status = 404;
		}
		return new Response(JSON.stringify(e.body), { headers: response.headers, status: e.status });
	};
}

function onFetch(func, method, route, callback)
{
	addHook(fetchHooks, func, method, route, callback);
}

function doFetch(method, url, body, referrer)
{
	const options =
	{
		method: method,
		cache: "no-cache", // sets cache-control
		credentials: "same-origin", // apparently sets cookie, otherwise try "include"
		headers: {
			"x-api-key": apiKey,
			"x-session-id": sessionId,
			"x-site": "planetromeo"
		}
	};
	if (body)
	{
		options.headers["content-type"] = "application/json";
		options.body = JSON.stringify(body);
	}
	if (referrer)
	{
		options.referrer = referrer;
	}
	return fetch(url, options);
}

// XHR

const xhrHooks = {};
const xhrOpened = {};

function initXhr()
{
	// Hook open.
	const realOpen = window.XMLHttpRequest.prototype.open;
	window.XMLHttpRequest.prototype.open = function (method, url, async, user, password)
	{
		const e = { method: method, url: url };
		xhrOpened[this] = e;

		if (callHook(xhrHooks, "open", e))
		{
			method = e.method;
			url = e.url;
		}
		if (!e.cancel)
			realOpen.apply(this, arguments);

		// Hook load.
		this.addEventListener("load", () =>
		{
			const json = isJson(this.response);
			e.body = json ? JSON.parse(this.response) : this.response;
			e.status = this.status;

			if (callHook(xhrHooks, "load", e))
			{
				Object.defineProperty(this, "responseText", { writable: true });
				this.responseText = json ? JSON.stringify(e.body) : e.body;
			}
			if (e.cancel)
			{
				this.response = null;
				this.responseText = null;
				this.status = 404;
			}
		});
	};

	// Hook send.
	const realSend = window.XMLHttpRequest.prototype.send;
	window.XMLHttpRequest.prototype.send = function (body)
	{
		const e = xhrOpened[this];
		delete xhrOpened[this];
		const json = isJson(body);
		if (body)
			e.body = json ? JSON.parse(body) : body;

		if (callHook(xhrHooks, "send", e) && e.body)
			body = json ? JSON.stringify(e.body) : body;
		if (!e.cancel)
			realSend.apply(this, arguments);
	};
}

function onXhr(func, method, route, callback)
{
	addHook(xhrHooks, func, method, route, callback);
}

// ---- Lists ----

addCss(`
.ra_list
{
	display: flex;
	flex-direction: column;
	height: 250px;
}

.ra_list > div > a
{
	margin: 0 8px;

	display: inline-flex;
}

.ra_list > ul
{
	flex: 1;
	padding: 2px;
	overflow-y: auto;

	display: flex;
	flex-wrap: wrap;
	align-content: flex-start;
}

.ra_list > ul > li
{
	flex: 50%;
	flex-grow: 0;
	padding: 2px;
}
`);

function createList(parent, { onGet, onName = null, onAdd = null, onRemove = null, onImport = null, onExport = null } = {})
{
	const container = addElement(parent, `<div class="ra_list"></div>`);

	function createButton(icon, text)
	{
		return `
			<a href="#" class="icon-labeled plain-text-link">
				<span class="icon icon-base ${icon}"></span>
				<span class="icon-labeled__label">${text}</span>
			</a>`;
	}

	// Add elements.
	const toolbar = addElement(container, `<div></div>`);
	const ul = addElement(container, `<ul></ul>`);

	// Create toolbar.
	const searchBox = addElement(toolbar, `<input class="input" type="text" placeholder="Search"></input>`);
	searchBox.addEventListener("input", e => updateList());

	function updateList()
	{
		const filter = searchBox.value.toUpperCase();
		ul.replaceChildren();

		const elements = onGet();
		for (const element of elements)
		{
			// Check if filtered away.
			const name = onName ? onName(element) : element;
			if (!name.toUpperCase().includes(filter))
				continue;

			// Create list entry.
			const li = addElement(ul, `<li></li>`);
			if (onRemove)
			{
				const deleteButton = addElement(li, createButton("icon-cross-negative", name));
				deleteButton.addEventListener("click", e =>
				{
					e.preventDefault();
					onRemove(element);
					li.remove();
				});
			}
			else
			{
				addElement(li, `<div>${name}</div>`);
			}
		}
	}

	if (onAdd)
	{
		const addButton = addElement(toolbar, createButton("icon-add-attachment", "Add"));
		addButton.addEventListener("click", e =>
		{
			e.preventDefault();
			onAdd(searchBox.value);
			updateList();
		});
	}

	if (onRemove)
	{
		const clearButton = addElement(toolbar, createButton("icon-trashcan", "Clear"));
		clearButton.addEventListener("click", e =>
		{
			e.preventDefault();
			if (confirm(translate("clearList")))
			{
				const elements = onGet();
				for (const element of elements)
					onRemove(element);
				updateList();
			}
		});
	}

	if (onImport)
	{
		const importButton = addElement(toolbar, createButton("icon-up-arrow", "Import"));
		importButton.addEventListener("click", e =>
		{
			e.preventDefault();
			// TODO: Handle import
			updateList();
		});
	}

	if (onExport)
	{
		const exportButton = addElement(toolbar, createButton("icon-down-arrow", "Export"));
		exportButton.addEventListener("click", e =>
		{
			e.preventDefault();
			// TODO: Handle export
			updateList();
		});
	}

	// Create list.
	updateList();
}

// ---- Romeo ----

addCss(`
:root
{
	--message-line-clamp: 2;
	--system-message-visibility: visible;
	--tile-headline-white-space: nowrap;
}

.feedback
{
	visibility: var(--system-message-visibility);
}

/* hide models on login page */
div[data-testid="desktop-image"]
{
	background-image: none;
}

/* hide PLUS message at bottom of visitor grid */
main#visitors > section
{
	display: none;
}

/* prevent mobile photo view on non-mobile displays */
@media screen and (min-width: 768px)
{
	.ReactModal__Overlay.ReactModal__Overlay--after-open > .ReactModal__Content.ReactModal__Content--after-open > main
	{
		background: none;
	}
	.container-button-next, .container-button-prev
	{
		display: flex;
	}
	.swiper-zoom-container > img
	{
		height: initial;
	}
}
`);

async function blockUser(username)
{
	if (!username)
		return false;

	const profileId = profileCache[username].id;
	if (!profileId)
		return false;

	const response = await doFetch("POST", "/api/v4/profiles/blocked", { profile_id: profileId, note: "" },
		`https://www.romeo.com/profile/${username}/report`);
	return response.ok;
}

function getBackgroundImageUrl(style)
{
	return style.match(/"(.+)"/)[1];
}

function getFullImageUrl(url)
{
	return url.replace(/squarish\/[0-9]*x[0-9]*\//, "");
}

function getUsernameFromHref(href)
{
	let start = href.indexOf("profile/");
	if (start === -1)
		start = href.indexOf("hunq/");
	return href.substring(start).split("/")[1];
}

function xhrHandleSession(reply)
{
	// Determine session ID.
	sessionId = reply.session_id;

	// Apply settings.
	const settings = reply.bb_settings;
	if (settings)
	{
		// Determine measurement locale.
		measurementSystem = settings.interface?.measurement_system ?? measurementSystem;

		// Determine radar filter, remove deleted ones.
		const radarFilterId = settings.bluebird?.search_filter?.id;
		radarFilter = getSavedRadarFilter(radarFilterId);
		for (const savedFilterId of Object.keys(getSavedRadarFilters()))
			if (savedFilterId && !reply.data.search_filters.find(x => x.id === savedFilterId))
				setSavedRadarFilter(savedFilterId);
	}

	// Enable client-side PLUS capabilities.
	const caps = reply.data?.capabilities;
	if (caps)
	{
		caps.can_save_unlimited_searches = true; // enables filter bookmarks
		caps.can_set_plus_radar_style = true; // enables Grid Stats selection
	}
}

onXhr("load", "GET", "v4/session", e => xhrHandleSession(e.body));

// ---- Menu ----

addCss(`
#ra_context_bg
{
	background: transparent;
	display: none;
	height: 100%;
	position: fixed;
	z-index: 10000;
	width: 100%;
}

#ra_context_ul
{
	background: #121212;
	border: 1px solid #000;
	display: none;
	font-size: 0.8rem;
	position: absolute;
	z-index: 10001;
}

.ra_context_li
{
	color: #FFF;
	cursor: default;
	padding: 5px 15px 5px 0px;
	white-space: nowrap;
}

.ra_context_li .icon
{
	color: #00BDFF;
	margin: 0px 10px 0px 13px;
}

.ra_context_li:hover
{
	background-color: hsla(0,0%,100%,.125);
}

@media screen and (max-width: 767px)
{
	#ra_context_bg
	{
		background: #0000007F;
	}
	#ra_context_ul
	{
		bottom: 0;
		left: unset !important;
		font-size: 1.2rem;
		top: unset !important;
		position: fixed;
		width: 100%;
	}
	.ra_context_li
	{
		padding: 20px;
	}
}
`);

let menuBg;
let menuUl;
let menuX;
let menuY;
const menuHandlers = {};

function initMenu()
{
	// Create context menu canceler.
	menuBg = addElement(document.body, "<div id='ra_context_bg'></ul>");
	menuBg.addEventListener("click", e => hideMenu());

	// Create context menu.
	menuUl = addElement(document.body, "<ul id='ra_context_ul'></ul>");

	// Attach to events.
	addEventListener("contextmenu", e =>
	{
		menuX = e.clientX;
		menuY = e.clientY;

		// Go through hierarchy of clicked elements.
		for (const el of document.elementsFromPoint(menuX, menuY))
		{
			// Stop when hitting a layer.
			if (el.classList.contains("layer")
				|| el.classList.contains("layout")
				|| el.classList.contains("ReactModal__Overlay"))
				break;
			// Invoke first context handler for this element.
			for (const [key, value] of Object.entries(menuHandlers))
			{
				if (el.matches(key))
				{
					log(`opening menu '${key}'`);
					value(el);
					e.preventDefault();
					return;
				}
			}
		}
	});
}

function showMenu()
{
	menuBg.style.display = "block";

	menuUl.replaceChildren();
	for (const item of arguments)
	{
		const li = addElement(menuUl, `
			<li class="ra_context_li">
				<span class="icon icon-${item.icon}"></span>
				${translate(item.text)}
			</li>`);
		li.addEventListener("click", e =>
		{
			hideMenu();
			item.onclick();
		});
	}

	menuUl.style.display = "block";
	const maxX = window.innerWidth - menuUl.offsetWidth;
	const maxY = window.innerHeight - menuUl.offsetHeight;
	menuUl.style.left = Math.min(menuX, maxX) + "px";
	menuUl.style.top = Math.min(menuY, maxY) + "px";
}

function hideMenu()
{
	menuBg.style.display = "none";
	menuUl.style.display = "none";
}

function onMenu(selector, handler)
{
	menuHandlers[selector] = handler;
}

function menuItem(icon, text, onclick)
{
	return { icon, text, onclick };
}

// ---- Previews ----

addCss(`
#ra_preview_inner
{
	background-color: black;
	display: grid;
	grid-template-rows: min-content auto;
	height: 100%;
}

#ra_image_content
{
	overflow-y: scroll;
	padding: 16px;
}

#ra_profile_content
{
	display: grid;
	font-family: Inter, Helvetica, Arial, "Open Sans", sans-serif;
	grid-template-columns: auto 352px;
	overflow-y: scroll;
	word-break: break-word;
}

#ra_profile_left
{
	background: #121212;
	overflow-y: scroll;
	padding: 16px;
}

#ra_profile_right
{
	overflow-y: scroll;
	padding: 16px;
}

.ra_profile_details:not(:first-child)
{
	border-top: 1px solid rgb(46, 46, 46);
	margin-top: 1rem;
}

.ra_profile_summary
{
	padding: 1rem 0;
}

.ra_profile_keyvalue
{
	display: grid;
	gap: 16px;
	grid-template-columns: minmax(0, 0.8fr) minmax(0, 1fr);
}

.ra_profile_keyvalue > :first-child
{
	color: rgba(255, 255, 255, 0.6);
	text-align: right;
}

#ra_profile_text
{
	white-space: pre-line;
}

@media screen and (max-width: 767px)
{
	#ra_profile_content
	{
		grid-template-columns: initial;
		grid-template-rows: auto auto;
	}
	#ra_profile_left
	{
		overflow-y: initial;
	}
	#ra_profile_right
	{
		overflow-y: initial;
	}
	#ra_profile_pic
	{
		width: 100%;
	}
}
`);

let previewLayer;

function createPreview(title)
{
	const container = document.querySelector("#spotlight-container");
	previewLayer = addElement(container, `
		<div class="layer layer--spotlight" style="top:0;z-index:100;">
			<div id="ra_preview_inner">
				<div class="js-header layout-item">
					<div class="layer-header layer-header--primary">
						<a class="back-button l-tappable js-back marionette" href="#">
							<span class="js-back-icon icon icon-cross icon-regular"></span>
						</a>
						<div class="layer-header__title js-title typo-section-navigation" style="text-align:center">
							<h2>${title}</h2>
						</div>
					</div>
				</div>
			</div>
		</div>`);

	previewLayer.addEventListener("click", e =>
	{
		if (e.target === previewLayer)
			previewLayer.remove();
	});
	previewLayer.querySelector(".js-back").addEventListener("click", e => previewLayer.remove());

	return previewLayer.querySelector("#ra_preview_inner");
}

function initPreviews()
{
	window.addEventListener("popstate", e =>
	{
		// Restore navigating back to preview.
		switch (e.state?.ra_preview)
		{
			case "image":
				showImagePreview(e.state.src, false);
				break;
			case "profile":
				showProfilePreview(e.state.username, false);
				break;
		}
	});
	window.navigation?.addEventListener("navigate", e =>
	{
		// Hide preview on any other navigation.
		previewLayer?.remove();
	});
}

function showImagePreview(src, pushHistory = true)
{
	if (pushHistory)
		history.pushState({ ra_preview: "image", src: src }, "");

	const content = addElement(createPreview(translate("viewFullImage")), `<div id="ra_image_content"></div>`);
	addElement(content, `<img id="ra_profile_pic" src="${src}"></img>`);
}

function showProfilePreview(username, pushHistory = true)
{
	function isEntry(value)
	{
		return value && value !== "NO_ENTRY";
	}

	function addSection(el, key)
	{
		return addElement(el, `
			<details class="ra_profile_details" open>
				<summary class="ra_profile_summary">${translate(key)}</summary>
			</details>`);
	}

	function add(section, key, value)
	{
		if (value)
		{
			addElement(section, `
				<div class="ra_profile_keyvalue">
					<div>${translate(key)}</div>
					<div>${value}</div>
				</div>`);
		}
	}
	function addAgeRange(section, range)
	{
		if (range)
			add(section, "ageRange", getProfileAgeRange(range));
	}
	function addArrayEnum(section, key, array)
	{
		if (!array)
			return;
		let values = [];
		for (let i = 0; i < array.length; i++)
			if (isEntry(array[i]))
				values.push(translateEnum(key, array[i]));
		if (values.length)
			add(section, key, values.join(", "));
	}
	function addDistance(section, distance, sensor)
	{
		let text = measurementSystem === "METRIC"
			? `${distance / 1000} km`
			: `${round(distance * M2MI, 1)}mi`;
		if (sensor)
			text += " (GPS)";
		add(section, "distance", text);
	}
	function addEnum(section, key, value)
	{
		if (isEntry(value))
			add(section, key, translateEnum(key, value));
	}
	function addGender(section, genderOrientation)
	{
		let values = [];
		if (isEntry(genderOrientation?.orientation))
			values.push(translateEnum("orientation", genderOrientation.orientation));
		if (isEntry(genderOrientation?.gender))
			values.push(translateEnum("gender", genderOrientation.gender));
		if (values.length)
			add(section, "genderOrientation", values.join(" / "));
	}

	const profile = profileCache[username];
	if (!profile)
		return;
	const personal = profile.personal;
	const sexual = profile.sexual;

	if (pushHistory)
		history.pushState({ ra_preview: "profile", username: username }, "");

	const content = addElement(createPreview(username), `<div id="ra_profile_content"></div>`);
	const left = addElement(content, `<div id="ra_profile_left"></div>`);
	const right = addElement(content, `<div id="ra_profile_right"></div>`);

	addElement(left, `<div>${escapeHtml(profile.headline ?? "")}</div>`);

	const img = addElement(left, `<img id="ra_profile_pic"></img>`);
	img.src = profile.pic ? `/img/usr/${profile.pic}.jpg` : "/assets/f8a7712027544ed03920.svg";

	const section = addSection(right, "metadata");
	addEnum(section, "onlineStatus", profile.online_status);
	add(section, "lastLogin", formatTime(profile.last_login));
	add(section, "location", `${profile.location.name}, ${profile.location.country}`);
	addDistance(section, profile.location.distance, profile.location.sensor);
	add(section, "profileId", profile.id);

	if (personal)
	{
		const section = addSection(right, "lookingFor");
		addArrayEnum(section, "openTo", personal.looking_for);
		addAgeRange(section, personal.target_age);
		addArrayEnum(section, "gender", personal.gender_orientation?.looking_for_gender);
		addArrayEnum(section, "orientation", personal.gender_orientation?.looking_for_orientation);
		if (!section.querySelectorAll(".ra_profile_keyvalue").length)
			section.remove();
	}

	if (personal)
	{
		const section = addSection(right, "general");
		add(section, "age", personal.age);
		add(section, "height", getProfileHeight(personal.height));
		add(section, "weight", getProfileWeight(personal.weight));
		add(section, "bmi", getProfileBmi(personal.height, personal.weight, true));
		add(section, "bodyType", getProfileEnum("bodyType", personal.body_type));
		add(section, "ethnicity", getProfileEnum("ethnicity", personal.ethnicity));
		addEnum(section, "hairLength", personal.hair_length);
		addEnum(section, "hairColor", personal.hair_color);
		addEnum(section, "beard", personal.beard);
		addEnum(section, "eyeColor", personal.eye_color);
		add(section, "bodyHair", getProfileEnum("bodyHair", personal.body_hair));
		addGender(section, personal?.gender_orientation);
		addEnum(section, "smoker", personal.smoker);
		addEnum(section, "tattoos", personal.tattoo);
		addEnum(section, "piercings", personal.piercing);
		addArrayEnum(section, "languages", personal.spoken_languages);
		add(section, "relationship", getProfileEnum("relationship", personal.relationship));
	}

	if (sexual)
	{
		const section = addSection(right, "sexual");
		add(section, "analPosition", getProfileEnum("analPosition", sexual.anal_position));
		add(section, "dick", getProfileDick(sexual.dick_size, sexual.concision));
		addArrayEnum(section, "fetish", sexual.fetish);
		add(section, "dirty", getProfileEnum("dirty", sexual.dirty_sex));
		addEnum(section, "fisting", sexual.fisting);
		addEnum(section, "sm", sexual.sm);
		add(section, "saferSex", getProfileEnum("saferSex", sexual.safer_sex));
		if (!section.querySelectorAll(".ra_profile_keyvalue").length)
			section.remove();
	}

	if (profile.personal?.profile_text)
	{
		const section = addSection(right, "aboutMe");
		addElement(section, `<div id="ra_profile_text">${profile.personal.profile_text}</div>`);
	}
}

// ---- Profiles ----

const profileCache = {};

function cacheProfile(profileObject)
{
	const existing = profileCache[profileObject.name];
	const profile =
	{
		id: profileObject.id,
		name: profileObject.name,
		headline: profileObject.headline ?? existing?.headline,
		last_login: profileObject.last_login ?? existing?.last_login,
		location: profileObject.location ?? existing?.location,
		online_status: profileObject.online_status ?? existing?.online_status,
		pic: profileObject.preview_pic?.url_token ?? existing?.pic, // not available if no picture
		personal: profileObject.profile?.personal ?? profileObject.personal ?? existing?.personal, // not available in activities
		sexual: profileObject.profile?.sexual ?? profileObject.sexual ?? existing?.sexual, // not available in activities
		albums: profileObject.albumsV2 ?? existing?.albumsV2
	};
	profileCache[profile.name] = profile;
	return profile;
}

function filterProfile(profile, hiddenMaxAge, hiddenMinAge, hiddenNames)
{
	// Return whether to display the profile.
	return (!profile.personal || profile.personal.age >= hiddenMinAge && profile.personal.age <= hiddenMaxAge)
		&& !hiddenNames.has(profile.name);
}

function filterItemsAndCacheProfiles(items, profileSelector, filter)
{
	let newItems = [];
	const hiddenMaxAge = getHiddenMaxAge();
	const hiddenMinAge = getHiddenMinAge();
	const hiddenNames = getHiddenUsers();

	for (const item of items ?? [])
	{
		const profile = cacheProfile(profileSelector(item));
		if (!filter || filterProfile(profile, hiddenMaxAge, hiddenMinAge, hiddenNames))
			newItems.push(item);
	}
	return newItems;
}

function getProfileAgeRange(range, short)
{
	if (range)
	{
		const min = range.min ?? "18";
		const max = range.max ?? "99";
		return short
			? `${min}-${max}`
			: translate("ageRangeValue").replace("$from", min).replace("$to", max);
	}
}
function getProfileBmi(height, weight, withName)
{
	if (height && weight)
	{
		const bmi = weight / Math.pow(height / 100, 2);
		let result = `${round(bmi, 1).toFixed(1)}`;

		if (withName)
		{
			for (const [max, key] of Object.entries({
				16: "bmiSevereThin",
				17: "bmiModerateThin",
				18.5: "bmiMildThin",
				25: "bmiNormal",
				30: "bmiPreObese",
				35: "bmiObese1",
				40: "bmiObese2",
				99: "bmiObese3",
			}))
			{
				if (bmi < max)
					return result + ` / ${translate(key)}`;
			}
		}
		return result;
	}
}
function getProfileDick(size, concision)
{
	let values = [];
	if (size && size !== "NO_ENTRY")
		values.push(translateEnum("dick", size));
	if (concision && concision !== "NO_ENTRY")
		values.push(translateEnum("concision", concision));
	if (values.length)
		return values.join(" - ");
}
function getProfileEnum(key, value)
{
	if (value && value !== "NO_ENTRY")
		return translateEnum(key, value);
}

function getProfileHeight(height)
{
	if (height)
	{
		return measurementSystem === "METRIC"
			? `${height}cm`
			: `${round(height * CM2FT, 2)} ft`;
	}
}
function getProfileWeight(weight)
{
	if (weight)
	{
		return measurementSystem === "METRIC"
			? `${weight}kg`
			: `${round(weight * KG2LBS)}lbs`;
	}
}

function fetchHandleReactionsCruiseLikes(e)
{
	e.body.items = filterItemsAndCacheProfiles(e.body.items, x => x.profile, getHideLikes());
}

function xhrHandleProfiles(e)
{
	e.body.items = filterItemsAndCacheProfiles(e.body.items, x => x, true);
	e.body.items_limited = e.body.items_total; // Remove PLUS ad tile.

	// Show every user as a large tile.
	if (getEnhancedTiles())
		for (const item of e.body.items ?? [])
			if (item.display)
				item.display.large_tile = true;
}
function xhrHandleVisits(e)
{
	e.body.items = filterItemsAndCacheProfiles(e.body.items, x => x, getHideVisits());
	e.body.items_limited = e.body.items_total; // Restore PLUS-visible visitors.
}

onXhr("load", "GET", "v4/contacts", e =>
{
	if (e.body.cursors)
		e.body.items = filterItemsAndCacheProfiles(e.body.items, x => x.profile, getHideContacts());
});

onXhr("load", "GET", "v4/messages/conversations", e =>
{
	e.body.items = filterItemsAndCacheProfiles(e.body.items, x => x.chat_partner, getHideMessages());
});

onXhr("load", "GET", "+/notifications/activity-stream", e =>
{
	e.body = filterItemsAndCacheProfiles(e.body, x => x.partner, getHideActivities());
});

onXhr("load", "GET", "v4/hunqz/profiles", e => xhrHandleProfiles(e));
onXhr("load", "GET", "v4/profiles", e => xhrHandleProfiles(e));
onXhr("load", "GET", "v4/profiles/list", e => xhrHandleProfiles(e));
onXhr("load", "GET", "v4/profiles/popular", e => xhrHandleProfiles(e));
onFetch("recv", "GET", "v4/profiles", e => xhrHandleProfiles(e));
onFetch("recv", "GET", "v4/profiles/popular", e => xhrHandleProfiles(e));

onFetch("recv", "GET", "v4/visitors", e => xhrHandleVisits(e));
onFetch("recv", "GET", "v4/visits", e => xhrHandleVisits(e));
onFetch("recv", "GET", "v4/reactions/cruise/likes", e => fetchHandleReactionsCruiseLikes(e));

onXhr("load", "GET", "v4/messages/*", e => cacheProfile(e.body));
onXhr("load", "GET", "v4/profiles/*", e => cacheProfile(e.body));
onXhr("load", "GET", "v4/profiles/*/full", e => cacheProfile(e.body));

onXhr("load", "GET", "v4/reactions/pictures/basic", e =>
{
	e.body.items = filterItemsAndCacheProfiles(e.body.items, x => x.user_id, getHideLikes());
});

// ---- Filter ----

addCss(`
/* enhanced radar filter */
.js-quick-filter
{
	overflow-y: scroll;
}

/* restore bookmark icon color */
.ui-navbar__button--bookmarks .icon.icon-bookmark-outlined
{
	color: #00bdff !important;
}

/* fix height of filter on mobile */
.sidebar .filter-container
{
	height: unset !important;
}
`);

onDom(`.js-quick-filter`, el =>
{
	replaceFilterContainer(el.parentNode);
});

function addRadarFilter(filter, key, value)
{
	if (!isMultiRadarFilter(key))
		filter[key] = value;
	else if (key in filter)
		filter[key].push(value);
	else
		filter[key] = [value];
}

function hasRadarFilter(filter, key, value)
{
	return key in filter
		&& (value === undefined || (isMultiRadarFilter(key)
			? filter[key].includes(value)
			: filter[key] === value));
}

function isMultiRadarFilter(key)
{
	return key.endsWith("[]");
}

function removeRadarFilter(filter, key, value)
{
	if (!hasRadarFilter(filter, key, value))
		return;
	if (isMultiRadarFilter(key))
	{
		filter[key] = filter[key].filter(x => x !== value);
		if (!filter[key].length)
			delete filter[key];
	}
	else
	{
		delete filter[key];
	}
}

function refreshFilter()
{
	// Save filter.
	setRadarFilter();
	// Reset filter title, enable filter reset button.
	document.querySelector(`.js-filter-header p[class^="ResponsiveBodyText-sc-"]`).innerHTML = translate("filters");
	document.querySelector(".js-clear-all").classList.remove("is-disabled");
	// Update results.
	document.querySelector("section.js-main-stage div.js-navigation a.is-selected, div.js-nav-item").click();
}

function packRadarFilter(keyValues)
{
	let filter = {};
	for (const [key, value] of keyValues)
		addRadarFilter(filter, key, value);
	return filter;
}

function unpackRadarFilter(filter)
{
	let keyValues = [];
	for (const key in filter)
	{
		if (isMultiRadarFilter(key))
		{
			for (const value of filter[key])
				keyValues.push([key, value]);
		}
		else
		{
			keyValues.push([key, filter[key]]);
		}
	}
	return keyValues;
}

function replaceFilterContainer(el)
{
	if (!getEnhancedFilter())
		return;

	// Remove plus color from bookmark action.
	const save = el.querySelector(".js-filter-actions .js-save");
	save?.classList.remove("is-plus");

	// Remove all filters on reset.
	const clearAll = el.querySelector(".js-filter-actions .js-clear-all");
	if (Object.keys(radarFilter).length)
		clearAll.classList.remove("is-disabled");
	clearAll.addEventListener("click", e =>
	{
		radarFilter = {};
		setRadarFilter();
		// Filter panel is recreated by default handler, recreate selections.
		setTimeout(() => replaceFilterContainer(el));
	});

	// Clear any remaining extended filters.
	filter = el.querySelector(".filter");
	for (const tags of filter.querySelectorAll(".filter__params-tags.js-tags-list"))
		tags.remove();

	// Remove PLUS-Filter ad if no original filters are selected.
	if (filter.querySelector(".js-quick-filter .js-add-params-button.plain-text-link"))
		filter.querySelector(".js-quick-filter .filter__group-more-options").remove();

	// Add custom filters.

	function addSection(text)
	{
		return addElement(filter, `
			<div class="filter__params-tags js-tags-list">
				<h3 class="typo mb-">${translate(text)}</h3>
			</div>`);
	}

	function addSectionList(text)
	{
		const section = addSection(text);
		return addElement(section, `<ul class="js-list tags-list"></ul>`);
	}

	function addSectionListMulti(text, prefix, filterKey, filterValues, hasNoEntry = true)
	{
		const section = addSectionList(text);
		for (const filterValue of filterValues)
			addListTagFilter(section, `${prefix}_${filterValue}`, filterKey, filterValue);
		if (hasNoEntry)
			addListTagFilter(section, "noEntry", filterKey, "NO_ENTRY");
		return section;
	};

	function addListTag(ul, text, selected, change)
	{
		const li = addElement(ul, `
			<li class="tags-list__item">
				<a class="js-tag ui-tag ui-tag--removable txt-truncate">
					<span class="ui-tag__label">${translate(text)}</span>
				</a>
			</li>`);
		const a = li.querySelector("a");
		if (selected)
			a.classList.add("ui-tag--selected");
		li.addEventListener("click", e =>
		{
			e.preventDefault();
			if (a.classList.contains("ui-tag--selected"))
			{
				change(false);
				a.classList.remove("ui-tag--selected");
			}
			else
			{
				change(true);
				a.classList.add("ui-tag--selected");
			}
		});
	}

	function addListTagFilter(ul, text, filterKey, filterValue)
	{
		let selected = hasRadarFilter(radarFilter, filterKey, filterValue);
		return addListTag(ul, text, selected, checked =>
		{
			if (checked)
				addRadarFilter(radarFilter, filterKey, filterValue);
			else
				removeRadarFilter(radarFilter, filterKey, filterValue);
			refreshFilter();
		});
	}

	function addInput(ul)
	{
		return addElement(ul, `
			<div class="filter__group">
				<div class="js-fulltext-input filter__group--fulltext">
					<div class="Container--uQSLs layout layout--v-center">
						<div class="layout-item layout-item--consume">
							<input class="js-input Input--EicBC input" autocorrect="off" autocapitalize="off" spellcheck="false">
						</div>
					</div>
				</div>
			</div>`).querySelector("input");
	}

	addSectionListMulti("lookingForOther", "openTo", "filter[personal][looking_for][]",
		["SEXDATES", "FRIENDSHIP", "RELATIONSHIP"]);

	addSectionListMulti("bodyType", "bodyType", "filter[personal][body_type][]",
		["SLIM", "AVERAGE", "ATHLETIC", "MUSCULAR", "BELLY", "STOCKY"]);
	addSectionListMulti("ethnicity", "ethnicity", "filter[personal][ethnicity][]",
		["CAUCASIAN", "ASIAN", "LATIN", "MEDITERRANEAN", "BLACK", "MIXED", "ARAB", "INDIAN"]);
	addSectionListMulti("hairLength", "hairLength", "filter[personal][hair_length][]",
		["SHAVED", "SHORT", "AVERAGE", "LONG", "PUNK"]);
	addSectionListMulti("hairColor", "hairColor", "filter[personal][hair_color][]",
		["BLOND", "LIGHT_BROWN", "BROWN", "BLACK", "GREY", "OTHER", "RED"]);
	addSectionListMulti("beard", "beard", "filter[personal][beard][]",
		["DESIGNER_STUBBLE", "MOUSTACHE", "GOATEE", "FULL_BEARD", "NO_BEARD"]);
	addSectionListMulti("eyeColor", "eyeColor", "filter[personal][eye_color][]",
		["BLUE", "BROWN", "GREY", "GREEN", "OTHER"]);
	addSectionListMulti("bodyHair", "bodyHair", "filter[personal][body_hair][]",
		["SMOOTH", "SHAVED", "LITTLE", "AVERAGE", "VERY_HAIRY"]);
	addSectionListMulti("gender", "gender", "filter[personal][gender_orientation][gender][]",
		["MAN", "TRANS_MAN", "TRANS_WOMAN", "NON_BINARY", "OTHER"]);
	addSectionListMulti("orientation", "orientation", "filter[personal][gender_orientation][orientation][]",
		["GAY", "BISEXUAL", "QUEER", "STRAIGHT", "OTHER"]);
	addSectionListMulti("smoker", "smoker", "filter[personal][smoker][]",
		["NO", "SOCIALLY", "YES"]);
	addSectionListMulti("tattoos", "tattoos", "filter[personal][tattoo][]",
		["A_FEW", "A_LOT", "NO"]);
	addSectionListMulti("piercings", "piercings", "filter[personal][piercing][]",
		["A_FEW", "A_LOT", "NO"]);
	addSectionListMulti("relationship", "relationship", "filter[personal][relationship][]",
		["SINGLE", "PARTNER", "OPEN", "MARRIED"]);

	addSectionListMulti("analPosition", "analPosition", "filter[sexual][anal_position][]",
		["TOP_ONLY", "MORE_TOP", "VERSATILE", "MORE_BOTTOM", "BOTTOM_ONLY", "NO"]);
	addSectionListMulti("dick", "dick", "filter[sexual][dick_size][]",
		["S", "M", "L", "XL", "XXL"]);
	addSectionListMulti("concision", "concision", "filter[sexual][concision][]",
		["CUT", "UNCUT"]);
	addSectionListMulti("fetish", "fetish", "filter[sexual][fetish][]",
		["LEATHER", "SPORTS", "SKATER", "RUBBER", "UNDERWEAR", "SKINS", "BOOTS", "LYCRA", "UNIFORM", "FORMAL",
			"TECHNO", "SNEAKERS", "JEANS", "DRAG", "WORKER", "CROSSDRESSING"]);
	addSectionListMulti("dirty", "dirty", "filter[sexual][dirty_sex][]",
		["YES", "NO", "WS_ONLY"]);
	addSectionListMulti("fisting", "fisting", "filter[sexual][fisting][]",
		["ACTIVE", "ACTIVE_PASSIVE", "PASSIVE", "NO"]);
	addSectionListMulti("sm", "sm", "filter[sexual][sm][]",
		["YES", "SOFT", "NO"]);
	addSectionListMulti("saferSex", "saferSex", "filter[sexual][safer_sex][]",
		["ALWAYS", "NEEDS_DISCUSSION", "CONDOM", "PREP", "PREP_AND_CONDOM", "TASP"]);

	addSectionListMulti("interests", "interests", "filter[hobby][interests][]",
		["ART", "BOARDGAME", "CAR", "COLLECT", "COMPUTER", "COOK", "DANCE", "FILM", "FOTO", "GAME", "LITERATURE",
			"MODELING", "MOTORBIKE", "MUSIC", "NATURE", "POLITICS", "TV"], false);

	const section = addSectionList("other");

	const coordInput = addInput(section);
	coordInput.type = "text";
	coordInput.placeholder = translate("latLong");
	if ("filter[location][lat]" in radarFilter && "filter[location][long]" in radarFilter)
		coordInput.value = `${radarFilter["filter[location][lat]"]}, ${radarFilter["filter[location][long]"]}`;
	coordInput.addEventListener("change", e =>
	{
		removeRadarFilter(radarFilter, "filter[location][lat]");
		removeRadarFilter(radarFilter, "filter[location][long]");
		const sep = e.target.value.indexOf(", ");
		if (sep !== -1)
		{
			const lat = parseFloat(e.target.value);
			const long = parseFloat(e.target.value.substring(sep + 2));
			if (!isNaN(lat) && !isNaN(long))
			{
				addRadarFilter(radarFilter, "filter[location][lat]", lat.toString());
				addRadarFilter(radarFilter, "filter[location][long]", long.toString());
			}
		}
		refreshFilter();
	});

	const radiusInput = addInput(section);
	radiusInput.type = "text";
	radiusInput.placeholder = translate("customRadius");
	if ("filter[location][radius]" in radarFilter)
	{
		const radius = radarFilter["filter[location][radius]"];
		radiusInput.value = measurementSystem === "METRIC"
			? radius / 1000
			: round(radius * M2MI, 1);
	}
	radiusInput.addEventListener("change", e =>
	{
		removeRadarFilter(radarFilter, "filter[location][radius]");
		if (parseInt(e.target.value))
		{
			const radius = measurementSystem === "METRIC"
				? e.target.value * 1000
				: e.target.value / M2MI;
			addRadarFilter(radarFilter, "filter[location][radius]", radius);
		}
		refreshFilter();
	});

	addListTagFilter(section, "bedAndBreakfast", "filter[bed_and_breakfast_filter]", "ONLY");
	addListTagFilter(section, "travelersOnly", "filter[travellers_filter]", "TRAVELLERS_ONLY");
	addListTagFilter(section, "speakingMyLanguage", "filter[personal][speaks_my_languages]", "true");
}

function xhrApplyRadarFilter(url)
{
	if (!getEnhancedFilter())
		return url;

	// Decode existing parameters.
	const sep = url.indexOf("?");
	const baseUrl = url.substring(0, sep);
	const query = url.substring(sep + 1);

	let keyValues = [];
	for (const param of query.split("&"))
	{
		let [key, value] = param.split("=");
		key = decodeURI(key);

		// Ignore pagination and "Explore" views as filters don't fully work in them.
		if (key === "cursor" || key === "length" && value === 48)
			return url;

		keyValues.push([key, value]);
	}

	// Overwrite with custom parameters.
	let filter = packRadarFilter(keyValues);
	filter = { ...filter, ...radarFilter };

	// Encode new parameters.
	url = `${baseUrl}?`;
	for (const [key, value] of unpackRadarFilter(filter))
		url += `${encodeURI(key)}=${value}&`;
	return url.slice(0, -1);
}

onXhr("open", "GET", "v4/hunqz/profiles", e => e.url = xhrApplyRadarFilter(e.url));
onXhr("open", "GET", "v4/profiles", e => e.url = xhrApplyRadarFilter(e.url));
onXhr("open", "GET", "v4/profiles/popular", e => e.url = xhrApplyRadarFilter(e.url));

onXhr("send", "PUT", "v4/settings/interface/bluebird", e =>
{
	// Changed filter.
	const id = e.body.search_filter.id;
	if (id)
		radarFilter = getSavedRadarFilter(id);
	replaceFilterContainer(document.querySelector(".js-quick-filter").parentNode);
});
onXhr("load", "DELETE", "v4/search/filters/*", e =>
{
	// Deleted filter.
	const id = e.args[0];
	setSavedRadarFilter(id);
});
onXhr("load", "POST", "v4/search/filters", e =>
{
	// Created filter.
	const id = e.body.id;
	setSavedRadarFilter(id, radarFilter);
});

// ---- Tiles ----

addCss(`
/* fix jumping fade in mobile visitors/visits during load */
div.BIG::before, div.SMALL::before
{
	inset: 60% 0px 0px !important;
}

/* tile description truncation */
.tile p[class^="SpecialText-"]
{
	white-space: var(--tile-headline-white-space);
}

.ra_tile_headline
{
	color: rgb(255, 255, 255);
	font-family: Inter, Helvetica, Arial, "Open Sans", sans-serif;
	font-size: 0.8125rem;
	font-weight: 400;
	line-height: 1.23077;
	overflow: hidden;
	text-overflow: ellipsis;
	text-shadow: rgba(0, 0, 0, 0.32) 0px 1px 1px, rgba(0, 0, 0, 0.42) 1px 1px 1px;
	white-space: var(--tile-headline-white-space);
}

.ra_tile_tag_row
{
	display: flex;
	flex-wrap: wrap;
	gap: 0.25rem;
	margin-top: 0.25rem;
}

.ra_tile_tag
{
	background-color: rgb(46, 46, 46);
	border-radius: 2px;
	box-shadow: rgba(0, 0, 0, 0.32) 0px 0px 1px, rgba(0, 0, 0, 0.24) 0px 0px 1px, rgba(0, 0, 0, 0.16) 0px 0px 3px;
	color: rgba(255, 255, 255, 0.87);
	font-family: Inter, Helvetica, Arial, "Open Sans", sans-serif;
	font-size: 0.8125rem;
	font-weight: 400;
	line-height: 1.23077;
	padding: 0px 2px;
}

.ra_tile_tag_new
{
	color: rgb(0, 209, 0);
}
`);

onDom(`div.grid-tile:not(div[class*="tile--loading--"]) > .reactView > a,
	main#visitors > ul > li > a,
	main#visited-grid > ul > li > a,
	section.js-main-stage > main main > section > ul > li > a`, a =>
{
	// Find profile cached for this tile.
	const username = getUsernameFromHref(a.href);
	const profile = profileCache[username];
	if (!profile)
	{
		return;
	}

	// Add custom tags and headline.
	let tagRow;
	let tagClasses;
	let tagNewClasses;
	function addTag(text, isNew)
	{
		if (text)
			addElement(tagRow, `<span class="${isNew ? tagNewClasses : tagClasses}">${text}</span>`);
	}

	const inner = a.firstChild;
	if (inner.classList.contains("BIG"))
	{
		// Find existing tag elements and classes.
		const lastTag = a.querySelector(`div:last-child > span[class^="SpecialText-"]:last-child`);
		if (!lastTag)
			return;
		tagRow = lastTag.parentNode;
		tagClasses = lastTag.classList;
		tagNewClasses = tagRow.firstChild.classList;
		// Clear existing tags.
		tagRow.replaceChildren();
	}
	else
	{
		const container = inner.lastChild;
		// Create headline.
		if (profile.headline)
			addElement(container, `<p class="ra_tile_headline">${profile.headline}</p>`);
		// Create tag row.
		tagRow = addElement(container, `<div class="ra_tile_tag_row">`);
		tagClasses = "ra_tile_tag";
		tagNewClasses = tagClasses + " ra_tile_tag_new";
	}

	// Add tags.
	if (tagNewClasses.value !== tagClasses.value)
		addTag(translate("new"), true);
	const personal = profile.personal;
	if (personal)
	{
		if (tileDetails.has("age")) addTag(personal.age);
		if (tileDetails.has("bodyHair")) addTag(getProfileEnum("bodyHair", personal.body_hair));
		if (tileDetails.has("height")) addTag(getProfileHeight(personal.height));
		if (tileDetails.has("weight")) addTag(getProfileWeight(personal.weight));
		if (tileDetails.has("bmi")) addTag(getProfileBmi(personal.height, personal.weight));
		if (tileDetails.has("ageRange")) addTag(getProfileAgeRange(personal.target_age, true));
		if (tileDetails.has("bodyType")) addTag(getProfileEnum("bodyType", personal.body_type));
		if (tileDetails.has("ethnicity")) addTag(getProfileEnum("ethnicity", personal.ethnicity));
		if (tileDetails.has("relationship")) addTag(getProfileEnum("relationship", personal.relationship));
		if (tileDetails.has("smoker"))
		{
			if (personal.smoker === "YES")
				addTag(translate("smoker"));
			else if (personal.smoker === "SOCIALLY")
				addTag(translate("socialSmoker"));
		}
		if (tileDetails.has("openTo") && personal.looking_for && personal.looking_for[0] !== "NO_ENTRY")
		{
			let text = "";
			for (let openTo of personal.looking_for)
				text += translateEnum("openTo", openTo)[0];
			addTag(text);
		}
	}
	const sexual = profile.sexual;
	if (sexual)
	{
		if (tileDetails.has("analPosition")) addTag(getProfileEnum("analPosition", sexual.anal_position));
		if (tileDetails.has("dick")) addTag(getProfileDick(sexual.dick_size, sexual.concision));
		if (tileDetails.has("saferSex")) addTag(getProfileEnum("saferSex", sexual.safer_sex));
		if (tileDetails.has("dirty")) addTag(getProfileEnum("dirty", sexual.dirty_sex));
		if (tileDetails.has("sm")) addTag(getProfileEnum("sm", sexual.sm));
		if (tileDetails.has("fisting")) addTag(getProfileEnum("fisting", sexual.fisting));
	}
});

onMenu(`section.js-main-stage > main main > section > ul > li,
	.search-results__item > .tile,
	.js-results .tile`, el =>
{
	// discover / radar / search > tile
	function hideTile()
	{
		if (el.classList.contains("tile--plus"))
			el.parentNode.style.display = "none"; // big radar tile
		else
			el.style.display = "none"; // small / normal tile
	}

	let inner;
	if (inner = el.querySelector("a"))
	{
		// full tile
		const username = getUsernameFromHref(inner.href);
		showMenu(
			menuItem("search", "viewProfile", () => showProfilePreview(username)),
			menuItem("hide-visit", "hideUser", () =>
			{
				setUserHidden(username, true);
				hideTile();
			}),
			menuItem("illegal", "blockUser", async () =>
			{
				if (await blockUser(username))
					hideTile();
			}),
		);
	}
	else if (inner = el.querySelector(".BIG") ?? el.querySelector(".SMALL"))
	{
		// image-only tile
		const imageUrl = getFullImageUrl(getBackgroundImageUrl(inner.style.backgroundImage));
		showMenu(
			menuItem("search", "viewFullImage", () => showImagePreview(imageUrl)),
		);
	}
});

onMenu(`
	main#visitors > ul > li,
	main#visited-grid > ul > li,
	main#likers-list > ul > li`, el =>
{
	// cruise > tiles
	const a = el.querySelector("a");
	const username = getUsernameFromHref(a.href);
	showMenu(
		menuItem("search", "viewProfile", () => showProfilePreview(username)),
		menuItem("hide-visit", "hideUser", () =>
		{
			setUserHidden(username, true);
			if (a.closest("main#likers-list") ? getHideLikes() : getHideVisits())
				el.style.display = "none";
		}),
		menuItem("illegal", "blockUser", async () =>
		{
			if (await blockUser(username))
				el.style.display = "none";
		}),
	);
});

onMenu("img.thumbnail", el =>
{
	// activities > activity > liked image
	showMenu(
		menuItem("search", "viewFullImage", () => showImagePreview(getFullImageUrl(el.src))),
	);
});

onMenu(".js-stream .js-list .listitem", el =>
{
	// activities > activity
	const a = el.querySelector("a");
	const username = getUsernameFromHref(a.href);
	showMenu(
		menuItem("search", "viewProfile", () => showProfilePreview(username)),
		menuItem("hide-visit", "hideUser", () =>
		{
			setUserHidden(username, true);
			if (getHideActivities())
				el.style.display = "none";
		}),
		menuItem("illegal", "blockUser", async () =>
		{
			if (await blockUser(username))
				el.style.display = "none";
		}),
	);
});

// ---- Messaging ----

addCss(`
/* message list truncation */
#messenger div[class^="TruncateBlock__Content-sc-"]
{
	-webkit-line-clamp: var(--message-line-clamp);
}
`);

onDom(`.js-send-region.layout-item > div`, el =>
{
	el.addEventListener("keydown", e =>
	{
		// Prevent site event handler from sending message or typing notifications.
		const enter = e.key === "Enter";
		const send = enter && (getSendEnter() || e.ctrlKey);
		const allow = send || getTypingNotifications() && !enter;
		if (!allow)
			e.stopPropagation();
	}, true);
});

onMenu(".js-chat .reactView", el =>
{
	// messages > message
	const a = el.querySelector("a");
	const username = getUsernameFromHref(a.href);
	showMenu(
		menuItem("search", "viewProfile", () => showProfilePreview(username)),
		menuItem("hide-visit", "hideUser", () =>
		{
			setUserHidden(username, true);
			if (getHideMessages())
				el.style.display = "none";
		}),
		menuItem("illegal", "blockUser", async () => blockUser(username)),
	);
});

onMenu(".js-chat .reactView img", el =>
{
	// messages > message > sent image
	showMenu(
		menuItem("search", "viewFullImage", () => showImagePreview(getFullImageUrl(el.src))),
	);
});

onMenu(".js-contacts .reactView", el =>
{
	// contacts > contact
	const a = el.querySelector("a");
	const username = getUsernameFromHref(a.href);
	showMenu(
		menuItem("search", "viewProfile", () => showProfilePreview(username)),
		menuItem("hide-visit", "hideUser", () =>
		{
			setUserHidden(username, true);
			if (getHideContacts())
				el.style.display = "none";
		}),
		menuItem("illegal", "blockUser", async () =>
		{
			if (await blockUser(username))
				el.style.display = "none";
		}),
	);
});

// ---- Albums ----

let changeProfilePic = false;

//addCss(`
///* ensure images are not cropped */
//div[role='dialog'] > div > main > ul > li > img
//{
//	object-fit: contain;
//}
//`);

onDom(`li#picture_menu_set-as-main-profile-picture`, el =>
{
	const button = el.querySelector("button");

	// Only allow profile pic being changed when manually clicking this button.
	button.parentNode.addEventListener("click", e => changeProfilePic = true, true);
});

onXhr("send", "PUT", "v4/profiles/me", e =>
{
	// Prevent automatic profile picture change when rearranging pictures.
	if (changeProfilePic)
		changeProfilePic = false;
	else if (e.body.preview_pic_id)
		e.cancel = true;
});

// ---- Settings ----

onDom(`li.js-settings > div.accordion > ul.js-list`, el =>
{
	// Add extension menu item.
	const linkClass = el.querySelector("a").className;
	const link = addElement(el, `
		<li>
			<div>
				<a class="${linkClass}" href="/me/romeoadditions">${GM_info.script.name}</a>
			</div>
		</li>`);
	link.addEventListener("click", e =>
	{
		if (link.classList.contains("is-selected"))
		{
			link.classList.remove("is-selected");
		}
		else
		{
			link.classList.add("is-selected");
			setTimeout(() => openSettingsPane(el, link)); // delayed execution to force open panel
		}
	});
	// Deselect menu item if others are clicked.
	for (const linkOther of el.querySelectorAll("li"))
		if (linkOther !== link)
			linkOther.addEventListener("click", e => link.classList.remove("is-selected"));
});

onDom(`#offcanvas-nav > .js-layer-content > main > div.layout > div.reactView--autoHeight > p[class^="MiniText-sc-"]`, el =>
{
	el.innerHTML += `<a class="marionette" style="display:block" href="https://greasyfork.org/en/scripts/419514" target="blank">${GM_info.script.name} ${GM_info.script.version}</a>`;
});

function openSettingsPane(ul, link)
{
	// Open pane.
	const layerContent = document.querySelector("#offcanvas-nav > .js-layer-content");
	layerContent.classList.add("is-open");

	// Create UI.
	const pane = layerContent.querySelector(".js-side-content");
	pane.replaceChildren();
	const p = addElement(pane, `
		<div class="layout layout--vertical layout--consume">
			<div class="layout-item layout-item--consume layout layout--vertical">

				<div class="js-header layout-item l-hidden-md-lg">
					<div class="layer-header layer-header--primary">
						<a class="back-button l-hidden-md-lg l-tappable js-back marionette" href="/me">
							<span class="js-back-icon icon icon-back icon-large"></span>
						</a>
						<div class="layer-header__title">
							<h2>${GM_info.script.name}</h2>
						</div>
					</div>
				</div>
				<div class="layout-item settings__navigation p l-hidden-sm">
					<div class="js-title typo-section-navigation">${GM_info.script.name}</div>
				</div>

				<div class="layout-item layout-item--consume">
					<div class="js-content js-scrollable fit scrollable">
						<div id="ra_settings_p" class="p"></div>
					</div>
				</div>
			</div>
		</div>`).querySelector("#ra_settings_p");

	function addSection(title)
	{
		return addElement(p, `
			<div class="settings__key">
				<div>
					<span>${translate(title)}</span>
				</div>
				<div class="separator separator--alt separator--narrow [ mb ] "></div>
			</div>`);
	}
	function addCheckbox(section, text, desc)
	{
		const input = addElement(section, `
			<div class="layout layout--v-center">
				<div class="layout-item [ 6/12--sm ]">
					<span>${translate(text)}</span>
				</div>
				<div class="layout-item [ 6/12--sm ]">
					<div class="js-toggle-show-headlines pull-right">
						<div>
							<span class="ui-toggle ui-toggle--default ui-toggle--right">
								<input class="ui-toggle__input" type="checkbox" id="ra_${text}">
								<label class="ui-toggle__label" for="ra_${text}" style="touch-action: pan-y; user-select: none; -webkit-user-drag: none; -webkit-tap-highlight-color: rgba(0, 0, 0, 0);"></label>
							</span>
						</div>
					</div>
				</div>
			</div>`).querySelector("input");
		if (desc)
		{
			addElement(section, `
				<div>
					<div class="settings__description">${translate(desc)}</div>
				</div>`);
		}
		return input;
	}
	function addNumber(section, text, min, max)
	{
		return addElement(section, `
			<div class="layout layout--v-center">
				<div class="layout-item [ 6/12--sm ] mv-">
					<span>${translate(text)}</span>
				</div>
				<div class="layout-item [ 6/12--sm ] mv-">
					<input class="input input--block" type="number" min="${min}" max="${max}"/>
				</div>
			</div>`).querySelector("input");
	}
	function addTagList(section)
	{
		return addElement(section, `
			<div class="mv js-grid-stats-selector">
				<div>
					<ul class="js-list tags-list tags-list--centered"/>
				</div>
			</div>`).querySelector("ul");
	}
	function addTag(ul, tag, text, selected, change)
	{
		const li = addElement(ul, `
			<li class="tags-list__item">
				<a class="js-tag ui-tag ui-tag--removable" href="#">
					<span class="ui-tag__label">${text}</span>
				</a>
			</li>`);
		const a = li.querySelector("a");
		if (selected)
			a.classList.add("ui-tag--selected");
		li.addEventListener("click", e =>
		{
			e.preventDefault();
			if (a.classList.contains("ui-tag--selected"))
			{
				change({ tag: tag, checked: false });
				a.classList.remove("ui-tag--selected");
			}
			else
			{
				change({ tag: tag, checked: true });
				a.classList.add("ui-tag--selected");
			}
		});
	}

	// Add tiles section.
	const tilesSection = addSection("tiles");

	const enhancedFilter = addCheckbox(tilesSection, "enhancedFilter", "enhancedFilterDesc");
	enhancedFilter.checked = getEnhancedFilter();
	enhancedFilter.addEventListener("change", e => setEnhancedFilter(e.target.checked));

	const enhancedTiles = addCheckbox(tilesSection, "enhancedTiles", "enhancedTilesDesc");
	enhancedTiles.checked = getEnhancedTiles();
	enhancedTiles.addEventListener("change", e => setEnhancedTiles(e.target.checked));

	const fullHeadlines = addCheckbox(tilesSection, "fullHeadlines", "fullHeadlinesDesc");
	fullHeadlines.checked = getFullHeadlines();
	fullHeadlines.addEventListener("change", e => setFullHeadlines(e.target.checked));

	const tileCount = addNumber(tilesSection, "tileCount", 0, 10);
	tileCount.value = getTileCount();
	tileCount.addEventListener("change", e => setTileCount(parseInt(e.target.value)));

	// Add tile details section.
	const tileDetailsSection = addSection("tileDetails");

	const tileDetailsList = addTagList(tileDetailsSection, "tileDetailsList");
	for (const tileDetail of ["age", "height", "weight", "bmi", "smoker", "ageRange",
		"bodyHair", "bodyType", "ethnicity", "relationship", "analPosition",
		"dick", "saferSex", "dirty", "sm", "fisting", "openTo"])
	{
		addTag(tileDetailsList, tileDetail, translate(tileDetail), tileDetails.has(tileDetail), e => setTileDetail(e.tag, e.checked));
	}

	// Add messages section.
	const messagesSection = addSection("messages");

	const systemMessages = addCheckbox(messagesSection, "systemMessages", "systemMessagesDesc");
	systemMessages.checked = getSystemMessages();
	systemMessages.addEventListener("change", e => setSystemMessages(e.target.checked));

	const fullMessages = addCheckbox(messagesSection, "fullMessages", "fullMessagesDesc");
	fullMessages.checked = getFullMessages();
	fullMessages.addEventListener("change", e => setFullMessages(e.target.checked));

	const typingNotifications = addCheckbox(messagesSection, "typingNotifications", "typingNotificationsDesc");
	typingNotifications.checked = getTypingNotifications();
	typingNotifications.addEventListener("change", e => setTypingNotifications(e.target.checked));

	const sendEnter = addCheckbox(messagesSection, "sendEnter", "sendEnterDesc");
	sendEnter.checked = getSendEnter();
	sendEnter.addEventListener("change", e => setSendEnter(e.target.checked));

	// Add hidden users section.
	const hiddenUsersSection = addSection("hiddenUsers");

	const hideContacts = addCheckbox(hiddenUsersSection, "hideContacts");
	hideContacts.checked = getHideContacts();
	hideContacts.addEventListener("change", e => setHideContacts(e.target.checked));

	const hideMessages = addCheckbox(hiddenUsersSection, "hideMessages");
	hideMessages.checked = getHideMessages();
	hideMessages.addEventListener("change", e => setHideMessages(e.target.checked));

	const hideVisits = addCheckbox(hiddenUsersSection, "hideVisits");
	hideVisits.checked = getHideVisits();
	hideVisits.addEventListener("change", e => setHideVisits(e.target.checked));

	const hideActivities = addCheckbox(hiddenUsersSection, "hideActivities");
	hideActivities.checked = getHideActivities();
	hideActivities.addEventListener("change", e => setHideActivities(e.target.checked));

	const hideLikes = addCheckbox(hiddenUsersSection, "hideLikes");
	hideLikes.checked = getHideLikes();
	hideLikes.addEventListener("change", e => setHideLikes(e.target.checked));

	const inMinAge = addNumber(hiddenUsersSection, "minAge", 18, 99);
	const inMaxAge = addNumber(hiddenUsersSection, "maxAge", 18, 99);
	let minAge = getHiddenMinAge();
	let maxAge = getHiddenMaxAge();
	inMinAge.value = minAge;
	inMaxAge.value = maxAge;
	inMinAge.addEventListener("change", e =>
	{
		minAge = parseInt(e.target.value);
		setHiddenMinAge(minAge);
		if (minAge > maxAge)
		{
			maxAge = minAge;
			setHiddenMaxAge(maxAge);
			inMaxAge.val(maxAge);
		}
	});
	inMaxAge.addEventListener("change", e =>
	{
		maxAge = parseInt(e.target.value);
		setHiddenMaxAge(maxAge);
		if (maxAge < minAge)
		{
			minAge = maxAge;
			setHiddenMinAge(minAge);
			inMinAge.val(minAge);
		}
	});

	createList(hiddenUsersSection, {
		onGet: () => Array.from(getHiddenUsers()).sort(Intl.Collator().compare),
		onAdd: e => setUserHidden(e, true),
		onRemove: e => setUserHidden(e, false)
	});
}

// ---- Load ----

setSystemMessages(getSystemMessages());
setFullHeadlines(getFullHeadlines());
setTileCount(getTileCount());
setFullMessages(getFullMessages());
tileDetails = getTileDetails();

initMenu();
initDom();
initPreviews();
initFetch();
initXhr();