Romeo Additions

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

Version vom 22.12.2022. Aktuellste Version

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

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

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

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

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

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

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

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

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

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

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

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

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

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

// ==UserScript==
// @name           Romeo Additions
// @name:de        Romeo Additions
// @namespace      https://greasyfork.org/en/users/723211-ray/
// @version        2.15.0
// @description    Allows to hide users, display their information on tiles, and enhances the Radar.
// @description:de Ermöglicht das Verstecken von Benutzern, die Anzeige ihrer Details auf Kacheln, und verbessert den Radar.
// @author         -Ray-, Djamana
// @include        *://*.romeo.com/*
// @grant          GM_addStyle
// @require        https://code.jquery.com/jquery-3.6.0.slim.min.js
// @require        https://greasyfork.org/scripts/383527-wait-for-key-elements/code/Wait_for_key_elements.js
// @license        MIT
// ==/UserScript==

// ==== CSS ====

GM_addStyle(`
#visits > .layer__container--wider { width:unset; max-width:1227px; }

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

.js-romeo-badge { display:none; } /* hide Plus user icon (it is faked for enhanced tiles */
div[data-testid='desktop-image'] { background-image: none; } /* hide models on login page */
#visits div[class*='UnlockMoreVisitorsGrid'] { display: none; } /* hide PLUS message at bottom of visitor grid */
#messenger div[class*='TruncateBlock__Content-sc-'] { -webkit-line-clamp: unset; } /* show full messages */
`);

// ==== Script ====

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

// ---- Language ----

const _strings = {
    "display": {
        "de": "Anzeige",
        "en": "Display"
    },
    "enhancedTiles": {
        "de": "Erweiterte Kacheln",
        "en": "Enhanced tiles"
    },
    "enhancedTilesDesc": {
        "de": "Zeigt alle Benutzerdetails auf den Kacheln. Im Radar wird dies Benutzer mit großen Kacheln darstellen.",
        "en": "Shows all user details on tiles. The radar will display users with large tiles."
    },
    "extensionTitle": {
        "en": "Romeo Additions"
    },
    "hiddenUsers": {
        "de": "Ausgeblendete Benutzer",
        "en": "Hidden users"
    },
    "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."
    },
    "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"
    },
    "maxAge": {
        "de": "Maximales Alter",
        "en": "Maximal age"
    },
    "minAge": {
        "de": "Minimales Alter",
        "en": "Minimal age"
    },
    "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."
    },
    "typingNotifications": {
        "de": "Tippbenachrichtigungen",
        "en": "Typing notifications"
    },
    "typingNotificationsDesc": {
        "de": "Wenn deaktiviert, können Empfänger nicht mehr sehen, dass eine Nachricht verfasst wird.",
        "en": "If disabled, receivers can no longer see that a message is being composed."
    },
    "viewFullImage": {
        "de": "Bild vergrößern",
        "en": "View full image"
    },
}

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() {
    const value = localStorage.getItem(settingNs + "enhancedTiles");
    return value === null ? true : value == "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 getHideActivities() {
    return localStorage.getItem(settingNs + "hideActivities") == "true";
}

function getHideMessages() {
    return localStorage.getItem(settingNs + "hideMessages") == "true";
}

function getSendEnter() {
    const value = localStorage.getItem(settingNs + "sendEnter");
    return value === null ? true : value == "true";
}

function getTypingNotifications() {
    const value = localStorage.getItem(settingNs + "typingNotifications");
    return value === null ? true : value == "true";
}

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 setHideActivities(value) {
    localStorage.setItem(settingNs + "hideActivities", value);
}

function setHideMessages(value) {
    localStorage.setItem(settingNs + "hideMessages", value);
}

function setSendEnter(value) {
    localStorage.setItem(settingNs + "sendEnter", value);
}

