The problem with google chat is... wait let me just list the things google got right with google chat as that list will be shorter: . One of the problems with google chat is the Home screen which could have been a great single place to view new messages was ruined because google won't add a mark all as read button. Wouldn't it be great if you had a single pane of new messages, and not see messages from 2023?
Well wait no more, here is a Greasemonkey script that will mark all the message in Home as read so you can actually use Home to monitor chat. The real question is if I could do this in a few minutes, why couldn't google?
// ==UserScript==
// @name Google Chat – Mark All as Read
// @namespace https://chat.google.com/
// @version 2.0
// @description Adds a floating button that marks every unread conversation as read
// @author analog
// @match https://chat.google.com/*
// @grant none
// @run-at document-idle
// ==/UserScript==
(function () {
'use strict';
// ── helpers ──────────────────────────────────────────────────────────────
const sleep = ms => new Promise(r => setTimeout(r, ms));
async function waitFor(predicate, timeout = 2000, interval = 80) {
const deadline = Date.now() + timeout;
while (Date.now() < deadline) {
const result = predicate();
if (result) return result;
await sleep(interval);
}
return null;
}
// ── unread-item detection ─────────────────────────────────────────────────
// Confirmed from live DOM inspection: data-is-unread="true" is the stable
// attribute Google Chat uses on every unread conversation/thread card.
function getUnreadItems() {
return [...document.querySelectorAll('[data-is-unread="true"]')];
}
// ── click / hover helpers ─────────────────────────────────────────────────
// Full mouse-event sequence with real coordinates for jsaction delegation.
function jsClick(el) {
const rect = el.getBoundingClientRect();
const x = rect.width > 0 ? rect.left + rect.width / 2 : 1;
const y = rect.height > 0 ? rect.top + rect.height / 2 : 1;
for (const type of ['mousedown', 'mouseup', 'click']) {
el.dispatchEvent(new MouseEvent(type, {
bubbles: true, cancelable: true, view: window,
clientX: x, clientY: y,
button: 0, buttons: type === 'click' ? 0 : 1,
}));
}
}
// PointerEvents (modern standard) + MouseEvents to trigger jsaction hover
// handlers that reveal the inline action buttons.
function jsHover(el) {
const rect = el.getBoundingClientRect();
const x = rect.left + rect.width / 2;
const y = rect.top + rect.height / 2;
for (const type of ['pointerover', 'pointerenter', 'pointermove',
'mouseover', 'mouseenter', 'mousemove']) {
const Cls = type.startsWith('pointer') ? PointerEvent : MouseEvent;
el.dispatchEvent(new Cls(type, {
bubbles: true, cancelable: true, view: window,
clientX: x, clientY: y,
}));
}
}
function closeMenu() {
document.dispatchEvent(new KeyboardEvent('keydown', { key: 'Escape', bubbles: true }));
}
// ── mark a single item as read ────────────────────────────────────────────
async function markItemAsRead(row) {
row.scrollIntoView({ block: 'center' });
await sleep(150);
// Strategy 1 ─────────────────────────────────────────────────────────────
// data-item="mark-thread-as-read" and "mark-as-read" are stable jsaction
// identifiers. These buttons may be present in the DOM (even if visually
// hidden); .click() fires on hidden elements.
const dataItemBtn = row.querySelector(
'[data-item="mark-thread-as-read"], [data-item="mark-as-read"]'
);
if (dataItemBtn) {
console.log('[mark-all-read] S1: data-item button found, clicking');
dataItemBtn.click();
await sleep(300);
return true;
}
// Strategy 2 ─────────────────────────────────────────────────────────────
// Trigger hover events so jsaction reveals the inline action toolbar,
// then look for aria-label="Mark as read" both inside the row and globally
// (Google Chat sometimes renders action overlays outside the row element).
jsHover(row);
await sleep(400);
let markBtn =
row.querySelector('[aria-label="Mark as read"], [data-item="mark-thread-as-read"], [data-item="mark-as-read"]') ||
document.querySelector('[aria-label="Mark as read"]');
if (markBtn) {
const rect = markBtn.getBoundingClientRect();
console.log('[mark-all-read] S2: mark-as-read button found (visible:', rect.width > 0, ')');
rect.width > 0 ? jsClick(markBtn) : markBtn.click();
await sleep(300);
return true;
}
// Strategy 3 ─────────────────────────────────────────────────────────────
// Click the Options (⋮) button revealed by hover, then pick "Mark as read"
// from the dropdown menu.
const optionsBtn = row.querySelector('[aria-label="Options"]');
if (optionsBtn) {
console.log('[mark-all-read] S3: trying Options menu');
jsClick(optionsBtn);
const menu = await waitFor(() => document.querySelector('[role="menu"]'), 1500);
if (menu) {
const items = [...menu.querySelectorAll('[role="menuitem"], [role="option"]')];
console.log('[mark-all-read] menu items:', items.map(el => el.textContent.trim()));
const markRead = items.find(el => /mark.*read/i.test(el.textContent));
if (markRead) {
markRead.click();
await sleep(200);
return true;
}
closeMenu();
await sleep(100);
}
} else {
const btns = [...row.querySelectorAll('button, [role="button"]')];
console.warn('[mark-all-read] S3: no Options btn; buttons in row:',
btns.map(b => b.getAttribute('aria-label') || b.textContent.trim()).filter(Boolean));
}
// Strategy 4 ─────────────────────────────────────────────────────────────
// Right-click context menu — contextmenu events propagate through jsaction
// even from dispatchEvent, unlike CSS :hover.
console.log('[mark-all-read] S4: trying contextmenu');
const rect = row.getBoundingClientRect();
row.dispatchEvent(new MouseEvent('contextmenu', {
bubbles: true, cancelable: true, view: window,
clientX: rect.left + rect.width / 2,
clientY: rect.top + rect.height / 2,
button: 2, buttons: 2,
}));
const ctxMenu = await waitFor(
() => [...document.querySelectorAll('[role="menu"]')]
.find(m => /mark.*read/i.test(m.textContent)),
1500
);
if (ctxMenu) {
const items = [...ctxMenu.querySelectorAll('[role="menuitem"]')];
const markRead = items.find(el => /mark.*read/i.test(el.textContent));
if (markRead) {
markRead.click();
await sleep(200);
return true;
}
closeMenu();
}
console.warn('[mark-all-read] all strategies failed for:',
row.id || row.dataset.groupId || row.textContent.slice(0, 60));
return false;
}
// ── main loop ─────────────────────────────────────────────────────────────
async function markAllAsRead(btn) {
btn.disabled = true;
btn.textContent = '⏳ Working…';
const rows = getUnreadItems();
console.log(`[mark-all-read] found ${rows.length} unread items`);
if (rows.length === 0) {
btn.textContent = '✅ All read';
await sleep(2000);
resetButton(btn);
return;
}
let marked = 0;
for (const row of rows) {
const ok = await markItemAsRead(row);
if (ok) marked++;
btn.textContent = `⏳ ${marked}/${rows.length}`;
}
btn.textContent = `✅ Marked ${marked}`;
await sleep(2500);
resetButton(btn);
}
// ── floating button UI ────────────────────────────────────────────────────
function resetButton(btn) {
btn.disabled = false;
btn.textContent = '✉ Mark all read';
}
function createButton() {
const btn = document.createElement('button');
btn.id = 'gchat-mark-all-read';
btn.textContent = '✉ Mark all read';
Object.assign(btn.style, {
position: 'fixed',
bottom: '20px',
right: '20px',
zIndex: '999999',
padding: '10px 16px',
background: '#1a73e8',
color: '#fff',
border: 'none',
borderRadius: '24px',
fontSize: '13px',
fontFamily: 'Google Sans, Roboto, sans-serif',
fontWeight: '500',
cursor: 'pointer',
boxShadow: '0 2px 8px rgba(0,0,0,.35)',
transition: 'opacity .2s',
});
btn.addEventListener('mouseenter', () => (btn.style.opacity = '0.85'));
btn.addEventListener('mouseleave', () => (btn.style.opacity = '1'));
btn.addEventListener('click', () => markAllAsRead(btn));
document.body.appendChild(btn);
return btn;
}
// ── bootstrap ─────────────────────────────────────────────────────────────
async function init() {
await waitFor(
() => document.querySelector('[data-is-unread="true"], [role="navigation"], nav'),
15000,
300
);
if (!document.getElementById('gchat-mark-all-read')) {
createButton();
}
}
init();
})();