Romeo Additions

Allows to hide users, display their information on tiles, and enhances the Radar.

ของเมื่อวันที่ 10-06-2023 ดู เวอร์ชันล่าสุด

คุณจะต้องติดตั้งส่วนขยาย เช่น Tampermonkey, Greasemonkey หรือ Violentmonkey เพื่อติดตั้งสคริปต์นี้

คุณจะต้องติดตั้งส่วนขยาย เช่น Tampermonkey หรือ Violentmonkey เพื่อติดตั้งสคริปต์นี้

คุณจะต้องติดตั้งส่วนขยาย เช่น Tampermonkey หรือ Violentmonkey เพื่อติดตั้งสคริปต์นี้

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.

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

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

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        5.0.5
// @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-y: 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;
}

/* 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;
    }
}

/* profile preview */
#ra_profile_wrapper {
    background-color: black;
    display: grid;
    grid-template-rows: min-content auto;
    height: 100%;
}
#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, 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;
}
@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%;
    }
}

/* context menu */
#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, .ra_context_li:active {
    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;
    }
}

#version {
    color: white;
    display: block;
}
`);

// ---- Script ----

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

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

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 onElement(selector, callback) {
    waitForKeyElements(selector, callback, false);
}

function showImage(src) {
    const spotlightContainer = document.querySelector("#spotlight-container");
    const layer = addElement(spotlightContainer, `<div class="layer layer--spotlight" style="top:0;z-index:100;"></div>`);
    addElement(layer, `<img src="${src}"></img>`).addEventListener("click", e => layer.remove());
    layer.addEventListener("click", e => {
        if (e.target === layer) {
            layer.remove()
        }
    });
}

function showProfileInfo(username) {
    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.querySelector(".js-back").addEventListener("click", e => layer.remove());
    layer.addEventListener("click", e => {
        if (e.target === layer) {
            layer.remove()
        }
    });
}

// ---- 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"
    },
    bedAndBreakfast: {
        en: "Bed & Breakfast"
    },
    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."
    },
    hideContacts: {
        de: "Auch Kontakte verstecken",
        en: "Also hide contacts"
    },
    hideContactsDesc: {
        de: "Versteckt ausgeblendete Benutzer auch in den Kontakten.",
        en: "Removes hidden users even in contacts."
    },
    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"
    },
    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"
    },
    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"
    },
    speakingMyLanguage: {
        de: "Spricht meine Sprache",
        en: "Speaking my language"
    },
    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 anzeigen",
        en: "View full image"
    },
    viewProfile: {
        de: "Profilvorschau anzeigen",
        en: "View profile prevew"
    },
    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 getHideContacts() {
    return load("hideContacts", false);
}
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 setHideContacts(value) {
    save("hideContacts", 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":
            case "profiles/popular":
                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 "contacts":
                    if (reply.cursors) {
                        reply.items = xhrProcessUserItems(reply.items, x => x.profile, getHideContacts());
                    }
                    break;
                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":
                case "profiles/popular":
                    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;
}

// ---- Filter ----

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

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 bookmark action if not available.
    const save = el.querySelector(".js-filter-actions .js-save");
    if (save && save.classList.contains("is-plus")) {
        save.parentNode.remove();
    }

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

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

    // 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) {
        const el = addElement(ul, `
            <div class="filter__group">
                <div class="js-fulltext-input filter__group--fulltext">
                    <div class="Container--f_nVe layout layout--v-center">
                        <div class="layout-item layout-item--consume">
                            <input class="js-input Input--9tGCI input" autocorrect="off" autocapitalize="off" spellcheck="false">
                        </div>
                    </div>
                </div>
            </div>`);
        return el.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", "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();
    });

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

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

// ---- 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 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="#">
                    <span class="js-back-icon icon icon-back icon-large"></span>
                </a>
                <div class="layer-header__title">
                    <h2>${profile.name}</h2>
                </div>
            </div>
        </div>
        <div class="layout-item p l-hidden-sm">
            <div class="js-title typo-section-navigation">${profile.name}</div>
        </div>
        <div id="ra_profile_content">
            <div id="ra_profile_left">
                <div>${escapeHtml(profile.headline)}</div>
                <img id="ra_profile_pic" src="/assets/3e6f78fd4e864c6071e6fa65d3d0c679c48b84a95dee9f232a2e86303c4ed5a1.svg"></img>
            </div>
            <div id="ra_profile_right"></div>
        </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>`);
    }
}

// ---- Tiles ----

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

    // Add tags.
    const username = getUsernameFromHref(tile.querySelector("a").href);
    addUserTags(tile, username);
});

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

    // Remove all existing tags.
    const tags = lastTag.parentNode;
    const newClassList = tags.firstChild.classList;
    const isNew = newClassList.value != lastTag.classList.value;
    tags.replaceChildren();

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

    if (isNew) {
        addTag(translate("new")).classList = newClassList;
    }

    const profile = profileCache[username];

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

    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 ----

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

// ---- Context Menu ----

