Romeo Additions

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

נכון ליום 10-06-2023. ראה הגרסה האחרונה.

// ==UserScript==
// @name           Romeo Additions
// @name:de        Romeo Additions
// @namespace      https://greasyfork.org/en/users/723211-ray/
// @version        5.0.4
// @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"))
                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");
    // 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="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 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("hideContacts")}</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_hideContacts">
                                                <label class="ui-toggle__label" for="ra_hideContacts" 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("hideContactsDesc")}</div>
                            </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 interactions.
    const inHideContacts = pane.querySelector("#ra_hideContacts");
    inHideContacts.checked = getHideContacts();
    inHideContacts.addEventListener("change", e => setHideContacts(e.target.checked));

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