YouTube Super Fast Chat

To make your YouTube Live Chat scroll instantly without smoothing transform CSS

Versión del día 02/07/2023. Echa un vistazo a la versión más reciente.

Tendrás que instalar una extensión para tu navegador como Tampermonkey, Greasemonkey o Violentmonkey si quieres utilizar este script.

You will need to install an extension such as Tampermonkey to install this script.

Tendrás que instalar una extensión como Tampermonkey o Violentmonkey para instalar este script.

Necesitarás instalar una extensión como Tampermonkey o Userscripts para instalar este script.

Tendrás que instalar una extensión como Tampermonkey antes de poder instalar este script.

Necesitarás instalar una extensión para administrar scripts de usuario si quieres instalar este script.

(Ya tengo un administrador de scripts de usuario, déjame instalarlo)

Tendrás que instalar una extensión como Stylus antes de poder instalar este script.

Tendrás que instalar una extensión como Stylus antes de poder instalar este script.

Tendrás que instalar una extensión como Stylus antes de poder instalar este script.

Para poder instalar esto tendrás que instalar primero una extensión de estilos de usuario.

Para poder instalar esto tendrás que instalar primero una extensión de estilos de usuario.

Para poder instalar esto tendrás que instalar primero una extensión de estilos de usuario.

(Ya tengo un administrador de estilos de usuario, déjame instalarlo)

// ==UserScript==
// @name                YouTube Super Fast Chat
// @name:ja             YouTube スーパーファーストチャット
// @name:zh-TW          YouTube 超快聊天
// @name:zh-CN          YouTube 超快聊天
// @namespace           UserScript
// @match               https://www.youtube.com/live_chat*
// @version             0.1.3
// @license             MIT
// @author              CY Fung
// @run-at              document-start
// @grant               none
// @unwrap
// @allFrames           true
// @inject-into         page
//
// @description         To make your YouTube Live Chat scroll instantly without smoothing transform CSS
// @description:ja      YouTubeライブチャットをスムーズな変形CSSなしで瞬時にスクロールさせるために。
// @description:zh-TW   讓您的 YouTube 直播聊天即時滾動,不經過平滑轉換 CSS。
// @description:zh-CN   让您的 YouTube 直播聊天即时滚动,不经过平滑转换 CSS。
//
// ==/UserScript==

