Прозрачный чат

🧩 Syntax:
// ==UserScript==
// @name         Lolz Transparent Chat — v2.9.18-opt (same look, fewer CPU/DOM hits)
// @namespace    http://tampermonkey.net/
// @version      2.9.18-opt
// @description  Тот же вид: прозрачность, серые грани, зелёные упоминания, крестики и полный H-скролл всей шапки. Оптимизация: кэш узлов, единый дебаунс мутаций, WeakSet для "стекла", аккуратные наблюдатели.
// @match        https://lzt.market/*
// @match        https://lolz.live/*
// @match        https://zelenka.guru/*
// @grant        none
// @license      MIT
// ==/UserScript==

(() => {
  'use strict';

  // ---- Константы/флаги (как раньше) ----
  const BLUR_PX = 12;
  const ALPHA_GLASS = 0.01; // лента
  const ALPHA_INPUT = 0.01; // поле ввода
  const SCOPE_CLASS = 'kyan-chat-scope';
  const STYLE_ID = 'kyan-chat-style';

  const IS_MOBILE = /Android|iPhone|iPad|Mobile/i.test(navigator.userAgent);
  const SUPPORTS_BLUR = (typeof CSS!=='undefined') &&
    (CSS.supports('backdrop-filter','blur(1px)') || CSS.supports('-webkit-backdrop-filter','blur(1px)'));
  const EDGE_MOBILE = /\bEdgA\/|EdgiOS\//i.test(navigator.userAgent);
  const USE_BLUR = SUPPORTS_BLUR && !EDGE_MOBILE;

  const MENTION_BORDER = '3px solid rgba(34,142,93,0.85)';

  // ---- Helpers ----
  const qs  = (r,s)=>r.querySelector(s);
  const qsa = (r,s)=>Array.from(r.querySelectorAll(s));

  // Запоминаем какие узлы уже «застеклены», чтобы не трогать повторно
  const glassed = new WeakSet();

  function inlineGlass(el, alpha, blur){
    if (!el || glassed.has(el)) return;
    el.style.setProperty('background', `rgba(0,0,0,${alpha})`, 'important');
    el.style.setProperty('background-color', `rgba(0,0,0,${alpha})`, 'important');
    el.style.setProperty('background-image', 'none', 'important');
    if (USE_BLUR) {
      el.style.setProperty('backdrop-filter', `blur(${blur}px)`, 'important');
      el.style.setProperty('-webkit-backdrop-filter', `blur(${blur}px)`, 'important');
    }
    glassed.add(el);
  }

  const GLASS_SELECTORS = [
    '#chat2-full','[class^="chat2-floating"]','.chat2','[class*="chat2-container"]','[class*="chatbox"]',
    '.scrollable-content','[class*="chat2-body"]','[class*="chat2-content"]','[class*="messages"]','[class*="message-list"]',
    '.chat2-widget-inner','[class*="primary-darker"]:not(.chat2-header):not([class*="chat2-header"])',
    '[class*="primary-dark"]:not(.chat2-header):not([class*="chat2-header"])','[class*="pane"]','[class*="layout"]','[class*="wrapper"]'
  ];

  function applyGlassSet(root){
    inlineGlass(root, ALPHA_GLASS, BLUR_PX);
    // Пробегаемся по селекторам, но не чаще чем нужно: WeakSet защитит от повторных операций
    for (const sel of GLASS_SELECTORS) {
      qsa(root, sel).forEach(el => inlineGlass(el, ALPHA_GLASS, BLUR_PX));
    }
    const scroll = findScroll(root);
    if (scroll){
      let p = scroll.parentElement, i=0;
      while (p && p!==root && i<6){
        inlineGlass(p, ALPHA_GLASS, BLUR_PX);
        p = p.parentElement; i++;
      }
    }
  }

  // ---- CSS (не меняем внешний вид) ----
  function injectCSS() {
    if (document.getElementById(STYLE_ID)) return;
    const css = `
      .${SCOPE_CLASS} {}

      /* Прозрачность внутренних контейнеров (ШАПКУ НЕ СТЕКЛИМ) */
      .${SCOPE_CLASS} .chat2-widget-inner,
      .${SCOPE_CLASS} .scrollable-content,
      .${SCOPE_CLASS} [class*="chat2-body"],
      .${SCOPE_CLASS} [class*="chat2-content"],
      .${SCOPE_CLASS} [class*="chat2-container"],
      .${SCOPE_CLASS} [class*="chat2-wrapper"],
      .${SCOPE_CLASS} [class*="messages"],
      .${SCOPE_CLASS} [class*="message-list"],
      .${SCOPE_CLASS} [class*="list"],
      .${SCOPE_CLASS} [class*="chat2-layout"],
      .${SCOPE_CLASS} [class*="pane"],
      .${SCOPE_CLASS} [class*="primary-darker"]:not(.chat2-header):not([class*="chat2-header"]),
      .${SCOPE_CLASS} [class*="primary-dark"]:not(.chat2-header):not([class*="chat2-header"]) {
        background: transparent !important;
        background-color: transparent !important;
        background-image: none !important;
      }

      /* Псевдоэлементы — не трогаем в шапке */
      .${SCOPE_CLASS} [class*="chat2-"]::before,
      .${SCOPE_CLASS} [class*="chat2-"]::after { background: transparent !important; }
      .${SCOPE_CLASS} .chat2-header *::before,
      .${SCOPE_CLASS} .chat2-header *::after { background: initial !important; }

      /* Сообщения: стабильные серые грани */
      .${SCOPE_CLASS} .chat2-message-block,
      .${SCOPE_CLASS} .reply-message {
        background: transparent !important;
        border: 1px solid rgba(128,128,128,.45) !important;
        border-radius: 6px !important;
      }

      /* Панель «Ответ…» */
      .${SCOPE_CLASS} .chat2-replying,
      .${SCOPE_CLASS} [class*="replying"] {
        background: rgba(0,0,0,.04) !important;
        border: 1px solid rgba(128,128,128,.35) !important;
        border-radius: 8px !important;
        box-shadow: none !important;
      }
      .${SCOPE_CLASS} .chat2-replying-author { background: initial !important; }
      .${SCOPE_CLASS} .chat2-replying * { border: none !important; box-shadow: none !important; }

      /* Заголовки/ники внутри сообщения — без блюров/фильтров */
      .${SCOPE_CLASS} .chat2-message-header,
      .${SCOPE_CLASS} .chat2-message-header * {
        backdrop-filter: none !important;
        -webkit-backdrop-filter: none !important;
        filter: none !important;
        box-shadow: revert !important;
      }

      /* Поле ввода — почти прозрачное */
      .${SCOPE_CLASS} .chat2-footer textarea,
      .${SCOPE_CLASS} .chat2-footer [contenteditable="true"],
      .${SCOPE_CLASS} .chat2-footer input[type="text"],
      .${SCOPE_CLASS} .chat2-footer input[type="search"],
      .${SCOPE_CLASS} .chat2-footer [class*="editor"] {
        background: rgba(0,0,0,${ALPHA_INPUT}) !important;
        border: none !important;
        border-radius: 8px !important;
        outline: none !important;
        box-shadow: none !important;
      }
      .${SCOPE_CLASS} .chat2-footer button,
      .${SCOPE_CLASS} .chat2-footer [role="button"],
      .${SCOPE_CLASS} .chat2-footer svg {
        opacity: revert !important;
        filter: none !important;
      }
      .${SCOPE_CLASS} .submit-btn { background: initial !important; }

      /* Крестики */
      .${SCOPE_CLASS} .chat2-footer .chat2-cancel-reply,
      .${SCOPE_CLASS} .chat2-message-editing .chat2-cancel-editing {
        display:inline-flex !important; align-items:center; justify-content:center;
        width:22px; height:22px; border-radius:6px; background: transparent !important; color:#fff !important;
        opacity:1 !important; filter: drop-shadow(0 0 1px rgba(0,0,0,.7)) !important; cursor: pointer !important;
      }
      .${SCOPE_CLASS} .chat2-footer .chat2-cancel-reply:empty::before,
      .${SCOPE_CLASS} .chat2-message-editing .chat2-cancel-editing:empty::before { content:'×'; font-size:16px; line-height:1; }

      /* Шапка — рамка без стекла */
      .${SCOPE_CLASS} .chat2-header,
      .${SCOPE_CLASS} .chat2-header.lztng-primary-dark,
      .${SCOPE_CLASS} [class*="chat2-header"].lztng-primary-dark {
        border: 1px solid rgba(128,128,128,.45) !important;
        border-radius: 8px !important;
        box-shadow: none !important;
        background: initial !important;
        backdrop-filter: none !important; -webkit-backdrop-filter: none !important;
        overflow: visible !important;
      }

      /* Упоминания — только рамка */
      .${SCOPE_CLASS} .chat2-message.chat2-message-tagged,
      .${SCOPE_CLASS} .chat2-message.chat2-message-tagged .chat2-message-block,
      .${SCOPE_CLASS} .chat2-message.chat2-message-tagged .chat2-message-text,
      .${SCOPE_CLASS} .chat2-message.chat2-message-tagged .chat2-message-header {
        background: transparent !important; background-color: transparent !important; box-shadow: none !important;
      }
      .${SCOPE_CLASS} .reply-message.reply-message-your {
        border: ${MENTION_BORDER} !important;
        background: transparent !important;
      }

      /* --- Полный H-скролл всей шапки (мобайл) --- */
      .kyan-head-outer {
        width: 100% !important;
        max-width: 100% !important;
        overflow: hidden !important;
      }
      .kyan-head-scroll {
        width: 100% !important;
        max-width: 100% !important;
        overflow-x: auto !important;
        overflow-y: hidden !important;
        -webkit-overflow-scrolling: touch !important;
        touch-action: pan-x !important;
        overscroll-behavior-x: contain !important;
        scrollbar-width: thin;
      }
      .kyan-head-strip {
        display: inline-flex !important;
        flex-wrap: nowrap !important;
        align-items: center !important;
        gap: .75rem !important;
        white-space: nowrap !important;
        min-width: max-content !important;
      }

      @media (max-width: 768px) {
        .${SCOPE_CLASS} .chat2-header,
        .${SCOPE_CLASS} .chat2-header > * {
          max-width: 100% !important;
          overflow: visible !important;
        }
      }
    `;
    const tag = document.createElement('style');
    tag.id = STYLE_ID;
    tag.textContent = css;
    document.documentElement.appendChild(tag);
  }

  // ---- Mentions/Replies ----
  function getMsgBlock(msg){
    return msg.querySelector('.chat2-message-block') ||
           msg.querySelector('.chat2-message-text') ||
           msg.querySelector('div');
  }
  function setMentionBorderOnMessage(msg, on){
    if (msg.classList.contains('reply-message') && msg.classList.contains('reply-message-your')){
      if (on){
        msg.style.setProperty('border', MENTION_BORDER, 'important');
        msg.style.setProperty('background', 'transparent', 'important');
        msg.style.setProperty('background-color', 'transparent', 'important');
      } else {
        msg.style.removeProperty('border');
        msg.style.removeProperty('background');
        msg.style.removeProperty('background-color');
      }
      return;
    }
    const block = getMsgBlock(msg);
    if (!block) return;
    if (on){
      block.style.setProperty('border', MENTION_BORDER, 'important');
      block.style.setProperty('background', 'transparent', 'important');
      block.style.setProperty('background-color', 'transparent', 'important');
    } else {
      block.style.removeProperty('border');
      block.style.removeProperty('background');
      block.style.removeProperty('background-color');
    }
  }
  function refreshMentions(root){
    // Только простая проверка классов — дёшево
    qsa(root, '.chat2-message, [class*="chat2-message"]').forEach(msg=>{
      setMentionBorderOnMessage(msg, msg.classList.contains('chat2-message-tagged'));
    });
    qsa(root, '.reply-message.reply-message-your').forEach(msg=>{
      setMentionBorderOnMessage(msg, true);
    });
  }

  // ---- Шапка: полный H-скролл ----
  let headerCached = null;
  let headWrapped = false;

  function setupFullHeaderScroll(header){
    if (!IS_MOBILE || !header || headWrapped) return;

    // Собираем всех детей шапки
    const kids = Array.from(header.childNodes);
    if (!kids.length) return;

    const outer   = document.createElement('div'); outer.className = 'kyan-head-outer';
    const scroll  = document.createElement('div'); scroll.className = 'kyan-head-scroll';
    const strip   = document.createElement('div'); strip.className  = 'kyan-head-strip';

    kids.forEach(n => strip.appendChild(n));
    scroll.appendChild(strip);
    outer.appendChild(scroll);
    header.appendChild(outer);

    headWrapped = true;
  }

  // ---- Root helpers ----
  function findChatRoot(){
    return qs(document,'[class^="chat2-floating"]') ||
           qs(document,'#chat2-full') ||
           qs(document,'.chat2') ||
           qs(document,'[class*="chat2-container"]') ||
           qs(document,'[class*="chatbox"]') ||
           document.body;
  }
  function findScroll(root){
    return qs(root,'.scrollable-content') ||
           qs(root,'[class*="chat2-body"]') ||
           qs(root,'[class*="chat2-content"]') ||
           qs(root,'[class*="messages"]') ||
           qs(root,'[class*="message-list"]');
  }
  function findHeader(root){
    return qs(root, '.chat2-header, [class*="chat2-header"]') ||
           document.querySelector('.chat2-header, [class*="chat2-header"]');
  }

  // ---- Orchestration with debounce ----
  let rootCached = null;
  let mo, moLayout;
  let debounced = false;
  const DEBOUNCE_MS = 120;

  function tick(){
    debounced = false;
    const root = rootCached || findChatRoot();
    if (!root) return;
    rootCached = root;

    applyGlassSet(root);
    refreshMentions(root);

    if (!headerCached || !document.contains(headerCached)) {
      headerCached = findHeader(root);
      headWrapped = false; // если шапка пере-создана, разрешим обёртку снова
    }
    if (headerCached) setupFullHeaderScroll(headerCached);
  }

  function scheduleTick(){
    if (debounced) return;
    debounced = true;
    setTimeout(tick, DEBOUNCE_MS);
  }

  function watchAll(root){
    mo?.disconnect();
    mo = new MutationObserver(scheduleTick);
    // Наблюдаем весь документ, но дешёвым debounced-тригером
    mo.observe(document.documentElement, { childList:true, subtree:true, attributes:true, attributeFilter:['class','style'] });

    moLayout?.disconnect();
    moLayout = new MutationObserver(scheduleTick);
    moLayout.observe(root, { childList:true, subtree:true });

    // сразу один проход
    tick();
  }

  // ---- Init ----
  function init(){
    injectCSS();
    rootCached = findChatRoot();
    if (!rootCached) { setTimeout(init, 150); return; }
    rootCached.classList.add(SCOPE_CLASS);
    watchAll(rootCached);
  }

  if (document.readyState === 'loading') {
    document.addEventListener('DOMContentLoaded', init, { once:true });
  } else {
    init();
  }
})();