Skip to content

Instantly share code, notes, and snippets.

@cw2k
Last active December 24, 2022 17:09
Show Gist options
  • Save cw2k/02723601c1422e5fd15adf6fcb70161b to your computer and use it in GitHub Desktop.
Save cw2k/02723601c1422e5fd15adf6fcb70161b to your computer and use it in GitHub Desktop.
Romeo Additions
// ==UserScript==
// @name myRomeo Additions
// @name:de myRomeo Additions
// @namespace https://greasyfork.org/en/users/723211-ray/
// @version 2.2.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/git/jquery-3.x-git.slim.min.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 2.2"
},
"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 (Nach Änderungen bitte die Seite Neuladen.)",
"en": "Hide user ( Please reload page manually to make changes take effect.)"
},
"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"
},
}
getFilename = (url) => url.split("/").at(-1)
getString = (key) => {
// Get current language
const lang = document.documentElement.getAttribute("lang") || "en"
// Select translation keyword
const translations = _strings[key]
// return translation - fallback#1 English translation
// fallback#2 example: %viewFullImage%
return !translations ? "%" + key + "% not in translationList" :
translations[lang] ||
translations.en ||
"%" + key + "% has no entries"
}
// ---- Settings ----
const settingNs = "RA_SETTINGS:";
nullIsTrue = expr => (expr === null) ? true : expr == "true"
nullIsFalse = expr => (expr === null) ? false : expr == "true"
getEnhancedTiles =() => nullIsTrue( localStorage.getItem(settingNs + "enhancedTiles") )
getHiddenMaxAge =() => parseInt( localStorage.getItem(settingNs + "hiddenMaxAge")) || 99
getHiddenMinAge =() => parseInt( localStorage.getItem(settingNs + "hiddenMinAge")) || 18
getHiddenUsers =() => JSON.parse( localStorage.getItem(settingNs + "hiddenUsers")) || []
getHideActivities =() => nullIsFalse( localStorage.getItem(settingNs + "hideActivities") )
getHideMessages =() => nullIsFalse( localStorage.getItem(settingNs + "hideMessages") )
getSendEnter =() => nullIsTrue( localStorage.getItem(settingNs + "sendEnter") )
getTypingNotifications =() => nullIsTrue( localStorage.getItem(settingNs + "typingNotifications") )
setEnhancedTiles = value => localStorage.setItem(settingNs + "enhancedTiles", value)
setHiddenMaxAge = value => localStorage.setItem(settingNs + "hiddenMaxAge", value)
setHiddenMinAge = value => localStorage.setItem(settingNs + "hiddenMinAge", value)
setHideActivities = value => localStorage.setItem(settingNs + "hideActivities", value)
setHideMessages = value => localStorage.setItem(settingNs + "hideMessages", value)
setSendEnter = value => localStorage.setItem(settingNs + "SendEnter", value)
setTypingNotifications = value => localStorage.setItem(settingNs + "typingNotifications", value)
setHiddenUsers = value => localStorage.setItem(settingNs + "hiddenUsers", JSON.stringify(value) )
arrayRemove = (arr, value) => arr.filter(el => el != value)
function setUserHidden(username, hide) {
let hiddenUsers = getHiddenUsers();
if (hide) {
// add item (no duplicates)
if ( hiddenUsers.includes(username) ) return // item already in list
hiddenUsers.push(username)
hiddenUsers.sort( (a, b) =>
a.toLowerCase()
.localeCompare(
b.toLowerCase()
));
} else {
// remove item
if ( !hiddenUsers.includes(username) ) return // item not in list
hiddenUsers = arrayRemove ( hiddenUsers, username );
}
setHiddenUsers(hiddenUsers);
}
// ---- XHR ----
filteredCount = 0
filterUser = user => {
try {
// deleted User do not offer age
if (user.deletion_date)
isAgeOkay = true
else
isAgeOkay =
user.personal.age >= getHiddenMinAge()
&& user.personal.age <= getHiddenMaxAge()
} catch (e) {
console.log("[RA] filterUser failed: " + e)
isAgeOkay = true
}
isUserOkay = !getHiddenUsers().includes(user.name)
// console.log ( user.name +
// " isAgeOkay:" + isAgeOkay +
// " isUserOkay:" + isUserOkay )
isOkay = isAgeOkay && isUserOkay
if (!isOkay) filteredCount +=1
return isOkay
}
function getApiVerb(url) {
// Extract verb in "/api/v#/verb?" or "/api/+/verb?" .
const match = url.match(
new RegExp( ""
+ "(?<=/api/[\\w+]*/)" // match should starts with "/api/v#" ( positive lookbehind )
+ "[\\w/-]*" // match a word and '-' and '/'
+ "(?=/?)" // match should end with "?" ( positive lookahead )
)
);
if (match)
return match[0];
return undefined;
}
// Sets Text on how many items where filtered as Tooltip ('Title')
function showFilterstatus( What, WhereToPut ) {
if ( !filteredCount ) return
var msg = getString("extensionTitle") + ": " + filteredCount + " " + What + " filtered."
console.log ("RA_"+ What +": " + msg )
waitForKeyElements(WhereToPut, jNode => {
jNode[0].title = msg } )
}
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 isString = typeof this.response === "string";
if ( !isString ) return
if ( url.includes("/stream") ) return
const verb = getApiVerb(url);
if ( !verb ) return
let reply = JSON.parse(this.response);
console.log("[RA] XHR verb '" + verb + "' reply:\n", reply);
filteredCount = 0
// Modify interesting data.
switch (verb) {
case "messages/conversations":
if (getHideMessages())
xhrHideMessages( reply )
showFilterstatus ( "conversations", "#messenger .refreshable" )
break
case "notifications/activity-stream":
if (getHideActivities())
reply = xhrHideActivities( reply )
showFilterstatus ( "activities", ".stream" )
break
case "session":
// Fake Plus to Romeo client to supress promoting plus
reply.is_plus = true
break
case "visitors":
xhrRestorePlusVisit( reply )
xhrEnhanceUsers( reply )
showFilterstatus ( "visitors", "#visits-received .refreshable" )
break
case "profiles":
case "visits":
xhrEnhanceUsers( reply )
showFilterstatus ( "visits", "#visits-made .refreshable" )
break;
default:
return
}
// Write back possibly modified data.
Object.defineProperty(this, "responseText", { writable: true });
this.responseText = JSON.stringify(reply);
} catch (e) {
console.log("[RA] XHR handler failed: " + e)
}
});
// Forward to client.
return realOpen.apply(this, arguments);
}
}
xhrHideActivities = reply =>
// Remove hidden users.
reply
.filter(x => filterUser( x.partner) )
xhrHideMessages = reply =>
// Remove hidden users.
reply.items = reply.items
.filter(x => filterUser( x.chat_partner ) )
xhrEnhanceUsers = reply => {
// Remove hidden users.
reply.items = reply.items
.filter(x => filterUser( x ) )
// Show as "large tiles" to display user details everywhere.
if ( getEnhancedTiles() )
reply.items
.map( item => item.display.large_tile = true )
}
xhrRestorePlusVisit = reply =>
// Restore PLUS-visible visitors.
delete reply.items_limited
// ---- Tile UI ----
waitForKeyElements(".tile > .reactView", jNode => {
const tile = jNode.parent(".tile")[0];
// Ignore placeholder tiles.
var tileIsLoading = false
tile.classList.forEach( cls =>
tileIsLoading |= cls.startsWith("tile--loading--") )
if (tileIsLoading) return
// Extract user name from link.
// Example: 'https://www.romeo.com/profile/USERFooBar'
const userlink = tile.querySelector("a");
const username = getFilename (userlink.href) ;
// Extract user avatar from link div.
// Example: 'url("https://www.romeo.com/assets/09114cba6c284f3a673d5f84300ab6b6.svg")'
const div = userlink.firstChild;
const divImg = window.getComputedStyle(div).getPropertyValue("background-image");
const imgUrl = divImg.split('"')[1];
// Add action bar.
const tileBar = $("<div class='tile__bar'></div>").appendTo(tile);
addHideUserAction( tileBar, tile, username);
addShowImageAction( tileBar, imgUrl);
});
// Add showuserimage icon to tile
function addShowImageAction(tileBar, url) {
if (url.endsWith(".svg")) return; // ignore "no photo" placeholders
const origUrl = "/img/usr/original/0x0/" + getFilename( url );
$("<a class='tile__bar_action' href='" + origUrl + "' title='" +
getString("viewFullImage") + "'><span class='icon icon-picture'></a>")
.on("click", e => {
debugger
//prevent open url
e.preventDefault();
// Open image in div on top of the others ( z = 100)
$(`<div class='layer layer--spotlight' style='top:0;z-index:100;'>
<img src='` + origUrl + `'></img>
</div>`)
.on("click", e => e.currentTarget.remove() )
.appendTo( $("#spotlight-container") );
})
.appendTo( tileBar );
}
// Add hideuser icon to tile
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);
//$(e.target).parent().parent().hide();
$(tile).hide();
})
.appendTo(tileBar);
}
// ---- Messaging UI ----
// Prevent site event handler from sending message or typing notifications.
waitForKeyElements(".js-send-region.layout-item > div", jNode => {
//TODO: Check is that is really working.
// before it was done via
// jNode[0].addEventListener("keydown", fnEnter, useCapture=true)
// useCapture: indicates whether events of this type will be dispatched to the registered
// listener before being dispatched to any EventTarget beneath it in the DOM tree.
// https://developer.mozilla.org/en-US/docs/Web/API/EventTarget/addEventListener?retiredLocale=de#parameters
jNode.on("keydown", e => {
const isEnter = e.key === 'Enter';
const send = isEnter && ( getSendEnter() || e.ctrlKey );
const allow = send || getTypingNotifications() && !isEnter;
if (!allow) {
e.stopPropagation();
}
} );
});
// ---- Settings UI ----
waitForKeyElements("li.js-settings > div.accordion > ul", jNode => {
// ^^- find Setting
// UL li:=Romeo= / li:About us / li:Help & Support
// SampleData: 'Item--2oX6- txt-truncate'
let itemClass = jNode.find("a").attr("class");
// Make top menu item for Romeo Additions
$("<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>`);
UI_addCheckBoxHandlers = (CssSelector, fnGetvalue, fnSetValue ) =>
$( CssSelector )
.prop("checked", fnGetvalue() )
.on("change", e =>
fnSetValue( e.target.checked )
);
// Handle enhanced user tiles.
UI_addCheckBoxHandlers ( "#ra_enhancedTiles", getEnhancedTiles, setEnhancedTiles );
// Handle typing notifications.
UI_addCheckBoxHandlers ( "#ra_typingNotifications", getTypingNotifications, setTypingNotifications );
// Handle send enter.
UI_addCheckBoxHandlers ( "#ra_sendEnter", getSendEnter, setSendEnter );
// Handle hidden interactions.
UI_addCheckBoxHandlers ( "#ra_hideMessages", getHideMessages, setHideMessages );
UI_addCheckBoxHandlers ( "#ra_hideActivities", getHideActivities, setHideActivities );
// Handle hidden age.
let minAge = getHiddenMinAge();
let maxAge = getHiddenMaxAge();
let inMinAge = $("#ra_hiddenMinAge")
.val(minAge)
.on("change", e => {
minAge = parseInt(e.target.value);
setHiddenMinAge(minAge);
// Handle min-max overlapse
if (minAge > maxAge) {
maxAge = minAge;
setHiddenMaxAge(maxAge);
inMaxAge.val(maxAge);
}
});
let inMaxAge = $("#ra_hiddenMaxAge")
.val(maxAge)
.on("change", e => {
maxAge = parseInt(e.target.value);
setHiddenMaxAge(maxAge);
// Handle min-max overlapse
if (maxAge < minAge) {
minAge = maxAge;
setHiddenMinAge(minAge);
inMinAge.val(minAge);
}
});
// Handle hidden user list.
const ul = $("#ra_hiddenUsers");
getHiddenUsers().forEach( item => {
// Create new li > a > span
$("<li class='tags-list__item'/>" +
"<a class='js-tag ui-tag ui-tag--removable ui-tag--selected' href='#'>" +
"<span class='ui-tag__label'>" + item + "</span></a></li>")
.on("click", e => {
const username = $(e.target).text();
setUserHidden( username , false);
// Remove in user list
// Note: 'currentTarget' is the element to which the event handler has been attached to
// => so here it's the li
$(e.currentTarget).hide();
})
.appendTo(ul);
}); //forEach
}) // on click
.appendTo(jNode);
});
/////////////////////////////
// Additional library
// Copy and pasted from here;
// @-require https://greasyfork.org/scripts/383527-wait-for-key-elements/code/Wait_for_key_elements.js
// @-source https://www.torn.com/forums.php#/p=threads&f=67&t=16100245&b=0&a=0
// since it also includes TinyMCE which is not needed
// ==/UserScript==
/*--- 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;
}
@cw2k
Copy link
Author

cw2k commented Dec 24, 2022

Based on version 2.15 of Romeo Additions I did applied a plenty refactoring and optimization.
You may try revisions to compare and see the changes.

As new functionality I add a status message when items got filtered.
It is shown via tooltip (title) when you hover over the concerning area.

Btw what is still missing is to show the version of Romeo Additions somewhere.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment