Romeo Additions

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

Від 27.12.2022. Дивіться остання версія.

You will need to install an extension such as Tampermonkey, Greasemonkey 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 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.

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

(У мене вже є менеджер скриптів, дайте мені встановити його!)

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        3.0.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
// @match          *://*.romeo.com/*
// @license        MIT
// ==/UserScript==

// ==== Dependencies ====

// ---- https://github.com/CoeJoder/GM_wrench ----

function addCss(css) {
    var style = document.createElement('style');
    style.type = 'text/css';
    // @ts-ignore
    if (style.styleSheet) {
        // @ts-ignore
        style.styleSheet.cssText = css;
    } else {
        style.appendChild(document.createTextNode(css));
    }
    document.getElementsByTagName('head')[0].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(`
.js-romeo-badge { display:none; } /* hide Plus user icon (it is faked for enhanced tiles */
#messenger div[class^="TruncateBlock__Content-sc-"] { -webkit-line-clamp: unset; } /* show full messages */
#visits > .layer__container--wider { width:unset; max-width:1227px; } /* wider visitor list */
#visits div[class^="UnlockMoreVisitorsGrid"] { display: none; } /* hide PLUS message at bottom of visitor grid */
div[data-testid="desktop-image"] { background-image: none; } /* hide models on login page */

.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; }
#version { color: white; }
`);

// ==== Script ====

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

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

function onElement(selector, callback) {
    waitForKeyElements(selector, callback, false, 500);
}

// ---- 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 translate(key) {
    const lang = document.documentElement.getAttribute("lang") || "en";
    const translations = _strings[key];
    return translations
        ? translations[lang] || translations.en || "%" + key + "%"
        : "%" + key + "%";
}

// ---- Settings ----

const settingsNs = "RA_SETTINGS:";

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 getEnhancedTiles() {
    return load("enhancedTiles", 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", false);
}
function getHideMessages() {
    return load("hideMessages", false);
}
function getSendEnter() {
    return load("sendEnter", true);
}
function getTypingNotifications() {
    return load("typingNotifications", true);
}
function setEnhancedTiles(value) {
    save("enhancedTiles", value);
}
function setHiddenMaxAge(value) {
    save("hiddenMaxAge", value);
}
function setHiddenMinAge(value) {
    save("hiddenMinAge", value);
}
function setHideActivities(value) {
    save( "hideActivities", value);
}
function setHideMessages(value) {
    save("hideMessages", value);
}
function setSendEnter(value) {
    save("sendEnter", 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 filterUser(user, hiddenMaxAge, hiddenMinAge, hiddenNames) {
    return (!user.personal
            || user.personal.age >= hiddenMinAge
            && user.personal.age <= hiddenMaxAge)
        && !hiddenNames.has(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);
            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;
        });

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

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

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

function xhrEnhanceUsers(reply) {
    if (!reply.items) {
        return;
    }
    // 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) {
                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 ----

onElement(`.tile:not(div[class*="tile--loading--"]) > .reactView`, el => {
    const tile = el.closest(".tile");
    if (!tile) {
        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 = addElement(tile, `<div class="tile__bar"></div>`);
    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;
    const action = addElement(tileBar, `<a class="tile__bar_action" href="${origUrl}" title="${translate("viewFullImage")}">
        <span class="icon icon-picture">
    </a>`);
    action.addEventListener("click", e => {
        e.preventDefault();
        const spotlightContainer = document.querySelector("#spotlight-container");
        const layer = addElement(spotlightContainer, `<div class="layer layer--spotlight" style="top:0;z-index:100;">
            <img src="${origUrl}"></img>
        </div>`);
        layer.addEventListener("click", e => layer.remove());
    });
}

function addHideUserAction(tileBar, tile, username) {
    const action = addElement(tileBar, `<a class="tile__bar_action" href="#" title="${translate("hideUser")}">
        <span class="icon icon-hide-visit">
    </a>`);
    action.addEventListener("click", e => {
        e.preventDefault();
        setUserHidden(username, true);
        tile.style.display = "none";
    });
}

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

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

// ---- Sidebar UI ----

onElement(`div[class^="Version--"]`, el => {
    el.innerHTML += `<br><a id="version" href="https://greasyfork.org/en/scripts/419514" target="blank">Romeo Additions 3.0.0</a>`;
});

onElement("li.js-settings > div.accordion > ul", el => {
    const linkClass = el.querySelector("a").className;
    const link = addElement(el, `<li><div><a class="${linkClass}">${translate("extensionTitle")}</a></div></li>`);
    link.addEventListener("click", e => {
        // Force open the setting pane and clear any existing contents.
        document.querySelector("#offcanvas-nav > .js-layer-content").classList.add("is-open");
        const pane = document.querySelector(".js-side-content");
        pane.replaceChildren();
        // Add pane and list.
        addElement(pane, `
<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">${translate("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>${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("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("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="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="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.
        const inEnhancedTiles = pane.querySelector("#ra_enhancedTiles");
        inEnhancedTiles.checked = getEnhancedTiles();
        inEnhancedTiles.addEventListener("change", e => setEnhancedTiles(e.target.checked));
        // Handle typing notifications.
        const inTypingNotifications = pane.querySelector("#ra_typingNotifications");
        inTypingNotifications.checked = getTypingNotifications();
        inTypingNotifications.addEventListener("change", e => setTypingNotifications(e.target.checked));
        // Handle send enter.
        const inSendEnter = pane.querySelector("#ra_sendEnter");
        inSendEnter.checked = getSendEnter();
        inSendEnter.addEventListener("change", e => setSendEnter(e.target.checked));
        // Handle hidden interactions.
        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));
        // 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 ul = pane.querySelector("#ra_hiddenUsers");
        const users = Array.from(getHiddenUsers()).sort(Intl.Collator().compare);
        for (const user of users) {
            const li = addElement(ul, `<li class="tags-list__item">
                <a class="js-tag ui-tag ui-tag--removable ui-tag--selected" href="#">
                    <span class="ui-tag__label">${user}</span>
                </a>
            </li>`);
            li.querySelector("a").addEventListener("click", e => {
                setUserHidden(e.target.innerHTML, false);
                e.target.closest(".tags-list__item").style.display = "none";
            });
        };
    });
});