Last active
January 27, 2026 12:42
-
-
Save maxtheaxe/04304ba7caf25d55fb94748818c5819f 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 collab email helper | |
| // @version 0.33 | |
| // @grant none | |
| // @match https://mail.google.com/* | |
| // @run-at document-idle | |
| // @downloadURL https://gist.github.com/maxtheaxe/04304ba7caf25d55fb94748818c5819f/raw/a04400197abeda9aa815c8f4ae1efb510732ee61/collab-email.user.js | |
| // @updateURL https://gist.github.com/maxtheaxe/04304ba7caf25d55fb94748818c5819f/raw/a04400197abeda9aa815c8f4ae1efb510732ee61/collab-email.user.js | |
| // ==/UserScript== | |
| (() => { | |
| 'use strict'; | |
| const LABEL_PREFIX = 'collab/'; | |
| const TARGET_BUTTON_TEXT = 'Reply all'; | |
| const DEBOUNCE_MS = 150; | |
| // threadId -> WeakSet<HTMLElement> | |
| const processedByThread = new Map(); | |
| function getThreadPermId() { | |
| // In thread view Gmail commonly renders the subject as h2 with data-thread-perm-id | |
| const h2 = document.querySelector('div[role="main"] h2[data-thread-perm-id]'); | |
| return h2?.getAttribute('data-thread-perm-id') ?? null; | |
| } | |
| function findReplyAllSpan() { | |
| const xpath = `//button[normalize-space(.)="${TARGET_BUTTON_TEXT}"]`; | |
| return document | |
| .evaluate(xpath, document, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null) | |
| .singleNodeValue; | |
| } | |
| function findState() { | |
| const threadPermId = getThreadPermId(); | |
| if (!threadPermId) return null; | |
| const labelEl = document.querySelector(`[aria-label^="Remove label ${LABEL_PREFIX}"]`); | |
| if (!labelEl || !labelEl.isConnected) return null; | |
| const replyAllSpan = findReplyAllSpan(); | |
| if (!replyAllSpan || !replyAllSpan.isConnected) return null; | |
| return { threadPermId, labelEl, replyAllSpan }; | |
| } | |
| function apply({ threadPermId, replyAllSpan }) { | |
| let set = processedByThread.get(threadPermId); | |
| if (!set) { | |
| set = new WeakSet(); | |
| processedByThread.set(threadPermId, set); | |
| } | |
| // If we already styled THIS exact node for THIS thread, don't do it again. | |
| if (set.has(replyAllSpan)) return; | |
| set.add(replyAllSpan); | |
| replyAllSpan.style.border = '2px solid lime'; | |
| } | |
| let runTimer = null; | |
| function debouncedCheck() { | |
| if (runTimer) clearTimeout(runTimer); | |
| runTimer = setTimeout(() => { | |
| runTimer = null; | |
| const state = findState(); | |
| if (state) apply(state); | |
| }, DEBOUNCE_MS); | |
| } | |
| const obs = new MutationObserver(debouncedCheck); | |
| obs.observe(document.documentElement, { childList: true, subtree: true }); | |
| debouncedCheck(); | |
| })(); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment