Created
January 22, 2026 16:47
-
-
Save Enzime/7bf2dbd88d4776bafaa09c23196eaad6 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| // ==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