Romeo Additions

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

< Feedback on Romeo Additions

Review: OK - script works, but has bugs

§
Posted: 2021-08-23

Hi Ray,
please update the script with the following version.
Major change is now it'll also work on romeo.com.
Other thing the 'cosmetic' fixed were improper implemented and never showed any effects.
I changed that now. see function xhrPoorMensPlus()
Best regards
Djamana

P.s.
EDIT: Only copy&paste the lines till
// ---- Settings UI ----
And keep the 'old part'
I didn't changed anything beyond that.
However the HTML gets interpreted, instead of just showing it, what messes up things.
...or just use this:
https://pastebin.com/ijxZGVPD


// ==UserScript==
// @name           Romeo Additions
// @namespace      https://greasyfork.org/en/users/723211-ray/
// @version        2.2
// @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
// @include        *://*.romeo.com/*
// @grant          GM_addStyle
// @require        https://code.jquery.com/git/jquery-git.slim.min.js
// @_require        https://code.jquery.com/jquery-3.6.0.slim.min.js
// ==/UserScript==

// ==== Dependencies ====

/*! waitForKeyElements | https://gist.github.com/BrockA/2625891 */
/*--- waitForKeyElements():  A utility function, for Greasemonkey scripts,
    that detects and handles AJAXed content.
    Usage example:
        waitForKeyElements (
            "div.comments"
            , commentCallbackFunction
        );
        //--- Page-specific function to do what we want when the node is found.
        function commentCallbackFunction (jNode) {
            jNode.text ("This comment changed by waitForKeyElements().");
        }
    IMPORTANT: This function requires your script to have loaded jQuery.
*/
function waitForKeyElements(
    selectorTxt,    /* Required: The jQuery selector string that
                        specifies the desired element(s).
                    */
    actionFunction, /* Required: The code to run when elements are
                        found. It is passed a jNode to the matched
                        element.
                    */
    bWaitOnce,      /* Optional: If false, will continue to scan for
                        new elements even after the first match is
                        found.
                    */
    iframeSelector  /* Optional: If set, identifies the iframe to
                        search.
                    */
) {
    var targetNodes, btargetsFound;

    if (typeof iframeSelector == "undefined")
        targetNodes = $(selectorTxt);
    else
        targetNodes = $(iframeSelector).contents()
            .find(selectorTxt);

    if (targetNodes && targetNodes.length > 0) {
        btargetsFound = true;
        /*--- Found target node(s).  Go through each and act if they
            are new.
        */
        targetNodes.each(function () {
            var jThis = $(this);
            var alreadyFound = jThis.data('alreadyFound') || false;

            if (!alreadyFound) {
                //--- Call the payload function.
                var cancelFound = actionFunction(jThis);
                if (cancelFound)
                    btargetsFound = false;
                else
                    jThis.data('alreadyFound', true);
            }
        });
    }
    else {
        btargetsFound = false;
    }

    //--- Get the timer-control variable for this selector.
    var controlObj = waitForKeyElements.controlObj || {};
    var controlKey = selectorTxt.replace(/[^\w]/g, "_");
    var timeControl = controlObj[controlKey];

    //--- Now set or clear the timer as appropriate.
    if (btargetsFound && bWaitOnce && timeControl) {
        //--- The only condition where we need to clear the timer.
        clearInterval(timeControl);
        delete controlObj[controlKey]
    }
    else {
        //--- Set a timer, if needed.
        if (!timeControl) {
            timeControl = setInterval(function () {
                waitForKeyElements(selectorTxt,
                    actionFunction,
                    bWaitOnce,
                    iframeSelector
                );
            },
                300
            );
            controlObj[controlKey] = timeControl;
        }
    }
    waitForKeyElements.controlObj = controlObj;
}

// ==== CSS ====

GM_addStyle(`
#visits > .layer__container--wider { width:unset; max-width:1227px; }
div[class*='tile--loading--'] .tile__image { background-image:url(/assets/05c2dc53b86dcd7abdb1d8a50346876b.svg); }

.tile__bar { position:absolute; bottom:0; right:0; visibility:hidden; }
.tile__bar_action { background:rgba(0,0,0,0.4); backdrop-filter: blur(3px); display: inline-block; color:white; margin-left: 1px; padding: 0.25rem 0.45rem; }
.tile__bar_action:hover { background-color:#00A3E4; }
.tile__bar_action:active { background-color:#06648B; }
.tile__link:hover .tile__bar { visibility:visible; }
`);

// ==== Script ====

(function () {
    'use strict';
    proxyXhr();
})();

// ---- Language ----

