Skip to content

Instantly share code, notes, and snippets.

@Enzime
Created January 22, 2026 16:47
Show Gist options
  • Select an option

  • Save Enzime/7bf2dbd88d4776bafaa09c23196eaad6 to your computer and use it in GitHub Desktop.

Select an option

Save Enzime/7bf2dbd88d4776bafaa09c23196eaad6 to your computer and use it in GitHub Desktop.
// ==UserScript==
// @name Element - Hide Activity-Only Notifications
// @namespace https://github.com/Enzime
// @match https://app.element.io/*
// @grant none
// @version 1.0
// @author Enzime
// @description Only show unread indicators for "Mentions and keywords" rooms when mentioned
// @license MIT
// @run-at document-start
// @top-level-await
// ==/UserScript==
// Room notification indicators in Element:
// - _unread_* (dot only): Activity level, no notification - HIDE
// - _unread-counter_*: Has count badge (DMs, regular messages) - KEEP
// - SVG @ icon + counter: Mentions/highlights - KEEP
// === Helpers ===
function waitForElement(selector, timeout = 10000) {
return new Promise((resolve, reject) => {
const element = document.querySelector(selector);
if (element) {
resolve(element);
return;
}
const observer = new MutationObserver(() => {
const element = document.querySelector(selector);
if (element) {
observer.disconnect();
resolve(element);
}
});
observer.observe(document.documentElement, {
childList: true,
subtree: true,
});
setTimeout(() => {
observer.disconnect();
reject(new Error(`Timeout waiting for ${selector}`));
}, timeout);
});
}
// === CSS: Hide only the dot indicator, keep counter badges ===
const styles = `
/* Hide unread dot indicators (Activity level) */
/* Match _unread_ but NOT _unread-counter_ */
.mx_RoomListItemView [class^='_unread_']:not([class*='counter']),
.mx_RoomListItemView [class*=' _unread_']:not([class*='counter']),
.mx_RoomTile [class^='_unread_']:not([class*='counter']),
.mx_RoomTile [class*=' _unread_']:not([class*='counter']),
.mx_NotificationBadge_dot {
display: none !important;
}
/* Hide unread dots on spaces (not counters) */
.mx_SpaceButton [class^='_unread_']:not([class*='counter']),
.mx_SpaceButton [class*=' _unread_']:not([class*='counter']),
.mx_SpacePanel [class^='_unread_']:not([class*='counter']),
.mx_SpacePanel [class*=' _unread_']:not([class*='counter']),
.mx_SpaceButton .mx_NotificationBadge_dot,
.mx_SpacePanel .mx_NotificationBadge_dot {
display: none !important;
}
`;
function injectStyles() {
const style = document.createElement("style");
style.id = "element-suppress-unread";
style.textContent = styles;
if (document.head) {
document.head.appendChild(style);
} else {
document.addEventListener("DOMContentLoaded", () => {
document.head.appendChild(style);
});
}
}
// === MutationObserver: Remove bold class from room tiles ===
function hasCounterOrMention(roomTile) {
// Use aria-label which reliably indicates unread status
const ariaLabel = roomTile.getAttribute("aria-label") || "";
if (ariaLabel.includes("unread message") || ariaLabel.includes("unread mention")) {
return true;
}
// Fallback: Check for counter badge (_unread-counter_*)
if (roomTile.querySelector("[class*='_unread-counter']")) return true;
// Check for old-style badges
if (roomTile.querySelector(".mx_NotificationBadge_level_notification, .mx_NotificationBadge_level_highlight")) {
return true;
}
return false;
}
function checkAndRemoveBold(target) {
const roomTile = target.closest(".mx_RoomListItemView, .mx_RoomTile") || target;
if (!hasCounterOrMention(roomTile)) {
target.classList.remove("mx_RoomListItemView_bold");
}
}
function processElement(element) {
const boldTiles = [
...element.querySelectorAll(".mx_RoomListItemView_bold"),
];
if (element.classList?.contains("mx_RoomListItemView_bold")) {
boldTiles.push(element);
}
boldTiles.forEach(checkAndRemoveBold);
}
async function setupRoomTileObserver() {
const container = await waitForElement("[class*='LeftPanel'], .mx_LeftPanel, #matrixchat");
const observer = new MutationObserver((mutations) => {
for (const mutation of mutations) {
if (mutation.type === "attributes" && mutation.attributeName === "class") {
const target = mutation.target;
if (target.classList?.contains("mx_RoomListItemView_bold")) {
checkAndRemoveBold(target);
}
}
if (mutation.type === "childList") {
for (const node of mutation.addedNodes) {
if (node.nodeType === Node.ELEMENT_NODE) {
processElement(node);
}
}
}
}
});
observer.observe(container, {
childList: true,
subtree: true,
attributes: true,
attributeFilter: ["class"],
});
processElement(container);
}
// === Title Observer: Remove asterisk from page title ===
function cleanTitle() {
const title = document.title;
if (title.includes("*")) {
// Format: "Element * | room name" -> "Element | room name"
const cleaned = title.replace(" * ", " ");
if (cleaned !== title) {
document.title = cleaned;
}
}
}
async function setupTitleObserver() {
const head = await waitForElement("head");
cleanTitle();
const observer = new MutationObserver(cleanTitle);
observer.observe(head, {
childList: true,
subtree: true,
characterData: true,
});
const titleEl = document.querySelector("title");
if (titleEl) {
observer.observe(titleEl, {
childList: true,
characterData: true,
subtree: true,
});
}
}
// === Initialize ===
injectStyles(); // Synchronous - inject CSS immediately
await Promise.all([
setupRoomTileObserver(),
setupTitleObserver(),
]);
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment