Romeo Additions

Ermöglicht das Verstecken von Benutzern, die Anzeige ihrer Details auf Kacheln, und verbessert den Radar.

Version vom 26.05.2023. Aktuellste Version

Du musst eine Erweiterung wie Tampermonkey, Greasemonkey oder Violentmonkey installieren, um dieses Skript zu installieren.

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

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

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

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

Sie müssten eine Skript Manager Erweiterung installieren damit sie dieses Skript installieren können

(Ich habe schon ein Skript Manager, Lass mich es installieren!)

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

(I already have a user style manager, let me install it!)

// ==UserScript==
// @name           Romeo Additions
// @name:de        Romeo Additions
// @namespace      https://greasyfork.org/en/users/723211-ray/
// @version        4.1.0
// @description    Allows to hide users, display their information on tiles, and enhances the Radar.
// @description:de Ermöglicht das Verstecken von Benutzern, die Anzeige ihrer Details auf Kacheln, und verbessert den Radar.
// @author         -Ray-, Djamana
// @match          *://*.romeo.com/*
// @license        MIT
// ==/UserScript==

// ==== Dependencies ====

// ---- https://github.com/CoeJoder/GM_wrench ----

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

function waitForKeyElements(selectorOrFunction, callback, waitOnce, interval, maxIntervals) {
    if (typeof waitOnce === 'undefined') {
        waitOnce = true;
    }
    if (typeof interval === 'undefined') {
        interval = 300;
    }
    if (typeof maxIntervals === 'undefined') {
        maxIntervals = -1;
    }
    var targetNodes = (typeof selectorOrFunction === 'function')
        ? selectorOrFunction()
        : document.querySelectorAll(selectorOrFunction);

    var targetsFound = targetNodes && targetNodes.length > 0;
    if (targetsFound) {
        targetNodes.forEach(function (targetNode) {
            var attrAlreadyFound = 'data-userscript-alreadyFound';
            var alreadyFound = targetNode.getAttribute(attrAlreadyFound) || false;
            if (!alreadyFound) {
                var cancelFound = callback(targetNode);
                if (cancelFound) {
                    targetsFound = false;
                }
                else {
                    targetNode.setAttribute(attrAlreadyFound, true);
                }
            }
        });
    }

    if (maxIntervals !== 0 && !(targetsFound && waitOnce)) {
        maxIntervals -= 1;
        setTimeout(function () {
            waitForKeyElements(selectorOrFunction, callback, waitOnce, interval, maxIntervals);
        }, interval);
    }
};

// ==== CSS ====

addCss(`
:root {
    --message-line-clamp: 2;
    --tile-headline-white-space: nowrap;
    --tile-size-factor: 0;
    --tile-size-group-factor: 0;
    --tile-size-xxlarge: calc(100% / max(1, var(--tile-size-factor) + 1));
    --tile-size-xlarge: calc(100% / max(1, var(--tile-size-factor) + 2));
    --tile-size-large: calc(100% / max(1, var(--tile-size-factor) + 3));
    --tile-size-medium: calc(100% / max(1, var(--tile-size-factor) + 4));
    --tile-size-small: calc(100% / max(1, var(--tile-size-factor) + 5));
    --tile-size-xsmall: calc(100% / max(1, var(--tile-size-factor) + 6));
    --tile-size-xxsmall: calc(100% / max(1, var(--tile-size-factor) + 7));
    --tile-size-group-large: calc(100% / max(1, var(--tile-size-group-factor) + 3));
    --tile-size-group-medium: calc(100% / max(1, var(--tile-size-group-factor) + 4));
    --tile-size-group-small: calc(100% / max(1, var(--tile-size-group-factor) + 5));
}

/* responsive tile size overrides (visitors) */
.grouped-tiles-small .tile {
    width: var(--tile-size-group-large);
}
@media screen and (min-width: 768px)and (max-width:1023px) {
    .grouped-tiles-small .tile {
        width: var(--tile-size-group-medium);
    }
}
@media screen and (min-width: 1024px) {
    .grouped-tiles-small .tile {
        width: var(--tile-size-group-small);
    }
}
@media screen and (min-width: 35rem)and (max-width:48rem) {
    .grouped-tiles-small .tile {
        width: var(--tile-size-group-medium);
    }
}

/* responsive tile size overrides (radar) */
@media screen and (min-width: 48rem) {
    .search-results--big-tiles .search-results__item,.search-results--mixed-tiles .search-results__item {
        padding-bottom: var(--tile-size-xlarge) !important;
        width: var(--tile-size-xlarge) !important;
    }
    .is-stream-opened .search-results--big-tiles .search-results__item,.is-stream-opened .search-results--mixed-tiles .search-results__item {
        padding-bottom: var(--tile-size-xlarge) !important;
        width: var(--tile-size-xlarge) !important;
    }
    .is-filter-opened .search-results--big-tiles .search-results__item,.is-filter-opened .search-results--mixed-tiles .search-results__item {
        padding-bottom: var(--tile-size-xxlarge) !important;
        width: var(--tile-size-xxlarge) !important;
    }
    .is-stream-opened .is-filter-opened .search-results--big-tiles .search-results__item,.is-stream-opened .is-filter-opened .search-results--mixed-tiles .search-results__item {
        padding-bottom: var(--tile-size-xxlarge) !important;
        width: var(--tile-size-xxlarge) !important;
    }
}
@media screen and (min-width: 60rem) {
    .search-results--big-tiles .search-results__item,.search-results--mixed-tiles .search-results__item {
        padding-bottom: var(--tile-size-large) !important;
        width: var(--tile-size-large) !important;
    }
    .is-filter-opened .search-results--big-tiles .search-results__item,.is-filter-opened .search-results--mixed-tiles .search-results__item,.is-stream-opened .search-results--big-tiles .search-results__item,.is-stream-opened .search-results--mixed-tiles .search-results__item {
        padding-bottom: var(--tile-size-xlarge) !important;
        width: var(--tile-size-xlarge) !important;
    }
    .is-stream-opened .is-filter-opened .search-results--big-tiles .search-results__item,.is-stream-opened .is-filter-opened .search-results--mixed-tiles .search-results__item {
        padding-bottom: var(--tile-size-xlarge) !important;
        width: var(--tile-size-xlarge) !important;
    }
}
@media screen and (min-width: 80rem) {
    .search-results--big-tiles .search-results__item,.search-results--mixed-tiles .search-results__item {
        padding-bottom: var(--tile-size-medium) !important;
        width: var(--tile-size-medium) !important;
    }
    .is-filter-opened .search-results--big-tiles .search-results__item,.is-filter-opened .search-results--mixed-tiles .search-results__item,.is-stream-opened .search-results--big-tiles .search-results__item,.is-stream-opened .search-results--mixed-tiles .search-results__item {
        padding-bottom: var(--tile-size-large) !important;
        width:var(--tile-size-large) !important;
    }
    .is-stream-opened .is-filter-opened .search-results--big-tiles .search-results__item,.is-stream-opened .is-filter-opened .search-results--mixed-tiles .search-results__item {
        padding-bottom: var(--tile-size-xlarge) !important;
        width: var(--tile-size-xlarge) !important;
    }
}
@media screen and (min-width: 100rem) {
    .search-results--big-tiles .search-results__item,.search-results--mixed-tiles .search-results__item {
        padding-bottom: var(--tile-size-small) !important;
        width: var(--tile-size-small) !important;
    }
    .is-filter-opened .search-results--big-tiles .search-results__item,.is-filter-opened .search-results--mixed-tiles .search-results__item,.is-stream-opened .search-results--big-tiles .search-results__item,.is-stream-opened .search-results--mixed-tiles .search-results__item {
        padding-bottom: var(--tile-size-medium) !important;
        width: var(--tile-size-medium) !important;
    }
    .is-stream-opened .is-filter-opened .search-results--big-tiles .search-results__item,.is-stream-opened .is-filter-opened .search-results--mixed-tiles .search-results__item {
        padding-bottom: var(--tile-size-large) !important;
        width: var(--tile-size-large) !important;
    }
}
@media screen and (min-width: 120rem) {
    .search-results--big-tiles .search-results__item,.search-results--mixed-tiles .search-results__item {
        padding-bottom: var(--tile-size-xsmall) !important;
        width: var(--tile-size-xsmall) !important;
    }
    .is-filter-opened .search-results--big-tiles .search-results__item,.is-filter-opened .search-results--mixed-tiles .search-results__item,.is-stream-opened .search-results--big-tiles .search-results__item,.is-stream-opened .search-results--mixed-tiles .search-results__item {
        padding-bottom: var(--tile-size-small) !important;
        width: var(--tile-size-small) !important;
    }
    .is-stream-opened .is-filter-opened .search-results--big-tiles .search-results__item,.is-stream-opened .is-filter-opened .search-results--mixed-tiles .search-results__item {
        padding-bottom: var(--tile-size-medium) !important;
        width: var(--tile-size-medium) !important;
    }
}
@media screen and (min-width: 140rem) {
    .search-results--big-tiles .search-results__item,.search-results--mixed-tiles .search-results__item {
        padding-bottom: var(--tile-size-xxsmall) !important;
        width: var(--tile-size-xxsmall) !important;
    }
    .is-filter-opened .search-results--big-tiles .search-results__item,.is-filter-opened .search-results--mixed-tiles .search-results__item,.is-stream-opened .search-results--big-tiles .search-results__item,.is-stream-opened .search-results--mixed-tiles .search-results__item {
        padding-bottom: var(--tile-size-xsmall) !important;
        width: var(--tile-size-xsmall) !important;
    }
    .is-stream-opened .is-filter-opened .search-results--big-tiles .search-results__item,.is-stream-opened .is-filter-opened .search-results--mixed-tiles .search-results__item {
        padding-bottom: var(--tile-size-small) !important;
        width: var(--tile-size-small) !important;
    }
}

/* enhanced radar filter */
.js-quick-filter {
    overflow: scroll;
}

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

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

/* hide PLUS icon as PLUS is faked to enhance tiles */
.js-romeo-badge {
    display: none;
}

/* wider visitor list */
#visits>.layer__container--wider {
    max-width: 1227px;
    width: unset;
}

/* hide PLUS message at bottom of visitor grid */
#visits div[class^="UnlockMoreVisitorsGrid"] {
    display: none;
}

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

/* allow clicking tile actions above stamps */
.listitem__highlight--footprint {
    pointer-events: none;
}

/* tile action bar */
.tile__bar {
    background: #121212;
    border-radius: 5px 0 0 5px;
    padding: 2px 1px 2px 2px;
    box-shadow: 0 0 8px 0 #0000007F;
    right: 0;
    opacity: 0;
    position: absolute;
    top: 50%;
    transform: translate(100%, -50%);
    transition: opacity 0.2s ease-out, transform 0.2s ease-out;
    z-index: 1;
}
@media screen and (hover:none) {
    .tile__bar {
        opacity: 0.5;
        transform: translate(0%, -50%);
    }
}

.tile__bar_action {
    border-radius: 3px;
    color: white;
    display: block;
    padding: 6px;
}
.tile__bar_action:hover {
    background-color: #00A3E4;
}
.tile__bar_action:active {
    background-color: #06648B;
}

.tile:hover .tile__bar {
    opacity: 0.5;
    transform: translate(0, -50%);
}
.tile:hover .tile__bar:hover {
    opacity: 1;
}

/* profile preview */
#ra_profile_wrapper {
    background-color: black;
    display: grid;
    font-family: Inter, Helvetica, Arial, "Open Sans", sans-serif;
    grid-template-columns: auto 352px;
    height: 100%;
    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, 1fr) 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;
}

#version {
    color: white;
}
`);