const _strings = {
    "display": {
        "de": "Anzeige",
        "en": "Display"
    },
    "enhancedTiles": {
        "de": "Erweiterte Kacheln",
        "en": "Enhanced tiles"
    },
    "enhancedTilesDesc": {
        "de": "Zeige alle Benutzerdetails auf den Kacheln. Im Radar wird dies Benutzer als 'Plus'-Abonnenten mit großen Kacheln darstellen.",
        "en": "Shows all user details on tiles. The radar will claim users to be 'Plus' subscribers in large tiles."
    },
    "extensionTitle": {
        "en": "Romeo Additions"
    },
    "hiddenUsers": {
        "de": "Ausgeblendete Benutzer",
        "en": "Hidden users"
    },
    "hideUser": {
        "de": "Benutzer ausblenden",
        "en": "Hide user"
    },
    "maxAge": {
        "de": "Maximales Alter",
        "en": "Maximal age"
    },
    "minAge": {
        "de": "Minimales Alter",
        "en": "Minimal age"
    },
    "viewFullImage": {
        "de": "Bild vergrößern",
        "en": "View full image"
    },
}

function getString(key) {
    const lang = document.documentElement.getAttribute("lang") || "en";
    const translations = _strings[key];
    if (translations)
        return translations[lang] || translations["en"] || "%" + key + "%";
    return "%" + key + "%";
}

// ---- Settings ----

const settingNs = "RA_SETTINGS:";

function getEnhancedTiles() {
    return localStorage.getItem(settingNs + "enhancedTiles") == "true";
}

function getHiddenMaxAge() {
    return parseInt(localStorage.getItem(settingNs + "hiddenMaxAge")) || 99;
}

function getHiddenMinAge() {
    return parseInt(localStorage.getItem(settingNs + "hiddenMinAge")) || 18;
}

function getHiddenUsers() {
    return JSON.parse(localStorage.getItem(settingNs + "hiddenUsers")) || [];
}

function setEnhancedTiles(value) {
    localStorage.setItem(settingNs + "enhancedTiles", value);
}

function setHiddenMaxAge(value) {
    localStorage.setItem(settingNs + "hiddenMaxAge", value);
}

function setHiddenMinAge(value) {
    localStorage.setItem(settingNs + "hiddenMinAge", value);
}

function setUserHidden(username, hide) {
    let hiddenUsers = getHiddenUsers();
    if (hide) {
        if (hiddenUsers.length < hiddenUsers.push(username)) {
            hiddenUsers.sort((a, b) => a.toLowerCase().localeCompare(b.toLowerCase()));
            localStorage.setItem(settingNs + "hiddenUsers", JSON.stringify(hiddenUsers));
        }
    } else {
        const prevLength = hiddenUsers.length;
        hiddenUsers = hiddenUsers.filter(e => e != username);
        if (prevLength > hiddenUsers.length)
            localStorage.setItem(settingNs + "hiddenUsers", JSON.stringify(hiddenUsers));
    }
}

// ---- XHR ----

function ReMaskDash(RE_WithDashes) {
    return RE_WithDashes
        .replaceAll("/","\\/")
}

function isApiRequest(url, verb) {
    // Request must start with "/api/v?/" or "/api/+/" followed by the given verb.
    //const matches = url.match(/\/api\/(v[0-9]|\+)\//);
    const matches = url.match(ReMaskDash("/api/(v[0-9]|\\+)/"));
    if (matches && matches.length)
        return url.startsWith(verb, matches[0].length);
    return false;
}

function filterUser(user, hiddenMaxAge, hiddenMinAge, hiddenNames) {
    return user.personal.age >= hiddenMinAge
        && user.personal.age <= hiddenMaxAge
        && !hiddenNames.includes(user.name)
}

