Untitled

 avatar
unknown
plain_text
2 months ago
5.1 kB
51
No Index
// ==UserScript==
// @name         Tandem – Focus Only (prep Enter)
// @namespace    https://tandem-helper.local
// @version      2.1
// @description  Injecte le message, active le textarea et place le caret pour que tu n'aies qu'à appuyer sur Enter
// @match        https://app.tandem.net/*
// @run-at       document-end
// @grant        none
// ==/UserScript==

(function () {
  'use strict';

  const MESSAGE_TEMPLATE = "Are you a kheyette, {name}?";
  const MAX_WAIT_LOOPS = 60; // 60 * 500ms = 30s
  const sleep = (ms) => new Promise(r => setTimeout(r, ms));

  function formatName(rawName) {
    return rawName
      .toLowerCase()
      .split(/\s+/)
      .filter(Boolean)
      .map(w => w.charAt(0).toUpperCase() + w.slice(1))
      .join(' ');
  }

  function setReactValue(el, value) {
    try {
      const setter = Object.getOwnPropertyDescriptor(
        window.HTMLTextAreaElement.prototype, 'value'
      ).set;
      setter.call(el, value);
      el.dispatchEvent(new Event('input', { bubbles: true }));
    } catch (e) {
      console.error('[TM DEBUG] setReactValue failed', e);
    }
  }

  async function prepareForEnter() {
    if (!/\/chats\//.test(location.href)) return;
    console.log('[TM DEBUG] prepareForEnter on', location.href);

    // attendre le textarea (ou un nouveau textarea si React le remplace)
    let textarea = null;
    for (let i = 0; i < MAX_WAIT_LOOPS; i++) {
      textarea = document.querySelector('textarea');
      if (textarea) break;
      await sleep(500);
    }
    if (!textarea) {
      console.warn('[TM DEBUG] textarea introuvable');
      return;
    }

    // Si conversation déjà entamée -> ne rien faire
    if (document.querySelector('.styles_timestamp__cjEoo')) {
      console.log('[TM DEBUG] Conversation non-vierge détectée, script ignoré');
      return;
    }

    // forceEnabled : retire disabled si React le remet
    const forceEnabled = () => {
      try {
        if (textarea && textarea.disabled) {
          textarea.removeAttribute('disabled');
          textarea.disabled = false;
          textarea.style.pointerEvents = 'auto';
          textarea.style.opacity = '1';
          // parfois React change l'élément ; on loggue pour debug
          console.log('[TM DEBUG] forced enable textarea');
        }
      } catch (e) { /* ignore */ }
    };

    // Observers et interval pour maintenir le champ activé
    forceEnabled();
    const obs = new MutationObserver(forceEnabled);
    obs.observe(textarea, { attributes: true, attributeFilter: ['disabled'] });
    const intervalId = setInterval(forceEnabled, 500);

    // Récupérer nom formaté
    const h3 = document.querySelector('.styles_name__MFxv7 h3');
    const rawName = h3 ? h3.textContent.trim() : 'there';
    const formatted = formatName(rawName);
    const message = MESSAGE_TEMPLATE.replace('{name}', formatted);

    // Injecter le message si le champ est vide
    if (!textarea.value) {
      // attendre un peu pour être sûr que la réactivation est prise en compte
      await sleep(300);
      setReactValue(textarea, message);
      console.log('[TM DEBUG] message injecté:', message);
    } else {
      console.log('[TM DEBUG] textarea déjà rempli, on skip l\'injection');
    }

    // Focus + click + placer le caret à la fin
    try {
      textarea.focus({ preventScroll: false });
      // trigger a real-like click sequence
      textarea.dispatchEvent(new MouseEvent('mousedown', { bubbles: true }));
      textarea.dispatchEvent(new MouseEvent('mouseup', { bubbles: true }));
      textarea.click();
      // set caret to end
      const len = textarea.value.length;
      textarea.setSelectionRange(len, len);
      textarea.scrollIntoView({ block: 'center', inline: 'nearest' });
      // petit highlight visuel pour indiquer que tu dois appuyer Enter
      const prevOutline = textarea.style.outline;
      textarea.style.outline = '3px solid rgba(0,150,255,0.35)';
      setTimeout(() => { textarea.style.outline = prevOutline; }, 2200);
      console.log('[TM DEBUG] focus posé, caret à la fin -> appuie sur Enter pour envoyer');
    } catch (e) {
      console.warn('[TM DEBUG] focus/caret failed', e);
    }

    // cleanup si le textarea change (on arrête interval + observer)
    const bodyObs = new MutationObserver(() => {
      const current = document.querySelector('textarea');
      if (current !== textarea) {
        obs.disconnect();
        bodyObs.disconnect();
        clearInterval(intervalId);
        console.log('[TM DEBUG] textarea remplacé, observers cleared');
      }
    });
    bodyObs.observe(document.body, { childList: true, subtree: true });
  }

  function onUrlChange(cb) {
    let oldHref = location.href;
    new MutationObserver(() => {
      if (location.href !== oldHref) {
        oldHref = location.href;
        cb();
      }
    }).observe(document.body, { childList: true, subtree: true });
  }

  // Exécution initiale + relance SPA
  setTimeout(prepareForEnter, 1400);
  onUrlChange(() => setTimeout(prepareForEnter, 1000));
})();
Editor is loading...
Leave a Comment