floodmeadows / Google Meet prefill default meeting code

// ==UserScript==
// @name         Google Meet prefill default meeting code
// @description  Prefills the "Enter code or nickname" field with your default meeting code using resilient selectors and SPA-safe hooks.
// @namespace    http://www.floodmeadows.com
// @copyright    2026, floodmeadows (https://openuserjs.org/users/floodmeadows)
// @license      MIT
// @version      1.0
// @match        https://meet.google.com/*
// @updateURL    https://openuserjs.org/meta/floodmeadows/Google_Meet_prefill_default_meeting_code.meta.js
// @downloadURL  https://openuserjs.org/src/scripts/floodmeadows/Google_Meet_prefill_default_meeting_code.user.js
// @run-at       document-idle
// @icon         https://www.google.com/s2/favicons?sz=64&domain=google.com
// @grant        none
// ==/UserScript==

// ==OpenUserJS==
// @authors       floodmeadows
// ==/OpenUserJS==

/* jshint esversion: 6 */

/* ---- CONFIG SECTION START ---- */
const MEETING_CODE = 'YOUR_MEETING_CODE'; // e.g. 'abc-defg-hij'
/* ----- CONFIG SECTION END ----- */

(() => {
  'use strict';

  // If you need a specific Google account, set AUTHUSER to "0" | "1" etc., else leave ''.
  const AUTHUSER = '';

  // Which paths count as "home" where the field exists.
  const HOME_PATH_RE = /^\/($|landing|start)/i;

  // Utility: robustly set value and notify React-like listeners.
  function setInputValue(input, value) {
    // Try the React setter if present
    const desc = Object.getOwnPropertyDescriptor(HTMLInputElement.prototype, 'value');
    if (desc && desc.set) {
      desc.set.call(input, value);
    } else {
      input.value = value;
    }
    input.dispatchEvent(new Event('input', { bubbles: true }));
    input.dispatchEvent(new Event('change', { bubbles: true }));
  }

  // Try to locate the “join by code” input using resilient selectors.
  function findJoinCodeInput(root = document) {
    // Prefer accessibility attributes. Case-insensitive contains on common strings.
    const candidates = root.querySelectorAll([
      'input[aria-label*="code" i]',
      'input[aria-label*="nickname" i]',
      'input[placeholder*="code" i]',
      'input[placeholder*="nickname" i]',
      // Fallbacks that sometimes appear:
      'input[name="i6"]',
      'input[type="text"]'
    ].join(','));

    // Heuristic: pick the visible, enabled one with longest width.
    let best = null, bestWidth = 0;
    for (const el of candidates) {
      const style = getComputedStyle(el);
      const visible = style.visibility !== 'hidden' && style.display !== 'none' && el.offsetParent !== null;
      const enabled = !el.disabled && !el.readOnly;
      if (visible && enabled) {
        const w = el.getBoundingClientRect().width;
        if (w > bestWidth) { best = el; bestWidth = w; }
      }
    }
    return best;
  }

  // Prefill once; no hard failure if not found.
  function prefillOnce() {
    if (!HOME_PATH_RE.test(location.pathname)) return;

    // Attempt in light DOM first
    let input = findJoinCodeInput(document);
    if (input) {
      setInputValue(input, MEETING_CODE);
      input.focus();
      return;
    }

    // Attempt in shallow shadow roots
    const shadows = Array.from(document.querySelectorAll('*'))
      .map(n => n.shadowRoot)
      .filter(Boolean);

    for (const sr of shadows) {
      input = findJoinCodeInput(sr);
      if (input) {
        setInputValue(input, MEETING_CODE);
        input.focus();
        return;
      }
    }
  }

  // Retry strategy: quick polling then give up quietly
  function tryPrefillWithRetries(timeoutMs = 1500, intervalMs = 120) {
    if (!HOME_PATH_RE.test(location.pathname)) return;

    const start = performance.now();
    const timer = setInterval(() => {
      prefillOnce();
      if (performance.now() - start > timeoutMs) clearInterval(timer);
    }, intervalMs);
  }

  // Watch SPA navigations by URL changes and DOM mutations.
  function watchSpaNavigations() {
    let last = location.pathname + location.search;
    const check = () => {
      const cur = location.pathname + location.search;
      if (cur !== last) {
        last = cur;
        // Re-attempt on new “home-like” screens
        tryPrefillWithRetries();
      }
    };
    // Poll URL (cheap + reliable)
    setInterval(check, 400);

    // Also observe major DOM swaps to catch app re-renders on same URL
    const mo = new MutationObserver(() => {
      if (HOME_PATH_RE.test(location.pathname)) tryPrefillWithRetries(800, 100);
    });
    mo.observe(document.documentElement, { childList: true, subtree: true });
  }

  // Optional: quick keyboard shortcut to jump directly to your room.
  function addHotkey() {
    document.addEventListener('keydown', (e) => {
      if (e.altKey && !e.shiftKey && !e.ctrlKey && !e.metaKey && e.key.toLowerCase() === 'm') {
        e.preventDefault();
        const url = `https://meet.google.com/${MEETING_CODE}` + (AUTHUSER ? `?authuser=${AUTHUSER}` : '');
        location.assign(url);
      }
    }, { capture: true });
  }

  // Boot
  tryPrefillWithRetries();
  watchSpaNavigations();
  addHotkey();
})();