function proxyXhr() {
    // Intercept XHR queries and replies by hooking the XHR open method.
    const realOpen = window.XMLHttpRequest.prototype.open;
    window.XMLHttpRequest.prototype.open = function (method, url, async, user, password) {
        this.addEventListener("load", () => {
            //console.log("[RA] XHR reply: method=" + method + ", url=" + url);
            try {
                // Parse data.
                const isString = typeof this.response === "string";
                reply = isString ?
                    JSON.parse(this.response) :
                               this.response;

                // Modify interesting data.
                if ( isApiRequest(url, "notifications") ) {
                    reply = xhrHideNotifications(reply);
                }
                if (
                     isApiRequest(url, "profiles") ||
                     isApiRequest(url, "visitors") ||
                     isApiRequest(url, "visits")
                   ) {
                    reply = xhrRestorePlusVisit( reply );
                    reply = xhrEnhanceUsers    ( reply );

                }
                if ( isApiRequest(url, "session") ) {
                    reply = xhrPoorMensPlus( reply );
                }


                // Write back possibly modified data.
                Object.defineProperty(this, "responseText", { writable: true });
                this.responseText = isString ? JSON.stringify(reply) : reply;
            } catch (e) {
                //console.log("[RA] XHR handler failed: " + e)
            }
        });

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

function xhrHideNotifications(reply) {
    // Remove manually hidden users.
    const hiddenMaxAge = getHiddenMaxAge();
    const hiddenMinAge = getHiddenMinAge();
    const hiddenNames = getHiddenUsers();
    return reply.filter(x => filterUser(x.partner, hiddenMaxAge, hiddenMinAge, hiddenNames));
}

function xhrEnhanceUsers(reply) {
    // Remove manually hidden users.
    const enhancedTiles = getEnhancedTiles();
    const hiddenMaxAge = getHiddenMaxAge();
    const hiddenMinAge = getHiddenMinAge();
    const hiddenNames = getHiddenUsers();
    let newItems = [];
    for (let item of reply.items) {
        if (filterUser(item, hiddenMaxAge, hiddenMinAge, hiddenNames)) {
            // Show as "large tiles" to display user details everywhere.
            if (enhancedTiles)
                item.display.large_tile = true;
            newItems.push(item);
        }
    }
    reply.items = newItems;
    return reply;
}


function xhrPoorMensPlus(reply) {
    // Cosmetic patch #1
    if (!reply.is_plus) {
        reply.is_plus = true
        reply.is_free_plus = true // maybe not needed
        reply.payment_group = "PLUS"
    }
    // Cosmetic patch #2
    if (reply.inferface) {
        reply.show_plus_badge = true // maybe not needed
        reply.show_ads = false  // maybe not needed
    }
    // Cosmetic patch #3
    if (reply.show_plus_badge) {
        reply.show_plus_badge = true // maybe not needed
    }
    return reply;
}
function xhrRestorePlusVisit(reply) {
    // Restore PLUS-visible visitors.
    reply.items_limited = 0;
    return reply;
}

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

waitForKeyElements(
    "a.tile__link, " +
    "div.js-profiles a.listresult, " +
    "div.js-wrapper a.tile__link > div.tile__image, " +
    "#visits a.listresult", jNode => {
        // Determine tile properties.
        const tile = jNode.parent(".tile");
        const tileLink = tile.children(".tile__link").first();
        if (!tileLink) // ignore placeholders
            return;
        // Add full headline as tooltip.
        const tileInfo = tileLink.children(".tile__info").first();
        const tileHeadline = tileInfo.children(".tile__headline").first();
        tileHeadline.attr("title", tileHeadline.text());
        // Add action bar.
        const tileImage = tileLink.children(".tile__image").first();
        const username = tileImage.attr("aria-label");
        const tileBar = $("
").appendTo(tileLink); addShowImageAction(tileBar, tileImage); addHideUserAction(tileBar, tile, username); }); function addShowImageAction(tileBar, tileImage) { const style = tileImage.attr("style"); if (!style) return; const url = style.substring(style.lastIndexOf("/") + 1, style.lastIndexOf(")")); if (url.endsWith(".svg")) // ignore "no photo" placeholders return; const origUrl = "/img/usr/original/0x0/" + url; $("") .on("click", e => { e.preventDefault(); window.open(origUrl, "_blank"); }) .appendTo(tileBar); } function addHideUserAction(tileBar, tile, username) { $("") .on("click", e => { e.preventDefault(); setUserHidden(username, true); tile.css("display", "none"); }) .appendTo(tileBar); } // ---- Settings UI ---- waitForKeyElements("li.js-settings > div.accordion > ul", jNode => { let itemClass = jNode.find("a").attr("class"); $("
  • ") .on("click", e => { // Force open the setting pane and clear any existing contents. $("#offcanvas-nav > .js-layer-content").addClass("is-open"); const pane = $(".js-side-content"); pane.empty(); // Add pane and list. pane.append(`
    ` + getString("extensionTitle") + `
    ` + getString("enhancedTiles") + `
    ` + getString("enhancedTilesDesc") + `
    ` + getString("hiddenUsers") + `
    ` + getString("minAge") + `
    ` + getString("maxAge") + `
    `); // Handle enhanced user tiles. let inEnhancedTiles = $("#ra_enhancedTiles"); inEnhancedTiles.prop("checked", getEnhancedTiles()); inEnhancedTiles.on("change", e => { setEnhancedTiles(e.target.checked); }); // Handle hidden age. let minAge = getHiddenMinAge(); let maxAge = getHiddenMaxAge(); let inMinAge = $("#ra_hiddenMinAge"); let inMaxAge = $("#ra_hiddenMaxAge"); inMinAge.val(minAge); inMaxAge.val(maxAge); inMinAge.on("change", e => { minAge = parseInt(e.target.value); setHiddenMinAge(minAge); if (minAge > maxAge) { maxAge = minAge; setHiddenMaxAge(maxAge); inMaxAge.val(maxAge); } }); inMaxAge.on("change", e => { maxAge = parseInt(e.target.value); setHiddenMaxAge(maxAge); if (maxAge < minAge) { minAge = maxAge; setHiddenMinAge(minAge); inMinAge.val(minAge); } }); // Handle hidden user list. const ul = $("#ra_hiddenUsers"); for (const item of getHiddenUsers()) { const li = $("
  • ").appendTo(ul); $("" + item + "") .on("click", e => { setUserHidden(e.target.innerHTML, false); $(e.target).closest(".tags-list__item").css("display", "none"); }) .appendTo(li); }; }) .appendTo(jNode); });
  • -Ray-Author
    §
    Posted: 2023-06-11

    Sorry, couldn't see your reply until now. The script has substantially changed by now, so I guess this is obsolete. Thanks though.

    Post reply

    Sign in to post a reply.