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

// ==UserScript==
// @name           Romeo Additions
// @name:de        Romeo Additions
// @namespace      https://greasyfork.org/en/users/723211-ray/
// @version        2.14.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"
    },
    "typingNotifications": {
        "de": "Tippbenachrichtigungen",
        "en": "Typing notifications"
    },
    "typingNotificationsDesc": {
        "de": "Wenn deaktiviert, können Empfängern 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 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 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", function(e) {
        if (!getTypingNotifications() && e.keyCode !== 13/*ENTER*/) {
            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>

                    <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 inTypingNotificatiosn = $("#ra_typingNotifications");
            inTypingNotificatiosn.prop("checked", getTypingNotifications());
            inTypingNotificatiosn.on("change", e => {
                setTypingNotifications(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);
});