// ==== Script ====

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

function onElement(selector, callback) {
    waitForKeyElements(selector, callback, false);
}

// ---- Language ----

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"
    },
    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"
    },
    concision: {
        de: "Beschneidung",
        en: "Concision"
    },
    concision_CUT: {
        de: "Beschnitten",
        en: "Cut"
    },
    concision_UNCUT: {
        de: "Unbeschnitten",
        en: "Uncut"
    },
    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: "Erweiterter Filter",
        en: "Enhanced filter"
    },
    enhancedFilterDesc: {
        de: "Erlaubt die Filterung von Radar-Ergebnissen ohne PLUS und fügt neue Filter hinzu.",
        en: "Allows filtering of radar results without having PLUS adds new filters."
    },
    enhancedTiles: {
        de: "Erweiterte Kacheln",
        en: "Enhanced tiles"
    },
    enhancedTilesDesc: {
        de: "Zeigt alle Details auf den Kacheln. Im Radar zeigt dies Benutzer mit großen Kacheln.",
        en: "Shows all user details on tiles. The radar will display users with large 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"
    },
    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 anzeigen",
        en: "Show full headlines"
    },
    fullHeadlinesDesc: {
        de: "Zeigt auch lange Profilüberschriften ungekürzt auf Kacheln.",
        en: "Shows even long profile headlines completely on tiles."
    },
    fullMessages: {
        de: "Vollständige Nachrichten anzeigen",
        en: "Show 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"
    },
    hiddenUsersList: {
        de: "Liste ausgeblendeter Benutzernamen",
        en: "List of hidden user names"
    },
    hideActivities: {
        de: "Auch Activities verstecken",
        en: "Also hide activities"
    },
    hideActivitiesDesc: {
        de: "Versteckt ausgeblendete Benutzer auch im Activity Stream.",
        en: "Removes hidden users even in the activity stream."
    },
    hideLikes: {
        de: "Auch Likes auf Bilder verstecken",
        en: "Also hide likes on pictures"
    },
    hideLikesDesc: {
        de: "Versteckt ausgeblendete Benutzer auch in der Liste an Likes von Bildern.",
        en: "Removes hidden users even in the list of likes on pictures."
    },
    hideMessages: {
        de: "Auch Nachrichten verstecken",
        en: "Also hide messages"
    },
    hideMessagesDesc: {
        de: "Versteckt ausgeblendete Benutzer auch in der Nachrichtenliste.",
        en: "Removes hidden users even in the message list."
    },
    hideUser: {
        de: "Benutzer ausblenden",
        en: "Hide user"
    },
    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"
    },
    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"
    },
    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"
    },
    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"
    },
    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: "Ich habe einen Partner",
        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"
    },
    searchOptions: {
        de: "Suchoptionen",
        en: "Search options"
    },
    sendEnter: {
        de: "Enter sendet Nachricht",
        en: "Enter sends message"
    },
    sendEnterDesc: {
        de: "Wenn deaktiviert, erzeugt Enter einen Absatz und Strg+Enter versendet 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"
    },
    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"
    },
    tileDetailsList: {
        de: "Profildetails auf Kacheln anzeigen",
        en: "Grid Stats"
    },
    tiles: {
        de: "Benutzerkacheln",
        en: "User tiles"
    },
    tileSizeFactor: {
        de: "Extra-Kacheln pro Zeile (Radar)",
        en: "Extra tiles per row (Radar)"
    },
    tileSizeGroupFactor: {
        de: "Extra-Kacheln pro Zeile (Besucher)",
        en: "Extra tiles per row (Visitors)"
    },
    typingNotifications: {
        de: "Tippbenachrichtigungen",
        en: "Typing notifications"
    },
    typingNotificationsDesc: {
        de: "Wenn deaktiviert, können Empfänger nicht mehr sehen, dass eine Nachricht verfasst wird.",
        en: "If disabled, receivers can no longer see that a message is being composed."
    },
    viewFullImage: {
        de: "Bild vergrößern",
        en: "View full image"
    },
    viewProfile: {
        de: "Profil ohne Besuch anzeigen",
        en: "View profile without visiting"
    },
    weight: {
        de: "Gewicht",
        en: "Weight"
    }
};