((__CONTEXT__) => {
  const addCss = () => document.head.appendChild(document.createElement('style')).textContent = `

      @supports (contain: layout paint style) and (content-visibility: auto) and (contain-intrinsic-size: auto var(--wsr94)) {

      [wSr93] {
        content-visibility: visible;
      }

      [wSr93="hidden"]:nth-last-child(n+4) {
        content-visibility: auto;
        contain-intrinsic-size: auto var(--wsr94);
      }

      }

    @supports (contain: layout paint style) {


/* optional */
      #item-offset.style-scope.yt-live-chat-item-list-renderer {
        height: auto !important;
        min-height: unset !important;
      }

      #items.style-scope.yt-live-chat-item-list-renderer {
        transform: translateY(0px) !important;
        /*padding-bottom: 0 !important;
        padding-top: 0 !important;*/
      }

/* optional */

      yt-icon[icon="down_arrow"] > *,
      yt-icon-button#show-more > * {
        pointer-events: none !important;
      }


      #item-list.style-scope.yt-live-chat-renderer,
      yt-live-chat-item-list-renderer.style-scope.yt-live-chat-renderer,
      #item-list.style-scope.yt-live-chat-renderer *,
      yt-live-chat-item-list-renderer.style-scope.yt-live-chat-renderer * {
        will-change: unset !important;
      }

      yt-img-shadow[height][width] {
        content-visibility: visible !important;
      }


      #item-offset.style-scope.yt-live-chat-item-list-renderer > #items.style-scope.yt-live-chat-item-list-renderer {
        position: static !important;
      }


/* ------------------------------------------------------------------------------------------------------------- */

      yt-live-chat-author-chip #chat-badges.yt-live-chat-author-chip,
      yt-live-chat-author-chip #chat-badges.yt-live-chat-author-chip yt-live-chat-author-badge-renderer,
      yt-live-chat-author-chip #chat-badges.yt-live-chat-author-chip yt-live-chat-author-badge-renderer #image,
      yt-live-chat-author-chip #chat-badges.yt-live-chat-author-chip yt-live-chat-author-badge-renderer #image img {
        contain: layout style;
      }

/*
      yt-live-chat-author-chip #chat-badges.yt-live-chat-author-chip,
      yt-live-chat-author-chip #chat-badges.yt-live-chat-author-chip yt-live-chat-author-badge-renderer,
      yt-live-chat-author-chip #chat-badges.yt-live-chat-author-chip yt-live-chat-author-badge-renderer #image,
      yt-live-chat-author-chip #chat-badges.yt-live-chat-author-chip yt-live-chat-author-badge-renderer #image img {
        contain: layout style;
        display: inline-flex;
        vertical-align: middle;
      }
      */

      #items yt-live-chat-text-message-renderer {
        contain: layout style;
      }

      yt-live-chat-item-list-renderer:not([allow-scroll]) #item-scroller.yt-live-chat-item-list-renderer {
        overflow-y: scroll;
        padding-right: 0;
      }

      body yt-live-chat-app {
        contain: size layout paint style;
        overflow: hidden;
      }

      #items.style-scope.yt-live-chat-item-list-renderer {
        contain: layout paint style;
      }

      #item-offset.style-scope.yt-live-chat-item-list-renderer {
        contain: style;
      }

      #item-scroller.style-scope.yt-live-chat-item-list-renderer {
        contain: size style;
      }

      #contents.style-scope.yt-live-chat-item-list-renderer,
      #chat.style-scope.yt-live-chat-renderer,
      img.style-scope.yt-img-shadow[width][height] {
        contain: size layout paint style;
      }

      .style-scope.yt-live-chat-ticker-renderer[role="button"][aria-label],
      .style-scope.yt-live-chat-ticker-renderer[role="button"][aria-label] > #container {
        contain: layout paint style;
      }

      yt-live-chat-text-message-renderer.style-scope.yt-live-chat-item-list-renderer,
      yt-live-chat-membership-item-renderer.style-scope.yt-live-chat-item-list-renderer,
      yt-live-chat-paid-message-renderer.style-scope.yt-live-chat-item-list-renderer,
      yt-live-chat-banner-manager.style-scope.yt-live-chat-item-list-renderer {
        contain: layout style;
      }

      tp-yt-paper-tooltip[style*="inset"][role="tooltip"] {
        contain: layout paint style;
      }

/*
      #item-offset.style-scope.yt-live-chat-item-list-renderer {
        position: relative !important;
        height: auto !important;
      }
*/

/* ------------------------------------------------------------------------------------------------------------- */


      #items.style-scope.yt-live-chat-item-list-renderer {
        padding-top: var(--items-top-padding);
      }


#continuations, #continuations * {
  contain: strict;
  position: fixed;
  top: 2px;
  height: 1px;
  width: 2px;
  height: 1px;
  visibility: collapse;
}


    }

  `;

  const { Promise, requestAnimationFrame } = __CONTEXT__;


  const isContainSupport = CSS.supports('contain', 'layout paint style');
  if (!isContainSupport) {
    console.error(`
YouTube Light Chat Scroll: Your browser does not support 'contain'.
Chrome >= 52; Edge >= 79; Safari >= 15.4, Firefox >= 69; Opera >= 39
`.trim());
    return;
  }

  // const APPLY_delayAppendChild = false;

  let activeDeferredAppendChild = false;

  let mpws = new WeakSet();
  let ops = [];
  let msqs = new Set();

  const sp7 = Symbol();


  let dt0 = Date.now() - 2000;
  const dateNow = () => Date.now() - dt0;
  let lastScroll = 0;
  let lastLShow = 0;

  const phFn = (dummy) => ({

    get(target, prop) {
      return (prop in dummy) ? dummy[prop] : prop === sp7 ? target : target[prop];
    },
    set(target, prop, value) {
      if (!(prop in dummy)) {
        target[prop] = value;
      }
      return true;

    },
    has(target, prop) {
      return (prop in target)
    },
    deleteProperty(target, prop) {

      return true;
    },
    ownKeys(target) {
      return Object.keys(target);
    },
    defineProperty(target, key, descriptor) {
      return Object.defineProperty(target, key, descriptor);
      // return true;
    },
    getOwnPropertyDescriptor(target, key) {
      return Object.getOwnPropertyDescriptor(target, key);
    },



  });


  // const dummy3v = {
  //   "background": "",
  //   "backgroundAttachment": "",
  //   "backgroundBlendMode": "",
  //   "backgroundClip": "",
  //   "backgroundColor": "",
  //   "backgroundImage": "",
  //   "backgroundOrigin": "",
  //   "backgroundPosition": "",
  //   "backgroundPositionX": "",
  //   "backgroundPositionY": "",
  //   "backgroundRepeat": "",
  //   "backgroundRepeatX": "",
  //   "backgroundRepeatY": "",
  //   "backgroundSize": ""
  // };
  // for (const k of ['toString', 'getPropertyPriority', 'getPropertyValue', 'item', 'removeProperty', 'setProperty']) {
  //   dummy3v[k] = ((k) => (function () { const style = this[sp7]; return style[k](...arguments); }))(k)
  // }

  // const dummy3p = phFn(dummy3v);

  const pt2DecimalFixer = (x) => Math.round(x * 5, 0) / 5;

  const tickerContainerSetAttribute = function (attrName, attrValue) {

    let yd = (this.__dataHost || 0).__data;

    if (arguments.length === 2 && attrName === 'style' && yd && attrValue) {

      // let v = yd.containerStyle.privateDoNotAccessOrElseSafeStyleWrappedValue_;
      let v = `${attrValue}`;
      // conside a ticker is 101px width
      // 1% = 1.01px
      // 0.2% = 0.202px

      const ratio1 = (yd.ratio * 100);
      const ratio2 = pt2DecimalFixer(ratio1);
      v = v.replace(`${ratio1}%`, `${ratio2}%`).replace(`${ratio1}%`, `${ratio2}%`)

      if (yd.__style_last__ === v) return;
      yd.__style_last__ = v;

      HTMLElement.prototype.setAttribute.call(this, attrName, v);



    } else {
      HTMLElement.prototype.setAttribute.apply(this, arguments);
    }

  };


  /*
   *
   *   const tickerContainerSetAttribute = function (attrName, attrValue) {

    const yd = (this.__dataHost||0).__data;
      if (arguments.length === 2 && attrName === 'style' && attrValue && yd){
          // let v = yd.containerStyle.privateDoNotAccessOrElseSafeStyleWrappedValue_;
          let v = attrValue;

          // conside a ticker is 101px width
          // 1% = 1.01px
          // 0.2% = 0.202px
          const ratio1 = (yd.ratio * 100);
          const ratio2 = pt2DecimalFixer(ratio1);
          v = v.replace(`${ratio1}%`, `${ratio2}%`).replace(`${ratio1}%`, `${ratio2}%`)

        console.log(ratio1, ratio2)
          if (yd.__style_last__ !== v) {
            yd.__style_last__ = v; // clear along with data change

            HTMLElement.prototype.setAttribute.call(this, attrName, v);
            return;
          }


      }
    return HTMLElement.prototype.setAttribute.apply(this, arguments);

        };

        */


  const createRAF = () => requestAnimationFrame(() => {
    const e = [...ops]
    ops.length = 0;
    for (const t of e) t();
  });

  Node.prototype.appendChild = ((appendChild) => (function (s) {
    if (arguments.length !== 1) return appendChild.apply(this, arguments);
    // console.log(34, 1, this.is, this.nodeName, activeDeferredAppendChild, s.nodeName)
    const stack = new Error().stack;

    if (activeDeferredAppendChild && (msqs.has(stack) || s.nodeName === 'YT-LIVE-CHAT-TEXT-MESSAGE-RENDERER') && typeof s.is === 'string') {

      msqs.add(stack);
      // this = '#document-fragment'
      /*
      if (this instanceof HTMLElement) {


        if (ops.length === 0) createRAF();
        ops.push(() => {
          appendChild.apply(this, arguments);
        })
        return s;

      } else {

        mpws.add(this);
        appendChild.apply(this, arguments);
        return s;
      }
      */
      mpws.add(this);
      if (ops.length === 0) createRAF();
      ops.push(() => {
        mpws.delete(this);
        appendChild.apply(this, arguments);
      })
      return s;

    } else if (activeDeferredAppendChild && mpws.has(s)) {

      /*
      if (this instanceof HTMLElement) {
        if (ops.length === 0) createRAF();
        ops.push(() => {
          mpws.delete(s);
          appendChild.apply(this, arguments);
        })
        return s;
      } else {

        mpws.delete(s);
        appendChild.apply(this, arguments);
        return s;
      }
      */

      if (ops.length === 0) createRAF();
      ops.push(() => {
        mpws.delete(s);
        appendChild.apply(this, arguments);
      })
      return s;
    } else if (this.nodeName === 'YT-LIVE-CHAT-TICKER-PAID-MESSAGE-ITEM-RENDERER') {



      appendChild.call(this, s);

      let container = this.$.container;
      if (container) {

        // const sp3v = new Proxy(container.style, dummy3p)

        // Object.defineProperty(container, 'style', {get(){return sp3v}, set() { }, enumerable: true, configurable: true });


        container.setAttribute = tickerContainerSetAttribute;


      }

      return s;
    }
    // if(activeDeferredAppendChild) return null;
    appendChild.call(this, s);
    return s;
  }))(Node.prototype.appendChild);

  /*
  Node.prototype.append = ((append) => (function () {
    // console.log(34,2 )
    return append.apply(this, arguments);
  }))(Node.prototype.append);

  Node.prototype.insertBefore = ((insertBefore) => (function () {
    // console.log(34,3, this.is, this.nodeName, activeDeferredAppendChild)
    // if(activeDeferredAppendChild) return null;
    return insertBefore.apply(this, arguments);
  }))(Node.prototype.insertBefore);

  Node.prototype.insertAfter = ((insertAfter) => (function () {
    // console.log(34,4)
    return insertAfter.apply(this, arguments);
  }))(Node.prototype.insertAfter);

  */




  const fxOperator = (proto, propertyName) => {
    let propertyDescriptorGetter = null;
    try {
      propertyDescriptorGetter = Object.getOwnPropertyDescriptor(proto, propertyName).get;
    } catch (e) { }
    return typeof propertyDescriptorGetter === 'function' ? (e) => propertyDescriptorGetter.call(e) : (e) => e[propertyName];
  };

  const nodeParent = fxOperator(Node.prototype, 'parentNode');
  // const nFirstElem = fxOperator(HTMLElement.prototype, 'firstElementChild');
  const nPrevElem = fxOperator(HTMLElement.prototype, 'previousElementSibling');
  const nNextElem = fxOperator(HTMLElement.prototype, 'nextElementSibling');
  const nLastElem = fxOperator(HTMLElement.prototype, 'lastElementChild');


  /* globals WeakRef:false */

  /** @type {(o: Object | null) => WeakRef | null} */
  const mWeakRef = typeof WeakRef === 'function' ? (o => o ? new WeakRef(o) : null) : (o => o || null); // typeof InvalidVar == 'undefined'

  /** @type {(wr: Object | null) => Object | null} */
  const kRef = (wr => (wr && wr.deref) ? wr.deref() : wr);

  const watchUserCSS = () => {

    if (!CSS.supports('contain-intrinsic-size', 'auto var(--wsr94)')) return;


    const clearContentVisibilitySizing = () => {
      Promise.resolve().then(() => {

        for (const elm of document.querySelectorAll('[wSr93]')) {
          elm.setAttribute('wSr93', '');
        }


      })


    }
    const mutObserver = new MutationObserver((mutations) => {
      for (const mutation of mutations) {
        if ((mutation.addedNodes || 0).length >= 1) {

          for (const addedNode of mutation.addedNodes) {
            if (addedNode.nodeName === 'SCRIPT') {
              clearContentVisibilitySizing();
              return;
            }

          }
        }
        if ((mutation.remove || 0).length >= 1) {

          for (const removedNode of mutation.removedNodes) {

            if (removedNode.nodeName === 'SCRIPT') {
              clearContentVisibilitySizing();
              return;
            }

          }
        }
      }
    });
    mutObserver.observe(document.documentElement, {
      childList: true,
      subtree: false
    })

    mutObserver.observe(document.head, {
      childList: true,
      subtree: false
    })
    mutObserver.observe(document.body, {
      childList: true,
      subtree: false
    });


  }

  let done = 0;
  let main = async (q) => {

    if (done) return;

    if (!q) return;
    let m1 = nodeParent(q);
    let m2 = q;
    if (!(m1 && m1.id === 'item-offset' && m2 && m2.id === 'items')) return;

    done = 1;

    Promise.resolve().then(watchUserCSS);

    addCss();

    const dummy1v = {
      transform: '',
      height: '',
      minHeight: '',
      paddingBottom: '',
      paddingTop: ''
    };
    for (const k of ['toString', 'getPropertyPriority', 'getPropertyValue', 'item', 'removeProperty', 'setProperty']) {
      dummy1v[k] = ((k) => (function () { const style = this[sp7]; return style[k](...arguments); }))(k)
    }



    const dummy1p = phFn(dummy1v);
    const sp1v = new Proxy(m1.style, dummy1p);
    const sp2v = new Proxy(m2.style, dummy1p);
    Object.defineProperty(m1, 'style', { get() { return sp1v }, set() { }, enumerable: true, configurable: true });
    Object.defineProperty(m2, 'style', { get() { return sp2v }, set() { }, enumerable: true, configurable: true });
    m1.removeAttribute("style");
    m2.removeAttribute("style");

    let lastClick = 0;
    document.addEventListener('click', (evt) => {
      if (!evt.isTrusted) return;
      const target = ((evt || 0).target || 0)
      if (target.id === 'show-more') {
        if (target.nodeName !== 'YT-ICON-BUTTON') return;

        if (Date.now() - lastClick < 80) return;
        requestAnimationFrame(() => {
          lastClick = Date.now();
          target.click();
        })
      }

    })

    let btnShowMoreWR = null;


    const clickShowMore = () => {
      let btnShowMore = kRef(btnShowMoreWR);
      if (!btnShowMore || !btnShowMore.isConnected) {
        btnShowMore = document.querySelector('#show-more.yt-live-chat-item-list-renderer');
        btnShowMoreWR = mWeakRef(btnShowMore);
      }
      if (btnShowMore) btnShowMore.click();
    };

    let hasFirstShowMore = false;

    const visObserver = new IntersectionObserver((entries) => {

      for (const entry of entries) {

        const target = entry.target;
        if (!target) continue;
        let isVisible = entry.isIntersecting === true && entry.intersectionRatio > 0.5;
        if (isVisible) {
          target.style.setProperty('--wsr94', entry.boundingClientRect.height + 'px');
          target.setAttribute('wSr93', 'visible');
          if (nNextElem(target) === null) {
            canSetMaxScrollTop = true;
            if (dateNow() - lastScroll < 80) {
              lastLShow = 0;
              lastScroll = 0;
              Promise.resolve().then(clickShowMore);
            } else {
              lastLShow = dateNow();
            }
          } else if (!hasFirstShowMore) { // should more than one item being visible
            // implement inside visObserver to ensure there is sufficient delay
            hasFirstShowMore = true;
            requestAnimationFrame(() => {
              // foreground page
              activeDeferredAppendChild = true;
              // page visibly ready -> load the latest comments at initial loading
              clickShowMore();
            });
          }
        }
        else if (target.getAttribute('wSr93') === 'visible') { // ignore target.getAttribute('wSr93') === '' to avoid wrong sizing

          target.style.setProperty('--wsr94', entry.boundingClientRect.height + 'px');
          target.setAttribute('wSr93', 'hidden');
        }

      }

    }, {
      /*
  root: items,
  rootMargin: "0px",
  threshold: 1.0,
  */
      root: document.querySelector('#item-scroller'), // nullable
      rootMargin: "0px",
      threshold: [0.05, 0.95],
    });

    //m2.style.visibility='';

    const mutFn = (items) => {
      let node = nLastElem(items);
      for (; node !== null; node = nPrevElem(node)) {
        if (node.hasAttribute('wSr93')) break;
        node.setAttribute('wSr93', '');
        visObserver.observe(node);
      }
    }

    const mutObserver = new MutationObserver((mutations) => {
      const items = (mutations[0] || 0).target;
      if (!items) return;
      mutFn(items);
    });
    mutObserver.observe(m2, {
      childList: true,
      subtree: false
    });
    mutFn(m2);


    /** @type {HTMLElement} */
    let c1 = nPrevElem(m1);
    if (c1 && c1.id === "live-chat-banner") {
      let rsObserver = new ResizeObserver((entries) => {

        for (const entry of entries) {
          const target = entry.target;
          if (target && target.id === "live-chat-banner") {
            let p = entry.borderBoxSize ? (entry.borderBoxSize[0] || 0).blockSize : 0;
            let c1h = p > entry.contentRect.height ? p : entry.contentRect.height + 16;
            document.documentElement.style.setProperty('--items-top-padding', (Math.ceil(c1h / 2) * 2) + 'px');
          }
        }

      });
      rsObserver.observe(c1);
    }

    let maxScrollTop = -1;
    let canSetMaxScrollTop = false;
    document.addEventListener('scroll', (evt) => {

      if (!evt || !evt.isTrusted) return;
      if (!canSetMaxScrollTop) return;

      const scrollTop = evt.target.scrollTop;
      if (scrollTop >= maxScrollTop) { // due to wheel or autofeed
        maxScrollTop = scrollTop;

        if (dateNow() - lastLShow < 80) {
          lastLShow = 0;
          lastScroll = 0;
          Promise.resolve().then(clickShowMore);
        } else {
          lastScroll = dateNow();
        }
      } else { // wheel up
        lastScroll = 0;
      }



    }, { passive: true, capture: true }) // support contain => support passive

  };



  function onReady() {
    let tmObserver = new MutationObserver(() => {

      let p = document.getElementById('items');
      if (!p) return;
      let q = document.querySelector('#item-offset.style-scope.yt-live-chat-item-list-renderer > #items.style-scope.yt-live-chat-item-list-renderer');

      if (q) {
        tmObserver.disconnect();
        tmObserver.takeRecords();
        tmObserver = null;
        Promise.resolve(q).then((q) => {
          // confirm Promis.resolve() is resolveable
          // execute main without direct blocking
          main(q);
        })
      }

    });

    tmObserver.observe(document.body, {
      childList: true,
      subtree: true
    });

  }



  if (document.readyState != 'loading') {
    onReady();
  } else {
    window.addEventListener("DOMContentLoaded", onReady, false);
  }


})({ Promise, requestAnimationFrame });