let contextMenuBg;
let contextMenuUl;
let contextMenuX;
let contextMenuY;
const contextMenuHandlers = {
    // radar > contact tile
    ".js-contact": el => {
        const username = getUsernameFromHref(el.href);
        showContextMenu(
            { icon: "search", text: "viewProfile", onclick: () => showProfileInfo(username) },
            {
                icon: "hide-visit", text: "hideUser", onclick: () => {
                    setUserHidden(username, true);
                    if (getHideContacts())
                        el.parentNode.style.display = "none";
                }
            }
        );
    },
    // radar > user tile
    ".search-results__item .tile:not(div[class*='tile--loading--'])": el => {
        const a = el.querySelector("a");
        const username = getUsernameFromHref(a.href);
        showContextMenu(
            { icon: "search", text: "viewProfile", onclick: () => showProfileInfo(username) },
            {
                icon: "hide-visit", text: "hideUser", onclick: () => {
                    setUserHidden(username, true);
                    if (el.classList.contains("tile--plus")) {
                        el.closest(".search-results__item").style.display = "none";
                    } else {
                        el.style.display = "none";
                    }
                }
            }
        );
    },
    // radar > small user tile
    ".search-results__item .tile[class*='tile--loading--']": el => {
        const small = el.querySelector("div.SMALL");
        showContextMenu(
            { icon: "search", text: "viewFullImage", onclick: () => showImage(getFullImageUrl(getBackgroundImageUrl(small.style.backgroundImage))) }
        );
    },

    // visitors > user tile
    ".visitors .tile:not(div[class*='tile--loading--'])": el => {
        const a = el.querySelector("a");
        const username = getUsernameFromHref(a.href);
        showContextMenu(
            { icon: "search", text: "viewProfile", onclick: () => showProfileInfo(username) },
            {
                icon: "hide-visit", text: "hideUser", onclick: () => {
                    setUserHidden(username, true);
                    el.style.display = "none";
                }
            }
        );
    },

    // messages > message > sent image
    ".js-chat .reactView img": el => {
        showContextMenu(
            { icon: "search", text: "viewFullImage", onclick: () => showImage(getFullImageUrl(el.src)) }
        );
    },
    // messages > message
    ".js-chat .reactView": el => {
        const a = el.querySelector("a");
        const username = getUsernameFromHref(a.href);
        showContextMenu(
            { icon: "search", text: "viewProfile", onclick: () => showProfileInfo(username) },
            {
                icon: "hide-visit", text: "hideUser", onclick: () => {
                    setUserHidden(username, true)
                    if (getHideMessages())
                        el.style.display = "none";
                }
            }
        );
    },

    // contacts > contact
    ".js-contacts .reactView": el => {
        const a = el.querySelector("a");
        const username = getUsernameFromHref(a.href);
        showContextMenu(
            { icon: "search", text: "viewProfile", onclick: () => showProfileInfo(username) },
            {
                icon: "hide-visit", text: "hideUser", onclick: () => {
                    setUserHidden(username, true);
                    if (getHideContacts())
                        el.style.display = "none";
                }
            }
        );
    },

    // activities > activity > liked image
    "img.thumbnail": el => {
        showContextMenu(
            { icon: "search", text: "viewFullImage", onclick: () => showImage(getFullImageUrl(el.src)) }
        );
    },
    // activities > activity
    ".js-stream .js-list .listitem": el => {
        const a = el.querySelector("a");
        const username = getUsernameFromHref(a.href);
        showContextMenu(
            { icon: "search", text: "viewProfile", onclick: () => showProfileInfo(username) },
            {
                icon: "hide-visit", text: "hideUser", onclick: () => {
                    setUserHidden(username, true);
                    if (getHideActivities())
                        el.style.display = "none";
                }
            }
        );
    }
};

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

    // Create context menu.
    contextMenuUl = addElement(document.body, "<ul id='ra_context_ul'></ul>")
    //contextMenuUl.addEventListener("mouseleave", e => hideContextMenu());

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

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

function showContextMenu() {
    contextMenuBg.style.display = "block";

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

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

function hideContextMenu() {
    contextMenuBg.style.display = "none";
    contextMenuUl.style.display = "none";
}

// ---- Sidebar ----

function openPane(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");
        addElement(section, `
            <div>
                <div class="settings__description">${translate(desc)}</div>
            </div>`);
        return input;
    }
    function addNumber(section, text, min, max) {
        return addElement(section, `
            <div class="settings__key">
                <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 ]">
                        <input class="input input--block" type="number" min="${min}" max="${max}"/>
                    </div>
                </div>
            </div>`).querySelector("input");
    }
    function addTagList(section, text) {
        return addElement(section, `
            <div class="settings__key">
                <div class="mb">
                    <span>${translate(text)}</span>
                </div>
                <div class="js-grid-stats-selector">
                    <div>
                        <ul class="js-list tags-list tags-list--centered"/>
                    </div>
                </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 search options section.
    const searchOptionsSection = addSection("searchOptions");

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

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

    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 tileSizeFactor = addNumber(tilesSection, "tileSizeFactor", -5, 5);
    tileSizeFactor.value = getTileSizeFactor();
    tileSizeFactor.addEventListener("change", e => setTileSizeFactor(parseInt(e.target.value)));

    const tileSizeGroupFactor = addNumber(tilesSection, "tileSizeGroupFactor", -5, 5);
    tileSizeGroupFactor.value = getTileSizeGroupFactor();
    tileSizeGroupFactor.addEventListener("change", e => setTileSizeGroupFactor(parseInt(e.target.value)));

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

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

    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", "hideContactsDesc");
    hideContacts.checked = getHideContacts();
    hideContacts.addEventListener("change", e => setHideContacts(e.target.checked));

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

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

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

    const inMinAge = addNumber(hiddenUsersSection, "hiddenMinAge", 18, 99);
    const inMaxAge = addNumber(hiddenUsersSection, "hiddenMaxAge", 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);
        }
    });

    const hiddenUsersList = addTagList(hiddenUsersSection, "hiddenUsersList");
    const users = Array.from(getHiddenUsers()).sort(Intl.Collator().compare);
    for (const user of users) {
        addTag(hiddenUsersList, 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 += `<a id="version" href="https://greasyfork.org/en/scripts/419514" target="blank">${GM_info.script.name} ${GM_info.script.version}</a>`;
});

// ---- Load ----

(function () {
    "use strict";

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

    proxyXhr();
})();