// ==UserScript==
// @name Romeo Additions
// @name:de Romeo Additions
// @namespace https://greasyfork.org/en/users/723211-ray/
// @version 2.12
// @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 */
#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": "Zeige 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"
},
"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 getHideActivities() {
return localStorage.getItem(settingNs + "hideActivities") == "true";
}
function getHideMessages() {
return localStorage.getItem(settingNs + "hideMessages") == "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 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() {
// 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 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 = 0;
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);
}
// ---- 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>
<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 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);
});