function setTypingNotifications(value) {
    localStorage.setItem(settingNs + "typingNotifications", 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 filterUser(user, hiddenMaxAge, hiddenMinAge, hiddenNames) {
    return user.personal.age >= hiddenMinAge
        && user.personal.age <= hiddenMaxAge
        && !hiddenNames.includes(user.name)
}

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

function proxyXhr() {
    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 verb = getApiVerb(url);
                const isJson = verb !== "stream" && typeof this.response === "string";
                let reply = isJson ? JSON.parse(this.response) : this.response;
                //console.log("[RA] XHR reply " + verb + "\n", reply);

                // Modify interesting data.
                switch (verb) {
                    case "messages/conversations":
                        reply = xhrHideMessages(reply);
                        break;
                    case "notifications/activity-stream":
                        reply = xhrHideActivities(reply);
                        break;
                    case "profiles":
                    case "visitors":
                    case "visits":
                        reply = xhrRestorePlusVisit(reply);
                        reply = xhrEnhanceUsers(reply);
                        break;
                }

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

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

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

function xhrHideMessages(reply) {
    if (!getHideMessages()) {
        return reply;
    }
    // Remove hidden users.
    const hiddenMaxAge = getHiddenMaxAge();
    const hiddenMinAge = getHiddenMinAge();
    const hiddenNames = getHiddenUsers();
    reply.items = reply.items.filter(x => filterUser(x.chat_partner, hiddenMaxAge, hiddenMinAge, hiddenNames));
    return reply;
}

function xhrEnhanceUsers(reply) {
    // Remove 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 xhrRestorePlusVisit(reply) {
    // Restore PLUS-visible visitors.
    reply.items_limited = reply.items_total;
    return reply;
}

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

waitForKeyElements(".tile > .reactView", jNode => {
    const tile = jNode.parent(".tile")[0];
    // Ignore placeholder tiles.
    for (const cls of tile.classList) {
        if (cls.startsWith("tile--loading--")) {
            return;
        }
    }
    // Extract user name from link.
    const a = tile.querySelector("a");
    const username = a.href.substring(a.href.indexOf("profile/")).split("/")[1];
    // Extract user avatar from link div.
    const div = a.firstChild;
    const divImg = window.getComputedStyle(div).getPropertyValue("background-image");
    const imgUrl = divImg.substring(divImg.indexOf('"') + 1, divImg.lastIndexOf('"'));
    // Add action bar.
    const tileBar = $("<div class='tile__bar'></div>").appendTo(tile);
    addHideUserAction(tileBar, tile, username);
    addShowImageAction(tileBar, imgUrl);
});

function addShowImageAction(tileBar, url) {
    if (url.endsWith(".svg")) { // ignore "no photo" placeholders
        return;
    }
    const fileName = url.substring(url.lastIndexOf("/") + 1);
    const origUrl = "/img/usr/original/0x0/" + fileName;
    $("<a class='tile__bar_action' href='" + origUrl + "' title='" + getString("viewFullImage") + "'><span class='icon icon-picture'></a>")
        .on("click", e => {
            e.preventDefault();
            const body = $("#spotlight-container");
            const layer = $(`<div class='layer layer--spotlight' style='top:0;z-index:100;'>
                <img src='` + origUrl + `'></img>
            </div>`);
            layer.on("click", e => layer.remove());
            layer.appendTo(body);
        })
        .appendTo(tileBar);
}

function addHideUserAction(tileBar, tile, username) {
    $("<a class='tile__bar_action' href='#' title='" + getString("hideUser") + "'><span class='icon icon-hide-visit'></a>")
        .on("click", e => {
            e.preventDefault();
            setUserHidden(username, true);
            tile.style.display = "none";
        })
        .appendTo(tileBar);
}

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

waitForKeyElements(".js-send-region.layout-item > div", jNode => {
    const sendRegionEl = jNode[0];
    sendRegionEl.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);
});

// ---- Settings UI ----

waitForKeyElements("li.js-settings > div.accordion > ul", jNode => {
    let itemClass = jNode.find("a").attr("class");
    $("<li><div><a class='" + itemClass + "'>" + getString("extensionTitle") + "</a></div></li>")
        .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(`
<div class='layout layout--vertical layout--consume'>
    <div class='layout-item layout-item--consume layout layout--vertical'>

        <div class='layout-item settings__navigation p l-hidden-sm'>
            <div class='js-title typo-section-navigation'>` + getString("extensionTitle") + `</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 class="layout layout--v-center">
                            <div class="layout-item [ 6/12--sm ]">
                                <span>` + getString("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">` + getString("enhancedTilesDesc") + `</div>
                        </div>

                        <div class="layout layout--v-center">
                            <div class="layout-item [ 6/12--sm ]">
                                <span>` + getString("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">` + getString("typingNotificationsDesc") + `</div>
                        </div>

                        <div class="layout layout--v-center">
                            <div class="layout-item [ 6/12--sm ]">
                                <span>` + getString("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">` + getString("sendEnterDesc") + `</div>
                        </div>
                    </div>

                    <div class="settings__key">
                        <div>
                            <span>` + getString("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>` + getString("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">` + getString("hideMessagesDesc") + `</div>
                        </div>

                        <div class="layout layout--v-center">
                            <div class="layout-item [ 6/12--sm ]">
                                <span>` + getString("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">` + getString("hideActivitiesDesc") + `</div>
                        </div>

                        <div class="settings__key">
                            <div class="layout layout--v-center">
                                <div class="layout-item [ 6/12--sm ]">
                                    <span>` + getString("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>` + getString("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="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>`);
            // Handle enhanced user tiles.
            let inEnhancedTiles = $("#ra_enhancedTiles");
            inEnhancedTiles.prop("checked", getEnhancedTiles());
            inEnhancedTiles.on("change", e => {
                setEnhancedTiles(e.target.checked);
            });
            // Handle typing notifications.
            let inTypingNotifications = $("#ra_typingNotifications");
            inTypingNotifications.prop("checked", getTypingNotifications());
            inTypingNotifications.on("change", e => {
                setTypingNotifications(e.target.checked);
            });
            // Handle send enter.
            let inSendEnter = $("#ra_sendEnter");
            inSendEnter.prop("checked", getSendEnter());
            inSendEnter.on("change", e => {
                setSendEnter(e.target.checked);
            });
            // Handle hidden interactions.
            let inHideMessages = $("#ra_hideMessages");
            inHideMessages.prop("checked", getHideMessages());
            inHideMessages.on("change", e => {
                setHideMessages(e.target.checked);
            });
            let inHideActivities = $("#ra_hideActivities");
            inHideActivities.prop("checked", getHideActivities());
            inHideActivities.on("change", e => {
                setHideActivities(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 = $("<li class='tags-list__item'/>").appendTo(ul);
                $("<a class='js-tag ui-tag ui-tag--removable ui-tag--selected' href='#'><span class='ui-tag__label'>" + item + "</span></a>")
                    .on("click", e => {
                        setUserHidden(e.target.innerHTML, false);
                        $(e.target).closest(".tags-list__item").css("display", "none");
                    })
                    .appendTo(li);
            };
        })
        .appendTo(jNode);
});