function translate(key) {
    const lang = document.documentElement.getAttribute("lang") || "en";
    const translations = _strings[key];
    return translations
        ? translations[lang] || 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();

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 setStyleProp(name, value) {
    document.documentElement.style.setProperty(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 getHideLikes() {
    return load("hideLikes", true);
}
function getHideMessages() {
    return load("hideMessages", false);
}
function getRadarFilter() {
    return JSON.parse(load("radarFilter", `{}`));
}
function getSendEnter() {
    return load("sendEnter", true);
}
function getTileDetails() {
    return new Set(JSON.parse(load("tileDetails", `[ "age", "height", "bodyHair", "bodyType", "relationship", "analPosition" ]`)));
}
function getTileSizeFactor() {
    return load("tileSizeFactor", 0);
}
function getTileSizeGroupFactor() {
    return load("tileSizeGroupFactor", 0);
}
function getTypingNotifications() {
    return load("typingNotifications", true);
}
function setEnhancedFilter(value) {
    save("enhancedFilter", value);
}
function setEnhancedTiles(value) {
    save("enhancedTiles", value);
}
function setFullHeadlines(value) {
    setStyleProp("--tile-headline-white-space", value ? "unset" : "nowrap");
    save("fullHeadlines", value);
}
function setFullMessages(value) {
    setStyleProp("--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 setHideLikes(value) {
    save("hideLikes", value);
}
function setHideMessages(value) {
    save("hideMessages", value);
}
function setRadarFilter() {
    save("radarFilter", JSON.stringify(radarFilter));
}
function setSendEnter(value) {
    save("sendEnter", value);
}
function setTileDetail(key, visible) {
    if (visible) {
        tileDetails.add(key);
    } else {
        tileDetails.delete(key);
    }
    save("tileDetails", JSON.stringify(Array.from(tileDetails)));
}
function setTileSizeFactor(value) {
    setStyleProp("--tile-size-factor", value);
    save("tileSizeFactor", value);
}
function setTileSizeGroupFactor(value) {
    setStyleProp("--tile-size-group-factor", value);
    save("tileSizeGroupFactor", value);
}
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)));
}

// ---- XHR ----

function getApiVerb(url) {
    if (url.includes("/api/stream")) {
        return "stream";
    }
    // Extract verb in "/api/v#/verb?" or "/api/+/verb?".
    const matches = url.match("/api/(v[0-9]|\\+)/(.*)\\?".replaceAll("/", "\\/"));
    if (matches && matches.length === 3) {
        return matches.at(-1);
    }
}

function proxyXhr() {
    const realOpen = window.XMLHttpRequest.prototype.open;
    window.XMLHttpRequest.prototype.open = function (method, url, async, user, password) {
        // Manipulate request.
        const verb = getApiVerb(url);
        switch (verb) {
            case "hunqz/profiles":
            case "profiles":
                url = xhrApplyRadarFilter(url);
                break;
        }

        // Manipulate reply.
        this.addEventListener("load", () => {
            //console.log(`[RA] ${method} ${url}`);
            if (method !== "GET") {
                return;
            }
            const verb = getApiVerb(url);
            const isJson = verb !== "stream" && typeof this.response === "string";
            let reply = isJson ? JSON.parse(this.response) : this.response;
            //console.log(`[RA] ${verb}`, reply);

            // Modify interesting data.
            switch (verb) {
                case "messages/conversations":
                    reply.items = xhrProcessUserItems(reply.items, x => x.chat_partner, getHideMessages());
                    break;
                case "notifications/activity-stream":
                    reply = xhrProcessUserItems(reply, x => x.partner, getHideActivities());
                    break;
                case "hunqz/profiles":
                case "profiles":
                    reply.items = xhrProcessUserItems(reply.items, x => x, true);
                    reply = xhrEnhanceProfilesAndVisits(reply);
                    break;
                case "session":
                    xhrHandleSession(reply);
                    break;
                case "visitors":
                case "visits":
                    reply.items = xhrProcessUserItems(reply.items, x => x, true);
                    reply = xhrEnhanceProfilesAndVisits(reply);
                    break;
                case "reactions/pictures/basic":
                    reply.items = xhrProcessUserItems(reply.items, x => x.user_id, getHideLikes());
                    break;
            }

            // Write back possibly modified data.
            Object.defineProperty(this, "responseText", { writable: true });
            this.responseText = isJson ? JSON.stringify(reply) : reply;
        });

        // Forward to client.
        return realOpen.apply(this, arguments);
    }
}

function xhrEnhanceProfilesAndVisits(reply) {
    // Restore PLUS-visible visitors.
    reply.items_limited = reply.items_total;

    // Show as "large tiles" to display user details everywhere.
    if (getEnhancedTiles()) {
        for (let item of reply.items ?? []) {
            if (item.display) {
                item.display.large_tile = true;
            }
        }
    }

    return reply;
}

function xhrHandleSession(reply) {
    measurementSystem = reply.bb_settings?.interface?.measurement_system ?? measurementSystem;
}

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

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

    return newItems;
}

// ---- Radar ----

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 && 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 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 all filters on reset.
    el.querySelector(".js-clear-all").addEventListener("click", e => {
        radarFilter = {};
        setRadarFilter();
        // Filter panel is recreated by default handler, recreate selections.
        setTimeout(() => replaceFilterContainer(el));
    });

    filter = el.querySelector(".filter");

    // Remove PLUS-Filter ad if no original filters are selected.
    if (filter.querySelector(".js-add-params-button.plain-text-link")) {
        filter.querySelector(".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);
            }
            // Save filter, enable reset filter button, and reload.
            setRadarFilter();
            document.querySelector(".js-clear-all").classList.remove("is-disabled");
            document.querySelector("section.js-main-stage div.js-navigation a.is-selected, div.js-nav-item").click();
        });
    }

    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", "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);

    // TODO: section = addSectionList("other");
    // filter[personal][speaks_my_languages]
    // filter[travellers_filter]
    // filter[bed_and_breakfast_filter]
}

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 continuations and non-fully-filterable views.
        if (key == "cursor" || key == "length" && value != 96) {
            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);
}

onElement(".filter-container", el => {
    replaceFilterContainer(el);
});

// ---- Profiles ----

let profileCache = {};

