Google Chat Mark all as Read


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();
})();