function cacheProfile(user) {
    // For activities, user is the activity partner.
    if (user.partner) {
        user = user.partner;
    }
    const existing = profileCache[user.name];
    const profile = {
        id: user.id,
        name: user.name,
        headline: user.headline ?? existing?.headline,
        last_login: user.last_login ?? existing?.last_login,
        location: user.location ?? existing?.location,
        online_status: user.online_status ?? existing?.online_status,
        pic: user.preview_pic?.url_token ?? existing?.pic, // not available if no picture
        personal: user.profile?.personal ?? user.personal ?? existing?.personal, // not available in activities
        sexual: user.profile?.sexual ?? user.sexual ?? existing?.sexual // not available in activities
    }
    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 getProfileAgeRange(range, short) {
    if (range) {
        return short
            ? `${range.min ?? 18}-${range.max ?? 99}`
            : translate("ageRangeValue").replace("$from", range.min ?? 18).replace("$to", range.max ?? 99);
    }
}
function getProfileBmi(height, weight, withName) {
    if (height && weight) {
        const bmi = weight / Math.pow(height / 100, 2);
        let result = `${(Math.round(bmi * 10) / 10).toFixed(1)}`;
        if (withName) {
            const name
                = bmi < 16 ? translate("bmiSevereThin")
                    : bmi < 17 ? translate("bmiModerateThin")
                        : bmi < 18.5 ? translate("bmiMildThin")
                            : bmi < 25 ? translate("bmiNormal")
                                : bmi < 30 ? translate("bmiPreObese")
                                    : bmi < 35 ? translate("bmiObese1")
                                        : bmi < 40 ? translate("bmiObese2")
                                            : translate("bmiObese3");
            result += ` / ${name}`;
        }
        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`
            : `${Math.round(height * 3.280839895) / 100} ft`;
    }
}
function getProfileWeight(weight) {
    if (weight) {
        return measurementSystem === "METRIC"
            ? `${weight}kg`
            : `${Math.round(weight * 2.20462262185)}lbs`;
    }
}

function showProfile(layer, profile) {
    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 addArray(section, key, array) {
        if (!array) {
            return;
        }
        let values = [];
        for (let i = 0; i < array.length; i++) {
            values.push(translate(array[i]));
        }
        if (values.length) {
            add(section, key, values.join(", "));
        }
    }
    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`
            : `${Math.round(distance * 0.006213712) / 10}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(" / "));
        }
    }

    // Create preview popup.
    const pane = addElement(layer, `<div id="ra_profile_wrapper">
        <div id="ra_profile_left">
            <h1>${profile.name}</h1>
            <div>${profile.headline}</div>
            <img id="ra_profile_pic" src="/assets/09114cba6c284f3a673d5f84300ab6b6.svg"></img>
        </div>
        <div id="ra_profile_right"></div>
    </div>`);
    const right = pane.querySelector("#ra_profile_right");

    // Set image.
    if (profile.pic) {
        const img = pane.querySelector("#ra_profile_pic");
        img.src = `/img/usr/${profile.pic}.jpg`;
    }

    // Set profile metadata.
    {
        const section = addSection(right, "metadata");
        addEnum(section, "onlineStatus", profile.online_status);
        add(section, "lastLogin", 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);
    }

    // Set general details.
    const personal = profile.personal;
    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));
    }

    // Set sexual details.
    const sexual = profile.sexual;
    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();
        }
    }

    // Set looking for details.
    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();
        }
    }

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

// ---- Tile UI ----

onElement(`.tile:not(div[class*="tile--loading--"]) > .reactView`, el => {
    const tile = el.closest(".tile");
    if (!tile) {
        return;
    }

    // Extract user name.
    const a = tile.querySelector("a");
    let start = a.href.indexOf("profile/");
    if (start === -1) {
        start = a.href.indexOf("hunq/");
    }
    const username = a.href.substring(start).split("/")[1];

    // Modify tile.
    const tileBar = addElement(tile, `<div class="tile__bar"></div>`);
    addHideUserAction(tileBar, tile, username);
    addShowProfileAction(tileBar, username);
    addUserTags(tile, username);
});

function addHideUserAction(tileBar, tile, username) {
    const action = addElement(tileBar, `<a class="tile__bar_action" href="#" title="${translate("hideUser")}">
        <span class="icon icon-hide-visit">
    </a>`);
    action.addEventListener("click", e => {
        e.preventDefault();
        setUserHidden(username, true);
        tile.style.display = "none";
    });
}

function addShowProfileAction(tileBar, username) {
    const action = addElement(tileBar, `<a class="tile__bar_action" title="${translate("viewProfile")}">
        <span class="icon icon-info">
    </a>`);
    action.addEventListener("click", e => {
        e.preventDefault();
        const spotlightContainer = document.querySelector("#spotlight-container");
        const layer = addElement(spotlightContainer, `<div class="layer layer--spotlight" style="top:0;z-index:100;"></div>`);
        showProfile(layer, profileCache[username]);
        layer.addEventListener("click", e => {
            if (e.target === layer) {
                layer.remove()
            }
        });
    });
}

function addUserTags(tile, username) {
    const tag = tile.querySelector(`span[class^="SpecialText-"]:last-child`);
    if (!tag) {
        return;
    }

    // Remove all existing tags.
    const tags = tag.parentNode;
    tags.replaceChildren();

    // Add user selected tags.
    function addTag(text) {
        if (text) {
            addElement(tags, `<span class="${tag.classList}">${text}</span>`);
        }
    }

    const profile = profileCache[username];

    // Add general details.
    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));
    }

    // Add sexual details.
    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));
    }
}

// ---- Messaging UI ----

onElement(".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);
});

// ---- Sidebar UI ----

function openPane(ul, link) {
    // Open pane.
    const layerContent = document.querySelector("#offcanvas-nav > .js-layer-content");
    layerContent.classList.add("is-open");
    // Replace pane contents.
    const pane = layerContent.querySelector(".js-side-content");
    pane.replaceChildren();
    addElement(pane, `
    <div class="layout layout--vertical layout--consume">
        <div class="layout-item layout-item--consume layout layout--vertical">

            <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 class="p">
                        <div class="settings__key">
                            <div>
                                <span>${translate("searchOptions")}</span>
                            </div>
                            <div class="separator separator--alt separator--narrow [ mb ] "></div>

                            <div class="layout layout--v-center">
                                <div class="layout-item [ 6/12--sm ]">
                                    <span>${translate("enhancedFilter")}</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_enhancedFilter">
                                                <label class="ui-toggle__label" for="ra_enhancedFilter" 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>
                            <div>
                                <div class="settings__description">${translate("enhancedFilterDesc")}</div>
                            </div>
                        </div>

                        <div class="settings__key">
                            <div>
                                <span>${translate("tiles")}</span>
                            </div>
                            <div class="separator separator--alt separator--narrow [ mb ] "></div>

                            <div class="layout layout--v-center">
                                <div class="layout-item [ 6/12--sm ]">
                                    <span>${translate("enhancedTiles")}</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_enhancedTiles">
                                                <label class="ui-toggle__label" for="ra_enhancedTiles" 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>
                            <div>
                                <div class="settings__description">${translate("enhancedTilesDesc")}</div>
                            </div>

                            <div class="layout layout--v-center">
                                <div class="layout-item [ 6/12--sm ]">
                                    <span>${translate("fullHeadlines")}</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_fullHeadlines">
                                                <label class="ui-toggle__label" for="ra_fullHeadlines" 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>
                            <div>
                                <div class="settings__description">${translate("fullHeadlinesDesc")}</div>
                            </div>

                            <div class="settings__key">
                                <div class="layout layout--v-center">
                                    <div class="layout-item [ 6/12--sm ]">
                                        <span>${translate("tileSizeFactor")}</span>
                                    </div>
                                    <div class="layout-item [ 6/12--sm ]">
                                        <input class="input input--block" id="ra_tileSizeFactor" type="number" min="-5" max="5"/>
                                    </div>
                                </div>
                            </div>

                            <div class="settings__key">
                                <div class="layout layout--v-center">
                                    <div class="layout-item [ 6/12--sm ]">
                                        <span>${translate("tileSizeGroupFactor")}</span>
                                    </div>
                                    <div class="layout-item [ 6/12--sm ]">
                                        <input class="input input--block" id="ra_tileSizeGroupFactor" type="number" min="-5" max="5"/>
                                    </div>
                                </div>
                            </div>

                            <div class="settings__key">
                                <div class="mb">
                                    <span>${translate("tileDetailsList")}</span>
                                </div>
                                <div class="js-grid-stats-selector">
                                    <div>
                                        <ul class="js-list tags-list tags-list--centered" id="ra_tileDetails"/>
                                    </div>
                                </div>
                            </div>
                        </div>

                        <div class="settings__key">
                            <div>
                                <span>${translate("messages")}</span>
                            </div>
                            <div class="separator separator--alt separator--narrow [ mb ] "></div>

                            <div class="layout layout--v-center">
                                <div class="layout-item [ 6/12--sm ]">
                                    <span>${translate("fullMessages")}</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_fullMessages">
                                                <label class="ui-toggle__label" for="ra_fullMessages" 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>
                            <div>
                                <div class="settings__description">${translate("fullMessagesDesc")}</div>
                            </div>

                            <div class="layout layout--v-center">
                                <div class="layout-item [ 6/12--sm ]">
                                    <span>${translate("typingNotifications")}</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_typingNotifications">
                                                <label class="ui-toggle__label" for="ra_typingNotifications" 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>
                            <div>
                                <div class="settings__description">${translate("typingNotificationsDesc")}</div>
                            </div>

                            <div class="layout layout--v-center">
                                <div class="layout-item [ 6/12--sm ]">
                                    <span>${translate("sendEnter")}</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_sendEnter">
                                                <label class="ui-toggle__label" for="ra_sendEnter" 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>
                            <div>
                                <div class="settings__description">${translate("sendEnterDesc")}</div>
                            </div>
                        </div>

                        <div class="settings__key">
                            <div>
                                <span>${translate("hiddenUsers")}</span>
                            </div>
                            <div class="separator separator--alt separator--narrow [ mb ] "></div>

                            <div class="layout layout--v-center">
                                <div class="layout-item [ 6/12--sm ]">
                                    <span>${translate("hideMessages")}</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_hideMessages">
                                                <label class="ui-toggle__label" for="ra_hideMessages" 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>
                            <div>
                                <div class="settings__description">${translate("hideMessagesDesc")}</div>
                            </div>

                            <div class="layout layout--v-center">
                                <div class="layout-item [ 6/12--sm ]">
                                    <span>${translate("hideActivities")}</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_hideActivities">
                                                <label class="ui-toggle__label" for="ra_hideActivities" 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>
                            <div>
                                <div class="settings__description">${translate("hideActivitiesDesc")}</div>
                            </div>

                            <div class="layout layout--v-center">
                                <div class="layout-item [ 6/12--sm ]">
                                    <span>${translate("hideLikes")}</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_hideLikes">
                                                <label class="ui-toggle__label" for="ra_hideLikes" 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>
                            <div>
                                <div class="settings__description">${translate("hideLikesDesc")}</div>
                            </div>

                            <div class="settings__key">
                                <div class="layout layout--v-center">
                                    <div class="layout-item [ 6/12--sm ]">
                                        <span>${translate("minAge")}</span>
                                    </div>
                                    <div class="layout-item [ 6/12--sm ]">
                                        <input class="input input--block" id="ra_hiddenMinAge" type="number" min="18" max="99"/>
                                    </div>
                                </div>
                            </div>

                            <div class="settings__key">
                                <div class="layout layout--v-center">
                                    <div class="layout-item [ 6/12--sm ]">
                                        <span>${translate("maxAge")}</span>
                                    </div>
                                    <div class="layout-item [ 6/12--sm ]">
                                        <input class="input input--block" id="ra_hiddenMaxAge" type="number" min="18" max="99"/>
                                    </div>
                                </div>
                            </div>

                            <div class="settings__key">
                                <div class="mb">
                                    <span>${translate("hiddenUsersList")}</span>
                                </div>
                                <div class="js-grid-stats-selector">
                                    <div>
                                        <ul class="js-list tags-list tags-list--centered" id="ra_hiddenUsers"/>
                                    </div>
                                </div>
                            </div>
                        </div>
                    </div>
                </div>
            </div>
        </div>
    </div>`);

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

    // Handle tile settings.
    const inEnhancedTiles = pane.querySelector("#ra_enhancedTiles");
    inEnhancedTiles.checked = getEnhancedTiles();
    inEnhancedTiles.addEventListener("change", e => setEnhancedTiles(e.target.checked));

    const inFullHeadlines = pane.querySelector("#ra_fullHeadlines");
    inFullHeadlines.checked = getFullHeadlines();
    inFullHeadlines.addEventListener("change", e => setFullHeadlines(e.target.checked));

    const inEnhancedFilter = pane.querySelector("#ra_enhancedFilter");
    inEnhancedFilter.checked = getEnhancedFilter();
    inEnhancedFilter.addEventListener("change", e => setEnhancedFilter(e.target.checked));

    const inTileSizeFactor = pane.querySelector("#ra_tileSizeFactor");
    inTileSizeFactor.value = getTileSizeFactor();
    inTileSizeFactor.addEventListener("change", e => setTileSizeFactor(parseInt(e.target.value)));

    const inTileSizeGroupFactor = pane.querySelector("#ra_tileSizeGroupFactor");
    inTileSizeGroupFactor.value = getTileSizeGroupFactor();
    inTileSizeGroupFactor.addEventListener("change", e => setTileSizeGroupFactor(parseInt(e.target.value)));

    const inTileDetails = pane.querySelector("#ra_tileDetails");
    for (const tileDetail of ["age", "height", "weight", "bmi", "ageRange",
        "bodyHair", "bodyType", "ethnicity", "relationship", "analPosition",
        "dick", "saferSex", "dirty", "sm", "fisting"]) {
        addTag(inTileDetails, tileDetail, translate(tileDetail), tileDetails.has(tileDetail), e => setTileDetail(e.tag, e.checked));
    }

    // Handle message settings.
    const inFullMessages = pane.querySelector("#ra_fullMessages");
    inFullMessages.checked = getFullMessages();
    inFullMessages.addEventListener("change", e => setFullMessages(e.target.checked));

    const inTypingNotifications = pane.querySelector("#ra_typingNotifications");
    inTypingNotifications.checked = getTypingNotifications();
    inTypingNotifications.addEventListener("change", e => setTypingNotifications(e.target.checked));

    const inSendEnter = pane.querySelector("#ra_sendEnter");
    inSendEnter.checked = getSendEnter();
    inSendEnter.addEventListener("change", e => setSendEnter(e.target.checked));

    // Handle hidden interacctions.
    const inHideMessages = pane.querySelector("#ra_hideMessages");
    inHideMessages.checked = getHideMessages();
    inHideMessages.addEventListener("change", e => setHideMessages(e.target.checked));

    const inHideActivities = pane.querySelector("#ra_hideActivities");
    inHideActivities.checked = getHideActivities();
    inHideActivities.addEventListener("change", e => setHideActivities(e.target.checked));

    const inHideLikes = pane.querySelector("#ra_hideLikes");
    inHideLikes.checked = getHideLikes();
    inHideLikes.addEventListener("change", e => setHideLikes(e.target.checked));

    // Handle hidden age.
    const inMinAge = pane.querySelector("#ra_hiddenMinAge");
    const inMaxAge = pane.querySelector("#ra_hiddenMaxAge");
    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);
        }
    });

    // Handle hidden user list.
    const hiddenUsers = pane.querySelector("#ra_hiddenUsers");
    const users = Array.from(getHiddenUsers()).sort(Intl.Collator().compare);
    for (const user of users) {
        addTag(hiddenUsers, user, user, true, e => setUserHidden(e.tag, e.checked));
    };
}

onElement("li.js-settings > div.accordion > ul", 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(() => openPane(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"));
        }
    }
});

onElement(`div[class^="Version--"]`, el => {
    el.innerHTML += `<br>
        <a id="version" href="https://greasyfork.org/en/scripts/419514" target="blank">${GM_info.script.name} ${GM_info.script.version}</a>`;
});

// ---- On Load ----

(function () {
    "use strict";

    setFullHeadlines(getFullHeadlines());
    setTileSizeFactor(getTileSizeFactor());
    setFullMessages(getFullMessages());
    radarFilter = getRadarFilter();
    tileDetails = getTileDetails();

    proxyXhr();
})();